stimeo-ui 0.3.0 → 0.5.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 +124 -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/aspect_ratio_controller.d.ts +2 -2
- package/dist/controllers/aspect_ratio_controller.js +1 -1
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +5 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js +5 -1
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +8 -3
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +4 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +4 -3
- package/dist/controllers/color_picker_controller.js +52 -2
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_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/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js +2 -2
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +24 -8
- package/dist/controllers/countdown_controller.js +117 -13
- 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 +55 -1
- 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/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +3 -3
- package/dist/controllers/direct_upload_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 +36 -11
- package/dist/controllers/empty_state_controller.js +128 -23
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +25 -5
- package/dist/controllers/flash_controller.js +161 -21
- package/dist/controllers/flash_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 +8 -2
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +31 -3
- package/dist/controllers/frame_loading_controller.js +261 -27
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +8 -4
- package/dist/controllers/highlight_controller.js +38 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +2 -1
- package/dist/controllers/idle_controller.js +13 -2
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +19 -3
- package/dist/controllers/local_time_controller.js +102 -6
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +22 -2
- package/dist/controllers/meter_controller.js +147 -26
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +25 -11
- package/dist/controllers/network_status_controller.js +29 -11
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +8 -0
- package/dist/controllers/number_input_controller.js +191 -24
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +34 -7
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js +5 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +1 -1
- package/dist/controllers/password_strength_controller.js +1 -1
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/portal_controller.js +10 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +17 -2
- package/dist/controllers/progress_controller.js +123 -12
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +27 -1
- package/dist/controllers/range_slider_controller.js +449 -93
- 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 +55 -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 +135 -12
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +1 -1
- package/dist/controllers/scroll_area_controller.js +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js +13 -17
- package/dist/controllers/separator_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 +143 -22
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +17 -1
- package/dist/controllers/slider_controller.js +342 -50
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +50 -12
- package/dist/controllers/spinner_controller.js +244 -28
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +13 -2
- package/dist/controllers/step_indicator_controller.js +85 -6
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.js +2 -0
- package/dist/controllers/stepper_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/controllers/switch_controller.d.ts +12 -8
- package/dist/controllers/switch_controller.js +162 -18
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +3 -0
- package/dist/controllers/time_picker_controller.js +6 -3
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -2
- package/dist/controllers/tree_view_controller.js +19 -1
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +2278 -596
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +76 -1
- package/dist/inspector/cli.js +937 -76
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +997 -78
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +20 -20
- package/dist/inspector/manifest.json +428 -33
- package/package.json +3 -3
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,11 +254,699 @@ 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
|
+
const number = /^[+-]?\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?/.exec(
|
|
614
|
+
code.slice(valueStart)
|
|
615
|
+
)?.[0];
|
|
616
|
+
if (number !== void 0) {
|
|
617
|
+
const end = valueStart + number.length;
|
|
618
|
+
const after = skipTrivia(code, end);
|
|
619
|
+
const whole = after >= code.length || code[after] === "," || code[after] === "}";
|
|
620
|
+
if (whole) {
|
|
621
|
+
return {
|
|
622
|
+
attr: {
|
|
623
|
+
name,
|
|
624
|
+
value: number,
|
|
625
|
+
...anchor,
|
|
626
|
+
valueStart: base + valueStart,
|
|
627
|
+
valueEnd: base + end
|
|
628
|
+
},
|
|
629
|
+
end
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return { attr: { name, value: null, ...anchor }, end: skipValue(code, valueStart) };
|
|
634
|
+
}
|
|
635
|
+
function readKey(code, start) {
|
|
636
|
+
const char = code[start];
|
|
637
|
+
if (char === '"' || char === "'") {
|
|
638
|
+
const literal = readString(code, start);
|
|
639
|
+
const valueAt = keySeparator(code, literal.end);
|
|
640
|
+
if (!literal.literal || valueAt === null) return null;
|
|
641
|
+
return { name: literal.value, end: literal.end, valueAt };
|
|
642
|
+
}
|
|
643
|
+
if (char === ":" && IDENT_START.test(code[start + 1] ?? "")) {
|
|
644
|
+
const name = readIdentifier(code, start + 1);
|
|
645
|
+
const valueAt = hashRocket(code, name.end);
|
|
646
|
+
return valueAt === null ? null : { name: name.text, end: name.end, valueAt };
|
|
647
|
+
}
|
|
648
|
+
if (char !== void 0 && IDENT_START.test(char)) {
|
|
649
|
+
const name = readIdentifier(code, start);
|
|
650
|
+
if (code[name.end] !== ":" || code[name.end + 1] === ":") return null;
|
|
651
|
+
return { name: name.text, end: name.end, valueAt: name.end + 1 };
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
function attributeName(key) {
|
|
656
|
+
const name = key.replaceAll("_", "-").toLowerCase();
|
|
657
|
+
return name.length > 0 && !/[\s"'=<>/]/.test(name) ? `data-${name}` : null;
|
|
658
|
+
}
|
|
659
|
+
function keySeparator(code, at) {
|
|
660
|
+
if (code[at] === ":" && code[at + 1] !== ":") return at + 1;
|
|
661
|
+
return hashRocket(code, at);
|
|
662
|
+
}
|
|
663
|
+
function hashRocket(code, at) {
|
|
664
|
+
const arrow = skipTrivia(code, at);
|
|
665
|
+
return code.startsWith("=>", arrow) ? arrow + 2 : null;
|
|
666
|
+
}
|
|
667
|
+
function readIdentifier(code, start) {
|
|
668
|
+
let end = start;
|
|
669
|
+
while (end < code.length && IDENT_CHAR.test(code[end])) end++;
|
|
670
|
+
return { text: code.slice(start, end), end };
|
|
671
|
+
}
|
|
672
|
+
function skipValue(code, start) {
|
|
673
|
+
let depth = 0;
|
|
674
|
+
let i = start;
|
|
675
|
+
while (i < code.length) {
|
|
676
|
+
const char = code[i];
|
|
677
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
678
|
+
i = readString(code, i).end;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (char === "#") {
|
|
682
|
+
i = endOfLine(code, i);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const literal = skipLiteral(code, i);
|
|
686
|
+
if (literal >= 0) {
|
|
687
|
+
i = literal;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (char === "(" || char === "[" || char === "{") depth++;
|
|
691
|
+
else if (char === ")" || char === "]") depth--;
|
|
692
|
+
else if (char === "}") {
|
|
693
|
+
if (depth === 0) return i;
|
|
694
|
+
depth--;
|
|
695
|
+
} else if (char === "," && depth === 0) return i;
|
|
696
|
+
i++;
|
|
697
|
+
}
|
|
698
|
+
return code.length;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/inspector/erb.ts
|
|
702
|
+
var ERB_TAG = /<%[\s\S]*?%>/g;
|
|
703
|
+
function neutralizeErb(source) {
|
|
704
|
+
return source.replace(ERB_TAG, (match) => match.replace(/[^\n]/g, " "));
|
|
705
|
+
}
|
|
706
|
+
function erbRanges(source) {
|
|
707
|
+
return [...source.matchAll(ERB_TAG)].map(
|
|
708
|
+
(match) => [match.index ?? 0, (match.index ?? 0) + match[0].length]
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
function erbElements(source) {
|
|
712
|
+
const tags = readErbTags(source);
|
|
713
|
+
const blockEnds = matchBlocks(tags);
|
|
714
|
+
const index = new PositionIndex(source);
|
|
715
|
+
const elements = [];
|
|
716
|
+
for (const [ordinal, tag] of tags.entries()) {
|
|
717
|
+
if (!tag.output) continue;
|
|
718
|
+
const option = readDataOption(tag.code, tag.codeStart);
|
|
719
|
+
if (!option.found) continue;
|
|
720
|
+
const end = blockEnds.get(ordinal) ?? tag.end;
|
|
721
|
+
elements.push({
|
|
722
|
+
tag: ERB_ELEMENT_TAG,
|
|
723
|
+
attrs: option.attrs.map((attr) => toParsedAttr(attr, index)),
|
|
724
|
+
start: tag.start,
|
|
725
|
+
// The token a reader can act on is the delimiter that opens the call.
|
|
726
|
+
sourceLength: tag.codeStart - tag.start,
|
|
727
|
+
end,
|
|
728
|
+
container: end > tag.end,
|
|
729
|
+
opaque: option.opaque
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
return elements;
|
|
733
|
+
}
|
|
734
|
+
function toParsedAttr(attr, index) {
|
|
735
|
+
const at = index.at(attr.keyStart);
|
|
736
|
+
return {
|
|
737
|
+
name: attr.name,
|
|
738
|
+
value: attr.value ?? "",
|
|
739
|
+
line: at.line,
|
|
740
|
+
column: at.column,
|
|
741
|
+
sourceLength: attr.keyEnd - attr.keyStart,
|
|
742
|
+
valueStart: attr.valueStart,
|
|
743
|
+
valueEnd: attr.valueEnd,
|
|
744
|
+
dynamicValue: attr.value === null
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function readErbTags(source) {
|
|
748
|
+
const tags = [];
|
|
749
|
+
for (const match of source.matchAll(ERB_TAG)) {
|
|
750
|
+
const raw = match[0];
|
|
751
|
+
const start = match.index ?? 0;
|
|
752
|
+
let open = 2;
|
|
753
|
+
let output = false;
|
|
754
|
+
if (raw[open] === "#") continue;
|
|
755
|
+
if (raw[open] === "=") {
|
|
756
|
+
output = true;
|
|
757
|
+
open++;
|
|
758
|
+
if (raw[open] === "=") open++;
|
|
759
|
+
} else if (raw[open] === "-") {
|
|
760
|
+
open++;
|
|
761
|
+
}
|
|
762
|
+
let close = raw.length - 2;
|
|
763
|
+
if (raw[close - 1] === "-") close--;
|
|
764
|
+
if (close < open) close = open;
|
|
765
|
+
tags.push({
|
|
766
|
+
start,
|
|
767
|
+
end: start + raw.length,
|
|
768
|
+
output,
|
|
769
|
+
code: raw.slice(open, close),
|
|
770
|
+
codeStart: start + open
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return tags;
|
|
774
|
+
}
|
|
775
|
+
function matchBlocks(tags) {
|
|
776
|
+
const ends = /* @__PURE__ */ new Map();
|
|
777
|
+
const open = [];
|
|
778
|
+
for (const [ordinal, tag] of tags.entries()) {
|
|
779
|
+
for (const event of blockEvents(tag.code)) {
|
|
780
|
+
if (event === "open") {
|
|
781
|
+
open.push(ordinal);
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const opener = open.pop();
|
|
785
|
+
if (opener !== void 0) ends.set(opener, tag.end);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return ends;
|
|
789
|
+
}
|
|
790
|
+
var BLOCK_OPENERS = /* @__PURE__ */ new Set(["case", "begin", "def", "class", "module", "for"]);
|
|
791
|
+
var BLOCK_MODIFIERS = /* @__PURE__ */ new Set(["if", "unless", "while", "until"]);
|
|
792
|
+
var BLOCK_CONTINUERS = /* @__PURE__ */ new Set(["elsif", "else", "when", "in", "then", "rescue", "ensure"]);
|
|
793
|
+
function blockEvents(source) {
|
|
794
|
+
const code = maskHeredocs(source);
|
|
795
|
+
const events = [];
|
|
796
|
+
let previous = "";
|
|
797
|
+
let loopHeader = false;
|
|
798
|
+
let i = 0;
|
|
799
|
+
while (i < code.length) {
|
|
800
|
+
const char = code[i];
|
|
801
|
+
if (char === "\n" || char === ";") {
|
|
802
|
+
previous = char;
|
|
803
|
+
loopHeader = false;
|
|
804
|
+
i++;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (SPACE.has(char)) {
|
|
808
|
+
i++;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (char === "#") {
|
|
812
|
+
i = endOfLine(code, i);
|
|
813
|
+
previous = "\n";
|
|
814
|
+
loopHeader = false;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
818
|
+
i = readString(code, i).end;
|
|
819
|
+
previous = "value";
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (char === ":" && IDENT_START.test(code[i + 1] ?? "")) {
|
|
823
|
+
i = identifierEnd(code, i + 1);
|
|
824
|
+
previous = "value";
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const literal = skipLiteral(code, i);
|
|
828
|
+
if (literal >= 0) {
|
|
829
|
+
i = literal;
|
|
830
|
+
previous = "value";
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (char === "{") {
|
|
834
|
+
events.push("open");
|
|
835
|
+
previous = "{";
|
|
836
|
+
i++;
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
if (char === "}") {
|
|
840
|
+
events.push("close");
|
|
841
|
+
previous = "value";
|
|
842
|
+
i++;
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (char === ")" || char === "]") {
|
|
846
|
+
previous = "value";
|
|
847
|
+
i++;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (!IDENT_START.test(char)) {
|
|
851
|
+
previous = char;
|
|
852
|
+
i++;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const end = identifierEnd(code, i);
|
|
856
|
+
const word = code.slice(i, end);
|
|
857
|
+
const keyword = !(code[end] === ":" && code[end + 1] !== ":") && code[i - 1] !== ".";
|
|
858
|
+
i = end;
|
|
859
|
+
if (keyword && word === "end") {
|
|
860
|
+
events.push("close");
|
|
861
|
+
previous = "value";
|
|
862
|
+
} else if (keyword && word === "do") {
|
|
863
|
+
if (loopHeader) loopHeader = false;
|
|
864
|
+
else events.push("open");
|
|
865
|
+
previous = "do";
|
|
866
|
+
} else if (keyword && BLOCK_OPENERS.has(word)) {
|
|
867
|
+
events.push("open");
|
|
868
|
+
loopHeader = word === "for";
|
|
869
|
+
previous = word;
|
|
870
|
+
} else if (keyword && BLOCK_MODIFIERS.has(word)) {
|
|
871
|
+
if (previous !== "value") {
|
|
872
|
+
events.push("open");
|
|
873
|
+
loopHeader = word !== "if" && word !== "unless";
|
|
874
|
+
}
|
|
875
|
+
previous = word;
|
|
876
|
+
} else if (keyword && BLOCK_CONTINUERS.has(word)) {
|
|
877
|
+
previous = word;
|
|
878
|
+
} else {
|
|
879
|
+
previous = "value";
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return events;
|
|
883
|
+
}
|
|
884
|
+
function identifierEnd(code, start) {
|
|
885
|
+
let end = start;
|
|
886
|
+
while (end < code.length && IDENT_CHAR.test(code[end])) end++;
|
|
887
|
+
return end;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/inspector/extract.ts
|
|
891
|
+
var NAMESPACE_PREFIX = "stimeo--";
|
|
892
|
+
function dasherize(value) {
|
|
893
|
+
return value.replace(/([A-Z])/g, (_match, char) => `-${char.toLowerCase()}`);
|
|
894
|
+
}
|
|
895
|
+
function controllerIdentifiers(dataControllerValue) {
|
|
896
|
+
const seen = /* @__PURE__ */ new Set();
|
|
897
|
+
for (const token of dataControllerValue.split(/\s+/)) {
|
|
898
|
+
if (token.startsWith(NAMESPACE_PREFIX)) seen.add(token);
|
|
899
|
+
}
|
|
900
|
+
return [...seen];
|
|
901
|
+
}
|
|
902
|
+
function parseTargetAttr(attrName) {
|
|
903
|
+
const match = /^data-(stimeo--[a-z0-9][a-z0-9-]*)-target$/.exec(attrName);
|
|
904
|
+
return match?.[1] ?? null;
|
|
905
|
+
}
|
|
906
|
+
function parseValueAttr(attrName, knownIdentifiers) {
|
|
907
|
+
const match = /^data-(stimeo--[a-z0-9-]+)-value$/.exec(attrName);
|
|
908
|
+
if (!match) return null;
|
|
909
|
+
const inner = match[1] ?? "";
|
|
910
|
+
let best = null;
|
|
911
|
+
for (const id of knownIdentifiers) {
|
|
912
|
+
if (inner.startsWith(`${id}-`)) {
|
|
913
|
+
const valueToken = inner.slice(id.length + 1);
|
|
914
|
+
if (valueToken.length > 0 && (!best || id.length > best.identifier.length)) {
|
|
915
|
+
best = { identifier: id, valueToken };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (best) return best;
|
|
920
|
+
const rest = inner.slice(NAMESPACE_PREFIX.length);
|
|
921
|
+
const dash = rest.indexOf("-");
|
|
922
|
+
const identifierGuess = dash === -1 ? inner : `${NAMESPACE_PREFIX}${rest.slice(0, dash)}`;
|
|
923
|
+
const valueGuess = dash === -1 ? "" : rest.slice(dash + 1);
|
|
924
|
+
return { identifier: null, valueToken: valueGuess || identifierGuess };
|
|
925
|
+
}
|
|
926
|
+
function actionDescriptors(dataActionValue) {
|
|
927
|
+
const descriptors = [];
|
|
928
|
+
for (const descriptor of dataActionValue.split(/\s+/)) {
|
|
929
|
+
const hash = descriptor.indexOf("#");
|
|
930
|
+
if (hash === -1) continue;
|
|
931
|
+
let lhs = descriptor.slice(0, hash);
|
|
932
|
+
const arrow = lhs.indexOf("->");
|
|
933
|
+
if (arrow !== -1) lhs = lhs.slice(arrow + 2);
|
|
934
|
+
if (!lhs.startsWith(NAMESPACE_PREFIX)) continue;
|
|
935
|
+
const method = /^[a-zA-Z_$][\w$]*/.exec(descriptor.slice(hash + 1))?.[0] ?? "";
|
|
936
|
+
descriptors.push({ identifier: lhs, method });
|
|
937
|
+
}
|
|
938
|
+
return descriptors;
|
|
939
|
+
}
|
|
940
|
+
function isStimeoDataAttr(attrName) {
|
|
941
|
+
return attrName.startsWith(`data-${NAMESPACE_PREFIX}`);
|
|
942
|
+
}
|
|
943
|
+
|
|
261
944
|
// src/inspector/types.ts
|
|
262
945
|
var DIAGNOSTIC_CODES = [
|
|
263
946
|
"unknown-controller",
|
|
264
947
|
"unknown-target",
|
|
265
948
|
"unknown-value",
|
|
949
|
+
"invalid-value",
|
|
266
950
|
"unknown-action-controller",
|
|
267
951
|
"unknown-action-method",
|
|
268
952
|
"orphan-target",
|
|
@@ -271,6 +955,7 @@ var DIAGNOSTIC_CODES = [
|
|
|
271
955
|
"missing-aria",
|
|
272
956
|
"invalid-aria-value",
|
|
273
957
|
"keyboard-inaccessible",
|
|
958
|
+
"invalid-host",
|
|
274
959
|
"unresolved-idref",
|
|
275
960
|
"managed-aria",
|
|
276
961
|
"composition-mismatch",
|
|
@@ -284,16 +969,15 @@ var DIAGNOSTIC_CODES = [
|
|
|
284
969
|
// src/inspector/check.ts
|
|
285
970
|
function checkSource(source, manifest) {
|
|
286
971
|
const diagnostics = [];
|
|
287
|
-
const tree = parseHtml(neutralizeErb(source));
|
|
972
|
+
const tree = parseHtml(neutralizeErb(source), erbElements(source));
|
|
288
973
|
const known = manifest.controllers;
|
|
289
974
|
const knownIdentifiers = Object.keys(known);
|
|
290
|
-
const
|
|
291
|
-
(m) => [m.index ?? 0, (m.index ?? 0) + m[0].length]
|
|
292
|
-
);
|
|
975
|
+
const ranges = erbRanges(source);
|
|
293
976
|
const isDynamicValue = (attr) => {
|
|
977
|
+
if (attr.dynamicValue !== void 0) return attr.dynamicValue;
|
|
294
978
|
const { valueStart, valueEnd } = attr;
|
|
295
979
|
if (valueStart === void 0 || valueEnd === void 0) return false;
|
|
296
|
-
return
|
|
980
|
+
return ranges.some(([start, end]) => start < valueEnd && end > valueStart);
|
|
297
981
|
};
|
|
298
982
|
const conditionHolds = (scope, identifier, when) => {
|
|
299
983
|
if (!when) return true;
|
|
@@ -303,6 +987,36 @@ function checkSource(source, manifest) {
|
|
|
303
987
|
if (hostAttr && isDynamicValue(hostAttr)) return null;
|
|
304
988
|
return when.equals.includes(hostAttr ? hostAttr.value.trim() : when.default);
|
|
305
989
|
};
|
|
990
|
+
const isContentEditable2 = (element) => {
|
|
991
|
+
let current = element;
|
|
992
|
+
while (current && current.tag !== "#root") {
|
|
993
|
+
const attr = current.attrs.find((candidate) => candidate.name === "contenteditable");
|
|
994
|
+
if (attr) {
|
|
995
|
+
if (isDynamicValue(attr)) return null;
|
|
996
|
+
const value = attr.value.trim().toLowerCase();
|
|
997
|
+
if (value === "false") return false;
|
|
998
|
+
if (value === "" || value === "true" || value === "plaintext-only") return true;
|
|
999
|
+
}
|
|
1000
|
+
current = current.parent;
|
|
1001
|
+
}
|
|
1002
|
+
return false;
|
|
1003
|
+
};
|
|
1004
|
+
const hostIsAllowed = (element, rule) => {
|
|
1005
|
+
if (element.tag === "button") {
|
|
1006
|
+
if (rule.mode === "non-interactive") return false;
|
|
1007
|
+
const type = element.attrs.find((attr) => attr.name === "type");
|
|
1008
|
+
if (type && isDynamicValue(type)) return null;
|
|
1009
|
+
const effectiveType = type?.value.trim().toLowerCase() || "submit";
|
|
1010
|
+
return (rule.buttonTypes ?? []).includes(effectiveType);
|
|
1011
|
+
}
|
|
1012
|
+
if (ALWAYS_INTERACTIVE_HOSTS.has(element.tag)) return false;
|
|
1013
|
+
if ((element.tag === "a" || element.tag === "area") && hasAttr(element, "href")) return false;
|
|
1014
|
+
if ((element.tag === "audio" || element.tag === "video") && hasAttr(element, "controls")) {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
const editable = isContentEditable2(element);
|
|
1018
|
+
return editable === null ? null : !editable;
|
|
1019
|
+
};
|
|
306
1020
|
const suppressed = (node, code) => {
|
|
307
1021
|
if (code === "unknown-ignore-code") return false;
|
|
308
1022
|
let current = node;
|
|
@@ -310,7 +1024,9 @@ function checkSource(source, manifest) {
|
|
|
310
1024
|
const ignore = current.attrs.find((a) => a.name === IGNORE_ATTR);
|
|
311
1025
|
if (ignore) {
|
|
312
1026
|
const codes = ignore.value.trim();
|
|
313
|
-
if (codes.length === 0
|
|
1027
|
+
if (codes.length === 0) {
|
|
1028
|
+
if (!isDynamicValue(ignore)) return true;
|
|
1029
|
+
} else if (codes.split(/\s+/).includes(code)) return true;
|
|
314
1030
|
}
|
|
315
1031
|
current = current.parent;
|
|
316
1032
|
}
|
|
@@ -319,6 +1035,26 @@ function checkSource(source, manifest) {
|
|
|
319
1035
|
const report = (owner, code, severity, message, at, suggestion, fix) => {
|
|
320
1036
|
if (!suppressed(owner, code)) push(diagnostics, code, severity, message, at, suggestion, fix);
|
|
321
1037
|
};
|
|
1038
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
1039
|
+
const unnamedTargets = [];
|
|
1040
|
+
const targetsUnreadable = (container, scope, identifier) => {
|
|
1041
|
+
for (const node of unresolved) {
|
|
1042
|
+
if (isWithin(node, container) && findOwner(node, identifier) === scope) return true;
|
|
1043
|
+
}
|
|
1044
|
+
for (const { node, identifier: hidden } of unnamedTargets) {
|
|
1045
|
+
if (hidden !== identifier) continue;
|
|
1046
|
+
if (isWithin(node, container) && findOwner(node, identifier) === scope) return true;
|
|
1047
|
+
}
|
|
1048
|
+
return false;
|
|
1049
|
+
};
|
|
1050
|
+
const unresolvedAbove = (node) => {
|
|
1051
|
+
let current = node;
|
|
1052
|
+
while (current && current.tag !== "#root") {
|
|
1053
|
+
if (unresolved.has(current)) return true;
|
|
1054
|
+
current = current.parent;
|
|
1055
|
+
}
|
|
1056
|
+
return false;
|
|
1057
|
+
};
|
|
322
1058
|
const scopes = [];
|
|
323
1059
|
const presence = /* @__PURE__ */ new Map();
|
|
324
1060
|
const targetNodes = /* @__PURE__ */ new Map();
|
|
@@ -373,6 +1109,10 @@ function checkSource(source, manifest) {
|
|
|
373
1109
|
}
|
|
374
1110
|
const declarationCandidates = [];
|
|
375
1111
|
walk(tree, (node) => {
|
|
1112
|
+
const declaredController = node.attrs.find((a) => a.name === "data-controller");
|
|
1113
|
+
if (node.opaque || declaredController && isDynamicValue(declaredController)) {
|
|
1114
|
+
unresolved.add(node);
|
|
1115
|
+
}
|
|
376
1116
|
if (declarationAttrs.size > 0 && node.attrs.some((a) => declarationAttrs.has(a.name))) {
|
|
377
1117
|
declarationCandidates.push(node);
|
|
378
1118
|
}
|
|
@@ -384,6 +1124,7 @@ function checkSource(source, manifest) {
|
|
|
384
1124
|
focusableRoleTally.set(role, (focusableRoleTally.get(role) ?? 0) + 1);
|
|
385
1125
|
}
|
|
386
1126
|
}
|
|
1127
|
+
const authoredValueIdentifiers = /* @__PURE__ */ new Set();
|
|
387
1128
|
for (const attr of node.attrs) {
|
|
388
1129
|
if (attr.name === IGNORE_ATTR) {
|
|
389
1130
|
for (const token of attr.value.split(/\s+/).filter((t) => t.length > 0)) {
|
|
@@ -480,14 +1221,18 @@ function checkSource(source, manifest) {
|
|
|
480
1221
|
if (targetName.length > 0) {
|
|
481
1222
|
recordPresence(owner, targetIdentifier, targetName);
|
|
482
1223
|
recordTargetNode(owner, targetIdentifier, targetName, node);
|
|
1224
|
+
} else if (isDynamicValue(attr)) {
|
|
1225
|
+
unnamedTargets.push({ node, identifier: targetIdentifier });
|
|
483
1226
|
}
|
|
484
1227
|
} else if (!inDeclaredFragment(node, targetIdentifier)) {
|
|
1228
|
+
const uncertain = unresolvedAbove(node);
|
|
485
1229
|
report(
|
|
486
1230
|
node,
|
|
487
1231
|
"orphan-target",
|
|
488
|
-
"error",
|
|
1232
|
+
uncertain ? "warning" : "error",
|
|
489
1233
|
`Target "${attr.name}" has no enclosing controller "${targetIdentifier}".`,
|
|
490
|
-
attr
|
|
1234
|
+
attr,
|
|
1235
|
+
uncertain ? UNRESOLVED_HOST_HINT : void 0
|
|
491
1236
|
);
|
|
492
1237
|
}
|
|
493
1238
|
continue;
|
|
@@ -504,6 +1249,7 @@ function checkSource(source, manifest) {
|
|
|
504
1249
|
attr
|
|
505
1250
|
);
|
|
506
1251
|
} else {
|
|
1252
|
+
authoredValueIdentifiers.add(parsed.identifier);
|
|
507
1253
|
const controller = known[parsed.identifier];
|
|
508
1254
|
const valid = controller?.values.some((v) => dasherize(v) === parsed.valueToken) ?? false;
|
|
509
1255
|
if (!valid) {
|
|
@@ -515,6 +1261,20 @@ function checkSource(source, manifest) {
|
|
|
515
1261
|
attr,
|
|
516
1262
|
didYouMean(parsed.valueToken, (controller?.values ?? []).map(dasherize))
|
|
517
1263
|
);
|
|
1264
|
+
} else if (!isDynamicValue(attr)) {
|
|
1265
|
+
const constraint = controller?.valueConstraints.find(
|
|
1266
|
+
(candidate) => dasherize(candidate.value) === parsed.valueToken
|
|
1267
|
+
);
|
|
1268
|
+
if (constraint && !valueSatisfiesConstraint(attr.value, constraint)) {
|
|
1269
|
+
report(
|
|
1270
|
+
node,
|
|
1271
|
+
"invalid-value",
|
|
1272
|
+
"error",
|
|
1273
|
+
`Invalid value "${attr.value.trim()}" for "${parsed.identifier}.${parsed.valueToken}". Expected ${describeValueConstraint(constraint)}.`,
|
|
1274
|
+
attr,
|
|
1275
|
+
constraint.suggestion
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
518
1278
|
}
|
|
519
1279
|
}
|
|
520
1280
|
continue;
|
|
@@ -549,19 +1309,50 @@ function checkSource(source, manifest) {
|
|
|
549
1309
|
}
|
|
550
1310
|
}
|
|
551
1311
|
}
|
|
1312
|
+
for (const identifier of authoredValueIdentifiers) {
|
|
1313
|
+
const controller = known[identifier];
|
|
1314
|
+
if (!controller) continue;
|
|
1315
|
+
for (const relation of controller.valueRelations) {
|
|
1316
|
+
const leftAttr = node.attrs.find(
|
|
1317
|
+
(attr) => attr.name === `data-${identifier}-${dasherize(relation.left)}-value`
|
|
1318
|
+
);
|
|
1319
|
+
const rightAttr = node.attrs.find(
|
|
1320
|
+
(attr) => attr.name === `data-${identifier}-${dasherize(relation.right)}-value`
|
|
1321
|
+
);
|
|
1322
|
+
const at = rightAttr ?? leftAttr;
|
|
1323
|
+
if (!at || leftAttr && isDynamicValue(leftAttr) || rightAttr && isDynamicValue(rightAttr)) {
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
const left = leftAttr ? decodeNumericValue(leftAttr.value) : relation.leftDefault;
|
|
1327
|
+
const right = rightAttr ? decodeNumericValue(rightAttr.value) : relation.rightDefault;
|
|
1328
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) continue;
|
|
1329
|
+
if (relation.operator === "less-than-or-equal" && left <= right) continue;
|
|
1330
|
+
report(
|
|
1331
|
+
node,
|
|
1332
|
+
"invalid-value",
|
|
1333
|
+
"error",
|
|
1334
|
+
`Invalid values for "${identifier}": "${relation.left}" (${left}) must be less than or equal to "${relation.right}" (${right}).`,
|
|
1335
|
+
at,
|
|
1336
|
+
relation.suggestion
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
552
1340
|
});
|
|
1341
|
+
const scopeIsComplete = (container, scope, identifier) => container.origin === "markup" && !targetsUnreadable(container, scope, identifier);
|
|
553
1342
|
for (const { node, identifier } of scopes) {
|
|
554
1343
|
const controller = known[identifier];
|
|
555
1344
|
if (!controller) continue;
|
|
1345
|
+
const complete = scopeIsComplete(node, node, identifier);
|
|
556
1346
|
const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
|
|
557
1347
|
for (const required of controller.requiredTargets) {
|
|
558
1348
|
if (!present.has(required)) {
|
|
559
1349
|
report(
|
|
560
1350
|
node,
|
|
561
1351
|
"missing-required-target",
|
|
562
|
-
"error",
|
|
1352
|
+
complete ? "error" : "warning",
|
|
563
1353
|
`"${identifier}" is missing required target "${required}".`,
|
|
564
|
-
node
|
|
1354
|
+
node,
|
|
1355
|
+
complete ? void 0 : UNRESOLVED_TARGET_HINT
|
|
565
1356
|
);
|
|
566
1357
|
}
|
|
567
1358
|
}
|
|
@@ -569,6 +1360,7 @@ function checkSource(source, manifest) {
|
|
|
569
1360
|
for (const { node, identifier } of scopes) {
|
|
570
1361
|
const controller = known[identifier];
|
|
571
1362
|
if (!controller) continue;
|
|
1363
|
+
const complete = scopeIsComplete(node, node, identifier);
|
|
572
1364
|
const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
|
|
573
1365
|
for (const rule of controller.conditionalTargets) {
|
|
574
1366
|
if (!present.has(rule.whenPresent)) continue;
|
|
@@ -577,10 +1369,10 @@ function checkSource(source, manifest) {
|
|
|
577
1369
|
report(
|
|
578
1370
|
node,
|
|
579
1371
|
"missing-conditional-target",
|
|
580
|
-
"error",
|
|
1372
|
+
complete ? "error" : "warning",
|
|
581
1373
|
`"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
|
|
582
1374
|
node,
|
|
583
|
-
rule.suggestion
|
|
1375
|
+
complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
|
|
584
1376
|
);
|
|
585
1377
|
}
|
|
586
1378
|
}
|
|
@@ -593,6 +1385,7 @@ function checkSource(source, manifest) {
|
|
|
593
1385
|
if (req.whenDocument && !documentHolds(req.whenDocument)) continue;
|
|
594
1386
|
const severity = documentHolds(req.escalateWhen) ? "error" : req.severity ?? "error";
|
|
595
1387
|
for (const el of elements) {
|
|
1388
|
+
if (!hasReadableMarkup(el)) continue;
|
|
596
1389
|
if (!contentHolds(node, identifier, el, req.whenContains)) continue;
|
|
597
1390
|
if (!elementHolds(el, req.whenElement)) continue;
|
|
598
1391
|
checkA11y(report, el, identifier, req, severity);
|
|
@@ -606,6 +1399,7 @@ function checkSource(source, manifest) {
|
|
|
606
1399
|
const reach = req.reach ?? "tab";
|
|
607
1400
|
const elements = targetNodes.get(node)?.get(identifier)?.get(req.target) ?? [];
|
|
608
1401
|
for (const el of elements) {
|
|
1402
|
+
if (!hasReadableMarkup(el)) continue;
|
|
609
1403
|
if (!isFocusable(el, reach)) {
|
|
610
1404
|
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
1405
|
report(
|
|
@@ -620,6 +1414,28 @@ function checkSource(source, manifest) {
|
|
|
620
1414
|
}
|
|
621
1415
|
}
|
|
622
1416
|
}
|
|
1417
|
+
for (const { node, identifier } of scopes) {
|
|
1418
|
+
const controller = known[identifier];
|
|
1419
|
+
if (!controller) continue;
|
|
1420
|
+
for (const rule of controller.hosts) {
|
|
1421
|
+
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
1422
|
+
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
1423
|
+
for (const element of elements) {
|
|
1424
|
+
if (!hasReadableMarkup(element)) continue;
|
|
1425
|
+
if (hostIsAllowed(element, rule) !== false) continue;
|
|
1426
|
+
const type = element.attrs.find((attr) => attr.name === "type");
|
|
1427
|
+
const detail = element.tag === "button" ? `the unsupported ${type ? `type="${type.value}"` : 'implicit type="submit"'}` : `an unsupported interactive <${element.tag}>`;
|
|
1428
|
+
report(
|
|
1429
|
+
element,
|
|
1430
|
+
"invalid-host",
|
|
1431
|
+
"error",
|
|
1432
|
+
`The ${where} of "${identifier}" uses ${detail} host.`,
|
|
1433
|
+
type ?? element,
|
|
1434
|
+
rule.suggestion
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
623
1439
|
for (const { node, identifier } of scopes) {
|
|
624
1440
|
const controller = known[identifier];
|
|
625
1441
|
if (!controller) continue;
|
|
@@ -627,6 +1443,7 @@ function checkSource(source, manifest) {
|
|
|
627
1443
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
628
1444
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
629
1445
|
for (const el of elements) {
|
|
1446
|
+
if (!hasReadableMarkup(el)) continue;
|
|
630
1447
|
for (const name of rule.attrs) {
|
|
631
1448
|
const attr = el.attrs.find((a) => a.name === name);
|
|
632
1449
|
if (!attr) continue;
|
|
@@ -649,6 +1466,7 @@ function checkSource(source, manifest) {
|
|
|
649
1466
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
650
1467
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
651
1468
|
for (const el of elements) {
|
|
1469
|
+
if (!hasReadableMarkup(el)) continue;
|
|
652
1470
|
if (!contentHolds(node, identifier, el, rule.whenContains)) continue;
|
|
653
1471
|
for (const name of rule.attrs) {
|
|
654
1472
|
const attr = el.attrs.find((a) => a.name === name);
|
|
@@ -679,6 +1497,7 @@ function checkSource(source, manifest) {
|
|
|
679
1497
|
if (conditionHolds(node, identifier, when) !== true) continue;
|
|
680
1498
|
const hostContext = when ? ` (${when.value} ${list(when.equals)})` : "";
|
|
681
1499
|
for (const el of elements) {
|
|
1500
|
+
if (el.opaque) continue;
|
|
682
1501
|
const declared = el.attrs.find((a) => a.name === "data-controller");
|
|
683
1502
|
if (!declared || !controllerIdentifiers(declared.value).includes(rule.coController)) {
|
|
684
1503
|
continue;
|
|
@@ -719,6 +1538,7 @@ function checkSource(source, manifest) {
|
|
|
719
1538
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
720
1539
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
721
1540
|
for (const el of elements) {
|
|
1541
|
+
if (el.opaque) continue;
|
|
722
1542
|
const declared = el.attrs.find((a) => a.name === "data-controller");
|
|
723
1543
|
if (declared && isDynamicValue(declared)) continue;
|
|
724
1544
|
if (declared && controllerIdentifiers(declared.value).includes(rule.controller)) continue;
|
|
@@ -807,13 +1627,14 @@ function checkSource(source, manifest) {
|
|
|
807
1627
|
}
|
|
808
1628
|
}
|
|
809
1629
|
if (rule.min !== void 0 && matched.length < rule.min) {
|
|
1630
|
+
const complete = scopeIsComplete(container, node, identifier);
|
|
810
1631
|
report(
|
|
811
1632
|
container,
|
|
812
1633
|
"cardinality-violation",
|
|
813
|
-
"error",
|
|
1634
|
+
complete ? "error" : "warning",
|
|
814
1635
|
`"${identifier}" requires at least ${rule.min} ${what} per ${where}, but found ${matched.length}.`,
|
|
815
1636
|
container,
|
|
816
|
-
rule.suggestion
|
|
1637
|
+
complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
|
|
817
1638
|
);
|
|
818
1639
|
}
|
|
819
1640
|
}
|
|
@@ -856,6 +1677,28 @@ function checkSource(source, manifest) {
|
|
|
856
1677
|
diagnostics.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
857
1678
|
return diagnostics;
|
|
858
1679
|
}
|
|
1680
|
+
function valueSatisfiesConstraint(raw, constraint) {
|
|
1681
|
+
const value = decodeNumericValue(raw);
|
|
1682
|
+
if (Number.isNaN(value)) return false;
|
|
1683
|
+
if (constraint.finite && !Number.isFinite(value)) return false;
|
|
1684
|
+
if (constraint.greaterThan !== void 0 && !(value > constraint.greaterThan)) return false;
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
function decodeNumericValue(raw) {
|
|
1688
|
+
return Number(raw.replace(/_/g, ""));
|
|
1689
|
+
}
|
|
1690
|
+
function describeValueConstraint(constraint) {
|
|
1691
|
+
const parts = [constraint.finite ? "a finite number" : "a number"];
|
|
1692
|
+
if (constraint.greaterThan !== void 0) {
|
|
1693
|
+
parts.push(`greater than ${constraint.greaterThan}`);
|
|
1694
|
+
}
|
|
1695
|
+
return parts.join(" ");
|
|
1696
|
+
}
|
|
1697
|
+
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.";
|
|
1698
|
+
var UNRESOLVED_HOST_HINT = "An enclosing helper's data: option is not a literal hash, so the host controller may be declared at runtime.";
|
|
1699
|
+
function hasReadableMarkup(el) {
|
|
1700
|
+
return el.origin === "markup";
|
|
1701
|
+
}
|
|
859
1702
|
function checkA11y(report, el, identifier, req, severity) {
|
|
860
1703
|
const groups = [
|
|
861
1704
|
{ attrs: req.attrs, values: req.values },
|
|
@@ -923,6 +1766,20 @@ function describeCounted(target, attr, values) {
|
|
|
923
1766
|
return `"${target}" targets with ${attr}${constrained}`;
|
|
924
1767
|
}
|
|
925
1768
|
var NATIVE_FOCUSABLE_TAGS = /* @__PURE__ */ new Set(["button", "select", "textarea", "summary"]);
|
|
1769
|
+
var ALWAYS_INTERACTIVE_HOSTS = /* @__PURE__ */ new Set([
|
|
1770
|
+
"input",
|
|
1771
|
+
"select",
|
|
1772
|
+
"textarea",
|
|
1773
|
+
"label",
|
|
1774
|
+
"summary",
|
|
1775
|
+
"details",
|
|
1776
|
+
"iframe",
|
|
1777
|
+
"object",
|
|
1778
|
+
"embed"
|
|
1779
|
+
]);
|
|
1780
|
+
function hasAttr(element, name) {
|
|
1781
|
+
return element.attrs.some((attr) => attr.name === name);
|
|
1782
|
+
}
|
|
926
1783
|
function isNativelyFocusable(el) {
|
|
927
1784
|
if (NATIVE_FOCUSABLE_TAGS.has(el.tag)) return true;
|
|
928
1785
|
if (el.tag === "input") {
|
|
@@ -984,6 +1841,7 @@ function inDeclaredFragment(node, identifier) {
|
|
|
984
1841
|
return false;
|
|
985
1842
|
}
|
|
986
1843
|
function anchorLength(at) {
|
|
1844
|
+
if (at.sourceLength !== void 0) return at.sourceLength;
|
|
987
1845
|
return "name" in at ? at.name.length : at.tag.length + 1;
|
|
988
1846
|
}
|
|
989
1847
|
function push(out, code, severity, message, at, suggestion, fix) {
|
|
@@ -1072,12 +1930,15 @@ function isCheckableFile(path) {
|
|
|
1072
1930
|
var CONTROLLER_ARRAY_FIELDS = [
|
|
1073
1931
|
"targets",
|
|
1074
1932
|
"values",
|
|
1933
|
+
"valueConstraints",
|
|
1934
|
+
"valueRelations",
|
|
1075
1935
|
"actions",
|
|
1076
1936
|
"events",
|
|
1077
1937
|
"requiredTargets",
|
|
1078
1938
|
"conditionalTargets",
|
|
1079
1939
|
"a11y",
|
|
1080
1940
|
"keyboard",
|
|
1941
|
+
"hosts",
|
|
1081
1942
|
"managedAria",
|
|
1082
1943
|
"compositions",
|
|
1083
1944
|
"companions",
|