Add CSSX and boot adapters to SX spec (style dictionary + browser lifecycle)
- cssx.sx: on-demand CSS style dictionary (variant splitting, atom resolution, content-addressed hashing, style merging) - boot.sx: browser boot lifecycle (script processing, mount/hydrate/update, component caching, head element hoisting) - bootstrap_js.py: platform JS for cssx (FNV-1a hash, regex, CSS injection) and boot (localStorage, cookies, DOM mounting) - Rebuilt sx-browser.js (136K) and sx-ref.js (148K) with all adapters Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -423,6 +423,65 @@ class JSEmitter:
|
||||
"format-date": "formatDate",
|
||||
"format-decimal": "formatDecimal",
|
||||
"parse-int": "parseInt_",
|
||||
# cssx.sx
|
||||
"_style-atoms": "_styleAtoms",
|
||||
"_pseudo-variants": "_pseudoVariants",
|
||||
"_responsive-breakpoints": "_responsiveBreakpoints",
|
||||
"_style-keyframes": "_styleKeyframes",
|
||||
"_arbitrary-patterns": "_arbitraryPatterns",
|
||||
"_child-selector-prefixes": "_childSelectorPrefixes",
|
||||
"_style-cache": "_styleCache",
|
||||
"_injected-styles": "_injectedStyles",
|
||||
"load-style-dict": "loadStyleDict",
|
||||
"split-variant": "splitVariant",
|
||||
"resolve-atom": "resolveAtom",
|
||||
"is-child-selector-atom?": "isChildSelectorAtom",
|
||||
"hash-style": "hashStyle",
|
||||
"resolve-style": "resolveStyle",
|
||||
"merge-style-values": "mergeStyleValues",
|
||||
"fnv1a-hash": "fnv1aHash",
|
||||
"compile-regex": "compileRegex",
|
||||
"regex-match": "regexMatch",
|
||||
"regex-replace-groups": "regexReplaceGroups",
|
||||
"make-style-value": "makeStyleValue_",
|
||||
"style-value-declarations": "styleValueDeclarations",
|
||||
"style-value-media-rules": "styleValueMediaRules",
|
||||
"style-value-pseudo-rules": "styleValuePseudoRules",
|
||||
"style-value-keyframes": "styleValueKeyframes_",
|
||||
"inject-style-value": "injectStyleValue",
|
||||
# boot.sx
|
||||
"HEAD_HOIST_SELECTOR": "HEAD_HOIST_SELECTOR",
|
||||
"hoist-head-elements-full": "hoistHeadElementsFull",
|
||||
"sx-mount": "sxMount",
|
||||
"sx-hydrate-elements": "sxHydrateElements",
|
||||
"sx-update-element": "sxUpdateElement",
|
||||
"sx-render-component": "sxRenderComponent",
|
||||
"process-sx-scripts": "processSxScripts",
|
||||
"process-component-script": "processComponentScript",
|
||||
"init-style-dict": "initStyleDict",
|
||||
"boot-init": "bootInit",
|
||||
"resolve-mount-target": "resolveMountTarget",
|
||||
"sx-render-with-env": "sxRenderWithEnv",
|
||||
"get-render-env": "getRenderEnv",
|
||||
"merge-envs": "mergeEnvs",
|
||||
"sx-load-components": "sxLoadComponents",
|
||||
"set-document-title": "setDocumentTitle",
|
||||
"remove-head-element": "removeHeadElement",
|
||||
"query-sx-scripts": "querySxScripts",
|
||||
"query-style-scripts": "queryStyleScripts",
|
||||
"local-storage-get": "localStorageGet",
|
||||
"local-storage-set": "localStorageSet",
|
||||
"local-storage-remove": "localStorageRemove",
|
||||
"set-sx-comp-cookie": "setSxCompCookie",
|
||||
"clear-sx-comp-cookie": "clearSxCompCookie",
|
||||
"set-sx-styles-cookie": "setSxStylesCookie",
|
||||
"clear-sx-styles-cookie": "clearSxStylesCookie",
|
||||
"parse-env-attr": "parseEnvAttr",
|
||||
"store-env-attr": "storeEnvAttr",
|
||||
"to-kebab": "toKebab",
|
||||
"log-info": "logInfo",
|
||||
"log-parse-error": "logParseError",
|
||||
"parse-and-load-style-dict": "parseAndLoadStyleDict",
|
||||
}
|
||||
if name in RENAMES:
|
||||
return RENAMES[name]
|
||||
@@ -710,10 +769,17 @@ ADAPTER_FILES = {
|
||||
"dom": ("adapter-dom.sx", "adapter-dom"),
|
||||
"engine": ("engine.sx", "engine"),
|
||||
"orchestration": ("orchestration.sx","orchestration"),
|
||||
"cssx": ("cssx.sx", "cssx"),
|
||||
"boot": ("boot.sx", "boot"),
|
||||
}
|
||||
|
||||
# Dependencies: orchestration requires engine+dom, engine requires dom
|
||||
ADAPTER_DEPS = {"engine": ["dom"], "orchestration": ["engine", "dom"]}
|
||||
# Dependencies
|
||||
ADAPTER_DEPS = {
|
||||
"engine": ["dom"],
|
||||
"orchestration": ["engine", "dom"],
|
||||
"cssx": [],
|
||||
"boot": ["dom", "engine", "orchestration", "cssx"],
|
||||
}
|
||||
|
||||
|
||||
def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
@@ -732,6 +798,8 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
"dom": PLATFORM_DOM_JS,
|
||||
"engine": PLATFORM_ENGINE_PURE_JS,
|
||||
"orchestration": PLATFORM_ORCHESTRATION_JS,
|
||||
"cssx": PLATFORM_CSSX_JS,
|
||||
"boot": PLATFORM_BOOT_JS,
|
||||
}
|
||||
|
||||
# Resolve adapter set
|
||||
@@ -752,7 +820,7 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
("eval.sx", "eval"),
|
||||
("render.sx", "render (core)"),
|
||||
]
|
||||
for name in ("html", "sx", "dom", "engine", "orchestration"):
|
||||
for name in ("html", "sx", "dom", "engine", "orchestration", "cssx", "boot"):
|
||||
if name in adapter_set:
|
||||
sx_files.append(ADAPTER_FILES[name])
|
||||
|
||||
@@ -772,6 +840,8 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
has_dom = "dom" in adapter_set
|
||||
has_engine = "engine" in adapter_set
|
||||
has_orch = "orchestration" in adapter_set
|
||||
has_cssx = "cssx" in adapter_set
|
||||
has_boot = "boot" in adapter_set
|
||||
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
|
||||
|
||||
parts = []
|
||||
@@ -787,12 +857,12 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
# Platform JS for selected adapters
|
||||
if not has_dom:
|
||||
parts.append("\n var _hasDom = false;\n")
|
||||
for name in ("dom", "engine", "orchestration"):
|
||||
for name in ("dom", "engine", "orchestration", "cssx", "boot"):
|
||||
if name in adapter_set and name in adapter_platform:
|
||||
parts.append(adapter_platform[name])
|
||||
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, adapter_label))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, adapter_label))
|
||||
parts.append(EPILOGUE)
|
||||
return "\n".join(parts)
|
||||
|
||||
@@ -2044,6 +2114,254 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
}
|
||||
"""
|
||||
|
||||
PLATFORM_CSSX_JS = """
|
||||
// =========================================================================
|
||||
// Platform interface — CSSX (style dictionary)
|
||||
// =========================================================================
|
||||
|
||||
function fnv1aHash(input) {
|
||||
var h = 0x811c9dc5;
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
h ^= input.charCodeAt(i);
|
||||
h = (h * 0x01000193) >>> 0;
|
||||
}
|
||||
return h.toString(16).padStart(8, "0").substring(0, 6);
|
||||
}
|
||||
|
||||
function compileRegex(pattern) {
|
||||
try { return new RegExp(pattern); } catch (e) { return null; }
|
||||
}
|
||||
|
||||
function regexMatch(re, s) {
|
||||
if (!re) return NIL;
|
||||
var m = s.match(re);
|
||||
return m ? Array.prototype.slice.call(m) : NIL;
|
||||
}
|
||||
|
||||
function regexReplaceGroups(tmpl, match) {
|
||||
var result = tmpl;
|
||||
for (var j = 1; j < match.length; j++) {
|
||||
result = result.split("{" + (j - 1) + "}").join(match[j]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeStyleValue_(cn, decls, media, pseudo, kf) {
|
||||
return new StyleValue(cn, decls || "", media || [], pseudo || [], kf || []);
|
||||
}
|
||||
|
||||
function styleValueDeclarations(sv) { return sv.declarations; }
|
||||
function styleValueMediaRules(sv) { return sv.mediaRules; }
|
||||
function styleValuePseudoRules(sv) { return sv.pseudoRules; }
|
||||
function styleValueKeyframes_(sv) { return sv.keyframes; }
|
||||
|
||||
function injectStyleValue(sv, atoms) {
|
||||
if (_injectedStyles[sv.className]) return;
|
||||
_injectedStyles[sv.className] = true;
|
||||
|
||||
if (!_hasDom) return;
|
||||
var cssTarget = document.getElementById("sx-css");
|
||||
if (!cssTarget) return;
|
||||
|
||||
var rules = [];
|
||||
if (sv.declarations) {
|
||||
var hasChild = false;
|
||||
if (atoms) {
|
||||
for (var ai = 0; ai < atoms.length; ai++) {
|
||||
if (isChildSelectorAtom(atoms[ai])) { hasChild = true; break; }
|
||||
}
|
||||
}
|
||||
if (hasChild) {
|
||||
rules.push("." + sv.className + ">:not(:first-child){" + sv.declarations + "}");
|
||||
} else {
|
||||
rules.push("." + sv.className + "{" + sv.declarations + "}");
|
||||
}
|
||||
}
|
||||
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
|
||||
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
|
||||
if (sel.indexOf("&") >= 0) {
|
||||
rules.push(sel.replace(/&/g, "." + sv.className) + "{" + decls + "}");
|
||||
} else {
|
||||
rules.push("." + sv.className + sel + "{" + decls + "}");
|
||||
}
|
||||
}
|
||||
for (var mi = 0; mi < sv.mediaRules.length; mi++) {
|
||||
rules.push("@media " + sv.mediaRules[mi][0] + "{." + sv.className + "{" + sv.mediaRules[mi][1] + "}}");
|
||||
}
|
||||
for (var ki = 0; ki < sv.keyframes.length; ki++) {
|
||||
rules.push(sv.keyframes[ki][1]);
|
||||
}
|
||||
cssTarget.textContent += rules.join("");
|
||||
}
|
||||
|
||||
// Replace stub css primitive with real CSSX implementation
|
||||
PRIMITIVES["css"] = function() {
|
||||
var atoms = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var a = arguments[i];
|
||||
if (isNil(a) || a === false) continue;
|
||||
atoms.push(isKw(a) ? a.name : String(a));
|
||||
}
|
||||
if (!atoms.length) return NIL;
|
||||
return resolveStyle(atoms);
|
||||
};
|
||||
|
||||
PRIMITIVES["merge-styles"] = function() {
|
||||
var valid = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
||||
}
|
||||
if (!valid.length) return NIL;
|
||||
if (valid.length === 1) return valid[0];
|
||||
return mergeStyleValues(valid);
|
||||
};
|
||||
"""
|
||||
|
||||
PLATFORM_BOOT_JS = """
|
||||
// =========================================================================
|
||||
// Platform interface — Boot (mount, hydrate, scripts, cookies)
|
||||
// =========================================================================
|
||||
|
||||
function resolveMountTarget(target) {
|
||||
if (typeof target === "string") return _hasDom ? document.querySelector(target) : null;
|
||||
return target;
|
||||
}
|
||||
|
||||
function sxRenderWithEnv(source, extraEnv) {
|
||||
var env = extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
|
||||
var exprs = parse(source);
|
||||
if (!_hasDom) return null;
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 0; i < exprs.length; i++) {
|
||||
var node = renderToDom(exprs[i], env, null);
|
||||
if (node) frag.appendChild(node);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
|
||||
function getRenderEnv(extraEnv) {
|
||||
return extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
|
||||
}
|
||||
|
||||
function mergeEnvs(base, newEnv) {
|
||||
return newEnv ? merge(componentEnv, base, newEnv) : merge(componentEnv, base);
|
||||
}
|
||||
|
||||
function sxLoadComponents(text) {
|
||||
try {
|
||||
var exprs = parse(text);
|
||||
for (var i = 0; i < exprs.length; i++) trampoline(evalExpr(exprs[i], componentEnv));
|
||||
} catch (err) {
|
||||
logParseError("loadComponents", text, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function setDocumentTitle(s) {
|
||||
if (_hasDom) document.title = s || "";
|
||||
}
|
||||
|
||||
function removeHeadElement(sel) {
|
||||
if (!_hasDom) return;
|
||||
var old = document.head.querySelector(sel);
|
||||
if (old) old.parentNode.removeChild(old);
|
||||
}
|
||||
|
||||
function querySxScripts(root) {
|
||||
if (!_hasDom) return [];
|
||||
return Array.prototype.slice.call(
|
||||
(root || document).querySelectorAll('script[type="text/sx"]'));
|
||||
}
|
||||
|
||||
function queryStyleScripts() {
|
||||
if (!_hasDom) return [];
|
||||
return Array.prototype.slice.call(
|
||||
document.querySelectorAll('script[type="text/sx-styles"]'));
|
||||
}
|
||||
|
||||
// --- localStorage ---
|
||||
|
||||
function localStorageGet(key) {
|
||||
try { var v = localStorage.getItem(key); return v === null ? NIL : v; }
|
||||
catch (e) { return NIL; }
|
||||
}
|
||||
|
||||
function localStorageSet(key, val) {
|
||||
try { localStorage.setItem(key, val); } catch (e) {}
|
||||
}
|
||||
|
||||
function localStorageRemove(key) {
|
||||
try { localStorage.removeItem(key); } catch (e) {}
|
||||
}
|
||||
|
||||
// --- Cookies ---
|
||||
|
||||
function setSxCompCookie(hash) {
|
||||
if (_hasDom) document.cookie = "sx-comp-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
||||
}
|
||||
|
||||
function clearSxCompCookie() {
|
||||
if (_hasDom) document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
function setSxStylesCookie(hash) {
|
||||
if (_hasDom) document.cookie = "sx-styles-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
||||
}
|
||||
|
||||
function clearSxStylesCookie() {
|
||||
if (_hasDom) document.cookie = "sx-styles-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
// --- Env helpers ---
|
||||
|
||||
function parseEnvAttr(el) {
|
||||
var attr = el && el.getAttribute ? el.getAttribute("data-sx-env") : null;
|
||||
if (!attr) return {};
|
||||
try { return JSON.parse(attr); } catch (e) { return {}; }
|
||||
}
|
||||
|
||||
function storeEnvAttr(el, base, newEnv) {
|
||||
var merged = merge(base, newEnv);
|
||||
if (el && el.setAttribute) el.setAttribute("data-sx-env", JSON.stringify(merged));
|
||||
}
|
||||
|
||||
function toKebab(s) { return s.replace(/_/g, "-"); }
|
||||
|
||||
// --- Logging ---
|
||||
|
||||
function logInfo(msg) {
|
||||
if (typeof console !== "undefined") console.log("[sx-ref] " + msg);
|
||||
}
|
||||
|
||||
function logParseError(label, text, err) {
|
||||
if (typeof console === "undefined") return;
|
||||
var msg = err && err.message ? err.message : String(err);
|
||||
var colMatch = msg.match(/col (\\d+)/);
|
||||
var lineMatch = msg.match(/line (\\d+)/);
|
||||
if (colMatch && text) {
|
||||
var errLine = lineMatch ? parseInt(lineMatch[1]) : 1;
|
||||
var errCol = parseInt(colMatch[1]);
|
||||
var lines = text.split("\\n");
|
||||
var pos = 0;
|
||||
for (var i = 0; i < errLine - 1 && i < lines.length; i++) pos += lines[i].length + 1;
|
||||
pos += errCol;
|
||||
var ws = 80;
|
||||
var start = Math.max(0, pos - ws);
|
||||
var end = Math.min(text.length, pos + ws);
|
||||
console.error("[sx-ref] " + label + ":", msg,
|
||||
"\\n around error (pos ~" + pos + "):",
|
||||
"\\n \\u00ab" + text.substring(start, pos) + "\\u26d4" + text.substring(pos, end) + "\\u00bb");
|
||||
} else {
|
||||
console.error("[sx-ref] " + label + ":", msg);
|
||||
}
|
||||
}
|
||||
|
||||
function parseAndLoadStyleDict(text) {
|
||||
try { loadStyleDict(JSON.parse(text)); }
|
||||
catch (e) { if (typeof console !== "undefined") console.warn("[sx-ref] style dict parse error", e); }
|
||||
}
|
||||
"""
|
||||
|
||||
def fixups_js(has_html, has_sx, has_dom):
|
||||
lines = ['''
|
||||
// =========================================================================
|
||||
@@ -2068,7 +2386,7 @@ def fixups_js(has_html, has_sx, has_dom):
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, adapter_label):
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, adapter_label):
|
||||
# Parser is always included
|
||||
parser = r'''
|
||||
// =========================================================================
|
||||
@@ -2268,6 +2586,15 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, adapter_label
|
||||
api_lines.append(' process: typeof processElements === "function" ? processElements : null,')
|
||||
api_lines.append(' executeRequest: typeof executeRequest === "function" ? executeRequest : null,')
|
||||
api_lines.append(' postSwap: typeof postSwap === "function" ? postSwap : null,')
|
||||
if has_boot:
|
||||
api_lines.append(' processScripts: typeof processSxScripts === "function" ? processSxScripts : null,')
|
||||
api_lines.append(' mount: typeof sxMount === "function" ? sxMount : null,')
|
||||
api_lines.append(' hydrate: typeof sxHydrateElements === "function" ? sxHydrateElements : null,')
|
||||
api_lines.append(' update: typeof sxUpdateElement === "function" ? sxUpdateElement : null,')
|
||||
api_lines.append(' renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,')
|
||||
api_lines.append(' getEnv: function() { return componentEnv; },')
|
||||
api_lines.append(' init: typeof bootInit === "function" ? bootInit : null,')
|
||||
elif has_orch:
|
||||
api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,')
|
||||
|
||||
api_lines.append(f' _version: "{version}"')
|
||||
@@ -2280,8 +2607,20 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, adapter_label
|
||||
window.addEventListener("popstate", function(e) {
|
||||
handlePopstate(e && e.state ? e.state.scrollY || 0 : 0);
|
||||
});
|
||||
}
|
||||
|
||||
}''')
|
||||
if has_boot:
|
||||
api_lines.append('''
|
||||
// --- Auto-init ---
|
||||
if (typeof document !== "undefined") {
|
||||
var _sxRefInit = function() { bootInit(); };
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", _sxRefInit);
|
||||
} else {
|
||||
_sxRefInit();
|
||||
}
|
||||
}''')
|
||||
elif has_orch:
|
||||
api_lines.append('''
|
||||
// --- Auto-init ---
|
||||
if (typeof document !== "undefined") {
|
||||
var _sxRefInit = function() { engineInit(); };
|
||||
|
||||
Reference in New Issue
Block a user