diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 221b289..80f1fca 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -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(/]*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); \ No newline at end of file diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py index 4551ffe..004083f 100644 --- a/shared/sx/helpers.py +++ b/shared/sx/helpers.py @@ -606,7 +606,7 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details. - + """ @@ -693,7 +693,7 @@ def sx_page(ctx: dict, page_sx: str, *, page_sx=page_sx, sx_css=sx_css, sx_css_classes=sx_css_classes, - sx_js_hash=_script_hash("sx.js"), + sx_js_hash=_script_hash("sx-browser.js"), body_js_hash=_script_hash("body.js"), ) diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index 99eebda..32db2a9 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -135,6 +135,8 @@ class JSEmitter: "eval-expr": "evalExpr", "eval-list": "evalList", "eval-call": "evalCall", + "is-render-expr?": "isRenderExpr", + "render-expr": "renderExpr", "call-lambda": "callLambda", "call-component": "callComponent", "parse-keyword-args": "parseKeywordArgs", @@ -559,7 +561,7 @@ class JSEmitter: def _emit_fn(self, expr) -> str: params = expr[1] - body = expr[2] + body = expr[2:] param_names = [] for p in params: if isinstance(p, Symbol): @@ -567,8 +569,16 @@ class JSEmitter: else: param_names.append(str(p)) params_str = ", ".join(param_names) - body_js = self.emit(body) - return f"function({params_str}) {{ return {body_js}; }}" + if len(body) == 1: + body_js = self.emit(body[0]) + return f"function({params_str}) {{ return {body_js}; }}" + # Multi-expression body: statements then return last + parts = [] + for b in body[:-1]: + parts.append(self.emit_statement(b)) + parts.append(f"return {self.emit(body[-1])};") + inner = "\n".join(parts) + return f"function({params_str}) {{ {inner} }}" def _emit_let(self, expr) -> str: bindings = expr[1] @@ -717,10 +727,10 @@ class JSEmitter: # If fn is an inline lambda, emit a for loop if isinstance(fn_expr, list) and fn_expr[0] == Symbol("fn"): params = fn_expr[1] - body = fn_expr[2] + body = fn_expr[2:] p = params[0].name if isinstance(params[0], Symbol) else str(params[0]) p_js = self._mangle(p) - body_js = self.emit_statement(body) + body_js = "\n".join(self.emit_statement(b) for b in body) return f"{{ var _c = {coll}; for (var _i = 0; _i < _c.length; _i++) {{ var {p_js} = _c[_i]; {body_js} }} }}" fn = self.emit(fn_expr) return f"{{ var _c = {coll}; for (var _i = 0; _i < _c.length; _i++) {{ {fn}(_c[_i]); }} }}" @@ -978,6 +988,7 @@ PLATFORM_JS = ''' 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"; @@ -1053,6 +1064,29 @@ PLATFORM_JS = ''' 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; } @@ -1366,6 +1400,9 @@ PLATFORM_DOM_JS = """ 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"; @@ -1649,7 +1686,7 @@ PLATFORM_ORCHESTRATION_JS = """ 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"] || "" }), @@ -2045,23 +2082,25 @@ PLATFORM_ORCHESTRATION_JS = """ // --- 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) === "~"; }); @@ -2070,7 +2109,7 @@ PLATFORM_ORCHESTRATION_JS = """ // --- 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(/]*type="text\\/sx"[^>]*data-components[^>]*>([\\s\\S]*?)<\\/script>/gi, function(_, defs) { if (SxObj && SxObj.loadComponents) SxObj.loadComponents(defs); return ""; }); } @@ -2269,8 +2308,9 @@ PLATFORM_BOOT_JS = """ 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() { @@ -2556,11 +2596,13 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has return parts.join(""); }''') - # Build SxRef object + # Build Sx object version = f"ref-2.0 ({adapter_label}, bootstrap-compiled)" api_lines.append(f''' - 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,{"" if has_html else ""} @@ -2569,6 +2611,8 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has NIL: NIL, Symbol: Symbol, Keyword: Keyword, + isTruthy: isSxTruthy, + isNil: isNil, componentEnv: componentEnv,''') if has_html: @@ -2612,27 +2656,27 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has api_lines.append(''' // --- 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(); } }''') elif has_orch: api_lines.append(''' // --- Auto-init --- if (typeof document !== "undefined") { - var _sxRefInit = function() { engineInit(); }; + var _sxInit = function() { engineInit(); }; if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", _sxRefInit); + document.addEventListener("DOMContentLoaded", _sxInit); } else { - _sxRefInit(); + _sxInit(); } }''') - api_lines.append(' if (typeof module !== "undefined" && module.exports) module.exports = SxRef;') - api_lines.append(' else global.SxRef = SxRef;') + api_lines.append(' if (typeof module !== "undefined" && module.exports) module.exports = Sx;') + api_lines.append(' else global.Sx = Sx;') return "\n".join(api_lines) diff --git a/shared/sx/ref/eval.sx b/shared/sx/ref/eval.sx index fc7e68e..eb4134f 100644 --- a/shared/sx/ref/eval.sx +++ b/shared/sx/ref/eval.sx @@ -165,6 +165,10 @@ (let ((mac (env-get env name))) (make-thunk (expand-macro mac args env) env)) + ;; Render expression — delegate to active adapter + (is-render-expr? expr) + (render-expr expr env) + ;; Fall through to function call :else (eval-call head args env))) diff --git a/shared/sx/ref/orchestration.sx b/shared/sx/ref/orchestration.sx index a08ad04..df14cdd 100644 --- a/shared/sx/ref/orchestration.sx +++ b/shared/sx/ref/orchestration.sx @@ -61,7 +61,7 @@ (define init-css-tracking (fn () ;; Read initial CSS hash from meta tag - (let ((meta (dom-query "meta[name=\"sx-css-hash\"]"))) + (let ((meta (dom-query "meta[name=\"sx-css-classes\"]"))) (when meta (let ((content (dom-get-attr meta "content"))) (when content @@ -76,8 +76,9 @@ (fn (el verbInfo extraParams) ;; Gate checks then delegate to do-fetch. ;; verbInfo: dict with "method" and "url" (or nil to read from element). + ;; Re-read from element in case attributes were morphed since binding. ;; Returns a promise. - (let ((info (or verbInfo (get-verb-info el)))) + (let ((info (or (get-verb-info el) verbInfo))) (if (nil? info) (promise-resolve nil) (let ((verb (get info "method"))