@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.
@@ -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 };