@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/dist/mcp.js CHANGED
@@ -1656,6 +1656,236 @@ function formatStatesVoiceOver(node) {
1656
1656
  return states.join(", ");
1657
1657
  }
1658
1658
 
1659
+ // src/renderer/narrator-renderer.ts
1660
+ function renderNarrator(model, colorize) {
1661
+ const c = createColors(colorize ?? false);
1662
+ const announcements = [];
1663
+ renderNodeNarrator(model.root, announcements, c);
1664
+ return announcements.join("\n");
1665
+ }
1666
+ function renderNodeNarrator(node, announcements, c) {
1667
+ const announcement = formatNodeNarrator(node, c);
1668
+ if (announcement) {
1669
+ announcements.push(announcement);
1670
+ }
1671
+ for (const child of node.children) {
1672
+ if (child.name && child.name === node.name && node.children.length === 1) {
1673
+ for (const grandchild of child.children) {
1674
+ renderNodeNarrator(grandchild, announcements, c);
1675
+ }
1676
+ continue;
1677
+ }
1678
+ renderNodeNarrator(child, announcements, c);
1679
+ }
1680
+ }
1681
+ function formatNodeNarrator(node, c) {
1682
+ const parts = [];
1683
+ const roleFirst = shouldAnnounceRoleFirst2(node.role);
1684
+ if (roleFirst) {
1685
+ const roleText = formatRoleNarrator(node);
1686
+ if (roleText) {
1687
+ parts.push(c.roleName(roleText));
1688
+ }
1689
+ if (node.name) {
1690
+ parts.push(c.elementName(node.name));
1691
+ }
1692
+ } else {
1693
+ if (node.name) {
1694
+ parts.push(c.elementName(node.name));
1695
+ }
1696
+ const roleText = formatRoleNarrator(node);
1697
+ if (roleText) {
1698
+ parts.push(c.roleName(roleText));
1699
+ }
1700
+ }
1701
+ const stateText = formatStatesNarrator(node);
1702
+ if (stateText) {
1703
+ parts.push(c.stateName(stateText));
1704
+ }
1705
+ if (node.value) {
1706
+ const valueText = node.value.text || String(node.value.current);
1707
+ if (valueText) {
1708
+ parts.push(valueText);
1709
+ }
1710
+ }
1711
+ if (node.description) {
1712
+ parts.push(c.description(node.description));
1713
+ }
1714
+ return parts.join(", ");
1715
+ }
1716
+ function shouldAnnounceRoleFirst2(role) {
1717
+ const roleFirstRoles = [
1718
+ "link",
1719
+ "heading",
1720
+ "navigation",
1721
+ "main",
1722
+ "banner",
1723
+ "contentinfo",
1724
+ "complementary",
1725
+ "region",
1726
+ "form",
1727
+ "search",
1728
+ "blockquote",
1729
+ "figure",
1730
+ "dialog",
1731
+ "group",
1732
+ "document"
1733
+ ];
1734
+ return roleFirstRoles.includes(role);
1735
+ }
1736
+ function formatRoleNarrator(node) {
1737
+ const role = node.role;
1738
+ switch (role) {
1739
+ case "button":
1740
+ return "button";
1741
+ case "link":
1742
+ return "link";
1743
+ case "heading":
1744
+ if (node.state.level) {
1745
+ return `Heading level ${node.state.level}`;
1746
+ }
1747
+ return "Heading";
1748
+ case "textbox":
1749
+ return "edit";
1750
+ case "checkbox":
1751
+ return "check box";
1752
+ case "radio":
1753
+ return "radio button";
1754
+ case "combobox":
1755
+ return "combo box";
1756
+ case "listbox":
1757
+ return "list box";
1758
+ case "option":
1759
+ return "option";
1760
+ case "list":
1761
+ return "list";
1762
+ case "listitem":
1763
+ return "list item";
1764
+ case "navigation":
1765
+ return "navigation";
1766
+ case "main":
1767
+ return "main";
1768
+ case "banner":
1769
+ return "banner";
1770
+ case "contentinfo":
1771
+ return "content info";
1772
+ case "complementary":
1773
+ return "complementary";
1774
+ case "form":
1775
+ return "form";
1776
+ case "search":
1777
+ return "search";
1778
+ case "region":
1779
+ return "region";
1780
+ case "img":
1781
+ return "image";
1782
+ case "article":
1783
+ return "article";
1784
+ case "generic":
1785
+ return "";
1786
+ case "staticText":
1787
+ case "paragraph":
1788
+ case "cell":
1789
+ case "term":
1790
+ case "definition":
1791
+ case "caption":
1792
+ return "";
1793
+ case "blockquote":
1794
+ return "block quote";
1795
+ case "code":
1796
+ return "code";
1797
+ case "table": {
1798
+ const rows = node.children.filter((c) => c.role === "row");
1799
+ const rowCount = rows.length;
1800
+ const colCount = rows.length > 0 ? rows[0].children.length : 0;
1801
+ return `table, ${rowCount} rows, ${colCount} columns`;
1802
+ }
1803
+ case "row":
1804
+ return "row";
1805
+ case "columnheader":
1806
+ return "column header";
1807
+ case "rowheader":
1808
+ return "row header";
1809
+ case "figure":
1810
+ return "figure";
1811
+ case "dialog":
1812
+ return "dialog";
1813
+ case "meter":
1814
+ return "meter";
1815
+ case "progressbar":
1816
+ return "progress bar";
1817
+ case "status":
1818
+ return "status";
1819
+ case "group":
1820
+ return node.name ? "group" : "";
1821
+ case "document":
1822
+ return "document";
1823
+ case "application":
1824
+ return "application";
1825
+ case "separator":
1826
+ return "separator";
1827
+ default:
1828
+ return role;
1829
+ }
1830
+ }
1831
+ function formatStatesNarrator(node) {
1832
+ const states = [];
1833
+ if (node.state.expanded !== void 0) {
1834
+ states.push(node.state.expanded ? "expanded" : "collapsed");
1835
+ }
1836
+ if (node.state.checked !== void 0) {
1837
+ if (node.state.checked === "mixed") {
1838
+ states.push("partially selected");
1839
+ } else {
1840
+ states.push(node.state.checked ? "checked" : "unchecked");
1841
+ }
1842
+ }
1843
+ if (node.state.pressed !== void 0) {
1844
+ if (node.state.pressed === "mixed") {
1845
+ states.push("partially pressed");
1846
+ } else {
1847
+ states.push(node.state.pressed ? "pressed" : "not pressed");
1848
+ }
1849
+ }
1850
+ if (node.state.selected !== void 0) {
1851
+ states.push(node.state.selected ? "selected" : "not selected");
1852
+ }
1853
+ if (node.state.disabled) {
1854
+ states.push("disabled");
1855
+ }
1856
+ if (node.state.invalid) {
1857
+ states.push("invalid");
1858
+ }
1859
+ if (node.state.required) {
1860
+ states.push("required");
1861
+ }
1862
+ if (node.state.readonly) {
1863
+ states.push("read only");
1864
+ }
1865
+ if (node.state.busy) {
1866
+ states.push("busy");
1867
+ }
1868
+ if (node.state.current) {
1869
+ if (node.state.current === "page") {
1870
+ states.push("current page");
1871
+ } else if (node.state.current === "step") {
1872
+ states.push("current step");
1873
+ } else if (node.state.current === "location") {
1874
+ states.push("current location");
1875
+ } else if (node.state.current === "date") {
1876
+ states.push("current date");
1877
+ } else if (node.state.current === "time") {
1878
+ states.push("current time");
1879
+ } else if (node.state.current === "true") {
1880
+ states.push("current");
1881
+ }
1882
+ }
1883
+ if (node.state.grabbed !== void 0) {
1884
+ states.push(node.state.grabbed ? "grabbed" : "not grabbed");
1885
+ }
1886
+ return states.join(", ");
1887
+ }
1888
+
1659
1889
  // src/renderer/audit-renderer.ts
1660
1890
  function renderAuditReport(model, colorize) {
1661
1891
  const report = generateAuditReport(model);
@@ -2193,7 +2423,10 @@ function renderOutput(model, screenReader) {
2193
2423
  renderJAWS(model),
2194
2424
  "",
2195
2425
  "--- VoiceOver ---",
2196
- renderVoiceOver(model)
2426
+ renderVoiceOver(model),
2427
+ "",
2428
+ "--- Narrator ---",
2429
+ renderNarrator(model)
2197
2430
  ].join("\n");
2198
2431
  }
2199
2432
  switch (screenReader) {
@@ -2203,6 +2436,8 @@ function renderOutput(model, screenReader) {
2203
2436
  return renderJAWS(model);
2204
2437
  case "voiceover":
2205
2438
  return renderVoiceOver(model);
2439
+ case "narrator":
2440
+ return renderNarrator(model);
2206
2441
  }
2207
2442
  }
2208
2443
  function analyzeHTML(html, screenReader, selector) {
@@ -2213,14 +2448,16 @@ function analyzeHTML(html, screenReader, selector) {
2213
2448
  }
2214
2449
  const warnings = parseWarnings.map((w) => w.message);
2215
2450
  if (selector) {
2216
- const { models, warnings: treeWarnings2 } = buildAccessibilityTreeWithSelector(body, selector);
2217
- warnings.push(...treeWarnings2.map((w) => w.message));
2218
- const outputs = models.map((m, i) => {
2219
- const prefix = models.length > 1 ? `=== Element ${i + 1} ===
2451
+ const results = buildAccessibilityTreeWithSelector(body, selector);
2452
+ for (const r of results) {
2453
+ warnings.push(...r.warnings.map((w) => w.message));
2454
+ }
2455
+ const outputs = results.map((r, i) => {
2456
+ const prefix = results.length > 1 ? `=== Element ${i + 1} ===
2220
2457
  ` : "";
2221
- return prefix + renderOutput(m, screenReader);
2458
+ return prefix + renderOutput(r.model, screenReader);
2222
2459
  });
2223
- return { output: outputs.join("\n\n"), warnings, model: models[0] || null };
2460
+ return { output: outputs.join("\n\n"), warnings, model: results[0]?.model || null };
2224
2461
  }
2225
2462
  const { model, warnings: treeWarnings } = buildAccessibilityTree(body);
2226
2463
  warnings.push(...treeWarnings.map((w) => w.message));
@@ -2232,10 +2469,10 @@ var server = new McpServer({
2232
2469
  });
2233
2470
  server.tool(
2234
2471
  "analyze_html",
2235
- "Predict how screen readers (NVDA, JAWS, VoiceOver) will announce HTML content. Returns the predicted speech output line by line.",
2472
+ "Predict how screen readers (NVDA, JAWS, VoiceOver, Narrator) will announce HTML content. Returns the predicted speech output line by line.",
2236
2473
  {
2237
2474
  html: z.string().describe("The HTML content to analyze"),
2238
- screen_reader: z.enum(["nvda", "jaws", "voiceover", "all"]).default("all").describe("Which screen reader to simulate (default: all)"),
2475
+ screen_reader: z.enum(["nvda", "jaws", "voiceover", "narrator", "all"]).default("all").describe("Which screen reader to simulate (default: all)"),
2239
2476
  selector: z.string().optional().describe("Optional CSS selector to focus analysis on specific elements")
2240
2477
  },
2241
2478
  async ({ html, screen_reader, selector }) => {
@@ -2276,14 +2513,16 @@ server.tool(
2276
2513
  let model;
2277
2514
  const warnings = parseWarnings.map((w) => w.message);
2278
2515
  if (selector) {
2279
- const { models, warnings: treeWarnings } = buildAccessibilityTreeWithSelector(body, selector);
2280
- warnings.push(...treeWarnings.map((w) => w.message));
2281
- if (models.length === 0) {
2516
+ const results = buildAccessibilityTreeWithSelector(body, selector);
2517
+ for (const r of results) {
2518
+ warnings.push(...r.warnings.map((w) => w.message));
2519
+ }
2520
+ if (results.length === 0) {
2282
2521
  return {
2283
2522
  content: [{ type: "text", text: `No elements found matching selector: ${selector}` }]
2284
2523
  };
2285
2524
  }
2286
- model = models[0];
2525
+ model = results[0].model;
2287
2526
  } else {
2288
2527
  const result = buildAccessibilityTree(body);
2289
2528
  warnings.push(...result.warnings.map((w) => w.message));
@@ -2326,20 +2565,20 @@ server.tool(
2326
2565
  let beforeModel;
2327
2566
  let afterModel;
2328
2567
  if (selector) {
2329
- const beforeResult = buildAccessibilityTreeWithSelector(beforeDoc.document.body, selector);
2330
- const afterResult = buildAccessibilityTreeWithSelector(afterDoc.document.body, selector);
2331
- if (beforeResult.models.length === 0 || afterResult.models.length === 0) {
2568
+ const beforeResults = buildAccessibilityTreeWithSelector(beforeDoc.document.body, selector);
2569
+ const afterResults = buildAccessibilityTreeWithSelector(afterDoc.document.body, selector);
2570
+ if (beforeResults.length === 0 || afterResults.length === 0) {
2332
2571
  return {
2333
2572
  content: [{ type: "text", text: `No elements found matching selector: ${selector}` }]
2334
2573
  };
2335
2574
  }
2336
- beforeModel = beforeResult.models[0];
2337
- afterModel = afterResult.models[0];
2575
+ beforeModel = beforeResults[0].model;
2576
+ afterModel = afterResults[0].model;
2338
2577
  } else {
2339
2578
  beforeModel = buildAccessibilityTree(beforeDoc.document.body).model;
2340
2579
  afterModel = buildAccessibilityTree(afterDoc.document.body).model;
2341
2580
  }
2342
- const diff = diffAccessibilityTrees(beforeModel, afterModel);
2581
+ const diff = diffAccessibilityTrees(beforeModel.root, afterModel.root);
2343
2582
  if (diff.changes.length === 0) {
2344
2583
  return {
2345
2584
  content: [{ type: "text", text: "No accessibility changes detected between the two versions." }]