Fix sx-browser.js navigation bugs: CSS tracking meta tag and stale verb info

Two fixes for sx-browser.js (spec-compiled) vs sx.js (hand-written):

1. CSS meta tag mismatch: initCssTracking read meta[name="sx-css-hash"]
   but the page template uses meta[name="sx-css-classes"]. This left
   _cssHash empty, causing the server to send ALL CSS as "new" on every
   navigation, appending duplicate rules that broke Tailwind responsive
   ordering (e.g. menu bar layout).

2. Stale verb info after morph: execute-request used captured verbInfo
   from bind time. After morph updated element attributes (e.g. during
   OOB nav swap), click handlers still fired with old URLs. Now re-reads
   verb info from the element first, matching sx.js behavior.

Also includes: render-expression dispatch in eval.sx, NIL guard for
preload cache in bootstrap_js.py, and helpers.py switched to
sx-browser.js.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 15:28:56 +00:00
parent af77fc32c7
commit cea009084f
5 changed files with 201 additions and 63 deletions

View File

@@ -103,6 +103,7 @@
if (x._macro) return "macro";
if (x._raw) return "raw-html";
if (x._styleValue) return "style-value";
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
if (Array.isArray(x)) return "list";
if (typeof x === "object") return "dict";
return "unknown";
@@ -178,6 +179,29 @@
function dictSet(d, k, v) { d[k] = v; }
function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; }
// Render-expression detection — lets the evaluator delegate to the active adapter.
// Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements.
function isRenderExpr(expr) {
if (!Array.isArray(expr) || !expr.length) return false;
var h = expr[0];
if (!h || !h._sym) return false;
var n = h.name;
return !!(n === "<>" || n === "raw!" ||
n.charAt(0) === "~" || n.indexOf("html:") === 0 ||
(typeof HTML_TAGS !== "undefined" && HTML_TAGS.indexOf(n) >= 0) ||
(typeof SVG_TAGS !== "undefined" && SVG_TAGS.indexOf(n) >= 0) ||
(n.indexOf("-") > 0 && expr.length > 1 && expr[1] && expr[1]._kw));
}
// Render dispatch — call the active adapter's render function.
// Set by each adapter when loaded; defaults to identity (no rendering).
var _renderExprFn = null;
function renderExpr(expr, env) {
if (_renderExprFn) return _renderExprFn(expr, env);
// No adapter loaded — just return the expression as-is
return expr;
}
function stripPrefix(s, prefix) {
return s.indexOf(prefix) === 0 ? s.slice(prefix.length) : s;
}
@@ -507,7 +531,7 @@
return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defkeyframes")) ? sfDefkeyframes(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefine(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
var mac = envGet(env, name);
return makeThunk(expandMacro(mac, args, env), env);
})() : evalCall(head, args, env)))))))))))))))))))))))))))))));
})() : (isSxTruthy(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))));
})() : evalCall(head, args, env)));
})(); };
@@ -1149,11 +1173,12 @@
var morphNode = function(oldNode, newNode) { return (isSxTruthy(sxOr(domHasAttr(oldNode, "sx-preserve"), domHasAttr(oldNode, "sx-ignore"))) ? NIL : (isSxTruthy(sxOr(!(domNodeType(oldNode) == domNodeType(newNode)), !(domNodeName(oldNode) == domNodeName(newNode)))) ? domReplaceChild(domParent(oldNode), domClone(newNode), oldNode) : (isSxTruthy(sxOr((domNodeType(oldNode) == 3), (domNodeType(oldNode) == 8))) ? (isSxTruthy(!(domTextContent(oldNode) == domTextContent(newNode))) ? domSetTextContent(oldNode, domTextContent(newNode)) : NIL) : (isSxTruthy((domNodeType(oldNode) == 1)) ? (syncAttrs(oldNode, newNode), (isSxTruthy(!(isSxTruthy(domIsActiveElement(oldNode)) && domIsInputElement(oldNode))) ? morphChildren(oldNode, newNode) : NIL)) : NIL)))); };
// sync-attrs
var syncAttrs = function(oldEl, newEl) { return forEach(function(attr) { return (function() {
var syncAttrs = function(oldEl, newEl) { { var _c = domAttrList(newEl); for (var _i = 0; _i < _c.length; _i++) { var attr = _c[_i]; (function() {
var name = first(attr);
var val = nth(attr, 1);
return (isSxTruthy(!(domGetAttr(oldEl, name) == val)) ? domSetAttr(oldEl, name, val) : NIL);
})(); }, domAttrList(newEl)); };
})(); } }
return forEach(function(attr) { return (isSxTruthy(!domHasAttr(newEl, first(attr))) ? domRemoveAttr(oldEl, first(attr)) : NIL); }, domAttrList(oldEl)); };
// morph-children
var morphChildren = function(oldParent, newParent) { return (function() {
@@ -1272,7 +1297,7 @@
// init-css-tracking
var initCssTracking = function() { return (function() {
var meta = domQuery("meta[name=\"sx-css-hash\"]");
var meta = domQuery("meta[name=\"sx-css-classes\"]");
return (isSxTruthy(meta) ? (function() {
var content = domGetAttr(meta, "content");
return (isSxTruthy(content) ? (_cssHash = content) : NIL);
@@ -1281,7 +1306,7 @@
// execute-request
var executeRequest = function(el, verbInfo, extraParams) { return (function() {
var info = sxOr(verbInfo, getVerbInfo(el));
var info = sxOr(getVerbInfo(el), verbInfo);
return (isSxTruthy(isNil(info)) ? promiseResolve(NIL) : (function() {
var verb = get(info, "method");
var url = get(info, "url");
@@ -1373,11 +1398,14 @@
var rendered = sxRender(trimmed);
var container = domCreateElement("div", NIL);
domAppend(container, rendered);
processOobSwaps(container, function(t, oob, s) { return swapDomNodes(t, oob, s); });
processOobSwaps(container, function(t, oob, s) { swapDomNodes(t, oob, s);
sxHydrate(t);
return processElements(t); });
return (function() {
var selectSel = domGetAttr(el, "sx-select");
var content = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container));
return withTransition(useTransition, function() { return swapDomNodes(target, content, swapStyle); });
return withTransition(useTransition, function() { swapDomNodes(target, content, swapStyle);
return postSwap(target); });
})();
})() : NIL);
})();
@@ -1391,13 +1419,16 @@
var selectSel = domGetAttr(el, "sx-select");
return (isSxTruthy(selectSel) ? (function() {
var html = selectHtmlFromDoc(doc, selectSel);
return withTransition(useTransition, function() { return swapHtmlString(target, html, swapStyle); });
return withTransition(useTransition, function() { swapHtmlString(target, html, swapStyle);
return postSwap(target); });
})() : (function() {
var container = domCreateElement("div", NIL);
domSetInnerHtml(container, domBodyInnerHtml(doc));
processOobSwaps(container, function(t, oob, s) { return swapDomNodes(t, oob, s); });
processOobSwaps(container, function(t, oob, s) { swapDomNodes(t, oob, s);
return postSwap(t); });
hoistHeadElements(container);
return withTransition(useTransition, function() { return swapDomNodes(target, childrenToFragment(container), swapStyle); });
return withTransition(useTransition, function() { swapDomNodes(target, childrenToFragment(container), swapStyle);
return postSwap(target); });
})());
})() : NIL);
})(); };
@@ -1444,7 +1475,10 @@
})(); };
// post-swap
var postSwap = function(root) { return activateScripts(root); };
var postSwap = function(root) { activateScripts(root);
sxProcessScripts(root);
sxHydrate(root);
return processElements(root); };
// activate-scripts
var activateScripts = function(root) { return (isSxTruthy(root) ? (function() {
@@ -1472,13 +1506,43 @@
})(); };
// hoist-head-elements
var hoistHeadElements = function(container) { return forEach(function(style) { return (isSxTruthy(domParent(style)) ? domRemoveChild(domParent(style), style) : NIL); }, domQueryAll(container, "style[data-sx-css]")); };
var hoistHeadElements = function(container) { { var _c = domQueryAll(container, "style[data-sx-css]"); for (var _i = 0; _i < _c.length; _i++) { var style = _c[_i]; if (isSxTruthy(domParent(style))) {
domRemoveChild(domParent(style), style);
}
domAppendToHead(style); } }
return forEach(function(link) { if (isSxTruthy(domParent(link))) {
domRemoveChild(domParent(link), link);
}
return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"]")); };
// process-boosted
var processBoosted = function(root) { return forEach(function(container) { return boostDescendants(container); }, domQueryAll(sxOr(root, domBody()), "[sx-boost]")); };
// boost-descendants
var boostDescendants = function(container) { return forEach(function(link) { return (isSxTruthy((isSxTruthy(!isProcessed(link, "boost")) && shouldBoostLink(link))) ? (markProcessed(link, "boost"), (isSxTruthy(!domHasAttr(link, "sx-target")) ? domSetAttr(link, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(link, "sx-swap")) ? domSetAttr(link, "sx-swap", "innerHTML") : NIL), (isSxTruthy(!domHasAttr(link, "sx-push-url")) ? domSetAttr(link, "sx-push-url", "true") : NIL), bindBoostLink(link, domGetAttr(link, "href"))) : NIL); }, domQueryAll(container, "a[href]")); };
var boostDescendants = function(container) { { var _c = domQueryAll(container, "a[href]"); for (var _i = 0; _i < _c.length; _i++) { var link = _c[_i]; if (isSxTruthy((isSxTruthy(!isProcessed(link, "boost")) && shouldBoostLink(link)))) {
markProcessed(link, "boost");
if (isSxTruthy(!domHasAttr(link, "sx-target"))) {
domSetAttr(link, "sx-target", "#main-panel");
}
if (isSxTruthy(!domHasAttr(link, "sx-swap"))) {
domSetAttr(link, "sx-swap", "innerHTML");
}
if (isSxTruthy(!domHasAttr(link, "sx-push-url"))) {
domSetAttr(link, "sx-push-url", "true");
}
bindBoostLink(link, domGetAttr(link, "href"));
} } }
return forEach(function(form) { return (isSxTruthy((isSxTruthy(!isProcessed(form, "boost")) && shouldBoostForm(form))) ? (markProcessed(form, "boost"), (function() {
var method = upper(sxOr(domGetAttr(form, "method"), "GET"));
var action = sxOr(domGetAttr(form, "action"), browserLocationHref());
if (isSxTruthy(!domHasAttr(form, "sx-target"))) {
domSetAttr(form, "sx-target", "#main-panel");
}
if (isSxTruthy(!domHasAttr(form, "sx-swap"))) {
domSetAttr(form, "sx-swap", "innerHTML");
}
return bindBoostForm(form, method, action);
})()) : NIL); }, domQueryAll(container, "form")); };
// process-sse
var processSse = function(root) { return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), bindSse(el)) : NIL); }, domQueryAll(sxOr(root, domBody()), "[sx-sse]")); };
@@ -1504,8 +1568,10 @@
var rendered = sxRender(trimmed);
var container = domCreateElement("div", NIL);
domAppend(container, rendered);
return withTransition(useTransition, function() { return swapDomNodes(target, childrenToFragment(container), swapStyle); });
})() : withTransition(useTransition, function() { return swapHtmlString(target, trimmed, swapStyle); })) : NIL);
return withTransition(useTransition, function() { swapDomNodes(target, childrenToFragment(container), swapStyle);
return postSwap(target); });
})() : withTransition(useTransition, function() { swapHtmlString(target, trimmed, swapStyle);
return postSwap(target); })) : NIL);
})(); };
// bind-inline-handlers
@@ -1540,10 +1606,13 @@
var VERB_SELECTOR = (String("[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]"));
// process-elements
var processElements = function(root) { return (function() {
var processElements = function(root) { (function() {
var els = domQueryAll(sxOr(root, domBody()), VERB_SELECTOR);
return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "verb")) ? (markProcessed(el, "verb"), processOne(el)) : NIL); }, els);
})(); };
})();
processBoosted(root);
processSse(root);
return bindInlineHandlers(root); };
// process-one
var processOne = function(el) { return (function() {
@@ -1592,7 +1661,13 @@
var _injectedStyles = {};
// load-style-dict
var loadStyleDict = function(data) { return (_styleAtoms = sxOr(get(data, "a"), {})); };
var loadStyleDict = function(data) { _styleAtoms = sxOr(get(data, "a"), {});
_pseudoVariants = sxOr(get(data, "v"), {});
_responsiveBreakpoints = sxOr(get(data, "b"), {});
_styleKeyframes = sxOr(get(data, "k"), {});
_childSelectorPrefixes = sxOr(get(data, "c"), []);
_arbitraryPatterns = map(function(pair) { return {["re"]: compileRegex((String("^") + String(first(pair)) + String("$"))), ["tmpl"]: nth(pair, 1)}; }, sxOr(get(data, "p"), []));
return (_styleCache = {}); };
// split-variant
var splitVariant = function(atom) { return (function() {
@@ -1714,7 +1789,10 @@
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));
} } }
}
allMedia = concat(allMedia, styleValueMediaRules(sv));
allPseudo = concat(allPseudo, styleValuePseudoRules(sv));
allKf = concat(allKf, styleValueKeyframes_(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("}")); } }
@@ -1796,7 +1874,8 @@
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))); } }
{ var _c = keys(kwargs); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; callExpr.push(makeKeyword(toKebab(k)));
callExpr.push(dictGet(kwargs, k)); } }
return renderToDom(callExpr, env, NIL);
})());
})();
@@ -1861,6 +1940,9 @@
var _hasDom = typeof document !== "undefined";
// Register DOM adapter as the render dispatch target for the evaluator.
_renderExprFn = function(expr, env) { return renderToDom(expr, env, null); };
var SVG_NS = "http://www.w3.org/2000/svg";
var MATH_NS = "http://www.w3.org/1998/Math/MathML";
@@ -2142,7 +2224,7 @@
if (config.body && config.method !== "GET") opts.body = config.body;
if (config["cross-origin"]) opts.credentials = "include";
var p = config.preloaded
var p = (config.preloaded && config.preloaded !== NIL)
? Promise.resolve({
ok: true, status: 200,
headers: new Headers({ "Content-Type": config.preloaded["content-type"] || "" }),
@@ -2538,23 +2620,25 @@
// --- SX API references ---
function sxRender(source) {
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
var SxObj = typeof Sx !== "undefined" ? Sx : null;
if (SxObj && SxObj.render) return SxObj.render(source);
throw new Error("No SX renderer available");
}
function sxProcessScripts(root) {
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
if (SxObj && SxObj.processScripts) SxObj.processScripts(root || undefined);
var SxObj = typeof Sx !== "undefined" ? Sx : null;
var r = (root && root !== NIL) ? root : undefined;
if (SxObj && SxObj.processScripts) SxObj.processScripts(r);
}
function sxHydrate(root) {
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
if (SxObj && SxObj.hydrate) SxObj.hydrate(root || undefined);
var SxObj = typeof Sx !== "undefined" ? Sx : null;
var r = (root && root !== NIL) ? root : undefined;
if (SxObj && SxObj.hydrate) SxObj.hydrate(r);
}
function loadedComponentNames() {
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
var SxObj = typeof Sx !== "undefined" ? Sx : null;
if (!SxObj) return [];
var env = SxObj.componentEnv || (SxObj.getEnv ? SxObj.getEnv() : {});
return Object.keys(env).filter(function(k) { return k.charAt(0) === "~"; });
@@ -2563,7 +2647,7 @@
// --- Response processing ---
function stripComponentScripts(text) {
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
var SxObj = typeof Sx !== "undefined" ? Sx : null;
return text.replace(/<script[^>]*type="text\/sx"[^>]*data-components[^>]*>([\s\S]*?)<\/script>/gi,
function(_, defs) { if (SxObj && SxObj.loadComponents) SxObj.loadComponents(defs); return ""; });
}
@@ -2760,8 +2844,9 @@
function querySxScripts(root) {
if (!_hasDom) return [];
var r = (root && root !== NIL) ? root : document;
return Array.prototype.slice.call(
(root || document).querySelectorAll('script[type="text/sx"]'));
r.querySelectorAll('script[type="text/sx"]'));
}
function queryStyleScripts() {
@@ -2989,8 +3074,10 @@
return frag;
}
var SxRef = {
var Sx = {
VERSION: "ref-2.0",
parse: parse,
parseAll: parse,
eval: function(expr, env) { return trampoline(evalExpr(expr, env || merge(componentEnv))); },
loadComponents: loadComponents,
render: render,
@@ -2999,6 +3086,8 @@
NIL: NIL,
Symbol: Symbol,
Keyword: Keyword,
isTruthy: isSxTruthy,
isNil: isNil,
componentEnv: componentEnv,
renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,
parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,
@@ -3028,14 +3117,14 @@
// --- Auto-init ---
if (typeof document !== "undefined") {
var _sxRefInit = function() { bootInit(); };
var _sxInit = function() { bootInit(); };
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", _sxRefInit);
document.addEventListener("DOMContentLoaded", _sxInit);
} else {
_sxRefInit();
_sxInit();
}
}
if (typeof module !== "undefined" && module.exports) module.exports = SxRef;
else global.SxRef = SxRef;
if (typeof module !== "undefined" && module.exports) module.exports = Sx;
else global.Sx = Sx;
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);