@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,355 @@
|
|
|
1
|
+
export function collectSnapshot() {
|
|
2
|
+
// Inlined, not imported: `page.evaluate` serializes only this function's own
|
|
3
|
+
// source, so nothing from module scope survives the trip into the page.
|
|
4
|
+
// Kept in sync with `limits.ts` by `test/unit/limits.test.ts`.
|
|
5
|
+
const MAX_ELEMENTS = 5000;
|
|
6
|
+
const SKIPPED_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG']);
|
|
7
|
+
const TEXT_PREVIEW_LENGTH = 60;
|
|
8
|
+
const NAME_LENGTH = 80;
|
|
9
|
+
const TESTID_ATTRS = ['data-testid', 'data-test-id', 'data-test', 'data-qa'];
|
|
10
|
+
// Kept in sync with `limits.ts#COLOR_TOKEN_CAP` by `test/unit/limits.test.ts` --
|
|
11
|
+
// duplicated, not imported, for the same reason `MAX_ELEMENTS` is: this
|
|
12
|
+
// function's source is serialized whole into the page, and a runtime import
|
|
13
|
+
// from another module would not survive that trip.
|
|
14
|
+
const COLOR_TOKEN_CAP = 120;
|
|
15
|
+
// Kept in sync with `limits.ts#capBackgroundImage` -- same reason, same
|
|
16
|
+
// discipline. Preserves both ends of a long gradient string rather than a
|
|
17
|
+
// plain prefix cut, so the *last* color stop survives capture; see that
|
|
18
|
+
// function's doc comment (A4).
|
|
19
|
+
function capBackgroundImage(raw) {
|
|
20
|
+
if (raw.length <= COLOR_TOKEN_CAP)
|
|
21
|
+
return raw;
|
|
22
|
+
const ELLIPSIS = '…';
|
|
23
|
+
const half = Math.max(0, Math.floor((COLOR_TOKEN_CAP - ELLIPSIS.length) / 2));
|
|
24
|
+
return `${raw.slice(0, half)}${ELLIPSIS}${raw.slice(raw.length - half)}`;
|
|
25
|
+
}
|
|
26
|
+
// ---- computed role ------------------------------------------------------
|
|
27
|
+
// The prototype captured `getAttribute('role')`, which is null on nearly
|
|
28
|
+
// every real element, so the identity ladder's strongest rung was dead on
|
|
29
|
+
// arrival. This is the implicit-role mapping that fixes it.
|
|
30
|
+
// biome-ignore format: a lookup table reads as a table; one tag per line is 37
|
|
31
|
+
// lines of noise for the same information.
|
|
32
|
+
const IMPLICIT_ROLE = {
|
|
33
|
+
ARTICLE: 'article', ASIDE: 'complementary', BUTTON: 'button', DIALOG: 'dialog',
|
|
34
|
+
FIELDSET: 'group', FIGURE: 'figure', FOOTER: 'contentinfo', FORM: 'form',
|
|
35
|
+
H1: 'heading', H2: 'heading', H3: 'heading', H4: 'heading', H5: 'heading',
|
|
36
|
+
H6: 'heading', HEADER: 'banner', HR: 'separator', IMG: 'img', LI: 'listitem',
|
|
37
|
+
MAIN: 'main', NAV: 'navigation', OL: 'list', OPTION: 'option', OUTPUT: 'status',
|
|
38
|
+
P: 'paragraph', PROGRESS: 'progressbar', SECTION: 'region', SELECT: 'combobox',
|
|
39
|
+
SUMMARY: 'button', TABLE: 'table', TBODY: 'rowgroup', TD: 'cell',
|
|
40
|
+
TEXTAREA: 'textbox', TFOOT: 'rowgroup', TH: 'columnheader', THEAD: 'rowgroup',
|
|
41
|
+
TR: 'row', UL: 'list',
|
|
42
|
+
};
|
|
43
|
+
// biome-ignore format: see above.
|
|
44
|
+
const INPUT_ROLE = {
|
|
45
|
+
button: 'button', checkbox: 'checkbox', email: 'textbox', image: 'button',
|
|
46
|
+
number: 'spinbutton', radio: 'radio', range: 'slider', reset: 'button',
|
|
47
|
+
search: 'searchbox', submit: 'button', tel: 'textbox', text: 'textbox',
|
|
48
|
+
url: 'textbox',
|
|
49
|
+
};
|
|
50
|
+
/** Roles whose accessible name comes from their own content. */
|
|
51
|
+
// biome-ignore format: see above.
|
|
52
|
+
const NAME_FROM_CONTENT = new Set([
|
|
53
|
+
'button', 'cell', 'checkbox', 'columnheader', 'gridcell', 'heading', 'link',
|
|
54
|
+
'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'row',
|
|
55
|
+
'rowheader', 'switch', 'tab', 'tooltip', 'treeitem',
|
|
56
|
+
]);
|
|
57
|
+
function computedRole(el) {
|
|
58
|
+
const explicit = (el.getAttribute('role') ?? '').trim().split(/\s+/)[0];
|
|
59
|
+
if (explicit)
|
|
60
|
+
return explicit;
|
|
61
|
+
const tag = el.tagName.toUpperCase();
|
|
62
|
+
if (tag === 'A' || tag === 'AREA')
|
|
63
|
+
return el.hasAttribute('href') ? 'link' : null;
|
|
64
|
+
if (tag === 'INPUT') {
|
|
65
|
+
const type = (el.getAttribute('type') ?? 'text').toLowerCase();
|
|
66
|
+
return INPUT_ROLE[type] ?? 'textbox';
|
|
67
|
+
}
|
|
68
|
+
// `header` and `footer` are landmarks only at the top level.
|
|
69
|
+
if ((tag === 'HEADER' || tag === 'FOOTER') && el.closest('article,aside,main,nav,section')) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// `section` is a region only when it has an accessible name.
|
|
73
|
+
if (tag === 'SECTION' &&
|
|
74
|
+
!el.getAttribute('aria-label') &&
|
|
75
|
+
!el.getAttribute('aria-labelledby')) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return IMPLICIT_ROLE[tag] ?? null;
|
|
79
|
+
}
|
|
80
|
+
function trimName(value) {
|
|
81
|
+
const text = (value ?? '').replace(/\s+/g, ' ').trim();
|
|
82
|
+
return text ? text.slice(0, NAME_LENGTH) : null;
|
|
83
|
+
}
|
|
84
|
+
function accessibleName(el, role) {
|
|
85
|
+
const label = trimName(el.getAttribute('aria-label'));
|
|
86
|
+
if (label)
|
|
87
|
+
return label;
|
|
88
|
+
const labelledby = el.getAttribute('aria-labelledby');
|
|
89
|
+
if (labelledby) {
|
|
90
|
+
const parts = [];
|
|
91
|
+
for (const id of labelledby.split(/\s+/)) {
|
|
92
|
+
parts.push(document.getElementById(id)?.textContent ?? '');
|
|
93
|
+
}
|
|
94
|
+
const joined = trimName(parts.join(' '));
|
|
95
|
+
if (joined)
|
|
96
|
+
return joined;
|
|
97
|
+
}
|
|
98
|
+
const tag = el.tagName.toUpperCase();
|
|
99
|
+
if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') {
|
|
100
|
+
if (el.id) {
|
|
101
|
+
const forLabel = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
|
|
102
|
+
const text = trimName(forLabel?.textContent);
|
|
103
|
+
if (text)
|
|
104
|
+
return text;
|
|
105
|
+
}
|
|
106
|
+
const wrapping = trimName(el.closest('label')?.textContent);
|
|
107
|
+
if (wrapping)
|
|
108
|
+
return wrapping;
|
|
109
|
+
const placeholder = trimName(el.getAttribute('placeholder'));
|
|
110
|
+
if (placeholder)
|
|
111
|
+
return placeholder;
|
|
112
|
+
}
|
|
113
|
+
if (tag === 'IMG')
|
|
114
|
+
return trimName(el.getAttribute('alt'));
|
|
115
|
+
if (role && NAME_FROM_CONTENT.has(role)) {
|
|
116
|
+
const text = trimName(el.textContent);
|
|
117
|
+
if (text)
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
return trimName(el.getAttribute('title'));
|
|
121
|
+
}
|
|
122
|
+
// ---- geometry / style ---------------------------------------------------
|
|
123
|
+
function childrenOf(el) {
|
|
124
|
+
const result = [];
|
|
125
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
126
|
+
const child = el.children[i];
|
|
127
|
+
if (child)
|
|
128
|
+
result.push(child);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
function boxSides(cs, prefix) {
|
|
133
|
+
const suffix = prefix === 'border' ? 'Width' : '';
|
|
134
|
+
return {
|
|
135
|
+
top: Number.parseFloat(cs.getPropertyValue(`${prefix}-top${suffix}`)) || 0,
|
|
136
|
+
right: Number.parseFloat(cs.getPropertyValue(`${prefix}-right${suffix}`)) || 0,
|
|
137
|
+
bottom: Number.parseFloat(cs.getPropertyValue(`${prefix}-bottom${suffix}`)) || 0,
|
|
138
|
+
left: Number.parseFloat(cs.getPropertyValue(`${prefix}-left${suffix}`)) || 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function headingLevel(el) {
|
|
142
|
+
const ariaLevel = el.getAttribute('aria-level');
|
|
143
|
+
if (ariaLevel && Number.isFinite(Number(ariaLevel)))
|
|
144
|
+
return Number(ariaLevel);
|
|
145
|
+
const match = /^H([1-6])$/.exec(el.tagName);
|
|
146
|
+
return match?.[1] ? Number(match[1]) : null;
|
|
147
|
+
}
|
|
148
|
+
function ownText(el) {
|
|
149
|
+
let text = '';
|
|
150
|
+
for (const node of el.childNodes) {
|
|
151
|
+
if (node.nodeType === 3 /* TEXT_NODE */)
|
|
152
|
+
text += node.textContent ?? '';
|
|
153
|
+
}
|
|
154
|
+
return text.replace(/\s+/g, ' ').trim().slice(0, TEXT_PREVIEW_LENGTH);
|
|
155
|
+
}
|
|
156
|
+
const elements = [];
|
|
157
|
+
const byId = new Map();
|
|
158
|
+
// The DOM element behind each captured record, kept only long enough for
|
|
159
|
+
// the second, post-walk pass below to hit-test it -- `visit` is a pre-order
|
|
160
|
+
// walk, so an *unrelated* overlay elsewhere in the document (a modal
|
|
161
|
+
// appended near the end of `<body>`, positioned over content captured
|
|
162
|
+
// earlier) would not have an id yet if this ran inline during the walk.
|
|
163
|
+
const domById = new Map();
|
|
164
|
+
let nextId = 0;
|
|
165
|
+
let truncated = false;
|
|
166
|
+
// `NotMeasured` counters. See the doc comment on the type: this walk never
|
|
167
|
+
// enters a shadow root or an iframe document, so these are the only record
|
|
168
|
+
// of what it walked past.
|
|
169
|
+
let shadowRootsOpen = 0;
|
|
170
|
+
let iframesSameOrigin = 0;
|
|
171
|
+
let iframesUnknownOrigin = 0;
|
|
172
|
+
function visit(el, parentId) {
|
|
173
|
+
if (elements.length >= MAX_ELEMENTS) {
|
|
174
|
+
truncated = true;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// SVG-namespace elements report a lowercase `tagName`, unlike HTML
|
|
178
|
+
// elements which are always uppercase — so this comparison must normalize
|
|
179
|
+
// case or the skip never fires for SVG internals.
|
|
180
|
+
const tagName = el.tagName.toUpperCase();
|
|
181
|
+
if (SKIPPED_TAGS.has(tagName))
|
|
182
|
+
return;
|
|
183
|
+
// `NotMeasured`: this walk reads `el.children`, which is light-DOM only --
|
|
184
|
+
// it never descends into a shadow root or an iframe document, so this is
|
|
185
|
+
// the only place either is ever seen at all.
|
|
186
|
+
//
|
|
187
|
+
// An *open* shadow root is detectable after the fact: `el.shadowRoot` is
|
|
188
|
+
// the live reference `attachShadow({mode:'open'})` returned. A *closed*
|
|
189
|
+
// one returns `null` from that same property -- indistinguishable from
|
|
190
|
+
// "no shadow root here" -- so closed roots stay uncounted; see the
|
|
191
|
+
// `NotMeasured` doc comment for why the header reports that as 'unknown'
|
|
192
|
+
// rather than guessing zero.
|
|
193
|
+
if (el.shadowRoot)
|
|
194
|
+
shadowRootsOpen++;
|
|
195
|
+
if (tagName === 'IFRAME') {
|
|
196
|
+
// `contentDocument` is `null` and does not throw whenever this document
|
|
197
|
+
// cannot read the frame's document -- but that is not only true
|
|
198
|
+
// cross-origin. It is also `null` for a same-origin frame on `file://`
|
|
199
|
+
// (no origin to compare against), a `sandbox`-ed frame missing
|
|
200
|
+
// `allow-same-origin` (an opaque origin, even when the `src` is the same
|
|
201
|
+
// server), and a frame that has not navigated yet. All four look
|
|
202
|
+
// identical from here, so a `null` reading can only ever mean "this walk
|
|
203
|
+
// cannot resolve the origin", never a positive cross-origin
|
|
204
|
+
// classification -- that is what `unknownOrigin` is for. A non-null
|
|
205
|
+
// `contentDocument` *is* a reliable same-origin signal, so that half
|
|
206
|
+
// stays a real count.
|
|
207
|
+
if (el.contentDocument !== null)
|
|
208
|
+
iframesSameOrigin++;
|
|
209
|
+
else
|
|
210
|
+
iframesUnknownOrigin++;
|
|
211
|
+
}
|
|
212
|
+
const cs = getComputedStyle(el);
|
|
213
|
+
const rect = el.getBoundingClientRect();
|
|
214
|
+
const opacity = Number.parseFloat(cs.getPropertyValue('opacity'));
|
|
215
|
+
const display = cs.getPropertyValue('display');
|
|
216
|
+
const visibility = cs.getPropertyValue('visibility');
|
|
217
|
+
const visible = display !== 'none' &&
|
|
218
|
+
visibility !== 'hidden' &&
|
|
219
|
+
!(Number.isFinite(opacity) && opacity === 0) &&
|
|
220
|
+
rect.width > 0 &&
|
|
221
|
+
rect.height > 0;
|
|
222
|
+
const id = `e${nextId}`;
|
|
223
|
+
nextId++;
|
|
224
|
+
let testId = null;
|
|
225
|
+
for (const attr of TESTID_ATTRS) {
|
|
226
|
+
const value = el.getAttribute(attr);
|
|
227
|
+
if (value) {
|
|
228
|
+
testId = value;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const role = computedRole(el);
|
|
233
|
+
const record = {
|
|
234
|
+
id,
|
|
235
|
+
tag: el.tagName.toLowerCase(),
|
|
236
|
+
testId,
|
|
237
|
+
domId: el.id || null,
|
|
238
|
+
role,
|
|
239
|
+
name: accessibleName(el, role),
|
|
240
|
+
className: el.getAttribute('class') ?? '',
|
|
241
|
+
headingLevel: headingLevel(el),
|
|
242
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
243
|
+
scrollWidth: el.scrollWidth,
|
|
244
|
+
scrollHeight: el.scrollHeight,
|
|
245
|
+
clientWidth: el.clientWidth,
|
|
246
|
+
clientHeight: el.clientHeight,
|
|
247
|
+
overflowX: cs.getPropertyValue('overflow-x'),
|
|
248
|
+
overflowY: cs.getPropertyValue('overflow-y'),
|
|
249
|
+
position: cs.getPropertyValue('position'),
|
|
250
|
+
display,
|
|
251
|
+
visibility,
|
|
252
|
+
opacity: Number.isFinite(opacity) ? opacity : 1,
|
|
253
|
+
margin: boxSides(cs, 'margin'),
|
|
254
|
+
padding: boxSides(cs, 'padding'),
|
|
255
|
+
gap: {
|
|
256
|
+
row: Number.parseFloat(cs.getPropertyValue('row-gap')) || 0,
|
|
257
|
+
column: Number.parseFloat(cs.getPropertyValue('column-gap')) || 0,
|
|
258
|
+
},
|
|
259
|
+
border: boxSides(cs, 'border'),
|
|
260
|
+
color: cs.getPropertyValue('color'),
|
|
261
|
+
backgroundColor: cs.getPropertyValue('background-color'),
|
|
262
|
+
backgroundImage: capBackgroundImage(cs.getPropertyValue('background-image')),
|
|
263
|
+
textShadow: cs.getPropertyValue('text-shadow').slice(0, COLOR_TOKEN_CAP),
|
|
264
|
+
textStrokeWidth: cs.getPropertyValue('-webkit-text-stroke-width'),
|
|
265
|
+
textStrokeColor: cs.getPropertyValue('-webkit-text-stroke-color'),
|
|
266
|
+
fontSize: cs.getPropertyValue('font-size'),
|
|
267
|
+
fontWeight: Number.parseFloat(cs.getPropertyValue('font-weight')) || 400,
|
|
268
|
+
textAlign: cs.getPropertyValue('text-align'),
|
|
269
|
+
flexDirection: cs.getPropertyValue('flex-direction'),
|
|
270
|
+
gridTemplateColumns: cs.getPropertyValue('grid-template-columns'),
|
|
271
|
+
textOverflow: cs.getPropertyValue('text-overflow'),
|
|
272
|
+
whiteSpace: cs.getPropertyValue('white-space'),
|
|
273
|
+
overflowWrap: cs.getPropertyValue('overflow-wrap'),
|
|
274
|
+
text: ownText(el),
|
|
275
|
+
parentId,
|
|
276
|
+
childIds: [],
|
|
277
|
+
visible,
|
|
278
|
+
ariaExpanded: el.getAttribute('aria-expanded'),
|
|
279
|
+
// Filled in by the second pass below, once every id in the document
|
|
280
|
+
// exists to resolve a hit-test against -- see `domById`'s doc comment.
|
|
281
|
+
hitStackIds: null,
|
|
282
|
+
paintOrder: null,
|
|
283
|
+
};
|
|
284
|
+
elements.push(record);
|
|
285
|
+
byId.set(id, record);
|
|
286
|
+
domById.set(id, el);
|
|
287
|
+
if (parentId)
|
|
288
|
+
byId.get(parentId)?.childIds.push(id);
|
|
289
|
+
for (const child of childrenOf(el)) {
|
|
290
|
+
if (elements.length >= MAX_ELEMENTS) {
|
|
291
|
+
truncated = true;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
visit(child, id);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
visit(document.body, null);
|
|
298
|
+
// The overlap signal `snapshot/color.ts` needs to tell "a real ancestor
|
|
299
|
+
// backdrop" from "some unrelated element painted over or behind this
|
|
300
|
+
// text" -- see `ElementRecord.hitStackIds`'s doc comment for why this is a
|
|
301
|
+
// second pass rather than done inline above. Gated on the same "visible,
|
|
302
|
+
// has its own text" condition the color fact itself uses (§4 of
|
|
303
|
+
// `docs/notes/03-color.md`): no text, no ink-on-backdrop question to probe,
|
|
304
|
+
// so no reason to pay for a hit test.
|
|
305
|
+
const reverseId = new Map();
|
|
306
|
+
for (const [id, domEl] of domById)
|
|
307
|
+
reverseId.set(domEl, id);
|
|
308
|
+
for (const [id, domEl] of domById) {
|
|
309
|
+
const record = byId.get(id);
|
|
310
|
+
if (!record?.visible || !record.text)
|
|
311
|
+
continue;
|
|
312
|
+
const rect = domEl.getBoundingClientRect();
|
|
313
|
+
const cx = rect.x + rect.width / 2;
|
|
314
|
+
const cy = rect.y + rect.height / 2;
|
|
315
|
+
const stack = document.elementsFromPoint(cx, cy);
|
|
316
|
+
const ids = [];
|
|
317
|
+
for (const hit of stack) {
|
|
318
|
+
const hitId = reverseId.get(hit);
|
|
319
|
+
// An entry this capture never assigned an id to (an `SVG` internal, a
|
|
320
|
+
// node past the element cap) carries no signal `color.ts` can act on --
|
|
321
|
+
// dropped rather than guessed at, the same discipline `NotMeasured`
|
|
322
|
+
// uses for what a capture walked past without measuring.
|
|
323
|
+
if (hitId)
|
|
324
|
+
ids.push(hitId);
|
|
325
|
+
}
|
|
326
|
+
record.hitStackIds = ids;
|
|
327
|
+
}
|
|
328
|
+
// `<html>` (`documentElement`) is never part of `elements` -- this walk
|
|
329
|
+
// starts at `<body>` (see this function's own `visit(document.body, null)`
|
|
330
|
+
// call below) -- so without reading it here, a compositing walk that runs
|
|
331
|
+
// off the top of the ancestor chain has no way to know the browser's real
|
|
332
|
+
// default canvas fill and no honest option but to guess (A2). See
|
|
333
|
+
// `DocumentBackground`'s doc comment.
|
|
334
|
+
const htmlStyle = getComputedStyle(document.documentElement);
|
|
335
|
+
return {
|
|
336
|
+
elements,
|
|
337
|
+
truncated,
|
|
338
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
339
|
+
notMeasured: {
|
|
340
|
+
shadowRoots: { open: shadowRootsOpen, closed: 'unknown' },
|
|
341
|
+
iframes: {
|
|
342
|
+
sameOrigin: iframesSameOrigin,
|
|
343
|
+
// This walk has no positive signal for cross-origin -- see the check
|
|
344
|
+
// above -- so it never asserts one. An unreachable frame is always
|
|
345
|
+
// counted as `unknownOrigin`, never guessed into `crossOrigin`.
|
|
346
|
+
crossOrigin: 0,
|
|
347
|
+
unknownOrigin: iframesUnknownOrigin,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
documentBackground: {
|
|
351
|
+
backgroundColor: htmlStyle.getPropertyValue('background-color'),
|
|
352
|
+
colorScheme: htmlStyle.getPropertyValue('color-scheme'),
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|