From aed4c035377d5a00aa1138564fb9388ced0470d4 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 5 Mar 2026 01:52:45 +0000 Subject: [PATCH] Fix highlight undefined symbol by expanding component results server-side When defpage content expressions use case/if branches that resolve to component calls (e.g. `(case slug "intro" (~docs-intro-content) ...)`), _aser serializes them for the client. Components containing Python-only helpers like `highlight` then fail with "Undefined symbol" on the client. Add _maybe_expand_component_result() which detects when the evaluated result (SxExpr or string) is a component call starting with "(~" and re-parses + expands it through async_eval_slot_to_sx server-side. Co-Authored-By: Claude Opus 4.6 --- shared/sx/async_eval.py | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/shared/sx/async_eval.py b/shared/sx/async_eval.py index bb617cf..395b10d 100644 --- a/shared/sx/async_eval.py +++ b/shared/sx/async_eval.py @@ -1018,6 +1018,31 @@ async def async_eval_to_sx( return SxExpr(serialize(result)) +async def _maybe_expand_component_result( + result: Any, + env: dict[str, Any], + ctx: RequestContext, +) -> Any: + """If *result* is a component call (SxExpr or string starting with + ``(~``), re-parse and expand it server-side. + + This ensures Python-only helpers (e.g. ``highlight``) inside the + component body are evaluated on the server rather than being + serialized for the client where they don't exist. + """ + raw = None + if isinstance(result, SxExpr): + raw = str(result).strip() + elif isinstance(result, str): + raw = result.strip() + if raw and raw.startswith("(~"): + from .parser import parse_all + parsed = parse_all(raw) + if parsed: + return await async_eval_slot_to_sx(parsed[0], env, ctx) + return result + + async def async_eval_slot_to_sx( expr: Any, env: dict[str, Any], @@ -1059,21 +1084,16 @@ async def async_eval_slot_to_sx( ) # Fall back to normal async_eval_to_sx result = await _aser(expr, env, ctx) + # If the result is a component call (from case/if/let branches or + # page helpers returning strings), re-parse and expand it server-side + # so that Python-only helpers like ``highlight`` in the component body + # get evaluated here, not on the client. + result = await _maybe_expand_component_result(result, env, ctx) if isinstance(result, SxExpr): return result if result is None or result is NIL: return SxExpr("") if isinstance(result, str): - # Page helpers return sx source strings. If the string is a - # component call (starts with "(~"), re-parse and expand it - # server-side so that Python-only helpers like ``highlight`` - # inside the component body get evaluated here, not on the client. - stripped = result.strip() - if stripped.startswith("(~"): - from .parser import parse_all - parsed = parse_all(stripped) - if parsed: - return await async_eval_slot_to_sx(parsed[0], env, ctx) return SxExpr(result) return SxExpr(serialize(result))