@fastbrowse/win32-x64 0.7.1 → 0.7.4

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.
Files changed (106) hide show
  1. package/fastbrowse/fastbrowse-mcp.exe +0 -0
  2. package/fastbrowse/fastbrowse.exe +0 -0
  3. package/fastbrowse.node +0 -0
  4. package/package.json +3 -2
  5. package/fastbrowse/_internal/VCRUNTIME140.dll +0 -0
  6. package/fastbrowse/_internal/VCRUNTIME140_1.dll +0 -0
  7. package/fastbrowse/_internal/_asyncio.pyd +0 -0
  8. package/fastbrowse/_internal/_bz2.pyd +0 -0
  9. package/fastbrowse/_internal/_ctypes.pyd +0 -0
  10. package/fastbrowse/_internal/_decimal.pyd +0 -0
  11. package/fastbrowse/_internal/_hashlib.pyd +0 -0
  12. package/fastbrowse/_internal/_lzma.pyd +0 -0
  13. package/fastbrowse/_internal/_multiprocessing.pyd +0 -0
  14. package/fastbrowse/_internal/_overlapped.pyd +0 -0
  15. package/fastbrowse/_internal/_queue.pyd +0 -0
  16. package/fastbrowse/_internal/_socket.pyd +0 -0
  17. package/fastbrowse/_internal/_ssl.pyd +0 -0
  18. package/fastbrowse/_internal/_uuid.pyd +0 -0
  19. package/fastbrowse/_internal/_wmi.pyd +0 -0
  20. package/fastbrowse/_internal/_zoneinfo.pyd +0 -0
  21. package/fastbrowse/_internal/api-ms-win-core-console-l1-1-0.dll +0 -0
  22. package/fastbrowse/_internal/api-ms-win-core-datetime-l1-1-0.dll +0 -0
  23. package/fastbrowse/_internal/api-ms-win-core-debug-l1-1-0.dll +0 -0
  24. package/fastbrowse/_internal/api-ms-win-core-errorhandling-l1-1-0.dll +0 -0
  25. package/fastbrowse/_internal/api-ms-win-core-fibers-l1-1-0.dll +0 -0
  26. package/fastbrowse/_internal/api-ms-win-core-fibers-l1-1-1.dll +0 -0
  27. package/fastbrowse/_internal/api-ms-win-core-file-l1-1-0.dll +0 -0
  28. package/fastbrowse/_internal/api-ms-win-core-file-l1-2-0.dll +0 -0
  29. package/fastbrowse/_internal/api-ms-win-core-file-l2-1-0.dll +0 -0
  30. package/fastbrowse/_internal/api-ms-win-core-handle-l1-1-0.dll +0 -0
  31. package/fastbrowse/_internal/api-ms-win-core-heap-l1-1-0.dll +0 -0
  32. package/fastbrowse/_internal/api-ms-win-core-interlocked-l1-1-0.dll +0 -0
  33. package/fastbrowse/_internal/api-ms-win-core-kernel32-legacy-l1-1-1.dll +0 -0
  34. package/fastbrowse/_internal/api-ms-win-core-libraryloader-l1-1-0.dll +0 -0
  35. package/fastbrowse/_internal/api-ms-win-core-localization-l1-2-0.dll +0 -0
  36. package/fastbrowse/_internal/api-ms-win-core-memory-l1-1-0.dll +0 -0
  37. package/fastbrowse/_internal/api-ms-win-core-namedpipe-l1-1-0.dll +0 -0
  38. package/fastbrowse/_internal/api-ms-win-core-processenvironment-l1-1-0.dll +0 -0
  39. package/fastbrowse/_internal/api-ms-win-core-processthreads-l1-1-0.dll +0 -0
  40. package/fastbrowse/_internal/api-ms-win-core-processthreads-l1-1-1.dll +0 -0
  41. package/fastbrowse/_internal/api-ms-win-core-profile-l1-1-0.dll +0 -0
  42. package/fastbrowse/_internal/api-ms-win-core-rtlsupport-l1-1-0.dll +0 -0
  43. package/fastbrowse/_internal/api-ms-win-core-string-l1-1-0.dll +0 -0
  44. package/fastbrowse/_internal/api-ms-win-core-synch-l1-1-0.dll +0 -0
  45. package/fastbrowse/_internal/api-ms-win-core-synch-l1-2-0.dll +0 -0
  46. package/fastbrowse/_internal/api-ms-win-core-sysinfo-l1-1-0.dll +0 -0
  47. package/fastbrowse/_internal/api-ms-win-core-sysinfo-l1-2-0.dll +0 -0
  48. package/fastbrowse/_internal/api-ms-win-core-timezone-l1-1-0.dll +0 -0
  49. package/fastbrowse/_internal/api-ms-win-core-util-l1-1-0.dll +0 -0
  50. package/fastbrowse/_internal/api-ms-win-crt-conio-l1-1-0.dll +0 -0
  51. package/fastbrowse/_internal/api-ms-win-crt-convert-l1-1-0.dll +0 -0
  52. package/fastbrowse/_internal/api-ms-win-crt-environment-l1-1-0.dll +0 -0
  53. package/fastbrowse/_internal/api-ms-win-crt-filesystem-l1-1-0.dll +0 -0
  54. package/fastbrowse/_internal/api-ms-win-crt-heap-l1-1-0.dll +0 -0
  55. package/fastbrowse/_internal/api-ms-win-crt-locale-l1-1-0.dll +0 -0
  56. package/fastbrowse/_internal/api-ms-win-crt-math-l1-1-0.dll +0 -0
  57. package/fastbrowse/_internal/api-ms-win-crt-process-l1-1-0.dll +0 -0
  58. package/fastbrowse/_internal/api-ms-win-crt-runtime-l1-1-0.dll +0 -0
  59. package/fastbrowse/_internal/api-ms-win-crt-stdio-l1-1-0.dll +0 -0
  60. package/fastbrowse/_internal/api-ms-win-crt-string-l1-1-0.dll +0 -0
  61. package/fastbrowse/_internal/api-ms-win-crt-time-l1-1-0.dll +0 -0
  62. package/fastbrowse/_internal/api-ms-win-crt-utility-l1-1-0.dll +0 -0
  63. package/fastbrowse/_internal/base_library.zip +0 -0
  64. package/fastbrowse/_internal/certifi/cacert.pem +0 -3959
  65. package/fastbrowse/_internal/certifi/py.typed +0 -0
  66. package/fastbrowse/_internal/fastbrowse/browser/autoconsent/LICENSE +0 -373
  67. package/fastbrowse/_internal/fastbrowse/browser/autoconsent/SOURCE.txt +0 -20
  68. package/fastbrowse/_internal/fastbrowse/browser/autoconsent/autoconsent.standalone.js +0 -3543
  69. package/fastbrowse/_internal/fastbrowse/browser/capture.js +0 -499
  70. package/fastbrowse/_internal/fastbrowse/browser/snapshot.js +0 -1098
  71. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/INSTALLER +0 -1
  72. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/METADATA +0 -616
  73. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/RECORD +0 -14
  74. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/REQUESTED +0 -0
  75. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/WHEEL +0 -4
  76. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/direct_url.json +0 -1
  77. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/entry_points.txt +0 -3
  78. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/licenses/LICENSE +0 -21
  79. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/licenses/NOTICE +0 -33
  80. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/uv_build.json +0 -1
  81. package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/uv_cache.json +0 -1
  82. package/fastbrowse/_internal/libcrypto-3-x64.dll +0 -0
  83. package/fastbrowse/_internal/libffi-8.dll +0 -0
  84. package/fastbrowse/_internal/libssl-3-x64.dll +0 -0
  85. package/fastbrowse/_internal/pydantic-2.13.4.dist-info/INSTALLER +0 -1
  86. package/fastbrowse/_internal/pydantic-2.13.4.dist-info/METADATA +0 -1294
  87. package/fastbrowse/_internal/pydantic-2.13.4.dist-info/RECORD +0 -113
  88. package/fastbrowse/_internal/pydantic-2.13.4.dist-info/REQUESTED +0 -0
  89. package/fastbrowse/_internal/pydantic-2.13.4.dist-info/WHEEL +0 -4
  90. package/fastbrowse/_internal/pydantic-2.13.4.dist-info/licenses/LICENSE +0 -21
  91. package/fastbrowse/_internal/pydantic_core/_pydantic_core.cp313-win_amd64.pyd +0 -0
  92. package/fastbrowse/_internal/pyexpat.pyd +0 -0
  93. package/fastbrowse/_internal/python313.dll +0 -0
  94. package/fastbrowse/_internal/select.pyd +0 -0
  95. package/fastbrowse/_internal/setuptools/_vendor/jaraco/text/Lorem ipsum.txt +0 -2
  96. package/fastbrowse/_internal/ucrtbase.dll +0 -0
  97. package/fastbrowse/_internal/unicodedata.pyd +0 -0
  98. package/fastbrowse/_internal/websockets/speedups.cp313-win_amd64.pyd +0 -0
  99. package/fastbrowse/_internal/websockets-17.2.dist-info/INSTALLER +0 -1
  100. package/fastbrowse/_internal/websockets-17.2.dist-info/METADATA +0 -170
  101. package/fastbrowse/_internal/websockets-17.2.dist-info/RECORD +0 -65
  102. package/fastbrowse/_internal/websockets-17.2.dist-info/REQUESTED +0 -0
  103. package/fastbrowse/_internal/websockets-17.2.dist-info/WHEEL +0 -5
  104. package/fastbrowse/_internal/websockets-17.2.dist-info/entry_points.txt +0 -2
  105. package/fastbrowse/_internal/websockets-17.2.dist-info/licenses/LICENSE +0 -24
  106. package/fastbrowse/_internal/websockets-17.2.dist-info/top_level.txt +0 -1
@@ -1,499 +0,0 @@
1
- // Complete structured text capture of one frame, as ordered blocks with heading paths.
2
- // Unlike snapshot.js (bounded, for Jev's action choice) this reads the whole rendered document, on screen or not,
3
- // so the reading path and answer-evidence quotes see everything a user could scroll to.
4
- // Adjacent inline content (text, <strong>, <a>, <span>...) is one block, so a value inside markup stays with its label.
5
- // biome-ignore format: Python embeds this function inside an expression, where a semicolon is invalid.
6
- ((allowedOrigins = null) => {
7
- if (!document.body) return null;
8
- const HEADINGS = { H1: 1, H2: 2, H3: 3, H4: 4, H5: 5, H6: 6 };
9
- const SKIP = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG', 'CANVAS', 'IFRAME']);
10
- const blocks = [];
11
- let path = [];
12
- let framePath = null;
13
- let sourcePath = '';
14
- let inaccessible = 0;
15
- let visibleH1 = false;
16
- let controlObservations = 0;
17
- let imageObservations = 0;
18
- let imagesOmitted = false;
19
- let linkObservations = 0;
20
- let linksOmitted = false;
21
- let controlsOmitted = false;
22
- const imageSource = source => {
23
- if (!source) return source;
24
- if (source.startsWith('data:')) {
25
- const comma = source.indexOf(',');
26
- return `${source.slice(0, comma < 0 ? 128 : Math.min(comma, 128))},[payload omitted]`;
27
- }
28
- if (source.startsWith('blob:')) return 'blob:[identifier omitted]';
29
- return source.length > 1024 ? `${source.slice(0, 1024)}[truncated]` : source;
30
- };
31
- const registry = (window.__fastbrowse ||= { ids: new WeakMap(), nodes: new Map(), next: 1 });
32
- const granted = doc => {
33
- if (allowedOrigins === null) return true;
34
- const frame = doc.defaultView.frameElement;
35
- if (frame) {
36
- if (frame.hasAttribute('srcdoc') || !frame.getAttribute('src')) return false;
37
- const source = new URL(frame.src);
38
- if (!['http:', 'https:'].includes(source.protocol) || !allowedOrigins.includes(source.origin)) return false;
39
- }
40
- const url = new URL(doc.URL);
41
- return ['http:', 'https:'].includes(url.protocol) && allowedOrigins.includes(url.origin);
42
- };
43
- const identity = e => {
44
- if (!registry.ids.has(e)) registry.ids.set(e, registry.next++);
45
- return registry.ids.get(e);
46
- };
47
-
48
- const clean = s =>
49
- s
50
- .replace(/[ \t\r\f\v]+/g, ' ')
51
- .replace(/ *\n */g, '\n')
52
- .replace(/\n{3,}/g, '\n\n')
53
- .trim();
54
- const textOf = e => clean(e.innerText ?? e.textContent ?? '');
55
- const hidden = e => e.hidden || getComputedStyle(e).display === 'none';
56
- const isBlock = e => {
57
- const display = getComputedStyle(e).display;
58
- return !(display.startsWith('inline') || display === 'contents') || e.tagName === 'BR';
59
- };
60
- const hrefOf = a => {
61
- const u = new URL(a.href, location.href);
62
- return u.origin === location.origin ? u.pathname + u.search : u.href;
63
- };
64
- const controlOf = nodes => {
65
- let found = null;
66
- for (const node of nodes) {
67
- if (!clean(node.nodeType === 3 ? node.textContent : (node.innerText ?? node.textContent ?? ''))) continue;
68
- let host = node.nodeType === 1 ? node : node.parentElement;
69
- while (host && !registry.hoverContexts?.has(host)) host = host.parentElement;
70
- if (!host?.isConnected || !host.matches(':hover')) return null;
71
- const binding = registry.hoverContexts.get(host);
72
- const siblings = [...(host.parentNode?.children || [])];
73
- const labelAttributes = [host.getAttribute('role'), host.getAttribute('aria-label'),
74
- host.getAttribute('title'), host.querySelector('img[alt]')?.getAttribute('alt') || null];
75
- if (host.innerText !== binding.text ||
76
- labelAttributes.some((value, index) => value !== binding.labelAttributes[index])) return null;
77
- // A reordered or replaced sibling set invalidates the observed ordinal before the quote is taken.
78
- if (host.parentNode !== binding.parent || siblings.length !== binding.siblings.length ||
79
- siblings.some((sibling, index) => sibling !== binding.siblings[index])) return null;
80
- if (found && found !== host) return null;
81
- found = host;
82
- }
83
- return found ? registry.hoverContexts.get(found).control : null;
84
- };
85
- const listIds = new WeakMap();
86
- const listFrameless = new WeakMap();
87
- let nextListId = 0;
88
- const listContext = nodes => {
89
- const lists = new Set();
90
- for (const node of nodes) {
91
- let element = node.nodeType === 1 ? node : node.parentElement;
92
- let list = null;
93
- for (let parent = element; parent; parent = parent.parentElement) {
94
- if (parent.matches('nav,[role="navigation"]')) return {};
95
- if (parent.matches('ul,ol,[role="list"]')) list = parent;
96
- }
97
- if (!list) {
98
- const nested = [...(element?.querySelectorAll('ul,ol,[role="list"]') ?? [])];
99
- const outer = nested.filter(candidate => !nested.some(other => other !== candidate && other.contains(candidate)));
100
- if (outer.length === 1) list = outer[0];
101
- }
102
- if (list) lists.add(list);
103
- }
104
- if (lists.size !== 1) return {};
105
- const list = [...lists][0];
106
- if (!listIds.has(list)) {
107
- listIds.set(list, String(nextListId++));
108
- // A frame the capture cannot open elsewhere on the page, such as an advertisement, cannot hold a member of a
109
- // list whose element contains no frame; one inside the list, or behind a shadow root the list or anything in it
110
- // holds, could.
111
- const framed = e => e.tagName === 'IFRAME' || e.tagName === 'FRAME' || !!e.shadowRoot;
112
- listFrameless.set(list, !framed(list) && ![...list.querySelectorAll('*')].some(framed));
113
- }
114
- const members = [...list.querySelectorAll('li,[role="listitem"]')].filter(member => {
115
- for (let parent = member; parent && parent !== list; parent = parent.parentElement) {
116
- if (hidden(parent)) return false;
117
- }
118
- return !!textOf(member);
119
- });
120
- return { list_id: listIds.get(list), list_count: members.length, list_frameless: listFrameless.get(list) };
121
- };
122
- const push = (kind, text, extra = {}, nodes = []) => {
123
- if (text)
124
- blocks.push({ kind, text, heading_path: [...path], frame_path: framePath, source_path: sourcePath, control_context: controlOf(nodes), ...listContext(nodes), ...extra });
125
- };
126
-
127
- // A cell can say what it says with an image or an icon alone: a flag marking the winner, a tick for "yes".
128
- // Its text is then empty and the table reads as if the cell were blank, so name what is drawn there: the
129
- // image's alt text or label, or failing that the icon font's class, which is how those fonts name a glyph.
130
- const ICON_CLASS = /^(?:glyphicon|fa|fas|far|bi|mdi|icon|ti|la)-(.+)$/;
131
- // Font sizes, weights and effects share the prefix without naming a glyph.
132
- const ICON_STYLE =
133
- /^(?:solid|regular|light|thin|duotone|brands|xs|sm|lg|xl|\d+x|fw|spin|pulse|border|inverse|stack.*|pull-.*|rotate-.*|flip-.*)$/;
134
- const drawn = c => {
135
- for (const e of c.querySelectorAll('img,svg,i,span,[role="img"]')) {
136
- if (hidden(e)) continue;
137
- const named =
138
- e.getAttribute('alt') ||
139
- e.getAttribute('aria-label') ||
140
- e.getAttribute('title') ||
141
- e.querySelector?.(':scope > title')?.textContent;
142
- if (named?.trim()) return `[${clean(named)}]`;
143
- for (const token of e.classList) {
144
- const glyph = ICON_CLASS.exec(token);
145
- if (glyph && !ICON_STYLE.test(glyph[1])) return `[${glyph[1].replace(/-/g, ' ')} icon]`;
146
- }
147
- }
148
- return '';
149
- };
150
- // A rating widget states its value only in markup a reader cannot see: a `star-rating` class carrying a
151
- // One..Five word token, or an accessible label the icons alone do not carry. Counting icons never works,
152
- // since a site colours a fixed row of them by class instead of drawing one per star (books.toscrape.com:
153
- // `<p class="star-rating Five">`, five identical `<i class="icon-star">` children whatever the rating).
154
- // Only these two explicit sources are trusted; a class that merely mentions "star" or an icon glyph is not
155
- // read as a rating, and a hidden widget states nothing a reader could see either.
156
- const RATING_WORD = { one: 'One', two: 'Two', three: 'Three', four: 'Four', five: 'Five' };
157
- const RATING_LABEL = /\b[1-5]\s*(?:\/|out of)\s*5\b|\b[1-5]\s*stars?\b/i;
158
- const ratingOf = e => {
159
- if (hidden(e) || getComputedStyle(e).visibility === 'hidden') return '';
160
- const classes = [...e.classList].map(c => c.toLowerCase());
161
- if (classes.includes('star-rating')) {
162
- for (const token of classes) if (RATING_WORD[token]) return `${RATING_WORD[token]} stars`;
163
- }
164
- const label = e.getAttribute('aria-label') || e.getAttribute('title');
165
- return label && RATING_LABEL.test(label) ? clean(label) : '';
166
- };
167
- // Checked on the element itself, then any descendant a class or role names as the widget, since a
168
- // role="img" label commonly sits one level below the card holding the title and price. A widget hidden
169
- // by an ancestor (rather than itself) states nothing a reader could see either, so ratingOf's own
170
- // `hidden` check is not enough: walk up to the search root and reject any candidate under a hidden one.
171
- const ratingIn = c =>
172
- ratingOf(c) ||
173
- [...c.querySelectorAll('[class*="star" i], [class*="rating" i], [role="img"]')]
174
- .filter(e => {
175
- for (let a = e.parentElement; a && a !== c; a = a.parentElement) if (hidden(a)) return false;
176
- return true;
177
- })
178
- .map(ratingOf)
179
- .find(Boolean) ||
180
- '';
181
- const withRating = (text, c) => {
182
- const rating = ratingIn(c);
183
- if (!rating || text.includes(rating)) return text;
184
- return text ? `${text} (${rating})` : rating;
185
- };
186
- const cellText = c => withRating(textOf(c) || drawn(c), c);
187
-
188
- // What a form holds is in its controls' values, which no text node carries: a task that asked for dates to be
189
- // chosen was refused as unanswered because "the dates chosen" had nothing to quote. A filled control reads as
190
- // "Label: value" after the text around it. Password and agent-typed secret values never leave the page.
191
- const CONTROLS = new Set(['INPUT', 'SELECT', 'TEXTAREA']);
192
- const UNFILLED = new Set(['hidden', 'password', 'checkbox', 'radio', 'button', 'submit', 'reset', 'image', 'file']);
193
- const nameOf = (e, seen = new Set()) => {
194
- if (!e || seen.has(e)) return '';
195
- seen.add(e);
196
- const root = e.getRootNode?.() ?? document;
197
- const byId = id => (root.getElementById ? root : document).getElementById(id);
198
- const referenced = (e.getAttribute('aria-labelledby') || '')
199
- .split(/\s+/)
200
- .filter(Boolean)
201
- .map(id => nameOf(byId(id), seen))
202
- .filter(Boolean)
203
- .join(' ');
204
- return clean(
205
- referenced ||
206
- e.getAttribute('aria-label') ||
207
- [...(e.labels || [])]
208
- .map(l => nameOf(l, seen))
209
- .filter(Boolean)
210
- .join(' ') ||
211
- (CONTROLS.has(e.tagName)
212
- ? ''
213
- : [...e.childNodes]
214
- .map(n =>
215
- n.nodeType === 3
216
- ? n.textContent
217
- : n.nodeType === 1 && !SKIP.has(n.tagName) && !CONTROLS.has(n.tagName)
218
- ? nameOf(n, seen)
219
- : '',
220
- )
221
- .join(' ')) ||
222
- e.getAttribute('title') ||
223
- e.getAttribute('placeholder') ||
224
- e.getAttribute('name') ||
225
- '',
226
- );
227
- };
228
- const fieldValue = e => {
229
- if (e.tagName === 'SELECT') return [...e.selectedOptions].map(o => clean(o.label || o.text)).join(', ');
230
- if (UNFILLED.has(e.type) || e.dataset?.fastbrowseSecret === '1') return '';
231
- return clean(e.value ?? '');
232
- };
233
- const fields = el =>
234
- (CONTROLS.has(el.tagName) ? [el] : [...el.querySelectorAll('input,select,textarea')])
235
- .filter(e => (e.checkVisibility ? e.checkVisibility({ visibilityProperty: true }) : !hidden(e)))
236
- .map(e => [nameOf(e), fieldValue(e)])
237
- .filter(([, value]) => value)
238
- .map(([name, value]) => (name ? `${name}: ${value}` : value));
239
-
240
- const renderTable = table => {
241
- // A table filter hides what it excludes with `hidden`, display or visibility (`collapse` is the one CSS made
242
- // for rows), and a column toggle hides cells; read as rows, a filtered table answered from rows not shown.
243
- // Visibility inherits, so the row's own style covers a collapsed section; display does not, so its section
244
- // is checked too. A cell hidden by visibility still holds its column, so it stays, blank, and the cells
245
- // after it keep their headers.
246
- const rows = [
247
- ...table.querySelectorAll(':scope > tr, :scope > thead > tr, :scope > tbody > tr, :scope > tfoot > tr'),
248
- ]
249
- .filter(row => !hidden(row) && getComputedStyle(row).visibility === 'visible' && !hidden(row.parentElement))
250
- .map(row => ({ row, cells: [...row.querySelectorAll(':scope > th, :scope > td')].filter(c => !hidden(c)) }))
251
- .filter(({ cells }) => cells.length);
252
- if (!rows.length) return [];
253
- let headers = rows.filter(({ row }) => row.parentElement.tagName === 'THEAD');
254
- if (!headers.length && rows[0].cells.every(c => c.tagName === 'TH')) headers = [rows[0]];
255
- const line = ({ cells }) => '| ' + cells.map(c => cellText(c).replace(/\|/g, '\\|')).join(' | ') + ' |';
256
- // Headerless comparison rows need leading cells for identity and separate source spans for counting.
257
- if (!headers.length) return rows.map((row, index) => (index ? [line(rows[0]), line(row)].join('\n') : line(row)));
258
- const prefix = headers.map(line);
259
- if (headers.length) prefix.push('| ' + headers[0].cells.map(() => '---').join(' | ') + ' |');
260
- const data = rows.filter(row => !headers.includes(row));
261
- return data.length ? data.map(row => [...prefix, line(row)].join('\n')) : [prefix.join('\n')];
262
- };
263
-
264
- function flush(run) {
265
- if (!run.length) return;
266
- const elements = run.filter(n => n.nodeType === 1);
267
- let text = clean(run.map(n => (n.nodeType === 3 ? n.textContent : (n.innerText ?? ''))).join(''));
268
- const onlyLink =
269
- elements.length === 1 &&
270
- elements[0].tagName === 'A' &&
271
- elements[0].href &&
272
- clean(elements[0].innerText ?? '') === text;
273
- for (const e of elements) text = withRating(text, e);
274
- if (onlyLink) push('link', text, { href: hrefOf(elements[0]) }, run);
275
- else push('paragraph', text, {}, run);
276
- for (const e of elements) for (const field of fields(e)) push('paragraph', field);
277
- run.length = 0;
278
- }
279
-
280
- function leaf(el) {
281
- return ![...el.children].some(c => !SKIP.has(c.tagName) && !hidden(c) && isBlock(c));
282
- }
283
-
284
- // A classless div repeated three times is usually page layout; a record is a styled unit or a semantic item.
285
- function repeated(el) {
286
- if (!el.classList.length && el.tagName !== 'ARTICLE' && el.tagName !== 'LI') return false;
287
- const siblings = [...(el.parentNode?.children ?? [])].filter(
288
- sibling =>
289
- sibling.tagName === el.tagName &&
290
- sibling.classList.length === el.classList.length &&
291
- [...el.classList].every(token => sibling.classList.contains(token)),
292
- );
293
- if (siblings.length >= 3) return true;
294
- // Linked cards can sit inside an unlabelled list wrapper, so distinct destinations establish the pair.
295
- if (siblings.length === 2 && el.tagName === 'A' && siblings.every(sibling => sibling.hasAttribute('href'))) {
296
- return new Set(siblings.map(sibling => sibling.href)).size === siblings.length;
297
- }
298
- // A styled pair can be a two-column layout, so require a heading outside both items to name their group.
299
- const section = el.parentElement;
300
- return (
301
- siblings.length === 2 &&
302
- [...(section?.querySelectorAll('h1,h2,h3,h4,h5,h6') ?? [])].some(heading => siblings.every(sibling => !sibling.contains(heading)))
303
- );
304
- }
305
-
306
- const records = new WeakMap();
307
- function recordText(el) {
308
- if (records.has(el)) return records.get(el);
309
- records.set(el, null);
310
- if (!repeated(el) || el.shadowRoot || el.querySelector('table,iframe,frame')) return null;
311
- const descendants = [...el.querySelectorAll('*')];
312
- if (descendants.some(c => c.shadowRoot) || descendants.filter(c => HEADINGS[c.tagName]).length > 1) return null;
313
- if (!descendants.some(c => !SKIP.has(c.tagName) && !hidden(c) && isBlock(c))) return null;
314
- const text = withRating(textOf(el), el);
315
- if (!text || text.length > 1500) return null;
316
- // A repeated wrapper around another list is a group, so keep the inner records separately citable.
317
- if (descendants.some(c => (c.tagName === 'LI' && leaf(c)) || recordText(c) !== null)) return null;
318
- records.set(el, text);
319
- return text;
320
- }
321
-
322
- function walk(el) {
323
- const run = [];
324
- for (const node of el.childNodes) {
325
- if (node.nodeType === 3) {
326
- run.push(node);
327
- continue;
328
- }
329
- if (node.nodeType !== 1 || hidden(node)) continue;
330
- if (FRAMES.has(node.tagName)) {
331
- flush(run);
332
- enter(node, path);
333
- continue;
334
- }
335
- if (SKIP.has(node.tagName)) continue;
336
- if (!node.matches('nav,[role="navigation"]') && !isBlock(node) && recordText(node) === null) {
337
- run.push(node);
338
- continue;
339
- }
340
- flush(run);
341
- block(node);
342
- }
343
- flush(run);
344
- }
345
-
346
- function block(el) {
347
- if (el.matches('nav,[role="navigation"]')) {
348
- // Navigation help headings describe their menu, not product facts in the following main content.
349
- const surrounding = [...path];
350
- walk(el);
351
- path = surrounding;
352
- return;
353
- }
354
- const level = HEADINGS[el.tagName];
355
- if (level) {
356
- const text = textOf(el);
357
- while (path.length >= level) path.pop();
358
- if (text) {
359
- push('heading', text, {}, [el]);
360
- path.push(text);
361
- }
362
- return;
363
- }
364
- if (el.tagName === 'TABLE') {
365
- for (const text of renderTable(el)) push('table', text, {}, [el]);
366
- // A form laid out in a table: its cells render as text, which holds none of the controls' values.
367
- for (const field of fields(el)) push('paragraph', field);
368
- return;
369
- }
370
- if (el.tagName === 'PRE') return push('code', textOf(el), {}, [el]);
371
- const record = recordText(el);
372
- if ((el.tagName === 'LI' && leaf(el)) || record !== null) {
373
- const links = el.matches('a[href]') ? [el] : el.querySelectorAll('a[href]');
374
- push(
375
- el.tagName === 'LI' ? 'list_item' : 'record',
376
- record ?? withRating(textOf(el), el),
377
- links.length === 1 ? { href: hrefOf(links[0]) } : {},
378
- [el],
379
- );
380
- for (const field of fields(el)) push('paragraph', field);
381
- return;
382
- }
383
- if (CONTROLS.has(el.tagName)) {
384
- for (const field of fields(el)) push('paragraph', field);
385
- return;
386
- }
387
- walk(el);
388
- }
389
-
390
- // A same-origin frame is read where it stands, under the headings above it: read after the whole page, an
391
- // embedded subscription form's heading followed the footer, and the reader, asked for the heading the
392
- // frame shows, answered with the page's heading over the frame instead.
393
- const FRAMES = new Set(['IFRAME', 'FRAME']);
394
- const entered = new WeakSet();
395
- function enter(e, headings) {
396
- entered.add(e);
397
- let inner = null;
398
- try {
399
- inner = e.contentDocument;
400
- } catch {
401
- inner = null;
402
- }
403
- if (!inner?.body || !granted(inner)) {
404
- inaccessible++;
405
- return;
406
- }
407
- const child = `${framePath || 'root'}/${identity(e)}`;
408
- scope(inner.body, child, child, headings);
409
- }
410
-
411
- function scope(root, frame, source, headings = []) {
412
- const previous = [path, framePath, sourcePath];
413
- path = [...headings];
414
- framePath = frame;
415
- sourcePath = source;
416
- walk(root);
417
- path = [...headings];
418
- const visible = e => e.checkVisibility ? e.checkVisibility({ visibilityProperty: true }) : !hidden(e);
419
- for (const e of [...root.querySelectorAll('input,select,textarea,button,[role="button"]')].filter(visible)) {
420
- if (controlObservations >= 64) {
421
- if (!controlsOmitted) push('observation', 'Additional control state observations omitted.');
422
- controlsOmitted = true;
423
- break;
424
- }
425
- // Placeholders suggest input; they do not label a value or prove an empty named field.
426
- if (CONTROLS.has(e.tagName) && !e.labels?.length && !e.hasAttribute('aria-label') && !e.hasAttribute('aria-labelledby')) continue;
427
- const name = nameOf(e) || clean(e.innerText ?? '');
428
- if (!name || e.dataset?.fastbrowseSecret === '1') continue;
429
- if (CONTROLS.has(e.tagName) && !UNFILLED.has(e.type) && !fieldValue(e)) {
430
- push('observation', `${name.slice(0, 1024)}: [empty]`);
431
- controlObservations++;
432
- }
433
- if (controlObservations < 64 && e.matches(':disabled,[aria-disabled="true"]')) {
434
- push('observation', `${name.slice(0, 1024)}: disabled`);
435
- controlObservations++;
436
- }
437
- }
438
- // Images carry no innerText, so a text-only capture cannot distinguish their presence from absence.
439
- for (const image of [...root.querySelectorAll('img')].filter(visible)) {
440
- if (imageObservations++ >= 32) {
441
- if (!imagesOmitted) push('observation', 'Additional visible image metadata omitted.');
442
- imagesOmitted = true;
443
- break;
444
- }
445
- // Loading changes without a page edit, so captures report presence rather than transient decode state.
446
- const metadata = { alt: image.getAttribute('alt')?.slice(0, 1024) ?? null,
447
- src: imageSource(image.getAttribute('src') ?? image.getAttribute('srcset')) };
448
- push('observation', `Image element DOM metadata: ${JSON.stringify(metadata)}. Pixels are not described.`);
449
- }
450
- // A link drawn only as an icon has no text, so its destination, the one thing it says, was nowhere a reader
451
- // could quote: asked for a release's download URL beside an icon-only download link, a run read every other
452
- // field of the row and then clicked the link again and again looking for the address. The row's text names
453
- // which item the link belongs to, since the observation is not read where the link stands.
454
- for (const link of [...root.querySelectorAll('a[href]')].filter(visible)) {
455
- if (clean(link.innerText ?? '')) continue;
456
- // An icon button written as `href="#"` or a script URL runs code here and leads nowhere to report. A bare
457
- // `#` parses to an empty hash, so the raw attribute says it is a fragment link, not the parsed URL.
458
- let target;
459
- try {
460
- target = new URL(link.getAttribute('href'), location.href);
461
- } catch {
462
- // One malformed address must not cost the page its whole capture.
463
- continue;
464
- }
465
- const page = location.href.split('#')[0];
466
- const fragment = link.getAttribute('href').trim().startsWith('#') || (target.hash && target.href.split('#')[0] === page);
467
- if (target.protocol === 'javascript:' || fragment) continue;
468
- if (linkObservations++ >= 32) {
469
- if (!linksOmitted) push('observation', 'Additional link destinations omitted.');
470
- linksOmitted = true;
471
- break;
472
- }
473
- let row = link.parentElement;
474
- while (row && row !== root && !clean(row.innerText ?? '')) row = row.parentElement;
475
- const metadata = {
476
- label: (nameOf(link) || drawn(link)).slice(0, 256) || null,
477
- href: imageSource(link.href),
478
- near: row ? textOf(row).replace(/\s+/g, ' ').slice(0, 256) || null : null,
479
- };
480
- push('observation', `Link element DOM metadata: ${JSON.stringify(metadata)}. The link has no text.`);
481
- }
482
- const h1 = [...root.querySelectorAll('h1,[role="heading"][aria-level="1"]')].filter(visible);
483
- visibleH1 ||= h1.length > 0;
484
- if (root === document.body) {
485
- const doc = document.documentElement;
486
- push('observation', `Observed viewport: Horizontal overflow: ${doc.scrollWidth > doc.clientWidth ? 'yes' : 'no'}`);
487
- }
488
- for (const e of root.querySelectorAll('*')) {
489
- if (e.shadowRoot) scope(e.shadowRoot, frame, `${source}/shadow:${identity(e)}`);
490
- // A frame the walk never reached (hidden, or inside a record or table read as one block) is still read.
491
- if (FRAMES.has(e.tagName) && !entered.has(e)) enter(e, []);
492
- }
493
- [path, framePath, sourcePath] = previous;
494
- }
495
-
496
- scope(document.body, null, '');
497
- if (!visibleH1) push('observation', 'Observed accessible DOM: Visible h1 headings: 0');
498
- return { url: location.href, title: document.title, blocks, inaccessible_frames: inaccessible, visible_h1: visibleH1 };
499
- })