@rettangoli/check 0.0.1
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/README.md +295 -0
- package/ROADMAP.md +175 -0
- package/package.json +46 -0
- package/src/cli/bin.js +325 -0
- package/src/cli/check.js +232 -0
- package/src/cli/index.js +1 -0
- package/src/core/analyze.js +227 -0
- package/src/core/discovery.js +83 -0
- package/src/core/exportedFunctions.js +235 -0
- package/src/core/model.js +898 -0
- package/src/core/parsers.js +2726 -0
- package/src/core/registry.js +779 -0
- package/src/core/schema.js +161 -0
- package/src/core/scopeGraph.js +1400 -0
- package/src/core/semantic.js +329 -0
- package/src/diagnostics/autofix.js +191 -0
- package/src/diagnostics/catalog.js +89 -0
- package/src/index.js +2 -0
- package/src/reporters/index.js +13 -0
- package/src/reporters/json.js +42 -0
- package/src/reporters/sarif.js +213 -0
- package/src/reporters/text.js +145 -0
- package/src/rules/compatibility.js +318 -0
- package/src/rules/constants.js +22 -0
- package/src/rules/crossFileSymbols.js +108 -0
- package/src/rules/expression.js +338 -0
- package/src/rules/feParity.js +65 -0
- package/src/rules/index.js +39 -0
- package/src/rules/jempl.js +80 -0
- package/src/rules/lifecycle.js +4 -0
- package/src/rules/listenerConfig.js +556 -0
- package/src/rules/listenerSymbols.js +49 -0
- package/src/rules/methods.js +117 -0
- package/src/rules/refs.js +20 -0
- package/src/rules/schema.js +118 -0
- package/src/rules/shared.js +20 -0
- package/src/rules/yahtmlAttrs.js +238 -0
- package/src/semantic/engine.js +778 -0
- package/src/semantic/index.js +9 -0
- package/src/types/lattice.js +281 -0
- package/src/utils/case.js +9 -0
- package/src/utils/fs.js +30 -0
|
@@ -0,0 +1,2726 @@
|
|
|
1
|
+
import { load as loadYaml } from "js-yaml";
|
|
2
|
+
import { parseSync } from "oxc-parser";
|
|
3
|
+
import { collectBindingNames as collectFeBindingNames } from "@rettangoli/fe/contracts";
|
|
4
|
+
import { parse as parseJempl } from "jempl";
|
|
5
|
+
import { parseElementKey as parseYahtmlElementKey } from "yahtml";
|
|
6
|
+
|
|
7
|
+
const CONTROL_PREFIXES = ["$if", "$elif", "$else", "$for"];
|
|
8
|
+
|
|
9
|
+
export const parseYamlSafe = ({ text, filePath }) => {
|
|
10
|
+
try {
|
|
11
|
+
return {
|
|
12
|
+
ok: true,
|
|
13
|
+
value: loadYaml(text) ?? {},
|
|
14
|
+
error: null,
|
|
15
|
+
};
|
|
16
|
+
} catch (err) {
|
|
17
|
+
const reason = typeof err?.reason === "string" && err.reason.trim()
|
|
18
|
+
? err.reason.trim().replace(/[.]+$/u, "")
|
|
19
|
+
: "invalid YAML syntax";
|
|
20
|
+
const line = Number.isInteger(err?.mark?.line) ? err.mark.line + 1 : undefined;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
value: null,
|
|
25
|
+
error: {
|
|
26
|
+
code: "RTGL-CHECK-PARSE-001",
|
|
27
|
+
severity: "error",
|
|
28
|
+
message: `Failed to parse YAML: ${reason}.`,
|
|
29
|
+
filePath,
|
|
30
|
+
line,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const maskChar = (char) => {
|
|
37
|
+
if (char === "\n" || char === "\r") {
|
|
38
|
+
return char;
|
|
39
|
+
}
|
|
40
|
+
return " ";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const maskCommentsAndStrings = (sourceCode = "") => {
|
|
44
|
+
let masked = "";
|
|
45
|
+
let index = 0;
|
|
46
|
+
|
|
47
|
+
while (index < sourceCode.length) {
|
|
48
|
+
const char = sourceCode[index];
|
|
49
|
+
const nextChar = sourceCode[index + 1];
|
|
50
|
+
|
|
51
|
+
if (char === "/" && nextChar === "/") {
|
|
52
|
+
masked += " ";
|
|
53
|
+
index += 2;
|
|
54
|
+
while (index < sourceCode.length && sourceCode[index] !== "\n") {
|
|
55
|
+
masked += " ";
|
|
56
|
+
index += 1;
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (char === "/" && nextChar === "*") {
|
|
62
|
+
masked += " ";
|
|
63
|
+
index += 2;
|
|
64
|
+
while (index < sourceCode.length) {
|
|
65
|
+
const blockChar = sourceCode[index];
|
|
66
|
+
const blockNextChar = sourceCode[index + 1];
|
|
67
|
+
if (blockChar === "*" && blockNextChar === "/") {
|
|
68
|
+
masked += " ";
|
|
69
|
+
index += 2;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
masked += maskChar(blockChar);
|
|
73
|
+
index += 1;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
79
|
+
const quote = char;
|
|
80
|
+
masked += " ";
|
|
81
|
+
index += 1;
|
|
82
|
+
|
|
83
|
+
while (index < sourceCode.length) {
|
|
84
|
+
const stringChar = sourceCode[index];
|
|
85
|
+
masked += maskChar(stringChar);
|
|
86
|
+
index += 1;
|
|
87
|
+
|
|
88
|
+
if (stringChar === "\\") {
|
|
89
|
+
if (index < sourceCode.length) {
|
|
90
|
+
masked += maskChar(sourceCode[index]);
|
|
91
|
+
index += 1;
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (stringChar === quote) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
masked += char;
|
|
104
|
+
index += 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return masked;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const IDENTIFIER_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
111
|
+
const splitTopLevelCommaSeparated = (value = "") => {
|
|
112
|
+
const parts = [];
|
|
113
|
+
let current = "";
|
|
114
|
+
let parenDepth = 0;
|
|
115
|
+
let bracketDepth = 0;
|
|
116
|
+
let braceDepth = 0;
|
|
117
|
+
|
|
118
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
119
|
+
const char = value[index];
|
|
120
|
+
|
|
121
|
+
if (char === "(") {
|
|
122
|
+
parenDepth += 1;
|
|
123
|
+
current += char;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (char === ")" && parenDepth > 0) {
|
|
127
|
+
parenDepth -= 1;
|
|
128
|
+
current += char;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (char === "[") {
|
|
132
|
+
bracketDepth += 1;
|
|
133
|
+
current += char;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (char === "]" && bracketDepth > 0) {
|
|
137
|
+
bracketDepth -= 1;
|
|
138
|
+
current += char;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (char === "{") {
|
|
142
|
+
braceDepth += 1;
|
|
143
|
+
current += char;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (char === "}" && braceDepth > 0) {
|
|
147
|
+
braceDepth -= 1;
|
|
148
|
+
current += char;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (char === "," && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
|
|
153
|
+
parts.push(current.trim());
|
|
154
|
+
current = "";
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
current += char;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const finalPart = current.trim();
|
|
162
|
+
if (finalPart.length > 0) {
|
|
163
|
+
parts.push(finalPart);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return parts;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const findTopLevelCharacterIndex = (value = "", character = "=") => {
|
|
170
|
+
let parenDepth = 0;
|
|
171
|
+
let bracketDepth = 0;
|
|
172
|
+
let braceDepth = 0;
|
|
173
|
+
let quote = null;
|
|
174
|
+
let escaped = false;
|
|
175
|
+
|
|
176
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
177
|
+
const char = value[index];
|
|
178
|
+
|
|
179
|
+
if (quote) {
|
|
180
|
+
if (escaped) {
|
|
181
|
+
escaped = false;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (char === "\\") {
|
|
186
|
+
escaped = true;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (char === quote) {
|
|
191
|
+
quote = null;
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
197
|
+
quote = char;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (char === "(") {
|
|
202
|
+
parenDepth += 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (char === ")" && parenDepth > 0) {
|
|
206
|
+
parenDepth -= 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (char === "[") {
|
|
210
|
+
bracketDepth += 1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (char === "]" && bracketDepth > 0) {
|
|
214
|
+
bracketDepth -= 1;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (char === "{") {
|
|
218
|
+
braceDepth += 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (char === "}" && braceDepth > 0) {
|
|
222
|
+
braceDepth -= 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (char === character && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
|
|
227
|
+
return index;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return -1;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const stripTopLevelDefaultAssignment = (value = "") => {
|
|
235
|
+
const assignmentIndex = findTopLevelCharacterIndex(value, "=");
|
|
236
|
+
if (assignmentIndex === -1) {
|
|
237
|
+
return value.trim();
|
|
238
|
+
}
|
|
239
|
+
return value.slice(0, assignmentIndex).trim();
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const stripTopLevelTypeAnnotation = (value = "") => {
|
|
243
|
+
const typeAnnotationIndex = findTopLevelCharacterIndex(value, ":");
|
|
244
|
+
if (typeAnnotationIndex === -1) {
|
|
245
|
+
return value.trim();
|
|
246
|
+
}
|
|
247
|
+
return value.slice(0, typeAnnotationIndex).trim();
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const stripTypeScriptDefiniteAssignment = (value = "") => {
|
|
251
|
+
const trimmed = value.trim();
|
|
252
|
+
if (!trimmed.endsWith("!")) {
|
|
253
|
+
return trimmed;
|
|
254
|
+
}
|
|
255
|
+
return trimmed.slice(0, -1).trim();
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const collectBindingNamesFromPattern = (pattern = "") => {
|
|
259
|
+
const trimmed = pattern.trim();
|
|
260
|
+
if (!trimmed) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (trimmed.startsWith("...")) {
|
|
265
|
+
return collectBindingNamesFromPattern(trimmed.slice(3));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const withoutTypeAnnotation = stripTopLevelTypeAnnotation(trimmed);
|
|
269
|
+
if (withoutTypeAnnotation !== trimmed) {
|
|
270
|
+
return collectBindingNamesFromPattern(withoutTypeAnnotation);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const withoutDefiniteAssignment = stripTypeScriptDefiniteAssignment(trimmed);
|
|
274
|
+
if (withoutDefiniteAssignment !== trimmed) {
|
|
275
|
+
return collectBindingNamesFromPattern(withoutDefiniteAssignment);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (IDENTIFIER_REGEX.test(trimmed)) {
|
|
279
|
+
return [trimmed];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const withoutDefault = stripTopLevelDefaultAssignment(trimmed);
|
|
283
|
+
if (withoutDefault !== trimmed) {
|
|
284
|
+
return collectBindingNamesFromPattern(withoutDefault);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
288
|
+
const objectBody = trimmed.slice(1, -1);
|
|
289
|
+
const parts = splitTopLevelCommaSeparated(objectBody);
|
|
290
|
+
const names = [];
|
|
291
|
+
|
|
292
|
+
parts.forEach((part) => {
|
|
293
|
+
const token = part.trim();
|
|
294
|
+
if (!token) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (token.startsWith("...")) {
|
|
299
|
+
names.push(...collectBindingNamesFromPattern(token.slice(3)));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const colonIndex = findTopLevelCharacterIndex(token, ":");
|
|
304
|
+
if (colonIndex !== -1) {
|
|
305
|
+
const valuePattern = token.slice(colonIndex + 1).trim();
|
|
306
|
+
names.push(...collectBindingNamesFromPattern(valuePattern));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
names.push(...collectBindingNamesFromPattern(stripTopLevelDefaultAssignment(token)));
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
return names;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
317
|
+
const arrayBody = trimmed.slice(1, -1);
|
|
318
|
+
const parts = splitTopLevelCommaSeparated(arrayBody);
|
|
319
|
+
const names = [];
|
|
320
|
+
|
|
321
|
+
parts.forEach((part) => {
|
|
322
|
+
const token = part.trim();
|
|
323
|
+
if (!token) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (token.startsWith("...")) {
|
|
328
|
+
names.push(...collectBindingNamesFromPattern(token.slice(3)));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
names.push(...collectBindingNamesFromPattern(stripTopLevelDefaultAssignment(token)));
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return names;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return [];
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const collectExportedVariableDeclarationNames = (sourceForScan = "") => {
|
|
342
|
+
const names = [];
|
|
343
|
+
const exportVariableStartRegex = /export\s+(?:const|let|var)\s+/g;
|
|
344
|
+
let match = exportVariableStartRegex.exec(sourceForScan);
|
|
345
|
+
|
|
346
|
+
while (match) {
|
|
347
|
+
let index = exportVariableStartRegex.lastIndex;
|
|
348
|
+
let declaration = "";
|
|
349
|
+
let parenDepth = 0;
|
|
350
|
+
let bracketDepth = 0;
|
|
351
|
+
let braceDepth = 0;
|
|
352
|
+
let lastNonWhitespace = "";
|
|
353
|
+
|
|
354
|
+
while (index < sourceForScan.length) {
|
|
355
|
+
const char = sourceForScan[index];
|
|
356
|
+
const isTopLevel = parenDepth === 0 && bracketDepth === 0 && braceDepth === 0;
|
|
357
|
+
const isLineBreak = char === "\n" || char === "\r";
|
|
358
|
+
|
|
359
|
+
if (char === "(") {
|
|
360
|
+
parenDepth += 1;
|
|
361
|
+
} else if (char === ")" && parenDepth > 0) {
|
|
362
|
+
parenDepth -= 1;
|
|
363
|
+
} else if (char === "[") {
|
|
364
|
+
bracketDepth += 1;
|
|
365
|
+
} else if (char === "]" && bracketDepth > 0) {
|
|
366
|
+
bracketDepth -= 1;
|
|
367
|
+
} else if (char === "{") {
|
|
368
|
+
braceDepth += 1;
|
|
369
|
+
} else if (char === "}" && braceDepth > 0) {
|
|
370
|
+
braceDepth -= 1;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (isTopLevel && (char === ";" || (isLineBreak && lastNonWhitespace !== ","))) {
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
declaration += char;
|
|
378
|
+
if (!/\s/.test(char)) {
|
|
379
|
+
lastNonWhitespace = char;
|
|
380
|
+
}
|
|
381
|
+
index += 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const declarators = splitTopLevelCommaSeparated(declaration);
|
|
385
|
+
declarators.forEach((declarator) => {
|
|
386
|
+
const assignmentIndex = findTopLevelCharacterIndex(declarator, "=");
|
|
387
|
+
const bindingPattern = assignmentIndex === -1
|
|
388
|
+
? declarator.trim()
|
|
389
|
+
: declarator.slice(0, assignmentIndex).trim();
|
|
390
|
+
collectBindingNamesFromPattern(bindingPattern).forEach((name) => {
|
|
391
|
+
names.push(name);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
match = exportVariableStartRegex.exec(sourceForScan);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return names;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const extractNamedExportsRegex = (sourceCode = "") => {
|
|
402
|
+
const exports = new Set();
|
|
403
|
+
const sourceForScan = maskCommentsAndStrings(sourceCode);
|
|
404
|
+
|
|
405
|
+
const functionRegex = /export\s+(?:async\s+)?function\s*\*?\s*([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
406
|
+
const classRegex = /export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
407
|
+
const abstractClassRegex = /export\s+abstract\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
408
|
+
const enumRegex = /export\s+(?:const\s+)?enum\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
409
|
+
const exportImportAliasRegex = /export\s+import\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=/g;
|
|
410
|
+
const tsExportAssignmentRegex = /export\s*=\s*/g;
|
|
411
|
+
const listRegex = /export\s*\{([^}]+)\}(?!\s*from\b)/g;
|
|
412
|
+
const defaultRegex = /export\s+default\b/g;
|
|
413
|
+
|
|
414
|
+
let match = functionRegex.exec(sourceForScan);
|
|
415
|
+
while (match) {
|
|
416
|
+
exports.add(match[1]);
|
|
417
|
+
match = functionRegex.exec(sourceForScan);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
match = classRegex.exec(sourceForScan);
|
|
421
|
+
while (match) {
|
|
422
|
+
exports.add(match[1]);
|
|
423
|
+
match = classRegex.exec(sourceForScan);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
match = abstractClassRegex.exec(sourceForScan);
|
|
427
|
+
while (match) {
|
|
428
|
+
exports.add(match[1]);
|
|
429
|
+
match = abstractClassRegex.exec(sourceForScan);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
match = enumRegex.exec(sourceForScan);
|
|
433
|
+
while (match) {
|
|
434
|
+
exports.add(match[1]);
|
|
435
|
+
match = enumRegex.exec(sourceForScan);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
match = exportImportAliasRegex.exec(sourceForScan);
|
|
439
|
+
while (match) {
|
|
440
|
+
exports.add(match[1]);
|
|
441
|
+
match = exportImportAliasRegex.exec(sourceForScan);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
match = listRegex.exec(sourceForScan);
|
|
445
|
+
while (match) {
|
|
446
|
+
const entries = match[1].split(",").map((part) => part.trim()).filter(Boolean);
|
|
447
|
+
entries.forEach((entry) => {
|
|
448
|
+
const [left, right] = entry.split(/\s+as\s+/).map((token) => token.trim());
|
|
449
|
+
if (right && IDENTIFIER_REGEX.test(right)) {
|
|
450
|
+
exports.add(right);
|
|
451
|
+
} else if (left && IDENTIFIER_REGEX.test(left)) {
|
|
452
|
+
exports.add(left);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
match = listRegex.exec(sourceForScan);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
collectExportedVariableDeclarationNames(sourceForScan).forEach((name) => {
|
|
459
|
+
exports.add(name);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (defaultRegex.test(sourceForScan)) {
|
|
463
|
+
exports.add("default");
|
|
464
|
+
}
|
|
465
|
+
if (tsExportAssignmentRegex.test(sourceForScan)) {
|
|
466
|
+
exports.add("default");
|
|
467
|
+
}
|
|
468
|
+
collectNamespaceReExportsRegex(sourceCode).forEach(({ exportedName }) => {
|
|
469
|
+
if (IDENTIFIER_REGEX.test(exportedName)) {
|
|
470
|
+
exports.add(exportedName);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
return exports;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const collectExportStarSpecifiersRegex = (sourceCode = "") => {
|
|
478
|
+
const specifiers = new Set();
|
|
479
|
+
const sourceForScan = maskCommentsAndStrings(sourceCode);
|
|
480
|
+
const exportStarRegex = /export\s*\*\s*from\b/g;
|
|
481
|
+
let match = exportStarRegex.exec(sourceForScan);
|
|
482
|
+
|
|
483
|
+
while (match) {
|
|
484
|
+
let index = exportStarRegex.lastIndex;
|
|
485
|
+
while (index < sourceCode.length && /\s/.test(sourceCode[index])) {
|
|
486
|
+
index += 1;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const quote = sourceCode[index];
|
|
490
|
+
if (quote === "\"" || quote === "'") {
|
|
491
|
+
index += 1;
|
|
492
|
+
let specifier = "";
|
|
493
|
+
while (index < sourceCode.length) {
|
|
494
|
+
const char = sourceCode[index];
|
|
495
|
+
if (char === quote) {
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
if (char === "\n" || char === "\r") {
|
|
499
|
+
specifier = "";
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
specifier += char;
|
|
503
|
+
index += 1;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (specifier) {
|
|
507
|
+
specifiers.add(specifier);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
match = exportStarRegex.exec(sourceForScan);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return [...specifiers];
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const collectNamespaceReExportsRegex = (sourceCode = "") => {
|
|
518
|
+
const namespaceReExports = [];
|
|
519
|
+
const sourceForScan = maskCommentsAndStrings(sourceCode);
|
|
520
|
+
const namespaceReExportRegex = /export\s*\*\s*as\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*from\b/g;
|
|
521
|
+
let match = namespaceReExportRegex.exec(sourceForScan);
|
|
522
|
+
|
|
523
|
+
while (match) {
|
|
524
|
+
const exportedName = match[1];
|
|
525
|
+
let index = namespaceReExportRegex.lastIndex;
|
|
526
|
+
while (index < sourceCode.length && /\s/.test(sourceCode[index])) {
|
|
527
|
+
index += 1;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let moduleRequest = "";
|
|
531
|
+
const quote = sourceCode[index];
|
|
532
|
+
if (quote === "\"" || quote === "'") {
|
|
533
|
+
index += 1;
|
|
534
|
+
while (index < sourceCode.length) {
|
|
535
|
+
const char = sourceCode[index];
|
|
536
|
+
if (char === quote) {
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
if (char === "\n" || char === "\r") {
|
|
540
|
+
moduleRequest = "";
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
moduleRequest += char;
|
|
544
|
+
index += 1;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (moduleRequest) {
|
|
549
|
+
namespaceReExports.push({
|
|
550
|
+
moduleRequest,
|
|
551
|
+
exportedName,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
match = namespaceReExportRegex.exec(sourceForScan);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return namespaceReExports;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const collectNamedReExportsRegex = (sourceCode = "") => {
|
|
562
|
+
const reExports = [];
|
|
563
|
+
const sourceForScan = maskCommentsAndStrings(sourceCode);
|
|
564
|
+
const namedReExportRegex = /export\s*\{([^}]*)\}\s*from\b/g;
|
|
565
|
+
let match = namedReExportRegex.exec(sourceForScan);
|
|
566
|
+
|
|
567
|
+
while (match) {
|
|
568
|
+
const rawSpecifiers = splitTopLevelCommaSeparated(match[1]);
|
|
569
|
+
let index = namedReExportRegex.lastIndex;
|
|
570
|
+
while (index < sourceCode.length && /\s/.test(sourceCode[index])) {
|
|
571
|
+
index += 1;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let moduleRequest = "";
|
|
575
|
+
const quote = sourceCode[index];
|
|
576
|
+
if (quote === "\"" || quote === "'") {
|
|
577
|
+
index += 1;
|
|
578
|
+
while (index < sourceCode.length) {
|
|
579
|
+
const char = sourceCode[index];
|
|
580
|
+
if (char === quote) {
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
if (char === "\n" || char === "\r") {
|
|
584
|
+
moduleRequest = "";
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
moduleRequest += char;
|
|
588
|
+
index += 1;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (moduleRequest) {
|
|
593
|
+
rawSpecifiers.forEach((rawSpecifier) => {
|
|
594
|
+
const normalized = rawSpecifier.trim().replace(/\s+/g, " ");
|
|
595
|
+
if (!normalized || normalized.startsWith("type ")) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const namedSpecifierMatch = normalized.match(
|
|
600
|
+
/^([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*))?$/,
|
|
601
|
+
);
|
|
602
|
+
if (!namedSpecifierMatch) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const importedName = namedSpecifierMatch[1];
|
|
607
|
+
const exportedName = namedSpecifierMatch[2] || importedName;
|
|
608
|
+
reExports.push({
|
|
609
|
+
moduleRequest,
|
|
610
|
+
importedName,
|
|
611
|
+
exportedName,
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
match = namedReExportRegex.exec(sourceForScan);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return reExports;
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const offsetToLineColumn = ({ lineOffsets = [], offset = 0 }) => {
|
|
623
|
+
if (!Array.isArray(lineOffsets) || lineOffsets.length === 0) {
|
|
624
|
+
return {};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const safeOffset = Math.max(0, Number.isInteger(offset) ? offset : 0);
|
|
628
|
+
let low = 0;
|
|
629
|
+
let high = lineOffsets.length - 1;
|
|
630
|
+
|
|
631
|
+
while (low <= high) {
|
|
632
|
+
const mid = Math.floor((low + high) / 2);
|
|
633
|
+
const start = lineOffsets[mid];
|
|
634
|
+
const next = lineOffsets[mid + 1] ?? Number.POSITIVE_INFINITY;
|
|
635
|
+
|
|
636
|
+
if (safeOffset >= start && safeOffset < next) {
|
|
637
|
+
return {
|
|
638
|
+
line: mid + 1,
|
|
639
|
+
column: safeOffset - start + 1,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (safeOffset < start) {
|
|
644
|
+
high = mid - 1;
|
|
645
|
+
} else {
|
|
646
|
+
low = mid + 1;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const lastLineIndex = lineOffsets.length - 1;
|
|
651
|
+
const lastStart = lineOffsets[lastLineIndex] ?? 0;
|
|
652
|
+
return {
|
|
653
|
+
line: lastLineIndex + 1,
|
|
654
|
+
column: Math.max(1, safeOffset - lastStart + 1),
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const offsetRangeToLocation = ({
|
|
659
|
+
lineOffsets = [],
|
|
660
|
+
startOffset,
|
|
661
|
+
endOffset,
|
|
662
|
+
}) => {
|
|
663
|
+
if (!Number.isInteger(startOffset)) {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const start = offsetToLineColumn({ lineOffsets, offset: startOffset });
|
|
668
|
+
const resolvedEndOffset = Number.isInteger(endOffset) && endOffset >= startOffset
|
|
669
|
+
? endOffset
|
|
670
|
+
: startOffset;
|
|
671
|
+
const end = offsetToLineColumn({ lineOffsets, offset: resolvedEndOffset });
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
line: start.line,
|
|
675
|
+
column: start.column,
|
|
676
|
+
endLine: end.line,
|
|
677
|
+
endColumn: end.column,
|
|
678
|
+
};
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const extractModuleExportsOxc = ({
|
|
682
|
+
sourceCode = "",
|
|
683
|
+
filePath = "unknown.js",
|
|
684
|
+
}) => {
|
|
685
|
+
const parseCandidates = [{
|
|
686
|
+
filename: filePath || "unknown.js",
|
|
687
|
+
lang: undefined,
|
|
688
|
+
}];
|
|
689
|
+
if (typeof filePath === "string") {
|
|
690
|
+
if (filePath.endsWith(".js")) {
|
|
691
|
+
parseCandidates.push({
|
|
692
|
+
filename: filePath.slice(0, -3) + ".ts",
|
|
693
|
+
lang: "ts",
|
|
694
|
+
});
|
|
695
|
+
} else if (filePath.endsWith(".mjs")) {
|
|
696
|
+
parseCandidates.push({
|
|
697
|
+
filename: filePath.slice(0, -4) + ".mts",
|
|
698
|
+
lang: "ts",
|
|
699
|
+
});
|
|
700
|
+
} else if (filePath.endsWith(".cjs")) {
|
|
701
|
+
parseCandidates.push({
|
|
702
|
+
filename: filePath.slice(0, -4) + ".cts",
|
|
703
|
+
lang: "ts",
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let parseResult = null;
|
|
709
|
+
for (let i = 0; i < parseCandidates.length; i += 1) {
|
|
710
|
+
try {
|
|
711
|
+
const candidateResult = parseSync(parseCandidates[i].filename, sourceCode, {
|
|
712
|
+
lang: parseCandidates[i].lang,
|
|
713
|
+
sourceType: "unambiguous",
|
|
714
|
+
});
|
|
715
|
+
if (Array.isArray(candidateResult?.errors) && candidateResult.errors.length > 0) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
parseResult = candidateResult;
|
|
719
|
+
break;
|
|
720
|
+
} catch {
|
|
721
|
+
// Try next parse candidate.
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (!parseResult) {
|
|
726
|
+
return {
|
|
727
|
+
ok: false,
|
|
728
|
+
namedExports: new Set(),
|
|
729
|
+
exportStarSpecifiers: [],
|
|
730
|
+
namespaceReExports: [],
|
|
731
|
+
namedReExports: [],
|
|
732
|
+
reExportReferences: [],
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (!parseResult?.module || !Array.isArray(parseResult.module.staticExports)) {
|
|
737
|
+
return {
|
|
738
|
+
ok: false,
|
|
739
|
+
namedExports: new Set(),
|
|
740
|
+
exportStarSpecifiers: [],
|
|
741
|
+
namespaceReExports: [],
|
|
742
|
+
namedReExports: [],
|
|
743
|
+
reExportReferences: [],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (Array.isArray(parseResult.errors) && parseResult.errors.length > 0) {
|
|
748
|
+
return {
|
|
749
|
+
ok: false,
|
|
750
|
+
namedExports: new Set(),
|
|
751
|
+
exportStarSpecifiers: [],
|
|
752
|
+
namespaceReExports: [],
|
|
753
|
+
namedReExports: [],
|
|
754
|
+
reExportReferences: [],
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const namedExports = new Set();
|
|
759
|
+
const exportStarSpecifiers = new Set();
|
|
760
|
+
const namespaceReExports = [];
|
|
761
|
+
const namedReExports = [];
|
|
762
|
+
const reExportReferences = [];
|
|
763
|
+
const lineOffsets = createLineOffsets(sourceCode);
|
|
764
|
+
const programBody = Array.isArray(parseResult?.program?.body)
|
|
765
|
+
? parseResult.program.body
|
|
766
|
+
: [];
|
|
767
|
+
|
|
768
|
+
programBody.forEach((statement) => {
|
|
769
|
+
if (statement?.type === "TSExportAssignment") {
|
|
770
|
+
namedExports.add("default");
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
parseResult.module.staticExports.forEach((staticExport) => {
|
|
775
|
+
if (!Array.isArray(staticExport?.entries)) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const statementRange = offsetRangeToLocation({
|
|
780
|
+
lineOffsets,
|
|
781
|
+
startOffset: staticExport?.start,
|
|
782
|
+
endOffset: staticExport?.end,
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
staticExport.entries.forEach((entry) => {
|
|
786
|
+
if (!entry || entry.isType) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const exportNameKind = entry?.exportName?.kind;
|
|
791
|
+
const exportName = entry?.exportName?.name;
|
|
792
|
+
const moduleRequest = entry?.moduleRequest?.value;
|
|
793
|
+
const moduleRequestRange = offsetRangeToLocation({
|
|
794
|
+
lineOffsets,
|
|
795
|
+
startOffset: entry?.moduleRequest?.start,
|
|
796
|
+
endOffset: entry?.moduleRequest?.end,
|
|
797
|
+
});
|
|
798
|
+
if (!moduleRequest && exportNameKind === "Name" && typeof exportName === "string"
|
|
799
|
+
&& IDENTIFIER_REGEX.test(exportName)) {
|
|
800
|
+
namedExports.add(exportName);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (exportNameKind === "Default") {
|
|
804
|
+
namedExports.add("default");
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const importNameKind = entry?.importName?.kind;
|
|
808
|
+
if (importNameKind === "AllButDefault" && typeof moduleRequest === "string" && moduleRequest.length > 0) {
|
|
809
|
+
exportStarSpecifiers.add(moduleRequest);
|
|
810
|
+
reExportReferences.push({
|
|
811
|
+
kind: "export-star",
|
|
812
|
+
moduleRequest,
|
|
813
|
+
range: statementRange,
|
|
814
|
+
moduleRequestRange,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
if (importNameKind === "All" && typeof moduleRequest === "string" && moduleRequest.length > 0
|
|
818
|
+
&& exportNameKind === "Name" && typeof exportName === "string" && IDENTIFIER_REGEX.test(exportName)) {
|
|
819
|
+
const exportedNameRange = offsetRangeToLocation({
|
|
820
|
+
lineOffsets,
|
|
821
|
+
startOffset: entry?.exportName?.start,
|
|
822
|
+
endOffset: entry?.exportName?.end,
|
|
823
|
+
});
|
|
824
|
+
namedExports.add(exportName);
|
|
825
|
+
namespaceReExports.push({
|
|
826
|
+
moduleRequest,
|
|
827
|
+
exportedName: exportName,
|
|
828
|
+
});
|
|
829
|
+
reExportReferences.push({
|
|
830
|
+
kind: "namespace-reexport",
|
|
831
|
+
moduleRequest,
|
|
832
|
+
exportedName: exportName,
|
|
833
|
+
range: statementRange,
|
|
834
|
+
moduleRequestRange,
|
|
835
|
+
exportedNameRange,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
let resolvedExportedName = null;
|
|
840
|
+
if (exportNameKind === "Name" && typeof exportName === "string" && IDENTIFIER_REGEX.test(exportName)) {
|
|
841
|
+
resolvedExportedName = exportName;
|
|
842
|
+
} else if (exportNameKind === "Default") {
|
|
843
|
+
resolvedExportedName = "default";
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (typeof moduleRequest === "string" && moduleRequest.length > 0 && resolvedExportedName) {
|
|
847
|
+
let importedName = null;
|
|
848
|
+
if (importNameKind === "Name" && typeof entry?.importName?.name === "string"
|
|
849
|
+
&& IDENTIFIER_REGEX.test(entry.importName.name)) {
|
|
850
|
+
importedName = entry.importName.name;
|
|
851
|
+
} else if (importNameKind === "Default") {
|
|
852
|
+
importedName = "default";
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (importedName) {
|
|
856
|
+
const importedNameRange = offsetRangeToLocation({
|
|
857
|
+
lineOffsets,
|
|
858
|
+
startOffset: entry?.importName?.start,
|
|
859
|
+
endOffset: entry?.importName?.end,
|
|
860
|
+
});
|
|
861
|
+
const exportedNameRange = offsetRangeToLocation({
|
|
862
|
+
lineOffsets,
|
|
863
|
+
startOffset: entry?.exportName?.start,
|
|
864
|
+
endOffset: entry?.exportName?.end,
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
namedReExports.push({
|
|
868
|
+
moduleRequest,
|
|
869
|
+
importedName,
|
|
870
|
+
exportedName: resolvedExportedName,
|
|
871
|
+
});
|
|
872
|
+
reExportReferences.push({
|
|
873
|
+
kind: "named-reexport",
|
|
874
|
+
moduleRequest,
|
|
875
|
+
importedName,
|
|
876
|
+
exportedName: resolvedExportedName,
|
|
877
|
+
range: statementRange,
|
|
878
|
+
moduleRequestRange,
|
|
879
|
+
importedNameRange,
|
|
880
|
+
exportedNameRange,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
ok: true,
|
|
889
|
+
namedExports,
|
|
890
|
+
exportStarSpecifiers: [...exportStarSpecifiers],
|
|
891
|
+
namespaceReExports,
|
|
892
|
+
namedReExports,
|
|
893
|
+
reExportReferences,
|
|
894
|
+
};
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
export const extractModuleExports = ({
|
|
898
|
+
sourceCode = "",
|
|
899
|
+
filePath = "unknown.js",
|
|
900
|
+
} = {}) => {
|
|
901
|
+
const oxcResult = extractModuleExportsOxc({ sourceCode, filePath });
|
|
902
|
+
if (oxcResult.ok) {
|
|
903
|
+
return {
|
|
904
|
+
...oxcResult,
|
|
905
|
+
backendUsed: "oxc",
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return {
|
|
910
|
+
namedExports: new Set(),
|
|
911
|
+
exportStarSpecifiers: [],
|
|
912
|
+
namespaceReExports: [],
|
|
913
|
+
namedReExports: [],
|
|
914
|
+
reExportReferences: [],
|
|
915
|
+
backendUsed: "oxc",
|
|
916
|
+
parseFailed: true,
|
|
917
|
+
};
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
export const extractModuleExportsRegexLegacy = ({
|
|
921
|
+
sourceCode = "",
|
|
922
|
+
} = {}) => {
|
|
923
|
+
return {
|
|
924
|
+
namedExports: extractNamedExportsRegex(sourceCode),
|
|
925
|
+
exportStarSpecifiers: collectExportStarSpecifiersRegex(sourceCode),
|
|
926
|
+
namespaceReExports: collectNamespaceReExportsRegex(sourceCode),
|
|
927
|
+
namedReExports: collectNamedReExportsRegex(sourceCode),
|
|
928
|
+
reExportReferences: [],
|
|
929
|
+
backendUsed: "regex-legacy",
|
|
930
|
+
};
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
export const extractNamedExports = (sourceCode = "", options = {}) => {
|
|
934
|
+
return extractModuleExports({ sourceCode, ...options }).namedExports;
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
export const collectExportStarSpecifiers = (sourceCode = "", options = {}) => {
|
|
938
|
+
return extractModuleExports({ sourceCode, ...options }).exportStarSpecifiers;
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
export const hasLegacyDotPropBinding = (node) => {
|
|
942
|
+
const LEGACY_PROP_BINDING_REGEX = /(^|\s)\.[A-Za-z_][A-Za-z0-9_-]*\s*=/;
|
|
943
|
+
if (Array.isArray(node)) {
|
|
944
|
+
return node.some((item) => hasLegacyDotPropBinding(item));
|
|
945
|
+
}
|
|
946
|
+
if (!node || typeof node !== "object") {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return Object.entries(node).some(([key, value]) => {
|
|
951
|
+
if (LEGACY_PROP_BINDING_REGEX.test(key)) {
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
return hasLegacyDotPropBinding(value);
|
|
955
|
+
});
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const stripOuterQuotes = (value) => {
|
|
959
|
+
if (!value || value.length < 2) {
|
|
960
|
+
return value;
|
|
961
|
+
}
|
|
962
|
+
if (value.startsWith("\"") && value.endsWith("\"")) {
|
|
963
|
+
const inner = value.slice(1, -1);
|
|
964
|
+
return inner.replace(/\\(["\\])/g, "$1");
|
|
965
|
+
}
|
|
966
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
967
|
+
return value.slice(1, -1).replace(/''/g, "'");
|
|
968
|
+
}
|
|
969
|
+
return value;
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
const stripYamlTrailingComment = (value = "") => {
|
|
973
|
+
let quote = null;
|
|
974
|
+
|
|
975
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
976
|
+
const char = value[index];
|
|
977
|
+
const prevChar = value[index - 1];
|
|
978
|
+
|
|
979
|
+
if (quote) {
|
|
980
|
+
if (quote === "\"" && char === "\\") {
|
|
981
|
+
index += 1;
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (quote === "'" && char === "'" && value[index + 1] === "'") {
|
|
986
|
+
index += 1;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (char === quote) {
|
|
991
|
+
quote = null;
|
|
992
|
+
}
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (char === "\"" || char === "'") {
|
|
997
|
+
quote = char;
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (char === "#" && (prevChar === undefined || /\s/.test(prevChar))) {
|
|
1002
|
+
return value.slice(0, index).trimEnd();
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return value;
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const splitYamlListLineKey = (line) => {
|
|
1010
|
+
const listMatch = line.match(/^\s*-\s+(.*)$/);
|
|
1011
|
+
if (!listMatch) {
|
|
1012
|
+
return {
|
|
1013
|
+
key: null,
|
|
1014
|
+
expectsMultilineExplicitKey: false,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const rest = listMatch[1];
|
|
1019
|
+
const cleanedRest = stripYamlTrailingComment(rest).trim();
|
|
1020
|
+
if (cleanedRest === "?") {
|
|
1021
|
+
return {
|
|
1022
|
+
key: null,
|
|
1023
|
+
expectsMultilineExplicitKey: true,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const explicitKeyMatch = cleanedRest.match(/^\?\s+(.+)$/);
|
|
1028
|
+
if (explicitKeyMatch) {
|
|
1029
|
+
const explicitKey = stripYamlTrailingComment(explicitKeyMatch[1]).trim();
|
|
1030
|
+
return {
|
|
1031
|
+
key: explicitKey || null,
|
|
1032
|
+
expectsMultilineExplicitKey: false,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
let quote = null;
|
|
1037
|
+
|
|
1038
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1039
|
+
const chr = rest[i];
|
|
1040
|
+
|
|
1041
|
+
if (quote) {
|
|
1042
|
+
if (quote === "\"" && chr === "\\") {
|
|
1043
|
+
i += 1;
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (quote === "'" && chr === "'" && rest[i + 1] === "'") {
|
|
1048
|
+
i += 1;
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (chr === quote) {
|
|
1053
|
+
quote = null;
|
|
1054
|
+
}
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (chr === "\"" || chr === "'") {
|
|
1059
|
+
quote = chr;
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (chr === ":") {
|
|
1064
|
+
const next = rest[i + 1];
|
|
1065
|
+
if (next === undefined || /\s/.test(next)) {
|
|
1066
|
+
return {
|
|
1067
|
+
key: rest.slice(0, i).trim(),
|
|
1068
|
+
expectsMultilineExplicitKey: false,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
key: null,
|
|
1076
|
+
expectsMultilineExplicitKey: false,
|
|
1077
|
+
};
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
const splitYamlMultilineExplicitKey = (line) => {
|
|
1081
|
+
const trimmed = stripYamlTrailingComment(line.trim()).trim();
|
|
1082
|
+
if (!trimmed || trimmed.startsWith(":")) {
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return stripOuterQuotes(trimmed);
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
const splitYamlMappingLineKey = (line) => {
|
|
1090
|
+
const rest = line.trimStart();
|
|
1091
|
+
if (!rest || rest.startsWith("-")) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
let quote = null;
|
|
1096
|
+
|
|
1097
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1098
|
+
const chr = rest[i];
|
|
1099
|
+
|
|
1100
|
+
if (quote) {
|
|
1101
|
+
if (quote === "\"" && chr === "\\") {
|
|
1102
|
+
i += 1;
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (quote === "'" && chr === "'" && rest[i + 1] === "'") {
|
|
1107
|
+
i += 1;
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (chr === quote) {
|
|
1112
|
+
quote = null;
|
|
1113
|
+
}
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (chr === "\"" || chr === "'") {
|
|
1118
|
+
quote = chr;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (chr === ":") {
|
|
1123
|
+
const next = rest[i + 1];
|
|
1124
|
+
if (next === undefined || /\s/.test(next)) {
|
|
1125
|
+
const rawKey = rest.slice(0, i).trim();
|
|
1126
|
+
return stripOuterQuotes(rawKey);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return null;
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const splitSelector = (selectorText) => {
|
|
1135
|
+
const trimmed = selectorText.trim();
|
|
1136
|
+
if (!trimmed) {
|
|
1137
|
+
return {
|
|
1138
|
+
selector: "",
|
|
1139
|
+
attrsText: "",
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const firstWhitespace = trimmed.search(/\s/u);
|
|
1144
|
+
if (firstWhitespace === -1) {
|
|
1145
|
+
return {
|
|
1146
|
+
selector: trimmed,
|
|
1147
|
+
attrsText: "",
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
return {
|
|
1152
|
+
selector: trimmed.slice(0, firstWhitespace),
|
|
1153
|
+
attrsText: trimmed.slice(firstWhitespace + 1).trim(),
|
|
1154
|
+
};
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
const resolveTagNameFromSelectorKey = ({ key, selector }) => {
|
|
1158
|
+
const fallbackTagName = selector.split(/[.#]/)[0] || "";
|
|
1159
|
+
|
|
1160
|
+
try {
|
|
1161
|
+
const parsedKey = parseYahtmlElementKey(key);
|
|
1162
|
+
if (parsedKey && typeof parsedKey.tag === "string" && parsedKey.tag.trim().length > 0) {
|
|
1163
|
+
return parsedKey.tag.trim();
|
|
1164
|
+
}
|
|
1165
|
+
} catch {
|
|
1166
|
+
// Fallback to local selector parsing when YAHTML parser throws.
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
return fallbackTagName;
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
const parseSelectorKeyWithFe = (key = "") => {
|
|
1173
|
+
const { selector, attrsText } = splitSelector(key);
|
|
1174
|
+
const tagName = resolveTagNameFromSelectorKey({ key, selector });
|
|
1175
|
+
const bindingNames = parseBindingNames(attrsText);
|
|
1176
|
+
|
|
1177
|
+
return {
|
|
1178
|
+
selector,
|
|
1179
|
+
attrsText,
|
|
1180
|
+
tagName,
|
|
1181
|
+
bindingNames,
|
|
1182
|
+
};
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
const collectTemplateSelectorLineCandidates = (viewText = "") => {
|
|
1186
|
+
const lines = viewText.split("\n");
|
|
1187
|
+
const candidates = [];
|
|
1188
|
+
let currentTopLevelKey = null;
|
|
1189
|
+
let expectsMultilineExplicitKey = false;
|
|
1190
|
+
|
|
1191
|
+
lines.forEach((line, lineIndex) => {
|
|
1192
|
+
const trimmed = line.trim();
|
|
1193
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const indent = line.match(/^\s*/)?.[0].length || 0;
|
|
1198
|
+
if (indent === 0) {
|
|
1199
|
+
currentTopLevelKey = splitYamlMappingLineKey(line) || null;
|
|
1200
|
+
expectsMultilineExplicitKey = false;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (currentTopLevelKey !== "template") {
|
|
1204
|
+
expectsMultilineExplicitKey = false;
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (expectsMultilineExplicitKey) {
|
|
1209
|
+
if (indent > 0) {
|
|
1210
|
+
const explicitKey = splitYamlMultilineExplicitKey(line);
|
|
1211
|
+
if (explicitKey) {
|
|
1212
|
+
candidates.push({
|
|
1213
|
+
key: explicitKey,
|
|
1214
|
+
line: lineIndex + 1,
|
|
1215
|
+
feParsed: parseSelectorKeyWithFe(explicitKey),
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
expectsMultilineExplicitKey = false;
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const listLine = splitYamlListLineKey(line);
|
|
1224
|
+
if (listLine.expectsMultilineExplicitKey) {
|
|
1225
|
+
expectsMultilineExplicitKey = true;
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const rawKey = listLine.key || ((indent > 0) ? splitYamlMappingLineKey(line) : null);
|
|
1230
|
+
if (!rawKey) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
let key = stripOuterQuotes(rawKey.trim());
|
|
1235
|
+
while (key.startsWith("- ")) {
|
|
1236
|
+
key = key.slice(2).trim();
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (!key) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
candidates.push({
|
|
1244
|
+
key,
|
|
1245
|
+
line: lineIndex + 1,
|
|
1246
|
+
feParsed: parseSelectorKeyWithFe(key),
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
return candidates;
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
export const JEMPL_NODE = {
|
|
1254
|
+
LITERAL: 0,
|
|
1255
|
+
PATH: 1,
|
|
1256
|
+
TEMPLATE: 2,
|
|
1257
|
+
BINARY: 4,
|
|
1258
|
+
UNARY: 5,
|
|
1259
|
+
CONDITIONAL: 6,
|
|
1260
|
+
LOOP: 7,
|
|
1261
|
+
OBJECT: 8,
|
|
1262
|
+
ARRAY: 9,
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
export const JEMPL_BINARY_OP = {
|
|
1266
|
+
EQ: 0,
|
|
1267
|
+
NEQ: 1,
|
|
1268
|
+
GT: 2,
|
|
1269
|
+
LT: 3,
|
|
1270
|
+
GTE: 4,
|
|
1271
|
+
LTE: 5,
|
|
1272
|
+
AND: 6,
|
|
1273
|
+
OR: 7,
|
|
1274
|
+
IN: 8,
|
|
1275
|
+
ADD: 10,
|
|
1276
|
+
SUBTRACT: 11,
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
export const JEMPL_UNARY_OP = {
|
|
1280
|
+
NOT: 0,
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
const JEMPL_BINARY_OPERATOR_SYMBOL_BY_OP = new Map([
|
|
1284
|
+
[JEMPL_BINARY_OP.EQ, "=="],
|
|
1285
|
+
[JEMPL_BINARY_OP.NEQ, "!="],
|
|
1286
|
+
[JEMPL_BINARY_OP.GT, ">"],
|
|
1287
|
+
[JEMPL_BINARY_OP.LT, "<"],
|
|
1288
|
+
[JEMPL_BINARY_OP.GTE, ">="],
|
|
1289
|
+
[JEMPL_BINARY_OP.LTE, "<="],
|
|
1290
|
+
[JEMPL_BINARY_OP.AND, "&&"],
|
|
1291
|
+
[JEMPL_BINARY_OP.OR, "||"],
|
|
1292
|
+
[JEMPL_BINARY_OP.IN, "in"],
|
|
1293
|
+
[JEMPL_BINARY_OP.ADD, "+"],
|
|
1294
|
+
[JEMPL_BINARY_OP.SUBTRACT, "-"],
|
|
1295
|
+
]);
|
|
1296
|
+
|
|
1297
|
+
const JEMPL_UNARY_OPERATOR_SYMBOL_BY_OP = new Map([
|
|
1298
|
+
[JEMPL_UNARY_OP.NOT, "!"],
|
|
1299
|
+
]);
|
|
1300
|
+
|
|
1301
|
+
const JEMPL_NODE_KIND_BY_TYPE = new Map([
|
|
1302
|
+
[JEMPL_NODE.LITERAL, "JemplLiteralAst"],
|
|
1303
|
+
[JEMPL_NODE.PATH, "JemplPathAst"],
|
|
1304
|
+
[JEMPL_NODE.TEMPLATE, "JemplTemplateAst"],
|
|
1305
|
+
[JEMPL_NODE.BINARY, "JemplBinaryAst"],
|
|
1306
|
+
[JEMPL_NODE.UNARY, "JemplUnaryAst"],
|
|
1307
|
+
[JEMPL_NODE.CONDITIONAL, "JemplConditionalAst"],
|
|
1308
|
+
[JEMPL_NODE.LOOP, "JemplLoopAst"],
|
|
1309
|
+
[JEMPL_NODE.OBJECT, "JemplObjectAst"],
|
|
1310
|
+
[JEMPL_NODE.ARRAY, "JemplArrayAst"],
|
|
1311
|
+
]);
|
|
1312
|
+
|
|
1313
|
+
const isControlKey = (key = "") => {
|
|
1314
|
+
return CONTROL_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
const resolveJemplNodeKind = (type) => {
|
|
1318
|
+
if (!Number.isInteger(type)) {
|
|
1319
|
+
return "JemplUnknownAst";
|
|
1320
|
+
}
|
|
1321
|
+
return JEMPL_NODE_KIND_BY_TYPE.get(type) || `JemplNodeType${type}`;
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
const mapJemplNodeToTypedAst = (node) => {
|
|
1325
|
+
if (Array.isArray(node)) {
|
|
1326
|
+
return node.map((item) => mapJemplNodeToTypedAst(item));
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (!node || typeof node !== "object") {
|
|
1330
|
+
return node;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const mappedNode = {};
|
|
1334
|
+
|
|
1335
|
+
Object.entries(node).forEach(([key, value]) => {
|
|
1336
|
+
mappedNode[key] = mapJemplNodeToTypedAst(value);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
if (node.type === JEMPL_NODE.BINARY && Number.isInteger(node.op)) {
|
|
1340
|
+
mappedNode.operator = JEMPL_BINARY_OPERATOR_SYMBOL_BY_OP.get(node.op) || null;
|
|
1341
|
+
}
|
|
1342
|
+
if (node.type === JEMPL_NODE.UNARY && Number.isInteger(node.op)) {
|
|
1343
|
+
mappedNode.operator = JEMPL_UNARY_OPERATOR_SYMBOL_BY_OP.get(node.op) || null;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
mappedNode.kind = resolveJemplNodeKind(node.type);
|
|
1347
|
+
return mappedNode;
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
export const normalizeJemplErrorMessage = (error, fallbackMessage) => {
|
|
1351
|
+
const rawMessage = typeof error?.message === "string" ? error.message : "";
|
|
1352
|
+
const normalizedMessage = rawMessage.replace(/\s+/gu, " ").trim();
|
|
1353
|
+
const message = normalizedMessage || fallbackMessage;
|
|
1354
|
+
return message.endsWith(".") ? message : `${message}.`;
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
const collectElementObjectBindings = (node) => {
|
|
1358
|
+
const bindingNames = [];
|
|
1359
|
+
const templateNodes = [];
|
|
1360
|
+
|
|
1361
|
+
if (!node || typeof node !== "object" || node.type !== JEMPL_NODE.OBJECT || !Array.isArray(node.properties)) {
|
|
1362
|
+
return { bindingNames, templateNodes };
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
node.properties.forEach((property) => {
|
|
1366
|
+
if (!property || typeof property !== "object" || typeof property.key !== "string") {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const key = property.key.trim();
|
|
1371
|
+
if (!key) {
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
if (key === "children" || isControlKey(key)) {
|
|
1376
|
+
templateNodes.push(property.value);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
bindingNames.push(key);
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
return { bindingNames, templateNodes };
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
const collectSelectorEntriesFromJemplAst = (node, collected = []) => {
|
|
1387
|
+
if (!node || typeof node !== "object") {
|
|
1388
|
+
return collected;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
if (node.type === JEMPL_NODE.OBJECT && Array.isArray(node.properties)) {
|
|
1392
|
+
node.properties.forEach((property) => {
|
|
1393
|
+
if (!property || typeof property !== "object" || typeof property.key !== "string") {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const key = property.key.trim();
|
|
1398
|
+
if (!key) {
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (key === "children" || isControlKey(key)) {
|
|
1403
|
+
collectSelectorEntriesFromJemplAst(property.value, collected);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const valueNode = property.value;
|
|
1408
|
+
const { bindingNames, templateNodes } = collectElementObjectBindings(valueNode);
|
|
1409
|
+
|
|
1410
|
+
collected.push({
|
|
1411
|
+
key,
|
|
1412
|
+
bindingNames,
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
templateNodes.forEach((templateNode) => {
|
|
1416
|
+
collectSelectorEntriesFromJemplAst(templateNode, collected);
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
if (valueNode?.type === JEMPL_NODE.ARRAY) {
|
|
1420
|
+
collectSelectorEntriesFromJemplAst(valueNode, collected);
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
return collected;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (node.type === JEMPL_NODE.ARRAY && Array.isArray(node.items)) {
|
|
1427
|
+
node.items.forEach((item) => {
|
|
1428
|
+
collectSelectorEntriesFromJemplAst(item, collected);
|
|
1429
|
+
});
|
|
1430
|
+
return collected;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
if (node.type === JEMPL_NODE.CONDITIONAL && Array.isArray(node.bodies)) {
|
|
1434
|
+
node.bodies.forEach((body) => {
|
|
1435
|
+
collectSelectorEntriesFromJemplAst(body, collected);
|
|
1436
|
+
});
|
|
1437
|
+
return collected;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (node.type === JEMPL_NODE.LOOP) {
|
|
1441
|
+
collectSelectorEntriesFromJemplAst(node.body, collected);
|
|
1442
|
+
return collected;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
return collected;
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
const normalizeSelectorKeyForMatching = (value = "") => {
|
|
1449
|
+
return String(value).trim();
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
const consumeCandidateLineForKey = ({
|
|
1453
|
+
candidates,
|
|
1454
|
+
key,
|
|
1455
|
+
lastMatchedIndex,
|
|
1456
|
+
}) => {
|
|
1457
|
+
const normalizedKey = normalizeSelectorKeyForMatching(key);
|
|
1458
|
+
if (!normalizedKey || !Array.isArray(candidates) || candidates.length === 0) {
|
|
1459
|
+
return {
|
|
1460
|
+
line: undefined,
|
|
1461
|
+
nextIndex: lastMatchedIndex,
|
|
1462
|
+
candidate: undefined,
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
for (let index = Math.max(lastMatchedIndex + 1, 0); index < candidates.length; index += 1) {
|
|
1467
|
+
if (normalizeSelectorKeyForMatching(candidates[index].key) === normalizedKey) {
|
|
1468
|
+
return {
|
|
1469
|
+
line: candidates[index].line,
|
|
1470
|
+
nextIndex: index,
|
|
1471
|
+
candidate: candidates[index],
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
const fallbackIndex = Math.max(lastMatchedIndex + 1, 0);
|
|
1477
|
+
if (fallbackIndex < candidates.length) {
|
|
1478
|
+
return {
|
|
1479
|
+
line: candidates[fallbackIndex].line,
|
|
1480
|
+
nextIndex: fallbackIndex,
|
|
1481
|
+
candidate: candidates[fallbackIndex],
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
return {
|
|
1486
|
+
line: undefined,
|
|
1487
|
+
nextIndex: lastMatchedIndex,
|
|
1488
|
+
candidate: undefined,
|
|
1489
|
+
};
|
|
1490
|
+
};
|
|
1491
|
+
|
|
1492
|
+
const createTemplateKeyLineResolver = ({
|
|
1493
|
+
viewText = "",
|
|
1494
|
+
fallbackLine,
|
|
1495
|
+
} = {}) => {
|
|
1496
|
+
const candidates = collectTemplateSelectorLineCandidates(viewText);
|
|
1497
|
+
let lastMatchedIndex = -1;
|
|
1498
|
+
|
|
1499
|
+
return {
|
|
1500
|
+
resolveLineForKey: (key = "") => {
|
|
1501
|
+
const lineMatch = consumeCandidateLineForKey({
|
|
1502
|
+
candidates,
|
|
1503
|
+
key,
|
|
1504
|
+
lastMatchedIndex,
|
|
1505
|
+
});
|
|
1506
|
+
lastMatchedIndex = lineMatch.nextIndex;
|
|
1507
|
+
|
|
1508
|
+
if (Number.isInteger(lineMatch.line)) {
|
|
1509
|
+
return lineMatch.line;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
return Number.isInteger(fallbackLine) ? fallbackLine : undefined;
|
|
1513
|
+
},
|
|
1514
|
+
};
|
|
1515
|
+
};
|
|
1516
|
+
|
|
1517
|
+
const classifyJemplControlDirective = (rawKey = "") => {
|
|
1518
|
+
const key = String(rawKey || "").trim();
|
|
1519
|
+
if (!key.startsWith("$")) {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
if (/^\$if(?:\s|\(|$)/u.test(key)) {
|
|
1523
|
+
return "if";
|
|
1524
|
+
}
|
|
1525
|
+
if (/^\$elif(?:\s|\(|$)/u.test(key)) {
|
|
1526
|
+
return "elif";
|
|
1527
|
+
}
|
|
1528
|
+
if (/^\$else(?:\s|$)/u.test(key)) {
|
|
1529
|
+
return "else";
|
|
1530
|
+
}
|
|
1531
|
+
if (/^\$for(?:\s|\(|$)/u.test(key)) {
|
|
1532
|
+
return "for";
|
|
1533
|
+
}
|
|
1534
|
+
return "unknown";
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
const validateJemplConditionDirectiveSyntax = (rawKey = "", directive = "$if") => {
|
|
1538
|
+
const suffix = rawKey.slice(directive.length).trim();
|
|
1539
|
+
if (!suffix) {
|
|
1540
|
+
return `missing condition after '${directive}'`;
|
|
1541
|
+
}
|
|
1542
|
+
if (suffix.startsWith("(") && suffix.endsWith(")")) {
|
|
1543
|
+
const expression = suffix.slice(1, -1).trim();
|
|
1544
|
+
if (!expression) {
|
|
1545
|
+
return `missing condition after '${directive}'`;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return null;
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
const validateJemplForDirectiveSyntax = (rawKey = "") => {
|
|
1552
|
+
const forMatchWithParentheses = rawKey.match(
|
|
1553
|
+
/^\$for\s*\(\s*([A-Za-z_$][A-Za-z0-9_$]*)(?:\s*,\s*([A-Za-z_$][A-Za-z0-9_$]*))?\s+in\s+(.+)\)$/u,
|
|
1554
|
+
);
|
|
1555
|
+
const forMatchWithoutParentheses = rawKey.match(
|
|
1556
|
+
/^\$for\s+([A-Za-z_$][A-Za-z0-9_$]*)(?:\s*,\s*([A-Za-z_$][A-Za-z0-9_$]*))?\s+in\s+(.+)$/u,
|
|
1557
|
+
);
|
|
1558
|
+
const forMatch = forMatchWithParentheses || forMatchWithoutParentheses;
|
|
1559
|
+
if (!forMatch) {
|
|
1560
|
+
return "expected '$for(item[, index] in iterable)' or '$for item[, index] in iterable'";
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const iterableExpression = String(forMatch[3] || "").trim();
|
|
1564
|
+
if (!iterableExpression) {
|
|
1565
|
+
return "missing iterable expression in '$for(...)'";
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
return null;
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
const createJemplControlDiagnostic = ({
|
|
1572
|
+
key,
|
|
1573
|
+
line,
|
|
1574
|
+
message,
|
|
1575
|
+
}) => {
|
|
1576
|
+
return {
|
|
1577
|
+
line,
|
|
1578
|
+
message: `Invalid Jempl control directive '${key}': ${message}.`,
|
|
1579
|
+
};
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1582
|
+
const collectJemplControlDirectiveDiagnostics = ({
|
|
1583
|
+
template,
|
|
1584
|
+
viewText = "",
|
|
1585
|
+
fallbackLine,
|
|
1586
|
+
} = {}) => {
|
|
1587
|
+
const diagnostics = [];
|
|
1588
|
+
const lineResolver = createTemplateKeyLineResolver({
|
|
1589
|
+
viewText,
|
|
1590
|
+
fallbackLine,
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
const visitNode = (node) => {
|
|
1594
|
+
if (Array.isArray(node)) {
|
|
1595
|
+
node.forEach((item) => visitNode(item));
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (!node || typeof node !== "object") {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
let hasOpenIfChain = false;
|
|
1604
|
+
Object.entries(node).forEach(([rawKey, value]) => {
|
|
1605
|
+
const key = String(rawKey || "").trim();
|
|
1606
|
+
const directiveKind = classifyJemplControlDirective(key);
|
|
1607
|
+
|
|
1608
|
+
if (!directiveKind) {
|
|
1609
|
+
hasOpenIfChain = false;
|
|
1610
|
+
visitNode(value);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const line = lineResolver.resolveLineForKey(key);
|
|
1615
|
+
if (directiveKind === "unknown") {
|
|
1616
|
+
diagnostics.push(createJemplControlDiagnostic({
|
|
1617
|
+
key,
|
|
1618
|
+
line,
|
|
1619
|
+
message: "unknown control keyword",
|
|
1620
|
+
}));
|
|
1621
|
+
hasOpenIfChain = false;
|
|
1622
|
+
visitNode(value);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
let syntaxError = null;
|
|
1627
|
+
if (directiveKind === "if") {
|
|
1628
|
+
syntaxError = validateJemplConditionDirectiveSyntax(key, "$if");
|
|
1629
|
+
} else if (directiveKind === "elif") {
|
|
1630
|
+
syntaxError = validateJemplConditionDirectiveSyntax(key, "$elif");
|
|
1631
|
+
} else if (directiveKind === "else" && key !== "$else") {
|
|
1632
|
+
syntaxError = "expected '$else' with no trailing tokens";
|
|
1633
|
+
} else if (directiveKind === "for") {
|
|
1634
|
+
syntaxError = validateJemplForDirectiveSyntax(key);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if (syntaxError) {
|
|
1638
|
+
diagnostics.push(createJemplControlDiagnostic({
|
|
1639
|
+
key,
|
|
1640
|
+
line,
|
|
1641
|
+
message: syntaxError,
|
|
1642
|
+
}));
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
if (directiveKind === "if") {
|
|
1646
|
+
hasOpenIfChain = !syntaxError;
|
|
1647
|
+
} else if (directiveKind === "elif") {
|
|
1648
|
+
if (!hasOpenIfChain) {
|
|
1649
|
+
diagnostics.push(createJemplControlDiagnostic({
|
|
1650
|
+
key,
|
|
1651
|
+
line,
|
|
1652
|
+
message: "'$elif' requires a preceding '$if' in the same object scope",
|
|
1653
|
+
}));
|
|
1654
|
+
}
|
|
1655
|
+
hasOpenIfChain = !syntaxError;
|
|
1656
|
+
} else if (directiveKind === "else") {
|
|
1657
|
+
if (!hasOpenIfChain) {
|
|
1658
|
+
diagnostics.push(createJemplControlDiagnostic({
|
|
1659
|
+
key,
|
|
1660
|
+
line,
|
|
1661
|
+
message: "'$else' requires a preceding '$if' or '$elif' in the same object scope",
|
|
1662
|
+
}));
|
|
1663
|
+
}
|
|
1664
|
+
hasOpenIfChain = false;
|
|
1665
|
+
} else {
|
|
1666
|
+
hasOpenIfChain = false;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
visitNode(value);
|
|
1670
|
+
});
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
visitNode(template);
|
|
1674
|
+
return diagnostics;
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
export const parseJemplForCompiler = ({
|
|
1678
|
+
source,
|
|
1679
|
+
viewText = "",
|
|
1680
|
+
fallbackLine,
|
|
1681
|
+
strictControlDirectives = false,
|
|
1682
|
+
} = {}) => {
|
|
1683
|
+
try {
|
|
1684
|
+
const ast = parseJempl(source);
|
|
1685
|
+
const controlDiagnostics = strictControlDirectives
|
|
1686
|
+
? collectJemplControlDirectiveDiagnostics({
|
|
1687
|
+
template: source,
|
|
1688
|
+
viewText,
|
|
1689
|
+
fallbackLine,
|
|
1690
|
+
})
|
|
1691
|
+
: [];
|
|
1692
|
+
|
|
1693
|
+
return {
|
|
1694
|
+
ast,
|
|
1695
|
+
typedAst: mapJemplNodeToTypedAst(ast),
|
|
1696
|
+
parseError: null,
|
|
1697
|
+
controlDiagnostics,
|
|
1698
|
+
};
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
return {
|
|
1701
|
+
ast: null,
|
|
1702
|
+
typedAst: null,
|
|
1703
|
+
parseError: {
|
|
1704
|
+
line: Number.isInteger(fallbackLine) ? fallbackLine : undefined,
|
|
1705
|
+
message: normalizeJemplErrorMessage(error, "Jempl parse failed"),
|
|
1706
|
+
},
|
|
1707
|
+
controlDiagnostics: [],
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
|
|
1712
|
+
const collectSelectorBindingsFromViewAst = ({
|
|
1713
|
+
viewText = "",
|
|
1714
|
+
template,
|
|
1715
|
+
}) => {
|
|
1716
|
+
const lineCandidates = collectTemplateSelectorLineCandidates(viewText);
|
|
1717
|
+
const parsedTemplate = parseJemplForCompiler({ source: template });
|
|
1718
|
+
if (!parsedTemplate.ast) {
|
|
1719
|
+
return [];
|
|
1720
|
+
}
|
|
1721
|
+
const jemplAst = parsedTemplate.ast;
|
|
1722
|
+
const selectorEntries = collectSelectorEntriesFromJemplAst(jemplAst);
|
|
1723
|
+
|
|
1724
|
+
let lastMatchedIndex = -1;
|
|
1725
|
+
|
|
1726
|
+
return selectorEntries
|
|
1727
|
+
.map((entry) => {
|
|
1728
|
+
const key = entry?.key;
|
|
1729
|
+
if (!key) {
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const lineMatch = consumeCandidateLineForKey({
|
|
1734
|
+
candidates: lineCandidates,
|
|
1735
|
+
key,
|
|
1736
|
+
lastMatchedIndex,
|
|
1737
|
+
});
|
|
1738
|
+
lastMatchedIndex = lineMatch.nextIndex;
|
|
1739
|
+
const feParsed = lineMatch.candidate?.feParsed || parseSelectorKeyWithFe(key);
|
|
1740
|
+
const bindingNames = [
|
|
1741
|
+
...new Set([
|
|
1742
|
+
...(Array.isArray(feParsed.bindingNames) ? feParsed.bindingNames : []),
|
|
1743
|
+
...(Array.isArray(entry.bindingNames) ? entry.bindingNames : []),
|
|
1744
|
+
]),
|
|
1745
|
+
];
|
|
1746
|
+
|
|
1747
|
+
return {
|
|
1748
|
+
line: lineMatch.line,
|
|
1749
|
+
rawKey: key,
|
|
1750
|
+
selector: feParsed.selector,
|
|
1751
|
+
tagName: feParsed.tagName,
|
|
1752
|
+
attrsText: feParsed.attrsText,
|
|
1753
|
+
bindingNames,
|
|
1754
|
+
filePath: null,
|
|
1755
|
+
};
|
|
1756
|
+
})
|
|
1757
|
+
.filter(Boolean);
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
const splitTopLevelWhitespace = (source = "") => {
|
|
1761
|
+
const tokens = [];
|
|
1762
|
+
let current = "";
|
|
1763
|
+
let quote = null;
|
|
1764
|
+
let parenDepth = 0;
|
|
1765
|
+
let bracketDepth = 0;
|
|
1766
|
+
let braceDepth = 0;
|
|
1767
|
+
|
|
1768
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1769
|
+
const char = source[index];
|
|
1770
|
+
const isTopLevel = parenDepth === 0 && bracketDepth === 0 && braceDepth === 0;
|
|
1771
|
+
|
|
1772
|
+
if (quote) {
|
|
1773
|
+
current += char;
|
|
1774
|
+
|
|
1775
|
+
if (quote === "\"" && char === "\\") {
|
|
1776
|
+
if (index + 1 < source.length) {
|
|
1777
|
+
current += source[index + 1];
|
|
1778
|
+
index += 1;
|
|
1779
|
+
}
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (quote === "'" && char === "'" && source[index + 1] === "'") {
|
|
1784
|
+
current += source[index + 1];
|
|
1785
|
+
index += 1;
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
if (char === quote) {
|
|
1790
|
+
quote = null;
|
|
1791
|
+
}
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
if (char === "\"" || char === "'") {
|
|
1796
|
+
quote = char;
|
|
1797
|
+
current += char;
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (char === "(") {
|
|
1802
|
+
parenDepth += 1;
|
|
1803
|
+
current += char;
|
|
1804
|
+
continue;
|
|
1805
|
+
}
|
|
1806
|
+
if (char === ")" && parenDepth > 0) {
|
|
1807
|
+
parenDepth -= 1;
|
|
1808
|
+
current += char;
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
if (char === "[") {
|
|
1812
|
+
bracketDepth += 1;
|
|
1813
|
+
current += char;
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
if (char === "]" && bracketDepth > 0) {
|
|
1817
|
+
bracketDepth -= 1;
|
|
1818
|
+
current += char;
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
if (char === "{") {
|
|
1822
|
+
braceDepth += 1;
|
|
1823
|
+
current += char;
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
if (char === "}" && braceDepth > 0) {
|
|
1827
|
+
braceDepth -= 1;
|
|
1828
|
+
current += char;
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
if (isTopLevel && /\s/u.test(char)) {
|
|
1833
|
+
const token = current.trim();
|
|
1834
|
+
if (token) {
|
|
1835
|
+
tokens.push(token);
|
|
1836
|
+
}
|
|
1837
|
+
current = "";
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
current += char;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
const finalToken = current.trim();
|
|
1845
|
+
if (finalToken) {
|
|
1846
|
+
tokens.push(finalToken);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
return tokens;
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
export const parseBindingNames = (attrsText = "") => {
|
|
1853
|
+
if (!attrsText) {
|
|
1854
|
+
return [];
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
const localTokens = splitTopLevelWhitespace(attrsText);
|
|
1858
|
+
const localBindingNames = [];
|
|
1859
|
+
const localSeen = new Set();
|
|
1860
|
+
localTokens.forEach((token) => {
|
|
1861
|
+
const assignmentIndex = token.indexOf("=");
|
|
1862
|
+
const name = (assignmentIndex === -1 ? token : token.slice(0, assignmentIndex)).trim();
|
|
1863
|
+
if (!name || localSeen.has(name)) {
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
localSeen.add(name);
|
|
1867
|
+
localBindingNames.push(name);
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
const looksMalformedBindingName = (name = "") => {
|
|
1871
|
+
if (!name) {
|
|
1872
|
+
return true;
|
|
1873
|
+
}
|
|
1874
|
+
if (name.includes("{") || name.includes("}")) {
|
|
1875
|
+
return true;
|
|
1876
|
+
}
|
|
1877
|
+
if (name.endsWith(":") && !name.startsWith(":")) {
|
|
1878
|
+
return true;
|
|
1879
|
+
}
|
|
1880
|
+
return false;
|
|
1881
|
+
};
|
|
1882
|
+
|
|
1883
|
+
const hasPrefixedBindingNameLoss = ({ localNames = [], sharedNames = [] } = {}) => {
|
|
1884
|
+
if (!localNames.length || !sharedNames.length) {
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
return localNames.some((name) => {
|
|
1889
|
+
if (!name || !["@", ":", "?", "."].some((prefix) => name.startsWith(prefix))) {
|
|
1890
|
+
return false;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const withoutPrefix = name.slice(1);
|
|
1894
|
+
if (!withoutPrefix) {
|
|
1895
|
+
return false;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
return !sharedNames.includes(name) && sharedNames.includes(withoutPrefix);
|
|
1899
|
+
});
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
try {
|
|
1903
|
+
const sharedNames = collectFeBindingNames(attrsText);
|
|
1904
|
+
if (Array.isArray(sharedNames)) {
|
|
1905
|
+
if (sharedNames.length === 0 && localBindingNames.length > 0) {
|
|
1906
|
+
return localBindingNames;
|
|
1907
|
+
}
|
|
1908
|
+
const sharedHasMalformed = sharedNames.some((name) => looksMalformedBindingName(name));
|
|
1909
|
+
const localHasMalformed = localBindingNames.some((name) => looksMalformedBindingName(name));
|
|
1910
|
+
if (sharedHasMalformed && !localHasMalformed && localBindingNames.length > 0) {
|
|
1911
|
+
return localBindingNames;
|
|
1912
|
+
}
|
|
1913
|
+
if (hasPrefixedBindingNameLoss({ localNames: localBindingNames, sharedNames })) {
|
|
1914
|
+
return localBindingNames;
|
|
1915
|
+
}
|
|
1916
|
+
return sharedNames;
|
|
1917
|
+
}
|
|
1918
|
+
} catch {
|
|
1919
|
+
// Fallback to local tokenizer when shared parser fails.
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
return localBindingNames;
|
|
1923
|
+
};
|
|
1924
|
+
|
|
1925
|
+
export const collectSelectorBindingsFromViewText = (viewText = "") => {
|
|
1926
|
+
const lines = viewText.split("\n");
|
|
1927
|
+
const results = [];
|
|
1928
|
+
let currentTopLevelKey = null;
|
|
1929
|
+
let expectsMultilineExplicitKey = false;
|
|
1930
|
+
|
|
1931
|
+
lines.forEach((line, lineIndex) => {
|
|
1932
|
+
const trimmed = line.trim();
|
|
1933
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
const indent = line.match(/^\s*/)?.[0].length || 0;
|
|
1938
|
+
if (indent === 0) {
|
|
1939
|
+
currentTopLevelKey = splitYamlMappingLineKey(line) || null;
|
|
1940
|
+
expectsMultilineExplicitKey = false;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
if (currentTopLevelKey !== "template") {
|
|
1944
|
+
expectsMultilineExplicitKey = false;
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
let rawKey = null;
|
|
1949
|
+
|
|
1950
|
+
if (expectsMultilineExplicitKey) {
|
|
1951
|
+
if (indent > 0) {
|
|
1952
|
+
rawKey = splitYamlMultilineExplicitKey(line);
|
|
1953
|
+
}
|
|
1954
|
+
expectsMultilineExplicitKey = false;
|
|
1955
|
+
} else {
|
|
1956
|
+
const listLine = splitYamlListLineKey(line);
|
|
1957
|
+
if (listLine.expectsMultilineExplicitKey) {
|
|
1958
|
+
expectsMultilineExplicitKey = true;
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
rawKey = listLine.key;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (!rawKey) {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
let key = stripOuterQuotes(rawKey.trim());
|
|
1969
|
+
while (key.startsWith("- ")) {
|
|
1970
|
+
key = key.slice(2).trim();
|
|
1971
|
+
}
|
|
1972
|
+
if (!key || CONTROL_PREFIXES.some((prefix) => key.startsWith(prefix))) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
const feParsed = parseSelectorKeyWithFe(key);
|
|
1977
|
+
|
|
1978
|
+
results.push({
|
|
1979
|
+
line: lineIndex + 1,
|
|
1980
|
+
rawKey: key,
|
|
1981
|
+
selector: feParsed.selector,
|
|
1982
|
+
tagName: feParsed.tagName,
|
|
1983
|
+
attrsText: feParsed.attrsText,
|
|
1984
|
+
bindingNames: feParsed.bindingNames,
|
|
1985
|
+
filePath: null,
|
|
1986
|
+
});
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
return results;
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
export const collectSelectorBindingsFromView = ({
|
|
1993
|
+
viewText = "",
|
|
1994
|
+
viewYaml,
|
|
1995
|
+
} = {}) => {
|
|
1996
|
+
const template = viewYaml?.template;
|
|
1997
|
+
if (template === undefined) {
|
|
1998
|
+
return collectSelectorBindingsFromViewText(viewText);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
return collectSelectorBindingsFromViewAst({
|
|
2002
|
+
viewText,
|
|
2003
|
+
template,
|
|
2004
|
+
});
|
|
2005
|
+
};
|
|
2006
|
+
|
|
2007
|
+
const createLineOffsets = (source = "") => {
|
|
2008
|
+
const offsets = [0];
|
|
2009
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
2010
|
+
if (source[index] === "\n") {
|
|
2011
|
+
offsets.push(index + 1);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return offsets;
|
|
2015
|
+
};
|
|
2016
|
+
|
|
2017
|
+
const getOffsetFromLineColumn = ({ lineOffsets = [], line = 1, column = 1 }) => {
|
|
2018
|
+
const lineIndex = Math.max(0, line - 1);
|
|
2019
|
+
const lineStart = lineOffsets[lineIndex] || 0;
|
|
2020
|
+
return lineStart + Math.max(0, column - 1);
|
|
2021
|
+
};
|
|
2022
|
+
|
|
2023
|
+
const toRangeWithLength = (range = {}) => {
|
|
2024
|
+
const offset = Number.isInteger(range.offset) ? range.offset : 0;
|
|
2025
|
+
const endOffset = Number.isInteger(range.endOffset) ? range.endOffset : offset;
|
|
2026
|
+
|
|
2027
|
+
return {
|
|
2028
|
+
...range,
|
|
2029
|
+
offset,
|
|
2030
|
+
endOffset,
|
|
2031
|
+
length: Math.max(0, endOffset - offset),
|
|
2032
|
+
};
|
|
2033
|
+
};
|
|
2034
|
+
|
|
2035
|
+
const detectBindingSourceType = (bindingName = "") => {
|
|
2036
|
+
if (bindingName.startsWith("@")) return "event";
|
|
2037
|
+
if (bindingName.startsWith(":")) return "prop";
|
|
2038
|
+
if (bindingName.startsWith("?")) return "boolean-attr";
|
|
2039
|
+
if (bindingName.startsWith(".")) return "legacy-prop";
|
|
2040
|
+
return "attr";
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
const stripBindingPrefix = (bindingName = "") => {
|
|
2044
|
+
return /^[.@:?]/.test(bindingName) ? bindingName.slice(1) : bindingName;
|
|
2045
|
+
};
|
|
2046
|
+
|
|
2047
|
+
const splitBindingNameAndValue = (rawToken = "") => {
|
|
2048
|
+
const assignmentIndex = rawToken.indexOf("=");
|
|
2049
|
+
if (assignmentIndex === -1) {
|
|
2050
|
+
return {
|
|
2051
|
+
bindingName: rawToken.trim(),
|
|
2052
|
+
valueText: undefined,
|
|
2053
|
+
valueSource: "",
|
|
2054
|
+
valueStartOffset: 0,
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
const valueSourceRaw = rawToken.slice(assignmentIndex + 1);
|
|
2059
|
+
const firstValueCharOffset = valueSourceRaw.search(/\S/u);
|
|
2060
|
+
const hasValue = firstValueCharOffset !== -1;
|
|
2061
|
+
const resolvedValueStartOffset = assignmentIndex + 1 + (hasValue ? firstValueCharOffset : valueSourceRaw.length);
|
|
2062
|
+
const valueSource = hasValue ? valueSourceRaw.slice(firstValueCharOffset) : "";
|
|
2063
|
+
|
|
2064
|
+
return {
|
|
2065
|
+
bindingName: rawToken.slice(0, assignmentIndex).trim(),
|
|
2066
|
+
valueText: hasValue ? valueSource.trimEnd() : undefined,
|
|
2067
|
+
valueSource,
|
|
2068
|
+
valueStartOffset: resolvedValueStartOffset,
|
|
2069
|
+
};
|
|
2070
|
+
};
|
|
2071
|
+
|
|
2072
|
+
const extractExpressionTokensWithRanges = (valueSource = "") => {
|
|
2073
|
+
if (!valueSource) {
|
|
2074
|
+
return [];
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const matches = [];
|
|
2078
|
+
const patterns = [
|
|
2079
|
+
{ regex: /\$\{([^}]*)\}/gu, wrapperStartLength: 2, wrapperEndLength: 1 },
|
|
2080
|
+
{ regex: /#\{([^}]*)\}/gu, wrapperStartLength: 2, wrapperEndLength: 1 },
|
|
2081
|
+
{ regex: /\{\{([^}]*)\}\}/gu, wrapperStartLength: 2, wrapperEndLength: 2 },
|
|
2082
|
+
];
|
|
2083
|
+
|
|
2084
|
+
patterns.forEach((pattern) => {
|
|
2085
|
+
let match = pattern.regex.exec(valueSource);
|
|
2086
|
+
while (match) {
|
|
2087
|
+
const rawBody = String(match[1] || "");
|
|
2088
|
+
const expression = rawBody.trim();
|
|
2089
|
+
if (expression) {
|
|
2090
|
+
const leadingWhitespaceLength = rawBody.length - rawBody.trimStart().length;
|
|
2091
|
+
const bodyStartOffset = match.index + pattern.wrapperStartLength + leadingWhitespaceLength;
|
|
2092
|
+
const bodyEndOffset = bodyStartOffset + expression.length;
|
|
2093
|
+
|
|
2094
|
+
matches.push({
|
|
2095
|
+
expression,
|
|
2096
|
+
raw: String(match[0] || ""),
|
|
2097
|
+
startOffset: bodyStartOffset,
|
|
2098
|
+
endOffset: bodyEndOffset,
|
|
2099
|
+
length: expression.length,
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
match = pattern.regex.exec(valueSource);
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
matches.sort((left, right) => {
|
|
2107
|
+
if (left.startOffset !== right.startOffset) {
|
|
2108
|
+
return left.startOffset - right.startOffset;
|
|
2109
|
+
}
|
|
2110
|
+
return left.endOffset - right.endOffset;
|
|
2111
|
+
});
|
|
2112
|
+
|
|
2113
|
+
return matches;
|
|
2114
|
+
};
|
|
2115
|
+
|
|
2116
|
+
const extractExpressionTokens = (valueText = "") => {
|
|
2117
|
+
return [...new Set(
|
|
2118
|
+
extractExpressionTokensWithRanges(valueText).map((entry) => entry.expression),
|
|
2119
|
+
)];
|
|
2120
|
+
};
|
|
2121
|
+
|
|
2122
|
+
const tokenizeTopLevelWhitespaceWithOffsets = (source = "") => {
|
|
2123
|
+
const tokens = [];
|
|
2124
|
+
let current = "";
|
|
2125
|
+
let currentStart = -1;
|
|
2126
|
+
let quote = null;
|
|
2127
|
+
let parenDepth = 0;
|
|
2128
|
+
let bracketDepth = 0;
|
|
2129
|
+
let braceDepth = 0;
|
|
2130
|
+
|
|
2131
|
+
const flush = (endIndexExclusive) => {
|
|
2132
|
+
const raw = current.trim();
|
|
2133
|
+
if (raw) {
|
|
2134
|
+
const leftTrim = current.search(/\S/u);
|
|
2135
|
+
const rightTrim = current.length - current.trimEnd().length;
|
|
2136
|
+
tokens.push({
|
|
2137
|
+
raw,
|
|
2138
|
+
start: currentStart + Math.max(leftTrim, 0),
|
|
2139
|
+
end: endIndexExclusive - rightTrim,
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
current = "";
|
|
2143
|
+
currentStart = -1;
|
|
2144
|
+
};
|
|
2145
|
+
|
|
2146
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
2147
|
+
const char = source[index];
|
|
2148
|
+
const isTopLevel = parenDepth === 0 && bracketDepth === 0 && braceDepth === 0;
|
|
2149
|
+
|
|
2150
|
+
if (currentStart === -1 && !/\s/u.test(char)) {
|
|
2151
|
+
currentStart = index;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
if (quote) {
|
|
2155
|
+
current += char;
|
|
2156
|
+
if (quote === "\"" && char === "\\") {
|
|
2157
|
+
if (index + 1 < source.length) {
|
|
2158
|
+
current += source[index + 1];
|
|
2159
|
+
index += 1;
|
|
2160
|
+
}
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2163
|
+
if (quote === "'" && char === "'" && source[index + 1] === "'") {
|
|
2164
|
+
current += source[index + 1];
|
|
2165
|
+
index += 1;
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
if (char === quote) {
|
|
2169
|
+
quote = null;
|
|
2170
|
+
}
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
if (char === "\"" || char === "'") {
|
|
2175
|
+
quote = char;
|
|
2176
|
+
current += char;
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
if (char === "(") {
|
|
2180
|
+
parenDepth += 1;
|
|
2181
|
+
current += char;
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
2184
|
+
if (char === ")" && parenDepth > 0) {
|
|
2185
|
+
parenDepth -= 1;
|
|
2186
|
+
current += char;
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
if (char === "[") {
|
|
2190
|
+
bracketDepth += 1;
|
|
2191
|
+
current += char;
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
if (char === "]" && bracketDepth > 0) {
|
|
2195
|
+
bracketDepth -= 1;
|
|
2196
|
+
current += char;
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
if (char === "{") {
|
|
2200
|
+
braceDepth += 1;
|
|
2201
|
+
current += char;
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
if (char === "}" && braceDepth > 0) {
|
|
2205
|
+
braceDepth -= 1;
|
|
2206
|
+
current += char;
|
|
2207
|
+
continue;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
if (isTopLevel && /\s/u.test(char)) {
|
|
2211
|
+
if (current) {
|
|
2212
|
+
flush(index);
|
|
2213
|
+
}
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
current += char;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
if (current) {
|
|
2221
|
+
flush(source.length);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
return tokens;
|
|
2225
|
+
};
|
|
2226
|
+
|
|
2227
|
+
const toRelativeTokenRange = ({ startOffset = 0, endOffset = 0 }) => {
|
|
2228
|
+
const safeStartOffset = Math.max(0, Number(startOffset) || 0);
|
|
2229
|
+
const safeEndOffset = Math.max(safeStartOffset, Number(endOffset) || safeStartOffset);
|
|
2230
|
+
const length = safeEndOffset - safeStartOffset;
|
|
2231
|
+
|
|
2232
|
+
return {
|
|
2233
|
+
startOffset: safeStartOffset,
|
|
2234
|
+
endOffset: safeEndOffset,
|
|
2235
|
+
length,
|
|
2236
|
+
column: safeStartOffset + 1,
|
|
2237
|
+
endColumn: safeStartOffset + Math.max(length, 1),
|
|
2238
|
+
};
|
|
2239
|
+
};
|
|
2240
|
+
|
|
2241
|
+
export const tokenizeYahtmlSelectorKey = (rawKey = "") => {
|
|
2242
|
+
const source = String(rawKey || "");
|
|
2243
|
+
let cursor = 0;
|
|
2244
|
+
|
|
2245
|
+
while (cursor < source.length && /\s/u.test(source[cursor])) {
|
|
2246
|
+
cursor += 1;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
const selectorStart = cursor;
|
|
2250
|
+
while (cursor < source.length && !/\s/u.test(source[cursor])) {
|
|
2251
|
+
cursor += 1;
|
|
2252
|
+
}
|
|
2253
|
+
const selectorEnd = cursor;
|
|
2254
|
+
const selectorRaw = source.slice(selectorStart, selectorEnd);
|
|
2255
|
+
|
|
2256
|
+
while (cursor < source.length && /\s/u.test(source[cursor])) {
|
|
2257
|
+
cursor += 1;
|
|
2258
|
+
}
|
|
2259
|
+
const attrsStart = cursor;
|
|
2260
|
+
const attrsText = source.slice(attrsStart);
|
|
2261
|
+
const attrsTextTokens = tokenizeTopLevelWhitespaceWithOffsets(attrsText);
|
|
2262
|
+
|
|
2263
|
+
const selectorToken = selectorRaw
|
|
2264
|
+
? {
|
|
2265
|
+
kind: "YahtmlSelectorToken",
|
|
2266
|
+
raw: selectorRaw,
|
|
2267
|
+
...toRelativeTokenRange({
|
|
2268
|
+
startOffset: selectorStart,
|
|
2269
|
+
endOffset: selectorEnd,
|
|
2270
|
+
}),
|
|
2271
|
+
}
|
|
2272
|
+
: null;
|
|
2273
|
+
|
|
2274
|
+
const attributeTokens = attrsTextTokens.map((token) => {
|
|
2275
|
+
const {
|
|
2276
|
+
bindingName,
|
|
2277
|
+
valueText,
|
|
2278
|
+
valueSource,
|
|
2279
|
+
valueStartOffset,
|
|
2280
|
+
} = splitBindingNameAndValue(token.raw);
|
|
2281
|
+
const expressionNodes = extractExpressionTokensWithRanges(valueSource).map((expressionNode) => ({
|
|
2282
|
+
kind: "YahtmlExpressionToken",
|
|
2283
|
+
expression: expressionNode.expression,
|
|
2284
|
+
raw: expressionNode.raw,
|
|
2285
|
+
...toRelativeTokenRange({
|
|
2286
|
+
startOffset: attrsStart + token.start + valueStartOffset + expressionNode.startOffset,
|
|
2287
|
+
endOffset: attrsStart + token.start + valueStartOffset + expressionNode.endOffset,
|
|
2288
|
+
}),
|
|
2289
|
+
}));
|
|
2290
|
+
|
|
2291
|
+
return {
|
|
2292
|
+
kind: "YahtmlAttributeToken",
|
|
2293
|
+
raw: token.raw,
|
|
2294
|
+
bindingName,
|
|
2295
|
+
sourceType: detectBindingSourceType(bindingName),
|
|
2296
|
+
name: stripBindingPrefix(bindingName),
|
|
2297
|
+
valueText,
|
|
2298
|
+
expressions: [...new Set(expressionNodes.map((node) => node.expression))],
|
|
2299
|
+
expressionNodes,
|
|
2300
|
+
...toRelativeTokenRange({
|
|
2301
|
+
startOffset: attrsStart + token.start,
|
|
2302
|
+
endOffset: attrsStart + token.end,
|
|
2303
|
+
}),
|
|
2304
|
+
};
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
return {
|
|
2308
|
+
kind: "YahtmlSelectorTokenStream",
|
|
2309
|
+
raw: source,
|
|
2310
|
+
selectorToken,
|
|
2311
|
+
attributeTokens,
|
|
2312
|
+
};
|
|
2313
|
+
};
|
|
2314
|
+
|
|
2315
|
+
const resolveRawKeyRange = ({
|
|
2316
|
+
lineText = "",
|
|
2317
|
+
rawKey = "",
|
|
2318
|
+
line,
|
|
2319
|
+
lineOffsets,
|
|
2320
|
+
}) => {
|
|
2321
|
+
const candidates = [
|
|
2322
|
+
rawKey,
|
|
2323
|
+
`'${rawKey}'`,
|
|
2324
|
+
`"${rawKey}"`,
|
|
2325
|
+
];
|
|
2326
|
+
|
|
2327
|
+
let matched = rawKey;
|
|
2328
|
+
let index = -1;
|
|
2329
|
+
for (let i = 0; i < candidates.length; i += 1) {
|
|
2330
|
+
const candidate = candidates[i];
|
|
2331
|
+
const found = lineText.indexOf(candidate);
|
|
2332
|
+
if (found !== -1) {
|
|
2333
|
+
matched = candidate;
|
|
2334
|
+
index = found;
|
|
2335
|
+
break;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
if (index === -1) {
|
|
2340
|
+
const listDashIndex = lineText.indexOf("-");
|
|
2341
|
+
index = listDashIndex === -1 ? 0 : listDashIndex + 1;
|
|
2342
|
+
matched = rawKey;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
const startColumn = index + 1;
|
|
2346
|
+
const endColumn = index + Math.max(matched.length, 1);
|
|
2347
|
+
|
|
2348
|
+
return toRangeWithLength({
|
|
2349
|
+
line,
|
|
2350
|
+
column: startColumn,
|
|
2351
|
+
endLine: line,
|
|
2352
|
+
endColumn,
|
|
2353
|
+
offset: getOffsetFromLineColumn({ lineOffsets, line, column: startColumn }),
|
|
2354
|
+
endOffset: getOffsetFromLineColumn({ lineOffsets, line, column: endColumn + 1 }),
|
|
2355
|
+
});
|
|
2356
|
+
};
|
|
2357
|
+
|
|
2358
|
+
const buildAttributeNodes = ({
|
|
2359
|
+
attrsText = "",
|
|
2360
|
+
rawKey = "",
|
|
2361
|
+
elementRange,
|
|
2362
|
+
lineOffsets,
|
|
2363
|
+
}) => {
|
|
2364
|
+
if (!attrsText) {
|
|
2365
|
+
return [];
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
const tokens = tokenizeTopLevelWhitespaceWithOffsets(attrsText);
|
|
2369
|
+
if (tokens.length === 0) {
|
|
2370
|
+
return [];
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
const attrsTextIndexInRaw = rawKey.indexOf(attrsText);
|
|
2374
|
+
const rawStartColumn = elementRange.column;
|
|
2375
|
+
|
|
2376
|
+
return tokens.map((token) => {
|
|
2377
|
+
const tokenIndexInRaw = attrsTextIndexInRaw >= 0
|
|
2378
|
+
? attrsTextIndexInRaw + token.start
|
|
2379
|
+
: Math.max(rawKey.indexOf(token.raw), 0);
|
|
2380
|
+
const tokenColumn = rawStartColumn + tokenIndexInRaw;
|
|
2381
|
+
const tokenEndColumn = tokenColumn + Math.max(token.raw.length, 1) - 1;
|
|
2382
|
+
const {
|
|
2383
|
+
bindingName,
|
|
2384
|
+
valueText,
|
|
2385
|
+
valueSource,
|
|
2386
|
+
valueStartOffset,
|
|
2387
|
+
} = splitBindingNameAndValue(token.raw);
|
|
2388
|
+
const sourceType = detectBindingSourceType(bindingName);
|
|
2389
|
+
const name = stripBindingPrefix(bindingName);
|
|
2390
|
+
const expressionNodes = extractExpressionTokensWithRanges(valueSource).map((expressionNode) => {
|
|
2391
|
+
const expressionColumn = tokenColumn + valueStartOffset + expressionNode.startOffset;
|
|
2392
|
+
const expressionEndColumn = expressionColumn + Math.max(expressionNode.length, 1) - 1;
|
|
2393
|
+
return {
|
|
2394
|
+
type: "Expression",
|
|
2395
|
+
kind: "YahtmlExpressionAst",
|
|
2396
|
+
expression: expressionNode.expression,
|
|
2397
|
+
raw: expressionNode.raw,
|
|
2398
|
+
rawLexeme: expressionNode.raw,
|
|
2399
|
+
range: toRangeWithLength({
|
|
2400
|
+
line: elementRange.line,
|
|
2401
|
+
column: expressionColumn,
|
|
2402
|
+
endLine: elementRange.endLine,
|
|
2403
|
+
endColumn: expressionEndColumn,
|
|
2404
|
+
offset: getOffsetFromLineColumn({
|
|
2405
|
+
lineOffsets,
|
|
2406
|
+
line: elementRange.line,
|
|
2407
|
+
column: expressionColumn,
|
|
2408
|
+
}),
|
|
2409
|
+
endOffset: getOffsetFromLineColumn({
|
|
2410
|
+
lineOffsets,
|
|
2411
|
+
line: elementRange.endLine,
|
|
2412
|
+
column: expressionEndColumn + 1,
|
|
2413
|
+
}),
|
|
2414
|
+
}),
|
|
2415
|
+
};
|
|
2416
|
+
});
|
|
2417
|
+
const expressions = [...new Set(expressionNodes.map((entry) => entry.expression))];
|
|
2418
|
+
|
|
2419
|
+
return {
|
|
2420
|
+
type: "Attribute",
|
|
2421
|
+
kind: "YahtmlAttributeAst",
|
|
2422
|
+
raw: token.raw,
|
|
2423
|
+
rawLexeme: token.raw,
|
|
2424
|
+
bindingName,
|
|
2425
|
+
sourceType,
|
|
2426
|
+
name,
|
|
2427
|
+
valueText,
|
|
2428
|
+
expressions,
|
|
2429
|
+
expressionNodes,
|
|
2430
|
+
range: toRangeWithLength({
|
|
2431
|
+
line: elementRange.line,
|
|
2432
|
+
column: tokenColumn,
|
|
2433
|
+
endLine: elementRange.endLine,
|
|
2434
|
+
endColumn: tokenEndColumn,
|
|
2435
|
+
offset: getOffsetFromLineColumn({
|
|
2436
|
+
lineOffsets,
|
|
2437
|
+
line: elementRange.line,
|
|
2438
|
+
column: tokenColumn,
|
|
2439
|
+
}),
|
|
2440
|
+
endOffset: getOffsetFromLineColumn({
|
|
2441
|
+
lineOffsets,
|
|
2442
|
+
line: elementRange.endLine,
|
|
2443
|
+
column: tokenEndColumn + 1,
|
|
2444
|
+
}),
|
|
2445
|
+
}),
|
|
2446
|
+
};
|
|
2447
|
+
});
|
|
2448
|
+
};
|
|
2449
|
+
|
|
2450
|
+
const parseYahtmlSelectorErrorMessage = (error) => {
|
|
2451
|
+
const fallback = "selector key parse failed";
|
|
2452
|
+
if (!error) {
|
|
2453
|
+
return fallback;
|
|
2454
|
+
}
|
|
2455
|
+
if (typeof error?.message === "string" && error.message.trim().length > 0) {
|
|
2456
|
+
return error.message.trim();
|
|
2457
|
+
}
|
|
2458
|
+
return fallback;
|
|
2459
|
+
};
|
|
2460
|
+
|
|
2461
|
+
const createYahtmlParseDiagnostic = ({
|
|
2462
|
+
code = "RTGL-CHECK-YAHTML-PARSE-001",
|
|
2463
|
+
filePath,
|
|
2464
|
+
line,
|
|
2465
|
+
column,
|
|
2466
|
+
endLine,
|
|
2467
|
+
endColumn,
|
|
2468
|
+
message,
|
|
2469
|
+
}) => {
|
|
2470
|
+
return {
|
|
2471
|
+
code,
|
|
2472
|
+
severity: "error",
|
|
2473
|
+
filePath,
|
|
2474
|
+
line,
|
|
2475
|
+
column,
|
|
2476
|
+
endLine,
|
|
2477
|
+
endColumn,
|
|
2478
|
+
message,
|
|
2479
|
+
};
|
|
2480
|
+
};
|
|
2481
|
+
|
|
2482
|
+
export const parseYahtmlSelectorKey = ({
|
|
2483
|
+
rawKey = "",
|
|
2484
|
+
line = 1,
|
|
2485
|
+
lineText = "",
|
|
2486
|
+
lineOffsets = [],
|
|
2487
|
+
filePath,
|
|
2488
|
+
} = {}) => {
|
|
2489
|
+
const keyText = String(rawKey || "");
|
|
2490
|
+
const parsedFe = parseSelectorKeyWithFe(keyText);
|
|
2491
|
+
const tokenStream = tokenizeYahtmlSelectorKey(keyText);
|
|
2492
|
+
const resolvedLine = Number.isInteger(line) ? line : 1;
|
|
2493
|
+
const resolvedLineText = lineText || keyText;
|
|
2494
|
+
const resolvedLineOffsets = Array.isArray(lineOffsets) && lineOffsets.length > 0
|
|
2495
|
+
? lineOffsets
|
|
2496
|
+
: createLineOffsets(resolvedLineText);
|
|
2497
|
+
const range = resolveRawKeyRange({
|
|
2498
|
+
lineText: resolvedLineText,
|
|
2499
|
+
rawKey: keyText,
|
|
2500
|
+
line: resolvedLine,
|
|
2501
|
+
lineOffsets: resolvedLineOffsets,
|
|
2502
|
+
});
|
|
2503
|
+
const attributes = buildAttributeNodes({
|
|
2504
|
+
attrsText: parsedFe.attrsText,
|
|
2505
|
+
rawKey: keyText,
|
|
2506
|
+
elementRange: range,
|
|
2507
|
+
lineOffsets: resolvedLineOffsets,
|
|
2508
|
+
});
|
|
2509
|
+
const diagnostics = [];
|
|
2510
|
+
|
|
2511
|
+
if (!parsedFe.selector) {
|
|
2512
|
+
diagnostics.push(createYahtmlParseDiagnostic({
|
|
2513
|
+
filePath,
|
|
2514
|
+
line: range.line,
|
|
2515
|
+
column: range.column,
|
|
2516
|
+
endLine: range.endLine,
|
|
2517
|
+
endColumn: range.endColumn,
|
|
2518
|
+
message: "Invalid YAHTML selector key: missing selector token.",
|
|
2519
|
+
}));
|
|
2520
|
+
} else {
|
|
2521
|
+
try {
|
|
2522
|
+
parseYahtmlElementKey(keyText);
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
diagnostics.push(createYahtmlParseDiagnostic({
|
|
2525
|
+
filePath,
|
|
2526
|
+
line: range.line,
|
|
2527
|
+
column: range.column,
|
|
2528
|
+
endLine: range.endLine,
|
|
2529
|
+
endColumn: range.endColumn,
|
|
2530
|
+
message: `Invalid YAHTML selector key '${keyText}': ${parseYahtmlSelectorErrorMessage(error)}.`,
|
|
2531
|
+
}));
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
tokenStream.attributeTokens.forEach((token) => {
|
|
2536
|
+
if (token.bindingName && token.bindingName.length > 0) {
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
const tokenColumn = range.column + token.startOffset;
|
|
2540
|
+
const tokenEndColumn = tokenColumn + Math.max(token.length, 1) - 1;
|
|
2541
|
+
diagnostics.push(createYahtmlParseDiagnostic({
|
|
2542
|
+
filePath,
|
|
2543
|
+
line: range.line,
|
|
2544
|
+
column: tokenColumn,
|
|
2545
|
+
endLine: range.endLine,
|
|
2546
|
+
endColumn: tokenEndColumn,
|
|
2547
|
+
message: `Invalid YAHTML attribute token '${token.raw}'.`,
|
|
2548
|
+
}));
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2551
|
+
const cst = {
|
|
2552
|
+
kind: "YahtmlElementCst",
|
|
2553
|
+
rawKey: keyText,
|
|
2554
|
+
selectorToken: tokenStream.selectorToken,
|
|
2555
|
+
attributeTokens: tokenStream.attributeTokens,
|
|
2556
|
+
};
|
|
2557
|
+
|
|
2558
|
+
return {
|
|
2559
|
+
ast: {
|
|
2560
|
+
type: "Element",
|
|
2561
|
+
kind: "YahtmlElementAst",
|
|
2562
|
+
tagName: parsedFe.tagName,
|
|
2563
|
+
selector: parsedFe.selector,
|
|
2564
|
+
rawKey: keyText,
|
|
2565
|
+
rawLexeme: keyText,
|
|
2566
|
+
range,
|
|
2567
|
+
attributes,
|
|
2568
|
+
},
|
|
2569
|
+
cst,
|
|
2570
|
+
diagnostics,
|
|
2571
|
+
};
|
|
2572
|
+
};
|
|
2573
|
+
|
|
2574
|
+
export const collectTemplateAstFromView = ({
|
|
2575
|
+
viewText = "",
|
|
2576
|
+
viewYaml,
|
|
2577
|
+
} = {}) => {
|
|
2578
|
+
const selectorBindings = collectSelectorBindingsFromView({ viewText, viewYaml });
|
|
2579
|
+
const lines = viewText.split("\n");
|
|
2580
|
+
const lineOffsets = createLineOffsets(viewText);
|
|
2581
|
+
const parseDiagnostics = [];
|
|
2582
|
+
const cstNodes = [];
|
|
2583
|
+
|
|
2584
|
+
const nodes = selectorBindings.map((binding) => {
|
|
2585
|
+
const line = Number.isInteger(binding?.line) ? binding.line : 1;
|
|
2586
|
+
const lineText = lines[line - 1] || "";
|
|
2587
|
+
const rawKey = String(binding?.rawKey || "");
|
|
2588
|
+
const parsedSelector = parseYahtmlSelectorKey({
|
|
2589
|
+
rawKey,
|
|
2590
|
+
line,
|
|
2591
|
+
lineText,
|
|
2592
|
+
lineOffsets,
|
|
2593
|
+
});
|
|
2594
|
+
if (Array.isArray(parsedSelector.diagnostics) && parsedSelector.diagnostics.length > 0) {
|
|
2595
|
+
parsedSelector.diagnostics.forEach((diagnostic) => {
|
|
2596
|
+
parseDiagnostics.push(diagnostic);
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
if (parsedSelector.cst) {
|
|
2600
|
+
cstNodes.push(parsedSelector.cst);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
return {
|
|
2604
|
+
...parsedSelector.ast,
|
|
2605
|
+
tagName: binding?.tagName || parsedSelector.ast.tagName || "",
|
|
2606
|
+
selector: binding?.selector || parsedSelector.ast.selector || "",
|
|
2607
|
+
rawKey,
|
|
2608
|
+
rawLexeme: rawKey,
|
|
2609
|
+
};
|
|
2610
|
+
});
|
|
2611
|
+
|
|
2612
|
+
return {
|
|
2613
|
+
type: "Template",
|
|
2614
|
+
kind: "YahtmlTemplateAst",
|
|
2615
|
+
nodes,
|
|
2616
|
+
cst: {
|
|
2617
|
+
kind: "YahtmlTemplateCst",
|
|
2618
|
+
nodes: cstNodes,
|
|
2619
|
+
},
|
|
2620
|
+
parseDiagnostics,
|
|
2621
|
+
};
|
|
2622
|
+
};
|
|
2623
|
+
|
|
2624
|
+
export const collectTopLevelYamlKeyLines = (yamlText = "") => {
|
|
2625
|
+
const lines = yamlText.split("\n");
|
|
2626
|
+
const keyLines = new Map();
|
|
2627
|
+
|
|
2628
|
+
lines.forEach((line, lineIndex) => {
|
|
2629
|
+
const trimmed = line.trim();
|
|
2630
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
const indent = line.match(/^\s*/)?.[0].length || 0;
|
|
2635
|
+
if (indent !== 0) {
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
const key = splitYamlMappingLineKey(line);
|
|
2640
|
+
if (!key || keyLines.has(key)) {
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
keyLines.set(key, lineIndex + 1);
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
return keyLines;
|
|
2648
|
+
};
|
|
2649
|
+
|
|
2650
|
+
const YAML_PATH_SEPARATOR = "\u0000";
|
|
2651
|
+
|
|
2652
|
+
const toYamlPathKey = (parts = []) => {
|
|
2653
|
+
return parts.join(YAML_PATH_SEPARATOR);
|
|
2654
|
+
};
|
|
2655
|
+
|
|
2656
|
+
export const collectYamlKeyPathLines = (yamlText = "") => {
|
|
2657
|
+
const lines = yamlText.split("\n");
|
|
2658
|
+
const keyPathLines = new Map();
|
|
2659
|
+
const keyStack = [];
|
|
2660
|
+
|
|
2661
|
+
lines.forEach((line, lineIndex) => {
|
|
2662
|
+
const trimmed = line.trim();
|
|
2663
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
const key = splitYamlMappingLineKey(line);
|
|
2668
|
+
if (!key) {
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
const indent = line.match(/^\s*/)?.[0].length || 0;
|
|
2673
|
+
while (keyStack.length > 0 && keyStack[keyStack.length - 1].indent >= indent) {
|
|
2674
|
+
keyStack.pop();
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
keyStack.push({ indent, key });
|
|
2678
|
+
const pathKey = toYamlPathKey(keyStack.map((entry) => entry.key));
|
|
2679
|
+
|
|
2680
|
+
if (!keyPathLines.has(pathKey)) {
|
|
2681
|
+
keyPathLines.set(pathKey, lineIndex + 1);
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
|
|
2685
|
+
return keyPathLines;
|
|
2686
|
+
};
|
|
2687
|
+
|
|
2688
|
+
export const collectRefListeners = (viewYaml, keyPathLines = new Map()) => {
|
|
2689
|
+
const listeners = [];
|
|
2690
|
+
const refs = viewYaml?.refs;
|
|
2691
|
+
|
|
2692
|
+
if (!refs || typeof refs !== "object" || Array.isArray(refs)) {
|
|
2693
|
+
return listeners;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
Object.entries(refs).forEach(([refKey, refConfig]) => {
|
|
2697
|
+
const eventListeners = refConfig?.eventListeners;
|
|
2698
|
+
if (!eventListeners || typeof eventListeners !== "object" || Array.isArray(eventListeners)) {
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
Object.entries(eventListeners).forEach(([eventType, eventConfig]) => {
|
|
2703
|
+
const basePath = ["refs", refKey, "eventListeners", eventType];
|
|
2704
|
+
const optionLines = {};
|
|
2705
|
+
|
|
2706
|
+
if (eventConfig && typeof eventConfig === "object" && !Array.isArray(eventConfig)) {
|
|
2707
|
+
Object.keys(eventConfig).forEach((optionName) => {
|
|
2708
|
+
const optionLine = keyPathLines.get(toYamlPathKey([...basePath, optionName]));
|
|
2709
|
+
if (Number.isInteger(optionLine)) {
|
|
2710
|
+
optionLines[optionName] = optionLine;
|
|
2711
|
+
}
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
listeners.push({
|
|
2716
|
+
refKey,
|
|
2717
|
+
eventType,
|
|
2718
|
+
eventConfig,
|
|
2719
|
+
line: keyPathLines.get(toYamlPathKey(basePath)),
|
|
2720
|
+
optionLines,
|
|
2721
|
+
});
|
|
2722
|
+
});
|
|
2723
|
+
});
|
|
2724
|
+
|
|
2725
|
+
return listeners;
|
|
2726
|
+
};
|