ripple 0.2.48 → 0.2.49

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.
@@ -7,786 +7,809 @@ import { regex_newline_characters } from '../../../utils/patterns.js';
7
7
  const parser = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }), RipplePlugin());
8
8
 
9
9
  function convert_from_jsx(node) {
10
- if (node.type === 'JSXIdentifier') {
11
- node.type = 'Identifier';
12
- } else if (node.type === 'JSXMemberExpression') {
13
- node.type = 'MemberExpression';
14
- node.object = convert_from_jsx(node.object);
15
- node.property = convert_from_jsx(node.property);
16
- }
17
- return node;
10
+ if (node.type === 'JSXIdentifier') {
11
+ node.type = 'Identifier';
12
+ } else if (node.type === 'JSXMemberExpression') {
13
+ node.type = 'MemberExpression';
14
+ node.object = convert_from_jsx(node.object);
15
+ node.property = convert_from_jsx(node.property);
16
+ }
17
+ return node;
18
18
  }
19
19
 
20
20
  function RipplePlugin(config) {
21
- return (Parser) => {
22
- const original = acorn.Parser.prototype;
23
- const tt = Parser.tokTypes || acorn.tokTypes;
24
- const tc = Parser.tokContexts || acorn.tokContexts;
25
-
26
- class RippleParser extends Parser {
27
- #path = [];
28
- skip_decorator = false;
29
-
30
- // Helper method to get the element name from a JSX identifier or member expression
31
- getElementName(node) {
32
- if (!node) return null;
33
- if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
34
- return node.name;
35
- } else if (node.type === 'MemberExpression' || node.type === 'JSXMemberExpression') {
36
- // For components like <Foo.Bar>, return "Foo.Bar"
37
- return this.getElementName(node.object) + '.' + this.getElementName(node.property);
38
- }
39
- return null;
40
- }
41
-
42
- // Override getTokenFromCode to handle @ as an identifier prefix
43
- getTokenFromCode(code) {
44
- if (code === 64) { // '@' character
45
- // Look ahead to see if this is followed by a valid identifier character
46
- if (this.pos + 1 < this.input.length) {
47
- const nextChar = this.input.charCodeAt(this.pos + 1);
48
- // Check if the next character can start an identifier
49
- if ((nextChar >= 65 && nextChar <= 90) || // A-Z
50
- (nextChar >= 97 && nextChar <= 122) || // a-z
51
- nextChar === 95 || nextChar === 36) { // _ or $
52
-
53
- // Check if we're in an expression context
54
- // In JSX expressions, inside parentheses, assignments, etc.
55
- // we want to treat @ as an identifier prefix rather than decorator
56
- const currentType = this.type;
57
- const inExpression = this.exprAllowed ||
58
- currentType === tt.braceL || // Inside { }
59
- currentType === tt.parenL || // Inside ( )
60
- currentType === tt.eq || // After =
61
- currentType === tt.comma || // After ,
62
- currentType === tt.colon || // After :
63
- currentType === tt.question || // After ?
64
- currentType === tt.logicalOR || // After ||
65
- currentType === tt.logicalAND || // After &&
66
- currentType === tt.dot || // After . (for member expressions like obj.@prop)
67
- currentType === tt.questionDot; // After ?. (for optional chaining like obj?.@prop)
68
-
69
- if (inExpression) {
70
- return this.readAtIdentifier();
71
- }
72
- }
73
- }
74
- }
75
- return super.getTokenFromCode(code);
76
- }
77
-
78
- // Read an @ prefixed identifier
79
- readAtIdentifier() {
80
- const start = this.pos;
81
- this.pos++; // skip '@'
82
-
83
- // Read the identifier part manually
84
- let word = '';
85
- while (this.pos < this.input.length) {
86
- const ch = this.input.charCodeAt(this.pos);
87
- if ((ch >= 65 && ch <= 90) || // A-Z
88
- (ch >= 97 && ch <= 122) || // a-z
89
- (ch >= 48 && ch <= 57) || // 0-9
90
- ch === 95 || ch === 36) { // _ or $
91
- word += this.input[this.pos++];
92
- } else {
93
- break;
94
- }
95
- }
96
-
97
- if (word === '') {
98
- this.raise(start, 'Invalid @ identifier');
99
- }
100
-
101
- // Return the full identifier including @
102
- return this.finishToken(tt.name, '@' + word);
103
- }
104
-
105
- // Override parseIdent to mark @ identifiers as tracked
106
- parseIdent(liberal) {
107
- const node = super.parseIdent(liberal);
108
- if (node.name && node.name.startsWith('@')) {
109
- node.name = node.name.slice(1); // Remove the '@' for internal use
110
- node.tracked = true;
111
- node.start++;
112
- const prev_pos = this.pos;
113
- this.pos = node.start;
114
- node.loc.start = this.curPosition();
115
- this.pos = prev_pos;
116
- }
117
- return node;
118
- }
119
-
120
- parseExportDefaultDeclaration() {
121
- // Check if this is "export default component"
122
- if (this.value === 'component') {
123
- const node = this.startNode();
124
- node.type = 'Component';
125
- node.css = null;
126
- node.default = true;
127
- this.next();
128
- this.enterScope(0);
129
-
130
- node.id = this.type.label === 'name' ? this.parseIdent() : null;
131
-
132
- this.parseFunctionParams(node);
133
- this.eat(tt.braceL);
134
- node.body = [];
135
- this.#path.push(node);
136
-
137
- this.parseTemplateBody(node.body);
138
-
139
- this.#path.pop();
140
- this.exitScope();
141
-
142
- this.next();
143
- this.finishNode(node, 'Component');
144
- this.awaitPos = 0;
145
-
146
- return node;
147
- }
148
-
149
- return super.parseExportDefaultDeclaration();
150
- }
151
-
152
- shouldParseExportStatement() {
153
- if (super.shouldParseExportStatement()) {
154
- return true;
155
- }
156
- if (this.value === 'component') {
157
- return true;
158
- }
159
- return this.type.keyword === 'var';
160
- }
161
-
162
- jsx_parseExpressionContainer() {
163
- let node = this.startNode();
164
- this.next();
165
-
166
- node.expression =
167
- this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
168
- this.expect(tt.braceR);
169
- return this.finishNode(node, 'JSXExpressionContainer');
170
- }
171
-
172
- jsx_parseTupleContainer() {
173
- var t = this.startNode();
174
- return (
175
- this.next(),
176
- (t.expression =
177
- this.type === tt.bracketR ? this.jsx_parseEmptyExpression() : this.parseExpression()),
178
- this.expect(tt.bracketR),
179
- this.finishNode(t, 'JSXExpressionContainer')
180
- );
181
- }
182
-
183
- jsx_parseAttribute() {
184
- let node = this.startNode();
185
- const lookahead = this.lookahead();
186
-
187
- if (lookahead.type?.label === ':') {
188
- let id = this.startNode();
189
- id.name = this.value;
190
- node.name = id;
191
- this.next();
192
- this.finishNode(id, 'Identifier');
193
-
194
- if (this.lookahead().value !== '=') {
195
- this.unexpected();
196
- }
197
- this.next();
198
- if (this.lookahead().type !== tt.braceL) {
199
- this.unexpected();
200
- }
201
- this.next();
202
- const value = this.jsx_parseAttributeValue();
203
- const expression = value.expression;
204
- node.get = null;
205
- node.set = null;
206
-
207
- if (expression.type == 'SequenceExpression') {
208
- node.get = expression.expressions[0];
209
- node.set = expression.expressions[1];
210
- if (expression.expressions.length > 2) {
211
- this.unexpected();
212
- }
213
- } else {
214
- node.get = expression;
215
- }
216
-
217
- return this.finishNode(node, 'AccessorAttribute');
218
- }
219
-
220
- if (this.eat(tt.braceL)) {
221
- if (this.value === 'ref') {
222
- this.next();
223
- if (this.type === tt.braceR) {
224
- this.raise(this.start, '"ref" is a Ripple keyword and must be used in the form {ref fn}');
225
- }
226
- node.argument = this.parseMaybeAssign();
227
- this.expect(tt.braceR);
228
- return this.finishNode(node, 'RefAttribute');
229
- } else if (this.type === tt.ellipsis) {
230
- this.expect(tt.ellipsis);
231
- node.argument = this.parseMaybeAssign();
232
- this.expect(tt.braceR);
233
- return this.finishNode(node, 'SpreadAttribute');
234
- } else if (this.lookahead().type === tt.ellipsis) {
235
- this.expect(tt.ellipsis);
236
- node.argument = this.parseMaybeAssign();
237
- this.expect(tt.braceR);
238
- return this.finishNode(node, 'SpreadAttribute');
239
- } else {
240
- const id = this.parseIdentNode();
241
- id.tracked = false;
242
- if (id.name.startsWith('@')) {
243
- id.tracked = true;
244
- id.name = id.name.slice(1);
245
- }
246
- this.finishNode(id, 'Identifier');
247
- node.name = id;
248
- node.value = id;
249
- this.next();
250
- this.expect(tt.braceR);
251
- return this.finishNode(node, 'Attribute');
252
- }
253
- }
254
- node.name = this.jsx_parseNamespacedName();
255
- node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null;
256
- return this.finishNode(node, 'JSXAttribute');
257
- }
258
-
259
- jsx_parseAttributeValue() {
260
- const tok = this.acornTypeScript.tokTypes;
261
-
262
- switch (this.type) {
263
- case tt.braceL:
264
- var t = this.jsx_parseExpressionContainer();
265
- return (
266
- 'JSXEmptyExpression' === t.expression.type &&
267
- this.raise(t.start, 'attributes must only be assigned a non-empty expression'),
268
- t
269
- );
270
- case tok.jsxTagStart:
271
- case tt.string:
272
- return this.parseExprAtom();
273
- default:
274
- this.raise(this.start, 'value should be either an expression or a quoted text');
275
- }
276
- }
277
-
278
-
279
- parseTryStatement(node) {
280
- this.next();
281
- node.block = this.parseBlock();
282
- node.handler = null;
283
- if (this.type === tt._catch) {
284
- var clause = this.startNode();
285
- this.next();
286
- if (this.eat(tt.parenL)) {
287
- clause.param = this.parseCatchClauseParam();
288
- } else {
289
- if (this.options.ecmaVersion < 10) {
290
- this.unexpected();
291
- }
292
- clause.param = null;
293
- this.enterScope(0);
294
- }
295
- clause.body = this.parseBlock(false);
296
- this.exitScope();
297
- node.handler = this.finishNode(clause, 'CatchClause');
298
- }
299
- node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null;
300
-
301
- if (this.value === 'async') {
302
- this.next();
303
- node.async = this.parseBlock();
304
- } else {
305
- node.async = null;
306
- }
307
-
308
- if (!node.handler && !node.finalizer && !node.async) {
309
- this.raise(node.start, 'Missing catch or finally clause');
310
- }
311
- return this.finishNode(node, 'TryStatement');
312
- }
313
- jsx_readToken() {
314
- let out = '',
315
- chunkStart = this.pos;
316
- const tok = this.acornTypeScript.tokTypes;
317
-
318
- for (;;) {
319
- if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
320
- let ch = this.input.charCodeAt(this.pos);
321
-
322
- switch (ch) {
323
- case 60: // '<'
324
- case 123: // '{'
325
- if (ch === 60 && this.exprAllowed) {
326
- ++this.pos;
327
- return this.finishToken(tok.jsxTagStart);
328
- }
329
- if (ch === 123 && this.exprAllowed) {
330
- return this.getTokenFromCode(ch);
331
- }
332
- throw new Error('TODO: Invalid syntax');
333
-
334
- case 47: // '/'
335
- // Check if this is a comment (// or /*)
336
- if (this.input.charCodeAt(this.pos + 1) === 47) {
337
- // '//'
338
- // Line comment - handle it properly
339
- const commentStart = this.pos;
340
- const startLoc = this.curPosition();
341
- this.pos += 2;
342
-
343
- let commentText = '';
344
- while (this.pos < this.input.length) {
345
- const nextCh = this.input.charCodeAt(this.pos);
346
- if (acorn.isNewLine(nextCh)) break;
347
- commentText += this.input[this.pos];
348
- this.pos++;
349
- }
350
-
351
- const commentEnd = this.pos;
352
- const endLoc = this.curPosition();
353
-
354
- // Call onComment if it exists
355
- if (this.options.onComment) {
356
- this.options.onComment(
357
- false,
358
- commentText,
359
- commentStart,
360
- commentEnd,
361
- startLoc,
362
- endLoc,
363
- );
364
- }
365
-
366
- // Continue processing from current position
367
- break;
368
- } else if (this.input.charCodeAt(this.pos + 1) === 42) {
369
- // '/*'
370
- // Block comment - handle it properly
371
- const commentStart = this.pos;
372
- const startLoc = this.curPosition();
373
- this.pos += 2;
374
-
375
- let commentText = '';
376
- while (this.pos < this.input.length - 1) {
377
- if (
378
- this.input.charCodeAt(this.pos) === 42 &&
379
- this.input.charCodeAt(this.pos + 1) === 47
380
- ) {
381
- this.pos += 2;
382
- break;
383
- }
384
- commentText += this.input[this.pos];
385
- this.pos++;
386
- }
387
-
388
- const commentEnd = this.pos;
389
- const endLoc = this.curPosition();
390
-
391
- // Call onComment if it exists
392
- if (this.options.onComment) {
393
- this.options.onComment(
394
- true,
395
- commentText,
396
- commentStart,
397
- commentEnd,
398
- startLoc,
399
- endLoc,
400
- );
401
- }
402
-
403
- // Continue processing from current position
404
- break;
405
- }
406
- // If not a comment, fall through to default case
407
- this.context.push(tc.b_stat);
408
- this.exprAllowed = true;
409
- return original.readToken.call(this, ch);
410
-
411
- case 38: // '&'
412
- out += this.input.slice(chunkStart, this.pos);
413
- out += this.jsx_readEntity();
414
- chunkStart = this.pos;
415
- break;
416
-
417
- case 62: // '>'
418
- case 125: {
419
- // '}'
420
- if (
421
- ch === 125 &&
422
- (this.#path.length === 0 || this.#path.at(-1)?.type === 'Component')
423
- ) {
424
- return original.readToken.call(this, ch);
425
- }
426
- this.raise(
427
- this.pos,
428
- 'Unexpected token `' +
429
- this.input[this.pos] +
430
- '`. Did you mean `' +
431
- (ch === 62 ? '&gt;' : '&rbrace;') +
432
- '` or ' +
433
- '`{"' +
434
- this.input[this.pos] +
435
- '"}' +
436
- '`?',
437
- );
438
- }
439
-
440
- default:
441
- if (acorn.isNewLine(ch)) {
442
- out += this.input.slice(chunkStart, this.pos);
443
- out += this.jsx_readNewLine(true);
444
- chunkStart = this.pos;
445
- } else if (ch === 32 || ch === 9) {
446
- ++this.pos;
447
- } else {
448
- this.context.push(tc.b_stat);
449
- this.exprAllowed = true;
450
- return original.readToken.call(this, ch);
451
- }
452
- }
453
- }
454
- }
455
-
456
- parseElement() {
457
- const tok = this.acornTypeScript.tokTypes;
458
- // Adjust the start so we capture the `<` as part of the element
459
- const prev_pos = this.pos;
460
- this.pos = this.start - 1;
461
- const position = this.curPosition();
462
- this.pos = prev_pos;
463
-
464
- const element = this.startNode();
465
- element.start = position.index;
466
- element.loc.start = position;
467
- element.type = 'Element';
468
- this.#path.push(element);
469
- element.children = [];
470
- const open = this.jsx_parseOpeningElementAt();
471
- for (const attr of open.attributes) {
472
- if (attr.type === 'JSXAttribute') {
473
- attr.type = 'Attribute';
474
- if (attr.name.type === 'JSXIdentifier') {
475
- attr.name.type = 'Identifier';
476
- }
477
- if (attr.value.type === 'JSXExpressionContainer') {
478
- attr.value = attr.value.expression;
479
- }
480
- }
481
- }
482
- if (open.name.type === 'JSXIdentifier') {
483
- open.name.type = 'Identifier';
484
- }
485
-
486
- element.id = convert_from_jsx(open.name);
487
- element.attributes = open.attributes;
488
- element.selfClosing = open.selfClosing;
489
- element.metadata = {};
490
-
491
- if (element.selfClosing) {
492
- this.#path.pop();
493
-
494
- if (this.type.label === '</>/<=/>=') {
495
- this.pos--;
496
- this.next();
497
- }
498
- } else {
499
- if (open.name.name === 'style') {
500
- // jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
501
- // So backtrack to the end of the <style> tag to make sure everything is included
502
- const start = open.end;
503
- const input = this.input.slice(start);
504
- const end = input.indexOf('</style>');
505
- const content = input.slice(0, end);
506
-
507
- const component = this.#path.findLast((n) => n.type === 'Component');
508
- if (component.css !== null) {
509
- throw new Error('Components can only have one style tag');
510
- }
511
- component.css = parse_style(content);
512
-
513
- const newLines = content.match(regex_newline_characters)?.length;
514
- if (newLines) {
515
- this.curLine = open.loc.end.line + newLines;
516
- this.lineStart = start + content.lastIndexOf('\n') + 1;
517
- }
518
- this.pos = start + content.length + 1;
519
-
520
- this.type = tok.jsxTagStart;
521
- this.next();
522
- if (this.value === '/') {
523
- this.next();
524
- this.jsx_parseElementName();
525
- this.exprAllowed = true;
526
- this.#path.pop();
527
- this.next();
528
- }
529
- // This node is used for Prettier, we don't actually need
530
- // the node for Ripple's transform process
531
- element.children = [component.css];
532
- // Ensure we escape JSX <tag></tag> context
533
- const tokContexts = this.acornTypeScript.tokContexts;
534
- const curContext = this.curContext();
535
-
536
- if (curContext === tokContexts.tc_expr) {
537
- this.context.pop();
538
- }
539
-
540
- this.finishNode(element, 'Element');
541
- return element;
542
- } else {
543
- this.enterScope(0);
544
- this.parseTemplateBody(element.children);
545
- this.exitScope();
546
-
547
- // Check if this element was properly closed
548
- // If we reach here and this element is still in the path, it means it was never closed
549
- if (this.#path[this.#path.length - 1] === element) {
550
- const tagName = this.getElementName(element.id);
551
- this.raise(this.start, `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`);
552
- }
553
- }
554
- // Ensure we escape JSX <tag></tag> context
555
- const tokContexts = this.acornTypeScript.tokContexts;
556
- const curContext = this.curContext();
557
-
558
- if (curContext === tokContexts.tc_expr) {
559
- this.context.pop();
560
- }
561
- }
562
-
563
- this.finishNode(element, 'Element');
564
- return element;
565
- }
566
-
567
- parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained, forInit) {
568
- if (this.value === '<' && this.#path.findLast((n) => n.type === 'Component')) {
569
- // Check if this looks like JSX by looking ahead
570
- const ahead = this.lookahead();
571
- const curContext = this.curContext();
572
- if (
573
- curContext.token !== '(' &&
574
- (ahead.type.label === 'name' || ahead.value === '/' || ahead.value === '>')
575
- ) {
576
- // This is JSX, rewind to the end of the object expression
577
- // and let ASI handle the semicolon insertion naturally
578
- this.pos = base.end;
579
- this.type = tt.braceR;
580
- this.value = '}';
581
- this.start = base.end - 1;
582
- this.end = base.end;
583
- const position = this.curPosition();
584
- this.startLoc = position;
585
- this.endLoc = position;
586
- // Avoid triggering onComment handlers, as they will have
587
- // already been triggered when parsing the subscript before
588
- const onComment = this.options.onComment;
589
- this.options.onComment = () => {};
590
- this.next();
591
- this.options.onComment = onComment;
592
-
593
- return base;
594
- }
595
- }
596
- return super.parseSubscript(
597
- base,
598
- startPos,
599
- startLoc,
600
- noCalls,
601
- maybeAsyncArrow,
602
- optionalChained,
603
- forInit,
604
- );
605
- }
606
-
607
- parseTemplateBody(body) {
608
- var inside_func =
609
- this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
610
-
611
- if (!inside_func) {
612
- if (this.type.label === 'return') {
613
- throw new Error('`return` statements are not allowed in components');
614
- }
615
- if (this.type.label === 'continue') {
616
- throw new Error('`continue` statements are not allowed in components');
617
- }
618
- if (this.type.label === 'break') {
619
- throw new Error('`break` statements are not allowed in components');
620
- }
621
- }
622
-
623
- if (this.type.label === '{') {
624
- const node = this.jsx_parseExpressionContainer();
625
- node.type = 'Text';
626
- body.push(node);
627
- } else if (this.type.label === '}') {
628
- return;
629
- } else if (this.type.label === 'jsxTagStart') {
630
- this.next();
631
- if (this.value === '/') {
632
- this.next();
633
- const closingTag = this.jsx_parseElementName();
634
- this.exprAllowed = true;
635
-
636
- // Validate that the closing tag matches the opening tag
637
- const currentElement = this.#path[this.#path.length - 1];
638
- if (!currentElement || currentElement.type !== 'Element') {
639
- this.raise(this.start, 'Unexpected closing tag');
640
- }
641
-
642
- const openingTagName = this.getElementName(currentElement.id);
643
- const closingTagName = this.getElementName(closingTag);
644
-
645
- if (openingTagName !== closingTagName) {
646
- this.raise(this.start, `Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`);
647
- }
648
-
649
- this.#path.pop();
650
- this.next();
651
- return;
652
- }
653
- const node = this.parseElement();
654
- if (node !== null) {
655
- body.push(node);
656
- }
657
- } else {
658
- const node = this.parseStatement(null);
659
- body.push(node);
660
- }
661
- this.parseTemplateBody(body);
662
- }
663
-
664
- parseStatement(context, topLevel, exports) {
665
- const tok = this.acornTypeScript.tokContexts;
666
-
667
- if (
668
- context !== 'for' &&
669
- context !== 'if' &&
670
- this.context.at(-1) === tc.b_stat &&
671
- this.type === tt.braceL &&
672
- this.context.some((c) => c === tok.tc_expr)
673
- ) {
674
- this.next();
675
- const node = this.jsx_parseExpressionContainer();
676
- node.type = 'Text';
677
- this.next();
678
- this.context.pop();
679
- this.context.pop();
680
- return node;
681
- }
682
-
683
- if (this.value === 'component') {
684
- const node = this.startNode();
685
- node.type = 'Component';
686
- node.css = null;
687
- this.next();
688
- this.enterScope(0);
689
- node.id = this.parseIdent();
690
- this.parseFunctionParams(node);
691
- this.eat(tt.braceL);
692
- node.body = [];
693
- this.#path.push(node);
694
-
695
- this.parseTemplateBody(node.body);
696
-
697
- this.#path.pop();
698
- this.exitScope();
699
-
700
- this.next();
701
- this.finishNode(node, 'Component');
702
- this.awaitPos = 0;
703
-
704
- return node;
705
- }
706
-
707
- if (this.type.label === '@') {
708
- // Try to parse as an expression statement first using tryParse
709
- // This allows us to handle Ripple @ syntax like @count++ without
710
- // interfering with legitimate decorator syntax
711
- this.skip_decorator = true;
712
- const expressionResult = this.tryParse(() => {
713
- const node = this.startNode();
714
- this.next();
715
- // Force expression context to ensure @ is tokenized correctly
716
- const oldExprAllowed = this.exprAllowed;
717
- this.exprAllowed = true;
718
- node.expression = this.parseExpression();
719
-
720
- if (node.expression.type === 'UpdateExpression') {
721
- let object = node.expression.argument;
722
- while (object.type === 'MemberExpression') {
723
- object = object.object;
724
- }
725
- if (object.type === 'Identifier') {
726
- object.tracked = true;
727
- }
728
- } else if (node.expression.type === 'AssignmentExpression') {
729
- let object = node.expression.left;
730
- while (object.type === 'MemberExpression') {
731
- object = object.object;
732
- }
733
- if (object.type === 'Identifier') {
734
- object.tracked = true;
735
- }
736
- } else if (node.expression.type === 'Identifier') {
737
- node.expression.tracked = true;
738
- } else {
739
- // TODO?
740
- }
741
-
742
- this.exprAllowed = oldExprAllowed;
743
- return this.finishNode(node, 'ExpressionStatement');
744
- });
745
- this.skip_decorator = false;
746
-
747
- // If parsing as expression statement succeeded, use that result
748
- if (expressionResult.node) {
749
- return expressionResult.node;
750
- }
751
-
752
- // Otherwise, fall back to default decorator parsing
753
- }
754
-
755
- return super.parseStatement(context, topLevel, exports);
756
- }
757
-
758
- parseBlock(createNewLexicalScope, node, exitStrict) {
759
- const parent = this.#path.at(-1);
760
-
761
- if (parent?.type === 'Component' || parent?.type === 'Element') {
762
- if (createNewLexicalScope === void 0) createNewLexicalScope = true;
763
- if (node === void 0) node = this.startNode();
764
-
765
- node.body = [];
766
- this.expect(tt.braceL);
767
- if (createNewLexicalScope) {
768
- this.enterScope(0);
769
- }
770
- this.parseTemplateBody(node.body);
771
-
772
- if (exitStrict) {
773
- this.strict = false;
774
- }
775
- this.exprAllowed = true;
776
-
777
- this.next();
778
- if (createNewLexicalScope) {
779
- this.exitScope();
780
- }
781
- return this.finishNode(node, 'BlockStatement');
782
- }
783
-
784
- return super.parseBlock(createNewLexicalScope, node, exitStrict);
785
- }
786
- }
787
-
788
- return RippleParser;
789
- };
21
+ return (Parser) => {
22
+ const original = acorn.Parser.prototype;
23
+ const tt = Parser.tokTypes || acorn.tokTypes;
24
+ const tc = Parser.tokContexts || acorn.tokContexts;
25
+
26
+ class RippleParser extends Parser {
27
+ #path = [];
28
+ skip_decorator = false;
29
+
30
+ // Helper method to get the element name from a JSX identifier or member expression
31
+ getElementName(node) {
32
+ if (!node) return null;
33
+ if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
34
+ return node.name;
35
+ } else if (node.type === 'MemberExpression' || node.type === 'JSXMemberExpression') {
36
+ // For components like <Foo.Bar>, return "Foo.Bar"
37
+ return this.getElementName(node.object) + '.' + this.getElementName(node.property);
38
+ }
39
+ return null;
40
+ }
41
+
42
+ // Override getTokenFromCode to handle @ as an identifier prefix
43
+ getTokenFromCode(code) {
44
+ if (code === 64) {
45
+ // '@' character
46
+ // Look ahead to see if this is followed by a valid identifier character
47
+ if (this.pos + 1 < this.input.length) {
48
+ const nextChar = this.input.charCodeAt(this.pos + 1);
49
+ // Check if the next character can start an identifier
50
+ if (
51
+ (nextChar >= 65 && nextChar <= 90) || // A-Z
52
+ (nextChar >= 97 && nextChar <= 122) || // a-z
53
+ nextChar === 95 ||
54
+ nextChar === 36
55
+ ) {
56
+ // _ or $
57
+
58
+ // Check if we're in an expression context
59
+ // In JSX expressions, inside parentheses, assignments, etc.
60
+ // we want to treat @ as an identifier prefix rather than decorator
61
+ const currentType = this.type;
62
+ const inExpression =
63
+ this.exprAllowed ||
64
+ currentType === tt.braceL || // Inside { }
65
+ currentType === tt.parenL || // Inside ( )
66
+ currentType === tt.eq || // After =
67
+ currentType === tt.comma || // After ,
68
+ currentType === tt.colon || // After :
69
+ currentType === tt.question || // After ?
70
+ currentType === tt.logicalOR || // After ||
71
+ currentType === tt.logicalAND || // After &&
72
+ currentType === tt.dot || // After . (for member expressions like obj.@prop)
73
+ currentType === tt.questionDot; // After ?. (for optional chaining like obj?.@prop)
74
+
75
+ if (inExpression) {
76
+ return this.readAtIdentifier();
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return super.getTokenFromCode(code);
82
+ }
83
+
84
+ // Read an @ prefixed identifier
85
+ readAtIdentifier() {
86
+ const start = this.pos;
87
+ this.pos++; // skip '@'
88
+
89
+ // Read the identifier part manually
90
+ let word = '';
91
+ while (this.pos < this.input.length) {
92
+ const ch = this.input.charCodeAt(this.pos);
93
+ if (
94
+ (ch >= 65 && ch <= 90) || // A-Z
95
+ (ch >= 97 && ch <= 122) || // a-z
96
+ (ch >= 48 && ch <= 57) || // 0-9
97
+ ch === 95 ||
98
+ ch === 36
99
+ ) {
100
+ // _ or $
101
+ word += this.input[this.pos++];
102
+ } else {
103
+ break;
104
+ }
105
+ }
106
+
107
+ if (word === '') {
108
+ this.raise(start, 'Invalid @ identifier');
109
+ }
110
+
111
+ // Return the full identifier including @
112
+ return this.finishToken(tt.name, '@' + word);
113
+ }
114
+
115
+ // Override parseIdent to mark @ identifiers as tracked
116
+ parseIdent(liberal) {
117
+ const node = super.parseIdent(liberal);
118
+ if (node.name && node.name.startsWith('@')) {
119
+ node.name = node.name.slice(1); // Remove the '@' for internal use
120
+ node.tracked = true;
121
+ node.start++;
122
+ const prev_pos = this.pos;
123
+ this.pos = node.start;
124
+ node.loc.start = this.curPosition();
125
+ this.pos = prev_pos;
126
+ }
127
+ return node;
128
+ }
129
+
130
+ parseExportDefaultDeclaration() {
131
+ // Check if this is "export default component"
132
+ if (this.value === 'component') {
133
+ const node = this.startNode();
134
+ node.type = 'Component';
135
+ node.css = null;
136
+ node.default = true;
137
+ this.next();
138
+ this.enterScope(0);
139
+
140
+ node.id = this.type.label === 'name' ? this.parseIdent() : null;
141
+
142
+ this.parseFunctionParams(node);
143
+ this.eat(tt.braceL);
144
+ node.body = [];
145
+ this.#path.push(node);
146
+
147
+ this.parseTemplateBody(node.body);
148
+
149
+ this.#path.pop();
150
+ this.exitScope();
151
+
152
+ this.next();
153
+ this.finishNode(node, 'Component');
154
+ this.awaitPos = 0;
155
+
156
+ return node;
157
+ }
158
+
159
+ return super.parseExportDefaultDeclaration();
160
+ }
161
+
162
+ shouldParseExportStatement() {
163
+ if (super.shouldParseExportStatement()) {
164
+ return true;
165
+ }
166
+ if (this.value === 'component') {
167
+ return true;
168
+ }
169
+ return this.type.keyword === 'var';
170
+ }
171
+
172
+ jsx_parseExpressionContainer() {
173
+ let node = this.startNode();
174
+ this.next();
175
+
176
+ node.expression =
177
+ this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
178
+ this.expect(tt.braceR);
179
+ return this.finishNode(node, 'JSXExpressionContainer');
180
+ }
181
+
182
+ jsx_parseTupleContainer() {
183
+ var t = this.startNode();
184
+ return (
185
+ this.next(),
186
+ (t.expression =
187
+ this.type === tt.bracketR ? this.jsx_parseEmptyExpression() : this.parseExpression()),
188
+ this.expect(tt.bracketR),
189
+ this.finishNode(t, 'JSXExpressionContainer')
190
+ );
191
+ }
192
+
193
+ jsx_parseAttribute() {
194
+ let node = this.startNode();
195
+ const lookahead = this.lookahead();
196
+
197
+ if (lookahead.type?.label === ':') {
198
+ let id = this.startNode();
199
+ id.name = this.value;
200
+ node.name = id;
201
+ this.next();
202
+ this.finishNode(id, 'Identifier');
203
+
204
+ if (this.lookahead().value !== '=') {
205
+ this.unexpected();
206
+ }
207
+ this.next();
208
+ if (this.lookahead().type !== tt.braceL) {
209
+ this.unexpected();
210
+ }
211
+ this.next();
212
+ const value = this.jsx_parseAttributeValue();
213
+ const expression = value.expression;
214
+ node.get = null;
215
+ node.set = null;
216
+
217
+ if (expression.type == 'SequenceExpression') {
218
+ node.get = expression.expressions[0];
219
+ node.set = expression.expressions[1];
220
+ if (expression.expressions.length > 2) {
221
+ this.unexpected();
222
+ }
223
+ } else {
224
+ node.get = expression;
225
+ }
226
+
227
+ return this.finishNode(node, 'AccessorAttribute');
228
+ }
229
+
230
+ if (this.eat(tt.braceL)) {
231
+ if (this.value === 'ref') {
232
+ this.next();
233
+ if (this.type === tt.braceR) {
234
+ this.raise(
235
+ this.start,
236
+ '"ref" is a Ripple keyword and must be used in the form {ref fn}',
237
+ );
238
+ }
239
+ node.argument = this.parseMaybeAssign();
240
+ this.expect(tt.braceR);
241
+ return this.finishNode(node, 'RefAttribute');
242
+ } else if (this.type === tt.ellipsis) {
243
+ this.expect(tt.ellipsis);
244
+ node.argument = this.parseMaybeAssign();
245
+ this.expect(tt.braceR);
246
+ return this.finishNode(node, 'SpreadAttribute');
247
+ } else if (this.lookahead().type === tt.ellipsis) {
248
+ this.expect(tt.ellipsis);
249
+ node.argument = this.parseMaybeAssign();
250
+ this.expect(tt.braceR);
251
+ return this.finishNode(node, 'SpreadAttribute');
252
+ } else {
253
+ const id = this.parseIdentNode();
254
+ id.tracked = false;
255
+ if (id.name.startsWith('@')) {
256
+ id.tracked = true;
257
+ id.name = id.name.slice(1);
258
+ }
259
+ this.finishNode(id, 'Identifier');
260
+ node.name = id;
261
+ node.value = id;
262
+ this.next();
263
+ this.expect(tt.braceR);
264
+ return this.finishNode(node, 'Attribute');
265
+ }
266
+ }
267
+ node.name = this.jsx_parseNamespacedName();
268
+ node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null;
269
+ return this.finishNode(node, 'JSXAttribute');
270
+ }
271
+
272
+ jsx_parseAttributeValue() {
273
+ const tok = this.acornTypeScript.tokTypes;
274
+
275
+ switch (this.type) {
276
+ case tt.braceL:
277
+ var t = this.jsx_parseExpressionContainer();
278
+ return (
279
+ 'JSXEmptyExpression' === t.expression.type &&
280
+ this.raise(t.start, 'attributes must only be assigned a non-empty expression'),
281
+ t
282
+ );
283
+ case tok.jsxTagStart:
284
+ case tt.string:
285
+ return this.parseExprAtom();
286
+ default:
287
+ this.raise(this.start, 'value should be either an expression or a quoted text');
288
+ }
289
+ }
290
+
291
+ parseTryStatement(node) {
292
+ this.next();
293
+ node.block = this.parseBlock();
294
+ node.handler = null;
295
+ if (this.type === tt._catch) {
296
+ var clause = this.startNode();
297
+ this.next();
298
+ if (this.eat(tt.parenL)) {
299
+ clause.param = this.parseCatchClauseParam();
300
+ } else {
301
+ if (this.options.ecmaVersion < 10) {
302
+ this.unexpected();
303
+ }
304
+ clause.param = null;
305
+ this.enterScope(0);
306
+ }
307
+ clause.body = this.parseBlock(false);
308
+ this.exitScope();
309
+ node.handler = this.finishNode(clause, 'CatchClause');
310
+ }
311
+ node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null;
312
+
313
+ if (this.value === 'async') {
314
+ this.next();
315
+ node.async = this.parseBlock();
316
+ } else {
317
+ node.async = null;
318
+ }
319
+
320
+ if (!node.handler && !node.finalizer && !node.async) {
321
+ this.raise(node.start, 'Missing catch or finally clause');
322
+ }
323
+ return this.finishNode(node, 'TryStatement');
324
+ }
325
+ jsx_readToken() {
326
+ let out = '',
327
+ chunkStart = this.pos;
328
+ const tok = this.acornTypeScript.tokTypes;
329
+
330
+ for (;;) {
331
+ if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
332
+ let ch = this.input.charCodeAt(this.pos);
333
+
334
+ switch (ch) {
335
+ case 60: // '<'
336
+ case 123: // '{'
337
+ if (ch === 60 && this.exprAllowed) {
338
+ ++this.pos;
339
+ return this.finishToken(tok.jsxTagStart);
340
+ }
341
+ if (ch === 123 && this.exprAllowed) {
342
+ return this.getTokenFromCode(ch);
343
+ }
344
+ throw new Error('TODO: Invalid syntax');
345
+
346
+ case 47: // '/'
347
+ // Check if this is a comment (// or /*)
348
+ if (this.input.charCodeAt(this.pos + 1) === 47) {
349
+ // '//'
350
+ // Line comment - handle it properly
351
+ const commentStart = this.pos;
352
+ const startLoc = this.curPosition();
353
+ this.pos += 2;
354
+
355
+ let commentText = '';
356
+ while (this.pos < this.input.length) {
357
+ const nextCh = this.input.charCodeAt(this.pos);
358
+ if (acorn.isNewLine(nextCh)) break;
359
+ commentText += this.input[this.pos];
360
+ this.pos++;
361
+ }
362
+
363
+ const commentEnd = this.pos;
364
+ const endLoc = this.curPosition();
365
+
366
+ // Call onComment if it exists
367
+ if (this.options.onComment) {
368
+ this.options.onComment(
369
+ false,
370
+ commentText,
371
+ commentStart,
372
+ commentEnd,
373
+ startLoc,
374
+ endLoc,
375
+ );
376
+ }
377
+
378
+ // Continue processing from current position
379
+ break;
380
+ } else if (this.input.charCodeAt(this.pos + 1) === 42) {
381
+ // '/*'
382
+ // Block comment - handle it properly
383
+ const commentStart = this.pos;
384
+ const startLoc = this.curPosition();
385
+ this.pos += 2;
386
+
387
+ let commentText = '';
388
+ while (this.pos < this.input.length - 1) {
389
+ if (
390
+ this.input.charCodeAt(this.pos) === 42 &&
391
+ this.input.charCodeAt(this.pos + 1) === 47
392
+ ) {
393
+ this.pos += 2;
394
+ break;
395
+ }
396
+ commentText += this.input[this.pos];
397
+ this.pos++;
398
+ }
399
+
400
+ const commentEnd = this.pos;
401
+ const endLoc = this.curPosition();
402
+
403
+ // Call onComment if it exists
404
+ if (this.options.onComment) {
405
+ this.options.onComment(
406
+ true,
407
+ commentText,
408
+ commentStart,
409
+ commentEnd,
410
+ startLoc,
411
+ endLoc,
412
+ );
413
+ }
414
+
415
+ // Continue processing from current position
416
+ break;
417
+ }
418
+ // If not a comment, fall through to default case
419
+ this.context.push(tc.b_stat);
420
+ this.exprAllowed = true;
421
+ return original.readToken.call(this, ch);
422
+
423
+ case 38: // '&'
424
+ out += this.input.slice(chunkStart, this.pos);
425
+ out += this.jsx_readEntity();
426
+ chunkStart = this.pos;
427
+ break;
428
+
429
+ case 62: // '>'
430
+ case 125: {
431
+ // '}'
432
+ if (
433
+ ch === 125 &&
434
+ (this.#path.length === 0 || this.#path.at(-1)?.type === 'Component')
435
+ ) {
436
+ return original.readToken.call(this, ch);
437
+ }
438
+ this.raise(
439
+ this.pos,
440
+ 'Unexpected token `' +
441
+ this.input[this.pos] +
442
+ '`. Did you mean `' +
443
+ (ch === 62 ? '&gt;' : '&rbrace;') +
444
+ '` or ' +
445
+ '`{"' +
446
+ this.input[this.pos] +
447
+ '"}' +
448
+ '`?',
449
+ );
450
+ }
451
+
452
+ default:
453
+ if (acorn.isNewLine(ch)) {
454
+ out += this.input.slice(chunkStart, this.pos);
455
+ out += this.jsx_readNewLine(true);
456
+ chunkStart = this.pos;
457
+ } else if (ch === 32 || ch === 9) {
458
+ ++this.pos;
459
+ } else {
460
+ this.context.push(tc.b_stat);
461
+ this.exprAllowed = true;
462
+ return original.readToken.call(this, ch);
463
+ }
464
+ }
465
+ }
466
+ }
467
+
468
+ parseElement() {
469
+ const tok = this.acornTypeScript.tokTypes;
470
+ // Adjust the start so we capture the `<` as part of the element
471
+ const prev_pos = this.pos;
472
+ this.pos = this.start - 1;
473
+ const position = this.curPosition();
474
+ this.pos = prev_pos;
475
+
476
+ const element = this.startNode();
477
+ element.start = position.index;
478
+ element.loc.start = position;
479
+ element.type = 'Element';
480
+ this.#path.push(element);
481
+ element.children = [];
482
+ const open = this.jsx_parseOpeningElementAt();
483
+ for (const attr of open.attributes) {
484
+ if (attr.type === 'JSXAttribute') {
485
+ attr.type = 'Attribute';
486
+ if (attr.name.type === 'JSXIdentifier') {
487
+ attr.name.type = 'Identifier';
488
+ }
489
+ if (attr.value.type === 'JSXExpressionContainer') {
490
+ attr.value = attr.value.expression;
491
+ }
492
+ }
493
+ }
494
+ if (open.name.type === 'JSXIdentifier') {
495
+ open.name.type = 'Identifier';
496
+ }
497
+
498
+ element.id = convert_from_jsx(open.name);
499
+ element.attributes = open.attributes;
500
+ element.selfClosing = open.selfClosing;
501
+ element.metadata = {};
502
+
503
+ if (element.selfClosing) {
504
+ this.#path.pop();
505
+
506
+ if (this.type.label === '</>/<=/>=') {
507
+ this.pos--;
508
+ this.next();
509
+ }
510
+ } else {
511
+ if (open.name.name === 'style') {
512
+ // jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
513
+ // So backtrack to the end of the <style> tag to make sure everything is included
514
+ const start = open.end;
515
+ const input = this.input.slice(start);
516
+ const end = input.indexOf('</style>');
517
+ const content = input.slice(0, end);
518
+
519
+ const component = this.#path.findLast((n) => n.type === 'Component');
520
+ if (component.css !== null) {
521
+ throw new Error('Components can only have one style tag');
522
+ }
523
+ component.css = parse_style(content);
524
+
525
+ const newLines = content.match(regex_newline_characters)?.length;
526
+ if (newLines) {
527
+ this.curLine = open.loc.end.line + newLines;
528
+ this.lineStart = start + content.lastIndexOf('\n') + 1;
529
+ }
530
+ this.pos = start + content.length + 1;
531
+
532
+ this.type = tok.jsxTagStart;
533
+ this.next();
534
+ if (this.value === '/') {
535
+ this.next();
536
+ this.jsx_parseElementName();
537
+ this.exprAllowed = true;
538
+ this.#path.pop();
539
+ this.next();
540
+ }
541
+ // This node is used for Prettier, we don't actually need
542
+ // the node for Ripple's transform process
543
+ element.children = [component.css];
544
+ // Ensure we escape JSX <tag></tag> context
545
+ const tokContexts = this.acornTypeScript.tokContexts;
546
+ const curContext = this.curContext();
547
+
548
+ if (curContext === tokContexts.tc_expr) {
549
+ this.context.pop();
550
+ }
551
+
552
+ this.finishNode(element, 'Element');
553
+ return element;
554
+ } else {
555
+ this.enterScope(0);
556
+ this.parseTemplateBody(element.children);
557
+ this.exitScope();
558
+
559
+ // Check if this element was properly closed
560
+ // If we reach here and this element is still in the path, it means it was never closed
561
+ if (this.#path[this.#path.length - 1] === element) {
562
+ const tagName = this.getElementName(element.id);
563
+ this.raise(
564
+ this.start,
565
+ `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
566
+ );
567
+ }
568
+ }
569
+ // Ensure we escape JSX <tag></tag> context
570
+ const tokContexts = this.acornTypeScript.tokContexts;
571
+ const curContext = this.curContext();
572
+
573
+ if (curContext === tokContexts.tc_expr) {
574
+ this.context.pop();
575
+ }
576
+ }
577
+
578
+ this.finishNode(element, 'Element');
579
+ return element;
580
+ }
581
+
582
+ parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained, forInit) {
583
+ const prev_char = this.input.at(this.pos - 2);
584
+
585
+ if (
586
+ this.value === '<' &&
587
+ (prev_char === ' ' || prev_char === '\t') &&
588
+ this.#path.findLast((n) => n.type === 'Component')
589
+ ) {
590
+ this.input.charCodeAt(this.pos);
591
+ // Check if this looks like JSX by looking ahead
592
+ const ahead = this.lookahead();
593
+ const curContext = this.curContext();
594
+ if (
595
+ curContext.token !== '(' &&
596
+ (ahead.type.label === 'name' || ahead.value === '/' || ahead.value === '>')
597
+ ) {
598
+ // This is JSX, rewind to the end of the object expression
599
+ // and let ASI handle the semicolon insertion naturally
600
+ this.pos = base.end;
601
+ this.type = tt.braceR;
602
+ this.value = '}';
603
+ this.start = base.end - 1;
604
+ this.end = base.end;
605
+ const position = this.curPosition();
606
+ this.startLoc = position;
607
+ this.endLoc = position;
608
+ // Avoid triggering onComment handlers, as they will have
609
+ // already been triggered when parsing the subscript before
610
+ const onComment = this.options.onComment;
611
+ this.options.onComment = () => {};
612
+ this.next();
613
+ this.options.onComment = onComment;
614
+
615
+ return base;
616
+ }
617
+ }
618
+ return super.parseSubscript(
619
+ base,
620
+ startPos,
621
+ startLoc,
622
+ noCalls,
623
+ maybeAsyncArrow,
624
+ optionalChained,
625
+ forInit,
626
+ );
627
+ }
628
+
629
+ parseTemplateBody(body) {
630
+ var inside_func =
631
+ this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
632
+
633
+ if (!inside_func) {
634
+ if (this.type.label === 'return') {
635
+ throw new Error('`return` statements are not allowed in components');
636
+ }
637
+ if (this.type.label === 'continue') {
638
+ throw new Error('`continue` statements are not allowed in components');
639
+ }
640
+ if (this.type.label === 'break') {
641
+ throw new Error('`break` statements are not allowed in components');
642
+ }
643
+ }
644
+
645
+ if (this.type.label === '{') {
646
+ const node = this.jsx_parseExpressionContainer();
647
+ node.type = 'Text';
648
+ body.push(node);
649
+ } else if (this.type.label === '}') {
650
+ return;
651
+ } else if (this.type.label === 'jsxTagStart') {
652
+ this.next();
653
+ if (this.value === '/') {
654
+ this.next();
655
+ const closingTag = this.jsx_parseElementName();
656
+ this.exprAllowed = true;
657
+
658
+ // Validate that the closing tag matches the opening tag
659
+ const currentElement = this.#path[this.#path.length - 1];
660
+ if (!currentElement || currentElement.type !== 'Element') {
661
+ this.raise(this.start, 'Unexpected closing tag');
662
+ }
663
+
664
+ const openingTagName = this.getElementName(currentElement.id);
665
+ const closingTagName = this.getElementName(closingTag);
666
+
667
+ if (openingTagName !== closingTagName) {
668
+ this.raise(
669
+ this.start,
670
+ `Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
671
+ );
672
+ }
673
+
674
+ this.#path.pop();
675
+ this.next();
676
+ return;
677
+ }
678
+ const node = this.parseElement();
679
+ if (node !== null) {
680
+ body.push(node);
681
+ }
682
+ } else {
683
+ const node = this.parseStatement(null);
684
+ body.push(node);
685
+ }
686
+ this.parseTemplateBody(body);
687
+ }
688
+
689
+ parseStatement(context, topLevel, exports) {
690
+ const tok = this.acornTypeScript.tokContexts;
691
+
692
+ if (
693
+ context !== 'for' &&
694
+ context !== 'if' &&
695
+ this.context.at(-1) === tc.b_stat &&
696
+ this.type === tt.braceL &&
697
+ this.context.some((c) => c === tok.tc_expr)
698
+ ) {
699
+ this.next();
700
+ const node = this.jsx_parseExpressionContainer();
701
+ node.type = 'Text';
702
+ this.next();
703
+ this.context.pop();
704
+ this.context.pop();
705
+ return node;
706
+ }
707
+
708
+ if (this.value === 'component') {
709
+ const node = this.startNode();
710
+ node.type = 'Component';
711
+ node.css = null;
712
+ this.next();
713
+ this.enterScope(0);
714
+ node.id = this.parseIdent();
715
+ this.parseFunctionParams(node);
716
+ this.eat(tt.braceL);
717
+ node.body = [];
718
+ this.#path.push(node);
719
+
720
+ this.parseTemplateBody(node.body);
721
+
722
+ this.#path.pop();
723
+ this.exitScope();
724
+
725
+ this.next();
726
+ this.finishNode(node, 'Component');
727
+ this.awaitPos = 0;
728
+
729
+ return node;
730
+ }
731
+
732
+ if (this.type.label === '@') {
733
+ // Try to parse as an expression statement first using tryParse
734
+ // This allows us to handle Ripple @ syntax like @count++ without
735
+ // interfering with legitimate decorator syntax
736
+ this.skip_decorator = true;
737
+ const expressionResult = this.tryParse(() => {
738
+ const node = this.startNode();
739
+ this.next();
740
+ // Force expression context to ensure @ is tokenized correctly
741
+ const oldExprAllowed = this.exprAllowed;
742
+ this.exprAllowed = true;
743
+ node.expression = this.parseExpression();
744
+
745
+ if (node.expression.type === 'UpdateExpression') {
746
+ let object = node.expression.argument;
747
+ while (object.type === 'MemberExpression') {
748
+ object = object.object;
749
+ }
750
+ if (object.type === 'Identifier') {
751
+ object.tracked = true;
752
+ }
753
+ } else if (node.expression.type === 'AssignmentExpression') {
754
+ let object = node.expression.left;
755
+ while (object.type === 'MemberExpression') {
756
+ object = object.object;
757
+ }
758
+ if (object.type === 'Identifier') {
759
+ object.tracked = true;
760
+ }
761
+ } else if (node.expression.type === 'Identifier') {
762
+ node.expression.tracked = true;
763
+ } else {
764
+ // TODO?
765
+ }
766
+
767
+ this.exprAllowed = oldExprAllowed;
768
+ return this.finishNode(node, 'ExpressionStatement');
769
+ });
770
+ this.skip_decorator = false;
771
+
772
+ // If parsing as expression statement succeeded, use that result
773
+ if (expressionResult.node) {
774
+ return expressionResult.node;
775
+ }
776
+ }
777
+
778
+ return super.parseStatement(context, topLevel, exports);
779
+ }
780
+
781
+ parseBlock(createNewLexicalScope, node, exitStrict) {
782
+ const parent = this.#path.at(-1);
783
+
784
+ if (parent?.type === 'Component' || parent?.type === 'Element') {
785
+ if (createNewLexicalScope === void 0) createNewLexicalScope = true;
786
+ if (node === void 0) node = this.startNode();
787
+
788
+ node.body = [];
789
+ this.expect(tt.braceL);
790
+ if (createNewLexicalScope) {
791
+ this.enterScope(0);
792
+ }
793
+ this.parseTemplateBody(node.body);
794
+
795
+ if (exitStrict) {
796
+ this.strict = false;
797
+ }
798
+ this.exprAllowed = true;
799
+
800
+ this.next();
801
+ if (createNewLexicalScope) {
802
+ this.exitScope();
803
+ }
804
+ return this.finishNode(node, 'BlockStatement');
805
+ }
806
+
807
+ return super.parseBlock(createNewLexicalScope, node, exitStrict);
808
+ }
809
+ }
810
+
811
+ return RippleParser;
812
+ };
790
813
  }
791
814
 
792
815
  /**
@@ -798,115 +821,115 @@ function RipplePlugin(config) {
798
821
  * @param {number} index
799
822
  */
800
823
  function get_comment_handlers(source, comments, index = 0) {
801
- return {
802
- onComment: (block, value, start, end, start_loc, end_loc) => {
803
- if (block && /\n/.test(value)) {
804
- let a = start;
805
- while (a > 0 && source[a - 1] !== '\n') a -= 1;
806
-
807
- let b = a;
808
- while (/[ \t]/.test(source[b])) b += 1;
809
-
810
- const indentation = source.slice(a, b);
811
- value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
812
- }
813
-
814
- comments.push({
815
- type: block ? 'Block' : 'Line',
816
- value,
817
- start,
818
- end,
819
- loc: {
820
- start: /** @type {import('acorn').Position} */ (start_loc),
821
- end: /** @type {import('acorn').Position} */ (end_loc),
822
- },
823
- });
824
- },
825
- add_comments: (ast) => {
826
- if (comments.length === 0) return;
827
-
828
- comments = comments
829
- .filter((comment) => comment.start >= index)
830
- .map(({ type, value, start, end }) => ({ type, value, start, end }));
831
-
832
- walk(ast, null, {
833
- _(node, { next, path }) {
834
- let comment;
835
-
836
- while (comments[0] && comments[0].start < node.start) {
837
- comment = /** @type {CommentWithLocation} */ (comments.shift());
838
- (node.leadingComments ||= []).push(comment);
839
- }
840
-
841
- next();
842
-
843
- if (comments[0]) {
844
- if (node.type === 'BlockStatement' && node.body.length === 0) {
845
- if (comments[0].start < node.end && comments[0].end < node.end) {
846
- comment = /** @type {CommentWithLocation} */ (comments.shift());
847
- (node.innerComments ||= []).push(comment);
848
- return;
849
- }
850
- }
851
- const parent = /** @type {any} */ (path.at(-1));
852
-
853
- if (parent === undefined || node.end !== parent.end) {
854
- const slice = source.slice(node.end, comments[0].start);
855
- const is_last_in_body =
856
- ((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
857
- parent.body.indexOf(node) === parent.body.length - 1) ||
858
- (parent?.type === 'ArrayExpression' &&
859
- parent.elements.indexOf(node) === parent.elements.length - 1) ||
860
- (parent?.type === 'ObjectExpression' &&
861
- parent.properties.indexOf(node) === parent.properties.length - 1);
862
-
863
- if (is_last_in_body) {
864
- // Special case: There can be multiple trailing comments after the last node in a block,
865
- // and they can be separated by newlines
866
- let end = node.end;
867
-
868
- while (comments.length) {
869
- const comment = comments[0];
870
- if (parent && comment.start >= parent.end) break;
871
-
872
- (node.trailingComments ||= []).push(comment);
873
- comments.shift();
874
- end = comment.end;
875
- }
876
- } else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
877
- node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
878
- }
879
- }
880
- }
881
- },
882
- });
883
-
884
- // Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
885
- // Adding them ensures that we can later detect the end of the expression tag correctly.
886
- if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
887
- (ast.trailingComments ||= []).push(...comments.splice(0));
888
- }
889
- },
890
- };
824
+ return {
825
+ onComment: (block, value, start, end, start_loc, end_loc) => {
826
+ if (block && /\n/.test(value)) {
827
+ let a = start;
828
+ while (a > 0 && source[a - 1] !== '\n') a -= 1;
829
+
830
+ let b = a;
831
+ while (/[ \t]/.test(source[b])) b += 1;
832
+
833
+ const indentation = source.slice(a, b);
834
+ value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
835
+ }
836
+
837
+ comments.push({
838
+ type: block ? 'Block' : 'Line',
839
+ value,
840
+ start,
841
+ end,
842
+ loc: {
843
+ start: /** @type {import('acorn').Position} */ (start_loc),
844
+ end: /** @type {import('acorn').Position} */ (end_loc),
845
+ },
846
+ });
847
+ },
848
+ add_comments: (ast) => {
849
+ if (comments.length === 0) return;
850
+
851
+ comments = comments
852
+ .filter((comment) => comment.start >= index)
853
+ .map(({ type, value, start, end }) => ({ type, value, start, end }));
854
+
855
+ walk(ast, null, {
856
+ _(node, { next, path }) {
857
+ let comment;
858
+
859
+ while (comments[0] && comments[0].start < node.start) {
860
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
861
+ (node.leadingComments ||= []).push(comment);
862
+ }
863
+
864
+ next();
865
+
866
+ if (comments[0]) {
867
+ if (node.type === 'BlockStatement' && node.body.length === 0) {
868
+ if (comments[0].start < node.end && comments[0].end < node.end) {
869
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
870
+ (node.innerComments ||= []).push(comment);
871
+ return;
872
+ }
873
+ }
874
+ const parent = /** @type {any} */ (path.at(-1));
875
+
876
+ if (parent === undefined || node.end !== parent.end) {
877
+ const slice = source.slice(node.end, comments[0].start);
878
+ const is_last_in_body =
879
+ ((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
880
+ parent.body.indexOf(node) === parent.body.length - 1) ||
881
+ (parent?.type === 'ArrayExpression' &&
882
+ parent.elements.indexOf(node) === parent.elements.length - 1) ||
883
+ (parent?.type === 'ObjectExpression' &&
884
+ parent.properties.indexOf(node) === parent.properties.length - 1);
885
+
886
+ if (is_last_in_body) {
887
+ // Special case: There can be multiple trailing comments after the last node in a block,
888
+ // and they can be separated by newlines
889
+ let end = node.end;
890
+
891
+ while (comments.length) {
892
+ const comment = comments[0];
893
+ if (parent && comment.start >= parent.end) break;
894
+
895
+ (node.trailingComments ||= []).push(comment);
896
+ comments.shift();
897
+ end = comment.end;
898
+ }
899
+ } else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
900
+ node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
901
+ }
902
+ }
903
+ }
904
+ },
905
+ });
906
+
907
+ // Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
908
+ // Adding them ensures that we can later detect the end of the expression tag correctly.
909
+ if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
910
+ (ast.trailingComments ||= []).push(...comments.splice(0));
911
+ }
912
+ },
913
+ };
891
914
  }
892
915
 
893
916
  export function parse(source) {
894
- const comments = [];
895
- const { onComment, add_comments } = get_comment_handlers(source, comments);
896
- let ast;
897
-
898
- try {
899
- ast = parser.parse(source, {
900
- sourceType: 'module',
901
- ecmaVersion: 13,
902
- locations: true,
903
- onComment,
904
- });
905
- } catch (e) {
906
- throw e;
907
- }
908
-
909
- add_comments(ast);
910
-
911
- return ast;
917
+ const comments = [];
918
+ const { onComment, add_comments } = get_comment_handlers(source, comments);
919
+ let ast;
920
+
921
+ try {
922
+ ast = parser.parse(source, {
923
+ sourceType: 'module',
924
+ ecmaVersion: 13,
925
+ locations: true,
926
+ onComment,
927
+ });
928
+ } catch (e) {
929
+ throw e;
930
+ }
931
+
932
+ add_comments(ast);
933
+
934
+ return ast;
912
935
  }