summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-24 21:46:31 +0300
committerPaul Buetow <paul@buetow.org>2026-06-24 21:46:31 +0300
commitf0154c2df57aae016f720122c8208fc4a1082495 (patch)
treed14856b669eba6714fe1f35a65b1223371c87bdd
parenta79e7d77cc6717f6c5bfaaefa5361f396c322de3 (diff)
Unify escape/date helper API; fix current_date_text caching
Make the template.source.sh escape/date helper family follow one consistent shape: each escaper now has a nameref <name>_to form (hot path, writes a named variable) plus a thin printf wrapper <name> that delegates to it. The leading "_" now exclusively marks private helpers; the public escape API (called from sibling modules) is unprefixed. - current_date_text now delegates to current_date_text_to so both forms share the SHURIKEN_CURRENT_DATE_TEXT cache; the printf form no longer silently re-execs `date` on every direct call. Output unchanged. - Drop the misleading "_" prefix on the public escape API and update all callers: _html_escape->html_escape, _css_string_escape-> css_string_escape, _json_string->json_string, _json_bool->json_bool, _json_string_escape->json_string_escape. - Add the missing JSON nameref forms: json_string_escape_to, json_string_to, json_bool_to (printf wrappers delegate to them). - Add tests: JSON printf-vs-nameref parity and a current_date_text caching + nameref-parity check. No escaping/encoding or date output changes -- API-shape/perf only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rwxr-xr-xbin/shuriken183
-rw-r--r--src/lib/album-metadata.source.sh4
-rw-r--r--src/lib/album-thumbnail-html.source.sh10
-rw-r--r--src/lib/generation-metadata.source.sh44
-rw-r--r--src/lib/stats-filter-album.source.sh4
-rw-r--r--src/lib/stats-render.source.sh6
-rw-r--r--src/lib/template.source.sh115
-rwxr-xr-xtests/cli.sh56
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 &amp; &quot;quoted&quot; &lt;title&gt; &#39;ok&#39;'
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 \