@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,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OVERFLOW_EPSILON = void 0;
|
|
4
|
+
exports.factsFor = factsFor;
|
|
5
|
+
exports.renderFacts = renderFacts;
|
|
6
|
+
exports.factsForAll = factsForAll;
|
|
7
|
+
/**
|
|
8
|
+
* Subpixel layout noise. Below this, an "overflow" is a rounding artefact.
|
|
9
|
+
* Exported so `render.ts`'s fold variance uses the same threshold rather than
|
|
10
|
+
* its own magic number -- a fold reporting a 1px spread that is really 0.2px
|
|
11
|
+
* of rounding is the same bug this constant exists to prevent here.
|
|
12
|
+
*/
|
|
13
|
+
exports.OVERFLOW_EPSILON = 1;
|
|
14
|
+
/** A box narrower or shorter than this has collapsed, whatever it intended. */
|
|
15
|
+
const ZERO_EPSILON = 1;
|
|
16
|
+
const CLIPS = /^(hidden|clip)$/;
|
|
17
|
+
const SCROLLS = /^(auto|scroll|overlay)$/;
|
|
18
|
+
/** White-space values under which text will not wrap. */
|
|
19
|
+
const NO_WRAP = /^(nowrap|pre)$/;
|
|
20
|
+
const round = (n) => Number.isInteger(n) ? String(n) : n.toFixed(1).replace(/\.0$/, '');
|
|
21
|
+
/**
|
|
22
|
+
* What happens to content that exceeds the box on this axis.
|
|
23
|
+
*
|
|
24
|
+
* Exactly one of `clip` / `scroll` / `visible` always applies -- it is a
|
|
25
|
+
* partition of the `overflow` values. The wrapping facts are additional, and
|
|
26
|
+
* `ellipsis` suppresses `wraps` because `text-overflow` only takes effect when
|
|
27
|
+
* wrapping is already off; printing both would be restating one CSS rule twice.
|
|
28
|
+
*/
|
|
29
|
+
function consequences(el, axis) {
|
|
30
|
+
const overflow = axis === 'x' ? el.overflowX : el.overflowY;
|
|
31
|
+
const out = [];
|
|
32
|
+
const clips = CLIPS.test(overflow);
|
|
33
|
+
if (clips)
|
|
34
|
+
out.push('clip');
|
|
35
|
+
else if (SCROLLS.test(overflow))
|
|
36
|
+
out.push('scroll');
|
|
37
|
+
else
|
|
38
|
+
out.push('visible');
|
|
39
|
+
if (axis === 'x') {
|
|
40
|
+
const ellipsis = el.textOverflow === 'ellipsis' && (clips || SCROLLS.test(overflow));
|
|
41
|
+
if (ellipsis)
|
|
42
|
+
out.push('ellipsis');
|
|
43
|
+
else if (!NO_WRAP.test(el.whiteSpace))
|
|
44
|
+
out.push('wraps');
|
|
45
|
+
else
|
|
46
|
+
out.push('nowrap');
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether a collapsed box is hiding anything.
|
|
52
|
+
*
|
|
53
|
+
* A `0x0` spacer is ordinary and uninteresting; a `0x1239` column with a page
|
|
54
|
+
* of content inside it is the bug the prototype measured and discarded. The
|
|
55
|
+
* difference is entirely in what the box contains, so that is what the fact
|
|
56
|
+
* carries.
|
|
57
|
+
*/
|
|
58
|
+
function contentOf(el, byId) {
|
|
59
|
+
if (el.text)
|
|
60
|
+
return `"${el.text}"`;
|
|
61
|
+
let text = '';
|
|
62
|
+
let elements = 0;
|
|
63
|
+
const queue = [...el.childIds];
|
|
64
|
+
while (queue.length > 0 && text.length < 40) {
|
|
65
|
+
const child = byId.get(queue.shift() ?? '');
|
|
66
|
+
if (!child)
|
|
67
|
+
continue;
|
|
68
|
+
elements++;
|
|
69
|
+
if (child.text)
|
|
70
|
+
text = `${text} ${child.text}`.trim();
|
|
71
|
+
queue.push(...child.childIds);
|
|
72
|
+
}
|
|
73
|
+
if (text)
|
|
74
|
+
return `"${text.slice(0, 40)}"`;
|
|
75
|
+
return elements > 0 ? `${elements} element${elements === 1 ? '' : 's'}` : null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* How much of an element an ancestor hides, and whether anyone can get to it.
|
|
79
|
+
*
|
|
80
|
+
* `attachClipFacts` has populated `ElementRecord.clip` on both capture paths
|
|
81
|
+
* since step 4, and until now nothing rendered it — the digest emitted the
|
|
82
|
+
* three fact kinds above and silently dropped this one. The V1 cold read's
|
|
83
|
+
* single strongest result came from the step-2 spike's renderer, not from
|
|
84
|
+
* shipped code, and nobody noticed because the tests asserted on `clip` the
|
|
85
|
+
* field rather than on the output.
|
|
86
|
+
*
|
|
87
|
+
* `scrollable` is the counter-fact that carries the whole meaning. A box
|
|
88
|
+
* outside an `overflow:auto` ancestor is a scroll region working as intended;
|
|
89
|
+
* the same box outside an `overflow:hidden` ancestor is content nobody can
|
|
90
|
+
* reach. Printing the extent without the reachability is a measurement the
|
|
91
|
+
* reader cannot act on.
|
|
92
|
+
*/
|
|
93
|
+
function clipFact(el) {
|
|
94
|
+
const clip = el.clip;
|
|
95
|
+
if (!clip)
|
|
96
|
+
return null;
|
|
97
|
+
const where = [
|
|
98
|
+
clip.hidden.top ? `${round(clip.hidden.top)}px above` : null,
|
|
99
|
+
clip.hidden.bottom ? `${round(clip.hidden.bottom)}px below` : null,
|
|
100
|
+
clip.hidden.left ? `${round(clip.hidden.left)}px left` : null,
|
|
101
|
+
clip.hidden.right ? `${round(clip.hidden.right)}px right` : null,
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(', ');
|
|
105
|
+
const visible = clip.visible.width < ZERO_EPSILON || clip.visible.height < ZERO_EPSILON
|
|
106
|
+
? 'nothing visible'
|
|
107
|
+
: `${round(clip.visible.width)}x${round(clip.visible.height)} visible`;
|
|
108
|
+
return {
|
|
109
|
+
kind: 'clipped',
|
|
110
|
+
text: `clipped by ${clip.by} ${clip.byTag}; overflow:${clip.overflow}, ` +
|
|
111
|
+
`${clip.scrollable ? 'scrollable' : 'not scrollable'}; ${where}, ${visible}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Every fact this element has. Unconditional: no thresholds beyond subpixel
|
|
116
|
+
* noise, no severity, no suppression.
|
|
117
|
+
*/
|
|
118
|
+
function factsFor(el, byId) {
|
|
119
|
+
const facts = [];
|
|
120
|
+
const overflowX = el.scrollWidth - el.clientWidth;
|
|
121
|
+
if (overflowX > exports.OVERFLOW_EPSILON) {
|
|
122
|
+
facts.push({
|
|
123
|
+
kind: 'overflow-x',
|
|
124
|
+
text: `overflow-x +${round(overflowX)}px; ${consequences(el, 'x').join(', ')}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const overflowY = el.scrollHeight - el.clientHeight;
|
|
128
|
+
if (overflowY > exports.OVERFLOW_EPSILON) {
|
|
129
|
+
facts.push({
|
|
130
|
+
kind: 'overflow-y',
|
|
131
|
+
text: `overflow-y +${round(overflowY)}px; ${consequences(el, 'y').join(', ')}`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// Only for elements that are still in the layout. `display:none` collapses
|
|
135
|
+
// everything to 0x0 and saying so about every hidden node is noise.
|
|
136
|
+
const collapsed = el.display !== 'none' &&
|
|
137
|
+
(el.rect.width < ZERO_EPSILON || el.rect.height < ZERO_EPSILON) &&
|
|
138
|
+
!(el.rect.width < ZERO_EPSILON && el.rect.height < ZERO_EPSILON && !el.childIds.length);
|
|
139
|
+
if (collapsed) {
|
|
140
|
+
const content = contentOf(el, byId);
|
|
141
|
+
if (content) {
|
|
142
|
+
facts.push({
|
|
143
|
+
kind: 'zero-box',
|
|
144
|
+
text: `${round(el.rect.width)}x${round(el.rect.height)} zero-box; content ${content}`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const clipped = clipFact(el);
|
|
149
|
+
if (clipped)
|
|
150
|
+
facts.push(clipped);
|
|
151
|
+
return facts;
|
|
152
|
+
}
|
|
153
|
+
/** `[fact] [fact]` — the form these take on a digest line. */
|
|
154
|
+
function renderFacts(facts) {
|
|
155
|
+
return facts.map((f) => `[${f.text}]`).join(' ');
|
|
156
|
+
}
|
|
157
|
+
/** Every element that has at least one fact, in document order. */
|
|
158
|
+
function factsForAll(elements) {
|
|
159
|
+
const byId = new Map(elements.map((e) => [e.id, e]));
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const element of elements) {
|
|
162
|
+
const facts = factsFor(element, byId);
|
|
163
|
+
if (facts.length > 0)
|
|
164
|
+
out.push({ element, facts });
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facts, with the facts that disambiguate them.
|
|
3
|
+
*
|
|
4
|
+
* This is the structural fix for how the prototype died. It shipped checks: a
|
|
5
|
+
* rule looked at `scrollWidth - clientWidth`, compared it to a threshold, and
|
|
6
|
+
* emitted a finding. On PR #4813 it measured `scrollWidth: 815, clientWidth:
|
|
7
|
+
* 668` -- a real 147px of truncated heading -- and threw the measurement away
|
|
8
|
+
* because the rule that owned it did not fire. Three separate false-positive
|
|
9
|
+
* classes in its final week all had the same shape: the check lacked a fact the
|
|
10
|
+
* DOM already had.
|
|
11
|
+
*
|
|
12
|
+
* So nothing here decides. A fact is emitted whenever the measurement exists,
|
|
13
|
+
* unconditionally, carrying the adjacent CSS that determines what the
|
|
14
|
+
* measurement *means*:
|
|
15
|
+
*
|
|
16
|
+
* [overflow-x +147px; clip, ellipsis] text is being cut off
|
|
17
|
+
* [overflow-x +227px; clip, wraps] same clip, text just got taller
|
|
18
|
+
* [overflow-x +90px; scroll] a scroll region, working as intended
|
|
19
|
+
*
|
|
20
|
+
* Same number, three different situations, and the difference is never in the
|
|
21
|
+
* number. The reader draws the conclusion; this file refuses to.
|
|
22
|
+
*/
|
|
23
|
+
import type { ElementRecord } from "./types.cjs";
|
|
24
|
+
export type FactKind = 'overflow-x' | 'overflow-y' | 'zero-box' | 'clipped';
|
|
25
|
+
export interface Fact {
|
|
26
|
+
kind: FactKind;
|
|
27
|
+
/** The rendered body, without the surrounding brackets. */
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Subpixel layout noise. Below this, an "overflow" is a rounding artefact.
|
|
32
|
+
* Exported so `render.ts`'s fold variance uses the same threshold rather than
|
|
33
|
+
* its own magic number -- a fold reporting a 1px spread that is really 0.2px
|
|
34
|
+
* of rounding is the same bug this constant exists to prevent here.
|
|
35
|
+
*/
|
|
36
|
+
export declare const OVERFLOW_EPSILON = 1;
|
|
37
|
+
/**
|
|
38
|
+
* Every fact this element has. Unconditional: no thresholds beyond subpixel
|
|
39
|
+
* noise, no severity, no suppression.
|
|
40
|
+
*/
|
|
41
|
+
export declare function factsFor(el: ElementRecord, byId: Map<string, ElementRecord>): Fact[];
|
|
42
|
+
/** `[fact] [fact]` — the form these take on a digest line. */
|
|
43
|
+
export declare function renderFacts(facts: Fact[]): string;
|
|
44
|
+
/** Every element that has at least one fact, in document order. */
|
|
45
|
+
export declare function factsForAll(elements: ElementRecord[]): Array<{
|
|
46
|
+
element: ElementRecord;
|
|
47
|
+
facts: Fact[];
|
|
48
|
+
}>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facts, with the facts that disambiguate them.
|
|
3
|
+
*
|
|
4
|
+
* This is the structural fix for how the prototype died. It shipped checks: a
|
|
5
|
+
* rule looked at `scrollWidth - clientWidth`, compared it to a threshold, and
|
|
6
|
+
* emitted a finding. On PR #4813 it measured `scrollWidth: 815, clientWidth:
|
|
7
|
+
* 668` -- a real 147px of truncated heading -- and threw the measurement away
|
|
8
|
+
* because the rule that owned it did not fire. Three separate false-positive
|
|
9
|
+
* classes in its final week all had the same shape: the check lacked a fact the
|
|
10
|
+
* DOM already had.
|
|
11
|
+
*
|
|
12
|
+
* So nothing here decides. A fact is emitted whenever the measurement exists,
|
|
13
|
+
* unconditionally, carrying the adjacent CSS that determines what the
|
|
14
|
+
* measurement *means*:
|
|
15
|
+
*
|
|
16
|
+
* [overflow-x +147px; clip, ellipsis] text is being cut off
|
|
17
|
+
* [overflow-x +227px; clip, wraps] same clip, text just got taller
|
|
18
|
+
* [overflow-x +90px; scroll] a scroll region, working as intended
|
|
19
|
+
*
|
|
20
|
+
* Same number, three different situations, and the difference is never in the
|
|
21
|
+
* number. The reader draws the conclusion; this file refuses to.
|
|
22
|
+
*/
|
|
23
|
+
import type { ElementRecord } from "./types.js";
|
|
24
|
+
export type FactKind = 'overflow-x' | 'overflow-y' | 'zero-box' | 'clipped';
|
|
25
|
+
export interface Fact {
|
|
26
|
+
kind: FactKind;
|
|
27
|
+
/** The rendered body, without the surrounding brackets. */
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Subpixel layout noise. Below this, an "overflow" is a rounding artefact.
|
|
32
|
+
* Exported so `render.ts`'s fold variance uses the same threshold rather than
|
|
33
|
+
* its own magic number -- a fold reporting a 1px spread that is really 0.2px
|
|
34
|
+
* of rounding is the same bug this constant exists to prevent here.
|
|
35
|
+
*/
|
|
36
|
+
export declare const OVERFLOW_EPSILON = 1;
|
|
37
|
+
/**
|
|
38
|
+
* Every fact this element has. Unconditional: no thresholds beyond subpixel
|
|
39
|
+
* noise, no severity, no suppression.
|
|
40
|
+
*/
|
|
41
|
+
export declare function factsFor(el: ElementRecord, byId: Map<string, ElementRecord>): Fact[];
|
|
42
|
+
/** `[fact] [fact]` — the form these take on a digest line. */
|
|
43
|
+
export declare function renderFacts(facts: Fact[]): string;
|
|
44
|
+
/** Every element that has at least one fact, in document order. */
|
|
45
|
+
export declare function factsForAll(elements: ElementRecord[]): Array<{
|
|
46
|
+
element: ElementRecord;
|
|
47
|
+
facts: Fact[];
|
|
48
|
+
}>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subpixel layout noise. Below this, an "overflow" is a rounding artefact.
|
|
3
|
+
* Exported so `render.ts`'s fold variance uses the same threshold rather than
|
|
4
|
+
* its own magic number -- a fold reporting a 1px spread that is really 0.2px
|
|
5
|
+
* of rounding is the same bug this constant exists to prevent here.
|
|
6
|
+
*/
|
|
7
|
+
export const OVERFLOW_EPSILON = 1;
|
|
8
|
+
/** A box narrower or shorter than this has collapsed, whatever it intended. */
|
|
9
|
+
const ZERO_EPSILON = 1;
|
|
10
|
+
const CLIPS = /^(hidden|clip)$/;
|
|
11
|
+
const SCROLLS = /^(auto|scroll|overlay)$/;
|
|
12
|
+
/** White-space values under which text will not wrap. */
|
|
13
|
+
const NO_WRAP = /^(nowrap|pre)$/;
|
|
14
|
+
const round = (n) => Number.isInteger(n) ? String(n) : n.toFixed(1).replace(/\.0$/, '');
|
|
15
|
+
/**
|
|
16
|
+
* What happens to content that exceeds the box on this axis.
|
|
17
|
+
*
|
|
18
|
+
* Exactly one of `clip` / `scroll` / `visible` always applies -- it is a
|
|
19
|
+
* partition of the `overflow` values. The wrapping facts are additional, and
|
|
20
|
+
* `ellipsis` suppresses `wraps` because `text-overflow` only takes effect when
|
|
21
|
+
* wrapping is already off; printing both would be restating one CSS rule twice.
|
|
22
|
+
*/
|
|
23
|
+
function consequences(el, axis) {
|
|
24
|
+
const overflow = axis === 'x' ? el.overflowX : el.overflowY;
|
|
25
|
+
const out = [];
|
|
26
|
+
const clips = CLIPS.test(overflow);
|
|
27
|
+
if (clips)
|
|
28
|
+
out.push('clip');
|
|
29
|
+
else if (SCROLLS.test(overflow))
|
|
30
|
+
out.push('scroll');
|
|
31
|
+
else
|
|
32
|
+
out.push('visible');
|
|
33
|
+
if (axis === 'x') {
|
|
34
|
+
const ellipsis = el.textOverflow === 'ellipsis' && (clips || SCROLLS.test(overflow));
|
|
35
|
+
if (ellipsis)
|
|
36
|
+
out.push('ellipsis');
|
|
37
|
+
else if (!NO_WRAP.test(el.whiteSpace))
|
|
38
|
+
out.push('wraps');
|
|
39
|
+
else
|
|
40
|
+
out.push('nowrap');
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether a collapsed box is hiding anything.
|
|
46
|
+
*
|
|
47
|
+
* A `0x0` spacer is ordinary and uninteresting; a `0x1239` column with a page
|
|
48
|
+
* of content inside it is the bug the prototype measured and discarded. The
|
|
49
|
+
* difference is entirely in what the box contains, so that is what the fact
|
|
50
|
+
* carries.
|
|
51
|
+
*/
|
|
52
|
+
function contentOf(el, byId) {
|
|
53
|
+
if (el.text)
|
|
54
|
+
return `"${el.text}"`;
|
|
55
|
+
let text = '';
|
|
56
|
+
let elements = 0;
|
|
57
|
+
const queue = [...el.childIds];
|
|
58
|
+
while (queue.length > 0 && text.length < 40) {
|
|
59
|
+
const child = byId.get(queue.shift() ?? '');
|
|
60
|
+
if (!child)
|
|
61
|
+
continue;
|
|
62
|
+
elements++;
|
|
63
|
+
if (child.text)
|
|
64
|
+
text = `${text} ${child.text}`.trim();
|
|
65
|
+
queue.push(...child.childIds);
|
|
66
|
+
}
|
|
67
|
+
if (text)
|
|
68
|
+
return `"${text.slice(0, 40)}"`;
|
|
69
|
+
return elements > 0 ? `${elements} element${elements === 1 ? '' : 's'}` : null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* How much of an element an ancestor hides, and whether anyone can get to it.
|
|
73
|
+
*
|
|
74
|
+
* `attachClipFacts` has populated `ElementRecord.clip` on both capture paths
|
|
75
|
+
* since step 4, and until now nothing rendered it — the digest emitted the
|
|
76
|
+
* three fact kinds above and silently dropped this one. The V1 cold read's
|
|
77
|
+
* single strongest result came from the step-2 spike's renderer, not from
|
|
78
|
+
* shipped code, and nobody noticed because the tests asserted on `clip` the
|
|
79
|
+
* field rather than on the output.
|
|
80
|
+
*
|
|
81
|
+
* `scrollable` is the counter-fact that carries the whole meaning. A box
|
|
82
|
+
* outside an `overflow:auto` ancestor is a scroll region working as intended;
|
|
83
|
+
* the same box outside an `overflow:hidden` ancestor is content nobody can
|
|
84
|
+
* reach. Printing the extent without the reachability is a measurement the
|
|
85
|
+
* reader cannot act on.
|
|
86
|
+
*/
|
|
87
|
+
function clipFact(el) {
|
|
88
|
+
const clip = el.clip;
|
|
89
|
+
if (!clip)
|
|
90
|
+
return null;
|
|
91
|
+
const where = [
|
|
92
|
+
clip.hidden.top ? `${round(clip.hidden.top)}px above` : null,
|
|
93
|
+
clip.hidden.bottom ? `${round(clip.hidden.bottom)}px below` : null,
|
|
94
|
+
clip.hidden.left ? `${round(clip.hidden.left)}px left` : null,
|
|
95
|
+
clip.hidden.right ? `${round(clip.hidden.right)}px right` : null,
|
|
96
|
+
]
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.join(', ');
|
|
99
|
+
const visible = clip.visible.width < ZERO_EPSILON || clip.visible.height < ZERO_EPSILON
|
|
100
|
+
? 'nothing visible'
|
|
101
|
+
: `${round(clip.visible.width)}x${round(clip.visible.height)} visible`;
|
|
102
|
+
return {
|
|
103
|
+
kind: 'clipped',
|
|
104
|
+
text: `clipped by ${clip.by} ${clip.byTag}; overflow:${clip.overflow}, ` +
|
|
105
|
+
`${clip.scrollable ? 'scrollable' : 'not scrollable'}; ${where}, ${visible}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Every fact this element has. Unconditional: no thresholds beyond subpixel
|
|
110
|
+
* noise, no severity, no suppression.
|
|
111
|
+
*/
|
|
112
|
+
export function factsFor(el, byId) {
|
|
113
|
+
const facts = [];
|
|
114
|
+
const overflowX = el.scrollWidth - el.clientWidth;
|
|
115
|
+
if (overflowX > OVERFLOW_EPSILON) {
|
|
116
|
+
facts.push({
|
|
117
|
+
kind: 'overflow-x',
|
|
118
|
+
text: `overflow-x +${round(overflowX)}px; ${consequences(el, 'x').join(', ')}`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const overflowY = el.scrollHeight - el.clientHeight;
|
|
122
|
+
if (overflowY > OVERFLOW_EPSILON) {
|
|
123
|
+
facts.push({
|
|
124
|
+
kind: 'overflow-y',
|
|
125
|
+
text: `overflow-y +${round(overflowY)}px; ${consequences(el, 'y').join(', ')}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Only for elements that are still in the layout. `display:none` collapses
|
|
129
|
+
// everything to 0x0 and saying so about every hidden node is noise.
|
|
130
|
+
const collapsed = el.display !== 'none' &&
|
|
131
|
+
(el.rect.width < ZERO_EPSILON || el.rect.height < ZERO_EPSILON) &&
|
|
132
|
+
!(el.rect.width < ZERO_EPSILON && el.rect.height < ZERO_EPSILON && !el.childIds.length);
|
|
133
|
+
if (collapsed) {
|
|
134
|
+
const content = contentOf(el, byId);
|
|
135
|
+
if (content) {
|
|
136
|
+
facts.push({
|
|
137
|
+
kind: 'zero-box',
|
|
138
|
+
text: `${round(el.rect.width)}x${round(el.rect.height)} zero-box; content ${content}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const clipped = clipFact(el);
|
|
143
|
+
if (clipped)
|
|
144
|
+
facts.push(clipped);
|
|
145
|
+
return facts;
|
|
146
|
+
}
|
|
147
|
+
/** `[fact] [fact]` — the form these take on a digest line. */
|
|
148
|
+
export function renderFacts(facts) {
|
|
149
|
+
return facts.map((f) => `[${f.text}]`).join(' ');
|
|
150
|
+
}
|
|
151
|
+
/** Every element that has at least one fact, in document order. */
|
|
152
|
+
export function factsForAll(elements) {
|
|
153
|
+
const byId = new Map(elements.map((e) => [e.id, e]));
|
|
154
|
+
const out = [];
|
|
155
|
+
for (const element of elements) {
|
|
156
|
+
const facts = factsFor(element, byId);
|
|
157
|
+
if (facts.length > 0)
|
|
158
|
+
out.push({ element, facts });
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Limits shared by both capture paths.
|
|
4
|
+
*
|
|
5
|
+
* They live here rather than in either collector because a capture has to mean
|
|
6
|
+
* the same thing whichever path produced it. The first benchmark caught them
|
|
7
|
+
* disagreeing: on a Wikipedia article the in-page walk stopped at 5,000
|
|
8
|
+
* elements and reported `truncated: true`, while the CDP path returned 6,927
|
|
9
|
+
* and hardcoded `truncated: false`. Two captures of one page that are not
|
|
10
|
+
* comparable, and a flag that silently means "maybe".
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.COLOR_TOKEN_CAP = exports.SKIPPED_TAGS = exports.TESTID_ATTRS = exports.NAME_LENGTH = exports.TEXT_PREVIEW_LENGTH = exports.MAX_ELEMENTS = void 0;
|
|
14
|
+
exports.capBackgroundImage = capBackgroundImage;
|
|
15
|
+
/**
|
|
16
|
+
* The element ceiling. Not a performance guard -- both paths are fast enough
|
|
17
|
+
* at this size -- but a token guard: a digest of 5,000 elements is already far
|
|
18
|
+
* past what any consumer will read, and past it the honest answer is
|
|
19
|
+
* `truncated: true` plus a narrower capture.
|
|
20
|
+
*/
|
|
21
|
+
exports.MAX_ELEMENTS = 5000;
|
|
22
|
+
/** Own-text preview length, in characters. */
|
|
23
|
+
exports.TEXT_PREVIEW_LENGTH = 60;
|
|
24
|
+
/** Accessible-name length, in characters. */
|
|
25
|
+
exports.NAME_LENGTH = 80;
|
|
26
|
+
/** Attributes treated as a stable test id, in priority order. */
|
|
27
|
+
exports.TESTID_ATTRS = ['data-testid', 'data-test-id', 'data-test', 'data-qa'];
|
|
28
|
+
/** Tags whose subtrees carry no layout worth measuring. */
|
|
29
|
+
exports.SKIPPED_TAGS = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG'];
|
|
30
|
+
/**
|
|
31
|
+
* `background-image` / `text-shadow` string cap, in characters.
|
|
32
|
+
*
|
|
33
|
+
* A gradient or shadow's computed value can run to hundreds of characters
|
|
34
|
+
* (many stops, multiple shadows) — `docs/notes/03-color.md` §7 estimates real
|
|
35
|
+
* bundle growth at ~105 bytes/element for the naive uncapped fields, so this
|
|
36
|
+
* caps the two least-bounded ones at the point of capture rather than on
|
|
37
|
+
* render. `snapshot/color.ts` only ever reads the first and last gradient
|
|
38
|
+
* stop and the first shadow color — but "only reads the ends" is exactly
|
|
39
|
+
* why a *plain prefix* cut is wrong for `background-image`: a real 137-char
|
|
40
|
+
* gradient truncated at 120 chars loses its own last stop, the one the
|
|
41
|
+
* gradient token needs (verified against a real capture; A4). See
|
|
42
|
+
* `capBackgroundImage` below for the fix. `text-shadow` has no such gap —
|
|
43
|
+
* `firstShadowColor` only ever reads the *first* comma-separated segment's
|
|
44
|
+
* first color token, so a plain prefix cut is still exactly right for it.
|
|
45
|
+
*/
|
|
46
|
+
exports.COLOR_TOKEN_CAP = 120;
|
|
47
|
+
/**
|
|
48
|
+
* Caps `raw` to `cap` characters while preserving *both* ends, joined by an
|
|
49
|
+
* ellipsis, instead of a plain prefix cut. `background-image`'s only two
|
|
50
|
+
* downstream readers (`resolveBackdrop`'s gradient-stop parse and `q color`'s
|
|
51
|
+
* display) need the *first* and *last* color token, never the middle — a
|
|
52
|
+
* plain `slice(0, cap)` can and does drop the last stop off a real multi-stop
|
|
53
|
+
* gradient whose computed string runs past `cap` (A4). Splitting the budget
|
|
54
|
+
* across both ends fixes that regardless of how many stops sit in the middle:
|
|
55
|
+
* the string might read `linear-gradient(...) 0%, …, rgb(255,255,255) 100%)`
|
|
56
|
+
* instead of every stop, but the two that matter always survive.
|
|
57
|
+
*
|
|
58
|
+
* `collect.ts` restates this inline (`page.evaluate` serializes only its own
|
|
59
|
+
* function source, so it cannot import this module) — kept in sync by
|
|
60
|
+
* `test/unit/limits.test.ts`, the same discipline `MAX_ELEMENTS` and the
|
|
61
|
+
* other capture-path constants already use.
|
|
62
|
+
*/
|
|
63
|
+
function capBackgroundImage(raw, cap = exports.COLOR_TOKEN_CAP) {
|
|
64
|
+
if (raw.length <= cap)
|
|
65
|
+
return raw;
|
|
66
|
+
const ELLIPSIS = '…';
|
|
67
|
+
const half = Math.max(0, Math.floor((cap - ELLIPSIS.length) / 2));
|
|
68
|
+
return `${raw.slice(0, half)}${ELLIPSIS}${raw.slice(raw.length - half)}`;
|
|
69
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Limits shared by both capture paths.
|
|
3
|
+
*
|
|
4
|
+
* They live here rather than in either collector because a capture has to mean
|
|
5
|
+
* the same thing whichever path produced it. The first benchmark caught them
|
|
6
|
+
* disagreeing: on a Wikipedia article the in-page walk stopped at 5,000
|
|
7
|
+
* elements and reported `truncated: true`, while the CDP path returned 6,927
|
|
8
|
+
* and hardcoded `truncated: false`. Two captures of one page that are not
|
|
9
|
+
* comparable, and a flag that silently means "maybe".
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The element ceiling. Not a performance guard -- both paths are fast enough
|
|
13
|
+
* at this size -- but a token guard: a digest of 5,000 elements is already far
|
|
14
|
+
* past what any consumer will read, and past it the honest answer is
|
|
15
|
+
* `truncated: true` plus a narrower capture.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MAX_ELEMENTS = 5000;
|
|
18
|
+
/** Own-text preview length, in characters. */
|
|
19
|
+
export declare const TEXT_PREVIEW_LENGTH = 60;
|
|
20
|
+
/** Accessible-name length, in characters. */
|
|
21
|
+
export declare const NAME_LENGTH = 80;
|
|
22
|
+
/** Attributes treated as a stable test id, in priority order. */
|
|
23
|
+
export declare const TESTID_ATTRS: readonly ["data-testid", "data-test-id", "data-test", "data-qa"];
|
|
24
|
+
/** Tags whose subtrees carry no layout worth measuring. */
|
|
25
|
+
export declare const SKIPPED_TAGS: readonly ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "SVG"];
|
|
26
|
+
/**
|
|
27
|
+
* `background-image` / `text-shadow` string cap, in characters.
|
|
28
|
+
*
|
|
29
|
+
* A gradient or shadow's computed value can run to hundreds of characters
|
|
30
|
+
* (many stops, multiple shadows) — `docs/notes/03-color.md` §7 estimates real
|
|
31
|
+
* bundle growth at ~105 bytes/element for the naive uncapped fields, so this
|
|
32
|
+
* caps the two least-bounded ones at the point of capture rather than on
|
|
33
|
+
* render. `snapshot/color.ts` only ever reads the first and last gradient
|
|
34
|
+
* stop and the first shadow color — but "only reads the ends" is exactly
|
|
35
|
+
* why a *plain prefix* cut is wrong for `background-image`: a real 137-char
|
|
36
|
+
* gradient truncated at 120 chars loses its own last stop, the one the
|
|
37
|
+
* gradient token needs (verified against a real capture; A4). See
|
|
38
|
+
* `capBackgroundImage` below for the fix. `text-shadow` has no such gap —
|
|
39
|
+
* `firstShadowColor` only ever reads the *first* comma-separated segment's
|
|
40
|
+
* first color token, so a plain prefix cut is still exactly right for it.
|
|
41
|
+
*/
|
|
42
|
+
export declare const COLOR_TOKEN_CAP = 120;
|
|
43
|
+
/**
|
|
44
|
+
* Caps `raw` to `cap` characters while preserving *both* ends, joined by an
|
|
45
|
+
* ellipsis, instead of a plain prefix cut. `background-image`'s only two
|
|
46
|
+
* downstream readers (`resolveBackdrop`'s gradient-stop parse and `q color`'s
|
|
47
|
+
* display) need the *first* and *last* color token, never the middle — a
|
|
48
|
+
* plain `slice(0, cap)` can and does drop the last stop off a real multi-stop
|
|
49
|
+
* gradient whose computed string runs past `cap` (A4). Splitting the budget
|
|
50
|
+
* across both ends fixes that regardless of how many stops sit in the middle:
|
|
51
|
+
* the string might read `linear-gradient(...) 0%, …, rgb(255,255,255) 100%)`
|
|
52
|
+
* instead of every stop, but the two that matter always survive.
|
|
53
|
+
*
|
|
54
|
+
* `collect.ts` restates this inline (`page.evaluate` serializes only its own
|
|
55
|
+
* function source, so it cannot import this module) — kept in sync by
|
|
56
|
+
* `test/unit/limits.test.ts`, the same discipline `MAX_ELEMENTS` and the
|
|
57
|
+
* other capture-path constants already use.
|
|
58
|
+
*/
|
|
59
|
+
export declare function capBackgroundImage(raw: string, cap?: number): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Limits shared by both capture paths.
|
|
3
|
+
*
|
|
4
|
+
* They live here rather than in either collector because a capture has to mean
|
|
5
|
+
* the same thing whichever path produced it. The first benchmark caught them
|
|
6
|
+
* disagreeing: on a Wikipedia article the in-page walk stopped at 5,000
|
|
7
|
+
* elements and reported `truncated: true`, while the CDP path returned 6,927
|
|
8
|
+
* and hardcoded `truncated: false`. Two captures of one page that are not
|
|
9
|
+
* comparable, and a flag that silently means "maybe".
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The element ceiling. Not a performance guard -- both paths are fast enough
|
|
13
|
+
* at this size -- but a token guard: a digest of 5,000 elements is already far
|
|
14
|
+
* past what any consumer will read, and past it the honest answer is
|
|
15
|
+
* `truncated: true` plus a narrower capture.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MAX_ELEMENTS = 5000;
|
|
18
|
+
/** Own-text preview length, in characters. */
|
|
19
|
+
export declare const TEXT_PREVIEW_LENGTH = 60;
|
|
20
|
+
/** Accessible-name length, in characters. */
|
|
21
|
+
export declare const NAME_LENGTH = 80;
|
|
22
|
+
/** Attributes treated as a stable test id, in priority order. */
|
|
23
|
+
export declare const TESTID_ATTRS: readonly ["data-testid", "data-test-id", "data-test", "data-qa"];
|
|
24
|
+
/** Tags whose subtrees carry no layout worth measuring. */
|
|
25
|
+
export declare const SKIPPED_TAGS: readonly ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "SVG"];
|
|
26
|
+
/**
|
|
27
|
+
* `background-image` / `text-shadow` string cap, in characters.
|
|
28
|
+
*
|
|
29
|
+
* A gradient or shadow's computed value can run to hundreds of characters
|
|
30
|
+
* (many stops, multiple shadows) — `docs/notes/03-color.md` §7 estimates real
|
|
31
|
+
* bundle growth at ~105 bytes/element for the naive uncapped fields, so this
|
|
32
|
+
* caps the two least-bounded ones at the point of capture rather than on
|
|
33
|
+
* render. `snapshot/color.ts` only ever reads the first and last gradient
|
|
34
|
+
* stop and the first shadow color — but "only reads the ends" is exactly
|
|
35
|
+
* why a *plain prefix* cut is wrong for `background-image`: a real 137-char
|
|
36
|
+
* gradient truncated at 120 chars loses its own last stop, the one the
|
|
37
|
+
* gradient token needs (verified against a real capture; A4). See
|
|
38
|
+
* `capBackgroundImage` below for the fix. `text-shadow` has no such gap —
|
|
39
|
+
* `firstShadowColor` only ever reads the *first* comma-separated segment's
|
|
40
|
+
* first color token, so a plain prefix cut is still exactly right for it.
|
|
41
|
+
*/
|
|
42
|
+
export declare const COLOR_TOKEN_CAP = 120;
|
|
43
|
+
/**
|
|
44
|
+
* Caps `raw` to `cap` characters while preserving *both* ends, joined by an
|
|
45
|
+
* ellipsis, instead of a plain prefix cut. `background-image`'s only two
|
|
46
|
+
* downstream readers (`resolveBackdrop`'s gradient-stop parse and `q color`'s
|
|
47
|
+
* display) need the *first* and *last* color token, never the middle — a
|
|
48
|
+
* plain `slice(0, cap)` can and does drop the last stop off a real multi-stop
|
|
49
|
+
* gradient whose computed string runs past `cap` (A4). Splitting the budget
|
|
50
|
+
* across both ends fixes that regardless of how many stops sit in the middle:
|
|
51
|
+
* the string might read `linear-gradient(...) 0%, …, rgb(255,255,255) 100%)`
|
|
52
|
+
* instead of every stop, but the two that matter always survive.
|
|
53
|
+
*
|
|
54
|
+
* `collect.ts` restates this inline (`page.evaluate` serializes only its own
|
|
55
|
+
* function source, so it cannot import this module) — kept in sync by
|
|
56
|
+
* `test/unit/limits.test.ts`, the same discipline `MAX_ELEMENTS` and the
|
|
57
|
+
* other capture-path constants already use.
|
|
58
|
+
*/
|
|
59
|
+
export declare function capBackgroundImage(raw: string, cap?: number): string;
|