✨ New Features: - Click2Crawl: Visual element selection with markdown conversion - Ctrl/Cmd+Click to select multiple elements - Visual text mode for WYSIWYG extraction - Real-time markdown preview with syntax highlighting - Export to .md file or clipboard - Schema Builder Enhancement: Instant data extraction without LLMs - Test schemas directly in browser - See JSON results immediately - Export data or Python code - Cloud deployment ready (coming soon) - Modular Architecture: - Separated into schemaBuilder.js, scriptBuilder.js, click2CrawlBuilder.js - Added contentAnalyzer.js and markdownConverter.js modules - Shared utilities and CSS reset system - Integrated marked.js for markdown rendering 🎨 UI/UX Improvements: - Added edgy cloud announcement banner with seamless shimmer animation - Direct, technical copy: "You don't need Puppeteer. You need Crawl4AI Cloud." - Enhanced feature cards with emojis - Fixed CSS conflicts with targeted reset approach - Improved badge hover effects (red on hover) - Added wrap toggle for code preview 📚 Documentation Updates: - Split extraction diagrams into LLM and no-LLM versions - Updated llms-full.txt with latest content - Added versioned LLM context (v0.1.1) 🔧 Technical Enhancements: - Refactored 3464 lines of monolithic content.js into modules - Added proper event handling and cleanup - Improved z-index management - Better scroll position tracking for badges - Enhanced error handling throughout This release transforms the Chrome Extension from a simple tool into a powerful visual data extraction suite, making web scraping accessible to everyone.
253 lines
8.0 KiB
JavaScript
253 lines
8.0 KiB
JavaScript
// Shared utilities for Crawl4AI Chrome Extension
|
|
|
|
// Make element draggable by its titlebar
|
|
function makeDraggable(element) {
|
|
let isDragging = false;
|
|
let startX, startY, initialX, initialY;
|
|
|
|
const titlebar = element.querySelector('.c4ai-toolbar-titlebar, .c4ai-titlebar');
|
|
if (!titlebar) return;
|
|
|
|
titlebar.addEventListener('mousedown', (e) => {
|
|
// Don't drag if clicking on buttons
|
|
if (e.target.classList.contains('c4ai-dot') || e.target.closest('button')) return;
|
|
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
initialX = rect.left;
|
|
initialY = rect.top;
|
|
|
|
element.style.transition = 'none';
|
|
titlebar.style.cursor = 'grabbing';
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
|
|
element.style.left = `${initialX + deltaX}px`;
|
|
element.style.top = `${initialY + deltaY}px`;
|
|
element.style.right = 'auto';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
element.style.transition = '';
|
|
titlebar.style.cursor = 'grab';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Make element draggable by a specific header element
|
|
function makeDraggableByHeader(element) {
|
|
let isDragging = false;
|
|
let startX, startY, initialX, initialY;
|
|
|
|
const header = element.querySelector('.c4ai-debugger-header');
|
|
if (!header) return;
|
|
|
|
header.addEventListener('mousedown', (e) => {
|
|
// Don't drag if clicking on close button
|
|
if (e.target.id === 'c4ai-close-debugger' || e.target.closest('#c4ai-close-debugger')) return;
|
|
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
initialX = rect.left;
|
|
initialY = rect.top;
|
|
|
|
element.style.transition = 'none';
|
|
header.style.cursor = 'grabbing';
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
|
|
element.style.left = `${initialX + deltaX}px`;
|
|
element.style.top = `${initialY + deltaY}px`;
|
|
element.style.right = 'auto';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
element.style.transition = '';
|
|
header.style.cursor = 'grab';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Escape HTML for safe display
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Apply syntax highlighting to Python code
|
|
function applySyntaxHighlighting(codeElement) {
|
|
const code = codeElement.textContent;
|
|
|
|
// Split by lines to handle line-by-line
|
|
const lines = code.split('\n');
|
|
const highlightedLines = lines.map(line => {
|
|
let highlightedLine = escapeHtml(line);
|
|
|
|
// Skip if line is empty
|
|
if (!highlightedLine.trim()) return highlightedLine;
|
|
|
|
// Comments (lines starting with #)
|
|
if (highlightedLine.trim().startsWith('#')) {
|
|
return `<span class="c4ai-comment">${highlightedLine}</span>`;
|
|
}
|
|
|
|
// Triple quoted strings
|
|
if (highlightedLine.includes('"""')) {
|
|
highlightedLine = highlightedLine.replace(/(""".*?""")/g, '<span class="c4ai-string">$1</span>');
|
|
}
|
|
|
|
// Regular strings - single and double quotes
|
|
highlightedLine = highlightedLine.replace(/(["'])([^"']*)\1/g, '<span class="c4ai-string">$1$2$1</span>');
|
|
|
|
// Keywords - only highlight if not inside a string
|
|
const keywords = ['import', 'from', 'async', 'def', 'await', 'try', 'except', 'with', 'as', 'for', 'if', 'else', 'elif', 'return', 'print', 'open', 'and', 'or', 'not', 'in', 'is', 'class', 'self', 'None', 'True', 'False', '__name__', '__main__'];
|
|
|
|
keywords.forEach(keyword => {
|
|
// Use word boundaries and lookahead/lookbehind to ensure we're not in a string
|
|
const regex = new RegExp(`\\b(${keyword})\\b(?![^<]*</span>)`, 'g');
|
|
highlightedLine = highlightedLine.replace(regex, '<span class="c4ai-keyword">$1</span>');
|
|
});
|
|
|
|
// Functions (word followed by parenthesis)
|
|
highlightedLine = highlightedLine.replace(/\b([a-zA-Z_]\w*)\s*\(/g, '<span class="c4ai-function">$1</span>(');
|
|
|
|
return highlightedLine;
|
|
});
|
|
|
|
codeElement.innerHTML = highlightedLines.join('\n');
|
|
}
|
|
|
|
// Apply syntax highlighting to JavaScript code
|
|
function applySyntaxHighlightingJS(codeElement) {
|
|
const code = codeElement.textContent;
|
|
|
|
// Split by lines to handle line-by-line
|
|
const lines = code.split('\n');
|
|
const highlightedLines = lines.map(line => {
|
|
let highlightedLine = escapeHtml(line);
|
|
|
|
// Skip if line is empty
|
|
if (!highlightedLine.trim()) return highlightedLine;
|
|
|
|
// Comments
|
|
if (highlightedLine.trim().startsWith('//')) {
|
|
return `<span class="c4ai-comment">${highlightedLine}</span>`;
|
|
}
|
|
|
|
// Multi-line comments
|
|
highlightedLine = highlightedLine.replace(/(\/\*.*?\*\/)/g, '<span class="c4ai-comment">$1</span>');
|
|
|
|
// Template literals
|
|
highlightedLine = highlightedLine.replace(/(`[^`]*`)/g, '<span class="c4ai-string">$1</span>');
|
|
|
|
// Regular strings - single and double quotes
|
|
highlightedLine = highlightedLine.replace(/(["'])([^"']*)\1/g, '<span class="c4ai-string">$1$2$1</span>');
|
|
|
|
// Keywords
|
|
const keywords = ['const', 'let', 'var', 'function', 'async', 'await', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'return', 'try', 'catch', 'finally', 'throw', 'new', 'this', 'class', 'extends', 'import', 'export', 'default', 'from', 'null', 'undefined', 'true', 'false'];
|
|
|
|
keywords.forEach(keyword => {
|
|
const regex = new RegExp(`\\b(${keyword})\\b(?![^<]*</span>)`, 'g');
|
|
highlightedLine = highlightedLine.replace(regex, '<span class="c4ai-keyword">$1</span>');
|
|
});
|
|
|
|
// Functions and methods
|
|
highlightedLine = highlightedLine.replace(/\b([a-zA-Z_$][\w$]*)\s*\(/g, '<span class="c4ai-function">$1</span>(');
|
|
|
|
// Numbers
|
|
highlightedLine = highlightedLine.replace(/\b(\d+)\b/g, '<span class="c4ai-number">$1</span>');
|
|
|
|
return highlightedLine;
|
|
});
|
|
|
|
codeElement.innerHTML = highlightedLines.join('\n');
|
|
}
|
|
|
|
// Get element selector
|
|
function getElementSelector(element) {
|
|
// Priority: ID > unique class > tag with position
|
|
if (element.id) {
|
|
return `#${element.id}`;
|
|
}
|
|
|
|
if (element.className && typeof element.className === 'string') {
|
|
const classes = element.className.split(' ').filter(c => c && !c.startsWith('c4ai-'));
|
|
if (classes.length > 0) {
|
|
const selector = `.${classes[0]}`;
|
|
if (document.querySelectorAll(selector).length === 1) {
|
|
return selector;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build a path selector
|
|
const path = [];
|
|
let current = element;
|
|
|
|
while (current && current !== document.body) {
|
|
const tagName = current.tagName.toLowerCase();
|
|
const parent = current.parentElement;
|
|
|
|
if (parent) {
|
|
const siblings = Array.from(parent.children);
|
|
const index = siblings.indexOf(current) + 1;
|
|
|
|
if (siblings.filter(s => s.tagName === current.tagName).length > 1) {
|
|
path.unshift(`${tagName}:nth-child(${index})`);
|
|
} else {
|
|
path.unshift(tagName);
|
|
}
|
|
} else {
|
|
path.unshift(tagName);
|
|
}
|
|
|
|
current = parent;
|
|
}
|
|
|
|
return path.join(' > ');
|
|
}
|
|
|
|
// Check if element is part of our extension UI
|
|
function isOurElement(element) {
|
|
return element.classList.contains('c4ai-highlight-box') ||
|
|
element.classList.contains('c4ai-toolbar') ||
|
|
element.closest('.c4ai-toolbar') ||
|
|
element.classList.contains('c4ai-script-toolbar') ||
|
|
element.closest('.c4ai-script-toolbar') ||
|
|
element.closest('.c4ai-field-dialog') ||
|
|
element.closest('.c4ai-code-modal') ||
|
|
element.closest('.c4ai-wait-dialog') ||
|
|
element.closest('.c4ai-timeline-modal');
|
|
}
|
|
|
|
// Export utilities
|
|
window.C4AI_Utils = {
|
|
makeDraggable,
|
|
makeDraggableByHeader,
|
|
escapeHtml,
|
|
applySyntaxHighlighting,
|
|
applySyntaxHighlightingJS,
|
|
getElementSelector,
|
|
isOurElement
|
|
}; |