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:
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||||
}
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user