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
|
@@ -7,66 +7,8 @@ import { createInterface } from 'readline';
|
|
|
7
7
|
|
|
8
8
|
// src/inspector/cli_bin.ts
|
|
9
9
|
|
|
10
|
-
// src/inspector/erb.ts
|
|
11
|
-
function neutralizeErb(source) {
|
|
12
|
-
return source.replace(/<%[\s\S]*?%>/g, (match) => match.replace(/[^\n]/g, " "));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// src/inspector/extract.ts
|
|
16
|
-
var NAMESPACE_PREFIX = "stimeo--";
|
|
17
|
-
function dasherize(value) {
|
|
18
|
-
return value.replace(/([A-Z])/g, (_match, char) => `-${char.toLowerCase()}`);
|
|
19
|
-
}
|
|
20
|
-
function controllerIdentifiers(dataControllerValue) {
|
|
21
|
-
const seen = /* @__PURE__ */ new Set();
|
|
22
|
-
for (const token of dataControllerValue.split(/\s+/)) {
|
|
23
|
-
if (token.startsWith(NAMESPACE_PREFIX)) seen.add(token);
|
|
24
|
-
}
|
|
25
|
-
return [...seen];
|
|
26
|
-
}
|
|
27
|
-
function parseTargetAttr(attrName) {
|
|
28
|
-
const match = /^data-(stimeo--[a-z0-9][a-z0-9-]*)-target$/.exec(attrName);
|
|
29
|
-
return match?.[1] ?? null;
|
|
30
|
-
}
|
|
31
|
-
function parseValueAttr(attrName, knownIdentifiers) {
|
|
32
|
-
const match = /^data-(stimeo--[a-z0-9-]+)-value$/.exec(attrName);
|
|
33
|
-
if (!match) return null;
|
|
34
|
-
const inner = match[1] ?? "";
|
|
35
|
-
let best = null;
|
|
36
|
-
for (const id of knownIdentifiers) {
|
|
37
|
-
if (inner.startsWith(`${id}-`)) {
|
|
38
|
-
const valueToken = inner.slice(id.length + 1);
|
|
39
|
-
if (valueToken.length > 0 && (!best || id.length > best.identifier.length)) {
|
|
40
|
-
best = { identifier: id, valueToken };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (best) return best;
|
|
45
|
-
const rest = inner.slice(NAMESPACE_PREFIX.length);
|
|
46
|
-
const dash = rest.indexOf("-");
|
|
47
|
-
const identifierGuess = dash === -1 ? inner : `${NAMESPACE_PREFIX}${rest.slice(0, dash)}`;
|
|
48
|
-
const valueGuess = dash === -1 ? "" : rest.slice(dash + 1);
|
|
49
|
-
return { identifier: null, valueToken: valueGuess || identifierGuess };
|
|
50
|
-
}
|
|
51
|
-
function actionDescriptors(dataActionValue) {
|
|
52
|
-
const descriptors = [];
|
|
53
|
-
for (const descriptor of dataActionValue.split(/\s+/)) {
|
|
54
|
-
const hash = descriptor.indexOf("#");
|
|
55
|
-
if (hash === -1) continue;
|
|
56
|
-
let lhs = descriptor.slice(0, hash);
|
|
57
|
-
const arrow = lhs.indexOf("->");
|
|
58
|
-
if (arrow !== -1) lhs = lhs.slice(arrow + 2);
|
|
59
|
-
if (!lhs.startsWith(NAMESPACE_PREFIX)) continue;
|
|
60
|
-
const method = /^[a-zA-Z_$][\w$]*/.exec(descriptor.slice(hash + 1))?.[0] ?? "";
|
|
61
|
-
descriptors.push({ identifier: lhs, method });
|
|
62
|
-
}
|
|
63
|
-
return descriptors;
|
|
64
|
-
}
|
|
65
|
-
function isStimeoDataAttr(attrName) {
|
|
66
|
-
return attrName.startsWith(`data-${NAMESPACE_PREFIX}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
10
|
// src/inspector/html_parser.ts
|
|
11
|
+
var ERB_ELEMENT_TAG = "#erb";
|
|
70
12
|
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
71
13
|
"area",
|
|
72
14
|
"base",
|
|
@@ -103,7 +45,7 @@ var PositionIndex = class {
|
|
|
103
45
|
return { line: lo + 1, column: offset - (this.#lineStarts[lo] ?? 0) + 1 };
|
|
104
46
|
}
|
|
105
47
|
};
|
|
106
|
-
function parseHtml(source) {
|
|
48
|
+
function parseHtml(source, synthetics = []) {
|
|
107
49
|
const pos = new PositionIndex(source);
|
|
108
50
|
const len = source.length;
|
|
109
51
|
const ch = (k) => k >= 0 && k < len ? source[k] : "";
|
|
@@ -113,12 +55,57 @@ function parseHtml(source) {
|
|
|
113
55
|
children: [],
|
|
114
56
|
parent: null,
|
|
115
57
|
line: 1,
|
|
116
|
-
column: 1
|
|
58
|
+
column: 1,
|
|
59
|
+
origin: "markup",
|
|
60
|
+
opaque: false
|
|
117
61
|
};
|
|
118
62
|
const stack = [root];
|
|
119
63
|
let i = 0;
|
|
120
64
|
const top = () => stack[stack.length - 1];
|
|
65
|
+
let pending = 0;
|
|
66
|
+
const closesAt = /* @__PURE__ */ new Map();
|
|
67
|
+
const takeSynthetics = (offset, splice) => {
|
|
68
|
+
while (pending < synthetics.length) {
|
|
69
|
+
const synthetic = synthetics[pending];
|
|
70
|
+
if (synthetic.start > offset) return;
|
|
71
|
+
pending++;
|
|
72
|
+
if (!splice) continue;
|
|
73
|
+
const at = pos.at(synthetic.start);
|
|
74
|
+
const node = {
|
|
75
|
+
tag: synthetic.tag,
|
|
76
|
+
attrs: synthetic.attrs,
|
|
77
|
+
children: [],
|
|
78
|
+
parent: top(),
|
|
79
|
+
line: at.line,
|
|
80
|
+
column: at.column,
|
|
81
|
+
sourceLength: synthetic.sourceLength,
|
|
82
|
+
origin: "erb",
|
|
83
|
+
opaque: synthetic.opaque
|
|
84
|
+
};
|
|
85
|
+
top().children.push(node);
|
|
86
|
+
if (synthetic.container) {
|
|
87
|
+
closesAt.set(node, synthetic.end);
|
|
88
|
+
stack.push(node);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const closeSynthetics = (offset) => {
|
|
93
|
+
for (; ; ) {
|
|
94
|
+
let expired = -1;
|
|
95
|
+
for (let depth = stack.length - 1; depth >= 1; depth--) {
|
|
96
|
+
const end = closesAt.get(stack[depth]);
|
|
97
|
+
if (end !== void 0 && end <= offset) {
|
|
98
|
+
expired = depth;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (expired < 0) return;
|
|
103
|
+
stack.length = expired;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
121
106
|
while (i < len) {
|
|
107
|
+
closeSynthetics(i);
|
|
108
|
+
takeSynthetics(i, true);
|
|
122
109
|
if (ch(i) !== "<") {
|
|
123
110
|
i++;
|
|
124
111
|
continue;
|
|
@@ -127,11 +114,13 @@ function parseHtml(source) {
|
|
|
127
114
|
if (source.startsWith("<!--", i)) {
|
|
128
115
|
const end = source.indexOf("-->", i + 4);
|
|
129
116
|
i = end === -1 ? len : end + 3;
|
|
117
|
+
takeSynthetics(i - 1, false);
|
|
130
118
|
continue;
|
|
131
119
|
}
|
|
132
120
|
if (next === "!" || next === "?") {
|
|
133
121
|
const end = source.indexOf(">", i);
|
|
134
122
|
i = end === -1 ? len : end + 1;
|
|
123
|
+
takeSynthetics(i - 1, false);
|
|
135
124
|
continue;
|
|
136
125
|
}
|
|
137
126
|
if (next === "/") {
|
|
@@ -143,6 +132,7 @@ function parseHtml(source) {
|
|
|
143
132
|
}
|
|
144
133
|
const end = source.indexOf(">", j2);
|
|
145
134
|
i = end === -1 ? len : end + 1;
|
|
135
|
+
takeSynthetics(i - 1, false);
|
|
146
136
|
closeTag(stack, name.toLowerCase());
|
|
147
137
|
continue;
|
|
148
138
|
}
|
|
@@ -223,10 +213,13 @@ function parseHtml(source) {
|
|
|
223
213
|
children: [],
|
|
224
214
|
parent: top(),
|
|
225
215
|
line: startPos.line,
|
|
226
|
-
column: startPos.column
|
|
216
|
+
column: startPos.column,
|
|
217
|
+
origin: "markup",
|
|
218
|
+
opaque: false
|
|
227
219
|
};
|
|
228
220
|
top().children.push(node);
|
|
229
221
|
i = j;
|
|
222
|
+
takeSynthetics(i - 1, false);
|
|
230
223
|
if (selfClosing || VOID_ELEMENTS.has(lowerTag)) {
|
|
231
224
|
continue;
|
|
232
225
|
}
|
|
@@ -239,10 +232,13 @@ function parseHtml(source) {
|
|
|
239
232
|
const gt = source.indexOf(">", idx);
|
|
240
233
|
i = gt === -1 ? len : gt + 1;
|
|
241
234
|
}
|
|
235
|
+
takeSynthetics(i - 1, false);
|
|
242
236
|
continue;
|
|
243
237
|
}
|
|
244
238
|
stack.push(node);
|
|
245
239
|
}
|
|
240
|
+
closeSynthetics(len);
|
|
241
|
+
takeSynthetics(len, true);
|
|
246
242
|
return root;
|
|
247
243
|
}
|
|
248
244
|
function closeTag(stack, name) {
|
|
@@ -260,11 +256,699 @@ function walk(root, visit) {
|
|
|
260
256
|
}
|
|
261
257
|
}
|
|
262
258
|
|
|
259
|
+
// src/inspector/ruby_scan.ts
|
|
260
|
+
var SPACE = /* @__PURE__ */ new Set([" ", " ", "\n", "\r", "\f"]);
|
|
261
|
+
var IDENT_START = /[A-Za-z_]/;
|
|
262
|
+
var IDENT_CHAR = /[A-Za-z0-9_]/;
|
|
263
|
+
var ESCAPES = {
|
|
264
|
+
n: "\n",
|
|
265
|
+
t: " ",
|
|
266
|
+
r: "\r",
|
|
267
|
+
f: "\f",
|
|
268
|
+
v: "\v",
|
|
269
|
+
b: "\b",
|
|
270
|
+
a: "\x07",
|
|
271
|
+
e: "\x1B",
|
|
272
|
+
s: " "
|
|
273
|
+
};
|
|
274
|
+
var UNDECODED_ESCAPE = /[uxcCM0-7]/;
|
|
275
|
+
function readString(code, start) {
|
|
276
|
+
const quote = code[start];
|
|
277
|
+
const single = quote === "'";
|
|
278
|
+
const contentStart = start + 1;
|
|
279
|
+
let i = contentStart;
|
|
280
|
+
let value = "";
|
|
281
|
+
let verbatim = true;
|
|
282
|
+
while (i < code.length) {
|
|
283
|
+
const char = code[i];
|
|
284
|
+
if (char === quote) {
|
|
285
|
+
return { end: i + 1, value, literal: true, verbatim, contentStart, contentEnd: i };
|
|
286
|
+
}
|
|
287
|
+
if (char === "\\") {
|
|
288
|
+
const escaped = code[i + 1];
|
|
289
|
+
if (escaped === void 0) break;
|
|
290
|
+
verbatim = false;
|
|
291
|
+
if (single) {
|
|
292
|
+
value += escaped === "'" || escaped === "\\" ? escaped : `\\${escaped}`;
|
|
293
|
+
} else if (UNDECODED_ESCAPE.test(escaped)) {
|
|
294
|
+
return {
|
|
295
|
+
end: closingQuote(code, i + 2, quote),
|
|
296
|
+
value,
|
|
297
|
+
literal: false,
|
|
298
|
+
verbatim: false,
|
|
299
|
+
contentStart,
|
|
300
|
+
contentEnd: i
|
|
301
|
+
};
|
|
302
|
+
} else if (escaped !== "\n") {
|
|
303
|
+
value += ESCAPES[escaped] ?? escaped;
|
|
304
|
+
}
|
|
305
|
+
i += 2;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (!single && char === "#" && code[i + 1] === "{") {
|
|
309
|
+
const close = skipBraces(code, i + 1);
|
|
310
|
+
return {
|
|
311
|
+
end: closingQuote(code, close, quote),
|
|
312
|
+
value,
|
|
313
|
+
literal: false,
|
|
314
|
+
verbatim: false,
|
|
315
|
+
contentStart,
|
|
316
|
+
contentEnd: i
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
value += char;
|
|
320
|
+
i++;
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
end: code.length,
|
|
324
|
+
value,
|
|
325
|
+
literal: false,
|
|
326
|
+
verbatim: false,
|
|
327
|
+
contentStart,
|
|
328
|
+
contentEnd: code.length
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function closingQuote(code, from, quote) {
|
|
332
|
+
for (let i = from; i < code.length; i++) {
|
|
333
|
+
if (code[i] === "\\") {
|
|
334
|
+
i++;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (code[i] === quote) return i + 1;
|
|
338
|
+
}
|
|
339
|
+
return code.length;
|
|
340
|
+
}
|
|
341
|
+
function skipBraces(code, open) {
|
|
342
|
+
let depth = 0;
|
|
343
|
+
let i = open;
|
|
344
|
+
while (i < code.length) {
|
|
345
|
+
const char = code[i];
|
|
346
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
347
|
+
i = readString(code, i).end;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (char === "{") depth++;
|
|
351
|
+
else if (char === "}") {
|
|
352
|
+
depth--;
|
|
353
|
+
if (depth === 0) return i + 1;
|
|
354
|
+
}
|
|
355
|
+
i++;
|
|
356
|
+
}
|
|
357
|
+
return code.length;
|
|
358
|
+
}
|
|
359
|
+
function endOfLine(code, at) {
|
|
360
|
+
const newline = code.indexOf("\n", at);
|
|
361
|
+
return newline === -1 ? code.length : newline + 1;
|
|
362
|
+
}
|
|
363
|
+
function skipTrivia(code, at) {
|
|
364
|
+
let i = at;
|
|
365
|
+
while (i < code.length) {
|
|
366
|
+
const char = code[i];
|
|
367
|
+
if (SPACE.has(char)) i++;
|
|
368
|
+
else if (char === "#") i = endOfLine(code, i);
|
|
369
|
+
else break;
|
|
370
|
+
}
|
|
371
|
+
return i;
|
|
372
|
+
}
|
|
373
|
+
function previousSignificant(code, at) {
|
|
374
|
+
let i = at - 1;
|
|
375
|
+
while (i >= 0 && (code[i] === " " || code[i] === " ")) i--;
|
|
376
|
+
return i >= 0 ? code[i] : "";
|
|
377
|
+
}
|
|
378
|
+
var EXPRESSION_LEAD = /* @__PURE__ */ new Set([
|
|
379
|
+
"",
|
|
380
|
+
"\n",
|
|
381
|
+
"\r",
|
|
382
|
+
";",
|
|
383
|
+
"(",
|
|
384
|
+
"[",
|
|
385
|
+
"{",
|
|
386
|
+
",",
|
|
387
|
+
":",
|
|
388
|
+
"=",
|
|
389
|
+
"<",
|
|
390
|
+
">",
|
|
391
|
+
"!",
|
|
392
|
+
"&",
|
|
393
|
+
"|",
|
|
394
|
+
"^",
|
|
395
|
+
"~",
|
|
396
|
+
"+",
|
|
397
|
+
"-",
|
|
398
|
+
"*",
|
|
399
|
+
"/",
|
|
400
|
+
"%",
|
|
401
|
+
"?"
|
|
402
|
+
]);
|
|
403
|
+
function opensExpression(code, at) {
|
|
404
|
+
return EXPRESSION_LEAD.has(previousSignificant(code, at));
|
|
405
|
+
}
|
|
406
|
+
var PERCENT_TYPES = "qQwWiIrsx";
|
|
407
|
+
var DELIMITER_PAIRS = { "(": ")", "[": "]", "{": "}", "<": ">" };
|
|
408
|
+
function skipLiteral(code, at) {
|
|
409
|
+
const char = code[at];
|
|
410
|
+
if (char === void 0) return -1;
|
|
411
|
+
if (char === "%") {
|
|
412
|
+
const typed = PERCENT_TYPES.includes(code[at + 1] ?? "");
|
|
413
|
+
const open = typed ? code[at + 2] : code[at + 1];
|
|
414
|
+
if (open === void 0 || IDENT_CHAR.test(open) || SPACE.has(open)) return -1;
|
|
415
|
+
if (!typed && !opensExpression(code, at) && !SPACE.has(code[at - 1] ?? "")) return -1;
|
|
416
|
+
const end = closeDelimited(code, typed ? at + 3 : at + 2, open);
|
|
417
|
+
return end < 0 ? code.length : end;
|
|
418
|
+
}
|
|
419
|
+
if (char === "/" && opensExpression(code, at)) {
|
|
420
|
+
const end = closeDelimited(code, at + 1, "/");
|
|
421
|
+
if (end < 0) return -1;
|
|
422
|
+
let i = end;
|
|
423
|
+
while (i < code.length && /[imxounse]/.test(code[i])) i++;
|
|
424
|
+
return i;
|
|
425
|
+
}
|
|
426
|
+
if (char === "?" && opensExpression(code, at)) {
|
|
427
|
+
const next = code[at + 1];
|
|
428
|
+
if (next === void 0 || SPACE.has(next)) return -1;
|
|
429
|
+
if (next === "\\") return at + 3;
|
|
430
|
+
if (IDENT_CHAR.test(next) && IDENT_CHAR.test(code[at + 2] ?? "")) return -1;
|
|
431
|
+
return at + 2;
|
|
432
|
+
}
|
|
433
|
+
return -1;
|
|
434
|
+
}
|
|
435
|
+
function closeDelimited(code, from, open) {
|
|
436
|
+
const close = DELIMITER_PAIRS[open] ?? open;
|
|
437
|
+
const nests = close !== open;
|
|
438
|
+
let depth = 1;
|
|
439
|
+
for (let i = from; i < code.length; i++) {
|
|
440
|
+
const char = code[i];
|
|
441
|
+
if (char === "\\") {
|
|
442
|
+
i++;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (nests && char === open) depth++;
|
|
446
|
+
else if (char === close) {
|
|
447
|
+
depth--;
|
|
448
|
+
if (depth === 0) return i + 1;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return -1;
|
|
452
|
+
}
|
|
453
|
+
var HEREDOC_OPEN = /^<<([-~])?(["'`])?([A-Za-z_]\w*)/;
|
|
454
|
+
function maskHeredocs(code) {
|
|
455
|
+
let masked = code;
|
|
456
|
+
let i = 0;
|
|
457
|
+
while (i < masked.length) {
|
|
458
|
+
const char = masked[i];
|
|
459
|
+
if (char === "#") {
|
|
460
|
+
i = endOfLine(masked, i);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
464
|
+
i = readString(masked, i).end;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const literal = skipLiteral(masked, i);
|
|
468
|
+
if (literal > i) {
|
|
469
|
+
i = literal;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (char === "<" && masked[i + 1] === "<") {
|
|
473
|
+
const [matched, dash, quote, identifier] = HEREDOC_OPEN.exec(masked.slice(i)) ?? [];
|
|
474
|
+
const opensBody = identifier !== void 0 && (dash !== void 0 || quote !== void 0 || /^[A-Z]/.test(identifier) && opensExpression(masked, i));
|
|
475
|
+
const consumed = (matched?.length ?? 0) + (quote === void 0 ? 0 : 1);
|
|
476
|
+
if (opensBody && (quote === void 0 || masked[i + consumed - 1] === quote)) {
|
|
477
|
+
const bodyStart = endOfLine(masked, i);
|
|
478
|
+
const bodyEnd = heredocEnd(masked, bodyStart, identifier);
|
|
479
|
+
masked = masked.slice(0, bodyStart) + masked.slice(bodyStart, bodyEnd).replace(/[^\n]/g, " ") + masked.slice(bodyEnd);
|
|
480
|
+
i += consumed;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
i++;
|
|
485
|
+
}
|
|
486
|
+
return masked;
|
|
487
|
+
}
|
|
488
|
+
function heredocEnd(code, bodyStart, identifier) {
|
|
489
|
+
let line = bodyStart;
|
|
490
|
+
while (line < code.length) {
|
|
491
|
+
const next = endOfLine(code, line);
|
|
492
|
+
if (code.slice(line, next).trim() === identifier) return next;
|
|
493
|
+
line = next;
|
|
494
|
+
}
|
|
495
|
+
return code.length;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/inspector/ruby_hash.ts
|
|
499
|
+
function readDataOption(source, base) {
|
|
500
|
+
const code = maskHeredocs(source);
|
|
501
|
+
const attrs = [];
|
|
502
|
+
let found = false;
|
|
503
|
+
let opaque = false;
|
|
504
|
+
let i = 0;
|
|
505
|
+
while (i < code.length) {
|
|
506
|
+
const char = code[i];
|
|
507
|
+
if (char === "#") {
|
|
508
|
+
i = endOfLine(code, i);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
512
|
+
const literal = readString(code, i);
|
|
513
|
+
const value = char === "`" ? null : keySeparator(code, literal.end);
|
|
514
|
+
if (literal.literal && literal.value === "data" && value !== null) {
|
|
515
|
+
i = value;
|
|
516
|
+
} else {
|
|
517
|
+
i = literal.end;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
} else if (char === ":" && IDENT_START.test(code[i + 1] ?? "")) {
|
|
521
|
+
const name = readIdentifier(code, i + 1);
|
|
522
|
+
const value = hashRocket(code, name.end);
|
|
523
|
+
if (name.text === "data" && value !== null) {
|
|
524
|
+
i = value;
|
|
525
|
+
} else {
|
|
526
|
+
i = name.end;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
} else if (IDENT_START.test(char) && !IDENT_CHAR.test(code[i - 1] ?? "")) {
|
|
530
|
+
const name = readIdentifier(code, i);
|
|
531
|
+
const keyed = name.text === "data" && code[name.end] === ":" && code[name.end + 1] !== ":";
|
|
532
|
+
if (keyed) {
|
|
533
|
+
i = name.end + 1;
|
|
534
|
+
} else {
|
|
535
|
+
i = name.end;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
const literal = skipLiteral(code, i);
|
|
540
|
+
i = literal < 0 ? i + 1 : literal;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
found = true;
|
|
544
|
+
const start = skipTrivia(code, i);
|
|
545
|
+
if (code[start] === "{") {
|
|
546
|
+
const hash = readHash(code, start, base);
|
|
547
|
+
attrs.push(...hash.attrs);
|
|
548
|
+
if (hash.partial || !optionEnds(code, hash.end)) opaque = true;
|
|
549
|
+
i = hash.end;
|
|
550
|
+
} else {
|
|
551
|
+
opaque = true;
|
|
552
|
+
i = start;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return { attrs, found, opaque };
|
|
556
|
+
}
|
|
557
|
+
var OPTION_TAIL = /^(?:do|if|unless|while|until|rescue|then|and|or|end)\b/;
|
|
558
|
+
function optionEnds(code, at) {
|
|
559
|
+
const i = skipTrivia(code, at);
|
|
560
|
+
if (i >= code.length) return true;
|
|
561
|
+
const char = code[i];
|
|
562
|
+
if (char === "," || char === "}" || char === ")" || char === "]" || char === ";") return true;
|
|
563
|
+
return OPTION_TAIL.test(code.slice(i));
|
|
564
|
+
}
|
|
565
|
+
function readHash(code, open, base) {
|
|
566
|
+
const attrs = [];
|
|
567
|
+
let partial = false;
|
|
568
|
+
let i = open + 1;
|
|
569
|
+
for (; ; ) {
|
|
570
|
+
i = skipTrivia(code, i);
|
|
571
|
+
if (i >= code.length) {
|
|
572
|
+
partial = true;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
if (code[i] === "}") {
|
|
576
|
+
i++;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
if (code[i] === ",") {
|
|
580
|
+
i++;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const entry = readEntry(code, i, base);
|
|
584
|
+
if (entry.attr) attrs.push(entry.attr);
|
|
585
|
+
else partial = true;
|
|
586
|
+
i = entry.end;
|
|
587
|
+
}
|
|
588
|
+
return { attrs, partial, end: i };
|
|
589
|
+
}
|
|
590
|
+
function readEntry(code, start, base) {
|
|
591
|
+
const key = readKey(code, start);
|
|
592
|
+
if (key === null) return { attr: null, end: skipValue(code, start) };
|
|
593
|
+
const valueStart = skipTrivia(code, key.valueAt);
|
|
594
|
+
const name = attributeName(key.name);
|
|
595
|
+
if (name === null) return { attr: null, end: skipValue(code, valueStart) };
|
|
596
|
+
const anchor = { keyStart: base + start, keyEnd: base + key.end };
|
|
597
|
+
const quote = code[valueStart];
|
|
598
|
+
if (quote === '"' || quote === "'") {
|
|
599
|
+
const literal = readString(code, valueStart);
|
|
600
|
+
const after = skipTrivia(code, literal.end);
|
|
601
|
+
const whole = after >= code.length || code[after] === "," || code[after] === "}";
|
|
602
|
+
if (literal.literal && whole) {
|
|
603
|
+
return {
|
|
604
|
+
attr: {
|
|
605
|
+
name,
|
|
606
|
+
value: literal.value,
|
|
607
|
+
...anchor,
|
|
608
|
+
valueStart: literal.verbatim ? base + literal.contentStart : void 0,
|
|
609
|
+
valueEnd: literal.verbatim ? base + literal.contentEnd : void 0
|
|
610
|
+
},
|
|
611
|
+
end: literal.end
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const number = /^[+-]?\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?/.exec(
|
|
616
|
+
code.slice(valueStart)
|
|
617
|
+
)?.[0];
|
|
618
|
+
if (number !== void 0) {
|
|
619
|
+
const end = valueStart + number.length;
|
|
620
|
+
const after = skipTrivia(code, end);
|
|
621
|
+
const whole = after >= code.length || code[after] === "," || code[after] === "}";
|
|
622
|
+
if (whole) {
|
|
623
|
+
return {
|
|
624
|
+
attr: {
|
|
625
|
+
name,
|
|
626
|
+
value: number,
|
|
627
|
+
...anchor,
|
|
628
|
+
valueStart: base + valueStart,
|
|
629
|
+
valueEnd: base + end
|
|
630
|
+
},
|
|
631
|
+
end
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return { attr: { name, value: null, ...anchor }, end: skipValue(code, valueStart) };
|
|
636
|
+
}
|
|
637
|
+
function readKey(code, start) {
|
|
638
|
+
const char = code[start];
|
|
639
|
+
if (char === '"' || char === "'") {
|
|
640
|
+
const literal = readString(code, start);
|
|
641
|
+
const valueAt = keySeparator(code, literal.end);
|
|
642
|
+
if (!literal.literal || valueAt === null) return null;
|
|
643
|
+
return { name: literal.value, end: literal.end, valueAt };
|
|
644
|
+
}
|
|
645
|
+
if (char === ":" && IDENT_START.test(code[start + 1] ?? "")) {
|
|
646
|
+
const name = readIdentifier(code, start + 1);
|
|
647
|
+
const valueAt = hashRocket(code, name.end);
|
|
648
|
+
return valueAt === null ? null : { name: name.text, end: name.end, valueAt };
|
|
649
|
+
}
|
|
650
|
+
if (char !== void 0 && IDENT_START.test(char)) {
|
|
651
|
+
const name = readIdentifier(code, start);
|
|
652
|
+
if (code[name.end] !== ":" || code[name.end + 1] === ":") return null;
|
|
653
|
+
return { name: name.text, end: name.end, valueAt: name.end + 1 };
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
function attributeName(key) {
|
|
658
|
+
const name = key.replaceAll("_", "-").toLowerCase();
|
|
659
|
+
return name.length > 0 && !/[\s"'=<>/]/.test(name) ? `data-${name}` : null;
|
|
660
|
+
}
|
|
661
|
+
function keySeparator(code, at) {
|
|
662
|
+
if (code[at] === ":" && code[at + 1] !== ":") return at + 1;
|
|
663
|
+
return hashRocket(code, at);
|
|
664
|
+
}
|
|
665
|
+
function hashRocket(code, at) {
|
|
666
|
+
const arrow = skipTrivia(code, at);
|
|
667
|
+
return code.startsWith("=>", arrow) ? arrow + 2 : null;
|
|
668
|
+
}
|
|
669
|
+
function readIdentifier(code, start) {
|
|
670
|
+
let end = start;
|
|
671
|
+
while (end < code.length && IDENT_CHAR.test(code[end])) end++;
|
|
672
|
+
return { text: code.slice(start, end), end };
|
|
673
|
+
}
|
|
674
|
+
function skipValue(code, start) {
|
|
675
|
+
let depth = 0;
|
|
676
|
+
let i = start;
|
|
677
|
+
while (i < code.length) {
|
|
678
|
+
const char = code[i];
|
|
679
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
680
|
+
i = readString(code, i).end;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (char === "#") {
|
|
684
|
+
i = endOfLine(code, i);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const literal = skipLiteral(code, i);
|
|
688
|
+
if (literal >= 0) {
|
|
689
|
+
i = literal;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (char === "(" || char === "[" || char === "{") depth++;
|
|
693
|
+
else if (char === ")" || char === "]") depth--;
|
|
694
|
+
else if (char === "}") {
|
|
695
|
+
if (depth === 0) return i;
|
|
696
|
+
depth--;
|
|
697
|
+
} else if (char === "," && depth === 0) return i;
|
|
698
|
+
i++;
|
|
699
|
+
}
|
|
700
|
+
return code.length;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/inspector/erb.ts
|
|
704
|
+
var ERB_TAG = /<%[\s\S]*?%>/g;
|
|
705
|
+
function neutralizeErb(source) {
|
|
706
|
+
return source.replace(ERB_TAG, (match) => match.replace(/[^\n]/g, " "));
|
|
707
|
+
}
|
|
708
|
+
function erbRanges(source) {
|
|
709
|
+
return [...source.matchAll(ERB_TAG)].map(
|
|
710
|
+
(match) => [match.index ?? 0, (match.index ?? 0) + match[0].length]
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
function erbElements(source) {
|
|
714
|
+
const tags = readErbTags(source);
|
|
715
|
+
const blockEnds = matchBlocks(tags);
|
|
716
|
+
const index = new PositionIndex(source);
|
|
717
|
+
const elements = [];
|
|
718
|
+
for (const [ordinal, tag] of tags.entries()) {
|
|
719
|
+
if (!tag.output) continue;
|
|
720
|
+
const option = readDataOption(tag.code, tag.codeStart);
|
|
721
|
+
if (!option.found) continue;
|
|
722
|
+
const end = blockEnds.get(ordinal) ?? tag.end;
|
|
723
|
+
elements.push({
|
|
724
|
+
tag: ERB_ELEMENT_TAG,
|
|
725
|
+
attrs: option.attrs.map((attr) => toParsedAttr(attr, index)),
|
|
726
|
+
start: tag.start,
|
|
727
|
+
// The token a reader can act on is the delimiter that opens the call.
|
|
728
|
+
sourceLength: tag.codeStart - tag.start,
|
|
729
|
+
end,
|
|
730
|
+
container: end > tag.end,
|
|
731
|
+
opaque: option.opaque
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
return elements;
|
|
735
|
+
}
|
|
736
|
+
function toParsedAttr(attr, index) {
|
|
737
|
+
const at = index.at(attr.keyStart);
|
|
738
|
+
return {
|
|
739
|
+
name: attr.name,
|
|
740
|
+
value: attr.value ?? "",
|
|
741
|
+
line: at.line,
|
|
742
|
+
column: at.column,
|
|
743
|
+
sourceLength: attr.keyEnd - attr.keyStart,
|
|
744
|
+
valueStart: attr.valueStart,
|
|
745
|
+
valueEnd: attr.valueEnd,
|
|
746
|
+
dynamicValue: attr.value === null
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function readErbTags(source) {
|
|
750
|
+
const tags = [];
|
|
751
|
+
for (const match of source.matchAll(ERB_TAG)) {
|
|
752
|
+
const raw = match[0];
|
|
753
|
+
const start = match.index ?? 0;
|
|
754
|
+
let open = 2;
|
|
755
|
+
let output = false;
|
|
756
|
+
if (raw[open] === "#") continue;
|
|
757
|
+
if (raw[open] === "=") {
|
|
758
|
+
output = true;
|
|
759
|
+
open++;
|
|
760
|
+
if (raw[open] === "=") open++;
|
|
761
|
+
} else if (raw[open] === "-") {
|
|
762
|
+
open++;
|
|
763
|
+
}
|
|
764
|
+
let close = raw.length - 2;
|
|
765
|
+
if (raw[close - 1] === "-") close--;
|
|
766
|
+
if (close < open) close = open;
|
|
767
|
+
tags.push({
|
|
768
|
+
start,
|
|
769
|
+
end: start + raw.length,
|
|
770
|
+
output,
|
|
771
|
+
code: raw.slice(open, close),
|
|
772
|
+
codeStart: start + open
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return tags;
|
|
776
|
+
}
|
|
777
|
+
function matchBlocks(tags) {
|
|
778
|
+
const ends = /* @__PURE__ */ new Map();
|
|
779
|
+
const open = [];
|
|
780
|
+
for (const [ordinal, tag] of tags.entries()) {
|
|
781
|
+
for (const event of blockEvents(tag.code)) {
|
|
782
|
+
if (event === "open") {
|
|
783
|
+
open.push(ordinal);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const opener = open.pop();
|
|
787
|
+
if (opener !== void 0) ends.set(opener, tag.end);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return ends;
|
|
791
|
+
}
|
|
792
|
+
var BLOCK_OPENERS = /* @__PURE__ */ new Set(["case", "begin", "def", "class", "module", "for"]);
|
|
793
|
+
var BLOCK_MODIFIERS = /* @__PURE__ */ new Set(["if", "unless", "while", "until"]);
|
|
794
|
+
var BLOCK_CONTINUERS = /* @__PURE__ */ new Set(["elsif", "else", "when", "in", "then", "rescue", "ensure"]);
|
|
795
|
+
function blockEvents(source) {
|
|
796
|
+
const code = maskHeredocs(source);
|
|
797
|
+
const events = [];
|
|
798
|
+
let previous = "";
|
|
799
|
+
let loopHeader = false;
|
|
800
|
+
let i = 0;
|
|
801
|
+
while (i < code.length) {
|
|
802
|
+
const char = code[i];
|
|
803
|
+
if (char === "\n" || char === ";") {
|
|
804
|
+
previous = char;
|
|
805
|
+
loopHeader = false;
|
|
806
|
+
i++;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (SPACE.has(char)) {
|
|
810
|
+
i++;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (char === "#") {
|
|
814
|
+
i = endOfLine(code, i);
|
|
815
|
+
previous = "\n";
|
|
816
|
+
loopHeader = false;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
820
|
+
i = readString(code, i).end;
|
|
821
|
+
previous = "value";
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (char === ":" && IDENT_START.test(code[i + 1] ?? "")) {
|
|
825
|
+
i = identifierEnd(code, i + 1);
|
|
826
|
+
previous = "value";
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const literal = skipLiteral(code, i);
|
|
830
|
+
if (literal >= 0) {
|
|
831
|
+
i = literal;
|
|
832
|
+
previous = "value";
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (char === "{") {
|
|
836
|
+
events.push("open");
|
|
837
|
+
previous = "{";
|
|
838
|
+
i++;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (char === "}") {
|
|
842
|
+
events.push("close");
|
|
843
|
+
previous = "value";
|
|
844
|
+
i++;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
if (char === ")" || char === "]") {
|
|
848
|
+
previous = "value";
|
|
849
|
+
i++;
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (!IDENT_START.test(char)) {
|
|
853
|
+
previous = char;
|
|
854
|
+
i++;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const end = identifierEnd(code, i);
|
|
858
|
+
const word = code.slice(i, end);
|
|
859
|
+
const keyword = !(code[end] === ":" && code[end + 1] !== ":") && code[i - 1] !== ".";
|
|
860
|
+
i = end;
|
|
861
|
+
if (keyword && word === "end") {
|
|
862
|
+
events.push("close");
|
|
863
|
+
previous = "value";
|
|
864
|
+
} else if (keyword && word === "do") {
|
|
865
|
+
if (loopHeader) loopHeader = false;
|
|
866
|
+
else events.push("open");
|
|
867
|
+
previous = "do";
|
|
868
|
+
} else if (keyword && BLOCK_OPENERS.has(word)) {
|
|
869
|
+
events.push("open");
|
|
870
|
+
loopHeader = word === "for";
|
|
871
|
+
previous = word;
|
|
872
|
+
} else if (keyword && BLOCK_MODIFIERS.has(word)) {
|
|
873
|
+
if (previous !== "value") {
|
|
874
|
+
events.push("open");
|
|
875
|
+
loopHeader = word !== "if" && word !== "unless";
|
|
876
|
+
}
|
|
877
|
+
previous = word;
|
|
878
|
+
} else if (keyword && BLOCK_CONTINUERS.has(word)) {
|
|
879
|
+
previous = word;
|
|
880
|
+
} else {
|
|
881
|
+
previous = "value";
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return events;
|
|
885
|
+
}
|
|
886
|
+
function identifierEnd(code, start) {
|
|
887
|
+
let end = start;
|
|
888
|
+
while (end < code.length && IDENT_CHAR.test(code[end])) end++;
|
|
889
|
+
return end;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/inspector/extract.ts
|
|
893
|
+
var NAMESPACE_PREFIX = "stimeo--";
|
|
894
|
+
function dasherize(value) {
|
|
895
|
+
return value.replace(/([A-Z])/g, (_match, char) => `-${char.toLowerCase()}`);
|
|
896
|
+
}
|
|
897
|
+
function controllerIdentifiers(dataControllerValue) {
|
|
898
|
+
const seen = /* @__PURE__ */ new Set();
|
|
899
|
+
for (const token of dataControllerValue.split(/\s+/)) {
|
|
900
|
+
if (token.startsWith(NAMESPACE_PREFIX)) seen.add(token);
|
|
901
|
+
}
|
|
902
|
+
return [...seen];
|
|
903
|
+
}
|
|
904
|
+
function parseTargetAttr(attrName) {
|
|
905
|
+
const match = /^data-(stimeo--[a-z0-9][a-z0-9-]*)-target$/.exec(attrName);
|
|
906
|
+
return match?.[1] ?? null;
|
|
907
|
+
}
|
|
908
|
+
function parseValueAttr(attrName, knownIdentifiers) {
|
|
909
|
+
const match = /^data-(stimeo--[a-z0-9-]+)-value$/.exec(attrName);
|
|
910
|
+
if (!match) return null;
|
|
911
|
+
const inner = match[1] ?? "";
|
|
912
|
+
let best = null;
|
|
913
|
+
for (const id of knownIdentifiers) {
|
|
914
|
+
if (inner.startsWith(`${id}-`)) {
|
|
915
|
+
const valueToken = inner.slice(id.length + 1);
|
|
916
|
+
if (valueToken.length > 0 && (!best || id.length > best.identifier.length)) {
|
|
917
|
+
best = { identifier: id, valueToken };
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (best) return best;
|
|
922
|
+
const rest = inner.slice(NAMESPACE_PREFIX.length);
|
|
923
|
+
const dash = rest.indexOf("-");
|
|
924
|
+
const identifierGuess = dash === -1 ? inner : `${NAMESPACE_PREFIX}${rest.slice(0, dash)}`;
|
|
925
|
+
const valueGuess = dash === -1 ? "" : rest.slice(dash + 1);
|
|
926
|
+
return { identifier: null, valueToken: valueGuess || identifierGuess };
|
|
927
|
+
}
|
|
928
|
+
function actionDescriptors(dataActionValue) {
|
|
929
|
+
const descriptors = [];
|
|
930
|
+
for (const descriptor of dataActionValue.split(/\s+/)) {
|
|
931
|
+
const hash = descriptor.indexOf("#");
|
|
932
|
+
if (hash === -1) continue;
|
|
933
|
+
let lhs = descriptor.slice(0, hash);
|
|
934
|
+
const arrow = lhs.indexOf("->");
|
|
935
|
+
if (arrow !== -1) lhs = lhs.slice(arrow + 2);
|
|
936
|
+
if (!lhs.startsWith(NAMESPACE_PREFIX)) continue;
|
|
937
|
+
const method = /^[a-zA-Z_$][\w$]*/.exec(descriptor.slice(hash + 1))?.[0] ?? "";
|
|
938
|
+
descriptors.push({ identifier: lhs, method });
|
|
939
|
+
}
|
|
940
|
+
return descriptors;
|
|
941
|
+
}
|
|
942
|
+
function isStimeoDataAttr(attrName) {
|
|
943
|
+
return attrName.startsWith(`data-${NAMESPACE_PREFIX}`);
|
|
944
|
+
}
|
|
945
|
+
|
|
263
946
|
// src/inspector/types.ts
|
|
264
947
|
var DIAGNOSTIC_CODES = [
|
|
265
948
|
"unknown-controller",
|
|
266
949
|
"unknown-target",
|
|
267
950
|
"unknown-value",
|
|
951
|
+
"invalid-value",
|
|
268
952
|
"unknown-action-controller",
|
|
269
953
|
"unknown-action-method",
|
|
270
954
|
"orphan-target",
|
|
@@ -273,6 +957,7 @@ var DIAGNOSTIC_CODES = [
|
|
|
273
957
|
"missing-aria",
|
|
274
958
|
"invalid-aria-value",
|
|
275
959
|
"keyboard-inaccessible",
|
|
960
|
+
"invalid-host",
|
|
276
961
|
"unresolved-idref",
|
|
277
962
|
"managed-aria",
|
|
278
963
|
"composition-mismatch",
|
|
@@ -286,16 +971,15 @@ var DIAGNOSTIC_CODES = [
|
|
|
286
971
|
// src/inspector/check.ts
|
|
287
972
|
function checkSource(source, manifest) {
|
|
288
973
|
const diagnostics = [];
|
|
289
|
-
const tree = parseHtml(neutralizeErb(source));
|
|
974
|
+
const tree = parseHtml(neutralizeErb(source), erbElements(source));
|
|
290
975
|
const known = manifest.controllers;
|
|
291
976
|
const knownIdentifiers = Object.keys(known);
|
|
292
|
-
const
|
|
293
|
-
(m) => [m.index ?? 0, (m.index ?? 0) + m[0].length]
|
|
294
|
-
);
|
|
977
|
+
const ranges = erbRanges(source);
|
|
295
978
|
const isDynamicValue = (attr) => {
|
|
979
|
+
if (attr.dynamicValue !== void 0) return attr.dynamicValue;
|
|
296
980
|
const { valueStart, valueEnd } = attr;
|
|
297
981
|
if (valueStart === void 0 || valueEnd === void 0) return false;
|
|
298
|
-
return
|
|
982
|
+
return ranges.some(([start, end]) => start < valueEnd && end > valueStart);
|
|
299
983
|
};
|
|
300
984
|
const conditionHolds = (scope, identifier, when) => {
|
|
301
985
|
if (!when) return true;
|
|
@@ -305,6 +989,36 @@ function checkSource(source, manifest) {
|
|
|
305
989
|
if (hostAttr && isDynamicValue(hostAttr)) return null;
|
|
306
990
|
return when.equals.includes(hostAttr ? hostAttr.value.trim() : when.default);
|
|
307
991
|
};
|
|
992
|
+
const isContentEditable2 = (element) => {
|
|
993
|
+
let current = element;
|
|
994
|
+
while (current && current.tag !== "#root") {
|
|
995
|
+
const attr = current.attrs.find((candidate) => candidate.name === "contenteditable");
|
|
996
|
+
if (attr) {
|
|
997
|
+
if (isDynamicValue(attr)) return null;
|
|
998
|
+
const value = attr.value.trim().toLowerCase();
|
|
999
|
+
if (value === "false") return false;
|
|
1000
|
+
if (value === "" || value === "true" || value === "plaintext-only") return true;
|
|
1001
|
+
}
|
|
1002
|
+
current = current.parent;
|
|
1003
|
+
}
|
|
1004
|
+
return false;
|
|
1005
|
+
};
|
|
1006
|
+
const hostIsAllowed = (element, rule) => {
|
|
1007
|
+
if (element.tag === "button") {
|
|
1008
|
+
if (rule.mode === "non-interactive") return false;
|
|
1009
|
+
const type = element.attrs.find((attr) => attr.name === "type");
|
|
1010
|
+
if (type && isDynamicValue(type)) return null;
|
|
1011
|
+
const effectiveType = type?.value.trim().toLowerCase() || "submit";
|
|
1012
|
+
return (rule.buttonTypes ?? []).includes(effectiveType);
|
|
1013
|
+
}
|
|
1014
|
+
if (ALWAYS_INTERACTIVE_HOSTS.has(element.tag)) return false;
|
|
1015
|
+
if ((element.tag === "a" || element.tag === "area") && hasAttr(element, "href")) return false;
|
|
1016
|
+
if ((element.tag === "audio" || element.tag === "video") && hasAttr(element, "controls")) {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
const editable = isContentEditable2(element);
|
|
1020
|
+
return editable === null ? null : !editable;
|
|
1021
|
+
};
|
|
308
1022
|
const suppressed = (node, code) => {
|
|
309
1023
|
if (code === "unknown-ignore-code") return false;
|
|
310
1024
|
let current = node;
|
|
@@ -312,7 +1026,9 @@ function checkSource(source, manifest) {
|
|
|
312
1026
|
const ignore = current.attrs.find((a) => a.name === IGNORE_ATTR);
|
|
313
1027
|
if (ignore) {
|
|
314
1028
|
const codes = ignore.value.trim();
|
|
315
|
-
if (codes.length === 0
|
|
1029
|
+
if (codes.length === 0) {
|
|
1030
|
+
if (!isDynamicValue(ignore)) return true;
|
|
1031
|
+
} else if (codes.split(/\s+/).includes(code)) return true;
|
|
316
1032
|
}
|
|
317
1033
|
current = current.parent;
|
|
318
1034
|
}
|
|
@@ -321,6 +1037,26 @@ function checkSource(source, manifest) {
|
|
|
321
1037
|
const report = (owner, code, severity, message, at, suggestion, fix) => {
|
|
322
1038
|
if (!suppressed(owner, code)) push(diagnostics, code, severity, message, at, suggestion, fix);
|
|
323
1039
|
};
|
|
1040
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
1041
|
+
const unnamedTargets = [];
|
|
1042
|
+
const targetsUnreadable = (container, scope, identifier) => {
|
|
1043
|
+
for (const node of unresolved) {
|
|
1044
|
+
if (isWithin(node, container) && findOwner(node, identifier) === scope) return true;
|
|
1045
|
+
}
|
|
1046
|
+
for (const { node, identifier: hidden } of unnamedTargets) {
|
|
1047
|
+
if (hidden !== identifier) continue;
|
|
1048
|
+
if (isWithin(node, container) && findOwner(node, identifier) === scope) return true;
|
|
1049
|
+
}
|
|
1050
|
+
return false;
|
|
1051
|
+
};
|
|
1052
|
+
const unresolvedAbove = (node) => {
|
|
1053
|
+
let current = node;
|
|
1054
|
+
while (current && current.tag !== "#root") {
|
|
1055
|
+
if (unresolved.has(current)) return true;
|
|
1056
|
+
current = current.parent;
|
|
1057
|
+
}
|
|
1058
|
+
return false;
|
|
1059
|
+
};
|
|
324
1060
|
const scopes = [];
|
|
325
1061
|
const presence = /* @__PURE__ */ new Map();
|
|
326
1062
|
const targetNodes = /* @__PURE__ */ new Map();
|
|
@@ -375,6 +1111,10 @@ function checkSource(source, manifest) {
|
|
|
375
1111
|
}
|
|
376
1112
|
const declarationCandidates = [];
|
|
377
1113
|
walk(tree, (node) => {
|
|
1114
|
+
const declaredController = node.attrs.find((a) => a.name === "data-controller");
|
|
1115
|
+
if (node.opaque || declaredController && isDynamicValue(declaredController)) {
|
|
1116
|
+
unresolved.add(node);
|
|
1117
|
+
}
|
|
378
1118
|
if (declarationAttrs.size > 0 && node.attrs.some((a) => declarationAttrs.has(a.name))) {
|
|
379
1119
|
declarationCandidates.push(node);
|
|
380
1120
|
}
|
|
@@ -386,6 +1126,7 @@ function checkSource(source, manifest) {
|
|
|
386
1126
|
focusableRoleTally.set(role, (focusableRoleTally.get(role) ?? 0) + 1);
|
|
387
1127
|
}
|
|
388
1128
|
}
|
|
1129
|
+
const authoredValueIdentifiers = /* @__PURE__ */ new Set();
|
|
389
1130
|
for (const attr of node.attrs) {
|
|
390
1131
|
if (attr.name === IGNORE_ATTR) {
|
|
391
1132
|
for (const token of attr.value.split(/\s+/).filter((t) => t.length > 0)) {
|
|
@@ -482,14 +1223,18 @@ function checkSource(source, manifest) {
|
|
|
482
1223
|
if (targetName.length > 0) {
|
|
483
1224
|
recordPresence(owner, targetIdentifier, targetName);
|
|
484
1225
|
recordTargetNode(owner, targetIdentifier, targetName, node);
|
|
1226
|
+
} else if (isDynamicValue(attr)) {
|
|
1227
|
+
unnamedTargets.push({ node, identifier: targetIdentifier });
|
|
485
1228
|
}
|
|
486
1229
|
} else if (!inDeclaredFragment(node, targetIdentifier)) {
|
|
1230
|
+
const uncertain = unresolvedAbove(node);
|
|
487
1231
|
report(
|
|
488
1232
|
node,
|
|
489
1233
|
"orphan-target",
|
|
490
|
-
"error",
|
|
1234
|
+
uncertain ? "warning" : "error",
|
|
491
1235
|
`Target "${attr.name}" has no enclosing controller "${targetIdentifier}".`,
|
|
492
|
-
attr
|
|
1236
|
+
attr,
|
|
1237
|
+
uncertain ? UNRESOLVED_HOST_HINT : void 0
|
|
493
1238
|
);
|
|
494
1239
|
}
|
|
495
1240
|
continue;
|
|
@@ -506,6 +1251,7 @@ function checkSource(source, manifest) {
|
|
|
506
1251
|
attr
|
|
507
1252
|
);
|
|
508
1253
|
} else {
|
|
1254
|
+
authoredValueIdentifiers.add(parsed.identifier);
|
|
509
1255
|
const controller = known[parsed.identifier];
|
|
510
1256
|
const valid = controller?.values.some((v) => dasherize(v) === parsed.valueToken) ?? false;
|
|
511
1257
|
if (!valid) {
|
|
@@ -517,6 +1263,20 @@ function checkSource(source, manifest) {
|
|
|
517
1263
|
attr,
|
|
518
1264
|
didYouMean(parsed.valueToken, (controller?.values ?? []).map(dasherize))
|
|
519
1265
|
);
|
|
1266
|
+
} else if (!isDynamicValue(attr)) {
|
|
1267
|
+
const constraint = controller?.valueConstraints.find(
|
|
1268
|
+
(candidate) => dasherize(candidate.value) === parsed.valueToken
|
|
1269
|
+
);
|
|
1270
|
+
if (constraint && !valueSatisfiesConstraint(attr.value, constraint)) {
|
|
1271
|
+
report(
|
|
1272
|
+
node,
|
|
1273
|
+
"invalid-value",
|
|
1274
|
+
"error",
|
|
1275
|
+
`Invalid value "${attr.value.trim()}" for "${parsed.identifier}.${parsed.valueToken}". Expected ${describeValueConstraint(constraint)}.`,
|
|
1276
|
+
attr,
|
|
1277
|
+
constraint.suggestion
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
520
1280
|
}
|
|
521
1281
|
}
|
|
522
1282
|
continue;
|
|
@@ -551,19 +1311,50 @@ function checkSource(source, manifest) {
|
|
|
551
1311
|
}
|
|
552
1312
|
}
|
|
553
1313
|
}
|
|
1314
|
+
for (const identifier of authoredValueIdentifiers) {
|
|
1315
|
+
const controller = known[identifier];
|
|
1316
|
+
if (!controller) continue;
|
|
1317
|
+
for (const relation of controller.valueRelations) {
|
|
1318
|
+
const leftAttr = node.attrs.find(
|
|
1319
|
+
(attr) => attr.name === `data-${identifier}-${dasherize(relation.left)}-value`
|
|
1320
|
+
);
|
|
1321
|
+
const rightAttr = node.attrs.find(
|
|
1322
|
+
(attr) => attr.name === `data-${identifier}-${dasherize(relation.right)}-value`
|
|
1323
|
+
);
|
|
1324
|
+
const at = rightAttr ?? leftAttr;
|
|
1325
|
+
if (!at || leftAttr && isDynamicValue(leftAttr) || rightAttr && isDynamicValue(rightAttr)) {
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const left = leftAttr ? decodeNumericValue(leftAttr.value) : relation.leftDefault;
|
|
1329
|
+
const right = rightAttr ? decodeNumericValue(rightAttr.value) : relation.rightDefault;
|
|
1330
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) continue;
|
|
1331
|
+
if (relation.operator === "less-than-or-equal" && left <= right) continue;
|
|
1332
|
+
report(
|
|
1333
|
+
node,
|
|
1334
|
+
"invalid-value",
|
|
1335
|
+
"error",
|
|
1336
|
+
`Invalid values for "${identifier}": "${relation.left}" (${left}) must be less than or equal to "${relation.right}" (${right}).`,
|
|
1337
|
+
at,
|
|
1338
|
+
relation.suggestion
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
554
1342
|
});
|
|
1343
|
+
const scopeIsComplete = (container, scope, identifier) => container.origin === "markup" && !targetsUnreadable(container, scope, identifier);
|
|
555
1344
|
for (const { node, identifier } of scopes) {
|
|
556
1345
|
const controller = known[identifier];
|
|
557
1346
|
if (!controller) continue;
|
|
1347
|
+
const complete = scopeIsComplete(node, node, identifier);
|
|
558
1348
|
const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
|
|
559
1349
|
for (const required of controller.requiredTargets) {
|
|
560
1350
|
if (!present.has(required)) {
|
|
561
1351
|
report(
|
|
562
1352
|
node,
|
|
563
1353
|
"missing-required-target",
|
|
564
|
-
"error",
|
|
1354
|
+
complete ? "error" : "warning",
|
|
565
1355
|
`"${identifier}" is missing required target "${required}".`,
|
|
566
|
-
node
|
|
1356
|
+
node,
|
|
1357
|
+
complete ? void 0 : UNRESOLVED_TARGET_HINT
|
|
567
1358
|
);
|
|
568
1359
|
}
|
|
569
1360
|
}
|
|
@@ -571,6 +1362,7 @@ function checkSource(source, manifest) {
|
|
|
571
1362
|
for (const { node, identifier } of scopes) {
|
|
572
1363
|
const controller = known[identifier];
|
|
573
1364
|
if (!controller) continue;
|
|
1365
|
+
const complete = scopeIsComplete(node, node, identifier);
|
|
574
1366
|
const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
|
|
575
1367
|
for (const rule of controller.conditionalTargets) {
|
|
576
1368
|
if (!present.has(rule.whenPresent)) continue;
|
|
@@ -579,10 +1371,10 @@ function checkSource(source, manifest) {
|
|
|
579
1371
|
report(
|
|
580
1372
|
node,
|
|
581
1373
|
"missing-conditional-target",
|
|
582
|
-
"error",
|
|
1374
|
+
complete ? "error" : "warning",
|
|
583
1375
|
`"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
|
|
584
1376
|
node,
|
|
585
|
-
rule.suggestion
|
|
1377
|
+
complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
|
|
586
1378
|
);
|
|
587
1379
|
}
|
|
588
1380
|
}
|
|
@@ -595,6 +1387,7 @@ function checkSource(source, manifest) {
|
|
|
595
1387
|
if (req.whenDocument && !documentHolds(req.whenDocument)) continue;
|
|
596
1388
|
const severity = documentHolds(req.escalateWhen) ? "error" : req.severity ?? "error";
|
|
597
1389
|
for (const el of elements) {
|
|
1390
|
+
if (!hasReadableMarkup(el)) continue;
|
|
598
1391
|
if (!contentHolds(node, identifier, el, req.whenContains)) continue;
|
|
599
1392
|
if (!elementHolds(el, req.whenElement)) continue;
|
|
600
1393
|
checkA11y(report, el, identifier, req, severity);
|
|
@@ -608,6 +1401,7 @@ function checkSource(source, manifest) {
|
|
|
608
1401
|
const reach = req.reach ?? "tab";
|
|
609
1402
|
const elements = targetNodes.get(node)?.get(identifier)?.get(req.target) ?? [];
|
|
610
1403
|
for (const el of elements) {
|
|
1404
|
+
if (!hasReadableMarkup(el)) continue;
|
|
611
1405
|
if (!isFocusable(el, reach)) {
|
|
612
1406
|
const problem = reach === "tab" ? 'not in the Tab order (needs a native control or tabindex="0")' : "not focusable (needs a native control or tabindex)";
|
|
613
1407
|
report(
|
|
@@ -622,6 +1416,28 @@ function checkSource(source, manifest) {
|
|
|
622
1416
|
}
|
|
623
1417
|
}
|
|
624
1418
|
}
|
|
1419
|
+
for (const { node, identifier } of scopes) {
|
|
1420
|
+
const controller = known[identifier];
|
|
1421
|
+
if (!controller) continue;
|
|
1422
|
+
for (const rule of controller.hosts) {
|
|
1423
|
+
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
1424
|
+
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
1425
|
+
for (const element of elements) {
|
|
1426
|
+
if (!hasReadableMarkup(element)) continue;
|
|
1427
|
+
if (hostIsAllowed(element, rule) !== false) continue;
|
|
1428
|
+
const type = element.attrs.find((attr) => attr.name === "type");
|
|
1429
|
+
const detail = element.tag === "button" ? `the unsupported ${type ? `type="${type.value}"` : 'implicit type="submit"'}` : `an unsupported interactive <${element.tag}>`;
|
|
1430
|
+
report(
|
|
1431
|
+
element,
|
|
1432
|
+
"invalid-host",
|
|
1433
|
+
"error",
|
|
1434
|
+
`The ${where} of "${identifier}" uses ${detail} host.`,
|
|
1435
|
+
type ?? element,
|
|
1436
|
+
rule.suggestion
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
625
1441
|
for (const { node, identifier } of scopes) {
|
|
626
1442
|
const controller = known[identifier];
|
|
627
1443
|
if (!controller) continue;
|
|
@@ -629,6 +1445,7 @@ function checkSource(source, manifest) {
|
|
|
629
1445
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
630
1446
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
631
1447
|
for (const el of elements) {
|
|
1448
|
+
if (!hasReadableMarkup(el)) continue;
|
|
632
1449
|
for (const name of rule.attrs) {
|
|
633
1450
|
const attr = el.attrs.find((a) => a.name === name);
|
|
634
1451
|
if (!attr) continue;
|
|
@@ -651,6 +1468,7 @@ function checkSource(source, manifest) {
|
|
|
651
1468
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
652
1469
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
653
1470
|
for (const el of elements) {
|
|
1471
|
+
if (!hasReadableMarkup(el)) continue;
|
|
654
1472
|
if (!contentHolds(node, identifier, el, rule.whenContains)) continue;
|
|
655
1473
|
for (const name of rule.attrs) {
|
|
656
1474
|
const attr = el.attrs.find((a) => a.name === name);
|
|
@@ -681,6 +1499,7 @@ function checkSource(source, manifest) {
|
|
|
681
1499
|
if (conditionHolds(node, identifier, when) !== true) continue;
|
|
682
1500
|
const hostContext = when ? ` (${when.value} ${list(when.equals)})` : "";
|
|
683
1501
|
for (const el of elements) {
|
|
1502
|
+
if (el.opaque) continue;
|
|
684
1503
|
const declared = el.attrs.find((a) => a.name === "data-controller");
|
|
685
1504
|
if (!declared || !controllerIdentifiers(declared.value).includes(rule.coController)) {
|
|
686
1505
|
continue;
|
|
@@ -721,6 +1540,7 @@ function checkSource(source, manifest) {
|
|
|
721
1540
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
722
1541
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
723
1542
|
for (const el of elements) {
|
|
1543
|
+
if (el.opaque) continue;
|
|
724
1544
|
const declared = el.attrs.find((a) => a.name === "data-controller");
|
|
725
1545
|
if (declared && isDynamicValue(declared)) continue;
|
|
726
1546
|
if (declared && controllerIdentifiers(declared.value).includes(rule.controller)) continue;
|
|
@@ -809,13 +1629,14 @@ function checkSource(source, manifest) {
|
|
|
809
1629
|
}
|
|
810
1630
|
}
|
|
811
1631
|
if (rule.min !== void 0 && matched.length < rule.min) {
|
|
1632
|
+
const complete = scopeIsComplete(container, node, identifier);
|
|
812
1633
|
report(
|
|
813
1634
|
container,
|
|
814
1635
|
"cardinality-violation",
|
|
815
|
-
"error",
|
|
1636
|
+
complete ? "error" : "warning",
|
|
816
1637
|
`"${identifier}" requires at least ${rule.min} ${what} per ${where}, but found ${matched.length}.`,
|
|
817
1638
|
container,
|
|
818
|
-
rule.suggestion
|
|
1639
|
+
complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
|
|
819
1640
|
);
|
|
820
1641
|
}
|
|
821
1642
|
}
|
|
@@ -858,6 +1679,28 @@ function checkSource(source, manifest) {
|
|
|
858
1679
|
diagnostics.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
859
1680
|
return diagnostics;
|
|
860
1681
|
}
|
|
1682
|
+
function valueSatisfiesConstraint(raw, constraint) {
|
|
1683
|
+
const value = decodeNumericValue(raw);
|
|
1684
|
+
if (Number.isNaN(value)) return false;
|
|
1685
|
+
if (constraint.finite && !Number.isFinite(value)) return false;
|
|
1686
|
+
if (constraint.greaterThan !== void 0 && !(value > constraint.greaterThan)) return false;
|
|
1687
|
+
return true;
|
|
1688
|
+
}
|
|
1689
|
+
function decodeNumericValue(raw) {
|
|
1690
|
+
return Number(raw.replace(/_/g, ""));
|
|
1691
|
+
}
|
|
1692
|
+
function describeValueConstraint(constraint) {
|
|
1693
|
+
const parts = [constraint.finite ? "a finite number" : "a number"];
|
|
1694
|
+
if (constraint.greaterThan !== void 0) {
|
|
1695
|
+
parts.push(`greater than ${constraint.greaterThan}`);
|
|
1696
|
+
}
|
|
1697
|
+
return parts.join(" ");
|
|
1698
|
+
}
|
|
1699
|
+
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.";
|
|
1700
|
+
var UNRESOLVED_HOST_HINT = "An enclosing helper's data: option is not a literal hash, so the host controller may be declared at runtime.";
|
|
1701
|
+
function hasReadableMarkup(el) {
|
|
1702
|
+
return el.origin === "markup";
|
|
1703
|
+
}
|
|
861
1704
|
function checkA11y(report, el, identifier, req, severity) {
|
|
862
1705
|
const groups = [
|
|
863
1706
|
{ attrs: req.attrs, values: req.values },
|
|
@@ -925,6 +1768,20 @@ function describeCounted(target, attr, values) {
|
|
|
925
1768
|
return `"${target}" targets with ${attr}${constrained}`;
|
|
926
1769
|
}
|
|
927
1770
|
var NATIVE_FOCUSABLE_TAGS = /* @__PURE__ */ new Set(["button", "select", "textarea", "summary"]);
|
|
1771
|
+
var ALWAYS_INTERACTIVE_HOSTS = /* @__PURE__ */ new Set([
|
|
1772
|
+
"input",
|
|
1773
|
+
"select",
|
|
1774
|
+
"textarea",
|
|
1775
|
+
"label",
|
|
1776
|
+
"summary",
|
|
1777
|
+
"details",
|
|
1778
|
+
"iframe",
|
|
1779
|
+
"object",
|
|
1780
|
+
"embed"
|
|
1781
|
+
]);
|
|
1782
|
+
function hasAttr(element, name) {
|
|
1783
|
+
return element.attrs.some((attr) => attr.name === name);
|
|
1784
|
+
}
|
|
928
1785
|
function isNativelyFocusable(el) {
|
|
929
1786
|
if (NATIVE_FOCUSABLE_TAGS.has(el.tag)) return true;
|
|
930
1787
|
if (el.tag === "input") {
|
|
@@ -986,6 +1843,7 @@ function inDeclaredFragment(node, identifier) {
|
|
|
986
1843
|
return false;
|
|
987
1844
|
}
|
|
988
1845
|
function anchorLength(at) {
|
|
1846
|
+
if (at.sourceLength !== void 0) return at.sourceLength;
|
|
989
1847
|
return "name" in at ? at.name.length : at.tag.length + 1;
|
|
990
1848
|
}
|
|
991
1849
|
function push(out, code, severity, message, at, suggestion, fix) {
|
|
@@ -1074,12 +1932,15 @@ function isCheckableFile(path) {
|
|
|
1074
1932
|
var CONTROLLER_ARRAY_FIELDS = [
|
|
1075
1933
|
"targets",
|
|
1076
1934
|
"values",
|
|
1935
|
+
"valueConstraints",
|
|
1936
|
+
"valueRelations",
|
|
1077
1937
|
"actions",
|
|
1078
1938
|
"events",
|
|
1079
1939
|
"requiredTargets",
|
|
1080
1940
|
"conditionalTargets",
|
|
1081
1941
|
"a11y",
|
|
1082
1942
|
"keyboard",
|
|
1943
|
+
"hosts",
|
|
1083
1944
|
"managedAria",
|
|
1084
1945
|
"compositions",
|
|
1085
1946
|
"companions",
|
|
@@ -1552,6 +2413,37 @@ var CONTROLLER_CONTRACT_SCHEMA = {
|
|
|
1552
2413
|
id: { type: "string", description: 'Controller identifier, e.g. "stimeo--tabs".' },
|
|
1553
2414
|
targets: STRING_ARRAY,
|
|
1554
2415
|
values: STRING_ARRAY,
|
|
2416
|
+
valueConstraints: {
|
|
2417
|
+
type: "array",
|
|
2418
|
+
description: "Statically checkable semantic bounds on authored Stimulus Values.",
|
|
2419
|
+
items: {
|
|
2420
|
+
type: "object",
|
|
2421
|
+
properties: {
|
|
2422
|
+
value: { type: "string", description: "Camel-case declared Value name." },
|
|
2423
|
+
type: { type: "string", enum: ["number"] },
|
|
2424
|
+
finite: { type: "boolean" },
|
|
2425
|
+
greaterThan: { type: "number" },
|
|
2426
|
+
suggestion: { type: "string" }
|
|
2427
|
+
},
|
|
2428
|
+
required: ["value", "type", "suggestion"]
|
|
2429
|
+
}
|
|
2430
|
+
},
|
|
2431
|
+
valueRelations: {
|
|
2432
|
+
type: "array",
|
|
2433
|
+
description: "Statically checkable relationships between authored Stimulus Values.",
|
|
2434
|
+
items: {
|
|
2435
|
+
type: "object",
|
|
2436
|
+
properties: {
|
|
2437
|
+
left: { type: "string", description: "Camel-case left Value name." },
|
|
2438
|
+
operator: { type: "string", enum: ["less-than-or-equal"] },
|
|
2439
|
+
right: { type: "string", description: "Camel-case right Value name." },
|
|
2440
|
+
leftDefault: { type: "number" },
|
|
2441
|
+
rightDefault: { type: "number" },
|
|
2442
|
+
suggestion: { type: "string" }
|
|
2443
|
+
},
|
|
2444
|
+
required: ["left", "operator", "right", "leftDefault", "rightDefault", "suggestion"]
|
|
2445
|
+
}
|
|
2446
|
+
},
|
|
1555
2447
|
actions: STRING_ARRAY,
|
|
1556
2448
|
events: STRING_ARRAY,
|
|
1557
2449
|
requiredTargets: {
|
|
@@ -1633,6 +2525,30 @@ var CONTROLLER_CONTRACT_SCHEMA = {
|
|
|
1633
2525
|
required: ["target", "suggestion"]
|
|
1634
2526
|
}
|
|
1635
2527
|
},
|
|
2528
|
+
hosts: {
|
|
2529
|
+
type: "array",
|
|
2530
|
+
description: "Scope or target host-element contracts that prevent the controller from competing with an incompatible native interaction.",
|
|
2531
|
+
items: {
|
|
2532
|
+
type: "object",
|
|
2533
|
+
properties: {
|
|
2534
|
+
target: {
|
|
2535
|
+
type: "string",
|
|
2536
|
+
description: 'Target name; "" means the data-controller element itself.'
|
|
2537
|
+
},
|
|
2538
|
+
mode: {
|
|
2539
|
+
type: "string",
|
|
2540
|
+
enum: ["non-interactive", "non-interactive-or-button"],
|
|
2541
|
+
description: "Supported static host family."
|
|
2542
|
+
},
|
|
2543
|
+
buttonTypes: {
|
|
2544
|
+
...STRING_ARRAY,
|
|
2545
|
+
description: "Literal button types admitted by non-interactive-or-button."
|
|
2546
|
+
},
|
|
2547
|
+
suggestion: { type: "string" }
|
|
2548
|
+
},
|
|
2549
|
+
required: ["target", "mode", "suggestion"]
|
|
2550
|
+
}
|
|
2551
|
+
},
|
|
1636
2552
|
managedAria: {
|
|
1637
2553
|
type: "array",
|
|
1638
2554
|
description: "ARIA the controller recomputes at runtime; authoring it is a warning.",
|
|
@@ -1769,12 +2685,15 @@ var CONTROLLER_CONTRACT_SCHEMA = {
|
|
|
1769
2685
|
"id",
|
|
1770
2686
|
"targets",
|
|
1771
2687
|
"values",
|
|
2688
|
+
"valueConstraints",
|
|
2689
|
+
"valueRelations",
|
|
1772
2690
|
"actions",
|
|
1773
2691
|
"events",
|
|
1774
2692
|
"requiredTargets",
|
|
1775
2693
|
"conditionalTargets",
|
|
1776
2694
|
"a11y",
|
|
1777
2695
|
"keyboard",
|
|
2696
|
+
"hosts",
|
|
1778
2697
|
"managedAria",
|
|
1779
2698
|
"compositions",
|
|
1780
2699
|
"companions",
|
|
@@ -1793,7 +2712,7 @@ var TOOL_DESCRIPTORS = [
|
|
|
1793
2712
|
{
|
|
1794
2713
|
name: "stimeo_check",
|
|
1795
2714
|
title: "Check Stimeo UI markup",
|
|
1796
|
-
description: "Statically check HTML or ERB source that uses Stimeo UI (stimeo--*) controllers \u2014 the headless, accessible Stimulus UI component library for Rails. Returns a CheckReport with diagnostics \u2014 unknown controllers/targets/values/action methods, missing required targets, missing or invalid author-supplied ARIA, keyboard-focusability prerequisites, and misaligned cross-controller composition values \u2014 each with a fix suggestion when available. Call this to validate any Rails view, ERB template, or HTML you write or edit with these components, before finalizing it; ok=true means no errors.",
|
|
2715
|
+
description: "Statically check HTML or ERB source that uses Stimeo UI (stimeo--*) controllers \u2014 the headless, accessible Stimulus UI component library for Rails. Returns a CheckReport with diagnostics \u2014 unknown controllers/targets/values/action methods, missing required targets, missing or invalid author-supplied ARIA, keyboard-focusability prerequisites, incompatible interactive hosts, and misaligned cross-controller composition values \u2014 each with a fix suggestion when available. Call this to validate any Rails view, ERB template, or HTML you write or edit with these components, before finalizing it; ok=true means no errors.",
|
|
1797
2716
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1798
2717
|
inputSchema: {
|
|
1799
2718
|
type: "object",
|
|
@@ -1827,7 +2746,7 @@ var TOOL_DESCRIPTORS = [
|
|
|
1827
2746
|
{
|
|
1828
2747
|
name: "stimeo_controller",
|
|
1829
2748
|
title: "Get one controller's usage contract",
|
|
1830
|
-
description: 'Fetch the full usage contract of one Stimeo UI controller by identifier (e.g. "stimeo--tabs"): targets, values, actions, events, required targets, and the accessibility contract \u2014 author-supplied ARIA requirements, keyboard-focusability prerequisites, the ARIA attributes the controller manages at runtime (which the author must NOT hardcode), and cross-controller composition value alignments. Use it after stimeo_catalog to write correct, accessible Rails ERB or HTML markup for that Stimulus component.',
|
|
2749
|
+
description: 'Fetch the full usage contract of one Stimeo UI controller by identifier (e.g. "stimeo--tabs"): targets, values, actions, events, required targets, and the accessibility contract \u2014 author-supplied ARIA requirements, keyboard-focusability prerequisites, supported host-element shapes, the ARIA attributes the controller manages at runtime (which the author must NOT hardcode), and cross-controller composition value alignments. Use it after stimeo_catalog to write correct, accessible Rails ERB or HTML markup for that Stimulus component.',
|
|
1831
2750
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1832
2751
|
inputSchema: {
|
|
1833
2752
|
type: "object",
|