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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +395 -0
- data/README.md +27 -19
- data/_data/authors.yml +154 -5
- data/_data/backlog.yml +5 -5
- data/_data/content_statistics.yml +273 -297
- data/_data/features.yml +4 -25
- data/_data/navigation/README.md +24 -0
- data/_data/navigation/about.yml +2 -0
- data/_data/navigation/main.yml +2 -7
- data/_data/roadmap.yml +86 -12
- data/_includes/components/author-avatar-url.html +28 -0
- data/_includes/components/author-bio.html +86 -0
- data/_includes/components/author-card.html +184 -121
- data/_includes/components/author-eeat.html +10 -4
- data/_includes/components/info-section.html +1 -1
- data/_includes/components/mermaid.html +0 -3
- data/_includes/components/post-card.html +19 -9
- data/_includes/content/giscus.html +3 -2
- data/_includes/core/footer-fabs.html +28 -0
- data/_includes/core/footer.html +7 -17
- data/_includes/core/head.html +2 -2
- data/_includes/navigation/breadcrumbs.html +20 -2
- data/_includes/navigation/local-graph.html +18 -2
- data/_includes/obsidian/full-graph.html +4 -6
- data/_layouts/article.html +44 -74
- data/_layouts/author.html +274 -0
- data/_layouts/authors.html +55 -0
- data/_layouts/news.html +3 -3
- data/_layouts/note.html +21 -6
- data/_layouts/notebook.html +21 -6
- data/_layouts/root.html +31 -17
- data/_layouts/section.html +3 -3
- data/_plugins/author_pages_generator.rb +121 -0
- data/_sass/components/_author.scss +219 -0
- data/_sass/components/_content-tables.scss +16 -1
- data/_sass/components/_notes-index.scss +102 -0
- data/_sass/components/_search-modal.scss +40 -0
- data/_sass/components/_ui-enhancements.scss +570 -0
- data/_sass/core/_docs-code-examples.scss +463 -0
- data/_sass/core/_docs-layout.scss +0 -453
- data/_sass/core/_navbar.scss +253 -0
- data/_sass/core/_sidebar-extras.scss +79 -0
- data/_sass/core/_toc.scss +87 -0
- data/_sass/core/_variables.scss +7 -142
- data/_sass/custom.scss +24 -1122
- data/_sass/layouts/_global-chrome.scss +59 -0
- data/assets/css/main.scss +19 -2
- data/assets/js/author-profile.js +190 -0
- data/assets/js/modules/navigation/navbar.js +104 -0
- data/assets/js/obsidian-graph.js +2 -2
- data/assets/js/obsidian-local-graph.js +11 -5
- data/assets/vendor/cytoscape/cytoscape.min.js +32 -0
- data/scripts/README.md +39 -0
- data/scripts/bin/validate +11 -1
- data/scripts/dev/css-diff.sh +49 -0
- data/scripts/dev/shot.js +37 -0
- data/scripts/features/generate-preview-images +110 -6
- data/scripts/features/pixelate-preview-images +126 -0
- data/scripts/features/pixelate_images.py +662 -0
- data/scripts/github-setup.sh +0 -0
- data/scripts/lib/preview_generator.py +47 -3
- data/scripts/pixelate-preview-images.sh +12 -0
- data/scripts/test/integration/auto-version +10 -8
- data/scripts/test/lib/run_tests.sh +2 -0
- data/scripts/test/lib/test_content_review.sh +205 -0
- data/scripts/test/lib/test_pixelate_images.sh +108 -0
- metadata +25 -20
- data/_data/hub.yml +0 -68
- data/_data/hub_index.yml +0 -203
- data/_data/navigation/hub.yml +0 -110
- data/assets/vendor/font-awesome/css/all.min.css +0 -9
- data/assets/vendor/font-awesome/webfonts/fa-brands-400.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-regular-400.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-solid-900.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 +0 -0
- data/assets/vendor/jquery/jquery-3.7.1.min.js +0 -2
- data/scripts/lib/hub.rb +0 -208
- data/scripts/provision-org-sites.rb +0 -252
- data/scripts/provision-org-sites.sh +0 -23
- data/scripts/sync-hub-metadata.rb +0 -184
- 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
|
|
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"
|
data/scripts/dev/shot.js
ADDED
|
@@ -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
|
|
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 "$@"
|