Skip to content

Commit 7d29054

Browse files
Restore support for text expressions
1 parent 0e345de commit 7d29054

2 files changed

Lines changed: 123 additions & 5 deletions

File tree

collagraph/sfc/compiler.py

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,63 @@ def ast_create_list_fragment(name: str, parent: str | None) -> ast.Assign:
368368
)
369369

370370

371+
def parse_text_interpolations(text: str) -> list[tuple[bool, str]]:
372+
"""
373+
Parse text with {{expression}} interpolations.
374+
375+
Returns a list of tuples where:
376+
- (False, str) represents static text
377+
- (True, str) represents an expression to be evaluated
378+
379+
Examples:
380+
"Hello {{name}}" -> [(False, "Hello "), (True, "name")]
381+
"Static text" -> [(False, "Static text")]
382+
"{{a}} and {{b}}" -> [(True, "a"), (False, " and "), (True, "b")]
383+
"""
384+
result = []
385+
i = 0
386+
n = len(text)
387+
388+
while i < n:
389+
# Look for start of interpolation
390+
start = text.find("{{", i)
391+
if start == -1:
392+
# No more interpolations, add remaining text
393+
if i < n:
394+
result.append((False, text[i:]))
395+
break
396+
397+
# Check for escape sequence \{{
398+
if start > 0 and text[start - 1] == "\\":
399+
# Escaped - include text up to and including the {{ as static
400+
# but remove the backslash
401+
if i < start - 1:
402+
result.append((False, text[i : start - 1]))
403+
result.append((False, "{{"))
404+
i = start + 2
405+
continue
406+
407+
# Add static text before the interpolation
408+
if start > i:
409+
result.append((False, text[i:start]))
410+
411+
# Find the end of the interpolation
412+
end = text.find("}}", start + 2)
413+
if end == -1:
414+
# Unclosed interpolation - treat rest as static
415+
result.append((False, text[start:]))
416+
break
417+
418+
# Extract the expression
419+
expr = text[start + 2 : end].strip()
420+
if expr:
421+
result.append((True, expr))
422+
423+
i = end + 2
424+
425+
return result
426+
427+
371428
def safe_tag(tag):
372429
return tag.replace("-", "_").replace(".", "_")
373430

@@ -550,9 +607,72 @@ def create_children(
550607
# Ignore comments
551608
continue
552609
if isinstance(child, TextElement):
553-
# children_args.extend(args_for_text_element(child, names=names))
554-
# continue
555-
raise NotImplementedError()
610+
# Create a text element fragment
611+
text_el = f"text{counter['text']}"
612+
counter["text"] += 1
613+
614+
# Create the TEXT_ELEMENT fragment
615+
result.append(
616+
ast.Assign(
617+
targets=[ast.Name(id=text_el, ctx=ast.Store())],
618+
value=ast.Call(
619+
func=ast.Name(id="Fragment", ctx=ast.Load()),
620+
args=[ast.Name(id="renderer", ctx=ast.Load())],
621+
keywords=[
622+
ast.keyword(
623+
arg="tag", value=ast.Constant(value="TEXT_ELEMENT")
624+
),
625+
ast.keyword(
626+
arg="parent",
627+
value=ast.Name(id=target, ctx=ast.Load())
628+
if target
629+
else ast.Constant(value=None),
630+
),
631+
],
632+
),
633+
)
634+
)
635+
636+
# Parse the text content for interpolations
637+
text_content = child.content
638+
has_interpolation = "{{" in text_content and "}}" in text_content
639+
640+
if has_interpolation:
641+
# Create a dynamic bind for text with interpolations
642+
# Build an f-string expression from the text
643+
parts = parse_text_interpolations(text_content)
644+
expr_parts = []
645+
for is_expr, part in parts:
646+
if is_expr:
647+
expr_parts.append("{" + part + "}")
648+
else:
649+
# Escape braces and backslashes in static parts for f-string
650+
# First escape backslashes that precede braces to avoid
651+
# invalid escape sequences like \{ in the f-string
652+
escaped = part.replace("\\{", "\\\\{").replace(
653+
"\\}", "\\\\}"
654+
)
655+
# Then escape standalone braces for f-string syntax
656+
escaped = escaped.replace("{", "{{").replace("}", "}}")
657+
expr_parts.append(escaped)
658+
fstring_content = "".join(expr_parts)
659+
# Create the bind with an f-string
660+
source = ast.parse(
661+
f'{text_el}.set_bind("content", lambda: f"{fstring_content}")',
662+
mode="eval",
663+
)
664+
result.append(
665+
ast_named_lambda(
666+
source,
667+
{"renderer", "new", text_el, "watch"} | names,
668+
list_names,
669+
)
670+
)
671+
else:
672+
# Static text - set attribute directly
673+
result.append(ast_set_attribute(text_el, "content", text_content))
674+
675+
continue
556676

557677
# Create element name
558678
tag = safe_tag(child.tag)

tests/test_text_expressions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import pytest
21
from observ import reactive
32

43
from collagraph import Collagraph, DictRenderer, EventLoopType
54
from collagraph.renderers.dict_renderer import format_dict
65

76

8-
@pytest.mark.xfail
97
def test_text_elements():
108
from tests.data.text.expressions import Example
119

0 commit comments

Comments
 (0)