From 9c7010ebe2041ae73195745d76403568237c2905 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 2 Jul 2026 13:35:28 -0400 Subject: feat(theme-studio): web-mode scene covering all 81 faces + HTML sample The web-mode preview is one mixed document: markup with every tag/attr variant, an inline CSS part, a generic template block (engine-agnostic on purpose), and a script part carrying a JSON island, JSX depths, nested template literals, SQL-in-a-string, a PHP preprocessor island, and JSDoc annotations. The realism gate now covers it, so all 81 faces are exercised. SAMPLES gains an HTML language, which also enriches the syntax and auto-dim previews. --- scripts/theme-studio/app.js | 1 + scripts/theme-studio/app_inventory.py | 1 + scripts/theme-studio/generate.py | 2 +- scripts/theme-studio/previews.js | 47 ++++++++++++++++++++++++++++++++ scripts/theme-studio/samples.py | 18 ++++++++++++ scripts/theme-studio/test_generate.py | 2 +- scripts/theme-studio/theme-studio.html | 50 +++++++++++++++++++++++++++++++++- 7 files changed, 118 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index 8428b407..8fccfd83 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -564,6 +564,7 @@ const PACKAGE_PREVIEWS={ novreading:renderNovReadingPreview,aiterm:renderAiTermPreview, company:renderCompanyPreview,companybox:renderCompanyBoxPreview,transient:renderTransientPreview, magitsection:renderMagitSectionPreview,rainbowdelims:renderRainbowDelimitersPreview, + webmode:renderWebModePreview, erc:renderErcPreview,orgdrill:renderOrgdrillPreview,orgnoter:renderOrgnoterPreview,signel:renderSignelPreview, pearl:renderPearlPreview,slack:renderSlackPreview,telega:renderTelegaPreview,shr:renderShrPreview, nerdicons:renderNerdIconsPreview diff --git a/scripts/theme-studio/app_inventory.py b/scripts/theme-studio/app_inventory.py index 81d6d532..d493475d 100644 --- a/scripts/theme-studio/app_inventory.py +++ b/scripts/theme-studio/app_inventory.py @@ -26,6 +26,7 @@ PREVIEW_KEYS = { "transient": "transient", "magit-section": "magitsection", "rainbow-delimiters": "rainbowdelims", + "web-mode": "webmode", } # Custom display labels for inventory apps whose package name is an acronym diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py index dc6d0a8e..5636d753 100644 --- a/scripts/theme-studio/generate.py +++ b/scripts/theme-studio/generate.py @@ -168,7 +168,7 @@ ns={} src=read_text('samples.py') exec(src[:src.index('# THEME_STUDIO_DATA_END')], ns) SAMPLES={"Elisp":ns['ELS'],"Go":ns['GOS'],"Python":ns['PYS'],"TypeScript":ns['TSS'],"Java":ns['JAS'],"C":ns['CS'],"C++":ns['CPS'],"Rust":ns['RUSTS'],"Zig":ns['ZIGS'],"Shell":ns['SHS'], - "Racket":ns['RACKETS'],"Scheme":ns['SCHEMES'],"Haskell":ns['HASKELLS'],"OCaml":ns['OCAMLS'],"Scala":ns['SCALAS'],"Kotlin":ns['KOTLINS'],"Swift":ns['SWIFTS'],"Lua":ns['LUAS'],"Ruby":ns['RUBYS'],"Perl":ns['PERLS'],"R":ns['RLANGS'],"Erlang":ns['ERLANGS'],"SQL":ns['SQLS'],"PHP":ns['PHPS'],"Ada":ns['ADAS'],"Fortran":ns['FORTRANS'],"MATLAB":ns['MATLABS'],"Assembly":ns['ASMS']} + "Racket":ns['RACKETS'],"Scheme":ns['SCHEMES'],"Haskell":ns['HASKELLS'],"OCaml":ns['OCAMLS'],"Scala":ns['SCALAS'],"Kotlin":ns['KOTLINS'],"Swift":ns['SWIFTS'],"Lua":ns['LUAS'],"Ruby":ns['RUBYS'],"Perl":ns['PERLS'],"R":ns['RLANGS'],"Erlang":ns['ERLANGS'],"SQL":ns['SQLS'],"PHP":ns['PHPS'],"Ada":ns['ADAS'],"Fortran":ns['FORTRANS'],"MATLAB":ns['MATLABS'],"Assembly":ns['ASMS'],"HTML":ns['HTMLS']} COLS=ns['COLS'] DEFAULT_FACES_PATH=os.path.join(HERE,'emacs-default-faces.json') diff --git a/scripts/theme-studio/previews.js b/scripts/theme-studio/previews.js index 4c9474fe..0cd4d86a 100644 --- a/scripts/theme-studio/previews.js +++ b/scripts/theme-studio/previews.js @@ -592,6 +592,53 @@ function renderRainbowDelimitersPreview(){const a='rainbow-delimiters',L=[]; L.push(os(a,'rainbow-delimiters-base-face','base-face')+' underlies every depth; ' +os(a,'rainbow-delimiters-base-error-face','base-error-face')+' underlies both error faces'); return previewLines(L);} +function renderWebModePreview(){const a='web-mode',L=[],o=(f,t)=>os(a,'web-mode-'+f,t); + // One mixed HTML document exercising all 81 faces: markup, an inline CSS + // part, a JS part with a JSON island / JSX / template literals / SQL-in-a- + // string, a generic {{...}}/{%...%} template block (engine-agnostic on + // purpose), and a JSDoc-style annotation comment. + const B=(t)=>o('html-tag-bracket-face',t); + L.push(o('doctype-face','')); + L.push(o('comment-face','')); + L.push(B('<')+o('html-tag-face','html')+' '+o('html-attr-name-face','lang')+o('html-attr-equal-face','=')+o('html-attr-value-face','"en"')+B('>')); + L.push(''); + L.push(B('<')+o('html-tag-face','style')+B('>')+o('style-face',' /* the css part rides web-mode-style-face */')); + L.push(' '+o('css-comment-face','/* layout */')); + L.push(' '+o('css-at-rule-face','@media')+' (min-width: 40rem) {'); + L.push(' '+o('css-selector-tag-face','body')+' '+o('css-selector-face','#page')+' '+o('css-selector-class-face','.card')+o('css-pseudo-class-face',':hover')+' {'); + L.push(' '+o('css-property-name-face','color')+': '+o('css-color-face','#67809c')+' '+o('css-priority-face','!important')+';'); + L.push(' '+o('css-property-name-face','width')+': '+o('css-function-face','calc')+'(100% - '+o('css-variable-face','--gutter')+');'); + L.push(' '+o('css-property-name-face','font-family')+': '+o('css-string-face','"Berkeley Mono"')+'; } }'); + L.push(B('')); + L.push(''); + L.push(B('<')+o('html-tag-face','body')+' '+o('html-attr-custom-face','data-theme')+o('html-attr-equal-face','=')+o('html-attr-value-face','"dupre"')+B('>')); + L.push(' '+o('current-element-highlight-face',B('<')+o('html-tag-face','main')+B('>'))+' '+o('comment-face','')); + L.push(' '+B('<')+o('html-tag-custom-face','cart-widget')+' '+o('html-attr-engine-face',':items')+o('html-attr-equal-face','=')+o('html-attr-value-face','"cart.lines"')+B('/>')); + L.push(' '+B('<')+o('html-tag-namespaced-face','svg:rect')+' '+o('html-attr-name-face','width')+o('html-attr-equal-face','=')+o('html-attr-value-face','"12"')+B('/>')); + L.push(' '+B('<')+o('html-tag-face','p')+B('>')+'Fish '+o('html-entity-face','&')+' chips '+B('<')+o('html-tag-face','b')+B('>')+o('bold-face','bold')+B('')+' '+B('<')+o('html-tag-face','i')+B('>')+o('italic-face','italic')+B('')+' '+B('<')+o('html-tag-face','u')+B('>')+o('underline-face','underlined')+B('')+B('')); + L.push(' '+B('<')+o('html-tag-unclosed-face','li')+B('>')+'this li never closes '+o('error-face','')+' '+o('warning-face','duplicate id')+' '+o('whitespace-face','···')+' '+o('folded-face','[details folded…]')+' '+o('inlay-face',' 3 items ')+' '+o('current-column-highlight-face','│')); + L.push(''); + L.push(' '+o('block-delimiter-face','{%')+' '+o('block-control-face','if')+' '+o('variable-name-face','user')+o('block-face','.signed_in')+' '+o('block-delimiter-face','%}')+' '+o('block-comment-face','{# template block, any engine #}')); + L.push(' '+o('block-delimiter-face','{{')+' '+o('variable-name-face','user')+o('block-face','.name')+' '+o('filter-face','| capitalize')+' '+o('block-delimiter-face','}}')); + L.push(' '+o('block-delimiter-face','{%')+' '+o('block-control-face','include')+' '+o('block-string-face','"badge.html"')+' '+o('block-attr-name-face','with')+' '+o('block-attr-value-face','tier=gold')+' '+o('block-delimiter-face','%}')); + L.push(' '+o('block-delimiter-face','{%')+' '+o('block-control-face','endif')+' '+o('block-delimiter-face','%}')); + L.push(''); + L.push(B('<')+o('html-tag-face','script')+B('>')+o('script-face',' // the js part rides web-mode-script-face')+o('part-face',' (embedded parts share web-mode-part-face)')); + L.push(' '+o('comment-face','/**')); + L.push(' '+o('annotation-face',' * Render one cart row.')); + L.push(' '+o('annotation-face',' * ')+o('annotation-tag-face','@param')+' '+o('annotation-type-face','{Line}')+' '+o('annotation-value-face','line')+' '+o('annotation-face','the row, e.g. ')+o('annotation-html-face','line.sku')); + L.push(' '+o('annotation-face',' */')); + L.push(' '+o('keyword-face','const')+' '+o('constant-face','VAT')+' = '+o('variable-name-face','rate')+' ?? 0.21; '+o('javascript-comment-face','// js comment')+' '+o('part-comment-face','/* part comment */')); + L.push(' '+o('keyword-face','function')+' '+o('function-name-face','renderLine')+'('+o('param-name-face','line')+', '+o('param-name-face','fmt')+') {'); + L.push(' '+o('keyword-face','const')+' '+o('type-face','Money')+' '+o('variable-name-face','total')+' = '+o('function-call-face','round')+'('+o('variable-name-face','line')+'.'+o('symbol-face','qty')+' * '+o('builtin-face','Number')+'('+o('variable-name-face','line')+'.price));'); + L.push(' '+o('keyword-face','return')+' '+o('javascript-string-face','`')+o('interpolate-color1-face','${')+o('interpolate-color2-face','fmt(')+o('interpolate-color3-face','${')+o('interpolate-color4-face','total')+o('interpolate-color3-face','}')+o('interpolate-color2-face',')')+o('interpolate-color1-face','}')+o('javascript-string-face',' EUR`')+'; }'); + L.push(' '+o('keyword-face','const')+' '+o('variable-name-face','q')+' = '+o('part-string-face','"')+o('sql-keyword-face','SELECT')+o('part-string-face',' sku ')+o('sql-keyword-face','FROM')+o('part-string-face',' lines ')+o('sql-keyword-face','WHERE')+o('part-string-face',' qty > 0"')+';'); + L.push(' '+o('keyword-face','const')+' '+o('variable-name-face','cfg')+' = '+o('json-context-face','{ ')+o('json-key-face','"currency"')+': '+o('json-string-face','"EUR"')+o('json-context-face',', ')+o('json-comment-face','/* json island */')+o('json-context-face',' }')+';'); + L.push(' '+o('keyword-face','return')+' ('+o('jsx-depth-1-face','')+o('jsx-depth-2-face','')+o('jsx-depth-3-face','')+o('jsx-depth-4-face','')+o('jsx-depth-5-face','{n}')+o('jsx-depth-4-face','')+o('jsx-depth-3-face','')+o('jsx-depth-2-face','')+o('jsx-depth-1-face','')+');'); + L.push(' '+o('preprocessor-face','')); + L.push(B('')); + L.push(B('')); + return previewLines(L);} function renderAiTermPreview(){const a='ai-term',L=[]; // What these faces actually paint: the Claude Code TUI inside an agent // terminal. The banner is the fixed accent (every session); each /color diff --git a/scripts/theme-studio/samples.py b/scripts/theme-studio/samples.py index feebd1b7..ca568ed3 100644 --- a/scripts/theme-studio/samples.py +++ b/scripts/theme-studio/samples.py @@ -704,6 +704,24 @@ ASMS=[ [('p',' '),('kw','xor'),('p',' '),('var','rdi'),('punc',','),('p',' '),('var','rdi'),('p',' '),('cmd',';'),('cm',' status 0')], [('p',' '),('kw','syscall')], ] +HTMLS=[ + [('cm','')], + [('cm','')], + [('punc','<'),('fnd','html'),('p',' '),('var','lang'),('op','='),('str','"en"'),('punc','>')], + [('punc','<'),('fnd','head'),('punc','>')], + [('p',' '),('punc','<'),('fnd','style'),('punc','>')], + [('p',' '),('pp','@media'),('p',' '),('punc','('),('prop','min-width'),('op',':'),('p',' '),('num','40rem'),('punc',')'),('p',' '),('punc','{')], + [('p',' '),('ty','body'),('p',' '),('fnc','.card'),('kw',':hover'),('p',' '),('punc','{'),('p',' '),('prop','color'),('op',':'),('p',' '),('con','#67809c'),('punc',';'),('p',' '),('punc','}}')], + [('p',' '),('punc','')], + [('punc','')], + [('punc','<'),('fnd','body'),('p',' '),('var','data-theme'),('op','='),('str','"dupre"'),('punc','>')], + [('p',' '),('punc','<'),('fnd','h1'),('punc','>'),('p','Fish '),('esc','&'),('p',' Chips'),('punc','')], + [('p',' '),('punc','<'),('fnd','script'),('punc','>')], + [('p',' '),('kw','const'),('p',' '),('var','total'),('p',' '),('op','='),('p',' '),('fnc','round'),('punc','('),('var','qty'),('p',' '),('op','*'),('p',' '),('var','price'),('punc',');'),('p',' '),('cmd','//'),('cm',' inline js')], + [('p',' '),('punc','')], + [('punc','')], + [('punc','')], +] # THEME_STUDIO_DATA_END: generate.py execs only the lines above this marker (the # code samples and COLS). Everything below is the standalone /tmp/dupre-canon.html diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py index 7fb0b60f..d391ab83 100644 --- a/scripts/theme-studio/test_generate.py +++ b/scripts/theme-studio/test_generate.py @@ -855,7 +855,7 @@ class BespokePreviewFaceCoverage(unittest.TestCase): FULL_COVERAGE_APPS = [ "company", "company-box", "transient", "magit-section", - "rainbow-delimiters", + "rainbow-delimiters", "web-mode", ] def test_every_face_appears_in_the_renderer(self): diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 73fba2b2..8ae812ac 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -297,7 +297,7 @@