jekyll-theme-zer0 1.19.1 → 1.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +395 -0
  3. data/README.md +27 -19
  4. data/_data/authors.yml +154 -5
  5. data/_data/backlog.yml +5 -5
  6. data/_data/content_statistics.yml +273 -297
  7. data/_data/features.yml +4 -25
  8. data/_data/navigation/README.md +24 -0
  9. data/_data/navigation/about.yml +2 -0
  10. data/_data/navigation/main.yml +2 -7
  11. data/_data/roadmap.yml +86 -12
  12. data/_includes/components/author-avatar-url.html +28 -0
  13. data/_includes/components/author-bio.html +86 -0
  14. data/_includes/components/author-card.html +184 -121
  15. data/_includes/components/author-eeat.html +10 -4
  16. data/_includes/components/info-section.html +1 -1
  17. data/_includes/components/mermaid.html +0 -3
  18. data/_includes/components/post-card.html +19 -9
  19. data/_includes/content/giscus.html +3 -2
  20. data/_includes/core/footer-fabs.html +28 -0
  21. data/_includes/core/footer.html +7 -17
  22. data/_includes/core/head.html +2 -2
  23. data/_includes/navigation/breadcrumbs.html +20 -2
  24. data/_includes/navigation/local-graph.html +18 -2
  25. data/_includes/obsidian/full-graph.html +4 -6
  26. data/_layouts/article.html +44 -74
  27. data/_layouts/author.html +274 -0
  28. data/_layouts/authors.html +55 -0
  29. data/_layouts/news.html +3 -3
  30. data/_layouts/note.html +21 -6
  31. data/_layouts/notebook.html +21 -6
  32. data/_layouts/root.html +31 -17
  33. data/_layouts/section.html +3 -3
  34. data/_plugins/author_pages_generator.rb +121 -0
  35. data/_sass/components/_author.scss +219 -0
  36. data/_sass/components/_content-tables.scss +16 -1
  37. data/_sass/components/_notes-index.scss +102 -0
  38. data/_sass/components/_search-modal.scss +40 -0
  39. data/_sass/components/_ui-enhancements.scss +570 -0
  40. data/_sass/core/_docs-code-examples.scss +463 -0
  41. data/_sass/core/_docs-layout.scss +0 -453
  42. data/_sass/core/_navbar.scss +253 -0
  43. data/_sass/core/_sidebar-extras.scss +79 -0
  44. data/_sass/core/_toc.scss +87 -0
  45. data/_sass/core/_variables.scss +7 -142
  46. data/_sass/custom.scss +24 -1122
  47. data/_sass/layouts/_global-chrome.scss +59 -0
  48. data/assets/css/main.scss +19 -2
  49. data/assets/js/author-profile.js +190 -0
  50. data/assets/js/modules/navigation/navbar.js +104 -0
  51. data/assets/js/obsidian-graph.js +2 -2
  52. data/assets/js/obsidian-local-graph.js +11 -5
  53. data/assets/vendor/cytoscape/cytoscape.min.js +32 -0
  54. data/scripts/README.md +39 -0
  55. data/scripts/bin/validate +11 -1
  56. data/scripts/dev/css-diff.sh +49 -0
  57. data/scripts/dev/shot.js +37 -0
  58. data/scripts/features/generate-preview-images +110 -6
  59. data/scripts/features/pixelate-preview-images +126 -0
  60. data/scripts/features/pixelate_images.py +662 -0
  61. data/scripts/github-setup.sh +0 -0
  62. data/scripts/lib/preview_generator.py +47 -3
  63. data/scripts/pixelate-preview-images.sh +12 -0
  64. data/scripts/test/integration/auto-version +10 -8
  65. data/scripts/test/lib/run_tests.sh +2 -0
  66. data/scripts/test/lib/test_content_review.sh +205 -0
  67. data/scripts/test/lib/test_pixelate_images.sh +108 -0
  68. metadata +25 -20
  69. data/_data/hub.yml +0 -68
  70. data/_data/hub_index.yml +0 -203
  71. data/_data/navigation/hub.yml +0 -110
  72. data/assets/vendor/font-awesome/css/all.min.css +0 -9
  73. data/assets/vendor/font-awesome/webfonts/fa-brands-400.ttf +0 -0
  74. data/assets/vendor/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
  75. data/assets/vendor/font-awesome/webfonts/fa-regular-400.ttf +0 -0
  76. data/assets/vendor/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
  77. data/assets/vendor/font-awesome/webfonts/fa-solid-900.ttf +0 -0
  78. data/assets/vendor/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
  79. data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.ttf +0 -0
  80. data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 +0 -0
  81. data/assets/vendor/jquery/jquery-3.7.1.min.js +0 -2
  82. data/scripts/lib/hub.rb +0 -208
  83. data/scripts/provision-org-sites.rb +0 -252
  84. data/scripts/provision-org-sites.sh +0 -23
  85. data/scripts/sync-hub-metadata.rb +0 -184
  86. data/scripts/sync-hub-metadata.sh +0 -22
data/scripts/README.md CHANGED
@@ -21,6 +21,8 @@ scripts/
21
21
  │ └── preview_generator.py # Python preview image generator
22
22
  ├── features/ # Feature-specific scripts
23
23
  │ ├── generate-preview-images # AI preview image generator
24
+ │ ├── pixelate-preview-images # Shrink preview banners (pixelate + PNG-8)
25
+ │ ├── pixelate_images.py # Pure-stdlib pixelate/quantize engine
24
26
  │ ├── install-preview-generator # Preview generator installer
25
27
  │ └── validate_preview_urls.py # Preview URL validator
26
28
  ├── utils/ # Utility scripts
@@ -123,6 +125,43 @@ AI Providers:
123
125
  xai - xAI Grok image generation (requires XAI_API_KEY)
124
126
  ```
125
127
 
128
+ #### `pixelate-preview-images`
129
+ Pixelate + palette-quantize the preview banners so they are dramatically smaller
130
+ files while retaining the retro pixel-art look. Pure Python stdlib (no
131
+ ImageMagick / Pillow / pngquant needed) — it downsamples the image and reduces
132
+ it to an indexed PNG-8 palette. Typical savings on the AI-generated banners are
133
+ ~90% (e.g. a 2.7 MB banner becomes ~230 KB).
134
+
135
+ ```bash
136
+ # Preview the savings for every banner (no changes), 4 workers:
137
+ ./scripts/features/pixelate-preview-images --dry-run -j 4
138
+
139
+ # Optimize all banners in place:
140
+ ./scripts/features/pixelate-preview-images -j 4
141
+
142
+ # Chunkier pixel-art look for one image:
143
+ ./scripts/features/pixelate-preview-images --block 6 --colors 64 \
144
+ assets/images/previews/about.png
145
+
146
+ Options (forwarded to scripts/features/pixelate_images.py):
147
+ -n, --dry-run Report savings without writing
148
+ --colors N Palette size 2-256 (default: 256)
149
+ --bits N Colour precision 1-8 while quantizing (default: 6)
150
+ --max-width N Downscale so width <= N, keep aspect (default: 1024)
151
+ --scale F Scale factor, e.g. 0.5
152
+ --block N Average NxN source pixels per output pixel (chunky)
153
+ --filter nearest|box Downsample filter (default: nearest)
154
+ --upscale Keep original WxH (chunky pixels) instead of reduced
155
+ --backup Keep <file>.orig when writing in place
156
+ --force Write even if the result is not smaller
157
+ -j, --jobs N Parallel worker processes (default: 1)
158
+ ```
159
+
160
+ With no path argument it processes `preview_images.output_dir` from
161
+ `_config.yml` (default `assets/images/previews`). Non-PNG and 16-bit/interlaced
162
+ inputs are skipped gracefully. The engine has a built-in `--selftest`, exercised
163
+ by `scripts/test/lib/test_pixelate_images.sh`.
164
+
126
165
  #### `install-preview-generator`
127
166
  Install the preview image generator feature.
128
167
 
data/scripts/bin/validate CHANGED
@@ -35,7 +35,7 @@ DESCRIPTION:
35
35
  DEFAULT CHECKS:
36
36
  - Required repository files
37
37
  - Gemspec parse
38
- - Version consistency between version.rb, package.json, and gemspec
38
+ - Version consistency between version.rb, package.json, gemspec, and Gemfile.lock
39
39
  - YAML parse for root config, _data, and .github/config files
40
40
  - Active configuration contract and config-file classification
41
41
  - Navigation data shape
@@ -241,6 +241,16 @@ if File.file?('package.json')
241
241
  versions['package.json'] = JSON.parse(File.read('package.json', encoding: 'UTF-8')).fetch('version').to_s
242
242
  end
243
243
 
244
+ # Gemfile.lock pins the gem's own version in its PATH specs block. It drifts
245
+ # from version.rb when a release path bumps the version without re-locking
246
+ # (the exact failure that left the lock at 1.19.1 while version.rb said 1.19.0).
247
+ if File.file?('Gemfile.lock')
248
+ lock = File.read('Gemfile.lock')
249
+ if (match = lock.match(/^\s*jekyll-theme-zer0 \(([^)]+)\)\s*$/))
250
+ versions['Gemfile.lock'] = match[1].to_s
251
+ end
252
+ end
253
+
244
254
  expected = versions.values.first
245
255
  mismatches = versions.select { |_file, version| version != expected }
246
256
 
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # css-diff.sh — compiled-CSS regression guard for the SCSS refactor
4
+ # ----------------------------------------------------------------------------
5
+ # Compares two compiled main.css files (Jekyll `style: expanded` output):
6
+ # 1. POSITIONAL diff — empty means byte-identical (ignoring blank-line/WS
7
+ # churn). Expected for true zero-diff relocations.
8
+ # 2. SORTED-CONTENT diff — empty means the exact same set of CSS lines exists
9
+ # in both files, only their order differs. This is the invariant we want
10
+ # for "pure relocation" phases: no declaration value changed, a block just
11
+ # moved position in the cascade.
12
+ #
13
+ # Usage: scripts/dev/css-diff.sh OLD.css NEW.css
14
+ # Exit: 0 if sorted-content identical (safe), 1 otherwise.
15
+ # ============================================================================
16
+ set -uo pipefail
17
+
18
+ OLD="${1:?usage: css-diff.sh OLD.css NEW.css}"
19
+ NEW="${2:?usage: css-diff.sh OLD.css NEW.css}"
20
+
21
+ # Normalize: drop blank lines, trim leading/trailing whitespace per line.
22
+ norm() { sed -e 's/[[:space:]]*$//' -e 's/^[[:space:]]*//' "$1" | grep -v '^$'; }
23
+
24
+ echo "=== POSITIONAL diff (empty = byte-identical modulo whitespace) ==="
25
+ if diff <(norm "$OLD") <(norm "$NEW"); then
26
+ echo "(positional: identical)"
27
+ POS=0
28
+ else
29
+ POS=1
30
+ fi
31
+
32
+ echo ""
33
+ echo "=== SORTED-CONTENT diff (empty = same rule content, order-only change) ==="
34
+ if diff <(norm "$OLD" | sort) <(norm "$NEW" | sort); then
35
+ echo "(sorted-content: identical — no value changed)"
36
+ SORTED=0
37
+ else
38
+ SORTED=1
39
+ fi
40
+
41
+ echo ""
42
+ if [ "$POS" -eq 0 ]; then
43
+ echo "RESULT: zero-diff (identical output)."
44
+ elif [ "$SORTED" -eq 0 ]; then
45
+ echo "RESULT: relocation-only (content identical, order changed). Review positional diff for cascade safety."
46
+ else
47
+ echo "RESULT: CONTENT CHANGED. Review the sorted-content diff above — declarations were added/removed/edited."
48
+ fi
49
+ exit "$SORTED"
@@ -0,0 +1,37 @@
1
+ // Screenshot helper for the SCSS refactor — captures key pages at several
2
+ // viewports against the running dev server. Usage:
3
+ // node scripts/dev/shot.js <label> [path] [skin]
4
+ // Outputs PNGs to /tmp/shots/<label>-<vw>.png. Reused across phases to compare
5
+ // before/after for cascade-sensitive or pixel-changing changes.
6
+ const { chromium } = require('@playwright/test');
7
+ const fs = require('fs');
8
+
9
+ const label = process.argv[2] || 'shot';
10
+ const path = process.argv[3] || '/';
11
+ const skin = process.argv[4] || null;
12
+ const BASE = process.env.BASE_URL || 'http://localhost:4000';
13
+ const VWS = [
14
+ { name: 'lg', width: 1280, height: 800 },
15
+ { name: 'md', width: 820, height: 1100 },
16
+ { name: 'sm', width: 390, height: 850 },
17
+ ];
18
+
19
+ (async () => {
20
+ fs.mkdirSync('/tmp/shots', { recursive: true });
21
+ const browser = await chromium.launch();
22
+ for (const vw of VWS) {
23
+ const ctx = await browser.newContext({ viewport: { width: vw.width, height: vw.height } });
24
+ const page = await ctx.newPage();
25
+ if (skin) {
26
+ await page.addInitScript((s) => {
27
+ try { localStorage.setItem('zer0-skin', s); } catch (e) {}
28
+ }, skin);
29
+ }
30
+ await page.goto(BASE + path, { waitUntil: 'networkidle' });
31
+ const out = `/tmp/shots/${label}-${vw.name}.png`;
32
+ await page.screenshot({ path: out, fullPage: false });
33
+ console.log('wrote', out);
34
+ await ctx.close();
35
+ }
36
+ await browser.close();
37
+ })();
@@ -91,9 +91,15 @@ fi
91
91
  # =============================================================================
92
92
  # Configuration Loading
93
93
  # =============================================================================
94
- # Priority: CLI args > Environment variables > _config.yml > Defaults
94
+ # Priority (per file): author preview overrides (_data/authors.yml) >
95
+ # CLI args > Environment variables > _config.yml > Defaults
96
+ # A post's `author:` may point at an entry in _data/authors.yml that carries a
97
+ # `preview:` block (style / style_modifiers / size / quality / model). Those win
98
+ # over the site-wide preview_images config for that post's banner, giving each
99
+ # AI author persona a distinct, recognisable art style.
95
100
 
96
101
  CONFIG_FILE="$PROJECT_ROOT/_config.yml"
102
+ AUTHORS_FILE="$PROJECT_ROOT/_data/authors.yml"
97
103
 
98
104
  # Function to read config value from _config.yml using grep (handles YAML anchors)
99
105
  read_config() {
@@ -283,6 +289,13 @@ CONFIGURATION:
283
289
  Default settings are loaded from _config.yml under 'preview_images' section.
284
290
  Environment variables override config file settings.
285
291
 
292
+ PER-AUTHOR ART STYLE:
293
+ A post's `author:` may reference an entry in _data/authors.yml that defines a
294
+ `preview:` block (style, style_modifiers, size, quality, model). When present,
295
+ those values override the site-wide preview_images settings for that post's
296
+ banner only — giving each AI author persona (e.g. cassandra, vega) a distinct
297
+ look. Other posts keep the default style.
298
+
286
299
  EXAMPLES:
287
300
  # List all files missing preview images
288
301
  ./scripts/generate-preview-images.sh --list-missing
@@ -482,6 +495,73 @@ except:
482
495
  echo "$result"
483
496
  }
484
497
 
498
+ # Read a per-author preview override from _data/authors.yml
499
+ # Usage: get_author_preview_value <author_key> <setting>
500
+ # e.g. get_author_preview_value cassandra style
501
+ # Returns the value (or empty string when the author / setting is absent).
502
+ get_author_preview_value() {
503
+ local author_key="$1"
504
+ local setting="$2"
505
+
506
+ [[ -z "$author_key" || ! -f "$AUTHORS_FILE" ]] && return 0
507
+
508
+ local result=""
509
+ if [[ "${YAML_PARSER:-python}" == "yq" ]]; then
510
+ # Capture the whole value (do NOT `head -1`) so a multi-line literal/folded
511
+ # scalar survives intact, matching the python3 branch below; trim a single
512
+ # trailing newline that yq appends.
513
+ result=$(yq eval ".\"${author_key}\".preview.${setting} // \"\"" "$AUTHORS_FILE" 2>/dev/null)
514
+ result="${result%$'\n'}"
515
+ [[ "$result" == "null" ]] && result=""
516
+ else
517
+ result=$(AUTHOR_KEY="$author_key" SETTING="$setting" AUTHORS_FILE="$AUTHORS_FILE" python3 -c '
518
+ import os, yaml
519
+ try:
520
+ data = yaml.safe_load(open(os.environ["AUTHORS_FILE"])) or {}
521
+ author = data.get(os.environ["AUTHOR_KEY"]) or {}
522
+ preview = author.get("preview") or {}
523
+ val = preview.get(os.environ["SETTING"])
524
+ if val is not None:
525
+ print(str(val).strip())
526
+ except Exception:
527
+ pass
528
+ ' 2>/dev/null)
529
+ fi
530
+
531
+ echo "$result"
532
+ }
533
+
534
+ # Apply a post author's per-author preview overrides (from _data/authors.yml) on
535
+ # top of the current image settings. MUST be called from a function that has
536
+ # already declared `local IMAGE_STYLE` (+ IMAGE_STYLE_MODIFIERS / IMAGE_SIZE /
537
+ # IMAGE_QUALITY / IMAGE_MODEL) — bash dynamic scoping lets this helper reassign
538
+ # those caller locals, scoping the override to the current file. Call it only at
539
+ # the point an image/prompt is actually built, so --list-missing stays clean and
540
+ # already-satisfied files don't pay for the lookup.
541
+ apply_author_preview_overrides() {
542
+ local author="$1"
543
+ [[ -z "$author" ]] && return 0
544
+
545
+ local ov_style ov_mods ov_size ov_quality ov_model
546
+ ov_style=$(get_author_preview_value "$author" "style")
547
+ ov_mods=$(get_author_preview_value "$author" "style_modifiers")
548
+ ov_size=$(get_author_preview_value "$author" "size")
549
+ ov_quality=$(get_author_preview_value "$author" "quality")
550
+ ov_model=$(get_author_preview_value "$author" "model")
551
+
552
+ [[ -n "$ov_style" ]] && IMAGE_STYLE="$ov_style"
553
+ [[ -n "$ov_mods" ]] && IMAGE_STYLE_MODIFIERS="$ov_mods"
554
+ [[ -n "$ov_size" ]] && IMAGE_SIZE="$ov_size"
555
+ [[ -n "$ov_quality" ]] && IMAGE_QUALITY="$ov_quality"
556
+ [[ -n "$ov_model" ]] && IMAGE_MODEL="$ov_model"
557
+
558
+ if [[ -n "$ov_style" || -n "$ov_mods" || -n "$ov_size" || -n "$ov_quality" || -n "$ov_model" ]]; then
559
+ info " ↳ Author '$author' preview overrides applied (style from _data/authors.yml)"
560
+ debug "Author override style: ${IMAGE_STYLE:0:120}..."
561
+ fi
562
+ return 0
563
+ }
564
+
485
565
  # Extract post content (without front matter)
486
566
  extract_content() {
487
567
  local file="$1"
@@ -983,15 +1063,33 @@ process_file() {
983
1063
  fi
984
1064
 
985
1065
  # Get metadata
986
- local title description categories preview
1066
+ local title description categories preview author
987
1067
  title=$(get_yaml_value "$front_matter" "title")
988
1068
  description=$(get_yaml_value "$front_matter" "description")
989
1069
  categories=$(get_yaml_value "$front_matter" "categories")
990
1070
  preview=$(get_yaml_value "$front_matter" "preview")
991
-
1071
+ author=$(get_yaml_value "$front_matter" "author")
1072
+
992
1073
  debug "Title: $title"
993
1074
  debug "Preview: $preview"
994
-
1075
+
1076
+ # -------------------------------------------------------------------------
1077
+ # Per-author art-style override (e.g. AI personas Cassandra / Vega).
1078
+ # These `local`s shadow the global image settings for the duration of this
1079
+ # function and everything it calls (generate_prompt, generate_image,
1080
+ # build_enhance_prompt) via bash dynamic scoping — so each worker/file gets
1081
+ # its author's style without leaking to other files, in serial or parallel.
1082
+ # The actual lookup runs later, via apply_author_preview_overrides, only
1083
+ # once an image/prompt is really built — never in --list-missing mode and
1084
+ # not for files whose preview already exists (keeps listing output clean and
1085
+ # skips wasted YAML-parser subprocesses).
1086
+ # -------------------------------------------------------------------------
1087
+ local IMAGE_STYLE="$IMAGE_STYLE"
1088
+ local IMAGE_STYLE_MODIFIERS="$IMAGE_STYLE_MODIFIERS"
1089
+ local IMAGE_SIZE="$IMAGE_SIZE"
1090
+ local IMAGE_QUALITY="$IMAGE_QUALITY"
1091
+ local IMAGE_MODEL="$IMAGE_MODEL"
1092
+
995
1093
  # =========================================================================
996
1094
  # ENHANCE MODE: improve an existing preview image
997
1095
  # =========================================================================
@@ -1010,7 +1108,10 @@ process_file() {
1010
1108
 
1011
1109
  info "Enhancing preview for: $title"
1012
1110
  debug "Source image: $existing_image ($(du -h "$existing_image" | cut -f1))"
1013
-
1111
+
1112
+ # Apply the author's art style (if any) before building the prompt
1113
+ apply_author_preview_overrides "$author"
1114
+
1014
1115
  # Build enhancement prompt
1015
1116
  local enhance_prompt
1016
1117
  enhance_prompt=$(build_enhance_prompt "$title" "$description" "$ENHANCE_PROMPT")
@@ -1085,10 +1186,13 @@ process_file() {
1085
1186
  # Preview path should NOT include /assets/ prefix since the template adds it
1086
1187
  local preview_path="/images/previews/${safe_filename}.png"
1087
1188
 
1189
+ # Apply the author's art style (if any) before building the prompt
1190
+ apply_author_preview_overrides "$author"
1191
+
1088
1192
  # Extract content for prompt generation
1089
1193
  local content
1090
1194
  content=$(extract_content "$file")
1091
-
1195
+
1092
1196
  # Generate prompt
1093
1197
  local prompt
1094
1198
  prompt=$(generate_prompt "$title" "$description" "$categories" "$content")
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Script Name: pixelate-preview-images
4
+ # Description: Pixelate + palette-quantize the Jekyll preview banners so they are
5
+ # much smaller files while retaining the retro pixel-art quality.
6
+ # Thin, conventions-friendly wrapper around the dependency-free
7
+ # Python engine in scripts/features/pixelate_images.py.
8
+ #
9
+ # Usage: ./scripts/features/pixelate-preview-images [OPTIONS] [PATHS...]
10
+ #
11
+ # With no PATHS, the configured preview_images.output_dir from _config.yml is
12
+ # processed (default: assets/images/previews).
13
+ #
14
+ # Options (wrapper):
15
+ # -h, --help Show this help
16
+ # -v, --version Print version
17
+ #
18
+ # Options (forwarded to the engine — see --help for the full list):
19
+ # -n, --dry-run Report savings without writing
20
+ # --colors N Palette size 2-256 (default: 256)
21
+ # --bits N Colour precision 1-8 used while quantizing (default: 6)
22
+ # --max-width N Downscale so width <= N (default: 1024)
23
+ # --scale F Scale factor, e.g. 0.5
24
+ # --block N Pixel block size (chunky NxN pixels)
25
+ # --filter nearest|box Downsample filter (default: nearest)
26
+ # --upscale Restore original WxH (chunky pixels) instead of reduced size
27
+ # --backup Keep <file>.orig when writing in place
28
+ # --force Write even if the result is not smaller
29
+ # -j, --jobs N Parallel worker processes (default: 1)
30
+ #
31
+ # Examples:
32
+ # # Preview the savings for every preview banner (no changes):
33
+ # ./scripts/features/pixelate-preview-images --dry-run -j 4
34
+ #
35
+ # # Optimize all preview banners in place, 4 workers:
36
+ # ./scripts/features/pixelate-preview-images -j 4
37
+ #
38
+ # # Optimize a single image with a chunkier pixel-art look:
39
+ # ./scripts/features/pixelate-preview-images --block 6 --colors 64 \
40
+ # assets/images/previews/about.png
41
+ #
42
+
43
+ set -euo pipefail
44
+ IFS=$'\n\t'
45
+
46
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
47
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
48
+ ENGINE="$SCRIPT_DIR/pixelate_images.py"
49
+ VERSION="1.0.0"
50
+
51
+ # Source shared logging helpers, with a minimal fallback.
52
+ if [[ -f "$PROJECT_ROOT/scripts/lib/common.sh" ]]; then
53
+ # shellcheck source=../lib/common.sh
54
+ source "$PROJECT_ROOT/scripts/lib/common.sh"
55
+ else
56
+ info() { echo "[INFO] $1"; }
57
+ success() { echo "[SUCCESS] $1"; }
58
+ warn() { echo "[WARNING] $1" >&2; }
59
+ error() { echo "[ERROR] $1" >&2; exit 1; }
60
+ print_header() { echo "== $1 =="; }
61
+ fi
62
+
63
+ usage() {
64
+ sed -n '2,49p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
65
+ echo ""
66
+ echo "Full engine options:"
67
+ python3 "$ENGINE" --help 2>/dev/null | sed -n '/^options:/,$p' | sed 's/^/ /'
68
+ }
69
+
70
+ # Resolve preview_images.output_dir from _config.yml (best-effort grep).
71
+ default_output_dir() {
72
+ local config="$PROJECT_ROOT/_config.yml"
73
+ local dir=""
74
+ if [[ -f "$config" ]]; then
75
+ dir="$(awk '
76
+ /^preview_images:/ { in_section=1; next }
77
+ in_section && /^[a-zA-Z_]+:/ { exit }
78
+ in_section && /output_dir/ {
79
+ sub(/.*output_dir[[:space:]]*:[[:space:]]*/, "")
80
+ gsub(/["\x27]/, ""); sub(/[[:space:]]*#.*/, "")
81
+ print; exit
82
+ }
83
+ ' "$config")"
84
+ fi
85
+ echo "${dir:-assets/images/previews}"
86
+ }
87
+
88
+ main() {
89
+ # Split off wrapper-only flags; forward everything else to the engine.
90
+ local forward=()
91
+ local arg
92
+ for arg in "$@"; do
93
+ case "$arg" in
94
+ -h|--help) usage; exit 0 ;;
95
+ -v|--version) echo "$VERSION"; exit 0 ;;
96
+ *) forward+=("$arg") ;;
97
+ esac
98
+ done
99
+
100
+ if ! command -v python3 >/dev/null 2>&1; then
101
+ error "python3 is required but was not found on PATH"
102
+ fi
103
+ if [[ ! -f "$ENGINE" ]]; then
104
+ error "Engine not found: $ENGINE"
105
+ fi
106
+
107
+ # If the caller did not name any existing path, default to the previews dir.
108
+ local has_path=false a
109
+ for a in ${forward[@]+"${forward[@]}"}; do
110
+ if [[ -e "$a" || -e "$PROJECT_ROOT/$a" ]]; then
111
+ has_path=true
112
+ break
113
+ fi
114
+ done
115
+ if [[ "$has_path" == false ]]; then
116
+ local dir
117
+ dir="$(default_output_dir)"
118
+ forward+=("$PROJECT_ROOT/$dir")
119
+ info "No path given; defaulting to: $dir"
120
+ fi
121
+
122
+ print_header "🟪 Pixelate Preview Images"
123
+ python3 "$ENGINE" ${forward[@]+"${forward[@]}"}
124
+ }
125
+
126
+ main "$@"