#!/bin/bash
#
# mdinkling - A simple static site generator from Markdown.
#
# mdinkling generates HTML pages from Markdown body text and HTML header and
# footer includes. Generated pages are static and may be gzip'ed. A CSS file
# may be specified, in which case it may also be gzip'ed.
#
# Copyright 2025 Andrew 'Diddymus' Rolfe. All rights reserved.
#
# Released into the public domain under “The Unlicense” license.
# SPDX-License-Identifier: Unlicense

version="0.0.2"

shopt -s extglob

# Commands and utilities this script requires not in Debian's coreutils
requirements=("cmp" "grep" "gzip" "lowdown")

# usage displays some help for the command when the -h option, or an invalid
# option, is used.
usage() {
cat <<EOT
Usage: mdinkling [-a] [-c CSS] [-d] [-h] [-i DIR] [-m MARKER] [-n] [-o DIR] [-v]

Flags:
  -a  rebuild all HTML files
  -c  CSS file to use (relative to input directory)
  -d  debug tracing
  -h  display this help
  -i  input directory for Markdown files, defaults current directory
  -m  marker, if found in generated HTML use alternate includes
  -n  dry run only
  -o  output directory for generated HTML, default current directory
  -v  verbose output
  -V  display version and exit
  -z  generate gzip'ed copy of HTML output

EOT
}

# log messages to stderr with a timestamp.
log() {
  [[ verbose -eq 1 ]] && echo "$(date +'%Y-%m-%dT%H:%M:%S%z'): $*" >&2
}

# Setup default settings
build_all=0 # Ignore file times and build everything? 1=yes, otherwise no
  dry_run=0 # Don't write any actual changes? 1=yes, otherwise no
  verbose=0 # Logging to stderr? 1=yes, otherwise no
  gzipped=0 # Generate gzip'ed HTML? 1=yes, otherwise no

   input=${PWD}
  output=${PWD}
 options="--html-no-escapehtml --html-no-skiphtml" # Passed to lowdown
includes="header.inc alt-header.inc footer.inc alt-footer.inc" # HTML includes
  marker=""
     css=""

# Process command line flags
while getopts ":ac:dhi:m:no:vzV" Option; do
  case $Option in
    a) build_all=1    ;;
    c) css=$OPTARG    ;;
    d) set -x         ;;
    h) usage; exit    ;;
    i) input=$OPTARG  ;;
    m) marker=$OPTARG ;;
    n) dry_run=1      ;;
    o) output=$OPTARG ;;
    v) verbose=1      ;;
    z) gzipped=1      ;;
    V) echo "$(basename ${0}) v${version}"; exit ;;
    *) echo "Invalid option: -${OPTARG}"; usage; exit ;;
  esac
done
shift $(($OPTIND - 1))

log "mdinkling v${version}, run started."
log "PATH=${PATH}"
log "PWD=${PWD}"

# Check for commands/utilities we need
for cmd in ${requirements[@]}; do
  command -vp ${cmd} 2>&1 > /dev/null
  if [[ "$?" -ne 0 ]]; then
    log "Cannot find command: ${cmd}"
    exit
  fi
done

# Resolve and validate input directory
clean="$(realpath -qe "${input}")"
if [[ "$?" -ne 0 ]]; then
  log "invalid input path used: ${input}"
  exit
fi
input=$clean

# Resolve and validate output directory
clean=$(realpath -qe "${output}")
if [[ "$?" -ne 0 ]]; then
  log "invalid output path used: ${output}"
  exit
fi
output=$clean

# Resolve and validate CSS (relative to input)
if [[ ! -z "$css" ]]; then
  clean=$(realpath -qe "${input}/${css}")
  if [[ "$?" -ne 0 ]]; then
    log "invalid CSS path used: ${css}"
    exit
  fi
  css=$(realpath -qe --relative-to="${input}" "$clean")
fi

# Calculate common root path of input and output directories
if [[ "${input}" == "$output" ]]; then
  common_path=${input}
else
  l=${#input}
  [[ ${#output} -lt ${#input} ]] && l=${#output}
  for (( x=0; x<l; x++ )); do
    [[ ${input:$x:1} != ${output:$x:1} ]] && break
  done
  common_path=${input:0:$x}
  [[ ${common_path:$x:1} != "/" ]] && common_path=${common_path%/*}"/"
fi

# Log settings used
log "      input path: ${input}"
log "output directory: ${output}"
log "common root path: ${common_path}"
[[ ! -z "$marker"  ]] && log "alternate marker: ${marker}"
[[ ! -z "$css"     ]] && log "        CSS file: ${input}/${css}"
[[ dry_run   -eq 1 ]] && log "+++ DRY RUN ONLY +++"
[[ build_all -eq 1 ]] && log "+++ BUILDING ALL +++"
[[ gzipped   -eq 1 ]] && log "+++ GZIP'ED HTML +++"

# Process *.md files in input directory, output HTML & GZIP to output directory
for md in $(find ${input} -name "*.md"); do
  html=$(realpath -q --relative-to=${input} ${md})
  html=${output}"/"${html/%.md/.html}

  # Check if HTML is out of date and needs rebuilding
  rebuild=0
  if [[ build_all -eq 1 ]]; then
    rebuild=1
  elif [ "$md" -nt "${html}" ]; then
    rebuild=1
  else
    for file in ${includes}; do
      if [ "${input}/${file}" -nt "${html}" ]; then
        rebuild=1
        break
      fi
    done
  fi

  if [[ rebuild -eq 0 ]]; then
    log "Not modified: ${md#$common_path} -> ${html#$common_path}"
  else
    log "Processing: ${md#$common_path}"

    # Get Markdown metadata
    declare -A meta
    meta=()
    while read tag content; do
      if [[ -z "$tag" || "${tag%:}" == "${tag}" ]]; then
        break
      fi
      tag=${tag/:/}
      meta[${tag}]="${content}"
      log "  Found metadata, tag: '${tag}' content: '${content}'"
    done <${md}

    # Check target directory exists (may be new sub directory).
    target_dir=${html%%$(basename ${html})}
    dummy=$(realpath -qe "${target_dir}")
    if [[ "$?" -ne 0 ]]; then
      log "  Creating dir: ${target_dir}"
      [[ dry_run -ne 1 ]] && mkdir -p "${target_dir}"
    fi

    # Generate HTML
    lowdown $options $md > ${input}/body.inc
    use_main=1

    # Combine generated HTML with main header/footer or alternates?
    if [[ ! -z "$marker" ]]; then
      grep -q "${marker}" ${input}/body.inc
      use_main=$?
    fi
    if [[ use_main -eq 0 ]]; then
      log "  Found marker '${marker}', using alternate header: ${html#$common_path}"
      page=$(cat "${input}/alt-header.inc" "${input}/body.inc" "${input}/alt-footer.inc")
    else
      log "  No alternate marker found: ${html#$common_path}"
      page=$(cat "${input}/header.inc" "${input}/body.inc" "${input}/footer.inc")
    fi

    # Substitute Metadata in generated HTML page
    log "  Checking metadata: ${html#$common_path}"
    for tag in ${!meta[*]}; do
      log "    Replacing '%%${tag}%%' with '${meta[$tag]}'"
      page=${page//%%${tag}%%/${meta[$tag]}}
    done
    page=${page//%%+([a-zA-Z0-9_-])%%/} # Remove unused tags

    # Write final generated HTML
    log "  Writing HTML: ${html#$common_path}"
    [[ dry_run -ne 1 ]] && echo "${page}" > $html

  fi

  # Create gzip'ed copy of HTML
  if [[ gzipped -eq 1 ]]; then
    if [[ build_all -eq 1 || rebuild -eq 1 || ! -e "${html}.gz" || "${html}" -nt "${html}.gz" ]]; then
      if [[ ! -e "${html}.gz" ]]; then
        log "  Creating GZIP: ${html#$common_path}.gz"
      else
        log "  Updating GZIP: ${html#$common_path}.gz"
      fi
      [[ dry_run -ne 1 ]] && gzip -f --best -k "${html}"
    fi
  fi
done

# Handle CSS
if [[ -z "$css" ]]; then
  log "No CSS specified."
else
  if [[ "${input}" == "${output}" ]]; then
    log "Inplace CSS not copied: ${output#$common_path}/${css}"
  else
    css_copied=0
    if [[ build_all -eq 1 || "${input}/${css}" -nt "${output}/${css}" ]]; then
      log "Copying CSS: ${output#$common_path}/${css}"

      # Check target directory exists (may be new sub directory).
      target_dir="${output}/${css%%$(basename ${css})}"
      dummy=$(realpath -qe "${target_dir}")
      if [[ "$?" -ne 0 ]]; then
        log "  Creating CSS dir: ${target_dir}"
        [[ dry_run -ne 1 ]] && mkdir -p "${target_dir}"
      fi

      [[ dry_run -ne 1 ]] && cp -r "${input}/${css}" "${output}/${css}"
      css_copied=1
    else
      log "Not modified: ${input#$common_path}/${css} -> ${output#$common_path}/${css}"
    fi
  fi

  # Create gzip'ed copy of CSS
  if [[ gzipped -eq 1 ]]; then
    if [[ build_all -eq 1 || css_copied -eq 1 || "${output}/${css}" -nt "${output}/${css}.gz" ]]; then
      log "Compressing CSS: ${output#$common_path}/${css}.gz"
      [[ dry_run -ne 1 ]] && gzip -f --best -k "${output}/${css}"
    fi
  fi
fi

log "Done!"
