commit b77b250a6fbc0ecf95bf91dff9ec25f6a5c4260b from: roman zolotarev date: Sun Jan 11 20:32:57 2026 UTC init commit - /dev/null commit + b77b250a6fbc0ecf95bf91dff9ec25f6a5c4260b blob - /dev/null blob + 042c10d5233cd0e9cb438930d675a1e979417de0 (mode 644) --- /dev/null +++ readme.md @@ -0,0 +1 @@ +https://romanzolotarev.com/ssg/ blob - /dev/null blob + a785d7f2fd619836522e66ce6d8d616c7fc05ed4 (mode 755) --- /dev/null +++ ssg.sh @@ -0,0 +1,399 @@ +#!/bin/ksh -eu + +# https://romanzolotarev.com/ssg/ +# copyright 2018-2026 romanzolotarev.com +# +# permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# the software is provided "as is" and the author disclaims all warranties with +# regard to this software including all implied warranties of merchantability +# and fitness. in no event shall the author be liable for any special, direct, +# indirect, or consequential damages or any damages whatsoever resulting from +# loss of use, data or profits, whether in an action of contract, negligence or +# other tortious action, arising out of or in connection with the use or +# performance of this software. + +info() { echo "$@" >&2; } +fail() { echo "$@" >&2 && exit 1; } +usage() { fail 'usage: '"${0##*/}"' '; } + +# exit if less than two arguments +fail_no_args() { if test $# -ne 2; then usage; fi; } + +# exit if src directory not found +fail_no_src() { if ! test -d "$1"; then fail "fail: $1 not found"; fi; } + +# return relative paths sorted +sort_relative() { sed "s,$1/,," | sort; } + +# return sorted file hashes for directory +hash_dir() { + dir="$1" + if ! test -d "$dir"; then return; fi + set -- find "$dir" -type f + while read -r line; do + if test -z "$line"; then continue; fi + set -f && for word in $line; do + if test -z "$word"; then continue; fi && set -- "$@" "$word" + done && set +f + done + "$@" -print0 | xargs -r -0 -n256 -P "$NCPU" sha256 -r | sort_relative "$dir" +} + +# return find expression to exclude paths +not_path() { + while read -r x; do + case "$x" in + */) echo "! -path $x*" ;; + *) echo "! -path $x" ;; + esac + done +} + +# return all .ssg.ignore files +find_ignore_files() { find "$SRC" -type f -name "$SSG_IGNORE"; } + +# return ignored paths from all .ssg.ignore files +ignored_paths() { + while read -r f; do d="$(dirname "$f")" && sed "s,^,${d}/," "$f"; done +} + +# return src directory hash, excluding paths from .ssg.ignore +hash_src() { find_ignore_files | ignored_paths | not_path | hash_dir "$SRC"; } + +# return find expression to exclude src and dst hash files +exclude_hash_files() { echo "! -name $SSG_DST ! -name $SSG_SRC"; } + +# return dst directory hash, excluding hash files +hash_dst() { exclude_hash_files | hash_dir "$DST"; } + +mustache() { + d=0 + s=-1 + while read -r line; do + o="" + rest="$line" + while :; do + case "$rest" in + *'{{'*) + before="${rest%%\{\{*}" + rest="${rest#*\{\{}" + tag="${rest%%\}\}*}" + rest="${rest#*\}\}}" + if test $s -lt 0; then o="${o}${before}"; fi + case "$tag" in + '#'*) + d=$((d + 1)) + if test $s -lt 0 -a -z "$(printenv "${tag#\#}")"; then s=$d; fi + ;; + '/'*) if test $s -eq $d; then s=-1; fi && d=$((d - 1)) ;; + '^'*) + d=$((d + 1)) + if test $s -lt 0 -a -n "$(printenv "${tag#^}")"; then s=$d; fi + ;; + *) if test $s -lt 0; then o="${o}$(printenv "$tag")"; fi ;; + esac + ;; + *) if test $s -lt 0; then o="${o}${rest}"; fi && break ;; + esac + done + if test $s -lt 0 -o -n "$o"; then printf '%s\n' "$o"; fi + done +} + +# returns page rendered with its template +render_page() { + # strip newlines and escape ampersands and slashes + esc() { tr -d '\n' | sed 's/[&/]/\\&/g'; } + # replace newlines with spaces and extract title from the first

tag + get_title() { tr '\n' ' ' | + sed -n 's/^[^<]*<[Hh]1[^>]*>\([^<]*\)<\/[Hh]1[^>]*>.*/\1/p'; } + content="$(cat)" + # use src dir name as site name + site="$(basename "$SRC" | esc)" + title="$(printf '%s' "$content" | get_title | esc)" + # replace {{title}} and {{site}} tags with values from variables, + # replace {{content}} tag with page content. + # use truthy tag to show title only when it's found in content, for example: + # {{#title}}{{title}: {{/title}} + export content site title && mustache <"$1" +} + +# return html converted from markdown +md_to_html() { + lowdown \ + --out-no-smarty \ + --html-no-escapehtml \ + --html-no-skiphtml \ + --parse-no-autolink \ + --parse-no-metadata +} + +# write zip unless its gz variant found in src +gz() { + if test -f "$SRC/$1.gz"; then return; fi + gzip -n -9 <"$DST/$1" >"$DST/$1.gz" +} + +# return nearest template for page +find_template() { + dir=$(cd "$(dirname "$SRC/$1")" && pwd) + root=$(dirname "$(cd "$SRC" && pwd)") + while test "$dir" != "$root"; do + t="$dir/$SSG_TEMPLATE" + if test -f "$t"; then echo "${t#"$SRC/"}" && return; fi + dir=$(dirname "$dir") + done +} + +# return relative paths to files if they found in directory +files_in() { + while read -r f; do if test -f "$1/$f"; then echo "$f"; fi; done +} + +# write and zip file to dst +generate_file() { + mkdir -p "$(dirname "$DST/$1")" + cp "$SRC/$1" "$DST/$1" + info "file $1" + gz "$1" + info "file $1 > $1.gz" +} + +# write file to dst +generate_copy() { + mkdir -p "$(dirname "$DST/$1")" + cp "$SRC/$1" "$DST/$1" + info "copy $1" +} + +# execute script and write zips for output files to dst +generate_sh() { + command ksh -- "$SRC/$1" "$SRC" "$DST" | + while read -r f; do + if test -z "$f"; then continue; fi + if test -f "$SRC/$f"; then fail "fail: $1 collides with $f"; fi + info "sh $1 > $f" + gz "$f" + info "sh $1 > $f.gz" + done +} + +# return true page has tag +has_html_tag() { grep -qi ']*>' "$SRC/$1"; } + +# write html page and zip to dst +generate_html() { + mkdir -p "$(dirname "$DST/$1")" + + # return content as is for pages with tag + if has_html_tag "$1"; then + cp "$SRC/$1" "$DST/$1" + info "html $1" + gz "$1" + info "html $1 > $1.gz" + return + fi + + # find template + t=$(find_template "$1") + + if test -f "$SRC/$t"; then + # return page rendered with template + render_page "$SRC/$t" <"$SRC/$1" >"$DST/$1" + info "html $1, $t > $1" + gz "$1" + info "html $1, $t > $1.gz" + else + # ...or return content as is if template not found + cp "$SRC/$1" "$DST/$1" + info "html $1" + gz "$1" + info "html $1 > $1.gz" + fi +} + +# write markdown page and zip to dst +generate_md() { + mkdir -p "$(dirname "$DST/$1")" + t=$(find_template "$1") + h="${1%.md}.html" + if test -f "$SRC/$h"; then fail "fail: $1 collides with $h"; fi + if test -f "$SRC/$t"; then + md_to_html <"$SRC/$1" | render_page "$SRC/$t" >"$DST/$h" + info "md $1, $t > $h" + gz "$h" + info "md $1, $t > $h.gz" + else + md_to_html <"$SRC/$1" >"$DST/$h" + info "md $1 > $h" + gz "$h" + info "md $1 > $h.gz" + fi +} + +# return diff of two values +diff_lines() { + fifo=$(mktemp -u) || fail 'fail: diff lines: can not mktemp' + mkfifo "$fifo" || fail 'fail: diff lines: can not mkfifo' + (printf '%s\n' "$2" >"$fifo") & + printf '%s\n' "$1" | diff - "$fifo" || : + rm -f "$fifo" +} + +# return second column and sort +cut_sort() { cut -d' ' -f2 | sort; } + +# return pages with their templates, excluding pages with tag +pages_with_templates() { + while read -r p; do + if has_html_tag "$p"; then continue; fi && + printf '%s\t%s\n' "$(find_template "$p")" "$p" + done +} + +# return pages related to template +pages_by_templates() { + grep "$SSG_TEMPLATE" | cut_sort | + while read -r t; do echo "$1" | grep "$t" | cut -f2; done +} + +# return file expected in dst directory +plan() { + while read -r k f; do + case "$k" in + copy) echo "$f" ;; + file) echo "$f" && echo "$f.gz" ;; + html) echo "$f" && echo "$f.gz" ;; + md) echo "${f%.md}.html" && echo "${f%.md}.html.gz" ;; + sh) command ksh -- "$SRC/$f" | + while read -r f; do + if test -z "$f"; then continue; fi && echo "$f" && echo "$f.gz" + done ;; + *) continue ;; + esac + done +} + +# make dst directory and return src hash as is +mkdir_select_all() { mkdir -p "$DST" && echo "$1" | cut_sort; } + +# remove dst directory and return src hash as is +rmdir_select_all() { rm -rf "$DST" && echo "$1" | cut_sort; } + +# remove files in dst +rm_files() { while read -r f; do rm "$DST/$f" && info "rm $f"; done; } + +# remove empty directories in dst +rm_empty_dirs() { + find "$DST" -type d -mindepth 1 | while read -r d; do + if test -z "$(find "$d" -mindepth 1 -type f)"; then echo "$d"; fi + done | sort -r | while read -r e; do + rmdir "$e" && info "rmdir ${e#"$DST"/}/" + done +} + +# return files with their kind prepended +prepend_kind() { + while read -r f; do + if test -z "$f"; then continue; fi + case "$f" in + *.html) k='html' ;; + *.md) k='md' ;; + *.ssg.ignore) k='ignore' ;; + *.ssg.*.sh | *.ssg.sh) k='sh' ;; + *.ssg.template) k='template' ;; + *.png | *.jpg | *.gif | *.mp4 | *.zip | *.gz) k='copy' ;; + *) k='file' ;; + esac + echo "$k $f" + done +} + +# return right side of diff +select_right() { sed -n 's/^> \([^\ ]*\).*/\1/p'; } + +# remove files and directories not present in plan from dst +clean_up_dst() { + dst_plan=$(echo "$1" | cut_sort | prepend_kind | plan | sort) + dst_files=$(echo "$2" | sort_relative "$DST" | cut_sort) + diff_lines "$dst_plan" "$dst_files" | select_right | files_in "$DST" | + rm_files + rm_empty_dirs +} + +# return sorted list of unique updated files and pages for updated template +select_updated() { + src_updated=$(echo "$2" | select_right | cut_sort) + pages=$(echo "$1" | grep -E '.html|.md' | cut_sort | pages_with_templates) + { + echo "$src_updated" + echo "$src_updated" | pages_by_templates "$pages" + } | sort -u +} + +is_empty() { test -z "$1"; } +is_dir() { test -d "$1"; } +is_ssg_dst() { test -f "$DST/$SSG_DST"; } +is_ssg_src() { test -f "$DST/$SSG_SRC"; } +is_matching_ssg_dst() { + test "$(sha256 <"$DST/$SSG_DST")" = "$(echo "$1" | sha256)" +} +diff_src() { diff_lines "$(cat "$DST/$SSG_SRC")" "$1"; } + +# return files to be updated +select_src_files() { + if is_empty "$1"; then return; fi + if ! is_dir "$DST"; then mkdir_select_all "$1" && return; fi + if ! is_ssg_src || ! is_ssg_dst; then rmdir_select_all "$1" && return; fi + dst_hash=$(hash_dst) + if ! is_matching_ssg_dst "$dst_hash"; then rmdir_select_all "$1" && return; fi + src_hash_diff=$(diff_src "$1") + if is_empty "$src_hash_diff"; then return; fi + clean_up_dst "$src_hash" "$dst_hash" + select_updated "$src_hash" "$src_hash_diff" +} + +# write files in dst directory +generate() { + while read -r k f; do + case "$k" in + copy) generate_copy "$f" ;; + file) generate_file "$f" ;; + html) generate_html "$f" ;; + md) generate_md "$f" ;; + sh) generate_sh "$f" ;; + template) info "template $f" ;; + ignore) info "ignore $f" ;; + *) info "unknown $f" ;; + esac + done +} + +# write src and dst hash files to dst directory +write_hashes() { + if ! test -d "$DST"; then return; fi + echo "$1" >"$DST/$SSG_SRC" + echo "$2" | tee "$DST/$SSG_DST" | sha256 >&2 +} + +main() { + fail_no_args "${@}" + fail_no_src "${@}" + + SRC=$(cd "$1" && pwd) + DST="$2" + SSG_IGNORE='.ssg.ignore' + SSG_TEMPLATE='.ssg.template' + SSG_SRC='.ssg.src' + SSG_DST='.ssg.dst' + NCPU=$(sysctl -n hw.ncpu 2>/dev/null || getconf NPROCESSORS_ONLN) + + src_hash=$(hash_src) + select_src_files "$src_hash" | prepend_kind | generate + write_hashes "$src_hash" "$(hash_dst)" +} + +main "${@}" blob - /dev/null blob + 81b21fe0ed41b084f56c25688252b3e98b8f95b8 (mode 755) --- /dev/null +++ ssg.test.sh @@ -0,0 +1,636 @@ +#!/bin/ksh -e + +ok_count=0 +ok_expected=24 + +plan() { + echo "$ok_expected..$ok_count" + test "$ok_expected" -eq "$ok_count" || { echo 'failed' && exit 1; } + echo 'passed' && exit 0 +} + +bench() { + i="$1" && shift + exec 3>&1 + { + time ( + n=1 && printf . >&3 + while [ "$n" -le "$i" ]; do + "$@" >/dev/null 2>&1 && n=$((n + 1)) + printf . >&3 + done + printf '\n' >&3 + ) + } 2>&1 | grep 'real' | cut -f2 | cut -d' ' -f5 + exec 3>&- +} + +ok() { echo "ok: $*" && ok_count=$((ok_count + 1)); } +not_ok() { echo "not ok: $*" && plan; } + +not_ok_diff_n() { + fifo=$(mktemp -u) || exit 1 + mkfifo "$fifo" || exit 1 + (printf "%s" "$2" >"$fifo") & + printf "\n%s\n" "$(cat)" | diff - "$fifo" || not_ok "$1" + rm -f "$fifo" +} + +not_ok_diff() { + fifo=$(mktemp -u) || exit 1 + mkfifo "$fifo" || exit 1 + (echo "$2" >"$fifo") & + diff - "$fifo" || not_ok "$1" + rm -f "$fifo" +} + +not_ok_find() { + find "$1" -type f | sort | sed "s,$1/,," | not_ok_diff_n "$2" "$3" +} + +base=$(dirname "$0") +cmd="$base/ssg.sh" +test -x "$cmd" || { echo "$cmd not found" >&2 && exit 1; } + +basic_case() { + dir=$(mktemp -d) + src="$dir/src" && dst="$dir/dst" + mkdir "$src" "$src/.git" + echo '# h1' >"$src/markdown.md" + echo '

h1

' >"$src/html1.html" + echo '' >"$src/html2.html" + echo '{{title}}:{{site}}{{content}}' >"$src/.ssg.template" + echo '.git/' >"$src/.ssg.ignore" + echo >"$src/.git/index" + echo >"$src/main.css" + echo >"$src/logo.png" + # shellcheck disable=2016 + echo ' +if test -z "$1"; then echo "x.txt" && exit; fi +echo . >"$2/x.txt" && echo "x.txt" +' >"$src/.ssg.sh" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "basic_case" ' +ignore .ssg.ignore +sh .ssg.sh > x.txt +sh .ssg.sh > x.txt.gz +template .ssg.template +html html1.html, .ssg.template > html1.html +html html1.html, .ssg.template > html1.html.gz +html html2.html +html html2.html > html2.html.gz +copy logo.png +file main.css +file main.css > main.css.gz +md markdown.md, .ssg.template > markdown.html +md markdown.md, .ssg.template > markdown.html.gz +56fd1f7d1f2bcbe8b452073cc657b27a4eee72cab0e531de679788e5744af652 +' + rm -rf "$dir" +} + +t() { + dir=$(mktemp -d) && src="$dir/src" && dst="$dir/dst" + case "$1" in + + fail_no_args) + if "$cmd" "$src" 2>/dev/null; then + exit_code="$?" && test "$exit_code" -eq 1 || + not_ok "$1: expected: exit code 1, actual: exit code $exit_code" + fi + test -d "$dst" && not_ok "$1" + ;; + + fail_no_src) + if "$cmd" "$src" "$dst" 2>/dev/null; then + exit_code="$?" && test "$exit_code" -eq 1 || + not_ok "$1: expected: exit code 1, actual: exit code $exit_code" + fi + test -d "$dst" && not_ok "$1" + ;; + + select_src_files_empty_src) + mkdir "$src" "$dst" && "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1" ' +01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b +' + not_ok_find "$dst" "$1: dst not empty" ' +.ssg.dst +.ssg.src +' + ;; + + select_src_files_ssg_ignore) + mkdir "$src" "$src/a" "$src/a/b" "$src/a/c" + echo '1.txt' >"$src/.ssg.ignore" + echo '2.txt +c/' >"$src/a/.ssg.ignore" + echo '3.txt' >"$src/a/b/.ssg.ignore" + echo >"$src/1.txt" + echo >"$src/a/2.txt" + echo >"$src/a/b/3.txt" + echo >"$src/a/c/4.txt" + echo >"$src/a/c/5.txt" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1" ' +ignore .ssg.ignore +ignore a/.ssg.ignore +ignore a/b/.ssg.ignore +01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +' + ;; + + select_src_files_no_dst) + mkdir "$src" && echo >"$src/t.png" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1" ' +copy t.png +9db7b136bc6fdd9c51009ce2f88c69ff64060c3f3ff540a9199f37d2aa404eaa +' + not_ok_find "$dst" "$1: dst has t.png" ' +.ssg.dst +.ssg.src +t.png +' + ;; + + select_src_files_no_ssg_dst_ssg_src) + mkdir "$src" "$dst" && echo >"$src/t.png" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +copy t.png +9db7b136bc6fdd9c51009ce2f88c69ff64060c3f3ff540a9199f37d2aa404eaa +' + rm "$dst/.ssg.src" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +copy t.png +9db7b136bc6fdd9c51009ce2f88c69ff64060c3f3ff540a9199f37d2aa404eaa +' + rm "$dst/.ssg.dst" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: third run" ' +copy t.png +9db7b136bc6fdd9c51009ce2f88c69ff64060c3f3ff540a9199f37d2aa404eaa +' + ;; + + select_src_files_no_dst_ssg_dst_match) + mkdir "$src" "$dst" && echo >"$src/t.png" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +copy t.png +9db7b136bc6fdd9c51009ce2f88c69ff64060c3f3ff540a9199f37d2aa404eaa +' + echo x >>"$dst/.ssg.dst" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +copy t.png +9db7b136bc6fdd9c51009ce2f88c69ff64060c3f3ff540a9199f37d2aa404eaa +' + ;; + + select_src_files_no_src_diff) + mkdir "$src" "$dst" && echo >"$src/t.png" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +copy t.png +9db7b136bc6fdd9c51009ce2f88c69ff64060c3f3ff540a9199f37d2aa404eaa +' + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +9db7b136bc6fdd9c51009ce2f88c69ff64060c3f3ff540a9199f37d2aa404eaa +' + ;; + + select_src_files_clean_dst) + mkdir "$src" && echo >"$src/t.png" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +copy t.png +9db7b136bc6fdd9c51009ce2f88c69ff64060c3f3ff540a9199f37d2aa404eaa +' + echo >"$dst/trash_file" + mkdir "$dst/trash_dir" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +copy t.png +9db7b136bc6fdd9c51009ce2f88c69ff64060c3f3ff540a9199f37d2aa404eaa +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +t.png +' + ;; + + select_src_files_clean_dst_dir) + mkdir "$src" "$src/dir" + echo >"$src/a.png" + echo >"$src/dir/b.png" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +copy a.png +copy dir/b.png +6e0b941542f81e1b299a21444d0efe2fa224a4220e67df9c37cc34a2c6f01b13 +' + rm "$src/dir/b.png" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +rm dir/b.png +rmdir dir/ +e86615a87eeeae97fb6302dd5013109f0ccfb7336f164a39457e684c30bae90e +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +a.png +' + ;; + + select_updated) + mkdir "$src" "$dst" + echo '' >"$src/.ssg.template" + echo '

h1

' >"$src/html1.html" + echo '' >"$src/html2.html" + echo '# h1' >"$src/markdown.md" + echo >"$src/t.png" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +template .ssg.template +html html1.html, .ssg.template > html1.html +html html1.html, .ssg.template > html1.html.gz +html html2.html +html html2.html > html2.html.gz +md markdown.md, .ssg.template > markdown.html +md markdown.md, .ssg.template > markdown.html.gz +copy t.png +5538af7d3a6abc2aad8609b32f32154ea5d23109e2630399fecd0755ebde544f +' + expected_dst=' +.ssg.dst +.ssg.src +html1.html +html1.html.gz +html2.html +html2.html.gz +markdown.html +markdown.html.gz +t.png +' + not_ok_find "$dst" "$1" "$expected_dst" + + echo 'x' >"$src/.ssg.template" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +template .ssg.template +html html1.html, .ssg.template > html1.html +html html1.html, .ssg.template > html1.html.gz +md markdown.md, .ssg.template > markdown.html +md markdown.md, .ssg.template > markdown.html.gz +c3f5b69d372ad66211a052e7b572715941522aae7779eff22cf7becc1c8d18e8 +' + not_ok_find "$dst" "$1" "$expected_dst" + ;; + + generate_copy) + mkdir "$src" "$dst" && echo 'png' >"$src/t.png" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +copy t.png +5265fde36fa46d08d2bc48d0f413d41c166ee966a4f94b5fd7ad0c23e1bb92d4 +' + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +5265fde36fa46d08d2bc48d0f413d41c166ee966a4f94b5fd7ad0c23e1bb92d4 +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +t.png +' + cat "$dst/t.png" | not_ok_diff "$1" 'png' + ;; + + generate_file) + mkdir "$src" "$dst" && echo 'txt' >"$src/t.txt" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +file t.txt +file t.txt > t.txt.gz +482d02d3fdd5ca854ffc9370f9cf3d4efa5bb640713c90dcf5c9800d5acf6812 +' + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +482d02d3fdd5ca854ffc9370f9cf3d4efa5bb640713c90dcf5c9800d5acf6812 +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +t.txt +t.txt.gz +' + cat "$dst/t.txt" | not_ok_diff "$1" 'txt' + hexdump -C "$dst/t.txt.gz" | not_ok_diff_n "$1" ' +00000000 1f 8b 08 00 00 00 00 00 02 03 2b a9 28 e1 02 00 |..........+.(...| +00000010 d3 84 7d 34 04 00 00 00 |..}4....| +00000018 +' + ;; + + generate_html) + mkdir "$src" "$dst" && echo '' >"$src/h.html" + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +html h.html +html h.html > h.html.gz +d167244df661961bfe78dd5e1b2c8c563cd588f3583a6438f2af3207401fb10c +' + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +d167244df661961bfe78dd5e1b2c8c563cd588f3583a6438f2af3207401fb10c +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +h.html +h.html.gz +' + cat "$dst/h.html" | not_ok_diff "$1" '' + hexdump -C "$dst/h.html.gz" | not_ok_diff_n "$1" ' +00000000 1f 8b 08 00 00 00 00 00 02 03 b3 c9 28 c9 cd b1 |............(...| +00000010 e3 02 00 99 34 cb 33 07 00 00 00 |....4.3....| +0000001b +' + ;; + + generate_html_with_template) + mkdir "$src" "$dst" + echo '

h1

' >"$src/h.html" + echo '{{title}}~{{site}}{{content}}' >"$src/.ssg.template" + + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +template .ssg.template +html h.html, .ssg.template > h.html +html h.html, .ssg.template > h.html.gz +7cbe380c112e232fa4b618b1837d11c47abdfd01863ae484fdb411ade0f43af1 +' + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +7cbe380c112e232fa4b618b1837d11c47abdfd01863ae484fdb411ade0f43af1 +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +h.html +h.html.gz +' + + cat "$dst/h.html" | not_ok_diff "$1" 'h1~src

h1

' + hexdump -C "$dst/h.html.gz" | not_ok_diff_n "$1" ' +00000000 1f 8b 08 00 00 00 00 00 02 03 b3 29 c9 2c c9 49 |...........).,.I| +00000010 b5 cb 30 ac 2b 2e 4a b6 d1 87 f0 6c 32 0c 81 22 |..0.+.J....l2.."| +00000020 36 fa 40 8a 0b 00 1e b5 6d 4c 21 00 00 00 |6.@.....mL!...| +0000002e +' + ;; + + generate_html_with_template_no_title) + mkdir "$src" "$dst" + echo '

h1

' >"$src/h.html" + echo 'p' >"$src/p.html" + echo '{{#title}}{{title}}: {{/title}}{{site}}{{content}}' >"$src/.ssg.template" + + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +template .ssg.template +html h.html, .ssg.template > h.html +html h.html, .ssg.template > h.html.gz +html p.html, .ssg.template > p.html +html p.html, .ssg.template > p.html.gz +816fcd36b20ed6114d6e13c15182ae33186676aa091558dce61f66c8e43c8b62 +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +h.html +h.html.gz +p.html +p.html.gz +' + + cat "$dst/h.html" | not_ok_diff "$1" 'h1: src

h1

' + cat "$dst/p.html" | not_ok_diff "$1" 'srcp' + ;; + + + generate_html_with_template_in_dir) + mkdir "$src" "$src/dir" + echo >"$src/h1.html" + echo >"$src/dir/h2.html" + echo '/' >"$src/.ssg.template" + echo '/dir' >"$src/dir/.ssg.template" + + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +template .ssg.template +template dir/.ssg.template +html dir/h2.html, dir/.ssg.template > dir/h2.html +html dir/h2.html, dir/.ssg.template > dir/h2.html.gz +html h1.html, .ssg.template > h1.html +html h1.html, .ssg.template > h1.html.gz +8602b68b3b149d967e43d9c4948099d4cbbc3edd2f0a5e46f01c37af78ba8506 +' + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +8602b68b3b149d967e43d9c4948099d4cbbc3edd2f0a5e46f01c37af78ba8506 +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +dir/h2.html +dir/h2.html.gz +h1.html +h1.html.gz +' + + cat "$dst/h1.html" | not_ok_diff "$1" '/' + cat "$dst/dir/h2.html" | not_ok_diff "$1" '/dir' + ;; + + generate_html_template_not_found) + mkdir "$src" "$dst" + echo '

h1

' >"$src/h.html" + + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +html h.html +html h.html > h.html.gz +255d20dac0c5587bd5499827d3528db1433d60c04168ca2d9b2427de0f9a440e +' + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +255d20dac0c5587bd5499827d3528db1433d60c04168ca2d9b2427de0f9a440e +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +h.html +h.html.gz +' + + cat "$dst/h.html" | not_ok_diff "$1" '

h1

' + hexdump -C "$dst/h.html.gz" | not_ok_diff_n "$1" ' +00000000 1f 8b 08 00 00 00 00 00 02 03 b3 c9 30 b4 cb 30 |............0..0| +00000010 b4 d1 07 52 5c 00 12 f0 3b a6 0c 00 00 00 |...R\...;.....| +0000001e +' + ;; + + generate_md_with_collision) + mkdir "$src" "$dst" + echo >"$src/h.md" + echo >"$src/h.html" + + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +html h.html +html h.html > h.html.gz +fail: h.md collides with h.html +' + ;; + + generate_md_with_template) + mkdir "$src" "$dst" + echo '# h1' >"$src/h.md" + echo '{{title}}~{{site}}{{content}}' >"$src/.ssg.template" + + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +template .ssg.template +md h.md, .ssg.template > h.html +md h.md, .ssg.template > h.html.gz +916917ec6944394281526a5ba9276e39bdff828105b965188c168c847065be63 +' + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +916917ec6944394281526a5ba9276e39bdff828105b965188c168c847065be63 +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +h.html +h.html.gz +' + + cat "$dst/h.html" | + not_ok_diff "$1" 'h1~src

h1

' + hexdump -C "$dst/h.html.gz" | not_ok_diff_n "$1" ' +00000000 1f 8b 08 00 00 00 00 00 02 03 b3 29 c9 2c c9 49 |...........).,.I| +00000010 b5 cb 30 ac 2b 2e 4a b6 d1 87 f0 6c 32 0c 15 32 |..0.+.J....l2..2| +00000020 53 6c 95 32 0c 95 80 32 36 fa 19 86 76 5c 00 0b |Sl.2...26...v\..| +00000030 26 40 c1 29 00 00 00 |&@.)...| +00000037 +' + ;; + + generate_md_template_not_found) + mkdir "$src" "$dst" + echo '# h1' >"$src/h.md" + + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +md h.md > h.html +md h.md > h.html.gz +2f33f1e74aa7d6f33502cc842f2001a7759da120f0fb0f84a6295c1fe81d0319 +' + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +2f33f1e74aa7d6f33502cc842f2001a7759da120f0fb0f84a6295c1fe81d0319 +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +h.html +h.html.gz +' + + cat "$dst/h.html" | not_ok_diff "$1" '

h1

' + hexdump -C "$dst/h.html.gz" | not_ok_diff_n "$1" ' +00000000 1f 8b 08 00 00 00 00 00 02 03 b3 c9 30 54 c8 4c |............0T.L| +00000010 b1 55 ca 30 54 b2 cb 30 b4 d1 cf 30 b4 e3 02 00 |.U.0T..0...0....| +00000020 0e 5d 6f 38 14 00 00 00 |.]o8....| +00000028 +' + ;; + + generate_sh) + mkdir "$src" + # shellcheck disable=2016 + echo ' +if test -z "$1"; then echo "x.txt" && exit; fi +echo . >"$2/x.txt" && echo "x.txt" +' >"$src/.ssg.sh" + + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +sh .ssg.sh > x.txt +sh .ssg.sh > x.txt.gz +99c418b0dcd6c6c2124e87b4857b415bcf0a12ba7c7540d8ac53fe73c2046a29 +' + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: second run" ' +99c418b0dcd6c6c2124e87b4857b415bcf0a12ba7c7540d8ac53fe73c2046a29 +' + not_ok_find "$dst" "$1" ' +.ssg.dst +.ssg.src +x.txt +x.txt.gz +' + + cat "$dst/x.txt" | not_ok_diff "$1" '.' + hexdump -C "$dst/x.txt.gz" | not_ok_diff_n "$1" ' +00000000 1f 8b 08 00 00 00 00 00 02 03 d3 e3 02 00 cd f2 |................| +00000010 0b aa 02 00 00 00 |......| +00000016 +' + ;; + + generate_sh_with_collision) + mkdir "$src" + echo >"$src/x.txt" + # shellcheck disable=2016 + echo ' +if test -z "$1"; then echo "x.txt" && exit; fi +echo . >"$2/x.txt" && echo "x.txt" +' >"$src/.ssg.sh" + + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +fail: .ssg.sh collides with x.txt +' + ;; + + write_hashes) + mkdir "$src" + echo >"$src/x.txt" + # shellcheck disable=2016 + "$cmd" "$src" "$dst" 2>&1 | not_ok_diff_n "$1: first run" ' +file x.txt +file x.txt > x.txt.gz +a12d7b67f235edb37cfcf1bdd5a50a2e0486e1612eda28210b816eaff424a100 +' + cat "$dst/.ssg.src" | not_ok_diff_n "$1" ' +01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b x.txt +' + cat "$dst/.ssg.dst" | not_ok_diff_n "$1" ' +01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b x.txt +34d5848c995803cfd00c2f7f02d807e2069b51ffca60a40f05d2d7229ef13b69 x.txt.gz +' + ;; + + *) not_ok "$1: not such test" ;; + + esac + + ok "$1" && rm -rf "$dir" +} + +# tests + +t fail_no_args +t fail_no_src +t select_src_files_empty_src +t select_src_files_ssg_ignore +t select_src_files_no_dst +t select_src_files_no_ssg_dst_ssg_src +t select_src_files_no_dst_ssg_dst_match +t select_src_files_no_src_diff +t select_src_files_clean_dst +t select_src_files_clean_dst_dir +t select_updated +t generate_copy +t generate_file +t generate_html +t generate_html_with_template +t generate_html_with_template_no_title +t generate_html_with_template_in_dir +t generate_html_template_not_found +t generate_md_with_collision +t generate_md_with_template +t generate_md_template_not_found +t generate_sh +t generate_sh_with_collision +t write_hashes + +basic_case && bench 4 basic_case + +plan