diff options
Diffstat (limited to 'scripts/theme-studio/generate.py')
| -rw-r--r-- | scripts/theme-studio/generate.py | 96 |
1 files changed, 82 insertions, 14 deletions
diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py index edfb7e52b..797fcc28e 100644 --- a/scripts/theme-studio/generate.py +++ b/scripts/theme-studio/generate.py @@ -1,4 +1,4 @@ -import json, os, re +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 * @@ -13,26 +13,40 @@ 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. - 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. + 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") - if not os.path.exists(path): - print(f"WARNING: nerd-icons legend absent ({path}); generic nerd-icons app") - return None - try: - with open(path) as src: - rows = json.load(src) - except (json.JSONDecodeError, OSError) as exc: - print(f"WARNING: nerd-icons legend malformed ({path}: {exc}); generic nerd-icons app") + 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 @@ -44,6 +58,38 @@ def load_nerd_icons_legend(path=None): 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>. @@ -68,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') @@ -300,7 +367,7 @@ def _build(): # 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()) + 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 @@ -321,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) |
