main
js 4,023 lines 121 KB
Raw
1 (() => {
2 const GLOBAL_KEY = "__spaceBrowserPageContent__";
3 const DOM_HELPER_KEY = "__spaceBrowserDomHelper__";
4 const VERSION = "13";
5 const REQUIRED_API_NAMES = Object.freeze([
6 "annotate",
7 "boundingBoxFor",
8 "capture",
9 "click",
10 "detail",
11 "fileInputElementFor",
12 "fileInputFor",
13 "pointFor",
14 "scroll",
15 "select",
16 "setChecked",
17 "submit",
18 "type",
19 "typeSubmit"
20 ]);
21
22 function patchOpenShadowDom() {
23 const original = Element.prototype.attachShadow;
24 if (!original || original.__a0BrowserOpenShadowPatch) {
25 return;
26 }
27 const patched = function attachShadow(options) {
28 return original.call(this, { ...(options || {}), mode: "open" });
29 };
30 patched.__a0BrowserOpenShadowPatch = true;
31 Element.prototype.attachShadow = patched;
32 }
33
34 patchOpenShadowDom();
35
36 const BLOCK_TAGS = new Set([
37 "ADDRESS",
38 "ARTICLE",
39 "ASIDE",
40 "BLOCKQUOTE",
41 "BODY",
42 "DETAILS",
43 "DIV",
44 "DL",
45 "FIELDSET",
46 "FIGCAPTION",
47 "FIGURE",
48 "FOOTER",
49 "FORM",
50 "H1",
51 "H2",
52 "H3",
53 "H4",
54 "H5",
55 "H6",
56 "HEADER",
57 "HR",
58 "HTML",
59 "LI",
60 "MAIN",
61 "NAV",
62 "OL",
63 "P",
64 "PRE",
65 "SECTION",
66 "TABLE",
67 "TBODY",
68 "TD",
69 "TFOOT",
70 "TH",
71 "THEAD",
72 "TR",
73 "UL"
74 ]);
75 const SKIP_TAGS = new Set([
76 "HEAD",
77 "LINK",
78 "META",
79 "NOSCRIPT",
80 "SCRIPT",
81 "STYLE",
82 "TEMPLATE"
83 ]);
84 const INTERACTIVE_ROLES = new Set([
85 "button",
86 "checkbox",
87 "combobox",
88 "link",
89 "menuitem",
90 "menuitemcheckbox",
91 "menuitemradio",
92 "option",
93 "radio",
94 "searchbox",
95 "slider",
96 "spinbutton",
97 "switch",
98 "tab",
99 "textbox"
100 ]);
101 const INTERACTIVE_EVENT_NAMES = new Set([
102 "auxclick",
103 "change",
104 "click",
105 "contextmenu",
106 "dblclick",
107 "input",
108 "keydown",
109 "keypress",
110 "keyup",
111 "mousedown",
112 "mouseup",
113 "pointerdown",
114 "pointerup",
115 "submit",
116 "touchend",
117 "touchstart"
118 ]);
119 const INTERACTIVE_EVENT_PROPERTIES = [...INTERACTIVE_EVENT_NAMES]
120 .map((eventName) => `on${eventName}`);
121
122 if (globalThis[GLOBAL_KEY]?.version === VERSION) {
123 return;
124 }
125
126 const state = {
127 backend: "live",
128 captureId: 0,
129 capturedAt: 0,
130 captureOptions: {
131 includeLabelQuotes: false,
132 includeLinkUrls: false,
133 includeSemanticTags: true,
134 includeStateTags: true,
135 includeListIndentation: true,
136 includeListMarkers: false
137 },
138 entries: new Map()
139 };
140
141 function isElementNode(value) {
142 return Boolean(value && value.nodeType === 1);
143 }
144
145 function isTextNode(value) {
146 return Boolean(value && value.nodeType === 3);
147 }
148
149 function normalizeText(value) {
150 return String(value ?? "")
151 .replace(/\s+/gu, " ")
152 .trim();
153 }
154
155 function looksLikeSerializedHtmlText(value) {
156 const normalizedValue = normalizeText(value);
157 if (!normalizedValue || !normalizedValue.includes("<") || !normalizedValue.includes(">")) {
158 return false;
159 }
160
161 if (/<!(?:doctype|--)\b/iu.test(normalizedValue)) {
162 return true;
163 }
164
165 if (/<\/?(?:style|script)\b[\s\S]*?>/iu.test(normalizedValue)) {
166 return true;
167 }
168
169 const tagMatches = normalizedValue.match(/<\/?[a-z][^>]*>/giu) || [];
170 return tagMatches.length >= 3 && normalizedValue.length >= 80;
171 }
172
173 function looksLikeBrowserHelperMarkupText(value) {
174 const normalizedValue = normalizeText(value);
175 if (!normalizedValue) {
176 return false;
177 }
178
179 return /space-browser-(?:frame-document|shadow-root)/iu.test(normalizedValue)
180 || /data-space-browser-(?:frame|node|status|frame-url|frame-title|frame-src)/iu.test(normalizedValue);
181 }
182
183 function looksLikeMinifiedScriptText(value) {
184 const normalizedValue = normalizeText(value);
185 if (!normalizedValue || normalizedValue.length < 400) {
186 return false;
187 }
188
189 const jsSignals = [
190 /\bfunction\b/u,
191 /\breturn\b/u,
192 /\bvar\b/u,
193 /\bnew\b/u,
194 /\bcase\b/u,
195 /\bswitch\b/u,
196 /\bwhile\b/u,
197 /\bfor\b/u,
198 /\b(?:localStorage|postMessage|document\.|window\.|parent\.)/u,
199 /\bthis\./u,
200 /(?:&&|\|\||>>>|!==|===)/u
201 ].reduce((count, pattern) => count + (pattern.test(normalizedValue) ? 1 : 0), 0);
202
203 if (jsSignals < 4) {
204 return false;
205 }
206
207 const punctuationCount = (normalizedValue.match(/[{}[\]();=<>\\]/gu) || []).length;
208 return punctuationCount / normalizedValue.length >= 0.12;
209 }
210
211 function shouldDropReadableText(value) {
212 const normalizedValue = normalizeText(value);
213 if (!normalizedValue) {
214 return true;
215 }
216
217 return looksLikeBrowserHelperMarkupText(normalizedValue)
218 || looksLikeSerializedHtmlText(normalizedValue)
219 || looksLikeMinifiedScriptText(normalizedValue);
220 }
221
222 function normalizeAttributeText(value) {
223 return normalizeText(value).slice(0, 160);
224 }
225
226 function escapeMarkdownText(value) {
227 return String(value ?? "").replace(/([\\`*_{}\[\]()#+\-!|>])/gu, "\\$1");
228 }
229
230 function quoteText(value) {
231 return JSON.stringify(String(value ?? ""));
232 }
233
234 function truncateText(value, maxLength = 120) {
235 const normalizedValue = normalizeText(value);
236 if (normalizedValue.length <= maxLength) {
237 return normalizedValue;
238 }
239
240 return `${normalizedValue.slice(0, Math.max(0, maxLength - 1)).trimEnd()}...`;
241 }
242
243 function delayMs(timeoutMs) {
244 return new Promise((resolve) => {
245 globalThis.setTimeout(resolve, Math.max(0, Number(timeoutMs) || 0));
246 });
247 }
248
249 function parseCssColor(value) {
250 const normalizedValue = normalizeText(value);
251 if (!normalizedValue || normalizedValue === "transparent") {
252 return null;
253 }
254
255 const rgbMatch = normalizedValue.match(/^rgba?\(([^)]+)\)$/iu);
256 if (rgbMatch) {
257 const parts = rgbMatch[1]
258 .split(",")
259 .map((part) => Number.parseFloat(String(part || "").trim()))
260 .filter((part) => Number.isFinite(part));
261 if (parts.length >= 3) {
262 return {
263 r: Math.max(0, Math.min(255, parts[0])),
264 g: Math.max(0, Math.min(255, parts[1])),
265 b: Math.max(0, Math.min(255, parts[2])),
266 a: parts.length >= 4 ? Math.max(0, Math.min(1, parts[3])) : 1
267 };
268 }
269 }
270
271 const hexMatch = normalizedValue.match(/^#([\da-f]{3,8})$/iu);
272 if (!hexMatch) {
273 return null;
274 }
275
276 const hex = hexMatch[1];
277 if (hex.length === 3 || hex.length === 4) {
278 const [r, g, b, a = "f"] = hex.split("");
279 return {
280 r: Number.parseInt(`${r}${r}`, 16),
281 g: Number.parseInt(`${g}${g}`, 16),
282 b: Number.parseInt(`${b}${b}`, 16),
283 a: Number.parseInt(`${a}${a}`, 16) / 255
284 };
285 }
286
287 if (hex.length === 6 || hex.length === 8) {
288 return {
289 r: Number.parseInt(hex.slice(0, 2), 16),
290 g: Number.parseInt(hex.slice(2, 4), 16),
291 b: Number.parseInt(hex.slice(4, 6), 16),
292 a: hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) / 255 : 1
293 };
294 }
295
296 return null;
297 }
298
299 function rgbToHsl(color) {
300 if (!color) {
301 return null;
302 }
303
304 const r = color.r / 255;
305 const g = color.g / 255;
306 const b = color.b / 255;
307 const max = Math.max(r, g, b);
308 const min = Math.min(r, g, b);
309 const delta = max - min;
310 const lightness = (max + min) / 2;
311 let hue = 0;
312 let saturation = 0;
313
314 if (delta > 0) {
315 saturation = delta / (1 - Math.abs(2 * lightness - 1));
316 if (max === r) {
317 hue = 60 * (((g - b) / delta) % 6);
318 } else if (max === g) {
319 hue = 60 * (((b - r) / delta) + 2);
320 } else {
321 hue = 60 * (((r - g) / delta) + 4);
322 }
323 }
324
325 if (hue < 0) {
326 hue += 360;
327 }
328
329 return {
330 hue,
331 lightness,
332 saturation
333 };
334 }
335
336 function isTrustedHtmlRequirementError(error) {
337 return /TrustedHTML/iu.test(String(error?.message || error || ""));
338 }
339
340 function joinBlocks(blocks) {
341 return blocks
342 .map((block) => String(block || "").trim())
343 .filter(Boolean)
344 .join("\n\n")
345 .trim();
346 }
347
348 function cleanReadableMarkdown(value) {
349 const lines = String(value || "")
350 .replace(/<style\\?>[\s\S]*?<\/style\\?>/giu, "")
351 .replace(/<script\\?>[\s\S]*?<\/script\\?>/giu, "")
352 .replace(/<space\\-browser\\-(?:frame\\-document|shadow\\-root)\b[\s\S]*?<\/space\\-browser\\-(?:frame\\-document|shadow\\-root)>/giu, "")
353 .split("\n");
354
355 const filteredLines = [];
356 let insideCodeFence = false;
357
358 lines.forEach((line) => {
359 const trimmedLine = String(line || "").trim();
360 if (trimmedLine.startsWith("```")) {
361 insideCodeFence = !insideCodeFence;
362 filteredLines.push(line);
363 return;
364 }
365
366 if (!trimmedLine || insideCodeFence) {
367 filteredLines.push(line);
368 return;
369 }
370
371 if (shouldDropReadableText(trimmedLine)) {
372 return;
373 }
374
375 filteredLines.push(line);
376 });
377
378 return filteredLines
379 .join("\n")
380 .replace(/\n{3,}/gu, "\n\n")
381 .trim();
382 }
383
384 function joinInlineParts(parts) {
385 return String(parts
386 .map((part) => String(part || "").trim())
387 .filter(Boolean)
388 .join(" "))
389 .replace(/\s+([,.;!?])/gu, "$1")
390 .replace(/([([{\u201c])\s+/gu, "$1")
391 .replace(/\s+([\])}\u201d])/gu, "$1")
392 .replace(/\s*\n\s*/gu, "\n")
393 .replace(/[ \t]+\n/gu, "\n")
394 .replace(/\n{3,}/gu, "\n\n")
395 .trim();
396 }
397
398 function indentBlock(text, level = 1) {
399 const prefix = " ".repeat(Math.max(0, level));
400 return String(text || "")
401 .split("\n")
402 .map((line) => `${prefix}${line}`)
403 .join("\n");
404 }
405
406 function createNamedError(name, message, details = {}) {
407 const error = new Error(message);
408 error.name = name;
409 Object.assign(error, details);
410 return error;
411 }
412
413 function coerceSelectorList(payload) {
414 if (typeof payload === "string") {
415 return [payload];
416 }
417
418 if (Array.isArray(payload?.selectors)) {
419 return payload.selectors;
420 }
421
422 if (typeof payload?.selectors === "string") {
423 return [payload.selectors];
424 }
425
426 if (Array.isArray(payload?.selector)) {
427 return payload.selector;
428 }
429
430 if (typeof payload?.selector === "string") {
431 return [payload.selector];
432 }
433
434 if (Array.isArray(payload)) {
435 return payload;
436 }
437
438 return [];
439 }
440
441 function normalizeSelectorList(payload) {
442 return coerceSelectorList(payload)
443 .map((selector) => String(selector || "").trim())
444 .filter(Boolean);
445 }
446
447 function normalizeIncludeLinkUrls(payload) {
448 return payload?.includeLinkUrls === true;
449 }
450
451 function normalizeIncludeLabelQuotes(payload) {
452 return payload?.includeLabelQuotes === true;
453 }
454
455 function normalizeIncludeListIndentation(payload) {
456 return payload?.includeListIndentation !== false;
457 }
458
459 function normalizeIncludeListMarkers(payload) {
460 return payload?.includeListMarkers === true;
461 }
462
463 function normalizeIncludeStateTags(payload) {
464 return payload?.includeStateTags !== false;
465 }
466
467 function normalizeIncludeSemanticTags(payload) {
468 return payload?.includeSemanticTags !== false;
469 }
470
471 function formatSummaryValue(value, options = {}) {
472 const normalizedValue = normalizeText(value);
473 if (!normalizedValue) {
474 return "";
475 }
476
477 if (options.includeLabelQuotes === true) {
478 return quoteText(normalizedValue);
479 }
480
481 return escapeMarkdownText(normalizedValue);
482 }
483
484 function normalizeFrameChain(value) {
485 const rawFrameChain = Array.isArray(value)
486 ? value
487 : typeof value === "string"
488 ? value.split(">")
489 : [];
490
491 return rawFrameChain
492 .map((entry) => String(entry || "").trim())
493 .filter(Boolean);
494 }
495
496 function getDomHelper() {
497 const helper = globalThis[DOM_HELPER_KEY];
498 if (
499 helper
500 && typeof helper.captureDocument === "function"
501 && typeof helper.detailNode === "function"
502 && typeof helper.clickNode === "function"
503 && typeof helper.typeNode === "function"
504 && typeof helper.submitNode === "function"
505 && typeof helper.typeSubmitNode === "function"
506 && typeof helper.scrollNode === "function"
507 ) {
508 return helper;
509 }
510
511 return null;
512 }
513
514 function requireDomHelper(actionLabel) {
515 const helper = getDomHelper();
516 if (helper) {
517 return helper;
518 }
519
520 throw createNamedError(
521 "BrowserPageContentHelperUnavailableError",
522 `Browser page content cannot ${actionLabel} without the desktop DOM helper.`,
523 {
524 code: "browser_page_content_dom_helper_unavailable",
525 details: {
526 action: String(actionLabel || "resolve")
527 }
528 }
529 );
530 }
531
532 function normalizeReferenceId(value) {
533 if (typeof value === "number" && Number.isFinite(value)) {
534 return String(Math.trunc(value));
535 }
536
537 if (typeof value === "string") {
538 return value.trim();
539 }
540
541 if (value && typeof value === "object") {
542 return normalizeReferenceId(value.referenceId ?? value.ref ?? value.id);
543 }
544
545 return "";
546 }
547
548 function getTagName(element) {
549 return String(element?.tagName || "").toUpperCase();
550 }
551
552 function expandSlotNodes(node) {
553 if (!isElementNode(node) || getTagName(node) !== "SLOT" || typeof node.assignedNodes !== "function") {
554 return [node];
555 }
556
557 try {
558 const assignedNodes = [...(node.assignedNodes({ flatten: true }) || [])].filter(Boolean);
559 if (assignedNodes.length) {
560 return assignedNodes.flatMap((assignedNode) => expandSlotNodes(assignedNode));
561 }
562 } catch {
563 // Fall through to the slot's fallback children.
564 }
565
566 return [...(node.childNodes || [])].flatMap((childNode) => expandSlotNodes(childNode));
567 }
568
569 function getReadableChildNodes(element) {
570 const shadowRoot = element?.shadowRoot;
571 if (shadowRoot) {
572 const shadowNodes = [...(shadowRoot.childNodes || [])].flatMap((childNode) => expandSlotNodes(childNode));
573 if (shadowNodes.length) {
574 return shadowNodes;
575 }
576 }
577
578 return [...(element?.childNodes || [])].flatMap((childNode) => expandSlotNodes(childNode));
579 }
580
581 function getReadableElementChildren(element) {
582 return getReadableChildNodes(element).filter((childNode) => isElementNode(childNode));
583 }
584
585 function getReadableNodeText(node) {
586 if (isTextNode(node)) {
587 return node.textContent || "";
588 }
589
590 if (!isElementNode(node) || isHiddenElement(node)) {
591 return "";
592 }
593
594 return getReadableChildNodes(node)
595 .map((childNode) => getReadableNodeText(childNode))
596 .filter(Boolean)
597 .join(" ");
598 }
599
600 function querySelectorAllDeep(selector, root = globalThis.document) {
601 const results = [];
602 const seen = new Set();
603
604 const addResult = (element) => {
605 if (element && !seen.has(element)) {
606 seen.add(element);
607 results.push(element);
608 }
609 };
610
611 const visitRoot = (scope) => {
612 if (!scope || typeof scope.querySelectorAll !== "function") {
613 return;
614 }
615
616 [...(scope.querySelectorAll(selector) || [])].forEach(addResult);
617 [...(scope.querySelectorAll("*") || [])].forEach((element) => {
618 if (element.shadowRoot) {
619 visitRoot(element.shadowRoot);
620 }
621 });
622 };
623
624 visitRoot(root);
625 return results;
626 }
627
628 function getAttributeNamesSafe(element) {
629 try {
630 if (typeof element?.getAttributeNames === "function") {
631 return element.getAttributeNames();
632 }
633
634 return [...(element?.attributes || [])]
635 .map((attribute) => String(attribute?.name || "").trim())
636 .filter(Boolean);
637 } catch {
638 return [];
639 }
640 }
641
642 function normalizeInteractiveEventName(value) {
643 return String(value || "")
644 .trim()
645 .toLowerCase()
646 .split(/[.:]/u, 1)[0];
647 }
648
649 function isGlobalOrDelegatedEventBinding(value) {
650 const parts = String(value || "")
651 .trim()
652 .toLowerCase()
653 .split(/[.:]/u)
654 .map((part) => part.trim())
655 .filter(Boolean);
656 return parts.includes("window")
657 || parts.includes("document")
658 || parts.includes("outside")
659 || parts.includes("away");
660 }
661
662 function isInteractiveEventName(value) {
663 return INTERACTIVE_EVENT_NAMES.has(normalizeInteractiveEventName(value));
664 }
665
666 function isInteractiveEventAttributeName(attributeName) {
667 const normalizedName = String(attributeName || "").trim().toLowerCase();
668 if (!normalizedName) {
669 return false;
670 }
671
672 if (normalizedName.startsWith("@")) {
673 if (isGlobalOrDelegatedEventBinding(normalizedName.slice(1))) {
674 return false;
675 }
676 return isInteractiveEventName(normalizedName.slice(1));
677 }
678
679 if (normalizedName.startsWith("x-on:") || normalizedName.startsWith("v-on:")) {
680 if (isGlobalOrDelegatedEventBinding(normalizedName.slice(5))) {
681 return false;
682 }
683 return isInteractiveEventName(normalizedName.slice(5));
684 }
685
686 if (normalizedName.startsWith("ng-")) {
687 return isInteractiveEventName(normalizedName.slice(3));
688 }
689
690 if (normalizedName.startsWith("on") && normalizedName.length > 2) {
691 return isInteractiveEventName(normalizedName.slice(2));
692 }
693
694 return false;
695 }
696
697 function hasHelperManagedNodeReference(element) {
698 return Boolean(normalizeAttributeText(element?.getAttribute?.("data-space-browser-node-id")));
699 }
700
701 function hasInteractiveEventHandlerAttribute(element) {
702 return getAttributeNamesSafe(element).some((attributeName) => {
703 return isInteractiveEventAttributeName(attributeName);
704 });
705 }
706
707 function hasInteractiveEventHandlerProperty(element) {
708 return INTERACTIVE_EVENT_PROPERTIES.some((propertyName) => {
709 return typeof element?.[propertyName] === "function";
710 });
711 }
712
713 function hasInteractiveEventHandler(element) {
714 return hasInteractiveEventHandlerAttribute(element) || hasInteractiveEventHandlerProperty(element);
715 }
716
717 function isStyleDeclarationHidden(styleValue) {
718 const normalizedStyleValue = String(styleValue || "")
719 .toLowerCase()
720 .replace(/\s+/gu, "");
721
722 if (!normalizedStyleValue) {
723 return false;
724 }
725
726 return /(?:^|;)display:none(?:;|$)/u.test(normalizedStyleValue)
727 || /(?:^|;)visibility:hidden(?:;|$)/u.test(normalizedStyleValue)
728 || /(?:^|;)visibility:collapse(?:;|$)/u.test(normalizedStyleValue)
729 || /(?:^|;)content-visibility:hidden(?:;|$)/u.test(normalizedStyleValue)
730 || /(?:^|;)opacity:0(?:\.0+)?(?:;|$)/u.test(normalizedStyleValue);
731 }
732
733 function isComputedStyleHidden(computedStyle) {
734 if (!computedStyle) {
735 return false;
736 }
737
738 const display = normalizeText(computedStyle.display).toLowerCase();
739 const visibility = normalizeText(computedStyle.visibility).toLowerCase();
740 const contentVisibility = normalizeText(computedStyle.contentVisibility).toLowerCase();
741 const opacity = Number(computedStyle.opacity || 1);
742
743 return display === "none"
744 || visibility === "hidden"
745 || visibility === "collapse"
746 || contentVisibility === "hidden"
747 || opacity <= 0;
748 }
749
750 function isEffectivelyHiddenByAncestor(element) {
751 let current = element;
752
753 while (isElementNode(current)) {
754 if (current.hidden || current.getAttribute?.("aria-hidden") === "true") {
755 return true;
756 }
757
758 if (isStyleDeclarationHidden(current.getAttribute?.("style"))) {
759 return true;
760 }
761
762 if (isComputedStyleHidden(getComputedStyleSafe(current))) {
763 return true;
764 }
765
766 current = current.parentElement;
767 }
768
769 return false;
770 }
771
772 function isHiddenElement(element) {
773 if (!isElementNode(element)) {
774 return true;
775 }
776
777 const tagName = getTagName(element);
778 if (SKIP_TAGS.has(tagName)) {
779 return true;
780 }
781
782 if (element.hidden || element.getAttribute?.("aria-hidden") === "true") {
783 return true;
784 }
785
786 if (tagName === "INPUT" && String(element.getAttribute?.("type") || "").toLowerCase() === "hidden") {
787 return true;
788 }
789
790 if (isStyleDeclarationHidden(element.getAttribute?.("style"))) {
791 return true;
792 }
793
794 const computedStyle = getComputedStyleSafe(element);
795 if (isComputedStyleHidden(computedStyle)) {
796 return true;
797 }
798
799 return isEffectivelyHiddenByAncestor(element.parentElement);
800 }
801
802 function isBlockElement(element) {
803 return BLOCK_TAGS.has(getTagName(element));
804 }
805
806 function isInteractiveElement(element) {
807 if (!isElementNode(element) || isHiddenElement(element)) {
808 return false;
809 }
810
811 if (hasHelperManagedNodeReference(element)) {
812 return true;
813 }
814
815 const tagName = getTagName(element);
816 if (tagName === "A" && element.hasAttribute?.("href")) {
817 return true;
818 }
819
820 if (tagName === "BUTTON" || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA" || tagName === "SUMMARY") {
821 return true;
822 }
823
824 if (String(element.getAttribute?.("contenteditable") || "").toLowerCase() === "true") {
825 return true;
826 }
827
828 const role = String(element.getAttribute?.("role") || "").trim().toLowerCase();
829 return INTERACTIVE_ROLES.has(role) || hasInteractiveEventHandler(element);
830 }
831
832 function isFileInputElement(element) {
833 return getTagName(element) === "INPUT"
834 && String(element.getAttribute?.("type") || element.type || "").toLowerCase() === "file";
835 }
836
837 function getAssociatedLabelFileInput(labelElement) {
838 if (getTagName(labelElement) !== "LABEL") {
839 return null;
840 }
841
842 if (isFileInputElement(labelElement.control)) {
843 return labelElement.control;
844 }
845
846 const descendantInput = labelElement.querySelector?.("input[type='file']");
847 if (isFileInputElement(descendantInput)) {
848 return descendantInput;
849 }
850
851 const forId = normalizeAttributeText(labelElement.getAttribute?.("for"));
852 if (!forId) {
853 return null;
854 }
855
856 return isFileInputElement(labelElement.ownerDocument?.getElementById?.(forId))
857 ? labelElement.ownerDocument.getElementById(forId)
858 : null;
859 }
860
861 function isFileInputLabel(element) {
862 if (getTagName(element) !== "LABEL" || isHiddenElement(element)) {
863 return false;
864 }
865
866 const input = getAssociatedLabelFileInput(element);
867 return Boolean(input && isHiddenElement(input));
868 }
869
870 function getComputedStyleSafe(element) {
871 try {
872 return globalThis.getComputedStyle?.(element) || null;
873 } catch {
874 return null;
875 }
876 }
877
878 function getElementRectSafe(element) {
879 try {
880 const rect = element?.getBoundingClientRect?.();
881 if (!rect) {
882 return null;
883 }
884
885 return {
886 height: Number(rect.height) || 0,
887 width: Number(rect.width) || 0,
888 x: Number(rect.x) || 0,
889 y: Number(rect.y) || 0
890 };
891 } catch {
892 return null;
893 }
894 }
895
896 function readSerializedTagList(element, attributeName) {
897 const rawValue = normalizeText(element?.getAttribute?.(attributeName));
898 if (!rawValue) {
899 return [];
900 }
901
902 return rawValue
903 .split(/\s+/u)
904 .map((part) => normalizeText(part))
905 .filter(Boolean);
906 }
907
908 function detectSemanticTone(element, computedStyle, metadata = {}) {
909 const opacity = Number(computedStyle?.opacity || 1);
910 const backgroundColor = parseCssColor(computedStyle?.backgroundColor || "");
911 const borderColor = parseCssColor(computedStyle?.borderTopColor || "");
912 const foregroundColor = parseCssColor(computedStyle?.color || "");
913 const isButtonLike = ["BUTTON", "INPUT", "SUMMARY"].includes(getTagName(element))
914 || ["button", "tab", "menuitem"].includes(String(element?.getAttribute?.("role") || "").trim().toLowerCase());
915
916 if (metadata.disabled || metadata.blocked || opacity <= 0.58) {
917 return "muted";
918 }
919
920 const preferredColor = [backgroundColor, borderColor, foregroundColor]
921 .filter((color) => color && color.a > 0.15)
922 .map((color) => ({
923 color,
924 hsl: rgbToHsl(color)
925 }))
926 .find((entry) => entry.hsl && entry.hsl.saturation >= 0.2);
927
928 if (!preferredColor) {
929 return "";
930 }
931
932 const {
933 hue,
934 lightness,
935 saturation
936 } = preferredColor.hsl;
937 if (saturation < 0.2) {
938 return "";
939 }
940
941 if ((hue >= 345 || hue < 20) && lightness >= 0.18 && lightness <= 0.82) {
942 return "error";
943 }
944
945 if (hue >= 20 && hue < 65 && lightness >= 0.2 && lightness <= 0.9) {
946 return "warning";
947 }
948
949 if (hue >= 65 && hue < 170 && lightness >= 0.16 && lightness <= 0.84) {
950 return "success";
951 }
952
953 if (hue >= 170 && hue < 280 && lightness >= 0.14 && lightness <= 0.82) {
954 if (isButtonLike && backgroundColor?.a > 0.2) {
955 return "primary";
956 }
957 return "";
958 }
959
960 return "";
961 }
962
963 function collectElementStateMetadata(element, options = {}) {
964 if (!isElementNode(element)) {
965 return {
966 descriptorTags: [],
967 semanticTags: [],
968 stateTags: []
969 };
970 }
971
972 const computedStyle = getComputedStyleSafe(element);
973 const rect = getElementRectSafe(element);
974 const tagName = getTagName(element);
975 const ariaDisabled = String(element.getAttribute?.("aria-disabled") || "").trim().toLowerCase() === "true";
976 const ariaBusy = String(element.getAttribute?.("aria-busy") || "").trim().toLowerCase() === "true";
977 const ariaChecked = String(element.getAttribute?.("aria-checked") || "").trim().toLowerCase() === "true";
978 const ariaCurrent = normalizeText(element.getAttribute?.("aria-current"));
979 const ariaInvalid = String(element.getAttribute?.("aria-invalid") || "").trim().toLowerCase() === "true";
980 const ariaPressed = String(element.getAttribute?.("aria-pressed") || "").trim().toLowerCase() === "true";
981 const ariaReadonly = String(element.getAttribute?.("aria-readonly") || "").trim().toLowerCase() === "true";
982 const ariaRequired = String(element.getAttribute?.("aria-required") || "").trim().toLowerCase() === "true";
983 const ariaSelected = String(element.getAttribute?.("aria-selected") || "").trim().toLowerCase() === "true";
984 const helperStateTags = readSerializedTagList(element, "data-space-browser-state-tags");
985 const helperSemanticTags = readSerializedTagList(element, "data-space-browser-semantic-tags");
986 const closestInert = typeof element.closest === "function" ? element.closest("[inert]") : null;
987 const pointerEventsNone = normalizeText(computedStyle?.pointerEvents || "").toLowerCase() === "none";
988 const disabled = Boolean(element.disabled || ariaDisabled || closestInert || helperStateTags.includes("disabled"));
989 const blocked = !disabled && (pointerEventsNone || helperStateTags.includes("blocked"));
990 const checked = Boolean(element.checked || ariaChecked || helperStateTags.includes("checked"));
991 const selected = tagName === "OPTION"
992 ? Boolean(element.selected)
993 : Boolean(ariaSelected || helperStateTags.includes("selected"));
994 const invalid = Boolean(ariaInvalid || helperStateTags.includes("invalid") || element.matches?.(":invalid"));
995 const readonly = Boolean(element.readOnly || ariaReadonly);
996 const required = Boolean(element.required || ariaRequired);
997 const expanded = String(element.getAttribute?.("aria-expanded") || "").trim().toLowerCase() === "true" || helperStateTags.includes("expanded");
998 const pressed = ariaPressed || helperStateTags.includes("pressed");
999 const busy = ariaBusy || helperStateTags.includes("busy");
1000 const current = Boolean((ariaCurrent && ariaCurrent !== "false") || helperStateTags.includes("current"));
1001 const zeroRect = Boolean(
1002 rect
1003 && element.ownerDocument === globalThis.document
1004 && rect.width <= 1
1005 && rect.height <= 1
1006 );
1007 const opacity = Number(computedStyle?.opacity || 1);
1008 const semanticTone = helperSemanticTags[0] || detectSemanticTone(element, computedStyle, {
1009 blocked,
1010 disabled
1011 });
1012 const stateTags = helperStateTags.length
1013 ? helperStateTags.slice()
1014 : [
1015 disabled ? "disabled" : "",
1016 !disabled && (blocked || zeroRect) ? "blocked" : "",
1017 checked ? "checked" : "",
1018 selected && tagName !== "SELECT" ? "selected" : "",
1019 invalid ? "invalid" : "",
1020 expanded ? "expanded" : "",
1021 pressed ? "pressed" : ""
1022 ].filter(Boolean);
1023
1024 const semanticTags = helperSemanticTags.length
1025 ? helperSemanticTags.slice(0, 1)
1026 : (semanticTone ? [semanticTone] : []);
1027 const descriptorTags = [
1028 ...(options.includeStateTags !== false ? stateTags : []),
1029 ...(options.includeSemanticTags !== false ? semanticTags : [])
1030 ];
1031
1032 return {
1033 blocked,
1034 busy,
1035 checked,
1036 current,
1037 cursor: normalizeText(computedStyle?.cursor || "").toLowerCase(),
1038 descriptorTags,
1039 disabled,
1040 expanded,
1041 invalid,
1042 opacity,
1043 pointerEventsNone,
1044 pressed,
1045 readonly,
1046 required,
1047 selected,
1048 semanticTags,
1049 semanticTone,
1050 stateTags,
1051 visible: !isHiddenElement(element),
1052 zeroRect
1053 };
1054 }
1055
1056 function getReferenceValueMetadata(element) {
1057 const tagName = getTagName(element);
1058 const helperLiveValue = normalizeText(element?.getAttribute?.("data-space-browser-live-value"));
1059 const helperSelectedValue = normalizeText(element?.getAttribute?.("data-space-browser-selected-text"));
1060 if (tagName === "INPUT") {
1061 const inputType = String(element.getAttribute?.("type") || element.type || "text").toLowerCase();
1062 if (inputType === "password") {
1063 return "";
1064 }
1065 return truncateText(helperLiveValue || element.value || element.getAttribute?.("value") || "", 96);
1066 }
1067
1068 if (tagName === "TEXTAREA") {
1069 return truncateText(helperLiveValue || element.value || "", 96);
1070 }
1071
1072 if (tagName === "SELECT") {
1073 if (helperSelectedValue) {
1074 return helperSelectedValue;
1075 }
1076 const selectedOptions = [...(element.selectedOptions || [])]
1077 .map((option) => truncateText(option.textContent || option.label || option.value || "", 48))
1078 .filter(Boolean);
1079 return selectedOptions.join(" | ");
1080 }
1081
1082 if (String(element.getAttribute?.("contenteditable") || "").toLowerCase() === "true") {
1083 return truncateText(element.textContent || "", 96);
1084 }
1085
1086 return "";
1087 }
1088
1089 function collectMetaLines(doc = globalThis.document) {
1090 const lines = [];
1091 const title = normalizeAttributeText(doc?.title || "");
1092 const description = normalizeAttributeText(
1093 doc?.querySelector?.('meta[name="description"]')?.getAttribute?.("content") || ""
1094 );
1095 const url = String(globalThis.location?.href || "");
1096
1097 if (!title && !description && !url) {
1098 return "";
1099 }
1100
1101 lines.push("---");
1102 if (title) {
1103 lines.push(`title: ${quoteText(title)}`);
1104 }
1105 if (description) {
1106 lines.push(`description: ${quoteText(description)}`);
1107 }
1108 if (url) {
1109 lines.push(`url: ${quoteText(url)}`);
1110 }
1111 lines.push("---");
1112 return lines.join("\n");
1113 }
1114
1115 function summarizeUrl(value) {
1116 const normalizedValue = String(value || "").trim();
1117 if (!normalizedValue) {
1118 return "";
1119 }
1120
1121 try {
1122 const url = new URL(normalizedValue, globalThis.location?.href || "http://localhost/");
1123 if (url.origin === globalThis.location?.origin) {
1124 const relative = `${url.pathname || "/"}${url.search || ""}${url.hash || ""}`;
1125 return truncateText(relative || "/", 96);
1126 }
1127
1128 return truncateText(`${url.hostname}${url.pathname || "/"}`, 96);
1129 } catch {
1130 return truncateText(normalizedValue, 96);
1131 }
1132 }
1133
1134 function getElementText(element) {
1135 const readableText = normalizeText(getReadableNodeText(element));
1136 return readableText || normalizeText(element?.textContent || "");
1137 }
1138
1139 function isLabelableControlForText(element) {
1140 return ["BUTTON", "INPUT", "METER", "OUTPUT", "PROGRESS", "SELECT", "TEXTAREA"].includes(getTagName(element));
1141 }
1142
1143 function getLabelElementText(labelElement, controlElement = null) {
1144 const collect = (node) => {
1145 if (isTextNode(node)) {
1146 return node.textContent || "";
1147 }
1148
1149 if (!isElementNode(node) || isHiddenElement(node)) {
1150 return "";
1151 }
1152
1153 if (node !== labelElement && (node === controlElement || isLabelableControlForText(node))) {
1154 return "";
1155 }
1156
1157 return getReadableChildNodes(node)
1158 .map((childNode) => collect(childNode))
1159 .filter(Boolean)
1160 .join(" ");
1161 };
1162
1163 return normalizeText(collect(labelElement)) || getElementText(labelElement);
1164 }
1165
1166 function collectLabelCandidates(element, options = {}) {
1167 const includeAlt = options.includeAlt !== false;
1168 const includeDescendantImageAlt = options.includeDescendantImageAlt !== false;
1169 const includePlaceholder = options.includePlaceholder === true;
1170 const includeText = options.includeText !== false;
1171 const collectedLabels = [];
1172
1173 try {
1174 if (Array.isArray(element?.labels) || typeof element?.labels?.forEach === "function") {
1175 element.labels.forEach((labelElement) => {
1176 const text = getLabelElementText(labelElement, element);
1177 if (text) {
1178 collectedLabels.push(text);
1179 }
1180 });
1181 }
1182 } catch {
1183 // Ignore labels lookup failures from non-form elements.
1184 }
1185
1186 [
1187 element?.getAttribute?.("aria-label"),
1188 element?.getAttribute?.("title")
1189 ].forEach((candidate) => {
1190 const text = normalizeAttributeText(candidate);
1191 if (text) {
1192 collectedLabels.push(text);
1193 }
1194 });
1195
1196 if (includeAlt) {
1197 const altText = normalizeAttributeText(element?.getAttribute?.("alt"));
1198 if (altText) {
1199 collectedLabels.push(altText);
1200 }
1201 }
1202
1203 if (includePlaceholder) {
1204 const placeholderText = normalizeAttributeText(element?.getAttribute?.("placeholder"));
1205 if (placeholderText) {
1206 collectedLabels.push(placeholderText);
1207 }
1208 }
1209
1210 if (includeDescendantImageAlt) {
1211 try {
1212 [...(element?.querySelectorAll?.("img[alt], img[title]") || [])]
1213 .slice(0, 3)
1214 .forEach((mediaElement) => {
1215 const text = normalizeAttributeText(
1216 mediaElement.getAttribute?.("alt")
1217 || mediaElement.getAttribute?.("title")
1218 );
1219 if (text) {
1220 collectedLabels.push(text);
1221 }
1222 });
1223 } catch {
1224 // Ignore descendant-media lookup failures.
1225 }
1226 }
1227
1228 if (includeText) {
1229 const textContent = getElementText(element);
1230 if (textContent) {
1231 collectedLabels.push(textContent);
1232 }
1233 }
1234
1235 return [...new Set(collectedLabels.filter(Boolean))];
1236 }
1237
1238 function getLabelText(element, options = {}) {
1239 return collectLabelCandidates(element, options)[0] || "";
1240 }
1241
1242 function serializeElementSnapshot(element) {
1243 if (!isElementNode(element)) {
1244 return "";
1245 }
1246
1247 try {
1248 if (typeof element.outerHTML === "string" && element.outerHTML) {
1249 return element.outerHTML;
1250 }
1251 } catch {
1252 // Fall through to XMLSerializer.
1253 }
1254
1255 try {
1256 if (typeof globalThis.XMLSerializer === "function") {
1257 return new globalThis.XMLSerializer().serializeToString(element);
1258 }
1259 } catch {
1260 // Ignore serialization errors.
1261 }
1262
1263 return "";
1264 }
1265
1266 function getReferenceKind(element) {
1267 const tagName = getTagName(element);
1268 const role = String(element.getAttribute?.("role") || "").trim().toLowerCase();
1269 const inputType = String(element.getAttribute?.("type") || element.type || "text").toLowerCase();
1270
1271 if (tagName === "A" || role === "link") {
1272 return "link";
1273 }
1274
1275 if (tagName === "IMG") {
1276 return "image";
1277 }
1278
1279 if (tagName === "BUTTON" || ["button", "menuitem", "tab"].includes(role)) {
1280 return "button";
1281 }
1282
1283 if (tagName === "TEXTAREA") {
1284 return "textarea";
1285 }
1286
1287 if (tagName === "SELECT" || role === "combobox") {
1288 return "select";
1289 }
1290
1291 if (tagName === "SUMMARY") {
1292 return "summary";
1293 }
1294
1295 if (tagName === "INPUT") {
1296 if (["button", "submit", "reset"].includes(inputType)) {
1297 return "button";
1298 }
1299
1300 if (inputType === "checkbox") {
1301 return "checkbox";
1302 }
1303
1304 if (inputType === "radio") {
1305 return "radio";
1306 }
1307
1308 return `input ${inputType || "text"}`;
1309 }
1310
1311 if (tagName === "LABEL" && isFileInputLabel(element)) {
1312 return "file input label";
1313 }
1314
1315 if (String(element.getAttribute?.("contenteditable") || "").toLowerCase() === "true") {
1316 return "editable";
1317 }
1318
1319 if (role === "searchbox") {
1320 return "input search";
1321 }
1322
1323 if (role === "textbox") {
1324 return "input text";
1325 }
1326
1327 if (hasHelperManagedNodeReference(element) || hasInteractiveEventHandler(element)) {
1328 return "button";
1329 }
1330
1331 return role || tagName.toLowerCase();
1332 }
1333
1334 function collectReferenceSummaryData(element, options = {}) {
1335 const tagName = getTagName(element);
1336 const role = String(element.getAttribute?.("role") || "").trim().toLowerCase();
1337 const id = normalizeAttributeText(element.getAttribute?.("id"));
1338 const name = normalizeAttributeText(element.getAttribute?.("name"));
1339 const kind = getReferenceKind(element);
1340 const stateMetadata = collectElementStateMetadata(element, options);
1341 const formatValue = (value) => formatSummaryValue(value, options);
1342 const includeLinkUrls = options.includeLinkUrls === true;
1343 const parts = [];
1344 const appendFallbackIdOrName = () => {
1345 if (id) {
1346 parts.push(`#${id}`);
1347 return;
1348 }
1349
1350 if (name) {
1351 parts.push(`name=${formatValue(name)}`);
1352 }
1353 };
1354
1355 if (tagName === "A" || role === "link") {
1356 const hrefSummary = summarizeUrl(element.getAttribute?.("href") || element.href || "");
1357 const label = truncateText(getLabelText(element, {
1358 includeAlt: false,
1359 includeDescendantImageAlt: true,
1360 includePlaceholder: false,
1361 includeText: true
1362 }), 120);
1363 const displayLabel = label || hrefSummary;
1364
1365 if (displayLabel) {
1366 parts.push(formatValue(displayLabel));
1367 } else {
1368 appendFallbackIdOrName();
1369 }
1370
1371 if (includeLinkUrls) {
1372 if (hrefSummary && hrefSummary !== displayLabel) {
1373 parts.push(`-> ${hrefSummary}`);
1374 }
1375 }
1376 } else if (tagName === "BUTTON" || ["button", "menuitem", "tab"].includes(role)) {
1377 const label = truncateText(getLabelText(element, {
1378 includeAlt: false,
1379 includeDescendantImageAlt: true,
1380 includePlaceholder: false,
1381 includeText: true
1382 }), 120);
1383 if (label) {
1384 parts.push(formatValue(label));
1385 } else {
1386 appendFallbackIdOrName();
1387 }
1388 } else if (tagName === "TEXTAREA" || role === "textbox" || role === "searchbox") {
1389 const label = truncateText(getLabelText(element, {
1390 includeAlt: false,
1391 includeDescendantImageAlt: false,
1392 includePlaceholder: false,
1393 includeText: true
1394 }), 120);
1395 if (label) {
1396 parts.push(formatValue(label));
1397 }
1398 const placeholder = normalizeAttributeText(element.getAttribute?.("placeholder"));
1399 if (placeholder) {
1400 parts.push(`placeholder=${formatValue(placeholder)}`);
1401 } else if (!label) {
1402 appendFallbackIdOrName();
1403 }
1404 } else if (tagName === "SELECT" || role === "combobox") {
1405 const label = truncateText(getLabelText(element, {
1406 includeAlt: false,
1407 includeDescendantImageAlt: false,
1408 includePlaceholder: false,
1409 includeText: true
1410 }), 120);
1411 if (label) {
1412 parts.push(formatValue(label));
1413 } else {
1414 appendFallbackIdOrName();
1415 }
1416
1417 const selectedValue = getReferenceValueMetadata(element);
1418 const selectedOptions = selectedValue
1419 ? [selectedValue]
1420 : [...(element.selectedOptions || [])]
1421 .map((option) => truncateText(option.textContent || "", 48))
1422 .filter(Boolean);
1423 if (selectedOptions.length) {
1424 parts.push(`selected=${formatValue(selectedOptions.join(" | "))}`);
1425 }
1426 } else if (tagName === "SUMMARY") {
1427 const label = truncateText(getLabelText(element, {
1428 includeAlt: false,
1429 includeDescendantImageAlt: true,
1430 includePlaceholder: false,
1431 includeText: true
1432 }), 120);
1433 if (label) {
1434 parts.push(formatValue(label));
1435 } else {
1436 appendFallbackIdOrName();
1437 }
1438 } else if (tagName === "INPUT") {
1439 const inputType = String(element.getAttribute?.("type") || element.type || "text").toLowerCase();
1440 if (["button", "submit", "reset"].includes(inputType)) {
1441 const label = truncateText(getLabelText(element, {
1442 includeAlt: false,
1443 includeDescendantImageAlt: false,
1444 includePlaceholder: false,
1445 includeText: false
1446 }) || element.value || "", 120);
1447 if (label) {
1448 parts.push(formatValue(label));
1449 } else {
1450 appendFallbackIdOrName();
1451 }
1452 } else if (["checkbox", "radio"].includes(inputType)) {
1453 const label = truncateText(getLabelText(element, {
1454 includeAlt: false,
1455 includeDescendantImageAlt: false,
1456 includePlaceholder: false,
1457 includeText: false
1458 }), 120);
1459 if (label) {
1460 parts.push(formatValue(label));
1461 } else {
1462 appendFallbackIdOrName();
1463 }
1464 } else if (inputType === "file") {
1465 const label = truncateText(getLabelText(element, {
1466 includeAlt: false,
1467 includeDescendantImageAlt: false,
1468 includePlaceholder: false,
1469 includeText: false
1470 }), 120);
1471 if (label) {
1472 parts.push(formatValue(label));
1473 } else {
1474 appendFallbackIdOrName();
1475 }
1476 } else {
1477 const label = truncateText(getLabelText(element, {
1478 includeAlt: false,
1479 includeDescendantImageAlt: false,
1480 includePlaceholder: false,
1481 includeText: false
1482 }), 120);
1483 if (label) {
1484 parts.push(formatValue(label));
1485 }
1486
1487 const placeholder = normalizeAttributeText(element.getAttribute?.("placeholder"));
1488 const value = inputType === "password"
1489 ? ""
1490 : getReferenceValueMetadata(element);
1491
1492 if (placeholder) {
1493 parts.push(`placeholder=${formatValue(placeholder)}`);
1494 }
1495 if (value) {
1496 parts.push(`value=${formatValue(value)}`);
1497 }
1498 if (!label && !placeholder && !value) {
1499 appendFallbackIdOrName();
1500 }
1501 }
1502 } else if (String(element.getAttribute?.("contenteditable") || "").toLowerCase() === "true") {
1503 const label = truncateText(getLabelText(element, {
1504 includeAlt: false,
1505 includeDescendantImageAlt: false,
1506 includePlaceholder: false,
1507 includeText: true
1508 }), 120);
1509 if (label) {
1510 parts.push(formatValue(label));
1511 } else {
1512 appendFallbackIdOrName();
1513 }
1514 } else if (tagName === "IMG") {
1515 const srcSummary = summarizeUrl(element.currentSrc || element.getAttribute?.("src") || element.src || "");
1516 const label = truncateText(getLabelText(element, {
1517 includeAlt: true,
1518 includeDescendantImageAlt: false,
1519 includePlaceholder: false,
1520 includeText: false
1521 }), 120);
1522 const displayLabel = label || srcSummary;
1523 if (displayLabel) {
1524 parts.push(formatValue(displayLabel));
1525 } else {
1526 appendFallbackIdOrName();
1527 }
1528 } else if (role) {
1529 const label = truncateText(getLabelText(element, {
1530 includeAlt: false,
1531 includeDescendantImageAlt: true,
1532 includePlaceholder: false,
1533 includeText: true
1534 }), 120);
1535 if (label) {
1536 parts.push(formatValue(label));
1537 } else {
1538 appendFallbackIdOrName();
1539 }
1540 } else {
1541 const label = truncateText(getLabelText(element, {
1542 includeAlt: false,
1543 includeDescendantImageAlt: true,
1544 includePlaceholder: false,
1545 includeText: true
1546 }), 120);
1547 if (label) {
1548 parts.push(formatValue(label));
1549 } else {
1550 appendFallbackIdOrName();
1551 }
1552 }
1553
1554 return {
1555 descriptorTags: stateMetadata.descriptorTags.slice(),
1556 kind,
1557 semanticTags: stateMetadata.semanticTags.slice(),
1558 state: stateMetadata,
1559 summary: parts.filter(Boolean).join(" ")
1560 };
1561 }
1562
1563 function createReferenceEntry(element, referenceId, options = {}) {
1564 const nodeId = normalizeAttributeText(element.getAttribute?.("data-space-browser-node-id"));
1565 const frameId = normalizeAttributeText(element.getAttribute?.("data-space-browser-frame-id"));
1566 const frameChain = normalizeFrameChain(element.getAttribute?.("data-space-browser-frame-chain"));
1567 const helperBacked = Boolean(nodeId && frameChain.length);
1568 const summaryData = collectReferenceSummaryData(element, options);
1569
1570 return {
1571 connected: helperBacked ? true : element.isConnected !== false,
1572 dom: serializeElementSnapshot(element),
1573 descriptorTags: summaryData.descriptorTags,
1574 element: helperBacked ? null : element,
1575 frameChain,
1576 frameId,
1577 helperBacked,
1578 id: normalizeAttributeText(element.getAttribute?.("id")),
1579 name: normalizeAttributeText(element.getAttribute?.("name")),
1580 nodeId,
1581 referenceId,
1582 kind: summaryData.kind,
1583 semanticTags: summaryData.semanticTags,
1584 state: summaryData.state,
1585 summary: summaryData.summary,
1586 tagName: getTagName(element)
1587 };
1588 }
1589
1590 function ensureReference(element, context) {
1591 if (context.referenceIdsByElement.has(element)) {
1592 return context.referenceIdsByElement.get(element);
1593 }
1594
1595 const referenceId = String(context.nextReferenceId++);
1596 const entry = createReferenceEntry(element, referenceId, context.options);
1597 context.referenceIdsByElement.set(element, referenceId);
1598 context.entries.set(referenceId, entry);
1599 return referenceId;
1600 }
1601
1602 function renderReference(element, context) {
1603 const referenceId = ensureReference(element, context);
1604 const entry = context.entries.get(referenceId);
1605 const kind = normalizeText(entry?.kind || getTagName(element).toLowerCase());
1606 const descriptorTags = Array.isArray(entry?.descriptorTags)
1607 ? entry.descriptorTags.map((tag) => normalizeText(tag)).filter(Boolean)
1608 : [];
1609 const summary = normalizeText(entry?.summary || "");
1610 const descriptor = [...descriptorTags, kind, referenceId].filter(Boolean).join(" ");
1611 return summary ? `[${descriptor}] ${summary}` : `[${descriptor}]`;
1612 }
1613
1614 function isReferenceableElement(element) {
1615 return isInteractiveElement(element) || getTagName(element) === "IMG" || isFileInputLabel(element);
1616 }
1617
1618 function collectLabelControlElements(labelElement) {
1619 const controls = [];
1620 const seen = new Set();
1621 const addControl = (element) => {
1622 if (!isElementNode(element) || seen.has(element) || !isReferenceableElement(element)) {
1623 return;
1624 }
1625
1626 seen.add(element);
1627 controls.push(element);
1628 };
1629
1630 [
1631 "input",
1632 "textarea",
1633 "select",
1634 "button",
1635 "summary",
1636 "a[href]",
1637 "[role]",
1638 "[contenteditable='true']",
1639 "[contenteditable='']"
1640 ].forEach((selector) => {
1641 try {
1642 [...(labelElement.querySelectorAll?.(selector) || [])].forEach(addControl);
1643 } catch {
1644 // Ignore unsupported selectors in unusual DOMs.
1645 }
1646 });
1647
1648 return controls;
1649 }
1650
1651 function renderControlLabelReferences(labelElement, context) {
1652 return collectLabelControlElements(labelElement)
1653 .map((controlElement) => renderReference(controlElement, context))
1654 .filter(Boolean)
1655 .join("\n");
1656 }
1657
1658 function renderInlineNode(node, context) {
1659 if (isTextNode(node)) {
1660 const textContent = normalizeText(node.textContent || "");
1661 if (shouldDropReadableText(textContent)) {
1662 return "";
1663 }
1664
1665 return escapeMarkdownText(textContent);
1666 }
1667
1668 if (!isElementNode(node) || isHiddenElement(node)) {
1669 return "";
1670 }
1671
1672 if (isReferenceableElement(node)) {
1673 return renderReference(node, context);
1674 }
1675
1676 const tagName = getTagName(node);
1677
1678 if (tagName === "LABEL" && (node.getAttribute?.("for") || node.querySelector?.("input, textarea, select, button"))) {
1679 return renderControlLabelReferences(node, context);
1680 }
1681
1682 if (tagName === "BR") {
1683 return "\n";
1684 }
1685
1686 if (tagName === "STRONG" || tagName === "B") {
1687 const content = renderInlineChildren(node, context);
1688 return content ? `**${content}**` : "";
1689 }
1690
1691 if (tagName === "EM" || tagName === "I") {
1692 const content = renderInlineChildren(node, context);
1693 return content ? `*${content}*` : "";
1694 }
1695
1696 if (tagName === "S" || tagName === "STRIKE" || tagName === "DEL") {
1697 const content = renderInlineChildren(node, context);
1698 return content ? `~~${content}~~` : "";
1699 }
1700
1701 if (tagName === "CODE") {
1702 const content = normalizeText(node.textContent || "");
1703 return content ? `\`${content.replace(/`/gu, "\\`")}\`` : "";
1704 }
1705
1706 return renderInlineChildren(node, context);
1707 }
1708
1709 function renderInlineChildren(element, context) {
1710 const parts = [];
1711
1712 getReadableChildNodes(element).forEach((childNode) => {
1713 const renderedChild = renderInlineNode(childNode, context);
1714 if (renderedChild) {
1715 parts.push(renderedChild);
1716 }
1717 });
1718
1719 return joinInlineParts(parts);
1720 }
1721
1722 function renderParagraph(element, context) {
1723 return renderInlineChildren(element, context);
1724 }
1725
1726 function renderHeading(element, context) {
1727 const level = Math.min(6, Math.max(1, Number.parseInt(getTagName(element).slice(1), 10) || 1));
1728 const content = renderInlineChildren(element, context);
1729 return content ? `${"#".repeat(level)} ${content}` : "";
1730 }
1731
1732 function renderCodeBlock(element) {
1733 const content = String(element.textContent || "").trimEnd();
1734 if (!content) {
1735 return "";
1736 }
1737
1738 return `\`\`\`\n${content.replace(/```/gu, "\\`\\`\\`")}\n\`\`\``;
1739 }
1740
1741 function renderBlockquote(element, context) {
1742 const content = renderBlockChildren(element, context);
1743 if (!content) {
1744 return "";
1745 }
1746
1747 return content
1748 .split("\n")
1749 .map((line) => `> ${line}`)
1750 .join("\n");
1751 }
1752
1753 function renderListItem(element, context, depth, index, ordered) {
1754 const includeListMarkers = context.options.includeListMarkers === true;
1755 const includeListIndentation = context.options.includeListIndentation !== false;
1756 const marker = includeListMarkers ? (ordered ? `${index + 1}.` : "-") : "";
1757 const indentation = includeListIndentation ? " ".repeat(Math.max(0, depth)) : "";
1758 const inlineParts = [];
1759 const nestedBlocks = [];
1760
1761 getReadableChildNodes(element).forEach((childNode) => {
1762 if (isElementNode(childNode) && (getTagName(childNode) === "UL" || getTagName(childNode) === "OL")) {
1763 const nestedList = renderList(childNode, context, depth + 1);
1764 if (nestedList) {
1765 nestedBlocks.push(nestedList);
1766 }
1767 return;
1768 }
1769
1770 const renderedChild = renderInlineNode(childNode, context);
1771 if (renderedChild) {
1772 inlineParts.push(renderedChild);
1773 }
1774 });
1775
1776 const head = joinInlineParts(inlineParts);
1777 const linePrefix = marker ? `${indentation}${marker} ` : indentation;
1778 const lines = [`${linePrefix}${head || "(empty)"}`];
1779 nestedBlocks.forEach((nestedBlock) => {
1780 lines.push(indentBlock(nestedBlock, includeListIndentation ? 1 : 0));
1781 });
1782 return lines.join("\n");
1783 }
1784
1785 function renderList(element, context, depth = 0) {
1786 const ordered = getTagName(element) === "OL";
1787 return getReadableElementChildren(element)
1788 .filter((child) => getTagName(child) === "LI" && !isHiddenElement(child))
1789 .map((item, index) => renderListItem(item, context, depth, index, ordered))
1790 .filter(Boolean)
1791 .join("\n");
1792 }
1793
1794 function renderTableCell(element, context) {
1795 return renderInlineChildren(element, context);
1796 }
1797
1798 function renderTable(element, context) {
1799 const rows = [...element.querySelectorAll?.(":scope > thead > tr, :scope > tbody > tr, :scope > tr, :scope > tfoot > tr") || []]
1800 .filter((row) => getTagName(row) === "TR");
1801
1802 if (!rows.length) {
1803 return "";
1804 }
1805
1806 const renderedRows = rows.map((row) => {
1807 return [...row.children]
1808 .filter((cell) => ["TD", "TH"].includes(getTagName(cell)) && !isHiddenElement(cell))
1809 .map((cell) => renderTableCell(cell, context));
1810 }).filter((cells) => cells.length);
1811
1812 if (!renderedRows.length) {
1813 return "";
1814 }
1815
1816 const columnCount = Math.max(...renderedRows.map((cells) => cells.length));
1817 const normalizedRows = renderedRows.map((cells) => {
1818 const nextCells = cells.slice();
1819 while (nextCells.length < columnCount) {
1820 nextCells.push("");
1821 }
1822 return nextCells;
1823 });
1824
1825 const headerRow = normalizedRows[0];
1826 const separatorRow = headerRow.map(() => "---");
1827 const tableLines = [
1828 `| ${headerRow.join(" | ")} |`,
1829 `| ${separatorRow.join(" | ")} |`
1830 ];
1831
1832 normalizedRows.slice(1).forEach((row) => {
1833 tableLines.push(`| ${row.join(" | ")} |`);
1834 });
1835
1836 return tableLines.join("\n");
1837 }
1838
1839 function renderGenericContainer(element, context) {
1840 return renderBlockChildren(element, context);
1841 }
1842
1843 function renderElementAsBlock(element, context) {
1844 if (!isElementNode(element) || isHiddenElement(element)) {
1845 return "";
1846 }
1847
1848 if (isReferenceableElement(element)) {
1849 return renderReference(element, context);
1850 }
1851
1852 const tagName = getTagName(element);
1853
1854 if (tagName === "LABEL" && (element.getAttribute?.("for") || element.querySelector?.("input, textarea, select, button"))) {
1855 return renderControlLabelReferences(element, context);
1856 }
1857
1858 if (/^H[1-6]$/u.test(tagName)) {
1859 return renderHeading(element, context);
1860 }
1861
1862 if (tagName === "P") {
1863 return renderParagraph(element, context);
1864 }
1865
1866 if (tagName === "PRE") {
1867 return renderCodeBlock(element);
1868 }
1869
1870 if (tagName === "BLOCKQUOTE") {
1871 return renderBlockquote(element, context);
1872 }
1873
1874 if (tagName === "UL" || tagName === "OL") {
1875 return renderList(element, context);
1876 }
1877
1878 if (tagName === "TABLE") {
1879 return renderTable(element, context);
1880 }
1881
1882 if (tagName === "HR") {
1883 return "---";
1884 }
1885
1886 return renderGenericContainer(element, context);
1887 }
1888
1889 function renderBlockChildren(element, context) {
1890 const blocks = [];
1891 const inlineParts = [];
1892
1893 const flushInlineParts = () => {
1894 const inlineText = joinInlineParts(inlineParts.splice(0, inlineParts.length));
1895 if (inlineText) {
1896 blocks.push(inlineText);
1897 }
1898 };
1899
1900 getReadableChildNodes(element).forEach((childNode) => {
1901 if (isTextNode(childNode)) {
1902 const rawTextContent = normalizeText(childNode.textContent || "");
1903 if (shouldDropReadableText(rawTextContent)) {
1904 return;
1905 }
1906
1907 const textContent = escapeMarkdownText(rawTextContent);
1908 if (textContent) {
1909 inlineParts.push(textContent);
1910 }
1911 return;
1912 }
1913
1914 if (!isElementNode(childNode) || isHiddenElement(childNode)) {
1915 return;
1916 }
1917
1918 const renderedChild = renderElementAsBlock(childNode, context);
1919 if (!renderedChild) {
1920 return;
1921 }
1922
1923 if (isBlockElement(childNode) || isReferenceableElement(childNode)) {
1924 flushInlineParts();
1925 blocks.push(renderedChild);
1926 return;
1927 }
1928
1929 inlineParts.push(renderedChild);
1930 });
1931
1932 flushInlineParts();
1933 return joinBlocks(blocks);
1934 }
1935
1936 function createCaptureContext(payload = null) {
1937 return {
1938 entries: new Map(),
1939 nextReferenceId: 1,
1940 options: {
1941 includeLabelQuotes: normalizeIncludeLabelQuotes(payload),
1942 includeLinkUrls: normalizeIncludeLinkUrls(payload),
1943 includeSemanticTags: normalizeIncludeSemanticTags(payload),
1944 includeStateTags: normalizeIncludeStateTags(payload),
1945 includeListIndentation: normalizeIncludeListIndentation(payload),
1946 includeListMarkers: normalizeIncludeListMarkers(payload)
1947 },
1948 referenceIdsByElement: new WeakMap()
1949 };
1950 }
1951
1952 function resolveSelectorTargets(payload, doc = globalThis.document) {
1953 const selectors = normalizeSelectorList(payload);
1954 if (!selectors.length) {
1955 return {
1956 includeMetaData: true,
1957 items: [
1958 {
1959 key: "document",
1960 targets: [doc?.body || doc?.documentElement].filter(Boolean)
1961 }
1962 ]
1963 };
1964 }
1965
1966 return {
1967 includeMetaData: false,
1968 items: selectors.map((selector) => {
1969 let targets = [];
1970 try {
1971 targets = doc === globalThis.document
1972 ? querySelectorAllDeep(selector, doc)
1973 : [...(doc?.querySelectorAll?.(selector) || [])];
1974 } catch (error) {
1975 throw createNamedError(
1976 "BrowserPageContentSelectorError",
1977 `Browser page content could not resolve selector "${selector}".`,
1978 {
1979 code: "browser_page_content_selector_error",
1980 details: {
1981 selector
1982 },
1983 cause: error
1984 }
1985 );
1986 }
1987
1988 return {
1989 key: selector,
1990 targets
1991 };
1992 })
1993 };
1994 }
1995
1996 function parseSnapshotFragment(html, parser) {
1997 return parser.parseFromString(
1998 `<!DOCTYPE html><html><body>${String(html || "")}</body></html>`,
1999 "text/html"
2000 );
2001 }
2002
2003 function renderSnapshotFragment(html, captureContext, parser) {
2004 const parsedDocument = parseSnapshotFragment(html, parser);
2005 const blocks = [];
2006 const inlineParts = [];
2007
2008 const flushInlineParts = () => {
2009 const inlineText = joinInlineParts(inlineParts.splice(0, inlineParts.length));
2010 if (inlineText) {
2011 blocks.push(inlineText);
2012 }
2013 };
2014
2015 parsedDocument.body.childNodes.forEach((childNode) => {
2016 if (isTextNode(childNode)) {
2017 const rawTextContent = normalizeText(childNode.textContent || "");
2018 if (shouldDropReadableText(rawTextContent)) {
2019 return;
2020 }
2021
2022 const textContent = escapeMarkdownText(rawTextContent);
2023 if (textContent) {
2024 inlineParts.push(textContent);
2025 }
2026 return;
2027 }
2028
2029 if (!isElementNode(childNode) || isHiddenElement(childNode)) {
2030 return;
2031 }
2032
2033 const renderedChild = renderElementAsBlock(childNode, captureContext);
2034 if (!renderedChild) {
2035 return;
2036 }
2037
2038 if (isBlockElement(childNode) || isReferenceableElement(childNode)) {
2039 flushInlineParts();
2040 blocks.push(renderedChild);
2041 return;
2042 }
2043
2044 inlineParts.push(renderedChild);
2045 });
2046
2047 flushInlineParts();
2048 return cleanReadableMarkdown(joinBlocks(blocks));
2049 }
2050
2051 function captureLive(payload = null) {
2052 const captureContext = createCaptureContext(payload);
2053 const resolvedTargets = resolveSelectorTargets(payload);
2054 const snapshot = {};
2055
2056 resolvedTargets.items.forEach((item) => {
2057 const blocks = [];
2058 if (resolvedTargets.includeMetaData && item.key === "document") {
2059 const meta = collectMetaLines(globalThis.document);
2060 if (meta) {
2061 blocks.push(meta);
2062 }
2063 }
2064
2065 item.targets.forEach((target) => {
2066 const renderedTarget = renderElementAsBlock(target, captureContext);
2067 if (renderedTarget) {
2068 blocks.push(renderedTarget);
2069 }
2070 });
2071
2072 snapshot[item.key] = cleanReadableMarkdown(joinBlocks(blocks));
2073 });
2074
2075 state.captureId += 1;
2076 state.capturedAt = Date.now();
2077 state.backend = "live";
2078 state.captureOptions = { ...captureContext.options };
2079 state.entries = captureContext.entries;
2080 return snapshot;
2081 }
2082
2083 async function captureWithDomHelper(payload = null) {
2084 const helper = requireDomHelper("capture content");
2085 const selectors = normalizeSelectorList(payload);
2086 const helperPayload = {
2087 snapshotMode: "content"
2088 };
2089 if (selectors.length) {
2090 helperPayload.selectors = selectors;
2091 }
2092 const documentSnapshot = await helper.captureDocument({
2093 ...helperPayload
2094 });
2095 const snapshot = {};
2096 const parser = new globalThis.DOMParser();
2097 const captureContext = createCaptureContext(payload);
2098 try {
2099 if (selectors.length && documentSnapshot?.targets && typeof documentSnapshot.targets === "object") {
2100 selectors.forEach((selector) => {
2101 snapshot[selector] = renderSnapshotFragment(documentSnapshot.targets?.[selector] || "", captureContext, parser);
2102 });
2103
2104 state.captureId += 1;
2105 state.capturedAt = Date.now();
2106 state.backend = "dom_helper";
2107 state.captureOptions = { ...captureContext.options };
2108 state.entries = captureContext.entries;
2109 return snapshot;
2110 }
2111
2112 const parsedDocument = parser.parseFromString(String(documentSnapshot?.html || ""), "text/html");
2113 const resolvedTargets = resolveSelectorTargets(payload, parsedDocument);
2114
2115 resolvedTargets.items.forEach((item) => {
2116 const blocks = [];
2117 if (resolvedTargets.includeMetaData && item.key === "document") {
2118 const meta = collectMetaLines(parsedDocument);
2119 if (meta) {
2120 blocks.push(meta);
2121 }
2122 }
2123
2124 item.targets.forEach((target) => {
2125 const renderedTarget = renderElementAsBlock(target, captureContext);
2126 if (renderedTarget) {
2127 blocks.push(renderedTarget);
2128 }
2129 });
2130
2131 snapshot[item.key] = cleanReadableMarkdown(joinBlocks(blocks));
2132 });
2133
2134 state.captureId += 1;
2135 state.capturedAt = Date.now();
2136 state.backend = "dom_helper";
2137 state.captureOptions = { ...captureContext.options };
2138 state.entries = captureContext.entries;
2139 return snapshot;
2140 } catch (error) {
2141 if (!isTrustedHtmlRequirementError(error)) {
2142 throw error;
2143 }
2144
2145 return captureLive(payload);
2146 }
2147 }
2148
2149 async function capture(payload = null) {
2150 if (getDomHelper()) {
2151 return captureWithDomHelper(payload);
2152 }
2153
2154 return captureLive(payload);
2155 }
2156
2157 function detailLive(entry) {
2158 const liveState = entry.connected && entry.element
2159 ? collectElementStateMetadata(entry.element, state.captureOptions)
2160 : entry.state || collectElementStateMetadata(null);
2161 return {
2162 captureId: state.captureId,
2163 capturedAt: state.capturedAt,
2164 connected: entry.connected,
2165 descriptorTags: liveState.descriptorTags,
2166 dom: entry.connected ? serializeElementSnapshot(entry.element) || entry.dom : entry.dom,
2167 referenceId: entry.referenceId,
2168 semanticTags: liveState.semanticTags,
2169 state: liveState,
2170 summary: entry.summary,
2171 tagName: entry.tagName
2172 };
2173 }
2174
2175 async function detail(referenceId) {
2176 const entry = requireReferenceEntry(referenceId, {
2177 actionLabel: "detail",
2178 requireConnected: false
2179 });
2180
2181 if (entry.helperBacked) {
2182 const helper = requireDomHelper("resolve detail");
2183 const resolvedDetail = await helper.detailNode(entry.frameChain, entry.nodeId);
2184 return {
2185 captureId: state.captureId,
2186 capturedAt: state.capturedAt,
2187 connected: resolvedDetail?.connected !== false,
2188 descriptorTags: Array.isArray(resolvedDetail?.descriptorTags) ? resolvedDetail.descriptorTags : (entry.descriptorTags || []),
2189 dom: String(resolvedDetail?.dom || entry.dom || ""),
2190 frameChain: entry.frameChain.slice(),
2191 frameId: entry.frameId,
2192 nodeId: entry.nodeId,
2193 referenceId: entry.referenceId,
2194 semanticTags: Array.isArray(resolvedDetail?.semanticTags) ? resolvedDetail.semanticTags : (entry.semanticTags || []),
2195 state: resolvedDetail?.state || entry.state || collectElementStateMetadata(null),
2196 summary: entry.summary,
2197 tagName: String(resolvedDetail?.tagName || entry.tagName || "")
2198 };
2199 }
2200
2201 return detailLive(entry);
2202 }
2203
2204 function requireReferenceEntry(referenceId, options = {}) {
2205 const normalizedReferenceId = normalizeReferenceId(referenceId);
2206 if (!normalizedReferenceId) {
2207 throw createNamedError(
2208 "BrowserPageContentReferenceError",
2209 "Browser page content requests require a reference id.",
2210 {
2211 code: "browser_page_content_reference_required",
2212 details: {
2213 action: String(options.actionLabel || "resolve")
2214 }
2215 }
2216 );
2217 }
2218
2219 if (!state.entries.size) {
2220 throw createNamedError(
2221 "BrowserPageContentReferenceError",
2222 `Browser page content has no reference capture for "${normalizedReferenceId}".`,
2223 {
2224 code: "browser_page_content_reference_missing_capture",
2225 details: {
2226 action: String(options.actionLabel || "resolve"),
2227 referenceId: normalizedReferenceId
2228 }
2229 }
2230 );
2231 }
2232
2233 const entry = state.entries.get(normalizedReferenceId);
2234 if (!entry) {
2235 throw createNamedError(
2236 "BrowserPageContentReferenceError",
2237 `Browser page content could not find reference "${normalizedReferenceId}".`,
2238 {
2239 code: "browser_page_content_reference_not_found",
2240 details: {
2241 action: String(options.actionLabel || "resolve"),
2242 referenceId: normalizedReferenceId
2243 }
2244 }
2245 );
2246 }
2247
2248 refreshReferenceEntry(entry);
2249
2250 if (options.requireConnected !== false && !entry.connected) {
2251 throw createNamedError(
2252 "BrowserPageContentReferenceError",
2253 `Browser page content reference "${normalizedReferenceId}" is no longer connected.`,
2254 {
2255 code: "browser_page_content_reference_disconnected",
2256 details: {
2257 action: String(options.actionLabel || "resolve"),
2258 referenceId: normalizedReferenceId
2259 }
2260 }
2261 );
2262 }
2263
2264 return entry;
2265 }
2266
2267 function computeStableSelector(el) {
2268 if (!el || el.nodeType !== 1) return null;
2269 const doc = el.ownerDocument || document;
2270 if (el.id && /^[A-Za-z_][\w-]*$/.test(el.id)) {
2271 const sel = "#" + (typeof CSS !== "undefined" && CSS.escape ? CSS.escape(el.id) : el.id);
2272 try {
2273 if (doc.querySelectorAll(sel).length === 1) return sel;
2274 } catch (_) {}
2275 }
2276 const parts = [];
2277 let node = el;
2278 while (node && node.nodeType === 1 && node !== doc.documentElement) {
2279 let part = node.tagName.toLowerCase();
2280 if (node.id && /^[A-Za-z_][\w-]*$/.test(node.id)) {
2281 const idSel = "#" + (typeof CSS !== "undefined" && CSS.escape ? CSS.escape(node.id) : node.id);
2282 try {
2283 if (doc.querySelectorAll(idSel).length === 1) {
2284 parts.unshift(idSel);
2285 break;
2286 }
2287 } catch (_) {}
2288 }
2289 const parent = node.parentElement;
2290 if (parent) {
2291 const sibs = parent.children;
2292 let idx = 0;
2293 let sameTag = 0;
2294 for (let i = 0; i < sibs.length; i++) {
2295 if (sibs[i].tagName === node.tagName) {
2296 sameTag++;
2297 if (sibs[i] === node) idx = sameTag;
2298 }
2299 }
2300 if (sameTag > 1) part += ":nth-of-type(" + idx + ")";
2301 }
2302 parts.unshift(part);
2303 node = parent;
2304 }
2305 const sel = parts.join(" > ");
2306 if (!sel) return null;
2307 try {
2308 if (doc.querySelectorAll(sel).length === 1) return sel;
2309 } catch (_) {}
2310 return null;
2311 }
2312
2313 function boundingBoxFor(referenceId) {
2314 const entry = requireReferenceEntry(referenceId, {
2315 actionLabel: "boundingBox",
2316 requireConnected: false
2317 });
2318 if (entry.helperBacked || !entry.element) return null;
2319 const el = entry.element;
2320 if (typeof el.getBoundingClientRect !== "function") return null;
2321 try {
2322 el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
2323 } catch (_) {}
2324 const r = el.getBoundingClientRect();
2325 const selector = computeStableSelector(el);
2326 const hasBox = r && r.width > 0 && r.height > 0;
2327 if (!hasBox && !selector) return null;
2328 return {
2329 x: hasBox ? r.left : 0,
2330 y: hasBox ? r.top : 0,
2331 width: hasBox ? r.width : 0,
2332 height: hasBox ? r.height : 0,
2333 selector: selector || null
2334 };
2335 }
2336
2337 function pointFor(referenceId, offsets = {}) {
2338 const entry = requireReferenceEntry(referenceId, {
2339 actionLabel: "point",
2340 requireConnected: true
2341 });
2342 if (entry.helperBacked || !entry.element) {
2343 throw createNamedError(
2344 "BrowserPageContentActionError",
2345 `Browser page content cannot resolve point for helper-backed reference "${entry.referenceId}".`,
2346 {
2347 code: "browser_page_content_point_helper_backed"
2348 }
2349 );
2350 }
2351
2352 const element = entry.element;
2353 scrollElementIntoView(element);
2354 const rect = getElementRectSafe(element);
2355 if (!rect || rect.width <= 0 || rect.height <= 0) {
2356 throw createNamedError(
2357 "BrowserPageContentActionError",
2358 `Browser page content reference "${entry.referenceId}" has no visible viewport box.`,
2359 {
2360 code: "browser_page_content_point_no_box"
2361 }
2362 );
2363 }
2364
2365 const offsetX = Number(offsets?.offset_x ?? offsets?.offsetX ?? 0) || 0;
2366 const offsetY = Number(offsets?.offset_y ?? offsets?.offsetY ?? 0) || 0;
2367 const useOffsets = offsets?.useOffsets === true || offsetX !== 0 || offsetY !== 0;
2368 return {
2369 rect,
2370 selector: computeStableSelector(element),
2371 x: rect.x + (useOffsets ? offsetX : rect.width / 2),
2372 y: rect.y + (useOffsets ? offsetY : rect.height / 2)
2373 };
2374 }
2375
2376 function normalizeActionValues(valueOrValues) {
2377 if (Array.isArray(valueOrValues)) {
2378 return valueOrValues.map((value) => String(value ?? ""));
2379 }
2380
2381 if (valueOrValues === null || valueOrValues === undefined) {
2382 return [];
2383 }
2384
2385 return [String(valueOrValues)];
2386 }
2387
2388 function optionMatchesValue(option, value) {
2389 const normalizedValue = normalizeText(value);
2390 const candidates = [
2391 option?.value,
2392 option?.label,
2393 option?.textContent,
2394 option?.getAttribute?.("aria-label"),
2395 option?.getAttribute?.("data-value"),
2396 option?.getAttribute?.("id")
2397 ].map((candidate) => normalizeText(candidate));
2398 return candidates.some((candidate) => candidate === normalizedValue);
2399 }
2400
2401 function findNativeSelectOption(selectElement, value) {
2402 const options = [...(selectElement.options || [])];
2403 return options.find((option) => optionMatchesValue(option, value)) || null;
2404 }
2405
2406 function setNativeChecked(element, checked) {
2407 const descriptor = Object.getOwnPropertyDescriptor(globalThis.HTMLInputElement?.prototype || {}, "checked");
2408 if (typeof descriptor?.set === "function") {
2409 descriptor.set.call(element, Boolean(checked));
2410 } else {
2411 element.checked = Boolean(checked);
2412 }
2413 }
2414
2415 async function selectNativeElement(entry, values) {
2416 const element = entry.element;
2417 const beforeSnapshot = captureActionEffectSnapshot(element);
2418 const requestedValues = values.length ? values : [""];
2419 const appliedValues = [];
2420
2421 const {
2422 observedMutations
2423 } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
2424 scrollElementIntoView(element);
2425 focusElement(element);
2426
2427 if (element.multiple) {
2428 const matchedOptions = requestedValues.map((requestedValue) => {
2429 const option = findNativeSelectOption(element, requestedValue);
2430 if (!option) {
2431 throw createNamedError(
2432 "BrowserPageContentActionError",
2433 `Browser page content could not find select option "${requestedValue}".`,
2434 {
2435 code: "browser_page_content_select_option_not_found"
2436 }
2437 );
2438 }
2439 return option;
2440 });
2441 const matchedSet = new Set(matchedOptions);
2442 [...(element.options || [])].forEach((option) => {
2443 option.selected = matchedSet.has(option);
2444 });
2445 matchedOptions.forEach((option) => appliedValues.push(option.value));
2446 } else {
2447 const option = findNativeSelectOption(element, requestedValues[0]);
2448 if (!option) {
2449 throw createNamedError(
2450 "BrowserPageContentActionError",
2451 `Browser page content could not find select option "${requestedValues[0]}".`,
2452 {
2453 code: "browser_page_content_select_option_not_found"
2454 }
2455 );
2456 }
2457 appliedValues.push(setNativeValue(element, option.value));
2458 }
2459
2460 dispatchDomEvent(element, "input", "InputEvent", {
2461 inputType: "insertReplacementText"
2462 });
2463 dispatchDomEvent(element, "change");
2464 return appliedValues.slice();
2465 });
2466
2467 refreshReferenceEntry(entry);
2468 return buildActionResult(entry, {
2469 ...buildActionEffectResult(entry, beforeSnapshot, captureActionEffectSnapshot(element), observedMutations),
2470 values: appliedValues.slice()
2471 });
2472 }
2473
2474 function ariaOptionMatchesValue(option, value) {
2475 const normalizedValue = normalizeText(value);
2476 const candidates = [
2477 option?.getAttribute?.("aria-label"),
2478 option?.getAttribute?.("data-value"),
2479 option?.getAttribute?.("value"),
2480 option?.getAttribute?.("id"),
2481 getElementText(option)
2482 ].map((candidate) => normalizeText(candidate));
2483 return candidates.some((candidate) => candidate === normalizedValue);
2484 }
2485
2486 function visibleAriaOptions(root) {
2487 const scope = isElementNode(root) && String(root.getAttribute?.("role") || "").trim().toLowerCase() === "listbox"
2488 ? root
2489 : globalThis.document;
2490 try {
2491 return [...(scope.querySelectorAll?.("[role='option']") || [])]
2492 .filter((option) => isElementNode(option) && !isHiddenElement(option));
2493 } catch {
2494 return [];
2495 }
2496 }
2497
2498 function findAriaOption(root, value) {
2499 const matches = visibleAriaOptions(root).filter((option) => ariaOptionMatchesValue(option, value));
2500 return matches.length === 1 ? matches[0] : null;
2501 }
2502
2503 async function selectAriaElement(entry, values) {
2504 const element = entry.element;
2505 const role = String(element.getAttribute?.("role") || "").trim().toLowerCase();
2506 if (!["combobox", "listbox"].includes(role)) {
2507 throw createNamedError(
2508 "BrowserPageContentActionError",
2509 `Browser page content cannot select options on <${getTagName(element).toLowerCase()}>.`,
2510 {
2511 code: "browser_page_content_select_unsupported"
2512 }
2513 );
2514 }
2515
2516 const beforeSnapshot = captureActionEffectSnapshot(element);
2517 const requestedValues = values.length ? values : [""];
2518 const appliedValues = [];
2519 const {
2520 observedMutations
2521 } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
2522 scrollElementIntoView(element);
2523 focusElement(element);
2524 if (role === "combobox") {
2525 dispatchDomEvent(element, "mousedown", "MouseEvent", { button: 0 });
2526 if (typeof element.click === "function") {
2527 element.click();
2528 } else {
2529 dispatchDomEvent(element, "click", "MouseEvent", { button: 0 });
2530 }
2531 await delayMs(80);
2532 }
2533
2534 for (const requestedValue of requestedValues) {
2535 const option = findAriaOption(element, requestedValue);
2536 if (!option) {
2537 throw createNamedError(
2538 "BrowserPageContentActionError",
2539 `Browser page content could not safely find one ARIA option "${requestedValue}".`,
2540 {
2541 code: "browser_page_content_aria_option_not_found"
2542 }
2543 );
2544 }
2545 scrollElementIntoView(option);
2546 dispatchDomEvent(option, "mousedown", "MouseEvent", { button: 0 });
2547 if (typeof option.click === "function") {
2548 option.click();
2549 } else {
2550 dispatchDomEvent(option, "click", "MouseEvent", { button: 0 });
2551 }
2552 appliedValues.push(requestedValue);
2553 await delayMs(40);
2554 }
2555 });
2556
2557 refreshReferenceEntry(entry);
2558 return buildActionResult(entry, {
2559 ...buildActionEffectResult(entry, beforeSnapshot, captureActionEffectSnapshot(element), observedMutations),
2560 values: appliedValues.slice()
2561 });
2562 }
2563
2564 async function selectReference(referenceId, valueOrValues) {
2565 const entry = requireReferenceEntry(referenceId, {
2566 actionLabel: "select"
2567 });
2568 if (entry.helperBacked) {
2569 throw createNamedError(
2570 "BrowserPageContentActionError",
2571 `Browser page content cannot select helper-backed reference "${entry.referenceId}".`,
2572 {
2573 code: "browser_page_content_select_helper_backed"
2574 }
2575 );
2576 }
2577
2578 const element = entry.element;
2579 const values = normalizeActionValues(valueOrValues);
2580 if (getTagName(element) === "SELECT") {
2581 return selectNativeElement(entry, values);
2582 }
2583
2584 return selectAriaElement(entry, values);
2585 }
2586
2587 function checkedStateForElement(element) {
2588 const tagName = getTagName(element);
2589 const role = String(element.getAttribute?.("role") || "").trim().toLowerCase();
2590 if (tagName === "INPUT") {
2591 const inputType = String(element.getAttribute?.("type") || element.type || "").toLowerCase();
2592 if (["checkbox", "radio"].includes(inputType)) {
2593 return Boolean(element.checked);
2594 }
2595 }
2596 if (["checkbox", "radio", "switch", "menuitemcheckbox", "menuitemradio"].includes(role)) {
2597 return String(element.getAttribute?.("aria-checked") || "").trim().toLowerCase() === "true";
2598 }
2599 if (role === "button" && element.hasAttribute?.("aria-pressed")) {
2600 return String(element.getAttribute?.("aria-pressed") || "").trim().toLowerCase() === "true";
2601 }
2602 return null;
2603 }
2604
2605 async function setCheckedReference(referenceId, checked = true) {
2606 const entry = requireReferenceEntry(referenceId, {
2607 actionLabel: "setChecked"
2608 });
2609 if (entry.helperBacked) {
2610 throw createNamedError(
2611 "BrowserPageContentActionError",
2612 `Browser page content cannot set helper-backed reference "${entry.referenceId}".`,
2613 {
2614 code: "browser_page_content_checked_helper_backed"
2615 }
2616 );
2617 }
2618
2619 const element = entry.element;
2620 const beforeSnapshot = captureActionEffectSnapshot(element);
2621 const desiredChecked = Boolean(checked);
2622 const tagName = getTagName(element);
2623 const role = String(element.getAttribute?.("role") || "").trim().toLowerCase();
2624 const currentChecked = checkedStateForElement(element);
2625 if (currentChecked === null) {
2626 throw createNamedError(
2627 "BrowserPageContentActionError",
2628 `Browser page content cannot set checked state on <${tagName.toLowerCase()}>.`,
2629 {
2630 code: "browser_page_content_checked_unsupported"
2631 }
2632 );
2633 }
2634
2635 const {
2636 observedMutations
2637 } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
2638 scrollElementIntoView(element);
2639 focusElement(element);
2640
2641 if (tagName === "INPUT") {
2642 setNativeChecked(element, desiredChecked);
2643 dispatchDomEvent(element, "input", "InputEvent", {
2644 inputType: "insertReplacementText"
2645 });
2646 dispatchDomEvent(element, "change");
2647 } else if (["checkbox", "radio", "switch", "menuitemcheckbox", "menuitemradio"].includes(role)) {
2648 if (currentChecked !== desiredChecked) {
2649 if (typeof element.click === "function") {
2650 element.click();
2651 } else {
2652 dispatchDomEvent(element, "click", "MouseEvent", {
2653 button: 0
2654 });
2655 }
2656 await delayMs(40);
2657 }
2658 if (checkedStateForElement(element) !== desiredChecked) {
2659 element.setAttribute("aria-checked", desiredChecked ? "true" : "false");
2660 dispatchDomEvent(element, "input", "InputEvent", {
2661 inputType: "insertReplacementText"
2662 });
2663 dispatchDomEvent(element, "change");
2664 }
2665 } else if (role === "button" && element.hasAttribute?.("aria-pressed")) {
2666 if (currentChecked !== desiredChecked) {
2667 if (typeof element.click === "function") {
2668 element.click();
2669 } else {
2670 dispatchDomEvent(element, "click", "MouseEvent", {
2671 button: 0
2672 });
2673 }
2674 await delayMs(40);
2675 }
2676 if (checkedStateForElement(element) !== desiredChecked) {
2677 element.setAttribute("aria-pressed", desiredChecked ? "true" : "false");
2678 }
2679 }
2680 });
2681
2682 refreshReferenceEntry(entry);
2683 return buildActionResult(entry, {
2684 ...buildActionEffectResult(entry, beforeSnapshot, captureActionEffectSnapshot(element), observedMutations),
2685 checked: desiredChecked
2686 });
2687 }
2688
2689 function resolveFileInputElement(referenceId) {
2690 const entry = requireReferenceEntry(referenceId, {
2691 actionLabel: "fileInput"
2692 });
2693 if (entry.helperBacked) {
2694 throw createNamedError(
2695 "BrowserPageContentActionError",
2696 `Browser page content cannot upload files through helper-backed reference "${entry.referenceId}".`,
2697 {
2698 code: "browser_page_content_file_input_helper_backed"
2699 }
2700 );
2701 }
2702
2703 const element = entry.element;
2704 const isFileInput = (candidate) => {
2705 return getTagName(candidate) === "INPUT"
2706 && String(candidate.getAttribute?.("type") || candidate.type || "").toLowerCase() === "file";
2707 };
2708
2709 if (isFileInput(element)) {
2710 return element;
2711 }
2712
2713 if (getTagName(element) === "LABEL") {
2714 if (isFileInput(element.control)) {
2715 return element.control;
2716 }
2717 const labelledInput = element.querySelector?.("input[type='file']");
2718 if (isFileInput(labelledInput)) {
2719 return labelledInput;
2720 }
2721 const forId = normalizeAttributeText(element.getAttribute?.("for"));
2722 if (forId) {
2723 const byId = element.ownerDocument?.getElementById?.(forId);
2724 if (isFileInput(byId)) {
2725 return byId;
2726 }
2727 }
2728 }
2729
2730 const descendantInput = element.querySelector?.("input[type='file']");
2731 if (isFileInput(descendantInput)) {
2732 return descendantInput;
2733 }
2734
2735 const closestLabel = element.closest?.("label");
2736 if (closestLabel) {
2737 if (isFileInput(closestLabel.control)) {
2738 return closestLabel.control;
2739 }
2740 const labelledInput = closestLabel.querySelector?.("input[type='file']");
2741 if (isFileInput(labelledInput)) {
2742 return labelledInput;
2743 }
2744 }
2745
2746 return null;
2747 }
2748
2749 function fileInputFor(referenceId) {
2750 const input = resolveFileInputElement(referenceId);
2751 if (!input) {
2752 throw createNamedError(
2753 "BrowserPageContentActionError",
2754 `Browser page content reference "${normalizeReferenceId(referenceId)}" is not a file input or associated label.`,
2755 {
2756 code: "browser_page_content_file_input_not_found"
2757 }
2758 );
2759 }
2760 return {
2761 accept: normalizeAttributeText(input.getAttribute?.("accept")),
2762 multiple: Boolean(input.multiple),
2763 name: normalizeAttributeText(input.getAttribute?.("name")),
2764 selector: computeStableSelector(input),
2765 tagName: getTagName(input),
2766 type: String(input.getAttribute?.("type") || input.type || "").toLowerCase()
2767 };
2768 }
2769
2770 function fileInputElementFor(referenceId) {
2771 return resolveFileInputElement(referenceId);
2772 }
2773
2774 function refreshReferenceEntry(entry) {
2775 if (!entry || entry.helperBacked || !entry.element) {
2776 return entry;
2777 }
2778
2779 entry.connected = entry.element.isConnected !== false;
2780 if (entry.connected) {
2781 entry.dom = serializeElementSnapshot(entry.element) || entry.dom;
2782 entry.id = normalizeAttributeText(entry.element.getAttribute?.("id"));
2783 entry.name = normalizeAttributeText(entry.element.getAttribute?.("name"));
2784 const summaryData = collectReferenceSummaryData(entry.element, state.captureOptions);
2785 entry.descriptorTags = summaryData.descriptorTags;
2786 entry.kind = summaryData.kind;
2787 entry.semanticTags = summaryData.semanticTags;
2788 entry.state = summaryData.state;
2789 entry.summary = summaryData.summary;
2790 entry.tagName = getTagName(entry.element);
2791 }
2792
2793 return entry;
2794 }
2795
2796 function scrollElementIntoView(element) {
2797 try {
2798 element.scrollIntoView?.({
2799 behavior: "auto",
2800 block: "center",
2801 inline: "center"
2802 });
2803 return true;
2804 } catch {
2805 return false;
2806 }
2807 }
2808
2809 function focusElement(element) {
2810 try {
2811 element.focus?.({
2812 preventScroll: true
2813 });
2814 return true;
2815 } catch {
2816 try {
2817 element.focus?.();
2818 return true;
2819 } catch {
2820 return false;
2821 }
2822 }
2823 }
2824
2825 function describeActiveElement(element) {
2826 if (!isElementNode(element)) {
2827 return "";
2828 }
2829
2830 const tagName = getTagName(element).toLowerCase();
2831 const id = normalizeAttributeText(element.getAttribute?.("id"));
2832 const name = normalizeAttributeText(element.getAttribute?.("name"));
2833 const label = truncateText(getLabelText(element, {
2834 includeAlt: false,
2835 includeDescendantImageAlt: true,
2836 includePlaceholder: false,
2837 includeText: false
2838 }), 48);
2839 return [tagName, id ? `#${id}` : "", name ? `name=${name}` : "", label].filter(Boolean).join(" ");
2840 }
2841
2842 function getActionObservationRoot(element) {
2843 if (!isElementNode(element)) {
2844 return globalThis.document?.body || globalThis.document?.documentElement || null;
2845 }
2846
2847 return element.closest?.("form, fieldset, dialog, [role='dialog'], [role='alert'], [role='status'], [aria-live], article, section, main, li, tr, td, th")
2848 || element.parentElement
2849 || element;
2850 }
2851
2852 function getElementDirectText(element) {
2853 if (!isElementNode(element)) {
2854 return "";
2855 }
2856
2857 return normalizeText(
2858 [...(element.childNodes || [])]
2859 .filter((node) => isTextNode(node))
2860 .map((node) => node.textContent || "")
2861 .join(" ")
2862 );
2863 }
2864
2865 function collectNearbyTextEntries(root, limit = 24) {
2866 if (!isElementNode(root)) {
2867 return [];
2868 }
2869
2870 const entries = [];
2871 const seen = new Set();
2872 const acceptElement = (element) => {
2873 if (!isElementNode(element) || isHiddenElement(element) || entries.length >= limit) {
2874 return;
2875 }
2876
2877 const role = normalizeText(element.getAttribute?.("role")).toLowerCase();
2878 const directText = getElementDirectText(element);
2879 const fallbackText = ["alert", "status"].includes(role) || element.hasAttribute?.("aria-live")
2880 ? getElementText(element)
2881 : "";
2882 const text = truncateText(directText || fallbackText, 220);
2883 if (!text) {
2884 return;
2885 }
2886
2887 const key = `${role}|${text}`;
2888 if (seen.has(key)) {
2889 return;
2890 }
2891 seen.add(key);
2892 const state = collectElementStateMetadata(element, {
2893 includeSemanticTags: true,
2894 includeStateTags: true
2895 });
2896 entries.push({
2897 invalid: state.invalid === true,
2898 role,
2899 semanticTone: state.semanticTone || "",
2900 text
2901 });
2902 };
2903
2904 acceptElement(root);
2905 const walker = globalThis.document?.createTreeWalker?.(root, globalThis.NodeFilter?.SHOW_ELEMENT ?? 1);
2906 if (!walker) {
2907 return entries;
2908 }
2909
2910 let currentNode = walker.nextNode();
2911 while (currentNode && entries.length < limit) {
2912 acceptElement(currentNode);
2913 currentNode = walker.nextNode();
2914 }
2915
2916 return entries;
2917 }
2918
2919 function captureActionEffectSnapshot(element) {
2920 const observationRoot = getActionObservationRoot(element);
2921 return {
2922 activeElement: describeActiveElement(globalThis.document?.activeElement),
2923 observationRoot,
2924 observationText: truncateText(getElementText(observationRoot), 2000),
2925 targetDom: truncateText(serializeElementSnapshot(element), 2000),
2926 targetState: collectElementStateMetadata(element, {
2927 includeSemanticTags: true,
2928 includeStateTags: true
2929 }),
2930 textEntries: collectNearbyTextEntries(observationRoot),
2931 value: getReferenceValueMetadata(element)
2932 };
2933 }
2934
2935 async function waitForObservedActionWindow(observationRoot, {
2936 quietMs = 40,
2937 timeoutMs = 180
2938 } = {}) {
2939 const target = observationRoot?.ownerDocument?.body
2940 || observationRoot?.ownerDocument?.documentElement
2941 || globalThis.document?.body
2942 || globalThis.document?.documentElement;
2943 if (!target || typeof globalThis.MutationObserver !== "function") {
2944 await delayMs(timeoutMs);
2945 return {
2946 attributeNames: [],
2947 mutationCount: 0
2948 };
2949 }
2950
2951 const attributeNames = new Set();
2952 let lastMutationAt = 0;
2953 let mutationCount = 0;
2954 const observer = new globalThis.MutationObserver((mutations) => {
2955 mutationCount += mutations.length;
2956 lastMutationAt = Date.now();
2957 mutations.forEach((mutation) => {
2958 if (mutation.type === "attributes" && mutation.attributeName) {
2959 attributeNames.add(String(mutation.attributeName));
2960 }
2961 });
2962 });
2963
2964 try {
2965 observer.observe(target, {
2966 attributes: true,
2967 characterData: true,
2968 childList: true,
2969 subtree: true
2970 });
2971 const startedAt = Date.now();
2972 while (Date.now() - startedAt < timeoutMs) {
2973 await delayMs(20);
2974 if (mutationCount > 0 && Date.now() - lastMutationAt >= quietMs) {
2975 break;
2976 }
2977 }
2978 } finally {
2979 observer.disconnect();
2980 }
2981
2982 return {
2983 attributeNames: [...attributeNames],
2984 mutationCount
2985 };
2986 }
2987
2988 async function withObservedActionWindow(observationRoot, action, options = {}) {
2989 const target = observationRoot?.ownerDocument?.body
2990 || observationRoot?.ownerDocument?.documentElement
2991 || globalThis.document?.body
2992 || globalThis.document?.documentElement;
2993 if (!target || typeof globalThis.MutationObserver !== "function") {
2994 const result = await action();
2995 const observedMutations = await waitForObservedActionWindow(observationRoot, options);
2996 return {
2997 observedMutations,
2998 result
2999 };
3000 }
3001
3002 const attributeNames = new Set();
3003 let lastMutationAt = 0;
3004 let mutationCount = 0;
3005 const observer = new globalThis.MutationObserver((mutations) => {
3006 mutationCount += mutations.length;
3007 lastMutationAt = Date.now();
3008 mutations.forEach((mutation) => {
3009 if (mutation.type === "attributes" && mutation.attributeName) {
3010 attributeNames.add(String(mutation.attributeName));
3011 }
3012 });
3013 });
3014
3015 try {
3016 observer.observe(target, {
3017 attributes: true,
3018 characterData: true,
3019 childList: true,
3020 subtree: true
3021 });
3022 const result = await action();
3023 const quietMs = Math.max(0, Number(options.quietMs) || 40);
3024 const timeoutMs = Math.max(0, Number(options.timeoutMs) || 180);
3025 const startedAt = Date.now();
3026 while (Date.now() - startedAt < timeoutMs) {
3027 await delayMs(20);
3028 if (mutationCount > 0 && Date.now() - lastMutationAt >= quietMs) {
3029 break;
3030 }
3031 }
3032 return {
3033 observedMutations: {
3034 attributeNames: [...attributeNames],
3035 mutationCount
3036 },
3037 result
3038 };
3039 } finally {
3040 observer.disconnect();
3041 }
3042 }
3043
3044 function compareDescriptorTags(beforeTags = [], afterTags = []) {
3045 const beforeValue = beforeTags.filter(Boolean).join("|");
3046 const afterValue = afterTags.filter(Boolean).join("|");
3047 return beforeValue !== afterValue;
3048 }
3049
3050 function buildActionEffectResult(entry, beforeSnapshot, afterSnapshot, observedMutations, extra = {}) {
3051 const newTextEntries = afterSnapshot.textEntries.filter((entryData) => {
3052 return !beforeSnapshot.textEntries.some((beforeEntry) => beforeEntry.text === entryData.text);
3053 });
3054 const validationEntries = newTextEntries.filter((entryData) => {
3055 return entryData.invalid
3056 || ["alert", "status"].includes(entryData.role)
3057 || ["error", "warning"].includes(entryData.semanticTone);
3058 });
3059 const focusChanged = beforeSnapshot.activeElement !== afterSnapshot.activeElement;
3060 const nearbyTextChanged = beforeSnapshot.observationText !== afterSnapshot.observationText;
3061 const valueChanged = beforeSnapshot.value !== afterSnapshot.value;
3062 const checkedChanged = beforeSnapshot.targetState.checked !== afterSnapshot.targetState.checked;
3063 const selectedChanged = beforeSnapshot.targetState.selected !== afterSnapshot.targetState.selected;
3064 const expandedChanged = beforeSnapshot.targetState.expanded !== afterSnapshot.targetState.expanded;
3065 const pressedChanged = beforeSnapshot.targetState.pressed !== afterSnapshot.targetState.pressed;
3066 const descriptorChanged = compareDescriptorTags(beforeSnapshot.targetState.descriptorTags, afterSnapshot.targetState.descriptorTags);
3067 const targetDomChanged = beforeSnapshot.targetDom !== afterSnapshot.targetDom;
3068 const domChanged = Boolean(observedMutations.mutationCount) || targetDomChanged || nearbyTextChanged;
3069 const status = {
3070 alertTextAdded: newTextEntries.some((entryData) => ["alert", "status"].includes(entryData.role)),
3071 checkedChanged,
3072 descriptorChanged,
3073 domChanged,
3074 expandedChanged,
3075 focusChanged,
3076 nearbyTextChanged,
3077 pressedChanged,
3078 reacted: false,
3079 selectedChanged,
3080 targetChanged: descriptorChanged || targetDomChanged || valueChanged || checkedChanged || selectedChanged || expandedChanged || pressedChanged,
3081 targetDomChanged,
3082 valueChanged,
3083 validationTextAdded: validationEntries.length > 0
3084 };
3085 status.reacted = Object.entries(status).some(([key, value]) => key !== "reacted" && value === true);
3086 status.noObservedEffect = !status.reacted;
3087
3088 return {
3089 ...extra,
3090 descriptorTags: afterSnapshot.targetState.descriptorTags.slice(),
3091 effect: {
3092 mutationAttributes: observedMutations.attributeNames.slice(0, 8),
3093 mutationCount: observedMutations.mutationCount,
3094 newText: newTextEntries.map((entryData) => entryData.text).slice(0, 3),
3095 semanticHints: [...new Set(newTextEntries.map((entryData) => entryData.semanticTone).filter(Boolean))].slice(0, 3),
3096 validationText: validationEntries.map((entryData) => entryData.text).slice(0, 3)
3097 },
3098 semanticTags: afterSnapshot.targetState.semanticTags.slice(),
3099 state: afterSnapshot.targetState,
3100 status
3101 };
3102 }
3103
3104 function buildActionResult(entry, extra = {}) {
3105 return {
3106 actionStrategy: entry.helperBacked ? "frame_chain_ref" : "dom_ref",
3107 captureId: state.captureId,
3108 descriptorTags: Array.isArray(entry?.descriptorTags) ? entry.descriptorTags.slice() : [],
3109 frameChain: Array.isArray(entry?.frameChain) ? entry.frameChain.slice() : [],
3110 frameId: entry.frameId || "",
3111 nodeId: entry.nodeId || "",
3112 referenceId: entry.referenceId,
3113 semanticTags: Array.isArray(entry?.semanticTags) ? entry.semanticTags.slice() : [],
3114 state: entry.state || collectElementStateMetadata(entry.element, state.captureOptions),
3115 summary: entry.summary,
3116 tagName: entry.tagName,
3117 ...extra
3118 };
3119 }
3120
3121 function buildHelperBackedActionResult(entry, helperResult, extra = {}) {
3122 return {
3123 actionStrategy: "frame_chain_ref",
3124 captureId: state.captureId,
3125 descriptorTags: Array.isArray(helperResult?.descriptorTags) ? helperResult.descriptorTags : (entry.descriptorTags || []),
3126 frameChain: entry.frameChain.slice(),
3127 frameId: entry.frameId,
3128 nodeId: entry.nodeId,
3129 referenceId: entry.referenceId,
3130 semanticTags: Array.isArray(helperResult?.semanticTags) ? helperResult.semanticTags : (entry.semanticTags || []),
3131 state: helperResult?.state || entry.state || collectElementStateMetadata(null),
3132 summary: entry.summary,
3133 tagName: String(helperResult?.tagName || entry.tagName || ""),
3134 ...extra
3135 };
3136 }
3137
3138 function mergeActionOutcomeResults(...results) {
3139 const normalizedResults = results.filter(Boolean);
3140 const mergedStatus = {};
3141 const mergedEffect = {
3142 mutationAttributes: [],
3143 mutationCount: 0,
3144 newText: [],
3145 semanticHints: [],
3146 validationText: []
3147 };
3148
3149 normalizedResults.forEach((result) => {
3150 Object.entries(result?.status || {}).forEach(([key, value]) => {
3151 if (typeof value === "boolean") {
3152 mergedStatus[key] = mergedStatus[key] === true || value === true;
3153 }
3154 });
3155 if (Number.isFinite(result?.effect?.mutationCount)) {
3156 mergedEffect.mutationCount += Number(result.effect.mutationCount);
3157 }
3158 ["mutationAttributes", "newText", "semanticHints", "validationText"].forEach((key) => {
3159 const values = Array.isArray(result?.effect?.[key]) ? result.effect[key] : [];
3160 values.forEach((value) => {
3161 if (value && !mergedEffect[key].includes(value)) {
3162 mergedEffect[key].push(value);
3163 }
3164 });
3165 });
3166 });
3167
3168 mergedStatus.reacted = Object.entries(mergedStatus).some(([key, value]) => key !== "reacted" && key !== "noObservedEffect" && value === true);
3169 mergedStatus.noObservedEffect = !mergedStatus.reacted;
3170 return {
3171 effect: mergedEffect,
3172 status: mergedStatus
3173 };
3174 }
3175
3176 function dispatchDomEvent(target, eventName, EventType = "Event", options = {}) {
3177 const EventConstructor = typeof globalThis[EventType] === "function"
3178 ? globalThis[EventType]
3179 : globalThis.Event;
3180 const event = new EventConstructor(eventName, {
3181 bubbles: true,
3182 cancelable: true,
3183 composed: true,
3184 ...options
3185 });
3186 target.dispatchEvent(event);
3187 return event;
3188 }
3189
3190 function dispatchKeyboardEvent(target, eventName, options = {}) {
3191 const KeyboardEventConstructor = typeof globalThis.KeyboardEvent === "function"
3192 ? globalThis.KeyboardEvent
3193 : globalThis.Event;
3194 const event = new KeyboardEventConstructor(eventName, {
3195 bubbles: true,
3196 cancelable: true,
3197 composed: true,
3198 code: "Enter",
3199 key: "Enter",
3200 ...options
3201 });
3202
3203 [
3204 ["charCode", Number(options.charCode ?? 0)],
3205 ["keyCode", Number(options.keyCode ?? 13)],
3206 ["which", Number(options.which ?? 13)]
3207 ].forEach(([propertyName, propertyValue]) => {
3208 try {
3209 if (typeof event[propertyName] !== "number") {
3210 Object.defineProperty(event, propertyName, {
3211 configurable: true,
3212 enumerable: true,
3213 value: propertyValue
3214 });
3215 }
3216 } catch {
3217 // Ignore read-only KeyboardEvent properties.
3218 }
3219 });
3220
3221 target.dispatchEvent(event);
3222 return event;
3223 }
3224
3225 function setNativeValue(element, nextValue) {
3226 const tagName = getTagName(element);
3227 const normalizedValue = String(nextValue ?? "");
3228
3229 if (tagName === "INPUT") {
3230 const descriptor = Object.getOwnPropertyDescriptor(globalThis.HTMLInputElement?.prototype || {}, "value");
3231 if (typeof descriptor?.set === "function") {
3232 descriptor.set.call(element, normalizedValue);
3233 } else {
3234 element.value = normalizedValue;
3235 }
3236 return normalizedValue;
3237 }
3238
3239 if (tagName === "TEXTAREA") {
3240 const descriptor = Object.getOwnPropertyDescriptor(globalThis.HTMLTextAreaElement?.prototype || {}, "value");
3241 if (typeof descriptor?.set === "function") {
3242 descriptor.set.call(element, normalizedValue);
3243 } else {
3244 element.value = normalizedValue;
3245 }
3246 return normalizedValue;
3247 }
3248
3249 if (tagName === "SELECT") {
3250 const matchedOption = [...(element.options || [])].find((option) => {
3251 return option.value === normalizedValue
3252 || normalizeText(option.textContent || "") === normalizeText(normalizedValue)
3253 || normalizeText(option.label || "") === normalizeText(normalizedValue);
3254 });
3255
3256 const resolvedValue = matchedOption ? matchedOption.value : normalizedValue;
3257 const descriptor = Object.getOwnPropertyDescriptor(globalThis.HTMLSelectElement?.prototype || {}, "value");
3258 if (typeof descriptor?.set === "function") {
3259 descriptor.set.call(element, resolvedValue);
3260 } else {
3261 element.value = resolvedValue;
3262 }
3263 return resolvedValue;
3264 }
3265
3266 if (String(element.getAttribute?.("contenteditable") || "").toLowerCase() === "true") {
3267 element.textContent = normalizedValue;
3268 return normalizedValue;
3269 }
3270
3271 throw createNamedError(
3272 "BrowserPageContentActionError",
3273 `Browser page content cannot type into <${getTagName(element).toLowerCase()}>.`,
3274 {
3275 code: "browser_page_content_type_unsupported"
3276 }
3277 );
3278 }
3279
3280 async function updateElementValue(referenceId, value) {
3281 const entry = requireReferenceEntry(referenceId, {
3282 actionLabel: "type"
3283 });
3284
3285 if (entry.helperBacked) {
3286 const helper = requireDomHelper("type into reference");
3287 const typedResult = await helper.typeNode(entry.frameChain, entry.nodeId, value);
3288 return buildHelperBackedActionResult(entry, typedResult, {
3289 effect: typedResult?.effect || {},
3290 status: typedResult?.status || {},
3291 value: typedResult?.value ?? String(value ?? "")
3292 });
3293 }
3294
3295 const element = entry.element;
3296 const beforeSnapshot = captureActionEffectSnapshot(element);
3297
3298 const {
3299 result: appliedValue,
3300 observedMutations
3301 } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
3302 scrollElementIntoView(element);
3303 focusElement(element);
3304 const nextValue = setNativeValue(element, value);
3305
3306 if (typeof element.setSelectionRange === "function") {
3307 try {
3308 element.setSelectionRange(String(nextValue).length, String(nextValue).length);
3309 } catch {
3310 // Ignore selection errors for unsupported input types.
3311 }
3312 }
3313
3314 dispatchDomEvent(element, "beforeinput", "InputEvent", {
3315 data: String(value ?? ""),
3316 inputType: "insertText"
3317 });
3318 dispatchDomEvent(element, "input", "InputEvent", {
3319 data: String(value ?? ""),
3320 inputType: "insertText"
3321 });
3322 dispatchDomEvent(element, "change");
3323 return nextValue;
3324 });
3325
3326 refreshReferenceEntry(entry);
3327 return buildActionResult(entry, {
3328 ...buildActionEffectResult(entry, beforeSnapshot, captureActionEffectSnapshot(element), observedMutations),
3329 value: appliedValue
3330 });
3331 }
3332
3333 async function activateElement(referenceId) {
3334 const entry = requireReferenceEntry(referenceId, {
3335 actionLabel: "click"
3336 });
3337
3338 if (entry.helperBacked) {
3339 const helper = requireDomHelper("click reference");
3340 const clickedResult = await helper.clickNode(entry.frameChain, entry.nodeId);
3341 return buildHelperBackedActionResult(entry, clickedResult, {
3342 effect: clickedResult?.effect || {},
3343 status: clickedResult?.status || {}
3344 });
3345 }
3346
3347 const element = entry.element;
3348 const beforeSnapshot = captureActionEffectSnapshot(element);
3349
3350 scrollElementIntoView(element);
3351 focusElement(element);
3352
3353 if (beforeSnapshot.targetState.disabled) {
3354 throw createNamedError(
3355 "BrowserPageContentActionError",
3356 `Browser page content reference "${entry.referenceId}" is disabled.`,
3357 {
3358 code: "browser_page_content_click_disabled"
3359 }
3360 );
3361 }
3362
3363 const {
3364 observedMutations
3365 } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
3366 if (typeof element.click === "function") {
3367 element.click();
3368 } else {
3369 dispatchDomEvent(element, "click", "MouseEvent", {
3370 button: 0
3371 });
3372 }
3373 });
3374
3375 refreshReferenceEntry(entry);
3376 return buildActionResult(entry, buildActionEffectResult(
3377 entry,
3378 beforeSnapshot,
3379 captureActionEffectSnapshot(element),
3380 observedMutations
3381 ));
3382 }
3383
3384 async function submitElement(referenceId) {
3385 const entry = requireReferenceEntry(referenceId, {
3386 actionLabel: "submit"
3387 });
3388
3389 if (entry.helperBacked) {
3390 const helper = requireDomHelper("submit reference");
3391 const submittedResult = await helper.submitNode(entry.frameChain, entry.nodeId);
3392 return buildHelperBackedActionResult(entry, submittedResult, {
3393 effect: submittedResult?.effect || {},
3394 status: submittedResult?.status || {}
3395 });
3396 }
3397
3398 const element = entry.element;
3399 const tagName = getTagName(element);
3400 const beforeSnapshot = captureActionEffectSnapshot(element);
3401
3402 const {
3403 observedMutations
3404 } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
3405 scrollElementIntoView(element);
3406 focusElement(element);
3407
3408 if (tagName === "FORM") {
3409 if (typeof element.requestSubmit === "function") {
3410 element.requestSubmit();
3411 } else {
3412 const submitEvent = dispatchDomEvent(element, "submit");
3413 if (!submitEvent.defaultPrevented) {
3414 element.submit?.();
3415 }
3416 }
3417 } else if (typeof element.form?.requestSubmit === "function") {
3418 if (tagName === "BUTTON" || tagName === "INPUT") {
3419 element.form.requestSubmit(element);
3420 } else {
3421 element.form.requestSubmit();
3422 }
3423 } else if (element.form) {
3424 const submitEvent = dispatchDomEvent(element.form, "submit");
3425 if (!submitEvent.defaultPrevented) {
3426 element.form.submit?.();
3427 }
3428 } else if (typeof element.click === "function") {
3429 element.click();
3430 } else {
3431 throw createNamedError(
3432 "BrowserPageContentActionError",
3433 `Browser page content cannot submit reference "${entry.referenceId}".`,
3434 {
3435 code: "browser_page_content_submit_unsupported"
3436 }
3437 );
3438 }
3439 });
3440
3441 refreshReferenceEntry(entry);
3442 return buildActionResult(entry, buildActionEffectResult(
3443 entry,
3444 beforeSnapshot,
3445 captureActionEffectSnapshot(element),
3446 observedMutations
3447 ));
3448 }
3449
3450 function shouldEnterSubmitForm(element) {
3451 const tagName = getTagName(element);
3452 if (tagName !== "INPUT") {
3453 return false;
3454 }
3455
3456 const inputType = String(element.getAttribute?.("type") || element.type || "text").toLowerCase();
3457 return ![
3458 "button",
3459 "checkbox",
3460 "color",
3461 "file",
3462 "hidden",
3463 "image",
3464 "radio",
3465 "range",
3466 "reset",
3467 "submit"
3468 ].includes(inputType);
3469 }
3470
3471 async function pressEnterElement(referenceId, actionLabel = "type_submit") {
3472 const entry = requireReferenceEntry(referenceId, {
3473 actionLabel
3474 });
3475
3476 if (entry.helperBacked) {
3477 const helper = requireDomHelper("press enter on reference");
3478 const submittedResult = await helper.typeSubmitNode(entry.frameChain, entry.nodeId, "");
3479 return buildHelperBackedActionResult(entry, submittedResult, {
3480 effect: submittedResult?.effect || {},
3481 status: submittedResult?.status || {}
3482 });
3483 }
3484
3485 const element = entry.element;
3486 const beforeSnapshot = captureActionEffectSnapshot(element);
3487
3488 const {
3489 observedMutations
3490 } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
3491 scrollElementIntoView(element);
3492 focusElement(element);
3493
3494 const keydownEvent = dispatchKeyboardEvent(element, "keydown", {
3495 charCode: 0,
3496 keyCode: 13,
3497 which: 13
3498 });
3499 const keypressEvent = dispatchKeyboardEvent(element, "keypress", {
3500 charCode: 13,
3501 keyCode: 13,
3502 which: 13
3503 });
3504 const keyupEvent = dispatchKeyboardEvent(element, "keyup", {
3505 charCode: 0,
3506 keyCode: 13,
3507 which: 13
3508 });
3509
3510 if (
3511 !keydownEvent.defaultPrevented
3512 && !keypressEvent.defaultPrevented
3513 && !keyupEvent.defaultPrevented
3514 && shouldEnterSubmitForm(element)
3515 ) {
3516 if (typeof element.form?.requestSubmit === "function") {
3517 element.form.requestSubmit();
3518 } else if (element.form) {
3519 const submitEvent = dispatchDomEvent(element.form, "submit");
3520 if (!submitEvent.defaultPrevented) {
3521 element.form.submit?.();
3522 }
3523 }
3524 }
3525 });
3526
3527 refreshReferenceEntry(entry);
3528 return buildActionResult(entry, buildActionEffectResult(
3529 entry,
3530 beforeSnapshot,
3531 captureActionEffectSnapshot(element),
3532 observedMutations
3533 ));
3534 }
3535
3536 async function typeAndSubmit(referenceId, value) {
3537 const entry = requireReferenceEntry(referenceId, {
3538 actionLabel: "type_submit"
3539 });
3540
3541 if (entry.helperBacked) {
3542 const helper = requireDomHelper("type and submit reference");
3543 const submittedResult = await helper.typeSubmitNode(entry.frameChain, entry.nodeId, value);
3544 return buildHelperBackedActionResult(entry, submittedResult, {
3545 effect: submittedResult?.effect || {},
3546 status: submittedResult?.status || {},
3547 value: submittedResult?.value ?? String(value ?? "")
3548 });
3549 }
3550
3551 const typed = await updateElementValue(referenceId, value);
3552 const submitted = await pressEnterElement(referenceId);
3553 const mergedOutcome = mergeActionOutcomeResults(typed, submitted);
3554
3555 return {
3556 ...submitted,
3557 ...mergedOutcome,
3558 value: typed.value
3559 };
3560 }
3561
3562 async function scrollToReference(referenceId) {
3563 const entry = requireReferenceEntry(referenceId, {
3564 actionLabel: "scroll"
3565 });
3566
3567 if (entry.helperBacked) {
3568 const helper = requireDomHelper("scroll to reference");
3569 const scrollResult = await helper.scrollNode(entry.frameChain, entry.nodeId);
3570 return buildHelperBackedActionResult(entry, scrollResult, {
3571 effect: scrollResult?.effect || {},
3572 status: scrollResult?.status || {}
3573 });
3574 }
3575
3576 const beforeSnapshot = captureActionEffectSnapshot(entry.element);
3577 scrollElementIntoView(entry.element);
3578 focusElement(entry.element);
3579 refreshReferenceEntry(entry);
3580 const afterSnapshot = captureActionEffectSnapshot(entry.element);
3581 const scrollEffect = buildActionEffectResult(entry, beforeSnapshot, afterSnapshot, {
3582 attributeNames: [],
3583 mutationCount: 0
3584 });
3585 return buildActionResult(entry, {
3586 ...scrollEffect,
3587 status: {
3588 ...scrollEffect.status,
3589 reacted: true,
3590 noObservedEffect: false
3591 }
3592 });
3593 }
3594
3595 function cssEscape(value) {
3596 const rawValue = String(value || "");
3597 if (!rawValue) {
3598 return "";
3599 }
3600
3601 if (typeof globalThis.CSS?.escape === "function") {
3602 return globalThis.CSS.escape(rawValue);
3603 }
3604
3605 return rawValue.replace(/[^a-zA-Z0-9_-]/gu, (character) => `\\${character}`);
3606 }
3607
3608 function getClassSummary(element) {
3609 try {
3610 return [...(element?.classList || [])]
3611 .map((className) => normalizeAttributeText(className))
3612 .filter(Boolean)
3613 .slice(0, 4)
3614 .join(" ");
3615 } catch {
3616 return "";
3617 }
3618 }
3619
3620 function buildCssSelector(element) {
3621 if (!isElementNode(element)) {
3622 return "";
3623 }
3624
3625 const id = normalizeAttributeText(element.getAttribute?.("id"));
3626 if (id) {
3627 return `#${cssEscape(id)}`;
3628 }
3629
3630 const parts = [];
3631 let current = element;
3632 while (isElementNode(current) && current !== globalThis.document?.documentElement && parts.length < 6) {
3633 const tagName = getTagName(current).toLowerCase();
3634 if (!tagName) {
3635 break;
3636 }
3637
3638 let part = tagName;
3639 const classes = getClassSummary(current)
3640 .split(/\s+/u)
3641 .filter(Boolean)
3642 .slice(0, 2);
3643 if (classes.length && !["body", "html"].includes(tagName)) {
3644 part += classes.map((className) => `.${cssEscape(className)}`).join("");
3645 }
3646
3647 const parent = current.parentElement;
3648 if (parent) {
3649 const siblings = [...parent.children].filter((sibling) => getTagName(sibling) === getTagName(current));
3650 if (siblings.length > 1) {
3651 part += `:nth-of-type(${siblings.indexOf(current) + 1})`;
3652 }
3653 }
3654
3655 parts.unshift(part);
3656 if (tagName === "body") {
3657 break;
3658 }
3659 current = parent;
3660 }
3661
3662 return parts.join(" > ");
3663 }
3664
3665 function sanitizeAnnotationDom(value) {
3666 return truncateText(
3667 String(value || "")
3668 .replace(/(<input\b(?=[^>]*\btype\s*=\s*(["'])?password\2?)[^>]*?)\s+value\s*=\s*(["'])[\s\S]*?\3/giu, "$1 value=\"[redacted]\"")
3669 .replace(/\svalue\s*=\s*(["'])[\s\S]{0,600}?\1/giu, " value=\"[redacted]\"")
3670 .replace(/\sdata-space-browser-live-value\s*=\s*(["'])[\s\S]{0,600}?\1/giu, "")
3671 .replace(/\sdata-space-browser-selected-text\s*=\s*(["'])[\s\S]{0,600}?\1/giu, ""),
3672 1200
3673 );
3674 }
3675
3676 function summarizeAnnotationElement(element) {
3677 if (!isElementNode(element)) {
3678 return null;
3679 }
3680
3681 const summaryData = collectReferenceSummaryData(element, {
3682 includeLabelQuotes: false,
3683 includeLinkUrls: true,
3684 includeSemanticTags: true,
3685 includeStateTags: true
3686 });
3687 const rawDom = serializeElementSnapshot(element);
3688 return {
3689 classes: getClassSummary(element),
3690 dom: sanitizeAnnotationDom(rawDom),
3691 id: normalizeAttributeText(element.getAttribute?.("id")),
3692 kind: summaryData.kind,
3693 name: normalizeAttributeText(element.getAttribute?.("name")),
3694 rect: getElementRectSafe(element),
3695 role: normalizeAttributeText(element.getAttribute?.("role")).toLowerCase(),
3696 selector: buildCssSelector(element),
3697 semanticTags: Array.isArray(summaryData.semanticTags) ? summaryData.semanticTags.slice(0, 4) : [],
3698 stateTags: Array.isArray(summaryData.state?.stateTags) ? summaryData.state.stateTags.slice(0, 8) : [],
3699 summary: truncateText(summaryData.summary || getLabelText(element, {
3700 includeAlt: true,
3701 includeDescendantImageAlt: true,
3702 includePlaceholder: true,
3703 includeText: true
3704 }), 240),
3705 tagName: getTagName(element)
3706 };
3707 }
3708
3709 function annotationViewport() {
3710 return {
3711 height: Math.max(0, Number(globalThis.innerHeight || globalThis.document?.documentElement?.clientHeight || 0)),
3712 scrollX: Number(globalThis.scrollX || globalThis.pageXOffset || 0),
3713 scrollY: Number(globalThis.scrollY || globalThis.pageYOffset || 0),
3714 width: Math.max(0, Number(globalThis.innerWidth || globalThis.document?.documentElement?.clientWidth || 0))
3715 };
3716 }
3717
3718 function normalizeAnnotationPoint(payload = {}, viewport = annotationViewport()) {
3719 const source = payload?.point && typeof payload.point === "object" ? payload.point : payload;
3720 const width = Math.max(1, Number(viewport.width || 1));
3721 const height = Math.max(1, Number(viewport.height || 1));
3722 return {
3723 x: Math.max(0, Math.min(width, Number(source?.x || 0))),
3724 y: Math.max(0, Math.min(height, Number(source?.y || 0)))
3725 };
3726 }
3727
3728 function normalizeAnnotationRectPayload(payload = {}, viewport = annotationViewport()) {
3729 const source = payload?.rect && typeof payload.rect === "object" ? payload.rect : payload;
3730 const width = Math.max(1, Number(viewport.width || 1));
3731 const height = Math.max(1, Number(viewport.height || 1));
3732 const x = Math.max(0, Math.min(width, Number(source?.x || 0)));
3733 const y = Math.max(0, Math.min(height, Number(source?.y || 0)));
3734 return {
3735 height: Math.max(1, Math.min(height - y, Number(source?.height || source?.h || 1))),
3736 width: Math.max(1, Math.min(width - x, Number(source?.width || source?.w || 1))),
3737 x,
3738 y
3739 };
3740 }
3741
3742 function intersectRects(leftRect, rightRect) {
3743 if (!leftRect || !rightRect) {
3744 return null;
3745 }
3746
3747 const x = Math.max(Number(leftRect.x || 0), Number(rightRect.x || 0));
3748 const y = Math.max(Number(leftRect.y || 0), Number(rightRect.y || 0));
3749 const right = Math.min(
3750 Number(leftRect.x || 0) + Number(leftRect.width || 0),
3751 Number(rightRect.x || 0) + Number(rightRect.width || 0)
3752 );
3753 const bottom = Math.min(
3754 Number(leftRect.y || 0) + Number(leftRect.height || 0),
3755 Number(rightRect.y || 0) + Number(rightRect.height || 0)
3756 );
3757 const width = right - x;
3758 const height = bottom - y;
3759 if (width <= 0 || height <= 0) {
3760 return null;
3761 }
3762 return {
3763 area: width * height,
3764 height,
3765 width,
3766 x,
3767 y
3768 };
3769 }
3770
3771 function deepElementFromPoint(x, y) {
3772 let element = null;
3773 try {
3774 element = globalThis.document?.elementFromPoint?.(x, y) || null;
3775 } catch {
3776 return null;
3777 }
3778
3779 let guard = 0;
3780 while (isElementNode(element) && element.shadowRoot && guard < 8) {
3781 guard += 1;
3782 try {
3783 const nestedElement = element.shadowRoot.elementFromPoint?.(x, y);
3784 if (!nestedElement || nestedElement === element) {
3785 break;
3786 }
3787 element = nestedElement;
3788 } catch {
3789 break;
3790 }
3791 }
3792
3793 return element;
3794 }
3795
3796 function findAnnotationTarget(element) {
3797 if (!isElementNode(element)) {
3798 return null;
3799 }
3800
3801 const selector = [
3802 "a[href]",
3803 "button",
3804 "input",
3805 "textarea",
3806 "select",
3807 "summary",
3808 "[role]",
3809 "img",
3810 "label",
3811 "form",
3812 "h1",
3813 "h2",
3814 "h3",
3815 "h4",
3816 "h5",
3817 "h6",
3818 "p",
3819 "li",
3820 "td",
3821 "th",
3822 "article",
3823 "section",
3824 "nav",
3825 "header",
3826 "main",
3827 "footer"
3828 ].join(",");
3829 const target = element.closest?.(selector) || element;
3830 return isElementNode(target) && !isHiddenElement(target) ? target : element;
3831 }
3832
3833 function isMeaningfulAnnotationElement(element) {
3834 if (!isElementNode(element) || isHiddenElement(element)) {
3835 return false;
3836 }
3837
3838 if (isInteractiveElement(element) || getTagName(element) === "IMG") {
3839 return true;
3840 }
3841
3842 const tagName = getTagName(element);
3843 const role = normalizeAttributeText(element.getAttribute?.("role")).toLowerCase();
3844 return Boolean(
3845 role
3846 || /^H[1-6]$/u.test(tagName)
3847 || ["ARTICLE", "SECTION", "MAIN", "NAV", "HEADER", "FOOTER", "FORM", "LABEL", "P", "LI", "TD", "TH"].includes(tagName)
3848 );
3849 }
3850
3851 function collectIntersectingAnnotationElements(rect) {
3852 const selector = [
3853 "a[href]",
3854 "button",
3855 "input",
3856 "textarea",
3857 "select",
3858 "summary",
3859 "[role]",
3860 "img",
3861 "label",
3862 "form",
3863 "h1",
3864 "h2",
3865 "h3",
3866 "h4",
3867 "h5",
3868 "h6",
3869 "p",
3870 "li",
3871 "td",
3872 "th",
3873 "article",
3874 "section",
3875 "main",
3876 "nav",
3877 "header",
3878 "footer"
3879 ].join(",");
3880 let candidates = [];
3881 try {
3882 candidates = [...(globalThis.document?.querySelectorAll?.(selector) || [])];
3883 } catch {
3884 candidates = [];
3885 }
3886
3887 const seen = new Set();
3888 return candidates
3889 .map((element) => {
3890 if (!isMeaningfulAnnotationElement(element) || seen.has(element)) {
3891 return null;
3892 }
3893 seen.add(element);
3894 const elementRect = getElementRectSafe(element);
3895 const intersection = intersectRects(rect, elementRect);
3896 if (!intersection || intersection.area < 48) {
3897 return null;
3898 }
3899 return {
3900 element,
3901 elementArea: Math.max(1, Number(elementRect.width || 0) * Number(elementRect.height || 0)),
3902 intersection
3903 };
3904 })
3905 .filter(Boolean)
3906 .sort((left, right) => {
3907 if (right.intersection.area !== left.intersection.area) {
3908 return right.intersection.area - left.intersection.area;
3909 }
3910 return left.elementArea - right.elementArea;
3911 })
3912 .slice(0, 12)
3913 .map((entry) => summarizeAnnotationElement(entry.element))
3914 .filter(Boolean);
3915 }
3916
3917 function annotate(payload = null) {
3918 const request = payload && typeof payload === "object" ? payload : {};
3919 const viewport = annotationViewport();
3920 const kind = request.kind === "area" || request.rect ? "area" : "element";
3921
3922 if (kind === "area") {
3923 const rect = normalizeAnnotationRectPayload(request, viewport);
3924 const point = {
3925 x: rect.x + rect.width / 2,
3926 y: rect.y + rect.height / 2
3927 };
3928 const elements = collectIntersectingAnnotationElements(rect);
3929 const fallbackElement = findAnnotationTarget(deepElementFromPoint(point.x, point.y));
3930 const fallbackTarget = fallbackElement ? summarizeAnnotationElement(fallbackElement) : null;
3931 return {
3932 elements,
3933 kind,
3934 point,
3935 rect,
3936 status: elements.length || fallbackTarget ? "ok" : "empty",
3937 target: elements[0] || fallbackTarget,
3938 viewport
3939 };
3940 }
3941
3942 const point = normalizeAnnotationPoint(request, viewport);
3943 const rawElement = deepElementFromPoint(point.x, point.y);
3944 const targetElement = findAnnotationTarget(rawElement);
3945 const target = targetElement ? summarizeAnnotationElement(targetElement) : null;
3946 return {
3947 kind,
3948 point,
3949 rect: target?.rect || {
3950 height: 1,
3951 width: 1,
3952 x: point.x,
3953 y: point.y
3954 },
3955 status: target ? "ok" : "empty",
3956 target,
3957 viewport
3958 };
3959 }
3960
3961 globalThis[GLOBAL_KEY] = {
3962 click(referenceId) {
3963 return activateElement(referenceId);
3964 },
3965 annotate,
3966 capture,
3967 clear() {
3968 state.captureId = 0;
3969 state.capturedAt = 0;
3970 state.captureOptions = {
3971 includeLabelQuotes: false,
3972 includeLinkUrls: false,
3973 includeSemanticTags: true,
3974 includeStateTags: true,
3975 includeListIndentation: true,
3976 includeListMarkers: false
3977 };
3978 state.entries = new Map();
3979 },
3980 detail,
3981 getState() {
3982 return {
3983 captureId: state.captureId,
3984 capturedAt: state.capturedAt,
3985 includeLabelQuotes: state.captureOptions.includeLabelQuotes === true,
3986 includeLinkUrls: state.captureOptions.includeLinkUrls === true,
3987 includeSemanticTags: state.captureOptions.includeSemanticTags !== false,
3988 includeStateTags: state.captureOptions.includeStateTags !== false,
3989 includeListIndentation: state.captureOptions.includeListIndentation !== false,
3990 includeListMarkers: state.captureOptions.includeListMarkers === true,
3991 referenceCount: state.entries.size
3992 };
3993 },
3994 scroll(referenceId) {
3995 return scrollToReference(referenceId);
3996 },
3997 submit(referenceId) {
3998 return submitElement(referenceId);
3999 },
4000 type(referenceId, value) {
4001 return updateElementValue(referenceId, value);
4002 },
4003 typeSubmit(referenceId, value) {
4004 return typeAndSubmit(referenceId, value);
4005 },
4006 boundingBoxFor,
4007 fileInputElementFor,
4008 fileInputFor,
4009 pointFor,
4010 select(referenceId, valueOrValues) {
4011 return selectReference(referenceId, valueOrValues);
4012 },
4013 setChecked(referenceId, checked) {
4014 return setCheckedReference(referenceId, checked);
4015 },
4016 ready() {
4017 const api = globalThis[GLOBAL_KEY];
4018 return Boolean(api && REQUIRED_API_NAMES.every((name) => typeof api[name] === "function"));
4019 },
4020 requiredApis: REQUIRED_API_NAMES.slice(),
4021 version: VERSION
4022 };
4023 })();