@fastbrowse/linux-arm64 0.0.0-stage → 0.5.19
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 +21 -0
- package/NOTICE +33 -0
- package/README.md +3 -2
- package/fastbrowse/_internal/base_library.zip +0 -0
- package/fastbrowse/_internal/certifi/cacert.pem +3959 -0
- package/fastbrowse/_internal/certifi/py.typed +0 -0
- package/fastbrowse/_internal/fastbrowse/browser/autoconsent/LICENSE +373 -0
- package/fastbrowse/_internal/fastbrowse/browser/autoconsent/SOURCE.txt +20 -0
- package/fastbrowse/_internal/fastbrowse/browser/autoconsent/autoconsent.standalone.js +3543 -0
- package/fastbrowse/_internal/fastbrowse/browser/capture.js +323 -0
- package/fastbrowse/_internal/fastbrowse/browser/snapshot.js +839 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/INSTALLER +1 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/METADATA +539 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/RECORD +14 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/REQUESTED +0 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/WHEEL +4 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/direct_url.json +1 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/entry_points.txt +3 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/licenses/LICENSE +21 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/licenses/NOTICE +33 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/uv_build.json +1 -0
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/uv_cache.json +1 -0
- package/fastbrowse/_internal/libgcc_s.so.1 +0 -0
- package/fastbrowse/_internal/libpython3.13.so.1.0 +0 -0
- package/fastbrowse/_internal/pydantic-2.13.4.dist-info/INSTALLER +1 -0
- package/fastbrowse/_internal/pydantic-2.13.4.dist-info/METADATA +1294 -0
- package/fastbrowse/_internal/pydantic-2.13.4.dist-info/RECORD +113 -0
- package/fastbrowse/_internal/pydantic-2.13.4.dist-info/REQUESTED +0 -0
- package/fastbrowse/_internal/pydantic-2.13.4.dist-info/WHEEL +4 -0
- package/fastbrowse/_internal/pydantic-2.13.4.dist-info/licenses/LICENSE +21 -0
- package/fastbrowse/_internal/pydantic_core/_pydantic_core.cpython-313-aarch64-linux-gnu.so +0 -0
- package/fastbrowse/_internal/setuptools/_vendor/jaraco/text/Lorem ipsum.txt +2 -0
- package/fastbrowse/_internal/websockets/speedups.cpython-313-aarch64-linux-gnu.so +0 -0
- package/fastbrowse/_internal/websockets-17.1.dist-info/INSTALLER +1 -0
- package/fastbrowse/_internal/websockets-17.1.dist-info/METADATA +170 -0
- package/fastbrowse/_internal/websockets-17.1.dist-info/RECORD +65 -0
- package/fastbrowse/_internal/websockets-17.1.dist-info/REQUESTED +0 -0
- package/fastbrowse/_internal/websockets-17.1.dist-info/WHEEL +7 -0
- package/fastbrowse/_internal/websockets-17.1.dist-info/entry_points.txt +2 -0
- package/fastbrowse/_internal/websockets-17.1.dist-info/licenses/LICENSE +24 -0
- package/fastbrowse/_internal/websockets-17.1.dist-info/top_level.txt +1 -0
- package/fastbrowse/fastbrowse +0 -0
- package/package.json +25 -4
|
@@ -0,0 +1,323 @@
|
|
|
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 IIFE inside an expression, where a semicolon is invalid.
|
|
6
|
+
(() => {
|
|
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
|
+
const registry = (window.__fastbrowse ||= { ids: new WeakMap(), nodes: new Map(), next: 1 });
|
|
16
|
+
const identity = e => {
|
|
17
|
+
if (!registry.ids.has(e)) registry.ids.set(e, registry.next++);
|
|
18
|
+
return registry.ids.get(e);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const clean = s =>
|
|
22
|
+
s
|
|
23
|
+
.replace(/[ \t\r\f\v]+/g, ' ')
|
|
24
|
+
.replace(/ *\n */g, '\n')
|
|
25
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
26
|
+
.trim();
|
|
27
|
+
const textOf = e => clean(e.innerText ?? e.textContent ?? '');
|
|
28
|
+
const hidden = e => e.hidden || getComputedStyle(e).display === 'none';
|
|
29
|
+
const isBlock = e => {
|
|
30
|
+
const display = getComputedStyle(e).display;
|
|
31
|
+
return !(display.startsWith('inline') || display === 'contents') || e.tagName === 'BR';
|
|
32
|
+
};
|
|
33
|
+
const hrefOf = a => {
|
|
34
|
+
const u = new URL(a.href, location.href);
|
|
35
|
+
return u.origin === location.origin ? u.pathname + u.search : u.href;
|
|
36
|
+
};
|
|
37
|
+
const push = (kind, text, extra = {}) => {
|
|
38
|
+
if (text)
|
|
39
|
+
blocks.push({ kind, text, heading_path: [...path], frame_path: framePath, source_path: sourcePath, ...extra });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// A cell can say what it says with an image or an icon alone: a flag marking the winner, a tick for "yes".
|
|
43
|
+
// Its text is then empty and the table reads as if the cell were blank, so name what is drawn there: the
|
|
44
|
+
// image's alt text or label, or failing that the icon font's class, which is how those fonts name a glyph.
|
|
45
|
+
const ICON_CLASS = /^(?:glyphicon|fa|fas|far|bi|mdi|icon|ti|la)-(.+)$/;
|
|
46
|
+
// Font sizes, weights and effects share the prefix without naming a glyph.
|
|
47
|
+
const ICON_STYLE =
|
|
48
|
+
/^(?:solid|regular|light|thin|duotone|brands|xs|sm|lg|xl|\d+x|fw|spin|pulse|border|inverse|stack.*|pull-.*|rotate-.*|flip-.*)$/;
|
|
49
|
+
const drawn = c => {
|
|
50
|
+
for (const e of c.querySelectorAll('img,svg,i,span,[role="img"]')) {
|
|
51
|
+
if (hidden(e)) continue;
|
|
52
|
+
const named =
|
|
53
|
+
e.getAttribute('alt') ||
|
|
54
|
+
e.getAttribute('aria-label') ||
|
|
55
|
+
e.getAttribute('title') ||
|
|
56
|
+
e.querySelector?.(':scope > title')?.textContent;
|
|
57
|
+
if (named?.trim()) return `[${clean(named)}]`;
|
|
58
|
+
for (const token of e.classList) {
|
|
59
|
+
const glyph = ICON_CLASS.exec(token);
|
|
60
|
+
if (glyph && !ICON_STYLE.test(glyph[1])) return `[${glyph[1].replace(/-/g, ' ')} icon]`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
};
|
|
65
|
+
// A rating widget states its value only in markup a reader cannot see: a `star-rating` class carrying a
|
|
66
|
+
// One..Five word token, or an accessible label the icons alone do not carry. Counting icons never works,
|
|
67
|
+
// since a site colours a fixed row of them by class instead of drawing one per star (books.toscrape.com:
|
|
68
|
+
// `<p class="star-rating Five">`, five identical `<i class="icon-star">` children whatever the rating).
|
|
69
|
+
// Only these two explicit sources are trusted; a class that merely mentions "star" or an icon glyph is not
|
|
70
|
+
// read as a rating, and a hidden widget states nothing a reader could see either.
|
|
71
|
+
const RATING_WORD = { one: 'One', two: 'Two', three: 'Three', four: 'Four', five: 'Five' };
|
|
72
|
+
const RATING_LABEL = /\b[1-5]\s*(?:\/|out of)\s*5\b|\b[1-5]\s*stars?\b/i;
|
|
73
|
+
const ratingOf = e => {
|
|
74
|
+
if (hidden(e) || getComputedStyle(e).visibility === 'hidden') return '';
|
|
75
|
+
const classes = [...e.classList].map(c => c.toLowerCase());
|
|
76
|
+
if (classes.includes('star-rating')) {
|
|
77
|
+
for (const token of classes) if (RATING_WORD[token]) return `${RATING_WORD[token]} stars`;
|
|
78
|
+
}
|
|
79
|
+
const label = e.getAttribute('aria-label') || e.getAttribute('title');
|
|
80
|
+
return label && RATING_LABEL.test(label) ? clean(label) : '';
|
|
81
|
+
};
|
|
82
|
+
// Checked on the element itself, then any descendant a class or role names as the widget, since a
|
|
83
|
+
// role="img" label commonly sits one level below the card holding the title and price. A widget hidden
|
|
84
|
+
// by an ancestor (rather than itself) states nothing a reader could see either, so ratingOf's own
|
|
85
|
+
// `hidden` check is not enough: walk up to the search root and reject any candidate under a hidden one.
|
|
86
|
+
const ratingIn = c =>
|
|
87
|
+
ratingOf(c) ||
|
|
88
|
+
[...c.querySelectorAll('[class*="star" i], [class*="rating" i], [role="img"]')]
|
|
89
|
+
.filter(e => {
|
|
90
|
+
for (let a = e.parentElement; a && a !== c; a = a.parentElement) if (hidden(a)) return false;
|
|
91
|
+
return true;
|
|
92
|
+
})
|
|
93
|
+
.map(ratingOf)
|
|
94
|
+
.find(Boolean) ||
|
|
95
|
+
'';
|
|
96
|
+
const withRating = (text, c) => {
|
|
97
|
+
const rating = ratingIn(c);
|
|
98
|
+
if (!rating || text.includes(rating)) return text;
|
|
99
|
+
return text ? `${text} (${rating})` : rating;
|
|
100
|
+
};
|
|
101
|
+
const cellText = c => withRating(textOf(c) || drawn(c), c);
|
|
102
|
+
|
|
103
|
+
// What a form holds is in its controls' values, which no text node carries: a task that asked for dates to be
|
|
104
|
+
// chosen was refused as unanswered because "the dates chosen" had nothing to quote. A filled control reads as
|
|
105
|
+
// "Label: value" after the text around it. Password and agent-typed secret values never leave the page.
|
|
106
|
+
const CONTROLS = new Set(['INPUT', 'SELECT', 'TEXTAREA']);
|
|
107
|
+
const UNFILLED = new Set(['hidden', 'password', 'checkbox', 'radio', 'button', 'submit', 'reset', 'image', 'file']);
|
|
108
|
+
const nameOf = (e, seen = new Set()) => {
|
|
109
|
+
if (!e || seen.has(e)) return '';
|
|
110
|
+
seen.add(e);
|
|
111
|
+
const root = e.getRootNode?.() ?? document;
|
|
112
|
+
const byId = id => (root.getElementById ? root : document).getElementById(id);
|
|
113
|
+
const referenced = (e.getAttribute('aria-labelledby') || '')
|
|
114
|
+
.split(/\s+/)
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map(id => nameOf(byId(id), seen))
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join(' ');
|
|
119
|
+
return clean(
|
|
120
|
+
referenced ||
|
|
121
|
+
e.getAttribute('aria-label') ||
|
|
122
|
+
[...(e.labels || [])]
|
|
123
|
+
.map(l => nameOf(l, seen))
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.join(' ') ||
|
|
126
|
+
(CONTROLS.has(e.tagName)
|
|
127
|
+
? ''
|
|
128
|
+
: [...e.childNodes]
|
|
129
|
+
.map(n =>
|
|
130
|
+
n.nodeType === 3
|
|
131
|
+
? n.textContent
|
|
132
|
+
: n.nodeType === 1 && !SKIP.has(n.tagName) && !CONTROLS.has(n.tagName)
|
|
133
|
+
? nameOf(n, seen)
|
|
134
|
+
: '',
|
|
135
|
+
)
|
|
136
|
+
.join(' ')) ||
|
|
137
|
+
e.getAttribute('title') ||
|
|
138
|
+
e.getAttribute('placeholder') ||
|
|
139
|
+
e.getAttribute('name') ||
|
|
140
|
+
'',
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
const fieldValue = e => {
|
|
144
|
+
if (e.tagName === 'SELECT') return [...e.selectedOptions].map(o => clean(o.label || o.text)).join(', ');
|
|
145
|
+
if (UNFILLED.has(e.type) || e.dataset?.fastbrowseSecret === '1') return '';
|
|
146
|
+
return clean(e.value ?? '');
|
|
147
|
+
};
|
|
148
|
+
const fields = el =>
|
|
149
|
+
(CONTROLS.has(el.tagName) ? [el] : [...el.querySelectorAll('input,select,textarea')])
|
|
150
|
+
.filter(e => (e.checkVisibility ? e.checkVisibility({ visibilityProperty: true }) : !hidden(e)))
|
|
151
|
+
.map(e => [nameOf(e), fieldValue(e)])
|
|
152
|
+
.filter(([, value]) => value)
|
|
153
|
+
.map(([name, value]) => (name ? `${name}: ${value}` : value));
|
|
154
|
+
|
|
155
|
+
const renderTable = table => {
|
|
156
|
+
// A table filter hides what it excludes with `hidden`, display or visibility (`collapse` is the one CSS made
|
|
157
|
+
// for rows), and a column toggle hides cells; read as rows, a filtered table answered from rows not shown.
|
|
158
|
+
// Visibility inherits, so the row's own style covers a collapsed section; display does not, so its section
|
|
159
|
+
// is checked too. A cell hidden by visibility still holds its column, so it stays, blank, and the cells
|
|
160
|
+
// after it keep their headers.
|
|
161
|
+
const rows = [
|
|
162
|
+
...table.querySelectorAll(':scope > tr, :scope > thead > tr, :scope > tbody > tr, :scope > tfoot > tr'),
|
|
163
|
+
]
|
|
164
|
+
.filter(row => !hidden(row) && getComputedStyle(row).visibility === 'visible' && !hidden(row.parentElement))
|
|
165
|
+
.map(row => ({ row, cells: [...row.querySelectorAll(':scope > th, :scope > td')].filter(c => !hidden(c)) }))
|
|
166
|
+
.filter(({ cells }) => cells.length);
|
|
167
|
+
if (!rows.length) return [];
|
|
168
|
+
let headers = rows.filter(({ row }) => row.parentElement.tagName === 'THEAD');
|
|
169
|
+
if (!headers.length && rows[0].cells.every(c => c.tagName === 'TH')) headers = [rows[0]];
|
|
170
|
+
const line = ({ cells }) => '| ' + cells.map(c => cellText(c).replace(/\|/g, '\\|')).join(' | ') + ' |';
|
|
171
|
+
const prefix = headers.map(line);
|
|
172
|
+
if (headers.length) prefix.push('| ' + headers[0].cells.map(() => '---').join(' | ') + ' |');
|
|
173
|
+
const data = rows.filter(row => !headers.includes(row));
|
|
174
|
+
return data.length ? data.map(row => [...prefix, line(row)].join('\n')) : [prefix.join('\n')];
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
function flush(run) {
|
|
178
|
+
if (!run.length) return;
|
|
179
|
+
const elements = run.filter(n => n.nodeType === 1);
|
|
180
|
+
let text = clean(run.map(n => (n.nodeType === 3 ? n.textContent : (n.innerText ?? ''))).join(''));
|
|
181
|
+
const onlyLink =
|
|
182
|
+
elements.length === 1 &&
|
|
183
|
+
elements[0].tagName === 'A' &&
|
|
184
|
+
elements[0].href &&
|
|
185
|
+
clean(elements[0].innerText ?? '') === text;
|
|
186
|
+
for (const e of elements) text = withRating(text, e);
|
|
187
|
+
if (onlyLink) push('link', text, { href: hrefOf(elements[0]) });
|
|
188
|
+
else push('paragraph', text);
|
|
189
|
+
for (const e of elements) for (const field of fields(e)) push('paragraph', field);
|
|
190
|
+
run.length = 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function leaf(el) {
|
|
194
|
+
return ![...el.children].some(c => !SKIP.has(c.tagName) && !hidden(c) && isBlock(c));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A classless div repeated three times is usually page layout; a record is a styled unit or a semantic item.
|
|
198
|
+
function repeated(el) {
|
|
199
|
+
if (!el.classList.length && el.tagName !== 'ARTICLE' && el.tagName !== 'LI') return false;
|
|
200
|
+
return (
|
|
201
|
+
[...(el.parentNode?.children ?? [])].filter(
|
|
202
|
+
sibling =>
|
|
203
|
+
sibling.tagName === el.tagName &&
|
|
204
|
+
sibling.classList.length === el.classList.length &&
|
|
205
|
+
[...el.classList].every(token => sibling.classList.contains(token)),
|
|
206
|
+
).length >= 3
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const records = new WeakMap();
|
|
211
|
+
function recordText(el) {
|
|
212
|
+
if (records.has(el)) return records.get(el);
|
|
213
|
+
records.set(el, null);
|
|
214
|
+
if (!repeated(el) || el.shadowRoot || el.querySelector('table,iframe,frame')) return null;
|
|
215
|
+
const descendants = [...el.querySelectorAll('*')];
|
|
216
|
+
if (descendants.some(c => c.shadowRoot) || descendants.filter(c => HEADINGS[c.tagName]).length > 1) return null;
|
|
217
|
+
if (!descendants.some(c => !SKIP.has(c.tagName) && !hidden(c) && isBlock(c))) return null;
|
|
218
|
+
const text = withRating(textOf(el), el);
|
|
219
|
+
if (!text || text.length > 1500) return null;
|
|
220
|
+
// A repeated wrapper around another list is a group, so keep the inner records separately citable.
|
|
221
|
+
if (descendants.some(c => (c.tagName === 'LI' && leaf(c)) || recordText(c) !== null)) return null;
|
|
222
|
+
records.set(el, text);
|
|
223
|
+
return text;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function walk(el) {
|
|
227
|
+
const run = [];
|
|
228
|
+
for (const node of el.childNodes) {
|
|
229
|
+
if (node.nodeType === 3) {
|
|
230
|
+
run.push(node);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (node.nodeType !== 1 || hidden(node)) continue;
|
|
234
|
+
if (FRAMES.has(node.tagName)) {
|
|
235
|
+
flush(run);
|
|
236
|
+
enter(node, path);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (SKIP.has(node.tagName)) continue;
|
|
240
|
+
if (!isBlock(node) && recordText(node) === null) {
|
|
241
|
+
run.push(node);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
flush(run);
|
|
245
|
+
block(node);
|
|
246
|
+
}
|
|
247
|
+
flush(run);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function block(el) {
|
|
251
|
+
const level = HEADINGS[el.tagName];
|
|
252
|
+
if (level) {
|
|
253
|
+
const text = textOf(el);
|
|
254
|
+
while (path.length >= level) path.pop();
|
|
255
|
+
if (text) {
|
|
256
|
+
push('heading', text);
|
|
257
|
+
path.push(text);
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (el.tagName === 'TABLE') {
|
|
262
|
+
for (const text of renderTable(el)) push('table', text);
|
|
263
|
+
// A form laid out in a table: its cells render as text, which holds none of the controls' values.
|
|
264
|
+
for (const field of fields(el)) push('paragraph', field);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (el.tagName === 'PRE') return push('code', textOf(el));
|
|
268
|
+
const record = recordText(el);
|
|
269
|
+
if ((el.tagName === 'LI' && leaf(el)) || record !== null) {
|
|
270
|
+
const links = el.querySelectorAll('a[href]');
|
|
271
|
+
push(
|
|
272
|
+
el.tagName === 'LI' ? 'list_item' : 'record',
|
|
273
|
+
record ?? withRating(textOf(el), el),
|
|
274
|
+
links.length === 1 ? { href: hrefOf(links[0]) } : {},
|
|
275
|
+
);
|
|
276
|
+
for (const field of fields(el)) push('paragraph', field);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (CONTROLS.has(el.tagName)) {
|
|
280
|
+
for (const field of fields(el)) push('paragraph', field);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
walk(el);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// A same-origin frame is read where it stands, under the headings above it: read after the whole page, an
|
|
287
|
+
// embedded subscription form's heading followed the footer, and the reader, asked for the heading the
|
|
288
|
+
// frame shows, answered with the page's heading over the frame instead.
|
|
289
|
+
const FRAMES = new Set(['IFRAME', 'FRAME']);
|
|
290
|
+
const entered = new WeakSet();
|
|
291
|
+
function enter(e, headings) {
|
|
292
|
+
entered.add(e);
|
|
293
|
+
let inner = null;
|
|
294
|
+
try {
|
|
295
|
+
inner = e.contentDocument;
|
|
296
|
+
} catch {
|
|
297
|
+
inner = null;
|
|
298
|
+
}
|
|
299
|
+
if (!inner?.body) {
|
|
300
|
+
inaccessible++;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const child = `${framePath || 'root'}/${identity(e)}`;
|
|
304
|
+
scope(inner.body, child, child, headings);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function scope(root, frame, source, headings = []) {
|
|
308
|
+
const previous = [path, framePath, sourcePath];
|
|
309
|
+
path = [...headings];
|
|
310
|
+
framePath = frame;
|
|
311
|
+
sourcePath = source;
|
|
312
|
+
walk(root);
|
|
313
|
+
for (const e of root.querySelectorAll('*')) {
|
|
314
|
+
if (e.shadowRoot) scope(e.shadowRoot, frame, `${source}/shadow:${identity(e)}`);
|
|
315
|
+
// A frame the walk never reached (hidden, or inside a record or table read as one block) is still read.
|
|
316
|
+
if (FRAMES.has(e.tagName) && !entered.has(e)) enter(e, []);
|
|
317
|
+
}
|
|
318
|
+
[path, framePath, sourcePath] = previous;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
scope(document.body, null, '');
|
|
322
|
+
return { url: location.href, title: document.title, blocks, inaccessible_frames: inaccessible };
|
|
323
|
+
})()
|