pulse-framework 0.1.46__py3-none-any.whl → 0.1.48__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 +9 -23
- pulse/app.py +6 -25
- pulse/cli/processes.py +1 -0
- pulse/codegen/codegen.py +43 -88
- pulse/codegen/js.py +35 -5
- pulse/codegen/templates/route.py +341 -254
- pulse/form.py +1 -1
- pulse/helpers.py +51 -27
- pulse/hooks/core.py +2 -2
- pulse/hooks/effects.py +1 -1
- pulse/hooks/init.py +2 -1
- pulse/hooks/setup.py +1 -1
- pulse/hooks/stable.py +2 -2
- pulse/hooks/states.py +2 -2
- pulse/html/props.py +3 -2
- pulse/html/tags.py +135 -0
- pulse/html/tags.pyi +4 -0
- pulse/js/__init__.py +110 -0
- pulse/js/__init__.pyi +95 -0
- pulse/js/_types.py +297 -0
- pulse/js/array.py +253 -0
- pulse/js/console.py +47 -0
- pulse/js/date.py +113 -0
- pulse/js/document.py +138 -0
- pulse/js/error.py +139 -0
- pulse/js/json.py +62 -0
- pulse/js/map.py +84 -0
- pulse/js/math.py +66 -0
- pulse/js/navigator.py +76 -0
- pulse/js/number.py +54 -0
- pulse/js/object.py +173 -0
- pulse/js/promise.py +150 -0
- pulse/js/regexp.py +54 -0
- pulse/js/set.py +109 -0
- pulse/js/string.py +35 -0
- pulse/js/weakmap.py +50 -0
- pulse/js/weakset.py +45 -0
- pulse/js/window.py +199 -0
- pulse/messages.py +22 -3
- pulse/proxy.py +21 -8
- pulse/react_component.py +167 -14
- pulse/reactive_extensions.py +5 -5
- pulse/render_session.py +144 -34
- pulse/renderer.py +80 -115
- pulse/routing.py +1 -18
- pulse/transpiler/__init__.py +131 -0
- pulse/transpiler/builtins.py +731 -0
- pulse/transpiler/constants.py +110 -0
- pulse/transpiler/context.py +26 -0
- pulse/transpiler/errors.py +2 -0
- pulse/transpiler/function.py +250 -0
- pulse/transpiler/ids.py +16 -0
- pulse/transpiler/imports.py +409 -0
- pulse/transpiler/js_module.py +274 -0
- pulse/transpiler/modules/__init__.py +30 -0
- pulse/transpiler/modules/asyncio.py +38 -0
- pulse/transpiler/modules/json.py +20 -0
- pulse/transpiler/modules/math.py +320 -0
- pulse/transpiler/modules/re.py +466 -0
- pulse/transpiler/modules/tags.py +268 -0
- pulse/transpiler/modules/typing.py +59 -0
- pulse/transpiler/nodes.py +1216 -0
- pulse/transpiler/py_module.py +119 -0
- pulse/transpiler/transpiler.py +938 -0
- pulse/transpiler/utils.py +4 -0
- pulse/vdom.py +112 -6
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/METADATA +1 -1
- pulse_framework-0.1.48.dist-info/RECORD +119 -0
- pulse/codegen/imports.py +0 -204
- pulse/css.py +0 -155
- pulse_framework-0.1.46.dist-info/RECORD +0 -80
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1216 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
from typing import Any, ClassVar, TypeVar, cast, overload, override
|
|
7
|
+
|
|
8
|
+
from pulse.transpiler.context import is_interpreted_mode
|
|
9
|
+
from pulse.transpiler.errors import JSCompilationError
|
|
10
|
+
|
|
11
|
+
# Global registry: id(value) -> JSExpr
|
|
12
|
+
# Used by JSExpr.of() to resolve registered Python values
|
|
13
|
+
JSEXPR_REGISTRY: dict[int, JSExpr] = {}
|
|
14
|
+
|
|
15
|
+
ALLOWED_BINOPS: dict[type[ast.operator], str] = {
|
|
16
|
+
ast.Add: "+",
|
|
17
|
+
ast.Sub: "-",
|
|
18
|
+
ast.Mult: "*",
|
|
19
|
+
ast.Div: "/",
|
|
20
|
+
ast.Mod: "%",
|
|
21
|
+
ast.Pow: "**",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ALLOWED_UNOPS: dict[type[ast.unaryop], str] = {
|
|
25
|
+
ast.UAdd: "+",
|
|
26
|
+
ast.USub: "-",
|
|
27
|
+
ast.Not: "!",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ALLOWED_CMPOPS: dict[type[ast.cmpop], str] = {
|
|
31
|
+
ast.Eq: "===",
|
|
32
|
+
ast.NotEq: "!==",
|
|
33
|
+
ast.Lt: "<",
|
|
34
|
+
ast.LtE: "<=",
|
|
35
|
+
ast.Gt: ">",
|
|
36
|
+
ast.GtE: ">=",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
###############################################################################
|
|
41
|
+
# JS AST
|
|
42
|
+
###############################################################################
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class JSNode(ABC):
|
|
46
|
+
__slots__ = () # pyright: ignore[reportUnannotatedClassAttribute]
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def emit(self) -> str:
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class JSExpr(JSNode, ABC):
|
|
54
|
+
"""Base class for JavaScript expressions.
|
|
55
|
+
|
|
56
|
+
Subclasses can override emit_call, emit_subscript, and emit_getattr to
|
|
57
|
+
customize how the expression behaves when called, indexed, or accessed.
|
|
58
|
+
This enables extensibility for things like JSX elements.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
__slots__ = () # pyright: ignore[reportUnannotatedClassAttribute]
|
|
62
|
+
|
|
63
|
+
# Set to True for expressions that emit JSX (should not be wrapped in {})
|
|
64
|
+
is_jsx: ClassVar[bool] = False
|
|
65
|
+
|
|
66
|
+
# Set to True for expressions that have primary precedence (identifiers, literals, etc.)
|
|
67
|
+
# Used by expr_precedence to determine if parenthesization is needed
|
|
68
|
+
is_primary: ClassVar[bool] = False
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def of(cls, value: Any) -> JSExpr:
|
|
72
|
+
"""Convert a Python value to a JSExpr.
|
|
73
|
+
|
|
74
|
+
Resolution order:
|
|
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)
|
|
79
|
+
"""
|
|
80
|
+
# Already a JSExpr
|
|
81
|
+
if isinstance(value, JSExpr):
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
# Check registry (for modules, functions, etc.)
|
|
85
|
+
if (expr := JSEXPR_REGISTRY.get(id(value))) is not None:
|
|
86
|
+
return expr
|
|
87
|
+
|
|
88
|
+
# Primitives
|
|
89
|
+
if isinstance(value, str):
|
|
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()
|
|
99
|
+
|
|
100
|
+
# Collections
|
|
101
|
+
if isinstance(value, (list, tuple)):
|
|
102
|
+
return JSArray([cls.of(v) for v in value])
|
|
103
|
+
if isinstance(value, dict):
|
|
104
|
+
props = [JSProp(JSString(str(k)), cls.of(v)) for k, v in value.items()] # pyright: ignore[reportUnknownArgumentType]
|
|
105
|
+
return JSObjectExpr(props)
|
|
106
|
+
|
|
107
|
+
raise TypeError(f"Cannot convert {type(value).__name__} to JSExpr")
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def register(cls, value: Any, expr: JSExpr | Callable[..., JSExpr]) -> None:
|
|
111
|
+
"""Register a Python value for conversion via JSExpr.of().
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
value: The Python object to register (function, constant, etc.)
|
|
115
|
+
expr: Either a JSExpr or a Callable[..., JSExpr] (will be wrapped in JSTransformer)
|
|
116
|
+
"""
|
|
117
|
+
if callable(expr) and not isinstance(expr, JSExpr):
|
|
118
|
+
expr = JSTransformer(expr)
|
|
119
|
+
JSEXPR_REGISTRY[id(value)] = expr
|
|
120
|
+
|
|
121
|
+
def emit_call(self, args: list[Any], kwargs: dict[str, Any]) -> JSExpr:
|
|
122
|
+
"""Called when this expression is used as a function: expr(args).
|
|
123
|
+
|
|
124
|
+
Override to customize call behavior. Default converts args/kwargs to
|
|
125
|
+
JSExpr via JSExpr.of() and emits JSCall(self, args).
|
|
126
|
+
Rejects keyword arguments by default.
|
|
127
|
+
|
|
128
|
+
Args receive raw Python values. Use JSExpr.of() to convert as needed.
|
|
129
|
+
|
|
130
|
+
The kwargs dict maps prop names to values:
|
|
131
|
+
- "propName" -> value for named kwargs
|
|
132
|
+
- "$spread{N}" -> JSSpread(expr) for **spread kwargs (already JSExpr)
|
|
133
|
+
|
|
134
|
+
Dict order is preserved, so iteration order matches source order.
|
|
135
|
+
"""
|
|
136
|
+
if kwargs:
|
|
137
|
+
raise JSCompilationError(
|
|
138
|
+
"Keyword arguments not supported in default function call"
|
|
139
|
+
)
|
|
140
|
+
return JSCall(self, [JSExpr.of(a) for a in args])
|
|
141
|
+
|
|
142
|
+
def emit_subscript(self, indices: list[Any]) -> JSExpr:
|
|
143
|
+
"""Called when this expression is indexed: expr[a, b, c].
|
|
144
|
+
|
|
145
|
+
Override to customize subscript behavior. Default requires single index
|
|
146
|
+
and emits JSSubscript(self, index).
|
|
147
|
+
|
|
148
|
+
Args receive raw Python values. Use JSExpr.of() to convert as needed.
|
|
149
|
+
"""
|
|
150
|
+
if len(indices) != 1:
|
|
151
|
+
raise JSCompilationError("Multiple indices not supported in subscript")
|
|
152
|
+
return JSSubscript(self, JSExpr.of(indices[0]))
|
|
153
|
+
|
|
154
|
+
def emit_getattr(self, attr: str) -> JSExpr:
|
|
155
|
+
"""Called when an attribute is accessed: expr.attr.
|
|
156
|
+
|
|
157
|
+
Override to customize attribute access. Default emits JSMember(self, attr).
|
|
158
|
+
"""
|
|
159
|
+
return JSMember(self, attr)
|
|
160
|
+
|
|
161
|
+
def __getattr__(self, attr: str) -> JSExpr:
|
|
162
|
+
"""Support attribute access at Python runtime.
|
|
163
|
+
|
|
164
|
+
Allows: expr.attr where expr is any JSExpr.
|
|
165
|
+
Delegates to emit_getattr for transpilation.
|
|
166
|
+
"""
|
|
167
|
+
return self.emit_getattr(attr)
|
|
168
|
+
|
|
169
|
+
def __call__(self, *args: Any, **kwargs: Any) -> JSExpr:
|
|
170
|
+
"""Support function calls at Python runtime.
|
|
171
|
+
|
|
172
|
+
Allows: expr(*args, **kwargs) where expr is any JSExpr.
|
|
173
|
+
Delegates to emit_call for transpilation.
|
|
174
|
+
"""
|
|
175
|
+
return self.emit_call(list(args), kwargs)
|
|
176
|
+
|
|
177
|
+
def __getitem__(self, key: Any) -> JSExpr:
|
|
178
|
+
"""Support subscript access at Python runtime.
|
|
179
|
+
|
|
180
|
+
Allows: expr[key] where expr is any JSExpr.
|
|
181
|
+
Delegates to emit_subscript for transpilation.
|
|
182
|
+
"""
|
|
183
|
+
return self.emit_subscript([key])
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class JSStmt(JSNode, ABC):
|
|
187
|
+
__slots__ = () # pyright: ignore[reportUnannotatedClassAttribute]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class JSIdentifier(JSExpr):
|
|
191
|
+
__slots__ = ("name",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
192
|
+
is_primary: ClassVar[bool] = True
|
|
193
|
+
name: str
|
|
194
|
+
|
|
195
|
+
def __init__(self, name: str):
|
|
196
|
+
self.name = name
|
|
197
|
+
|
|
198
|
+
@override
|
|
199
|
+
def emit(self) -> str:
|
|
200
|
+
return self.name
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class JSString(JSExpr):
|
|
204
|
+
__slots__ = ("value",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
205
|
+
is_primary: ClassVar[bool] = True
|
|
206
|
+
value: str
|
|
207
|
+
|
|
208
|
+
def __init__(self, value: str):
|
|
209
|
+
self.value = value
|
|
210
|
+
|
|
211
|
+
@override
|
|
212
|
+
def emit(self) -> str:
|
|
213
|
+
s = self.value
|
|
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}"'
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class JSNumber(JSExpr):
|
|
232
|
+
__slots__ = ("value",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
233
|
+
is_primary: ClassVar[bool] = True
|
|
234
|
+
value: int | float
|
|
235
|
+
|
|
236
|
+
def __init__(self, value: int | float):
|
|
237
|
+
self.value = value
|
|
238
|
+
|
|
239
|
+
@override
|
|
240
|
+
def emit(self) -> str:
|
|
241
|
+
return str(self.value)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class JSBoolean(JSExpr):
|
|
245
|
+
__slots__ = ("value",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
246
|
+
is_primary: ClassVar[bool] = True
|
|
247
|
+
value: bool
|
|
248
|
+
|
|
249
|
+
def __init__(self, value: bool):
|
|
250
|
+
self.value = value
|
|
251
|
+
|
|
252
|
+
@override
|
|
253
|
+
def emit(self) -> str:
|
|
254
|
+
return "true" if self.value else "false"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class JSNull(JSExpr):
|
|
258
|
+
__slots__ = () # pyright: ignore[reportUnannotatedClassAttribute]
|
|
259
|
+
is_primary: ClassVar[bool] = True
|
|
260
|
+
|
|
261
|
+
@override
|
|
262
|
+
def emit(self) -> str:
|
|
263
|
+
return "null"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class JSUndefined(JSExpr):
|
|
267
|
+
__slots__ = () # pyright: ignore[reportUnannotatedClassAttribute]
|
|
268
|
+
is_primary: ClassVar[bool] = True
|
|
269
|
+
|
|
270
|
+
@override
|
|
271
|
+
def emit(self) -> str:
|
|
272
|
+
return "undefined"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class JSArray(JSExpr):
|
|
276
|
+
__slots__ = ("elements",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
277
|
+
is_primary: ClassVar[bool] = True
|
|
278
|
+
elements: Sequence[JSExpr]
|
|
279
|
+
|
|
280
|
+
def __init__(self, elements: Sequence[JSExpr]):
|
|
281
|
+
self.elements = elements
|
|
282
|
+
|
|
283
|
+
@override
|
|
284
|
+
def emit(self) -> str:
|
|
285
|
+
inner = ", ".join(e.emit() for e in self.elements)
|
|
286
|
+
return f"[{inner}]"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class JSSpread(JSExpr):
|
|
290
|
+
__slots__ = ("expr",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
291
|
+
expr: JSExpr
|
|
292
|
+
|
|
293
|
+
def __init__(self, expr: JSExpr):
|
|
294
|
+
self.expr = expr
|
|
295
|
+
|
|
296
|
+
@override
|
|
297
|
+
def emit(self) -> str:
|
|
298
|
+
return f"...{self.expr.emit()}"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class JSProp(JSExpr):
|
|
302
|
+
__slots__ = ("key", "value") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
303
|
+
key: JSString
|
|
304
|
+
value: JSExpr
|
|
305
|
+
|
|
306
|
+
def __init__(self, key: JSString, value: JSExpr):
|
|
307
|
+
self.key = key
|
|
308
|
+
self.value = value
|
|
309
|
+
|
|
310
|
+
@override
|
|
311
|
+
def emit(self) -> str:
|
|
312
|
+
return f"{self.key.emit()}: {self.value.emit()}"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class JSComputedProp(JSExpr):
|
|
316
|
+
__slots__ = ("key", "value") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
317
|
+
key: JSExpr
|
|
318
|
+
value: JSExpr
|
|
319
|
+
|
|
320
|
+
def __init__(self, key: JSExpr, value: JSExpr):
|
|
321
|
+
self.key = key
|
|
322
|
+
self.value = value
|
|
323
|
+
|
|
324
|
+
@override
|
|
325
|
+
def emit(self) -> str:
|
|
326
|
+
return f"[{self.key.emit()}]: {self.value.emit()}"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class JSObjectExpr(JSExpr):
|
|
330
|
+
__slots__ = ("props",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
331
|
+
is_primary: ClassVar[bool] = True
|
|
332
|
+
props: Sequence[JSProp | JSComputedProp | JSSpread]
|
|
333
|
+
|
|
334
|
+
def __init__(self, props: Sequence[JSProp | JSComputedProp | JSSpread]):
|
|
335
|
+
self.props = props
|
|
336
|
+
|
|
337
|
+
@override
|
|
338
|
+
def emit(self) -> str:
|
|
339
|
+
inner = ", ".join(p.emit() for p in self.props)
|
|
340
|
+
return "{" + inner + "}"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class JSUnary(JSExpr):
|
|
344
|
+
__slots__ = ("op", "operand") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
345
|
+
op: str # '-', '+', '!', 'typeof', 'await'
|
|
346
|
+
operand: JSExpr
|
|
347
|
+
|
|
348
|
+
def __init__(self, op: str, operand: JSExpr):
|
|
349
|
+
self.op = op
|
|
350
|
+
self.operand = operand
|
|
351
|
+
|
|
352
|
+
@override
|
|
353
|
+
def emit(self) -> str:
|
|
354
|
+
operand_code = _emit_child_for_binary_like(
|
|
355
|
+
self.operand, parent_op=self.op, side="unary"
|
|
356
|
+
)
|
|
357
|
+
if self.op == "typeof":
|
|
358
|
+
return f"typeof {operand_code}"
|
|
359
|
+
return f"{self.op}{operand_code}"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class JSAwait(JSExpr):
|
|
363
|
+
__slots__ = ("operand",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
364
|
+
operand: JSExpr
|
|
365
|
+
|
|
366
|
+
def __init__(self, operand: JSExpr):
|
|
367
|
+
self.operand = operand
|
|
368
|
+
|
|
369
|
+
@override
|
|
370
|
+
def emit(self) -> str:
|
|
371
|
+
operand_code = _emit_child_for_binary_like(
|
|
372
|
+
self.operand, parent_op="await", side="unary"
|
|
373
|
+
)
|
|
374
|
+
return f"await {operand_code}"
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class JSBinary(JSExpr):
|
|
378
|
+
__slots__ = ("left", "op", "right") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
379
|
+
left: JSExpr
|
|
380
|
+
op: str
|
|
381
|
+
right: JSExpr
|
|
382
|
+
|
|
383
|
+
def __init__(self, left: JSExpr, op: str, right: JSExpr):
|
|
384
|
+
self.left = left
|
|
385
|
+
self.op = op
|
|
386
|
+
self.right = right
|
|
387
|
+
|
|
388
|
+
@override
|
|
389
|
+
def emit(self) -> str:
|
|
390
|
+
# Left child
|
|
391
|
+
force_left_paren = False
|
|
392
|
+
# Special JS grammar rule: left operand of ** cannot be a unary +/- without parentheses
|
|
393
|
+
if (
|
|
394
|
+
self.op == "**"
|
|
395
|
+
and isinstance(self.left, JSUnary)
|
|
396
|
+
and self.left.op in {"-", "+"}
|
|
397
|
+
):
|
|
398
|
+
force_left_paren = True
|
|
399
|
+
left_code = _emit_child_for_binary_like(
|
|
400
|
+
self.left,
|
|
401
|
+
parent_op=self.op,
|
|
402
|
+
side="left",
|
|
403
|
+
force_paren=force_left_paren,
|
|
404
|
+
)
|
|
405
|
+
# Right child
|
|
406
|
+
right_code = _emit_child_for_binary_like(
|
|
407
|
+
self.right, parent_op=self.op, side="right"
|
|
408
|
+
)
|
|
409
|
+
return f"{left_code} {self.op} {right_code}"
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class JSLogicalChain(JSExpr):
|
|
413
|
+
__slots__ = ("op", "values") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
414
|
+
op: str # '&&' or '||'
|
|
415
|
+
values: Sequence[JSExpr]
|
|
416
|
+
|
|
417
|
+
def __init__(self, op: str, values: Sequence[JSExpr]):
|
|
418
|
+
self.op = op
|
|
419
|
+
self.values = values
|
|
420
|
+
|
|
421
|
+
@override
|
|
422
|
+
def emit(self) -> str:
|
|
423
|
+
if len(self.values) == 1:
|
|
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)
|
|
431
|
+
|
|
432
|
+
|
|
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
|
+
|
|
439
|
+
def __init__(self, test: JSExpr, if_true: JSExpr, if_false: JSExpr):
|
|
440
|
+
self.test = test
|
|
441
|
+
self.if_true = if_true
|
|
442
|
+
self.if_false = if_false
|
|
443
|
+
|
|
444
|
+
@override
|
|
445
|
+
def emit(self) -> str:
|
|
446
|
+
return f"{self.test.emit()} ? {self.if_true.emit()} : {self.if_false.emit()}"
|
|
447
|
+
|
|
448
|
+
|
|
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
|
+
|
|
456
|
+
def __init__(
|
|
457
|
+
self,
|
|
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}}"
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class JSTemplate(JSExpr):
|
|
479
|
+
__slots__ = ("parts",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
480
|
+
is_primary: ClassVar[bool] = True
|
|
481
|
+
parts: Sequence[str | JSExpr]
|
|
482
|
+
|
|
483
|
+
def __init__(self, parts: Sequence[str | JSExpr]):
|
|
484
|
+
# parts are either raw strings (literal text) or JSExpr instances which are
|
|
485
|
+
# emitted inside ${...}
|
|
486
|
+
self.parts = parts
|
|
487
|
+
|
|
488
|
+
@override
|
|
489
|
+
def emit(self) -> str:
|
|
490
|
+
out: list[str] = ["`"]
|
|
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)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class JSMember(JSExpr):
|
|
514
|
+
__slots__ = ("obj", "prop") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
515
|
+
obj: JSExpr
|
|
516
|
+
prop: str
|
|
517
|
+
|
|
518
|
+
def __init__(self, obj: JSExpr, prop: str):
|
|
519
|
+
self.obj = obj
|
|
520
|
+
self.prop = prop
|
|
521
|
+
|
|
522
|
+
@override
|
|
523
|
+
def emit(self) -> str:
|
|
524
|
+
obj_code = _emit_child_for_primary(self.obj)
|
|
525
|
+
return f"{obj_code}.{self.prop}"
|
|
526
|
+
|
|
527
|
+
@override
|
|
528
|
+
def emit_call(self, args: list[Any], kwargs: dict[str, Any]) -> JSExpr:
|
|
529
|
+
"""Called when this member is used as a function: obj.prop(args).
|
|
530
|
+
|
|
531
|
+
Checks for Python builtin method transpilation (e.g., str.upper -> toUpperCase),
|
|
532
|
+
then falls back to regular JSMemberCall.
|
|
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
|
|
540
|
+
|
|
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
|
+
|
|
546
|
+
|
|
547
|
+
class JSSubscript(JSExpr):
|
|
548
|
+
__slots__ = ("obj", "index") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
549
|
+
obj: JSExpr
|
|
550
|
+
index: JSExpr
|
|
551
|
+
|
|
552
|
+
def __init__(self, obj: JSExpr, index: JSExpr):
|
|
553
|
+
self.obj = obj
|
|
554
|
+
self.index = index
|
|
555
|
+
|
|
556
|
+
@override
|
|
557
|
+
def emit(self) -> str:
|
|
558
|
+
obj_code = _emit_child_for_primary(self.obj)
|
|
559
|
+
return f"{obj_code}[{self.index.emit()}]"
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
class JSCall(JSExpr):
|
|
563
|
+
__slots__ = ("callee", "args") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
564
|
+
callee: JSExpr # typically JSIdentifier
|
|
565
|
+
args: Sequence[JSExpr]
|
|
566
|
+
|
|
567
|
+
def __init__(self, callee: JSExpr, args: Sequence[JSExpr]):
|
|
568
|
+
self.callee = callee
|
|
569
|
+
self.args = args
|
|
570
|
+
|
|
571
|
+
@override
|
|
572
|
+
def emit(self) -> str:
|
|
573
|
+
fn = _emit_child_for_primary(self.callee)
|
|
574
|
+
return f"{fn}({', '.join(a.emit() for a in self.args)})"
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class JSMemberCall(JSExpr):
|
|
578
|
+
__slots__ = ("obj", "method", "args") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
579
|
+
obj: JSExpr
|
|
580
|
+
method: str
|
|
581
|
+
args: Sequence[JSExpr]
|
|
582
|
+
|
|
583
|
+
def __init__(self, obj: JSExpr, method: str, args: Sequence[JSExpr]):
|
|
584
|
+
self.obj = obj
|
|
585
|
+
self.method = method
|
|
586
|
+
self.args = args
|
|
587
|
+
|
|
588
|
+
@override
|
|
589
|
+
def emit(self) -> str:
|
|
590
|
+
obj_code = _emit_child_for_primary(self.obj)
|
|
591
|
+
return f"{obj_code}.{self.method}({', '.join(a.emit() for a in self.args)})"
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class JSNew(JSExpr):
|
|
595
|
+
__slots__ = ("ctor", "args") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
596
|
+
ctor: JSExpr
|
|
597
|
+
args: Sequence[JSExpr]
|
|
598
|
+
|
|
599
|
+
def __init__(self, ctor: JSExpr, args: Sequence[JSExpr]):
|
|
600
|
+
self.ctor = ctor
|
|
601
|
+
self.args = args
|
|
602
|
+
|
|
603
|
+
@override
|
|
604
|
+
def emit(self) -> str:
|
|
605
|
+
ctor_code = _emit_child_for_primary(self.ctor)
|
|
606
|
+
return f"new {ctor_code}({', '.join(a.emit() for a in self.args)})"
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class JSArrowFunction(JSExpr):
|
|
610
|
+
__slots__ = ("params_code", "body") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
611
|
+
params_code: str # already formatted e.g. 'x' or '(a, b)' or '([k, v])'
|
|
612
|
+
body: JSExpr | JSBlock
|
|
613
|
+
|
|
614
|
+
def __init__(self, params_code: str, body: JSExpr | JSBlock):
|
|
615
|
+
self.params_code = params_code
|
|
616
|
+
self.body = body
|
|
617
|
+
|
|
618
|
+
@override
|
|
619
|
+
def emit(self) -> str:
|
|
620
|
+
return f"{self.params_code} => {self.body.emit()}"
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class JSComma(JSExpr):
|
|
624
|
+
__slots__ = ("values",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
625
|
+
values: Sequence[JSExpr]
|
|
626
|
+
|
|
627
|
+
def __init__(self, values: Sequence[JSExpr]):
|
|
628
|
+
self.values = values
|
|
629
|
+
|
|
630
|
+
@override
|
|
631
|
+
def emit(self) -> str:
|
|
632
|
+
# Always wrap comma expressions in parentheses to avoid precedence surprises
|
|
633
|
+
inner = ", ".join(v.emit() for v in self.values)
|
|
634
|
+
return f"({inner})"
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class JSTransformer(JSExpr):
|
|
638
|
+
"""JSExpr that wraps a function transforming JSExpr args to JSExpr output.
|
|
639
|
+
|
|
640
|
+
Generalizes the pattern of call-only expressions. The wrapped function
|
|
641
|
+
receives positional and keyword JSExpr arguments and returns a JSExpr.
|
|
642
|
+
|
|
643
|
+
Example:
|
|
644
|
+
emit_len = JSTransformer(lambda x: JSMember(x, "length"), name="len")
|
|
645
|
+
# When called: emit_len.emit_call([some_expr], {}) -> JSMember(some_expr, "length")
|
|
646
|
+
"""
|
|
647
|
+
|
|
648
|
+
__slots__ = ("fn", "name") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
649
|
+
fn: Callable[..., JSExpr]
|
|
650
|
+
name: str # Optional name for error messages
|
|
651
|
+
|
|
652
|
+
def __init__(self, fn: Callable[..., JSExpr], name: str = ""):
|
|
653
|
+
self.fn = fn
|
|
654
|
+
self.name = name
|
|
655
|
+
|
|
656
|
+
@override
|
|
657
|
+
def emit(self) -> str:
|
|
658
|
+
label = self.name or "JSTransformer"
|
|
659
|
+
raise JSCompilationError(f"{label} cannot be emitted directly - must be called")
|
|
660
|
+
|
|
661
|
+
@override
|
|
662
|
+
def emit_call(self, args: list[Any], kwargs: dict[str, Any]) -> JSExpr:
|
|
663
|
+
# Pass raw args to the transformer function - it decides what to convert
|
|
664
|
+
if kwargs:
|
|
665
|
+
return self.fn(*args, **kwargs)
|
|
666
|
+
return self.fn(*args)
|
|
667
|
+
|
|
668
|
+
@override
|
|
669
|
+
def emit_subscript(self, indices: list[Any]) -> JSExpr:
|
|
670
|
+
label = self.name or "JSTransformer"
|
|
671
|
+
raise JSCompilationError(f"{label} cannot be subscripted")
|
|
672
|
+
|
|
673
|
+
@override
|
|
674
|
+
def emit_getattr(self, attr: str) -> JSExpr:
|
|
675
|
+
label = self.name or "JSTransformer"
|
|
676
|
+
raise JSCompilationError(f"{label} cannot have attributes")
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@overload
|
|
683
|
+
def js_transformer(arg: str) -> Callable[[_F], _F]: ...
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@overload
|
|
687
|
+
def js_transformer(arg: _F) -> _F: ...
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def js_transformer(arg: str | _F) -> Callable[[_F], _F] | _F:
|
|
691
|
+
"""Decorator/helper for JSTransformer.
|
|
692
|
+
|
|
693
|
+
Usage:
|
|
694
|
+
@js_transformer("len")
|
|
695
|
+
def emit_len(x): ...
|
|
696
|
+
or:
|
|
697
|
+
emit_len = js_transformer(lambda x: ...)
|
|
698
|
+
|
|
699
|
+
Returns a JSTransformer, but the type signature lies and preserves
|
|
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):
|
|
704
|
+
|
|
705
|
+
def decorator(fn: _F) -> _F:
|
|
706
|
+
return cast(_F, JSTransformer(fn, name=arg))
|
|
707
|
+
|
|
708
|
+
return decorator
|
|
709
|
+
elif callable(arg):
|
|
710
|
+
return cast(_F, JSTransformer(arg))
|
|
711
|
+
else:
|
|
712
|
+
raise TypeError(
|
|
713
|
+
"js_transformer expects a function or string (for decorator usage)"
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
class JSReturn(JSStmt):
|
|
718
|
+
__slots__ = ("value",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
719
|
+
value: JSExpr
|
|
720
|
+
|
|
721
|
+
def __init__(self, value: JSExpr):
|
|
722
|
+
self.value = value
|
|
723
|
+
|
|
724
|
+
@override
|
|
725
|
+
def emit(self) -> str:
|
|
726
|
+
return f"return {self.value.emit()};"
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
class JSThrow(JSStmt):
|
|
730
|
+
__slots__ = ("value",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
731
|
+
value: JSExpr
|
|
732
|
+
|
|
733
|
+
def __init__(self, value: JSExpr):
|
|
734
|
+
self.value = value
|
|
735
|
+
|
|
736
|
+
@override
|
|
737
|
+
def emit(self) -> str:
|
|
738
|
+
return f"throw {self.value.emit()};"
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class JSStmtExpr(JSExpr):
|
|
742
|
+
"""Expression wrapper for a statement (e.g., throw).
|
|
743
|
+
|
|
744
|
+
Used for constructs like `throw(x)` that syntactically look like function calls
|
|
745
|
+
but must be emitted as statements. When used as an expression-statement,
|
|
746
|
+
the transpiler unwraps this and emits the inner statement directly.
|
|
747
|
+
"""
|
|
748
|
+
|
|
749
|
+
__slots__ = ("stmt", "name") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
750
|
+
stmt: JSStmt
|
|
751
|
+
name: str # For error messages (e.g., "throw")
|
|
752
|
+
|
|
753
|
+
def __init__(self, stmt: JSStmt, name: str = ""):
|
|
754
|
+
self.stmt = stmt
|
|
755
|
+
self.name = name
|
|
756
|
+
|
|
757
|
+
@override
|
|
758
|
+
def emit(self) -> str:
|
|
759
|
+
label = self.name or "statement"
|
|
760
|
+
raise JSCompilationError(
|
|
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
|
+
)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class JSAssign(JSStmt):
|
|
768
|
+
__slots__ = ("name", "value", "declare") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
769
|
+
name: str
|
|
770
|
+
value: JSExpr
|
|
771
|
+
declare: bool # when True emit 'let name = ...'
|
|
772
|
+
|
|
773
|
+
def __init__(self, name: str, value: JSExpr, declare: bool = False):
|
|
774
|
+
self.name = name
|
|
775
|
+
self.value = value
|
|
776
|
+
self.declare = declare
|
|
777
|
+
|
|
778
|
+
@override
|
|
779
|
+
def emit(self) -> str:
|
|
780
|
+
if self.declare:
|
|
781
|
+
return f"let {self.name} = {self.value.emit()};"
|
|
782
|
+
return f"{self.name} = {self.value.emit()};"
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
class JSRaw(JSExpr):
|
|
786
|
+
__slots__ = ("content",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
787
|
+
is_primary: ClassVar[bool] = True
|
|
788
|
+
content: str
|
|
789
|
+
|
|
790
|
+
def __init__(self, content: str):
|
|
791
|
+
self.content = content
|
|
792
|
+
|
|
793
|
+
@override
|
|
794
|
+
def emit(self) -> str:
|
|
795
|
+
return self.content
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
###############################################################################
|
|
799
|
+
# JSX AST (minimal)
|
|
800
|
+
###############################################################################
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _check_not_interpreted_mode(node_type: str) -> None:
|
|
804
|
+
"""Raise an error if we're in interpreted mode - JSX can't be eval'd."""
|
|
805
|
+
if is_interpreted_mode():
|
|
806
|
+
raise ValueError(
|
|
807
|
+
f"{node_type} cannot be used in interpreted mode (as a prop or child value). "
|
|
808
|
+
+ "JSX syntax requires transpilation and cannot be evaluated at runtime. "
|
|
809
|
+
+ "Use standard VDOM elements (ps.div, ps.span, etc.) instead."
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _escape_jsx_text(text: str) -> str:
|
|
814
|
+
# Minimal escaping for text nodes
|
|
815
|
+
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
class JSXProp(JSExpr):
|
|
819
|
+
__slots__ = ("name", "value") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
820
|
+
name: str
|
|
821
|
+
value: JSExpr | None
|
|
822
|
+
|
|
823
|
+
def __init__(self, name: str, value: JSExpr | None = None):
|
|
824
|
+
self.name = name
|
|
825
|
+
self.value = value
|
|
826
|
+
|
|
827
|
+
@override
|
|
828
|
+
def emit(self) -> str:
|
|
829
|
+
_check_not_interpreted_mode("JSXProp")
|
|
830
|
+
if self.value is None:
|
|
831
|
+
return self.name
|
|
832
|
+
# Prefer compact string literal attribute when possible
|
|
833
|
+
if isinstance(self.value, JSString):
|
|
834
|
+
return f"{self.name}={self.value.emit()}"
|
|
835
|
+
return self.name + "={" + self.value.emit() + "}"
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
class JSXSpreadProp(JSExpr):
|
|
839
|
+
__slots__ = ("value",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
840
|
+
value: JSExpr
|
|
841
|
+
|
|
842
|
+
def __init__(self, value: JSExpr):
|
|
843
|
+
self.value = value
|
|
844
|
+
|
|
845
|
+
@override
|
|
846
|
+
def emit(self) -> str:
|
|
847
|
+
_check_not_interpreted_mode("JSXSpreadProp")
|
|
848
|
+
return f"{{...{self.value.emit()}}}"
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
class JSXElement(JSExpr):
|
|
852
|
+
__slots__ = ("tag", "props", "children") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
853
|
+
is_jsx: ClassVar[bool] = True
|
|
854
|
+
is_primary: ClassVar[bool] = True
|
|
855
|
+
tag: str | JSExpr
|
|
856
|
+
props: Sequence[JSXProp | JSXSpreadProp]
|
|
857
|
+
children: Sequence[str | JSExpr | JSXElement]
|
|
858
|
+
|
|
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
|
+
|
|
869
|
+
@override
|
|
870
|
+
def emit(self) -> str:
|
|
871
|
+
_check_not_interpreted_mode("JSXElement")
|
|
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) + "</>"
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
class JSImport:
|
|
919
|
+
__slots__ = ("src", "default", "named") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
920
|
+
src: str
|
|
921
|
+
default: str | None
|
|
922
|
+
named: list[str | tuple[str, str]]
|
|
923
|
+
|
|
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
|
+
|
|
1039
|
+
def __init__(self, name: str, op: str, value: JSExpr):
|
|
1040
|
+
self.name = name
|
|
1041
|
+
self.op = op
|
|
1042
|
+
self.value = value
|
|
1043
|
+
|
|
1044
|
+
@override
|
|
1045
|
+
def emit(self) -> str:
|
|
1046
|
+
return f"{self.name} {self.op}= {self.value.emit()};"
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
class JSConstAssign(JSStmt):
|
|
1050
|
+
__slots__ = ("name", "value") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1051
|
+
name: str
|
|
1052
|
+
value: JSExpr
|
|
1053
|
+
|
|
1054
|
+
def __init__(self, name: str, value: JSExpr):
|
|
1055
|
+
self.name = name
|
|
1056
|
+
self.value = value
|
|
1057
|
+
|
|
1058
|
+
@override
|
|
1059
|
+
def emit(self) -> str:
|
|
1060
|
+
return f"const {self.name} = {self.value.emit()};"
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
class JSSingleStmt(JSStmt):
|
|
1064
|
+
__slots__ = ("expr",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1065
|
+
expr: JSExpr
|
|
1066
|
+
|
|
1067
|
+
def __init__(self, expr: JSExpr):
|
|
1068
|
+
self.expr = expr
|
|
1069
|
+
|
|
1070
|
+
@override
|
|
1071
|
+
def emit(self) -> str:
|
|
1072
|
+
return f"{self.expr.emit()};"
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
class JSMultiStmt(JSStmt):
|
|
1076
|
+
__slots__ = ("stmts",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1077
|
+
stmts: Sequence[JSStmt]
|
|
1078
|
+
|
|
1079
|
+
def __init__(self, stmts: Sequence[JSStmt]):
|
|
1080
|
+
self.stmts = stmts
|
|
1081
|
+
|
|
1082
|
+
@override
|
|
1083
|
+
def emit(self) -> str:
|
|
1084
|
+
return "\n".join(s.emit() for s in self.stmts)
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
class JSIf(JSStmt):
|
|
1088
|
+
__slots__ = ("test", "body", "orelse") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1089
|
+
test: JSExpr
|
|
1090
|
+
body: Sequence[JSStmt]
|
|
1091
|
+
orelse: Sequence[JSStmt]
|
|
1092
|
+
|
|
1093
|
+
def __init__(
|
|
1094
|
+
self, test: JSExpr, body: Sequence[JSStmt], orelse: Sequence[JSStmt] = ()
|
|
1095
|
+
):
|
|
1096
|
+
self.test = test
|
|
1097
|
+
self.body = body
|
|
1098
|
+
self.orelse = orelse
|
|
1099
|
+
|
|
1100
|
+
@override
|
|
1101
|
+
def emit(self) -> str:
|
|
1102
|
+
body_code = "\n".join(s.emit() for s in self.body)
|
|
1103
|
+
if not self.orelse:
|
|
1104
|
+
return f"if ({self.test.emit()}){{\n{body_code}\n}}"
|
|
1105
|
+
else_code = "\n".join(s.emit() for s in self.orelse)
|
|
1106
|
+
return f"if ({self.test.emit()}){{\n{body_code}\n}} else {{\n{else_code}\n}}"
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
class JSForOf(JSStmt):
|
|
1110
|
+
__slots__ = ("target", "iter_expr", "body") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1111
|
+
target: str | list[str]
|
|
1112
|
+
iter_expr: JSExpr
|
|
1113
|
+
body: Sequence[JSStmt]
|
|
1114
|
+
|
|
1115
|
+
def __init__(
|
|
1116
|
+
self, target: str | list[str], iter_expr: JSExpr, body: Sequence[JSStmt]
|
|
1117
|
+
):
|
|
1118
|
+
self.target = target
|
|
1119
|
+
self.iter_expr = iter_expr
|
|
1120
|
+
self.body = body
|
|
1121
|
+
|
|
1122
|
+
@override
|
|
1123
|
+
def emit(self) -> str:
|
|
1124
|
+
body_code = "\n".join(s.emit() for s in self.body)
|
|
1125
|
+
target = self.target
|
|
1126
|
+
if not isinstance(target, str):
|
|
1127
|
+
target = f"[{', '.join(x for x in target)}]"
|
|
1128
|
+
return f"for (const {target} of {self.iter_expr.emit()}){{\n{body_code}\n}}"
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
class JSWhile(JSStmt):
|
|
1132
|
+
__slots__ = ("test", "body") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1133
|
+
test: JSExpr
|
|
1134
|
+
body: Sequence[JSStmt]
|
|
1135
|
+
|
|
1136
|
+
def __init__(self, test: JSExpr, body: Sequence[JSStmt]):
|
|
1137
|
+
self.test = test
|
|
1138
|
+
self.body = body
|
|
1139
|
+
|
|
1140
|
+
@override
|
|
1141
|
+
def emit(self) -> str:
|
|
1142
|
+
body_code = "\n".join(s.emit() for s in self.body)
|
|
1143
|
+
return f"while ({self.test.emit()}){{\n{body_code}\n}}"
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
class JSBreak(JSStmt):
|
|
1147
|
+
__slots__ = () # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1148
|
+
|
|
1149
|
+
@override
|
|
1150
|
+
def emit(self) -> str:
|
|
1151
|
+
return "break;"
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
class JSContinue(JSStmt):
|
|
1155
|
+
__slots__ = () # pyright: ignore[reportUnannotatedClassAttribute]
|
|
1156
|
+
|
|
1157
|
+
@override
|
|
1158
|
+
def emit(self) -> str:
|
|
1159
|
+
return "continue;"
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _mixes_nullish_and_logical(parent_op: str, child: JSExpr) -> bool:
|
|
1163
|
+
if parent_op in {"&&", "||"} and isinstance(child, JSBinary) and child.op == "??":
|
|
1164
|
+
return True
|
|
1165
|
+
if parent_op == "??" and isinstance(child, JSLogicalChain):
|
|
1166
|
+
return True
|
|
1167
|
+
return False
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def _emit_child_for_binary_like(
|
|
1171
|
+
child: JSExpr, parent_op: str, side: str, force_paren: bool = False
|
|
1172
|
+
) -> str:
|
|
1173
|
+
# side is one of: 'left', 'right', 'unary', 'chain'
|
|
1174
|
+
code = child.emit()
|
|
1175
|
+
if force_paren:
|
|
1176
|
+
return f"({code})"
|
|
1177
|
+
# Ternary as child should always be wrapped under binary-like contexts
|
|
1178
|
+
if isinstance(child, JSTertiary):
|
|
1179
|
+
return f"({code})"
|
|
1180
|
+
# Explicit parens when mixing ?? with &&/||
|
|
1181
|
+
if _mixes_nullish_and_logical(parent_op, child):
|
|
1182
|
+
return f"({code})"
|
|
1183
|
+
child_prec = expr_precedence(child)
|
|
1184
|
+
parent_prec = op_precedence(parent_op)
|
|
1185
|
+
if child_prec < parent_prec:
|
|
1186
|
+
return f"({code})"
|
|
1187
|
+
if child_prec == parent_prec:
|
|
1188
|
+
# Handle associativity for exact same precedence buckets
|
|
1189
|
+
if isinstance(child, JSBinary):
|
|
1190
|
+
if op_is_right_associative(parent_op):
|
|
1191
|
+
# Need parens on left child for same prec to preserve grouping
|
|
1192
|
+
if side == "left":
|
|
1193
|
+
return f"({code})"
|
|
1194
|
+
else:
|
|
1195
|
+
# Left-associative: protect right child when equal precedence
|
|
1196
|
+
if side == "right":
|
|
1197
|
+
return f"({code})"
|
|
1198
|
+
if isinstance(child, JSLogicalChain):
|
|
1199
|
+
# Same op chains don't need parens; different logical ops rely on precedence
|
|
1200
|
+
if child.op != parent_op:
|
|
1201
|
+
# '&&' has higher precedence than '||'; no parens needed for tighter child
|
|
1202
|
+
# But if equal (shouldn't happen here), remain as-is
|
|
1203
|
+
pass
|
|
1204
|
+
# For other equal-precedence non-binary nodes, keep as-is
|
|
1205
|
+
return code
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def _emit_child_for_primary(expr: JSExpr) -> str:
|
|
1209
|
+
code = expr.emit()
|
|
1210
|
+
if expr_precedence(expr) < PRIMARY_PRECEDENCE or isinstance(expr, JSTertiary):
|
|
1211
|
+
return f"({code})"
|
|
1212
|
+
return code
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def is_primary(expr: JSExpr):
|
|
1216
|
+
return isinstance(expr, (JSNumber, JSString, JSUndefined, JSNull, JSIdentifier))
|