screen-reader-mirror 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Lanzon and screen-reader-mirror contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,122 @@
1
- # Temporary Holding Version
1
+ # screen-reader-mirror
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > See what a screen reader interprets from your UI — zero install, client-side, no data leaves the
4
+ > machine.
5
+
6
+ The shared engine behind [screen-reader-mirror](https://github.com/Koshux/screen-reader-mirror).
7
+ `linearize(element)` walks a DOM subtree and returns the **reading order a screen reader would
8
+ traverse**: each node's WAI-ARIA role and accessible name, with assistive-tech-hidden content
9
+ removed and the common encoding mistakes flagged (a styled `<span>` that only *looks* like a
10
+ heading, a button with no name, an image with no `alt`). A sighted developer can see the
11
+ screen-reader view without a screen reader installed.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install screen-reader-mirror
17
+ # or
18
+ pnpm add screen-reader-mirror
19
+ ```
20
+
21
+ Or straight from a CDN — ESM, no build step, no account:
22
+
23
+ ```html
24
+ <script type="module">
25
+ import { linearize } from "https://cdn.jsdelivr.net/npm/screen-reader-mirror/+esm";
26
+ // or: https://unpkg.com/screen-reader-mirror?module
27
+ console.table(linearize(document.body));
28
+ </script>
29
+ ```
30
+
31
+ ## Quickstart
32
+
33
+ ```html
34
+ <main>
35
+ <h1>Orders</h1>
36
+ <p>3 open, 1 overdue</p>
37
+ <span class="h2">Recent</span>
38
+ <button><svg aria-hidden="true"></svg></button>
39
+ <img src="chart.png">
40
+ <a href="/orders/42">Details</a>
41
+ </main>
42
+ ```
43
+
44
+ ```ts
45
+ import { linearize } from "screen-reader-mirror";
46
+
47
+ const nodes = linearize(document.querySelector("main")!);
48
+ ```
49
+
50
+ `nodes` is plain, serialisable data — one entry per thing a screen reader would land on, in order:
51
+
52
+ ```jsonc
53
+ [
54
+ { "role": "main", "name": "", "ref": { "tag": "main" } },
55
+ { "role": "heading", "name": "Orders", "ref": { "tag": "h1" }, "level": 1 },
56
+ { "role": "text", "name": "3 open, 1 overdue" },
57
+ { "role": "text", "name": "Recent" }, // ← styled span: NOT a heading
58
+ { "role": "button", "name": "", "ref": { "tag": "button" }, "flags": { "unnamed": true } },
59
+ { "role": "img", "name": "", "ref": { "tag": "img" }, "flags": { "missingAlt": true } },
60
+ { "role": "link", "name": "Details", "ref": { "tag": "a" } }
61
+ ]
62
+ ```
63
+
64
+ Reading it:
65
+
66
+ - **`role`** is the computed WAI-ARIA role (explicit `role` wins; otherwise the *conditional*
67
+ implicit role — `<a>` is a `link` only with `href`, `<li>` is a `listitem` only inside a list,
68
+ an unnamed `<section>` is not a landmark). Bare text runs are `"text"`.
69
+ - **`name`** is the accessible name per AccName 1.2. An element whose name comes from its content
70
+ (heading, link, cell…) carries that text in `name`; it is not read twice. Nested elements
71
+ (the link inside a heading, the image inside a button) are still emitted after their parent.
72
+ - **`level`** is the heading level (`<h1>`–`<h6>`, or `aria-level`; `role="heading"` defaults to 2).
73
+ - **`flags.unnamed`** — an interactive control with no accessible name.
74
+ **`flags.missingAlt`** — an `<img>` with no `alt` attribute at all (`alt=""` is treated as
75
+ decorative and omitted, as a screen reader would).
76
+ - Content hidden from assistive tech (`aria-hidden="true"`, `hidden`, `display:none`,
77
+ `visibility:hidden`) is excluded along with its subtree.
78
+
79
+ ## API
80
+
81
+ ```ts
82
+ function linearize(root: Element, options?: LinearizeOptions): InterpretationNode[];
83
+
84
+ interface InterpretationNode {
85
+ role: string; // computed ARIA role, or "text"
86
+ name: string; // accessible name (may be "")
87
+ level?: number; // headings only
88
+ flags?: { unnamed?: boolean; missingAlt?: boolean };
89
+ ref?: { tag: string }; // serialisable pointer — never a live node
90
+ }
91
+
92
+ interface LinearizeOptions {
93
+ /** Override the computed-style source (for jsdom/happy-dom or non-browser hosts). */
94
+ getComputedStyle?: (el: Element) => CSSStyleDeclaration | null | undefined;
95
+ }
96
+ ```
97
+
98
+ Reading order follows **DOM order**, as screen readers do — CSS reordering (`flex`/`grid` `order`,
99
+ positioning) is intentionally not reflected. Shadow DOM, iframes, live regions and per-screen-reader
100
+ phrasing (NVDA / JAWS / VoiceOver verbosity) are tracked follow-ups on the
101
+ [roadmap](https://github.com/Koshux/screen-reader-mirror/blob/main/docs/project/board.md).
102
+
103
+ ## Guarantees
104
+
105
+ - **No network calls — ever.** The engine is pure computation over the `Element` you pass in,
106
+ enforced by lint and a commit-time hook. Safe for regulated, EU and internal pages.
107
+ - **Tree-shakeable ESM**, `sideEffects: false`, TypeScript types included. Two runtime
108
+ dependencies, both permissively licensed: [`dom-accessibility-api`](https://github.com/eps1lon/dom-accessibility-api)
109
+ (MIT, the AccName implementation Testing Library trusts) and
110
+ [`aria-query`](https://github.com/A11yance/aria-query) (Apache-2.0, the WAI-ARIA role data).
111
+ - **MIT licensed**, public npm registry only, no account or API key.
112
+
113
+ ## Status
114
+
115
+ `0.1.x` implements [SPEC-001](https://github.com/Koshux/screen-reader-mirror/blob/main/docs/specs/SPEC-001-core-linearize-foundation.md)
116
+ (reading order, roles, names, levels, flags, hidden-content exclusion) with a golden HTML fixture
117
+ per behaviour. The pure-JS role/name computation is the portable path; hosts that can reach a real
118
+ browser accessibility tree (the upcoming extension) will use it as the higher-fidelity oracle.
119
+
120
+ ## License
121
+
122
+ [MIT](https://github.com/Koshux/screen-reader-mirror/blob/main/LICENSE)
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The interpretation model — what a screen reader would encounter, as plain serialisable data.
3
+ *
4
+ * Every node is plain, JSON-serialisable data with **no live DOM references**, so the output can
5
+ * cross a boundary (content-script → side panel, CLI → report file, worker → UI) without leaking a
6
+ * node handle. This model is the public contract from SPEC-001 §3 (spec-reviewed 2026-06-15).
7
+ */
8
+ /**
9
+ * Conditions worth surfacing to the developer about a node. Each flag is omitted unless true, so an
10
+ * absent `flags` (or `flags.x`) means "not applicable / fine".
11
+ */
12
+ interface NodeFlags {
13
+ /** An interactive (widget-role) element with no accessible name. SPEC-001 AC-5. */
14
+ unnamed?: boolean;
15
+ /** An `<img>` with no `alt` attribute at all — distinct from an intentional `alt=""`. SPEC-001 AC-6. */
16
+ missingAlt?: boolean;
17
+ }
18
+ /**
19
+ * One entry in the linearized, screen-reader reading order.
20
+ *
21
+ * A node is either an **element** (with its computed `role` and accessible `name`) or a **bare run
22
+ * of text** (`role: "text"`). Per the "no double-read" rule (SPEC-001 §3), an element whose role
23
+ * takes its name from content carries that text in `name` and does *not* also produce `text` nodes
24
+ * for the same subtree.
25
+ */
26
+ interface InterpretationNode {
27
+ /**
28
+ * Computed WAI-ARIA role (e.g. `"heading"`, `"link"`, `"button"`), or the literal `"text"` for a
29
+ * bare text run. Resolved per SPEC-001 §3 — an explicit `role` wins; otherwise the *conditional*
30
+ * implicit role (e.g. `<a>` is only a `link` with an `href`) — never a naive tag→role map.
31
+ */
32
+ role: string;
33
+ /** Accessible name per AccName 1.2. May be an empty string (see {@link NodeFlags.unnamed}). */
34
+ name: string;
35
+ /**
36
+ * Heading level, present only when the node is a heading: 1–6 from `<h1>`–`<h6>`, any integer
37
+ * ≥ 1 from an author `aria-level` (which wins over the tag), 2 when neither is given.
38
+ */
39
+ level?: number;
40
+ /** Notable conditions; omitted entirely when there are none. */
41
+ flags?: NodeFlags;
42
+ /** Minimal, serialisable pointer back to the source element — a tag name only, never a live node. */
43
+ ref?: {
44
+ tag: string;
45
+ };
46
+ }
47
+
48
+ /**
49
+ * AT-visibility predicate — SPEC-001 §3 / AC-3.
50
+ *
51
+ * Returns `true` when an element is excluded from the accessibility tree, so the linearizer skips
52
+ * it (and, for the MVP, its subtree). The four exclusion conditions:
53
+ * - `aria-hidden="true"`
54
+ * - the `hidden` attribute (except `hidden="until-found"`, which stays in the tree)
55
+ * - `display: none`
56
+ * - `visibility: hidden` / `collapse`
57
+ *
58
+ * Attribute conditions (`aria-hidden`, `hidden`) are reliable everywhere. The CSS conditions need
59
+ * `getComputedStyle`, which jsdom implements only partially — pass inline styles in tests or use
60
+ * happy-dom / a real browser (SPEC-001 §5). Style is read **defensively**: if it can't be computed
61
+ * (detached node, no view, throwing impl), the element is treated as visible so we never wrongly
62
+ * drop content.
63
+ *
64
+ * Host-agnostic by design: the computed-style source comes from the element's own
65
+ * `ownerDocument.defaultView`, never a global `window` (core makes no global/network assumptions).
66
+ *
67
+ * Note: `visibility:hidden` can be overridden by a descendant (`visibility:visible`); honouring that
68
+ * per-descendant is a deferred refinement (SPEC-001 non-goals) — the MVP treats it as hidden here.
69
+ */
70
+ interface VisibilityOptions {
71
+ /** Override the computed-style source (defaults to the element's own document view). */
72
+ getComputedStyle?: (el: Element) => CSSStyleDeclaration | null | undefined;
73
+ }
74
+
75
+ type LinearizeOptions = VisibilityOptions;
76
+ /**
77
+ * Linearize a DOM subtree into the ordered sequence a screen reader would traverse (SPEC-001).
78
+ *
79
+ * Each element is one of three kinds:
80
+ * - **transparent** (no role, or a silent structural role) — emits nothing, recurses so its text
81
+ * and descendants surface.
82
+ * - **leaf** — a form control / image, or a role whose children are presentational *and* whose
83
+ * name cannot come from content (img, slider, separator, progressbar, …): emits one node and
84
+ * does **not** recurse, because nothing below it is exposed.
85
+ * - **container** — everything else (landmark, list, table, row, cell, link, heading, and also
86
+ * the content widgets button/tab/option/switch whose descendants browsers do keep in the tree):
87
+ * emits a node *and* recurses, so nested elements — the link inside a heading, the missing-alt
88
+ * image inside a button, the button inside a table cell — stay in the reading order.
89
+ *
90
+ * The **no-double-read rule** (SPEC-001 §3): a role that takes its name from content (heading,
91
+ * link, cell, row, button, …) has already consumed its descendant *text* into `name`, so bare
92
+ * text runs inside it are not emitted again — but descendant *elements* with roles of their own
93
+ * still are, and each emitted container starts a fresh text context: an author-named container
94
+ * (list, listitem, group, landmark) nested in a consumed subtree surfaces its own text.
95
+ * Elements hidden from assistive tech are skipped along with their subtree.
96
+ */
97
+ declare function linearize(root: Element, options?: LinearizeOptions): InterpretationNode[];
98
+
99
+ export { linearize };
100
+ export type { InterpretationNode, LinearizeOptions, NodeFlags };
package/dist/index.js ADDED
@@ -0,0 +1,313 @@
1
+ import { roles } from 'aria-query';
2
+ import { computeAccessibleName } from 'dom-accessibility-api';
3
+
4
+ // src/visibility.ts
5
+ function isHiddenFromAT(el, options = {}) {
6
+ if (el.getAttribute("aria-hidden")?.toLowerCase() === "true") return true;
7
+ const hidden = el.getAttribute("hidden");
8
+ if (hidden !== null && hidden.toLowerCase() !== "until-found") return true;
9
+ const style = resolveStyle(el, options);
10
+ if (style) {
11
+ if (style.display === "none") return true;
12
+ if (style.visibility === "hidden" || style.visibility === "collapse") return true;
13
+ }
14
+ return false;
15
+ }
16
+ function resolveStyle(el, options) {
17
+ try {
18
+ if (options.getComputedStyle) return options.getComputedStyle(el) ?? null;
19
+ const view = el.ownerDocument?.defaultView;
20
+ return view ? view.getComputedStyle(el) : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ var roleMap = roles;
26
+ var NAME_FROM_CONTENT_OVERRIDES = { rowgroup: false };
27
+ var PRESENTATIONAL = /* @__PURE__ */ new Set(["presentation", "none"]);
28
+ function resolveRole(el) {
29
+ const explicit = el.getAttribute("role");
30
+ if (explicit) {
31
+ for (const token of explicit.trim().toLowerCase().split(/\s+/)) {
32
+ const def = roleMap.get(token);
33
+ if (!def || def.abstract) continue;
34
+ if (PRESENTATIONAL.has(token) && presentationIsOverridden(el)) return implicitRole(el);
35
+ return token;
36
+ }
37
+ }
38
+ return implicitRole(el);
39
+ }
40
+ function implicitRole(el) {
41
+ const tag = el.tagName.toLowerCase();
42
+ switch (tag) {
43
+ case "a":
44
+ case "area":
45
+ return el.hasAttribute("href") ? "link" : null;
46
+ // generic without href
47
+ case "button":
48
+ return "button";
49
+ case "h1":
50
+ case "h2":
51
+ case "h3":
52
+ case "h4":
53
+ case "h5":
54
+ case "h6":
55
+ return "heading";
56
+ case "nav":
57
+ return "navigation";
58
+ case "main":
59
+ return "main";
60
+ case "header":
61
+ return scopeOf(el) === "body" ? "banner" : null;
62
+ case "footer":
63
+ return scopeOf(el) === "body" ? "contentinfo" : null;
64
+ case "aside":
65
+ return scopeOf(el) !== "sectioning" || hasAccessibleName(el) ? "complementary" : null;
66
+ case "ul":
67
+ case "ol":
68
+ case "menu":
69
+ return "list";
70
+ case "li":
71
+ return el.parentElement && resolveRole(el.parentElement) === "list" ? "listitem" : null;
72
+ case "p":
73
+ return "paragraph";
74
+ case "img":
75
+ return el.getAttribute("alt") === "" && !presentationIsOverridden(el) ? "presentation" : "img";
76
+ case "svg":
77
+ return hasAccessibleName(el) ? "img" : null;
78
+ case "input":
79
+ return inputRole(el);
80
+ case "textarea":
81
+ return "textbox";
82
+ case "select":
83
+ return el.hasAttribute("multiple") || numericSize(el) > 1 ? "listbox" : "combobox";
84
+ case "table":
85
+ return "table";
86
+ case "thead":
87
+ case "tbody":
88
+ case "tfoot":
89
+ return tableIsPresentational(el) ? null : "rowgroup";
90
+ case "tr":
91
+ return tableIsPresentational(el) ? null : "row";
92
+ case "td": {
93
+ const table = ancestorTableRole(el);
94
+ if (table && PRESENTATIONAL.has(table)) return null;
95
+ return table === "grid" || table === "treegrid" ? "gridcell" : "cell";
96
+ }
97
+ case "th": {
98
+ if (tableIsPresentational(el)) return null;
99
+ const scope = el.getAttribute("scope")?.toLowerCase();
100
+ return scope === "row" || scope === "rowgroup" ? "rowheader" : "columnheader";
101
+ }
102
+ case "article":
103
+ return "article";
104
+ case "section":
105
+ return hasAccessibleName(el) ? "region" : null;
106
+ case "form":
107
+ return hasAccessibleName(el) ? "form" : null;
108
+ case "fieldset":
109
+ return "group";
110
+ case "figure":
111
+ return "figure";
112
+ case "dialog":
113
+ return "dialog";
114
+ case "hr":
115
+ return "separator";
116
+ default:
117
+ return null;
118
+ }
119
+ }
120
+ function inputRole(el) {
121
+ const type = (el.getAttribute("type") ?? "text").toLowerCase();
122
+ switch (type) {
123
+ case "button":
124
+ case "submit":
125
+ case "reset":
126
+ case "image":
127
+ case "file":
128
+ return "button";
129
+ case "checkbox":
130
+ return "checkbox";
131
+ case "radio":
132
+ return "radio";
133
+ case "range":
134
+ return "slider";
135
+ case "number":
136
+ return "spinbutton";
137
+ case "search":
138
+ return el.hasAttribute("list") ? "combobox" : "searchbox";
139
+ // HTML-AAM: datalist → combobox
140
+ case "email":
141
+ case "tel":
142
+ case "url":
143
+ case "text":
144
+ return el.hasAttribute("list") ? "combobox" : "textbox";
145
+ case "password":
146
+ return "textbox";
147
+ case "hidden":
148
+ return null;
149
+ // not in the accessibility tree at all
150
+ default:
151
+ return "textbox";
152
+ }
153
+ }
154
+ var GLOBAL_ARIA_ATTRIBUTES = [
155
+ "aria-atomic",
156
+ "aria-busy",
157
+ "aria-controls",
158
+ "aria-current",
159
+ "aria-describedby",
160
+ "aria-details",
161
+ "aria-disabled",
162
+ "aria-dropeffect",
163
+ "aria-errormessage",
164
+ "aria-flowto",
165
+ "aria-grabbed",
166
+ "aria-haspopup",
167
+ "aria-hidden",
168
+ "aria-invalid",
169
+ "aria-keyshortcuts",
170
+ "aria-label",
171
+ "aria-labelledby",
172
+ "aria-live",
173
+ "aria-owns",
174
+ "aria-relevant",
175
+ "aria-roledescription"
176
+ ];
177
+ function presentationIsOverridden(el) {
178
+ return isFocusable(el) || GLOBAL_ARIA_ATTRIBUTES.some((a) => el.hasAttribute(a));
179
+ }
180
+ function isFocusable(el) {
181
+ if (el.hasAttribute("tabindex") || el.hasAttribute("contenteditable")) return true;
182
+ const tag = el.tagName.toLowerCase();
183
+ if ((tag === "a" || tag === "area") && el.hasAttribute("href")) return true;
184
+ if (tag === "button" || tag === "select" || tag === "textarea") return !el.hasAttribute("disabled");
185
+ if (tag === "input") {
186
+ return !el.hasAttribute("disabled") && (el.getAttribute("type") ?? "").toLowerCase() !== "hidden";
187
+ }
188
+ return tag === "summary" || tag === "iframe";
189
+ }
190
+ function ancestorTableRole(el) {
191
+ const table = el.parentElement?.closest("table");
192
+ return table ? resolveRole(table) : null;
193
+ }
194
+ function tableIsPresentational(el) {
195
+ const role = ancestorTableRole(el);
196
+ return role !== null && PRESENTATIONAL.has(role);
197
+ }
198
+ var SECTIONING_CONTENT_TAGS = /* @__PURE__ */ new Set(["article", "aside", "nav", "section"]);
199
+ var SECTIONING_CONTENT_ROLES = /* @__PURE__ */ new Set(["article", "complementary", "navigation", "region"]);
200
+ var SECTIONING_ROOT_TAGS = /* @__PURE__ */ new Set(["blockquote", "details", "dialog", "fieldset", "figure", "td"]);
201
+ function scopeOf(el) {
202
+ for (let p = el.parentElement; p; p = p.parentElement) {
203
+ const tag = p.tagName.toLowerCase();
204
+ if (tag === "body") return "body";
205
+ if (SECTIONING_CONTENT_TAGS.has(tag) || SECTIONING_ROOT_TAGS.has(tag)) return "sectioning";
206
+ if (tag === "main") return "main";
207
+ if (p.hasAttribute("role")) {
208
+ const role = resolveRole(p);
209
+ if (role === "main") return "main";
210
+ if (role && SECTIONING_CONTENT_ROLES.has(role)) return "sectioning";
211
+ }
212
+ }
213
+ return "body";
214
+ }
215
+ function numericSize(el) {
216
+ const n = Number(el.getAttribute("size"));
217
+ return Number.isFinite(n) ? n : 0;
218
+ }
219
+ function accessibleName(el) {
220
+ try {
221
+ return computeAccessibleName(el).replace(/\s+/g, " ").trim();
222
+ } catch {
223
+ return "";
224
+ }
225
+ }
226
+ function hasAccessibleName(el) {
227
+ return accessibleName(el) !== "";
228
+ }
229
+ function takesNameFromContent(role) {
230
+ if (!role) return false;
231
+ const override = NAME_FROM_CONTENT_OVERRIDES[role];
232
+ if (override !== void 0) return override;
233
+ return roleMap.get(role)?.nameFrom?.includes("contents") ?? false;
234
+ }
235
+ function isChildrenPresentational(role) {
236
+ if (!role) return false;
237
+ return roleMap.get(role)?.childrenPresentational ?? false;
238
+ }
239
+ var DUAL_INHERITANCE_WIDGETS = /* @__PURE__ */ new Set(["slider", "spinbutton", "tab", "treeitem", "listbox", "tree"]);
240
+ function isWidget(role) {
241
+ if (!role) return false;
242
+ if (DUAL_INHERITANCE_WIDGETS.has(role)) return true;
243
+ const chains = roleMap.get(role)?.superClass;
244
+ return !!chains && chains.length > 0 && chains.every((chain) => chain.includes("widget") && !chain.includes("composite"));
245
+ }
246
+
247
+ // src/linearize.ts
248
+ var ELEMENT_NODE = 1;
249
+ var TEXT_NODE = 3;
250
+ var LEAF_TAGS = /* @__PURE__ */ new Set(["img", "input", "textarea", "select", "hr"]);
251
+ var TRANSPARENT_ROLES = /* @__PURE__ */ new Set(["paragraph", "presentation", "none", "generic"]);
252
+ var SVG_NS = "http://www.w3.org/2000/svg";
253
+ var SVG_METADATA_TAGS = /* @__PURE__ */ new Set(["title", "desc", "metadata"]);
254
+ function linearize(root, options = {}) {
255
+ const out = [];
256
+ emitElement(root, out, options, false);
257
+ return out;
258
+ }
259
+ function emitElement(el, out, options, textConsumed) {
260
+ if (isHiddenFromAT(el, options)) return;
261
+ const tag = el.tagName.toLowerCase();
262
+ if (el.namespaceURI === SVG_NS && SVG_METADATA_TAGS.has(tag)) return;
263
+ const role = resolveRole(el);
264
+ if (role === "presentation" && tag === "img") return;
265
+ if (role === null || TRANSPARENT_ROLES.has(role)) {
266
+ walkChildren(el, out, options, textConsumed);
267
+ return;
268
+ }
269
+ out.push(buildNode(el, role, tag));
270
+ const consumesText = takesNameFromContent(role);
271
+ if (LEAF_TAGS.has(tag) || isChildrenPresentational(role) && !consumesText) return;
272
+ walkChildren(el, out, options, consumesText);
273
+ }
274
+ function walkChildren(node, out, options, textConsumed) {
275
+ node.childNodes.forEach((child) => {
276
+ if (child.nodeType === TEXT_NODE) {
277
+ if (textConsumed) return;
278
+ const text = collapseWhitespace(child.textContent ?? "");
279
+ if (text) out.push({ role: "text", name: text });
280
+ return;
281
+ }
282
+ if (child.nodeType === ELEMENT_NODE) emitElement(child, out, options, textConsumed);
283
+ });
284
+ }
285
+ function buildNode(el, role, tag) {
286
+ const name = accessibleName(el);
287
+ const node = { role, name, ref: { tag } };
288
+ const level = headingLevel(el, role);
289
+ if (level !== void 0) node.level = level;
290
+ const flags = computeFlags(el, role, tag, name);
291
+ if (flags) node.flags = flags;
292
+ return node;
293
+ }
294
+ function headingLevel(el, role) {
295
+ if (role !== "heading") return void 0;
296
+ const ariaLevel = Number(el.getAttribute("aria-level"));
297
+ if (Number.isInteger(ariaLevel) && ariaLevel >= 1) return ariaLevel;
298
+ const tagMatch = /^h([1-6])$/.exec(el.tagName.toLowerCase());
299
+ return tagMatch ? Number(tagMatch[1]) : 2;
300
+ }
301
+ function computeFlags(el, role, tag, name) {
302
+ const flags = {};
303
+ if (isWidget(role) && name === "") flags.unnamed = true;
304
+ if (tag === "img" && !el.hasAttribute("alt")) flags.missingAlt = true;
305
+ return Object.keys(flags).length > 0 ? flags : void 0;
306
+ }
307
+ function collapseWhitespace(s) {
308
+ return s.replace(/\s+/g, " ").trim();
309
+ }
310
+
311
+ export { linearize };
312
+ //# sourceMappingURL=index.js.map
313
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/visibility.ts","../src/roles.ts","../src/linearize.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * AT-visibility predicate — SPEC-001 §3 / AC-3.\n *\n * Returns `true` when an element is excluded from the accessibility tree, so the linearizer skips\n * it (and, for the MVP, its subtree). The four exclusion conditions:\n * - `aria-hidden=\"true\"`\n * - the `hidden` attribute (except `hidden=\"until-found\"`, which stays in the tree)\n * - `display: none`\n * - `visibility: hidden` / `collapse`\n *\n * Attribute conditions (`aria-hidden`, `hidden`) are reliable everywhere. The CSS conditions need\n * `getComputedStyle`, which jsdom implements only partially — pass inline styles in tests or use\n * happy-dom / a real browser (SPEC-001 §5). Style is read **defensively**: if it can't be computed\n * (detached node, no view, throwing impl), the element is treated as visible so we never wrongly\n * drop content.\n *\n * Host-agnostic by design: the computed-style source comes from the element's own\n * `ownerDocument.defaultView`, never a global `window` (core makes no global/network assumptions).\n *\n * Note: `visibility:hidden` can be overridden by a descendant (`visibility:visible`); honouring that\n * per-descendant is a deferred refinement (SPEC-001 non-goals) — the MVP treats it as hidden here.\n */\nexport interface VisibilityOptions {\n /** Override the computed-style source (defaults to the element's own document view). */\n getComputedStyle?: (el: Element) => CSSStyleDeclaration | null | undefined;\n}\n\nexport function isHiddenFromAT(el: Element, options: VisibilityOptions = {}): boolean {\n // 1. aria-hidden — case-insensitive \"true\" token (matches browser/Blink behaviour; any non-\"true\"\n // value, including \"false\" and the empty string, does not hide).\n if (el.getAttribute(\"aria-hidden\")?.toLowerCase() === \"true\") return true;\n\n // 2. the `hidden` attribute, except hidden=\"until-found\" (visually hidden but still in the tree).\n const hidden = el.getAttribute(\"hidden\");\n if (hidden !== null && hidden.toLowerCase() !== \"until-found\") return true;\n\n // 3 & 4. CSS-based hiding, read defensively.\n const style = resolveStyle(el, options);\n if (style) {\n if (style.display === \"none\") return true;\n if (style.visibility === \"hidden\" || style.visibility === \"collapse\") return true;\n }\n return false;\n}\n\nfunction resolveStyle(el: Element, options: VisibilityOptions): CSSStyleDeclaration | null {\n try {\n if (options.getComputedStyle) return options.getComputedStyle(el) ?? null;\n const view = el.ownerDocument?.defaultView;\n return view ? view.getComputedStyle(el) : null;\n } catch {\n return null; // fail-open: can't compute → treat as visible\n }\n}\n","import { roles } from \"aria-query\";\nimport { computeAccessibleName } from \"dom-accessibility-api\";\n\n/**\n * Role resolution for the interpretation (SPEC-001 §3).\n *\n * `dom-accessibility-api` computes the *name*, not the role. `aria-query` gives role *definitions*\n * (used here for the name-from-content, children-presentational, abstract and widget\n * determinations), but the implicit role of an HTML element is **conditional** — so the tag→role\n * mapping below applies those conditions (e.g. `<a>` is a link only with `href`, `<li>` is a\n * `listitem` only inside a list, `<section>` is a `region` only when named, `<header>` is a\n * `banner` only when scoped to the body) rather than a naive lookup. This is a **curated** resolver\n * covering the elements the engine exercises today; broadening it to the full HTML-AAM mapping\n * (and using `Element.getComputedRole()` as a host oracle) is a tracked follow-up.\n */\n\ntype RoleDef = {\n abstract?: boolean;\n childrenPresentational?: boolean;\n nameFrom?: readonly string[];\n superClass?: ReadonlyArray<readonly string[]>;\n};\nconst roleMap = roles as unknown as { get(key: string): RoleDef | undefined };\n\n/**\n * Where `aria-query` (5.x) still carries ARIA 1.1 data, ARIA 1.2 wins. `rowgroup` is\n * \"Name From: author\" in ARIA 1.2; with the 1.1 value, bare text directly inside a\n * `role=\"rowgroup\"` container would be wrongly suppressed as \"already in the name\".\n */\nconst NAME_FROM_CONTENT_OVERRIDES: Readonly<Record<string, boolean>> = { rowgroup: false };\n\nconst PRESENTATIONAL = new Set([\"presentation\", \"none\"]);\n\n/**\n * Resolve an element's role: explicit `role` wins; else the conditional implicit role; else null\n * (generic / no role).\n *\n * WAI-ARIA 1.2 role-attribute processing: the value is a space-separated list of fallback tokens\n * and the user agent uses the **first token that names a non-abstract role** — so `role=\"foo\n * button\"` is a button, and an abstract role like `role=\"widget\"` is ignored.\n */\nexport function resolveRole(el: Element): string | null {\n const explicit = el.getAttribute(\"role\");\n if (explicit) {\n for (const token of explicit.trim().toLowerCase().split(/\\s+/)) {\n const def = roleMap.get(token);\n if (!def || def.abstract) continue;\n // ARIA 1.2 presentation-role conflict resolution: a focusable element, or one carrying a\n // global ARIA attribute, ignores `presentation`/`none` and keeps its implicit role.\n if (PRESENTATIONAL.has(token) && presentationIsOverridden(el)) return implicitRole(el);\n return token;\n }\n }\n return implicitRole(el);\n}\n\nfunction implicitRole(el: Element): string | null {\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"a\":\n case \"area\":\n return el.hasAttribute(\"href\") ? \"link\" : null; // generic without href\n case \"button\":\n return \"button\";\n case \"h1\":\n case \"h2\":\n case \"h3\":\n case \"h4\":\n case \"h5\":\n case \"h6\":\n return \"heading\";\n case \"nav\":\n return \"navigation\";\n case \"main\":\n return \"main\";\n case \"header\":\n // ARIA in HTML: banner only when scoped to the body — not inside main, sectioning content\n // (article/aside/nav/section or those roles) or a sectioning root (dialog, figure, td, …).\n return scopeOf(el) === \"body\" ? \"banner\" : null;\n case \"footer\":\n return scopeOf(el) === \"body\" ? \"contentinfo\" : null;\n case \"aside\":\n // ARIA in HTML: complementary when scoped to body or main; inside sectioning content or a\n // sectioning root only when it has an accessible name, else generic.\n return scopeOf(el) !== \"sectioning\" || hasAccessibleName(el) ? \"complementary\" : null;\n case \"ul\":\n case \"ol\":\n case \"menu\":\n return \"list\";\n case \"li\":\n // HTML-AAM / ARIA in HTML: `listitem` only as the child of an element exposed as a list\n // (`<ul>`/`<ol>`/`<menu>` or `role=\"list\"`); an orphan `<li>` is generic. Going through\n // `resolveRole` on the parent also honours `<ul role=\"presentation\">` (ARIA presentation\n // inheritance for required owned elements).\n return el.parentElement && resolveRole(el.parentElement) === \"list\" ? \"listitem\" : null;\n case \"p\":\n return \"paragraph\";\n case \"img\":\n // alt=\"\" = decorative (presentation) — unless focusable / global ARIA re-exposes it.\n return el.getAttribute(\"alt\") === \"\" && !presentationIsOverridden(el) ? \"presentation\" : \"img\";\n case \"svg\":\n // SVG-AAM: the root is an image-like object named by its <title> / aria-label. Without a\n // name it is treated as decorative here (its <title>/<desc> are never read as text).\n return hasAccessibleName(el) ? \"img\" : null;\n case \"input\":\n return inputRole(el);\n case \"textarea\":\n return \"textbox\";\n case \"select\":\n return el.hasAttribute(\"multiple\") || numericSize(el) > 1 ? \"listbox\" : \"combobox\";\n case \"table\":\n return \"table\";\n case \"thead\":\n case \"tbody\":\n case \"tfoot\":\n return tableIsPresentational(el) ? null : \"rowgroup\";\n case \"tr\":\n return tableIsPresentational(el) ? null : \"row\";\n case \"td\": {\n const table = ancestorTableRole(el);\n if (table && PRESENTATIONAL.has(table)) return null;\n return table === \"grid\" || table === \"treegrid\" ? \"gridcell\" : \"cell\"; // HTML-AAM\n }\n case \"th\": {\n if (tableIsPresentational(el)) return null;\n // HTML-AAM: scope=\"row\"/\"rowgroup\" → rowheader; otherwise columnheader (the HTML-AAM\n // \"in a thead / first cell\" heuristics for scope-less <th> are a simplification here).\n const scope = el.getAttribute(\"scope\")?.toLowerCase();\n return scope === \"row\" || scope === \"rowgroup\" ? \"rowheader\" : \"columnheader\";\n }\n case \"article\":\n return \"article\";\n case \"section\":\n // ARIA in HTML: `region` only with an accessible name, otherwise generic.\n return hasAccessibleName(el) ? \"region\" : null;\n case \"form\":\n // HTML-AAM: an unnamed `<form>` is not exposed as a landmark.\n return hasAccessibleName(el) ? \"form\" : null;\n case \"fieldset\":\n return \"group\";\n case \"figure\":\n return \"figure\";\n case \"dialog\":\n return \"dialog\";\n case \"hr\":\n return \"separator\";\n default:\n return null; // generic / no implicit role\n }\n}\n\nfunction inputRole(el: Element): string | null {\n const type = (el.getAttribute(\"type\") ?? \"text\").toLowerCase();\n switch (type) {\n case \"button\":\n case \"submit\":\n case \"reset\":\n case \"image\":\n case \"file\": // HTML-AAM: no ARIA role; browsers expose the upload control as a button\n return \"button\";\n case \"checkbox\":\n return \"checkbox\";\n case \"radio\":\n return \"radio\";\n case \"range\":\n return \"slider\";\n case \"number\":\n return \"spinbutton\";\n case \"search\":\n return el.hasAttribute(\"list\") ? \"combobox\" : \"searchbox\"; // HTML-AAM: datalist → combobox\n case \"email\":\n case \"tel\":\n case \"url\":\n case \"text\":\n return el.hasAttribute(\"list\") ? \"combobox\" : \"textbox\";\n case \"password\": // no ARIA role *name*, but every platform exposes it as a (protected) textbox\n return \"textbox\";\n case \"hidden\":\n return null; // not in the accessibility tree at all\n default:\n return \"textbox\"; // date/time/color/month/week: HTML-AAM \"no corresponding role\" — approximation\n }\n}\n\n/** Global ARIA states/properties (ARIA 1.2 §6.5) — any of these re-exposes a presentational element. */\nconst GLOBAL_ARIA_ATTRIBUTES = [\n \"aria-atomic\",\n \"aria-busy\",\n \"aria-controls\",\n \"aria-current\",\n \"aria-describedby\",\n \"aria-details\",\n \"aria-disabled\",\n \"aria-dropeffect\",\n \"aria-errormessage\",\n \"aria-flowto\",\n \"aria-grabbed\",\n \"aria-haspopup\",\n \"aria-hidden\",\n \"aria-invalid\",\n \"aria-keyshortcuts\",\n \"aria-label\",\n \"aria-labelledby\",\n \"aria-live\",\n \"aria-owns\",\n \"aria-relevant\",\n \"aria-roledescription\",\n];\n\nfunction presentationIsOverridden(el: Element): boolean {\n return isFocusable(el) || GLOBAL_ARIA_ATTRIBUTES.some((a) => el.hasAttribute(a));\n}\n\n/** Host-language focusability (the cases that matter for conflict resolution, not a full tabbing model). */\nfunction isFocusable(el: Element): boolean {\n if (el.hasAttribute(\"tabindex\") || el.hasAttribute(\"contenteditable\")) return true;\n const tag = el.tagName.toLowerCase();\n if ((tag === \"a\" || tag === \"area\") && el.hasAttribute(\"href\")) return true;\n if (tag === \"button\" || tag === \"select\" || tag === \"textarea\") return !el.hasAttribute(\"disabled\");\n if (tag === \"input\") {\n return !el.hasAttribute(\"disabled\") && (el.getAttribute(\"type\") ?? \"\").toLowerCase() !== \"hidden\";\n }\n return tag === \"summary\" || tag === \"iframe\";\n}\n\n/** Resolved role of the closest ancestor `<table>`, or null when there is none. */\nfunction ancestorTableRole(el: Element): string | null {\n const table = el.parentElement?.closest(\"table\");\n return table ? resolveRole(table) : null;\n}\n\n/**\n * ARIA 1.2 presentation inheritance: when a `table` is presentational, its required owned\n * structure (`thead`/`tbody`/`tfoot`/`tr`/`td`/`th`) without an explicit role is presentational\n * too — the classic layout table. (`<li>` under a presentational list is handled by the `li` case.)\n */\nfunction tableIsPresentational(el: Element): boolean {\n const role = ancestorTableRole(el);\n return role !== null && PRESENTATIONAL.has(role);\n}\n\n// HTML sectioning content, the landmark roles ARIA in HTML scopes the same way, and the sectioning\n// roots other than body (HTML §4.3.11).\nconst SECTIONING_CONTENT_TAGS = new Set([\"article\", \"aside\", \"nav\", \"section\"]);\nconst SECTIONING_CONTENT_ROLES = new Set([\"article\", \"complementary\", \"navigation\", \"region\"]);\nconst SECTIONING_ROOT_TAGS = new Set([\"blockquote\", \"details\", \"dialog\", \"fieldset\", \"figure\", \"td\"]);\n\n/**\n * ARIA in HTML scoping for `<header>`/`<footer>`/`<aside>`: the nearest scoping ancestor is the\n * body, `<main>` (or `role=\"main\"`), or sectioning content / a sectioning root.\n */\nfunction scopeOf(el: Element): \"body\" | \"main\" | \"sectioning\" {\n for (let p = el.parentElement; p; p = p.parentElement) {\n const tag = p.tagName.toLowerCase();\n if (tag === \"body\") return \"body\";\n if (SECTIONING_CONTENT_TAGS.has(tag) || SECTIONING_ROOT_TAGS.has(tag)) return \"sectioning\";\n if (tag === \"main\") return \"main\";\n if (p.hasAttribute(\"role\")) {\n const role = resolveRole(p);\n if (role === \"main\") return \"main\";\n if (role && SECTIONING_CONTENT_ROLES.has(role)) return \"sectioning\";\n }\n }\n return \"body\";\n}\n\nfunction numericSize(el: Element): number {\n const n = Number(el.getAttribute(\"size\"));\n return Number.isFinite(n) ? n : 0;\n}\n\n/**\n * Accessible name per AccName 1.2 via `dom-accessibility-api` (never hand-rolled),\n * whitespace-collapsed. Read defensively: a name-computation hiccup must never drop a node or\n * promote/demote a role by accident, so it yields \"\" instead of throwing.\n */\nexport function accessibleName(el: Element): string {\n try {\n return computeAccessibleName(el).replace(/\\s+/g, \" \").trim();\n } catch {\n return \"\";\n }\n}\n\nfunction hasAccessibleName(el: Element): boolean {\n return accessibleName(el) !== \"\";\n}\n\n/** True if the role takes its accessible name from its descendant content (AccName step 2F). */\nexport function takesNameFromContent(role: string | null): boolean {\n if (!role) return false;\n const override = NAME_FROM_CONTENT_OVERRIDES[role];\n if (override !== undefined) return override;\n return roleMap.get(role)?.nameFrom?.includes(\"contents\") ?? false;\n}\n\n/**\n * True if the role's DOM descendants are presentational (WAI-ARIA \"Children Presentational: True\"\n * — button, img, checkbox, tab, slider, …): user agents are not expected to expose them.\n */\nexport function isChildrenPresentational(role: string | null): boolean {\n if (!role) return false;\n return roleMap.get(role)?.childrenPresentational ?? false;\n}\n\n/**\n * Dual-inheritance roles (structure *and* widget in ARIA's superclass graph) that are genuinely\n * operable controls and so count as widgets for the \"unnamed control\" flag. The other\n * dual-inheritance roles — `row`, `gridcell`, `columnheader`, `rowheader`, `grid`, `progressbar`,\n * `scrollbar` — are table/structure roles a screen reader does not present as controls, and an\n * empty `<th>` corner cell or spacer `<tr>` must not be reported as an unnamed control.\n */\nconst DUAL_INHERITANCE_WIDGETS = new Set([\"slider\", \"spinbutton\", \"tab\", \"treeitem\", \"listbox\", \"tree\"]);\n\n/**\n * True if the role is an interactive widget — used for the \"unnamed control\" flag (AC-5). Pure\n * widget roles (every superclass chain goes through `widget`) count, except composite containers\n * such as `tablist` whose name is optional; the operable dual-inheritance roles above are added.\n */\nexport function isWidget(role: string | null): boolean {\n if (!role) return false;\n if (DUAL_INHERITANCE_WIDGETS.has(role)) return true;\n const chains = roleMap.get(role)?.superClass;\n return (\n !!chains &&\n chains.length > 0 &&\n chains.every((chain) => chain.includes(\"widget\") && !chain.includes(\"composite\"))\n );\n}\n","import type { InterpretationNode, NodeFlags } from \"./types\";\nimport { isHiddenFromAT, type VisibilityOptions } from \"./visibility\";\nimport {\n resolveRole,\n accessibleName,\n takesNameFromContent,\n isChildrenPresentational,\n isWidget,\n} from \"./roles\";\n\nexport type LinearizeOptions = VisibilityOptions;\n\nconst ELEMENT_NODE = 1;\nconst TEXT_NODE = 3;\n\n// Host-language leaves: form controls and images have no exposed DOM children (an <input> has none;\n// a <select>'s options and a <textarea>'s text are its *value*), so they emit one node.\nconst LEAF_TAGS = new Set([\"img\", \"input\", \"textarea\", \"select\", \"hr\"]);\n\n// Roles with no announcement value of their own — emit nothing, just surface their content. (A\n// `<p>` and a generic `<div>` both read as their text; the screen reader doesn't announce them.)\nconst TRANSPARENT_ROLES = new Set([\"paragraph\", \"presentation\", \"none\", \"generic\"]);\n\n// SVG <title>/<desc>/<metadata> are name/description metadata (SVG-AAM), never rendered text.\nconst SVG_NS = \"http://www.w3.org/2000/svg\";\nconst SVG_METADATA_TAGS = new Set([\"title\", \"desc\", \"metadata\"]);\n\n/**\n * Linearize a DOM subtree into the ordered sequence a screen reader would traverse (SPEC-001).\n *\n * Each element is one of three kinds:\n * - **transparent** (no role, or a silent structural role) — emits nothing, recurses so its text\n * and descendants surface.\n * - **leaf** — a form control / image, or a role whose children are presentational *and* whose\n * name cannot come from content (img, slider, separator, progressbar, …): emits one node and\n * does **not** recurse, because nothing below it is exposed.\n * - **container** — everything else (landmark, list, table, row, cell, link, heading, and also\n * the content widgets button/tab/option/switch whose descendants browsers do keep in the tree):\n * emits a node *and* recurses, so nested elements — the link inside a heading, the missing-alt\n * image inside a button, the button inside a table cell — stay in the reading order.\n *\n * The **no-double-read rule** (SPEC-001 §3): a role that takes its name from content (heading,\n * link, cell, row, button, …) has already consumed its descendant *text* into `name`, so bare\n * text runs inside it are not emitted again — but descendant *elements* with roles of their own\n * still are, and each emitted container starts a fresh text context: an author-named container\n * (list, listitem, group, landmark) nested in a consumed subtree surfaces its own text.\n * Elements hidden from assistive tech are skipped along with their subtree.\n */\nexport function linearize(root: Element, options: LinearizeOptions = {}): InterpretationNode[] {\n const out: InterpretationNode[] = [];\n emitElement(root, out, options, false);\n return out;\n}\n\nfunction emitElement(\n el: Element,\n out: InterpretationNode[],\n options: LinearizeOptions,\n textConsumed: boolean,\n): void {\n if (isHiddenFromAT(el, options)) return; // skip element AND its subtree\n\n const tag = el.tagName.toLowerCase();\n if (el.namespaceURI === SVG_NS && SVG_METADATA_TAGS.has(tag)) return;\n\n const role = resolveRole(el);\n\n // Decorative image (alt=\"\") — excluded from the reading order entirely.\n if (role === \"presentation\" && tag === \"img\") return;\n\n // Transparent: surface descendant content, no node of our own.\n if (role === null || TRANSPARENT_ROLES.has(role)) {\n walkChildren(el, out, options, textConsumed);\n return;\n }\n\n out.push(buildNode(el, role, tag));\n\n const consumesText = takesNameFromContent(role);\n\n // Leaf: nothing below is exposed.\n if (LEAF_TAGS.has(tag) || (isChildrenPresentational(role) && !consumesText)) return;\n\n // Container: fresh text context — suppressed only if this node's own name came from content.\n walkChildren(el, out, options, consumesText);\n}\n\nfunction walkChildren(\n node: Node,\n out: InterpretationNode[],\n options: LinearizeOptions,\n textConsumed: boolean,\n): void {\n node.childNodes.forEach((child) => {\n if (child.nodeType === TEXT_NODE) {\n if (textConsumed) return;\n const text = collapseWhitespace(child.textContent ?? \"\");\n if (text) out.push({ role: \"text\", name: text });\n return;\n }\n if (child.nodeType === ELEMENT_NODE) emitElement(child as Element, out, options, textConsumed);\n });\n}\n\nfunction buildNode(el: Element, role: string, tag: string): InterpretationNode {\n const name = accessibleName(el);\n const node: InterpretationNode = { role, name, ref: { tag } };\n const level = headingLevel(el, role);\n if (level !== undefined) node.level = level;\n const flags = computeFlags(el, role, tag, name);\n if (flags) node.flags = flags;\n return node;\n}\n\nfunction headingLevel(el: Element, role: string): number | undefined {\n if (role !== \"heading\") return undefined;\n // An author `aria-level` wins, on <h1>–<h6> too (browsers honour it over the tag).\n const ariaLevel = Number(el.getAttribute(\"aria-level\"));\n if (Number.isInteger(ariaLevel) && ariaLevel >= 1) return ariaLevel;\n const tagMatch = /^h([1-6])$/.exec(el.tagName.toLowerCase());\n return tagMatch ? Number(tagMatch[1]) : 2; // WAI-ARIA 1.2: implicit aria-level of a heading is 2\n}\n\nfunction computeFlags(el: Element, role: string, tag: string, name: string): NodeFlags | undefined {\n const flags: NodeFlags = {};\n if (isWidget(role) && name === \"\") flags.unnamed = true; // interactive control, no name (AC-5)\n if (tag === \"img\" && !el.hasAttribute(\"alt\")) flags.missingAlt = true; // missing alt (AC-6)\n return Object.keys(flags).length > 0 ? flags : undefined;\n}\n\nfunction collapseWhitespace(s: string): string {\n return s.replace(/\\s+/g, \" \").trim();\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "screen-reader-mirror",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "See what a screen reader interprets from your UI — a zero-install, client-side accessibility-tree linearization engine. No data leaves the machine.",
5
+ "license": "MIT",
6
+ "author": "James Lanzon",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "main": "./dist/index.js",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "sideEffects": false,
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "keywords": [
26
+ "accessibility",
27
+ "a11y",
28
+ "screen-reader",
29
+ "aria",
30
+ "wcag",
31
+ "accessibility-tree",
32
+ "ax-tree",
33
+ "accname",
34
+ "testing",
35
+ "semantics"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/Koshux/screen-reader-mirror.git",
40
+ "directory": "packages/core"
41
+ },
42
+ "homepage": "https://github.com/Koshux/screen-reader-mirror#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/Koshux/screen-reader-mirror/issues"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "provenance": true
49
+ },
50
+ "devDependencies": {
51
+ "@types/aria-query": "^5.0.4",
52
+ "tsup": "^9.0.0",
53
+ "typescript": "^5.9.3"
54
+ },
55
+ "dependencies": {
56
+ "aria-query": "^5.3.2",
57
+ "dom-accessibility-api": "^0.7.1"
58
+ },
59
+ "scripts": {
60
+ "build": "tsup",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "vitest run"
63
+ }
6
64
  }