@xaendar/compiler 0.7.30 → 0.7.32
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.
- package/dist/compiler.js +15 -0
- package/package.json +8 -11
- package/dist/xaendar-compiler.es.d.ts +0 -241
- package/dist/xaendar-compiler.es.js +0 -3144
|
@@ -1,3144 +0,0 @@
|
|
|
1
|
-
import { Stack, indent } from "@xaendar/common";
|
|
2
|
-
import { ScriptTarget, SyntaxKind, createSourceFile, forEachChild, isExpressionStatement, isIdentifier, isPropertyAccessExpression, isPropertyAssignment } from "typescript";
|
|
3
|
-
//#region ../packages/compiler/src/parser/types/node.enum.ts
|
|
4
|
-
/**
|
|
5
|
-
* Discriminant values that identify the type of each AST node produced by the parser.
|
|
6
|
-
*/
|
|
7
|
-
var ASTNodeType = /* @__PURE__ */ function(ASTNodeType) {
|
|
8
|
-
/**
|
|
9
|
-
* An HTML element node with a tag name, attributes, events, and children.
|
|
10
|
-
*/
|
|
11
|
-
ASTNodeType[ASTNodeType["Element"] = 0] = "Element";
|
|
12
|
-
/**
|
|
13
|
-
* A plain text node.
|
|
14
|
-
*/
|
|
15
|
-
ASTNodeType[ASTNodeType["Text"] = 1] = "Text";
|
|
16
|
-
/**
|
|
17
|
-
* An inline interpolation expression or literal.
|
|
18
|
-
*/
|
|
19
|
-
ASTNodeType[ASTNodeType["Interpolation"] = 2] = "Interpolation";
|
|
20
|
-
/**
|
|
21
|
-
* An `@if` conditional node.
|
|
22
|
-
*/
|
|
23
|
-
ASTNodeType[ASTNodeType["If"] = 3] = "If";
|
|
24
|
-
/**
|
|
25
|
-
* An `@else` branch node attached to an `@if`.
|
|
26
|
-
*/
|
|
27
|
-
ASTNodeType[ASTNodeType["Else"] = 4] = "Else";
|
|
28
|
-
/**
|
|
29
|
-
* An `@else if` branch node attached to an `@if`.
|
|
30
|
-
*/
|
|
31
|
-
ASTNodeType[ASTNodeType["ElseIf"] = 5] = "ElseIf";
|
|
32
|
-
/**
|
|
33
|
-
* An `@for` iteration node.
|
|
34
|
-
*/
|
|
35
|
-
ASTNodeType[ASTNodeType["For"] = 6] = "For";
|
|
36
|
-
/**
|
|
37
|
-
* An `@switch` node containing one or more case nodes.
|
|
38
|
-
*/
|
|
39
|
-
ASTNodeType[ASTNodeType["Switch"] = 7] = "Switch";
|
|
40
|
-
/**
|
|
41
|
-
* A `@case` or `@default` branch inside a `@switch`.
|
|
42
|
-
*/
|
|
43
|
-
ASTNodeType[ASTNodeType["Case"] = 8] = "Case";
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
*/
|
|
47
|
-
ASTNodeType[ASTNodeType["Import"] = 9] = "Import";
|
|
48
|
-
return ASTNodeType;
|
|
49
|
-
}({});
|
|
50
|
-
//#endregion
|
|
51
|
-
//#region ../packages/compiler/src/generator/models/compiler-context.model.ts
|
|
52
|
-
/**
|
|
53
|
-
* Tracks identifier scope during render code generation.
|
|
54
|
-
* Each `Context` instance represents one lexical scope (e.g. a `@for` loop body)
|
|
55
|
-
* and can be chained to a parent context for outer-scope resolution.
|
|
56
|
-
*/
|
|
57
|
-
var CompilerContext = class {
|
|
58
|
-
_parent;
|
|
59
|
-
/**
|
|
60
|
-
* Identifiers declared directly in this scope, mapped to whether they
|
|
61
|
-
* hold a plain value or a signal (e.g. `@for` implicit variables like
|
|
62
|
-
* `$index` are signals; the loop item itself is a plain value).
|
|
63
|
-
*/
|
|
64
|
-
_identifiers = /* @__PURE__ */ new Map();
|
|
65
|
-
/**
|
|
66
|
-
* List of identifiers that should not be resolved
|
|
67
|
-
* and touched in anyway
|
|
68
|
-
* (e.g. $event)
|
|
69
|
-
*/
|
|
70
|
-
_unresolvableIdentifiers = new Array();
|
|
71
|
-
/**
|
|
72
|
-
* Creates a new scope context.
|
|
73
|
-
*
|
|
74
|
-
* @param identifiers - Named identifier bindings declared in this scope.
|
|
75
|
-
* Plain strings default to kind `'value'`; pass a `[name, kind]` tuple
|
|
76
|
-
* to declare a signal-backed identifier.
|
|
77
|
-
* @param _parent - Optional parent context representing the enclosing scope.
|
|
78
|
-
*/
|
|
79
|
-
constructor(identifiers = [], _parent) {
|
|
80
|
-
this._parent = _parent;
|
|
81
|
-
for (const identifier of identifiers) typeof identifier === "string" ? this._identifiers.set(identifier, "value") : this._identifiers.set(identifier[0], identifier[1]);
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Registers a new identifier name in this scope.
|
|
85
|
-
*
|
|
86
|
-
* @param name - The identifier name to register.
|
|
87
|
-
* @param kind - Whether the identifier holds a plain value or a signal.
|
|
88
|
-
* Defaults to `'value'`.
|
|
89
|
-
* @throws When an identifier with the same name is already declared in this scope.
|
|
90
|
-
*/
|
|
91
|
-
addIdentifier(name, kind = "value") {
|
|
92
|
-
if (this.hasIdentifier(name)) throw new Error(`Identifier "${name}" is already declared in this scope.`);
|
|
93
|
-
this._identifiers.set(name, kind);
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Registers a new unresolvable identifier name in this scope.
|
|
97
|
-
* Unresolvable identifiers (e.g. `$event`) are tracked so lookups via
|
|
98
|
-
* {@link hasIdentifier} recognize them, but they are never meant to be
|
|
99
|
-
* resolved or otherwise manipulated by the compiler.
|
|
100
|
-
*
|
|
101
|
-
* @param name - The identifier name to register.
|
|
102
|
-
* @throws When an identifier with the same name is already declared in this scope.
|
|
103
|
-
*/
|
|
104
|
-
addUnresolvableIdentifier(name) {
|
|
105
|
-
if (this.hasIdentifier(name)) throw new Error(`Identifier "${name}" is already declared in this scope.`);
|
|
106
|
-
this._unresolvableIdentifiers.push(name);
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Removes a previously registered identifier from this scope, if present.
|
|
110
|
-
* Does nothing if no identifier with the given name is declared in this scope.
|
|
111
|
-
* Note: this only affects the current scope, not any ancestor scopes.
|
|
112
|
-
*
|
|
113
|
-
* @param name - The identifier name to remove.
|
|
114
|
-
*/
|
|
115
|
-
removeIdentifier(name) {
|
|
116
|
-
this._identifiers.delete(name);
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Removes a previously registered unresolvable identifier from this scope, if present.
|
|
120
|
-
* Does nothing if no unresolvable identifier with the given name is declared in this scope.
|
|
121
|
-
* Note: this only affects the current scope, not any ancestor scopes.
|
|
122
|
-
*
|
|
123
|
-
* @param name - The identifier name to remove.
|
|
124
|
-
*/
|
|
125
|
-
removeUnresolvabledIdentifier(name) {
|
|
126
|
-
this._unresolvableIdentifiers = this._unresolvableIdentifiers.filter((identifier) => identifier !== name);
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Returns `true` if an identifier with the given name is declared in this
|
|
130
|
-
* scope or any of its ancestor scopes.
|
|
131
|
-
*
|
|
132
|
-
* @param name - The identifier name to look up.
|
|
133
|
-
* @returns `true` if the identifier exists in the scope chain, `false` otherwise.
|
|
134
|
-
*/
|
|
135
|
-
hasIdentifier(name) {
|
|
136
|
-
return this._identifiers.has(name) || (this._parent?.hasIdentifier(name) ?? false);
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Resolves the kind (`'value'` or `'signal'`) of a declared identifier,
|
|
140
|
-
* walking up the scope chain if not found in this scope.
|
|
141
|
-
*
|
|
142
|
-
* @param name - The identifier name to look up.
|
|
143
|
-
* @returns The identifier's kind, or `undefined` if it isn't declared
|
|
144
|
-
* anywhere in the scope chain.
|
|
145
|
-
*/
|
|
146
|
-
getIdentifierKind(name) {
|
|
147
|
-
return this._identifiers.get(name) ?? this._parent?.getIdentifierKind(name);
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Returns `true` if an identifier with the given name is declared in this
|
|
151
|
-
* scope or any of its ancestor scopes.
|
|
152
|
-
*
|
|
153
|
-
* @param name - The identifier name to look up.
|
|
154
|
-
* @returns `true` if the identifier exists in the scope chain, `false` otherwise.
|
|
155
|
-
*/
|
|
156
|
-
hasUnresolvableIdentifier(name) {
|
|
157
|
-
return this._unresolvableIdentifiers.includes(name);
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
//#endregion
|
|
161
|
-
//#region ../packages/compiler/src/generator/utils/generator.utils.ts
|
|
162
|
-
/**
|
|
163
|
-
* Complete set of JavaScript global identifiers up to ES2026.
|
|
164
|
-
*
|
|
165
|
-
* These are identifiers that TypeScript's parser classifies as
|
|
166
|
-
* `SyntaxKind.Identifier` (unlike true keywords such as `typeof`,
|
|
167
|
-
* `instanceof`, `true`, `false`, `null` which have their own SyntaxKind)
|
|
168
|
-
* but that must never be prefixed with `this.` inside a template expression
|
|
169
|
-
* because they refer to well-known globals, not to component properties.
|
|
170
|
-
*
|
|
171
|
-
* Organised by ECMAScript category, mirroring the MDN "Standard built-in
|
|
172
|
-
* objects" reference, plus the ES2026 additions (Temporal, DisposableStack,
|
|
173
|
-
* AsyncDisposableStack, SuppressedError, Math.sumPrecise surface).
|
|
174
|
-
*
|
|
175
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
|
|
176
|
-
*/
|
|
177
|
-
var GLOBAL_IDENTIFIERS = /* @__PURE__ */ new Set([
|
|
178
|
-
"undefined",
|
|
179
|
-
"NaN",
|
|
180
|
-
"Infinity",
|
|
181
|
-
"globalThis",
|
|
182
|
-
"eval",
|
|
183
|
-
"isFinite",
|
|
184
|
-
"isNaN",
|
|
185
|
-
"parseFloat",
|
|
186
|
-
"parseInt",
|
|
187
|
-
"decodeURI",
|
|
188
|
-
"decodeURIComponent",
|
|
189
|
-
"encodeURI",
|
|
190
|
-
"encodeURIComponent",
|
|
191
|
-
"escape",
|
|
192
|
-
"unescape",
|
|
193
|
-
"Object",
|
|
194
|
-
"Function",
|
|
195
|
-
"Boolean",
|
|
196
|
-
"Symbol",
|
|
197
|
-
"Error",
|
|
198
|
-
"AggregateError",
|
|
199
|
-
"EvalError",
|
|
200
|
-
"RangeError",
|
|
201
|
-
"ReferenceError",
|
|
202
|
-
"SyntaxError",
|
|
203
|
-
"TypeError",
|
|
204
|
-
"URIError",
|
|
205
|
-
"SuppressedError",
|
|
206
|
-
"InternalError",
|
|
207
|
-
"Number",
|
|
208
|
-
"BigInt",
|
|
209
|
-
"Math",
|
|
210
|
-
"Date",
|
|
211
|
-
"Temporal",
|
|
212
|
-
"String",
|
|
213
|
-
"RegExp",
|
|
214
|
-
"Array",
|
|
215
|
-
"TypedArray",
|
|
216
|
-
"Int8Array",
|
|
217
|
-
"Uint8Array",
|
|
218
|
-
"Uint8ClampedArray",
|
|
219
|
-
"Int16Array",
|
|
220
|
-
"Uint16Array",
|
|
221
|
-
"Int32Array",
|
|
222
|
-
"Uint32Array",
|
|
223
|
-
"BigInt64Array",
|
|
224
|
-
"BigUint64Array",
|
|
225
|
-
"Float16Array",
|
|
226
|
-
"Float32Array",
|
|
227
|
-
"Float64Array",
|
|
228
|
-
"Map",
|
|
229
|
-
"Set",
|
|
230
|
-
"WeakMap",
|
|
231
|
-
"WeakSet",
|
|
232
|
-
"ArrayBuffer",
|
|
233
|
-
"SharedArrayBuffer",
|
|
234
|
-
"DataView",
|
|
235
|
-
"Atomics",
|
|
236
|
-
"JSON",
|
|
237
|
-
"WeakRef",
|
|
238
|
-
"FinalizationRegistry",
|
|
239
|
-
"Iterator",
|
|
240
|
-
"AsyncIterator",
|
|
241
|
-
"Promise",
|
|
242
|
-
"GeneratorFunction",
|
|
243
|
-
"AsyncGeneratorFunction",
|
|
244
|
-
"Generator",
|
|
245
|
-
"AsyncGenerator",
|
|
246
|
-
"AsyncFunction",
|
|
247
|
-
"DisposableStack",
|
|
248
|
-
"AsyncDisposableStack",
|
|
249
|
-
"Reflect",
|
|
250
|
-
"Proxy",
|
|
251
|
-
"Intl",
|
|
252
|
-
"WebAssembly",
|
|
253
|
-
"window",
|
|
254
|
-
"document",
|
|
255
|
-
"navigator",
|
|
256
|
-
"location",
|
|
257
|
-
"history",
|
|
258
|
-
"screen",
|
|
259
|
-
"console",
|
|
260
|
-
"performance",
|
|
261
|
-
"crypto",
|
|
262
|
-
"fetch",
|
|
263
|
-
"alert",
|
|
264
|
-
"confirm",
|
|
265
|
-
"prompt",
|
|
266
|
-
"setTimeout",
|
|
267
|
-
"setInterval",
|
|
268
|
-
"clearTimeout",
|
|
269
|
-
"clearInterval",
|
|
270
|
-
"requestAnimationFrame",
|
|
271
|
-
"cancelAnimationFrame",
|
|
272
|
-
"queueMicrotask",
|
|
273
|
-
"structuredClone",
|
|
274
|
-
"URL",
|
|
275
|
-
"URLSearchParams",
|
|
276
|
-
"FormData",
|
|
277
|
-
"Headers",
|
|
278
|
-
"Request",
|
|
279
|
-
"Response",
|
|
280
|
-
"AbortController",
|
|
281
|
-
"AbortSignal",
|
|
282
|
-
"CustomEvent",
|
|
283
|
-
"Event",
|
|
284
|
-
"EventTarget",
|
|
285
|
-
"MutationObserver",
|
|
286
|
-
"IntersectionObserver",
|
|
287
|
-
"ResizeObserver",
|
|
288
|
-
"PerformanceObserver",
|
|
289
|
-
"Worker",
|
|
290
|
-
"SharedWorker",
|
|
291
|
-
"ServiceWorker",
|
|
292
|
-
"Blob",
|
|
293
|
-
"File",
|
|
294
|
-
"FileReader",
|
|
295
|
-
"ReadableStream",
|
|
296
|
-
"WritableStream",
|
|
297
|
-
"TransformStream",
|
|
298
|
-
"TextEncoder",
|
|
299
|
-
"TextDecoder",
|
|
300
|
-
"ImageData",
|
|
301
|
-
"Canvas",
|
|
302
|
-
"Storage",
|
|
303
|
-
"localStorage",
|
|
304
|
-
"sessionStorage",
|
|
305
|
-
"indexedDB",
|
|
306
|
-
"WebSocket",
|
|
307
|
-
"XMLHttpRequest",
|
|
308
|
-
"HTMLElement",
|
|
309
|
-
"HTMLInputElement",
|
|
310
|
-
"HTMLButtonElement",
|
|
311
|
-
"HTMLFormElement",
|
|
312
|
-
"HTMLAnchorElement",
|
|
313
|
-
"HTMLImageElement",
|
|
314
|
-
"HTMLVideoElement",
|
|
315
|
-
"HTMLAudioElement",
|
|
316
|
-
"HTMLCanvasElement",
|
|
317
|
-
"HTMLSelectElement",
|
|
318
|
-
"HTMLTextAreaElement",
|
|
319
|
-
"HTMLDivElement",
|
|
320
|
-
"HTMLSpanElement",
|
|
321
|
-
"HTMLParagraphElement",
|
|
322
|
-
"HTMLHeadingElement",
|
|
323
|
-
"HTMLTableElement",
|
|
324
|
-
"HTMLTableRowElement",
|
|
325
|
-
"HTMLTableCellElement",
|
|
326
|
-
"HTMLUListElement",
|
|
327
|
-
"HTMLOListElement",
|
|
328
|
-
"HTMLLIElement",
|
|
329
|
-
"HTMLLabelElement",
|
|
330
|
-
"HTMLDialogElement",
|
|
331
|
-
"HTMLDetailsElement",
|
|
332
|
-
"HTMLSlotElement",
|
|
333
|
-
"HTMLTemplateElement",
|
|
334
|
-
"SVGElement",
|
|
335
|
-
"SVGSVGElement",
|
|
336
|
-
"Element",
|
|
337
|
-
"Node",
|
|
338
|
-
"NodeList",
|
|
339
|
-
"DocumentFragment",
|
|
340
|
-
"ShadowRoot",
|
|
341
|
-
"Document",
|
|
342
|
-
"Window"
|
|
343
|
-
]);
|
|
344
|
-
var ROOT_NODE = "root";
|
|
345
|
-
/**
|
|
346
|
-
* Resolves references to component properties inside a template expression.
|
|
347
|
-
*
|
|
348
|
-
* Identifiers that are not found in the active scope chain and are not
|
|
349
|
-
* well-known globals are prefixed with `this.` so they resolve against
|
|
350
|
-
* the component instance at runtime. Identifiers found in the scope chain
|
|
351
|
-
* are resolved either as a bare local reference (if declared directly in
|
|
352
|
-
* the current generated function's own scope) or via a runtime
|
|
353
|
-
* `parentContext.get(...)` traversal (if inherited from an enclosing
|
|
354
|
-
* scope) — and a trailing `()` is appended in either case if the
|
|
355
|
-
* identifier was declared as a signal, so the generated code always
|
|
356
|
-
* correctly unwraps it.
|
|
357
|
-
*
|
|
358
|
-
* The original formatting of the expression — parentheses, spacing,
|
|
359
|
-
* operator tokens, member access dots — is preserved verbatim by delegating
|
|
360
|
-
* to `node.getText()` for any subtree that contains no resolvable identifiers.
|
|
361
|
-
*
|
|
362
|
-
* @param expression - Either a raw identifier string or a validated
|
|
363
|
-
* `Expression` node produced by `validateExpression`.
|
|
364
|
-
* @param compilerContext - The active template scope context.
|
|
365
|
-
* @returns The resolved expression as a JavaScript string ready for codegen.
|
|
366
|
-
*
|
|
367
|
-
* @example
|
|
368
|
-
* // Simple identifier
|
|
369
|
-
* resolveExpression('items', context) // → 'this.items'
|
|
370
|
-
*
|
|
371
|
-
* @example
|
|
372
|
-
* // Signal identifier inherited from an ancestor scope (e.g. `$index` in a
|
|
373
|
-
* // nested @for body)
|
|
374
|
-
* resolveExpression('$index', context) // → "parentContext.get('$index')()"
|
|
375
|
-
*
|
|
376
|
-
* @example
|
|
377
|
-
* // Complex expression — formatting preserved
|
|
378
|
-
* resolveExpression(node, context)
|
|
379
|
-
* // typeof id !== 'boolean' || pippo instanceof HTMLElement
|
|
380
|
-
* // → typeof this.id !== 'boolean' || this.pippo instanceof HTMLElement
|
|
381
|
-
*/
|
|
382
|
-
function resolveExpression(expression, compilerContext, options) {
|
|
383
|
-
return emitNode(expression, expression, compilerContext, mapDefaultOptions(options));
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Emits the resolved text for a node.
|
|
387
|
-
*
|
|
388
|
-
* - If the node has no resolvable identifiers in its subtree, emits
|
|
389
|
-
* `node.getText()` verbatim — preserving all original spacing,
|
|
390
|
-
* parentheses, dots, and punctuation.
|
|
391
|
-
* - If the node is a resolvable Identifier, emits the resolved access
|
|
392
|
-
* expression (see {@link resolveIdentifierAccess}).
|
|
393
|
-
* - Otherwise recurses into children and concatenates their output.
|
|
394
|
-
*/
|
|
395
|
-
function emitNode(node, parent, compilerContext, options) {
|
|
396
|
-
if (isIdentifier(node) && needsResolution(node, parent)) {
|
|
397
|
-
const text = node.text;
|
|
398
|
-
if (compilerContext?.hasUnresolvableIdentifier(text) || options.skipResolution) return text;
|
|
399
|
-
if (options.resolver) return `${options.resolver}.${text}`;
|
|
400
|
-
if (compilerContext) return resolveIdentifierAccess(text, compilerContext);
|
|
401
|
-
return text;
|
|
402
|
-
}
|
|
403
|
-
if (!containsResolvableIdentifier(node, parent)) return node.getText();
|
|
404
|
-
const sourceText = node.getSourceFile().text;
|
|
405
|
-
let result = "";
|
|
406
|
-
let lastEnd = node.getStart();
|
|
407
|
-
forEachChild(node, (child) => {
|
|
408
|
-
result = `${result}${sourceText.slice(lastEnd, child.getStart())}${emitNode(child, node, compilerContext, options)}`;
|
|
409
|
-
lastEnd = child.getEnd();
|
|
410
|
-
});
|
|
411
|
-
return `${result}${sourceText.slice(lastEnd, node.getEnd())}`;
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Decides how to access a resolvable identifier's value in generated code,
|
|
415
|
-
* based purely on compile-time scope information — no runtime type
|
|
416
|
-
* detection is ever needed:
|
|
417
|
-
*
|
|
418
|
-
* - Declared directly in the CURRENT generated function's own scope (e.g.
|
|
419
|
-
* destructured from `vars` in a `@for` body) → bare reference: `name`,
|
|
420
|
-
* or `name()` if it's a signal.
|
|
421
|
-
* - Declared in an ANCESTOR scope (an enclosing `@for`/`@if`/element-children
|
|
422
|
-
* function) → must cross the closure boundary through the runtime
|
|
423
|
-
* `Context` chain: `parentContext.get('name')`, or with a trailing `()`
|
|
424
|
-
* if it's a signal.
|
|
425
|
-
* - Not declared anywhere in the template scope chain → assumed to be a
|
|
426
|
-
* component member: `this.name`.
|
|
427
|
-
*
|
|
428
|
-
* @param text - The identifier name to resolve.
|
|
429
|
-
* @param compilerContext - The active template scope context.
|
|
430
|
-
* @returns The generated code expression that yields the identifier's value.
|
|
431
|
-
*/
|
|
432
|
-
function resolveIdentifierAccess(text, compilerContext) {
|
|
433
|
-
if (compilerContext.hasIdentifier(text)) {
|
|
434
|
-
const kind = compilerContext.getIdentifierKind(text);
|
|
435
|
-
const access = `context.get('${text}')`;
|
|
436
|
-
return kind === "signal" ? `${access}()` : access;
|
|
437
|
-
}
|
|
438
|
-
return text;
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Returns true if the subtree rooted at `node` contains at least one
|
|
442
|
-
* Identifier that needs context resolution.
|
|
443
|
-
*
|
|
444
|
-
* Short-circuits as soon as one is found to avoid visiting the whole tree.
|
|
445
|
-
*/
|
|
446
|
-
function containsResolvableIdentifier(node, parent) {
|
|
447
|
-
if (isIdentifier(node) && needsResolution(node, parent)) return true;
|
|
448
|
-
let found = false;
|
|
449
|
-
forEachChild(node, (child) => {
|
|
450
|
-
if (!found) found = containsResolvableIdentifier(child, node);
|
|
451
|
-
});
|
|
452
|
-
return found;
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* Returns true if the identifier needs to be resolved against the context
|
|
456
|
-
* or prefixed with `this.` — i.e. it is not a global/builtin identifier
|
|
457
|
-
* and not the property-name side of a member access expression.
|
|
458
|
-
*/
|
|
459
|
-
function needsResolution(node, parent) {
|
|
460
|
-
return !(isPropertyAccessExpression(parent) && parent.name === node || isPropertyAssignment(parent) && parent.name === node || GLOBAL_IDENTIFIERS.has(node.text) || !node.text);
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Generates a unique variable name for a DOM element based on its tag name
|
|
464
|
-
* and parent node context.
|
|
465
|
-
*
|
|
466
|
-
* When the parent is the root node (`this._root`), the identifier is based
|
|
467
|
-
* solely on the tag name; otherwise the parent name is prepended to ensure
|
|
468
|
-
* uniqueness within nested structures. Hyphens in tag names are replaced
|
|
469
|
-
* with underscores to produce a valid JavaScript identifier.
|
|
470
|
-
*
|
|
471
|
-
* @param node - The `ElementNode` for which to generate the identifier.
|
|
472
|
-
* @param parentNode - The variable name of the parent node.
|
|
473
|
-
* @param index - A numeric suffix to disambiguate sibling elements of the same type.
|
|
474
|
-
* @returns A unique variable name string for the element.
|
|
475
|
-
*/
|
|
476
|
-
function getElementIdentifier(node, parentNode, index) {
|
|
477
|
-
return getIdentifier(node.tagName, parentNode, index);
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Generates a unique variable name for a text or interpolation node.
|
|
481
|
-
*
|
|
482
|
-
* @param prefix - Optional prefix to use instead of the default `'text'`.
|
|
483
|
-
* @param parentNode - The variable name of the parent node.
|
|
484
|
-
* @param index - A numeric suffix to disambiguate sibling text nodes.
|
|
485
|
-
* @returns A unique variable name string for the text node.
|
|
486
|
-
*/
|
|
487
|
-
function getTextIdentifier(prefix = "text", parentNode, index) {
|
|
488
|
-
return getIdentifier(prefix, parentNode, index);
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Generates a unique variable name for a control-flow block (if, else-if, else,
|
|
492
|
-
* for, switch, case, or default).
|
|
493
|
-
*
|
|
494
|
-
* @param prefix - The block type prefix (`'if'`, `'elseIf'`, `'else'`, etc.).
|
|
495
|
-
* @param parentNode - The variable name of the parent node.
|
|
496
|
-
* @param index - A suffix to disambiguate sibling blocks.
|
|
497
|
-
* @returns A unique variable name string for the block.
|
|
498
|
-
*/
|
|
499
|
-
function getBlockIdentifier(prefix, parentNode, index) {
|
|
500
|
-
return getIdentifier(prefix, parentNode, index);
|
|
501
|
-
}
|
|
502
|
-
function getIdentifier(prefix, parentNode, index) {
|
|
503
|
-
return (parentNode !== "root" ? `${parentNode}__${prefix}${index}` : `${prefix}${index}`).replace(/-/g, "_");
|
|
504
|
-
}
|
|
505
|
-
function mapDefaultOptions(options) {
|
|
506
|
-
return {
|
|
507
|
-
skipResolution: options?.skipResolution ?? false,
|
|
508
|
-
resolver: options?.resolver ?? "this"
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
//#endregion
|
|
512
|
-
//#region ../packages/compiler/src/generator/states/generate-element.state.ts
|
|
513
|
-
/**
|
|
514
|
-
* Generates code for an HTML element node: creates the DOM element, sets attributes,
|
|
515
|
-
* attaches event listeners, appends it to the parent, and recursively processes children.
|
|
516
|
-
*
|
|
517
|
-
* @param node - The `ElementNode` to process.
|
|
518
|
-
* @param index - Variable name to use for the created DOM element.
|
|
519
|
-
* @param parentNode - Variable name of the parent DOM node to append to.
|
|
520
|
-
* @param compilerContext - Current render scope context.
|
|
521
|
-
* @returns Array of generated code lines.
|
|
522
|
-
*/
|
|
523
|
-
function generateElement(node, parentNode, index, compilerContext, anchor) {
|
|
524
|
-
const attributes = mapAttributes(node.attributes, compilerContext);
|
|
525
|
-
const events = mapEvents(node.events, compilerContext);
|
|
526
|
-
const nodeName = getElementIdentifier(node, parentNode, index);
|
|
527
|
-
const tagName = node.tagName;
|
|
528
|
-
const retVal = {
|
|
529
|
-
code: [],
|
|
530
|
-
functionsToProcess: /* @__PURE__ */ new Map()
|
|
531
|
-
};
|
|
532
|
-
switch (tagName) {
|
|
533
|
-
case "svg":
|
|
534
|
-
retVal.code.push("context.createElement = createSVGElement");
|
|
535
|
-
break;
|
|
536
|
-
case "math": retVal.code.push("context.createElement = createMATHMLElement");
|
|
537
|
-
}
|
|
538
|
-
retVal.code.push(`const ${nodeName} = _renderElement(${parentNode}, context, ${anchor}, '${tagName}',`);
|
|
539
|
-
attributes.length ? retVal.code.push(...indent([
|
|
540
|
-
"[",
|
|
541
|
-
...indent(attributes),
|
|
542
|
-
"],"
|
|
543
|
-
])) : retVal.code[retVal.code.length - 1] = `${retVal.code[retVal.code.length - 1]} [],`;
|
|
544
|
-
events.length ? retVal.code.push(...indent([
|
|
545
|
-
"[",
|
|
546
|
-
...indent(events),
|
|
547
|
-
"]"
|
|
548
|
-
]), ");") : retVal.code[retVal.code.length - 1] = `${retVal.code[retVal.code.length - 1]} []);`;
|
|
549
|
-
switch (tagName) {
|
|
550
|
-
case "svg":
|
|
551
|
-
case "math": retVal.code.push("context.createElement = createElement");
|
|
552
|
-
}
|
|
553
|
-
if (node.children.length) {
|
|
554
|
-
retVal.functionsToProcess.set(`${nodeName}Children`, {
|
|
555
|
-
fn: {
|
|
556
|
-
node,
|
|
557
|
-
parentNode: nodeName,
|
|
558
|
-
context: compilerContext,
|
|
559
|
-
precode: getPrecode(tagName)
|
|
560
|
-
},
|
|
561
|
-
args: [nodeName, "parentContext"]
|
|
562
|
-
});
|
|
563
|
-
retVal.code.push(`this.${nodeName}Children(${nodeName}, context);`);
|
|
564
|
-
}
|
|
565
|
-
return retVal;
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* Maps attribute nodes to their corresponding generated code lines.
|
|
569
|
-
*
|
|
570
|
-
* @param attributes - The attribute nodes to map onto the element.
|
|
571
|
-
* @param compilerContext - Current render scope context, used to resolve identifier references.
|
|
572
|
-
* @returns Array of generated code strings, one per attribute.
|
|
573
|
-
*/
|
|
574
|
-
function mapAttributes(attributes, compilerContext) {
|
|
575
|
-
return attributes?.map(({ name, value }) => {
|
|
576
|
-
const isLiteral = typeof value === "string";
|
|
577
|
-
return `{ name: '${name}', value: () => ${isLiteral ? `'${value}'` : resolveExpression(value.expression, compilerContext)}, literal: ${isLiteral} },`;
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Generates code that attaches event listeners to a DOM element.
|
|
582
|
-
*
|
|
583
|
-
* For each event node an `addEventListener` call is emitted, binding the event
|
|
584
|
-
* to the component instance handler and exposing the native event as `$event`.
|
|
585
|
-
*
|
|
586
|
-
* @param events - The event nodes to bind to the element.
|
|
587
|
-
* @param compilerContext - Current render scope context, used to resolve identifier references.
|
|
588
|
-
* @returns Array of generated code lines, one per event listener.
|
|
589
|
-
*/
|
|
590
|
-
function mapEvents(events, compilerContext) {
|
|
591
|
-
compilerContext.addUnresolvableIdentifier("$event");
|
|
592
|
-
const mappedEvents = events.map((event) => {
|
|
593
|
-
let parsedEventParameter = false;
|
|
594
|
-
const parameters = event.parameters.map((parameter) => {
|
|
595
|
-
const resolvedParameter = resolveExpression(parameter, compilerContext);
|
|
596
|
-
if (!parsedEventParameter && resolvedParameter === "$event") {
|
|
597
|
-
parsedEventParameter = true;
|
|
598
|
-
return `($event) => ${resolvedParameter},`;
|
|
599
|
-
} else return `() => ${resolvedParameter},`;
|
|
600
|
-
});
|
|
601
|
-
const eventCode = ["{", ...indent([
|
|
602
|
-
`name: '${event.name}',`,
|
|
603
|
-
`handler: '${event.handler}',`,
|
|
604
|
-
"parameters: ["
|
|
605
|
-
])];
|
|
606
|
-
if (parameters.length) eventCode.push(...indent([...indent(parameters), "]"]), "},");
|
|
607
|
-
else {
|
|
608
|
-
eventCode[eventCode.length - 1] = `${eventCode[eventCode.length - 1]}]`;
|
|
609
|
-
eventCode.push("},");
|
|
610
|
-
}
|
|
611
|
-
return eventCode;
|
|
612
|
-
}).flat();
|
|
613
|
-
compilerContext.removeIdentifier("$event");
|
|
614
|
-
return mappedEvents;
|
|
615
|
-
}
|
|
616
|
-
function getPrecode(tagName) {
|
|
617
|
-
switch (tagName) {
|
|
618
|
-
case "svg": return "context.createElement = createSVGElement;";
|
|
619
|
-
case "math": return "context.createElement = createMATHMLElement;";
|
|
620
|
-
default: return "";
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
//#endregion
|
|
624
|
-
//#region ../packages/compiler/src/generator/states/generate-for.state.ts
|
|
625
|
-
/**
|
|
626
|
-
* Generates code for a `@for` iteration node.
|
|
627
|
-
*
|
|
628
|
-
* Emits a classic index-based `for` loop with all implicit variables
|
|
629
|
-
* declared at the top of the loop body:
|
|
630
|
-
*
|
|
631
|
-
* ```javascript
|
|
632
|
-
* for (let $i = 0; $i < ctx_items.length; $i++) {
|
|
633
|
-
* const item = items[$i];
|
|
634
|
-
* const $index = $i;
|
|
635
|
-
* const $first = $i === 0;
|
|
636
|
-
* const $last = $i === items.length - 1;
|
|
637
|
-
* const $even = $i % 2 === 0;
|
|
638
|
-
* const $odd = $i % 2 !== 0;
|
|
639
|
-
* // ... child nodes
|
|
640
|
-
* }
|
|
641
|
-
* ```
|
|
642
|
-
*
|
|
643
|
-
* The iterable identifier is resolved through the active {@link Context}:
|
|
644
|
-
* if found in scope it is used as-is, otherwise `this.` is prepended.
|
|
645
|
-
*
|
|
646
|
-
* The internal loop counter is always named `$i_<nodeName>` to avoid
|
|
647
|
-
* collisions when `@for` blocks are nested.
|
|
648
|
-
*
|
|
649
|
-
* @param node - The `ForNode` to process.
|
|
650
|
-
* @param index - Base variable name prefix used for child nodes and
|
|
651
|
-
* to produce a unique loop counter identifier.
|
|
652
|
-
* @param parentNode - Variable name of the parent DOM node.
|
|
653
|
-
* @param compilerContext - The enclosing scope context.
|
|
654
|
-
* @returns Array of generated code lines.
|
|
655
|
-
*/
|
|
656
|
-
function generateFor(node, parentNode, index, compilerContext) {
|
|
657
|
-
const retVal = {
|
|
658
|
-
code: [],
|
|
659
|
-
functionsToProcess: /* @__PURE__ */ new Map()
|
|
660
|
-
};
|
|
661
|
-
const iterableSource = node.iterableSource;
|
|
662
|
-
const iterableExpr = compilerContext.hasIdentifier(iterableSource) ? iterableSource : `this.${iterableSource}`;
|
|
663
|
-
const itemsName = getTextIdentifier("items", parentNode, index);
|
|
664
|
-
const counterName = getTextIdentifier("i", parentNode, index);
|
|
665
|
-
const indexName = resolveImplicit$1(node, "$index");
|
|
666
|
-
const firstName = resolveImplicit$1(node, "$first");
|
|
667
|
-
const lastName = resolveImplicit$1(node, "$last");
|
|
668
|
-
const evenName = resolveImplicit$1(node, "$even");
|
|
669
|
-
const oddName = resolveImplicit$1(node, "$odd");
|
|
670
|
-
const forContext = new CompilerContext([
|
|
671
|
-
node.itemAlias,
|
|
672
|
-
[indexName, "signal"],
|
|
673
|
-
[firstName, "signal"],
|
|
674
|
-
[lastName, "signal"],
|
|
675
|
-
[evenName, "signal"],
|
|
676
|
-
[oddName, "signal"]
|
|
677
|
-
], compilerContext);
|
|
678
|
-
const forKey = getBlockIdentifier("for", parentNode, index);
|
|
679
|
-
retVal.functionsToProcess.set(forKey, {
|
|
680
|
-
fn: {
|
|
681
|
-
precode: `const { vars, update } = _iterationVariables(context, ${itemsName}, ${counterName}, '${node.itemAlias}', {
|
|
682
|
-
$index: '${indexName}',
|
|
683
|
-
$first: '${firstName}',
|
|
684
|
-
$last: '${lastName}',
|
|
685
|
-
$even: '${evenName}',
|
|
686
|
-
$odd: '${oddName}'
|
|
687
|
-
});
|
|
688
|
-
const { ${node.itemAlias}, ${indexName}, ${firstName}, ${lastName}, ${evenName}, ${oddName} } = vars;`,
|
|
689
|
-
node,
|
|
690
|
-
parentNode: forKey,
|
|
691
|
-
context: forContext,
|
|
692
|
-
anchor: "anchor",
|
|
693
|
-
isForBody: true
|
|
694
|
-
},
|
|
695
|
-
args: [
|
|
696
|
-
forKey,
|
|
697
|
-
"parentContext",
|
|
698
|
-
itemsName,
|
|
699
|
-
counterName,
|
|
700
|
-
"anchor"
|
|
701
|
-
]
|
|
702
|
-
});
|
|
703
|
-
retVal.code.push(`_for(${parentNode}, context, () => ${iterableExpr}, ${node.itemAlias} => ${resolveExpression(node.trackExpression, forContext, { skipResolution: true })}, this.${forKey}.bind(this));`);
|
|
704
|
-
return retVal;
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* Resolves the name that should be used in generated code for a given
|
|
708
|
-
* implicit variable.
|
|
709
|
-
*
|
|
710
|
-
* If the template declared an explicit alias for the variable
|
|
711
|
-
* (e.g. `; $index = i`) that alias is returned. Otherwise the default
|
|
712
|
-
* implicit variable name (e.g. `$index`) is used.
|
|
713
|
-
*
|
|
714
|
-
* @param node - The `ForNode` whose implicit alias map is consulted.
|
|
715
|
-
* @param implicit - The implicit variable to look up (e.g. `'$index'`).
|
|
716
|
-
* @returns The alias string if one was declared, otherwise `implicit` itself.
|
|
717
|
-
*/
|
|
718
|
-
function resolveImplicit$1(node, implicit) {
|
|
719
|
-
return node.implicitAliases.get(implicit) ?? implicit;
|
|
720
|
-
}
|
|
721
|
-
//#endregion
|
|
722
|
-
//#region ../packages/compiler/src/generator/states/generate-if.state.ts
|
|
723
|
-
function generateIf(node, parentNode, index, compilerContext) {
|
|
724
|
-
const ifContext = new CompilerContext([], compilerContext);
|
|
725
|
-
const retVal = {
|
|
726
|
-
code: [],
|
|
727
|
-
functionsToProcess: /* @__PURE__ */ new Map()
|
|
728
|
-
};
|
|
729
|
-
retVal.code.push(`_if(${parentNode}, context, [`);
|
|
730
|
-
const ifKey = getBlockIdentifier("if", parentNode, index);
|
|
731
|
-
retVal.code.push(...indent([
|
|
732
|
-
"{",
|
|
733
|
-
...indent([`condition: () => ${resolveExpression(node.conditionNode, compilerContext)},`, `block: this.${ifKey}.bind(this)`]),
|
|
734
|
-
"},"
|
|
735
|
-
]));
|
|
736
|
-
retVal.functionsToProcess.set(ifKey, {
|
|
737
|
-
fn: {
|
|
738
|
-
node,
|
|
739
|
-
parentNode: ifKey,
|
|
740
|
-
context: ifContext,
|
|
741
|
-
anchor: "anchor"
|
|
742
|
-
},
|
|
743
|
-
args: [
|
|
744
|
-
ifKey,
|
|
745
|
-
"parentContext",
|
|
746
|
-
"anchor"
|
|
747
|
-
]
|
|
748
|
-
});
|
|
749
|
-
let alt = node.alternate;
|
|
750
|
-
let i = 0;
|
|
751
|
-
while (alt?.type === ASTNodeType.ElseIf) {
|
|
752
|
-
const elseIfContext = new CompilerContext([], compilerContext);
|
|
753
|
-
const keyElseIf = getBlockIdentifier("elseIf", parentNode, `${index}_${i}`);
|
|
754
|
-
const conditionNode = alt.conditionNode;
|
|
755
|
-
retVal.code.push(...indent([
|
|
756
|
-
"{",
|
|
757
|
-
...indent([`condition: () => ${resolveExpression(conditionNode, compilerContext)},`, `block: this.${keyElseIf}.bind(this)`]),
|
|
758
|
-
"},"
|
|
759
|
-
]));
|
|
760
|
-
retVal.functionsToProcess.set(keyElseIf, {
|
|
761
|
-
fn: {
|
|
762
|
-
node: alt,
|
|
763
|
-
parentNode,
|
|
764
|
-
context: elseIfContext,
|
|
765
|
-
anchor: "anchor"
|
|
766
|
-
},
|
|
767
|
-
args: [
|
|
768
|
-
parentNode,
|
|
769
|
-
"parentContext",
|
|
770
|
-
"anchor"
|
|
771
|
-
]
|
|
772
|
-
});
|
|
773
|
-
alt = alt.alternate;
|
|
774
|
-
i++;
|
|
775
|
-
}
|
|
776
|
-
if (alt) {
|
|
777
|
-
const elseContext = new CompilerContext([], compilerContext);
|
|
778
|
-
const keyElse = getBlockIdentifier("else", parentNode, index);
|
|
779
|
-
retVal.code.push(...indent([
|
|
780
|
-
"{",
|
|
781
|
-
...indent([`block: this.${keyElse}.bind(this)`]),
|
|
782
|
-
"},"
|
|
783
|
-
]));
|
|
784
|
-
retVal.functionsToProcess.set(keyElse, {
|
|
785
|
-
fn: {
|
|
786
|
-
node: alt,
|
|
787
|
-
parentNode,
|
|
788
|
-
context: elseContext,
|
|
789
|
-
anchor: "anchor"
|
|
790
|
-
},
|
|
791
|
-
args: [
|
|
792
|
-
parentNode,
|
|
793
|
-
"parentContext",
|
|
794
|
-
"anchor"
|
|
795
|
-
]
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
retVal.code.push("]);");
|
|
799
|
-
return retVal;
|
|
800
|
-
}
|
|
801
|
-
//#endregion
|
|
802
|
-
//#region ../packages/compiler/src/generator/states/generate-switch.state.ts
|
|
803
|
-
/**
|
|
804
|
-
* Generates code for a `@switch` node.
|
|
805
|
-
* Emits a `_switch(...)` call that delegates to the reactive `_switch` runtime utility.
|
|
806
|
-
*
|
|
807
|
-
* @param node - The `SwitchNode` to process.
|
|
808
|
-
* @param index - Base variable name prefix for child nodes.
|
|
809
|
-
* @param parentNode - Variable name of the parent DOM node.
|
|
810
|
-
* @param compilerContext - Current render scope context.
|
|
811
|
-
* @returns An object with the main block code lines and a map of helper functions to register.
|
|
812
|
-
*/
|
|
813
|
-
function generateSwitch(node, parentNode, index, compilerContext) {
|
|
814
|
-
const retVal = {
|
|
815
|
-
code: [],
|
|
816
|
-
functionsToProcess: /* @__PURE__ */ new Map()
|
|
817
|
-
};
|
|
818
|
-
retVal.code.push(`_switch(${parentNode}, context, () => ${resolveExpression(node.expression, compilerContext)}, [`);
|
|
819
|
-
node.children.forEach((caseNode, i) => {
|
|
820
|
-
const caseContext = new CompilerContext([], compilerContext);
|
|
821
|
-
const caseKey = caseNode.condition ? getBlockIdentifier("case", parentNode, `${index}_${i}`) : getBlockIdentifier("default", parentNode, index);
|
|
822
|
-
retVal.functionsToProcess.set(caseKey, {
|
|
823
|
-
fn: {
|
|
824
|
-
node: caseNode,
|
|
825
|
-
parentNode: caseKey,
|
|
826
|
-
context: caseContext
|
|
827
|
-
},
|
|
828
|
-
args: [
|
|
829
|
-
caseKey,
|
|
830
|
-
"parentContext",
|
|
831
|
-
"anchor"
|
|
832
|
-
]
|
|
833
|
-
});
|
|
834
|
-
const fnName = `this.${caseKey}.bind(this)`;
|
|
835
|
-
retVal.code.push(...indent([
|
|
836
|
-
"{",
|
|
837
|
-
...indent([`condition: ${caseNode.condition ? `[${caseNode.condition.join(", ")}]` : `null`},`, `block: ${fnName}`]),
|
|
838
|
-
"},"
|
|
839
|
-
]));
|
|
840
|
-
});
|
|
841
|
-
retVal.code.push("])");
|
|
842
|
-
return retVal;
|
|
843
|
-
}
|
|
844
|
-
//#endregion
|
|
845
|
-
//#region ../packages/compiler/src/generator/states/generate-text-and-interpolation.state.ts
|
|
846
|
-
/**
|
|
847
|
-
* Generates code for a text or interpolation node.
|
|
848
|
-
*
|
|
849
|
-
* Emits a `_renderLiteralText` call for plain text nodes and a
|
|
850
|
-
* `_renderText` call for interpolation nodes, both appending a DOM text
|
|
851
|
-
* node to the parent.
|
|
852
|
-
*
|
|
853
|
-
* @param node - A `TextNode` or `InterpolationNode` to process.
|
|
854
|
-
* @param parentNode - Variable name of the parent DOM node to append to.
|
|
855
|
-
* @returns Array of generated code lines.
|
|
856
|
-
*/
|
|
857
|
-
function generateTextAndInterpolation(node, parentNode, _index, compilerContext) {
|
|
858
|
-
return { code: [`${node.type === ASTNodeType.Text ? `_renderLiteralText(${parentNode}, context, '${node.value}');` : `_renderText(${parentNode}, context, () => ${resolveExpression(node.expression, compilerContext)});`}`] };
|
|
859
|
-
}
|
|
860
|
-
//#endregion
|
|
861
|
-
//#region ../packages/compiler/src/generator/states/skip-generation.state.ts
|
|
862
|
-
function skipGeneration(_node, _parentNode, _index, _compilerContext) {}
|
|
863
|
-
//#endregion
|
|
864
|
-
//#region ../packages/compiler/src/generator/generator.ts
|
|
865
|
-
var Generator = class {
|
|
866
|
-
_ast;
|
|
867
|
-
_nodeToProcess = /* @__PURE__ */ new Map();
|
|
868
|
-
_states = {
|
|
869
|
-
[ASTNodeType.Text]: generateTextAndInterpolation,
|
|
870
|
-
[ASTNodeType.Interpolation]: generateTextAndInterpolation,
|
|
871
|
-
[ASTNodeType.Element]: generateElement,
|
|
872
|
-
[ASTNodeType.If]: generateIf,
|
|
873
|
-
[ASTNodeType.For]: generateFor,
|
|
874
|
-
[ASTNodeType.Switch]: generateSwitch,
|
|
875
|
-
[ASTNodeType.Import]: skipGeneration
|
|
876
|
-
};
|
|
877
|
-
constructor(_ast) {
|
|
878
|
-
this._ast = _ast;
|
|
879
|
-
}
|
|
880
|
-
generate(cssVariableName) {
|
|
881
|
-
this._nodeToProcess.clear();
|
|
882
|
-
const compilerContext = new CompilerContext();
|
|
883
|
-
const generatedCode = ["_render() {", ...indent([`const ${ROOT_NODE} = this._root;`, "const context = new Context(this, { createElement: document.createElement.bind(document), get: () => undefined });"])];
|
|
884
|
-
if (cssVariableName) generatedCode.push(indent(`${ROOT_NODE}.adoptedStyleSheets = [${cssVariableName}];`));
|
|
885
|
-
for (let i = 0; i < this._ast.length; i++) {
|
|
886
|
-
const result = this._processNode(this._ast[i], ROOT_NODE, i.toString(), compilerContext, null);
|
|
887
|
-
if (result) {
|
|
888
|
-
const { code, functionsToProcess } = result;
|
|
889
|
-
functionsToProcess?.forEach((value, key) => this._nodeToProcess.set(key, value));
|
|
890
|
-
generatedCode.push(...indent(code));
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
generatedCode.push(...indent(["return context;"]), "}");
|
|
894
|
-
for (const [key, fnData] of this._nodeToProcess.entries()) {
|
|
895
|
-
const { node, parentNode, context, precode, anchor } = fnData.fn;
|
|
896
|
-
generatedCode.push(`\n${key}(${fnData.args?.join(", ")}) {`, ...indent(["const context = new Context(this, parentContext);"]));
|
|
897
|
-
if (precode) generatedCode.push(indent(precode));
|
|
898
|
-
generatedCode.push(...indent([...node.children.map((child, i) => {
|
|
899
|
-
const result = this._processNode(child, parentNode, i.toString(), context, anchor ?? null);
|
|
900
|
-
if (result) {
|
|
901
|
-
const { code, functionsToProcess } = result;
|
|
902
|
-
functionsToProcess?.forEach((value, key) => this._nodeToProcess.set(key, value));
|
|
903
|
-
return code;
|
|
904
|
-
}
|
|
905
|
-
return "";
|
|
906
|
-
}).flat(), fnData.fn.isForBody ? "return { context, update };" : "return context;"]), "}");
|
|
907
|
-
}
|
|
908
|
-
return generatedCode.join("\n");
|
|
909
|
-
}
|
|
910
|
-
_processNode(node, parentNode, index, compilerContext, anchor) {
|
|
911
|
-
const state = this._states[node.type];
|
|
912
|
-
if (!state) throw new Error(`[Generator] No transition function for token type ${ASTNodeType[node.type]}`);
|
|
913
|
-
return state(node, parentNode, index, compilerContext, anchor);
|
|
914
|
-
}
|
|
915
|
-
};
|
|
916
|
-
//#endregion
|
|
917
|
-
//#region ../packages/compiler/src/lexer/types/lexer-state.enum.ts
|
|
918
|
-
/**
|
|
919
|
-
* Represents the set of states the lexer can be in while processing template input.
|
|
920
|
-
*/
|
|
921
|
-
var LexerState = /* @__PURE__ */ function(LexerState) {
|
|
922
|
-
/**
|
|
923
|
-
* Consuming plain text content between tags or at the top level.
|
|
924
|
-
*/
|
|
925
|
-
LexerState["TEXT"] = "text";
|
|
926
|
-
/**
|
|
927
|
-
* Consuming the opening tag name after `<`.
|
|
928
|
-
*/
|
|
929
|
-
LexerState["TAG_OPEN_NAME"] = "tag-open-name";
|
|
930
|
-
/**
|
|
931
|
-
* Inside an open tag body, scanning for attributes, events, or the closing `>`.
|
|
932
|
-
*/
|
|
933
|
-
LexerState["TAG_BODY"] = "tag-body";
|
|
934
|
-
/**
|
|
935
|
-
* Processing the end of an open tag: `>` or `/>`.
|
|
936
|
-
*/
|
|
937
|
-
LexerState["TAG_OPEN_END"] = "tag-open-end";
|
|
938
|
-
/**
|
|
939
|
-
* Consuming a closing tag `</tagName>`.
|
|
940
|
-
*/
|
|
941
|
-
LexerState["TAG_CLOSE"] = "tag-close";
|
|
942
|
-
/**
|
|
943
|
-
* Consuming an HTML attribute name and its optional value.
|
|
944
|
-
*/
|
|
945
|
-
LexerState["ATTRIBUTE"] = "attribute";
|
|
946
|
-
/**
|
|
947
|
-
* Consuming a DOM event binding starting with `@`.
|
|
948
|
-
*/
|
|
949
|
-
LexerState["EVENT"] = "event";
|
|
950
|
-
/**
|
|
951
|
-
* Consuming a DOM event parameter
|
|
952
|
-
*/
|
|
953
|
-
LexerState["EVENT_PARAMETER"] = "parameter";
|
|
954
|
-
/**
|
|
955
|
-
* Dispatching a flow-control keyword (@if, @for, @switch, etc.).
|
|
956
|
-
*/
|
|
957
|
-
LexerState["FLOW_CONTROL"] = "flow-control";
|
|
958
|
-
/**
|
|
959
|
-
* Consuming the condition expression `(...)` of a flow-control directive.
|
|
960
|
-
*/
|
|
961
|
-
LexerState["FLOW_CONTROL_CONDITION"] = "flow-control-condition";
|
|
962
|
-
/**
|
|
963
|
-
* Consuming the condition expression `(...)` of a @case directive.
|
|
964
|
-
* This is needed to correctly handle special consecutives @case
|
|
965
|
-
*/
|
|
966
|
-
LexerState["CASE_FLOW_CONTROL_CONDITION"] = "case-flow-control-condition";
|
|
967
|
-
/**
|
|
968
|
-
* Consuming the opening `{` of a flow-control block body.
|
|
969
|
-
*/
|
|
970
|
-
LexerState["FLOW_CONTROL_BLOCK"] = "flow-control-block";
|
|
971
|
-
/**
|
|
972
|
-
* Consuming an attribute literal value
|
|
973
|
-
*/
|
|
974
|
-
LexerState["ATTRIBUTE_VALUE"] = "attribute-value";
|
|
975
|
-
/**
|
|
976
|
-
* Dispatching between an expression or literal interpolation after `{`.
|
|
977
|
-
*/
|
|
978
|
-
LexerState["INTERPOLATION"] = "interpolation";
|
|
979
|
-
/**
|
|
980
|
-
* Consuming a JavaScript expression inside `{ }`.
|
|
981
|
-
*/
|
|
982
|
-
LexerState["INTERPOLATION_EXPRESSION"] = "interpolation-expression";
|
|
983
|
-
/**
|
|
984
|
-
* Consuming a template-literal string inside `` {`...`} ``.
|
|
985
|
-
*/
|
|
986
|
-
LexerState["INTERPOLATION_LITERAL"] = "interpolation-literal";
|
|
987
|
-
/**
|
|
988
|
-
* Consuming an import statement `@import { X, Y, ... }
|
|
989
|
-
*/
|
|
990
|
-
LexerState["IMPORT"] = "import";
|
|
991
|
-
LexerState["IMPORT_PATH"] = "import-path";
|
|
992
|
-
return LexerState;
|
|
993
|
-
}({});
|
|
994
|
-
//#endregion
|
|
995
|
-
//#region ../packages/compiler/src/lexer/types/token-type.enum.ts
|
|
996
|
-
/**
|
|
997
|
-
* Discriminant values that identify the type of each token emitted by the lexer.
|
|
998
|
-
*/
|
|
999
|
-
var TokenType = /* @__PURE__ */ function(TokenType) {
|
|
1000
|
-
/**
|
|
1001
|
-
* A plain text node between tags or at the top level.
|
|
1002
|
-
*/
|
|
1003
|
-
TokenType[TokenType["TEXT"] = 0] = "TEXT";
|
|
1004
|
-
/**
|
|
1005
|
-
* The name portion of an opening tag, e.g. `div` in `<div`.
|
|
1006
|
-
*/
|
|
1007
|
-
TokenType[TokenType["TAG_OPEN_NAME"] = 1] = "TAG_OPEN_NAME";
|
|
1008
|
-
/**
|
|
1009
|
-
* A self-closing tag marker `/>`.
|
|
1010
|
-
*/
|
|
1011
|
-
TokenType[TokenType["TAG_SELF_CLOSE"] = 2] = "TAG_SELF_CLOSE";
|
|
1012
|
-
/**
|
|
1013
|
-
* The closing `>` of an opening tag.
|
|
1014
|
-
*/
|
|
1015
|
-
TokenType[TokenType["TAG_OPEN_END"] = 3] = "TAG_OPEN_END";
|
|
1016
|
-
/**
|
|
1017
|
-
* The name portion of a closing tag, e.g. `div` in `</div>`.
|
|
1018
|
-
*/
|
|
1019
|
-
TokenType[TokenType["TAG_CLOSE_NAME"] = 4] = "TAG_CLOSE_NAME";
|
|
1020
|
-
/**
|
|
1021
|
-
* An HTML attribute
|
|
1022
|
-
*/
|
|
1023
|
-
TokenType[TokenType["ATTRIBUTE"] = 5] = "ATTRIBUTE";
|
|
1024
|
-
/**
|
|
1025
|
-
* An HTML attribute literal value
|
|
1026
|
-
*/
|
|
1027
|
-
TokenType[TokenType["ATTRIBUTE_VALUE"] = 6] = "ATTRIBUTE_VALUE";
|
|
1028
|
-
/**
|
|
1029
|
-
* A DOM event binding declared with `@eventName=handler`.
|
|
1030
|
-
*/
|
|
1031
|
-
TokenType[TokenType["EVENT"] = 7] = "EVENT";
|
|
1032
|
-
/**
|
|
1033
|
-
* An event paremeter included in a event call '()'
|
|
1034
|
-
*/
|
|
1035
|
-
TokenType[TokenType["EVENT_PAREMETER"] = 8] = "EVENT_PAREMETER";
|
|
1036
|
-
/**
|
|
1037
|
-
* A template-literal interpolation string enclosed in `` {`...`} ``.
|
|
1038
|
-
*/
|
|
1039
|
-
TokenType[TokenType["INTERPOLATION_LITERAL"] = 9] = "INTERPOLATION_LITERAL";
|
|
1040
|
-
/**
|
|
1041
|
-
* A JavaScript expression interpolation enclosed in `{ }`.
|
|
1042
|
-
*/
|
|
1043
|
-
TokenType[TokenType["INTERPOLATION_EXPRESSION"] = 10] = "INTERPOLATION_EXPRESSION";
|
|
1044
|
-
/**
|
|
1045
|
-
* Opening keyword of an `@if` directive.
|
|
1046
|
-
*/
|
|
1047
|
-
TokenType[TokenType["IF"] = 11] = "IF";
|
|
1048
|
-
/**
|
|
1049
|
-
* Opening keyword of a `@for` directive.
|
|
1050
|
-
*/
|
|
1051
|
-
TokenType[TokenType["FOR"] = 12] = "FOR";
|
|
1052
|
-
/**
|
|
1053
|
-
* Opening keyword of an `@else` branch.
|
|
1054
|
-
*/
|
|
1055
|
-
TokenType[TokenType["ELSE"] = 13] = "ELSE";
|
|
1056
|
-
/**
|
|
1057
|
-
* Opening keyword of an `@else if` branch.
|
|
1058
|
-
*/
|
|
1059
|
-
TokenType[TokenType["ELSE_IF"] = 14] = "ELSE_IF";
|
|
1060
|
-
/**
|
|
1061
|
-
* Opening keyword of a `@switch` directive.
|
|
1062
|
-
*/
|
|
1063
|
-
TokenType[TokenType["SWITCH"] = 15] = "SWITCH";
|
|
1064
|
-
/**
|
|
1065
|
-
* Opening keyword of a `@case` branch.
|
|
1066
|
-
*/
|
|
1067
|
-
TokenType[TokenType["CASE"] = 16] = "CASE";
|
|
1068
|
-
/**
|
|
1069
|
-
* Opening keyword of a `@default` branch.
|
|
1070
|
-
*/
|
|
1071
|
-
TokenType[TokenType["DEFAULT"] = 17] = "DEFAULT";
|
|
1072
|
-
/**
|
|
1073
|
-
* The condition expression `(...)` associated with a flow-control directive.
|
|
1074
|
-
*/
|
|
1075
|
-
TokenType[TokenType["CONDITION"] = 18] = "CONDITION";
|
|
1076
|
-
/**
|
|
1077
|
-
* The opening `{` of a flow-control block body.
|
|
1078
|
-
*/
|
|
1079
|
-
TokenType[TokenType["BLOCK_OPEN"] = 19] = "BLOCK_OPEN";
|
|
1080
|
-
/**
|
|
1081
|
-
* The closing `}` of a flow-control block body.
|
|
1082
|
-
*/
|
|
1083
|
-
TokenType[TokenType["BLOCK_CLOSE"] = 20] = "BLOCK_CLOSE";
|
|
1084
|
-
TokenType[TokenType["IMPORT"] = 21] = "IMPORT";
|
|
1085
|
-
TokenType[TokenType["IMPORT_PATH"] = 22] = "IMPORT_PATH";
|
|
1086
|
-
/**
|
|
1087
|
-
* Sentinel token emitted when the end of the input is reached.
|
|
1088
|
-
*/
|
|
1089
|
-
TokenType[TokenType["EOF"] = 23] = "EOF";
|
|
1090
|
-
return TokenType;
|
|
1091
|
-
}({});
|
|
1092
|
-
//#endregion
|
|
1093
|
-
//#region ../packages/compiler/src/lexer/states/lex-attribute-value.state.ts
|
|
1094
|
-
/**
|
|
1095
|
-
* Consumes a quoted attribute value `"..."`, collecting characters until
|
|
1096
|
-
* the closing `"` is found. Emits an ATTRIBUTE_VALUE token and transitions
|
|
1097
|
-
* back to TAG_BODY.
|
|
1098
|
-
*
|
|
1099
|
-
* @param cursor - The lexer cursor positioned at the first character of the value (after the opening `"`).
|
|
1100
|
-
* @param _context - Unused lexer context.
|
|
1101
|
-
* @returns Transition result with the ATTRIBUTE_VALUE token and the TAG_BODY state.
|
|
1102
|
-
*/
|
|
1103
|
-
function lexAttributeValue(cursor, _context) {
|
|
1104
|
-
let read = true;
|
|
1105
|
-
let value = "";
|
|
1106
|
-
let retVal;
|
|
1107
|
-
while (read) switch (cursor.peek()) {
|
|
1108
|
-
case 34:
|
|
1109
|
-
cursor.advance();
|
|
1110
|
-
read = false;
|
|
1111
|
-
retVal = {
|
|
1112
|
-
state: LexerState.TAG_BODY,
|
|
1113
|
-
tokens: [{
|
|
1114
|
-
type: TokenType.ATTRIBUTE_VALUE,
|
|
1115
|
-
parts: [value]
|
|
1116
|
-
}],
|
|
1117
|
-
popState: true
|
|
1118
|
-
};
|
|
1119
|
-
break;
|
|
1120
|
-
default:
|
|
1121
|
-
cursor.advance();
|
|
1122
|
-
value = `${value}${cursor.currentChar.value}`;
|
|
1123
|
-
}
|
|
1124
|
-
return retVal;
|
|
1125
|
-
}
|
|
1126
|
-
//#endregion
|
|
1127
|
-
//#region ../packages/compiler/src/lexer/states/lex-attribute.state.ts
|
|
1128
|
-
/**
|
|
1129
|
-
* Consumes an attribute name and optional value from the current position,
|
|
1130
|
-
* transitioning back to TAG_BODY when a space, `/`, or `>` is encountered.
|
|
1131
|
-
* If the attribute value is an interpolation, pushes the INTERPOLATION state.
|
|
1132
|
-
*
|
|
1133
|
-
* @param cursor - The lexer cursor positioned at the start of the attribute.
|
|
1134
|
-
* @param _context - Unused lexer context.
|
|
1135
|
-
* @returns Transition result with the ATTRIBUTE token and next state.
|
|
1136
|
-
*/
|
|
1137
|
-
function lexAttribute(cursor, _context) {
|
|
1138
|
-
let read = true;
|
|
1139
|
-
let attribute = "";
|
|
1140
|
-
let retVal;
|
|
1141
|
-
while (read) switch (cursor.peek()) {
|
|
1142
|
-
case 32:
|
|
1143
|
-
cursor.advance();
|
|
1144
|
-
read = false;
|
|
1145
|
-
retVal = {
|
|
1146
|
-
state: LexerState.TAG_BODY,
|
|
1147
|
-
tokens: [{
|
|
1148
|
-
type: TokenType.ATTRIBUTE,
|
|
1149
|
-
parts: [attribute]
|
|
1150
|
-
}]
|
|
1151
|
-
};
|
|
1152
|
-
break;
|
|
1153
|
-
case 61:
|
|
1154
|
-
cursor.advance();
|
|
1155
|
-
if (cursor.peek() !== 34) {
|
|
1156
|
-
const { row, column } = cursor.position;
|
|
1157
|
-
throw new Error(`Attribute value must start with double quotes '"'.Row ${row} Col ${column}`);
|
|
1158
|
-
}
|
|
1159
|
-
cursor.advance();
|
|
1160
|
-
read = false;
|
|
1161
|
-
retVal = {
|
|
1162
|
-
state: cursor.peek() === 123 ? LexerState.INTERPOLATION : LexerState.ATTRIBUTE_VALUE,
|
|
1163
|
-
pushState: true,
|
|
1164
|
-
tokens: [{
|
|
1165
|
-
type: TokenType.ATTRIBUTE,
|
|
1166
|
-
parts: [attribute]
|
|
1167
|
-
}]
|
|
1168
|
-
};
|
|
1169
|
-
break;
|
|
1170
|
-
default:
|
|
1171
|
-
cursor.advance();
|
|
1172
|
-
attribute = `${attribute}${cursor.currentChar.value}`;
|
|
1173
|
-
}
|
|
1174
|
-
return retVal;
|
|
1175
|
-
}
|
|
1176
|
-
//#endregion
|
|
1177
|
-
//#region ../packages/compiler/src/lexer/utils/consume-flow-control-condition.utils.ts
|
|
1178
|
-
/**
|
|
1179
|
-
* Consumes a parenthesised flow-control condition expression from the cursor.
|
|
1180
|
-
*
|
|
1181
|
-
* Skips leading whitespace, then expects `(` followed by a balanced
|
|
1182
|
-
* parenthesised expression. Handles nested parentheses and returns the
|
|
1183
|
-
* raw expression string (excluding the outer `(` and `)`).
|
|
1184
|
-
*
|
|
1185
|
-
* @param cursor - The lexer cursor positioned before the opening `(`.
|
|
1186
|
-
* @param _context - Unused lexer context (kept for signature consistency).
|
|
1187
|
-
* @returns The raw expression string extracted from inside the parentheses.
|
|
1188
|
-
* @throws When the next non-space character is not `(`.
|
|
1189
|
-
*/
|
|
1190
|
-
function consumeFlowControlCondition(cursor, _context) {
|
|
1191
|
-
cursor.skipSpaces();
|
|
1192
|
-
if (cursor.peek() !== 40) throw new Error(`Expected '(' but got '${String.fromCharCode(cursor.peek())}' at row ${cursor.position.row}, col ${cursor.position.column}`);
|
|
1193
|
-
cursor.advance();
|
|
1194
|
-
let expression = "";
|
|
1195
|
-
let depth = 1;
|
|
1196
|
-
while (depth > 0) switch (cursor.peek()) {
|
|
1197
|
-
case 40:
|
|
1198
|
-
depth++;
|
|
1199
|
-
expression = addCharacter$3(cursor, expression);
|
|
1200
|
-
break;
|
|
1201
|
-
case 41:
|
|
1202
|
-
depth--;
|
|
1203
|
-
if (!depth) {
|
|
1204
|
-
cursor.advance();
|
|
1205
|
-
break;
|
|
1206
|
-
}
|
|
1207
|
-
expression = addCharacter$3(cursor, expression);
|
|
1208
|
-
break;
|
|
1209
|
-
default: expression = addCharacter$3(cursor, expression);
|
|
1210
|
-
}
|
|
1211
|
-
return expression;
|
|
1212
|
-
}
|
|
1213
|
-
/**
|
|
1214
|
-
* Advances the cursor by one character and appends it to the accumulator string.
|
|
1215
|
-
*
|
|
1216
|
-
* @param cursor - The lexer cursor to advance.
|
|
1217
|
-
* @param expression - The current accumulated expression string.
|
|
1218
|
-
* @returns The updated string with the newly consumed character appended.
|
|
1219
|
-
*/
|
|
1220
|
-
function addCharacter$3(cursor, expression) {
|
|
1221
|
-
cursor.advance();
|
|
1222
|
-
return `${expression}${cursor.currentChar.value}`;
|
|
1223
|
-
}
|
|
1224
|
-
//#endregion
|
|
1225
|
-
//#region ../packages/compiler/src/lexer/states/lex-case-flow-control-condition.state.ts
|
|
1226
|
-
/**
|
|
1227
|
-
* Consumes the condition expression `(...)` of a flow-control directive,
|
|
1228
|
-
* handling nested parentheses correctly. Emits a CONDITION token with the
|
|
1229
|
-
* raw expression string and transitions to FLOW_CONTROL_BLOCK.
|
|
1230
|
-
*
|
|
1231
|
-
* @param cursor - The lexer cursor positioned at the opening `(`.
|
|
1232
|
-
* @param _context - Unused lexer context.
|
|
1233
|
-
* @returns Transition result with the CONDITION token and the FLOW_CONTROL_BLOCK state.
|
|
1234
|
-
*/
|
|
1235
|
-
function lexCaseFlowControlCondition(cursor, _context) {
|
|
1236
|
-
const condition = consumeFlowControlCondition(cursor, _context);
|
|
1237
|
-
cursor.skipSpaces();
|
|
1238
|
-
return {
|
|
1239
|
-
state: cursor.peekMatch("@case") ? LexerState.FLOW_CONTROL : LexerState.FLOW_CONTROL_BLOCK,
|
|
1240
|
-
tokens: [{
|
|
1241
|
-
type: TokenType.CONDITION,
|
|
1242
|
-
parts: [condition]
|
|
1243
|
-
}],
|
|
1244
|
-
popState: true
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
//#endregion
|
|
1248
|
-
//#region ../packages/compiler/src/lexer/states/lex-default-flow-control-condition.state.ts
|
|
1249
|
-
/**
|
|
1250
|
-
* Consumes the condition expression `(...)` of a flow-control directive,
|
|
1251
|
-
* handling nested parentheses correctly. Emits a CONDITION token with the
|
|
1252
|
-
* raw expression string and transitions to FLOW_CONTROL_BLOCK.
|
|
1253
|
-
*
|
|
1254
|
-
* @param cursor - The lexer cursor positioned at the opening `(`.
|
|
1255
|
-
* @param _context - Unused lexer context.
|
|
1256
|
-
* @returns Transition result with the CONDITION token and the FLOW_CONTROL_BLOCK state.
|
|
1257
|
-
*/
|
|
1258
|
-
function lexDefaultFlowControlCondition(cursor, _context) {
|
|
1259
|
-
return {
|
|
1260
|
-
state: LexerState.FLOW_CONTROL_BLOCK,
|
|
1261
|
-
tokens: [{
|
|
1262
|
-
type: TokenType.CONDITION,
|
|
1263
|
-
parts: [consumeFlowControlCondition(cursor, _context)]
|
|
1264
|
-
}],
|
|
1265
|
-
popState: true
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
//#endregion
|
|
1269
|
-
//#region ../packages/compiler/src/lexer/states/lex-event-parameter.state.ts
|
|
1270
|
-
/**
|
|
1271
|
-
* Consumes an event parameter and reads until a ',' or ')'.
|
|
1272
|
-
* Emits an EVENT_ATTRIBUTE token containing the raw paremeter string.
|
|
1273
|
-
*
|
|
1274
|
-
* @param cursor - The lexer cursor positioned on the `@` character.
|
|
1275
|
-
* @param _context - Unused lexer context.
|
|
1276
|
-
* @returns Transition result with the EVENT_PAREMETER token and the EVENT state.
|
|
1277
|
-
*/
|
|
1278
|
-
function lexEventParameter(cursor, _context) {
|
|
1279
|
-
let read = true;
|
|
1280
|
-
let eventParameter = "";
|
|
1281
|
-
let charDelimiter = "";
|
|
1282
|
-
const retVal = {
|
|
1283
|
-
state: LexerState.TAG_BODY,
|
|
1284
|
-
tokens: []
|
|
1285
|
-
};
|
|
1286
|
-
while (read) switch (cursor.peek()) {
|
|
1287
|
-
case 91:
|
|
1288
|
-
case 93:
|
|
1289
|
-
case 123:
|
|
1290
|
-
case 125:
|
|
1291
|
-
case 34:
|
|
1292
|
-
case 39:
|
|
1293
|
-
case 40:
|
|
1294
|
-
eventParameter = addCharacter$2(cursor, eventParameter);
|
|
1295
|
-
if (!charDelimiter) charDelimiter = cursor.currentChar.value;
|
|
1296
|
-
else if (charDelimiter === cursor.currentChar.value || charDelimiter === "[" && cursor.currentChar.value === "]" || charDelimiter === "{" && cursor.currentChar.value === "}") charDelimiter = "";
|
|
1297
|
-
break;
|
|
1298
|
-
case 44:
|
|
1299
|
-
if (!charDelimiter) {
|
|
1300
|
-
retVal.tokens.push({
|
|
1301
|
-
type: TokenType.EVENT_PAREMETER,
|
|
1302
|
-
parts: [eventParameter]
|
|
1303
|
-
});
|
|
1304
|
-
cursor.advance();
|
|
1305
|
-
cursor.skipSpaces();
|
|
1306
|
-
eventParameter = "";
|
|
1307
|
-
} else eventParameter = addCharacter$2(cursor, eventParameter);
|
|
1308
|
-
break;
|
|
1309
|
-
case 41:
|
|
1310
|
-
cursor.advance();
|
|
1311
|
-
if (!charDelimiter) {
|
|
1312
|
-
retVal.tokens.push({
|
|
1313
|
-
type: TokenType.EVENT_PAREMETER,
|
|
1314
|
-
parts: [eventParameter]
|
|
1315
|
-
});
|
|
1316
|
-
read = false;
|
|
1317
|
-
}
|
|
1318
|
-
default: eventParameter = addCharacter$2(cursor, eventParameter);
|
|
1319
|
-
}
|
|
1320
|
-
return retVal;
|
|
1321
|
-
}
|
|
1322
|
-
function addCharacter$2(cursor, eventParameter) {
|
|
1323
|
-
cursor.advance();
|
|
1324
|
-
return `${eventParameter}${cursor.currentChar.value}`;
|
|
1325
|
-
}
|
|
1326
|
-
//#endregion
|
|
1327
|
-
//#region ../packages/compiler/src/lexer/states/lex-event.state.ts
|
|
1328
|
-
/**
|
|
1329
|
-
* Consumes a DOM event binding starting with `@` and reads until a delimiter
|
|
1330
|
-
* (space, `/`, or `>`) is found. Emits an EVENT token containing the raw binding string.
|
|
1331
|
-
*
|
|
1332
|
-
* @param cursor - The lexer cursor positioned on the `@` character.
|
|
1333
|
-
* @param _context - Unused lexer context.
|
|
1334
|
-
* @returns Transition result with the EVENT token and the TAG_BODY state.
|
|
1335
|
-
*/
|
|
1336
|
-
function lexEvent(cursor, _context) {
|
|
1337
|
-
let read = true;
|
|
1338
|
-
let event = "";
|
|
1339
|
-
let retVal;
|
|
1340
|
-
cursor.advance();
|
|
1341
|
-
while (read) switch (cursor.peek()) {
|
|
1342
|
-
case 32:
|
|
1343
|
-
case 47:
|
|
1344
|
-
case 62:
|
|
1345
|
-
retVal = { state: LexerState.TAG_BODY };
|
|
1346
|
-
read = false;
|
|
1347
|
-
break;
|
|
1348
|
-
case 40:
|
|
1349
|
-
let state = LexerState.EVENT_PARAMETER;
|
|
1350
|
-
cursor.advance();
|
|
1351
|
-
cursor.skipSpaces();
|
|
1352
|
-
if (cursor.peek() === 41) {
|
|
1353
|
-
state = LexerState.TAG_BODY;
|
|
1354
|
-
cursor.advance(2);
|
|
1355
|
-
}
|
|
1356
|
-
retVal = {
|
|
1357
|
-
state,
|
|
1358
|
-
tokens: [{
|
|
1359
|
-
type: TokenType.EVENT,
|
|
1360
|
-
parts: [event]
|
|
1361
|
-
}]
|
|
1362
|
-
};
|
|
1363
|
-
read = false;
|
|
1364
|
-
break;
|
|
1365
|
-
default:
|
|
1366
|
-
cursor.advance();
|
|
1367
|
-
event = `${event}${cursor.currentChar.value}`;
|
|
1368
|
-
}
|
|
1369
|
-
return retVal;
|
|
1370
|
-
}
|
|
1371
|
-
//#endregion
|
|
1372
|
-
//#region ../packages/compiler/src/lexer/states/lex-flow-control.ts
|
|
1373
|
-
/**
|
|
1374
|
-
* Dispatches on a `@keyword` to determine which flow-control directive begins here.
|
|
1375
|
-
* Recognises `@if`, `@for`, `@else`, `@switch`, `@case`, `@default`, and `@const`.
|
|
1376
|
-
* Advances the cursor past the keyword and transitions to the appropriate next state.
|
|
1377
|
-
*
|
|
1378
|
-
* @param cursor - The lexer cursor positioned on the `@` character.
|
|
1379
|
-
* @param _context - Unused lexer context.
|
|
1380
|
-
* @returns Transition result with the matching flow-control token and next state.
|
|
1381
|
-
*/
|
|
1382
|
-
function lexFlowControl(cursor, _context) {
|
|
1383
|
-
let retVal;
|
|
1384
|
-
cursor.advance();
|
|
1385
|
-
if (cursor.peekMatch("for ")) {
|
|
1386
|
-
cursor.advance(4);
|
|
1387
|
-
retVal = {
|
|
1388
|
-
state: LexerState.FLOW_CONTROL_CONDITION,
|
|
1389
|
-
tokens: [{ type: TokenType.FOR }],
|
|
1390
|
-
pushState: true
|
|
1391
|
-
};
|
|
1392
|
-
} else if (cursor.peekMatch("if ")) {
|
|
1393
|
-
cursor.advance(2);
|
|
1394
|
-
retVal = {
|
|
1395
|
-
state: LexerState.FLOW_CONTROL_CONDITION,
|
|
1396
|
-
tokens: [{ type: TokenType.IF }],
|
|
1397
|
-
pushState: true
|
|
1398
|
-
};
|
|
1399
|
-
} else if (cursor.peekMatch("else if ")) {
|
|
1400
|
-
cursor.advance(8);
|
|
1401
|
-
retVal = {
|
|
1402
|
-
state: LexerState.FLOW_CONTROL_CONDITION,
|
|
1403
|
-
tokens: [{ type: TokenType.ELSE_IF }],
|
|
1404
|
-
pushState: true
|
|
1405
|
-
};
|
|
1406
|
-
} else if (cursor.peekMatch("else ")) {
|
|
1407
|
-
cursor.advance(5);
|
|
1408
|
-
retVal = {
|
|
1409
|
-
state: LexerState.FLOW_CONTROL_BLOCK,
|
|
1410
|
-
tokens: [{ type: TokenType.ELSE }]
|
|
1411
|
-
};
|
|
1412
|
-
} else if (cursor.peekMatch("switch ")) {
|
|
1413
|
-
cursor.advance(7);
|
|
1414
|
-
retVal = {
|
|
1415
|
-
state: LexerState.FLOW_CONTROL_CONDITION,
|
|
1416
|
-
tokens: [{ type: TokenType.SWITCH }],
|
|
1417
|
-
pushState: true
|
|
1418
|
-
};
|
|
1419
|
-
} else if (cursor.peekMatch("case ")) {
|
|
1420
|
-
cursor.advance(5);
|
|
1421
|
-
retVal = {
|
|
1422
|
-
state: LexerState.CASE_FLOW_CONTROL_CONDITION,
|
|
1423
|
-
tokens: [{ type: TokenType.CASE }],
|
|
1424
|
-
pushState: true
|
|
1425
|
-
};
|
|
1426
|
-
} else if (cursor.peekMatch("default ")) {
|
|
1427
|
-
cursor.advance(8);
|
|
1428
|
-
retVal = {
|
|
1429
|
-
state: LexerState.FLOW_CONTROL_BLOCK,
|
|
1430
|
-
tokens: [{ type: TokenType.DEFAULT }]
|
|
1431
|
-
};
|
|
1432
|
-
} else if (cursor.peekMatch("import ")) {
|
|
1433
|
-
cursor.advance(7);
|
|
1434
|
-
retVal = { state: LexerState.IMPORT };
|
|
1435
|
-
}
|
|
1436
|
-
return retVal;
|
|
1437
|
-
}
|
|
1438
|
-
//#endregion
|
|
1439
|
-
//#region ../packages/compiler/src/lexer/states/lex-flow-control-block.state.ts
|
|
1440
|
-
/**
|
|
1441
|
-
* Consumes the opening `{` of a flow-control block body,
|
|
1442
|
-
* skipping any leading whitespace before it.
|
|
1443
|
-
*
|
|
1444
|
-
* Emits a BLOCK_OPEN token and transitions to TEXT, pushing FLOW_CONTROL_BLOCK
|
|
1445
|
-
* onto the state stack so that `consumeText` later recognises the matching `}`
|
|
1446
|
-
* as a BLOCK_CLOSE rather than an interpolation boundary.
|
|
1447
|
-
*
|
|
1448
|
-
* Used by: `@if`, `@for`, `@switch`, `@case`, `@else`, `@default`.
|
|
1449
|
-
*
|
|
1450
|
-
* @param cursor - The lexer cursor positioned before the opening `{`.
|
|
1451
|
-
* @param _context - Unused lexer context.
|
|
1452
|
-
* @returns Transition result with the BLOCK_OPEN token and the TEXT state.
|
|
1453
|
-
*/
|
|
1454
|
-
function lexFlowControlBlock(cursor, _context) {
|
|
1455
|
-
cursor.skipSpaces();
|
|
1456
|
-
if (cursor.peek() !== 123) throw new Error(`Expected '{' but got '${String.fromCharCode(cursor.peek())}' at row ${cursor.position.row}, col ${cursor.position.column}`);
|
|
1457
|
-
cursor.advance();
|
|
1458
|
-
return {
|
|
1459
|
-
state: LexerState.TEXT,
|
|
1460
|
-
tokens: [{ type: TokenType.BLOCK_OPEN }],
|
|
1461
|
-
pushState: true
|
|
1462
|
-
};
|
|
1463
|
-
}
|
|
1464
|
-
//#endregion
|
|
1465
|
-
//#region ../packages/compiler/src/lexer/states/lex-import-path.state.ts
|
|
1466
|
-
function lexImportPath(cursor, _context) {
|
|
1467
|
-
let read = true;
|
|
1468
|
-
let path = "";
|
|
1469
|
-
let retVal;
|
|
1470
|
-
let singleQuote = false;
|
|
1471
|
-
cursor.skipSpaces();
|
|
1472
|
-
if (!cursor.peekMatch("from")) throw new Error(`Expected from keywork after list of imports at ${cursor.currentChar}`);
|
|
1473
|
-
cursor.advance(4);
|
|
1474
|
-
cursor.skipSpaces();
|
|
1475
|
-
cursor.advance();
|
|
1476
|
-
switch (cursor.currentChar.code) {
|
|
1477
|
-
case 39:
|
|
1478
|
-
singleQuote = true;
|
|
1479
|
-
break;
|
|
1480
|
-
case 34: break;
|
|
1481
|
-
default: throw new Error(`Import statement must start with ' or ".\nFound character ${cursor.currentChar.value} at ${cursor.formattedPosition}`);
|
|
1482
|
-
}
|
|
1483
|
-
const delimiter = singleQuote ? 39 : 34;
|
|
1484
|
-
while (read) switch (cursor.peek()) {
|
|
1485
|
-
case delimiter:
|
|
1486
|
-
cursor.advance();
|
|
1487
|
-
read = false;
|
|
1488
|
-
retVal = {
|
|
1489
|
-
state: LexerState.TEXT,
|
|
1490
|
-
tokens: [{
|
|
1491
|
-
type: TokenType.IMPORT_PATH,
|
|
1492
|
-
parts: [path]
|
|
1493
|
-
}]
|
|
1494
|
-
};
|
|
1495
|
-
break;
|
|
1496
|
-
default:
|
|
1497
|
-
cursor.advance();
|
|
1498
|
-
path = `${path}${cursor.currentChar.value}`;
|
|
1499
|
-
}
|
|
1500
|
-
return retVal;
|
|
1501
|
-
}
|
|
1502
|
-
//#endregion
|
|
1503
|
-
//#region ../packages/compiler/src/lexer/states/lex-import.state.ts
|
|
1504
|
-
function lexImport(cursor, _context) {
|
|
1505
|
-
let read = true;
|
|
1506
|
-
let importValue = "";
|
|
1507
|
-
let retVal = {
|
|
1508
|
-
state: LexerState.IMPORT_PATH,
|
|
1509
|
-
tokens: []
|
|
1510
|
-
};
|
|
1511
|
-
cursor.skipSpaces();
|
|
1512
|
-
cursor.advance();
|
|
1513
|
-
if (cursor.currentChar.code !== 123) throw new Error(`Expected { after @import at ${cursor.formattedPosition}`);
|
|
1514
|
-
while (read) switch (cursor.peek()) {
|
|
1515
|
-
case 32:
|
|
1516
|
-
cursor.skipSpaces();
|
|
1517
|
-
break;
|
|
1518
|
-
case 44:
|
|
1519
|
-
addImport(retVal, cursor, importValue);
|
|
1520
|
-
importValue = "";
|
|
1521
|
-
break;
|
|
1522
|
-
case 125:
|
|
1523
|
-
addImport(retVal, cursor, importValue);
|
|
1524
|
-
read = false;
|
|
1525
|
-
break;
|
|
1526
|
-
default:
|
|
1527
|
-
cursor.advance();
|
|
1528
|
-
importValue = `${importValue}${cursor.currentChar.value}`;
|
|
1529
|
-
}
|
|
1530
|
-
return retVal;
|
|
1531
|
-
}
|
|
1532
|
-
function addImport(retVal, cursor, value) {
|
|
1533
|
-
cursor.advance();
|
|
1534
|
-
retVal.tokens.push({
|
|
1535
|
-
type: TokenType.IMPORT,
|
|
1536
|
-
parts: [value]
|
|
1537
|
-
});
|
|
1538
|
-
}
|
|
1539
|
-
//#endregion
|
|
1540
|
-
//#region ../packages/compiler/src/lexer/states/lex-interpolation-expression.state.ts
|
|
1541
|
-
/**
|
|
1542
|
-
* Consumes a JavaScript expression interpolation `{ expression }`, tracking nested
|
|
1543
|
-
* brace depth. Emits an INTERPOLATION_EXPRESSION token and pops the state stack to
|
|
1544
|
-
* return to the previous state (ATTRIBUTE or TEXT).
|
|
1545
|
-
*
|
|
1546
|
-
* @param cursor - The lexer cursor positioned at the first character of the expression.
|
|
1547
|
-
* @param context - The lexer context used to retrieve the previous state for restoration.
|
|
1548
|
-
* @returns Transition result with the INTERPOLATION_EXPRESSION token and restored state.
|
|
1549
|
-
*/
|
|
1550
|
-
function lexInterpolationExpression(cursor, context) {
|
|
1551
|
-
let read = true;
|
|
1552
|
-
let interpolation = "";
|
|
1553
|
-
let deep = 1;
|
|
1554
|
-
let retVal;
|
|
1555
|
-
while (read) switch (cursor.peek()) {
|
|
1556
|
-
case 123:
|
|
1557
|
-
deep++;
|
|
1558
|
-
interpolation = addCharacter$1(cursor, interpolation);
|
|
1559
|
-
break;
|
|
1560
|
-
case 125:
|
|
1561
|
-
deep--;
|
|
1562
|
-
if (deep === 0) {
|
|
1563
|
-
cursor.advance();
|
|
1564
|
-
interpolation = interpolation.trimEnd();
|
|
1565
|
-
const previousState = context.history.pop();
|
|
1566
|
-
let state;
|
|
1567
|
-
switch (previousState) {
|
|
1568
|
-
case LexerState.ATTRIBUTE:
|
|
1569
|
-
if (cursor.peek() !== 34) throw new Error(`Interpolation must end with double quotes '"' Found ${String.fromCharCode(cursor.peek())} at ${cursor.formattedPosition}`);
|
|
1570
|
-
cursor.advance();
|
|
1571
|
-
state = LexerState.TAG_BODY;
|
|
1572
|
-
break;
|
|
1573
|
-
case LexerState.TEXT: state = LexerState.TEXT;
|
|
1574
|
-
}
|
|
1575
|
-
retVal = {
|
|
1576
|
-
state,
|
|
1577
|
-
tokens: [{
|
|
1578
|
-
type: TokenType.INTERPOLATION_EXPRESSION,
|
|
1579
|
-
parts: [interpolation]
|
|
1580
|
-
}],
|
|
1581
|
-
popState: true
|
|
1582
|
-
};
|
|
1583
|
-
read = false;
|
|
1584
|
-
} else interpolation = addCharacter$1(cursor, interpolation);
|
|
1585
|
-
break;
|
|
1586
|
-
default: interpolation = addCharacter$1(cursor, interpolation);
|
|
1587
|
-
}
|
|
1588
|
-
return retVal;
|
|
1589
|
-
}
|
|
1590
|
-
/**
|
|
1591
|
-
* Advances the cursor by one character and appends it to the accumulator string.
|
|
1592
|
-
*
|
|
1593
|
-
* @param cursor - The lexer cursor to advance.
|
|
1594
|
-
* @param interpolation - The current accumulated expression string.
|
|
1595
|
-
* @returns The updated string with the newly consumed character appended.
|
|
1596
|
-
*/
|
|
1597
|
-
function addCharacter$1(cursor, interpolation) {
|
|
1598
|
-
cursor.advance(1);
|
|
1599
|
-
return `${interpolation}${cursor.currentChar.value}`;
|
|
1600
|
-
}
|
|
1601
|
-
//#endregion
|
|
1602
|
-
//#region ../packages/compiler/src/lexer/states/lex-interpolation-literal.state.ts
|
|
1603
|
-
/**
|
|
1604
|
-
* Consumes a template-literal interpolation `` {`...`} ``, collecting characters
|
|
1605
|
-
* until the closing backtick followed by `}`. Emits an INTERPOLATION_LITERAL token
|
|
1606
|
-
* and pops the state stack to return to the previous state.
|
|
1607
|
-
*
|
|
1608
|
-
* @param cursor - The lexer cursor positioned at the opening backtick.
|
|
1609
|
-
* @param context - The lexer context used to retrieve the previous state for restoration.
|
|
1610
|
-
* @returns Transition result with the INTERPOLATION_LITERAL token and restored state.
|
|
1611
|
-
*/
|
|
1612
|
-
function lexInterpolationliteral(cursor, context) {
|
|
1613
|
-
let read = true;
|
|
1614
|
-
let interpolation = "`";
|
|
1615
|
-
let retVal;
|
|
1616
|
-
cursor.advance();
|
|
1617
|
-
while (read) switch (cursor.peek()) {
|
|
1618
|
-
case 96:
|
|
1619
|
-
interpolation = addCharacter(cursor, interpolation);
|
|
1620
|
-
if (cursor.peek() === 125) {
|
|
1621
|
-
cursor.advance();
|
|
1622
|
-
const previousState = context.history.pop();
|
|
1623
|
-
let state;
|
|
1624
|
-
switch (previousState) {
|
|
1625
|
-
case LexerState.ATTRIBUTE:
|
|
1626
|
-
if (cursor.peek() !== 34) throw new Error(`Attribute interpolation expression must end with double quotes at ${cursor.formattedPosition}`);
|
|
1627
|
-
cursor.advance();
|
|
1628
|
-
state = LexerState.TAG_BODY;
|
|
1629
|
-
break;
|
|
1630
|
-
case LexerState.TEXT: state = LexerState.TEXT;
|
|
1631
|
-
}
|
|
1632
|
-
retVal = {
|
|
1633
|
-
state,
|
|
1634
|
-
tokens: [{
|
|
1635
|
-
type: TokenType.INTERPOLATION_LITERAL,
|
|
1636
|
-
parts: [interpolation]
|
|
1637
|
-
}],
|
|
1638
|
-
popState: true
|
|
1639
|
-
};
|
|
1640
|
-
read = false;
|
|
1641
|
-
} else interpolation = `${interpolation}${cursor.currentChar.value}`;
|
|
1642
|
-
break;
|
|
1643
|
-
default: interpolation = addCharacter(cursor, interpolation);
|
|
1644
|
-
}
|
|
1645
|
-
return retVal;
|
|
1646
|
-
}
|
|
1647
|
-
/**
|
|
1648
|
-
* Advances the cursor by one character and appends it to the accumulator string.
|
|
1649
|
-
*
|
|
1650
|
-
* @param cursor - The lexer cursor to advance.
|
|
1651
|
-
* @param interpolation - The current accumulated expression string.
|
|
1652
|
-
* @returns The updated string with the newly consumed character appended.
|
|
1653
|
-
*/
|
|
1654
|
-
function addCharacter(cursor, interpolation) {
|
|
1655
|
-
cursor.advance();
|
|
1656
|
-
return `${interpolation}${cursor.currentChar.value}`;
|
|
1657
|
-
}
|
|
1658
|
-
//#endregion
|
|
1659
|
-
//#region ../packages/compiler/src/lexer/states/lex-interpolation.state.ts
|
|
1660
|
-
/**
|
|
1661
|
-
* Dispatches between an expression and a literal interpolation after the opening `{`.
|
|
1662
|
-
* Advances past `{` and any leading spaces, then inspects the next character:
|
|
1663
|
-
* a backtick routes to INTERPOLATION_LITERAL, a JS identifier start routes to INTERPOLATION_EXPRESSION.
|
|
1664
|
-
*
|
|
1665
|
-
* @param cursor - The lexer cursor positioned on the `{` character.
|
|
1666
|
-
* @param _context - Unused lexer context.
|
|
1667
|
-
* @returns Transition result with the appropriate interpolation sub-state.
|
|
1668
|
-
*/
|
|
1669
|
-
function lexInterpolation(cursor, _context) {
|
|
1670
|
-
cursor.advance();
|
|
1671
|
-
cursor.skipSpaces();
|
|
1672
|
-
return cursor.peek() === 96 ? { state: LexerState.INTERPOLATION_LITERAL } : { state: LexerState.INTERPOLATION_EXPRESSION };
|
|
1673
|
-
}
|
|
1674
|
-
//#endregion
|
|
1675
|
-
//#region ../packages/compiler/src/lexer/states/lex-tag-body.state.ts
|
|
1676
|
-
/**
|
|
1677
|
-
* Scans the body of an open tag to determine what comes next:
|
|
1678
|
-
* an event binding (`@`), an attribute, the end of the tag (`>` or `/`), or whitespace.
|
|
1679
|
-
* Transitions to the appropriate state without emitting any tokens.
|
|
1680
|
-
*
|
|
1681
|
-
* @param cursor - The lexer cursor positioned inside a tag body.
|
|
1682
|
-
* @param _context - Unused lexer context.
|
|
1683
|
-
* @returns Transition result with the next state and no tokens.
|
|
1684
|
-
*/
|
|
1685
|
-
function lexTagBody(cursor, _context) {
|
|
1686
|
-
let read = true;
|
|
1687
|
-
let retVal;
|
|
1688
|
-
while (read) switch (cursor.peek()) {
|
|
1689
|
-
case 64:
|
|
1690
|
-
retVal = { state: LexerState.EVENT };
|
|
1691
|
-
read = false;
|
|
1692
|
-
break;
|
|
1693
|
-
case 32:
|
|
1694
|
-
cursor.skipSpaces();
|
|
1695
|
-
break;
|
|
1696
|
-
case 62:
|
|
1697
|
-
case 47:
|
|
1698
|
-
retVal = { state: LexerState.TAG_OPEN_END };
|
|
1699
|
-
read = false;
|
|
1700
|
-
break;
|
|
1701
|
-
default:
|
|
1702
|
-
retVal = { state: LexerState.ATTRIBUTE };
|
|
1703
|
-
read = false;
|
|
1704
|
-
}
|
|
1705
|
-
return retVal;
|
|
1706
|
-
}
|
|
1707
|
-
//#endregion
|
|
1708
|
-
//#region ../packages/compiler/src/lexer/states/lex-tag-close.state.ts
|
|
1709
|
-
/**
|
|
1710
|
-
* Consumes a closing tag `</tagName>`, skipping the `</` prefix and any surrounding
|
|
1711
|
-
* whitespace. Emits a TAG_CLOSE_NAME token with the tag name and transitions to TEXT.
|
|
1712
|
-
*
|
|
1713
|
-
* @param cursor - The lexer cursor positioned at the `<` of a closing tag.
|
|
1714
|
-
* @param _context - Unused lexer context.
|
|
1715
|
-
* @returns Transition result with the TAG_CLOSE_NAME token and the TEXT state.
|
|
1716
|
-
*/
|
|
1717
|
-
function lexTagClose(cursor, _context) {
|
|
1718
|
-
let read = true;
|
|
1719
|
-
let tagName = "";
|
|
1720
|
-
let retVal;
|
|
1721
|
-
cursor.advance(2);
|
|
1722
|
-
cursor.skipSpaces();
|
|
1723
|
-
while (read) switch (cursor.peek()) {
|
|
1724
|
-
case 62:
|
|
1725
|
-
cursor.advance();
|
|
1726
|
-
retVal = {
|
|
1727
|
-
state: LexerState.TEXT,
|
|
1728
|
-
tokens: [{
|
|
1729
|
-
type: TokenType.TAG_CLOSE_NAME,
|
|
1730
|
-
parts: [tagName]
|
|
1731
|
-
}]
|
|
1732
|
-
};
|
|
1733
|
-
read = false;
|
|
1734
|
-
break;
|
|
1735
|
-
case 32: throw new Error("Tag Close Name cannot contains spaces");
|
|
1736
|
-
default:
|
|
1737
|
-
cursor.advance();
|
|
1738
|
-
tagName = `${tagName}${cursor.currentChar.value}`;
|
|
1739
|
-
}
|
|
1740
|
-
return retVal;
|
|
1741
|
-
}
|
|
1742
|
-
//#endregion
|
|
1743
|
-
//#region ../packages/compiler/src/lexer/states/lex-tag-open-end.state.ts
|
|
1744
|
-
/**
|
|
1745
|
-
* Consumes the closing characters of an open tag: `>` emits TAG_OPEN_END and
|
|
1746
|
-
* transitions to TEXT, while `/>` emits TAG_SELF_CLOSE and also transitions to TEXT.
|
|
1747
|
-
*
|
|
1748
|
-
* @param cursor - The lexer cursor positioned at `>` or `/`.
|
|
1749
|
-
* @param _context - Unused lexer context.
|
|
1750
|
-
* @returns Transition result with TAG_OPEN_END or TAG_SELF_CLOSE and the TEXT state.
|
|
1751
|
-
*/
|
|
1752
|
-
function lexTagOpenEnd(cursor, _context) {
|
|
1753
|
-
let retVal;
|
|
1754
|
-
if (cursor.peek() === 62) {
|
|
1755
|
-
cursor.advance();
|
|
1756
|
-
retVal = {
|
|
1757
|
-
state: LexerState.TEXT,
|
|
1758
|
-
tokens: [{
|
|
1759
|
-
type: TokenType.TAG_OPEN_END,
|
|
1760
|
-
parts: []
|
|
1761
|
-
}]
|
|
1762
|
-
};
|
|
1763
|
-
} else {
|
|
1764
|
-
cursor.advance();
|
|
1765
|
-
const nextChar = cursor.peek();
|
|
1766
|
-
if (nextChar === 62) {
|
|
1767
|
-
cursor.advance();
|
|
1768
|
-
retVal = {
|
|
1769
|
-
state: LexerState.TEXT,
|
|
1770
|
-
tokens: [{
|
|
1771
|
-
type: TokenType.TAG_SELF_CLOSE,
|
|
1772
|
-
parts: []
|
|
1773
|
-
}]
|
|
1774
|
-
};
|
|
1775
|
-
} else throw new Error(`Unexpected character ${nextChar} for closing tag.\nExpected />\nRead of /${String.fromCharCode(nextChar)} at ${cursor.formattedPosition}`);
|
|
1776
|
-
}
|
|
1777
|
-
return retVal;
|
|
1778
|
-
}
|
|
1779
|
-
//#endregion
|
|
1780
|
-
//#region ../packages/compiler/src/lexer/states/lex-tag-open-name.state.ts
|
|
1781
|
-
/**
|
|
1782
|
-
* Consumes an opening tag name after `<`, reading until a space, `/`, or `>` is found.
|
|
1783
|
-
* Emits a TAG_OPEN_NAME token with the tag name and transitions to TAG_BODY.
|
|
1784
|
-
*
|
|
1785
|
-
* @param cursor - The lexer cursor positioned at the `<` character.
|
|
1786
|
-
* @param _context - Unused lexer context.
|
|
1787
|
-
* @returns Transition result with the TAG_OPEN_NAME token and the TAG_BODY state.
|
|
1788
|
-
*/
|
|
1789
|
-
function lexTagOpenName(cursor, _context) {
|
|
1790
|
-
let read = true;
|
|
1791
|
-
let tagName = "";
|
|
1792
|
-
let retVal;
|
|
1793
|
-
cursor.advance();
|
|
1794
|
-
cursor.skipSpaces();
|
|
1795
|
-
while (read) switch (cursor.peek()) {
|
|
1796
|
-
case 32:
|
|
1797
|
-
case 47:
|
|
1798
|
-
case 62:
|
|
1799
|
-
retVal = {
|
|
1800
|
-
state: LexerState.TAG_BODY,
|
|
1801
|
-
tokens: [{
|
|
1802
|
-
type: TokenType.TAG_OPEN_NAME,
|
|
1803
|
-
parts: [tagName]
|
|
1804
|
-
}]
|
|
1805
|
-
};
|
|
1806
|
-
read = false;
|
|
1807
|
-
break;
|
|
1808
|
-
default:
|
|
1809
|
-
cursor.advance();
|
|
1810
|
-
tagName = `${tagName}${cursor.currentChar.value}`;
|
|
1811
|
-
}
|
|
1812
|
-
return retVal;
|
|
1813
|
-
}
|
|
1814
|
-
//#endregion
|
|
1815
|
-
//#region ../packages/compiler/src/utils/chars.utils.ts
|
|
1816
|
-
/**
|
|
1817
|
-
* Checks whether a string contains at least one non-whitespace character.
|
|
1818
|
-
*
|
|
1819
|
-
* Whitespace characters include space, `\n`, `\r`, `\t`, `\f`, and `\v`.
|
|
1820
|
-
*
|
|
1821
|
-
* @param str - The string to check.
|
|
1822
|
-
* @returns `true` if the string is not blank, `false` if it consists entirely of whitespace.
|
|
1823
|
-
*/
|
|
1824
|
-
function isNotBlank(str) {
|
|
1825
|
-
return /\S/.test(str);
|
|
1826
|
-
}
|
|
1827
|
-
//#endregion
|
|
1828
|
-
//#region ../packages/compiler/src/lexer/states/lex-text.state.ts
|
|
1829
|
-
/**
|
|
1830
|
-
* Consumes plain text content, accumulating characters until a structural boundary
|
|
1831
|
-
* is reached: `<` (tag open/close), `{` (interpolation), `@` (flow-control or event),
|
|
1832
|
-
* or `}` (block close). Emits a TEXT token if non-blank text was accumulated.
|
|
1833
|
-
*
|
|
1834
|
-
* @param cursor - The lexer cursor positioned at the start of text content.
|
|
1835
|
-
* @param context - The lexer context used to detect flow-control block boundaries.
|
|
1836
|
-
* @returns Transition result with an optional TEXT token and the next state.
|
|
1837
|
-
*/
|
|
1838
|
-
function lexText(cursor, context) {
|
|
1839
|
-
let read = true;
|
|
1840
|
-
let text = "";
|
|
1841
|
-
let retVal;
|
|
1842
|
-
while (read) switch (cursor.peek()) {
|
|
1843
|
-
case 60:
|
|
1844
|
-
retVal = { state: cursor.peek(1, { offset: 1 }) === 47 ? LexerState.TAG_CLOSE : LexerState.TAG_OPEN_NAME };
|
|
1845
|
-
read = false;
|
|
1846
|
-
break;
|
|
1847
|
-
case 123:
|
|
1848
|
-
retVal = {
|
|
1849
|
-
state: LexerState.INTERPOLATION,
|
|
1850
|
-
pushState: true
|
|
1851
|
-
};
|
|
1852
|
-
read = false;
|
|
1853
|
-
break;
|
|
1854
|
-
case 64:
|
|
1855
|
-
retVal = { state: LexerState.FLOW_CONTROL };
|
|
1856
|
-
read = false;
|
|
1857
|
-
break;
|
|
1858
|
-
case 125:
|
|
1859
|
-
if (context.history[context.history.length - 1] === LexerState.FLOW_CONTROL_BLOCK) {
|
|
1860
|
-
cursor.advance();
|
|
1861
|
-
retVal = {
|
|
1862
|
-
state: LexerState.TEXT,
|
|
1863
|
-
tokens: [{ type: TokenType.BLOCK_CLOSE }],
|
|
1864
|
-
popState: true
|
|
1865
|
-
};
|
|
1866
|
-
read = false;
|
|
1867
|
-
} else {
|
|
1868
|
-
cursor.advance();
|
|
1869
|
-
text = `${text}${cursor.currentChar.value}`;
|
|
1870
|
-
}
|
|
1871
|
-
break;
|
|
1872
|
-
case 10:
|
|
1873
|
-
case 13:
|
|
1874
|
-
cursor.advance();
|
|
1875
|
-
break;
|
|
1876
|
-
default:
|
|
1877
|
-
cursor.advance();
|
|
1878
|
-
text = `${text}${cursor.currentChar.value}`;
|
|
1879
|
-
}
|
|
1880
|
-
retVal.tokens ??= isNotBlank(text) ? [{
|
|
1881
|
-
type: TokenType.TEXT,
|
|
1882
|
-
parts: [text]
|
|
1883
|
-
}] : void 0;
|
|
1884
|
-
return retVal;
|
|
1885
|
-
}
|
|
1886
|
-
//#endregion
|
|
1887
|
-
//#region ../packages/compiler/src/lexer/types/lexer-cursor.model.ts
|
|
1888
|
-
/**
|
|
1889
|
-
* Cursor abstraction used by the Lexer to navigate the input source.
|
|
1890
|
-
*
|
|
1891
|
-
* The LexerCursor is responsible for:
|
|
1892
|
-
* - Sequential character consumption
|
|
1893
|
-
* - Lookahead (peek) operations without state mutation
|
|
1894
|
-
* - Tracking logical position (row, column)
|
|
1895
|
-
* - Handling end-of-file conditions
|
|
1896
|
-
*
|
|
1897
|
-
* This class deliberately contains **no lexer logic**:
|
|
1898
|
-
* it does not know about tokens, states, or grammar rules.
|
|
1899
|
-
* Its sole responsibility is controlled navigation of the input stream.
|
|
1900
|
-
*/
|
|
1901
|
-
var LexerCursor = class {
|
|
1902
|
-
input;
|
|
1903
|
-
/**
|
|
1904
|
-
* Representation of the current character.
|
|
1905
|
-
*
|
|
1906
|
-
* - `index`: absolute index within the input string
|
|
1907
|
-
* - `code`: Unicode code point of the character
|
|
1908
|
-
* - `value`: actual character value
|
|
1909
|
-
*
|
|
1910
|
-
* An index of `-1` indicates that the cursor has not yet consumed
|
|
1911
|
-
* any character or has reached EOF.
|
|
1912
|
-
*/
|
|
1913
|
-
_currentChar = {
|
|
1914
|
-
code: 0,
|
|
1915
|
-
index: -1,
|
|
1916
|
-
value: ""
|
|
1917
|
-
};
|
|
1918
|
-
/**
|
|
1919
|
-
* Returns a read-only snapshot of the current character.
|
|
1920
|
-
*/
|
|
1921
|
-
get currentChar() {
|
|
1922
|
-
return this._currentChar;
|
|
1923
|
-
}
|
|
1924
|
-
/**
|
|
1925
|
-
* Cache used by peek operations to avoid re-reading
|
|
1926
|
-
* the same character positions multiple times.
|
|
1927
|
-
*
|
|
1928
|
-
* Key: absolute character index
|
|
1929
|
-
* Value: Unicode code point
|
|
1930
|
-
*/
|
|
1931
|
-
_peekCache = /* @__PURE__ */ new Map();
|
|
1932
|
-
/**
|
|
1933
|
-
* Logical position of the cursor in the input.
|
|
1934
|
-
*
|
|
1935
|
-
* - `row`: zero-based line number
|
|
1936
|
-
* - `column`: zero-based column number
|
|
1937
|
-
*/
|
|
1938
|
-
_position = {
|
|
1939
|
-
row: 0,
|
|
1940
|
-
column: 0
|
|
1941
|
-
};
|
|
1942
|
-
/**
|
|
1943
|
-
* Returns a read-only snapshot of the current cursor position.
|
|
1944
|
-
*/
|
|
1945
|
-
get position() {
|
|
1946
|
-
return this._position;
|
|
1947
|
-
}
|
|
1948
|
-
get formattedPosition() {
|
|
1949
|
-
return `[Ln ${this._position.row}, Col ${this._position.column}]`;
|
|
1950
|
-
}
|
|
1951
|
-
/**
|
|
1952
|
-
* Creates a new cursor for the given input source.
|
|
1953
|
-
*
|
|
1954
|
-
* @param input - Full source string to be tokenised.
|
|
1955
|
-
*/
|
|
1956
|
-
constructor(input) {
|
|
1957
|
-
this.input = input;
|
|
1958
|
-
}
|
|
1959
|
-
/**
|
|
1960
|
-
* Advances the cursor by the specified number of characters.
|
|
1961
|
-
*
|
|
1962
|
-
* This method:
|
|
1963
|
-
* - Updates the current character
|
|
1964
|
-
* - Updates row/column position
|
|
1965
|
-
* - Detects line breaks (LF / CR)
|
|
1966
|
-
* - Throws an EOF error when the end of the input is reached
|
|
1967
|
-
*
|
|
1968
|
-
* @param chars - Number of characters to consume. Must be >= 1.
|
|
1969
|
-
* @throws When `chars` is less than 1 or when advancing past the end of the input.
|
|
1970
|
-
*/
|
|
1971
|
-
advance(chars = 1) {
|
|
1972
|
-
if (chars < 1) throw new Error(`${chars} is not a valid value. Please enter a number equal or greater than 1`);
|
|
1973
|
-
const newIndex = this._currentChar.index + chars;
|
|
1974
|
-
if (newIndex >= this.input.length) {
|
|
1975
|
-
this._currentChar.code = 0;
|
|
1976
|
-
this._currentChar.index = -1;
|
|
1977
|
-
this._currentChar.value = "";
|
|
1978
|
-
this.throwEOFError();
|
|
1979
|
-
} else {
|
|
1980
|
-
if ([10, 13].includes(this._currentChar.code)) {
|
|
1981
|
-
this._position.row++;
|
|
1982
|
-
this._position.column = 0;
|
|
1983
|
-
} else this._position.column++;
|
|
1984
|
-
this._currentChar.index = newIndex;
|
|
1985
|
-
this._currentChar.value = this.input[newIndex];
|
|
1986
|
-
this._currentChar.code = this.input.charCodeAt(newIndex);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
peekMatch(pattern, length) {
|
|
1990
|
-
if (typeof pattern === "string") {
|
|
1991
|
-
const peekedChars = this.peek(pattern.length);
|
|
1992
|
-
for (let i = 0; i < pattern.length; i++) if (peekedChars[i] !== pattern.charCodeAt(i)) return false;
|
|
1993
|
-
return true;
|
|
1994
|
-
}
|
|
1995
|
-
const start = this._currentChar.index + 1;
|
|
1996
|
-
const slice = this.input.slice(start, start + length);
|
|
1997
|
-
return pattern.test(slice);
|
|
1998
|
-
}
|
|
1999
|
-
peek(charsOrOptions, options) {
|
|
2000
|
-
const cache = this._peekCache;
|
|
2001
|
-
const chars = typeof charsOrOptions === "number" ? charsOrOptions : 1;
|
|
2002
|
-
const offset = (typeof charsOrOptions === "object" ? charsOrOptions : options)?.offset ?? 0;
|
|
2003
|
-
return chars === 1 ? this.peekOneChar(this._currentChar.index + offset + 1, cache) : this.peekMany(chars + offset, cache);
|
|
2004
|
-
}
|
|
2005
|
-
/**
|
|
2006
|
-
* Skips all consecutive space characters from the current position.
|
|
2007
|
-
*/
|
|
2008
|
-
skipSpaces() {
|
|
2009
|
-
while (this.peek() === 32 || this.peek() === 10 || this.peek() === 13) this.advance();
|
|
2010
|
-
}
|
|
2011
|
-
/**
|
|
2012
|
-
* Peeks multiple characters ahead.
|
|
2013
|
-
*/
|
|
2014
|
-
peekMany(chars, cache) {
|
|
2015
|
-
const peekedChars = new Array();
|
|
2016
|
-
const nextCharIndex = this._currentChar.index + 1;
|
|
2017
|
-
for (let i = nextCharIndex; i < nextCharIndex + chars; i++) peekedChars.push(this.peekOneChar(i, cache));
|
|
2018
|
-
return peekedChars;
|
|
2019
|
-
}
|
|
2020
|
-
/**
|
|
2021
|
-
* Peeks a single character at the given absolute index.
|
|
2022
|
-
*/
|
|
2023
|
-
peekOneChar(index, cache) {
|
|
2024
|
-
if (cache.has(index)) return cache.get(index);
|
|
2025
|
-
if (index >= this.input.length) this.throwEOFError();
|
|
2026
|
-
const charCode = this.input.charCodeAt(index);
|
|
2027
|
-
cache.set(index, charCode);
|
|
2028
|
-
return charCode;
|
|
2029
|
-
}
|
|
2030
|
-
/**
|
|
2031
|
-
* Throws a standardized EOF error used by the lexer engine
|
|
2032
|
-
* to terminate tokenization.
|
|
2033
|
-
*/
|
|
2034
|
-
throwEOFError() {
|
|
2035
|
-
throw new Error("", { cause: 0 });
|
|
2036
|
-
}
|
|
2037
|
-
};
|
|
2038
|
-
//#endregion
|
|
2039
|
-
//#region ../packages/compiler/src/lexer/lexer.ts
|
|
2040
|
-
/**
|
|
2041
|
-
* Utility class that emulates a cursor navigating through a template string.
|
|
2042
|
-
*
|
|
2043
|
-
* The cursor keeps track of the current character, its absolute position
|
|
2044
|
-
* within the text, and its logical position expressed as row and column.
|
|
2045
|
-
* This is useful when parsing or analyzing template content character by character.
|
|
2046
|
-
*/
|
|
2047
|
-
var Lexer = class {
|
|
2048
|
-
input;
|
|
2049
|
-
/**
|
|
2050
|
-
* Cursor for navigating the input character stream.
|
|
2051
|
-
*/
|
|
2052
|
-
_cursor;
|
|
2053
|
-
/**
|
|
2054
|
-
* Current lexer state.
|
|
2055
|
-
*/
|
|
2056
|
-
_state = LexerState.TEXT;
|
|
2057
|
-
/**
|
|
2058
|
-
* State stack used to support nested states (e.g. interpolations).
|
|
2059
|
-
*/
|
|
2060
|
-
_stack = new Stack();
|
|
2061
|
-
/**
|
|
2062
|
-
* Accumulated list of tokens emitted during tokenization.
|
|
2063
|
-
*/
|
|
2064
|
-
_tokens = new Array();
|
|
2065
|
-
/**
|
|
2066
|
-
* Maps each lexer state to its corresponding transition function.
|
|
2067
|
-
*/
|
|
2068
|
-
_states = {
|
|
2069
|
-
[LexerState.TEXT]: lexText,
|
|
2070
|
-
[LexerState.TAG_OPEN_NAME]: lexTagOpenName,
|
|
2071
|
-
[LexerState.TAG_BODY]: lexTagBody,
|
|
2072
|
-
[LexerState.TAG_OPEN_END]: lexTagOpenEnd,
|
|
2073
|
-
[LexerState.TAG_CLOSE]: lexTagClose,
|
|
2074
|
-
[LexerState.ATTRIBUTE]: lexAttribute,
|
|
2075
|
-
[LexerState.ATTRIBUTE_VALUE]: lexAttributeValue,
|
|
2076
|
-
[LexerState.EVENT]: lexEvent,
|
|
2077
|
-
[LexerState.EVENT_PARAMETER]: lexEventParameter,
|
|
2078
|
-
[LexerState.FLOW_CONTROL]: lexFlowControl,
|
|
2079
|
-
[LexerState.FLOW_CONTROL_CONDITION]: lexDefaultFlowControlCondition,
|
|
2080
|
-
[LexerState.CASE_FLOW_CONTROL_CONDITION]: lexCaseFlowControlCondition,
|
|
2081
|
-
[LexerState.FLOW_CONTROL_BLOCK]: lexFlowControlBlock,
|
|
2082
|
-
[LexerState.INTERPOLATION]: lexInterpolation,
|
|
2083
|
-
[LexerState.INTERPOLATION_EXPRESSION]: lexInterpolationExpression,
|
|
2084
|
-
[LexerState.INTERPOLATION_LITERAL]: lexInterpolationliteral,
|
|
2085
|
-
[LexerState.IMPORT]: lexImport,
|
|
2086
|
-
[LexerState.IMPORT_PATH]: lexImportPath
|
|
2087
|
-
};
|
|
2088
|
-
/**
|
|
2089
|
-
* Creates a new Lexer instance for the given template content.
|
|
2090
|
-
*
|
|
2091
|
-
* @param input - The full template text to tokenise.
|
|
2092
|
-
*/
|
|
2093
|
-
constructor(input) {
|
|
2094
|
-
this.input = input;
|
|
2095
|
-
this._cursor = new LexerCursor(this.input);
|
|
2096
|
-
}
|
|
2097
|
-
/**
|
|
2098
|
-
* Runs the lexer over the input string and returns the full token array.
|
|
2099
|
-
* Drives the state machine until EOF is reached.
|
|
2100
|
-
*
|
|
2101
|
-
* @returns Array of all tokens produced from the input.
|
|
2102
|
-
*/
|
|
2103
|
-
tokenize() {
|
|
2104
|
-
let eof = false;
|
|
2105
|
-
while (!eof) try {
|
|
2106
|
-
const transitionFunction = this._states[this._state];
|
|
2107
|
-
const { state, tokens, popState, pushState } = transitionFunction(this._cursor, {
|
|
2108
|
-
history: this._stack.values,
|
|
2109
|
-
tokens: [...this._tokens]
|
|
2110
|
-
});
|
|
2111
|
-
if (tokens?.length) this._tokens.push(...tokens);
|
|
2112
|
-
if (pushState) this._stack.push(this._state);
|
|
2113
|
-
if (popState) this._stack.pop();
|
|
2114
|
-
this._state = state;
|
|
2115
|
-
} catch (err) {
|
|
2116
|
-
if (err.cause === 0) eof = true;
|
|
2117
|
-
else {
|
|
2118
|
-
console.log(`Something went wrong while computing state ${this._state} at ${this._cursor.formattedPosition}`);
|
|
2119
|
-
throw err;
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
return this._tokens;
|
|
2123
|
-
}
|
|
2124
|
-
};
|
|
2125
|
-
//#endregion
|
|
2126
|
-
//#region ../packages/compiler/src/parser/models/parser-cursor.model.ts
|
|
2127
|
-
/**
|
|
2128
|
-
* Cursor abstraction used by the Parser to navigate
|
|
2129
|
-
* through a sequence of tokens produced by the Lexer.
|
|
2130
|
-
*
|
|
2131
|
-
* Responsibilities:
|
|
2132
|
-
* - Sequential token consumption
|
|
2133
|
-
* - Lookahead (peek) operations without mutating state
|
|
2134
|
-
* - Handling end-of-file conditions
|
|
2135
|
-
*
|
|
2136
|
-
* This class does not perform parsing itself: it only
|
|
2137
|
-
* manages position and access to the token stream.
|
|
2138
|
-
*/
|
|
2139
|
-
var ParserCursor = class {
|
|
2140
|
-
_tokens;
|
|
2141
|
-
/**
|
|
2142
|
-
* Representation of the current token.
|
|
2143
|
-
*
|
|
2144
|
-
* - `index`: absolute index within the token array
|
|
2145
|
-
* - `value`: current token object (or EOF token)
|
|
2146
|
-
*
|
|
2147
|
-
* An index of `-1` indicates that the cursor has not
|
|
2148
|
-
* yet consumed any token or has reached EOF.
|
|
2149
|
-
*/
|
|
2150
|
-
_currentToken = {
|
|
2151
|
-
value: { type: TokenType.EOF },
|
|
2152
|
-
index: -1
|
|
2153
|
-
};
|
|
2154
|
-
/**
|
|
2155
|
-
* Returns a read-only snapshot of the current token.
|
|
2156
|
-
*/
|
|
2157
|
-
getCurrentToken() {
|
|
2158
|
-
return this._currentToken;
|
|
2159
|
-
}
|
|
2160
|
-
/**
|
|
2161
|
-
* Creates a new ParserCursor for the given token array.
|
|
2162
|
-
*
|
|
2163
|
-
* @param _tokens - The array of tokens to navigate.
|
|
2164
|
-
*/
|
|
2165
|
-
constructor(_tokens) {
|
|
2166
|
-
this._tokens = _tokens;
|
|
2167
|
-
}
|
|
2168
|
-
/**
|
|
2169
|
-
* Advances the cursor by the specified number of tokens.
|
|
2170
|
-
*
|
|
2171
|
-
* Updates the current token and its index.
|
|
2172
|
-
*
|
|
2173
|
-
* @param chars - Number of tokens to advance. Must be >= 1.
|
|
2174
|
-
* @throws When `chars` is less than 1.
|
|
2175
|
-
*/
|
|
2176
|
-
advance(chars = 1) {
|
|
2177
|
-
if (chars < 1) throw new Error(`${chars} is not a valid value. Please enter a number equal or greater than 1`);
|
|
2178
|
-
const newIndex = this._currentToken.index + chars;
|
|
2179
|
-
if (newIndex >= this._tokens.length) {
|
|
2180
|
-
this._currentToken.value = { type: TokenType.EOF };
|
|
2181
|
-
this._currentToken.index = -1;
|
|
2182
|
-
} else {
|
|
2183
|
-
this._currentToken.index = newIndex;
|
|
2184
|
-
this._currentToken.value = this._tokens[newIndex];
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
peek(charsOrOptions, options) {
|
|
2188
|
-
const tokens = typeof charsOrOptions === "number" ? charsOrOptions : 1;
|
|
2189
|
-
const offset = (typeof charsOrOptions === "object" ? charsOrOptions : options)?.offset ?? 0;
|
|
2190
|
-
return tokens === 1 ? this.peekOneToken(this._currentToken.index + offset + 1) : this.peekMany(tokens + offset);
|
|
2191
|
-
}
|
|
2192
|
-
/**
|
|
2193
|
-
* Peeks multiple tokens ahead.
|
|
2194
|
-
*/
|
|
2195
|
-
peekMany(chars) {
|
|
2196
|
-
const peekedTokens = new Array();
|
|
2197
|
-
const nextTokenIndex = this._currentToken.index + 1;
|
|
2198
|
-
for (let i = nextTokenIndex; i < nextTokenIndex + chars; i++) peekedTokens.push(this.peekOneToken(i));
|
|
2199
|
-
return peekedTokens;
|
|
2200
|
-
}
|
|
2201
|
-
/**
|
|
2202
|
-
* Peeks a single token at the given absolute index.
|
|
2203
|
-
*/
|
|
2204
|
-
peekOneToken(index) {
|
|
2205
|
-
return index < this._tokens.length ? this._tokens[index] : { type: TokenType.EOF };
|
|
2206
|
-
}
|
|
2207
|
-
};
|
|
2208
|
-
//#endregion
|
|
2209
|
-
//#region ../packages/compiler/src/parser/utils/expression-validator.ts
|
|
2210
|
-
/**
|
|
2211
|
-
* Validates that a string contains a single expression belonging to the
|
|
2212
|
-
* permitted subset of JavaScript supported inside Xaendar template expressions.
|
|
2213
|
-
*
|
|
2214
|
-
* ## Permitted constructs
|
|
2215
|
-
*
|
|
2216
|
-
* - **Literals** — strings, numbers, bigints, booleans, `null`, `undefined`
|
|
2217
|
-
* - **Identifiers** — resolved at runtime against the active scope chain
|
|
2218
|
-
* - **Member access** — `user.name`, `user.address.city`, `items[0]`
|
|
2219
|
-
* - **Call expressions** — `user.getFullName()`, `user.hasRole('admin')`
|
|
2220
|
-
* - **Binary expressions** — arithmetic (`+`, `-`, `*`, `/`, `%`, `**`),
|
|
2221
|
-
* comparison (`===`, `!==`, `<`, `>`, `<=`, `>=`),
|
|
2222
|
-
* logical (`&&`, `||`, `??`),
|
|
2223
|
-
* bitwise (`&`, `|`, `^`, `<<`, `>>`, `>>>`)
|
|
2224
|
-
* - **Unary expressions** — `!`, `~`, `+`, `-`, `typeof`, `void`
|
|
2225
|
-
* - **Conditional (ternary)** — `isAdmin ? 'yes' : 'no'`
|
|
2226
|
-
* - **Parenthesised expressions** — `(user.age > 18)`
|
|
2227
|
-
* - **Template literals** — `` `Hello ${user.name}` ``
|
|
2228
|
-
* - **Array literals** — `[1, 2, 3]`, `[...items]`
|
|
2229
|
-
* - **Object literals** — `{ key: value }`, `{ ...defaults, name }`
|
|
2230
|
-
* - **Spread** — `foo(...args)`, `[...items]`, `{ ...obj }`
|
|
2231
|
-
* - **`typeof` / `instanceof`** — `typeof user.role`, `user instanceof AdminUser`
|
|
2232
|
-
*
|
|
2233
|
-
* ## Prohibited constructs
|
|
2234
|
-
*
|
|
2235
|
-
* - Assignments (`=`, `+=`, `&&=`, etc.) — use `@const` for local bindings
|
|
2236
|
-
* - `await` and `yield`
|
|
2237
|
-
* - `new` expressions
|
|
2238
|
-
* - Function and arrow function expressions
|
|
2239
|
-
* - Tagged template expressions
|
|
2240
|
-
*
|
|
2241
|
-
* ## Scope resolution
|
|
2242
|
-
*
|
|
2243
|
-
* This function performs **syntactic** validation only. Identifier resolution
|
|
2244
|
-
* (scope chain walk → `ctx.` prefix injection) is the responsibility of the
|
|
2245
|
-
* caller and must be performed on the returned `node` after this function
|
|
2246
|
-
* reports no diagnostics.
|
|
2247
|
-
*
|
|
2248
|
-
* @param source - The raw expression string extracted from the template.
|
|
2249
|
-
* @returns A {@link ExpressionValidationResult} containing the parsed AST node
|
|
2250
|
-
* (when valid) and any diagnostics produced during validation.
|
|
2251
|
-
*
|
|
2252
|
-
* @example
|
|
2253
|
-
* const result = validateExpression('user.hasRole("admin") && isVerified');
|
|
2254
|
-
* if (result.diagnostics.length === 0) {
|
|
2255
|
-
* // result.node is safe to use
|
|
2256
|
-
* }
|
|
2257
|
-
*
|
|
2258
|
-
* @example
|
|
2259
|
-
* const result = validateExpression('await user.load()');
|
|
2260
|
-
* // result.diagnostics[0].message →
|
|
2261
|
-
* // "'await' is not allowed inside template expressions."
|
|
2262
|
-
*/
|
|
2263
|
-
function validateExpression(source) {
|
|
2264
|
-
const expression = createSourceFile("expression.ts", `const x = ${source}`, ScriptTarget.ESNext, true).statements[0].declarationList.declarations[0].initializer;
|
|
2265
|
-
const diagnostics = new Array();
|
|
2266
|
-
visitNode(expression, 10, diagnostics);
|
|
2267
|
-
if (diagnostics.length) throw new Error(diagnostics.reduce((acc, d) => `${acc}${d.message}\n`, ""));
|
|
2268
|
-
return { node: expression };
|
|
2269
|
-
}
|
|
2270
|
-
/**
|
|
2271
|
-
* Recursively visits an AST node and appends a diagnostic for every node
|
|
2272
|
-
* kind that is not part of the permitted expression subset.
|
|
2273
|
-
*
|
|
2274
|
-
* Recursion stops at the first disallowed node to avoid producing a cascade
|
|
2275
|
-
* of redundant diagnostics for its children.
|
|
2276
|
-
*
|
|
2277
|
-
* @param node - The AST node to inspect.
|
|
2278
|
-
* @param offset - Number of characters to subtract from raw node positions
|
|
2279
|
-
* to obtain offsets relative to the original expression string.
|
|
2280
|
-
* @param diagnostics - Accumulator for diagnostics found during the walk.
|
|
2281
|
-
*/
|
|
2282
|
-
function visitNode(node, offset, diagnostics) {
|
|
2283
|
-
if (!isAllowedNode(node)) throw new Error(buildDisallowedMessage(node));
|
|
2284
|
-
forEachChild(node, (child) => visitNode(child, offset, diagnostics));
|
|
2285
|
-
if (diagnostics.length) throw new Error(diagnostics[0].message);
|
|
2286
|
-
}
|
|
2287
|
-
/**
|
|
2288
|
-
* Returns `true` if the given AST node kind is permitted inside a
|
|
2289
|
-
* Xaendar template expression.
|
|
2290
|
-
*
|
|
2291
|
-
* Assignment operators nested inside a `BinaryExpression` are handled
|
|
2292
|
-
* separately in {@link buildDisallowedMessage} since TypeScript does not
|
|
2293
|
-
* distinguish them at the node-kind level.
|
|
2294
|
-
*/
|
|
2295
|
-
function isAllowedNode(node) {
|
|
2296
|
-
switch (node.kind) {
|
|
2297
|
-
case SyntaxKind.StringLiteral:
|
|
2298
|
-
case SyntaxKind.NumericLiteral:
|
|
2299
|
-
case SyntaxKind.BigIntLiteral:
|
|
2300
|
-
case SyntaxKind.TrueKeyword:
|
|
2301
|
-
case SyntaxKind.FalseKeyword:
|
|
2302
|
-
case SyntaxKind.NullKeyword:
|
|
2303
|
-
case SyntaxKind.UndefinedKeyword:
|
|
2304
|
-
case SyntaxKind.Identifier:
|
|
2305
|
-
case SyntaxKind.PropertyAccessExpression:
|
|
2306
|
-
case SyntaxKind.ElementAccessExpression:
|
|
2307
|
-
case SyntaxKind.CallExpression:
|
|
2308
|
-
case SyntaxKind.BinaryExpression:
|
|
2309
|
-
case SyntaxKind.EqualsEqualsToken:
|
|
2310
|
-
case SyntaxKind.EqualsEqualsEqualsToken:
|
|
2311
|
-
case SyntaxKind.ExclamationEqualsToken:
|
|
2312
|
-
case SyntaxKind.ExclamationEqualsEqualsToken:
|
|
2313
|
-
case SyntaxKind.LessThanToken:
|
|
2314
|
-
case SyntaxKind.LessThanEqualsToken:
|
|
2315
|
-
case SyntaxKind.GreaterThanToken:
|
|
2316
|
-
case SyntaxKind.GreaterThanEqualsToken:
|
|
2317
|
-
case SyntaxKind.PlusToken:
|
|
2318
|
-
case SyntaxKind.MinusToken:
|
|
2319
|
-
case SyntaxKind.AsteriskToken:
|
|
2320
|
-
case SyntaxKind.SlashToken:
|
|
2321
|
-
case SyntaxKind.PercentToken:
|
|
2322
|
-
case SyntaxKind.AsteriskAsteriskToken:
|
|
2323
|
-
case SyntaxKind.AmpersandAmpersandToken:
|
|
2324
|
-
case SyntaxKind.BarBarToken:
|
|
2325
|
-
case SyntaxKind.QuestionQuestionToken:
|
|
2326
|
-
case SyntaxKind.QuestionDotToken:
|
|
2327
|
-
case SyntaxKind.QuestionToken:
|
|
2328
|
-
case SyntaxKind.ColonToken:
|
|
2329
|
-
case SyntaxKind.AmpersandToken:
|
|
2330
|
-
case SyntaxKind.BarToken:
|
|
2331
|
-
case SyntaxKind.CaretToken:
|
|
2332
|
-
case SyntaxKind.LessThanLessThanToken:
|
|
2333
|
-
case SyntaxKind.GreaterThanGreaterThanToken:
|
|
2334
|
-
case SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
|
|
2335
|
-
case SyntaxKind.InstanceOfKeyword:
|
|
2336
|
-
case SyntaxKind.InKeyword:
|
|
2337
|
-
case SyntaxKind.PrefixUnaryExpression:
|
|
2338
|
-
case SyntaxKind.PostfixUnaryExpression:
|
|
2339
|
-
case SyntaxKind.TypeOfExpression:
|
|
2340
|
-
case SyntaxKind.VoidExpression:
|
|
2341
|
-
case SyntaxKind.ConditionalExpression:
|
|
2342
|
-
case SyntaxKind.ParenthesizedExpression:
|
|
2343
|
-
case SyntaxKind.TemplateExpression:
|
|
2344
|
-
case SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
2345
|
-
case SyntaxKind.TemplateHead:
|
|
2346
|
-
case SyntaxKind.TemplateMiddle:
|
|
2347
|
-
case SyntaxKind.TemplateTail:
|
|
2348
|
-
case SyntaxKind.TemplateSpan:
|
|
2349
|
-
case SyntaxKind.ArrayLiteralExpression:
|
|
2350
|
-
case SyntaxKind.ObjectLiteralExpression:
|
|
2351
|
-
case SyntaxKind.PropertyAssignment:
|
|
2352
|
-
case SyntaxKind.ShorthandPropertyAssignment:
|
|
2353
|
-
case SyntaxKind.SpreadAssignment:
|
|
2354
|
-
case SyntaxKind.SpreadElement:
|
|
2355
|
-
case SyntaxKind.TaggedTemplateExpression:
|
|
2356
|
-
case SyntaxKind.SyntaxList: return true;
|
|
2357
|
-
default: return false;
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2360
|
-
/**
|
|
2361
|
-
* Builds a human-readable diagnostic message for a node that is not permitted
|
|
2362
|
-
* inside a Xaendar template expression.
|
|
2363
|
-
*
|
|
2364
|
-
* Provides specific messages for the most common mistakes (assignments, `await`,
|
|
2365
|
-
* `new`, functions) and falls back to a generic message for anything else.
|
|
2366
|
-
*/
|
|
2367
|
-
function buildDisallowedMessage(node) {
|
|
2368
|
-
switch (node.kind) {
|
|
2369
|
-
case SyntaxKind.AwaitExpression: return "'await' is not allowed inside template expressions.";
|
|
2370
|
-
case SyntaxKind.YieldExpression: return "'yield' is not allowed inside template expressions.";
|
|
2371
|
-
case SyntaxKind.NewExpression: return "'new' is not allowed inside template expressions.";
|
|
2372
|
-
case SyntaxKind.ArrowFunction:
|
|
2373
|
-
case SyntaxKind.FunctionExpression: return "Function expressions are not allowed inside template expressions.";
|
|
2374
|
-
case SyntaxKind.BinaryExpression: return isAssignmentOperator(node.operatorToken.kind) ? "Assignments are not allowed inside template expressions. Use @const to declare local template variables instead." : `'${SyntaxKind[node.kind]}' is not allowed inside template expressions.`;
|
|
2375
|
-
default: return `'${SyntaxKind[node.kind]}' is not allowed inside template expressions.`;
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
/**
|
|
2379
|
-
* Returns `true` if the given {@link SyntaxKind} is an assignment operator.
|
|
2380
|
-
*
|
|
2381
|
-
* Covers simple assignment (`=`) as well as all compound assignment operators
|
|
2382
|
-
* (`+=`, `-=`, `&&=`, `||=`, `??=`, etc.) as defined by the TypeScript
|
|
2383
|
-
* `FirstAssignment`–`LastAssignment` range.
|
|
2384
|
-
*/
|
|
2385
|
-
function isAssignmentOperator(kind) {
|
|
2386
|
-
return kind >= SyntaxKind.FirstAssignment && kind <= SyntaxKind.LastAssignment;
|
|
2387
|
-
}
|
|
2388
|
-
//#endregion
|
|
2389
|
-
//#region ../packages/compiler/src/parser/states/parse-interpolation.state.ts
|
|
2390
|
-
/**
|
|
2391
|
-
* Parses an interpolation expression or literal token into an `InterpolationNode`.
|
|
2392
|
-
*
|
|
2393
|
-
* @param cursor - Parser cursor; advanced past the interpolation token.
|
|
2394
|
-
* @param _parseNode - Unused parser function (kept for signature consistency).
|
|
2395
|
-
* @param token - The INTERPOLATION_EXPRESSION or INTERPOLATION_LITERAL token.
|
|
2396
|
-
* @returns The parsed `InterpolationNode`.
|
|
2397
|
-
*/
|
|
2398
|
-
function parseInterpolation(cursor, _parseNode, token) {
|
|
2399
|
-
cursor.advance();
|
|
2400
|
-
return {
|
|
2401
|
-
type: ASTNodeType.Interpolation,
|
|
2402
|
-
expression: validateExpression(token.parts[0]).node
|
|
2403
|
-
};
|
|
2404
|
-
}
|
|
2405
|
-
//#endregion
|
|
2406
|
-
//#region ../packages/compiler/src/parser/states/parse-attribute.state.ts
|
|
2407
|
-
/**
|
|
2408
|
-
* Parses an ATTRIBUTE token into an `AttributeNode`.
|
|
2409
|
-
* Handles boolean attributes (no `=`), string values, and interpolation values.
|
|
2410
|
-
*
|
|
2411
|
-
* @param cursor - Parser cursor; advanced past the ATTRIBUTE token.
|
|
2412
|
-
* @param parseNode - Parser function for recursive child parsing.
|
|
2413
|
-
* @param token - The ATTRIBUTE token to parse.
|
|
2414
|
-
* @returns The parsed `AttributeNode`.
|
|
2415
|
-
*/
|
|
2416
|
-
function parseAttribute(cursor, parseNode, token) {
|
|
2417
|
-
cursor.advance();
|
|
2418
|
-
const name = token.parts[0];
|
|
2419
|
-
const nextToken = cursor.peek();
|
|
2420
|
-
if (nextToken.type === TokenType.ATTRIBUTE || nextToken.type === TokenType.TAG_CLOSE_NAME || nextToken.type === TokenType.EVENT) return {
|
|
2421
|
-
name,
|
|
2422
|
-
value: "true"
|
|
2423
|
-
};
|
|
2424
|
-
if (nextToken.type === TokenType.INTERPOLATION_EXPRESSION || nextToken.type === TokenType.INTERPOLATION_LITERAL) return {
|
|
2425
|
-
name,
|
|
2426
|
-
value: parseInterpolation(cursor, parseNode, nextToken)
|
|
2427
|
-
};
|
|
2428
|
-
if (nextToken.type !== TokenType.ATTRIBUTE_VALUE) throw new Error(`[Parser] Attribute value missing for ${name} in: ${name}`);
|
|
2429
|
-
cursor.advance();
|
|
2430
|
-
return {
|
|
2431
|
-
name,
|
|
2432
|
-
value: nextToken.parts[0]
|
|
2433
|
-
};
|
|
2434
|
-
}
|
|
2435
|
-
//#endregion
|
|
2436
|
-
//#region ../packages/compiler/src/parser/states/parse-event.state.ts
|
|
2437
|
-
/**
|
|
2438
|
-
* Parses an EVENT token into an `EventNode` by splitting the raw
|
|
2439
|
-
* `eventName=handler` string.
|
|
2440
|
-
*
|
|
2441
|
-
* @param cursor - Parser cursor; advanced past the EVENT token.
|
|
2442
|
-
* @param _parseNode - Unused parser function (kept for signature consistency).
|
|
2443
|
-
* @param token - The EVENT token to parse.
|
|
2444
|
-
* @returns The parsed `EventNode`.
|
|
2445
|
-
*/
|
|
2446
|
-
function parseEvent(cursor, _parseNode, token) {
|
|
2447
|
-
cursor.advance();
|
|
2448
|
-
const raw = token.parts[0];
|
|
2449
|
-
const [name, value] = raw.split("=");
|
|
2450
|
-
if (!name || !value) throw new Error(`[Parser] Invalid event format: ${raw}`);
|
|
2451
|
-
const parameters = new Array();
|
|
2452
|
-
while (cursor.peek().type === TokenType.EVENT_PAREMETER) {
|
|
2453
|
-
cursor.advance();
|
|
2454
|
-
parameters.push(validateExpression(cursor.getCurrentToken().value.parts[0]).node);
|
|
2455
|
-
}
|
|
2456
|
-
return {
|
|
2457
|
-
name,
|
|
2458
|
-
handler: value.replace(/^[""]|[""]$/g, ""),
|
|
2459
|
-
parameters
|
|
2460
|
-
};
|
|
2461
|
-
}
|
|
2462
|
-
//#endregion
|
|
2463
|
-
//#region ../packages/compiler/src/parser/states/parse-element.state.ts
|
|
2464
|
-
/**
|
|
2465
|
-
* Parses a TAG_OPEN_NAME token and the subsequent attributes, events, and children
|
|
2466
|
-
* into an `ElementNode`. Handles both regular and self-closing tags.
|
|
2467
|
-
*
|
|
2468
|
-
* @param cursor - Parser cursor positioned at the TAG_OPEN_NAME token.
|
|
2469
|
-
* @param parseNode - Parser function for recursive child parsing.
|
|
2470
|
-
* @param token - The TAG_OPEN_NAME token containing the tag name.
|
|
2471
|
-
* @returns The parsed `ElementNode`.
|
|
2472
|
-
*/
|
|
2473
|
-
function parseElement(cursor, parseNode, token) {
|
|
2474
|
-
cursor.advance();
|
|
2475
|
-
const tagName = token.parts[0];
|
|
2476
|
-
const attributes = new Array();
|
|
2477
|
-
const events = new Array();
|
|
2478
|
-
let read = true;
|
|
2479
|
-
while (read) {
|
|
2480
|
-
const token = cursor.peek();
|
|
2481
|
-
switch (token.type) {
|
|
2482
|
-
case TokenType.ATTRIBUTE:
|
|
2483
|
-
attributes.push(parseAttribute(cursor, parseNode, token));
|
|
2484
|
-
break;
|
|
2485
|
-
case TokenType.EVENT:
|
|
2486
|
-
events.push(parseEvent(cursor, parseNode, token));
|
|
2487
|
-
break;
|
|
2488
|
-
default: read = false;
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
if (cursor.peek().type === TokenType.TAG_OPEN_END) cursor.advance();
|
|
2492
|
-
if (cursor.peek().type === TokenType.TAG_SELF_CLOSE) {
|
|
2493
|
-
cursor.advance();
|
|
2494
|
-
return {
|
|
2495
|
-
type: ASTNodeType.Element,
|
|
2496
|
-
tagName,
|
|
2497
|
-
attributes,
|
|
2498
|
-
events,
|
|
2499
|
-
children: []
|
|
2500
|
-
};
|
|
2501
|
-
}
|
|
2502
|
-
const children = new Array();
|
|
2503
|
-
while (!isTagClose(cursor, tagName)) {
|
|
2504
|
-
const child = parseNode();
|
|
2505
|
-
if (child) children.push(child);
|
|
2506
|
-
}
|
|
2507
|
-
cursor.advance();
|
|
2508
|
-
return {
|
|
2509
|
-
type: ASTNodeType.Element,
|
|
2510
|
-
tagName,
|
|
2511
|
-
attributes,
|
|
2512
|
-
events,
|
|
2513
|
-
children
|
|
2514
|
-
};
|
|
2515
|
-
}
|
|
2516
|
-
/**
|
|
2517
|
-
* Returns `true` if the next token in the stream is a closing tag for the given tag name.
|
|
2518
|
-
*
|
|
2519
|
-
* @param cursor - Parser cursor to peek from.
|
|
2520
|
-
* @param tagName - The expected tag name to match.
|
|
2521
|
-
* @returns `true` if the next token is TAG_CLOSE_NAME matching `tagName`.
|
|
2522
|
-
*/
|
|
2523
|
-
function isTagClose(cursor, tagName) {
|
|
2524
|
-
const nextToken = cursor.peek();
|
|
2525
|
-
return nextToken.type === TokenType.TAG_CLOSE_NAME && nextToken.parts[0] === tagName;
|
|
2526
|
-
}
|
|
2527
|
-
//#endregion
|
|
2528
|
-
//#region ../packages/compiler/src/parser/states/parse-block-children.state.ts
|
|
2529
|
-
/**
|
|
2530
|
-
* Parses child AST nodes inside a flow-control block until a BLOCK_CLOSE token is reached.
|
|
2531
|
-
* Consumes the BLOCK_CLOSE token before returning.
|
|
2532
|
-
*
|
|
2533
|
-
* @param cursor - Parser cursor positioned at the first token inside the block.
|
|
2534
|
-
* @param parseNode - Parser function for recursive child parsing.
|
|
2535
|
-
* @returns Array of parsed child `ASTNode`s.
|
|
2536
|
-
*/
|
|
2537
|
-
function parseBlockChildren(cursor, parseNode) {
|
|
2538
|
-
const children = new Array();
|
|
2539
|
-
while (cursor.peek().type !== TokenType.BLOCK_CLOSE) {
|
|
2540
|
-
const child = parseNode();
|
|
2541
|
-
if (child) children.push(child);
|
|
2542
|
-
}
|
|
2543
|
-
cursor.advance();
|
|
2544
|
-
return children;
|
|
2545
|
-
}
|
|
2546
|
-
//#endregion
|
|
2547
|
-
//#region ../packages/compiler/src/parser/states/parse-for.state.ts
|
|
2548
|
-
/**
|
|
2549
|
-
* Parses a `@for` directive, consuming the FOR token, the CONDITION token,
|
|
2550
|
-
* the BLOCK_OPEN token, and all child nodes until BLOCK_CLOSE.
|
|
2551
|
-
*
|
|
2552
|
-
* @param cursor - Parser cursor positioned at the FOR token.
|
|
2553
|
-
* @param parseNode - Parser function for recursive child parsing.
|
|
2554
|
-
* @param _token - The FOR token (consumed for position advancement).
|
|
2555
|
-
* @returns The parsed `ForNode`.
|
|
2556
|
-
*/
|
|
2557
|
-
function parseForControlFlow(cursor, parseNode, _token) {
|
|
2558
|
-
cursor.advance();
|
|
2559
|
-
const conditionToken = cursor.peek();
|
|
2560
|
-
if (conditionToken.type !== TokenType.CONDITION) throw new Error(`[Parser] Expected CONDITION after FOR, got ${TokenType[conditionToken.type]}`);
|
|
2561
|
-
const expression = parseForExpression(conditionToken.parts[0], 0);
|
|
2562
|
-
cursor.advance(2);
|
|
2563
|
-
const children = parseBlockChildren(cursor, parseNode);
|
|
2564
|
-
return {
|
|
2565
|
-
type: ASTNodeType.For,
|
|
2566
|
-
...expression,
|
|
2567
|
-
children
|
|
2568
|
-
};
|
|
2569
|
-
}
|
|
2570
|
-
/**
|
|
2571
|
-
* Parses the body of an `@for` block into a structured {@link ForExpression}.
|
|
2572
|
-
*
|
|
2573
|
-
* The expected format is:
|
|
2574
|
-
* ```
|
|
2575
|
-
* item of iterable; track expr[; $implicit = alias, ...]
|
|
2576
|
-
* ```
|
|
2577
|
-
*
|
|
2578
|
-
* @param source - The raw string content of the `@for(...)` expression.
|
|
2579
|
-
* @param baseOffset - Character offset of `source` within the original template,
|
|
2580
|
-
* used to produce accurate diagnostic positions.
|
|
2581
|
-
* @returns A {@link ForExpression} object. When unrecoverable syntax errors are
|
|
2582
|
-
* found the returned object contains only `diagnostics`.
|
|
2583
|
-
*/
|
|
2584
|
-
function parseForExpression(source, baseOffset) {
|
|
2585
|
-
const sections = splitForSections(source);
|
|
2586
|
-
if (sections.length < 2) throw new Error(`[Parser] @for requires at least "item of iterable; track expr".`);
|
|
2587
|
-
const iterSection = sections[0].trim();
|
|
2588
|
-
const ofIndex = iterSection.indexOf(" of ");
|
|
2589
|
-
if (ofIndex === -1) throw new Error(`[Parser] @for expression must be in the form "item of iterable".`);
|
|
2590
|
-
const itemAlias = iterSection.slice(0, ofIndex).trim();
|
|
2591
|
-
const iterableSource = iterSection.slice(ofIndex + 4).trim();
|
|
2592
|
-
if (!isValidIdentifier(itemAlias)) throw new Error(`[Parser] '${itemAlias}' is not a valid item alias.`);
|
|
2593
|
-
const iterValidation = validateExpression(iterableSource);
|
|
2594
|
-
const trackSection = sections[1].trim();
|
|
2595
|
-
if (!trackSection.startsWith("track ")) throw new Error(`[Parser] Second section of @for must start with "track".`);
|
|
2596
|
-
const trackSource = trackSection.slice(6).trim();
|
|
2597
|
-
const trackValidation = validateExpression(trackSource);
|
|
2598
|
-
const implicitAliases = /* @__PURE__ */ new Map();
|
|
2599
|
-
if (sections.length >= 3 && sections[2] !== void 0) parseImplicitAliases(sections[2].trim(), baseOffset + source.indexOf(sections[2]), implicitAliases);
|
|
2600
|
-
return {
|
|
2601
|
-
itemAlias,
|
|
2602
|
-
iterableExpression: iterValidation.node,
|
|
2603
|
-
iterableSource,
|
|
2604
|
-
trackExpression: trackValidation.node,
|
|
2605
|
-
trackSource,
|
|
2606
|
-
implicitAliases
|
|
2607
|
-
};
|
|
2608
|
-
}
|
|
2609
|
-
/**
|
|
2610
|
-
* Parses the optional third section of an `@for` expression, which declares
|
|
2611
|
-
* aliases for implicit loop variables (e.g. `$index = i, $last = l, $even = isEven`).
|
|
2612
|
-
*
|
|
2613
|
-
* Valid entries are comma-separated pairs in the form `$implicit = alias`.
|
|
2614
|
-
*
|
|
2615
|
-
* @param source - The raw alias-declarations string (everything after the second `;`).
|
|
2616
|
-
* @param baseOffset - Character offset of `source` within the original template.
|
|
2617
|
-
* @param out - Map to populate with `alias → implicit-variable` entries.
|
|
2618
|
-
*/
|
|
2619
|
-
function parseImplicitAliases(source, baseOffset, out) {
|
|
2620
|
-
const entries = source.split(",");
|
|
2621
|
-
let cursor = 0;
|
|
2622
|
-
const IMPLICIT_VARIABLES = /* @__PURE__ */ new Set([
|
|
2623
|
-
"$index",
|
|
2624
|
-
"$last",
|
|
2625
|
-
"$first",
|
|
2626
|
-
"$even",
|
|
2627
|
-
"$odd"
|
|
2628
|
-
]);
|
|
2629
|
-
for (const entry of entries) {
|
|
2630
|
-
const trimmed = entry.trim();
|
|
2631
|
-
const eqIndex = trimmed.indexOf("=");
|
|
2632
|
-
if (eqIndex === -1) throw new Error(`[Parser] Invalid alias declaration '${trimmed}'. Expected '$implicit = alias'.`);
|
|
2633
|
-
cursor += entry.length + 1;
|
|
2634
|
-
const alias = trimmed.slice(0, eqIndex).trim();
|
|
2635
|
-
const implicit = trimmed.slice(eqIndex + 1).trim();
|
|
2636
|
-
const isImplicitVariable = (value) => IMPLICIT_VARIABLES.has(value);
|
|
2637
|
-
if (!isImplicitVariable(implicit)) throw new Error(`[Parser] '${implicit}' is not a known implicit variable. Known variables: ${[...IMPLICIT_VARIABLES].join(", ")}.`);
|
|
2638
|
-
cursor += entry.length + 1;
|
|
2639
|
-
if (!isValidIdentifier(alias)) throw new Error(`[Parser] '${alias}' is not a valid alias identifier.`);
|
|
2640
|
-
cursor += entry.length + 1;
|
|
2641
|
-
if (out.has(implicit)) throw new Error(`[Parser] '${implicit}' is already aliased in this @for expression.`);
|
|
2642
|
-
else out.set(implicit, alias);
|
|
2643
|
-
cursor += entry.length + 1;
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
/**
|
|
2647
|
-
* Splits the raw `@for(...)` body into its semicolon-delimited sections,
|
|
2648
|
-
* respecting nested brackets and string literals so that semicolons inside
|
|
2649
|
-
* them are never treated as section separators.
|
|
2650
|
-
*
|
|
2651
|
-
* Example input: `"item of items; track item.id; $index = i"`
|
|
2652
|
-
* Example output: `["item of items", " track item.id", " $index = i"]`
|
|
2653
|
-
*
|
|
2654
|
-
* @param source - The raw content of the `@for(...)` expression.
|
|
2655
|
-
* @returns An array of section strings (without the `;` separators).
|
|
2656
|
-
*/
|
|
2657
|
-
function splitForSections(source) {
|
|
2658
|
-
const sections = new Array();
|
|
2659
|
-
let current = "";
|
|
2660
|
-
let depth = 0;
|
|
2661
|
-
let inString;
|
|
2662
|
-
for (let i = 0; i < source.length; i++) {
|
|
2663
|
-
const char = source[i];
|
|
2664
|
-
if (!current && char === " ") continue;
|
|
2665
|
-
if (inString) {
|
|
2666
|
-
current += char;
|
|
2667
|
-
if (char === inString && source[i - 1] !== "\\") inString = null;
|
|
2668
|
-
continue;
|
|
2669
|
-
}
|
|
2670
|
-
if (char === "\"" || char === "'" || char === "`") {
|
|
2671
|
-
inString = char;
|
|
2672
|
-
current += char;
|
|
2673
|
-
continue;
|
|
2674
|
-
}
|
|
2675
|
-
if (char === "(" || char === "[" || char === "{") {
|
|
2676
|
-
depth++;
|
|
2677
|
-
current += char;
|
|
2678
|
-
continue;
|
|
2679
|
-
}
|
|
2680
|
-
if (char === ")" || char === "]" || char === "}") {
|
|
2681
|
-
depth--;
|
|
2682
|
-
current += char;
|
|
2683
|
-
continue;
|
|
2684
|
-
}
|
|
2685
|
-
if (char === ";" && depth === 0) {
|
|
2686
|
-
sections.push(current);
|
|
2687
|
-
current = "";
|
|
2688
|
-
continue;
|
|
2689
|
-
}
|
|
2690
|
-
current += char;
|
|
2691
|
-
}
|
|
2692
|
-
if (current.trim().length) sections.push(current);
|
|
2693
|
-
return sections;
|
|
2694
|
-
}
|
|
2695
|
-
/**
|
|
2696
|
-
* Checks whether `name` is a valid JavaScript identifier by delegating to
|
|
2697
|
-
* the TypeScript parser.
|
|
2698
|
-
*
|
|
2699
|
-
* A string is considered valid when the TS parser produces a single
|
|
2700
|
-
* `ExpressionStatement` whose expression is an `Identifier` with the
|
|
2701
|
-
* same text.
|
|
2702
|
-
*
|
|
2703
|
-
* @param name - The string to validate.
|
|
2704
|
-
* @returns `true` if `name` is a valid JS identifier, `false` otherwise.
|
|
2705
|
-
*/
|
|
2706
|
-
function isValidIdentifier(name) {
|
|
2707
|
-
if (!name.length) return false;
|
|
2708
|
-
const statement = createSourceFile("__id.ts", name, ScriptTarget.ESNext, false).statements[0];
|
|
2709
|
-
return !!statement && isExpressionStatement(statement) && isIdentifier(statement.expression) && statement.expression.text === name;
|
|
2710
|
-
}
|
|
2711
|
-
//#endregion
|
|
2712
|
-
//#region ../packages/compiler/src/parser/states/parse-if.state.ts
|
|
2713
|
-
/**
|
|
2714
|
-
* Parses an `@if` directive, consuming the IF token, the CONDITION token,
|
|
2715
|
-
* the BLOCK_OPEN token, all consequent children, and an optional `@else` branch.
|
|
2716
|
-
*
|
|
2717
|
-
* @param cursor - Parser cursor positioned at the IF token.
|
|
2718
|
-
* @param context - Parser function for recursive child parsing.
|
|
2719
|
-
* @param token - The IF token (consumed for position advancement).
|
|
2720
|
-
* @returns The parsed `IfNode`.
|
|
2721
|
-
*/
|
|
2722
|
-
function parseIfControlFlow(cursor, parseNode, token) {
|
|
2723
|
-
return parseIfOrElseIf(cursor, parseNode, token);
|
|
2724
|
-
}
|
|
2725
|
-
function parseElseIfRecursively(cursor, parseNode, token) {
|
|
2726
|
-
switch (token.type) {
|
|
2727
|
-
case TokenType.ELSE_IF: return parseIfOrElseIf(cursor, parseNode, token);
|
|
2728
|
-
case TokenType.ELSE:
|
|
2729
|
-
cursor.advance(2);
|
|
2730
|
-
const elseChildren = parseBlockChildren(cursor, parseNode);
|
|
2731
|
-
return {
|
|
2732
|
-
type: ASTNodeType.Else,
|
|
2733
|
-
children: elseChildren
|
|
2734
|
-
};
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
function parseIfOrElseIf(cursor, parseNode, token) {
|
|
2738
|
-
cursor.advance();
|
|
2739
|
-
const conditionToken = cursor.peek();
|
|
2740
|
-
if (conditionToken.type !== TokenType.CONDITION) throw new Error(`[Parser] Expected CONDITION after ${TokenType[token.type]}, got ${TokenType[conditionToken.type]}`);
|
|
2741
|
-
cursor.advance(2);
|
|
2742
|
-
const condition = conditionToken.parts[0];
|
|
2743
|
-
const validationResult = validateExpression(condition);
|
|
2744
|
-
const consequent = parseBlockChildren(cursor, parseNode);
|
|
2745
|
-
return {
|
|
2746
|
-
type: token.type === TokenType.IF ? ASTNodeType.If : ASTNodeType.ElseIf,
|
|
2747
|
-
condition,
|
|
2748
|
-
conditionNode: validationResult.node,
|
|
2749
|
-
children: consequent,
|
|
2750
|
-
alternate: parseElseIfRecursively(cursor, parseNode, cursor.peek())
|
|
2751
|
-
};
|
|
2752
|
-
}
|
|
2753
|
-
//#endregion
|
|
2754
|
-
//#region ../packages/compiler/src/parser/states/parse-import.state.ts
|
|
2755
|
-
function parseImport(cursor, _parseNode, _token) {
|
|
2756
|
-
const imports = new Array();
|
|
2757
|
-
while (cursor.peek().type === TokenType.IMPORT) {
|
|
2758
|
-
cursor.advance();
|
|
2759
|
-
imports.push(cursor.getCurrentToken().value.parts[0]);
|
|
2760
|
-
}
|
|
2761
|
-
cursor.advance();
|
|
2762
|
-
return {
|
|
2763
|
-
type: ASTNodeType.Import,
|
|
2764
|
-
values: imports,
|
|
2765
|
-
path: cursor.getCurrentToken().value.parts[0]
|
|
2766
|
-
};
|
|
2767
|
-
}
|
|
2768
|
-
//#endregion
|
|
2769
|
-
//#region ../packages/compiler/src/parser/states/parse-switch.state.ts
|
|
2770
|
-
/**
|
|
2771
|
-
* Parses a `@switch` directive, consuming the SWITCH token, the CONDITION token,
|
|
2772
|
-
* the outer BLOCK_OPEN, all `@case` and `@default` branches, and the outer BLOCK_CLOSE.
|
|
2773
|
-
*
|
|
2774
|
-
* @param cursor - Parser cursor positioned at the SWITCH token.
|
|
2775
|
-
* @param parseNode - Parser function for recursive child parsing.
|
|
2776
|
-
* @param _token - The SWITCH token (consumed for position advancement).
|
|
2777
|
-
* @returns The parsed `SwitchNode`.
|
|
2778
|
-
*/
|
|
2779
|
-
function parseSwitchControlFlow(cursor, parseNode, _token) {
|
|
2780
|
-
cursor.advance();
|
|
2781
|
-
const conditionToken = cursor.peek();
|
|
2782
|
-
if (conditionToken.type !== TokenType.CONDITION) throw new Error(`[Parser] Expected CONDITION after SWITCH, got ${TokenType[conditionToken.type]}`);
|
|
2783
|
-
const expression = validateExpression(conditionToken.parts[0]).node;
|
|
2784
|
-
cursor.advance(2);
|
|
2785
|
-
const cases = new Array();
|
|
2786
|
-
while (cursor.peek().type !== TokenType.BLOCK_CLOSE) switch (cursor.peek().type) {
|
|
2787
|
-
case TokenType.CASE:
|
|
2788
|
-
const condition = new Array();
|
|
2789
|
-
do {
|
|
2790
|
-
cursor.advance();
|
|
2791
|
-
const caseCondition = cursor.peek();
|
|
2792
|
-
if (caseCondition.type !== TokenType.CONDITION) throw new Error(`[Parser] Expected CONDITION after CASE`);
|
|
2793
|
-
condition.push(caseCondition.parts[0]);
|
|
2794
|
-
cursor.advance();
|
|
2795
|
-
} while (cursor.peek().type !== TokenType.BLOCK_OPEN);
|
|
2796
|
-
cursor.advance();
|
|
2797
|
-
cases.push({
|
|
2798
|
-
type: ASTNodeType.Case,
|
|
2799
|
-
condition,
|
|
2800
|
-
children: parseBlockChildren(cursor, parseNode)
|
|
2801
|
-
});
|
|
2802
|
-
break;
|
|
2803
|
-
case TokenType.DEFAULT:
|
|
2804
|
-
cursor.advance(2);
|
|
2805
|
-
cases.push({
|
|
2806
|
-
type: ASTNodeType.Case,
|
|
2807
|
-
condition: null,
|
|
2808
|
-
children: parseBlockChildren(cursor, parseNode)
|
|
2809
|
-
});
|
|
2810
|
-
break;
|
|
2811
|
-
}
|
|
2812
|
-
cursor.advance();
|
|
2813
|
-
return {
|
|
2814
|
-
type: ASTNodeType.Switch,
|
|
2815
|
-
expression,
|
|
2816
|
-
children: cases
|
|
2817
|
-
};
|
|
2818
|
-
}
|
|
2819
|
-
//#endregion
|
|
2820
|
-
//#region ../packages/compiler/src/parser/states/parse-text.state.ts
|
|
2821
|
-
/**
|
|
2822
|
-
* Parses a TEXT token into a `TextNode`.
|
|
2823
|
-
*
|
|
2824
|
-
* @param cursor - Parser cursor; advanced past the TEXT token.
|
|
2825
|
-
* @param _parseNode - Unused parser function (kept for signature consistency).
|
|
2826
|
-
* @param token - The TEXT token containing the raw text content.
|
|
2827
|
-
* @returns The parsed `TextNode`.
|
|
2828
|
-
*/
|
|
2829
|
-
function parseText(cursor, _parseNode, token) {
|
|
2830
|
-
cursor.advance();
|
|
2831
|
-
return {
|
|
2832
|
-
type: ASTNodeType.Text,
|
|
2833
|
-
value: token.parts[0]
|
|
2834
|
-
};
|
|
2835
|
-
}
|
|
2836
|
-
//#endregion
|
|
2837
|
-
//#region ../packages/compiler/src/parser/parser.ts
|
|
2838
|
-
/**
|
|
2839
|
-
* Parser class that transforms a stream of tokens (from the Lexer)
|
|
2840
|
-
* into an Abstract Syntax Tree (AST) representing the template structure.
|
|
2841
|
-
*
|
|
2842
|
-
* Responsibilities:
|
|
2843
|
-
* - Parse text nodes, elements, attributes, events, and interpolations
|
|
2844
|
-
* - Maintain cursor state for sequential token consumption
|
|
2845
|
-
* - Detect tag boundaries and nested structures
|
|
2846
|
-
*
|
|
2847
|
-
* The parser assumes that the token stream is syntactically valid according
|
|
2848
|
-
* to the Lexer rules. Parsing errors are thrown as exceptions.
|
|
2849
|
-
*/
|
|
2850
|
-
var Parser = class {
|
|
2851
|
-
/**
|
|
2852
|
-
* Internal cursor for navigating tokens
|
|
2853
|
-
*/
|
|
2854
|
-
_cursor;
|
|
2855
|
-
/**
|
|
2856
|
-
* Mapping of token types to their corresponding parser transition functions,
|
|
2857
|
-
* which handle the logic for parsing each token type into AST nodes.
|
|
2858
|
-
*/
|
|
2859
|
-
_states = {
|
|
2860
|
-
[TokenType.TEXT]: parseText,
|
|
2861
|
-
[TokenType.INTERPOLATION_EXPRESSION]: parseInterpolation,
|
|
2862
|
-
[TokenType.INTERPOLATION_LITERAL]: parseInterpolation,
|
|
2863
|
-
[TokenType.TAG_OPEN_NAME]: parseElement,
|
|
2864
|
-
[TokenType.IF]: parseIfControlFlow,
|
|
2865
|
-
[TokenType.FOR]: parseForControlFlow,
|
|
2866
|
-
[TokenType.SWITCH]: parseSwitchControlFlow,
|
|
2867
|
-
[TokenType.IMPORT]: parseImport
|
|
2868
|
-
};
|
|
2869
|
-
/**
|
|
2870
|
-
* Creates a new Parser instance.
|
|
2871
|
-
*
|
|
2872
|
-
* @param tokens - Array of tokens produced by the Lexer.
|
|
2873
|
-
*/
|
|
2874
|
-
constructor(tokens) {
|
|
2875
|
-
this._cursor = new ParserCursor(tokens);
|
|
2876
|
-
}
|
|
2877
|
-
/**
|
|
2878
|
-
* Entry point for parsing the token stream into AST nodes.
|
|
2879
|
-
*
|
|
2880
|
-
* @returns Array of top-level AST nodes.
|
|
2881
|
-
*/
|
|
2882
|
-
parse() {
|
|
2883
|
-
const nodes = new Array();
|
|
2884
|
-
while (this._cursor.peek().type !== TokenType.EOF) {
|
|
2885
|
-
const parseNode = this.parseNode();
|
|
2886
|
-
if (parseNode) nodes.push(parseNode);
|
|
2887
|
-
}
|
|
2888
|
-
return nodes;
|
|
2889
|
-
}
|
|
2890
|
-
/**
|
|
2891
|
-
* Parses the next AST node based on the current token.
|
|
2892
|
-
*
|
|
2893
|
-
* @returns The parsed AST node, or `undefined` for EOF.
|
|
2894
|
-
* @throws When no transition function is registered for the current token type.
|
|
2895
|
-
*/
|
|
2896
|
-
parseNode() {
|
|
2897
|
-
const token = this._cursor.peek();
|
|
2898
|
-
if (token.type === TokenType.EOF) return;
|
|
2899
|
-
const state = this._states[token.type];
|
|
2900
|
-
if (!state) throw new Error(`[Parser] No transition function for token type ${TokenType[token.type]}`);
|
|
2901
|
-
return state(this._cursor, this.parseNode.bind(this), token);
|
|
2902
|
-
}
|
|
2903
|
-
};
|
|
2904
|
-
//#endregion
|
|
2905
|
-
//#region ../packages/compiler/src/type-checker/states/type-check-element.state.ts
|
|
2906
|
-
/**
|
|
2907
|
-
* Type-checks an element node: its attribute/property bindings, its event
|
|
2908
|
-
* handlers, and recursively its children.
|
|
2909
|
-
*
|
|
2910
|
-
* No variable is declared for the element itself (see the module doc on
|
|
2911
|
-
* `TypeChecker` for why) — attribute expressions and event calls are
|
|
2912
|
-
* emitted as bare statements, validated in place.
|
|
2913
|
-
*/
|
|
2914
|
-
function typeCheckElement(node, processNode, context) {
|
|
2915
|
-
const lines = new Array();
|
|
2916
|
-
node.attributes.forEach(({ value }) => {
|
|
2917
|
-
if (typeof value !== "string") lines.push(`${resolveExpression(value.expression, context, { resolver: "root" })};`);
|
|
2918
|
-
});
|
|
2919
|
-
node.events.forEach(({ handler, parameters }) => {
|
|
2920
|
-
const eventContext = new CompilerContext([], context);
|
|
2921
|
-
eventContext.addUnresolvableIdentifier("$event");
|
|
2922
|
-
const args = parameters.map((parameter) => resolveExpression(parameter, eventContext, { resolver: "root" })).join(", ");
|
|
2923
|
-
lines.push(`root.${handler}(${args});`);
|
|
2924
|
-
});
|
|
2925
|
-
node.children.forEach((child) => lines.push(...processNode(child, context)));
|
|
2926
|
-
return lines;
|
|
2927
|
-
}
|
|
2928
|
-
//#endregion
|
|
2929
|
-
//#region ../packages/compiler/src/type-checker/states/type-check-for.state.ts
|
|
2930
|
-
/**
|
|
2931
|
-
* Type-checks an `@for` block using a real `for...of` loop.
|
|
2932
|
-
*
|
|
2933
|
-
* This replaces the previous "synthetic function with a `typeof array`
|
|
2934
|
-
* parameter" trick, which mistyped the loop variable as the *whole array*
|
|
2935
|
-
* rather than a single element. A real `for (const item of array)` lets
|
|
2936
|
-
* TypeScript infer `item`'s type correctly as the array's element type —
|
|
2937
|
-
* exactly like it would for a loop written by hand — with no synthetic
|
|
2938
|
-
* function boundary needed.
|
|
2939
|
-
*/
|
|
2940
|
-
function typeCheckFor(node, processNode, context) {
|
|
2941
|
-
const forContext = new CompilerContext([], context);
|
|
2942
|
-
const indexName = resolveImplicit(node, "$index");
|
|
2943
|
-
const firstName = resolveImplicit(node, "$first");
|
|
2944
|
-
const lastName = resolveImplicit(node, "$last");
|
|
2945
|
-
const evenName = resolveImplicit(node, "$even");
|
|
2946
|
-
const oddName = resolveImplicit(node, "$odd");
|
|
2947
|
-
[
|
|
2948
|
-
indexName,
|
|
2949
|
-
firstName,
|
|
2950
|
-
lastName,
|
|
2951
|
-
evenName,
|
|
2952
|
-
oddName,
|
|
2953
|
-
node.itemAlias
|
|
2954
|
-
].forEach((identifier) => forContext.addUnresolvableIdentifier(identifier));
|
|
2955
|
-
const lines = new Array();
|
|
2956
|
-
lines.push(`for (const ${node.itemAlias} of root.${node.iterableSource}) {`);
|
|
2957
|
-
lines.push(...indent([
|
|
2958
|
-
`let ${indexName}!: number;`,
|
|
2959
|
-
`let ${firstName}!: boolean;`,
|
|
2960
|
-
`let ${lastName}!: boolean;`,
|
|
2961
|
-
`let ${evenName}!: boolean;`,
|
|
2962
|
-
`let ${oddName}!: boolean;`,
|
|
2963
|
-
`${resolveExpression(node.trackExpression, context, { skipResolution: true })};`,
|
|
2964
|
-
...node.children.flatMap((child) => processNode(child, forContext))
|
|
2965
|
-
]));
|
|
2966
|
-
lines.push("}");
|
|
2967
|
-
return lines;
|
|
2968
|
-
}
|
|
2969
|
-
/**
|
|
2970
|
-
* Resolves the name that should be used in generated code for a given
|
|
2971
|
-
* implicit variable.
|
|
2972
|
-
*
|
|
2973
|
-
* If the template declared an explicit alias for the variable
|
|
2974
|
-
* (e.g. `; $index = i`) that alias is returned. Otherwise the default
|
|
2975
|
-
* implicit variable name (e.g. `$index`) is used.
|
|
2976
|
-
*
|
|
2977
|
-
* @param node - The `ForNode` whose implicit alias map is consulted.
|
|
2978
|
-
* @param implicit - The implicit variable to look up (e.g. `'$index'`).
|
|
2979
|
-
* @returns The alias string if one was declared, otherwise `implicit` itself.
|
|
2980
|
-
*/
|
|
2981
|
-
function resolveImplicit(node, implicit) {
|
|
2982
|
-
return node.implicitAliases.get(implicit) ?? implicit;
|
|
2983
|
-
}
|
|
2984
|
-
//#endregion
|
|
2985
|
-
//#region ../packages/compiler/src/type-checker/states/type-check-if.state.ts
|
|
2986
|
-
/**
|
|
2987
|
-
* Type-checks an `@if`/`@else if`/`@else` chain using real TypeScript
|
|
2988
|
-
* `if` / `else if` / `else` blocks.
|
|
2989
|
-
*
|
|
2990
|
-
* This is a genuine correctness improvement over the previous
|
|
2991
|
-
* sibling-functions approach, not just a simplification: a real
|
|
2992
|
-
* `if`/`else if` chain gives the TS compiler's control-flow analysis the
|
|
2993
|
-
* negated narrowing of every preceding condition for free (e.g. inside an
|
|
2994
|
-
* `else if`, TS already knows the first condition was false) — something
|
|
2995
|
-
* flat sibling functions could never express.
|
|
2996
|
-
*/
|
|
2997
|
-
function typeCheckIf(node, processNode, context) {
|
|
2998
|
-
const lines = new Array();
|
|
2999
|
-
const condition = resolveExpression(node.conditionNode, context, { resolver: "root" });
|
|
3000
|
-
lines.push(`if (${condition}) {`);
|
|
3001
|
-
lines.push(...indent(node.children.flatMap((child) => processNode(child, context))));
|
|
3002
|
-
lines.push("}");
|
|
3003
|
-
let alt = node.alternate;
|
|
3004
|
-
while (alt?.type === ASTNodeType.ElseIf) {
|
|
3005
|
-
const elseIfCondition = resolveExpression(alt.conditionNode, context, { resolver: "root" });
|
|
3006
|
-
lines.push(`else if (${elseIfCondition}) {`);
|
|
3007
|
-
lines.push(...indent(alt.children.flatMap((child) => processNode(child, context))));
|
|
3008
|
-
lines.push("}");
|
|
3009
|
-
alt = alt.alternate;
|
|
3010
|
-
}
|
|
3011
|
-
if (alt) {
|
|
3012
|
-
lines.push("else {");
|
|
3013
|
-
lines.push(...indent(alt.children.flatMap((child) => processNode(child, context))));
|
|
3014
|
-
lines.push("}");
|
|
3015
|
-
}
|
|
3016
|
-
return lines;
|
|
3017
|
-
}
|
|
3018
|
-
//#endregion
|
|
3019
|
-
//#region ../packages/compiler/src/type-checker/states/type-check-switch.state.ts
|
|
3020
|
-
/**
|
|
3021
|
-
* Type-checks a `@switch` block using a real TypeScript `switch` statement.
|
|
3022
|
-
*
|
|
3023
|
-
* This drops the previous `const case_0: typeof switchExpr = 'literal';`
|
|
3024
|
-
* trick entirely: a real `switch (expr) { case 'literal': ... }` already
|
|
3025
|
-
* makes TS validate that each case value is assignable to the switch
|
|
3026
|
-
* expression's type as part of ordinary switch-statement semantics — and,
|
|
3027
|
-
* as a bonus, narrows the switch expression's type inside each case block
|
|
3028
|
-
* (e.g. from a `'loading' | 'error' | 'idle'` union down to just
|
|
3029
|
-
* `'loading'`), which the previous approach never provided.
|
|
3030
|
-
*
|
|
3031
|
-
* Multiple case labels that shared one body in the AST (fallthrough) are
|
|
3032
|
-
* emitted as stacked `case` labels sharing that same body, matching real
|
|
3033
|
-
* JS/TS fallthrough syntax directly.
|
|
3034
|
-
*/
|
|
3035
|
-
function typeCheckSwitch(node, processNode, context) {
|
|
3036
|
-
const lines = [`switch (${resolveExpression(node.expression, context, { resolver: "root" })}) {`];
|
|
3037
|
-
node.children.forEach((caseNode) => {
|
|
3038
|
-
caseNode.condition?.length ? caseNode.condition.forEach((conditionValue) => lines.push(` case ${conditionValue}:`)) : lines.push(" default:");
|
|
3039
|
-
lines.push(...indent(indent([...caseNode.children.flatMap((child) => processNode(child, context)), "break;"])));
|
|
3040
|
-
});
|
|
3041
|
-
lines.push("}");
|
|
3042
|
-
return lines;
|
|
3043
|
-
}
|
|
3044
|
-
//#endregion
|
|
3045
|
-
//#region ../packages/compiler/src/type-checker/states/type-check-text-and-interpolation.state.ts
|
|
3046
|
-
/**
|
|
3047
|
-
* Type-checks a text or interpolation node.
|
|
3048
|
-
*
|
|
3049
|
-
* Plain text nodes carry no expression, so they produce no lines. An
|
|
3050
|
-
* interpolation's expression is emitted as a bare statement — enough for
|
|
3051
|
-
* TS to validate it, with no name needing to be bound to the result.
|
|
3052
|
-
*/
|
|
3053
|
-
function typeCheckTextAndInterpolation(node, _processNode, context) {
|
|
3054
|
-
return node.type === ASTNodeType.Interpolation ? [`${resolveExpression(node.expression, context, { resolver: "root" })};`] : [];
|
|
3055
|
-
}
|
|
3056
|
-
//#endregion
|
|
3057
|
-
//#region ../packages/compiler/src/type-checker/type-checker.ts
|
|
3058
|
-
/**
|
|
3059
|
-
* Generates a single, flat TypeScript function body ("shim") from a
|
|
3060
|
-
* template AST, meant only to be fed to the TS compiler / LanguageService
|
|
3061
|
-
* for diagnostics — it is never executed and never emitted as real output.
|
|
3062
|
-
*
|
|
3063
|
-
* This deliberately does NOT mirror the JS code generator's structure:
|
|
3064
|
-
*
|
|
3065
|
-
* - No variable is declared per HTML element. Element identifiers exist in
|
|
3066
|
-
* the JS output purely so runtime code can create/reference the actual
|
|
3067
|
-
* DOM node; a type-check expression never references "the element
|
|
3068
|
-
* itself" (the DSL has no template-ref syntax), so an `HTMLElement`
|
|
3069
|
-
* local would add zero type-checking value.
|
|
3070
|
-
* - No control-flow block gets its own function. In the JS output, each
|
|
3071
|
-
* `@if`/`@for`/`@switch` becomes a separate function because it needs
|
|
3072
|
-
* its own runtime closure over the `Context` chain. The type checker has
|
|
3073
|
-
* no runtime at all, so real, nested TypeScript blocks — `if`, `for`,
|
|
3074
|
-
* `switch` — give correct scoping and (as a bonus) real control-flow
|
|
3075
|
-
* narrowing, for free, with no synthetic machinery.
|
|
3076
|
-
*
|
|
3077
|
-
* Every AST node turns directly into TypeScript lines, recursively, inside
|
|
3078
|
-
* one single `typeCheck()` function.
|
|
3079
|
-
*/
|
|
3080
|
-
var TypeChecker = class {
|
|
3081
|
-
_ast;
|
|
3082
|
-
_states = {
|
|
3083
|
-
[ASTNodeType.Text]: typeCheckTextAndInterpolation,
|
|
3084
|
-
[ASTNodeType.Interpolation]: typeCheckTextAndInterpolation,
|
|
3085
|
-
[ASTNodeType.Element]: typeCheckElement,
|
|
3086
|
-
[ASTNodeType.If]: typeCheckIf,
|
|
3087
|
-
[ASTNodeType.For]: typeCheckFor,
|
|
3088
|
-
[ASTNodeType.Switch]: typeCheckSwitch,
|
|
3089
|
-
[ASTNodeType.Import]: skipGeneration
|
|
3090
|
-
};
|
|
3091
|
-
constructor(_ast) {
|
|
3092
|
-
this._ast = _ast;
|
|
3093
|
-
}
|
|
3094
|
-
/**
|
|
3095
|
-
* Generates the full `function typeCheck() { ... }` shim body for the
|
|
3096
|
-
* component's template.
|
|
3097
|
-
*
|
|
3098
|
-
* A `let $event!: Event;` declaration is prepended only if the generated
|
|
3099
|
-
* body actually references `$event` (event handler bindings), so shims
|
|
3100
|
-
* for templates with no event bindings don't carry an unused local —
|
|
3101
|
-
* relevant if the consuming project has `noUnusedLocals` enabled.
|
|
3102
|
-
*/
|
|
3103
|
-
generate() {
|
|
3104
|
-
return [
|
|
3105
|
-
"function typeCheck() {",
|
|
3106
|
-
...indent(this._ast.flatMap((node) => this._processNode(node))),
|
|
3107
|
-
"}"
|
|
3108
|
-
].join("\n");
|
|
3109
|
-
}
|
|
3110
|
-
/**
|
|
3111
|
-
* Dispatches a single AST node to its state function, passing itself
|
|
3112
|
-
* back down as `processNode` so state functions can recurse into their
|
|
3113
|
-
* own children inline.
|
|
3114
|
-
*/
|
|
3115
|
-
_processNode = (node, context) => {
|
|
3116
|
-
const state = this._states[node.type];
|
|
3117
|
-
if (!state) throw new Error(`[Type Checker] No transition function for token type ${ASTNodeType[node.type]}`);
|
|
3118
|
-
return state(node, this._processNode, context);
|
|
3119
|
-
};
|
|
3120
|
-
};
|
|
3121
|
-
//#endregion
|
|
3122
|
-
//#region ../packages/compiler/src/compile.ts
|
|
3123
|
-
/**
|
|
3124
|
-
* Compiles a template string into a Javascript render function body.
|
|
3125
|
-
*
|
|
3126
|
-
* Runs the three-stage pipeline:
|
|
3127
|
-
* 1. **Lexer** — tokenises the raw template text.
|
|
3128
|
-
* 2. **Parser** — transforms the token stream into an AST.
|
|
3129
|
-
* 3. **Render generator** — emits Javascript source lines from the AST.
|
|
3130
|
-
*
|
|
3131
|
-
* @param input - The raw HTML-like template source to compile.
|
|
3132
|
-
* @param cssVariableName - Optional name of the CSS variable to inject
|
|
3133
|
-
* into the generated `adoptedStyleSheets` assignment.
|
|
3134
|
-
* @returns A string containing the compiled Javascript render method body.
|
|
3135
|
-
*/
|
|
3136
|
-
function compile(input, cssVariableName) {
|
|
3137
|
-
const nodes = new Parser(new Lexer(input).tokenize()).parse();
|
|
3138
|
-
return {
|
|
3139
|
-
javascript: new Generator(nodes).generate(cssVariableName),
|
|
3140
|
-
typescript: new TypeChecker(nodes).generate()
|
|
3141
|
-
};
|
|
3142
|
-
}
|
|
3143
|
-
//#endregion
|
|
3144
|
-
export { compile };
|