tw-framework 0.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. tw_framework/__init__.py +13 -0
  2. tw_framework/advanced_diagnostics.py +25 -0
  3. tw_framework/asset_optimizer.py +30 -0
  4. tw_framework/ast_nodes.py +283 -0
  5. tw_framework/build.py +7 -0
  6. tw_framework/build_performance.py +26 -0
  7. tw_framework/build_report.py +84 -0
  8. tw_framework/cli.py +1258 -0
  9. tw_framework/code_splitting.py +76 -0
  10. tw_framework/common.py +22 -0
  11. tw_framework/compiler.py +4817 -0
  12. tw_framework/compiler_stats.py +51 -0
  13. tw_framework/dead_code.py +44 -0
  14. tw_framework/dependency_graph.py +91 -0
  15. tw_framework/deploy.py +7 -0
  16. tw_framework/dev.py +9 -0
  17. tw_framework/diagnostics.py +98 -0
  18. tw_framework/dynamic_imports.py +51 -0
  19. tw_framework/error_formatter.py +53 -0
  20. tw_framework/framework.py +3216 -0
  21. tw_framework/hydration.py +129 -0
  22. tw_framework/incremental_cache.py +53 -0
  23. tw_framework/interpreter.py +42 -0
  24. tw_framework/ir.py +171 -0
  25. tw_framework/lexer.py +52 -0
  26. tw_framework/lowering.py +68 -0
  27. tw_framework/parser.py +167 -0
  28. tw_framework/partial_rebuild.py +24 -0
  29. tw_framework/performance_analyzer.py +37 -0
  30. tw_framework/plugin_runtime.py +196 -0
  31. tw_framework/production_optimizer.py +82 -0
  32. tw_framework/reactivity.py +349 -0
  33. tw_framework/render_css.py +22 -0
  34. tw_framework/render_html.py +259 -0
  35. tw_framework/route_optimizer.py +40 -0
  36. tw_framework/router.py +7 -0
  37. tw_framework/runtime_model.py +23 -0
  38. tw_framework/runtime_values.py +31 -0
  39. tw_framework/semantic.py +162 -0
  40. tw_framework/server.py +532 -0
  41. tw_framework/signature.py +51 -0
  42. tw_framework/static_dynamic_auto.py +32 -0
  43. tw_framework/streaming.py +135 -0
  44. tw_framework/test_build_cli_pytest.py +1078 -0
  45. tw_framework/tree_shaking.py +48 -0
  46. tw_framework/twm_parser.py +274 -0
  47. tw_framework-0.2.3.dist-info/METADATA +8 -0
  48. tw_framework-0.2.3.dist-info/RECORD +51 -0
  49. tw_framework-0.2.3.dist-info/WHEEL +5 -0
  50. tw_framework-0.2.3.dist-info/entry_points.txt +2 -0
  51. tw_framework-0.2.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,129 @@
1
+ """
2
+ Partial hydration / islands architecture for TW Framework.
3
+
4
+ Interactive components are wrapped with data attributes and a small runtime
5
+ that hydrates only those components, leaving static HTML untouched.
6
+ """
7
+
8
+ import html
9
+ import json
10
+ import logging
11
+ from typing import Dict, List, Optional
12
+
13
+ from .ir import IRComponent, IRElement, IRProgram
14
+ from .runtime_values import RuntimeEnvironment
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _is_interactive(node) -> bool:
20
+ """Check if a node has interactive attributes (events, router, or lazy component)."""
21
+ if isinstance(node, IRElement):
22
+ if node.events or node.router:
23
+ return True
24
+ for child in node.children:
25
+ if _is_interactive(child):
26
+ return True
27
+ if isinstance(node, IRComponent):
28
+ for prop in node.props:
29
+ if prop.get("name") == "lazy" and prop.get("value") in (True, "true"):
30
+ return True
31
+ for child in node.children:
32
+ if _is_interactive(child):
33
+ return True
34
+ return False
35
+
36
+
37
+ def _collect_interactive_nodes(node, path: str, interactive: List[Dict]):
38
+ """Recursively collect interactive nodes with their paths."""
39
+ if isinstance(node, IRElement):
40
+ if node.events or node.router:
41
+ interactive.append({
42
+ "path": path,
43
+ "tag": node.tag,
44
+ "events": [{"name": e["name"], "value": e["value"]} for e in node.events],
45
+ "router": dict(node.router),
46
+ })
47
+ for i, child in enumerate(node.children):
48
+ _collect_interactive_nodes(child, f"{path}.children[{i}]", interactive)
49
+ if isinstance(node, IRComponent):
50
+ for prop in node.props:
51
+ if prop.get("name") == "lazy" and prop.get("value") in (True, "true"):
52
+ interactive.append({
53
+ "path": path,
54
+ "tag": f"component:{node.name}",
55
+ "lazy": True,
56
+ })
57
+ for i, child in enumerate(node.children):
58
+ _collect_interactive_nodes(child, f"{path}.children[{i}]", interactive)
59
+
60
+
61
+ def wrap_interactive_nodes(html: str, program: IRProgram, context: Optional[Dict] = None) -> str:
62
+ """Wrap interactive nodes with data-tw-hydrate attributes and inject hydration script."""
63
+ interactive = []
64
+ for i, node in enumerate(program.body):
65
+ _collect_interactive_nodes(node, f"body[{i}]", interactive)
66
+ if not interactive:
67
+ return html
68
+
69
+ # Inject data attributes
70
+ for item in interactive:
71
+ path = item["path"]
72
+ tag = item["tag"]
73
+ if item.get("lazy"):
74
+ html = html.replace(
75
+ f"<{tag}",
76
+ f'<{tag} data-tw-hydrate="lazy" data-tw-path="{html.escape(path, quote=True)}"',
77
+ 1,
78
+ )
79
+ else:
80
+ html = html.replace(
81
+ f"<{tag}",
82
+ f'<{tag} data-tw-hydrate="interactive" data-tw-path="{html.escape(path, quote=True)}"',
83
+ 1,
84
+ )
85
+
86
+ # Inject hydration runtime
87
+ runtime_js = """
88
+ (function() {
89
+ var hydratable = document.querySelectorAll('[data-tw-hydrate]');
90
+ if (!hydratable.length) return;
91
+ var observer = new IntersectionObserver(function(entries) {
92
+ entries.forEach(function(entry) {
93
+ if (entry.isIntersecting) {
94
+ var el = entry.target;
95
+ var type = el.getAttribute('data-tw-hydrate');
96
+ if (type === 'lazy') {
97
+ // Lazy component: load chunk and hydrate
98
+ var path = el.getAttribute('data-tw-path');
99
+ import('/_tw/chunks/' + path + '.js').then(function(mod) {
100
+ if (mod.hydrate) mod.hydrate(el);
101
+ }).catch(function(err) {
102
+ console.warn('TW hydration failed for', path, err);
103
+ });
104
+ } else {
105
+ // Interactive element: attach events
106
+ var events = JSON.parse(el.getAttribute('data-tw-events') || '[]');
107
+ events.forEach(function(ev) {
108
+ el.addEventListener(ev.name, function(event) {
109
+ try {
110
+ var fn = new Function('event', ev.value);
111
+ fn(event);
112
+ } catch(e) {
113
+ console.warn('TW event handler error', e);
114
+ }
115
+ });
116
+ });
117
+ }
118
+ observer.unobserve(el);
119
+ }
120
+ });
121
+ }, { rootMargin: '200px' });
122
+ hydratable.forEach(function(el) { observer.observe(el); });
123
+ })();
124
+ """
125
+ html = html.replace("</body>", f"<script>{runtime_js}</script></body>", 1)
126
+ return html
127
+
128
+
129
+ __all__ = ["wrap_interactive_nodes", "_is_interactive"]
@@ -0,0 +1,53 @@
1
+ """
2
+ Incremental compiler cache for TW Framework.
3
+
4
+ Caches parsed AST, compiled output, dependency graph, CSS, and assets.
5
+ Only rebuilds changed nodes.
6
+ """
7
+
8
+ import hashlib
9
+ import json
10
+ import logging
11
+ import os
12
+ from typing import Any, Dict, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class IncrementalCache:
18
+ """Persistent cache for compiler artifacts."""
19
+
20
+ def __init__(self, project_root: str):
21
+ self.cache_dir = os.path.join(project_root, ".tw", "cache")
22
+ os.makedirs(self.cache_dir, exist_ok=True)
23
+
24
+ def _key_path(self, key: str) -> str:
25
+ return os.path.join(self.cache_dir, f"{key}.json")
26
+
27
+ def get(self, key: str) -> Optional[Any]:
28
+ path = self._key_path(key)
29
+ if not os.path.exists(path):
30
+ return None
31
+ try:
32
+ with open(path, "r", encoding="utf-8") as f:
33
+ return json.load(f)
34
+ except (json.JSONDecodeError, OSError):
35
+ return None
36
+
37
+ def set(self, key: str, value: Any):
38
+ path = self._key_path(key)
39
+ with open(path, "w", encoding="utf-8") as f:
40
+ json.dump(value, f, indent=2)
41
+
42
+ def invalidate(self, key: str):
43
+ path = self._key_path(key)
44
+ if os.path.exists(path):
45
+ os.remove(path)
46
+
47
+ def clear(self):
48
+ for fname in os.listdir(self.cache_dir):
49
+ if fname.endswith(".json"):
50
+ os.remove(os.path.join(self.cache_dir, fname))
51
+
52
+
53
+ __all__ = ["IncrementalCache"]
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Dict, Optional
5
+
6
+ from .lowering import lower_program
7
+ from .parser import parse_file, parse_text
8
+ from .render_html import build_runtime_context, render_program_document
9
+ from .runtime_values import RuntimeEnvironment
10
+
11
+
12
+ @dataclass
13
+ class InterpreterResult:
14
+ html: str
15
+ context: Dict
16
+
17
+
18
+ class Interpreter:
19
+ def run_program(self, program, context: Optional[Dict] = None, css_href: Optional[str] = None) -> InterpreterResult:
20
+ ir_program = lower_program(program)
21
+ runtime_context = build_runtime_context(program, context=context)
22
+ html = render_program_document(ir_program, page_program=program, context=runtime_context, css_href=css_href)
23
+ return InterpreterResult(html=html, context=runtime_context)
24
+
25
+ def run_text(self, text: str, *, base_dir: str = ".", file_path: str = "<memory>", context: Optional[Dict] = None, css_href: Optional[str] = None, debug: bool = False) -> InterpreterResult:
26
+ program = parse_text(text, base_dir=base_dir, file_path=file_path, debug=debug)
27
+ return self.run_program(program, context=context, css_href=css_href)
28
+
29
+ def run_file(self, path: str, context: Optional[Dict] = None, css_href: Optional[str] = None, debug: bool = False) -> InterpreterResult:
30
+ program = parse_file(path, debug=debug)
31
+ return self.run_program(program, context=context, css_href=css_href)
32
+
33
+
34
+ def run_file(path: str, context: Optional[Dict] = None, css_href: Optional[str] = None) -> InterpreterResult:
35
+ return Interpreter().run_file(path, context=context, css_href=css_href)
36
+
37
+
38
+ def build_runtime_environment(initial: Optional[Dict] = None) -> RuntimeEnvironment:
39
+ return RuntimeEnvironment(values=dict(initial or {}))
40
+
41
+
42
+ __all__ = ["Interpreter", "InterpreterResult", "build_runtime_environment", "run_file"]
tw_framework/ir.py ADDED
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Dict, List, Optional
5
+
6
+
7
+ @dataclass
8
+ class IRNode:
9
+ kind: str
10
+
11
+
12
+ @dataclass
13
+ class IRText(IRNode):
14
+ value: str
15
+
16
+ def __init__(self, value: str):
17
+ super().__init__("text")
18
+ self.value = value
19
+
20
+
21
+ @dataclass
22
+ class IRLet(IRNode):
23
+ name: str
24
+ value: Any
25
+
26
+ def __init__(self, name: str, value: Any):
27
+ super().__init__("let")
28
+ self.name = name
29
+ self.value = value
30
+
31
+
32
+ @dataclass
33
+ class IRIf(IRNode):
34
+ condition: str
35
+ children: List[IRNode] = field(default_factory=list)
36
+ else_children: List[IRNode] = field(default_factory=list)
37
+
38
+ def __init__(self, condition: str, children=None, else_children=None):
39
+ super().__init__("if")
40
+ self.condition = condition
41
+ self.children = children or []
42
+ self.else_children = else_children or []
43
+
44
+
45
+ @dataclass
46
+ class IRFor(IRNode):
47
+ var_name: str
48
+ iterable: str
49
+ children: List[IRNode] = field(default_factory=list)
50
+
51
+ def __init__(self, var_name: str, iterable: str, children=None):
52
+ super().__init__("for")
53
+ self.var_name = var_name
54
+ self.iterable = iterable
55
+ self.children = children or []
56
+
57
+
58
+ @dataclass
59
+ class IRScript(IRNode):
60
+ raw_js: str
61
+
62
+ def __init__(self, raw_js: str):
63
+ super().__init__("script")
64
+ self.raw_js = raw_js
65
+
66
+
67
+ @dataclass
68
+ class IRElement(IRNode):
69
+ tag: str
70
+ text: Optional[str] = None
71
+ attrs: List[Dict[str, Any]] = field(default_factory=list)
72
+ styles: List[Dict[str, Any]] = field(default_factory=list)
73
+ events: List[Dict[str, Any]] = field(default_factory=list)
74
+ router: Dict[str, Any] = field(default_factory=dict)
75
+ children: List[IRNode] = field(default_factory=list)
76
+
77
+ def __init__(self, tag: str, text=None, attrs=None, styles=None, events=None, router=None, children=None):
78
+ super().__init__("element")
79
+ self.tag = tag
80
+ self.text = text
81
+ self.attrs = attrs or []
82
+ self.styles = styles or []
83
+ self.events = events or []
84
+ self.router = router or {}
85
+ self.children = children or []
86
+
87
+
88
+ @dataclass
89
+ class IRComponent(IRNode):
90
+ name: str
91
+ props: List[Dict[str, Any]] = field(default_factory=list)
92
+ children: List[IRNode] = field(default_factory=list)
93
+
94
+ def __init__(self, name: str, props=None, children=None):
95
+ super().__init__("component")
96
+ self.name = name
97
+ self.props = props or []
98
+ self.children = children or []
99
+
100
+
101
+ @dataclass
102
+ class IRProgram:
103
+ meta: Dict[str, Any]
104
+ head: Dict[str, Any]
105
+ lets: Dict[str, Any] = field(default_factory=dict)
106
+ state: Dict[str, Any] = field(default_factory=dict)
107
+ body: List[IRNode] = field(default_factory=list)
108
+
109
+ def to_dict(self):
110
+ return {
111
+ "meta": dict(self.meta),
112
+ "head": dict(self.head),
113
+ "lets": dict(self.lets),
114
+ "state": dict(self.state),
115
+ "body": [ir_node_to_dict(node) for node in self.body],
116
+ }
117
+
118
+
119
+ def ir_node_to_dict(node: IRNode):
120
+ if isinstance(node, IRText):
121
+ return {"kind": node.kind, "value": node.value}
122
+ if isinstance(node, IRLet):
123
+ return {"kind": node.kind, "name": node.name, "value": node.value}
124
+ if isinstance(node, IRIf):
125
+ return {
126
+ "kind": node.kind,
127
+ "condition": node.condition,
128
+ "children": [ir_node_to_dict(child) for child in node.children],
129
+ "else_children": [ir_node_to_dict(child) for child in node.else_children],
130
+ }
131
+ if isinstance(node, IRFor):
132
+ return {
133
+ "kind": node.kind,
134
+ "var_name": node.var_name,
135
+ "iterable": node.iterable,
136
+ "children": [ir_node_to_dict(child) for child in node.children],
137
+ }
138
+ if isinstance(node, IRScript):
139
+ return {"kind": node.kind, "raw_js": node.raw_js}
140
+ if isinstance(node, IRElement):
141
+ return {
142
+ "kind": node.kind,
143
+ "tag": node.tag,
144
+ "text": node.text,
145
+ "attrs": list(node.attrs),
146
+ "styles": list(node.styles),
147
+ "events": list(node.events),
148
+ "router": dict(node.router),
149
+ "children": [ir_node_to_dict(child) for child in node.children],
150
+ }
151
+ if isinstance(node, IRComponent):
152
+ return {
153
+ "kind": node.kind,
154
+ "name": node.name,
155
+ "props": list(node.props),
156
+ "children": [ir_node_to_dict(child) for child in node.children],
157
+ }
158
+ return {"kind": getattr(node, "kind", "unknown"), "repr": repr(node)}
159
+
160
+
161
+ __all__ = [
162
+ "IRComponent",
163
+ "IRElement",
164
+ "IRFor",
165
+ "IRIf",
166
+ "IRLet",
167
+ "IRProgram",
168
+ "IRScript",
169
+ "IRText",
170
+ "ir_node_to_dict",
171
+ ]
tw_framework/lexer.py ADDED
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List
5
+
6
+
7
+ @dataclass
8
+ class LexerToken:
9
+ type: str
10
+ value: str
11
+ line: int
12
+ col: int
13
+
14
+ def to_dict(self):
15
+ return {
16
+ "type": self.type,
17
+ "value": self.value,
18
+ "line": self.line,
19
+ "col": self.col,
20
+ }
21
+
22
+
23
+ def _legacy():
24
+ from . import compiler
25
+
26
+ return compiler
27
+
28
+
29
+ def tokenize(code: str, allow_inline_scripts: bool = True) -> List[LexerToken]:
30
+ compiler = _legacy()
31
+ tokens = compiler.tokenize(code, allow_inline_scripts=allow_inline_scripts)
32
+ return [
33
+ LexerToken(
34
+ type=token.type,
35
+ value=token.value,
36
+ line=token.line,
37
+ col=token.col,
38
+ )
39
+ for token in tokens
40
+ ]
41
+
42
+
43
+ def tokenize_tw(code: str) -> List[LexerToken]:
44
+ return tokenize(code, allow_inline_scripts=True)
45
+
46
+
47
+ def tokenize_file(path: str) -> List[LexerToken]:
48
+ compiler = _legacy()
49
+ return tokenize_tw(compiler.read_text_file(path))
50
+
51
+
52
+ __all__ = ["LexerToken", "tokenize", "tokenize_file", "tokenize_tw"]
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from .ast_nodes import ComponentNode, ElementNode, ForNode, IfNode, LetNode, Program, ScriptNode, TextNode
4
+ from .ir import IRComponent, IRElement, IRFor, IRIf, IRLet, IRProgram, IRScript, IRText
5
+
6
+
7
+ def lower_node(node):
8
+ if isinstance(node, TextNode):
9
+ return IRText(node.value)
10
+ if isinstance(node, LetNode):
11
+ return IRLet(node.name, node.value)
12
+ if isinstance(node, IfNode):
13
+ return IRIf(
14
+ condition=node.condition,
15
+ children=[lower_node(child) for child in node.children],
16
+ else_children=[lower_node(child) for child in node.else_children],
17
+ )
18
+ if isinstance(node, ForNode):
19
+ return IRFor(
20
+ var_name=node.var_name,
21
+ iterable=node.iterable,
22
+ children=[lower_node(child) for child in node.children],
23
+ )
24
+ if isinstance(node, ScriptNode):
25
+ return IRScript(node.raw_js)
26
+ if isinstance(node, ComponentNode):
27
+ return IRComponent(
28
+ name=node.name,
29
+ props=[{"name": prop.name, "value": prop.value} for prop in node.props],
30
+ children=[lower_node(child) for child in node.children],
31
+ )
32
+ if isinstance(node, ElementNode):
33
+ return IRElement(
34
+ tag=node.tag,
35
+ text=node.text,
36
+ attrs=[{"name": attr.name, "value": attr.value} for attr in node.attrs],
37
+ styles=[{"name": attr.name, "value": attr.value} for attr in node.styles],
38
+ events=[{"name": attr.name, "value": attr.value} for attr in node.events],
39
+ router=dict(node.router),
40
+ children=[lower_node(child) for child in node.children],
41
+ )
42
+ raise TypeError(f"Unsupported AST node: {type(node)!r}")
43
+
44
+
45
+ def lower_program(program: Program) -> IRProgram:
46
+ return IRProgram(
47
+ meta={
48
+ "title": program.meta.title,
49
+ "layout": program.meta.layout,
50
+ "layouts": list(program.meta.layouts),
51
+ "render_mode": program.meta.render_mode,
52
+ "revalidate": program.meta.revalidate,
53
+ "redirect_to": program.meta.redirect_to,
54
+ "rewrite_to": program.meta.rewrite_to,
55
+ "responsive": program.meta.responsive,
56
+ },
57
+ head={
58
+ "metas": list(program.head.metas),
59
+ "icon": program.head.icon,
60
+ "seo": dict(program.head.seo),
61
+ },
62
+ lets=dict(program.lets),
63
+ state=dict(program.state),
64
+ body=[lower_node(node) for node in program.body],
65
+ )
66
+
67
+
68
+ __all__ = ["lower_node", "lower_program"]
tw_framework/parser.py ADDED
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import ast
5
+ import warnings
6
+ from typing import Any, List
7
+
8
+ from .ast_nodes import (
9
+ Attribute,
10
+ ComponentNode,
11
+ ElementNode,
12
+ ForNode,
13
+ HeadModel,
14
+ IfNode,
15
+ LetNode,
16
+ PageMeta,
17
+ Program,
18
+ ScriptNode,
19
+ node_to_dict,
20
+ )
21
+ from .lexer import tokenize_tw
22
+
23
+
24
+ def _legacy():
25
+ from . import compiler
26
+
27
+ return compiler
28
+
29
+
30
+ def _convert_node(node: Any):
31
+ tag = getattr(node, "tag", "")
32
+ if tag == "__let__":
33
+ return LetNode(node.name, node.value)
34
+ if tag == "__if__":
35
+ return IfNode(
36
+ condition=node.condition,
37
+ children=[_convert_node(child) for child in getattr(node, "children", [])],
38
+ else_children=[_convert_node(child) for child in getattr(node, "else_children", [])],
39
+ )
40
+ if tag == "__for__":
41
+ return ForNode(
42
+ var_name=node.var_name,
43
+ iterable=node.list_expr,
44
+ children=[_convert_node(child) for child in getattr(node, "children", [])],
45
+ )
46
+ if tag == "__script__":
47
+ return ScriptNode(node.raw_js)
48
+ if tag == "__script_tag__":
49
+ # Declarative external script tag (Next.js-like). In the modular AST we
50
+ # model it as a regular <script> element with a data attribute so the
51
+ # structure is still meaningful for analysis/debugging.
52
+ return ElementNode(
53
+ tag="script",
54
+ text=None,
55
+ attrs=[
56
+ Attribute(name="src", value=getattr(node, "src", "")),
57
+ Attribute(name="data-tw-strategy", value=getattr(node, "strategy", "")),
58
+ ],
59
+ styles=[],
60
+ events=[],
61
+ router={},
62
+ children=[],
63
+ )
64
+ if tag == "__component__":
65
+ return ComponentNode(
66
+ name=node.name,
67
+ props=[Attribute(name=name, value=value) for name, value in getattr(node, "props", [])],
68
+ children=[_convert_node(child) for child in getattr(node, "children", [])],
69
+ )
70
+ return ElementNode(
71
+ tag=getattr(node, "tag", "div"),
72
+ text=getattr(node, "text", None),
73
+ attrs=[Attribute(name=name, value=value) for name, value in getattr(node, "attrs", [])],
74
+ styles=[Attribute(name=name, value=value) for name, value in getattr(node, "inline_style", [])],
75
+ events=[Attribute(name=name, value=value) for name, value in getattr(node, "events", [])],
76
+ router=dict(getattr(node, "router", {}) or {}),
77
+ children=[_convert_node(child) for child in getattr(node, "children", [])],
78
+ )
79
+
80
+
81
+ def _normalize_scalar(value: Any):
82
+ compiler = _legacy()
83
+ if not isinstance(value, str):
84
+ return value
85
+ stripped = value.strip()
86
+ if len(stripped) >= 2 and stripped[0] == stripped[-1] and stripped[0] in {"'", '"'}:
87
+ try:
88
+ return ast.literal_eval(stripped)
89
+ except Exception as err:
90
+ warnings.warn(
91
+ f"Malformed quoted literal {stripped!r}: {err}. Falling back to raw string content.",
92
+ RuntimeWarning,
93
+ )
94
+ return stripped[1:-1]
95
+ return compiler.parse_literal_value(stripped)
96
+
97
+
98
+ def from_legacy_page(page, source_path: str = "") -> Program:
99
+ program = Program(
100
+ meta=PageMeta(
101
+ title=page.title,
102
+ layout=page.layout,
103
+ layouts=list(getattr(page, "layouts", []) or []),
104
+ render_mode=page.render_mode,
105
+ revalidate=page.revalidate,
106
+ redirect_to=page.redirect_to,
107
+ rewrite_to=page.rewrite_to,
108
+ responsive=bool(getattr(page, "responsive", False)),
109
+ ),
110
+ head=HeadModel(
111
+ metas=list(getattr(page.head, "metas", []) or []),
112
+ icon=getattr(page.head, "icon", None),
113
+ seo=dict(getattr(page.head, "seo", {}) or {}),
114
+ ),
115
+ lets={key: _normalize_scalar(value) for key, value in dict(getattr(page, "let_vars", {}) or {}).items()},
116
+ state={key: _normalize_scalar(value) for key, value in dict(getattr(page, "state_vars", {}) or {}).items()},
117
+ body=[_convert_node(node) for node in getattr(page, "body", [])],
118
+ loaded_sheets=list(getattr(page, "loaded_sheets", []) or []),
119
+ loaded_json=list(getattr(page, "loaded_json", []) or []),
120
+ source_path=source_path or getattr(page, "_tw_source_path", ""),
121
+ legacy_page=page,
122
+ )
123
+ return program
124
+
125
+
126
+ def parse_text(text: str, base_dir: str = ".", file_path: str = "<memory>", debug: bool = False) -> Program:
127
+ compiler = _legacy()
128
+ tokens = compiler.tokenize_tw(text)
129
+ page = compiler.build_tw_ast(tokens, base_dir, file_path, text)
130
+ page._tw_source_path = file_path
131
+ return from_legacy_page(page, source_path=file_path)
132
+
133
+
134
+ def parse_file(path: str, debug: bool = False) -> Program:
135
+ compiler = _legacy()
136
+ source = compiler.read_text_file(path)
137
+ return parse_text(source, base_dir=compiler.normalize_path(os.path.dirname(path)), file_path=path, debug=debug)
138
+
139
+
140
+ def build_tw_ast(tokens, base_dir, file_path, source):
141
+ compiler = _legacy()
142
+ return compiler.build_tw_ast(tokens, base_dir, file_path, source)
143
+
144
+
145
+ def build_tss_ast_from_text(text: str):
146
+ compiler = _legacy()
147
+ return compiler.build_tss_ast_from_text(text)
148
+
149
+
150
+ def program_to_dict(program: Program):
151
+ return program.to_dict()
152
+
153
+
154
+ def nodes_to_dict(nodes: List[Any]):
155
+ return [node_to_dict(node) for node in nodes]
156
+
157
+
158
+ __all__ = [
159
+ "build_tss_ast_from_text",
160
+ "build_tw_ast",
161
+ "from_legacy_page",
162
+ "nodes_to_dict",
163
+ "parse_file",
164
+ "parse_text",
165
+ "program_to_dict",
166
+ "tokenize_tw",
167
+ ]