stimeo-ui 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/dist/controllers/alert_dialog_controller.js +32 -5
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +32 -6
- package/dist/controllers/announcer_controller.js +255 -20
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +2 -1
- package/dist/controllers/color_picker_controller.js +46 -0
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js +32 -5
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +32 -5
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +19 -5
- package/dist/controllers/countdown_controller.js +112 -12
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +4 -1
- package/dist/controllers/date_range_picker_controller.js +50 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +32 -5
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +32 -5
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +9 -4
- package/dist/controllers/empty_state_controller.js +24 -10
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/focus_controller.js +32 -5
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +14 -3
- package/dist/controllers/frame_loading_controller.js +177 -15
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +19 -3
- package/dist/controllers/local_time_controller.js +100 -6
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +22 -2
- package/dist/controllers/meter_controller.js +145 -26
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +13 -1
- package/dist/controllers/network_status_controller.js +28 -8
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +33 -6
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +16 -1
- package/dist/controllers/progress_controller.js +116 -9
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +4 -1
- package/dist/controllers/range_slider_controller.js +68 -3
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +4 -1
- package/dist/controllers/rating_controller.js +53 -0
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +13 -0
- package/dist/controllers/relative_time_controller.js +133 -12
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +1 -11
- package/dist/controllers/sidebar_controller.js +37 -8
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +7 -2
- package/dist/controllers/skeleton_controller.js +73 -20
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.js +17 -2
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +37 -4
- package/dist/controllers/spinner_controller.js +228 -27
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +12 -1
- package/dist/controllers/step_indicator_controller.js +82 -5
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +26 -2
- package/dist/controllers/stick_to_bottom_controller.js +60 -10
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/index.js +1056 -281
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.js +785 -76
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +785 -76
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +5 -5
- package/dist/inspector/manifest.json +21 -8
- package/package.json +1 -1
package/dist/inspector/cli.js
CHANGED
|
@@ -5,66 +5,8 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
|
|
6
6
|
// src/inspector/cli.ts
|
|
7
7
|
|
|
8
|
-
// src/inspector/erb.ts
|
|
9
|
-
function neutralizeErb(source) {
|
|
10
|
-
return source.replace(/<%[\s\S]*?%>/g, (match) => match.replace(/[^\n]/g, " "));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// src/inspector/extract.ts
|
|
14
|
-
var NAMESPACE_PREFIX = "stimeo--";
|
|
15
|
-
function dasherize(value) {
|
|
16
|
-
return value.replace(/([A-Z])/g, (_match, char) => `-${char.toLowerCase()}`);
|
|
17
|
-
}
|
|
18
|
-
function controllerIdentifiers(dataControllerValue) {
|
|
19
|
-
const seen = /* @__PURE__ */ new Set();
|
|
20
|
-
for (const token of dataControllerValue.split(/\s+/)) {
|
|
21
|
-
if (token.startsWith(NAMESPACE_PREFIX)) seen.add(token);
|
|
22
|
-
}
|
|
23
|
-
return [...seen];
|
|
24
|
-
}
|
|
25
|
-
function parseTargetAttr(attrName) {
|
|
26
|
-
const match = /^data-(stimeo--[a-z0-9][a-z0-9-]*)-target$/.exec(attrName);
|
|
27
|
-
return match?.[1] ?? null;
|
|
28
|
-
}
|
|
29
|
-
function parseValueAttr(attrName, knownIdentifiers) {
|
|
30
|
-
const match = /^data-(stimeo--[a-z0-9-]+)-value$/.exec(attrName);
|
|
31
|
-
if (!match) return null;
|
|
32
|
-
const inner = match[1] ?? "";
|
|
33
|
-
let best = null;
|
|
34
|
-
for (const id of knownIdentifiers) {
|
|
35
|
-
if (inner.startsWith(`${id}-`)) {
|
|
36
|
-
const valueToken = inner.slice(id.length + 1);
|
|
37
|
-
if (valueToken.length > 0 && (!best || id.length > best.identifier.length)) {
|
|
38
|
-
best = { identifier: id, valueToken };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (best) return best;
|
|
43
|
-
const rest = inner.slice(NAMESPACE_PREFIX.length);
|
|
44
|
-
const dash = rest.indexOf("-");
|
|
45
|
-
const identifierGuess = dash === -1 ? inner : `${NAMESPACE_PREFIX}${rest.slice(0, dash)}`;
|
|
46
|
-
const valueGuess = dash === -1 ? "" : rest.slice(dash + 1);
|
|
47
|
-
return { identifier: null, valueToken: valueGuess || identifierGuess };
|
|
48
|
-
}
|
|
49
|
-
function actionDescriptors(dataActionValue) {
|
|
50
|
-
const descriptors = [];
|
|
51
|
-
for (const descriptor of dataActionValue.split(/\s+/)) {
|
|
52
|
-
const hash = descriptor.indexOf("#");
|
|
53
|
-
if (hash === -1) continue;
|
|
54
|
-
let lhs = descriptor.slice(0, hash);
|
|
55
|
-
const arrow = lhs.indexOf("->");
|
|
56
|
-
if (arrow !== -1) lhs = lhs.slice(arrow + 2);
|
|
57
|
-
if (!lhs.startsWith(NAMESPACE_PREFIX)) continue;
|
|
58
|
-
const method = /^[a-zA-Z_$][\w$]*/.exec(descriptor.slice(hash + 1))?.[0] ?? "";
|
|
59
|
-
descriptors.push({ identifier: lhs, method });
|
|
60
|
-
}
|
|
61
|
-
return descriptors;
|
|
62
|
-
}
|
|
63
|
-
function isStimeoDataAttr(attrName) {
|
|
64
|
-
return attrName.startsWith(`data-${NAMESPACE_PREFIX}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
8
|
// src/inspector/html_parser.ts
|
|
9
|
+
var ERB_ELEMENT_TAG = "#erb";
|
|
68
10
|
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
69
11
|
"area",
|
|
70
12
|
"base",
|
|
@@ -101,7 +43,7 @@ var PositionIndex = class {
|
|
|
101
43
|
return { line: lo + 1, column: offset - (this.#lineStarts[lo] ?? 0) + 1 };
|
|
102
44
|
}
|
|
103
45
|
};
|
|
104
|
-
function parseHtml(source) {
|
|
46
|
+
function parseHtml(source, synthetics = []) {
|
|
105
47
|
const pos = new PositionIndex(source);
|
|
106
48
|
const len = source.length;
|
|
107
49
|
const ch = (k) => k >= 0 && k < len ? source[k] : "";
|
|
@@ -111,12 +53,57 @@ function parseHtml(source) {
|
|
|
111
53
|
children: [],
|
|
112
54
|
parent: null,
|
|
113
55
|
line: 1,
|
|
114
|
-
column: 1
|
|
56
|
+
column: 1,
|
|
57
|
+
origin: "markup",
|
|
58
|
+
opaque: false
|
|
115
59
|
};
|
|
116
60
|
const stack = [root];
|
|
117
61
|
let i = 0;
|
|
118
62
|
const top = () => stack[stack.length - 1];
|
|
63
|
+
let pending = 0;
|
|
64
|
+
const closesAt = /* @__PURE__ */ new Map();
|
|
65
|
+
const takeSynthetics = (offset, splice) => {
|
|
66
|
+
while (pending < synthetics.length) {
|
|
67
|
+
const synthetic = synthetics[pending];
|
|
68
|
+
if (synthetic.start > offset) return;
|
|
69
|
+
pending++;
|
|
70
|
+
if (!splice) continue;
|
|
71
|
+
const at = pos.at(synthetic.start);
|
|
72
|
+
const node = {
|
|
73
|
+
tag: synthetic.tag,
|
|
74
|
+
attrs: synthetic.attrs,
|
|
75
|
+
children: [],
|
|
76
|
+
parent: top(),
|
|
77
|
+
line: at.line,
|
|
78
|
+
column: at.column,
|
|
79
|
+
sourceLength: synthetic.sourceLength,
|
|
80
|
+
origin: "erb",
|
|
81
|
+
opaque: synthetic.opaque
|
|
82
|
+
};
|
|
83
|
+
top().children.push(node);
|
|
84
|
+
if (synthetic.container) {
|
|
85
|
+
closesAt.set(node, synthetic.end);
|
|
86
|
+
stack.push(node);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const closeSynthetics = (offset) => {
|
|
91
|
+
for (; ; ) {
|
|
92
|
+
let expired = -1;
|
|
93
|
+
for (let depth = stack.length - 1; depth >= 1; depth--) {
|
|
94
|
+
const end = closesAt.get(stack[depth]);
|
|
95
|
+
if (end !== void 0 && end <= offset) {
|
|
96
|
+
expired = depth;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (expired < 0) return;
|
|
101
|
+
stack.length = expired;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
119
104
|
while (i < len) {
|
|
105
|
+
closeSynthetics(i);
|
|
106
|
+
takeSynthetics(i, true);
|
|
120
107
|
if (ch(i) !== "<") {
|
|
121
108
|
i++;
|
|
122
109
|
continue;
|
|
@@ -125,11 +112,13 @@ function parseHtml(source) {
|
|
|
125
112
|
if (source.startsWith("<!--", i)) {
|
|
126
113
|
const end = source.indexOf("-->", i + 4);
|
|
127
114
|
i = end === -1 ? len : end + 3;
|
|
115
|
+
takeSynthetics(i - 1, false);
|
|
128
116
|
continue;
|
|
129
117
|
}
|
|
130
118
|
if (next === "!" || next === "?") {
|
|
131
119
|
const end = source.indexOf(">", i);
|
|
132
120
|
i = end === -1 ? len : end + 1;
|
|
121
|
+
takeSynthetics(i - 1, false);
|
|
133
122
|
continue;
|
|
134
123
|
}
|
|
135
124
|
if (next === "/") {
|
|
@@ -141,6 +130,7 @@ function parseHtml(source) {
|
|
|
141
130
|
}
|
|
142
131
|
const end = source.indexOf(">", j2);
|
|
143
132
|
i = end === -1 ? len : end + 1;
|
|
133
|
+
takeSynthetics(i - 1, false);
|
|
144
134
|
closeTag(stack, name.toLowerCase());
|
|
145
135
|
continue;
|
|
146
136
|
}
|
|
@@ -221,10 +211,13 @@ function parseHtml(source) {
|
|
|
221
211
|
children: [],
|
|
222
212
|
parent: top(),
|
|
223
213
|
line: startPos.line,
|
|
224
|
-
column: startPos.column
|
|
214
|
+
column: startPos.column,
|
|
215
|
+
origin: "markup",
|
|
216
|
+
opaque: false
|
|
225
217
|
};
|
|
226
218
|
top().children.push(node);
|
|
227
219
|
i = j;
|
|
220
|
+
takeSynthetics(i - 1, false);
|
|
228
221
|
if (selfClosing || VOID_ELEMENTS.has(lowerTag)) {
|
|
229
222
|
continue;
|
|
230
223
|
}
|
|
@@ -237,10 +230,13 @@ function parseHtml(source) {
|
|
|
237
230
|
const gt = source.indexOf(">", idx);
|
|
238
231
|
i = gt === -1 ? len : gt + 1;
|
|
239
232
|
}
|
|
233
|
+
takeSynthetics(i - 1, false);
|
|
240
234
|
continue;
|
|
241
235
|
}
|
|
242
236
|
stack.push(node);
|
|
243
237
|
}
|
|
238
|
+
closeSynthetics(len);
|
|
239
|
+
takeSynthetics(len, true);
|
|
244
240
|
return root;
|
|
245
241
|
}
|
|
246
242
|
function closeTag(stack, name) {
|
|
@@ -258,6 +254,673 @@ function walk(root, visit) {
|
|
|
258
254
|
}
|
|
259
255
|
}
|
|
260
256
|
|
|
257
|
+
// src/inspector/ruby_scan.ts
|
|
258
|
+
var SPACE = /* @__PURE__ */ new Set([" ", " ", "\n", "\r", "\f"]);
|
|
259
|
+
var IDENT_START = /[A-Za-z_]/;
|
|
260
|
+
var IDENT_CHAR = /[A-Za-z0-9_]/;
|
|
261
|
+
var ESCAPES = {
|
|
262
|
+
n: "\n",
|
|
263
|
+
t: " ",
|
|
264
|
+
r: "\r",
|
|
265
|
+
f: "\f",
|
|
266
|
+
v: "\v",
|
|
267
|
+
b: "\b",
|
|
268
|
+
a: "\x07",
|
|
269
|
+
e: "\x1B",
|
|
270
|
+
s: " "
|
|
271
|
+
};
|
|
272
|
+
var UNDECODED_ESCAPE = /[uxcCM0-7]/;
|
|
273
|
+
function readString(code, start) {
|
|
274
|
+
const quote = code[start];
|
|
275
|
+
const single = quote === "'";
|
|
276
|
+
const contentStart = start + 1;
|
|
277
|
+
let i = contentStart;
|
|
278
|
+
let value = "";
|
|
279
|
+
let verbatim = true;
|
|
280
|
+
while (i < code.length) {
|
|
281
|
+
const char = code[i];
|
|
282
|
+
if (char === quote) {
|
|
283
|
+
return { end: i + 1, value, literal: true, verbatim, contentStart, contentEnd: i };
|
|
284
|
+
}
|
|
285
|
+
if (char === "\\") {
|
|
286
|
+
const escaped = code[i + 1];
|
|
287
|
+
if (escaped === void 0) break;
|
|
288
|
+
verbatim = false;
|
|
289
|
+
if (single) {
|
|
290
|
+
value += escaped === "'" || escaped === "\\" ? escaped : `\\${escaped}`;
|
|
291
|
+
} else if (UNDECODED_ESCAPE.test(escaped)) {
|
|
292
|
+
return {
|
|
293
|
+
end: closingQuote(code, i + 2, quote),
|
|
294
|
+
value,
|
|
295
|
+
literal: false,
|
|
296
|
+
verbatim: false,
|
|
297
|
+
contentStart,
|
|
298
|
+
contentEnd: i
|
|
299
|
+
};
|
|
300
|
+
} else if (escaped !== "\n") {
|
|
301
|
+
value += ESCAPES[escaped] ?? escaped;
|
|
302
|
+
}
|
|
303
|
+
i += 2;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (!single && char === "#" && code[i + 1] === "{") {
|
|
307
|
+
const close = skipBraces(code, i + 1);
|
|
308
|
+
return {
|
|
309
|
+
end: closingQuote(code, close, quote),
|
|
310
|
+
value,
|
|
311
|
+
literal: false,
|
|
312
|
+
verbatim: false,
|
|
313
|
+
contentStart,
|
|
314
|
+
contentEnd: i
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
value += char;
|
|
318
|
+
i++;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
end: code.length,
|
|
322
|
+
value,
|
|
323
|
+
literal: false,
|
|
324
|
+
verbatim: false,
|
|
325
|
+
contentStart,
|
|
326
|
+
contentEnd: code.length
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function closingQuote(code, from, quote) {
|
|
330
|
+
for (let i = from; i < code.length; i++) {
|
|
331
|
+
if (code[i] === "\\") {
|
|
332
|
+
i++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (code[i] === quote) return i + 1;
|
|
336
|
+
}
|
|
337
|
+
return code.length;
|
|
338
|
+
}
|
|
339
|
+
function skipBraces(code, open) {
|
|
340
|
+
let depth = 0;
|
|
341
|
+
let i = open;
|
|
342
|
+
while (i < code.length) {
|
|
343
|
+
const char = code[i];
|
|
344
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
345
|
+
i = readString(code, i).end;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (char === "{") depth++;
|
|
349
|
+
else if (char === "}") {
|
|
350
|
+
depth--;
|
|
351
|
+
if (depth === 0) return i + 1;
|
|
352
|
+
}
|
|
353
|
+
i++;
|
|
354
|
+
}
|
|
355
|
+
return code.length;
|
|
356
|
+
}
|
|
357
|
+
function endOfLine(code, at) {
|
|
358
|
+
const newline = code.indexOf("\n", at);
|
|
359
|
+
return newline === -1 ? code.length : newline + 1;
|
|
360
|
+
}
|
|
361
|
+
function skipTrivia(code, at) {
|
|
362
|
+
let i = at;
|
|
363
|
+
while (i < code.length) {
|
|
364
|
+
const char = code[i];
|
|
365
|
+
if (SPACE.has(char)) i++;
|
|
366
|
+
else if (char === "#") i = endOfLine(code, i);
|
|
367
|
+
else break;
|
|
368
|
+
}
|
|
369
|
+
return i;
|
|
370
|
+
}
|
|
371
|
+
function previousSignificant(code, at) {
|
|
372
|
+
let i = at - 1;
|
|
373
|
+
while (i >= 0 && (code[i] === " " || code[i] === " ")) i--;
|
|
374
|
+
return i >= 0 ? code[i] : "";
|
|
375
|
+
}
|
|
376
|
+
var EXPRESSION_LEAD = /* @__PURE__ */ new Set([
|
|
377
|
+
"",
|
|
378
|
+
"\n",
|
|
379
|
+
"\r",
|
|
380
|
+
";",
|
|
381
|
+
"(",
|
|
382
|
+
"[",
|
|
383
|
+
"{",
|
|
384
|
+
",",
|
|
385
|
+
":",
|
|
386
|
+
"=",
|
|
387
|
+
"<",
|
|
388
|
+
">",
|
|
389
|
+
"!",
|
|
390
|
+
"&",
|
|
391
|
+
"|",
|
|
392
|
+
"^",
|
|
393
|
+
"~",
|
|
394
|
+
"+",
|
|
395
|
+
"-",
|
|
396
|
+
"*",
|
|
397
|
+
"/",
|
|
398
|
+
"%",
|
|
399
|
+
"?"
|
|
400
|
+
]);
|
|
401
|
+
function opensExpression(code, at) {
|
|
402
|
+
return EXPRESSION_LEAD.has(previousSignificant(code, at));
|
|
403
|
+
}
|
|
404
|
+
var PERCENT_TYPES = "qQwWiIrsx";
|
|
405
|
+
var DELIMITER_PAIRS = { "(": ")", "[": "]", "{": "}", "<": ">" };
|
|
406
|
+
function skipLiteral(code, at) {
|
|
407
|
+
const char = code[at];
|
|
408
|
+
if (char === void 0) return -1;
|
|
409
|
+
if (char === "%") {
|
|
410
|
+
const typed = PERCENT_TYPES.includes(code[at + 1] ?? "");
|
|
411
|
+
const open = typed ? code[at + 2] : code[at + 1];
|
|
412
|
+
if (open === void 0 || IDENT_CHAR.test(open) || SPACE.has(open)) return -1;
|
|
413
|
+
if (!typed && !opensExpression(code, at) && !SPACE.has(code[at - 1] ?? "")) return -1;
|
|
414
|
+
const end = closeDelimited(code, typed ? at + 3 : at + 2, open);
|
|
415
|
+
return end < 0 ? code.length : end;
|
|
416
|
+
}
|
|
417
|
+
if (char === "/" && opensExpression(code, at)) {
|
|
418
|
+
const end = closeDelimited(code, at + 1, "/");
|
|
419
|
+
if (end < 0) return -1;
|
|
420
|
+
let i = end;
|
|
421
|
+
while (i < code.length && /[imxounse]/.test(code[i])) i++;
|
|
422
|
+
return i;
|
|
423
|
+
}
|
|
424
|
+
if (char === "?" && opensExpression(code, at)) {
|
|
425
|
+
const next = code[at + 1];
|
|
426
|
+
if (next === void 0 || SPACE.has(next)) return -1;
|
|
427
|
+
if (next === "\\") return at + 3;
|
|
428
|
+
if (IDENT_CHAR.test(next) && IDENT_CHAR.test(code[at + 2] ?? "")) return -1;
|
|
429
|
+
return at + 2;
|
|
430
|
+
}
|
|
431
|
+
return -1;
|
|
432
|
+
}
|
|
433
|
+
function closeDelimited(code, from, open) {
|
|
434
|
+
const close = DELIMITER_PAIRS[open] ?? open;
|
|
435
|
+
const nests = close !== open;
|
|
436
|
+
let depth = 1;
|
|
437
|
+
for (let i = from; i < code.length; i++) {
|
|
438
|
+
const char = code[i];
|
|
439
|
+
if (char === "\\") {
|
|
440
|
+
i++;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (nests && char === open) depth++;
|
|
444
|
+
else if (char === close) {
|
|
445
|
+
depth--;
|
|
446
|
+
if (depth === 0) return i + 1;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return -1;
|
|
450
|
+
}
|
|
451
|
+
var HEREDOC_OPEN = /^<<([-~])?(["'`])?([A-Za-z_]\w*)/;
|
|
452
|
+
function maskHeredocs(code) {
|
|
453
|
+
let masked = code;
|
|
454
|
+
let i = 0;
|
|
455
|
+
while (i < masked.length) {
|
|
456
|
+
const char = masked[i];
|
|
457
|
+
if (char === "#") {
|
|
458
|
+
i = endOfLine(masked, i);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
462
|
+
i = readString(masked, i).end;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const literal = skipLiteral(masked, i);
|
|
466
|
+
if (literal > i) {
|
|
467
|
+
i = literal;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (char === "<" && masked[i + 1] === "<") {
|
|
471
|
+
const [matched, dash, quote, identifier] = HEREDOC_OPEN.exec(masked.slice(i)) ?? [];
|
|
472
|
+
const opensBody = identifier !== void 0 && (dash !== void 0 || quote !== void 0 || /^[A-Z]/.test(identifier) && opensExpression(masked, i));
|
|
473
|
+
const consumed = (matched?.length ?? 0) + (quote === void 0 ? 0 : 1);
|
|
474
|
+
if (opensBody && (quote === void 0 || masked[i + consumed - 1] === quote)) {
|
|
475
|
+
const bodyStart = endOfLine(masked, i);
|
|
476
|
+
const bodyEnd = heredocEnd(masked, bodyStart, identifier);
|
|
477
|
+
masked = masked.slice(0, bodyStart) + masked.slice(bodyStart, bodyEnd).replace(/[^\n]/g, " ") + masked.slice(bodyEnd);
|
|
478
|
+
i += consumed;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
i++;
|
|
483
|
+
}
|
|
484
|
+
return masked;
|
|
485
|
+
}
|
|
486
|
+
function heredocEnd(code, bodyStart, identifier) {
|
|
487
|
+
let line = bodyStart;
|
|
488
|
+
while (line < code.length) {
|
|
489
|
+
const next = endOfLine(code, line);
|
|
490
|
+
if (code.slice(line, next).trim() === identifier) return next;
|
|
491
|
+
line = next;
|
|
492
|
+
}
|
|
493
|
+
return code.length;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/inspector/ruby_hash.ts
|
|
497
|
+
function readDataOption(source, base) {
|
|
498
|
+
const code = maskHeredocs(source);
|
|
499
|
+
const attrs = [];
|
|
500
|
+
let found = false;
|
|
501
|
+
let opaque = false;
|
|
502
|
+
let i = 0;
|
|
503
|
+
while (i < code.length) {
|
|
504
|
+
const char = code[i];
|
|
505
|
+
if (char === "#") {
|
|
506
|
+
i = endOfLine(code, i);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
510
|
+
const literal = readString(code, i);
|
|
511
|
+
const value = char === "`" ? null : keySeparator(code, literal.end);
|
|
512
|
+
if (literal.literal && literal.value === "data" && value !== null) {
|
|
513
|
+
i = value;
|
|
514
|
+
} else {
|
|
515
|
+
i = literal.end;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
} else if (char === ":" && IDENT_START.test(code[i + 1] ?? "")) {
|
|
519
|
+
const name = readIdentifier(code, i + 1);
|
|
520
|
+
const value = hashRocket(code, name.end);
|
|
521
|
+
if (name.text === "data" && value !== null) {
|
|
522
|
+
i = value;
|
|
523
|
+
} else {
|
|
524
|
+
i = name.end;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
} else if (IDENT_START.test(char) && !IDENT_CHAR.test(code[i - 1] ?? "")) {
|
|
528
|
+
const name = readIdentifier(code, i);
|
|
529
|
+
const keyed = name.text === "data" && code[name.end] === ":" && code[name.end + 1] !== ":";
|
|
530
|
+
if (keyed) {
|
|
531
|
+
i = name.end + 1;
|
|
532
|
+
} else {
|
|
533
|
+
i = name.end;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
const literal = skipLiteral(code, i);
|
|
538
|
+
i = literal < 0 ? i + 1 : literal;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
found = true;
|
|
542
|
+
const start = skipTrivia(code, i);
|
|
543
|
+
if (code[start] === "{") {
|
|
544
|
+
const hash = readHash(code, start, base);
|
|
545
|
+
attrs.push(...hash.attrs);
|
|
546
|
+
if (hash.partial || !optionEnds(code, hash.end)) opaque = true;
|
|
547
|
+
i = hash.end;
|
|
548
|
+
} else {
|
|
549
|
+
opaque = true;
|
|
550
|
+
i = start;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { attrs, found, opaque };
|
|
554
|
+
}
|
|
555
|
+
var OPTION_TAIL = /^(?:do|if|unless|while|until|rescue|then|and|or|end)\b/;
|
|
556
|
+
function optionEnds(code, at) {
|
|
557
|
+
const i = skipTrivia(code, at);
|
|
558
|
+
if (i >= code.length) return true;
|
|
559
|
+
const char = code[i];
|
|
560
|
+
if (char === "," || char === "}" || char === ")" || char === "]" || char === ";") return true;
|
|
561
|
+
return OPTION_TAIL.test(code.slice(i));
|
|
562
|
+
}
|
|
563
|
+
function readHash(code, open, base) {
|
|
564
|
+
const attrs = [];
|
|
565
|
+
let partial = false;
|
|
566
|
+
let i = open + 1;
|
|
567
|
+
for (; ; ) {
|
|
568
|
+
i = skipTrivia(code, i);
|
|
569
|
+
if (i >= code.length) {
|
|
570
|
+
partial = true;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
if (code[i] === "}") {
|
|
574
|
+
i++;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
if (code[i] === ",") {
|
|
578
|
+
i++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const entry = readEntry(code, i, base);
|
|
582
|
+
if (entry.attr) attrs.push(entry.attr);
|
|
583
|
+
else partial = true;
|
|
584
|
+
i = entry.end;
|
|
585
|
+
}
|
|
586
|
+
return { attrs, partial, end: i };
|
|
587
|
+
}
|
|
588
|
+
function readEntry(code, start, base) {
|
|
589
|
+
const key = readKey(code, start);
|
|
590
|
+
if (key === null) return { attr: null, end: skipValue(code, start) };
|
|
591
|
+
const valueStart = skipTrivia(code, key.valueAt);
|
|
592
|
+
const name = attributeName(key.name);
|
|
593
|
+
if (name === null) return { attr: null, end: skipValue(code, valueStart) };
|
|
594
|
+
const anchor = { keyStart: base + start, keyEnd: base + key.end };
|
|
595
|
+
const quote = code[valueStart];
|
|
596
|
+
if (quote === '"' || quote === "'") {
|
|
597
|
+
const literal = readString(code, valueStart);
|
|
598
|
+
const after = skipTrivia(code, literal.end);
|
|
599
|
+
const whole = after >= code.length || code[after] === "," || code[after] === "}";
|
|
600
|
+
if (literal.literal && whole) {
|
|
601
|
+
return {
|
|
602
|
+
attr: {
|
|
603
|
+
name,
|
|
604
|
+
value: literal.value,
|
|
605
|
+
...anchor,
|
|
606
|
+
valueStart: literal.verbatim ? base + literal.contentStart : void 0,
|
|
607
|
+
valueEnd: literal.verbatim ? base + literal.contentEnd : void 0
|
|
608
|
+
},
|
|
609
|
+
end: literal.end
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return { attr: { name, value: null, ...anchor }, end: skipValue(code, valueStart) };
|
|
614
|
+
}
|
|
615
|
+
function readKey(code, start) {
|
|
616
|
+
const char = code[start];
|
|
617
|
+
if (char === '"' || char === "'") {
|
|
618
|
+
const literal = readString(code, start);
|
|
619
|
+
const valueAt = keySeparator(code, literal.end);
|
|
620
|
+
if (!literal.literal || valueAt === null) return null;
|
|
621
|
+
return { name: literal.value, end: literal.end, valueAt };
|
|
622
|
+
}
|
|
623
|
+
if (char === ":" && IDENT_START.test(code[start + 1] ?? "")) {
|
|
624
|
+
const name = readIdentifier(code, start + 1);
|
|
625
|
+
const valueAt = hashRocket(code, name.end);
|
|
626
|
+
return valueAt === null ? null : { name: name.text, end: name.end, valueAt };
|
|
627
|
+
}
|
|
628
|
+
if (char !== void 0 && IDENT_START.test(char)) {
|
|
629
|
+
const name = readIdentifier(code, start);
|
|
630
|
+
if (code[name.end] !== ":" || code[name.end + 1] === ":") return null;
|
|
631
|
+
return { name: name.text, end: name.end, valueAt: name.end + 1 };
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
function attributeName(key) {
|
|
636
|
+
const name = key.replaceAll("_", "-").toLowerCase();
|
|
637
|
+
return name.length > 0 && !/[\s"'=<>/]/.test(name) ? `data-${name}` : null;
|
|
638
|
+
}
|
|
639
|
+
function keySeparator(code, at) {
|
|
640
|
+
if (code[at] === ":" && code[at + 1] !== ":") return at + 1;
|
|
641
|
+
return hashRocket(code, at);
|
|
642
|
+
}
|
|
643
|
+
function hashRocket(code, at) {
|
|
644
|
+
const arrow = skipTrivia(code, at);
|
|
645
|
+
return code.startsWith("=>", arrow) ? arrow + 2 : null;
|
|
646
|
+
}
|
|
647
|
+
function readIdentifier(code, start) {
|
|
648
|
+
let end = start;
|
|
649
|
+
while (end < code.length && IDENT_CHAR.test(code[end])) end++;
|
|
650
|
+
return { text: code.slice(start, end), end };
|
|
651
|
+
}
|
|
652
|
+
function skipValue(code, start) {
|
|
653
|
+
let depth = 0;
|
|
654
|
+
let i = start;
|
|
655
|
+
while (i < code.length) {
|
|
656
|
+
const char = code[i];
|
|
657
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
658
|
+
i = readString(code, i).end;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (char === "#") {
|
|
662
|
+
i = endOfLine(code, i);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const literal = skipLiteral(code, i);
|
|
666
|
+
if (literal >= 0) {
|
|
667
|
+
i = literal;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (char === "(" || char === "[" || char === "{") depth++;
|
|
671
|
+
else if (char === ")" || char === "]") depth--;
|
|
672
|
+
else if (char === "}") {
|
|
673
|
+
if (depth === 0) return i;
|
|
674
|
+
depth--;
|
|
675
|
+
} else if (char === "," && depth === 0) return i;
|
|
676
|
+
i++;
|
|
677
|
+
}
|
|
678
|
+
return code.length;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/inspector/erb.ts
|
|
682
|
+
var ERB_TAG = /<%[\s\S]*?%>/g;
|
|
683
|
+
function neutralizeErb(source) {
|
|
684
|
+
return source.replace(ERB_TAG, (match) => match.replace(/[^\n]/g, " "));
|
|
685
|
+
}
|
|
686
|
+
function erbRanges(source) {
|
|
687
|
+
return [...source.matchAll(ERB_TAG)].map(
|
|
688
|
+
(match) => [match.index ?? 0, (match.index ?? 0) + match[0].length]
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
function erbElements(source) {
|
|
692
|
+
const tags = readErbTags(source);
|
|
693
|
+
const blockEnds = matchBlocks(tags);
|
|
694
|
+
const index = new PositionIndex(source);
|
|
695
|
+
const elements = [];
|
|
696
|
+
for (const [ordinal, tag] of tags.entries()) {
|
|
697
|
+
if (!tag.output) continue;
|
|
698
|
+
const option = readDataOption(tag.code, tag.codeStart);
|
|
699
|
+
if (!option.found) continue;
|
|
700
|
+
const end = blockEnds.get(ordinal) ?? tag.end;
|
|
701
|
+
elements.push({
|
|
702
|
+
tag: ERB_ELEMENT_TAG,
|
|
703
|
+
attrs: option.attrs.map((attr) => toParsedAttr(attr, index)),
|
|
704
|
+
start: tag.start,
|
|
705
|
+
// The token a reader can act on is the delimiter that opens the call.
|
|
706
|
+
sourceLength: tag.codeStart - tag.start,
|
|
707
|
+
end,
|
|
708
|
+
container: end > tag.end,
|
|
709
|
+
opaque: option.opaque
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
return elements;
|
|
713
|
+
}
|
|
714
|
+
function toParsedAttr(attr, index) {
|
|
715
|
+
const at = index.at(attr.keyStart);
|
|
716
|
+
return {
|
|
717
|
+
name: attr.name,
|
|
718
|
+
value: attr.value ?? "",
|
|
719
|
+
line: at.line,
|
|
720
|
+
column: at.column,
|
|
721
|
+
sourceLength: attr.keyEnd - attr.keyStart,
|
|
722
|
+
valueStart: attr.valueStart,
|
|
723
|
+
valueEnd: attr.valueEnd,
|
|
724
|
+
dynamicValue: attr.value === null
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function readErbTags(source) {
|
|
728
|
+
const tags = [];
|
|
729
|
+
for (const match of source.matchAll(ERB_TAG)) {
|
|
730
|
+
const raw = match[0];
|
|
731
|
+
const start = match.index ?? 0;
|
|
732
|
+
let open = 2;
|
|
733
|
+
let output = false;
|
|
734
|
+
if (raw[open] === "#") continue;
|
|
735
|
+
if (raw[open] === "=") {
|
|
736
|
+
output = true;
|
|
737
|
+
open++;
|
|
738
|
+
if (raw[open] === "=") open++;
|
|
739
|
+
} else if (raw[open] === "-") {
|
|
740
|
+
open++;
|
|
741
|
+
}
|
|
742
|
+
let close = raw.length - 2;
|
|
743
|
+
if (raw[close - 1] === "-") close--;
|
|
744
|
+
if (close < open) close = open;
|
|
745
|
+
tags.push({
|
|
746
|
+
start,
|
|
747
|
+
end: start + raw.length,
|
|
748
|
+
output,
|
|
749
|
+
code: raw.slice(open, close),
|
|
750
|
+
codeStart: start + open
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
return tags;
|
|
754
|
+
}
|
|
755
|
+
function matchBlocks(tags) {
|
|
756
|
+
const ends = /* @__PURE__ */ new Map();
|
|
757
|
+
const open = [];
|
|
758
|
+
for (const [ordinal, tag] of tags.entries()) {
|
|
759
|
+
for (const event of blockEvents(tag.code)) {
|
|
760
|
+
if (event === "open") {
|
|
761
|
+
open.push(ordinal);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const opener = open.pop();
|
|
765
|
+
if (opener !== void 0) ends.set(opener, tag.end);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return ends;
|
|
769
|
+
}
|
|
770
|
+
var BLOCK_OPENERS = /* @__PURE__ */ new Set(["case", "begin", "def", "class", "module", "for"]);
|
|
771
|
+
var BLOCK_MODIFIERS = /* @__PURE__ */ new Set(["if", "unless", "while", "until"]);
|
|
772
|
+
var BLOCK_CONTINUERS = /* @__PURE__ */ new Set(["elsif", "else", "when", "in", "then", "rescue", "ensure"]);
|
|
773
|
+
function blockEvents(source) {
|
|
774
|
+
const code = maskHeredocs(source);
|
|
775
|
+
const events = [];
|
|
776
|
+
let previous = "";
|
|
777
|
+
let loopHeader = false;
|
|
778
|
+
let i = 0;
|
|
779
|
+
while (i < code.length) {
|
|
780
|
+
const char = code[i];
|
|
781
|
+
if (char === "\n" || char === ";") {
|
|
782
|
+
previous = char;
|
|
783
|
+
loopHeader = false;
|
|
784
|
+
i++;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (SPACE.has(char)) {
|
|
788
|
+
i++;
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
if (char === "#") {
|
|
792
|
+
i = endOfLine(code, i);
|
|
793
|
+
previous = "\n";
|
|
794
|
+
loopHeader = false;
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
798
|
+
i = readString(code, i).end;
|
|
799
|
+
previous = "value";
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (char === ":" && IDENT_START.test(code[i + 1] ?? "")) {
|
|
803
|
+
i = identifierEnd(code, i + 1);
|
|
804
|
+
previous = "value";
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const literal = skipLiteral(code, i);
|
|
808
|
+
if (literal >= 0) {
|
|
809
|
+
i = literal;
|
|
810
|
+
previous = "value";
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (char === "{") {
|
|
814
|
+
events.push("open");
|
|
815
|
+
previous = "{";
|
|
816
|
+
i++;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (char === "}") {
|
|
820
|
+
events.push("close");
|
|
821
|
+
previous = "value";
|
|
822
|
+
i++;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (char === ")" || char === "]") {
|
|
826
|
+
previous = "value";
|
|
827
|
+
i++;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if (!IDENT_START.test(char)) {
|
|
831
|
+
previous = char;
|
|
832
|
+
i++;
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const end = identifierEnd(code, i);
|
|
836
|
+
const word = code.slice(i, end);
|
|
837
|
+
const keyword = !(code[end] === ":" && code[end + 1] !== ":") && code[i - 1] !== ".";
|
|
838
|
+
i = end;
|
|
839
|
+
if (keyword && word === "end") {
|
|
840
|
+
events.push("close");
|
|
841
|
+
previous = "value";
|
|
842
|
+
} else if (keyword && word === "do") {
|
|
843
|
+
if (loopHeader) loopHeader = false;
|
|
844
|
+
else events.push("open");
|
|
845
|
+
previous = "do";
|
|
846
|
+
} else if (keyword && BLOCK_OPENERS.has(word)) {
|
|
847
|
+
events.push("open");
|
|
848
|
+
loopHeader = word === "for";
|
|
849
|
+
previous = word;
|
|
850
|
+
} else if (keyword && BLOCK_MODIFIERS.has(word)) {
|
|
851
|
+
if (previous !== "value") {
|
|
852
|
+
events.push("open");
|
|
853
|
+
loopHeader = word !== "if" && word !== "unless";
|
|
854
|
+
}
|
|
855
|
+
previous = word;
|
|
856
|
+
} else if (keyword && BLOCK_CONTINUERS.has(word)) {
|
|
857
|
+
previous = word;
|
|
858
|
+
} else {
|
|
859
|
+
previous = "value";
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return events;
|
|
863
|
+
}
|
|
864
|
+
function identifierEnd(code, start) {
|
|
865
|
+
let end = start;
|
|
866
|
+
while (end < code.length && IDENT_CHAR.test(code[end])) end++;
|
|
867
|
+
return end;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/inspector/extract.ts
|
|
871
|
+
var NAMESPACE_PREFIX = "stimeo--";
|
|
872
|
+
function dasherize(value) {
|
|
873
|
+
return value.replace(/([A-Z])/g, (_match, char) => `-${char.toLowerCase()}`);
|
|
874
|
+
}
|
|
875
|
+
function controllerIdentifiers(dataControllerValue) {
|
|
876
|
+
const seen = /* @__PURE__ */ new Set();
|
|
877
|
+
for (const token of dataControllerValue.split(/\s+/)) {
|
|
878
|
+
if (token.startsWith(NAMESPACE_PREFIX)) seen.add(token);
|
|
879
|
+
}
|
|
880
|
+
return [...seen];
|
|
881
|
+
}
|
|
882
|
+
function parseTargetAttr(attrName) {
|
|
883
|
+
const match = /^data-(stimeo--[a-z0-9][a-z0-9-]*)-target$/.exec(attrName);
|
|
884
|
+
return match?.[1] ?? null;
|
|
885
|
+
}
|
|
886
|
+
function parseValueAttr(attrName, knownIdentifiers) {
|
|
887
|
+
const match = /^data-(stimeo--[a-z0-9-]+)-value$/.exec(attrName);
|
|
888
|
+
if (!match) return null;
|
|
889
|
+
const inner = match[1] ?? "";
|
|
890
|
+
let best = null;
|
|
891
|
+
for (const id of knownIdentifiers) {
|
|
892
|
+
if (inner.startsWith(`${id}-`)) {
|
|
893
|
+
const valueToken = inner.slice(id.length + 1);
|
|
894
|
+
if (valueToken.length > 0 && (!best || id.length > best.identifier.length)) {
|
|
895
|
+
best = { identifier: id, valueToken };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (best) return best;
|
|
900
|
+
const rest = inner.slice(NAMESPACE_PREFIX.length);
|
|
901
|
+
const dash = rest.indexOf("-");
|
|
902
|
+
const identifierGuess = dash === -1 ? inner : `${NAMESPACE_PREFIX}${rest.slice(0, dash)}`;
|
|
903
|
+
const valueGuess = dash === -1 ? "" : rest.slice(dash + 1);
|
|
904
|
+
return { identifier: null, valueToken: valueGuess || identifierGuess };
|
|
905
|
+
}
|
|
906
|
+
function actionDescriptors(dataActionValue) {
|
|
907
|
+
const descriptors = [];
|
|
908
|
+
for (const descriptor of dataActionValue.split(/\s+/)) {
|
|
909
|
+
const hash = descriptor.indexOf("#");
|
|
910
|
+
if (hash === -1) continue;
|
|
911
|
+
let lhs = descriptor.slice(0, hash);
|
|
912
|
+
const arrow = lhs.indexOf("->");
|
|
913
|
+
if (arrow !== -1) lhs = lhs.slice(arrow + 2);
|
|
914
|
+
if (!lhs.startsWith(NAMESPACE_PREFIX)) continue;
|
|
915
|
+
const method = /^[a-zA-Z_$][\w$]*/.exec(descriptor.slice(hash + 1))?.[0] ?? "";
|
|
916
|
+
descriptors.push({ identifier: lhs, method });
|
|
917
|
+
}
|
|
918
|
+
return descriptors;
|
|
919
|
+
}
|
|
920
|
+
function isStimeoDataAttr(attrName) {
|
|
921
|
+
return attrName.startsWith(`data-${NAMESPACE_PREFIX}`);
|
|
922
|
+
}
|
|
923
|
+
|
|
261
924
|
// src/inspector/types.ts
|
|
262
925
|
var DIAGNOSTIC_CODES = [
|
|
263
926
|
"unknown-controller",
|
|
@@ -284,16 +947,15 @@ var DIAGNOSTIC_CODES = [
|
|
|
284
947
|
// src/inspector/check.ts
|
|
285
948
|
function checkSource(source, manifest) {
|
|
286
949
|
const diagnostics = [];
|
|
287
|
-
const tree = parseHtml(neutralizeErb(source));
|
|
950
|
+
const tree = parseHtml(neutralizeErb(source), erbElements(source));
|
|
288
951
|
const known = manifest.controllers;
|
|
289
952
|
const knownIdentifiers = Object.keys(known);
|
|
290
|
-
const
|
|
291
|
-
(m) => [m.index ?? 0, (m.index ?? 0) + m[0].length]
|
|
292
|
-
);
|
|
953
|
+
const ranges = erbRanges(source);
|
|
293
954
|
const isDynamicValue = (attr) => {
|
|
955
|
+
if (attr.dynamicValue !== void 0) return attr.dynamicValue;
|
|
294
956
|
const { valueStart, valueEnd } = attr;
|
|
295
957
|
if (valueStart === void 0 || valueEnd === void 0) return false;
|
|
296
|
-
return
|
|
958
|
+
return ranges.some(([start, end]) => start < valueEnd && end > valueStart);
|
|
297
959
|
};
|
|
298
960
|
const conditionHolds = (scope, identifier, when) => {
|
|
299
961
|
if (!when) return true;
|
|
@@ -310,7 +972,9 @@ function checkSource(source, manifest) {
|
|
|
310
972
|
const ignore = current.attrs.find((a) => a.name === IGNORE_ATTR);
|
|
311
973
|
if (ignore) {
|
|
312
974
|
const codes = ignore.value.trim();
|
|
313
|
-
if (codes.length === 0
|
|
975
|
+
if (codes.length === 0) {
|
|
976
|
+
if (!isDynamicValue(ignore)) return true;
|
|
977
|
+
} else if (codes.split(/\s+/).includes(code)) return true;
|
|
314
978
|
}
|
|
315
979
|
current = current.parent;
|
|
316
980
|
}
|
|
@@ -319,6 +983,26 @@ function checkSource(source, manifest) {
|
|
|
319
983
|
const report = (owner, code, severity, message, at, suggestion, fix) => {
|
|
320
984
|
if (!suppressed(owner, code)) push(diagnostics, code, severity, message, at, suggestion, fix);
|
|
321
985
|
};
|
|
986
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
987
|
+
const unnamedTargets = [];
|
|
988
|
+
const targetsUnreadable = (container, scope, identifier) => {
|
|
989
|
+
for (const node of unresolved) {
|
|
990
|
+
if (isWithin(node, container) && findOwner(node, identifier) === scope) return true;
|
|
991
|
+
}
|
|
992
|
+
for (const { node, identifier: hidden } of unnamedTargets) {
|
|
993
|
+
if (hidden !== identifier) continue;
|
|
994
|
+
if (isWithin(node, container) && findOwner(node, identifier) === scope) return true;
|
|
995
|
+
}
|
|
996
|
+
return false;
|
|
997
|
+
};
|
|
998
|
+
const unresolvedAbove = (node) => {
|
|
999
|
+
let current = node;
|
|
1000
|
+
while (current && current.tag !== "#root") {
|
|
1001
|
+
if (unresolved.has(current)) return true;
|
|
1002
|
+
current = current.parent;
|
|
1003
|
+
}
|
|
1004
|
+
return false;
|
|
1005
|
+
};
|
|
322
1006
|
const scopes = [];
|
|
323
1007
|
const presence = /* @__PURE__ */ new Map();
|
|
324
1008
|
const targetNodes = /* @__PURE__ */ new Map();
|
|
@@ -373,6 +1057,10 @@ function checkSource(source, manifest) {
|
|
|
373
1057
|
}
|
|
374
1058
|
const declarationCandidates = [];
|
|
375
1059
|
walk(tree, (node) => {
|
|
1060
|
+
const declaredController = node.attrs.find((a) => a.name === "data-controller");
|
|
1061
|
+
if (node.opaque || declaredController && isDynamicValue(declaredController)) {
|
|
1062
|
+
unresolved.add(node);
|
|
1063
|
+
}
|
|
376
1064
|
if (declarationAttrs.size > 0 && node.attrs.some((a) => declarationAttrs.has(a.name))) {
|
|
377
1065
|
declarationCandidates.push(node);
|
|
378
1066
|
}
|
|
@@ -480,14 +1168,18 @@ function checkSource(source, manifest) {
|
|
|
480
1168
|
if (targetName.length > 0) {
|
|
481
1169
|
recordPresence(owner, targetIdentifier, targetName);
|
|
482
1170
|
recordTargetNode(owner, targetIdentifier, targetName, node);
|
|
1171
|
+
} else if (isDynamicValue(attr)) {
|
|
1172
|
+
unnamedTargets.push({ node, identifier: targetIdentifier });
|
|
483
1173
|
}
|
|
484
1174
|
} else if (!inDeclaredFragment(node, targetIdentifier)) {
|
|
1175
|
+
const uncertain = unresolvedAbove(node);
|
|
485
1176
|
report(
|
|
486
1177
|
node,
|
|
487
1178
|
"orphan-target",
|
|
488
|
-
"error",
|
|
1179
|
+
uncertain ? "warning" : "error",
|
|
489
1180
|
`Target "${attr.name}" has no enclosing controller "${targetIdentifier}".`,
|
|
490
|
-
attr
|
|
1181
|
+
attr,
|
|
1182
|
+
uncertain ? UNRESOLVED_HOST_HINT : void 0
|
|
491
1183
|
);
|
|
492
1184
|
}
|
|
493
1185
|
continue;
|
|
@@ -550,18 +1242,21 @@ function checkSource(source, manifest) {
|
|
|
550
1242
|
}
|
|
551
1243
|
}
|
|
552
1244
|
});
|
|
1245
|
+
const scopeIsComplete = (container, scope, identifier) => container.origin === "markup" && !targetsUnreadable(container, scope, identifier);
|
|
553
1246
|
for (const { node, identifier } of scopes) {
|
|
554
1247
|
const controller = known[identifier];
|
|
555
1248
|
if (!controller) continue;
|
|
1249
|
+
const complete = scopeIsComplete(node, node, identifier);
|
|
556
1250
|
const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
|
|
557
1251
|
for (const required of controller.requiredTargets) {
|
|
558
1252
|
if (!present.has(required)) {
|
|
559
1253
|
report(
|
|
560
1254
|
node,
|
|
561
1255
|
"missing-required-target",
|
|
562
|
-
"error",
|
|
1256
|
+
complete ? "error" : "warning",
|
|
563
1257
|
`"${identifier}" is missing required target "${required}".`,
|
|
564
|
-
node
|
|
1258
|
+
node,
|
|
1259
|
+
complete ? void 0 : UNRESOLVED_TARGET_HINT
|
|
565
1260
|
);
|
|
566
1261
|
}
|
|
567
1262
|
}
|
|
@@ -569,6 +1264,7 @@ function checkSource(source, manifest) {
|
|
|
569
1264
|
for (const { node, identifier } of scopes) {
|
|
570
1265
|
const controller = known[identifier];
|
|
571
1266
|
if (!controller) continue;
|
|
1267
|
+
const complete = scopeIsComplete(node, node, identifier);
|
|
572
1268
|
const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
|
|
573
1269
|
for (const rule of controller.conditionalTargets) {
|
|
574
1270
|
if (!present.has(rule.whenPresent)) continue;
|
|
@@ -577,10 +1273,10 @@ function checkSource(source, manifest) {
|
|
|
577
1273
|
report(
|
|
578
1274
|
node,
|
|
579
1275
|
"missing-conditional-target",
|
|
580
|
-
"error",
|
|
1276
|
+
complete ? "error" : "warning",
|
|
581
1277
|
`"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
|
|
582
1278
|
node,
|
|
583
|
-
rule.suggestion
|
|
1279
|
+
complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
|
|
584
1280
|
);
|
|
585
1281
|
}
|
|
586
1282
|
}
|
|
@@ -593,6 +1289,7 @@ function checkSource(source, manifest) {
|
|
|
593
1289
|
if (req.whenDocument && !documentHolds(req.whenDocument)) continue;
|
|
594
1290
|
const severity = documentHolds(req.escalateWhen) ? "error" : req.severity ?? "error";
|
|
595
1291
|
for (const el of elements) {
|
|
1292
|
+
if (!hasReadableMarkup(el)) continue;
|
|
596
1293
|
if (!contentHolds(node, identifier, el, req.whenContains)) continue;
|
|
597
1294
|
if (!elementHolds(el, req.whenElement)) continue;
|
|
598
1295
|
checkA11y(report, el, identifier, req, severity);
|
|
@@ -606,6 +1303,7 @@ function checkSource(source, manifest) {
|
|
|
606
1303
|
const reach = req.reach ?? "tab";
|
|
607
1304
|
const elements = targetNodes.get(node)?.get(identifier)?.get(req.target) ?? [];
|
|
608
1305
|
for (const el of elements) {
|
|
1306
|
+
if (!hasReadableMarkup(el)) continue;
|
|
609
1307
|
if (!isFocusable(el, reach)) {
|
|
610
1308
|
const problem = reach === "tab" ? 'not in the Tab order (needs a native control or tabindex="0")' : "not focusable (needs a native control or tabindex)";
|
|
611
1309
|
report(
|
|
@@ -627,6 +1325,7 @@ function checkSource(source, manifest) {
|
|
|
627
1325
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
628
1326
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
629
1327
|
for (const el of elements) {
|
|
1328
|
+
if (!hasReadableMarkup(el)) continue;
|
|
630
1329
|
for (const name of rule.attrs) {
|
|
631
1330
|
const attr = el.attrs.find((a) => a.name === name);
|
|
632
1331
|
if (!attr) continue;
|
|
@@ -649,6 +1348,7 @@ function checkSource(source, manifest) {
|
|
|
649
1348
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
650
1349
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
651
1350
|
for (const el of elements) {
|
|
1351
|
+
if (!hasReadableMarkup(el)) continue;
|
|
652
1352
|
if (!contentHolds(node, identifier, el, rule.whenContains)) continue;
|
|
653
1353
|
for (const name of rule.attrs) {
|
|
654
1354
|
const attr = el.attrs.find((a) => a.name === name);
|
|
@@ -679,6 +1379,7 @@ function checkSource(source, manifest) {
|
|
|
679
1379
|
if (conditionHolds(node, identifier, when) !== true) continue;
|
|
680
1380
|
const hostContext = when ? ` (${when.value} ${list(when.equals)})` : "";
|
|
681
1381
|
for (const el of elements) {
|
|
1382
|
+
if (el.opaque) continue;
|
|
682
1383
|
const declared = el.attrs.find((a) => a.name === "data-controller");
|
|
683
1384
|
if (!declared || !controllerIdentifiers(declared.value).includes(rule.coController)) {
|
|
684
1385
|
continue;
|
|
@@ -719,6 +1420,7 @@ function checkSource(source, manifest) {
|
|
|
719
1420
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
720
1421
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
721
1422
|
for (const el of elements) {
|
|
1423
|
+
if (el.opaque) continue;
|
|
722
1424
|
const declared = el.attrs.find((a) => a.name === "data-controller");
|
|
723
1425
|
if (declared && isDynamicValue(declared)) continue;
|
|
724
1426
|
if (declared && controllerIdentifiers(declared.value).includes(rule.controller)) continue;
|
|
@@ -807,13 +1509,14 @@ function checkSource(source, manifest) {
|
|
|
807
1509
|
}
|
|
808
1510
|
}
|
|
809
1511
|
if (rule.min !== void 0 && matched.length < rule.min) {
|
|
1512
|
+
const complete = scopeIsComplete(container, node, identifier);
|
|
810
1513
|
report(
|
|
811
1514
|
container,
|
|
812
1515
|
"cardinality-violation",
|
|
813
|
-
"error",
|
|
1516
|
+
complete ? "error" : "warning",
|
|
814
1517
|
`"${identifier}" requires at least ${rule.min} ${what} per ${where}, but found ${matched.length}.`,
|
|
815
1518
|
container,
|
|
816
|
-
rule.suggestion
|
|
1519
|
+
complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
|
|
817
1520
|
);
|
|
818
1521
|
}
|
|
819
1522
|
}
|
|
@@ -856,6 +1559,11 @@ function checkSource(source, manifest) {
|
|
|
856
1559
|
diagnostics.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
857
1560
|
return diagnostics;
|
|
858
1561
|
}
|
|
1562
|
+
var UNRESOLVED_TARGET_HINT = "Part of this scope is rendered by a helper or a non-literal data: hash, so the target may exist at runtime.";
|
|
1563
|
+
var UNRESOLVED_HOST_HINT = "An enclosing helper's data: option is not a literal hash, so the host controller may be declared at runtime.";
|
|
1564
|
+
function hasReadableMarkup(el) {
|
|
1565
|
+
return el.origin === "markup";
|
|
1566
|
+
}
|
|
859
1567
|
function checkA11y(report, el, identifier, req, severity) {
|
|
860
1568
|
const groups = [
|
|
861
1569
|
{ attrs: req.attrs, values: req.values },
|
|
@@ -984,6 +1692,7 @@ function inDeclaredFragment(node, identifier) {
|
|
|
984
1692
|
return false;
|
|
985
1693
|
}
|
|
986
1694
|
function anchorLength(at) {
|
|
1695
|
+
if (at.sourceLength !== void 0) return at.sourceLength;
|
|
987
1696
|
return "name" in at ? at.name.length : at.tag.length + 1;
|
|
988
1697
|
}
|
|
989
1698
|
function push(out, code, severity, message, at, suggestion, fix) {
|