Restructure SX ref spec into core + selectable adapters

Split monolithic render.sx into core (tag registries, shared utils) plus
four adapter .sx files: adapter-html (server HTML strings), adapter-sx
(SX wire format), adapter-dom (browser DOM nodes), and engine (SxEngine
triggers, morphing, swaps). All adapters written in s-expressions with
platform interface declarations for JS bridge functions.

Bootstrap compiler now accepts --adapters flag to emit targeted builds:
  -a html        → server-only (1108 lines)
  -a dom,engine  → browser-only (1634 lines)
  -a html,sx     → server with SX wire (1169 lines)
  (default)      → all adapters (1800 lines)

Fixes: keyword arg i-counter desync in reduce across all adapters,
render-aware special forms (let/if/when/cond/map) in HTML adapter,
component children double-escaping, ~prefixed macro dispatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 11:49:44 +00:00
parent a9526c4fa1
commit daeecab310
8 changed files with 3083 additions and 384 deletions

View File

@@ -146,6 +146,7 @@ class JSEmitter:
"render-value-to-html": "renderValueToHtml",
"render-list-to-html": "renderListToHtml",
"render-html-element": "renderHtmlElement",
"render-html-component": "renderHtmlComponent",
"parse-element-args": "parseElementArgs",
"render-attrs": "renderAttrs",
"aser-list": "aserList",
@@ -195,6 +196,120 @@ class JSEmitter:
"HTML_TAGS": "HTML_TAGS",
"VOID_ELEMENTS": "VOID_ELEMENTS",
"BOOLEAN_ATTRS": "BOOLEAN_ATTRS",
# render.sx core
"definition-form?": "isDefinitionForm",
# adapter-html.sx
"RENDER_HTML_FORMS": "RENDER_HTML_FORMS",
"render-html-form?": "isRenderHtmlForm",
"dispatch-html-form": "dispatchHtmlForm",
"render-lambda-html": "renderLambdaHtml",
"make-raw-html": "makeRawHtml",
# adapter-dom.sx
"SVG_NS": "SVG_NS",
"MATH_NS": "MATH_NS",
"render-to-dom": "renderToDom",
"render-dom-list": "renderDomList",
"render-dom-element": "renderDomElement",
"render-dom-component": "renderDomComponent",
"render-dom-fragment": "renderDomFragment",
"render-dom-raw": "renderDomRaw",
"render-dom-unknown-component": "renderDomUnknownComponent",
"RENDER_DOM_FORMS": "RENDER_DOM_FORMS",
"render-dom-form?": "isRenderDomForm",
"dispatch-render-form": "dispatchRenderForm",
"render-lambda-dom": "renderLambdaDom",
"dom-create-element": "domCreateElement",
"dom-append": "domAppend",
"dom-set-attr": "domSetAttr",
"dom-get-attr": "domGetAttr",
"dom-remove-attr": "domRemoveAttr",
"dom-has-attr?": "domHasAttr",
"dom-parse-html": "domParseHtml",
"dom-clone": "domClone",
"create-text-node": "createTextNode",
"create-fragment": "createFragment",
"dom-parent": "domParent",
"dom-id": "domId",
"dom-node-type": "domNodeType",
"dom-node-name": "domNodeName",
"dom-text-content": "domTextContent",
"dom-set-text-content": "domSetTextContent",
"dom-is-fragment?": "domIsFragment",
"dom-is-child-of?": "domIsChildOf",
"dom-is-active-element?": "domIsActiveElement",
"dom-is-input-element?": "domIsInputElement",
"dom-first-child": "domFirstChild",
"dom-next-sibling": "domNextSibling",
"dom-child-list": "domChildList",
"dom-attr-list": "domAttrList",
"dom-insert-before": "domInsertBefore",
"dom-insert-after": "domInsertAfter",
"dom-prepend": "domPrepend",
"dom-remove-child": "domRemoveChild",
"dom-replace-child": "domReplaceChild",
"dom-set-inner-html": "domSetInnerHtml",
"dom-insert-adjacent-html": "domInsertAdjacentHtml",
"dom-get-style": "domGetStyle",
"dom-set-style": "domSetStyle",
"dom-get-prop": "domGetProp",
"dom-set-prop": "domSetProp",
"dom-add-class": "domAddClass",
"dom-remove-class": "domRemoveClass",
"dom-dispatch": "domDispatch",
"dom-query": "domQuery",
"dom-query-all": "domQueryAll",
"dom-tag-name": "domTagName",
"dict-has?": "dictHas",
"dict-delete!": "dictDelete",
"process-bindings": "processBindings",
"eval-cond": "evalCond",
"for-each-indexed": "forEachIndexed",
"index-of": "indexOf_",
"component-has-children?": "componentHasChildren",
# engine.sx
"ENGINE_VERBS": "ENGINE_VERBS",
"DEFAULT_SWAP": "DEFAULT_SWAP",
"parse-time": "parseTime",
"parse-trigger-spec": "parseTriggerSpec",
"default-trigger": "defaultTrigger",
"get-verb-info": "getVerbInfo",
"build-request-headers": "buildRequestHeaders",
"process-response-headers": "processResponseHeaders",
"parse-swap-spec": "parseSwapSpec",
"parse-retry-spec": "parseRetrySpec",
"next-retry-ms": "nextRetryMs",
"filter-params": "filterParams",
"resolve-target": "resolveTarget",
"apply-optimistic": "applyOptimistic",
"revert-optimistic": "revertOptimistic",
"find-oob-swaps": "findOobSwaps",
"morph-node": "morphNode",
"sync-attrs": "syncAttrs",
"morph-children": "morphChildren",
"swap-dom-nodes": "swapDomNodes",
"insert-remaining-siblings": "insertRemainingSiblings",
"swap-html-string": "swapHtmlString",
"handle-history": "handleHistory",
"PRELOAD_TTL": "PRELOAD_TTL",
"preload-cache-get": "preloadCacheGet",
"preload-cache-set": "preloadCacheSet",
"classify-trigger": "classifyTrigger",
"should-boost-link?": "shouldBoostLink",
"should-boost-form?": "shouldBoostForm",
"parse-sse-swap": "parseSseSwap",
"browser-location-href": "browserLocationHref",
"browser-same-origin?": "browserSameOrigin",
"browser-push-state": "browserPushState",
"browser-replace-state": "browserReplaceState",
"browser-navigate": "browserNavigate",
"browser-reload": "browserReload",
"browser-scroll-to": "browserScrollTo",
"browser-media-matches?": "browserMediaMatches",
"browser-confirm": "browserConfirm",
"browser-prompt": "browserPrompt",
"now-ms": "nowMs",
"parse-header-value": "parseHeaderValue",
"replace": "replace_",
"whitespace?": "isWhitespace",
"digit?": "isDigit",
"ident-start?": "isIdentStart",
@@ -503,36 +618,92 @@ def extract_defines(source: str) -> list[tuple[str, list]]:
return defines
def compile_ref_to_js() -> str:
"""Read reference .sx files and emit JavaScript."""
ADAPTER_FILES = {
"html": ("adapter-html.sx", "adapter-html"),
"sx": ("adapter-sx.sx", "adapter-sx"),
"dom": ("adapter-dom.sx", "adapter-dom"),
"engine": ("engine.sx", "engine"),
}
# Dependencies: engine requires dom
ADAPTER_DEPS = {"engine": ["dom"]}
def compile_ref_to_js(adapters: list[str] | None = None) -> str:
"""Read reference .sx files and emit JavaScript.
Args:
adapters: List of adapter names to include.
Valid names: html, sx, dom, engine.
None = include all adapters.
"""
ref_dir = os.path.dirname(os.path.abspath(__file__))
emitter = JSEmitter()
# Read reference files
with open(os.path.join(ref_dir, "eval.sx")) as f:
eval_src = f.read()
with open(os.path.join(ref_dir, "render.sx")) as f:
render_src = f.read()
# Platform JS blocks keyed by adapter name
adapter_platform = {
"dom": PLATFORM_DOM_JS,
"engine": PLATFORM_ENGINE_JS,
}
eval_defines = extract_defines(eval_src)
render_defines = extract_defines(render_src)
# Resolve adapter set
if adapters is None:
adapter_set = set(ADAPTER_FILES.keys())
else:
adapter_set = set()
for a in adapters:
if a not in ADAPTER_FILES:
raise ValueError(f"Unknown adapter: {a!r}. Valid: {', '.join(ADAPTER_FILES)}")
adapter_set.add(a)
# Pull in dependencies
for dep in ADAPTER_DEPS.get(a, []):
adapter_set.add(dep)
# Core files always included, then selected adapters
sx_files = [
("eval.sx", "eval"),
("render.sx", "render (core)"),
]
for name in ("html", "sx", "dom", "engine"):
if name in adapter_set:
sx_files.append(ADAPTER_FILES[name])
all_sections = []
for filename, label in sx_files:
filepath = os.path.join(ref_dir, filename)
if not os.path.exists(filepath):
continue
with open(filepath) as f:
src = f.read()
defines = extract_defines(src)
all_sections.append((label, defines))
# Build output
has_html = "html" in adapter_set
has_sx = "sx" in adapter_set
has_dom = "dom" in adapter_set
has_engine = "engine" in adapter_set
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
parts = []
parts.append(PREAMBLE)
parts.append(PLATFORM_JS)
parts.append("\n // === Transpiled from eval.sx ===\n")
for name, expr in eval_defines:
parts.append(f" // {name}")
parts.append(f" {emitter.emit_statement(expr)}")
parts.append("")
parts.append("\n // === Transpiled from render.sx ===\n")
for name, expr in render_defines:
parts.append(f" // {name}")
parts.append(f" {emitter.emit_statement(expr)}")
parts.append("")
parts.append(FIXUPS)
parts.append(PUBLIC_API)
for label, defines in all_sections:
parts.append(f"\n // === Transpiled from {label} ===\n")
for name, expr in defines:
parts.append(f" // {name}")
parts.append(f" {emitter.emit_statement(expr)}")
parts.append("")
# Platform JS for selected adapters
if not has_dom:
parts.append("\n var _hasDom = false;\n")
for name in ("dom", "engine"):
if name in adapter_set and name in adapter_platform:
parts.append(adapter_platform[name])
parts.append(fixups_js(has_html, has_sx, has_dom))
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, adapter_label))
parts.append(EPILOGUE)
return "\n".join(parts)
@@ -950,12 +1121,23 @@ PLATFORM_JS = '''
function append_b(arr, x) { arr.push(x); return arr; }
var apply = function(f, args) { return f.apply(null, args); };
// Additional primitive aliases used by adapter/engine transpiled code
var split = PRIMITIVES["split"];
var trim = PRIMITIVES["trim"];
var upper = PRIMITIVES["upper"];
var lower = PRIMITIVES["lower"];
var replace_ = function(s, old, nw) { return s.split(old).join(nw); };
var endsWith = PRIMITIVES["ends-with?"];
var parseInt_ = PRIMITIVES["parse-int"];
var dict_fn = PRIMITIVES["dict"];
// HTML rendering helpers
function escapeHtml(s) {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}
function escapeAttr(s) { return escapeHtml(s); }
function rawHtmlContent(r) { return r.html; }
function makeRawHtml(s) { return { _raw: true, html: s }; }
// Serializer
function serialize(val) {
@@ -977,9 +1159,271 @@ PLATFORM_JS = '''
}; }
function isHoForm(n) { return n in {
"map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1
}; }'''
}; }
FIXUPS = '''
// processBindings and evalCond — exposed for DOM adapter render forms
function processBindings(bindings, env) {
var local = merge(env);
for (var i = 0; i < bindings.length; i++) {
var pair = bindings[i];
if (Array.isArray(pair) && pair.length >= 2) {
var name = isSym(pair[0]) ? pair[0].name : String(pair[0]);
local[name] = trampoline(evalExpr(pair[1], local));
}
}
return local;
}
function evalCond(clauses, env) {
for (var i = 0; i < clauses.length; i += 2) {
var test = clauses[i];
if (isSym(test) && test.name === ":else") return clauses[i + 1];
if (isKw(test) && test.name === "else") return clauses[i + 1];
if (isSxTruthy(trampoline(evalExpr(test, env)))) return clauses[i + 1];
}
return null;
}
function isDefinitionForm(name) {
return name === "define" || name === "defcomp" || name === "defmacro" ||
name === "defstyle" || name === "defkeyframes" || name === "defhandler";
}
function indexOf_(s, ch) {
return typeof s === "string" ? s.indexOf(ch) : -1;
}
function dictHas(d, k) { return d != null && k in d; }
function dictDelete(d, k) { delete d[k]; }
function forEachIndexed(fn, coll) {
for (var i = 0; i < coll.length; i++) fn(i, coll[i]);
return NIL;
}'''
PLATFORM_DOM_JS = """
// =========================================================================
// Platform interface — DOM adapter (browser-only)
// =========================================================================
var _hasDom = typeof document !== "undefined";
var SVG_NS = "http://www.w3.org/2000/svg";
var MATH_NS = "http://www.w3.org/1998/Math/MathML";
function domCreateElement(tag, ns) {
if (!_hasDom) return null;
if (ns) return document.createElementNS(ns, tag);
return document.createElement(tag);
}
function createTextNode(s) {
return _hasDom ? document.createTextNode(s) : null;
}
function createFragment() {
return _hasDom ? document.createDocumentFragment() : null;
}
function domAppend(parent, child) {
if (parent && child) parent.appendChild(child);
}
function domPrepend(parent, child) {
if (parent && child) parent.insertBefore(child, parent.firstChild);
}
function domSetAttr(el, name, val) {
if (el && el.setAttribute) el.setAttribute(name, val);
}
function domGetAttr(el, name) {
if (!el || !el.getAttribute) return NIL;
var v = el.getAttribute(name);
return v === null ? NIL : v;
}
function domRemoveAttr(el, name) {
if (el && el.removeAttribute) el.removeAttribute(name);
}
function domHasAttr(el, name) {
return !!(el && el.hasAttribute && el.hasAttribute(name));
}
function domParseHtml(html) {
if (!_hasDom) return null;
var tpl = document.createElement("template");
tpl.innerHTML = html;
return tpl.content;
}
function domClone(node) {
return node && node.cloneNode ? node.cloneNode(true) : node;
}
function domParent(el) { return el ? el.parentNode : null; }
function domId(el) { return el && el.id ? el.id : NIL; }
function domNodeType(el) { return el ? el.nodeType : 0; }
function domNodeName(el) { return el ? el.nodeName : ""; }
function domTextContent(el) { return el ? el.textContent || el.nodeValue || "" : ""; }
function domSetTextContent(el, s) { if (el) { if (el.nodeType === 3 || el.nodeType === 8) el.nodeValue = s; else el.textContent = s; } }
function domIsFragment(el) { return el ? el.nodeType === 11 : false; }
function domIsChildOf(child, parent) { return !!(parent && child && child.parentNode === parent); }
function domIsActiveElement(el) { return _hasDom && el === document.activeElement; }
function domIsInputElement(el) {
if (!el || !el.tagName) return false;
var t = el.tagName;
return t === "INPUT" || t === "TEXTAREA" || t === "SELECT";
}
function domFirstChild(el) { return el ? el.firstChild : null; }
function domNextSibling(el) { return el ? el.nextSibling : null; }
function domChildList(el) {
if (!el || !el.childNodes) return [];
return Array.prototype.slice.call(el.childNodes);
}
function domAttrList(el) {
if (!el || !el.attributes) return [];
var r = [];
for (var i = 0; i < el.attributes.length; i++) {
r.push([el.attributes[i].name, el.attributes[i].value]);
}
return r;
}
function domInsertBefore(parent, node, ref) {
if (parent && node) parent.insertBefore(node, ref || null);
}
function domInsertAfter(ref, node) {
if (ref && ref.parentNode && node) {
ref.parentNode.insertBefore(node, ref.nextSibling);
}
}
function domRemoveChild(parent, child) {
if (parent && child && child.parentNode === parent) parent.removeChild(child);
}
function domReplaceChild(parent, newChild, oldChild) {
if (parent && newChild && oldChild) parent.replaceChild(newChild, oldChild);
}
function domSetInnerHtml(el, html) {
if (el) el.innerHTML = html;
}
function domInsertAdjacentHtml(el, pos, html) {
if (el && el.insertAdjacentHTML) el.insertAdjacentHTML(pos, html);
}
function domGetStyle(el, prop) {
return el && el.style ? el.style[prop] || "" : "";
}
function domSetStyle(el, prop, val) {
if (el && el.style) el.style[prop] = val;
}
function domGetProp(el, name) { return el ? el[name] : NIL; }
function domSetProp(el, name, val) { if (el) el[name] = val; }
function domAddClass(el, cls) {
if (el && el.classList) el.classList.add(cls);
}
function domRemoveClass(el, cls) {
if (el && el.classList) el.classList.remove(cls);
}
function domDispatch(el, name, detail) {
if (!_hasDom || !el) return false;
var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} });
return el.dispatchEvent(evt);
}
function domQuery(sel) {
return _hasDom ? document.querySelector(sel) : null;
}
function domQueryAll(root, sel) {
if (!root || !root.querySelectorAll) return [];
return Array.prototype.slice.call(root.querySelectorAll(sel));
}
function domTagName(el) { return el && el.tagName ? el.tagName : ""; }
"""
PLATFORM_ENGINE_JS = """
// =========================================================================
// Platform interface — Engine (browser-only)
// =========================================================================
function browserLocationHref() {
return typeof location !== "undefined" ? location.href : "";
}
function browserSameOrigin(url) {
try { return new URL(url, location.href).origin === location.origin; }
catch (e) { return true; }
}
function browserPushState(url) {
if (typeof history !== "undefined") {
try { history.pushState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); }
catch (e) {}
}
}
function browserReplaceState(url) {
if (typeof history !== "undefined") {
try { history.replaceState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); }
catch (e) {}
}
}
function browserNavigate(url) {
if (typeof location !== "undefined") location.assign(url);
}
function browserReload() {
if (typeof location !== "undefined") location.reload();
}
function browserScrollTo(x, y) {
if (typeof window !== "undefined") window.scrollTo(x, y);
}
function browserMediaMatches(query) {
if (typeof window === "undefined") return false;
return window.matchMedia(query).matches;
}
function browserConfirm(msg) {
if (typeof window === "undefined") return false;
return window.confirm(msg);
}
function browserPrompt(msg) {
if (typeof window === "undefined") return NIL;
var r = window.prompt(msg);
return r === null ? NIL : r;
}
function nowMs() { return Date.now(); }
function parseHeaderValue(s) {
if (!s) return null;
try {
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
return JSON.parse(s);
} catch (e) { return null; }
}
"""
def fixups_js(has_html, has_sx, has_dom):
lines = ['''
// =========================================================================
// Post-transpilation fixups
// =========================================================================
@@ -991,29 +1435,31 @@ FIXUPS = '''
return _rawCallLambda(f, args, callerEnv);
};
// Expose render functions as primitives so SX code can call them
if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml;
if (typeof renderToSx === "function") PRIMITIVES["render-to-sx"] = renderToSx;
if (typeof aser === "function") PRIMITIVES["aser"] = aser;'''
// Expose render functions as primitives so SX code can call them''']
if has_html:
lines.append(' if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml;')
if has_sx:
lines.append(' if (typeof renderToSx === "function") PRIMITIVES["render-to-sx"] = renderToSx;')
lines.append(' if (typeof aser === "function") PRIMITIVES["aser"] = aser;')
if has_dom:
lines.append(' if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;')
return "\n".join(lines)
PUBLIC_API = '''
// =========================================================================
// Parser (reused from reference — hand-written for bootstrap simplicity)
// =========================================================================
// The parser is the one piece we keep as hand-written JS since the
// reference parser.sx is more of a spec than directly compilable code
// (it uses mutable cursor state that doesn't map cleanly to the
// transpiler's functional output). A future version could bootstrap
// the parser too.
def public_api_js(has_html, has_sx, has_dom, has_engine, adapter_label):
# Parser is always included
parser = r'''
// =========================================================================
// Parser
// =========================================================================
function parse(text) {
var pos = 0;
function skipWs() {
while (pos < text.length) {
var ch = text[pos];
if (ch === " " || ch === "\\t" || ch === "\\n" || ch === "\\r") { pos++; continue; }
if (ch === ";") { while (pos < text.length && text[pos] !== "\\n") pos++; continue; }
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") { pos++; continue; }
if (ch === ";") { while (pos < text.length && text[pos] !== "\n") pos++; continue; }
break;
}
}
@@ -1024,7 +1470,7 @@ PUBLIC_API = '''
if (ch === "(") { pos++; return readList(")"); }
if (ch === "[") { pos++; return readList("]"); }
if (ch === "{") { pos++; return readMap(); }
if (ch === \'"\') return readString();
if (ch === '"') return readString();
if (ch === ":") return readKeyword();
if (ch === "`") { pos++; return [new Symbol("quasiquote"), readExpr()]; }
if (ch === ",") {
@@ -1061,8 +1507,8 @@ PUBLIC_API = '''
var s = "";
while (pos < text.length) {
var ch = text[pos];
if (ch === \'"\') { pos++; return s; }
if (ch === "\\\\") { pos++; var esc = text[pos]; s += esc === "n" ? "\\n" : esc === "t" ? "\\t" : esc === "r" ? "\\r" : esc; pos++; continue; }
if (ch === '"') { pos++; return s; }
if (ch === "\\") { pos++; var esc = text[pos]; s += esc === "n" ? "\n" : esc === "t" ? "\t" : esc === "r" ? "\r" : esc; pos++; continue; }
s += ch; pos++;
}
throw new Error("Unterminated string");
@@ -1086,7 +1532,7 @@ PUBLIC_API = '''
}
function readIdent() {
var start = pos;
while (pos < text.length && /[a-zA-Z0-9_~*+\\-><=/!?.:&]/.test(text[pos])) pos++;
while (pos < text.length && /[a-zA-Z0-9_~*+\-><=/!?.:&]/.test(text[pos])) pos++;
return text.slice(start, pos);
}
function readSymbol() {
@@ -1103,8 +1549,10 @@ PUBLIC_API = '''
exprs.push(readExpr());
}
return exprs;
}
}'''
# Public API — conditional on adapters
api_lines = [parser, '''
// =========================================================================
// Public API
// =========================================================================
@@ -1116,52 +1564,92 @@ PUBLIC_API = '''
for (var i = 0; i < exprs.length; i++) {
trampoline(evalExpr(exprs[i], componentEnv));
}
}
}''']
# render() — auto-dispatches based on available adapters
if has_html and has_dom:
api_lines.append('''
function render(source) {
if (!_hasDom) {
var exprs = parse(source);
var parts = [];
for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv)));
return parts.join("");
}
var exprs = parse(source);
var frag = document.createDocumentFragment();
for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null));
return frag;
}''')
elif has_dom:
api_lines.append('''
function render(source) {
var exprs = parse(source);
var frag = document.createDocumentFragment();
for (var i = 0; i < exprs.length; i++) {
var result = trampoline(evalExpr(exprs[i], merge(componentEnv)));
appendToDOM(frag, result, merge(componentEnv));
}
for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null));
return frag;
}
}''')
elif has_html:
api_lines.append('''
function render(source) {
var exprs = parse(source);
var parts = [];
for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv)));
return parts.join("");
}''')
else:
api_lines.append('''
function render(source) {
var exprs = parse(source);
var results = [];
for (var i = 0; i < exprs.length; i++) results.push(trampoline(evalExpr(exprs[i], merge(componentEnv))));
return results.length === 1 ? results[0] : results;
}''')
function appendToDOM(parent, val, env) {
if (isNil(val)) return;
if (typeof val === "string") { parent.appendChild(document.createTextNode(val)); return; }
if (typeof val === "number") { parent.appendChild(document.createTextNode(String(val))); return; }
if (val._raw) { var t = document.createElement("template"); t.innerHTML = val.html; parent.appendChild(t.content); return; }
if (Array.isArray(val)) {
// Could be a rendered element or a list of results
if (val.length > 0 && isSym(val[0])) {
// It's an unevaluated expression — evaluate it
var result = trampoline(evalExpr(val, env));
appendToDOM(parent, result, env);
} else {
for (var i = 0; i < val.length; i++) appendToDOM(parent, val[i], env);
}
return;
}
parent.appendChild(document.createTextNode(String(val)));
}
# renderToString helper
if has_html:
api_lines.append('''
function renderToString(source) {
var exprs = parse(source);
var parts = [];
for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv)));
return parts.join("");
}''')
var SxRef = {
# Build SxRef object
version = f"ref-2.0 ({adapter_label}, bootstrap-compiled)"
api_lines.append(f'''
var SxRef = {{
parse: parse,
eval: function(expr, env) { return trampoline(evalExpr(expr, env || merge(componentEnv))); },
eval: function(expr, env) {{ return trampoline(evalExpr(expr, env || merge(componentEnv))); }},
loadComponents: loadComponents,
render: render,
render: render,{"" if has_html else ""}
{"renderToString: renderToString," if has_html else ""}
serialize: serialize,
NIL: NIL,
Symbol: Symbol,
Keyword: Keyword,
componentEnv: componentEnv,
_version: "ref-1.0 (bootstrap-compiled)"
};
componentEnv: componentEnv,''')
if (typeof module !== "undefined" && module.exports) module.exports = SxRef;
else global.SxRef = SxRef;'''
if has_html:
api_lines.append(' renderToHtml: function(expr, env) { return renderToHtml(expr, env || merge(componentEnv)); },')
if has_sx:
api_lines.append(' renderToSx: function(expr, env) { return renderToSx(expr, env || merge(componentEnv)); },')
if has_dom:
api_lines.append(' renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,')
if has_engine:
api_lines.append(' parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,')
api_lines.append(' morphNode: typeof morphNode === "function" ? morphNode : null,')
api_lines.append(' morphChildren: typeof morphChildren === "function" ? morphChildren : null,')
api_lines.append(' swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,')
api_lines.append(f' _version: "{version}"')
api_lines.append(' };')
api_lines.append('')
api_lines.append(' if (typeof module !== "undefined" && module.exports) module.exports = SxRef;')
api_lines.append(' else global.SxRef = SxRef;')
return "\n".join(api_lines)
EPILOGUE = '''
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);'''
@@ -1172,4 +1660,22 @@ EPILOGUE = '''
# ---------------------------------------------------------------------------
if __name__ == "__main__":
print(compile_ref_to_js())
import argparse
p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript")
p.add_argument("--adapters", "-a",
help="Comma-separated adapter list (html,sx,dom,engine). Default: all")
p.add_argument("--output", "-o",
help="Output file (default: stdout)")
args = p.parse_args()
adapters = args.adapters.split(",") if args.adapters else None
js = compile_ref_to_js(adapters)
if args.output:
with open(args.output, "w") as f:
f.write(js)
included = ", ".join(adapters) if adapters else "all"
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included})",
file=sys.stderr)
else:
print(js)