diff options
Diffstat (limited to 'scripts/theme-studio/generate.py')
| -rw-r--r-- | scripts/theme-studio/generate.py | 153 |
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__': |
