diff options
| -rwxr-xr-x | bin/shuriken | 183 | ||||
| -rw-r--r-- | src/lib/album-metadata.source.sh | 4 | ||||
| -rw-r--r-- | src/lib/album-thumbnail-html.source.sh | 10 | ||||
| -rw-r--r-- | src/lib/generation-metadata.source.sh | 44 | ||||
| -rw-r--r-- | src/lib/stats-filter-album.source.sh | 4 | ||||
| -rw-r--r-- | src/lib/stats-render.source.sh | 6 | ||||
| -rw-r--r-- | src/lib/template.source.sh | 115 | ||||
| -rwxr-xr-x | tests/cli.sh | 56 |
8 files changed, 302 insertions, 120 deletions
diff --git a/bin/shuriken b/bin/shuriken index 280d8b6..2f898f3 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -537,13 +537,28 @@ resolve_tar_opts() { } # Inlined from src/lib/template.source.sh -_html_escape() { - local -r text="$1"; shift - local escaped_text - - html_escape_to escaped_text "$text" - printf '%s\n' "$escaped_text" -} +# --- Escape helper family: one consistent API shape ------------------------- +# +# Every escaper in this file follows the SAME two-form convention: +# +# <name>_to <out_var> <text> nameref form, the hot path. Writes the escaped +# result into the named variable with no +# subshell/command substitution. +# <name> <text> thin printf convenience wrapper. Writes the +# escaped result to stdout (for $(...) callers). +# It just delegates to <name>_to and prints. +# +# Naming convention for the "_" prefix in THIS file: +# - A leading "_" marks a genuinely PRIVATE/internal helper (only called from +# within the same module). +# - Helpers that are part of the public escape API -- called from other +# modules -- carry NO leading "_". html_escape / css_string_escape / +# json_string / json_bool are all called from sibling lib modules, so they +# are public and unprefixed. (They used to be "_"-prefixed, which wrongly +# signalled "private" for what is effectively a public API.) +# +# Only the escaping/encoding logic is authoritative; the printf wrappers add no +# behavior of their own beyond appending a trailing newline for stdout use. html_escape_to() { local -n output_ref="$1"; shift @@ -558,11 +573,12 @@ html_escape_to() { output_ref="$text" } -_css_string_escape() { +# Thin printf wrapper around html_escape_to for $(...) callers. +html_escape() { local -r text="$1"; shift local escaped_text - css_string_escape_to escaped_text "$text" + html_escape_to escaped_text "$text" printf '%s\n' "$escaped_text" } @@ -580,7 +596,17 @@ css_string_escape_to() { output_ref="$text" } -_json_string_escape() { +# Thin printf wrapper around css_string_escape_to for $(...) callers. +css_string_escape() { + local -r text="$1"; shift + local escaped_text + + css_string_escape_to escaped_text "$text" + printf '%s\n' "$escaped_text" +} + +json_string_escape_to() { + local -n output_ref="$1"; shift local text="$1"; shift local char local escaped='' @@ -625,28 +651,63 @@ _json_string_escape() { esac done - printf '%s\n' "$escaped" + output_ref="$escaped" } -_json_string() { +# Thin printf wrapper around json_string_escape_to for $(...) callers. +json_string_escape() { local -r text="$1"; shift + local escaped_text - printf '"%s"' "$(_json_string_escape "$text")" + json_string_escape_to escaped_text "$text" + printf '%s\n' "$escaped_text" } -_json_bool() { +# Nameref form: write the JSON-quoted ("...") string into out_var. +json_string_to() { + local -n string_output_ref="$1"; shift + local -r text="$1"; shift + local escaped_text + + json_string_escape_to escaped_text "$text" + # shellcheck disable=SC2034 # written through the output nameref + string_output_ref="\"$escaped_text\"" +} + +# Thin printf wrapper around json_string_to for $(...) callers. +json_string() { + local -r text="$1"; shift + local quoted_text + + json_string_to quoted_text "$text" + printf '%s' "$quoted_text" +} + +# Nameref form: write the JSON boolean literal into out_var. +json_bool_to() { + local -n bool_output_ref="$1"; shift local -r value="$1"; shift + # shellcheck disable=SC2034 # written through the output nameref case "$value" in yes) - printf 'true' + bool_output_ref='true' ;; *) - printf 'false' + bool_output_ref='false' ;; esac } +# Thin printf wrapper around json_bool_to for $(...) callers. +json_bool() { + local -r value="$1"; shift + local bool_literal + + json_bool_to bool_literal "$value" + printf '%s' "$bool_literal" +} + _display_path() { local -r path="$1"; shift local -r final_dist="${SHURIKEN_FINAL_DIST_DIR:-}" @@ -658,14 +719,11 @@ _display_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 -} - +# Nameref form: write the current date text into out_var, caching it in +# SHURIKEN_CURRENT_DATE_TEXT so repeated calls within a run do not re-exec date +# (the value is constant for a generation run). The wrapper below delegates here +# so BOTH forms share this cache -- a hot-path caller using the printf form no +# longer silently re-runs `date` every call. current_date_text_to() { local -n output_ref="$1"; shift @@ -680,6 +738,15 @@ current_date_text_to() { output_ref="$SHURIKEN_CURRENT_DATE_TEXT" } +# Thin printf wrapper around current_date_text_to for stdout/$(...) callers. +# Delegates so it shares the SHURIKEN_CURRENT_DATE_TEXT cache (output unchanged). +current_date_text() { + local date_text + + current_date_text_to date_text + printf '%s\n' "$date_text" +} + # Each entry: render_var|kind|source_name|context_var|required_templates # # render_var name of the render_* variable handed to the .tmpl @@ -2184,8 +2251,8 @@ photo_exif_details_html() { continue fi value="${exif_values[$key]}" - key_html=$(_html_escape "exif:$key") - value_html=$(_html_escape "$value") + key_html=$(html_escape "exif:$key") + value_html=$(html_escape "$value") if (( exif_count == 0 )); then printf '<table class="details">\n' @@ -2309,7 +2376,7 @@ photo_exif_tooltip_text() { # presentation that stays in the album module. The collector depends on the file # counters (count_files / count_incoming_images / count_tree_files, now in # image.source.sh), current_timestamp_iso (template.source.sh) and the JSON -# helpers (_json_string / _json_bool, template.source.sh); all are sourced before +# helpers (json_string / json_bool, template.source.sh); all are sourced before # this module, and these are runtime calls anyway, so source order documents the # dependency without affecting availability. Behaviour and signatures are # unchanged by the move. @@ -2351,23 +2418,23 @@ _generation_metadata_json() { printf '{\n' printf ' "generator": {\n' printf ' "name": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["generator_name"]}")" + "$(json_string "${_GENERATION_METADATA["generator_name"]}")" printf ' "version": %s\n' \ - "$(_json_string "${_GENERATION_METADATA["generator_version"]}")" + "$(json_string "${_GENERATION_METADATA["generator_version"]}")" printf ' },\n' printf ' "generated_at": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["generated_at"]}")" + "$(json_string "${_GENERATION_METADATA["generated_at"]}")" printf ' "config_source": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["config_source"]}")" + "$(json_string "${_GENERATION_METADATA["config_source"]}")" printf ' "template": {\n' printf ' "name": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["template_name"]}")" + "$(json_string "${_GENERATION_METADATA["template_name"]}")" printf ' "directory": %s\n' \ - "$(_json_string "${_GENERATION_METADATA["template_directory"]}")" + "$(json_string "${_GENERATION_METADATA["template_directory"]}")" printf ' },\n' printf ' "source": {\n' printf ' "incoming_dir": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["source_incoming_dir"]}")" + "$(json_string "${_GENERATION_METADATA["source_incoming_dir"]}")" printf ' "image_count": %s\n' \ "${_GENERATION_METADATA["source_image_count"]}" printf ' },\n' @@ -2381,35 +2448,35 @@ _generation_metadata_json() { printf ' },\n' printf ' "tarball": {\n' printf ' "included": %s,\n' \ - "$(_json_bool "${_GENERATION_METADATA["tarball_included"]}")" + "$(json_bool "${_GENERATION_METADATA["tarball_included"]}")" printf ' "file": %s\n' \ - "$(_json_string "${_GENERATION_METADATA["tarball_file"]}")" + "$(json_string "${_GENERATION_METADATA["tarball_file"]}")" printf ' },\n' printf ' "settings": {\n' printf ' "title": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_title"]}")" + "$(json_string "${_GENERATION_METADATA["settings_title"]}")" printf ' "height": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_height"]}")" + "$(json_string "${_GENERATION_METADATA["settings_height"]}")" printf ' "thumbheight": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_thumbheight"]}")" + "$(json_string "${_GENERATION_METADATA["settings_thumbheight"]}")" printf ' "maxpreviews": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_maxpreviews"]}")" + "$(json_string "${_GENERATION_METADATA["settings_maxpreviews"]}")" printf ' "subdivide_percent": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_subdivide_percent"]}")" + "$(json_string "${_GENERATION_METADATA["settings_subdivide_percent"]}")" printf ' "feature_percent": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_feature_percent"]}")" + "$(json_string "${_GENERATION_METADATA["settings_feature_percent"]}")" printf ' "image_jobs": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_image_jobs"]}")" + "$(json_string "${_GENERATION_METADATA["settings_image_jobs"]}")" printf ' "random_seed": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_random_seed"]}")" + "$(json_string "${_GENERATION_METADATA["settings_random_seed"]}")" printf ' "shuffle": %s,\n' \ - "$(_json_bool "${_GENERATION_METADATA["settings_shuffle"]}")" + "$(json_bool "${_GENERATION_METADATA["settings_shuffle"]}")" printf ' "splash_page": %s,\n' \ - "$(_json_bool "${_GENERATION_METADATA["settings_splash_page"]}")" + "$(json_bool "${_GENERATION_METADATA["settings_splash_page"]}")" printf ' "stats_page": %s,\n' \ - "$(_json_bool "${_GENERATION_METADATA["settings_stats_page"]}")" + "$(json_bool "${_GENERATION_METADATA["settings_stats_page"]}")" printf ' "original_basepath": %s\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_original_basepath"]}")" + "$(json_string "${_GENERATION_METADATA["settings_original_basepath"]}")" printf ' }\n' printf '}\n' } @@ -2799,12 +2866,12 @@ build_preview_thumbnail() { local thumbs_dir_html local anchor_class_html - photo_html=$(_html_escape "$photo_file") - anim_html=$(_html_escape "$animation_class") - backhref_html=$(_html_escape "$backhref") - thumbs_dir_html=$(_html_escape "$thumbs_dir") + photo_html=$(html_escape "$photo_file") + anim_html=$(html_escape "$animation_class") + backhref_html=$(html_escape "$backhref") + thumbs_dir_html=$(html_escape "$thumbs_dir") if [ -n "$anchor_class" ]; then - anchor_class_html=$(_html_escape "$anchor_class") + anchor_class_html=$(html_escape "$anchor_class") printf '<a id=%s class=%s href=%s>\n' \ "'$photo_html'" "'$anchor_class_html'" \ "'${href_prefix}${preview_num}.html'" @@ -4386,7 +4453,7 @@ _stats_section_open() { if [ -n "$list_class" ]; then ul_class+=" $list_class" fi - heading_html=$(_html_escape "$heading") + heading_html=$(html_escape "$heading") printf '<section class="stats-section">\n' printf '<h2>%s</h2>\n' "$heading_html" printf '<ul class="%s">\n' "$ul_class" @@ -4458,7 +4525,7 @@ _stats_render_ordered_section() { if [ -z "${counts_ref[$bucket]:-}" ]; then continue fi - row_html=$(_stats_filter_link "$prefix" "$bucket" "$(_html_escape "$bucket")") + row_html=$(_stats_filter_link "$prefix" "$bucket" "$(html_escape "$bucket")") _stats_bar_row "$row_html" "${counts_ref[$bucket]}" "$total" "$max" done _stats_section_close @@ -4485,7 +4552,7 @@ _stats_render_ranked_section() { max=$(_stats_max_count "$array_name") _stats_section_open "$heading" "$list_class" while IFS= read -r key; do - row_html=$(_stats_filter_link "$prefix" "$key" "$(_html_escape "$key")") + row_html=$(_stats_filter_link "$prefix" "$key" "$(html_escape "$key")") _stats_bar_row "$row_html" "${counts_ref[$key]}" "$total" "$max" done < <(_stats_keys_by_count_desc "$array_name") _stats_section_close @@ -4824,11 +4891,11 @@ _stats_build_filterview_body() { local view_page local details_link='' - photo_html=$(_html_escape "$photo") + photo_html=$(html_escape "$photo") animation_class=$(random_animation_css_class fast "$photo") tooltip=$(photo_exif_tooltip_text "$photo" "$INCOMING_DIR/$photo") if [ -n "$tooltip" ]; then - tooltip_attr=" title=\"$(_html_escape "$tooltip")\"" + tooltip_attr=" title=\"$(html_escape "$tooltip")\"" fi view_page=$(album_view_page_for_photo "$photo") if [ -n "$view_page" ]; then diff --git a/src/lib/album-metadata.source.sh b/src/lib/album-metadata.source.sh index b96c817..b1bed7a 100644 --- a/src/lib/album-metadata.source.sh +++ b/src/lib/album-metadata.source.sh @@ -37,8 +37,8 @@ photo_exif_details_html() { continue fi value="${exif_values[$key]}" - key_html=$(_html_escape "exif:$key") - value_html=$(_html_escape "$value") + key_html=$(html_escape "exif:$key") + value_html=$(html_escape "$value") if (( exif_count == 0 )); then printf '<table class="details">\n' diff --git a/src/lib/album-thumbnail-html.source.sh b/src/lib/album-thumbnail-html.source.sh index 110a0fa..8f34695 100644 --- a/src/lib/album-thumbnail-html.source.sh +++ b/src/lib/album-thumbnail-html.source.sh @@ -87,12 +87,12 @@ build_preview_thumbnail() { local thumbs_dir_html local anchor_class_html - photo_html=$(_html_escape "$photo_file") - anim_html=$(_html_escape "$animation_class") - backhref_html=$(_html_escape "$backhref") - thumbs_dir_html=$(_html_escape "$thumbs_dir") + photo_html=$(html_escape "$photo_file") + anim_html=$(html_escape "$animation_class") + backhref_html=$(html_escape "$backhref") + thumbs_dir_html=$(html_escape "$thumbs_dir") if [ -n "$anchor_class" ]; then - anchor_class_html=$(_html_escape "$anchor_class") + anchor_class_html=$(html_escape "$anchor_class") printf '<a id=%s class=%s href=%s>\n' \ "'$photo_html'" "'$anchor_class_html'" \ "'${href_prefix}${preview_num}.html'" diff --git a/src/lib/generation-metadata.source.sh b/src/lib/generation-metadata.source.sh index b75af1b..931bd92 100644 --- a/src/lib/generation-metadata.source.sh +++ b/src/lib/generation-metadata.source.sh @@ -5,7 +5,7 @@ # presentation that stays in the album module. The collector depends on the file # counters (count_files / count_incoming_images / count_tree_files, now in # image.source.sh), current_timestamp_iso (template.source.sh) and the JSON -# helpers (_json_string / _json_bool, template.source.sh); all are sourced before +# helpers (json_string / json_bool, template.source.sh); all are sourced before # this module, and these are runtime calls anyway, so source order documents the # dependency without affecting availability. Behaviour and signatures are # unchanged by the move. @@ -47,23 +47,23 @@ _generation_metadata_json() { printf '{\n' printf ' "generator": {\n' printf ' "name": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["generator_name"]}")" + "$(json_string "${_GENERATION_METADATA["generator_name"]}")" printf ' "version": %s\n' \ - "$(_json_string "${_GENERATION_METADATA["generator_version"]}")" + "$(json_string "${_GENERATION_METADATA["generator_version"]}")" printf ' },\n' printf ' "generated_at": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["generated_at"]}")" + "$(json_string "${_GENERATION_METADATA["generated_at"]}")" printf ' "config_source": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["config_source"]}")" + "$(json_string "${_GENERATION_METADATA["config_source"]}")" printf ' "template": {\n' printf ' "name": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["template_name"]}")" + "$(json_string "${_GENERATION_METADATA["template_name"]}")" printf ' "directory": %s\n' \ - "$(_json_string "${_GENERATION_METADATA["template_directory"]}")" + "$(json_string "${_GENERATION_METADATA["template_directory"]}")" printf ' },\n' printf ' "source": {\n' printf ' "incoming_dir": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["source_incoming_dir"]}")" + "$(json_string "${_GENERATION_METADATA["source_incoming_dir"]}")" printf ' "image_count": %s\n' \ "${_GENERATION_METADATA["source_image_count"]}" printf ' },\n' @@ -77,35 +77,35 @@ _generation_metadata_json() { printf ' },\n' printf ' "tarball": {\n' printf ' "included": %s,\n' \ - "$(_json_bool "${_GENERATION_METADATA["tarball_included"]}")" + "$(json_bool "${_GENERATION_METADATA["tarball_included"]}")" printf ' "file": %s\n' \ - "$(_json_string "${_GENERATION_METADATA["tarball_file"]}")" + "$(json_string "${_GENERATION_METADATA["tarball_file"]}")" printf ' },\n' printf ' "settings": {\n' printf ' "title": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_title"]}")" + "$(json_string "${_GENERATION_METADATA["settings_title"]}")" printf ' "height": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_height"]}")" + "$(json_string "${_GENERATION_METADATA["settings_height"]}")" printf ' "thumbheight": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_thumbheight"]}")" + "$(json_string "${_GENERATION_METADATA["settings_thumbheight"]}")" printf ' "maxpreviews": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_maxpreviews"]}")" + "$(json_string "${_GENERATION_METADATA["settings_maxpreviews"]}")" printf ' "subdivide_percent": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_subdivide_percent"]}")" + "$(json_string "${_GENERATION_METADATA["settings_subdivide_percent"]}")" printf ' "feature_percent": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_feature_percent"]}")" + "$(json_string "${_GENERATION_METADATA["settings_feature_percent"]}")" printf ' "image_jobs": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_image_jobs"]}")" + "$(json_string "${_GENERATION_METADATA["settings_image_jobs"]}")" printf ' "random_seed": %s,\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_random_seed"]}")" + "$(json_string "${_GENERATION_METADATA["settings_random_seed"]}")" printf ' "shuffle": %s,\n' \ - "$(_json_bool "${_GENERATION_METADATA["settings_shuffle"]}")" + "$(json_bool "${_GENERATION_METADATA["settings_shuffle"]}")" printf ' "splash_page": %s,\n' \ - "$(_json_bool "${_GENERATION_METADATA["settings_splash_page"]}")" + "$(json_bool "${_GENERATION_METADATA["settings_splash_page"]}")" printf ' "stats_page": %s,\n' \ - "$(_json_bool "${_GENERATION_METADATA["settings_stats_page"]}")" + "$(json_bool "${_GENERATION_METADATA["settings_stats_page"]}")" printf ' "original_basepath": %s\n' \ - "$(_json_string "${_GENERATION_METADATA["settings_original_basepath"]}")" + "$(json_string "${_GENERATION_METADATA["settings_original_basepath"]}")" printf ' }\n' printf '}\n' } diff --git a/src/lib/stats-filter-album.source.sh b/src/lib/stats-filter-album.source.sh index c475006..aac6834 100644 --- a/src/lib/stats-filter-album.source.sh +++ b/src/lib/stats-filter-album.source.sh @@ -146,11 +146,11 @@ _stats_build_filterview_body() { local view_page local details_link='' - photo_html=$(_html_escape "$photo") + photo_html=$(html_escape "$photo") animation_class=$(random_animation_css_class fast "$photo") tooltip=$(photo_exif_tooltip_text "$photo" "$INCOMING_DIR/$photo") if [ -n "$tooltip" ]; then - tooltip_attr=" title=\"$(_html_escape "$tooltip")\"" + tooltip_attr=" title=\"$(html_escape "$tooltip")\"" fi view_page=$(album_view_page_for_photo "$photo") if [ -n "$view_page" ]; then diff --git a/src/lib/stats-render.source.sh b/src/lib/stats-render.source.sh index 5431d51..b9bc067 100644 --- a/src/lib/stats-render.source.sh +++ b/src/lib/stats-render.source.sh @@ -84,7 +84,7 @@ _stats_section_open() { if [ -n "$list_class" ]; then ul_class+=" $list_class" fi - heading_html=$(_html_escape "$heading") + heading_html=$(html_escape "$heading") printf '<section class="stats-section">\n' printf '<h2>%s</h2>\n' "$heading_html" printf '<ul class="%s">\n' "$ul_class" @@ -156,7 +156,7 @@ _stats_render_ordered_section() { if [ -z "${counts_ref[$bucket]:-}" ]; then continue fi - row_html=$(_stats_filter_link "$prefix" "$bucket" "$(_html_escape "$bucket")") + row_html=$(_stats_filter_link "$prefix" "$bucket" "$(html_escape "$bucket")") _stats_bar_row "$row_html" "${counts_ref[$bucket]}" "$total" "$max" done _stats_section_close @@ -183,7 +183,7 @@ _stats_render_ranked_section() { max=$(_stats_max_count "$array_name") _stats_section_open "$heading" "$list_class" while IFS= read -r key; do - row_html=$(_stats_filter_link "$prefix" "$key" "$(_html_escape "$key")") + row_html=$(_stats_filter_link "$prefix" "$key" "$(html_escape "$key")") _stats_bar_row "$row_html" "${counts_ref[$key]}" "$total" "$max" done < <(_stats_keys_by_count_desc "$array_name") _stats_section_close diff --git a/src/lib/template.source.sh b/src/lib/template.source.sh index 398e17d..016d2a2 100644 --- a/src/lib/template.source.sh +++ b/src/lib/template.source.sh @@ -1,10 +1,25 @@ -_html_escape() { - local -r text="$1"; shift - local escaped_text - - html_escape_to escaped_text "$text" - printf '%s\n' "$escaped_text" -} +# --- Escape helper family: one consistent API shape ------------------------- +# +# Every escaper in this file follows the SAME two-form convention: +# +# <name>_to <out_var> <text> nameref form, the hot path. Writes the escaped +# result into the named variable with no +# subshell/command substitution. +# <name> <text> thin printf convenience wrapper. Writes the +# escaped result to stdout (for $(...) callers). +# It just delegates to <name>_to and prints. +# +# Naming convention for the "_" prefix in THIS file: +# - A leading "_" marks a genuinely PRIVATE/internal helper (only called from +# within the same module). +# - Helpers that are part of the public escape API -- called from other +# modules -- carry NO leading "_". html_escape / css_string_escape / +# json_string / json_bool are all called from sibling lib modules, so they +# are public and unprefixed. (They used to be "_"-prefixed, which wrongly +# signalled "private" for what is effectively a public API.) +# +# Only the escaping/encoding logic is authoritative; the printf wrappers add no +# behavior of their own beyond appending a trailing newline for stdout use. html_escape_to() { local -n output_ref="$1"; shift @@ -19,11 +34,12 @@ html_escape_to() { output_ref="$text" } -_css_string_escape() { +# Thin printf wrapper around html_escape_to for $(...) callers. +html_escape() { local -r text="$1"; shift local escaped_text - css_string_escape_to escaped_text "$text" + html_escape_to escaped_text "$text" printf '%s\n' "$escaped_text" } @@ -41,7 +57,17 @@ css_string_escape_to() { output_ref="$text" } -_json_string_escape() { +# Thin printf wrapper around css_string_escape_to for $(...) callers. +css_string_escape() { + local -r text="$1"; shift + local escaped_text + + css_string_escape_to escaped_text "$text" + printf '%s\n' "$escaped_text" +} + +json_string_escape_to() { + local -n output_ref="$1"; shift local text="$1"; shift local char local escaped='' @@ -86,28 +112,63 @@ _json_string_escape() { esac done - printf '%s\n' "$escaped" + output_ref="$escaped" } -_json_string() { +# Thin printf wrapper around json_string_escape_to for $(...) callers. +json_string_escape() { local -r text="$1"; shift + local escaped_text - printf '"%s"' "$(_json_string_escape "$text")" + json_string_escape_to escaped_text "$text" + printf '%s\n' "$escaped_text" } -_json_bool() { +# Nameref form: write the JSON-quoted ("...") string into out_var. +json_string_to() { + local -n string_output_ref="$1"; shift + local -r text="$1"; shift + local escaped_text + + json_string_escape_to escaped_text "$text" + # shellcheck disable=SC2034 # written through the output nameref + string_output_ref="\"$escaped_text\"" +} + +# Thin printf wrapper around json_string_to for $(...) callers. +json_string() { + local -r text="$1"; shift + local quoted_text + + json_string_to quoted_text "$text" + printf '%s' "$quoted_text" +} + +# Nameref form: write the JSON boolean literal into out_var. +json_bool_to() { + local -n bool_output_ref="$1"; shift local -r value="$1"; shift + # shellcheck disable=SC2034 # written through the output nameref case "$value" in yes) - printf 'true' + bool_output_ref='true' ;; *) - printf 'false' + bool_output_ref='false' ;; esac } +# Thin printf wrapper around json_bool_to for $(...) callers. +json_bool() { + local -r value="$1"; shift + local bool_literal + + json_bool_to bool_literal "$value" + printf '%s' "$bool_literal" +} + _display_path() { local -r path="$1"; shift local -r final_dist="${SHURIKEN_FINAL_DIST_DIR:-}" @@ -119,14 +180,11 @@ _display_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 -} - +# Nameref form: write the current date text into out_var, caching it in +# SHURIKEN_CURRENT_DATE_TEXT so repeated calls within a run do not re-exec date +# (the value is constant for a generation run). The wrapper below delegates here +# so BOTH forms share this cache -- a hot-path caller using the printf form no +# longer silently re-runs `date` every call. current_date_text_to() { local -n output_ref="$1"; shift @@ -141,6 +199,15 @@ current_date_text_to() { output_ref="$SHURIKEN_CURRENT_DATE_TEXT" } +# Thin printf wrapper around current_date_text_to for stdout/$(...) callers. +# Delegates so it shares the SHURIKEN_CURRENT_DATE_TEXT cache (output unchanged). +current_date_text() { + local date_text + + current_date_text_to date_text + printf '%s\n' "$date_text" +} + # Each entry: render_var|kind|source_name|context_var|required_templates # # render_var name of the render_* variable handed to the .tmpl diff --git a/tests/cli.sh b/tests/cli.sh index 5d69684..2fff1a7 100755 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -5415,6 +5415,8 @@ test_template_stdout_escape_helpers_match_nameref_helpers() { local html_text local html_to local html_stdout_file + local json_text + local json_to # shellcheck source=src/lib/template.source.sh source "$TEST_REPO_ROOT/src/lib/template.source.sh" @@ -5427,30 +5429,73 @@ test_template_stdout_escape_helpers_match_nameref_helpers() { html_text=$'A & "quoted" <title> \'ok\'' html_escape_to html_to "$html_text" - _html_escape "$html_text" > "$html_stdout_file" + html_escape "$html_text" > "$html_stdout_file" printf '%s\n' "$html_to" > "$html_expected" cmp -s "$html_expected" "$html_stdout_file" test "$(<"$html_stdout_file")" = \ 'A & "quoted" <title> 'ok'' html_escape_to html_to '' - _html_escape '' > "$html_stdout_file" + html_escape '' > "$html_stdout_file" printf '%s\n' "$html_to" > "$html_expected" cmp -s "$html_expected" "$html_stdout_file" css_text=$'path\\kid\'s_"<tag>&.jpg' css_string_escape_to css_to "$css_text" - _css_string_escape "$css_text" > "$css_stdout_file" + css_string_escape "$css_text" > "$css_stdout_file" printf '%s\n' "$css_to" > "$css_expected" cmp -s "$css_expected" "$css_stdout_file" test "$(<"$css_stdout_file")" = \ 'path\\kid\000027s_\000022\00003ctag\00003e\000026.jpg' css_string_escape_to css_to '' - _css_string_escape '' > "$css_stdout_file" + css_string_escape '' > "$css_stdout_file" printf '%s\n' "$css_to" > "$css_expected" cmp -s "$css_expected" "$css_stdout_file" + # JSON family: the printf wrappers must agree with their _to nameref forms. + json_text=$'tab\tand "quote" \\back\nnewline' + json_string_escape_to json_to "$json_text" + test "$json_to" = "$(json_string_escape "$json_text")" + json_string_to json_to "$json_text" + test "$json_to" = "$(json_string "$json_text")" + test "$(json_string "$json_text")" = \ + '"tab\tand \"quote\" \\back\nnewline"' + json_bool_to json_to yes + test "$json_to" = 'true' + test "$(json_bool yes)" = 'true' + test "$(json_bool no)" = 'false' + + test::teardown +} + +test_template_current_date_text_caches_and_matches_nameref() { + local date_to + + # shellcheck source=src/lib/random.source.sh + source "$TEST_REPO_ROOT/src/lib/random.source.sh" + # shellcheck source=src/lib/template.source.sh + source "$TEST_REPO_ROOT/src/lib/template.source.sh" + + test::setup + + # Deterministic path so the value is fixed and we can assert exact output. + RANDOM_SEED='1' + SHURIKEN_CURRENT_DATE_TEXT='' + + # A direct (non-subshell) call through the printf wrapper primes the shared + # cache var, since it now delegates to current_date_text_to. (A $(...) call + # would prime the cache only inside its own command-substitution subshell -- + # that is inherent to command substitution, not specific to this helper.) + date_to=$(current_date_text) + test "$date_to" = 'Thu Jan 1 00:00:00 UTC 1970' + current_date_text > /dev/null + test "$SHURIKEN_CURRENT_DATE_TEXT" = 'Thu Jan 1 00:00:00 UTC 1970' + + # Both forms read the same cache, so they agree. + current_date_text_to date_to + test "$date_to" = "$(current_date_text)" + test::teardown } @@ -6825,6 +6870,9 @@ main() { 'template stdout escapers match nameref helpers' \ test_template_stdout_escape_helpers_match_nameref_helpers test::run_case \ + 'current_date_text caches and matches nameref form' \ + test_template_current_date_text_caches_and_matches_nameref + test::run_case \ '--generate escapes generated HTML values' \ test_generate_escapes_html_values test::run_case \ |
