#!/bin/bash
# marsh - build static websites using markdown and bash
#
# Copyright 2021 Bradley Sepos
# Released under the MIT License. See LICENSE for details.
# https://github.com/bradleysepos/marsh
NAME="marsh"
VERSION="0.8.1"
SELF="${BASH_SOURCE[0]}"
SELF_DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd -P)
SELF_DIR="${SELF_DIR:-$(pwd)}"
SELF_NAME=$(basename "${SELF}")
SYS_NAME=$(uname | awk '{ print tolower($0)}')
MARKDOWN_OPTIONS_DEFAULT=(-1.0 +alphalist -autolink -cdata +definitionlist -divquote -dldiscount +dlextra -emphasis +ext +fencedcode +footnote +githubtags -header +html +image -latex +links -safelink +smarty +strikethrough +style +superscript +tables -tabstop -toc)
HELP="\
usage: ${SELF_NAME} [-h | --help]
${SELF_NAME} [-v | --version]
${SELF_NAME} build [--fetch] [--force] [-j # | --jobs #]
[--markdown markdown] [--markdown-options opt1,opt2] [config_file]
where:
-h, --help
display this help text
-v, --version
display version information
--fetch
download remote resources
--force
remove and replace existing targets (overwrite)
-j, --jobs
number of concurrent jobs to run using GNU Parallel
default: automatically chosen by GNU Parallel, if available
--markdown
path to markdown application
--markdown-options
markdown translation options (comma delimited)
default: $(echo ${MARKDOWN_OPTIONS_DEFAULT[@]} | sed 's/ /,/g')"
# creates bash variables from yaml records
# extended to support arrays, empty values, and more
# https://gist.github.com/DinoChiesa/3e3c3866b51290f31243
# https://gist.github.com/epiloque/8cf512c6d64641bde388
# https://gist.github.com/pkuczynski/8665367
function parse_yaml { # parse_yaml yaml_string_or_file variables_prefix spaces_per_indentation
local INPUT PREFIX INDENT S W FS
INPUT="${1:-}"
[[ "${INPUT}" == "" ]] && return 0
if [[ -f "${INPUT}" ]] && [[ -r "${INPUT}" ]]; then
INPUT=$(<"${INPUT}")
fi
PREFIX="${2:-}"
INDENT="${3:-}"
if [[ ! "${INDENT}" =~ '^[0-9]+$' ]] || [[ ! "${INDENT}" -eq 0 ]]; then
INDENT=2
fi
S='[[:space:]]*'
W='[a-zA-Z0-9_]*'
FS=$(echo @|tr @ '\034')
echo "${INPUT}" |
sed -n \
-e "s|^\(\(${S}\)\(-\) \)\{1\}\(${S}\)\(${W}\)${S}:${S}\(\".*\"\)\${S}\$|\3${FS}\2 \4${FS}\5${FS}\6|p" \
-e "s|^\(\(${S}\)\(-\) \)\{1\}\(${S}\)\(${W}\)${S}:${S}\(.*\)${S}\$|\3${FS}\2 \4${FS}\5${FS}\6|p" \
-e "s|^\(\(${S}\)\(-\) \)\{1\}\(${S}\)\(\".*\"\)${S}\$|\3${FS}\2 \4${FS}${FS}\5|p" \
-e "s|^\(\(${S}\)\(-\) \)\{1\}\(${S}\)\([^:].*\)${S}\$|\3${FS}\2 \4${FS}${FS}\5|p" \
-e "s|^\(${S}\)\(${W}\)${S}:${S}\(\".*\"\)${S}\$|${FS}\1${FS}\2${FS}\3|p" \
-e "s|^\(${S}\)\(${W}\)${S}:${S}\(.*\)${S}\$|${FS}\1${FS}\2${FS}\3|p" |
awk -F"${FS}" \
'#
# parse_csv, public domain, http://lorance.freeshell.org/csv/
#**************************************************************************
#
# This file is in the public domain.
#
# For more information email LoranceStinson+csv@gmail.com.
# Or see http://lorance.freeshell.org/csv/
#
# Parse a CSV string into an array.
# The number of fields found is returned.
# In the event of an error a negative value is returned and csverr is set to
# the error. See below for the error values.
#
# Parameters:
# string = The string to parse.
# csv = The array to parse the fields into.
# sep = The field separator character. Normally ,
# quote = The string quote character. Normally "
# escape = The quote escape character. Normally "
# newline = Handle embedded newlines. Provide either a newline or the
# string to use in place of a newline. If left empty embedded
# newlines cause an error.
# trim = When true spaces around the separator are removed.
# This affects parsing. Without this a space between the
# separator and quote result in the quote being ignored.
#
# These variables are private:
# fields = The number of fields found thus far.
# pos = Where to pull a field from the string.
# strtrim = True when a string is found so we know to remove the quotes.
#
# Error conditions:
# -1 = Unable to read the next line.
# -2 = Missing end quote.
# -3 = Missing separator.
#
# Notes:
# The code assumes that every field is preceded by a separator, even the
# first field. This makes the logic much simpler, but also requires a
# separator be prepended to the string before parsing.
function parse_csv(string,csv,sep,quote,escape,newline,trim, fields,pos,strtrim) {
# Make sure there is something to parse.
if (length(string) == 0) return 0;
string = sep string; # The code below assumes ,FIELD.
fields = 0; # The number of fields found thus far.
while (length(string) > 0) {
# Remove spaces after the separator if requested.
if (trim && substr(string, 2, 1) == " ") {
if (length(string) == 1) return fields;
string = substr(string, 2);
continue;
}
strtrim = 0; # Used to trim quotes off strings.
# Handle a quoted field.
if (substr(string, 2, 1) == quote) {
pos = 2;
do {
pos++
if (pos != length(string) &&
substr(string, pos, 1) == escape &&
(substr(string, pos + 1, 1) == quote ||
substr(string, pos + 1, 1) == escape)) {
# Remove escaped quote characters.
string = substr(string, 1, pos - 1) substr(string, pos + 1);
} else if (substr(string, pos, 1) == quote) {
# Found the end of the string.
strtrim = 1;
} else if (newline && pos >= length(string)) {
# Handle embedded newlines if requested.
if (getline == -1) {
csverr = "Unable to read the next line.";
return -1;
}
string = string newline $0;
}
} while (pos < length(string) && strtrim == 0)
if (strtrim == 0) {
csverr = "Missing end quote.";
return -2;
}
} else {
# Handle an empty field.
if (length(string) == 1 || substr(string, 2, 1) == sep) {
csv[fields] = "";
fields++;
if (length(string) == 1)
return fields;
string = substr(string, 2);
continue;
}
# Search for a separator.
pos = index(substr(string, 2), sep);
# If there is no separator the rest of the string is a field.
if (pos == 0) {
csv[fields] = substr(string, 2);
fields++;
return fields;
}
}
# Remove spaces after the separator if requested.
if (trim && pos != length(string) && substr(string, pos + strtrim, 1) == " ") {
trim = strtrim
# Count the number fo spaces found.
while (pos < length(string) && substr(string, pos + trim, 1) == " ") {
trim++
}
# Remove them from the string.
string = substr(string, 1, pos + strtrim - 1) substr(string, pos + trim);
# Adjust pos with the trimmed spaces if a quotes string was not found.
if (!strtrim) {
pos -= trim;
}
}
# Make sure we are at the end of the string or there is a separator.
if ((pos != length(string) && substr(string, pos + 1, 1) != sep)) {
csverr = "Missing separator.";
return -3;
}
# Gather the field.
csv[fields] = substr(string, 2 + strtrim, pos - (1 + strtrim * 2));
fields++;
# Remove the field from the string for the next pass.
string = substr(string, pos + 1);
}
return fields;
}
BEGIN { prefix = "'"${PREFIX}"'"; indent = '"${INDENT}"'; prev_level = 0; }
{
type = $1;
level = length($2)/indent;
key = $3;
val = $4;
out = "";
array = 0;
if (level < prev_level) { adepth[prev_level] = -1; }
if (adepth[level] == "") { adepth[level] = -1; }
if (type == "-") { adepth[level]++; }
vname[level] = key;
for (i in vname) {
if (i > (level)) { vname[i] = ""; }
}
for (i = 0; i < level; i++) {
if (vname[i] != "") {
out = out "_" vname[i];
if (adepth[i] > -1) { out = out "_" adepth[i+1]; }
} else {
out = out "_" adepth[i+1];
}
}
if (substr(val,1,1) == "[" && substr(val,length(val),1) == "]") {
array = 1;
gsub(/^\[[ ]*/, "", val);
gsub(/[ ]*\]$/, "", val);
num_vals = parse_csv(val, vals, ",", "\"", "\"", "\\n", 1);
if (num_vals < 0) {
# Error parsing inline array
exit 1;
}
} else {
if (substr(val,1,1) == "\"" && substr(val,length(val),1) == "\"") {
num_vals = parse_csv(val, vals, "'"${FS}"'", "\"", "\"", "\\n", 0);
if (num_vals < 0) {
# Error parsing
exit 1;
}
} else {
vals[1] = val;
}
}
if (key != "") { out = out "_" key; }
out = prefix out;
gsub(/__/, "_", out);
gsub(/_=/, "", out);
#for (i = 1; i <= length(vals); i++) {
for (i in vals) {
gsub(/"/, "\\\"", vals[i]);
if (vals[i] != "true" && vals[i] != "false") {
vals[i]="\"" vals[i] "\""
}
if (array == 1) {
print out "_" i "=" vals[i];
} else {
print out "=" vals[i];
}
}
delete vals;
prev_level = level;
}'
}
# tests whether a string is an exact match for an array item
function in_array { # in_array needle haystack[@]
local e
for e in "${@:2}"; do
[[ "${e}" == "${1}" ]] && return 0;
done
return 1
}
# creates an array from the contents of flat variables
# TEST_0="foo"; TEST_1="bar"; TEST_2="baz"
# eval $(flat_to_array "TEST" "TEST_") # loops over TEST_{0..2}
# echo "${TEST[*]}" # foo bar baz
function flat_to_array { # flat_to_array array_name variable_prefix
local ARRAY_NAME PREFIX INDEX ITEM
ARRAY_NAME="${1:-}"
[[ "${ARRAY_NAME}" == "" ]] && return 1
PREFIX="${2:-}"
[[ "${PREFIX}" == "" ]] && return 1
echo "${ARRAY_NAME}=()"
INDEX=0
ITEM="${PREFIX}${INDEX}"
ITEM="${!ITEM:-}"
while [[ "${ITEM}" != "" ]]; do
echo "${ARRAY_NAME}+=(\"${ITEM//\"/\\\"}\")"
INDEX=$((INDEX + 1))
ITEM="${PREFIX}${INDEX}"
ITEM="${!ITEM:-}"
done
return 0
}
# prints arguments combined with delimiters
# join_delimited ', ' ', ' ', ' foo bar baz # foo, bar, baz
# join_delimited ', ' ' and ' ', and ' foo bar baz # foo, bar, and baz
function join_delimited { # join_delimited delimiter1 delimiter2 delimiter3 args[@]
local DELIM1 DELIM2 DELIM3
DELIM1="${1:-}"
shift 1
DELIM2="${1:-}"
shift 1
DELIM3="${1:-}"
shift 1
if [[ "${#@}" -gt 0 ]]; then
echo -n "${1}"
if [[ "${#@}" -eq 2 ]]; then
echo "${DELIM2}${2}"
return 0
fi
while [[ "${#@}" -gt 2 ]]; do
echo -n "${DELIM1}${2}"
shift 1
done
echo -n "${2+$DELIM3$2}"
echo
fi
return 0
}
# prints md5 hash for a given string
function md5_string { # md5_string string
local STRING SYS_NAME
STRING="${1:-}"
SYS_NAME=$(uname | awk '{ print tolower($0)}')
if [[ "${SYS_NAME}" == "darwin" ]]; then
echo -n "${STRING}" | md5 -r | awk '{ print $1 }'
else
echo -n "${STRING}" | md5sum | awk '{ print $1 }'
fi
return 0
}
# converts html name or number entities to utf-8 character
function decode_entities { # decode_entities
TEXT=$(/g' -e 's/>/>/g' -e 's/ / /g' -e 's/ / /g' -e 's/¡/¡/g' -e 's/¡/¡/g' -e 's/¢/¢/g' -e 's/¢/¢/g' -e 's/£/£/g' -e 's/£/£/g' -e 's/¤/¤/g' -e 's/¤/¤/g' -e 's/¥/¥/g' -e 's/¥/¥/g' -e 's/¦/¦/g' -e 's/¦/¦/g' -e 's/§/§/g' -e 's/§/§/g' -e 's/¨/¨/g' -e 's/¨/¨/g' -e 's/©/©/g' -e 's/©/©/g' -e 's/ª/ª/g' -e 's/ª/ª/g' -e 's/«/«/g' -e 's/«/«/g' -e 's/¬/¬/g' -e 's/¬/¬/g' -e 's///g' -e 's///g' -e 's/®/®/g' -e 's/®/®/g' -e 's/¯/¯/g' -e 's/¯/¯/g' -e 's/°/°/g' -e 's/°/°/g' -e 's/±/±/g' -e 's/±/±/g' -e 's/²/²/g' -e 's/²/²/g' -e 's/³/³/g' -e 's/³/³/g' -e 's/´/´/g' -e 's/´/´/g' -e 's/µ/µ/g' -e 's/µ/µ/g' -e 's/¶/¶/g' -e 's/¶/¶/g' -e 's/·/·/g' -e 's/·/·/g' -e 's/¸/¸/g' -e 's/¸/¸/g' -e 's/¹/¹/g' -e 's/¹/¹/g' -e 's/º/º/g' -e 's/º/º/g' -e 's/»/»/g' -e 's/»/»/g' -e 's/¼/¼/g' -e 's/¼/¼/g' -e 's/½/½/g' -e 's/½/½/g' -e 's/¾/¾/g' -e 's/¾/¾/g' -e 's/¿/¿/g' -e 's/¿/¿/g' -e 's/×/×/g' -e 's/×/×/g' -e 's/÷/÷/g' -e 's/÷/÷/g' -e 's/À/À/g' -e 's/À/À/g' -e 's/Á/Á/g' -e 's/Á/Á/g' -e 's/Â/Â/g' -e 's/Â/Â/g' -e 's/Ã/Ã/g' -e 's/Ã/Ã/g' -e 's/Ä/Ä/g' -e 's/Ä/Ä/g' -e 's/Å/Å/g' -e 's/Å/Å/g' -e 's/Æ/Æ/g' -e 's/Æ/Æ/g' -e 's/Ç/Ç/g' -e 's/Ç/Ç/g' -e 's/È/È/g' -e 's/È/È/g' -e 's/É/É/g' -e 's/É/É/g' -e 's/Ê/Ê/g' -e 's/Ê/Ê/g' -e 's/Ë/Ë/g' -e 's/Ë/Ë/g' -e 's/Ì/Ì/g' -e 's/Ì/Ì/g' -e 's/Í/Í/g' -e 's/Í/Í/g' -e 's/Î/Î/g' -e 's/Î/Î/g' -e 's/Ï/Ï/g' -e 's/Ï/Ï/g' -e 's/Ð/Ð/g' -e 's/Ð/Ð/g' -e 's/Ñ/Ñ/g' -e 's/Ñ/Ñ/g' -e 's/Ò/Ò/g' -e 's/Ò/Ò/g' -e 's/Ó/Ó/g' -e 's/Ó/Ó/g' -e 's/Ô/Ô/g' -e 's/Ô/Ô/g' -e 's/Õ/Õ/g' -e 's/Õ/Õ/g' -e 's/Ö/Ö/g' -e 's/Ö/Ö/g' -e 's/Ø/Ø/g' -e 's/Ø/Ø/g' -e 's/Ù/Ù/g' -e 's/Ù/Ù/g' -e 's/Ú/Ú/g' -e 's/Ú/Ú/g' -e 's/Û/Û/g' -e 's/Û/Û/g' -e 's/Ü/Ü/g' -e 's/Ü/Ü/g' -e 's/Ý/Ý/g' -e 's/Ý/Ý/g' -e 's/Þ/Þ/g' -e 's/Þ/Þ/g' -e 's/ß/ß/g' -e 's/ß/ß/g' -e 's/à/à/g' -e 's/à/à/g' -e 's/á/á/g' -e 's/á/á/g' -e 's/â/â/g' -e 's/â/â/g' -e 's/ã/ã/g' -e 's/ã/ã/g' -e 's/ä/ä/g' -e 's/ä/ä/g' -e 's/å/å/g' -e 's/å/å/g' -e 's/æ/æ/g' -e 's/æ/æ/g' -e 's/ç/ç/g' -e 's/ç/ç/g' -e 's/è/è/g' -e 's/è/è/g' -e 's/é/é/g' -e 's/é/é/g' -e 's/ê/ê/g' -e 's/ê/ê/g' -e 's/ë/ë/g' -e 's/ë/ë/g' -e 's/ì/ì/g' -e 's/ì/ì/g' -e 's/í/í/g' -e 's/í/í/g' -e 's/î/î/g' -e 's/î/î/g' -e 's/ï/ï/g' -e 's/ï/ï/g' -e 's/ð/ð/g' -e 's/ð/ð/g' -e 's/ñ/ñ/g' -e 's/ñ/ñ/g' -e 's/ò/ò/g' -e 's/ò/ò/g' -e 's/ó/ó/g' -e 's/ó/ó/g' -e 's/ô/ô/g' -e 's/ô/ô/g' -e 's/õ/õ/g' -e 's/õ/õ/g' -e 's/ö/ö/g' -e 's/ö/ö/g' -e 's/ø/ø/g' -e 's/ø/ø/g' -e 's/ù/ù/g' -e 's/ù/ù/g' -e 's/ú/ú/g' -e 's/ú/ú/g' -e 's/û/û/g' -e 's/û/û/g' -e 's/ü/ü/g' -e 's/ü/ü/g' -e 's/ý/ý/g' -e 's/ý/ý/g' -e 's/þ/þ/g' -e 's/þ/þ/g' -e 's/ÿ/ÿ/g' -e 's/ÿ/ÿ/g' -e 's/∀/∀/g' -e 's/∀/∀/g' -e 's/∂/∂/g' -e 's/∂/∂/g' -e 's/∃/∃/g' -e 's/∃/∃/g' -e 's/∅/∅/g' -e 's/∅/∅/g' -e 's/∇/∇/g' -e 's/∇/∇/g' -e 's/∈/∈/g' -e 's/∈/∈/g' -e 's/∉/∉/g' -e 's/∉/∉/g' -e 's/∋/∋/g' -e 's/∋/∋/g' -e 's/∏/∏/g' -e 's/∏/∏/g' -e 's/∑/∑/g' -e 's/∑/∑/g' -e 's/−/−/g' -e 's/−/−/g' -e 's/∗/∗/g' -e 's/∗/∗/g' -e 's/√/√/g' -e 's/√/√/g' -e 's/∝/∝/g' -e 's/∝/∝/g' -e 's/∞/∞/g' -e 's/∞/∞/g' -e 's/∠/∠/g' -e 's/∠/∠/g' -e 's/∧/∧/g' -e 's/∧/∧/g' -e 's/∨/∨/g' -e 's/∨/∨/g' -e 's/∩/∩/g' -e 's/∩/∩/g' -e 's/∪/∪/g' -e 's/∪/∪/g' -e 's/∫/∫/g' -e 's/∫/∫/g' -e 's/∴/∴/g' -e 's/∴/∴/g' -e 's/∼/∼/g' -e 's/∼/∼/g' -e 's/≅/≅/g' -e 's/≅/≅/g' -e 's/≈/≈/g' -e 's/≈/≈/g' -e 's/≠/≠/g' -e 's/≠/≠/g' -e 's/≡/≡/g' -e 's/≡/≡/g' -e 's/≤/≤/g' -e 's/≤/≤/g' -e 's/≥/≥/g' -e 's/≥/≥/g' -e 's/⊂/⊂/g' -e 's/⊂/⊂/g' -e 's/⊃/⊃/g' -e 's/⊃/⊃/g' -e 's/⊄/⊄/g' -e 's/⊄/⊄/g' -e 's/⊆/⊆/g' -e 's/⊆/⊆/g' -e 's/⊇/⊇/g' -e 's/⊇/⊇/g' -e 's/⊕/⊕/g' -e 's/⊕/⊕/g' -e 's/⊗/⊗/g' -e 's/⊗/⊗/g' -e 's/⊥/⊥/g' -e 's/⊥/⊥/g' -e 's/⋅/⋅/g' -e 's/⋅/⋅/g' -e 's/Α/Α/g' -e 's/Α/Α/g' -e 's/Β/Β/g' -e 's/Β/Β/g' -e 's/Γ/Γ/g' -e 's/Γ/Γ/g' -e 's/Δ/Δ/g' -e 's/Δ/Δ/g' -e 's/Ε/Ε/g' -e 's/Ε/Ε/g' -e 's/Ζ/Ζ/g' -e 's/Ζ/Ζ/g' -e 's/Η/Η/g' -e 's/Η/Η/g' -e 's/Θ/Θ/g' -e 's/Θ/Θ/g' -e 's/Ι/Ι/g' -e 's/Ι/Ι/g' -e 's/Κ/Κ/g' -e 's/Κ/Κ/g' -e 's/Λ/Λ/g' -e 's/Λ/Λ/g' -e 's/Μ/Μ/g' -e 's/Μ/Μ/g' -e 's/Ν/Ν/g' -e 's/Ν/Ν/g' -e 's/Ξ/Ξ/g' -e 's/Ξ/Ξ/g' -e 's/Ο/Ο/g' -e 's/Ο/Ο/g' -e 's/Π/Π/g' -e 's/Π/Π/g' -e 's/Ρ/Ρ/g' -e 's/Ρ/Ρ/g' -e 's/Σ/Σ/g' -e 's/Σ/Σ/g' -e 's/Τ/Τ/g' -e 's/Τ/Τ/g' -e 's/Υ/Υ/g' -e 's/Υ/Υ/g' -e 's/Φ/Φ/g' -e 's/Φ/Φ/g' -e 's/Χ/Χ/g' -e 's/Χ/Χ/g' -e 's/Ψ/Ψ/g' -e 's/Ψ/Ψ/g' -e 's/Ω/Ω/g' -e 's/Ω/Ω/g' -e 's/α/α/g' -e 's/α/α/g' -e 's/β/β/g' -e 's/β/β/g' -e 's/γ/γ/g' -e 's/γ/γ/g' -e 's/δ/δ/g' -e 's/δ/δ/g' -e 's/ε/ε/g' -e 's/ε/ε/g' -e 's/ζ/ζ/g' -e 's/ζ/ζ/g' -e 's/η/η/g' -e 's/η/η/g' -e 's/θ/θ/g' -e 's/θ/θ/g' -e 's/ι/ι/g' -e 's/ι/ι/g' -e 's/κ/κ/g' -e 's/κ/κ/g' -e 's/λ/λ/g' -e 's/λ/λ/g' -e 's/μ/μ/g' -e 's/μ/μ/g' -e 's/ν/ν/g' -e 's/ν/ν/g' -e 's/ξ/ξ/g' -e 's/ξ/ξ/g' -e 's/ο/ο/g' -e 's/ο/ο/g' -e 's/π/π/g' -e 's/π/π/g' -e 's/ρ/ρ/g' -e 's/ρ/ρ/g' -e 's/ς/ς/g' -e 's/ς/ς/g' -e 's/σ/σ/g' -e 's/σ/σ/g' -e 's/τ/τ/g' -e 's/τ/τ/g' -e 's/υ/υ/g' -e 's/υ/υ/g' -e 's/φ/φ/g' -e 's/φ/φ/g' -e 's/χ/χ/g' -e 's/χ/χ/g' -e 's/ψ/ψ/g' -e 's/ψ/ψ/g' -e 's/ω/ω/g' -e 's/ω/ω/g' -e 's/ϑ/ϑ/g' -e 's/ϑ/ϑ/g' -e 's/ϒ/ϒ/g' -e 's/ϒ/ϒ/g' -e 's/ϖ/ϖ/g' -e 's/ϖ/ϖ/g' -e 's/Œ/Œ/g' -e 's/Œ/Œ/g' -e 's/œ/œ/g' -e 's/œ/œ/g' -e 's/Š/Š/g' -e 's/Š/Š/g' -e 's/š/š/g' -e 's/š/š/g' -e 's/Ÿ/Ÿ/g' -e 's/Ÿ/Ÿ/g' -e 's/ƒ/ƒ/g' -e 's/ƒ/ƒ/g' -e 's/ˆ/ˆ/g' -e 's/ˆ/ˆ/g' -e 's/˜/˜/g' -e 's/˜/˜/g' -e 's/ / /g' -e 's/ / /g' -e 's/ / /g' -e 's/ / /g' -e 's/ / /g' -e 's/ / /g' -e 's///g' -e 's///g' -e 's///g' -e 's///g' -e 's///g' -e 's///g' -e 's///g' -e 's///g' -e 's/–/–/g' -e 's/–/–/g' -e 's/—/—/g' -e 's/—/—/g' -e 's/‘/‘/g' -e 's/‘/‘/g' -e 's/’/’/g' -e 's/’/’/g' -e 's/‚/‚/g' -e 's/‚/‚/g' -e 's/“/“/g' -e 's/“/“/g' -e 's/”/”/g' -e 's/”/”/g' -e 's/„/„/g' -e 's/„/„/g' -e 's/†/†/g' -e 's/†/†/g' -e 's/‡/‡/g' -e 's/‡/‡/g' -e 's/•/•/g' -e 's/•/•/g' -e 's/…/…/g' -e 's/…/…/g' -e 's/‰/‰/g' -e 's/‰/‰/g' -e 's/′/′/g' -e 's/′/′/g' -e 's/″/″/g' -e 's/″/″/g' -e 's/‹/‹/g' -e 's/‹/‹/g' -e 's/›/›/g' -e 's/›/›/g' -e 's/‾/‾/g' -e 's/‾/‾/g' -e 's/€/€/g' -e 's/€/€/g' -e 's/™/™/g' -e 's//™/g' -e 's/™/™/g' -e 's/←/←/g' -e 's/←/←/g' -e 's/↑/↑/g' -e 's/↑/↑/g' -e 's/→/→/g' -e 's/→/→/g' -e 's/↓/↓/g' -e 's/↓/↓/g' -e 's/↔/↔/g' -e 's/↔/↔/g' -e 's/↵/↵/g' -e 's/↵/↵/g' -e 's/⌈/⌈/g' -e 's/⌈/⌈/g' -e 's/⌉/⌉/g' -e 's/⌉/⌉/g' -e 's/⌊/⌊/g' -e 's/⌊/⌊/g' -e 's/⌋/⌋/g' -e 's/⌋/⌋/g' -e 's/◊/◊/g' -e 's/◊/◊/g' -e 's/♠/♠/g' -e 's/♠/♠/g' -e 's/♣/♣/g' -e 's/♣/♣/g' -e 's/♥/♥/g' -e 's/♥/♥/g' -e 's/♦/♦/g' -e 's/♦/♦/g'
return 0
}
# transforms a string
function text_filter { # text_filter filters[@]
local FILTERS
local LF TEXT OPTIONS TEXT_TEMP DATE_FORMAT MAX_CHARS MAX_PARAS WRAP
LF=$'\n'
TEXT=$(/dev/null | sed -E -e 's/([0-9][0-9])$/:\1/')
[[ $? -ne 0 ]] && return 1
;;
esac
TEXT="${TEXT_TEMP}"
fi
;;
escape:entities)
TEXT=$(echo "${TEXT}" | decode_entities);
;;
escape:html)
TEXT=$(echo "${TEXT}" | decode_entities);
TEXT=$(echo "${TEXT}" | sed -e 's/&/\&/g' -e 's/\</g' -e 's/>/\>/g' -e 's/"/\"/g' -e 's/'"'"'/\'/g');
;;
escape:json-val)
TEXT=$(echo "${TEXT}" | sed -e 's/"/\\"/g' | awk '(NR==1) { printf "%s", $0 } (NR>1) { printf "\\n%s", $0 }');
;;
excerpt:*)
MAX_CHARS="${OPTIONS[0]:-}"
MAX_PARAS="${OPTIONS[1]:-}"
if [[ "${MAX_CHARS}" != "" ]]; then
[[ "${MAX_CHARS}" -lt 1 ]] && TEXT="" && break
TEXT=$(echo "${TEXT}" | text_filter headings:remove plaintext)
if [[ "${MAX_PARAS}" != "" ]]; then
TEXT=$(echo "${TEXT}" | awk -v RS="\n\n" 'NR<='${MAX_PARAS}'+1 { print $0 }')
fi
TEXT_TEMP=$(echo "${TEXT:0:$MAX_CHARS}")
if [[ "${TEXT}" != "${TEXT_TEMP}" ]]; then
if [[ "${MAX_CHARS}" -gt 3 ]]; then
TEXT=$(echo "${TEXT_TEMP:0:$((MAX_CHARS-3))}")
else
TEXT=""
fi
TEXT="${TEXT}..."
fi
TEXT=$(echo "${TEXT}${LF}")
fi
;;
excerpt)
TEXT=$(echo "${TEXT}" | text_filter excerpt:300:2)
;;
headings:push)
TEXT=$(echo "${TEXT}${LF}" | sed -E -e 'N;s/^(.*)\n([-]+|[=]+)$/# \1/' | sed -E -e 's/^([#]+ )/#\1/' -e 's/^####### (.+)$/**\1**/')
TEXT=$(echo "${TEXT}" | sed -E -e '/<\/?h[0-6][^>]*>/{s/<(\/)?h6/<\1strong/g' -e 's/<(\/)?h5/<\1h6/g' -e 's/<(\/)?h4/<\1h5/g' -e 's/<(\/)?h3/<\1h4/g' -e 's/<(\/)?h2/<\1h3/g' -e 's/<(\/)?h1/<\1h2/g' -e '}')
;;
headings:shift)
TEXT=$(echo "${TEXT}${LF}" | sed -E -e 'N;s/^(.*)\n([-]+|[=]+)$/# \1/' | sed -E -e '/^# .+$/d' -e 's/^#([#]+ )/\1/')
TEXT=$(echo "${TEXT}" | sed -E -e '/
]*>.*<\/h1>/s///g' | sed -E -e '/<\/?h[0-6][^>]*>/{s/<(\/)h2/<\1h1/g' -e 's/<(\/)h3/<\1h2/g' -e 's/<(\/)h4/<\1h3/g' -e 's/<(\/)h5/<\1h4/g' -e 's/<(\/)h6/<\1h5/g' -e '}')
# '/./,$!d' deletes blank lines at start
TEXT=$(echo "${TEXT}" | sed -e '/./,$!d')
;;
headings:remove)
TEXT=$(echo "${TEXT}${LF}" | sed -E -e 'N;s/^(.*)\n([-]+|[=]+)$/# \1/' | sed -E -e '/^[#]+ .+$/d')
TEXT=$(echo "${TEXT}" | sed -E -e 's/<(h1|h2|h3|h4|h5|h6)[^>]*>.*<\/(h1|h2|h3|h4|h5|h6)>//g')
;;
plaintext)
# ':a;s/<[^>]*>//g;/]*>//g')
# '/./,/^$/!d' deletes blank lines at start and collapses consecutive blank lines into one, leaving up to one blank line at end
TEXT=$(echo "${TEXT}" | sed -e ':a' -e 's/<[^>]*>//g' -e '/${LF}${TEXT}${LF}${LF}")
;;
esac
done
echo "${TEXT}"
}
# prints a list of variables given their assignment commands, one per line, e.g. FOO="bar" prints FOO
function get_variables { # get_variables assignments_string
local STRING
STRING=$(/dev/null 2>&1 || return 1
DIRECTORY=$(pwd -P . 2>/dev/null || command pwd)
cd "${CURRENT_DIR}"
# child dir
cd "${CHILD_DIR}" >/dev/null 2>&1 || return 1
CHILD_DIR=$(pwd -P . 2>/dev/null || command pwd)
cd "${CURRENT_DIR}"
# compare
if [[ -f "${CHILD}" ]] && [[ "${DIRECTORY}" == "${CHILD_DIR}" ]]; then
# file in directory root
return 0
fi
if [[ "${DIRECTORY}" != "${CHILD_DIR}" ]] && [[ "${CHILD_DIR#$DIRECTORY}" != "${CHILD_DIR}" ]]; then
# subdirectory or file in subdirectory
return 0
fi
return 1
}
# prints an integer transformation of a version string
# useful for simple comparisons, max input 999.999.999
# [[ $(version 2.10.9) -gt $(version 1.0.5) ]] # true
# [[ 2010009 -gt 1000005 ]] # true
function version { # version x(.x)?(.x)?
echo $(printf "%03d%03d%03d\n" $(echo "${1:-}" | sed -E -e 's/^([0-9]+(\.[0-9]+(\.[0-9]+)?)?).*$/\1/' | tr '.' '\n' | head -n 3)) | sed 's/^0*//' | sed 's/^$/0/'
}
# checks for required external tool
function dependency_exists { # dependency_exists dep
local DEP ERROR
DEP="${1:-}"; [[ "${DEP}" == "" ]] && return 1
ERROR=false
if echo "${DEP}" | grep '/' >/dev/null 2>&1 && [[ ! -x "${DEP}" ]]; then
echo "Unable to find command: ${DEP}" >&2
ERROR=true
elif ! hash "${DEP}" >/dev/null 2>&1; then
echo "Unable to find command: ${DEP}" >&2
ERROR=true
fi
[[ "${ERROR}" == true ]] && return 1
return 0
}
# checks for required external tools (plural)
function dependencies_exist { # dependencies_exist deps[@]
local DEPS ERRORS
DEPS=("${@}");
ERRORS=()
for DEP in ${DEPS[@]}; do
dependency_exists "${DEP}" 2>/dev/null || ERRORS+=("${DEP}")
done
if [[ "${#ERRORS[@]}" -ne 0 ]]; then
echo "Unable to find command(s): ${ERRORS[*]}" >&2
return 1
fi
return 0
}
# prints dependency requirements
function print_dependency { # print_dependency name url required_version found_version
local NAME URL VERSION FOUND
NAME="${1:-}"; [[ "${NAME}" == "" ]] && return 1
URL="${2:-}"
VERSION="${3:-}"
FOUND="${4:-}"
echo -n "${NAME}" >&2
if [[ "${VERSION}" != "" ]]; then
echo -n " version ${VERSION} or later" >&2
fi
echo -n " is required" >&2
if [[ "${FOUND}" != "" ]]; then
echo -n " (found version ${FOUND})" >&2
fi
if [[ "${URL}" != "" ]]; then
echo -n ": ${URL}" >&2
fi
echo >&2
}
# prints bash variable assignments for template metadata
function parse_template_metadata { # parse_template_metadata prefix template_dir template_config_extra
local PREFIX TEMPLATE_DIR TEMPLATE_CONFIG_EXTRA
local TEMPLATE_META TEMPLATE_META_VARS
local TEMPLATE_ASSETS_FONTS TEMPLATE_ASSETS_STYLES TEMPLATE_ASSETS_SCRIPTS TEMPLATE_ASSETS_IMAGES TEMPLATE_ASSETS_AUDIO TEMPLATE_ASSETS_VIDEO TEMPLATE_ASSETS_DOCUMENTS TEMPLATE_ASSETS_OTHER
local INDEX ASSET_TYPES ASSET_TYPE_UPPER ASSET FILTER_INDEX REGEX EXTENDED MATCH REPLACE EXTRA ASSETS_INDEX ASSETS ASSETS_TEMP
# init
PREFIX="${1:-}"
TEMPLATE_DIR="${2:-}"
[[ "${TEMPLATE_DIR}" == "" ]] && return 1
TEMPLATE_CONFIG_EXTRA="${3:-}"
# load config
if [[ -f "${TEMPLATE_DIR}/template.yaml" ]] && [[ -r "${TEMPLATE_DIR}/template.yaml" ]]; then
TEMPLATE_META=$(parse_yaml "${TEMPLATE_DIR}/template.yaml" "MARSH_TEMPLATE_")
[[ $? -ne 0 ]] && echo "Unable to parse yaml file: ${TEMPLATE_DIR}/template.yaml" >&2 && return 1
TEMPLATE_META_VARS=$(echo "${TEMPLATE_META}" | get_variables)
eval "${TEMPLATE_META}"
else
echo "Template configuration file not found: ${TEMPLATE_DIR}/template.yaml" >&2
return 1
fi
if [[ "${TEMPLATE_CONFIG_EXTRA}" != "" ]]; then
# load extra config
if [[ -f "${TEMPLATE_CONFIG_EXTRA}" ]] && [[ -r "${TEMPLATE_CONFIG_EXTRA}" ]]; then
TEMPLATE_META=$(parse_yaml "${TEMPLATE_CONFIG_EXTRA}" "MARSH_TEMPLATE_")
[[ $? -ne 0 ]] && echo "Unable to parse yaml file: ${TEMPLATE_CONFIG_EXTRA}" >&2 && return 1
TEMPLATE_META_VARS=$(echo "${TEMPLATE_META_VARS}"$'\n'"${TEMPLATE_META}" | get_variables)
eval "${TEMPLATE_META}"
else
echo "Template configuration file not found: ${TEMPLATE_CONFIG_EXTRA}" >&2
return 1
fi
fi
# print partials paths relative to template dir
[[ "${MARSH_TEMPLATE_Template_Partials_Base:-}" != "" ]] && echo "${PREFIX}Template_Partials_Base=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Base//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Body:-}" != "" ]] && echo "${PREFIX}Template_Partials_Body=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Body//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Document:-}" != "" ]] && echo "${PREFIX}Template_Partials_Document=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Document//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Header:-}" != "" ]] && echo "${PREFIX}Template_Partials_Header=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Header//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Nav:-}" != "" ]] && echo "${PREFIX}Template_Partials_Nav=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Nav//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Notice:-}" != "" ]] && echo "${PREFIX}Template_Partials_Notice=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Notice//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Footer:-}" != "" ]] && echo "${PREFIX}Template_Partials_Footer=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Footer//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Redirect:-}" != "" ]] && echo "${PREFIX}Template_Partials_Redirect=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Redirect//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Archive_HTML_Body:-}" != "" ]] && echo "${PREFIX}Template_Partials_Archive_HTML_Body=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Archive_HTML_Body//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Archive_HTML_Document:-}" != "" ]] && echo "${PREFIX}Template_Partials_Archive_HTML_Document=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Archive_HTML_Document//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Archive_Atom_Body:-}" != "" ]] && echo "${PREFIX}Template_Partials_Archive_Atom_Body=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Archive_Atom_Body//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Archive_Atom_Document:-}" != "" ]] && echo "${PREFIX}Template_Partials_Archive_Atom_Document=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Archive_Atom_Document//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Archive_JSON_Body:-}" != "" ]] && echo "${PREFIX}Template_Partials_Archive_JSON_Body=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Archive_JSON_Body//\"/\\\"}\""
[[ "${MARSH_TEMPLATE_Template_Partials_Archive_JSON_Document:-}" != "" ]] && echo "${PREFIX}Template_Partials_Archive_JSON_Document=\"${TEMPLATE_DIR}/${MARSH_TEMPLATE_Template_Partials_Archive_JSON_Document//\"/\\\"}\""
# assets
ASSET_TYPES=('Fonts' 'Styles' 'Scripts' 'Images' 'Audio' 'Video' 'Documents' 'Binaries' 'Other')
for ASSET_TYPE in ${ASSET_TYPES[@]}; do
eval "TEMPLATE_ASSETS_${ASSET_TYPE}=()"
INDEX=0
ASSET="MARSH_TEMPLATE_Template_Assets_${ASSET_TYPE}_${INDEX}"
ASSET="${!ASSET:-}"
while [[ "${ASSET}" != "" ]]; do
eval "TEMPLATE_ASSETS_${ASSET_TYPE}+=(\"${ASSET//\"/\\\"}\")"
INDEX=$((INDEX+1))
ASSET="MARSH_TEMPLATE_Template_Assets_${ASSET_TYPE}_${INDEX}"
ASSET="${!ASSET:-}"
done
done
# assets filters
FILTER_INDEX=0
REGEX="MARSH_TEMPLATE_Template_Filters_${FILTER_INDEX}_Regex"
REGEX="${!REGEX:-}"
EXTENDED="MARSH_TEMPLATE_Template_Filters_${FILTER_INDEX}_Extended"
EXTENDED="${!EXTENDED:-}"
# loop over assets filters
while [[ "${REGEX}" != "" ]]; do
# escape match
if [[ "$(echo ${REGEX} | grep -E '^s/.+[^\]/.*[^\]/g?$')" == "${REGEX}" ]]; then
MATCH=$(echo "${REGEX}" | sed -E 's/^s\/(.+[^\])\/.*[^\]\/g?/\1/' | sed 's/[[]\.|$(){}?+*^]/\\&/g')
REPLACE=$(echo "${REGEX}" | sed -E 's/^s\/.+[^\]\/(.*[^\])\/g?/\1/')
EXTRA=$(echo "${REGEX}" | sed -E 's/^s\/.+[^\]\/.*[^\]\/(g?)/\1/')
REGEX="s/${MATCH}/${REPLACE}/${EXTRA}"
elif [[ "$(echo ${REGEX} | grep -E '^s\|.+[^\]\|.*[^\]\|g?$')" == "${REGEX}" ]]; then
MATCH=$(echo "${REGEX}" | sed -E 's/^s\|(.+[^\])\|.*[^\]\|g?/\1/' | sed 's/[[]\.|$(){}?+*^]/\\&/g')
REPLACE=$(echo "${REGEX}" | sed -E 's/^s\|.+[^\]\|(.*[^\])\|g?/\1/')
EXTRA=$(echo "${REGEX}" | sed -E 's/^s\|.+[^\]\|.*[^\]\|(g?)/\1/')
REGEX="s|${MATCH}|${REPLACE}|${EXTRA}"
elif [[ "$(echo ${REGEX} | grep -E '^/.+[^\]/d?$')" == "${REGEX}" ]]; then
MATCH=$(echo "${REGEX}" | sed -E 's/^\/(.+[^\])\/d?/\1/' | sed 's/[[]\.|$(){}?+*^]/\\&/g')
REPLACE=""
EXTRA=$(echo "${REGEX}" | sed -E 's/^\/.+[^\]\/(d?)/\1/')
REGEX="/${MATCH}/${EXTRA}"
else
continue
fi
ASSETS_INDEX=0
ASSETS_TYPE="MARSH_TEMPLATE_Template_Filters_${FILTER_INDEX}_Assets_${ASSETS_INDEX}"
ASSETS_TYPE="${!ASSETS_TYPE}"
# loop over assets types for filter
while [[ "${ASSETS_TYPE}" != "" ]]; do
ASSETS="TEMPLATE_ASSETS_${ASSETS_TYPE}[@]"
ASSETS=(${!ASSETS})
ASSETS_TEMP=()
# loop over all individual assets of this type
for ASSET in ${ASSETS[@]}; do
# do regex
if [[ "${EXTENDED}" == true ]]; then
ASSET=$(echo "${ASSET}" | sed -E "${REGEX}" 2>/dev/null)
else
ASSET=$(echo "${ASSET}" | sed "${REGEX}" 2>/dev/null)
fi
# remove empty
if [[ "${ASSET:-}" != "" ]]; then
ASSETS_TEMP+=("${ASSET}")
fi
done
# update original array
eval "TEMPLATE_ASSETS_${ASSETS_TYPE}=()"
for I in "${!ASSETS_TEMP[@]}"; do
eval "TEMPLATE_ASSETS_${ASSETS_TYPE}+=(\"${ASSETS_TEMP[$I]//\"/\\\"}\")"
done
ASSETS_INDEX=$((ASSETS_INDEX+1))
ASSETS_TYPE="MARSH_TEMPLATE_Template_Filters_${FILTER_INDEX}_Assets_${ASSETS_INDEX}"
ASSETS_TYPE="${!ASSETS_TYPE:-}"
done
FILTER_INDEX=$((FILTER_INDEX+1))
REGEX="MARSH_TEMPLATE_Template_Filters_${FILTER_INDEX}_Regex"
REGEX="${!REGEX:-}"
EXTENDED="MARSH_TEMPLATE_Template_Filters_${FILTER_INDEX}_Extended"
EXTENDED="${!EXTENDED:-}"
ASSETS_INDEX=0
ASSETS_TYPE="MARSH_TEMPLATE_Template_Filters_${FILTER_INDEX}_Assets_${ASSETS_INDEX}"
ASSETS_TYPE="${!ASSETS_TYPE:-}"
done
# print assets
for ASSET_TYPE in ${ASSET_TYPES[@]}; do
ASSET_TYPE_UPPER=$(echo "${ASSET_TYPE}" | awk '{ print toupper($0) }')
ASSETS="TEMPLATE_ASSETS_${ASSET_TYPE}[@]"
ASSETS=(${!ASSETS})
echo "${PREFIX}ASSETS_${ASSET_TYPE_UPPER}=()"
for ASSET in ${ASSETS[@]}; do
[[ "${ASSET:-}" != "" ]] && echo "${PREFIX}ASSETS_${ASSET_TYPE_UPPER}+=(\"${ASSET//\"/\\\"}\")"
done
done
# unset temporary global variables
for VAR in ${TEMPLATE_META_VARS[@]}; do unset "${VAR}"; done
}
# loads target metadata into convenience variables
function get_target_metadata { # get_target_metadata source_prefix prefix
local SOURCE_PREFIX PREFIX
local TARGET_PATH TARGET_NAME TEMPLATE_SOURCE TARGET_TYPE TEMPLATE_REMOTE TARGET_FORCE TEMPLATE_FETCH TARGET_NAV
local II TEMPLATE_SOURCE TEMPLATE_REMOTE TEMPLATE_FETCH TEMPLATE_CONFIG
local ARCHIVE ARCHIVE_NAME ARCHIVE_ENCODING ARCHIVE_COUNT ARCHIVE_CONTENT ARCHIVE_TYPES ARCHIVE_EXCLUDED_STATES
# init
SOURCE_PREFIX="${1:-}"
PREFIX="${2:-}"
# root
TARGET_PATH="${SOURCE_PREFIX}Path"
[[ -z ${!TARGET_PATH+isset} ]] && return 1
TARGET_NAME="${SOURCE_PREFIX}Name"
TARGET_SOURCE="${SOURCE_PREFIX}Source"
TARGET_TYPE="${SOURCE_PREFIX}Type"
TARGET_REMOTE="${SOURCE_PREFIX}Remote"
TARGET_FORCE="${SOURCE_PREFIX}Force"
TARGET_FETCH="${SOURCE_PREFIX}Fetch"
TARGET_NAV="${SOURCE_PREFIX}Navigation"
[[ "${!TARGET_PATH:-}" != "" ]] && echo "${PREFIX}PATH=\"${!TARGET_PATH//\"/\\\"}\""
[[ "${!TARGET_NAME:-}" != "" ]] && echo "${PREFIX}NAME=\"${!TARGET_NAME//\"/\\\"}\""
[[ "${!TARGET_SOURCE:-}" != "" ]] && echo "${PREFIX}SOURCE=\"${!TARGET_SOURCE//\"/\\\"}\""
[[ "${!TARGET_TYPE:-}" != "" ]] && echo "${PREFIX}TYPE=\"${!TARGET_TYPE//\"/\\\"}\""
[[ "${!TARGET_REMOTE:-}" != "" ]] && echo "${PREFIX}REMOTE=\"${!TARGET_REMOTE//\"/\\\"}\""
[[ "${!TARGET_NAV:-}" != "" ]] && echo "${PREFIX}NAV=\"${!TARGET_NAV//\"/\\\"}\""
[[ "${!TARGET_FORCE:-}" != "" ]] && echo "${PREFIX}FORCE=\"${!TARGET_FORCE//\"/\\\"}\""
[[ "${!TARGET_FETCH:-}" != "" ]] && echo "${PREFIX}FETCH=\"${!TARGET_FETCH//\"/\\\"}\""
# templates
echo "${PREFIX}TEMPLATES_SOURCE=()"
echo "${PREFIX}TEMPLATES_REMOTE=()"
echo "${PREFIX}TEMPLATES_FETCH=()"
echo "${PREFIX}TEMPLATES_CONFIG=()"
echo "${PREFIX}TEMPLATES=()"
II=0
TEMPLATE_SOURCE="${SOURCE_PREFIX}Templates_${II}_Source"
TEMPLATE_REMOTE="${SOURCE_PREFIX}Templates_${II}_Remote"
TEMPLATE_FETCH="${SOURCE_PREFIX}Templates_${II}_Fetch"
TEMPLATE_CONFIG="${SOURCE_PREFIX}Templates_${II}_Config"
while [[ "${!TEMPLATE_SOURCE:-}" != "" ]]; do
echo "${PREFIX}TEMPLATES_SOURCE+=(\"${!TEMPLATE_SOURCE//\"/\\\"}\")"
echo "${PREFIX}TEMPLATES_REMOTE+=(\"${!TEMPLATE_REMOTE//\"/\\\"}\")"
if [[ "${!TEMPLATE_FETCH:-}" == "true" ]]; then
echo "${PREFIX}TEMPLATES_FETCH+=(true)"
else
echo "${PREFIX}TEMPLATES_FETCH+=(false)"
fi
echo "${PREFIX}TEMPLATES_CONFIG+=(\"${!TEMPLATE_CONFIG//\"/\\\"}\")"
echo "${PREFIX}TEMPLATES+=(\"${!TEMPLATE_SOURCE##*/}-$(md5_string ${!TEMPLATE_REMOTE:-local}:${!TEMPLATE_SOURCE})\")"
II=$((II+1))
TEMPLATE_SOURCE="${SOURCE_PREFIX}Templates_${II}_Source"
TEMPLATE_REMOTE="${SOURCE_PREFIX}Templates_${II}_Remote"
TEMPLATE_FETCH="${SOURCE_PREFIX}Templates_${II}_Fetch"
TEMPLATE_CONFIG="${SOURCE_PREFIX}Templates_${II}_Config"
done
# archives
echo "${PREFIX}ARCHIVES=()"
echo "${PREFIX}ARCHIVES_ENCODING=()"
echo "${PREFIX}ARCHIVES_NAME=()"
echo "${PREFIX}ARCHIVES_COUNT=()"
echo "${PREFIX}ARCHIVES_CONTENT=()"
echo "${PREFIX}ARCHIVES_TYPES=()"
echo "${PREFIX}ARCHIVES_EXCLUDED_STATES=()"
II=0
ARCHIVE="${SOURCE_PREFIX}Archives_${II}_Path"
ARCHIVE_ENCODING="${SOURCE_PREFIX}Archives_${II}_Encoding"
ARCHIVE_NAME="${SOURCE_PREFIX}Archives_${II}_Name"
ARCHIVE_COUNT="${SOURCE_PREFIX}Archives_${II}_Count"
ARCHIVE_CONTENT="${SOURCE_PREFIX}Archives_${II}_Content"
while [[ "${!ARCHIVE:-}" != "" ]]; do
echo "${PREFIX}ARCHIVES+=(\"${!ARCHIVE//\"/\\\"}\")"
echo "${PREFIX}ARCHIVES_ENCODING+=(\"${!ARCHIVE_ENCODING//\"/\\\"}\")"
echo "${PREFIX}ARCHIVES_NAME+=(\"${!ARCHIVE_NAME//\"/\\\"}\")"
echo "${PREFIX}ARCHIVES_COUNT+=(\"${!ARCHIVE_COUNT//\"/\\\"}\")"
echo "${PREFIX}ARCHIVES_CONTENT+=(\"${!ARCHIVE_CONTENT//\"/\\\"}\")"
eval $(flat_to_array "ARCHIVE_TYPES" "${SOURCE_PREFIX}Archives_${II}_Types_")
OIFS="${IFS}"; IFS=$(echo @|tr @ '\034'); echo "${PREFIX}ARCHIVES_TYPES+=(\"${ARCHIVE_TYPES[*]//\"/\\\"}\")"; IFS="${OIFS}"
eval $(flat_to_array "ARCHIVE_EXCLUDED_STATES" "${SOURCE_PREFIX}Archives_${II}_Exclude_")
OIFS="${IFS}"; IFS=$(echo @|tr @ '\034'); echo "${PREFIX}ARCHIVES_EXCLUDED_STATES+=(\"${ARCHIVE_EXCLUDED_STATES[*]//\"/\\\"}\")"; IFS="${OIFS}"
II=$((II+1))
ARCHIVE="${SOURCE_PREFIX}Archives_${II}_Path"
ARCHIVE_ENCODING="${SOURCE_PREFIX}Archives_${II}_Encoding"
ARCHIVE_NAME="${SOURCE_PREFIX}Archives_${II}_Name"
ARCHIVE_COUNT="${SOURCE_PREFIX}Archives_${II}_Count"
ARCHIVE_CONTENT="${SOURCE_PREFIX}Archives_${II}_Content"
done
return 0
}
# copies files, fetching if necessary and authorized
function get_files { # get_files source dest force fetch uri
local SOURCE DEST FORCE FETCH URI
local SOURCE_DIR CACHE TYPE REMOTE FILES
SOURCE="${1:-}"
DEST="${2:-}"
if [[ "${SOURCE}" == "" ]]; then
echo "Internal error copying files: source empty" >&2
return 1
elif [[ "${DEST}" == "" ]]; then
echo "Internal error copying files: destination empty" >&2
return 1
fi
SOURCE_DIR="${SOURCE%/*}"
SOURCE_DIR="${SOURCE_DIR:-.}"
FORCE="${3:-}"
FETCH="${4:-}"
URI="${5:-}"
CACHE=$(md5_string "${URI}")
[[ "${CACHE}" == "" ]] && CACHE=$(md5_string "${SOURCE}")
CACHE="${MARSH_TEMP}/${CACHE}"
TYPE="file"
REMOTE=false
if [[ "${URI}" != "" ]]; then
if [[ "${URI}" =~ ^(git|https?|ssh): ]] || [[ "${URI}" =~ ^.+@.+: ]]; then
# remote git
TYPE="git"
REMOTE=true
elif [[ "${URI}" =~ ^file: ]] || [[ "${URI}" =~ .git$ ]]; then
# local git
TYPE="git"
fi
fi
if [[ "${REMOTE}" != true ]] || [[ "${FETCH}" == true ]]; then
# fetch unnecessary/approved
if [[ "${TYPE}" != "file" ]] && [[ ! -e "${CACHE}" ]]; then
# cache does not exist, create it
mkdir -p "${CACHE}"
if [[ "${TYPE}" == "git" ]]; then
# clone repository
echo "Retrieving source: ${URI}"
if ! "${GIT}" clone --depth 1 --recursive "${URI}" "${CACHE}" >/dev/null 2>&1; then
# unable to clone repository
echo "Unable to retrieve source: ${URI}" >&2
return 1
fi
fi
fi
if [[ "${TYPE}" == "git" ]]; then
# files path relative to cache
SOURCE="${CACHE}/${SOURCE}"
SOURCE_DIR="${SOURCE%/*}"
SOURCE_DIR="${SOURCE_DIR:-.}"
fi
# select files for copy
if [[ -d "${SOURCE}" ]]; then
# entire directory
FILES="${SOURCE#$SOURCE_DIR/}"
DEST="${DEST%/$FILES}"
FILES=("${FILES}")
else
# directory contents
OIFS="${IFS}"
IFS=$'\n'
FILES=($(cd "${SOURCE_DIR}" >/dev/null 2>&1 && find . -maxdepth 1 -name "${SOURCE##*/}" | sed -e 's/^\.\///' -e 's/^\.$//'))
[[ $? -ne 0 ]] && IFS="${OIFS}" && echo "Unable to change into directory: ${SOURCE_DIR}" >&2 && return 1
IFS="${OIFS}"
fi
# loop over files
for FILE in ${FILES[@]}; do
# sanity
if [[ "${SOURCE_DIR}/${FILE}" == "${DEST%/}/${FILE}" ]]; then
# same source and destination
echo "Source and destination paths are equal: ${DEST}" >&2
return 1
fi
if [[ -e "${DEST%/}/${FILE}" ]] && [[ "${FORCE}" != true ]]; then
# path exists
echo "Path exists (--force to overwrite): ${DEST%/}/${FILE}" >&2
return 1
fi
# create path
DEST="${DEST%/.}"
if ! mkdir -p "${DEST}"; then
echo "Unable to create path: ${DEST}" >&2
return 1
fi
# clean destination
if ! rm -rf "${DEST%/}/${FILE}"; then
echo "Unable to replace path: ${DEST}" >&2
return 1
fi
# copy
if ! cp -R "${SOURCE_DIR}/${FILE}" "${DEST%/}/${FILE}"; then
echo "Unable to copy file: ${SOURCE_DIR}/${FILE} -> ${DEST%/}/${FILE}" >&2
return 1
fi
done
return 0
else
# fetch not explicitly authorized
echo "Remote source not authorized (--fetch to override): ${URI}" >&2
return 1
fi
}
# builds an archive of directory contents, sorted by descending date
function build_archive { # build_archive encoding target_dir dest_file dest_title content_length count #types[@] types[@] #excluded_states[@] excluded_states[@]
local ENCODING TARGET_DIR DEST DEST_SUB_PREFIX DEST_TITLE CONTENT COUNT NUM_TYPES TYPES NUM_EXCLUDED_STATES EXCLUDED_STATES
local VALID DOCUMENTS DOCUMENTS_MAX DOCUMENT_META_PREFIX DOCUMENT_META DOCUMENT_META_VARS
local DOCUMENT TITLE DATE DATE_PRETTY LANGCODE STATE EXCLUDE DEST_SUB URI ABSPATH
# init
ENCODING="${1:-html}"
shift 1
TARGET_DIR="${1:-}"; [[ "${TARGET_DIR}" == "" ]] && return 1
DEST="${2:-}"; [[ "${DEST}" == "" ]] && return 1
DEST="${TARGET_DIR}/${DEST}"
DEST_SUB_PREFIX="${DEST}.${MARSH_UID}.subdocuments"
DEST_TITLE="${3:-}"
CONTENT="${4:-}"
CONTENT_VALID=('excerpt' 'none' '')
if ! in_array "${CONTENT%%:*}" "${CONTENT_VALID[@]}"; then
echo "Invalid archive content type: ${CONTENT%%:*}" >&2
return 1
fi
COUNT="${5:-}"
shift 5
NUM_TYPES="${1:-0}"
shift 1
TYPES=()
while [[ "${NUM_TYPES}" -gt 0 ]] && [[ "${#@}" -gt 0 ]]; do
TYPES+=("${1}")
NUM_TYPES=$((NUM_TYPES - 1 ))
shift
done
NUM_EXCLUDED_STATES="${1:-0}"
shift 1
EXCLUDED_STATES=()
while [[ "${NUM_EXCLUDED_STATES}" -gt 0 ]] && [[ "${#@}" -gt 0 ]]; do
EXCLUDED_STATES+=("${1}")
NUM_EXCLUDED_STATES=$((NUM_EXCLUDED_STATES - 1 ))
shift
done
# create directory
mkdir -p "${DEST%/*}" >/dev/null 2>&1 || return 1
# write yaml header
echo "---" > "${DEST}" 2>/dev/null || return 1
if [[ "${ENCODING}" == "html" ]]; then
echo "Type: archive" >> "${DEST}"
else
echo "Type: archive-${ENCODING}" >> "${DEST}"
fi
echo "Date: $(date -u '+%Y-%m-%d %H:%M:%S')" >> "${DEST}"
echo "Title: ${DEST_TITLE}" >> "${DEST}"
echo "Language: English" >> "${DEST}"
echo "Language_Code: en" >> "${DEST}"
echo "---" >> "${DEST}"
echo "" >> "${DEST}"
if [[ "${ENCODING}" == "html" ]]; then
echo "# ${DEST_TITLE}" >> "${DEST}"
echo "" >> "${DEST}"
fi
# loop over documents
VALID=0
OIFS="${IFS}"
IFS=$'\n'
DOCUMENTS=($(cd "${TARGET_DIR}" && find . -name '*.'"${MARSH_UID}"'.document' | sed 's/^\.\///' | sort -nr -t/))
IFS="${OIFS}"
DOCUMENTS_MAX="${#DOCUMENTS[@]}"
DOCUMENTS_MAX=$((DOCUMENTS_MAX - 1))
for DOCUMENT in "${DOCUMENTS[@]}"; do
if [[ "${COUNT}" != "" ]] && [[ "${VALID}" -ge "${COUNT}" ]]; then
break
fi
DOCUMENT_META_PREFIX=$(echo "marsh_meta_${DOCUMENT}_" | sed -E -e 's/[^A-Za-z0-9_]/_/g')
DOCUMENT_META=$(parse_yaml "${TARGET_DIR}/${DOCUMENT%.document}.meta.yaml" "${DOCUMENT_META_PREFIX}")
[[ $? -ne 0 ]] && echo "Unable to parse yaml file: ${TARGET_DIR}/${DOCUMENT%.document}.meta.yaml" >&2 && return 1
DOCUMENT_META_VARS=$(echo "${DOCUMENT_META}" | get_variables)
eval "${DOCUMENT_META}"
TYPE="${DOCUMENT_META_PREFIX}Type"
TYPE="${!TYPE:-}"
if [[ "${TYPE%%-*}" == "archive" ]]; then
# archives are skipped by default
continue
fi
if [[ "${#TYPES[@]}" -gt 0 ]] && ! in_array "${TYPE}" "${TYPES[@]}"; then
# skip type if not in list
continue
fi
if [[ "${#EXCLUDED_STATES[@]}" -gt 0 ]]; then
STATE="${DOCUMENT_META_PREFIX}State"
STATE="${!STATE}"
if [[ "${STATE}" != "" ]]; then
# deprecated (no brackets)
# Key: foo, bar, baz
STATE=(${STATE//, /})
else
# YAML inline array
# Key: [ foo, bar, baz ]
eval $(flat_to_array "STATE" "${DOCUMENT_META_PREFIX}State_")
fi
for E in "${EXCLUDED_STATES[@]}"; do
if in_array "${E}" "${STATE[@]}"; then
# skip excluded state
continue 2
fi
done
fi
# valid subdocument
VALID=$((VALID + 1))
DEST_SUB="${DEST_SUB_PREFIX}.$((VALID-1))"
# add subdocument title
TITLE="${DOCUMENT_META_PREFIX}Title"
TITLE="${!TITLE:-}"
URI="${TARGET_DIR#$MARSH_CONFIG_PATH}"
URI="${MARSH_CONFIG_URL%/}/${URI#/}/${DOCUMENT%.${MARSH_UID}.document}"
URI="${URI%.markdown}.html"
ABSPATH=""
[[ "${URI}" =~ / ]] && ABSPATH="${URI%/*}"
if [[ "${ENCODING}" == "html" ]]; then
echo "## [${TITLE}](${URI})" >> "${DEST_SUB}.document"
echo "" >> "${DEST_SUB}.document"
fi
# process text
if [[ "${CONTENT}" == "none" ]]; then
# no content
# title already added
touch "${DEST_SUB}.document"
elif [[ "${CONTENT%%:}" == "excerpt" ]]; then
# excerpt
# filters strip duplicate title
<"${TARGET_DIR:-.}/${DOCUMENT}" text_filter headings:push "${CONTENT}" "wrap:div.$(echo ${CONTENT} | sed -e 's/:[^ ]//g' -e 's/ /./g')" >> "${DEST_SUB}.document"
else
# full
# filters strip duplicate title
<"${TARGET_DIR:-.}/${DOCUMENT}" text_filter headings:shift headings:push headings:push >> "${DEST_SUB}.document"
fi
# copy metadata
cp "${TARGET_DIR}/${DOCUMENT%.document}.meta.yaml" "${DEST_SUB}.meta.yaml.temp" >/dev/null 2>&1
# store paths (cannot calculate original path later)
<"${DEST_SUB}.meta.yaml.temp" sed -e '/^\.\.\.$/d' > "${DEST_SUB}.meta.yaml"
echo "---" >> "${DEST_SUB}.meta.yaml"
echo "ABSPATH: ${ABSPATH}" >> "${DEST_SUB}.meta.yaml"
echo "URI: ${URI}" >> "${DEST_SUB}.meta.yaml"
echo "..." >> "${DEST_SUB}.meta.yaml"
# unset temporary global variables
for VAR in ${DOCUMENT_META_VARS[@]}; do unset "${VAR}"; done
done
return 0
}
# builds advanced navigation
function build_advanced_nav { # build_advanced_nav target_dir navigation_file
local TARGET_DIR NAV_NAME
local NAV_TARGETS NAV_DEST
# init
TARGET_DIR="${1:-}"; [[ "${TARGET_DIR}" == "" ]] && return 1
NAV_NAME="${2:-}"; [[ "${NAV_NAME}" == "" ]] && return 1
NAV_TARGETS=$(cd "${TARGET_DIR}" && find . -name "${NAV_NAME}" | sed 's/^\.\///')
for NAV_TARGET in ${NAV_TARGETS[@]}; do
NAV_TARGET="${TARGET_DIR}/${NAV_TARGET}"
NAV_DEST="${NAV_TARGET}.${MARSH_UID}.advanced-nav.html"
cp "${NAV_TARGET}" "${NAV_DEST}"
if head -n 1 "${NAV_DEST}" 2>/dev/null | grep -E '^---[ ]*(#.*)?$' >/dev/null 2>&1; then
# remove yaml
<"${NAV_DEST}" tail -n +$(awk '{ drop = 0; } /^---[ ]*(#.*)?$/ { if (NR==1) { drop = 1 } else if (NR>1) { exit } else { drop = 0; next } } drop == 0 { print }' "${NAV_DEST}" | wc -l | awk '{ print $1+3 }') > "${NAV_DEST}.temp"
else
cp "${NAV_DEST}" "${NAV_DEST}.temp"
fi
<"${NAV_DEST}.temp" process_markdown > "${NAV_DEST}"
rm -f "${NAV_DEST}.temp"
done
}
# builds a template and applies it to a document
function build_template { # build_template dest_dir target_dir document templates_dir template template_config_extra
function process_authors { # process_authors dest_array source_array
local DEST_ARRAY SOURCE_ARRAY ITEM
DEST_ARRAY="${1:-}"
[[ "${DEST_ARRAY}" == "" ]] && return 1
SOURCE_ARRAY="${2:-}"
[[ "${SOURCE_ARRAY}" == "" ]] && return 1
echo "${DEST_ARRAY}=()"
SOURCE_ARRAY="${SOURCE_ARRAY}[@]"
for ITEM in "${!SOURCE_ARRAY}"; do
ITEM=$(echo "${ITEM}" | sed -E -e 's/[ ]+<[^>]+>//g' -e 's/[ ]/\ \;/g')
echo "${DEST_ARRAY}+=(\"${ITEM}\")"
done
return 0
}
function process_conditionals { # process_conditionals file tag comparitor list[@]
local FILE TAG COMPARITOR LIST
local FILE_TEMP
FILE="${1:-}"; [[ "${FILE}" == "" ]] && return 1
TAG="${2:-}"; [[ "${TAG}" == "" ]] && return 1
COMPARITOR="${3:-}"; [[ "${COMPARITOR}" == "" ]] && return 1
LIST="${4:-}"; [[ "${LIST}" == "" ]] && return 1
LIST=(${4:-})
FILE_TEMP=$(mktemp "${MARSH_TEMP}/conditional-XXXXXX")
# process match
for ITEM in ${LIST[@]}; do
cp "${FILE}" "${FILE_TEMP}"
<"${FILE_TEMP}" \
awk '/^[ ]*\{%[ ]*if[ ]+'${TAG//\./\.}'[ ]+'${COMPARITOR// /[ ]+}'[ ]+"'${ITEM}'"[ ]*%\}[ ]*$/ {
tag=1;
line=1;
delete buffer;
}
{
buffer[line++]=$0;
}
(tag==1 && (/^[ ]*\{%[ ]*endif[ ]*%\}[ ]*$/ || /^[ ]*\{%[ ]*else[ ]*%\}[ ]*$/)) {
for (i=2; i < line-1; i++) {
print buffer[i];
}
}
(tag>0 && /^[ ]*\{%[ ]*else[ ]*%\}[ ]*$/) {
tag=2;
next;
}
(tag>0 && /^[ ]*\{%[ ]*endif[ ]*%\}[ ]*$/) {
tag=0;
next;
}
(tag==0) {
print;
}' \
> "${FILE}"
done
# process else
cp "${FILE}" "${FILE_TEMP}"
<"${FILE_TEMP}" \
awk '/^[ ]*\{%[ ]*if[ ]+'${TAG//\./\.}'[ ]+'${COMPARITOR// /[ ]+}'[ ]+".+"[ ]*%\}[ ]*$/ {
tag=1;
line=1;
delete buffer;
}
{
buffer[line++]=$0;
}
(tag==1) {
delete buffer;
}
(tag==2 && /^[ ]*\{%[ ]*endif[ ]*%\}[ ]*$/) {
for (i=2; i < line-1; i++) {
print buffer[i];
}
}
(tag>0 && /^[ ]*\{%[ ]*else[ ]*%\}[ ]*$/) {
tag=2;
next;
}
(tag>0 && /^[ ]*\{%[ ]*endif[ ]*%\}[ ]*$/) {
tag=0;
next;
}
(tag==0) {
print;
}' \
> "${FILE}"
}
function clean_conditionals { # clean_conditionals file
local FILE FILE_TEMP
FILE="${1:-}"; [[ "${FILE}" == "" ]] && return 1
FILE_TEMP=$(mktemp "${MARSH_TEMP}/conditional-XXXXXX")
# remove unsupported conditionals
cp "${FILE}" "${FILE_TEMP}"
<"${FILE_TEMP}" awk '/^[ ]*\{%[ ]*if[^%]+%\}[ ]*$/ {k=1;i=1;delete a} {a[i++]=$0} (k==1 && /^[ ]*\{%[ ]*endif[ ]*%\}[ ]*$/) {k=0;next} (k==0) { print }' > "${FILE}"
return 0
}
function replace_tags { # replace_tags document prefix
local DOCUMENT PREFIX
local TAGS TAG_ESC TAG_VAL TAG_NAME TAG_FILTERS TAG_VAR TAG_READFILE
# init
DOCUMENT="${1:-}"
PREFIX="${2:-}"
[[ "${DOCUMENT}" == "" ]] && return 1
# find template tags
OIFS="${IFS}"
IFS=$'\n'
TAGS=($(<"${DOCUMENT}" grep -Eo '\{\{[ ]*([a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)?)[ ]*(([ ]*\|[ ]*[a-z][a-z0-9-]*(\:[a-z0-9][a-z0-9-]*)*)*)[ ]*\}\}'))
IFS="${OIFS}"
# loop over tags
for I in "${!TAGS[@]}"; do
TAG_ESC=$(echo "${TAGS[$I]}" | sed 's/[][/.|$(){}?+*^]/\\&/g')
TAG_VAL=$(echo "${TAGS[$I]}" | sed -E -e 's/\{\{[ ]*([a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)?)[ ]*(([ ]*\|[ ]*[a-z][a-z0-9-]*(\:[a-z0-9][a-z0-9-]*)*)*)[ ]*\}\}/\1\3/')
TAG_NAME="${TAG_VAL%%|*}"
TAG_FILTERS="${TAG_VAL#$TAG_NAME}"
TAG_FILTERS=(${TAG_FILTERS//|/ })
TAG_READFILE=false
# map tag to variable
case "${TAG_NAME}" in
base.relpath)
TAG_VAR="${PREFIX}RELPATH"
;;
document.relpath)
TAG_VAR="${PREFIX}RELPATH"
;;
base.abspath)
TAG_VAR="${PREFIX}ABSPATH"
;;
document.abspath)
TAG_VAR="${PREFIX}ABSPATH"
;;
document.uri)
TAG_VAR="${PREFIX}URI"
;;
document)
TAG_VAR="${PREFIX}CONTENT"
TAG_READFILE=true
;;
document.authors)
TAG_VAR="${PREFIX}AUTHORS"
;;
document.copyright)
TAG_VAR="${PREFIX}Copyright"
;;
document.credits-url)
TAG_VAR="${PREFIX}Credits_URL"
;;
document.date)
TAG_VAR="${PREFIX}Date"
;;
document.language)
TAG_VAR="${PREFIX}Language"
;;
document.language-code)
TAG_VAR="${PREFIX}Language_Code"
;;
document.license)
TAG_VAR="${PREFIX}License"
;;
document.license-abbr)
TAG_VAR="${PREFIX}License_Abbr"
;;
document.license-url)
TAG_VAR="${PREFIX}License_URL"
;;
document.project)
TAG_VAR="${PREFIX}Project"
;;
document.project-version)
TAG_VAR="${PREFIX}Project_Version"
;;
document.project-url)
TAG_VAR="${PREFIX}Project_URL"
;;
document.redirect-url)
TAG_VAR="${PREFIX}Redirect_URL"
;;
document.state)
TAG_VAR="${PREFIX}State"
;;
document.title)
TAG_VAR="${PREFIX}Title"
;;
document.type)
TAG_VAR="${PREFIX}Type"
;;
util.json-separator)
TAG_VAR="UTIL_JSON_SEPARATOR"
;;
*)
TAG_VAR=""
;;
esac
if [[ "${TAG_VAR}" != "" ]]; then
if [[ "${TAG_READFILE}" == true ]]; then
if [[ -f "${!TAG_VAR}" ]] && [[ -r "${!TAG_VAR}" ]]; then
TAG_VAR=$(<"${!TAG_VAR}")
else
return 1
fi
else
TAG_VAR="${!TAG_VAR}"
fi
TAG_VAR=$(echo "${TAG_VAR}" | text_filter "${TAG_FILTERS[@]}" | sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
TAG_VAR=${TAG_VAR%$'\n'}
sed -E \
-e "/${TAG_ESC}/s//${TAG_VAR:-}/g" \
-i".${MARSH_UID}.backup" "${DOCUMENT}" && rm -f "${DOCUMENT}.${MARSH_UID}.backup"
else
# ignore unknown tags
:
fi
done
}
local DEST_DIR TARGET_DIR
local DOCUMENT DOCUMENT_EXT DOCUMENT_DIR DOCUMENT_RELPATH DOCUMENT_ABSPATH DOCUMENT_URI
local DEST DEST_NAME
local TEMPLATE_DIR TEMPLATE TEMPLATE_CONFIG_EXTRA
local TEMPLATE_PREFIX TEMPLATE_META TEMPLATE_META_VARS PARTIALS TEMPLATE_BASE TEMPLATE_TEMP
local TEMPLATE_STYLES TEMPLATE_SCRIPTS
local INDEX DOCUMENT_AUTHORS DOCUMENT_META DOCUMENT_META_VARS
local SUBDOCUMENTS NUM_SUBDOCUMENTS
local SUBDOCUMENT SUBDOCUMENT_AUTHORS SUBDOCUMENT_CONTENT SUBDOCUMENT_META SUBDOCUMENT_META_VARS
local DOCUMENT_Authors DOCUMENT_State SUBDOCUMENT_Authors
local NAV NAV_DIR NAV_NAME NAV_RELPATH
# init
DEST_DIR="${1:-.}"
DEST_DIR="${DEST_DIR%/}"
TARGET_DIR="${2:-.}"
TARGET_DIR="${TARGET_DIR%/}"
DOCUMENT="${3:-}"
[[ "${DOCUMENT}" == "" ]] && return 1
DOCUMENT_EXT="${DOCUMENT##*.}"
DOCUMENT_DIR="./${DOCUMENT}"
DOCUMENT_DIR="${DOCUMENT_DIR%/*}"
DOCUMENT_DIR="${DOCUMENT_DIR#./}"
DOCUMENT_RELPATH="${DOCUMENT#./}" # strip current dir
DOCUMENT_RELPATH="${DOCUMENT_RELPATH//[^\/]}" # leave only slashes
DOCUMENT_RELPATH="${DOCUMENT_RELPATH//[\/]/../}" # slashes to dirs
DOCUMENT_RELPATH="${DOCUMENT_RELPATH:-./}" # empty to current dir
DOCUMENT_ABSPATH="${MARSH_CONFIG_URL:-}"
DOCUMENT_ABSPATH="${DOCUMENT_ABSPATH%/}/${TARGET_DIR}/${DOCUMENT_DIR}"
DOCUMENT_ABSPATH="${DOCUMENT_ABSPATH%/.}"
DEST="${DOCUMENT}"
DEST_NAME="${DEST%.$DOCUMENT_EXT}"
DEST_NAME="${DEST_NAME##*/}"
if echo "${DOCUMENT_EXT}" | grep -Ei '^(markdown|md|mkd|mkdn|mdown)$' >/dev/null 2>&1; then
DEST="${DEST%.*}.html"
fi
if [[ "${TARGET_DIR}" != "." ]]; then
DEST="${TARGET_DIR}/${DEST}"
fi
DEST="${DEST_DIR}/${DEST}"
DOCUMENT="${DEST_DIR}/${TARGET_DIR}/${DOCUMENT}"
DOCUMENT_URI="${DOCUMENT_ABSPATH:+$DOCUMENT_ABSPATH/}${DEST##*/}"
TEMPLATE_DIR="${4:-}"
TEMPLATE="${5:-}"
TEMPLATE_CONFIG_EXTRA="${6:-}"
# utility vars
UTIL_JSON_SEPARATOR=","
# verbosity
# seems logical to put this elsewhere, but this is a good catch-all for now
echo " ${DEST#$DEST_DIR/}"
# document
DOCUMENT_CONTENT="${DOCUMENT}.${MARSH_UID}.document"
[[ -e "${DOCUMENT_CONTENT}" ]] || return 1
# document metadata
if [[ -f "${DOCUMENT}.${MARSH_UID}.meta.yaml" ]] && [[ -r "${DOCUMENT}.${MARSH_UID}.meta.yaml" ]]; then
# parse metadata
DOCUMENT_META=$(parse_yaml "${DOCUMENT}.${MARSH_UID}.meta.yaml" "DOCUMENT_")
[[ $? -ne 0 ]] && echo "Unable to parse yaml file: ${DOCUMENT}.${MARSH_UID}.meta.yaml" >&2 && return 1
DOCUMENT_META_VARS=$(echo "${DOCUMENT_META}" | sed -E -e 's/^([^=]+)=.*/\1/')
eval "${DOCUMENT_META}"
fi
# authors
DOCUMENT_AUTHORS=()
eval $(flat_to_array "DOCUMENT_Authors" "DOCUMENT_Authors_")
eval $(process_authors "DOCUMENT_AUTHORS" "DOCUMENT_Authors")
DOCUMENT_AUTHORS=$(join_delimited ', ' ' and ' ', and ' ${DOCUMENT_AUTHORS[@]})
# state
eval $(flat_to_array "DOCUMENT_State" "DOCUMENT_State_")
# template
if [[ "${TEMPLATE_DIR}" != "" ]]; then
TEMPLATE_PREFIX="TEMPLATE_"
TEMPLATE_META=$(parse_template_metadata "${TEMPLATE_PREFIX}" "${TEMPLATE_DIR}/${TEMPLATE}" "${TEMPLATE_CONFIG_EXTRA}")
[[ $? -ne 0 ]] && return 1
TEMPLATE_META_VARS=$(echo "${TEMPLATE_META}" | get_variables)
eval "${TEMPLATE_META}"
# partials
PARTIALS=("${TEMPLATE_Template_Partials_Redirect:-}" "${TEMPLATE_Template_Partials_Base:-}" "${TEMPLATE_Template_Partials_Body:-}" "${TEMPLATE_Template_Partials_Document:-}" "${TEMPLATE_Template_Partials_Header:-}" "${TEMPLATE_Template_Partials_Footer:-}" "${TEMPLATE_Template_Partials_Nav:-}" "${TEMPLATE_Template_Partials_Notice:-}" "${TEMPLATE_Template_Partials_Archive_HTML_Body:-}" "${TEMPLATE_Template_Partials_Archive_HTML_Document:-}" "${TEMPLATE_Template_Partials_Archive_Atom_Body:-}" "${TEMPLATE_Template_Partials_Archive_Atom_Document:-}" "${TEMPLATE_Template_Partials_Archive_JSON_Body:-}" "${TEMPLATE_Template_Partials_Archive_JSON_Document:-}")
# conditionals
for J in "${!PARTIALS[@]}"; do
[[ "${PARTIALS[$J]}" == "" ]] && continue
# grep to avoid unnecessary processing
if [[ "$(<${PARTIALS[$J]} grep -Eo '\{\%[ ]*if[ ]+')" != "" ]]; then
# template contains conditionals, operate using a copy
TEMPLATE_TEMP=$(mktemp "${MARSH_TEMP}/template-XXXXXX")
cp "${PARTIALS[$J]}" "${TEMPLATE_TEMP}"
PARTIALS[$J]="${TEMPLATE_TEMP}"
# process
process_conditionals "${PARTIALS[$J]}" "document.state" "contains" "${DOCUMENT_State[@]}"
# remove remaining conditionals
clean_conditionals "${PARTIALS[$J]}"
fi
done
TEMPLATE_Template_Partials_Redirect="${PARTIALS[0]:-}"
TEMPLATE_Template_Partials_Base="${PARTIALS[1]:-}"
TEMPLATE_Template_Partials_Body="${PARTIALS[2]:-}"
TEMPLATE_Template_Partials_Document="${PARTIALS[3]:-}"
TEMPLATE_Template_Partials_Header="${PARTIALS[4]:-}"
TEMPLATE_Template_Partials_Footer="${PARTIALS[5]:-}"
TEMPLATE_Template_Partials_Nav="${PARTIALS[6]:-}"
TEMPLATE_Template_Partials_Notice="${PARTIALS[7]:-}"
TEMPLATE_Template_Partials_Archive_HTML_Body="${PARTIALS[8]:-}"
TEMPLATE_Template_Partials_Archive_HTML_Document="${PARTIALS[9]:-}"
TEMPLATE_Template_Partials_Archive_Atom_Body="${PARTIALS[10]:-}"
TEMPLATE_Template_Partials_Archive_Atom_Document="${PARTIALS[11]:-}"
TEMPLATE_Template_Partials_Archive_JSON_Body="${PARTIALS[12]:-}"
TEMPLATE_Template_Partials_Archive_JSON_Document="${PARTIALS[13]:-}"
# body template
if [[ "${DOCUMENT_Type}" == "archive-atom" ]]; then
if [[ "${TEMPLATE_Template_Partials_Archive_Atom_Body:-}" != "" ]]; then
cp "${TEMPLATE_Template_Partials_Archive_Atom_Body}" "${DEST}"
else
echo "Atom archive body partial not found for primary template: ${TEMPLATE}" >&2
return 1
fi
elif [[ "${DOCUMENT_Type}" == "archive-json" ]]; then
if [[ "${TEMPLATE_Template_Partials_Archive_JSON_Body:-}" != "" ]]; then
cp "${TEMPLATE_Template_Partials_Archive_JSON_Body}" "${DEST}"
else
echo "JSON archive body partial not found for primary template: ${TEMPLATE}" >&2
return 1
fi
elif [[ "${DOCUMENT_Type}" == "archive" ]]; then
if [[ "${TEMPLATE_Template_Partials_Archive_HTML_Body:-}" != "" ]]; then
cp "${TEMPLATE_Template_Partials_Archive_HTML_Body}" "${DEST}"
else
echo "HTML archive body partial not found for primary template: ${TEMPLATE}" >&2
return 1
fi
else
if [[ "${TEMPLATE_Template_Partials_Body}" != "" ]]; then
cp "${TEMPLATE_Template_Partials_Body}" "${DEST}"
else
echo "Body partial not found for primary template: ${TEMPLATE}" >&2
return 1
fi
fi
# base template
if [[ "${DOCUMENT_Type}" != "archive-atom" ]] && [[ "${DOCUMENT_Type}" != "archive-json" ]]; then
if [[ "${DOCUMENT_Redirect_URL:-}" != "" ]]; then
# redirect
TEMPLATE_BASE="${TEMPLATE_Template_Partials_Redirect}"
fi
TEMPLATE_BASE="${TEMPLATE_BASE:-$TEMPLATE_Template_Partials_Base}"
if [[ "${TEMPLATE_BASE}" != "" ]]; then
# inject into base template
if [[ -f "${TEMPLATE_BASE}" ]] && [[ -r "${TEMPLATE_BASE}" ]]; then
cp "${DEST}" "${DEST}.${MARSH_UID}.temp"
cp "${TEMPLATE_BASE}" "${DEST}"
SUB=$(<"${DEST}.${MARSH_UID}.temp" sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
SUB=${SUB%$'\n'}
SUB=${SUB%\\}
sed -E \
-e "/\{\{[ ]*template\.body[ ]*\}\}/s//${SUB:-}/g" \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
unset SUB
fi
fi
fi
# remaining templates
if [[ "${DOCUMENT_Type}" == "archive" ]]; then
if [[ -f "${TEMPLATE_Template_Partials_Archive_HTML_Document}" ]] && [[ -r "${TEMPLATE_Template_Partials_Archive_HTML_Document}" ]]; then
SUB=$(<"${TEMPLATE_Template_Partials_Archive_HTML_Document}" sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
SUB=${SUB%$'\n'}
SUB=${SUB%\\}
sed -E \
-e "/\{\{[ ]*template\.document[ ]*\}\}/s//${SUB:-}/g" \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
unset SUB
fi
else
if [[ -f "${TEMPLATE_Template_Partials_Document}" ]] && [[ -r "${TEMPLATE_Template_Partials_Document}" ]]; then
SUB=$(<"${TEMPLATE_Template_Partials_Document}" sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
SUB=${SUB%$'\n'}
SUB=${SUB%\\}
sed -E \
-e "/\{\{[ ]*template\.document[ ]*\}\}/s//${SUB:-}/g" \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
unset SUB
fi
fi
if [[ -f "${TEMPLATE_Template_Partials_Header}" ]] && [[ -r "${TEMPLATE_Template_Partials_Header}" ]]; then
SUB=$(<"${TEMPLATE_Template_Partials_Header}" sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
SUB=${SUB%$'\n'}
SUB=${SUB%\\}
sed -E \
-e "/\{\{[ ]*template\.header[ ]*\}\}/s//${SUB:-}/g" \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
unset SUB
fi
if [[ -f "${TEMPLATE_Template_Partials_Footer}" ]] && [[ -r "${TEMPLATE_Template_Partials_Footer}" ]]; then
SUB=$(<"${TEMPLATE_Template_Partials_Footer}" sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
SUB=${SUB%$'\n'}
SUB=${SUB%\\}
sed -E \
-e "/\{\{[ ]*template\.footer[ ]*\}\}/s//${SUB:-}/g" \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
unset SUB
fi
if [[ -f "${TEMPLATE_Template_Partials_Nav}" ]] && [[ -r "${TEMPLATE_Template_Partials_Nav}" ]]; then
SUB=$(<"${TEMPLATE_Template_Partials_Nav}" sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
SUB=${SUB%$'\n'}
SUB=${SUB%\\}
sed -E \
-e "/\{\{[ ]*template\.nav[ ]*\}\}/s//${SUB:-}/g" \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
unset SUB
fi
if [[ -f "${TEMPLATE_Template_Partials_Notice}" ]] && [[ -r "${TEMPLATE_Template_Partials_Notice}" ]]; then
SUB=$(<"${TEMPLATE_Template_Partials_Notice}" sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
SUB=${SUB%$'\n'}
SUB=${SUB%\\}
sed -E \
-e "/\{\{[ ]*template\.notice[ ]*\}\}/s//${SUB:-}/g" \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
unset SUB
fi
# scripts and styles
TEMPLATE_SCRIPTS=""
for SCRIPT in ${TEMPLATE_ASSETS_SCRIPTS[@]}; do
TEMPLATE_SCRIPTS="${TEMPLATE_SCRIPTS}"
done
TEMPLATE_SCRIPTS=$(echo "${TEMPLATE_SCRIPTS}" | sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
TEMPLATE_STYLES=""
for STYLE in ${TEMPLATE_ASSETS_STYLES[@]}; do
TEMPLATE_STYLES="${TEMPLATE_STYLES}"
done
TEMPLATE_STYLES=$(echo "${TEMPLATE_STYLES}" | sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
sed -E \
-e '/\{\{[ ]*template\.scripts[ ]*\}\}/s||'"${TEMPLATE_SCRIPTS}"'|g' \
-e '/\{\{[ ]*template\.styles[ ]*\}\}/s||'"${TEMPLATE_STYLES}"'|g' \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
# advanced navigation
for NAV in $(cd "${DEST_DIR}/${TARGET_DIR}" && find . -name "*.${MARSH_UID}.advanced-nav.html" | sed 's/^\.\///'); do
NAV_NAME="${NAV##*/}"
NAV_NAME="${NAV_NAME%.$MARSH_UID.advanced-nav.html}.html"
NAV_NAME=$(echo "${NAV_NAME}" | sed -E -e 's/\.(markdown|md|mkd|mkdn|mdown)(.html)$/\2/')
NAV_DIR="./${NAV}"
NAV_DIR="${NAV_DIR%/*}"
NAV_DIR="${NAV_DIR#./}"
if is_child "${DEST_DIR}/${TARGET_DIR}/${NAV_DIR}" "${DOCUMENT}"; then
NAV_RELPATH="${DOCUMENT_DIR#$NAV_DIR}"
NAV_RELPATH="${NAV_RELPATH//[^\/]}" # leave only slashes
NAV_RELPATH="${NAV_RELPATH//[\/]/../}" # slashes to dirs
NAV_RELPATH="${NAV_RELPATH:-.}" # empty to current dir
NAV_RELPATH="${NAV_RELPATH%/}" # drop trailing slash
<"${DEST_DIR}/${TARGET_DIR}/${NAV}" sed -E \
-e 's|([^<]+)
||' \
-e 's|(]*)>([^<>]+)|\2|g' \
> "${DEST}.${MARSH_UID}.temp"
SUB=$(<"${DEST}.${MARSH_UID}.temp" sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
SUB=${SUB%$'\n'}
SUB=${SUB%\\}
sed -E \
-e "/\{\{[ ]*navigation[ ]*\}\}/s//${SUB}/g" \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
unset SUB
fi
done
sed -E \
-e 's/\{\{[ ]*navigation[ ]*\}\}//g' \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
# remove unused/unknown template includes
sed -E \
-e '/\{\{[ ]*template(\.[a-z]+)?[ ]*\}\}/s///' \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
# template tags
replace_tags "${DEST}" "DOCUMENT_"
# subdocuments
OIFS="${IFS}"
IFS=$'\n'
SUBDOCUMENTS=($(cd "${DEST_DIR}/${TARGET_DIR}/${DOCUMENT_DIR}" && find . -name "${DEST_NAME}${DOCUMENT_EXT:+.$DOCUMENT_EXT}.*" | grep -E '.+\.'"${MARSH_UID}"'\.subdocuments\.[0-9]+\.document$' | sed 's/^\.\///' | sed -E -e 's/(\.'"${MARSH_UID}"'\.subdocuments\.)/\1\|/' | sort -t \| -k2 -g | sed -E -e 's/(\.'"${MARSH_UID}"'\.subdocuments\.)\|/\1/'))
NUM_SUBDOCUMENTS="${#SUBDOCUMENTS[@]}"
IFS="${OIFS}"
if [[ "${NUM_SUBDOCUMENTS}" -gt 0 ]]; then
for J in ${!SUBDOCUMENTS[@]}; do
# full path
SUBDOCUMENT="${DEST_DIR}/${TARGET_DIR}/${DOCUMENT_DIR}/${SUBDOCUMENTS[$J]}"
# document metadata
if [[ -f "${SUBDOCUMENT%.document}.meta.yaml" ]] && [[ -r "${SUBDOCUMENT%.document}.meta.yaml" ]]; then
# parse metadata
SUBDOCUMENT_META=$(parse_yaml "${SUBDOCUMENT%.document}.meta.yaml" "SUBDOCUMENT_")
[[ $? -ne 0 ]] && echo "Unable to parse yaml file: ${SUBDOCUMENT%.document}.meta.yaml" >&2 && return 1
SUBDOCUMENT_META_VARS=$(echo "${SUBDOCUMENT_META}" | sed -E -e 's/^([^=]+)=.*/\1/')
eval "${SUBDOCUMENT_META}"
fi
cp "${SUBDOCUMENT}" "${SUBDOCUMENT}.document"
SUBDOCUMENT_CONTENT="${SUBDOCUMENT}.document"
# authors
SUBDOCUMENT_AUTHORS=()
eval $(flat_to_array "SUBDOCUMENT_Authors" "SUBDOCUMENT_Authors_")
eval $(process_authors "SUBDOCUMENT_AUTHORS" "SUBDOCUMENT_Authors")
SUBDOCUMENT_AUTHORS=$(join_delimited ', ' ' and ' ', and ' ${SUBDOCUMENT_AUTHORS[@]})
# state
eval $(flat_to_array "SUBDOCUMENT_State" "SUBDOCUMENT_State_")
if [[ "${DOCUMENT_Type}" == "archive-atom" ]]; then
# document template
if [[ "${TEMPLATE_Template_Partials_Archive_Atom_Document:-}" != "" ]]; then
cp "${SUBDOCUMENT}" "${SUBDOCUMENT}.document"
cp "${TEMPLATE_Template_Partials_Archive_Atom_Document}" "${SUBDOCUMENT}"
fi
elif [[ "${DOCUMENT_Type}" == "archive-json" ]]; then
# document template
if [[ "${TEMPLATE_Template_Partials_Archive_JSON_Document:-}" != "" ]]; then
cp "${SUBDOCUMENT}" "${SUBDOCUMENT}.document"
cp "${TEMPLATE_Template_Partials_Archive_JSON_Document}" "${SUBDOCUMENT}"
fi
else
# markdown to html
<"${SUBDOCUMENT}" process_markdown > "${SUBDOCUMENT}.document"
# document template
if [[ "${DOCUMENT_Type}" == "archive" ]]; then
cp "${TEMPLATE_Template_Partials_Archive_HTML_Document}" "${SUBDOCUMENT}"
else
cp "${TEMPLATE_Template_Partials_Document}" "${SUBDOCUMENT}"
fi
fi
# inject html into template
SUB=$(<"${SUBDOCUMENT}.document" sed -e ':a' -e '$!{N' -e 'ba' -e '}' -e 's/[&/\]/\\&/g' -e 's/\n/\\&/g')
SUB=${SUB%$'\n'}
SUB=${SUB%\\}
sed -E \
-e "/\{\{[ ]*document[ ]*\}\}/s//${SUB}/g" \
-i".${MARSH_UID}.backup" "${SUBDOCUMENT}" && rm -f "${SUBDOCUMENT}.${MARSH_UID}.backup"
unset SUB
sed -E \
-e 's/\{\{[ ]*subdocuments[ ]*\}\}//g' \
-i".${MARSH_UID}.backup" "${SUBDOCUMENT}" && rm -f "${SUBDOCUMENT}.${MARSH_UID}.backup"
# template tags
if [[ "${DOCUMENT_Type}" == "archive-json" ]] && [[ "${J}" -eq $((NUM_SUBDOCUMENTS - 1)) ]]; then
sed -E \
-e 's/\{\{[ ]*util.json-separator[ ]*\}\}//g' \
-i".${MARSH_UID}.backup" "${SUBDOCUMENT}" && rm -f "${SUBDOCUMENT}.${MARSH_UID}.backup"
fi
replace_tags "${SUBDOCUMENT}" "SUBDOCUMENT_"
# combine with previous subdocuments
cat "${SUBDOCUMENT}" >> "${DOCUMENT}.${MARSH_UID}.subdocuments"
# unset temporary global variables
for VAR in ${SUBDOCUMENT_META_VARS[@]}; do unset "${VAR}"; done
done
cp "${DEST}" "${DEST}.${MARSH_UID}.temp"
<"${DEST}.${MARSH_UID}.temp" awk 'BEGIN { while (getline < "'"${DOCUMENT}.${MARSH_UID}.subdocuments"'") txt=txt $0 "\n"; gsub(/[&]/, "\\\\&", txt) } /\{\{[ ]*subdocuments[ ]*\}\}/{ gsub(/\{\{[ ]*subdocuments[ ]*\}\}/, txt) } 1' > "${DEST}"
fi
# unset temporary global variables
for VAR in ${TEMPLATE_META_VARS[@]}; do unset "${VAR}"; done
else
cp "${DOCUMENT_CONTENT}" "${DEST}"
fi # TEMPLATE_DIR != "" else
# smart comment tags
sed -E \
-e 's||<\1 class="\2"\3>|g' \
-e 's||<\1 id="\2"\3>|g' \
-e 's||<\1 class="\2">|g' \
-e 's||<\1 id="\2">|g' \
-e 's||\1>|g' \
-e 's|||g' \
-e 's||
|g' \
-e 's||
|g' \
-e 's||
|g' \
-e 's||
|g' \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
# multiple classes foo.bar.baz to foo bar baz
sed -E \
-e ':a' -e 's|class="([^."]+)\.([^"]+)"|class="\1 \2"|g' -e 'ta' \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
# escape double quotes for JSON
if [[ "${DOCUMENT_Type}" == "archive-json" ]]; then
sed -E \
-e ':a' -e 's|class="([^"]+)"|class=\\"\1\\"|g' -e 'ta' \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
fi
# unwrap figures
sed -E \
-e 's|
||' \
-e 's|
||' \
-i".${MARSH_UID}.backup" "${DEST}" && rm -f "${DEST}.${MARSH_UID}.backup"
# unset temporary global variables
for VAR in ${DOCUMENT_META_VARS[@]}; do unset "${VAR}"; done
}
# builds a document from a single file
function build_document { # build_document dest_dir target_dir document advanced_nav_name
local DEST_DIR TARGET_DIR DOCUMENT ADVANCED_NAV
local DOCUMENT_EXT DEST DEST_NAME
# init
DEST_DIR="${1:-.}"
DEST_DIR="${DEST_DIR%/}"
TARGET_DIR="${2:-.}"
TARGET_DIR="${TARGET_DIR%/}"
DOCUMENT="${3:-}"
[[ "${DOCUMENT}" == "" ]] && return 1
DOCUMENT_EXT="${DOCUMENT##*.}"
DOCUMENT_DIR="./${DOCUMENT}"
DOCUMENT_DIR="${DOCUMENT_DIR%/*}"
DOCUMENT_DIR="${DOCUMENT_DIR#./}"
DOCUMENT="${DEST_DIR}/${TARGET_DIR}/${DOCUMENT}"
TEMPLATE_DIR="${4:-}"
TEMPLATE_CONFIG_EXTRA="${5:-}"
ADVANCED_NAV="${6:-}"
ADVANCED_NAV="${ADVANCED_NAV%.$DOCUMENT_EXT}"
# normalize extensions
if echo "${DOCUMENT_EXT}" | grep -Ei '^(markdown|md|mkd|mkdn|mdown)$' >/dev/null 2>&1; then
DOCUMENT_EXT="markdown"
fi
# split yaml and markdown, keep original
if head -n 1 "${DOCUMENT}" 2>/dev/null | grep -E '^---[ ]*(#.*)?$' >/dev/null 2>&1; then
echo "---" > "${DOCUMENT}.${MARSH_UID}.meta.yaml"
<"${DOCUMENT}" awk '{ drop = 0; } /^---[ ]*(#.*)?$/ { if (NR==1) { drop = 1 } else if (NR>1) { exit } else { drop = 0; next } } drop == 0 { print }' >> "${DOCUMENT}.${MARSH_UID}.meta.yaml"
<"${DOCUMENT}" tail -n +$(wc -l "${DOCUMENT}.${MARSH_UID}.meta.yaml" | awk '{ print $1+3 }') > "${DOCUMENT}.${MARSH_UID}.document"
echo "..." >> "${DOCUMENT}.${MARSH_UID}.meta.yaml"
fi
if [[ "${DOCUMENT_EXT}" == "markdown" ]]; then
# preprocess markdown to add implicit figures
# convert preprocessed markdown to html
# unwrap figures (remove p tag)
# process images with multiple resolution assets
cp "${DOCUMENT}.${MARSH_UID}.document" "${DOCUMENT}.${MARSH_UID}.document.temp"
<"${DOCUMENT}.${MARSH_UID}.document.temp" sed -E \
-e 's|^!\[(.+)]\([ ]*([^ ]+)[ ]*"(.+)"[ ]*\)$|
\3|' |
process_markdown |
sed -E \
-e 's|
||' \
-e 's|
||' |
awk '
{
if ($0 ~ " src=\"[^\"]+\"")
{
split($0, a, " ");
for(i=1; i<=length(a); i++)
{
if (a[i] ~ "^src=\"[^\"]+\"$")
{
f=a[i]; gsub(/^src="/, "", f);
gsub(/"$/, "", f);
f1=f " 1x"; f2=f;
gsub(/\.[^\.]+$/, "@2x&", f2);
f2abs="'${DEST_DIR}/${TARGET_DIR}/${DOCUMENT_DIR}/'" f2;
f3=f;
gsub(/\.[^\.]+$/, "@3x&", f3);
f3abs="'${DEST_DIR}/${TARGET_DIR}/${DOCUMENT_DIR}/'" f3;
if (getline temp < f2abs > 0)
{
f2=", " f2 " 2x";
} else {
f2="";
}
if (getline temp < f3abs > 0)
{
f3=", " f3 " 3x";
} else {
f3="";
}
print "src=\"" f "\" srcset=\"" f1 f2 f3 "\" ";
} else {
print a[i];
}
}
} else {
print $0;
}
}' > "${DOCUMENT}.${MARSH_UID}.document"
fi
# unset temporary global variables
for VAR in ${DOCUMENT_META_VARS[@]}; do unset "${VAR}"; done
}
# builds a target, parallel if possible
function build_target { # build_target dest_dir target advanced_nav templates_dir #templates[@] templates[@] #templates_config_extra[@] templates_config_extra[@]
local DEST_DIR TARGET ADVANCED_NAV TEMPLATES_DIR NUM_TEMPLATES TEMPLATES NUM_TEMPLATES_CONFIG_EXTRA TEMPLATES_CONFIG_EXTRA
local TEMPLATE_META TEMPLATE_META_VARS
local TARGET_DIR TEMPLATE_PREFIX DOCUMENTS PARALLEL
# init
DEST_DIR="${1:-.}"
DEST_DIR="${DEST_DIR%/}"
TARGET="${2:-}"
TARGET="${TARGET%/}"
TARGET_DIR="${TARGET%/*}"
TARGET_DIR="${TARGET_DIR:-.}"
TARGET="${TARGET:-*}"
ADVANCED_NAV="${3:-}"
TEMPLATES_DIR="${4:-}"
shift 4
NUM_TEMPLATES="${1:-0}"
shift 1
TEMPLATES=()
while [[ "${NUM_TEMPLATES}" -gt 0 ]] && [[ "${#@}" -gt 0 ]]; do
TEMPLATES+=("${1}")
NUM_TEMPLATES=$((NUM_TEMPLATES - 1 ))
shift
done
NUM_TEMPLATES_CONFIG_EXTRA="${1:-0}"
shift 1
TEMPLATES_CONFIG_EXTRA=()
while [[ "${NUM_TEMPLATES_CONFIG_EXTRA}" -gt 0 ]] && [[ "${#@}" -gt 0 ]]; do
TEMPLATES_CONFIG_EXTRA+=("${1}")
NUM_TEMPLATES_CONFIG_EXTRA=$((NUM_TEMPLATES_CONFIG_EXTRA - 1 ))
shift
done
# copy template assets
if [[ "${TEMPLATES_DIR}" != "" ]]; then
for I in "${!TEMPLATES[@]}"; do
TEMPLATE_PREFIX="TEMPLATE_"
TEMPLATE_META=$(parse_template_metadata "${TEMPLATE_PREFIX}" "${TEMPLATES_DIR}/${TEMPLATES[$I]}" "${TEMPLATES_CONFIG_EXTRA[$I]}")
[[ $? -ne 0 ]] && return 1
TEMPLATE_META_VARS=$(echo "${TEMPLATE_META}" | get_variables)
eval "${TEMPLATE_META}"
for ASSET in ${TEMPLATE_ASSETS_FONTS[@]} ${TEMPLATE_ASSETS_STYLES[@]} ${TEMPLATE_ASSETS_SCRIPTS[@]} ${TEMPLATE_ASSETS_IMAGES[@]} ${TEMPLATE_ASSETS_AUDIO[@]} ${TEMPLATE_ASSETS_VIDEO[@]} ${TEMPLATE_ASSETS_DOCUMENTS[@]} ${TEMPLATE_ASSETS_BINARIES[@]} ${TEMPLATE_ASSETS_OTHER[@]}; do
ASSET="${ASSET%\?*}"
if [[ -d "${TEMPLATES_DIR}/${TEMPLATES[$I]}/${ASSET}" ]]; then
mkdir -p "${DEST_DIR}/${TARGET_DIR}/${ASSET}"
elif [[ -f "${TEMPLATES_DIR}/${TEMPLATES[$I]}/${ASSET}" ]]; then
mkdir -p "${DEST_DIR}/${TARGET_DIR}/${ASSET%/*}"
fi
if [[ -e "${TEMPLATES_DIR}/${TEMPLATES[$I]}/${ASSET}" ]] && [[ -r "${TEMPLATES_DIR}/${TEMPLATES[$I]}/${ASSET}" ]]; then
cp -R "${TEMPLATES_DIR}/${TEMPLATES[$I]}/${ASSET}" "${DEST_DIR}/${TARGET_DIR}/${ASSET}"
fi
done
# unset temporary global variables
for VAR in ${TEMPLATE_META_VARS[@]}; do unset "${VAR}"; done
done
fi
# build target documents
if [[ "${TARGET}" == '*' ]]; then
DOCUMENTS=$(cd "${DEST_DIR}/${TARGET_DIR}" && find *)
elif [[ -d "${DEST_DIR}/${TARGET}" ]]; then
if [[ "${TARGET}" == "." ]]; then
DOCUMENTS=$(cd "${DEST_DIR}" && find . -maxdepth 1)
else
DOCUMENTS=$(cd "${DEST_DIR}/${TARGET}" && find .)
fi
else
DOCUMENTS=$(cd "${DEST_DIR}/${TARGET_DIR}" && find "${TARGET##*/}")
fi
DOCUMENTS=$(echo "${DOCUMENTS}" | grep -Ei '\.(markdown|md|mkd|mkdn|mdown|xml|json)$' | sed 's/^\.\///')
PARALLEL=$(which parallel)
PARALLEL_MARKDOWN=""
[[ "${MARKDOWN}" != "markdown" ]] && PARALLEL_MARKDOWN="${MARKDOWN}"
if [[ "${JOBS:-1}" -gt 1 ]] && [[ "${PARALLEL:-}" != "" ]] && [[ "$(${PARALLEL} --version --no-notice 2>/dev/null | head -n 1 | grep -o 'GNU parallel')" == "GNU parallel" ]]; then
# in parallel
export -f build_document
if ! "${PARALLEL}" --halt now,fail=1 --quote --no-notice --env process_markdown --env build_document --jobs="${JOBS:-1}" "${SELF_DIR}/${SELF_NAME}" build-document --markdown="${PARALLEL_MARKDOWN}" --marsh-uid="${MARSH_UID}" --marsh-temp="${MARSH_TEMP}" --marsh-config-file="${MARSH_CONFIG_FILE}" --marsh-config-file-dir="${MARSH_CONFIG_FILE_DIR}" --marsh-config-path="${MARSH_CONFIG_PATH}" --marsh-config-url="${MARSH_CONFIG_URL}" "${DEST_DIR}" "${TARGET_DIR}" {} {} ::: "${DOCUMENTS[@]}" ::: "${ADVANCED_NAV}"; then
echo "Unable to build template: ${TEMPLATES[$I]}"
return 1
fi
if [[ "${TEMPLATES_DIR}" != "" ]] && [[ "${#TEMPLATES[@]}" -gt 0 ]]; then
export -f build_template
for I in "${!TEMPLATES[@]}"; do
if ! "${PARALLEL}" --halt now,fail=1 --quote --no-notice --env process_markdown --env build_template --jobs="${JOBS:-1}" "${SELF_DIR}/${SELF_NAME}" build-templates --markdown="${PARALLEL_MARKDOWN}" --marsh-uid="${MARSH_UID}" --marsh-temp="${MARSH_TEMP}" --marsh-config-file="${MARSH_CONFIG_FILE}" --marsh-config-file-dir="${MARSH_CONFIG_FILE_DIR}" --marsh-config-path="${MARSH_CONFIG_PATH}" --marsh-config-url="${MARSH_CONFIG_URL}" "${DEST_DIR}" "${TARGET_DIR}" {} "${TEMPLATES_DIR}" "${TEMPLATES[$I]}" "${TEMPLATES_CONFIG_EXTRA[$I]:-}" ::: "${DOCUMENTS[@]}"; then
echo "Unable to build template: ${TEMPLATES[$I]}"
return 1
fi
done
fi
else
# serially
OIFS="${IFS}"
IFS=$'\n'
DOCUMENTS=($(echo "${DOCUMENTS[@]}"))
IFS="${OIFS}"
for DOCUMENT in "${DOCUMENTS[@]}"; do
if ! build_document "${DEST_DIR}" "${TARGET_DIR}" "${DOCUMENT}" "${ADVANCED_NAV}"; then
echo "Unable to build document: ${DOCUMENT}"
return 1
fi
if [[ "${TEMPLATES_DIR}" != "" ]] && [[ "${#TEMPLATES[@]}" -gt 0 ]]; then
for I in "${!TEMPLATES[@]}"; do
if ! build_template "${DEST_DIR}" "${TARGET_DIR}" "${DOCUMENT}" "${TEMPLATES_DIR}" "${TEMPLATES[$I]}" "${TEMPLATES_CONFIG_EXTRA[$I]:-}"; then
echo "Unable to build template: ${TEMPLATES[$I]}"
return 1
fi
done
fi
done
fi
}
# removes temporary files used during target builds
function clean_target { # clean_target target
local TARGET
TARGET="${1:-}"; [[ "${TARGET}" == "" ]] && return 1
find "${TARGET}" \( -iname '*.markdown' -or -iname '*.md' -or -iname '*.mkd' -or -iname '*.mkdn' -or -iname '*.mdown' -or -name '*.'"${MARSH_UID}"'.*' \) -delete
}
# commands
COMMANDS=('build' 'build-document' 'build-templates')
COMMAND="${1:-}"
# validate command
if [[ ! "${COMMAND}" =~ ^- ]]; then
if [[ "${COMMAND}" == "" ]]; then
# command not specified
echo "Command not specified" >&2
echo -e "${HELP}"
exit 1
elif ! in_array "${COMMAND}" "${COMMANDS[@]}"; then
# invalid command
echo "Invalid command: ${COMMAND}" >&2
echo -e "${HELP}"
exit 1
fi
shift 1
else
# continue processing arguments
COMMAND=""
fi
# args
FETCH=false
FORCE=false
JOBS=1
if [[ "${SYS_NAME}" == "darwin" ]]; then
JOBS=$(sysctl -n hw.ncpu)
elif [[ "${SYS_NAME}" == "linux" ]]; then
JOBS=$(nproc)
fi
MARKDOWN=""
OPTIND=1
OPTSPEC=":-:hvj:"
OPTARRAY=('-h' '--help' '-v' '--version' '--fetch' '--force' '-j' '--jobs' '--markdown' '--markdown-options') # all short and long options
OPTARRAY+=('marsh-uid' 'marsh-temp' 'marsh-config-file' 'marsh-config-file-dir' 'marsh-config-path' 'marsh-config-url') # all private options
while getopts "${OPTSPEC}" OPT; do
case "${OPT}" in
-)
case "${OPTARG}" in
help)
# Print help and exit
echo -e "${HELP}"
exit 0
;;
help=*)
# Print help and exit
echo -e "${HELP}"
exit 0
;;
version)
# Print version and exit
echo -e "${NAME} ${VERSION}"
exit 0
;;
version=*)
# Print version and exit
echo -e "${NAME} ${VERSION}"
exit 0
;;
jobs)
if [[ -z ${!OPTIND+isset} ]] || in_array "${!OPTIND}" "${OPTARRAY[@]}"; then
# Option without required argument
echo "Option --${OPTARG} requires a value" >&2
echo -e "${HELP}"
exit 1
fi
JOBS="${!OPTIND}"
if [[ ! "${JOBS}" =~ ^[1-9][0-9]*$ ]]; then
echo "Option --${OPTARG} requires a numeric value greater than zero" >&2
echo -e "${HELP}"
exit 1
fi
OPTIND=$((OPTIND + 1))
;;
jobs=*)
JOBS="${OPTARG#*=}"
if [[ ! "${JOBS}" =~ ^[1-9][0-9]*$ ]]; then
echo "Option --${OPTARG} requires a numeric value greater than zero" >&2
echo -e "${HELP}"
exit 1
fi
;;
markdown-options)
if [[ -z ${!OPTIND+isset} ]] || in_array "${!OPTIND}" "${OPTARRAY[@]}"; then
# Option without required argument
echo "Option --${OPTARG} requires a value" >&2
echo -e "${HELP}"
exit 1
fi
MARKDOWN_OPTIONS="${!OPTIND}"
OIFS="${IFS}"
IFS=$'\n'
MARKDOWN_OPTIONS=(${MARKDOWN_OPTIONS//,/$IFS})
IFS="${OIFS}"
OPTIND=$((OPTIND + 1))
;;
markdown-options=*)
MARKDOWN_OPTIONS="${OPTARG#*=}"
OIFS="${IFS}"
IFS=$'\n'
MARKDOWN_OPTIONS=(${MARKDOWN_OPTIONS//,/$IFS})
IFS="${OIFS}"
;;
markdown)
if [[ -z ${!OPTIND+isset} ]] || in_array "${!OPTIND}" "${OPTARRAY[@]}"; then
# Option without required argument
echo "Option --${OPTARG} requires a value" >&2
echo -e "${HELP}"
exit 1
fi
MARKDOWN="${!OPTIND}"
OPTIND=$((OPTIND + 1))
;;
markdown=*)
MARKDOWN="${OPTARG#*=}"
;;
fetch)
FETCH=true
;;
fetch=*)
# Option with prohibited value
echo "Option --${OPTARG%%=*} takes no value" >&2
echo -e "${HELP}"
exit 1
;;
force)
FORCE=true
;;
force=*)
# Option with prohibited value
echo "Option --${OPTARG%%=*} takes no value" >&2
echo -e "${HELP}"
exit 1
;;
marsh-uid)
if [[ -z ${!OPTIND+isset} ]] || in_array "${!OPTIND}" "${OPTARRAY[@]}"; then
# Option without required argument
echo "Option --${OPTARG} requires a value" >&2
echo -e "${HELP}"
exit 1
fi
MARSH_UID="${!OPTIND}"
OPTIND=$((OPTIND + 1))
;;
marsh-uid=*)
MARSH_UID="${OPTARG#*=}"
;;
marsh-temp)
if [[ -z ${!OPTIND+isset} ]] || in_array "${!OPTIND}" "${OPTARRAY[@]}"; then
# Option without required argument
echo "Option --${OPTARG} requires a value" >&2
echo -e "${HELP}"
exit 1
fi
MARSH_TEMP="${!OPTIND}"
OPTIND=$((OPTIND + 1))
;;
marsh-temp=*)
MARSH_TEMP="${OPTARG#*=}"
;;
marsh-config-file)
if [[ -z ${!OPTIND+isset} ]] || in_array "${!OPTIND}" "${OPTARRAY[@]}"; then
# Option without required argument
echo "Option --${OPTARG} requires a value" >&2
echo -e "${HELP}"
exit 1
fi
MARSH_CONFIG_FILE="${!OPTIND}"
OPTIND=$((OPTIND + 1))
;;
marsh-config-file=*)
MARSH_CONFIG_FILE="${OPTARG#*=}"
;;
marsh-config-file-dir)
if [[ -z ${!OPTIND+isset} ]] || in_array "${!OPTIND}" "${OPTARRAY[@]}"; then
# Option without required argument
echo "Option --${OPTARG} requires a value" >&2
echo -e "${HELP}"
exit 1
fi
MARSH_CONFIG_FILE_DIR="${!OPTIND}"
OPTIND=$((OPTIND + 1))
;;
marsh-config-file-dir=*)
MARSH_CONFIG_FILE_DIR="${OPTARG#*=}"
;;
marsh-config-path)
if [[ -z ${!OPTIND+isset} ]] || in_array "${!OPTIND}" "${OPTARRAY[@]}"; then
# Option without required argument
echo "Option --${OPTARG} requires a value" >&2
echo -e "${HELP}"
exit 1
fi
MARSH_CONFIG_PATH="${!OPTIND}"
OPTIND=$((OPTIND + 1))
;;
marsh-config-path=*)
MARSH_CONFIG_PATH="${OPTARG#*=}"
;;
marsh-config-url)
if [[ -z ${!OPTIND+isset} ]] || in_array "${!OPTIND}" "${OPTARRAY[@]}"; then
# Option without required argument
echo "Option --${OPTARG} requires a value" >&2
echo -e "${HELP}"
exit 1
fi
MARSH_CONFIG_URL="${!OPTIND}"
OPTIND=$((OPTIND + 1))
;;
marsh-config-url=*)
MARSH_CONFIG_URL="${OPTARG#*=}"
;;
*)
if [[ "${OPTERR}" == 1 ]]; then
# Invalid option specified
echo "Invalid option: --${OPTARG}" >&2
echo -e "${HELP}"
exit 1
fi
;;
esac
;;
h)
# Print help and exit
echo -e "${HELP}"
exit 0
;;
v)
# Print version and exit
echo "${NAME} ${VERSION}"
exit 0
;;
j)
# Number of jobs
JOBS="${OPTARG}"
if [[ ! "${JOBS}" =~ ^[1-9][0-9]*$ ]]; then
echo "Option -${OPT} requires a numeric value greater than zero" >&2
echo -e "${HELP}"
exit 1
fi
;;
:)
# Option without required value
echo "Option -${OPTARG} requires a value" >&2
echo -e "${HELP}"
exit 1
;;
\?)
# Invalid option specified
echo "Invalid option: -${OPTARG}" >&2
echo -e "${HELP}"
exit 1
;;
esac
done
shift $((OPTIND - 1))
# revalidate command
if [[ "${COMMAND}" == "" ]]; then
# command not specified
echo "Command not specified" >&2
echo -e "${HELP}"
exit 1
elif ! in_array "${COMMAND}" "${COMMANDS[@]}"; then
# invalid command
echo "Invalid command: ${COMMAND}" >&2
echo -e "${HELP}"
exit 1
fi
# parse config
if [[ "${MARSH_CONFIG_FILE:-}" == "" ]]; then
MARSH_CONFIG_FILE="${1:-$(pwd -P)}"
if [[ -d "${MARSH_CONFIG_FILE}" ]]; then
MARSH_CONFIG_FILE=$(cd "${MARSH_CONFIG_FILE}" && (pwd -P . 2>/dev/null || command pwd))
[[ "${MARSH_CONFIG_FILE}" != "" ]] && MARSH_CONFIG_FILE="${MARSH_CONFIG_FILE%/}/config.yaml"
fi
if [[ -f "${MARSH_CONFIG_FILE}" ]] && [[ -r "${MARSH_CONFIG_FILE}" ]]; then
MARSH_CONFIG=$(parse_yaml "${MARSH_CONFIG_FILE}" "CONFIG_")
[[ $? -ne 0 ]] && echo "Unable to parse yaml file: ${MARSH_CONFIG_FILE}" >&2 && return 1
eval "${MARSH_CONFIG}"
else
echo "Configuration file not found: ${MARSH_CONFIG_FILE#$SELF_DIR/}" >&2
exit 1
fi
if [[ "${CONFIG_Build_Path:-}" != "" ]]; then
MARSH_CONFIG_PATH="${CONFIG_Build_Path}"
[[ "${MARSH_CONFIG_FILE}" =~ / ]] && MARSH_CONFIG_FILE_DIR="${MARSH_CONFIG_FILE%/*}"
MARSH_CONFIG_FILE_DIR="${MARSH_CONFIG_FILE_DIR:-.}"
[[ ! "${MARSH_CONFIG_PATH}" =~ ^/ ]] && MARSH_CONFIG_PATH="${MARSH_CONFIG_FILE_DIR}/${MARSH_CONFIG_PATH}"
MARSH_CONFIG_PATH="${MARSH_CONFIG_PATH#./}"
else
echo "Build directory empty or not found in config: ${MARSH_CONFIG_FILE}" >&2
exit 1
fi
MARSH_CONFIG_URL="${CONFIG_Build_URL:-}"
#export MARSH_CONFIG_FILE MARSH_CONFIG_FILE_DIR MARSH_CONFIG_PATH MARSH_CONFIG_URL
fi
# dependencies
GIT="git"
MD5="md5sum"
[[ "$(uname | awk '{ print tolower($0) }')" == "darwin" ]] && MD5="md5"
if [[ "${MARKDOWN}" != "" ]] && [[ "${MARKDOWN#\/}" == "${MARKDOWN}" ]]; then
MARKDOWN="${SELF_DIR}/${MARKDOWN}"
fi
MARKDOWN="${MARKDOWN:-markdown}"
MARKDOWN_VERSION_REQ="2.2.2"
DEPS_MISSING=()
dependency_exists "${MD5}" || DEPS_MISSING+=("${MD5}")
if dependency_exists "${GIT}"; then
GIT_VERSION=$("${GIT}" --version | head -n 1 | sed -E -e 's/^git +version +([0-9]+(\.[0-9]+(\.[0-9]+)?)?).*$/\1/')
[[ $(version "${GIT_VERSION:-}") -ge $(version "2.3") ]] && export GIT_TERMINAL_PROMPT=0
else
DEPS_MISSING+=("${GIT}")
fi
if dependency_exists "${MARKDOWN}"; then
MARKDOWN_VERSION=$("${MARKDOWN%%\ *}" -V | head -n 1 | sed -E -e 's/^markdown: +discount +(\(.+\) *)?([0-9]+(\.[0-9]+(\.[0-9]+)?)?).*$/\2/')
[[ $(version "${MARKDOWN_VERSION:-}") -ge $(version "${MARKDOWN_VERSION_REQ}") ]] || DEPS_MISSING+=("${MARKDOWN}")
else
DEPS_MISSING+=("${MARKDOWN}")
fi
if [[ "${#DEPS_MISSING[@]}" -gt 0 ]]; then
in_array "${MD5}" "${DEPS_MISSING[@]}" && print_dependency "md5sum (md5 on Darwin/macOS)"
in_array "${GIT}" "${DEPS_MISSING[@]}" && print_dependency "Git" "https://git-scm.com/"
in_array "${MARKDOWN}" "${DEPS_MISSING[@]}" && print_dependency "Discount markdown" "http://www.pell.portland.or.us/~orc/Code/discount/" "${MARKDOWN_VERSION_REQ:-}" "${MARKDOWN_VERSION:-}"
exit 1
fi
# discount markdown options
if [[ -z ${MARKDOWN_OPTIONS+isset} ]]; then
MARKDOWN_OPTIONS=(${MARKDOWN_OPTIONS_DEFAULT[@]})
fi
for I in "${!MARKDOWN_OPTIONS[@]}"; do
MARKDOWN_OPTIONS[$I]="-f${MARKDOWN_OPTIONS[$I]}"
done
OIFS="${IFS}"
IFS=$'\n'
MARKDOWN_UNKNOWN=($("${MARKDOWN}" ${MARKDOWN_OPTIONS[@]} -VV 2>&1 >/dev/null | grep -i "unknown" | sed -E -e 's/^markdown: unknown option <([^>]+)>$/\1/'))
IFS="${OIFS}"
if [[ "${#MARKDOWN_UNKNOWN[@]}" -gt 0 ]]; then
echo -n "Unknown markdown options:" >&2
for UNKNOWN in "${MARKDOWN_UNKNOWN[@]}"; do
echo -n " '${UNKNOWN}'" >&2
done
echo ""
exit
fi
function process_markdown {
# re[lv]="footnote" is not valid html5; remove it
if [[ "${#MARKDOWN_OPTIONS[@]}" -gt 0 ]]; then
"${MARKDOWN}" ${MARKDOWN_OPTIONS[@]} ${@} | sed -E -e '/
/s/ re[lv]="footnote"//g'
else
"${MARKDOWN}" ${@} | sed -E -e '//s/ re[lv]="footnote"//g'
fi
}
#export MARKDOWN
export -f process_markdown
# create target directory
mkdir -p "${MARSH_CONFIG_PATH}"
# keep temporary directory creation close to trap to catch quick SIGINT
# temporary directory
if [[ "${MARSH_UID:-}" == "" ]]; then
MARSH_TEMP=$(mktemp -d "${TMPDIR:-/tmp/}marsh-XXXXXX")
MARSH_UID="marsh-${MARSH_TEMP##*marsh-}"
#export MARSH_TEMP MARSH_UID
fi
TEMPLATES_DIR="${MARSH_TEMP}/templates"
mkdir -p "${TEMPLATES_DIR}"
# kills child processes and removes temporary files
function die_gracefully { # die_gracefully
trap - EXIT INT
trap ":" INT # prevent recursion due to spamming ctrl-c
echo "Killing all processes and cleaning up (ETA ~3 seconds)..." >&2
#trap - INT && kill -2 -- -$$ # should not be needed with `parallel --halt now,fail=1 ...`
sleep 2
rm -rf "${MARSH_TEMP}" || "Temporary files may still remain at: ${MARSH_TEMP}" >&2
sleep 1
trap - TERM && kill -- -$$
}
# work
if [[ "${COMMAND}" == "build" ]]; then
# die gracefully
trap "die_gracefully" EXIT INT TERM
# build everything
echo -n "${NAME} ${VERSION}"
[[ "${JOBS:-1}" -gt 1 ]] && echo -n " (${JOBS} jobs)"
echo ""
echo "${CONFIG_Build_Name:-No Name} (${MARSH_CONFIG_FILE#$SELF_DIR/})"
echo "${CONFIG_Build_Path%/} -> ${CONFIG_Build_URL%/}"
# build
INDEX=0
PREFIX="CONFIG_Build_Targets_${INDEX}_"
while META=$(get_target_metadata "${PREFIX}" "TARGET_"); do
eval "${META}"
META_VARS=$(echo "${META}" | get_variables)
# get template
for I in "${!TARGET_TEMPLATES[@]}"; do
if [[ ! -e "${TEMPLATES_DIR}/${TARGET_TEMPLATES[$I]}" ]]; then
# fetch remote?
if [[ "${TARGET_TEMPLATES_FETCH[$I]}" == true ]] || [[ "${FETCH}" == true ]]; then
TARGET_TEMPLATES_FETCH[$I]=true
fi
if [[ "${TARGET_TEMPLATES_REMOTE[$I]}" == "" ]] && [[ ! "${TARGET_TEMPLATES_SOURCE[$I]}" =~ ^/ ]]; then
TARGET_TEMPLATES_SOURCE[$I]="${MARSH_CONFIG_FILE_DIR}/${TARGET_TEMPLATES_SOURCE[$I]}"
TARGET_TEMPLATES_SOURCE[$I]="${TARGET_TEMPLATES_SOURCE[$I]#./}"
fi
if [[ "${TARGET_TEMPLATES_CONFIG[$I]}" != "" ]] && [[ ! "${TARGET_TEMPLATES_CONFIG[$I]}" =~ ^/ ]]; then
TARGET_TEMPLATES_CONFIG[$I]="${MARSH_CONFIG_FILE_DIR}/${TARGET_TEMPLATES_CONFIG[$I]}"
TARGET_TEMPLATES_CONFIG[$I]="${TARGET_TEMPLATES_CONFIG[$I]#./}"
fi
get_files "${TARGET_TEMPLATES_SOURCE[$I]}/*" \
"${TEMPLATES_DIR}/${TARGET_TEMPLATES[$I]}" \
false \
"${TARGET_TEMPLATES_FETCH[$I]}" \
"${TARGET_TEMPLATES_REMOTE[$I]}" || die_gracefully
fi
done
# get source files for target
if [[ "${TARGET_FORCE}" == true ]] || [[ "${FORCE}" == true ]]; then
TARGET_FORCE=true
fi
if [[ "${TARGET_FETCH}" == true ]] || [[ "${FETCH}" == true ]]; then
TARGET_FETCH=true
fi
if [[ "${TARGET_REMOTE}" == "" ]] && [[ ! "${TARGET_SOURCE}" =~ ^/ ]]; then
TARGET_SOURCE="${MARSH_CONFIG_FILE_DIR}/${TARGET_SOURCE}"
TARGET_SOURCE="${TARGET_SOURCE#./}"
fi
if [[ "${TARGET_SOURCE}" != "" ]]; then
get_files "${TARGET_SOURCE}" \
"${MARSH_CONFIG_PATH}/${TARGET_PATH}" \
"${TARGET_FORCE}" \
"${TARGET_FETCH}" \
"${TARGET_REMOTE}" || die_gracefully
fi
# build advanced navigation
if [[ "${TARGET_NAV}" != "" ]]; then
build_advanced_nav "${MARSH_CONFIG_PATH}/${TARGET_PATH}" \
"${TARGET_NAV}" || die_gracefully
fi
# build target
build_target "${MARSH_CONFIG_PATH}" \
"${TARGET_PATH}" \
"${TARGET_NAV}" \
"${TEMPLATES_DIR}" \
"${#TARGET_TEMPLATES[@]}" \
"${TARGET_TEMPLATES[@]}" \
"${#TARGET_TEMPLATES_CONFIG[@]}" \
"${TARGET_TEMPLATES_CONFIG[@]}" || die_gracefully
# archives
for I in "${!TARGET_ARCHIVES[@]}"; do
if [[ "${TARGET_ARCHIVES[$I]}" != "" ]]; then
OIFS="${IFS}"
IFS=$(echo @|tr @ '\034')
TARGET_ARCHIVE_TYPES=(${TARGET_ARCHIVES_TYPES[$I]:-})
TARGET_ARCHIVE_EXCLUDED_STATES=(${TARGET_ARCHIVES_EXCLUDED_STATES[$I]:-})
IFS="${OIFS}"
if ! build_archive "${TARGET_ARCHIVES_ENCODING[$I]}" \
"${MARSH_CONFIG_PATH}/${TARGET_PATH}" \
"${TARGET_ARCHIVES[$I]}" \
"${TARGET_ARCHIVES_NAME[$I]:-$TARGET_NAME}" \
"${TARGET_ARCHIVES_CONTENT[$I]}" \
"${TARGET_ARCHIVES_COUNT[$I]}" \
"${#TARGET_ARCHIVE_TYPES[@]}" \
"${TARGET_ARCHIVE_TYPES[@]}" \
"${#TARGET_ARCHIVE_EXCLUDED_STATES[@]}" \
"${TARGET_ARCHIVE_EXCLUDED_STATES[@]}" \
|| ! build_target "${MARSH_CONFIG_PATH}" \
"${TARGET_PATH}/${TARGET_ARCHIVES[$I]}" \
"" \
"${TEMPLATES_DIR}" \
"${#TARGET_TEMPLATES[@]}" \
"${TARGET_TEMPLATES[@]}" \
"${#TARGET_TEMPLATES_CONFIG[@]}" \
"${TARGET_TEMPLATES_CONFIG[@]}"
then
die_gracefully
fi
unset TARGET_ARCHIVE_TYPES TARGET_ARCHIVE_EXCLUDED_STATES
fi
done
# clean build files
clean_target "${MARSH_CONFIG_PATH}/${TARGET_PATH}"
# unset temporary global variables
for VAR in ${META_VARS[@]}; do unset "${VAR}"; done
INDEX=$((INDEX+1))
PREFIX="CONFIG_Build_Targets_${INDEX}_"
done
# clean up
rm -rf "${MARSH_TEMP}"
trap - EXIT INT TERM
elif [[ "${COMMAND}" == "build-document" ]]; then
# build single document
build_document "${@}"
elif [[ "${COMMAND}" == "build-templates" ]]; then
# build templates for document
build_template "${@}"
fi
# done
exit 0