@reticular/speakable 1.2.0 → 1.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Speakable
2
2
 
3
- Simulate how NVDA, JAWS, and VoiceOver interpret your HTML — from a single CLI tool. Catch screen reader issues early, detect regressions in CI/CD, and reduce manual testing overhead.
3
+ Simulate how NVDA, JAWS, VoiceOver, and Windows Narrator interpret your HTML — from a single CLI tool. Catch screen reader issues early, detect regressions in CI/CD, and reduce manual testing overhead.
4
4
 
5
5
  **Try it in your browser — no install needed:** [getspeakable.dev/tool](https://getspeakable.dev/tool)
6
6
 
@@ -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 VoiceOver 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.
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
- | All | `-s all` | All three readers side by side |
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 VoiceOver says "navigation".
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
 
@@ -182,7 +183,7 @@ Add to your MCP config (Kiro, VS Code, Cursor, Claude Desktop, Windsurf):
182
183
 
183
184
  | Tool | Description |
184
185
  |------|-------------|
185
- | `analyze_html` | Predict screen reader output for NVDA, JAWS, and VoiceOver |
186
+ | `analyze_html` | Predict screen reader output for NVDA, JAWS, VoiceOver, and Narrator |
186
187
  | `audit_html` | Generate accessibility audit report with issues and remediation |
187
188
  | `diff_html` | Compare two HTML versions for accessibility regressions |
188
189
 
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: