From a84916e82f9c2510a0d355985046972dc5e54a53 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 4 Mar 2026 14:03:02 +0000 Subject: [PATCH] Fix filter/map tag disambiguation inside SVG context without keyword attrs (filter (feTurbulence ...)) inside (svg ...) has no keyword first arg, so the keyword-only check dispatched it as a HO function. Now also check SVG/MathML context (ns in client, _svg_context in server). Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx.js | 4 ++-- shared/sx/async_eval.py | 14 ++++++++++---- shared/sx/html.py | 9 ++++++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/shared/static/scripts/sx.js b/shared/static/scripts/sx.js index 472caad..a94ff97 100644 --- a/shared/static/scripts/sx.js +++ b/shared/static/scripts/sx.js @@ -1288,9 +1288,9 @@ if (name.indexOf("html:") === 0) return renderElement(name.substring(5), expr.slice(1), env, ns); // Render-aware special forms - // If name is also an HTML tag and first arg is Keyword → tag call + // If name is also an HTML tag and (keyword arg or SVG/MathML ns) → tag call if (RENDER_FORMS[name]) { - if (HTML_TAGS[name] && expr.length > 1 && isKw(expr[1])) return renderElement(name, expr.slice(1), env, ns); + if (HTML_TAGS[name] && ((expr.length > 1 && isKw(expr[1])) || ns)) return renderElement(name, expr.slice(1), env, ns); return RENDER_FORMS[name](expr, env, ns); } diff --git a/shared/sx/async_eval.py b/shared/sx/async_eval.py index 28bdf44..2ba461c 100644 --- a/shared/sx/async_eval.py +++ b/shared/sx/async_eval.py @@ -648,10 +648,13 @@ async def _arender_list(expr: list, env: dict[str, Any], ctx: RequestContext) -> return await _arender_element(name[5:], expr[1:], env, ctx) # Render-aware special forms - # If name is also an HTML tag and first arg is Keyword → tag call + # If name is also an HTML tag and (keyword arg or SVG context) → tag call arsf = _ASYNC_RENDER_FORMS.get(name) if arsf is not None: - if name in HTML_TAGS and len(expr) > 1 and isinstance(expr[1], Keyword): + if name in HTML_TAGS and ( + (len(expr) > 1 and isinstance(expr[1], Keyword)) + or _svg_context.get(False) + ): return await _arender_element(name, expr[1:], env, ctx) return await arsf(expr, env, ctx) @@ -1098,10 +1101,13 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any: # Serialize-mode special/HO forms (checked BEFORE HTML_TAGS # 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. + # If name is also an HTML tag and (keyword arg or SVG context) → tag call. sf = _ASER_FORMS.get(name) if sf is not None: - if name in HTML_TAGS and len(expr) > 1 and isinstance(expr[1], Keyword): + if name in HTML_TAGS and ( + (len(expr) > 1 and isinstance(expr[1], Keyword)) + or _svg_context.get(False) + ): return await _aser_call(name, expr[1:], env, ctx) return await sf(expr, env, ctx) diff --git a/shared/sx/html.py b/shared/sx/html.py index d739574..50f5092 100644 --- a/shared/sx/html.py +++ b/shared/sx/html.py @@ -442,11 +442,14 @@ def _render_list(expr: list, env: dict[str, Any]) -> str: # --- Render-aware special forms -------------------------------------- # Check BEFORE HTML_TAGS because some names overlap (e.g. `map`). - # But if the name is ALSO an HTML tag and first arg is a Keyword, - # it's a tag call (e.g. (filter :id "x" ...)), not a HO function. + # But if the name is ALSO an HTML tag and (a) first arg is a Keyword + # or (b) we're inside SVG/MathML context, it's a tag call. rsf = _RENDER_FORMS.get(name) if rsf is not None: - if name in HTML_TAGS and len(expr) > 1 and isinstance(expr[1], Keyword): + if name in HTML_TAGS and ( + (len(expr) > 1 and isinstance(expr[1], Keyword)) + or _svg_context.get(False) + ): return _render_element(name, expr[1:], env) return rsf(expr, env)