aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/generate.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio/generate.py')
-rw-r--r--scripts/theme-studio/generate.py108
1 files changed, 106 insertions, 2 deletions
diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py
index 6baa67a91..797fcc28e 100644
--- a/scripts/theme-studio/generate.py
+++ b/scripts/theme-studio/generate.py
@@ -1,5 +1,5 @@
-import json, os, re
-from app_inventory import add_inventory_apps, apply_default_face_seeds, apply_package_overrides, face_rows
+import json, os, re, base64
+from app_inventory import add_inventory_apps, add_nerd_icons_app, apply_default_face_seeds, apply_package_overrides, face_rows
from default_faces import DefaultFaces
from face_data import *
from face_specs import face_spec, ui_face_spec, migrate_legacy
@@ -12,6 +12,84 @@ def read_text(name):
def read_json(name):
return json.loads(read_text(name))
+NERD_ICONS_LEGEND_FIELDS = ("key", "label", "face", "category", "glyph")
+NERD_ICONS_GALLERY_GLYPH_FIELDS = ("glyph", "name")
+
+_NO_ARTIFACT = object() # distinguishes absent/malformed from a file that parsed to null
+
+def _load_nerd_icons_artifact(path, kind, tail):
+ """Open and JSON-parse the nerd-icons artifact at PATH. Return the parsed value,
+ or _NO_ARTIFACT (with a KIND/TAIL-labeled warning) when absent or malformed.
+ Shared skeleton for the legend and gallery loaders."""
+ if not os.path.exists(path):
+ print(f"WARNING: nerd-icons {kind} absent ({path}); {tail}")
+ return _NO_ARTIFACT
+ try:
+ with open(path) as src:
+ return json.load(src)
+ except (json.JSONDecodeError, OSError) as exc:
+ print(f"WARNING: nerd-icons {kind} malformed ({path}: {exc}); {tail}")
+ return _NO_ARTIFACT
+
+def load_nerd_icons_legend(path=None):
+ """Return the nerd-icons legend rows, or None when the artifact is unusable.
+
+ The legend is captured by build-nerd-icons-legend.el into nerd-icons-legend.json.
+ The artifact is a JSON object {legend, gallery}; a legacy bare array is read as
+ the legend directly (back-compat). Absent, malformed, empty, or carrying a row
+ without all five string fields (key/label/face/category/glyph) -> None, with a
+ warning, so the caller falls back to the generic nerd-icons app instead of
+ erroring. nerd-icons not being installed at capture time yields an empty/absent
+ file, which lands here as None.
+ """
+ path = path or os.path.join(HERE, "nerd-icons-legend.json")
+ data = _load_nerd_icons_artifact(path, "legend", "generic nerd-icons app")
+ if data is _NO_ARTIFACT:
+ return None
+ rows = data.get("legend") if isinstance(data, dict) else data
+ if not isinstance(rows, list) or not rows:
+ print(f"WARNING: nerd-icons legend empty ({path}); generic nerd-icons app")
+ return None
+ for row in rows:
+ if not (isinstance(row, dict)
+ and all(isinstance(row.get(f), str) and row.get(f)
+ for f in NERD_ICONS_LEGEND_FIELDS)):
+ print(f"WARNING: nerd-icons legend row invalid ({row!r}); generic nerd-icons app")
+ return None
+ return rows
+
+def load_nerd_icons_gallery(path=None):
+ """Return the nerd-icons gallery groups, or None when absent/unusable.
+
+ The gallery (the full colored catalog) rides nerd-icons-legend.json under the
+ "gallery" key: a list of {face, hue, glyphs:[{glyph,name}]} groups captured by
+ build-nerd-icons-legend.el, one group per color face, ordered by hue. A legacy
+ array-only artifact (legend, no gallery), an absent/malformed file, or a
+ structurally invalid group -> None, so the caller simply omits the gallery while
+ the legend data still loads. Never raises.
+ """
+ path = path or os.path.join(HERE, "nerd-icons-legend.json")
+ data = _load_nerd_icons_artifact(path, "gallery", "legend without gallery")
+ if data is _NO_ARTIFACT:
+ return None
+ groups = data.get("gallery") if isinstance(data, dict) else None
+ if not isinstance(groups, list) or not groups:
+ return None # legacy/array-only artifact: legend present, no gallery — not an error
+ for group in groups:
+ if not (isinstance(group, dict)
+ and isinstance(group.get("face"), str) and group["face"].startswith("nerd-icons-")
+ and isinstance(group.get("hue"), (int, float))
+ and isinstance(group.get("glyphs"), list) and group["glyphs"]):
+ print(f"WARNING: nerd-icons gallery group invalid ({group!r}); legend without gallery")
+ return None
+ for entry in group["glyphs"]:
+ if not (isinstance(entry, dict)
+ and all(isinstance(entry.get(f), str) and entry.get(f)
+ for f in NERD_ICONS_GALLERY_GLYPH_FIELDS)):
+ print(f"WARNING: nerd-icons gallery glyph invalid ({entry!r}); legend without gallery")
+ return None
+ return groups
+
def strip_exports(src):
"""Drop ES-module `export`/`import` lines so the body loads as a classic <script>.
@@ -36,7 +114,28 @@ COLORMATH_BODY=strip_exports(read_text('colormath.js'))
# template, filled at generate time. app.js carries the data placeholders
# (MAP_J, PALETTE_J, COLORMATH_J, ...); those are filled after it is spliced in.
STYLES=read_text('styles.css')
+# Inline the embedded nerd font as a base64 data: URI. The @font-face in
+# styles.css references the woff2 by a relative path so the source stays editable;
+# here that url is rewritten to a self-contained data: URI at generate time. The
+# payoff is portability — the page renders the glyphs on any clone, with no
+# dependency on a separately-shipped font file or a system-installed copy — and it
+# removes any question about how a file:// font url loads across browsers.
+# (The tofu bug this feature chased was NOT a load failure: the confirmed causes
+# were a double-quoted font-family inside an inline style attribute, which
+# silently dropped the family — see previews.js PREVIEW_FONT — and a woff2 encoded
+# by woff2_compress that headed Chrome/Firefox reject; the woff2 is now encoded by
+# fontTools via `make font`. The data: URI is the durable self-contained form, not
+# the fix for those two bugs.)
+_FONT_WOFF2='SymbolsNerdFontMono-Regular.woff2'
+if os.path.exists(os.path.join(HERE,_FONT_WOFF2)):
+ with open(os.path.join(HERE,_FONT_WOFF2),'rb') as _ff:
+ _FONT_B64=base64.b64encode(_ff.read()).decode('ascii')
+ STYLES=STYLES.replace('url("%s")'%_FONT_WOFF2,
+ 'url("data:font/woff2;base64,%s")'%_FONT_B64)
APP_BODY=read_text('app.js')
+# Custom dropdown / detail-editor / expander factories, split from app.js for
+# navigability and spliced in at the CONTROLS_J token. Raw (no imports/exports).
+CONTROLS_BODY=read_text('controls.js')
# Bespoke per-package preview renderers, spliced into the page <script> via the
# PREVIEWS_J token in app.js. No imports/exports, so read raw.
PREVIEWS_BODY=read_text('previews.js')
@@ -265,6 +364,10 @@ def _build():
# Phase 6: merge the generated all-package inventory (refresh with build-inventory.el).
# Bespoke apps stay; every other installed package becomes an editable generic app.
_inv_path=os.path.join(HERE,"package-inventory.json")
+ # nerd-icons becomes a bespoke filetype-legend app when its captured legend is
+ # valid; otherwise add_inventory_apps below makes it a plain generic app (the
+ # fallback). Must precede add_inventory_apps so the generic path skips it.
+ add_nerd_icons_app(APPS, _inv_path, load_nerd_icons_legend(), load_nerd_icons_gallery())
add_inventory_apps(APPS, _inv_path)
apply_default_face_seeds(APPS, DEFAULTS)
# Apply seed theme package overrides when THEME_STUDIO_SEED is set: each full
@@ -285,6 +388,7 @@ def _build():
def fill_data(s):
return (s.replace("COLORMATH_J",COLORMATH_BODY)
.replace("APP_CORE_J",APP_CORE_BODY)
+ .replace("CONTROLS_J",CONTROLS_BODY)
.replace("PREVIEWS_J",PREVIEWS_BODY)
.replace("APP_UTIL_J",APP_UTIL_BODY)
.replace("PALETTE_GENERATOR_CORE_J",PALETTE_GENERATOR_CORE_BODY)