#!/bin/bash
#
# Compile, link, run and cross-check every example in docs/.
#
# libakgl exports 156 functions and, before this existed, the only user-facing
# documentation was a FAQ in README.md whose examples did not compile: two
# unbalanced PASS() calls, a `sprite->frameids = [0, 1, 2, 3];` that is not C in
# any dialect, a stray `9` inside a bitmask expression, an `int screenwidth =
# NULL`, and -- in the first snippet a reader ever saw -- the exact strncpy call
# AGENTS.md forbids. The prose had drifted the same way: two claims in the
# headers were false against src/.
#
# That is not a reader routing around typos. It is what happens to samples and
# prose that nothing executes. AGENTS.md already makes the argument in another
# context -- "A test that has not failed has not been tested" -- and the sibling
# akbasic repository solved this exact problem, so this is its harness with the
# interpreter taken out and the ability to link and run put in.
#
# Documentation goes stale because the *code* moved, not because somebody edited
# a chapter, so this runs on every ctest rather than behind a docs path filter.
#
# **Exit status is the number of failed examples**, the house convention. 2 for a
# usage or setup error, which is a different thing and has to be distinguishable.
#
# The contract with the documentation is a set of fence info strings. They are
# ordinary markdown, they render as syntax highlighting on the forge, and they
# sit next to the thing they describe. docs/MAINTENANCE.md is the reference; the
# short version:
#
#   ```c                    a translation unit: compile it with -fsyntax-only
#   ```c wrap=NAME          the same, wrapped in tests/docs_preludes/NAME.pre/.post
#   ```c run=NAME           wrapped, linked against akgl, and run headless
#   ```c excerpt=PATH       must appear verbatim in PATH; not compiled
#   ```c screenshot=NAME    also the source of docs/images/NAME.png
#   ```json kind=KIND       loaded through the real akgl_*_load_json
#   ```json excerpt=PATH    must appear verbatim in PATH; not loaded
#   ```output               the exact stdout of the runnable block above it
#   ```sh                   run in a sandbox; must exit 0
#   ```sh setup=NAME        the same, after tests/docs_setups/NAME.sh
#   ```sh norun             destructive, networked, or re-enters this suite
#   ```cmake                never executed; hand-maintained, by decision
#   ```text                 a diagram or a captured dump; nothing to execute
#   ```norun                shown, never executed, whatever it is
#
# A block with **no** info string is a hard error, and so is one whose info
# string this script does not know. That is deliberate: the failure mode the
# whole harness exists to avoid is passing because it quietly ran nothing, so an
# unannotated block is a missing decision rather than a default.

set -u

ROOT=""
CFLAGS_FILE=""
LDFLAGS_FILE=""
CHECKJSON=""

usage()
{
    cat >&2 <<'EOF'
usage: docs_examples.sh --root DIR [--cflags-file FILE] [--ldflags-file FILE]
                        [--checkjson PATH] [FILE...]

  --root DIR           repository root; documentation paths are relative to it
  --cflags-file FILE   one compiler flag per line, for the ```c blocks
  --ldflags-file FILE  one linker argument per line, for the ```c run= blocks
  --checkjson PATH     the built akgl_docs_checkjson, for the ```json blocks
  FILE...              which documents to check (default: docs/*.md)

Exit status is the number of failed examples, or 2 for a usage or setup error.
EOF
    exit 2
}

while [ $# -gt 0 ]; do
    case "$1" in
    --root)         ROOT="$2";         shift 2 ;;
    --cflags-file)  CFLAGS_FILE="$2";  shift 2 ;;
    --ldflags-file) LDFLAGS_FILE="$2"; shift 2 ;;
    --checkjson)    CHECKJSON="$2";    shift 2 ;;
    --help|-h)      usage ;;
    --*)            echo "unknown option $1" >&2; usage ;;
    *)              break ;;
    esac
done

[ -n "${ROOT}" ] || usage

# Every path is forced absolute before anything else runs, because this script
# cd's into sandbox directories. akbasic learned that the hard way: a relative
# --root could not find tests/docs_setups, which is invisible under CTest -- it
# passes absolute paths -- and immediate for anybody running one document by
# hand.
for _var in CHECKJSON CFLAGS_FILE LDFLAGS_FILE; do
    _val="${!_var}"
    case "${_val}" in
    ""|/*) ;;
    *)     printf -v "${_var}" '%s' "${PWD}/${_val}" ;;
    esac
done
unset _var _val

cd "${ROOT}" || exit 2
ROOT="${PWD}"

DOCS=("$@")
if [ ${#DOCS[@]} -eq 0 ]; then
    # The default list is a glob and is allowed to match nothing: the chapters
    # are written in parallel with this, and a harness that refuses to configure
    # until docs/ exists is a harness nobody can land first. A document named on
    # the command line is a different case entirely -- see below.
    DOCS=()
    for _doc in docs/*.md; do
	[ -r "${_doc}" ] && DOCS+=("${_doc}")
    done
    unset _doc
    if [ ${#DOCS[@]} -eq 0 ]; then
	echo "ran: nothing -- no documents matched docs/*.md"
	echo "PASS: there is no documentation to check yet"
	exit 0
    fi
fi

# Refuse a named document that is not there rather than checking nothing and
# passing. An empty argument is the specific case that got here in akbasic: a
# CMake generator expression evaluating to nothing still contributes an empty
# argument, which looked like a filename and silently replaced the whole list.
for _doc in "${DOCS[@]}"; do
    if [ ! -r "${_doc}" ]; then
	echo "FAIL: no document at \"${_doc}\"" >&2
	exit 2
    fi
done
unset _doc

WORK="$(mktemp -d)"
trap 'rm -rf "${WORK}"' EXIT

# The compiler flags the ```c blocks need. CMake writes them, because the
# include path is transitive through akerror, akstdlib, SDL3 and jansson, and
# hardcoding it here would rot exactly the way the documentation does.
CFLAGS=()
if [ -n "${CFLAGS_FILE}" ] && [ -r "${CFLAGS_FILE}" ]; then
    while IFS= read -r _flag; do
	[ -n "${_flag}" ] && CFLAGS+=("${_flag}")
    done < "${CFLAGS_FILE}"
    # The preludes reach their shared include set as <docs_prelude.h>, and the
    # unit is compiled from a scratch directory, so the search path has to say
    # where that is.
    CFLAGS+=("-I${ROOT}/tests/docs_preludes")
fi

LDFLAGS=()
if [ -n "${LDFLAGS_FILE}" ] && [ -r "${LDFLAGS_FILE}" ]; then
    while IFS= read -r _flag; do
	[ -n "${_flag}" ] && LDFLAGS+=("${_flag}")
    done < "${LDFLAGS_FILE}"
fi
unset _flag

CC="${CC:-cc}"

FAILURES=0
declare -A RAN=([c]=0 [run]=0 [output]=0 [excerpt]=0 [json]=0 [sh]=0 [screenshot]=0)
declare -A SKIPPED=([norun]=0 [cmake]=0 [text]=0 [nocc]=0 [nold]=0 [nojson]=0)

# Named `json` blocks, so a later one can name them in `preload=`. Reset per
# document: a name is a fact about one chapter, and letting chapter 13 reach
# into chapter 11 would make the order documents are checked in load-bearing.
declare -A JSON_KIND_OF=()
declare -A JSON_BODY_OF=()

# Every failure names the file and the line of the block's *opening* fence, so
# the message points at the thing to edit rather than at this script.
function fail()
{
    local where="$1"; shift
    echo "FAIL ${where}: $*" >&2
    FAILURES=$((FAILURES + 1))
}

# Show a byte-exact comparison. `cat -A` because the errors this catches are
# trailing spaces and missing newlines, which a plain diff renders invisibly.
function show_diff()
{
    local want="$1" got="$2"
    echo "--- expected ---" >&2
    cat -A "${want}" >&2
    echo "--- actual ---" >&2
    cat -A "${got}" >&2
}

# ---------------------------------------------------------------- extraction
#
# One pass per document, writing each block's body to ${WORK}/blocks/NNNN and a
# line to ${WORK}/blocks/index. Keeping the bodies in files rather than in shell
# variables means a block containing a NUL, a backslash or an unbalanced quote
# is carried through untouched.
function extract()
{
    local doc="$1" out="$2"

    awk -v OUT="${out}" -v INDEX="${out}/index" '
	function pad(n) { return sprintf("%04d", n) }
	/^```/ {
	    if ( !inblock ) {
		inblock = 1
		tag = substr($0, 4)
		sub(/[ \t]+$/, "", tag)
		start = NR
		count++
		body = OUT "/" pad(count)
		printf "" > body
		next
	    }
	    if ( $0 == "```" ) {
		close(body)
		printf "%s\t%d\t%s\n", pad(count), start, tag >> INDEX
		inblock = 0
		next
	    }
	}
	inblock { print >> body }
    ' "${doc}"
}

# ------------------------------------------------------------------- helpers

# The value of attribute $1 in an info string $2, or empty. `wrap=akglbody` and
# `kind=sprite` are both read this way.
function attr()
{
    local name="$1" info="$2" word
    for word in ${info}; do
	case "${word}" in
	"${name}"=*) echo "${word#*=}"; return ;;
	esac
    done
}

# True when a bare word appears in an info string. Used for `norun`.
function has_word()
{
    local want="$1" info="$2" word
    for word in ${info}; do
	[ "${word}" = "${want}" ] && return 0
    done
    return 1
}

# Strip C comments and collapse whitespace, so an excerpt can be compared to the
# header it came from without either side having to carry the other's doc
# comments or indentation. The bit-flag tables in actor.h and iterator.h are
# hand-aligned on purpose -- scripts/reindent.el goes out of its way not to
# disturb them -- so an excerpt of one has to survive being re-aligned.
function normalise_c()
{
    awk '
	{
	    line = $0
	    out = ""
	    while ( length(line) > 0 ) {
		if ( incomment ) {
		    i = index(line, "*/")
		    if ( i == 0 ) { line = ""; break }
		    incomment = 0
		    line = substr(line, i + 2)
		    continue
		}
		i = index(line, "/*")
		j = index(line, "//")
		if ( j > 0 && (i == 0 || j < i) ) {
		    out = out substr(line, 1, j - 1)
		    line = ""
		    break
		}
		if ( i == 0 ) { out = out line; line = ""; break }
		out = out substr(line, 1, i - 1)
		incomment = 1
		line = substr(line, i + 2)
	    }
	    printf "%s ", out
	}
	END { printf "\n" }
    ' "$1" | tr -s ' \t' ' ' | sed -e 's/^ //' -e 's/ $//'
}

# Run tests/docs_setups/NAME.sh in ${1}, with the repository root as its only
# argument. An example that loads an asset needs that asset; putting the fixture
# in a setup script rather than in the chapter keeps the example the shape a
# reader wants to see.
function run_setup()
{
    local sandbox="$1" setup="$2" where="$3"

    [ -n "${setup}" ] || return 0
    if [ ! -r "${ROOT}/tests/docs_setups/${setup}.sh" ]; then
	fail "${where}" "setup=${setup} names no tests/docs_setups/${setup}.sh"
	return 1
    fi
    if ! ( cd "${sandbox}" && bash "${ROOT}/tests/docs_setups/${setup}.sh" "${ROOT}" ) \
	 >/dev/null 2>&1; then
	fail "${where}" "setup=${setup} failed"
	return 1
    fi
    return 0
}

# Assemble the translation unit: prelude, a #line directive naming the markdown,
# the body, and the closing half of the prelude. Writes ${WORK}/unit.c.
#
# The #line is what makes a diagnostic read `docs/14-physics.md:112: error: ...`
# rather than pointing into a scratch file nobody can open.
function build_unit()
{
    local where="$1" body="$2" wrap="$3" doc="$4" line="$5"
    local unit="${WORK}/unit.c"

    : > "${unit}"
    if [ -n "${wrap}" ]; then
	if [ ! -r "${ROOT}/tests/docs_preludes/${wrap}.pre" ]; then
	    fail "${where}" "names no tests/docs_preludes/${wrap}.pre"
	    return 1
	fi
	cat "${ROOT}/tests/docs_preludes/${wrap}.pre" >> "${unit}"
    fi
    printf '#line %d "%s"\n' "$((line + 1))" "${doc}" >> "${unit}"
    cat "${body}" >> "${unit}"
    if [ -n "${wrap}" ] && [ -r "${ROOT}/tests/docs_preludes/${wrap}.post" ]; then
	printf '#line 1 "tests/docs_preludes/%s.post"\n' "${wrap}" >> "${unit}"
	cat "${ROOT}/tests/docs_preludes/${wrap}.post" >> "${unit}"
    fi
    return 0
}

# --------------------------------------------------------------- block kinds

# A C snippet, compiled and not linked: what most of these examples are for is
# showing the API, and the API is exactly what -fsyntax-only checks.
#
# -Werror here, unlike the library. AGENTS.md keeps AKGL_WERROR off by default
# because libakgl is consumed with add_subdirectory() and a new compiler's
# diagnostic should not break somebody else's build. A documentation snippet is
# not a consumer, and a sample that warns is a sample that teaches the warning.
#
# gnu99 rather than c99: akerror.h uses PATH_MAX, which <limits.h> hides under
# __STRICT_ANSI__, so a stricter dialect would fail on the dependency rather
# than on the example.
function run_c()
{
    local where="$1" body="$2" info="$3" doc="$4" line="$5"
    local wrap log="${WORK}/cc.log"

    if [ ${#CFLAGS[@]} -eq 0 ]; then
	SKIPPED[nocc]=$((SKIPPED[nocc] + 1))
	return
    fi

    wrap="$(attr wrap "${info}")"
    build_unit "${where}" "${body}" "${wrap}" "${doc}" "${line}" || return

    if "${CC}" -fsyntax-only -std=gnu99 -Wall -Werror \
	       "${CFLAGS[@]}" "${WORK}/unit.c" > "${log}" 2>&1; then
	RAN[c]=$((RAN[c] + 1))
    else
	fail "${where}" "does not compile"
	sed -n '1,25p' "${log}" >&2
    fi
}

# A C snippet linked into a real program and run headless.
#
# This is the reason to port the harness at all for a game library. -fsyntax-only
# proves a call typechecks; it does not prove the startup order works, that a
# sprite loads, or that an ATTEMPT block releases what it acquired. A run= block
# asserts a clean exit *and* that nothing reported a raised context on stderr,
# because a libakgl program can fail and still exit 0.
function run_c_run()
{
    local where="$1" body="$2" info="$3" doc="$4" line="$5" expected="$6"
    local wrap log="${WORK}/cc.log" prog="${WORK}/prog"
    local sandbox="${WORK}/run" got="${WORK}/got" errout="${WORK}/stderr" status=0

    if [ ${#CFLAGS[@]} -eq 0 ] || [ ${#LDFLAGS[@]} -eq 0 ]; then
	SKIPPED[nold]=$((SKIPPED[nold] + 1))
	return
    fi

    wrap="$(attr run "${info}")"
    build_unit "${where}" "${body}" "${wrap}" "${doc}" "${line}" || return

    rm -f "${prog}"
    if ! "${CC}" -std=gnu99 -Wall -Werror "${CFLAGS[@]}" \
		 -o "${prog}" "${WORK}/unit.c" "${LDFLAGS[@]}" > "${log}" 2>&1; then
	fail "${where}" "does not build"
	sed -n '1,25p' "${log}" >&2
	return
    fi

    rm -rf "${sandbox}"
    mkdir -p "${sandbox}"
    run_setup "${sandbox}" "$(attr setup "${info}")" "${where}" || return

    # The same headless forcing scripts/memcheck.sh uses, so the answer does not
    # depend on whose desktop it ran on.
    ( cd "${sandbox}" && \
	  SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIODRIVER=dummy \
	  "${prog}" ) > "${got}" 2> "${errout}"
    status=$?
    RAN[run]=$((RAN[run] + 1))

    if [ "${status}" -ne 0 ]; then
	fail "${where}" "the example exited ${status}"
	sed -n '1,20p' "${got}" "${errout}" >&2
	return
    fi

    # An error that was ignored, or one that reached libakerror's unhandled
    # handler, is a failure even where the process still exited 0. Both print a
    # recognisable line, and both mean the example did not do what it says.
    if grep -qE '\*\* IGNORED ERROR \*\*|Unhandled Error' "${errout}"; then
	fail "${where}" "the example raised an error it did not handle"
	grep -E '\*\* IGNORED ERROR \*\*|Unhandled Error' "${errout}" >&2
	return
    fi

    # stdout only. libakgl logs registry activity to stderr -- three lines every
    # time a sprite is created -- and folding that into the comparison would put
    # library chatter into a chapter's expected output.
    if [ -n "${expected}" ]; then
	RAN[output]=$((RAN[output] + 1))
	if ! cmp -s "${expected}" "${got}"; then
	    fail "${where}" "output does not match the block below it"
	    show_diff "${expected}" "${got}"
	fi
    fi
}

# A declaration echoed from a header, or a fragment quoted out of an asset file
# too large to show whole. Compiling or loading it is not the point -- the check
# is that it still says what the file says, which is the drift that actually
# happens and the one a compile could not catch.
function run_excerpt()
{
    local where="$1" body="$2" path="$3"

    if [ ! -r "${path}" ]; then
	fail "${where}" "excerpt=${path} names no such file"
	return
    fi
    RAN[excerpt]=$((RAN[excerpt] + 1))
    if ! normalise_c "${path}" | grep -qF -- "$(normalise_c "${body}")"; then
	fail "${where}" "no longer appears in ${path}"
	echo "--- the documentation says ---" >&2
	cat "${body}" >&2
	echo "--- ${path} has drifted from it; comments and whitespace are ignored ---" >&2
    fi
}

# An asset file, run through the loader that really reads it.
#
# The formats are documented in prose and read by src/sprite.c, src/character.c,
# src/tilemap.c and src/registry.c, with nothing tying the two together. That gap
# is not hypothetical: util/assets/littleguy.json is already invalid against the
# loader it ships beside. tools/docs_checkjson.c closes it.
function run_json()
{
    local where="$1" body="$2" info="$3"
    local kind name preload preloads target
    local sandbox="${WORK}/json" log="${WORK}/json.log"
    local args=()

    kind="$(attr kind "${info}")"
    if [ -z "${kind}" ]; then
	fail "${where}" "a json block with no kind= (sprite, character, tilemap or properties)"
	return
    fi

    # A named block can be preloaded by a later one. Recorded whether or not the
    # validator is available, so omitting --checkjson does not turn a bad
    # preload= into a different failure.
    name="$(attr name "${info}")"
    if [ -n "${name}" ]; then
	JSON_KIND_OF["${name}"]="${kind}"
	JSON_BODY_OF["${name}"]="${body}"
    fi

    if [ -z "${CHECKJSON}" ] || [ ! -x "${CHECKJSON}" ]; then
	SKIPPED[nojson]=$((SKIPPED[nojson] + 1))
	return
    fi

    rm -rf "${sandbox}"
    mkdir -p "${sandbox}"
    run_setup "${sandbox}" "$(attr setup "${info}")" "${where}" || return

    # Three of the four formats are not self-contained: a character names sprites
    # that have to be in the registry already, and a tilemap names characters.
    # `preload=` lists the earlier named blocks that have to be loaded first, in
    # order -- which puts the dependency in front of the reader rather than in a
    # setup script they cannot see.
    preloads="$(attr preload "${info}")"
    for preload in ${preloads//,/ }; do
	if [ -z "${JSON_KIND_OF[${preload}]:-}" ]; then
	    fail "${where}" "preload=${preload} names no earlier \`json name=${preload}\` block in this document"
	    return
	fi
	cp "${JSON_BODY_OF[${preload}]}" "${sandbox}/${preload}.json"
	args+=("${JSON_KIND_OF[${preload}]}" "${preload}.json")
    done

    target="${name:-docs_example}"
    cp "${body}" "${sandbox}/${target}.json"
    args+=("${kind}" "${target}.json")

    if ( cd "${sandbox}" && \
	     SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIODRIVER=dummy \
	     "${CHECKJSON}" "${args[@]}" ) > "${log}" 2>&1; then
	RAN[json]=$((RAN[json] + 1))
    else
	fail "${where}" "is not valid ${kind} JSON as far as the loader is concerned"
	sed -n '1,25p' "${log}" >&2
    fi
}

# A shell command, in a sandbox.
function run_sh()
{
    local where="$1" body="$2" info="$3" expected="$4"
    local sandbox="${WORK}/sh" script="${WORK}/block.sh" got="${WORK}/got"

    rm -rf "${sandbox}"
    mkdir -p "${sandbox}"
    run_setup "${sandbox}" "$(attr setup "${info}")" "${where}" || return

    # A leading "$ " is a prompt in a transcript, not part of the command.
    sed -e 's/^\$ //' "${body}" > "${script}"
    if ! ( cd "${sandbox}" && bash -e "${script}" ) > "${got}" 2>&1; then
	fail "${where}" "the command failed"
	sed -n '1,20p' "${got}" >&2
	return
    fi
    RAN[sh]=$((RAN[sh] + 1))

    if [ -n "${expected}" ]; then
	RAN[output]=$((RAN[output] + 1))
	if ! cmp -s "${expected}" "${got}"; then
	    fail "${where}" "output does not match the block below it"
	    show_diff "${expected}" "${got}"
	fi
    fi
}

# ------------------------------------------------------------------ the pass
for DOC in "${DOCS[@]}"; do
    BLOCKS="${WORK}/blocks"
    rm -rf "${BLOCKS}"
    mkdir -p "${BLOCKS}"
    : > "${BLOCKS}/index"
    extract "${DOC}" "${BLOCKS}"
    JSON_KIND_OF=()
    JSON_BODY_OF=()

    # Read the index into arrays first. An `output` block belongs to the block
    # before it, so each iteration has to be able to look one ahead.
    CLAIMED=0
    IDS=(); LINES=(); INFOS=()
    while IFS=$'\t' read -r _id _line _info; do
	IDS+=("${_id}"); LINES+=("${_line}"); INFOS+=("${_info}")
    done < "${BLOCKS}/index"

    for ((n = 0; n < ${#IDS[@]}; n++)); do
	INFO="${INFOS[n]}"
	BODY="${BLOCKS}/${IDS[n]}"
	WHERE="${DOC}:${LINES[n]}"
	KIND="${INFO%% *}"

	# An `output` block belongs to the block above it, which claimed it on
	# the way past. One that nothing claimed is compared against nothing at
	# all -- indistinguishable from a passing test, and the exact failure
	# this harness exists to rule out. A *skipped* block still claims its
	# output: `norun` is a decision, not an accident.
	if [ "${KIND}" = "output" ]; then
	    if [ "${CLAIMED}" -eq 0 ]; then
		fail "${WHERE}" "an output block with nothing runnable above it"
	    fi
	    CLAIMED=0
	    continue
	fi
	CLAIMED=0

	EXPECTED=""
	if [ $((n + 1)) -lt ${#IDS[@]} ] && [ "${INFOS[n + 1]%% *}" = "output" ]; then
	    case "${KIND}" in
	    sh)
		EXPECTED="${BLOCKS}/${IDS[n + 1]}"
		CLAIMED=1
		;;
	    c)
		if [ -n "$(attr run "${INFO}")" ]; then
		    EXPECTED="${BLOCKS}/${IDS[n + 1]}"
		    CLAIMED=1
		fi
		;;
	    esac
	fi

	if has_word norun "${INFO}"; then
	    SKIPPED[norun]=$((SKIPPED[norun] + 1))
	    continue
	fi

	# A `screenshot=NAME` block is the source of docs/images/NAME.png,
	# generated by tools/docs_screenshots.sh. The image is tracked, because a
	# reader on the forge has no build tree -- so nothing would otherwise
	# notice a figure that was tagged and never generated, and the chapter
	# would render a broken image. Whether the file is *of that listing* is
	# the docs_screenshots test's question, not this one's.
	SHOT="$(attr screenshot "${INFO}")"
	if [ -n "${SHOT}" ]; then
	    if [ -r "docs/images/${SHOT}.png" ]; then
		RAN[screenshot]=$((RAN[screenshot] + 1))
	    else
		fail "${WHERE}" "screenshot=${SHOT} has no docs/images/${SHOT}.png; run \`cmake --build build --target docs_screenshots\`"
	    fi
	fi

	case "${KIND}" in
	c)
	    EXCERPT="$(attr excerpt "${INFO}")"
	    if [ -n "${EXCERPT}" ]; then
		run_excerpt "${WHERE}" "${BODY}" "${EXCERPT}"
	    elif [ -n "$(attr run "${INFO}")" ]; then
		run_c_run "${WHERE}" "${BODY}" "${INFO}" "${DOC}" "${LINES[n]}" "${EXPECTED}"
	    elif [ -n "${SHOT}" ]; then
		# Compiled here as well as rendered there, so a figure's listing
		# is held to the same -Werror as every other snippet even in a
		# tree where the figures are not being regenerated. wrap= is
		# implied: a frame body is not a translation unit.
		run_c "${WHERE}" "${BODY}" "wrap=akglframe" "${DOC}" "${LINES[n]}"
	    else
		run_c "${WHERE}" "${BODY}" "${INFO}" "${DOC}" "${LINES[n]}"
	    fi
	    ;;
	json)
	    EXCERPT="$(attr excerpt "${INFO}")"
	    if [ -n "${EXCERPT}" ]; then
		run_excerpt "${WHERE}" "${BODY}" "${EXCERPT}"
	    else
		run_json "${WHERE}" "${BODY}" "${INFO}"
	    fi
	    ;;
	sh)
	    run_sh "${WHERE}" "${BODY}" "${INFO}" "${EXPECTED}"
	    ;;
	cmake)
	    SKIPPED[cmake]=$((SKIPPED[cmake] + 1))
	    ;;
	# A block diagram, or a stack trace captured from a real run. There is no
	# executable claim in either, so the tag exists to say that out loud
	# rather than to let one sit here untagged and unnoticed.
	text)
	    SKIPPED[text]=$((SKIPPED[text] + 1))
	    ;;
	norun)
	    SKIPPED[norun]=$((SKIPPED[norun] + 1))
	    ;;
	"")
	    fail "${WHERE}" "fenced block with no info string; tag it (see docs/MAINTENANCE.md)"
	    ;;
	*)
	    fail "${WHERE}" "unknown info string \"${INFO}\" (see docs/MAINTENANCE.md)"
	    ;;
	esac
    done
done

# The count is part of the result, not decoration. A harness that passes because
# a chapter stopped matching the extractor looks exactly like a harness that
# passes because the documentation is correct, and this is what tells them apart.
# A rising `norun` is the harness being talked out of its job.
echo "ran: ${RAN[c]} c snippets, ${RAN[run]} linked programs, ${RAN[output]} output comparisons, ${RAN[excerpt]} excerpts, ${RAN[json]} json blocks, ${RAN[sh]} shell blocks, ${RAN[screenshot]} figures"
echo "skipped: ${SKIPPED[norun]} norun, ${SKIPPED[cmake]} cmake, ${SKIPPED[text]} text, ${SKIPPED[nocc]} c (no compiler flags given), ${SKIPPED[nold]} run (no linker flags given), ${SKIPPED[nojson]} json (no validator given)"

if [ "${FAILURES}" -eq 0 ]; then
    echo "PASS: every documented example does what the documentation says"
fi
exit "${FAILURES}"
