From c11ad211f5d72b6ee2b48d80f25d16e3e85248eb Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 24 Jun 2026 14:04:48 -0400 Subject: fix(theme-studio): render nerd-icon glyphs in previews instead of tofu The legend, dashboard, and package previews drew nerd-icon glyphs as empty boxes. The font-family never reached them: PREVIEW_FONT was spliced into inline style="..." attributes with a double-quoted family name, so the inner quote closed the attribute early and the font was silently dropped. Dropping the quotes fixes it. A no-space family name needs none. I embedded the glyph font directly: Symbols Nerd Font Mono, encoded with fontTools (woff2_compress output is rejected by headed Chrome and Firefox), inlined as a data: URI under the unique family name ThemeStudioNerd so it resolves to the embed rather than a system-installed copy of the same name. The page is self-contained and renders on any clone. I added a #fonttest gate that parses previewLines output and asserts the resolved font-family plus glyph coverage, plus a make font target that re-encodes the woff2 with fontTools. --- scripts/theme-studio/Makefile | 17 +++++- .../theme-studio/SymbolsNerdFontMono-Regular.woff2 | Bin 0 -> 1177892 bytes scripts/theme-studio/browser-gates.js | 35 ++++++++++++ scripts/theme-studio/generate.py | 20 ++++++- scripts/theme-studio/previews.js | 27 +++++++-- scripts/theme-studio/styles.css | 1 + scripts/theme-studio/theme-studio.html | 63 +++++++++++++++++++-- 7 files changed, 151 insertions(+), 12 deletions(-) create mode 100644 scripts/theme-studio/SymbolsNerdFontMono-Regular.woff2 (limited to 'scripts/theme-studio') diff --git a/scripts/theme-studio/Makefile b/scripts/theme-studio/Makefile index 7b8430182..02540720e 100644 --- a/scripts/theme-studio/Makefile +++ b/scripts/theme-studio/Makefile @@ -16,7 +16,13 @@ OUT ?= ../../themes EMACS ?= emacs EMACSCLIENT ?= emacsclient -.PHONY: help test check check-generated coverage gen open theme theme-load theme-reload face-coverage-dump face-coverage face-coverage-diff +# Source TTF for the embedded nerd-icon font. `make font` re-encodes it to the +# committed woff2 with fontTools — NOT woff2_compress, whose output headed Chrome +# and Firefox reject (they render tofu) even though old-headless Chrome accepts it. +NERD_TTF ?= /usr/share/fonts/TTF/SymbolsNerdFontMono-Regular.ttf +NERD_WOFF2 ?= SymbolsNerdFontMono-Regular.woff2 + +.PHONY: help test check check-generated coverage gen open theme theme-load theme-reload face-coverage-dump face-coverage face-coverage-diff font # Scratch path for the face-coverage Emacs data dump. FACE_DUMP ?= /tmp/face-coverage-data.json @@ -36,6 +42,15 @@ help: @echo " make theme-reload JSON=x - Convert JSON, then cleanly reload its theme in current Emacs" @echo " make face-coverage - Regenerate face-coverage.org from the live Emacs daemon" @echo " make face-coverage-diff - Show the coverage delta vs the committed face-coverage.org" + @echo " make font - Re-encode the embedded nerd woff2 from NERD_TTF (fontTools)" + +font: + @python3 -c "import os,sys; from fontTools.ttLib import TTFont; \ +src='$(NERD_TTF)'; \ +sys.exit('NERD_TTF not found: '+src) if not os.path.exists(src) else None; \ +f=TTFont(src); f.flavor='woff2'; f.save('$(NERD_WOFF2)'); \ +print('wrote $(NERD_WOFF2) (%d bytes) from %s' % (os.path.getsize('$(NERD_WOFF2)'), src))" + @echo "now run: make gen (re-inlines the woff2 as a data: URI into theme-studio.html)" test: @./run-tests.sh diff --git a/scripts/theme-studio/SymbolsNerdFontMono-Regular.woff2 b/scripts/theme-studio/SymbolsNerdFontMono-Regular.woff2 new file mode 100644 index 000000000..2dc2a17de Binary files /dev/null and b/scripts/theme-studio/SymbolsNerdFontMono-Regular.woff2 differ diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index a4412d0b5..0f2f8e5a7 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -1291,3 +1291,38 @@ if(location.hash==='#locateclicktest')gate('locateclicktest',A=>withSavedState([ mspan.dispatchEvent(new MouseEvent('click',{bubbles:true})); A(urow()&&urow().classList.contains('flash'),'a UI mock span still flashes its row through the unified dispatcher');} })); +// Embedded-font gate (open with #fonttest): the nerd-icons legend, dashboard +// navigator, and package previews render their glyphs in a real nerd font +// instead of tofu. Verifies (1) the ThemeStudioNerd @font-face is registered, +// (2) previewLines actually APPLIES that family — the div is parsed into the DOM +// and getComputedStyle must resolve to ThemeStudioNerd (a double-quoted family in +// the inline style attribute silently drops it, so a plain string match would +// false-pass), and (3) the embedded woff2 loads AND covers the glyph codepoints +// the previews use — both a BMP glyph (U+F121) and a supplementary-plane Material +// Design glyph (U+F0474), the range most likely missing from a partial font. +// Async: it awaits the font load, then appends the verdict (the runner's +// virtual-time budget covers it). +if(location.hash==='#fonttest'){ + const fam='ThemeStudioNerd',notes=[]; + const bmp='',supp='\u{f0474}'; + const finish=()=>{const v='FONTTEST '+(notes.length?'FAIL':'PASS');document.title=v; + const d=document.createElement('div');d.id='fonttest'; + d.textContent=v+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);}; + const registered=[...document.fonts].some(f=>f.family.replace(/["']/g,'')===fam); + if(!registered)notes.push('no-fontface'); + // Parse the actual previewLines output into the DOM and read the resolved + // font-family off the rendered element — not a substring of the HTML string. A + // double-quoted family name inside the inline style="..." attribute terminates + // the attribute early and silently drops the font-family, so a string match + // passes while the rendered font is empty; computed style catches that. + const probe=document.createElement('div');probe.innerHTML=previewLines(['x']); + document.body.appendChild(probe); + const inner=probe.firstElementChild; + const ff=inner?getComputedStyle(inner).fontFamily:''; + if(ff.indexOf(fam)<0)notes.push('previews-font-not-applied('+(ff||'empty')+')'); + Promise.all([document.fonts.load('16px "'+fam+'"',bmp),document.fonts.load('16px "'+fam+'"',supp)]).then(()=>{ + if(!document.fonts.check('16px "'+fam+'"',bmp))notes.push('bmp-glyph-missing'); + if(!document.fonts.check('16px "'+fam+'"',supp))notes.push('supp-glyph-missing'); + probe.remove();finish(); + }).catch(e=>{probe.remove();notes.push('load-error:'+(e&&e.message||e));finish();}); +} diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py index edfb7e52b..6053aa62f 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 * @@ -68,6 +68,24 @@ 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') # Bespoke per-package preview renderers, spliced into the page -- cgit v1.2.3