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
|
@@ -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,6 +256,673 @@ 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
|
+
return { attr: { name, value: null, ...anchor }, end: skipValue(code, valueStart) };
|
|
616
|
+
}
|
|
617
|
+
function readKey(code, start) {
|
|
618
|
+
const char = code[start];
|
|
619
|
+
if (char === '"' || char === "'") {
|
|
620
|
+
const literal = readString(code, start);
|
|
621
|
+
const valueAt = keySeparator(code, literal.end);
|
|
622
|
+
if (!literal.literal || valueAt === null) return null;
|
|
623
|
+
return { name: literal.value, end: literal.end, valueAt };
|
|
624
|
+
}
|
|
625
|
+
if (char === ":" && IDENT_START.test(code[start + 1] ?? "")) {
|
|
626
|
+
const name = readIdentifier(code, start + 1);
|
|
627
|
+
const valueAt = hashRocket(code, name.end);
|
|
628
|
+
return valueAt === null ? null : { name: name.text, end: name.end, valueAt };
|
|
629
|
+
}
|
|
630
|
+
if (char !== void 0 && IDENT_START.test(char)) {
|
|
631
|
+
const name = readIdentifier(code, start);
|
|
632
|
+
if (code[name.end] !== ":" || code[name.end + 1] === ":") return null;
|
|
633
|
+
return { name: name.text, end: name.end, valueAt: name.end + 1 };
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
function attributeName(key) {
|
|
638
|
+
const name = key.replaceAll("_", "-").toLowerCase();
|
|
639
|
+
return name.length > 0 && !/[\s"'=<>/]/.test(name) ? `data-${name}` : null;
|
|
640
|
+
}
|
|
641
|
+
function keySeparator(code, at) {
|
|
642
|
+
if (code[at] === ":" && code[at + 1] !== ":") return at + 1;
|
|
643
|
+
return hashRocket(code, at);
|
|
644
|
+
}
|
|
645
|
+
function hashRocket(code, at) {
|
|
646
|
+
const arrow = skipTrivia(code, at);
|
|
647
|
+
return code.startsWith("=>", arrow) ? arrow + 2 : null;
|
|
648
|
+
}
|
|
649
|
+
function readIdentifier(code, start) {
|
|
650
|
+
let end = start;
|
|
651
|
+
while (end < code.length && IDENT_CHAR.test(code[end])) end++;
|
|
652
|
+
return { text: code.slice(start, end), end };
|
|
653
|
+
}
|
|
654
|
+
function skipValue(code, start) {
|
|
655
|
+
let depth = 0;
|
|
656
|
+
let i = start;
|
|
657
|
+
while (i < code.length) {
|
|
658
|
+
const char = code[i];
|
|
659
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
660
|
+
i = readString(code, i).end;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (char === "#") {
|
|
664
|
+
i = endOfLine(code, i);
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
const literal = skipLiteral(code, i);
|
|
668
|
+
if (literal >= 0) {
|
|
669
|
+
i = literal;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (char === "(" || char === "[" || char === "{") depth++;
|
|
673
|
+
else if (char === ")" || char === "]") depth--;
|
|
674
|
+
else if (char === "}") {
|
|
675
|
+
if (depth === 0) return i;
|
|
676
|
+
depth--;
|
|
677
|
+
} else if (char === "," && depth === 0) return i;
|
|
678
|
+
i++;
|
|
679
|
+
}
|
|
680
|
+
return code.length;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/inspector/erb.ts
|
|
684
|
+
var ERB_TAG = /<%[\s\S]*?%>/g;
|
|
685
|
+
function neutralizeErb(source) {
|
|
686
|
+
return source.replace(ERB_TAG, (match) => match.replace(/[^\n]/g, " "));
|
|
687
|
+
}
|
|
688
|
+
function erbRanges(source) {
|
|
689
|
+
return [...source.matchAll(ERB_TAG)].map(
|
|
690
|
+
(match) => [match.index ?? 0, (match.index ?? 0) + match[0].length]
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
function erbElements(source) {
|
|
694
|
+
const tags = readErbTags(source);
|
|
695
|
+
const blockEnds = matchBlocks(tags);
|
|
696
|
+
const index = new PositionIndex(source);
|
|
697
|
+
const elements = [];
|
|
698
|
+
for (const [ordinal, tag] of tags.entries()) {
|
|
699
|
+
if (!tag.output) continue;
|
|
700
|
+
const option = readDataOption(tag.code, tag.codeStart);
|
|
701
|
+
if (!option.found) continue;
|
|
702
|
+
const end = blockEnds.get(ordinal) ?? tag.end;
|
|
703
|
+
elements.push({
|
|
704
|
+
tag: ERB_ELEMENT_TAG,
|
|
705
|
+
attrs: option.attrs.map((attr) => toParsedAttr(attr, index)),
|
|
706
|
+
start: tag.start,
|
|
707
|
+
// The token a reader can act on is the delimiter that opens the call.
|
|
708
|
+
sourceLength: tag.codeStart - tag.start,
|
|
709
|
+
end,
|
|
710
|
+
container: end > tag.end,
|
|
711
|
+
opaque: option.opaque
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return elements;
|
|
715
|
+
}
|
|
716
|
+
function toParsedAttr(attr, index) {
|
|
717
|
+
const at = index.at(attr.keyStart);
|
|
718
|
+
return {
|
|
719
|
+
name: attr.name,
|
|
720
|
+
value: attr.value ?? "",
|
|
721
|
+
line: at.line,
|
|
722
|
+
column: at.column,
|
|
723
|
+
sourceLength: attr.keyEnd - attr.keyStart,
|
|
724
|
+
valueStart: attr.valueStart,
|
|
725
|
+
valueEnd: attr.valueEnd,
|
|
726
|
+
dynamicValue: attr.value === null
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function readErbTags(source) {
|
|
730
|
+
const tags = [];
|
|
731
|
+
for (const match of source.matchAll(ERB_TAG)) {
|
|
732
|
+
const raw = match[0];
|
|
733
|
+
const start = match.index ?? 0;
|
|
734
|
+
let open = 2;
|
|
735
|
+
let output = false;
|
|
736
|
+
if (raw[open] === "#") continue;
|
|
737
|
+
if (raw[open] === "=") {
|
|
738
|
+
output = true;
|
|
739
|
+
open++;
|
|
740
|
+
if (raw[open] === "=") open++;
|
|
741
|
+
} else if (raw[open] === "-") {
|
|
742
|
+
open++;
|
|
743
|
+
}
|
|
744
|
+
let close = raw.length - 2;
|
|
745
|
+
if (raw[close - 1] === "-") close--;
|
|
746
|
+
if (close < open) close = open;
|
|
747
|
+
tags.push({
|
|
748
|
+
start,
|
|
749
|
+
end: start + raw.length,
|
|
750
|
+
output,
|
|
751
|
+
code: raw.slice(open, close),
|
|
752
|
+
codeStart: start + open
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
return tags;
|
|
756
|
+
}
|
|
757
|
+
function matchBlocks(tags) {
|
|
758
|
+
const ends = /* @__PURE__ */ new Map();
|
|
759
|
+
const open = [];
|
|
760
|
+
for (const [ordinal, tag] of tags.entries()) {
|
|
761
|
+
for (const event of blockEvents(tag.code)) {
|
|
762
|
+
if (event === "open") {
|
|
763
|
+
open.push(ordinal);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const opener = open.pop();
|
|
767
|
+
if (opener !== void 0) ends.set(opener, tag.end);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return ends;
|
|
771
|
+
}
|
|
772
|
+
var BLOCK_OPENERS = /* @__PURE__ */ new Set(["case", "begin", "def", "class", "module", "for"]);
|
|
773
|
+
var BLOCK_MODIFIERS = /* @__PURE__ */ new Set(["if", "unless", "while", "until"]);
|
|
774
|
+
var BLOCK_CONTINUERS = /* @__PURE__ */ new Set(["elsif", "else", "when", "in", "then", "rescue", "ensure"]);
|
|
775
|
+
function blockEvents(source) {
|
|
776
|
+
const code = maskHeredocs(source);
|
|
777
|
+
const events = [];
|
|
778
|
+
let previous = "";
|
|
779
|
+
let loopHeader = false;
|
|
780
|
+
let i = 0;
|
|
781
|
+
while (i < code.length) {
|
|
782
|
+
const char = code[i];
|
|
783
|
+
if (char === "\n" || char === ";") {
|
|
784
|
+
previous = char;
|
|
785
|
+
loopHeader = false;
|
|
786
|
+
i++;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (SPACE.has(char)) {
|
|
790
|
+
i++;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if (char === "#") {
|
|
794
|
+
i = endOfLine(code, i);
|
|
795
|
+
previous = "\n";
|
|
796
|
+
loopHeader = false;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
800
|
+
i = readString(code, i).end;
|
|
801
|
+
previous = "value";
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (char === ":" && IDENT_START.test(code[i + 1] ?? "")) {
|
|
805
|
+
i = identifierEnd(code, i + 1);
|
|
806
|
+
previous = "value";
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
const literal = skipLiteral(code, i);
|
|
810
|
+
if (literal >= 0) {
|
|
811
|
+
i = literal;
|
|
812
|
+
previous = "value";
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
if (char === "{") {
|
|
816
|
+
events.push("open");
|
|
817
|
+
previous = "{";
|
|
818
|
+
i++;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (char === "}") {
|
|
822
|
+
events.push("close");
|
|
823
|
+
previous = "value";
|
|
824
|
+
i++;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (char === ")" || char === "]") {
|
|
828
|
+
previous = "value";
|
|
829
|
+
i++;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (!IDENT_START.test(char)) {
|
|
833
|
+
previous = char;
|
|
834
|
+
i++;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
const end = identifierEnd(code, i);
|
|
838
|
+
const word = code.slice(i, end);
|
|
839
|
+
const keyword = !(code[end] === ":" && code[end + 1] !== ":") && code[i - 1] !== ".";
|
|
840
|
+
i = end;
|
|
841
|
+
if (keyword && word === "end") {
|
|
842
|
+
events.push("close");
|
|
843
|
+
previous = "value";
|
|
844
|
+
} else if (keyword && word === "do") {
|
|
845
|
+
if (loopHeader) loopHeader = false;
|
|
846
|
+
else events.push("open");
|
|
847
|
+
previous = "do";
|
|
848
|
+
} else if (keyword && BLOCK_OPENERS.has(word)) {
|
|
849
|
+
events.push("open");
|
|
850
|
+
loopHeader = word === "for";
|
|
851
|
+
previous = word;
|
|
852
|
+
} else if (keyword && BLOCK_MODIFIERS.has(word)) {
|
|
853
|
+
if (previous !== "value") {
|
|
854
|
+
events.push("open");
|
|
855
|
+
loopHeader = word !== "if" && word !== "unless";
|
|
856
|
+
}
|
|
857
|
+
previous = word;
|
|
858
|
+
} else if (keyword && BLOCK_CONTINUERS.has(word)) {
|
|
859
|
+
previous = word;
|
|
860
|
+
} else {
|
|
861
|
+
previous = "value";
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return events;
|
|
865
|
+
}
|
|
866
|
+
function identifierEnd(code, start) {
|
|
867
|
+
let end = start;
|
|
868
|
+
while (end < code.length && IDENT_CHAR.test(code[end])) end++;
|
|
869
|
+
return end;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/inspector/extract.ts
|
|
873
|
+
var NAMESPACE_PREFIX = "stimeo--";
|
|
874
|
+
function dasherize(value) {
|
|
875
|
+
return value.replace(/([A-Z])/g, (_match, char) => `-${char.toLowerCase()}`);
|
|
876
|
+
}
|
|
877
|
+
function controllerIdentifiers(dataControllerValue) {
|
|
878
|
+
const seen = /* @__PURE__ */ new Set();
|
|
879
|
+
for (const token of dataControllerValue.split(/\s+/)) {
|
|
880
|
+
if (token.startsWith(NAMESPACE_PREFIX)) seen.add(token);
|
|
881
|
+
}
|
|
882
|
+
return [...seen];
|
|
883
|
+
}
|
|
884
|
+
function parseTargetAttr(attrName) {
|
|
885
|
+
const match = /^data-(stimeo--[a-z0-9][a-z0-9-]*)-target$/.exec(attrName);
|
|
886
|
+
return match?.[1] ?? null;
|
|
887
|
+
}
|
|
888
|
+
function parseValueAttr(attrName, knownIdentifiers) {
|
|
889
|
+
const match = /^data-(stimeo--[a-z0-9-]+)-value$/.exec(attrName);
|
|
890
|
+
if (!match) return null;
|
|
891
|
+
const inner = match[1] ?? "";
|
|
892
|
+
let best = null;
|
|
893
|
+
for (const id of knownIdentifiers) {
|
|
894
|
+
if (inner.startsWith(`${id}-`)) {
|
|
895
|
+
const valueToken = inner.slice(id.length + 1);
|
|
896
|
+
if (valueToken.length > 0 && (!best || id.length > best.identifier.length)) {
|
|
897
|
+
best = { identifier: id, valueToken };
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (best) return best;
|
|
902
|
+
const rest = inner.slice(NAMESPACE_PREFIX.length);
|
|
903
|
+
const dash = rest.indexOf("-");
|
|
904
|
+
const identifierGuess = dash === -1 ? inner : `${NAMESPACE_PREFIX}${rest.slice(0, dash)}`;
|
|
905
|
+
const valueGuess = dash === -1 ? "" : rest.slice(dash + 1);
|
|
906
|
+
return { identifier: null, valueToken: valueGuess || identifierGuess };
|
|
907
|
+
}
|
|
908
|
+
function actionDescriptors(dataActionValue) {
|
|
909
|
+
const descriptors = [];
|
|
910
|
+
for (const descriptor of dataActionValue.split(/\s+/)) {
|
|
911
|
+
const hash = descriptor.indexOf("#");
|
|
912
|
+
if (hash === -1) continue;
|
|
913
|
+
let lhs = descriptor.slice(0, hash);
|
|
914
|
+
const arrow = lhs.indexOf("->");
|
|
915
|
+
if (arrow !== -1) lhs = lhs.slice(arrow + 2);
|
|
916
|
+
if (!lhs.startsWith(NAMESPACE_PREFIX)) continue;
|
|
917
|
+
const method = /^[a-zA-Z_$][\w$]*/.exec(descriptor.slice(hash + 1))?.[0] ?? "";
|
|
918
|
+
descriptors.push({ identifier: lhs, method });
|
|
919
|
+
}
|
|
920
|
+
return descriptors;
|
|
921
|
+
}
|
|
922
|
+
function isStimeoDataAttr(attrName) {
|
|
923
|
+
return attrName.startsWith(`data-${NAMESPACE_PREFIX}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
263
926
|
// src/inspector/types.ts
|
|
264
927
|
var DIAGNOSTIC_CODES = [
|
|
265
928
|
"unknown-controller",
|
|
@@ -269,28 +932,40 @@ var DIAGNOSTIC_CODES = [
|
|
|
269
932
|
"unknown-action-method",
|
|
270
933
|
"orphan-target",
|
|
271
934
|
"missing-required-target",
|
|
935
|
+
"missing-conditional-target",
|
|
272
936
|
"missing-aria",
|
|
273
937
|
"invalid-aria-value",
|
|
274
938
|
"keyboard-inaccessible",
|
|
275
939
|
"unresolved-idref",
|
|
276
940
|
"managed-aria",
|
|
277
941
|
"composition-mismatch",
|
|
942
|
+
"missing-companion",
|
|
943
|
+
"undeclared-target",
|
|
944
|
+
"cardinality-violation",
|
|
945
|
+
"forbidden-aria",
|
|
278
946
|
"unknown-ignore-code"
|
|
279
947
|
];
|
|
280
948
|
|
|
281
949
|
// src/inspector/check.ts
|
|
282
950
|
function checkSource(source, manifest) {
|
|
283
951
|
const diagnostics = [];
|
|
284
|
-
const tree = parseHtml(neutralizeErb(source));
|
|
952
|
+
const tree = parseHtml(neutralizeErb(source), erbElements(source));
|
|
285
953
|
const known = manifest.controllers;
|
|
286
954
|
const knownIdentifiers = Object.keys(known);
|
|
287
|
-
const
|
|
288
|
-
(m) => [m.index ?? 0, (m.index ?? 0) + m[0].length]
|
|
289
|
-
);
|
|
955
|
+
const ranges = erbRanges(source);
|
|
290
956
|
const isDynamicValue = (attr) => {
|
|
957
|
+
if (attr.dynamicValue !== void 0) return attr.dynamicValue;
|
|
291
958
|
const { valueStart, valueEnd } = attr;
|
|
292
959
|
if (valueStart === void 0 || valueEnd === void 0) return false;
|
|
293
|
-
return
|
|
960
|
+
return ranges.some(([start, end]) => start < valueEnd && end > valueStart);
|
|
961
|
+
};
|
|
962
|
+
const conditionHolds = (scope, identifier, when) => {
|
|
963
|
+
if (!when) return true;
|
|
964
|
+
const hostAttr = scope.attrs.find(
|
|
965
|
+
(a) => a.name === `data-${identifier}-${dasherize(when.value)}-value`
|
|
966
|
+
);
|
|
967
|
+
if (hostAttr && isDynamicValue(hostAttr)) return null;
|
|
968
|
+
return when.equals.includes(hostAttr ? hostAttr.value.trim() : when.default);
|
|
294
969
|
};
|
|
295
970
|
const suppressed = (node, code) => {
|
|
296
971
|
if (code === "unknown-ignore-code") return false;
|
|
@@ -299,7 +974,9 @@ function checkSource(source, manifest) {
|
|
|
299
974
|
const ignore = current.attrs.find((a) => a.name === IGNORE_ATTR);
|
|
300
975
|
if (ignore) {
|
|
301
976
|
const codes = ignore.value.trim();
|
|
302
|
-
if (codes.length === 0
|
|
977
|
+
if (codes.length === 0) {
|
|
978
|
+
if (!isDynamicValue(ignore)) return true;
|
|
979
|
+
} else if (codes.split(/\s+/).includes(code)) return true;
|
|
303
980
|
}
|
|
304
981
|
current = current.parent;
|
|
305
982
|
}
|
|
@@ -308,6 +985,26 @@ function checkSource(source, manifest) {
|
|
|
308
985
|
const report = (owner, code, severity, message, at, suggestion, fix) => {
|
|
309
986
|
if (!suppressed(owner, code)) push(diagnostics, code, severity, message, at, suggestion, fix);
|
|
310
987
|
};
|
|
988
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
989
|
+
const unnamedTargets = [];
|
|
990
|
+
const targetsUnreadable = (container, scope, identifier) => {
|
|
991
|
+
for (const node of unresolved) {
|
|
992
|
+
if (isWithin(node, container) && findOwner(node, identifier) === scope) return true;
|
|
993
|
+
}
|
|
994
|
+
for (const { node, identifier: hidden } of unnamedTargets) {
|
|
995
|
+
if (hidden !== identifier) continue;
|
|
996
|
+
if (isWithin(node, container) && findOwner(node, identifier) === scope) return true;
|
|
997
|
+
}
|
|
998
|
+
return false;
|
|
999
|
+
};
|
|
1000
|
+
const unresolvedAbove = (node) => {
|
|
1001
|
+
let current = node;
|
|
1002
|
+
while (current && current.tag !== "#root") {
|
|
1003
|
+
if (unresolved.has(current)) return true;
|
|
1004
|
+
current = current.parent;
|
|
1005
|
+
}
|
|
1006
|
+
return false;
|
|
1007
|
+
};
|
|
311
1008
|
const scopes = [];
|
|
312
1009
|
const presence = /* @__PURE__ */ new Map();
|
|
313
1010
|
const targetNodes = /* @__PURE__ */ new Map();
|
|
@@ -339,7 +1036,44 @@ function checkSource(source, manifest) {
|
|
|
339
1036
|
if (found) found.push(el);
|
|
340
1037
|
else byName.set(target, [el]);
|
|
341
1038
|
};
|
|
1039
|
+
const contentHolds = (scope, identifier, element, condition) => {
|
|
1040
|
+
if (!condition) return true;
|
|
1041
|
+
const nodes = targetNodes.get(scope)?.get(identifier)?.get(condition.target) ?? [];
|
|
1042
|
+
let held = 0;
|
|
1043
|
+
for (const node of nodes) if (isWithin(node, element)) held += 1;
|
|
1044
|
+
if (condition.min !== void 0 && held < condition.min) return false;
|
|
1045
|
+
if (condition.max !== void 0 && held > condition.max) return false;
|
|
1046
|
+
return true;
|
|
1047
|
+
};
|
|
1048
|
+
const elementHolds = (element, condition) => !condition?.exceptTags.includes(element.tag);
|
|
1049
|
+
const roleTally = /* @__PURE__ */ new Map();
|
|
1050
|
+
const focusableRoleTally = /* @__PURE__ */ new Map();
|
|
1051
|
+
const documentHolds = (condition) => {
|
|
1052
|
+
if (condition === void 0) return false;
|
|
1053
|
+
const tally = condition.focusable ? focusableRoleTally : roleTally;
|
|
1054
|
+
return (tally.get(condition.role) ?? 0) >= condition.atLeast;
|
|
1055
|
+
};
|
|
1056
|
+
const declarationAttrs = /* @__PURE__ */ new Set();
|
|
1057
|
+
for (const controller of Object.values(known)) {
|
|
1058
|
+
for (const rule of controller.targetDeclarations) declarationAttrs.add(rule.attr);
|
|
1059
|
+
}
|
|
1060
|
+
const declarationCandidates = [];
|
|
342
1061
|
walk(tree, (node) => {
|
|
1062
|
+
const declaredController = node.attrs.find((a) => a.name === "data-controller");
|
|
1063
|
+
if (node.opaque || declaredController && isDynamicValue(declaredController)) {
|
|
1064
|
+
unresolved.add(node);
|
|
1065
|
+
}
|
|
1066
|
+
if (declarationAttrs.size > 0 && node.attrs.some((a) => declarationAttrs.has(a.name))) {
|
|
1067
|
+
declarationCandidates.push(node);
|
|
1068
|
+
}
|
|
1069
|
+
const roleAttr = node.attrs.find((a) => a.name === "role");
|
|
1070
|
+
if (roleAttr && !isDynamicValue(roleAttr)) {
|
|
1071
|
+
const role = roleAttr.value.trim();
|
|
1072
|
+
roleTally.set(role, (roleTally.get(role) ?? 0) + 1);
|
|
1073
|
+
if (isFocusable(node, "tab")) {
|
|
1074
|
+
focusableRoleTally.set(role, (focusableRoleTally.get(role) ?? 0) + 1);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
343
1077
|
for (const attr of node.attrs) {
|
|
344
1078
|
if (attr.name === IGNORE_ATTR) {
|
|
345
1079
|
for (const token of attr.value.split(/\s+/).filter((t) => t.length > 0)) {
|
|
@@ -418,7 +1152,7 @@ function checkSource(source, manifest) {
|
|
|
418
1152
|
);
|
|
419
1153
|
continue;
|
|
420
1154
|
}
|
|
421
|
-
const targetName = attr.value.trim();
|
|
1155
|
+
const targetName = isDynamicValue(attr) ? "" : attr.value.trim();
|
|
422
1156
|
if (targetName.length > 0 && !controller.targets.includes(targetName)) {
|
|
423
1157
|
const best = nearestName(targetName, controller.targets);
|
|
424
1158
|
report(
|
|
@@ -436,14 +1170,18 @@ function checkSource(source, manifest) {
|
|
|
436
1170
|
if (targetName.length > 0) {
|
|
437
1171
|
recordPresence(owner, targetIdentifier, targetName);
|
|
438
1172
|
recordTargetNode(owner, targetIdentifier, targetName, node);
|
|
1173
|
+
} else if (isDynamicValue(attr)) {
|
|
1174
|
+
unnamedTargets.push({ node, identifier: targetIdentifier });
|
|
439
1175
|
}
|
|
440
1176
|
} else if (!inDeclaredFragment(node, targetIdentifier)) {
|
|
1177
|
+
const uncertain = unresolvedAbove(node);
|
|
441
1178
|
report(
|
|
442
1179
|
node,
|
|
443
1180
|
"orphan-target",
|
|
444
|
-
"error",
|
|
1181
|
+
uncertain ? "warning" : "error",
|
|
445
1182
|
`Target "${attr.name}" has no enclosing controller "${targetIdentifier}".`,
|
|
446
|
-
attr
|
|
1183
|
+
attr,
|
|
1184
|
+
uncertain ? UNRESOLVED_HOST_HINT : void 0
|
|
447
1185
|
);
|
|
448
1186
|
}
|
|
449
1187
|
continue;
|
|
@@ -506,29 +1244,57 @@ function checkSource(source, manifest) {
|
|
|
506
1244
|
}
|
|
507
1245
|
}
|
|
508
1246
|
});
|
|
1247
|
+
const scopeIsComplete = (container, scope, identifier) => container.origin === "markup" && !targetsUnreadable(container, scope, identifier);
|
|
509
1248
|
for (const { node, identifier } of scopes) {
|
|
510
1249
|
const controller = known[identifier];
|
|
511
1250
|
if (!controller) continue;
|
|
1251
|
+
const complete = scopeIsComplete(node, node, identifier);
|
|
512
1252
|
const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
|
|
513
1253
|
for (const required of controller.requiredTargets) {
|
|
514
1254
|
if (!present.has(required)) {
|
|
515
1255
|
report(
|
|
516
1256
|
node,
|
|
517
1257
|
"missing-required-target",
|
|
518
|
-
"error",
|
|
1258
|
+
complete ? "error" : "warning",
|
|
519
1259
|
`"${identifier}" is missing required target "${required}".`,
|
|
520
|
-
node
|
|
1260
|
+
node,
|
|
1261
|
+
complete ? void 0 : UNRESOLVED_TARGET_HINT
|
|
521
1262
|
);
|
|
522
1263
|
}
|
|
523
1264
|
}
|
|
524
1265
|
}
|
|
1266
|
+
for (const { node, identifier } of scopes) {
|
|
1267
|
+
const controller = known[identifier];
|
|
1268
|
+
if (!controller) continue;
|
|
1269
|
+
const complete = scopeIsComplete(node, node, identifier);
|
|
1270
|
+
const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
|
|
1271
|
+
for (const rule of controller.conditionalTargets) {
|
|
1272
|
+
if (!present.has(rule.whenPresent)) continue;
|
|
1273
|
+
const missing = rule.require.filter((target) => !present.has(target));
|
|
1274
|
+
if (missing.length === 0) continue;
|
|
1275
|
+
report(
|
|
1276
|
+
node,
|
|
1277
|
+
"missing-conditional-target",
|
|
1278
|
+
complete ? "error" : "warning",
|
|
1279
|
+
`"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
|
|
1280
|
+
node,
|
|
1281
|
+
complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
525
1285
|
for (const { node, identifier } of scopes) {
|
|
526
1286
|
const controller = known[identifier];
|
|
527
1287
|
if (!controller) continue;
|
|
528
1288
|
for (const req of controller.a11y) {
|
|
1289
|
+
if (conditionHolds(node, identifier, req.when) !== true) continue;
|
|
529
1290
|
const elements = req.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(req.target) ?? [];
|
|
1291
|
+
if (req.whenDocument && !documentHolds(req.whenDocument)) continue;
|
|
1292
|
+
const severity = documentHolds(req.escalateWhen) ? "error" : req.severity ?? "error";
|
|
530
1293
|
for (const el of elements) {
|
|
531
|
-
|
|
1294
|
+
if (!hasReadableMarkup(el)) continue;
|
|
1295
|
+
if (!contentHolds(node, identifier, el, req.whenContains)) continue;
|
|
1296
|
+
if (!elementHolds(el, req.whenElement)) continue;
|
|
1297
|
+
checkA11y(report, el, identifier, req, severity);
|
|
532
1298
|
}
|
|
533
1299
|
}
|
|
534
1300
|
}
|
|
@@ -539,6 +1305,7 @@ function checkSource(source, manifest) {
|
|
|
539
1305
|
const reach = req.reach ?? "tab";
|
|
540
1306
|
const elements = targetNodes.get(node)?.get(identifier)?.get(req.target) ?? [];
|
|
541
1307
|
for (const el of elements) {
|
|
1308
|
+
if (!hasReadableMarkup(el)) continue;
|
|
542
1309
|
if (!isFocusable(el, reach)) {
|
|
543
1310
|
const problem = reach === "tab" ? 'not in the Tab order (needs a native control or tabindex="0")' : "not focusable (needs a native control or tabindex)";
|
|
544
1311
|
report(
|
|
@@ -560,6 +1327,7 @@ function checkSource(source, manifest) {
|
|
|
560
1327
|
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
561
1328
|
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
562
1329
|
for (const el of elements) {
|
|
1330
|
+
if (!hasReadableMarkup(el)) continue;
|
|
563
1331
|
for (const name of rule.attrs) {
|
|
564
1332
|
const attr = el.attrs.find((a) => a.name === name);
|
|
565
1333
|
if (!attr) continue;
|
|
@@ -575,6 +1343,33 @@ function checkSource(source, manifest) {
|
|
|
575
1343
|
}
|
|
576
1344
|
}
|
|
577
1345
|
}
|
|
1346
|
+
for (const { node, identifier } of scopes) {
|
|
1347
|
+
const controller = known[identifier];
|
|
1348
|
+
if (!controller) continue;
|
|
1349
|
+
for (const rule of controller.forbiddenAria) {
|
|
1350
|
+
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
1351
|
+
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
1352
|
+
for (const el of elements) {
|
|
1353
|
+
if (!hasReadableMarkup(el)) continue;
|
|
1354
|
+
if (!contentHolds(node, identifier, el, rule.whenContains)) continue;
|
|
1355
|
+
for (const name of rule.attrs) {
|
|
1356
|
+
const attr = el.attrs.find((a) => a.name === name);
|
|
1357
|
+
if (!attr) continue;
|
|
1358
|
+
if (rule.values && (isDynamicValue(attr) || !rule.values.includes(attr.value.trim()))) {
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
report(
|
|
1362
|
+
el,
|
|
1363
|
+
"forbidden-aria",
|
|
1364
|
+
"warning",
|
|
1365
|
+
`${name} on the ${where} of "${identifier}" contradicts the markup around it.`,
|
|
1366
|
+
attr,
|
|
1367
|
+
rule.suggestion
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
578
1373
|
for (const { node, identifier } of scopes) {
|
|
579
1374
|
const controller = known[identifier];
|
|
580
1375
|
if (!controller) continue;
|
|
@@ -583,16 +1378,10 @@ function checkSource(source, manifest) {
|
|
|
583
1378
|
const elements = rule.target === "" ? [node] : targeted ?? (rule.fallbackToScope ? [node] : []);
|
|
584
1379
|
const where = rule.target === "" || !targeted ? "scope element" : `"${rule.target}" target`;
|
|
585
1380
|
const when = rule.when;
|
|
586
|
-
if (when)
|
|
587
|
-
const hostAttr = node.attrs.find(
|
|
588
|
-
(a) => a.name === `data-${identifier}-${dasherize(when.value)}-value`
|
|
589
|
-
);
|
|
590
|
-
if (hostAttr && isDynamicValue(hostAttr)) continue;
|
|
591
|
-
const hostEffective = hostAttr ? hostAttr.value.trim() : when.default;
|
|
592
|
-
if (!when.equals.includes(hostEffective)) continue;
|
|
593
|
-
}
|
|
1381
|
+
if (conditionHolds(node, identifier, when) !== true) continue;
|
|
594
1382
|
const hostContext = when ? ` (${when.value} ${list(when.equals)})` : "";
|
|
595
1383
|
for (const el of elements) {
|
|
1384
|
+
if (el.opaque) continue;
|
|
596
1385
|
const declared = el.attrs.find((a) => a.name === "data-controller");
|
|
597
1386
|
if (!declared || !controllerIdentifiers(declared.value).includes(rule.coController)) {
|
|
598
1387
|
continue;
|
|
@@ -626,6 +1415,115 @@ function checkSource(source, manifest) {
|
|
|
626
1415
|
}
|
|
627
1416
|
}
|
|
628
1417
|
}
|
|
1418
|
+
for (const { node, identifier } of scopes) {
|
|
1419
|
+
const controller = known[identifier];
|
|
1420
|
+
if (!controller) continue;
|
|
1421
|
+
for (const rule of controller.companions) {
|
|
1422
|
+
const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
|
|
1423
|
+
const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
|
|
1424
|
+
for (const el of elements) {
|
|
1425
|
+
if (el.opaque) continue;
|
|
1426
|
+
const declared = el.attrs.find((a) => a.name === "data-controller");
|
|
1427
|
+
if (declared && isDynamicValue(declared)) continue;
|
|
1428
|
+
if (declared && controllerIdentifiers(declared.value).includes(rule.controller)) continue;
|
|
1429
|
+
report(
|
|
1430
|
+
el,
|
|
1431
|
+
"missing-companion",
|
|
1432
|
+
"error",
|
|
1433
|
+
`The ${where} of "${identifier}" must also declare "${rule.controller}" in data-controller.`,
|
|
1434
|
+
declared ?? el,
|
|
1435
|
+
rule.suggestion,
|
|
1436
|
+
// Appending to an existing list is unambiguous; creating the
|
|
1437
|
+
// attribute from nothing is an insertion the anchor cannot express.
|
|
1438
|
+
declared ? valueFix(
|
|
1439
|
+
declared,
|
|
1440
|
+
`${declared.value.trim()} ${rule.controller}`.trim(),
|
|
1441
|
+
`Add "${rule.controller}" to data-controller`
|
|
1442
|
+
) : void 0
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
for (const { node, identifier } of scopes) {
|
|
1448
|
+
const controller = known[identifier];
|
|
1449
|
+
if (!controller) continue;
|
|
1450
|
+
if (controller.targetDeclarations.length === 0) continue;
|
|
1451
|
+
const targetAttrName = `data-${identifier}-target`;
|
|
1452
|
+
for (const rule of controller.targetDeclarations) {
|
|
1453
|
+
for (const el of declarationCandidates) {
|
|
1454
|
+
const marker = el.attrs.find((a) => a.name === rule.attr);
|
|
1455
|
+
if (!marker || isDynamicValue(marker)) continue;
|
|
1456
|
+
const value = marker.value.trim();
|
|
1457
|
+
if (value.length === 0) continue;
|
|
1458
|
+
if (rule.values && !rule.values.includes(value)) continue;
|
|
1459
|
+
if (findOwner(el, identifier) !== node) continue;
|
|
1460
|
+
const declaredAs = el.attrs.find((a) => a.name === targetAttrName);
|
|
1461
|
+
if (declaredAs && !isDynamicValue(declaredAs) && declaredAs.value.trim() === rule.target) {
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (declaredAs && isDynamicValue(declaredAs)) continue;
|
|
1465
|
+
report(
|
|
1466
|
+
el,
|
|
1467
|
+
"undeclared-target",
|
|
1468
|
+
"error",
|
|
1469
|
+
`${rule.attr}="${value}" inside "${identifier}" is not declared as its "${rule.target}" target, so the controller never manages it.`,
|
|
1470
|
+
marker,
|
|
1471
|
+
rule.suggestion
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
for (const { node, identifier } of scopes) {
|
|
1477
|
+
const controller = known[identifier];
|
|
1478
|
+
if (!controller) continue;
|
|
1479
|
+
for (const rule of controller.cardinality) {
|
|
1480
|
+
if (conditionHolds(node, identifier, rule.when) !== true) continue;
|
|
1481
|
+
const byName = targetNodes.get(node)?.get(identifier);
|
|
1482
|
+
const counted = byName?.get(rule.target) ?? [];
|
|
1483
|
+
const containers = rule.within === "" ? [node] : byName?.get(rule.within) ?? [];
|
|
1484
|
+
const what = describeCounted(rule.target, rule.attr, rule.values);
|
|
1485
|
+
const where = rule.within === "" ? "controller scope" : `"${rule.within}" target`;
|
|
1486
|
+
for (const container of containers) {
|
|
1487
|
+
const matched = [];
|
|
1488
|
+
for (const el of counted) {
|
|
1489
|
+
if (!isWithin(el, container)) continue;
|
|
1490
|
+
if (rule.attr === void 0) {
|
|
1491
|
+
matched.push({ el, at: el });
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
const attr = el.attrs.find((a) => a.name === rule.attr);
|
|
1495
|
+
if (!attr) continue;
|
|
1496
|
+
if (rule.values && (isDynamicValue(attr) || !rule.values.includes(attr.value.trim()))) {
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
matched.push({ el, at: attr });
|
|
1500
|
+
}
|
|
1501
|
+
if (rule.max !== void 0 && matched.length > rule.max) {
|
|
1502
|
+
for (const { el, at } of matched.slice(rule.max)) {
|
|
1503
|
+
report(
|
|
1504
|
+
el,
|
|
1505
|
+
"cardinality-violation",
|
|
1506
|
+
"error",
|
|
1507
|
+
`"${identifier}" allows at most ${rule.max} ${what} per ${where}, but found ${matched.length}.`,
|
|
1508
|
+
at,
|
|
1509
|
+
rule.suggestion
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (rule.min !== void 0 && matched.length < rule.min) {
|
|
1514
|
+
const complete = scopeIsComplete(container, node, identifier);
|
|
1515
|
+
report(
|
|
1516
|
+
container,
|
|
1517
|
+
"cardinality-violation",
|
|
1518
|
+
complete ? "error" : "warning",
|
|
1519
|
+
`"${identifier}" requires at least ${rule.min} ${what} per ${where}, but found ${matched.length}.`,
|
|
1520
|
+
container,
|
|
1521
|
+
complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
629
1527
|
const ids = /* @__PURE__ */ new Set();
|
|
630
1528
|
walk(tree, (node) => {
|
|
631
1529
|
const id = node.attrs.find((a) => a.name === "id");
|
|
@@ -663,7 +1561,12 @@ function checkSource(source, manifest) {
|
|
|
663
1561
|
diagnostics.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
664
1562
|
return diagnostics;
|
|
665
1563
|
}
|
|
666
|
-
|
|
1564
|
+
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.";
|
|
1565
|
+
var UNRESOLVED_HOST_HINT = "An enclosing helper's data: option is not a literal hash, so the host controller may be declared at runtime.";
|
|
1566
|
+
function hasReadableMarkup(el) {
|
|
1567
|
+
return el.origin === "markup";
|
|
1568
|
+
}
|
|
1569
|
+
function checkA11y(report, el, identifier, req, severity) {
|
|
667
1570
|
const groups = [
|
|
668
1571
|
{ attrs: req.attrs, values: req.values },
|
|
669
1572
|
...req.or ?? []
|
|
@@ -692,11 +1595,12 @@ function checkA11y(report, el, identifier, req) {
|
|
|
692
1595
|
}
|
|
693
1596
|
}
|
|
694
1597
|
if (!anyPresent) {
|
|
1598
|
+
const verb = severity === "warning" ? "recommends" : "requires";
|
|
695
1599
|
report(
|
|
696
1600
|
el,
|
|
697
1601
|
"missing-aria",
|
|
698
|
-
|
|
699
|
-
`"${identifier}"
|
|
1602
|
+
severity,
|
|
1603
|
+
`"${identifier}" ${verb} ${describeAttrs([...allowedByAttr.keys()])} on its ${where}.`,
|
|
700
1604
|
el,
|
|
701
1605
|
req.suggestion
|
|
702
1606
|
);
|
|
@@ -715,6 +1619,19 @@ function checkA11y(report, el, identifier, req) {
|
|
|
715
1619
|
);
|
|
716
1620
|
}
|
|
717
1621
|
}
|
|
1622
|
+
function isWithin(node, container) {
|
|
1623
|
+
let current = node;
|
|
1624
|
+
while (current) {
|
|
1625
|
+
if (current === container) return true;
|
|
1626
|
+
current = current.parent;
|
|
1627
|
+
}
|
|
1628
|
+
return false;
|
|
1629
|
+
}
|
|
1630
|
+
function describeCounted(target, attr, values) {
|
|
1631
|
+
if (attr === void 0) return `"${target}" targets`;
|
|
1632
|
+
const constrained = values && values.length > 0 ? ` set to ${list(values)}` : "";
|
|
1633
|
+
return `"${target}" targets with ${attr}${constrained}`;
|
|
1634
|
+
}
|
|
718
1635
|
var NATIVE_FOCUSABLE_TAGS = /* @__PURE__ */ new Set(["button", "select", "textarea", "summary"]);
|
|
719
1636
|
function isNativelyFocusable(el) {
|
|
720
1637
|
if (NATIVE_FOCUSABLE_TAGS.has(el.tag)) return true;
|
|
@@ -777,6 +1694,7 @@ function inDeclaredFragment(node, identifier) {
|
|
|
777
1694
|
return false;
|
|
778
1695
|
}
|
|
779
1696
|
function anchorLength(at) {
|
|
1697
|
+
if (at.sourceLength !== void 0) return at.sourceLength;
|
|
780
1698
|
return "name" in at ? at.name.length : at.tag.length + 1;
|
|
781
1699
|
}
|
|
782
1700
|
function push(out, code, severity, message, at, suggestion, fix) {
|
|
@@ -868,10 +1786,15 @@ var CONTROLLER_ARRAY_FIELDS = [
|
|
|
868
1786
|
"actions",
|
|
869
1787
|
"events",
|
|
870
1788
|
"requiredTargets",
|
|
1789
|
+
"conditionalTargets",
|
|
871
1790
|
"a11y",
|
|
872
1791
|
"keyboard",
|
|
873
1792
|
"managedAria",
|
|
874
|
-
"compositions"
|
|
1793
|
+
"compositions",
|
|
1794
|
+
"companions",
|
|
1795
|
+
"targetDeclarations",
|
|
1796
|
+
"cardinality",
|
|
1797
|
+
"forbiddenAria"
|
|
875
1798
|
];
|
|
876
1799
|
function isCompatibleManifest(value) {
|
|
877
1800
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
@@ -1297,6 +2220,41 @@ var EXAMPLE_RESULT_SCHEMA = {
|
|
|
1297
2220
|
},
|
|
1298
2221
|
required: ["id", "file", "source", "guidance"]
|
|
1299
2222
|
};
|
|
2223
|
+
var CONTENT_CONDITION_SCHEMA = {
|
|
2224
|
+
type: "object",
|
|
2225
|
+
description: "Condition on the element's own contents arming the rule; absent means it does not depend on them. Evaluated per element, unlike the scope-level `when`.",
|
|
2226
|
+
properties: {
|
|
2227
|
+
target: { type: "string", description: "Target counted inside the element." },
|
|
2228
|
+
min: { type: "integer", description: "Arms the rule when the count is at least this." },
|
|
2229
|
+
max: { type: "integer", description: "Arms the rule when the count is at most this." }
|
|
2230
|
+
},
|
|
2231
|
+
required: ["target"]
|
|
2232
|
+
};
|
|
2233
|
+
var ELEMENT_CONDITION_SCHEMA = {
|
|
2234
|
+
type: "object",
|
|
2235
|
+
description: "Tags the rule is disarmed on because they name the role natively (a <table>'s <caption>, a <fieldset>'s <legend>, an <input>'s <label for>); absent means the rule applies to every spelling.",
|
|
2236
|
+
properties: {
|
|
2237
|
+
exceptTags: {
|
|
2238
|
+
type: "array",
|
|
2239
|
+
items: { type: "string" },
|
|
2240
|
+
description: "Lowercase tag names whose native naming path disarms the rule."
|
|
2241
|
+
}
|
|
2242
|
+
},
|
|
2243
|
+
required: ["exceptTags"]
|
|
2244
|
+
};
|
|
2245
|
+
var DOCUMENT_CONDITION_SCHEMA = {
|
|
2246
|
+
type: "object",
|
|
2247
|
+
description: "Condition on the whole file: how many elements in it carry a role. Backs ARIA's conditional name levels (a toolbar's name becomes required on the second toolbar). Counts per file, which under-approximates a page built from partials.",
|
|
2248
|
+
properties: {
|
|
2249
|
+
role: { type: "string", description: "Role counted across the file." },
|
|
2250
|
+
atLeast: { type: "integer", description: "Holds at this many or more." },
|
|
2251
|
+
focusable: {
|
|
2252
|
+
type: "boolean",
|
|
2253
|
+
description: "Counts only Tab-reachable carriers of the role; absent counts all. ARIA qualifies a separator's condition by focusability (a decorative hr cannot be landed on) but a toolbar's not at all."
|
|
2254
|
+
}
|
|
2255
|
+
},
|
|
2256
|
+
required: ["role", "atLeast"]
|
|
2257
|
+
};
|
|
1300
2258
|
var CONTROLLER_CONTRACT_SCHEMA = {
|
|
1301
2259
|
type: "object",
|
|
1302
2260
|
properties: {
|
|
@@ -1309,6 +2267,19 @@ var CONTROLLER_CONTRACT_SCHEMA = {
|
|
|
1309
2267
|
...STRING_ARRAY,
|
|
1310
2268
|
description: "Targets that must be present at least once inside the controller scope."
|
|
1311
2269
|
},
|
|
2270
|
+
conditionalTargets: {
|
|
2271
|
+
type: "array",
|
|
2272
|
+
description: "Targets that become required only once another optional target appears \u2014 an opt-in feature that is incomplete without its whole set. Omitting one half passes every other check and silently does nothing.",
|
|
2273
|
+
items: {
|
|
2274
|
+
type: "object",
|
|
2275
|
+
properties: {
|
|
2276
|
+
whenPresent: { type: "string", description: "The optional target that turns it on." },
|
|
2277
|
+
require: { ...STRING_ARRAY, description: "Targets required once it appears." },
|
|
2278
|
+
suggestion: { type: "string" }
|
|
2279
|
+
},
|
|
2280
|
+
required: ["whenPresent", "require", "suggestion"]
|
|
2281
|
+
}
|
|
2282
|
+
},
|
|
1312
2283
|
a11y: {
|
|
1313
2284
|
type: "array",
|
|
1314
2285
|
description: "Author-supplied ARIA the controller relies on but never sets itself.",
|
|
@@ -1330,6 +2301,25 @@ var CONTROLLER_CONTRACT_SCHEMA = {
|
|
|
1330
2301
|
required: ["attrs"]
|
|
1331
2302
|
}
|
|
1332
2303
|
},
|
|
2304
|
+
when: {
|
|
2305
|
+
type: "object",
|
|
2306
|
+
description: "Own-value condition arming the requirement; absent means unconditional. Outside the named configuration the ARIA is not optional but wrong.",
|
|
2307
|
+
properties: {
|
|
2308
|
+
value: { type: "string" },
|
|
2309
|
+
equals: STRING_ARRAY,
|
|
2310
|
+
default: { type: "string" }
|
|
2311
|
+
},
|
|
2312
|
+
required: ["value", "equals", "default"]
|
|
2313
|
+
},
|
|
2314
|
+
whenContains: CONTENT_CONDITION_SCHEMA,
|
|
2315
|
+
whenElement: ELEMENT_CONDITION_SCHEMA,
|
|
2316
|
+
whenDocument: DOCUMENT_CONDITION_SCHEMA,
|
|
2317
|
+
severity: {
|
|
2318
|
+
type: "string",
|
|
2319
|
+
enum: ["error", "warning"],
|
|
2320
|
+
description: 'Level of an unmet requirement; absent means "error". ARIA recommends some names rather than requiring them, and those report as warnings.'
|
|
2321
|
+
},
|
|
2322
|
+
escalateWhen: DOCUMENT_CONDITION_SCHEMA,
|
|
1333
2323
|
suggestion: { type: "string" }
|
|
1334
2324
|
},
|
|
1335
2325
|
required: ["target", "attrs", "suggestion"]
|
|
@@ -1404,6 +2394,84 @@ var CONTROLLER_CONTRACT_SCHEMA = {
|
|
|
1404
2394
|
},
|
|
1405
2395
|
required: ["target", "coController", "require", "suggestion"]
|
|
1406
2396
|
}
|
|
2397
|
+
},
|
|
2398
|
+
companions: {
|
|
2399
|
+
type: "array",
|
|
2400
|
+
description: "Controllers the named target must ALSO declare. Unlike compositions (which only check an already co-located companion), these report the companion being missing.",
|
|
2401
|
+
items: {
|
|
2402
|
+
type: "object",
|
|
2403
|
+
properties: {
|
|
2404
|
+
target: {
|
|
2405
|
+
type: "string",
|
|
2406
|
+
description: 'Target that must declare it; "" means the data-controller element.'
|
|
2407
|
+
},
|
|
2408
|
+
controller: { type: "string", description: "Required companion identifier." },
|
|
2409
|
+
suggestion: { type: "string" }
|
|
2410
|
+
},
|
|
2411
|
+
required: ["target", "controller", "suggestion"]
|
|
2412
|
+
}
|
|
2413
|
+
},
|
|
2414
|
+
targetDeclarations: {
|
|
2415
|
+
type: "array",
|
|
2416
|
+
description: "Reverse direction: elements in scope carrying the attribute must be declared as the named target, or the controller never manages them.",
|
|
2417
|
+
items: {
|
|
2418
|
+
type: "object",
|
|
2419
|
+
properties: {
|
|
2420
|
+
attr: { type: "string", description: 'Marking attribute, e.g. "role".' },
|
|
2421
|
+
values: { ...STRING_ARRAY, description: "Marking values; absent = any value." },
|
|
2422
|
+
target: {
|
|
2423
|
+
type: "string",
|
|
2424
|
+
description: "Target the matched element must be declared as."
|
|
2425
|
+
},
|
|
2426
|
+
suggestion: { type: "string" }
|
|
2427
|
+
},
|
|
2428
|
+
required: ["attr", "target", "suggestion"]
|
|
2429
|
+
}
|
|
2430
|
+
},
|
|
2431
|
+
cardinality: {
|
|
2432
|
+
type: "array",
|
|
2433
|
+
description: "Set-level count bounds: how many of the named target may or must exist per scope or per container target, optionally narrowed to elements carrying an attribute.",
|
|
2434
|
+
items: {
|
|
2435
|
+
type: "object",
|
|
2436
|
+
properties: {
|
|
2437
|
+
within: {
|
|
2438
|
+
type: "string",
|
|
2439
|
+
description: 'Container target bounding the count; "" means the whole scope.'
|
|
2440
|
+
},
|
|
2441
|
+
target: { type: "string", description: "Target name being counted." },
|
|
2442
|
+
attr: { type: "string", description: "Count only elements carrying this attribute." },
|
|
2443
|
+
values: { ...STRING_ARRAY, description: "Restrict the attribute to these values." },
|
|
2444
|
+
min: { type: "integer", description: "Smallest permitted count (inclusive)." },
|
|
2445
|
+
max: { type: "integer", description: "Largest permitted count (inclusive)." },
|
|
2446
|
+
when: {
|
|
2447
|
+
type: "object",
|
|
2448
|
+
description: "Own-value condition arming the bound; absent means unconditional.",
|
|
2449
|
+
properties: {
|
|
2450
|
+
value: { type: "string" },
|
|
2451
|
+
equals: STRING_ARRAY,
|
|
2452
|
+
default: { type: "string" }
|
|
2453
|
+
},
|
|
2454
|
+
required: ["value", "equals", "default"]
|
|
2455
|
+
},
|
|
2456
|
+
suggestion: { type: "string" }
|
|
2457
|
+
},
|
|
2458
|
+
required: ["within", "target", "suggestion"]
|
|
2459
|
+
}
|
|
2460
|
+
},
|
|
2461
|
+
forbiddenAria: {
|
|
2462
|
+
type: "array",
|
|
2463
|
+
description: "ARIA the author owns that the surrounding markup contradicts (e.g. a menu still declaring itself busy once its items are present). Reported as a warning \u2014 unlike managedAria, the attribute is the author's and is required in the other configuration.",
|
|
2464
|
+
items: {
|
|
2465
|
+
type: "object",
|
|
2466
|
+
properties: {
|
|
2467
|
+
target: { type: "string" },
|
|
2468
|
+
attrs: STRING_ARRAY,
|
|
2469
|
+
values: { ...STRING_ARRAY, description: "Forbidden values; absent = any value." },
|
|
2470
|
+
whenContains: CONTENT_CONDITION_SCHEMA,
|
|
2471
|
+
suggestion: { type: "string" }
|
|
2472
|
+
},
|
|
2473
|
+
required: ["target", "attrs", "suggestion"]
|
|
2474
|
+
}
|
|
1407
2475
|
}
|
|
1408
2476
|
},
|
|
1409
2477
|
required: [
|
|
@@ -1413,10 +2481,15 @@ var CONTROLLER_CONTRACT_SCHEMA = {
|
|
|
1413
2481
|
"actions",
|
|
1414
2482
|
"events",
|
|
1415
2483
|
"requiredTargets",
|
|
2484
|
+
"conditionalTargets",
|
|
1416
2485
|
"a11y",
|
|
1417
2486
|
"keyboard",
|
|
1418
2487
|
"managedAria",
|
|
1419
|
-
"compositions"
|
|
2488
|
+
"compositions",
|
|
2489
|
+
"companions",
|
|
2490
|
+
"targetDeclarations",
|
|
2491
|
+
"cardinality",
|
|
2492
|
+
"forbiddenAria"
|
|
1420
2493
|
]
|
|
1421
2494
|
};
|
|
1422
2495
|
var InvalidToolArgsError = class extends Error {
|
|
@@ -1429,7 +2502,7 @@ var TOOL_DESCRIPTORS = [
|
|
|
1429
2502
|
{
|
|
1430
2503
|
name: "stimeo_check",
|
|
1431
2504
|
title: "Check Stimeo UI markup",
|
|
1432
|
-
description: "Statically check HTML or ERB source that uses Stimeo UI (stimeo--*) controllers. 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
|
|
2505
|
+
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.",
|
|
1433
2506
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1434
2507
|
inputSchema: {
|
|
1435
2508
|
type: "object",
|
|
@@ -1451,7 +2524,7 @@ var TOOL_DESCRIPTORS = [
|
|
|
1451
2524
|
{
|
|
1452
2525
|
name: "stimeo_catalog",
|
|
1453
2526
|
title: "List Stimeo UI controllers",
|
|
1454
|
-
description: "List every Stimeo UI controller with its targets, values, actions, and events.
|
|
2527
|
+
description: "List every Stimeo UI controller with its targets, values, actions, and events. Stimeo UI is a headless, accessible (WAI-ARIA APG) Stimulus UI component library for Rails: dialog/modal, drawer, dropdown menu, tabs, accordion, combobox, listbox, tooltip, popover, toast, carousel, data grid, date picker, tree view, and many more. Start here to find which stimeo--* controller implements a requested component and the data-* API it accepts before writing markup.",
|
|
1455
2528
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1456
2529
|
inputSchema: {
|
|
1457
2530
|
type: "object",
|
|
@@ -1463,7 +2536,7 @@ var TOOL_DESCRIPTORS = [
|
|
|
1463
2536
|
{
|
|
1464
2537
|
name: "stimeo_controller",
|
|
1465
2538
|
title: "Get one controller's usage contract",
|
|
1466
|
-
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.',
|
|
2539
|
+
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.',
|
|
1467
2540
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1468
2541
|
inputSchema: {
|
|
1469
2542
|
type: "object",
|
|
@@ -1481,7 +2554,7 @@ var TOOL_DESCRIPTORS = [
|
|
|
1481
2554
|
{
|
|
1482
2555
|
name: "stimeo_example",
|
|
1483
2556
|
title: "Get one controller's verified example markup",
|
|
1484
|
-
description: 'Fetch verified example markup for one Stimeo UI controller by identifier (e.g. "stimeo--menu"). The example is the official catalog demo \u2014 it passes stimeo_check against the bundled manifest, so use it as the reference for correct structure, required targets, and author-supplied ARIA. class attributes in it are demo styling placeholders to replace; the stimeo--* data attributes and role/aria-* attributes are the contract to keep.',
|
|
2557
|
+
description: 'Fetch verified example markup for one Stimeo UI controller by identifier (e.g. "stimeo--menu"). The example is the official catalog demo \u2014 it passes stimeo_check against the bundled manifest, so use it as the reference for correct structure, required targets, and author-supplied ARIA. class attributes in it are demo styling placeholders to replace; the stimeo--* data attributes and role/aria-* attributes are the contract to keep. Copy it as the starting point when adding that accessible Stimulus component to a Rails ERB view.',
|
|
1485
2558
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1486
2559
|
inputSchema: {
|
|
1487
2560
|
type: "object",
|
|
@@ -1693,7 +2766,13 @@ var McpSession = class {
|
|
|
1693
2766
|
title: "Stimeo UI Inspector",
|
|
1694
2767
|
version: this.#context.manifest.packageVersion
|
|
1695
2768
|
},
|
|
1696
|
-
|
|
2769
|
+
// The server's own account of itself. Per the MCP schema this is a hint
|
|
2770
|
+
// a client MAY put in front of the model, which makes it the earliest
|
|
2771
|
+
// point this server can be routed to — clients that defer tool
|
|
2772
|
+
// definitions still tend to load it alongside the tool names. Hence the
|
|
2773
|
+
// trigger condition first, in the words a request would use, and a
|
|
2774
|
+
// length that survives truncation.
|
|
2775
|
+
instructions: "Search for these tools whenever a task involves building, editing, or reviewing an accessible UI component in a Rails view \u2014 dialog/modal, drawer, dropdown menu, tabs, accordion, combobox, toast, and the rest of the catalog \u2014 or whenever HTML/ERB carrying stimeo--* attributes is in play. Stimeo UI is a headless Stimulus UI framework for Rails. Use stimeo_catalog to discover controllers, stimeo_controller for one controller's usage and accessibility contract, stimeo_example for verified reference markup, and stimeo_check to validate generated HTML/ERB before presenting it. The full manifest and every example are also exposed as resources (stimeo:// URIs), and the stimeo_build_ui / stimeo_fix_markup prompts bake this workflow into user-invokable templates."
|
|
1697
2776
|
});
|
|
1698
2777
|
}
|
|
1699
2778
|
#getPrompt(id, params) {
|
|
@@ -1807,40 +2886,41 @@ async function runMcpServer(io) {
|
|
|
1807
2886
|
|
|
1808
2887
|
// src/inspector/cli_bin.ts
|
|
1809
2888
|
var argv = process2.argv.slice(2);
|
|
2889
|
+
process2.stdout.on("error", (error) => {
|
|
2890
|
+
process2.exit(error.code === "EPIPE" ? 0 : 1);
|
|
2891
|
+
});
|
|
1810
2892
|
if (argv[0] === "mcp") {
|
|
1811
2893
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1812
|
-
process2.
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
input: process2.stdin,
|
|
1819
|
-
write: (line) => {
|
|
1820
|
-
process2.stdout.write(`${line}
|
|
2894
|
+
process2.exitCode = runCli(["--help"]);
|
|
2895
|
+
} else {
|
|
2896
|
+
runMcpServer({
|
|
2897
|
+
input: process2.stdin,
|
|
2898
|
+
write: (line) => {
|
|
2899
|
+
process2.stdout.write(`${line}
|
|
1821
2900
|
`);
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2901
|
+
},
|
|
2902
|
+
load: () => ({ manifest: loadManifest(), examples: loadExamplesIndex() }),
|
|
2903
|
+
logError: (message) => {
|
|
2904
|
+
process2.stderr.write(`stimeo mcp: ${message}
|
|
1826
2905
|
`);
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2906
|
+
}
|
|
2907
|
+
}).then(
|
|
2908
|
+
// Set the exit code instead of calling process.exit so any buffered
|
|
2909
|
+
// stdout responses are flushed before the process ends.
|
|
2910
|
+
() => {
|
|
2911
|
+
process2.exitCode = 0;
|
|
2912
|
+
},
|
|
2913
|
+
(error) => {
|
|
2914
|
+
process2.stderr.write(
|
|
2915
|
+
`stimeo mcp: ${error instanceof Error ? error.message : String(error)}
|
|
1837
2916
|
`
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2917
|
+
);
|
|
2918
|
+
process2.exitCode = 1;
|
|
2919
|
+
}
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
1842
2922
|
} else {
|
|
1843
|
-
process2.
|
|
2923
|
+
process2.exitCode = runCli(argv);
|
|
1844
2924
|
}
|
|
1845
2925
|
//# sourceMappingURL=cli_bin.js.map
|
|
1846
2926
|
//# sourceMappingURL=cli_bin.js.map
|