@reticular/speakable 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -9
- package/dist/cli.js +240 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1058 -1
- package/dist/index.js +4715 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +258 -19
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Speakable
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Predict how NVDA, JAWS, VoiceOver, and Windows Narrator interpret your HTML, and detect accessibility behavior regressions during component interaction. Catch screen reader issues early, analyze runtime focus management, and prevent regressions in CI/CD.
|
|
4
4
|
|
|
5
|
-
**Try it in your browser
|
|
5
|
+
**Try it in your browser:** [getspeakable.dev/tool](https://getspeakable.dev/tool)
|
|
6
6
|
|
|
7
|
-
Speakable bridges the gap between rule-based linting tools (like axe) and manual screen reader testing. It
|
|
7
|
+
Speakable bridges the gap between rule-based linting tools (like axe) and manual screen reader testing. It provides both static HTML analysis and runtime accessibility behavior tracking, giving you automated insight into how assistive technologies interpret your UI throughout the development lifecycle.
|
|
8
8
|
|
|
9
9
|
> Screen reader output is heuristic and may differ from actual behavior. Speakable complements manual testing — it doesn't replace it.
|
|
10
10
|
|
|
@@ -43,7 +43,7 @@ cat page.html | speakable -
|
|
|
43
43
|
|
|
44
44
|
## Web Analyzer
|
|
45
45
|
|
|
46
|
-
If you want to test HTML quickly without installing anything, paste it directly into the web analyzer at [getspeakable.dev/tool](https://getspeakable.dev/tool). It runs the same simulation engine in the browser — supports NVDA, JAWS, and
|
|
46
|
+
If you want to test HTML quickly without installing anything, paste it directly into the web analyzer at [getspeakable.dev/tool](https://getspeakable.dev/tool). It runs the same simulation engine in the browser — supports NVDA, JAWS, VoiceOver, and Narrator output, CSS selector filtering, and diff mode. Useful for quick iteration, debugging a specific component, or sharing results with teammates who don't have the CLI installed.
|
|
47
47
|
|
|
48
48
|
## CLI Reference
|
|
49
49
|
|
|
@@ -83,9 +83,10 @@ Options:
|
|
|
83
83
|
| NVDA | `-s nvda` | Windows |
|
|
84
84
|
| JAWS | `-s jaws` | Windows |
|
|
85
85
|
| VoiceOver | `-s voiceover` | macOS |
|
|
86
|
-
|
|
|
86
|
+
| Narrator | `-s narrator` | Windows |
|
|
87
|
+
| All | `-s all` | All four readers side by side |
|
|
87
88
|
|
|
88
|
-
Each renderer produces heuristic output approximating the real screen reader's behavior. Key differences are preserved — for example, NVDA says "navigation landmark" while JAWS says "navigation region" and
|
|
89
|
+
Each renderer produces heuristic output approximating the real screen reader's behavior. Key differences are preserved — for example, NVDA says "navigation landmark" while JAWS says "navigation region", VoiceOver says "navigation", and Narrator says "navigation".
|
|
89
90
|
|
|
90
91
|
## Features
|
|
91
92
|
|
|
@@ -159,6 +160,123 @@ Verify that the accessibility model serializes and deserializes consistently:
|
|
|
159
160
|
speakable page.html --validate
|
|
160
161
|
```
|
|
161
162
|
|
|
163
|
+
## Runtime Accessibility Analysis
|
|
164
|
+
|
|
165
|
+
Beyond static HTML analysis, Speakable tracks accessibility behavior during component interaction: focus transitions, ARIA state changes, live region announcements, and dialog lifecycle events.
|
|
166
|
+
|
|
167
|
+
### CLI Usage
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Analyze runtime behavior of a URL
|
|
171
|
+
speakable runtime https://localhost:3000/settings
|
|
172
|
+
|
|
173
|
+
# Use a built-in interaction pattern
|
|
174
|
+
speakable runtime https://localhost:3000 --interaction modal-dialog
|
|
175
|
+
|
|
176
|
+
# Analyze all Storybook stories
|
|
177
|
+
speakable runtime http://localhost:6006 --storybook
|
|
178
|
+
|
|
179
|
+
# Filter to specific components
|
|
180
|
+
speakable runtime http://localhost:6006 --storybook --story "Dialog*"
|
|
181
|
+
|
|
182
|
+
# Save baselines for regression detection
|
|
183
|
+
speakable runtime http://localhost:6006 --storybook --runtime-snapshot ./baselines
|
|
184
|
+
|
|
185
|
+
# CI mode: fail on behavior regressions
|
|
186
|
+
speakable runtime http://localhost:6006 --storybook --runtime-snapshot ./baselines --runtime-ci
|
|
187
|
+
|
|
188
|
+
# Authenticate with protected Storybook instances
|
|
189
|
+
speakable runtime https://storybook.internal.co --storybook \
|
|
190
|
+
--storybook-auth-header "Bearer your-token"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Programmatic API
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { runtime } from '@reticular/speakable';
|
|
197
|
+
|
|
198
|
+
const generator = runtime.createTimelineGenerator({
|
|
199
|
+
document: myDocument,
|
|
200
|
+
componentName: 'SettingsDialog',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const sequence = runtime.getBuiltinPattern('modal-dialog', {
|
|
204
|
+
trigger: 'button.open-settings',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const timeline = await generator.capture(sequence);
|
|
208
|
+
|
|
209
|
+
// Inspect events
|
|
210
|
+
timeline.events.forEach(event => {
|
|
211
|
+
console.log(`${event.type}: ${event.target.accessibleName}`);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Check for warnings (focus escape, missing announcements, etc.)
|
|
215
|
+
timeline.warnings.forEach(w => {
|
|
216
|
+
console.warn(w.payload.message);
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Built-in Interaction Patterns
|
|
221
|
+
|
|
222
|
+
| Pattern | What it tests |
|
|
223
|
+
|---------|---------------|
|
|
224
|
+
| `modal-dialog` | Focus moves to dialog, stays trapped, returns on close |
|
|
225
|
+
| `combobox` | Options announced on arrow navigation, selection confirmed |
|
|
226
|
+
| `tabs` | Arrow keys switch tabs, panel content communicated |
|
|
227
|
+
| `accordion` | Expanded/collapsed state announced on toggle |
|
|
228
|
+
|
|
229
|
+
### Heuristic Warnings
|
|
230
|
+
|
|
231
|
+
The runtime engine automatically detects common anti-patterns without needing a baseline:
|
|
232
|
+
|
|
233
|
+
- Focus not moved into modal dialog on open
|
|
234
|
+
- Focus escaped modal dialog (broken focus trap)
|
|
235
|
+
- Rapid announcements (aria-live flooding)
|
|
236
|
+
- Keyboard action with no accessibility response
|
|
237
|
+
- Focused element removed without focus management
|
|
238
|
+
|
|
239
|
+
### Behavior Diffing and Regression Detection
|
|
240
|
+
|
|
241
|
+
Compare accessibility timelines across builds to catch regressions:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { runtime } from '@reticular/speakable';
|
|
245
|
+
|
|
246
|
+
const diff = runtime.diffTimelines(baselineTimeline, currentTimeline);
|
|
247
|
+
// diff.added, diff.removed, diff.modified
|
|
248
|
+
|
|
249
|
+
const classified = runtime.classifyDiff(diff);
|
|
250
|
+
// classified.highestSeverity: 'critical' | 'high' | 'medium' | 'low'
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Severity levels:
|
|
254
|
+
- **Critical**: Focus loss, modal focus escape, missing required announcements
|
|
255
|
+
- **High**: Incorrect focus restoration, keyboard navigation regression
|
|
256
|
+
- **Medium**: Additional announcements, accessible name changes
|
|
257
|
+
- **Low**: Timing differences, non-breaking behavior changes
|
|
258
|
+
|
|
259
|
+
### Storybook Integration
|
|
260
|
+
|
|
261
|
+
Connect to a running Storybook instance to analyze accessibility behavior across your component library:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { createStorybookAdapter, createStoryLoader } from '@reticular/speakable/storybook';
|
|
265
|
+
|
|
266
|
+
const adapter = createStorybookAdapter({
|
|
267
|
+
url: 'http://localhost:6006',
|
|
268
|
+
componentFilter: 'Dialog*',
|
|
269
|
+
authHeader: 'Bearer token', // for protected instances
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await adapter.connect();
|
|
273
|
+
const stories = await adapter.discoverStories();
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The Storybook pipeline discovers stories, loads them in isolation, runs interaction patterns, and produces timelines that can be baselined and compared. Works with Storybook 7.x and 8.x.
|
|
277
|
+
|
|
278
|
+
> See the [Runtime Analysis docs](https://getspeakable.dev/docs/runtime-analysis) for interactive demos and full API reference.
|
|
279
|
+
|
|
162
280
|
## MCP Integration (AI Assistants)
|
|
163
281
|
|
|
164
282
|
Speakable includes a Model Context Protocol (MCP) server that lets AI coding assistants analyze accessibility in real-time. Ask your assistant "check if this component is accessible" and it calls Speakable automatically.
|
|
@@ -182,9 +300,11 @@ Add to your MCP config (Kiro, VS Code, Cursor, Claude Desktop, Windsurf):
|
|
|
182
300
|
|
|
183
301
|
| Tool | Description |
|
|
184
302
|
|------|-------------|
|
|
185
|
-
| `analyze_html` | Predict screen reader output for NVDA, JAWS, and
|
|
303
|
+
| `analyze_html` | Predict screen reader output for NVDA, JAWS, VoiceOver, and Narrator |
|
|
186
304
|
| `audit_html` | Generate accessibility audit report with issues and remediation |
|
|
187
305
|
| `diff_html` | Compare two HTML versions for accessibility regressions |
|
|
306
|
+
| `analyze_runtime` | Run runtime accessibility analysis with interaction sequences |
|
|
307
|
+
| `diff_runtime` | Compare two accessibility timelines for behavior regressions |
|
|
188
308
|
|
|
189
309
|
All tools run locally — no network requests, no data leaves your machine. See the [MCP Integration docs](https://getspeakable.dev/docs/mcp-integration) for full setup instructions.
|
|
190
310
|
|
|
@@ -269,10 +389,10 @@ Speakable processes HTML through a four-stage pipeline:
|
|
|
269
389
|
|
|
270
390
|
- Replace manual screen reader testing with real assistive technology
|
|
271
391
|
- Guarantee WCAG compliance
|
|
272
|
-
- Test dynamic/JavaScript-driven content (it analyzes static HTML)
|
|
273
392
|
- Perfectly replicate screen reader behavior (output is heuristic)
|
|
393
|
+
- Execute JavaScript in static analysis mode (use runtime mode for dynamic content)
|
|
274
394
|
|
|
275
|
-
Use Speakable to catch issues early and reduce the manual testing burden
|
|
395
|
+
Use Speakable to catch issues early and reduce the manual testing burden. Validate critical flows with real screen readers.
|
|
276
396
|
|
|
277
397
|
## Best Practices
|
|
278
398
|
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ function validateOptions(rawOptions) {
|
|
|
22
22
|
`Invalid format: ${format}. Must be one of: ${validFormats.join(", ")}`
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
|
-
const validReaders = ["nvda", "jaws", "voiceover", "all"];
|
|
25
|
+
const validReaders = ["nvda", "jaws", "voiceover", "narrator", "all"];
|
|
26
26
|
if (!validReaders.includes(screenReader)) {
|
|
27
27
|
throw new Error(
|
|
28
28
|
`Invalid screen reader: ${screenReader}. Must be one of: ${validReaders.join(", ")}`
|
|
@@ -2019,6 +2019,236 @@ function formatStatesVoiceOver(node) {
|
|
|
2019
2019
|
return states.join(", ");
|
|
2020
2020
|
}
|
|
2021
2021
|
|
|
2022
|
+
// src/renderer/narrator-renderer.ts
|
|
2023
|
+
function renderNarrator(model, colorize) {
|
|
2024
|
+
const c = createColors(colorize ?? false);
|
|
2025
|
+
const announcements = [];
|
|
2026
|
+
renderNodeNarrator(model.root, announcements, c);
|
|
2027
|
+
return announcements.join("\n");
|
|
2028
|
+
}
|
|
2029
|
+
function renderNodeNarrator(node, announcements, c) {
|
|
2030
|
+
const announcement = formatNodeNarrator(node, c);
|
|
2031
|
+
if (announcement) {
|
|
2032
|
+
announcements.push(announcement);
|
|
2033
|
+
}
|
|
2034
|
+
for (const child of node.children) {
|
|
2035
|
+
if (child.name && child.name === node.name && node.children.length === 1) {
|
|
2036
|
+
for (const grandchild of child.children) {
|
|
2037
|
+
renderNodeNarrator(grandchild, announcements, c);
|
|
2038
|
+
}
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
renderNodeNarrator(child, announcements, c);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function formatNodeNarrator(node, c) {
|
|
2045
|
+
const parts = [];
|
|
2046
|
+
const roleFirst = shouldAnnounceRoleFirst2(node.role);
|
|
2047
|
+
if (roleFirst) {
|
|
2048
|
+
const roleText = formatRoleNarrator(node);
|
|
2049
|
+
if (roleText) {
|
|
2050
|
+
parts.push(c.roleName(roleText));
|
|
2051
|
+
}
|
|
2052
|
+
if (node.name) {
|
|
2053
|
+
parts.push(c.elementName(node.name));
|
|
2054
|
+
}
|
|
2055
|
+
} else {
|
|
2056
|
+
if (node.name) {
|
|
2057
|
+
parts.push(c.elementName(node.name));
|
|
2058
|
+
}
|
|
2059
|
+
const roleText = formatRoleNarrator(node);
|
|
2060
|
+
if (roleText) {
|
|
2061
|
+
parts.push(c.roleName(roleText));
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
const stateText = formatStatesNarrator(node);
|
|
2065
|
+
if (stateText) {
|
|
2066
|
+
parts.push(c.stateName(stateText));
|
|
2067
|
+
}
|
|
2068
|
+
if (node.value) {
|
|
2069
|
+
const valueText = node.value.text || String(node.value.current);
|
|
2070
|
+
if (valueText) {
|
|
2071
|
+
parts.push(valueText);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
if (node.description) {
|
|
2075
|
+
parts.push(c.description(node.description));
|
|
2076
|
+
}
|
|
2077
|
+
return parts.join(", ");
|
|
2078
|
+
}
|
|
2079
|
+
function shouldAnnounceRoleFirst2(role) {
|
|
2080
|
+
const roleFirstRoles = [
|
|
2081
|
+
"link",
|
|
2082
|
+
"heading",
|
|
2083
|
+
"navigation",
|
|
2084
|
+
"main",
|
|
2085
|
+
"banner",
|
|
2086
|
+
"contentinfo",
|
|
2087
|
+
"complementary",
|
|
2088
|
+
"region",
|
|
2089
|
+
"form",
|
|
2090
|
+
"search",
|
|
2091
|
+
"blockquote",
|
|
2092
|
+
"figure",
|
|
2093
|
+
"dialog",
|
|
2094
|
+
"group",
|
|
2095
|
+
"document"
|
|
2096
|
+
];
|
|
2097
|
+
return roleFirstRoles.includes(role);
|
|
2098
|
+
}
|
|
2099
|
+
function formatRoleNarrator(node) {
|
|
2100
|
+
const role = node.role;
|
|
2101
|
+
switch (role) {
|
|
2102
|
+
case "button":
|
|
2103
|
+
return "button";
|
|
2104
|
+
case "link":
|
|
2105
|
+
return "link";
|
|
2106
|
+
case "heading":
|
|
2107
|
+
if (node.state.level) {
|
|
2108
|
+
return `Heading level ${node.state.level}`;
|
|
2109
|
+
}
|
|
2110
|
+
return "Heading";
|
|
2111
|
+
case "textbox":
|
|
2112
|
+
return "edit";
|
|
2113
|
+
case "checkbox":
|
|
2114
|
+
return "check box";
|
|
2115
|
+
case "radio":
|
|
2116
|
+
return "radio button";
|
|
2117
|
+
case "combobox":
|
|
2118
|
+
return "combo box";
|
|
2119
|
+
case "listbox":
|
|
2120
|
+
return "list box";
|
|
2121
|
+
case "option":
|
|
2122
|
+
return "option";
|
|
2123
|
+
case "list":
|
|
2124
|
+
return "list";
|
|
2125
|
+
case "listitem":
|
|
2126
|
+
return "list item";
|
|
2127
|
+
case "navigation":
|
|
2128
|
+
return "navigation";
|
|
2129
|
+
case "main":
|
|
2130
|
+
return "main";
|
|
2131
|
+
case "banner":
|
|
2132
|
+
return "banner";
|
|
2133
|
+
case "contentinfo":
|
|
2134
|
+
return "content info";
|
|
2135
|
+
case "complementary":
|
|
2136
|
+
return "complementary";
|
|
2137
|
+
case "form":
|
|
2138
|
+
return "form";
|
|
2139
|
+
case "search":
|
|
2140
|
+
return "search";
|
|
2141
|
+
case "region":
|
|
2142
|
+
return "region";
|
|
2143
|
+
case "img":
|
|
2144
|
+
return "image";
|
|
2145
|
+
case "article":
|
|
2146
|
+
return "article";
|
|
2147
|
+
case "generic":
|
|
2148
|
+
return "";
|
|
2149
|
+
case "staticText":
|
|
2150
|
+
case "paragraph":
|
|
2151
|
+
case "cell":
|
|
2152
|
+
case "term":
|
|
2153
|
+
case "definition":
|
|
2154
|
+
case "caption":
|
|
2155
|
+
return "";
|
|
2156
|
+
case "blockquote":
|
|
2157
|
+
return "block quote";
|
|
2158
|
+
case "code":
|
|
2159
|
+
return "code";
|
|
2160
|
+
case "table": {
|
|
2161
|
+
const rows = node.children.filter((c) => c.role === "row");
|
|
2162
|
+
const rowCount = rows.length;
|
|
2163
|
+
const colCount = rows.length > 0 ? rows[0].children.length : 0;
|
|
2164
|
+
return `table, ${rowCount} rows, ${colCount} columns`;
|
|
2165
|
+
}
|
|
2166
|
+
case "row":
|
|
2167
|
+
return "row";
|
|
2168
|
+
case "columnheader":
|
|
2169
|
+
return "column header";
|
|
2170
|
+
case "rowheader":
|
|
2171
|
+
return "row header";
|
|
2172
|
+
case "figure":
|
|
2173
|
+
return "figure";
|
|
2174
|
+
case "dialog":
|
|
2175
|
+
return "dialog";
|
|
2176
|
+
case "meter":
|
|
2177
|
+
return "meter";
|
|
2178
|
+
case "progressbar":
|
|
2179
|
+
return "progress bar";
|
|
2180
|
+
case "status":
|
|
2181
|
+
return "status";
|
|
2182
|
+
case "group":
|
|
2183
|
+
return node.name ? "group" : "";
|
|
2184
|
+
case "document":
|
|
2185
|
+
return "document";
|
|
2186
|
+
case "application":
|
|
2187
|
+
return "application";
|
|
2188
|
+
case "separator":
|
|
2189
|
+
return "separator";
|
|
2190
|
+
default:
|
|
2191
|
+
return role;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
function formatStatesNarrator(node) {
|
|
2195
|
+
const states = [];
|
|
2196
|
+
if (node.state.expanded !== void 0) {
|
|
2197
|
+
states.push(node.state.expanded ? "expanded" : "collapsed");
|
|
2198
|
+
}
|
|
2199
|
+
if (node.state.checked !== void 0) {
|
|
2200
|
+
if (node.state.checked === "mixed") {
|
|
2201
|
+
states.push("partially selected");
|
|
2202
|
+
} else {
|
|
2203
|
+
states.push(node.state.checked ? "checked" : "unchecked");
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
if (node.state.pressed !== void 0) {
|
|
2207
|
+
if (node.state.pressed === "mixed") {
|
|
2208
|
+
states.push("partially pressed");
|
|
2209
|
+
} else {
|
|
2210
|
+
states.push(node.state.pressed ? "pressed" : "not pressed");
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
if (node.state.selected !== void 0) {
|
|
2214
|
+
states.push(node.state.selected ? "selected" : "not selected");
|
|
2215
|
+
}
|
|
2216
|
+
if (node.state.disabled) {
|
|
2217
|
+
states.push("disabled");
|
|
2218
|
+
}
|
|
2219
|
+
if (node.state.invalid) {
|
|
2220
|
+
states.push("invalid");
|
|
2221
|
+
}
|
|
2222
|
+
if (node.state.required) {
|
|
2223
|
+
states.push("required");
|
|
2224
|
+
}
|
|
2225
|
+
if (node.state.readonly) {
|
|
2226
|
+
states.push("read only");
|
|
2227
|
+
}
|
|
2228
|
+
if (node.state.busy) {
|
|
2229
|
+
states.push("busy");
|
|
2230
|
+
}
|
|
2231
|
+
if (node.state.current) {
|
|
2232
|
+
if (node.state.current === "page") {
|
|
2233
|
+
states.push("current page");
|
|
2234
|
+
} else if (node.state.current === "step") {
|
|
2235
|
+
states.push("current step");
|
|
2236
|
+
} else if (node.state.current === "location") {
|
|
2237
|
+
states.push("current location");
|
|
2238
|
+
} else if (node.state.current === "date") {
|
|
2239
|
+
states.push("current date");
|
|
2240
|
+
} else if (node.state.current === "time") {
|
|
2241
|
+
states.push("current time");
|
|
2242
|
+
} else if (node.state.current === "true") {
|
|
2243
|
+
states.push("current");
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
if (node.state.grabbed !== void 0) {
|
|
2247
|
+
states.push(node.state.grabbed ? "grabbed" : "not grabbed");
|
|
2248
|
+
}
|
|
2249
|
+
return states.join(", ");
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2022
2252
|
// src/renderer/audit-renderer.ts
|
|
2023
2253
|
function renderAuditReport(model, colorize) {
|
|
2024
2254
|
const report = generateAuditReport(model);
|
|
@@ -2686,6 +2916,7 @@ function formatScreenReaderOutput(model, screenReader, colorize) {
|
|
|
2686
2916
|
const nvda = renderNVDA(model, colorize);
|
|
2687
2917
|
const jaws = renderJAWS(model, colorize);
|
|
2688
2918
|
const voiceover = renderVoiceOver(model, colorize);
|
|
2919
|
+
const narrator = renderNarrator(model, colorize);
|
|
2689
2920
|
const c = createColors(colorize);
|
|
2690
2921
|
return [
|
|
2691
2922
|
c.sectionHeader("=== NVDA ==="),
|
|
@@ -2695,7 +2926,10 @@ function formatScreenReaderOutput(model, screenReader, colorize) {
|
|
|
2695
2926
|
jaws,
|
|
2696
2927
|
"",
|
|
2697
2928
|
c.sectionHeader("=== VoiceOver ==="),
|
|
2698
|
-
voiceover
|
|
2929
|
+
voiceover,
|
|
2930
|
+
"",
|
|
2931
|
+
c.sectionHeader("=== Narrator ==="),
|
|
2932
|
+
narrator
|
|
2699
2933
|
].join("\n");
|
|
2700
2934
|
}
|
|
2701
2935
|
switch (screenReader) {
|
|
@@ -2705,6 +2939,8 @@ function formatScreenReaderOutput(model, screenReader, colorize) {
|
|
|
2705
2939
|
return renderJAWS(model, colorize);
|
|
2706
2940
|
case "voiceover":
|
|
2707
2941
|
return renderVoiceOver(model, colorize);
|
|
2942
|
+
case "narrator":
|
|
2943
|
+
return renderNarrator(model, colorize);
|
|
2708
2944
|
default:
|
|
2709
2945
|
throw new Error(`Unknown screen reader: ${screenReader}`);
|
|
2710
2946
|
}
|
|
@@ -2806,7 +3042,7 @@ program.argument("[input...]", 'HTML file path(s) or "-" for stdin').option("-o,
|
|
|
2806
3042
|
"json"
|
|
2807
3043
|
).option(
|
|
2808
3044
|
"-s, --screen-reader <reader>",
|
|
2809
|
-
"Screen reader: nvda, jaws, voiceover, or all (default: nvda)",
|
|
3045
|
+
"Screen reader: nvda, jaws, voiceover, narrator, or all (default: nvda)",
|
|
2810
3046
|
"nvda"
|
|
2811
3047
|
).option(
|
|
2812
3048
|
"--selector <selector>",
|
|
@@ -2877,6 +3113,7 @@ Screen Readers:
|
|
|
2877
3113
|
nvda - NVDA (Windows)
|
|
2878
3114
|
jaws - JAWS (Windows)
|
|
2879
3115
|
voiceover - VoiceOver (macOS)
|
|
3116
|
+
narrator - Narrator (Windows)
|
|
2880
3117
|
all - All screen readers
|
|
2881
3118
|
|
|
2882
3119
|
Output Formats:
|