@reticular/speakable 1.0.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/README.md +31 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2862 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +348 -0
- package/dist/index.js +512 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2862 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
|
|
15
|
+
// src/cli/options.ts
|
|
16
|
+
function validateOptions(rawOptions) {
|
|
17
|
+
const format = rawOptions.format;
|
|
18
|
+
const screenReader = rawOptions.screenReader;
|
|
19
|
+
const validFormats = ["json", "text", "audit", "both"];
|
|
20
|
+
if (!validFormats.includes(format)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Invalid format: ${format}. Must be one of: ${validFormats.join(", ")}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const validReaders = ["nvda", "jaws", "voiceover", "all"];
|
|
26
|
+
if (!validReaders.includes(screenReader)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Invalid screen reader: ${screenReader}. Must be one of: ${validReaders.join(", ")}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
output: rawOptions.output,
|
|
33
|
+
format,
|
|
34
|
+
screenReader,
|
|
35
|
+
selector: rawOptions.selector,
|
|
36
|
+
validate: rawOptions.validate || false,
|
|
37
|
+
diff: rawOptions.diff,
|
|
38
|
+
batch: rawOptions.batch || false
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function parseInput(input) {
|
|
42
|
+
if (Array.isArray(input)) {
|
|
43
|
+
if (input.length === 0) {
|
|
44
|
+
return {
|
|
45
|
+
input: void 0,
|
|
46
|
+
isStdin: false,
|
|
47
|
+
inputs: []
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (input.includes("-")) {
|
|
51
|
+
if (input.length === 1) {
|
|
52
|
+
return { input: void 0, isStdin: true };
|
|
53
|
+
}
|
|
54
|
+
throw new Error("Batch mode cannot be used with stdin input");
|
|
55
|
+
}
|
|
56
|
+
if (input.length === 1) {
|
|
57
|
+
return {
|
|
58
|
+
input: input[0],
|
|
59
|
+
isStdin: false
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
input: void 0,
|
|
64
|
+
isStdin: false,
|
|
65
|
+
inputs: input
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!input) {
|
|
69
|
+
return {
|
|
70
|
+
input: void 0,
|
|
71
|
+
isStdin: false
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (input === "-") {
|
|
75
|
+
return {
|
|
76
|
+
input: void 0,
|
|
77
|
+
isStdin: true
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
input,
|
|
82
|
+
isStdin: false
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function validateInput(parsedInput) {
|
|
86
|
+
const hasInputs = parsedInput.inputs && parsedInput.inputs.length > 0;
|
|
87
|
+
if (!parsedInput.input && !parsedInput.isStdin && !hasInputs) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'No input provided. Specify an HTML file path or use "-" for stdin.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function validateDiffMode(options, parsedInput) {
|
|
94
|
+
if (options.diff) {
|
|
95
|
+
if (parsedInput.isStdin) {
|
|
96
|
+
throw new Error("Diff mode cannot be used with stdin input");
|
|
97
|
+
}
|
|
98
|
+
if (!parsedInput.input) {
|
|
99
|
+
throw new Error("Diff mode requires an input file");
|
|
100
|
+
}
|
|
101
|
+
if (parsedInput.inputs && parsedInput.inputs.length > 0) {
|
|
102
|
+
throw new Error("Diff mode cannot be used with batch mode");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function validateBatchMode(options, parsedInput) {
|
|
107
|
+
if (options.batch) {
|
|
108
|
+
if (parsedInput.isStdin) {
|
|
109
|
+
throw new Error("Batch mode cannot be used with stdin input");
|
|
110
|
+
}
|
|
111
|
+
if (options.diff) {
|
|
112
|
+
throw new Error("Batch mode cannot be used with diff mode");
|
|
113
|
+
}
|
|
114
|
+
if (!parsedInput.inputs || parsedInput.inputs.length === 0) {
|
|
115
|
+
throw new Error("Batch mode requires multiple input files");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/cli/io.ts
|
|
121
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
122
|
+
import { stdin } from "process";
|
|
123
|
+
var FileIOError = class extends Error {
|
|
124
|
+
constructor(message, code, path) {
|
|
125
|
+
super(message);
|
|
126
|
+
this.code = code;
|
|
127
|
+
this.path = path;
|
|
128
|
+
this.name = "FileIOError";
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
function readHTMLFromFile(filePath) {
|
|
132
|
+
try {
|
|
133
|
+
return readFileSync(filePath, "utf-8");
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error.code === "ENOENT") {
|
|
136
|
+
throw new FileIOError(
|
|
137
|
+
`File not found: ${filePath}`,
|
|
138
|
+
"ENOENT",
|
|
139
|
+
filePath
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (error.code === "EACCES") {
|
|
143
|
+
throw new FileIOError(
|
|
144
|
+
`Permission denied: ${filePath}`,
|
|
145
|
+
"EACCES",
|
|
146
|
+
filePath
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (error.code === "EISDIR") {
|
|
150
|
+
throw new FileIOError(
|
|
151
|
+
`Path is a directory, not a file: ${filePath}`,
|
|
152
|
+
"EISDIR",
|
|
153
|
+
filePath
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
throw new FileIOError(
|
|
157
|
+
`Failed to read file: ${filePath}. ${error.message}`,
|
|
158
|
+
error.code,
|
|
159
|
+
filePath
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function readHTMLFromStdin() {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const chunks = [];
|
|
166
|
+
stdin.on("data", (chunk) => {
|
|
167
|
+
chunks.push(chunk);
|
|
168
|
+
});
|
|
169
|
+
stdin.on("end", () => {
|
|
170
|
+
const content = Buffer.concat(chunks).toString("utf-8");
|
|
171
|
+
resolve(content);
|
|
172
|
+
});
|
|
173
|
+
stdin.on("error", (error) => {
|
|
174
|
+
reject(new FileIOError(
|
|
175
|
+
`Failed to read from stdin: ${error.message}`,
|
|
176
|
+
"STDIN_ERROR"
|
|
177
|
+
));
|
|
178
|
+
});
|
|
179
|
+
stdin.resume();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function writeOutputToFile(filePath, content) {
|
|
183
|
+
try {
|
|
184
|
+
writeFileSync(filePath, content, "utf-8");
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error.code === "EACCES") {
|
|
187
|
+
throw new FileIOError(
|
|
188
|
+
`Permission denied: ${filePath}`,
|
|
189
|
+
"EACCES",
|
|
190
|
+
filePath
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (error.code === "ENOSPC") {
|
|
194
|
+
throw new FileIOError(
|
|
195
|
+
`No space left on device: ${filePath}`,
|
|
196
|
+
"ENOSPC",
|
|
197
|
+
filePath
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (error.code === "EISDIR") {
|
|
201
|
+
throw new FileIOError(
|
|
202
|
+
`Path is a directory, not a file: ${filePath}`,
|
|
203
|
+
"EISDIR",
|
|
204
|
+
filePath
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
throw new FileIOError(
|
|
208
|
+
`Failed to write file: ${filePath}. ${error.message}`,
|
|
209
|
+
error.code,
|
|
210
|
+
filePath
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function writeOutputToStdout(content) {
|
|
215
|
+
console.log(content);
|
|
216
|
+
}
|
|
217
|
+
function readHTML(filePath, isStdin) {
|
|
218
|
+
if (isStdin) {
|
|
219
|
+
return readHTMLFromStdin();
|
|
220
|
+
}
|
|
221
|
+
if (!filePath) {
|
|
222
|
+
throw new FileIOError("No input source specified");
|
|
223
|
+
}
|
|
224
|
+
return readHTMLFromFile(filePath);
|
|
225
|
+
}
|
|
226
|
+
function writeOutput(content, outputPath) {
|
|
227
|
+
if (outputPath) {
|
|
228
|
+
writeOutputToFile(outputPath, content);
|
|
229
|
+
} else {
|
|
230
|
+
writeOutputToStdout(content);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/parser/html-parser.ts
|
|
235
|
+
import { JSDOM } from "jsdom";
|
|
236
|
+
var ParsingError = class extends Error {
|
|
237
|
+
constructor(message, cause) {
|
|
238
|
+
super(message);
|
|
239
|
+
this.cause = cause;
|
|
240
|
+
this.name = "ParsingError";
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
function parseHTML(html) {
|
|
244
|
+
const warnings = [];
|
|
245
|
+
try {
|
|
246
|
+
const dom = new JSDOM(html, {
|
|
247
|
+
contentType: "text/html",
|
|
248
|
+
// Include useful defaults for accessibility tree extraction
|
|
249
|
+
includeNodeLocations: false,
|
|
250
|
+
storageQuota: 0
|
|
251
|
+
});
|
|
252
|
+
const document = dom.window.document;
|
|
253
|
+
if (!document.documentElement) {
|
|
254
|
+
warnings.push({
|
|
255
|
+
message: "Document has no root element. HTML may be severely malformed."
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (!document.body) {
|
|
259
|
+
warnings.push({
|
|
260
|
+
message: "Document has no body element. HTML may be malformed."
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
document,
|
|
265
|
+
warnings
|
|
266
|
+
};
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw new ParsingError(
|
|
269
|
+
`Failed to parse HTML: ${error instanceof Error ? error.message : String(error)}`,
|
|
270
|
+
error instanceof Error ? error : void 0
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/extractor/aria-name.ts
|
|
276
|
+
function computeAccessibleName(element, visited = /* @__PURE__ */ new Set()) {
|
|
277
|
+
const warnings = [];
|
|
278
|
+
if (visited.has(element)) {
|
|
279
|
+
warnings.push({
|
|
280
|
+
message: "Circular reference detected in aria-labelledby chain",
|
|
281
|
+
element
|
|
282
|
+
});
|
|
283
|
+
return { name: "", warnings };
|
|
284
|
+
}
|
|
285
|
+
visited.add(element);
|
|
286
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
287
|
+
if (labelledBy) {
|
|
288
|
+
const name = computeNameFromLabelledBy(element, labelledBy, visited, warnings);
|
|
289
|
+
return { name, warnings };
|
|
290
|
+
}
|
|
291
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
292
|
+
if (ariaLabel && ariaLabel.trim()) {
|
|
293
|
+
return { name: ariaLabel.trim(), warnings };
|
|
294
|
+
}
|
|
295
|
+
if (isFormControl(element)) {
|
|
296
|
+
const labelName = computeNameFromLabel(element);
|
|
297
|
+
if (labelName) {
|
|
298
|
+
return { name: labelName, warnings };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (element.tagName.toLowerCase() === "img") {
|
|
302
|
+
const alt = element.getAttribute("alt");
|
|
303
|
+
if (alt !== null) {
|
|
304
|
+
return { name: alt.trim(), warnings };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (supportsNameFromContent(element)) {
|
|
308
|
+
const textName = computeNameFromContent(element, visited);
|
|
309
|
+
if (textName) {
|
|
310
|
+
return { name: textName, warnings };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const title = element.getAttribute("title");
|
|
314
|
+
if (title && title.trim()) {
|
|
315
|
+
return { name: title.trim(), warnings };
|
|
316
|
+
}
|
|
317
|
+
return { name: "", warnings };
|
|
318
|
+
}
|
|
319
|
+
function computeNameFromLabelledBy(element, labelledBy, visited, warnings) {
|
|
320
|
+
const document = element.ownerDocument;
|
|
321
|
+
if (!document) {
|
|
322
|
+
return "";
|
|
323
|
+
}
|
|
324
|
+
const ids = labelledBy.trim().split(/\s+/);
|
|
325
|
+
const names = [];
|
|
326
|
+
for (const id of ids) {
|
|
327
|
+
if (!id) continue;
|
|
328
|
+
const referencedElement = document.getElementById(id);
|
|
329
|
+
if (!referencedElement) {
|
|
330
|
+
warnings.push({
|
|
331
|
+
message: `aria-labelledby references non-existent ID: ${id}`,
|
|
332
|
+
element
|
|
333
|
+
});
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const result = computeAccessibleName(referencedElement, new Set(visited));
|
|
337
|
+
warnings.push(...result.warnings);
|
|
338
|
+
const name = result.name || getTextContent(referencedElement);
|
|
339
|
+
if (name) {
|
|
340
|
+
names.push(name);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return names.join(" ").trim();
|
|
344
|
+
}
|
|
345
|
+
function computeNameFromLabel(element) {
|
|
346
|
+
const document = element.ownerDocument;
|
|
347
|
+
if (!document) {
|
|
348
|
+
return "";
|
|
349
|
+
}
|
|
350
|
+
const id = element.getAttribute("id");
|
|
351
|
+
if (id) {
|
|
352
|
+
const label = document.querySelector(`label[for="${id}"]`);
|
|
353
|
+
if (label) {
|
|
354
|
+
return getTextContent(label);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const parentLabel = element.closest("label");
|
|
358
|
+
if (parentLabel) {
|
|
359
|
+
return getTextContent(parentLabel);
|
|
360
|
+
}
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
function computeNameFromContent(element, _visited) {
|
|
364
|
+
return getTextContent(element);
|
|
365
|
+
}
|
|
366
|
+
function getTextContent(element) {
|
|
367
|
+
if (element.getAttribute("aria-hidden") === "true") {
|
|
368
|
+
return "";
|
|
369
|
+
}
|
|
370
|
+
let text = "";
|
|
371
|
+
for (const node of Array.from(element.childNodes)) {
|
|
372
|
+
if (node.nodeType === 3) {
|
|
373
|
+
text += node.textContent || "";
|
|
374
|
+
} else if (node.nodeType === 1) {
|
|
375
|
+
const childElement = node;
|
|
376
|
+
if (childElement.getAttribute("aria-hidden") === "true") {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
text += getTextContent(childElement);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return text.trim();
|
|
383
|
+
}
|
|
384
|
+
function isFormControl(element) {
|
|
385
|
+
const tagName = element.tagName.toLowerCase();
|
|
386
|
+
return tagName === "input" || tagName === "textarea" || tagName === "select" || tagName === "button";
|
|
387
|
+
}
|
|
388
|
+
function supportsNameFromContent(element) {
|
|
389
|
+
const tagName = element.tagName.toLowerCase();
|
|
390
|
+
const role = element.getAttribute("role");
|
|
391
|
+
const contentElements = [
|
|
392
|
+
"button",
|
|
393
|
+
"a",
|
|
394
|
+
"h1",
|
|
395
|
+
"h2",
|
|
396
|
+
"h3",
|
|
397
|
+
"h4",
|
|
398
|
+
"h5",
|
|
399
|
+
"h6",
|
|
400
|
+
"summary",
|
|
401
|
+
"figcaption",
|
|
402
|
+
"legend",
|
|
403
|
+
"caption",
|
|
404
|
+
"th",
|
|
405
|
+
"td",
|
|
406
|
+
"li",
|
|
407
|
+
"dt",
|
|
408
|
+
"dd"
|
|
409
|
+
];
|
|
410
|
+
if (contentElements.includes(tagName)) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
const contentRoles = [
|
|
414
|
+
"button",
|
|
415
|
+
"link",
|
|
416
|
+
"heading",
|
|
417
|
+
"tab",
|
|
418
|
+
"treeitem",
|
|
419
|
+
"option",
|
|
420
|
+
"row",
|
|
421
|
+
"cell",
|
|
422
|
+
"columnheader",
|
|
423
|
+
"rowheader",
|
|
424
|
+
"tooltip",
|
|
425
|
+
"menuitem",
|
|
426
|
+
"menuitemcheckbox",
|
|
427
|
+
"menuitemradio"
|
|
428
|
+
];
|
|
429
|
+
if (role && contentRoles.includes(role)) {
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
function computeAccessibleDescription(element) {
|
|
435
|
+
const warnings = [];
|
|
436
|
+
const describedBy = element.getAttribute("aria-describedby");
|
|
437
|
+
if (describedBy) {
|
|
438
|
+
const description = computeDescriptionFromDescribedBy(
|
|
439
|
+
element,
|
|
440
|
+
describedBy,
|
|
441
|
+
warnings
|
|
442
|
+
);
|
|
443
|
+
if (description) {
|
|
444
|
+
return { description, warnings };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const title = element.getAttribute("title");
|
|
448
|
+
if (title && title.trim()) {
|
|
449
|
+
return { description: title.trim(), warnings };
|
|
450
|
+
}
|
|
451
|
+
return { description: "", warnings };
|
|
452
|
+
}
|
|
453
|
+
function computeDescriptionFromDescribedBy(element, describedBy, warnings) {
|
|
454
|
+
const document = element.ownerDocument;
|
|
455
|
+
if (!document) {
|
|
456
|
+
return "";
|
|
457
|
+
}
|
|
458
|
+
const ids = describedBy.trim().split(/\s+/);
|
|
459
|
+
const descriptions = [];
|
|
460
|
+
for (const id of ids) {
|
|
461
|
+
if (!id) continue;
|
|
462
|
+
const referencedElement = document.getElementById(id);
|
|
463
|
+
if (!referencedElement) {
|
|
464
|
+
warnings.push({
|
|
465
|
+
message: `aria-describedby references non-existent ID: ${id}`,
|
|
466
|
+
element
|
|
467
|
+
});
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const text = getTextContent(referencedElement);
|
|
471
|
+
if (text) {
|
|
472
|
+
descriptions.push(text);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return descriptions.join(" ").trim();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/model/types.ts
|
|
479
|
+
var SUPPORTED_ROLES = [
|
|
480
|
+
// Original roles (22)
|
|
481
|
+
"button",
|
|
482
|
+
"link",
|
|
483
|
+
"heading",
|
|
484
|
+
"textbox",
|
|
485
|
+
"checkbox",
|
|
486
|
+
"radio",
|
|
487
|
+
"combobox",
|
|
488
|
+
"listbox",
|
|
489
|
+
"option",
|
|
490
|
+
"list",
|
|
491
|
+
"listitem",
|
|
492
|
+
"navigation",
|
|
493
|
+
"main",
|
|
494
|
+
"banner",
|
|
495
|
+
"contentinfo",
|
|
496
|
+
"region",
|
|
497
|
+
"img",
|
|
498
|
+
"article",
|
|
499
|
+
"complementary",
|
|
500
|
+
"form",
|
|
501
|
+
"search",
|
|
502
|
+
"generic",
|
|
503
|
+
// New roles (21)
|
|
504
|
+
"paragraph",
|
|
505
|
+
"blockquote",
|
|
506
|
+
"code",
|
|
507
|
+
"staticText",
|
|
508
|
+
"table",
|
|
509
|
+
"row",
|
|
510
|
+
"cell",
|
|
511
|
+
"columnheader",
|
|
512
|
+
"rowheader",
|
|
513
|
+
"term",
|
|
514
|
+
"definition",
|
|
515
|
+
"figure",
|
|
516
|
+
"caption",
|
|
517
|
+
"group",
|
|
518
|
+
"dialog",
|
|
519
|
+
"meter",
|
|
520
|
+
"progressbar",
|
|
521
|
+
"status",
|
|
522
|
+
"document",
|
|
523
|
+
"application",
|
|
524
|
+
"separator"
|
|
525
|
+
];
|
|
526
|
+
var CURRENT_MODEL_VERSION = {
|
|
527
|
+
major: 1,
|
|
528
|
+
minor: 0
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// src/extractor/role-mapper.ts
|
|
532
|
+
var IMPLICIT_ROLE_MAP = {
|
|
533
|
+
// Interactive elements
|
|
534
|
+
"button": "button",
|
|
535
|
+
"a": "link",
|
|
536
|
+
// Only if href attribute present
|
|
537
|
+
// Form controls
|
|
538
|
+
"input": "textbox",
|
|
539
|
+
// Default, varies by type
|
|
540
|
+
"textarea": "textbox",
|
|
541
|
+
"select": "listbox",
|
|
542
|
+
// Headings
|
|
543
|
+
"h1": "heading",
|
|
544
|
+
"h2": "heading",
|
|
545
|
+
"h3": "heading",
|
|
546
|
+
"h4": "heading",
|
|
547
|
+
"h5": "heading",
|
|
548
|
+
"h6": "heading",
|
|
549
|
+
// Landmarks
|
|
550
|
+
"nav": "navigation",
|
|
551
|
+
"main": "main",
|
|
552
|
+
"header": "banner",
|
|
553
|
+
// Only if not nested in article/section
|
|
554
|
+
"footer": "contentinfo",
|
|
555
|
+
// Only if not nested in article/section
|
|
556
|
+
"aside": "complementary",
|
|
557
|
+
"form": "form",
|
|
558
|
+
"section": "region",
|
|
559
|
+
// Only if has accessible name
|
|
560
|
+
// Lists
|
|
561
|
+
"ul": "list",
|
|
562
|
+
"ol": "list",
|
|
563
|
+
"li": "listitem",
|
|
564
|
+
// Images
|
|
565
|
+
"img": "img",
|
|
566
|
+
// Articles
|
|
567
|
+
"article": "article",
|
|
568
|
+
// Static content (new)
|
|
569
|
+
"p": "paragraph",
|
|
570
|
+
"blockquote": "blockquote",
|
|
571
|
+
"code": "code",
|
|
572
|
+
"pre": "code",
|
|
573
|
+
// Tables (new)
|
|
574
|
+
"table": "table",
|
|
575
|
+
"tr": "row",
|
|
576
|
+
"td": "cell",
|
|
577
|
+
"th": "columnheader",
|
|
578
|
+
// Default; scope="row" handled in computeImplicitRole
|
|
579
|
+
// Definition lists (new)
|
|
580
|
+
"dl": "list",
|
|
581
|
+
"dt": "term",
|
|
582
|
+
"dd": "definition",
|
|
583
|
+
// Figures (new)
|
|
584
|
+
"figure": "figure",
|
|
585
|
+
"figcaption": "caption",
|
|
586
|
+
// Disclosure (new)
|
|
587
|
+
"details": "group",
|
|
588
|
+
"summary": "button",
|
|
589
|
+
// Dialogs and widgets (new)
|
|
590
|
+
"dialog": "dialog",
|
|
591
|
+
"meter": "meter",
|
|
592
|
+
"progress": "progressbar",
|
|
593
|
+
"output": "status",
|
|
594
|
+
// Forms (new)
|
|
595
|
+
"fieldset": "group",
|
|
596
|
+
"legend": "caption",
|
|
597
|
+
// Embedded content (new)
|
|
598
|
+
"iframe": "document",
|
|
599
|
+
"video": "application",
|
|
600
|
+
"audio": "application",
|
|
601
|
+
// Separators (new)
|
|
602
|
+
"hr": "separator",
|
|
603
|
+
// Table caption (new)
|
|
604
|
+
"caption": "caption"
|
|
605
|
+
};
|
|
606
|
+
var INPUT_TYPE_ROLE_MAP = {
|
|
607
|
+
"button": "button",
|
|
608
|
+
"submit": "button",
|
|
609
|
+
"reset": "button",
|
|
610
|
+
"checkbox": "checkbox",
|
|
611
|
+
"radio": "radio",
|
|
612
|
+
"text": "textbox",
|
|
613
|
+
"email": "textbox",
|
|
614
|
+
"password": "textbox",
|
|
615
|
+
"search": "textbox",
|
|
616
|
+
"tel": "textbox",
|
|
617
|
+
"url": "textbox",
|
|
618
|
+
"number": "textbox"
|
|
619
|
+
};
|
|
620
|
+
function computeRole(element) {
|
|
621
|
+
const warnings = [];
|
|
622
|
+
const explicitRole = element.getAttribute("role");
|
|
623
|
+
if (explicitRole) {
|
|
624
|
+
const role = validateAndNormalizeRole(explicitRole, element, warnings);
|
|
625
|
+
if (role) {
|
|
626
|
+
return { role, warnings };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const implicitRole = computeImplicitRole(element, warnings);
|
|
630
|
+
return { role: implicitRole, warnings };
|
|
631
|
+
}
|
|
632
|
+
function validateAndNormalizeRole(roleAttr, element, warnings) {
|
|
633
|
+
const role = roleAttr.trim().toLowerCase();
|
|
634
|
+
if (role === "presentation" || role === "none") {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
if (SUPPORTED_ROLES.includes(role)) {
|
|
638
|
+
return role;
|
|
639
|
+
}
|
|
640
|
+
warnings.push({
|
|
641
|
+
message: `Invalid or unsupported role: "${roleAttr}". Falling back to implicit role.`,
|
|
642
|
+
element
|
|
643
|
+
});
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function computeImplicitRole(element, _warnings) {
|
|
647
|
+
const tagName = element.tagName.toLowerCase();
|
|
648
|
+
if (tagName === "input") {
|
|
649
|
+
return computeInputRole(element);
|
|
650
|
+
}
|
|
651
|
+
if (tagName === "a") {
|
|
652
|
+
return element.hasAttribute("href") ? "link" : null;
|
|
653
|
+
}
|
|
654
|
+
if (tagName === "header") {
|
|
655
|
+
return isTopLevelLandmark(element) ? "banner" : null;
|
|
656
|
+
}
|
|
657
|
+
if (tagName === "footer") {
|
|
658
|
+
return isTopLevelLandmark(element) ? "contentinfo" : null;
|
|
659
|
+
}
|
|
660
|
+
if (tagName === "th") {
|
|
661
|
+
const scope = element.getAttribute("scope");
|
|
662
|
+
return scope === "row" ? "rowheader" : "columnheader";
|
|
663
|
+
}
|
|
664
|
+
if (tagName === "section") {
|
|
665
|
+
return element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") ? "region" : null;
|
|
666
|
+
}
|
|
667
|
+
const implicitRole = IMPLICIT_ROLE_MAP[tagName];
|
|
668
|
+
return implicitRole || null;
|
|
669
|
+
}
|
|
670
|
+
function computeInputRole(input) {
|
|
671
|
+
const type = (input.getAttribute("type") || "text").toLowerCase();
|
|
672
|
+
return INPUT_TYPE_ROLE_MAP[type] || "textbox";
|
|
673
|
+
}
|
|
674
|
+
function isTopLevelLandmark(element) {
|
|
675
|
+
let parent = element.parentElement;
|
|
676
|
+
while (parent) {
|
|
677
|
+
const tagName = parent.tagName.toLowerCase();
|
|
678
|
+
if (tagName === "article" || tagName === "section") {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
parent = parent.parentElement;
|
|
682
|
+
}
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
function isAccessible(element) {
|
|
686
|
+
if (element.getAttribute("aria-hidden") === "true") {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const role = element.getAttribute("role");
|
|
690
|
+
if (role === "presentation" || role === "none") {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
const roleResult = computeRole(element);
|
|
694
|
+
return roleResult.role !== null;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/extractor/state-extractor.ts
|
|
698
|
+
function extractState(element) {
|
|
699
|
+
const warnings = [];
|
|
700
|
+
const state = {};
|
|
701
|
+
const expanded = extractBooleanAttribute(element, "aria-expanded", warnings);
|
|
702
|
+
if (expanded !== void 0) {
|
|
703
|
+
state.expanded = expanded;
|
|
704
|
+
}
|
|
705
|
+
const checked = extractTriStateAttribute(element, "aria-checked", warnings);
|
|
706
|
+
if (checked !== void 0) {
|
|
707
|
+
state.checked = checked;
|
|
708
|
+
}
|
|
709
|
+
const pressed = extractTriStateAttribute(element, "aria-pressed", warnings);
|
|
710
|
+
if (pressed !== void 0) {
|
|
711
|
+
state.pressed = pressed;
|
|
712
|
+
}
|
|
713
|
+
const selected = extractBooleanAttribute(element, "aria-selected", warnings);
|
|
714
|
+
if (selected !== void 0) {
|
|
715
|
+
state.selected = selected;
|
|
716
|
+
}
|
|
717
|
+
const disabled = extractBooleanAttribute(element, "aria-disabled", warnings);
|
|
718
|
+
if (disabled !== void 0) {
|
|
719
|
+
state.disabled = disabled;
|
|
720
|
+
}
|
|
721
|
+
const invalid = extractBooleanAttribute(element, "aria-invalid", warnings);
|
|
722
|
+
if (invalid !== void 0) {
|
|
723
|
+
state.invalid = invalid;
|
|
724
|
+
}
|
|
725
|
+
const required = extractBooleanAttribute(element, "aria-required", warnings);
|
|
726
|
+
if (required !== void 0) {
|
|
727
|
+
state.required = required;
|
|
728
|
+
}
|
|
729
|
+
const readonly = extractBooleanAttribute(element, "aria-readonly", warnings);
|
|
730
|
+
if (readonly !== void 0) {
|
|
731
|
+
state.readonly = readonly;
|
|
732
|
+
}
|
|
733
|
+
const busy = extractBooleanAttribute(element, "aria-busy", warnings);
|
|
734
|
+
if (busy !== void 0) {
|
|
735
|
+
state.busy = busy;
|
|
736
|
+
}
|
|
737
|
+
const current = extractCurrentAttribute(element, warnings);
|
|
738
|
+
if (current !== void 0) {
|
|
739
|
+
state.current = current;
|
|
740
|
+
}
|
|
741
|
+
const grabbed = extractBooleanAttribute(element, "aria-grabbed", warnings);
|
|
742
|
+
if (grabbed !== void 0) {
|
|
743
|
+
state.grabbed = grabbed;
|
|
744
|
+
}
|
|
745
|
+
const hidden = extractBooleanAttribute(element, "aria-hidden", warnings);
|
|
746
|
+
if (hidden !== void 0) {
|
|
747
|
+
state.hidden = hidden;
|
|
748
|
+
}
|
|
749
|
+
const level = extractLevelAttribute(element, warnings);
|
|
750
|
+
if (level !== void 0) {
|
|
751
|
+
state.level = level;
|
|
752
|
+
}
|
|
753
|
+
const posinset = extractNumberAttribute(element, "aria-posinset", warnings);
|
|
754
|
+
if (posinset !== void 0) {
|
|
755
|
+
state.posinset = posinset;
|
|
756
|
+
}
|
|
757
|
+
const setsize = extractNumberAttribute(element, "aria-setsize", warnings);
|
|
758
|
+
if (setsize !== void 0) {
|
|
759
|
+
state.setsize = setsize;
|
|
760
|
+
}
|
|
761
|
+
extractNativeStates(element, state, warnings);
|
|
762
|
+
return { state, warnings };
|
|
763
|
+
}
|
|
764
|
+
function extractValue(element) {
|
|
765
|
+
const warnings = [];
|
|
766
|
+
const tagName = element.tagName.toLowerCase();
|
|
767
|
+
if (tagName === "input") {
|
|
768
|
+
return extractInputValue(element, warnings);
|
|
769
|
+
}
|
|
770
|
+
if (tagName === "textarea") {
|
|
771
|
+
return extractTextareaValue(element, warnings);
|
|
772
|
+
}
|
|
773
|
+
if (tagName === "select") {
|
|
774
|
+
return extractSelectValue(element, warnings);
|
|
775
|
+
}
|
|
776
|
+
const valueNow = element.getAttribute("aria-valuenow");
|
|
777
|
+
if (valueNow !== null) {
|
|
778
|
+
const current = parseFloat(valueNow);
|
|
779
|
+
if (!isNaN(current)) {
|
|
780
|
+
const valueMin = element.getAttribute("aria-valuemin");
|
|
781
|
+
const valueMax = element.getAttribute("aria-valuemax");
|
|
782
|
+
const valueText = element.getAttribute("aria-valuetext");
|
|
783
|
+
return {
|
|
784
|
+
value: {
|
|
785
|
+
current,
|
|
786
|
+
min: valueMin !== null ? parseFloat(valueMin) : void 0,
|
|
787
|
+
max: valueMax !== null ? parseFloat(valueMax) : void 0,
|
|
788
|
+
text: valueText || void 0
|
|
789
|
+
},
|
|
790
|
+
warnings
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return { value: void 0, warnings };
|
|
795
|
+
}
|
|
796
|
+
function extractBooleanAttribute(element, attrName, warnings) {
|
|
797
|
+
const value = element.getAttribute(attrName);
|
|
798
|
+
if (value === null) {
|
|
799
|
+
return void 0;
|
|
800
|
+
}
|
|
801
|
+
const normalized = value.toLowerCase().trim();
|
|
802
|
+
if (normalized === "true") {
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
if (normalized === "false") {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
warnings.push({
|
|
809
|
+
message: `Invalid ${attrName} value: "${value}". Expected "true" or "false".`,
|
|
810
|
+
element
|
|
811
|
+
});
|
|
812
|
+
return void 0;
|
|
813
|
+
}
|
|
814
|
+
function extractTriStateAttribute(element, attrName, warnings) {
|
|
815
|
+
const value = element.getAttribute(attrName);
|
|
816
|
+
if (value === null) {
|
|
817
|
+
return void 0;
|
|
818
|
+
}
|
|
819
|
+
const normalized = value.toLowerCase().trim();
|
|
820
|
+
if (normalized === "true") {
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
if (normalized === "false") {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
if (normalized === "mixed") {
|
|
827
|
+
return "mixed";
|
|
828
|
+
}
|
|
829
|
+
warnings.push({
|
|
830
|
+
message: `Invalid ${attrName} value: "${value}". Expected "true", "false", or "mixed".`,
|
|
831
|
+
element
|
|
832
|
+
});
|
|
833
|
+
return void 0;
|
|
834
|
+
}
|
|
835
|
+
function extractCurrentAttribute(element, warnings) {
|
|
836
|
+
const value = element.getAttribute("aria-current");
|
|
837
|
+
if (value === null) {
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
const normalized = value.toLowerCase().trim();
|
|
841
|
+
const validValues = ["page", "step", "location", "date", "time", "true", "false"];
|
|
842
|
+
if (validValues.includes(normalized)) {
|
|
843
|
+
return normalized === "false" ? false : normalized;
|
|
844
|
+
}
|
|
845
|
+
warnings.push({
|
|
846
|
+
message: `Invalid aria-current value: "${value}". Expected one of: ${validValues.join(", ")}.`,
|
|
847
|
+
element
|
|
848
|
+
});
|
|
849
|
+
return void 0;
|
|
850
|
+
}
|
|
851
|
+
function extractLevelAttribute(element, warnings) {
|
|
852
|
+
const ariaLevel = element.getAttribute("aria-level");
|
|
853
|
+
if (ariaLevel !== null) {
|
|
854
|
+
const level = parseInt(ariaLevel, 10);
|
|
855
|
+
if (!isNaN(level) && level >= 1 && level <= 6) {
|
|
856
|
+
return level;
|
|
857
|
+
}
|
|
858
|
+
warnings.push({
|
|
859
|
+
message: `Invalid aria-level value: "${ariaLevel}". Expected integer 1-6.`,
|
|
860
|
+
element
|
|
861
|
+
});
|
|
862
|
+
return void 0;
|
|
863
|
+
}
|
|
864
|
+
const tagName = element.tagName.toLowerCase();
|
|
865
|
+
const match = tagName.match(/^h([1-6])$/);
|
|
866
|
+
if (match) {
|
|
867
|
+
return parseInt(match[1], 10);
|
|
868
|
+
}
|
|
869
|
+
return void 0;
|
|
870
|
+
}
|
|
871
|
+
function extractNumberAttribute(element, attrName, warnings) {
|
|
872
|
+
const value = element.getAttribute(attrName);
|
|
873
|
+
if (value === null) {
|
|
874
|
+
return void 0;
|
|
875
|
+
}
|
|
876
|
+
const num = parseInt(value, 10);
|
|
877
|
+
if (isNaN(num) || num < 1) {
|
|
878
|
+
warnings.push({
|
|
879
|
+
message: `Invalid ${attrName} value: "${value}". Expected positive integer.`,
|
|
880
|
+
element
|
|
881
|
+
});
|
|
882
|
+
return void 0;
|
|
883
|
+
}
|
|
884
|
+
return num;
|
|
885
|
+
}
|
|
886
|
+
function extractNativeStates(element, state, _warnings) {
|
|
887
|
+
const tagName = element.tagName.toLowerCase();
|
|
888
|
+
if (tagName === "input") {
|
|
889
|
+
const input = element;
|
|
890
|
+
const type = input.type.toLowerCase();
|
|
891
|
+
if (type === "checkbox" || type === "radio") {
|
|
892
|
+
if (state.checked === void 0) {
|
|
893
|
+
state.checked = input.checked;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if ("disabled" in element) {
|
|
898
|
+
const htmlElement = element;
|
|
899
|
+
if (state.disabled === void 0 && htmlElement.disabled) {
|
|
900
|
+
state.disabled = true;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if ("required" in element) {
|
|
904
|
+
const htmlElement = element;
|
|
905
|
+
if (state.required === void 0 && htmlElement.required) {
|
|
906
|
+
state.required = true;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if ("readOnly" in element) {
|
|
910
|
+
const htmlElement = element;
|
|
911
|
+
if (state.readonly === void 0 && htmlElement.readOnly) {
|
|
912
|
+
state.readonly = true;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function extractInputValue(input, warnings) {
|
|
917
|
+
const type = input.type.toLowerCase();
|
|
918
|
+
if (type === "checkbox" || type === "radio") {
|
|
919
|
+
return { value: void 0, warnings };
|
|
920
|
+
}
|
|
921
|
+
if (type === "button" || type === "submit" || type === "reset") {
|
|
922
|
+
return { value: void 0, warnings };
|
|
923
|
+
}
|
|
924
|
+
const value = input.value;
|
|
925
|
+
if (value) {
|
|
926
|
+
return {
|
|
927
|
+
value: {
|
|
928
|
+
current: value,
|
|
929
|
+
text: value
|
|
930
|
+
},
|
|
931
|
+
warnings
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
return { value: void 0, warnings };
|
|
935
|
+
}
|
|
936
|
+
function extractTextareaValue(textarea, warnings) {
|
|
937
|
+
const value = textarea.value;
|
|
938
|
+
if (value) {
|
|
939
|
+
return {
|
|
940
|
+
value: {
|
|
941
|
+
current: value,
|
|
942
|
+
text: value
|
|
943
|
+
},
|
|
944
|
+
warnings
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
return { value: void 0, warnings };
|
|
948
|
+
}
|
|
949
|
+
function extractSelectValue(select, warnings) {
|
|
950
|
+
const value = select.value;
|
|
951
|
+
const selectedOption = select.options[select.selectedIndex];
|
|
952
|
+
const text = selectedOption?.textContent || value;
|
|
953
|
+
if (value) {
|
|
954
|
+
return {
|
|
955
|
+
value: {
|
|
956
|
+
current: value,
|
|
957
|
+
text
|
|
958
|
+
},
|
|
959
|
+
warnings
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
return { value: void 0, warnings };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/extractor/focus-extractor.ts
|
|
966
|
+
var NATIVELY_FOCUSABLE_ELEMENTS = [
|
|
967
|
+
"a",
|
|
968
|
+
"button",
|
|
969
|
+
"input",
|
|
970
|
+
"select",
|
|
971
|
+
"textarea",
|
|
972
|
+
"area",
|
|
973
|
+
"iframe",
|
|
974
|
+
"object",
|
|
975
|
+
"embed",
|
|
976
|
+
"audio",
|
|
977
|
+
"video"
|
|
978
|
+
];
|
|
979
|
+
function extractFocusInfo(element) {
|
|
980
|
+
const tabindexAttr = element.getAttribute("tabindex");
|
|
981
|
+
const tabindex = tabindexAttr !== null ? parseInt(tabindexAttr, 10) : void 0;
|
|
982
|
+
const focusable = isFocusable(element, tabindex);
|
|
983
|
+
return {
|
|
984
|
+
focusable,
|
|
985
|
+
...tabindexAttr !== null && !isNaN(tabindex) && { tabindex }
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function isFocusable(element, tabindex) {
|
|
989
|
+
const tagName = element.tagName.toLowerCase();
|
|
990
|
+
if (isDisabled(element)) {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
if (tabindex !== void 0 && !isNaN(tabindex)) {
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
if (isNativelyFocusable(element, tagName)) {
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
function isNativelyFocusable(element, tagName) {
|
|
1002
|
+
if (tagName === "a" || tagName === "area") {
|
|
1003
|
+
return element.hasAttribute("href");
|
|
1004
|
+
}
|
|
1005
|
+
if (NATIVELY_FOCUSABLE_ELEMENTS.includes(tagName)) {
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
function isDisabled(element) {
|
|
1011
|
+
if ("disabled" in element) {
|
|
1012
|
+
const htmlElement = element;
|
|
1013
|
+
if (htmlElement.disabled) {
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const ariaDisabled = element.getAttribute("aria-disabled");
|
|
1018
|
+
if (ariaDisabled === "true") {
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/extractor/tree-builder.ts
|
|
1025
|
+
var SelectorError = class extends Error {
|
|
1026
|
+
constructor(message) {
|
|
1027
|
+
super(message);
|
|
1028
|
+
this.name = "SelectorError";
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
function isHidden(element) {
|
|
1032
|
+
if (element.getAttribute("aria-hidden") === "true") return true;
|
|
1033
|
+
if (typeof getComputedStyle === "function") {
|
|
1034
|
+
try {
|
|
1035
|
+
const style = getComputedStyle(element);
|
|
1036
|
+
if (style.display === "none") return true;
|
|
1037
|
+
if (style.visibility === "hidden") return true;
|
|
1038
|
+
} catch {
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
function processChildNode(node, children, warnings) {
|
|
1044
|
+
if (node.nodeType === 3) {
|
|
1045
|
+
const text = (node.textContent || "").trim();
|
|
1046
|
+
if (text.length > 0) {
|
|
1047
|
+
children.push({
|
|
1048
|
+
role: "staticText",
|
|
1049
|
+
name: text,
|
|
1050
|
+
state: {},
|
|
1051
|
+
focus: { focusable: false },
|
|
1052
|
+
children: []
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (node.nodeType === 1) {
|
|
1058
|
+
const element = node;
|
|
1059
|
+
if (isHidden(element)) return;
|
|
1060
|
+
if (!isAccessible(element)) {
|
|
1061
|
+
const grandchildren = collectChildrenFromRolelessElement(element, warnings);
|
|
1062
|
+
children.push(...grandchildren);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const childNode = buildNodeRecursive(element, warnings);
|
|
1066
|
+
if (childNode) children.push(childNode);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
function collectChildrenFromRolelessElement(element, warnings) {
|
|
1070
|
+
const children = [];
|
|
1071
|
+
if (element.shadowRoot) {
|
|
1072
|
+
for (const child of Array.from(element.shadowRoot.childNodes)) {
|
|
1073
|
+
processChildNode(child, children, warnings);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
for (const child of Array.from(element.childNodes)) {
|
|
1077
|
+
processChildNode(child, children, warnings);
|
|
1078
|
+
}
|
|
1079
|
+
return children;
|
|
1080
|
+
}
|
|
1081
|
+
function buildAccessibilityTree(rootElement, sourceHash) {
|
|
1082
|
+
const warnings = [];
|
|
1083
|
+
if (isHidden(rootElement)) {
|
|
1084
|
+
const model2 = {
|
|
1085
|
+
version: CURRENT_MODEL_VERSION,
|
|
1086
|
+
root: {
|
|
1087
|
+
role: "generic",
|
|
1088
|
+
name: "",
|
|
1089
|
+
state: {},
|
|
1090
|
+
focus: { focusable: false },
|
|
1091
|
+
children: []
|
|
1092
|
+
},
|
|
1093
|
+
metadata: {
|
|
1094
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1095
|
+
...sourceHash && { sourceHash }
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
return { model: model2, warnings };
|
|
1099
|
+
}
|
|
1100
|
+
const rootNode = buildNodeRecursive(rootElement, warnings);
|
|
1101
|
+
const accessibleRoot = rootNode || createGenericContainer(rootElement, warnings);
|
|
1102
|
+
const model = {
|
|
1103
|
+
version: CURRENT_MODEL_VERSION,
|
|
1104
|
+
root: accessibleRoot,
|
|
1105
|
+
metadata: {
|
|
1106
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1107
|
+
...sourceHash && { sourceHash }
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
return { model, warnings };
|
|
1111
|
+
}
|
|
1112
|
+
function buildAccessibilityTreeWithSelector(rootElement, selector, sourceHash) {
|
|
1113
|
+
const document = rootElement.ownerDocument;
|
|
1114
|
+
if (!document) {
|
|
1115
|
+
throw new SelectorError("Element has no owner document");
|
|
1116
|
+
}
|
|
1117
|
+
let matchingElements;
|
|
1118
|
+
try {
|
|
1119
|
+
if (rootElement.matches(selector)) {
|
|
1120
|
+
matchingElements = [rootElement];
|
|
1121
|
+
} else {
|
|
1122
|
+
matchingElements = Array.from(rootElement.querySelectorAll(selector));
|
|
1123
|
+
}
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
throw new SelectorError(`Invalid CSS selector: "${selector}". ${error instanceof Error ? error.message : String(error)}`);
|
|
1126
|
+
}
|
|
1127
|
+
if (matchingElements.length === 0) {
|
|
1128
|
+
throw new SelectorError(`No elements match selector: "${selector}"`);
|
|
1129
|
+
}
|
|
1130
|
+
const results = [];
|
|
1131
|
+
for (const element of matchingElements) {
|
|
1132
|
+
const result = buildAccessibilityTree(element, sourceHash);
|
|
1133
|
+
results.push(result);
|
|
1134
|
+
}
|
|
1135
|
+
return results;
|
|
1136
|
+
}
|
|
1137
|
+
function buildNodeRecursive(element, warnings) {
|
|
1138
|
+
if (isHidden(element)) {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
if (!isAccessible(element)) {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
const roleResult = computeRole(element);
|
|
1145
|
+
warnings.push(...roleResult.warnings.map((w) => ({
|
|
1146
|
+
message: w.message,
|
|
1147
|
+
element: w.element
|
|
1148
|
+
})));
|
|
1149
|
+
if (!roleResult.role) {
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
const nameResult = computeAccessibleName(element);
|
|
1153
|
+
warnings.push(...nameResult.warnings.map((w) => ({
|
|
1154
|
+
message: w.message,
|
|
1155
|
+
element: w.element
|
|
1156
|
+
})));
|
|
1157
|
+
const descResult = computeAccessibleDescription(element);
|
|
1158
|
+
warnings.push(...descResult.warnings.map((w) => ({
|
|
1159
|
+
message: w.message,
|
|
1160
|
+
element: w.element
|
|
1161
|
+
})));
|
|
1162
|
+
const stateResult = extractState(element);
|
|
1163
|
+
warnings.push(...stateResult.warnings.map((w) => ({
|
|
1164
|
+
message: w.message,
|
|
1165
|
+
element: w.element
|
|
1166
|
+
})));
|
|
1167
|
+
const valueResult = extractValue(element);
|
|
1168
|
+
warnings.push(...valueResult.warnings.map((w) => ({
|
|
1169
|
+
message: w.message,
|
|
1170
|
+
element: w.element
|
|
1171
|
+
})));
|
|
1172
|
+
const focusInfo = extractFocusInfo(element);
|
|
1173
|
+
const children = [];
|
|
1174
|
+
if (element.shadowRoot) {
|
|
1175
|
+
for (const child of Array.from(element.shadowRoot.childNodes)) {
|
|
1176
|
+
processChildNode(child, children, warnings);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
for (const child of Array.from(element.childNodes)) {
|
|
1180
|
+
processChildNode(child, children, warnings);
|
|
1181
|
+
}
|
|
1182
|
+
const node = {
|
|
1183
|
+
role: roleResult.role,
|
|
1184
|
+
name: nameResult.name,
|
|
1185
|
+
...descResult.description && { description: descResult.description },
|
|
1186
|
+
...valueResult.value && { value: valueResult.value },
|
|
1187
|
+
state: stateResult.state,
|
|
1188
|
+
focus: focusInfo,
|
|
1189
|
+
children
|
|
1190
|
+
};
|
|
1191
|
+
return node;
|
|
1192
|
+
}
|
|
1193
|
+
function createGenericContainer(rootElement, warnings) {
|
|
1194
|
+
const children = [];
|
|
1195
|
+
if (rootElement.shadowRoot) {
|
|
1196
|
+
for (const child of Array.from(rootElement.shadowRoot.childNodes)) {
|
|
1197
|
+
processChildNode(child, children, warnings);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
for (const child of Array.from(rootElement.childNodes)) {
|
|
1201
|
+
processChildNode(child, children, warnings);
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
role: "generic",
|
|
1205
|
+
name: "",
|
|
1206
|
+
state: {},
|
|
1207
|
+
focus: {
|
|
1208
|
+
focusable: false
|
|
1209
|
+
},
|
|
1210
|
+
children
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// src/model/validation.ts
|
|
1215
|
+
var ValidationError = class extends Error {
|
|
1216
|
+
constructor(message) {
|
|
1217
|
+
super(message);
|
|
1218
|
+
this.name = "ValidationError";
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
function validateRole(role) {
|
|
1222
|
+
if (!SUPPORTED_ROLES.includes(role)) {
|
|
1223
|
+
throw new ValidationError(
|
|
1224
|
+
`Invalid role: "${role}". Supported roles: ${SUPPORTED_ROLES.join(", ")}`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
function validateState(state) {
|
|
1230
|
+
if (state.level !== void 0) {
|
|
1231
|
+
if (!Number.isInteger(state.level) || state.level < 1 || state.level > 6) {
|
|
1232
|
+
throw new ValidationError(
|
|
1233
|
+
`Invalid heading level: ${state.level}. Must be an integer between 1 and 6.`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (state.posinset !== void 0) {
|
|
1238
|
+
if (!Number.isInteger(state.posinset) || state.posinset < 1) {
|
|
1239
|
+
throw new ValidationError(
|
|
1240
|
+
`Invalid posinset: ${state.posinset}. Must be a positive integer.`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (state.setsize !== void 0) {
|
|
1245
|
+
if (!Number.isInteger(state.setsize) || state.setsize < 1) {
|
|
1246
|
+
throw new ValidationError(
|
|
1247
|
+
`Invalid setsize: ${state.setsize}. Must be a positive integer.`
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (state.posinset !== void 0 && state.setsize !== void 0 && state.posinset > state.setsize) {
|
|
1252
|
+
throw new ValidationError(
|
|
1253
|
+
`Invalid set position: posinset (${state.posinset}) cannot exceed setsize (${state.setsize}).`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
if (state.checked !== void 0) {
|
|
1257
|
+
if (typeof state.checked !== "boolean" && state.checked !== "mixed") {
|
|
1258
|
+
throw new ValidationError(
|
|
1259
|
+
`Invalid checked value: ${state.checked}. Must be boolean or 'mixed'.`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (state.pressed !== void 0) {
|
|
1264
|
+
if (typeof state.pressed !== "boolean" && state.pressed !== "mixed") {
|
|
1265
|
+
throw new ValidationError(
|
|
1266
|
+
`Invalid pressed value: ${state.pressed}. Must be boolean or 'mixed'.`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (state.current !== void 0) {
|
|
1271
|
+
const validCurrentValues = ["page", "step", "location", "date", "time", "true", false];
|
|
1272
|
+
if (!validCurrentValues.includes(state.current)) {
|
|
1273
|
+
throw new ValidationError(
|
|
1274
|
+
`Invalid current value: ${state.current}. Must be one of: ${validCurrentValues.join(", ")}`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function validateTreeStructure(node, visited = /* @__PURE__ */ new Set()) {
|
|
1280
|
+
if (visited.has(node)) {
|
|
1281
|
+
throw new ValidationError(
|
|
1282
|
+
"Circular reference detected in accessibility tree. Tree must be acyclic."
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
visited.add(node);
|
|
1286
|
+
validateRole(node.role);
|
|
1287
|
+
validateState(node.state);
|
|
1288
|
+
for (const child of node.children) {
|
|
1289
|
+
validateTreeStructure(child, new Set(visited));
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function validateModel(model) {
|
|
1293
|
+
if (!model.version || typeof model.version.major !== "number" || typeof model.version.minor !== "number") {
|
|
1294
|
+
throw new ValidationError("Invalid model version. Must have major and minor number fields.");
|
|
1295
|
+
}
|
|
1296
|
+
if (!model.metadata || !model.metadata.extractedAt) {
|
|
1297
|
+
throw new ValidationError("Invalid metadata. Must have extractedAt timestamp.");
|
|
1298
|
+
}
|
|
1299
|
+
const timestamp = new Date(model.metadata.extractedAt);
|
|
1300
|
+
if (isNaN(timestamp.getTime())) {
|
|
1301
|
+
throw new ValidationError(
|
|
1302
|
+
`Invalid extractedAt timestamp: ${model.metadata.extractedAt}. Must be ISO 8601 format.`
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
if (!model.root) {
|
|
1306
|
+
throw new ValidationError("Model must have a root node.");
|
|
1307
|
+
}
|
|
1308
|
+
validateTreeStructure(model.root);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/model/serialization.ts
|
|
1312
|
+
function sortObjectKeys(obj) {
|
|
1313
|
+
if (obj === null || typeof obj !== "object") {
|
|
1314
|
+
return obj;
|
|
1315
|
+
}
|
|
1316
|
+
if (Array.isArray(obj)) {
|
|
1317
|
+
return obj.map(sortObjectKeys);
|
|
1318
|
+
}
|
|
1319
|
+
const sorted = {};
|
|
1320
|
+
const keys = Object.keys(obj).sort();
|
|
1321
|
+
for (const key of keys) {
|
|
1322
|
+
const value = obj[key];
|
|
1323
|
+
sorted[key] = typeof value === "object" && value !== null ? sortObjectKeys(value) : value;
|
|
1324
|
+
}
|
|
1325
|
+
return sorted;
|
|
1326
|
+
}
|
|
1327
|
+
function serializeModel(model, options = {}) {
|
|
1328
|
+
const { pretty = false, validate = true } = options;
|
|
1329
|
+
if (validate) {
|
|
1330
|
+
validateModel(model);
|
|
1331
|
+
}
|
|
1332
|
+
const sorted = sortObjectKeys(model);
|
|
1333
|
+
return pretty ? JSON.stringify(sorted, null, 2) : JSON.stringify(sorted);
|
|
1334
|
+
}
|
|
1335
|
+
function deserializeModel(json, options = {}) {
|
|
1336
|
+
const { validate = true } = options;
|
|
1337
|
+
const model = JSON.parse(json);
|
|
1338
|
+
if (validate) {
|
|
1339
|
+
validateModel(model);
|
|
1340
|
+
}
|
|
1341
|
+
return model;
|
|
1342
|
+
}
|
|
1343
|
+
function modelsEqual(a, b) {
|
|
1344
|
+
const jsonA = serializeModel(a, { validate: false });
|
|
1345
|
+
const jsonB = serializeModel(b, { validate: false });
|
|
1346
|
+
return jsonA === jsonB;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/cli/colors.ts
|
|
1350
|
+
import pc from "picocolors";
|
|
1351
|
+
var identity = (s) => s;
|
|
1352
|
+
function isColorEnabled(stream) {
|
|
1353
|
+
return stream.isTTY === true;
|
|
1354
|
+
}
|
|
1355
|
+
function createColors(enabled) {
|
|
1356
|
+
if (!enabled) {
|
|
1357
|
+
return {
|
|
1358
|
+
error: identity,
|
|
1359
|
+
warning: identity,
|
|
1360
|
+
info: identity,
|
|
1361
|
+
success: identity,
|
|
1362
|
+
heading: identity,
|
|
1363
|
+
title: identity,
|
|
1364
|
+
dim: identity,
|
|
1365
|
+
roleName: identity,
|
|
1366
|
+
stateName: identity,
|
|
1367
|
+
elementName: identity,
|
|
1368
|
+
sectionHeader: identity,
|
|
1369
|
+
description: identity,
|
|
1370
|
+
bold: identity,
|
|
1371
|
+
enabled: false
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
error: pc.red,
|
|
1376
|
+
warning: pc.yellow,
|
|
1377
|
+
info: pc.blue,
|
|
1378
|
+
success: pc.green,
|
|
1379
|
+
heading: (s) => pc.bold(pc.cyan(s)),
|
|
1380
|
+
title: (s) => pc.bold(pc.cyan(s)),
|
|
1381
|
+
dim: pc.dim,
|
|
1382
|
+
roleName: pc.cyan,
|
|
1383
|
+
stateName: pc.yellow,
|
|
1384
|
+
elementName: (s) => pc.bold(pc.white(s)),
|
|
1385
|
+
sectionHeader: (s) => pc.bold(pc.white(s)),
|
|
1386
|
+
description: pc.dim,
|
|
1387
|
+
bold: pc.bold,
|
|
1388
|
+
enabled: true
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// src/renderer/nvda-renderer.ts
|
|
1393
|
+
function renderNVDA(model, colorize) {
|
|
1394
|
+
const c = createColors(colorize ?? false);
|
|
1395
|
+
const announcements = [];
|
|
1396
|
+
renderNodeNVDA(model.root, announcements, c);
|
|
1397
|
+
return announcements.join("\n");
|
|
1398
|
+
}
|
|
1399
|
+
function renderNodeNVDA(node, announcements, c) {
|
|
1400
|
+
const announcement = formatNodeNVDA(node, c);
|
|
1401
|
+
if (announcement) {
|
|
1402
|
+
announcements.push(announcement);
|
|
1403
|
+
}
|
|
1404
|
+
for (const child of node.children) {
|
|
1405
|
+
renderNodeNVDA(child, announcements, c);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function formatNodeNVDA(node, c) {
|
|
1409
|
+
const parts = [];
|
|
1410
|
+
if (node.name) {
|
|
1411
|
+
parts.push(c.elementName(node.name));
|
|
1412
|
+
}
|
|
1413
|
+
const roleText = formatRoleNVDA(node);
|
|
1414
|
+
if (roleText) {
|
|
1415
|
+
parts.push(c.roleName(roleText));
|
|
1416
|
+
}
|
|
1417
|
+
const stateText = formatStatesNVDA(node);
|
|
1418
|
+
if (stateText) {
|
|
1419
|
+
parts.push(c.stateName(stateText));
|
|
1420
|
+
}
|
|
1421
|
+
if (node.value) {
|
|
1422
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1423
|
+
if (valueText) {
|
|
1424
|
+
parts.push(valueText);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (node.description) {
|
|
1428
|
+
parts.push(c.description(node.description));
|
|
1429
|
+
}
|
|
1430
|
+
return parts.join(", ");
|
|
1431
|
+
}
|
|
1432
|
+
function formatRoleNVDA(node) {
|
|
1433
|
+
const role = node.role;
|
|
1434
|
+
switch (role) {
|
|
1435
|
+
case "button":
|
|
1436
|
+
return "button";
|
|
1437
|
+
case "link":
|
|
1438
|
+
return "link";
|
|
1439
|
+
case "heading":
|
|
1440
|
+
if (node.state.level) {
|
|
1441
|
+
return `heading level ${node.state.level}`;
|
|
1442
|
+
}
|
|
1443
|
+
return "heading";
|
|
1444
|
+
case "textbox":
|
|
1445
|
+
return "edit";
|
|
1446
|
+
case "checkbox":
|
|
1447
|
+
return "checkbox";
|
|
1448
|
+
case "radio":
|
|
1449
|
+
return "radio button";
|
|
1450
|
+
case "combobox":
|
|
1451
|
+
return "combo box";
|
|
1452
|
+
case "listbox":
|
|
1453
|
+
return "list box";
|
|
1454
|
+
case "option":
|
|
1455
|
+
return "option";
|
|
1456
|
+
case "list":
|
|
1457
|
+
return "list";
|
|
1458
|
+
case "listitem":
|
|
1459
|
+
return "list item";
|
|
1460
|
+
case "navigation":
|
|
1461
|
+
return "navigation landmark";
|
|
1462
|
+
case "main":
|
|
1463
|
+
return "main landmark";
|
|
1464
|
+
case "banner":
|
|
1465
|
+
return "banner landmark";
|
|
1466
|
+
case "contentinfo":
|
|
1467
|
+
return "content information landmark";
|
|
1468
|
+
case "region":
|
|
1469
|
+
return "region landmark";
|
|
1470
|
+
case "complementary":
|
|
1471
|
+
return "complementary landmark";
|
|
1472
|
+
case "form":
|
|
1473
|
+
return "form landmark";
|
|
1474
|
+
case "search":
|
|
1475
|
+
return "search landmark";
|
|
1476
|
+
case "img":
|
|
1477
|
+
return "graphic";
|
|
1478
|
+
case "article":
|
|
1479
|
+
return "article";
|
|
1480
|
+
case "generic":
|
|
1481
|
+
return "";
|
|
1482
|
+
case "staticText":
|
|
1483
|
+
case "paragraph":
|
|
1484
|
+
case "cell":
|
|
1485
|
+
case "term":
|
|
1486
|
+
case "definition":
|
|
1487
|
+
case "caption":
|
|
1488
|
+
return "";
|
|
1489
|
+
case "blockquote":
|
|
1490
|
+
return "block quote";
|
|
1491
|
+
case "code":
|
|
1492
|
+
return "code";
|
|
1493
|
+
case "table":
|
|
1494
|
+
return "table";
|
|
1495
|
+
case "row":
|
|
1496
|
+
return "row";
|
|
1497
|
+
case "columnheader":
|
|
1498
|
+
return "column header";
|
|
1499
|
+
case "rowheader":
|
|
1500
|
+
return "row header";
|
|
1501
|
+
case "figure":
|
|
1502
|
+
return "figure";
|
|
1503
|
+
case "dialog":
|
|
1504
|
+
return "dialog";
|
|
1505
|
+
case "meter":
|
|
1506
|
+
return "meter";
|
|
1507
|
+
case "progressbar":
|
|
1508
|
+
return "progress bar";
|
|
1509
|
+
case "status":
|
|
1510
|
+
return "status";
|
|
1511
|
+
case "group":
|
|
1512
|
+
return node.name ? "grouping" : "";
|
|
1513
|
+
case "document":
|
|
1514
|
+
return "document";
|
|
1515
|
+
case "application":
|
|
1516
|
+
return "embedded object";
|
|
1517
|
+
case "separator":
|
|
1518
|
+
return "separator";
|
|
1519
|
+
default:
|
|
1520
|
+
return role;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
function formatStatesNVDA(node) {
|
|
1524
|
+
const states = [];
|
|
1525
|
+
if (node.state.expanded !== void 0) {
|
|
1526
|
+
states.push(node.state.expanded ? "expanded" : "collapsed");
|
|
1527
|
+
}
|
|
1528
|
+
if (node.state.checked !== void 0) {
|
|
1529
|
+
if (node.state.checked === "mixed") {
|
|
1530
|
+
states.push("half checked");
|
|
1531
|
+
} else {
|
|
1532
|
+
states.push(node.state.checked ? "checked" : "not checked");
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (node.state.pressed !== void 0) {
|
|
1536
|
+
if (node.state.pressed === "mixed") {
|
|
1537
|
+
states.push("half pressed");
|
|
1538
|
+
} else {
|
|
1539
|
+
states.push(node.state.pressed ? "pressed" : "not pressed");
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (node.state.selected !== void 0) {
|
|
1543
|
+
states.push(node.state.selected ? "selected" : "not selected");
|
|
1544
|
+
}
|
|
1545
|
+
if (node.state.disabled) {
|
|
1546
|
+
states.push("unavailable");
|
|
1547
|
+
}
|
|
1548
|
+
if (node.state.invalid) {
|
|
1549
|
+
states.push("invalid entry");
|
|
1550
|
+
}
|
|
1551
|
+
if (node.state.required) {
|
|
1552
|
+
states.push("required");
|
|
1553
|
+
}
|
|
1554
|
+
if (node.state.readonly) {
|
|
1555
|
+
states.push("read only");
|
|
1556
|
+
}
|
|
1557
|
+
if (node.state.busy) {
|
|
1558
|
+
states.push("busy");
|
|
1559
|
+
}
|
|
1560
|
+
if (node.state.current) {
|
|
1561
|
+
if (node.state.current === "page") {
|
|
1562
|
+
states.push("current page");
|
|
1563
|
+
} else if (node.state.current === "step") {
|
|
1564
|
+
states.push("current step");
|
|
1565
|
+
} else if (node.state.current === "location") {
|
|
1566
|
+
states.push("current location");
|
|
1567
|
+
} else if (node.state.current === "date") {
|
|
1568
|
+
states.push("current date");
|
|
1569
|
+
} else if (node.state.current === "time") {
|
|
1570
|
+
states.push("current time");
|
|
1571
|
+
} else if (node.state.current === "true") {
|
|
1572
|
+
states.push("current");
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (node.state.grabbed !== void 0) {
|
|
1576
|
+
states.push(node.state.grabbed ? "grabbed" : "not grabbed");
|
|
1577
|
+
}
|
|
1578
|
+
return states.join(", ");
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/renderer/jaws-renderer.ts
|
|
1582
|
+
function renderJAWS(model, colorize) {
|
|
1583
|
+
const c = createColors(colorize ?? false);
|
|
1584
|
+
const announcements = [];
|
|
1585
|
+
renderNodeJAWS(model.root, announcements, c);
|
|
1586
|
+
return announcements.join("\n");
|
|
1587
|
+
}
|
|
1588
|
+
function renderNodeJAWS(node, announcements, c) {
|
|
1589
|
+
const announcement = formatNodeJAWS(node, c);
|
|
1590
|
+
if (announcement) {
|
|
1591
|
+
announcements.push(announcement);
|
|
1592
|
+
}
|
|
1593
|
+
for (const child of node.children) {
|
|
1594
|
+
renderNodeJAWS(child, announcements, c);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
function formatNodeJAWS(node, c) {
|
|
1598
|
+
const parts = [];
|
|
1599
|
+
if (node.name) {
|
|
1600
|
+
parts.push(c.elementName(node.name));
|
|
1601
|
+
}
|
|
1602
|
+
const roleText = formatRoleJAWS(node);
|
|
1603
|
+
if (roleText) {
|
|
1604
|
+
parts.push(c.roleName(roleText));
|
|
1605
|
+
}
|
|
1606
|
+
const stateText = formatStatesJAWS(node);
|
|
1607
|
+
if (stateText) {
|
|
1608
|
+
parts.push(c.stateName(stateText));
|
|
1609
|
+
}
|
|
1610
|
+
if (node.value) {
|
|
1611
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1612
|
+
if (valueText) {
|
|
1613
|
+
parts.push(valueText);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
if (node.description) {
|
|
1617
|
+
parts.push(c.description(node.description));
|
|
1618
|
+
}
|
|
1619
|
+
return parts.join(", ");
|
|
1620
|
+
}
|
|
1621
|
+
function formatRoleJAWS(node) {
|
|
1622
|
+
const role = node.role;
|
|
1623
|
+
switch (role) {
|
|
1624
|
+
case "button":
|
|
1625
|
+
return "button";
|
|
1626
|
+
case "link":
|
|
1627
|
+
return "clickable";
|
|
1628
|
+
case "heading":
|
|
1629
|
+
if (node.state.level) {
|
|
1630
|
+
return `heading ${node.state.level}`;
|
|
1631
|
+
}
|
|
1632
|
+
return "heading";
|
|
1633
|
+
case "textbox":
|
|
1634
|
+
return "edit";
|
|
1635
|
+
case "checkbox":
|
|
1636
|
+
return "check box";
|
|
1637
|
+
case "radio":
|
|
1638
|
+
return "radio button";
|
|
1639
|
+
case "combobox":
|
|
1640
|
+
return "combo box";
|
|
1641
|
+
case "listbox":
|
|
1642
|
+
return "list box";
|
|
1643
|
+
case "option":
|
|
1644
|
+
return "option";
|
|
1645
|
+
case "list":
|
|
1646
|
+
return "list";
|
|
1647
|
+
case "listitem":
|
|
1648
|
+
return "list item";
|
|
1649
|
+
case "navigation":
|
|
1650
|
+
return "navigation region";
|
|
1651
|
+
case "main":
|
|
1652
|
+
return "main region";
|
|
1653
|
+
case "banner":
|
|
1654
|
+
return "banner region";
|
|
1655
|
+
case "contentinfo":
|
|
1656
|
+
return "content information region";
|
|
1657
|
+
case "region":
|
|
1658
|
+
return "region";
|
|
1659
|
+
case "img":
|
|
1660
|
+
return "graphic";
|
|
1661
|
+
case "article":
|
|
1662
|
+
return "article";
|
|
1663
|
+
case "complementary":
|
|
1664
|
+
return "complementary region";
|
|
1665
|
+
case "form":
|
|
1666
|
+
return "form";
|
|
1667
|
+
case "search":
|
|
1668
|
+
return "search region";
|
|
1669
|
+
case "generic":
|
|
1670
|
+
return "";
|
|
1671
|
+
case "staticText":
|
|
1672
|
+
case "paragraph":
|
|
1673
|
+
case "term":
|
|
1674
|
+
case "definition":
|
|
1675
|
+
case "caption":
|
|
1676
|
+
return "";
|
|
1677
|
+
case "blockquote":
|
|
1678
|
+
return "block quote";
|
|
1679
|
+
case "code":
|
|
1680
|
+
return "code";
|
|
1681
|
+
case "table": {
|
|
1682
|
+
const rows = node.children.filter((c) => c.role === "row");
|
|
1683
|
+
const rowCount = rows.length;
|
|
1684
|
+
const colCount = rows.length > 0 ? rows[0].children.length : 0;
|
|
1685
|
+
return `table with ${rowCount} rows and ${colCount} columns`;
|
|
1686
|
+
}
|
|
1687
|
+
case "row": {
|
|
1688
|
+
const pos = node.state.posinset ?? 0;
|
|
1689
|
+
return `row ${pos}`;
|
|
1690
|
+
}
|
|
1691
|
+
case "cell": {
|
|
1692
|
+
const pos = node.state.posinset ?? 0;
|
|
1693
|
+
return `column ${pos}`;
|
|
1694
|
+
}
|
|
1695
|
+
case "columnheader":
|
|
1696
|
+
return "column header";
|
|
1697
|
+
case "rowheader":
|
|
1698
|
+
return "row header";
|
|
1699
|
+
case "figure":
|
|
1700
|
+
return "figure";
|
|
1701
|
+
case "dialog":
|
|
1702
|
+
return "dialog";
|
|
1703
|
+
case "meter":
|
|
1704
|
+
return "meter";
|
|
1705
|
+
case "progressbar":
|
|
1706
|
+
return "progress bar";
|
|
1707
|
+
case "status":
|
|
1708
|
+
return "status";
|
|
1709
|
+
case "group":
|
|
1710
|
+
return node.name ? "group" : "";
|
|
1711
|
+
case "document":
|
|
1712
|
+
return "frame";
|
|
1713
|
+
case "application":
|
|
1714
|
+
return "embedded object";
|
|
1715
|
+
case "separator":
|
|
1716
|
+
return "separator";
|
|
1717
|
+
default:
|
|
1718
|
+
return role;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
function formatStatesJAWS(node) {
|
|
1722
|
+
const states = [];
|
|
1723
|
+
if (node.state.expanded !== void 0) {
|
|
1724
|
+
states.push(node.state.expanded ? "expanded" : "collapsed");
|
|
1725
|
+
}
|
|
1726
|
+
if (node.state.checked !== void 0) {
|
|
1727
|
+
if (node.state.checked === "mixed") {
|
|
1728
|
+
states.push("partially checked");
|
|
1729
|
+
} else if (node.state.checked) {
|
|
1730
|
+
states.push("checked");
|
|
1731
|
+
} else {
|
|
1732
|
+
states.push("not checked");
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (node.state.pressed !== void 0) {
|
|
1736
|
+
if (node.state.pressed === "mixed") {
|
|
1737
|
+
states.push("partially pressed");
|
|
1738
|
+
} else if (node.state.pressed) {
|
|
1739
|
+
states.push("pressed");
|
|
1740
|
+
} else {
|
|
1741
|
+
states.push("not pressed");
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
if (node.state.selected !== void 0) {
|
|
1745
|
+
states.push(node.state.selected ? "selected" : "not selected");
|
|
1746
|
+
}
|
|
1747
|
+
if (node.state.disabled) {
|
|
1748
|
+
states.push("unavailable");
|
|
1749
|
+
}
|
|
1750
|
+
if (node.state.invalid) {
|
|
1751
|
+
states.push("invalid entry");
|
|
1752
|
+
}
|
|
1753
|
+
if (node.state.required) {
|
|
1754
|
+
states.push("required");
|
|
1755
|
+
}
|
|
1756
|
+
if (node.state.readonly) {
|
|
1757
|
+
states.push("read only");
|
|
1758
|
+
}
|
|
1759
|
+
if (node.state.busy) {
|
|
1760
|
+
states.push("busy");
|
|
1761
|
+
}
|
|
1762
|
+
if (node.state.current) {
|
|
1763
|
+
switch (node.state.current) {
|
|
1764
|
+
case "page":
|
|
1765
|
+
states.push("current page");
|
|
1766
|
+
break;
|
|
1767
|
+
case "step":
|
|
1768
|
+
states.push("current step");
|
|
1769
|
+
break;
|
|
1770
|
+
case "location":
|
|
1771
|
+
states.push("current location");
|
|
1772
|
+
break;
|
|
1773
|
+
case "date":
|
|
1774
|
+
states.push("current date");
|
|
1775
|
+
break;
|
|
1776
|
+
case "time":
|
|
1777
|
+
states.push("current time");
|
|
1778
|
+
break;
|
|
1779
|
+
case "true":
|
|
1780
|
+
states.push("current");
|
|
1781
|
+
break;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (node.state.grabbed !== void 0) {
|
|
1785
|
+
states.push(node.state.grabbed ? "grabbed" : "not grabbed");
|
|
1786
|
+
}
|
|
1787
|
+
return states.join(", ");
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// src/renderer/voiceover-renderer.ts
|
|
1791
|
+
function renderVoiceOver(model, colorize) {
|
|
1792
|
+
const c = createColors(colorize ?? false);
|
|
1793
|
+
const announcements = [];
|
|
1794
|
+
renderNodeVoiceOver(model.root, announcements, c);
|
|
1795
|
+
return announcements.join("\n");
|
|
1796
|
+
}
|
|
1797
|
+
function renderNodeVoiceOver(node, announcements, c) {
|
|
1798
|
+
const announcement = formatNodeVoiceOver(node, c);
|
|
1799
|
+
if (announcement) {
|
|
1800
|
+
announcements.push(announcement);
|
|
1801
|
+
}
|
|
1802
|
+
for (const child of node.children) {
|
|
1803
|
+
renderNodeVoiceOver(child, announcements, c);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function formatNodeVoiceOver(node, c) {
|
|
1807
|
+
const parts = [];
|
|
1808
|
+
const roleFirst = shouldAnnounceRoleFirst(node.role);
|
|
1809
|
+
if (roleFirst) {
|
|
1810
|
+
const roleText = formatRoleVoiceOver(node);
|
|
1811
|
+
if (roleText) {
|
|
1812
|
+
parts.push(c.roleName(roleText));
|
|
1813
|
+
}
|
|
1814
|
+
if (node.name) {
|
|
1815
|
+
parts.push(c.elementName(node.name));
|
|
1816
|
+
}
|
|
1817
|
+
} else {
|
|
1818
|
+
if (node.name) {
|
|
1819
|
+
parts.push(c.elementName(node.name));
|
|
1820
|
+
}
|
|
1821
|
+
const roleText = formatRoleVoiceOver(node);
|
|
1822
|
+
if (roleText) {
|
|
1823
|
+
parts.push(c.roleName(roleText));
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
const stateText = formatStatesVoiceOver(node);
|
|
1827
|
+
if (stateText) {
|
|
1828
|
+
parts.push(c.stateName(stateText));
|
|
1829
|
+
}
|
|
1830
|
+
if (node.value) {
|
|
1831
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1832
|
+
if (valueText) {
|
|
1833
|
+
parts.push(valueText);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
if (node.description) {
|
|
1837
|
+
parts.push(c.description(node.description));
|
|
1838
|
+
}
|
|
1839
|
+
return parts.join(", ");
|
|
1840
|
+
}
|
|
1841
|
+
function shouldAnnounceRoleFirst(role) {
|
|
1842
|
+
return role === "heading" || role === "navigation" || role === "main" || role === "banner" || role === "contentinfo" || role === "complementary" || role === "region" || role === "form" || role === "search" || role === "blockquote" || role === "figure" || role === "dialog" || role === "group" || role === "document";
|
|
1843
|
+
}
|
|
1844
|
+
function formatRoleVoiceOver(node) {
|
|
1845
|
+
const role = node.role;
|
|
1846
|
+
switch (role) {
|
|
1847
|
+
case "button":
|
|
1848
|
+
return "button";
|
|
1849
|
+
case "link":
|
|
1850
|
+
return "link";
|
|
1851
|
+
case "heading":
|
|
1852
|
+
if (node.state.level) {
|
|
1853
|
+
return `heading level ${node.state.level}`;
|
|
1854
|
+
}
|
|
1855
|
+
return "heading";
|
|
1856
|
+
case "textbox":
|
|
1857
|
+
return "edit text";
|
|
1858
|
+
case "checkbox":
|
|
1859
|
+
return "checkbox";
|
|
1860
|
+
case "radio":
|
|
1861
|
+
return "radio button";
|
|
1862
|
+
case "combobox":
|
|
1863
|
+
return "combo box";
|
|
1864
|
+
case "listbox":
|
|
1865
|
+
return "list box";
|
|
1866
|
+
case "option":
|
|
1867
|
+
return "option";
|
|
1868
|
+
case "list":
|
|
1869
|
+
return "list";
|
|
1870
|
+
case "listitem":
|
|
1871
|
+
return "item";
|
|
1872
|
+
case "navigation":
|
|
1873
|
+
return "navigation";
|
|
1874
|
+
case "main":
|
|
1875
|
+
return "main";
|
|
1876
|
+
case "banner":
|
|
1877
|
+
return "banner";
|
|
1878
|
+
case "contentinfo":
|
|
1879
|
+
return "content information";
|
|
1880
|
+
case "region":
|
|
1881
|
+
return "region";
|
|
1882
|
+
case "complementary":
|
|
1883
|
+
return "complementary";
|
|
1884
|
+
case "form":
|
|
1885
|
+
return "form";
|
|
1886
|
+
case "search":
|
|
1887
|
+
return "search";
|
|
1888
|
+
case "img":
|
|
1889
|
+
return "image";
|
|
1890
|
+
case "article":
|
|
1891
|
+
return "article";
|
|
1892
|
+
case "generic":
|
|
1893
|
+
return "";
|
|
1894
|
+
case "staticText":
|
|
1895
|
+
case "paragraph":
|
|
1896
|
+
case "cell":
|
|
1897
|
+
case "term":
|
|
1898
|
+
case "definition":
|
|
1899
|
+
case "caption":
|
|
1900
|
+
return "";
|
|
1901
|
+
case "blockquote":
|
|
1902
|
+
return "blockquote";
|
|
1903
|
+
case "code":
|
|
1904
|
+
return "code";
|
|
1905
|
+
case "table": {
|
|
1906
|
+
const rows = node.children.filter((c) => c.role === "row");
|
|
1907
|
+
const rowCount = rows.length;
|
|
1908
|
+
const colCount = rows.length > 0 ? rows[0].children.length : 0;
|
|
1909
|
+
return `table, ${rowCount} rows, ${colCount} columns`;
|
|
1910
|
+
}
|
|
1911
|
+
case "row":
|
|
1912
|
+
return "row";
|
|
1913
|
+
case "columnheader":
|
|
1914
|
+
return "column header";
|
|
1915
|
+
case "rowheader":
|
|
1916
|
+
return "row header";
|
|
1917
|
+
case "figure":
|
|
1918
|
+
return "figure";
|
|
1919
|
+
case "dialog":
|
|
1920
|
+
return "web dialog";
|
|
1921
|
+
case "meter":
|
|
1922
|
+
return "level indicator";
|
|
1923
|
+
case "progressbar":
|
|
1924
|
+
return "progress indicator";
|
|
1925
|
+
case "status":
|
|
1926
|
+
return "status";
|
|
1927
|
+
case "group":
|
|
1928
|
+
return node.name ? "group" : "";
|
|
1929
|
+
case "document":
|
|
1930
|
+
return "frame";
|
|
1931
|
+
case "application":
|
|
1932
|
+
return "embedded object";
|
|
1933
|
+
case "separator":
|
|
1934
|
+
return "separator";
|
|
1935
|
+
default:
|
|
1936
|
+
return role;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
function formatStatesVoiceOver(node) {
|
|
1940
|
+
const states = [];
|
|
1941
|
+
if (node.state.expanded !== void 0) {
|
|
1942
|
+
states.push(node.state.expanded ? "expanded" : "collapsed");
|
|
1943
|
+
}
|
|
1944
|
+
if (node.state.checked !== void 0) {
|
|
1945
|
+
if (node.state.checked === "mixed") {
|
|
1946
|
+
states.push("mixed");
|
|
1947
|
+
} else {
|
|
1948
|
+
states.push(node.state.checked ? "checked" : "unchecked");
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
if (node.state.pressed !== void 0) {
|
|
1952
|
+
if (node.state.pressed === "mixed") {
|
|
1953
|
+
states.push("mixed");
|
|
1954
|
+
} else {
|
|
1955
|
+
states.push(node.state.pressed ? "pressed" : "not pressed");
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (node.state.selected !== void 0) {
|
|
1959
|
+
states.push(node.state.selected ? "selected" : "unselected");
|
|
1960
|
+
}
|
|
1961
|
+
if (node.state.disabled) {
|
|
1962
|
+
states.push("dimmed");
|
|
1963
|
+
}
|
|
1964
|
+
if (node.state.invalid) {
|
|
1965
|
+
states.push("invalid data");
|
|
1966
|
+
}
|
|
1967
|
+
if (node.state.required) {
|
|
1968
|
+
states.push("required");
|
|
1969
|
+
}
|
|
1970
|
+
if (node.state.readonly) {
|
|
1971
|
+
states.push("read only");
|
|
1972
|
+
}
|
|
1973
|
+
if (node.state.busy) {
|
|
1974
|
+
states.push("busy");
|
|
1975
|
+
}
|
|
1976
|
+
if (node.state.current) {
|
|
1977
|
+
if (node.state.current === "page") {
|
|
1978
|
+
states.push("current page");
|
|
1979
|
+
} else if (node.state.current === "step") {
|
|
1980
|
+
states.push("current step");
|
|
1981
|
+
} else if (node.state.current === "location") {
|
|
1982
|
+
states.push("current location");
|
|
1983
|
+
} else if (node.state.current === "date") {
|
|
1984
|
+
states.push("current date");
|
|
1985
|
+
} else if (node.state.current === "time") {
|
|
1986
|
+
states.push("current time");
|
|
1987
|
+
} else if (node.state.current === "true") {
|
|
1988
|
+
states.push("current");
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
if (node.state.grabbed !== void 0) {
|
|
1992
|
+
states.push(node.state.grabbed ? "grabbed" : "not grabbed");
|
|
1993
|
+
}
|
|
1994
|
+
return states.join(", ");
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// src/renderer/audit-renderer.ts
|
|
1998
|
+
function renderAuditReport(model, colorize) {
|
|
1999
|
+
const report = generateAuditReport(model);
|
|
2000
|
+
return formatAuditReport(report, model, colorize);
|
|
2001
|
+
}
|
|
2002
|
+
function generateAuditReport(model) {
|
|
2003
|
+
const statistics = {
|
|
2004
|
+
totalElements: 0,
|
|
2005
|
+
roleDistribution: {},
|
|
2006
|
+
landmarkCount: 0,
|
|
2007
|
+
headingCount: 0,
|
|
2008
|
+
interactiveCount: 0,
|
|
2009
|
+
focusableCount: 0,
|
|
2010
|
+
statesUsed: /* @__PURE__ */ new Set()
|
|
2011
|
+
};
|
|
2012
|
+
const landmarks = [];
|
|
2013
|
+
const headings = [];
|
|
2014
|
+
const interactiveElements = [];
|
|
2015
|
+
const issues = [];
|
|
2016
|
+
collectAuditData(model.root, statistics, landmarks, headings, interactiveElements);
|
|
2017
|
+
detectLandmarkIssues(landmarks, issues);
|
|
2018
|
+
detectHeadingIssues(headings, issues);
|
|
2019
|
+
detectInteractiveIssues(interactiveElements, issues);
|
|
2020
|
+
return {
|
|
2021
|
+
statistics,
|
|
2022
|
+
landmarks,
|
|
2023
|
+
headings,
|
|
2024
|
+
interactiveElements,
|
|
2025
|
+
issues
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
function collectAuditData(node, statistics, landmarks, headings, interactiveElements) {
|
|
2029
|
+
if (node.role !== "generic") {
|
|
2030
|
+
statistics.totalElements++;
|
|
2031
|
+
statistics.roleDistribution[node.role] = (statistics.roleDistribution[node.role] || 0) + 1;
|
|
2032
|
+
if (isLandmark(node.role)) {
|
|
2033
|
+
statistics.landmarkCount++;
|
|
2034
|
+
landmarks.push(node);
|
|
2035
|
+
}
|
|
2036
|
+
if (node.role === "heading") {
|
|
2037
|
+
statistics.headingCount++;
|
|
2038
|
+
headings.push(node);
|
|
2039
|
+
}
|
|
2040
|
+
if (isInteractive(node.role)) {
|
|
2041
|
+
statistics.interactiveCount++;
|
|
2042
|
+
interactiveElements.push(node);
|
|
2043
|
+
}
|
|
2044
|
+
if (node.focus.focusable) {
|
|
2045
|
+
statistics.focusableCount++;
|
|
2046
|
+
}
|
|
2047
|
+
Object.keys(node.state).forEach((state) => {
|
|
2048
|
+
if (node.state[state] !== void 0) {
|
|
2049
|
+
statistics.statesUsed.add(state);
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
for (const child of node.children) {
|
|
2054
|
+
collectAuditData(child, statistics, landmarks, headings, interactiveElements);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
function isLandmark(role) {
|
|
2058
|
+
return [
|
|
2059
|
+
"navigation",
|
|
2060
|
+
"main",
|
|
2061
|
+
"banner",
|
|
2062
|
+
"contentinfo",
|
|
2063
|
+
"region",
|
|
2064
|
+
"complementary",
|
|
2065
|
+
"form",
|
|
2066
|
+
"search"
|
|
2067
|
+
].includes(role);
|
|
2068
|
+
}
|
|
2069
|
+
function isInteractive(role) {
|
|
2070
|
+
return [
|
|
2071
|
+
"button",
|
|
2072
|
+
"link",
|
|
2073
|
+
"textbox",
|
|
2074
|
+
"checkbox",
|
|
2075
|
+
"radio",
|
|
2076
|
+
"combobox",
|
|
2077
|
+
"listbox",
|
|
2078
|
+
"option"
|
|
2079
|
+
].includes(role);
|
|
2080
|
+
}
|
|
2081
|
+
function detectLandmarkIssues(landmarks, issues) {
|
|
2082
|
+
if (landmarks.length === 0) {
|
|
2083
|
+
issues.push({
|
|
2084
|
+
severity: "info",
|
|
2085
|
+
message: "No landmarks found",
|
|
2086
|
+
suggestion: "Consider adding semantic landmarks (main, nav, aside, etc.)"
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
landmarks.forEach((landmark) => {
|
|
2090
|
+
if (!landmark.name && ["navigation", "region", "complementary", "form"].includes(landmark.role)) {
|
|
2091
|
+
issues.push({
|
|
2092
|
+
severity: "warning",
|
|
2093
|
+
message: `${landmark.role} landmark has no accessible name`,
|
|
2094
|
+
suggestion: `Add aria-label="${landmark.role === "navigation" ? "Main navigation" : "Descriptive name"}" to the ${landmark.role} element`,
|
|
2095
|
+
element: {
|
|
2096
|
+
role: landmark.role,
|
|
2097
|
+
name: landmark.name
|
|
2098
|
+
}
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
const landmarksByRole = /* @__PURE__ */ new Map();
|
|
2103
|
+
landmarks.forEach((landmark) => {
|
|
2104
|
+
if (!landmarksByRole.has(landmark.role)) {
|
|
2105
|
+
landmarksByRole.set(landmark.role, []);
|
|
2106
|
+
}
|
|
2107
|
+
landmarksByRole.get(landmark.role).push(landmark);
|
|
2108
|
+
});
|
|
2109
|
+
landmarksByRole.forEach((nodes, role) => {
|
|
2110
|
+
if (nodes.length > 1) {
|
|
2111
|
+
const allUnnamed = nodes.every((n) => !n.name);
|
|
2112
|
+
const allSameName = nodes.every((n) => n.name === nodes[0].name);
|
|
2113
|
+
if (allUnnamed || allSameName) {
|
|
2114
|
+
issues.push({
|
|
2115
|
+
severity: "warning",
|
|
2116
|
+
message: `Multiple ${role} landmarks without distinguishing names`,
|
|
2117
|
+
suggestion: `Add unique aria-label attributes to distinguish between ${role} landmarks`
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
function detectHeadingIssues(headings, issues) {
|
|
2124
|
+
if (headings.length === 0) {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
const firstHeading = headings[0];
|
|
2128
|
+
if (firstHeading.state.level && firstHeading.state.level !== 1) {
|
|
2129
|
+
issues.push({
|
|
2130
|
+
severity: "error",
|
|
2131
|
+
message: `First heading is h${firstHeading.state.level} (should be h1)`,
|
|
2132
|
+
suggestion: `Change <h${firstHeading.state.level}> to <h1> or add h1 before it`,
|
|
2133
|
+
element: {
|
|
2134
|
+
role: firstHeading.role,
|
|
2135
|
+
name: firstHeading.name
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
for (let i = 1; i < headings.length; i++) {
|
|
2140
|
+
const prevLevel = headings[i - 1].state.level || 1;
|
|
2141
|
+
const currLevel = headings[i].state.level || 1;
|
|
2142
|
+
if (currLevel > prevLevel + 1) {
|
|
2143
|
+
issues.push({
|
|
2144
|
+
severity: "error",
|
|
2145
|
+
message: `Heading hierarchy violation: h${prevLevel} followed by h${currLevel} (skipped h${prevLevel + 1})`,
|
|
2146
|
+
suggestion: `Change h${currLevel} to h${prevLevel + 1} or add intermediate heading levels`,
|
|
2147
|
+
element: {
|
|
2148
|
+
role: headings[i].role,
|
|
2149
|
+
name: headings[i].name
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
function detectInteractiveIssues(elements, issues) {
|
|
2156
|
+
elements.forEach((element) => {
|
|
2157
|
+
if (!element.name) {
|
|
2158
|
+
issues.push({
|
|
2159
|
+
severity: "error",
|
|
2160
|
+
message: `${element.role} has no accessible name`,
|
|
2161
|
+
suggestion: element.role === "button" ? "Add text content or aria-label to button" : element.role === "link" ? "Add text content or aria-label to link" : `Add aria-label to ${element.role}`,
|
|
2162
|
+
element: {
|
|
2163
|
+
role: element.role,
|
|
2164
|
+
name: element.name
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
if (element.role === "textbox" && !element.name) {
|
|
2169
|
+
issues.push({
|
|
2170
|
+
severity: "error",
|
|
2171
|
+
message: "Input has no associated label",
|
|
2172
|
+
suggestion: "Add <label> element or aria-label attribute",
|
|
2173
|
+
element: {
|
|
2174
|
+
role: element.role,
|
|
2175
|
+
name: element.name
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
function formatAuditReport(report, model, colorize) {
|
|
2182
|
+
const c = createColors(colorize ?? false);
|
|
2183
|
+
const lines = [];
|
|
2184
|
+
lines.push(c.title("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
2185
|
+
lines.push(c.title("\u2551 ACCESSIBILITY AUDIT REPORT \u2551"));
|
|
2186
|
+
lines.push(c.title("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
2187
|
+
lines.push("");
|
|
2188
|
+
lines.push(c.dim(`Analyzed: ${model.metadata.extractedAt}`));
|
|
2189
|
+
lines.push("");
|
|
2190
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2191
|
+
lines.push(c.heading("\u{1F4CD} LANDMARK STRUCTURE"));
|
|
2192
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2193
|
+
lines.push("");
|
|
2194
|
+
if (report.landmarks.length === 0) {
|
|
2195
|
+
lines.push(c.error("\u2717 No landmarks found"));
|
|
2196
|
+
} else {
|
|
2197
|
+
lines.push(c.success(`\u2713 ${report.landmarks.length} landmark(s) found`));
|
|
2198
|
+
lines.push("");
|
|
2199
|
+
report.landmarks.forEach((landmark) => {
|
|
2200
|
+
const name = landmark.name ? `"${landmark.name}"` : "(unnamed)";
|
|
2201
|
+
lines.push(`${landmark.role} ${name}`);
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
lines.push("");
|
|
2205
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2206
|
+
lines.push(c.heading("\u{1F4D1} HEADING HIERARCHY"));
|
|
2207
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2208
|
+
lines.push("");
|
|
2209
|
+
if (report.headings.length === 0) {
|
|
2210
|
+
lines.push(c.info("\u2139 No headings found"));
|
|
2211
|
+
} else {
|
|
2212
|
+
const hasHierarchyIssues = report.issues.some(
|
|
2213
|
+
(i) => i.message.includes("heading") || i.message.includes("h1") || i.message.includes("h2")
|
|
2214
|
+
);
|
|
2215
|
+
lines.push(`${hasHierarchyIssues ? c.error("\u2717") : c.success("\u2713")} ${report.headings.length} heading(s) found ${hasHierarchyIssues ? "(HIERARCHY VIOLATION)" : "(proper hierarchy)"}`);
|
|
2216
|
+
lines.push("");
|
|
2217
|
+
report.headings.forEach((heading) => {
|
|
2218
|
+
const level = heading.state.level || 1;
|
|
2219
|
+
const indent = " ".repeat(level - 1);
|
|
2220
|
+
lines.push(`${indent}h${level} "${heading.name}"`);
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
lines.push("");
|
|
2224
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2225
|
+
lines.push(c.heading("\u{1F3AF} INTERACTIVE ELEMENTS"));
|
|
2226
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2227
|
+
lines.push("");
|
|
2228
|
+
if (report.interactiveElements.length === 0) {
|
|
2229
|
+
lines.push(c.info("\u2139 No interactive elements found"));
|
|
2230
|
+
} else {
|
|
2231
|
+
lines.push(c.success(`\u2713 ${report.interactiveElements.length} interactive element(s) found`));
|
|
2232
|
+
lines.push("");
|
|
2233
|
+
const byRole = /* @__PURE__ */ new Map();
|
|
2234
|
+
report.interactiveElements.forEach((el) => {
|
|
2235
|
+
if (!byRole.has(el.role)) {
|
|
2236
|
+
byRole.set(el.role, []);
|
|
2237
|
+
}
|
|
2238
|
+
byRole.get(el.role).push(el);
|
|
2239
|
+
});
|
|
2240
|
+
byRole.forEach((elements, role) => {
|
|
2241
|
+
lines.push(`${role}s (${elements.length}):`);
|
|
2242
|
+
elements.forEach((el) => {
|
|
2243
|
+
const name = el.name ? `"${el.name}"` : "(unnamed)";
|
|
2244
|
+
const states = Object.keys(el.state).filter((k) => el.state[k] !== void 0);
|
|
2245
|
+
const stateStr = states.length > 0 ? ` [${states.join(", ")}]` : "";
|
|
2246
|
+
lines.push(` \u2022 ${name}${stateStr}`);
|
|
2247
|
+
});
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
lines.push("");
|
|
2251
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2252
|
+
lines.push(c.heading("\u{1F4CA} SUMMARY STATISTICS"));
|
|
2253
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2254
|
+
lines.push("");
|
|
2255
|
+
lines.push(`Total accessible elements: ${report.statistics.totalElements}`);
|
|
2256
|
+
lines.push("");
|
|
2257
|
+
lines.push("Role distribution:");
|
|
2258
|
+
Object.entries(report.statistics.roleDistribution).sort((a, b) => b[1] - a[1]).forEach(([role, count]) => {
|
|
2259
|
+
lines.push(c.dim(` \u2022 ${role}: ${count}`));
|
|
2260
|
+
});
|
|
2261
|
+
lines.push("");
|
|
2262
|
+
lines.push(c.dim(`Focusable elements: ${report.statistics.focusableCount}`));
|
|
2263
|
+
lines.push("");
|
|
2264
|
+
if (report.issues.length > 0) {
|
|
2265
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2266
|
+
lines.push(c.heading("\u26A0\uFE0F ACCESSIBILITY ISSUES"));
|
|
2267
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2268
|
+
lines.push("");
|
|
2269
|
+
const errors = report.issues.filter((i) => i.severity === "error");
|
|
2270
|
+
const warnings = report.issues.filter((i) => i.severity === "warning");
|
|
2271
|
+
const infos = report.issues.filter((i) => i.severity === "info");
|
|
2272
|
+
if (errors.length > 0) {
|
|
2273
|
+
lines.push(c.error(`\u2717 Error (${errors.length}):`));
|
|
2274
|
+
errors.forEach((issue) => {
|
|
2275
|
+
lines.push(c.error(` \u2022 ${issue.message}`));
|
|
2276
|
+
if (issue.suggestion) {
|
|
2277
|
+
lines.push(` \u2192 ${issue.suggestion}`);
|
|
2278
|
+
}
|
|
2279
|
+
});
|
|
2280
|
+
lines.push("");
|
|
2281
|
+
}
|
|
2282
|
+
if (warnings.length > 0) {
|
|
2283
|
+
lines.push(c.warning(`\u26A0 Warning (${warnings.length}):`));
|
|
2284
|
+
warnings.forEach((issue) => {
|
|
2285
|
+
lines.push(c.warning(` \u2022 ${issue.message}`));
|
|
2286
|
+
if (issue.suggestion) {
|
|
2287
|
+
lines.push(` \u2192 ${issue.suggestion}`);
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
lines.push("");
|
|
2291
|
+
}
|
|
2292
|
+
if (infos.length > 0) {
|
|
2293
|
+
lines.push(c.info(`\u2139 Info (${infos.length}):`));
|
|
2294
|
+
infos.forEach((issue) => {
|
|
2295
|
+
lines.push(c.info(` \u2022 ${issue.message}`));
|
|
2296
|
+
if (issue.suggestion) {
|
|
2297
|
+
lines.push(` \u2192 ${issue.suggestion}`);
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2300
|
+
lines.push("");
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2304
|
+
lines.push(c.success("\u2705 OVERALL ASSESSMENT"));
|
|
2305
|
+
lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
2306
|
+
lines.push("");
|
|
2307
|
+
const errorCount = report.issues.filter((i) => i.severity === "error").length;
|
|
2308
|
+
const warningCount = report.issues.filter((i) => i.severity === "warning").length;
|
|
2309
|
+
if (errorCount === 0 && warningCount === 0) {
|
|
2310
|
+
lines.push(c.success("Excellent! No critical issues found."));
|
|
2311
|
+
} else if (errorCount > 0) {
|
|
2312
|
+
lines.push(`Critical accessibility issues found:`);
|
|
2313
|
+
lines.push(c.error(` \u2717 ${errorCount} error(s)`));
|
|
2314
|
+
if (warningCount > 0) {
|
|
2315
|
+
lines.push(c.warning(` \u26A0 ${warningCount} warning(s)`));
|
|
2316
|
+
}
|
|
2317
|
+
lines.push("");
|
|
2318
|
+
lines.push("Recommendation: Address errors before deployment");
|
|
2319
|
+
} else {
|
|
2320
|
+
lines.push(`Good structure with minor improvements needed:`);
|
|
2321
|
+
lines.push(c.warning(` \u26A0 ${warningCount} warning(s)`));
|
|
2322
|
+
}
|
|
2323
|
+
return lines.join("\n");
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// src/diff/diff-algorithm.ts
|
|
2327
|
+
function diffAccessibilityTrees(oldTree, newTree) {
|
|
2328
|
+
const changes = [];
|
|
2329
|
+
const oldNodes = buildNodeMap(oldTree, "root");
|
|
2330
|
+
const newNodes = buildNodeMap(newTree, "root");
|
|
2331
|
+
for (const [path, node] of oldNodes) {
|
|
2332
|
+
if (!newNodes.has(path)) {
|
|
2333
|
+
changes.push({
|
|
2334
|
+
type: "removed",
|
|
2335
|
+
path,
|
|
2336
|
+
node
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
for (const [path, node] of newNodes) {
|
|
2341
|
+
if (!oldNodes.has(path)) {
|
|
2342
|
+
changes.push({
|
|
2343
|
+
type: "added",
|
|
2344
|
+
path,
|
|
2345
|
+
node
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
for (const [path, oldNode] of oldNodes) {
|
|
2350
|
+
const newNode = newNodes.get(path);
|
|
2351
|
+
if (newNode) {
|
|
2352
|
+
const propertyChanges = compareNodes(oldNode, newNode);
|
|
2353
|
+
if (propertyChanges.length > 0) {
|
|
2354
|
+
changes.push({
|
|
2355
|
+
type: "changed",
|
|
2356
|
+
path,
|
|
2357
|
+
changes: propertyChanges
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
changes.sort((a, b) => a.path.localeCompare(b.path));
|
|
2363
|
+
const summary = {
|
|
2364
|
+
added: changes.filter((c) => c.type === "added").length,
|
|
2365
|
+
removed: changes.filter((c) => c.type === "removed").length,
|
|
2366
|
+
changed: changes.filter((c) => c.type === "changed").length,
|
|
2367
|
+
total: changes.length
|
|
2368
|
+
};
|
|
2369
|
+
return { changes, summary };
|
|
2370
|
+
}
|
|
2371
|
+
function buildNodeMap(node, path) {
|
|
2372
|
+
const map = /* @__PURE__ */ new Map();
|
|
2373
|
+
map.set(path, node);
|
|
2374
|
+
node.children.forEach((child, index) => {
|
|
2375
|
+
const childPath = `${path}.children[${index}]`;
|
|
2376
|
+
const childMap = buildNodeMap(child, childPath);
|
|
2377
|
+
for (const [childPath2, childNode] of childMap) {
|
|
2378
|
+
map.set(childPath2, childNode);
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
return map;
|
|
2382
|
+
}
|
|
2383
|
+
function compareNodes(oldNode, newNode) {
|
|
2384
|
+
const changes = [];
|
|
2385
|
+
if (oldNode.role !== newNode.role) {
|
|
2386
|
+
changes.push({
|
|
2387
|
+
property: "role",
|
|
2388
|
+
oldValue: oldNode.role,
|
|
2389
|
+
newValue: newNode.role
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
if (oldNode.name !== newNode.name) {
|
|
2393
|
+
changes.push({
|
|
2394
|
+
property: "name",
|
|
2395
|
+
oldValue: oldNode.name,
|
|
2396
|
+
newValue: newNode.name
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
if (oldNode.description !== newNode.description) {
|
|
2400
|
+
changes.push({
|
|
2401
|
+
property: "description",
|
|
2402
|
+
oldValue: oldNode.description,
|
|
2403
|
+
newValue: newNode.description
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
if (!valuesEqual(oldNode.value, newNode.value)) {
|
|
2407
|
+
changes.push({
|
|
2408
|
+
property: "value",
|
|
2409
|
+
oldValue: oldNode.value,
|
|
2410
|
+
newValue: newNode.value
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
if (!statesEqual(oldNode.state, newNode.state)) {
|
|
2414
|
+
changes.push({
|
|
2415
|
+
property: "state",
|
|
2416
|
+
oldValue: oldNode.state,
|
|
2417
|
+
newValue: newNode.state
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
if (!focusEqual(oldNode.focus, newNode.focus)) {
|
|
2421
|
+
changes.push({
|
|
2422
|
+
property: "focus",
|
|
2423
|
+
oldValue: oldNode.focus,
|
|
2424
|
+
newValue: newNode.focus
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
return changes;
|
|
2428
|
+
}
|
|
2429
|
+
function valuesEqual(a, b) {
|
|
2430
|
+
if (a === void 0 && b === void 0) return true;
|
|
2431
|
+
if (a === void 0 || b === void 0) return false;
|
|
2432
|
+
return a.current === b.current && a.min === b.min && a.max === b.max && a.text === b.text;
|
|
2433
|
+
}
|
|
2434
|
+
function statesEqual(a, b) {
|
|
2435
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
2436
|
+
for (const key of keys) {
|
|
2437
|
+
const aValue = a[key];
|
|
2438
|
+
const bValue = b[key];
|
|
2439
|
+
if (aValue !== bValue) {
|
|
2440
|
+
return false;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return true;
|
|
2444
|
+
}
|
|
2445
|
+
function focusEqual(a, b) {
|
|
2446
|
+
return a.focusable === b.focusable && a.tabindex === b.tabindex;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// src/diff/formatter.ts
|
|
2450
|
+
function formatDiffAsJSON(diff) {
|
|
2451
|
+
return JSON.stringify(diff, null, 2);
|
|
2452
|
+
}
|
|
2453
|
+
function formatDiffAsText(diff) {
|
|
2454
|
+
const lines = [];
|
|
2455
|
+
lines.push("=== Accessibility Tree Diff ===");
|
|
2456
|
+
lines.push("");
|
|
2457
|
+
lines.push(`Total changes: ${diff.summary.total}`);
|
|
2458
|
+
lines.push(` Added: ${diff.summary.added}`);
|
|
2459
|
+
lines.push(` Removed: ${diff.summary.removed}`);
|
|
2460
|
+
lines.push(` Changed: ${diff.summary.changed}`);
|
|
2461
|
+
lines.push("");
|
|
2462
|
+
if (diff.changes.length === 0) {
|
|
2463
|
+
lines.push("No changes detected.");
|
|
2464
|
+
return lines.join("\n");
|
|
2465
|
+
}
|
|
2466
|
+
const added = diff.changes.filter((c) => c.type === "added");
|
|
2467
|
+
const removed = diff.changes.filter((c) => c.type === "removed");
|
|
2468
|
+
const changed = diff.changes.filter((c) => c.type === "changed");
|
|
2469
|
+
if (added.length > 0) {
|
|
2470
|
+
lines.push("--- Added Nodes ---");
|
|
2471
|
+
for (const change of added) {
|
|
2472
|
+
lines.push(`+ ${change.path}`);
|
|
2473
|
+
lines.push(` Role: ${change.node?.role}`);
|
|
2474
|
+
lines.push(` Name: "${change.node?.name}"`);
|
|
2475
|
+
if (change.node?.description) {
|
|
2476
|
+
lines.push(` Description: "${change.node.description}"`);
|
|
2477
|
+
}
|
|
2478
|
+
lines.push("");
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
if (removed.length > 0) {
|
|
2482
|
+
lines.push("--- Removed Nodes ---");
|
|
2483
|
+
for (const change of removed) {
|
|
2484
|
+
lines.push(`- ${change.path}`);
|
|
2485
|
+
lines.push(` Role: ${change.node?.role}`);
|
|
2486
|
+
lines.push(` Name: "${change.node?.name}"`);
|
|
2487
|
+
if (change.node?.description) {
|
|
2488
|
+
lines.push(` Description: "${change.node.description}"`);
|
|
2489
|
+
}
|
|
2490
|
+
lines.push("");
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
if (changed.length > 0) {
|
|
2494
|
+
lines.push("--- Changed Nodes ---");
|
|
2495
|
+
for (const change of changed) {
|
|
2496
|
+
lines.push(`~ ${change.path}`);
|
|
2497
|
+
if (change.changes) {
|
|
2498
|
+
for (const propChange of change.changes) {
|
|
2499
|
+
lines.push(` ${formatPropertyChange(propChange)}`);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
lines.push("");
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return lines.join("\n");
|
|
2506
|
+
}
|
|
2507
|
+
function formatPropertyChange(change) {
|
|
2508
|
+
const oldVal = formatValue(change.oldValue);
|
|
2509
|
+
const newVal = formatValue(change.newValue);
|
|
2510
|
+
return `${change.property}: ${oldVal} \u2192 ${newVal}`;
|
|
2511
|
+
}
|
|
2512
|
+
function formatValue(value) {
|
|
2513
|
+
if (value === void 0) return "undefined";
|
|
2514
|
+
if (value === null) return "null";
|
|
2515
|
+
if (typeof value === "string") return `"${value}"`;
|
|
2516
|
+
if (typeof value === "object") {
|
|
2517
|
+
const json = JSON.stringify(value);
|
|
2518
|
+
if (json.length > 50) {
|
|
2519
|
+
return json.substring(0, 47) + "...";
|
|
2520
|
+
}
|
|
2521
|
+
return json;
|
|
2522
|
+
}
|
|
2523
|
+
return String(value);
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// src/cli/orchestrator.ts
|
|
2527
|
+
function processHTML(html, options, diffHTML) {
|
|
2528
|
+
const warnings = [];
|
|
2529
|
+
const colorize = isColorEnabled(process.stdout) && options.format !== "json" && !options.output;
|
|
2530
|
+
try {
|
|
2531
|
+
if (options.diff && diffHTML) {
|
|
2532
|
+
return processDiff(html, diffHTML, options, warnings, colorize);
|
|
2533
|
+
}
|
|
2534
|
+
if (options.validate) {
|
|
2535
|
+
return processValidation(html, options, warnings);
|
|
2536
|
+
}
|
|
2537
|
+
return processNormal(html, options, warnings, colorize);
|
|
2538
|
+
} catch (error) {
|
|
2539
|
+
if (error instanceof Error) {
|
|
2540
|
+
return {
|
|
2541
|
+
output: `Error: ${error.message}`,
|
|
2542
|
+
exitCode: 2,
|
|
2543
|
+
// Content error
|
|
2544
|
+
warnings
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
throw error;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
function processNormal(html, options, warnings, colorize) {
|
|
2551
|
+
const doc = parseHTML(html);
|
|
2552
|
+
if (doc.warnings.length > 0) {
|
|
2553
|
+
warnings.push(...doc.warnings.map((w) => w.message));
|
|
2554
|
+
}
|
|
2555
|
+
let model;
|
|
2556
|
+
if (options.selector) {
|
|
2557
|
+
const results = buildAccessibilityTreeWithSelector(doc.document.body, options.selector);
|
|
2558
|
+
if (results.length === 0) {
|
|
2559
|
+
return {
|
|
2560
|
+
output: `Error: No elements match selector: ${options.selector}`,
|
|
2561
|
+
exitCode: 2,
|
|
2562
|
+
warnings
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
model = results[0].model;
|
|
2566
|
+
for (const result of results) {
|
|
2567
|
+
if (result.warnings.length > 0) {
|
|
2568
|
+
warnings.push(...result.warnings.map((w) => w.message));
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
} else {
|
|
2572
|
+
const result = buildAccessibilityTree(doc.document.body);
|
|
2573
|
+
model = result.model;
|
|
2574
|
+
if (result.warnings.length > 0) {
|
|
2575
|
+
warnings.push(...result.warnings.map((w) => w.message));
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
const output = formatOutput(model, options, colorize);
|
|
2579
|
+
return {
|
|
2580
|
+
output,
|
|
2581
|
+
exitCode: 0,
|
|
2582
|
+
warnings
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
function processDiff(html1, html2, options, warnings, _colorize) {
|
|
2586
|
+
const doc1 = parseHTML(html1);
|
|
2587
|
+
const doc2 = parseHTML(html2);
|
|
2588
|
+
warnings.push(...doc1.warnings.map((w) => w.message), ...doc2.warnings.map((w) => w.message));
|
|
2589
|
+
const result1 = buildAccessibilityTree(doc1.document.body);
|
|
2590
|
+
const result2 = buildAccessibilityTree(doc2.document.body);
|
|
2591
|
+
warnings.push(...result1.warnings.map((w) => w.message), ...result2.warnings.map((w) => w.message));
|
|
2592
|
+
const diff = diffAccessibilityTrees(result1.model.root, result2.model.root);
|
|
2593
|
+
let output;
|
|
2594
|
+
if (options.format === "json") {
|
|
2595
|
+
output = formatDiffAsJSON(diff);
|
|
2596
|
+
} else {
|
|
2597
|
+
output = formatDiffAsText(diff);
|
|
2598
|
+
}
|
|
2599
|
+
return {
|
|
2600
|
+
output,
|
|
2601
|
+
exitCode: 0,
|
|
2602
|
+
warnings
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
function processValidation(html, _options, warnings) {
|
|
2606
|
+
const doc = parseHTML(html);
|
|
2607
|
+
warnings.push(...doc.warnings.map((w) => w.message));
|
|
2608
|
+
const result = buildAccessibilityTree(doc.document.body);
|
|
2609
|
+
warnings.push(...result.warnings.map((w) => w.message));
|
|
2610
|
+
const model = result.model;
|
|
2611
|
+
const serialized = serializeModel(model);
|
|
2612
|
+
const deserialized = deserializeModel(serialized);
|
|
2613
|
+
const isEqual = modelsEqual(model, deserialized);
|
|
2614
|
+
if (isEqual) {
|
|
2615
|
+
return {
|
|
2616
|
+
output: "Validation passed: Model serialization is consistent",
|
|
2617
|
+
exitCode: 0,
|
|
2618
|
+
warnings
|
|
2619
|
+
};
|
|
2620
|
+
} else {
|
|
2621
|
+
return {
|
|
2622
|
+
output: "Validation failed: Model serialization is inconsistent",
|
|
2623
|
+
exitCode: 2,
|
|
2624
|
+
warnings
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
function formatOutput(model, options, colorize) {
|
|
2629
|
+
const { format, screenReader } = options;
|
|
2630
|
+
if (format === "audit") {
|
|
2631
|
+
return renderAuditReport(model, colorize);
|
|
2632
|
+
}
|
|
2633
|
+
if (format === "json") {
|
|
2634
|
+
return serializeModel(model);
|
|
2635
|
+
}
|
|
2636
|
+
if (format === "text") {
|
|
2637
|
+
return formatScreenReaderOutput(model, screenReader, colorize);
|
|
2638
|
+
}
|
|
2639
|
+
if (format === "both") {
|
|
2640
|
+
const json = serializeModel(model);
|
|
2641
|
+
const text = formatScreenReaderOutput(model, screenReader, colorize);
|
|
2642
|
+
const c = createColors(colorize);
|
|
2643
|
+
return `${c.sectionHeader("=== JSON Output ===")}
|
|
2644
|
+
${json}
|
|
2645
|
+
|
|
2646
|
+
${c.sectionHeader("=== Screen Reader Output ===")}
|
|
2647
|
+
${text}`;
|
|
2648
|
+
}
|
|
2649
|
+
throw new Error(`Unknown format: ${format}`);
|
|
2650
|
+
}
|
|
2651
|
+
function formatScreenReaderOutput(model, screenReader, colorize) {
|
|
2652
|
+
if (screenReader === "all") {
|
|
2653
|
+
const nvda = renderNVDA(model, colorize);
|
|
2654
|
+
const jaws = renderJAWS(model, colorize);
|
|
2655
|
+
const voiceover = renderVoiceOver(model, colorize);
|
|
2656
|
+
const c = createColors(colorize);
|
|
2657
|
+
return [
|
|
2658
|
+
c.sectionHeader("=== NVDA ==="),
|
|
2659
|
+
nvda,
|
|
2660
|
+
"",
|
|
2661
|
+
c.sectionHeader("=== JAWS ==="),
|
|
2662
|
+
jaws,
|
|
2663
|
+
"",
|
|
2664
|
+
c.sectionHeader("=== VoiceOver ==="),
|
|
2665
|
+
voiceover
|
|
2666
|
+
].join("\n");
|
|
2667
|
+
}
|
|
2668
|
+
switch (screenReader) {
|
|
2669
|
+
case "nvda":
|
|
2670
|
+
return renderNVDA(model, colorize);
|
|
2671
|
+
case "jaws":
|
|
2672
|
+
return renderJAWS(model, colorize);
|
|
2673
|
+
case "voiceover":
|
|
2674
|
+
return renderVoiceOver(model, colorize);
|
|
2675
|
+
default:
|
|
2676
|
+
throw new Error(`Unknown screen reader: ${screenReader}`);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
function formatWarnings(warnings, colorize) {
|
|
2680
|
+
if (warnings.length === 0) {
|
|
2681
|
+
return "";
|
|
2682
|
+
}
|
|
2683
|
+
const c = createColors(colorize ?? false);
|
|
2684
|
+
const header = colorize ? c.warning(c.bold("=== Warnings ===")) : "=== Warnings ===";
|
|
2685
|
+
const lines = [
|
|
2686
|
+
"",
|
|
2687
|
+
header,
|
|
2688
|
+
...warnings.map((w) => colorize ? `${c.warning("\u26A0\uFE0F")} ${w}` : `\u26A0\uFE0F ${w}`),
|
|
2689
|
+
""
|
|
2690
|
+
];
|
|
2691
|
+
return lines.join("\n");
|
|
2692
|
+
}
|
|
2693
|
+
function processBatch(filePaths, options) {
|
|
2694
|
+
const results = [];
|
|
2695
|
+
const allWarnings = [];
|
|
2696
|
+
let anyFailed = false;
|
|
2697
|
+
for (const filePath of filePaths) {
|
|
2698
|
+
try {
|
|
2699
|
+
const { readFileSync: readFileSync3 } = __require("fs");
|
|
2700
|
+
const html = readFileSync3(filePath, "utf-8");
|
|
2701
|
+
const result = processHTML(html, options);
|
|
2702
|
+
results.push({
|
|
2703
|
+
filePath,
|
|
2704
|
+
success: result.exitCode === 0,
|
|
2705
|
+
output: result.output,
|
|
2706
|
+
warnings: result.warnings
|
|
2707
|
+
});
|
|
2708
|
+
allWarnings.push(...result.warnings);
|
|
2709
|
+
if (result.exitCode !== 0) {
|
|
2710
|
+
anyFailed = true;
|
|
2711
|
+
}
|
|
2712
|
+
} catch (error) {
|
|
2713
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2714
|
+
results.push({
|
|
2715
|
+
filePath,
|
|
2716
|
+
success: false,
|
|
2717
|
+
error: errorMessage,
|
|
2718
|
+
warnings: []
|
|
2719
|
+
});
|
|
2720
|
+
anyFailed = true;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
const output = formatBatchOutput(results, options);
|
|
2724
|
+
return {
|
|
2725
|
+
results,
|
|
2726
|
+
output,
|
|
2727
|
+
exitCode: anyFailed ? 2 : 0,
|
|
2728
|
+
warnings: allWarnings
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
function formatBatchOutput(results, _options) {
|
|
2732
|
+
const lines = [];
|
|
2733
|
+
lines.push("=== Batch Processing Results ===");
|
|
2734
|
+
lines.push("");
|
|
2735
|
+
for (const result of results) {
|
|
2736
|
+
lines.push(`File: ${result.filePath}`);
|
|
2737
|
+
if (result.success) {
|
|
2738
|
+
lines.push("Status: \u2713 Success");
|
|
2739
|
+
if (result.warnings.length > 0) {
|
|
2740
|
+
lines.push(`Warnings: ${result.warnings.length}`);
|
|
2741
|
+
}
|
|
2742
|
+
lines.push("");
|
|
2743
|
+
lines.push("--- Output ---");
|
|
2744
|
+
lines.push(result.output || "");
|
|
2745
|
+
} else {
|
|
2746
|
+
lines.push("Status: \u2717 Failed");
|
|
2747
|
+
lines.push(`Error: ${result.error}`);
|
|
2748
|
+
}
|
|
2749
|
+
lines.push("");
|
|
2750
|
+
lines.push("---");
|
|
2751
|
+
lines.push("");
|
|
2752
|
+
}
|
|
2753
|
+
const successCount = results.filter((r) => r.success).length;
|
|
2754
|
+
const failCount = results.filter((r) => !r.success).length;
|
|
2755
|
+
lines.push("=== Summary ===");
|
|
2756
|
+
lines.push(`Total files: ${results.length}`);
|
|
2757
|
+
lines.push(`Successful: ${successCount}`);
|
|
2758
|
+
lines.push(`Failed: ${failCount}`);
|
|
2759
|
+
return lines.join("\n");
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// src/cli.ts
|
|
2763
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
2764
|
+
var __dirname2 = dirname(__filename2);
|
|
2765
|
+
var packageJson = JSON.parse(
|
|
2766
|
+
readFileSync2(join(__dirname2, "../package.json"), "utf-8")
|
|
2767
|
+
);
|
|
2768
|
+
var program = new Command();
|
|
2769
|
+
program.name("speakable").description("Analyze HTML accessibility announcements and generate screen reader output").version(packageJson.version, "-v, --version", "Output the current version").helpOption("-h, --help", "Display help for command");
|
|
2770
|
+
program.argument("[input...]", 'HTML file path(s) or "-" for stdin').option("-o, --output <path>", "Output file path (default: stdout)").option(
|
|
2771
|
+
"-f, --format <format>",
|
|
2772
|
+
"Output format: json, text, audit, or both (default: json)",
|
|
2773
|
+
"json"
|
|
2774
|
+
).option(
|
|
2775
|
+
"-s, --screen-reader <reader>",
|
|
2776
|
+
"Screen reader: nvda, jaws, voiceover, or all (default: nvda)",
|
|
2777
|
+
"nvda"
|
|
2778
|
+
).option(
|
|
2779
|
+
"--selector <selector>",
|
|
2780
|
+
"CSS selector to filter elements"
|
|
2781
|
+
).option(
|
|
2782
|
+
"--validate",
|
|
2783
|
+
"Validate round-trip serialization"
|
|
2784
|
+
).option(
|
|
2785
|
+
"--diff <file>",
|
|
2786
|
+
"Compare with another HTML file (semantic diff mode)"
|
|
2787
|
+
).option(
|
|
2788
|
+
"--batch",
|
|
2789
|
+
"Process multiple files in batch mode"
|
|
2790
|
+
).action(async (input, rawOptions) => {
|
|
2791
|
+
try {
|
|
2792
|
+
const options = validateOptions(rawOptions);
|
|
2793
|
+
const parsedInput = parseInput(input);
|
|
2794
|
+
validateInput(parsedInput);
|
|
2795
|
+
validateDiffMode(options, parsedInput);
|
|
2796
|
+
validateBatchMode(options, parsedInput);
|
|
2797
|
+
if (options.batch && parsedInput.inputs && parsedInput.inputs.length > 0) {
|
|
2798
|
+
const result2 = processBatch(parsedInput.inputs, options);
|
|
2799
|
+
writeOutput(result2.output, options.output);
|
|
2800
|
+
if (result2.warnings.length > 0) {
|
|
2801
|
+
console.error(formatWarnings(result2.warnings, isColorEnabled(process.stderr)));
|
|
2802
|
+
}
|
|
2803
|
+
process.exit(result2.exitCode);
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
const htmlResult = readHTML(parsedInput.input, parsedInput.isStdin);
|
|
2807
|
+
const html = await Promise.resolve(htmlResult);
|
|
2808
|
+
let diffHTML;
|
|
2809
|
+
if (options.diff) {
|
|
2810
|
+
diffHTML = readHTML(options.diff, false);
|
|
2811
|
+
}
|
|
2812
|
+
const result = processHTML(html, options, diffHTML);
|
|
2813
|
+
writeOutput(result.output, options.output);
|
|
2814
|
+
if (result.warnings.length > 0) {
|
|
2815
|
+
console.error(formatWarnings(result.warnings, isColorEnabled(process.stderr)));
|
|
2816
|
+
}
|
|
2817
|
+
process.exit(result.exitCode);
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
if (error instanceof FileIOError) {
|
|
2820
|
+
console.error(`Error: ${error.message}`);
|
|
2821
|
+
process.exit(3);
|
|
2822
|
+
}
|
|
2823
|
+
if (error instanceof Error) {
|
|
2824
|
+
console.error(`Error: ${error.message}`);
|
|
2825
|
+
process.exit(1);
|
|
2826
|
+
}
|
|
2827
|
+
throw error;
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
program.addHelpText("after", `
|
|
2831
|
+
|
|
2832
|
+
Examples:
|
|
2833
|
+
$ speakable input.html
|
|
2834
|
+
$ speakable input.html -f text -s nvda
|
|
2835
|
+
$ speakable input.html -f both -s all
|
|
2836
|
+
$ speakable input.html --selector "button"
|
|
2837
|
+
$ speakable input.html --diff old.html
|
|
2838
|
+
$ speakable --batch file1.html file2.html file3.html
|
|
2839
|
+
$ cat input.html | speakable -
|
|
2840
|
+
$ speakable input.html -o output.json
|
|
2841
|
+
$ speakable input.html -f audit
|
|
2842
|
+
|
|
2843
|
+
Screen Readers:
|
|
2844
|
+
nvda - NVDA (Windows)
|
|
2845
|
+
jaws - JAWS (Windows)
|
|
2846
|
+
voiceover - VoiceOver (macOS)
|
|
2847
|
+
all - All screen readers
|
|
2848
|
+
|
|
2849
|
+
Output Formats:
|
|
2850
|
+
json - Semantic model as JSON
|
|
2851
|
+
text - Screen reader announcement text
|
|
2852
|
+
audit - Developer-friendly audit report
|
|
2853
|
+
both - Both JSON and text
|
|
2854
|
+
|
|
2855
|
+
Notes:
|
|
2856
|
+
- Screen reader output is heuristic and may differ from actual behavior
|
|
2857
|
+
- Use --validate to check serialization round-trip integrity
|
|
2858
|
+
- Use --diff to detect accessibility changes between versions
|
|
2859
|
+
- Use --batch to process multiple files (continues on individual errors)
|
|
2860
|
+
`);
|
|
2861
|
+
program.parse();
|
|
2862
|
+
//# sourceMappingURL=cli.js.map
|