UNPKG

@hyperbrowser/agent

Version:

Hyperbrowsers Web Agent

2 lines (1 loc) 18.5 kB
export declare const buildDomViewJs = "(() => {\n // src/context-providers/dom/const.ts\n var INTERACTIVE_ELEMENTS = /* @__PURE__ */ new Set([\n \"a\",\n \"input\",\n \"button\",\n \"select\",\n \"menu\",\n \"menuitem\",\n \"textarea\",\n \"canvas\",\n \"embed\"\n ]);\n var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([\n \"button\",\n \"link\",\n \"checkbox\",\n \"radio\",\n \"textbox\",\n \"menuitem\",\n \"tab\",\n \"tabpanel\",\n \"tooltip\",\n \"slider\",\n \"progressbar\",\n \"switch\",\n \"listbox\",\n \"option\",\n \"combobox\",\n \"menu\",\n \"treeitem\",\n \"tree\",\n \"spinbutton\",\n \"scrollbar\",\n \"menuitemcheckbox\",\n \"menuitemradio\",\n \"action\"\n ]);\n var INTERACTIVE_ARIA_PROPS = [\n \"aria-expanded\",\n \"aria-pressed\",\n \"aria-selected\",\n \"aria-checked\"\n ];\n var CLICK_ATTRIBUTES = [\"onclick\", \"ng-click\", \"@click\", \"v-on:click\"];\n var CONTEXT_ATTRIBUTES = [\n \"title\",\n \"type\",\n \"name\",\n \"role\",\n \"aria-label\",\n \"placeholder\",\n \"value\",\n \"checked\",\n \"alt\",\n \"aria-expanded\"\n ];\n\n // src/context-providers/dom/elem-interactive.ts\n var isInteractiveElem = (element) => {\n const tagName = element.tagName.toLowerCase();\n const role = element.getAttribute(\"role\");\n const ariaRole = element.getAttribute(\"aria-role\");\n const hasInteractiveRole = INTERACTIVE_ELEMENTS.has(tagName) || INTERACTIVE_ROLES.has(role || \"\") || INTERACTIVE_ROLES.has(ariaRole || \"\");\n if (hasInteractiveRole) {\n let reason = \"\";\n if (INTERACTIVE_ELEMENTS.has(tagName)) {\n reason = `Interactive HTML element: <${tagName}>`;\n } else if (INTERACTIVE_ROLES.has(role || \"\")) {\n reason = `Interactive role: ${role}`;\n } else if (INTERACTIVE_ROLES.has(ariaRole || \"\")) {\n reason = `Interactive aria-role: ${ariaRole}`;\n }\n return { isInteractive: true, reason };\n }\n const hasClickHandler = element.onclick !== null || element.getAttribute(\"onclick\") !== null || CLICK_ATTRIBUTES.some((attr) => element.hasAttribute(attr));\n if (hasClickHandler) {\n return { isInteractive: true, reason: \"Has click handler\" };\n }\n const hasInjectedListener = element.hasAttribute(\"data-has-interactive-listener\");\n if (hasInjectedListener) {\n return { isInteractive: true, reason: \"Has interactive event listener (tracked)\" };\n }\n const hasAriaProps = INTERACTIVE_ARIA_PROPS.some(\n (prop) => element.hasAttribute(prop)\n );\n if (hasAriaProps) {\n const props = INTERACTIVE_ARIA_PROPS.filter(\n (prop) => element.hasAttribute(prop)\n );\n return {\n isInteractive: true,\n reason: `Has interactive ARIA properties: ${props.join(\", \")}`\n };\n }\n const isContentEditable = element.getAttribute(\"contenteditable\") === \"true\" || element.isContentEditable;\n if (isContentEditable) {\n return { isInteractive: true, reason: \"Is content editable\" };\n }\n const isDraggable = element.draggable || element.getAttribute(\"draggable\") === \"true\";\n if (isDraggable) {\n return { isInteractive: true, reason: \"Is draggable\" };\n }\n return { isInteractive: false, reason: \"Not interactive\" };\n };\n var isIgnoredElem = (element) => {\n const rect = element.getBoundingClientRect();\n const isNotVisible = rect.width === 0 || rect.height === 0;\n return element.tagName.toLowerCase() === \"html\" || element.tagName.toLowerCase() === \"body\" || isNotVisible || element.hasAttribute(\"disabled\") || element.getAttribute(\"aria-disabled\") === \"true\";\n };\n\n // src/context-providers/dom/find-interactive-elements.ts\n var findInteractiveElements = () => {\n const intereactiveElements = [];\n const processedElements = /* @__PURE__ */ new Set();\n const processRoot = (root, rootInfo = {}) => {\n const elements = root.querySelectorAll(\"*\");\n for (let i = 0; i < elements.length; i++) {\n const element = elements[i];\n if (processedElements.has(element)) {\n continue;\n }\n processedElements.add(element);\n if (element.shadowRoot) {\n processRoot(element.shadowRoot, {\n iframe: rootInfo.iframe,\n shadowHost: element\n });\n }\n const { isInteractive, reason } = isInteractiveElem(element);\n if (isIgnoredElem(element) || !isInteractive) {\n continue;\n }\n intereactiveElements.push({\n element,\n iframe: rootInfo.iframe,\n shadowHost: rootInfo.shadowHost,\n rect: element.getBoundingClientRect(),\n interactiveReason: reason,\n isUnderShadowRoot: element.getRootNode().nodeType === Node.DOCUMENT_FRAGMENT_NODE,\n cssPath: \"\",\n xpath: \"\"\n });\n }\n };\n processRoot(document);\n const iframes = document.querySelectorAll(\"iframe\");\n for (let i = 0; i < iframes.length; i++) {\n const iframe = iframes[i];\n try {\n const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;\n if (iframeDoc) {\n processRoot(iframeDoc, { iframe });\n }\n } catch (e) {\n console.warn(\"error processing iframe\", e);\n }\n }\n return intereactiveElements;\n };\n\n // src/context-providers/dom/highlight.ts\n var isElementPartiallyVisible = (rect) => {\n return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight && // These checks are relative to the current viewport\n rect.bottom > 0 && // where the rect was calculated.\n rect.left < window.innerWidth && rect.right > 0;\n };\n var getHighlightColor = (index) => {\n const colors = [\n \"#FF0000\",\n \"#00FF00\",\n \"#0000FF\",\n \"#FFA500\",\n \"#800080\",\n \"#008080\",\n \"#FF69B4\",\n \"#4B0082\",\n \"#FF4500\",\n \"#2E8B57\",\n \"#DC143C\",\n \"#4682B4\"\n ];\n const colorIndex = index % colors.length;\n const baseColor = colors[colorIndex];\n const backgroundColor = baseColor + \"1A\";\n return { baseColor, backgroundColor };\n };\n var calculateLabelPosition = (rect, iframeOffset, labelWidth, labelHeight, canvasWidth, canvasHeight) => {\n const top = rect.top + iframeOffset.y;\n const left = rect.left + iframeOffset.x;\n let labelTop = top - labelHeight;\n let labelLeft = left + rect.width - labelWidth;\n labelTop = Math.min(labelTop, canvasHeight - labelHeight);\n labelLeft = Math.min(labelLeft, canvasWidth - labelWidth);\n const elementBottom = top + rect.height;\n const elementRight = left + rect.width;\n if (labelTop + labelHeight > top && labelTop < elementBottom && labelLeft + labelWidth > left && labelLeft < elementRight) {\n labelTop = elementBottom;\n labelLeft = elementRight - labelWidth;\n labelTop = Math.min(labelTop, canvasHeight - labelHeight);\n labelLeft = Math.min(labelLeft, canvasWidth - labelWidth);\n }\n return { top: labelTop, left: labelLeft };\n };\n function renderHighlightsOffscreen(highlightInfos, width, height) {\n if (width <= 0 || height <= 0) {\n console.warn(\n \"Attempted to render highlights on zero-sized canvas. Will default to innerWidth x innerHeight\"\n );\n const emptyCanvas = new OffscreenCanvas(\n window.innerWidth,\n window.innerHeight\n );\n return emptyCanvas.transferToImageBitmap();\n }\n const dpr = window.devicePixelRatio || 1;\n const canvasWidth = width * dpr;\n const canvasHeight = height * dpr;\n const offscreenCanvas = new OffscreenCanvas(canvasWidth, canvasHeight);\n const ctx = offscreenCanvas.getContext(\"2d\", {\n alpha: true\n });\n ctx.scale(dpr, dpr);\n ctx.clearRect(0, 0, width, height);\n try {\n highlightInfos.forEach(({ element, index, parentIframe }) => {\n if (!document.body.contains(element)) {\n return;\n }\n const rect = element.getBoundingClientRect();\n if (!rect || rect.width === 0 || rect.height === 0 || !isElementPartiallyVisible(rect)) {\n return;\n }\n const iframeOffset = { x: 0, y: 0 };\n if (parentIframe && document.body.contains(parentIframe)) {\n const iframeRect = parentIframe.getBoundingClientRect();\n iframeOffset.x = iframeRect.left;\n iframeOffset.y = iframeRect.top;\n }\n const colors = getHighlightColor(index);\n const drawTop = rect.top + iframeOffset.y;\n const drawLeft = rect.left + iframeOffset.x;\n ctx.fillStyle = colors.backgroundColor;\n ctx.fillRect(drawLeft, drawTop, rect.width, rect.height);\n ctx.strokeStyle = colors.baseColor;\n ctx.lineWidth = 1;\n ctx.strokeRect(drawLeft, drawTop, rect.width, rect.height);\n const labelText = index.toString();\n const fontSize = Math.min(12, Math.max(9, rect.height * 0.3));\n ctx.font = `bold ${fontSize}px sans-serif`;\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n const textMetrics = ctx.measureText(labelText);\n const labelPadding = 4;\n const labelHeight = fontSize + labelPadding;\n const labelWidth = Math.max(\n labelHeight,\n textMetrics.width + labelPadding * 2\n );\n const labelPos = calculateLabelPosition(\n rect,\n iframeOffset,\n labelWidth,\n labelHeight,\n width,\n height\n );\n ctx.fillStyle = colors.baseColor;\n ctx.fillRect(labelPos.left, labelPos.top, labelWidth, labelHeight);\n ctx.fillStyle = \"white\";\n ctx.fillText(\n labelText,\n labelPos.left + labelWidth / 2,\n labelPos.top + labelHeight / 2\n );\n });\n return offscreenCanvas.transferToImageBitmap();\n } catch (error) {\n console.error(\"Error drawing highlights onto OffscreenCanvas:\", error);\n const emptyCanvas = new OffscreenCanvas(1, 1);\n return emptyCanvas.transferToImageBitmap();\n }\n }\n\n // src/context-providers/dom/get-css-path.ts\n var escapeSelector = (value) => {\n return CSS.escape(value);\n };\n var getUniqueSegment = (element) => {\n const tagName = element.tagName.toLowerCase();\n const parent = element.parentElement;\n if (element.id) {\n const idSelector = `#${escapeSelector(element.id)}`;\n return idSelector;\n }\n const classes = Array.from(element.classList).map(escapeSelector).join(\".\");\n if (classes && parent) {\n const classSelector = `${tagName}.${classes}`;\n const siblingsWithSameClasses = Array.from(\n parent.querySelectorAll(`:scope > ${classSelector}`)\n );\n if (siblingsWithSameClasses.length === 1 && siblingsWithSameClasses[0] === element) {\n return classSelector;\n }\n }\n let index = 1;\n let sibling = element.previousElementSibling;\n while (sibling) {\n if (sibling.tagName === element.tagName) {\n index++;\n }\n sibling = sibling.previousElementSibling;\n }\n let hasSameTypeSiblings = index > 1;\n if (!hasSameTypeSiblings && parent) {\n sibling = element.nextElementSibling;\n while (sibling) {\n if (sibling.tagName === element.tagName) {\n hasSameTypeSiblings = true;\n break;\n }\n sibling = sibling.nextElementSibling;\n }\n }\n return hasSameTypeSiblings ? `${tagName}:nth-of-type(${index})` : tagName;\n };\n var getRelativeCSSPath = (element, boundary) => {\n if (element === boundary) {\n return \"\";\n }\n const segments = [];\n let currentElement = element;\n while (currentElement && currentElement !== boundary && currentElement.nodeType === Node.ELEMENT_NODE) {\n const segment = getUniqueSegment(currentElement);\n segments.unshift(segment);\n const parent = currentElement.parentElement;\n if (!parent || parent === boundary || parent.nodeType !== Node.ELEMENT_NODE) {\n break;\n }\n currentElement = parent;\n }\n return segments.join(\" > \");\n };\n var getCSSPath = (element) => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return \"\";\n }\n if (!element.isConnected) {\n }\n const root = element.getRootNode();\n if (root instanceof ShadowRoot) {\n const host = root.host;\n if (!host) {\n console.warn(\"ShadowRoot found without a host element:\", root);\n return \"\";\n }\n const hostPath = getCSSPath(host);\n const relativePath = getRelativeCSSPath(element, root);\n if (!hostPath) {\n console.warn(\"Could not determine CSS path for host element:\", host);\n return \"\";\n }\n if (!relativePath) {\n console.warn(\n \"Could not determine relative CSS path within ShadowRoot for:\",\n element\n );\n return \"\";\n }\n return `${hostPath} >> ${relativePath}`;\n } else if (root instanceof Document) {\n return getRelativeCSSPath(element, root);\n } else {\n console.warn(\n \"Element root is neither Document nor ShadowRoot:\",\n root,\n \"for element:\",\n element\n );\n return getRelativeCSSPath(element, root);\n }\n };\n\n // src/context-providers/dom/get-x-path.ts\n var getXPath = (element) => {\n const segments = [];\n let currentElement = element;\n while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {\n if (currentElement.parentNode instanceof ShadowRoot || currentElement.parentNode instanceof HTMLIFrameElement) {\n break;\n }\n let index = 0;\n let hasSiblings = false;\n let sibling = currentElement.previousSibling;\n while (sibling) {\n if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === currentElement.nodeName) {\n index++;\n hasSiblings = true;\n }\n sibling = sibling.previousSibling;\n }\n if (!hasSiblings) {\n sibling = currentElement.nextSibling;\n while (sibling) {\n if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === currentElement.nodeName) {\n hasSiblings = true;\n break;\n }\n sibling = sibling.nextSibling;\n }\n }\n const tagName = currentElement.nodeName.toLowerCase();\n const xpathIndex = hasSiblings ? `[${index + 1}]` : \"\";\n if (currentElement.id && currentElement.id.toString().trim() !== \"\") {\n segments.unshift(`${tagName}[@id=\"${currentElement.id}\"]`);\n } else {\n segments.unshift(`${tagName}${xpathIndex}`);\n }\n currentElement = currentElement.parentElement;\n }\n return segments.join(\"/\");\n };\n\n // src/context-providers/dom/build-dom-view.ts\n var imageBitmapToPngDataUrl = (bitmap) => {\n try {\n const canvas = document.createElement(\"canvas\");\n canvas.width = bitmap.width;\n canvas.height = bitmap.height;\n const ctx = canvas.getContext(\"2d\");\n ctx.drawImage(bitmap, 0, 0);\n return canvas.toDataURL(\"image/png\");\n } finally {\n bitmap.close();\n }\n };\n var getElementTextContent = (el) => {\n const tagName = el.tagName.toLowerCase();\n if (tagName === \"input\") {\n const inputElement = el;\n let labelText = null;\n if (inputElement.id) {\n const label = document.querySelector(`label[for=\"${inputElement.id}\"]`);\n if (label) {\n labelText = label.textContent?.trim() || null;\n }\n }\n return labelText ?? inputElement.value?.trim() ?? \"\";\n } else {\n return el.textContent?.trim() || \"\";\n }\n };\n var buildDomView = () => {\n const interactiveElements = findInteractiveElements();\n const screenBitmap = renderHighlightsOffscreen(\n interactiveElements.map((element, index) => ({\n element: element.element,\n index: index + 1,\n // index range from 1 -> index\n parentIframe: element.iframe ?? null\n })),\n window.innerWidth,\n window.innerHeight\n );\n const screenshotPngDataUrl = imageBitmapToPngDataUrl(screenBitmap);\n for (let idx = 0; idx < interactiveElements.length; idx++) {\n const element = interactiveElements[idx];\n element.highlightIndex = idx + 1;\n element.cssPath = getCSSPath(element.element);\n element.xpath = getXPath(element.element);\n }\n const domRepresentation = [];\n const getTextBetween = (node, nextNode) => {\n const texts = [];\n let current = node.nextSibling;\n while (current && current !== nextNode) {\n if (current.nodeType === Node.TEXT_NODE && current.textContent) {\n const text = current.textContent.trim();\n if (text) texts.push(text);\n }\n current = current.nextSibling;\n }\n return texts.join(\" \");\n };\n for (let i = 0; i < interactiveElements.length; i++) {\n const element = interactiveElements[i];\n const el = element.element;\n const tagName = el.tagName.toLowerCase();\n let attributes = \"\";\n Array.from(el.attributes).forEach((attr) => {\n if (CONTEXT_ATTRIBUTES.includes(attr.name)) {\n attributes += ` ${attr.name}=\"${attr.value}\"`;\n }\n });\n const textContent = getElementTextContent(el);\n const indexPrefix = `[${element.highlightIndex}]`;\n const elementString = `${indexPrefix}<${tagName}${attributes}>${textContent.replace(/\\s+/g, \" \")}</${tagName}>`;\n domRepresentation.push(elementString);\n const nextElement = interactiveElements[i + 1]?.element || null;\n const betweenText = getTextBetween(el, nextElement);\n if (betweenText) {\n domRepresentation.push(betweenText);\n }\n }\n return {\n elements: interactiveElements,\n domState: domRepresentation.join(\"\\n\"),\n screenshot: screenshotPngDataUrl\n };\n };\n return buildDomView();\n})();";