#!/bin/sh set -eu export LANG=POSIX.UTF-8 ## -m so a path need not exist: po4a's outputs are named here ## before po4a has written them --- naming them is what the rules ## being generated are for --- and realpath without it fails on ## the first one, taking the whole script with it under set -e. ## For a path that does exist the answer is the same either way. normalize() { ## realpath(1) did this, but it is a page of string work and ## this runs it dozens of times: doing it here saves a ## process and most of the time. awk ' { n = split($0, c, "/") top = 0 for (i = 1; i <= n; i++) { if (c[i] == "" || c[i] == ".") { continue } if (c[i] == ".." && top > 0 && out[top] != "..") { top-- continue } out[++top] = c[i] } p = "" for (i = 1; i <= top; i++) { p = p (i > 1 ? "/" : "") out[i] } if (p != "") { print p } } ' | sort } ## Every page there is: what the repository carries, plus what ## po4a writes from it. A translation need not exist yet -- on a ## fresh clone none of them do -- so it is named, not found. universe() { { find src/content -type f -name '*.adoc' page_translations } | normalize | uniq } ## The classifying rules read the masters, which the repository ## always carries; a translation is whatever its master was. homes() { find src/content/*/index.adoc -type f | normalize | only_masters | with_translations | normalize } collections() { grep -l '^:type: collection$' src/content/*/*/index.adoc | normalize | only_masters | xargs -n1 dirname } indexes() { all_collections | sed 's|$|/index.adoc|' | normalize } ## Every collection there is, translations included: the rules ## below emit a listing, a feed and a sortdata list for each, and ## a translated collection needs all three as much as its master. all_collections() { collections | sed 's|$|/index.adoc|' | with_translations | sed 's|/index.adoc$||' | normalize } categories() { grep -l '^:type: categories$' src/content/*/*/*.adoc | normalize | only_masters | with_translations | normalize } articles() { collections | xargs -I% find % -mindepth 3 -name '*.adoc' | normalize | only_masters | with_translations | normalize } slides() { find src/content -type f -name '*.eslaides' | normalize } ## Whatever no other rule claimed, at any depth. pages() { claimed="$(mktemp)" { homes; indexes; categories; articles; } | sort -u > "$claimed" universe | grep -vxF -f "$claimed" rm -f "$claimed" } media() { # src/content/music is excluded: those .ogg files are score # renderings, not published media, and the old site never # offered them over BitTorrent. find src/content/ -path 'src/content/music' -prune -or \ \( \( -type f -and \! -type l \) -and \( \ -name '*.flac' -or \ -name '*.ogg' -or \ -name '*.webm' \ \) -print \) | normalize } ## A glob that matches nothing makes find exit non-zero, which ## under set -e would end the script. There is no music yet. music() { find src/content/music/*.ly 2>/dev/null | normalize || true } tarballs() { find src/content/ \( -type f -and \! -type l \) -and \( \ -name '*.tar.gz' \ \) | normalize } ## Short paths that stand in for a page's own, kept as symlinks ## beside it. Guarded like music() above: a site need not have any, ## and find exits non-zero on a directory that is not there. shortener() { find src/content/l -type l 2>/dev/null | normalize || true } extras() { media tarballs shortener } ## Which pages are one another's translations. po4a already says ## so, set by set, so read it there rather than keep a second list ## that can quietly disagree with the first. ## The languages po4a translates into: whichever .po files exist. langs() { if [ -z "${LANGS_CACHE:-}" ]; then LANGS_CACHE="$( find po -maxdepth 1 -name '*.po' | sed 's|.*/||; s|\.po$||' | sort )" export LANGS_CACHE fi echo "$LANGS_CACHE" } ## One stanza per line, master first, then the files po4a writes ## from it. "$lang:" stands for every language, and "add_$lang:" ## names an addendum rather than an output. po4a_sets() { langs | tr '\n' ' ' | { ## read hits EOF on the last field and reports it, which ## under set -e would end the block before awk ran. read -r all || true awk -v langs="$all" ' /^\[type:/ { set = "" } /^\[type:/ || cont { line = $0 sub(/^\[type:[^]]*\][ \t]*/, "", line) cont = (line ~ /\\$/) sub(/[ \t]*\\$/, "", line) n = split(line, f, /[ \t]+/) nl = split(langs, L, " ") for (i = 1; i <= n; i++) { if (f[i] == "" || f[i] ~ /^add_/) continue if (f[i] ~ /^\$lang:/) { sub(/^\$lang:/, "", f[i]) for (j = 1; j <= nl; j++) { t = f[i] gsub(/\$lang/, L[j], t) set = set " " t } continue } sub(/^[a-z]+:/, "", f[i]) set = set (set == "" ? "" : " ") f[i] } if (!cont && set != "") { print set; set = "" } } ' po/po4a.cfg } } ## Everything po4a writes: every field of a stanza but the first. translations() { cut -d' ' -f2- "$(po4a_cache)" | tr ' ' '\n' | grep . | normalize } ## Just the pages among them. The switcher offers a reader the ## same page in another language; the data files under src/i18n ## are not pages. page_translations() { translations | grep '\.adoc$' } ## Drops the translations from a list of paths, so a rule reads ## the masters whether or not po4a has run. ## po4a.cfg does not change while this runs, so parse it once: ## every selector below asks for the same answer. po4a_cache() { if [ -z "${PO4A_CACHE:-}" ]; then PO4A_CACHE="$(mktemp)" po4a_sets > "$PO4A_CACHE" export PO4A_CACHE fi echo "$PO4A_CACHE" } translations_cache() { if [ -z "${TRANSLATIONS_CACHE:-}" ]; then TRANSLATIONS_CACHE="$(mktemp)" translations > "$TRANSLATIONS_CACHE" export TRANSLATIONS_CACHE fi echo "$TRANSLATIONS_CACHE" } only_masters() { awk 'NR == FNR { t[$0]; next } !($0 in t)' \ "$(translations_cache)" - } ## Reads masters on stdin, writes them and their translations. with_translations() { awk -v sets="$(po4a_cache)" ' BEGIN { while ((getline line < sets) > 0) { n = split(line, f, /[ \t]+/) for (i = 2; i <= n; i++) { t[f[1]] = t[f[1]] " " f[i] } } } { print if ($0 in t) { n = split(t[$0], g, " ") for (i = 1; i <= n; i++) { print g[i] } } } ' } ## Prime the caches here, in this shell. Every pipeline stage ## below runs in a subshell, so a cache filled inside one is gone ## by the next: filling them once, up here, is what makes them ## caches rather than seven identical computations. PO4A_CACHE="$(mktemp)" po4a_sets > "$PO4A_CACHE" TRANSLATIONS_CACHE="$(mktemp)" translations > "$TRANSLATIONS_CACHE" LANGS_CACHE="$(find po -maxdepth 1 -name '*.po' | sed 's|.*/||; s|\.po$||' | sort)" export PO4A_CACHE TRANSLATIONS_CACHE LANGS_CACHE trap 'rm -f "$PO4A_CACHE" "$TRANSLATIONS_CACHE"' EXIT pages | varlist 'pages.adoc' homes | varlist 'homes.adoc' articles | varlist 'articles.adoc' slides | varlist 'slides.eslaides' categories | varlist 'categories.adoc' indexes | varlist 'indexes.adoc' indexes | sed 's|/index\.adoc$|/feed.xml|' | varlist 'feeds.xml' find src/content/img/ -name '*.svg' | sort | varlist 'images.svg' media | varlist 'sources.media' tarballs | varlist 'sources.tarballs' extras | varlist 'sources.extras' find po/*.po po/*.pot | varlist 'sources.po' music | varlist 'music.ly' find src/content/music/*.ogg 2>/dev/null | varlist 'music.ogg' find src/content/music/*.ly.include 2>/dev/null | \ varlist 'music.include' # lilypond emits the score alongside the MIDI, from one run. music | sed 's/^\(.*\)\.ly$/\1.pdf:\t\1.midi/' ## A short link points into the built tree, so on a fresh clone it ## dangles, and make --- which stats through a symlink --- reports no ## rule for a file that is right there. Name the page each one points ## at as what the link is made from. shortener | while read -r link; do target="$(dirname "$link")/$(readlink "$link")" printf '%s:\t%s\n' "$link" \ "$(printf '%s\n' "$target" | normalize)" done ## What this site has, for mkwb to say what each artifact is ## built from. Finding them is the site's half: only it knows ## where its pages live, or that it keeps decks and no music. { pages | sed 's/^/page\t/' homes | sed 's/^/home\t/' articles | sed 's/^/article\t/' indexes | sed 's/^/index\t/' categories | sed 's/^/category\t/' slides | sed 's/^/slide\t/' media | sed 's/^/media\t/' } | mkwb deps src/config.json ## What po4a writes. Naming them here is what lets them be ## derived: deps.mk is written before make runs, so a translation ## that is not named cannot be built, and would have to be ## committed instead. translations | varlist 'translations' ## One run writes all of them, so they hang off a stamp rather ## than each invoking po4a for itself. printf 'src/i18n.stamp: po/po4a.cfg %s\n' "$(find po -maxdepth 1 -name '*.po' | sort | tr '\n' ' ')" printf '\tpo4a po/po4a.cfg\n' printf '\ttouch $@\n\n' printf '$(translations): src/i18n.stamp\n\n' ## The map mkwb conf reads to link a page to its translations, ## written out here so it cannot drift from po4a.cfg. printf '\ntranslations.txt = src/translations.txt\n\n' printf 'src/translations.txt: deps.mk\n' printf '\tprintf %s \\\n' "'%s\\n'" po4a_sets | grep '\.adoc' | sed "s/^/\t\t'/; s/\$/' \\\\/" printf '\t\t> $@\n\n' ## The canonical rules, inlined so no include is needed ## at make time. echo mkwb rules ## Everything mkwb renders reads the site's wording table, which ## po4a writes. The rules above define the variables, so this ## comes after them. It has to name what renders rather than ## what a page is made of: an article never writes a .htmlheader ## of its own, so naming that missed every one of them. printf '\n$(sources.html) $(sources.json): src/i18n.stamp\n' printf '$(listings.htmllisting) $(categories.xml): src/i18n.stamp\n' printf '$(articles.feedentry): src/i18n.stamp\n'