ripple 0.2.83 → 0.2.84

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,1022 +7,1052 @@ 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
-
29
- // Helper method to get the element name from a JSX identifier or member expression
30
- getElementName(node) {
31
- if (!node) return null;
32
- if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
33
- return node.name;
34
- } else if (node.type === 'MemberExpression' || node.type === 'JSXMemberExpression') {
35
- // For components like <Foo.Bar>, return "Foo.Bar"
36
- return this.getElementName(node.object) + '.' + this.getElementName(node.property);
37
- }
38
- return null;
39
- }
40
-
41
- getTokenFromCode(code) {
42
- if (code === 60) {
43
- // < character
44
- if (this.#path.findLast((n) => n.type === 'Component')) {
45
- // Check if everything before this position on the current line is whitespace
46
- let lineStart = this.pos - 1;
47
- while (
48
- lineStart >= 0 &&
49
- this.input.charCodeAt(lineStart) !== 10 &&
50
- this.input.charCodeAt(lineStart) !== 13
51
- ) {
52
- lineStart--;
53
- }
54
- lineStart++; // Move past the newline character
55
-
56
- // Check if all characters from line start to current position are whitespace
57
- let allWhitespace = true;
58
- for (let i = lineStart; i < this.pos; i++) {
59
- const ch = this.input.charCodeAt(i);
60
- if (ch !== 32 && ch !== 9) {
61
- allWhitespace = false;
62
- break;
63
- }
64
- }
65
-
66
- // Check if the character after < is not whitespace
67
- if (allWhitespace && this.pos + 1 < this.input.length) {
68
- const nextChar = this.input.charCodeAt(this.pos + 1);
69
- if (nextChar !== 32 && nextChar !== 9 && nextChar !== 10 && nextChar !== 13) {
70
- const tokTypes = this.acornTypeScript.tokTypes;
71
- ++this.pos;
72
- return this.finishToken(tokTypes.jsxTagStart);
73
- }
74
- }
75
- }
76
- }
77
-
78
- if (code === 35) {
79
- // # character
80
- // Look ahead to see if this is followed by [ for tuple syntax
81
- if (this.pos + 1 < this.input.length) {
82
- const nextChar = this.input.charCodeAt(this.pos + 1);
83
- if (nextChar === 91) { // [ character
84
- // This is a tuple literal #[
85
- // Consume both # and [
86
- ++this.pos; // consume #
87
- ++this.pos; // consume [
88
- return this.finishToken(tt.bracketL, '#[');
89
- }
90
- }
91
- }
92
-
93
- if (code === 64) {
94
- // @ character
95
- // Look ahead to see if this is followed by a valid identifier character
96
- if (this.pos + 1 < this.input.length) {
97
- const nextChar = this.input.charCodeAt(this.pos + 1);
98
- // Check if the next character can start an identifier
99
- if (
100
- (nextChar >= 65 && nextChar <= 90) || // A-Z
101
- (nextChar >= 97 && nextChar <= 122) || // a-z
102
- nextChar === 95 ||
103
- nextChar === 36
104
- ) {
105
- // _ or $
106
-
107
- // Check if we're in an expression context
108
- // In JSX expressions, inside parentheses, assignments, etc.
109
- // we want to treat @ as an identifier prefix rather than decorator
110
- const currentType = this.type;
111
- const inExpression =
112
- this.exprAllowed ||
113
- currentType === tt.braceL || // Inside { }
114
- currentType === tt.parenL || // Inside ( )
115
- currentType === tt.eq || // After =
116
- currentType === tt.comma || // After ,
117
- currentType === tt.colon || // After :
118
- currentType === tt.question || // After ?
119
- currentType === tt.logicalOR || // After ||
120
- currentType === tt.logicalAND || // After &&
121
- currentType === tt.dot || // After . (for member expressions like obj.@prop)
122
- currentType === tt.questionDot; // After ?. (for optional chaining like obj?.@prop)
123
-
124
- if (inExpression) {
125
- return this.readAtIdentifier();
126
- }
127
- }
128
- }
129
- }
130
- return super.getTokenFromCode(code);
131
- }
132
-
133
- // Read an @ prefixed identifier
134
- readAtIdentifier() {
135
- const start = this.pos;
136
- this.pos++; // skip '@'
137
-
138
- // Read the identifier part manually
139
- let word = '';
140
- while (this.pos < this.input.length) {
141
- const ch = this.input.charCodeAt(this.pos);
142
- if (
143
- (ch >= 65 && ch <= 90) || // A-Z
144
- (ch >= 97 && ch <= 122) || // a-z
145
- (ch >= 48 && ch <= 57) || // 0-9
146
- ch === 95 ||
147
- ch === 36
148
- ) {
149
- // _ or $
150
- word += this.input[this.pos++];
151
- } else {
152
- break;
153
- }
154
- }
155
-
156
- if (word === '') {
157
- this.raise(start, 'Invalid @ identifier');
158
- }
159
-
160
- // Return the full identifier including @
161
- return this.finishToken(tt.name, '@' + word);
162
- }
163
-
164
- // Override parseIdent to mark @ identifiers as tracked
165
- parseIdent(liberal) {
166
- const node = super.parseIdent(liberal);
167
- if (node.name && node.name.startsWith('@')) {
168
- node.name = node.name.slice(1); // Remove the '@' for internal use
169
- node.tracked = true;
170
- node.start++;
171
- const prev_pos = this.pos;
172
- this.pos = node.start;
173
- node.loc.start = this.curPosition();
174
- this.pos = prev_pos;
175
- }
176
- return node;
177
- }
178
-
179
- parseExprAtom(refDestructuringErrors, forNew, forInit) {
180
- // Check if this is a tuple literal starting with #[
181
- if (this.type === tt.bracketL && this.value === '#[') {
182
- return this.parseTrackedArrayExpression();
183
- }
184
-
185
- return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
186
- }
187
-
188
- parseTrackedArrayExpression() {
189
- const node = this.startNode();
190
- this.next(); // consume the '#['
191
-
192
- node.elements = [];
193
-
194
- // Parse array elements similar to regular array parsing
195
- let first = true;
196
- while (!this.eat(tt.bracketR)) {
197
- if (!first) {
198
- this.expect(tt.comma);
199
- if (this.afterTrailingComma(tt.bracketR)) break;
200
- } else {
201
- first = false;
202
- }
203
-
204
- if (this.type === tt.comma) {
205
- // Hole in array
206
- node.elements.push(null);
207
- } else if (this.type === tt.ellipsis) {
208
- // Spread element
209
- const element = this.parseSpread();
210
- node.elements.push(element);
211
- if (this.type === tt.comma && this.input.charCodeAt(this.pos) === 93) {
212
- this.raise(this.pos, "Trailing comma is not permitted after the rest element");
213
- }
214
- } else {
215
- // Regular element
216
- node.elements.push(this.parseMaybeAssign(false));
217
- }
218
- }
219
-
220
- return this.finishNode(node, 'TrackedArrayExpression');
221
- }
222
-
223
- parseExportDefaultDeclaration() {
224
- // Check if this is "export default component"
225
- if (this.value === 'component') {
226
- const node = this.startNode();
227
- node.type = 'Component';
228
- node.css = null;
229
- node.default = true;
230
- this.next();
231
- this.enterScope(0);
232
-
233
- node.id = this.type.label === 'name' ? this.parseIdent() : null;
234
-
235
- this.parseFunctionParams(node);
236
- this.eat(tt.braceL);
237
- node.body = [];
238
- this.#path.push(node);
239
-
240
- this.parseTemplateBody(node.body);
241
- this.#path.pop();
242
- this.exitScope();
243
-
244
- this.next();
245
- this.finishNode(node, 'Component');
246
- this.awaitPos = 0;
247
-
248
- return node;
249
- }
250
-
251
- return super.parseExportDefaultDeclaration();
252
- }
253
-
254
- parseForStatement(node) {
255
- this.next()
256
- let awaitAt = (this.options.ecmaVersion >= 9 && this.canAwait && this.eatContextual("await")) ? this.lastTokStart : -1
257
- this.labels.push({kind: "loop"})
258
- this.enterScope(0)
259
- this.expect(tt.parenL)
260
-
261
- if (this.type === tt.semi) {
262
- if (awaitAt > -1) this.unexpected(awaitAt)
263
- return this.parseFor(node, null)
264
- }
265
-
266
- let isLet = this.isLet()
267
- if (this.type === tt._var || this.type === tt._const || isLet) {
268
- let init = this.startNode(), kind = isLet ? "let" : this.value
269
- this.next()
270
- this.parseVar(init, true, kind)
271
- this.finishNode(init, "VariableDeclaration")
272
- return this.parseForAfterInitWithIndex(node, init, awaitAt)
273
- }
274
-
275
- // Handle other cases like using declarations if they exist
276
- let startsWithLet = this.isContextual("let"), isForOf = false
277
- let usingKind = (this.isUsing && this.isUsing(true)) ? "using" : (this.isAwaitUsing && this.isAwaitUsing(true)) ? "await using" : null
278
- if (usingKind) {
279
- let init = this.startNode()
280
- this.next()
281
- if (usingKind === "await using") {
282
- if (!this.canAwait) {
283
- this.raise(this.start, "Await using cannot appear outside of async function")
284
- }
285
- this.next()
286
- }
287
- this.parseVar(init, true, usingKind)
288
- this.finishNode(init, "VariableDeclaration")
289
- return this.parseForAfterInitWithIndex(node, init, awaitAt)
290
- }
291
-
292
- let containsEsc = this.containsEsc
293
- let refDestructuringErrors = {}
294
- let initPos = this.start
295
- let init = awaitAt > -1
296
- ? this.parseExprSubscripts(refDestructuringErrors, "await")
297
- : this.parseExpression(true, refDestructuringErrors)
298
-
299
- if (this.type === tt._in || (isForOf = this.options.ecmaVersion >= 6 && this.isContextual("of"))) {
300
- if (awaitAt > -1) { // implies `ecmaVersion >= 9`
301
- if (this.type === tt._in) this.unexpected(awaitAt)
302
- node.await = true
303
- } else if (isForOf && this.options.ecmaVersion >= 8) {
304
- if (init.start === initPos && !containsEsc && init.type === "Identifier" && init.name === "async") this.unexpected()
305
- else if (this.options.ecmaVersion >= 9) node.await = false
306
- }
307
- if (startsWithLet && isForOf) this.raise(init.start, "The left-hand side of a for-of loop may not start with 'let'.")
308
- this.toAssignable(init, false, refDestructuringErrors)
309
- this.checkLValPattern(init)
310
- return this.parseForInWithIndex(node, init)
311
- } else {
312
- this.checkExpressionErrors(refDestructuringErrors, true)
313
- }
314
-
315
- if (awaitAt > -1) this.unexpected(awaitAt)
316
- return this.parseFor(node, init)
317
- }
318
-
319
- parseForAfterInitWithIndex(node, init, awaitAt) {
320
- if ((this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init.declarations.length === 1) {
321
- if (this.options.ecmaVersion >= 9) {
322
- if (this.type === tt._in) {
323
- if (awaitAt > -1) this.unexpected(awaitAt)
324
- } else node.await = awaitAt > -1
325
- }
326
- return this.parseForInWithIndex(node, init)
327
- }
328
- if (awaitAt > -1) this.unexpected(awaitAt)
329
- return this.parseFor(node, init)
330
- }
331
-
332
- parseForInWithIndex(node, init) {
333
- const isForIn = this.type === tt._in
334
- this.next()
335
-
336
- if (
337
- init.type === "VariableDeclaration" &&
338
- init.declarations[0].init != null &&
339
- (
340
- !isForIn ||
341
- this.options.ecmaVersion < 8 ||
342
- this.strict ||
343
- init.kind !== "var" ||
344
- init.declarations[0].id.type !== "Identifier"
345
- )
346
- ) {
347
- this.raise(
348
- init.start,
349
- `${isForIn ? "for-in" : "for-of"} loop variable declaration may not have an initializer`
350
- )
351
- }
352
-
353
- node.left = init
354
- node.right = isForIn ? this.parseExpression() : this.parseMaybeAssign()
355
-
356
- // Check for our extended syntax: "; index varName"
357
- if (!isForIn && this.type === tt.semi) {
358
- this.next() // consume ';'
359
-
360
- if (this.isContextual('index')) {
361
- this.next() // consume 'index'
362
-
363
- if (this.type === tt.name) {
364
- node.index = this.parseIdent()
365
- } else {
366
- this.raise(this.start, 'Expected identifier after "index" keyword')
367
- }
368
- } else {
369
- this.raise(this.start, 'Expected "index" keyword after semicolon in for-of loop')
370
- }
371
- } else if (!isForIn) {
372
- // Set index to null for standard for-of loops
373
- node.index = null
374
- }
375
-
376
- this.expect(tt.parenR)
377
- node.body = this.parseStatement("for")
378
- this.exitScope()
379
- this.labels.pop()
380
- return this.finishNode(node, isForIn ? "ForInStatement" : "ForOfStatement")
381
- }
382
-
383
- shouldParseExportStatement() {
384
- if (super.shouldParseExportStatement()) {
385
- return true;
386
- }
387
- if (this.value === 'component') {
388
- return true;
389
- }
390
- return this.type.keyword === 'var';
391
- }
392
-
393
- jsx_parseExpressionContainer() {
394
- let node = this.startNode();
395
- this.next();
396
-
397
- node.expression =
398
- this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
399
- this.expect(tt.braceR);
400
- return this.finishNode(node, 'JSXExpressionContainer');
401
- }
402
-
403
- jsx_parseTupleContainer() {
404
- var t = this.startNode();
405
- return (
406
- this.next(),
407
- (t.expression =
408
- this.type === tt.bracketR ? this.jsx_parseEmptyExpression() : this.parseExpression()),
409
- this.expect(tt.bracketR),
410
- this.finishNode(t, 'JSXExpressionContainer')
411
- );
412
- }
413
-
414
- jsx_parseAttribute() {
415
- let node = this.startNode();
416
-
417
- if (this.eat(tt.braceL)) {
418
- if (this.value === 'ref') {
419
- this.next();
420
- if (this.type === tt.braceR) {
421
- this.raise(
422
- this.start,
423
- '"ref" is a Ripple keyword and must be used in the form {ref fn}',
424
- );
425
- }
426
- node.argument = this.parseMaybeAssign();
427
- this.expect(tt.braceR);
428
- return this.finishNode(node, 'RefAttribute');
429
- } else if (this.type === tt.ellipsis) {
430
- this.expect(tt.ellipsis);
431
- node.argument = this.parseMaybeAssign();
432
- this.expect(tt.braceR);
433
- return this.finishNode(node, 'SpreadAttribute');
434
- } else if (this.lookahead().type === tt.ellipsis) {
435
- this.expect(tt.ellipsis);
436
- node.argument = this.parseMaybeAssign();
437
- this.expect(tt.braceR);
438
- return this.finishNode(node, 'SpreadAttribute');
439
- } else {
440
- const id = this.parseIdentNode();
441
- id.tracked = false;
442
- if (id.name.startsWith('@')) {
443
- id.tracked = true;
444
- id.name = id.name.slice(1);
445
- }
446
- this.finishNode(id, 'Identifier');
447
- node.name = id;
448
- node.value = id;
449
- this.next();
450
- this.expect(tt.braceR);
451
- return this.finishNode(node, 'Attribute');
452
- }
453
- }
454
- node.name = this.jsx_parseNamespacedName();
455
- node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null;
456
- return this.finishNode(node, 'JSXAttribute');
457
- }
458
-
459
- jsx_parseNamespacedName() {
460
- const base = this.jsx_parseIdentifier();
461
- if (!this.eat(tt.colon)) return base;
462
- const node = this.startNodeAt(base.start, base.loc.start);
463
- node.namespace = base;
464
- node.name = this.jsx_parseIdentifier();
465
- return this.finishNode(node, 'JSXNamespacedName');
466
- }
467
-
468
- jsx_parseIdentifier() {
469
- const node = this.startNode();
470
-
471
- if (this.type.label === '@') {
472
- this.next(); // consume @
473
-
474
- if (this.type === tt.name || this.type.label === 'jsxName') {
475
- node.name = this.value;
476
- node.tracked = true;
477
- this.next();
478
- } else {
479
- // Unexpected token after @
480
- this.unexpected();
481
- }
482
- } else if (
483
- (this.type === tt.name || this.type.label === 'jsxName') &&
484
- this.value &&
485
- this.value.startsWith('@')
486
- ) {
487
- node.name = this.value.substring(1);
488
- node.tracked = true;
489
- this.next();
490
- } else if (this.type === tt.name || this.type.keyword || this.type.label === 'jsxName') {
491
- node.name = this.value;
492
- node.tracked = false; // Explicitly mark as not tracked
493
- this.next();
494
- } else {
495
- return super.jsx_parseIdentifier();
496
- }
497
-
498
- return this.finishNode(node, 'JSXIdentifier');
499
- }
500
-
501
- // Override jsx_parseElementName to support @ syntax in member expressions
502
- jsx_parseElementName() {
503
- let node = this.jsx_parseIdentifier();
504
- if (this.eat(tt.dot)) {
505
- let memberExpr = this.startNodeAt(node.start, node.loc && node.loc.start);
506
- memberExpr.object = node;
507
- memberExpr.property = this.jsx_parseIdentifier();
508
- memberExpr.computed = false;
509
- while (this.eat(tt.dot)) {
510
- let newMemberExpr = this.startNodeAt(
511
- memberExpr.start,
512
- memberExpr.loc && memberExpr.loc.start,
513
- );
514
- newMemberExpr.object = memberExpr;
515
- newMemberExpr.property = this.jsx_parseIdentifier();
516
- newMemberExpr.computed = false;
517
- memberExpr = this.finishNode(newMemberExpr, 'JSXMemberExpression');
518
- }
519
- return this.finishNode(memberExpr, 'JSXMemberExpression');
520
- }
521
- return node;
522
- }
523
-
524
- jsx_parseAttributeValue() {
525
- const tok = this.acornTypeScript.tokTypes;
526
-
527
- switch (this.type) {
528
- case tt.braceL:
529
- var t = this.jsx_parseExpressionContainer();
530
- return (
531
- 'JSXEmptyExpression' === t.expression.type &&
532
- this.raise(t.start, 'attributes must only be assigned a non-empty expression'),
533
- t
534
- );
535
- case tok.jsxTagStart:
536
- case tt.string:
537
- return this.parseExprAtom();
538
- default:
539
- this.raise(this.start, 'value should be either an expression or a quoted text');
540
- }
541
- }
542
-
543
- parseTryStatement(node) {
544
- this.next();
545
- node.block = this.parseBlock();
546
- node.handler = null;
547
-
548
- if (this.value === 'pending') {
549
- this.next();
550
- node.pending = this.parseBlock();
551
- } else {
552
- node.pending = null;
553
- }
554
-
555
- if (this.type === tt._catch) {
556
- var clause = this.startNode();
557
- this.next();
558
- if (this.eat(tt.parenL)) {
559
- clause.param = this.parseCatchClauseParam();
560
- } else {
561
- if (this.options.ecmaVersion < 10) {
562
- this.unexpected();
563
- }
564
- clause.param = null;
565
- this.enterScope(0);
566
- }
567
- clause.body = this.parseBlock(false);
568
- this.exitScope();
569
- node.handler = this.finishNode(clause, 'CatchClause');
570
- }
571
- node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null;
572
-
573
- if (!node.handler && !node.finalizer && !node.pending) {
574
- this.raise(node.start, 'Missing catch or finally clause');
575
- }
576
- return this.finishNode(node, 'TryStatement');
577
- }
578
-
579
- jsx_readToken() {
580
- let out = '',
581
- chunkStart = this.pos;
582
- const tok = this.acornTypeScript.tokTypes;
583
-
584
- for (;;) {
585
- if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
586
- let ch = this.input.charCodeAt(this.pos);
587
-
588
- switch (ch) {
589
- case 60: // '<'
590
- case 123: // '{'
591
- if (ch === 60 && this.exprAllowed) {
592
- ++this.pos;
593
- return this.finishToken(tok.jsxTagStart);
594
- }
595
- if (ch === 123 && this.exprAllowed) {
596
- return this.getTokenFromCode(ch);
597
- }
598
- throw new Error('TODO: Invalid syntax');
599
-
600
- case 47: // '/'
601
- // Check if this is a comment (// or /*)
602
- if (this.input.charCodeAt(this.pos + 1) === 47) {
603
- // '//'
604
- // Line comment - handle it properly
605
- const commentStart = this.pos;
606
- const startLoc = this.curPosition();
607
- this.pos += 2;
608
-
609
- let commentText = '';
610
- while (this.pos < this.input.length) {
611
- const nextCh = this.input.charCodeAt(this.pos);
612
- if (acorn.isNewLine(nextCh)) break;
613
- commentText += this.input[this.pos];
614
- this.pos++;
615
- }
616
-
617
- const commentEnd = this.pos;
618
- const endLoc = this.curPosition();
619
-
620
- // Call onComment if it exists
621
- if (this.options.onComment) {
622
- this.options.onComment(
623
- false,
624
- commentText,
625
- commentStart,
626
- commentEnd,
627
- startLoc,
628
- endLoc,
629
- );
630
- }
631
-
632
- // Continue processing from current position
633
- break;
634
- } else if (this.input.charCodeAt(this.pos + 1) === 42) {
635
- // '/*'
636
- // Block comment - handle it properly
637
- const commentStart = this.pos;
638
- const startLoc = this.curPosition();
639
- this.pos += 2;
640
-
641
- let commentText = '';
642
- while (this.pos < this.input.length - 1) {
643
- if (
644
- this.input.charCodeAt(this.pos) === 42 &&
645
- this.input.charCodeAt(this.pos + 1) === 47
646
- ) {
647
- this.pos += 2;
648
- break;
649
- }
650
- commentText += this.input[this.pos];
651
- this.pos++;
652
- }
653
-
654
- const commentEnd = this.pos;
655
- const endLoc = this.curPosition();
656
-
657
- // Call onComment if it exists
658
- if (this.options.onComment) {
659
- this.options.onComment(
660
- true,
661
- commentText,
662
- commentStart,
663
- commentEnd,
664
- startLoc,
665
- endLoc,
666
- );
667
- }
668
-
669
- // Continue processing from current position
670
- break;
671
- }
672
- // If not a comment, fall through to default case
673
- this.context.push(tc.b_stat);
674
- this.exprAllowed = true;
675
- return original.readToken.call(this, ch);
676
-
677
- case 38: // '&'
678
- out += this.input.slice(chunkStart, this.pos);
679
- out += this.jsx_readEntity();
680
- chunkStart = this.pos;
681
- break;
682
-
683
- case 62: // '>'
684
- case 125: {
685
- // '}'
686
- if (
687
- ch === 125 &&
688
- (this.#path.length === 0 ||
689
- this.#path.at(-1)?.type === 'Component' ||
690
- this.#path.at(-1)?.type === 'Element')
691
- ) {
692
- return original.readToken.call(this, ch);
693
- }
694
- this.raise(
695
- this.pos,
696
- 'Unexpected token `' +
697
- this.input[this.pos] +
698
- '`. Did you mean `' +
699
- (ch === 62 ? '&gt;' : '&rbrace;') +
700
- '` or ' +
701
- '`{"' +
702
- this.input[this.pos] +
703
- '"}' +
704
- '`?',
705
- );
706
- }
707
-
708
- default:
709
- if (acorn.isNewLine(ch)) {
710
- out += this.input.slice(chunkStart, this.pos);
711
- out += this.jsx_readNewLine(true);
712
- chunkStart = this.pos;
713
- } else if (ch === 32 || ch === 9) {
714
- ++this.pos;
715
- } else {
716
- this.context.push(tc.b_stat);
717
- this.exprAllowed = true;
718
- return original.readToken.call(this, ch);
719
- }
720
- }
721
- }
722
- }
723
-
724
- parseElement() {
725
- const tok = this.acornTypeScript.tokTypes;
726
- // Adjust the start so we capture the `<` as part of the element
727
- const prev_pos = this.pos;
728
- this.pos = this.start - 1;
729
- const position = this.curPosition();
730
- this.pos = prev_pos;
731
-
732
- const element = this.startNode();
733
- element.start = position.index;
734
- element.loc.start = position;
735
- element.type = 'Element';
736
- this.#path.push(element);
737
- element.children = [];
738
- const open = this.jsx_parseOpeningElementAt();
739
- for (const attr of open.attributes) {
740
- if (attr.type === 'JSXAttribute') {
741
- attr.type = 'Attribute';
742
- if (attr.name.type === 'JSXIdentifier') {
743
- attr.name.type = 'Identifier';
744
- }
745
- if (attr.value !== null) {
746
- if (attr.value.type === 'JSXExpressionContainer') {
747
- attr.value = attr.value.expression;
748
- }
749
- }
750
- }
751
- }
752
- if (open.name.type === 'JSXIdentifier') {
753
- open.name.type = 'Identifier';
754
- }
755
-
756
- element.id = convert_from_jsx(open.name);
757
- element.attributes = open.attributes;
758
- element.selfClosing = open.selfClosing;
759
- element.metadata = {};
760
-
761
- if (element.selfClosing) {
762
- this.#path.pop();
763
-
764
- if (this.type.label === '</>/<=/>=') {
765
- this.pos--;
766
- this.next();
767
- }
768
- } else {
769
- if (open.name.name === 'style') {
770
- // jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
771
- // So backtrack to the end of the <style> tag to make sure everything is included
772
- const start = open.end;
773
- const input = this.input.slice(start);
774
- const end = input.indexOf('</style>');
775
- const content = input.slice(0, end);
776
-
777
- const component = this.#path.findLast((n) => n.type === 'Component');
778
- if (component.css !== null) {
779
- throw new Error('Components can only have one style tag');
780
- }
781
- component.css = parse_style(content);
782
-
783
- const newLines = content.match(regex_newline_characters)?.length;
784
- if (newLines) {
785
- this.curLine = open.loc.end.line + newLines;
786
- this.lineStart = start + content.lastIndexOf('\n') + 1;
787
- }
788
- this.pos = start + content.length + 1;
789
-
790
- this.type = tok.jsxTagStart;
791
- this.next();
792
- if (this.value === '/') {
793
- this.next();
794
- this.jsx_parseElementName();
795
- this.exprAllowed = true;
796
- this.#path.pop();
797
- this.next();
798
- }
799
- // This node is used for Prettier, we don't actually need
800
- // the node for Ripple's transform process
801
- element.children = [component.css];
802
- // Ensure we escape JSX <tag></tag> context
803
- const tokContexts = this.acornTypeScript.tokContexts;
804
- const curContext = this.curContext();
805
-
806
- if (curContext === tokContexts.tc_expr) {
807
- this.context.pop();
808
- }
809
-
810
- this.finishNode(element, 'Element');
811
- return element;
812
- } else {
813
- this.enterScope(0);
814
- this.parseTemplateBody(element.children);
815
- this.exitScope();
816
-
817
- // Check if this element was properly closed
818
- // If we reach here and this element is still in the path, it means it was never closed
819
- if (this.#path[this.#path.length - 1] === element) {
820
- const tagName = this.getElementName(element.id);
821
- this.raise(
822
- this.start,
823
- `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
824
- );
825
- }
826
- }
827
- // Ensure we escape JSX <tag></tag> context
828
- const tokContexts = this.acornTypeScript.tokContexts;
829
- const curContext = this.curContext();
830
-
831
- if (curContext === tokContexts.tc_expr) {
832
- this.context.pop();
833
- }
834
- }
835
-
836
- this.finishNode(element, 'Element');
837
- return element;
838
- }
839
-
840
- parseTemplateBody(body) {
841
- var inside_func =
842
- this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
843
-
844
- if (!inside_func) {
845
- if (this.type.label === 'return') {
846
- throw new Error('`return` statements are not allowed in components');
847
- }
848
- if (this.type.label === 'continue') {
849
- throw new Error('`continue` statements are not allowed in components');
850
- }
851
- if (this.type.label === 'break') {
852
- throw new Error('`break` statements are not allowed in components');
853
- }
854
- }
855
-
856
- if (this.type.label === '{') {
857
- const node = this.jsx_parseExpressionContainer();
858
- node.type = 'Text';
859
- body.push(node);
860
- } else if (this.type.label === '}') {
861
- return;
862
- } else if (this.type.label === 'jsxTagStart') {
863
- this.next();
864
- if (this.value === '/') {
865
- this.next();
866
- const closingTag = this.jsx_parseElementName();
867
- this.exprAllowed = true;
868
-
869
- // Validate that the closing tag matches the opening tag
870
- const currentElement = this.#path[this.#path.length - 1];
871
- if (!currentElement || currentElement.type !== 'Element') {
872
- this.raise(this.start, 'Unexpected closing tag');
873
- }
874
-
875
- const openingTagName = this.getElementName(currentElement.id);
876
- const closingTagName = this.getElementName(closingTag);
877
-
878
- if (openingTagName !== closingTagName) {
879
- this.raise(
880
- this.start,
881
- `Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
882
- );
883
- }
884
-
885
- this.#path.pop();
886
- this.next();
887
- return;
888
- }
889
- const node = this.parseElement();
890
- if (node !== null) {
891
- body.push(node);
892
- }
893
- } else {
894
- const node = this.parseStatement(null);
895
- body.push(node);
896
- }
897
- this.parseTemplateBody(body);
898
- }
899
-
900
- parseStatement(context, topLevel, exports) {
901
- const tok = this.acornTypeScript.tokContexts;
902
-
903
- if (
904
- context !== 'for' &&
905
- context !== 'if' &&
906
- this.context.at(-1) === tc.b_stat &&
907
- this.type === tt.braceL &&
908
- this.context.some((c) => c === tok.tc_expr)
909
- ) {
910
- this.next();
911
- const node = this.jsx_parseExpressionContainer();
912
- node.type = 'Text';
913
- this.next();
914
- this.context.pop();
915
- this.context.pop();
916
- return node;
917
- }
918
-
919
- if (this.value === 'component') {
920
- const node = this.startNode();
921
- node.type = 'Component';
922
- node.css = null;
923
- this.next();
924
- this.enterScope(0);
925
- node.id = this.parseIdent();
926
- this.parseFunctionParams(node);
927
- this.eat(tt.braceL);
928
- node.body = [];
929
- this.#path.push(node);
930
-
931
- this.parseTemplateBody(node.body);
932
-
933
- this.#path.pop();
934
- this.exitScope();
935
-
936
- this.next();
937
- this.finishNode(node, 'Component');
938
- this.awaitPos = 0;
939
-
940
- return node;
941
- }
942
-
943
-
944
-
945
- if (this.type.label === '@') {
946
- // Try to parse as an expression statement first using tryParse
947
- // This allows us to handle Ripple @ syntax like @count++ without
948
- // interfering with legitimate decorator syntax
949
- this.skip_decorator = true;
950
- const expressionResult = this.tryParse(() => {
951
- const node = this.startNode();
952
- this.next();
953
- // Force expression context to ensure @ is tokenized correctly
954
- const old_expr_allowed = this.exprAllowed;
955
- this.exprAllowed = true;
956
- node.expression = this.parseExpression();
957
-
958
- if (node.expression.type === 'UpdateExpression') {
959
- let object = node.expression.argument;
960
- while (object.type === 'MemberExpression') {
961
- object = object.object;
962
- }
963
- if (object.type === 'Identifier') {
964
- object.tracked = true;
965
- }
966
- } else if (node.expression.type === 'AssignmentExpression') {
967
- let object = node.expression.left;
968
- while (object.type === 'MemberExpression') {
969
- object = object.object;
970
- }
971
- if (object.type === 'Identifier') {
972
- object.tracked = true;
973
- }
974
- } else if (node.expression.type === 'Identifier') {
975
- node.expression.tracked = true;
976
- } else {
977
- // TODO?
978
- }
979
-
980
- this.exprAllowed = old_expr_allowed;
981
- return this.finishNode(node, 'ExpressionStatement');
982
- });
983
- this.skip_decorator = false;
984
-
985
- // If parsing as expression statement succeeded, use that result
986
- if (expressionResult.node) {
987
- return expressionResult.node;
988
- }
989
- }
990
-
991
- return super.parseStatement(context, topLevel, exports);
992
- }
993
-
994
- parseBlock(createNewLexicalScope, node, exitStrict) {
995
- const parent = this.#path.at(-1);
996
-
997
- if (parent?.type === 'Component' || parent?.type === 'Element') {
998
- if (createNewLexicalScope === void 0) createNewLexicalScope = true;
999
- if (node === void 0) node = this.startNode();
1000
-
1001
- node.body = [];
1002
- this.expect(tt.braceL);
1003
- if (createNewLexicalScope) {
1004
- this.enterScope(0);
1005
- }
1006
- this.parseTemplateBody(node.body);
1007
-
1008
- if (exitStrict) {
1009
- this.strict = false;
1010
- }
1011
- this.exprAllowed = true;
1012
-
1013
- this.next();
1014
- if (createNewLexicalScope) {
1015
- this.exitScope();
1016
- }
1017
- return this.finishNode(node, 'BlockStatement');
1018
- }
1019
-
1020
- return super.parseBlock(createNewLexicalScope, node, exitStrict);
1021
- }
1022
- }
1023
-
1024
- return RippleParser;
1025
- };
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
+
29
+ // Helper method to get the element name from a JSX identifier or member expression
30
+ getElementName(node) {
31
+ if (!node) return null;
32
+ if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
33
+ return node.name;
34
+ } else if (node.type === 'MemberExpression' || node.type === 'JSXMemberExpression') {
35
+ // For components like <Foo.Bar>, return "Foo.Bar"
36
+ return this.getElementName(node.object) + '.' + this.getElementName(node.property);
37
+ }
38
+ return null;
39
+ }
40
+
41
+ getTokenFromCode(code) {
42
+ if (code === 60) {
43
+ // < character
44
+ if (this.#path.findLast((n) => n.type === 'Component')) {
45
+ // Check if everything before this position on the current line is whitespace
46
+ let lineStart = this.pos - 1;
47
+ while (
48
+ lineStart >= 0 &&
49
+ this.input.charCodeAt(lineStart) !== 10 &&
50
+ this.input.charCodeAt(lineStart) !== 13
51
+ ) {
52
+ lineStart--;
53
+ }
54
+ lineStart++; // Move past the newline character
55
+
56
+ // Check if all characters from line start to current position are whitespace
57
+ let allWhitespace = true;
58
+ for (let i = lineStart; i < this.pos; i++) {
59
+ const ch = this.input.charCodeAt(i);
60
+ if (ch !== 32 && ch !== 9) {
61
+ allWhitespace = false;
62
+ break;
63
+ }
64
+ }
65
+
66
+ // Check if the character after < is not whitespace
67
+ if (allWhitespace && this.pos + 1 < this.input.length) {
68
+ const nextChar = this.input.charCodeAt(this.pos + 1);
69
+ if (nextChar !== 32 && nextChar !== 9 && nextChar !== 10 && nextChar !== 13) {
70
+ const tokTypes = this.acornTypeScript.tokTypes;
71
+ ++this.pos;
72
+ return this.finishToken(tokTypes.jsxTagStart);
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ if (code === 35) {
79
+ // # character
80
+ // Look ahead to see if this is followed by [ for tuple syntax
81
+ if (this.pos + 1 < this.input.length) {
82
+ const nextChar = this.input.charCodeAt(this.pos + 1);
83
+ if (nextChar === 91) {
84
+ // [ character
85
+ // This is a tuple literal #[
86
+ // Consume both # and [
87
+ ++this.pos; // consume #
88
+ ++this.pos; // consume [
89
+ return this.finishToken(tt.bracketL, '#[');
90
+ }
91
+ }
92
+ }
93
+
94
+ if (code === 64) {
95
+ // @ character
96
+ // Look ahead to see if this is followed by a valid identifier character
97
+ if (this.pos + 1 < this.input.length) {
98
+ const nextChar = this.input.charCodeAt(this.pos + 1);
99
+ // Check if the next character can start an identifier
100
+ if (
101
+ (nextChar >= 65 && nextChar <= 90) || // A-Z
102
+ (nextChar >= 97 && nextChar <= 122) || // a-z
103
+ nextChar === 95 ||
104
+ nextChar === 36
105
+ ) {
106
+ // _ or $
107
+
108
+ // Check if we're in an expression context
109
+ // In JSX expressions, inside parentheses, assignments, etc.
110
+ // we want to treat @ as an identifier prefix rather than decorator
111
+ const currentType = this.type;
112
+ const inExpression =
113
+ this.exprAllowed ||
114
+ currentType === tt.braceL || // Inside { }
115
+ currentType === tt.parenL || // Inside ( )
116
+ currentType === tt.eq || // After =
117
+ currentType === tt.comma || // After ,
118
+ currentType === tt.colon || // After :
119
+ currentType === tt.question || // After ?
120
+ currentType === tt.logicalOR || // After ||
121
+ currentType === tt.logicalAND || // After &&
122
+ currentType === tt.dot || // After . (for member expressions like obj.@prop)
123
+ currentType === tt.questionDot; // After ?. (for optional chaining like obj?.@prop)
124
+
125
+ if (inExpression) {
126
+ return this.readAtIdentifier();
127
+ }
128
+ }
129
+ }
130
+ }
131
+ return super.getTokenFromCode(code);
132
+ }
133
+
134
+ // Read an @ prefixed identifier
135
+ readAtIdentifier() {
136
+ const start = this.pos;
137
+ this.pos++; // skip '@'
138
+
139
+ // Read the identifier part manually
140
+ let word = '';
141
+ while (this.pos < this.input.length) {
142
+ const ch = this.input.charCodeAt(this.pos);
143
+ if (
144
+ (ch >= 65 && ch <= 90) || // A-Z
145
+ (ch >= 97 && ch <= 122) || // a-z
146
+ (ch >= 48 && ch <= 57) || // 0-9
147
+ ch === 95 ||
148
+ ch === 36
149
+ ) {
150
+ // _ or $
151
+ word += this.input[this.pos++];
152
+ } else {
153
+ break;
154
+ }
155
+ }
156
+
157
+ if (word === '') {
158
+ this.raise(start, 'Invalid @ identifier');
159
+ }
160
+
161
+ // Return the full identifier including @
162
+ return this.finishToken(tt.name, '@' + word);
163
+ }
164
+
165
+ // Override parseIdent to mark @ identifiers as tracked
166
+ parseIdent(liberal) {
167
+ const node = super.parseIdent(liberal);
168
+ if (node.name && node.name.startsWith('@')) {
169
+ node.name = node.name.slice(1); // Remove the '@' for internal use
170
+ node.tracked = true;
171
+ node.start++;
172
+ const prev_pos = this.pos;
173
+ this.pos = node.start;
174
+ node.loc.start = this.curPosition();
175
+ this.pos = prev_pos;
176
+ }
177
+ return node;
178
+ }
179
+
180
+ parseExprAtom(refDestructuringErrors, forNew, forInit) {
181
+ // Check if this is a tuple literal starting with #[
182
+ if (this.type === tt.bracketL && this.value === '#[') {
183
+ return this.parseTrackedArrayExpression();
184
+ }
185
+
186
+ return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
187
+ }
188
+
189
+ parseTrackedArrayExpression() {
190
+ const node = this.startNode();
191
+ this.next(); // consume the '#['
192
+
193
+ node.elements = [];
194
+
195
+ // Parse array elements similar to regular array parsing
196
+ let first = true;
197
+ while (!this.eat(tt.bracketR)) {
198
+ if (!first) {
199
+ this.expect(tt.comma);
200
+ if (this.afterTrailingComma(tt.bracketR)) break;
201
+ } else {
202
+ first = false;
203
+ }
204
+
205
+ if (this.type === tt.comma) {
206
+ // Hole in array
207
+ node.elements.push(null);
208
+ } else if (this.type === tt.ellipsis) {
209
+ // Spread element
210
+ const element = this.parseSpread();
211
+ node.elements.push(element);
212
+ if (this.type === tt.comma && this.input.charCodeAt(this.pos) === 93) {
213
+ this.raise(this.pos, 'Trailing comma is not permitted after the rest element');
214
+ }
215
+ } else {
216
+ // Regular element
217
+ node.elements.push(this.parseMaybeAssign(false));
218
+ }
219
+ }
220
+
221
+ return this.finishNode(node, 'TrackedArrayExpression');
222
+ }
223
+
224
+ parseExportDefaultDeclaration() {
225
+ // Check if this is "export default component"
226
+ if (this.value === 'component') {
227
+ const node = this.startNode();
228
+ node.type = 'Component';
229
+ node.css = null;
230
+ node.default = true;
231
+ this.next();
232
+ this.enterScope(0);
233
+
234
+ node.id = this.type.label === 'name' ? this.parseIdent() : null;
235
+
236
+ this.parseFunctionParams(node);
237
+ this.eat(tt.braceL);
238
+ node.body = [];
239
+ this.#path.push(node);
240
+
241
+ this.parseTemplateBody(node.body);
242
+ this.#path.pop();
243
+ this.exitScope();
244
+
245
+ this.next();
246
+ this.finishNode(node, 'Component');
247
+ this.awaitPos = 0;
248
+
249
+ return node;
250
+ }
251
+
252
+ return super.parseExportDefaultDeclaration();
253
+ }
254
+
255
+ parseForStatement(node) {
256
+ this.next();
257
+ let awaitAt =
258
+ this.options.ecmaVersion >= 9 && this.canAwait && this.eatContextual('await')
259
+ ? this.lastTokStart
260
+ : -1;
261
+ this.labels.push({ kind: 'loop' });
262
+ this.enterScope(0);
263
+ this.expect(tt.parenL);
264
+
265
+ if (this.type === tt.semi) {
266
+ if (awaitAt > -1) this.unexpected(awaitAt);
267
+ return this.parseFor(node, null);
268
+ }
269
+
270
+ let isLet = this.isLet();
271
+ if (this.type === tt._var || this.type === tt._const || isLet) {
272
+ let init = this.startNode(),
273
+ kind = isLet ? 'let' : this.value;
274
+ this.next();
275
+ this.parseVar(init, true, kind);
276
+ this.finishNode(init, 'VariableDeclaration');
277
+ return this.parseForAfterInitWithIndex(node, init, awaitAt);
278
+ }
279
+
280
+ // Handle other cases like using declarations if they exist
281
+ let startsWithLet = this.isContextual('let'),
282
+ isForOf = false;
283
+ let usingKind =
284
+ this.isUsing && this.isUsing(true)
285
+ ? 'using'
286
+ : this.isAwaitUsing && this.isAwaitUsing(true)
287
+ ? 'await using'
288
+ : null;
289
+ if (usingKind) {
290
+ let init = this.startNode();
291
+ this.next();
292
+ if (usingKind === 'await using') {
293
+ if (!this.canAwait) {
294
+ this.raise(this.start, 'Await using cannot appear outside of async function');
295
+ }
296
+ this.next();
297
+ }
298
+ this.parseVar(init, true, usingKind);
299
+ this.finishNode(init, 'VariableDeclaration');
300
+ return this.parseForAfterInitWithIndex(node, init, awaitAt);
301
+ }
302
+
303
+ let containsEsc = this.containsEsc;
304
+ let refDestructuringErrors = {};
305
+ let initPos = this.start;
306
+ let init =
307
+ awaitAt > -1
308
+ ? this.parseExprSubscripts(refDestructuringErrors, 'await')
309
+ : this.parseExpression(true, refDestructuringErrors);
310
+
311
+ if (
312
+ this.type === tt._in ||
313
+ (isForOf = this.options.ecmaVersion >= 6 && this.isContextual('of'))
314
+ ) {
315
+ if (awaitAt > -1) {
316
+ // implies `ecmaVersion >= 9`
317
+ if (this.type === tt._in) this.unexpected(awaitAt);
318
+ node.await = true;
319
+ } else if (isForOf && this.options.ecmaVersion >= 8) {
320
+ if (
321
+ init.start === initPos &&
322
+ !containsEsc &&
323
+ init.type === 'Identifier' &&
324
+ init.name === 'async'
325
+ )
326
+ this.unexpected();
327
+ else if (this.options.ecmaVersion >= 9) node.await = false;
328
+ }
329
+ if (startsWithLet && isForOf)
330
+ this.raise(init.start, "The left-hand side of a for-of loop may not start with 'let'.");
331
+ this.toAssignable(init, false, refDestructuringErrors);
332
+ this.checkLValPattern(init);
333
+ return this.parseForInWithIndex(node, init);
334
+ } else {
335
+ this.checkExpressionErrors(refDestructuringErrors, true);
336
+ }
337
+
338
+ if (awaitAt > -1) this.unexpected(awaitAt);
339
+ return this.parseFor(node, init);
340
+ }
341
+
342
+ parseForAfterInitWithIndex(node, init, awaitAt) {
343
+ if (
344
+ (this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual('of'))) &&
345
+ init.declarations.length === 1
346
+ ) {
347
+ if (this.options.ecmaVersion >= 9) {
348
+ if (this.type === tt._in) {
349
+ if (awaitAt > -1) this.unexpected(awaitAt);
350
+ } else node.await = awaitAt > -1;
351
+ }
352
+ return this.parseForInWithIndex(node, init);
353
+ }
354
+ if (awaitAt > -1) this.unexpected(awaitAt);
355
+ return this.parseFor(node, init);
356
+ }
357
+
358
+ parseForInWithIndex(node, init) {
359
+ const isForIn = this.type === tt._in;
360
+ this.next();
361
+
362
+ if (
363
+ init.type === 'VariableDeclaration' &&
364
+ init.declarations[0].init != null &&
365
+ (!isForIn ||
366
+ this.options.ecmaVersion < 8 ||
367
+ this.strict ||
368
+ init.kind !== 'var' ||
369
+ init.declarations[0].id.type !== 'Identifier')
370
+ ) {
371
+ this.raise(
372
+ init.start,
373
+ `${isForIn ? 'for-in' : 'for-of'} loop variable declaration may not have an initializer`,
374
+ );
375
+ }
376
+
377
+ node.left = init;
378
+ node.right = isForIn ? this.parseExpression() : this.parseMaybeAssign();
379
+
380
+ // Check for our extended syntax: "; index varName"
381
+ if (!isForIn && this.type === tt.semi) {
382
+ this.next(); // consume ';'
383
+
384
+ if (this.isContextual('index')) {
385
+ this.next(); // consume 'index'
386
+
387
+ if (this.type === tt.name) {
388
+ node.index = this.parseIdent();
389
+ } else {
390
+ this.raise(this.start, 'Expected identifier after "index" keyword');
391
+ }
392
+ } else {
393
+ this.raise(this.start, 'Expected "index" keyword after semicolon in for-of loop');
394
+ }
395
+ } else if (!isForIn) {
396
+ // Set index to null for standard for-of loops
397
+ node.index = null;
398
+ }
399
+
400
+ this.expect(tt.parenR);
401
+ node.body = this.parseStatement('for');
402
+ this.exitScope();
403
+ this.labels.pop();
404
+ return this.finishNode(node, isForIn ? 'ForInStatement' : 'ForOfStatement');
405
+ }
406
+
407
+ shouldParseExportStatement() {
408
+ if (super.shouldParseExportStatement()) {
409
+ return true;
410
+ }
411
+ if (this.value === 'component') {
412
+ return true;
413
+ }
414
+ return this.type.keyword === 'var';
415
+ }
416
+
417
+ jsx_parseExpressionContainer() {
418
+ let node = this.startNode();
419
+ this.next();
420
+
421
+ node.expression =
422
+ this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
423
+ this.expect(tt.braceR);
424
+ return this.finishNode(node, 'JSXExpressionContainer');
425
+ }
426
+
427
+ jsx_parseTupleContainer() {
428
+ var t = this.startNode();
429
+ return (
430
+ this.next(),
431
+ (t.expression =
432
+ this.type === tt.bracketR ? this.jsx_parseEmptyExpression() : this.parseExpression()),
433
+ this.expect(tt.bracketR),
434
+ this.finishNode(t, 'JSXExpressionContainer')
435
+ );
436
+ }
437
+
438
+ jsx_parseAttribute() {
439
+ let node = this.startNode();
440
+
441
+ if (this.eat(tt.braceL)) {
442
+ if (this.value === 'ref') {
443
+ this.next();
444
+ if (this.type === tt.braceR) {
445
+ this.raise(
446
+ this.start,
447
+ '"ref" is a Ripple keyword and must be used in the form {ref fn}',
448
+ );
449
+ }
450
+ node.argument = this.parseMaybeAssign();
451
+ this.expect(tt.braceR);
452
+ return this.finishNode(node, 'RefAttribute');
453
+ } else if (this.type === tt.ellipsis) {
454
+ this.expect(tt.ellipsis);
455
+ node.argument = this.parseMaybeAssign();
456
+ this.expect(tt.braceR);
457
+ return this.finishNode(node, 'SpreadAttribute');
458
+ } else if (this.lookahead().type === tt.ellipsis) {
459
+ this.expect(tt.ellipsis);
460
+ node.argument = this.parseMaybeAssign();
461
+ this.expect(tt.braceR);
462
+ return this.finishNode(node, 'SpreadAttribute');
463
+ } else {
464
+ const id = this.parseIdentNode();
465
+ id.tracked = false;
466
+ if (id.name.startsWith('@')) {
467
+ id.tracked = true;
468
+ id.name = id.name.slice(1);
469
+ }
470
+ this.finishNode(id, 'Identifier');
471
+ node.name = id;
472
+ node.value = id;
473
+ this.next();
474
+ this.expect(tt.braceR);
475
+ return this.finishNode(node, 'Attribute');
476
+ }
477
+ }
478
+ node.name = this.jsx_parseNamespacedName();
479
+ node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null;
480
+ return this.finishNode(node, 'JSXAttribute');
481
+ }
482
+
483
+ jsx_parseNamespacedName() {
484
+ const base = this.jsx_parseIdentifier();
485
+ if (!this.eat(tt.colon)) return base;
486
+ const node = this.startNodeAt(base.start, base.loc.start);
487
+ node.namespace = base;
488
+ node.name = this.jsx_parseIdentifier();
489
+ return this.finishNode(node, 'JSXNamespacedName');
490
+ }
491
+
492
+ jsx_parseIdentifier() {
493
+ const node = this.startNode();
494
+
495
+ if (this.type.label === '@') {
496
+ this.next(); // consume @
497
+
498
+ if (this.type === tt.name || this.type.label === 'jsxName') {
499
+ node.name = this.value;
500
+ node.tracked = true;
501
+ this.next();
502
+ } else {
503
+ // Unexpected token after @
504
+ this.unexpected();
505
+ }
506
+ } else if (
507
+ (this.type === tt.name || this.type.label === 'jsxName') &&
508
+ this.value &&
509
+ this.value.startsWith('@')
510
+ ) {
511
+ node.name = this.value.substring(1);
512
+ node.tracked = true;
513
+ this.next();
514
+ } else if (this.type === tt.name || this.type.keyword || this.type.label === 'jsxName') {
515
+ node.name = this.value;
516
+ node.tracked = false; // Explicitly mark as not tracked
517
+ this.next();
518
+ } else {
519
+ return super.jsx_parseIdentifier();
520
+ }
521
+
522
+ return this.finishNode(node, 'JSXIdentifier');
523
+ }
524
+
525
+ // Override jsx_parseElementName to support @ syntax in member expressions
526
+ jsx_parseElementName() {
527
+ let node = this.jsx_parseIdentifier();
528
+ if (this.eat(tt.dot)) {
529
+ let memberExpr = this.startNodeAt(node.start, node.loc && node.loc.start);
530
+ memberExpr.object = node;
531
+ memberExpr.property = this.jsx_parseIdentifier();
532
+ memberExpr.computed = false;
533
+ while (this.eat(tt.dot)) {
534
+ let newMemberExpr = this.startNodeAt(
535
+ memberExpr.start,
536
+ memberExpr.loc && memberExpr.loc.start,
537
+ );
538
+ newMemberExpr.object = memberExpr;
539
+ newMemberExpr.property = this.jsx_parseIdentifier();
540
+ newMemberExpr.computed = false;
541
+ memberExpr = this.finishNode(newMemberExpr, 'JSXMemberExpression');
542
+ }
543
+ return this.finishNode(memberExpr, 'JSXMemberExpression');
544
+ }
545
+ return node;
546
+ }
547
+
548
+ jsx_parseAttributeValue() {
549
+ const tok = this.acornTypeScript.tokTypes;
550
+
551
+ switch (this.type) {
552
+ case tt.braceL:
553
+ var t = this.jsx_parseExpressionContainer();
554
+ return (
555
+ 'JSXEmptyExpression' === t.expression.type &&
556
+ this.raise(t.start, 'attributes must only be assigned a non-empty expression'),
557
+ t
558
+ );
559
+ case tok.jsxTagStart:
560
+ case tt.string:
561
+ return this.parseExprAtom();
562
+ default:
563
+ this.raise(this.start, 'value should be either an expression or a quoted text');
564
+ }
565
+ }
566
+
567
+ parseTryStatement(node) {
568
+ this.next();
569
+ node.block = this.parseBlock();
570
+ node.handler = null;
571
+
572
+ if (this.value === 'pending') {
573
+ this.next();
574
+ node.pending = this.parseBlock();
575
+ } else {
576
+ node.pending = null;
577
+ }
578
+
579
+ if (this.type === tt._catch) {
580
+ var clause = this.startNode();
581
+ this.next();
582
+ if (this.eat(tt.parenL)) {
583
+ clause.param = this.parseCatchClauseParam();
584
+ } else {
585
+ if (this.options.ecmaVersion < 10) {
586
+ this.unexpected();
587
+ }
588
+ clause.param = null;
589
+ this.enterScope(0);
590
+ }
591
+ clause.body = this.parseBlock(false);
592
+ this.exitScope();
593
+ node.handler = this.finishNode(clause, 'CatchClause');
594
+ }
595
+ node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null;
596
+
597
+ if (!node.handler && !node.finalizer && !node.pending) {
598
+ this.raise(node.start, 'Missing catch or finally clause');
599
+ }
600
+ return this.finishNode(node, 'TryStatement');
601
+ }
602
+
603
+ jsx_readToken() {
604
+ let out = '',
605
+ chunkStart = this.pos;
606
+ const tok = this.acornTypeScript.tokTypes;
607
+
608
+ for (;;) {
609
+ if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
610
+ let ch = this.input.charCodeAt(this.pos);
611
+
612
+ switch (ch) {
613
+ case 60: // '<'
614
+ case 123: // '{'
615
+ if (ch === 60 && this.exprAllowed) {
616
+ ++this.pos;
617
+ return this.finishToken(tok.jsxTagStart);
618
+ }
619
+ if (ch === 123 && this.exprAllowed) {
620
+ return this.getTokenFromCode(ch);
621
+ }
622
+ throw new Error('TODO: Invalid syntax');
623
+
624
+ case 47: // '/'
625
+ // Check if this is a comment (// or /*)
626
+ if (this.input.charCodeAt(this.pos + 1) === 47) {
627
+ // '//'
628
+ // Line comment - handle it properly
629
+ const commentStart = this.pos;
630
+ const startLoc = this.curPosition();
631
+ this.pos += 2;
632
+
633
+ let commentText = '';
634
+ while (this.pos < this.input.length) {
635
+ const nextCh = this.input.charCodeAt(this.pos);
636
+ if (acorn.isNewLine(nextCh)) break;
637
+ commentText += this.input[this.pos];
638
+ this.pos++;
639
+ }
640
+
641
+ const commentEnd = this.pos;
642
+ const endLoc = this.curPosition();
643
+
644
+ // Call onComment if it exists
645
+ if (this.options.onComment) {
646
+ this.options.onComment(
647
+ false,
648
+ commentText,
649
+ commentStart,
650
+ commentEnd,
651
+ startLoc,
652
+ endLoc,
653
+ );
654
+ }
655
+
656
+ // Continue processing from current position
657
+ break;
658
+ } else if (this.input.charCodeAt(this.pos + 1) === 42) {
659
+ // '/*'
660
+ // Block comment - handle it properly
661
+ const commentStart = this.pos;
662
+ const startLoc = this.curPosition();
663
+ this.pos += 2;
664
+
665
+ let commentText = '';
666
+ while (this.pos < this.input.length - 1) {
667
+ if (
668
+ this.input.charCodeAt(this.pos) === 42 &&
669
+ this.input.charCodeAt(this.pos + 1) === 47
670
+ ) {
671
+ this.pos += 2;
672
+ break;
673
+ }
674
+ commentText += this.input[this.pos];
675
+ this.pos++;
676
+ }
677
+
678
+ const commentEnd = this.pos;
679
+ const endLoc = this.curPosition();
680
+
681
+ // Call onComment if it exists
682
+ if (this.options.onComment) {
683
+ this.options.onComment(
684
+ true,
685
+ commentText,
686
+ commentStart,
687
+ commentEnd,
688
+ startLoc,
689
+ endLoc,
690
+ );
691
+ }
692
+
693
+ // Continue processing from current position
694
+ break;
695
+ }
696
+ // If not a comment, fall through to default case
697
+ this.context.push(tc.b_stat);
698
+ this.exprAllowed = true;
699
+ return original.readToken.call(this, ch);
700
+
701
+ case 38: // '&'
702
+ out += this.input.slice(chunkStart, this.pos);
703
+ out += this.jsx_readEntity();
704
+ chunkStart = this.pos;
705
+ break;
706
+
707
+ case 62: // '>'
708
+ case 125: {
709
+ // '}'
710
+ if (
711
+ ch === 125 &&
712
+ (this.#path.length === 0 ||
713
+ this.#path.at(-1)?.type === 'Component' ||
714
+ this.#path.at(-1)?.type === 'Element')
715
+ ) {
716
+ return original.readToken.call(this, ch);
717
+ }
718
+ this.raise(
719
+ this.pos,
720
+ 'Unexpected token `' +
721
+ this.input[this.pos] +
722
+ '`. Did you mean `' +
723
+ (ch === 62 ? '&gt;' : '&rbrace;') +
724
+ '` or ' +
725
+ '`{"' +
726
+ this.input[this.pos] +
727
+ '"}' +
728
+ '`?',
729
+ );
730
+ }
731
+
732
+ default:
733
+ if (acorn.isNewLine(ch)) {
734
+ out += this.input.slice(chunkStart, this.pos);
735
+ out += this.jsx_readNewLine(true);
736
+ chunkStart = this.pos;
737
+ } else if (ch === 32 || ch === 9) {
738
+ ++this.pos;
739
+ } else {
740
+ this.context.push(tc.b_stat);
741
+ this.exprAllowed = true;
742
+ return original.readToken.call(this, ch);
743
+ }
744
+ }
745
+ }
746
+ }
747
+
748
+ parseElement() {
749
+ const inside_head = this.#path.findLast(
750
+ (n) => n.type === 'Element' && n.id.type === 'Identifier' && n.id.name === 'head',
751
+ );
752
+ const tok = this.acornTypeScript.tokTypes;
753
+ // Adjust the start so we capture the `<` as part of the element
754
+ const prev_pos = this.pos;
755
+ this.pos = this.start - 1;
756
+ const position = this.curPosition();
757
+ this.pos = prev_pos;
758
+
759
+ const element = this.startNode();
760
+ element.start = position.index;
761
+ element.loc.start = position;
762
+ element.type = 'Element';
763
+ this.#path.push(element);
764
+ element.children = [];
765
+ const open = this.jsx_parseOpeningElementAt();
766
+ for (const attr of open.attributes) {
767
+ if (attr.type === 'JSXAttribute') {
768
+ attr.type = 'Attribute';
769
+ if (attr.name.type === 'JSXIdentifier') {
770
+ attr.name.type = 'Identifier';
771
+ }
772
+ if (attr.value !== null) {
773
+ if (attr.value.type === 'JSXExpressionContainer') {
774
+ attr.value = attr.value.expression;
775
+ }
776
+ }
777
+ }
778
+ }
779
+ if (open.name.type === 'JSXIdentifier') {
780
+ open.name.type = 'Identifier';
781
+ }
782
+
783
+ element.id = convert_from_jsx(open.name);
784
+ element.attributes = open.attributes;
785
+ element.selfClosing = open.selfClosing;
786
+ element.metadata = {};
787
+
788
+ if (element.selfClosing) {
789
+ this.#path.pop();
790
+
791
+ if (this.type.label === '</>/<=/>=') {
792
+ this.pos--;
793
+ this.next();
794
+ }
795
+ } else {
796
+ if (open.name.name === 'style') {
797
+ // jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
798
+ // So backtrack to the end of the <style> tag to make sure everything is included
799
+ const start = open.end;
800
+ const input = this.input.slice(start);
801
+ const end = input.indexOf('</style>');
802
+ const content = input.slice(0, end);
803
+
804
+ const component = this.#path.findLast((n) => n.type === 'Component');
805
+ if (!inside_head) {
806
+ if (component.css !== null) {
807
+ throw new Error('Components can only have one style tag');
808
+ }
809
+ component.css = parse_style(content);
810
+ }
811
+
812
+ const newLines = content.match(regex_newline_characters)?.length;
813
+ if (newLines) {
814
+ this.curLine = open.loc.end.line + newLines;
815
+ this.lineStart = start + content.lastIndexOf('\n') + 1;
816
+ }
817
+ this.pos = start + content.length + 1;
818
+
819
+ this.type = tok.jsxTagStart;
820
+ this.next();
821
+ if (this.value === '/') {
822
+ this.next();
823
+ this.jsx_parseElementName();
824
+ this.exprAllowed = true;
825
+ this.#path.pop();
826
+ this.next();
827
+ }
828
+ // This node is used for Prettier, we don't actually need
829
+ // the node for Ripple's transform process
830
+ if (!inside_head) {
831
+ element.children = [component.css];
832
+ }
833
+ // Ensure we escape JSX <tag></tag> context
834
+ const tokContexts = this.acornTypeScript.tokContexts;
835
+ const curContext = this.curContext();
836
+
837
+ if (curContext === tokContexts.tc_expr) {
838
+ this.context.pop();
839
+ }
840
+
841
+ element.css = content;
842
+ this.finishNode(element, 'Element');
843
+ return element;
844
+ } else {
845
+ this.enterScope(0);
846
+ this.parseTemplateBody(element.children);
847
+ this.exitScope();
848
+
849
+ // Check if this element was properly closed
850
+ // If we reach here and this element is still in the path, it means it was never closed
851
+ if (this.#path[this.#path.length - 1] === element) {
852
+ const tagName = this.getElementName(element.id);
853
+ this.raise(
854
+ this.start,
855
+ `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
856
+ );
857
+ }
858
+ }
859
+ // Ensure we escape JSX <tag></tag> context
860
+ const tokContexts = this.acornTypeScript.tokContexts;
861
+ const curContext = this.curContext();
862
+
863
+ if (curContext === tokContexts.tc_expr) {
864
+ this.context.pop();
865
+ }
866
+ }
867
+
868
+ this.finishNode(element, 'Element');
869
+ return element;
870
+ }
871
+
872
+ parseTemplateBody(body) {
873
+ var inside_func =
874
+ this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
875
+
876
+ if (!inside_func) {
877
+ if (this.type.label === 'return') {
878
+ throw new Error('`return` statements are not allowed in components');
879
+ }
880
+ if (this.type.label === 'continue') {
881
+ throw new Error('`continue` statements are not allowed in components');
882
+ }
883
+ if (this.type.label === 'break') {
884
+ throw new Error('`break` statements are not allowed in components');
885
+ }
886
+ }
887
+
888
+ if (this.type.label === '{') {
889
+ const node = this.jsx_parseExpressionContainer();
890
+ node.type = 'Text';
891
+ body.push(node);
892
+ } else if (this.type.label === '}') {
893
+ return;
894
+ } else if (this.type.label === 'jsxTagStart') {
895
+ this.next();
896
+ if (this.value === '/') {
897
+ this.next();
898
+ const closingTag = this.jsx_parseElementName();
899
+ this.exprAllowed = true;
900
+
901
+ // Validate that the closing tag matches the opening tag
902
+ const currentElement = this.#path[this.#path.length - 1];
903
+ if (!currentElement || currentElement.type !== 'Element') {
904
+ this.raise(this.start, 'Unexpected closing tag');
905
+ }
906
+
907
+ const openingTagName = this.getElementName(currentElement.id);
908
+ const closingTagName = this.getElementName(closingTag);
909
+
910
+ if (openingTagName !== closingTagName) {
911
+ this.raise(
912
+ this.start,
913
+ `Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
914
+ );
915
+ }
916
+
917
+ this.#path.pop();
918
+ this.next();
919
+ return;
920
+ }
921
+ const node = this.parseElement();
922
+ if (node !== null) {
923
+ body.push(node);
924
+ }
925
+ } else {
926
+ const node = this.parseStatement(null);
927
+ body.push(node);
928
+ }
929
+ this.parseTemplateBody(body);
930
+ }
931
+
932
+ parseStatement(context, topLevel, exports) {
933
+ const tok = this.acornTypeScript.tokContexts;
934
+
935
+ if (
936
+ context !== 'for' &&
937
+ context !== 'if' &&
938
+ this.context.at(-1) === tc.b_stat &&
939
+ this.type === tt.braceL &&
940
+ this.context.some((c) => c === tok.tc_expr)
941
+ ) {
942
+ this.next();
943
+ const node = this.jsx_parseExpressionContainer();
944
+ node.type = 'Text';
945
+ this.next();
946
+ this.context.pop();
947
+ this.context.pop();
948
+ return node;
949
+ }
950
+
951
+ if (this.value === 'component') {
952
+ const node = this.startNode();
953
+ node.type = 'Component';
954
+ node.css = null;
955
+ this.next();
956
+ this.enterScope(0);
957
+ node.id = this.parseIdent();
958
+ this.parseFunctionParams(node);
959
+ this.eat(tt.braceL);
960
+ node.body = [];
961
+ this.#path.push(node);
962
+
963
+ this.parseTemplateBody(node.body);
964
+
965
+ this.#path.pop();
966
+ this.exitScope();
967
+
968
+ this.next();
969
+ this.finishNode(node, 'Component');
970
+ this.awaitPos = 0;
971
+
972
+ return node;
973
+ }
974
+
975
+ if (this.type.label === '@') {
976
+ // Try to parse as an expression statement first using tryParse
977
+ // This allows us to handle Ripple @ syntax like @count++ without
978
+ // interfering with legitimate decorator syntax
979
+ this.skip_decorator = true;
980
+ const expressionResult = this.tryParse(() => {
981
+ const node = this.startNode();
982
+ this.next();
983
+ // Force expression context to ensure @ is tokenized correctly
984
+ const old_expr_allowed = this.exprAllowed;
985
+ this.exprAllowed = true;
986
+ node.expression = this.parseExpression();
987
+
988
+ if (node.expression.type === 'UpdateExpression') {
989
+ let object = node.expression.argument;
990
+ while (object.type === 'MemberExpression') {
991
+ object = object.object;
992
+ }
993
+ if (object.type === 'Identifier') {
994
+ object.tracked = true;
995
+ }
996
+ } else if (node.expression.type === 'AssignmentExpression') {
997
+ let object = node.expression.left;
998
+ while (object.type === 'MemberExpression') {
999
+ object = object.object;
1000
+ }
1001
+ if (object.type === 'Identifier') {
1002
+ object.tracked = true;
1003
+ }
1004
+ } else if (node.expression.type === 'Identifier') {
1005
+ node.expression.tracked = true;
1006
+ } else {
1007
+ // TODO?
1008
+ }
1009
+
1010
+ this.exprAllowed = old_expr_allowed;
1011
+ return this.finishNode(node, 'ExpressionStatement');
1012
+ });
1013
+ this.skip_decorator = false;
1014
+
1015
+ // If parsing as expression statement succeeded, use that result
1016
+ if (expressionResult.node) {
1017
+ return expressionResult.node;
1018
+ }
1019
+ }
1020
+
1021
+ return super.parseStatement(context, topLevel, exports);
1022
+ }
1023
+
1024
+ parseBlock(createNewLexicalScope, node, exitStrict) {
1025
+ const parent = this.#path.at(-1);
1026
+
1027
+ if (parent?.type === 'Component' || parent?.type === 'Element') {
1028
+ if (createNewLexicalScope === void 0) createNewLexicalScope = true;
1029
+ if (node === void 0) node = this.startNode();
1030
+
1031
+ node.body = [];
1032
+ this.expect(tt.braceL);
1033
+ if (createNewLexicalScope) {
1034
+ this.enterScope(0);
1035
+ }
1036
+ this.parseTemplateBody(node.body);
1037
+
1038
+ if (exitStrict) {
1039
+ this.strict = false;
1040
+ }
1041
+ this.exprAllowed = true;
1042
+
1043
+ this.next();
1044
+ if (createNewLexicalScope) {
1045
+ this.exitScope();
1046
+ }
1047
+ return this.finishNode(node, 'BlockStatement');
1048
+ }
1049
+
1050
+ return super.parseBlock(createNewLexicalScope, node, exitStrict);
1051
+ }
1052
+ }
1053
+
1054
+ return RippleParser;
1055
+ };
1026
1056
  }
1027
1057
 
1028
1058
  /**
@@ -1034,115 +1064,115 @@ function RipplePlugin(config) {
1034
1064
  * @param {number} index
1035
1065
  */
1036
1066
  function get_comment_handlers(source, comments, index = 0) {
1037
- return {
1038
- onComment: (block, value, start, end, start_loc, end_loc) => {
1039
- if (block && /\n/.test(value)) {
1040
- let a = start;
1041
- while (a > 0 && source[a - 1] !== '\n') a -= 1;
1042
-
1043
- let b = a;
1044
- while (/[ \t]/.test(source[b])) b += 1;
1045
-
1046
- const indentation = source.slice(a, b);
1047
- value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
1048
- }
1049
-
1050
- comments.push({
1051
- type: block ? 'Block' : 'Line',
1052
- value,
1053
- start,
1054
- end,
1055
- loc: {
1056
- start: /** @type {import('acorn').Position} */ (start_loc),
1057
- end: /** @type {import('acorn').Position} */ (end_loc),
1058
- },
1059
- });
1060
- },
1061
- add_comments: (ast) => {
1062
- if (comments.length === 0) return;
1063
-
1064
- comments = comments
1065
- .filter((comment) => comment.start >= index)
1066
- .map(({ type, value, start, end }) => ({ type, value, start, end }));
1067
-
1068
- walk(ast, null, {
1069
- _(node, { next, path }) {
1070
- let comment;
1071
-
1072
- while (comments[0] && comments[0].start < node.start) {
1073
- comment = /** @type {CommentWithLocation} */ (comments.shift());
1074
- (node.leadingComments ||= []).push(comment);
1075
- }
1076
-
1077
- next();
1078
-
1079
- if (comments[0]) {
1080
- if (node.type === 'BlockStatement' && node.body.length === 0) {
1081
- if (comments[0].start < node.end && comments[0].end < node.end) {
1082
- comment = /** @type {CommentWithLocation} */ (comments.shift());
1083
- (node.innerComments ||= []).push(comment);
1084
- return;
1085
- }
1086
- }
1087
- const parent = /** @type {any} */ (path.at(-1));
1088
-
1089
- if (parent === undefined || node.end !== parent.end) {
1090
- const slice = source.slice(node.end, comments[0].start);
1091
- const is_last_in_body =
1092
- ((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
1093
- parent.body.indexOf(node) === parent.body.length - 1) ||
1094
- (parent?.type === 'ArrayExpression' &&
1095
- parent.elements.indexOf(node) === parent.elements.length - 1) ||
1096
- (parent?.type === 'ObjectExpression' &&
1097
- parent.properties.indexOf(node) === parent.properties.length - 1);
1098
-
1099
- if (is_last_in_body) {
1100
- // Special case: There can be multiple trailing comments after the last node in a block,
1101
- // and they can be separated by newlines
1102
- let end = node.end;
1103
-
1104
- while (comments.length) {
1105
- const comment = comments[0];
1106
- if (parent && comment.start >= parent.end) break;
1107
-
1108
- (node.trailingComments ||= []).push(comment);
1109
- comments.shift();
1110
- end = comment.end;
1111
- }
1112
- } else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
1113
- node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
1114
- }
1115
- }
1116
- }
1117
- },
1118
- });
1119
-
1120
- // Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
1121
- // Adding them ensures that we can later detect the end of the expression tag correctly.
1122
- if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
1123
- (ast.trailingComments ||= []).push(...comments.splice(0));
1124
- }
1125
- },
1126
- };
1067
+ return {
1068
+ onComment: (block, value, start, end, start_loc, end_loc) => {
1069
+ if (block && /\n/.test(value)) {
1070
+ let a = start;
1071
+ while (a > 0 && source[a - 1] !== '\n') a -= 1;
1072
+
1073
+ let b = a;
1074
+ while (/[ \t]/.test(source[b])) b += 1;
1075
+
1076
+ const indentation = source.slice(a, b);
1077
+ value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
1078
+ }
1079
+
1080
+ comments.push({
1081
+ type: block ? 'Block' : 'Line',
1082
+ value,
1083
+ start,
1084
+ end,
1085
+ loc: {
1086
+ start: /** @type {import('acorn').Position} */ (start_loc),
1087
+ end: /** @type {import('acorn').Position} */ (end_loc),
1088
+ },
1089
+ });
1090
+ },
1091
+ add_comments: (ast) => {
1092
+ if (comments.length === 0) return;
1093
+
1094
+ comments = comments
1095
+ .filter((comment) => comment.start >= index)
1096
+ .map(({ type, value, start, end }) => ({ type, value, start, end }));
1097
+
1098
+ walk(ast, null, {
1099
+ _(node, { next, path }) {
1100
+ let comment;
1101
+
1102
+ while (comments[0] && comments[0].start < node.start) {
1103
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
1104
+ (node.leadingComments ||= []).push(comment);
1105
+ }
1106
+
1107
+ next();
1108
+
1109
+ if (comments[0]) {
1110
+ if (node.type === 'BlockStatement' && node.body.length === 0) {
1111
+ if (comments[0].start < node.end && comments[0].end < node.end) {
1112
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
1113
+ (node.innerComments ||= []).push(comment);
1114
+ return;
1115
+ }
1116
+ }
1117
+ const parent = /** @type {any} */ (path.at(-1));
1118
+
1119
+ if (parent === undefined || node.end !== parent.end) {
1120
+ const slice = source.slice(node.end, comments[0].start);
1121
+ const is_last_in_body =
1122
+ ((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
1123
+ parent.body.indexOf(node) === parent.body.length - 1) ||
1124
+ (parent?.type === 'ArrayExpression' &&
1125
+ parent.elements.indexOf(node) === parent.elements.length - 1) ||
1126
+ (parent?.type === 'ObjectExpression' &&
1127
+ parent.properties.indexOf(node) === parent.properties.length - 1);
1128
+
1129
+ if (is_last_in_body) {
1130
+ // Special case: There can be multiple trailing comments after the last node in a block,
1131
+ // and they can be separated by newlines
1132
+ let end = node.end;
1133
+
1134
+ while (comments.length) {
1135
+ const comment = comments[0];
1136
+ if (parent && comment.start >= parent.end) break;
1137
+
1138
+ (node.trailingComments ||= []).push(comment);
1139
+ comments.shift();
1140
+ end = comment.end;
1141
+ }
1142
+ } else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
1143
+ node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
1144
+ }
1145
+ }
1146
+ }
1147
+ },
1148
+ });
1149
+
1150
+ // Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
1151
+ // Adding them ensures that we can later detect the end of the expression tag correctly.
1152
+ if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
1153
+ (ast.trailingComments ||= []).push(...comments.splice(0));
1154
+ }
1155
+ },
1156
+ };
1127
1157
  }
1128
1158
 
1129
1159
  export function parse(source) {
1130
- const comments = [];
1131
- const { onComment, add_comments } = get_comment_handlers(source, comments);
1132
- let ast;
1133
-
1134
- try {
1135
- ast = parser.parse(source, {
1136
- sourceType: 'module',
1137
- ecmaVersion: 13,
1138
- locations: true,
1139
- onComment,
1140
- });
1141
- } catch (e) {
1142
- throw e;
1143
- }
1144
-
1145
- add_comments(ast);
1146
-
1147
- return ast;
1160
+ const comments = [];
1161
+ const { onComment, add_comments } = get_comment_handlers(source, comments);
1162
+ let ast;
1163
+
1164
+ try {
1165
+ ast = parser.parse(source, {
1166
+ sourceType: 'module',
1167
+ ecmaVersion: 13,
1168
+ locations: true,
1169
+ onComment,
1170
+ });
1171
+ } catch (e) {
1172
+ throw e;
1173
+ }
1174
+
1175
+ add_comments(ast);
1176
+
1177
+ return ast;
1148
1178
  }