@reticular/speakable 1.1.1 → 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/dist/mcp.js CHANGED
@@ -15,8 +15,15 @@ var ParsingError = class extends Error {
15
15
  this.name = "ParsingError";
16
16
  }
17
17
  };
18
+ var MAX_INPUT_SIZE = 10 * 1024 * 1024;
18
19
  function parseHTML(html) {
19
20
  const warnings = [];
21
+ const inputSize = Buffer.byteLength(html, "utf-8");
22
+ if (inputSize > MAX_INPUT_SIZE) {
23
+ throw new ParsingError(
24
+ `Input exceeds maximum size of ${MAX_INPUT_SIZE / (1024 * 1024)} MB (received ${(inputSize / (1024 * 1024)).toFixed(1)} MB). Use CSS selectors to analyze specific sections of large documents.`
25
+ );
26
+ }
20
27
  try {
21
28
  const dom = new JSDOM(html, {
22
29
  contentType: "text/html",
@@ -1649,6 +1656,236 @@ function formatStatesVoiceOver(node) {
1649
1656
  return states.join(", ");
1650
1657
  }
1651
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
+
1652
1889
  // src/renderer/audit-renderer.ts
1653
1890
  function renderAuditReport(model, colorize) {
1654
1891
  const report = generateAuditReport(model);
@@ -2186,7 +2423,10 @@ function renderOutput(model, screenReader) {
2186
2423
  renderJAWS(model),
2187
2424
  "",
2188
2425
  "--- VoiceOver ---",
2189
- renderVoiceOver(model)
2426
+ renderVoiceOver(model),
2427
+ "",
2428
+ "--- Narrator ---",
2429
+ renderNarrator(model)
2190
2430
  ].join("\n");
2191
2431
  }
2192
2432
  switch (screenReader) {
@@ -2196,6 +2436,8 @@ function renderOutput(model, screenReader) {
2196
2436
  return renderJAWS(model);
2197
2437
  case "voiceover":
2198
2438
  return renderVoiceOver(model);
2439
+ case "narrator":
2440
+ return renderNarrator(model);
2199
2441
  }
2200
2442
  }
2201
2443
  function analyzeHTML(html, screenReader, selector) {
@@ -2206,14 +2448,16 @@ function analyzeHTML(html, screenReader, selector) {
2206
2448
  }
2207
2449
  const warnings = parseWarnings.map((w) => w.message);
2208
2450
  if (selector) {
2209
- const { models, warnings: treeWarnings2 } = buildAccessibilityTreeWithSelector(body, selector);
2210
- warnings.push(...treeWarnings2.map((w) => w.message));
2211
- const outputs = models.map((m, i) => {
2212
- 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} ===
2213
2457
  ` : "";
2214
- return prefix + renderOutput(m, screenReader);
2458
+ return prefix + renderOutput(r.model, screenReader);
2215
2459
  });
2216
- return { output: outputs.join("\n\n"), warnings, model: models[0] || null };
2460
+ return { output: outputs.join("\n\n"), warnings, model: results[0]?.model || null };
2217
2461
  }
2218
2462
  const { model, warnings: treeWarnings } = buildAccessibilityTree(body);
2219
2463
  warnings.push(...treeWarnings.map((w) => w.message));
@@ -2225,10 +2469,10 @@ var server = new McpServer({
2225
2469
  });
2226
2470
  server.tool(
2227
2471
  "analyze_html",
2228
- "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.",
2229
2473
  {
2230
2474
  html: z.string().describe("The HTML content to analyze"),
2231
- 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)"),
2232
2476
  selector: z.string().optional().describe("Optional CSS selector to focus analysis on specific elements")
2233
2477
  },
2234
2478
  async ({ html, screen_reader, selector }) => {
@@ -2269,14 +2513,16 @@ server.tool(
2269
2513
  let model;
2270
2514
  const warnings = parseWarnings.map((w) => w.message);
2271
2515
  if (selector) {
2272
- const { models, warnings: treeWarnings } = buildAccessibilityTreeWithSelector(body, selector);
2273
- warnings.push(...treeWarnings.map((w) => w.message));
2274
- 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) {
2275
2521
  return {
2276
2522
  content: [{ type: "text", text: `No elements found matching selector: ${selector}` }]
2277
2523
  };
2278
2524
  }
2279
- model = models[0];
2525
+ model = results[0].model;
2280
2526
  } else {
2281
2527
  const result = buildAccessibilityTree(body);
2282
2528
  warnings.push(...result.warnings.map((w) => w.message));
@@ -2319,20 +2565,20 @@ server.tool(
2319
2565
  let beforeModel;
2320
2566
  let afterModel;
2321
2567
  if (selector) {
2322
- const beforeResult = buildAccessibilityTreeWithSelector(beforeDoc.document.body, selector);
2323
- const afterResult = buildAccessibilityTreeWithSelector(afterDoc.document.body, selector);
2324
- 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) {
2325
2571
  return {
2326
2572
  content: [{ type: "text", text: `No elements found matching selector: ${selector}` }]
2327
2573
  };
2328
2574
  }
2329
- beforeModel = beforeResult.models[0];
2330
- afterModel = afterResult.models[0];
2575
+ beforeModel = beforeResults[0].model;
2576
+ afterModel = afterResults[0].model;
2331
2577
  } else {
2332
2578
  beforeModel = buildAccessibilityTree(beforeDoc.document.body).model;
2333
2579
  afterModel = buildAccessibilityTree(afterDoc.document.body).model;
2334
2580
  }
2335
- const diff = diffAccessibilityTrees(beforeModel, afterModel);
2581
+ const diff = diffAccessibilityTrees(beforeModel.root, afterModel.root);
2336
2582
  if (diff.changes.length === 0) {
2337
2583
  return {
2338
2584
  content: [{ type: "text", text: "No accessibility changes detected between the two versions." }]