Add SVG namespace auto-detection, custom elements, html: prefix, and fix filter/map tag collision
- 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];
|
var sf = SPECIAL_FORMS[head.name];
|
||||||
if (sf) return sf(expr, env);
|
if (sf) return sf(expr, env);
|
||||||
var ho = HO_FORMS[head.name];
|
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
|
// Macro expansion
|
||||||
if (head.name in env) {
|
if (head.name in env) {
|
||||||
@@ -1056,12 +1059,16 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
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).
|
* Render an s-expression to DOM node(s).
|
||||||
* Returns a DocumentFragment, Element, or Text node.
|
* 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
|
// nil / false → empty
|
||||||
if (isNil(expr) || expr === false || expr === true) return document.createDocumentFragment();
|
if (isNil(expr) || expr === false || expr === true) return document.createDocumentFragment();
|
||||||
|
|
||||||
@@ -1079,7 +1086,7 @@
|
|||||||
if (typeof expr === "number") return document.createTextNode(String(expr));
|
if (typeof expr === "number") return document.createTextNode(String(expr));
|
||||||
|
|
||||||
// Symbol → evaluate then render
|
// 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
|
// Keyword → text
|
||||||
if (isKw(expr)) return document.createTextNode(expr.name);
|
if (isKw(expr)) return document.createTextNode(expr.name);
|
||||||
@@ -1093,7 +1100,7 @@
|
|||||||
// List → dispatch
|
// List → dispatch
|
||||||
if (Array.isArray(expr)) {
|
if (Array.isArray(expr)) {
|
||||||
if (!expr.length) return document.createDocumentFragment();
|
if (!expr.length) return document.createDocumentFragment();
|
||||||
return renderList(expr, env);
|
return renderList(expr, env, ns);
|
||||||
}
|
}
|
||||||
|
|
||||||
return document.createTextNode(String(expr));
|
return document.createTextNode(String(expr));
|
||||||
@@ -1102,34 +1109,34 @@
|
|||||||
/** Render-aware special forms for DOM output. */
|
/** Render-aware special forms for DOM output. */
|
||||||
var RENDER_FORMS = {};
|
var RENDER_FORMS = {};
|
||||||
|
|
||||||
RENDER_FORMS["if"] = function (expr, env) {
|
RENDER_FORMS["if"] = function (expr, env, ns) {
|
||||||
var cond = sxEval(expr[1], env);
|
var cond = sxEval(expr[1], env);
|
||||||
if (isSxTruthy(cond)) return renderDOM(expr[2], env);
|
if (isSxTruthy(cond)) return renderDOM(expr[2], env, ns);
|
||||||
return expr.length > 3 ? renderDOM(expr[3], env) : document.createDocumentFragment();
|
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();
|
if (!isSxTruthy(sxEval(expr[1], env))) return document.createDocumentFragment();
|
||||||
var frag = 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;
|
return frag;
|
||||||
};
|
};
|
||||||
|
|
||||||
RENDER_FORMS["cond"] = function (expr, env) {
|
RENDER_FORMS["cond"] = function (expr, env, ns) {
|
||||||
var branch = _evalCond(expr.slice(1), env);
|
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 local = _processBindings(expr[1], env);
|
||||||
var frag = document.createDocumentFragment();
|
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;
|
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();
|
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;
|
return frag;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1140,45 +1147,45 @@
|
|||||||
RENDER_FORMS["defmacro"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
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["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 fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
|
||||||
var frag = document.createDocumentFragment();
|
var frag = document.createDocumentFragment();
|
||||||
for (var i = 0; i < coll.length; i++) {
|
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);
|
frag.appendChild(val);
|
||||||
}
|
}
|
||||||
return frag;
|
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 fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
|
||||||
var frag = document.createDocumentFragment();
|
var frag = document.createDocumentFragment();
|
||||||
for (var i = 0; i < coll.length; i++) {
|
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);
|
frag.appendChild(val);
|
||||||
}
|
}
|
||||||
return frag;
|
return frag;
|
||||||
};
|
};
|
||||||
|
|
||||||
RENDER_FORMS["filter"] = function (expr, env) {
|
RENDER_FORMS["filter"] = function (expr, env, ns) {
|
||||||
var result = sxEval(expr, env);
|
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 fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
|
||||||
var frag = document.createDocumentFragment();
|
var frag = document.createDocumentFragment();
|
||||||
for (var i = 0; i < coll.length; i++) {
|
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);
|
frag.appendChild(val);
|
||||||
}
|
}
|
||||||
return frag;
|
return frag;
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderLambdaDOM(fn, args, env) {
|
function renderLambdaDOM(fn, args, env, ns) {
|
||||||
var local = merge({}, fn.closure, env);
|
var local = merge({}, fn.closure, env);
|
||||||
for (var i = 0; i < fn.params.length; i++) local[fn.params[i]] = args[i];
|
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). */
|
/** True when the array expr is a render-only form (HTML tag, <>, raw!, ~comp). */
|
||||||
@@ -1187,7 +1194,7 @@
|
|||||||
var h = v[0];
|
var h = v[0];
|
||||||
if (!isSym(h)) return false;
|
if (!isSym(h)) return false;
|
||||||
var n = h.name;
|
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) {
|
function renderComponentDOM(comp, args, env) {
|
||||||
@@ -1235,7 +1242,7 @@
|
|||||||
return renderDOM(comp.body, local);
|
return renderDOM(comp.body, local);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderList(expr, env) {
|
function renderList(expr, env, ns) {
|
||||||
var head = expr[0];
|
var head = expr[0];
|
||||||
|
|
||||||
if (isSym(head)) {
|
if (isSym(head)) {
|
||||||
@@ -1273,21 +1280,28 @@
|
|||||||
// <> → fragment
|
// <> → fragment
|
||||||
if (name === "<>") {
|
if (name === "<>") {
|
||||||
var f = document.createDocumentFragment();
|
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;
|
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
|
// 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
|
// Macro expansion
|
||||||
if (name in env && isMacro(env[name])) {
|
if (name in env && isMacro(env[name])) {
|
||||||
var mExpanded = expandMacro(env[name], expr.slice(1), env);
|
var mExpanded = expandMacro(env[name], expr.slice(1), env);
|
||||||
return renderDOM(mExpanded, env);
|
return renderDOM(mExpanded, env, ns);
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML tag
|
// 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
|
// Component
|
||||||
if (name.charAt(0) === "~") {
|
if (name.charAt(0) === "~") {
|
||||||
@@ -1303,23 +1317,34 @@
|
|||||||
return warn;
|
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
|
// Fallback: evaluate then render
|
||||||
return renderDOM(sxEval(expr, env), env);
|
return renderDOM(sxEval(expr, env), env, ns);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lambda/list head → evaluate
|
// 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
|
// Data list
|
||||||
var dl = document.createDocumentFragment();
|
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;
|
return dl;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderElement(tag, args, env) {
|
function renderElement(tag, args, env, ns) {
|
||||||
var el = SVG_TAGS[tag]
|
// Detect namespace from tag: svg → SVG_NS, math → MATH_NS
|
||||||
? document.createElementNS(SVG_NS, tag)
|
if (tag === "svg") ns = SVG_NS;
|
||||||
: document.createElement(tag);
|
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 extraClass = null;
|
||||||
var i = 0;
|
var i = 0;
|
||||||
@@ -1345,7 +1370,7 @@
|
|||||||
} else {
|
} else {
|
||||||
// Child
|
// Child
|
||||||
if (!(tag in VOID_ELEMENTS)) {
|
if (!(tag in VOID_ELEMENTS)) {
|
||||||
el.appendChild(renderDOM(arg, env));
|
el.appendChild(renderDOM(arg, env, ns));
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io
|
|||||||
from .parser import SxExpr, serialize
|
from .parser import SxExpr, serialize
|
||||||
from .html import (
|
from .html import (
|
||||||
HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
|
HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
|
||||||
escape_text, escape_attr, _RawHTML, css_class_collector,
|
escape_text, escape_attr, _RawHTML, css_class_collector, _svg_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -643,9 +643,16 @@ async def _arender_list(expr: list, env: dict[str, Any], ctx: RequestContext) ->
|
|||||||
parts.append(await _arender(child, env, ctx))
|
parts.append(await _arender(child, env, ctx))
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
|
|
||||||
|
# html: prefix → force tag rendering
|
||||||
|
if name.startswith("html:"):
|
||||||
|
return await _arender_element(name[5:], expr[1:], env, ctx)
|
||||||
|
|
||||||
# Render-aware special forms
|
# Render-aware special forms
|
||||||
|
# If name is also an HTML tag and first arg is Keyword → tag call
|
||||||
arsf = _ASYNC_RENDER_FORMS.get(name)
|
arsf = _ASYNC_RENDER_FORMS.get(name)
|
||||||
if arsf is not None:
|
if arsf is not None:
|
||||||
|
if name in HTML_TAGS and len(expr) > 1 and isinstance(expr[1], Keyword):
|
||||||
|
return await _arender_element(name, expr[1:], env, ctx)
|
||||||
return await arsf(expr, env, ctx)
|
return await arsf(expr, env, ctx)
|
||||||
|
|
||||||
# Macro expansion
|
# Macro expansion
|
||||||
@@ -665,6 +672,14 @@ async def _arender_list(expr: list, env: dict[str, Any], ctx: RequestContext) ->
|
|||||||
if isinstance(val, Component):
|
if isinstance(val, Component):
|
||||||
return await _arender_component(val, expr[1:], env, ctx)
|
return await _arender_component(val, expr[1:], env, ctx)
|
||||||
|
|
||||||
|
# Custom element (hyphenated name) → render as tag
|
||||||
|
if "-" in name:
|
||||||
|
return await _arender_element(name, expr[1:], env, ctx)
|
||||||
|
|
||||||
|
# SVG/MathML context → unknown names are child elements
|
||||||
|
if _svg_context.get(False):
|
||||||
|
return await _arender_element(name, expr[1:], env, ctx)
|
||||||
|
|
||||||
# Fallback — evaluate then render
|
# Fallback — evaluate then render
|
||||||
result = await async_eval(expr, env, ctx)
|
result = await async_eval(expr, env, ctx)
|
||||||
return await _arender(result, env, ctx)
|
return await _arender(result, env, ctx)
|
||||||
@@ -731,9 +746,19 @@ async def _arender_element(
|
|||||||
if tag in VOID_ELEMENTS:
|
if tag in VOID_ELEMENTS:
|
||||||
return opening
|
return opening
|
||||||
|
|
||||||
child_parts = []
|
# SVG/MathML namespace auto-detection: set context for children
|
||||||
for child in children:
|
token = None
|
||||||
child_parts.append(await _arender(child, env, ctx))
|
if tag in ("svg", "math"):
|
||||||
|
token = _svg_context.set(True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
child_parts = []
|
||||||
|
for child in children:
|
||||||
|
child_parts.append(await _arender(child, env, ctx))
|
||||||
|
finally:
|
||||||
|
if token is not None:
|
||||||
|
_svg_context.reset(token)
|
||||||
|
|
||||||
return f"{opening}{''.join(child_parts)}</{tag}>"
|
return f"{opening}{''.join(child_parts)}</{tag}>"
|
||||||
|
|
||||||
|
|
||||||
@@ -1063,14 +1088,21 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
|
|||||||
if name == "raw!":
|
if name == "raw!":
|
||||||
return await _aser_call("raw!", expr[1:], env, ctx)
|
return await _aser_call("raw!", expr[1:], env, ctx)
|
||||||
|
|
||||||
|
# html: prefix → force tag serialization
|
||||||
|
if name.startswith("html:"):
|
||||||
|
return await _aser_call(name[5:], expr[1:], env, ctx)
|
||||||
|
|
||||||
# Component call — serialize (don't expand)
|
# Component call — serialize (don't expand)
|
||||||
if name.startswith("~"):
|
if name.startswith("~"):
|
||||||
return await _aser_call(name, expr[1:], env, ctx)
|
return await _aser_call(name, expr[1:], env, ctx)
|
||||||
|
|
||||||
# Serialize-mode special/HO forms (checked BEFORE HTML_TAGS
|
# Serialize-mode special/HO forms (checked BEFORE HTML_TAGS
|
||||||
# because some names like "map" are both HTML tags and sx forms)
|
# because some names like "map" are both HTML tags and sx forms).
|
||||||
|
# If name is also an HTML tag and first arg is Keyword → tag call.
|
||||||
sf = _ASER_FORMS.get(name)
|
sf = _ASER_FORMS.get(name)
|
||||||
if sf is not None:
|
if sf is not None:
|
||||||
|
if name in HTML_TAGS and len(expr) > 1 and isinstance(expr[1], Keyword):
|
||||||
|
return await _aser_call(name, expr[1:], env, ctx)
|
||||||
return await sf(expr, env, ctx)
|
return await sf(expr, env, ctx)
|
||||||
|
|
||||||
# HTML tag — serialize (don't render to HTML)
|
# HTML tag — serialize (don't render to HTML)
|
||||||
@@ -1084,6 +1116,14 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
|
|||||||
expanded = _expand_macro(val, expr[1:], env)
|
expanded = _expand_macro(val, expr[1:], env)
|
||||||
return await _aser(expanded, env, ctx)
|
return await _aser(expanded, env, ctx)
|
||||||
|
|
||||||
|
# Custom element (hyphenated name) → serialize as tag
|
||||||
|
if "-" in name:
|
||||||
|
return await _aser_call(name, expr[1:], env, ctx)
|
||||||
|
|
||||||
|
# SVG/MathML context → unknown names are child elements
|
||||||
|
if _svg_context.get(False):
|
||||||
|
return await _aser_call(name, expr[1:], env, ctx)
|
||||||
|
|
||||||
# Function / lambda call — evaluate (produces data, not rendering)
|
# Function / lambda call — evaluate (produces data, not rendering)
|
||||||
fn = await async_eval(head, env, ctx)
|
fn = await async_eval(head, env, ctx)
|
||||||
args = [await async_eval(a, env, ctx) for a in expr[1:]]
|
args = [await async_eval(a, env, ctx) for a in expr[1:]]
|
||||||
@@ -1150,32 +1190,41 @@ async def _aser_call(
|
|||||||
) -> SxExpr:
|
) -> SxExpr:
|
||||||
"""Serialize ``(name :key val child ...)`` — evaluate args but keep
|
"""Serialize ``(name :key val child ...)`` — evaluate args but keep
|
||||||
as sx source instead of rendering to HTML."""
|
as sx source instead of rendering to HTML."""
|
||||||
parts = [name]
|
# SVG/MathML namespace auto-detection for serializer
|
||||||
extra_class: str | None = None # from :style StyleValue conversion
|
token = None
|
||||||
i = 0
|
if name in ("svg", "math"):
|
||||||
while i < len(args):
|
token = _svg_context.set(True)
|
||||||
arg = args[i]
|
|
||||||
if isinstance(arg, Keyword) and i + 1 < len(args):
|
try:
|
||||||
val = await _aser(args[i + 1], env, ctx)
|
parts = [name]
|
||||||
if val is not NIL and val is not None:
|
extra_class: str | None = None # from :style StyleValue conversion
|
||||||
# :style StyleValue → convert to :class and register CSS
|
i = 0
|
||||||
if arg.name == "style" and isinstance(val, StyleValue):
|
while i < len(args):
|
||||||
from .css_registry import register_generated_rule
|
arg = args[i]
|
||||||
register_generated_rule(val)
|
if isinstance(arg, Keyword) and i + 1 < len(args):
|
||||||
extra_class = val.class_name
|
val = await _aser(args[i + 1], env, ctx)
|
||||||
else:
|
if val is not NIL and val is not None:
|
||||||
parts.append(f":{arg.name}")
|
# :style StyleValue → convert to :class and register CSS
|
||||||
parts.append(serialize(val))
|
if arg.name == "style" and isinstance(val, StyleValue):
|
||||||
i += 2
|
from .css_registry import register_generated_rule
|
||||||
else:
|
register_generated_rule(val)
|
||||||
result = await _aser(arg, env, ctx)
|
extra_class = val.class_name
|
||||||
if result is not NIL and result is not None:
|
else:
|
||||||
parts.append(serialize(result))
|
parts.append(f":{arg.name}")
|
||||||
i += 1
|
parts.append(serialize(val))
|
||||||
# If we converted a :style to a class, merge into existing :class or add it
|
i += 2
|
||||||
if extra_class:
|
else:
|
||||||
_merge_class_into_parts(parts, extra_class)
|
result = await _aser(arg, env, ctx)
|
||||||
return SxExpr("(" + " ".join(parts) + ")")
|
if result is not NIL and result is not None:
|
||||||
|
parts.append(serialize(result))
|
||||||
|
i += 1
|
||||||
|
# If we converted a :style to a class, merge into existing :class or add it
|
||||||
|
if extra_class:
|
||||||
|
_merge_class_into_parts(parts, extra_class)
|
||||||
|
return SxExpr("(" + " ".join(parts) + ")")
|
||||||
|
finally:
|
||||||
|
if token is not None:
|
||||||
|
_svg_context.reset(token)
|
||||||
|
|
||||||
|
|
||||||
def _merge_class_into_parts(parts: list[str], class_name: str) -> None:
|
def _merge_class_into_parts(parts: list[str], class_name: str) -> None:
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ css_class_collector: contextvars.ContextVar[set[str] | None] = contextvars.Conte
|
|||||||
"css_class_collector", default=None
|
"css_class_collector", default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ContextVar for SVG/MathML namespace auto-detection.
|
||||||
|
# When True, unknown tag names inside (svg ...) or (math ...) are treated as elements.
|
||||||
|
_svg_context: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
||||||
|
"_svg_context", default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _RawHTML:
|
class _RawHTML:
|
||||||
"""Marker for pre-rendered HTML that should not be escaped."""
|
"""Marker for pre-rendered HTML that should not be escaped."""
|
||||||
@@ -430,10 +436,19 @@ def _render_list(expr: list, env: dict[str, Any]) -> str:
|
|||||||
if name == "<>":
|
if name == "<>":
|
||||||
return "".join(_render(child, env) for child in expr[1:])
|
return "".join(_render(child, env) for child in expr[1:])
|
||||||
|
|
||||||
|
# --- html: prefix → force tag rendering --------------------------
|
||||||
|
if name.startswith("html:"):
|
||||||
|
return _render_element(name[5:], expr[1:], env)
|
||||||
|
|
||||||
# --- Render-aware special forms --------------------------------------
|
# --- Render-aware special forms --------------------------------------
|
||||||
# Check BEFORE HTML_TAGS because some names overlap (e.g. `map`).
|
# Check BEFORE HTML_TAGS because some names overlap (e.g. `map`).
|
||||||
if name in _RENDER_FORMS:
|
# But if the name is ALSO an HTML tag and first arg is a Keyword,
|
||||||
return _RENDER_FORMS[name](expr, env)
|
# it's a tag call (e.g. (filter :id "x" ...)), not a HO function.
|
||||||
|
rsf = _RENDER_FORMS.get(name)
|
||||||
|
if rsf is not None:
|
||||||
|
if name in HTML_TAGS and len(expr) > 1 and isinstance(expr[1], Keyword):
|
||||||
|
return _render_element(name, expr[1:], env)
|
||||||
|
return rsf(expr, env)
|
||||||
|
|
||||||
# --- Macro expansion → expand then render --------------------------
|
# --- Macro expansion → expand then render --------------------------
|
||||||
if name in env:
|
if name in env:
|
||||||
@@ -453,6 +468,14 @@ def _render_list(expr: list, env: dict[str, Any]) -> str:
|
|||||||
return _render_component(val, expr[1:], env)
|
return _render_component(val, expr[1:], env)
|
||||||
# Fall through to evaluation
|
# Fall through to evaluation
|
||||||
|
|
||||||
|
# --- Custom element (hyphenated name) → render as tag ------------
|
||||||
|
if "-" in name:
|
||||||
|
return _render_element(name, expr[1:], env)
|
||||||
|
|
||||||
|
# --- SVG/MathML context → unknown names are child elements --------
|
||||||
|
if _svg_context.get(False):
|
||||||
|
return _render_element(name, expr[1:], env)
|
||||||
|
|
||||||
# --- Other special forms / function calls → evaluate then render ---
|
# --- Other special forms / function calls → evaluate then render ---
|
||||||
result = _eval(expr, env)
|
result = _eval(expr, env)
|
||||||
return _render(result, env)
|
return _render(result, env)
|
||||||
@@ -524,7 +547,15 @@ def _render_element(tag: str, args: list, env: dict[str, Any]) -> str:
|
|||||||
if tag in VOID_ELEMENTS:
|
if tag in VOID_ELEMENTS:
|
||||||
return opening
|
return opening
|
||||||
|
|
||||||
# Render children
|
# SVG/MathML namespace auto-detection: set context for children
|
||||||
child_html = "".join(_render(child, env) for child in children)
|
token = None
|
||||||
|
if tag in ("svg", "math"):
|
||||||
|
token = _svg_context.set(True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
child_html = "".join(_render(child, env) for child in children)
|
||||||
|
finally:
|
||||||
|
if token is not None:
|
||||||
|
_svg_context.reset(token)
|
||||||
|
|
||||||
return f"{opening}{child_html}</{tag}>"
|
return f"{opening}{child_html}</{tag}>"
|
||||||
|
|||||||
Reference in New Issue
Block a user