@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/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
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
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(
|
|
2458
|
+
return prefix + renderOutput(r.model, screenReader);
|
|
2222
2459
|
});
|
|
2223
|
-
return { output: outputs.join("\n\n"), warnings, model:
|
|
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
|
|
2280
|
-
|
|
2281
|
-
|
|
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 =
|
|
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
|
|
2330
|
-
const
|
|
2331
|
-
if (
|
|
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 =
|
|
2337
|
-
afterModel =
|
|
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." }]
|