#!/bin/sh
set -eu

LC_ALL=C
export LC_ALL
unset SOURCE_HIGHLIGHT_DATADIR

root=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
articles_dir=$root/articles
outlang=$root/tools/html-fragment.outlang
theme_menu=$root/tools/theme-menu.html
styles_file=$root/styles.css
license_file=${LICENSE_FILE:-$root/tools/license.txt}
site_title=${SITE_TITLE:-arachnopress}
site_mark=${SITE_MARK:-䷖}
site_mark_color=${SITE_MARK_COLOR:-#2aa198}
generator_name=${GENERATOR_NAME:-arachnopress}
generator_version=${GENERATOR_VERSION:-1.0.19}
build_date=$(date -u '+%Y-%m-%d %H:%M UTC')
build_day=$(date -u '+%Y-%m-%d')
copyright_year=$(date -u '+%Y')
build_user=$(id -un 2>/dev/null || printf unknown)
build_host=$(uname -n 2>/dev/null || printf localhost)
copyright_holder=${COPYRIGHT_HOLDER:-$build_user@$build_host}
default_theme=${DEFAULT_THEME:-solarized-dark}
article_order=${ARTICLE_ORDER:-title}
highlighter=${HIGHLIGHTER:-pygments}
code_footer_lines=${CODE_FOOTER_LINES:-23}
pygmentize=$(command -v pygmentize 2>/dev/null || :)
source_highlight=$(command -v source-highlight 2>/dev/null || :)
sha256_bin=$(command -v sha256 2>/dev/null || :)
sha256sum_bin=$(command -v sha256sum 2>/dev/null || :)
shasum_bin=$(command -v shasum 2>/dev/null || :)
openssl_bin=$(command -v openssl 2>/dev/null || :)
tmp=$(mktemp -d "${TMPDIR:-/tmp}/arachnopress-build.XXXXXXXXXX")
output_tmp=
nav_tmp=
attrs_file=$tmp/attrs.txt
tab=$(printf '\t')
block_index=0
highlighted_blocks=0
raw_code_blocks=0

cleanup() {
	rm -rf "$tmp"
	[ -z "$output_tmp" ] || rm -f "$output_tmp"
	[ -z "$nav_tmp" ] || rm -f "$nav_tmp"
}

on_signal() {
	trap - HUP INT TERM
	exit 1
}

trap cleanup EXIT
trap on_signal HUP INT TERM

die() {
	printf '%s\n' "$*" >&2
	exit 1
}

html_escape() {
	awk '{
		gsub(/&/, "\\&amp;")
		gsub(/</, "\\&lt;")
		gsub(/>/, "\\&gt;")
		print
	}' "$@"
}

html_escape_attr() {
	printf '%s\n' "$1" | awk '{
		gsub(/&/, "\\&amp;")
		gsub(/</, "\\&lt;")
		gsub(/>/, "\\&gt;")
		gsub(/"/, "\\&quot;")
		printf "%s", $0
	}'
}

sanitize_identifier() {
	printf '%s' "$1" |
		tr -c 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-' '_' |
		sed -e 's/__*/_/g' -e 's/^_*//' -e 's/_*$//'
}

url_escape() {
	od -A n -t x1 -v | awk '{
		for (i = 1; i <= NF; i++)
			printf "%%%s", $i
	}'
}

favicon_href() {
	printf 'data:image/svg+xml,'
	{
		printf '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">'
		printf '<text x="50" y="76" text-anchor="middle" font-size="82" fill="'
		html_escape_attr "$site_mark_color"
		printf '">'
		html_escape_attr "$1"
		printf '</text></svg>'
	} |
		url_escape
}

license_escape() {
	env ARACHNOPRESS_COPYRIGHT_HOLDER="$copyright_holder" \
	awk -v year="$copyright_year" '
		function replace(s, needle, value,   p, out) {
			out = ""
			while (p = index(s, needle)) {
				out = out substr(s, 1, p - 1) value
				s = substr(s, p + length(needle))
			}
			return out s
		}
		BEGIN {
			holder = ENVIRON["ARACHNOPRESS_COPYRIGHT_HOLDER"]
		}
		{
			line = replace($0, "<YEAR>", year)
			line = replace(line, "<COPYRIGHT_HOLDER>", holder)
			gsub(/&/, "\\&amp;", line)
			gsub(/</, "\\&lt;", line)
			gsub(/>/, "\\&gt;", line)
			print line
		}
	' "$license_file"
}

attr() {
	name=$1
	printf '%s\n' "$2" |
	awk -v name="$name" '
		{
			line = $0
			p = index(line, " " name "=\"")
			if (!p)
				p = index(line, "\t" name "=\"")
			if (!p)
				exit
			s = substr(line, p + length(name) + 3)
			q = index(s, "\"")
			if (q)
				print substr(s, 1, q - 1)
			exit
		}
	'
}

write_attrs() {
	line=$1
	out=$2
	shift 2
	names=$*

	printf '%s\n' "$line" |
	awk -v names="$names" '
		function decode(s) {
			gsub(/&quot;|&#34;/, "\"", s)
			gsub(/&apos;|&#39;/, "\047", s)
			gsub(/&lt;|&#60;/, "<", s)
			gsub(/&gt;|&#62;/, ">", s)
			gsub(/&amp;|&#38;/, "\\&", s)
			return s
		}
		function attr(field,   p, s, q) {
			p = index(line, " " field "=\"")
			if (!p)
				p = index(line, "\t" field "=\"")
			if (!p)
				return ""
			s = substr(line, p + length(field) + 3)
			q = index(s, "\"")
			if (!q)
				return ""
			return decode(substr(s, 1, q - 1))
		}
		{
			line = $0
			count = split(names, fields, /[[:space:]][[:space:]]*/)
			for (i = 1; i <= count; i++)
				print attr(fields[i])
			exit
		}
	' > "$out"
}

normalize_lang() {
	case $1 in
		'') printf '' ;;
		auto|guess) printf 'auto' ;;
		text|txt|plain|none|nohilite) printf 'text' ;;
		patch|udiff) printf 'diff' ;;
		makefile|mf) printf 'make' ;;
		*) printf '%s' "$1" ;;
	esac
}

details_open_attr() {
	case $1 in
		0|false|no|closed|collapsed) printf '' ;;
		*) printf ' open' ;;
	esac
}

show_footer() {
	case $1 in
		0|false|no|off|hidden|hide) return 1 ;;
		*) return 0 ;;
	esac
}

show_header() {
	case $1 in
		0|false|no|off|hidden|hide) return 1 ;;
		*) return 0 ;;
	esac
}

show_explicit_footer() {
	case $1 in
		1|true|yes|on|shown|show) return 0 ;;
		*) return 1 ;;
	esac
}

file_line_count_le() {
	awk -v limit="$2" 'END { exit !(NR <= limit) }' "$1"
}

next_block_id() {
	while :; do
		block_index=$((block_index + 1))
		block_id=block-$block_index
		block_footer_id=$block_id-footer
		if ! grep -F -x -q -e "$block_id" -e "$block_footer_id" "$anchor_ids"; then
			return
		fi
	done
}

format_size() {
	bytes=$(wc -c < "$1")
	awk -v bytes="$bytes" '
		BEGIN {
			split("B KiB MiB GiB TiB", unit)
			size = bytes + 0
			i = 1
			while (size >= 1024 && i < 5) {
				size /= 1024
				i++
			}
			if (i == 1)
				printf "%d %s", size, unit[i]
			else if (size < 10)
				printf "%.1f %s", size, unit[i]
			else
				printf "%.0f %s", size, unit[i]
		}
	'
}

print_sha256() {
	printf '%s\n' "$1" |
	awk '
		length($1) == 64 && $1 ~ /^[0-9A-Fa-f]+$/ {
			print $1
			found = 1
		}
		END { exit found ? 0 : 1 }
	'
}

file_sha256() {
	file=$1
	hash=

	if [ -n "$sha256_bin" ]; then
		hash=$("$sha256_bin" -q "$file" 2>/dev/null) &&
			print_sha256 "$hash" && return 0
	fi
	if [ -n "$sha256sum_bin" ]; then
		hash=$("$sha256sum_bin" "$file" 2>/dev/null) &&
			print_sha256 "$hash" && return 0
	fi
	if [ -n "$shasum_bin" ]; then
		hash=$("$shasum_bin" -a 256 "$file" 2>/dev/null) &&
			print_sha256 "$hash" && return 0
	fi
	if [ -n "$openssl_bin" ]; then
		hash=$("$openssl_bin" dgst -sha256 -r "$file" 2>/dev/null) &&
			print_sha256 "$hash" && return 0
	fi

	return 1
}

safe_name() {
	case $1 in
		''|/*|*/*|..|*[!A-Za-z0-9._-]*) return 1 ;;
	esac
}

reserve_anchor() {
	id=$1
	context=$2

	safe_name "$id" || die "Unsafe HTML id in $context: $id"
	if grep -F -x -q -e "$id" "$anchor_ids"; then
		die "Duplicate HTML id: $id"
	fi
	printf '%s\n' "$id" >> "$anchor_ids"
}

safe_file_path() {
	dir=$1
	src=$2
	rest=$src
	path=

	case $src in
		''|/*|*//*) return 1 ;;
	esac

	while :; do
		case $rest in
			*/*)
				part=${rest%%/*}
				rest=${rest#*/}
				;;
			*)
				part=$rest
				rest=
				;;
		esac

		case $part in
			''|.|..|*[!A-Za-z0-9._-]*) return 1 ;;
		esac

		path=${path:+$path/}$part
		[ ! -L "$dir/$path" ] || return 1
		[ -n "$rest" ] || break
	done
}

safe_lang() {
	case $1 in
		-*|*[!A-Za-z0-9_+-]*) return 1 ;;
	esac
}

safe_color() {
	case $1 in
		\#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]|\#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]) return 0 ;;
	esac

	return 1
}

safe_date() {
	case $1 in
		*"$tab"*) return 1 ;;
		[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]*) return 0 ;;
	esac

	return 1
}

normalize_theme() {
	case $1 in
		theme-*) printf '%s' "$1" ;;
		*) printf 'theme-%s' "$1" ;;
	esac
}

sort_articles() {
	case $1 in
		title) sort -f -t "$tab" -k 2,2 -k 1,1 "$article_meta" ;;
		modified) sort -t "$tab" -k 3,3r -k 1,1 "$article_meta" ;;
		created) sort -t "$tab" -k 4,4r -k 1,1 "$article_meta" ;;
	esac
}

slugify() {
	printf '%s\n' "$1" |
		tr '[:upper:]' '[:lower:]' |
		sed -e 's/[^a-z0-9][^a-z0-9]*/-/g' \
		    -e 's/^-//' \
		    -e 's/-$//' \
		    -e 's/^$/section/'
}

heading_text() {
	tag=$1
	printf '%s\n' "$2" |
	awk -v tag="$tag" '
		function decode(s) {
			gsub(/&quot;|&#34;/, "\"", s)
			gsub(/&apos;|&#39;/, "\047", s)
			gsub(/&lt;|&#60;/, "<", s)
			gsub(/&gt;|&#62;/, ">", s)
			gsub(/&amp;|&#38;/, "\\&", s)
			return s
		}
		{
			line = $0
			start = index(line, "<" tag)
			end = index(line, "</" tag ">")
			if (!start || !end || end < start)
				exit
			head = substr(line, start, end - start)
			for (i = 1; i <= length(head); i++)
				if (substr(head, i, 1) == ">") {
					close_pos = i
					break
				}
			if (!close_pos)
				exit
			text = substr(head, close_pos + 1)
			if (text !~ /</)
				print decode(text)
			exit
		}
	'
}

unique_auto_anchor() {
	base=$1
	id=$base
	n=1

	while grep -F -x -q -e "$id" "$anchor_ids"; do
		n=$((n + 1))
		id=$base-$n
	done

	printf '%s' "$id"
}

marker_empty() {
	line=$1
	tag=$2

	printf '%s\n' "$line" |
	awk -v tag="$tag" -v endtag="</$tag>" '
		{
			start = index($0, "<" tag)
			if (!start)
				exit 1
			prefix = substr($0, 1, start - 1)
			if (prefix !~ /^[[:space:]]*$/)
				exit 1
			pos = index($0, endtag)
			if (!pos || pos < start)
				exit 1
			before = substr($0, start, pos - start)
			after = substr($0, pos + length(endtag))
			open_end = 0
			for (i = 1; i <= length(before); i++)
				if (substr(before, i, 1) == ">")
					open_end = i
			if (!open_end)
				exit 1
			body = substr(before, open_end + 1)
			if (body !~ /^[[:space:]]*$/ || after !~ /^[[:space:]]*$/)
				exit 1
		}
	'
}

require_empty_marker() {
	source=$1
	line=$2
	tag=$3
	name=$4

	marker_empty "$line" "$tag" ||
		die "$name marker must be empty and contained on one line: $source"
}

read_sections() {
	source=$1
	slug=$2
	heading_meta=$tmp/headings-$slug.tsv
	section_ids=$tmp/section-ids-$slug.txt
	subsection_meta=$tmp/subsections-$slug.tsv
	subsection_parents=$tmp/subsection-parents-$slug.txt
	: > "$heading_meta"
	: > "$section_ids"
	: > "$subsection_meta"
	: > "$subsection_parents"
	current_section=

	while IFS= read -r line || [ -n "$line" ]; do
		case $line in
			*'<h2'*)
				heading_tag=h2
				closing='</h2>'
				;;
			*'<h3'*)
				heading_tag=h3
				closing='</h3>'
				;;
			*) continue ;;
		esac
		case $line in
			*"$closing"*) ;;
			*) die "Indexable $heading_tag must be plain one-line text: $source" ;;
		esac

		heading_title=$(heading_text "$heading_tag" "$line")
		[ -n "$heading_title" ] ||
			die "Indexable $heading_tag must contain plain one-line text: $source"
		case $heading_title in
			*"$tab"*) die "Heading title may not contain tabs: $source" ;;
		esac

		heading_id=$(attr id "$line")
		if [ -n "$heading_id" ]; then
			safe_name "$heading_id" ||
				die "Unsafe $heading_tag id in $source: $heading_id"
		else
			case $heading_tag in
				h2) anchor_base=$slug-$(slugify "$heading_title") ;;
				h3)
					[ -n "$current_section" ] ||
						die "Indexable h3 must follow an h2: $source"
					anchor_base=$current_section-$(slugify "$heading_title")
					;;
			esac
			heading_id=$(unique_auto_anchor "$anchor_base")
		fi
		reserve_anchor "$heading_id" "$source"

		case $heading_tag in
			h2)
				current_section=$heading_id
				parent_id=
				printf '%s\n' "$heading_id" >> "$section_ids"
				;;
			h3)
				[ -n "$current_section" ] ||
					die "Indexable h3 must follow an h2: $source"
				parent_id=$current_section
				printf '%s\t%s\n' "$parent_id" "$heading_id" >> "$subsection_meta"
				if ! grep -F -x -q -e "$parent_id" "$subsection_parents"; then
					printf '%s\n' "$parent_id" >> "$subsection_parents"
				fi
				subsection_count=$((subsection_count + 1))
				;;
		esac
		printf '%s\t%s\t%s\t%s\n' \
			"$heading_tag" "$heading_id" "$heading_title" "$parent_id" >> "$heading_meta"
	done < "$source"
}

write_theme_menu() {
	awk -v theme_id="$theme_id" '
		/type="radio"/ && /name="theme"/ {
			gsub(/[[:space:]]checked/, "")
			if (index($0, "id=\"" theme_id "\""))
				sub(/>$/, " checked>")
		}
		{ print }
	' "$theme_menu"
}

optimize_highlighted_html() {
	sed \
		-e 's/<span class="w">\([[:space:]]*\)<\/span>/\1/g' \
		-e 's/<span class="sh-normal">\([[:space:]]*\)<\/span>/\1/g' \
		"$1"
}

highlight_code() {
	code_file=$1
	lang=$2
	highlight_output=$3
	unoptimized_file=$tmp/code-unoptimized.html

	[ "$lang" != text ] || return 1
	rm -f "$unoptimized_file"

	case $highlighter in
		pygments)
			highlight_pygments "$code_file" "$lang" "$unoptimized_file" || return 1
			;;
		source-highlight)
			highlight_source "$code_file" "$lang" "$unoptimized_file" || return 1
			;;
		none) return 1 ;;
	esac

	optimize_highlighted_html "$unoptimized_file" > "$highlight_output" || return 1
}

highlight_pygments() {
	code_file=$1
	lang=$2
	output_file=$3

	[ -n "$pygmentize" ] || return 1

	case $lang in
		'')
			"$pygmentize" -f html -O nowrap "$code_file" > "$output_file"
			;;
		auto)
			auto_lexer=$("$pygmentize" -N "$code_file" 2>/dev/null || printf text)
			case $auto_lexer in
				''|text)
					"$pygmentize" -f html -O nowrap -g "$code_file" > "$output_file"
					;;
				*)
					if safe_lang "$auto_lexer"; then
						"$pygmentize" -f html -O nowrap -l "$auto_lexer" "$code_file" > "$output_file"
					else
						"$pygmentize" -f html -O nowrap -g "$code_file" > "$output_file"
					fi
					;;
			esac
			;;
		*)
			"$pygmentize" -f html -O nowrap -l "$lang" "$code_file" > "$output_file"
			;;
	esac
}

highlight_source() {
	code_file=$1
	lang=$2
	output_file=$3

	[ -n "$source_highlight" ] && [ -f "$outlang" ] || return 1

	case $lang in
		''|auto)
			"$source_highlight" --no-doc --outlang-def="$outlang" \
				--input="$code_file" --output=STDOUT > "$output_file"
			;;
		make)
			"$source_highlight" --no-doc --outlang-def="$outlang" \
				--src-lang=makefile \
				--input="$code_file" --output=STDOUT > "$output_file"
			;;
		*)
			"$source_highlight" --no-doc --outlang-def="$outlang" \
				--src-lang="$lang" \
				--input="$code_file" --output=STDOUT > "$output_file"
			;;
	esac
}

render_missing_block() {
	message=$1
	title=$2
	note=$3
	header=$4

	if show_header "$header"; then
		printf '<details class="code-details" open>\n'
		render_block_summary "$title" "$note" '' '' ''
		class='code-block code-block-missing'
	else
		class='code-block code-block-missing code-block-standalone'
	fi
	printf '  <pre class="%s"><code>' "$class"
	printf '%s\n' "$message" | html_escape
	printf '</code></pre>\n'
	if show_header "$header"; then
		printf '</details>\n'
	fi
}

render_block_label() {
	title_html=$(html_escape_attr "$1")
	note_html=$(html_escape_attr "$2")

	printf '<span class="block-label"><code class="block-title">%s</code>' "$title_html"
	if [ -n "$2" ]; then
		printf '<span class="block-note">%s</span>' "$note_html"
	fi
	printf '</span>'
}

render_block_summary() {
	title=$1
	note=$2
	href=$3
	id=$4
	bottom=$5

	if [ -n "$id" ]; then
		printf '  <summary id="%s">' "$id"
	else
		printf '  <summary>'
	fi
	render_block_label "$title" "$note"
	if [ -n "$href" ]; then
		printf '<a class="block-action code-download" href="%s" download>raw</a>' "$href"
	fi
	if [ -n "$bottom" ]; then
		printf '<a class="block-action block-jump" href="#%s" aria-label="Bottom">↓</a>' "$bottom"
	fi
	printf '</summary>\n'
}

render_block_footer() {
	title=$1
	size=$2
	href=$3
	top=$4
	id=$5

	if [ -n "$id" ]; then
		printf '  <footer class="block-footer" id="%s">' "$id"
	else
		printf '  <footer class="block-footer">'
	fi
	render_block_label "$title" ''
	printf '<span class="block-size">%s</span>' "$size"
	if [ -n "$href" ]; then
		printf '<a class="block-action block-footer-link" href="%s" download>raw</a>' "$href"
	fi
	if [ -n "$top" ]; then
		printf '<a class="block-action block-footer-link block-jump" href="#%s" aria-label="Top">↑</a>' "$top"
	fi
	printf '</footer>\n'
}

render_code_block() {
	dir=$1
	src=$2
	lang=$(normalize_lang "$3")
	open_attr=$(details_open_attr "$4")
	title=$5
	note=$6
	footer=$7
	header=$8

	[ -n "$title" ] || title=$src
	if ! safe_file_path "$dir" "$src"; then
		render_missing_block "Unsafe code source path: $src" "$title" "$note" "$header"
		return
	fi
	if ! safe_lang "$lang"; then
		render_missing_block "Unsafe code language: $lang" "$title" "$note" "$header"
		return
	fi

	code_file=$dir/$src
	title_html=$(html_escape_attr "$title")
	raw_href=articles/${dir##*/}/$src
	lang_attr=
	highlight_tmp=$tmp/code.html

	[ -n "$lang" ] && lang_attr=" data-lang=\"$lang\""

	if [ ! -f "$code_file" ]; then
		render_missing_block "Missing code source: $src" "$title" "$note" "$header"
		return
	fi
	if show_header "$header"; then
		want_header=1
		if [ -z "$footer" ] &&
		    file_line_count_le "$code_file" "$code_footer_lines"; then
			footer=false
		fi
		if show_footer "$footer"; then
			want_footer=1
		else
			want_footer=0
		fi
	else
		want_header=0
		if show_explicit_footer "$footer"; then
			want_footer=1
		else
			want_footer=0
		fi
	fi

	if [ "$want_header" = 1 ]; then
		next_block_id
	fi
	if [ "$want_footer" = 1 ]; then
		size=$(format_size "$code_file")
	fi
	rm -f "$highlight_tmp"
	case $highlighter in
		none)
			raw_code_blocks=$((raw_code_blocks + 1))
			;;
		*)
			if highlight_code "$code_file" "$lang" "$highlight_tmp" 2>/dev/null &&
			   { [ ! -s "$code_file" ] || [ -s "$highlight_tmp" ]; }; then
				highlighted_blocks=$((highlighted_blocks + 1))
			else
				rm -f "$highlight_tmp"
				raw_code_blocks=$((raw_code_blocks + 1))
			fi
			;;
	esac

	if [ "$want_header" = 1 ]; then
		printf '<details class="code-details"%s>\n' "$open_attr"
		if [ "$want_footer" = 1 ]; then
			render_block_summary "$title" "$note" "$raw_href" "$block_id" "$block_id-footer"
		else
			render_block_summary "$title" "$note" "$raw_href" "$block_id" ''
		fi
		block_class=code-block
	elif [ "$want_footer" = 1 ]; then
		printf '<div class="block-standalone">\n'
		block_class=code-block
	else
		block_class='code-block code-block-standalone'
	fi
	printf '  <pre class="%s" data-src="%s" data-title="%s"%s><code>' \
		"$block_class" "$src" "$title_html" "$lang_attr"
	if [ -f "$highlight_tmp" ]; then
		cat "$highlight_tmp"
	else
		html_escape "$code_file"
	fi
	printf '</code></pre>\n'
	if [ "$want_footer" = 1 ]; then
		if [ "$want_header" = 1 ]; then
			render_block_footer "$title" "$size" "$raw_href" "$block_id" "$block_id-footer"
		else
			render_block_footer "$title" "$size" "$raw_href" '' ''
		fi
	fi
	if [ "$want_header" = 1 ]; then
		printf '</details>\n'
	elif [ "$want_footer" = 1 ]; then
		printf '</div>\n'
	fi
}

render_missing_image() {
	message=$1
	title=$2
	note=$3
	header=$4

	if show_header "$header"; then
		printf '<details class="media-details" open>\n'
		render_block_summary "$title" "$note" '' '' ''
		class='image-block image-block-missing'
	else
		class='image-block image-block-missing image-block-standalone'
	fi
	printf '  <figure class="%s">\n' "$class"
	printf '    <p>'
	printf '%s\n' "$message" | html_escape
	printf '</p>\n'
	printf '  </figure>\n'
	if show_header "$header"; then
		printf '</details>\n'
	fi
}

render_image_block() {
	dir=$1
	src=$2
	alt=$3
	caption=$4
	open_attr=$(details_open_attr "$5")
	title=$6
	note=$7
	footer=$8
	header=$9

	[ -n "$title" ] || title=$src
	if ! safe_file_path "$dir" "$src"; then
		render_missing_image "Unsafe image source path: $src" "$title" "$note" "$header"
		return
	fi

	image_file=$dir/$src
	image_href=articles/${dir##*/}/$src
	alt_html=$(html_escape_attr "$alt")
	caption_html=$(html_escape_attr "$caption")

	if [ ! -f "$image_file" ]; then
		render_missing_image "Missing image source: $src" "$title" "$note" "$header"
		return
	fi

	if show_header "$header"; then
		want_header=1
		next_block_id
		if show_footer "$footer"; then
			want_footer=1
		else
			want_footer=0
		fi
	else
		want_header=0
		if show_explicit_footer "$footer"; then
			want_footer=1
		else
			want_footer=0
		fi
	fi
	if [ "$want_footer" = 1 ]; then
		size=$(format_size "$image_file")
	fi
	if [ "$want_header" = 1 ]; then
		printf '<details class="media-details"%s>\n' "$open_attr"
		if [ "$want_footer" = 1 ]; then
			render_block_summary "$title" "$note" "$image_href" "$block_id" "$block_id-footer"
		else
			render_block_summary "$title" "$note" "$image_href" "$block_id" ''
		fi
		block_class=image-block
	elif [ "$want_footer" = 1 ]; then
		printf '<div class="block-standalone">\n'
		block_class=image-block
	else
		block_class='image-block image-block-standalone'
	fi
	printf '  <figure class="%s">\n' "$block_class"
	printf '    <img src="%s" alt="%s" loading="lazy" decoding="async">\n' "$image_href" "$alt_html"
	if [ -n "$caption" ]; then
		printf '    <figcaption>%s</figcaption>\n' "$caption_html"
	fi
	printf '  </figure>\n'
	if [ "$want_footer" = 1 ]; then
		if [ "$want_header" = 1 ]; then
			render_block_footer "$title" "$size" "$image_href" "$block_id" "$block_id-footer"
		else
			render_block_footer "$title" "$size" "$image_href" '' ''
		fi
	fi
	if [ "$want_header" = 1 ]; then
		printf '</details>\n'
	elif [ "$want_footer" = 1 ]; then
		printf '</div>\n'
	fi
}

render_download() {
	dir=$1
	src=$2
	title=$3
	note=$4

	[ -n "$title" ] || title=$src
	title_html=$(html_escape_attr "$title")
	note_html=$(html_escape_attr "$note")
	href=articles/${dir##*/}/$src
	file=$dir/$src
	printf '<div class="article-downloads">\n'
	if safe_file_path "$dir" "$src" && [ -f "$file" ]; then
		size=$(format_size "$file")
		checksum=$(file_sha256 "$file" || :)
		printf '  <div class="article-download">'
		printf '<a class="article-download-hit" href="%s" download aria-label="Download %s"></a>' \
			"$href" "$title_html"
		printf '<span class="article-download-kind">Download</span>'
		render_block_label "$title" "$note"
		printf '<span class="block-size">%s</span>' "$size"
		if [ -n "$checksum" ]; then
			printf '<span class="block-checksum">SHA256 (%s) = %s</span>' \
				"$src" "$checksum"
		fi
		printf '</div>\n'
	else
		printf '  <span class="article-download article-download-missing">'
		printf '<span class="article-download-kind">Missing</span>'
		printf '<span class="block-label"><code class="block-title">%s</code>' "$title_html"
		if [ -n "$note" ]; then
			printf '<span class="block-note">%s</span>' "$note_html"
		fi
		printf '</span></span>\n'
	fi
	printf '</div>\n'
}

render_article() {
	dir=$1
	source=$2
	slug=${dir##*/}
	heading_meta=$tmp/headings-$slug.tsv

	exec 3< "$heading_meta"
	while IFS= read -r line || [ -n "$line" ]; do
		case $line in
			*'<div'*'class="article-downloads"'*)
				require_empty_marker "$source" "$line" div "Download"
				case $line in
					*'data-downloads='*)
						die "Download marker uses obsolete data-downloads; use one data-src per download: $source"
						;;
				esac
				write_attrs "$line" "$attrs_file" \
					data-src data-title data-note
				{
					IFS= read -r data_src
					IFS= read -r data_title
					IFS= read -r data_note
				} < "$attrs_file"
				render_download "$dir" "$data_src" "$data_title" "$data_note"
				;;
			*'<pre'*'class="code-block"'*)
				require_empty_marker "$source" "$line" pre "Code block"
				write_attrs "$line" "$attrs_file" \
					data-src data-lang data-open data-title data-note data-footer data-header
				{
					IFS= read -r data_src
					IFS= read -r data_lang
					IFS= read -r data_open
					IFS= read -r data_title
					IFS= read -r data_note
					IFS= read -r data_footer
					IFS= read -r data_header
				} < "$attrs_file"
				render_code_block "$dir" "$data_src" "$data_lang" \
					"$data_open" "$data_title" "$data_note" "$data_footer" "$data_header"
				;;
			*'<figure'*'class="image-block"'*)
				require_empty_marker "$source" "$line" figure "Image"
				write_attrs "$line" "$attrs_file" \
					data-src data-alt data-caption data-open data-title data-note data-footer data-header
				{
					IFS= read -r data_src
					IFS= read -r data_alt
					IFS= read -r data_caption
					IFS= read -r data_open
					IFS= read -r data_title
					IFS= read -r data_note
					IFS= read -r data_footer
					IFS= read -r data_header
				} < "$attrs_file"
				render_image_block "$dir" "$data_src" "$data_alt" \
					"$data_caption" "$data_open" "$data_title" "$data_note" "$data_footer" "$data_header"
				;;
			*'<h2'*'</h2>'*|*'<h3'*'</h3>'*)
				case $line in
					*'<h2'*) heading_tag=h2 ;;
					*) heading_tag=h3 ;;
				esac
				IFS="$tab" read -r indexed_tag heading_id heading_title parent_id <&3 ||
					die "Heading index mismatch while rendering $source"
				[ "$indexed_tag" = "$heading_tag" ] ||
					die "Heading index mismatch while rendering $source"
				if [ -z "$(attr id "$line")" ]; then
					printf '%s\n' "$line" |
						sed "s/<$heading_tag/<$heading_tag id=\"$heading_id\"/"
				else
					printf '%s\n' "$line"
				fi
				;;
			*)
				printf '%s\n' "$line"
			;;
		esac
	done < "$source"
	if IFS="$tab" read -r indexed_tag heading_id heading_title parent_id <&3; then
		die "Heading index mismatch while rendering $source"
	fi
	exec 3<&-
}

write_order_input() {
	order=$1
	label=$2
	checked=

	[ "$article_order" = "$order" ] && checked=' checked'

	printf '        <input class="index-order-input" type="radio" '
	printf 'name="index-order" id="index-order-%s" aria-label="Sort by %s"%s>\n' \
		"$order" "$label" "$checked"
}

write_order_label() {
	order=$1
	label=$2
	text=$3

	printf '            <label for="index-order-%s" title="Sort by %s">%s</label>\n' \
		"$order" "$label" "$text"
}

write_nav_list() {
	order=$1
	file=$2

	printf '        <ol class="index-list index-list-%s">\n' "$order"
	while IFS="$tab" read -r slug title modified created; do
		slug_html=$(html_escape_attr "$slug")
		printf '          <li class="index-item" data-article="%s">\n' "$slug_html"
		printf '            <a class="index-article" href="#%s">%s</a>\n' \
			"$slug_html" "$(html_escape_attr "$title")"
		printf '          </li>\n'
	done < "$file"
	printf '        </ol>\n'
}

write_article_section_list() {
	slug=$1
	title=$2
	heading_meta=$3
	slug_html=$(html_escape_attr "$slug")
	title_html=$(html_escape_attr "$title")
	section_open=0
	subsection_open=0

	printf '          <ol class="index-section-list" data-article="%s" aria-label="Sections in %s">\n' \
		"$slug_html" "$title_html"
	while IFS="$tab" read -r heading_tag heading_id heading_title parent_id; do
		heading_id_html=$(html_escape_attr "$heading_id")
		heading_title_html=$(html_escape_attr "$heading_title")
		case $heading_tag in
			h2)
				if [ "$subsection_open" = 1 ]; then
					printf '                </ol>\n'
					subsection_open=0
				fi
				[ "$section_open" = 0 ] || printf '              </li>\n'
				printf '              <li class="index-section-item" data-section="%s">\n' \
					"$heading_id_html"
				printf '                <a href="#%s">%s</a>\n' \
					"$heading_id_html" "$heading_title_html"
				section_title_html=$heading_title_html
				section_open=1
				;;
			h3)
				if [ "$subsection_open" = 0 ]; then
					printf '                <ol class="index-subsection-list" data-section="%s" aria-label="Subsections under %s">\n' \
						"$(html_escape_attr "$parent_id")" "$section_title_html"
					subsection_open=1
				fi
				printf '                  <li><a href="#%s">%s</a></li>\n' \
					"$heading_id_html" "$heading_title_html"
				;;
		esac
	done < "$heading_meta"
	[ "$subsection_open" = 0 ] || printf '                </ol>\n'
	[ "$section_open" = 0 ] || printf '              </li>\n'
	printf '          </ol>\n'
}

write_article_subsection_lists() {
	slug=$1
	heading_meta=$2
	slug_html=$(html_escape_attr "$slug")
	current_section=
	subsection_open=0

	while IFS="$tab" read -r heading_tag heading_id heading_title parent_id; do
		case $heading_tag in
			h2)
				[ "$subsection_open" = 0 ] || printf '          </ol>\n'
				current_section=$heading_id
				section_title=$heading_title
				subsection_open=0
				;;
			h3)
				if [ "$subsection_open" = 0 ]; then
					printf '          <ol class="index-subsection-list" data-article="%s" data-section="%s" aria-label="Subsections under %s">\n' \
						"$slug_html" "$(html_escape_attr "$current_section")" \
						"$(html_escape_attr "$section_title")"
					subsection_open=1
				fi
				printf '            <li><a href="#%s">%s</a></li>\n' \
					"$(html_escape_attr "$heading_id")" \
					"$(html_escape_attr "$heading_title")"
				;;
		esac
	done < "$heading_meta"
	[ "$subsection_open" = 0 ] || printf '          </ol>\n'
}

write_section_lists() {
	file=$1

	printf '        <div class="index-section-lists">\n'
	while IFS="$tab" read -r slug title modified created; do
		heading_meta=$tmp/headings-$slug.tsv
		[ -s "$heading_meta" ] || continue
		write_article_section_list "$slug" "$title" "$heading_meta"
	done < "$file"
	printf '        </div>\n'

	if [ "$subsection_count" -gt 0 ]; then
		printf '        <div class="index-subsection-lists">\n'
		while IFS="$tab" read -r slug title modified created; do
			subsection_meta=$tmp/subsections-$slug.tsv
			[ -s "$subsection_meta" ] || continue
			write_article_subsection_lists "$slug" "$tmp/headings-$slug.tsv"
		done < "$file"
		printf '        </div>\n'
	fi
}

write_index_selected_style() {
	printf '  background: var(--index-selected-bg);\n'
	printf '  border-color: var(--index-selected-border);\n'
	printf '  color: var(--index-selected-text);\n'
}

write_index_subsection_selected_style() {
	printf '  background: var(--index-subsection-selected-bg);\n'
	printf '  border-color: var(--index-subsection-selected-border);\n'
	printf '  color: var(--index-subsection-selected-text);\n'
}

write_portrait_subsection_css() {
	slug=$1
	section_id=$2
	target_id=$3

	printf 'body:has(.article[id="%s"] [id="%s"]:target) {\n' "$slug" "$target_id"
	printf '  --mobile-index-height: 12rem;\n'
	printf '}\n'
	printf 'body:has(.article[id="%s"] [id="%s"]:target) .index-pane {\n' "$slug" "$target_id"
	printf '  grid-template-rows: auto 2.625rem 2.75rem 2.75rem 1.125rem;\n'
	printf '}\n'
	printf 'body:has(.article[id="%s"] [id="%s"]:target) .index-footer {\n' "$slug" "$target_id"
	printf '  grid-row: 5;\n'
	printf '}\n'
	printf 'body:has(.article[id="%s"] [id="%s"]:target) .index-subsection-lists,\n' \
		"$slug" "$target_id"
	printf 'body:has(.article[id="%s"] [id="%s"]:target) .index-subsection-list[data-article="%s"][data-section="%s"] {\n' \
		"$slug" "$target_id" "$slug" "$section_id"
	printf '  display: flex;\n'
	printf '}\n'
}

write_section_nav_css() {
	printf '/* Generated by tools/build.sh. Do not edit by hand. */\n'
	if [ -s "$tmp/section-ids-$first_slug.txt" ]; then
		printf '.shell:not(:has(.article:target)):not(:has(.article :target)) .index-section-lists,\n'
		printf '.shell:not(:has(.article:target)):not(:has(.article :target)) .index-section-list[data-article="%s"] {\n' "$first_slug"
		printf '  display: flex;\n'
		printf '}\n'
	fi
	printf '.shell:not(:has(.article:target)):not(:has(.article :target)) .index-item[data-article="%s"] > .index-article {\n' "$first_slug"
	write_index_selected_style
	printf '}\n'
	while IFS="$tab" read -r slug title modified created; do
		printf 'body:has(.article[id="%s"]:target) .index-item[data-article="%s"] > .index-article,\n' "$slug" "$slug"
		printf 'body:has(.article[id="%s"] :target) .index-item[data-article="%s"] > .index-article {\n' "$slug" "$slug"
		write_index_selected_style
		printf '}\n'
		if [ -s "$tmp/section-ids-$slug.txt" ]; then
			printf 'body:has(.article[id="%s"]:target) .index-section-lists,\n' "$slug"
			printf 'body:has(.article[id="%s"] :target) .index-section-lists,\n' "$slug"
			printf 'body:has(.article[id="%s"]:target) .index-section-list[data-article="%s"],\n' "$slug" "$slug"
			printf 'body:has(.article[id="%s"] :target) .index-section-list[data-article="%s"] {\n' "$slug" "$slug"
			printf '  display: flex;\n'
			printf '}\n'
			while IFS= read -r section_id; do
				printf 'body:has(.article[id="%s"] [id="%s"]:target) .index-section-list[data-article="%s"] .index-section-item[data-section="%s"] > a[href="#%s"] {\n' \
					"$slug" "$section_id" "$slug" "$section_id" "$section_id"
				write_index_selected_style
				printf '}\n'
			done < "$tmp/section-ids-$slug.txt"

			while IFS= read -r section_id; do
				printf 'body:has(.article[id="%s"] [id="%s"]:target) .index-section-list[data-article="%s"] .index-section-item[data-section="%s"] > .index-subsection-list {\n' \
					"$slug" "$section_id" "$slug" "$section_id"
				printf '  display: flex;\n'
				printf '}\n'
			done < "$tmp/subsection-parents-$slug.txt"

			while IFS="$tab" read -r section_id subsection_id; do
				printf 'body:has(.article[id="%s"] [id="%s"]:target) .index-section-list[data-article="%s"] .index-section-item[data-section="%s"] > a[href="#%s"] {\n' \
					"$slug" "$subsection_id" "$slug" "$section_id" "$section_id"
				write_index_selected_style
				printf '}\n'
				printf 'body:has(.article[id="%s"] [id="%s"]:target) .index-subsection-list[data-section="%s"] a[href="#%s"] {\n' \
					"$slug" "$subsection_id" "$section_id" "$subsection_id"
				write_index_subsection_selected_style
				printf '}\n'
				printf 'body:has(.article[id="%s"] [id="%s"]:target) .index-section-list[data-article="%s"] .index-section-item[data-section="%s"] > .index-subsection-list {\n' \
					"$slug" "$subsection_id" "$slug" "$section_id"
				printf '  display: flex;\n'
				printf '}\n'
			done < "$tmp/subsections-$slug.tsv"
		fi
	done < "$article_meta"

	if [ "$subsection_count" -gt 0 ]; then
		printf '@media (max-width: 760px) and (orientation: portrait) {\n'
		while IFS="$tab" read -r slug title modified created; do
			while IFS= read -r section_id; do
				write_portrait_subsection_css "$slug" "$section_id" "$section_id"
			done < "$tmp/subsection-parents-$slug.txt"
			while IFS="$tab" read -r section_id subsection_id; do
				write_portrait_subsection_css "$slug" "$section_id" "$subsection_id"
			done < "$tmp/subsections-$slug.tsv"
		done < "$article_meta"
		printf '}\n'
	fi
}

write_license_popover() {
	cat <<EOF
        <div class="license-popover" id="license-popover" popover>
          <div class="license-head">
            <h2>$license_title_html</h2>
            <button class="license-close" type="button" popovertarget="license-popover" popovertargetaction="hide" aria-label="Close license">X</button>
          </div>
          <pre>$license_body_html</pre>
        </div>
EOF
}

write_contact_popover() {
	cat <<EOF
        <div class="contact-popover" id="contact-popover" popover>
          <div class="contact-head">
            <h2>Contact</h2>
            <button class="contact-close" type="button" popovertarget="contact-popover" popovertargetaction="hide" aria-label="Close contact form">X</button>
          </div>
          <form class="contact-form" action="./#contact-confirmation" method="get" accept-charset="UTF-8">
            <input type="hidden" name="$contact_field_name_html" value="1">
            <p class="contact-note">Messages are encrypted in transit over HTTPS, but remain in the URL and server log. Do not include confidential information.</p>
            <label class="contact-field">
              <span>Reply address (optional)</span>
              <input type="email" name="reply" maxlength="254" autocomplete="email">
            </label>
            <label class="contact-field">
              <span>Message</span>
              <textarea name="message" maxlength="500" required></textarea>
            </label>
            <button class="contact-submit" type="submit">Send</button>
          </form>
        </div>
EOF
}

write_contact_confirmation() {
	cat <<'EOF'
    <section class="contact-confirmation" id="contact-confirmation" aria-label="Contact confirmation">
      <div class="contact-head">
        <h2>Message sent</h2>
        <a class="contact-close" href="./" aria-label="Close confirmation">X</a>
      </div>
      <p>Thank you. Your message has been sent.</p>
    </section>
EOF
}

write_page_start() {
	cat <<EOF
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>$site_title_html</title>
    <link rel="icon" href="$favicon_href_html">
    <link rel="stylesheet" href="styles.css?v=$asset_version_html">
    <link rel="stylesheet" href="nav-sections.css?v=$asset_version_html">
  </head>
  <body>
    <header class="site-header">
      <a class="brand" href="#$first_slug_html" aria-label="$site_title_html home">
        <span class="brand-mark" aria-hidden="true" style="--brand-mark-color: $site_mark_color_html">$site_mark_html</span>
        $site_title_html
      </a>
EOF
	write_theme_menu
	cat <<EOF
    </header>

    <main class="shell">
      <nav class="index-pane" aria-label="Article index">
EOF
	write_order_input title title
	write_order_input created creation
	write_order_input modified modification
	cat <<EOF
        <div class="index-head">
          <h2>Index</h2>
          <div class="index-order" aria-label="Index order">
EOF
	write_order_label title title A
	write_order_label created creation C
	write_order_label modified modification M
	cat <<EOF
          </div>
        </div>
EOF
}

articles_file=$tmp/articles.html
article_meta=$tmp/articles.tsv
anchor_ids=$tmp/anchors.txt
order_title_file=$tmp/order-title.tsv
order_created_file=$tmp/order-created.tsv
order_modified_file=$tmp/order-modified.tsv
: > "$articles_file"
: > "$article_meta"
: > "$anchor_ids"
count=0
subsection_count=0

[ -f "$theme_menu" ] || die "Missing theme menu: $theme_menu"
[ -f "$styles_file" ] || die "Missing stylesheet: $styles_file"
[ -f "$license_file" ] || die "Missing license file: $license_file"

reserve_anchor index-order-title "generated index controls"
reserve_anchor index-order-created "generated index controls"
reserve_anchor index-order-modified "generated index controls"
reserve_anchor contact-popover "generated contact popover"
reserve_anchor contact-confirmation "generated contact confirmation"
reserve_anchor license-popover "generated license popover"
theme_ids=$tmp/theme-ids.txt
sed -n 's/.*[[:space:]]id="\([^"]*\)".*/\1/p' "$theme_menu" > "$theme_ids"
while IFS= read -r theme_anchor; do
	reserve_anchor "$theme_anchor" "$theme_menu"
done < "$theme_ids"

case $highlighter in
	pygments|source-highlight|none) ;;
	*) die "HIGHLIGHTER must be pygments, source-highlight, or none." ;;
esac
case $generator_version in
	''|*[!0-9.]*|.*|*.|*..*)
		die "GENERATOR_VERSION must contain dot-separated digits only."
		;;
esac
generator_id=$(sanitize_identifier "$generator_name")
[ -n "$generator_id" ] ||
	die "GENERATOR_NAME must contain an ASCII letter, digit, dot, underscore, or hyphen."
contact_field_name=${generator_id}_contact
case $code_footer_lines in
	''|*[!0-9]*) die "CODE_FOOTER_LINES must be a non-negative integer." ;;
esac
case $article_order in
	title|'') article_order=title ;;
	created|creation|ctime) article_order=created ;;
	modified|modification|mtime) article_order=modified ;;
	*) die "ARTICLE_ORDER must be title, created, or modified." ;;
esac

theme_id=$(normalize_theme "$default_theme")
safe_name "$theme_id" || die "Unsafe DEFAULT_THEME: $default_theme"
grep -F -q -e "id=\"$theme_id\"" "$theme_menu" ||
	die "DEFAULT_THEME does not exist in theme menu: $default_theme"
safe_color "$site_mark_color" || die "SITE_MARK_COLOR must be #rgb or #rrggbb."

for source in "$articles_dir"/*/article.html; do
	[ -f "$source" ] || continue
	dir=${source%/*}
	slug=${dir##*/}

	[ ! -L "$dir" ] || die "Refusing symlink article directory: $dir"
	[ ! -L "$source" ] || die "Refusing symlink article: $source"
	safe_name "$slug" || die "Unsafe article directory name: $slug"

	article_line=$(sed -n '/<article /{p;q;}' "$source")
	write_attrs "$article_line" "$attrs_file" \
		class data-title id data-created data-modified
	{
		IFS= read -r article_class
		IFS= read -r title
		IFS= read -r article_id
		IFS= read -r created
		IFS= read -r modified
	} < "$attrs_file"

	case $article_line in
		*'>'*) ;;
		*) die "Article opening tag must be contained on one line: $source" ;;
	esac
	case " $article_class " in
		*' article '*) ;;
		*) die "$source article element must include class=\"article\"." ;;
	esac
	[ -n "$title" ] || die "Missing article data-title in $source"
	[ -n "$created" ] || created=$build_day
	[ -n "$modified" ] || modified=$created
	safe_date "$created" || die "Article data-created must begin YYYY-MM-DD: $source"
	safe_date "$modified" || die "Article data-modified must begin YYYY-MM-DD: $source"
	[ "$article_id" = "$slug" ] || die "$source must contain id=\"$slug\" on its article element."
	reserve_anchor "$slug" "$source"
	case $title in
		*"$tab"*) die "Article title may not contain tabs: $source" ;;
	esac

	count=$((count + 1))

	printf '%s\t%s\t%s\t%s\n' \
		"$slug" "$title" "$modified" "$created" >> "$article_meta"
	read_sections "$source" "$slug"
done

[ "$count" -gt 0 ] || die "No articles found under $articles_dir/*/article.html."

sort_articles title > "$order_title_file"
sort_articles created > "$order_created_file"
sort_articles modified > "$order_modified_file"

case $article_order in
	title) order_file=$order_title_file ;;
	created) order_file=$order_created_file ;;
	modified) order_file=$order_modified_file ;;
esac

while IFS="$tab" read -r slug title modified created; do
	[ -n "${first_slug:-}" ] || first_slug=$slug
	render_article "$articles_dir/$slug" "$articles_dir/$slug/article.html" >> "$articles_file"
	printf '\n' >> "$articles_file"
done < "$order_file"

site_title_html=$(html_escape_attr "$site_title")
site_mark_html=$(html_escape_attr "$site_mark")
site_mark_color_html=$(html_escape_attr "$site_mark_color")
favicon_href_html=$(html_escape_attr "$(favicon_href "$site_mark")")
first_slug_html=$(html_escape_attr "$first_slug")
generator_label_html=$(html_escape_attr "$generator_name $generator_version")
contact_field_name_html=$(html_escape_attr "$contact_field_name")
build_date_html=$(html_escape_attr "$build_date")
license_html=$tmp/license.html
license_escape > "$license_html"
license_title_html=$(sed -n '/[^[:space:]]/{p;q;}' "$license_html")
[ -n "$license_title_html" ] || die "License file has no title: $license_file"
license_body_html=$(awk '
	seen { print }
	!seen && /[^[:space:]]/ { seen = 1 }
' "$license_html")
nav_tmp=$(mktemp "$root/.nav-sections.css.XXXXXXXXXX")
write_section_nav_css > "$nav_tmp"
asset_version=$(
	{
		cksum < "$styles_file"
		cksum < "$nav_tmp"
	} | cksum | awk '{ print $1 }'
)
asset_version_html=$(html_escape_attr "$asset_version")
output_tmp=$(mktemp "$root/.index.html.XXXXXXXXXX")

{
	write_page_start
	write_nav_list title "$order_title_file"
	write_nav_list created "$order_created_file"
	write_nav_list modified "$order_modified_file"
	write_section_lists "$order_file"
	cat <<EOF
        <footer class="index-footer">
          <p>$generator_label_html</p>
          <p>zero-JS, cookie-free static site</p>
          <p>Built $build_date_html</p>
          <p class="index-footer-actions">
            <button class="contact-button" type="button" popovertarget="contact-popover">Contact</button>
            <button class="license-button" type="button" popovertarget="license-popover">$license_title_html</button>
          </p>
        </footer>
EOF
	write_contact_popover
	write_license_popover
	cat <<EOF
      </nav>

      <section class="article-pane" aria-label="Articles">
EOF
	cat "$articles_file"
	cat <<EOF
      </section>
    </main>
EOF
	write_contact_confirmation
	cat <<EOF
  </body>
</html>
EOF
} > "$output_tmp"

chmod 0644 "$nav_tmp" "$output_tmp"
mv "$nav_tmp" "$root/nav-sections.css"
nav_tmp=
mv "$output_tmp" "$root/index.html"
output_tmp=

printf 'Built %s articles into index.html.\n' "$count"
printf 'Default theme: %s. Article order: %s.\n' "$theme_id" "$article_order"
case $highlighter in
	pygments)
		if [ -n "$pygmentize" ]; then
			printf 'Pygments available: %s highlighted, %s escaped raw.\n' \
				"$highlighted_blocks" "$raw_code_blocks"
		else
			printf 'pygmentize not found; %s code blocks use escaped raw source.\n' \
				"$raw_code_blocks"
		fi
		;;
	source-highlight)
		if [ -z "$source_highlight" ]; then
			printf 'source-highlight not found; %s code blocks use escaped raw source.\n' \
				"$raw_code_blocks"
		elif [ ! -f "$outlang" ]; then
			printf 'Source-highlight output definition missing; %s code blocks use escaped raw source.\n' \
				"$raw_code_blocks"
		else
			printf 'Source-highlight available: %s highlighted, %s escaped raw.\n' \
				"$highlighted_blocks" "$raw_code_blocks"
		fi
		;;
	none)
		printf 'Syntax highlighting disabled; %s code blocks use escaped raw source.\n' \
			"$raw_code_blocks"
		;;
esac
