stimeo-ui 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +160 -0
- package/dist/cable/index.d.ts +16 -16
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.js +10 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- 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/breadcrumb_controller.d.ts +97 -10
- package/dist/controllers/breadcrumb_controller.js +225 -13
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +11 -1
- package/dist/controllers/calendar_controller.js +89 -22
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +16 -1
- package/dist/controllers/carousel_controller.js +47 -6
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.js +2 -2
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +14 -2
- package/dist/controllers/color_picker_controller.js +92 -7
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +14 -1
- package/dist/controllers/combobox_controller.js +162 -23
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +226 -22
- 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 +7 -6
- package/dist/controllers/context_menu_controller.js +32 -10
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +19 -5
- package/dist/controllers/countdown_controller.js +112 -12
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +19 -1
- package/dist/controllers/data_grid_controller.js +82 -4
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +5 -1
- package/dist/controllers/date_range_picker_controller.js +77 -3
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +32 -5
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +32 -5
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +1 -2
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +1 -0
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +9 -4
- package/dist/controllers/empty_state_controller.js +24 -10
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +1 -1
- package/dist/controllers/focus_controller.js +32 -5
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +14 -3
- package/dist/controllers/frame_loading_controller.js +177 -15
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +1 -1
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +36 -11
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +31 -10
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +9 -2
- package/dist/controllers/listbox_controller.js +257 -53
- 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 -8
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +29 -11
- package/dist/controllers/menu_controller.js +104 -17
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +91 -11
- package/dist/controllers/menubar_controller.js +415 -63
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +22 -2
- package/dist/controllers/meter_controller.js +145 -26
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +31 -5
- package/dist/controllers/multi_select_controller.js +312 -29
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +65 -18
- package/dist/controllers/navigation_menu_controller.js +154 -27
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +4 -5
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +13 -1
- package/dist/controllers/network_status_controller.js +28 -8
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +1 -1
- package/dist/controllers/number_input_controller.js +7 -0
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js +18 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +3 -2
- package/dist/controllers/overflow_indicator_controller.js +81 -13
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +78 -19
- package/dist/controllers/overflow_menu_controller.js +408 -57
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +36 -5
- package/dist/controllers/pagination_controller.js +163 -32
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js +6 -6
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +2 -3
- package/dist/controllers/pointer_drag_controller.js +9 -1
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +1 -2
- package/dist/controllers/popover_controller.js +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +4 -4
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +16 -1
- package/dist/controllers/progress_controller.js +116 -9
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js +22 -3
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +18 -5
- package/dist/controllers/range_slider_controller.js +100 -9
- 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 +69 -2
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.js +63 -19
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +13 -0
- package/dist/controllers/relative_time_controller.js +133 -12
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +3 -1
- package/dist/controllers/resizable_controller.js +65 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +17 -2
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +86 -12
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +1 -1
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
- package/dist/controllers/scroll_visibility_controller.js +33 -3
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +64 -15
- package/dist/controllers/scrollspy_controller.js +346 -73
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +20 -5
- package/dist/controllers/separator_controller.js +9 -0
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +2 -12
- package/dist/controllers/sidebar_controller.js +37 -8
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +7 -2
- package/dist/controllers/skeleton_controller.js +73 -20
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +11 -1
- package/dist/controllers/slider_controller.js +49 -8
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +3 -1
- package/dist/controllers/sortable_controller.js +34 -3
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +37 -4
- package/dist/controllers/spinner_controller.js +228 -27
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +12 -1
- package/dist/controllers/step_indicator_controller.js +82 -5
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +31 -8
- package/dist/controllers/stick_to_bottom_controller.js +61 -10
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js +32 -11
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +2 -2
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js +1 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +11 -3
- package/dist/controllers/tabs_controller.js +26 -3
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.js +22 -2
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +1 -1
- package/dist/controllers/theme_controller.js +22 -3
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js +20 -1
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +4 -5
- package/dist/controllers/toast_controller.js +4 -5
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js +23 -2
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +53 -13
- package/dist/controllers/toolbar_controller.js +230 -31
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +1 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +87 -6
- package/dist/controllers/tree_view_controller.js +467 -51
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +4507 -907
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +375 -39
- package/dist/inspector/cli.js +1009 -86
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +1200 -120
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +28 -28
- package/dist/inspector/manifest.json +1060 -132
- package/package.json +5 -6
package/dist/inspector/cli.js
CHANGED
|
@@ -5,66 +5,8 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
|
|
6
6
|
// src/inspector/cli.ts
|
|
7
7
|
|
|
8
|
-
// src/inspector/erb.ts
|
|
9
|
-
function neutralizeErb(source) {
|
|
10
|
-
return source.replace(/<%[\s\S]*?%>/g, (match) => match.replace(/[^\n]/g, " "));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// src/inspector/extract.ts
|
|
14
|
-
var NAMESPACE_PREFIX = "stimeo--";
|
|
15
|
-
function dasherize(value) {
|
|
16
|
-
return value.replace(/([A-Z])/g, (_match, char) => `-${char.toLowerCase()}`);
|
|
17
|
-
}
|
|
18
|
-
function controllerIdentifiers(dataControllerValue) {
|
|
19
|
-
const seen = /* @__PURE__ */ new Set();
|
|
20
|
-
for (const token of dataControllerValue.split(/\s+/)) {
|
|
21
|
-
if (token.startsWith(NAMESPACE_PREFIX)) seen.add(token);
|
|
22
|
-
}
|
|
23
|
-
return [...seen];
|
|
24
|
-
}
|
|
25
|
-
function parseTargetAttr(attrName) {
|
|
26
|
-
const match = /^data-(stimeo--[a-z0-9][a-z0-9-]*)-target$/.exec(attrName);
|
|
27
|
-
return match?.[1] ?? null;
|
|
28
|
-
}
|
|
29
|
-
function parseValueAttr(attrName, knownIdentifiers) {
|
|
30
|
-
const match = /^data-(stimeo--[a-z0-9-]+)-value$/.exec(attrName);
|
|
31
|
-
if (!match) return null;
|
|
32
|
-
const inner = match[1] ?? "";
|
|
33
|
-
let best = null;
|
|
34
|
-
for (const id of knownIdentifiers) {
|
|
35
|
-
if (inner.startsWith(`${id}-`)) {
|
|
36
|
-
const valueToken = inner.slice(id.length + 1);
|
|
37
|
-
if (valueToken.length > 0 && (!best || id.length > best.identifier.length)) {
|
|
38
|
-
best = { identifier: id, valueToken };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (best) return best;
|
|
43
|
-
const rest = inner.slice(NAMESPACE_PREFIX.length);
|
|
44
|
-
const dash = rest.indexOf("-");
|
|
45
|
-
const identifierGuess = dash === -1 ? inner : `${NAMESPACE_PREFIX}${rest.slice(0, dash)}`;
|
|
46
|
-
const valueGuess = dash === -1 ? "" : rest.slice(dash + 1);
|
|
47
|
-
return { identifier: null, valueToken: valueGuess || identifierGuess };
|
|
48
|
-
}
|
|
49
|
-
function actionDescriptors(dataActionValue) {
|
|
50
|
-
const descriptors = [];
|
|
51
|
-
for (const descriptor of dataActionValue.split(/\s+/)) {
|
|
52
|
-
const hash = descriptor.indexOf("#");
|
|
53
|
-
if (hash === -1) continue;
|
|
54
|
-
let lhs = descriptor.slice(0, hash);
|
|
55
|
-
const arrow = lhs.indexOf("->");
|
|
56
|
-
if (arrow !== -1) lhs = lhs.slice(arrow + 2);
|
|
57
|
-
if (!lhs.startsWith(NAMESPACE_PREFIX)) continue;
|
|
58
|
-
const method = /^[a-zA-Z_$][\w$]*/.exec(descriptor.slice(hash + 1))?.[0] ?? "";
|
|
59
|
-
descriptors.push({ identifier: lhs, method });
|
|
60
|
-
}
|
|
61
|
-
return descriptors;
|
|
62
|
-
}
|
|
63
|
-
function isStimeoDataAttr(attrName) {
|
|
64
|
-
return attrName.startsWith(`data-${NAMESPACE_PREFIX}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
8
|
// src/inspector/html_parser.ts
|
|
9
|
+
var ERB_ELEMENT_TAG = "#erb";
|
|
68
10
|
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
69
11
|
"area",
|
|
70
12
|
"base",
|
|
@@ -101,7 +43,7 @@ var PositionIndex = class {
|
|
|
101
43
|
return { line: lo + 1, column: offset - (this.#lineStarts[lo] ?? 0) + 1 };
|
|
102
44
|
}
|
|
103
45
|
};
|
|
104
|
-
function parseHtml(source) {
|
|
46
|
+
function parseHtml(source, synthetics = []) {
|
|
105
47
|
const pos = new PositionIndex(source);
|
|
106
48
|
const len = source.length;
|
|
107
49
|
const ch = (k) => k >= 0 && k < len ? source[k] : "";
|
|
@@ -111,12 +53,57 @@ function parseHtml(source) {
|
|
|
111
53
|
children: [],
|
|
112
54
|
parent: null,
|
|
113
55
|
line: 1,
|
|
114
|
-
column: 1
|
|
56
|
+
column: 1,
|
|
57
|
+
origin: "markup",
|
|
58
|
+
opaque: false
|
|
115
59
|
};
|
|
116
60
|
const stack = [root];
|
|
117
61
|
let i = 0;
|
|
118
62
|
const top = () => stack[stack.length - 1];
|
|
63
|
+
let pending = 0;
|
|
64
|
+
const closesAt = /* @__PURE__ */ new Map();
|
|
65
|
+
const takeSynthetics = (offset, splice) => {
|
|
66
|
+
while (pending < synthetics.length) {
|
|
67
|
+
const synthetic = synthetics[pending];
|
|
68
|
+
if (synthetic.start > offset) return;
|
|
69
|
+
pending++;
|
|
70
|
+
if (!splice) continue;
|
|
71
|
+
const at = pos.at(synthetic.start);
|
|
72
|
+
const node = {
|
|
73
|
+
tag: synthetic.tag,
|
|
74
|
+
attrs: synthetic.attrs,
|
|
75
|
+
children: [],
|
|
76
|
+
parent: top(),
|
|
77
|
+
line: at.line,
|
|
78
|
+
column: at.column,
|
|
79
|
+
sourceLength: synthetic.sourceLength,
|
|
80
|
+
origin: "erb",
|
|
81
|
+
opaque: synthetic.opaque
|
|
82
|
+
};
|
|
83
|
+
top().children.push(node);
|
|
84
|
+
if (synthetic.container) {
|
|
85
|
+
closesAt.set(node, synthetic.end);
|
|
86
|
+
stack.push(node);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const closeSynthetics = (offset) => {
|
|
91
|
+
for (; ; ) {
|
|
92
|
+
let expired = -1;
|
|
93
|
+
for (let depth = stack.length - 1; depth >= 1; depth--) {
|
|
94
|
+
const end = closesAt.get(stack[depth]);
|
|
95
|
+
if (end !== void 0 && end <= offset) {
|
|
96
|
+
expired = depth;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (expired < 0) return;
|
|
101
|
+
stack.length = expired;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
119
104
|
while (i < len) {
|
|
105
|
+
closeSynthetics(i);
|
|
106
|
+
takeSynthetics(i, true);
|
|
120
107
|
if (ch(i) !== "<") {
|
|
121
108
|
i++;
|
|
122
109
|
continue;
|
|
@@ -125,11 +112,13 @@ function parseHtml(source) {
|
|
|
125
112
|
if (source.startsWith("<!--", i)) {
|
|
126
113
|
const end = source.indexOf("-->", i + 4);
|
|
127
114
|
i = end === -1 ? len : end + 3;
|
|
115
|
+
takeSynthetics(i - 1, false);
|
|
128
116
|
continue;
|
|
129
117
|
}
|
|
130
118
|
if (next === "!" || next === "?") {
|
|
131
119
|
const end = source.indexOf(">", i);
|
|
132
120
|
i = end === -1 ? len : end + 1;
|
|
121
|
+
takeSynthetics(i - 1, false);
|
|
133
122
|
continue;
|
|
134
123
|
}
|
|
135
124
|
if (next === "/") {
|
|
@@ -141,6 +130,7 @@ function parseHtml(source) {
|
|
|
141
130
|
}
|
|
142
131
|
const end = source.indexOf(">", j2);
|
|
143
132
|
i = end === -1 ? len : end + 1;
|
|
133
|
+
takeSynthetics(i - 1, false);
|
|
144
134
|
closeTag(stack, name.toLowerCase());
|
|
145
135
|
continue;
|
|
146
136
|
}
|
|
@@ -221,10 +211,13 @@ function parseHtml(source) {
|
|
|
221
211
|
children: [],
|
|
222
212
|
parent: top(),
|
|
223
213
|
line: startPos.line,
|
|
224
|
-
column: startPos.column
|
|
214
|
+
column: startPos.column,
|
|
215
|
+
origin: "markup",
|
|
216
|
+
opaque: false
|
|
225
217
|
};
|
|
226
218
|
top().children.push(node);
|
|
227
219
|
i = j;
|
|
220
|
+
takeSynthetics(i - 1, false);
|
|
228
221
|
if (selfClosing || VOID_ELEMENTS.has(lowerTag)) {
|
|
229
222
|
continue;
|
|
230
223
|
}
|
|
@@ -237,10 +230,13 @@ function parseHtml(source) {
|
|
|
237
230
|
const gt = source.indexOf(">", idx);
|
|
238
231
|
i = gt === -1 ? len : gt + 1;
|
|
239
232
|
}
|
|
233
|
+
takeSynthetics(i - 1, false);
|
|
240
234
|
continue;
|
|
241
235
|
}
|
|
242
236
|
stack.push(node);
|
|
243
237
|
}
|
|
238
|
+
closeSynthetics(len);
|
|
239
|
+
takeSynthetics(len, true);
|
|
244
240
|
return root;
|
|
245
241
|
}
|
|
246
242
|
function closeTag(stack, name) {
|
|
@@ -258,6 +254,673 @@ function walk(root, visit) {
|
|
|
258
254
|
}
|
|
259
255
|
}
|
|
260
256
|
|
|
257
|
+
// src/inspector/ruby_scan.ts
|
|
258
|
+
var SPACE = /* @__PURE__ */ new Set([" ", " ", "\n", "\r", "\f"]);
|
|
259
|
+
var IDENT_START = /[A-Za-z_]/;
|
|
260
|
+
var IDENT_CHAR = /[A-Za-z0-9_]/;
|
|
261
|
+
var ESCAPES = {
|
|
262
|
+
n: "\n",
|
|
263
|
+
t: " ",
|
|
264
|
+
r: "\r",
|
|
265
|
+
f: "\f",
|
|
266
|
+
v: "\v",
|
|
267
|
+
b: "\b",
|
|
268
|
+
a: "\x07",
|
|
269
|
+
e: "\x1B",
|
|
270
|
+
s: " "
|
|
271
|
+
};
|
|
272
|
+
var UNDECODED_ESCAPE = /[uxcCM0-7]/;
|
|
273
|
+
function readString(code, start) {
|
|
274
|
+
const quote = code[start];
|
|
275
|
+
const single = quote === "'";
|
|
276
|
+
const contentStart = start + 1;
|
|
277
|
+
let i = contentStart;
|
|
278
|
+
let value = "";
|
|
279
|
+
let verbatim = true;
|
|
280
|
+
while (i < code.length) {
|
|
281
|
+
const char = code[i];
|
|
282
|
+
if (char === quote) {
|
|
283
|
+
return { end: i + 1, value, literal: true, verbatim, contentStart, contentEnd: i };
|
|
284
|
+
}
|
|
285
|
+
if (char === "\\") {
|
|
286
|
+
const escaped = code[i + 1];
|
|
287
|
+
if (escaped === void 0) break;
|
|
288
|
+
verbatim = false;
|
|
289
|
+
if (single) {
|
|
290
|
+
value += escaped === "'" || escaped === "\\" ? escaped : `\\${escaped}`;
|
|
291
|
+
} else if (UNDECODED_ESCAPE.test(escaped)) {
|
|
292
|
+
return {
|
|
293
|
+
end: closingQuote(code, i + 2, quote),
|
|
294
|
+
value,
|
|
295
|
+
literal: false,
|
|
296
|
+
verbatim: false,
|
|
297
|
+
contentStart,
|
|
298
|
+
contentEnd: i
|
|
299
|
+
};
|
|
300
|
+
} else if (escaped !== "\n") {
|
|
301
|
+
value += ESCAPES[escaped] ?? escaped;
|
|
302
|
+
}
|
|
303
|
+
i += 2;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (!single && char === "#" && code[i + 1] === "{") {
|
|
307
|
+
const close = skipBraces(code, i + 1);
|
|
308
|
+
return {
|
|
309
|
+
end: closingQuote(code, close, quote),
|
|
310
|
+
value,
|
|
311
|
+
literal: false,
|
|
312
|
+
verbatim: false,
|
|
313
|
+
contentStart,
|
|
314
|
+
contentEnd: i
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
value += char;
|
|
318
|
+
i++;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
end: code.length,
|
|
322
|
+
value,
|
|
323
|
+
literal: false,
|
|
324
|
+
verbatim: false,
|
|
325
|
+
contentStart,
|
|
326
|
+
contentEnd: code.length
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function closingQuote(code, from, quote) {
|
|
330
|
+
for (let i = from; i < code.length; i++) {
|
|
331
|
+
if (code[i] === "\\") {
|
|
332
|
+
i++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (code[i] === quote) return i + 1;
|
|
336
|
+
}
|
|
337
|
+
return code.length;
|
|
338
|
+
}
|
|
339
|
+
function skipBraces(code, open) {
|
|
340
|
+
let depth = 0;
|
|
341
|
+
let i = open;
|
|
342
|
+
while (i < code.length) {
|
|
343
|
+
const char = code[i];
|
|
344
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
345
|
+
i = readString(code, i).end;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (char === "{") depth++;
|
|
349
|
+
else if (char === "}") {
|
|
350
|
+
depth--;
|
|
351
|
+
if (depth === 0) return i + 1;
|
|
352
|
+
}
|
|
353
|
+
i++;
|
|
354
|
+
}
|
|
355
|
+
return code.length;
|
|
356
|
+
}
|
|
357
|
+
function endOfLine(code, at) {
|
|
358
|
+
const newline = code.indexOf("\n", at);
|
|
359
|
+
return newline === -1 ? code.length : newline + 1;
|
|
360
|
+
}
|
|
361
|
+
function skipTrivia(code, at) {
|
|
362
|
+
let i = at;
|
|
363
|
+
while (i < code.length) {
|
|
364
|
+
const char = code[i];
|
|
365
|
+
if (SPACE.has(char)) i++;
|
|
366
|
+
else if (char === "#") i = endOfLine(code, i);
|
|
367
|
+
else break;
|
|
368
|
+
}
|
|
369
|
+
return i;
|
|
370
|
+
}
|
|
371
|
+
function previousSignificant(code, at) {
|
|
372
|
+
let i = at - 1;
|
|
373
|
+
while (i >= 0 && (code[i] === " " || code[i] === " ")) i--;
|
|
374
|
+
return i >= 0 ? code[i] : "";
|
|
375
|
+
}
|
|
376
|
+
var EXPRESSION_LEAD = /* @__PURE__ */ new Set([
|
|
377
|
+
"",
|
|
378
|
+
"\n",
|
|
379
|
+
"\r",
|
|
380
|
+
";",
|
|
381
|
+
"(",
|
|
382
|
+
"[",
|
|
383
|
+
"{",
|
|
384
|
+
",",
|
|
385
|
+
":",
|
|
386
|
+
"=",
|
|
387
|
+
"<",
|
|
388
|
+
">",
|
|
389
|
+
"!",
|
|
390
|
+
"&",
|
|
391
|
+
"|",
|
|
392
|
+
"^",
|
|
393
|
+
"~",
|
|
394
|
+
"+",
|
|
395
|
+
"-",
|
|
396
|
+
"*",
|
|
397
|
+
"/",
|
|
398
|
+
"%",
|
|
399
|
+
"?"
|
|
400
|
+
]);
|
|
401
|
+
function opensExpression(code, at) {
|
|
402
|
+
return EXPRESSION_LEAD.has(previousSignificant(code, at));
|
|
403
|
+
}
|
|
404
|
+
var PERCENT_TYPES = "qQwWiIrsx";
|
|
405
|
+
var DELIMITER_PAIRS = { "(": ")", "[": "]", "{": "}", "<": ">" };
|
|
406
|
+
function skipLiteral(code, at) {
|
|
407
|
+
const char = code[at];
|
|
408
|
+
if (char === void 0) return -1;
|
|
409
|
+
if (char === "%") {
|
|
410
|
+
const typed = PERCENT_TYPES.includes(code[at + 1] ?? "");
|
|
411
|
+
const open = typed ? code[at + 2] : code[at + 1];
|
|
412
|
+
if (open === void 0 || IDENT_CHAR.test(open) || SPACE.has(open)) return -1;
|
|
413
|
+
if (!typed && !opensExpression(code, at) && !SPACE.has(code[at - 1] ?? "")) return -1;
|
|
414
|
+
const end = closeDelimited(code, typed ? at + 3 : at + 2, open);
|
|
415
|
+
return end < 0 ? code.length : end;
|
|
416
|
+
}
|
|
417
|
+
if (char === "/" && opensExpression(code, at)) {
|
|
418
|
+
const end = closeDelimited(code, at + 1, "/");
|
|
419
|
+
if (end < 0) return -1;
|
|
420
|
+
let i = end;
|
|
421
|
+
while (i < code.length && /[imxounse]/.test(code[i])) i++;
|
|
422
|
+
return i;
|
|
423
|
+
}
|
|
424
|
+
if (char === "?" && opensExpression(code, at)) {
|
|
425
|
+
const next = code[at + 1];
|
|
426
|
+
if (next === void 0 || SPACE.has(next)) return -1;
|
|
427
|
+
if (next === "\\") return at + 3;
|
|
428
|
+
if (IDENT_CHAR.test(next) && IDENT_CHAR.test(code[at + 2] ?? "")) return -1;
|
|
429
|
+
return at + 2;
|
|
430
|
+
}
|
|
431
|
+
return -1;
|
|
432
|
+
}
|
|
433
|
+
function closeDelimited(code, from, open) {
|
|
434
|
+
const close = DELIMITER_PAIRS[open] ?? open;
|
|
435
|
+
const nests = close !== open;
|
|
436
|
+
let depth = 1;
|
|
437
|
+
for (let i = from; i < code.length; i++) {
|
|
438
|
+
const char = code[i];
|
|
439
|
+
if (char === "\\") {
|
|
440
|
+
i++;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (nests && char === open) depth++;
|
|
444
|
+
else if (char === close) {
|
|
445
|
+
depth--;
|
|
446
|
+
if (depth === 0) return i + 1;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return -1;
|
|
450
|
+
}
|
|
451
|
+
var HEREDOC_OPEN = /^<<([-~])?(["'`])?([A-Za-z_]\w*)/;
|
|
452
|
+
function maskHeredocs(code) {
|
|
453
|
+
let masked = code;
|
|
454
|
+
let i = 0;
|
|
455
|
+
while (i < masked.length) {
|
|
456
|
+
const char = masked[i];
|
|
457
|
+
if (char === "#") {
|
|
458
|
+
i = endOfLine(masked, i);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
462
|
+
i = readString(masked, i).end;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const literal = skipLiteral(masked, i);
|
|
466
|
+
if (literal > i) {
|
|
467
|
+
i = literal;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (char === "<" && masked[i + 1] === "<") {
|
|
471
|
+
const [matched, dash, quote, identifier] = HEREDOC_OPEN.exec(masked.slice(i)) ?? [];
|
|
472
|
+
const opensBody = identifier !== void 0 && (dash !== void 0 || quote !== void 0 || /^[A-Z]/.test(identifier) && opensExpression(masked, i));
|
|
473
|
+
const consumed = (matched?.length ?? 0) + (quote === void 0 ? 0 : 1);
|
|
474
|
+
if (opensBody && (quote === void 0 || masked[i + consumed - 1] === quote)) {
|
|
475
|
+
const bodyStart = endOfLine(masked, i);
|
|
476
|
+
const bodyEnd = heredocEnd(masked, bodyStart, identifier);
|
|
477
|
+
masked = masked.slice(0, bodyStart) + masked.slice(bodyStart, bodyEnd).replace(/[^\n]/g, " ") + masked.slice(bodyEnd);
|
|
478
|
+
i += consumed;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
i++;
|
|
483
|
+
}
|
|
484
|
+
return masked;
|
|
485
|
+
}
|
|
486
|
+
function heredocEnd(code, bodyStart, identifier) {
|
|
487
|
+
let line = bodyStart;
|
|
488
|
+
while (line < code.length) {
|
|
489
|
+
const next = endOfLine(code, line);
|
|
490
|
+
if (code.slice(line, next).trim() === identifier) return next;
|
|
491
|
+
line = next;
|
|
492
|
+
}
|
|
493
|
+
return code.length;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/inspector/ruby_hash.ts
|
|
497
|
+
function readDataOption(source, base) {
|
|
498
|
+
const code = maskHeredocs(source);
|
|
499
|
+
const attrs = [];
|
|
500
|
+
let found = false;
|
|
501
|
+
let opaque = false;
|
|
502
|
+
let i = 0;
|
|
503
|
+
while (i < code.length) {
|
|
504
|
+
const char = code[i];
|
|
505
|
+
if (char === "#") {
|
|
506
|
+
i = endOfLine(code, i);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
510
|
+
const literal = readString(code, i);
|
|
511
|
+
const value = char === "`" ? null : keySeparator(code, literal.end);
|
|
512
|
+
if (literal.literal && literal.value === "data" && value !== null) {
|
|
513
|
+
i = value;
|
|
514
|
+
} else {
|
|
515
|
+
i = literal.end;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
} else if (char === ":" && IDENT_START.test(code[i + 1] ?? "")) {
|
|
519
|
+
const name = readIdentifier(code, i + 1);
|
|
520
|
+
const value = hashRocket(code, name.end);
|
|
521
|
+
if (name.text === "data" && value !== null) {
|
|
522
|
+
i = value;
|
|
523
|
+
} else {
|
|
524
|
+
i = name.end;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
} else if (IDENT_START.test(char) && !IDENT_CHAR.test(code[i - 1] ?? "")) {
|
|
528
|
+
const name = readIdentifier(code, i);
|
|
529
|
+
const keyed = name.text === "data" && code[name.end] === ":" && code[name.end + 1] !== ":";
|
|
530
|
+
if (keyed) {
|
|
531
|
+
i = name.end + 1;
|
|
532
|
+
} else {
|
|
533
|
+
i = name.end;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
const literal = skipLiteral(code, i);
|
|
538
|
+
i = literal < 0 ? i + 1 : literal;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
found = true;
|
|
542
|
+
const start = skipTrivia(code, i);
|
|
543
|
+
if (code[start] === "{") {
|
|
544
|
+
const hash = readHash(code, start, base);
|
|
545
|
+
attrs.push(...hash.attrs);
|
|
546
|
+
if (hash.partial || !optionEnds(code, hash.end)) opaque = true;
|
|
547
|
+
i = hash.end;
|
|
548
|
+
} else {
|
|
549
|
+
opaque = true;
|
|
550
|
+
i = start;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { attrs, found, opaque };
|
|
554
|
+
}
|
|
555
|
+
var OPTION_TAIL = /^(?:do|if|unless|while|until|rescue|then|and|or|end)\b/;
|
|
556
|
+
function optionEnds(code, at) {
|
|
557
|
+
const i = skipTrivia(code, at);
|
|
558
|
+
if (i >= code.length) return true;
|
|
559
|
+
const char = code[i];
|
|
560
|
+
if (char === "," || char === "}" || char === ")" || char === "]" || char === ";") return true;
|
|
561
|
+
return OPTION_TAIL.test(code.slice(i));
|
|
562
|
+
}
|
|
563
|
+
function readHash(code, open, base) {
|
|
564
|
+
const attrs = [];
|
|
565
|
+
let partial = false;
|
|
566
|
+
let i = open + 1;
|
|
567
|
+
for (; ; ) {
|
|
568
|
+
i = skipTrivia(code, i);
|
|
569
|
+
if (i >= code.length) {
|
|
570
|
+
partial = true;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
if (code[i] === "}") {
|
|
574
|
+
i++;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
if (code[i] === ",") {
|
|
578
|
+
i++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const entry = readEntry(code, i, base);
|
|
582
|
+
if (entry.attr) attrs.push(entry.attr);
|
|
583
|
+
else partial = true;
|
|
584
|
+
i = entry.end;
|
|
585
|
+
}
|
|
586
|
+
return { attrs, partial, end: i };
|
|
587
|
+
}
|
|
588
|
+
function readEntry(code, start, base) {
|
|
589
|
+
const key = readKey(code, start);
|
|
590
|
+
if (key === null) return { attr: null, end: skipValue(code, start) };
|
|
591
|
+
const valueStart = skipTrivia(code, key.valueAt);
|
|
592
|
+
const name = attributeName(key.name);
|
|
593
|
+
if (name === null) return { attr: null, end: skipValue(code, valueStart) };
|
|
594
|
+
const anchor = { keyStart: base + start, keyEnd: base + key.end };
|
|
595
|
+
const quote = code[valueStart];
|
|
596
|
+
if (quote === '"' || quote === "'") {
|
|
597
|
+
const literal = readString(code, valueStart);
|
|
598
|
+
const after = skipTrivia(code, literal.end);
|
|
599
|
+
const whole = after >= code.length || code[after] === "," || code[after] === "}";
|
|
600
|
+
if (literal.literal && whole) {
|
|
601
|
+
return {
|
|
602
|
+
attr: {
|
|
603
|
+
name,
|
|
604
|
+
value: literal.value,
|
|
605
|
+
...anchor,
|
|
606
|
+
valueStart: literal.verbatim ? base + literal.contentStart : void 0,
|
|
607
|
+
valueEnd: literal.verbatim ? base + literal.contentEnd : void 0
|
|
608
|
+
},
|
|
609
|
+
end: literal.end
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return { attr: { name, value: null, ...anchor }, end: skipValue(code, valueStart) };
|
|
614
|
+
}
|
|
615
|
+
function readKey(code, start) {
|
|
616
|
+
const char = code[start];
|
|
617
|
+
if (char === '"' || char === "'") {
|
|
618
|
+
const literal = readString(code, start);
|
|
619
|
+
const valueAt = keySeparator(code, literal.end);
|
|
620
|
+
if (!literal.literal || valueAt === null) return null;
|
|
621
|
+
return { name: literal.value, end: literal.end, valueAt };
|
|
622
|
+
}
|
|
623
|
+
if (char === ":" && IDENT_START.test(code[start + 1] ?? "")) {
|
|
624
|
+
const name = readIdentifier(code, start + 1);
|
|
625
|
+
const valueAt = hashRocket(code, name.end);
|
|
626
|
+
return valueAt === null ? null : { name: name.text, end: name.end, valueAt };
|
|
627
|
+
}
|
|
628
|
+
if (char !== void 0 && IDENT_START.test(char)) {
|
|
629
|
+
const name = readIdentifier(code, start);
|
|
630
|
+
if (code[name.end] !== ":" || code[name.end + 1] === ":") return null;
|
|
631
|
+
return { name: name.text, end: name.end, valueAt: name.end + 1 };
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
function attributeName(key) {
|
|
636
|
+
const name = key.replaceAll("_", "-").toLowerCase();
|
|
637
|
+
return name.length > 0 && !/[\s"'=<>/]/.test(name) ? `data-${name}` : null;
|
|
638
|
+
}
|
|
639
|
+
function keySeparator(code, at) {
|
|
640
|
+
if (code[at] === ":" && code[at + 1] !== ":") return at + 1;
|
|
641
|
+
return hashRocket(code, at);
|
|
642
|
+
}
|
|
643
|
+
function hashRocket(code, at) {
|
|
644
|
+
const arrow = skipTrivia(code, at);
|
|
645
|
+
return code.startsWith("=>", arrow) ? arrow + 2 : null;
|
|
646
|
+
}
|
|
647
|
+
function readIdentifier(code, start) {
|
|
648
|
+
let end = start;
|
|
649
|
+
while (end < code.length && IDENT_CHAR.test(code[end])) end++;
|
|
650
|
+
return { text: code.slice(start, end), end };
|
|
651
|
+
}
|
|
652
|
+
function skipValue(code, start) {
|
|
653
|
+
let depth = 0;
|
|
654
|
+
let i = start;
|
|
655
|
+
while (i < code.length) {
|
|
656
|
+
const char = code[i];
|
|
657
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
658
|
+
i = readString(code, i).end;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (char === "#") {
|
|
662
|
+
i = endOfLine(code, i);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const literal = skipLiteral(code, i);
|
|
666
|
+
if (literal >= 0) {
|
|
667
|
+
i = literal;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (char === "(" || char === "[" || char === "{") depth++;
|
|
671
|
+
else if (char === ")" || char === "]") depth--;
|
|
672
|
+
else if (char === "}") {
|
|
673
|
+
if (depth === 0) return i;
|
|
674
|
+
depth--;
|
|
675
|
+
} else if (char === "," && depth === 0) return i;
|
|
676
|
+
i++;
|
|
677
|
+
}
|
|
678
|
+
return code.length;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/inspector/erb.ts
|
|
682
|
+
var ERB_TAG = /<%[\s\S]*?%>/g;
|
|
683
|
+
function neutralizeErb(source) {
|
|
684
|
+
return source.replace(ERB_TAG, (match) => match.replace(/[^\n]/g, " "));
|
|
685
|
+
}
|
|
686
|
+
function erbRanges(source) {
|
|
687
|
+
return [...source.matchAll(ERB_TAG)].map(
|
|
688
|
+
(match) => [match.index ?? 0, (match.index ?? 0) + match[0].length]
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
function erbElements(source) {
|
|
692
|
+
const tags = readErbTags(source);
|
|
693
|
+
const blockEnds = matchBlocks(tags);
|
|
694
|
+
const index = new PositionIndex(source);
|
|
695
|
+
const elements = [];
|
|
696
|
+
for (const [ordinal, tag] of tags.entries()) {
|
|
697
|
+
if (!tag.output) continue;
|
|
698
|
+
const option = readDataOption(tag.code, tag.codeStart);
|
|
699
|
+
if (!option.found) continue;
|
|
700
|
+
const end = blockEnds.get(ordinal) ?? tag.end;
|
|
701
|
+
elements.push({
|
|
702
|
+
tag: ERB_ELEMENT_TAG,
|
|
703
|
+
attrs: option.attrs.map((attr) => toParsedAttr(attr, index)),
|
|
704
|
+
start: tag.start,
|
|
705
|
+
// The token a reader can act on is the delimiter that opens the call.
|
|
706
|
+
sourceLength: tag.codeStart - tag.start,
|
|
707
|
+
end,
|
|
708
|
+
container: end > tag.end,
|
|
709
|
+
opaque: option.opaque
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
return elements;
|
|
713
|
+
}
|
|
714
|
+
function toParsedAttr(attr, index) {
|
|
715
|
+
const at = index.at(attr.keyStart);
|
|
716
|
+
return {
|
|
717
|
+
name: attr.name,
|
|
718
|
+
value: attr.value ?? "",
|
|
719
|
+
line: at.line,
|
|
720
|
+
column: at.column,
|
|
721
|
+
sourceLength: attr.keyEnd - attr.keyStart,
|
|
722
|
+
valueStart: attr.valueStart,
|
|
723
|
+
valueEnd: attr.valueEnd,
|
|
724
|
+
dynamicValue: attr.value === null
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function readErbTags(source) {
|
|
728
|
+
const tags = [];
|
|
729
|
+
for (const match of source.matchAll(ERB_TAG)) {
|
|
730
|
+
const raw = match[0];
|
|
731
|
+
const start = match.index ?? 0;
|
|
732
|
+
let open = 2;
|
|
733
|
+
let output = false;
|
|
734
|
+
if (raw[open] === "#") continue;
|
|
735
|
+
if (raw[open] === "=") {
|
|
736
|
+
output = true;
|
|
737
|
+
open++;
|
|
738
|
+
if (raw[open] === "=") open++;
|
|
739
|
+
} else if (raw[open] === "-") {
|
|
740
|
+
open++;
|
|
741
|
+
}
|
|
742
|
+
let close = raw.length - 2;
|
|
743
|
+
if (raw[close - 1] === "-") close--;
|
|
744
|
+
if (close < open) close = open;
|
|
745
|
+
tags.push({
|
|
746
|
+
start,
|
|
747
|
+
end: start + raw.length,
|
|
748
|
+
output,
|
|
749
|
+
code: raw.slice(open, close),
|
|
750
|
+
codeStart: start + open
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
return tags;
|
|
754
|
+
}
|
|
755
|
+
function matchBlocks(tags) {
|
|
756
|
+
const ends = /* @__PURE__ */ new Map();
|
|
757
|
+
const open = [];
|
|
758
|
+
for (const [ordinal, tag] of tags.entries()) {
|
|
759
|
+
for (const event of blockEvents(tag.code)) {
|
|
760
|
+
if (event === "open") {
|
|
761
|
+
open.push(ordinal);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const opener = open.pop();
|
|
765
|
+
if (opener !== void 0) ends.set(opener, tag.end);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return ends;
|
|
769
|
+
}
|
|
770
|
+
var BLOCK_OPENERS = /* @__PURE__ */ new Set(["case", "begin", "def", "class", "module", "for"]);
|
|
771
|
+
var BLOCK_MODIFIERS = /* @__PURE__ */ new Set(["if", "unless", "while", "until"]);
|
|
772
|
+
var BLOCK_CONTINUERS = /* @__PURE__ */ new Set(["elsif", "else", "when", "in", "then", "rescue", "ensure"]);
|
|
773
|
+
function blockEvents(source) {
|
|
774
|
+
const code = maskHeredocs(source);
|
|
775
|
+
const events = [];
|
|
776
|
+
let previous = "";
|
|
777
|
+
let loopHeader = false;
|
|
778
|
+
let i = 0;
|
|
779
|
+
while (i < code.length) {
|
|
780
|
+
const char = code[i];
|
|
781
|
+
if (char === "\n" || char === ";") {
|
|
782
|
+
previous = char;
|
|
783
|
+
loopHeader = false;
|
|
784
|
+
i++;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (SPACE.has(char)) {
|
|
788
|
+
i++;
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
if (char === "#") {
|
|
792
|
+
i = endOfLine(code, i);
|
|
793
|
+
previous = "\n";
|
|
794
|
+
loopHeader = false;
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
798
|
+
i = readString(code, i).end;
|
|
799
|
+
previous = "value";
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (char === ":" && IDENT_START.test(code[i + 1] ?? "")) {
|
|
803
|
+
i = identifierEnd(code, i + 1);
|
|
804
|
+
previous = "value";
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const literal = skipLiteral(code, i);
|
|
808
|
+
if (literal >= 0) {
|
|
809
|
+
i = literal;
|
|
810
|
+
previous = "value";
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (char === "{") {
|
|
814
|
+
events.push("open");
|
|
815
|
+
previous = "{";
|
|
816
|
+
i++;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (char === "}") {
|
|
820
|
+
events.push("close");
|
|
821
|
+
previous = "value";
|
|
822
|
+
i++;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (char === ")" || char === "]") {
|
|
826
|
+
previous = "value";
|
|
827
|
+
i++;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if (!IDENT_START.test(char)) {
|
|
831
|
+
previous = char;
|
|
832
|
+
i++;
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const end = identifierEnd(code, i);
|
|
836
|
+
const word = code.slice(i, end);
|
|
837
|
+
const keyword = !(code[end] === ":" && code[end + 1] !== ":") && code[i - 1] !== ".";
|
|
838
|
+
i = end;
|
|
839
|
+
if (keyword && word === "end") {
|
|
840
|
+
events.push("close");
|
|
841
|
+
previous = "value";
|
|
842
|
+
} else if (keyword && word === "do") {
|
|
843
|
+
if (loopHeader) loopHeader = false;
|
|
844
|
+
else events.push("open");
|
|
845
|
+
previous = "do";
|
|
846
|
+
} else if (keyword && BLOCK_OPENERS.has(word)) {
|
|
847
|
+
events.push("open");
|
|
848
|
+
loopHeader = word === "for";
|
|
849
|
+
previous = word;
|
|
850
|
+
} else if (keyword && BLOCK_MODIFIERS.has(word)) {
|
|
851
|
+
if (previous !== "value") {
|
|
852
|
+
events.push("open");
|
|
853
|
+
loopHeader = word !== "if" && word !== "unless";
|
|
854
|
+
}
|
|
855
|
+
previous = word;
|
|
856
|
+
} else if (keyword && BLOCK_CONTINUERS.has(word)) {
|
|
857
|
+
previous = word;
|
|
858
|
+
} else {
|
|
859
|
+
previous = "value";
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return events;
|
|
863
|
+
}
|
|
864
|
+
function identifierEnd(code, start) {
|
|
865
|
+
let end = start;
|
|
866
|
+
while (end < code.length && IDENT_CHAR.test(code[end])) end++;
|
|
867
|
+
return end;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/inspector/extract.ts
|
|
871
|
+
var NAMESPACE_PREFIX = "stimeo--";
|
|
872
|
+
function dasherize(value) {
|
|
873
|
+
return value.replace(/([A-Z])/g, (_match, char) => `-${char.toLowerCase()}`);
|
|
874
|
+
}
|
|
875
|
+
function controllerIdentifiers(dataControllerValue) {
|
|
876
|
+
const seen = /* @__PURE__ */ new Set();
|
|
877
|
+
for (const token of dataControllerValue.split(/\s+/)) {
|
|
878
|
+
if (token.startsWith(NAMESPACE_PREFIX)) seen.add(token);
|
|
879
|
+
}
|
|
880
|
+
return [...seen];
|
|
881
|
+
}
|
|
882
|
+
function parseTargetAttr(attrName) {
|
|
883
|
+
const match = /^data-(stimeo--[a-z0-9][a-z0-9-]*)-target$/.exec(attrName);
|
|
884
|
+
return match?.[1] ?? null;
|
|
885
|
+
}
|
|
886
|
+
function parseValueAttr(attrName, knownIdentifiers) {
|
|
887
|
+
const match = /^data-(stimeo--[a-z0-9-]+)-value$/.exec(attrName);
|
|
888
|
+
if (!match) return null;
|
|
889
|
+
const inner = match[1] ?? "";
|
|
890
|
+
let best = null;
|
|
891
|
+
for (const id of knownIdentifiers) {
|
|
892
|
+
if (inner.startsWith(`${id}-`)) {
|
|
893
|
+
const valueToken = inner.slice(id.length + 1);
|
|
894
|
+
if (valueToken.length > 0 && (!best || id.length > best.identifier.length)) {
|
|
895
|
+
best = { identifier: id, valueToken };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (best) return best;
|
|
900
|
+
const rest = inner.slice(NAMESPACE_PREFIX.length);
|
|
901
|
+
const dash = rest.indexOf("-");
|
|
902
|
+
const identifierGuess = dash === -1 ? inner : `${NAMESPACE_PREFIX}${rest.slice(0, dash)}`;
|
|
903
|
+
const valueGuess = dash === -1 ? "" : rest.slice(dash + 1);
|
|
904
|
+
return { identifier: null, valueToken: valueGuess || identifierGuess };
|
|
905
|
+
}
|
|
906
|
+
function actionDescriptors(dataActionValue) {
|
|
907
|
+
const descriptors = [];
|
|
908
|
+
for (const descriptor of dataActionValue.split(/\s+/)) {
|
|
909
|
+
const hash = descriptor.indexOf("#");
|
|
910
|
+
if (hash === -1) continue;
|
|
911
|
+
let lhs = descriptor.slice(0, hash);
|
|
912
|
+
const arrow = lhs.indexOf("->");
|
|
913
|
+
if (arrow !== -1) lhs = lhs.slice(arrow + 2);
|
|
914
|
+
if (!lhs.startsWith(NAMESPACE_PREFIX)) continue;
|
|
915
|
+
const method = /^[a-zA-Z_$][\w$]*/.exec(descriptor.slice(hash + 1))?.[0] ?? "";
|
|
916
|
+
descriptors.push({ identifier: lhs, method });
|
|
917
|
+
}
|
|
918
|
+
return descriptors;
|
|
919
|
+
}
|
|
920
|
+
function isStimeoDataAttr(attrName) {
|
|
921
|
+
return attrName.startsWith(`data-${NAMESPACE_PREFIX}`);
|
|
922
|
+
}
|
|
923
|
+
|
|
261
924
|
// src/inspector/types.ts
|
|
262
925
|
var DIAGNOSTIC_CODES = [
|
|
263
926
|
"unknown-controller",
|
|
@@ -267,28 +930,40 @@ var DIAGNOSTIC_CODES = [
|
|
|
267
930
|
"unknown-action-method",
|
|
268
931
|
"orphan-target",
|
|
269
932
|
"missing-required-target",
|
|
933
|
+
"missing-conditional-target",
|
|
270
934
|
"missing-aria",
|
|
271
935
|
"invalid-aria-value",
|
|
272
936
|
"keyboard-inaccessible",
|
|
273
937
|
"unresolved-idref",
|
|
274
938
|
"managed-aria",
|
|
275
939
|
"composition-mismatch",
|
|
940
|
+
"missing-companion",
|
|
941
|
+
"undeclared-target",
|
|
942
|
+
"cardinality-violation",
|
|
943
|
+
"forbidden-aria",
|
|
276
944
|
"unknown-ignore-code"
|
|
277
945
|
];
|
|
278
946
|
|
|
279
947
|
// src/inspector/check.ts
|
|
280
948
|
function checkSource(source, manifest) {
|
|
281
949
|
const diagnostics = [];
|
|
282
|
-
const tree = parseHtml(neutralizeErb(source));
|
|
950
|
+
const tree = parseHtml(neutralizeErb(source), erbElements(source));
|
|
283
951
|
const known = manifest.controllers;
|
|
284
952
|
const knownIdentifiers = Object.keys(known);
|
|
285
|
-
const
|
|
286
|
-
(m) => [m.index ?? 0, (m.index ?? 0) + m[0].length]
|
|
287
|
-
);
|
|
953
|
+
const ranges = erbRanges(source);
|
|
288
954
|
const isDynamicValue = (attr) => {
|
|
955
|
+
if (attr.dynamicValue !== void 0) return attr.dynamicValue;
|
|
289
956
|
const { valueStart, valueEnd } = attr;
|
|
290
957
|
if (valueStart === void 0 || valueEnd === void 0) return false;
|
|
291
|
-
return
|
|
958
|
+
return ranges.some(([start, end]) => start < valueEnd && end > valueStart);
|
|
959
|
+
};
|
|
960
|
+
const conditionHolds = (scope, identifier, when) => {
|
|
961
|
+
if (!when) return true;
|
|
962
|
+
const hostAttr = scope.attrs.find(
|
|
963
|
+
(a) => a.name === `data-${identifier}-${dasherize(when.value)}-value`
|
|
964
|
+
);
|
|
965
|
+
if (hostAttr && isDynamicValue(hostAttr)) return null;
|
|
966
|
+
return when.equals.includes(hostAttr ? hostAttr.value.trim() : when.default);
|
|
292
967
|
};
|
|
293
968
|
const suppressed = (node, code) => {
|
|
294
969
|
if (code === "unknown-ignore-code") return false;
|
|
@@ -297,7 +972,9 @@ function checkSource(source, manifest) {
|
|
|
297
972
|
const ignore = current.attrs.find((a) => a.name === IGNORE_ATTR);
|
|
298
973
|
if (ignore) {
|
|
299
974
|
const codes = ignore.value.trim();
|
|
300
|
-
if (codes.length === 0
|
|
975
|
+
if (codes.length === 0) {
|
|
976
|
+
if (!isDynamicValue(ignore)) return true;
|
|
977
|
+
} else if (codes.split(/\s+/).includes(code)) return true;
|
|
301
978
|
}
|
|
302
979
|
current = current.parent;
|
|
303
980
|
}
|
|
@@ -306,6 +983,26 @@ function checkSource(source, manifest) {
|
|
|
306
983
|
const report = (owner, code, severity, message, at, suggestion, fix) => {
|
|
307
984
|
if (!suppressed(owner, code)) push(diagnostics, code, severity, message, at, suggestion, fix);
|
|
308
985
|
};
|
|
986
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
987
|
+
const unnamedTargets = [];
|
|
988
|
+
const targetsUnreadable = (container, scope, identifier) => {
|
|
989
|
+
for (const node of unresolved) {
|
|
990
|
+
if (isWithin(node, container) && findOwner(node, identifier) === scope) return true;
|
|
991
|
+
}
|
|
992
|
+
for (const { node, identifier: hidden } of unnamedTargets) {
|
|
993
|
+
if (hidden !== identifier) continue;
|
|
994
|
+
if (isWithin(node, container) && findOwner(node, identifier) === scope) return true;
|
|
995
|
+
}
|
|
996
|
+
return false;
|
|
997
|
+
};
|
|
998
|
+
const unresolvedAbove = (node) => {
|
|
999
|
+
let current = node;
|
|
1000
|
+
while (current && current.tag !== "#root") {
|
|
1001
|
+
if (unresolved.has(current)) return true;
|
|
1002
|
+
current = current.parent;
|
|
1003
|
+
}
|
|
1004
|
+
return false;
|
|
1005
|
+
};
|
|
309
1006
|
const scopes = [];
|
|
310
1007
|
const presence = /* @__PURE__ */ new Map();
|
|
311
1008
|
const targetNodes = /* @__PURE__ */ new Map();
|
|
@@ -337,7 +1034,44 @@ function checkSource(source, manifest) {
|
|
|
337
1034
|
if (found) found.push(el);
|
|
338
1035
|
else byName.set(target, [el]);
|
|
339
1036
|
};
|
|
1037
|
+
const contentHolds = (scope, identifier, element, condition) => {
|
|
1038
|
+
if (!condition) return true;
|
|
1039
|
+
const nodes = targetNodes.get(scope)?.get(identifier)?.get(condition.target) ?? [];
|
|
1040
|
+
let held = 0;
|
|
1041
|
+
for (const node of nodes) if (isWithin(node, element)) held += 1;
|
|
1042
|
+
if (condition.min !== void 0 && held < condition.min) return false;
|
|
1043
|
+
if (condition.max !== void 0 && held > condition.max) return false;
|
|
1044
|
+
return true;
|
|
1045
|
+
};
|
|
1046
|
+
const elementHolds = (element, condition) => !condition?.exceptTags.includes(element.tag);
|
|
1047
|
+
const roleTally = /* @__PURE__ */ new Map();
|
|
1048
|
+
const focusableRoleTally = /* @__PURE__ */ new Map();
|
|
1049
|
+
const documentHolds = (condition) => {
|
|
1050
|
+
if (condition === void 0) return false;
|
|
1051
|
+
const tally = condition.focusable ? focusableRoleTally : roleTally;
|
|
1052
|
+
return (tally.get(condition.role) ?? 0) >= condition.atLeast;
|
|
1053
|
+
};
|
|
1054
|
+
const declarationAttrs = /* @__PURE__ */ new Set();
|
|
1055
|
+
for (const controller of Object.values(known)) {
|
|
1056
|
+
for (const rule of controller.targetDeclarations) declarationAttrs.add(rule.attr);
|
|
1057
|
+
}
|
|
1058
|
+
const declarationCandidates = [];
|
|
340
1059
|
walk(tree, (node) => {
|
|
1060
|
+
const declaredController = node.attrs.find((a) => a.name === "data-controller");
|
|
1061
|
+
if (node.opaque || declaredController && isDynamicValue(declaredController)) {
|
|
1062
|
+
unresolved.add(node);
|
|
1063
|
+
}
|
|
1064
|
+
if (declarationAttrs.size > 0 && node.attrs.some((a) => declarationAttrs.has(a.name))) {
|
|
1065
|
+
declarationCandidates.push(node);
|
|
1066
|
+
}
|
|
1067
|
+
const roleAttr = node.attrs.find((a) => a.name === "role");
|
|
1068
|
+
if (roleAttr && !isDynamicValue(roleAttr)) {
|
|
1069
|
+
const role = roleAttr.value.trim();
|
|
1070
|
+
roleTally.set(role, (roleTally.get(role) ?? 0) + 1);
|
|
1071
|
+
if (isFocusable(node, "tab")) {
|
|
1072
|
+
focusableRoleTally.set(role, (focusableRoleTally.get(role) ?? 0) + 1);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
341
1075
|
for (const attr of node.attrs) {
|
|
342
1076
|
if (attr.name === IGNORE_ATTR) {
|
|
343
1077
|
for (const token of attr.value.split(/\s+/).filter((t) => t.length > 0)) {
|
|
@@ -416,7 +1150,7 @@ function checkSource(source, manifest) {
|
|
|
416
1150
|
);
|
|
417
1151
|
continue;
|
|
418
1152
|
}
|
|
419
|
-
const targetName = attr.value.trim();
|
|
1153
|
+
const targetName = isDynamicValue(attr) ? "" : attr.value.trim();
|
|
420
1154
|
if (targetName.length > 0 && !controller.targets.includes(targetName)) {
|
|
421
1155
|
const best = nearestName(targetName, controller.targets);
|
|
422
1156
|
report(
|
|
@@ -434,14 +1168,18 @@ function checkSource(source, manifest) {
|
|
|
434
1168
|
if (targetName.length > 0) {
|
|
435
1169
|
recordPresence(owner, targetIdentifier, targetName);
|
|
436
1170
|
recordTargetNode(owner, targetIdentifier, targetName, node);
|
|
1171
|
+
} else if (isDynamicValue(attr)) {
|
|
1172
|
+
unnamedTargets.push({ node, identifier: targetIdentifier });
|
|
437
1173
|
}
|
|
438
1174
|
} else if (!inDeclaredFragment(node, targetIdentifier)) {
|
|
1175
|
+
const uncertain = unresolvedAbove(node);
|
|
439
1176
|
report(
|
|
440
1177
|
node,
|
|
441
1178
|
"orphan-target",
|
|
442
|
-
"error",
|
|
1179
|
+
uncertain ? "warning" : "error",
|
|
443
1180
|
`Target "${attr.name}" has no enclosing controller "${targetIdentifier}".`,
|
|
444
|
-
attr
|
|
1181
|
+
attr,
|
|
1182
|
+
uncertain ? UNRESOLVED_HOST_HINT : void 0
|
|
445
1183
|
);
|
|
446
1184
|
}
|
|
447
1185
|
continue;
|
|
@@ -504,29 +1242,57 @@ function checkSource(source, manifest) {
|
|
|
504
1242
|
}
|
|
505
1243
|
}
|
|
506
1244
|
});
|
|
1245
|
+
const scopeIsComplete = (container, scope, identifier) => container.origin === "markup" && !targetsUnreadable(container, scope, identifier);
|
|
507
1246
|
for (const { node, identifier } of scopes) {
|
|
508
1247
|
const controller = known[identifier];
|
|
509
1248
|
if (!controller) continue;
|
|
1249
|
+
const complete = scopeIsComplete(node, node, identifier);
|
|
510
1250
|
const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
|
|
511
1251
|
for (const required of controller.requiredTargets) {
|
|
512
1252
|
if (!present.has(required)) {
|
|
513
1253
|
report(
|
|
514
1254
|
node,
|
|
515
1255
|
"missing-required-target",
|
|
516
|
-
"error",
|
|
1256
|
+
complete ? "error" : "warning",
|
|
517
1257
|
`"${identifier}" is missing required target "${required}".`,
|
|
518
|
-
node
|
|
1258
|
+
node,
|
|
1259
|
+
complete ? void 0 : UNRESOLVED_TARGET_HINT
|
|
519
1260
|
);
|
|
520
1261
|
}
|
|
521
1262
|
}
|
|
522
1263
|
}
|
|
1264
|
+
for (const { node, identifier } of scopes) {
|
|
1265
|
+
const controller = known[identifier];
|
|
1266
|
+
if (!controller) continue;
|
|
1267
|
+
const complete = scopeIsComplete(node, node, identifier);
|
|
1268
|
+
const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
|
|
1269
|
+
for (const rule of controller.conditionalTargets) {
|
|
1270
|
+
if (!present.has(rule.whenPresent)) continue;
|
|
1271
|
+
const missing = rule.require.filter((target) => !present.has(target));
|
|
1272
|
+
if (missing.length === 0) continue;
|
|
1273
|
+
report(
|
|
1274
|
+
node,
|
|
1275
|
+
"missing-conditional-target",
|
|
1276
|
+
complete ? "error" : "warning",
|
|
1277
|
+
`"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
|
|
1278
|
+
node,
|
|
1279
|
+
complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
523
1283
|
for (const { node, identifier } of scopes) {
|
|
524
1284
|
const controller = known[identifier];
|
|
525
1285
|
if (!controller) continue;
|
|
526
1286
|
for (const req of controller.a11y) {
|
|
1287
|
+
if (conditionHolds(node, identifier, req.when) !== true) continue;
|
|
527
1288
|
const elements = req.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(req.target) ?? [];
|
|
1289
|
+
if (req.whenDocument && !documentHolds(req.whenDocument)) continue;
|
|
1290
|
+
const severity = documentHolds(req.escalateWhen) ? "error" : req.severity ?? "error";
|
|
528
1291
|
for (const el of elements) {
|
|
529
|
-
|
|
1292
|
+
if (!hasReadableMarkup(el)) continue;
|
|
1293
|
+
if (!contentHolds(node, identifier, el, req.whenContains)) continue;
|
|
1294
|
+
if (!elementHolds(el, req.whenElement)) continue;
|
|
1295
|
+
checkA11y(report, el, identifier, req, severity);
|
|
530
1296
|
}
|
|
531
1297
|
}
|
|
532
1298
|
}
|
|
@@ -537,6 +1303,7 @@ function checkSource(source, manifest) {
|
|
|
537
1303
|
const reach = req.reach ?? "tab";
|
|
538
1304
|
const elements = targetNodes.get(node)?.get(identifier)?.get(req.target) ?? [];
|
|
539
1305
|
for (const el of elements) {
|
|
1306
|
+
if (!hasReadableMarkup(el)) continue;
|
|
540
1307
|
if (!isFocusable(el, reach)) {
|
|
541
1308
|
const problem = reach === "tab" ? 'not in the Tab order (needs a native control or tabindex="0")' : "not focusable (needs a native control or tabindex)";
|
|
542
1309
|
report(
|
|
@@ -558,6 +1325,7 @@ function checkSource(source, manifest) {
|
|
|
558
1325
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
559
1326
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
560
1327
|
for (const el of elements) {
|
|
1328
|
+
if (!hasReadableMarkup(el)) continue;
|
|
561
1329
|
for (const name of rule.attrs) {
|
|
562
1330
|
const attr = el.attrs.find((a) => a.name === name);
|
|
563
1331
|
if (!attr) continue;
|
|
@@ -573,6 +1341,33 @@ function checkSource(source, manifest) {
|
|
|
573
1341
|
}
|
|
574
1342
|
}
|
|
575
1343
|
}
|
|
1344
|
+
for (const { node, identifier } of scopes) {
|
|
1345
|
+
const controller = known[identifier];
|
|
1346
|
+
if (!controller) continue;
|
|
1347
|
+
for (const rule of controller.forbiddenAria) {
|
|
1348
|
+
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
1349
|
+
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
1350
|
+
for (const el of elements) {
|
|
1351
|
+
if (!hasReadableMarkup(el)) continue;
|
|
1352
|
+
if (!contentHolds(node, identifier, el, rule.whenContains)) continue;
|
|
1353
|
+
for (const name of rule.attrs) {
|
|
1354
|
+
const attr = el.attrs.find((a) => a.name === name);
|
|
1355
|
+
if (!attr) continue;
|
|
1356
|
+
if (rule.values && (isDynamicValue(attr) || !rule.values.includes(attr.value.trim()))) {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
report(
|
|
1360
|
+
el,
|
|
1361
|
+
"forbidden-aria",
|
|
1362
|
+
"warning",
|
|
1363
|
+
`${name} on the ${where} of "${identifier}" contradicts the markup around it.`,
|
|
1364
|
+
attr,
|
|
1365
|
+
rule.suggestion
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
576
1371
|
for (const { node, identifier } of scopes) {
|
|
577
1372
|
const controller = known[identifier];
|
|
578
1373
|
if (!controller) continue;
|
|
@@ -581,16 +1376,10 @@ function checkSource(source, manifest) {
|
|
|
581
1376
|
const elements = rule.target === "" ? [node] : targeted ?? (rule.fallbackToScope ? [node] : []);
|
|
582
1377
|
const where = rule.target === "" || !targeted ? "scope element" : `"${rule.target}" target`;
|
|
583
1378
|
const when = rule.when;
|
|
584
|
-
if (when)
|
|
585
|
-
const hostAttr = node.attrs.find(
|
|
586
|
-
(a) => a.name === `data-${identifier}-${dasherize(when.value)}-value`
|
|
587
|
-
);
|
|
588
|
-
if (hostAttr && isDynamicValue(hostAttr)) continue;
|
|
589
|
-
const hostEffective = hostAttr ? hostAttr.value.trim() : when.default;
|
|
590
|
-
if (!when.equals.includes(hostEffective)) continue;
|
|
591
|
-
}
|
|
1379
|
+
if (conditionHolds(node, identifier, when) !== true) continue;
|
|
592
1380
|
const hostContext = when ? ` (${when.value} ${list(when.equals)})` : "";
|
|
593
1381
|
for (const el of elements) {
|
|
1382
|
+
if (el.opaque) continue;
|
|
594
1383
|
const declared = el.attrs.find((a) => a.name === "data-controller");
|
|
595
1384
|
if (!declared || !controllerIdentifiers(declared.value).includes(rule.coController)) {
|
|
596
1385
|
continue;
|
|
@@ -624,6 +1413,115 @@ function checkSource(source, manifest) {
|
|
|
624
1413
|
}
|
|
625
1414
|
}
|
|
626
1415
|
}
|
|
1416
|
+
for (const { node, identifier } of scopes) {
|
|
1417
|
+
const controller = known[identifier];
|
|
1418
|
+
if (!controller) continue;
|
|
1419
|
+
for (const rule of controller.companions) {
|
|
1420
|
+
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
1421
|
+
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
1422
|
+
for (const el of elements) {
|
|
1423
|
+
if (el.opaque) continue;
|
|
1424
|
+
const declared = el.attrs.find((a) => a.name === "data-controller");
|
|
1425
|
+
if (declared && isDynamicValue(declared)) continue;
|
|
1426
|
+
if (declared && controllerIdentifiers(declared.value).includes(rule.controller)) continue;
|
|
1427
|
+
report(
|
|
1428
|
+
el,
|
|
1429
|
+
"missing-companion",
|
|
1430
|
+
"error",
|
|
1431
|
+
`The ${where} of "${identifier}" must also declare "${rule.controller}" in data-controller.`,
|
|
1432
|
+
declared ?? el,
|
|
1433
|
+
rule.suggestion,
|
|
1434
|
+
// Appending to an existing list is unambiguous; creating the
|
|
1435
|
+
// attribute from nothing is an insertion the anchor cannot express.
|
|
1436
|
+
declared ? valueFix(
|
|
1437
|
+
declared,
|
|
1438
|
+
`${declared.value.trim()} ${rule.controller}`.trim(),
|
|
1439
|
+
`Add "${rule.controller}" to data-controller`
|
|
1440
|
+
) : void 0
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
for (const { node, identifier } of scopes) {
|
|
1446
|
+
const controller = known[identifier];
|
|
1447
|
+
if (!controller) continue;
|
|
1448
|
+
if (controller.targetDeclarations.length === 0) continue;
|
|
1449
|
+
const targetAttrName = `data-${identifier}-target`;
|
|
1450
|
+
for (const rule of controller.targetDeclarations) {
|
|
1451
|
+
for (const el of declarationCandidates) {
|
|
1452
|
+
const marker = el.attrs.find((a) => a.name === rule.attr);
|
|
1453
|
+
if (!marker || isDynamicValue(marker)) continue;
|
|
1454
|
+
const value = marker.value.trim();
|
|
1455
|
+
if (value.length === 0) continue;
|
|
1456
|
+
if (rule.values && !rule.values.includes(value)) continue;
|
|
1457
|
+
if (findOwner(el, identifier) !== node) continue;
|
|
1458
|
+
const declaredAs = el.attrs.find((a) => a.name === targetAttrName);
|
|
1459
|
+
if (declaredAs && !isDynamicValue(declaredAs) && declaredAs.value.trim() === rule.target) {
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (declaredAs && isDynamicValue(declaredAs)) continue;
|
|
1463
|
+
report(
|
|
1464
|
+
el,
|
|
1465
|
+
"undeclared-target",
|
|
1466
|
+
"error",
|
|
1467
|
+
`${rule.attr}="${value}" inside "${identifier}" is not declared as its "${rule.target}" target, so the controller never manages it.`,
|
|
1468
|
+
marker,
|
|
1469
|
+
rule.suggestion
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
for (const { node, identifier } of scopes) {
|
|
1475
|
+
const controller = known[identifier];
|
|
1476
|
+
if (!controller) continue;
|
|
1477
|
+
for (const rule of controller.cardinality) {
|
|
1478
|
+
if (conditionHolds(node, identifier, rule.when) !== true) continue;
|
|
1479
|
+
const byName = targetNodes.get(node)?.get(identifier);
|
|
1480
|
+
const counted = byName?.get(rule.target) ?? [];
|
|
1481
|
+
const containers = rule.within === "" ? [node] : byName?.get(rule.within) ?? [];
|
|
1482
|
+
const what = describeCounted(rule.target, rule.attr, rule.values);
|
|
1483
|
+
const where = rule.within === "" ? "controller scope" : `"${rule.within}" target`;
|
|
1484
|
+
for (const container of containers) {
|
|
1485
|
+
const matched = [];
|
|
1486
|
+
for (const el of counted) {
|
|
1487
|
+
if (!isWithin(el, container)) continue;
|
|
1488
|
+
if (rule.attr === void 0) {
|
|
1489
|
+
matched.push({ el, at: el });
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
const attr = el.attrs.find((a) => a.name === rule.attr);
|
|
1493
|
+
if (!attr) continue;
|
|
1494
|
+
if (rule.values && (isDynamicValue(attr) || !rule.values.includes(attr.value.trim()))) {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
matched.push({ el, at: attr });
|
|
1498
|
+
}
|
|
1499
|
+
if (rule.max !== void 0 && matched.length > rule.max) {
|
|
1500
|
+
for (const { el, at } of matched.slice(rule.max)) {
|
|
1501
|
+
report(
|
|
1502
|
+
el,
|
|
1503
|
+
"cardinality-violation",
|
|
1504
|
+
"error",
|
|
1505
|
+
`"${identifier}" allows at most ${rule.max} ${what} per ${where}, but found ${matched.length}.`,
|
|
1506
|
+
at,
|
|
1507
|
+
rule.suggestion
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (rule.min !== void 0 && matched.length < rule.min) {
|
|
1512
|
+
const complete = scopeIsComplete(container, node, identifier);
|
|
1513
|
+
report(
|
|
1514
|
+
container,
|
|
1515
|
+
"cardinality-violation",
|
|
1516
|
+
complete ? "error" : "warning",
|
|
1517
|
+
`"${identifier}" requires at least ${rule.min} ${what} per ${where}, but found ${matched.length}.`,
|
|
1518
|
+
container,
|
|
1519
|
+
complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
627
1525
|
const ids = /* @__PURE__ */ new Set();
|
|
628
1526
|
walk(tree, (node) => {
|
|
629
1527
|
const id = node.attrs.find((a) => a.name === "id");
|
|
@@ -661,7 +1559,12 @@ function checkSource(source, manifest) {
|
|
|
661
1559
|
diagnostics.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
662
1560
|
return diagnostics;
|
|
663
1561
|
}
|
|
664
|
-
|
|
1562
|
+
var UNRESOLVED_TARGET_HINT = "Part of this scope is rendered by a helper or a non-literal data: hash, so the target may exist at runtime.";
|
|
1563
|
+
var UNRESOLVED_HOST_HINT = "An enclosing helper's data: option is not a literal hash, so the host controller may be declared at runtime.";
|
|
1564
|
+
function hasReadableMarkup(el) {
|
|
1565
|
+
return el.origin === "markup";
|
|
1566
|
+
}
|
|
1567
|
+
function checkA11y(report, el, identifier, req, severity) {
|
|
665
1568
|
const groups = [
|
|
666
1569
|
{ attrs: req.attrs, values: req.values },
|
|
667
1570
|
...req.or ?? []
|
|
@@ -690,11 +1593,12 @@ function checkA11y(report, el, identifier, req) {
|
|
|
690
1593
|
}
|
|
691
1594
|
}
|
|
692
1595
|
if (!anyPresent) {
|
|
1596
|
+
const verb = severity === "warning" ? "recommends" : "requires";
|
|
693
1597
|
report(
|
|
694
1598
|
el,
|
|
695
1599
|
"missing-aria",
|
|
696
|
-
|
|
697
|
-
`"${identifier}"
|
|
1600
|
+
severity,
|
|
1601
|
+
`"${identifier}" ${verb} ${describeAttrs([...allowedByAttr.keys()])} on its ${where}.`,
|
|
698
1602
|
el,
|
|
699
1603
|
req.suggestion
|
|
700
1604
|
);
|
|
@@ -713,6 +1617,19 @@ function checkA11y(report, el, identifier, req) {
|
|
|
713
1617
|
);
|
|
714
1618
|
}
|
|
715
1619
|
}
|
|
1620
|
+
function isWithin(node, container) {
|
|
1621
|
+
let current = node;
|
|
1622
|
+
while (current) {
|
|
1623
|
+
if (current === container) return true;
|
|
1624
|
+
current = current.parent;
|
|
1625
|
+
}
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
function describeCounted(target, attr, values) {
|
|
1629
|
+
if (attr === void 0) return `"${target}" targets`;
|
|
1630
|
+
const constrained = values && values.length > 0 ? ` set to ${list(values)}` : "";
|
|
1631
|
+
return `"${target}" targets with ${attr}${constrained}`;
|
|
1632
|
+
}
|
|
716
1633
|
var NATIVE_FOCUSABLE_TAGS = /* @__PURE__ */ new Set(["button", "select", "textarea", "summary"]);
|
|
717
1634
|
function isNativelyFocusable(el) {
|
|
718
1635
|
if (NATIVE_FOCUSABLE_TAGS.has(el.tag)) return true;
|
|
@@ -775,6 +1692,7 @@ function inDeclaredFragment(node, identifier) {
|
|
|
775
1692
|
return false;
|
|
776
1693
|
}
|
|
777
1694
|
function anchorLength(at) {
|
|
1695
|
+
if (at.sourceLength !== void 0) return at.sourceLength;
|
|
778
1696
|
return "name" in at ? at.name.length : at.tag.length + 1;
|
|
779
1697
|
}
|
|
780
1698
|
function push(out, code, severity, message, at, suggestion, fix) {
|
|
@@ -866,10 +1784,15 @@ var CONTROLLER_ARRAY_FIELDS = [
|
|
|
866
1784
|
"actions",
|
|
867
1785
|
"events",
|
|
868
1786
|
"requiredTargets",
|
|
1787
|
+
"conditionalTargets",
|
|
869
1788
|
"a11y",
|
|
870
1789
|
"keyboard",
|
|
871
1790
|
"managedAria",
|
|
872
|
-
"compositions"
|
|
1791
|
+
"compositions",
|
|
1792
|
+
"companions",
|
|
1793
|
+
"targetDeclarations",
|
|
1794
|
+
"cardinality",
|
|
1795
|
+
"forbiddenAria"
|
|
873
1796
|
];
|
|
874
1797
|
function isCompatibleManifest(value) {
|
|
875
1798
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|