By dkl9, written 2023-170, revised 2025-230 (13 revisions)
This is my overengineered static site generator. There are many like it, but this one is mine. If you want to run this for yourself, download the source and run it as a shell script.
getfieldGiven stdin = simplified Recfile, $1 = key, output "key: value" line
getfield() {
FLINE=`grep "^${1}:"`
FLINE="${FLINE#*: }"
echo -n "$FLINE"
}
varsubsGiven $1 = base filename, $2 = simplified Recfile of substitutions, replace $var references in base file with values, and omit $var? references when they match nothing in the Recfile
varsubs() {
OLDIFS="$IFS"
IFS=': '
SCL=""
while read KEY VALUE
do
SCL="${SCL}s@\$${KEY}?\?@${VALUE}@g;"
done <"$2"
grep -o '\$[a-z]\+?' <"$1" | while read VN
do
SCL="${SCL}s@${VN}@@g;"
done
sed -i "$SCL" "$1"
IFS="$OLDIFS"
}
havesourceGiven $1 = sources field, $2, $3, etc = desired sources, succeed iff $2, $3, etc are all in $1
havesource() {
SL="$1"
shift
while [ -n "$1" ]
do
echo "$SL" | grep -Fqw "$1" || return 1
shift
done
}
recfillGiven $1 = Markdown template filename, $2 = simplified Recfile of entries, overwrite $1 with expanded-and-substituted Markdown
recfill() {
REC_PARTS=`mktemp -d`
CUR_REC=0
ON_REC=false
MD_TEMP=`mktemp`
FULL_MD=`mktemp`
echo "${P}splitting rec$R"
while read REC_LINE
do
if [ -n "$REC_LINE" ]
then
if ! "$ON_REC"
then
ON_REC=true
CUR_REC=$((CUR_REC+1))
fi
echo "$REC_LINE" >>"${REC_PARTS}/${CUR_REC}"
else
ON_REC=false
fi
done <"$2"
MAX_REC="$CUR_REC"
CUR_REC=1
echo "${P}processing sections$R"
while [ "$CUR_REC" -le "$MAX_REC" ]
do
cp "$1" "$MD_TEMP"
varsubs "$MD_TEMP" "${REC_PARTS}/${CUR_REC}"
cat "$MD_TEMP" >>"$FULL_MD"
CUR_REC=$((CUR_REC+1))
done
echo "${P}processed$R"
mv "$FULL_MD" "$1"
rm -r "$REC_PARTS" "$MD_TEMP"
}
md2htmlGiven stdin = Markdown, output HTML in the formatting we want
md2html() {
IFS=''
cmark --unsafe | while read -r L
do
if [ "${L#<h?>}" != "$L" ]
then
RH="${L#<h}"
RH="${RH%%>*}"
HT="${L#<h?>}"
HT="${HT%</h?>}"
cf old commonplace (unpublished) #486: normalise the headings for fragment slugs
ID=`echo "$HT" | sed -E 's/<[^>]+>//g' | tr A-Z a-z | tr -cd 'a-z0-9 - -' | tr ' ' '-' | sed 's/--\+/-/g'`
echo "<h${RH}><a name=\"${ID}\" href=\"#${ID}\">§</a> $HT</h${RH}>"
else
echo "$L"
fi
done
}
P=`printf '\x1b[92m'`
Q=`printf '\x1b[91m'`
R=`printf '\x1b[0m'`
For each argument (a document slug), build the document.
while [ -n "$1" ]
do
echo "${P}building $1$R"
TARGETS=`getfield targets <"${1}.."`
TARGETS="${TARGETS} "
SOURCES=`getfield sources <"${1}.."`
For each target (a file extension), build slug.target from the sources, by a procedure that varies depending on the target and source types.
while [ -n "$TARGETS" ]
do
TARGET="${TARGETS%% *}"
TARGETS="${TARGETS#* }"
HF=`getfield header <"${1}.."`
HF="${HF:-header_en}.${TARGET}"
FF=`getfield footer <"${1}.."`
FF="${FF:-footer_en}.${TARGET}"
html from shRun the script slug.sh, saving its standard output to slug.html.
(Special case for build.sh itself.)
if [ "$TARGET" = html ] && havesource "$SOURCES" "sh"
then
echo "${P}sh to html$R"
if [ "$1" = `basename "${0%.*}"` ]
then
cp "$HF" "${1}.html"
varsubs "${1}.html" "${1}.."
ICB=false
IFS=''
while read -r L
do
if [ "$L" = '' ] || [ "${L###!}" != "$L" ]
then
continue
fi
TL=`echo "$L" | sed 's/^ \+//'`
if [ "${TL###}" != "$TL" ] && "$ICB"
then
echo '```'
ICB=false
elif [ "${TL###}" = "$TL" ] && ! "$ICB"
then
echo '```sh'
ICB=true
fi
if "$ICB"
then
echo "$L"
else
echo "${TL### }"
fi
done <"${1}.sh" | tee "${1}.md" | md2html | cat - "$FF" >>"${1}.html"
else
"./${1}.sh" >"${1}.html"
fi
html from ..., rec, mdGenerate a temporary Recfile by merging metadata for all documents.
Then fill in templates from slug.md according to slug.rec and the temporary Recfile, respectively.
Combine all that, converted to HTML, into slug.html.
elif [ "$TARGET" = html ] && havesource "$SOURCES" "..." "rec" "md"
then
echo "${P}... + rec + md to html$R"
cp "$HF" "${1}.html"
varsubs "${1}.html" "${1}.."
K=0
TPLA=`mktemp`
TPLB=`mktemp`
while read SL
do
if [ "$SL" = '---' ]
then
K="$((K+1))"
else
case "$K" in
0) echo "$SL" ;;
1) echo "$SL" >>"$TPLA" ;;
2) echo "$SL" >>"$TPLB" ;;
esac
fi
done <"${1}.md" | md2html >>"${1}.html"
recfill "$TPLA" "${1}.rec"
md2html <"$TPLA" >>"${1}.html"
RECB=`mktemp`
for A in *..
do
if [ -z `getfield unlisted <"$A"` ]
then
echo -e "$(getfield pub_date <"$A")\t$A"
fi
done | sort -r | cut -f 2 | while read B
do
echo "slug: ${B%..}"
cat "$B"
echo
done >"$RECB"
recfill "$TPLB" "$RECB"
echo -e '<hr>\n<ul>' >>"${1}.html"
md2html <"$TPLB" >>"${1}.html"
rm "$TPLA" "$TPLB" "$RECB"
echo -e '</ul>\n</nav>' >>"${1}.html"
cat "$FF" >>"${1}.html"
html from rec, mdTake the part of slug.md before --- as a one-time introduction.
Take the part after --- as a template to be filled in with values from each record in slug.rec.
Combine all that, converted to HTML, into slug.html.
elif [ "$TARGET" = html ] && havesource "$SOURCES" "rec" "md"
then
echo "${P}rec + md to html$R"
cp "$HF" "${1}.html"
varsubs "${1}.html" "${1}.."
sed '/^---$/q' <"${1}.md" | md2html >>"${1}.html"
TEMPLATE=`mktemp`
sed '1,/^---$/d' <"${1}.md" >"$TEMPLATE"
recfill "$TEMPLATE" "${1}.rec"
md2html <"$TEMPLATE" >>"${1}.html"
rm "$TEMPLATE"
cat "$FF" >>"${1}.html"
html from mdConvert Markdown (slug.md) to HTML with the CommonMark cmark utility.
elif [ "$TARGET" = html ] && havesource "$SOURCES" "md"
then
echo "${P}md to html$R"
cp "$HF" "${1}.html"
varsubs "${1}.html" "${1}.."
md2html <"${1}.md" >>"${1}.html"
cat "$FF" >>"${1}.html"
pdf from mdConvert Markdown to PDF with cmark and groff.
elif [ "$TARGET" = pdf ] && havesource "$SOURCES" "md"
then
echo "${P}md to pdf$R"
MS_FILE=`mktemp`
cp "${HF%.pdf}.ms" "$MS_FILE"
varsubs "$MS_FILE" "${1}.."
sed 's/^##/#/' <"${1}.md" | \
cmark -t man | \
sed 's/^\.SH$/.SH 2/; s/^\.SS$/.SH 3/; s/^\.PP$/.LP/' >>"$MS_FILE"
cat "${FF%.pdf}.ms" >>"$MS_FILE"
groff -D utf8 -T pdf -ms <"$MS_FILE" >"${1}.pdf"
rm "$MS_FILE"
atom from ..., xmlGenerate a temporary Recfile by merging metadata for all documents.
Then fill in templates from slug.xml according to the temporary Recfile.
Combine those filled templates with a header and footer into slug.atom.
elif [ "$TARGET" = atom ] && havesource "$SOURCES" "xml"
then
echo "${P}... + xml to atom$R"
cp "$HF" "${1}.atom"
varsubs "${1}.atom" "${1}.."
K=0
TPLA=`mktemp`
cp "${1}.xml" "$TPLA"
RECB=`mktemp`
for A in *..
do
if [ -z `getfield unlisted <"$A"` ]
then
echo -e "$(getfield pub_date <"$A")\t$A"
fi
done | sort -r | cut -f 2 | while read B
do
echo "slug: ${B%..}"
cat "$B"
echo
done >"$RECB"
recfill "$TPLA" "$RECB"
cat "$TPLA" >>"${1}.atom"
rm "$TPLA" "$RECB"
cat "$FF" >>"${1}.atom"
else
echo "${Q}can't build to target '$TARGET'$R"
fi
done
shift
done