_html_escape() { local -r text="$1"; shift local escaped_text html_escape_to escaped_text "$text" printf '%s\n' "$escaped_text" } html_escape_to() { local -n output_ref="$1"; shift local text="$1"; shift text=${text//&/\&} text=${text///\>} text=${text//\"/\"} text=${text//\'/\'} output_ref="$text" } _css_string_escape() { local -r text="$1"; shift local escaped_text css_string_escape_to escaped_text "$text" printf '%s\n' "$escaped_text" } css_string_escape_to() { local -n output_ref="$1"; shift local text="$1"; shift text=${text//\\/\\\\} text=${text//&/\\000026} text=${text///\\00003e} text=${text//\"/\\000022} text=${text//\'/\\000027} output_ref="$text" } _json_string_escape() { local text="$1"; shift local char local escaped='' local escaped_char local -i code local -i i local LC_ALL=C for (( i = 0; i < ${#text}; i++ )); do char="${text:i:1}" case "$char" in $'\\') escaped+=$'\\\\' ;; '"') escaped+='\"' ;; $'\b') escaped+='\b' ;; $'\f') escaped+='\f' ;; $'\n') escaped+='\n' ;; $'\r') escaped+='\r' ;; $'\t') escaped+='\t' ;; *) code=$(printf '%d' "'$char") if (( code < 32 )); then printf -v escaped_char '\\u%04x' "$code" escaped+="$escaped_char" else escaped+="$char" fi ;; esac done printf '%s\n' "$escaped" } _json_string() { local -r text="$1"; shift printf '"%s"' "$(_json_string_escape "$text")" } _json_bool() { local -r value="$1"; shift case "$value" in yes) printf 'true' ;; *) printf 'false' ;; esac } _display_path() { local -r path="$1"; shift local -r final_dist="${SHURIKEN_FINAL_DIST_DIR:-}" if [[ -n "$final_dist" && "$path" == "$DIST_DIR"* ]]; then printf '%s%s\n' "$final_dist" "${path#"$DIST_DIR"}" else printf '%s\n' "$path" fi } current_date_text() { if random_seed_is_set; then printf 'Thu Jan 1 00:00:00 UTC 1970\n' else command date fi } current_date_text_to() { local -n output_ref="$1"; shift if [ -z "$SHURIKEN_CURRENT_DATE_TEXT" ]; then if random_seed_is_set; then SHURIKEN_CURRENT_DATE_TEXT='Thu Jan 1 00:00:00 UTC 1970' else SHURIKEN_CURRENT_DATE_TEXT=$(command date) fi fi output_ref="$SHURIKEN_CURRENT_DATE_TEXT" } declare -ra TEMPLATE_RENDER_FIELD_SPECS=( 'render_animation_class_html|context_html|animation_class|animation_class|preview details view' 'render_backhref_css|context_css|backhref|backhref|header splash' 'render_backhref_html|context_html|backhref|backhref|footer header preview splash details view stats camera' 'render_background_image_css|context_css|background_image|background_image|header splash' 'render_blurs_dir_css|context_css|blurs_dir|blurs_dir|header splash' 'render_camera_name_html|context_html|camera_name|camera_name|camera' 'render_cameraview_body_html|context_raw|cameraview_body|cameraview_body|cameraview' 'render_camera_thumbs_html|context_raw|camera_thumbs|camera_thumbs|camera' 'render_current_date_text|current_date_html|||' 'render_enter_page_html|context_html|enter_page|enter_page|splash' 'render_exif_details_html|context_raw|exif_details|exif_details|details' 'render_exif_tooltip_html|context_html|exif_tooltip|exif_tooltip|details view' 'render_height_html|config_html|HEIGHT||' 'render_html_dir_html|context_html|html_dir|html_dir|*' 'render_maxpreviews_html|config_html|MAXPREVIEWS||' 'render_next_html|context_html|next|next|next' 'render_original_basepath_is_set|original_basepath_is_set|||' 'render_original_basepath_html|config_html|ORIGINAL_BASEPATH||' 'render_page_num_html|context_html|page_num|page_num|preview details view' 'render_photo_html|context_html|photo|photo|preview splash details view' 'render_photos_dir_html|context_html|photos_dir|photos_dir|splash details view' 'render_prev_html|context_html|prev|prev|prev' 'render_preview_num_html|context_html|preview_num|preview_num|preview details view' 'render_redirect_page_html|context_html|redirect_page|redirect_page|redirect' 'render_show_header_bar|context_raw|show_header_bar|show_header_bar|header' 'render_stats_body_html|context_raw|stats_body|stats_body|stats' 'render_stats_page_html|config_html|STATS_PAGE||' 'render_tarball_include|tarball_include|||' 'render_tarball_name_html|context_html|tarball_name|tarball_name|footer' 'render_thumbheight_html|config_html|THUMBHEIGHT||' 'render_thumbs_dir_html|context_html|thumbs_dir|thumbs_dir|preview' 'render_title_html|config_html|TITLE||' 'render_view_next_html|preview_num_next_html|preview_num||' 'render_view_prev_html|preview_num_prev_html|preview_num||' ) current_timestamp_slug() { if random_seed_is_set; then printf '1970-01-01-000000\n' else command date +'%Y-%m-%d-%H%M%S' fi } current_timestamp_iso() { if random_seed_is_set; then printf '1970-01-01T00:00:00Z\n' else command date -u +'%Y-%m-%dT%H:%M:%SZ' fi } template_context_value() { # shellcheck disable=SC2178 local -n context_ref="$1"; shift local -r name="$1"; shift printf '%s\n' "${context_ref[$name]:-}" } template_context_value_to() { local -n output_ref="$1"; shift # shellcheck disable=SC2178 local -n context_ref="$1"; shift local -r name="$1"; shift # shellcheck disable=SC2034 output_ref="${context_ref[$name]:-}" } require_template_context_vars() { local -r template_name="$1"; shift # shellcheck disable=SC2178 local -n context_ref="$1"; shift local name for name in "$@"; do if [ -z "${context_ref[$name]+x}" ]; then config_error "template $template_name requires render variable $name" return 1 fi done } template_render_field_is_required_for() { local -r template_name="$1"; shift local -r required_template_names="$1"; shift case "$required_template_names" in '*') return 0 ;; '') return 1 ;; *) [[ " $required_template_names " == *" $template_name "* ]] ;; esac } template_required_context_vars_to() { # shellcheck disable=SC2178 local -n required_vars_ref="$1"; shift local -r template_name="$1"; shift local -A required_var_seen=() local field_spec local _kind local _render_var local required_context_var local required_template_names local _source_name required_vars_ref=() for field_spec in "${TEMPLATE_RENDER_FIELD_SPECS[@]}"; do IFS='|' read -r _render_var _kind _source_name \ required_context_var required_template_names <<< "$field_spec" if [ -n "$required_context_var" ] \ && template_render_field_is_required_for \ "$template_name" "$required_template_names" \ && [ -z "${required_var_seen[$required_context_var]+x}" ]; then required_vars_ref+=("$required_context_var") required_var_seen["$required_context_var"]=yes fi done } template_required_context_vars() { local -r template_name="$1"; shift local -a required_vars=() template_required_context_vars_to required_vars "$template_name" if (( ${#required_vars[@]} > 0 )); then printf '%s\n' "${required_vars[@]}" fi } validate_template_context() { local -r template_name="$1"; shift local -r context_name="$1"; shift local -i status=0 local -a required_vars=() template_required_context_vars_to required_vars "$template_name" require_template_context_vars "$template_name" "$context_name" \ "${required_vars[@]}" status=$? if (( status != 0 )); then return "$status" fi } source_template_file() { local -r template_path="$1"; shift local -r output_path="$1"; shift local -r render_vars_name="$1"; shift local context_file local sig local -i status=0 context_file=$(mktemp) status=$? if (( status != 0 )); then return "$status" fi # Single cleanup point for the BASH_ENV context tempfile. The traps MUST be # registered here in source_template_file's own body (not in a helper): a # RETURN trap is not function-scoped unless functrace is enabled, so a trap # set inside a helper would fire when that helper returns and delete the file # before the render even runs. # # The RETURN trap covers normal and error returns (errexit unwinds through # it) and clears ALL of these traps, including itself, so a lingering RETURN # trap cannot fire again on an enclosing function's return against the now # out-of-scope context_file local (which would trip set -u). # # A RETURN trap alone does NOT fire when a signal terminates the shell with # its default disposition. source_template_file also runs in backgrounded # render subshells (see queue_album_view_render_job) that get SIGTERM'd by # terminate_active_generation on interrupt, so we additionally trap the # terminating signals: each handler removes the file, clears the traps and # re-raises the original signal so the process still exits with that signal's # default disposition. # # We deliberately do NOT trap PIPE: the internal "{ ... } | bash" pipeline # below legitimately produces SIGPIPE when the reader closes early, and # trapping it would tear the render down mid-flight. SIGKILL cannot be # trapped either, so a KILL escalation may still leak a file that the OS tmp # reaper later clears; that is the only unavoidable residual case. trap 'rm -f "$context_file"; trap - INT TERM HUP RETURN' RETURN for sig in INT TERM HUP; do # $sig is intentionally expanded now (so each handler re-raises its own # signal); $context_file and $BASHPID are escaped to expand when the trap # runs. We re-raise to $BASHPID, not $$: source_template_file commonly # runs in backgrounded render subshells where $$ is the main shuriken # PID, so kill -s $sig $$ would terminate the main shell (mid-cleanup, # after its own staging traps were cleared) instead of this subshell. # $BASHPID is the current (sub)shell's real PID and equals $$ in the # foreground case, so it is correct everywhere. # shellcheck disable=SC2064 trap "rm -f \"\$context_file\"; trap - INT TERM HUP RETURN; \ kill -s $sig \"\$BASHPID\"" "$sig" done if { declare -p TEMPLATE_RENDER_FIELD_SPECS declare -p "$render_vars_name" declare -f printf 'serialize_template_render_context %q\n' "$render_vars_name" printf "printf 'unset BASH_ENV\\\\n'\n" } | bash -euo pipefail > "$context_file"; then status=0 else status=$? fi if (( status != 0 )); then return "$status" fi if env -i PATH="$PATH" BASH_ENV="$context_file" \ bash -euo pipefail -- "$template_path" >> "$output_path"; then status=0 else status=$? fi if (( status != 0 )); then return "$status" fi } parse_template_context() { local -r template_name="$1"; shift local -n context_ref="$1"; shift local context_key local context_value while (( $# > 0 )); do if (( $# < 2 )); then config_error "template $template_name render context is incomplete" return 1 fi context_key="$1" context_value="$2" shift 2 if [[ ! "$context_key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then config_error \ "template $template_name render variable $context_key is invalid" return 1 fi # shellcheck disable=SC2034 context_ref["$context_key"]="$context_value" done } serialize_template_render_var() { local -r name="$1"; shift local -r value="$1"; shift printf '%s=%q\n' "$name" "$value" } serialize_template_render_context() { local -r render_vars_name="$1"; shift # shellcheck disable=SC2178 local -n render_vars_ref="$render_vars_name" local field_spec local _kind local render_var local _required_context_var local _required_templates local _source_name for field_spec in "${TEMPLATE_RENDER_FIELD_SPECS[@]}"; do IFS='|' read -r render_var _kind _source_name \ _required_context_var _required_templates <<< "$field_spec" serialize_template_render_var \ "$render_var" "${render_vars_ref[$render_var]}" done } prepare_template_render_vars() { local -r render_vars_name="$1"; shift local -r context_name="$1"; shift # shellcheck disable=SC2178 local -n render_vars_ref="$render_vars_name" local context_value local field_spec local kind local render_value local render_var local _required_context_var local _required_templates local source_name render_vars_ref=() for field_spec in "${TEMPLATE_RENDER_FIELD_SPECS[@]}"; do IFS='|' read -r render_var kind source_name \ _required_context_var _required_templates <<< "$field_spec" case "$kind" in context_css) template_context_value_to \ context_value "$context_name" "$source_name" css_string_escape_to render_value "$context_value" ;; context_html) template_context_value_to \ context_value "$context_name" "$source_name" html_escape_to render_value "$context_value" ;; context_raw) template_context_value_to \ render_value "$context_name" "$source_name" ;; current_date_html) current_date_text_to context_value html_escape_to render_value "$context_value" ;; config_html) case "$source_name" in HEIGHT) context_value="$HEIGHT" ;; MAXPREVIEWS) # Refresh-only configs do not require generation # sizing fields; render absent values as empty. context_value="${MAXPREVIEWS:-}" ;; ORIGINAL_BASEPATH) context_value="$ORIGINAL_BASEPATH" ;; STATS_PAGE) # Always defaulted by apply_config_defaults; degrade to # "no" (link hidden) if somehow unset so the header bar # never references a stats page that was not generated. context_value="${STATS_PAGE:-no}" ;; THUMBHEIGHT) # Refresh-only configs do not require generation # sizing fields; render absent values as empty. context_value="${THUMBHEIGHT:-}" ;; TITLE) context_value="$TITLE" ;; *) config_error \ "unknown template render config $source_name" return 1 ;; esac html_escape_to render_value "$context_value" ;; original_basepath_is_set) if [ -n "$ORIGINAL_BASEPATH" ]; then render_value='yes' else render_value='no' fi ;; preview_num_next_html) template_context_value_to \ context_value "$context_name" "$source_name" if [ -n "$context_value" ]; then html_escape_to render_value "$(( context_value + 1 ))" else render_value='' fi ;; preview_num_prev_html) template_context_value_to \ context_value "$context_name" "$source_name" if [ -n "$context_value" ]; then html_escape_to render_value "$(( context_value - 1 ))" else render_value='' fi ;; tarball_include) render_value="$TARBALL_INCLUDE" ;; *) config_error "unknown template render field kind $kind" return 1 ;; esac render_vars_ref["$render_var"]="$render_value" done } validate_template_render_request() { local -r template_name="$1"; shift local -r context_name="$1"; shift local -r template_path="$TEMPLATE_DIR/$template_name.tmpl" local -i status=0 if [ ! -r "$template_path" ]; then config_error "template file $template_path must be readable" return 1 fi validate_template_context "$template_name" "$context_name" status=$? if (( status != 0 )); then return "$status" fi } render_template() { local -r template_name="$1"; shift local -r html="$1"; shift local -r context_name="$1"; shift local -r template_path="$TEMPLATE_DIR/$template_name.tmpl" local dist_html local html_dir local -i status=0 # Passed by name to prepare_template_render_vars and source_template_file. # shellcheck disable=SC2034 local -A render_vars=() html_dir=$(template_context_value "$context_name" html_dir) dist_html="$DIST_DIR/$html_dir" log_info \ "Rendering $template_name template into $(_display_path "$dist_html")/$html" mkdir -p "$dist_html" status=$? if (( status != 0 )); then return "$status" fi prepare_template_render_vars render_vars "$context_name" status=$? if (( status != 0 )); then return "$status" fi source_template_file "$template_path" "$dist_html/$html" render_vars status=$? if (( status != 0 )); then return "$status" fi } template() { local -r template_name="$1"; shift local -r html="$1"; shift local -i status=0 # shellcheck disable=SC2034 local -A render_context=() parse_template_context "$template_name" render_context "$@" status=$? if (( status != 0 )); then return "$status" fi validate_template_render_request "$template_name" render_context status=$? if (( status != 0 )); then return "$status" fi render_template "$template_name" "$html" render_context status=$? if (( status != 0 )); then return "$status" fi }