@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,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure two-rect primitives — no tree awareness, no `ElementRecord`. Every
|
|
3
|
+
* function here takes plain `Rect`s so the collector, the digest, and the
|
|
4
|
+
* state diff can all share the exact same arithmetic.
|
|
5
|
+
*
|
|
6
|
+
* Ported unchanged from the prototype (`packages/layout-audit/src/analysis/
|
|
7
|
+
* geometry.ts`), where it was the best-tested pure module in the package.
|
|
8
|
+
*/
|
|
9
|
+
import type { Rect } from "./types.cjs";
|
|
10
|
+
/**
|
|
11
|
+
* The overlapping rectangle between `a` and `b`, or `null` if they don't
|
|
12
|
+
* overlap on both axes. Touching edges count as no overlap — the intersection
|
|
13
|
+
* would have zero area, and a zero-area rect is not a thing anything on screen
|
|
14
|
+
* can be inside of.
|
|
15
|
+
*/
|
|
16
|
+
export declare function intersect(a: Rect, b: Rect): Rect | null;
|
|
17
|
+
/** True if `inner` is fully within `outer`. Edges touching counts as contained. */
|
|
18
|
+
export declare function contains(outer: Rect, inner: Rect): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Signed edge-to-edge distance between `a` and `b` along one axis — positive is
|
|
21
|
+
* a real gap, negative is how far the two rects overlap along that axis.
|
|
22
|
+
*
|
|
23
|
+
* Unlike `intersect`, this is per-axis: two rects side by side in a row overlap
|
|
24
|
+
* on `y` and have a positive gap on `x`, which is exactly the question a gutter
|
|
25
|
+
* measurement asks.
|
|
26
|
+
*/
|
|
27
|
+
export declare function gapAlong(a: Rect, b: Rect, axis: 'x' | 'y'): number;
|
|
28
|
+
/** Rect area in px². */
|
|
29
|
+
export declare function area(r: Rect): number;
|
|
30
|
+
/** Rect center point. */
|
|
31
|
+
export declare function center(r: Rect): {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure two-rect primitives — no tree awareness, no `ElementRecord`. Every
|
|
3
|
+
* function here takes plain `Rect`s so the collector, the digest, and the
|
|
4
|
+
* state diff can all share the exact same arithmetic.
|
|
5
|
+
*
|
|
6
|
+
* Ported unchanged from the prototype (`packages/layout-audit/src/analysis/
|
|
7
|
+
* geometry.ts`), where it was the best-tested pure module in the package.
|
|
8
|
+
*/
|
|
9
|
+
import type { Rect } from "./types.js";
|
|
10
|
+
/**
|
|
11
|
+
* The overlapping rectangle between `a` and `b`, or `null` if they don't
|
|
12
|
+
* overlap on both axes. Touching edges count as no overlap — the intersection
|
|
13
|
+
* would have zero area, and a zero-area rect is not a thing anything on screen
|
|
14
|
+
* can be inside of.
|
|
15
|
+
*/
|
|
16
|
+
export declare function intersect(a: Rect, b: Rect): Rect | null;
|
|
17
|
+
/** True if `inner` is fully within `outer`. Edges touching counts as contained. */
|
|
18
|
+
export declare function contains(outer: Rect, inner: Rect): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Signed edge-to-edge distance between `a` and `b` along one axis — positive is
|
|
21
|
+
* a real gap, negative is how far the two rects overlap along that axis.
|
|
22
|
+
*
|
|
23
|
+
* Unlike `intersect`, this is per-axis: two rects side by side in a row overlap
|
|
24
|
+
* on `y` and have a positive gap on `x`, which is exactly the question a gutter
|
|
25
|
+
* measurement asks.
|
|
26
|
+
*/
|
|
27
|
+
export declare function gapAlong(a: Rect, b: Rect, axis: 'x' | 'y'): number;
|
|
28
|
+
/** Rect area in px². */
|
|
29
|
+
export declare function area(r: Rect): number;
|
|
30
|
+
/** Rect center point. */
|
|
31
|
+
export declare function center(r: Rect): {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The overlapping rectangle between `a` and `b`, or `null` if they don't
|
|
3
|
+
* overlap on both axes. Touching edges count as no overlap — the intersection
|
|
4
|
+
* would have zero area, and a zero-area rect is not a thing anything on screen
|
|
5
|
+
* can be inside of.
|
|
6
|
+
*/
|
|
7
|
+
export function intersect(a, b) {
|
|
8
|
+
const x = Math.max(a.x, b.x);
|
|
9
|
+
const y = Math.max(a.y, b.y);
|
|
10
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
11
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
12
|
+
const width = right - x;
|
|
13
|
+
const height = bottom - y;
|
|
14
|
+
if (width <= 0 || height <= 0)
|
|
15
|
+
return null;
|
|
16
|
+
return { x, y, width, height };
|
|
17
|
+
}
|
|
18
|
+
/** True if `inner` is fully within `outer`. Edges touching counts as contained. */
|
|
19
|
+
export function contains(outer, inner) {
|
|
20
|
+
return (inner.x >= outer.x &&
|
|
21
|
+
inner.y >= outer.y &&
|
|
22
|
+
inner.x + inner.width <= outer.x + outer.width &&
|
|
23
|
+
inner.y + inner.height <= outer.y + outer.height);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Signed edge-to-edge distance between `a` and `b` along one axis — positive is
|
|
27
|
+
* a real gap, negative is how far the two rects overlap along that axis.
|
|
28
|
+
*
|
|
29
|
+
* Unlike `intersect`, this is per-axis: two rects side by side in a row overlap
|
|
30
|
+
* on `y` and have a positive gap on `x`, which is exactly the question a gutter
|
|
31
|
+
* measurement asks.
|
|
32
|
+
*/
|
|
33
|
+
export function gapAlong(a, b, axis) {
|
|
34
|
+
const size = axis === 'x' ? 'width' : 'height';
|
|
35
|
+
const aStart = a[axis];
|
|
36
|
+
const aEnd = aStart + a[size];
|
|
37
|
+
const bStart = b[axis];
|
|
38
|
+
const bEnd = bStart + b[size];
|
|
39
|
+
if (bStart >= aEnd)
|
|
40
|
+
return bStart - aEnd;
|
|
41
|
+
if (aStart >= bEnd)
|
|
42
|
+
return aStart - bEnd;
|
|
43
|
+
// Overlapping along this axis: negative distance, magnitude of the overlap.
|
|
44
|
+
return Math.max(aStart, bStart) - Math.min(aEnd, bEnd);
|
|
45
|
+
}
|
|
46
|
+
/** Rect area in px². */
|
|
47
|
+
export function area(r) {
|
|
48
|
+
return r.width * r.height;
|
|
49
|
+
}
|
|
50
|
+
/** Rect center point. */
|
|
51
|
+
export function center(r) {
|
|
52
|
+
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
|
53
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildLayoutTree = buildLayoutTree;
|
|
4
|
+
/**
|
|
5
|
+
* `ElementRecord.tag` is always `el.tagName.toLowerCase()` (see
|
|
6
|
+
* `snapshot/collect.ts`) — HTML and SVG tags are stored in the same case,
|
|
7
|
+
* so there is no "starts lowercase" signal left in the data to check by the
|
|
8
|
+
* time it reaches this module (the step-6 fix already drops the `<svg>`
|
|
9
|
+
* subtree, root included, at collection time). This set is the practical
|
|
10
|
+
* replacement: a fixed list of SVG-only tag names, kept as a defensive net
|
|
11
|
+
* in case `collectSnapshot`'s `SKIPPED_TAGS` ever changes to stop dropping
|
|
12
|
+
* the `<svg>` root itself. `svg` is deliberately absent — the root svg
|
|
13
|
+
* element is a leaf "icon" box per the classification pass below, not
|
|
14
|
+
* dropped.
|
|
15
|
+
*/
|
|
16
|
+
const SVG_INTERNAL_TAGS = new Set([
|
|
17
|
+
'g',
|
|
18
|
+
'path',
|
|
19
|
+
'rect',
|
|
20
|
+
'circle',
|
|
21
|
+
'ellipse',
|
|
22
|
+
'line',
|
|
23
|
+
'polyline',
|
|
24
|
+
'polygon',
|
|
25
|
+
'defs',
|
|
26
|
+
'clippath',
|
|
27
|
+
'lineargradient',
|
|
28
|
+
'radialgradient',
|
|
29
|
+
'stop',
|
|
30
|
+
'use',
|
|
31
|
+
'symbol',
|
|
32
|
+
'mask',
|
|
33
|
+
'pattern',
|
|
34
|
+
'text',
|
|
35
|
+
'tspan',
|
|
36
|
+
'foreignobject',
|
|
37
|
+
]);
|
|
38
|
+
const REPLACED_TAGS = new Set(['img', 'input', 'textarea', 'select', 'button']);
|
|
39
|
+
/** ~1px tolerance for the "rect equals parent's content box" collapse
|
|
40
|
+
* check — real layouts carry subpixel rounding. */
|
|
41
|
+
const COLLAPSE_TOLERANCE = 1;
|
|
42
|
+
function contentBox(el) {
|
|
43
|
+
return {
|
|
44
|
+
x: el.rect.x + el.padding.left + el.border.left,
|
|
45
|
+
y: el.rect.y + el.padding.top + el.border.top,
|
|
46
|
+
width: el.rect.width - el.padding.left - el.padding.right - el.border.left - el.border.right,
|
|
47
|
+
height: el.rect.height - el.padding.top - el.padding.bottom - el.border.top - el.border.bottom,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function rectsMatch(a, b, tolerance = COLLAPSE_TOLERANCE) {
|
|
51
|
+
return (Math.abs(a.x - b.x) <= tolerance &&
|
|
52
|
+
Math.abs(a.y - b.y) <= tolerance &&
|
|
53
|
+
Math.abs(a.width - b.width) <= tolerance &&
|
|
54
|
+
Math.abs(a.height - b.height) <= tolerance);
|
|
55
|
+
}
|
|
56
|
+
function hasVisibleBorder(el) {
|
|
57
|
+
return el.border.top > 0 || el.border.right > 0 || el.border.bottom > 0 || el.border.left > 0;
|
|
58
|
+
}
|
|
59
|
+
function sameBorder(a, b) {
|
|
60
|
+
return (a.border.top === b.border.top &&
|
|
61
|
+
a.border.right === b.border.right &&
|
|
62
|
+
a.border.bottom === b.border.bottom &&
|
|
63
|
+
a.border.left === b.border.left);
|
|
64
|
+
}
|
|
65
|
+
/** `rgba(r, g, b, 0)` / `transparent` are the only two forms
|
|
66
|
+
* `getComputedStyle` returns for "no background" — anything else paints
|
|
67
|
+
* something, even a translucent color. */
|
|
68
|
+
function hasVisibleBackground(el) {
|
|
69
|
+
const bg = el.backgroundColor.trim();
|
|
70
|
+
if (bg === '' || bg === 'transparent')
|
|
71
|
+
return false;
|
|
72
|
+
const alphaMatch = /rgba?\([^)]*,\s*([\d.]+)\s*\)/.exec(bg);
|
|
73
|
+
if (alphaMatch?.[1] !== undefined && Number.parseFloat(alphaMatch[1]) === 0)
|
|
74
|
+
return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Leaf if: has non-empty own text, is a replaced element or an SVG root, or
|
|
79
|
+
* carries a visible border/background distinct from its parent's (a
|
|
80
|
+
* decorated box like a card or a chip is treated as one atomic layout unit
|
|
81
|
+
* — its internal structure isn't relevant to gap/overlap/uniformity
|
|
82
|
+
* analysis at the level above it). `parent` is `null` only at a document
|
|
83
|
+
* root, where "distinct from parent" degrades to "has one at all".
|
|
84
|
+
*/
|
|
85
|
+
function isLeaf(el, parent) {
|
|
86
|
+
if (el.text.trim().length > 0)
|
|
87
|
+
return true;
|
|
88
|
+
if (REPLACED_TAGS.has(el.tag))
|
|
89
|
+
return true;
|
|
90
|
+
if (el.tag === 'svg')
|
|
91
|
+
return true;
|
|
92
|
+
const ownBorder = hasVisibleBorder(el);
|
|
93
|
+
const ownBackground = hasVisibleBackground(el);
|
|
94
|
+
if (!parent)
|
|
95
|
+
return ownBorder || ownBackground;
|
|
96
|
+
if (ownBorder && !sameBorder(el, parent))
|
|
97
|
+
return true;
|
|
98
|
+
if (ownBackground && el.backgroundColor !== parent.backgroundColor)
|
|
99
|
+
return true;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Every flex container reported `row` until this took `flex-direction` into
|
|
104
|
+
* account, and `column` -- a value the grammar, the parser and the `Flow` type
|
|
105
|
+
* all carried -- was unreachable.
|
|
106
|
+
*
|
|
107
|
+
* It was not only a mislabelled token. The digest measures a child's gap along
|
|
108
|
+
* its parent's flow axis, so every `flex-direction: column` container had its
|
|
109
|
+
* children measured *horizontally*: a stack of full-width rows reported `→0`
|
|
110
|
+
* between each pair instead of the vertical gap that is actually there.
|
|
111
|
+
*/
|
|
112
|
+
function flowFromDisplay(display, flexDirection) {
|
|
113
|
+
if (display.includes('grid'))
|
|
114
|
+
return 'grid';
|
|
115
|
+
if (display.includes('flex')) {
|
|
116
|
+
return flexDirection.startsWith('column') ? 'column' : 'row';
|
|
117
|
+
}
|
|
118
|
+
return 'none';
|
|
119
|
+
}
|
|
120
|
+
function buildLayoutTree(elements, options = {}) {
|
|
121
|
+
const byId = new Map(elements.map((el) => [el.id, el]));
|
|
122
|
+
const keepIds = options.keepIds;
|
|
123
|
+
let svgInternal = 0;
|
|
124
|
+
let invisible = 0;
|
|
125
|
+
let wrappersFolded = 0;
|
|
126
|
+
function keep(el) {
|
|
127
|
+
// `keepIds` has to be the first check, not the second: an SVG `<path>`
|
|
128
|
+
// (or any other tag in `SVG_INTERNAL_TAGS`) that `factsFor` found
|
|
129
|
+
// something to say about -- most commonly `clipped`, on an icon
|
|
130
|
+
// partially hidden by an ancestor's `overflow` -- used to be dropped
|
|
131
|
+
// here before this function ever looked at `keepIds`, because the
|
|
132
|
+
// SVG-internal check ran first and returned unconditionally. Checking
|
|
133
|
+
// `keepIds` first makes the override total, the same way the doc
|
|
134
|
+
// comment on `BuildLayoutTreeOptions.keepIds` already promises ("the
|
|
135
|
+
// digest passes every element carrying a fact") rather than true only
|
|
136
|
+
// for the one reason (`!el.visible`) that happened to be checked after
|
|
137
|
+
// it.
|
|
138
|
+
if (keepIds?.has(el.id))
|
|
139
|
+
return true;
|
|
140
|
+
if (SVG_INTERNAL_TAGS.has(el.tag)) {
|
|
141
|
+
svgInternal++;
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (!el.visible) {
|
|
145
|
+
invisible++;
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
// Pass 1: filter + reparent. A dropped element's surviving children are
|
|
151
|
+
// spliced up to its nearest surviving ancestor's child list — dropping a
|
|
152
|
+
// node never deletes a subtree, only the one uninformative box.
|
|
153
|
+
function buildChildren(childIds) {
|
|
154
|
+
const result = [];
|
|
155
|
+
for (const id of childIds) {
|
|
156
|
+
const el = byId.get(id);
|
|
157
|
+
if (!el)
|
|
158
|
+
continue;
|
|
159
|
+
if (keep(el)) {
|
|
160
|
+
result.push({ el, children: buildChildren(el.childIds) });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
result.push(...buildChildren(el.childIds));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
// Pass 2 (collapse) + pass 3 (classify), combined per node bottom-up: a
|
|
169
|
+
// node's children must already be finalized before we can test whether
|
|
170
|
+
// this node has exactly one surviving child whose rect matches this
|
|
171
|
+
// node's own content box.
|
|
172
|
+
function build(raw, parent) {
|
|
173
|
+
let children = raw.children.map((child) => build(child, raw.el));
|
|
174
|
+
let wrapped = 0;
|
|
175
|
+
// The representative element for identity/classification purposes —
|
|
176
|
+
// starts as this node's own element, and becomes the innermost folded
|
|
177
|
+
// child's element as wrapper levels collapse into it. The box test
|
|
178
|
+
// itself always uses `raw.el`'s own content box (fixed for this node
|
|
179
|
+
// regardless of how many levels have folded in) — that's the box every
|
|
180
|
+
// absorbed child proved it matched.
|
|
181
|
+
let inner = raw.el;
|
|
182
|
+
const box = contentBox(raw.el);
|
|
183
|
+
while (children.length === 1) {
|
|
184
|
+
// A wrapper that itself carries a fact (`keepIds` -- the same set pass
|
|
185
|
+
// 1's `keep()` checks above, threaded in from `renderDigest` as every
|
|
186
|
+
// id `factsFor` found something to say about) must not have its
|
|
187
|
+
// identity silently swapped out for its child's here. Before this
|
|
188
|
+
// check, `inner`'s reassignment below was unconditional: the wrapper's
|
|
189
|
+
// own id -- and with it, the only thing that could carry a fact
|
|
190
|
+
// computed for the wrapper itself, most commonly `<body>`'s own
|
|
191
|
+
// `overflow-y` when `<body>` has exactly one child filling it -- simply
|
|
192
|
+
// stopped being reachable from any `LayoutNode` in the tree, with no
|
|
193
|
+
// line anywhere disclosing that it happened. `keepIds` already exists
|
|
194
|
+
// for exactly this guarantee ("the digest passes every element
|
|
195
|
+
// carrying a fact") but was only ever consulted by pass 1's filter;
|
|
196
|
+
// this is the same guarantee at the one other point in the pipeline
|
|
197
|
+
// that can erase an element's identity. Stopping the fold at this
|
|
198
|
+
// level -- rather than hoisting the fact onto the surviving child, or
|
|
199
|
+
// collapsing anyway with a disclosure -- keeps the fix inside this
|
|
200
|
+
// module and inside `keepIds`'s existing contract: the wrapper gets its
|
|
201
|
+
// own line, exactly the "direct" route every other fact-bearing
|
|
202
|
+
// element already uses, so no new disclosure format is needed and no
|
|
203
|
+
// fact ever needs re-attributing to an id that didn't measure it.
|
|
204
|
+
if (keepIds?.has(inner.id))
|
|
205
|
+
break;
|
|
206
|
+
const only = children[0];
|
|
207
|
+
if (only && rectsMatch(only.rect, box)) {
|
|
208
|
+
const onlyEl = byId.get(only.elementId);
|
|
209
|
+
if (!onlyEl)
|
|
210
|
+
break;
|
|
211
|
+
wrapped += 1 + only.wrapped;
|
|
212
|
+
wrappersFolded += 1;
|
|
213
|
+
inner = onlyEl;
|
|
214
|
+
children = only.children;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// `kind` is classification metadata only — it does not prune `children`.
|
|
221
|
+
// A card with a background distinct from its parent is a `leaf` in the
|
|
222
|
+
// sense that a digest/query consumer can treat it as one atomic visual
|
|
223
|
+
// unit, but `overlaps`/`siblingGaps`/`siblingUniformity` still need its
|
|
224
|
+
// real children (e.g. an absolutely-positioned badge nested inside a
|
|
225
|
+
// colored card) to be present in the tree to find them.
|
|
226
|
+
const leaf = isLeaf(inner, parent);
|
|
227
|
+
return {
|
|
228
|
+
elementId: inner.id,
|
|
229
|
+
tag: inner.tag,
|
|
230
|
+
kind: leaf ? 'leaf' : 'container',
|
|
231
|
+
flow: flowFromDisplay(inner.display, inner.flexDirection),
|
|
232
|
+
position: inner.position,
|
|
233
|
+
wrapped,
|
|
234
|
+
rect: inner.rect,
|
|
235
|
+
children,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const rootIds = elements.filter((el) => el.parentId === null).map((el) => el.id);
|
|
239
|
+
const rawRoots = buildChildren(rootIds);
|
|
240
|
+
const tree = rawRoots.map((raw) => build(raw, null));
|
|
241
|
+
return {
|
|
242
|
+
tree,
|
|
243
|
+
dropped: { svgInternal, invisible, wrappers: wrappersFolded },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a `LayoutNode` tree from a flat `ElementRecord[]` snapshot — the
|
|
3
|
+
* keystone module of `src/analysis/`: `neighbors.ts` and `uniformity.ts`
|
|
4
|
+
* both walk the tree this produces, and the future digest renderer renders
|
|
5
|
+
* it directly. Get this right; everything else depends on it.
|
|
6
|
+
*
|
|
7
|
+
* Three passes, each pure and independently reasoned about:
|
|
8
|
+
* 1. Filter + reparent — drop elements that aren't layout-meaningful
|
|
9
|
+
* (SVG-internals, invisible, zero-size), splicing any surviving
|
|
10
|
+
* children up to the nearest surviving ancestor so a dropped node never
|
|
11
|
+
* silently deletes a whole subtree.
|
|
12
|
+
* 2. Collapse — fold a lone wrapper child into its parent when the child's
|
|
13
|
+
* rect matches the parent's content box, tracked as `wrapped`, unless the
|
|
14
|
+
* wrapper itself is in `keepIds` (it carries a fact of its own), in which
|
|
15
|
+
* case it keeps its own line instead of being folded away.
|
|
16
|
+
* 3. Classify — decide `leaf` vs `container` per surviving node.
|
|
17
|
+
*/
|
|
18
|
+
import type { ElementRecord } from "../snapshot/types.cjs";
|
|
19
|
+
import type { LayoutTreeResult } from "./types.cjs";
|
|
20
|
+
export interface BuildLayoutTreeOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Elements that must survive whatever their geometry says -- checked at
|
|
23
|
+
* both points in this file that can otherwise erase an element's identity:
|
|
24
|
+
* pass 1's filter (`keep()`, below) and pass 2's wrapper collapse (the
|
|
25
|
+
* `while (children.length === 1)` loop in `build()`).
|
|
26
|
+
*
|
|
27
|
+
* Pass 1 drops anything with no visible box, which is right for the
|
|
28
|
+
* hundreds of zero-size nodes on a real page and catastrophically wrong for
|
|
29
|
+
* the one that matters: a column that collapsed to `0x1239` *around a page of
|
|
30
|
+
* content* has no visible box either. Dropping it deletes the element the
|
|
31
|
+
* whole finding is about, which is the prototype's failure mode wearing a
|
|
32
|
+
* different hat -- the measurement exists and the rendering throws it away.
|
|
33
|
+
*
|
|
34
|
+
* Pass 2's wrapper collapse can do the same thing a different way: it
|
|
35
|
+
* doesn't drop the wrapper's `ElementRecord`, but it does replace the
|
|
36
|
+
* wrapper's identity with its single child's in the rendered tree, and a
|
|
37
|
+
* fact keyed to the wrapper's own id (most commonly `<body>`'s own
|
|
38
|
+
* `overflow-y`) has nowhere left to attach once no `LayoutNode` carries
|
|
39
|
+
* that id anymore.
|
|
40
|
+
*
|
|
41
|
+
* The digest passes every element carrying a fact.
|
|
42
|
+
*/
|
|
43
|
+
keepIds?: ReadonlySet<string>;
|
|
44
|
+
}
|
|
45
|
+
export declare function buildLayoutTree(elements: ElementRecord[], options?: BuildLayoutTreeOptions): LayoutTreeResult;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a `LayoutNode` tree from a flat `ElementRecord[]` snapshot — the
|
|
3
|
+
* keystone module of `src/analysis/`: `neighbors.ts` and `uniformity.ts`
|
|
4
|
+
* both walk the tree this produces, and the future digest renderer renders
|
|
5
|
+
* it directly. Get this right; everything else depends on it.
|
|
6
|
+
*
|
|
7
|
+
* Three passes, each pure and independently reasoned about:
|
|
8
|
+
* 1. Filter + reparent — drop elements that aren't layout-meaningful
|
|
9
|
+
* (SVG-internals, invisible, zero-size), splicing any surviving
|
|
10
|
+
* children up to the nearest surviving ancestor so a dropped node never
|
|
11
|
+
* silently deletes a whole subtree.
|
|
12
|
+
* 2. Collapse — fold a lone wrapper child into its parent when the child's
|
|
13
|
+
* rect matches the parent's content box, tracked as `wrapped`, unless the
|
|
14
|
+
* wrapper itself is in `keepIds` (it carries a fact of its own), in which
|
|
15
|
+
* case it keeps its own line instead of being folded away.
|
|
16
|
+
* 3. Classify — decide `leaf` vs `container` per surviving node.
|
|
17
|
+
*/
|
|
18
|
+
import type { ElementRecord } from "../snapshot/types.js";
|
|
19
|
+
import type { LayoutTreeResult } from "./types.js";
|
|
20
|
+
export interface BuildLayoutTreeOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Elements that must survive whatever their geometry says -- checked at
|
|
23
|
+
* both points in this file that can otherwise erase an element's identity:
|
|
24
|
+
* pass 1's filter (`keep()`, below) and pass 2's wrapper collapse (the
|
|
25
|
+
* `while (children.length === 1)` loop in `build()`).
|
|
26
|
+
*
|
|
27
|
+
* Pass 1 drops anything with no visible box, which is right for the
|
|
28
|
+
* hundreds of zero-size nodes on a real page and catastrophically wrong for
|
|
29
|
+
* the one that matters: a column that collapsed to `0x1239` *around a page of
|
|
30
|
+
* content* has no visible box either. Dropping it deletes the element the
|
|
31
|
+
* whole finding is about, which is the prototype's failure mode wearing a
|
|
32
|
+
* different hat -- the measurement exists and the rendering throws it away.
|
|
33
|
+
*
|
|
34
|
+
* Pass 2's wrapper collapse can do the same thing a different way: it
|
|
35
|
+
* doesn't drop the wrapper's `ElementRecord`, but it does replace the
|
|
36
|
+
* wrapper's identity with its single child's in the rendered tree, and a
|
|
37
|
+
* fact keyed to the wrapper's own id (most commonly `<body>`'s own
|
|
38
|
+
* `overflow-y`) has nowhere left to attach once no `LayoutNode` carries
|
|
39
|
+
* that id anymore.
|
|
40
|
+
*
|
|
41
|
+
* The digest passes every element carrying a fact.
|
|
42
|
+
*/
|
|
43
|
+
keepIds?: ReadonlySet<string>;
|
|
44
|
+
}
|
|
45
|
+
export declare function buildLayoutTree(elements: ElementRecord[], options?: BuildLayoutTreeOptions): LayoutTreeResult;
|