#!/bin/bash
#
# Author: Merlin Büge
# Date: 20221010
#
# Inspired by: https://romanzolotarev.com/ssg.html
#
# TODO:


usage() {
	cat <<EOF
usage: ${0##*/} [options] <src> <dst>

Options: none

A simple static site generator featuring incremental updates.

<src> is the source directory, containing markdown files and various other files.
<dst> is the destination directory, where the generated HTML files and various other files get copied to.

How it works:

- <src> is scanned for files that have been modified since the last run (see below).
  - If any partials have changed, all dependent markdown files will be processed, too.
- Markdown files are processed, and the resulting HTML files are copied to <dst>:
  (1) render to HTML
  (2) prepend <src>/partials/_header.html
  (3) append <src>/partials/_footer.html
- All remaining files (incl. HTML files) are copied over to <dst>.
  - Note: HTML files in <src> take priority over markdown files in <src>.
    E.g., a file <src>/index.html will overwrite the generated HTML from <src>/index.md.

Incremental updates:

- In every run, the list of processed files is saved to the file <dst>/.files.
- Its timestamp will be used in the next run to determine the set of modified files.
- To rebuild everything, remove <dst>/.files before running ssg.
- Note: Currently, the actual content of <dst>/.files is not used.

Ignoring files:

- Files to be ignored can be specified in the file <src>/.ssgignore.
- Each line is a shell pattern (which is passed to the find command).
- Any file that matches any of those patterns will be ignored.
- By default (if .ssgignore does not exist), all hidden files are ignored: */.*
- Files that are always ignored: ./partials/*

Tips:

- When moving/copying files to <src>, update their mtime (via touch) to make sure they get processed in the next run.
- When deleting files from <src>, they will not be automatically deleted from <dst>. If unsure, rebuild everything after rm -rf <dst>.
- After deleting <src>/page.html, touch (i.e. update mtime of) <src>/page.md (if existent) to make sure it is processed.

Dependencies:

- GNU coreutils (cat, cp, readlink, ...)
- GNU findutils (find)
- pandoc

Some of the dependencies use GNU-style extensions.

EOF
}

# render markdown file $1 to HTML file $2
render_md_file() {
	local infile="$1"
	local outfile="$2"
	cat \
		"$h_file" \
		<(pandoc --wrap=preserve -f markdown-native_divs-native_spans \
			--strip-comments -t html "$infile") \
		"$f_file" \
		> "$outfile"
}

# ------------------------------------------------------------------------------

# parse options
while true; do
	case "$1" in
		--) # end of all options
			shift
			break
			;;
		-*) # unknown option
			printf 'error: unknown option %s\n' "$1"
			exit 1
			;;
		*) # no more options
			break
			;;
	esac
done

# parse arguments
if [[ "$#" -ne 2 ]]; then
	usage
	exit 1
fi
if [[ ! -d "$1" ]]; then
	printf 'error: not a directory: %s\n' "$1"
	exit 1
fi
if [[ ! -d "$2" ]]; then
	printf 'error: not a directory: %s\n' "$2"
	exit 1
fi
src="$(readlink -f "$1")"
dst="$(readlink -f "$2")"
shift 2

# check presence of files
h_file="${src}/partials/_header.html"
if [[ ! -f "$h_file" ]]; then
	printf 'error: not a file: %s\n' "$h_file"
	exit 1
fi
f_file="${src}/partials/_footer.html"
if [[ ! -f "$f_file" ]]; then
	printf 'error: not a file: %s\n' "$f_file"
	exit 1
fi

# ------------------------------------------------------------------------------

# build ignore_expr (for find command) based on .ssgignore
ignore_expr=( "!" "-path" "./partials/*" )
if [[ -f "${src}/.ssgignore" ]]; then
	while read -r line; do
		[[ -n "$line" ]] || continue
		ignore_expr+=( "!" "-path" "${line}" )
	done < "${src}/.ssgignore"
else
	ignore_expr+=( "!" "-path" "*/.*" )	# ignore hidden files/dirs
fi

# gather files to process (order does not matter, but no duplicates!)
files_to_process=()
if ! [[ -f "${dst}/.files" ]]; then # gather all files
	printf '[ssg] processing all files, since no timestamp file <dst>/.files found.\n'
	while read -r file; do
		[[ -n "$file" ]] || continue
		files_to_process+=( "$file" )
	done <<< "$(cd "$src" && find . -type f "${ignore_expr[@]}")"
else # gather only modified files
	while read -r file; do
		[[ -n "$file" ]] || continue

		# HTML files have precedence over markdown files
		if [[ -f "${src}/${file%.md}.html" ]]; then
			printf '[ssg] ignoring %s (would be replaced by HTML version)\n' "${src}/${file}"
			continue
		fi

		if [[ "${src}/${file}" -nt "${dst}/.files" ]] \
				|| [[ "$h_file" -nt "${dst}/.files" ]] \
				|| [[ "$f_file" -nt "${dst}/.files" ]]; then
			files_to_process+=( "$file" )
		fi
	done <<< "$(cd "$src" && find . -type f "${ignore_expr[@]}" -iname '*.md')"

	while read -r file; do
		[[ -n "$file" ]] || continue
		files_to_process+=( "$file" )
	done <<< "$(cd "$src" && find . -type f "${ignore_expr[@]}" -not -iname '*.md' -newer "${dst}/.files")"
fi

# save files_to_process to $dst/.files (its timestamp will be used in the next run)
printf '%s\n' "${files_to_process[@]}" > "${dst}/.files"

# exit if nothing to do
if [[ "${#files_to_process[@]}" -eq 0 ]]; then
	printf '[ssg] all files up to date. nothing to do.\n'
	exit 0
fi

# process markdown files
for file in "${files_to_process[@]}"; do
	[[ "$file" = *.md ]] || continue

	mkdir -p "${dst}/$(dirname "$file")"
	infile="$(readlink -f "${src}/${file}")"
	outfile="$(readlink -f "${dst}/${file%.md}.html")"
	render_md_file "$infile" "$outfile"

	printf '[ssg] %s -> %s\n' "$infile" "$outfile"
done

# process other files
for file in "${files_to_process[@]}"; do
	[[ "$file" != *.md ]] || continue

	mkdir -p "${dst}/$(dirname "$file")"
	infile="$(readlink -f "${src}/${file}")"
	outfile="$(readlink -f "${dst}/${file}")"
	cp "$infile" "$outfile"

	printf '[ssg] %s -> %s\n' "$infile" "$outfile"
done

unset infile
unset outfile

