playthrough 0.2.0 → 0.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.
@@ -0,0 +1,278 @@
1
+ // src/read.ts
2
+ function readPage(page) {
3
+ return page.evaluate(read);
4
+ }
5
+ async function unhovered(page, look) {
6
+ await page.evaluate(hovering, false);
7
+ try {
8
+ return await look();
9
+ } finally {
10
+ await page.evaluate(hovering, true);
11
+ }
12
+ }
13
+ function hovering(on) {
14
+ const kept = window;
15
+ if (on) {
16
+ for (const [rule, selector] of kept.__playthroughHover ?? []) rule.selectorText = selector;
17
+ kept.__playthroughHover = [];
18
+ return;
19
+ }
20
+ const set = kept.__playthroughHover = [];
21
+ const visit = (rules) => {
22
+ for (const rule of rules) {
23
+ if (rule instanceof CSSStyleRule && rule.selectorText.includes(":hover")) {
24
+ set.push([rule, rule.selectorText]);
25
+ rule.selectorText = rule.selectorText.replaceAll(":hover", ":not(*)");
26
+ }
27
+ if ("cssRules" in rule) visit(rule.cssRules);
28
+ }
29
+ };
30
+ for (const sheet of [...document.styleSheets, ...document.adoptedStyleSheets]) try {
31
+ visit(sheet.cssRules);
32
+ } catch {}
33
+ getComputedStyle(document.body).color;
34
+ for (const animation of document.getAnimations()) if (animation instanceof CSSTransition) animation.finish();
35
+ }
36
+ function read() {
37
+ const ROLES = /^(button|link|dialog|alertdialog|group|radiogroup|table|grid|treegrid|heading|status|alert|textbox|searchbox|combobox|checkbox|radio|switch|slider|listbox|option|tab|menu|menuitem|img)$/;
38
+ const HOLDERS = /^(dialog|alertdialog|status|alert|group|radiogroup|table|grid|treegrid|listbox|menu|box)$/;
39
+ const NAMED_BY_WORDS = /^(button|link|heading|option|tab|menuitem|radio|checkbox|switch)$/;
40
+ const MARKED = /^(B|STRONG|I|EM|U|S|MARK|SMALL|CODE|KBD|SPAN|ABBR|TIME|SUB|SUP|Q|DATA|VAR|DEL|INS|BDI|CITE|DFN)$/;
41
+ const INPUTS = {
42
+ checkbox: "checkbox",
43
+ radio: "radio",
44
+ range: "slider",
45
+ button: "button",
46
+ submit: "button",
47
+ reset: "button",
48
+ search: "searchbox"
49
+ };
50
+ const TAGS = {
51
+ BUTTON: "button",
52
+ SUMMARY: "button",
53
+ DIALOG: "dialog",
54
+ TABLE: "table",
55
+ SELECT: "combobox",
56
+ TEXTAREA: "textbox",
57
+ OUTPUT: "status",
58
+ FIELDSET: "group",
59
+ IMG: "img",
60
+ H1: "heading",
61
+ H2: "heading",
62
+ H3: "heading",
63
+ H4: "heading",
64
+ H5: "heading",
65
+ H6: "heading"
66
+ };
67
+ const TABLES = "table, [role=table], [role=grid], [role=treegrid]";
68
+ const mirror = window.rrwebRecord?.record?.mirror;
69
+ const plain = (words) => words.replace(/\s+/g, " ").trim();
70
+ const cut = (words, most) => words.length > most ? words.slice(0, most - 1).trimEnd() + "…" : words;
71
+ const style = (element) => getComputedStyle(element);
72
+ function roleOf(element) {
73
+ const given = element.getAttribute("role")?.trim().split(/\s+/)[0];
74
+ if (given) return given;
75
+ if (element.tagName === "A") return element.hasAttribute("href") ? "link" : void 0;
76
+ if (element.tagName === "INPUT") {
77
+ const type = (element.getAttribute("type") ?? "text").toLowerCase();
78
+ return type === "hidden" ? void 0 : INPUTS[type] ?? "textbox";
79
+ }
80
+ return TAGS[element.tagName];
81
+ }
82
+ const apart = (element, side) => {
83
+ const looks = style(element);
84
+ return !looks.display.startsWith("inline") || parseFloat(looks[`margin${side}`]) + parseFloat(looks[`padding${side}`]) > 0 ? " " : "";
85
+ };
86
+ function spoken(element) {
87
+ let words = "";
88
+ for (const node of element.childNodes) if (node.nodeType === Node.TEXT_NODE) words += node.textContent;
89
+ else if (node instanceof HTMLBRElement) words += " ";
90
+ else if (node instanceof Element) words += apart(node, "Left") + spoken(node) + apart(node, "Right");
91
+ return words;
92
+ }
93
+ function ownWords(element) {
94
+ let words = "";
95
+ for (const node of element.childNodes) if (node.nodeType === Node.TEXT_NODE) words += node.textContent;
96
+ else if (node instanceof Element && MARKED.test(node.tagName) && !called(node) && !panel(node)) {
97
+ const looks = style(node);
98
+ if (looks.display === "none" || looks.visibility === "hidden") continue;
99
+ words += apart(node, "Left") + ownWords(node) + apart(node, "Right");
100
+ }
101
+ return plain(words);
102
+ }
103
+ const labelOf = (element) => {
104
+ const by = element.getAttribute("aria-labelledby");
105
+ return plain(by ? by.split(/\s+/).map((id) => document.getElementById(id)?.textContent ?? "").join(" ") : element.getAttribute("aria-label") ?? "");
106
+ };
107
+ function nameOf(element, role) {
108
+ let name = labelOf(element);
109
+ if (!name && NAMED_BY_WORDS.test(role)) name = plain(spoken(element));
110
+ if (!name && "labels" in element) name = plain([...element.labels ?? []].map((label) => label.textContent).join(" "));
111
+ if (!name) name = element.getAttribute("alt") ?? element.getAttribute("title") ?? element.getAttribute("placeholder") ?? "";
112
+ return cut(plain(name), 120);
113
+ }
114
+ function called(element) {
115
+ const role = roleOf(element);
116
+ if (!role || !ROLES.test(role)) return void 0;
117
+ const name = nameOf(element, role);
118
+ return /group$/.test(role) && !name ? void 0 : {
119
+ role,
120
+ name
121
+ };
122
+ }
123
+ const labelledPart = (element) => (/^(SECTION|ASIDE|NAV|FORM|ARTICLE)$/.test(element.tagName) || /^(region|form|navigation|complementary|search|article)$/.test(element.getAttribute("role") ?? "")) && !!labelOf(element);
124
+ function panel(element) {
125
+ const box = element.getBoundingClientRect();
126
+ if (box.width < 12 || box.height < 12 || box.width * box.height > .9 * innerWidth * innerHeight) return false;
127
+ if (/^(HTML|BODY|MAIN|TR|TBODY|THEAD|TD|TH)$/.test(element.tagName)) return false;
128
+ if (labelledPart(element)) return true;
129
+ const looks = style(element);
130
+ const background = looks.backgroundColor;
131
+ const around = element.parentElement && style(element.parentElement).backgroundColor;
132
+ if (!/^(transparent|rgba\(0, 0, 0, 0\))$/.test(background) && background !== around) return true;
133
+ if (looks.boxShadow !== "none") return true;
134
+ return [
135
+ "Top",
136
+ "Right",
137
+ "Bottom",
138
+ "Left"
139
+ ].filter((side) => parseFloat(looks[`border${side}Width`]) > 0 && looks[`border${side}Style`] !== "none").length >= 3;
140
+ }
141
+ const firstWords = (element) => {
142
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
143
+ for (let node; node = walker.nextNode();) if (plain(node.textContent ?? "")) return plain(node.textContent ?? "");
144
+ return "";
145
+ };
146
+ const allWords = (element) => {
147
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
148
+ const words = [];
149
+ for (let node; node = walker.nextNode();) words.push(node.textContent ?? "");
150
+ return plain(words.join(" "));
151
+ };
152
+ function headingsOf(table) {
153
+ const cells = [...table.querySelectorAll("th, [role=columnheader]")].filter((cell) => cell.closest(TABLES) === table);
154
+ const row = cells[0]?.parentElement;
155
+ const headings = cells.filter((cell) => cell.parentElement === row).map((cell) => {
156
+ const box = cell.getBoundingClientRect();
157
+ return {
158
+ name: cut(plain(spoken(cell)), 120),
159
+ from: box.left,
160
+ to: box.right,
161
+ bottom: box.bottom
162
+ };
163
+ }).filter((heading) => heading.name && heading.to > heading.from);
164
+ return headings.length ? headings : void 0;
165
+ }
166
+ const ITEMS = "li, tr, [role=listitem], [role=row]";
167
+ const alikeBeside = (element) => typeof element.className === "string" && !!element.className && [...element.parentElement?.children ?? []].some((other) => other !== element && other.tagName === element.tagName && other.className === element.className);
168
+ const lists = /* @__PURE__ */ new Map();
169
+ const items = [];
170
+ const itemOf = (inItem) => {
171
+ if (!inItem) return void 0;
172
+ if (inItem.index === void 0) {
173
+ const { element } = inItem;
174
+ const box = element.getBoundingClientRect();
175
+ const list = lists.get(element.parentElement) ?? lists.set(element.parentElement, lists.size).get(element.parentElement);
176
+ const node = mirror?.getId(element);
177
+ items.push({
178
+ role: element.matches("tr, [role=row]") ? "row" : "listitem",
179
+ words: "",
180
+ box: {
181
+ x: Math.round(box.left),
182
+ y: Math.round(box.top),
183
+ width: Math.round(box.width),
184
+ height: Math.round(box.height)
185
+ },
186
+ page: {
187
+ x: Math.round(box.left + scrollX),
188
+ y: Math.round(box.top + scrollY)
189
+ },
190
+ list,
191
+ ...inItem.holder !== void 0 && { holder: inItem.holder },
192
+ ...node !== void 0 && node >= 0 && { node }
193
+ });
194
+ inItem.index = items.length - 1;
195
+ }
196
+ return inItem.index;
197
+ };
198
+ const things = [];
199
+ function walk(element, holder, parent, headings, inItem) {
200
+ for (const child of element.children) {
201
+ const looks = style(child);
202
+ if (looks.display === "none" || looks.visibility === "hidden" || Number(looks.opacity) === 0) continue;
203
+ const box = child.getBoundingClientRect();
204
+ const item = child.matches(ITEMS) || alikeBeside(child) ? {
205
+ element: child,
206
+ ...holder !== void 0 && { holder },
207
+ ...inItem && { outer: inItem }
208
+ } : inItem;
209
+ const under = child.matches(TABLES) ? headingsOf(child) : headings;
210
+ const own = plain([...child.childNodes].filter((node) => node.nodeType === Node.TEXT_NODE).map((node) => node.textContent).join(" "));
211
+ let known = called(child);
212
+ if (!known && !own && panel(child)) {
213
+ const name = labelOf(child) || firstWords(child);
214
+ if (name) known = {
215
+ role: "box",
216
+ name: cut(name, 120)
217
+ };
218
+ }
219
+ let inside = holder;
220
+ let readParent = parent;
221
+ if ((known || own) && box.width >= 1 && box.height >= 1) {
222
+ const middle = box.left + box.width / 2;
223
+ const column = headings?.find((heading) => middle >= heading.from && middle < heading.to && box.top >= heading.bottom - 2)?.name;
224
+ const node = mirror?.getId(child);
225
+ const words = cut(own ? ownWords(child) : "", 300);
226
+ const wordsInside = known && /^(box|menu)$/.test(known.role) ? cut(allWords(child), 300) : void 0;
227
+ const ofItem = itemOf(item);
228
+ for (let around = item; around && words; around = around.outer) {
229
+ const kept = items[itemOf(around)];
230
+ kept.words = cut(kept.words ? `${kept.words} ${words}` : words, 300);
231
+ }
232
+ things.push({
233
+ role: known?.role ?? "text",
234
+ name: known?.name ?? cut(words, 120),
235
+ words,
236
+ ...wordsInside && wordsInside !== words && { wordsInside },
237
+ box: {
238
+ x: Math.round(box.left),
239
+ y: Math.round(box.top),
240
+ width: Math.round(box.width),
241
+ height: Math.round(box.height)
242
+ },
243
+ page: {
244
+ x: Math.round(box.left + scrollX),
245
+ y: Math.round(box.top + scrollY)
246
+ },
247
+ look: {
248
+ color: looks.color,
249
+ background: looks.backgroundColor,
250
+ size: looks.fontSize,
251
+ weight: looks.fontWeight,
252
+ border: parseFloat(looks.borderTopWidth) && looks.borderTopStyle !== "none" ? `${looks.borderTopWidth} ${looks.borderTopStyle} ${looks.borderTopColor}` : "none"
253
+ },
254
+ ...holder !== void 0 && { holder },
255
+ ...parent !== void 0 && { parent },
256
+ ...column && { column },
257
+ ...known?.role === "box" && labelOf(child) && { labelled: true },
258
+ ...child.closest("[aria-live]:not([aria-live=\"off\"]), [role=\"status\"], [role=\"alert\"], [role=\"log\"]") && { live: true },
259
+ ...node !== void 0 && node >= 0 && { node },
260
+ ...ofItem !== void 0 && { item: ofItem }
261
+ });
262
+ if (known && HOLDERS.test(known.role)) inside = things.length - 1;
263
+ readParent = things.length - 1;
264
+ }
265
+ walk(child, inside, readParent, under, item);
266
+ }
267
+ }
268
+ walk(document.body, void 0, void 0, void 0, void 0);
269
+ return {
270
+ screen: {
271
+ width: innerWidth,
272
+ height: innerHeight
273
+ },
274
+ things,
275
+ items
276
+ };
277
+ }
278
+ export { readPage as n, unhovered as r, read as t };