/** * src/core/extractor.js * Renders HTML in headless Playwright, extracts computed styles * and bounding rects for every DOM element. */ import { chromium } from 'playwright-core'; import { existsSync, statSync } from 'fs'; import { dirname, resolve } from 'path'; import { pathToFileURL } from 'url'; /** * @param {string} filePath - absolute or relative path to HTML file * @param {{ width: number, height: number }} viewport * @returns {{ domTree, title: string }} */ export async function extractFromFile(filePath, { width = 1440, height = 900 } = {}) { const absPath = resolve(filePath); const browser = await chromium.launch({ args: ['--disable-web-security'] }); const page = await browser.newPage({ viewport: { width, height } }); await page.goto(pathToFileURL(absPath).href); const result = await extractFromPage(page); await browser.close(); return result; } /** * @param {string} html * @param {{ width?: number, height?: number, baseUrl?: string | null }} options * @returns {{ domTree, title: string }} */ export async function extractFromHtml(html, { width = 1440, height = 900, baseUrl = null } = {}) { const browser = await chromium.launch({ args: ['--disable-web-security'] }); const page = await browser.newPage({ viewport: { width, height } }); const htmlWithBase = injectBaseHref(html, normalizeBaseUrl(baseUrl)); await page.setContent(htmlWithBase, { waitUntil: 'load' }); const result = await extractFromPage(page); await browser.close(); return result; } async function extractFromPage(page) { await stabilizePage(page); // Walk the full DOM and capture computed styles + rects const title = await page.title(); const domTree = await page.evaluate(walkDOMInBrowser); return { domTree, title }; } async function stabilizePage(page) { await page.waitForLoadState('networkidle'); // Wait for all images to finish loading completely (with a 2-second timeout) await page.evaluate(async () => { const imgs = Array.from(document.querySelectorAll('img')); await Promise.all(imgs.map(img => { if (img.complete) return Promise.resolve(); return new Promise(resolve => { const timer = setTimeout(resolve, 2000); img.onload = () => { clearTimeout(timer); resolve(); }; img.onerror = () => { clearTimeout(timer); resolve(); }; }); })); }); await page.evaluate(() => { document.querySelectorAll('.reveal').forEach(el => el.classList.add('visible')); const animated = Array.from(document.querySelectorAll('*')).filter((el) => { const cs = window.getComputedStyle(el); return cs.animationName !== 'none' || cs.transitionDuration !== '0s'; }); animated.forEach((el) => el.setAttribute('data-morphus-animated', '1')); const style = document.createElement('style'); style.textContent = '*, *::before, *::after { animation: none !important; transition: none !important; }'; document.head.appendChild(style); document.querySelectorAll('[data-morphus-animated="1"]').forEach((el) => { const cs = window.getComputedStyle(el); if (cs.opacity === '0' && shouldForceAnimatedElementVisible(el, cs)) { el.style.opacity = '1'; if (isTranslateOnlyTransform(cs.transform)) { el.style.transform = 'none'; } } }); limitPaginatedTableRows(); function shouldForceAnimatedElementVisible(el, cs) { if (cs.display === 'none' || cs.visibility === 'hidden') { return false; } if (cs.position === 'fixed') { return false; } if (el.closest('[hidden], [aria-hidden="true"], [inert]')) { return false; } if (hasLiveOrProgressSemantics(el)) { return false; } const rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } function hasLiveOrProgressSemantics(el) { if (!el || el.nodeType !== Node.ELEMENT_NODE) { return false; } const role = String(el.getAttribute('role') || '').toLowerCase(); if (role === 'progressbar' || role === 'status' || role === 'alert') { return true; } if (el.hasAttribute('aria-busy') || el.hasAttribute('aria-live')) { return true; } return Boolean(el.querySelector('progress, meter, [role="progressbar"], [aria-busy], [aria-live]')); } function isTranslateOnlyTransform(value) { if (!value || value === 'none') { return false; } const text = String(value).trim(); const matrixMatch = text.match(/^matrix\(([^)]+)\)$/i); if (matrixMatch) { const values = matrixMatch[1] .split(',') .map((part) => parseFloat(part.trim())); if (values.length === 6 && values.every((number) => Number.isFinite(number))) { const tolerance = 0.001; return Math.abs(values[0] - 1) <= tolerance && Math.abs(values[1]) <= tolerance && Math.abs(values[2]) <= tolerance && Math.abs(values[3] - 1) <= tolerance; } } return /^translate(?:3d|x|y)?\(/i.test(text); } function limitPaginatedTableRows() { const pagers = Array.from(document.querySelectorAll('*')).filter((el) => isPaginationElement(el)); for (const pager of pagers) { const pagerRect = pager.getBoundingClientRect(); if (pagerRect.width <= 0 || pagerRect.height <= 0) { continue; } const container = findPaginatedDataContainer(pager, pagerRect); if (!container) { continue; } const rows = getDataRows(container).filter((row) => !pager.contains(row)); if (rows.length < 8) { continue; } const cutoffY = pagerRect.bottom - 1; let hiddenCount = 0; for (const row of rows) { const rect = row.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) { continue; } if (rect.top >= cutoffY) { row.setAttribute('data-morphus-paginated-row-clipped', '1'); row.style.display = 'none'; hiddenCount++; } } if (hiddenCount > 0) { container.setAttribute('data-morphus-paginated-preview', '1'); } } } function isPaginationElement(el) { if (!el || el.nodeType !== Node.ELEMENT_NODE) { return false; } const identity = `${el.id || ''} ${String(el.className || '')} ${el.getAttribute('role') || ''}`.toLowerCase(); if (/(pagination|paginator|pager|page-nav|page-control)/.test(identity)) { return true; } const text = normalizePaginationText(el.innerText || el.textContent || ''); if (!text || text.length > 160) { return false; } return /\b(hal|page)\s*\d+\s*\/\s*\d+\b/i.test(text) || /\b(baris|rows?)\s+\d+\s*[–-]\s*\d+\b/i.test(text); } function findPaginatedDataContainer(pager, pagerRect) { let current = pager.parentElement; while (current && current !== document.documentElement) { const rows = getDataRows(current).filter((row) => !pager.contains(row)); if (rows.length >= 8) { const before = rows.filter((row) => { const rect = row.getBoundingClientRect(); return rect.width > 0 && rect.height > 0 && rect.bottom <= pagerRect.top + 1; }); const after = rows.filter((row) => { const rect = row.getBoundingClientRect(); return rect.width > 0 && rect.height > 0 && rect.top >= pagerRect.bottom - 1; }); if (before.length >= 3 && after.length >= 1) { return current; } } current = current.parentElement; } return null; } function getDataRows(container) { const seen = new Set(); const rows = []; const selectors = ['tr', '[role="row"]']; for (const selector of selectors) { for (const row of container.querySelectorAll(selector)) { if (seen.has(row) || row.closest('thead')) { continue; } seen.add(row); rows.push(row); } } return rows; } function normalizePaginationText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); } }); await waitForCanvasPaint(page); } async function waitForCanvasPaint(page) { const canvasCount = await page.locator('canvas').count(); if (canvasCount === 0) { return; } try { await page.waitForFunction(() => { const canvases = Array.from(document.querySelectorAll('canvas')) .filter((canvas) => { const rect = canvas.getBoundingClientRect(); const cs = window.getComputedStyle(canvas); const opacity = parseFloat(cs.opacity); return rect.width > 0 && rect.height > 0 && cs.display !== 'none' && cs.visibility !== 'hidden' && (!Number.isFinite(opacity) || opacity > 0); }); if (canvases.length === 0) { return true; } const now = performance.now(); const state = window.__morphusCanvasCaptureState || { startedAt: now, lastSignature: '', stableCount: 0, }; const snapshots = canvases.map((canvas) => captureCanvasSnapshot(canvas)); const readableSnapshots = snapshots.filter((snapshot) => snapshot.readable); if (readableSnapshots.length === 0) { return true; } const signature = readableSnapshots.map((snapshot) => snapshot.signature).join('|'); if (signature && signature === state.lastSignature) { state.stableCount += 1; } else { state.lastSignature = signature; state.stableCount = 0; } window.__morphusCanvasCaptureState = state; return state.stableCount >= 2 && now - state.startedAt >= 500; function captureCanvasSnapshot(canvas) { const width = Math.floor(canvas.width || 0); const height = Math.floor(canvas.height || 0); if (width <= 0 || height <= 0) { return { readable: false }; } let src = ''; try { src = canvas.toDataURL('image/png'); } catch (err) { return { readable: false }; } canvas.__morphusCanvasCaptureSrc = src; return { readable: Boolean(src && src !== 'data:,'), signature: `${width}x${height}:${src.length}:${hashString(src)}`, }; } function hashString(value) { let hash = 2166136261; const text = String(value || ''); for (let index = 0; index < text.length; index++) { hash ^= text.charCodeAt(index); hash = Math.imul(hash, 16777619); } return hash >>> 0; } }, null, { timeout: 2500, polling: 80 }); } catch (err) { // Continue with the best available canvas state instead of failing conversion. } } function normalizeBaseUrl(baseUrl) { if (!baseUrl) return null; if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(baseUrl)) return baseUrl; const absPath = resolve(baseUrl); const targetPath = existsSync(absPath) && !statSync(absPath).isDirectory() ? dirname(absPath) : absPath; let href = pathToFileURL(targetPath).href; if (!href.endsWith('/')) { href += '/'; } return href; } function injectBaseHref(html, baseHref) { if (!baseHref || /`; if (/]*>/i.test(html)) { return html.replace(/]*)>/i, `\n${baseTag}`); } if (/]*>/i.test(html)) { return html.replace(/]*)>/i, `${baseTag}`); } return `${baseTag}${html}`; } function escapeHtmlAttribute(value) { return String(value) .replace(/&/g, '&') .replace(/"/g, '"'); } /** * This function is serialized and run inside the browser context. * It must be self-contained (no imports). */ function walkDOMInBrowser() { const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'LINK', 'META', 'HEAD', 'NOSCRIPT']); const TEXT_TAGS = new Set(['p', 'span', 'a', 'label', 'em', 'strong', 'b', 'i', 'small', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'th']); const INLINE_TAGS = new Set([ 'a', 'abbr', 'b', 'bdi', 'bdo', 'cite', 'code', 'data', 'dfn', 'em', 'i', 'kbd', 'label', 'mark', 'q', 's', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'br', 'wbr', ]); const TEXT_INPUT_TYPES = new Set([ '', 'date', 'datetime-local', 'email', 'month', 'number', 'password', 'search', 'tel', 'text', 'time', 'url', 'week', ]); function getNode(el, depth = 0) { if (SKIP_TAGS.has(el.tagName)) return null; const rect = el.getBoundingClientRect(); const cs = window.getComputedStyle(el); const csBefore = window.getComputedStyle(el, '::before'); const csAfter = window.getComputedStyle(el, '::after'); const tag = el.tagName.toLowerCase(); const isSvg = tag === 'svg'; const isImage = tag === 'img'; const isCanvas = tag === 'canvas'; // Skip invisible/zero-size elements if (rect.width === 0 && rect.height === 0 && cs.position === 'static') return null; if (isVisuallyHiddenElement(cs)) return null; const rawText = normalizeTextContent(el.innerText || el.textContent || ''); const hasVisualBox = !isTransparentColor(cs.backgroundColor) || cs.backgroundImage !== 'none' || cs.borderStyle !== 'none' || parseFloat(cs.borderTopWidth) > 0 || parseFloat(cs.borderRightWidth) > 0 || parseFloat(cs.borderBottomWidth) > 0 || parseFloat(cs.borderLeftWidth) > 0 || parseFloat(cs.paddingTop) > 0 || parseFloat(cs.paddingRight) > 0 || parseFloat(cs.paddingBottom) > 0 || parseFloat(cs.paddingLeft) > 0 || cs.boxShadow !== 'none' || cs.filter !== 'none'; const inlineVisualFragments = extractInlineVisualFragments(el, tag, cs, csBefore, csAfter, rect, rawText, hasVisualBox); if (inlineVisualFragments) { return inlineVisualFragments; } const inlineTextFragments = extractInlineTextFragments(el, tag, cs, csBefore, csAfter, rect, rawText, hasVisualBox); if (inlineTextFragments) { return inlineTextFragments; } const hasOnlyInlineTextChildren = Boolean(rawText) && Array.from(el.children).length > 0 && Array.from(el.children).every((child) => isInlineTextChild(child)); const isTextContainer = Boolean(rawText) && !hasVisualBox && !hasRenderablePseudo(csBefore) && !hasRenderablePseudo(csAfter) && canCollapseToTextContainer(el, tag, cs, hasOnlyInlineTextChildren); const beforeData = extractPseudoElementData(el, tag, cs, csBefore, 'before'); const afterData = extractPseudoElementData(el, tag, cs, csAfter, 'after'); const formControl = extractFormControlData(el, tag, cs); const nodeText = formControl ? null : rawText; const textData = nodeText ? extractTextData(el) : null; const renderedText = textData?.text || nodeText; const svgMarkup = isSvg ? serializeSvgElement(el, rect) : null; const imageData = isImage ? extractImageData(el) : isCanvas ? extractCanvasImageData(el) : null; const children = isSvg || isImage || isCanvas || isTextContainer || formControl ? [] : Array.from(el.childNodes) .map((child) => getChildNode(child, el, cs, depth + 1)) .filter(Boolean); return { tag, id: el.id || null, classList: Array.from(el.classList), text: renderedText || null, textRuns: textData?.runs || [], isTextContainer, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height, offsetWidth: el.offsetWidth !== undefined ? el.offsetWidth : rect.width, offsetHeight: el.offsetHeight !== undefined ? el.offsetHeight : rect.height, }, computed: extractRelevantStyles(cs), ...(formControl ? { formControl } : {}), ...(svgMarkup ? { svgMarkup } : {}), ...(imageData ? { imageData } : {}), pseudo: { before: beforeData, after: afterData, }, children, }; } function extractInlineVisualFragments(el, tag, cs, csBefore, csAfter, rect, rawText, hasVisualBox) { if (!shouldSplitInlineVisualElement(el, tag, cs, csBefore, csAfter, rawText, hasVisualBox)) { return null; } const fragmentRects = getElementClientRects(el); if (fragmentRects.length <= 1) { return null; } const lines = collectInlineVisualTextLines(el, fragmentRects); if (!lines.some((line) => line.text)) { return null; } const wrapperComputed = makeTransparentInlineWrapperStyles(cs, rect); const fragmentNodes = fragmentRects .map((fragmentRect, index) => buildInlineVisualFragmentNode(el, tag, cs, fragmentRect, lines[index])) .filter(Boolean); if (fragmentNodes.length <= 1) { return null; } return { tag, id: el.id || null, classList: Array.from(el.classList), text: null, textRuns: [], isTextContainer: false, _inlineFragmentGroup: true, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, computed: wrapperComputed, pseudo: { before: null, after: null, }, children: fragmentNodes, }; } function extractInlineTextFragments(el, tag, cs, csBefore, csAfter, rect, rawText, hasVisualBox) { if (!shouldSplitInlineTextElement(el, tag, cs, csBefore, csAfter, rawText, hasVisualBox)) { return null; } const fragmentRects = getElementClientRects(el); if (fragmentRects.length <= 1) { return null; } const lines = collectInlineVisualTextLines(el, fragmentRects); const fragmentNodes = lines .map((line) => buildInlineTextFragmentNode(el, cs, line)) .filter(Boolean); if (fragmentNodes.length <= 1) { return null; } return { tag, id: el.id || null, classList: Array.from(el.classList), text: null, textRuns: [], isTextContainer: false, _inlineTextFragmentGroup: true, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, computed: makeTransparentInlineWrapperStyles(cs, rect), pseudo: { before: null, after: null, }, children: fragmentNodes, }; } function shouldSplitInlineTextElement(el, tag, cs, csBefore, csAfter, rawText, hasVisualBox) { if (hasVisualBox || !rawText || !INLINE_TAGS.has(tag)) { return false; } if (cs.display !== 'inline' || cs.position !== 'static') { return false; } if (hasRenderablePseudo(csBefore) || hasRenderablePseudo(csAfter)) { return false; } return getElementClientRects(el).length > 1; } function buildInlineTextFragmentNode(el, cs, line) { if (!line?.text || !line.textRect) { return null; } const computed = extractRelevantStyles(cs); computed.display = 'inline'; computed.position = 'static'; computed.width = `${line.textRect.width}px`; computed.height = `${line.textRect.height}px`; computed.minWidth = '0px'; computed.minHeight = '0px'; return { tag: el.tagName.toLowerCase(), id: null, classList: Array.from(el.classList), text: line.text, textRuns: line.runs, isTextContainer: true, _inlineTextFragment: true, rect: line.textRect, computed, pseudo: { before: null, after: null, }, children: [], }; } function shouldSplitInlineVisualElement(el, tag, cs, csBefore, csAfter, rawText, hasVisualBox) { if (!hasVisualBox || !rawText || !INLINE_TAGS.has(tag)) { return false; } if (cs.display !== 'inline' || cs.position !== 'static') { return false; } if (hasRenderablePseudo(csBefore) || hasRenderablePseudo(csAfter)) { return false; } return getElementClientRects(el).length > 1; } function getElementClientRects(el) { return Array.from(el.getClientRects()) .map(rectToPlainObject) .filter((rect) => rect.width > 0 && rect.height > 0); } function buildInlineVisualFragmentNode(el, tag, cs, fragmentRect, line) { const computed = extractRelevantStyles(cs); computed.display = 'inline'; computed.position = 'static'; computed.width = `${fragmentRect.width}px`; computed.height = `${fragmentRect.height}px`; computed.minWidth = '0px'; computed.minHeight = '0px'; const textNode = line?.text ? buildInlineFragmentTextNode(cs, line) : null; return { tag, id: null, classList: Array.from(el.classList), text: null, textRuns: [], isTextContainer: false, _inlineFragment: true, rect: fragmentRect, computed, pseudo: { before: null, after: null, }, children: textNode ? [textNode] : [], }; } function buildInlineFragmentTextNode(cs, line) { const computed = extractRelevantStyles(cs); computed.display = 'inline'; computed.position = 'static'; computed.width = `${line.textRect.width}px`; computed.height = `${line.textRect.height}px`; computed.minWidth = '0px'; computed.minHeight = '0px'; computed.backgroundColor = 'rgba(0, 0, 0, 0)'; computed.backgroundImage = 'none'; computed.paddingTop = '0px'; computed.paddingRight = '0px'; computed.paddingBottom = '0px'; computed.paddingLeft = '0px'; computed.border = '0px none rgba(0, 0, 0, 0)'; computed.borderWidth = '0px'; computed.borderStyle = 'none'; computed.boxShadow = 'none'; return { tag: 'span', id: null, classList: [], text: line.text, textRuns: line.runs, isTextContainer: true, _inlineFragmentText: true, rect: line.textRect, computed, pseudo: { before: null, after: null, }, children: [], }; } function collectInlineVisualTextLines(el, fragmentRects) { const lines = fragmentRects.map((fragmentRect) => ({ fragmentRect, text: '', runs: [], textRect: null, })); let pendingWhitespace = false; let previousLineIndex = null; function walkInlineText(node, styleEl) { if (node.nodeType === Node.TEXT_NODE) { collectTextNodeLines(node, styleEl); return; } if (node.nodeType !== Node.ELEMENT_NODE) { return; } const element = node; if (element.tagName.toLowerCase() === 'br') { pendingWhitespace = false; previousLineIndex = null; return; } for (const child of element.childNodes) { walkInlineText(child, element); } } function collectTextNodeLines(textNode, styleEl) { const text = textNode.textContent || ''; const tokenPattern = /\s+|\S+/g; let match; while ((match = tokenPattern.exec(text)) !== null) { const token = match[0]; if (/^\s+$/.test(token)) { pendingWhitespace = true; continue; } appendMeasuredToken(textNode, match.index, match.index + token.length, token, styleEl); } } function appendMeasuredToken(textNode, start, end, token, styleEl) { const tokenRects = measureTextRangeClientRects(textNode, start, end); if (tokenRects.length <= 1) { const rect = tokenRects[0]; if (!rect) return; appendLineText(findBestFragmentIndex(rect, fragmentRects), token, rect, styleEl); return; } let segment = ''; let segmentLineIndex = null; let segmentRect = null; for (let offset = start; offset < end; offset++) { const char = (textNode.textContent || '').slice(offset, offset + 1); const charRect = measureTextRangeClientRects(textNode, offset, offset + 1)[0]; if (!charRect) continue; const lineIndex = findBestFragmentIndex(charRect, fragmentRects); if (segment && lineIndex !== segmentLineIndex) { appendLineText(segmentLineIndex, segment, segmentRect, styleEl); segment = ''; segmentRect = null; } segment += char; segmentLineIndex = lineIndex; segmentRect = unionTwoRects(segmentRect, charRect); } if (segment) { appendLineText(segmentLineIndex, segment, segmentRect, styleEl); } } function appendLineText(lineIndex, rawToken, rect, styleEl) { const line = lines[lineIndex]; if (!line || !rawToken || !rect) { return; } const normalizedToken = normalizeTextFragment(rawToken); if (!normalizedToken) { return; } const prefix = pendingWhitespace && previousLineIndex === lineIndex && line.text ? ' ' : ''; const text = prefix + normalizedToken; line.text += text; line.runs.push({ text, lineIndex: 0, computed: extractTextRunStyles(window.getComputedStyle(styleEl || el)), }); line.textRect = unionTwoRects(line.textRect, rect); pendingWhitespace = false; previousLineIndex = lineIndex; } for (const child of el.childNodes) { walkInlineText(child, el); } for (const line of lines) { if (!line.textRect) { line.textRect = line.fragmentRect; } line.text = normalizeTextContent(line.text); } return lines; } function measureTextRangeClientRects(textNode, start, end) { const range = document.createRange(); range.setStart(textNode, start); range.setEnd(textNode, end); return Array.from(range.getClientRects()) .map(rectToPlainObject) .filter((rect) => rect.width > 0 && rect.height > 0); } function findBestFragmentIndex(rect, fragmentRects) { let bestIndex = 0; let bestScore = -1; const centerY = rect.y + rect.height / 2; for (let index = 0; index < fragmentRects.length; index++) { const fragment = fragmentRects[index]; const overlapX = Math.max(0, Math.min(rect.x + rect.width, fragment.x + fragment.width) - Math.max(rect.x, fragment.x)); const overlapY = Math.max(0, Math.min(rect.y + rect.height, fragment.y + fragment.height) - Math.max(rect.y, fragment.y)); const area = overlapX * overlapY; const yDistance = Math.abs(centerY - (fragment.y + fragment.height / 2)); const score = area > 0 ? area : -yDistance; if (score > bestScore) { bestScore = score; bestIndex = index; } } return bestIndex; } function makeTransparentInlineWrapperStyles(cs, rect) { const computed = extractRelevantStyles(cs); computed.display = 'inline'; computed.position = 'static'; computed.width = `${rect.width}px`; computed.height = `${rect.height}px`; computed.minWidth = '0px'; computed.minHeight = '0px'; computed.paddingTop = '0px'; computed.paddingRight = '0px'; computed.paddingBottom = '0px'; computed.paddingLeft = '0px'; computed.backgroundColor = 'rgba(0, 0, 0, 0)'; computed.backgroundImage = 'none'; computed.borderRadius = '0px'; computed.borderTopLeftRadius = '0px'; computed.borderTopRightRadius = '0px'; computed.borderBottomRightRadius = '0px'; computed.borderBottomLeftRadius = '0px'; computed.border = '0px none rgba(0, 0, 0, 0)'; computed.borderWidth = '0px'; computed.borderColor = 'rgba(0, 0, 0, 0)'; computed.borderStyle = 'none'; computed.borderTopWidth = '0px'; computed.borderRightWidth = '0px'; computed.borderBottomWidth = '0px'; computed.borderLeftWidth = '0px'; computed.borderTopColor = 'rgba(0, 0, 0, 0)'; computed.borderRightColor = 'rgba(0, 0, 0, 0)'; computed.borderBottomColor = 'rgba(0, 0, 0, 0)'; computed.borderLeftColor = 'rgba(0, 0, 0, 0)'; computed.borderTopStyle = 'none'; computed.borderRightStyle = 'none'; computed.borderBottomStyle = 'none'; computed.borderLeftStyle = 'none'; computed.boxShadow = 'none'; return computed; } function rectToPlainObject(rect) { return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, }; } function unionTwoRects(a, b) { if (!a) return b ? { x: b.x, y: b.y, width: b.width, height: b.height } : null; if (!b) return { x: a.x, y: a.y, width: a.width, height: a.height }; const left = Math.min(a.x, b.x); const top = Math.min(a.y, b.y); const right = Math.max(a.x + a.width, b.x + b.width); const bottom = Math.max(a.y + a.height, b.y + b.height); return { x: left, y: top, width: Math.max(right - left, 0), height: Math.max(bottom - top, 0), }; } function extractFormControlData(el, tag, computedStyles) { if (tag === 'select') { return extractSelectControlData(el); } if (tag !== 'input' && tag !== 'textarea') { return null; } const type = tag === 'input' ? String(el.getAttribute('type') || 'text').trim().toLowerCase() : 'textarea'; if (tag === 'input' && !TEXT_INPUT_TYPES.has(type)) { return null; } const placeholder = normalizeFormControlText(el.getAttribute('placeholder') || '', tag === 'textarea'); const value = type === 'password' ? '' : normalizeFormControlText(el.value || '', tag === 'textarea'); if (!placeholder && !value) { return null; } const placeholderComputed = placeholder ? extractPlaceholderStyles(el, computedStyles) : null; return { type, value, placeholder, ...(placeholderComputed ? { placeholderComputed } : {}), }; } function extractSelectControlData(el) { const selectedOptions = Array.from(el.selectedOptions || []); const renderedOptions = selectedOptions.length > 0 ? selectedOptions : [el.options && el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null].filter(Boolean); const renderedText = normalizeFormControlText( renderedOptions .map((option) => option.label || option.textContent || '') .filter(Boolean) .join(el.multiple ? '\n' : ' ') ); if (!renderedText) { return null; } const size = Number.parseInt(el.getAttribute('size') || '1', 10); return { type: 'select', value: renderedText, optionValue: renderedOptions[0] ? renderedOptions[0].value : el.value, hasChevron: !el.multiple && (!Number.isFinite(size) || size <= 1), }; } function extractPlaceholderStyles(el, fallbackStyles) { try { const placeholderStyles = window.getComputedStyle(el, '::placeholder'); if (placeholderStyles) { return extractRelevantStyles(placeholderStyles); } } catch (err) {} return extractRelevantStyles(fallbackStyles); } function extractRelevantStyles(cs) { return { display: cs.display, position: cs.position, zIndex: cs.zIndex, // Layout flexDirection: cs.flexDirection, justifyContent: cs.justifyContent, alignItems: cs.alignItems, flexWrap: cs.flexWrap, flexGrow: cs.flexGrow, flexShrink: cs.flexShrink, flexBasis: cs.flexBasis, gap: cs.gap, columnGap: cs.columnGap, rowGap: cs.rowGap, gridTemplateColumns: cs.gridTemplateColumns, gridTemplateRows: cs.gridTemplateRows, gridRow: cs.gridRow, gridColumn: cs.gridColumn, // Sizing width: cs.width, height: cs.height, minWidth: cs.minWidth, maxWidth: cs.maxWidth, minHeight: cs.minHeight, // Spacing paddingTop: cs.paddingTop, paddingRight: cs.paddingRight, paddingBottom: cs.paddingBottom, paddingLeft: cs.paddingLeft, marginTop: cs.marginTop, marginRight: cs.marginRight, marginBottom: cs.marginBottom, marginLeft: cs.marginLeft, // Visual backgroundColor: cs.backgroundColor, backgroundImage: cs.backgroundImage, backgroundSize: cs.backgroundSize, backgroundPosition: cs.backgroundPosition, objectFit: cs.objectFit, objectPosition: cs.objectPosition, color: cs.color, opacity: cs.opacity, borderRadius: cs.borderRadius, borderTopLeftRadius: cs.borderTopLeftRadius, borderTopRightRadius: cs.borderTopRightRadius, borderBottomRightRadius: cs.borderBottomRightRadius, borderBottomLeftRadius: cs.borderBottomLeftRadius, border: cs.border, borderWidth: cs.borderWidth, borderColor: cs.borderColor, borderStyle: cs.borderStyle, borderTopWidth: cs.borderTopWidth, borderRightWidth: cs.borderRightWidth, borderBottomWidth: cs.borderBottomWidth, borderLeftWidth: cs.borderLeftWidth, borderTopColor: cs.borderTopColor, borderRightColor: cs.borderRightColor, borderBottomColor: cs.borderBottomColor, borderLeftColor: cs.borderLeftColor, borderTopStyle: cs.borderTopStyle, borderRightStyle: cs.borderRightStyle, borderBottomStyle: cs.borderBottomStyle, borderLeftStyle: cs.borderLeftStyle, boxShadow: cs.boxShadow, filter: cs.filter, overflow: cs.overflow, overflowX: cs.overflowX, overflowY: cs.overflowY, clipPath: cs.clipPath, mixBlendMode: cs.mixBlendMode, transform: cs.transform, // Typography fontFamily: cs.fontFamily, fontSize: cs.fontSize, fontWeight: cs.fontWeight, fontStyle: cs.fontStyle, lineHeight: cs.lineHeight, letterSpacing: cs.letterSpacing, textAlign: cs.textAlign, textTransform: cs.textTransform, whiteSpace: cs.whiteSpace, textOverflow: cs.textOverflow, textShadow: cs.textShadow, textDecoration: cs.textDecoration, textDecorationLine: cs.textDecorationLine, textDecorationStyle: cs.textDecorationStyle, textDecorationColor: cs.textDecorationColor, textDecorationThickness: cs.textDecorationThickness, webkitTextStrokeWidth: cs.webkitTextStrokeWidth, webkitTextStrokeColor: cs.webkitTextStrokeColor, writingMode: cs.writingMode, // Positioning top: cs.top, right: cs.right, bottom: cs.bottom, left: cs.left, inset: cs.inset, // Content (for pseudo-elements) content: cs.content, }; } function isVisuallyHiddenElement(cs) { if (!cs) { return true; } const opacity = parseFloat(cs.opacity); return cs.display === 'none' || cs.visibility === 'hidden' || (Number.isFinite(opacity) && opacity <= 0); } function extractImageData(el) { const src = String(el.currentSrc || el.src || el.getAttribute('src') || '').trim(); if (!src) { return null; } let base64Src = src; try { if (el.complete && el.naturalWidth > 0 && el.naturalHeight > 0) { const canvas = document.createElement('canvas'); canvas.width = el.naturalWidth; canvas.height = el.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(el, 0, 0); base64Src = canvas.toDataURL('image/png'); } } catch (err) { // Fallback to original src if canvas conversion fails } return { src: base64Src, alt: el.getAttribute('alt') || '', naturalWidth: Number.isFinite(el.naturalWidth) ? el.naturalWidth : 0, naturalHeight: Number.isFinite(el.naturalHeight) ? el.naturalHeight : 0, }; } function extractCanvasImageData(el) { const width = Number(el.width) || 0; const height = Number(el.height) || 0; if (width <= 0 || height <= 0 || typeof el.toDataURL !== 'function') { return null; } let src = String(el.__morphusCanvasCaptureSrc || ''); try { src = src || el.toDataURL('image/png'); } catch (err) { return null; } if (!src || src === 'data:,') { return null; } return { src, alt: el.getAttribute('aria-label') || el.getAttribute('title') || '', naturalWidth: width, naturalHeight: height, }; } function extractTextData(el) { const runs = []; const pieces = []; let lineIndex = 0; function pushText(text, styleSource) { const normalized = normalizeTextFragment(text); if (!normalized) return; pieces.push(normalized); runs.push({ text: normalized, lineIndex, computed: extractTextRunStyles(resolveTextRunComputedStyles(styleSource)), }); } function pushPseudoText(element, pseudoType) { const pseudoStyles = window.getComputedStyle(element, pseudoType); const content = parseCssContent(pseudoStyles.content); if (!content) return; pushText(content, pseudoStyles); } function walkText(node, styleEl) { if (node.nodeType === Node.TEXT_NODE) { pushText(node.textContent || '', styleEl); return; } if (node.nodeType !== Node.ELEMENT_NODE) return; const element = node; const tagName = element.tagName.toLowerCase(); if (tagName === 'br') { pieces.push('\n'); lineIndex++; return; } const nextStyleEl = element; pushPseudoText(element, '::before'); for (const child of element.childNodes) { walkText(child, nextStyleEl); } pushPseudoText(element, '::after'); } for (const child of el.childNodes) { walkText(child, el); } const text = pieces .join('') .replace(/[ \t]*\n[ \t]*/g, '\n') .replace(/[ \t]{2,}/g, ' ') .trim(); return { text, runs }; } function resolveTextRunComputedStyles(styleSource) { if (styleSource && typeof styleSource.getPropertyValue === 'function') { return styleSource; } return window.getComputedStyle(styleSource); } function extractTextRunStyles(cs) { return { display: cs.display, position: cs.position, fontFamily: cs.fontFamily, fontSize: cs.fontSize, fontWeight: cs.fontWeight, fontStyle: cs.fontStyle, lineHeight: cs.lineHeight, letterSpacing: cs.letterSpacing, textAlign: cs.textAlign, textTransform: cs.textTransform, color: cs.color, opacity: cs.opacity, textShadow: cs.textShadow, textDecoration: cs.textDecoration, textDecorationLine: cs.textDecorationLine, textDecorationStyle: cs.textDecorationStyle, textDecorationColor: cs.textDecorationColor, textDecorationThickness: cs.textDecorationThickness, webkitTextStrokeWidth: cs.webkitTextStrokeWidth, webkitTextStrokeColor: cs.webkitTextStrokeColor, writingMode: cs.writingMode, filter: cs.filter, }; } function serializeSvgElement(svgEl, rect) { const clone = svgEl.cloneNode(true); clone.setAttribute('xmlns', clone.getAttribute('xmlns') || 'http://www.w3.org/2000/svg'); clone.setAttribute('width', formatSvgNumber(rect.width)); clone.setAttribute('height', formatSvgNumber(rect.height)); clone.removeAttribute('opacity'); if (clone.style) { clone.style.removeProperty('opacity'); } inlineSvgPresentationStyles(svgEl, clone); return new XMLSerializer().serializeToString(clone); } function inlineSvgPresentationStyles(sourceRoot, cloneRoot) { const sourceElements = [sourceRoot].concat(Array.from(sourceRoot.querySelectorAll('*'))); const cloneElements = [cloneRoot].concat(Array.from(cloneRoot.querySelectorAll('*'))); for (let index = 0; index < sourceElements.length; index++) { const sourceEl = sourceElements[index]; const cloneEl = cloneElements[index]; if (!sourceEl || !cloneEl) continue; cloneEl.removeAttribute('data-morphus-animated'); const cs = window.getComputedStyle(sourceEl); const isRoot = index === 0; setSvgPresentationAttribute(cloneEl, 'fill', cs.fill); setSvgPresentationAttribute(cloneEl, 'stroke', cs.stroke); setSvgPresentationAttribute(cloneEl, 'stroke-width', cs.strokeWidth); setSvgPresentationAttribute(cloneEl, 'stroke-linecap', cs.strokeLinecap); setSvgPresentationAttribute(cloneEl, 'stroke-linejoin', cs.strokeLinejoin); setSvgPresentationAttribute(cloneEl, 'stroke-miterlimit', cs.strokeMiterlimit); setSvgPresentationAttribute(cloneEl, 'stroke-dasharray', cs.strokeDasharray); setSvgPresentationAttribute(cloneEl, 'fill-rule', cs.fillRule); setSvgPresentationAttribute(cloneEl, 'clip-rule', cs.clipRule); setSvgPresentationAttribute(cloneEl, 'vector-effect', cs.vectorEffect); if (!isRoot) { setSvgPresentationAttribute(cloneEl, 'opacity', cs.opacity); setSvgPresentationAttribute(cloneEl, 'fill-opacity', cs.fillOpacity); setSvgPresentationAttribute(cloneEl, 'stroke-opacity', cs.strokeOpacity); } } } function setSvgPresentationAttribute(el, name, value) { if (!isUsableSvgPresentationValue(value)) { return; } el.setAttribute(name, normalizeSvgPresentationValue(value)); } function isUsableSvgPresentationValue(value) { if (value === undefined || value === null) { return false; } const normalized = String(value).trim(); return normalized !== '' && normalized !== 'normal' && normalized !== 'auto'; } function normalizeSvgPresentationValue(value) { return String(value).trim(); } function formatSvgNumber(value) { const number = Number(value); if (!Number.isFinite(number)) { return '1'; } return String(Math.max(Math.round(number * 1000) / 1000, 1)); } function getChildNode(child, parentEl, parentStyles, depth) { if (child.nodeType === Node.TEXT_NODE) { return getDirectTextNode(child, parentEl, parentStyles); } if (child.nodeType === Node.ELEMENT_NODE) { const node = getNode(child, depth); if (!node) { return null; } if (parentEl && parentStyles && clippingEnabled(parentStyles)) { const parentRect = parentEl.getBoundingClientRect(); if (isClippedOutsideParent(node.rect, parentRect, parentStyles)) { return null; } } return node; } return null; } function getDirectTextNode(textNode, parentEl, parentStyles) { const normalizedText = normalizeTextFragment(textNode.textContent || '').trim(); if (!normalizedText) { return null; } const range = document.createRange(); range.selectNodeContents(textNode); const rect = range.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) { return null; } if (parentEl && parentStyles && clippingEnabled(parentStyles)) { const parentRect = parentEl.getBoundingClientRect(); if (isClippedOutsideParent(rect, parentRect, parentStyles)) { return null; } } const computed = extractRelevantStyles(parentStyles); computed.display = 'inline'; computed.position = 'static'; computed.width = `${rect.width}px`; computed.height = `${rect.height}px`; computed.minWidth = '0px'; computed.minHeight = '0px'; return { tag: 'span', id: null, classList: [], text: normalizedText, textRuns: [{ text: normalizedText, lineIndex: 0, computed: extractTextRunStyles(parentStyles), }], isTextContainer: true, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, computed, pseudo: { before: null, after: null, }, children: [], }; } function normalizeTextFragment(text) { return String(text || '') .replace(/\r/g, '') .replace(/\u00a0/g, ' ') .replace(/\s+/g, ' '); } function isInlineTextChild(child) { if (!child || child.nodeType !== Node.ELEMENT_NODE) return false; const childTag = child.tagName.toLowerCase(); if (!INLINE_TAGS.has(childTag)) return false; const childCs = window.getComputedStyle(child); if (childCs.position !== 'static') return false; if (childCs.display !== 'inline' && childCs.display !== 'contents') return false; return !hasVisualBoxForStyles(childCs); } function hasVisualBoxForStyles(cs) { return !isTransparentColor(cs.backgroundColor) || cs.backgroundImage !== 'none' || cs.borderStyle !== 'none' || parseFloat(cs.borderTopWidth) > 0 || parseFloat(cs.borderRightWidth) > 0 || parseFloat(cs.borderBottomWidth) > 0 || parseFloat(cs.borderLeftWidth) > 0 || parseFloat(cs.paddingTop) > 0 || parseFloat(cs.paddingRight) > 0 || parseFloat(cs.paddingBottom) > 0 || parseFloat(cs.paddingLeft) > 0 || cs.boxShadow !== 'none'; } function hasRenderablePseudo(cs) { if (!cs || cs.content === 'none' || cs.content === 'normal') { return false; } return parseCssContent(cs.content) !== '' || hasSupportedPseudoVisual(cs); } function isVisuallyHiddenPseudo(cs, rect = null, parentRect = null, parentStyles = null) { if (!cs) { return true; } const opacity = parseFloat(cs.opacity); if (cs.display === 'none' || cs.visibility === 'hidden' || (Number.isFinite(opacity) && opacity <= 0)) { return true; } if (hasCollapsedTransform(cs.transform)) { return true; } if (rect && isFullyClippedByClipPath(cs.clipPath, rect)) { return true; } if (rect && parentRect && parentStyles && isClippedOutsideParent(rect, parentRect, parentStyles)) { return true; } return false; } function hasCollapsedTransform(transformValue) { if (!transformValue || transformValue === 'none') { return false; } const scale = parseTransformScale(transformValue); if (!scale) { return false; } const tolerance = 0.001; return scale.x <= tolerance || scale.y <= tolerance; } function parseTransformScale(transformValue) { const value = String(transformValue).trim(); const matrixMatch = value.match(/^matrix\(([^)]+)\)$/i); if (matrixMatch) { const values = parseTransformNumbers(matrixMatch[1]); if (values.length === 6) { return { x: Math.hypot(values[0], values[1]), y: Math.hypot(values[2], values[3]), }; } } const matrix3dMatch = value.match(/^matrix3d\(([^)]+)\)$/i); if (matrix3dMatch) { const values = parseTransformNumbers(matrix3dMatch[1]); if (values.length === 16) { return { x: Math.hypot(values[0], values[1], values[2]), y: Math.hypot(values[4], values[5], values[6]), }; } } return parseScaleFunction(value); } function parseTransformNumbers(value) { return String(value) .split(',') .map((part) => parseFloat(part.trim())) .filter((number) => Number.isFinite(number)); } function parseScaleFunction(value) { const scaleX = value.match(/scaleX\(\s*([-+]?\d*\.?\d+)/i); const scaleY = value.match(/scaleY\(\s*([-+]?\d*\.?\d+)/i); const scale = value.match(/scale\(\s*([-+]?\d*\.?\d+)(?:\s*,\s*([-+]?\d*\.?\d+))?/i); if (scaleX || scaleY || scale) { const uniformScale = scale ? Math.abs(parseFloat(scale[1])) : 1; return { x: scaleX ? Math.abs(parseFloat(scaleX[1])) : uniformScale, y: scaleY ? Math.abs(parseFloat(scaleY[1])) : (scale?.[2] ? Math.abs(parseFloat(scale[2])) : uniformScale), }; } return null; } function extractPseudoElementData(el, tag, parentStyles, pseudoStyles, pseudoType) { if (!hasRenderablePseudo(pseudoStyles)) { return null; } const content = parseCssContent(pseudoStyles.content); if (!content && !hasSupportedPseudoVisual(pseudoStyles)) { return null; } const parentRect = el.getBoundingClientRect(); const rect = estimatePseudoTextRect(parentRect, parentStyles, pseudoStyles, pseudoType, content); const transformedRect = applyPseudoTransformRect(rect, pseudoStyles.transform); const finalRect = transformedRect || rect; if (isVisuallyHiddenPseudo(pseudoStyles, finalRect, parentRect, parentStyles)) { return null; } if (!content && (finalRect.width <= 0 || finalRect.height <= 0)) { return null; } if (finalRect.width === 0 && finalRect.height === 0) { return null; } return { name: `${buildPseudoName(el, tag)}::${pseudoType}`, type: content ? 'text' : 'box', content: content || null, rect: finalRect, fillColor: pseudoStyles.color, opacity: Number.isFinite(parseFloat(pseudoStyles.opacity)) ? parseFloat(pseudoStyles.opacity) : 1, position: pseudoStyles.position, zOrder: resolvePseudoZOrder(pseudoStyles, pseudoType), computed: extractRelevantStyles(pseudoStyles), }; } function resolvePseudoZOrder(pseudoStyles, pseudoType) { const zIndex = parseFloat(pseudoStyles.zIndex); if (Number.isFinite(zIndex)) { return zIndex < 0 ? 'bottom' : 'top'; } return pseudoType === 'before' ? 'bottom' : 'top'; } function hasSupportedPseudoVisual(cs) { return !isTransparentColor(cs.backgroundColor) || String(cs.backgroundImage || '').includes('linear-gradient') || cs.borderStyle !== 'none' || parseFloat(cs.borderTopWidth) > 0 || parseFloat(cs.borderRightWidth) > 0 || parseFloat(cs.borderBottomWidth) > 0 || parseFloat(cs.borderLeftWidth) > 0 || cs.boxShadow !== 'none' || cs.filter !== 'none'; } function estimatePseudoTextRect(parentRect, parentStyles, pseudoStyles, pseudoType, content = '') { const metrics = content ? measurePseudoTextMetrics(content, pseudoStyles) : null; const width = Math.max(parseCssPx(pseudoStyles.width), metrics?.width || 0); const height = Math.max( parseCssPx(pseudoStyles.height) || parseCssPx(pseudoStyles.lineHeight) || parseCssPx(pseudoStyles.fontSize), metrics?.height || 0 ); const position = pseudoStyles.position; if (position === 'absolute' || position === 'fixed') { return expandRectForTextMetrics(estimatePositionedPseudoRect(parentRect, pseudoStyles, width, height), metrics); } if (parentStyles.display === 'flex' || parentStyles.display === 'inline-flex') { return expandRectForTextMetrics(estimateFlexPseudoRect(parentRect, parentStyles, width, height, pseudoType), metrics); } return expandRectForTextMetrics({ x: pseudoType === 'before' ? parentRect.x : parentRect.right - width, y: parentRect.y + Math.max((parentRect.height - height) / 2, 0), width, height, }, metrics); } function measurePseudoTextMetrics(content, pseudoStyles) { try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { return null; } ctx.font = [ pseudoStyles.fontStyle, pseudoStyles.fontVariant, pseudoStyles.fontWeight, pseudoStyles.fontSize, pseudoStyles.fontFamily, ].filter(Boolean).join(' '); const metrics = ctx.measureText(content); const left = Number.isFinite(metrics.actualBoundingBoxLeft) ? metrics.actualBoundingBoxLeft : 0; const right = Number.isFinite(metrics.actualBoundingBoxRight) ? metrics.actualBoundingBoxRight : metrics.width; const ascent = Number.isFinite(metrics.actualBoundingBoxAscent) ? metrics.actualBoundingBoxAscent : 0; const descent = Number.isFinite(metrics.actualBoundingBoxDescent) ? metrics.actualBoundingBoxDescent : 0; return { width: Math.max(metrics.width || 0, Math.abs(left) + Math.abs(right)), height: Math.max(parseCssPx(pseudoStyles.lineHeight), Math.abs(ascent) + Math.abs(descent)), leftOverflow: Math.max(left, 0), rightOverflow: Math.max(right - (metrics.width || 0), 0), topOverflow: Math.max(ascent - parseCssPx(pseudoStyles.lineHeight), 0), bottomOverflow: Math.max(descent, 0), }; } catch (err) { return null; } } function expandRectForTextMetrics(rect, metrics) { if (!metrics) { return rect; } const leftOverflow = metrics.leftOverflow || 0; const rightOverflow = metrics.rightOverflow || 0; const topOverflow = metrics.topOverflow || 0; const bottomOverflow = metrics.bottomOverflow || 0; return { x: rect.x - leftOverflow, y: rect.y - topOverflow, width: Math.max(rect.width + leftOverflow + rightOverflow, metrics.width), height: Math.max(rect.height + topOverflow + bottomOverflow, metrics.height), }; } function applyPseudoTransformRect(rect, transformValue) { const matrix = parseCssTransformMatrix(transformValue); if (!matrix) { return rect; } const points = [ transformPoint(matrix, rect.x, rect.y), transformPoint(matrix, rect.x + rect.width, rect.y), transformPoint(matrix, rect.x, rect.y + rect.height), transformPoint(matrix, rect.x + rect.width, rect.y + rect.height), ]; const xs = points.map((point) => point.x); const ys = points.map((point) => point.y); const minX = Math.min(...xs); const maxX = Math.max(...xs); const minY = Math.min(...ys); const maxY = Math.max(...ys); return { x: minX, y: minY, width: Math.max(maxX - minX, 0), height: Math.max(maxY - minY, 0), }; } function parseCssTransformMatrix(transformValue) { const value = String(transformValue || '').trim(); if (!value || value === 'none') { return null; } const matrixMatch = value.match(/^matrix\(([^)]+)\)$/i); if (matrixMatch) { const values = parseTransformNumbers(matrixMatch[1]); if (values.length === 6) { return { a: values[0], b: values[1], c: values[2], d: values[3], e: values[4], f: values[5], }; } } const matrix3dMatch = value.match(/^matrix3d\(([^)]+)\)$/i); if (matrix3dMatch) { const values = parseTransformNumbers(matrix3dMatch[1]); if (values.length === 16) { return { a: values[0], b: values[1], c: values[4], d: values[5], e: values[12], f: values[13], }; } } return null; } function transformPoint(matrix, x, y) { return { x: (matrix.a * x) + (matrix.c * y) + matrix.e, y: (matrix.b * x) + (matrix.d * y) + matrix.f, }; } function isFullyClippedByClipPath(clipPath, rect) { const value = String(clipPath || '').trim(); if (!value || value === 'none') { return false; } const insetMatch = value.match(/^inset\((.+)\)$/i); if (!insetMatch) { return false; } const parts = splitInsetTokens(insetMatch[1]); const [topToken, rightToken, bottomToken, leftToken] = normalizeInsetTokens(parts); const top = resolveInsetValue(topToken, rect.height); const right = resolveInsetValue(rightToken, rect.width); const bottom = resolveInsetValue(bottomToken, rect.height); const left = resolveInsetValue(leftToken, rect.width); return rect.width - left - right <= 0 || rect.height - top - bottom <= 0; } function splitInsetTokens(value) { return String(value) .split(/\s+round\s+/i)[0] .trim() .split(/\s+/) .filter(Boolean); } function normalizeInsetTokens(tokens) { if (tokens.length === 1) { return [tokens[0], tokens[0], tokens[0], tokens[0]]; } if (tokens.length === 2) { return [tokens[0], tokens[1], tokens[0], tokens[1]]; } if (tokens.length === 3) { return [tokens[0], tokens[1], tokens[2], tokens[1]]; } return [tokens[0], tokens[1], tokens[2], tokens[3]]; } function resolveInsetValue(token, size) { const value = String(token || '').trim(); if (!value || value === 'auto') { return 0; } if (value.endsWith('%')) { const ratio = parseFloat(value); return Number.isFinite(ratio) ? (ratio / 100) * size : 0; } const parsed = parseFloat(value); return Number.isFinite(parsed) ? parsed : 0; } function isClippedOutsideParent(rect, parentRect, parentStyles) { if (!clippingEnabled(parentStyles)) { return false; } const intersectionWidth = Math.min(rect.x + rect.width, parentRect.x + parentRect.width) - Math.max(rect.x, parentRect.x); const intersectionHeight = Math.min(rect.y + rect.height, parentRect.y + parentRect.height) - Math.max(rect.y, parentRect.y); return intersectionWidth <= 0.5 || intersectionHeight <= 0.5; } function clippingEnabled(parentStyles) { if (!parentStyles) { return false; } return ['overflow', 'overflowX', 'overflowY'].some((prop) => { const value = String(parentStyles[prop] || '').toLowerCase(); return value === 'hidden' || value === 'clip' || value === 'scroll' || value === 'auto'; }); } function estimatePositionedPseudoRect(parentRect, pseudoStyles, width, height) { const left = pseudoStyles.left !== 'auto' ? parseCssPx(pseudoStyles.left) : null; const right = pseudoStyles.right !== 'auto' ? parseCssPx(pseudoStyles.right) : null; const top = pseudoStyles.top !== 'auto' ? parseCssPx(pseudoStyles.top) : null; const bottom = pseudoStyles.bottom !== 'auto' ? parseCssPx(pseudoStyles.bottom) : null; return { x: parentRect.x + (left !== null ? left : parentRect.width - width - (right || 0)), y: parentRect.y + (top !== null ? top : parentRect.height - height - (bottom || 0)), width, height, }; } function estimateFlexPseudoRect(parentRect, parentStyles, width, height, pseudoType) { const isRow = parentStyles.flexDirection !== 'column' && parentStyles.flexDirection !== 'column-reverse'; const isReverse = parentStyles.flexDirection === 'row-reverse' || parentStyles.flexDirection === 'column-reverse'; const isEnd = (pseudoType === 'after') !== isReverse; if (isRow) { return { x: isEnd ? parentRect.right - width : parentRect.x, y: alignCrossAxis(parentRect.y, parentRect.height, height, parentStyles.alignItems), width, height, }; } return { x: alignCrossAxis(parentRect.x, parentRect.width, width, parentStyles.alignItems), y: isEnd ? parentRect.bottom - height : parentRect.y, width, height, }; } function alignCrossAxis(start, parentSize, childSize, alignItems) { if (alignItems === 'center') { return start + Math.max((parentSize - childSize) / 2, 0); } if (alignItems === 'flex-end') { return start + Math.max(parentSize - childSize, 0); } return start; } function parseCssContent(value) { if (!value || value === 'none' || value === 'normal') return ''; const trimmed = String(value).trim(); if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { return trimmed.slice(1, -1) .replace(/\\"/g, '"') .replace(/\\'/g, "'"); } return trimmed; } function parseCssPx(value) { if (!value || value === 'auto' || value === 'normal' || value === 'none') return 0; const parsed = parseFloat(value); return Number.isFinite(parsed) ? parsed : 0; } function buildPseudoName(el, tag) { const classPart = Array.from(el.classList || []).slice(0, 2).join('.'); return classPart ? `${tag}.${classPart}` : tag; } function canCollapseToTextContainer(el, tag, cs, hasOnlyInlineTextChildren) { const hasElementChildren = el.children.length > 0; if (!hasElementChildren) { return true; } if (!hasOnlyInlineTextChildren) { return false; } return TEXT_TAGS.has(tag) || tag === 'div'; } function normalizeTextContent(value) { return String(value || '') .replace(/\r/g, '') .replace(/\u00a0/g, ' ') .replace(/[ \t]+\n/g, '\n') .replace(/\n[ \t]+/g, '\n') .replace(/[ \t]{2,}/g, ' ') .trim(); } function normalizeFormControlText(value, preserveLineBreaks = false) { const text = String(value || '').replace(/\r/g, ''); if (preserveLineBreaks) { return text.trim(); } return normalizeTextContent(text); } function isTransparentColor(value) { return !value || value === 'transparent' || value === 'none' || value === 'rgba(0, 0, 0, 0)'; } return getNode(document.body); }