pulse-js-framework 1.10.0 → 1.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compiler/parser/_extract.js +393 -0
- package/compiler/parser/blocks.js +361 -0
- package/compiler/parser/core.js +306 -0
- package/compiler/parser/expressions.js +386 -0
- package/compiler/parser/imports.js +108 -0
- package/compiler/parser/index.js +47 -0
- package/compiler/parser/state.js +155 -0
- package/compiler/parser/style.js +445 -0
- package/compiler/parser/view.js +632 -0
- package/compiler/parser.js +15 -2372
- package/compiler/parser.js.original +2376 -0
- package/package.json +2 -1
- package/runtime/a11y/announcements.js +213 -0
- package/runtime/a11y/contrast.js +125 -0
- package/runtime/a11y/focus.js +412 -0
- package/runtime/a11y/index.js +35 -0
- package/runtime/a11y/preferences.js +121 -0
- package/runtime/a11y/utils.js +164 -0
- package/runtime/a11y/validation.js +258 -0
- package/runtime/a11y/widgets.js +545 -0
- package/runtime/a11y.js +15 -1840
- package/runtime/a11y.js.original +1844 -0
- package/runtime/graphql/cache.js +69 -0
- package/runtime/graphql/client.js +563 -0
- package/runtime/graphql/hooks.js +492 -0
- package/runtime/graphql/index.js +62 -0
- package/runtime/graphql/subscriptions.js +241 -0
- package/runtime/graphql.js +12 -1322
- package/runtime/graphql.js.original +1326 -0
- package/runtime/router/core.js +956 -0
- package/runtime/router/guards.js +90 -0
- package/runtime/router/history.js +204 -0
- package/runtime/router/index.js +36 -0
- package/runtime/router/lazy.js +180 -0
- package/runtime/router/utils.js +226 -0
- package/runtime/router.js +12 -1600
- package/runtime/router.js.original +1605 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Parser - View Block Parsing
|
|
3
|
+
*
|
|
4
|
+
* Handles parsing of view blocks including elements, directives, and components
|
|
5
|
+
*
|
|
6
|
+
* @module compiler/parser/view
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { TokenType } from '../lexer.js';
|
|
10
|
+
import { NodeType, ASTNode, Parser } from './core.js';
|
|
11
|
+
import { SUGGESTIONS } from '../../runtime/errors.js';
|
|
12
|
+
|
|
13
|
+
// ============================================================
|
|
14
|
+
// View Block Parsing
|
|
15
|
+
// ============================================================
|
|
16
|
+
|
|
17
|
+
Parser.prototype.parseViewBlock = function() {
|
|
18
|
+
this.expect(TokenType.VIEW);
|
|
19
|
+
this.expect(TokenType.LBRACE);
|
|
20
|
+
|
|
21
|
+
const children = [];
|
|
22
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
23
|
+
children.push(this.parseViewChild());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.expect(TokenType.RBRACE);
|
|
27
|
+
return new ASTNode(NodeType.ViewBlock, { children });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a view child (element, directive, slot, or text)
|
|
32
|
+
*/
|
|
33
|
+
Parser.prototype.parseViewChild = function() {
|
|
34
|
+
if (this.is(TokenType.AT)) {
|
|
35
|
+
return this.parseDirective();
|
|
36
|
+
}
|
|
37
|
+
// Slot element
|
|
38
|
+
if (this.is(TokenType.SLOT)) {
|
|
39
|
+
return this.parseSlotElement();
|
|
40
|
+
}
|
|
41
|
+
if (this.is(TokenType.SELECTOR) || this.is(TokenType.IDENT)) {
|
|
42
|
+
return this.parseElement();
|
|
43
|
+
}
|
|
44
|
+
if (this.is(TokenType.STRING)) {
|
|
45
|
+
return this.parseTextNode();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const token = this.current();
|
|
49
|
+
throw this.createError(
|
|
50
|
+
`Unexpected token '${token?.value || token?.type}' in view block. ` +
|
|
51
|
+
`Expected: element selector, @directive, slot, or "text"`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse slot element for component composition
|
|
57
|
+
* Supports:
|
|
58
|
+
* slot - default slot
|
|
59
|
+
* slot "name" - named slot
|
|
60
|
+
* slot { default content }
|
|
61
|
+
*/
|
|
62
|
+
Parser.prototype.parseSlotElement = function() {
|
|
63
|
+
const startToken = this.expect(TokenType.SLOT);
|
|
64
|
+
let name = 'default';
|
|
65
|
+
const fallback = [];
|
|
66
|
+
|
|
67
|
+
// Named slot: slot "header"
|
|
68
|
+
if (this.is(TokenType.STRING)) {
|
|
69
|
+
name = this.advance().value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fallback content: slot { ... }
|
|
73
|
+
if (this.is(TokenType.LBRACE)) {
|
|
74
|
+
this.advance();
|
|
75
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
76
|
+
fallback.push(this.parseViewChild());
|
|
77
|
+
}
|
|
78
|
+
this.expect(TokenType.RBRACE);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new ASTNode(NodeType.SlotElement, {
|
|
82
|
+
name,
|
|
83
|
+
fallback,
|
|
84
|
+
line: startToken.line,
|
|
85
|
+
column: startToken.column
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse an element
|
|
91
|
+
*/
|
|
92
|
+
Parser.prototype.parseElement = function() {
|
|
93
|
+
const selector = this.isAny(TokenType.SELECTOR, TokenType.IDENT)
|
|
94
|
+
? this.advance().value
|
|
95
|
+
: '';
|
|
96
|
+
|
|
97
|
+
const directives = [];
|
|
98
|
+
const textContent = [];
|
|
99
|
+
const children = [];
|
|
100
|
+
const props = []; // Props passed to component
|
|
101
|
+
|
|
102
|
+
// Check if this is a component with props: Component(prop=value, ...)
|
|
103
|
+
if (this.is(TokenType.LPAREN)) {
|
|
104
|
+
this.advance(); // consume (
|
|
105
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
106
|
+
props.push(this.parseComponentProp());
|
|
107
|
+
if (this.is(TokenType.COMMA)) {
|
|
108
|
+
this.advance();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.expect(TokenType.RPAREN);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Parse inline directives and text
|
|
115
|
+
while (!this.is(TokenType.LBRACE) && !this.is(TokenType.RBRACE) &&
|
|
116
|
+
!this.is(TokenType.SELECTOR) && !this.is(TokenType.EOF)) {
|
|
117
|
+
if (this.is(TokenType.AT)) {
|
|
118
|
+
// Check if this is a block directive (@if, @for, @each) - if so, break
|
|
119
|
+
const nextToken = this.peek();
|
|
120
|
+
if (nextToken && (nextToken.type === TokenType.IF ||
|
|
121
|
+
nextToken.type === TokenType.FOR ||
|
|
122
|
+
nextToken.type === TokenType.EACH)) {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
directives.push(this.parseInlineDirective());
|
|
126
|
+
} else if (this.is(TokenType.STRING)) {
|
|
127
|
+
textContent.push(this.parseTextNode());
|
|
128
|
+
} else if (this.is(TokenType.IDENT) && !this.couldBeElement()) {
|
|
129
|
+
break;
|
|
130
|
+
} else {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Parse children if there's a block
|
|
136
|
+
if (this.is(TokenType.LBRACE)) {
|
|
137
|
+
this.advance();
|
|
138
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
139
|
+
children.push(this.parseViewChild());
|
|
140
|
+
}
|
|
141
|
+
this.expect(TokenType.RBRACE);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return new ASTNode(NodeType.Element, {
|
|
145
|
+
selector,
|
|
146
|
+
directives,
|
|
147
|
+
textContent,
|
|
148
|
+
children,
|
|
149
|
+
props
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parse a component prop: name=value or name={expression}
|
|
155
|
+
*/
|
|
156
|
+
Parser.prototype.parseComponentProp = function() {
|
|
157
|
+
const name = this.expect(TokenType.IDENT);
|
|
158
|
+
this.expect(TokenType.EQ);
|
|
159
|
+
|
|
160
|
+
let value;
|
|
161
|
+
if (this.is(TokenType.LBRACE)) {
|
|
162
|
+
this.advance();
|
|
163
|
+
value = this.parseExpression();
|
|
164
|
+
this.expect(TokenType.RBRACE);
|
|
165
|
+
} else {
|
|
166
|
+
value = this.tryParseLiteral();
|
|
167
|
+
if (!value) {
|
|
168
|
+
if (this.is(TokenType.IDENT)) {
|
|
169
|
+
value = this.parseIdentifierOrExpression();
|
|
170
|
+
} else {
|
|
171
|
+
throw this.createError(`Unexpected token in prop value: ${this.current()?.type}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return new ASTNode(NodeType.Property, { name: name.value, value });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if current position could be an element
|
|
181
|
+
*/
|
|
182
|
+
Parser.prototype.couldBeElement = function() {
|
|
183
|
+
const next = this.peek();
|
|
184
|
+
return next?.type === TokenType.LBRACE ||
|
|
185
|
+
next?.type === TokenType.AT ||
|
|
186
|
+
next?.type === TokenType.STRING;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parse a text node
|
|
191
|
+
*/
|
|
192
|
+
Parser.prototype.parseTextNode = function() {
|
|
193
|
+
const token = this.expect(TokenType.STRING);
|
|
194
|
+
const parts = this.parseInterpolatedString(token.value);
|
|
195
|
+
return new ASTNode(NodeType.TextNode, { parts });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Parse interpolated string into parts
|
|
200
|
+
* "Hello, {name}!" -> ["Hello, ", { expr: "name" }, "!"]
|
|
201
|
+
*/
|
|
202
|
+
Parser.prototype.parseInterpolatedString = function(str) {
|
|
203
|
+
const parts = [];
|
|
204
|
+
let current = '';
|
|
205
|
+
let i = 0;
|
|
206
|
+
|
|
207
|
+
while (i < str.length) {
|
|
208
|
+
if (str[i] === '{') {
|
|
209
|
+
if (current) {
|
|
210
|
+
parts.push(current);
|
|
211
|
+
current = '';
|
|
212
|
+
}
|
|
213
|
+
i++; // skip {
|
|
214
|
+
let expr = '';
|
|
215
|
+
let braceCount = 1;
|
|
216
|
+
while (i < str.length && braceCount > 0) {
|
|
217
|
+
if (str[i] === '{') braceCount++;
|
|
218
|
+
else if (str[i] === '}') braceCount--;
|
|
219
|
+
if (braceCount > 0) expr += str[i];
|
|
220
|
+
i++;
|
|
221
|
+
}
|
|
222
|
+
parts.push(new ASTNode(NodeType.Interpolation, { expression: expr.trim() }));
|
|
223
|
+
} else {
|
|
224
|
+
current += str[i];
|
|
225
|
+
i++;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (current) {
|
|
230
|
+
parts.push(current);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return parts;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Parse a directive (@if, @for, @each, @click, @link, @outlet, @navigate, etc.)
|
|
238
|
+
*/
|
|
239
|
+
Parser.prototype.parseDirective = function() {
|
|
240
|
+
this.expect(TokenType.AT);
|
|
241
|
+
|
|
242
|
+
// Handle @if - IF is a keyword token, not IDENT
|
|
243
|
+
if (this.is(TokenType.IF)) {
|
|
244
|
+
this.advance();
|
|
245
|
+
return this.parseIfDirective();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Handle @for - FOR is a keyword token, not IDENT
|
|
249
|
+
if (this.is(TokenType.FOR)) {
|
|
250
|
+
this.advance();
|
|
251
|
+
return this.parseEachDirective();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle router directives
|
|
255
|
+
if (this.is(TokenType.LINK)) {
|
|
256
|
+
this.advance();
|
|
257
|
+
return this.parseLinkDirective();
|
|
258
|
+
}
|
|
259
|
+
if (this.is(TokenType.OUTLET)) {
|
|
260
|
+
this.advance();
|
|
261
|
+
return this.parseOutletDirective();
|
|
262
|
+
}
|
|
263
|
+
if (this.is(TokenType.NAVIGATE)) {
|
|
264
|
+
this.advance();
|
|
265
|
+
return this.parseNavigateDirective();
|
|
266
|
+
}
|
|
267
|
+
if (this.is(TokenType.BACK)) {
|
|
268
|
+
this.advance();
|
|
269
|
+
return new ASTNode(NodeType.NavigateDirective, { action: 'back' });
|
|
270
|
+
}
|
|
271
|
+
if (this.is(TokenType.FORWARD)) {
|
|
272
|
+
this.advance();
|
|
273
|
+
return new ASTNode(NodeType.NavigateDirective, { action: 'forward' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
277
|
+
|
|
278
|
+
// Collect modifiers (.prevent, .stop, .enter, .lazy, etc.)
|
|
279
|
+
const modifiers = [];
|
|
280
|
+
while (this.is(TokenType.DIRECTIVE_MOD)) {
|
|
281
|
+
modifiers.push(this.advance().value);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (name === 'if') {
|
|
285
|
+
return this.parseIfDirective();
|
|
286
|
+
}
|
|
287
|
+
if (name === 'each' || name === 'for') {
|
|
288
|
+
return this.parseEachDirective();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Accessibility directives
|
|
292
|
+
if (name === 'a11y') {
|
|
293
|
+
return this.parseA11yDirective();
|
|
294
|
+
}
|
|
295
|
+
if (name === 'live') {
|
|
296
|
+
return this.parseLiveDirective();
|
|
297
|
+
}
|
|
298
|
+
if (name === 'focusTrap') {
|
|
299
|
+
return this.parseFocusTrapDirective();
|
|
300
|
+
}
|
|
301
|
+
if (name === 'srOnly') {
|
|
302
|
+
return this.parseSrOnlyDirective();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// SSR directives
|
|
306
|
+
if (name === 'client') {
|
|
307
|
+
return new ASTNode(NodeType.ClientDirective, {});
|
|
308
|
+
}
|
|
309
|
+
if (name === 'server') {
|
|
310
|
+
return new ASTNode(NodeType.ServerDirective, {});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// @model directive for two-way binding
|
|
314
|
+
if (name === 'model') {
|
|
315
|
+
return this.parseModelDirective(modifiers);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Event directive like @click
|
|
319
|
+
return this.parseEventDirective(name, modifiers);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Parse inline directive
|
|
324
|
+
*/
|
|
325
|
+
Parser.prototype.parseInlineDirective = function() {
|
|
326
|
+
this.expect(TokenType.AT);
|
|
327
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
328
|
+
|
|
329
|
+
// Collect modifiers (.prevent, .stop, .enter, .lazy, etc.)
|
|
330
|
+
const modifiers = [];
|
|
331
|
+
while (this.is(TokenType.DIRECTIVE_MOD)) {
|
|
332
|
+
modifiers.push(this.advance().value);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Check for a11y directives
|
|
336
|
+
if (name === 'a11y') {
|
|
337
|
+
return this.parseA11yDirective();
|
|
338
|
+
}
|
|
339
|
+
if (name === 'live') {
|
|
340
|
+
return this.parseLiveDirective();
|
|
341
|
+
}
|
|
342
|
+
if (name === 'focusTrap') {
|
|
343
|
+
return this.parseFocusTrapDirective();
|
|
344
|
+
}
|
|
345
|
+
if (name === 'srOnly') {
|
|
346
|
+
return this.parseSrOnlyDirective();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// SSR directives
|
|
350
|
+
if (name === 'client') {
|
|
351
|
+
return new ASTNode(NodeType.ClientDirective, {});
|
|
352
|
+
}
|
|
353
|
+
if (name === 'server') {
|
|
354
|
+
return new ASTNode(NodeType.ServerDirective, {});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// @model directive for two-way binding
|
|
358
|
+
if (name === 'model') {
|
|
359
|
+
return this.parseModelDirective(modifiers);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Event directive (click, submit, etc.)
|
|
363
|
+
this.expect(TokenType.LPAREN);
|
|
364
|
+
const expression = this.parseExpression();
|
|
365
|
+
this.expect(TokenType.RPAREN);
|
|
366
|
+
|
|
367
|
+
return new ASTNode(NodeType.EventDirective, { event: name, handler: expression, modifiers });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Parse @if directive with @else-if/@else chains
|
|
372
|
+
* Syntax: @if (cond) { } @else-if (cond) { } @else { }
|
|
373
|
+
*/
|
|
374
|
+
Parser.prototype.parseIfDirective = function() {
|
|
375
|
+
this.expect(TokenType.LPAREN);
|
|
376
|
+
const condition = this.parseExpression();
|
|
377
|
+
this.expect(TokenType.RPAREN);
|
|
378
|
+
|
|
379
|
+
this.expect(TokenType.LBRACE);
|
|
380
|
+
const consequent = [];
|
|
381
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
382
|
+
consequent.push(this.parseViewChild());
|
|
383
|
+
}
|
|
384
|
+
this.expect(TokenType.RBRACE);
|
|
385
|
+
|
|
386
|
+
const elseIfBranches = [];
|
|
387
|
+
let alternate = null;
|
|
388
|
+
|
|
389
|
+
// Parse @else-if and @else chains
|
|
390
|
+
while (this.is(TokenType.AT)) {
|
|
391
|
+
const nextToken = this.peek();
|
|
392
|
+
|
|
393
|
+
// Check for @else or @else-if
|
|
394
|
+
if (nextToken?.value === 'else') {
|
|
395
|
+
this.advance(); // @
|
|
396
|
+
this.advance(); // else
|
|
397
|
+
|
|
398
|
+
// Check if followed by @if or -if (making @else @if or @else-if)
|
|
399
|
+
if (this.is(TokenType.AT) && (this.peek()?.type === TokenType.IF || this.peek()?.value === 'if')) {
|
|
400
|
+
// @else @if pattern
|
|
401
|
+
this.advance(); // @
|
|
402
|
+
this.advance(); // if
|
|
403
|
+
|
|
404
|
+
this.expect(TokenType.LPAREN);
|
|
405
|
+
const elseIfCondition = this.parseExpression();
|
|
406
|
+
this.expect(TokenType.RPAREN);
|
|
407
|
+
|
|
408
|
+
this.expect(TokenType.LBRACE);
|
|
409
|
+
const elseIfConsequent = [];
|
|
410
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
411
|
+
elseIfConsequent.push(this.parseViewChild());
|
|
412
|
+
}
|
|
413
|
+
this.expect(TokenType.RBRACE);
|
|
414
|
+
|
|
415
|
+
elseIfBranches.push({ condition: elseIfCondition, consequent: elseIfConsequent });
|
|
416
|
+
}
|
|
417
|
+
// Check for -if pattern (@else-if as hyphenated)
|
|
418
|
+
else if (this.is(TokenType.MINUS) && (this.peek()?.type === TokenType.IF || this.peek()?.value === 'if')) {
|
|
419
|
+
this.advance(); // -
|
|
420
|
+
this.advance(); // if
|
|
421
|
+
|
|
422
|
+
this.expect(TokenType.LPAREN);
|
|
423
|
+
const elseIfCondition = this.parseExpression();
|
|
424
|
+
this.expect(TokenType.RPAREN);
|
|
425
|
+
|
|
426
|
+
this.expect(TokenType.LBRACE);
|
|
427
|
+
const elseIfConsequent = [];
|
|
428
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
429
|
+
elseIfConsequent.push(this.parseViewChild());
|
|
430
|
+
}
|
|
431
|
+
this.expect(TokenType.RBRACE);
|
|
432
|
+
|
|
433
|
+
elseIfBranches.push({ condition: elseIfCondition, consequent: elseIfConsequent });
|
|
434
|
+
}
|
|
435
|
+
// Plain @else
|
|
436
|
+
else {
|
|
437
|
+
this.expect(TokenType.LBRACE);
|
|
438
|
+
alternate = [];
|
|
439
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
440
|
+
alternate.push(this.parseViewChild());
|
|
441
|
+
}
|
|
442
|
+
this.expect(TokenType.RBRACE);
|
|
443
|
+
break; // @else terminates the chain
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
break; // Not an @else variant
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return new ASTNode(NodeType.IfDirective, { condition, consequent, elseIfBranches, alternate });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Parse @each/@for directive with optional key function
|
|
455
|
+
* Syntax: @for (item of items) key(item.id) { ... }
|
|
456
|
+
*/
|
|
457
|
+
Parser.prototype.parseEachDirective = function() {
|
|
458
|
+
this.expect(TokenType.LPAREN);
|
|
459
|
+
const itemName = this.expect(TokenType.IDENT).value;
|
|
460
|
+
// Accept both 'in' and 'of' keywords
|
|
461
|
+
if (this.is(TokenType.IN)) {
|
|
462
|
+
this.advance();
|
|
463
|
+
} else if (this.is(TokenType.OF)) {
|
|
464
|
+
this.advance();
|
|
465
|
+
} else {
|
|
466
|
+
throw this.createError('Expected "in" or "of" in loop directive');
|
|
467
|
+
}
|
|
468
|
+
const iterable = this.parseExpression();
|
|
469
|
+
this.expect(TokenType.RPAREN);
|
|
470
|
+
|
|
471
|
+
// Parse optional key function: key(item.id)
|
|
472
|
+
let keyExpr = null;
|
|
473
|
+
if (this.is(TokenType.IDENT) && this.current().value === 'key') {
|
|
474
|
+
this.advance(); // consume 'key'
|
|
475
|
+
this.expect(TokenType.LPAREN);
|
|
476
|
+
keyExpr = this.parseExpression();
|
|
477
|
+
this.expect(TokenType.RPAREN);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
this.expect(TokenType.LBRACE);
|
|
481
|
+
const template = [];
|
|
482
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
483
|
+
template.push(this.parseViewChild());
|
|
484
|
+
}
|
|
485
|
+
this.expect(TokenType.RBRACE);
|
|
486
|
+
|
|
487
|
+
return new ASTNode(NodeType.EachDirective, { itemName, iterable, template, keyExpr });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Parse event directive with optional modifiers
|
|
492
|
+
* @param {string} event - Event name (click, keydown, etc.)
|
|
493
|
+
* @param {string[]} modifiers - Array of modifier names (prevent, stop, enter, etc.)
|
|
494
|
+
*/
|
|
495
|
+
Parser.prototype.parseEventDirective = function(event, modifiers = []) {
|
|
496
|
+
this.expect(TokenType.LPAREN);
|
|
497
|
+
const handler = this.parseExpression();
|
|
498
|
+
this.expect(TokenType.RPAREN);
|
|
499
|
+
|
|
500
|
+
const children = [];
|
|
501
|
+
if (this.is(TokenType.LBRACE)) {
|
|
502
|
+
this.advance();
|
|
503
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
504
|
+
children.push(this.parseViewChild());
|
|
505
|
+
}
|
|
506
|
+
this.expect(TokenType.RBRACE);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return new ASTNode(NodeType.EventDirective, { event, handler, children, modifiers });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Parse @model directive for two-way binding
|
|
514
|
+
* @model(name) or @model.lazy(name) or @model.lazy.trim(name)
|
|
515
|
+
* @param {string[]} modifiers - Array of modifier names (lazy, trim, number)
|
|
516
|
+
*/
|
|
517
|
+
Parser.prototype.parseModelDirective = function(modifiers = []) {
|
|
518
|
+
this.expect(TokenType.LPAREN);
|
|
519
|
+
const binding = this.parseExpression();
|
|
520
|
+
this.expect(TokenType.RPAREN);
|
|
521
|
+
|
|
522
|
+
return new ASTNode(NodeType.ModelDirective, { binding, modifiers });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Parse @a11y directive - sets aria attributes
|
|
527
|
+
* @a11y(label="Close menu") or @a11y(label="Close", describedby="desc")
|
|
528
|
+
*/
|
|
529
|
+
Parser.prototype.parseA11yDirective = function() {
|
|
530
|
+
this.expect(TokenType.LPAREN);
|
|
531
|
+
|
|
532
|
+
const attrs = {};
|
|
533
|
+
|
|
534
|
+
// Parse key=value pairs
|
|
535
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
536
|
+
const key = this.expect(TokenType.IDENT).value;
|
|
537
|
+
this.expect(TokenType.EQ);
|
|
538
|
+
|
|
539
|
+
let value;
|
|
540
|
+
if (this.is(TokenType.STRING)) {
|
|
541
|
+
value = this.advance().value;
|
|
542
|
+
} else if (this.is(TokenType.TRUE)) {
|
|
543
|
+
value = true;
|
|
544
|
+
this.advance();
|
|
545
|
+
} else if (this.is(TokenType.FALSE)) {
|
|
546
|
+
value = false;
|
|
547
|
+
this.advance();
|
|
548
|
+
} else if (this.is(TokenType.IDENT)) {
|
|
549
|
+
// Treat unquoted identifier as a string (e.g., role=dialog -> "dialog")
|
|
550
|
+
value = this.advance().value;
|
|
551
|
+
} else {
|
|
552
|
+
value = this.parseExpression();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
attrs[key] = value;
|
|
556
|
+
|
|
557
|
+
if (this.is(TokenType.COMMA)) {
|
|
558
|
+
this.advance();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
this.expect(TokenType.RPAREN);
|
|
563
|
+
|
|
564
|
+
return new ASTNode(NodeType.A11yDirective, { attrs });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Parse @live directive - creates live region for screen readers
|
|
569
|
+
* @live(polite) or @live(assertive)
|
|
570
|
+
*/
|
|
571
|
+
Parser.prototype.parseLiveDirective = function() {
|
|
572
|
+
this.expect(TokenType.LPAREN);
|
|
573
|
+
|
|
574
|
+
let priority = 'polite';
|
|
575
|
+
if (this.is(TokenType.IDENT)) {
|
|
576
|
+
priority = this.advance().value;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
this.expect(TokenType.RPAREN);
|
|
580
|
+
|
|
581
|
+
return new ASTNode(NodeType.LiveDirective, { priority });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Parse @focusTrap directive - traps focus within element
|
|
586
|
+
* @focusTrap or @focusTrap(autoFocus=true)
|
|
587
|
+
*/
|
|
588
|
+
Parser.prototype.parseFocusTrapDirective = function() {
|
|
589
|
+
const options = {};
|
|
590
|
+
|
|
591
|
+
if (this.is(TokenType.LPAREN)) {
|
|
592
|
+
this.advance();
|
|
593
|
+
|
|
594
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
595
|
+
const key = this.expect(TokenType.IDENT).value;
|
|
596
|
+
|
|
597
|
+
if (this.is(TokenType.EQ)) {
|
|
598
|
+
this.advance();
|
|
599
|
+
if (this.is(TokenType.TRUE)) {
|
|
600
|
+
options[key] = true;
|
|
601
|
+
this.advance();
|
|
602
|
+
} else if (this.is(TokenType.FALSE)) {
|
|
603
|
+
options[key] = false;
|
|
604
|
+
this.advance();
|
|
605
|
+
} else if (this.is(TokenType.STRING)) {
|
|
606
|
+
options[key] = this.advance().value;
|
|
607
|
+
} else {
|
|
608
|
+
options[key] = this.parseExpression();
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
options[key] = true;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (this.is(TokenType.COMMA)) {
|
|
615
|
+
this.advance();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
this.expect(TokenType.RPAREN);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return new ASTNode(NodeType.FocusTrapDirective, { options });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Parse @srOnly directive - visually hidden but accessible text
|
|
627
|
+
*/
|
|
628
|
+
Parser.prototype.parseSrOnlyDirective = function() {
|
|
629
|
+
return new ASTNode(NodeType.A11yDirective, {
|
|
630
|
+
attrs: { srOnly: true }
|
|
631
|
+
});
|
|
632
|
+
};
|