diff --git a/shared/static/scripts/sx-ref.js b/shared/static/scripts/sx-ref.js index 4b9b587..c7aff82 100644 --- a/shared/static/scripts/sx-ref.js +++ b/shared/static/scripts/sx-ref.js @@ -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,"""); } 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(""))))); -})(); }; + // 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(""))))); +})(); }; + + + // === 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); \ No newline at end of file diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx new file mode 100644 index 0000000..e3f41cc --- /dev/null +++ b/shared/sx/ref/adapter-dom.sx @@ -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 +;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/adapter-html.sx b/shared/sx/ref/adapter-html.sx new file mode 100644 index 0000000..cf9e702 --- /dev/null +++ b/shared/sx/ref/adapter-html.sx @@ -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)) + "")))))) + + +;; -------------------------------------------------------------------------- +;; 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 +;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/adapter-sx.sx b/shared/sx/ref/adapter-sx.sx new file mode 100644 index 0000000..b84106c --- /dev/null +++ b/shared/sx/ref/adapter-sx.sx @@ -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 +;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index bb53b8a..5c9ac5f 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -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,"""); } 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) diff --git a/shared/sx/ref/engine.sx b/shared/sx/ref/engine.sx new file mode 100644 index 0000000..d4e9a56 --- /dev/null +++ b/shared/sx/ref/engine.sx @@ -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 +;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/eval.sx b/shared/sx/ref/eval.sx index fd1a60e..fc7e68e 100644 --- a/shared/sx/ref/eval.sx +++ b/shared/sx/ref/eval.sx @@ -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)))) diff --git a/shared/sx/ref/render.sx b/shared/sx/ref/render.sx index 624d781..7bead17 100644 --- a/shared/sx/ref/render.sx +++ b/shared/sx/ref/render.sx @@ -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)) - "")))))) +(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