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:
@@ -405,12 +405,23 @@
|
||||
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) {
|
||||
@@ -434,7 +445,46 @@
|
||||
"map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1
|
||||
}; }
|
||||
|
||||
// === Transpiled from eval.sx ===
|
||||
// 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;
|
||||
}
|
||||
|
||||
// === Transpiled from eval ===
|
||||
|
||||
// trampoline
|
||||
var trampoline = function(val) { return (function() {
|
||||
@@ -620,7 +670,7 @@
|
||||
{ var _c = paramsExpr; for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; if (isSxTruthy((typeOf(p) == "symbol"))) {
|
||||
(function() {
|
||||
var name = symbolName(p);
|
||||
return (isSxTruthy((name == "&key")) ? (inKey = true) : (isSxTruthy((name == "&rest")) ? (hasChildren = true) : (isSxTruthy((isSxTruthy(inKey) && !hasChildren)) ? append_b(params, name) : append_b(params, name))));
|
||||
return (isSxTruthy((name == "&key")) ? (inKey = true) : (isSxTruthy((name == "&rest")) ? (hasChildren = true) : (isSxTruthy((name == "&children")) ? (hasChildren = true) : (isSxTruthy(hasChildren) ? NIL : (isSxTruthy(inKey) ? append_b(params, name) : append_b(params, name))))));
|
||||
})();
|
||||
} } }
|
||||
return [params, hasChildren];
|
||||
@@ -765,7 +815,7 @@
|
||||
})(); };
|
||||
|
||||
|
||||
// === Transpiled from render.sx ===
|
||||
// === Transpiled from render (core) ===
|
||||
|
||||
// HTML_TAGS
|
||||
var HTML_TAGS = ["html", "head", "body", "title", "meta", "link", "script", "style", "noscript", "header", "nav", "main", "section", "article", "aside", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "div", "p", "blockquote", "pre", "figure", "figcaption", "address", "details", "summary", "a", "span", "em", "strong", "small", "b", "i", "u", "s", "mark", "sub", "sup", "abbr", "cite", "code", "time", "br", "wbr", "hr", "ul", "ol", "li", "dl", "dt", "dd", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption", "colgroup", "col", "form", "input", "textarea", "select", "option", "optgroup", "button", "label", "fieldset", "legend", "output", "datalist", "img", "video", "audio", "source", "picture", "canvas", "iframe", "svg", "math", "path", "circle", "ellipse", "rect", "line", "polyline", "polygon", "text", "tspan", "g", "defs", "use", "clipPath", "mask", "pattern", "linearGradient", "radialGradient", "stop", "filter", "feGaussianBlur", "feOffset", "feBlend", "feColorMatrix", "feComposite", "feMerge", "feMergeNode", "feTurbulence", "feComponentTransfer", "feFuncR", "feFuncG", "feFuncB", "feFuncA", "feDisplacementMap", "feFlood", "feImage", "feMorphology", "feSpecularLighting", "feDiffuseLighting", "fePointLight", "feSpotLight", "feDistantLight", "animate", "animateTransform", "foreignObject", "template", "slot", "dialog", "menu"];
|
||||
@@ -776,33 +826,8 @@
|
||||
// BOOLEAN_ATTRS
|
||||
var BOOLEAN_ATTRS = ["async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "inert", "ismap", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected"];
|
||||
|
||||
// render-to-html
|
||||
var renderToHtml = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); };
|
||||
|
||||
// render-value-to-html
|
||||
var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); if (_m == "style-value") return styleValueClass(val); return escapeHtml((String(val))); })(); };
|
||||
|
||||
// render-list-to-html
|
||||
var renderListToHtml = function(expr, env) { return (isSxTruthy(isEmpty(expr)) ? "" : (function() {
|
||||
var head = first(expr);
|
||||
return (isSxTruthy(!(typeOf(head) == "symbol")) ? join("", map(function(x) { return renderValueToHtml(x, env); }, expr)) : (function() {
|
||||
var name = symbolName(head);
|
||||
var args = rest(expr);
|
||||
return (isSxTruthy((name == "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy((name == "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() {
|
||||
var comp = envGet(env, name);
|
||||
return (isSxTruthy(isComponent(comp)) ? renderToHtml(trampoline(callComponent(comp, args, env)), env) : error((String("Unknown component: ") + String(name))));
|
||||
})() : (isSxTruthy(sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defkeyframes"), (name == "defhandler"))) ? (trampoline(evalExpr(expr, env)), "") : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(trampoline(evalExpr(expandMacro(envGet(env, name), args, env), env)), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))));
|
||||
})());
|
||||
})()); };
|
||||
|
||||
// render-html-element
|
||||
var renderHtmlElement = function(tag, args, env) { return (function() {
|
||||
var parsed = parseElementArgs(args, env);
|
||||
var attrs = first(parsed);
|
||||
var children = nth(parsed, 1);
|
||||
var isVoid = contains(VOID_ELEMENTS, tag);
|
||||
return (String("<") + String(tag) + String(renderAttrs(attrs)) + String((isSxTruthy(isVoid) ? " />" : (String(">") + String(join("", map(function(c) { return renderToHtml(c, env); }, children))) + String("</") + String(tag) + String(">")))));
|
||||
})(); };
|
||||
// definition-form?
|
||||
var isDefinitionForm = function(name) { return sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defkeyframes"), (name == "defhandler")); };
|
||||
|
||||
// parse-element-args
|
||||
var parseElementArgs = function(args, env) { return (function() {
|
||||
@@ -810,7 +835,7 @@
|
||||
var children = [];
|
||||
reduce(function(state, arg) { return (function() {
|
||||
var skip = get(state, "skip");
|
||||
return (isSxTruthy(skip) ? assoc(state, "skip", false) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
|
||||
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
|
||||
var val = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
|
||||
attrs[keywordName(arg)] = val;
|
||||
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
||||
@@ -825,10 +850,103 @@
|
||||
return (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && val)) ? (String(" ") + String(key)) : (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && !val)) ? "" : (isSxTruthy(isNil(val)) ? "" : (isSxTruthy((isSxTruthy((key == "style")) && isStyleValue(val))) ? (String(" class=\"") + String(styleValueClass(val)) + String("\"")) : (String(" ") + String(key) + String("=\"") + String(escapeAttr((String(val)))) + String("\""))))));
|
||||
})(); }, keys(attrs))); };
|
||||
|
||||
|
||||
// === Transpiled from adapter-html ===
|
||||
|
||||
// render-to-html
|
||||
var renderToHtml = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); };
|
||||
|
||||
// render-value-to-html
|
||||
var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); if (_m == "style-value") return styleValueClass(val); return escapeHtml((String(val))); })(); };
|
||||
|
||||
// RENDER_HTML_FORMS
|
||||
var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defmacro", "defstyle", "defkeyframes", "defhandler", "map", "map-indexed", "filter", "for-each"];
|
||||
|
||||
// render-html-form?
|
||||
var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); };
|
||||
|
||||
// render-list-to-html
|
||||
var renderListToHtml = function(expr, env) { return (isSxTruthy(isEmpty(expr)) ? "" : (function() {
|
||||
var head = first(expr);
|
||||
return (isSxTruthy(!(typeOf(head) == "symbol")) ? join("", map(function(x) { return renderValueToHtml(x, env); }, expr)) : (function() {
|
||||
var name = symbolName(head);
|
||||
var args = rest(expr);
|
||||
return (isSxTruthy((name == "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy((name == "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() {
|
||||
var val = envGet(env, name);
|
||||
return (isSxTruthy(isComponent(val)) ? renderHtmlComponent(val, args, env) : (isSxTruthy(isMacro(val)) ? renderToHtml(expandMacro(val, args, env), env) : error((String("Unknown component: ") + String(name)))));
|
||||
})() : (isSxTruthy(isRenderHtmlForm(name)) ? dispatchHtmlForm(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(expandMacro(envGet(env, name), args, env), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))));
|
||||
})());
|
||||
})()); };
|
||||
|
||||
// dispatch-html-form
|
||||
var dispatchHtmlForm = function(name, expr, env) { return (isSxTruthy((name == "if")) ? (function() {
|
||||
var condVal = trampoline(evalExpr(nth(expr, 1), env));
|
||||
return (isSxTruthy(condVal) ? renderToHtml(nth(expr, 2), env) : (isSxTruthy((len(expr) > 3)) ? renderToHtml(nth(expr, 3), env) : ""));
|
||||
})() : (isSxTruthy((name == "when")) ? (isSxTruthy(!trampoline(evalExpr(nth(expr, 1), env))) ? "" : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(2, len(expr))))) : (isSxTruthy((name == "cond")) ? (function() {
|
||||
var branch = evalCond(rest(expr), env);
|
||||
return (isSxTruthy(branch) ? renderToHtml(branch, env) : "");
|
||||
})() : (isSxTruthy((name == "case")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() {
|
||||
var local = processBindings(nth(expr, 1), env);
|
||||
return join("", map(function(i) { return renderToHtml(nth(expr, i), local); }, range(2, len(expr))));
|
||||
})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(1, len(expr)))) : (isSxTruthy(isDefinitionForm(name)) ? (trampoline(evalExpr(expr, env)), "") : (isSxTruthy((name == "map")) ? (function() {
|
||||
var f = trampoline(evalExpr(nth(expr, 1), env));
|
||||
var coll = trampoline(evalExpr(nth(expr, 2), env));
|
||||
return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll));
|
||||
})() : (isSxTruthy((name == "map-indexed")) ? (function() {
|
||||
var f = trampoline(evalExpr(nth(expr, 1), env));
|
||||
var coll = trampoline(evalExpr(nth(expr, 2), env));
|
||||
return join("", mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [i, item], env) : renderToHtml(apply(f, [i, item]), env)); }, coll));
|
||||
})() : (isSxTruthy((name == "filter")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy((name == "for-each")) ? (function() {
|
||||
var f = trampoline(evalExpr(nth(expr, 1), env));
|
||||
var coll = trampoline(evalExpr(nth(expr, 2), env));
|
||||
return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll));
|
||||
})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))))); };
|
||||
|
||||
// render-lambda-html
|
||||
var renderLambdaHtml = function(f, args, env) { return (function() {
|
||||
var local = envMerge(lambdaClosure(f), env);
|
||||
forEachIndexed(function(i, p) { return envSet(local, p, nth(args, i)); }, lambdaParams(f));
|
||||
return renderToHtml(lambdaBody(f), local);
|
||||
})(); };
|
||||
|
||||
// render-html-component
|
||||
var renderHtmlComponent = function(comp, args, env) { return (function() {
|
||||
var kwargs = {};
|
||||
var children = [];
|
||||
reduce(function(state, arg) { return (function() {
|
||||
var skip = get(state, "skip");
|
||||
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
|
||||
var val = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
|
||||
kwargs[keywordName(arg)] = val;
|
||||
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
||||
})() : (append_b(children, arg), assoc(state, "i", (get(state, "i") + 1)))));
|
||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||
return (function() {
|
||||
var local = envMerge(componentClosure(comp), env);
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
|
||||
if (isSxTruthy(componentHasChildren(comp))) {
|
||||
local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)));
|
||||
}
|
||||
return renderToHtml(componentBody(comp), local);
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// render-html-element
|
||||
var renderHtmlElement = function(tag, args, env) { return (function() {
|
||||
var parsed = parseElementArgs(args, env);
|
||||
var attrs = first(parsed);
|
||||
var children = nth(parsed, 1);
|
||||
var isVoid = contains(VOID_ELEMENTS, tag);
|
||||
return (String("<") + String(tag) + String(renderAttrs(attrs)) + String((isSxTruthy(isVoid) ? " />" : (String(">") + String(join("", map(function(c) { return renderToHtml(c, env); }, children))) + String("</") + String(tag) + String(">")))));
|
||||
})(); };
|
||||
|
||||
|
||||
// === Transpiled from adapter-sx ===
|
||||
|
||||
// render-to-sx
|
||||
var renderToSx = function(expr, env) { return (function() {
|
||||
var result = aser(expr, env);
|
||||
return serialize(result);
|
||||
return (isSxTruthy((typeOf(result) == "string")) ? result : serialize(result));
|
||||
})(); };
|
||||
|
||||
// aser
|
||||
@@ -862,7 +980,7 @@
|
||||
var parts = [name];
|
||||
reduce(function(state, arg) { return (function() {
|
||||
var skip = get(state, "skip");
|
||||
return (isSxTruthy(skip) ? assoc(state, "skip", false) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
|
||||
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
|
||||
var val = aser(nth(args, (get(state, "i") + 1)), env);
|
||||
if (isSxTruthy(!isNil(val))) {
|
||||
parts.push((String(":") + String(keywordName(arg))));
|
||||
@@ -881,6 +999,628 @@
|
||||
})(); };
|
||||
|
||||
|
||||
// === Transpiled from adapter-dom ===
|
||||
|
||||
// SVG_NS
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
// MATH_NS
|
||||
var MATH_NS = "http://www.w3.org/1998/Math/MathML";
|
||||
|
||||
// render-to-dom
|
||||
var renderToDom = function(expr, env, ns) { return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); if (_m == "style-value") return createTextNode(styleValueClass(expr)); return createTextNode((String(expr))); })(); };
|
||||
|
||||
// render-dom-list
|
||||
var renderDomList = function(expr, env, ns) { return (function() {
|
||||
var head = first(expr);
|
||||
return (isSxTruthy((typeOf(head) == "symbol")) ? (function() {
|
||||
var name = symbolName(head);
|
||||
var args = rest(expr);
|
||||
return (isSxTruthy((name == "raw!")) ? renderDomRaw(args, env) : (isSxTruthy((name == "<>")) ? renderDomFragment(args, env, ns) : (isSxTruthy(startsWith(name, "html:")) ? renderDomElement(slice(name, 5), args, env, ns) : (isSxTruthy(isRenderDomForm(name)) ? (isSxTruthy((isSxTruthy(contains(HTML_TAGS, name)) && sxOr((isSxTruthy((len(args) > 0)) && (typeOf(first(args)) == "keyword")), ns))) ? renderDomElement(name, args, env, ns) : dispatchRenderForm(name, expr, env, ns)) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToDom(expandMacro(envGet(env, name), args, env), env, ns) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderDomElement(name, args, env, ns) : (isSxTruthy(startsWith(name, "~")) ? (function() {
|
||||
var comp = envGet(env, name);
|
||||
return (isSxTruthy(isComponent(comp)) ? renderDomComponent(comp, args, env, ns) : renderDomUnknownComponent(name));
|
||||
})() : (isSxTruthy((isSxTruthy((indexOf_(name, "-") > 0)) && isSxTruthy((len(args) > 0)) && (typeOf(first(args)) == "keyword"))) ? renderDomElement(name, args, env, ns) : (isSxTruthy(ns) ? renderDomElement(name, args, env, ns) : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))));
|
||||
})() : (isSxTruthy(sxOr(isLambda(head), (typeOf(head) == "list"))) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (function() {
|
||||
var frag = createFragment();
|
||||
{ var _c = expr; for (var _i = 0; _i < _c.length; _i++) { var x = _c[_i]; domAppend(frag, renderToDom(x, env, ns)); } }
|
||||
return frag;
|
||||
})()));
|
||||
})(); };
|
||||
|
||||
// render-dom-element
|
||||
var renderDomElement = function(tag, args, env, ns) { return (function() {
|
||||
var newNs = (isSxTruthy((tag == "svg")) ? SVG_NS : (isSxTruthy((tag == "math")) ? MATH_NS : ns));
|
||||
var el = domCreateElement(tag, newNs);
|
||||
var extraClass = NIL;
|
||||
reduce(function(state, arg) { return (function() {
|
||||
var skip = get(state, "skip");
|
||||
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
|
||||
var attrName = keywordName(arg);
|
||||
var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
|
||||
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy((attrName == "style")) && isStyleValue(attrVal))) ? (extraClass = styleValueClass(attrVal)) : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal)))))));
|
||||
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
||||
})() : ((isSxTruthy(!contains(VOID_ELEMENTS, tag)) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1)))));
|
||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||
if (isSxTruthy(extraClass)) {
|
||||
(function() {
|
||||
var existing = domGetAttr(el, "class");
|
||||
return domSetAttr(el, "class", (isSxTruthy(existing) ? (String(existing) + String(" ") + String(extraClass)) : extraClass));
|
||||
})();
|
||||
}
|
||||
return el;
|
||||
})(); };
|
||||
|
||||
// render-dom-component
|
||||
var renderDomComponent = function(comp, args, env, ns) { return (function() {
|
||||
var kwargs = {};
|
||||
var children = [];
|
||||
reduce(function(state, arg) { return (function() {
|
||||
var skip = get(state, "skip");
|
||||
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
|
||||
var val = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
|
||||
kwargs[keywordName(arg)] = val;
|
||||
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
||||
})() : (append_b(children, arg), assoc(state, "i", (get(state, "i") + 1)))));
|
||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||
return (function() {
|
||||
var local = envMerge(componentClosure(comp), env);
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
|
||||
if (isSxTruthy(componentHasChildren(comp))) {
|
||||
(function() {
|
||||
var childFrag = createFragment();
|
||||
{ var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; domAppend(childFrag, renderToDom(c, env, ns)); } }
|
||||
return envSet(local, "children", childFrag);
|
||||
})();
|
||||
}
|
||||
return renderToDom(componentBody(comp), local, ns);
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// render-dom-fragment
|
||||
var renderDomFragment = function(args, env, ns) { return (function() {
|
||||
var frag = createFragment();
|
||||
{ var _c = args; for (var _i = 0; _i < _c.length; _i++) { var x = _c[_i]; domAppend(frag, renderToDom(x, env, ns)); } }
|
||||
return frag;
|
||||
})(); };
|
||||
|
||||
// render-dom-raw
|
||||
var renderDomRaw = function(args, env) { return (function() {
|
||||
var frag = createFragment();
|
||||
{ var _c = args; for (var _i = 0; _i < _c.length; _i++) { var arg = _c[_i]; (function() {
|
||||
var val = trampoline(evalExpr(arg, env));
|
||||
return (isSxTruthy((typeOf(val) == "string")) ? domAppend(frag, domParseHtml(val)) : (isSxTruthy((typeOf(val) == "dom-node")) ? domAppend(frag, domClone(val)) : (isSxTruthy(!isNil(val)) ? domAppend(frag, createTextNode((String(val)))) : NIL)));
|
||||
})(); } }
|
||||
return frag;
|
||||
})(); };
|
||||
|
||||
// render-dom-unknown-component
|
||||
var renderDomUnknownComponent = function(name) { return (function() {
|
||||
var el = domCreateElement("div", NIL);
|
||||
domSetAttr(el, "style", "background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;padding:4px 8px;margin:2px;border-radius:4px;font-size:12px;font-family:monospace");
|
||||
domAppend(el, createTextNode((String("Unknown component: ") + String(name))));
|
||||
return el;
|
||||
})(); };
|
||||
|
||||
// RENDER_DOM_FORMS
|
||||
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defmacro", "defstyle", "defkeyframes", "defhandler", "map", "map-indexed", "filter", "for-each"];
|
||||
|
||||
// render-dom-form?
|
||||
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
|
||||
|
||||
// dispatch-render-form
|
||||
var dispatchRenderForm = function(name, expr, env, ns) { return (isSxTruthy((name == "if")) ? (function() {
|
||||
var condVal = trampoline(evalExpr(nth(expr, 1), env));
|
||||
return (isSxTruthy(condVal) ? renderToDom(nth(expr, 2), env, ns) : (isSxTruthy((len(expr) > 3)) ? renderToDom(nth(expr, 3), env, ns) : createFragment()));
|
||||
})() : (isSxTruthy((name == "when")) ? (isSxTruthy(!trampoline(evalExpr(nth(expr, 1), env))) ? createFragment() : (function() {
|
||||
var frag = createFragment();
|
||||
{ var _c = range(2, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), env, ns)); } }
|
||||
return frag;
|
||||
})()) : (isSxTruthy((name == "cond")) ? (function() {
|
||||
var branch = evalCond(rest(expr), env);
|
||||
return (isSxTruthy(branch) ? renderToDom(branch, env, ns) : createFragment());
|
||||
})() : (isSxTruthy((name == "case")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() {
|
||||
var local = processBindings(nth(expr, 1), env);
|
||||
var frag = createFragment();
|
||||
{ var _c = range(2, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), local, ns)); } }
|
||||
return frag;
|
||||
})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? (function() {
|
||||
var frag = createFragment();
|
||||
{ var _c = range(1, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), env, ns)); } }
|
||||
return frag;
|
||||
})() : (isSxTruthy(isDefinitionForm(name)) ? (trampoline(evalExpr(expr, env)), createFragment()) : (isSxTruthy((name == "map")) ? (function() {
|
||||
var f = trampoline(evalExpr(nth(expr, 1), env));
|
||||
var coll = trampoline(evalExpr(nth(expr, 2), env));
|
||||
var frag = createFragment();
|
||||
{ var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var item = _c[_i]; (function() {
|
||||
var val = (isSxTruthy(isLambda(f)) ? renderLambdaDom(f, [item], env, ns) : renderToDom(apply(f, [item]), env, ns));
|
||||
return domAppend(frag, val);
|
||||
})(); } }
|
||||
return frag;
|
||||
})() : (isSxTruthy((name == "map-indexed")) ? (function() {
|
||||
var f = trampoline(evalExpr(nth(expr, 1), env));
|
||||
var coll = trampoline(evalExpr(nth(expr, 2), env));
|
||||
var frag = createFragment();
|
||||
forEachIndexed(function(i, item) { return (function() {
|
||||
var val = (isSxTruthy(isLambda(f)) ? renderLambdaDom(f, [i, item], env, ns) : renderToDom(apply(f, [i, item]), env, ns));
|
||||
return domAppend(frag, val);
|
||||
})(); }, coll);
|
||||
return frag;
|
||||
})() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
|
||||
var f = trampoline(evalExpr(nth(expr, 1), env));
|
||||
var coll = trampoline(evalExpr(nth(expr, 2), env));
|
||||
var frag = createFragment();
|
||||
{ var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var item = _c[_i]; (function() {
|
||||
var val = (isSxTruthy(isLambda(f)) ? renderLambdaDom(f, [item], env, ns) : renderToDom(apply(f, [item]), env, ns));
|
||||
return domAppend(frag, val);
|
||||
})(); } }
|
||||
return frag;
|
||||
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))); };
|
||||
|
||||
// render-lambda-dom
|
||||
var renderLambdaDom = function(f, args, env, ns) { return (function() {
|
||||
var local = envMerge(lambdaClosure(f), env);
|
||||
forEachIndexed(function(i, p) { return envSet(local, p, nth(args, i)); }, lambdaParams(f));
|
||||
return renderToDom(lambdaBody(f), local, ns);
|
||||
})(); };
|
||||
|
||||
|
||||
// === Transpiled from engine ===
|
||||
|
||||
// ENGINE_VERBS
|
||||
var ENGINE_VERBS = ["get", "post", "put", "delete", "patch"];
|
||||
|
||||
// DEFAULT_SWAP
|
||||
var DEFAULT_SWAP = "outerHTML";
|
||||
|
||||
// parse-time
|
||||
var parseTime = function(s) { return (isSxTruthy(isNil(s)) ? 0 : (isSxTruthy(endsWith(s, "ms")) ? parseInt_(s, 0) : (isSxTruthy(endsWith(s, "s")) ? (parseInt_(replace_(s, "s", ""), 0) * 1000) : parseInt_(s, 0)))); };
|
||||
|
||||
// parse-trigger-spec
|
||||
var parseTriggerSpec = function(spec) { return (isSxTruthy(isNil(spec)) ? NIL : (function() {
|
||||
var rawParts = split(spec, ",");
|
||||
return filter(function(x) { return !isNil(x); }, map(function(part) { return (function() {
|
||||
var tokens = split(trim(part), " ");
|
||||
return (isSxTruthy(isEmpty(tokens)) ? NIL : (isSxTruthy((isSxTruthy((first(tokens) == "every")) && (len(tokens) >= 2))) ? {["event"]: "every", ["modifiers"]: {["interval"]: parseTime(nth(tokens, 1))}} : (function() {
|
||||
var mods = {};
|
||||
{ var _c = rest(tokens); for (var _i = 0; _i < _c.length; _i++) { var tok = _c[_i]; (isSxTruthy((tok == "once")) ? dictSet(mods, "once", true) : (isSxTruthy((tok == "changed")) ? dictSet(mods, "changed", true) : (isSxTruthy(startsWith(tok, "delay:")) ? dictSet(mods, "delay", parseTime(slice(tok, 6))) : (isSxTruthy(startsWith(tok, "from:")) ? dictSet(mods, "from", slice(tok, 5)) : NIL)))); } }
|
||||
return {["event"]: first(tokens), ["modifiers"]: mods};
|
||||
})()));
|
||||
})(); }, rawParts));
|
||||
})()); };
|
||||
|
||||
// default-trigger
|
||||
var defaultTrigger = function(tagName) { return (isSxTruthy((tagName == "FORM")) ? [{["event"]: "submit", ["modifiers"]: {}}] : (isSxTruthy(sxOr((tagName == "INPUT"), (tagName == "SELECT"), (tagName == "TEXTAREA"))) ? [{["event"]: "change", ["modifiers"]: {}}] : [{["event"]: "click", ["modifiers"]: {}}])); };
|
||||
|
||||
// get-verb-info
|
||||
var getVerbInfo = function(el) { return some(function(verb) { return (function() {
|
||||
var url = domGetAttr(el, (String("sx-") + String(verb)));
|
||||
return (isSxTruthy(url) ? {["method"]: upper(verb), ["url"]: url} : NIL);
|
||||
})(); }, ENGINE_VERBS); };
|
||||
|
||||
// build-request-headers
|
||||
var buildRequestHeaders = function(el, loadedComponents, cssHash) { return (function() {
|
||||
var headers = {["SX-Request"]: "true", ["SX-Current-URL"]: browserLocationHref()};
|
||||
(function() {
|
||||
var targetSel = domGetAttr(el, "sx-target");
|
||||
return (isSxTruthy(targetSel) ? dictSet(headers, "SX-Target", targetSel) : NIL);
|
||||
})();
|
||||
if (isSxTruthy(!isEmpty(loadedComponents))) {
|
||||
headers["SX-Components"] = join(",", loadedComponents);
|
||||
}
|
||||
if (isSxTruthy(cssHash)) {
|
||||
headers["SX-Css"] = cssHash;
|
||||
}
|
||||
(function() {
|
||||
var extraH = domGetAttr(el, "sx-headers");
|
||||
return (isSxTruthy(extraH) ? (function() {
|
||||
var parsed = parseHeaderValue(extraH);
|
||||
return (isSxTruthy(parsed) ? forEach(function(key) { return dictSet(headers, key, (String(get(parsed, key)))); }, keys(parsed)) : NIL);
|
||||
})() : NIL);
|
||||
})();
|
||||
return headers;
|
||||
})(); };
|
||||
|
||||
// process-response-headers
|
||||
var processResponseHeaders = function(getHeader) { return {["redirect"]: getHeader("SX-Redirect"), ["refresh"]: getHeader("SX-Refresh"), ["trigger"]: getHeader("SX-Trigger"), ["retarget"]: getHeader("SX-Retarget"), ["reswap"]: getHeader("SX-Reswap"), ["location"]: getHeader("SX-Location"), ["replace-url"]: getHeader("SX-Replace-Url"), ["css-hash"]: getHeader("SX-Css-Hash"), ["trigger-swap"]: getHeader("SX-Trigger-After-Swap"), ["trigger-settle"]: getHeader("SX-Trigger-After-Settle"), ["content-type"]: getHeader("Content-Type")}; };
|
||||
|
||||
// parse-swap-spec
|
||||
var parseSwapSpec = function(rawSwap, globalTransitions_p) { return (function() {
|
||||
var parts = split(sxOr(rawSwap, DEFAULT_SWAP), " ");
|
||||
var style = first(parts);
|
||||
var useTransition = globalTransitions_p;
|
||||
{ var _c = rest(parts); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; (isSxTruthy((p == "transition:true")) ? (useTransition = true) : (isSxTruthy((p == "transition:false")) ? (useTransition = false) : NIL)); } }
|
||||
return {["style"]: style, ["transition"]: useTransition};
|
||||
})(); };
|
||||
|
||||
// parse-retry-spec
|
||||
var parseRetrySpec = function(retryAttr) { return (isSxTruthy(isNil(retryAttr)) ? NIL : (function() {
|
||||
var parts = split(retryAttr, ":");
|
||||
return {["strategy"]: first(parts), ["start-ms"]: parseInt_(nth(parts, 1), 1000), ["cap-ms"]: parseInt_(nth(parts, 2), 30000)};
|
||||
})()); };
|
||||
|
||||
// next-retry-ms
|
||||
var nextRetryMs = function(currentMs, capMs) { return min((currentMs * 2), capMs); };
|
||||
|
||||
// filter-params
|
||||
var filterParams = function(paramsSpec, allParams) { return (isSxTruthy(isNil(paramsSpec)) ? allParams : (isSxTruthy((paramsSpec == "none")) ? [] : (isSxTruthy((paramsSpec == "*")) ? allParams : (isSxTruthy(startsWith(paramsSpec, "not ")) ? (function() {
|
||||
var excluded = map(trim, split(slice(paramsSpec, 4), ","));
|
||||
return filter(function(p) { return !contains(excluded, first(p)); }, allParams);
|
||||
})() : (function() {
|
||||
var allowed = map(trim, split(paramsSpec, ","));
|
||||
return filter(function(p) { return contains(allowed, first(p)); }, allParams);
|
||||
})())))); };
|
||||
|
||||
// resolve-target
|
||||
var resolveTarget = function(el) { return (function() {
|
||||
var sel = domGetAttr(el, "sx-target");
|
||||
return (isSxTruthy(sxOr(isNil(sel), (sel == "this"))) ? el : (isSxTruthy((sel == "closest")) ? domParent(el) : domQuery(sel)));
|
||||
})(); };
|
||||
|
||||
// apply-optimistic
|
||||
var applyOptimistic = function(el) { return (function() {
|
||||
var directive = domGetAttr(el, "sx-optimistic");
|
||||
return (isSxTruthy(isNil(directive)) ? NIL : (function() {
|
||||
var target = sxOr(resolveTarget(el), el);
|
||||
var state = {["target"]: target, ["directive"]: directive};
|
||||
(isSxTruthy((directive == "remove")) ? (dictSet(state, "opacity", domGetStyle(target, "opacity")), domSetStyle(target, "opacity", "0"), domSetStyle(target, "pointer-events", "none")) : (isSxTruthy((directive == "disable")) ? (dictSet(state, "disabled", domGetProp(target, "disabled")), domSetProp(target, "disabled", true)) : (isSxTruthy(startsWith(directive, "add-class:")) ? (function() {
|
||||
var cls = slice(directive, 10);
|
||||
state["add-class"] = cls;
|
||||
return domAddClass(target, cls);
|
||||
})() : NIL)));
|
||||
return state;
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// revert-optimistic
|
||||
var revertOptimistic = function(state) { return (isSxTruthy(state) ? (function() {
|
||||
var target = get(state, "target");
|
||||
var directive = get(state, "directive");
|
||||
return (isSxTruthy((directive == "remove")) ? (domSetStyle(target, "opacity", sxOr(get(state, "opacity"), "")), domSetStyle(target, "pointer-events", "")) : (isSxTruthy((directive == "disable")) ? domSetProp(target, "disabled", sxOr(get(state, "disabled"), false)) : (isSxTruthy(get(state, "add-class")) ? domRemoveClass(target, get(state, "add-class")) : NIL)));
|
||||
})() : NIL); };
|
||||
|
||||
// find-oob-swaps
|
||||
var findOobSwaps = function(container) { return (function() {
|
||||
var results = [];
|
||||
{ var _c = ["sx-swap-oob", "hx-swap-oob"]; for (var _i = 0; _i < _c.length; _i++) { var attr = _c[_i]; (function() {
|
||||
var oobEls = domQueryAll(container, (String("[") + String(attr) + String("]")));
|
||||
return forEach(function(oob) { return (function() {
|
||||
var swapType = sxOr(domGetAttr(oob, attr), "outerHTML");
|
||||
var targetId = domId(oob);
|
||||
domRemoveAttr(oob, attr);
|
||||
return (isSxTruthy(targetId) ? append_b(results, {["element"]: oob, ["swap-type"]: swapType, ["target-id"]: targetId}) : NIL);
|
||||
})(); }, oobEls);
|
||||
})(); } }
|
||||
return results;
|
||||
})(); };
|
||||
|
||||
// morph-node
|
||||
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 name = first(attr);
|
||||
var val = nth(attr, 1);
|
||||
return (isSxTruthy(!(domGetAttr(oldEl, name) == val)) ? domSetAttr(oldEl, name, val) : NIL);
|
||||
})(); }, domAttrList(newEl)); };
|
||||
|
||||
// morph-children
|
||||
var morphChildren = function(oldParent, newParent) { return (function() {
|
||||
var oldKids = domChildList(oldParent);
|
||||
var newKids = domChildList(newParent);
|
||||
var oldById = reduce(function(acc, kid) { return (function() {
|
||||
var id = domId(kid);
|
||||
return (isSxTruthy(id) ? (dictSet(acc, id, kid), acc) : acc);
|
||||
})(); }, {}, oldKids);
|
||||
var oi = 0;
|
||||
{ var _c = newKids; for (var _i = 0; _i < _c.length; _i++) { var newChild = _c[_i]; (function() {
|
||||
var matchId = domId(newChild);
|
||||
var matchById = (isSxTruthy(matchId) ? dictGet(oldById, matchId) : NIL);
|
||||
return (isSxTruthy((isSxTruthy(matchById) && !isNil(matchById))) ? ((isSxTruthy((isSxTruthy((oi < len(oldKids))) && !(matchById == nth(oldKids, oi)))) ? domInsertBefore(oldParent, matchById, (isSxTruthy((oi < len(oldKids))) ? nth(oldKids, oi) : NIL)) : NIL), morphNode(matchById, newChild), (oi = (oi + 1))) : (isSxTruthy((oi < len(oldKids))) ? (function() {
|
||||
var oldChild = nth(oldKids, oi);
|
||||
return (isSxTruthy((isSxTruthy(domId(oldChild)) && !matchId)) ? domInsertBefore(oldParent, domClone(newChild), oldChild) : (morphNode(oldChild, newChild), (oi = (oi + 1))));
|
||||
})() : domAppend(oldParent, domClone(newChild))));
|
||||
})(); } }
|
||||
return forEach(function(i) { return (isSxTruthy((i >= oi)) ? (function() {
|
||||
var leftover = nth(oldKids, i);
|
||||
return (isSxTruthy((isSxTruthy(domIsChildOf(leftover, oldParent)) && isSxTruthy(!domHasAttr(leftover, "sx-preserve")) && !domHasAttr(leftover, "sx-ignore"))) ? domRemoveChild(oldParent, leftover) : NIL);
|
||||
})() : NIL); }, range(oi, len(oldKids)));
|
||||
})(); };
|
||||
|
||||
// swap-dom-nodes
|
||||
var swapDomNodes = function(target, newNodes, strategy) { return (function() { var _m = strategy; if (_m == "innerHTML") return (isSxTruthy(domIsFragment(newNodes)) ? morphChildren(target, newNodes) : (function() {
|
||||
var wrapper = domCreateElement("div", NIL);
|
||||
domAppend(wrapper, newNodes);
|
||||
return morphChildren(target, wrapper);
|
||||
})()); if (_m == "outerHTML") return (function() {
|
||||
var parent = domParent(target);
|
||||
(isSxTruthy(domIsFragment(newNodes)) ? (function() {
|
||||
var fc = domFirstChild(newNodes);
|
||||
return (isSxTruthy(fc) ? (morphNode(target, fc), (function() {
|
||||
var sib = domNextSibling(fc);
|
||||
return insertRemainingSiblings(parent, target, sib);
|
||||
})()) : domRemoveChild(parent, target));
|
||||
})() : morphNode(target, newNodes));
|
||||
return parent;
|
||||
})(); if (_m == "afterend") return domInsertAfter(target, newNodes); if (_m == "beforeend") return domAppend(target, newNodes); if (_m == "afterbegin") return domPrepend(target, newNodes); if (_m == "beforebegin") return domInsertBefore(domParent(target), newNodes, target); if (_m == "delete") return domRemoveChild(domParent(target), target); if (_m == "none") return NIL; return (isSxTruthy(domIsFragment(newNodes)) ? morphChildren(target, newNodes) : (function() {
|
||||
var wrapper = domCreateElement("div", NIL);
|
||||
domAppend(wrapper, newNodes);
|
||||
return morphChildren(target, wrapper);
|
||||
})()); })(); };
|
||||
|
||||
// insert-remaining-siblings
|
||||
var insertRemainingSiblings = function(parent, refNode, sib) { return (isSxTruthy(sib) ? (function() {
|
||||
var next = domNextSibling(sib);
|
||||
domInsertAfter(refNode, sib);
|
||||
return insertRemainingSiblings(parent, sib, next);
|
||||
})() : NIL); };
|
||||
|
||||
// swap-html-string
|
||||
var swapHtmlString = function(target, html, strategy) { return (function() { var _m = strategy; if (_m == "innerHTML") return domSetInnerHtml(target, html); if (_m == "outerHTML") return (function() {
|
||||
var parent = domParent(target);
|
||||
domInsertAdjacentHtml(target, "afterend", html);
|
||||
domRemoveChild(parent, target);
|
||||
return parent;
|
||||
})(); if (_m == "afterend") return domInsertAdjacentHtml(target, "afterend", html); if (_m == "beforeend") return domInsertAdjacentHtml(target, "beforeend", html); if (_m == "afterbegin") return domInsertAdjacentHtml(target, "afterbegin", html); if (_m == "beforebegin") return domInsertAdjacentHtml(target, "beforebegin", html); if (_m == "delete") return domRemoveChild(domParent(target), target); if (_m == "none") return NIL; return domSetInnerHtml(target, html); })(); };
|
||||
|
||||
// handle-history
|
||||
var handleHistory = function(el, url, respHeaders) { return (function() {
|
||||
var pushUrl = domGetAttr(el, "sx-push-url");
|
||||
var replaceUrl = domGetAttr(el, "sx-replace-url");
|
||||
var hdrReplace = get(respHeaders, "replace-url");
|
||||
return (isSxTruthy(hdrReplace) ? browserReplaceState(hdrReplace) : (isSxTruthy((isSxTruthy(pushUrl) && !(pushUrl == "false"))) ? browserPushState((isSxTruthy((pushUrl == "true")) ? url : pushUrl)) : (isSxTruthy((isSxTruthy(replaceUrl) && !(replaceUrl == "false"))) ? browserReplaceState((isSxTruthy((replaceUrl == "true")) ? url : replaceUrl)) : NIL)));
|
||||
})(); };
|
||||
|
||||
// PRELOAD_TTL
|
||||
var PRELOAD_TTL = 30000;
|
||||
|
||||
// preload-cache-get
|
||||
var preloadCacheGet = function(cache, url) { return (function() {
|
||||
var entry = dictGet(cache, url);
|
||||
return (isSxTruthy(isNil(entry)) ? NIL : (isSxTruthy(((nowMs() - get(entry, "timestamp")) > PRELOAD_TTL)) ? (dictDelete(cache, url), NIL) : (dictDelete(cache, url), entry)));
|
||||
})(); };
|
||||
|
||||
// preload-cache-set
|
||||
var preloadCacheSet = function(cache, url, text, contentType) { return dictSet(cache, url, {["text"]: text, ["content-type"]: contentType, ["timestamp"]: nowMs()}); };
|
||||
|
||||
// classify-trigger
|
||||
var classifyTrigger = function(trigger) { return (function() {
|
||||
var event = get(trigger, "event");
|
||||
return (isSxTruthy((event == "every")) ? "poll" : (isSxTruthy((event == "intersect")) ? "intersect" : (isSxTruthy((event == "load")) ? "load" : (isSxTruthy((event == "revealed")) ? "revealed" : "event"))));
|
||||
})(); };
|
||||
|
||||
// should-boost-link?
|
||||
var shouldBoostLink = function(link) { return (function() {
|
||||
var href = domGetAttr(link, "href");
|
||||
return (isSxTruthy(href) && isSxTruthy(!startsWith(href, "#")) && isSxTruthy(!startsWith(href, "javascript:")) && isSxTruthy(!startsWith(href, "mailto:")) && isSxTruthy(browserSameOrigin(href)) && isSxTruthy(!domHasAttr(link, "sx-get")) && isSxTruthy(!domHasAttr(link, "sx-post")) && !domHasAttr(link, "sx-disable"));
|
||||
})(); };
|
||||
|
||||
// should-boost-form?
|
||||
var shouldBoostForm = function(form) { return (isSxTruthy(!domHasAttr(form, "sx-get")) && isSxTruthy(!domHasAttr(form, "sx-post")) && !domHasAttr(form, "sx-disable")); };
|
||||
|
||||
// parse-sse-swap
|
||||
var parseSseSwap = function(el) { return sxOr(domGetAttr(el, "sx-sse-swap"), "message"); };
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// 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 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; }
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Post-transpilation fixups
|
||||
// =========================================================================
|
||||
@@ -896,17 +1636,12 @@
|
||||
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;
|
||||
if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;
|
||||
|
||||
// =========================================================================
|
||||
// Parser (reused from reference — hand-written for bootstrap simplicity)
|
||||
// Parser
|
||||
// =========================================================================
|
||||
|
||||
// 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.
|
||||
|
||||
function parse(text) {
|
||||
var pos = 0;
|
||||
function skipWs() {
|
||||
@@ -1019,32 +1754,23 @@
|
||||
}
|
||||
|
||||
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++) {
|
||||
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;
|
||||
}
|
||||
|
||||
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)));
|
||||
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 = {
|
||||
@@ -1052,15 +1778,23 @@
|
||||
eval: function(expr, env) { return trampoline(evalExpr(expr, env || merge(componentEnv))); },
|
||||
loadComponents: loadComponents,
|
||||
render: render,
|
||||
renderToString: renderToString,
|
||||
serialize: serialize,
|
||||
NIL: NIL,
|
||||
Symbol: Symbol,
|
||||
Keyword: Keyword,
|
||||
componentEnv: componentEnv,
|
||||
_version: "ref-1.0 (bootstrap-compiled)"
|
||||
renderToHtml: function(expr, env) { return renderToHtml(expr, env || merge(componentEnv)); },
|
||||
renderToSx: function(expr, env) { return renderToSx(expr, env || merge(componentEnv)); },
|
||||
renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,
|
||||
parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,
|
||||
morphNode: typeof morphNode === "function" ? morphNode : null,
|
||||
morphChildren: typeof morphChildren === "function" ? morphChildren : null,
|
||||
swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,
|
||||
_version: "ref-2.0 (dom+engine+html+sx, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = SxRef;
|
||||
else global.SxRef = SxRef;
|
||||
|
||||
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
|
||||
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
|
||||
469
shared/sx/ref/adapter-dom.sx
Normal file
469
shared/sx/ref/adapter-dom.sx
Normal file
@@ -0,0 +1,469 @@
|
||||
;; ==========================================================================
|
||||
;; adapter-dom.sx — DOM rendering adapter
|
||||
;;
|
||||
;; Renders SX expressions to live DOM nodes. Browser-only.
|
||||
;; Mirrors the render-to-html adapter but produces Element/Text/Fragment
|
||||
;; nodes instead of HTML strings.
|
||||
;;
|
||||
;; Depends on:
|
||||
;; render.sx — HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, definition-form?
|
||||
;; eval.sx — eval-expr, trampoline, call-component, expand-macro
|
||||
;; ==========================================================================
|
||||
|
||||
(define SVG_NS "http://www.w3.org/2000/svg")
|
||||
(define MATH_NS "http://www.w3.org/1998/Math/MathML")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-to-dom — main entry point
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-to-dom
|
||||
(fn (expr env ns)
|
||||
(case (type-of expr)
|
||||
;; nil / boolean false / boolean true → empty fragment
|
||||
"nil" (create-fragment)
|
||||
"boolean" (create-fragment)
|
||||
|
||||
;; Pre-rendered raw HTML → parse into fragment
|
||||
"raw-html" (dom-parse-html (raw-html-content expr))
|
||||
|
||||
;; String → text node
|
||||
"string" (create-text-node expr)
|
||||
|
||||
;; Number → text node
|
||||
"number" (create-text-node (str expr))
|
||||
|
||||
;; Symbol → evaluate then render
|
||||
"symbol" (render-to-dom (trampoline (eval-expr expr env)) env ns)
|
||||
|
||||
;; Keyword → text
|
||||
"keyword" (create-text-node (keyword-name expr))
|
||||
|
||||
;; Pre-rendered DOM node → pass through
|
||||
"dom-node" expr
|
||||
|
||||
;; Dict → empty
|
||||
"dict" (create-fragment)
|
||||
|
||||
;; List → dispatch
|
||||
"list"
|
||||
(if (empty? expr)
|
||||
(create-fragment)
|
||||
(render-dom-list expr env ns))
|
||||
|
||||
;; Style value → text of class name
|
||||
"style-value" (create-text-node (style-value-class expr))
|
||||
|
||||
;; Fallback
|
||||
:else (create-text-node (str expr)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-list — dispatch on list head
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-dom-list
|
||||
(fn (expr env ns)
|
||||
(let ((head (first expr)))
|
||||
(cond
|
||||
;; Symbol head — dispatch on name
|
||||
(= (type-of head) "symbol")
|
||||
(let ((name (symbol-name head))
|
||||
(args (rest expr)))
|
||||
(cond
|
||||
;; raw! → insert unescaped HTML
|
||||
(= name "raw!")
|
||||
(render-dom-raw args env)
|
||||
|
||||
;; <> → fragment
|
||||
(= name "<>")
|
||||
(render-dom-fragment args env ns)
|
||||
|
||||
;; html: prefix → force element rendering
|
||||
(starts-with? name "html:")
|
||||
(render-dom-element (slice name 5) args env ns)
|
||||
|
||||
;; Render-aware special forms
|
||||
(render-dom-form? name)
|
||||
(if (and (contains? HTML_TAGS name)
|
||||
(or (and (> (len args) 0)
|
||||
(= (type-of (first args)) "keyword"))
|
||||
ns))
|
||||
;; Ambiguous: tag name that's also a form — treat as tag
|
||||
;; when keyword arg or namespace present
|
||||
(render-dom-element name args env ns)
|
||||
(dispatch-render-form name expr env ns))
|
||||
|
||||
;; Macro expansion
|
||||
(and (env-has? env name) (macro? (env-get env name)))
|
||||
(render-to-dom
|
||||
(expand-macro (env-get env name) args env)
|
||||
env ns)
|
||||
|
||||
;; HTML tag
|
||||
(contains? HTML_TAGS name)
|
||||
(render-dom-element name args env ns)
|
||||
|
||||
;; Component (~name)
|
||||
(starts-with? name "~")
|
||||
(let ((comp (env-get env name)))
|
||||
(if (component? comp)
|
||||
(render-dom-component comp args env ns)
|
||||
(render-dom-unknown-component name)))
|
||||
|
||||
;; Custom element (hyphenated with keyword attrs)
|
||||
(and (> (index-of name "-") 0)
|
||||
(> (len args) 0)
|
||||
(= (type-of (first args)) "keyword"))
|
||||
(render-dom-element name args env ns)
|
||||
|
||||
;; Inside SVG/MathML namespace — treat as element
|
||||
ns
|
||||
(render-dom-element name args env ns)
|
||||
|
||||
;; Fallback — evaluate then render
|
||||
:else
|
||||
(render-to-dom (trampoline (eval-expr expr env)) env ns)))
|
||||
|
||||
;; Lambda or list head → evaluate
|
||||
(or (lambda? head) (= (type-of head) "list"))
|
||||
(render-to-dom (trampoline (eval-expr expr env)) env ns)
|
||||
|
||||
;; Data list
|
||||
:else
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each (fn (x) (dom-append frag (render-to-dom x env ns))) expr)
|
||||
frag)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-element — create a DOM element with attrs and children
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-dom-element
|
||||
(fn (tag args env ns)
|
||||
;; Detect namespace from tag
|
||||
(let ((new-ns (cond (= tag "svg") SVG_NS
|
||||
(= tag "math") MATH_NS
|
||||
:else ns))
|
||||
(el (dom-create-element tag new-ns))
|
||||
(extra-class nil))
|
||||
|
||||
;; Process args: keywords → attrs, others → children
|
||||
(reduce
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false "i" (inc (get state "i")))
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
;; Keyword arg → attribute
|
||||
(let ((attr-name (keyword-name arg))
|
||||
(attr-val (trampoline
|
||||
(eval-expr
|
||||
(nth args (inc (get state "i")))
|
||||
env))))
|
||||
(cond
|
||||
;; nil or false → skip
|
||||
(or (nil? attr-val) (= attr-val false))
|
||||
nil
|
||||
;; :style StyleValue → convert to class
|
||||
(and (= attr-name "style") (style-value? attr-val))
|
||||
(set! extra-class (style-value-class attr-val))
|
||||
;; Boolean attr
|
||||
(contains? BOOLEAN_ATTRS attr-name)
|
||||
(when attr-val (dom-set-attr el attr-name ""))
|
||||
;; true → empty attr
|
||||
(= attr-val true)
|
||||
(dom-set-attr el attr-name "")
|
||||
;; Normal attr
|
||||
:else
|
||||
(dom-set-attr el attr-name (str attr-val)))
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
|
||||
;; Positional arg → child
|
||||
(do
|
||||
(when (not (contains? VOID_ELEMENTS tag))
|
||||
(dom-append el (render-to-dom arg env new-ns)))
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
|
||||
;; Merge StyleValue class
|
||||
(when extra-class
|
||||
(let ((existing (dom-get-attr el "class")))
|
||||
(dom-set-attr el "class"
|
||||
(if existing (str existing " " extra-class) extra-class))))
|
||||
|
||||
el)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-component — expand and render a component
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-dom-component
|
||||
(fn (comp args env ns)
|
||||
;; Parse kwargs and children, bind into component env, render body.
|
||||
(let ((kwargs (dict))
|
||||
(children (list)))
|
||||
;; Separate keyword args from positional children
|
||||
(reduce
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false "i" (inc (get state "i")))
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
;; Keyword arg — evaluate in caller's env
|
||||
(let ((val (trampoline
|
||||
(eval-expr (nth args (inc (get state "i"))) env))))
|
||||
(dict-set! kwargs (keyword-name arg) val)
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
(do
|
||||
(append! children arg)
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
|
||||
;; Build component env: closure + caller env + params
|
||||
(let ((local (env-merge (component-closure comp) env)))
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn (p)
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
|
||||
;; If component accepts children, pre-render them to a fragment
|
||||
(when (component-has-children? comp)
|
||||
(let ((child-frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (c) (dom-append child-frag (render-to-dom c env ns)))
|
||||
children)
|
||||
(env-set! local "children" child-frag)))
|
||||
|
||||
(render-to-dom (component-body comp) local ns)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-fragment — render children into a DocumentFragment
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-dom-fragment
|
||||
(fn (args env ns)
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (x) (dom-append frag (render-to-dom x env ns)))
|
||||
args)
|
||||
frag)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-raw — insert unescaped content
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-dom-raw
|
||||
(fn (args env)
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (arg)
|
||||
(let ((val (trampoline (eval-expr arg env))))
|
||||
(cond
|
||||
(= (type-of val) "string")
|
||||
(dom-append frag (dom-parse-html val))
|
||||
(= (type-of val) "dom-node")
|
||||
(dom-append frag (dom-clone val))
|
||||
(not (nil? val))
|
||||
(dom-append frag (create-text-node (str val))))))
|
||||
args)
|
||||
frag)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-unknown-component — visible warning element
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-dom-unknown-component
|
||||
(fn (name)
|
||||
(let ((el (dom-create-element "div" nil)))
|
||||
(dom-set-attr el "style"
|
||||
"background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;padding:4px 8px;margin:2px;border-radius:4px;font-size:12px;font-family:monospace")
|
||||
(dom-append el (create-text-node (str "Unknown component: " name)))
|
||||
el)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Render-aware special forms for DOM output
|
||||
;; --------------------------------------------------------------------------
|
||||
;; These forms need special handling in DOM rendering because they
|
||||
;; produce DOM nodes rather than evaluated values.
|
||||
|
||||
(define RENDER_DOM_FORMS
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defkeyframes" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
|
||||
(define render-dom-form?
|
||||
(fn (name)
|
||||
(contains? RENDER_DOM_FORMS name)))
|
||||
|
||||
(define dispatch-render-form
|
||||
(fn (name expr env ns)
|
||||
(cond
|
||||
;; if
|
||||
(= name "if")
|
||||
(let ((cond-val (trampoline (eval-expr (nth expr 1) env))))
|
||||
(if cond-val
|
||||
(render-to-dom (nth expr 2) env ns)
|
||||
(if (> (len expr) 3)
|
||||
(render-to-dom (nth expr 3) env ns)
|
||||
(create-fragment))))
|
||||
|
||||
;; when
|
||||
(= name "when")
|
||||
(if (not (trampoline (eval-expr (nth expr 1) env)))
|
||||
(create-fragment)
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (i)
|
||||
(dom-append frag (render-to-dom (nth expr i) env ns)))
|
||||
(range 2 (len expr)))
|
||||
frag))
|
||||
|
||||
;; cond
|
||||
(= name "cond")
|
||||
(let ((branch (eval-cond (rest expr) env)))
|
||||
(if branch
|
||||
(render-to-dom branch env ns)
|
||||
(create-fragment)))
|
||||
|
||||
;; case
|
||||
(= name "case")
|
||||
(render-to-dom (trampoline (eval-expr expr env)) env ns)
|
||||
|
||||
;; let / let*
|
||||
(or (= name "let") (= name "let*"))
|
||||
(let ((local (process-bindings (nth expr 1) env))
|
||||
(frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (i)
|
||||
(dom-append frag (render-to-dom (nth expr i) local ns)))
|
||||
(range 2 (len expr)))
|
||||
frag)
|
||||
|
||||
;; begin / do
|
||||
(or (= name "begin") (= name "do"))
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (i)
|
||||
(dom-append frag (render-to-dom (nth expr i) env ns)))
|
||||
(range 1 (len expr)))
|
||||
frag)
|
||||
|
||||
;; Definition forms — eval for side effects
|
||||
(definition-form? name)
|
||||
(do (trampoline (eval-expr expr env)) (create-fragment))
|
||||
|
||||
;; map
|
||||
(= name "map")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env)))
|
||||
(frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (item)
|
||||
(let ((val (if (lambda? f)
|
||||
(render-lambda-dom f (list item) env ns)
|
||||
(render-to-dom (apply f (list item)) env ns))))
|
||||
(dom-append frag val)))
|
||||
coll)
|
||||
frag)
|
||||
|
||||
;; map-indexed
|
||||
(= name "map-indexed")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env)))
|
||||
(frag (create-fragment)))
|
||||
(for-each-indexed
|
||||
(fn (i item)
|
||||
(let ((val (if (lambda? f)
|
||||
(render-lambda-dom f (list i item) env ns)
|
||||
(render-to-dom (apply f (list i item)) env ns))))
|
||||
(dom-append frag val)))
|
||||
coll)
|
||||
frag)
|
||||
|
||||
;; filter — evaluate fully then render
|
||||
(= name "filter")
|
||||
(render-to-dom (trampoline (eval-expr expr env)) env ns)
|
||||
|
||||
;; for-each (render variant)
|
||||
(= name "for-each")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env)))
|
||||
(frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (item)
|
||||
(let ((val (if (lambda? f)
|
||||
(render-lambda-dom f (list item) env ns)
|
||||
(render-to-dom (apply f (list item)) env ns))))
|
||||
(dom-append frag val)))
|
||||
coll)
|
||||
frag)
|
||||
|
||||
;; Fallback
|
||||
:else
|
||||
(render-to-dom (trampoline (eval-expr expr env)) env ns))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-lambda-dom — render a lambda body in DOM context
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-lambda-dom
|
||||
(fn (f args env ns)
|
||||
;; Bind lambda params and render body as DOM
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(for-each-indexed
|
||||
(fn (i p)
|
||||
(env-set! local p (nth args i)))
|
||||
(lambda-params f))
|
||||
(render-to-dom (lambda-body f) local ns))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — DOM adapter
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Element creation:
|
||||
;; (dom-create-element tag ns) → Element (ns=nil for HTML, string for SVG/MathML)
|
||||
;; (create-text-node s) → Text node
|
||||
;; (create-fragment) → DocumentFragment
|
||||
;;
|
||||
;; Tree mutation:
|
||||
;; (dom-append parent child) → void (appendChild)
|
||||
;; (dom-set-attr el name val) → void (setAttribute)
|
||||
;; (dom-get-attr el name) → string or nil (getAttribute)
|
||||
;;
|
||||
;; Content parsing:
|
||||
;; (dom-parse-html s) → DocumentFragment from HTML string
|
||||
;; (dom-clone node) → deep clone of a DOM node
|
||||
;;
|
||||
;; Type checking:
|
||||
;; DOM nodes have type-of → "dom-node"
|
||||
;;
|
||||
;; From render.sx:
|
||||
;; HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, definition-form?
|
||||
;; style-value?, style-value-class
|
||||
;;
|
||||
;; From eval.sx:
|
||||
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
|
||||
;; env-has?, env-get, env-set!, env-merge
|
||||
;; lambda?, component?, macro?
|
||||
;; lambda-closure, lambda-params, lambda-body
|
||||
;; component-params, component-body, component-closure,
|
||||
;; component-has-children?, component-name
|
||||
;;
|
||||
;; Iteration:
|
||||
;; (for-each-indexed fn coll) → call fn(index, item) for each element
|
||||
;; --------------------------------------------------------------------------
|
||||
312
shared/sx/ref/adapter-html.sx
Normal file
312
shared/sx/ref/adapter-html.sx
Normal file
@@ -0,0 +1,312 @@
|
||||
;; ==========================================================================
|
||||
;; adapter-html.sx — HTML string rendering adapter
|
||||
;;
|
||||
;; Renders evaluated SX expressions to HTML strings. Used server-side.
|
||||
;;
|
||||
;; Depends on:
|
||||
;; render.sx — HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
|
||||
;; parse-element-args, render-attrs, definition-form?
|
||||
;; eval.sx — eval-expr, trampoline, expand-macro, process-bindings,
|
||||
;; eval-cond, env-has?, env-get, env-set!, env-merge,
|
||||
;; lambda?, component?, macro?,
|
||||
;; lambda-closure, lambda-params, lambda-body
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
(define render-to-html
|
||||
(fn (expr env)
|
||||
(case (type-of expr)
|
||||
;; Literals — render directly
|
||||
"nil" ""
|
||||
"string" (escape-html expr)
|
||||
"number" (str expr)
|
||||
"boolean" (if expr "true" "false")
|
||||
;; List — dispatch to render-list which handles HTML tags, special forms, etc.
|
||||
"list" (if (empty? expr) "" (render-list-to-html expr env))
|
||||
;; Symbol — evaluate then render
|
||||
"symbol" (render-value-to-html (trampoline (eval-expr expr env)) env)
|
||||
;; Keyword — render as text
|
||||
"keyword" (escape-html (keyword-name expr))
|
||||
;; Raw HTML passthrough
|
||||
"raw-html" (raw-html-content expr)
|
||||
;; Everything else — evaluate first
|
||||
:else (render-value-to-html (trampoline (eval-expr expr env)) env))))
|
||||
|
||||
(define render-value-to-html
|
||||
(fn (val env)
|
||||
(case (type-of val)
|
||||
"nil" ""
|
||||
"string" (escape-html val)
|
||||
"number" (str val)
|
||||
"boolean" (if val "true" "false")
|
||||
"list" (render-list-to-html val env)
|
||||
"raw-html" (raw-html-content val)
|
||||
"style-value" (style-value-class val)
|
||||
:else (escape-html (str val)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Render-aware form classification
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define RENDER_HTML_FORMS
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defkeyframes" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
|
||||
(define render-html-form?
|
||||
(fn (name)
|
||||
(contains? RENDER_HTML_FORMS name)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-list-to-html — dispatch on list head
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-list-to-html
|
||||
(fn (expr env)
|
||||
(if (empty? expr)
|
||||
""
|
||||
(let ((head (first expr)))
|
||||
(if (not (= (type-of head) "symbol"))
|
||||
;; Data list — render each item
|
||||
(join "" (map (fn (x) (render-value-to-html x env)) expr))
|
||||
(let ((name (symbol-name head))
|
||||
(args (rest expr)))
|
||||
(cond
|
||||
;; Fragment
|
||||
(= name "<>")
|
||||
(join "" (map (fn (x) (render-to-html x env)) args))
|
||||
|
||||
;; Raw HTML passthrough
|
||||
(= name "raw!")
|
||||
(join "" (map (fn (x) (str (trampoline (eval-expr x env)))) args))
|
||||
|
||||
;; HTML tag
|
||||
(contains? HTML_TAGS name)
|
||||
(render-html-element name args env)
|
||||
|
||||
;; Component or macro call (~name)
|
||||
(starts-with? name "~")
|
||||
(let ((val (env-get env name)))
|
||||
(cond
|
||||
(component? val)
|
||||
(render-html-component val args env)
|
||||
(macro? val)
|
||||
(render-to-html
|
||||
(expand-macro val args env)
|
||||
env)
|
||||
:else
|
||||
(error (str "Unknown component: " name))))
|
||||
|
||||
;; Render-aware special forms
|
||||
(render-html-form? name)
|
||||
(dispatch-html-form name expr env)
|
||||
|
||||
;; Macro expansion
|
||||
(and (env-has? env name) (macro? (env-get env name)))
|
||||
(render-to-html
|
||||
(expand-macro (env-get env name) args env)
|
||||
env)
|
||||
|
||||
;; Fallback — evaluate then render result
|
||||
:else
|
||||
(render-value-to-html
|
||||
(trampoline (eval-expr expr env))
|
||||
env))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; dispatch-html-form — render-aware special form handling for HTML output
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dispatch-html-form
|
||||
(fn (name expr env)
|
||||
(cond
|
||||
;; if
|
||||
(= name "if")
|
||||
(let ((cond-val (trampoline (eval-expr (nth expr 1) env))))
|
||||
(if cond-val
|
||||
(render-to-html (nth expr 2) env)
|
||||
(if (> (len expr) 3)
|
||||
(render-to-html (nth expr 3) env)
|
||||
"")))
|
||||
|
||||
;; when
|
||||
(= name "when")
|
||||
(if (not (trampoline (eval-expr (nth expr 1) env)))
|
||||
""
|
||||
(join ""
|
||||
(map
|
||||
(fn (i) (render-to-html (nth expr i) env))
|
||||
(range 2 (len expr)))))
|
||||
|
||||
;; cond
|
||||
(= name "cond")
|
||||
(let ((branch (eval-cond (rest expr) env)))
|
||||
(if branch
|
||||
(render-to-html branch env)
|
||||
""))
|
||||
|
||||
;; case
|
||||
(= name "case")
|
||||
(render-to-html (trampoline (eval-expr expr env)) env)
|
||||
|
||||
;; let / let*
|
||||
(or (= name "let") (= name "let*"))
|
||||
(let ((local (process-bindings (nth expr 1) env)))
|
||||
(join ""
|
||||
(map
|
||||
(fn (i) (render-to-html (nth expr i) local))
|
||||
(range 2 (len expr)))))
|
||||
|
||||
;; begin / do
|
||||
(or (= name "begin") (= name "do"))
|
||||
(join ""
|
||||
(map
|
||||
(fn (i) (render-to-html (nth expr i) env))
|
||||
(range 1 (len expr))))
|
||||
|
||||
;; Definition forms — eval for side effects
|
||||
(definition-form? name)
|
||||
(do (trampoline (eval-expr expr env)) "")
|
||||
|
||||
;; map
|
||||
(= name "map")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env))))
|
||||
(join ""
|
||||
(map
|
||||
(fn (item)
|
||||
(if (lambda? f)
|
||||
(render-lambda-html f (list item) env)
|
||||
(render-to-html (apply f (list item)) env)))
|
||||
coll)))
|
||||
|
||||
;; map-indexed
|
||||
(= name "map-indexed")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env))))
|
||||
(join ""
|
||||
(map-indexed
|
||||
(fn (i item)
|
||||
(if (lambda? f)
|
||||
(render-lambda-html f (list i item) env)
|
||||
(render-to-html (apply f (list i item)) env)))
|
||||
coll)))
|
||||
|
||||
;; filter — evaluate fully then render
|
||||
(= name "filter")
|
||||
(render-to-html (trampoline (eval-expr expr env)) env)
|
||||
|
||||
;; for-each (render variant)
|
||||
(= name "for-each")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env))))
|
||||
(join ""
|
||||
(map
|
||||
(fn (item)
|
||||
(if (lambda? f)
|
||||
(render-lambda-html f (list item) env)
|
||||
(render-to-html (apply f (list item)) env)))
|
||||
coll)))
|
||||
|
||||
;; Fallback
|
||||
:else
|
||||
(render-value-to-html (trampoline (eval-expr expr env)) env))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-lambda-html — render a lambda body in HTML context
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-lambda-html
|
||||
(fn (f args env)
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(for-each-indexed
|
||||
(fn (i p)
|
||||
(env-set! local p (nth args i)))
|
||||
(lambda-params f))
|
||||
(render-to-html (lambda-body f) local))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-html-component — expand and render a component
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-html-component
|
||||
(fn (comp args env)
|
||||
;; Expand component and render body through HTML adapter.
|
||||
;; Component body contains rendering forms (HTML tags) that only the
|
||||
;; adapter understands, so expansion must happen here, not in eval-expr.
|
||||
(let ((kwargs (dict))
|
||||
(children (list)))
|
||||
;; Separate keyword args from positional children
|
||||
(reduce
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false "i" (inc (get state "i")))
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
(let ((val (trampoline
|
||||
(eval-expr (nth args (inc (get state "i"))) env))))
|
||||
(dict-set! kwargs (keyword-name arg) val)
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
(do
|
||||
(append! children arg)
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
;; Build component env: closure + caller env + params
|
||||
(let ((local (env-merge (component-closure comp) env)))
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn (p)
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
;; If component accepts children, pre-render them to raw HTML
|
||||
(when (component-has-children? comp)
|
||||
(env-set! local "children"
|
||||
(make-raw-html
|
||||
(join "" (map (fn (c) (render-to-html c env)) children)))))
|
||||
(render-to-html (component-body comp) local)))))
|
||||
|
||||
|
||||
(define render-html-element
|
||||
(fn (tag args env)
|
||||
(let ((parsed (parse-element-args args env))
|
||||
(attrs (first parsed))
|
||||
(children (nth parsed 1))
|
||||
(is-void (contains? VOID_ELEMENTS tag)))
|
||||
(str "<" tag
|
||||
(render-attrs attrs)
|
||||
(if is-void
|
||||
" />"
|
||||
(str ">"
|
||||
(join "" (map (fn (c) (render-to-html c env)) children))
|
||||
"</" tag ">"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — HTML adapter
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Inherited from render.sx:
|
||||
;; escape-html, escape-attr, raw-html-content, style-value?, style-value-class
|
||||
;;
|
||||
;; From eval.sx:
|
||||
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
|
||||
;; env-has?, env-get, env-set!, env-merge
|
||||
;; lambda?, component?, macro?
|
||||
;; lambda-closure, lambda-params, lambda-body
|
||||
;; component-params, component-body, component-closure,
|
||||
;; component-has-children?, component-name
|
||||
;;
|
||||
;; Raw HTML construction:
|
||||
;; (make-raw-html s) → wrap string as raw HTML (not double-escaped)
|
||||
;;
|
||||
;; Iteration:
|
||||
;; (for-each-indexed fn coll) → call fn(index, item) for each element
|
||||
;; (map-indexed fn coll) → map fn(index, item) over each element
|
||||
;; --------------------------------------------------------------------------
|
||||
147
shared/sx/ref/adapter-sx.sx
Normal file
147
shared/sx/ref/adapter-sx.sx
Normal file
@@ -0,0 +1,147 @@
|
||||
;; ==========================================================================
|
||||
;; adapter-sx.sx — SX wire format rendering adapter
|
||||
;;
|
||||
;; Serializes SX expressions for client-side rendering.
|
||||
;; Component calls are NOT expanded — they're sent to the client as-is.
|
||||
;; HTML tags are serialized as SX source text. Special forms are evaluated.
|
||||
;;
|
||||
;; Depends on:
|
||||
;; render.sx — HTML_TAGS
|
||||
;; eval.sx — eval-expr, trampoline, call-lambda, expand-macro
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
(define render-to-sx
|
||||
(fn (expr env)
|
||||
(let ((result (aser expr env)))
|
||||
;; aser-call already returns serialized SX strings;
|
||||
;; only serialize non-string values
|
||||
(if (= (type-of result) "string")
|
||||
result
|
||||
(serialize result)))))
|
||||
|
||||
(define aser
|
||||
(fn (expr env)
|
||||
;; Evaluate for SX wire format — serialize rendering forms,
|
||||
;; evaluate control flow and function calls.
|
||||
(case (type-of expr)
|
||||
"number" expr
|
||||
"string" expr
|
||||
"boolean" expr
|
||||
"nil" nil
|
||||
|
||||
"symbol"
|
||||
(let ((name (symbol-name expr)))
|
||||
(cond
|
||||
(env-has? env name) (env-get env name)
|
||||
(primitive? name) (get-primitive name)
|
||||
(= name "true") true
|
||||
(= name "false") false
|
||||
(= name "nil") nil
|
||||
:else (error (str "Undefined symbol: " name))))
|
||||
|
||||
"keyword" (keyword-name expr)
|
||||
|
||||
"list"
|
||||
(if (empty? expr)
|
||||
(list)
|
||||
(aser-list expr env))
|
||||
|
||||
:else expr)))
|
||||
|
||||
|
||||
(define aser-list
|
||||
(fn (expr env)
|
||||
(let ((head (first expr))
|
||||
(args (rest expr)))
|
||||
(if (not (= (type-of head) "symbol"))
|
||||
(map (fn (x) (aser x env)) expr)
|
||||
(let ((name (symbol-name head)))
|
||||
(cond
|
||||
;; Fragment — serialize children
|
||||
(= name "<>")
|
||||
(aser-fragment args env)
|
||||
|
||||
;; Component call — serialize WITHOUT expanding
|
||||
(starts-with? name "~")
|
||||
(aser-call name args env)
|
||||
|
||||
;; HTML tag — serialize
|
||||
(contains? HTML_TAGS name)
|
||||
(aser-call name args env)
|
||||
|
||||
;; Special/HO forms — evaluate (produces data)
|
||||
(or (special-form? name) (ho-form? name))
|
||||
(aser-special name expr env)
|
||||
|
||||
;; Macro — expand then aser
|
||||
(and (env-has? env name) (macro? (env-get env name)))
|
||||
(aser (expand-macro (env-get env name) args env) env)
|
||||
|
||||
;; Function call — evaluate fully
|
||||
:else
|
||||
(let ((f (trampoline (eval-expr head env)))
|
||||
(evaled-args (map (fn (a) (trampoline (eval-expr a env))) args)))
|
||||
(cond
|
||||
(and (callable? f) (not (lambda? f)) (not (component? f)))
|
||||
(apply f evaled-args)
|
||||
(lambda? f)
|
||||
(trampoline (call-lambda f evaled-args env))
|
||||
(component? f)
|
||||
(aser-call (str "~" (component-name f)) args env)
|
||||
:else (error (str "Not callable: " (inspect f)))))))))))
|
||||
|
||||
|
||||
(define aser-fragment
|
||||
(fn (children env)
|
||||
;; Serialize (<> child1 child2 ...) to sx source string
|
||||
(let ((parts (filter
|
||||
(fn (x) (not (nil? x)))
|
||||
(map (fn (c) (aser c env)) children))))
|
||||
(if (empty? parts)
|
||||
""
|
||||
(str "(<> " (join " " (map serialize parts)) ")")))))
|
||||
|
||||
|
||||
(define aser-call
|
||||
(fn (name args env)
|
||||
;; Serialize (name :key val child ...) — evaluate args but keep as sx
|
||||
(let ((parts (list name)))
|
||||
(reduce
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false "i" (inc (get state "i")))
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
(let ((val (aser (nth args (inc (get state "i"))) env)))
|
||||
(when (not (nil? val))
|
||||
(append! parts (str ":" (keyword-name arg)))
|
||||
(append! parts (serialize val)))
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
(let ((val (aser arg env)))
|
||||
(when (not (nil? val))
|
||||
(append! parts (serialize val)))
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
(str "(" (join " " parts) ")"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — SX wire adapter
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Serialization:
|
||||
;; (serialize val) → SX source string representation of val
|
||||
;;
|
||||
;; Form classification:
|
||||
;; (special-form? name) → boolean
|
||||
;; (ho-form? name) → boolean
|
||||
;; (aser-special name expr env) → evaluate special/HO form through aser
|
||||
;;
|
||||
;; From eval.sx:
|
||||
;; eval-expr, trampoline, call-lambda, expand-macro
|
||||
;; env-has?, env-get, callable?, lambda?, component?, macro?
|
||||
;; primitive?, get-primitive, component-name
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -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)
|
||||
|
||||
747
shared/sx/ref/engine.sx
Normal file
747
shared/sx/ref/engine.sx
Normal file
@@ -0,0 +1,747 @@
|
||||
;; ==========================================================================
|
||||
;; engine.sx — SxEngine specification
|
||||
;;
|
||||
;; Fetch/swap/history engine for browser-side SX. Like HTMX but native
|
||||
;; to the SX rendering pipeline.
|
||||
;;
|
||||
;; This file specifies the LOGIC of the engine in s-expressions.
|
||||
;; Browser-specific APIs (fetch, DOM, history, events) are declared as
|
||||
;; platform interface at the bottom.
|
||||
;;
|
||||
;; The engine processes elements with sx-* attributes:
|
||||
;; sx-get, sx-post, sx-put, sx-delete, sx-patch — HTTP verb + URL
|
||||
;; sx-trigger — when to fire (click, submit, change, every 5s, ...)
|
||||
;; sx-target — where to swap response (#selector, "this", "closest")
|
||||
;; sx-swap — how to swap (innerHTML, outerHTML, afterend, ...)
|
||||
;; sx-select — filter response (CSS selector)
|
||||
;; sx-confirm — confirmation dialog before request
|
||||
;; sx-prompt — prompt dialog, sends result as SX-Prompt header
|
||||
;; sx-validate — form validation before request
|
||||
;; sx-encoding — "json" for JSON body instead of form-encoded
|
||||
;; sx-params — filter form fields (include, exclude, none)
|
||||
;; sx-include — include extra inputs from other elements
|
||||
;; sx-vals — extra key-value pairs to send
|
||||
;; sx-headers — extra request headers
|
||||
;; sx-indicator — show/hide loading indicator
|
||||
;; sx-disabled-elt — disable elements during request
|
||||
;; sx-push-url — push to browser history
|
||||
;; sx-replace-url — replace browser history
|
||||
;; sx-sync — abort previous request ("replace")
|
||||
;; sx-media — only fire if media query matches
|
||||
;; sx-preload — preload on mousedown/mouseover
|
||||
;; sx-boost — auto-boost links and forms in container
|
||||
;; sx-sse — connect to Server-Sent Events
|
||||
;; sx-retry — retry on failure (exponential:startMs:capMs)
|
||||
;; sx-optimistic — optimistic update (remove, disable, add-class:name)
|
||||
;; sx-preserve — don't morph this element during swap
|
||||
;; sx-ignore — skip morphing entirely
|
||||
;; sx-on:* — inline event handlers (beforeRequest, afterSwap, ...)
|
||||
;;
|
||||
;; Depends on:
|
||||
;; adapter-dom.sx — render-to-dom (for SX response rendering)
|
||||
;; render.sx — shared registries
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Constants
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define ENGINE_VERBS (list "get" "post" "put" "delete" "patch"))
|
||||
(define DEFAULT_SWAP "outerHTML")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Trigger parsing
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Parses the sx-trigger attribute value into a list of trigger descriptors.
|
||||
;; Each descriptor is a dict with "event" and "modifiers" keys.
|
||||
|
||||
(define parse-time
|
||||
(fn (s)
|
||||
;; Parse time string: "2s" → 2000, "500ms" → 500
|
||||
(cond
|
||||
(nil? s) 0
|
||||
(ends-with? s "ms") (parse-int s 0)
|
||||
(ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000)
|
||||
:else (parse-int s 0))))
|
||||
|
||||
|
||||
(define parse-trigger-spec
|
||||
(fn (spec)
|
||||
;; Parse "click delay:500ms once,change" → list of trigger descriptors
|
||||
(if (nil? spec)
|
||||
nil
|
||||
(let ((raw-parts (split spec ",")))
|
||||
(filter
|
||||
(fn (x) (not (nil? x)))
|
||||
(map
|
||||
(fn (part)
|
||||
(let ((tokens (split (trim part) " ")))
|
||||
(if (empty? tokens)
|
||||
nil
|
||||
(if (and (= (first tokens) "every") (>= (len tokens) 2))
|
||||
;; Polling trigger
|
||||
(dict
|
||||
"event" "every"
|
||||
"modifiers" (dict "interval" (parse-time (nth tokens 1))))
|
||||
;; Normal trigger with optional modifiers
|
||||
(let ((mods (dict)))
|
||||
(for-each
|
||||
(fn (tok)
|
||||
(cond
|
||||
(= tok "once")
|
||||
(dict-set! mods "once" true)
|
||||
(= tok "changed")
|
||||
(dict-set! mods "changed" true)
|
||||
(starts-with? tok "delay:")
|
||||
(dict-set! mods "delay"
|
||||
(parse-time (slice tok 6)))
|
||||
(starts-with? tok "from:")
|
||||
(dict-set! mods "from"
|
||||
(slice tok 5))))
|
||||
(rest tokens))
|
||||
(dict "event" (first tokens) "modifiers" mods))))))
|
||||
raw-parts))))))
|
||||
|
||||
|
||||
(define default-trigger
|
||||
(fn (tag-name)
|
||||
;; Default trigger for element type
|
||||
(cond
|
||||
(= tag-name "FORM")
|
||||
(list (dict "event" "submit" "modifiers" (dict)))
|
||||
(or (= tag-name "INPUT")
|
||||
(= tag-name "SELECT")
|
||||
(= tag-name "TEXTAREA"))
|
||||
(list (dict "event" "change" "modifiers" (dict)))
|
||||
:else
|
||||
(list (dict "event" "click" "modifiers" (dict))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Verb extraction
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define get-verb-info
|
||||
(fn (el)
|
||||
;; Check element for sx-get, sx-post, etc. Returns (dict "method" "url") or nil.
|
||||
(some
|
||||
(fn (verb)
|
||||
(let ((url (dom-get-attr el (str "sx-" verb))))
|
||||
(if url
|
||||
(dict "method" (upper verb) "url" url)
|
||||
nil)))
|
||||
ENGINE_VERBS)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Request header building
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-request-headers
|
||||
(fn (el loaded-components css-hash)
|
||||
;; Build the SX request headers dict
|
||||
(let ((headers (dict
|
||||
"SX-Request" "true"
|
||||
"SX-Current-URL" (browser-location-href))))
|
||||
;; Target selector
|
||||
(let ((target-sel (dom-get-attr el "sx-target")))
|
||||
(when target-sel
|
||||
(dict-set! headers "SX-Target" target-sel)))
|
||||
|
||||
;; Loaded component names
|
||||
(when (not (empty? loaded-components))
|
||||
(dict-set! headers "SX-Components"
|
||||
(join "," loaded-components)))
|
||||
|
||||
;; CSS class hash
|
||||
(when css-hash
|
||||
(dict-set! headers "SX-Css" css-hash))
|
||||
|
||||
;; Extra headers from sx-headers attribute
|
||||
(let ((extra-h (dom-get-attr el "sx-headers")))
|
||||
(when extra-h
|
||||
(let ((parsed (parse-header-value extra-h)))
|
||||
(when parsed
|
||||
(for-each
|
||||
(fn (key) (dict-set! headers key (str (get parsed key))))
|
||||
(keys parsed))))))
|
||||
|
||||
headers)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Response header processing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define process-response-headers
|
||||
(fn (get-header)
|
||||
;; Extract all SX response header directives into a dict.
|
||||
;; get-header is (fn (name) → string or nil).
|
||||
(dict
|
||||
"redirect" (get-header "SX-Redirect")
|
||||
"refresh" (get-header "SX-Refresh")
|
||||
"trigger" (get-header "SX-Trigger")
|
||||
"retarget" (get-header "SX-Retarget")
|
||||
"reswap" (get-header "SX-Reswap")
|
||||
"location" (get-header "SX-Location")
|
||||
"replace-url" (get-header "SX-Replace-Url")
|
||||
"css-hash" (get-header "SX-Css-Hash")
|
||||
"trigger-swap" (get-header "SX-Trigger-After-Swap")
|
||||
"trigger-settle" (get-header "SX-Trigger-After-Settle")
|
||||
"content-type" (get-header "Content-Type"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Swap specification parsing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define parse-swap-spec
|
||||
(fn (raw-swap global-transitions?)
|
||||
;; Parse "innerHTML transition:true" → dict with style + transition flag
|
||||
(let ((parts (split (or raw-swap DEFAULT_SWAP) " "))
|
||||
(style (first parts))
|
||||
(use-transition global-transitions?))
|
||||
(for-each
|
||||
(fn (p)
|
||||
(cond
|
||||
(= p "transition:true") (set! use-transition true)
|
||||
(= p "transition:false") (set! use-transition false)))
|
||||
(rest parts))
|
||||
(dict "style" style "transition" use-transition))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Retry logic
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define parse-retry-spec
|
||||
(fn (retry-attr)
|
||||
;; Parse "exponential:1000:30000" → spec dict or nil
|
||||
(if (nil? retry-attr)
|
||||
nil
|
||||
(let ((parts (split retry-attr ":")))
|
||||
(dict
|
||||
"strategy" (first parts)
|
||||
"start-ms" (parse-int (nth parts 1) 1000)
|
||||
"cap-ms" (parse-int (nth parts 2) 30000))))))
|
||||
|
||||
|
||||
(define next-retry-ms
|
||||
(fn (current-ms cap-ms)
|
||||
;; Exponential backoff: double current, cap at max
|
||||
(min (* current-ms 2) cap-ms)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Form parameter filtering
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define filter-params
|
||||
(fn (params-spec all-params)
|
||||
;; Filter form parameters by sx-params spec.
|
||||
;; all-params is a list of (key value) pairs.
|
||||
;; Returns filtered list of (key value) pairs.
|
||||
(cond
|
||||
(nil? params-spec) all-params
|
||||
(= params-spec "none") (list)
|
||||
(= params-spec "*") all-params
|
||||
(starts-with? params-spec "not ")
|
||||
(let ((excluded (map trim (split (slice params-spec 4) ","))))
|
||||
(filter
|
||||
(fn (p) (not (contains? excluded (first p))))
|
||||
all-params))
|
||||
:else
|
||||
(let ((allowed (map trim (split params-spec ","))))
|
||||
(filter
|
||||
(fn (p) (contains? allowed (first p)))
|
||||
all-params)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Target resolution
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define resolve-target
|
||||
(fn (el)
|
||||
;; Resolve the swap target for an element
|
||||
(let ((sel (dom-get-attr el "sx-target")))
|
||||
(cond
|
||||
(or (nil? sel) (= sel "this")) el
|
||||
(= sel "closest") (dom-parent el)
|
||||
:else (dom-query sel)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Optimistic updates
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define apply-optimistic
|
||||
(fn (el)
|
||||
;; Apply optimistic update preview. Returns state for reverting, or nil.
|
||||
(let ((directive (dom-get-attr el "sx-optimistic")))
|
||||
(if (nil? directive)
|
||||
nil
|
||||
(let ((target (or (resolve-target el) el))
|
||||
(state (dict "target" target "directive" directive)))
|
||||
(cond
|
||||
(= directive "remove")
|
||||
(do
|
||||
(dict-set! state "opacity" (dom-get-style target "opacity"))
|
||||
(dom-set-style target "opacity" "0")
|
||||
(dom-set-style target "pointer-events" "none"))
|
||||
(= directive "disable")
|
||||
(do
|
||||
(dict-set! state "disabled" (dom-get-prop target "disabled"))
|
||||
(dom-set-prop target "disabled" true))
|
||||
(starts-with? directive "add-class:")
|
||||
(let ((cls (slice directive 10)))
|
||||
(dict-set! state "add-class" cls)
|
||||
(dom-add-class target cls)))
|
||||
state)))))
|
||||
|
||||
|
||||
(define revert-optimistic
|
||||
(fn (state)
|
||||
;; Revert an optimistic update
|
||||
(when state
|
||||
(let ((target (get state "target"))
|
||||
(directive (get state "directive")))
|
||||
(cond
|
||||
(= directive "remove")
|
||||
(do
|
||||
(dom-set-style target "opacity" (or (get state "opacity") ""))
|
||||
(dom-set-style target "pointer-events" ""))
|
||||
(= directive "disable")
|
||||
(dom-set-prop target "disabled" (or (get state "disabled") false))
|
||||
(get state "add-class")
|
||||
(dom-remove-class target (get state "add-class")))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Out-of-band swap identification
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define find-oob-swaps
|
||||
(fn (container)
|
||||
;; Find elements marked for out-of-band swapping.
|
||||
;; Returns list of (dict "element" el "swap-type" type "target-id" id).
|
||||
(let ((results (list)))
|
||||
(for-each
|
||||
(fn (attr)
|
||||
(let ((oob-els (dom-query-all container (str "[" attr "]"))))
|
||||
(for-each
|
||||
(fn (oob)
|
||||
(let ((swap-type (or (dom-get-attr oob attr) "outerHTML"))
|
||||
(target-id (dom-id oob)))
|
||||
(dom-remove-attr oob attr)
|
||||
(when target-id
|
||||
(append! results
|
||||
(dict "element" oob
|
||||
"swap-type" swap-type
|
||||
"target-id" target-id)))))
|
||||
oob-els)))
|
||||
(list "sx-swap-oob" "hx-swap-oob"))
|
||||
results)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; DOM morph algorithm
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Lightweight reconciler: patches oldNode to match newNode in-place,
|
||||
;; preserving event listeners, focus, scroll position, and form state
|
||||
;; on keyed (id) elements.
|
||||
|
||||
(define morph-node
|
||||
(fn (old-node new-node)
|
||||
;; Morph old-node to match new-node, preserving listeners/state.
|
||||
(cond
|
||||
;; sx-preserve / sx-ignore → skip
|
||||
(or (dom-has-attr? old-node "sx-preserve")
|
||||
(dom-has-attr? old-node "sx-ignore"))
|
||||
nil
|
||||
|
||||
;; Different node type or tag → replace wholesale
|
||||
(or (not (= (dom-node-type old-node) (dom-node-type new-node)))
|
||||
(not (= (dom-node-name old-node) (dom-node-name new-node))))
|
||||
(dom-replace-child (dom-parent old-node)
|
||||
(dom-clone new-node) old-node)
|
||||
|
||||
;; Text/comment nodes → update content
|
||||
(or (= (dom-node-type old-node) 3) (= (dom-node-type old-node) 8))
|
||||
(when (not (= (dom-text-content old-node) (dom-text-content new-node)))
|
||||
(dom-set-text-content old-node (dom-text-content new-node)))
|
||||
|
||||
;; Element nodes → sync attributes, then recurse children
|
||||
(= (dom-node-type old-node) 1)
|
||||
(do
|
||||
(sync-attrs old-node new-node)
|
||||
;; Skip morphing focused input to preserve user's in-progress edits
|
||||
(when (not (and (dom-is-active-element? old-node)
|
||||
(dom-is-input-element? old-node)))
|
||||
(morph-children old-node new-node))))))
|
||||
|
||||
|
||||
(define sync-attrs
|
||||
(fn (old-el new-el)
|
||||
;; Add/update attributes from new, remove those not in new
|
||||
(for-each
|
||||
(fn (attr)
|
||||
(let ((name (first attr))
|
||||
(val (nth attr 1)))
|
||||
(when (not (= (dom-get-attr old-el name) val))
|
||||
(dom-set-attr old-el name val))))
|
||||
(dom-attr-list new-el))
|
||||
(for-each
|
||||
(fn (attr)
|
||||
(when (not (dom-has-attr? new-el (first attr)))
|
||||
(dom-remove-attr old-el (first attr))))
|
||||
(dom-attr-list old-el))))
|
||||
|
||||
|
||||
(define morph-children
|
||||
(fn (old-parent new-parent)
|
||||
;; Reconcile children of old-parent to match new-parent.
|
||||
;; Keyed elements (with id) are matched and moved in-place.
|
||||
(let ((old-kids (dom-child-list old-parent))
|
||||
(new-kids (dom-child-list new-parent))
|
||||
;; Build ID map of old children for keyed matching
|
||||
(old-by-id (reduce
|
||||
(fn (acc kid)
|
||||
(let ((id (dom-id kid)))
|
||||
(if id (do (dict-set! acc id kid) acc) acc)))
|
||||
(dict) old-kids))
|
||||
(oi 0))
|
||||
|
||||
;; Walk new children, morph/insert/append
|
||||
(for-each
|
||||
(fn (new-child)
|
||||
(let ((match-id (dom-id new-child))
|
||||
(match-by-id (if match-id (dict-get old-by-id match-id) nil)))
|
||||
(cond
|
||||
;; Keyed match — move into position if needed, then morph
|
||||
(and match-by-id (not (nil? match-by-id)))
|
||||
(do
|
||||
(when (and (< oi (len old-kids))
|
||||
(not (= match-by-id (nth old-kids oi))))
|
||||
(dom-insert-before old-parent match-by-id
|
||||
(if (< oi (len old-kids)) (nth old-kids oi) nil)))
|
||||
(morph-node match-by-id new-child)
|
||||
(set! oi (inc oi)))
|
||||
|
||||
;; Positional match
|
||||
(< oi (len old-kids))
|
||||
(let ((old-child (nth old-kids oi)))
|
||||
(if (and (dom-id old-child) (not match-id))
|
||||
;; Old has ID, new doesn't — insert new before old
|
||||
(dom-insert-before old-parent
|
||||
(dom-clone new-child) old-child)
|
||||
;; Normal positional morph
|
||||
(do
|
||||
(morph-node old-child new-child)
|
||||
(set! oi (inc oi)))))
|
||||
|
||||
;; Extra new children — append
|
||||
:else
|
||||
(dom-append old-parent (dom-clone new-child)))))
|
||||
new-kids)
|
||||
|
||||
;; Remove leftover old children
|
||||
(for-each
|
||||
(fn (i)
|
||||
(when (>= i oi)
|
||||
(let ((leftover (nth old-kids i)))
|
||||
(when (and (dom-is-child-of? leftover old-parent)
|
||||
(not (dom-has-attr? leftover "sx-preserve"))
|
||||
(not (dom-has-attr? leftover "sx-ignore")))
|
||||
(dom-remove-child old-parent leftover)))))
|
||||
(range oi (len old-kids))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Swap dispatch
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define swap-dom-nodes
|
||||
(fn (target new-nodes strategy)
|
||||
;; Execute a swap strategy on live DOM nodes.
|
||||
;; new-nodes is typically a DocumentFragment or Element.
|
||||
(case strategy
|
||||
"innerHTML"
|
||||
(if (dom-is-fragment? new-nodes)
|
||||
(morph-children target new-nodes)
|
||||
(let ((wrapper (dom-create-element "div" nil)))
|
||||
(dom-append wrapper new-nodes)
|
||||
(morph-children target wrapper)))
|
||||
|
||||
"outerHTML"
|
||||
(let ((parent (dom-parent target)))
|
||||
(if (dom-is-fragment? new-nodes)
|
||||
;; Fragment — morph first child, insert rest
|
||||
(let ((fc (dom-first-child new-nodes)))
|
||||
(if fc
|
||||
(do
|
||||
(morph-node target fc)
|
||||
;; Insert remaining siblings after morphed element
|
||||
(let ((sib (dom-next-sibling fc)))
|
||||
(insert-remaining-siblings parent target sib)))
|
||||
(dom-remove-child parent target)))
|
||||
(morph-node target new-nodes))
|
||||
parent)
|
||||
|
||||
"afterend"
|
||||
(dom-insert-after target new-nodes)
|
||||
|
||||
"beforeend"
|
||||
(dom-append target new-nodes)
|
||||
|
||||
"afterbegin"
|
||||
(dom-prepend target new-nodes)
|
||||
|
||||
"beforebegin"
|
||||
(dom-insert-before (dom-parent target) new-nodes target)
|
||||
|
||||
"delete"
|
||||
(dom-remove-child (dom-parent target) target)
|
||||
|
||||
"none"
|
||||
nil
|
||||
|
||||
;; Default = innerHTML
|
||||
:else
|
||||
(if (dom-is-fragment? new-nodes)
|
||||
(morph-children target new-nodes)
|
||||
(let ((wrapper (dom-create-element "div" nil)))
|
||||
(dom-append wrapper new-nodes)
|
||||
(morph-children target wrapper))))))
|
||||
|
||||
|
||||
(define insert-remaining-siblings
|
||||
(fn (parent ref-node sib)
|
||||
;; Insert sibling chain after ref-node
|
||||
(when sib
|
||||
(let ((next (dom-next-sibling sib)))
|
||||
(dom-insert-after ref-node sib)
|
||||
(insert-remaining-siblings parent sib next)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; String-based swap (fallback for HTML responses)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define swap-html-string
|
||||
(fn (target html strategy)
|
||||
;; Execute a swap strategy using an HTML string (DOMParser pipeline).
|
||||
(case strategy
|
||||
"innerHTML"
|
||||
(dom-set-inner-html target html)
|
||||
"outerHTML"
|
||||
(let ((parent (dom-parent target)))
|
||||
(dom-insert-adjacent-html target "afterend" html)
|
||||
(dom-remove-child parent target)
|
||||
parent)
|
||||
"afterend"
|
||||
(dom-insert-adjacent-html target "afterend" html)
|
||||
"beforeend"
|
||||
(dom-insert-adjacent-html target "beforeend" html)
|
||||
"afterbegin"
|
||||
(dom-insert-adjacent-html target "afterbegin" html)
|
||||
"beforebegin"
|
||||
(dom-insert-adjacent-html target "beforebegin" html)
|
||||
"delete"
|
||||
(dom-remove-child (dom-parent target) target)
|
||||
"none"
|
||||
nil
|
||||
:else
|
||||
(dom-set-inner-html target html))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; History management
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define handle-history
|
||||
(fn (el url resp-headers)
|
||||
;; Process history push/replace based on element attrs and response headers
|
||||
(let ((push-url (dom-get-attr el "sx-push-url"))
|
||||
(replace-url (dom-get-attr el "sx-replace-url"))
|
||||
(hdr-replace (get resp-headers "replace-url")))
|
||||
(cond
|
||||
;; Server override
|
||||
hdr-replace
|
||||
(browser-replace-state hdr-replace)
|
||||
;; Client push
|
||||
(and push-url (not (= push-url "false")))
|
||||
(browser-push-state
|
||||
(if (= push-url "true") url push-url))
|
||||
;; Client replace
|
||||
(and replace-url (not (= replace-url "false")))
|
||||
(browser-replace-state
|
||||
(if (= replace-url "true") url replace-url))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Preload cache
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define PRELOAD_TTL 30000) ;; 30 seconds
|
||||
|
||||
(define preload-cache-get
|
||||
(fn (cache url)
|
||||
;; Get and consume a cached preload response.
|
||||
;; Returns (dict "text" ... "content-type" ...) or nil.
|
||||
(let ((entry (dict-get cache url)))
|
||||
(if (nil? entry)
|
||||
nil
|
||||
(if (> (- (now-ms) (get entry "timestamp")) PRELOAD_TTL)
|
||||
(do (dict-delete! cache url) nil)
|
||||
(do (dict-delete! cache url) entry))))))
|
||||
|
||||
|
||||
(define preload-cache-set
|
||||
(fn (cache url text content-type)
|
||||
;; Store a preloaded response
|
||||
(dict-set! cache url
|
||||
(dict "text" text "content-type" content-type "timestamp" (now-ms)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Trigger dispatch table
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Maps trigger event names to binding strategies.
|
||||
;; This is the logic; actual browser event binding is platform interface.
|
||||
|
||||
(define classify-trigger
|
||||
(fn (trigger)
|
||||
;; Classify a parsed trigger descriptor for binding.
|
||||
;; Returns one of: "poll", "intersect", "load", "revealed", "event"
|
||||
(let ((event (get trigger "event")))
|
||||
(cond
|
||||
(= event "every") "poll"
|
||||
(= event "intersect") "intersect"
|
||||
(= event "load") "load"
|
||||
(= event "revealed") "revealed"
|
||||
:else "event"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Boost logic
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define should-boost-link?
|
||||
(fn (link)
|
||||
;; Whether a link inside an sx-boost container should be boosted
|
||||
(let ((href (dom-get-attr link "href")))
|
||||
(and href
|
||||
(not (starts-with? href "#"))
|
||||
(not (starts-with? href "javascript:"))
|
||||
(not (starts-with? href "mailto:"))
|
||||
(browser-same-origin? href)
|
||||
(not (dom-has-attr? link "sx-get"))
|
||||
(not (dom-has-attr? link "sx-post"))
|
||||
(not (dom-has-attr? link "sx-disable"))))))
|
||||
|
||||
|
||||
(define should-boost-form?
|
||||
(fn (form)
|
||||
;; Whether a form inside an sx-boost container should be boosted
|
||||
(and (not (dom-has-attr? form "sx-get"))
|
||||
(not (dom-has-attr? form "sx-post"))
|
||||
(not (dom-has-attr? form "sx-disable")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; SSE event classification
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define parse-sse-swap
|
||||
(fn (el)
|
||||
;; Parse sx-sse-swap attribute
|
||||
;; Returns event name to listen for (default "message")
|
||||
(or (dom-get-attr el "sx-sse-swap") "message")))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — Engine
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Browser/Network:
|
||||
;; (browser-location-href) → current URL string
|
||||
;; (browser-same-origin? url) → boolean
|
||||
;; (browser-push-state url) → void (history.pushState)
|
||||
;; (browser-replace-state url) → void (history.replaceState)
|
||||
;; (browser-navigate url) → void (location.assign)
|
||||
;; (browser-reload) → void (location.reload)
|
||||
;; (browser-scroll-to x y) → void
|
||||
;; (browser-media-matches? query) → boolean (matchMedia)
|
||||
;; (browser-confirm msg) → boolean
|
||||
;; (browser-prompt msg) → string or nil
|
||||
;; (now-ms) → current timestamp in milliseconds
|
||||
;;
|
||||
;; Fetch:
|
||||
;; (browser-fetch url opts) → Promise-like
|
||||
;; (browser-abort-controller) → AbortController
|
||||
;;
|
||||
;; DOM query:
|
||||
;; (dom-query sel) → Element or nil
|
||||
;; (dom-query-all root sel) → list of Elements
|
||||
;; (dom-id el) → string id or nil
|
||||
;; (dom-parent el) → parent Element
|
||||
;; (dom-first-child el) → first child node
|
||||
;; (dom-next-sibling el) → next sibling node
|
||||
;; (dom-child-list el) → list of child nodes
|
||||
;; (dom-tag-name el) → uppercase tag name
|
||||
;;
|
||||
;; DOM mutation:
|
||||
;; (dom-create-element tag ns) → Element
|
||||
;; (dom-append parent child) → void
|
||||
;; (dom-prepend parent child) → void
|
||||
;; (dom-insert-before parent node ref) → void
|
||||
;; (dom-insert-after ref node) → void
|
||||
;; (dom-remove-child parent child) → void
|
||||
;; (dom-replace-child parent new old) → void
|
||||
;; (dom-clone node) → deep clone
|
||||
;;
|
||||
;; DOM attributes:
|
||||
;; (dom-get-attr el name) → string or nil
|
||||
;; (dom-set-attr el name val) → void
|
||||
;; (dom-remove-attr el name) → void
|
||||
;; (dom-has-attr? el name) → boolean
|
||||
;; (dom-attr-list el) → list of (name value) pairs
|
||||
;;
|
||||
;; DOM properties:
|
||||
;; (dom-get-prop el name) → value
|
||||
;; (dom-set-prop el name val) → void
|
||||
;; (dom-get-style el prop) → string
|
||||
;; (dom-set-style el prop val) → void
|
||||
;; (dom-add-class el cls) → void
|
||||
;; (dom-remove-class el cls) → void
|
||||
;;
|
||||
;; DOM inspection:
|
||||
;; (dom-node-type el) → number (1=element, 3=text, 8=comment, 11=fragment)
|
||||
;; (dom-node-name el) → string (uppercase tag or #text/#comment)
|
||||
;; (dom-text-content el) → string
|
||||
;; (dom-set-text-content el s) → void
|
||||
;; (dom-is-fragment? el) → boolean (nodeType === 11)
|
||||
;; (dom-is-child-of? child parent) → boolean
|
||||
;; (dom-is-active-element? el) → boolean (el === document.activeElement)
|
||||
;; (dom-is-input-element? el) → boolean (INPUT/TEXTAREA/SELECT)
|
||||
;;
|
||||
;; DOM content:
|
||||
;; (dom-set-inner-html el html) → void
|
||||
;; (dom-insert-adjacent-html el pos html) → void
|
||||
;; (dom-parse-html-string text) → parsed document
|
||||
;;
|
||||
;; Events:
|
||||
;; (dom-dispatch el name detail) → boolean (dispatchEvent)
|
||||
;; (dom-add-listener el event fn opts) → void
|
||||
;; (dom-remove-listener el event fn) → void
|
||||
;;
|
||||
;; Parsing:
|
||||
;; (parse-header-value s) → dict (parse JSON or SX dict from string)
|
||||
;;
|
||||
;; Misc:
|
||||
;; (dict-has? d key) → boolean
|
||||
;; (dict-delete! d key) → void
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -446,7 +446,8 @@
|
||||
|
||||
(define parse-comp-params
|
||||
(fn (params-expr)
|
||||
;; Parse (&key param1 param2 &rest children) → (params has-children)
|
||||
;; Parse (&key param1 param2 &children) → (params has-children)
|
||||
;; Also accepts &rest as synonym for &children.
|
||||
(let ((params (list))
|
||||
(has-children false)
|
||||
(in-key false))
|
||||
@@ -455,12 +456,12 @@
|
||||
(when (= (type-of p) "symbol")
|
||||
(let ((name (symbol-name p)))
|
||||
(cond
|
||||
(= name "&key") (set! in-key true)
|
||||
(= name "&rest") (set! has-children true)
|
||||
(and in-key (not has-children))
|
||||
(append! params name)
|
||||
:else
|
||||
(append! params name)))))
|
||||
(= name "&key") (set! in-key true)
|
||||
(= name "&rest") (set! has-children true)
|
||||
(= name "&children") (set! has-children true)
|
||||
has-children nil ;; skip params after &children/&rest
|
||||
in-key (append! params name)
|
||||
:else (append! params name)))))
|
||||
params-expr)
|
||||
(list params has-children))))
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
;; ==========================================================================
|
||||
;; render.sx — Reference rendering specification
|
||||
;; render.sx — Core rendering specification
|
||||
;;
|
||||
;; Defines how evaluated SX expressions become output (DOM nodes, HTML
|
||||
;; strings, or SX wire format). Each target provides a renderer adapter
|
||||
;; that implements the platform-specific output operations.
|
||||
;; Shared registries and utilities used by all rendering adapters.
|
||||
;; This file defines WHAT is renderable (tag registries, attribute rules)
|
||||
;; and HOW arguments are parsed — but not the output format.
|
||||
;;
|
||||
;; Three rendering modes (matching the Python/JS implementations):
|
||||
;; Adapters:
|
||||
;; adapter-html.sx — HTML string output (server)
|
||||
;; adapter-sx.sx — SX wire format output (server → client)
|
||||
;; adapter-dom.sx — Live DOM node output (browser)
|
||||
;;
|
||||
;; 1. render-to-dom — produces DOM nodes (browser only)
|
||||
;; 2. render-to-html — produces HTML string (server)
|
||||
;; 3. render-to-sx — produces SX wire format (server → client)
|
||||
;;
|
||||
;; This file specifies the LOGIC of rendering. Platform-specific
|
||||
;; operations are declared as interfaces at the bottom.
|
||||
;; Each adapter imports these shared definitions and provides its own
|
||||
;; render entry point (render-to-html, render-to-sx, render-to-dom).
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
@@ -69,104 +68,13 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-to-html — server-side HTML rendering
|
||||
;; Shared utilities
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-to-html
|
||||
(fn (expr env)
|
||||
(case (type-of expr)
|
||||
;; Literals — render directly
|
||||
"nil" ""
|
||||
"string" (escape-html expr)
|
||||
"number" (str expr)
|
||||
"boolean" (if expr "true" "false")
|
||||
;; List — dispatch to render-list which handles HTML tags, special forms, etc.
|
||||
"list" (if (empty? expr) "" (render-list-to-html expr env))
|
||||
;; Symbol — evaluate then render
|
||||
"symbol" (render-value-to-html (trampoline (eval-expr expr env)) env)
|
||||
;; Keyword — render as text
|
||||
"keyword" (escape-html (keyword-name expr))
|
||||
;; Everything else — evaluate first
|
||||
:else (render-value-to-html (trampoline (eval-expr expr env)) env))))
|
||||
|
||||
(define render-value-to-html
|
||||
(fn (val env)
|
||||
(case (type-of val)
|
||||
"nil" ""
|
||||
"string" (escape-html val)
|
||||
"number" (str val)
|
||||
"boolean" (if val "true" "false")
|
||||
"list" (render-list-to-html val env)
|
||||
"raw-html" (raw-html-content val)
|
||||
"style-value" (style-value-class val)
|
||||
:else (escape-html (str val)))))
|
||||
|
||||
(define render-list-to-html
|
||||
(fn (expr env)
|
||||
(if (empty? expr)
|
||||
""
|
||||
(let ((head (first expr)))
|
||||
(if (not (= (type-of head) "symbol"))
|
||||
;; Data list — render each item
|
||||
(join "" (map (fn (x) (render-value-to-html x env)) expr))
|
||||
(let ((name (symbol-name head))
|
||||
(args (rest expr)))
|
||||
(cond
|
||||
;; Fragment
|
||||
(= name "<>")
|
||||
(join "" (map (fn (x) (render-to-html x env)) args))
|
||||
|
||||
;; Raw HTML passthrough
|
||||
(= name "raw!")
|
||||
(join "" (map (fn (x) (str (trampoline (eval-expr x env)))) args))
|
||||
|
||||
;; HTML tag
|
||||
(contains? HTML_TAGS name)
|
||||
(render-html-element name args env)
|
||||
|
||||
;; Component call (~name)
|
||||
(starts-with? name "~")
|
||||
(let ((comp (env-get env name)))
|
||||
(if (component? comp)
|
||||
(render-to-html
|
||||
(trampoline (call-component comp args env))
|
||||
env)
|
||||
(error (str "Unknown component: " name))))
|
||||
|
||||
;; Definitions — evaluate for side effects, render nothing
|
||||
(or (= name "define") (= name "defcomp") (= name "defmacro")
|
||||
(= name "defstyle") (= name "defkeyframes") (= name "defhandler"))
|
||||
(do (trampoline (eval-expr expr env)) "")
|
||||
|
||||
;; Macro expansion
|
||||
(and (env-has? env name) (macro? (env-get env name)))
|
||||
(render-to-html
|
||||
(trampoline
|
||||
(eval-expr
|
||||
(expand-macro (env-get env name) args env)
|
||||
env))
|
||||
env)
|
||||
|
||||
;; Special form / function call — evaluate then render result
|
||||
:else
|
||||
(render-value-to-html
|
||||
(trampoline (eval-expr expr env))
|
||||
env))))))))
|
||||
|
||||
|
||||
(define render-html-element
|
||||
(fn (tag args env)
|
||||
(let ((parsed (parse-element-args args env))
|
||||
(attrs (first parsed))
|
||||
(children (nth parsed 1))
|
||||
(is-void (contains? VOID_ELEMENTS tag)))
|
||||
(str "<" tag
|
||||
(render-attrs attrs)
|
||||
(if is-void
|
||||
" />"
|
||||
(str ">"
|
||||
(join "" (map (fn (c) (render-to-html c env)) children))
|
||||
"</" tag ">"))))))
|
||||
(define definition-form?
|
||||
(fn (name)
|
||||
(or (= name "define") (= name "defcomp") (= name "defmacro")
|
||||
(= name "defstyle") (= name "defkeyframes") (= name "defhandler"))))
|
||||
|
||||
|
||||
(define parse-element-args
|
||||
@@ -178,7 +86,7 @@
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false)
|
||||
(assoc state "skip" false "i" (inc (get state "i")))
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
(let ((val (trampoline (eval-expr (nth args (inc (get state "i"))) env))))
|
||||
@@ -194,6 +102,8 @@
|
||||
|
||||
(define render-attrs
|
||||
(fn (attrs)
|
||||
;; Render an attrs dict to an HTML attribute string.
|
||||
;; Used by adapter-html.sx and adapter-sx.sx.
|
||||
(join ""
|
||||
(map
|
||||
(fn (key)
|
||||
@@ -215,141 +125,14 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-to-sx — server-side SX wire format (for client rendering)
|
||||
;; --------------------------------------------------------------------------
|
||||
;; This mode serializes the expression as SX source text.
|
||||
;; Component calls are NOT expanded — they're sent to the client.
|
||||
;; HTML tags are serialized as-is. Special forms are evaluated.
|
||||
|
||||
(define render-to-sx
|
||||
(fn (expr env)
|
||||
(let ((result (aser expr env)))
|
||||
(serialize result))))
|
||||
|
||||
(define aser
|
||||
(fn (expr env)
|
||||
;; Evaluate for SX wire format — serialize rendering forms,
|
||||
;; evaluate control flow and function calls.
|
||||
(case (type-of expr)
|
||||
"number" expr
|
||||
"string" expr
|
||||
"boolean" expr
|
||||
"nil" nil
|
||||
|
||||
"symbol"
|
||||
(let ((name (symbol-name expr)))
|
||||
(cond
|
||||
(env-has? env name) (env-get env name)
|
||||
(primitive? name) (get-primitive name)
|
||||
(= name "true") true
|
||||
(= name "false") false
|
||||
(= name "nil") nil
|
||||
:else (error (str "Undefined symbol: " name))))
|
||||
|
||||
"keyword" (keyword-name expr)
|
||||
|
||||
"list"
|
||||
(if (empty? expr)
|
||||
(list)
|
||||
(aser-list expr env))
|
||||
|
||||
:else expr)))
|
||||
|
||||
|
||||
(define aser-list
|
||||
(fn (expr env)
|
||||
(let ((head (first expr))
|
||||
(args (rest expr)))
|
||||
(if (not (= (type-of head) "symbol"))
|
||||
(map (fn (x) (aser x env)) expr)
|
||||
(let ((name (symbol-name head)))
|
||||
(cond
|
||||
;; Fragment — serialize children
|
||||
(= name "<>")
|
||||
(aser-fragment args env)
|
||||
|
||||
;; Component call — serialize WITHOUT expanding
|
||||
(starts-with? name "~")
|
||||
(aser-call name args env)
|
||||
|
||||
;; HTML tag — serialize
|
||||
(contains? HTML_TAGS name)
|
||||
(aser-call name args env)
|
||||
|
||||
;; Special/HO forms — evaluate (produces data)
|
||||
(or (special-form? name) (ho-form? name))
|
||||
(aser-special name expr env)
|
||||
|
||||
;; Macro — expand then aser
|
||||
(and (env-has? env name) (macro? (env-get env name)))
|
||||
(aser (expand-macro (env-get env name) args env) env)
|
||||
|
||||
;; Function call — evaluate fully
|
||||
:else
|
||||
(let ((f (trampoline (eval-expr head env)))
|
||||
(evaled-args (map (fn (a) (trampoline (eval-expr a env))) args)))
|
||||
(cond
|
||||
(and (callable? f) (not (lambda? f)) (not (component? f)))
|
||||
(apply f evaled-args)
|
||||
(lambda? f)
|
||||
(trampoline (call-lambda f evaled-args env))
|
||||
(component? f)
|
||||
(aser-call (str "~" (component-name f)) args env)
|
||||
:else (error (str "Not callable: " (inspect f)))))))))))
|
||||
|
||||
|
||||
(define aser-fragment
|
||||
(fn (children env)
|
||||
;; Serialize (<> child1 child2 ...) to sx source string
|
||||
(let ((parts (filter
|
||||
(fn (x) (not (nil? x)))
|
||||
(map (fn (c) (aser c env)) children))))
|
||||
(if (empty? parts)
|
||||
""
|
||||
(str "(<> " (join " " (map serialize parts)) ")")))))
|
||||
|
||||
|
||||
(define aser-call
|
||||
(fn (name args env)
|
||||
;; Serialize (name :key val child ...) — evaluate args but keep as sx
|
||||
(let ((parts (list name)))
|
||||
(reduce
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false)
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
(let ((val (aser (nth args (inc (get state "i"))) env)))
|
||||
(when (not (nil? val))
|
||||
(append! parts (str ":" (keyword-name arg)))
|
||||
(append! parts (serialize val)))
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
(let ((val (aser arg env)))
|
||||
(when (not (nil? val))
|
||||
(append! parts (serialize val)))
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
(str "(" (join " " parts) ")"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform rendering interface
|
||||
;; Platform interface (shared across adapters)
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; HTML rendering (server targets):
|
||||
;; HTML/attribute escaping (used by HTML and SX wire adapters):
|
||||
;; (escape-html s) → HTML-escaped string
|
||||
;; (escape-attr s) → attribute-value-escaped string
|
||||
;; (raw-html-content r) → unwrap RawHTML marker to string
|
||||
;;
|
||||
;; DOM rendering (browser target):
|
||||
;; (create-element tag) → DOM Element
|
||||
;; (create-text-node s) → DOM Text
|
||||
;; (create-fragment) → DOM DocumentFragment
|
||||
;; (set-attribute el k v) → void
|
||||
;; (append-child parent c) → void
|
||||
;;
|
||||
;; StyleValue:
|
||||
;; (style-value? x) → boolean (is x a StyleValue?)
|
||||
;; (style-value-class sv) → string (CSS class name)
|
||||
@@ -357,7 +140,7 @@
|
||||
;; Serialization:
|
||||
;; (serialize val) → SX source string representation of val
|
||||
;;
|
||||
;; Form classification:
|
||||
;; Form classification (used by SX wire adapter):
|
||||
;; (special-form? name) → boolean
|
||||
;; (ho-form? name) → boolean
|
||||
;; (aser-special name expr env) → evaluate special/HO form through aser
|
||||
|
||||
Reference in New Issue
Block a user