pywire 0.1.0__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.
- pywire/__init__.py +2 -0
- pywire/cli/__init__.py +1 -0
- pywire/cli/generators.py +48 -0
- pywire/cli/main.py +309 -0
- pywire/cli/tui.py +563 -0
- pywire/cli/validate.py +26 -0
- pywire/client/.prettierignore +8 -0
- pywire/client/.prettierrc +7 -0
- pywire/client/build.mjs +73 -0
- pywire/client/eslint.config.js +46 -0
- pywire/client/package.json +39 -0
- pywire/client/pnpm-lock.yaml +2971 -0
- pywire/client/src/core/app.ts +263 -0
- pywire/client/src/core/dom-updater.test.ts +78 -0
- pywire/client/src/core/dom-updater.ts +321 -0
- pywire/client/src/core/index.ts +5 -0
- pywire/client/src/core/transport-manager.test.ts +179 -0
- pywire/client/src/core/transport-manager.ts +159 -0
- pywire/client/src/core/transports/base.ts +122 -0
- pywire/client/src/core/transports/http.ts +142 -0
- pywire/client/src/core/transports/index.ts +13 -0
- pywire/client/src/core/transports/websocket.ts +97 -0
- pywire/client/src/core/transports/webtransport.ts +149 -0
- pywire/client/src/dev/dev-app.ts +93 -0
- pywire/client/src/dev/error-trace.test.ts +97 -0
- pywire/client/src/dev/error-trace.ts +76 -0
- pywire/client/src/dev/index.ts +4 -0
- pywire/client/src/dev/status-overlay.ts +63 -0
- pywire/client/src/events/handler.test.ts +318 -0
- pywire/client/src/events/handler.ts +454 -0
- pywire/client/src/pywire.core.ts +22 -0
- pywire/client/src/pywire.dev.ts +27 -0
- pywire/client/tsconfig.json +17 -0
- pywire/client/vitest.config.ts +15 -0
- pywire/compiler/__init__.py +6 -0
- pywire/compiler/ast_nodes.py +304 -0
- pywire/compiler/attributes/__init__.py +6 -0
- pywire/compiler/attributes/base.py +24 -0
- pywire/compiler/attributes/conditional.py +37 -0
- pywire/compiler/attributes/events.py +55 -0
- pywire/compiler/attributes/form.py +37 -0
- pywire/compiler/attributes/loop.py +75 -0
- pywire/compiler/attributes/reactive.py +34 -0
- pywire/compiler/build.py +28 -0
- pywire/compiler/build_artifacts.py +342 -0
- pywire/compiler/codegen/__init__.py +5 -0
- pywire/compiler/codegen/attributes/__init__.py +6 -0
- pywire/compiler/codegen/attributes/base.py +19 -0
- pywire/compiler/codegen/attributes/events.py +35 -0
- pywire/compiler/codegen/directives/__init__.py +6 -0
- pywire/compiler/codegen/directives/base.py +16 -0
- pywire/compiler/codegen/directives/path.py +53 -0
- pywire/compiler/codegen/generator.py +2341 -0
- pywire/compiler/codegen/template.py +2178 -0
- pywire/compiler/directives/__init__.py +7 -0
- pywire/compiler/directives/base.py +20 -0
- pywire/compiler/directives/component.py +33 -0
- pywire/compiler/directives/context.py +93 -0
- pywire/compiler/directives/layout.py +49 -0
- pywire/compiler/directives/no_spa.py +24 -0
- pywire/compiler/directives/path.py +71 -0
- pywire/compiler/directives/props.py +88 -0
- pywire/compiler/exceptions.py +19 -0
- pywire/compiler/interpolation/__init__.py +6 -0
- pywire/compiler/interpolation/base.py +28 -0
- pywire/compiler/interpolation/jinja.py +272 -0
- pywire/compiler/parser.py +750 -0
- pywire/compiler/paths.py +29 -0
- pywire/compiler/preprocessor.py +43 -0
- pywire/core/wire.py +119 -0
- pywire/py.typed +0 -0
- pywire/runtime/__init__.py +7 -0
- pywire/runtime/aioquic_server.py +194 -0
- pywire/runtime/app.py +889 -0
- pywire/runtime/compile_error_page.py +195 -0
- pywire/runtime/debug.py +203 -0
- pywire/runtime/dev_server.py +434 -0
- pywire/runtime/dev_server.py.broken +268 -0
- pywire/runtime/error_page.py +64 -0
- pywire/runtime/error_renderer.py +23 -0
- pywire/runtime/escape.py +23 -0
- pywire/runtime/files.py +40 -0
- pywire/runtime/helpers.py +97 -0
- pywire/runtime/http_transport.py +253 -0
- pywire/runtime/loader.py +272 -0
- pywire/runtime/logging.py +72 -0
- pywire/runtime/page.py +384 -0
- pywire/runtime/pydantic_integration.py +52 -0
- pywire/runtime/router.py +229 -0
- pywire/runtime/server.py +25 -0
- pywire/runtime/style_collector.py +31 -0
- pywire/runtime/upload_manager.py +76 -0
- pywire/runtime/validation.py +449 -0
- pywire/runtime/websocket.py +665 -0
- pywire/runtime/webtransport_handler.py +195 -0
- pywire/templates/error/404.html +11 -0
- pywire/templates/error/500.html +38 -0
- pywire/templates/error/base.html +207 -0
- pywire/templates/error/compile_error.html +31 -0
- pywire-0.1.0.dist-info/METADATA +50 -0
- pywire-0.1.0.dist-info/RECORD +104 -0
- pywire-0.1.0.dist-info/WHEEL +4 -0
- pywire-0.1.0.dist-info/entry_points.txt +2 -0
- pywire-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,2178 @@
|
|
|
1
|
+
"""Template rendering code generation."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import dataclasses
|
|
5
|
+
import re
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast
|
|
9
|
+
|
|
10
|
+
from pywire.compiler.ast_nodes import (
|
|
11
|
+
EventAttribute,
|
|
12
|
+
ForAttribute,
|
|
13
|
+
IfAttribute,
|
|
14
|
+
InterpolationNode,
|
|
15
|
+
KeyAttribute,
|
|
16
|
+
ReactiveAttribute,
|
|
17
|
+
ShowAttribute,
|
|
18
|
+
TemplateNode,
|
|
19
|
+
)
|
|
20
|
+
from pywire.compiler.interpolation.jinja import JinjaInterpolationParser
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TemplateCodegen:
|
|
24
|
+
"""Generates Python AST for rendering template."""
|
|
25
|
+
|
|
26
|
+
# HTML void elements that don't have closing tags
|
|
27
|
+
VOID_ELEMENTS = {
|
|
28
|
+
"area",
|
|
29
|
+
"base",
|
|
30
|
+
"br",
|
|
31
|
+
"col",
|
|
32
|
+
"embed",
|
|
33
|
+
"hr",
|
|
34
|
+
"img",
|
|
35
|
+
"input",
|
|
36
|
+
"link",
|
|
37
|
+
"meta",
|
|
38
|
+
"param",
|
|
39
|
+
"source",
|
|
40
|
+
"track",
|
|
41
|
+
"wbr",
|
|
42
|
+
"slot",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
self.interpolation_parser = JinjaInterpolationParser()
|
|
47
|
+
self._slot_default_counter = 0
|
|
48
|
+
self.auxiliary_functions: List[ast.AsyncFunctionDef] = []
|
|
49
|
+
self.has_file_inputs = False
|
|
50
|
+
self._region_counter = 0
|
|
51
|
+
self.region_renderers: Dict[str, str] = {}
|
|
52
|
+
|
|
53
|
+
def generate_render_method(
|
|
54
|
+
self,
|
|
55
|
+
template_nodes: List[TemplateNode],
|
|
56
|
+
layout_id: Optional[str] = None,
|
|
57
|
+
known_methods: Optional[Set[str]] = None,
|
|
58
|
+
known_globals: Optional[Set[str]] = None,
|
|
59
|
+
async_methods: Optional[Set[str]] = None,
|
|
60
|
+
component_map: Optional[Dict[str, str]] = None,
|
|
61
|
+
scope_id: Optional[str] = None,
|
|
62
|
+
initial_locals: Optional[Set[str]] = None,
|
|
63
|
+
) -> Tuple[ast.AsyncFunctionDef, List[ast.AsyncFunctionDef]]:
|
|
64
|
+
"""
|
|
65
|
+
Generate standard _render_template method.
|
|
66
|
+
Returns: (main_function_ast, list_of_auxiliary_function_asts)
|
|
67
|
+
"""
|
|
68
|
+
self._reset_state()
|
|
69
|
+
# Check for explicit spread
|
|
70
|
+
has_spread = self._has_spread_attribute(template_nodes)
|
|
71
|
+
implicit_root_source = "attrs" if not has_spread and layout_id else None
|
|
72
|
+
|
|
73
|
+
main_func = self._generate_function(
|
|
74
|
+
template_nodes,
|
|
75
|
+
"_render_template",
|
|
76
|
+
is_async=True,
|
|
77
|
+
layout_id=layout_id,
|
|
78
|
+
known_methods=known_methods,
|
|
79
|
+
known_globals=known_globals,
|
|
80
|
+
async_methods=async_methods,
|
|
81
|
+
component_map=component_map,
|
|
82
|
+
scope_id=scope_id,
|
|
83
|
+
initial_locals=initial_locals,
|
|
84
|
+
implicit_root_source=implicit_root_source,
|
|
85
|
+
)
|
|
86
|
+
return main_func, self.auxiliary_functions
|
|
87
|
+
|
|
88
|
+
def generate_slot_methods(
|
|
89
|
+
self,
|
|
90
|
+
template_nodes: List[TemplateNode],
|
|
91
|
+
file_id: str = "",
|
|
92
|
+
known_globals: Optional[Set[str]] = None,
|
|
93
|
+
layout_id: Optional[str] = None,
|
|
94
|
+
component_map: Optional[Dict[str, str]] = None,
|
|
95
|
+
) -> Tuple[Dict[str, ast.AsyncFunctionDef], List[ast.AsyncFunctionDef]]:
|
|
96
|
+
"""
|
|
97
|
+
Generate slot filler methods for child pages.
|
|
98
|
+
Returns: ({slot_name: function_ast}, list_of_auxiliary_function_asts)
|
|
99
|
+
"""
|
|
100
|
+
self._reset_state()
|
|
101
|
+
slots = defaultdict(list)
|
|
102
|
+
|
|
103
|
+
# Generate a short hash from file_id to make method names unique per file
|
|
104
|
+
import hashlib
|
|
105
|
+
|
|
106
|
+
file_hash = hashlib.md5(file_id.encode()).hexdigest()[:8] if file_id else ""
|
|
107
|
+
|
|
108
|
+
# 1. Bucket nodes into slots based on wrapper elements
|
|
109
|
+
for node in template_nodes:
|
|
110
|
+
if node.tag == "slot" and node.attributes and "name" in node.attributes:
|
|
111
|
+
slot_name = node.attributes["name"]
|
|
112
|
+
for child in node.children:
|
|
113
|
+
slots[slot_name].append(child)
|
|
114
|
+
elif node.tag == "pywire-head":
|
|
115
|
+
for child in node.children:
|
|
116
|
+
slots["$head"].append(child)
|
|
117
|
+
else:
|
|
118
|
+
slots["default"].append(node)
|
|
119
|
+
|
|
120
|
+
# 2. Generate functions for each slot
|
|
121
|
+
slot_funcs = {}
|
|
122
|
+
for slot_name, nodes in slots.items():
|
|
123
|
+
safe_name = (
|
|
124
|
+
slot_name.replace("$", "_head_").replace("-", "_")
|
|
125
|
+
if slot_name.startswith("$")
|
|
126
|
+
else slot_name.replace("-", "_")
|
|
127
|
+
)
|
|
128
|
+
func_name = (
|
|
129
|
+
f"_render_slot_fill_{safe_name}_{file_hash}"
|
|
130
|
+
if file_hash
|
|
131
|
+
else f"_render_slot_fill_{safe_name}"
|
|
132
|
+
)
|
|
133
|
+
slot_funcs[slot_name] = self._generate_function(
|
|
134
|
+
nodes,
|
|
135
|
+
func_name,
|
|
136
|
+
is_async=True,
|
|
137
|
+
known_globals=known_globals,
|
|
138
|
+
layout_id=layout_id,
|
|
139
|
+
component_map=component_map,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return slot_funcs, self.auxiliary_functions
|
|
143
|
+
|
|
144
|
+
def _reset_state(self) -> None:
|
|
145
|
+
self._slot_default_counter = 0
|
|
146
|
+
self.auxiliary_functions = []
|
|
147
|
+
self.has_file_inputs = False
|
|
148
|
+
self._region_counter = 0
|
|
149
|
+
self.region_renderers = {}
|
|
150
|
+
|
|
151
|
+
def _generate_function(
|
|
152
|
+
self,
|
|
153
|
+
nodes: List[TemplateNode],
|
|
154
|
+
func_name: str,
|
|
155
|
+
is_async: bool = False,
|
|
156
|
+
layout_id: Optional[str] = None,
|
|
157
|
+
known_methods: Optional[Set[str]] = None,
|
|
158
|
+
known_globals: Optional[Set[str]] = None,
|
|
159
|
+
async_methods: Optional[Set[str]] = None,
|
|
160
|
+
component_map: Optional[Dict[str, str]] = None,
|
|
161
|
+
scope_id: Optional[str] = None,
|
|
162
|
+
initial_locals: Optional[Set[str]] = None,
|
|
163
|
+
implicit_root_source: Optional[str] = None,
|
|
164
|
+
enable_regions: bool = True,
|
|
165
|
+
root_region_id: Optional[str] = None,
|
|
166
|
+
) -> ast.AsyncFunctionDef:
|
|
167
|
+
"""Generate a single function body as AST."""
|
|
168
|
+
|
|
169
|
+
if initial_locals is None:
|
|
170
|
+
initial_locals = set()
|
|
171
|
+
else:
|
|
172
|
+
initial_locals = initial_locals.copy()
|
|
173
|
+
|
|
174
|
+
if known_methods is None:
|
|
175
|
+
known_methods = set()
|
|
176
|
+
if known_globals is None:
|
|
177
|
+
known_globals = set()
|
|
178
|
+
if async_methods is None:
|
|
179
|
+
async_methods = set()
|
|
180
|
+
if component_map is None:
|
|
181
|
+
component_map = {}
|
|
182
|
+
|
|
183
|
+
# 'json' is imported in the body, so we treat it as local to avoid transforming to self.json
|
|
184
|
+
initial_locals.add("json")
|
|
185
|
+
|
|
186
|
+
# parts = []
|
|
187
|
+
body: List[ast.stmt] = [
|
|
188
|
+
ast.Assign(
|
|
189
|
+
targets=[ast.Name(id="parts", ctx=ast.Store())],
|
|
190
|
+
value=ast.List(elts=[], ctx=ast.Load()),
|
|
191
|
+
),
|
|
192
|
+
ast.Import(names=[ast.alias(name="json", asname=None)]),
|
|
193
|
+
# import helper
|
|
194
|
+
ast.ImportFrom(
|
|
195
|
+
module="pywire.runtime.helpers",
|
|
196
|
+
names=[ast.alias(name="ensure_async_iterator", asname=None)],
|
|
197
|
+
level=0,
|
|
198
|
+
),
|
|
199
|
+
# import escape_html for XSS prevention
|
|
200
|
+
ast.ImportFrom(
|
|
201
|
+
module="pywire.runtime.escape",
|
|
202
|
+
names=[ast.alias(name="escape_html", asname=None)],
|
|
203
|
+
level=0,
|
|
204
|
+
),
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
root_element = self._get_root_element(nodes)
|
|
208
|
+
|
|
209
|
+
for node in nodes:
|
|
210
|
+
# Pass implicit root source ONLY to the root element if it matches
|
|
211
|
+
node_root_source = (
|
|
212
|
+
implicit_root_source
|
|
213
|
+
if (implicit_root_source and node is root_element)
|
|
214
|
+
else None
|
|
215
|
+
)
|
|
216
|
+
node_region_id = (
|
|
217
|
+
root_region_id if (root_region_id and node is root_element) else None
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
self._add_node(
|
|
221
|
+
node,
|
|
222
|
+
body,
|
|
223
|
+
layout_id=layout_id,
|
|
224
|
+
known_methods=known_methods,
|
|
225
|
+
known_globals=known_globals,
|
|
226
|
+
async_methods=async_methods,
|
|
227
|
+
component_map=component_map,
|
|
228
|
+
scope_id=scope_id,
|
|
229
|
+
local_vars=initial_locals,
|
|
230
|
+
implicit_root_source=node_root_source,
|
|
231
|
+
enable_regions=enable_regions,
|
|
232
|
+
region_id=node_region_id,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# return "".join(parts)
|
|
236
|
+
body.append(
|
|
237
|
+
ast.Return(
|
|
238
|
+
value=ast.Call(
|
|
239
|
+
func=ast.Attribute(
|
|
240
|
+
value=ast.Constant(value=""), attr="join", ctx=ast.Load()
|
|
241
|
+
),
|
|
242
|
+
args=[ast.Name(id="parts", ctx=ast.Load())],
|
|
243
|
+
keywords=[],
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
func_def = ast.AsyncFunctionDef(
|
|
249
|
+
name=func_name,
|
|
250
|
+
args=ast.arguments(
|
|
251
|
+
posonlyargs=[],
|
|
252
|
+
args=[ast.arg(arg="self")],
|
|
253
|
+
vararg=None,
|
|
254
|
+
kwonlyargs=[],
|
|
255
|
+
kw_defaults=[],
|
|
256
|
+
defaults=[],
|
|
257
|
+
),
|
|
258
|
+
body=body,
|
|
259
|
+
decorator_list=[],
|
|
260
|
+
returns=None,
|
|
261
|
+
)
|
|
262
|
+
# We don't set lineno on the function def itself as it's generated,
|
|
263
|
+
# but we could set it to the first node's line?
|
|
264
|
+
# Better to leave it (defaults to 1?) or set to 0.
|
|
265
|
+
# The body statements will have correct linenos.
|
|
266
|
+
return func_def
|
|
267
|
+
|
|
268
|
+
def _transform_expr(
|
|
269
|
+
self,
|
|
270
|
+
expr_str: str,
|
|
271
|
+
local_vars: Set[str],
|
|
272
|
+
known_globals: Optional[Set[str]] = None,
|
|
273
|
+
line_offset: int = 0,
|
|
274
|
+
col_offset: int = 0,
|
|
275
|
+
) -> ast.expr:
|
|
276
|
+
"""Transform expression string to AST with self. handling."""
|
|
277
|
+
expr_str = expr_str.strip()
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
from pywire.compiler.preprocessor import preprocess_python_code
|
|
281
|
+
|
|
282
|
+
expr_str = preprocess_python_code(expr_str)
|
|
283
|
+
tree = ast.parse(expr_str, mode="eval")
|
|
284
|
+
if line_offset > 0:
|
|
285
|
+
# ast.increment_lineno uses 1-based indexing for AST, but adds diff
|
|
286
|
+
# We want result to be line_offset.
|
|
287
|
+
# Current starts at 1.
|
|
288
|
+
# diff = line_offset - 1
|
|
289
|
+
ast.increment_lineno(tree, line_offset - 1)
|
|
290
|
+
except SyntaxError:
|
|
291
|
+
# Fallback for complex/invalid syntax (legacy support)
|
|
292
|
+
# Try regex replacement then parse
|
|
293
|
+
def repl(m: re.Match) -> str:
|
|
294
|
+
word = str(m.group(1))
|
|
295
|
+
if word in local_vars:
|
|
296
|
+
return word
|
|
297
|
+
if known_globals is not None and word in known_globals:
|
|
298
|
+
return word
|
|
299
|
+
keywords = {
|
|
300
|
+
"if",
|
|
301
|
+
"else",
|
|
302
|
+
"and",
|
|
303
|
+
"or",
|
|
304
|
+
"not",
|
|
305
|
+
"in",
|
|
306
|
+
"is",
|
|
307
|
+
"True",
|
|
308
|
+
"False",
|
|
309
|
+
"None",
|
|
310
|
+
}
|
|
311
|
+
if word in keywords:
|
|
312
|
+
return word
|
|
313
|
+
return f"self.{word}"
|
|
314
|
+
|
|
315
|
+
replaced = re.sub(r"\\b([a-zA-Z_]\w*)\\b(?!\s*[(\[])", repl, expr_str)
|
|
316
|
+
tree = ast.parse(replaced, mode="eval")
|
|
317
|
+
|
|
318
|
+
class AddSelfTransformer(ast.NodeTransformer):
|
|
319
|
+
def visit_Name(self, node: ast.Name) -> Any:
|
|
320
|
+
import builtins
|
|
321
|
+
|
|
322
|
+
# 1. If locally defined, keep as is
|
|
323
|
+
if node.id in local_vars:
|
|
324
|
+
return node
|
|
325
|
+
|
|
326
|
+
# 2. If explicitly known as global/instance var, transform to self.<name>
|
|
327
|
+
if known_globals is not None and node.id in known_globals:
|
|
328
|
+
return ast.Attribute(
|
|
329
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
330
|
+
attr=node.id,
|
|
331
|
+
ctx=node.ctx,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# 3. If builtin, keep as is (unless matched by step 1/2)
|
|
335
|
+
if node.id in dir(builtins):
|
|
336
|
+
return node
|
|
337
|
+
|
|
338
|
+
# 4. Otherwise, assume implicit instance attribute
|
|
339
|
+
return ast.Attribute(
|
|
340
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
341
|
+
attr=node.id,
|
|
342
|
+
ctx=node.ctx,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
new_tree = AddSelfTransformer().visit(tree)
|
|
346
|
+
# Returns the expression node
|
|
347
|
+
return cast(ast.Expression, new_tree).body
|
|
348
|
+
|
|
349
|
+
def _transform_reactive_expr(
|
|
350
|
+
self,
|
|
351
|
+
expr_str: str,
|
|
352
|
+
local_vars: Set[str],
|
|
353
|
+
known_methods: Optional[Set[str]] = None,
|
|
354
|
+
known_globals: Optional[Set[str]] = None,
|
|
355
|
+
async_methods: Optional[Set[str]] = None,
|
|
356
|
+
line_offset: int = 0,
|
|
357
|
+
col_offset: int = 0,
|
|
358
|
+
) -> ast.expr:
|
|
359
|
+
"""Transform reactive expression to AST, handling async calls and self."""
|
|
360
|
+
base_expr = self._transform_expr(
|
|
361
|
+
expr_str, local_vars, known_globals, line_offset, col_offset
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Auto-call if it matches self.method
|
|
365
|
+
if (
|
|
366
|
+
isinstance(base_expr, ast.Attribute)
|
|
367
|
+
and isinstance(base_expr.value, ast.Name)
|
|
368
|
+
and base_expr.value.id == "self"
|
|
369
|
+
):
|
|
370
|
+
if known_methods and base_expr.attr in known_methods:
|
|
371
|
+
base_expr = ast.Call(func=base_expr, args=[], keywords=[])
|
|
372
|
+
|
|
373
|
+
# Async handling
|
|
374
|
+
if async_methods:
|
|
375
|
+
|
|
376
|
+
class AsyncAwaiter(ast.NodeTransformer):
|
|
377
|
+
def __init__(self) -> None:
|
|
378
|
+
self.in_await = False
|
|
379
|
+
|
|
380
|
+
def visit_Await(self, node: ast.Await) -> Any:
|
|
381
|
+
self.in_await = True
|
|
382
|
+
self.generic_visit(node)
|
|
383
|
+
self.in_await = False
|
|
384
|
+
return node
|
|
385
|
+
|
|
386
|
+
def visit_Call(self, node: ast.Call) -> Any:
|
|
387
|
+
# Check if already awaited
|
|
388
|
+
if self.in_await:
|
|
389
|
+
return self.generic_visit(node)
|
|
390
|
+
|
|
391
|
+
if (
|
|
392
|
+
isinstance(node.func, ast.Attribute)
|
|
393
|
+
and isinstance(node.func.value, ast.Name)
|
|
394
|
+
and node.func.value.id == "self"
|
|
395
|
+
and async_methods is not None
|
|
396
|
+
and node.func.attr in async_methods
|
|
397
|
+
):
|
|
398
|
+
return ast.Await(value=node)
|
|
399
|
+
return self.generic_visit(node)
|
|
400
|
+
|
|
401
|
+
# Wrap in Module/Expr to visit
|
|
402
|
+
mod = ast.Module(body=[ast.Expr(value=base_expr)], type_ignores=[])
|
|
403
|
+
AsyncAwaiter().visit(mod)
|
|
404
|
+
base_expr = cast(ast.Expr, mod.body[0]).value
|
|
405
|
+
|
|
406
|
+
return base_expr
|
|
407
|
+
|
|
408
|
+
def _wrap_unwrap_wire(self, expr: ast.expr) -> ast.expr:
|
|
409
|
+
return ast.Call(
|
|
410
|
+
func=ast.Name(id="unwrap_wire", ctx=ast.Load()),
|
|
411
|
+
args=[expr],
|
|
412
|
+
keywords=[],
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def _next_region_id(self) -> str:
|
|
416
|
+
self._region_counter += 1
|
|
417
|
+
return f"r{self._region_counter}"
|
|
418
|
+
|
|
419
|
+
def _node_is_dynamic(
|
|
420
|
+
self, node: TemplateNode, known_globals: Optional[Set[str]] = None
|
|
421
|
+
) -> bool:
|
|
422
|
+
if node.tag is None:
|
|
423
|
+
if any(
|
|
424
|
+
isinstance(attr, InterpolationNode) for attr in node.special_attributes
|
|
425
|
+
):
|
|
426
|
+
return True
|
|
427
|
+
if node.text_content and not node.is_raw:
|
|
428
|
+
parts = self.interpolation_parser.parse(
|
|
429
|
+
node.text_content, node.line, node.column
|
|
430
|
+
)
|
|
431
|
+
return any(isinstance(part, InterpolationNode) for part in parts)
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
for attr in node.special_attributes:
|
|
435
|
+
if isinstance(attr, EventAttribute):
|
|
436
|
+
continue
|
|
437
|
+
return True
|
|
438
|
+
|
|
439
|
+
return any(
|
|
440
|
+
self._node_is_dynamic(child, known_globals) for child in node.children
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
def _generate_region_method(
|
|
444
|
+
self,
|
|
445
|
+
node: TemplateNode,
|
|
446
|
+
func_name: str,
|
|
447
|
+
region_id: str,
|
|
448
|
+
layout_id: Optional[str],
|
|
449
|
+
known_methods: Optional[Set[str]],
|
|
450
|
+
known_globals: Optional[Set[str]],
|
|
451
|
+
async_methods: Optional[Set[str]],
|
|
452
|
+
component_map: Optional[Dict[str, str]],
|
|
453
|
+
scope_id: Optional[str],
|
|
454
|
+
implicit_root_source: Optional[str],
|
|
455
|
+
) -> ast.AsyncFunctionDef:
|
|
456
|
+
func_def = self._generate_function(
|
|
457
|
+
[node],
|
|
458
|
+
func_name,
|
|
459
|
+
is_async=True,
|
|
460
|
+
layout_id=layout_id,
|
|
461
|
+
known_methods=known_methods,
|
|
462
|
+
known_globals=known_globals,
|
|
463
|
+
async_methods=async_methods,
|
|
464
|
+
component_map=component_map,
|
|
465
|
+
scope_id=scope_id,
|
|
466
|
+
implicit_root_source=implicit_root_source,
|
|
467
|
+
enable_regions=False,
|
|
468
|
+
root_region_id=region_id,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
if len(func_def.body) < 3:
|
|
472
|
+
return func_def
|
|
473
|
+
|
|
474
|
+
setup = func_def.body[:3]
|
|
475
|
+
render_body = func_def.body[3:]
|
|
476
|
+
|
|
477
|
+
begin_render = ast.Expr(
|
|
478
|
+
value=ast.Call(
|
|
479
|
+
func=ast.Attribute(
|
|
480
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
481
|
+
attr="_begin_region_render",
|
|
482
|
+
ctx=ast.Load(),
|
|
483
|
+
),
|
|
484
|
+
args=[ast.Constant(value=region_id)],
|
|
485
|
+
keywords=[],
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
render_body.insert(0, begin_render)
|
|
489
|
+
|
|
490
|
+
token_assign = ast.Assign(
|
|
491
|
+
targets=[ast.Name(id="_render_token", ctx=ast.Store())],
|
|
492
|
+
value=ast.Call(
|
|
493
|
+
func=ast.Name(id="set_render_context", ctx=ast.Load()),
|
|
494
|
+
args=[
|
|
495
|
+
ast.Name(id="self", ctx=ast.Load()),
|
|
496
|
+
ast.Constant(value=region_id),
|
|
497
|
+
],
|
|
498
|
+
keywords=[],
|
|
499
|
+
),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
reset_stmt = ast.Expr(
|
|
503
|
+
value=ast.Call(
|
|
504
|
+
func=ast.Name(id="reset_render_context", ctx=ast.Load()),
|
|
505
|
+
args=[ast.Name(id="_render_token", ctx=ast.Load())],
|
|
506
|
+
keywords=[],
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
func_def.body = setup + [
|
|
511
|
+
token_assign,
|
|
512
|
+
ast.Try(body=render_body, orelse=[], finalbody=[reset_stmt], handlers=[]),
|
|
513
|
+
]
|
|
514
|
+
return func_def
|
|
515
|
+
|
|
516
|
+
def _has_spread_attribute(self, nodes: List[TemplateNode]) -> bool:
|
|
517
|
+
"""Check if any node in the tree has a SpreadAttribute."""
|
|
518
|
+
from pywire.compiler.ast_nodes import SpreadAttribute
|
|
519
|
+
|
|
520
|
+
for node in nodes:
|
|
521
|
+
if any(isinstance(a, SpreadAttribute) for a in node.special_attributes):
|
|
522
|
+
return True
|
|
523
|
+
if self._has_spread_attribute(node.children):
|
|
524
|
+
return True
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
def _get_root_element(self, nodes: List[TemplateNode]) -> Optional[TemplateNode]:
|
|
528
|
+
"""Find the single root element if it exists (ignoring text/whitespace and metadata)."""
|
|
529
|
+
# Exclude style and script tags from root consideration
|
|
530
|
+
elements = [
|
|
531
|
+
n
|
|
532
|
+
for n in nodes
|
|
533
|
+
if n.tag is not None and n.tag.lower() not in ("style", "script")
|
|
534
|
+
]
|
|
535
|
+
if len(elements) == 1:
|
|
536
|
+
return elements[0]
|
|
537
|
+
return None
|
|
538
|
+
|
|
539
|
+
def _set_line(self, node: ast.AST, template_node: TemplateNode) -> ast.AST:
|
|
540
|
+
"""Helper to set line number on AST node."""
|
|
541
|
+
if template_node.line > 0 and hasattr(node, "lineno"):
|
|
542
|
+
node.lineno = template_node.line
|
|
543
|
+
node.col_offset = template_node.column # type: ignore
|
|
544
|
+
node.end_lineno = template_node.line # type: ignore # Single line approximation
|
|
545
|
+
node.end_col_offset = template_node.column + 1 # type: ignore
|
|
546
|
+
return node
|
|
547
|
+
|
|
548
|
+
def _add_node(
|
|
549
|
+
self,
|
|
550
|
+
node: TemplateNode,
|
|
551
|
+
body: List[ast.stmt],
|
|
552
|
+
local_vars: Optional[Set[str]] = None,
|
|
553
|
+
bound_var: Union[str, ast.expr, None] = None,
|
|
554
|
+
layout_id: Optional[str] = None,
|
|
555
|
+
known_methods: Optional[Set[str]] = None,
|
|
556
|
+
known_globals: Optional[Set[str]] = None,
|
|
557
|
+
async_methods: Optional[Set[str]] = None,
|
|
558
|
+
component_map: Optional[Dict[str, str]] = None,
|
|
559
|
+
scope_id: Optional[str] = None,
|
|
560
|
+
parts_var: str = "parts",
|
|
561
|
+
implicit_root_source: Optional[str] = None,
|
|
562
|
+
enable_regions: bool = True,
|
|
563
|
+
region_id: Optional[str] = None,
|
|
564
|
+
) -> None:
|
|
565
|
+
if local_vars is None:
|
|
566
|
+
local_vars = set()
|
|
567
|
+
else:
|
|
568
|
+
local_vars = local_vars.copy()
|
|
569
|
+
|
|
570
|
+
# Ensure helper availability
|
|
571
|
+
# We can't easily check if already imported in this scope, but
|
|
572
|
+
# re-import is cheap inside func or we assume generator handles it.
|
|
573
|
+
# TemplateCodegen usually assumes outside context.
|
|
574
|
+
# But wait, helper functions generated by this class do imports.
|
|
575
|
+
# Let's add import if we are about to use render_attrs?
|
|
576
|
+
# Easier to ensure it's imported at top of _render_template in
|
|
577
|
+
# generator.py?
|
|
578
|
+
# No, generator.py calls this.
|
|
579
|
+
# We can add a "has_render_attrs_usage" flag or just import it in the generated body
|
|
580
|
+
# if implicit_root_source is set or spread attr found.
|
|
581
|
+
# Let's just rely on generator to import common helpers, or add specific
|
|
582
|
+
# import here if needed.
|
|
583
|
+
# Actually existing code imports `ensure_async_iterator` locally (line 271).
|
|
584
|
+
pass
|
|
585
|
+
|
|
586
|
+
# 1. Handle $for
|
|
587
|
+
for_attr = next(
|
|
588
|
+
(a for a in node.special_attributes if isinstance(a, ForAttribute)), None
|
|
589
|
+
)
|
|
590
|
+
if for_attr:
|
|
591
|
+
loop_vars_str = for_attr.loop_vars
|
|
592
|
+
new_locals = local_vars.copy()
|
|
593
|
+
|
|
594
|
+
# Parse loop vars to handle tuple unpacking
|
|
595
|
+
# "x, y" -> targets
|
|
596
|
+
assign_stmt = ast.parse(f"{loop_vars_str} = 1").body[0]
|
|
597
|
+
assert isinstance(assign_stmt, ast.Assign)
|
|
598
|
+
loop_targets_node = assign_stmt.targets[0]
|
|
599
|
+
|
|
600
|
+
def extract_names(n: ast.AST) -> None:
|
|
601
|
+
if isinstance(n, ast.Name):
|
|
602
|
+
new_locals.add(n.id)
|
|
603
|
+
elif isinstance(n, (ast.Tuple, ast.List)):
|
|
604
|
+
for elt in n.elts:
|
|
605
|
+
extract_names(elt)
|
|
606
|
+
|
|
607
|
+
extract_names(loop_targets_node)
|
|
608
|
+
|
|
609
|
+
iterable_expr = self._transform_expr(
|
|
610
|
+
for_attr.iterable,
|
|
611
|
+
local_vars,
|
|
612
|
+
known_globals,
|
|
613
|
+
line_offset=node.line,
|
|
614
|
+
col_offset=node.column,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
for_body: List[ast.stmt] = []
|
|
618
|
+
|
|
619
|
+
new_attrs = [a for a in node.special_attributes if a is not for_attr]
|
|
620
|
+
if node.tag == "template":
|
|
621
|
+
for child in node.children:
|
|
622
|
+
self._add_node(
|
|
623
|
+
child,
|
|
624
|
+
for_body,
|
|
625
|
+
new_locals,
|
|
626
|
+
bound_var,
|
|
627
|
+
layout_id,
|
|
628
|
+
known_methods,
|
|
629
|
+
known_globals,
|
|
630
|
+
async_methods,
|
|
631
|
+
component_map,
|
|
632
|
+
scope_id,
|
|
633
|
+
parts_var=parts_var,
|
|
634
|
+
enable_regions=enable_regions,
|
|
635
|
+
)
|
|
636
|
+
else:
|
|
637
|
+
new_node = dataclasses.replace(node, special_attributes=new_attrs)
|
|
638
|
+
self._add_node(
|
|
639
|
+
new_node,
|
|
640
|
+
for_body,
|
|
641
|
+
new_locals,
|
|
642
|
+
bound_var,
|
|
643
|
+
layout_id,
|
|
644
|
+
known_methods,
|
|
645
|
+
known_globals,
|
|
646
|
+
async_methods,
|
|
647
|
+
component_map,
|
|
648
|
+
scope_id,
|
|
649
|
+
parts_var=parts_var,
|
|
650
|
+
enable_regions=enable_regions,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
# Wrap iterable in ensure_async_iterator
|
|
654
|
+
wrapped_iterable = ast.Call(
|
|
655
|
+
func=ast.Name(id="ensure_async_iterator", ctx=ast.Load()),
|
|
656
|
+
args=[iterable_expr],
|
|
657
|
+
keywords=[],
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
for_stmt = ast.AsyncFor(
|
|
661
|
+
target=loop_targets_node,
|
|
662
|
+
iter=wrapped_iterable,
|
|
663
|
+
body=for_body,
|
|
664
|
+
orelse=[],
|
|
665
|
+
)
|
|
666
|
+
# Tag with line number
|
|
667
|
+
self._set_line(for_stmt, node)
|
|
668
|
+
body.append(for_stmt)
|
|
669
|
+
return
|
|
670
|
+
|
|
671
|
+
# 2. Handle $if
|
|
672
|
+
if_attr = next(
|
|
673
|
+
(a for a in node.special_attributes if isinstance(a, IfAttribute)), None
|
|
674
|
+
)
|
|
675
|
+
if if_attr:
|
|
676
|
+
cond_expr = self._transform_expr(
|
|
677
|
+
if_attr.condition,
|
|
678
|
+
local_vars,
|
|
679
|
+
known_globals,
|
|
680
|
+
line_offset=node.line,
|
|
681
|
+
col_offset=node.column,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
if_body: List[ast.stmt] = []
|
|
685
|
+
new_attrs = [a for a in node.special_attributes if a is not if_attr]
|
|
686
|
+
new_node = dataclasses.replace(node, special_attributes=new_attrs)
|
|
687
|
+
self._add_node(
|
|
688
|
+
new_node,
|
|
689
|
+
if_body,
|
|
690
|
+
local_vars,
|
|
691
|
+
bound_var,
|
|
692
|
+
layout_id,
|
|
693
|
+
known_methods,
|
|
694
|
+
known_globals,
|
|
695
|
+
async_methods,
|
|
696
|
+
component_map,
|
|
697
|
+
scope_id,
|
|
698
|
+
parts_var=parts_var,
|
|
699
|
+
enable_regions=enable_regions,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
if_stmt = ast.If(test=cond_expr, body=if_body, orelse=[])
|
|
703
|
+
|
|
704
|
+
if_stmt = ast.If(test=cond_expr, body=if_body, orelse=[])
|
|
705
|
+
self._set_line(if_stmt, node)
|
|
706
|
+
body.append(if_stmt)
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
# --- Handle <slot> ---
|
|
710
|
+
if node.tag == "slot":
|
|
711
|
+
slot_name = node.attributes.get("name", "default")
|
|
712
|
+
is_head_slot = "$head" in node.attributes
|
|
713
|
+
|
|
714
|
+
default_renderer_arg: ast.expr = ast.Constant(value=None)
|
|
715
|
+
if node.children:
|
|
716
|
+
self._slot_default_counter += 1
|
|
717
|
+
func_name = (
|
|
718
|
+
f"_render_slot_default_{slot_name}_{self._slot_default_counter}"
|
|
719
|
+
)
|
|
720
|
+
aux_func = self._generate_function(
|
|
721
|
+
node.children, func_name, is_async=True
|
|
722
|
+
)
|
|
723
|
+
self.auxiliary_functions.append(aux_func)
|
|
724
|
+
default_renderer_arg = ast.Attribute(
|
|
725
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
726
|
+
attr=func_name,
|
|
727
|
+
ctx=ast.Load(),
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
call_kwargs = [
|
|
731
|
+
ast.keyword(arg="default_renderer", value=default_renderer_arg),
|
|
732
|
+
ast.keyword(
|
|
733
|
+
arg="layout_id",
|
|
734
|
+
value=ast.Constant(value=layout_id)
|
|
735
|
+
if layout_id
|
|
736
|
+
else ast.Call(
|
|
737
|
+
func=ast.Name(id="getattr", ctx=ast.Load()),
|
|
738
|
+
args=[
|
|
739
|
+
ast.Name(id="self", ctx=ast.Load()),
|
|
740
|
+
ast.Constant(value="LAYOUT_ID"),
|
|
741
|
+
ast.Constant(value=None),
|
|
742
|
+
],
|
|
743
|
+
keywords=[],
|
|
744
|
+
),
|
|
745
|
+
),
|
|
746
|
+
]
|
|
747
|
+
|
|
748
|
+
if is_head_slot:
|
|
749
|
+
call_kwargs.append(
|
|
750
|
+
ast.keyword(arg="append", value=ast.Constant(value=True))
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
render_call = ast.Call(
|
|
754
|
+
func=ast.Attribute(
|
|
755
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
756
|
+
attr="render_slot",
|
|
757
|
+
ctx=ast.Load(),
|
|
758
|
+
),
|
|
759
|
+
args=[ast.Constant(value=slot_name)],
|
|
760
|
+
keywords=call_kwargs,
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
append_stmt = ast.Expr(
|
|
764
|
+
value=ast.Call(
|
|
765
|
+
func=ast.Attribute(
|
|
766
|
+
value=ast.Name(id="parts", ctx=ast.Load()),
|
|
767
|
+
attr="append",
|
|
768
|
+
ctx=ast.Load(),
|
|
769
|
+
),
|
|
770
|
+
args=[ast.Await(value=render_call)],
|
|
771
|
+
keywords=[],
|
|
772
|
+
)
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
append_stmt = ast.Expr(
|
|
776
|
+
value=ast.Call(
|
|
777
|
+
func=ast.Attribute(
|
|
778
|
+
value=ast.Name(id=parts_var, ctx=ast.Load()),
|
|
779
|
+
attr="append",
|
|
780
|
+
ctx=ast.Load(),
|
|
781
|
+
),
|
|
782
|
+
args=[ast.Await(value=render_call)],
|
|
783
|
+
keywords=[],
|
|
784
|
+
)
|
|
785
|
+
)
|
|
786
|
+
self._set_line(append_stmt, node)
|
|
787
|
+
body.append(append_stmt)
|
|
788
|
+
return
|
|
789
|
+
|
|
790
|
+
if component_map and node.tag in component_map:
|
|
791
|
+
cls_name = component_map[node.tag]
|
|
792
|
+
|
|
793
|
+
# Prepare arguments (kwargs)
|
|
794
|
+
# Prepare arguments (kwargs dict keys/values)
|
|
795
|
+
dict_keys: List[Optional[ast.expr]] = []
|
|
796
|
+
dict_values: List[ast.expr] = []
|
|
797
|
+
|
|
798
|
+
# 1. Pass implicit context props (request, params, etc.)
|
|
799
|
+
for ctx_prop in ["request", "params", "query", "path", "url"]:
|
|
800
|
+
dict_keys.append(ast.Constant(value=ctx_prop))
|
|
801
|
+
dict_values.append(
|
|
802
|
+
ast.Attribute(
|
|
803
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
804
|
+
attr=ctx_prop,
|
|
805
|
+
ctx=ast.Load(),
|
|
806
|
+
)
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
# Pass __is_component__ flag
|
|
810
|
+
dict_keys.append(ast.Constant(value="__is_component__"))
|
|
811
|
+
dict_values.append(ast.Constant(value=True))
|
|
812
|
+
|
|
813
|
+
# Pass style collector
|
|
814
|
+
dict_keys.append(ast.Constant(value="_style_collector"))
|
|
815
|
+
dict_values.append(
|
|
816
|
+
ast.Attribute(
|
|
817
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
818
|
+
attr="_style_collector",
|
|
819
|
+
ctx=ast.Load(),
|
|
820
|
+
)
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
# Pass context for !provide/!inject
|
|
824
|
+
dict_keys.append(ast.Constant(value="_context"))
|
|
825
|
+
dict_values.append(
|
|
826
|
+
ast.Attribute(
|
|
827
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
828
|
+
attr="context",
|
|
829
|
+
ctx=ast.Load(),
|
|
830
|
+
)
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
# 2. Pass explicitly defined props (static)
|
|
834
|
+
for k, v in node.attributes.items():
|
|
835
|
+
dict_keys.append(ast.Constant(value=k))
|
|
836
|
+
|
|
837
|
+
val_expr = None
|
|
838
|
+
if "{" in v and "}" in v:
|
|
839
|
+
v_stripped = v.strip()
|
|
840
|
+
if (
|
|
841
|
+
v_stripped.startswith("{")
|
|
842
|
+
and v_stripped.endswith("}")
|
|
843
|
+
and v_stripped.count("{") == 1
|
|
844
|
+
):
|
|
845
|
+
# Single expression
|
|
846
|
+
expr_code = v_stripped[1:-1]
|
|
847
|
+
val_expr = self._transform_expr(
|
|
848
|
+
expr_code,
|
|
849
|
+
local_vars,
|
|
850
|
+
known_globals,
|
|
851
|
+
line_offset=node.line,
|
|
852
|
+
col_offset=node.column,
|
|
853
|
+
)
|
|
854
|
+
else:
|
|
855
|
+
# String interpolation
|
|
856
|
+
parts = self.interpolation_parser.parse(
|
|
857
|
+
v, node.line, node.column
|
|
858
|
+
)
|
|
859
|
+
current_concat: Optional[ast.expr] = None
|
|
860
|
+
for part in parts:
|
|
861
|
+
term: ast.expr
|
|
862
|
+
if isinstance(part, str):
|
|
863
|
+
term = ast.Constant(value=part)
|
|
864
|
+
else:
|
|
865
|
+
term = ast.Call(
|
|
866
|
+
func=ast.Name(id="str", ctx=ast.Load()),
|
|
867
|
+
args=[
|
|
868
|
+
self._transform_expr(
|
|
869
|
+
part.expression,
|
|
870
|
+
local_vars,
|
|
871
|
+
known_globals,
|
|
872
|
+
line_offset=part.line,
|
|
873
|
+
col_offset=part.column,
|
|
874
|
+
)
|
|
875
|
+
],
|
|
876
|
+
keywords=[],
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
if current_concat is None:
|
|
880
|
+
current_concat = term
|
|
881
|
+
else:
|
|
882
|
+
current_concat = ast.BinOp(
|
|
883
|
+
left=current_concat, op=ast.Add(), right=term
|
|
884
|
+
)
|
|
885
|
+
val_expr = (
|
|
886
|
+
current_concat if current_concat else ast.Constant(value="")
|
|
887
|
+
)
|
|
888
|
+
else:
|
|
889
|
+
# Static string
|
|
890
|
+
val_expr = ast.Constant(value=v)
|
|
891
|
+
|
|
892
|
+
dict_values.append(val_expr)
|
|
893
|
+
|
|
894
|
+
# 3. Handle special attributes
|
|
895
|
+
# from pywire.compiler.ast_nodes import ReactiveAttribute, EventAttribute
|
|
896
|
+
# # Shadowing global
|
|
897
|
+
|
|
898
|
+
# Group events by type for batch handling logic
|
|
899
|
+
event_attrs_by_type = defaultdict(list)
|
|
900
|
+
for attr in node.special_attributes:
|
|
901
|
+
if isinstance(attr, EventAttribute):
|
|
902
|
+
event_attrs_by_type[attr.event_type].append(attr)
|
|
903
|
+
|
|
904
|
+
# Process non-event special attributes (Reactive) and Events
|
|
905
|
+
for attr in node.special_attributes:
|
|
906
|
+
if isinstance(attr, ReactiveAttribute):
|
|
907
|
+
dict_keys.append(ast.Constant(value=attr.name))
|
|
908
|
+
expr = self._transform_reactive_expr(
|
|
909
|
+
attr.expr,
|
|
910
|
+
local_vars,
|
|
911
|
+
known_methods,
|
|
912
|
+
known_globals,
|
|
913
|
+
async_methods,
|
|
914
|
+
line_offset=node.line,
|
|
915
|
+
col_offset=node.column,
|
|
916
|
+
)
|
|
917
|
+
dict_values.append(expr)
|
|
918
|
+
|
|
919
|
+
# Compile events into data-on-* attributes to pass as props
|
|
920
|
+
# This logic mirrors the standard element event generation
|
|
921
|
+
for event_type, attrs_list in event_attrs_by_type.items():
|
|
922
|
+
if len(attrs_list) == 1:
|
|
923
|
+
# Single handler
|
|
924
|
+
attr = attrs_list[0]
|
|
925
|
+
|
|
926
|
+
# data-on-X
|
|
927
|
+
dict_keys.append(ast.Constant(value=f"data-on-{event_type}"))
|
|
928
|
+
|
|
929
|
+
# Resolve handler string/expr
|
|
930
|
+
raw_handler = attr.handler_name
|
|
931
|
+
if raw_handler.strip().startswith(
|
|
932
|
+
"{"
|
|
933
|
+
) and raw_handler.strip().endswith("}"):
|
|
934
|
+
# New syntax: {expr} -> Evaluate it?
|
|
935
|
+
# Wait, standard event logic treats handler_name as STRING NAME usually.
|
|
936
|
+
# If it's an expression like {print('hi')}, it evaluates to None.
|
|
937
|
+
# We need to register it?
|
|
938
|
+
# Actually, standard element logic (lines 880+) sets value=ast.Constant(
|
|
939
|
+
# value=attr.handler_name
|
|
940
|
+
# ).
|
|
941
|
+
# It assumes the handler_name is a STRING that refers to a method.
|
|
942
|
+
# OR it assumes the runtime handles looking it up?
|
|
943
|
+
# If user wrote @click={print('hi')}, the parser makes
|
|
944
|
+
# handler_name="{print('hi')}".
|
|
945
|
+
# The standard logic just dumps that string?
|
|
946
|
+
# Let's check runtime/client code.
|
|
947
|
+
# If client receives data-on-click="{print('hi')}", it likely tries to
|
|
948
|
+
# eval/run it within context.
|
|
949
|
+
# So we should pass it AS A STRING.
|
|
950
|
+
# BUT, if we evaluated it in my previous attempt (`val =
|
|
951
|
+
# transform_expr...`), we passed the RESULT (None).
|
|
952
|
+
|
|
953
|
+
# CORRECT APPROACH: Pass the handler identifier string or expression
|
|
954
|
+
# string AS IS.
|
|
955
|
+
# The client side `pywire.js` parses the `data-on-click` value.
|
|
956
|
+
# If it's a method name "onClick", it calls it.
|
|
957
|
+
# If it's code "print('hi')", it might eval it?
|
|
958
|
+
# Actually pywire seems to rely on named handlers mostly.
|
|
959
|
+
# The `run_demo_test` output showed: `data-on-click="<bound method...>"`
|
|
960
|
+
# That happened because I evaluated it.
|
|
961
|
+
# If I pass the raw string "print('hi')", it will render as
|
|
962
|
+
# `data-on-click="print('hi')"`.
|
|
963
|
+
# Does the client support eval?
|
|
964
|
+
# Looking at `attributes/events.py`, parser stores raw string.
|
|
965
|
+
|
|
966
|
+
dict_values.append(ast.Constant(value=attr.handler_name))
|
|
967
|
+
|
|
968
|
+
else:
|
|
969
|
+
dict_values.append(ast.Constant(value=attr.handler_name))
|
|
970
|
+
|
|
971
|
+
# Modifiers
|
|
972
|
+
if attr.modifiers:
|
|
973
|
+
dict_keys.append(
|
|
974
|
+
ast.Constant(value=f"data-modifiers-{event_type}")
|
|
975
|
+
)
|
|
976
|
+
dict_values.append(ast.Constant(value=" ".join(attr.modifiers)))
|
|
977
|
+
|
|
978
|
+
# Args
|
|
979
|
+
for i, arg_expr in enumerate(attr.args):
|
|
980
|
+
dict_keys.append(ast.Constant(value=f"data-arg-{i}"))
|
|
981
|
+
# Evaluate arg expr and json dump
|
|
982
|
+
val = self._transform_expr(
|
|
983
|
+
arg_expr,
|
|
984
|
+
local_vars,
|
|
985
|
+
known_globals,
|
|
986
|
+
line_offset=node.line,
|
|
987
|
+
col_offset=node.column,
|
|
988
|
+
)
|
|
989
|
+
dump_call = ast.Call(
|
|
990
|
+
func=ast.Attribute(
|
|
991
|
+
value=ast.Name(id="json", ctx=ast.Load()),
|
|
992
|
+
attr="dumps",
|
|
993
|
+
ctx=ast.Load(),
|
|
994
|
+
),
|
|
995
|
+
args=[val],
|
|
996
|
+
keywords=[],
|
|
997
|
+
)
|
|
998
|
+
dict_values.append(dump_call)
|
|
999
|
+
|
|
1000
|
+
else:
|
|
1001
|
+
# Multiple handlers -> compile to JSON structure
|
|
1002
|
+
# We need to construct the list of dicts at runtime and json dump it
|
|
1003
|
+
# This is complex to do inline in dict_values construction.
|
|
1004
|
+
# Helper var needed?
|
|
1005
|
+
# We are inside `_add_node` building `body`.
|
|
1006
|
+
# We can prepend statements to `body` to build the list, then reference it.
|
|
1007
|
+
# But here we are building `dict_values` list for the `ast.Dict`.
|
|
1008
|
+
# We can put an `ast.Call` that invokes `json.dumps` on a list comprehension?
|
|
1009
|
+
# Or simpler: Just emit the logic to build the list into a temp var, use temp
|
|
1010
|
+
# var here.
|
|
1011
|
+
|
|
1012
|
+
# Generate temp var name
|
|
1013
|
+
handler_list_name = (
|
|
1014
|
+
f"_handlers_{event_type}_{node.line}_{node.column}"
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
# ... [Code similar to lines 907+ to build the list] ...
|
|
1018
|
+
# But wait, lines 907+ append to `body`.
|
|
1019
|
+
# I can do that here! I am in `_add_node`.
|
|
1020
|
+
# I just need to interrupt the `dict` building?
|
|
1021
|
+
# No, I am building lists `dict_keys`, `dict_values`.
|
|
1022
|
+
# I can append statements to `body` *before* the final
|
|
1023
|
+
# `keywords.append(...)` call.
|
|
1024
|
+
|
|
1025
|
+
# [Insert list building logic here]
|
|
1026
|
+
# Since I am replacing a block, I can add statements to body!
|
|
1027
|
+
# Wait, `body` is passed in.
|
|
1028
|
+
# `dict_keys` and `dict_values` are python lists I am building to
|
|
1029
|
+
# *eventually* make an AST node.
|
|
1030
|
+
|
|
1031
|
+
# Let's support single handler first as it covers 99% cases and the
|
|
1032
|
+
# specific bug.
|
|
1033
|
+
# Complex multi-handlers need full porting.
|
|
1034
|
+
pass
|
|
1035
|
+
|
|
1036
|
+
# Add keyword(arg=None, value=dict) for **kwargs
|
|
1037
|
+
keywords = []
|
|
1038
|
+
keywords.append(
|
|
1039
|
+
ast.keyword(
|
|
1040
|
+
arg=None, value=ast.Dict(keys=dict_keys, values=dict_values)
|
|
1041
|
+
)
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
# 4. Handle Slots (Children)
|
|
1045
|
+
# Group children by slot name
|
|
1046
|
+
slots_map: Dict[str, List[TemplateNode]] = {}
|
|
1047
|
+
default_slot_nodes = []
|
|
1048
|
+
|
|
1049
|
+
for child in node.children:
|
|
1050
|
+
# Check for slot="..." attribute on child
|
|
1051
|
+
# Note: child is TemplateNode. attributes dict.
|
|
1052
|
+
# If element:
|
|
1053
|
+
child_slot_name: Optional[str] = None
|
|
1054
|
+
if child.tag and "slot" in child.attributes:
|
|
1055
|
+
child_slot_name = child.attributes["slot"]
|
|
1056
|
+
# Remove slot attribute? Optional but cleaner.
|
|
1057
|
+
|
|
1058
|
+
if child_slot_name:
|
|
1059
|
+
if child_slot_name not in slots_map:
|
|
1060
|
+
slots_map[child_slot_name] = []
|
|
1061
|
+
slots_map[child_slot_name].append(child)
|
|
1062
|
+
else:
|
|
1063
|
+
default_slot_nodes.append(child)
|
|
1064
|
+
|
|
1065
|
+
if default_slot_nodes:
|
|
1066
|
+
slots_map["default"] = default_slot_nodes
|
|
1067
|
+
|
|
1068
|
+
keys: List[Optional[ast.expr]] = []
|
|
1069
|
+
values: List[ast.expr] = []
|
|
1070
|
+
|
|
1071
|
+
for s_name, s_nodes in slots_map.items():
|
|
1072
|
+
slot_var_name = f"_slot_{s_name}_{node.line}_{node.column}".replace(
|
|
1073
|
+
"-", "_"
|
|
1074
|
+
)
|
|
1075
|
+
slot_parts_var = f"{slot_var_name}_parts"
|
|
1076
|
+
|
|
1077
|
+
body.append(
|
|
1078
|
+
ast.Assign(
|
|
1079
|
+
targets=[ast.Name(id=slot_parts_var, ctx=ast.Store())],
|
|
1080
|
+
value=ast.List(elts=[], ctx=ast.Load()),
|
|
1081
|
+
)
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
for s_node in s_nodes:
|
|
1085
|
+
self._add_node(
|
|
1086
|
+
s_node,
|
|
1087
|
+
body,
|
|
1088
|
+
local_vars,
|
|
1089
|
+
bound_var,
|
|
1090
|
+
layout_id,
|
|
1091
|
+
known_methods,
|
|
1092
|
+
known_globals,
|
|
1093
|
+
async_methods,
|
|
1094
|
+
component_map,
|
|
1095
|
+
scope_id,
|
|
1096
|
+
parts_var=slot_parts_var,
|
|
1097
|
+
enable_regions=enable_regions,
|
|
1098
|
+
) # PASS slot_parts_var
|
|
1099
|
+
|
|
1100
|
+
# Join parts -> slot string
|
|
1101
|
+
# rendered_slot = "".join(slot_parts_var)
|
|
1102
|
+
body.append(
|
|
1103
|
+
ast.Assign(
|
|
1104
|
+
targets=[ast.Name(id=slot_var_name, ctx=ast.Store())],
|
|
1105
|
+
value=ast.Call(
|
|
1106
|
+
func=ast.Attribute(
|
|
1107
|
+
value=ast.Constant(value=""),
|
|
1108
|
+
attr="join",
|
|
1109
|
+
ctx=ast.Load(),
|
|
1110
|
+
),
|
|
1111
|
+
args=[ast.Name(id=slot_parts_var, ctx=ast.Load())],
|
|
1112
|
+
keywords=[],
|
|
1113
|
+
),
|
|
1114
|
+
)
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
keys.append(ast.Constant(value=s_name))
|
|
1118
|
+
values.append(ast.Name(id=slot_var_name, ctx=ast.Load()))
|
|
1119
|
+
|
|
1120
|
+
# Add slots=... to keywords
|
|
1121
|
+
if keys:
|
|
1122
|
+
keywords.append(
|
|
1123
|
+
ast.keyword(
|
|
1124
|
+
arg="slots",
|
|
1125
|
+
value=ast.Dict(
|
|
1126
|
+
keys=keys,
|
|
1127
|
+
values=values,
|
|
1128
|
+
),
|
|
1129
|
+
)
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
# Instantiate component
|
|
1133
|
+
instantiation = ast.Call(
|
|
1134
|
+
func=ast.Name(id=cls_name, ctx=ast.Load()), args=[], keywords=keywords
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
render_call = ast.Call(
|
|
1138
|
+
func=ast.Attribute(
|
|
1139
|
+
value=instantiation, attr="_render_template", ctx=ast.Load()
|
|
1140
|
+
),
|
|
1141
|
+
args=[],
|
|
1142
|
+
keywords=[],
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
# Append result
|
|
1146
|
+
# parts.append(await ...)
|
|
1147
|
+
append_stmt = ast.Expr(
|
|
1148
|
+
value=ast.Call(
|
|
1149
|
+
func=ast.Attribute(
|
|
1150
|
+
value=ast.Name(id=parts_var, ctx=ast.Load()),
|
|
1151
|
+
attr="append",
|
|
1152
|
+
ctx=ast.Load(),
|
|
1153
|
+
),
|
|
1154
|
+
args=[ast.Await(value=render_call)],
|
|
1155
|
+
keywords=[],
|
|
1156
|
+
)
|
|
1157
|
+
)
|
|
1158
|
+
self._set_line(append_stmt, node)
|
|
1159
|
+
body.append(append_stmt)
|
|
1160
|
+
return
|
|
1161
|
+
|
|
1162
|
+
# 3. Render Node
|
|
1163
|
+
if node.tag is None:
|
|
1164
|
+
# Text
|
|
1165
|
+
if node.text_content:
|
|
1166
|
+
parts = []
|
|
1167
|
+
if node.is_raw:
|
|
1168
|
+
parts = [node.text_content]
|
|
1169
|
+
else:
|
|
1170
|
+
parts = self.interpolation_parser.parse(
|
|
1171
|
+
node.text_content, node.line, node.column
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
# Optimizations: single string -> simple append
|
|
1175
|
+
if len(parts) == 1 and isinstance(parts[0], str):
|
|
1176
|
+
append_stmt = ast.Expr(
|
|
1177
|
+
value=ast.Call(
|
|
1178
|
+
func=ast.Attribute(
|
|
1179
|
+
value=ast.Name(id=parts_var, ctx=ast.Load()),
|
|
1180
|
+
attr="append",
|
|
1181
|
+
ctx=ast.Load(),
|
|
1182
|
+
),
|
|
1183
|
+
args=[ast.Constant(value=parts[0])],
|
|
1184
|
+
keywords=[],
|
|
1185
|
+
)
|
|
1186
|
+
)
|
|
1187
|
+
self._set_line(append_stmt, node)
|
|
1188
|
+
body.append(append_stmt)
|
|
1189
|
+
else:
|
|
1190
|
+
# Mixed parts: construct concatenation
|
|
1191
|
+
current_concat = None
|
|
1192
|
+
|
|
1193
|
+
for part in parts:
|
|
1194
|
+
if isinstance(part, str):
|
|
1195
|
+
term = ast.Constant(value=part)
|
|
1196
|
+
else:
|
|
1197
|
+
expr = self._transform_expr(
|
|
1198
|
+
part.expression,
|
|
1199
|
+
local_vars,
|
|
1200
|
+
known_globals,
|
|
1201
|
+
line_offset=part.line,
|
|
1202
|
+
col_offset=part.column,
|
|
1203
|
+
)
|
|
1204
|
+
# Check if this is a raw (unescaped) interpolation
|
|
1205
|
+
is_raw = getattr(part, "is_raw", False)
|
|
1206
|
+
if is_raw:
|
|
1207
|
+
# Raw HTML - no escaping
|
|
1208
|
+
term = ast.Call(
|
|
1209
|
+
func=ast.Name(id="str", ctx=ast.Load()),
|
|
1210
|
+
args=[self._wrap_unwrap_wire(expr)],
|
|
1211
|
+
keywords=[],
|
|
1212
|
+
)
|
|
1213
|
+
else:
|
|
1214
|
+
# Default: escape HTML for XSS prevention
|
|
1215
|
+
term = ast.Call(
|
|
1216
|
+
func=ast.Name(id="escape_html", ctx=ast.Load()),
|
|
1217
|
+
args=[self._wrap_unwrap_wire(expr)],
|
|
1218
|
+
keywords=[],
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
if current_concat is None:
|
|
1222
|
+
current_concat = term
|
|
1223
|
+
else:
|
|
1224
|
+
current_concat = ast.BinOp(
|
|
1225
|
+
left=current_concat, op=ast.Add(), right=term
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
if current_concat:
|
|
1229
|
+
append_stmt = ast.Expr(
|
|
1230
|
+
value=ast.Call(
|
|
1231
|
+
func=ast.Attribute(
|
|
1232
|
+
value=ast.Name(id=parts_var, ctx=ast.Load()),
|
|
1233
|
+
attr="append",
|
|
1234
|
+
ctx=ast.Load(),
|
|
1235
|
+
),
|
|
1236
|
+
args=[current_concat],
|
|
1237
|
+
keywords=[],
|
|
1238
|
+
)
|
|
1239
|
+
)
|
|
1240
|
+
self._set_line(append_stmt, node)
|
|
1241
|
+
body.append(append_stmt)
|
|
1242
|
+
elif node.special_attributes and isinstance(
|
|
1243
|
+
node.special_attributes[0], InterpolationNode
|
|
1244
|
+
):
|
|
1245
|
+
# Handle standalone interpolation node from parser splitting
|
|
1246
|
+
interp = node.special_attributes[0]
|
|
1247
|
+
expr = self._transform_expr(
|
|
1248
|
+
interp.expression,
|
|
1249
|
+
local_vars,
|
|
1250
|
+
known_globals,
|
|
1251
|
+
line_offset=interp.line,
|
|
1252
|
+
col_offset=interp.column,
|
|
1253
|
+
)
|
|
1254
|
+
# Check if this is a raw (unescaped) interpolation
|
|
1255
|
+
is_raw = getattr(interp, "is_raw", False)
|
|
1256
|
+
if is_raw:
|
|
1257
|
+
# Raw HTML - no escaping
|
|
1258
|
+
term = ast.Call(
|
|
1259
|
+
func=ast.Name(id="str", ctx=ast.Load()),
|
|
1260
|
+
args=[self._wrap_unwrap_wire(expr)],
|
|
1261
|
+
keywords=[],
|
|
1262
|
+
)
|
|
1263
|
+
else:
|
|
1264
|
+
# Default: escape HTML for XSS prevention
|
|
1265
|
+
term = ast.Call(
|
|
1266
|
+
func=ast.Name(id="escape_html", ctx=ast.Load()),
|
|
1267
|
+
args=[self._wrap_unwrap_wire(expr)],
|
|
1268
|
+
keywords=[],
|
|
1269
|
+
)
|
|
1270
|
+
append_stmt = ast.Expr(
|
|
1271
|
+
value=ast.Call(
|
|
1272
|
+
func=ast.Attribute(
|
|
1273
|
+
value=ast.Name(id=parts_var, ctx=ast.Load()),
|
|
1274
|
+
attr="append",
|
|
1275
|
+
ctx=ast.Load(),
|
|
1276
|
+
),
|
|
1277
|
+
args=[term],
|
|
1278
|
+
keywords=[],
|
|
1279
|
+
)
|
|
1280
|
+
)
|
|
1281
|
+
self._set_line(append_stmt, node)
|
|
1282
|
+
body.append(append_stmt)
|
|
1283
|
+
pass
|
|
1284
|
+
else:
|
|
1285
|
+
# Element
|
|
1286
|
+
if enable_regions and self._node_is_dynamic(node, known_globals):
|
|
1287
|
+
region_id = self._next_region_id()
|
|
1288
|
+
method_name = f"_render_region_{region_id}"
|
|
1289
|
+
self.region_renderers[region_id] = method_name
|
|
1290
|
+
self.auxiliary_functions.append(
|
|
1291
|
+
self._generate_region_method(
|
|
1292
|
+
node,
|
|
1293
|
+
method_name,
|
|
1294
|
+
region_id,
|
|
1295
|
+
layout_id,
|
|
1296
|
+
known_methods,
|
|
1297
|
+
known_globals,
|
|
1298
|
+
async_methods,
|
|
1299
|
+
component_map,
|
|
1300
|
+
scope_id,
|
|
1301
|
+
implicit_root_source,
|
|
1302
|
+
)
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
append_stmt = ast.Expr(
|
|
1306
|
+
value=ast.Call(
|
|
1307
|
+
func=ast.Attribute(
|
|
1308
|
+
value=ast.Name(id=parts_var, ctx=ast.Load()),
|
|
1309
|
+
attr="append",
|
|
1310
|
+
ctx=ast.Load(),
|
|
1311
|
+
),
|
|
1312
|
+
args=[
|
|
1313
|
+
ast.Await(
|
|
1314
|
+
value=ast.Call(
|
|
1315
|
+
func=ast.Attribute(
|
|
1316
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1317
|
+
attr=method_name,
|
|
1318
|
+
ctx=ast.Load(),
|
|
1319
|
+
),
|
|
1320
|
+
args=[],
|
|
1321
|
+
keywords=[],
|
|
1322
|
+
)
|
|
1323
|
+
)
|
|
1324
|
+
],
|
|
1325
|
+
keywords=[],
|
|
1326
|
+
)
|
|
1327
|
+
)
|
|
1328
|
+
self._set_line(append_stmt, node)
|
|
1329
|
+
body.append(append_stmt)
|
|
1330
|
+
return
|
|
1331
|
+
|
|
1332
|
+
bindings: Dict[str, ast.expr] = {}
|
|
1333
|
+
new_bound_var = bound_var
|
|
1334
|
+
if region_id:
|
|
1335
|
+
bindings["data-pw-region"] = ast.Constant(value=region_id)
|
|
1336
|
+
|
|
1337
|
+
show_attr = next(
|
|
1338
|
+
(a for a in node.special_attributes if isinstance(a, ShowAttribute)),
|
|
1339
|
+
None,
|
|
1340
|
+
)
|
|
1341
|
+
key_attr = next(
|
|
1342
|
+
(a for a in node.special_attributes if isinstance(a, KeyAttribute)),
|
|
1343
|
+
None,
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
if key_attr:
|
|
1347
|
+
bindings["id"] = ast.Call(
|
|
1348
|
+
func=ast.Name(id="str", ctx=ast.Load()),
|
|
1349
|
+
args=[
|
|
1350
|
+
self._transform_expr(
|
|
1351
|
+
key_attr.expr,
|
|
1352
|
+
local_vars,
|
|
1353
|
+
known_globals,
|
|
1354
|
+
line_offset=node.line,
|
|
1355
|
+
col_offset=node.column,
|
|
1356
|
+
)
|
|
1357
|
+
],
|
|
1358
|
+
keywords=[],
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1361
|
+
# attrs = {}
|
|
1362
|
+
body.append(
|
|
1363
|
+
ast.Assign(
|
|
1364
|
+
targets=[ast.Name(id="attrs", ctx=ast.Store())],
|
|
1365
|
+
value=ast.Dict(keys=[], values=[]),
|
|
1366
|
+
)
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
# Identify if we need to apply scope
|
|
1370
|
+
# Apply to all elements if scope_id is present
|
|
1371
|
+
# BUT: do not apply to <style> tag itself (unless we want to?), or <script>.
|
|
1372
|
+
# And <slot>.
|
|
1373
|
+
# <style scoped> handling is separate (reshaping content).
|
|
1374
|
+
|
|
1375
|
+
apply_scope = scope_id and node.tag not in (
|
|
1376
|
+
"style",
|
|
1377
|
+
"script",
|
|
1378
|
+
"slot",
|
|
1379
|
+
"template",
|
|
1380
|
+
)
|
|
1381
|
+
if apply_scope:
|
|
1382
|
+
body.append(
|
|
1383
|
+
ast.Assign(
|
|
1384
|
+
targets=[
|
|
1385
|
+
ast.Subscript(
|
|
1386
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1387
|
+
slice=ast.Constant(value=f"data-ph-{scope_id}"),
|
|
1388
|
+
ctx=ast.Store(),
|
|
1389
|
+
)
|
|
1390
|
+
],
|
|
1391
|
+
value=ast.Constant(value=""),
|
|
1392
|
+
)
|
|
1393
|
+
)
|
|
1394
|
+
|
|
1395
|
+
# Handle <style scoped> content rewriting
|
|
1396
|
+
if node.tag == "style" and scope_id and "scoped" in node.attributes:
|
|
1397
|
+
# Rewrite content
|
|
1398
|
+
if node.children and node.children[0].text_content:
|
|
1399
|
+
original_css = node.children[0].text_content
|
|
1400
|
+
|
|
1401
|
+
# Rewrite CSS with scope ID
|
|
1402
|
+
def rewrite_css(css: str, sid: str) -> str:
|
|
1403
|
+
new_parts = []
|
|
1404
|
+
last_idx = 0
|
|
1405
|
+
in_brace = False
|
|
1406
|
+
for i, char in enumerate(css):
|
|
1407
|
+
if char == "{":
|
|
1408
|
+
if not in_brace:
|
|
1409
|
+
selectors = css[last_idx:i]
|
|
1410
|
+
rewritten_selectors = ",".join(
|
|
1411
|
+
[
|
|
1412
|
+
f"{s.strip()}[data-ph-{sid}]"
|
|
1413
|
+
for s in selectors.split(",")
|
|
1414
|
+
if s.strip()
|
|
1415
|
+
]
|
|
1416
|
+
)
|
|
1417
|
+
new_parts.append(rewritten_selectors)
|
|
1418
|
+
in_brace = True
|
|
1419
|
+
last_idx = i
|
|
1420
|
+
elif char == "}":
|
|
1421
|
+
if in_brace:
|
|
1422
|
+
new_parts.append(css[last_idx : i + 1])
|
|
1423
|
+
in_brace = False
|
|
1424
|
+
last_idx = i + 1
|
|
1425
|
+
|
|
1426
|
+
new_parts.append(css[last_idx:])
|
|
1427
|
+
return "".join(new_parts)
|
|
1428
|
+
|
|
1429
|
+
rewritten_css = rewrite_css(original_css, scope_id)
|
|
1430
|
+
|
|
1431
|
+
# Generate code to add style to collector:
|
|
1432
|
+
# self._style_collector.add(scope_id, rewritten_css)
|
|
1433
|
+
body.append(
|
|
1434
|
+
ast.Expr(
|
|
1435
|
+
value=ast.Call(
|
|
1436
|
+
func=ast.Attribute(
|
|
1437
|
+
value=ast.Attribute(
|
|
1438
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1439
|
+
attr="_style_collector",
|
|
1440
|
+
ctx=ast.Load(),
|
|
1441
|
+
),
|
|
1442
|
+
attr="add",
|
|
1443
|
+
ctx=ast.Load(),
|
|
1444
|
+
),
|
|
1445
|
+
args=[
|
|
1446
|
+
ast.Constant(value=scope_id),
|
|
1447
|
+
ast.Constant(value=rewritten_css),
|
|
1448
|
+
],
|
|
1449
|
+
keywords=[],
|
|
1450
|
+
)
|
|
1451
|
+
)
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
# DO NOT output the style node to `parts`.
|
|
1455
|
+
# We just return here because we've handled the "rendering" of this node
|
|
1456
|
+
# (by registering side effect)
|
|
1457
|
+
return
|
|
1458
|
+
|
|
1459
|
+
# Static attrs
|
|
1460
|
+
for k, v in node.attributes.items():
|
|
1461
|
+
if "{" in v and "}" in v:
|
|
1462
|
+
parts = self.interpolation_parser.parse(v, node.line, node.column)
|
|
1463
|
+
current_concat = None
|
|
1464
|
+
for part in parts:
|
|
1465
|
+
if isinstance(part, str):
|
|
1466
|
+
term = ast.Constant(value=part)
|
|
1467
|
+
else:
|
|
1468
|
+
term = ast.Call(
|
|
1469
|
+
func=ast.Name(id="str", ctx=ast.Load()),
|
|
1470
|
+
args=[
|
|
1471
|
+
self._transform_expr(
|
|
1472
|
+
part.expression, local_vars, known_globals
|
|
1473
|
+
)
|
|
1474
|
+
],
|
|
1475
|
+
keywords=[],
|
|
1476
|
+
)
|
|
1477
|
+
if current_concat is None:
|
|
1478
|
+
current_concat = term
|
|
1479
|
+
else:
|
|
1480
|
+
current_concat = ast.BinOp(
|
|
1481
|
+
left=current_concat, op=ast.Add(), right=term
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
val_expr = (
|
|
1485
|
+
current_concat if current_concat else ast.Constant(value="")
|
|
1486
|
+
)
|
|
1487
|
+
else:
|
|
1488
|
+
val_expr = ast.Constant(value=v)
|
|
1489
|
+
|
|
1490
|
+
body.append(
|
|
1491
|
+
ast.Assign(
|
|
1492
|
+
targets=[
|
|
1493
|
+
ast.Subscript(
|
|
1494
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1495
|
+
slice=ast.Constant(value=k),
|
|
1496
|
+
ctx=ast.Store(),
|
|
1497
|
+
)
|
|
1498
|
+
],
|
|
1499
|
+
value=val_expr,
|
|
1500
|
+
)
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
# Bindings
|
|
1504
|
+
for k, binding_expr in bindings.items():
|
|
1505
|
+
if k == "checked":
|
|
1506
|
+
# if binding_expr: attrs['checked'] = ""
|
|
1507
|
+
body.append(
|
|
1508
|
+
ast.If(
|
|
1509
|
+
test=binding_expr,
|
|
1510
|
+
body=[
|
|
1511
|
+
ast.Assign(
|
|
1512
|
+
targets=[
|
|
1513
|
+
ast.Subscript(
|
|
1514
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1515
|
+
slice=ast.Constant(value="checked"),
|
|
1516
|
+
ctx=ast.Store(),
|
|
1517
|
+
)
|
|
1518
|
+
],
|
|
1519
|
+
value=ast.Constant(value=""),
|
|
1520
|
+
)
|
|
1521
|
+
],
|
|
1522
|
+
orelse=[],
|
|
1523
|
+
)
|
|
1524
|
+
)
|
|
1525
|
+
else:
|
|
1526
|
+
# attrs[k] = str(binding_expr) usually?
|
|
1527
|
+
# If binding_expr is AST expression (from target_var_expr), wrap in str()
|
|
1528
|
+
# If binding_expr is Constant string, direct.
|
|
1529
|
+
# Warning: bindings[k] contains AST nodes now.
|
|
1530
|
+
|
|
1531
|
+
wrapper = binding_expr
|
|
1532
|
+
if not isinstance(binding_expr, ast.Constant):
|
|
1533
|
+
wrapper = ast.Call(
|
|
1534
|
+
func=ast.Name(id="str", ctx=ast.Load()),
|
|
1535
|
+
args=[self._wrap_unwrap_wire(binding_expr)],
|
|
1536
|
+
keywords=[],
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
body.append(
|
|
1540
|
+
ast.Assign(
|
|
1541
|
+
targets=[
|
|
1542
|
+
ast.Subscript(
|
|
1543
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1544
|
+
slice=ast.Constant(value=k),
|
|
1545
|
+
ctx=ast.Store(),
|
|
1546
|
+
)
|
|
1547
|
+
],
|
|
1548
|
+
value=wrapper,
|
|
1549
|
+
)
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
# Group and generate event attributes (handling multiples via JSON)
|
|
1553
|
+
event_attrs_by_type = defaultdict(list)
|
|
1554
|
+
for attr in node.special_attributes:
|
|
1555
|
+
if isinstance(attr, EventAttribute):
|
|
1556
|
+
event_attrs_by_type[attr.event_type].append(attr)
|
|
1557
|
+
|
|
1558
|
+
for event_type, attrs_list in event_attrs_by_type.items():
|
|
1559
|
+
if len(attrs_list) == 1:
|
|
1560
|
+
# Single handler
|
|
1561
|
+
attr = attrs_list[0]
|
|
1562
|
+
# attrs["data-on-X"] = "handler"
|
|
1563
|
+
body.append(
|
|
1564
|
+
ast.Assign(
|
|
1565
|
+
targets=[
|
|
1566
|
+
ast.Subscript(
|
|
1567
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1568
|
+
slice=ast.Constant(value=f"data-on-{event_type}"),
|
|
1569
|
+
ctx=ast.Store(),
|
|
1570
|
+
)
|
|
1571
|
+
],
|
|
1572
|
+
value=ast.Constant(value=attr.handler_name),
|
|
1573
|
+
)
|
|
1574
|
+
)
|
|
1575
|
+
|
|
1576
|
+
# Add modifiers if present
|
|
1577
|
+
if attr.modifiers:
|
|
1578
|
+
modifiers_str = " ".join(attr.modifiers)
|
|
1579
|
+
body.append(
|
|
1580
|
+
ast.Assign(
|
|
1581
|
+
targets=[
|
|
1582
|
+
ast.Subscript(
|
|
1583
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1584
|
+
slice=ast.Constant(
|
|
1585
|
+
value=f"data-modifiers-{event_type}"
|
|
1586
|
+
),
|
|
1587
|
+
ctx=ast.Store(),
|
|
1588
|
+
)
|
|
1589
|
+
],
|
|
1590
|
+
value=ast.Constant(value=modifiers_str),
|
|
1591
|
+
)
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
# Add args
|
|
1595
|
+
for i, arg_expr in enumerate(attr.args):
|
|
1596
|
+
val = self._transform_expr(
|
|
1597
|
+
arg_expr,
|
|
1598
|
+
local_vars,
|
|
1599
|
+
known_globals,
|
|
1600
|
+
line_offset=node.line,
|
|
1601
|
+
col_offset=node.column,
|
|
1602
|
+
)
|
|
1603
|
+
dump_call = ast.Call(
|
|
1604
|
+
func=ast.Attribute(
|
|
1605
|
+
value=ast.Name(id="json", ctx=ast.Load()),
|
|
1606
|
+
attr="dumps",
|
|
1607
|
+
ctx=ast.Load(),
|
|
1608
|
+
),
|
|
1609
|
+
args=[val],
|
|
1610
|
+
keywords=[],
|
|
1611
|
+
)
|
|
1612
|
+
body.append(
|
|
1613
|
+
ast.Assign(
|
|
1614
|
+
targets=[
|
|
1615
|
+
ast.Subscript(
|
|
1616
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1617
|
+
slice=ast.Constant(value=f"data-arg-{i}"),
|
|
1618
|
+
ctx=ast.Store(),
|
|
1619
|
+
)
|
|
1620
|
+
],
|
|
1621
|
+
value=dump_call,
|
|
1622
|
+
)
|
|
1623
|
+
)
|
|
1624
|
+
else:
|
|
1625
|
+
# Multiple handlers - JSON format
|
|
1626
|
+
# _handlers_X = []
|
|
1627
|
+
handler_list_name = f"_handlers_{event_type}"
|
|
1628
|
+
body.append(
|
|
1629
|
+
ast.Assign(
|
|
1630
|
+
targets=[ast.Name(id=handler_list_name, ctx=ast.Store())],
|
|
1631
|
+
value=ast.List(elts=[], ctx=ast.Load()),
|
|
1632
|
+
)
|
|
1633
|
+
)
|
|
1634
|
+
|
|
1635
|
+
all_modifiers = set()
|
|
1636
|
+
for attr in attrs_list:
|
|
1637
|
+
modifiers = attr.modifiers or []
|
|
1638
|
+
all_modifiers.update(modifiers)
|
|
1639
|
+
|
|
1640
|
+
# _h = {"handler": "...", "modifiers": [...]}
|
|
1641
|
+
handler_dict = ast.Dict(
|
|
1642
|
+
keys=[
|
|
1643
|
+
ast.Constant(value="handler"),
|
|
1644
|
+
ast.Constant(value="modifiers"),
|
|
1645
|
+
],
|
|
1646
|
+
values=[
|
|
1647
|
+
ast.Constant(value=attr.handler_name),
|
|
1648
|
+
ast.List(
|
|
1649
|
+
elts=[ast.Constant(value=m) for m in modifiers],
|
|
1650
|
+
ctx=ast.Load(),
|
|
1651
|
+
),
|
|
1652
|
+
],
|
|
1653
|
+
)
|
|
1654
|
+
body.append(
|
|
1655
|
+
ast.Assign(
|
|
1656
|
+
targets=[ast.Name(id="_h", ctx=ast.Store())],
|
|
1657
|
+
value=handler_dict,
|
|
1658
|
+
)
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
if attr.args:
|
|
1662
|
+
# _args = [...]
|
|
1663
|
+
args_list = []
|
|
1664
|
+
for arg_expr in attr.args:
|
|
1665
|
+
val = self._transform_expr(
|
|
1666
|
+
arg_expr,
|
|
1667
|
+
local_vars,
|
|
1668
|
+
known_globals,
|
|
1669
|
+
line_offset=node.line,
|
|
1670
|
+
col_offset=node.column,
|
|
1671
|
+
)
|
|
1672
|
+
args_list.append(val)
|
|
1673
|
+
body.append(
|
|
1674
|
+
ast.Assign(
|
|
1675
|
+
targets=[
|
|
1676
|
+
ast.Subscript(
|
|
1677
|
+
value=ast.Name(id="_h", ctx=ast.Load()),
|
|
1678
|
+
slice=ast.Constant(value="args"),
|
|
1679
|
+
ctx=ast.Store(),
|
|
1680
|
+
)
|
|
1681
|
+
],
|
|
1682
|
+
value=ast.List(elts=args_list, ctx=ast.Load()),
|
|
1683
|
+
)
|
|
1684
|
+
)
|
|
1685
|
+
|
|
1686
|
+
# _handlers_X.append(_h)
|
|
1687
|
+
body.append(
|
|
1688
|
+
ast.Expr(
|
|
1689
|
+
value=ast.Call(
|
|
1690
|
+
func=ast.Attribute(
|
|
1691
|
+
value=ast.Name(
|
|
1692
|
+
id=handler_list_name, ctx=ast.Load()
|
|
1693
|
+
),
|
|
1694
|
+
attr="append",
|
|
1695
|
+
ctx=ast.Load(),
|
|
1696
|
+
),
|
|
1697
|
+
args=[ast.Name(id="_h", ctx=ast.Load())],
|
|
1698
|
+
keywords=[],
|
|
1699
|
+
)
|
|
1700
|
+
)
|
|
1701
|
+
)
|
|
1702
|
+
|
|
1703
|
+
# attrs["data-on-X"] = json.dumps(_handlers_X)
|
|
1704
|
+
body.append(
|
|
1705
|
+
ast.Assign(
|
|
1706
|
+
targets=[
|
|
1707
|
+
ast.Subscript(
|
|
1708
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1709
|
+
slice=ast.Constant(value=f"data-on-{event_type}"),
|
|
1710
|
+
ctx=ast.Store(),
|
|
1711
|
+
)
|
|
1712
|
+
],
|
|
1713
|
+
value=ast.Call(
|
|
1714
|
+
func=ast.Attribute(
|
|
1715
|
+
value=ast.Name(id="json", ctx=ast.Load()),
|
|
1716
|
+
attr="dumps",
|
|
1717
|
+
ctx=ast.Load(),
|
|
1718
|
+
),
|
|
1719
|
+
args=[ast.Name(id=handler_list_name, ctx=ast.Load())],
|
|
1720
|
+
keywords=[],
|
|
1721
|
+
),
|
|
1722
|
+
)
|
|
1723
|
+
)
|
|
1724
|
+
|
|
1725
|
+
if all_modifiers:
|
|
1726
|
+
modifiers_str = " ".join(all_modifiers)
|
|
1727
|
+
body.append(
|
|
1728
|
+
ast.Assign(
|
|
1729
|
+
targets=[
|
|
1730
|
+
ast.Subscript(
|
|
1731
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1732
|
+
slice=ast.Constant(
|
|
1733
|
+
value=f"data-modifiers-{event_type}"
|
|
1734
|
+
),
|
|
1735
|
+
ctx=ast.Store(),
|
|
1736
|
+
)
|
|
1737
|
+
],
|
|
1738
|
+
value=ast.Constant(value=modifiers_str),
|
|
1739
|
+
)
|
|
1740
|
+
)
|
|
1741
|
+
|
|
1742
|
+
# Handle other special attributes
|
|
1743
|
+
for attr in node.special_attributes:
|
|
1744
|
+
if isinstance(attr, EventAttribute):
|
|
1745
|
+
continue
|
|
1746
|
+
elif isinstance(attr, ReactiveAttribute):
|
|
1747
|
+
val_expr = self._transform_reactive_expr(
|
|
1748
|
+
attr.expr,
|
|
1749
|
+
local_vars,
|
|
1750
|
+
known_methods,
|
|
1751
|
+
known_globals,
|
|
1752
|
+
async_methods,
|
|
1753
|
+
line_offset=node.line,
|
|
1754
|
+
col_offset=node.column,
|
|
1755
|
+
)
|
|
1756
|
+
val_expr = self._wrap_unwrap_wire(val_expr)
|
|
1757
|
+
|
|
1758
|
+
# _r_val = val_expr
|
|
1759
|
+
body.append(
|
|
1760
|
+
ast.Assign(
|
|
1761
|
+
targets=[ast.Name(id="_r_val", ctx=ast.Store())],
|
|
1762
|
+
value=val_expr,
|
|
1763
|
+
)
|
|
1764
|
+
)
|
|
1765
|
+
|
|
1766
|
+
is_aria = attr.name.lower().startswith("aria-")
|
|
1767
|
+
|
|
1768
|
+
if is_aria:
|
|
1769
|
+
# if _r_val is True: attrs["X"] = "true"
|
|
1770
|
+
# elif _r_val is False: attrs["X"] = "false"
|
|
1771
|
+
# elif _r_val is not None: attrs["X"] = str(_r_val)
|
|
1772
|
+
|
|
1773
|
+
body.append(
|
|
1774
|
+
ast.If(
|
|
1775
|
+
test=ast.Compare(
|
|
1776
|
+
left=ast.Name(id="_r_val", ctx=ast.Load()),
|
|
1777
|
+
ops=[ast.Is()],
|
|
1778
|
+
comparators=[ast.Constant(value=True)],
|
|
1779
|
+
),
|
|
1780
|
+
body=[
|
|
1781
|
+
ast.Assign(
|
|
1782
|
+
targets=[
|
|
1783
|
+
ast.Subscript(
|
|
1784
|
+
value=ast.Name(
|
|
1785
|
+
id="attrs", ctx=ast.Load()
|
|
1786
|
+
),
|
|
1787
|
+
slice=ast.Constant(value=attr.name),
|
|
1788
|
+
ctx=ast.Store(),
|
|
1789
|
+
)
|
|
1790
|
+
],
|
|
1791
|
+
value=ast.Constant(value="true"),
|
|
1792
|
+
)
|
|
1793
|
+
],
|
|
1794
|
+
orelse=[
|
|
1795
|
+
ast.If(
|
|
1796
|
+
test=ast.Compare(
|
|
1797
|
+
left=ast.Name(id="_r_val", ctx=ast.Load()),
|
|
1798
|
+
ops=[ast.Is()],
|
|
1799
|
+
comparators=[ast.Constant(value=False)],
|
|
1800
|
+
),
|
|
1801
|
+
body=[
|
|
1802
|
+
ast.Assign(
|
|
1803
|
+
targets=[
|
|
1804
|
+
ast.Subscript(
|
|
1805
|
+
value=ast.Name(
|
|
1806
|
+
id="attrs", ctx=ast.Load()
|
|
1807
|
+
),
|
|
1808
|
+
slice=ast.Constant(
|
|
1809
|
+
value=attr.name
|
|
1810
|
+
),
|
|
1811
|
+
ctx=ast.Store(),
|
|
1812
|
+
)
|
|
1813
|
+
],
|
|
1814
|
+
value=ast.Constant(value="false"),
|
|
1815
|
+
)
|
|
1816
|
+
],
|
|
1817
|
+
orelse=[
|
|
1818
|
+
ast.If(
|
|
1819
|
+
test=ast.Compare(
|
|
1820
|
+
left=ast.Name(
|
|
1821
|
+
id="_r_val", ctx=ast.Load()
|
|
1822
|
+
),
|
|
1823
|
+
ops=[ast.IsNot()],
|
|
1824
|
+
comparators=[
|
|
1825
|
+
ast.Constant(value=None)
|
|
1826
|
+
],
|
|
1827
|
+
),
|
|
1828
|
+
body=[
|
|
1829
|
+
ast.Assign(
|
|
1830
|
+
targets=[
|
|
1831
|
+
ast.Subscript(
|
|
1832
|
+
value=ast.Name(
|
|
1833
|
+
id="attrs",
|
|
1834
|
+
ctx=ast.Load(),
|
|
1835
|
+
),
|
|
1836
|
+
slice=ast.Constant(
|
|
1837
|
+
value=attr.name
|
|
1838
|
+
),
|
|
1839
|
+
ctx=ast.Store(),
|
|
1840
|
+
)
|
|
1841
|
+
],
|
|
1842
|
+
value=ast.Call(
|
|
1843
|
+
func=ast.Name(
|
|
1844
|
+
id="str", ctx=ast.Load()
|
|
1845
|
+
),
|
|
1846
|
+
args=[
|
|
1847
|
+
ast.Name(
|
|
1848
|
+
id="_r_val",
|
|
1849
|
+
ctx=ast.Load(),
|
|
1850
|
+
)
|
|
1851
|
+
],
|
|
1852
|
+
keywords=[],
|
|
1853
|
+
),
|
|
1854
|
+
)
|
|
1855
|
+
],
|
|
1856
|
+
orelse=[],
|
|
1857
|
+
)
|
|
1858
|
+
],
|
|
1859
|
+
)
|
|
1860
|
+
],
|
|
1861
|
+
)
|
|
1862
|
+
)
|
|
1863
|
+
else:
|
|
1864
|
+
# Default bool behavior
|
|
1865
|
+
# if _r_val is True: attrs["X"] = ""
|
|
1866
|
+
# elif _r_val is not False and _r_val is not None: attrs["X"] = str(_r_val)
|
|
1867
|
+
|
|
1868
|
+
body.append(
|
|
1869
|
+
ast.If(
|
|
1870
|
+
test=ast.Compare(
|
|
1871
|
+
left=ast.Name(id="_r_val", ctx=ast.Load()),
|
|
1872
|
+
ops=[ast.Is()],
|
|
1873
|
+
comparators=[ast.Constant(value=True)],
|
|
1874
|
+
),
|
|
1875
|
+
body=[
|
|
1876
|
+
ast.Assign(
|
|
1877
|
+
targets=[
|
|
1878
|
+
ast.Subscript(
|
|
1879
|
+
value=ast.Name(
|
|
1880
|
+
id="attrs", ctx=ast.Load()
|
|
1881
|
+
),
|
|
1882
|
+
slice=ast.Constant(value=attr.name),
|
|
1883
|
+
ctx=ast.Store(),
|
|
1884
|
+
)
|
|
1885
|
+
],
|
|
1886
|
+
value=ast.Constant(value=""),
|
|
1887
|
+
)
|
|
1888
|
+
],
|
|
1889
|
+
orelse=[
|
|
1890
|
+
ast.If(
|
|
1891
|
+
test=ast.BoolOp(
|
|
1892
|
+
op=ast.And(),
|
|
1893
|
+
values=[
|
|
1894
|
+
ast.Compare(
|
|
1895
|
+
left=ast.Name(
|
|
1896
|
+
id="_r_val", ctx=ast.Load()
|
|
1897
|
+
),
|
|
1898
|
+
ops=[ast.IsNot()],
|
|
1899
|
+
comparators=[
|
|
1900
|
+
ast.Constant(value=False)
|
|
1901
|
+
],
|
|
1902
|
+
),
|
|
1903
|
+
ast.Compare(
|
|
1904
|
+
left=ast.Name(
|
|
1905
|
+
id="_r_val", ctx=ast.Load()
|
|
1906
|
+
),
|
|
1907
|
+
ops=[ast.IsNot()],
|
|
1908
|
+
comparators=[
|
|
1909
|
+
ast.Constant(value=None)
|
|
1910
|
+
],
|
|
1911
|
+
),
|
|
1912
|
+
],
|
|
1913
|
+
),
|
|
1914
|
+
body=[
|
|
1915
|
+
ast.Assign(
|
|
1916
|
+
targets=[
|
|
1917
|
+
ast.Subscript(
|
|
1918
|
+
value=ast.Name(
|
|
1919
|
+
id="attrs", ctx=ast.Load()
|
|
1920
|
+
),
|
|
1921
|
+
slice=ast.Constant(
|
|
1922
|
+
value=attr.name
|
|
1923
|
+
),
|
|
1924
|
+
ctx=ast.Store(),
|
|
1925
|
+
)
|
|
1926
|
+
],
|
|
1927
|
+
value=ast.Call(
|
|
1928
|
+
func=ast.Name(
|
|
1929
|
+
id="str", ctx=ast.Load()
|
|
1930
|
+
),
|
|
1931
|
+
args=[
|
|
1932
|
+
ast.Name(
|
|
1933
|
+
id="_r_val", ctx=ast.Load()
|
|
1934
|
+
)
|
|
1935
|
+
],
|
|
1936
|
+
keywords=[],
|
|
1937
|
+
),
|
|
1938
|
+
)
|
|
1939
|
+
],
|
|
1940
|
+
orelse=[],
|
|
1941
|
+
)
|
|
1942
|
+
],
|
|
1943
|
+
)
|
|
1944
|
+
)
|
|
1945
|
+
|
|
1946
|
+
if show_attr:
|
|
1947
|
+
cond = self._transform_expr(
|
|
1948
|
+
show_attr.condition,
|
|
1949
|
+
local_vars,
|
|
1950
|
+
known_globals,
|
|
1951
|
+
line_offset=node.line,
|
|
1952
|
+
col_offset=node.column,
|
|
1953
|
+
)
|
|
1954
|
+
# if not cond: attrs['style'] = ...
|
|
1955
|
+
body.append(
|
|
1956
|
+
ast.If(
|
|
1957
|
+
test=ast.UnaryOp(op=ast.Not(), operand=cond),
|
|
1958
|
+
body=[
|
|
1959
|
+
ast.Assign(
|
|
1960
|
+
targets=[
|
|
1961
|
+
ast.Subscript(
|
|
1962
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1963
|
+
slice=ast.Constant(value="style"),
|
|
1964
|
+
ctx=ast.Store(),
|
|
1965
|
+
)
|
|
1966
|
+
],
|
|
1967
|
+
value=ast.BinOp(
|
|
1968
|
+
left=ast.Call(
|
|
1969
|
+
func=ast.Attribute(
|
|
1970
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
1971
|
+
attr="get",
|
|
1972
|
+
ctx=ast.Load(),
|
|
1973
|
+
),
|
|
1974
|
+
args=[
|
|
1975
|
+
ast.Constant(value="style"),
|
|
1976
|
+
ast.Constant(value=""),
|
|
1977
|
+
],
|
|
1978
|
+
keywords=[],
|
|
1979
|
+
),
|
|
1980
|
+
op=ast.Add(),
|
|
1981
|
+
right=ast.Constant(value="; display: none"),
|
|
1982
|
+
),
|
|
1983
|
+
)
|
|
1984
|
+
],
|
|
1985
|
+
orelse=[],
|
|
1986
|
+
)
|
|
1987
|
+
)
|
|
1988
|
+
|
|
1989
|
+
if node.tag.lower() == "option" and bound_var:
|
|
1990
|
+
# if "value" in attrs and str(attrs["value"]) == str(bound_var):
|
|
1991
|
+
# attrs["selected"] = ""
|
|
1992
|
+
# bound_var is AST node here
|
|
1993
|
+
# We need to reuse bound_var AST node carefully (if it's complex,
|
|
1994
|
+
# it might be evaluated multiple times, but usually it's just
|
|
1995
|
+
# Name or Attribute)
|
|
1996
|
+
|
|
1997
|
+
check = ast.If(
|
|
1998
|
+
test=ast.BoolOp(
|
|
1999
|
+
op=ast.And(),
|
|
2000
|
+
values=[
|
|
2001
|
+
ast.Compare(
|
|
2002
|
+
left=ast.Constant(value="value"),
|
|
2003
|
+
ops=[ast.In()],
|
|
2004
|
+
comparators=[ast.Name(id="attrs", ctx=ast.Load())],
|
|
2005
|
+
),
|
|
2006
|
+
ast.Compare(
|
|
2007
|
+
left=ast.Call(
|
|
2008
|
+
func=ast.Name(id="str", ctx=ast.Load()),
|
|
2009
|
+
args=[
|
|
2010
|
+
ast.Subscript(
|
|
2011
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
2012
|
+
slice=ast.Constant(value="value"),
|
|
2013
|
+
ctx=ast.Load(),
|
|
2014
|
+
)
|
|
2015
|
+
],
|
|
2016
|
+
keywords=[],
|
|
2017
|
+
),
|
|
2018
|
+
ops=[ast.Eq()],
|
|
2019
|
+
comparators=[
|
|
2020
|
+
ast.Call(
|
|
2021
|
+
func=ast.Name(id="str", ctx=ast.Load()),
|
|
2022
|
+
args=[
|
|
2023
|
+
ast.Constant(value=bound_var)
|
|
2024
|
+
if isinstance(bound_var, str)
|
|
2025
|
+
else bound_var
|
|
2026
|
+
],
|
|
2027
|
+
keywords=[],
|
|
2028
|
+
)
|
|
2029
|
+
],
|
|
2030
|
+
),
|
|
2031
|
+
],
|
|
2032
|
+
),
|
|
2033
|
+
body=[
|
|
2034
|
+
ast.Assign(
|
|
2035
|
+
targets=[
|
|
2036
|
+
ast.Subscript(
|
|
2037
|
+
value=ast.Name(id="attrs", ctx=ast.Load()),
|
|
2038
|
+
slice=ast.Constant(value="selected"),
|
|
2039
|
+
ctx=ast.Store(),
|
|
2040
|
+
)
|
|
2041
|
+
],
|
|
2042
|
+
value=ast.Constant(value=""),
|
|
2043
|
+
)
|
|
2044
|
+
],
|
|
2045
|
+
orelse=[],
|
|
2046
|
+
)
|
|
2047
|
+
body.append(check)
|
|
2048
|
+
|
|
2049
|
+
# Generate opening tag
|
|
2050
|
+
# header_parts = [] ...
|
|
2051
|
+
# parts.append(f"<{tag}{''.join(header_parts)}>")
|
|
2052
|
+
|
|
2053
|
+
# Determine spread attributes (explicit or implicit)
|
|
2054
|
+
spread_expr = None
|
|
2055
|
+
|
|
2056
|
+
# 1. Explicit spread {**attrs}
|
|
2057
|
+
from pywire.compiler.ast_nodes import SpreadAttribute
|
|
2058
|
+
|
|
2059
|
+
explicit_spread = next(
|
|
2060
|
+
(a for a in node.special_attributes if isinstance(a, SpreadAttribute)),
|
|
2061
|
+
None,
|
|
2062
|
+
)
|
|
2063
|
+
if explicit_spread:
|
|
2064
|
+
# expr is likely 'attrs' or similar
|
|
2065
|
+
# transform it to AST load
|
|
2066
|
+
spread_expr = self._transform_expr(
|
|
2067
|
+
explicit_spread.expr,
|
|
2068
|
+
local_vars,
|
|
2069
|
+
known_globals,
|
|
2070
|
+
line_offset=node.line,
|
|
2071
|
+
col_offset=node.column,
|
|
2072
|
+
)
|
|
2073
|
+
|
|
2074
|
+
# 2. Implicit root injection
|
|
2075
|
+
# Only if no explicit spread AND implicit_root_source is active AND is an element
|
|
2076
|
+
elif implicit_root_source:
|
|
2077
|
+
spread_expr = ast.Attribute(
|
|
2078
|
+
value=ast.Name(id="self", ctx=ast.Load()),
|
|
2079
|
+
attr=implicit_root_source,
|
|
2080
|
+
ctx=ast.Load(),
|
|
2081
|
+
)
|
|
2082
|
+
implicit_root_source = None # Consumed
|
|
2083
|
+
|
|
2084
|
+
# Import render_attrs locally to ensure availability
|
|
2085
|
+
body.append(
|
|
2086
|
+
ast.ImportFrom(
|
|
2087
|
+
module="pywire.runtime.helpers",
|
|
2088
|
+
names=[ast.alias(name="render_attrs", asname=None)],
|
|
2089
|
+
level=0,
|
|
2090
|
+
)
|
|
2091
|
+
)
|
|
2092
|
+
|
|
2093
|
+
# Generate start tag
|
|
2094
|
+
body.append(
|
|
2095
|
+
ast.Expr(
|
|
2096
|
+
value=ast.Call(
|
|
2097
|
+
func=ast.Attribute(
|
|
2098
|
+
value=ast.Name(id=parts_var, ctx=ast.Load()),
|
|
2099
|
+
attr="append",
|
|
2100
|
+
ctx=ast.Load(),
|
|
2101
|
+
),
|
|
2102
|
+
args=[ast.Constant(value=f"<{node.tag}")],
|
|
2103
|
+
keywords=[],
|
|
2104
|
+
)
|
|
2105
|
+
)
|
|
2106
|
+
)
|
|
2107
|
+
|
|
2108
|
+
# render_attrs(attrs, spread_expr)
|
|
2109
|
+
# attrs is the runtime dict populated with static/dynamic bindings
|
|
2110
|
+
render_call = ast.Call(
|
|
2111
|
+
func=ast.Name(id="render_attrs", ctx=ast.Load()),
|
|
2112
|
+
args=[
|
|
2113
|
+
ast.Name(id="attrs", ctx=ast.Load()),
|
|
2114
|
+
spread_expr if spread_expr else ast.Constant(value=None),
|
|
2115
|
+
],
|
|
2116
|
+
keywords=[],
|
|
2117
|
+
)
|
|
2118
|
+
|
|
2119
|
+
body.append(
|
|
2120
|
+
ast.Expr(
|
|
2121
|
+
value=ast.Call(
|
|
2122
|
+
func=ast.Attribute(
|
|
2123
|
+
value=ast.Name(id=parts_var, ctx=ast.Load()),
|
|
2124
|
+
attr="append",
|
|
2125
|
+
ctx=ast.Load(),
|
|
2126
|
+
),
|
|
2127
|
+
args=[render_call],
|
|
2128
|
+
keywords=[],
|
|
2129
|
+
)
|
|
2130
|
+
)
|
|
2131
|
+
)
|
|
2132
|
+
|
|
2133
|
+
# Close opening tag
|
|
2134
|
+
body.append(
|
|
2135
|
+
ast.Expr(
|
|
2136
|
+
value=ast.Call(
|
|
2137
|
+
func=ast.Attribute(
|
|
2138
|
+
value=ast.Name(id=parts_var, ctx=ast.Load()),
|
|
2139
|
+
attr="append",
|
|
2140
|
+
ctx=ast.Load(),
|
|
2141
|
+
),
|
|
2142
|
+
args=[ast.Constant(value=">")],
|
|
2143
|
+
keywords=[],
|
|
2144
|
+
)
|
|
2145
|
+
)
|
|
2146
|
+
)
|
|
2147
|
+
|
|
2148
|
+
for child in node.children:
|
|
2149
|
+
self._add_node(
|
|
2150
|
+
child,
|
|
2151
|
+
body,
|
|
2152
|
+
local_vars,
|
|
2153
|
+
new_bound_var,
|
|
2154
|
+
layout_id,
|
|
2155
|
+
known_methods,
|
|
2156
|
+
known_globals,
|
|
2157
|
+
async_methods,
|
|
2158
|
+
component_map,
|
|
2159
|
+
scope_id,
|
|
2160
|
+
parts_var=parts_var,
|
|
2161
|
+
implicit_root_source=implicit_root_source,
|
|
2162
|
+
enable_regions=enable_regions,
|
|
2163
|
+
)
|
|
2164
|
+
|
|
2165
|
+
if node.tag.lower() not in self.VOID_ELEMENTS:
|
|
2166
|
+
body.append(
|
|
2167
|
+
ast.Expr(
|
|
2168
|
+
value=ast.Call(
|
|
2169
|
+
func=ast.Attribute(
|
|
2170
|
+
value=ast.Name(id=parts_var, ctx=ast.Load()),
|
|
2171
|
+
attr="append",
|
|
2172
|
+
ctx=ast.Load(),
|
|
2173
|
+
),
|
|
2174
|
+
args=[ast.Constant(value=f"</{node.tag}>")],
|
|
2175
|
+
keywords=[],
|
|
2176
|
+
)
|
|
2177
|
+
)
|
|
2178
|
+
)
|