pulse-framework 0.1.50__py3-none-any.whl → 0.1.52__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.
- pulse/__init__.py +542 -562
- pulse/_examples.py +29 -0
- pulse/app.py +0 -14
- pulse/cli/cmd.py +96 -80
- pulse/cli/dependencies.py +10 -41
- pulse/cli/folder_lock.py +3 -3
- pulse/cli/helpers.py +40 -67
- pulse/cli/logging.py +102 -0
- pulse/cli/packages.py +16 -0
- pulse/cli/processes.py +40 -23
- pulse/codegen/codegen.py +70 -35
- pulse/codegen/js.py +2 -4
- pulse/codegen/templates/route.py +94 -146
- pulse/component.py +115 -0
- pulse/components/for_.py +1 -1
- pulse/components/if_.py +1 -1
- pulse/components/react_router.py +16 -22
- pulse/{html → dom}/events.py +1 -1
- pulse/{html → dom}/props.py +6 -6
- pulse/{html → dom}/tags.py +11 -11
- pulse/dom/tags.pyi +480 -0
- pulse/form.py +7 -6
- pulse/hooks/init.py +1 -13
- pulse/js/__init__.py +37 -41
- pulse/js/__init__.pyi +22 -2
- pulse/js/_types.py +5 -3
- pulse/js/array.py +121 -38
- pulse/js/console.py +9 -9
- pulse/js/date.py +22 -19
- pulse/js/document.py +8 -4
- pulse/js/error.py +12 -14
- pulse/js/json.py +4 -3
- pulse/js/map.py +17 -7
- pulse/js/math.py +2 -2
- pulse/js/navigator.py +4 -4
- pulse/js/number.py +8 -8
- pulse/js/object.py +9 -13
- pulse/js/promise.py +25 -9
- pulse/js/regexp.py +6 -6
- pulse/js/set.py +20 -8
- pulse/js/string.py +7 -7
- pulse/js/weakmap.py +6 -6
- pulse/js/weakset.py +6 -6
- pulse/js/window.py +17 -14
- pulse/messages.py +1 -4
- pulse/react_component.py +3 -999
- pulse/render_session.py +74 -66
- pulse/renderer.py +311 -238
- pulse/routing.py +1 -10
- pulse/serializer.py +11 -1
- pulse/transpiler/__init__.py +84 -114
- pulse/transpiler/builtins.py +661 -343
- pulse/transpiler/errors.py +78 -2
- pulse/transpiler/function.py +463 -133
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +230 -325
- pulse/transpiler/js_module.py +218 -209
- pulse/transpiler/modules/__init__.py +16 -13
- pulse/transpiler/modules/asyncio.py +45 -26
- pulse/transpiler/modules/json.py +12 -8
- pulse/transpiler/modules/math.py +161 -216
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +231 -0
- pulse/transpiler/modules/typing.py +33 -28
- pulse/transpiler/nodes.py +1607 -923
- pulse/transpiler/py_module.py +118 -95
- pulse/transpiler/react_component.py +51 -0
- pulse/transpiler/transpiler.py +593 -437
- pulse/transpiler/vdom.py +255 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/METADATA +1 -1
- pulse_framework-0.1.52.dist-info/RECORD +120 -0
- pulse/html/tags.pyi +0 -470
- pulse/transpiler/constants.py +0 -110
- pulse/transpiler/context.py +0 -26
- pulse/transpiler/ids.py +0 -16
- pulse/transpiler/modules/re.py +0 -466
- pulse/transpiler/modules/tags.py +0 -268
- pulse/transpiler/utils.py +0 -4
- pulse/vdom.py +0 -667
- pulse_framework-0.1.50.dist-info/RECORD +0 -119
- /pulse/{html → dom}/__init__.py +0 -0
- /pulse/{html → dom}/elements.py +0 -0
- /pulse/{html → dom}/svg.py +0 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
pulse/transpiler/nodes.py
CHANGED
|
@@ -1,1216 +1,1900 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import string
|
|
6
|
+
import warnings
|
|
4
7
|
from abc import ABC, abstractmethod
|
|
5
|
-
from collections.abc import Callable, Sequence
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from inspect import isfunction, signature
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Any,
|
|
14
|
+
Generic,
|
|
15
|
+
Protocol,
|
|
16
|
+
TypeAlias,
|
|
17
|
+
TypeVar,
|
|
18
|
+
cast,
|
|
19
|
+
overload,
|
|
20
|
+
override,
|
|
21
|
+
)
|
|
22
|
+
from typing import Literal as Lit
|
|
23
|
+
|
|
24
|
+
from pulse.env import env
|
|
25
|
+
from pulse.transpiler.errors import TranspileError
|
|
26
|
+
from pulse.transpiler.vdom import VDOMNode
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from pulse.transpiler.transpiler import Transpiler
|
|
30
|
+
|
|
31
|
+
_T = TypeVar("_T")
|
|
32
|
+
Primitive: TypeAlias = bool | int | float | str | dt.datetime | None
|
|
33
|
+
|
|
34
|
+
_JS_IDENTIFIER_START = set(string.ascii_letters + "_")
|
|
35
|
+
_JS_IDENTIFIER_CONTINUE = set(string.ascii_letters + string.digits + "_")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def to_js_identifier(name: str) -> str:
|
|
39
|
+
"""Normalize a string to a JS-compatible identifier."""
|
|
40
|
+
if not name:
|
|
41
|
+
return "_"
|
|
42
|
+
out: list[str] = []
|
|
43
|
+
for ch in name:
|
|
44
|
+
out.append(ch if ch in _JS_IDENTIFIER_CONTINUE else "_")
|
|
45
|
+
if not out or out[0] not in _JS_IDENTIFIER_START:
|
|
46
|
+
out.insert(0, "_")
|
|
47
|
+
return "".join(out)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =============================================================================
|
|
51
|
+
# Global registries
|
|
52
|
+
# =============================================================================
|
|
53
|
+
|
|
54
|
+
# Global registry: id(value) -> Expr
|
|
55
|
+
# Used by Expr.of() to resolve registered Python values
|
|
56
|
+
EXPR_REGISTRY: dict[int, "Expr"] = {}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Base classes
|
|
61
|
+
# =============================================================================
|
|
62
|
+
class Expr(ABC):
|
|
63
|
+
"""Base class for expression nodes.
|
|
64
|
+
|
|
65
|
+
Provides hooks for custom transpilation behavior:
|
|
66
|
+
- transpile_call: customize behavior when called as a function
|
|
67
|
+
- transpile_getattr: customize attribute access
|
|
68
|
+
- transpile_subscript: customize subscript access
|
|
69
|
+
|
|
70
|
+
And serialization for client-side rendering:
|
|
71
|
+
- render: serialize to dict for client renderer (stub for now)
|
|
72
|
+
"""
|
|
23
73
|
|
|
24
|
-
|
|
25
|
-
ast.UAdd: "+",
|
|
26
|
-
ast.USub: "-",
|
|
27
|
-
ast.Not: "!",
|
|
28
|
-
}
|
|
74
|
+
__slots__: tuple[str, ...] = ()
|
|
29
75
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
ast.Lt: "<",
|
|
34
|
-
ast.LtE: "<=",
|
|
35
|
-
ast.Gt: ">",
|
|
36
|
-
ast.GtE: ">=",
|
|
37
|
-
}
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def emit(self, out: list[str]) -> None:
|
|
78
|
+
"""Emit this expression as JavaScript/JSX code into the output buffer."""
|
|
38
79
|
|
|
80
|
+
def precedence(self) -> int:
|
|
81
|
+
"""Operator precedence (higher = binds tighter). Default: primary (20)."""
|
|
82
|
+
return 20
|
|
39
83
|
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
|
|
84
|
+
# -------------------------------------------------------------------------
|
|
85
|
+
# Transpilation hooks (override to customize behavior)
|
|
86
|
+
# -------------------------------------------------------------------------
|
|
43
87
|
|
|
88
|
+
def transpile_call(
|
|
89
|
+
self,
|
|
90
|
+
args: list[ast.expr],
|
|
91
|
+
kwargs: dict[str, ast.expr],
|
|
92
|
+
ctx: Transpiler,
|
|
93
|
+
) -> Expr:
|
|
94
|
+
"""Called when this expression is used as a function: expr(args).
|
|
44
95
|
|
|
45
|
-
|
|
46
|
-
|
|
96
|
+
Override to customize call behavior.
|
|
97
|
+
Default raises - most expressions are not callable.
|
|
47
98
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
99
|
+
Args and kwargs are raw Python `ast.expr` nodes (not yet transpiled).
|
|
100
|
+
Use ctx.emit_expr() to convert them to Expr as needed.
|
|
101
|
+
"""
|
|
102
|
+
if kwargs:
|
|
103
|
+
raise TranspileError("Keyword arguments not yet supported in v2 transpiler")
|
|
104
|
+
return Call(self, [ctx.emit_expr(a) for a in args])
|
|
51
105
|
|
|
106
|
+
def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
|
|
107
|
+
"""Called when an attribute is accessed: expr.attr.
|
|
52
108
|
|
|
53
|
-
|
|
54
|
-
|
|
109
|
+
Override to customize attribute access.
|
|
110
|
+
Default returns Member(self, attr).
|
|
111
|
+
"""
|
|
112
|
+
return Member(self, attr)
|
|
55
113
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
This enables extensibility for things like JSX elements.
|
|
59
|
-
"""
|
|
114
|
+
def transpile_subscript(self, key: ast.expr, ctx: Transpiler) -> Expr:
|
|
115
|
+
"""Called when subscripted: expr[key].
|
|
60
116
|
|
|
61
|
-
|
|
117
|
+
Override to customize subscript behavior.
|
|
118
|
+
Default returns Subscript(self, emitted_key).
|
|
119
|
+
"""
|
|
120
|
+
if isinstance(key, ast.Tuple):
|
|
121
|
+
raise TranspileError(
|
|
122
|
+
"Multiple indices not supported in subscript", node=key
|
|
123
|
+
)
|
|
124
|
+
return Subscript(self, ctx.emit_expr(key))
|
|
62
125
|
|
|
63
|
-
#
|
|
64
|
-
|
|
126
|
+
# -------------------------------------------------------------------------
|
|
127
|
+
# Serialization for client-side rendering
|
|
128
|
+
# -------------------------------------------------------------------------
|
|
65
129
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def render(self) -> VDOMNode:
|
|
132
|
+
"""Serialize this expression node for client-side rendering.
|
|
69
133
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"""Convert a Python value to a JSExpr.
|
|
134
|
+
Returns a VDOMNode (primitive or dict) that can be JSON-serialized and
|
|
135
|
+
evaluated on the client. Override in each concrete Expr subclass.
|
|
73
136
|
|
|
74
|
-
|
|
75
|
-
1. Already a JSExpr: returned as-is
|
|
76
|
-
2. Registered in JSEXPR_REGISTRY: return the registered expr
|
|
77
|
-
3. Primitives: str->JSString, int/float->JSNumber, bool->JSBoolean, None->JSNull
|
|
78
|
-
4. Collections: list/tuple->JSArray, dict->JSObjectExpr (recursively converted)
|
|
137
|
+
Raises TypeError for nodes that cannot be serialized (e.g., Transformer).
|
|
79
138
|
"""
|
|
80
|
-
# Already a JSExpr
|
|
81
|
-
if isinstance(value, JSExpr):
|
|
82
|
-
return value
|
|
83
139
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
140
|
+
# -------------------------------------------------------------------------
|
|
141
|
+
# Python dunder methods for natural syntax in @javascript functions
|
|
142
|
+
# These return Expr nodes that represent the operations at transpile time.
|
|
143
|
+
# -------------------------------------------------------------------------
|
|
87
144
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return JSString(value)
|
|
91
|
-
if isinstance(
|
|
92
|
-
value, bool
|
|
93
|
-
): # Must check before int since bool is subclass of int
|
|
94
|
-
return JSBoolean(value)
|
|
95
|
-
if isinstance(value, (int, float)):
|
|
96
|
-
return JSNumber(value)
|
|
97
|
-
if value is None:
|
|
98
|
-
return JSNull()
|
|
145
|
+
def __call__(self, *args: object, **kwargs: object) -> "Call":
|
|
146
|
+
"""Allow calling Expr objects in Python code.
|
|
99
147
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
props = [JSProp(JSString(str(k)), cls.of(v)) for k, v in value.items()] # pyright: ignore[reportUnknownArgumentType]
|
|
105
|
-
return JSObjectExpr(props)
|
|
148
|
+
Returns a Call expression. Subclasses may override to return more
|
|
149
|
+
specific types (e.g., Element for JSX components).
|
|
150
|
+
"""
|
|
151
|
+
return Call(self, [Expr.of(a) for a in args])
|
|
106
152
|
|
|
107
|
-
|
|
153
|
+
def __getitem__(self, key: object) -> "Subscript":
|
|
154
|
+
"""Allow subscript access on Expr objects in Python code.
|
|
108
155
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
156
|
+
Returns a Subscript expression for type checking.
|
|
157
|
+
"""
|
|
158
|
+
return Subscript(self, Expr.of(key))
|
|
112
159
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
160
|
+
def __getattr__(self, attr: str) -> "Member":
|
|
161
|
+
"""Allow attribute access on Expr objects in Python code.
|
|
162
|
+
|
|
163
|
+
Returns a Member expression for type checking.
|
|
116
164
|
"""
|
|
117
|
-
|
|
118
|
-
expr = JSTransformer(expr)
|
|
119
|
-
JSEXPR_REGISTRY[id(value)] = expr
|
|
165
|
+
return Member(self, attr)
|
|
120
166
|
|
|
121
|
-
def
|
|
122
|
-
"""
|
|
167
|
+
def __add__(self, other: object) -> "Binary":
|
|
168
|
+
"""Allow + operator on Expr objects."""
|
|
169
|
+
return Binary(self, "+", Expr.of(other))
|
|
123
170
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
171
|
+
def __sub__(self, other: object) -> "Binary":
|
|
172
|
+
"""Allow - operator on Expr objects."""
|
|
173
|
+
return Binary(self, "-", Expr.of(other))
|
|
127
174
|
|
|
128
|
-
|
|
175
|
+
def __mul__(self, other: object) -> "Binary":
|
|
176
|
+
"""Allow * operator on Expr objects."""
|
|
177
|
+
return Binary(self, "*", Expr.of(other))
|
|
129
178
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
179
|
+
def __truediv__(self, other: object) -> "Binary":
|
|
180
|
+
"""Allow / operator on Expr objects."""
|
|
181
|
+
return Binary(self, "/", Expr.of(other))
|
|
133
182
|
|
|
134
|
-
|
|
135
|
-
"""
|
|
136
|
-
|
|
137
|
-
raise JSCompilationError(
|
|
138
|
-
"Keyword arguments not supported in default function call"
|
|
139
|
-
)
|
|
140
|
-
return JSCall(self, [JSExpr.of(a) for a in args])
|
|
183
|
+
def __mod__(self, other: object) -> "Binary":
|
|
184
|
+
"""Allow % operator on Expr objects."""
|
|
185
|
+
return Binary(self, "%", Expr.of(other))
|
|
141
186
|
|
|
142
|
-
def
|
|
143
|
-
"""
|
|
187
|
+
def __and__(self, other: object) -> "Binary":
|
|
188
|
+
"""Allow & operator on Expr objects (maps to &&)."""
|
|
189
|
+
return Binary(self, "&&", Expr.of(other))
|
|
144
190
|
|
|
145
|
-
|
|
146
|
-
|
|
191
|
+
def __or__(self, other: object) -> "Binary":
|
|
192
|
+
"""Allow | operator on Expr objects (maps to ||)."""
|
|
193
|
+
return Binary(self, "||", Expr.of(other))
|
|
147
194
|
|
|
148
|
-
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
raise JSCompilationError("Multiple indices not supported in subscript")
|
|
152
|
-
return JSSubscript(self, JSExpr.of(indices[0]))
|
|
195
|
+
def __neg__(self) -> "Unary":
|
|
196
|
+
"""Allow unary - operator on Expr objects."""
|
|
197
|
+
return Unary("-", self)
|
|
153
198
|
|
|
154
|
-
def
|
|
155
|
-
"""
|
|
199
|
+
def __pos__(self) -> "Unary":
|
|
200
|
+
"""Allow unary + operator on Expr objects."""
|
|
201
|
+
return Unary("+", self)
|
|
156
202
|
|
|
157
|
-
|
|
158
|
-
"""
|
|
159
|
-
return
|
|
203
|
+
def __invert__(self) -> "Unary":
|
|
204
|
+
"""Allow ~ operator on Expr objects (maps to !)."""
|
|
205
|
+
return Unary("!", self)
|
|
160
206
|
|
|
161
|
-
|
|
162
|
-
|
|
207
|
+
# -------------------------------------------------------------------------
|
|
208
|
+
# Type casting and wrapper methods
|
|
209
|
+
# -------------------------------------------------------------------------
|
|
163
210
|
|
|
164
|
-
|
|
165
|
-
|
|
211
|
+
def as_(self, typ_: "_T | type[_T]") -> "_T":
|
|
212
|
+
"""Cast this expression to a type or use as a decorator.
|
|
213
|
+
|
|
214
|
+
Usage as decorator:
|
|
215
|
+
@Import(...).as_
|
|
216
|
+
def fn(): ...
|
|
217
|
+
|
|
218
|
+
Usage for type casting:
|
|
219
|
+
clsx = Import(...).as_(Callable[[str, ...], str])
|
|
220
|
+
|
|
221
|
+
If typ_ is a user-defined callable (function or lambda),
|
|
222
|
+
wraps the expression in a Signature node that stores the callable's
|
|
223
|
+
signature for type introspection.
|
|
166
224
|
"""
|
|
167
|
-
|
|
225
|
+
# Only wrap for user-defined functions (lambdas, def functions)
|
|
226
|
+
# Skip for types (str, int, etc.) used as type annotations
|
|
227
|
+
if isfunction(typ_):
|
|
228
|
+
try:
|
|
229
|
+
sig = signature(typ_)
|
|
230
|
+
return cast("_T", Signature(self, sig))
|
|
231
|
+
except (ValueError, TypeError):
|
|
232
|
+
# Signature not available (e.g., for built-ins), return self
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
return cast("_T", self)
|
|
168
236
|
|
|
169
|
-
def
|
|
170
|
-
"""
|
|
237
|
+
def jsx(self) -> "Jsx":
|
|
238
|
+
"""Wrap this expression as a JSX component.
|
|
171
239
|
|
|
172
|
-
|
|
173
|
-
Delegates to emit_call for transpilation.
|
|
240
|
+
When called in transpiled code, produces Element(tag=self, ...).
|
|
174
241
|
"""
|
|
175
|
-
return self
|
|
242
|
+
return Jsx(self)
|
|
176
243
|
|
|
177
|
-
|
|
178
|
-
|
|
244
|
+
# -------------------------------------------------------------------------
|
|
245
|
+
# Registry for Python value -> Expr mapping
|
|
246
|
+
# -------------------------------------------------------------------------
|
|
179
247
|
|
|
180
|
-
|
|
181
|
-
|
|
248
|
+
@staticmethod
|
|
249
|
+
def of(value: Any) -> Expr:
|
|
250
|
+
"""Convert a Python value to an Expr.
|
|
251
|
+
|
|
252
|
+
Resolution order:
|
|
253
|
+
1. Already an Expr: returned as-is
|
|
254
|
+
2. Registered in EXPR_REGISTRY: return the registered expr
|
|
255
|
+
3. Primitives: str/int/float -> Literal, bool -> Literal, None -> Literal(None)
|
|
256
|
+
4. Collections: list/tuple -> Array, dict -> Object (recursively converted)
|
|
257
|
+
5. set -> Call(Identifier("Set"), [Array(...)])
|
|
258
|
+
|
|
259
|
+
Raises TypeError for unconvertible values.
|
|
182
260
|
"""
|
|
183
|
-
|
|
261
|
+
# Already an Expr
|
|
262
|
+
if isinstance(value, Expr):
|
|
263
|
+
return value
|
|
184
264
|
|
|
265
|
+
# Check registry (for modules, functions, etc.)
|
|
266
|
+
if (expr := EXPR_REGISTRY.get(id(value))) is not None:
|
|
267
|
+
return expr
|
|
185
268
|
|
|
186
|
-
|
|
187
|
-
|
|
269
|
+
# Primitives - must check bool before int since bool is subclass of int
|
|
270
|
+
if isinstance(value, bool):
|
|
271
|
+
return Literal(value)
|
|
272
|
+
if isinstance(value, (int, float, str)):
|
|
273
|
+
return Literal(value)
|
|
274
|
+
if value is None:
|
|
275
|
+
return Literal(None)
|
|
188
276
|
|
|
277
|
+
# Collections
|
|
278
|
+
if isinstance(value, (list, tuple)):
|
|
279
|
+
return Array([Expr.of(v) for v in value])
|
|
280
|
+
if isinstance(value, dict):
|
|
281
|
+
props = [(str(k), Expr.of(v)) for k, v in value.items()] # pyright: ignore[reportUnknownArgumentType]
|
|
282
|
+
return Object(props)
|
|
283
|
+
if isinstance(value, set):
|
|
284
|
+
# new Set([...])
|
|
285
|
+
return New(Identifier("Set"), [Array([Expr.of(v) for v in value])])
|
|
189
286
|
|
|
190
|
-
|
|
191
|
-
__slots__ = ("name",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
192
|
-
is_primary: ClassVar[bool] = True
|
|
193
|
-
name: str
|
|
287
|
+
raise TypeError(f"Cannot convert {type(value).__name__} to Expr")
|
|
194
288
|
|
|
195
|
-
|
|
196
|
-
|
|
289
|
+
@staticmethod
|
|
290
|
+
def register(value: Any, expr: Expr | Callable[..., Expr]) -> None:
|
|
291
|
+
"""Register a Python value for conversion via Expr.of().
|
|
197
292
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
293
|
+
Args:
|
|
294
|
+
value: The Python object to register (function, constant, etc.)
|
|
295
|
+
expr: Either an Expr or a Callable[..., Expr] (will be wrapped in Transformer)
|
|
296
|
+
"""
|
|
297
|
+
if callable(expr) and not isinstance(expr, Expr):
|
|
298
|
+
expr = Transformer(expr)
|
|
299
|
+
EXPR_REGISTRY[id(value)] = expr
|
|
201
300
|
|
|
202
301
|
|
|
203
|
-
class
|
|
204
|
-
|
|
205
|
-
is_primary: ClassVar[bool] = True
|
|
206
|
-
value: str
|
|
302
|
+
class Stmt(ABC):
|
|
303
|
+
"""Base class for statement nodes."""
|
|
207
304
|
|
|
208
|
-
|
|
209
|
-
self.value = value
|
|
305
|
+
__slots__: tuple[str, ...] = ()
|
|
210
306
|
|
|
211
|
-
@
|
|
212
|
-
def emit(self) ->
|
|
213
|
-
|
|
214
|
-
# Escape for double-quoted JS string literals
|
|
215
|
-
s = (
|
|
216
|
-
s.replace("\\", "\\\\")
|
|
217
|
-
.replace('"', '\\"')
|
|
218
|
-
.replace("\n", "\\n")
|
|
219
|
-
.replace("\r", "\\r")
|
|
220
|
-
.replace("\t", "\\t")
|
|
221
|
-
.replace("\b", "\\b")
|
|
222
|
-
.replace("\f", "\\f")
|
|
223
|
-
.replace("\v", "\\v")
|
|
224
|
-
.replace("\x00", "\\x00")
|
|
225
|
-
.replace("\u2028", "\\u2028")
|
|
226
|
-
.replace("\u2029", "\\u2029")
|
|
227
|
-
)
|
|
228
|
-
return f'"{s}"'
|
|
307
|
+
@abstractmethod
|
|
308
|
+
def emit(self, out: list[str]) -> None:
|
|
309
|
+
"""Emit this statement as JavaScript code into the output buffer."""
|
|
229
310
|
|
|
230
311
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
value: int | float
|
|
312
|
+
# =============================================================================
|
|
313
|
+
# Data Nodes
|
|
314
|
+
# =============================================================================
|
|
235
315
|
|
|
236
|
-
def __init__(self, value: int | float):
|
|
237
|
-
self.value = value
|
|
238
316
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
317
|
+
class ExprWrapper(Expr):
|
|
318
|
+
"""Base class for Expr wrappers that delegate to an underlying expression.
|
|
319
|
+
|
|
320
|
+
Subclasses must define an `expr` attribute (via __slots__ or dataclass).
|
|
321
|
+
All Expr methods delegate to self.expr by default. Override specific
|
|
322
|
+
methods to customize behavior.
|
|
323
|
+
"""
|
|
242
324
|
|
|
325
|
+
__slots__: tuple[str, ...] = ("expr",)
|
|
326
|
+
expr: Expr
|
|
243
327
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
value: bool
|
|
328
|
+
@override
|
|
329
|
+
def emit(self, out: list[str]) -> None:
|
|
330
|
+
self.expr.emit(out)
|
|
248
331
|
|
|
249
|
-
|
|
250
|
-
|
|
332
|
+
@override
|
|
333
|
+
def render(self) -> VDOMNode:
|
|
334
|
+
return self.expr.render()
|
|
251
335
|
|
|
252
336
|
@override
|
|
253
|
-
def
|
|
254
|
-
return
|
|
337
|
+
def precedence(self) -> int:
|
|
338
|
+
return self.expr.precedence()
|
|
255
339
|
|
|
340
|
+
@override
|
|
341
|
+
def transpile_call(
|
|
342
|
+
self,
|
|
343
|
+
args: list[ast.expr],
|
|
344
|
+
kwargs: dict[str, ast.expr],
|
|
345
|
+
ctx: Transpiler,
|
|
346
|
+
) -> Expr:
|
|
347
|
+
return self.expr.transpile_call(args, kwargs, ctx)
|
|
256
348
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
349
|
+
@override
|
|
350
|
+
def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
|
|
351
|
+
return self.expr.transpile_getattr(attr, ctx)
|
|
260
352
|
|
|
261
353
|
@override
|
|
262
|
-
def
|
|
263
|
-
return
|
|
354
|
+
def transpile_subscript(self, key: ast.expr, ctx: Transpiler) -> Expr:
|
|
355
|
+
return self.expr.transpile_subscript(key, ctx)
|
|
264
356
|
|
|
357
|
+
@override
|
|
358
|
+
def __call__(self, *args: object, **kwargs: object) -> Expr: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
359
|
+
return self.expr(*args, **kwargs)
|
|
265
360
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
361
|
+
@override
|
|
362
|
+
def __getitem__(self, key: object) -> Expr: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
363
|
+
return self.expr[key]
|
|
269
364
|
|
|
270
365
|
@override
|
|
271
|
-
def
|
|
272
|
-
return
|
|
366
|
+
def __getattr__(self, attr: str) -> Expr: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
367
|
+
return getattr(self.expr, attr)
|
|
273
368
|
|
|
274
369
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
elements: Sequence[JSExpr]
|
|
370
|
+
@dataclass(slots=True, init=False)
|
|
371
|
+
class Jsx(ExprWrapper):
|
|
372
|
+
"""JSX wrapper that makes any Expr callable as a component.
|
|
279
373
|
|
|
280
|
-
|
|
281
|
-
|
|
374
|
+
When called in transpiled code, produces Element(tag=expr, ...).
|
|
375
|
+
This enables patterns like `Jsx(Member(AppShell, "Header"))` to emit
|
|
376
|
+
`<AppShell.Header ... />`.
|
|
282
377
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
378
|
+
Example:
|
|
379
|
+
app_shell = Import("AppShell", "@mantine/core")
|
|
380
|
+
Header = Jsx(Member(app_shell, "Header"))
|
|
381
|
+
# In @javascript:
|
|
382
|
+
# Header(height=60) -> <AppShell_1.Header height={60} />
|
|
383
|
+
"""
|
|
287
384
|
|
|
385
|
+
expr: Expr
|
|
386
|
+
id: str
|
|
288
387
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
expr: JSExpr
|
|
388
|
+
def __init__(self, expr: Expr) -> None:
|
|
389
|
+
from pulse.transpiler.id import next_id
|
|
292
390
|
|
|
293
|
-
def __init__(self, expr: JSExpr):
|
|
294
391
|
self.expr = expr
|
|
392
|
+
self.id = next_id()
|
|
295
393
|
|
|
296
394
|
@override
|
|
297
|
-
def
|
|
298
|
-
|
|
395
|
+
def transpile_call(
|
|
396
|
+
self,
|
|
397
|
+
args: list[ast.expr],
|
|
398
|
+
kwargs: dict[str, ast.expr],
|
|
399
|
+
ctx: "Transpiler",
|
|
400
|
+
) -> Expr:
|
|
401
|
+
"""Transpile a call to this JSX wrapper into an Element.
|
|
402
|
+
|
|
403
|
+
Positional args become children, keyword args become props.
|
|
404
|
+
The `key` kwarg is extracted specially.
|
|
405
|
+
"""
|
|
406
|
+
children: list[Node] = [ctx.emit_expr(a) for a in args]
|
|
407
|
+
|
|
408
|
+
props: dict[str, Prop] = {}
|
|
409
|
+
key: str | Expr | None = None
|
|
410
|
+
for k, v in kwargs.items():
|
|
411
|
+
v = ctx.emit_expr(v)
|
|
412
|
+
if k == "key":
|
|
413
|
+
# Accept any expression as key for transpilation
|
|
414
|
+
if isinstance(v, Literal) and isinstance(v.value, str):
|
|
415
|
+
key = v.value # Optimize string literals
|
|
416
|
+
else:
|
|
417
|
+
key = v # Keep as expression
|
|
418
|
+
else:
|
|
419
|
+
props[k] = v
|
|
299
420
|
|
|
421
|
+
return Element(
|
|
422
|
+
tag=self.expr,
|
|
423
|
+
props=props if props else None,
|
|
424
|
+
children=children if children else None,
|
|
425
|
+
key=key,
|
|
426
|
+
)
|
|
300
427
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
value: JSExpr
|
|
428
|
+
@override
|
|
429
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Element:
|
|
430
|
+
"""Allow calling Jsx in Python code.
|
|
305
431
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
432
|
+
Supports two usage patterns:
|
|
433
|
+
1. Decorator: @Jsx(expr) def Component(...): ...
|
|
434
|
+
2. Call: Jsx(expr)(props, children) -> Element
|
|
435
|
+
"""
|
|
309
436
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
437
|
+
# Normal call: build Element
|
|
438
|
+
props: dict[str, object] = {}
|
|
439
|
+
key: str | None = None
|
|
440
|
+
children: list[Node] = list(args)
|
|
313
441
|
|
|
442
|
+
for k, v in kwargs.items():
|
|
443
|
+
if k == "key":
|
|
444
|
+
if not isinstance(v, str):
|
|
445
|
+
raise ValueError("key must be a string")
|
|
446
|
+
key = v
|
|
447
|
+
else:
|
|
448
|
+
props[k] = v
|
|
314
449
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
450
|
+
return Element(
|
|
451
|
+
tag=self.expr,
|
|
452
|
+
props=props if props else None,
|
|
453
|
+
children=children if children else None,
|
|
454
|
+
key=key,
|
|
455
|
+
)
|
|
319
456
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
457
|
+
|
|
458
|
+
@dataclass(slots=True)
|
|
459
|
+
class Signature(ExprWrapper):
|
|
460
|
+
"""Wraps an Expr with signature information for type checking.
|
|
461
|
+
|
|
462
|
+
When you call expr.as_(callable_type), this creates a Signature wrapper
|
|
463
|
+
that stores the callable's signature for introspection, while delegating
|
|
464
|
+
all other behavior to the wrapped expression.
|
|
465
|
+
|
|
466
|
+
Example:
|
|
467
|
+
button = Import("Button", "@mantine/core")
|
|
468
|
+
typed_button = Signature(button, signature_of_callable)
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
expr: Expr
|
|
472
|
+
sig: Any # inspect.Signature, but use Any for type compatibility
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@dataclass(slots=True)
|
|
476
|
+
class Value(Expr):
|
|
477
|
+
"""Wraps a non-primitive Python value for pass-through serialization.
|
|
478
|
+
|
|
479
|
+
Use cases:
|
|
480
|
+
- Complex prop values: options={"a": 1, "b": 2}
|
|
481
|
+
- Server-computed data passed to client components
|
|
482
|
+
- Any value that doesn't need expression semantics
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
value: Any
|
|
323
486
|
|
|
324
487
|
@override
|
|
325
|
-
def emit(self) ->
|
|
326
|
-
|
|
488
|
+
def emit(self, out: list[str]) -> None:
|
|
489
|
+
_emit_value(self.value, out)
|
|
490
|
+
|
|
491
|
+
@override
|
|
492
|
+
def render(self) -> VDOMNode:
|
|
493
|
+
raise TypeError("Value cannot be rendered as VDOMExpr; use coerce_json instead")
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class Element(Expr):
|
|
497
|
+
"""A React element: built-in tag, fragment, or client component.
|
|
327
498
|
|
|
499
|
+
Tag conventions:
|
|
500
|
+
- "" (empty string): Fragment
|
|
501
|
+
- "div", "span", etc.: HTML element
|
|
502
|
+
- "$$ComponentId": Client component (registered in JS registry)
|
|
503
|
+
- Expr (Import, Member, etc.): Direct component reference for transpilation
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
__slots__: tuple[str, ...] = ("tag", "props", "children", "key")
|
|
328
507
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
508
|
+
tag: str | Expr
|
|
509
|
+
props: dict[str, Any] | None
|
|
510
|
+
children: Sequence[Node] | None
|
|
511
|
+
key: str | Expr | None
|
|
333
512
|
|
|
334
|
-
def __init__(
|
|
513
|
+
def __init__(
|
|
514
|
+
self,
|
|
515
|
+
tag: str | Expr,
|
|
516
|
+
props: dict[str, Any] | None = None,
|
|
517
|
+
children: Sequence[Node] | None = None,
|
|
518
|
+
key: str | Expr | None = None,
|
|
519
|
+
) -> None:
|
|
520
|
+
self.tag = tag
|
|
335
521
|
self.props = props
|
|
522
|
+
if children is None:
|
|
523
|
+
self.children = None
|
|
524
|
+
else:
|
|
525
|
+
if isinstance(tag, str):
|
|
526
|
+
parent_name = tag[2:] if tag.startswith("$$") else tag
|
|
527
|
+
else:
|
|
528
|
+
tag_out: list[str] = []
|
|
529
|
+
tag.emit(tag_out)
|
|
530
|
+
parent_name = "".join(tag_out)
|
|
531
|
+
self.children = flatten_children(
|
|
532
|
+
children,
|
|
533
|
+
parent_name=parent_name,
|
|
534
|
+
warn_stacklevel=5,
|
|
535
|
+
)
|
|
536
|
+
self.key = key
|
|
537
|
+
if self.key is None and self.props:
|
|
538
|
+
self.key = self.props.pop("key", None)
|
|
539
|
+
|
|
540
|
+
def _emit_key(self, out: list[str]) -> None:
|
|
541
|
+
"""Emit key prop (string or expression)."""
|
|
542
|
+
if self.key is None:
|
|
543
|
+
return
|
|
544
|
+
if isinstance(self.key, str):
|
|
545
|
+
out.append('key="')
|
|
546
|
+
out.append(_escape_jsx_attr(self.key))
|
|
547
|
+
out.append('"')
|
|
548
|
+
else:
|
|
549
|
+
# Expression key: key={expr}
|
|
550
|
+
out.append("key={")
|
|
551
|
+
self.key.emit(out)
|
|
552
|
+
out.append("}")
|
|
336
553
|
|
|
337
554
|
@override
|
|
338
|
-
def emit(self) ->
|
|
339
|
-
|
|
340
|
-
|
|
555
|
+
def emit(self, out: list[str]) -> None:
|
|
556
|
+
# Fragment (only for string tags)
|
|
557
|
+
if self.tag == "":
|
|
558
|
+
if self.key is not None:
|
|
559
|
+
# Fragment with key needs explicit Fragment component
|
|
560
|
+
out.append("<Fragment ")
|
|
561
|
+
self._emit_key(out)
|
|
562
|
+
out.append(">")
|
|
563
|
+
for c in self.children or []:
|
|
564
|
+
_emit_jsx_child(c, out)
|
|
565
|
+
out.append("</Fragment>")
|
|
566
|
+
else:
|
|
567
|
+
out.append("<>")
|
|
568
|
+
for c in self.children or []:
|
|
569
|
+
_emit_jsx_child(c, out)
|
|
570
|
+
out.append("</>")
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
# Resolve tag - either emit Expr or use string (strip $$ prefix)
|
|
574
|
+
tag_out: list[str] = []
|
|
575
|
+
if isinstance(self.tag, Expr):
|
|
576
|
+
self.tag.emit(tag_out)
|
|
577
|
+
else:
|
|
578
|
+
tag_out.append(self.tag[2:] if self.tag.startswith("$$") else self.tag)
|
|
579
|
+
tag_str = "".join(tag_out)
|
|
580
|
+
|
|
581
|
+
# Build props into a separate buffer to check if empty
|
|
582
|
+
props_out: list[str] = []
|
|
583
|
+
if self.key is not None:
|
|
584
|
+
self._emit_key(props_out)
|
|
585
|
+
if self.props:
|
|
586
|
+
for name, value in self.props.items():
|
|
587
|
+
if props_out:
|
|
588
|
+
props_out.append(" ")
|
|
589
|
+
_emit_jsx_prop(name, value, props_out)
|
|
590
|
+
|
|
591
|
+
# Build children into a separate buffer to check if empty
|
|
592
|
+
children_out: list[str] = []
|
|
593
|
+
for c in self.children or []:
|
|
594
|
+
_emit_jsx_child(c, children_out)
|
|
595
|
+
|
|
596
|
+
# Self-closing if no children
|
|
597
|
+
if not children_out:
|
|
598
|
+
out.append("<")
|
|
599
|
+
out.append(tag_str)
|
|
600
|
+
if props_out:
|
|
601
|
+
out.append(" ")
|
|
602
|
+
out.extend(props_out)
|
|
603
|
+
out.append(" />")
|
|
604
|
+
return
|
|
341
605
|
|
|
606
|
+
# Open tag
|
|
607
|
+
out.append("<")
|
|
608
|
+
out.append(tag_str)
|
|
609
|
+
if props_out:
|
|
610
|
+
out.append(" ")
|
|
611
|
+
out.extend(props_out)
|
|
612
|
+
out.append(">")
|
|
613
|
+
# Children
|
|
614
|
+
out.extend(children_out)
|
|
615
|
+
# Close tag
|
|
616
|
+
out.append("</")
|
|
617
|
+
out.append(tag_str)
|
|
618
|
+
out.append(">")
|
|
342
619
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
operand: JSExpr
|
|
620
|
+
@override
|
|
621
|
+
def transpile_subscript(self, key: ast.expr, ctx: Transpiler) -> Expr:
|
|
622
|
+
"""Transpile subscript as adding children to this element.
|
|
347
623
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
self.
|
|
624
|
+
Handles both single children and tuple of children.
|
|
625
|
+
"""
|
|
626
|
+
if self.children:
|
|
627
|
+
raise TranspileError(
|
|
628
|
+
f"Element '{self.tag}' already has children; cannot add more via subscript"
|
|
629
|
+
)
|
|
351
630
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
631
|
+
# Convert key to list of children
|
|
632
|
+
if isinstance(key, ast.Tuple):
|
|
633
|
+
children = [ctx.emit_expr(e) for e in key.elts]
|
|
634
|
+
else:
|
|
635
|
+
children = [ctx.emit_expr(key)]
|
|
636
|
+
|
|
637
|
+
return Element(
|
|
638
|
+
tag=self.tag,
|
|
639
|
+
props=self.props,
|
|
640
|
+
children=children,
|
|
641
|
+
key=self.key,
|
|
356
642
|
)
|
|
357
|
-
if self.op == "typeof":
|
|
358
|
-
return f"typeof {operand_code}"
|
|
359
|
-
return f"{self.op}{operand_code}"
|
|
360
643
|
|
|
644
|
+
@override
|
|
645
|
+
def __getitem__(self, key: Any) -> Element: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
646
|
+
"""Return new Element with children set via subscript.
|
|
647
|
+
|
|
648
|
+
Raises if this element already has children.
|
|
649
|
+
Accepts a single child or a Sequence of children.
|
|
650
|
+
"""
|
|
651
|
+
if self.children:
|
|
652
|
+
raise ValueError(
|
|
653
|
+
f"Element '{self.tag}' already has children; cannot add more via subscript"
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Convert key to sequence of children
|
|
657
|
+
if isinstance(key, (list, tuple)):
|
|
658
|
+
children = list(cast(list[Any] | tuple[Any, ...], key))
|
|
659
|
+
else:
|
|
660
|
+
children = [key]
|
|
661
|
+
|
|
662
|
+
return Element(
|
|
663
|
+
tag=self.tag,
|
|
664
|
+
props=self.props,
|
|
665
|
+
children=children,
|
|
666
|
+
key=self.key,
|
|
667
|
+
)
|
|
361
668
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
operand: JSExpr
|
|
669
|
+
def with_children(self, children: Sequence[Node]) -> Element:
|
|
670
|
+
"""Return new Element with children set.
|
|
365
671
|
|
|
366
|
-
|
|
367
|
-
|
|
672
|
+
Raises if this element already has children.
|
|
673
|
+
"""
|
|
674
|
+
if self.children:
|
|
675
|
+
raise ValueError(
|
|
676
|
+
f"Element '{self.tag}' already has children; cannot add more via subscript"
|
|
677
|
+
)
|
|
678
|
+
return Element(
|
|
679
|
+
tag=self.tag,
|
|
680
|
+
props=self.props,
|
|
681
|
+
children=list(children),
|
|
682
|
+
key=self.key,
|
|
683
|
+
)
|
|
368
684
|
|
|
369
685
|
@override
|
|
370
|
-
def
|
|
371
|
-
|
|
372
|
-
|
|
686
|
+
def render(self) -> VDOMNode:
|
|
687
|
+
"""Element rendering is handled by Renderer.render_node(), not render().
|
|
688
|
+
|
|
689
|
+
This method validates render-time constraints and raises TypeError
|
|
690
|
+
because Element produces VDOMElement, not VDOMExpr.
|
|
691
|
+
"""
|
|
692
|
+
# Validate key is string or numeric (not arbitrary Expr) during rendering
|
|
693
|
+
if self.key is not None and not isinstance(self.key, (str, int)):
|
|
694
|
+
raise TypeError(
|
|
695
|
+
f"Element key must be a string or int for rendering, got {type(self.key).__name__}. "
|
|
696
|
+
+ "Expression keys are only valid during transpilation (emit)."
|
|
697
|
+
)
|
|
698
|
+
raise TypeError(
|
|
699
|
+
"Element cannot be rendered as VDOMExpr; use Renderer.render_node() instead"
|
|
373
700
|
)
|
|
374
|
-
return f"await {operand_code}"
|
|
375
701
|
|
|
376
702
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
op: str
|
|
381
|
-
right: JSExpr
|
|
703
|
+
@dataclass(slots=True)
|
|
704
|
+
class PulseNode:
|
|
705
|
+
"""A Pulse server-side component instance.
|
|
382
706
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
self.right = right
|
|
707
|
+
During rendering, PulseNode is called and replaced by its returned tree.
|
|
708
|
+
Can only appear in VDOM context (render path), never in transpiled code.
|
|
709
|
+
"""
|
|
387
710
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
711
|
+
fn: Any # Callable[..., Node]
|
|
712
|
+
args: tuple[Any, ...] = ()
|
|
713
|
+
kwargs: dict[str, Any] = field(default_factory=dict)
|
|
714
|
+
key: str | None = None
|
|
715
|
+
name: str | None = None # Optional component name for debug messages.
|
|
716
|
+
# Renderer state (mutable, set during render)
|
|
717
|
+
hooks: Any = None # HookContext
|
|
718
|
+
contents: Node | None = None
|
|
719
|
+
|
|
720
|
+
def emit(self, out: list[str]) -> None:
|
|
721
|
+
fn_name = getattr(self.fn, "__name__", "unknown")
|
|
722
|
+
raise TypeError(
|
|
723
|
+
f"Cannot transpile PulseNode '{fn_name}'. "
|
|
724
|
+
+ "Server components must be rendered, not transpiled."
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
def __getitem__(self, children_arg: "Node | tuple[Node, ...]"):
|
|
728
|
+
if self.args:
|
|
729
|
+
raise ValueError(
|
|
730
|
+
"PulseNode already received positional args; pass children in the call or via brackets, not both."
|
|
731
|
+
)
|
|
732
|
+
if not isinstance(children_arg, tuple):
|
|
733
|
+
children_arg = (children_arg,)
|
|
734
|
+
parent_name = self.name
|
|
735
|
+
if parent_name is None:
|
|
736
|
+
parent_name = getattr(self.fn, "__name__", "Component")
|
|
737
|
+
flat = flatten_children(
|
|
738
|
+
children_arg,
|
|
739
|
+
parent_name=parent_name,
|
|
740
|
+
warn_stacklevel=5,
|
|
404
741
|
)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
742
|
+
return PulseNode(
|
|
743
|
+
fn=self.fn,
|
|
744
|
+
args=tuple(flat),
|
|
745
|
+
kwargs=self.kwargs,
|
|
746
|
+
key=self.key,
|
|
747
|
+
name=self.name,
|
|
408
748
|
)
|
|
409
|
-
return f"{left_code} {self.op} {right_code}"
|
|
410
749
|
|
|
411
750
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
751
|
+
# =============================================================================
|
|
752
|
+
# Children normalization helpers
|
|
753
|
+
# =============================================================================
|
|
754
|
+
def flatten_children(
|
|
755
|
+
children: Sequence[Node | Iterable[Node]],
|
|
756
|
+
*,
|
|
757
|
+
parent_name: str,
|
|
758
|
+
warn_stacklevel: int = 5,
|
|
759
|
+
) -> list[Node]:
|
|
760
|
+
if env.pulse_env == "dev":
|
|
761
|
+
return _flatten_children_dev(
|
|
762
|
+
children, parent_name=parent_name, warn_stacklevel=warn_stacklevel
|
|
763
|
+
)
|
|
764
|
+
return _flatten_children_prod(children)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _flatten_children_prod(children: Sequence[Node | Iterable[Node]]) -> list[Node]:
|
|
768
|
+
flat: list[Node] = []
|
|
769
|
+
|
|
770
|
+
def visit(item: Node | Iterable[Node]) -> None:
|
|
771
|
+
if isinstance(item, dict):
|
|
772
|
+
raise TypeError("Dict is not a valid child")
|
|
773
|
+
if isinstance(item, Iterable) and not isinstance(item, str):
|
|
774
|
+
for sub in item:
|
|
775
|
+
visit(sub)
|
|
776
|
+
else:
|
|
777
|
+
flat.append(item)
|
|
778
|
+
|
|
779
|
+
for child in children:
|
|
780
|
+
visit(child)
|
|
781
|
+
|
|
782
|
+
return flat
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _flatten_children_dev(
|
|
786
|
+
children: Sequence[Node | Iterable[Node]],
|
|
787
|
+
*,
|
|
788
|
+
parent_name: str,
|
|
789
|
+
warn_stacklevel: int = 5,
|
|
790
|
+
) -> list[Node]:
|
|
791
|
+
flat: list[Node] = []
|
|
792
|
+
seen_keys: set[str] = set()
|
|
793
|
+
|
|
794
|
+
def visit(item: Node | Iterable[Node]) -> None:
|
|
795
|
+
if isinstance(item, dict):
|
|
796
|
+
raise TypeError("Dict is not a valid child")
|
|
797
|
+
if isinstance(item, Iterable) and not isinstance(item, str):
|
|
798
|
+
missing_key = False
|
|
799
|
+
for sub in item:
|
|
800
|
+
if isinstance(sub, PulseNode) and sub.key is None:
|
|
801
|
+
missing_key = True
|
|
802
|
+
if isinstance(sub, Element) and _normalize_key(sub.key) is None:
|
|
803
|
+
missing_key = True
|
|
804
|
+
visit(sub) # type: ignore[arg-type]
|
|
805
|
+
if missing_key:
|
|
806
|
+
clean_name = clean_element_name(parent_name)
|
|
807
|
+
warnings.warn(
|
|
808
|
+
(
|
|
809
|
+
f"[Pulse] Iterable children of {clean_name} contain elements without 'key'. "
|
|
810
|
+
"Add a stable 'key' to each element inside iterables to improve reconciliation."
|
|
811
|
+
),
|
|
812
|
+
stacklevel=warn_stacklevel,
|
|
813
|
+
)
|
|
814
|
+
else:
|
|
815
|
+
if isinstance(item, PulseNode) and item.key is not None:
|
|
816
|
+
if item.key in seen_keys:
|
|
817
|
+
clean_name = clean_element_name(parent_name)
|
|
818
|
+
raise ValueError(
|
|
819
|
+
f"[Pulse] Duplicate key '{item.key}' found among children of {clean_name}. "
|
|
820
|
+
+ "Keys must be unique per sibling set."
|
|
821
|
+
)
|
|
822
|
+
seen_keys.add(item.key)
|
|
823
|
+
if isinstance(item, Element):
|
|
824
|
+
key = _normalize_key(item.key)
|
|
825
|
+
if key is not None:
|
|
826
|
+
if key in seen_keys:
|
|
827
|
+
clean_name = clean_element_name(parent_name)
|
|
828
|
+
raise ValueError(
|
|
829
|
+
f"[Pulse] Duplicate key '{key}' found among children of {clean_name}. "
|
|
830
|
+
+ "Keys must be unique per sibling set."
|
|
831
|
+
)
|
|
832
|
+
seen_keys.add(key)
|
|
833
|
+
flat.append(item)
|
|
834
|
+
|
|
835
|
+
for child in children:
|
|
836
|
+
visit(child)
|
|
837
|
+
|
|
838
|
+
return flat
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def clean_element_name(parent_name: str) -> str:
|
|
842
|
+
if parent_name.startswith("<") and parent_name.endswith(">"):
|
|
843
|
+
return parent_name
|
|
844
|
+
return f"<{parent_name}>"
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _normalize_key(key: object | None) -> str | None:
|
|
848
|
+
if isinstance(key, Literal):
|
|
849
|
+
return key.value if isinstance(key.value, str) else None
|
|
850
|
+
return key if isinstance(key, str) else None
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
# =============================================================================
|
|
854
|
+
# Expression Nodes
|
|
855
|
+
# =============================================================================
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
@dataclass(slots=True)
|
|
859
|
+
class Identifier(Expr):
|
|
860
|
+
"""JS identifier: x, foo, myFunc"""
|
|
416
861
|
|
|
417
|
-
|
|
418
|
-
self.op = op
|
|
419
|
-
self.values = values
|
|
862
|
+
name: str
|
|
420
863
|
|
|
421
864
|
@override
|
|
422
|
-
def emit(self) ->
|
|
423
|
-
|
|
424
|
-
return self.values[0].emit()
|
|
425
|
-
parts: list[str] = []
|
|
426
|
-
for v in self.values:
|
|
427
|
-
# No strict left/right in chains, but treat as middle
|
|
428
|
-
code = _emit_child_for_binary_like(v, parent_op=self.op, side="chain")
|
|
429
|
-
parts.append(code)
|
|
430
|
-
return f" {self.op} ".join(parts)
|
|
865
|
+
def emit(self, out: list[str]) -> None:
|
|
866
|
+
out.append(self.name)
|
|
431
867
|
|
|
868
|
+
@override
|
|
869
|
+
def render(self) -> VDOMNode:
|
|
870
|
+
return {"t": "id", "name": self.name}
|
|
432
871
|
|
|
433
|
-
class JSTertiary(JSExpr):
|
|
434
|
-
__slots__ = ("test", "if_true", "if_false") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
435
|
-
test: JSExpr
|
|
436
|
-
if_true: JSExpr
|
|
437
|
-
if_false: JSExpr
|
|
438
872
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
873
|
+
@dataclass(slots=True)
|
|
874
|
+
class Literal(Expr):
|
|
875
|
+
"""JS literal: 42, "hello", true, null"""
|
|
876
|
+
|
|
877
|
+
value: int | float | str | bool | None
|
|
443
878
|
|
|
444
879
|
@override
|
|
445
|
-
def emit(self) ->
|
|
446
|
-
|
|
880
|
+
def emit(self, out: list[str]) -> None:
|
|
881
|
+
if self.value is None:
|
|
882
|
+
out.append("null")
|
|
883
|
+
elif isinstance(self.value, bool):
|
|
884
|
+
out.append("true" if self.value else "false")
|
|
885
|
+
elif isinstance(self.value, str):
|
|
886
|
+
out.append('"')
|
|
887
|
+
out.append(_escape_string(self.value))
|
|
888
|
+
out.append('"')
|
|
889
|
+
else:
|
|
890
|
+
out.append(str(self.value))
|
|
447
891
|
|
|
892
|
+
@override
|
|
893
|
+
def render(self) -> VDOMNode:
|
|
894
|
+
return self.value
|
|
448
895
|
|
|
449
|
-
class JSFunctionDef(JSExpr):
|
|
450
|
-
__slots__ = ("params", "body", "name", "is_async") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
451
|
-
params: Sequence[str]
|
|
452
|
-
body: Sequence[JSStmt]
|
|
453
|
-
name: str | None
|
|
454
|
-
is_async: bool
|
|
455
896
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
params: Sequence[str],
|
|
459
|
-
body: Sequence[JSStmt],
|
|
460
|
-
name: str | None = None,
|
|
461
|
-
is_async: bool = False,
|
|
462
|
-
):
|
|
463
|
-
self.params = params
|
|
464
|
-
self.body = body
|
|
465
|
-
self.name = name
|
|
466
|
-
self.is_async = is_async
|
|
467
|
-
|
|
468
|
-
@override
|
|
469
|
-
def emit(self) -> str:
|
|
470
|
-
params = ", ".join(self.params)
|
|
471
|
-
body_code = "\n".join(s.emit() for s in self.body)
|
|
472
|
-
prefix = "async " if self.is_async else ""
|
|
473
|
-
if self.name:
|
|
474
|
-
return f"{prefix}function {self.name}({params}){{\n{body_code}\n}}"
|
|
475
|
-
return f"{prefix}function({params}){{\n{body_code}\n}}"
|
|
897
|
+
class Undefined(Expr):
|
|
898
|
+
"""JS undefined literal.
|
|
476
899
|
|
|
900
|
+
Use Undefined() for JS `undefined`. Literal(None) emits `null`.
|
|
901
|
+
This is a singleton-like class with no fields.
|
|
902
|
+
"""
|
|
477
903
|
|
|
478
|
-
|
|
479
|
-
__slots__ = ("parts",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
480
|
-
is_primary: ClassVar[bool] = True
|
|
481
|
-
parts: Sequence[str | JSExpr]
|
|
904
|
+
__slots__: tuple[str, ...] = ()
|
|
482
905
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
self.parts = parts
|
|
906
|
+
@override
|
|
907
|
+
def emit(self, out: list[str]) -> None:
|
|
908
|
+
out.append("undefined")
|
|
487
909
|
|
|
488
910
|
@override
|
|
489
|
-
def
|
|
490
|
-
|
|
491
|
-
for p in self.parts:
|
|
492
|
-
if isinstance(p, str):
|
|
493
|
-
out.append(
|
|
494
|
-
p.replace("\\", "\\\\")
|
|
495
|
-
.replace("`", "\\`")
|
|
496
|
-
.replace("${", "\\${")
|
|
497
|
-
.replace("\n", "\\n")
|
|
498
|
-
.replace("\r", "\\r")
|
|
499
|
-
.replace("\t", "\\t")
|
|
500
|
-
.replace("\b", "\\b")
|
|
501
|
-
.replace("\f", "\\f")
|
|
502
|
-
.replace("\v", "\\v")
|
|
503
|
-
.replace("\x00", "\\x00")
|
|
504
|
-
.replace("\u2028", "\\u2028")
|
|
505
|
-
.replace("\u2029", "\\u2029")
|
|
506
|
-
)
|
|
507
|
-
else:
|
|
508
|
-
out.append("${" + p.emit() + "}")
|
|
509
|
-
out.append("`")
|
|
510
|
-
return "".join(out)
|
|
911
|
+
def render(self) -> VDOMNode:
|
|
912
|
+
return {"t": "undef"}
|
|
511
913
|
|
|
512
914
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
obj: JSExpr
|
|
516
|
-
prop: str
|
|
915
|
+
# Singleton instance for convenience
|
|
916
|
+
UNDEFINED = Undefined()
|
|
517
917
|
|
|
518
|
-
def __init__(self, obj: JSExpr, prop: str):
|
|
519
|
-
self.obj = obj
|
|
520
|
-
self.prop = prop
|
|
521
918
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
919
|
+
@dataclass(slots=True)
|
|
920
|
+
class Array(Expr):
|
|
921
|
+
"""JS array: [a, b, c]"""
|
|
922
|
+
|
|
923
|
+
elements: Sequence[Expr]
|
|
526
924
|
|
|
527
925
|
@override
|
|
528
|
-
def
|
|
529
|
-
""
|
|
926
|
+
def emit(self, out: list[str]) -> None:
|
|
927
|
+
out.append("[")
|
|
928
|
+
for i, e in enumerate(self.elements):
|
|
929
|
+
if i > 0:
|
|
930
|
+
out.append(", ")
|
|
931
|
+
e.emit(out)
|
|
932
|
+
out.append("]")
|
|
530
933
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
"""
|
|
534
|
-
if kwargs:
|
|
535
|
-
raise JSCompilationError("Keyword arguments not supported in method call")
|
|
536
|
-
# Convert args to JSExpr
|
|
537
|
-
js_args = [JSExpr.of(a) for a in args]
|
|
538
|
-
# Check for Python builtin method transpilation (late import to avoid cycle)
|
|
539
|
-
from pulse.transpiler.builtins import emit_method
|
|
934
|
+
@override
|
|
935
|
+
def render(self) -> VDOMNode:
|
|
936
|
+
return {"t": "array", "items": [e.render() for e in self.elements]}
|
|
540
937
|
|
|
541
|
-
result = emit_method(self.obj, self.prop, js_args)
|
|
542
|
-
if result is not None:
|
|
543
|
-
return result
|
|
544
|
-
return JSMemberCall(self.obj, self.prop, js_args)
|
|
545
938
|
|
|
939
|
+
@dataclass(slots=True)
|
|
940
|
+
class Object(Expr):
|
|
941
|
+
"""JS object: { key: value }"""
|
|
546
942
|
|
|
547
|
-
|
|
548
|
-
__slots__ = ("obj", "index") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
549
|
-
obj: JSExpr
|
|
550
|
-
index: JSExpr
|
|
943
|
+
props: Sequence[tuple[str, Expr]]
|
|
551
944
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
945
|
+
@override
|
|
946
|
+
def emit(self, out: list[str]) -> None:
|
|
947
|
+
out.append("{")
|
|
948
|
+
for i, (k, v) in enumerate(self.props):
|
|
949
|
+
if i > 0:
|
|
950
|
+
out.append(", ")
|
|
951
|
+
out.append('"')
|
|
952
|
+
out.append(_escape_string(k))
|
|
953
|
+
out.append('": ')
|
|
954
|
+
v.emit(out)
|
|
955
|
+
out.append("}")
|
|
555
956
|
|
|
556
957
|
@override
|
|
557
|
-
def
|
|
558
|
-
|
|
559
|
-
|
|
958
|
+
def render(self) -> VDOMNode:
|
|
959
|
+
return {"t": "object", "props": {k: v.render() for k, v in self.props}}
|
|
960
|
+
|
|
560
961
|
|
|
962
|
+
@dataclass(slots=True)
|
|
963
|
+
class Member(Expr):
|
|
964
|
+
"""JS member access: obj.prop"""
|
|
561
965
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
callee: JSExpr # typically JSIdentifier
|
|
565
|
-
args: Sequence[JSExpr]
|
|
966
|
+
obj: Expr
|
|
967
|
+
prop: str
|
|
566
968
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
self.
|
|
969
|
+
@override
|
|
970
|
+
def emit(self, out: list[str]) -> None:
|
|
971
|
+
_emit_primary(self.obj, out)
|
|
972
|
+
out.append(".")
|
|
973
|
+
out.append(self.prop)
|
|
570
974
|
|
|
571
975
|
@override
|
|
572
|
-
def
|
|
573
|
-
|
|
574
|
-
return f"{fn}({', '.join(a.emit() for a in self.args)})"
|
|
976
|
+
def render(self) -> VDOMNode:
|
|
977
|
+
return {"t": "member", "obj": self.obj.render(), "prop": self.prop}
|
|
575
978
|
|
|
576
979
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
method: str
|
|
581
|
-
args: Sequence[JSExpr]
|
|
980
|
+
@dataclass(slots=True)
|
|
981
|
+
class Subscript(Expr):
|
|
982
|
+
"""JS subscript access: obj[key]"""
|
|
582
983
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
self.method = method
|
|
586
|
-
self.args = args
|
|
984
|
+
obj: Expr
|
|
985
|
+
key: Expr
|
|
587
986
|
|
|
588
987
|
@override
|
|
589
|
-
def emit(self) ->
|
|
590
|
-
|
|
591
|
-
|
|
988
|
+
def emit(self, out: list[str]) -> None:
|
|
989
|
+
_emit_primary(self.obj, out)
|
|
990
|
+
out.append("[")
|
|
991
|
+
self.key.emit(out)
|
|
992
|
+
out.append("]")
|
|
592
993
|
|
|
994
|
+
@override
|
|
995
|
+
def render(self) -> VDOMNode:
|
|
996
|
+
return {"t": "sub", "obj": self.obj.render(), "key": self.key.render()}
|
|
593
997
|
|
|
594
|
-
class JSNew(JSExpr):
|
|
595
|
-
__slots__ = ("ctor", "args") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
596
|
-
ctor: JSExpr
|
|
597
|
-
args: Sequence[JSExpr]
|
|
598
998
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
999
|
+
@dataclass(slots=True)
|
|
1000
|
+
class Call(Expr):
|
|
1001
|
+
"""JS function call: fn(args)"""
|
|
1002
|
+
|
|
1003
|
+
callee: Expr
|
|
1004
|
+
args: Sequence[Expr]
|
|
602
1005
|
|
|
603
1006
|
@override
|
|
604
|
-
def emit(self) ->
|
|
605
|
-
|
|
606
|
-
|
|
1007
|
+
def emit(self, out: list[str]) -> None:
|
|
1008
|
+
_emit_primary(self.callee, out)
|
|
1009
|
+
out.append("(")
|
|
1010
|
+
for i, a in enumerate(self.args):
|
|
1011
|
+
if i > 0:
|
|
1012
|
+
out.append(", ")
|
|
1013
|
+
a.emit(out)
|
|
1014
|
+
out.append(")")
|
|
607
1015
|
|
|
1016
|
+
@override
|
|
1017
|
+
def render(self) -> VDOMNode:
|
|
1018
|
+
return {
|
|
1019
|
+
"t": "call",
|
|
1020
|
+
"callee": self.callee.render(),
|
|
1021
|
+
"args": [a.render() for a in self.args],
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
@dataclass(slots=True)
|
|
1026
|
+
class Unary(Expr):
|
|
1027
|
+
"""JS unary expression: -x, !x, typeof x"""
|
|
1028
|
+
|
|
1029
|
+
op: str
|
|
1030
|
+
operand: Expr
|
|
608
1031
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1032
|
+
@override
|
|
1033
|
+
def precedence(self) -> int:
|
|
1034
|
+
op = self.op
|
|
1035
|
+
tag = "+u" if op == "+" else ("-u" if op == "-" else op)
|
|
1036
|
+
return _PRECEDENCE.get(tag, 17)
|
|
613
1037
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
self.
|
|
1038
|
+
@override
|
|
1039
|
+
def emit(self, out: list[str]) -> None:
|
|
1040
|
+
if self.op in {"typeof", "await", "void", "delete"}:
|
|
1041
|
+
out.append(self.op)
|
|
1042
|
+
out.append(" ")
|
|
1043
|
+
else:
|
|
1044
|
+
out.append(self.op)
|
|
1045
|
+
_emit_paren(self.operand, self.op, "unary", out)
|
|
617
1046
|
|
|
618
1047
|
@override
|
|
619
|
-
def
|
|
620
|
-
|
|
1048
|
+
def render(self) -> VDOMNode:
|
|
1049
|
+
if self.op == "await":
|
|
1050
|
+
raise TypeError("await is not supported in VDOM expressions")
|
|
1051
|
+
return {"t": "unary", "op": self.op, "arg": self.operand.render()}
|
|
621
1052
|
|
|
622
1053
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1054
|
+
@dataclass(slots=True)
|
|
1055
|
+
class Binary(Expr):
|
|
1056
|
+
"""JS binary expression: x + y, a && b"""
|
|
626
1057
|
|
|
627
|
-
|
|
628
|
-
|
|
1058
|
+
left: Expr
|
|
1059
|
+
op: str
|
|
1060
|
+
right: Expr
|
|
629
1061
|
|
|
630
1062
|
@override
|
|
631
|
-
def
|
|
632
|
-
|
|
633
|
-
inner = ", ".join(v.emit() for v in self.values)
|
|
634
|
-
return f"({inner})"
|
|
1063
|
+
def precedence(self) -> int:
|
|
1064
|
+
return _PRECEDENCE.get(self.op, 0)
|
|
635
1065
|
|
|
1066
|
+
@override
|
|
1067
|
+
def emit(self, out: list[str]) -> None:
|
|
1068
|
+
# Special: ** with unary +/- on left needs parens
|
|
1069
|
+
force_left = (
|
|
1070
|
+
self.op == "**"
|
|
1071
|
+
and isinstance(self.left, Unary)
|
|
1072
|
+
and self.left.op in {"-", "+"}
|
|
1073
|
+
)
|
|
1074
|
+
if force_left:
|
|
1075
|
+
out.append("(")
|
|
1076
|
+
self.left.emit(out)
|
|
1077
|
+
out.append(")")
|
|
1078
|
+
else:
|
|
1079
|
+
_emit_paren(self.left, self.op, "left", out)
|
|
1080
|
+
out.append(" ")
|
|
1081
|
+
out.append(self.op)
|
|
1082
|
+
out.append(" ")
|
|
1083
|
+
_emit_paren(self.right, self.op, "right", out)
|
|
636
1084
|
|
|
637
|
-
|
|
638
|
-
|
|
1085
|
+
@override
|
|
1086
|
+
def render(self) -> VDOMNode:
|
|
1087
|
+
return {
|
|
1088
|
+
"t": "binary",
|
|
1089
|
+
"op": self.op,
|
|
1090
|
+
"left": self.left.render(),
|
|
1091
|
+
"right": self.right.render(),
|
|
1092
|
+
}
|
|
639
1093
|
|
|
640
|
-
Generalizes the pattern of call-only expressions. The wrapped function
|
|
641
|
-
receives positional and keyword JSExpr arguments and returns a JSExpr.
|
|
642
1094
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
"""
|
|
1095
|
+
@dataclass(slots=True)
|
|
1096
|
+
class Ternary(Expr):
|
|
1097
|
+
"""JS ternary expression: cond ? a : b"""
|
|
647
1098
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1099
|
+
cond: Expr
|
|
1100
|
+
then: Expr
|
|
1101
|
+
else_: Expr
|
|
651
1102
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1103
|
+
@override
|
|
1104
|
+
def precedence(self) -> int:
|
|
1105
|
+
return _PRECEDENCE["?:"]
|
|
655
1106
|
|
|
656
1107
|
@override
|
|
657
|
-
def emit(self) ->
|
|
658
|
-
|
|
659
|
-
|
|
1108
|
+
def emit(self, out: list[str]) -> None:
|
|
1109
|
+
self.cond.emit(out)
|
|
1110
|
+
out.append(" ? ")
|
|
1111
|
+
self.then.emit(out)
|
|
1112
|
+
out.append(" : ")
|
|
1113
|
+
self.else_.emit(out)
|
|
660
1114
|
|
|
661
1115
|
@override
|
|
662
|
-
def
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1116
|
+
def render(self) -> VDOMNode:
|
|
1117
|
+
return {
|
|
1118
|
+
"t": "ternary",
|
|
1119
|
+
"cond": self.cond.render(),
|
|
1120
|
+
"then": self.then.render(),
|
|
1121
|
+
"else_": self.else_.render(),
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
@dataclass(slots=True)
|
|
1126
|
+
class Arrow(Expr):
|
|
1127
|
+
"""JS arrow function: (x) => expr or (x) => { ... }
|
|
1128
|
+
|
|
1129
|
+
body can be:
|
|
1130
|
+
- Expr: expression body, emits as `() => expr`
|
|
1131
|
+
- Sequence[Stmt]: statement body, emits as `() => { stmt1; stmt2; }`
|
|
1132
|
+
"""
|
|
1133
|
+
|
|
1134
|
+
params: Sequence[str]
|
|
1135
|
+
body: Expr | Sequence[Stmt]
|
|
667
1136
|
|
|
668
1137
|
@override
|
|
669
|
-
def
|
|
670
|
-
|
|
671
|
-
|
|
1138
|
+
def precedence(self) -> int:
|
|
1139
|
+
# Arrow functions have very low precedence (assignment level)
|
|
1140
|
+
# This ensures they get wrapped in parens when used as callee in Call
|
|
1141
|
+
return 3
|
|
672
1142
|
|
|
673
1143
|
@override
|
|
674
|
-
def
|
|
675
|
-
|
|
676
|
-
|
|
1144
|
+
def emit(self, out: list[str]) -> None:
|
|
1145
|
+
if len(self.params) == 1:
|
|
1146
|
+
out.append(self.params[0])
|
|
1147
|
+
else:
|
|
1148
|
+
out.append("(")
|
|
1149
|
+
out.append(", ".join(self.params))
|
|
1150
|
+
out.append(")")
|
|
1151
|
+
out.append(" => ")
|
|
1152
|
+
if isinstance(self.body, Expr):
|
|
1153
|
+
self.body.emit(out)
|
|
1154
|
+
else:
|
|
1155
|
+
out.append("{ ")
|
|
1156
|
+
for stmt in self.body:
|
|
1157
|
+
stmt.emit(out)
|
|
1158
|
+
out.append(" ")
|
|
1159
|
+
out.append("}")
|
|
677
1160
|
|
|
1161
|
+
@override
|
|
1162
|
+
def render(self) -> VDOMNode:
|
|
1163
|
+
if not isinstance(self.body, Expr):
|
|
1164
|
+
raise TypeError("Arrow with statement body cannot be rendered as VDOMExpr")
|
|
1165
|
+
return {"t": "arrow", "params": list(self.params), "body": self.body.render()}
|
|
678
1166
|
|
|
679
|
-
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
680
1167
|
|
|
1168
|
+
@dataclass(slots=True)
|
|
1169
|
+
class Template(Expr):
|
|
1170
|
+
"""JS template literal: `hello ${name}`
|
|
681
1171
|
|
|
682
|
-
|
|
683
|
-
|
|
1172
|
+
Parts alternate: [str, Expr, str, Expr, str, ...]
|
|
1173
|
+
Always starts and ends with a string (may be empty).
|
|
1174
|
+
"""
|
|
684
1175
|
|
|
1176
|
+
parts: Sequence[str | Expr] # alternating, starting with str
|
|
685
1177
|
|
|
686
|
-
@
|
|
687
|
-
def
|
|
1178
|
+
@override
|
|
1179
|
+
def emit(self, out: list[str]) -> None:
|
|
1180
|
+
out.append("`")
|
|
1181
|
+
for p in self.parts:
|
|
1182
|
+
if isinstance(p, str):
|
|
1183
|
+
out.append(_escape_template(p))
|
|
1184
|
+
else:
|
|
1185
|
+
out.append("${")
|
|
1186
|
+
p.emit(out)
|
|
1187
|
+
out.append("}")
|
|
1188
|
+
out.append("`")
|
|
688
1189
|
|
|
1190
|
+
@override
|
|
1191
|
+
def render(self) -> VDOMNode:
|
|
1192
|
+
rendered_parts: list[str | VDOMNode] = []
|
|
1193
|
+
for p in self.parts:
|
|
1194
|
+
if isinstance(p, str):
|
|
1195
|
+
rendered_parts.append(p)
|
|
1196
|
+
else:
|
|
1197
|
+
rendered_parts.append(p.render())
|
|
1198
|
+
return {"t": "template", "parts": rendered_parts}
|
|
689
1199
|
|
|
690
|
-
def js_transformer(arg: str | _F) -> Callable[[_F], _F] | _F:
|
|
691
|
-
"""Decorator/helper for JSTransformer.
|
|
692
1200
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
or:
|
|
697
|
-
emit_len = js_transformer(lambda x: ...)
|
|
1201
|
+
@dataclass(slots=True)
|
|
1202
|
+
class Spread(Expr):
|
|
1203
|
+
"""JS spread: ...expr"""
|
|
698
1204
|
|
|
699
|
-
|
|
700
|
-
the original function type. This allows decorated functions to have
|
|
701
|
-
proper return types (e.g., NoReturn for throw).
|
|
702
|
-
"""
|
|
703
|
-
if isinstance(arg, str):
|
|
1205
|
+
expr: Expr
|
|
704
1206
|
|
|
705
|
-
|
|
706
|
-
|
|
1207
|
+
@override
|
|
1208
|
+
def emit(self, out: list[str]) -> None:
|
|
1209
|
+
out.append("...")
|
|
1210
|
+
self.expr.emit(out)
|
|
707
1211
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
else:
|
|
712
|
-
raise TypeError(
|
|
713
|
-
"js_transformer expects a function or string (for decorator usage)"
|
|
714
|
-
)
|
|
1212
|
+
@override
|
|
1213
|
+
def render(self) -> VDOMNode:
|
|
1214
|
+
raise TypeError("Spread cannot be rendered as VDOMExpr directly")
|
|
715
1215
|
|
|
716
1216
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1217
|
+
@dataclass(slots=True)
|
|
1218
|
+
class New(Expr):
|
|
1219
|
+
"""JS new expression: new Ctor(args)"""
|
|
720
1220
|
|
|
721
|
-
|
|
722
|
-
|
|
1221
|
+
ctor: Expr
|
|
1222
|
+
args: Sequence[Expr]
|
|
723
1223
|
|
|
724
1224
|
@override
|
|
725
|
-
def emit(self) ->
|
|
726
|
-
|
|
1225
|
+
def emit(self, out: list[str]) -> None:
|
|
1226
|
+
out.append("new ")
|
|
1227
|
+
self.ctor.emit(out)
|
|
1228
|
+
out.append("(")
|
|
1229
|
+
for i, a in enumerate(self.args):
|
|
1230
|
+
if i > 0:
|
|
1231
|
+
out.append(", ")
|
|
1232
|
+
a.emit(out)
|
|
1233
|
+
out.append(")")
|
|
727
1234
|
|
|
1235
|
+
@override
|
|
1236
|
+
def render(self) -> VDOMNode:
|
|
1237
|
+
return {
|
|
1238
|
+
"t": "new",
|
|
1239
|
+
"ctor": self.ctor.render(),
|
|
1240
|
+
"args": [a.render() for a in self.args],
|
|
1241
|
+
}
|
|
728
1242
|
|
|
729
|
-
class JSThrow(JSStmt):
|
|
730
|
-
__slots__ = ("value",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
731
|
-
value: JSExpr
|
|
732
1243
|
|
|
733
|
-
|
|
734
|
-
|
|
1244
|
+
class TransformerFn(Protocol):
|
|
1245
|
+
def __call__(self, *args: Any, ctx: Transpiler, **kwargs: Any) -> Expr: ...
|
|
735
1246
|
|
|
736
|
-
@override
|
|
737
|
-
def emit(self) -> str:
|
|
738
|
-
return f"throw {self.value.emit()};"
|
|
739
1247
|
|
|
1248
|
+
_F = TypeVar("_F", bound=TransformerFn)
|
|
740
1249
|
|
|
741
|
-
class JSStmtExpr(JSExpr):
|
|
742
|
-
"""Expression wrapper for a statement (e.g., throw).
|
|
743
1250
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
"""
|
|
1251
|
+
@dataclass(slots=True)
|
|
1252
|
+
class Transformer(Expr, Generic[_F]):
|
|
1253
|
+
"""Expr that wraps a function transforming args to Expr output.
|
|
748
1254
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
name: str # For error messages (e.g., "throw")
|
|
1255
|
+
Used for Python->JS transpilation of functions, builtins, and module attrs.
|
|
1256
|
+
The wrapped function receives args/kwargs and ctx, and returns an Expr.
|
|
752
1257
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1258
|
+
Example:
|
|
1259
|
+
emit_len = Transformer(lambda x, ctx: Member(ctx.emit_expr(x), "length"), name="len")
|
|
1260
|
+
# When called: emit_len.transpile_call([some_ast], {}, ctx) -> Member(some_expr, "length")
|
|
1261
|
+
"""
|
|
1262
|
+
|
|
1263
|
+
fn: _F
|
|
1264
|
+
name: str = "" # For error messages
|
|
756
1265
|
|
|
757
1266
|
@override
|
|
758
|
-
def emit(self) ->
|
|
759
|
-
label = self.name or "
|
|
760
|
-
raise
|
|
761
|
-
f"'{label}' cannot be used inside an expression. "
|
|
762
|
-
+ "Use it as a standalone statement instead. "
|
|
763
|
-
+ f"For example, write `{label}(x)` on its own line, not `y = {label}(x)` or `f({label}(x))`."
|
|
764
|
-
)
|
|
1267
|
+
def emit(self, out: list[str]) -> None:
|
|
1268
|
+
label = self.name or "Transformer"
|
|
1269
|
+
raise TypeError(f"{label} cannot be emitted directly - must be called")
|
|
765
1270
|
|
|
1271
|
+
@override
|
|
1272
|
+
def transpile_call(
|
|
1273
|
+
self,
|
|
1274
|
+
args: list[ast.expr],
|
|
1275
|
+
kwargs: dict[str, ast.expr],
|
|
1276
|
+
ctx: Transpiler,
|
|
1277
|
+
) -> Expr:
|
|
1278
|
+
if kwargs:
|
|
1279
|
+
return self.fn(*args, ctx=ctx, **kwargs)
|
|
1280
|
+
return self.fn(*args, ctx=ctx)
|
|
766
1281
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
declare: bool # when True emit 'let name = ...'
|
|
1282
|
+
@override
|
|
1283
|
+
def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
|
|
1284
|
+
label = self.name or "Transformer"
|
|
1285
|
+
raise TypeError(f"{label} cannot have attributes")
|
|
772
1286
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
self.
|
|
776
|
-
|
|
1287
|
+
@override
|
|
1288
|
+
def transpile_subscript(self, key: ast.expr, ctx: Transpiler) -> Expr:
|
|
1289
|
+
label = self.name or "Transformer"
|
|
1290
|
+
raise TypeError(f"{label} cannot be subscripted")
|
|
777
1291
|
|
|
778
1292
|
@override
|
|
779
|
-
def
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
return f"{self.name} = {self.value.emit()};"
|
|
1293
|
+
def render(self) -> VDOMNode:
|
|
1294
|
+
label = self.name or "Transformer"
|
|
1295
|
+
raise TypeError(f"{label} cannot be rendered - must be called")
|
|
783
1296
|
|
|
784
1297
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
is_primary: ClassVar[bool] = True
|
|
788
|
-
content: str
|
|
1298
|
+
@overload
|
|
1299
|
+
def transformer(arg: str) -> Callable[[_F], _F]: ...
|
|
789
1300
|
|
|
790
|
-
def __init__(self, content: str):
|
|
791
|
-
self.content = content
|
|
792
1301
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
return self.content
|
|
1302
|
+
@overload
|
|
1303
|
+
def transformer(arg: _F) -> _F: ...
|
|
796
1304
|
|
|
797
1305
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1306
|
+
def transformer(arg: str | _F) -> Callable[[_F], _F] | _F:
|
|
1307
|
+
"""Decorator/helper for Transformer.
|
|
1308
|
+
|
|
1309
|
+
Usage:
|
|
1310
|
+
@transformer("len")
|
|
1311
|
+
def emit_len(x, *, ctx): ...
|
|
1312
|
+
or:
|
|
1313
|
+
emit_len = transformer(lambda x, *, ctx: ...)
|
|
1314
|
+
|
|
1315
|
+
Returns a Transformer, but the type signature lies and preserves
|
|
1316
|
+
the original function type.
|
|
1317
|
+
"""
|
|
1318
|
+
if isinstance(arg, str):
|
|
801
1319
|
|
|
1320
|
+
def decorator(fn: _F) -> _F:
|
|
1321
|
+
return cast(_F, Transformer(fn, name=arg))
|
|
802
1322
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1323
|
+
return decorator
|
|
1324
|
+
elif isfunction(arg):
|
|
1325
|
+
# Use empty name for lambdas, function name for named functions
|
|
1326
|
+
name = "" if arg.__name__ == "<lambda>" else arg.__name__
|
|
1327
|
+
return cast(_F, Transformer(arg, name=name))
|
|
1328
|
+
else:
|
|
1329
|
+
raise TypeError(
|
|
1330
|
+
"transformer expects a function or string (for decorator usage)"
|
|
810
1331
|
)
|
|
811
1332
|
|
|
812
1333
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1334
|
+
# =============================================================================
|
|
1335
|
+
# Statement Nodes
|
|
1336
|
+
# =============================================================================
|
|
816
1337
|
|
|
817
1338
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
value: JSExpr | None
|
|
1339
|
+
@dataclass(slots=True)
|
|
1340
|
+
class Return(Stmt):
|
|
1341
|
+
"""JS return statement: return expr;"""
|
|
822
1342
|
|
|
823
|
-
|
|
824
|
-
self.name = name
|
|
825
|
-
self.value = value
|
|
1343
|
+
value: Expr | None = None
|
|
826
1344
|
|
|
827
1345
|
@override
|
|
828
|
-
def emit(self) ->
|
|
829
|
-
|
|
830
|
-
if self.value is None:
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
return f"{self.name}={self.value.emit()}"
|
|
835
|
-
return self.name + "={" + self.value.emit() + "}"
|
|
1346
|
+
def emit(self, out: list[str]) -> None:
|
|
1347
|
+
out.append("return")
|
|
1348
|
+
if self.value is not None:
|
|
1349
|
+
out.append(" ")
|
|
1350
|
+
self.value.emit(out)
|
|
1351
|
+
out.append(";")
|
|
836
1352
|
|
|
837
1353
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1354
|
+
@dataclass(slots=True)
|
|
1355
|
+
class If(Stmt):
|
|
1356
|
+
"""JS if statement: if (cond) { ... } else { ... }"""
|
|
841
1357
|
|
|
842
|
-
|
|
843
|
-
|
|
1358
|
+
cond: Expr
|
|
1359
|
+
then: Sequence[Stmt]
|
|
1360
|
+
else_: Sequence[Stmt] = ()
|
|
844
1361
|
|
|
845
1362
|
@override
|
|
846
|
-
def emit(self) ->
|
|
847
|
-
|
|
848
|
-
|
|
1363
|
+
def emit(self, out: list[str]) -> None:
|
|
1364
|
+
out.append("if (")
|
|
1365
|
+
self.cond.emit(out)
|
|
1366
|
+
out.append(") {\n")
|
|
1367
|
+
for stmt in self.then:
|
|
1368
|
+
stmt.emit(out)
|
|
1369
|
+
out.append("\n")
|
|
1370
|
+
out.append("}")
|
|
1371
|
+
if self.else_:
|
|
1372
|
+
out.append(" else {\n")
|
|
1373
|
+
for stmt in self.else_:
|
|
1374
|
+
stmt.emit(out)
|
|
1375
|
+
out.append("\n")
|
|
1376
|
+
out.append("}")
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
@dataclass(slots=True)
|
|
1380
|
+
class ForOf(Stmt):
|
|
1381
|
+
"""JS for-of loop: for (const x of iter) { ... }
|
|
1382
|
+
|
|
1383
|
+
target can be a single name or array pattern for destructuring: [a, b]
|
|
1384
|
+
"""
|
|
849
1385
|
|
|
1386
|
+
target: str
|
|
1387
|
+
iter: Expr
|
|
1388
|
+
body: Sequence[Stmt]
|
|
850
1389
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1390
|
+
@override
|
|
1391
|
+
def emit(self, out: list[str]) -> None:
|
|
1392
|
+
out.append("for (const ")
|
|
1393
|
+
out.append(self.target)
|
|
1394
|
+
out.append(" of ")
|
|
1395
|
+
self.iter.emit(out)
|
|
1396
|
+
out.append(") {\n")
|
|
1397
|
+
for stmt in self.body:
|
|
1398
|
+
stmt.emit(out)
|
|
1399
|
+
out.append("\n")
|
|
1400
|
+
out.append("}")
|
|
858
1401
|
|
|
859
|
-
def __init__(
|
|
860
|
-
self,
|
|
861
|
-
tag: str | JSExpr,
|
|
862
|
-
props: Sequence[JSXProp | JSXSpreadProp] = (),
|
|
863
|
-
children: Sequence[str | JSExpr | JSXElement] = (),
|
|
864
|
-
):
|
|
865
|
-
self.tag = tag
|
|
866
|
-
self.props = props
|
|
867
|
-
self.children = children
|
|
868
1402
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
tag_code = self.tag if isinstance(self.tag, str) else self.tag.emit()
|
|
873
|
-
props_code = " ".join(p.emit() for p in self.props) if self.props else ""
|
|
874
|
-
if not self.children:
|
|
875
|
-
if props_code:
|
|
876
|
-
return f"<{tag_code} {props_code} />"
|
|
877
|
-
return f"<{tag_code} />"
|
|
878
|
-
# Open tag
|
|
879
|
-
open_tag = f"<{tag_code}>" if not props_code else f"<{tag_code} {props_code}>"
|
|
880
|
-
# Children
|
|
881
|
-
child_parts: list[str] = []
|
|
882
|
-
for c in self.children:
|
|
883
|
-
if isinstance(c, str):
|
|
884
|
-
child_parts.append(_escape_jsx_text(c))
|
|
885
|
-
elif isinstance(c, JSXElement) or (isinstance(c, JSExpr) and c.is_jsx):
|
|
886
|
-
child_parts.append(c.emit())
|
|
887
|
-
else:
|
|
888
|
-
child_parts.append("{" + c.emit() + "}")
|
|
889
|
-
inner = "".join(child_parts)
|
|
890
|
-
return f"{open_tag}{inner}</{tag_code}>"
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
class JSXFragment(JSExpr):
|
|
894
|
-
__slots__ = ("children",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
895
|
-
is_jsx: ClassVar[bool] = True
|
|
896
|
-
is_primary: ClassVar[bool] = True
|
|
897
|
-
children: Sequence[str | JSExpr | JSXElement]
|
|
898
|
-
|
|
899
|
-
def __init__(self, children: Sequence[str | JSExpr | JSXElement] = ()):
|
|
900
|
-
self.children = children
|
|
901
|
-
|
|
902
|
-
@override
|
|
903
|
-
def emit(self) -> str:
|
|
904
|
-
_check_not_interpreted_mode("JSXFragment")
|
|
905
|
-
if not self.children:
|
|
906
|
-
return "<></>"
|
|
907
|
-
parts: list[str] = []
|
|
908
|
-
for c in self.children:
|
|
909
|
-
if isinstance(c, str):
|
|
910
|
-
parts.append(_escape_jsx_text(c))
|
|
911
|
-
elif isinstance(c, JSXElement) or (isinstance(c, JSExpr) and c.is_jsx):
|
|
912
|
-
parts.append(c.emit())
|
|
913
|
-
else:
|
|
914
|
-
parts.append("{" + c.emit() + "}")
|
|
915
|
-
return "<>" + "".join(parts) + "</>"
|
|
1403
|
+
@dataclass(slots=True)
|
|
1404
|
+
class While(Stmt):
|
|
1405
|
+
"""JS while loop: while (cond) { ... }"""
|
|
916
1406
|
|
|
1407
|
+
cond: Expr
|
|
1408
|
+
body: Sequence[Stmt]
|
|
917
1409
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1410
|
+
@override
|
|
1411
|
+
def emit(self, out: list[str]) -> None:
|
|
1412
|
+
out.append("while (")
|
|
1413
|
+
self.cond.emit(out)
|
|
1414
|
+
out.append(") {\n")
|
|
1415
|
+
for stmt in self.body:
|
|
1416
|
+
stmt.emit(out)
|
|
1417
|
+
out.append("\n")
|
|
1418
|
+
out.append("}")
|
|
923
1419
|
|
|
924
|
-
def __init__(
|
|
925
|
-
self,
|
|
926
|
-
src: str,
|
|
927
|
-
default: str | None = None,
|
|
928
|
-
named: list[str | tuple[str, str]] | None = None,
|
|
929
|
-
):
|
|
930
|
-
self.src = src
|
|
931
|
-
self.default = default
|
|
932
|
-
self.named = named if named is not None else []
|
|
933
|
-
|
|
934
|
-
def emit(self) -> str:
|
|
935
|
-
parts: list[str] = []
|
|
936
|
-
if self.default:
|
|
937
|
-
parts.append(self.default)
|
|
938
|
-
if self.named:
|
|
939
|
-
named_parts: list[str] = []
|
|
940
|
-
for n in self.named:
|
|
941
|
-
if isinstance(n, tuple):
|
|
942
|
-
named_parts.append(f"{n[0]} as {n[1]}")
|
|
943
|
-
else:
|
|
944
|
-
named_parts.append(n)
|
|
945
|
-
if named_parts:
|
|
946
|
-
if self.default:
|
|
947
|
-
parts.append(",")
|
|
948
|
-
parts.append("{" + ", ".join(named_parts) + "}")
|
|
949
|
-
return f"import {' '.join(parts)} from {JSString(self.src).emit()};"
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
# -----------------------------
|
|
953
|
-
# Precedence helpers
|
|
954
|
-
# -----------------------------
|
|
955
|
-
|
|
956
|
-
PRIMARY_PRECEDENCE = 20
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
def op_precedence(op: str) -> int:
|
|
960
|
-
# Higher number = binds tighter
|
|
961
|
-
if op in {".", "[]", "()"}: # pseudo ops for primary contexts
|
|
962
|
-
return PRIMARY_PRECEDENCE
|
|
963
|
-
if op in {"!", "+u", "-u"}: # unary; we encode + and - as unary with +u/-u
|
|
964
|
-
return 17
|
|
965
|
-
if op in {"typeof", "await"}:
|
|
966
|
-
return 17
|
|
967
|
-
if op == "**":
|
|
968
|
-
return 16
|
|
969
|
-
if op in {"*", "/", "%"}:
|
|
970
|
-
return 15
|
|
971
|
-
if op in {"+", "-"}:
|
|
972
|
-
return 14
|
|
973
|
-
if op in {"<", "<=", ">", ">=", "===", "!=="}:
|
|
974
|
-
return 12
|
|
975
|
-
if op == "instanceof":
|
|
976
|
-
return 12
|
|
977
|
-
if op == "in":
|
|
978
|
-
return 12
|
|
979
|
-
if op == "&&":
|
|
980
|
-
return 7
|
|
981
|
-
if op == "||":
|
|
982
|
-
return 6
|
|
983
|
-
if op == "??":
|
|
984
|
-
return 6
|
|
985
|
-
if op == "?:": # ternary
|
|
986
|
-
return 4
|
|
987
|
-
if op == ",":
|
|
988
|
-
return 1
|
|
989
|
-
return 0
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
def op_is_right_associative(op: str) -> bool:
|
|
993
|
-
return op == "**"
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
def expr_precedence(e: JSExpr) -> int:
|
|
997
|
-
if isinstance(e, JSBinary):
|
|
998
|
-
return op_precedence(e.op)
|
|
999
|
-
if isinstance(e, JSUnary):
|
|
1000
|
-
# Distinguish unary + and - from binary precedence table by tag
|
|
1001
|
-
tag = "+u" if e.op == "+" else ("-u" if e.op == "-" else e.op)
|
|
1002
|
-
return op_precedence(tag)
|
|
1003
|
-
if isinstance(e, JSAwait):
|
|
1004
|
-
return op_precedence("await")
|
|
1005
|
-
if isinstance(e, JSTertiary):
|
|
1006
|
-
return op_precedence("?:")
|
|
1007
|
-
if isinstance(e, JSLogicalChain):
|
|
1008
|
-
return op_precedence(e.op)
|
|
1009
|
-
if isinstance(e, JSComma):
|
|
1010
|
-
return op_precedence(",")
|
|
1011
|
-
# Nullish now represented as JSBinary with op "??"; precedence resolved below
|
|
1012
|
-
if isinstance(e, (JSMember, JSSubscript, JSCall, JSMemberCall, JSNew)):
|
|
1013
|
-
return op_precedence(".")
|
|
1014
|
-
# Primary expressions (identifiers, literals, containers) don't need parens
|
|
1015
|
-
if e.is_primary:
|
|
1016
|
-
return PRIMARY_PRECEDENCE
|
|
1017
|
-
return 0
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
class JSBlock(JSStmt):
|
|
1021
|
-
__slots__ = ("body",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1022
|
-
body: Sequence[JSStmt]
|
|
1023
|
-
|
|
1024
|
-
def __init__(self, body: Sequence[JSStmt]):
|
|
1025
|
-
self.body = body
|
|
1026
|
-
|
|
1027
|
-
@override
|
|
1028
|
-
def emit(self) -> str:
|
|
1029
|
-
body_code = "\n".join(s.emit() for s in self.body)
|
|
1030
|
-
return f"{{\n{body_code}\n}}"
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
class JSAugAssign(JSStmt):
|
|
1034
|
-
__slots__ = ("name", "op", "value") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1035
|
-
name: str
|
|
1036
|
-
op: str
|
|
1037
|
-
value: JSExpr
|
|
1038
1420
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
self.value = value
|
|
1421
|
+
@dataclass(slots=True)
|
|
1422
|
+
class Break(Stmt):
|
|
1423
|
+
"""JS break statement."""
|
|
1043
1424
|
|
|
1044
1425
|
@override
|
|
1045
|
-
def emit(self) ->
|
|
1046
|
-
|
|
1426
|
+
def emit(self, out: list[str]) -> None:
|
|
1427
|
+
out.append("break;")
|
|
1047
1428
|
|
|
1048
1429
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
value: JSExpr
|
|
1053
|
-
|
|
1054
|
-
def __init__(self, name: str, value: JSExpr):
|
|
1055
|
-
self.name = name
|
|
1056
|
-
self.value = value
|
|
1430
|
+
@dataclass(slots=True)
|
|
1431
|
+
class Continue(Stmt):
|
|
1432
|
+
"""JS continue statement."""
|
|
1057
1433
|
|
|
1058
1434
|
@override
|
|
1059
|
-
def emit(self) ->
|
|
1060
|
-
|
|
1435
|
+
def emit(self, out: list[str]) -> None:
|
|
1436
|
+
out.append("continue;")
|
|
1061
1437
|
|
|
1062
1438
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1439
|
+
@dataclass(slots=True)
|
|
1440
|
+
class Assign(Stmt):
|
|
1441
|
+
"""JS assignment: let x = expr; or x = expr; or x += expr;
|
|
1066
1442
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1443
|
+
declare: "let", "const", or None (reassignment)
|
|
1444
|
+
op: None for =, or "+", "-", etc. for augmented assignment
|
|
1445
|
+
"""
|
|
1446
|
+
|
|
1447
|
+
target: str
|
|
1448
|
+
value: Expr
|
|
1449
|
+
declare: Lit["let", "const"] | None = None
|
|
1450
|
+
op: str | None = None # For augmented: +=, -=, etc.
|
|
1069
1451
|
|
|
1070
1452
|
@override
|
|
1071
|
-
def emit(self) ->
|
|
1072
|
-
|
|
1453
|
+
def emit(self, out: list[str]) -> None:
|
|
1454
|
+
if self.declare:
|
|
1455
|
+
out.append(self.declare)
|
|
1456
|
+
out.append(" ")
|
|
1457
|
+
out.append(self.target)
|
|
1458
|
+
if self.op:
|
|
1459
|
+
out.append(" ")
|
|
1460
|
+
out.append(self.op)
|
|
1461
|
+
out.append("= ")
|
|
1462
|
+
else:
|
|
1463
|
+
out.append(" = ")
|
|
1464
|
+
self.value.emit(out)
|
|
1465
|
+
out.append(";")
|
|
1073
1466
|
|
|
1074
1467
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1468
|
+
@dataclass(slots=True)
|
|
1469
|
+
class ExprStmt(Stmt):
|
|
1470
|
+
"""JS expression statement: expr;"""
|
|
1078
1471
|
|
|
1079
|
-
|
|
1080
|
-
self.stmts = stmts
|
|
1472
|
+
expr: Expr
|
|
1081
1473
|
|
|
1082
1474
|
@override
|
|
1083
|
-
def emit(self) ->
|
|
1084
|
-
|
|
1475
|
+
def emit(self, out: list[str]) -> None:
|
|
1476
|
+
self.expr.emit(out)
|
|
1477
|
+
out.append(";")
|
|
1085
1478
|
|
|
1086
1479
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
body: Sequence[JSStmt]
|
|
1091
|
-
orelse: Sequence[JSStmt]
|
|
1480
|
+
@dataclass(slots=True)
|
|
1481
|
+
class Block(Stmt):
|
|
1482
|
+
"""JS block: { ... } - a sequence of statements."""
|
|
1092
1483
|
|
|
1093
|
-
|
|
1094
|
-
self, test: JSExpr, body: Sequence[JSStmt], orelse: Sequence[JSStmt] = ()
|
|
1095
|
-
):
|
|
1096
|
-
self.test = test
|
|
1097
|
-
self.body = body
|
|
1098
|
-
self.orelse = orelse
|
|
1484
|
+
body: Sequence[Stmt]
|
|
1099
1485
|
|
|
1100
1486
|
@override
|
|
1101
|
-
def emit(self) ->
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1487
|
+
def emit(self, out: list[str]) -> None:
|
|
1488
|
+
out.append("{\n")
|
|
1489
|
+
for stmt in self.body:
|
|
1490
|
+
stmt.emit(out)
|
|
1491
|
+
out.append("\n")
|
|
1492
|
+
out.append("}")
|
|
1107
1493
|
|
|
1108
1494
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
iter_expr: JSExpr
|
|
1113
|
-
body: Sequence[JSStmt]
|
|
1495
|
+
@dataclass(slots=True)
|
|
1496
|
+
class StmtSequence(Stmt):
|
|
1497
|
+
"""A sequence of statements without block braces.
|
|
1114
1498
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
self.body = body
|
|
1499
|
+
Used for tuple unpacking where we need multiple statements
|
|
1500
|
+
but don't want to create a new scope.
|
|
1501
|
+
"""
|
|
1502
|
+
|
|
1503
|
+
body: Sequence[Stmt]
|
|
1121
1504
|
|
|
1122
1505
|
@override
|
|
1123
|
-
def emit(self) ->
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
return f"for (const {target} of {self.iter_expr.emit()}){{\n{body_code}\n}}"
|
|
1506
|
+
def emit(self, out: list[str]) -> None:
|
|
1507
|
+
for i, stmt in enumerate(self.body):
|
|
1508
|
+
stmt.emit(out)
|
|
1509
|
+
if i < len(self.body) - 1:
|
|
1510
|
+
out.append("\n")
|
|
1129
1511
|
|
|
1130
1512
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
body: Sequence[JSStmt]
|
|
1513
|
+
@dataclass(slots=True)
|
|
1514
|
+
class Throw(Stmt):
|
|
1515
|
+
"""JS throw statement: throw expr;"""
|
|
1135
1516
|
|
|
1136
|
-
|
|
1137
|
-
self.test = test
|
|
1138
|
-
self.body = body
|
|
1517
|
+
value: Expr
|
|
1139
1518
|
|
|
1140
1519
|
@override
|
|
1141
|
-
def emit(self) ->
|
|
1142
|
-
|
|
1143
|
-
|
|
1520
|
+
def emit(self, out: list[str]) -> None:
|
|
1521
|
+
out.append("throw ")
|
|
1522
|
+
self.value.emit(out)
|
|
1523
|
+
out.append(";")
|
|
1524
|
+
|
|
1144
1525
|
|
|
1526
|
+
@dataclass(slots=True)
|
|
1527
|
+
class TryStmt(Stmt):
|
|
1528
|
+
"""JS try/catch/finally statement.
|
|
1529
|
+
|
|
1530
|
+
try { body } catch (param) { handler } finally { finalizer }
|
|
1531
|
+
"""
|
|
1145
1532
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1533
|
+
body: Sequence[Stmt]
|
|
1534
|
+
catch_param: str | None = None # None for bare except
|
|
1535
|
+
catch_body: Sequence[Stmt] | None = None
|
|
1536
|
+
finally_body: Sequence[Stmt] | None = None
|
|
1148
1537
|
|
|
1149
1538
|
@override
|
|
1150
|
-
def emit(self) ->
|
|
1151
|
-
|
|
1539
|
+
def emit(self, out: list[str]) -> None:
|
|
1540
|
+
out.append("try {\n")
|
|
1541
|
+
for stmt in self.body:
|
|
1542
|
+
stmt.emit(out)
|
|
1543
|
+
out.append("\n")
|
|
1544
|
+
out.append("}")
|
|
1545
|
+
|
|
1546
|
+
if self.catch_body is not None:
|
|
1547
|
+
if self.catch_param:
|
|
1548
|
+
out.append(f" catch ({self.catch_param}) {{\n")
|
|
1549
|
+
else:
|
|
1550
|
+
out.append(" catch {\n")
|
|
1551
|
+
for stmt in self.catch_body:
|
|
1552
|
+
stmt.emit(out)
|
|
1553
|
+
out.append("\n")
|
|
1554
|
+
out.append("}")
|
|
1152
1555
|
|
|
1556
|
+
if self.finally_body is not None:
|
|
1557
|
+
out.append(" finally {\n")
|
|
1558
|
+
for stmt in self.finally_body:
|
|
1559
|
+
stmt.emit(out)
|
|
1560
|
+
out.append("\n")
|
|
1561
|
+
out.append("}")
|
|
1153
1562
|
|
|
1154
|
-
class JSContinue(JSStmt):
|
|
1155
|
-
__slots__ = () # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1156
1563
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1564
|
+
@dataclass(slots=True)
|
|
1565
|
+
class Function(Expr):
|
|
1566
|
+
"""JS function: function name(params) { ... } or async function ...
|
|
1160
1567
|
|
|
1568
|
+
For statement-bodied functions. Use Arrow for expression-bodied.
|
|
1569
|
+
"""
|
|
1161
1570
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
return True
|
|
1167
|
-
return False
|
|
1571
|
+
params: Sequence[str]
|
|
1572
|
+
body: Sequence[Stmt]
|
|
1573
|
+
name: str | None = None
|
|
1574
|
+
is_async: bool = False
|
|
1168
1575
|
|
|
1576
|
+
@override
|
|
1577
|
+
def emit(self, out: list[str]) -> None:
|
|
1578
|
+
if self.is_async:
|
|
1579
|
+
out.append("async ")
|
|
1580
|
+
out.append("function")
|
|
1581
|
+
if self.name:
|
|
1582
|
+
out.append(" ")
|
|
1583
|
+
out.append(self.name)
|
|
1584
|
+
out.append("(")
|
|
1585
|
+
out.append(", ".join(self.params))
|
|
1586
|
+
out.append(") {\n")
|
|
1587
|
+
for stmt in self.body:
|
|
1588
|
+
stmt.emit(out)
|
|
1589
|
+
out.append("\n")
|
|
1590
|
+
out.append("}")
|
|
1169
1591
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1592
|
+
@override
|
|
1593
|
+
def render(self) -> VDOMNode:
|
|
1594
|
+
raise TypeError("Function cannot be rendered as VDOMExpr")
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
Node: TypeAlias = Primitive | Expr | PulseNode
|
|
1598
|
+
Child: TypeAlias = Node | Iterable[Node]
|
|
1599
|
+
Children: TypeAlias = Sequence[Child]
|
|
1600
|
+
Prop: TypeAlias = Primitive | Expr
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
# =============================================================================
|
|
1604
|
+
# Emit logic
|
|
1605
|
+
# =============================================================================
|
|
1606
|
+
|
|
1607
|
+
|
|
1608
|
+
Emittable: TypeAlias = Expr | Stmt
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
def emit(node: Emittable) -> str:
|
|
1612
|
+
"""Emit an expression or statement as JavaScript/JSX code."""
|
|
1613
|
+
out: list[str] = []
|
|
1614
|
+
node.emit(out)
|
|
1615
|
+
return "".join(out)
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
# Operator precedence table (higher = binds tighter)
|
|
1619
|
+
_PRECEDENCE: dict[str, int] = {
|
|
1620
|
+
# Primary
|
|
1621
|
+
".": 20,
|
|
1622
|
+
"[]": 20,
|
|
1623
|
+
"()": 20,
|
|
1624
|
+
# Unary
|
|
1625
|
+
"!": 17,
|
|
1626
|
+
"+u": 17,
|
|
1627
|
+
"-u": 17,
|
|
1628
|
+
"typeof": 17,
|
|
1629
|
+
"await": 17,
|
|
1630
|
+
# Exponentiation (right-assoc)
|
|
1631
|
+
"**": 16,
|
|
1632
|
+
# Multiplicative
|
|
1633
|
+
"*": 15,
|
|
1634
|
+
"/": 15,
|
|
1635
|
+
"%": 15,
|
|
1636
|
+
# Additive
|
|
1637
|
+
"+": 14,
|
|
1638
|
+
"-": 14,
|
|
1639
|
+
# Relational
|
|
1640
|
+
"<": 12,
|
|
1641
|
+
"<=": 12,
|
|
1642
|
+
">": 12,
|
|
1643
|
+
">=": 12,
|
|
1644
|
+
"===": 12,
|
|
1645
|
+
"!==": 12,
|
|
1646
|
+
"instanceof": 12,
|
|
1647
|
+
"in": 12,
|
|
1648
|
+
# Logical
|
|
1649
|
+
"&&": 7,
|
|
1650
|
+
"||": 6,
|
|
1651
|
+
"??": 6,
|
|
1652
|
+
# Ternary
|
|
1653
|
+
"?:": 4,
|
|
1654
|
+
# Comma
|
|
1655
|
+
",": 1,
|
|
1656
|
+
}
|
|
1206
1657
|
|
|
1658
|
+
_RIGHT_ASSOC = {"**"}
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
def _escape_string(s: str) -> str:
|
|
1662
|
+
"""Escape for double-quoted JS string literals."""
|
|
1663
|
+
return (
|
|
1664
|
+
s.replace("\\", "\\\\")
|
|
1665
|
+
.replace('"', '\\"')
|
|
1666
|
+
.replace("\n", "\\n")
|
|
1667
|
+
.replace("\r", "\\r")
|
|
1668
|
+
.replace("\t", "\\t")
|
|
1669
|
+
.replace("\b", "\\b")
|
|
1670
|
+
.replace("\f", "\\f")
|
|
1671
|
+
.replace("\v", "\\v")
|
|
1672
|
+
.replace("\x00", "\\x00")
|
|
1673
|
+
.replace("\u2028", "\\u2028")
|
|
1674
|
+
.replace("\u2029", "\\u2029")
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def _escape_template(s: str) -> str:
|
|
1679
|
+
"""Escape for template literal strings."""
|
|
1680
|
+
return (
|
|
1681
|
+
s.replace("\\", "\\\\")
|
|
1682
|
+
.replace("`", "\\`")
|
|
1683
|
+
.replace("${", "\\${")
|
|
1684
|
+
.replace("\n", "\\n")
|
|
1685
|
+
.replace("\r", "\\r")
|
|
1686
|
+
.replace("\t", "\\t")
|
|
1687
|
+
.replace("\b", "\\b")
|
|
1688
|
+
.replace("\f", "\\f")
|
|
1689
|
+
.replace("\v", "\\v")
|
|
1690
|
+
.replace("\x00", "\\x00")
|
|
1691
|
+
.replace("\u2028", "\\u2028")
|
|
1692
|
+
.replace("\u2029", "\\u2029")
|
|
1693
|
+
)
|
|
1694
|
+
|
|
1695
|
+
|
|
1696
|
+
def _escape_jsx_text(s: str) -> str:
|
|
1697
|
+
"""Escape text content for JSX."""
|
|
1698
|
+
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
def _escape_jsx_attr(s: str) -> str:
|
|
1702
|
+
"""Escape attribute value for JSX."""
|
|
1703
|
+
return s.replace("&", "&").replace('"', """)
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
def _emit_paren(node: Expr, parent_op: str, side: str, out: list[str]) -> None:
|
|
1707
|
+
"""Emit child with parens if needed for precedence."""
|
|
1708
|
+
# Ternary as child of binary always needs parens
|
|
1709
|
+
needs_parens = False
|
|
1710
|
+
if isinstance(node, Ternary):
|
|
1711
|
+
needs_parens = True
|
|
1712
|
+
else:
|
|
1713
|
+
child_prec = node.precedence()
|
|
1714
|
+
parent_prec = _PRECEDENCE.get(parent_op, 0)
|
|
1715
|
+
if child_prec < parent_prec:
|
|
1716
|
+
needs_parens = True
|
|
1717
|
+
elif child_prec == parent_prec and isinstance(node, Binary):
|
|
1718
|
+
# Handle associativity
|
|
1719
|
+
if parent_op in _RIGHT_ASSOC:
|
|
1720
|
+
needs_parens = side == "left"
|
|
1721
|
+
else:
|
|
1722
|
+
needs_parens = side == "right"
|
|
1207
1723
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1724
|
+
if needs_parens:
|
|
1725
|
+
out.append("(")
|
|
1726
|
+
node.emit(out)
|
|
1727
|
+
out.append(")")
|
|
1728
|
+
else:
|
|
1729
|
+
node.emit(out)
|
|
1213
1730
|
|
|
1214
1731
|
|
|
1215
|
-
def
|
|
1216
|
-
|
|
1732
|
+
def _emit_primary(node: Expr, out: list[str]) -> None:
|
|
1733
|
+
"""Emit with parens if not primary precedence."""
|
|
1734
|
+
if node.precedence() < 20 or isinstance(node, Ternary):
|
|
1735
|
+
out.append("(")
|
|
1736
|
+
node.emit(out)
|
|
1737
|
+
out.append(")")
|
|
1738
|
+
else:
|
|
1739
|
+
node.emit(out)
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
def _emit_value(value: Any, out: list[str]) -> None:
|
|
1743
|
+
"""Emit a Python value as JavaScript literal."""
|
|
1744
|
+
if value is None:
|
|
1745
|
+
out.append("null")
|
|
1746
|
+
elif isinstance(value, bool):
|
|
1747
|
+
out.append("true" if value else "false")
|
|
1748
|
+
elif isinstance(value, str):
|
|
1749
|
+
out.append('"')
|
|
1750
|
+
out.append(_escape_string(value))
|
|
1751
|
+
out.append('"')
|
|
1752
|
+
elif isinstance(value, (int, float)):
|
|
1753
|
+
out.append(str(value))
|
|
1754
|
+
elif isinstance(value, dt.datetime):
|
|
1755
|
+
out.append("new Date(")
|
|
1756
|
+
out.append(str(int(value.timestamp() * 1000)))
|
|
1757
|
+
out.append(")")
|
|
1758
|
+
elif isinstance(value, list):
|
|
1759
|
+
out.append("[")
|
|
1760
|
+
for i, v in enumerate(value): # pyright: ignore[reportUnknownArgumentType]
|
|
1761
|
+
if i > 0:
|
|
1762
|
+
out.append(", ")
|
|
1763
|
+
_emit_value(v, out)
|
|
1764
|
+
out.append("]")
|
|
1765
|
+
elif isinstance(value, dict):
|
|
1766
|
+
out.append("{")
|
|
1767
|
+
for i, (k, v) in enumerate(value.items()): # pyright: ignore[reportUnknownArgumentType]
|
|
1768
|
+
if i > 0:
|
|
1769
|
+
out.append(", ")
|
|
1770
|
+
out.append('"')
|
|
1771
|
+
out.append(_escape_string(str(k))) # pyright: ignore[reportUnknownArgumentType]
|
|
1772
|
+
out.append('": ')
|
|
1773
|
+
_emit_value(v, out)
|
|
1774
|
+
out.append("}")
|
|
1775
|
+
elif isinstance(value, set):
|
|
1776
|
+
out.append("new Set([")
|
|
1777
|
+
for i, v in enumerate(value): # pyright: ignore[reportUnknownArgumentType]
|
|
1778
|
+
if i > 0:
|
|
1779
|
+
out.append(", ")
|
|
1780
|
+
_emit_value(v, out)
|
|
1781
|
+
out.append("])")
|
|
1782
|
+
else:
|
|
1783
|
+
raise TypeError(f"Cannot emit {type(value).__name__} as JavaScript")
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
def _emit_jsx_prop(name: str, value: Prop, out: list[str]) -> None:
|
|
1787
|
+
"""Emit a single JSX prop."""
|
|
1788
|
+
# Spread props
|
|
1789
|
+
if isinstance(value, Spread):
|
|
1790
|
+
out.append("{...")
|
|
1791
|
+
value.expr.emit(out)
|
|
1792
|
+
out.append("}")
|
|
1793
|
+
return
|
|
1794
|
+
# Expression nodes
|
|
1795
|
+
if isinstance(value, Expr):
|
|
1796
|
+
# String literals can use compact form
|
|
1797
|
+
if isinstance(value, Literal) and isinstance(value.value, str):
|
|
1798
|
+
out.append(name)
|
|
1799
|
+
out.append('="')
|
|
1800
|
+
out.append(_escape_jsx_attr(value.value))
|
|
1801
|
+
out.append('"')
|
|
1802
|
+
else:
|
|
1803
|
+
out.append(name)
|
|
1804
|
+
out.append("={")
|
|
1805
|
+
value.emit(out)
|
|
1806
|
+
out.append("}")
|
|
1807
|
+
return
|
|
1808
|
+
# Primitives
|
|
1809
|
+
if value is None:
|
|
1810
|
+
out.append(name)
|
|
1811
|
+
out.append("={null}")
|
|
1812
|
+
return
|
|
1813
|
+
if isinstance(value, bool):
|
|
1814
|
+
out.append(name)
|
|
1815
|
+
out.append("={true}" if value else "={false}")
|
|
1816
|
+
return
|
|
1817
|
+
if isinstance(value, str):
|
|
1818
|
+
out.append(name)
|
|
1819
|
+
out.append('="')
|
|
1820
|
+
out.append(_escape_jsx_attr(value))
|
|
1821
|
+
out.append('"')
|
|
1822
|
+
return
|
|
1823
|
+
if isinstance(value, (int, float)):
|
|
1824
|
+
out.append(name)
|
|
1825
|
+
out.append("={")
|
|
1826
|
+
out.append(str(value))
|
|
1827
|
+
out.append("}")
|
|
1828
|
+
return
|
|
1829
|
+
# Value
|
|
1830
|
+
if isinstance(value, Value):
|
|
1831
|
+
out.append(name)
|
|
1832
|
+
out.append("={")
|
|
1833
|
+
_emit_value(value.value, out)
|
|
1834
|
+
out.append("}")
|
|
1835
|
+
return
|
|
1836
|
+
# Nested Element (render prop)
|
|
1837
|
+
if isinstance(value, Element):
|
|
1838
|
+
out.append(name)
|
|
1839
|
+
out.append("={")
|
|
1840
|
+
value.emit(out)
|
|
1841
|
+
out.append("}")
|
|
1842
|
+
return
|
|
1843
|
+
# Callable - error
|
|
1844
|
+
if callable(value):
|
|
1845
|
+
raise TypeError("Cannot emit callable in transpile context")
|
|
1846
|
+
# Fallback for other data
|
|
1847
|
+
out.append(name)
|
|
1848
|
+
out.append("={")
|
|
1849
|
+
_emit_value(value, out)
|
|
1850
|
+
out.append("}")
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
def _emit_jsx_child(child: Node, out: list[str]) -> None:
|
|
1854
|
+
"""Emit a single JSX child."""
|
|
1855
|
+
# Primitives
|
|
1856
|
+
if child is None or isinstance(child, bool):
|
|
1857
|
+
return # React ignores None/bool
|
|
1858
|
+
if isinstance(child, str):
|
|
1859
|
+
out.append(_escape_jsx_text(child))
|
|
1860
|
+
return
|
|
1861
|
+
if isinstance(child, (int, float)):
|
|
1862
|
+
out.append("{")
|
|
1863
|
+
out.append(str(child))
|
|
1864
|
+
out.append("}")
|
|
1865
|
+
return
|
|
1866
|
+
if isinstance(child, dt.datetime):
|
|
1867
|
+
out.append("{")
|
|
1868
|
+
_emit_value(child, out)
|
|
1869
|
+
out.append("}")
|
|
1870
|
+
return
|
|
1871
|
+
# PulseNode - error
|
|
1872
|
+
if isinstance(child, PulseNode):
|
|
1873
|
+
fn_name = getattr(child.fn, "__name__", "unknown")
|
|
1874
|
+
raise TypeError(
|
|
1875
|
+
f"Cannot transpile PulseNode '{fn_name}'. "
|
|
1876
|
+
+ "Server components must be rendered, not transpiled."
|
|
1877
|
+
)
|
|
1878
|
+
# Element - recurse
|
|
1879
|
+
if isinstance(child, Element):
|
|
1880
|
+
child.emit(out)
|
|
1881
|
+
return
|
|
1882
|
+
# Spread - emit as {expr} without the spread operator (arrays are already iterable in JSX)
|
|
1883
|
+
if isinstance(child, Spread):
|
|
1884
|
+
out.append("{")
|
|
1885
|
+
child.expr.emit(out)
|
|
1886
|
+
out.append("}")
|
|
1887
|
+
return
|
|
1888
|
+
# Expr
|
|
1889
|
+
if isinstance(child, Expr):
|
|
1890
|
+
out.append("{")
|
|
1891
|
+
child.emit(out)
|
|
1892
|
+
out.append("}")
|
|
1893
|
+
return
|
|
1894
|
+
# Value
|
|
1895
|
+
if isinstance(child, Value):
|
|
1896
|
+
out.append("{")
|
|
1897
|
+
_emit_value(child.value, out)
|
|
1898
|
+
out.append("}")
|
|
1899
|
+
return
|
|
1900
|
+
raise TypeError(f"Cannot emit {type(child).__name__} as JSX child")
|