Fix render-expr in eval position: wrap result in raw-html

The spec's eval-list calls render-expr for HTML tags/components in eval
position but returned a plain string. When that string was later passed
through _arender (e.g. as a component keyword arg), it got HTML-escaped.

Fix in eval.sx: wrap render-expr result in make-raw-html so the value
carries the raw-html type through any evaluator boundary. Also add
is_render_expr check in async_eval_ref.py as belt-and-suspenders for
the same issue in the async wrapper.

This fixes the streaming demo where suspense placeholder divs were
displayed as escaped text instead of real DOM elements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 18:58:42 +00:00
parent 309579aec7
commit 668a46bec0
3 changed files with 20 additions and 5 deletions

View File

@@ -87,9 +87,19 @@ async def _async_eval(expr, env, ctx):
args, kwargs = await _parse_io_args(expr[1:], env, ctx)
return await execute_io(head.name, args, kwargs, ctx)
# Check if this is a render expression (HTML tag, component, fragment)
# so we can wrap the result in _RawHTML to prevent double-escaping.
# The sync evaluator returns plain strings from render_list_to_html;
# the async renderer would HTML-escape those without this wrapper.
is_render = isinstance(expr, list) and sx_ref.is_render_expr(expr)
# For everything else, use the sync transpiled evaluator
result = sx_ref.eval_expr(expr, env)
return sx_ref.trampoline(result)
result = sx_ref.trampoline(result)
if is_render and isinstance(result, str):
return _RawHTML(result)
return result
async def _parse_io_args(exprs, env, ctx):
@@ -124,6 +134,9 @@ async def _arender(expr, env, ctx):
return ""
if isinstance(expr, _RawHTML):
return expr.html
# Also handle sx_ref._RawHTML from the sync evaluator
if isinstance(expr, sx_ref._RawHTML):
return expr.html
if isinstance(expr, str):
return escape_text(expr)
if isinstance(expr, (int, float)):