|
2 | 2 |
|
3 | 3 | import ast |
4 | 4 | import logging |
| 5 | +import re |
5 | 6 | import sys |
6 | 7 | import textwrap |
7 | 8 | from collections import defaultdict |
@@ -368,6 +369,118 @@ def ast_create_list_fragment(name: str, parent: str | None) -> ast.Assign: |
368 | 369 | ) |
369 | 370 |
|
370 | 371 |
|
| 372 | +def parse_text_interpolations(text: str) -> list[tuple[bool, str]]: |
| 373 | + """ |
| 374 | + Parse text with {{expression}} interpolations. |
| 375 | +
|
| 376 | + Returns a list of tuples where: |
| 377 | + - (False, str) represents static text |
| 378 | + - (True, str) represents an expression to be evaluated |
| 379 | +
|
| 380 | + Examples: |
| 381 | + "Hello {{name}}" -> [(False, "Hello "), (True, "name")] |
| 382 | + "Static text" -> [(False, "Static text")] |
| 383 | + "{{a}} and {{b}}" -> [(True, "a"), (False, " and "), (True, "b")] |
| 384 | + """ |
| 385 | + result = [] |
| 386 | + i = 0 |
| 387 | + n = len(text) |
| 388 | + |
| 389 | + while i < n: |
| 390 | + # Look for start of interpolation |
| 391 | + start = text.find("{{", i) |
| 392 | + if start == -1: |
| 393 | + # No more interpolations, add remaining text |
| 394 | + if i < n: |
| 395 | + result.append((False, text[i:])) |
| 396 | + break |
| 397 | + |
| 398 | + # Check for escape sequence \{{ |
| 399 | + if start > 0 and text[start - 1] == "\\": |
| 400 | + # Escaped - include text up to and including the {{ as static |
| 401 | + # but remove the backslash |
| 402 | + if i < start - 1: |
| 403 | + result.append((False, text[i : start - 1])) |
| 404 | + result.append((False, "{{")) |
| 405 | + i = start + 2 |
| 406 | + continue |
| 407 | + |
| 408 | + # Add static text before the interpolation |
| 409 | + if start > i: |
| 410 | + result.append((False, text[i:start])) |
| 411 | + |
| 412 | + # Find the end of the interpolation |
| 413 | + end = text.find("}}", start + 2) |
| 414 | + if end == -1: |
| 415 | + # Unclosed interpolation - treat rest as static |
| 416 | + result.append((False, text[start:])) |
| 417 | + break |
| 418 | + |
| 419 | + # Extract the expression |
| 420 | + expr = text[start + 2 : end].strip() |
| 421 | + if expr: |
| 422 | + result.append((True, expr)) |
| 423 | + |
| 424 | + i = end + 2 |
| 425 | + |
| 426 | + return result |
| 427 | + |
| 428 | + |
| 429 | +def normalize_multiline_text(text: str) -> str: |
| 430 | + if "\n" not in text and "\r" not in text: |
| 431 | + return text |
| 432 | + return re.sub(r"(\r?\n)[ \t]+", r"\1", text) |
| 433 | + |
| 434 | + |
| 435 | +def ast_set_text_bind( |
| 436 | + el: str, |
| 437 | + parts: list[tuple[bool, str]], |
| 438 | + names: set[str], |
| 439 | + list_names: list[dict[str, set[str]]], |
| 440 | +) -> ast.Expr: |
| 441 | + joined_values: list[ast.Constant | ast.FormattedValue] = [] |
| 442 | + for is_expr, part in parts: |
| 443 | + if is_expr: |
| 444 | + expression_ast = ast.parse(part, mode="eval") |
| 445 | + joined_values.append( |
| 446 | + ast.FormattedValue( |
| 447 | + value=expression_ast.body, |
| 448 | + conversion=-1, |
| 449 | + ) |
| 450 | + ) |
| 451 | + elif part: |
| 452 | + joined_values.append(ast.Constant(value=part)) |
| 453 | + |
| 454 | + source = ast.Expression( |
| 455 | + body=ast.Call( |
| 456 | + func=ast.Attribute( |
| 457 | + value=ast.Name(id=el, ctx=ast.Load()), |
| 458 | + attr="set_bind", |
| 459 | + ctx=ast.Load(), |
| 460 | + ), |
| 461 | + args=[ |
| 462 | + ast.Constant(value="content"), |
| 463 | + ast.Lambda( |
| 464 | + args=ast.arguments( |
| 465 | + posonlyargs=[], |
| 466 | + args=[], |
| 467 | + kwonlyargs=[], |
| 468 | + kw_defaults=[], |
| 469 | + defaults=[], |
| 470 | + ), |
| 471 | + body=ast.JoinedStr(values=joined_values), |
| 472 | + ), |
| 473 | + ], |
| 474 | + keywords=[], |
| 475 | + ) |
| 476 | + ) |
| 477 | + return ast_named_lambda( |
| 478 | + source, |
| 479 | + {"renderer", "new", el, "watch"} | names, |
| 480 | + list_names, |
| 481 | + ) |
| 482 | + |
| 483 | + |
371 | 484 | def safe_tag(tag): |
372 | 485 | return tag.replace("-", "_").replace(".", "_") |
373 | 486 |
|
@@ -550,9 +663,57 @@ def create_children( |
550 | 663 | # Ignore comments |
551 | 664 | continue |
552 | 665 | if isinstance(child, TextElement): |
553 | | - # children_args.extend(args_for_text_element(child, names=names)) |
554 | | - # continue |
555 | | - raise NotImplementedError() |
| 666 | + # Create a text element fragment |
| 667 | + text_el = f"text{counter['text']}" |
| 668 | + counter["text"] += 1 |
| 669 | + |
| 670 | + # Create the TEXT_ELEMENT fragment |
| 671 | + result.append( |
| 672 | + ast.Assign( |
| 673 | + targets=[ast.Name(id=text_el, ctx=ast.Store())], |
| 674 | + value=ast.Call( |
| 675 | + func=ast.Name(id="Fragment", ctx=ast.Load()), |
| 676 | + args=[ast.Name(id="renderer", ctx=ast.Load())], |
| 677 | + keywords=[ |
| 678 | + ast.keyword( |
| 679 | + arg="tag", value=ast.Constant(value="TEXT_ELEMENT") |
| 680 | + ), |
| 681 | + ast.keyword( |
| 682 | + arg="parent", |
| 683 | + value=ast.Name(id=target, ctx=ast.Load()) |
| 684 | + if target |
| 685 | + else ast.Constant(value=None), |
| 686 | + ), |
| 687 | + ], |
| 688 | + ), |
| 689 | + ) |
| 690 | + ) |
| 691 | + |
| 692 | + # Parse the text content for interpolations |
| 693 | + text_content = normalize_multiline_text(child.content) |
| 694 | + has_interpolation = "{{" in text_content and "}}" in text_content |
| 695 | + |
| 696 | + if has_interpolation: |
| 697 | + parts = parse_text_interpolations(text_content) |
| 698 | + if any(is_expr for is_expr, _ in parts): |
| 699 | + result.append( |
| 700 | + ast_set_text_bind( |
| 701 | + text_el, |
| 702 | + parts, |
| 703 | + names, |
| 704 | + list_names, |
| 705 | + ) |
| 706 | + ) |
| 707 | + else: |
| 708 | + # Only escaped/unclosed interpolation-like text was found. |
| 709 | + result.append( |
| 710 | + ast_set_attribute(text_el, "content", text_content) |
| 711 | + ) |
| 712 | + else: |
| 713 | + # Static text - set attribute directly |
| 714 | + result.append(ast_set_attribute(text_el, "content", text_content)) |
| 715 | + |
| 716 | + continue |
556 | 717 |
|
557 | 718 | # Create element name |
558 | 719 | tag = safe_tag(child.tag) |
|
0 commit comments