@michaelmagan/dnumb 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/AGENTS.md +180 -0
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/analysis/cluster.cjs +114 -0
- package/dist/analysis/cluster.d.cts +68 -0
- package/dist/analysis/cluster.d.ts +68 -0
- package/dist/analysis/cluster.js +109 -0
- package/dist/analysis/distances.cjs +124 -0
- package/dist/analysis/distances.d.cts +58 -0
- package/dist/analysis/distances.d.ts +58 -0
- package/dist/analysis/distances.js +119 -0
- package/dist/analysis/geometry.cjs +60 -0
- package/dist/analysis/geometry.d.cts +34 -0
- package/dist/analysis/geometry.d.ts +34 -0
- package/dist/analysis/geometry.js +53 -0
- package/dist/analysis/layout-tree.cjs +245 -0
- package/dist/analysis/layout-tree.d.cts +45 -0
- package/dist/analysis/layout-tree.d.ts +45 -0
- package/dist/analysis/layout-tree.js +242 -0
- package/dist/analysis/region-query.cjs +71 -0
- package/dist/analysis/region-query.d.cts +25 -0
- package/dist/analysis/region-query.d.ts +25 -0
- package/dist/analysis/region-query.js +67 -0
- package/dist/analysis/types.cjs +7 -0
- package/dist/analysis/types.d.cts +143 -0
- package/dist/analysis/types.d.ts +143 -0
- package/dist/analysis/types.js +6 -0
- package/dist/bundle.cjs +182 -0
- package/dist/bundle.d.cts +91 -0
- package/dist/bundle.d.ts +91 -0
- package/dist/bundle.js +175 -0
- package/dist/cli.cjs +755 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +719 -0
- package/dist/diff/changes.cjs +656 -0
- package/dist/diff/changes.d.cts +2 -0
- package/dist/diff/changes.d.ts +2 -0
- package/dist/diff/changes.js +653 -0
- package/dist/diff/identity.cjs +167 -0
- package/dist/diff/identity.d.cts +62 -0
- package/dist/diff/identity.d.ts +62 -0
- package/dist/diff/identity.js +159 -0
- package/dist/diff/index.cjs +84 -0
- package/dist/diff/index.d.cts +76 -0
- package/dist/diff/index.d.ts +76 -0
- package/dist/diff/index.js +68 -0
- package/dist/diff/match.cjs +156 -0
- package/dist/diff/match.d.cts +17 -0
- package/dist/diff/match.d.ts +17 -0
- package/dist/diff/match.js +152 -0
- package/dist/diff/render.cjs +540 -0
- package/dist/diff/render.d.cts +2 -0
- package/dist/diff/render.d.ts +2 -0
- package/dist/diff/render.js +537 -0
- package/dist/diff/types.cjs +2 -0
- package/dist/diff/types.d.cts +211 -0
- package/dist/diff/types.d.ts +211 -0
- package/dist/diff/types.js +1 -0
- package/dist/digest/index.cjs +8 -0
- package/dist/digest/index.d.cts +4 -0
- package/dist/digest/index.d.ts +4 -0
- package/dist/digest/index.js +2 -0
- package/dist/digest/parse.cjs +163 -0
- package/dist/digest/parse.d.cts +80 -0
- package/dist/digest/parse.d.ts +80 -0
- package/dist/digest/parse.js +160 -0
- package/dist/digest/render.cjs +1575 -0
- package/dist/digest/render.d.cts +41 -0
- package/dist/digest/render.d.ts +41 -0
- package/dist/digest/render.js +1571 -0
- package/dist/digest/types.cjs +10 -0
- package/dist/digest/types.d.cts +86 -0
- package/dist/digest/types.d.ts +86 -0
- package/dist/digest/types.js +9 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +19 -0
- package/dist/page.cjs +35 -0
- package/dist/page.d.cts +40 -0
- package/dist/page.d.ts +40 -0
- package/dist/page.js +31 -0
- package/dist/playwright.cjs +138 -0
- package/dist/playwright.d.cts +69 -0
- package/dist/playwright.d.ts +69 -0
- package/dist/playwright.js +131 -0
- package/dist/query.cjs +1152 -0
- package/dist/query.d.cts +47 -0
- package/dist/query.d.ts +47 -0
- package/dist/query.js +1146 -0
- package/dist/snapshot/capture.cjs +80 -0
- package/dist/snapshot/capture.d.cts +34 -0
- package/dist/snapshot/capture.d.ts +34 -0
- package/dist/snapshot/capture.js +77 -0
- package/dist/snapshot/cdp.cjs +450 -0
- package/dist/snapshot/cdp.d.cts +36 -0
- package/dist/snapshot/cdp.d.ts +36 -0
- package/dist/snapshot/cdp.js +447 -0
- package/dist/snapshot/clip.cjs +100 -0
- package/dist/snapshot/clip.d.cts +26 -0
- package/dist/snapshot/clip.d.ts +26 -0
- package/dist/snapshot/clip.js +96 -0
- package/dist/snapshot/collect.cjs +358 -0
- package/dist/snapshot/collect.d.cts +18 -0
- package/dist/snapshot/collect.d.ts +18 -0
- package/dist/snapshot/collect.js +355 -0
- package/dist/snapshot/color.cjs +1039 -0
- package/dist/snapshot/color.d.cts +316 -0
- package/dist/snapshot/color.d.ts +316 -0
- package/dist/snapshot/color.js +1026 -0
- package/dist/snapshot/facts.cjs +167 -0
- package/dist/snapshot/facts.d.cts +48 -0
- package/dist/snapshot/facts.d.ts +48 -0
- package/dist/snapshot/facts.js +161 -0
- package/dist/snapshot/limits.cjs +69 -0
- package/dist/snapshot/limits.d.cts +59 -0
- package/dist/snapshot/limits.d.ts +59 -0
- package/dist/snapshot/limits.js +65 -0
- package/dist/snapshot/not-measured.cjs +44 -0
- package/dist/snapshot/not-measured.d.cts +14 -0
- package/dist/snapshot/not-measured.d.ts +14 -0
- package/dist/snapshot/not-measured.js +41 -0
- package/dist/snapshot/types.cjs +2 -0
- package/dist/snapshot/types.d.cts +238 -0
- package/dist/snapshot/types.d.ts +238 -0
- package/dist/snapshot/types.js +1 -0
- package/dist/spec/index.cjs +9 -0
- package/dist/spec/index.d.cts +15 -0
- package/dist/spec/index.d.ts +15 -0
- package/dist/spec/index.js +6 -0
- package/package.json +141 -0
- package/skills/dnumb/SKILL.md +136 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `DOMSnapshot.captureSnapshot` fast path. Chromium only.
|
|
3
|
+
*
|
|
4
|
+
* One round trip returns the whole document: per-node bounds, scroll/client
|
|
5
|
+
* rects, paint order, and any computed style requested by name. Measured ~6x
|
|
6
|
+
* faster than an equivalent in-page walk (68ms vs 412ms on a Wikipedia
|
|
7
|
+
* article) with a smaller payload. `scrollRects[2] === el.scrollWidth` and
|
|
8
|
+
* `clientRects[2] === el.clientWidth` natively — the pair the whole overflow
|
|
9
|
+
* fact rests on.
|
|
10
|
+
*
|
|
11
|
+
* The cost is that everything arrives as flat parallel arrays of indexes into a
|
|
12
|
+
* shared string table, so rebuilding `ElementRecord[]` is genuine index
|
|
13
|
+
* juggling. Three things that are easy to get wrong, all handled below:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Coordinates are document-space and scroll-invariant.** The in-page walk
|
|
16
|
+
* returns viewport-space rects from `getBoundingClientRect`. Subtracting the
|
|
17
|
+
* document scroll offset is what makes the two paths agree.
|
|
18
|
+
* 2. **A node with no layout row has no box at all.** That absence *is* the
|
|
19
|
+
* `display:none` signal, and it is cleaner than a boolean.
|
|
20
|
+
* 3. **Style names are validated server-side and one bad name throws the whole
|
|
21
|
+
* call.** They must be kebab-case and Blink-recognized; custom properties
|
|
22
|
+
* cannot be requested at all.
|
|
23
|
+
*
|
|
24
|
+
* `DOMSnapshot` is an experimental domain. Every failure here falls back to the
|
|
25
|
+
* portable walk rather than surfacing.
|
|
26
|
+
*/
|
|
27
|
+
import type { CDPCapablePage } from "../page.cjs";
|
|
28
|
+
import type { Snapshot } from "./types.cjs";
|
|
29
|
+
/**
|
|
30
|
+
* Capture via CDP, or `null` if this page or this Chromium cannot.
|
|
31
|
+
*
|
|
32
|
+
* Returning null rather than throwing is deliberate: the caller's job is to
|
|
33
|
+
* fall back to the portable walk, and an experimental domain failing is an
|
|
34
|
+
* expected outcome, not an error.
|
|
35
|
+
*/
|
|
36
|
+
export declare function captureViaCDP(page: CDPCapablePage): Promise<Snapshot | null>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `DOMSnapshot.captureSnapshot` fast path. Chromium only.
|
|
3
|
+
*
|
|
4
|
+
* One round trip returns the whole document: per-node bounds, scroll/client
|
|
5
|
+
* rects, paint order, and any computed style requested by name. Measured ~6x
|
|
6
|
+
* faster than an equivalent in-page walk (68ms vs 412ms on a Wikipedia
|
|
7
|
+
* article) with a smaller payload. `scrollRects[2] === el.scrollWidth` and
|
|
8
|
+
* `clientRects[2] === el.clientWidth` natively — the pair the whole overflow
|
|
9
|
+
* fact rests on.
|
|
10
|
+
*
|
|
11
|
+
* The cost is that everything arrives as flat parallel arrays of indexes into a
|
|
12
|
+
* shared string table, so rebuilding `ElementRecord[]` is genuine index
|
|
13
|
+
* juggling. Three things that are easy to get wrong, all handled below:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Coordinates are document-space and scroll-invariant.** The in-page walk
|
|
16
|
+
* returns viewport-space rects from `getBoundingClientRect`. Subtracting the
|
|
17
|
+
* document scroll offset is what makes the two paths agree.
|
|
18
|
+
* 2. **A node with no layout row has no box at all.** That absence *is* the
|
|
19
|
+
* `display:none` signal, and it is cleaner than a boolean.
|
|
20
|
+
* 3. **Style names are validated server-side and one bad name throws the whole
|
|
21
|
+
* call.** They must be kebab-case and Blink-recognized; custom properties
|
|
22
|
+
* cannot be requested at all.
|
|
23
|
+
*
|
|
24
|
+
* `DOMSnapshot` is an experimental domain. Every failure here falls back to the
|
|
25
|
+
* portable walk rather than surfacing.
|
|
26
|
+
*/
|
|
27
|
+
import type { CDPCapablePage } from "../page.js";
|
|
28
|
+
import type { Snapshot } from "./types.js";
|
|
29
|
+
/**
|
|
30
|
+
* Capture via CDP, or `null` if this page or this Chromium cannot.
|
|
31
|
+
*
|
|
32
|
+
* Returning null rather than throwing is deliberate: the caller's job is to
|
|
33
|
+
* fall back to the portable walk, and an experimental domain failing is an
|
|
34
|
+
* expected outcome, not an error.
|
|
35
|
+
*/
|
|
36
|
+
export declare function captureViaCDP(page: CDPCapablePage): Promise<Snapshot | null>;
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { COLOR_TOKEN_CAP, capBackgroundImage, MAX_ELEMENTS, NAME_LENGTH, SKIPPED_TAGS, TESTID_ATTRS, TEXT_PREVIEW_LENGTH, } from "./limits.js";
|
|
2
|
+
/**
|
|
3
|
+
* Requested in this exact order; `layout.styles[i]` comes back in it. Every
|
|
4
|
+
* name is kebab-case because Blink rejects camelCase and takes the whole call
|
|
5
|
+
* down with it (`whiteSpace` -> "Protocol error: invalid CSS property").
|
|
6
|
+
*/
|
|
7
|
+
// biome-ignore format: the order of this list is the wire format -- one name per
|
|
8
|
+
// line would hide the grouping that makes a mismatch with `S` visible.
|
|
9
|
+
const STYLES = [
|
|
10
|
+
'color', 'background-color', 'font-size', 'font-weight', 'text-align',
|
|
11
|
+
'overflow-x', 'overflow-y', 'position', 'display', 'visibility', 'opacity',
|
|
12
|
+
'flex-direction', 'grid-template-columns', 'row-gap', 'column-gap',
|
|
13
|
+
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
14
|
+
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
15
|
+
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
|
16
|
+
'text-overflow', 'white-space', 'overflow-wrap',
|
|
17
|
+
'background-image', 'text-shadow', '-webkit-text-stroke-width', '-webkit-text-stroke-color',
|
|
18
|
+
// Requested so `build()` can read `<html>`'s own values off this same
|
|
19
|
+
// per-node styles array -- see `DocumentBackground` (A2). Not per-element
|
|
20
|
+
// data; every other record's slot for this name is simply unused.
|
|
21
|
+
'color-scheme',
|
|
22
|
+
];
|
|
23
|
+
const S = Object.fromEntries(STYLES.map((name, i) => [name, i]));
|
|
24
|
+
const ELEMENT_NODE = 1;
|
|
25
|
+
const TEXT_NODE = 3;
|
|
26
|
+
/** Roles too generic to be worth carrying; matches the identity ladder's set. */
|
|
27
|
+
const WEAK_AX_ROLES = new Set(['generic', 'none', 'presentation', 'InlineTextBox']);
|
|
28
|
+
function rect(r, dx = 0, dy = 0) {
|
|
29
|
+
return {
|
|
30
|
+
x: (r?.[0] ?? 0) - dx,
|
|
31
|
+
y: (r?.[1] ?? 0) - dy,
|
|
32
|
+
width: r?.[2] ?? 0,
|
|
33
|
+
height: r?.[3] ?? 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function sides(values, strings, keys) {
|
|
37
|
+
const num = (i) => Number.parseFloat(strings[values?.[i] ?? -1] ?? '') || 0;
|
|
38
|
+
return { top: num(keys[0]), right: num(keys[1]), bottom: num(keys[2]), left: num(keys[3]) };
|
|
39
|
+
}
|
|
40
|
+
function trimName(value) {
|
|
41
|
+
const text = (value ?? '').replace(/\s+/g, ' ').trim();
|
|
42
|
+
return text ? text.slice(0, NAME_LENGTH) : null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Same-origin vs cross-origin for an `<iframe>` found in `documents[0]`, using
|
|
46
|
+
* only that document's own data: resolve `src` against the document's own
|
|
47
|
+
* `baseURL`, exactly as the browser resolves it when it assigns `iframe.src`.
|
|
48
|
+
* This never reads `documents[1..]` -- the slots `captureViaCDP` does not use,
|
|
49
|
+
* per the file header -- classifying an iframe's origin is not the same as
|
|
50
|
+
* reading what is inside it.
|
|
51
|
+
*/
|
|
52
|
+
function iframeOrigin(attrs, baseURL) {
|
|
53
|
+
// `srcdoc` and no `src` both resolve to a document that inherits the
|
|
54
|
+
// parent's origin (`about:srcdoc` / `about:blank`), not the frame's own.
|
|
55
|
+
if (attrs.has('srcdoc'))
|
|
56
|
+
return 'same';
|
|
57
|
+
const src = (attrs.get('src') ?? '').trim();
|
|
58
|
+
if (src === '' || src.toLowerCase() === 'about:blank')
|
|
59
|
+
return 'same';
|
|
60
|
+
try {
|
|
61
|
+
return new URL(src, baseURL).origin === new URL(baseURL).origin ? 'same' : 'cross';
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return 'unknown';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Real open/closed shadow root counts from `nodes.shadowRootType`, or the
|
|
69
|
+
* honest "cannot know" shape when the payload has no such field at all.
|
|
70
|
+
*
|
|
71
|
+
* Every node flattened out of a shadow root is tagged `'open'` or `'closed'`
|
|
72
|
+
* (see `NodeArrays.shadowRootType`'s doc comment), not just the root itself,
|
|
73
|
+
* so counting tagged *nodes* would count a shadow root once per descendant it
|
|
74
|
+
* flattened in. Instead, for each tagged node this walks `parentIndex` up
|
|
75
|
+
* through other tagged nodes until it reaches the first untagged ancestor --
|
|
76
|
+
* that ancestor is the shadow host -- and counts each distinct host once,
|
|
77
|
+
* keyed by the type its tagged descendants carried. A shadow root with no
|
|
78
|
+
* flattened content at all (empty) has no tagged node to find and is not
|
|
79
|
+
* counted; a shadow root nested inside another shadow root is counted once
|
|
80
|
+
* under the outer host rather than twice, because the walk does not stop at
|
|
81
|
+
* the inner boundary -- an known, narrow gap on real pages, which is why this
|
|
82
|
+
* function is a best-effort count, not a claim of perfect precision.
|
|
83
|
+
*/
|
|
84
|
+
function countShadowRoots(nodes, strings) {
|
|
85
|
+
const srt = nodes.shadowRootType;
|
|
86
|
+
// The field is absent, not merely empty, on a payload this build has never
|
|
87
|
+
// seen shaped this way -- report the same "cannot know" shape the walk
|
|
88
|
+
// path uses rather than assuming zero.
|
|
89
|
+
if (!srt)
|
|
90
|
+
return { open: 0, closed: 'unknown' };
|
|
91
|
+
const typeByNode = new Map();
|
|
92
|
+
for (let i = 0; i < srt.index.length; i++) {
|
|
93
|
+
const nodeIdx = srt.index[i];
|
|
94
|
+
const valueIdx = srt.value[i];
|
|
95
|
+
if (nodeIdx === undefined || valueIdx === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
const value = strings[valueIdx];
|
|
98
|
+
// `'user-agent'` (a browser-internal shadow root, e.g. inside `<video>`)
|
|
99
|
+
// is a real `ShadowRootType` this field can carry but which this package
|
|
100
|
+
// has no author-facing fact to report -- excluded from both counts rather
|
|
101
|
+
// than folded into either.
|
|
102
|
+
typeByNode.set(nodeIdx, value === 'open' ? 'open' : value === 'closed' ? 'closed' : null);
|
|
103
|
+
}
|
|
104
|
+
const hostType = new Map();
|
|
105
|
+
for (const [nodeIdx, type] of typeByNode) {
|
|
106
|
+
if (!type)
|
|
107
|
+
continue;
|
|
108
|
+
let cursor = nodes.parentIndex[nodeIdx];
|
|
109
|
+
while (cursor !== undefined && cursor >= 0 && typeByNode.has(cursor)) {
|
|
110
|
+
cursor = nodes.parentIndex[cursor];
|
|
111
|
+
}
|
|
112
|
+
if (cursor === undefined || cursor < 0)
|
|
113
|
+
continue;
|
|
114
|
+
hostType.set(cursor, type);
|
|
115
|
+
}
|
|
116
|
+
let open = 0;
|
|
117
|
+
let closed = 0;
|
|
118
|
+
for (const type of hostType.values()) {
|
|
119
|
+
if (type === 'open')
|
|
120
|
+
open++;
|
|
121
|
+
else
|
|
122
|
+
closed++;
|
|
123
|
+
}
|
|
124
|
+
return { open, closed };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Computed role and accessible name, which `captureSnapshot` does not return —
|
|
128
|
+
* it carries only the literal `role=` attribute, so a `<button>` comes back
|
|
129
|
+
* with no role at all. This is a second experimental call joined on
|
|
130
|
+
* `backendDOMNodeId`.
|
|
131
|
+
*
|
|
132
|
+
* Failure is non-fatal: without it the records simply carry the attribute role,
|
|
133
|
+
* which is the state the prototype shipped in.
|
|
134
|
+
*/
|
|
135
|
+
async function axIndex(cdp) {
|
|
136
|
+
const out = new Map();
|
|
137
|
+
let tree;
|
|
138
|
+
try {
|
|
139
|
+
tree = (await cdp.send('Accessibility.getFullAXTree'));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
for (const node of tree.nodes ?? []) {
|
|
145
|
+
if (node.backendDOMNodeId === undefined || node.ignored)
|
|
146
|
+
continue;
|
|
147
|
+
const role = typeof node.role?.value === 'string' ? node.role.value : null;
|
|
148
|
+
const name = typeof node.name?.value === 'string' ? node.name.value : null;
|
|
149
|
+
out.set(node.backendDOMNodeId, {
|
|
150
|
+
role: role && !WEAK_AX_ROLES.has(role) ? role : null,
|
|
151
|
+
name: trimName(name),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Capture via CDP, or `null` if this page or this Chromium cannot.
|
|
158
|
+
*
|
|
159
|
+
* Returning null rather than throwing is deliberate: the caller's job is to
|
|
160
|
+
* fall back to the portable walk, and an experimental domain failing is an
|
|
161
|
+
* expected outcome, not an error.
|
|
162
|
+
*/
|
|
163
|
+
export async function captureViaCDP(page) {
|
|
164
|
+
let cdp;
|
|
165
|
+
try {
|
|
166
|
+
cdp = await page.context().newCDPSession(page);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const snapshot = (await cdp.send('DOMSnapshot.captureSnapshot', {
|
|
173
|
+
computedStyles: [...STYLES],
|
|
174
|
+
includeDOMRects: true,
|
|
175
|
+
includePaintOrder: true,
|
|
176
|
+
}));
|
|
177
|
+
const viewport = await page.evaluate(() => ({
|
|
178
|
+
width: window.innerWidth,
|
|
179
|
+
height: window.innerHeight,
|
|
180
|
+
}));
|
|
181
|
+
const ax = await axIndex(cdp);
|
|
182
|
+
const doc = snapshot.documents[0];
|
|
183
|
+
if (!doc)
|
|
184
|
+
return null;
|
|
185
|
+
return { ...build(doc, snapshot.strings, ax), viewport };
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
await cdp.detach().catch(() => { });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function build(doc, strings, ax) {
|
|
195
|
+
const { nodes, layout } = doc;
|
|
196
|
+
const str = (i) => i === undefined || i < 0 ? '' : (strings[i] ?? '');
|
|
197
|
+
// layout rows are sparse: only nodes that generated a box have one.
|
|
198
|
+
const layoutOf = new Map();
|
|
199
|
+
for (let layoutIdx = 0; layoutIdx < layout.nodeIndex.length; layoutIdx++) {
|
|
200
|
+
const nodeIdx = layout.nodeIndex[layoutIdx];
|
|
201
|
+
if (nodeIdx !== undefined)
|
|
202
|
+
layoutOf.set(nodeIdx, layoutIdx);
|
|
203
|
+
}
|
|
204
|
+
const attrsOf = (nodeIdx) => {
|
|
205
|
+
const flat = nodes.attributes[nodeIdx] ?? [];
|
|
206
|
+
const map = new Map();
|
|
207
|
+
for (let i = 0; i + 1 < flat.length; i += 2) {
|
|
208
|
+
map.set(str(flat[i]), str(flat[i + 1]));
|
|
209
|
+
}
|
|
210
|
+
return map;
|
|
211
|
+
};
|
|
212
|
+
// Own text: direct text-node children only, matching the walk's `ownText`.
|
|
213
|
+
const ownText = new Map();
|
|
214
|
+
nodes.nodeType.forEach((type, nodeIdx) => {
|
|
215
|
+
if (type !== TEXT_NODE)
|
|
216
|
+
return;
|
|
217
|
+
const parent = nodes.parentIndex[nodeIdx];
|
|
218
|
+
if (parent === undefined || parent < 0)
|
|
219
|
+
return;
|
|
220
|
+
ownText.set(parent, (ownText.get(parent) ?? '') + str(nodes.nodeValue[nodeIdx]));
|
|
221
|
+
});
|
|
222
|
+
// `<html>`'s own `background-color` / `color-scheme` -- the real terminal
|
|
223
|
+
// layer a compositing walk that runs off the top of `<body>`'s ancestor
|
|
224
|
+
// chain reaches (A2). Read the same way any element's own styles are, off
|
|
225
|
+
// this same per-node `layout.styles` array -- `<html>` itself is never one
|
|
226
|
+
// of `elements` below, this walk stays inside the <body> subtree exactly
|
|
227
|
+
// like the portable walk does, so without this it would be unreachable.
|
|
228
|
+
//
|
|
229
|
+
// The `nodeType === ELEMENT_NODE` guard is required, not defensive
|
|
230
|
+
// decoration: `<!doctype html>` is its own node in this same
|
|
231
|
+
// `nodes.nodeName` array (a `DocumentType` node, name `"html"`), and it
|
|
232
|
+
// sorts *before* the real `<html>` element -- an unguarded search finds
|
|
233
|
+
// the doctype first, which has no layout row at all, and every style
|
|
234
|
+
// this function reads off it comes back the empty string. *Verified*:
|
|
235
|
+
// this is exactly what happened on the first pass of this fix, caught by
|
|
236
|
+
// `test/browser/color-canvas.test.ts`'s `cdp`-path case.
|
|
237
|
+
const htmlIndex = nodes.nodeName.findIndex((name, i) => nodes.nodeType[i] === ELEMENT_NODE && str(name).toUpperCase() === 'HTML');
|
|
238
|
+
const htmlLayoutIdx = htmlIndex < 0 ? undefined : layoutOf.get(htmlIndex);
|
|
239
|
+
const htmlStyles = htmlLayoutIdx === undefined ? undefined : layout.styles[htmlLayoutIdx];
|
|
240
|
+
const htmlStyle = (name) => str(htmlStyles?.[S[name]]);
|
|
241
|
+
const documentBackground = {
|
|
242
|
+
backgroundColor: htmlStyle('background-color'),
|
|
243
|
+
colorScheme: htmlStyle('color-scheme'),
|
|
244
|
+
};
|
|
245
|
+
// Only the <body> subtree, matching the walk's entry point.
|
|
246
|
+
const bodyIndex = nodes.nodeName.findIndex((name) => str(name).toUpperCase() === 'BODY');
|
|
247
|
+
if (bodyIndex < 0) {
|
|
248
|
+
return {
|
|
249
|
+
elements: [],
|
|
250
|
+
truncated: false,
|
|
251
|
+
notMeasured: {
|
|
252
|
+
shadowRoots: countShadowRoots(nodes, strings),
|
|
253
|
+
iframes: { sameOrigin: 0, crossOrigin: 0, unknownOrigin: 0 },
|
|
254
|
+
},
|
|
255
|
+
documentBackground,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const SKIPPED = new Set(SKIPPED_TAGS);
|
|
259
|
+
const idOf = new Map();
|
|
260
|
+
const elements = [];
|
|
261
|
+
let truncated = false;
|
|
262
|
+
const baseURL = str(doc.baseURL);
|
|
263
|
+
let iframesSameOrigin = 0;
|
|
264
|
+
let iframesCrossOrigin = 0;
|
|
265
|
+
let iframesUnknownOrigin = 0;
|
|
266
|
+
const inBody = (nodeIdx) => {
|
|
267
|
+
let cursor = nodeIdx;
|
|
268
|
+
while (cursor !== undefined && cursor >= 0) {
|
|
269
|
+
if (cursor === bodyIndex)
|
|
270
|
+
return true;
|
|
271
|
+
cursor = nodes.parentIndex[cursor];
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
};
|
|
275
|
+
// Document order is array order, so one forward pass keys parents before
|
|
276
|
+
// children — the same invariant the recursive walk gets for free.
|
|
277
|
+
for (let nodeIdx = 0; nodeIdx < nodes.nodeType.length; nodeIdx++) {
|
|
278
|
+
// Same ceiling as the walk, so `truncated` means one thing across paths.
|
|
279
|
+
if (elements.length >= MAX_ELEMENTS) {
|
|
280
|
+
truncated = true;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
if (nodes.nodeType[nodeIdx] !== ELEMENT_NODE)
|
|
284
|
+
continue;
|
|
285
|
+
if (!inBody(nodeIdx))
|
|
286
|
+
continue;
|
|
287
|
+
const tagName = str(nodes.nodeName[nodeIdx]).toUpperCase();
|
|
288
|
+
if (SKIPPED.has(tagName))
|
|
289
|
+
continue;
|
|
290
|
+
// `documents[0]` is the only slot this path reads (see the file header),
|
|
291
|
+
// so an iframe's own content document is never entered -- counted here
|
|
292
|
+
// regardless of whether the iframe itself survives the ancestor check
|
|
293
|
+
// below, because it was encountered either way.
|
|
294
|
+
if (tagName === 'IFRAME') {
|
|
295
|
+
switch (iframeOrigin(attrsOf(nodeIdx), baseURL)) {
|
|
296
|
+
case 'same':
|
|
297
|
+
iframesSameOrigin++;
|
|
298
|
+
break;
|
|
299
|
+
case 'cross':
|
|
300
|
+
iframesCrossOrigin++;
|
|
301
|
+
break;
|
|
302
|
+
case 'unknown':
|
|
303
|
+
iframesUnknownOrigin++;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// An ancestor that was skipped takes its subtree with it.
|
|
308
|
+
let parentIdx = nodes.parentIndex[nodeIdx];
|
|
309
|
+
while (parentIdx !== undefined && parentIdx >= 0 && !idOf.has(parentIdx)) {
|
|
310
|
+
if (parentIdx === bodyIndex)
|
|
311
|
+
break;
|
|
312
|
+
parentIdx = nodes.parentIndex[parentIdx];
|
|
313
|
+
}
|
|
314
|
+
const parentId = parentIdx !== undefined && parentIdx >= 0 ? (idOf.get(parentIdx) ?? null) : null;
|
|
315
|
+
if (nodeIdx !== bodyIndex && parentId === null)
|
|
316
|
+
continue;
|
|
317
|
+
const id = `e${elements.length}`;
|
|
318
|
+
idOf.set(nodeIdx, id);
|
|
319
|
+
const attrs = attrsOf(nodeIdx);
|
|
320
|
+
const layoutIdx = layoutOf.get(nodeIdx);
|
|
321
|
+
const styles = layoutIdx === undefined ? undefined : layout.styles[layoutIdx];
|
|
322
|
+
const box = rect(layoutIdx === undefined ? undefined : layout.bounds[layoutIdx], doc.scrollOffsetX, doc.scrollOffsetY);
|
|
323
|
+
const scrollRect = layoutIdx === undefined ? undefined : layout.scrollRects?.[layoutIdx];
|
|
324
|
+
const clientRect = layoutIdx === undefined ? undefined : layout.clientRects?.[layoutIdx];
|
|
325
|
+
const style = (name) => str(styles?.[S[name]]);
|
|
326
|
+
// A node with no layout row generated no box: that absence is the signal.
|
|
327
|
+
const display = layoutIdx === undefined ? 'none' : style('display') || 'block';
|
|
328
|
+
const visibility = style('visibility') || 'visible';
|
|
329
|
+
const opacity = Number.parseFloat(style('opacity'));
|
|
330
|
+
let testId = null;
|
|
331
|
+
for (const attr of TESTID_ATTRS) {
|
|
332
|
+
const value = attrs.get(attr);
|
|
333
|
+
if (value) {
|
|
334
|
+
testId = value;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const backendId = nodes.backendNodeId[nodeIdx];
|
|
339
|
+
const computed = backendId === undefined ? undefined : ax.get(backendId);
|
|
340
|
+
const attrRole = (attrs.get('role') ?? '').trim().split(/\s+/)[0] || null;
|
|
341
|
+
const ariaLevel = Number(attrs.get('aria-level'));
|
|
342
|
+
const headingMatch = /^H([1-6])$/.exec(tagName);
|
|
343
|
+
elements.push({
|
|
344
|
+
id,
|
|
345
|
+
tag: tagName.toLowerCase(),
|
|
346
|
+
testId,
|
|
347
|
+
domId: attrs.get('id') ?? null,
|
|
348
|
+
role: computed?.role ?? attrRole,
|
|
349
|
+
name: computed?.name ?? null,
|
|
350
|
+
className: attrs.get('class') ?? '',
|
|
351
|
+
headingLevel: Number.isFinite(ariaLevel) && attrs.has('aria-level')
|
|
352
|
+
? ariaLevel
|
|
353
|
+
: headingMatch?.[1]
|
|
354
|
+
? Number(headingMatch[1])
|
|
355
|
+
: null,
|
|
356
|
+
rect: box,
|
|
357
|
+
scrollWidth: scrollRect?.[2] ?? box.width,
|
|
358
|
+
scrollHeight: scrollRect?.[3] ?? box.height,
|
|
359
|
+
clientWidth: clientRect?.[2] ?? box.width,
|
|
360
|
+
clientHeight: clientRect?.[3] ?? box.height,
|
|
361
|
+
overflowX: style('overflow-x') || 'visible',
|
|
362
|
+
overflowY: style('overflow-y') || 'visible',
|
|
363
|
+
position: style('position') || 'static',
|
|
364
|
+
display,
|
|
365
|
+
visibility,
|
|
366
|
+
opacity: Number.isFinite(opacity) ? opacity : 1,
|
|
367
|
+
margin: sides(styles, strings, [
|
|
368
|
+
S['margin-top'],
|
|
369
|
+
S['margin-right'],
|
|
370
|
+
S['margin-bottom'],
|
|
371
|
+
S['margin-left'],
|
|
372
|
+
]),
|
|
373
|
+
padding: sides(styles, strings, [
|
|
374
|
+
S['padding-top'],
|
|
375
|
+
S['padding-right'],
|
|
376
|
+
S['padding-bottom'],
|
|
377
|
+
S['padding-left'],
|
|
378
|
+
]),
|
|
379
|
+
gap: {
|
|
380
|
+
row: Number.parseFloat(style('row-gap')) || 0,
|
|
381
|
+
column: Number.parseFloat(style('column-gap')) || 0,
|
|
382
|
+
},
|
|
383
|
+
border: sides(styles, strings, [
|
|
384
|
+
S['border-top-width'],
|
|
385
|
+
S['border-right-width'],
|
|
386
|
+
S['border-bottom-width'],
|
|
387
|
+
S['border-left-width'],
|
|
388
|
+
]),
|
|
389
|
+
color: style('color'),
|
|
390
|
+
backgroundColor: style('background-color'),
|
|
391
|
+
backgroundImage: capBackgroundImage(style('background-image')),
|
|
392
|
+
textShadow: style('text-shadow').slice(0, COLOR_TOKEN_CAP),
|
|
393
|
+
textStrokeWidth: style('-webkit-text-stroke-width') || '0px',
|
|
394
|
+
textStrokeColor: style('-webkit-text-stroke-color'),
|
|
395
|
+
fontSize: style('font-size'),
|
|
396
|
+
fontWeight: Number.parseFloat(style('font-weight')) || 400,
|
|
397
|
+
textAlign: style('text-align'),
|
|
398
|
+
flexDirection: style('flex-direction'),
|
|
399
|
+
gridTemplateColumns: style('grid-template-columns'),
|
|
400
|
+
textOverflow: style('text-overflow') || 'clip',
|
|
401
|
+
whiteSpace: style('white-space') || 'normal',
|
|
402
|
+
overflowWrap: style('overflow-wrap') || 'normal',
|
|
403
|
+
text: (ownText.get(nodeIdx) ?? '').replace(/\s+/g, ' ').trim().slice(0, TEXT_PREVIEW_LENGTH),
|
|
404
|
+
parentId,
|
|
405
|
+
childIds: [],
|
|
406
|
+
visible: display !== 'none' &&
|
|
407
|
+
visibility !== 'hidden' &&
|
|
408
|
+
!(Number.isFinite(opacity) && opacity === 0) &&
|
|
409
|
+
box.width > 0 &&
|
|
410
|
+
box.height > 0,
|
|
411
|
+
ariaExpanded: attrs.get('aria-expanded') ?? null,
|
|
412
|
+
// `cdp` never calls `elementsFromPoint` -- it has `paintOrder` plus
|
|
413
|
+
// rects instead, which is the whole-rect answer to the same question
|
|
414
|
+
// `hitStackIds` answers on the `walk` path. See both fields' doc
|
|
415
|
+
// comments on `ElementRecord`.
|
|
416
|
+
hitStackIds: null,
|
|
417
|
+
paintOrder: layoutIdx === undefined ? null : (layout.paintOrders?.[layoutIdx] ?? null),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const byId = new Map(elements.map((el) => [el.id, el]));
|
|
421
|
+
for (const el of elements) {
|
|
422
|
+
if (el.parentId)
|
|
423
|
+
byId.get(el.parentId)?.childIds.push(el.id);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
elements,
|
|
427
|
+
truncated,
|
|
428
|
+
notMeasured: {
|
|
429
|
+
// Counted from `nodes.shadowRootType`, not assumed: `DOMSnapshot`
|
|
430
|
+
// flattens an author shadow root's content directly under its host
|
|
431
|
+
// (both open and closed -- verified in `test/browser/not-measured.test.ts`)
|
|
432
|
+
// and tags every flattened node, so `countShadowRoots` recovers the real
|
|
433
|
+
// per-host counts from those tags instead of asserting a permanent zero.
|
|
434
|
+
// A page whose shadow content this specific capture failed to flatten
|
|
435
|
+
// for any reason now reports that failure as a nonzero count rather
|
|
436
|
+
// than silence -- see `countShadowRoots`'s doc comment for the one
|
|
437
|
+
// known narrow gap (nested shadow roots) and the absent-field fallback.
|
|
438
|
+
shadowRoots: countShadowRoots(nodes, strings),
|
|
439
|
+
iframes: {
|
|
440
|
+
sameOrigin: iframesSameOrigin,
|
|
441
|
+
crossOrigin: iframesCrossOrigin,
|
|
442
|
+
unknownOrigin: iframesUnknownOrigin,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
documentBackground,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clipFactFor = clipFactFor;
|
|
4
|
+
exports.attachClipFacts = attachClipFacts;
|
|
5
|
+
/** `overflow` values that actually clip. Everything else lets content escape. */
|
|
6
|
+
const CLIPS = /^(hidden|scroll|auto|clip|overlay)$/;
|
|
7
|
+
/** …and of those, the ones a user can scroll to reveal what is hidden. */
|
|
8
|
+
const REACHABLE = /^(auto|scroll|overlay)$/;
|
|
9
|
+
/**
|
|
10
|
+
* The nearest ancestor that establishes this element's containing block.
|
|
11
|
+
*
|
|
12
|
+
* - `position: fixed` escapes every scroll container: its containing block is
|
|
13
|
+
* the viewport, so nothing in the DOM chain clips it.
|
|
14
|
+
* - `position: absolute` skips ancestors that are `position: static`.
|
|
15
|
+
* - everything else is clipped by its DOM ancestors, in order.
|
|
16
|
+
*/
|
|
17
|
+
function* clippingAncestors(el, byId) {
|
|
18
|
+
if (el.position === 'fixed')
|
|
19
|
+
return;
|
|
20
|
+
const absolute = el.position === 'absolute';
|
|
21
|
+
let cursor = el.parentId;
|
|
22
|
+
while (cursor) {
|
|
23
|
+
const ancestor = byId.get(cursor);
|
|
24
|
+
if (!ancestor)
|
|
25
|
+
return;
|
|
26
|
+
if (!absolute || ancestor.position !== 'static')
|
|
27
|
+
yield ancestor;
|
|
28
|
+
cursor = ancestor.parentId;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function overflowLabel(el) {
|
|
32
|
+
return el.overflowX === el.overflowY ? el.overflowX : `${el.overflowX}/${el.overflowY}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* How far `el` extends past `clipper` on each side. All four are >= 0; a zero
|
|
36
|
+
* on every side means the element is fully inside.
|
|
37
|
+
*/
|
|
38
|
+
function hiddenExtents(el, clipper) {
|
|
39
|
+
return {
|
|
40
|
+
top: Math.max(0, clipper.rect.y - el.rect.y),
|
|
41
|
+
left: Math.max(0, clipper.rect.x - el.rect.x),
|
|
42
|
+
bottom: Math.max(0, el.rect.y + el.rect.height - (clipper.rect.y + clipper.rect.height)),
|
|
43
|
+
right: Math.max(0, el.rect.x + el.rect.width - (clipper.rect.x + clipper.rect.width)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The clip fact for one element, or `undefined` when nothing hides any of it.
|
|
48
|
+
*
|
|
49
|
+
* The returned fact deliberately carries `overflow` and `scrollable` beside the
|
|
50
|
+
* hidden extents. A box outside an `overflow:auto` ancestor is a scroll region;
|
|
51
|
+
* the same box outside an `overflow:hidden` ancestor is content nobody can
|
|
52
|
+
* reach. Reporting the extent without the reachability would be a measurement
|
|
53
|
+
* the reader cannot act on.
|
|
54
|
+
*/
|
|
55
|
+
function clipFactFor(el, byId) {
|
|
56
|
+
if (!el.visible)
|
|
57
|
+
return undefined;
|
|
58
|
+
for (const ancestor of clippingAncestors(el, byId)) {
|
|
59
|
+
const clipsX = CLIPS.test(ancestor.overflowX);
|
|
60
|
+
const clipsY = CLIPS.test(ancestor.overflowY);
|
|
61
|
+
if (!clipsX && !clipsY)
|
|
62
|
+
continue;
|
|
63
|
+
const hidden = hiddenExtents(el, ancestor);
|
|
64
|
+
// An axis that does not clip cannot hide anything on that axis.
|
|
65
|
+
if (!clipsX) {
|
|
66
|
+
hidden.left = 0;
|
|
67
|
+
hidden.right = 0;
|
|
68
|
+
}
|
|
69
|
+
if (!clipsY) {
|
|
70
|
+
hidden.top = 0;
|
|
71
|
+
hidden.bottom = 0;
|
|
72
|
+
}
|
|
73
|
+
if (!hidden.top && !hidden.bottom && !hidden.left && !hidden.right)
|
|
74
|
+
return undefined;
|
|
75
|
+
const visibleWidth = Math.max(0, el.rect.width - hidden.left - hidden.right);
|
|
76
|
+
const visibleHeight = Math.max(0, el.rect.height - hidden.top - hidden.bottom);
|
|
77
|
+
return {
|
|
78
|
+
by: ancestor.id,
|
|
79
|
+
byTag: ancestor.tag,
|
|
80
|
+
overflow: overflowLabel(ancestor),
|
|
81
|
+
hidden,
|
|
82
|
+
visible: { width: visibleWidth, height: visibleHeight },
|
|
83
|
+
// Not the same question as `scrollHeight > clientHeight`: an
|
|
84
|
+
// `overflow:hidden` box has scroll extent and no reachability.
|
|
85
|
+
scrollable: (REACHABLE.test(ancestor.overflowY) && ancestor.scrollHeight - ancestor.clientHeight > 1) ||
|
|
86
|
+
(REACHABLE.test(ancestor.overflowX) && ancestor.scrollWidth - ancestor.clientWidth > 1),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
/** Attach `clip` to every element it applies to. Mutates and returns `elements`. */
|
|
92
|
+
function attachClipFacts(elements) {
|
|
93
|
+
const byId = new Map(elements.map((el) => [el.id, el]));
|
|
94
|
+
for (const el of elements) {
|
|
95
|
+
const clip = clipFactFor(el, byId);
|
|
96
|
+
if (clip)
|
|
97
|
+
el.clip = clip;
|
|
98
|
+
}
|
|
99
|
+
return elements;
|
|
100
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ancestor clip-rect intersection.
|
|
3
|
+
*
|
|
4
|
+
* This runs on the Node side over already-collected records, not inside the
|
|
5
|
+
* page, for two reasons: both capture paths (the in-page walk and the CDP fast
|
|
6
|
+
* path) need it and neither should own it, and as a pure function over an array
|
|
7
|
+
* it is unit-testable without a browser.
|
|
8
|
+
*
|
|
9
|
+
* CDP does not solve this and makes it harder, not easier: `parentIndex` is the
|
|
10
|
+
* DOM parent chain, and for `position: fixed | absolute` the real clipping
|
|
11
|
+
* ancestor is on the *containing block* chain, which has to be reconstructed
|
|
12
|
+
* from the ancestors' `position` values.
|
|
13
|
+
*/
|
|
14
|
+
import type { ClipFact, ElementRecord } from "./types.cjs";
|
|
15
|
+
/**
|
|
16
|
+
* The clip fact for one element, or `undefined` when nothing hides any of it.
|
|
17
|
+
*
|
|
18
|
+
* The returned fact deliberately carries `overflow` and `scrollable` beside the
|
|
19
|
+
* hidden extents. A box outside an `overflow:auto` ancestor is a scroll region;
|
|
20
|
+
* the same box outside an `overflow:hidden` ancestor is content nobody can
|
|
21
|
+
* reach. Reporting the extent without the reachability would be a measurement
|
|
22
|
+
* the reader cannot act on.
|
|
23
|
+
*/
|
|
24
|
+
export declare function clipFactFor(el: ElementRecord, byId: Map<string, ElementRecord>): ClipFact | undefined;
|
|
25
|
+
/** Attach `clip` to every element it applies to. Mutates and returns `elements`. */
|
|
26
|
+
export declare function attachClipFacts(elements: ElementRecord[]): ElementRecord[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ancestor clip-rect intersection.
|
|
3
|
+
*
|
|
4
|
+
* This runs on the Node side over already-collected records, not inside the
|
|
5
|
+
* page, for two reasons: both capture paths (the in-page walk and the CDP fast
|
|
6
|
+
* path) need it and neither should own it, and as a pure function over an array
|
|
7
|
+
* it is unit-testable without a browser.
|
|
8
|
+
*
|
|
9
|
+
* CDP does not solve this and makes it harder, not easier: `parentIndex` is the
|
|
10
|
+
* DOM parent chain, and for `position: fixed | absolute` the real clipping
|
|
11
|
+
* ancestor is on the *containing block* chain, which has to be reconstructed
|
|
12
|
+
* from the ancestors' `position` values.
|
|
13
|
+
*/
|
|
14
|
+
import type { ClipFact, ElementRecord } from "./types.js";
|
|
15
|
+
/**
|
|
16
|
+
* The clip fact for one element, or `undefined` when nothing hides any of it.
|
|
17
|
+
*
|
|
18
|
+
* The returned fact deliberately carries `overflow` and `scrollable` beside the
|
|
19
|
+
* hidden extents. A box outside an `overflow:auto` ancestor is a scroll region;
|
|
20
|
+
* the same box outside an `overflow:hidden` ancestor is content nobody can
|
|
21
|
+
* reach. Reporting the extent without the reachability would be a measurement
|
|
22
|
+
* the reader cannot act on.
|
|
23
|
+
*/
|
|
24
|
+
export declare function clipFactFor(el: ElementRecord, byId: Map<string, ElementRecord>): ClipFact | undefined;
|
|
25
|
+
/** Attach `clip` to every element it applies to. Mutates and returns `elements`. */
|
|
26
|
+
export declare function attachClipFacts(elements: ElementRecord[]): ElementRecord[];
|