@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/README.md +6 -5
- package/dist/cli.js +257 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +406 -1
- package/dist/index.js +2194 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +265 -19
- package/dist/mcp.js.map +1 -1
- package/package.json +12 -4
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
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
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(
|
|
2458
|
+
return prefix + renderOutput(r.model, screenReader);
|
|
2215
2459
|
});
|
|
2216
|
-
return { output: outputs.join("\n\n"), warnings, model:
|
|
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
|
|
2273
|
-
|
|
2274
|
-
|
|
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 =
|
|
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
|
|
2323
|
-
const
|
|
2324
|
-
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) {
|
|
2325
2571
|
return {
|
|
2326
2572
|
content: [{ type: "text", text: `No elements found matching selector: ${selector}` }]
|
|
2327
2573
|
};
|
|
2328
2574
|
}
|
|
2329
|
-
beforeModel =
|
|
2330
|
-
afterModel =
|
|
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." }]
|