From eaf169045a1106935dd887d8d795d0138ad2b8a5 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Tue, 9 Jun 2026 05:00:53 -0500 Subject: refactor(theme-studio): extract CSS and JS to files, inline at generate time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate.py was 1378 lines, ~1300 of them a single triple-quoted string holding the whole app. Moved the +STYLES_CSS

Untitled: theme

@@ -576,830 +496,22 @@ HTML = """theme-studio
""" -HTML=(HTML.replace("COLORMATH_J",COLORMATH_BODY) - .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("BOLD_J",json.dumps(BOLD)).replace("MAP_J",json.dumps(MAP)).replace("LOCKS_J",json.dumps(LOCKS)).replace("ITALIC_J",json.dumps({k:True for k in ITALIC}))) +APP_JS""" +# 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. +def fill_data(s): + return (s.replace("COLORMATH_J",COLORMATH_BODY) + .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("BOLD_J",json.dumps(BOLD)).replace("MAP_J",json.dumps(MAP)).replace("LOCKS_J",json.dumps(LOCKS)).replace("ITALIC_J",json.dumps({k:True for k in ITALIC}))) + +# 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 __name__=='__main__': diff --git a/scripts/theme-studio/styles.css b/scripts/theme-studio/styles.css new file mode 100644 index 00000000..72541ca0 --- /dev/null +++ b/scripts/theme-studio/styles.css @@ -0,0 +1,87 @@ + body{background:#0d0b0a;color:#cdced1;font:15px/1.55 monospace;margin:20px} + h1{font-size:22px;font-weight:normal;color:#e8bd30;margin:26px 0 10px;border-bottom:1px solid #252321;padding-bottom:6px} + h2{font-size:10pt;color:#8a9496;font-weight:normal;margin:0 0 4px} + .wrap{display:flex;flex-wrap:nowrap;overflow-x:auto;gap:14px;padding-bottom:10px} + .col{flex:0 0 auto;width:460px} + pre{background:#0d0b0a;border:1px solid #252321;border-radius:8px;padding:14px 16px;font-size:12pt;overflow:auto;white-space:pre} + table.leg{border-collapse:collapse} table.leg td{padding:4px 12px;vertical-align:middle} + table.leg th{cursor:pointer;color:#b4b1a2;text-align:left;padding:4px 12px;user-select:none;font-weight:normal} + table.leg th:hover{color:#e8bd30} + select.chip{appearance:none;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 14px monospace;width:160px;cursor:pointer} + .cdd{display:inline-flex;align-items:center;gap:7px;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 13px monospace;width:160px;cursor:pointer;box-sizing:border-box;overflow:hidden;white-space:nowrap} + .cddsw{display:inline-block;width:13px;height:13px;border-radius:3px;border:1px solid #0007;flex:none} + .cddpop{position:fixed;z-index:200;background:#161412;border:1px solid #3a3a3a;border-radius:6px;box-shadow:0 12px 34px #000c;max-height:60vh;overflow:auto;padding:4px} + .cddrow{display:flex;align-items:center;gap:9px;padding:4px 9px;cursor:pointer;color:#cdced1;font:12px monospace;border-radius:4px;white-space:nowrap} + .cddrow:hover{background:#252321} + .cddrow.sel{outline:1px solid #e8bd30;outline-offset:-1px} + .cddrow .cddnm{flex:1} + .cddrow .cddhx{opacity:.55;margin-left:10px} + .cdd.locked{cursor:default;opacity:.85;box-shadow:inset 0 0 0 2px #e8bd3088} + .lockbtn{background:none;border:none;cursor:pointer;font-size:15px;line-height:1;padding:2px 4px;opacity:.5;filter:grayscale(1)} + .lockbtn.on{opacity:1;filter:none} + .legctl{margin:0 0 8px;display:flex;gap:8px;align-items:center} + .cat{color:#b4b1a2} .ex{font-size:17px} + .sbtn{width:26px;height:24px;border:1px solid #3a3a3a;border-radius:3px;background:#eaeaea;color:#111;cursor:pointer;font-size:15px;margin-right:2px;padding:0} + .sbtn.on{background:#0d0b0a;color:#cdced1;border-color:#8a9496} + .pals{display:flex;gap:8px;flex-wrap:wrap} + .palwarn{display:none;margin-top:8px;font:10pt monospace;color:#cb6b4d} + .palwarn .pwh{font-weight:bold;margin-bottom:2px} + .palwarn .pwl{opacity:.92} + .pchip{width:128px;height:58px;border-radius:6px;border:1px solid #555;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:grab} + .pchip.drag{opacity:.4} .pchip.sel{outline:3px solid #e8bd30;outline-offset:2px} .pchip.over{outline:2px dashed #e8bd30;outline-offset:1px} .pchip input.nm{background:transparent;border:none;text-align:center;font:bold 10pt monospace;width:108px;outline:none} + .pchip .mv{position:absolute;bottom:-1px;background:none;border:none;cursor:pointer;font-size:22px;line-height:1;font-weight:bold;opacity:.5;padding:0 5px} .pchip .mv:hover{opacity:1} .pchip .mv.l{left:0} .pchip .mv.r{right:0} + .pchip .hx{font-size:10pt;opacity:.8} .pchip .rm{position:absolute;top:2px;right:5px;background:none;border:none;cursor:pointer;font-size:14px;font-weight:bold;opacity:.7} + .pchip .lock{position:absolute;top:3px;right:5px;font-size:10px;opacity:.6} + .palctl{margin-top:12px;display:flex;gap:8px;align-items:center;flex-wrap:wrap} + .palctl input[type=text]{background:#161412;border:1px solid #252321;color:#cdced1;border-radius:4px;padding:5px 8px;font:10pt monospace} + .palctl input[type=text]::placeholder{color:#b4b1a2;opacity:1} + .palctl{position:relative} + .swatch{width:128px;height:58px;border:1px solid #555;border-radius:6px;cursor:pointer;background:#888} + .picker{display:none;position:absolute;top:66px;left:0;z-index:60;background:#161412;border:1px solid #3a3a3a;border-radius:8px;padding:12px;box-shadow:0 10px 30px #000b;width:470px} + .picker .prow{display:flex;gap:10px} + .sv{position:relative;width:400px;height:320px;border-radius:4px;cursor:crosshair} + .svmask{position:absolute;inset:0;pointer-events:none;border-radius:4px} + .pmode{margin:2px 2px 8px;font:10pt monospace;color:#b4b1a2;display:flex;gap:6px;align-items:center} + .pmode button{background:#252321;color:#cdced1;border:1px solid #3a3a3a;border-radius:4px;padding:2px 9px;font:10pt monospace;cursor:pointer} + .pmode button.on{background:#e8bd30;color:#000;border-color:#e8bd30} + .pmodel{margin:8px 2px 4px;font:10pt monospace;color:#b4b1a2;display:flex;gap:6px;align-items:center} + .pmodel button{background:#252321;color:#cdced1;border:1px solid #3a3a3a;border-radius:4px;padding:2px 9px;font:10pt monospace;cursor:pointer} + .pmodel button.on{background:#67809c;color:#000;border-color:#67809c} + .oklchctl{display:none;margin:0 2px 6px;font:10pt monospace;color:#9aa3ad} + .oklchctl.show{display:block} + .oklchctl .ocrow{display:flex;align-items:center;gap:6px;margin:3px 0} + .oklchctl .ocrow label{width:12px;color:#cdced1} + .oklchctl .ocrow input[type=range]{flex:1} + .oklchctl .ocrow input[type=number]{width:62px;background:#252321;color:#cdced1;border:1px solid #3a3a3a;border-radius:3px;font:10pt monospace;padding:1px 3px} + .oklchctl .pclamp{display:none;color:#cb6b4d;margin-top:3px} + .oklchctl .pclamp.show{display:block} + .svcur{position:absolute;width:16px;height:16px;border:2px solid #fff;border-radius:50%;transform:translate(-50%,-50%);box-shadow:0 0 0 1px #0008;pointer-events:none} + .hue{position:relative;width:34px;height:320px;border-radius:4px;cursor:ns-resize;background:linear-gradient(to bottom,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)} + .huecur{position:absolute;left:-2px;right:-2px;height:4px;background:#fff;border:1px solid #0008;transform:translateY(-50%);pointer-events:none} + .pinfo{display:flex;justify-content:space-between;margin:10px 2px 4px;font:12pt monospace;color:#cdced1} + .pinfo2{display:flex;justify-content:space-between;margin:0 2px 9px;font:10pt monospace;color:#9aa3ad} + .pinfo2 span{cursor:default} + .pkchips{display:flex;flex-wrap:wrap;gap:5px} .pkchips .pc{width:28px;height:28px;border-radius:3px;border:1px solid #555;cursor:pointer} + .palctl button,.filebar button,.fbtn{background:#252321;color:#e8bd30;border:1px solid #3a3a3a;border-radius:4px;padding:6px 12px;font:10pt monospace;cursor:pointer} + #palmsg{font:10pt monospace;opacity:0;transition:opacity .35s;margin-left:6px} + #export{width:100%;height:180px;margin-top:10px;background:#0d0b0a;color:#a4ac64;border:1px solid #252321;border-radius:6px;font:10pt monospace;padding:10px} + .filebar{margin:6px 0 0;display:flex;gap:8px;align-items:center} + #pagetitle{font-size:30px;color:#cdced1;font-weight:normal;border:none;margin:4px 0 18px;padding:0} + .cols{display:flex;gap:28px;align-items:flex-start} .cols.stretch{align-items:stretch} + .pane{min-width:0} .pane.grow{flex:1} .pane.saveload{flex:0 0 auto;margin-left:auto} + .pane h1{margin-top:0} + .filebar.end{justify-content:flex-end} .langbar{margin-bottom:10px;display:flex;gap:8px;align-items:center} + .pkgbar{margin:0 0 10px;display:flex;gap:8px;align-items:center;flex-wrap:wrap} + .pkgbar button{background:#252321;color:#e8bd30;border:1px solid #3a3a3a;border-radius:4px;padding:6px 12px;font:10pt monospace;cursor:pointer} + .hstep{background:#161412;border:1px solid #252321;color:#cdced1;border-radius:4px;padding:3px 4px;font:10pt monospace;width:56px} + #pkgbody td{padding:3px 8px} + #codepre{width:100%;box-sizing:border-box} + .mock{border:1px solid #252321;border-radius:8px;overflow:hidden;font:12pt/1.7 monospace;display:flex;flex-direction:column} + .mock .mbuf{flex:1} .mock .ln{display:flex;align-items:stretch;white-space:pre} + .mock .fr{width:14px;flex:0 0 auto;border-right:1px solid #ffffff14} .mock .num{width:36px;flex:0 0 auto;text-align:right;padding-right:10px} + .mock .cd{flex:1;padding-left:8px} .mock .bar,.mock .echo{padding:4px 10px;white-space:pre} + #codepre [data-k],.mock [data-k],.mock [data-face]{cursor:pointer} + @keyframes flashcell{0%,55%{background:#e8bd3066}100%{background:transparent}} + tr.flash td{animation:flashcell 1.1s ease-out} + @keyframes flashtok{0%,55%{background:#e8bd30aa;color:#000}100%{background:transparent}} + .flashtok{animation:flashtok 1.1s ease-out;border-radius:2px} diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py index e76acdad..7a9079ac 100644 --- a/scripts/theme-studio/test_generate.py +++ b/scripts/theme-studio/test_generate.py @@ -62,6 +62,7 @@ class ColormathInlining(unittest.TestCase): class AssembledPage(unittest.TestCase): PLACEHOLDERS = [ + "STYLES_CSS", "APP_JS", "COLORMATH_J", "SAMPLES_J", "PALETTE_J", "CATS_J", "UIFACES_J", "UIMAP_J", "APPS_J", "BOLD_J", "MAP_J", ] @@ -75,6 +76,17 @@ class AssembledPage(unittest.TestCase): # checked at the point the page is built rather than after a round-trip. self.assertIn(generate.COLORMATH_BODY, generate.HTML) + def test_page_carries_the_stylesheet_verbatim(self): + # styles.css has no placeholders, so it inlines verbatim: the inlined copy + # and the source file cannot drift. + self.assertIn(generate.STYLES, generate.HTML) + + def test_page_carries_the_app_script_faithfully(self): + # app.js does carry placeholders, so the page holds it as fill_data renders + # it (APP_FILLED), not the raw file. This guards the splice: the script + # reaches the page intact, with its data placeholders correctly filled. + self.assertIn(generate.APP_FILLED, generate.HTML) + def test_page_is_a_single_script_document(self): self.assertEqual(generate.HTML.count(""), 1) -- cgit v1.2.3