@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,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ElementRecord.tag` is always `el.tagName.toLowerCase()` (see
|
|
3
|
+
* `snapshot/collect.ts`) — HTML and SVG tags are stored in the same case,
|
|
4
|
+
* so there is no "starts lowercase" signal left in the data to check by the
|
|
5
|
+
* time it reaches this module (the step-6 fix already drops the `<svg>`
|
|
6
|
+
* subtree, root included, at collection time). This set is the practical
|
|
7
|
+
* replacement: a fixed list of SVG-only tag names, kept as a defensive net
|
|
8
|
+
* in case `collectSnapshot`'s `SKIPPED_TAGS` ever changes to stop dropping
|
|
9
|
+
* the `<svg>` root itself. `svg` is deliberately absent — the root svg
|
|
10
|
+
* element is a leaf "icon" box per the classification pass below, not
|
|
11
|
+
* dropped.
|
|
12
|
+
*/
|
|
13
|
+
const SVG_INTERNAL_TAGS = new Set([
|
|
14
|
+
'g',
|
|
15
|
+
'path',
|
|
16
|
+
'rect',
|
|
17
|
+
'circle',
|
|
18
|
+
'ellipse',
|
|
19
|
+
'line',
|
|
20
|
+
'polyline',
|
|
21
|
+
'polygon',
|
|
22
|
+
'defs',
|
|
23
|
+
'clippath',
|
|
24
|
+
'lineargradient',
|
|
25
|
+
'radialgradient',
|
|
26
|
+
'stop',
|
|
27
|
+
'use',
|
|
28
|
+
'symbol',
|
|
29
|
+
'mask',
|
|
30
|
+
'pattern',
|
|
31
|
+
'text',
|
|
32
|
+
'tspan',
|
|
33
|
+
'foreignobject',
|
|
34
|
+
]);
|
|
35
|
+
const REPLACED_TAGS = new Set(['img', 'input', 'textarea', 'select', 'button']);
|
|
36
|
+
/** ~1px tolerance for the "rect equals parent's content box" collapse
|
|
37
|
+
* check — real layouts carry subpixel rounding. */
|
|
38
|
+
const COLLAPSE_TOLERANCE = 1;
|
|
39
|
+
function contentBox(el) {
|
|
40
|
+
return {
|
|
41
|
+
x: el.rect.x + el.padding.left + el.border.left,
|
|
42
|
+
y: el.rect.y + el.padding.top + el.border.top,
|
|
43
|
+
width: el.rect.width - el.padding.left - el.padding.right - el.border.left - el.border.right,
|
|
44
|
+
height: el.rect.height - el.padding.top - el.padding.bottom - el.border.top - el.border.bottom,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function rectsMatch(a, b, tolerance = COLLAPSE_TOLERANCE) {
|
|
48
|
+
return (Math.abs(a.x - b.x) <= tolerance &&
|
|
49
|
+
Math.abs(a.y - b.y) <= tolerance &&
|
|
50
|
+
Math.abs(a.width - b.width) <= tolerance &&
|
|
51
|
+
Math.abs(a.height - b.height) <= tolerance);
|
|
52
|
+
}
|
|
53
|
+
function hasVisibleBorder(el) {
|
|
54
|
+
return el.border.top > 0 || el.border.right > 0 || el.border.bottom > 0 || el.border.left > 0;
|
|
55
|
+
}
|
|
56
|
+
function sameBorder(a, b) {
|
|
57
|
+
return (a.border.top === b.border.top &&
|
|
58
|
+
a.border.right === b.border.right &&
|
|
59
|
+
a.border.bottom === b.border.bottom &&
|
|
60
|
+
a.border.left === b.border.left);
|
|
61
|
+
}
|
|
62
|
+
/** `rgba(r, g, b, 0)` / `transparent` are the only two forms
|
|
63
|
+
* `getComputedStyle` returns for "no background" — anything else paints
|
|
64
|
+
* something, even a translucent color. */
|
|
65
|
+
function hasVisibleBackground(el) {
|
|
66
|
+
const bg = el.backgroundColor.trim();
|
|
67
|
+
if (bg === '' || bg === 'transparent')
|
|
68
|
+
return false;
|
|
69
|
+
const alphaMatch = /rgba?\([^)]*,\s*([\d.]+)\s*\)/.exec(bg);
|
|
70
|
+
if (alphaMatch?.[1] !== undefined && Number.parseFloat(alphaMatch[1]) === 0)
|
|
71
|
+
return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Leaf if: has non-empty own text, is a replaced element or an SVG root, or
|
|
76
|
+
* carries a visible border/background distinct from its parent's (a
|
|
77
|
+
* decorated box like a card or a chip is treated as one atomic layout unit
|
|
78
|
+
* — its internal structure isn't relevant to gap/overlap/uniformity
|
|
79
|
+
* analysis at the level above it). `parent` is `null` only at a document
|
|
80
|
+
* root, where "distinct from parent" degrades to "has one at all".
|
|
81
|
+
*/
|
|
82
|
+
function isLeaf(el, parent) {
|
|
83
|
+
if (el.text.trim().length > 0)
|
|
84
|
+
return true;
|
|
85
|
+
if (REPLACED_TAGS.has(el.tag))
|
|
86
|
+
return true;
|
|
87
|
+
if (el.tag === 'svg')
|
|
88
|
+
return true;
|
|
89
|
+
const ownBorder = hasVisibleBorder(el);
|
|
90
|
+
const ownBackground = hasVisibleBackground(el);
|
|
91
|
+
if (!parent)
|
|
92
|
+
return ownBorder || ownBackground;
|
|
93
|
+
if (ownBorder && !sameBorder(el, parent))
|
|
94
|
+
return true;
|
|
95
|
+
if (ownBackground && el.backgroundColor !== parent.backgroundColor)
|
|
96
|
+
return true;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Every flex container reported `row` until this took `flex-direction` into
|
|
101
|
+
* account, and `column` -- a value the grammar, the parser and the `Flow` type
|
|
102
|
+
* all carried -- was unreachable.
|
|
103
|
+
*
|
|
104
|
+
* It was not only a mislabelled token. The digest measures a child's gap along
|
|
105
|
+
* its parent's flow axis, so every `flex-direction: column` container had its
|
|
106
|
+
* children measured *horizontally*: a stack of full-width rows reported `→0`
|
|
107
|
+
* between each pair instead of the vertical gap that is actually there.
|
|
108
|
+
*/
|
|
109
|
+
function flowFromDisplay(display, flexDirection) {
|
|
110
|
+
if (display.includes('grid'))
|
|
111
|
+
return 'grid';
|
|
112
|
+
if (display.includes('flex')) {
|
|
113
|
+
return flexDirection.startsWith('column') ? 'column' : 'row';
|
|
114
|
+
}
|
|
115
|
+
return 'none';
|
|
116
|
+
}
|
|
117
|
+
export function buildLayoutTree(elements, options = {}) {
|
|
118
|
+
const byId = new Map(elements.map((el) => [el.id, el]));
|
|
119
|
+
const keepIds = options.keepIds;
|
|
120
|
+
let svgInternal = 0;
|
|
121
|
+
let invisible = 0;
|
|
122
|
+
let wrappersFolded = 0;
|
|
123
|
+
function keep(el) {
|
|
124
|
+
// `keepIds` has to be the first check, not the second: an SVG `<path>`
|
|
125
|
+
// (or any other tag in `SVG_INTERNAL_TAGS`) that `factsFor` found
|
|
126
|
+
// something to say about -- most commonly `clipped`, on an icon
|
|
127
|
+
// partially hidden by an ancestor's `overflow` -- used to be dropped
|
|
128
|
+
// here before this function ever looked at `keepIds`, because the
|
|
129
|
+
// SVG-internal check ran first and returned unconditionally. Checking
|
|
130
|
+
// `keepIds` first makes the override total, the same way the doc
|
|
131
|
+
// comment on `BuildLayoutTreeOptions.keepIds` already promises ("the
|
|
132
|
+
// digest passes every element carrying a fact") rather than true only
|
|
133
|
+
// for the one reason (`!el.visible`) that happened to be checked after
|
|
134
|
+
// it.
|
|
135
|
+
if (keepIds?.has(el.id))
|
|
136
|
+
return true;
|
|
137
|
+
if (SVG_INTERNAL_TAGS.has(el.tag)) {
|
|
138
|
+
svgInternal++;
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
if (!el.visible) {
|
|
142
|
+
invisible++;
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
// Pass 1: filter + reparent. A dropped element's surviving children are
|
|
148
|
+
// spliced up to its nearest surviving ancestor's child list — dropping a
|
|
149
|
+
// node never deletes a subtree, only the one uninformative box.
|
|
150
|
+
function buildChildren(childIds) {
|
|
151
|
+
const result = [];
|
|
152
|
+
for (const id of childIds) {
|
|
153
|
+
const el = byId.get(id);
|
|
154
|
+
if (!el)
|
|
155
|
+
continue;
|
|
156
|
+
if (keep(el)) {
|
|
157
|
+
result.push({ el, children: buildChildren(el.childIds) });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
result.push(...buildChildren(el.childIds));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
// Pass 2 (collapse) + pass 3 (classify), combined per node bottom-up: a
|
|
166
|
+
// node's children must already be finalized before we can test whether
|
|
167
|
+
// this node has exactly one surviving child whose rect matches this
|
|
168
|
+
// node's own content box.
|
|
169
|
+
function build(raw, parent) {
|
|
170
|
+
let children = raw.children.map((child) => build(child, raw.el));
|
|
171
|
+
let wrapped = 0;
|
|
172
|
+
// The representative element for identity/classification purposes —
|
|
173
|
+
// starts as this node's own element, and becomes the innermost folded
|
|
174
|
+
// child's element as wrapper levels collapse into it. The box test
|
|
175
|
+
// itself always uses `raw.el`'s own content box (fixed for this node
|
|
176
|
+
// regardless of how many levels have folded in) — that's the box every
|
|
177
|
+
// absorbed child proved it matched.
|
|
178
|
+
let inner = raw.el;
|
|
179
|
+
const box = contentBox(raw.el);
|
|
180
|
+
while (children.length === 1) {
|
|
181
|
+
// A wrapper that itself carries a fact (`keepIds` -- the same set pass
|
|
182
|
+
// 1's `keep()` checks above, threaded in from `renderDigest` as every
|
|
183
|
+
// id `factsFor` found something to say about) must not have its
|
|
184
|
+
// identity silently swapped out for its child's here. Before this
|
|
185
|
+
// check, `inner`'s reassignment below was unconditional: the wrapper's
|
|
186
|
+
// own id -- and with it, the only thing that could carry a fact
|
|
187
|
+
// computed for the wrapper itself, most commonly `<body>`'s own
|
|
188
|
+
// `overflow-y` when `<body>` has exactly one child filling it -- simply
|
|
189
|
+
// stopped being reachable from any `LayoutNode` in the tree, with no
|
|
190
|
+
// line anywhere disclosing that it happened. `keepIds` already exists
|
|
191
|
+
// for exactly this guarantee ("the digest passes every element
|
|
192
|
+
// carrying a fact") but was only ever consulted by pass 1's filter;
|
|
193
|
+
// this is the same guarantee at the one other point in the pipeline
|
|
194
|
+
// that can erase an element's identity. Stopping the fold at this
|
|
195
|
+
// level -- rather than hoisting the fact onto the surviving child, or
|
|
196
|
+
// collapsing anyway with a disclosure -- keeps the fix inside this
|
|
197
|
+
// module and inside `keepIds`'s existing contract: the wrapper gets its
|
|
198
|
+
// own line, exactly the "direct" route every other fact-bearing
|
|
199
|
+
// element already uses, so no new disclosure format is needed and no
|
|
200
|
+
// fact ever needs re-attributing to an id that didn't measure it.
|
|
201
|
+
if (keepIds?.has(inner.id))
|
|
202
|
+
break;
|
|
203
|
+
const only = children[0];
|
|
204
|
+
if (only && rectsMatch(only.rect, box)) {
|
|
205
|
+
const onlyEl = byId.get(only.elementId);
|
|
206
|
+
if (!onlyEl)
|
|
207
|
+
break;
|
|
208
|
+
wrapped += 1 + only.wrapped;
|
|
209
|
+
wrappersFolded += 1;
|
|
210
|
+
inner = onlyEl;
|
|
211
|
+
children = only.children;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// `kind` is classification metadata only — it does not prune `children`.
|
|
218
|
+
// A card with a background distinct from its parent is a `leaf` in the
|
|
219
|
+
// sense that a digest/query consumer can treat it as one atomic visual
|
|
220
|
+
// unit, but `overlaps`/`siblingGaps`/`siblingUniformity` still need its
|
|
221
|
+
// real children (e.g. an absolutely-positioned badge nested inside a
|
|
222
|
+
// colored card) to be present in the tree to find them.
|
|
223
|
+
const leaf = isLeaf(inner, parent);
|
|
224
|
+
return {
|
|
225
|
+
elementId: inner.id,
|
|
226
|
+
tag: inner.tag,
|
|
227
|
+
kind: leaf ? 'leaf' : 'container',
|
|
228
|
+
flow: flowFromDisplay(inner.display, inner.flexDirection),
|
|
229
|
+
position: inner.position,
|
|
230
|
+
wrapped,
|
|
231
|
+
rect: inner.rect,
|
|
232
|
+
children,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const rootIds = elements.filter((el) => el.parentId === null).map((el) => el.id);
|
|
236
|
+
const rawRoots = buildChildren(rootIds);
|
|
237
|
+
const tree = rawRoots.map((raw) => build(raw, null));
|
|
238
|
+
return {
|
|
239
|
+
tree,
|
|
240
|
+
dropped: { svgInternal, invisible, wrappers: wrappersFolded },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nodesAtPoint = nodesAtPoint;
|
|
4
|
+
exports.nodesInRegion = nodesInRegion;
|
|
5
|
+
/**
|
|
6
|
+
* Point/region lookup over an already-built layout tree — the geometric
|
|
7
|
+
* half of `query`'s `at`/`within` verbs (Step 17). Pure, tree-shaped, no
|
|
8
|
+
* `ElementRecord`, same boundary rules as the rest of `src/analysis/`. This
|
|
9
|
+
* is the shared helper both verbs dispatch through in `query.ts`, so a point
|
|
10
|
+
* query and a region query use the exact same rect-arithmetic and traversal
|
|
11
|
+
* rather than two hand-rolled walks that could quietly diverge.
|
|
12
|
+
*
|
|
13
|
+
* Both functions identify nodes by rect, not by `selector` — the join key
|
|
14
|
+
* `digest/render.ts#findBySelector` is stuck with and the one Step 16 found
|
|
15
|
+
* is not globally unique on real pages (duplicate `nth-of-type` selectors
|
|
16
|
+
* under a repeated table structure). A point or region is geometry from the
|
|
17
|
+
* start, so there is no selector-collision risk here at all.
|
|
18
|
+
*/
|
|
19
|
+
const geometry_js_1 = require("./geometry.cjs");
|
|
20
|
+
/** Flattens the tree into every node at every depth, root-first, parent
|
|
21
|
+
* before children — same shape `distances.ts#flattenWithLineage` builds,
|
|
22
|
+
* reimplemented here since this module only needs the flat node list, not
|
|
23
|
+
* lineage. */
|
|
24
|
+
function flatten(tree) {
|
|
25
|
+
const out = [];
|
|
26
|
+
function visit(node) {
|
|
27
|
+
out.push(node);
|
|
28
|
+
for (const child of node.children)
|
|
29
|
+
visit(child);
|
|
30
|
+
}
|
|
31
|
+
for (const root of tree)
|
|
32
|
+
visit(root);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** True if the point `(x, y)` falls within `rect`, edges inclusive (a point
|
|
36
|
+
* exactly on a border counts as contained — the same "touching counts"
|
|
37
|
+
* convention `geometry.ts#contains` uses for rect-in-rect). */
|
|
38
|
+
function containsPoint(rect, x, y) {
|
|
39
|
+
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Every `LayoutNode` (at any depth) whose rect contains the point `(x, y)`,
|
|
43
|
+
* sorted most-specific first (ascending area — the smallest containing rect,
|
|
44
|
+
* typically a leaf, comes before its containers, all the way out to a page
|
|
45
|
+
* root). Empty array if the point is outside every node, including the page
|
|
46
|
+
* roots. Callers wanting a shorter "leaf plus a bit of context" list should
|
|
47
|
+
* slice the result themselves — this returns every ancestor rank, uncapped.
|
|
48
|
+
*/
|
|
49
|
+
function nodesAtPoint(tree, x, y) {
|
|
50
|
+
return flatten(tree)
|
|
51
|
+
.filter((node) => containsPoint(node.rect, x, y))
|
|
52
|
+
.sort((a, b) => (0, geometry_js_1.area)(a.rect) - (0, geometry_js_1.area)(b.rect));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Every `LayoutNode` (at any depth) whose rect intersects `region` —
|
|
56
|
+
* partial overlap counts, not just full containment (a region query is
|
|
57
|
+
* meant to catch things partially inside it too; see
|
|
58
|
+
* `geometry.ts#intersect`, which is the same primitive `overlap`/`query
|
|
59
|
+
* overlap` already use). Sorted by intersection area descending
|
|
60
|
+
* (most-overlapping first). Empty array if nothing overlaps.
|
|
61
|
+
*/
|
|
62
|
+
function nodesInRegion(tree, region) {
|
|
63
|
+
const matches = [];
|
|
64
|
+
for (const node of flatten(tree)) {
|
|
65
|
+
const overlap = (0, geometry_js_1.intersect)(node.rect, region);
|
|
66
|
+
if (overlap)
|
|
67
|
+
matches.push({ node, intersectionArea: (0, geometry_js_1.area)(overlap) });
|
|
68
|
+
}
|
|
69
|
+
matches.sort((a, b) => b.intersectionArea - a.intersectionArea);
|
|
70
|
+
return matches;
|
|
71
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LayoutNode, Rect } from "./types.cjs";
|
|
2
|
+
/**
|
|
3
|
+
* Every `LayoutNode` (at any depth) whose rect contains the point `(x, y)`,
|
|
4
|
+
* sorted most-specific first (ascending area — the smallest containing rect,
|
|
5
|
+
* typically a leaf, comes before its containers, all the way out to a page
|
|
6
|
+
* root). Empty array if the point is outside every node, including the page
|
|
7
|
+
* roots. Callers wanting a shorter "leaf plus a bit of context" list should
|
|
8
|
+
* slice the result themselves — this returns every ancestor rank, uncapped.
|
|
9
|
+
*/
|
|
10
|
+
export declare function nodesAtPoint(tree: LayoutNode[], x: number, y: number): LayoutNode[];
|
|
11
|
+
export interface RegionMatch {
|
|
12
|
+
node: LayoutNode;
|
|
13
|
+
/** Area (px²) of the overlap between `node.rect` and the queried region —
|
|
14
|
+
* what the result is sorted by (descending, most-overlapping first). */
|
|
15
|
+
intersectionArea: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Every `LayoutNode` (at any depth) whose rect intersects `region` —
|
|
19
|
+
* partial overlap counts, not just full containment (a region query is
|
|
20
|
+
* meant to catch things partially inside it too; see
|
|
21
|
+
* `geometry.ts#intersect`, which is the same primitive `overlap`/`query
|
|
22
|
+
* overlap` already use). Sorted by intersection area descending
|
|
23
|
+
* (most-overlapping first). Empty array if nothing overlaps.
|
|
24
|
+
*/
|
|
25
|
+
export declare function nodesInRegion(tree: LayoutNode[], region: Rect): RegionMatch[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LayoutNode, Rect } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Every `LayoutNode` (at any depth) whose rect contains the point `(x, y)`,
|
|
4
|
+
* sorted most-specific first (ascending area — the smallest containing rect,
|
|
5
|
+
* typically a leaf, comes before its containers, all the way out to a page
|
|
6
|
+
* root). Empty array if the point is outside every node, including the page
|
|
7
|
+
* roots. Callers wanting a shorter "leaf plus a bit of context" list should
|
|
8
|
+
* slice the result themselves — this returns every ancestor rank, uncapped.
|
|
9
|
+
*/
|
|
10
|
+
export declare function nodesAtPoint(tree: LayoutNode[], x: number, y: number): LayoutNode[];
|
|
11
|
+
export interface RegionMatch {
|
|
12
|
+
node: LayoutNode;
|
|
13
|
+
/** Area (px²) of the overlap between `node.rect` and the queried region —
|
|
14
|
+
* what the result is sorted by (descending, most-overlapping first). */
|
|
15
|
+
intersectionArea: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Every `LayoutNode` (at any depth) whose rect intersects `region` —
|
|
19
|
+
* partial overlap counts, not just full containment (a region query is
|
|
20
|
+
* meant to catch things partially inside it too; see
|
|
21
|
+
* `geometry.ts#intersect`, which is the same primitive `overlap`/`query
|
|
22
|
+
* overlap` already use). Sorted by intersection area descending
|
|
23
|
+
* (most-overlapping first). Empty array if nothing overlaps.
|
|
24
|
+
*/
|
|
25
|
+
export declare function nodesInRegion(tree: LayoutNode[], region: Rect): RegionMatch[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Point/region lookup over an already-built layout tree — the geometric
|
|
3
|
+
* half of `query`'s `at`/`within` verbs (Step 17). Pure, tree-shaped, no
|
|
4
|
+
* `ElementRecord`, same boundary rules as the rest of `src/analysis/`. This
|
|
5
|
+
* is the shared helper both verbs dispatch through in `query.ts`, so a point
|
|
6
|
+
* query and a region query use the exact same rect-arithmetic and traversal
|
|
7
|
+
* rather than two hand-rolled walks that could quietly diverge.
|
|
8
|
+
*
|
|
9
|
+
* Both functions identify nodes by rect, not by `selector` — the join key
|
|
10
|
+
* `digest/render.ts#findBySelector` is stuck with and the one Step 16 found
|
|
11
|
+
* is not globally unique on real pages (duplicate `nth-of-type` selectors
|
|
12
|
+
* under a repeated table structure). A point or region is geometry from the
|
|
13
|
+
* start, so there is no selector-collision risk here at all.
|
|
14
|
+
*/
|
|
15
|
+
import { area, intersect } from "./geometry.js";
|
|
16
|
+
/** Flattens the tree into every node at every depth, root-first, parent
|
|
17
|
+
* before children — same shape `distances.ts#flattenWithLineage` builds,
|
|
18
|
+
* reimplemented here since this module only needs the flat node list, not
|
|
19
|
+
* lineage. */
|
|
20
|
+
function flatten(tree) {
|
|
21
|
+
const out = [];
|
|
22
|
+
function visit(node) {
|
|
23
|
+
out.push(node);
|
|
24
|
+
for (const child of node.children)
|
|
25
|
+
visit(child);
|
|
26
|
+
}
|
|
27
|
+
for (const root of tree)
|
|
28
|
+
visit(root);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
/** True if the point `(x, y)` falls within `rect`, edges inclusive (a point
|
|
32
|
+
* exactly on a border counts as contained — the same "touching counts"
|
|
33
|
+
* convention `geometry.ts#contains` uses for rect-in-rect). */
|
|
34
|
+
function containsPoint(rect, x, y) {
|
|
35
|
+
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Every `LayoutNode` (at any depth) whose rect contains the point `(x, y)`,
|
|
39
|
+
* sorted most-specific first (ascending area — the smallest containing rect,
|
|
40
|
+
* typically a leaf, comes before its containers, all the way out to a page
|
|
41
|
+
* root). Empty array if the point is outside every node, including the page
|
|
42
|
+
* roots. Callers wanting a shorter "leaf plus a bit of context" list should
|
|
43
|
+
* slice the result themselves — this returns every ancestor rank, uncapped.
|
|
44
|
+
*/
|
|
45
|
+
export function nodesAtPoint(tree, x, y) {
|
|
46
|
+
return flatten(tree)
|
|
47
|
+
.filter((node) => containsPoint(node.rect, x, y))
|
|
48
|
+
.sort((a, b) => area(a.rect) - area(b.rect));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Every `LayoutNode` (at any depth) whose rect intersects `region` —
|
|
52
|
+
* partial overlap counts, not just full containment (a region query is
|
|
53
|
+
* meant to catch things partially inside it too; see
|
|
54
|
+
* `geometry.ts#intersect`, which is the same primitive `overlap`/`query
|
|
55
|
+
* overlap` already use). Sorted by intersection area descending
|
|
56
|
+
* (most-overlapping first). Empty array if nothing overlaps.
|
|
57
|
+
*/
|
|
58
|
+
export function nodesInRegion(tree, region) {
|
|
59
|
+
const matches = [];
|
|
60
|
+
for (const node of flatten(tree)) {
|
|
61
|
+
const overlap = intersect(node.rect, region);
|
|
62
|
+
if (overlap)
|
|
63
|
+
matches.push({ node, intersectionArea: area(overlap) });
|
|
64
|
+
}
|
|
65
|
+
matches.sort((a, b) => b.intersectionArea - a.intersectionArea);
|
|
66
|
+
return matches;
|
|
67
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Geometry primitives. Deliberately structural and tree-unaware: every
|
|
4
|
+
* function in `geometry.ts` takes plain rects, so the collector, the digest,
|
|
5
|
+
* and the diff all share one arithmetic implementation rather than three.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geometry primitives. Deliberately structural and tree-unaware: every
|
|
3
|
+
* function in `geometry.ts` takes plain rects, so the collector, the digest,
|
|
4
|
+
* and the diff all share one arithmetic implementation rather than three.
|
|
5
|
+
*/
|
|
6
|
+
/** A CSS-pixel rectangle in viewport coordinates. */
|
|
7
|
+
export interface Rect {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}
|
|
13
|
+
/** The four sides of a box property (`margin`, `padding`, `border`). */
|
|
14
|
+
export interface BoxSides {
|
|
15
|
+
top: number;
|
|
16
|
+
right: number;
|
|
17
|
+
bottom: number;
|
|
18
|
+
left: number;
|
|
19
|
+
}
|
|
20
|
+
export type Flow = 'row' | 'column' | 'grid' | 'none';
|
|
21
|
+
export type LayoutNodeKind = 'leaf' | 'container';
|
|
22
|
+
/**
|
|
23
|
+
* One node in the layout tree `buildLayoutTree` produces. `elementId`/`tag`/`rect`
|
|
24
|
+
* identify the *representative* underlying element — for a
|
|
25
|
+
* folded wrapper chain (see `wrapped`) that's the innermost element, since
|
|
26
|
+
* it carries the meaningful content (text, replaced-element type, or a
|
|
27
|
+
* distinct border/background); the outer wrapper's box is what proved the
|
|
28
|
+
* fold was safe (its content box matched), but it has nothing worth
|
|
29
|
+
* reporting on its own once absorbed.
|
|
30
|
+
*/
|
|
31
|
+
export interface LayoutNode {
|
|
32
|
+
elementId: string;
|
|
33
|
+
tag: string;
|
|
34
|
+
kind: LayoutNodeKind;
|
|
35
|
+
flow: Flow;
|
|
36
|
+
/** Computed CSS `position` of the representative element (`static`,
|
|
37
|
+
* `relative`, `absolute`, `fixed`, `sticky`) — carried through from
|
|
38
|
+
* `ElementRecord.position` so `Analysis`-level geometry (`overlaps`,
|
|
39
|
+
* `proximities`) can exclude `fixed`/`sticky` chrome the same way
|
|
40
|
+
* `checks/overflow.ts#UNCONTAINED_POSITIONS` already does at the
|
|
41
|
+
* `ElementRecord` level. */
|
|
42
|
+
position: string;
|
|
43
|
+
/** Count of wrapper levels folded into this node (0 if none). */
|
|
44
|
+
wrapped: number;
|
|
45
|
+
rect: Rect;
|
|
46
|
+
children: LayoutNode[];
|
|
47
|
+
}
|
|
48
|
+
export interface LayoutTreeDropped {
|
|
49
|
+
svgInternal: number;
|
|
50
|
+
invisible: number;
|
|
51
|
+
wrappers: number;
|
|
52
|
+
}
|
|
53
|
+
export interface LayoutTreeResult {
|
|
54
|
+
tree: LayoutNode[];
|
|
55
|
+
dropped: LayoutTreeDropped;
|
|
56
|
+
}
|
|
57
|
+
export interface SiblingGapPair {
|
|
58
|
+
a: string;
|
|
59
|
+
b: string;
|
|
60
|
+
gap: number;
|
|
61
|
+
}
|
|
62
|
+
export interface SiblingGapGroup {
|
|
63
|
+
containerId: string;
|
|
64
|
+
axis: 'x' | 'y';
|
|
65
|
+
pairs: SiblingGapPair[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `SiblingGapGroup` plus base-unit detection over the group's own gap
|
|
69
|
+
* values (`pairs[].gap`) — `null` unit means no candidate explained enough
|
|
70
|
+
* of the gaps (see `cluster.ts#detectBaseUnit`). This is what `Analysis.gaps`
|
|
71
|
+
* actually holds: extending `SiblingGapGroup` rather than a parallel array
|
|
72
|
+
* keyed by `containerId`, since every consumer that already has a gap group
|
|
73
|
+
* (the digest renderer, the future spacing check) needs the group's own
|
|
74
|
+
* spacing stats in the same place it reads `pairs`, and a parallel array
|
|
75
|
+
* would just be a second lookup by the same key for no benefit — see
|
|
76
|
+
* LOG.md step 9. `siblingGaps()` in `neighbors.ts` itself still returns the
|
|
77
|
+
* plain `SiblingGapGroup` (it has no candidates/tolerance to detect a base
|
|
78
|
+
* unit with); `analyze()` is what adds these fields on top.
|
|
79
|
+
*/
|
|
80
|
+
export interface SpacingGapGroup extends SiblingGapGroup {
|
|
81
|
+
baseUnit: number | null;
|
|
82
|
+
onGridShare: number;
|
|
83
|
+
offGrid: number[];
|
|
84
|
+
distinct: Record<string, number>;
|
|
85
|
+
}
|
|
86
|
+
export interface OverlapPair {
|
|
87
|
+
a: string;
|
|
88
|
+
b: string;
|
|
89
|
+
area: number;
|
|
90
|
+
rect: Rect;
|
|
91
|
+
}
|
|
92
|
+
export interface ProximityPair {
|
|
93
|
+
a: string;
|
|
94
|
+
b: string;
|
|
95
|
+
edge: number;
|
|
96
|
+
dx: number;
|
|
97
|
+
dy: number;
|
|
98
|
+
}
|
|
99
|
+
export interface EdgeClusterSet {
|
|
100
|
+
left: Array<{
|
|
101
|
+
value: number;
|
|
102
|
+
count: number;
|
|
103
|
+
members: number[];
|
|
104
|
+
}>;
|
|
105
|
+
right: Array<{
|
|
106
|
+
value: number;
|
|
107
|
+
count: number;
|
|
108
|
+
members: number[];
|
|
109
|
+
}>;
|
|
110
|
+
top: Array<{
|
|
111
|
+
value: number;
|
|
112
|
+
count: number;
|
|
113
|
+
members: number[];
|
|
114
|
+
}>;
|
|
115
|
+
}
|
|
116
|
+
export interface NearMiss {
|
|
117
|
+
id: string;
|
|
118
|
+
axis: 'left' | 'right' | 'top';
|
|
119
|
+
actual: number;
|
|
120
|
+
nearestClusterValue: number;
|
|
121
|
+
delta: number;
|
|
122
|
+
/** Count of `nearestClusterValue`'s members that are container-scoped to
|
|
123
|
+
* this node — nearest common ancestor no more than `index.ts`'s
|
|
124
|
+
* `MAX_ANCESTOR_DISTANCE` combined steps away — not the cluster's raw
|
|
125
|
+
* page-wide `count` in `Analysis.edges`. `checkAlignment`'s
|
|
126
|
+
* `minClusterSize` gate reads this field instead of looking up
|
|
127
|
+
* `Analysis.edges`, so an unrelated page-wide cluster (large `count`,
|
|
128
|
+
* but zero members actually related to this node) can't pass the gate
|
|
129
|
+
* on size alone. See LOG.md Step 11b. */
|
|
130
|
+
scopedMemberCount: number;
|
|
131
|
+
}
|
|
132
|
+
export interface UniformityDeviant {
|
|
133
|
+
id: string;
|
|
134
|
+
reason: string;
|
|
135
|
+
}
|
|
136
|
+
export interface SiblingUniformityGroup {
|
|
137
|
+
containerId: string;
|
|
138
|
+
children: number;
|
|
139
|
+
medianWidth: number;
|
|
140
|
+
medianHeight: number;
|
|
141
|
+
medianGap: number;
|
|
142
|
+
deviants: UniformityDeviant[];
|
|
143
|
+
}
|