UNPKG

browser-x-mcp

Version:

AI-Powered Browser Automation with Advanced Form Testing - A Model Context Provider (MCP) server that enables intelligent browser automation with form testing, element extraction, and comprehensive logging

245 lines (207 loc) 8.09 kB
/** * XML Canvas Extractor for Browser[X]MCP * Clean implementation using ID-based element indexing * Ultra-compact format with coordinate lookup * * @author Browser[X]MCP Team * @version 4.0.0 */ class XMLCanvasExtractor { constructor() { this.viewport = this.getViewport(); this.typeMap = { // Ultra-compact type mapping 'input': 'i', 'button': 'b', 'a': 'l', 'select': 's', 'textarea': 't', 'div': 'd', 'span': 'p', 'img': 'g', 'form': 'f' }; this.elementIndex = 0; this.coordinateMap = new Map(); // ID -> coordinates mapping } getViewport() { return { x: window.pageXOffset || document.documentElement.scrollLeft, y: window.pageYOffset || document.documentElement.scrollTop, width: window.innerWidth, height: window.innerHeight }; } isElementVisible(element) { const rect = element.getBoundingClientRect(); return ( rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && rect.top < this.viewport.height && rect.left < this.viewport.width ); } getElementType(element) { const tag = element.tagName.toLowerCase(); const type = element.getAttribute('type')?.toLowerCase(); if (tag === 'input') { return type || 'text'; } return this.typeMap[tag] || tag.substring(0, 1); } getElementContent(element) { const text = (element.textContent || '').trim(); const placeholder = element.getAttribute('placeholder') || ''; const value = element.value || ''; const alt = element.getAttribute('alt') || ''; const title = element.getAttribute('title') || ''; const content = text || placeholder || value || alt || title || ''; return content.substring(0, 20).replace(/[<>&"'\s]/g, ''); // Clean & compact for XML } isInteractive(element) { const interactiveTags = ['input', 'button', 'a', 'select', 'textarea']; const interactiveRoles = ['button', 'link', 'tab', 'checkbox', 'radio']; return interactiveTags.includes(element.tagName.toLowerCase()) || interactiveRoles.includes(element.getAttribute('role')) || element.hasAttribute('onclick') || element.tabIndex >= 0; } generateElementId() { return (this.elementIndex++).toString(36); // Base36 for ultra-compact IDs: 0,1,2...9,a,b,c...z,10,11... } extractElements() { const allElements = document.querySelectorAll('*'); const elements = []; this.coordinateMap.clear(); this.elementIndex = 0; allElements.forEach((element) => { if (!this.isElementVisible(element)) return; const rect = element.getBoundingClientRect(); const interactive = this.isInteractive(element); // Skip tiny elements and non-interactive unless important if (rect.width < 5 || rect.height < 5) { if (!interactive) return; } const id = this.generateElementId(); const centerX = Math.round(rect.left + rect.width / 2); const centerY = Math.round(rect.top + rect.height / 2); // Store coordinates in lookup map this.coordinateMap.set(id, { x: centerX, y: centerY, w: Math.round(rect.width), h: Math.round(rect.height), left: Math.round(rect.left), top: Math.round(rect.top) }); const elementData = { id, type: this.getElementType(element), interactive, content: this.getElementContent(element) }; elements.push(elementData); }); return elements; } generateCompactXML() { const elements = this.extractElements(); const interactiveElements = elements.filter(el => el.interactive); // Start with minimal page container let xml = `<p w="${this.viewport.width}" h="${this.viewport.height}">`; // Add interactive elements with minimal attributes interactiveElements.forEach((el) => { xml += `<e id="${el.id}" t="${el.type}"`; if (el.content) { xml += ` c="${el.content}"`; } xml += `/>`; }); xml += '</p>'; return xml; } getScrollInfo() { const documentHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight ); const maxScrollY = Math.max(0, documentHeight - this.viewport.height); return { scrollable: documentHeight > this.viewport.height, current_y: this.viewport.y, max_y: maxScrollY, progress: maxScrollY > 0 ? this.viewport.y / maxScrollY : 0 }; } // Convert element ID to click coordinates getClickCoordinates(elementId) { // Build XML and coordinate map if not already done if (!this.coordinateMap || this.coordinateMap.size === 0) { this.buildXML(); } const coords = this.coordinateMap.get(elementId); if (!coords) { throw new Error(`Element ID ${elementId} not found in coordinate map`); } return { x: coords.x, y: coords.y, bounds: { left: coords.left, top: coords.top, width: coords.w, height: coords.h } }; } // Get all available element IDs with their types getElementIndex() { const elements = this.extractElements(); const index = {}; elements.filter(el => el.interactive).forEach(el => { index[el.id] = { type: el.type, content: el.content, coords: this.coordinateMap.get(el.id) }; }); return index; } extract() { const xml = this.generateCompactXML(); const elements = this.extractElements(); const interactiveElements = elements.filter(el => el.interactive); const scrollInfo = this.getScrollInfo(); const elementIndex = this.getElementIndex(); // Calculate efficiency const xmlSize = new Blob([xml]).size; const coordinateMapSize = new Blob([JSON.stringify(Object.fromEntries(this.coordinateMap))]).size; const totalSize = xmlSize + coordinateMapSize; const estimatedScreenshotSize = this.viewport.width * this.viewport.height * 3; // RGB const compressionRatio = Math.round(estimatedScreenshotSize / totalSize); return { format: 'compact_xml', xml: xml, coordinate_map: Object.fromEntries(this.coordinateMap), element_index: elementIndex, scroll: scrollInfo, stats: { total_elements: elements.length, interactive_elements: interactiveElements.length, xml_size: xmlSize, coordinate_map_size: coordinateMapSize, total_size: totalSize, estimated_screenshot_size: estimatedScreenshotSize, compression_ratio: compressionRatio, efficiency_gain: Math.round((1 - totalSize / estimatedScreenshotSize) * 100) }, timestamp: Date.now() }; } } // Make available globally if (typeof window !== 'undefined') { window.XMLCanvasExtractor = XMLCanvasExtractor; }