Add SVG namespace auto-detection, custom elements, html: prefix, and fix filter/map tag collision
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
- Fix filter/map dispatching as HO functions when used as SVG/HTML tags (peek at first arg — Keyword means tag call, not function call) - Add html: prefix escape hatch to force any name to render as an element - Support custom elements (hyphenated names) per Web Components spec - SVG/MathML namespace auto-detection: client threads ns param through render chain; server uses _svg_context ContextVar so unknown tags inside (svg ...) or (math ...) render as elements without enumeration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -627,7 +627,10 @@
|
||||
var sf = SPECIAL_FORMS[head.name];
|
||||
if (sf) return sf(expr, env);
|
||||
var ho = HO_FORMS[head.name];
|
||||
if (ho) return ho(expr, env);
|
||||
if (ho) {
|
||||
// If name is also an HTML tag and first arg is Keyword → not a HO call
|
||||
if (!(HTML_TAGS[head.name] && expr.length > 1 && isKw(expr[1]))) return ho(expr, env);
|
||||
}
|
||||
|
||||
// Macro expansion
|
||||
if (head.name in env) {
|
||||
@@ -1056,12 +1059,16 @@
|
||||
);
|
||||
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var MATH_NS = "http://www.w3.org/1998/Math/MathML";
|
||||
|
||||
/**
|
||||
* Render an s-expression to DOM node(s).
|
||||
* Returns a DocumentFragment, Element, or Text node.
|
||||
* @param {*} expr - s-expression
|
||||
* @param {Object} env - variable bindings
|
||||
* @param {string|null} ns - namespace URI (SVG_NS or MATH_NS) when inside svg/math
|
||||
*/
|
||||
function renderDOM(expr, env) {
|
||||
function renderDOM(expr, env, ns) {
|
||||
// nil / false → empty
|
||||
if (isNil(expr) || expr === false || expr === true) return document.createDocumentFragment();
|
||||
|
||||
@@ -1079,7 +1086,7 @@
|
||||
if (typeof expr === "number") return document.createTextNode(String(expr));
|
||||
|
||||
// Symbol → evaluate then render
|
||||
if (isSym(expr)) return renderDOM(sxEval(expr, env), env);
|
||||
if (isSym(expr)) return renderDOM(sxEval(expr, env), env, ns);
|
||||
|
||||
// Keyword → text
|
||||
if (isKw(expr)) return document.createTextNode(expr.name);
|
||||
@@ -1093,7 +1100,7 @@
|
||||
// List → dispatch
|
||||
if (Array.isArray(expr)) {
|
||||
if (!expr.length) return document.createDocumentFragment();
|
||||
return renderList(expr, env);
|
||||
return renderList(expr, env, ns);
|
||||
}
|
||||
|
||||
return document.createTextNode(String(expr));
|
||||
@@ -1102,34 +1109,34 @@
|
||||
/** Render-aware special forms for DOM output. */
|
||||
var RENDER_FORMS = {};
|
||||
|
||||
RENDER_FORMS["if"] = function (expr, env) {
|
||||
RENDER_FORMS["if"] = function (expr, env, ns) {
|
||||
var cond = sxEval(expr[1], env);
|
||||
if (isSxTruthy(cond)) return renderDOM(expr[2], env);
|
||||
return expr.length > 3 ? renderDOM(expr[3], env) : document.createDocumentFragment();
|
||||
if (isSxTruthy(cond)) return renderDOM(expr[2], env, ns);
|
||||
return expr.length > 3 ? renderDOM(expr[3], env, ns) : document.createDocumentFragment();
|
||||
};
|
||||
|
||||
RENDER_FORMS["when"] = function (expr, env) {
|
||||
RENDER_FORMS["when"] = function (expr, env, ns) {
|
||||
if (!isSxTruthy(sxEval(expr[1], env))) return document.createDocumentFragment();
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 2; i < expr.length; i++) frag.appendChild(renderDOM(expr[i], env));
|
||||
for (var i = 2; i < expr.length; i++) frag.appendChild(renderDOM(expr[i], env, ns));
|
||||
return frag;
|
||||
};
|
||||
|
||||
RENDER_FORMS["cond"] = function (expr, env) {
|
||||
RENDER_FORMS["cond"] = function (expr, env, ns) {
|
||||
var branch = _evalCond(expr.slice(1), env);
|
||||
return branch ? renderDOM(branch, env) : document.createDocumentFragment();
|
||||
return branch ? renderDOM(branch, env, ns) : document.createDocumentFragment();
|
||||
};
|
||||
|
||||
RENDER_FORMS["let"] = RENDER_FORMS["let*"] = function (expr, env) {
|
||||
RENDER_FORMS["let"] = RENDER_FORMS["let*"] = function (expr, env, ns) {
|
||||
var local = _processBindings(expr[1], env);
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var k = 2; k < expr.length; k++) frag.appendChild(renderDOM(expr[k], local));
|
||||
for (var k = 2; k < expr.length; k++) frag.appendChild(renderDOM(expr[k], local, ns));
|
||||
return frag;
|
||||
};
|
||||
|
||||
RENDER_FORMS["begin"] = RENDER_FORMS["do"] = function (expr, env) {
|
||||
RENDER_FORMS["begin"] = RENDER_FORMS["do"] = function (expr, env, ns) {
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 1; i < expr.length; i++) frag.appendChild(renderDOM(expr[i], env));
|
||||
for (var i = 1; i < expr.length; i++) frag.appendChild(renderDOM(expr[i], env, ns));
|
||||
return frag;
|
||||
};
|
||||
|
||||
@@ -1140,45 +1147,45 @@
|
||||
RENDER_FORMS["defmacro"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defhandler"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
|
||||
RENDER_FORMS["map"] = function (expr, env) {
|
||||
RENDER_FORMS["map"] = function (expr, env, ns) {
|
||||
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 0; i < coll.length; i++) {
|
||||
var val = isLambda(fn) ? renderLambdaDOM(fn, [coll[i]], env) : renderDOM(fn(coll[i]), env);
|
||||
var val = isLambda(fn) ? renderLambdaDOM(fn, [coll[i]], env, ns) : renderDOM(fn(coll[i]), env, ns);
|
||||
frag.appendChild(val);
|
||||
}
|
||||
return frag;
|
||||
};
|
||||
|
||||
RENDER_FORMS["map-indexed"] = function (expr, env) {
|
||||
RENDER_FORMS["map-indexed"] = function (expr, env, ns) {
|
||||
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 0; i < coll.length; i++) {
|
||||
var val = isLambda(fn) ? renderLambdaDOM(fn, [i, coll[i]], env) : renderDOM(fn(i, coll[i]), env);
|
||||
var val = isLambda(fn) ? renderLambdaDOM(fn, [i, coll[i]], env, ns) : renderDOM(fn(i, coll[i]), env, ns);
|
||||
frag.appendChild(val);
|
||||
}
|
||||
return frag;
|
||||
};
|
||||
|
||||
RENDER_FORMS["filter"] = function (expr, env) {
|
||||
RENDER_FORMS["filter"] = function (expr, env, ns) {
|
||||
var result = sxEval(expr, env);
|
||||
return renderDOM(result, env);
|
||||
return renderDOM(result, env, ns);
|
||||
};
|
||||
|
||||
RENDER_FORMS["for-each"] = function (expr, env) {
|
||||
RENDER_FORMS["for-each"] = function (expr, env, ns) {
|
||||
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 0; i < coll.length; i++) {
|
||||
var val = isLambda(fn) ? renderLambdaDOM(fn, [coll[i]], env) : renderDOM(fn(coll[i]), env);
|
||||
var val = isLambda(fn) ? renderLambdaDOM(fn, [coll[i]], env, ns) : renderDOM(fn(coll[i]), env, ns);
|
||||
frag.appendChild(val);
|
||||
}
|
||||
return frag;
|
||||
};
|
||||
|
||||
function renderLambdaDOM(fn, args, env) {
|
||||
function renderLambdaDOM(fn, args, env, ns) {
|
||||
var local = merge({}, fn.closure, env);
|
||||
for (var i = 0; i < fn.params.length; i++) local[fn.params[i]] = args[i];
|
||||
return renderDOM(fn.body, local);
|
||||
return renderDOM(fn.body, local, ns);
|
||||
}
|
||||
|
||||
/** True when the array expr is a render-only form (HTML tag, <>, raw!, ~comp). */
|
||||
@@ -1187,7 +1194,7 @@
|
||||
var h = v[0];
|
||||
if (!isSym(h)) return false;
|
||||
var n = h.name;
|
||||
return !!(HTML_TAGS[n] || SVG_TAGS[n] || n === "<>" || n === "raw!" || n.charAt(0) === "~");
|
||||
return !!(HTML_TAGS[n] || SVG_TAGS[n] || n === "<>" || n === "raw!" || n.charAt(0) === "~" || n.indexOf("html:") === 0 || n.indexOf("-") > 0);
|
||||
}
|
||||
|
||||
function renderComponentDOM(comp, args, env) {
|
||||
@@ -1235,7 +1242,7 @@
|
||||
return renderDOM(comp.body, local);
|
||||
}
|
||||
|
||||
function renderList(expr, env) {
|
||||
function renderList(expr, env, ns) {
|
||||
var head = expr[0];
|
||||
|
||||
if (isSym(head)) {
|
||||
@@ -1273,21 +1280,28 @@
|
||||
// <> → fragment
|
||||
if (name === "<>") {
|
||||
var f = document.createDocumentFragment();
|
||||
for (var fi = 1; fi < expr.length; fi++) f.appendChild(renderDOM(expr[fi], env));
|
||||
for (var fi = 1; fi < expr.length; fi++) f.appendChild(renderDOM(expr[fi], env, ns));
|
||||
return f;
|
||||
}
|
||||
|
||||
// html: prefix → force element rendering
|
||||
if (name.indexOf("html:") === 0) return renderElement(name.substring(5), expr.slice(1), env, ns);
|
||||
|
||||
// Render-aware special forms
|
||||
if (RENDER_FORMS[name]) return RENDER_FORMS[name](expr, env);
|
||||
// If name is also an HTML tag and first arg is Keyword → tag call
|
||||
if (RENDER_FORMS[name]) {
|
||||
if (HTML_TAGS[name] && expr.length > 1 && isKw(expr[1])) return renderElement(name, expr.slice(1), env, ns);
|
||||
return RENDER_FORMS[name](expr, env, ns);
|
||||
}
|
||||
|
||||
// Macro expansion
|
||||
if (name in env && isMacro(env[name])) {
|
||||
var mExpanded = expandMacro(env[name], expr.slice(1), env);
|
||||
return renderDOM(mExpanded, env);
|
||||
return renderDOM(mExpanded, env, ns);
|
||||
}
|
||||
|
||||
// HTML tag
|
||||
if (HTML_TAGS[name]) return renderElement(name, expr.slice(1), env);
|
||||
if (HTML_TAGS[name]) return renderElement(name, expr.slice(1), env, ns);
|
||||
|
||||
// Component
|
||||
if (name.charAt(0) === "~") {
|
||||
@@ -1303,23 +1317,34 @@
|
||||
return warn;
|
||||
}
|
||||
|
||||
// Custom element (hyphenated name) → render as element
|
||||
if (name.indexOf("-") > 0) return renderElement(name, expr.slice(1), env, ns);
|
||||
|
||||
// SVG/MathML namespace auto-detection: inside (svg ...) or (math ...),
|
||||
// unknown tags are created with the inherited namespace
|
||||
if (ns) return renderElement(name, expr.slice(1), env, ns);
|
||||
|
||||
// Fallback: evaluate then render
|
||||
return renderDOM(sxEval(expr, env), env);
|
||||
return renderDOM(sxEval(expr, env), env, ns);
|
||||
}
|
||||
|
||||
// Lambda/list head → evaluate
|
||||
if (isLambda(head) || Array.isArray(head)) return renderDOM(sxEval(expr, env), env);
|
||||
if (isLambda(head) || Array.isArray(head)) return renderDOM(sxEval(expr, env), env, ns);
|
||||
|
||||
// Data list
|
||||
var dl = document.createDocumentFragment();
|
||||
for (var di = 0; di < expr.length; di++) dl.appendChild(renderDOM(expr[di], env));
|
||||
for (var di = 0; di < expr.length; di++) dl.appendChild(renderDOM(expr[di], env, ns));
|
||||
return dl;
|
||||
}
|
||||
|
||||
function renderElement(tag, args, env) {
|
||||
var el = SVG_TAGS[tag]
|
||||
? document.createElementNS(SVG_NS, tag)
|
||||
: document.createElement(tag);
|
||||
function renderElement(tag, args, env, ns) {
|
||||
// Detect namespace from tag: svg → SVG_NS, math → MATH_NS
|
||||
if (tag === "svg") ns = SVG_NS;
|
||||
else if (tag === "math") ns = MATH_NS;
|
||||
|
||||
var el = ns
|
||||
? document.createElementNS(ns, tag)
|
||||
: (SVG_TAGS[tag] ? document.createElementNS(SVG_NS, tag) : document.createElement(tag));
|
||||
|
||||
var extraClass = null;
|
||||
var i = 0;
|
||||
@@ -1345,7 +1370,7 @@
|
||||
} else {
|
||||
// Child
|
||||
if (!(tag in VOID_ELEMENTS)) {
|
||||
el.appendChild(renderDOM(arg, env));
|
||||
el.appendChild(renderDOM(arg, env, ns));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user