#!/bin/sh # vienna --- a tiny, tasty ssg # by C. Duckworth # shellcheck disable=1090,2030,2031,2035 ### Entry point usage() { cat < USAGE: vienna [-h] vienna [-q] [-c CONFIG] [-C DIR] [-o DIR] [-r URL] [COMMAND...] FLAGS: -h view this help -q don't output any diagnostic information OPTIONS: -c FILE use FILE as configuration. [default: $VIENNA_CONFIG] -C DIR operate in DIR, instead of \$PWD. -o DIR output built site to DIR. [default: ./out] -r URL use URL as basis for urls. [default: example.com] COMMANDS: init initialize a vienna site with minimalist defaults and exit. clean remove output directory before building. preview preview the site locally after building. NOTE: this requires a .vienna.sh file in the site root. you'll have to define a \`preview' function there. publish publish the site after building. NOTE: this requires a .vienna.sh file in the site root. you'll have to define a \`publish' function there. you can redefine any variables or functions vienna uses in \$VIENNA_CONFIG, which by default is ./.vienna.sh. vienna uses heredoc-inspired templating, so you can include shell snippets and variables by doubling the dollar signs. EOF exit "${1:-0}" } configure() { ## Set up environment URL_ROOT="${VIENNA_URL_ROOT:-https://www.example.com}" TEMPDIR="${VIENNA_TEMPDIR:-/tmp/vienna}" WORKDIR="${VIENNA_WORKDIR:-$PWD}" OUTDIR="${VIENNA_OUTDIR:-out}" PLUGINDIR="${VIENNA_PLUGINDIR:-.plugins}" CONFIG="${VIENNA_CONFIG:-./.vienna.sh}" # Templates PAGE_TEMPLATE="${VIENNA_PAGE_TEMPLATE:-.page.tmpl.html}" INDEX_TEMPLATE="${VIENNA_INDEX_TEMPLATE:-.index.tmpl.html}" FEED_TEMPLATE="${VIENNA_FEED_TEMPLATE:-.feed.tmpl.xml}" # Options PHTML_OPTIONS="${VIENNA_PHTML_OPTIONS:-expand entities}" # File extensions RAW_PAGE_EXTENSION="${VIENNA_RAW_PAGE_EXTENSION:-htm}" # Logging LOG=true ## Parse command line arguments while getopts hqC:c:o:r: opt; do case "$opt" in h) usage 0 ;; q) LOG=false ;; C) WORKDIR="$OPTARG" ;; c) CONFIG="$OPTARG" # To error later if a config is specified on the command # line but doesn't exist. CONFIG_ARG=1 ;; o) OUTDIR="$OPTARG" ;; r) URL_ROOT="$OPTARG" ;; *) exit 1 ;; esac done ## Initialize state FILE= ## Cleanup after we're done trap cleanup INT QUIT } main() { # State predicates alias pagep=false indexp=false feedp=false # Convenience aliases alias body=cat title='meta title' pubdate='meta date' # Configure configure "$@" shift "$((OPTIND - 1))" # Prepare cd "$WORKDIR" || exit 2 # Source config if test -f "$CONFIG"; then # Source ./.vienna.sh, if it exists. . "$CONFIG" elif test -n "$CONFIG_ARG"; then # If a -c option was passed on the command line but the file # doesn't exist, that's an error. If we're just looking for the # default file, however, there is no error---the user might want # to use the default configuration. log error "Can't find configuration \`$CONFIG'." exit 2 else print >&2 "I'm not sure this is a \`vienna' site directory." if yornp "Initialize? (y/N)"; then initialize else yornp "Continue building? (y/N)" || exit 2 fi fi # Further argument processing --- pre-build preprocess "$@" || shift log vienna config # Log configuration variables log config 'base url': "$URL_ROOT" log config 'work dir': "$WORKDIR" log config output: "$OUTDIR" log template page: "$PAGE_TEMPLATE" log template index: "$INDEX_TEMPLATE" log template feed: "$FEED_TEMPLATE" # Plugins for plugin in "$PLUGINDIR"/*.sh; do test -f "$plugin" || continue log plugin "$plugin" . "$plugin" done # Prepare output directories mkdir -p "$OUTDIR" || exit 2 mkdir -p "$TEMPDIR" || exit 2 log vienna build # Build pages alias pagep=true genpage *."$RAW_PAGE_EXTENSION" || exit 2 alias pagep=false # Build index alias indexp=true genlist index_item "$INDEX_TEMPLATE" *."$RAW_PAGE_EXTENSION" >"$OUTDIR/index.html" || exit 2 alias indexp=false # Build feed alias feedp=true genlist feed_item "$FEED_TEMPLATE" *."$RAW_PAGE_EXTENSION" >"$OUTDIR/feed.xml" || exit 2 alias feedp=false # Copy static files static * || exit 2 # Further argument processing --- post-build postprocess "$@" } preprocess() { case "${1:-ok}" in ok) ;; init) shift initialize "$@" # exit ;; clean) log vienna clean rm -r "$OUTDIR" cleanup if [ $# -eq 0 ]; then exit # Quit when only cleaning else return 1 # Otherwise, continue processing fi ;; esac } postprocess() { case "${1:-ok}" in ok) ;; publish) log vienna publish publish "$OUTDIR" ;; preview) log vienna preview preview "$OUTDIR" ;; *) log error "Don't know command \`$1'." exit 1 ;; esac } cleanup() { test -z "$DEBUG" && test -z "$NODEP_RM" && rm -r "$TEMPDIR" } _publish() { cat <&2 I want to publish your website but I don't know how. $(if test -f "$CONFIG"; then echo "Edit the" else echo "Write a" fi) \`publish' function in the \`$CONFIG' file in this directory that tells me what to do. EOF exit 3 } publish() { _publish; } _preview() { cat <&2 I want to show you a preview of your website but I don't know how. $(if test -f "$CONFIG"; then echo "Edit the" else echo "Write a" fi) \`preview' function in the \`$CONFIG' file in this directory that tells me what to do. EOF exit 3 } preview() { _preview; } initialize() { # initialize log init "$CONFIG" cat >"$CONFIG" </dev/null 2>&1; # vienna && "$_PYTHON" -m http.server -d "$OUTDIR"' # else # log error "\\\`python' not found." # _preview # fi # } # publish() { # if [ -n "$SERVER_ROOT" ]; then # rsync -avzP --delete "$OUTDIR/" "$SERVER_ROOT/" # else # _publish # fi # } EOF log init "$PAGE_TEMPLATE" cat >"$PAGE_TEMPLATE" <<\EOF $$(title) $$(body) EOF log init "$INDEX_TEMPLATE" cat >"$INDEX_TEMPLATE" <<\EOF a home page!

hey! it's a home page of some sort!

    $$(body)
EOF log init "$FEED_TEMPLATE" cat >"$FEED_TEMPLATE" <<\EOF a feed! $$BASEURL $$(body) EOF exit } ### Utility log() { if "$LOG"; then printf >&2 '[%s] ' "$1" shift printf >&2 "%s\t" "$@" echo >&2 fi } print() { printf '%s\n' "$*" } yornp() { # yornp PROMPT printf >&2 '%s \n' "$@" read -r yn case "$yn" in [Nn]*) return 1 ;; [Yy]*) return 0 ;; *) return 2 ;; esac } ### File processing ## Building block functions shellfix() { # shellfix FILE... ## Replace ` with \`, $ with \$, and $$ with $ # shellcheck disable=2016 sed -E \ -e 's/`/\\`/g' \ -e 's/\$\$\$/\\&/g' \ -e 's/(^|[^\$])\$([^\$]|$)/\1\\$\2/g' \ -e 's/\$\$/$/g' \ "$@" } expand() { # expand TEMPLATE... < INPUT ## Print TEMPLATE to stdout, expanding shell constructs. end="expand_:_${count:=0}_:_end" eval "$( echo "cat<<$end" shellfix "$@" echo echo "$end" )" && count=$((count + 1)) } phtml() { # phtml < INPUT ## Output HTML, pretty much. # Paragraphs unadorned with html tags will be wrapped in

tags, and # &, <, > will be escaped unless prepended with \. Paragraphs where the # first character is < will be left as-is, excepting indentation on the # first line (an implementation detail). case "$PHTML_OPTIONS" in *entities*) _entities='s#([^\\])&#\1\&#g; s#([^\\])<#\1\<#g; s#([^\\])>#\1\>#g; s#\\([&<>])#\1#g;' ;; *) _entities= ;; esac sed -E ' /./ {H;$!d}; x s#^[ \n\t]+([^<].*)#\1# t par; b end :par '"$_entities"' s#.*#

&

# :end s#^[ \n\t]+## $!a ' } meta_init() { # meta_init FILE ## Extract metadata from FILE for later processing. # Metadata should exist as colon-separated data in HTML comments in the # input file. m=false t=false while read -r line; do case "$line" in '') m=false ;; *title:*) t=true && print "$line" ;; *) "$m" && print "$line" ;; esac done <"$1" if ! "$t"; then title="${1##*/}" title="${title%.*}" print "title: ${title%.*}" fi } meta() { # meta FIELD [FILE] ## Extract metadata FIELDS from INPUT. # FILE gives the filename to save metadata to in the $WORKDIR. It # defaults to the current value for $FILE. # # Metadata should exist as colon-separated data in an HTML comment at # the beginning of an input file. sed -n "s/^[ \t]*$1:[ \t]*//p" <"${2:-$META}" } ## Customizable bits filters() { # filters < INPUT ## The filters to run input through. # This is a good candidate for customization in .vienna.sh. phtml | case "$PHTML_OPTIONS" in *expand*) expand ;; *) cat ;; esac } ### Site building genpage() { # genpage PAGE... ## Compile PAGE(s) into $OUTDIR for publication. # Outputs a file of the format $OUTDIR//index.html. test -f "$PAGE_TEMPLATE" || return 1 for FILE; do test -f "$FILE" || continue log genpage "$FILE" outd="$OUTDIR/${FILE%.$RAW_PAGE_EXTENSION}" outf="$outd/index.html" tmpf="$TEMPDIR/$FILE.tmp" META="$TEMPDIR/$FILE.meta" mkdir -p "$outd" meta_init "$FILE" >"$META" filters <"$FILE" >"$tmpf" expand "$PAGE_TEMPLATE" <"$tmpf" >"$outf" done } genlist() { # genlist PERITEM_FUNC TEMPLATE_FILE PAGE... ## Generate a list. peritem_func="$1" template_file="$2" tmpf="$TEMPDIR/$1" shift 2 || return 2 test -f "$template_file" || return 1 printf '%s\n' "$@" | sort_items | while read -r FILE; do test -f "$FILE" || continue log genlist "$peritem_func:" "$FILE" LINK="$URL_ROOT${URL_ROOT:+/}${FILE%.$RAW_PAGE_EXTENSION}" META="$TEMPDIR/$FILE.meta" "$peritem_func" "$FILE" done | expand "$template_file" } sort_items() { # sort_items < ITEMS ## Sort ITEMS separated by newlines. # This function assumes that no ITEM contains a newline. cat } index_item() { # index_item PAGE ## Construct a single item in an index.html. print "
  • $(meta title "$1")
  • " } feed_item() { # feed_item PAGE ## Construct a single item in an RSS feed. date="$(meta date "$1")" cat < $(meta title "$1") $LINK $LINK $(test -n "$date" && print "$date") EOF } static() { # static FILE... ## Copy static FILE(s) to $OUTDIR as-is. # Performs a simple heuristic to determine whether to copy a file or # not. for FILE; do test -f "$FILE" || continue case "$FILE" in .*) continue ;; *.htm) continue ;; "$OUTDIR") continue ;; *) cp -r "$FILE" "$OUTDIR/" ;; esac done } ### Do the thing! test -n "$DEBUG" && set -x test -n "$SOURCE" || main "$@"