#!/bin/bash # # Regenerate every figure in the documentation from the listing shown beside it. # # A chapter that says "CIRCLE draws ellipses" and shows a listing wants a picture # of what that listing draws, and the failure mode of a picture is that it stops # being of the code next to it. Nothing tells you: a screenshot taken by hand in # 2026 still looks like a screenshot in 2027, long after the verb it illustrates # has changed. # # So the figure is not an asset, it is *output*. A block tagged # # ```basic requires=akgl screenshot=circle # # is run by tools/screenshot.c and written to docs/images/circle.png, and the # chapter shows that file. Regenerate and the picture follows the code. The # generated PNGs are checked in on purpose -- a reader on the forge has no build # tree -- and tests/docs_examples.sh fails if a tagged block has no image, so a # new figure cannot be forgotten. # # **This is not run by the build.** `cmake --build build-akgl --target # docs_screenshots` is deliberate; see the target's comment in CMakeLists.txt. # # Exit status is the number of figures that failed, the house convention. set -u ROOT="" TOOL="" CHECK=0 FAILURES=0 usage() { cat >&2 <<'EOF' usage: docs_screenshots.sh --root DIR --tool PATH [--check] [FILE...] --root DIR repository root; images are written to DIR/docs/images --tool PATH the built akbasic_screenshot --check render to a scratch directory and compare, changing nothing FILE... which documents to regenerate (default: docs/*.md) EOF exit 2 } while [ $# -gt 0 ]; do case "$1" in --root) ROOT="$2"; shift 2 ;; --tool) TOOL="$2"; shift 2 ;; --check) CHECK=1; shift ;; --help|-h) usage ;; --*) echo "unknown option $1" >&2; usage ;; *) break ;; esac done [ -n "${ROOT}" ] || usage [ -n "${TOOL}" ] || usage [ -x "${TOOL}" ] || { echo "FAIL: no screenshot tool at ${TOOL}" >&2; exit 2; } # Absolute, because the loop below cd's into a sandbox to run each program -- # a listing that loads an asset resolves it relative to its own directory. case "${TOOL}" in /*) ;; *) TOOL="${PWD}/${TOOL}" ;; esac cd "${ROOT}" || exit 2 ROOT="${PWD}" DOCS=("$@") if [ ${#DOCS[@]} -eq 0 ]; then DOCS=(docs/*.md) fi IMAGES="${ROOT}/docs/images" mkdir -p "${IMAGES}" || exit 2 WORK="$(mktemp -d)" trap 'rm -rf "${WORK}"' EXIT # --check renders somewhere else entirely and compares, so a run that finds a # stale figure does not also fix it. A test that repairs what it is measuring # passes the second time for the wrong reason. if [ "${CHECK}" -eq 1 ]; then OUTDIR="${WORK}/rendered" mkdir -p "${OUTDIR}" || exit 2 else OUTDIR="${IMAGES}" fi # The value of attribute $1 in an info string $2, or empty. Same shape as the # `attr` in tests/docs_examples.sh, and deliberately so -- the two read the same # fence tags and a second dialect of them would be a bug waiting to happen. attr() { local name="$1" info="$2" word for word in ${info}; do case "${word}" in "${name}"=*) echo "${word#*=}"; return 0 ;; esac done echo "" } # One figure: write the listing to a file, run it, keep the PNG. render() { local name="$1" body="$2" size="$3" where="$4" setup="$5" local w=320 h=200 out="${OUTDIR}/${name}.png" log="" if [ -n "${size}" ]; then w="${size%x*}" h="${size#*x}" fi # The same setup scripts tests/docs_examples.sh uses, run in the same place # relative to the program. A listing that loads `ship.png` needs one whether # it is being checked or being photographed. if [ -n "${setup}" ]; then if [ ! -x "${ROOT}/tests/docs_setups/${setup}.sh" ]; then echo "FAIL ${where}: no setup script ${setup}.sh" >&2 FAILURES=$((FAILURES + 1)) return fi ( cd "${WORK}" && "${ROOT}/tests/docs_setups/${setup}.sh" ) || { echo "FAIL ${where}: setup ${setup} failed" >&2 FAILURES=$((FAILURES + 1)) return } fi printf '%s' "${body}" > "${WORK}/${name}.bas" # **stdout only.** The tool puts the interpreter's sink on stdout and libakgl # logs its registry chatter -- "Actor akbasic:actor:1 initialized" -- to # stderr, so merging the two would make every sprite figure look like a # failing program. stderr is shown when the tool itself refuses, which is # when it is worth reading. if ! log="$(cd "${WORK}" && "${TOOL}" "${name}.bas" "${out}" "${w}" "${h}" \ 2>"${WORK}/${name}.err")"; then echo "FAIL ${where}: ${name} did not render" >&2 cat "${WORK}/${name}.err" >&2 FAILURES=$((FAILURES + 1)) return fi # A program that raised prints its error line and still exits zero, because # a BASIC error is the script's and not the host's. For a figure that is # still a failure: an image of a blank screen is worse than no image. if [ -n "${log}" ]; then echo "FAIL ${where}: ${name} rendered, but the program reported:" >&2 echo "${log}" >&2 FAILURES=$((FAILURES + 1)) return fi if [ "${CHECK}" -eq 1 ]; then if [ ! -r "${IMAGES}/${name}.png" ]; then echo "FAIL ${where}: docs/images/${name}.png does not exist" >&2 FAILURES=$((FAILURES + 1)) return fi if ! cmp -s "${out}" "${IMAGES}/${name}.png"; then echo "FAIL ${where}: docs/images/${name}.png is not what that listing draws" >&2 echo " regenerate with: cmake --build --target docs_screenshots" >&2 FAILURES=$((FAILURES + 1)) return fi echo " ${name}.png matches" return fi echo " ${name}.png (${w}x${h})" } for DOC in "${DOCS[@]}"; do [ -r "${DOC}" ] || { echo "FAIL: no document at \"${DOC}\"" >&2; exit 2; } # Walk the file collecting fenced blocks. Only `screenshot=` ones are kept; # everything else is somebody else's problem, and docs_examples.sh is that # somebody. IN_BLOCK=0 INFO="" BODY="" START=0 LINENO=0 while IFS= read -r LINE; do LINENO=$((LINENO + 1)) case "${LINE}" in '```'*) if [ "${IN_BLOCK}" -eq 0 ]; then IN_BLOCK=1 INFO="${LINE#'```'}" BODY="" START="${LINENO}" else IN_BLOCK=0 NAME="$(attr screenshot "${INFO}")" if [ -n "${NAME}" ]; then render "${NAME}" "${BODY}" "$(attr size "${INFO}")" \ "${DOC}:${START}" "$(attr setup "${INFO}")" fi fi ;; *) if [ "${IN_BLOCK}" -eq 1 ]; then BODY="${BODY}${LINE}"$'\n' fi ;; esac done < "${DOC}" done if [ "${FAILURES}" -ne 0 ]; then echo "${FAILURES} figure(s) failed" >&2 fi exit "${FAILURES}"