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