ripple 0.3.13 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/package.json +5 -30
- package/src/runtime/array.js +38 -38
- package/src/runtime/create-subscriber.js +2 -2
- package/src/runtime/internal/client/bindings.js +4 -6
- package/src/runtime/internal/client/events.js +8 -3
- package/src/runtime/internal/client/hmr.js +5 -17
- package/src/runtime/internal/client/runtime.js +1 -0
- package/src/runtime/internal/server/blocks.js +7 -9
- package/src/runtime/internal/server/index.js +14 -22
- package/src/runtime/media-query.js +34 -33
- package/src/runtime/object.js +7 -10
- package/src/runtime/proxy.js +2 -3
- package/src/runtime/reactive-value.js +23 -21
- package/src/utils/ast.js +1 -1
- package/src/utils/attributes.js +43 -0
- package/src/utils/builders.js +2 -2
- package/tests/client/basic/basic.components.test.rsrx +103 -1
- package/tests/client/basic/basic.errors.test.rsrx +1 -1
- package/tests/client/basic/basic.styling.test.rsrx +1 -1
- package/tests/client/compiler/compiler.assignments.test.rsrx +1 -1
- package/tests/client/compiler/compiler.attributes.test.rsrx +1 -1
- package/tests/client/compiler/compiler.basic.test.rsrx +51 -14
- package/tests/client/compiler/compiler.tracked-access.test.rsrx +1 -1
- package/tests/client/compiler/compiler.try-in-function.test.rsrx +1 -1
- package/tests/client/compiler/compiler.typescript.test.rsrx +1 -1
- package/tests/client/css/global-additional-cases.test.rsrx +1 -1
- package/tests/client/css/global-advanced-selectors.test.rsrx +1 -1
- package/tests/client/css/global-at-rules.test.rsrx +1 -1
- package/tests/client/css/global-basic.test.rsrx +1 -1
- package/tests/client/css/global-classes-ids.test.rsrx +1 -1
- package/tests/client/css/global-combinators.test.rsrx +1 -1
- package/tests/client/css/global-complex-nesting.test.rsrx +1 -1
- package/tests/client/css/global-edge-cases.test.rsrx +1 -1
- package/tests/client/css/global-keyframes.test.rsrx +1 -1
- package/tests/client/css/global-nested.test.rsrx +1 -1
- package/tests/client/css/global-pseudo.test.rsrx +1 -1
- package/tests/client/css/global-scoping.test.rsrx +1 -1
- package/tests/client/css/style-identifier.test.rsrx +1 -1
- package/tests/client/return.test.rsrx +1 -1
- package/tests/hydration/build-components.js +1 -1
- package/tests/server/basic.components.test.rsrx +114 -0
- package/tests/server/compiler.test.rsrx +38 -1
- package/tests/server/style-identifier.test.rsrx +1 -1
- package/tests/setup-server.js +1 -1
- package/tests/utils/compiler-compat-config.test.js +1 -1
- package/types/index.d.ts +1 -1
- package/src/compiler/comment-utils.js +0 -91
- package/src/compiler/errors.js +0 -77
- package/src/compiler/identifier-utils.js +0 -80
- package/src/compiler/index.d.ts +0 -127
- package/src/compiler/index.js +0 -89
- package/src/compiler/phases/1-parse/index.js +0 -3007
- package/src/compiler/phases/1-parse/style.js +0 -704
- package/src/compiler/phases/2-analyze/css-analyze.js +0 -160
- package/src/compiler/phases/2-analyze/index.js +0 -2208
- package/src/compiler/phases/2-analyze/prune.js +0 -1131
- package/src/compiler/phases/2-analyze/validation.js +0 -168
- package/src/compiler/phases/3-transform/client/index.js +0 -5264
- package/src/compiler/phases/3-transform/segments.js +0 -2125
- package/src/compiler/phases/3-transform/server/index.js +0 -1749
- package/src/compiler/phases/3-transform/stylesheet.js +0 -545
- package/src/compiler/scope.js +0 -476
- package/src/compiler/source-map-utils.js +0 -358
- package/src/compiler/types/acorn.d.ts +0 -11
- package/src/compiler/types/estree-jsx.d.ts +0 -11
- package/src/compiler/types/estree.d.ts +0 -11
- package/src/compiler/types/index.d.ts +0 -1411
- package/src/compiler/types/parse.d.ts +0 -1723
- package/src/compiler/utils.js +0 -1258
|
@@ -1,3007 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
@import * as AST from 'estree'
|
|
3
|
-
@import * as ESTreeJSX from 'estree-jsx'
|
|
4
|
-
@import { Parse } from '#parser'
|
|
5
|
-
@import { RipplePluginConfig } from '#compiler';
|
|
6
|
-
@import { ParseOptions, RippleCompileError } from 'ripple/compiler'
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as acorn from 'acorn';
|
|
10
|
-
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
|
11
|
-
import { parse_style } from './style.js';
|
|
12
|
-
import { walk } from 'zimmerframe';
|
|
13
|
-
import { regex_newline_characters } from '../../../utils/patterns.js';
|
|
14
|
-
import { error } from '../../errors.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @typedef {(BaseParser: typeof acorn.Parser) => typeof acorn.Parser} AcornPlugin
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const parser = /** @type {Parse.ParserConstructor} */ (
|
|
21
|
-
/** @type {unknown} */ (
|
|
22
|
-
acorn.Parser.extend(
|
|
23
|
-
tsPlugin({ jsx: true }),
|
|
24
|
-
/** @type {AcornPlugin} */ (/** @type {unknown} */ (RipplePlugin())),
|
|
25
|
-
)
|
|
26
|
-
)
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
/** @type {Parse.BindingType} */
|
|
30
|
-
const BINDING_TYPES = {
|
|
31
|
-
BIND_NONE: 0, // Not a binding
|
|
32
|
-
BIND_VAR: 1, // Var-style binding
|
|
33
|
-
BIND_LEXICAL: 2, // Let- or const-style binding
|
|
34
|
-
BIND_FUNCTION: 3, // Function declaration
|
|
35
|
-
BIND_SIMPLE_CATCH: 4, // Simple (identifier pattern) catch binding
|
|
36
|
-
BIND_OUTSIDE: 5, // Special case for function names as bound inside the function
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @this {Parse.DestructuringErrors}
|
|
41
|
-
* @returns {Parse.DestructuringErrors}
|
|
42
|
-
*/
|
|
43
|
-
function DestructuringErrors() {
|
|
44
|
-
if (!(this instanceof DestructuringErrors)) {
|
|
45
|
-
throw new TypeError("'DestructuringErrors' must be invoked with 'new'");
|
|
46
|
-
}
|
|
47
|
-
this.shorthandAssign = -1;
|
|
48
|
-
this.trailingComma = -1;
|
|
49
|
-
this.parenthesizedAssign = -1;
|
|
50
|
-
this.parenthesizedBind = -1;
|
|
51
|
-
this.doubleProto = -1;
|
|
52
|
-
return this;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Convert JSX node types to regular JavaScript node types
|
|
57
|
-
* @param {ESTreeJSX.JSXIdentifier | ESTreeJSX.JSXMemberExpression | AST.Node} node - The JSX node to convert
|
|
58
|
-
* @returns {AST.Identifier | AST.MemberExpression | AST.Node} The converted node
|
|
59
|
-
*/
|
|
60
|
-
function convert_from_jsx(node) {
|
|
61
|
-
/** @type {AST.Identifier | AST.MemberExpression | AST.Node} */
|
|
62
|
-
let converted_node;
|
|
63
|
-
if (node.type === 'JSXIdentifier') {
|
|
64
|
-
converted_node = /** @type {AST.Identifier} */ (/** @type {unknown} */ (node));
|
|
65
|
-
converted_node.type = 'Identifier';
|
|
66
|
-
} else if (node.type === 'JSXMemberExpression') {
|
|
67
|
-
converted_node = /** @type {AST.MemberExpression} */ (/** @type {unknown} */ (node));
|
|
68
|
-
converted_node.type = 'MemberExpression';
|
|
69
|
-
converted_node.object = /** @type {AST.Identifier | AST.MemberExpression} */ (
|
|
70
|
-
convert_from_jsx(converted_node.object)
|
|
71
|
-
);
|
|
72
|
-
converted_node.property = /** @type {AST.Identifier} */ (
|
|
73
|
-
convert_from_jsx(converted_node.property)
|
|
74
|
-
);
|
|
75
|
-
} else {
|
|
76
|
-
converted_node = node;
|
|
77
|
-
}
|
|
78
|
-
return converted_node;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const regex_whitespace_only = /\s/;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Skip whitespace characters without skipping comments.
|
|
85
|
-
* This is needed because Acorn's skipSpace() also skips comments, which breaks
|
|
86
|
-
* parsing in certain contexts. Updates parser position and line tracking.
|
|
87
|
-
* @param {Parse.Parser} parser
|
|
88
|
-
*/
|
|
89
|
-
function skipWhitespace(parser) {
|
|
90
|
-
const originalStart = parser.start;
|
|
91
|
-
/** @type {acorn.Position | undefined} */
|
|
92
|
-
let lineInfo;
|
|
93
|
-
while (
|
|
94
|
-
parser.start < parser.input.length &&
|
|
95
|
-
regex_whitespace_only.test(parser.input[parser.start])
|
|
96
|
-
) {
|
|
97
|
-
parser.start++;
|
|
98
|
-
}
|
|
99
|
-
// Update line tracking if whitespace was skipped
|
|
100
|
-
if (parser.start !== originalStart) {
|
|
101
|
-
lineInfo = acorn.getLineInfo(parser.input, parser.start);
|
|
102
|
-
// Only update curLine/lineStart if the tokenizer hasn't already
|
|
103
|
-
// advanced past this position. When parser.pos > parser.start,
|
|
104
|
-
// acorn's internal skipSpace() has already processed comments and
|
|
105
|
-
// whitespace beyond where we stopped, so curLine/lineStart already
|
|
106
|
-
// reflect a later (correct) position that we must not overwrite.
|
|
107
|
-
if (parser.pos <= parser.start) {
|
|
108
|
-
parser.curLine = lineInfo.line;
|
|
109
|
-
parser.lineStart = parser.start - lineInfo.column;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// After skipping whitespace, update startLoc to reflect our actual position
|
|
114
|
-
// so the next node's start location is correct
|
|
115
|
-
parser.startLoc = lineInfo || acorn.getLineInfo(parser.input, parser.start);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* @param {AST.Node | null | undefined} node
|
|
120
|
-
* @returns {boolean}
|
|
121
|
-
*/
|
|
122
|
-
function isWhitespaceTextNode(node) {
|
|
123
|
-
if (!node || node.type !== 'Text') {
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const expr = node.expression;
|
|
128
|
-
if (expr && expr.type === 'Literal' && typeof expr.value === 'string') {
|
|
129
|
-
return /^\s*$/.test(expr.value);
|
|
130
|
-
}
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* @param {AST.Element} element
|
|
136
|
-
* @param {ESTreeJSX.JSXOpeningElement} open
|
|
137
|
-
*/
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Acorn parser plugin for Ripple syntax extensions
|
|
141
|
-
* @param {RipplePluginConfig} [config] - Plugin configuration
|
|
142
|
-
* @returns {(Parser: Parse.ParserConstructor) => Parse.ParserConstructor} Parser extension function
|
|
143
|
-
*/
|
|
144
|
-
function RipplePlugin(config) {
|
|
145
|
-
return (/** @type {Parse.ParserConstructor} */ Parser) => {
|
|
146
|
-
const original = acorn.Parser.prototype;
|
|
147
|
-
const tt = Parser.tokTypes || acorn.tokTypes;
|
|
148
|
-
const tc = Parser.tokContexts || acorn.tokContexts;
|
|
149
|
-
// Some parser constructors (e.g. via TS plugins) expose `tokContexts` without `b_stat`.
|
|
150
|
-
// If we push an undefined context, Acorn's tokenizer will later crash reading `.override`.
|
|
151
|
-
const b_stat = tc.b_stat || acorn.tokContexts.b_stat;
|
|
152
|
-
const tstt = Parser.acornTypeScript.tokTypes;
|
|
153
|
-
const tstc = Parser.acornTypeScript.tokContexts;
|
|
154
|
-
|
|
155
|
-
class RippleParser extends Parser {
|
|
156
|
-
/** @type {AST.Node[]} */
|
|
157
|
-
#path = [];
|
|
158
|
-
#commentContextId = 0;
|
|
159
|
-
#loose = false;
|
|
160
|
-
/** @type {RippleCompileError[] | undefined} */
|
|
161
|
-
#errors = undefined;
|
|
162
|
-
/** @type {string | null} */
|
|
163
|
-
#filename = null;
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* @param {Parse.Options} options
|
|
167
|
-
* @param {string} input
|
|
168
|
-
*/
|
|
169
|
-
constructor(options, input) {
|
|
170
|
-
super(options, input);
|
|
171
|
-
this.#loose = options?.rippleOptions.loose === true;
|
|
172
|
-
this.#errors = options?.rippleOptions.errors;
|
|
173
|
-
this.#filename = options?.rippleOptions.filename || null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* @param {number} position
|
|
178
|
-
* @param {string} message
|
|
179
|
-
*/
|
|
180
|
-
#report_recoverable_error(position, message) {
|
|
181
|
-
const start = Math.max(0, Math.min(position, this.input.length));
|
|
182
|
-
const end = Math.min(this.input.length, start + 1);
|
|
183
|
-
const start_loc = acorn.getLineInfo(this.input, start);
|
|
184
|
-
const end_loc = acorn.getLineInfo(this.input, end);
|
|
185
|
-
|
|
186
|
-
error(
|
|
187
|
-
message,
|
|
188
|
-
this.#filename,
|
|
189
|
-
/** @type {AST.NodeWithLocation} */ ({
|
|
190
|
-
start,
|
|
191
|
-
end,
|
|
192
|
-
loc: {
|
|
193
|
-
start: start_loc,
|
|
194
|
-
end: end_loc,
|
|
195
|
-
},
|
|
196
|
-
}),
|
|
197
|
-
this.#loose ? this.#errors : undefined,
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* In loose mode, keep parsing after duplicate declaration diagnostics so
|
|
203
|
-
* editor tooling can continue producing AST and mappings.
|
|
204
|
-
* @param {number} position
|
|
205
|
-
* @param {string | { message?: string }} message
|
|
206
|
-
*/
|
|
207
|
-
raiseRecoverable(position, message) {
|
|
208
|
-
const error_message =
|
|
209
|
-
typeof message === 'string'
|
|
210
|
-
? message
|
|
211
|
-
: typeof message?.message === 'string'
|
|
212
|
-
? message.message
|
|
213
|
-
: String(message);
|
|
214
|
-
|
|
215
|
-
if (error_message.includes('has already been declared')) {
|
|
216
|
-
this.#report_recoverable_error(position, error_message);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return super.raiseRecoverable(position, error_message);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Override to allow single-parameter generic arrow functions without trailing comma.
|
|
225
|
-
* By default, @sveltejs/acorn-typescript throws an error for `<T>() => {}` when JSX is enabled
|
|
226
|
-
* because it can't disambiguate from JSX. However, the parser still parses it correctly
|
|
227
|
-
* using tryParse - it just throws afterwards. By overriding this to do nothing, we allow
|
|
228
|
-
* the valid parse to succeed.
|
|
229
|
-
* @param {AST.TSTypeParameterDeclaration} node
|
|
230
|
-
*/
|
|
231
|
-
reportReservedArrowTypeParam(node) {
|
|
232
|
-
// Allow <T>() => {} syntax without requiring trailing comma
|
|
233
|
-
if (this.#loose && node.params.length === 1 && node.extra?.trailingComma === undefined) {
|
|
234
|
-
error(
|
|
235
|
-
'This syntax is reserved in files with the .mts or .cts extension. Add a trailing comma, as in `<T,>() => ...`.',
|
|
236
|
-
this.#filename,
|
|
237
|
-
node,
|
|
238
|
-
this.#errors,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Override to allow `readonly` type modifier on any type in loose mode.
|
|
245
|
-
* By default, @sveltejs/acorn-typescript throws an error for `readonly { ... }`
|
|
246
|
-
* because TypeScript only permits `readonly` on array and tuple types.
|
|
247
|
-
* Suppress the error in the strict mode as ts is compiled away.
|
|
248
|
-
* @param {AST.TSTypeOperator} node
|
|
249
|
-
*/
|
|
250
|
-
tsCheckTypeAnnotationForReadOnly(node) {
|
|
251
|
-
const typeAnnotation = /** @type {AST.TypeNode} */ (node.typeAnnotation);
|
|
252
|
-
if (typeAnnotation.type === 'TSTupleType' || typeAnnotation.type === 'TSArrayType') {
|
|
253
|
-
// Valid readonly usage, no error needed
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (this.#loose) {
|
|
258
|
-
error(
|
|
259
|
-
"'readonly' type modifier is only permitted on array and tuple literal types.",
|
|
260
|
-
this.#filename,
|
|
261
|
-
typeAnnotation,
|
|
262
|
-
this.#errors,
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Override parseProperty to support component methods in object literals.
|
|
269
|
-
* Handles syntax like `{ component something() { <div /> } }`
|
|
270
|
-
* Also supports computed names: `{ component ['something']() { <div /> } }`
|
|
271
|
-
* @type {Parse.Parser['parseProperty']}
|
|
272
|
-
*/
|
|
273
|
-
parseProperty(isPattern, refDestructuringErrors) {
|
|
274
|
-
// Check if this is a component method: component name( ... ) { ... }
|
|
275
|
-
if (!isPattern && this.type === tt.name && this.value === 'component') {
|
|
276
|
-
// Look ahead to see if this is "component identifier(", "component identifier<", "component [", or "component 'string'"
|
|
277
|
-
const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
|
|
278
|
-
if (lookahead) {
|
|
279
|
-
// This is a component method definition
|
|
280
|
-
const prop = /** @type {AST.Property} */ (this.startNode());
|
|
281
|
-
const isComputed = lookahead[0].trim().startsWith('[');
|
|
282
|
-
const isStringLiteral = /^['"]/.test(lookahead[0].trim());
|
|
283
|
-
|
|
284
|
-
if (isComputed) {
|
|
285
|
-
// For computed names, consume 'component'
|
|
286
|
-
// parse the key, then parse component without name
|
|
287
|
-
this.next(); // consume 'component'
|
|
288
|
-
this.next(); // consume '['
|
|
289
|
-
prop.key = this.parseExpression();
|
|
290
|
-
this.expect(tt.bracketR);
|
|
291
|
-
prop.computed = true;
|
|
292
|
-
|
|
293
|
-
// Parse component without name (skipName: true)
|
|
294
|
-
const component_node = this.parseComponent({ skipName: true });
|
|
295
|
-
/** @type {AST.RippleProperty} */ (prop).value = component_node;
|
|
296
|
-
} else if (isStringLiteral) {
|
|
297
|
-
// For string literal names, consume 'component'
|
|
298
|
-
// parse the string key, then parse component without name
|
|
299
|
-
this.next(); // consume 'component'
|
|
300
|
-
prop.key = /** @type {AST.Literal} */ (this.parseExprAtom());
|
|
301
|
-
prop.computed = false;
|
|
302
|
-
|
|
303
|
-
// Parse component without name (skipName: true)
|
|
304
|
-
const component_node = this.parseComponent({ skipName: true });
|
|
305
|
-
/** @type {AST.RippleProperty} */ (prop).value = component_node;
|
|
306
|
-
} else {
|
|
307
|
-
const component_node = this.parseComponent({ requireName: true });
|
|
308
|
-
|
|
309
|
-
prop.key = /** @type {AST.Identifier} */ (component_node.id);
|
|
310
|
-
/** @type {AST.RippleProperty} */ (prop).value = component_node;
|
|
311
|
-
prop.computed = false;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
prop.shorthand = false;
|
|
315
|
-
prop.method = true;
|
|
316
|
-
prop.kind = 'init';
|
|
317
|
-
|
|
318
|
-
return this.finishNode(prop, 'Property');
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return super.parseProperty(isPattern, refDestructuringErrors);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Override parseClassElement to support component methods in classes.
|
|
327
|
-
* Handles syntax like `class Foo { component something() { <div /> } }`
|
|
328
|
-
* Also supports computed names: `class Foo { component ['something']() { <div /> } }`
|
|
329
|
-
* @type {Parse.Parser['parseClassElement']}
|
|
330
|
-
*/
|
|
331
|
-
parseClassElement(constructorAllowsSuper) {
|
|
332
|
-
// Check if this is a component method: component name( ... ) { ... }
|
|
333
|
-
if (this.type === tt.name && this.value === 'component') {
|
|
334
|
-
// Look ahead to see if this is "component identifier(",
|
|
335
|
-
// "component identifier<", "component [", or "component 'string'"
|
|
336
|
-
const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
|
|
337
|
-
if (lookahead) {
|
|
338
|
-
// This is a component method definition
|
|
339
|
-
const node = /** @type {AST.MethodDefinition} */ (this.startNode());
|
|
340
|
-
const isComputed = lookahead[0].trim().startsWith('[');
|
|
341
|
-
const isStringLiteral = /^['"]/.test(lookahead[0].trim());
|
|
342
|
-
|
|
343
|
-
if (isComputed) {
|
|
344
|
-
// For computed names, consume 'component'
|
|
345
|
-
// parse the key, then parse component without name
|
|
346
|
-
this.next(); // consume 'component'
|
|
347
|
-
this.next(); // consume '['
|
|
348
|
-
node.key = this.parseExpression();
|
|
349
|
-
this.expect(tt.bracketR);
|
|
350
|
-
node.computed = true;
|
|
351
|
-
|
|
352
|
-
// Parse component without name (skipName: true)
|
|
353
|
-
const component_node = this.parseComponent({ skipName: true });
|
|
354
|
-
/** @type {AST.RippleMethodDefinition} */ (node).value = component_node;
|
|
355
|
-
} else if (isStringLiteral) {
|
|
356
|
-
// For string literal names, consume 'component'
|
|
357
|
-
// parse the string key, then parse component without name
|
|
358
|
-
this.next(); // consume 'component'
|
|
359
|
-
node.key = /** @type {AST.Literal} */ (this.parseExprAtom());
|
|
360
|
-
node.computed = false;
|
|
361
|
-
|
|
362
|
-
// Parse component without name (skipName: true)
|
|
363
|
-
const component_node = this.parseComponent({ skipName: true });
|
|
364
|
-
/** @type {AST.RippleMethodDefinition} */ (node).value = component_node;
|
|
365
|
-
} else {
|
|
366
|
-
// Use parseComponent which handles consuming 'component', parsing name, params, and body
|
|
367
|
-
const component_node = this.parseComponent({ requireName: true });
|
|
368
|
-
|
|
369
|
-
node.key = /** @type {AST.Identifier} */ (component_node.id);
|
|
370
|
-
/** @type {AST.RippleMethodDefinition} */ (node).value = component_node;
|
|
371
|
-
node.computed = false;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
node.static = false;
|
|
375
|
-
node.kind = 'method';
|
|
376
|
-
|
|
377
|
-
return this.finishNode(node, 'MethodDefinition');
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return super.parseClassElement(constructorAllowsSuper);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Override parsePropertyValue to support TypeScript generic methods in object literals.
|
|
386
|
-
* By default, acorn-typescript doesn't handle `{ method<T>() {} }` syntax.
|
|
387
|
-
* This override checks for type parameters before parsing the method.
|
|
388
|
-
* @type {Parse.Parser['parsePropertyValue']}
|
|
389
|
-
*/
|
|
390
|
-
parsePropertyValue(
|
|
391
|
-
prop,
|
|
392
|
-
isPattern,
|
|
393
|
-
isGenerator,
|
|
394
|
-
isAsync,
|
|
395
|
-
startPos,
|
|
396
|
-
startLoc,
|
|
397
|
-
refDestructuringErrors,
|
|
398
|
-
containsEsc,
|
|
399
|
-
) {
|
|
400
|
-
// Check if this is a method with type parameters (e.g., `method<T>() {}`)
|
|
401
|
-
// We need to parse type parameters before the parentheses
|
|
402
|
-
if (
|
|
403
|
-
!isPattern &&
|
|
404
|
-
!isGenerator &&
|
|
405
|
-
!isAsync &&
|
|
406
|
-
this.type === tt.relational &&
|
|
407
|
-
this.value === '<'
|
|
408
|
-
) {
|
|
409
|
-
// Try to parse type parameters
|
|
410
|
-
const typeParameters = this.tsTryParseTypeParameters();
|
|
411
|
-
if (typeParameters && this.type === tt.parenL) {
|
|
412
|
-
// This is a method with type parameters
|
|
413
|
-
/** @type {AST.Property} */ (prop).method = true;
|
|
414
|
-
/** @type {AST.Property} */ (prop).kind = 'init';
|
|
415
|
-
/** @type {AST.Property} */ (prop).value = this.parseMethod(false, false);
|
|
416
|
-
/** @type {AST.FunctionExpression} */ (
|
|
417
|
-
/** @type {AST.Property} */ (prop).value
|
|
418
|
-
).typeParameters = typeParameters;
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
return super.parsePropertyValue(
|
|
424
|
-
prop,
|
|
425
|
-
isPattern,
|
|
426
|
-
isGenerator,
|
|
427
|
-
isAsync,
|
|
428
|
-
startPos,
|
|
429
|
-
startLoc,
|
|
430
|
-
refDestructuringErrors,
|
|
431
|
-
containsEsc,
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Acorn expects `this.context` to always contain at least one tokContext.
|
|
437
|
-
* Some of our template/JSX escape hatches can pop contexts aggressively;
|
|
438
|
-
* if the stack becomes empty, Acorn will crash reading `curContext().override`.
|
|
439
|
-
* @type {Parse.Parser['nextToken']}
|
|
440
|
-
*/
|
|
441
|
-
nextToken() {
|
|
442
|
-
while (this.context.length && this.context[this.context.length - 1] == null) {
|
|
443
|
-
this.context.pop();
|
|
444
|
-
}
|
|
445
|
-
if (this.context.length === 0) {
|
|
446
|
-
this.context.push(b_stat);
|
|
447
|
-
}
|
|
448
|
-
return super.nextToken();
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* @returns {Parse.CommentMetaData | null}
|
|
453
|
-
*/
|
|
454
|
-
#createCommentMetadata() {
|
|
455
|
-
if (this.#path.length === 0) {
|
|
456
|
-
return null;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const container = this.#path[this.#path.length - 1];
|
|
460
|
-
if (!container || container.type !== 'Element') {
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const children = Array.isArray(container.children) ? container.children : [];
|
|
465
|
-
const hasMeaningfulChildren = children.some(
|
|
466
|
-
(child) => child && !isWhitespaceTextNode(child),
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
if (hasMeaningfulChildren) {
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
container.metadata ??= { path: [] };
|
|
474
|
-
if (container.metadata.commentContainerId === undefined) {
|
|
475
|
-
container.metadata.commentContainerId = ++this.#commentContextId;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return /*** @type {Parse.CommentMetaData} */ ({
|
|
479
|
-
containerId: container.metadata.commentContainerId,
|
|
480
|
-
childIndex: children.length,
|
|
481
|
-
beforeMeaningfulChild: !hasMeaningfulChildren,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Helper method to get the element name from a JSX identifier or member expression
|
|
487
|
-
* @type {Parse.Parser['getElementName']}
|
|
488
|
-
*/
|
|
489
|
-
getElementName(node) {
|
|
490
|
-
if (!node) return null;
|
|
491
|
-
if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
|
|
492
|
-
return node.name;
|
|
493
|
-
} else if (node.type === 'MemberExpression' || node.type === 'JSXMemberExpression') {
|
|
494
|
-
// For components like <Foo.Bar>, return "Foo.Bar"
|
|
495
|
-
return this.getElementName(node.object) + '.' + this.getElementName(node.property);
|
|
496
|
-
}
|
|
497
|
-
return null;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Get token from character code - handles Ripple-specific tokens
|
|
502
|
-
* @type {Parse.Parser['getTokenFromCode']}
|
|
503
|
-
*/
|
|
504
|
-
getTokenFromCode(code) {
|
|
505
|
-
if (code === 60) {
|
|
506
|
-
// < character
|
|
507
|
-
const inComponent = this.#path.findLast((n) => n.type === 'Component');
|
|
508
|
-
/** @type {number | null} */
|
|
509
|
-
let prevNonWhitespaceChar = null;
|
|
510
|
-
|
|
511
|
-
// Check if this could be TypeScript generics instead of JSX
|
|
512
|
-
// TypeScript generics appear after: identifiers, closing parens, 'new' keyword
|
|
513
|
-
// For example: Array<T>, func<T>(), new Map<K,V>(), method<T>()
|
|
514
|
-
// This check applies everywhere, not just inside components
|
|
515
|
-
|
|
516
|
-
// Look back to see what precedes the <
|
|
517
|
-
let lookback = this.pos - 1;
|
|
518
|
-
|
|
519
|
-
// Skip whitespace backwards
|
|
520
|
-
while (lookback >= 0) {
|
|
521
|
-
const ch = this.input.charCodeAt(lookback);
|
|
522
|
-
if (ch !== 32 && ch !== 9) break; // not space or tab
|
|
523
|
-
lookback--;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Check what character/token precedes the <
|
|
527
|
-
if (lookback >= 0) {
|
|
528
|
-
const prevChar = this.input.charCodeAt(lookback);
|
|
529
|
-
prevNonWhitespaceChar = prevChar;
|
|
530
|
-
|
|
531
|
-
// If preceded by identifier character (letter, digit, _, $) or closing paren,
|
|
532
|
-
// this is likely TypeScript generics, not JSX
|
|
533
|
-
const isIdentifierChar =
|
|
534
|
-
(prevChar >= 65 && prevChar <= 90) || // A-Z
|
|
535
|
-
(prevChar >= 97 && prevChar <= 122) || // a-z
|
|
536
|
-
(prevChar >= 48 && prevChar <= 57) || // 0-9
|
|
537
|
-
prevChar === 95 || // _
|
|
538
|
-
prevChar === 36 || // $
|
|
539
|
-
prevChar === 41; // )
|
|
540
|
-
|
|
541
|
-
if (isIdentifierChar) {
|
|
542
|
-
return super.getTokenFromCode(code);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Support parsing standalone template markup at the top-level (outside `component`)
|
|
547
|
-
// for tooling like Prettier, e.g.:
|
|
548
|
-
// <Something>...</Something>\n\n<Child />
|
|
549
|
-
// <head><style>...</style></head>
|
|
550
|
-
// We only do this when '<' is in a tag-like position.
|
|
551
|
-
const nextChar =
|
|
552
|
-
this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
|
|
553
|
-
const isWhitespaceAfterLt =
|
|
554
|
-
nextChar === 32 || nextChar === 9 || nextChar === 10 || nextChar === 13;
|
|
555
|
-
const isTagLikeAfterLt =
|
|
556
|
-
!isWhitespaceAfterLt &&
|
|
557
|
-
(nextChar === 47 || // '/'
|
|
558
|
-
(nextChar >= 65 && nextChar <= 90) || // A-Z
|
|
559
|
-
(nextChar >= 97 && nextChar <= 122)); // a-z
|
|
560
|
-
const prevAllowsTagStart =
|
|
561
|
-
prevNonWhitespaceChar === null ||
|
|
562
|
-
prevNonWhitespaceChar === 10 || // '\n'
|
|
563
|
-
prevNonWhitespaceChar === 13 || // '\r'
|
|
564
|
-
prevNonWhitespaceChar === 123 || // '{'
|
|
565
|
-
prevNonWhitespaceChar === 125 || // '}'
|
|
566
|
-
prevNonWhitespaceChar === 62; // '>'
|
|
567
|
-
|
|
568
|
-
if (!inComponent && prevAllowsTagStart && isTagLikeAfterLt) {
|
|
569
|
-
++this.pos;
|
|
570
|
-
return this.finishToken(tstt.jsxTagStart);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (inComponent) {
|
|
574
|
-
// Inside component template bodies, allow adjacent tags without requiring
|
|
575
|
-
// a newline/indentation before the next '<'. This is important for inputs
|
|
576
|
-
// like `<div />` and `</div><style>...</style>` which Prettier formats.
|
|
577
|
-
if (prevNonWhitespaceChar === 123 /* '{' */ || prevNonWhitespaceChar === 62 /* '>' */) {
|
|
578
|
-
if (!isWhitespaceAfterLt) {
|
|
579
|
-
++this.pos;
|
|
580
|
-
return this.finishToken(tstt.jsxTagStart);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Check if we're inside a nested function (arrow function, function expression, etc.)
|
|
585
|
-
// We need to distinguish between being inside a function vs just being in nested scopes
|
|
586
|
-
// (like for loops, if blocks, JSX elements, etc.)
|
|
587
|
-
const nestedFunctionContext = this.context.some((ctx) => ctx.token === 'function');
|
|
588
|
-
|
|
589
|
-
// Inside nested functions, treat < as relational/generic operator
|
|
590
|
-
// BUT: if the < is followed by /, it's a closing JSX tag, not a less-than operator
|
|
591
|
-
const nextChar =
|
|
592
|
-
this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
|
|
593
|
-
const isClosingTag = nextChar === 47; // '/'
|
|
594
|
-
|
|
595
|
-
if (nestedFunctionContext && !isClosingTag) {
|
|
596
|
-
// Inside function - treat as TypeScript generic, not JSX
|
|
597
|
-
++this.pos;
|
|
598
|
-
return this.finishToken(tt.relational, '<');
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Check if everything before this position on the current line is whitespace
|
|
602
|
-
let lineStart = this.pos - 1;
|
|
603
|
-
while (
|
|
604
|
-
lineStart >= 0 &&
|
|
605
|
-
this.input.charCodeAt(lineStart) !== 10 &&
|
|
606
|
-
this.input.charCodeAt(lineStart) !== 13
|
|
607
|
-
) {
|
|
608
|
-
lineStart--;
|
|
609
|
-
}
|
|
610
|
-
lineStart++; // Move past the newline character
|
|
611
|
-
|
|
612
|
-
// Check if all characters from line start to current position are whitespace
|
|
613
|
-
let allWhitespace = true;
|
|
614
|
-
for (let i = lineStart; i < this.pos; i++) {
|
|
615
|
-
const ch = this.input.charCodeAt(i);
|
|
616
|
-
if (ch !== 32 && ch !== 9) {
|
|
617
|
-
allWhitespace = false;
|
|
618
|
-
break;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Check if the character after < is not whitespace
|
|
623
|
-
if (allWhitespace && this.pos + 1 < this.input.length) {
|
|
624
|
-
const nextChar = this.input.charCodeAt(this.pos + 1);
|
|
625
|
-
if (nextChar !== 32 && nextChar !== 9 && nextChar !== 10 && nextChar !== 13) {
|
|
626
|
-
++this.pos;
|
|
627
|
-
return this.finishToken(tstt.jsxTagStart);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if (code === 35) {
|
|
634
|
-
// # character
|
|
635
|
-
if (this.pos + 1 < this.input.length) {
|
|
636
|
-
/** @param {string} value */
|
|
637
|
-
const startsWith = (value) =>
|
|
638
|
-
this.input.slice(this.pos, this.pos + value.length) === value;
|
|
639
|
-
/** @param {number} length */
|
|
640
|
-
const char_after = (length) =>
|
|
641
|
-
this.pos + length < this.input.length ? this.input.charCodeAt(this.pos + length) : -1;
|
|
642
|
-
/** @param {number} ch */
|
|
643
|
-
const is_ripple_delimiter = (ch) =>
|
|
644
|
-
ch === 40 || // (
|
|
645
|
-
ch === 41 || // )
|
|
646
|
-
ch === 60 || // <
|
|
647
|
-
ch === 46 || // .
|
|
648
|
-
ch === 44 || // ,
|
|
649
|
-
ch === 59 || // ;
|
|
650
|
-
ch === 91 || // [
|
|
651
|
-
ch === 93 || // ]
|
|
652
|
-
ch === 123 || // {
|
|
653
|
-
ch === 125 || // }
|
|
654
|
-
ch === 32 || // space
|
|
655
|
-
ch === 9 || // tab
|
|
656
|
-
ch === 10 || // newline
|
|
657
|
-
ch === 13 || // carriage return
|
|
658
|
-
ch === -1; // EOF
|
|
659
|
-
|
|
660
|
-
if (startsWith('#server') && is_ripple_delimiter(char_after(7))) {
|
|
661
|
-
this.pos += 7;
|
|
662
|
-
return this.finishToken(tt.name, '#server');
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
if (startsWith('#style') && is_ripple_delimiter(char_after(6))) {
|
|
666
|
-
this.pos += 6;
|
|
667
|
-
return this.finishToken(tt.name, '#style');
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return super.getTokenFromCode(code);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Override isLet to recognize `let &{` and `let &[` as variable declarations.
|
|
676
|
-
* Acorn's isLet checks the char after `let` and only recognizes `{`, `[`, or identifiers.
|
|
677
|
-
* The `&` char (38) is not in that set, so `let &{...}` would not be parsed as a declaration.
|
|
678
|
-
* @type {Parse.Parser['isLet']}
|
|
679
|
-
*/
|
|
680
|
-
isLet(context) {
|
|
681
|
-
if (!this.isContextual('let')) return false;
|
|
682
|
-
const skip = /\s*/y;
|
|
683
|
-
skip.lastIndex = this.pos;
|
|
684
|
-
const match = skip.exec(this.input);
|
|
685
|
-
if (!match) return super.isLet(context);
|
|
686
|
-
const next = this.pos + match[0].length;
|
|
687
|
-
const nextCh = this.input.charCodeAt(next);
|
|
688
|
-
// If next char is &, check if char after & is { or [
|
|
689
|
-
if (nextCh === 38) {
|
|
690
|
-
const afterAmp = this.input.charCodeAt(next + 1);
|
|
691
|
-
if (afterAmp === 123 || afterAmp === 91) return true;
|
|
692
|
-
}
|
|
693
|
-
return super.isLet(context);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
/**
|
|
697
|
-
* Parse binding atom - handles lazy destructuring patterns (&{...} and &[...])
|
|
698
|
-
* When & is directly followed by { or [, parse as a lazy destructuring pattern.
|
|
699
|
-
* The resulting ObjectPattern/ArrayPattern node gets a `lazy: true` flag.
|
|
700
|
-
*/
|
|
701
|
-
parseBindingAtom() {
|
|
702
|
-
if (this.type === tt.bitwiseAND) {
|
|
703
|
-
// Check that the char immediately after & is { or [ (no whitespace)
|
|
704
|
-
const charAfterAmp = this.input.charCodeAt(this.end);
|
|
705
|
-
if (charAfterAmp === 123 || charAfterAmp === 91) {
|
|
706
|
-
// & directly followed by { or [ — lazy destructuring
|
|
707
|
-
this.next(); // consume &, now current token is { or [
|
|
708
|
-
const pattern = super.parseBindingAtom();
|
|
709
|
-
/** @type {AST.ObjectPattern | AST.ArrayPattern} */ (pattern).lazy = true;
|
|
710
|
-
return pattern;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
return super.parseBindingAtom();
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* Parse expression atom - handles RippleArray and RippleObject literals
|
|
718
|
-
* @type {Parse.Parser['parseExprAtom']}
|
|
719
|
-
*/
|
|
720
|
-
parseExprAtom(refDestructuringErrors, forNew, forInit) {
|
|
721
|
-
const lookahead_type = this.lookahead().type;
|
|
722
|
-
const is_next_call_token = lookahead_type === tt.parenL || lookahead_type === tt.relational;
|
|
723
|
-
|
|
724
|
-
// Check if this is #server identifier for server function calls
|
|
725
|
-
if (this.type === tt.name && this.value === '#server') {
|
|
726
|
-
const node = this.startNode();
|
|
727
|
-
this.next();
|
|
728
|
-
return /** @type {AST.ServerIdentifier} */ (this.finishNode(node, 'ServerIdentifier'));
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
if (this.type === tt.name && this.value === '#style') {
|
|
732
|
-
const node = this.startNode();
|
|
733
|
-
this.next();
|
|
734
|
-
return /** @type {AST.StyleIdentifier} */ (this.finishNode(node, 'StyleIdentifier'));
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Check if this is a component expression (e.g., in object literal values)
|
|
738
|
-
if (this.type === tt.name && this.value === 'component') {
|
|
739
|
-
return this.parseComponent();
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/**
|
|
746
|
-
* Override to track parenthesized expressions in metadata
|
|
747
|
-
* This allows the prettier plugin to preserve parentheses where they existed
|
|
748
|
-
* @type {Parse.Parser['parseParenAndDistinguishExpression']}
|
|
749
|
-
*/
|
|
750
|
-
parseParenAndDistinguishExpression(canBeArrow, forInit) {
|
|
751
|
-
const startPos = this.start;
|
|
752
|
-
const expr = super.parseParenAndDistinguishExpression(canBeArrow, forInit);
|
|
753
|
-
|
|
754
|
-
// If the expression's start position is after the opening paren,
|
|
755
|
-
// it means it was wrapped in parentheses. Mark it in metadata.
|
|
756
|
-
if (expr && /** @type {AST.NodeWithLocation} */ (expr).start > startPos) {
|
|
757
|
-
expr.metadata ??= { path: [] };
|
|
758
|
-
expr.metadata.parenthesized = true;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
return expr;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
/**
|
|
765
|
-
* Override checkLocalExport to check all scopes in the scope stack.
|
|
766
|
-
* This is needed because server blocks create nested scopes, but exports
|
|
767
|
-
* from within server blocks should still be valid if the identifier is
|
|
768
|
-
* declared in the server block's scope (not just the top-level module scope).
|
|
769
|
-
* @type {Parse.Parser['checkLocalExport']}
|
|
770
|
-
*/
|
|
771
|
-
checkLocalExport(id) {
|
|
772
|
-
const { name } = id;
|
|
773
|
-
if (this.hasImport(name)) return;
|
|
774
|
-
// Check all scopes in the scope stack, not just the top-level scope
|
|
775
|
-
for (let i = this.scopeStack.length - 1; i >= 0; i--) {
|
|
776
|
-
const scope = this.scopeStack[i];
|
|
777
|
-
if (scope.lexical.indexOf(name) !== -1 || scope.var.indexOf(name) !== -1) {
|
|
778
|
-
// Found in a scope, remove from undefinedExports if it was added
|
|
779
|
-
delete this.undefinedExports[name];
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
// Not found in any scope, add to undefinedExports for later error
|
|
784
|
-
this.undefinedExports[name] = id;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
/**
|
|
788
|
-
* @type {Parse.Parser['parseServerBlock']}
|
|
789
|
-
*/
|
|
790
|
-
parseServerBlock() {
|
|
791
|
-
const node = /** @type {AST.ServerBlock} */ (this.startNode());
|
|
792
|
-
this.next();
|
|
793
|
-
|
|
794
|
-
const body = /** @type {AST.ServerBlockStatement} */ (this.startNode());
|
|
795
|
-
node.body = body;
|
|
796
|
-
body.body = [];
|
|
797
|
-
|
|
798
|
-
this.expect(tt.braceL);
|
|
799
|
-
this.enterScope(0);
|
|
800
|
-
while (this.type !== tt.braceR) {
|
|
801
|
-
const stmt = /** @type {AST.Statement} */ (this.parseStatement(null, true));
|
|
802
|
-
body.body.push(stmt);
|
|
803
|
-
}
|
|
804
|
-
this.next();
|
|
805
|
-
this.exitScope();
|
|
806
|
-
this.finishNode(body, 'BlockStatement');
|
|
807
|
-
|
|
808
|
-
this.awaitPos = 0;
|
|
809
|
-
return this.finishNode(node, 'ServerBlock');
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
* Parse a component - common implementation used by statements, expressions, and export defaults
|
|
814
|
-
* @type {Parse.Parser['parseComponent']}
|
|
815
|
-
*/
|
|
816
|
-
parseComponent({
|
|
817
|
-
requireName = false,
|
|
818
|
-
isDefault = false,
|
|
819
|
-
declareName = false,
|
|
820
|
-
skipName = false,
|
|
821
|
-
} = {}) {
|
|
822
|
-
const node = /** @type {AST.Component} */ (this.startNode());
|
|
823
|
-
node.type = 'Component';
|
|
824
|
-
node.css = null;
|
|
825
|
-
node.default = isDefault;
|
|
826
|
-
|
|
827
|
-
// skipName is used for computed property names where 'component' and the key
|
|
828
|
-
// have already been consumed before calling parseComponent
|
|
829
|
-
if (!skipName) {
|
|
830
|
-
this.next(); // consume 'component'
|
|
831
|
-
}
|
|
832
|
-
this.enterScope(0);
|
|
833
|
-
|
|
834
|
-
if (skipName) {
|
|
835
|
-
// For computed names, the key is parsed separately, so id is null
|
|
836
|
-
node.id = null;
|
|
837
|
-
} else if (requireName) {
|
|
838
|
-
node.id = this.parseIdent();
|
|
839
|
-
if (declareName) {
|
|
840
|
-
this.declareName(
|
|
841
|
-
node.id.name,
|
|
842
|
-
BINDING_TYPES.BIND_FUNCTION,
|
|
843
|
-
/** @type {AST.NodeWithLocation} */ (node.id).start,
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
} else {
|
|
847
|
-
node.id = this.type.label === 'name' ? this.parseIdent() : null;
|
|
848
|
-
if (declareName && node.id) {
|
|
849
|
-
this.declareName(
|
|
850
|
-
node.id.name,
|
|
851
|
-
BINDING_TYPES.BIND_FUNCTION,
|
|
852
|
-
/** @type {AST.NodeWithLocation} */ (node.id).start,
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
this.parseFunctionParams(node);
|
|
858
|
-
this.eat(tt.braceL);
|
|
859
|
-
node.body = [];
|
|
860
|
-
this.#path.push(node);
|
|
861
|
-
|
|
862
|
-
this.parseTemplateBody(node.body);
|
|
863
|
-
this.#path.pop();
|
|
864
|
-
this.exitScope();
|
|
865
|
-
|
|
866
|
-
this.next();
|
|
867
|
-
skipWhitespace(this);
|
|
868
|
-
this.finishNode(node, 'Component');
|
|
869
|
-
this.awaitPos = 0;
|
|
870
|
-
|
|
871
|
-
return node;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* @type {Parse.Parser['parseExportDefaultDeclaration']}
|
|
876
|
-
*/
|
|
877
|
-
parseExportDefaultDeclaration() {
|
|
878
|
-
// Check if this is "export default component"
|
|
879
|
-
if (this.value === 'component') {
|
|
880
|
-
return this.parseComponent({ isDefault: true });
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
return super.parseExportDefaultDeclaration();
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/** @type {Parse.Parser['parseForStatement']} */
|
|
887
|
-
parseForStatement(node) {
|
|
888
|
-
this.next();
|
|
889
|
-
let awaitAt =
|
|
890
|
-
this.options.ecmaVersion >= 9 && this.canAwait && this.eatContextual('await')
|
|
891
|
-
? this.lastTokStart
|
|
892
|
-
: -1;
|
|
893
|
-
this.labels.push({ kind: 'loop' });
|
|
894
|
-
this.enterScope(0);
|
|
895
|
-
this.expect(tt.parenL);
|
|
896
|
-
|
|
897
|
-
if (this.type === tt.semi) {
|
|
898
|
-
if (awaitAt > -1) this.unexpected(awaitAt);
|
|
899
|
-
return this.parseFor(node, null);
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// @ts-ignore — acorn internal: isLet accepts 0 args at runtime
|
|
903
|
-
let isLet = this.isLet();
|
|
904
|
-
if (this.type === tt._var || this.type === tt._const || isLet) {
|
|
905
|
-
let init = /** @type {AST.VariableDeclaration} */ (this.startNode()),
|
|
906
|
-
kind = isLet ? 'let' : /** @type {AST.VariableDeclaration['kind']} */ (this.value);
|
|
907
|
-
this.next();
|
|
908
|
-
this.parseVar(init, true, kind);
|
|
909
|
-
this.finishNode(init, 'VariableDeclaration');
|
|
910
|
-
return this.parseForAfterInitWithIndex(
|
|
911
|
-
/** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
|
|
912
|
-
init,
|
|
913
|
-
awaitAt,
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// Handle other cases like using declarations if they exist
|
|
918
|
-
let startsWithLet = this.isContextual('let'),
|
|
919
|
-
isForOf = false;
|
|
920
|
-
let usingKind =
|
|
921
|
-
this.isUsing && this.isUsing(true)
|
|
922
|
-
? 'using'
|
|
923
|
-
: this.isAwaitUsing && this.isAwaitUsing(true)
|
|
924
|
-
? 'await using'
|
|
925
|
-
: null;
|
|
926
|
-
if (usingKind) {
|
|
927
|
-
let init = /** @type {AST.VariableDeclaration} */ (this.startNode());
|
|
928
|
-
this.next();
|
|
929
|
-
if (usingKind === 'await using') {
|
|
930
|
-
if (!this.canAwait) {
|
|
931
|
-
this.raise(this.start, 'Await using cannot appear outside of async function');
|
|
932
|
-
}
|
|
933
|
-
this.next();
|
|
934
|
-
}
|
|
935
|
-
this.parseVar(init, true, usingKind);
|
|
936
|
-
this.finishNode(init, 'VariableDeclaration');
|
|
937
|
-
return this.parseForAfterInitWithIndex(
|
|
938
|
-
/** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
|
|
939
|
-
init,
|
|
940
|
-
awaitAt,
|
|
941
|
-
);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
let containsEsc = this.containsEsc;
|
|
945
|
-
let refDestructuringErrors = new /** @type {new () => Parse.DestructuringErrors} */ (
|
|
946
|
-
/** @type {unknown} */ (DestructuringErrors)
|
|
947
|
-
)();
|
|
948
|
-
let initPos = this.start;
|
|
949
|
-
let init_expr =
|
|
950
|
-
awaitAt > -1
|
|
951
|
-
? this.parseExprSubscripts(refDestructuringErrors, 'await')
|
|
952
|
-
: this.parseExpression(true, refDestructuringErrors);
|
|
953
|
-
|
|
954
|
-
if (
|
|
955
|
-
this.type === tt._in ||
|
|
956
|
-
(isForOf = this.options.ecmaVersion >= 6 && this.isContextual('of'))
|
|
957
|
-
) {
|
|
958
|
-
if (awaitAt > -1) {
|
|
959
|
-
// implies `ecmaVersion >= 9`
|
|
960
|
-
if (this.type === tt._in) this.unexpected(awaitAt);
|
|
961
|
-
/** @type {AST.ForOfStatement} */ (node).await = true;
|
|
962
|
-
} else if (isForOf && this.options.ecmaVersion >= 8) {
|
|
963
|
-
if (
|
|
964
|
-
init_expr.start === initPos &&
|
|
965
|
-
!containsEsc &&
|
|
966
|
-
init_expr.type === 'Identifier' &&
|
|
967
|
-
init_expr.name === 'async'
|
|
968
|
-
)
|
|
969
|
-
this.unexpected();
|
|
970
|
-
else if (this.options.ecmaVersion >= 9)
|
|
971
|
-
/** @type {AST.ForOfStatement} */ (node).await = false;
|
|
972
|
-
}
|
|
973
|
-
if (startsWithLet && isForOf)
|
|
974
|
-
this.raise(
|
|
975
|
-
/** @type {AST.NodeWithLocation} */ (init_expr).start,
|
|
976
|
-
"The left-hand side of a for-of loop may not start with 'let'.",
|
|
977
|
-
);
|
|
978
|
-
const init = this.toAssignable(init_expr, false, refDestructuringErrors);
|
|
979
|
-
this.checkLValPattern(init);
|
|
980
|
-
return this.parseForInWithIndex(
|
|
981
|
-
/** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
|
|
982
|
-
init,
|
|
983
|
-
);
|
|
984
|
-
} else {
|
|
985
|
-
this.checkExpressionErrors(refDestructuringErrors, true);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
if (awaitAt > -1) this.unexpected(awaitAt);
|
|
989
|
-
return this.parseFor(node, init_expr);
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
/** @type {Parse.Parser['parseForAfterInitWithIndex']} */
|
|
993
|
-
parseForAfterInitWithIndex(node, init, awaitAt) {
|
|
994
|
-
if (
|
|
995
|
-
(this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual('of'))) &&
|
|
996
|
-
init.declarations.length === 1
|
|
997
|
-
) {
|
|
998
|
-
if (this.options.ecmaVersion >= 9) {
|
|
999
|
-
if (this.type === tt._in) {
|
|
1000
|
-
if (awaitAt > -1) {
|
|
1001
|
-
this.unexpected(awaitAt);
|
|
1002
|
-
}
|
|
1003
|
-
} else {
|
|
1004
|
-
/** @type {AST.ForOfStatement} */ (node).await = awaitAt > -1;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
return this.parseForInWithIndex(
|
|
1008
|
-
/** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
|
|
1009
|
-
init,
|
|
1010
|
-
);
|
|
1011
|
-
}
|
|
1012
|
-
if (awaitAt > -1) {
|
|
1013
|
-
this.unexpected(awaitAt);
|
|
1014
|
-
}
|
|
1015
|
-
return this.parseFor(node, init);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
/** @type {Parse.Parser['parseForInWithIndex']} */
|
|
1019
|
-
parseForInWithIndex(node, init) {
|
|
1020
|
-
const isForIn = this.type === tt._in;
|
|
1021
|
-
this.next();
|
|
1022
|
-
|
|
1023
|
-
if (
|
|
1024
|
-
init.type === 'VariableDeclaration' &&
|
|
1025
|
-
init.declarations[0].init != null &&
|
|
1026
|
-
(!isForIn ||
|
|
1027
|
-
this.options.ecmaVersion < 8 ||
|
|
1028
|
-
this.strict ||
|
|
1029
|
-
init.kind !== 'var' ||
|
|
1030
|
-
init.declarations[0].id.type !== 'Identifier')
|
|
1031
|
-
) {
|
|
1032
|
-
this.raise(
|
|
1033
|
-
/** @type {AST.NodeWithLocation} */ (init).start,
|
|
1034
|
-
`${isForIn ? 'for-in' : 'for-of'} loop variable declaration may not have an initializer`,
|
|
1035
|
-
);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
node.left = init;
|
|
1039
|
-
node.right = isForIn ? this.parseExpression() : this.parseMaybeAssign();
|
|
1040
|
-
|
|
1041
|
-
// Check for our extended syntax: "; index varName"
|
|
1042
|
-
if (!isForIn && this.type === tt.semi) {
|
|
1043
|
-
this.next(); // consume ';'
|
|
1044
|
-
|
|
1045
|
-
if (this.isContextual('index')) {
|
|
1046
|
-
this.next(); // consume 'index'
|
|
1047
|
-
/** @type {AST.ForOfStatement} */ (node).index = /** @type {AST.Identifier} */ (
|
|
1048
|
-
this.parseExpression()
|
|
1049
|
-
);
|
|
1050
|
-
if (
|
|
1051
|
-
/** @type {AST.Identifier} */ (/** @type {AST.ForOfStatement} */ (node).index)
|
|
1052
|
-
.type !== 'Identifier'
|
|
1053
|
-
) {
|
|
1054
|
-
this.raise(this.start, 'Expected identifier after "index" keyword');
|
|
1055
|
-
}
|
|
1056
|
-
this.eat(tt.semi);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
if (this.isContextual('key')) {
|
|
1060
|
-
this.next(); // consume 'key'
|
|
1061
|
-
/** @type {AST.ForOfStatement} */ (node).key = this.parseExpression();
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
if (this.isContextual('index')) {
|
|
1065
|
-
this.raise(this.start, '"index" must come before "key" in for-of loop');
|
|
1066
|
-
}
|
|
1067
|
-
} else if (!isForIn) {
|
|
1068
|
-
// Set index to null for standard for-of loops
|
|
1069
|
-
/** @type {AST.ForOfStatement} */ (node).index = null;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
this.expect(tt.parenR);
|
|
1073
|
-
node.body = /** @type {AST.BlockStatement} */ (this.parseStatement('for'));
|
|
1074
|
-
this.exitScope();
|
|
1075
|
-
this.labels.pop();
|
|
1076
|
-
return this.finishNode(node, isForIn ? 'ForInStatement' : 'ForOfStatement');
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
/**
|
|
1080
|
-
* @type {Parse.Parser['checkUnreserved']}
|
|
1081
|
-
*/
|
|
1082
|
-
checkUnreserved(ref) {
|
|
1083
|
-
if (ref.name === 'component') {
|
|
1084
|
-
// Allow 'component' when it's followed by an identifier and '(' or '<' (component method in object literal or class)
|
|
1085
|
-
// e.g., { component something() { ... } } or class Foo { component something<T>() { ... } }
|
|
1086
|
-
// Also allow computed names: { component ['name']() { ... } }
|
|
1087
|
-
// Also allow string literal names: { component 'name'() { ... } }
|
|
1088
|
-
const nextChars = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
|
|
1089
|
-
if (!nextChars) {
|
|
1090
|
-
this.raise(
|
|
1091
|
-
ref.start,
|
|
1092
|
-
'"component" is a Ripple keyword and cannot be used as an identifier',
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
return super.checkUnreserved(ref);
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/** @type {Parse.Parser['shouldParseExportStatement']} */
|
|
1100
|
-
shouldParseExportStatement() {
|
|
1101
|
-
if (super.shouldParseExportStatement()) {
|
|
1102
|
-
return true;
|
|
1103
|
-
}
|
|
1104
|
-
if (this.value === 'component') {
|
|
1105
|
-
return true;
|
|
1106
|
-
}
|
|
1107
|
-
return this.type.keyword === 'var';
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
/**
|
|
1111
|
-
* @return {ESTreeJSX.JSXExpressionContainer}
|
|
1112
|
-
*/
|
|
1113
|
-
jsx_parseExpressionContainer() {
|
|
1114
|
-
let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
|
|
1115
|
-
this.next();
|
|
1116
|
-
|
|
1117
|
-
if (this.type === tt.name && this.value === 'html') {
|
|
1118
|
-
node.html = true;
|
|
1119
|
-
this.next();
|
|
1120
|
-
if (this.type === tt.braceR) {
|
|
1121
|
-
this.raise(
|
|
1122
|
-
this.start,
|
|
1123
|
-
'"html" is a Ripple keyword and must be used in the form {html some_content}',
|
|
1124
|
-
);
|
|
1125
|
-
}
|
|
1126
|
-
} else if (this.type === tt.name && this.value === 'text') {
|
|
1127
|
-
node.text = true;
|
|
1128
|
-
this.next();
|
|
1129
|
-
if (this.type === tt.braceR) {
|
|
1130
|
-
this.raise(
|
|
1131
|
-
this.start,
|
|
1132
|
-
'"text" is a Ripple keyword and must be used in the form {text some_value}',
|
|
1133
|
-
);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
node.expression =
|
|
1138
|
-
this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
|
|
1139
|
-
this.expect(tt.braceR);
|
|
1140
|
-
|
|
1141
|
-
return this.finishNode(node, 'JSXExpressionContainer');
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
* @type {Parse.Parser['jsx_parseEmptyExpression']}
|
|
1146
|
-
*/
|
|
1147
|
-
jsx_parseEmptyExpression() {
|
|
1148
|
-
// Override to properly handle the range for JSXEmptyExpression
|
|
1149
|
-
// The range should be from after { to before }
|
|
1150
|
-
const node = /** @type {ESTreeJSX.JSXEmptyExpression} */ (
|
|
1151
|
-
this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc)
|
|
1152
|
-
);
|
|
1153
|
-
node.end = this.start;
|
|
1154
|
-
node.loc.end = this.startLoc;
|
|
1155
|
-
return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
/**
|
|
1159
|
-
* @type {Parse.Parser['jsx_parseTupleContainer']}
|
|
1160
|
-
*/
|
|
1161
|
-
jsx_parseTupleContainer() {
|
|
1162
|
-
const t = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
|
|
1163
|
-
return (
|
|
1164
|
-
this.next(),
|
|
1165
|
-
(t.expression =
|
|
1166
|
-
this.type === tt.bracketR ? this.jsx_parseEmptyExpression() : this.parseExpression()),
|
|
1167
|
-
this.expect(tt.bracketR),
|
|
1168
|
-
this.finishNode(t, 'JSXExpressionContainer')
|
|
1169
|
-
);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
/**
|
|
1173
|
-
* @type {Parse.Parser['jsx_parseAttribute']}
|
|
1174
|
-
*/
|
|
1175
|
-
jsx_parseAttribute() {
|
|
1176
|
-
let node = /** @type {AST.RippleAttribute | ESTreeJSX.JSXAttribute} */ (this.startNode());
|
|
1177
|
-
|
|
1178
|
-
if (this.eat(tt.braceL)) {
|
|
1179
|
-
if (this.value === 'ref') {
|
|
1180
|
-
this.next();
|
|
1181
|
-
if (this.type === tt.braceR) {
|
|
1182
|
-
this.raise(
|
|
1183
|
-
this.start,
|
|
1184
|
-
'"ref" is a Ripple keyword and must be used in the form {ref fn}',
|
|
1185
|
-
);
|
|
1186
|
-
}
|
|
1187
|
-
/** @type {AST.RefAttribute} */ (node).argument = this.parseMaybeAssign();
|
|
1188
|
-
this.expect(tt.braceR);
|
|
1189
|
-
return /** @type {AST.RefAttribute} */ (this.finishNode(node, 'RefAttribute'));
|
|
1190
|
-
} else if (this.type === tt.ellipsis) {
|
|
1191
|
-
this.expect(tt.ellipsis);
|
|
1192
|
-
/** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
|
|
1193
|
-
this.expect(tt.braceR);
|
|
1194
|
-
return this.finishNode(node, 'SpreadAttribute');
|
|
1195
|
-
} else if (this.lookahead().type === tt.ellipsis) {
|
|
1196
|
-
this.expect(tt.ellipsis);
|
|
1197
|
-
/** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
|
|
1198
|
-
this.expect(tt.braceR);
|
|
1199
|
-
return this.finishNode(node, 'SpreadAttribute');
|
|
1200
|
-
} else {
|
|
1201
|
-
const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
|
|
1202
|
-
id.tracked = false;
|
|
1203
|
-
this.finishNode(id, 'Identifier');
|
|
1204
|
-
/** @type {AST.Attribute} */ (node).name = id;
|
|
1205
|
-
/** @type {AST.Attribute} */ (node).value = id;
|
|
1206
|
-
/** @type {AST.Attribute} */ (node).shorthand = true; // Mark as shorthand since name and value are the same
|
|
1207
|
-
this.next();
|
|
1208
|
-
this.expect(tt.braceR);
|
|
1209
|
-
return this.finishNode(node, 'Attribute');
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
/** @type {ESTreeJSX.JSXAttribute} */ (node).name = this.jsx_parseNamespacedName();
|
|
1213
|
-
/** @type {ESTreeJSX.JSXAttribute} */ (node).value =
|
|
1214
|
-
/** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
|
|
1215
|
-
this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
|
|
1216
|
-
);
|
|
1217
|
-
return this.finishNode(node, 'JSXAttribute');
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
/**
|
|
1221
|
-
* @type {Parse.Parser['jsx_parseNamespacedName']}
|
|
1222
|
-
*/
|
|
1223
|
-
jsx_parseNamespacedName() {
|
|
1224
|
-
const base = this.jsx_parseIdentifier();
|
|
1225
|
-
if (!this.eat(tt.colon)) return base;
|
|
1226
|
-
const node = /** @type {ESTreeJSX.JSXNamespacedName} */ (
|
|
1227
|
-
this.startNodeAt(
|
|
1228
|
-
/** @type {AST.NodeWithLocation} */ (base).start,
|
|
1229
|
-
/** @type {AST.NodeWithLocation} */ (base).loc.start,
|
|
1230
|
-
)
|
|
1231
|
-
);
|
|
1232
|
-
node.namespace = base;
|
|
1233
|
-
node.name = this.jsx_parseIdentifier();
|
|
1234
|
-
return this.finishNode(node, 'JSXNamespacedName');
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
/**
|
|
1238
|
-
* @type {Parse.Parser['jsx_parseIdentifier']}
|
|
1239
|
-
*/
|
|
1240
|
-
jsx_parseIdentifier() {
|
|
1241
|
-
const node = /** @type {ESTreeJSX.JSXIdentifier} */ (this.startNode());
|
|
1242
|
-
|
|
1243
|
-
if (this.type.label === '@') {
|
|
1244
|
-
this.next(); // consume @
|
|
1245
|
-
|
|
1246
|
-
if (this.type === tt.name || this.type === tstt.jsxName) {
|
|
1247
|
-
node.name = /** @type {string} */ (this.value);
|
|
1248
|
-
node.tracked = true;
|
|
1249
|
-
this.next();
|
|
1250
|
-
} else {
|
|
1251
|
-
// Unexpected token after @
|
|
1252
|
-
this.unexpected();
|
|
1253
|
-
}
|
|
1254
|
-
} else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
|
|
1255
|
-
node.name = /** @type {string} */ (this.value);
|
|
1256
|
-
node.tracked = false; // Explicitly mark as not tracked
|
|
1257
|
-
this.next();
|
|
1258
|
-
} else {
|
|
1259
|
-
return super.jsx_parseIdentifier();
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
return this.finishNode(node, 'JSXIdentifier');
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
/**
|
|
1266
|
-
* @type {Parse.Parser['jsx_parseElementName']}
|
|
1267
|
-
*/
|
|
1268
|
-
jsx_parseElementName() {
|
|
1269
|
-
if (this.type === tstt.jsxTagEnd) {
|
|
1270
|
-
return '';
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
let node = this.jsx_parseNamespacedName();
|
|
1274
|
-
|
|
1275
|
-
if (node.type === 'JSXNamespacedName') {
|
|
1276
|
-
return node;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
if (this.eat(tt.dot)) {
|
|
1280
|
-
let memberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
|
|
1281
|
-
this.startNodeAt(
|
|
1282
|
-
/** @type {AST.NodeWithLocation} */ (node).start,
|
|
1283
|
-
/** @type {AST.NodeWithLocation} */ (node).loc.start,
|
|
1284
|
-
)
|
|
1285
|
-
);
|
|
1286
|
-
memberExpr.object = node;
|
|
1287
|
-
memberExpr.property = this.jsx_parseIdentifier();
|
|
1288
|
-
memberExpr.computed = false;
|
|
1289
|
-
memberExpr = this.finishNode(memberExpr, 'JSXMemberExpression');
|
|
1290
|
-
while (this.eat(tt.dot)) {
|
|
1291
|
-
let newMemberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
|
|
1292
|
-
this.startNodeAt(
|
|
1293
|
-
/** @type {AST.NodeWithLocation} */ (memberExpr).start,
|
|
1294
|
-
/** @type {AST.NodeWithLocation} */ (memberExpr).loc.start,
|
|
1295
|
-
)
|
|
1296
|
-
);
|
|
1297
|
-
newMemberExpr.object = memberExpr;
|
|
1298
|
-
newMemberExpr.property = this.jsx_parseIdentifier();
|
|
1299
|
-
newMemberExpr.computed = false;
|
|
1300
|
-
memberExpr = this.finishNode(newMemberExpr, 'JSXMemberExpression');
|
|
1301
|
-
}
|
|
1302
|
-
return memberExpr;
|
|
1303
|
-
}
|
|
1304
|
-
return node;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
/** @type {Parse.Parser['jsx_parseAttributeValue']} */
|
|
1308
|
-
jsx_parseAttributeValue() {
|
|
1309
|
-
switch (this.type) {
|
|
1310
|
-
case tt.braceL:
|
|
1311
|
-
return this.jsx_parseExpressionContainer();
|
|
1312
|
-
case tstt.jsxTagStart:
|
|
1313
|
-
case tt.string:
|
|
1314
|
-
return this.parseExprAtom();
|
|
1315
|
-
default:
|
|
1316
|
-
this.raise(this.start, 'value should be either an expression or a quoted text');
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
/**
|
|
1321
|
-
* @type {Parse.Parser['parseTryStatement']}
|
|
1322
|
-
*/
|
|
1323
|
-
parseTryStatement(node) {
|
|
1324
|
-
this.next();
|
|
1325
|
-
node.block = this.parseBlock();
|
|
1326
|
-
node.handler = null;
|
|
1327
|
-
|
|
1328
|
-
if (this.value === 'pending') {
|
|
1329
|
-
this.next();
|
|
1330
|
-
node.pending = this.parseBlock();
|
|
1331
|
-
} else {
|
|
1332
|
-
node.pending = null;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
if (this.type === tt._catch) {
|
|
1336
|
-
const clause = /** @type {AST.CatchClause} */ (this.startNode());
|
|
1337
|
-
this.next();
|
|
1338
|
-
if (this.eat(tt.parenL)) {
|
|
1339
|
-
// Parse first param (error) manually to support optional second param (reset).
|
|
1340
|
-
// We can't use parseCatchClauseParam() because it eats the closing paren.
|
|
1341
|
-
const param = this.parseBindingAtom();
|
|
1342
|
-
const simple = param.type === 'Identifier';
|
|
1343
|
-
this.enterScope(simple ? BINDING_TYPES.BIND_SIMPLE_CATCH : 0);
|
|
1344
|
-
this.checkLValPattern(
|
|
1345
|
-
param,
|
|
1346
|
-
simple ? BINDING_TYPES.BIND_SIMPLE_CATCH : BINDING_TYPES.BIND_LEXICAL,
|
|
1347
|
-
);
|
|
1348
|
-
const type = this.tsTryParseTypeAnnotation();
|
|
1349
|
-
if (type) {
|
|
1350
|
-
param.typeAnnotation = type;
|
|
1351
|
-
this.resetEndLocation(param);
|
|
1352
|
-
}
|
|
1353
|
-
clause.param = param;
|
|
1354
|
-
|
|
1355
|
-
// Optional second parameter: reset function
|
|
1356
|
-
if (this.eat(tt.comma)) {
|
|
1357
|
-
const reset_param = this.parseBindingAtom();
|
|
1358
|
-
this.checkLValSimple(reset_param, BINDING_TYPES.BIND_LEXICAL);
|
|
1359
|
-
const reset_type = this.tsTryParseTypeAnnotation();
|
|
1360
|
-
if (reset_type) {
|
|
1361
|
-
reset_param.typeAnnotation = reset_type;
|
|
1362
|
-
this.resetEndLocation(reset_param);
|
|
1363
|
-
}
|
|
1364
|
-
clause.resetParam = reset_param;
|
|
1365
|
-
} else {
|
|
1366
|
-
clause.resetParam = null;
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
this.expect(tt.parenR);
|
|
1370
|
-
} else {
|
|
1371
|
-
clause.param = null;
|
|
1372
|
-
clause.resetParam = null;
|
|
1373
|
-
this.enterScope(0);
|
|
1374
|
-
}
|
|
1375
|
-
clause.body = this.parseBlock(false);
|
|
1376
|
-
this.exitScope();
|
|
1377
|
-
node.handler = this.finishNode(clause, 'CatchClause');
|
|
1378
|
-
}
|
|
1379
|
-
node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null;
|
|
1380
|
-
|
|
1381
|
-
if (!node.handler && !node.finalizer && !node.pending) {
|
|
1382
|
-
this.raise(
|
|
1383
|
-
/** @type {AST.NodeWithLocation} */ (node).start,
|
|
1384
|
-
'Missing catch or finally clause',
|
|
1385
|
-
);
|
|
1386
|
-
}
|
|
1387
|
-
return this.finishNode(node, 'TryStatement');
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
/** @type {Parse.Parser['jsx_readToken']} */
|
|
1391
|
-
jsx_readToken() {
|
|
1392
|
-
const inside_tsx_compat = this.#path.findLast(
|
|
1393
|
-
(n) => n.type === 'TsxCompat' || n.type === 'Tsx',
|
|
1394
|
-
);
|
|
1395
|
-
if (inside_tsx_compat) {
|
|
1396
|
-
return super.jsx_readToken();
|
|
1397
|
-
}
|
|
1398
|
-
let out = '',
|
|
1399
|
-
chunkStart = this.pos;
|
|
1400
|
-
|
|
1401
|
-
while (true) {
|
|
1402
|
-
if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
|
|
1403
|
-
let ch = this.input.charCodeAt(this.pos);
|
|
1404
|
-
|
|
1405
|
-
switch (ch) {
|
|
1406
|
-
case 60: // '<'
|
|
1407
|
-
case 123: // '{'
|
|
1408
|
-
// In JSX text mode, '<' and '{' always start a tag/expression container.
|
|
1409
|
-
// `exprAllowed` can be false here due to surrounding parser state, but
|
|
1410
|
-
// throwing breaks valid templates (e.g. sibling tags after a close).
|
|
1411
|
-
if (ch === 60) {
|
|
1412
|
-
++this.pos;
|
|
1413
|
-
return this.finishToken(tstt.jsxTagStart);
|
|
1414
|
-
}
|
|
1415
|
-
return this.getTokenFromCode(ch);
|
|
1416
|
-
|
|
1417
|
-
case 47: // '/'
|
|
1418
|
-
// Check if this is a comment (// or /*)
|
|
1419
|
-
if (this.input.charCodeAt(this.pos + 1) === 47) {
|
|
1420
|
-
// '//'
|
|
1421
|
-
// Line comment - handle it properly
|
|
1422
|
-
const commentStart = this.pos;
|
|
1423
|
-
const startLoc = this.curPosition();
|
|
1424
|
-
this.pos += 2;
|
|
1425
|
-
|
|
1426
|
-
let commentText = '';
|
|
1427
|
-
while (this.pos < this.input.length) {
|
|
1428
|
-
const nextCh = this.input.charCodeAt(this.pos);
|
|
1429
|
-
if (acorn.isNewLine(nextCh)) break;
|
|
1430
|
-
commentText += this.input[this.pos];
|
|
1431
|
-
this.pos++;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
const commentEnd = this.pos;
|
|
1435
|
-
const endLoc = this.curPosition();
|
|
1436
|
-
|
|
1437
|
-
// Call onComment if it exists
|
|
1438
|
-
if (this.options.onComment) {
|
|
1439
|
-
const metadata = this.#createCommentMetadata();
|
|
1440
|
-
this.options.onComment(
|
|
1441
|
-
false,
|
|
1442
|
-
commentText,
|
|
1443
|
-
commentStart,
|
|
1444
|
-
commentEnd,
|
|
1445
|
-
startLoc,
|
|
1446
|
-
endLoc,
|
|
1447
|
-
metadata,
|
|
1448
|
-
);
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
// Continue processing from current position
|
|
1452
|
-
break;
|
|
1453
|
-
} else if (this.input.charCodeAt(this.pos + 1) === 42) {
|
|
1454
|
-
// '/*'
|
|
1455
|
-
// Block comment - handle it properly
|
|
1456
|
-
const commentStart = this.pos;
|
|
1457
|
-
const startLoc = this.curPosition();
|
|
1458
|
-
this.pos += 2;
|
|
1459
|
-
|
|
1460
|
-
let commentText = '';
|
|
1461
|
-
while (this.pos < this.input.length - 1) {
|
|
1462
|
-
if (
|
|
1463
|
-
this.input.charCodeAt(this.pos) === 42 &&
|
|
1464
|
-
this.input.charCodeAt(this.pos + 1) === 47
|
|
1465
|
-
) {
|
|
1466
|
-
this.pos += 2;
|
|
1467
|
-
break;
|
|
1468
|
-
}
|
|
1469
|
-
commentText += this.input[this.pos];
|
|
1470
|
-
this.pos++;
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
const commentEnd = this.pos;
|
|
1474
|
-
const endLoc = this.curPosition();
|
|
1475
|
-
|
|
1476
|
-
// Call onComment if it exists
|
|
1477
|
-
if (this.options.onComment) {
|
|
1478
|
-
const metadata = this.#createCommentMetadata();
|
|
1479
|
-
this.options.onComment(
|
|
1480
|
-
true,
|
|
1481
|
-
commentText,
|
|
1482
|
-
commentStart,
|
|
1483
|
-
commentEnd,
|
|
1484
|
-
startLoc,
|
|
1485
|
-
endLoc,
|
|
1486
|
-
metadata,
|
|
1487
|
-
);
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
// Continue processing from current position
|
|
1491
|
-
break;
|
|
1492
|
-
}
|
|
1493
|
-
// If not a comment, fall through to default case
|
|
1494
|
-
this.context.push(b_stat);
|
|
1495
|
-
this.exprAllowed = true;
|
|
1496
|
-
return original.readToken.call(this, ch);
|
|
1497
|
-
|
|
1498
|
-
case 38: // '&'
|
|
1499
|
-
out += this.input.slice(chunkStart, this.pos);
|
|
1500
|
-
out += this.jsx_readEntity();
|
|
1501
|
-
chunkStart = this.pos;
|
|
1502
|
-
break;
|
|
1503
|
-
|
|
1504
|
-
case 62: // '>'
|
|
1505
|
-
case 125: {
|
|
1506
|
-
// '}'
|
|
1507
|
-
if (
|
|
1508
|
-
ch === 125 &&
|
|
1509
|
-
(this.#path.length === 0 ||
|
|
1510
|
-
this.#path.at(-1)?.type === 'Component' ||
|
|
1511
|
-
this.#path.at(-1)?.type === 'Element')
|
|
1512
|
-
) {
|
|
1513
|
-
return original.readToken.call(this, ch);
|
|
1514
|
-
}
|
|
1515
|
-
this.raise(
|
|
1516
|
-
this.pos,
|
|
1517
|
-
'Unexpected token `' +
|
|
1518
|
-
this.input[this.pos] +
|
|
1519
|
-
'`. Did you mean `' +
|
|
1520
|
-
(ch === 62 ? '>' : '}') +
|
|
1521
|
-
'` or ' +
|
|
1522
|
-
'`{"' +
|
|
1523
|
-
this.input[this.pos] +
|
|
1524
|
-
'"}' +
|
|
1525
|
-
'`?',
|
|
1526
|
-
);
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
default:
|
|
1530
|
-
if (acorn.isNewLine(ch)) {
|
|
1531
|
-
out += this.input.slice(chunkStart, this.pos);
|
|
1532
|
-
out += this.jsx_readNewLine(true);
|
|
1533
|
-
chunkStart = this.pos;
|
|
1534
|
-
} else if (ch === 32 || ch === 9) {
|
|
1535
|
-
++this.pos;
|
|
1536
|
-
} else {
|
|
1537
|
-
this.context.push(b_stat);
|
|
1538
|
-
this.exprAllowed = true;
|
|
1539
|
-
return original.readToken.call(this, ch);
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
/**
|
|
1546
|
-
* Override jsx_parseElement to intercept expression-level JSX.
|
|
1547
|
-
* This is called by acorn-jsx's parseExprAtom when it encounters <
|
|
1548
|
-
* in expression position. Only <tsx> and <tsx:*> are allowed.
|
|
1549
|
-
* @type {Parse.Parser['jsx_parseElement']}
|
|
1550
|
-
*/
|
|
1551
|
-
jsx_parseElement() {
|
|
1552
|
-
const inside_tsx = this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx');
|
|
1553
|
-
if (inside_tsx) {
|
|
1554
|
-
// Inside tsx/tsx:*, let acorn-jsx handle it normally
|
|
1555
|
-
return super.jsx_parseElement();
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
// Check if the element being parsed IS a <tsx> or <tsx:*> tag
|
|
1559
|
-
// Current token is jsxTagStart, this.end is position after '<'
|
|
1560
|
-
const tag_name_start = this.end;
|
|
1561
|
-
const char_after_tsx = this.input.charCodeAt(tag_name_start + 3);
|
|
1562
|
-
const is_tsx_tag =
|
|
1563
|
-
this.input.startsWith('tsx', tag_name_start) &&
|
|
1564
|
-
(tag_name_start + 3 >= this.input.length ||
|
|
1565
|
-
char_after_tsx === 62 || // >
|
|
1566
|
-
char_after_tsx === 47 || // / (self-closing)
|
|
1567
|
-
char_after_tsx === 32 || // space
|
|
1568
|
-
char_after_tsx === 9 || // tab
|
|
1569
|
-
char_after_tsx === 10 || // newline
|
|
1570
|
-
char_after_tsx === 13 || // carriage return
|
|
1571
|
-
char_after_tsx === 58); // : (tsx:react)
|
|
1572
|
-
|
|
1573
|
-
if (is_tsx_tag) {
|
|
1574
|
-
// Use Ripple's parseElement to create a Tsx/TsxCompat node
|
|
1575
|
-
this.next();
|
|
1576
|
-
return /** @type {import('estree-jsx').JSXElement} */ (
|
|
1577
|
-
/** @type {unknown} */ (this.parseElement())
|
|
1578
|
-
);
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
this.raise(
|
|
1582
|
-
this.start,
|
|
1583
|
-
'JSX elements cannot be used as expressions. Wrap with `<tsx>...</tsx>` or use elements as statements within a component.',
|
|
1584
|
-
);
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
/**
|
|
1588
|
-
* @type {Parse.Parser['parseElement']}
|
|
1589
|
-
*/
|
|
1590
|
-
parseElement() {
|
|
1591
|
-
const inside_head = this.#path.findLast(
|
|
1592
|
-
(n) => n.type === 'Element' && n.id.type === 'Identifier' && n.id.name === 'head',
|
|
1593
|
-
);
|
|
1594
|
-
// Adjust the start so we capture the `<` as part of the element
|
|
1595
|
-
const start = this.start - 1;
|
|
1596
|
-
const position = new acorn.Position(this.curLine, start - this.lineStart);
|
|
1597
|
-
|
|
1598
|
-
const element = /** @type {AST.Element | AST.Tsx | AST.TsxCompat} */ (this.startNode());
|
|
1599
|
-
element.start = start;
|
|
1600
|
-
/** @type {AST.NodeWithLocation} */ (element).loc.start = position;
|
|
1601
|
-
element.metadata = { path: [] };
|
|
1602
|
-
element.children = [];
|
|
1603
|
-
|
|
1604
|
-
const open = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
|
|
1605
|
-
this.jsx_parseOpeningElementAt(start, position)
|
|
1606
|
-
);
|
|
1607
|
-
|
|
1608
|
-
// Always attach the concrete opening element node for accurate source mapping
|
|
1609
|
-
element.openingElement = open;
|
|
1610
|
-
|
|
1611
|
-
// Check if this is a namespaced element (tsx:react)
|
|
1612
|
-
const is_tsx_compat = open.name.type === 'JSXNamespacedName';
|
|
1613
|
-
const is_tsx =
|
|
1614
|
-
!is_tsx_compat && open.name.type === 'JSXIdentifier' && open.name.name === 'tsx';
|
|
1615
|
-
|
|
1616
|
-
if (is_tsx_compat) {
|
|
1617
|
-
const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
|
|
1618
|
-
/** @type {AST.TsxCompat} */ (element).type = 'TsxCompat';
|
|
1619
|
-
/** @type {AST.TsxCompat} */ (element).kind = namespace_node.name.name; // e.g., "react" from "tsx:react"
|
|
1620
|
-
|
|
1621
|
-
if (open.selfClosing) {
|
|
1622
|
-
const tagName = namespace_node.namespace.name + ':' + namespace_node.name.name;
|
|
1623
|
-
this.raise(
|
|
1624
|
-
open.start,
|
|
1625
|
-
`TSX compatibility elements cannot be self-closing. '<${tagName} />' must have a closing tag '</${tagName}>'.`,
|
|
1626
|
-
);
|
|
1627
|
-
}
|
|
1628
|
-
} else if (is_tsx) {
|
|
1629
|
-
/** @type {AST.Tsx} */ (element).type = 'Tsx';
|
|
1630
|
-
|
|
1631
|
-
if (open.selfClosing) {
|
|
1632
|
-
this.raise(
|
|
1633
|
-
open.start,
|
|
1634
|
-
`TSX elements cannot be self-closing. '<tsx />' must have a closing tag '</tsx>'.`,
|
|
1635
|
-
);
|
|
1636
|
-
}
|
|
1637
|
-
} else {
|
|
1638
|
-
element.type = 'Element';
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
this.#path.push(element);
|
|
1642
|
-
|
|
1643
|
-
for (const attr of open.attributes) {
|
|
1644
|
-
if (attr.type === 'JSXAttribute') {
|
|
1645
|
-
/** @type {AST.Attribute} */ (/** @type {unknown} */ (attr)).type = 'Attribute';
|
|
1646
|
-
if (attr.name.type === 'JSXIdentifier') {
|
|
1647
|
-
/** @type {AST.Identifier} */ (/** @type {unknown} */ (attr.name)).type =
|
|
1648
|
-
'Identifier';
|
|
1649
|
-
}
|
|
1650
|
-
if (attr.value !== null) {
|
|
1651
|
-
if (attr.value.type === 'JSXExpressionContainer') {
|
|
1652
|
-
const expression = attr.value.expression;
|
|
1653
|
-
if (expression.type === 'Literal') {
|
|
1654
|
-
expression.was_expression = true;
|
|
1655
|
-
}
|
|
1656
|
-
// @ts-ignore — intentional AST node conversion from JSX to Ripple
|
|
1657
|
-
/** @type {ESTreeJSX.JSXAttribute} */ (attr).value =
|
|
1658
|
-
/** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (expression);
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
if (!is_tsx_compat && !is_tsx) {
|
|
1665
|
-
/** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
|
|
1666
|
-
convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
|
|
1667
|
-
);
|
|
1668
|
-
element.selfClosing = open.selfClosing;
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
element.attributes = open.attributes;
|
|
1672
|
-
element.metadata ??= { path: [] };
|
|
1673
|
-
element.metadata.commentContainerId = ++this.#commentContextId;
|
|
1674
|
-
|
|
1675
|
-
if (element.selfClosing) {
|
|
1676
|
-
this.#path.pop();
|
|
1677
|
-
|
|
1678
|
-
if (this.type.label === '</>/<=/>=') {
|
|
1679
|
-
this.pos--;
|
|
1680
|
-
this.next();
|
|
1681
|
-
}
|
|
1682
|
-
} else {
|
|
1683
|
-
if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'script') {
|
|
1684
|
-
let content = '';
|
|
1685
|
-
|
|
1686
|
-
// TODO implement this where we get a string for content of the content of the script tag
|
|
1687
|
-
// This is a temporary workaround to get the content of the script tag
|
|
1688
|
-
const start = open.end;
|
|
1689
|
-
const input = this.input.slice(start);
|
|
1690
|
-
const end = input.indexOf('</script>');
|
|
1691
|
-
content = end === -1 ? input : input.slice(0, end);
|
|
1692
|
-
|
|
1693
|
-
const newLines = content.match(regex_newline_characters)?.length;
|
|
1694
|
-
if (newLines) {
|
|
1695
|
-
this.curLine = open.loc.end.line + newLines;
|
|
1696
|
-
this.lineStart = start + content.lastIndexOf('\n') + 1;
|
|
1697
|
-
}
|
|
1698
|
-
if (end !== -1) {
|
|
1699
|
-
const closingStart = start + content.length;
|
|
1700
|
-
const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
|
|
1701
|
-
const closingStartLoc = new acorn.Position(
|
|
1702
|
-
closingLineInfo.line,
|
|
1703
|
-
closingLineInfo.column,
|
|
1704
|
-
);
|
|
1705
|
-
|
|
1706
|
-
// Ensure `</script>` can't be tokenized as `<` followed by a regexp
|
|
1707
|
-
// start when we manually advance to the `/`.
|
|
1708
|
-
this.exprAllowed = false;
|
|
1709
|
-
|
|
1710
|
-
// Position after '<' (so next() reads '/')
|
|
1711
|
-
this.pos = closingStart + 1;
|
|
1712
|
-
this.type = tstt.jsxTagStart;
|
|
1713
|
-
this.start = closingStart;
|
|
1714
|
-
this.startLoc = closingStartLoc;
|
|
1715
|
-
this.next();
|
|
1716
|
-
|
|
1717
|
-
// Consume '/'
|
|
1718
|
-
this.next();
|
|
1719
|
-
|
|
1720
|
-
const closingElement = this.jsx_parseClosingElementAt(closingStart, closingStartLoc);
|
|
1721
|
-
element.closingElement = closingElement;
|
|
1722
|
-
this.exprAllowed = false;
|
|
1723
|
-
|
|
1724
|
-
const contentStartLineInfo = acorn.getLineInfo(this.input, start);
|
|
1725
|
-
const contentStartLoc = new acorn.Position(
|
|
1726
|
-
contentStartLineInfo.line,
|
|
1727
|
-
contentStartLineInfo.column,
|
|
1728
|
-
);
|
|
1729
|
-
|
|
1730
|
-
const contentEndLineInfo = acorn.getLineInfo(this.input, closingStart);
|
|
1731
|
-
const contentEndLoc = new acorn.Position(
|
|
1732
|
-
contentEndLineInfo.line,
|
|
1733
|
-
contentEndLineInfo.column,
|
|
1734
|
-
);
|
|
1735
|
-
|
|
1736
|
-
element.children = [
|
|
1737
|
-
/** @type {AST.ScriptContent} */ (
|
|
1738
|
-
/** @type {unknown} */ ({
|
|
1739
|
-
type: 'ScriptContent',
|
|
1740
|
-
content,
|
|
1741
|
-
start,
|
|
1742
|
-
end: closingStart,
|
|
1743
|
-
loc: { start: contentStartLoc, end: contentEndLoc },
|
|
1744
|
-
})
|
|
1745
|
-
),
|
|
1746
|
-
];
|
|
1747
|
-
|
|
1748
|
-
this.#path.pop();
|
|
1749
|
-
} else {
|
|
1750
|
-
// No closing tag
|
|
1751
|
-
if (!this.#loose) {
|
|
1752
|
-
this.raise(
|
|
1753
|
-
open.end,
|
|
1754
|
-
"Unclosed tag '<script>'. Expected '</script>' before end of component.",
|
|
1755
|
-
);
|
|
1756
|
-
}
|
|
1757
|
-
/** @type {AST.Element} */ (element).unclosed = true;
|
|
1758
|
-
this.#path.pop();
|
|
1759
|
-
}
|
|
1760
|
-
} else if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'style') {
|
|
1761
|
-
// jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
|
|
1762
|
-
// So backtrack to the end of the <style> tag to make sure everything is included
|
|
1763
|
-
const start = open.end;
|
|
1764
|
-
const input = this.input.slice(start);
|
|
1765
|
-
const end = input.indexOf('</style>');
|
|
1766
|
-
const content = end === -1 ? input : input.slice(0, end);
|
|
1767
|
-
|
|
1768
|
-
const component = /** @type {AST.Component} */ (
|
|
1769
|
-
this.#path.findLast((n) => n.type === 'Component')
|
|
1770
|
-
);
|
|
1771
|
-
const parsed_css = parse_style(content, { loose: this.#loose });
|
|
1772
|
-
|
|
1773
|
-
if (!inside_head) {
|
|
1774
|
-
if (component.css !== null) {
|
|
1775
|
-
throw new Error('Components can only have one style tag');
|
|
1776
|
-
}
|
|
1777
|
-
component.css = parsed_css;
|
|
1778
|
-
/** @type {AST.Element} */ (element).metadata.styleScopeHash = parsed_css.hash;
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
const newLines = content.match(regex_newline_characters)?.length;
|
|
1782
|
-
if (newLines) {
|
|
1783
|
-
this.curLine = open.loc.end.line + newLines;
|
|
1784
|
-
this.lineStart = start + content.lastIndexOf('\n') + 1;
|
|
1785
|
-
}
|
|
1786
|
-
if (end !== -1) {
|
|
1787
|
-
const closingStart = start + content.length;
|
|
1788
|
-
const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
|
|
1789
|
-
const closingStartLoc = new acorn.Position(
|
|
1790
|
-
closingLineInfo.line,
|
|
1791
|
-
closingLineInfo.column,
|
|
1792
|
-
);
|
|
1793
|
-
|
|
1794
|
-
// Ensure `</style>` can't be tokenized as `<` followed by a regexp
|
|
1795
|
-
// start when we manually advance to the `/`.
|
|
1796
|
-
this.exprAllowed = false;
|
|
1797
|
-
|
|
1798
|
-
// Position after '<' (so next() reads '/')
|
|
1799
|
-
this.pos = closingStart + 1;
|
|
1800
|
-
this.type = tstt.jsxTagStart;
|
|
1801
|
-
this.start = closingStart;
|
|
1802
|
-
this.startLoc = closingStartLoc;
|
|
1803
|
-
this.next();
|
|
1804
|
-
|
|
1805
|
-
// Consume '/'
|
|
1806
|
-
this.next();
|
|
1807
|
-
|
|
1808
|
-
const closingElement = this.jsx_parseClosingElementAt(closingStart, closingStartLoc);
|
|
1809
|
-
element.closingElement = closingElement;
|
|
1810
|
-
this.exprAllowed = false;
|
|
1811
|
-
this.#path.pop();
|
|
1812
|
-
} else {
|
|
1813
|
-
if (!this.#loose) {
|
|
1814
|
-
this.raise(
|
|
1815
|
-
open.end,
|
|
1816
|
-
"Unclosed tag '<style>'. Expected '</style>' before end of component.",
|
|
1817
|
-
);
|
|
1818
|
-
}
|
|
1819
|
-
/** @type {AST.Element} */ (element).unclosed = true;
|
|
1820
|
-
this.#path.pop();
|
|
1821
|
-
}
|
|
1822
|
-
// This node is used for Prettier - always add parsed CSS as children
|
|
1823
|
-
// for proper formatting, regardless of whether it's inside head or not
|
|
1824
|
-
/** @type {AST.Element} */ (element).children = [
|
|
1825
|
-
/** @type {AST.Node} */ (/** @type {unknown} */ (parsed_css)),
|
|
1826
|
-
];
|
|
1827
|
-
|
|
1828
|
-
// Ensure we escape JSX <tag></tag> context
|
|
1829
|
-
const curContext = this.curContext();
|
|
1830
|
-
const parent = this.#path.at(-1);
|
|
1831
|
-
const insideTemplate =
|
|
1832
|
-
parent?.type === 'Component' ||
|
|
1833
|
-
parent?.type === 'Element' ||
|
|
1834
|
-
parent?.type === 'Tsx' ||
|
|
1835
|
-
parent?.type === 'TsxCompat';
|
|
1836
|
-
|
|
1837
|
-
if (curContext === tstc.tc_expr && !insideTemplate) {
|
|
1838
|
-
this.context.pop();
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
/** @type {AST.Element} */ (element).css = content;
|
|
1842
|
-
} else {
|
|
1843
|
-
this.enterScope(0);
|
|
1844
|
-
this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
|
|
1845
|
-
this.exitScope();
|
|
1846
|
-
|
|
1847
|
-
if (element.type === 'Tsx') {
|
|
1848
|
-
this.#path.pop();
|
|
1849
|
-
|
|
1850
|
-
if (!element.unclosed) {
|
|
1851
|
-
const raise_error = () => {
|
|
1852
|
-
this.raise(this.start, `Expected closing tag '</tsx>'`);
|
|
1853
|
-
};
|
|
1854
|
-
|
|
1855
|
-
this.next();
|
|
1856
|
-
// we should expect to see </tsx>
|
|
1857
|
-
if (this.value !== '/') {
|
|
1858
|
-
raise_error();
|
|
1859
|
-
}
|
|
1860
|
-
this.next();
|
|
1861
|
-
if (this.value !== 'tsx') {
|
|
1862
|
-
raise_error();
|
|
1863
|
-
}
|
|
1864
|
-
this.next();
|
|
1865
|
-
if (this.type !== tstt.jsxTagEnd) {
|
|
1866
|
-
raise_error();
|
|
1867
|
-
}
|
|
1868
|
-
this.next();
|
|
1869
|
-
}
|
|
1870
|
-
} else if (element.type === 'TsxCompat') {
|
|
1871
|
-
this.#path.pop();
|
|
1872
|
-
|
|
1873
|
-
if (!element.unclosed) {
|
|
1874
|
-
const raise_error = () => {
|
|
1875
|
-
this.raise(this.start, `Expected closing tag '</tsx:${element.kind}>'`);
|
|
1876
|
-
};
|
|
1877
|
-
|
|
1878
|
-
this.next();
|
|
1879
|
-
// we should expect to see </tsx:kind>
|
|
1880
|
-
if (this.value !== '/') {
|
|
1881
|
-
raise_error();
|
|
1882
|
-
}
|
|
1883
|
-
this.next();
|
|
1884
|
-
if (this.value !== 'tsx') {
|
|
1885
|
-
raise_error();
|
|
1886
|
-
}
|
|
1887
|
-
this.next();
|
|
1888
|
-
if (this.type.label !== ':') {
|
|
1889
|
-
raise_error();
|
|
1890
|
-
}
|
|
1891
|
-
this.next();
|
|
1892
|
-
if (this.value !== element.kind) {
|
|
1893
|
-
raise_error();
|
|
1894
|
-
}
|
|
1895
|
-
this.next();
|
|
1896
|
-
if (this.type !== tstt.jsxTagEnd) {
|
|
1897
|
-
raise_error();
|
|
1898
|
-
}
|
|
1899
|
-
this.next();
|
|
1900
|
-
}
|
|
1901
|
-
} else if (this.#path[this.#path.length - 1] === element) {
|
|
1902
|
-
// Check if this element was properly closed
|
|
1903
|
-
if (!this.#loose) {
|
|
1904
|
-
const tagName = this.getElementName(element.id);
|
|
1905
|
-
this.raise(
|
|
1906
|
-
this.start,
|
|
1907
|
-
`Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
|
|
1908
|
-
);
|
|
1909
|
-
} else {
|
|
1910
|
-
element.unclosed = true;
|
|
1911
|
-
element.loc.end = {
|
|
1912
|
-
.../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
|
|
1913
|
-
};
|
|
1914
|
-
element.end = element.openingElement.end;
|
|
1915
|
-
this.#path.pop();
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
// Ensure we escape JSX <tag></tag> context
|
|
1921
|
-
const curContext = this.curContext();
|
|
1922
|
-
const parent = this.#path.at(-1);
|
|
1923
|
-
const insideTemplate =
|
|
1924
|
-
parent?.type === 'Component' ||
|
|
1925
|
-
parent?.type === 'Element' ||
|
|
1926
|
-
parent?.type === 'Tsx' ||
|
|
1927
|
-
parent?.type === 'TsxCompat';
|
|
1928
|
-
|
|
1929
|
-
if (curContext === tstc.tc_expr && !insideTemplate) {
|
|
1930
|
-
this.context.pop();
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
if (element.closingElement && !is_tsx_compat && !is_tsx) {
|
|
1935
|
-
/** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
|
|
1936
|
-
element.closingElement.name,
|
|
1937
|
-
);
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
this.finishNode(element, element.type);
|
|
1941
|
-
return element;
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
/**
|
|
1945
|
-
* @type {Parse.Parser['parseTemplateBody']}
|
|
1946
|
-
*/
|
|
1947
|
-
parseTemplateBody(body) {
|
|
1948
|
-
const inside_func =
|
|
1949
|
-
this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
|
|
1950
|
-
const inside_tsx = this.#path.findLast((n) => n.type === 'Tsx');
|
|
1951
|
-
const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
|
|
1952
|
-
|
|
1953
|
-
if (!inside_func) {
|
|
1954
|
-
if (this.type.label === 'continue') {
|
|
1955
|
-
throw new Error('`continue` statements are not allowed in components');
|
|
1956
|
-
}
|
|
1957
|
-
if (this.type.label === 'break') {
|
|
1958
|
-
throw new Error('`break` statements are not allowed in components');
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
if (inside_tsx) {
|
|
1963
|
-
this.exprAllowed = true;
|
|
1964
|
-
|
|
1965
|
-
while (true) {
|
|
1966
|
-
if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
|
|
1967
|
-
if (!this.#loose) {
|
|
1968
|
-
this.raise(
|
|
1969
|
-
this.start,
|
|
1970
|
-
`Unclosed tag '<tsx>'. Expected '</tsx>' before end of component.`,
|
|
1971
|
-
);
|
|
1972
|
-
} else {
|
|
1973
|
-
inside_tsx.unclosed = true;
|
|
1974
|
-
/** @type {AST.NodeWithLocation} */ (inside_tsx).loc.end = {
|
|
1975
|
-
.../** @type {AST.SourceLocation} */ (inside_tsx.openingElement.loc).end,
|
|
1976
|
-
};
|
|
1977
|
-
inside_tsx.end = inside_tsx.openingElement.end;
|
|
1978
|
-
}
|
|
1979
|
-
return;
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
if (this.input.slice(this.pos, this.pos + 4) === '/tsx') {
|
|
1983
|
-
const after = this.input.charCodeAt(this.pos + 4);
|
|
1984
|
-
// Make sure it's </tsx> and not </tsx:...>
|
|
1985
|
-
if (after === 62 /* > */) {
|
|
1986
|
-
return;
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
if (this.type === tt.braceL) {
|
|
1991
|
-
const node = this.jsx_parseExpressionContainer();
|
|
1992
|
-
body.push(node);
|
|
1993
|
-
} else if (this.type === tstt.jsxTagStart) {
|
|
1994
|
-
// Parse JSX element
|
|
1995
|
-
const node = super.parseExpression();
|
|
1996
|
-
body.push(node);
|
|
1997
|
-
} else {
|
|
1998
|
-
const start = this.start;
|
|
1999
|
-
this.pos = start;
|
|
2000
|
-
let text = '';
|
|
2001
|
-
|
|
2002
|
-
while (this.pos < this.input.length) {
|
|
2003
|
-
const ch = this.input.charCodeAt(this.pos);
|
|
2004
|
-
|
|
2005
|
-
// Stop at opening tag, expression, or the component-closing brace
|
|
2006
|
-
if (ch === 60 || ch === 123 || ch === 125) {
|
|
2007
|
-
// < or { or }
|
|
2008
|
-
break;
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
text += this.input[this.pos];
|
|
2012
|
-
this.pos++;
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
if (text) {
|
|
2016
|
-
const node = /** @type {ESTreeJSX.JSXText} */ ({
|
|
2017
|
-
type: 'JSXText',
|
|
2018
|
-
value: text,
|
|
2019
|
-
raw: text,
|
|
2020
|
-
start,
|
|
2021
|
-
end: this.pos,
|
|
2022
|
-
});
|
|
2023
|
-
body.push(node);
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
// Always call next() to ensure parser makes progress
|
|
2027
|
-
this.next();
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
if (inside_tsx_compat) {
|
|
2032
|
-
this.exprAllowed = true;
|
|
2033
|
-
|
|
2034
|
-
while (true) {
|
|
2035
|
-
if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
|
|
2036
|
-
if (!this.#loose) {
|
|
2037
|
-
this.raise(
|
|
2038
|
-
this.start,
|
|
2039
|
-
`Unclosed tag '<tsx:${inside_tsx_compat.kind}>'. Expected '</tsx:${inside_tsx_compat.kind}>' before end of component.`,
|
|
2040
|
-
);
|
|
2041
|
-
} else {
|
|
2042
|
-
inside_tsx_compat.unclosed = true;
|
|
2043
|
-
/** @type {AST.NodeWithLocation} */ (inside_tsx_compat).loc.end = {
|
|
2044
|
-
.../** @type {AST.SourceLocation} */ (inside_tsx_compat.openingElement.loc).end,
|
|
2045
|
-
};
|
|
2046
|
-
inside_tsx_compat.end = inside_tsx_compat.openingElement.end;
|
|
2047
|
-
}
|
|
2048
|
-
return;
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
|
|
2052
|
-
return;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
if (this.type === tt.braceL) {
|
|
2056
|
-
const node = this.jsx_parseExpressionContainer();
|
|
2057
|
-
body.push(node);
|
|
2058
|
-
} else if (this.type === tstt.jsxTagStart) {
|
|
2059
|
-
// Parse JSX element
|
|
2060
|
-
const node = super.parseExpression();
|
|
2061
|
-
body.push(node);
|
|
2062
|
-
} else {
|
|
2063
|
-
const start = this.start;
|
|
2064
|
-
this.pos = start;
|
|
2065
|
-
let text = '';
|
|
2066
|
-
|
|
2067
|
-
while (this.pos < this.input.length) {
|
|
2068
|
-
const ch = this.input.charCodeAt(this.pos);
|
|
2069
|
-
|
|
2070
|
-
// Stop at opening tag, expression, or the component-closing brace
|
|
2071
|
-
if (ch === 60 || ch === 123 || ch === 125) {
|
|
2072
|
-
// < or { or }
|
|
2073
|
-
break;
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
text += this.input[this.pos];
|
|
2077
|
-
this.pos++;
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
if (text) {
|
|
2081
|
-
const node = /** @type {ESTreeJSX.JSXText} */ ({
|
|
2082
|
-
type: 'JSXText',
|
|
2083
|
-
value: text,
|
|
2084
|
-
raw: text,
|
|
2085
|
-
start,
|
|
2086
|
-
end: this.pos,
|
|
2087
|
-
});
|
|
2088
|
-
body.push(node);
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
this.next();
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
if (this.type === tt.braceL) {
|
|
2096
|
-
const node = this.jsx_parseExpressionContainer();
|
|
2097
|
-
// Keep JSXEmptyExpression as-is (for prettier to handle comments)
|
|
2098
|
-
// but convert other expressions to Html/RippleExpression/Text nodes
|
|
2099
|
-
if (node.expression.type !== 'JSXEmptyExpression') {
|
|
2100
|
-
/** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (
|
|
2101
|
-
/** @type {unknown} */ (node)
|
|
2102
|
-
).type = node.html ? 'Html' : node.text ? 'Text' : 'RippleExpression';
|
|
2103
|
-
delete node.html;
|
|
2104
|
-
delete node.text;
|
|
2105
|
-
}
|
|
2106
|
-
body.push(node);
|
|
2107
|
-
} else if (this.type === tt.braceR) {
|
|
2108
|
-
// Leaving a component/template body. We may still be in TSX/JSX tokenization
|
|
2109
|
-
// context (e.g. after parsing markup), but the closing `}` is a JS token.
|
|
2110
|
-
// If we don't reset this here, the following `next()` can read EOF using
|
|
2111
|
-
// `jsx_readToken()` and throw "Unterminated JSX contents".
|
|
2112
|
-
while (this.curContext() === tstc.tc_expr) {
|
|
2113
|
-
this.context.pop();
|
|
2114
|
-
}
|
|
2115
|
-
return;
|
|
2116
|
-
} else if (this.type === tstt.jsxTagStart) {
|
|
2117
|
-
const startPos = this.start;
|
|
2118
|
-
const startLoc = this.startLoc;
|
|
2119
|
-
this.next();
|
|
2120
|
-
if (this.value === '/' || this.type === tt.slash) {
|
|
2121
|
-
// Consume '/'
|
|
2122
|
-
this.next();
|
|
2123
|
-
|
|
2124
|
-
const closingElement =
|
|
2125
|
-
/** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
|
|
2126
|
-
this.jsx_parseClosingElementAt(startPos, startLoc)
|
|
2127
|
-
);
|
|
2128
|
-
this.exprAllowed = false;
|
|
2129
|
-
|
|
2130
|
-
// Validate that the closing tag matches the opening tag
|
|
2131
|
-
const currentElement = this.#path[this.#path.length - 1];
|
|
2132
|
-
if (
|
|
2133
|
-
!currentElement ||
|
|
2134
|
-
(currentElement.type !== 'Element' &&
|
|
2135
|
-
currentElement.type !== 'Tsx' &&
|
|
2136
|
-
currentElement.type !== 'TsxCompat')
|
|
2137
|
-
) {
|
|
2138
|
-
this.raise(this.start, 'Unexpected closing tag');
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
/** @type {string | null} */
|
|
2142
|
-
let openingTagName;
|
|
2143
|
-
/** @type {string | null} */
|
|
2144
|
-
let closingTagName;
|
|
2145
|
-
|
|
2146
|
-
if (currentElement.type === 'TsxCompat') {
|
|
2147
|
-
openingTagName = 'tsx:' + currentElement.kind;
|
|
2148
|
-
closingTagName =
|
|
2149
|
-
closingElement.name?.type === 'JSXNamespacedName'
|
|
2150
|
-
? closingElement.name.namespace.name + ':' + closingElement.name.name.name
|
|
2151
|
-
: this.getElementName(closingElement.name);
|
|
2152
|
-
} else if (currentElement.type === 'Tsx') {
|
|
2153
|
-
openingTagName = 'tsx';
|
|
2154
|
-
closingTagName =
|
|
2155
|
-
closingElement.name?.type === 'JSXNamespacedName'
|
|
2156
|
-
? closingElement.name.namespace.name + ':' + closingElement.name.name.name
|
|
2157
|
-
: this.getElementName(closingElement.name);
|
|
2158
|
-
} else {
|
|
2159
|
-
// Regular Element node
|
|
2160
|
-
openingTagName = this.getElementName(currentElement.id);
|
|
2161
|
-
closingTagName =
|
|
2162
|
-
closingElement.name?.type === 'JSXNamespacedName'
|
|
2163
|
-
? closingElement.name.namespace.name + ':' + closingElement.name.name.name
|
|
2164
|
-
: this.getElementName(closingElement.name);
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
if (openingTagName !== closingTagName) {
|
|
2168
|
-
if (!this.#loose) {
|
|
2169
|
-
this.raise(
|
|
2170
|
-
closingElement.start,
|
|
2171
|
-
`Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
|
|
2172
|
-
);
|
|
2173
|
-
} else {
|
|
2174
|
-
// Loop through all unclosed elements on the stack
|
|
2175
|
-
while (this.#path.length > 0) {
|
|
2176
|
-
const elem = this.#path[this.#path.length - 1];
|
|
2177
|
-
|
|
2178
|
-
// Stop at non-Element boundaries (Component, etc.)
|
|
2179
|
-
if (elem.type !== 'Element' && elem.type !== 'Tsx' && elem.type !== 'TsxCompat') {
|
|
2180
|
-
break;
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
const elemName =
|
|
2184
|
-
elem.type === 'TsxCompat'
|
|
2185
|
-
? 'tsx:' + elem.kind
|
|
2186
|
-
: elem.type === 'Tsx'
|
|
2187
|
-
? 'tsx'
|
|
2188
|
-
: this.getElementName(elem.id);
|
|
2189
|
-
|
|
2190
|
-
// Found matching opening tag
|
|
2191
|
-
if (elemName === closingTagName) {
|
|
2192
|
-
break;
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
// Mark as unclosed and adjust location
|
|
2196
|
-
elem.unclosed = true;
|
|
2197
|
-
/** @type {AST.NodeWithLocation} */ (elem).loc.end = {
|
|
2198
|
-
.../** @type {AST.SourceLocation} */ (elem.openingElement.loc).end,
|
|
2199
|
-
};
|
|
2200
|
-
elem.end = elem.openingElement.end;
|
|
2201
|
-
|
|
2202
|
-
this.#path.pop(); // Remove from stack
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
const elementToClose = this.#path[this.#path.length - 1];
|
|
2208
|
-
if (elementToClose && elementToClose.type === 'Element') {
|
|
2209
|
-
const elementToCloseName = this.getElementName(
|
|
2210
|
-
/** @type {AST.Element} */ (elementToClose).id,
|
|
2211
|
-
);
|
|
2212
|
-
if (elementToCloseName === closingTagName) {
|
|
2213
|
-
/** @type {AST.Element} */ (elementToClose).closingElement = closingElement;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
this.#path.pop();
|
|
2218
|
-
skipWhitespace(this);
|
|
2219
|
-
return;
|
|
2220
|
-
}
|
|
2221
|
-
const node = this.parseElement();
|
|
2222
|
-
if (node !== null) {
|
|
2223
|
-
body.push(node);
|
|
2224
|
-
}
|
|
2225
|
-
} else {
|
|
2226
|
-
skipWhitespace(this);
|
|
2227
|
-
const node = this.parseStatement(null);
|
|
2228
|
-
body.push(node);
|
|
2229
|
-
|
|
2230
|
-
// Ensure we're not in JSX context before recursing
|
|
2231
|
-
// This is important when elements are parsed at statement level
|
|
2232
|
-
if (this.curContext() === tstc.tc_expr) {
|
|
2233
|
-
this.context.pop();
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
this.parseTemplateBody(body);
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
/**
|
|
2241
|
-
* @type {Parse.Parser['parseStatement']}
|
|
2242
|
-
*/
|
|
2243
|
-
parseStatement(context, topLevel, exports) {
|
|
2244
|
-
if (
|
|
2245
|
-
context !== 'for' &&
|
|
2246
|
-
context !== 'if' &&
|
|
2247
|
-
this.context.at(-1) === b_stat &&
|
|
2248
|
-
this.type === tt.braceL &&
|
|
2249
|
-
this.context.some((c) => c === tstc.tc_expr)
|
|
2250
|
-
) {
|
|
2251
|
-
const node = this.jsx_parseExpressionContainer();
|
|
2252
|
-
// Keep JSXEmptyExpression as-is (don't convert to RippleExpression/Text/Html)
|
|
2253
|
-
if (node.expression.type !== 'JSXEmptyExpression') {
|
|
2254
|
-
/** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (
|
|
2255
|
-
/** @type {unknown} */ (node)
|
|
2256
|
-
).type = node.html ? 'Html' : node.text ? 'Text' : 'RippleExpression';
|
|
2257
|
-
delete node.html;
|
|
2258
|
-
delete node.text;
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
return /** @type {ESTreeJSX.JSXEmptyExpression | AST.RippleExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
|
|
2262
|
-
/** @type {unknown} */ (node)
|
|
2263
|
-
);
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
if (this.value === '#server') {
|
|
2267
|
-
// Peek ahead to see if this is a server block (#server { ... }) vs
|
|
2268
|
-
// a server identifier expression (#server.fn(), #server.fn().then())
|
|
2269
|
-
let peek_pos = this.end;
|
|
2270
|
-
while (peek_pos < this.input.length && /\s/.test(this.input[peek_pos])) peek_pos++;
|
|
2271
|
-
if (peek_pos < this.input.length && this.input.charCodeAt(peek_pos) === 123) {
|
|
2272
|
-
// Next non-whitespace character is '{' — parse as server block
|
|
2273
|
-
return this.parseServerBlock();
|
|
2274
|
-
}
|
|
2275
|
-
// Otherwise fall through to parse as expression statement (e.g., #server.fn().then(...))
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
if (this.value === 'component') {
|
|
2279
|
-
this.awaitPos = 0;
|
|
2280
|
-
return this.parseComponent({ requireName: true, declareName: true });
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
if (this.type === tstt.jsxTagStart) {
|
|
2284
|
-
this.next();
|
|
2285
|
-
if (this.value === '/') {
|
|
2286
|
-
this.unexpected();
|
|
2287
|
-
}
|
|
2288
|
-
const node = this.parseElement();
|
|
2289
|
-
|
|
2290
|
-
if (!node) {
|
|
2291
|
-
this.unexpected();
|
|
2292
|
-
}
|
|
2293
|
-
return node;
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
// &[ or &{ at statement level — lazy destructuring assignment
|
|
2297
|
-
// e.g., &[data] = track(0); or &{x, y} = obj;
|
|
2298
|
-
if (this.type === tt.bitwiseAND) {
|
|
2299
|
-
const charAfterAmp = this.input.charCodeAt(this.end);
|
|
2300
|
-
if (charAfterAmp === 123 || charAfterAmp === 91) {
|
|
2301
|
-
const node = /** @type {AST.ExpressionStatement} */ (this.startNode());
|
|
2302
|
-
const assign_node = /** @type {AST.AssignmentExpression} */ (this.startNode());
|
|
2303
|
-
this.next(); // consume &
|
|
2304
|
-
// Parse the left-hand side (array or object expression)
|
|
2305
|
-
const left = /** @type {AST.ArrayPattern | AST.ObjectPattern} */ (
|
|
2306
|
-
/** @type {unknown} */ (this.parseExprAtom())
|
|
2307
|
-
);
|
|
2308
|
-
// Convert expression to destructuring pattern
|
|
2309
|
-
this.toAssignable(left, false);
|
|
2310
|
-
left.lazy = true;
|
|
2311
|
-
// Expect = operator
|
|
2312
|
-
this.expect(tt.eq);
|
|
2313
|
-
// Parse the right-hand side
|
|
2314
|
-
assign_node.operator = '=';
|
|
2315
|
-
assign_node.left = left;
|
|
2316
|
-
assign_node.right = /** @type {AST.Expression} */ (this.parseMaybeAssign());
|
|
2317
|
-
node.expression = /** @type {AST.AssignmentExpression} */ (
|
|
2318
|
-
this.finishNode(assign_node, 'AssignmentExpression')
|
|
2319
|
-
);
|
|
2320
|
-
this.semicolon();
|
|
2321
|
-
return /** @type {AST.ExpressionStatement} */ (
|
|
2322
|
-
this.finishNode(node, 'ExpressionStatement')
|
|
2323
|
-
);
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
return super.parseStatement(context, topLevel, exports);
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
/**
|
|
2331
|
-
* @type {Parse.Parser['parseBlock']}
|
|
2332
|
-
*/
|
|
2333
|
-
parseBlock(createNewLexicalScope, node, exitStrict) {
|
|
2334
|
-
const parent = this.#path.at(-1);
|
|
2335
|
-
|
|
2336
|
-
if (parent?.type === 'Component' || parent?.type === 'Element') {
|
|
2337
|
-
if (createNewLexicalScope === void 0) createNewLexicalScope = true;
|
|
2338
|
-
if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());
|
|
2339
|
-
|
|
2340
|
-
node.body = [];
|
|
2341
|
-
this.expect(tt.braceL);
|
|
2342
|
-
if (createNewLexicalScope) {
|
|
2343
|
-
this.enterScope(0);
|
|
2344
|
-
}
|
|
2345
|
-
this.parseTemplateBody(node.body);
|
|
2346
|
-
|
|
2347
|
-
if (exitStrict) {
|
|
2348
|
-
this.strict = false;
|
|
2349
|
-
}
|
|
2350
|
-
this.exprAllowed = true;
|
|
2351
|
-
|
|
2352
|
-
this.next();
|
|
2353
|
-
if (createNewLexicalScope) {
|
|
2354
|
-
this.exitScope();
|
|
2355
|
-
}
|
|
2356
|
-
return this.finishNode(node, 'BlockStatement');
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
return super.parseBlock(createNewLexicalScope, node, exitStrict);
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
return /** @type {Parse.ParserConstructor} */ (RippleParser);
|
|
2364
|
-
};
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
/**
|
|
2368
|
-
* Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
|
|
2369
|
-
* to add them after the fact. They are needed in order to support `ripple-ignore` comments
|
|
2370
|
-
* in JS code and so that `prettier-plugin` doesn't remove all comments when formatting.
|
|
2371
|
-
* @param {string} source
|
|
2372
|
-
* @param {AST.CommentWithLocation[]} comments
|
|
2373
|
-
* @param {number} [index=0] - Starting index
|
|
2374
|
-
* @returns {{onComment: Parse.Options['onComment'], add_comments: (ast: AST.Node) => void}}
|
|
2375
|
-
*/
|
|
2376
|
-
function get_comment_handlers(source, comments, index = 0) {
|
|
2377
|
-
/**
|
|
2378
|
-
* @param {string} text
|
|
2379
|
-
* @param {number} startIndex
|
|
2380
|
-
* @returns {string | null}
|
|
2381
|
-
*/
|
|
2382
|
-
function getNextNonWhitespaceCharacter(text, startIndex) {
|
|
2383
|
-
for (let i = startIndex; i < text.length; i++) {
|
|
2384
|
-
const char = text[i];
|
|
2385
|
-
if (char !== ' ' && char !== '\t' && char !== '\n' && char !== '\r') {
|
|
2386
|
-
return char;
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
return null;
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
return {
|
|
2393
|
-
/**
|
|
2394
|
-
* @type {Parse.Options['onComment']}
|
|
2395
|
-
*/
|
|
2396
|
-
onComment: (block, value, start, end, start_loc, end_loc, metadata) => {
|
|
2397
|
-
if (block && /\n/.test(value)) {
|
|
2398
|
-
let a = start;
|
|
2399
|
-
while (a > 0 && source[a - 1] !== '\n') a -= 1;
|
|
2400
|
-
|
|
2401
|
-
let b = a;
|
|
2402
|
-
while (/[ \t]/.test(source[b])) b += 1;
|
|
2403
|
-
|
|
2404
|
-
const indentation = source.slice(a, b);
|
|
2405
|
-
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
comments.push({
|
|
2409
|
-
type: block ? 'Block' : 'Line',
|
|
2410
|
-
value,
|
|
2411
|
-
start,
|
|
2412
|
-
end,
|
|
2413
|
-
loc: {
|
|
2414
|
-
start: start_loc,
|
|
2415
|
-
end: end_loc,
|
|
2416
|
-
},
|
|
2417
|
-
context: metadata ?? null,
|
|
2418
|
-
});
|
|
2419
|
-
},
|
|
2420
|
-
|
|
2421
|
-
/**
|
|
2422
|
-
* @param {AST.Node | AST.CSS.StyleSheet} ast
|
|
2423
|
-
*/
|
|
2424
|
-
add_comments: (ast) => {
|
|
2425
|
-
if (comments.length === 0) return;
|
|
2426
|
-
|
|
2427
|
-
comments = comments
|
|
2428
|
-
.filter((comment) => comment.start >= index)
|
|
2429
|
-
.map(({ type, value, start, end, loc, context }) => ({
|
|
2430
|
-
type,
|
|
2431
|
-
value,
|
|
2432
|
-
start,
|
|
2433
|
-
end,
|
|
2434
|
-
loc,
|
|
2435
|
-
context,
|
|
2436
|
-
}));
|
|
2437
|
-
|
|
2438
|
-
walk(ast, null, {
|
|
2439
|
-
_(node, { next, path }) {
|
|
2440
|
-
const metadata = /** @type {AST.Node} */ (node)?.metadata;
|
|
2441
|
-
|
|
2442
|
-
/**
|
|
2443
|
-
* Check if a comment is inside an attribute expression
|
|
2444
|
-
* of any ancestor Elements.
|
|
2445
|
-
* @returns {boolean}
|
|
2446
|
-
*/
|
|
2447
|
-
function isCommentInsideAttributeExpression() {
|
|
2448
|
-
for (let i = path.length - 1; i >= 0; i--) {
|
|
2449
|
-
const ancestor = path[i];
|
|
2450
|
-
if (
|
|
2451
|
-
ancestor &&
|
|
2452
|
-
(ancestor.type === 'JSXAttribute' ||
|
|
2453
|
-
ancestor.type === 'Attribute' ||
|
|
2454
|
-
ancestor.type === 'JSXExpressionContainer')
|
|
2455
|
-
) {
|
|
2456
|
-
return true;
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
return false;
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
/**
|
|
2463
|
-
* Check if a comment is inside any attribute of ancestor Elements,
|
|
2464
|
-
* but NOT if we're currently traversing inside that attribute.
|
|
2465
|
-
* @param {AST.CommentWithLocation} comment
|
|
2466
|
-
* @returns {boolean}
|
|
2467
|
-
*/
|
|
2468
|
-
function isCommentInsideUnvisitedAttribute(comment) {
|
|
2469
|
-
for (let i = path.length - 1; i >= 0; i--) {
|
|
2470
|
-
const ancestor = path[i];
|
|
2471
|
-
// we would definitely reach the attribute first before getting to the element
|
|
2472
|
-
if (ancestor.type === 'JSXAttribute' || ancestor.type === 'Attribute') {
|
|
2473
|
-
return false;
|
|
2474
|
-
}
|
|
2475
|
-
if (ancestor && ancestor.type === 'Element') {
|
|
2476
|
-
for (const attr of /** @type {(AST.Attribute & AST.NodeWithLocation)[]} */ (
|
|
2477
|
-
ancestor.attributes
|
|
2478
|
-
)) {
|
|
2479
|
-
if (comment.start >= attr.start && comment.end <= attr.end) {
|
|
2480
|
-
return true;
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
return false;
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
/**
|
|
2489
|
-
* If a comment is located between an empty Element's opening and closing tags,
|
|
2490
|
-
* attach it to the Element as `innerComments`.
|
|
2491
|
-
* @param {AST.CommentWithLocation} comment
|
|
2492
|
-
* @returns {AST.Element | null}
|
|
2493
|
-
*/
|
|
2494
|
-
function getEmptyElementInnerCommentTarget(comment) {
|
|
2495
|
-
const element = /** @type {AST.Element | undefined} */ (
|
|
2496
|
-
path.findLast((ancestor) => ancestor && ancestor.type === 'Element')
|
|
2497
|
-
);
|
|
2498
|
-
if (
|
|
2499
|
-
!element ||
|
|
2500
|
-
element.children.length > 0 ||
|
|
2501
|
-
!element.closingElement ||
|
|
2502
|
-
!(
|
|
2503
|
-
comment.start >= /** @type {AST.NodeWithLocation} */ (element.openingElement).end &&
|
|
2504
|
-
comment.end <= /** @type {AST.NodeWithLocation} */ (element).end
|
|
2505
|
-
)
|
|
2506
|
-
) {
|
|
2507
|
-
return null;
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
|
-
return element;
|
|
2511
|
-
}
|
|
2512
|
-
|
|
2513
|
-
// Skip CSS nodes entirely - they use CSS-local positions (relative to
|
|
2514
|
-
// the <style> tag content) which would incorrectly match against
|
|
2515
|
-
// absolute source positions of JS/HTML comments. Also consume any
|
|
2516
|
-
// CSS comments (which have absolute positions) that fall within the
|
|
2517
|
-
// parent <style> element's content range so they don't leak to
|
|
2518
|
-
// subsequent JS nodes.
|
|
2519
|
-
if (node.type === 'StyleSheet') {
|
|
2520
|
-
const styleElement = /** @type {AST.Element & AST.NodeWithLocation | undefined} */ (
|
|
2521
|
-
path.findLast(
|
|
2522
|
-
(ancestor) =>
|
|
2523
|
-
ancestor &&
|
|
2524
|
-
ancestor.type === 'Element' &&
|
|
2525
|
-
ancestor.id &&
|
|
2526
|
-
/** @type {AST.Identifier} */ (ancestor.id).name === 'style',
|
|
2527
|
-
)
|
|
2528
|
-
);
|
|
2529
|
-
if (styleElement) {
|
|
2530
|
-
const cssStart =
|
|
2531
|
-
/** @type {AST.NodeWithLocation} */ (styleElement.openingElement)?.end ??
|
|
2532
|
-
styleElement.start;
|
|
2533
|
-
const cssEnd =
|
|
2534
|
-
/** @type {AST.NodeWithLocation} */ (styleElement.closingElement)?.start ??
|
|
2535
|
-
styleElement.end;
|
|
2536
|
-
while (comments[0] && comments[0].start >= cssStart && comments[0].end <= cssEnd) {
|
|
2537
|
-
comments.shift();
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
return;
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
if (metadata && metadata.commentContainerId !== undefined) {
|
|
2544
|
-
// For empty template elements, keep comments as `innerComments`.
|
|
2545
|
-
// The Prettier plugin uses `innerComments` to preserve them and
|
|
2546
|
-
// to avoid collapsing the element into self-closing syntax.
|
|
2547
|
-
const isEmptyElement =
|
|
2548
|
-
node.type === 'Element' && (!node.children || node.children.length === 0);
|
|
2549
|
-
if (!isEmptyElement) {
|
|
2550
|
-
while (
|
|
2551
|
-
comments[0] &&
|
|
2552
|
-
comments[0].context &&
|
|
2553
|
-
comments[0].context.containerId === metadata.commentContainerId &&
|
|
2554
|
-
comments[0].context.beforeMeaningfulChild
|
|
2555
|
-
) {
|
|
2556
|
-
// Check that the comment is actually in this element's own content
|
|
2557
|
-
// area, not positionally inside a child element. This handles the
|
|
2558
|
-
// case where jsx_parseOpeningElementAt() triggers jsx_readToken()
|
|
2559
|
-
// before the child element is pushed to the parser's #path, causing
|
|
2560
|
-
// comments inside the child to get the parent's containerId.
|
|
2561
|
-
const commentStart = comments[0].start;
|
|
2562
|
-
const isInsideChildElement = /** @type {AST.NodeWithChildren} */ (
|
|
2563
|
-
node
|
|
2564
|
-
).children?.some(
|
|
2565
|
-
(child) =>
|
|
2566
|
-
child &&
|
|
2567
|
-
child.start !== undefined &&
|
|
2568
|
-
child.end !== undefined &&
|
|
2569
|
-
commentStart >= child.start &&
|
|
2570
|
-
commentStart < child.end,
|
|
2571
|
-
);
|
|
2572
|
-
if (isInsideChildElement) break;
|
|
2573
|
-
|
|
2574
|
-
const elementComment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
2575
|
-
|
|
2576
|
-
(metadata.elementLeadingComments ||= []).push(elementComment);
|
|
2577
|
-
}
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
while (
|
|
2582
|
-
comments[0] &&
|
|
2583
|
-
comments[0].start < /** @type {AST.NodeWithLocation} */ (node).start
|
|
2584
|
-
) {
|
|
2585
|
-
// Skip comments that are inside an attribute of an ancestor Element.
|
|
2586
|
-
// Since zimmerframe visits children before attributes, we need to leave
|
|
2587
|
-
// these comments for when the attribute nodes are visited.
|
|
2588
|
-
if (
|
|
2589
|
-
isCommentInsideUnvisitedAttribute(
|
|
2590
|
-
/** @type {AST.CommentWithLocation} */ (comments[0]),
|
|
2591
|
-
)
|
|
2592
|
-
) {
|
|
2593
|
-
break;
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
const maybeInner = getEmptyElementInnerCommentTarget(
|
|
2597
|
-
/** @type {AST.CommentWithLocation} */ (comments[0]),
|
|
2598
|
-
);
|
|
2599
|
-
if (maybeInner) {
|
|
2600
|
-
(maybeInner.innerComments ||= []).push(
|
|
2601
|
-
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
2602
|
-
);
|
|
2603
|
-
continue;
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
2607
|
-
|
|
2608
|
-
// Skip leading comments for BlockStatement that is a function body
|
|
2609
|
-
// These comments should be dangling on the function instead
|
|
2610
|
-
if (node.type === 'BlockStatement') {
|
|
2611
|
-
const parent = path.at(-1);
|
|
2612
|
-
if (
|
|
2613
|
-
parent &&
|
|
2614
|
-
(parent.type === 'FunctionDeclaration' ||
|
|
2615
|
-
parent.type === 'FunctionExpression' ||
|
|
2616
|
-
parent.type === 'ArrowFunctionExpression') &&
|
|
2617
|
-
parent.body === node
|
|
2618
|
-
) {
|
|
2619
|
-
// This is a function body - don't attach comment, let it be handled by function
|
|
2620
|
-
(parent.comments ||= []).push(comment);
|
|
2621
|
-
continue;
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
if (isCommentInsideAttributeExpression()) {
|
|
2626
|
-
(node.leadingComments ||= []).push(comment);
|
|
2627
|
-
continue;
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
const ancestorElements = /** @type {(AST.Element & AST.NodeWithLocation)[]} */ (
|
|
2631
|
-
path.filter((ancestor) => ancestor && ancestor.type === 'Element' && ancestor.loc)
|
|
2632
|
-
).sort((a, b) => a.loc.start.line - b.loc.start.line);
|
|
2633
|
-
|
|
2634
|
-
const targetAncestor = ancestorElements.find(
|
|
2635
|
-
(ancestor) => comment.loc.start.line < ancestor.loc.start.line,
|
|
2636
|
-
);
|
|
2637
|
-
|
|
2638
|
-
if (targetAncestor) {
|
|
2639
|
-
targetAncestor.metadata ??= { path: [] };
|
|
2640
|
-
(targetAncestor.metadata.elementLeadingComments ||= []).push(comment);
|
|
2641
|
-
continue;
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
(node.leadingComments ||= []).push(comment);
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
next();
|
|
2648
|
-
|
|
2649
|
-
if (comments[0]) {
|
|
2650
|
-
if (node.type === 'Program' && node.body.length === 0) {
|
|
2651
|
-
// Collect all comments in an empty program (file with only comments)
|
|
2652
|
-
while (comments.length) {
|
|
2653
|
-
const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
2654
|
-
(node.innerComments ||= []).push(comment);
|
|
2655
|
-
}
|
|
2656
|
-
if (node.innerComments && node.innerComments.length > 0) {
|
|
2657
|
-
return;
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
if (node.type === 'BlockStatement' && node.body.length === 0) {
|
|
2661
|
-
// Collect all comments that fall within this empty block
|
|
2662
|
-
while (
|
|
2663
|
-
comments[0] &&
|
|
2664
|
-
comments[0].start < /** @type {AST.NodeWithLocation} */ (node).end &&
|
|
2665
|
-
comments[0].end < /** @type {AST.NodeWithLocation} */ (node).end
|
|
2666
|
-
) {
|
|
2667
|
-
const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
2668
|
-
(node.innerComments ||= []).push(comment);
|
|
2669
|
-
}
|
|
2670
|
-
if (node.innerComments && node.innerComments.length > 0) {
|
|
2671
|
-
return;
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
// Handle JSXEmptyExpression - these represent {/* comment */} in JSX
|
|
2675
|
-
if (node.type === 'JSXEmptyExpression') {
|
|
2676
|
-
// Collect all comments that fall within this JSXEmptyExpression
|
|
2677
|
-
while (
|
|
2678
|
-
comments[0] &&
|
|
2679
|
-
comments[0].start >= /** @type {AST.NodeWithLocation} */ (node).start &&
|
|
2680
|
-
comments[0].end <= /** @type {AST.NodeWithLocation} */ (node).end
|
|
2681
|
-
) {
|
|
2682
|
-
const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
2683
|
-
(node.innerComments ||= []).push(comment);
|
|
2684
|
-
}
|
|
2685
|
-
if (node.innerComments && node.innerComments.length > 0) {
|
|
2686
|
-
return;
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2689
|
-
// Handle empty Element nodes the same way as empty BlockStatements
|
|
2690
|
-
if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
|
|
2691
|
-
// Collect all comments that fall within this empty element
|
|
2692
|
-
while (
|
|
2693
|
-
comments[0] &&
|
|
2694
|
-
comments[0].start < /** @type {AST.NodeWithLocation} */ (node).end &&
|
|
2695
|
-
comments[0].end < /** @type {AST.NodeWithLocation} */ (node).end
|
|
2696
|
-
) {
|
|
2697
|
-
const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
2698
|
-
(node.innerComments ||= []).push(comment);
|
|
2699
|
-
}
|
|
2700
|
-
if (node.innerComments && node.innerComments.length > 0) {
|
|
2701
|
-
return;
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
const parent = /** @type {AST.Node & AST.NodeWithLocation} */ (path.at(-1));
|
|
2706
|
-
|
|
2707
|
-
if (parent === undefined || node.end !== parent.end) {
|
|
2708
|
-
const slice = source.slice(node.end, comments[0].start);
|
|
2709
|
-
|
|
2710
|
-
// Check if this node is the last item in an array-like structure
|
|
2711
|
-
let is_last_in_array = false;
|
|
2712
|
-
/** @type {(AST.Node | null)[] | null} */
|
|
2713
|
-
let node_array = null;
|
|
2714
|
-
let isParam = false;
|
|
2715
|
-
let isArgument = false;
|
|
2716
|
-
let isSwitchCaseSibling = false;
|
|
2717
|
-
|
|
2718
|
-
if (parent) {
|
|
2719
|
-
if (
|
|
2720
|
-
parent.type === 'BlockStatement' ||
|
|
2721
|
-
parent.type === 'Program' ||
|
|
2722
|
-
parent.type === 'Component' ||
|
|
2723
|
-
parent.type === 'ClassBody'
|
|
2724
|
-
) {
|
|
2725
|
-
node_array = parent.body;
|
|
2726
|
-
} else if (parent.type === 'SwitchStatement') {
|
|
2727
|
-
node_array = parent.cases;
|
|
2728
|
-
isSwitchCaseSibling = true;
|
|
2729
|
-
} else if (parent.type === 'SwitchCase') {
|
|
2730
|
-
node_array = parent.consequent;
|
|
2731
|
-
} else if (parent.type === 'ArrayExpression') {
|
|
2732
|
-
node_array = parent.elements;
|
|
2733
|
-
} else if (parent.type === 'ObjectExpression') {
|
|
2734
|
-
node_array = parent.properties;
|
|
2735
|
-
} else if (
|
|
2736
|
-
parent.type === 'FunctionDeclaration' ||
|
|
2737
|
-
parent.type === 'FunctionExpression' ||
|
|
2738
|
-
parent.type === 'ArrowFunctionExpression'
|
|
2739
|
-
) {
|
|
2740
|
-
node_array = parent.params;
|
|
2741
|
-
isParam = true;
|
|
2742
|
-
} else if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
|
|
2743
|
-
node_array = parent.arguments;
|
|
2744
|
-
isArgument = true;
|
|
2745
|
-
}
|
|
2746
|
-
}
|
|
2747
|
-
|
|
2748
|
-
if (node_array && Array.isArray(node_array)) {
|
|
2749
|
-
is_last_in_array = node_array.indexOf(node) === node_array.length - 1;
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2752
|
-
if (is_last_in_array) {
|
|
2753
|
-
if (isParam || isArgument) {
|
|
2754
|
-
while (comments.length) {
|
|
2755
|
-
const potentialComment = comments[0];
|
|
2756
|
-
if (parent && potentialComment.start >= parent.end) {
|
|
2757
|
-
break;
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
const maybeInner = getEmptyElementInnerCommentTarget(potentialComment);
|
|
2761
|
-
if (maybeInner) {
|
|
2762
|
-
(maybeInner.innerComments ||= []).push(
|
|
2763
|
-
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
2764
|
-
);
|
|
2765
|
-
continue;
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
const nextChar = getNextNonWhitespaceCharacter(source, potentialComment.end);
|
|
2769
|
-
if (nextChar === ')') {
|
|
2770
|
-
(node.trailingComments ||= []).push(
|
|
2771
|
-
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
2772
|
-
);
|
|
2773
|
-
continue;
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
break;
|
|
2777
|
-
}
|
|
2778
|
-
} else {
|
|
2779
|
-
// Special case: There can be multiple trailing comments after the last node in a block,
|
|
2780
|
-
// and they can be separated by newlines
|
|
2781
|
-
while (comments.length) {
|
|
2782
|
-
const comment = comments[0];
|
|
2783
|
-
if (parent && comment.start >= parent.end) break;
|
|
2784
|
-
|
|
2785
|
-
const maybeInner = getEmptyElementInnerCommentTarget(comment);
|
|
2786
|
-
if (maybeInner) {
|
|
2787
|
-
(maybeInner.innerComments ||= []).push(
|
|
2788
|
-
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
2789
|
-
);
|
|
2790
|
-
continue;
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
(node.trailingComments ||= []).push(comment);
|
|
2794
|
-
comments.shift();
|
|
2795
|
-
}
|
|
2796
|
-
}
|
|
2797
|
-
} else if (/** @type {AST.NodeWithLocation} */ (node).end <= comments[0].start) {
|
|
2798
|
-
const maybeInner = getEmptyElementInnerCommentTarget(
|
|
2799
|
-
/** @type {AST.CommentWithLocation} */ (comments[0]),
|
|
2800
|
-
);
|
|
2801
|
-
if (maybeInner) {
|
|
2802
|
-
(maybeInner.innerComments ||= []).push(
|
|
2803
|
-
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
2804
|
-
);
|
|
2805
|
-
return;
|
|
2806
|
-
}
|
|
2807
|
-
|
|
2808
|
-
const onlySimpleWhitespace = /^[,) \t]*$/.test(slice);
|
|
2809
|
-
const onlyWhitespace = /^\s*$/.test(slice);
|
|
2810
|
-
const hasBlankLine = /\n\s*\n/.test(slice);
|
|
2811
|
-
const nodeEndLine = node.loc?.end?.line ?? null;
|
|
2812
|
-
const commentStartLine = comments[0].loc?.start?.line ?? null;
|
|
2813
|
-
const isImmediateNextLine =
|
|
2814
|
-
nodeEndLine !== null &&
|
|
2815
|
-
commentStartLine !== null &&
|
|
2816
|
-
commentStartLine === nodeEndLine + 1;
|
|
2817
|
-
|
|
2818
|
-
if (isSwitchCaseSibling && !is_last_in_array) {
|
|
2819
|
-
if (
|
|
2820
|
-
nodeEndLine !== null &&
|
|
2821
|
-
commentStartLine !== null &&
|
|
2822
|
-
nodeEndLine === commentStartLine
|
|
2823
|
-
) {
|
|
2824
|
-
node.trailingComments = [
|
|
2825
|
-
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
2826
|
-
];
|
|
2827
|
-
}
|
|
2828
|
-
return;
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
if (
|
|
2832
|
-
onlySimpleWhitespace ||
|
|
2833
|
-
(onlyWhitespace && !hasBlankLine && isImmediateNextLine)
|
|
2834
|
-
) {
|
|
2835
|
-
// Check if this is a block comment that's inline with the next statement
|
|
2836
|
-
// e.g., /** @type {SomeType} */ (a) = 5;
|
|
2837
|
-
// These should be leading comments, not trailing
|
|
2838
|
-
if (comments[0].type === 'Block' && !is_last_in_array && node_array) {
|
|
2839
|
-
const currentIndex = node_array.indexOf(node);
|
|
2840
|
-
const nextSibling = node_array[currentIndex + 1];
|
|
2841
|
-
|
|
2842
|
-
if (nextSibling && nextSibling.loc) {
|
|
2843
|
-
const commentEndLine = comments[0].loc?.end?.line;
|
|
2844
|
-
const nextSiblingStartLine = nextSibling.loc?.start?.line;
|
|
2845
|
-
|
|
2846
|
-
// If comment ends on same line as next sibling starts, it's inline with next
|
|
2847
|
-
if (commentEndLine === nextSiblingStartLine) {
|
|
2848
|
-
// Leave it for next sibling's leading comments
|
|
2849
|
-
return;
|
|
2850
|
-
}
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
// For function parameters, only attach as trailing comment if it's on the same line
|
|
2855
|
-
// Comments on next line after comma should be leading comments of next parameter
|
|
2856
|
-
if (isParam) {
|
|
2857
|
-
// Check if comment is on same line as the node
|
|
2858
|
-
const nodeEndLine = source.slice(0, node.end).split('\n').length;
|
|
2859
|
-
const commentStartLine = source.slice(0, comments[0].start).split('\n').length;
|
|
2860
|
-
if (nodeEndLine === commentStartLine) {
|
|
2861
|
-
node.trailingComments = [
|
|
2862
|
-
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
2863
|
-
];
|
|
2864
|
-
}
|
|
2865
|
-
// Otherwise leave it for next parameter's leading comments
|
|
2866
|
-
} else {
|
|
2867
|
-
// Line comments on the next line should be leading comments
|
|
2868
|
-
// for the next statement, not trailing comments for this one.
|
|
2869
|
-
// Only attach as trailing if:
|
|
2870
|
-
// 1. It's on the same line as this node, OR
|
|
2871
|
-
// 2. This is the last item in the array (no next sibling to attach to)
|
|
2872
|
-
const commentOnSameLine =
|
|
2873
|
-
nodeEndLine !== null &&
|
|
2874
|
-
commentStartLine !== null &&
|
|
2875
|
-
nodeEndLine === commentStartLine;
|
|
2876
|
-
|
|
2877
|
-
if (commentOnSameLine || is_last_in_array) {
|
|
2878
|
-
node.trailingComments = [
|
|
2879
|
-
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
2880
|
-
];
|
|
2881
|
-
}
|
|
2882
|
-
// Otherwise leave it for next sibling's leading comments
|
|
2883
|
-
}
|
|
2884
|
-
} else if (hasBlankLine && onlyWhitespace && node_array) {
|
|
2885
|
-
// When there's a blank line between node and comment(s),
|
|
2886
|
-
// check if there's also a blank line after the comment(s) before the next node
|
|
2887
|
-
// If so, attach comments as trailing to preserve the grouping
|
|
2888
|
-
// Only do this for statement-level contexts (BlockStatement, Program),
|
|
2889
|
-
// not for Element children or other contexts
|
|
2890
|
-
const isStatementContext =
|
|
2891
|
-
parent.type === 'BlockStatement' || parent.type === 'Program';
|
|
2892
|
-
|
|
2893
|
-
// Don't apply for Component - let Prettier handle comment attachment there
|
|
2894
|
-
// Component bodies have different comment handling via metadata.elementLeadingComments
|
|
2895
|
-
if (!isStatementContext) {
|
|
2896
|
-
return;
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
const currentIndex = node_array.indexOf(node);
|
|
2900
|
-
const nextSibling = node_array[currentIndex + 1];
|
|
2901
|
-
|
|
2902
|
-
if (nextSibling && nextSibling.loc) {
|
|
2903
|
-
// Find where the comment block ends
|
|
2904
|
-
let lastCommentIndex = 0;
|
|
2905
|
-
let lastCommentEnd = comments[0].end;
|
|
2906
|
-
|
|
2907
|
-
// Collect consecutive comments (without blank lines between them)
|
|
2908
|
-
while (comments[lastCommentIndex + 1]) {
|
|
2909
|
-
const currentComment = comments[lastCommentIndex];
|
|
2910
|
-
const nextComment = comments[lastCommentIndex + 1];
|
|
2911
|
-
const sliceBetween = source.slice(currentComment.end, nextComment.start);
|
|
2912
|
-
|
|
2913
|
-
// If there's a blank line, stop
|
|
2914
|
-
if (/\n\s*\n/.test(sliceBetween)) {
|
|
2915
|
-
break;
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
|
-
lastCommentIndex++;
|
|
2919
|
-
lastCommentEnd = nextComment.end;
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
// Check if there's a blank line after the last comment and before next sibling
|
|
2923
|
-
const sliceAfterComments = source.slice(lastCommentEnd, nextSibling.start);
|
|
2924
|
-
const hasBlankLineAfter = /\n\s*\n/.test(sliceAfterComments);
|
|
2925
|
-
|
|
2926
|
-
if (hasBlankLineAfter) {
|
|
2927
|
-
// Don't attach comments as trailing if next sibling is an Element
|
|
2928
|
-
// and any comment falls within the Element's line range
|
|
2929
|
-
// This means the comments are inside the Element (between opening and closing tags)
|
|
2930
|
-
const nextIsElement = nextSibling.type === 'Element';
|
|
2931
|
-
const commentsInsideElement =
|
|
2932
|
-
nextIsElement &&
|
|
2933
|
-
nextSibling.loc &&
|
|
2934
|
-
comments.some((c) => {
|
|
2935
|
-
if (!c.loc) return false;
|
|
2936
|
-
// Check if comment is on a line between Element's start and end lines
|
|
2937
|
-
return (
|
|
2938
|
-
c.loc.start.line >= nextSibling.loc.start.line &&
|
|
2939
|
-
c.loc.end.line <= nextSibling.loc.end.line
|
|
2940
|
-
);
|
|
2941
|
-
});
|
|
2942
|
-
|
|
2943
|
-
if (!commentsInsideElement) {
|
|
2944
|
-
// Attach all the comments as trailing
|
|
2945
|
-
for (let i = 0; i <= lastCommentIndex; i++) {
|
|
2946
|
-
(node.trailingComments ||= []).push(
|
|
2947
|
-
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
2948
|
-
);
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
},
|
|
2958
|
-
});
|
|
2959
|
-
},
|
|
2960
|
-
};
|
|
2961
|
-
}
|
|
2962
|
-
|
|
2963
|
-
/**
|
|
2964
|
-
* Parse Ripple source code into an AST
|
|
2965
|
-
* @param {string} source
|
|
2966
|
-
* @param {string} [filename]
|
|
2967
|
-
* @param {ParseOptions} [options]
|
|
2968
|
-
* @returns {AST.Program}
|
|
2969
|
-
*/
|
|
2970
|
-
export function parse(source, filename, options) {
|
|
2971
|
-
/** @type {AST.CommentWithLocation[]} */
|
|
2972
|
-
const comments = [];
|
|
2973
|
-
const output_comments = options?.comments;
|
|
2974
|
-
|
|
2975
|
-
const { onComment, add_comments } = get_comment_handlers(source, comments);
|
|
2976
|
-
/** @type {AST.Program} */
|
|
2977
|
-
let ast;
|
|
2978
|
-
|
|
2979
|
-
try {
|
|
2980
|
-
ast = parser.parse(source, {
|
|
2981
|
-
sourceType: 'module',
|
|
2982
|
-
ecmaVersion: 13,
|
|
2983
|
-
allowReturnOutsideFunction: true,
|
|
2984
|
-
locations: true,
|
|
2985
|
-
onComment,
|
|
2986
|
-
rippleOptions: {
|
|
2987
|
-
filename,
|
|
2988
|
-
errors: options?.errors ?? [],
|
|
2989
|
-
loose: options?.loose || false,
|
|
2990
|
-
},
|
|
2991
|
-
});
|
|
2992
|
-
} catch (e) {
|
|
2993
|
-
throw e;
|
|
2994
|
-
}
|
|
2995
|
-
|
|
2996
|
-
if (output_comments) {
|
|
2997
|
-
// Copy comments to output array
|
|
2998
|
-
// as add_comments modifies the original array (e.g. shift)
|
|
2999
|
-
for (let i = 0; i < comments.length; i++) {
|
|
3000
|
-
output_comments.push(comments[i]);
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
add_comments(ast);
|
|
3005
|
-
|
|
3006
|
-
return ast;
|
|
3007
|
-
}
|