diff --git a/capabilities/testdrive-jsui/tests/test_complete.html b/capabilities/testdrive-jsui/tests/test_complete.html index a7431280..dd154e72 100644 --- a/capabilities/testdrive-jsui/tests/test_complete.html +++ b/capabilities/testdrive-jsui/tests/test_complete.html @@ -170,11 +170,11 @@ - - - - - + + + + + diff --git a/capabilities/testdrive-jsui/tests/test_guardrail_js.html b/capabilities/testdrive-jsui/tests/test_guardrail_js.html index 357851d9..8122916c 100644 --- a/capabilities/testdrive-jsui/tests/test_guardrail_js.html +++ b/capabilities/testdrive-jsui/tests/test_guardrail_js.html @@ -87,10 +87,10 @@ function testFunction() { - + - + diff --git a/capabilities/testdrive-jsui/tests/test_integration.html b/capabilities/testdrive-jsui/tests/test_integration.html index 8603a47a..050c14fb 100644 --- a/capabilities/testdrive-jsui/tests/test_integration.html +++ b/capabilities/testdrive-jsui/tests/test_integration.html @@ -195,8 +195,8 @@ - - + + diff --git a/markitect/plugins/testdrive_jsui.py b/markitect/plugins/testdrive_jsui.py index 21465c94..7017418a 100644 --- a/markitect/plugins/testdrive_jsui.py +++ b/markitect/plugins/testdrive_jsui.py @@ -43,11 +43,11 @@ class TestDriveJSUIEngine(RenderingEnginePlugin): "static/js/core/section-manager.js", "static/js/components/debug-panel.js", "static/js/components/dom-renderer.js", - "static/js/controls/control-base.js", - "static/js/controls/contents-control.js", - "static/js/controls/status-control.js", - "static/js/controls/debug-control.js", - "static/js/controls/edit-control.js", + "../capabilities/testdrive-jsui/js/controls/control-base.js", + "../capabilities/testdrive-jsui/js/controls/contents-control.js", + "../capabilities/testdrive-jsui/js/controls/status-control.js", + "../capabilities/testdrive-jsui/js/controls/debug-control.js", + "../capabilities/testdrive-jsui/js/controls/edit-control.js", "static/js/config-loader.js", "static/js/main-updated.js" ], diff --git a/markitect/static/js/controls/contents-control.js b/markitect/static/js/controls/contents-control.js deleted file mode 100644 index b4351052..00000000 --- a/markitect/static/js/controls/contents-control.js +++ /dev/null @@ -1,336 +0,0 @@ -/** - * ContentsControl - Table of Contents Display Control - * - * Provides an interactive table of contents for document navigation. - * Extracts headings from the document and displays them in a hierarchical - * structure with clickable links for quick navigation. - * - * Features: - * - Automatic heading extraction from document - * - Hierarchical display with proper indentation - * - Clickable navigation links with smooth scrolling - * - Real-time updates when document structure changes - * - Collapsible sections for better organization - * - Search functionality within the table of contents - * - * Dependencies: - * - ControlBase (base control functionality) - */ - -/** - * ContentsControl - Interactive table of contents control - * - * This control scans the document for headings (h1-h6) and presents them - * in a navigable tree structure. Users can click on any heading to jump - * directly to that section with smooth scrolling. - */ -class ContentsControl extends ControlBase { - constructor() { - super(); - - // Configure for contents functionality - this.config = { - icon: 'π', - title: 'Contents', - className: 'contents-control', - defaultContent: 'Loading table of contents...', - ariaLabel: 'Table of Contents Control', - position: 'w' // West positioning - }; - - // Contents-specific state - this.headings = []; - this.lastScanTime = null; - this.updateInterval = null; - this.searchQuery = ''; - } - - /** - * Extract all headings from the document - * Creates a hierarchical structure of the document's heading elements - */ - extractHeadings() { - return this.safeOperation(() => { - const headingSelectors = 'h1, h2, h3, h4, h5, h6'; - const headingElements = document.querySelectorAll(headingSelectors); - const extractedHeadings = []; - - headingElements.forEach((heading, index) => { - const level = parseInt(heading.tagName.charAt(1)); - const text = heading.textContent.trim(); - - // Generate or use existing ID for anchor links - let id = heading.id; - if (!id) { - id = text.toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .substring(0, 50); - - // Ensure uniqueness - let counter = 1; - let uniqueId = id; - while (document.getElementById(uniqueId)) { - uniqueId = `${id}-${counter}`; - counter++; - } - - heading.id = uniqueId; - id = uniqueId; - } - - extractedHeadings.push({ - id, - text, - level, - element: heading, - index - }); - }); - - this.headings = extractedHeadings; - this.lastScanTime = Date.now(); - return extractedHeadings; - - }, [], 'extractHeadings'); - } - - /** - * Filter headings based on search query - */ - filterHeadings(headings, query) { - if (!query || query.trim() === '') { - return headings; - } - - const normalizedQuery = query.toLowerCase().trim(); - return headings.filter(heading => - heading.text.toLowerCase().includes(normalizedQuery) - ); - } - - /** - * Generate HTML for the table of contents - */ - generateContentsHTML(headings = null) { - return this.safeOperation(() => { - const displayHeadings = headings || this.headings; - - if (displayHeadings.length === 0) { - return ` -
No headings found in document
- -Error generating contents
', 'generateContentsHTML'); - } - - /** - * Navigate to a specific heading with smooth scrolling - */ - navigateToHeading(headingId) { - return this.safeOperation(() => { - const targetElement = document.getElementById(headingId); - if (targetElement) { - targetElement.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - - // Highlight the target temporarily - const originalStyle = targetElement.style.backgroundColor; - targetElement.style.backgroundColor = '#fff3cd'; - targetElement.style.transition = 'background-color 0.3s ease'; - - setTimeout(() => { - targetElement.style.backgroundColor = originalStyle; - setTimeout(() => { - targetElement.style.transition = ''; - }, 300); - }, 1500); - - return true; - } - return false; - }, false, 'navigateToHeading'); - } - - /** - * Handle search input - */ - handleSearch(query) { - this.searchQuery = query; - const filteredHeadings = this.filterHeadings(this.headings, query); - this.updateContentsDisplay(filteredHeadings); - } - - /** - * Update the contents display with new headings - */ - updateContentsDisplay(headings) { - return this.safeOperation(() => { - const content = this.element?.querySelector('.control-content'); - if (content) { - content.innerHTML = this.generateContentsHTML(headings); - } - }, null, 'updateContentsDisplay'); - } - - /** - * Refresh the contents by re-scanning the document - */ - refreshContents() { - return this.safeOperation(() => { - this.extractHeadings(); - const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery); - this.updateContentsDisplay(filteredHeadings); - - // Show success feedback - const refreshBtn = this.element?.querySelector('button'); - if (refreshBtn) { - const originalText = refreshBtn.innerHTML; - refreshBtn.innerHTML = 'β Updated'; - refreshBtn.style.background = '#28a745'; - - setTimeout(() => { - refreshBtn.innerHTML = originalText; - refreshBtn.style.background = '#28a745'; - }, 1000); - } - }, null, 'refreshContents'); - } - - /** - * Build the control content - * Override of base class method to provide contents-specific functionality - */ - buildContent() { - return this.safeOperation(() => { - // Extract headings on first build - this.extractHeadings(); - - // Generate and set content - const content = this.element?.querySelector('.control-content'); - if (content) { - content.innerHTML = this.generateContentsHTML(); - - // Store reference to this control for onclick handlers - this.element.contentsControl = this; - } - - // Set up auto-refresh for dynamic content - if (this.updateInterval) { - clearInterval(this.updateInterval); - } - - this.updateInterval = setInterval(() => { - const currentHeadingCount = document.querySelectorAll('h1, h2, h3, h4, h5, h6').length; - if (currentHeadingCount !== this.headings.length) { - this.refreshContents(); - } - }, 5000); // Check every 5 seconds - - }, null, 'buildContent'); - } - - /** - * Get statistics about the document structure - */ - getDocumentStats() { - return this.safeOperation(() => { - const stats = { - totalHeadings: this.headings.length, - byLevel: {}, - deepestLevel: 1, - structure: 'flat' - }; - - // Count headings by level - this.headings.forEach(heading => { - stats.byLevel[heading.level] = (stats.byLevel[heading.level] || 0) + 1; - stats.deepestLevel = Math.max(stats.deepestLevel, heading.level); - }); - - // Determine structure type - const levels = Object.keys(stats.byLevel).map(Number).sort(); - if (levels.length > 1) { - const hasSequentialLevels = levels.every((level, index) => - index === 0 || level <= levels[index - 1] + 1 - ); - stats.structure = hasSequentialLevels ? 'hierarchical' : 'mixed'; - } - - return stats; - }, {}, 'getDocumentStats'); - } - - /** - * Clean up resources when control is destroyed - */ - destroy() { - if (this.updateInterval) { - clearInterval(this.updateInterval); - this.updateInterval = null; - } - super.destroy(); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = ContentsControl; -} else { - window.ContentsControl = ContentsControl; -} \ No newline at end of file diff --git a/markitect/static/js/controls/control-base.js b/markitect/static/js/controls/control-base.js deleted file mode 100644 index 0706c4b7..00000000 --- a/markitect/static/js/controls/control-base.js +++ /dev/null @@ -1,631 +0,0 @@ -/** - * Base Control Class for TestDrive-JSUI Controls - * - * Provides common functionality for positioning, drag, resize, expand/collapse operations. - * This is the foundation class that all UI controls inherit from to ensure consistent - * behavior across the TestDrive-JSUI component system. - * - * Key Features: - * - Drag and drop positioning with compass-based anchoring - * - Resize handles with hover-based visibility - * - Expand/collapse state management - * - Safe operation wrappers with error handling - * - Development mode with strict error checking - * - Accessibility support with proper ARIA labels - * - * Dependencies: - * - None (standalone base class) - * - * Usage: - * Controls inherit from this base by using Object.create(Control) and - * implementing their specific buildContent() methods. - */ - -// Development mode detection for enhanced error reporting -const MARKITECT_STRICT_MODE = ( - window.location.hostname === 'localhost' || - window.location.hostname === '127.0.0.1' || - window.location.search.includes('strict=true') || - window.markitectStrictMode === true -); - -/** - * ControlBase - Foundation class for all TestDrive-JSUI controls - * - * Provides the base functionality that all controls inherit: - * - DOM element management - * - Positioning and drag behavior - * - Resize handle management - * - State persistence - * - Error handling with strict mode support - */ -class ControlBase { - constructor() { - // Default configuration that controls can override - this.config = { - icon: 'π§', - title: 'Control', - className: 'base-control', - defaultContent: 'Control content', - ariaLabel: 'Base Control', - position: 'w', // Compass position: west (middle-left) - footer: null // Custom footer text - }; - - // Internal state - this.element = null; - this.isExpanded = false; - this.isHeaderOnly = false; // New state for header-only visibility - this.isDragging = false; - this.isResizing = false; - this.position = { x: 0, y: 0 }; - this.size = { width: 300, height: 200 }; - this.originalPosition = null; // Store original position for collapse - - // Event handlers storage - this.eventHandlers = new Map(); - } - - /** - * Safe operation wrapper with error handling - * Provides consistent error handling across all control operations - */ - safeOperation(operation, fallback = null, context = 'Unknown') { - try { - return operation(); - } catch (error) { - console.error(`Control operation failed in ${context}:`, error); - - if (MARKITECT_STRICT_MODE) { - throw error; // Re-throw in strict mode for debugging - } - - return fallback; - } - } - - /** - * Create and initialize the control element - * This method sets up the basic DOM structure that all controls use - */ - createElement() { - return this.safeOperation(() => { - if (this.element) { - this.destroy(); // Clean up existing element - } - - const control = document.createElement('div'); - control.className = `control-panel ${this.config.className}`; - control.setAttribute('role', 'dialog'); - control.setAttribute('aria-label', this.config.ariaLabel); - - control.innerHTML = ` - - - `; - - this.element = control; - this.setupStyles(); - this.setupEventListeners(); - return control; - - }, null, 'createElement'); - } - - /** - * Set up base styles for the control - */ - setupStyles() { - if (!this.element) return; - - // Position the element - this.element.style.position = 'fixed'; - this.element.style.zIndex = '1000'; - - // Store original position for collapse - this.storeOriginalPosition(); - - // Style the icon-only toggle button - const toggleBtn = this.element.querySelector('.control-toggle'); - if (toggleBtn) { - toggleBtn.style.cssText = ` - width: 40px; - height: 40px; - border: none; - background: rgba(248, 249, 250, 0.95); - border: 1px solid #dee2e6; - border-radius: 8px; - cursor: pointer; - font-size: 16px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - transition: all 0.2s ease; - `; - } - } - - /** - * Set up event listeners for control interaction - * Handles dragging, resizing, and toggle functionality - */ - setupEventListeners() { - if (!this.element) return; - - // Icon toggle to expand - const toggleBtn = this.element.querySelector('.control-toggle'); - if (toggleBtn) { - this.addEventListener(toggleBtn, 'click', () => this.expand()); - } - - // Close button to collapse back to icon - const closeBtn = this.element.querySelector('.control-close'); - if (closeBtn) { - this.addEventListener(closeBtn, 'click', () => this.collapse()); - } - - // Header title click to toggle content visibility - const title = this.element.querySelector('.control-title'); - if (title) { - this.addEventListener(title, 'click', () => this.toggleHeaderOnly()); - } - - // Drag functionality on header when expanded - const header = this.element.querySelector('.control-header'); - if (header) { - this.addEventListener(header, 'mousedown', (e) => { - if (this.isExpanded && e.target !== title && e.target !== closeBtn) { - this.startDrag(e); - } - }); - } - } - - /** - * Add event listener with automatic cleanup tracking - */ - addEventListener(element, event, handler) { - const key = `${element.className}_${event}`; - - // Remove existing handler if it exists - if (this.eventHandlers.has(key)) { - const [oldElement, oldEvent, oldHandler] = this.eventHandlers.get(key); - oldElement.removeEventListener(oldEvent, oldHandler); - } - - // Add new handler - element.addEventListener(event, handler); - this.eventHandlers.set(key, [element, event, handler]); - } - - /** - * Store original position for collapse restoration - */ - storeOriginalPosition() { - if (!this.element) return; - - const positionStyles = this.getCompassPosition(); - this.originalPosition = { - top: positionStyles.top, - left: positionStyles.left, - right: positionStyles.right, - bottom: positionStyles.bottom, - transform: positionStyles.transform - }; - - // Apply original position - Object.assign(this.element.style, positionStyles); - } - - /** - * Get compass-based positioning styles - */ - getCompassPosition() { - const positions = { - 'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' }, - 'ne': { top: '20px', right: '20px' }, - 'e': { right: '20px', top: '50%', transform: 'translateY(-50%)' }, - 'se': { bottom: '20px', right: '20px' }, - 's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' }, - 'sw': { bottom: '20px', left: '20px' }, - 'w': { left: '20px', top: '50%', transform: 'translateY(-50%)' }, - 'nw': { top: '20px', left: '20px' } - }; - - return positions[this.config.position] || positions['w']; - } - - /** - * Expand the control from icon-only state - */ - expand() { - return this.safeOperation(() => { - this.isExpanded = true; - const panel = this.element?.querySelector('.control-panel-expanded'); - const toggleBtn = this.element?.querySelector('.control-toggle'); - - if (panel && toggleBtn) { - panel.style.display = 'block'; - toggleBtn.style.display = 'none'; - - // Style expanded panel - panel.style.cssText = ` - background: rgba(248, 249, 250, 0.95); - border: 1px solid #dee2e6; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - backdrop-filter: blur(8px); - min-width: 300px; - min-height: 200px; - `; - - // Style header - const header = this.element.querySelector('.control-header'); - if (header) { - header.style.cssText = ` - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - background: rgba(0,0,0,0.05); - border-bottom: 1px solid #dee2e6; - cursor: move; - user-select: none; - `; - } - - // Style close button - const closeBtn = this.element.querySelector('.control-close'); - if (closeBtn) { - closeBtn.style.cssText = ` - background: none; - border: none; - font-size: 16px; - cursor: pointer; - padding: 0; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - `; - } - - // Add resize handle - this.addResizeHandle(); - this.buildContent(); - } - - return this.isExpanded; - }, false, 'expand'); - } - - /** - * Collapse back to icon-only state at original position - */ - collapse() { - return this.safeOperation(() => { - this.isExpanded = false; - this.isHeaderOnly = false; - const panel = this.element?.querySelector('.control-panel-expanded'); - const toggleBtn = this.element?.querySelector('.control-toggle'); - - if (panel && toggleBtn) { - panel.style.display = 'none'; - toggleBtn.style.display = 'block'; - - // Restore original position - if (this.originalPosition) { - // Clear any drag positioning - this.element.style.left = this.originalPosition.left || ''; - this.element.style.right = this.originalPosition.right || ''; - this.element.style.top = this.originalPosition.top || ''; - this.element.style.bottom = this.originalPosition.bottom || ''; - this.element.style.transform = this.originalPosition.transform || ''; - } - - // Remove resize handle - this.removeResizeHandle(); - } - - return !this.isExpanded; - }, false, 'collapse'); - } - - /** - * Toggle header-only visibility (content show/hide) - */ - toggleHeaderOnly() { - return this.safeOperation(() => { - if (!this.isExpanded) { - // If collapsed, expand first - this.expand(); - return; - } - - const content = this.element?.querySelector('.control-content'); - if (content) { - this.isHeaderOnly = !this.isHeaderOnly; - content.style.display = this.isHeaderOnly ? 'none' : 'block'; - } - - return this.isHeaderOnly; - }, false, 'toggleHeaderOnly'); - } - - /** - * Start drag operation - */ - startDrag(event) { - if (!this.isExpanded) return; // Only drag when expanded - - this.isDragging = true; - const rect = this.element.getBoundingClientRect(); - - // Calculate offset from mouse to element origin - this.dragOffset = { - x: event.clientX - rect.left, - y: event.clientY - rect.top - }; - - // Clear any positioning styles that interfere with dragging - this.element.style.right = ''; - this.element.style.bottom = ''; - this.element.style.transform = ''; - - // Add global mouse move and up handlers - const handleMouseMove = (e) => this.handleDrag(e); - const handleMouseUp = () => this.stopDrag(); - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - - // Store handlers for cleanup (but don't use the tracked version to avoid conflicts) - this._dragHandlers = { move: handleMouseMove, up: handleMouseUp }; - - event.preventDefault(); - } - - /** - * Handle drag movement - */ - handleDrag(event) { - if (!this.isDragging || !this.element) return; - - // Calculate new position based on mouse position and offset - const newX = event.clientX - this.dragOffset.x; - const newY = event.clientY - this.dragOffset.y; - - // Update element position - this.element.style.left = `${newX}px`; - this.element.style.top = `${newY}px`; - - this.position.x = newX; - this.position.y = newY; - - event.preventDefault(); - } - - /** - * Stop drag operation - */ - stopDrag() { - if (!this.isDragging) return; - - this.isDragging = false; - - // Clean up event handlers - if (this._dragHandlers) { - document.removeEventListener('mousemove', this._dragHandlers.move); - document.removeEventListener('mouseup', this._dragHandlers.up); - delete this._dragHandlers; - } - } - - /** - * Add resize handle to expanded control - */ - addResizeHandle() { - // Remove existing resize handle if any - this.removeResizeHandle(); - - const resizeHandle = document.createElement('div'); - resizeHandle.className = 'control-resize-handle'; - resizeHandle.innerHTML = 'β'; // Bottom-left resize indicator - resizeHandle.style.cssText = ` - position: absolute; - bottom: 0; - left: 0; - width: 20px; - height: 20px; - cursor: nw-resize; - font-size: 16px; - display: flex; - align-items: center; - justify-content: center; - background: rgba(0,0,0,0.1); - border-radius: 0 8px 0 0; - user-select: none; - `; - - // Add to the expanded panel - const panel = this.element?.querySelector('.control-panel-expanded'); - if (panel) { - panel.appendChild(resizeHandle); - - // Set up resize handlers - this.addEventListener(resizeHandle, 'mousedown', (e) => this.startResize(e)); - } - } - - /** - * Remove resize handle - */ - removeResizeHandle() { - const handle = this.element?.querySelector('.control-resize-handle'); - if (handle && handle.parentNode) { - handle.parentNode.removeChild(handle); - } - } - - /** - * Start resize operation - */ - startResize(event) { - event.stopPropagation(); // Prevent drag from starting - if (!this.isExpanded) return; - - this.isResizing = true; - const rect = this.element.getBoundingClientRect(); - - // Store initial size and mouse position - this.resizeStart = { - width: rect.width, - height: rect.height, - mouseX: event.clientX, - mouseY: event.clientY - }; - - // Add global mouse move and up handlers - const handleMouseMove = (e) => this.handleResize(e); - const handleMouseUp = () => this.stopResize(); - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - - // Store handlers for cleanup - this._resizeHandlers = { move: handleMouseMove, up: handleMouseUp }; - - event.preventDefault(); - } - - /** - * Handle resize movement (bottom-left corner resize) - */ - handleResize(event) { - if (!this.isResizing || !this.element) return; - - const panel = this.element.querySelector('.control-panel-expanded'); - if (!panel) return; - - // Calculate size change based on mouse movement - const deltaX = this.resizeStart.mouseX - event.clientX; // Inverted for left edge - const deltaY = event.clientY - this.resizeStart.mouseY; - - // Calculate new dimensions (minimum size constraints) - const newWidth = Math.max(200, this.resizeStart.width + deltaX); - const newHeight = Math.max(150, this.resizeStart.height + deltaY); - - // Apply new size to the panel - panel.style.width = `${newWidth}px`; - panel.style.height = `${newHeight}px`; - - // Update stored size - this.size.width = newWidth; - this.size.height = newHeight; - - event.preventDefault(); - } - - /** - * Stop resize operation - */ - stopResize() { - if (!this.isResizing) return; - - this.isResizing = false; - - // Clean up event handlers - if (this._resizeHandlers) { - document.removeEventListener('mousemove', this._resizeHandlers.move); - document.removeEventListener('mouseup', this._resizeHandlers.up); - delete this._resizeHandlers; - } - } - - /** - * Position the control based on compass position (used by show method) - */ - positionControl() { - if (!this.element) return; - - // Use the compass positioning from setupStyles - this.storeOriginalPosition(); - } - - /** - * Build the control content (to be overridden by subclasses) - */ - buildContent() { - // Default implementation - subclasses should override this - const content = this.element?.querySelector('.control-content'); - if (content) { - content.innerHTML = this.config.defaultContent; - } - } - - /** - * Show the control - */ - show() { - return this.safeOperation(() => { - if (!this.element) { - this.createElement(); - } - - document.body.appendChild(this.element); - this.positionControl(); - this.buildContent(); - - return this.element; - }, null, 'show'); - } - - /** - * Hide the control - */ - hide() { - return this.safeOperation(() => { - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - }, null, 'hide'); - } - - /** - * Destroy the control and clean up resources - */ - destroy() { - return this.safeOperation(() => { - // Clean up event listeners - for (const [element, event, handler] of this.eventHandlers.values()) { - element.removeEventListener(event, handler); - } - this.eventHandlers.clear(); - - // Remove element from DOM - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - - this.element = null; - }, null, 'destroy'); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = ControlBase; -} else { - window.ControlBase = ControlBase; -} \ No newline at end of file diff --git a/markitect/static/js/controls/debug-control.js b/markitect/static/js/controls/debug-control.js deleted file mode 100644 index 1135235b..00000000 --- a/markitect/static/js/controls/debug-control.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Debug Control - Displays debug information and system messages - * Implements the Robustness Principle with Fail Fast mode support - */ - -class DebugControl extends ControlBase { - constructor() { - super(); - this.config = { - icon: 'πͺ²', - title: 'Debug', - className: 'debug-control', - defaultContent: 'Click to view debug information', - ariaLabel: 'Debug Control', - position: 'w' - }; - - // Store messages for debug display - this.messages = []; - } - - buildContent() { - const content = this.element?.querySelector('.control-content'); - if (content) { - const messages = window.MarkitectDebugSystem ? - window.MarkitectDebugSystem.getMessages() : []; - - content.innerHTML = ` -No debug messages yet
' - } -Error generating edit tools
', 'generateEditToolsHTML'); - } - - /** - * Print the document - */ - printDocument() { - return this.safeOperation(() => { - window.print(); - - // Show feedback - this.showActionFeedback('π¨οΈ Print dialog opened', '#28a745'); - }, null, 'printDocument'); - } - - /** - * Save document (placeholder - would integrate with actual save system) - */ - saveDocument() { - return this.safeOperation(() => { - // In a real implementation, this would save to a backend - this.lastSaveTime = Date.now(); - this.unsavedChanges = false; - - // Update display - this.buildContent(); - - // Show feedback - this.showActionFeedback('πΎ Document saved', '#007bff'); - }, null, 'saveDocument'); - } - - /** - * Export document to various formats - */ - exportDocument() { - return this.safeOperation(() => { - const contentArea = document.querySelector('#markitect-content') || document.body; - const htmlContent = contentArea.innerHTML; - const textContent = contentArea.textContent; - - // Create export menu - const exportMenu = document.createElement('div'); - exportMenu.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: white; - border: 2px solid #007bff; - border-radius: 8px; - padding: 1rem; - z-index: 10000; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - `; - - exportMenu.innerHTML = ` -]*>(.*?)<\/p>/gi, '$1\n\n')
- .replace(/]*>(.*?)<\/strong>/gi, '**$1**')
- .replace(/]*>(.*?)<\/em>/gi, '*$1*')
- .replace(/<[^>]*>/g, ''); // Remove remaining HTML tags
-
- this.downloadFile(markdown, 'document.md', 'text/markdown');
- document.body.removeChild(exportMenu);
- };
-
- document.body.appendChild(exportMenu);
-
- }, null, 'exportDocument');
- }
-
- /**
- * Download a file with given content
- */
- downloadFile(content, filename, mimeType) {
- const blob = new Blob([content], { type: mimeType });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = filename;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
- }
-
- /**
- * Reset all changes and restore document to original state
- */
- resetAll() {
- return this.safeOperation(() => {
- // Show confirmation dialog
- const confirmed = window.confirm(
- 'Reset all changes?\n\nThis will:\n' +
- 'β’ Restore document to original state\n' +
- 'β’ Clear all unsaved changes\n' +
- 'β’ Reset font size and other settings\n\n' +
- 'This action cannot be undone.'
- );
-
- if (!confirmed) {
- this.showActionFeedback('π« Reset cancelled', '#6c757d');
- return;
- }
-
- // Reset edit control state
- this.fontSize = 16;
- this.editingMode = 'view';
- this.unsavedChanges = false;
- this.lastSaveTime = null;
-
- // Reset font size
- this.applyFontSize();
-
- // Clear any highlights
- document.querySelectorAll('.edit-highlight').forEach(el => {
- el.outerHTML = el.innerHTML;
- });
-
- // Try to reset sections if SectionManager is available
- if (window.sectionManager && typeof window.sectionManager.resetAllSections === 'function') {
- window.sectionManager.resetAllSections();
- }
-
- // Try to reset document controls if available
- if (window.documentControls && typeof window.documentControls.resetAllChanges === 'function') {
- window.documentControls.resetAllChanges();
- }
-
- // Clear any debug messages if debug control is available
- if (window.debugControl && typeof window.debugControl.clearMessages === 'function') {
- window.debugControl.clearMessages();
- }
-
- // Reload the page as ultimate fallback
- if (window.confirm('Reload page to complete reset?')) {
- window.location.reload();
- return;
- }
-
- // Update the control display
- this.buildContent();
-
- // Show feedback
- this.showActionFeedback('π All changes reset', '#ffc107', '#212529');
-
- }, null, 'resetAll');
- }
-
- /**
- * Scroll to top of document
- */
- scrollToTop() {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- this.showActionFeedback('β¬οΈ Scrolled to top', '#6c757d');
- }
-
- /**
- * Scroll to bottom of document
- */
- scrollToBottom() {
- window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
- this.showActionFeedback('β¬οΈ Scrolled to bottom', '#6c757d');
- }
-
- /**
- * Show go to line dialog
- */
- showGoToLine() {
- const lineNumber = prompt('Go to line number:');
- if (lineNumber && !isNaN(lineNumber)) {
- // Simple implementation - scroll to approximate position
- const totalHeight = document.body.scrollHeight;
- const approximatePosition = (parseInt(lineNumber) / 100) * totalHeight;
- window.scrollTo({ top: approximatePosition, behavior: 'smooth' });
- this.showActionFeedback(`π― Went to line ${lineNumber}`, '#6c757d');
- }
- }
-
- /**
- * Show find and replace dialog
- */
- showFindReplace() {
- const searchTerm = prompt('Find text:');
- if (searchTerm) {
- // Simple highlight implementation
- this.highlightText(searchTerm);
- this.showActionFeedback(`π Highlighted "${searchTerm}"`, '#ffc107', '#000');
- }
- }
-
- /**
- * Highlight text in the document
- */
- highlightText(searchTerm) {
- return this.safeOperation(() => {
- // Remove previous highlights
- document.querySelectorAll('.edit-highlight').forEach(el => {
- el.outerHTML = el.innerHTML;
- });
-
- // Add new highlights
- const contentArea = document.querySelector('#markitect-content') || document.body;
- const walker = document.createTreeWalker(
- contentArea,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- const textNodes = [];
- let node;
- while (node = walker.nextNode()) {
- textNodes.push(node);
- }
-
- textNodes.forEach(textNode => {
- const parent = textNode.parentNode;
- const text = textNode.textContent;
- if (text.toLowerCase().includes(searchTerm.toLowerCase())) {
- const regex = new RegExp(`(${searchTerm})`, 'gi');
- const highlightedHTML = text.replace(regex, '$1');
-
- const wrapper = document.createElement('div');
- wrapper.innerHTML = highlightedHTML;
- while (wrapper.firstChild) {
- parent.insertBefore(wrapper.firstChild, textNode);
- }
- parent.removeChild(textNode);
- }
- });
- }, null, 'highlightText');
- }
-
- /**
- * Increase font size
- */
- increaseFontSize() {
- this.fontSize = Math.min(this.fontSize + 2, 24);
- this.applyFontSize();
- this.buildContent();
- }
-
- /**
- * Decrease font size
- */
- decreaseFontSize() {
- this.fontSize = Math.max(this.fontSize - 2, 12);
- this.applyFontSize();
- this.buildContent();
- }
-
- /**
- * Apply font size to document
- */
- applyFontSize() {
- const contentArea = document.querySelector('#markitect-content') || document.body;
- contentArea.style.fontSize = `${this.fontSize}px`;
- }
-
- /**
- * Copy page link to clipboard
- */
- copyLink() {
- return this.safeOperation(() => {
- const url = window.location.href;
- if (navigator.clipboard) {
- navigator.clipboard.writeText(url).then(() => {
- this.showActionFeedback('π Link copied to clipboard', '#fd7e14');
- });
- } else {
- // Fallback for older browsers
- prompt('Copy this link:', url);
- this.showActionFeedback('π Link displayed for copying', '#fd7e14');
- }
- }, null, 'copyLink');
- }
-
- /**
- * Insert markdown formatting
- */
- insertMarkdown(prefix, suffix, placeholder) {
- // This would integrate with an actual text editor
- // For now, just show what would be inserted
- const text = `${prefix}${placeholder}${suffix}`;
- if (navigator.clipboard) {
- navigator.clipboard.writeText(text);
- this.showActionFeedback(`π Copied: ${text}`, '#495057');
- } else {
- prompt('Markdown to copy:', text);
- }
- }
-
- /**
- * Show action feedback message
- */
- showActionFeedback(message, backgroundColor, color = 'white') {
- const feedback = document.createElement('div');
- feedback.style.cssText = `
- position: fixed;
- top: 20px;
- right: 20px;
- background: ${backgroundColor};
- color: ${color};
- padding: 0.5rem 1rem;
- border-radius: 4px;
- z-index: 9999;
- font-size: 0.8rem;
- box-shadow: 0 2px 10px rgba(0,0,0,0.2);
- `;
- feedback.textContent = message;
- document.body.appendChild(feedback);
-
- setTimeout(() => {
- if (feedback.parentNode) {
- document.body.removeChild(feedback);
- }
- }, 3000);
- }
-
- /**
- * Build the control content
- * Override of base class method to provide edit-specific functionality
- */
- buildContent() {
- return this.safeOperation(() => {
- const content = this.element?.querySelector('.control-content');
- if (content) {
- content.innerHTML = this.generateEditToolsHTML();
-
- // Store reference to this control for onclick handlers
- this.element.editControl = this;
- }
- }, null, 'buildContent');
- }
-
- /**
- * Exit edit mode
- */
- exitEditMode() {
- this.editingMode = 'view';
- this.buildContent();
- }
-}
-
-// Export for module systems or attach to global for direct usage
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = EditControl;
-} else {
- window.EditControl = EditControl;
-}
\ No newline at end of file
diff --git a/markitect/static/js/controls/status-control.js b/markitect/static/js/controls/status-control.js
deleted file mode 100644
index f3ca19d2..00000000
--- a/markitect/static/js/controls/status-control.js
+++ /dev/null
@@ -1,368 +0,0 @@
-/**
- * StatusControl - Document Statistics and Change Tracking Control
- *
- * Provides real-time document statistics including word count, character count,
- * reading time estimation, and change tracking. Monitors document modifications
- * and provides insights into document structure and content metrics.
- *
- * Features:
- * - Real-time word and character counting
- * - Reading time estimation based on content
- * - Document structure analysis (headings, paragraphs, lists)
- * - Change tracking with before/after comparisons
- * - Content complexity metrics
- * - Export functionality for statistics
- *
- * Dependencies:
- * - ControlBase (base control functionality)
- */
-
-/**
- * StatusControl - Document statistics and monitoring control
- *
- * This control continuously monitors the document for changes and provides
- * detailed statistics about content, structure, and reading metrics.
- * Useful for writers, editors, and content creators.
- */
-class StatusControl extends ControlBase {
- constructor() {
- super();
-
- // Configure for status functionality
- this.config = {
- icon: 'π',
- title: 'Status',
- className: 'status-control',
- defaultContent: 'Loading document statistics...',
- ariaLabel: 'Document Status Control',
- position: 'e' // East positioning
- };
-
- // Status tracking state
- this.stats = {
- characters: 0,
- charactersNoSpaces: 0,
- words: 0,
- sentences: 0,
- paragraphs: 0,
- headings: 0,
- lists: 0,
- images: 0,
- links: 0,
- readingTimeMinutes: 0
- };
-
- this.previousStats = { ...this.stats };
- this.lastUpdateTime = null;
- this.updateInterval = null;
- this.wordsPerMinute = 200; // Average reading speed
- }
-
- /**
- * Extract and count document content statistics
- */
- analyzeDocument() {
- return this.safeOperation(() => {
- const contentArea = document.querySelector('#markitect-content') || document.body;
- const textContent = contentArea.textContent || '';
-
- // Basic text statistics
- this.stats.characters = textContent.length;
- this.stats.charactersNoSpaces = textContent.replace(/\s/g, '').length;
-
- // Word counting (more accurate)
- const words = textContent.trim().split(/\s+/).filter(word => word.length > 0);
- this.stats.words = words.length;
-
- // Sentence counting (approximate)
- const sentences = textContent.split(/[.!?]+/).filter(s => s.trim().length > 0);
- this.stats.sentences = sentences.length;
-
- // Structural elements
- this.stats.paragraphs = contentArea.querySelectorAll('p').length;
- this.stats.headings = contentArea.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
- this.stats.lists = contentArea.querySelectorAll('ul, ol').length;
- this.stats.images = contentArea.querySelectorAll('img').length;
- this.stats.links = contentArea.querySelectorAll('a').length;
-
- // Reading time calculation
- this.stats.readingTimeMinutes = Math.ceil(this.stats.words / this.wordsPerMinute);
-
- this.lastUpdateTime = Date.now();
- return this.stats;
-
- }, this.stats, 'analyzeDocument');
- }
-
- /**
- * Calculate changes since last analysis
- */
- calculateChanges() {
- return this.safeOperation(() => {
- const changes = {};
- for (const [key, currentValue] of Object.entries(this.stats)) {
- const previousValue = this.previousStats[key] || 0;
- const difference = currentValue - previousValue;
- changes[key] = {
- current: currentValue,
- previous: previousValue,
- change: difference,
- hasChanged: difference !== 0
- };
- }
- return changes;
- }, {}, 'calculateChanges');
- }
-
- /**
- * Format statistics for display
- */
- formatStatistics() {
- return this.safeOperation(() => {
- const changes = this.calculateChanges();
-
- const formatChange = (changeData) => {
- if (!changeData.hasChanged) return '';
- const sign = changeData.change > 0 ? '+' : '';
- const color = changeData.change > 0 ? '#28a745' : '#dc3545';
- return ` (${sign}${changeData.change})`;
- };
-
- const formatNumber = (num) => num.toLocaleString();
-
- return `
- Error displaying statistics No headings found in document Panel content goes here... Error displaying messages Error generating edit tools ]*>(.*?)<\/p>/gi, '$1\n\n')
- .replace(/]*>(.*?)<\/strong>/gi, '**$1**')
- .replace(/]*>(.*?)<\/em>/gi, '*$1*')
- .replace(/<[^>]*>/g, ''); // Remove remaining HTML tags
-
- this.downloadFile(markdown, 'document.md', 'text/markdown');
- document.body.removeChild(exportMenu);
- };
-
- document.body.appendChild(exportMenu);
-
- }, null, 'exportDocument');
- }
-
- /**
- * Download a file with given content
- */
- downloadFile(content, filename, mimeType) {
- const blob = new Blob([content], { type: mimeType });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = filename;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
- }
-
- /**
- * Reset all changes and restore document to original state
- */
- resetAll() {
- return this.safeOperation(() => {
- // Show confirmation dialog
- const confirmed = window.confirm(
- 'Reset all changes?\n\nThis will:\n' +
- 'β’ Restore document to original state\n' +
- 'β’ Clear all unsaved changes\n' +
- 'β’ Reset font size and other settings\n\n' +
- 'This action cannot be undone.'
- );
-
- if (!confirmed) {
- this.showActionFeedback('π« Reset cancelled', '#6c757d');
- return;
- }
-
- // Reset edit control state
- this.fontSize = 16;
- this.editingMode = 'view';
- this.unsavedChanges = false;
- this.lastSaveTime = null;
-
- // Reset font size
- this.applyFontSize();
-
- // Clear any highlights
- document.querySelectorAll('.edit-highlight').forEach(el => {
- el.outerHTML = el.innerHTML;
- });
-
- // Try to reset sections if SectionManager is available
- if (window.sectionManager && typeof window.sectionManager.resetAllSections === 'function') {
- window.sectionManager.resetAllSections();
- }
-
- // Try to reset document controls if available
- if (window.documentControls && typeof window.documentControls.resetAllChanges === 'function') {
- window.documentControls.resetAllChanges();
- }
-
- // Clear any debug messages if debug control is available
- if (window.debugControl && typeof window.debugControl.clearMessages === 'function') {
- window.debugControl.clearMessages();
- }
-
- // Reload the page as ultimate fallback
- if (window.confirm('Reload page to complete reset?')) {
- window.location.reload();
- return;
- }
-
- // Update the control display
- this.buildContent();
-
- // Show feedback
- this.showActionFeedback('π All changes reset', '#ffc107', '#212529');
-
- }, null, 'resetAll');
- }
-
- /**
- * Scroll to top of document
- */
- scrollToTop() {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- this.showActionFeedback('β¬οΈ Scrolled to top', '#6c757d');
- }
-
- /**
- * Scroll to bottom of document
- */
- scrollToBottom() {
- window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
- this.showActionFeedback('β¬οΈ Scrolled to bottom', '#6c757d');
- }
-
- /**
- * Show go to line dialog
- */
- showGoToLine() {
- const lineNumber = prompt('Go to line number:');
- if (lineNumber && !isNaN(lineNumber)) {
- // Simple implementation - scroll to approximate position
- const totalHeight = document.body.scrollHeight;
- const approximatePosition = (parseInt(lineNumber) / 100) * totalHeight;
- window.scrollTo({ top: approximatePosition, behavior: 'smooth' });
- this.showActionFeedback(`π― Went to line ${lineNumber}`, '#6c757d');
- }
- }
-
- /**
- * Show find and replace dialog
- */
- showFindReplace() {
- const searchTerm = prompt('Find text:');
- if (searchTerm) {
- // Simple highlight implementation
- this.highlightText(searchTerm);
- this.showActionFeedback(`π Highlighted "${searchTerm}"`, '#ffc107', '#000');
- }
- }
-
- /**
- * Highlight text in the document
- */
- highlightText(searchTerm) {
- return this.safeOperation(() => {
- // Remove previous highlights
- document.querySelectorAll('.edit-highlight').forEach(el => {
- el.outerHTML = el.innerHTML;
- });
-
- // Add new highlights
- const contentArea = document.querySelector('#markitect-content') || document.body;
- const walker = document.createTreeWalker(
- contentArea,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- const textNodes = [];
- let node;
- while (node = walker.nextNode()) {
- textNodes.push(node);
- }
-
- textNodes.forEach(textNode => {
- const parent = textNode.parentNode;
- const text = textNode.textContent;
- if (text.toLowerCase().includes(searchTerm.toLowerCase())) {
- const regex = new RegExp(`(${searchTerm})`, 'gi');
- const highlightedHTML = text.replace(regex, '$1');
-
- const wrapper = document.createElement('div');
- wrapper.innerHTML = highlightedHTML;
- while (wrapper.firstChild) {
- parent.insertBefore(wrapper.firstChild, textNode);
- }
- parent.removeChild(textNode);
- }
- });
- }, null, 'highlightText');
- }
-
- /**
- * Increase font size
- */
- increaseFontSize() {
- this.fontSize = Math.min(this.fontSize + 2, 24);
- this.applyFontSize();
- this.buildContent();
- }
-
- /**
- * Decrease font size
- */
- decreaseFontSize() {
- this.fontSize = Math.max(this.fontSize - 2, 12);
- this.applyFontSize();
- this.buildContent();
- }
-
- /**
- * Apply font size to document
- */
- applyFontSize() {
- const contentArea = document.querySelector('#markitect-content') || document.body;
- contentArea.style.fontSize = `${this.fontSize}px`;
- }
-
- /**
- * Copy page link to clipboard
- */
- copyLink() {
- return this.safeOperation(() => {
- const url = window.location.href;
- if (navigator.clipboard) {
- navigator.clipboard.writeText(url).then(() => {
- this.showActionFeedback('π Link copied to clipboard', '#fd7e14');
- });
- } else {
- // Fallback for older browsers
- prompt('Copy this link:', url);
- this.showActionFeedback('π Link displayed for copying', '#fd7e14');
- }
- }, null, 'copyLink');
- }
-
- /**
- * Insert markdown formatting
- */
- insertMarkdown(prefix, suffix, placeholder) {
- // This would integrate with an actual text editor
- // For now, just show what would be inserted
- const text = `${prefix}${placeholder}${suffix}`;
- if (navigator.clipboard) {
- navigator.clipboard.writeText(text);
- this.showActionFeedback(`π Copied: ${text}`, '#495057');
- } else {
- prompt('Markdown to copy:', text);
- }
- }
-
- /**
- * Show action feedback message
- */
- showActionFeedback(message, backgroundColor, color = 'white') {
- const feedback = document.createElement('div');
- feedback.style.cssText = `
- position: fixed;
- top: 20px;
- right: 20px;
- background: ${backgroundColor};
- color: ${color};
- padding: 0.5rem 1rem;
- border-radius: 4px;
- z-index: 9999;
- font-size: 0.8rem;
- box-shadow: 0 2px 10px rgba(0,0,0,0.2);
- `;
- feedback.textContent = message;
- document.body.appendChild(feedback);
-
- setTimeout(() => {
- if (feedback.parentNode) {
- document.body.removeChild(feedback);
- }
- }, 3000);
- }
-
- /**
- * Build the control content
- * Override of base class method to provide edit-specific functionality
- */
- /**
- * Generate edit control content (called by base class buildContent)
- */
- generateContent() {
- return this.safeOperation(() => {
- return this.generateEditToolsHTML();
- }, 'Error generating edit content', 'generateContent');
- }
-
- /**
- * Override buildContent to add control reference
- */
- buildContent() {
- super.buildContent();
-
- // Store reference to this control for onclick handlers
- if (this.element) {
- this.element.editControl = this;
- }
- }
-
- /**
- * Exit edit mode
- */
- exitEditMode() {
- this.editingMode = 'view';
- this.buildContent();
- }
-}
-
-// Export for module systems or attach to global for direct usage
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = EditControl;
-} else {
- window.EditControl = EditControl;
-}
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/controls/status-control.js b/testdrive-jsui/static/js/controls/status-control.js
deleted file mode 100644
index 4f1ae60c..00000000
--- a/testdrive-jsui/static/js/controls/status-control.js
+++ /dev/null
@@ -1,371 +0,0 @@
-/**
- * StatusControl - Document Statistics and Change Tracking Control
- *
- * Provides real-time document statistics including word count, character count,
- * reading time estimation, and change tracking. Monitors document modifications
- * and provides insights into document structure and content metrics.
- *
- * Features:
- * - Real-time word and character counting
- * - Reading time estimation based on content
- * - Document structure analysis (headings, paragraphs, lists)
- * - Change tracking with before/after comparisons
- * - Content complexity metrics
- * - Export functionality for statistics
- *
- * Dependencies:
- * - ControlBase (base control functionality)
- */
-
-/**
- * StatusControl - Document statistics and monitoring control
- *
- * This control continuously monitors the document for changes and provides
- * detailed statistics about content, structure, and reading metrics.
- * Useful for writers, editors, and content creators.
- */
-class StatusControl extends ControlBase {
- constructor() {
- super();
-
- // Configure for status functionality
- this.config = {
- icon: 'π',
- title: 'Status',
- className: 'status-control',
- defaultContent: 'Loading document statistics...',
- ariaLabel: 'Document Status Control',
- position: 'e' // East positioning
- };
-
- // Status tracking state
- this.stats = {
- characters: 0,
- charactersNoSpaces: 0,
- words: 0,
- sentences: 0,
- paragraphs: 0,
- headings: 0,
- lists: 0,
- images: 0,
- links: 0,
- readingTimeMinutes: 0
- };
-
- this.previousStats = { ...this.stats };
- this.lastUpdateTime = null;
- this.updateInterval = null;
- this.wordsPerMinute = 200; // Average reading speed
- }
-
- /**
- * Extract and count document content statistics
- */
- analyzeDocument() {
- return this.safeOperation(() => {
- const contentArea = document.querySelector('#markitect-content') || document.body;
- const textContent = contentArea.textContent || '';
-
- // Basic text statistics
- this.stats.characters = textContent.length;
- this.stats.charactersNoSpaces = textContent.replace(/\s/g, '').length;
-
- // Word counting (more accurate)
- const words = textContent.trim().split(/\s+/).filter(word => word.length > 0);
- this.stats.words = words.length;
-
- // Sentence counting (approximate)
- const sentences = textContent.split(/[.!?]+/).filter(s => s.trim().length > 0);
- this.stats.sentences = sentences.length;
-
- // Structural elements
- this.stats.paragraphs = contentArea.querySelectorAll('p').length;
- this.stats.headings = contentArea.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
- this.stats.lists = contentArea.querySelectorAll('ul, ol').length;
- this.stats.images = contentArea.querySelectorAll('img').length;
- this.stats.links = contentArea.querySelectorAll('a').length;
-
- // Reading time calculation
- this.stats.readingTimeMinutes = Math.ceil(this.stats.words / this.wordsPerMinute);
-
- this.lastUpdateTime = Date.now();
- return this.stats;
-
- }, this.stats, 'analyzeDocument');
- }
-
- /**
- * Calculate changes since last analysis
- */
- calculateChanges() {
- return this.safeOperation(() => {
- const changes = {};
- for (const [key, currentValue] of Object.entries(this.stats)) {
- const previousValue = this.previousStats[key] || 0;
- const difference = currentValue - previousValue;
- changes[key] = {
- current: currentValue,
- previous: previousValue,
- change: difference,
- hasChanged: difference !== 0
- };
- }
- return changes;
- }, {}, 'calculateChanges');
- }
-
- /**
- * Format statistics for display
- */
- formatStatistics() {
- return this.safeOperation(() => {
- const changes = this.calculateChanges();
-
- const formatChange = (changeData) => {
- if (!changeData.hasChanged) return '';
- const sign = changeData.change > 0 ? '+' : '';
- const color = changeData.change > 0 ? '#28a745' : '#dc3545';
- return ` (${sign}${changeData.change})`;
- };
-
- const formatNumber = (num) => num.toLocaleString();
-
- return `
- Error displaying statisticsDocument Statistics
-
-
- ${formatNumber(this.stats.words)}
- ${formatChange(changes.words)}
-
- ${formatNumber(this.stats.characters)}
- ${formatChange(changes.characters)}
-
- ${this.stats.readingTimeMinutes} min
- ${formatChange(changes.readingTimeMinutes)}
-
- ${formatNumber(this.stats.sentences)}
- ${formatChange(changes.sentences)}
- Document Structure
-
-
- ]*>(.*?)<\/h1>/gi, '# $1\n\n')
- .replace(/
]*>(.*?)<\/h2>/gi, '## $1\n\n')
- .replace(/
]*>(.*?)<\/h3>/gi, '### $1\n\n')
- .replace(/
- ${formatNumber(this.stats.words)}
- ${formatChange(changes.words)}
-
- ${formatNumber(this.stats.characters)}
- ${formatChange(changes.characters)}
-
- ${this.stats.readingTimeMinutes} min
- ${formatChange(changes.readingTimeMinutes)}
-
- ${formatNumber(this.stats.sentences)}
- ${formatChange(changes.sentences)}
-