@ubio/webvision 1.2.6 → 2.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.
Files changed (47) hide show
  1. package/build/page.mjs +760 -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/frame.d.ts +22 -0
  9. package/out/page/frame.js +69 -0
  10. package/out/page/frame.js.map +1 -0
  11. package/out/page/index.d.ts +9 -3
  12. package/out/page/index.js +9 -3
  13. package/out/page/index.js.map +1 -1
  14. package/out/page/overlay.d.ts +3 -0
  15. package/out/page/overlay.js +75 -0
  16. package/out/page/overlay.js.map +1 -0
  17. package/out/page/parser.d.ts +59 -0
  18. package/out/page/parser.js +249 -0
  19. package/out/page/parser.js.map +1 -0
  20. package/out/page/probe.d.ts +5 -0
  21. package/out/page/probe.js +36 -0
  22. package/out/page/probe.js.map +1 -0
  23. package/out/page/render.d.ts +12 -8
  24. package/out/page/render.js +73 -38
  25. package/out/page/render.js.map +1 -1
  26. package/out/page/snapshot.d.ts +9 -68
  27. package/out/page/snapshot.js +23 -183
  28. package/out/page/snapshot.js.map +1 -1
  29. package/out/page/traverse.d.ts +5 -0
  30. package/out/page/traverse.js +7 -0
  31. package/out/page/traverse.js.map +1 -0
  32. package/out/page/tree.d.ts +22 -0
  33. package/out/page/tree.js +59 -0
  34. package/out/page/tree.js.map +1 -0
  35. package/out/page/util.d.ts +3 -0
  36. package/out/page/util.js +9 -0
  37. package/out/page/util.js.map +1 -0
  38. package/package.json +6 -3
  39. package/out/page/highlight.d.ts +0 -5
  40. package/out/page/highlight.js +0 -72
  41. package/out/page/highlight.js.map +0 -1
  42. package/out/page/html.d.ts +0 -4
  43. package/out/page/html.js +0 -42
  44. package/out/page/html.js.map +0 -1
  45. package/out/page/utils.d.ts +0 -19
  46. package/out/page/utils.js +0 -68
  47. package/out/page/utils.js.map +0 -1
package/build/page.mjs CHANGED
@@ -1,243 +1,254 @@
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;
114
+ }
115
+ if (htmlEl.onclick != null) {
116
+ return true;
113
117
  }
114
- return "";
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;
197
- }
198
- if (!hasVisibleArea(element)) {
199
- return [...element.children].every((el) => deepIsHidden(el, options));
186
+ function fixZIndex(el) {
187
+ const style = getComputedStyle(el);
188
+ if (Number(style.zIndex) > 2147483600) {
189
+ el.style.setProperty("z-index", "2147483600", "important");
200
190
  }
201
- return false;
202
- }
203
- function normalizeText(str) {
204
- return str.replace(/\p{Cf}/gu, " ").replace(/\s+/g, " ").trim();
205
- }
206
- function containsSelector(el, selector) {
207
- return el.matches(selector) || !!el.querySelector(selector);
208
191
  }
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;
226
224
  }
227
225
 
228
- // src/page/snapshot.ts
229
- var DEFAULT_SKIP_TAGS = ["svg", "script", "noscript", "style", "link", "meta"];
230
- var DEFAULT_SEMANTIC_TAGS = [
226
+ // src/page/util.ts
227
+ function isContainerNode(vxNode) {
228
+ const children = vxNode.children ?? [];
229
+ const hasTextChildren = children.some((child) => !child.tagName);
230
+ return children.length > 0 && !hasTextChildren;
231
+ }
232
+ function isObjectEmpty(obj) {
233
+ return !Object.values(obj).some((value) => !!value);
234
+ }
235
+
236
+ // src/page/parser.ts
237
+ var VX_NODE_SYMBOL = Symbol("vx:node");
238
+ var VX_IGNORE_SYMBOL = Symbol("vx:ignore");
239
+ var VX_IGNORE_TAGS = ["svg", "script", "noscript", "style", "link", "meta"];
240
+ var VX_LABEL_ATTRS = ["title", "alt", "placeholder", "aria-label"];
241
+ var VX_VALUE_ATTRS = ["value", "checked", "selected", "disabled", "readonly"];
242
+ var VX_SRC_ATTRS = ["src", "href"];
243
+ var VX_TAG_PREFERENCE = [
231
244
  "a",
245
+ "iframe",
246
+ "input",
232
247
  "button",
248
+ "select",
249
+ "textarea",
250
+ "img",
233
251
  "label",
234
- "section",
235
- "article",
236
- "main",
237
- "header",
238
- "footer",
239
- "nav",
240
- "aside",
241
252
  "h1",
242
253
  "h2",
243
254
  "h3",
@@ -250,6 +261,20 @@ var DEFAULT_SEMANTIC_TAGS = [
250
261
  "dl",
251
262
  "dt",
252
263
  "dd",
264
+ "section",
265
+ "article",
266
+ "main",
267
+ "header",
268
+ "footer",
269
+ "nav",
270
+ "aside",
271
+ "form",
272
+ "input",
273
+ "textarea",
274
+ "select",
275
+ "option",
276
+ "fieldset",
277
+ "legend",
253
278
  "p",
254
279
  "pre",
255
280
  "code",
@@ -262,204 +287,584 @@ var DEFAULT_SEMANTIC_TAGS = [
262
287
  "tr",
263
288
  "td",
264
289
  "th",
265
- "form",
266
- "input",
267
- "textarea",
268
- "select",
269
- "option",
270
- "fieldset",
271
- "legend",
272
290
  "strong",
273
291
  "em",
274
292
  "sub",
275
293
  "sup"
276
294
  ];
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;
295
+ var VX_KEEP_SELECTOR = "a, button, input, textarea, select, label, iframe";
296
+ var VxTreeParser = class {
297
+ constructor(options = {}) {
307
298
  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));
299
+ this.probeElements = [];
300
+ this.domRefMap = /* @__PURE__ */ new Map();
301
+ const { startRef = 0 } = options;
302
+ this.viewport = getViewportSize();
303
+ this.counter = new Counter(startRef);
304
+ if (options.probeViewport) {
305
+ this.probeElements = probeViewport(8);
313
306
  }
307
+ const vxRoot = this.parseDocument();
308
+ this.refRange = [startRef, this.counter.current()];
309
+ this.vxNodes = this.pruneRecursive(vxRoot);
314
310
  }
315
- get element() {
316
- return this.node instanceof Element ? this.node : null;
311
+ parseDocument() {
312
+ return this.parseNode(document.documentElement, null);
317
313
  }
318
- get depth() {
319
- return this.parent ? this.parent.depth + 1 : 0;
314
+ getNodes() {
315
+ return this.vxNodes;
320
316
  }
321
- get leaf() {
322
- return this.children.length === 0;
317
+ getRefRange() {
318
+ return this.refRange;
323
319
  }
324
- get tagName() {
325
- return this.node instanceof Element ? this.node.tagName.toLowerCase() : void 0;
320
+ getDomMap() {
321
+ return this.domRefMap;
326
322
  }
327
- get href() {
328
- return this.node instanceof HTMLAnchorElement ? this.node.href : void 0;
329
- }
330
- get src() {
331
- return this.node.src ?? void 0;
323
+ parseNode(node, parent) {
324
+ if (node[VX_IGNORE_SYMBOL]) {
325
+ return null;
326
+ }
327
+ if (node instanceof Text) {
328
+ const textContent = normalizeText(node.textContent ?? "");
329
+ if (textContent) {
330
+ return this.makeNode(node, {
331
+ textContent,
332
+ hasVisibleArea: parent?.hasVisibleArea,
333
+ isOutsideViewport: parent?.isOutsideViewport,
334
+ isProbeHit: parent?.isProbeHit
335
+ });
336
+ }
337
+ return null;
338
+ }
339
+ if (node instanceof Element) {
340
+ if (this.options.opaqueOverlays) {
341
+ makeOverlaysOpaque(node);
342
+ }
343
+ fixZIndex(node);
344
+ return this.parseElement(node);
345
+ }
346
+ return null;
332
347
  }
333
- get clientRect() {
334
- if (this.node instanceof Element) {
335
- return this.node.getBoundingClientRect();
348
+ parseElement(el) {
349
+ if (VX_IGNORE_TAGS.includes(el.tagName.toLowerCase())) {
350
+ return null;
351
+ }
352
+ if (isHidden(el)) {
353
+ return null;
336
354
  }
337
- const range = document.createRange();
338
- range.selectNodeContents(this.node);
339
- return range.getBoundingClientRect();
355
+ const rect = el.getBoundingClientRect();
356
+ const id = el.getAttribute("id") ?? "";
357
+ const vxNode = this.makeNode(el, {
358
+ tagName: el.tagName.toLowerCase(),
359
+ id: isRandomIdentifier(id) ? void 0 : id,
360
+ classList: Array.from(el.classList).filter((cls) => !isRandomIdentifier(cls)).slice(0, 4),
361
+ labelAttrs: this.collectLabelAttrs(el),
362
+ valueAttrs: this.collectValueAttrs(el),
363
+ srcAttrs: this.collectSrcAttrs(el),
364
+ hasVisibleArea: hasVisibleArea(el),
365
+ isInteractive: isInteractive(el),
366
+ isOutsideViewport: !isRectInViewport(rect, this.viewport),
367
+ isProbeHit: this.isProbeHit(el),
368
+ isKept: el.matches(VX_KEEP_SELECTOR)
369
+ });
370
+ const children = [...el.childNodes].map((child) => this.parseNode(child, vxNode)).filter((_) => _ != null);
371
+ if (children.length === 1 && !children[0]?.tagName) {
372
+ vxNode.textContent = normalizeText(children[0].textContent ?? "");
373
+ } else {
374
+ vxNode.children = children;
375
+ }
376
+ return vxNode;
377
+ }
378
+ makeNode(node, spec) {
379
+ const ref = this.counter.next();
380
+ const vxNode = {
381
+ ...spec,
382
+ ref
383
+ };
384
+ this.domRefMap.set(ref, node);
385
+ node[VX_NODE_SYMBOL] = vxNode;
386
+ return vxNode;
340
387
  }
341
- parseTree(el, childNodes) {
342
- this.children = [];
343
- if (childNodes.length === 1) {
344
- return this.collapseWrapper(el, childNodes[0]);
388
+ pruneRecursive(vxNode) {
389
+ if (!vxNode) {
390
+ return [];
345
391
  }
346
- if (this.options.collapseInline && isRecursiveInline(el, this.options.tagPreference)) {
347
- return;
392
+ const children = vxNode.children ?? [];
393
+ const newChildren = children.flatMap((child) => this.pruneRecursive(child));
394
+ if (this.shouldOmit(vxNode)) {
395
+ return newChildren;
348
396
  }
349
- for (const childNode of childNodes) {
350
- const snapshot = new _SnapshotTree(childNode, this, this.counter, this.options);
351
- this.children.push(snapshot);
397
+ vxNode.children = newChildren;
398
+ if (newChildren.length === 1) {
399
+ const child = newChildren[0];
400
+ return this.collapseSingleChild(vxNode, child);
352
401
  }
402
+ return [vxNode];
403
+ }
404
+ collapseSingleChild(parent, child) {
405
+ const preferParent = this.collapsePreferParent(parent, child);
406
+ const merged = preferParent ? this.collapseMerge(parent, child) : this.collapseMerge(child, parent);
407
+ return this.pruneRecursive({
408
+ ...merged,
409
+ children: child.children,
410
+ textContent: child.textContent
411
+ });
412
+ }
413
+ collapsePreferParent(parent, child) {
414
+ const parentRank = VX_TAG_PREFERENCE.indexOf(parent.tagName ?? "");
415
+ const childRank = VX_TAG_PREFERENCE.indexOf(child.tagName ?? "");
416
+ if (parentRank === -1 && childRank === -1) {
417
+ const parentAttrCount = Object.keys({
418
+ ...parent.labelAttrs,
419
+ ...parent.valueAttrs,
420
+ ...parent.srcAttrs
421
+ }).length;
422
+ const childAttrCount = Object.keys({
423
+ ...child.labelAttrs,
424
+ ...child.valueAttrs,
425
+ ...child.srcAttrs
426
+ }).length;
427
+ return parentAttrCount > childAttrCount;
428
+ }
429
+ return parentRank !== -1 && (childRank === -1 || parentRank < childRank);
353
430
  }
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;
431
+ collapseMerge(a, b) {
432
+ return {
433
+ ...b,
434
+ ...a,
435
+ labelAttrs: { ...b.labelAttrs, ...a.labelAttrs },
436
+ valueAttrs: { ...b.valueAttrs, ...a.valueAttrs },
437
+ srcAttrs: { ...b.srcAttrs, ...a.srcAttrs }
438
+ };
439
+ }
440
+ shouldOmit(vxNode) {
441
+ const tagName = vxNode.tagName ?? "";
442
+ if (!vxNode.hasVisibleArea) {
443
+ return true;
444
+ }
445
+ if (this.options.viewportOnly && vxNode.isOutsideViewport) {
446
+ return true;
364
447
  }
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;
448
+ if (this.options.probeViewport && !vxNode.isProbeHit) {
449
+ return true;
450
+ }
451
+ if (this.options.skipImages && vxNode.tagName === "img") {
452
+ return true;
453
+ }
454
+ const hasText = !!vxNode.textContent;
455
+ const hasChildren = (vxNode.children ?? []).length > 0;
456
+ const hasAttrs = [vxNode.labelAttrs, vxNode.valueAttrs, vxNode.srcAttrs].some((attrs) => !isObjectEmpty(attrs ?? {}));
457
+ if (this.options.unnestDivs) {
458
+ const isDivOrSpan = ["div", "span"].includes(tagName);
459
+ if (isDivOrSpan && hasChildren && !hasAttrs) {
460
+ return true;
461
+ }
371
462
  }
372
- if (this.element) {
373
- this.parseTree(this.element, this.getAcceptedChildren(child));
463
+ if (vxNode.isKept) {
464
+ return false;
374
465
  }
466
+ return !hasText && !hasAttrs && !hasChildren;
375
467
  }
376
- getAcceptedChildren(el) {
377
- const childNodes = [...el.childNodes];
378
- return childNodes.filter((node) => {
379
- if (!(node instanceof Element || node instanceof Text)) {
380
- return false;
468
+ isProbeHit(el) {
469
+ for (const probeEl of this.probeElements) {
470
+ if (el.contains(probeEl) || probeEl.contains(el)) {
471
+ return true;
381
472
  }
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
- }
473
+ }
474
+ return false;
475
+ }
476
+ collectLabelAttrs(el) {
477
+ const attrs = {};
478
+ for (const attr of VX_LABEL_ATTRS) {
479
+ const value = truncateAttrValue(el.getAttribute(attr) ?? "");
480
+ if (value) {
481
+ attrs[attr] = value;
398
482
  }
399
- if (this.options.skipEmptyText) {
400
- const isEmptyText = normalizeText(node.textContent ?? "").length === 0;
401
- if (isEmptyText) {
402
- return false;
403
- }
483
+ }
484
+ return attrs;
485
+ }
486
+ collectValueAttrs(el) {
487
+ const attrs = {};
488
+ for (const attr of VX_VALUE_ATTRS) {
489
+ const value = el[attr] ?? "";
490
+ if (value) {
491
+ attrs[attr] = String(value);
404
492
  }
405
- return true;
406
- });
493
+ }
494
+ return attrs;
495
+ }
496
+ collectSrcAttrs(el) {
497
+ const attrs = {};
498
+ for (const attr of VX_SRC_ATTRS) {
499
+ const value = truncateAttrValue(el[attr] ?? "");
500
+ if (value) {
501
+ attrs[attr] = value;
502
+ }
503
+ }
504
+ return attrs;
505
+ }
506
+ };
507
+
508
+ // src/page/overlay.ts
509
+ function showPoint(x, y, clear = true) {
510
+ if (clear) {
511
+ clearOverlay();
512
+ }
513
+ const point = document.createElement("div");
514
+ point.style.position = "absolute";
515
+ point.style.left = `${x}px`;
516
+ point.style.top = `${y}px`;
517
+ point.style.width = "32px";
518
+ point.style.height = "32px";
519
+ point.style.transform = "translate(-50%, -50%)";
520
+ point.style.backgroundColor = "red";
521
+ point.style.borderRadius = "100%";
522
+ point.style.opacity = "0.5";
523
+ const container = getOverlayContainer();
524
+ container.appendChild(point);
525
+ }
526
+ function clearOverlay() {
527
+ const container = getOverlayContainer();
528
+ container.remove();
529
+ }
530
+ function highlightEl(el, ref = 0) {
531
+ if (!(el instanceof Element)) {
532
+ return;
533
+ }
534
+ const container = getOverlayContainer();
535
+ const color = getColor(ref);
536
+ const rect = el.getBoundingClientRect();
537
+ const overlay = document.createElement("div");
538
+ container.appendChild(overlay);
539
+ overlay.style.position = "absolute";
540
+ overlay.style.top = `${rect.top}px`;
541
+ overlay.style.left = `${rect.left}px`;
542
+ overlay.style.width = `${rect.width}px`;
543
+ overlay.style.height = `${rect.height}px`;
544
+ overlay.style.border = `2px solid ${color}`;
545
+ const label = document.createElement("div");
546
+ overlay.appendChild(label);
547
+ label.style.position = "absolute";
548
+ label.style.bottom = `100%`;
549
+ label.style.left = `0`;
550
+ label.style.backgroundColor = color;
551
+ label.style.color = "white";
552
+ label.style.fontSize = "10px";
553
+ label.style.fontFamily = "monospace";
554
+ label.style.fontWeight = "normal";
555
+ label.style.fontStyle = "normal";
556
+ label.style.opacity = "0.8";
557
+ label.style.padding = "0 2px";
558
+ label.style.transform = "translateY(50%)";
559
+ label.textContent = String(ref);
560
+ }
561
+ function getOverlayContainer() {
562
+ let container = document.querySelector("#webvision-overlay");
563
+ if (!container) {
564
+ container = document.createElement("div");
565
+ container[VX_IGNORE_SYMBOL] = true;
566
+ container.id = "webvision-overlay";
567
+ container.style.position = "fixed";
568
+ container.style.top = "0";
569
+ container.style.left = "0";
570
+ container.style.bottom = "0";
571
+ container.style.right = "0";
572
+ container.style.zIndex = "2147483647";
573
+ container.style.pointerEvents = "none";
574
+ document.body.appendChild(container);
575
+ }
576
+ return container;
577
+ }
578
+ function getColor(index) {
579
+ const hue = index * 120 * 0.382 % 360;
580
+ return `hsl(${hue}, 85%, 50%)`;
581
+ }
582
+
583
+ // src/page/traverse.ts
584
+ function* traverseVxNode(vxNode, depth = 0) {
585
+ yield { vxNode, depth };
586
+ for (const child of vxNode.children ?? []) {
587
+ yield* traverseVxNode(child, depth + 1);
407
588
  }
408
- fillMap(map) {
409
- map.set(this.ref, this.node);
410
- for (const child of this.children) {
411
- child.fillMap(map);
589
+ }
590
+
591
+ // src/page/render.ts
592
+ function renderVxNode(scope, options = {}) {
593
+ const buffer = [];
594
+ const whitelistRefs = options.whitelistRefs ?? [];
595
+ for (const { vxNode, depth } of traverseVxNode(scope)) {
596
+ if (whitelistRefs.length > 0) {
597
+ if (!whitelistRefs.includes(vxNode.ref)) {
598
+ continue;
599
+ }
412
600
  }
601
+ const indent = " ".repeat(depth);
602
+ buffer.push(renderIndentedLine(indent, vxNode, options));
413
603
  }
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
- };
604
+ return buffer.join("\n");
605
+ }
606
+ function renderIndentedLine(indent, vxNode, options = {}) {
607
+ if (!vxNode.tagName) {
608
+ return [indent, vxNode.textContent].filter(Boolean).join("");
609
+ }
610
+ const tagLine = renderTagLine(vxNode, options);
611
+ const htmlStyle = options.renderStyle === "html";
612
+ return [
613
+ indent,
614
+ tagLine,
615
+ htmlStyle ? "" : " ",
616
+ vxNode.textContent ?? ""
617
+ ].join("");
618
+ }
619
+ function renderTagLine(vxNode, options) {
620
+ const htmlStyle = options.renderStyle === "html";
621
+ const components = [];
622
+ if (options.renderTagNames && vxNode.tagName) {
623
+ components.push(vxNode.tagName);
624
+ }
625
+ if (options.renderIds && vxNode.id) {
626
+ if (htmlStyle) {
627
+ components.push(`id="${vxNode.id}"`);
628
+ } else {
629
+ components.push(`#${vxNode.id}`);
630
+ }
631
+ }
632
+ if (options.renderClassNames && vxNode.classList?.length) {
633
+ if (htmlStyle) {
634
+ components.push(`class="${vxNode.classList.join(" ")}"`);
635
+ } else {
636
+ components.push("." + vxNode.classList.join("."));
637
+ }
638
+ }
639
+ if (options.renderRefs) {
640
+ const isRenderRef = [
641
+ options.renderRefs === "all",
642
+ options.renderRefs === true && !isContainerNode(vxNode)
643
+ ].some(Boolean);
644
+ if (isRenderRef) {
645
+ components.push(`[@${vxNode.ref}]`);
646
+ }
647
+ }
648
+ const attrs = [];
649
+ if (options.renderLabelAttrs) {
650
+ for (const [attr, value] of Object.entries(vxNode.labelAttrs ?? {})) {
651
+ attrs.push(`${attr}="${truncateAttrValue(value)}"`);
652
+ }
653
+ }
654
+ if (options.renderValueAttrs) {
655
+ for (const [attr, value] of Object.entries(vxNode.valueAttrs ?? {})) {
656
+ attrs.push(`${attr}="${truncateAttrValue(value)}"`);
657
+ }
658
+ }
659
+ if (options.renderSrcAttrs) {
660
+ for (const [attr, value] of Object.entries(vxNode.srcAttrs ?? {})) {
661
+ attrs.push(`${attr}="${truncateAttrValue(value)}"`);
662
+ }
663
+ }
664
+ if (htmlStyle) {
665
+ components.push(...attrs);
666
+ } else {
667
+ components.push(...attrs.map((attr) => `[${attr}]`));
668
+ }
669
+ return htmlStyle ? `<${components.join(" ")}>` : components.join("");
670
+ }
671
+
672
+ // src/page/snapshot.ts
673
+ var VX_DOM_SYMBOL = Symbol("vx:dom");
674
+ var VX_TREE_SYMBOL = Symbol("vx:tree");
675
+ function captureSnapshot(options = {}) {
676
+ const parser = new VxTreeParser(options);
677
+ const vxNodes = parser.getNodes();
678
+ const domMap = parser.getDomMap();
679
+ const vxTree = new VxTreeView(vxNodes);
680
+ globalThis[VX_DOM_SYMBOL] = domMap;
681
+ globalThis[VX_TREE_SYMBOL] = vxTree;
682
+ return {
683
+ nodes: vxNodes,
684
+ refRange: parser.getRefRange()
685
+ };
686
+ }
687
+ function getSnapshot() {
688
+ const vxTree = globalThis[VX_TREE_SYMBOL];
689
+ if (!vxTree) {
690
+ throw new Error("[VX] Snapshot not found");
691
+ }
692
+ return vxTree;
693
+ }
694
+ function resolveDomNode(ref) {
695
+ const domMap = globalThis[VX_DOM_SYMBOL];
696
+ if (!domMap) {
697
+ return null;
698
+ }
699
+ return domMap.get(ref) ?? null;
700
+ }
701
+
702
+ // src/page/tree.ts
703
+ var VxTreeView = class {
704
+ constructor(nodes) {
705
+ this.nodes = nodes;
706
+ this.refMap = /* @__PURE__ */ new Map();
707
+ this.buildRefMap(nodes);
708
+ }
709
+ *traverse() {
710
+ for (const vxNode of this.nodes) {
711
+ yield* traverseVxNode(vxNode, 0);
712
+ }
713
+ }
714
+ buildRefMap(nodes) {
715
+ for (const node of nodes) {
716
+ this.refMap.set(node.ref, node);
717
+ this.buildRefMap(node.children ?? []);
718
+ }
719
+ }
720
+ findNode(ref) {
721
+ return this.refMap.get(ref) ?? null;
722
+ }
723
+ render(options = {}) {
724
+ return this.nodes.map((node) => {
725
+ return renderVxNode(node, options);
726
+ }).join("\n");
727
+ }
728
+ highlight(options = {}) {
729
+ if (options.clearOverlay) {
730
+ clearOverlay();
731
+ }
732
+ const leafOnly = !options.includeAll;
733
+ const refs = this.collectRefs(leafOnly, options.filterRefs);
734
+ for (const ref of refs) {
735
+ const el = resolveDomNode(ref);
736
+ if (el instanceof Element) {
737
+ highlightEl(el);
738
+ }
739
+ }
740
+ return refs;
741
+ }
742
+ collectRefs(leafOnly = true, filterRefs) {
743
+ const refs = [];
744
+ for (const { vxNode } of this.traverse()) {
745
+ if (leafOnly && isContainerNode(vxNode)) {
746
+ continue;
747
+ }
748
+ if (filterRefs && !filterRefs.includes(vxNode.ref)) {
749
+ continue;
750
+ }
751
+ refs.push(vxNode.ref);
752
+ }
753
+ return refs;
433
754
  }
434
755
  };
435
- var Counter = class {
436
- constructor(value = 0) {
437
- this.value = value;
756
+
757
+ // src/page/frame.ts
758
+ var VxPageView = class {
759
+ constructor(vxFrames) {
760
+ this.vxFrames = vxFrames;
761
+ this.vxFrameMap = /* @__PURE__ */ new Map();
762
+ this.vxTreeMap = /* @__PURE__ */ new Map();
763
+ for (const frame of vxFrames) {
764
+ this.vxFrameMap.set(frame.frameId, frame);
765
+ this.vxTreeMap.set(frame.frameId, new VxTreeView(frame.nodes));
766
+ }
438
767
  }
439
- next() {
440
- this.value += 1;
441
- return this.value;
768
+ findFrameByFrameId(frameId) {
769
+ return this.vxFrameMap.get(frameId) ?? null;
770
+ }
771
+ getFrameByFrameId(frameId) {
772
+ const vxFrame = this.vxFrameMap.get(frameId);
773
+ if (vxFrame == null) {
774
+ throw new Error(`[VX] Frame not found for [frameId=${frameId}]`);
775
+ }
776
+ return vxFrame;
777
+ }
778
+ findFrameByRef(ref) {
779
+ const vxFrame = this.vxFrames.find(
780
+ (frame) => ref >= frame.refRange[0] && ref <= frame.refRange[1]
781
+ );
782
+ if (!vxFrame) {
783
+ return null;
784
+ }
785
+ return this.vxFrameMap.get(vxFrame.frameId) ?? null;
786
+ }
787
+ getFrameByRef(ref) {
788
+ const vxFrame = this.findFrameByRef(ref);
789
+ if (vxFrame == null) {
790
+ throw new Error(`[VX] Frame not found for [ref=${ref}]`);
791
+ }
792
+ return vxFrame;
793
+ }
794
+ findParentFrame(frameId) {
795
+ const frame = this.getFrameByFrameId(frameId);
796
+ const iframeRef = frame?.iframeRef;
797
+ if (iframeRef == null) {
798
+ return null;
799
+ }
800
+ return this.findFrameByRef(iframeRef);
801
+ }
802
+ isFrameShown(frameId) {
803
+ const frame = this.getFrameByFrameId(frameId);
804
+ if (!frame) {
805
+ return false;
806
+ }
807
+ const parentFrame = this.findParentFrame(frameId);
808
+ const { iframeRef } = frame;
809
+ if (parentFrame == null || iframeRef == null) {
810
+ return true;
811
+ }
812
+ const vxTree = this.vxTreeMap.get(frameId);
813
+ const existsInParent = !!vxTree?.findNode(iframeRef);
814
+ return existsInParent ? this.isFrameShown(parentFrame.frameId) : false;
815
+ }
816
+ renderAll(options) {
817
+ return this.vxFrames.map((frame) => {
818
+ const vxTree = this.vxTreeMap.get(frame.frameId);
819
+ const rendered = vxTree?.render(options);
820
+ return [
821
+ `FRAME ${frame.frameId}`,
822
+ rendered
823
+ ].join("\n\n");
824
+ }).join("\n\n");
442
825
  }
443
826
  };
444
827
  export {
445
828
  Counter,
446
- DEFAULT_SEMANTIC_TAGS,
447
- DEFAULT_SKIP_TAGS,
448
- SnapshotTree,
449
- captureAncestorsHtml,
450
- captureHtml,
451
- captureHtmlLine,
829
+ INTERACTIVE_ROLES,
830
+ INTERACTIVE_TAGS,
831
+ VX_DOM_SYMBOL,
832
+ VX_IGNORE_SYMBOL,
833
+ VX_IGNORE_TAGS,
834
+ VX_LABEL_ATTRS,
835
+ VX_NODE_SYMBOL,
836
+ VX_SRC_ATTRS,
837
+ VX_TAG_PREFERENCE,
838
+ VX_TREE_SYMBOL,
839
+ VX_VALUE_ATTRS,
840
+ VxPageView,
841
+ VxTreeParser,
842
+ VxTreeView,
843
+ captureSnapshot,
844
+ checkOccluded,
845
+ clearOverlay,
452
846
  containsSelector,
453
- createSnapshot,
454
- deepIsHidden,
455
- getHighlightContainer,
847
+ escapeAttribute,
848
+ fixZIndex,
849
+ getNormalizedText,
850
+ getOffsetTop,
851
+ getSnapshot,
852
+ getViewportSize,
456
853
  hasVisibleArea,
457
854
  highlightEl,
458
- highlightSnapshot,
855
+ isContainerNode,
856
+ isDeepHidden,
459
857
  isHidden,
460
- isRecursiveInline,
858
+ isInteractive,
859
+ isObjectEmpty,
860
+ isRandomIdentifier,
861
+ isRectInViewport,
862
+ makeOverlaysOpaque,
461
863
  normalizeText,
462
- removeHighlight,
463
- renderLine,
464
- renderSnapshot
864
+ probeViewport,
865
+ renderVxNode,
866
+ resolveDomNode,
867
+ showPoint,
868
+ traverseVxNode,
869
+ truncateAttrValue
465
870
  };