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:
@@ -1565,6 +1565,296 @@
|
|||||||
var engineInit = function() { return (initCssTracking(), sxProcessScripts(NIL), sxHydrate(NIL), processElements(NIL)); };
|
var engineInit = function() { return (initCssTracking(), sxProcessScripts(NIL), sxHydrate(NIL), processElements(NIL)); };
|
||||||
|
|
||||||
|
|
||||||
|
// === Transpiled from cssx ===
|
||||||
|
|
||||||
|
// _style-atoms
|
||||||
|
var _styleAtoms = {};
|
||||||
|
|
||||||
|
// _pseudo-variants
|
||||||
|
var _pseudoVariants = {};
|
||||||
|
|
||||||
|
// _responsive-breakpoints
|
||||||
|
var _responsiveBreakpoints = {};
|
||||||
|
|
||||||
|
// _style-keyframes
|
||||||
|
var _styleKeyframes = {};
|
||||||
|
|
||||||
|
// _arbitrary-patterns
|
||||||
|
var _arbitraryPatterns = [];
|
||||||
|
|
||||||
|
// _child-selector-prefixes
|
||||||
|
var _childSelectorPrefixes = [];
|
||||||
|
|
||||||
|
// _style-cache
|
||||||
|
var _styleCache = {};
|
||||||
|
|
||||||
|
// _injected-styles
|
||||||
|
var _injectedStyles = {};
|
||||||
|
|
||||||
|
// load-style-dict
|
||||||
|
var loadStyleDict = function(data) { return (_styleAtoms = sxOr(get(data, "a"), {})); };
|
||||||
|
|
||||||
|
// split-variant
|
||||||
|
var splitVariant = function(atom) { return (function() {
|
||||||
|
var result = NIL;
|
||||||
|
{ var _c = keys(_responsiveBreakpoints); for (var _i = 0; _i < _c.length; _i++) { var bp = _c[_i]; if (isSxTruthy(isNil(result))) {
|
||||||
|
(function() {
|
||||||
|
var prefix = (String(bp) + String(":"));
|
||||||
|
return (isSxTruthy(startsWith(atom, prefix)) ? (function() {
|
||||||
|
var restAtom = slice(atom, len(prefix));
|
||||||
|
return (function() {
|
||||||
|
var innerMatch = NIL;
|
||||||
|
{ var _c = keys(_pseudoVariants); for (var _i = 0; _i < _c.length; _i++) { var pv = _c[_i]; if (isSxTruthy(isNil(innerMatch))) {
|
||||||
|
(function() {
|
||||||
|
var innerPrefix = (String(pv) + String(":"));
|
||||||
|
return (isSxTruthy(startsWith(restAtom, innerPrefix)) ? (innerMatch = [(String(bp) + String(":") + String(pv)), slice(restAtom, len(innerPrefix))]) : NIL);
|
||||||
|
})();
|
||||||
|
} } }
|
||||||
|
return (result = sxOr(innerMatch, [bp, restAtom]));
|
||||||
|
})();
|
||||||
|
})() : NIL);
|
||||||
|
})();
|
||||||
|
} } }
|
||||||
|
if (isSxTruthy(isNil(result))) {
|
||||||
|
{ var _c = keys(_pseudoVariants); for (var _i = 0; _i < _c.length; _i++) { var pv = _c[_i]; if (isSxTruthy(isNil(result))) {
|
||||||
|
(function() {
|
||||||
|
var prefix = (String(pv) + String(":"));
|
||||||
|
return (isSxTruthy(startsWith(atom, prefix)) ? (result = [pv, slice(atom, len(prefix))]) : NIL);
|
||||||
|
})();
|
||||||
|
} } }
|
||||||
|
}
|
||||||
|
return sxOr(result, [NIL, atom]);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// resolve-atom
|
||||||
|
var resolveAtom = function(atom) { return (function() {
|
||||||
|
var decls = dictGet(_styleAtoms, atom);
|
||||||
|
return (isSxTruthy(!isNil(decls)) ? decls : (isSxTruthy(startsWith(atom, "animate-")) ? (function() {
|
||||||
|
var kfName = slice(atom, 8);
|
||||||
|
return (isSxTruthy(dictHas(_styleKeyframes, kfName)) ? (String("animation-name:") + String(kfName)) : NIL);
|
||||||
|
})() : (function() {
|
||||||
|
var matchResult = NIL;
|
||||||
|
{ var _c = _arbitraryPatterns; for (var _i = 0; _i < _c.length; _i++) { var pat = _c[_i]; if (isSxTruthy(isNil(matchResult))) {
|
||||||
|
(function() {
|
||||||
|
var m = regexMatch(get(pat, "re"), atom);
|
||||||
|
return (isSxTruthy(m) ? (matchResult = regexReplaceGroups(get(pat, "tmpl"), m)) : NIL);
|
||||||
|
})();
|
||||||
|
} } }
|
||||||
|
return matchResult;
|
||||||
|
})()));
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// is-child-selector-atom?
|
||||||
|
var isChildSelectorAtom = function(atom) { return some(function(prefix) { return startsWith(atom, prefix); }, _childSelectorPrefixes); };
|
||||||
|
|
||||||
|
// hash-style
|
||||||
|
var hashStyle = function(input) { return fnv1aHash(input); };
|
||||||
|
|
||||||
|
// resolve-style
|
||||||
|
var resolveStyle = function(atoms) { return (function() {
|
||||||
|
var key = join("\\0", atoms);
|
||||||
|
return (function() {
|
||||||
|
var cached = dictGet(_styleCache, key);
|
||||||
|
return (isSxTruthy(!isNil(cached)) ? cached : (function() {
|
||||||
|
var baseDecls = [];
|
||||||
|
var mediaRules = [];
|
||||||
|
var pseudoRules = [];
|
||||||
|
var kfNeeded = [];
|
||||||
|
{ var _c = atoms; for (var _i = 0; _i < _c.length; _i++) { var a = _c[_i]; if (isSxTruthy(a)) {
|
||||||
|
(function() {
|
||||||
|
var clean = (isSxTruthy(startsWith(a, ":")) ? slice(a, 1) : a);
|
||||||
|
return (function() {
|
||||||
|
var parts = splitVariant(clean);
|
||||||
|
return (function() {
|
||||||
|
var variant = first(parts);
|
||||||
|
var base = nth(parts, 1);
|
||||||
|
var decls = resolveAtom(base);
|
||||||
|
return (isSxTruthy(decls) ? ((isSxTruthy(startsWith(base, "animate-")) ? (function() {
|
||||||
|
var kfName = slice(base, 8);
|
||||||
|
return (isSxTruthy(dictHas(_styleKeyframes, kfName)) ? append_b(kfNeeded, [kfName, dictGet(_styleKeyframes, kfName)]) : NIL);
|
||||||
|
})() : NIL), (isSxTruthy(isNil(variant)) ? append_b(baseDecls, decls) : (isSxTruthy(dictHas(_responsiveBreakpoints, variant)) ? append_b(mediaRules, [dictGet(_responsiveBreakpoints, variant), decls]) : (isSxTruthy(dictHas(_pseudoVariants, variant)) ? append_b(pseudoRules, [dictGet(_pseudoVariants, variant), decls]) : (function() {
|
||||||
|
var vparts = split(variant, ":");
|
||||||
|
var mediaPart = NIL;
|
||||||
|
var pseudoPart = NIL;
|
||||||
|
{ var _c = vparts; for (var _i = 0; _i < _c.length; _i++) { var vp = _c[_i]; (isSxTruthy(dictHas(_responsiveBreakpoints, vp)) ? (mediaPart = dictGet(_responsiveBreakpoints, vp)) : (isSxTruthy(dictHas(_pseudoVariants, vp)) ? (pseudoPart = dictGet(_pseudoVariants, vp)) : NIL)); } }
|
||||||
|
if (isSxTruthy(mediaPart)) {
|
||||||
|
mediaRules.push([mediaPart, decls]);
|
||||||
|
}
|
||||||
|
if (isSxTruthy(pseudoPart)) {
|
||||||
|
pseudoRules.push([pseudoPart, decls]);
|
||||||
|
}
|
||||||
|
return (isSxTruthy((isSxTruthy(isNil(mediaPart)) && isNil(pseudoPart))) ? append_b(baseDecls, decls) : NIL);
|
||||||
|
})())))) : NIL);
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
} } }
|
||||||
|
return (function() {
|
||||||
|
var hashInput = join(";", baseDecls);
|
||||||
|
{ var _c = chunkEvery(mediaRules, 2); for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
|
||||||
|
{ var _c = chunkEvery(pseudoRules, 2); for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
|
||||||
|
{ var _c = chunkEvery(kfNeeded, 2); for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
|
||||||
|
return (function() {
|
||||||
|
var cn = (String("sx-") + String(hashStyle(hashInput)));
|
||||||
|
var sv = makeStyleValue_(cn, join(";", baseDecls), chunkEvery(mediaRules, 2), chunkEvery(pseudoRules, 2), chunkEvery(kfNeeded, 2));
|
||||||
|
_styleCache[key] = sv;
|
||||||
|
injectStyleValue(sv, atoms);
|
||||||
|
return sv;
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
})());
|
||||||
|
})();
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// merge-style-values
|
||||||
|
var mergeStyleValues = function(styles) { return (isSxTruthy((len(styles) == 1)) ? first(styles) : (function() {
|
||||||
|
var allDecls = [];
|
||||||
|
var allMedia = [];
|
||||||
|
var allPseudo = [];
|
||||||
|
var allKf = [];
|
||||||
|
{ var _c = styles; for (var _i = 0; _i < _c.length; _i++) { var sv = _c[_i]; if (isSxTruthy(styleValueDeclarations(sv))) {
|
||||||
|
allDecls.push(styleValueDeclarations(sv));
|
||||||
|
} } }
|
||||||
|
return (function() {
|
||||||
|
var hashInput = join(";", allDecls);
|
||||||
|
{ var _c = allMedia; for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
|
||||||
|
{ var _c = allPseudo; for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
|
||||||
|
{ var _c = allKf; for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
|
||||||
|
return (function() {
|
||||||
|
var cn = (String("sx-") + String(hashStyle(hashInput)));
|
||||||
|
var merged = makeStyleValue_(cn, join(";", allDecls), allMedia, allPseudo, allKf);
|
||||||
|
injectStyleValue(merged, []);
|
||||||
|
return merged;
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
})()); };
|
||||||
|
|
||||||
|
|
||||||
|
// === Transpiled from boot ===
|
||||||
|
|
||||||
|
// HEAD_HOIST_SELECTOR
|
||||||
|
var HEAD_HOIST_SELECTOR = "meta, title, link[rel='canonical'], script[type='application/ld+json']";
|
||||||
|
|
||||||
|
// hoist-head-elements-full
|
||||||
|
var hoistHeadElementsFull = function(root) { return (function() {
|
||||||
|
var els = domQueryAll(root, HEAD_HOIST_SELECTOR);
|
||||||
|
return forEach(function(el) { return (function() {
|
||||||
|
var tag = lower(domTagName(el));
|
||||||
|
return (isSxTruthy((tag == "title")) ? (setDocumentTitle(domTextContent(el)), domRemoveChild(domParent(el), el)) : (isSxTruthy((tag == "meta")) ? ((function() {
|
||||||
|
var name = domGetAttr(el, "name");
|
||||||
|
var prop = domGetAttr(el, "property");
|
||||||
|
if (isSxTruthy(name)) {
|
||||||
|
removeHeadElement((String("meta[name=\"") + String(name) + String("\"]")));
|
||||||
|
}
|
||||||
|
return (isSxTruthy(prop) ? removeHeadElement((String("meta[property=\"") + String(prop) + String("\"]"))) : NIL);
|
||||||
|
})(), domRemoveChild(domParent(el), el), domAppendToHead(el)) : (isSxTruthy((isSxTruthy((tag == "link")) && (domGetAttr(el, "rel") == "canonical"))) ? (removeHeadElement("link[rel=\"canonical\"]"), domRemoveChild(domParent(el), el), domAppendToHead(el)) : (domRemoveChild(domParent(el), el), domAppendToHead(el)))));
|
||||||
|
})(); }, els);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// sx-mount
|
||||||
|
var sxMount = function(target, source, extraEnv) { return (function() {
|
||||||
|
var el = resolveMountTarget(target);
|
||||||
|
return (isSxTruthy(el) ? (function() {
|
||||||
|
var node = sxRenderWithEnv(source, extraEnv);
|
||||||
|
domSetTextContent(el, "");
|
||||||
|
domAppend(el, node);
|
||||||
|
hoistHeadElementsFull(el);
|
||||||
|
processElements(el);
|
||||||
|
return sxHydrateElements(el);
|
||||||
|
})() : NIL);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// sx-hydrate-elements
|
||||||
|
var sxHydrateElements = function(root) { return (function() {
|
||||||
|
var els = domQueryAll(sxOr(root, domBody()), "[data-sx]");
|
||||||
|
return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "hydrated")) ? (markProcessed(el, "hydrated"), sxUpdateElement(el, NIL)) : NIL); }, els);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// sx-update-element
|
||||||
|
var sxUpdateElement = function(el, newEnv) { return (function() {
|
||||||
|
var target = resolveMountTarget(el);
|
||||||
|
return (isSxTruthy(target) ? (function() {
|
||||||
|
var source = domGetAttr(target, "data-sx");
|
||||||
|
return (isSxTruthy(source) ? (function() {
|
||||||
|
var baseEnv = parseEnvAttr(target);
|
||||||
|
var env = mergeEnvs(baseEnv, newEnv);
|
||||||
|
return (function() {
|
||||||
|
var node = sxRenderWithEnv(source, env);
|
||||||
|
domSetTextContent(target, "");
|
||||||
|
domAppend(target, node);
|
||||||
|
return (isSxTruthy(newEnv) ? storeEnvAttr(target, baseEnv, newEnv) : NIL);
|
||||||
|
})();
|
||||||
|
})() : NIL);
|
||||||
|
})() : NIL);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// sx-render-component
|
||||||
|
var sxRenderComponent = function(name, kwargs, extraEnv) { return (function() {
|
||||||
|
var fullName = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||||
|
return (function() {
|
||||||
|
var env = getRenderEnv(extraEnv);
|
||||||
|
var comp = envGet(env, fullName);
|
||||||
|
return (isSxTruthy(!isComponent(comp)) ? error((String("Unknown component: ") + String(fullName))) : (function() {
|
||||||
|
var callExpr = [makeSymbol(fullName)];
|
||||||
|
{ var _c = keys(kwargs); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; callExpr.push(makeKeyword(toKebab(k))); } }
|
||||||
|
return renderToDom(callExpr, env, NIL);
|
||||||
|
})());
|
||||||
|
})();
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// process-sx-scripts
|
||||||
|
var processSxScripts = function(root) { return (function() {
|
||||||
|
var scripts = querySxScripts(root);
|
||||||
|
return forEach(function(s) { return (isSxTruthy(!isProcessed(s, "script")) ? (markProcessed(s, "script"), (function() {
|
||||||
|
var text = domTextContent(s);
|
||||||
|
return (isSxTruthy(domHasAttr(s, "data-components")) ? processComponentScript(s, text) : (isSxTruthy(sxOr(isNil(text), isEmpty(trim(text)))) ? NIL : (isSxTruthy(domHasAttr(s, "data-mount")) ? (function() {
|
||||||
|
var mountSel = domGetAttr(s, "data-mount");
|
||||||
|
var target = domQuery(mountSel);
|
||||||
|
return (isSxTruthy(target) ? sxMount(target, text, NIL) : NIL);
|
||||||
|
})() : sxLoadComponents(text))));
|
||||||
|
})()) : NIL); }, scripts);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// process-component-script
|
||||||
|
var processComponentScript = function(script, text) { return (function() {
|
||||||
|
var hash = domGetAttr(script, "data-hash");
|
||||||
|
return (isSxTruthy(isNil(hash)) ? (isSxTruthy((isSxTruthy(text) && !isEmpty(trim(text)))) ? sxLoadComponents(text) : NIL) : (function() {
|
||||||
|
var hasInline = (isSxTruthy(text) && !isEmpty(trim(text)));
|
||||||
|
(function() {
|
||||||
|
var cachedHash = localStorageGet("sx-components-hash");
|
||||||
|
return (isSxTruthy((cachedHash == hash)) ? (isSxTruthy(hasInline) ? (localStorageSet("sx-components-hash", hash), localStorageSet("sx-components-src", text), sxLoadComponents(text), logInfo("components: downloaded (cookie stale)")) : (function() {
|
||||||
|
var cached = localStorageGet("sx-components-src");
|
||||||
|
return (isSxTruthy(cached) ? (sxLoadComponents(cached), logInfo((String("components: cached (") + String(hash) + String(")")))) : (clearSxCompCookie(), browserReload()));
|
||||||
|
})()) : (isSxTruthy(hasInline) ? (localStorageSet("sx-components-hash", hash), localStorageSet("sx-components-src", text), sxLoadComponents(text), logInfo((String("components: downloaded (") + String(hash) + String(")")))) : (localStorageRemove("sx-components-hash"), localStorageRemove("sx-components-src"), clearSxCompCookie(), browserReload())));
|
||||||
|
})();
|
||||||
|
return setSxCompCookie(hash);
|
||||||
|
})());
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// init-style-dict
|
||||||
|
var initStyleDict = function() { return (function() {
|
||||||
|
var scripts = queryStyleScripts();
|
||||||
|
return forEach(function(s) { return (isSxTruthy(!isProcessed(s, "styles")) ? (markProcessed(s, "styles"), (function() {
|
||||||
|
var text = domTextContent(s);
|
||||||
|
var hash = domGetAttr(s, "data-hash");
|
||||||
|
return (isSxTruthy(isNil(hash)) ? (isSxTruthy((isSxTruthy(text) && !isEmpty(trim(text)))) ? parseAndLoadStyleDict(text) : NIL) : (function() {
|
||||||
|
var hasInline = (isSxTruthy(text) && !isEmpty(trim(text)));
|
||||||
|
(function() {
|
||||||
|
var cachedHash = localStorageGet("sx-styles-hash");
|
||||||
|
return (isSxTruthy((cachedHash == hash)) ? (isSxTruthy(hasInline) ? (localStorageSet("sx-styles-src", text), parseAndLoadStyleDict(text), logInfo("styles: downloaded (cookie stale)")) : (function() {
|
||||||
|
var cached = localStorageGet("sx-styles-src");
|
||||||
|
return (isSxTruthy(cached) ? (parseAndLoadStyleDict(cached), logInfo((String("styles: cached (") + String(hash) + String(")")))) : (clearSxStylesCookie(), browserReload()));
|
||||||
|
})()) : (isSxTruthy(hasInline) ? (localStorageSet("sx-styles-hash", hash), localStorageSet("sx-styles-src", text), parseAndLoadStyleDict(text), logInfo((String("styles: downloaded (") + String(hash) + String(")")))) : (localStorageRemove("sx-styles-hash"), localStorageRemove("sx-styles-src"), clearSxStylesCookie(), browserReload())));
|
||||||
|
})();
|
||||||
|
return setSxStylesCookie(hash);
|
||||||
|
})());
|
||||||
|
})()) : NIL); }, scripts);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// boot-init
|
||||||
|
var bootInit = function() { return (initCssTracking(), initStyleDict(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||||
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Platform interface — DOM adapter (browser-only)
|
// Platform interface — DOM adapter (browser-only)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -2317,6 +2607,252 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 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 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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Post-transpilation fixups
|
// Post-transpilation fixups
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -2472,8 +3008,14 @@
|
|||||||
process: typeof processElements === "function" ? processElements : null,
|
process: typeof processElements === "function" ? processElements : null,
|
||||||
executeRequest: typeof executeRequest === "function" ? executeRequest : null,
|
executeRequest: typeof executeRequest === "function" ? executeRequest : null,
|
||||||
postSwap: typeof postSwap === "function" ? postSwap : null,
|
postSwap: typeof postSwap === "function" ? postSwap : null,
|
||||||
init: typeof engineInit === "function" ? engineInit : null,
|
processScripts: typeof processSxScripts === "function" ? processSxScripts : null,
|
||||||
_version: "ref-2.0 (dom+engine+orchestration, bootstrap-compiled)"
|
mount: typeof sxMount === "function" ? sxMount : null,
|
||||||
|
hydrate: typeof sxHydrateElements === "function" ? sxHydrateElements : null,
|
||||||
|
update: typeof sxUpdateElement === "function" ? sxUpdateElement : null,
|
||||||
|
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
|
||||||
|
getEnv: function() { return componentEnv; },
|
||||||
|
init: typeof bootInit === "function" ? bootInit : null,
|
||||||
|
_version: "ref-2.0 (boot+cssx+dom+engine+orchestration, bootstrap-compiled)"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -2486,7 +3028,7 @@
|
|||||||
|
|
||||||
// --- Auto-init ---
|
// --- Auto-init ---
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
var _sxRefInit = function() { engineInit(); };
|
var _sxRefInit = function() { bootInit(); };
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", _sxRefInit);
|
document.addEventListener("DOMContentLoaded", _sxRefInit);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1713,6 +1713,296 @@
|
|||||||
var engineInit = function() { return (initCssTracking(), sxProcessScripts(NIL), sxHydrate(NIL), processElements(NIL)); };
|
var engineInit = function() { return (initCssTracking(), sxProcessScripts(NIL), sxHydrate(NIL), processElements(NIL)); };
|
||||||
|
|
||||||
|
|
||||||
|
// === Transpiled from cssx ===
|
||||||
|
|
||||||
|
// _style-atoms
|
||||||
|
var _styleAtoms = {};
|
||||||
|
|
||||||
|
// _pseudo-variants
|
||||||
|
var _pseudoVariants = {};
|
||||||
|
|
||||||
|
// _responsive-breakpoints
|
||||||
|
var _responsiveBreakpoints = {};
|
||||||
|
|
||||||
|
// _style-keyframes
|
||||||
|
var _styleKeyframes = {};
|
||||||
|
|
||||||
|
// _arbitrary-patterns
|
||||||
|
var _arbitraryPatterns = [];
|
||||||
|
|
||||||
|
// _child-selector-prefixes
|
||||||
|
var _childSelectorPrefixes = [];
|
||||||
|
|
||||||
|
// _style-cache
|
||||||
|
var _styleCache = {};
|
||||||
|
|
||||||
|
// _injected-styles
|
||||||
|
var _injectedStyles = {};
|
||||||
|
|
||||||
|
// load-style-dict
|
||||||
|
var loadStyleDict = function(data) { return (_styleAtoms = sxOr(get(data, "a"), {})); };
|
||||||
|
|
||||||
|
// split-variant
|
||||||
|
var splitVariant = function(atom) { return (function() {
|
||||||
|
var result = NIL;
|
||||||
|
{ var _c = keys(_responsiveBreakpoints); for (var _i = 0; _i < _c.length; _i++) { var bp = _c[_i]; if (isSxTruthy(isNil(result))) {
|
||||||
|
(function() {
|
||||||
|
var prefix = (String(bp) + String(":"));
|
||||||
|
return (isSxTruthy(startsWith(atom, prefix)) ? (function() {
|
||||||
|
var restAtom = slice(atom, len(prefix));
|
||||||
|
return (function() {
|
||||||
|
var innerMatch = NIL;
|
||||||
|
{ var _c = keys(_pseudoVariants); for (var _i = 0; _i < _c.length; _i++) { var pv = _c[_i]; if (isSxTruthy(isNil(innerMatch))) {
|
||||||
|
(function() {
|
||||||
|
var innerPrefix = (String(pv) + String(":"));
|
||||||
|
return (isSxTruthy(startsWith(restAtom, innerPrefix)) ? (innerMatch = [(String(bp) + String(":") + String(pv)), slice(restAtom, len(innerPrefix))]) : NIL);
|
||||||
|
})();
|
||||||
|
} } }
|
||||||
|
return (result = sxOr(innerMatch, [bp, restAtom]));
|
||||||
|
})();
|
||||||
|
})() : NIL);
|
||||||
|
})();
|
||||||
|
} } }
|
||||||
|
if (isSxTruthy(isNil(result))) {
|
||||||
|
{ var _c = keys(_pseudoVariants); for (var _i = 0; _i < _c.length; _i++) { var pv = _c[_i]; if (isSxTruthy(isNil(result))) {
|
||||||
|
(function() {
|
||||||
|
var prefix = (String(pv) + String(":"));
|
||||||
|
return (isSxTruthy(startsWith(atom, prefix)) ? (result = [pv, slice(atom, len(prefix))]) : NIL);
|
||||||
|
})();
|
||||||
|
} } }
|
||||||
|
}
|
||||||
|
return sxOr(result, [NIL, atom]);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// resolve-atom
|
||||||
|
var resolveAtom = function(atom) { return (function() {
|
||||||
|
var decls = dictGet(_styleAtoms, atom);
|
||||||
|
return (isSxTruthy(!isNil(decls)) ? decls : (isSxTruthy(startsWith(atom, "animate-")) ? (function() {
|
||||||
|
var kfName = slice(atom, 8);
|
||||||
|
return (isSxTruthy(dictHas(_styleKeyframes, kfName)) ? (String("animation-name:") + String(kfName)) : NIL);
|
||||||
|
})() : (function() {
|
||||||
|
var matchResult = NIL;
|
||||||
|
{ var _c = _arbitraryPatterns; for (var _i = 0; _i < _c.length; _i++) { var pat = _c[_i]; if (isSxTruthy(isNil(matchResult))) {
|
||||||
|
(function() {
|
||||||
|
var m = regexMatch(get(pat, "re"), atom);
|
||||||
|
return (isSxTruthy(m) ? (matchResult = regexReplaceGroups(get(pat, "tmpl"), m)) : NIL);
|
||||||
|
})();
|
||||||
|
} } }
|
||||||
|
return matchResult;
|
||||||
|
})()));
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// is-child-selector-atom?
|
||||||
|
var isChildSelectorAtom = function(atom) { return some(function(prefix) { return startsWith(atom, prefix); }, _childSelectorPrefixes); };
|
||||||
|
|
||||||
|
// hash-style
|
||||||
|
var hashStyle = function(input) { return fnv1aHash(input); };
|
||||||
|
|
||||||
|
// resolve-style
|
||||||
|
var resolveStyle = function(atoms) { return (function() {
|
||||||
|
var key = join("\\0", atoms);
|
||||||
|
return (function() {
|
||||||
|
var cached = dictGet(_styleCache, key);
|
||||||
|
return (isSxTruthy(!isNil(cached)) ? cached : (function() {
|
||||||
|
var baseDecls = [];
|
||||||
|
var mediaRules = [];
|
||||||
|
var pseudoRules = [];
|
||||||
|
var kfNeeded = [];
|
||||||
|
{ var _c = atoms; for (var _i = 0; _i < _c.length; _i++) { var a = _c[_i]; if (isSxTruthy(a)) {
|
||||||
|
(function() {
|
||||||
|
var clean = (isSxTruthy(startsWith(a, ":")) ? slice(a, 1) : a);
|
||||||
|
return (function() {
|
||||||
|
var parts = splitVariant(clean);
|
||||||
|
return (function() {
|
||||||
|
var variant = first(parts);
|
||||||
|
var base = nth(parts, 1);
|
||||||
|
var decls = resolveAtom(base);
|
||||||
|
return (isSxTruthy(decls) ? ((isSxTruthy(startsWith(base, "animate-")) ? (function() {
|
||||||
|
var kfName = slice(base, 8);
|
||||||
|
return (isSxTruthy(dictHas(_styleKeyframes, kfName)) ? append_b(kfNeeded, [kfName, dictGet(_styleKeyframes, kfName)]) : NIL);
|
||||||
|
})() : NIL), (isSxTruthy(isNil(variant)) ? append_b(baseDecls, decls) : (isSxTruthy(dictHas(_responsiveBreakpoints, variant)) ? append_b(mediaRules, [dictGet(_responsiveBreakpoints, variant), decls]) : (isSxTruthy(dictHas(_pseudoVariants, variant)) ? append_b(pseudoRules, [dictGet(_pseudoVariants, variant), decls]) : (function() {
|
||||||
|
var vparts = split(variant, ":");
|
||||||
|
var mediaPart = NIL;
|
||||||
|
var pseudoPart = NIL;
|
||||||
|
{ var _c = vparts; for (var _i = 0; _i < _c.length; _i++) { var vp = _c[_i]; (isSxTruthy(dictHas(_responsiveBreakpoints, vp)) ? (mediaPart = dictGet(_responsiveBreakpoints, vp)) : (isSxTruthy(dictHas(_pseudoVariants, vp)) ? (pseudoPart = dictGet(_pseudoVariants, vp)) : NIL)); } }
|
||||||
|
if (isSxTruthy(mediaPart)) {
|
||||||
|
mediaRules.push([mediaPart, decls]);
|
||||||
|
}
|
||||||
|
if (isSxTruthy(pseudoPart)) {
|
||||||
|
pseudoRules.push([pseudoPart, decls]);
|
||||||
|
}
|
||||||
|
return (isSxTruthy((isSxTruthy(isNil(mediaPart)) && isNil(pseudoPart))) ? append_b(baseDecls, decls) : NIL);
|
||||||
|
})())))) : NIL);
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
} } }
|
||||||
|
return (function() {
|
||||||
|
var hashInput = join(";", baseDecls);
|
||||||
|
{ var _c = chunkEvery(mediaRules, 2); for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
|
||||||
|
{ var _c = chunkEvery(pseudoRules, 2); for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
|
||||||
|
{ var _c = chunkEvery(kfNeeded, 2); for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
|
||||||
|
return (function() {
|
||||||
|
var cn = (String("sx-") + String(hashStyle(hashInput)));
|
||||||
|
var sv = makeStyleValue_(cn, join(";", baseDecls), chunkEvery(mediaRules, 2), chunkEvery(pseudoRules, 2), chunkEvery(kfNeeded, 2));
|
||||||
|
_styleCache[key] = sv;
|
||||||
|
injectStyleValue(sv, atoms);
|
||||||
|
return sv;
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
})());
|
||||||
|
})();
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// merge-style-values
|
||||||
|
var mergeStyleValues = function(styles) { return (isSxTruthy((len(styles) == 1)) ? first(styles) : (function() {
|
||||||
|
var allDecls = [];
|
||||||
|
var allMedia = [];
|
||||||
|
var allPseudo = [];
|
||||||
|
var allKf = [];
|
||||||
|
{ var _c = styles; for (var _i = 0; _i < _c.length; _i++) { var sv = _c[_i]; if (isSxTruthy(styleValueDeclarations(sv))) {
|
||||||
|
allDecls.push(styleValueDeclarations(sv));
|
||||||
|
} } }
|
||||||
|
return (function() {
|
||||||
|
var hashInput = join(";", allDecls);
|
||||||
|
{ var _c = allMedia; for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
|
||||||
|
{ var _c = allPseudo; for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
|
||||||
|
{ var _c = allKf; for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
|
||||||
|
return (function() {
|
||||||
|
var cn = (String("sx-") + String(hashStyle(hashInput)));
|
||||||
|
var merged = makeStyleValue_(cn, join(";", allDecls), allMedia, allPseudo, allKf);
|
||||||
|
injectStyleValue(merged, []);
|
||||||
|
return merged;
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
})()); };
|
||||||
|
|
||||||
|
|
||||||
|
// === Transpiled from boot ===
|
||||||
|
|
||||||
|
// HEAD_HOIST_SELECTOR
|
||||||
|
var HEAD_HOIST_SELECTOR = "meta, title, link[rel='canonical'], script[type='application/ld+json']";
|
||||||
|
|
||||||
|
// hoist-head-elements-full
|
||||||
|
var hoistHeadElementsFull = function(root) { return (function() {
|
||||||
|
var els = domQueryAll(root, HEAD_HOIST_SELECTOR);
|
||||||
|
return forEach(function(el) { return (function() {
|
||||||
|
var tag = lower(domTagName(el));
|
||||||
|
return (isSxTruthy((tag == "title")) ? (setDocumentTitle(domTextContent(el)), domRemoveChild(domParent(el), el)) : (isSxTruthy((tag == "meta")) ? ((function() {
|
||||||
|
var name = domGetAttr(el, "name");
|
||||||
|
var prop = domGetAttr(el, "property");
|
||||||
|
if (isSxTruthy(name)) {
|
||||||
|
removeHeadElement((String("meta[name=\"") + String(name) + String("\"]")));
|
||||||
|
}
|
||||||
|
return (isSxTruthy(prop) ? removeHeadElement((String("meta[property=\"") + String(prop) + String("\"]"))) : NIL);
|
||||||
|
})(), domRemoveChild(domParent(el), el), domAppendToHead(el)) : (isSxTruthy((isSxTruthy((tag == "link")) && (domGetAttr(el, "rel") == "canonical"))) ? (removeHeadElement("link[rel=\"canonical\"]"), domRemoveChild(domParent(el), el), domAppendToHead(el)) : (domRemoveChild(domParent(el), el), domAppendToHead(el)))));
|
||||||
|
})(); }, els);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// sx-mount
|
||||||
|
var sxMount = function(target, source, extraEnv) { return (function() {
|
||||||
|
var el = resolveMountTarget(target);
|
||||||
|
return (isSxTruthy(el) ? (function() {
|
||||||
|
var node = sxRenderWithEnv(source, extraEnv);
|
||||||
|
domSetTextContent(el, "");
|
||||||
|
domAppend(el, node);
|
||||||
|
hoistHeadElementsFull(el);
|
||||||
|
processElements(el);
|
||||||
|
return sxHydrateElements(el);
|
||||||
|
})() : NIL);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// sx-hydrate-elements
|
||||||
|
var sxHydrateElements = function(root) { return (function() {
|
||||||
|
var els = domQueryAll(sxOr(root, domBody()), "[data-sx]");
|
||||||
|
return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "hydrated")) ? (markProcessed(el, "hydrated"), sxUpdateElement(el, NIL)) : NIL); }, els);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// sx-update-element
|
||||||
|
var sxUpdateElement = function(el, newEnv) { return (function() {
|
||||||
|
var target = resolveMountTarget(el);
|
||||||
|
return (isSxTruthy(target) ? (function() {
|
||||||
|
var source = domGetAttr(target, "data-sx");
|
||||||
|
return (isSxTruthy(source) ? (function() {
|
||||||
|
var baseEnv = parseEnvAttr(target);
|
||||||
|
var env = mergeEnvs(baseEnv, newEnv);
|
||||||
|
return (function() {
|
||||||
|
var node = sxRenderWithEnv(source, env);
|
||||||
|
domSetTextContent(target, "");
|
||||||
|
domAppend(target, node);
|
||||||
|
return (isSxTruthy(newEnv) ? storeEnvAttr(target, baseEnv, newEnv) : NIL);
|
||||||
|
})();
|
||||||
|
})() : NIL);
|
||||||
|
})() : NIL);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// sx-render-component
|
||||||
|
var sxRenderComponent = function(name, kwargs, extraEnv) { return (function() {
|
||||||
|
var fullName = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||||
|
return (function() {
|
||||||
|
var env = getRenderEnv(extraEnv);
|
||||||
|
var comp = envGet(env, fullName);
|
||||||
|
return (isSxTruthy(!isComponent(comp)) ? error((String("Unknown component: ") + String(fullName))) : (function() {
|
||||||
|
var callExpr = [makeSymbol(fullName)];
|
||||||
|
{ var _c = keys(kwargs); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; callExpr.push(makeKeyword(toKebab(k))); } }
|
||||||
|
return renderToDom(callExpr, env, NIL);
|
||||||
|
})());
|
||||||
|
})();
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// process-sx-scripts
|
||||||
|
var processSxScripts = function(root) { return (function() {
|
||||||
|
var scripts = querySxScripts(root);
|
||||||
|
return forEach(function(s) { return (isSxTruthy(!isProcessed(s, "script")) ? (markProcessed(s, "script"), (function() {
|
||||||
|
var text = domTextContent(s);
|
||||||
|
return (isSxTruthy(domHasAttr(s, "data-components")) ? processComponentScript(s, text) : (isSxTruthy(sxOr(isNil(text), isEmpty(trim(text)))) ? NIL : (isSxTruthy(domHasAttr(s, "data-mount")) ? (function() {
|
||||||
|
var mountSel = domGetAttr(s, "data-mount");
|
||||||
|
var target = domQuery(mountSel);
|
||||||
|
return (isSxTruthy(target) ? sxMount(target, text, NIL) : NIL);
|
||||||
|
})() : sxLoadComponents(text))));
|
||||||
|
})()) : NIL); }, scripts);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// process-component-script
|
||||||
|
var processComponentScript = function(script, text) { return (function() {
|
||||||
|
var hash = domGetAttr(script, "data-hash");
|
||||||
|
return (isSxTruthy(isNil(hash)) ? (isSxTruthy((isSxTruthy(text) && !isEmpty(trim(text)))) ? sxLoadComponents(text) : NIL) : (function() {
|
||||||
|
var hasInline = (isSxTruthy(text) && !isEmpty(trim(text)));
|
||||||
|
(function() {
|
||||||
|
var cachedHash = localStorageGet("sx-components-hash");
|
||||||
|
return (isSxTruthy((cachedHash == hash)) ? (isSxTruthy(hasInline) ? (localStorageSet("sx-components-hash", hash), localStorageSet("sx-components-src", text), sxLoadComponents(text), logInfo("components: downloaded (cookie stale)")) : (function() {
|
||||||
|
var cached = localStorageGet("sx-components-src");
|
||||||
|
return (isSxTruthy(cached) ? (sxLoadComponents(cached), logInfo((String("components: cached (") + String(hash) + String(")")))) : (clearSxCompCookie(), browserReload()));
|
||||||
|
})()) : (isSxTruthy(hasInline) ? (localStorageSet("sx-components-hash", hash), localStorageSet("sx-components-src", text), sxLoadComponents(text), logInfo((String("components: downloaded (") + String(hash) + String(")")))) : (localStorageRemove("sx-components-hash"), localStorageRemove("sx-components-src"), clearSxCompCookie(), browserReload())));
|
||||||
|
})();
|
||||||
|
return setSxCompCookie(hash);
|
||||||
|
})());
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// init-style-dict
|
||||||
|
var initStyleDict = function() { return (function() {
|
||||||
|
var scripts = queryStyleScripts();
|
||||||
|
return forEach(function(s) { return (isSxTruthy(!isProcessed(s, "styles")) ? (markProcessed(s, "styles"), (function() {
|
||||||
|
var text = domTextContent(s);
|
||||||
|
var hash = domGetAttr(s, "data-hash");
|
||||||
|
return (isSxTruthy(isNil(hash)) ? (isSxTruthy((isSxTruthy(text) && !isEmpty(trim(text)))) ? parseAndLoadStyleDict(text) : NIL) : (function() {
|
||||||
|
var hasInline = (isSxTruthy(text) && !isEmpty(trim(text)));
|
||||||
|
(function() {
|
||||||
|
var cachedHash = localStorageGet("sx-styles-hash");
|
||||||
|
return (isSxTruthy((cachedHash == hash)) ? (isSxTruthy(hasInline) ? (localStorageSet("sx-styles-src", text), parseAndLoadStyleDict(text), logInfo("styles: downloaded (cookie stale)")) : (function() {
|
||||||
|
var cached = localStorageGet("sx-styles-src");
|
||||||
|
return (isSxTruthy(cached) ? (parseAndLoadStyleDict(cached), logInfo((String("styles: cached (") + String(hash) + String(")")))) : (clearSxStylesCookie(), browserReload()));
|
||||||
|
})()) : (isSxTruthy(hasInline) ? (localStorageSet("sx-styles-hash", hash), localStorageSet("sx-styles-src", text), parseAndLoadStyleDict(text), logInfo((String("styles: downloaded (") + String(hash) + String(")")))) : (localStorageRemove("sx-styles-hash"), localStorageRemove("sx-styles-src"), clearSxStylesCookie(), browserReload())));
|
||||||
|
})();
|
||||||
|
return setSxStylesCookie(hash);
|
||||||
|
})());
|
||||||
|
})()) : NIL); }, scripts);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// boot-init
|
||||||
|
var bootInit = function() { return (initCssTracking(), initStyleDict(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||||
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Platform interface — DOM adapter (browser-only)
|
// Platform interface — DOM adapter (browser-only)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -2465,6 +2755,252 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 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 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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Post-transpilation fixups
|
// Post-transpilation fixups
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -2638,8 +3174,14 @@
|
|||||||
process: typeof processElements === "function" ? processElements : null,
|
process: typeof processElements === "function" ? processElements : null,
|
||||||
executeRequest: typeof executeRequest === "function" ? executeRequest : null,
|
executeRequest: typeof executeRequest === "function" ? executeRequest : null,
|
||||||
postSwap: typeof postSwap === "function" ? postSwap : null,
|
postSwap: typeof postSwap === "function" ? postSwap : null,
|
||||||
init: typeof engineInit === "function" ? engineInit : null,
|
processScripts: typeof processSxScripts === "function" ? processSxScripts : null,
|
||||||
_version: "ref-2.0 (dom+engine+html+orchestration+sx, bootstrap-compiled)"
|
mount: typeof sxMount === "function" ? sxMount : null,
|
||||||
|
hydrate: typeof sxHydrateElements === "function" ? sxHydrateElements : null,
|
||||||
|
update: typeof sxUpdateElement === "function" ? sxUpdateElement : null,
|
||||||
|
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
|
||||||
|
getEnv: function() { return componentEnv; },
|
||||||
|
init: typeof bootInit === "function" ? bootInit : null,
|
||||||
|
_version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+sx, bootstrap-compiled)"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -2652,7 +3194,7 @@
|
|||||||
|
|
||||||
// --- Auto-init ---
|
// --- Auto-init ---
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
var _sxRefInit = function() { engineInit(); };
|
var _sxRefInit = function() { bootInit(); };
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", _sxRefInit);
|
document.addEventListener("DOMContentLoaded", _sxRefInit);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
384
shared/sx/ref/boot.sx
Normal file
384
shared/sx/ref/boot.sx
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
;; ==========================================================================
|
||||||
|
;; boot.sx — Browser boot, mount, hydrate, script processing
|
||||||
|
;;
|
||||||
|
;; Handles the browser startup lifecycle:
|
||||||
|
;; 1. CSS tracking init
|
||||||
|
;; 2. Style dictionary loading (from <script type="text/sx-styles">)
|
||||||
|
;; 3. Component script processing (from <script type="text/sx">)
|
||||||
|
;; 4. Hydration of [data-sx] elements
|
||||||
|
;; 5. Engine element processing
|
||||||
|
;;
|
||||||
|
;; Also provides the public mounting/hydration API:
|
||||||
|
;; mount, hydrate, update, render-component
|
||||||
|
;;
|
||||||
|
;; Depends on:
|
||||||
|
;; cssx.sx — load-style-dict
|
||||||
|
;; orchestration.sx — process-elements, engine-init
|
||||||
|
;; adapter-dom.sx — render-to-dom
|
||||||
|
;; render.sx — shared registries
|
||||||
|
;; ==========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Head element hoisting (full version)
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Moves <meta>, <title>, <link rel=canonical>, <script type=application/ld+json>
|
||||||
|
;; from rendered content to <head>, deduplicating as needed.
|
||||||
|
|
||||||
|
(define HEAD_HOIST_SELECTOR
|
||||||
|
"meta, title, link[rel='canonical'], script[type='application/ld+json']")
|
||||||
|
|
||||||
|
(define hoist-head-elements-full
|
||||||
|
(fn (root)
|
||||||
|
(let ((els (dom-query-all root HEAD_HOIST_SELECTOR)))
|
||||||
|
(for-each
|
||||||
|
(fn (el)
|
||||||
|
(let ((tag (lower (dom-tag-name el))))
|
||||||
|
(cond
|
||||||
|
;; <title> — replace document title
|
||||||
|
(= tag "title")
|
||||||
|
(do
|
||||||
|
(set-document-title (dom-text-content el))
|
||||||
|
(dom-remove-child (dom-parent el) el))
|
||||||
|
|
||||||
|
;; <meta> — deduplicate by name or property
|
||||||
|
(= tag "meta")
|
||||||
|
(do
|
||||||
|
(let ((name (dom-get-attr el "name"))
|
||||||
|
(prop (dom-get-attr el "property")))
|
||||||
|
(when name
|
||||||
|
(remove-head-element (str "meta[name=\"" name "\"]")))
|
||||||
|
(when prop
|
||||||
|
(remove-head-element (str "meta[property=\"" prop "\"]"))))
|
||||||
|
(dom-remove-child (dom-parent el) el)
|
||||||
|
(dom-append-to-head el))
|
||||||
|
|
||||||
|
;; <link rel=canonical> — deduplicate
|
||||||
|
(and (= tag "link")
|
||||||
|
(= (dom-get-attr el "rel") "canonical"))
|
||||||
|
(do
|
||||||
|
(remove-head-element "link[rel=\"canonical\"]")
|
||||||
|
(dom-remove-child (dom-parent el) el)
|
||||||
|
(dom-append-to-head el))
|
||||||
|
|
||||||
|
;; Everything else (ld+json, etc.) — just move
|
||||||
|
:else
|
||||||
|
(do
|
||||||
|
(dom-remove-child (dom-parent el) el)
|
||||||
|
(dom-append-to-head el)))))
|
||||||
|
els))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Mount — render SX source into a DOM element
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define sx-mount
|
||||||
|
(fn (target source extra-env)
|
||||||
|
;; Render SX source string into target element.
|
||||||
|
;; target: Element or CSS selector string
|
||||||
|
;; source: SX source string
|
||||||
|
;; extra-env: optional extra bindings dict
|
||||||
|
(let ((el (resolve-mount-target target)))
|
||||||
|
(when el
|
||||||
|
(let ((node (sx-render-with-env source extra-env)))
|
||||||
|
(dom-set-text-content el "")
|
||||||
|
(dom-append el node)
|
||||||
|
;; Hoist head elements from rendered content
|
||||||
|
(hoist-head-elements-full el)
|
||||||
|
;; Process sx- attributes and hydrate
|
||||||
|
(process-elements el)
|
||||||
|
(sx-hydrate-elements el))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Hydrate — render all [data-sx] elements
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define sx-hydrate-elements
|
||||||
|
(fn (root)
|
||||||
|
;; Find all [data-sx] elements within root and render them.
|
||||||
|
(let ((els (dom-query-all (or root (dom-body)) "[data-sx]")))
|
||||||
|
(for-each
|
||||||
|
(fn (el)
|
||||||
|
(when (not (is-processed? el "hydrated"))
|
||||||
|
(mark-processed! el "hydrated")
|
||||||
|
(sx-update-element el nil)))
|
||||||
|
els))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Update — re-render a [data-sx] element with new env data
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define sx-update-element
|
||||||
|
(fn (el new-env)
|
||||||
|
;; Re-render a [data-sx] element.
|
||||||
|
;; Reads source from data-sx attr, base env from data-sx-env attr.
|
||||||
|
(let ((target (resolve-mount-target el)))
|
||||||
|
(when target
|
||||||
|
(let ((source (dom-get-attr target "data-sx")))
|
||||||
|
(when source
|
||||||
|
(let ((base-env (parse-env-attr target))
|
||||||
|
(env (merge-envs base-env new-env)))
|
||||||
|
(let ((node (sx-render-with-env source env)))
|
||||||
|
(dom-set-text-content target "")
|
||||||
|
(dom-append target node)
|
||||||
|
;; Update stored env if new-env provided
|
||||||
|
(when new-env
|
||||||
|
(store-env-attr target base-env new-env))))))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Render component — build synthetic call from kwargs dict
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define sx-render-component
|
||||||
|
(fn (name kwargs extra-env)
|
||||||
|
;; Render a named component with keyword args.
|
||||||
|
;; name: component name (with or without ~ prefix)
|
||||||
|
;; kwargs: dict of param-name → value
|
||||||
|
;; extra-env: optional extra env bindings
|
||||||
|
(let ((full-name (if (starts-with? name "~") name (str "~" name))))
|
||||||
|
(let ((env (get-render-env extra-env))
|
||||||
|
(comp (env-get env full-name)))
|
||||||
|
(if (not (component? comp))
|
||||||
|
(error (str "Unknown component: " full-name))
|
||||||
|
;; Build synthetic call expression
|
||||||
|
(let ((call-expr (list (make-symbol full-name))))
|
||||||
|
(for-each
|
||||||
|
(fn (k)
|
||||||
|
(append! call-expr (make-keyword (to-kebab k)))
|
||||||
|
(append! call-expr (dict-get kwargs k)))
|
||||||
|
(keys kwargs))
|
||||||
|
(render-to-dom call-expr env nil)))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Script processing — <script type="text/sx">
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define process-sx-scripts
|
||||||
|
(fn (root)
|
||||||
|
;; Process all <script type="text/sx"> tags.
|
||||||
|
;; - data-components + data-hash → localStorage cache
|
||||||
|
;; - data-mount="<selector>" → render into target
|
||||||
|
;; - Default: load as components
|
||||||
|
(let ((scripts (query-sx-scripts root)))
|
||||||
|
(for-each
|
||||||
|
(fn (s)
|
||||||
|
(when (not (is-processed? s "script"))
|
||||||
|
(mark-processed! s "script")
|
||||||
|
(let ((text (dom-text-content s)))
|
||||||
|
(cond
|
||||||
|
;; Component definitions
|
||||||
|
(dom-has-attr? s "data-components")
|
||||||
|
(process-component-script s text)
|
||||||
|
|
||||||
|
;; Empty script — skip
|
||||||
|
(or (nil? text) (empty? (trim text)))
|
||||||
|
nil
|
||||||
|
|
||||||
|
;; Mount directive
|
||||||
|
(dom-has-attr? s "data-mount")
|
||||||
|
(let ((mount-sel (dom-get-attr s "data-mount"))
|
||||||
|
(target (dom-query mount-sel)))
|
||||||
|
(when target
|
||||||
|
(sx-mount target text nil)))
|
||||||
|
|
||||||
|
;; Default: load as components
|
||||||
|
:else
|
||||||
|
(sx-load-components text)))))
|
||||||
|
scripts))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Component script with caching
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define process-component-script
|
||||||
|
(fn (script text)
|
||||||
|
;; Handle <script type="text/sx" data-components data-hash="...">
|
||||||
|
(let ((hash (dom-get-attr script "data-hash")))
|
||||||
|
(if (nil? hash)
|
||||||
|
;; Legacy: no hash — just load inline
|
||||||
|
(when (and text (not (empty? (trim text))))
|
||||||
|
(sx-load-components text))
|
||||||
|
;; Hash-based caching
|
||||||
|
(let ((has-inline (and text (not (empty? (trim text))))))
|
||||||
|
(let ((cached-hash (local-storage-get "sx-components-hash")))
|
||||||
|
(if (= cached-hash hash)
|
||||||
|
;; Cache hit
|
||||||
|
(if has-inline
|
||||||
|
;; Server sent full source (cookie stale) — update cache
|
||||||
|
(do
|
||||||
|
(local-storage-set "sx-components-hash" hash)
|
||||||
|
(local-storage-set "sx-components-src" text)
|
||||||
|
(sx-load-components text)
|
||||||
|
(log-info "components: downloaded (cookie stale)"))
|
||||||
|
;; Server omitted source — load from cache
|
||||||
|
(let ((cached (local-storage-get "sx-components-src")))
|
||||||
|
(if cached
|
||||||
|
(do
|
||||||
|
(sx-load-components cached)
|
||||||
|
(log-info (str "components: cached (" hash ")")))
|
||||||
|
;; Cache entry missing — clear cookie and reload
|
||||||
|
(do
|
||||||
|
(clear-sx-comp-cookie)
|
||||||
|
(browser-reload)))))
|
||||||
|
;; Cache miss — hash mismatch
|
||||||
|
(if has-inline
|
||||||
|
;; Server sent full source — cache it
|
||||||
|
(do
|
||||||
|
(local-storage-set "sx-components-hash" hash)
|
||||||
|
(local-storage-set "sx-components-src" text)
|
||||||
|
(sx-load-components text)
|
||||||
|
(log-info (str "components: downloaded (" hash ")")))
|
||||||
|
;; Server omitted but cache stale — clear and reload
|
||||||
|
(do
|
||||||
|
(local-storage-remove "sx-components-hash")
|
||||||
|
(local-storage-remove "sx-components-src")
|
||||||
|
(clear-sx-comp-cookie)
|
||||||
|
(browser-reload)))))
|
||||||
|
(set-sx-comp-cookie hash))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Style dictionary initialization
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define init-style-dict
|
||||||
|
(fn ()
|
||||||
|
;; Process <script type="text/sx-styles"> tags with caching.
|
||||||
|
(let ((scripts (query-style-scripts)))
|
||||||
|
(for-each
|
||||||
|
(fn (s)
|
||||||
|
(when (not (is-processed? s "styles"))
|
||||||
|
(mark-processed! s "styles")
|
||||||
|
(let ((text (dom-text-content s))
|
||||||
|
(hash (dom-get-attr s "data-hash")))
|
||||||
|
(if (nil? hash)
|
||||||
|
;; No hash — just parse inline
|
||||||
|
(when (and text (not (empty? (trim text))))
|
||||||
|
(parse-and-load-style-dict text))
|
||||||
|
;; Hash-based caching
|
||||||
|
(let ((has-inline (and text (not (empty? (trim text))))))
|
||||||
|
(let ((cached-hash (local-storage-get "sx-styles-hash")))
|
||||||
|
(if (= cached-hash hash)
|
||||||
|
;; Cache hit
|
||||||
|
(if has-inline
|
||||||
|
(do
|
||||||
|
(local-storage-set "sx-styles-src" text)
|
||||||
|
(parse-and-load-style-dict text)
|
||||||
|
(log-info "styles: downloaded (cookie stale)"))
|
||||||
|
(let ((cached (local-storage-get "sx-styles-src")))
|
||||||
|
(if cached
|
||||||
|
(do
|
||||||
|
(parse-and-load-style-dict cached)
|
||||||
|
(log-info (str "styles: cached (" hash ")")))
|
||||||
|
(do
|
||||||
|
(clear-sx-styles-cookie)
|
||||||
|
(browser-reload)))))
|
||||||
|
;; Cache miss
|
||||||
|
(if has-inline
|
||||||
|
(do
|
||||||
|
(local-storage-set "sx-styles-hash" hash)
|
||||||
|
(local-storage-set "sx-styles-src" text)
|
||||||
|
(parse-and-load-style-dict text)
|
||||||
|
(log-info (str "styles: downloaded (" hash ")")))
|
||||||
|
(do
|
||||||
|
(local-storage-remove "sx-styles-hash")
|
||||||
|
(local-storage-remove "sx-styles-src")
|
||||||
|
(clear-sx-styles-cookie)
|
||||||
|
(browser-reload)))))
|
||||||
|
(set-sx-styles-cookie hash))))))
|
||||||
|
scripts))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Full boot sequence
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define boot-init
|
||||||
|
(fn ()
|
||||||
|
;; Full browser initialization:
|
||||||
|
;; 1. CSS tracking
|
||||||
|
;; 2. Style dictionary
|
||||||
|
;; 3. Process scripts (components + mounts)
|
||||||
|
;; 4. Hydrate [data-sx] elements
|
||||||
|
;; 5. Process engine elements
|
||||||
|
(do
|
||||||
|
(init-css-tracking)
|
||||||
|
(init-style-dict)
|
||||||
|
(process-sx-scripts nil)
|
||||||
|
(sx-hydrate-elements nil)
|
||||||
|
(process-elements nil))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Platform interface — Boot
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;;
|
||||||
|
;; From orchestration.sx:
|
||||||
|
;; process-elements, init-css-tracking
|
||||||
|
;;
|
||||||
|
;; From cssx.sx:
|
||||||
|
;; load-style-dict
|
||||||
|
;;
|
||||||
|
;; === DOM / Render ===
|
||||||
|
;; (resolve-mount-target target) → Element (string → querySelector, else identity)
|
||||||
|
;; (sx-render-with-env source extra-env) → DOM node (parse + render with componentEnv + extra)
|
||||||
|
;; (get-render-env extra-env) → merged component env + extra
|
||||||
|
;; (merge-envs base new) → merged env dict
|
||||||
|
;; (render-to-dom expr env ns) → DOM node
|
||||||
|
;; (sx-load-components text) → void (parse + eval into componentEnv)
|
||||||
|
;;
|
||||||
|
;; === DOM queries ===
|
||||||
|
;; (dom-query sel) → Element or nil
|
||||||
|
;; (dom-query-all root sel) → list of Elements
|
||||||
|
;; (dom-body) → document.body
|
||||||
|
;; (dom-get-attr el name) → string or nil
|
||||||
|
;; (dom-has-attr? el name) → boolean
|
||||||
|
;; (dom-text-content el) → string
|
||||||
|
;; (dom-set-text-content el s) → void
|
||||||
|
;; (dom-append el child) → void
|
||||||
|
;; (dom-remove-child parent el) → void
|
||||||
|
;; (dom-parent el) → Element
|
||||||
|
;; (dom-append-to-head el) → void
|
||||||
|
;; (dom-tag-name el) → string
|
||||||
|
;;
|
||||||
|
;; === Head hoisting ===
|
||||||
|
;; (set-document-title s) → void (document.title = s)
|
||||||
|
;; (remove-head-element sel) → void (remove matching element from <head>)
|
||||||
|
;;
|
||||||
|
;; === Script queries ===
|
||||||
|
;; (query-sx-scripts root) → list of <script type="text/sx"> elements
|
||||||
|
;; (query-style-scripts) → list of <script type="text/sx-styles"> elements
|
||||||
|
;;
|
||||||
|
;; === localStorage ===
|
||||||
|
;; (local-storage-get key) → string or nil
|
||||||
|
;; (local-storage-set key val) → void
|
||||||
|
;; (local-storage-remove key) → void
|
||||||
|
;;
|
||||||
|
;; === Cookies ===
|
||||||
|
;; (set-sx-comp-cookie hash) → void
|
||||||
|
;; (clear-sx-comp-cookie) → void
|
||||||
|
;; (set-sx-styles-cookie hash) → void
|
||||||
|
;; (clear-sx-styles-cookie) → void
|
||||||
|
;;
|
||||||
|
;; === Env ===
|
||||||
|
;; (parse-env-attr el) → dict (parse data-sx-env JSON attr)
|
||||||
|
;; (store-env-attr el base new) → void (merge and store back as JSON)
|
||||||
|
;; (to-kebab s) → string (underscore → kebab-case)
|
||||||
|
;;
|
||||||
|
;; === Logging ===
|
||||||
|
;; (log-info msg) → void (console.log with prefix)
|
||||||
|
;; (log-parse-error label text err) → void (diagnostic parse error)
|
||||||
|
;;
|
||||||
|
;; === JSON parsing ===
|
||||||
|
;; (parse-and-load-style-dict text) → void (JSON.parse + load-style-dict)
|
||||||
|
;;
|
||||||
|
;; === Processing markers ===
|
||||||
|
;; (mark-processed! el key) → void
|
||||||
|
;; (is-processed? el key) → boolean
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
@@ -423,6 +423,65 @@ class JSEmitter:
|
|||||||
"format-date": "formatDate",
|
"format-date": "formatDate",
|
||||||
"format-decimal": "formatDecimal",
|
"format-decimal": "formatDecimal",
|
||||||
"parse-int": "parseInt_",
|
"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:
|
if name in RENAMES:
|
||||||
return RENAMES[name]
|
return RENAMES[name]
|
||||||
@@ -710,10 +769,17 @@ ADAPTER_FILES = {
|
|||||||
"dom": ("adapter-dom.sx", "adapter-dom"),
|
"dom": ("adapter-dom.sx", "adapter-dom"),
|
||||||
"engine": ("engine.sx", "engine"),
|
"engine": ("engine.sx", "engine"),
|
||||||
"orchestration": ("orchestration.sx","orchestration"),
|
"orchestration": ("orchestration.sx","orchestration"),
|
||||||
|
"cssx": ("cssx.sx", "cssx"),
|
||||||
|
"boot": ("boot.sx", "boot"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Dependencies: orchestration requires engine+dom, engine requires dom
|
# Dependencies
|
||||||
ADAPTER_DEPS = {"engine": ["dom"], "orchestration": ["engine", "dom"]}
|
ADAPTER_DEPS = {
|
||||||
|
"engine": ["dom"],
|
||||||
|
"orchestration": ["engine", "dom"],
|
||||||
|
"cssx": [],
|
||||||
|
"boot": ["dom", "engine", "orchestration", "cssx"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
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,
|
"dom": PLATFORM_DOM_JS,
|
||||||
"engine": PLATFORM_ENGINE_PURE_JS,
|
"engine": PLATFORM_ENGINE_PURE_JS,
|
||||||
"orchestration": PLATFORM_ORCHESTRATION_JS,
|
"orchestration": PLATFORM_ORCHESTRATION_JS,
|
||||||
|
"cssx": PLATFORM_CSSX_JS,
|
||||||
|
"boot": PLATFORM_BOOT_JS,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve adapter set
|
# Resolve adapter set
|
||||||
@@ -752,7 +820,7 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
|||||||
("eval.sx", "eval"),
|
("eval.sx", "eval"),
|
||||||
("render.sx", "render (core)"),
|
("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:
|
if name in adapter_set:
|
||||||
sx_files.append(ADAPTER_FILES[name])
|
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_dom = "dom" in adapter_set
|
||||||
has_engine = "engine" in adapter_set
|
has_engine = "engine" in adapter_set
|
||||||
has_orch = "orchestration" 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"
|
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
@@ -787,12 +857,12 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
|||||||
# Platform JS for selected adapters
|
# Platform JS for selected adapters
|
||||||
if not has_dom:
|
if not has_dom:
|
||||||
parts.append("\n var _hasDom = false;\n")
|
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:
|
if name in adapter_set and name in adapter_platform:
|
||||||
parts.append(adapter_platform[name])
|
parts.append(adapter_platform[name])
|
||||||
|
|
||||||
parts.append(fixups_js(has_html, has_sx, has_dom))
|
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)
|
parts.append(EPILOGUE)
|
||||||
return "\n".join(parts)
|
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):
|
def fixups_js(has_html, has_sx, has_dom):
|
||||||
lines = ['''
|
lines = ['''
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -2068,7 +2386,7 @@ def fixups_js(has_html, has_sx, has_dom):
|
|||||||
return "\n".join(lines)
|
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 is always included
|
||||||
parser = r'''
|
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(' process: typeof processElements === "function" ? processElements : null,')
|
||||||
api_lines.append(' executeRequest: typeof executeRequest === "function" ? executeRequest : null,')
|
api_lines.append(' executeRequest: typeof executeRequest === "function" ? executeRequest : null,')
|
||||||
api_lines.append(' postSwap: typeof postSwap === "function" ? postSwap : 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(' init: typeof engineInit === "function" ? engineInit : null,')
|
||||||
|
|
||||||
api_lines.append(f' _version: "{version}"')
|
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) {
|
window.addEventListener("popstate", function(e) {
|
||||||
handlePopstate(e && e.state ? e.state.scrollY || 0 : 0);
|
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 ---
|
// --- Auto-init ---
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
var _sxRefInit = function() { engineInit(); };
|
var _sxRefInit = function() { engineInit(); };
|
||||||
|
|||||||
314
shared/sx/ref/cssx.sx
Normal file
314
shared/sx/ref/cssx.sx
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
;; ==========================================================================
|
||||||
|
;; cssx.sx — On-demand CSS style dictionary
|
||||||
|
;;
|
||||||
|
;; Resolves keyword atoms (e.g. :flex, :gap-4, :hover:bg-sky-200) into
|
||||||
|
;; StyleValue objects with content-addressed class names. CSS rules are
|
||||||
|
;; injected into the document on first use.
|
||||||
|
;;
|
||||||
|
;; The style dictionary is loaded from a JSON blob (typically served
|
||||||
|
;; inline in a <script type="text/sx-styles"> tag) containing:
|
||||||
|
;; a — atom → CSS declarations map
|
||||||
|
;; v — pseudo-variant → CSS pseudo-selector map
|
||||||
|
;; b — responsive breakpoint → media query map
|
||||||
|
;; k — keyframe name → @keyframes rule map
|
||||||
|
;; p — arbitrary patterns: [[regex, template], ...]
|
||||||
|
;; c — child selector prefixes: ["space-x-", "space-y-", ...]
|
||||||
|
;;
|
||||||
|
;; Depends on:
|
||||||
|
;; render.sx — StyleValue type
|
||||||
|
;; ==========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; State — populated by load-style-dict
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define _style-atoms (dict))
|
||||||
|
(define _pseudo-variants (dict))
|
||||||
|
(define _responsive-breakpoints (dict))
|
||||||
|
(define _style-keyframes (dict))
|
||||||
|
(define _arbitrary-patterns (list))
|
||||||
|
(define _child-selector-prefixes (list))
|
||||||
|
(define _style-cache (dict))
|
||||||
|
(define _injected-styles (dict))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Load style dictionary from parsed JSON data
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define load-style-dict
|
||||||
|
(fn (data)
|
||||||
|
(set! _style-atoms (or (get data "a") (dict)))
|
||||||
|
(set! _pseudo-variants (or (get data "v") (dict)))
|
||||||
|
(set! _responsive-breakpoints (or (get data "b") (dict)))
|
||||||
|
(set! _style-keyframes (or (get data "k") (dict)))
|
||||||
|
(set! _child-selector-prefixes (or (get data "c") (list)))
|
||||||
|
;; Compile arbitrary patterns from [regex, template] pairs
|
||||||
|
(set! _arbitrary-patterns
|
||||||
|
(map
|
||||||
|
(fn (pair)
|
||||||
|
(dict "re" (compile-regex (str "^" (first pair) "$"))
|
||||||
|
"tmpl" (nth pair 1)))
|
||||||
|
(or (get data "p") (list))))
|
||||||
|
;; Clear cache on reload
|
||||||
|
(set! _style-cache (dict))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Variant splitting
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define split-variant
|
||||||
|
(fn (atom)
|
||||||
|
;; Parse variant prefixes: "sm:hover:bg-sky-200" → ["sm:hover", "bg-sky-200"]
|
||||||
|
;; Returns [variant, base] where variant is nil for no variant.
|
||||||
|
|
||||||
|
;; Check responsive prefix first
|
||||||
|
(let ((result nil))
|
||||||
|
(for-each
|
||||||
|
(fn (bp)
|
||||||
|
(when (nil? result)
|
||||||
|
(let ((prefix (str bp ":")))
|
||||||
|
(when (starts-with? atom prefix)
|
||||||
|
(let ((rest-atom (slice atom (len prefix))))
|
||||||
|
;; Check for compound variant (sm:hover:...)
|
||||||
|
(let ((inner-match nil))
|
||||||
|
(for-each
|
||||||
|
(fn (pv)
|
||||||
|
(when (nil? inner-match)
|
||||||
|
(let ((inner-prefix (str pv ":")))
|
||||||
|
(when (starts-with? rest-atom inner-prefix)
|
||||||
|
(set! inner-match
|
||||||
|
(list (str bp ":" pv)
|
||||||
|
(slice rest-atom (len inner-prefix))))))))
|
||||||
|
(keys _pseudo-variants))
|
||||||
|
(set! result
|
||||||
|
(or inner-match (list bp rest-atom)))))))))
|
||||||
|
(keys _responsive-breakpoints))
|
||||||
|
|
||||||
|
(when (nil? result)
|
||||||
|
;; Check pseudo variants
|
||||||
|
(for-each
|
||||||
|
(fn (pv)
|
||||||
|
(when (nil? result)
|
||||||
|
(let ((prefix (str pv ":")))
|
||||||
|
(when (starts-with? atom prefix)
|
||||||
|
(set! result (list pv (slice atom (len prefix))))))))
|
||||||
|
(keys _pseudo-variants)))
|
||||||
|
|
||||||
|
(or result (list nil atom)))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Atom resolution
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define resolve-atom
|
||||||
|
(fn (atom)
|
||||||
|
;; Look up atom → CSS declarations string, or nil
|
||||||
|
(let ((decls (dict-get _style-atoms atom)))
|
||||||
|
(if (not (nil? decls))
|
||||||
|
decls
|
||||||
|
;; Dynamic keyframes: animate-{name}
|
||||||
|
(if (starts-with? atom "animate-")
|
||||||
|
(let ((kf-name (slice atom 8)))
|
||||||
|
(if (dict-has? _style-keyframes kf-name)
|
||||||
|
(str "animation-name:" kf-name)
|
||||||
|
nil))
|
||||||
|
;; Try arbitrary patterns
|
||||||
|
(let ((match-result nil))
|
||||||
|
(for-each
|
||||||
|
(fn (pat)
|
||||||
|
(when (nil? match-result)
|
||||||
|
(let ((m (regex-match (get pat "re") atom)))
|
||||||
|
(when m
|
||||||
|
(set! match-result
|
||||||
|
(regex-replace-groups (get pat "tmpl") m))))))
|
||||||
|
_arbitrary-patterns)
|
||||||
|
match-result))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Child selector detection
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define is-child-selector-atom?
|
||||||
|
(fn (atom)
|
||||||
|
(some
|
||||||
|
(fn (prefix) (starts-with? atom prefix))
|
||||||
|
_child-selector-prefixes)))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; FNV-1a 32-bit hash → 6 hex chars
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define hash-style
|
||||||
|
(fn (input)
|
||||||
|
;; FNV-1a 32-bit hash for content-addressed class names
|
||||||
|
(fnv1a-hash input)))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Full style resolution pipeline
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define resolve-style
|
||||||
|
(fn (atoms)
|
||||||
|
;; Resolve a list of atom strings into a StyleValue.
|
||||||
|
;; Uses content-addressed caching.
|
||||||
|
(let ((key (join "\0" atoms)))
|
||||||
|
(let ((cached (dict-get _style-cache key)))
|
||||||
|
(if (not (nil? cached))
|
||||||
|
cached
|
||||||
|
;; Resolve each atom
|
||||||
|
(let ((base-decls (list))
|
||||||
|
(media-rules (list))
|
||||||
|
(pseudo-rules (list))
|
||||||
|
(kf-needed (list)))
|
||||||
|
(for-each
|
||||||
|
(fn (a)
|
||||||
|
(when a
|
||||||
|
(let ((clean (if (starts-with? a ":") (slice a 1) a)))
|
||||||
|
(let ((parts (split-variant clean)))
|
||||||
|
(let ((variant (first parts))
|
||||||
|
(base (nth parts 1))
|
||||||
|
(decls (resolve-atom base)))
|
||||||
|
(when decls
|
||||||
|
;; Check keyframes
|
||||||
|
(when (starts-with? base "animate-")
|
||||||
|
(let ((kf-name (slice base 8)))
|
||||||
|
(when (dict-has? _style-keyframes kf-name)
|
||||||
|
(append! kf-needed
|
||||||
|
(list kf-name (dict-get _style-keyframes kf-name))))))
|
||||||
|
|
||||||
|
(cond
|
||||||
|
(nil? variant)
|
||||||
|
(append! base-decls decls)
|
||||||
|
|
||||||
|
(dict-has? _responsive-breakpoints variant)
|
||||||
|
(append! media-rules
|
||||||
|
(list (dict-get _responsive-breakpoints variant) decls))
|
||||||
|
|
||||||
|
(dict-has? _pseudo-variants variant)
|
||||||
|
(append! pseudo-rules
|
||||||
|
(list (dict-get _pseudo-variants variant) decls))
|
||||||
|
|
||||||
|
;; Compound variant: "sm:hover"
|
||||||
|
:else
|
||||||
|
(let ((vparts (split variant ":"))
|
||||||
|
(media-part nil)
|
||||||
|
(pseudo-part nil))
|
||||||
|
(for-each
|
||||||
|
(fn (vp)
|
||||||
|
(cond
|
||||||
|
(dict-has? _responsive-breakpoints vp)
|
||||||
|
(set! media-part (dict-get _responsive-breakpoints vp))
|
||||||
|
(dict-has? _pseudo-variants vp)
|
||||||
|
(set! pseudo-part (dict-get _pseudo-variants vp))))
|
||||||
|
vparts)
|
||||||
|
(when media-part
|
||||||
|
(append! media-rules (list media-part decls)))
|
||||||
|
(when pseudo-part
|
||||||
|
(append! pseudo-rules (list pseudo-part decls)))
|
||||||
|
(when (and (nil? media-part) (nil? pseudo-part))
|
||||||
|
(append! base-decls decls))))))))))
|
||||||
|
atoms)
|
||||||
|
|
||||||
|
;; Build hash input
|
||||||
|
(let ((hash-input (join ";" base-decls)))
|
||||||
|
(for-each
|
||||||
|
(fn (mr)
|
||||||
|
(set! hash-input
|
||||||
|
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
|
||||||
|
(chunk-every media-rules 2))
|
||||||
|
(for-each
|
||||||
|
(fn (pr)
|
||||||
|
(set! hash-input
|
||||||
|
(str hash-input (first pr) "{" (nth pr 1) "}")))
|
||||||
|
(chunk-every pseudo-rules 2))
|
||||||
|
(for-each
|
||||||
|
(fn (kf)
|
||||||
|
(set! hash-input (str hash-input (nth kf 1))))
|
||||||
|
(chunk-every kf-needed 2))
|
||||||
|
|
||||||
|
(let ((cn (str "sx-" (hash-style hash-input)))
|
||||||
|
(sv (make-style-value cn
|
||||||
|
(join ";" base-decls)
|
||||||
|
(chunk-every media-rules 2)
|
||||||
|
(chunk-every pseudo-rules 2)
|
||||||
|
(chunk-every kf-needed 2))))
|
||||||
|
(dict-set! _style-cache key sv)
|
||||||
|
;; Inject CSS rules
|
||||||
|
(inject-style-value sv atoms)
|
||||||
|
sv))))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Merge multiple StyleValues
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define merge-style-values
|
||||||
|
(fn (styles)
|
||||||
|
(if (= (len styles) 1)
|
||||||
|
(first styles)
|
||||||
|
(let ((all-decls (list))
|
||||||
|
(all-media (list))
|
||||||
|
(all-pseudo (list))
|
||||||
|
(all-kf (list)))
|
||||||
|
(for-each
|
||||||
|
(fn (sv)
|
||||||
|
(when (style-value-declarations sv)
|
||||||
|
(append! all-decls (style-value-declarations sv)))
|
||||||
|
(set! all-media (concat all-media (style-value-media-rules sv)))
|
||||||
|
(set! all-pseudo (concat all-pseudo (style-value-pseudo-rules sv)))
|
||||||
|
(set! all-kf (concat all-kf (style-value-keyframes sv))))
|
||||||
|
styles)
|
||||||
|
|
||||||
|
(let ((hash-input (join ";" all-decls)))
|
||||||
|
(for-each
|
||||||
|
(fn (mr)
|
||||||
|
(set! hash-input
|
||||||
|
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
|
||||||
|
all-media)
|
||||||
|
(for-each
|
||||||
|
(fn (pr)
|
||||||
|
(set! hash-input
|
||||||
|
(str hash-input (first pr) "{" (nth pr 1) "}")))
|
||||||
|
all-pseudo)
|
||||||
|
(for-each
|
||||||
|
(fn (kf)
|
||||||
|
(set! hash-input (str hash-input (nth kf 1))))
|
||||||
|
all-kf)
|
||||||
|
|
||||||
|
(let ((cn (str "sx-" (hash-style hash-input)))
|
||||||
|
(merged (make-style-value cn
|
||||||
|
(join ";" all-decls)
|
||||||
|
all-media all-pseudo all-kf)))
|
||||||
|
(inject-style-value merged (list))
|
||||||
|
merged))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Platform interface — CSSX
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;;
|
||||||
|
;; Hash:
|
||||||
|
;; (fnv1a-hash input) → 6-char hex string (FNV-1a 32-bit)
|
||||||
|
;;
|
||||||
|
;; Regex:
|
||||||
|
;; (compile-regex pattern) → compiled regex object
|
||||||
|
;; (regex-match re str) → match array or nil
|
||||||
|
;; (regex-replace-groups tmpl match) → string with {0},{1},... replaced
|
||||||
|
;;
|
||||||
|
;; StyleValue construction:
|
||||||
|
;; (make-style-value cn decls media pseudo kf) → StyleValue object
|
||||||
|
;; (style-value-declarations sv) → declarations string
|
||||||
|
;; (style-value-media-rules sv) → list of [query, decls] pairs
|
||||||
|
;; (style-value-pseudo-rules sv) → list of [selector, decls] pairs
|
||||||
|
;; (style-value-keyframes sv) → list of [name, rule] pairs
|
||||||
|
;;
|
||||||
|
;; CSS injection:
|
||||||
|
;; (inject-style-value sv atoms) → void (append CSS rules to <style id="sx-css">)
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
Reference in New Issue
Block a user