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.py153
1 files changed, 88 insertions, 65 deletions
diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py
index 251c05036..6baa67a91 100644
--- a/scripts/theme-studio/generate.py
+++ b/scripts/theme-studio/generate.py
@@ -68,10 +68,11 @@ SYNTAX_DOCS=_face_docs['syntax']
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']}
+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']}
COLS=ns['COLS']
DEFAULT_FACES_PATH=os.path.join(HERE,'emacs-default-faces.json')
-DEFAULTS=DefaultFaces.from_path(DEFAULT_FACES_PATH)
+
def column_id(name):
name = name or 'color'
if re.fullmatch(r'color-\d+', name):
@@ -206,9 +207,6 @@ def apply_seed_packages(apps,data,seed):
if seed:
apply_package_overrides(apps,data.get('packages'))
-MAP,BOLD,ITALIC_MAP=initial_maps(COLS,DEFAULTS)
-
-PALETTE=[[MAP['bg'],"bg","ground"],[MAP['p'],"fg","ground"]]
CATS=[["bg","bg (ground)","Aa Bb 123"],["p","fg","other / whitespace"],["kw","keyword","class def if return"],["bi","builtin","len echo printf"],
["pp","preprocessor","#include #define"],["fnd","function · def","resolve push"],
["fnc","function · call","printf rsync get"],["dec","decorator → type","@dataclass"],
@@ -221,7 +219,7 @@ CATS=[["bg","bg (ground)","Aa Bb 123"],["p","fg","other / whitespace"],["kw","ke
UI_FACES=[["cursor","cursor","Aa|"],["region","region (selection)","selected text"],
["hl-line","hl-line (current line)","current line"],["highlight","highlight","hover"],
["mode-line","mode-line","status active"],
- ["mode-line-highlight","mode-line-highlight (mode-line hover)","git:main"],
+ ["mode-line-highlight","mode-line-highlight (hover)","git:main"],
["mode-line-inactive","mode-line-inactive","status idle"],
["fringe","fringe","| |"],["line-number","line-number"," 42"],
["line-number-current-line","line-number-current-line","> 42"],["minibuffer-prompt","minibuffer-prompt","M-x "],
@@ -230,72 +228,97 @@ UI_FACES=[["cursor","cursor","Aa|"],["region","region (selection)","selected tex
["show-paren-mismatch","show-paren-mismatch",") ("],["link","link","https://"],
["error","error","error!"],["warning","warning","warning"],
["success","success","ok"],["vertical-border","vertical-border","|"]]
-UIMAP=build_uimap(UI_FACES,DEFAULTS)
-# Optional palette seed: THEME_STUDIO_SEED=<file.json> seeds the tool's starting
-# palette / syntax / UI from a theme.json (path relative to
-# this dir), instead of the hardcoded defaults above. Unset leaves them unchanged.
-# Placed after every default it overrides (notably UIMAP) so the merge has targets.
-# Mirrors what the in-page Import does, so reseed and import agree.
-LOCKS=[]
-# THEME_STUDIO_SEED=<file>.json opens an existing theme as the starting point.
-# Unset starts empty: only bg/fg are in the palette.
-_seed=os.environ.get('THEME_STUDIO_SEED')
-_d=load_seed_data(_seed)
-PALETTE,UIMAP,LOCKS=apply_seed_basics(_d,PALETTE,UIMAP,LOCKS)
-PALETTE=normalize_palette(PALETTE)
-SYNTAX=build_syntax(COLS,MAP,BOLD,ITALIC_MAP,DEFAULTS)
-apply_syntax_seed(_d if _seed else {},SYNTAX,MAP)
-# Bespoke apps are single-sourced as BESPOKE_APP_SPECS in face_data.py (one
-# row per app: key, label, preview, FACES, prefix, SEED).
-APPS={key:{"label":label,"preview":preview,"faces":face_rows(faces,prefix,seed)}
- for key,label,preview,faces,prefix,seed in BESPOKE_APP_SPECS}
-# 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")
-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
-# per-face spec (color + structure) replaces the hardcoded face seed before render.
-apply_seed_packages(APPS,_d,_seed)
+OUT=os.path.join(HERE,'theme-studio.html')
+_CACHE={}
-if DEFAULTS.available:
- add_default_palette_colors(PALETTE,MAP,SYNTAX,UIMAP,APPS,DEFAULTS)
+def _build():
+ """Assemble the page, caching the derived data + HTML. Deferred from import
+ so a consumer that only needs the cheap module constants (e.g.
+ face_coverage.py reading UI_FACES) does not pay the full DEFAULTS + inventory
+ + fill cost; the file write stays __main__-guarded as before."""
+ if _CACHE:
+ return _CACHE
+ DEFAULTS=DefaultFaces.from_path(DEFAULT_FACES_PATH)
+ MAP,BOLD,ITALIC_MAP=initial_maps(COLS,DEFAULTS)
+ PALETTE=[[MAP['bg'],"bg","ground"],[MAP['p'],"fg","ground"]]
+ UIMAP=build_uimap(UI_FACES,DEFAULTS)
-PALETTE=normalize_palette(PALETTE)
-HTML=read_text('theme-studio.template.html')
-# Fill the data placeholders. str.replace is literal (no backref interpretation),
-# so backslashes in the inlined JS survive intact — the escaping-bug class that
-# the triple-quoted string used to cause is gone now that app.js is a real file.
-# Caveat: these tokens are replaced everywhere they appear, including inside code
-# comments. Don't write a placeholder name (COLORMATH_J, APP_CORE_J, ...) in
-# prose in any inlined file, or that prose gets the body spliced into it too.
-def fill_data(s):
- return (s.replace("COLORMATH_J",COLORMATH_BODY)
- .replace("APP_CORE_J",APP_CORE_BODY)
- .replace("PREVIEWS_J",PREVIEWS_BODY)
- .replace("APP_UTIL_J",APP_UTIL_BODY)
- .replace("PALETTE_GENERATOR_CORE_J",PALETTE_GENERATOR_CORE_BODY)
- .replace("PALETTE_GENERATOR_UI_J",PALETTE_GENERATOR_UI_BODY)
- .replace("PALETTE_ACTIONS_J",PALETTE_ACTIONS_BODY)
- .replace("BROWSER_GATES_J",BROWSER_GATES_BODY)
- .replace("COLOR_NAMES_J",json.dumps(COLOR_NAMES))
- .replace("FACE_DOCS_J",json.dumps(FACE_DOCS)).replace("SYNTAX_DOCS_J",json.dumps(SYNTAX_DOCS))
- .replace("SAMPLES_J",json.dumps(SAMPLES))
- .replace("PALETTE_J",json.dumps(PALETTE)).replace("CATS_J",json.dumps(CATS))
- .replace("UIFACES_J",json.dumps(UI_FACES)).replace("UIMAP_J",json.dumps(UIMAP)).replace("APPS_J",json.dumps(APPS))
- .replace("SYNTAX_J",json.dumps(SYNTAX)).replace("MAP_J",json.dumps(MAP)).replace("LOCKS_J",json.dumps(LOCKS)))
+ # Optional palette seed: THEME_STUDIO_SEED=<file.json> seeds the tool's starting
+ # palette / syntax / UI from a theme.json (path relative to
+ # this dir), instead of the hardcoded defaults above. Unset leaves them unchanged.
+ # Placed after every default it overrides (notably UIMAP) so the merge has targets.
+ # Mirrors what the in-page Import does, so reseed and import agree.
+ LOCKS=[]
+ # THEME_STUDIO_SEED=<file>.json opens an existing theme as the starting point.
+ # Unset starts empty: only bg/fg are in the palette.
+ _seed=os.environ.get('THEME_STUDIO_SEED')
+ _d=load_seed_data(_seed)
+ PALETTE,UIMAP,LOCKS=apply_seed_basics(_d,PALETTE,UIMAP,LOCKS)
+ PALETTE=normalize_palette(PALETTE)
+ SYNTAX=build_syntax(COLS,MAP,BOLD,ITALIC_MAP,DEFAULTS)
+ apply_syntax_seed(_d if _seed else {},SYNTAX,MAP)
+ # Bespoke apps are single-sourced as BESPOKE_APP_SPECS in face_data.py (one
+ # row per app: key, label, preview, FACES, prefix, SEED).
+ APPS={key:{"label":label,"preview":preview,"faces":face_rows(faces,prefix,seed)}
+ for key,label,preview,faces,prefix,seed in BESPOKE_APP_SPECS}
+ # 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")
+ 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
+ # per-face spec (color + structure) replaces the hardcoded face seed before render.
+ apply_seed_packages(APPS,_d,_seed)
-# Splice the stylesheet and script in first, then fill the data placeholders they
-# carry. The page contains app.js exactly as fill_data(APP_BODY) renders it —
-# APP_FILLED is that rendering, the handle the inline-integrity test asserts on.
-HTML=fill_data(HTML.replace("STYLES_CSS",STYLES).replace("APP_JS",APP_BODY))
-APP_FILLED=fill_data(APP_BODY)
-OUT=os.path.join(HERE,'theme-studio.html')
+ if DEFAULTS.available:
+ add_default_palette_colors(PALETTE,MAP,SYNTAX,UIMAP,APPS,DEFAULTS)
+
+ PALETTE=normalize_palette(PALETTE)
+ HTML=read_text('theme-studio.template.html')
+ # Fill the data placeholders. str.replace is literal (no backref interpretation),
+ # so backslashes in the inlined JS survive intact — the escaping-bug class that
+ # the triple-quoted string used to cause is gone now that app.js is a real file.
+ # Caveat: these tokens are replaced everywhere they appear, including inside code
+ # comments. Don't write a placeholder name (COLORMATH_J, APP_CORE_J, ...) in
+ # prose in any inlined file, or that prose gets the body spliced into it too.
+ def fill_data(s):
+ return (s.replace("COLORMATH_J",COLORMATH_BODY)
+ .replace("APP_CORE_J",APP_CORE_BODY)
+ .replace("PREVIEWS_J",PREVIEWS_BODY)
+ .replace("APP_UTIL_J",APP_UTIL_BODY)
+ .replace("PALETTE_GENERATOR_CORE_J",PALETTE_GENERATOR_CORE_BODY)
+ .replace("PALETTE_GENERATOR_UI_J",PALETTE_GENERATOR_UI_BODY)
+ .replace("PALETTE_ACTIONS_J",PALETTE_ACTIONS_BODY)
+ .replace("BROWSER_GATES_J",BROWSER_GATES_BODY)
+ .replace("COLOR_NAMES_J",json.dumps(COLOR_NAMES))
+ .replace("FACE_DOCS_J",json.dumps(FACE_DOCS)).replace("SYNTAX_DOCS_J",json.dumps(SYNTAX_DOCS))
+ .replace("SAMPLES_J",json.dumps(SAMPLES))
+ .replace("PALETTE_J",json.dumps(PALETTE)).replace("CATS_J",json.dumps(CATS))
+ .replace("UIFACES_J",json.dumps(UI_FACES)).replace("UIMAP_J",json.dumps(UIMAP)).replace("APPS_J",json.dumps(APPS))
+ .replace("SYNTAX_J",json.dumps(SYNTAX)).replace("MAP_J",json.dumps(MAP)).replace("LOCKS_J",json.dumps(LOCKS)))
+
+ # Splice the stylesheet and script in first, then fill the data placeholders they
+ # carry. The page contains app.js exactly as fill_data(APP_BODY) renders it —
+ # APP_FILLED is that rendering, the handle the inline-integrity test asserts on.
+ HTML=fill_data(HTML.replace("STYLES_CSS",STYLES).replace("APP_JS",APP_BODY))
+ APP_FILLED=fill_data(APP_BODY)
+ _CACHE.update(DEFAULTS=DEFAULTS, MAP=MAP, BOLD=BOLD, ITALIC_MAP=ITALIC_MAP,
+ PALETTE=PALETTE, UIMAP=UIMAP, LOCKS=LOCKS, SYNTAX=SYNTAX,
+ APPS=APPS, HTML=HTML, APP_FILLED=APP_FILLED)
+ return _CACHE
+
+def __getattr__(name):
+ # PEP 562: lazily expose any built attribute (HTML, MAP, APPS, ...). Every
+ # other name is a real module global and never reaches here.
+ built = _build()
+ if name in built:
+ return built[name]
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def render_theme_studio(out_path=OUT):
with open(out_path,"w") as out:
- out.write(HTML)
+ out.write(_build()['HTML'])
print("wrote",out_path)
if __name__=='__main__':