@ubio/webvision 1.2.6 → 2.0.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.
Files changed (44) hide show
  1. package/build/page.mjs +723 -355
  2. package/out/page/counter.d.ts +6 -0
  3. package/out/page/counter.js +13 -0
  4. package/out/page/counter.js.map +1 -0
  5. package/out/page/dom.d.ts +21 -0
  6. package/out/page/dom.js +172 -0
  7. package/out/page/dom.js.map +1 -0
  8. package/out/page/index.d.ts +8 -4
  9. package/out/page/index.js +8 -4
  10. package/out/page/index.js.map +1 -1
  11. package/out/page/overlay.d.ts +3 -0
  12. package/out/page/overlay.js +75 -0
  13. package/out/page/overlay.js.map +1 -0
  14. package/out/page/parser.d.ts +66 -0
  15. package/out/page/parser.js +249 -0
  16. package/out/page/parser.js.map +1 -0
  17. package/out/page/probe.d.ts +5 -0
  18. package/out/page/probe.js +36 -0
  19. package/out/page/probe.js.map +1 -0
  20. package/out/page/render.d.ts +12 -8
  21. package/out/page/render.js +73 -38
  22. package/out/page/render.js.map +1 -1
  23. package/out/page/traverse.d.ts +5 -0
  24. package/out/page/traverse.js +7 -0
  25. package/out/page/traverse.js.map +1 -0
  26. package/out/page/util.d.ts +6 -0
  27. package/out/page/util.js +28 -0
  28. package/out/page/util.js.map +1 -0
  29. package/out/page/view.d.ts +30 -0
  30. package/out/page/view.js +103 -0
  31. package/out/page/view.js.map +1 -0
  32. package/package.json +6 -3
  33. package/out/page/highlight.d.ts +0 -5
  34. package/out/page/highlight.js +0 -72
  35. package/out/page/highlight.js.map +0 -1
  36. package/out/page/html.d.ts +0 -4
  37. package/out/page/html.js +0 -42
  38. package/out/page/html.js.map +0 -1
  39. package/out/page/snapshot.d.ts +0 -69
  40. package/out/page/snapshot.js +0 -191
  41. package/out/page/snapshot.js.map +0 -1
  42. package/out/page/utils.d.ts +0 -19
  43. package/out/page/utils.js +0 -68
  44. package/out/page/utils.js.map +0 -1
package/build/page.mjs CHANGED
@@ -1,243 +1,273 @@
1
- // src/page/highlight.ts
2
- function highlightSnapshot(snapshot, refMap) {
3
- removeHighlight();
4
- const container = getHighlightContainer();
5
- container.innerHTML = "";
6
- highlightRecursive(snapshot, refMap, container);
7
- }
8
- function highlightRecursive(snapshot, refMap, container) {
9
- highlightEl(snapshot, refMap, container);
10
- for (const child of snapshot.children ?? []) {
11
- highlightRecursive(child, refMap, container);
12
- }
13
- }
14
- function highlightEl(snapshot, refMap, container) {
15
- const isContainerEl = !snapshot.leaf && snapshot.children?.every((child) => child.nodeType === "element");
16
- if (isContainerEl) {
17
- return;
1
+ // src/page/counter.ts
2
+ var Counter = class {
3
+ constructor(value = 0) {
4
+ this.value = value;
18
5
  }
19
- const node = refMap.get(snapshot.ref);
20
- if (!(node instanceof Element)) {
21
- return;
6
+ next() {
7
+ this.value += 1;
8
+ return this.value;
22
9
  }
23
- const color = getColor(snapshot.ref);
24
- const rect = node.getBoundingClientRect();
25
- const overlay = document.createElement("div");
26
- container.appendChild(overlay);
27
- overlay.style.position = "absolute";
28
- overlay.style.top = `${rect.top}px`;
29
- overlay.style.left = `${rect.left}px`;
30
- overlay.style.width = `${rect.width}px`;
31
- overlay.style.height = `${rect.height}px`;
32
- overlay.style.border = `2px solid ${color}`;
33
- const label = document.createElement("div");
34
- overlay.appendChild(label);
35
- label.style.position = "absolute";
36
- label.style.bottom = `100%`;
37
- label.style.left = `0`;
38
- label.style.backgroundColor = color;
39
- label.style.color = "white";
40
- label.style.fontSize = "10px";
41
- label.style.fontFamily = "monospace";
42
- label.style.fontWeight = "normal";
43
- label.style.fontStyle = "normal";
44
- label.style.opacity = "0.8";
45
- label.style.padding = "0 2px";
46
- label.style.transform = "translateY(50%)";
47
- label.textContent = String(snapshot.ref);
10
+ current() {
11
+ return this.value;
12
+ }
13
+ };
14
+
15
+ // src/page/dom.ts
16
+ var ORIGINAL_STYLE_SYMBOL = Symbol("vx:originalStyle");
17
+ var INTERACTIVE_TAGS = ["a", "button", "input", "textarea", "select", "label"];
18
+ var INTERACTIVE_ROLES = [
19
+ "button",
20
+ "link",
21
+ "checkbox",
22
+ "radio",
23
+ "textbox",
24
+ "combobox",
25
+ "listbox",
26
+ "menu",
27
+ "menuitem",
28
+ "menuitemcheckbox",
29
+ "menuitemradio",
30
+ "option",
31
+ "optgroup",
32
+ "progressbar",
33
+ "scrollbar",
34
+ "slider",
35
+ "spinbutton",
36
+ "switch",
37
+ "tab",
38
+ "tablist",
39
+ "timer",
40
+ "toolbar"
41
+ ];
42
+ function isHidden(el) {
43
+ const style = getComputedStyle(el);
44
+ if (style.display === "none") {
45
+ return true;
46
+ }
47
+ if (style.visibility === "hidden") {
48
+ return true;
49
+ }
50
+ if (style.opacity === "0") {
51
+ return true;
52
+ }
53
+ return false;
48
54
  }
49
- function removeHighlight() {
50
- const container = getHighlightContainer();
51
- document.documentElement.removeChild(container);
55
+ function getNormalizedText(el) {
56
+ return normalizeText(el.textContent ?? "");
52
57
  }
53
- function getHighlightContainer() {
54
- let container = document.querySelector("#webvision-highlight");
55
- if (!container) {
56
- container = document.createElement("div");
57
- container.id = "webvision-highlight";
58
- container.style.position = "absolute";
59
- container.style.pointerEvents = "none";
60
- container.style.top = "0";
61
- container.style.left = "0";
62
- container.style.width = "100%";
63
- container.style.height = "100%";
64
- container.style.zIndex = "2147483646";
65
- document.documentElement.appendChild(container);
58
+ function normalizeText(str) {
59
+ return str.replace(/\p{Cf}/gu, " ").replace(/\s+/g, " ").trim();
60
+ }
61
+ function truncateAttrValue(value, limit = 100) {
62
+ if (value.match(/^(javascript:)?void/)) {
63
+ return "";
66
64
  }
67
- return container;
65
+ if (value.startsWith("data:")) {
66
+ return "";
67
+ }
68
+ if (value.length > limit) {
69
+ return value.slice(0, limit) + "\u2026";
70
+ }
71
+ return value;
68
72
  }
69
- function getColor(index) {
70
- const hue = index * 120 * 0.382 % 360;
71
- return `hsl(${hue}, 85%, 50%)`;
73
+ function escapeAttribute(value) {
74
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\r?\n/g, " ");
72
75
  }
73
-
74
- // src/page/html.ts
75
- function captureAncestorsHtml(el, includeText = true) {
76
- const path = [];
77
- let current = el;
76
+ function containsSelector(el, selector) {
77
+ return el.matches(selector) || !!el.querySelector(selector);
78
+ }
79
+ function getOffsetTop(node) {
80
+ let y = 0;
81
+ let current = node;
78
82
  while (current) {
79
- path.push(captureHtmlLine(current));
80
- current = current.parentElement;
81
- }
82
- if (includeText) {
83
- const anyEl = el;
84
- path.unshift(anyEl.value || anyEl.innerText || anyEl.textContent || "");
83
+ y += current.offsetTop ?? 0;
84
+ current = current.offsetParent;
85
85
  }
86
- return path.reverse().filter(Boolean);
86
+ return y;
87
87
  }
88
- function captureHtmlLine(el) {
89
- const html = [];
90
- if (el instanceof HTMLElement) {
91
- html.push(`${el.tagName.toLowerCase()}`);
92
- for (const attr of el.attributes) {
93
- html.push(`${attr.name}="${attr.value}"`);
94
- }
95
- return `<${html.join(" ")}>`;
88
+ function hasVisibleArea(element) {
89
+ const rect = element.getBoundingClientRect();
90
+ const area = rect.width * rect.height;
91
+ return area > 64;
92
+ }
93
+ function isDeepHidden(element) {
94
+ if (isHidden(element)) {
95
+ return true;
96
96
  }
97
- if (el instanceof Text) {
98
- return el.textContent;
97
+ if (!hasVisibleArea(element)) {
98
+ return [...element.children].every((el) => isDeepHidden(el));
99
99
  }
100
- return "";
100
+ return false;
101
101
  }
102
- function captureHtml(el) {
103
- if (el instanceof HTMLElement) {
104
- const anyEl = el;
105
- return [
106
- captureHtmlLine(el),
107
- anyEl.value || anyEl.innerText || anyEl.textContent || "",
108
- `</${el.tagName.toLowerCase()}>`
109
- ].filter(Boolean).join("");
102
+ function isInteractive(el) {
103
+ const htmlEl = el;
104
+ if (INTERACTIVE_TAGS.includes(el.tagName.toLowerCase())) {
105
+ return true;
110
106
  }
111
- if (el instanceof Text) {
112
- return el.textContent;
107
+ const tabindex = htmlEl.getAttribute("tabindex");
108
+ if (tabindex && parseInt(tabindex) >= 0) {
109
+ return true;
110
+ }
111
+ const role = htmlEl.getAttribute("role") ?? htmlEl.getAttribute("aria-role") ?? "";
112
+ if (INTERACTIVE_ROLES.includes(role)) {
113
+ return true;
113
114
  }
114
- return "";
115
+ if (htmlEl.onclick != null) {
116
+ return true;
117
+ }
118
+ return false;
115
119
  }
116
-
117
- // src/page/render.ts
118
- function renderSnapshot(snapshot, options = {}) {
119
- const opts = {
120
- depth: 0,
121
- includeRef: false,
122
- includeClassList: false,
123
- ...options
120
+ function getViewportSize() {
121
+ return {
122
+ width: window.innerWidth,
123
+ height: window.innerHeight
124
124
  };
125
- if (opts.maxHeight && snapshot.rect.y > opts.maxHeight) {
126
- return "";
125
+ }
126
+ function isRectInViewport(rect, viewport) {
127
+ return rect.left < viewport.width && rect.right > 0 && rect.top < viewport.height && rect.bottom > 0;
128
+ }
129
+ function makeOverlaysOpaque(el) {
130
+ if (!el[ORIGINAL_STYLE_SYMBOL]) {
131
+ el[ORIGINAL_STYLE_SYMBOL] = el.getAttribute("style");
127
132
  }
128
- const buffer = [
129
- renderLine(snapshot, opts)
130
- ];
131
- for (const child of snapshot.children ?? []) {
132
- const childSnapshot = renderSnapshot(child, {
133
- ...opts,
134
- depth: opts.depth + 1
135
- });
136
- if (childSnapshot) {
137
- buffer.push(childSnapshot);
138
- }
133
+ const rect = el.getBoundingClientRect();
134
+ if (rect.width < 500 || rect.height < 500) {
135
+ return;
139
136
  }
140
- return buffer.join("\n");
137
+ const style = getComputedStyle(el);
138
+ if (style.pointerEvents === "none") {
139
+ return;
140
+ }
141
+ el.style.setProperty("animation", "none", "important");
142
+ el.style.setProperty("transition", "none", "important");
143
+ el.style.setProperty("backdrop-filter", "none", "important");
144
+ el.style.setProperty("mix-blend-mode", "normal", "important");
145
+ el.style.setProperty("opacity", "1", "important");
146
+ el.style.setProperty("filter", "none", "important");
147
+ const bg = style.backgroundColor;
148
+ el.style.setProperty("background-color", removeAlpha(bg));
141
149
  }
142
- function renderLine(snapshot, options) {
143
- const indent = " ".repeat(options.depth);
144
- const components = [indent];
145
- if (snapshot.nodeType === "text") {
146
- return [indent, snapshot.textContent].filter(Boolean).join(" ");
150
+ function removeAlpha(color) {
151
+ const rgba = color.match(/^rgba\((.*)\)$/);
152
+ if (!rgba) {
153
+ return color;
147
154
  }
148
- components.push(snapshot.tagName ?? "");
149
- if (options.includeRef) {
150
- components.push(`[ref=${snapshot.ref}]`);
155
+ const [r, g, b, a] = rgba[1].split(",").map(parseFloat);
156
+ if (a > 0 && a < 1) {
157
+ return `rgba(${r},${g},${b},1)`;
151
158
  }
152
- if (options.includeClassList) {
153
- for (const className of snapshot.classList ?? []) {
154
- components.push(`.${className}`);
155
- }
156
- }
157
- if (snapshot.src) {
158
- components.push(`(${snapshot.src})`);
159
+ return color;
160
+ }
161
+ function isRandomIdentifier(str) {
162
+ const words = str.split(/[_-]/g);
163
+ return !words.some((w) => isWordLike(w)) || words.some((w) => isRandomString(w));
164
+ }
165
+ function isWordLike(str) {
166
+ if (str.length <= 2) {
167
+ return false;
159
168
  }
160
- if (snapshot.href) {
161
- components.push(`(${snapshot.href})`);
169
+ if (/[^eyuioa]{4,}/.test(str)) {
170
+ return false;
162
171
  }
163
- if (snapshot.textContent) {
164
- components.push(" " + snapshot.textContent);
172
+ if (/[eyuioa]{4,}/.test(str)) {
173
+ return false;
165
174
  }
166
- return components.filter(Boolean).join("");
175
+ return true;
167
176
  }
168
-
169
- // src/page/utils.ts
170
- function isHidden(element, options = {}) {
171
- const {
172
- checkOpacity = true,
173
- checkVisibility = true,
174
- checkTransform = true
175
- } = options;
176
- const style = getComputedStyle(element);
177
- const opacity = Number(style.opacity);
178
- const display = style.display;
179
- const visibility = style.visibility;
180
- const transform = style.transform;
181
- if (display === "none" || checkOpacity && opacity === 0 || checkVisibility && visibility === "hidden" || checkTransform && transform.includes("scale(0)")) {
177
+ function isRandomString(str) {
178
+ if (/^[0-9a-f]+$/i.test(str) && /[0-9]/.test(str)) {
182
179
  return true;
183
180
  }
184
- if (!element.checkVisibility()) {
181
+ if (/[0-9][^0-9]+[0-9]/.test(str)) {
185
182
  return true;
186
183
  }
187
184
  return false;
188
185
  }
189
- function hasVisibleArea(element) {
190
- const rect = element.getBoundingClientRect();
191
- const area = rect.width * rect.height;
192
- return area > 100;
193
- }
194
- function deepIsHidden(element, options = {}) {
195
- if (isHidden(element, options)) {
196
- return true;
186
+ function fixZIndex(el) {
187
+ const style = getComputedStyle(el);
188
+ if (Number(style.zIndex) > 2147483600) {
189
+ el.style.setProperty("z-index", "2147483600", "important");
197
190
  }
198
- if (!hasVisibleArea(element)) {
199
- return [...element.children].every((el) => deepIsHidden(el, options));
200
- }
201
- return false;
202
- }
203
- function normalizeText(str) {
204
- return str.replace(/\p{Cf}/gu, " ").replace(/\s+/g, " ").trim();
205
191
  }
206
- function containsSelector(el, selector) {
207
- return el.matches(selector) || !!el.querySelector(selector);
208
- }
209
- function isRecursiveInline(el, ignoreTags = []) {
210
- for (const child of el.childNodes) {
211
- if (child instanceof Element) {
212
- if (ignoreTags.includes(child.tagName.toLowerCase())) {
213
- return false;
192
+
193
+ // src/page/probe.ts
194
+ function probeViewport(gridSize = 32) {
195
+ const { width, height } = getViewportSize();
196
+ const elements = [];
197
+ for (let y = gridSize / 2; y < height; y += gridSize) {
198
+ for (let x = gridSize / 2; x < width; x += gridSize) {
199
+ const element = document.elementFromPoint(x, y);
200
+ if (element && !elements.includes(element)) {
201
+ elements.push(element);
214
202
  }
215
- const display = getComputedStyle(child).display;
216
- const inline = display === "inline" || display === "inline-block";
217
- if (!inline) {
218
- return false;
203
+ }
204
+ }
205
+ return elements;
206
+ }
207
+ function checkOccluded(element, gridSize = 8) {
208
+ let total = 0;
209
+ let hits = 0;
210
+ const { top, left, width, height } = element.getBoundingClientRect();
211
+ for (let x = left + gridSize / 2; x < left + width; x += gridSize) {
212
+ for (let y = top + gridSize / 2; y < top + height; y += gridSize) {
213
+ total++;
214
+ const el = document.elementFromPoint(x, y);
215
+ if (!el) {
216
+ continue;
219
217
  }
220
- if (!isRecursiveInline(child, ignoreTags)) {
221
- return false;
218
+ if (element.contains(el) || el.contains(element)) {
219
+ hits++;
222
220
  }
223
221
  }
224
222
  }
225
- return true;
223
+ return hits / total < 0.5;
224
+ }
225
+
226
+ // src/page/util.ts
227
+ function findFrameTreeByRef(frameTrees, ref) {
228
+ for (const frameTree of frameTrees) {
229
+ if (ref >= frameTree.refRange[0] && ref <= frameTree.refRange[1]) {
230
+ return frameTree;
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+ function findParentFrameTree(frameTrees, frameTree) {
236
+ const { iframeRef } = frameTree;
237
+ if (iframeRef == null) {
238
+ return null;
239
+ }
240
+ return findFrameTreeByRef(frameTrees, iframeRef);
241
+ }
242
+ function checkFrameOmitted(frameTrees, frameTree) {
243
+ const parentFrameTree = findParentFrameTree(frameTrees, frameTree);
244
+ return parentFrameTree?.isOmitted ?? false;
245
+ }
246
+ function isContainerNode(vxNode) {
247
+ const children = vxNode.children ?? [];
248
+ const hasTextChildren = children.some((child) => !child.tagName);
249
+ return children.length > 0 && !hasTextChildren;
250
+ }
251
+ function isObjectEmpty(obj) {
252
+ return !Object.values(obj).some((value) => !!value);
226
253
  }
227
254
 
228
- // src/page/snapshot.ts
229
- var DEFAULT_SKIP_TAGS = ["svg", "script", "noscript", "style", "link", "meta"];
230
- var DEFAULT_SEMANTIC_TAGS = [
255
+ // src/page/parser.ts
256
+ var VX_NODE_SYMBOL = Symbol("vx:node");
257
+ var VX_IGNORE_SYMBOL = Symbol("vx:ignore");
258
+ var VX_IGNORE_TAGS = ["svg", "script", "noscript", "style", "link", "meta"];
259
+ var VX_LABEL_ATTRS = ["title", "alt", "placeholder", "aria-label"];
260
+ var VX_VALUE_ATTRS = ["value", "checked", "selected", "disabled", "readonly"];
261
+ var VX_SRC_ATTRS = ["src", "href"];
262
+ var VX_TAG_PREFERENCE = [
231
263
  "a",
264
+ "iframe",
265
+ "input",
232
266
  "button",
267
+ "select",
268
+ "textarea",
269
+ "img",
233
270
  "label",
234
- "section",
235
- "article",
236
- "main",
237
- "header",
238
- "footer",
239
- "nav",
240
- "aside",
241
271
  "h1",
242
272
  "h2",
243
273
  "h3",
@@ -250,6 +280,20 @@ var DEFAULT_SEMANTIC_TAGS = [
250
280
  "dl",
251
281
  "dt",
252
282
  "dd",
283
+ "section",
284
+ "article",
285
+ "main",
286
+ "header",
287
+ "footer",
288
+ "nav",
289
+ "aside",
290
+ "form",
291
+ "input",
292
+ "textarea",
293
+ "select",
294
+ "option",
295
+ "fieldset",
296
+ "legend",
253
297
  "p",
254
298
  "pre",
255
299
  "code",
@@ -262,204 +306,528 @@ var DEFAULT_SEMANTIC_TAGS = [
262
306
  "tr",
263
307
  "td",
264
308
  "th",
265
- "form",
266
- "input",
267
- "textarea",
268
- "select",
269
- "option",
270
- "fieldset",
271
- "legend",
272
309
  "strong",
273
310
  "em",
274
311
  "sub",
275
312
  "sup"
276
313
  ];
277
- function createSnapshot(root, options = {}) {
278
- document.querySelectorAll("script, noscript, style").forEach((el) => {
279
- el.style.display = "none";
280
- });
281
- const opts = {
282
- startId: 0,
283
- skipHidden: true,
284
- skipEmptyText: true,
285
- skipImages: false,
286
- skipIframes: false,
287
- skipTags: DEFAULT_SKIP_TAGS,
288
- tagPreference: DEFAULT_SEMANTIC_TAGS,
289
- collapseInline: true,
290
- ...options
291
- };
292
- const counter = new Counter(opts.startId);
293
- const refMap = /* @__PURE__ */ new Map();
294
- const tree = new SnapshotTree(root, null, counter, opts);
295
- tree.fillMap(refMap);
296
- return {
297
- refMap,
298
- snapshot: tree.toJson(),
299
- maxId: counter.value
300
- };
301
- }
302
- var SnapshotTree = class _SnapshotTree {
303
- constructor(node, parent, counter, options) {
304
- this.node = node;
305
- this.parent = parent;
306
- this.counter = counter;
314
+ var VX_KEEP_SELECTOR = "a, button, input, textarea, select, label, iframe";
315
+ var VxTreeParser = class {
316
+ constructor(options = {}) {
307
317
  this.options = options;
308
- this.children = [];
309
- this.ref = this.counter.next();
310
- this.classList = [...this.element?.classList ?? []];
311
- if (this.element) {
312
- this.parseTree(this.element, this.getAcceptedChildren(this.element));
318
+ this.probeElements = [];
319
+ this.domRefMap = /* @__PURE__ */ new Map();
320
+ const { startRef = 0 } = options;
321
+ this.viewport = getViewportSize();
322
+ this.counter = new Counter(startRef);
323
+ if (options.probeViewport) {
324
+ this.probeElements = probeViewport(8);
313
325
  }
326
+ const vxRoot = this.parseDocument();
327
+ this.refRange = [startRef, this.counter.current()];
328
+ this.vxNodes = this.pruneRecursive(vxRoot);
314
329
  }
315
- get element() {
316
- return this.node instanceof Element ? this.node : null;
317
- }
318
- get depth() {
319
- return this.parent ? this.parent.depth + 1 : 0;
330
+ parseDocument() {
331
+ return this.parseNode(document.documentElement, null);
320
332
  }
321
- get leaf() {
322
- return this.children.length === 0;
333
+ getNodes() {
334
+ return this.vxNodes;
323
335
  }
324
- get tagName() {
325
- return this.node instanceof Element ? this.node.tagName.toLowerCase() : void 0;
336
+ getRefRange() {
337
+ return this.refRange;
326
338
  }
327
- get href() {
328
- return this.node instanceof HTMLAnchorElement ? this.node.href : void 0;
339
+ getDomMap() {
340
+ return this.domRefMap;
329
341
  }
330
- get src() {
331
- return this.node.src ?? void 0;
342
+ parseNode(node, parent) {
343
+ if (node[VX_IGNORE_SYMBOL]) {
344
+ return null;
345
+ }
346
+ if (node instanceof Text) {
347
+ const textContent = normalizeText(node.textContent ?? "");
348
+ if (textContent) {
349
+ return this.makeNode(node, {
350
+ textContent,
351
+ hasVisibleArea: parent?.hasVisibleArea,
352
+ isOutsideViewport: parent?.isOutsideViewport,
353
+ isProbeHit: parent?.isProbeHit
354
+ });
355
+ }
356
+ return null;
357
+ }
358
+ if (node instanceof Element) {
359
+ if (this.options.opaqueOverlays) {
360
+ makeOverlaysOpaque(node);
361
+ }
362
+ fixZIndex(node);
363
+ return this.parseElement(node);
364
+ }
365
+ return null;
332
366
  }
333
- get clientRect() {
334
- if (this.node instanceof Element) {
335
- return this.node.getBoundingClientRect();
367
+ parseElement(el) {
368
+ if (VX_IGNORE_TAGS.includes(el.tagName.toLowerCase())) {
369
+ return null;
336
370
  }
337
- const range = document.createRange();
338
- range.selectNodeContents(this.node);
339
- return range.getBoundingClientRect();
371
+ if (isHidden(el)) {
372
+ return null;
373
+ }
374
+ const rect = el.getBoundingClientRect();
375
+ const id = el.getAttribute("id") ?? "";
376
+ const vxNode = this.makeNode(el, {
377
+ tagName: el.tagName.toLowerCase(),
378
+ id: isRandomIdentifier(id) ? void 0 : id,
379
+ classList: Array.from(el.classList).filter((cls) => !isRandomIdentifier(cls)).slice(0, 4),
380
+ labelAttrs: this.collectLabelAttrs(el),
381
+ valueAttrs: this.collectValueAttrs(el),
382
+ srcAttrs: this.collectSrcAttrs(el),
383
+ hasVisibleArea: hasVisibleArea(el),
384
+ isInteractive: isInteractive(el),
385
+ isOutsideViewport: !isRectInViewport(rect, this.viewport),
386
+ isProbeHit: this.isProbeHit(el),
387
+ isKept: el.matches(VX_KEEP_SELECTOR)
388
+ });
389
+ const children = [...el.childNodes].map((child) => this.parseNode(child, vxNode)).filter((_) => _ != null);
390
+ if (children.length === 1 && !children[0]?.tagName) {
391
+ vxNode.textContent = normalizeText(children[0].textContent ?? "");
392
+ } else {
393
+ vxNode.children = children;
394
+ }
395
+ return vxNode;
396
+ }
397
+ makeNode(node, spec) {
398
+ const ref = this.counter.next();
399
+ const vxNode = {
400
+ ...spec,
401
+ ref
402
+ };
403
+ this.domRefMap.set(ref, node);
404
+ node[VX_NODE_SYMBOL] = vxNode;
405
+ return vxNode;
340
406
  }
341
- parseTree(el, childNodes) {
342
- this.children = [];
343
- if (childNodes.length === 1) {
344
- return this.collapseWrapper(el, childNodes[0]);
407
+ pruneRecursive(vxNode) {
408
+ if (!vxNode) {
409
+ return [];
345
410
  }
346
- if (this.options.collapseInline && isRecursiveInline(el, this.options.tagPreference)) {
347
- return;
411
+ const children = vxNode.children ?? [];
412
+ const newChildren = children.flatMap((child) => this.pruneRecursive(child));
413
+ if (this.shouldOmit(vxNode)) {
414
+ return newChildren;
348
415
  }
349
- for (const childNode of childNodes) {
350
- const snapshot = new _SnapshotTree(childNode, this, this.counter, this.options);
351
- this.children.push(snapshot);
416
+ vxNode.children = newChildren;
417
+ if (newChildren.length === 1) {
418
+ const child = newChildren[0];
419
+ return this.collapseSingleChild(vxNode, child);
352
420
  }
421
+ return [vxNode];
422
+ }
423
+ collapseSingleChild(parent, child) {
424
+ const preferParent = this.collapsePreferParent(parent, child);
425
+ const merged = preferParent ? this.collapseMerge(parent, child) : this.collapseMerge(child, parent);
426
+ return this.pruneRecursive({
427
+ ...merged,
428
+ children: child.children,
429
+ textContent: child.textContent
430
+ });
353
431
  }
354
- /**
355
- * Collapses an element with only one visible child into one.
356
- *
357
- * Wrapper element is preferred if it's a link or a button,
358
- * in other cases child element is preferred.
359
- */
360
- collapseWrapper(el, child) {
361
- if (child instanceof Text) {
362
- this.textContent = normalizeText(child.textContent ?? "");
363
- return;
432
+ collapsePreferParent(parent, child) {
433
+ const parentRank = VX_TAG_PREFERENCE.indexOf(parent.tagName ?? "");
434
+ const childRank = VX_TAG_PREFERENCE.indexOf(child.tagName ?? "");
435
+ if (parentRank === -1 && childRank === -1) {
436
+ const parentAttrCount = Object.keys({
437
+ ...parent.labelAttrs,
438
+ ...parent.valueAttrs,
439
+ ...parent.srcAttrs
440
+ }).length;
441
+ const childAttrCount = Object.keys({
442
+ ...child.labelAttrs,
443
+ ...child.valueAttrs,
444
+ ...child.srcAttrs
445
+ }).length;
446
+ return parentAttrCount > childAttrCount;
447
+ }
448
+ return parentRank !== -1 && (childRank === -1 || parentRank < childRank);
449
+ }
450
+ collapseMerge(a, b) {
451
+ return {
452
+ ...b,
453
+ ...a,
454
+ labelAttrs: { ...b.labelAttrs, ...a.labelAttrs },
455
+ valueAttrs: { ...b.valueAttrs, ...a.valueAttrs },
456
+ srcAttrs: { ...b.srcAttrs, ...a.srcAttrs }
457
+ };
458
+ }
459
+ shouldOmit(vxNode) {
460
+ const tagName = vxNode.tagName ?? "";
461
+ if (!vxNode.hasVisibleArea) {
462
+ return true;
463
+ }
464
+ if (this.options.viewportOnly && vxNode.isOutsideViewport) {
465
+ return true;
364
466
  }
365
- this.classList.push(...child.classList);
366
- const parentRank = this.options.tagPreference.indexOf(el.tagName.toLowerCase());
367
- const childRank = this.options.tagPreference.indexOf(child.tagName.toLowerCase());
368
- const preferParent = parentRank !== -1 && (parentRank < childRank || childRank === -1);
369
- if (!preferParent) {
370
- this.node = child;
467
+ if (this.options.probeViewport && !vxNode.isProbeHit) {
468
+ return true;
371
469
  }
372
- if (this.element) {
373
- this.parseTree(this.element, this.getAcceptedChildren(child));
470
+ if (this.options.skipImages && vxNode.tagName === "img") {
471
+ return true;
472
+ }
473
+ const hasText = !!vxNode.textContent;
474
+ const hasChildren = (vxNode.children ?? []).length > 0;
475
+ const hasAttrs = [vxNode.labelAttrs, vxNode.valueAttrs, vxNode.srcAttrs].some((attrs) => !isObjectEmpty(attrs ?? {}));
476
+ if (this.options.unnestDivs) {
477
+ const isDivOrSpan = ["div", "span"].includes(tagName);
478
+ if (isDivOrSpan && hasChildren && !hasAttrs) {
479
+ return true;
480
+ }
374
481
  }
482
+ if (vxNode.isKept) {
483
+ return false;
484
+ }
485
+ return !hasText && !hasAttrs && !hasChildren;
375
486
  }
376
- getAcceptedChildren(el) {
377
- const childNodes = [...el.childNodes];
378
- return childNodes.filter((node) => {
379
- if (!(node instanceof Element || node instanceof Text)) {
380
- return false;
487
+ isProbeHit(el) {
488
+ for (const probeEl of this.probeElements) {
489
+ if (el.contains(probeEl) || probeEl.contains(el)) {
490
+ return true;
381
491
  }
382
- if (node instanceof Element) {
383
- if (this.options.skipHidden && deepIsHidden(node)) {
384
- return false;
385
- }
386
- if (containsSelector(node, "input")) {
387
- return true;
388
- }
389
- if (!this.options.skipIframes && containsSelector(node, "iframe")) {
390
- return true;
391
- }
392
- if (!this.options.skipImages && containsSelector(node, "img")) {
393
- return true;
394
- }
395
- if (this.options.skipTags.includes(node.tagName.toLowerCase())) {
396
- return false;
397
- }
492
+ }
493
+ return false;
494
+ }
495
+ collectLabelAttrs(el) {
496
+ const attrs = {};
497
+ for (const attr of VX_LABEL_ATTRS) {
498
+ const value = truncateAttrValue(el.getAttribute(attr) ?? "");
499
+ if (value) {
500
+ attrs[attr] = value;
398
501
  }
399
- if (this.options.skipEmptyText) {
400
- const isEmptyText = normalizeText(node.textContent ?? "").length === 0;
401
- if (isEmptyText) {
402
- return false;
403
- }
502
+ }
503
+ return attrs;
504
+ }
505
+ collectValueAttrs(el) {
506
+ const attrs = {};
507
+ for (const attr of VX_VALUE_ATTRS) {
508
+ const value = el[attr] ?? "";
509
+ if (value) {
510
+ attrs[attr] = String(value);
404
511
  }
405
- return true;
406
- });
512
+ }
513
+ return attrs;
514
+ }
515
+ collectSrcAttrs(el) {
516
+ const attrs = {};
517
+ for (const attr of VX_SRC_ATTRS) {
518
+ const value = truncateAttrValue(el[attr] ?? "");
519
+ if (value) {
520
+ attrs[attr] = value;
521
+ }
522
+ }
523
+ return attrs;
524
+ }
525
+ };
526
+
527
+ // src/page/overlay.ts
528
+ function showPoint(x, y, clear = true) {
529
+ if (clear) {
530
+ clearOverlay();
531
+ }
532
+ const point = document.createElement("div");
533
+ point.style.position = "absolute";
534
+ point.style.left = `${x}px`;
535
+ point.style.top = `${y}px`;
536
+ point.style.width = "32px";
537
+ point.style.height = "32px";
538
+ point.style.transform = "translate(-50%, -50%)";
539
+ point.style.backgroundColor = "red";
540
+ point.style.borderRadius = "100%";
541
+ point.style.opacity = "0.5";
542
+ const container = getOverlayContainer();
543
+ container.appendChild(point);
544
+ }
545
+ function clearOverlay() {
546
+ const container = getOverlayContainer();
547
+ container.remove();
548
+ }
549
+ function highlightEl(el, ref = 0) {
550
+ if (!(el instanceof Element)) {
551
+ return;
552
+ }
553
+ const container = getOverlayContainer();
554
+ const color = getColor(ref);
555
+ const rect = el.getBoundingClientRect();
556
+ const overlay = document.createElement("div");
557
+ container.appendChild(overlay);
558
+ overlay.style.position = "absolute";
559
+ overlay.style.top = `${rect.top}px`;
560
+ overlay.style.left = `${rect.left}px`;
561
+ overlay.style.width = `${rect.width}px`;
562
+ overlay.style.height = `${rect.height}px`;
563
+ overlay.style.border = `2px solid ${color}`;
564
+ const label = document.createElement("div");
565
+ overlay.appendChild(label);
566
+ label.style.position = "absolute";
567
+ label.style.bottom = `100%`;
568
+ label.style.left = `0`;
569
+ label.style.backgroundColor = color;
570
+ label.style.color = "white";
571
+ label.style.fontSize = "10px";
572
+ label.style.fontFamily = "monospace";
573
+ label.style.fontWeight = "normal";
574
+ label.style.fontStyle = "normal";
575
+ label.style.opacity = "0.8";
576
+ label.style.padding = "0 2px";
577
+ label.style.transform = "translateY(50%)";
578
+ label.textContent = String(ref);
579
+ }
580
+ function getOverlayContainer() {
581
+ let container = document.querySelector("#webvision-overlay");
582
+ if (!container) {
583
+ container = document.createElement("div");
584
+ container[VX_IGNORE_SYMBOL] = true;
585
+ container.id = "webvision-overlay";
586
+ container.style.position = "fixed";
587
+ container.style.top = "0";
588
+ container.style.left = "0";
589
+ container.style.bottom = "0";
590
+ container.style.right = "0";
591
+ container.style.zIndex = "2147483647";
592
+ container.style.pointerEvents = "none";
593
+ document.body.appendChild(container);
594
+ }
595
+ return container;
596
+ }
597
+ function getColor(index) {
598
+ const hue = index * 120 * 0.382 % 360;
599
+ return `hsl(${hue}, 85%, 50%)`;
600
+ }
601
+
602
+ // src/page/traverse.ts
603
+ function* traverseVxNode(vxNode, depth = 0) {
604
+ yield { vxNode, depth };
605
+ for (const child of vxNode.children ?? []) {
606
+ yield* traverseVxNode(child, depth + 1);
407
607
  }
408
- fillMap(map) {
409
- map.set(this.ref, this.node);
410
- for (const child of this.children) {
411
- child.fillMap(map);
608
+ }
609
+
610
+ // src/page/render.ts
611
+ function renderVxNode(scope, options = {}) {
612
+ const buffer = [];
613
+ const whitelistRefs = options.whitelistRefs ?? [];
614
+ for (const { vxNode, depth } of traverseVxNode(scope)) {
615
+ if (whitelistRefs.length > 0) {
616
+ if (!whitelistRefs.includes(vxNode.ref)) {
617
+ continue;
618
+ }
412
619
  }
620
+ const indent = " ".repeat(depth);
621
+ buffer.push(renderIndentedLine(indent, vxNode, options));
413
622
  }
414
- toJson() {
415
- const { top, left, width, height } = this.clientRect;
416
- return {
417
- ref: this.ref,
418
- nodeType: this.node instanceof Element ? "element" : "text",
419
- leaf: this.leaf,
420
- tagName: this.tagName,
421
- rect: {
422
- x: left,
423
- y: top,
424
- width,
425
- height
426
- },
427
- classList: this.node instanceof Element ? this.classList : void 0,
428
- textContent: this.textContent,
429
- href: this.href,
430
- src: this.src,
431
- children: this.leaf ? void 0 : this.children.map((child) => child.toJson())
432
- };
623
+ return buffer.join("\n");
624
+ }
625
+ function renderIndentedLine(indent, vxNode, options = {}) {
626
+ if (!vxNode.tagName) {
627
+ return [indent, vxNode.textContent].filter(Boolean).join("");
628
+ }
629
+ const tagLine = renderTagLine(vxNode, options);
630
+ const htmlStyle = options.renderStyle === "html";
631
+ return [
632
+ indent,
633
+ tagLine,
634
+ htmlStyle ? "" : " ",
635
+ vxNode.textContent ?? ""
636
+ ].join("");
637
+ }
638
+ function renderTagLine(vxNode, options) {
639
+ const htmlStyle = options.renderStyle === "html";
640
+ const components = [];
641
+ if (options.renderTagNames && vxNode.tagName) {
642
+ components.push(vxNode.tagName);
643
+ }
644
+ if (options.renderIds && vxNode.id) {
645
+ if (htmlStyle) {
646
+ components.push(`id="${vxNode.id}"`);
647
+ } else {
648
+ components.push(`#${vxNode.id}`);
649
+ }
650
+ }
651
+ if (options.renderClassNames && vxNode.classList?.length) {
652
+ if (htmlStyle) {
653
+ components.push(`class="${vxNode.classList.join(" ")}"`);
654
+ } else {
655
+ components.push("." + vxNode.classList.join("."));
656
+ }
657
+ }
658
+ if (options.renderRefs) {
659
+ const isRenderRef = [
660
+ options.renderRefs === "all",
661
+ options.renderRefs === true && !isContainerNode(vxNode)
662
+ ].some(Boolean);
663
+ if (isRenderRef) {
664
+ components.push(`[@${vxNode.ref}]`);
665
+ }
666
+ }
667
+ const attrs = [];
668
+ if (options.renderLabelAttrs) {
669
+ for (const [attr, value] of Object.entries(vxNode.labelAttrs ?? {})) {
670
+ attrs.push(`${attr}="${truncateAttrValue(value)}"`);
671
+ }
672
+ }
673
+ if (options.renderValueAttrs) {
674
+ for (const [attr, value] of Object.entries(vxNode.valueAttrs ?? {})) {
675
+ attrs.push(`${attr}="${truncateAttrValue(value)}"`);
676
+ }
677
+ }
678
+ if (options.renderSrcAttrs) {
679
+ for (const [attr, value] of Object.entries(vxNode.srcAttrs ?? {})) {
680
+ attrs.push(`${attr}="${truncateAttrValue(value)}"`);
681
+ }
682
+ }
683
+ if (htmlStyle) {
684
+ components.push(...attrs);
685
+ } else {
686
+ components.push(...attrs.map((attr) => `[${attr}]`));
687
+ }
688
+ return htmlStyle ? `<${components.join(" ")}>` : components.join("");
689
+ }
690
+
691
+ // src/page/view.ts
692
+ var VxPageView = class {
693
+ constructor(frameTrees) {
694
+ this.frameTrees = frameTrees;
695
+ this.vxTreeViewMap = /* @__PURE__ */ new Map();
696
+ for (const frameTree of frameTrees) {
697
+ const vxTreeView = new VxTreeView(this, frameTree);
698
+ this.vxTreeViewMap.set(frameTree.frameId, vxTreeView);
699
+ }
700
+ }
701
+ get vxTrees() {
702
+ return Array.from(this.vxTreeViewMap.values());
703
+ }
704
+ findTreeByFrameId(frameId) {
705
+ return this.vxTreeViewMap.get(frameId) ?? null;
706
+ }
707
+ getTreeByFrameId(frameId) {
708
+ const vxTreeView = this.vxTreeViewMap.get(frameId);
709
+ if (vxTreeView == null) {
710
+ throw new Error(`Frame tree not found for [frameId=${frameId}]`);
711
+ }
712
+ return vxTreeView;
713
+ }
714
+ findTreeByRef(ref) {
715
+ const frameTree = this.frameTrees.find(
716
+ (tree) => ref >= tree.refRange[0] && ref <= tree.refRange[1]
717
+ );
718
+ if (!frameTree) {
719
+ return null;
720
+ }
721
+ return this.vxTreeViewMap.get(frameTree.frameId) ?? null;
722
+ }
723
+ getTreeByRef(ref) {
724
+ const vxTreeView = this.findTreeByRef(ref);
725
+ if (vxTreeView == null) {
726
+ throw new Error(`Frame tree not found for [ref=${ref}]`);
727
+ }
728
+ return vxTreeView;
729
+ }
730
+ renderAll(options) {
731
+ return this.frameTrees.map((frameTree) => {
732
+ return frameTree.nodes.map((node) => {
733
+ return renderVxNode(node, options);
734
+ }).join("\n");
735
+ }).join("\n\n");
433
736
  }
434
737
  };
435
- var Counter = class {
436
- constructor(value = 0) {
437
- this.value = value;
738
+ var VxTreeView = class {
739
+ constructor(vxPage, vxTree) {
740
+ this.vxPage = vxPage;
741
+ this.vxTree = vxTree;
742
+ this.refMap = /* @__PURE__ */ new Map();
743
+ this.buildRefMap(vxTree.nodes);
438
744
  }
439
- next() {
440
- this.value += 1;
441
- return this.value;
745
+ get frameId() {
746
+ return this.vxTree.frameId;
747
+ }
748
+ get refRange() {
749
+ return this.vxTree.refRange;
750
+ }
751
+ findNode(ref) {
752
+ return this.refMap.get(ref) ?? null;
753
+ }
754
+ *traverse() {
755
+ for (const vxNode of this.vxTree.nodes) {
756
+ yield* traverseVxNode(vxNode, 0);
757
+ }
758
+ }
759
+ findParentTree() {
760
+ const { iframeRef } = this.vxTree;
761
+ if (iframeRef == null) {
762
+ return null;
763
+ }
764
+ return this.vxPage.findTreeByRef(iframeRef);
765
+ }
766
+ isFrameShown() {
767
+ const parentTree = this.findParentTree();
768
+ const { iframeRef } = this.vxTree;
769
+ if (parentTree == null || iframeRef == null) {
770
+ return true;
771
+ }
772
+ const existsInParent = !!parentTree.findNode(iframeRef);
773
+ return existsInParent ? parentTree.isFrameShown() : false;
774
+ }
775
+ collectRefs(leafOnly = true) {
776
+ const refs = [];
777
+ for (const { vxNode } of this.traverse()) {
778
+ if (leafOnly && isContainerNode(vxNode)) {
779
+ continue;
780
+ }
781
+ refs.push(vxNode.ref);
782
+ }
783
+ return refs;
784
+ }
785
+ buildRefMap(nodes) {
786
+ for (const node of nodes) {
787
+ this.refMap.set(node.ref, node);
788
+ this.buildRefMap(node.children ?? []);
789
+ }
442
790
  }
443
791
  };
444
792
  export {
445
793
  Counter,
446
- DEFAULT_SEMANTIC_TAGS,
447
- DEFAULT_SKIP_TAGS,
448
- SnapshotTree,
449
- captureAncestorsHtml,
450
- captureHtml,
451
- captureHtmlLine,
794
+ INTERACTIVE_ROLES,
795
+ INTERACTIVE_TAGS,
796
+ VX_IGNORE_SYMBOL,
797
+ VX_IGNORE_TAGS,
798
+ VX_LABEL_ATTRS,
799
+ VX_NODE_SYMBOL,
800
+ VX_SRC_ATTRS,
801
+ VX_TAG_PREFERENCE,
802
+ VX_VALUE_ATTRS,
803
+ VxPageView,
804
+ VxTreeParser,
805
+ VxTreeView,
806
+ checkFrameOmitted,
807
+ checkOccluded,
808
+ clearOverlay,
452
809
  containsSelector,
453
- createSnapshot,
454
- deepIsHidden,
455
- getHighlightContainer,
810
+ escapeAttribute,
811
+ findFrameTreeByRef,
812
+ findParentFrameTree,
813
+ fixZIndex,
814
+ getNormalizedText,
815
+ getOffsetTop,
816
+ getViewportSize,
456
817
  hasVisibleArea,
457
818
  highlightEl,
458
- highlightSnapshot,
819
+ isContainerNode,
820
+ isDeepHidden,
459
821
  isHidden,
460
- isRecursiveInline,
822
+ isInteractive,
823
+ isObjectEmpty,
824
+ isRandomIdentifier,
825
+ isRectInViewport,
826
+ makeOverlaysOpaque,
461
827
  normalizeText,
462
- removeHighlight,
463
- renderLine,
464
- renderSnapshot
828
+ probeViewport,
829
+ renderVxNode,
830
+ showPoint,
831
+ traverseVxNode,
832
+ truncateAttrValue
465
833
  };