diff --git a/REFACTORING_SESSION_REPORT.md b/REFACTORING_SESSION_REPORT.md new file mode 100644 index 00000000..1cbf568c --- /dev/null +++ b/REFACTORING_SESSION_REPORT.md @@ -0,0 +1,206 @@ +# Refactoring Session Report: Edit Mode Recovery Attempt + +**Date:** 2025-11-12 +**Session Goal:** Recover working edit mode functionality from git history +**Outcome:** Partial success with valuable lessons learned, but became overly complex + +## ๐ŸŽฏ Achievements + +### 1. **Robustness Principle Implementation** +- โœ… Successfully implemented dual-mode error handling (development vs production) +- โœ… Added comprehensive safety utilities in `control-base.js` +- โœ… Created sophisticated failure detection with clear error messages +- โœ… Implemented graceful degradation for missing components + +### 2. **Error Detection System** +- โœ… Automatic detection of broken edit mode functionality +- โœ… Component availability checking before attempting to load edit mode +- โœ… Clear error messages explaining what went wrong and how to fix it +- โœ… Dual-mode behavior: fail fast in development, warn in production + +### 3. **Template System Understanding** +- โœ… Identified the difference between embedded vs external JavaScript delivery +- โœ… Understood that edit modes require embedded JavaScript for immediate availability +- โœ… Successfully implemented template variable substitution (`{title}`, `{version}`) +- โœ… Fixed initialization flow to ensure components are properly loaded + +### 4. **Git History Recovery** +- โœ… Successfully recovered original JavaScript components from git history: + - `js/core/section-manager.js` + - `js/components/debug-panel.js` + - `js/components/document-controls.js` + - `js/components/dom-renderer.js` +- โœ… Restored `_get_clean_editor_scripts()` functionality +- โœ… Implemented proper component loading and concatenation + +## โŒ Problems Encountered + +### 1. **GUARDRAILS.md Violation** +- **Issue:** We ended up with JavaScript code embedded in Python strings again +- **Root Cause:** The template generation in `_generate_html_template()` contains JavaScript +- **Impact:** Violated the core principle of keeping JS separate from Python code +- **Status:** Not resolved - would require architectural redesign + +### 2. **Component Integration Issues** +- **Issue:** Old retired edit controls showing instead of new abstract controls +- **Root Cause:** Mixed old and new component systems without proper migration +- **Impact:** Confusing UI with non-functional controls +- **Status:** Not resolved - needs careful component cleanup + +### 3. **Content Rendering Problems** +- **Issue:** No content visible despite successful component initialization +- **Root Cause:** Modular architecture not properly connected to content rendering +- **Impact:** Interactive editor loads but has no content to edit +- **Status:** Not resolved - requires debugging the content flow + +### 4. **Complexity Accumulation** +- **Issue:** Session became overly complex with multiple parallel concerns +- **Root Cause:** Trying to solve too many problems simultaneously +- **Impact:** Lost track of original goal and created technical debt +- **Status:** Requires reset and focused approach + +## ๐Ÿ” Key Technical Insights + +### 1. **Template Architecture** +```python +# DISCOVERED: Two different template approaches needed +if edit_mode or insert_mode: + # Embedded JavaScript for immediate availability + template_content = f"""......""" +else: + # External JavaScript files for lazy loading + template_content = load_external_template() +``` + +### 2. **Component Loading Strategy** +```python +# WORKING: Component concatenation approach +def _get_clean_editor_scripts(self) -> str: + components = [ + 'js/core/section-manager.js', + 'js/components/debug-panel.js', + 'js/components/document-controls.js', + 'js/components/dom-renderer.js' + ] + # Load and concatenate components +``` + +### 3. **Initialization Flow Discovery** +```javascript +// CRITICAL: Editor initialization must happen before component detection +// Initialize edit/insert capabilities first (always needed) +if (MARKITECT_EDIT_MODE || MARKITECT_INSERT_MODE) { + initializeCleanEditor(); // Must happen first +} +// Then check for modular components +if (typeof SectionManager !== 'undefined') { + // Skip fallback rendering +} +``` + +## ๐Ÿ“‹ Lessons Learned + +### 1. **Focus is Critical** +- Trying to solve multiple problems simultaneously leads to confusion +- Should have focused solely on edit mode recovery +- Error detection system, while valuable, was a distraction from core goal + +### 2. **GUARDRAILS.md Must Be Respected** +- The rule against JavaScript in Python strings exists for good reasons +- Template generation approach violates this principle +- Need architectural solution that keeps JS in separate files + +### 3. **Component Migration Requires Planning** +- Cannot mix old and new component systems without explicit migration plan +- Need to identify and remove deprecated components first +- Should have focused on one component system at a time + +### 4. **Testing Must Be Incremental** +- Should test each change individually before proceeding +- Complex changes make it difficult to identify root causes +- Browser testing should happen after each major change + +## ๐Ÿš€ Recommendations for Next Attempt + +### 1. **Start with Simple Goal** +- Focus ONLY on making existing edit mode work +- Don't attempt to improve or refactor simultaneously +- Get basic functionality working first + +### 2. **Respect Architecture Constraints** +- Keep JavaScript in separate `.js` files (honor GUARDRAILS.md) +- Load components via HTTP requests, not embedded strings +- Use the external template approach consistently + +### 3. **Incremental Approach** +1. First: Get content rendering working in browser +2. Second: Add basic edit controls +3. Third: Test each control individually +4. Fourth: Add advanced features + +### 4. **Clean Component System** +- Remove old deprecated controls before adding new ones +- Use only the abstract control system consistently +- Document which components are active vs deprecated + +## ๐Ÿ’ก Valuable Code Patterns Discovered + +### 1. **Safe Operation Wrapper** +```javascript +safeOperation: function(operation, fallback = null, context = 'Unknown') { + try { + return operation(); + } catch (error) { + if (MARKITECT_STRICT_MODE) { + throw error; // Fail fast in development + } + return typeof fallback === 'function' ? fallback() : fallback; + } +} +``` + +### 2. **Component Availability Check** +```python +def check_edit_mode_components(self): + components_to_check = [ + 'js/core/section-manager.js', + 'js/components/debug-panel.js', + 'js/components/document-controls.js', + 'js/components/dom-renderer.js' + ] + missing = [c for c in components_to_check if not (base_path / c).exists()] + return len(missing) == 0, missing +``` + +### 3. **Dual-Mode Error Handling** +```python +if self._should_fail_fast(): + raise EditModeError("Edit mode components missing") +else: + print("โš ๏ธ WARNING: Edit mode requested but components missing") +``` + +## ๐ŸŽฏ Success Metrics for Next Attempt + +1. **Functional:** Click section โ†’ edit textarea appears โ†’ save works +2. **Visual:** Content visible, proper title, working controls +3. **Architecture:** No JavaScript in Python strings +4. **Clean:** Only new control system components active +5. **Simple:** Minimal changes to get core functionality working + +## ๐Ÿ“Š Final Assessment + +**What Worked:** +- Error detection and reporting +- Component recovery from git history +- Template variable substitution +- Initialization flow understanding + +**What Didn't Work:** +- Overly complex approach +- GUARDRAILS.md violations +- Component system mixing +- Content rendering integration + +**Recommendation:** +Reset to a working commit and take a focused, incremental approach that respects the architectural constraints while achieving the core goal of functional edit mode. \ No newline at end of file diff --git a/docs/ERROR_HANDLING_STRATEGY.md b/docs/ERROR_HANDLING_STRATEGY.md new file mode 100644 index 00000000..ab759a06 --- /dev/null +++ b/docs/ERROR_HANDLING_STRATEGY.md @@ -0,0 +1,263 @@ +# Error Handling Strategy: Fail Fast + Robustness Balance + +## Overview + +This document defines the balanced error handling strategy that combines **Fail Fast** principles for development with **Robustness Principles** for production, preventing both cascading failures and difficult diagnosis. + +## Core Philosophy + +### ๐Ÿšจ **Development Mode (Fail Fast)** +- **Immediate failure** on errors for fast debugging +- **Strict validation** with exceptions on invalid input +- **No silent failures** - all problems surface immediately +- **Clear error messages** with full context + +### ๐Ÿ›ก๏ธ **Production Mode (Robust)** +- **Graceful degradation** when components fail +- **Fallback behaviors** for non-critical failures +- **Silent recovery** for user experience +- **Detailed logging** for post-mortem analysis + +## Implementation Strategy + +### Mode Detection +```javascript +const MARKITECT_STRICT_MODE = ( + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + window.location.search.includes('strict=true') || + window.markitectStrictMode === true +); +``` + +### Dual-Behavior Error Handling +```javascript +safeOperation: function(operation, fallback = null, context = 'Unknown') { + try { + return operation(); + } catch (error) { + console.warn(`Operation failed in ${context}:`, error); + + // Fail Fast in development mode + if (MARKITECT_STRICT_MODE) { + console.error(`๐Ÿšจ STRICT MODE: Operation failed in ${context}`); + throw error; // Re-throw for immediate debugging + } + + // Robust handling in production + if (window.MarkitectDebugSystem) { + window.MarkitectDebugSystem.addMessage( + `Safe operation failed: ${error.message}`, + 'WARNING', + 'System', + { context, eventType: 'ERROR' } + ); + } + return typeof fallback === 'function' ? fallback() : fallback; + } +} +``` + +## Error Categories & Responses + +### 1. **Critical System Errors** +| Error Type | Development Response | Production Response | +|------------|---------------------|-------------------| +| Missing Dependencies | `throw Error()` immediately | Skip with warning, continue | +| Invalid Configuration | `throw Error()` immediately | Use defaults, log error | +| DOM Not Ready | `throw Error()` immediately | Retry with timeout | + +### 2. **Input Validation Errors** +| Error Type | Development Response | Production Response | +|------------|---------------------|-------------------| +| Malformed Data | `throw Error()` with details | Sanitize and continue | +| Oversized Input | `throw Error()` immediately | Truncate with warning | +| Invalid Selectors | `throw Error()` with context | Return null, log warning | + +### 3. **Resource Errors** +| Error Type | Development Response | Production Response | +|------------|---------------------|-------------------| +| Memory Exhaustion | `throw Error()` to prevent hang | Apply limits, degrade features | +| Network Failures | `throw Error()` for debugging | Use cached data, retry logic | +| Timeout Exceeded | `throw Error()` immediately | Cancel operation, fallback | + +### 4. **UI Component Errors** +| Error Type | Development Response | Production Response | +|------------|---------------------|-------------------| +| Control Creation Failed | `throw Error()` with stack | Create minimal fallback | +| DOM Manipulation Failed | `throw Error()` with element | Skip operation, continue | +| Event Handler Error | `throw Error()` to debug | Log error, disable feature | + +## Logging Strategy + +### Development Mode +```javascript +// Immediate console errors +console.error(`๐Ÿšจ STRICT MODE: ${message}`); +throw new Error(message); +``` + +### Production Mode +```javascript +// Silent logging with context +window.MarkitectDebugSystem.addMessage( + message, + 'ERROR', + component, + { context, stackTrace: error.stack } +); + +// User-friendly fallbacks +return fallbackValue || defaultBehavior(); +``` + +## Testing Approach + +### Development Testing +- **Error Injection**: Intentionally trigger failures +- **Boundary Testing**: Test limits and edge cases +- **Dependency Mocking**: Remove required components +- **Strict Validation**: Ensure all errors surface + +### Production Testing +- **Graceful Degradation**: Verify fallbacks work +- **Performance Under Load**: Stress test with errors +- **User Experience**: No broken interfaces +- **Recovery Scenarios**: System self-healing + +## Implementation Examples + +### Control Initialization +```javascript +initializeControl: function(controlClass, controlName, icon = '๐Ÿ”ง') { + try { + if (!controlClass) { + const message = `${controlName} class not available`; + + // Fail Fast in development + if (MARKITECT_STRICT_MODE) { + throw new Error(message); + } + + // Graceful in production + console.warn(message); + return this.createFallbackControl(controlName, icon); + } + + return new controlClass().createControl(); + } catch (error) { + if (MARKITECT_STRICT_MODE) { + throw error; // Let it bubble up + } + + // Production: log and continue + this.logError(error, controlName); + return null; + } +} +``` + +### Input Validation +```javascript +validateAndSanitize: function(input, maxLength = 1000) { + if (typeof input !== 'string') { + const error = new TypeError('Input must be string'); + + if (MARKITECT_STRICT_MODE) { + throw error; + } + + return String(input).slice(0, maxLength); + } + + if (input.length > maxLength) { + const error = new Error(`Input exceeds ${maxLength} characters`); + + if (MARKITECT_STRICT_MODE) { + throw error; + } + + console.warn('Input truncated to fit limits'); + return input.slice(0, maxLength); + } + + return input; +} +``` + +## Benefits + +### ๐Ÿš€ **Development Benefits** +- **Fast Problem Discovery**: Errors surface immediately +- **Clear Error Context**: Full stack traces and details +- **Prevents Technical Debt**: Forces proper error handling +- **Debugging Efficiency**: No need to backtrack from symptoms + +### ๐Ÿ›ก๏ธ **Production Benefits** +- **System Stability**: Graceful degradation prevents crashes +- **User Experience**: No broken interfaces or white screens +- **Self-Healing**: Automatic fallbacks and recovery +- **Operational Monitoring**: Detailed error telemetry + +### โš–๏ธ **Balance Benefits** +- **Best of Both Worlds**: Development speed + Production stability +- **Context-Appropriate**: Right behavior for the right environment +- **Maintainable**: Clear patterns and consistent implementation +- **Scalable**: Works from development to enterprise deployment + +## Activation Guide + +### Automatic Detection +- `localhost` and `127.0.0.1` automatically enable strict mode +- URL parameter `?strict=true` forces strict mode +- Global flag `window.markitectStrictMode = true` + +### Manual Control +```javascript +// Force strict mode for testing +window.markitectStrictMode = true; + +// Force production mode (disable strict) +window.markitectStrictMode = false; +``` + +### Environment Configuration +```javascript +// In development builds +const DEVELOPMENT_BUILD = true; +const MARKITECT_STRICT_MODE = DEVELOPMENT_BUILD || detectDevelopmentEnvironment(); + +// In production builds +const DEVELOPMENT_BUILD = false; +const MARKITECT_STRICT_MODE = false; // Always robust in production +``` + +## Monitoring & Metrics + +### Development Metrics +- **Error Count**: Number of strict mode exceptions +- **Error Categories**: Types of failures encountered +- **Resolution Time**: Time to fix after error discovery +- **Test Coverage**: Percentage of error paths tested + +### Production Metrics +- **Fallback Usage**: How often graceful degradation occurs +- **Recovery Success**: Percentage of successful recoveries +- **User Impact**: Features disabled vs. core functionality maintained +- **Error Patterns**: Common failure modes for improvement + +## Future Evolution + +### Enhanced Detection +- **CI/CD Integration**: Automatic strict mode in testing pipelines +- **Feature Flags**: Remote control of error handling behavior +- **A/B Testing**: Compare error handling strategies +- **Machine Learning**: Predict and prevent common failures + +### Advanced Recovery +- **Smart Fallbacks**: Context-aware recovery strategies +- **Progressive Enhancement**: Gradually restore failed features +- **User Notification**: Inform users of degraded functionality +- **Automatic Reporting**: Send error telemetry to development team + +This balanced approach ensures we catch problems early in development while maintaining a bulletproof production experience. \ No newline at end of file diff --git a/docs/adr/ADR-002-robustness-principle-for-production-use.md b/docs/adr/ADR-002-robustness-principle-for-production-use.md new file mode 100644 index 00000000..9534a2fc --- /dev/null +++ b/docs/adr/ADR-002-robustness-principle-for-production-use.md @@ -0,0 +1,384 @@ +# ADR-002: Robustness Principle for Production Use + +## Status +**Accepted** - 2025-11-11 + +## Context + +The Markitect application operates in unpredictable client-side environments where JavaScript execution can fail due to malicious input, network issues, browser inconsistencies, missing dependencies, or resource exhaustion. Traditional defensive programming approaches often result in cascading failures that crash entire UI components or leave the application in an unusable state. + +### Requirements +- **Fault Tolerance**: System must continue operating when individual components fail +- **Security**: Protection against malicious input and injection attacks +- **Resource Protection**: Prevention of DoS attacks through resource exhaustion +- **Graceful Degradation**: Non-essential features should fail without breaking core functionality +- **Error Containment**: Failures should be isolated and not cascade throughout the system +- **User Experience**: Users should never see white screens or completely broken interfaces +- **Developer Experience**: Clear error reporting and debugging capabilities + +### Problem Statement +The existing JavaScript codebase was vulnerable to: +1. **Uncaught Exceptions**: Single errors could crash entire UI components +2. **Input Validation Gaps**: Malicious or malformed input could break processing +3. **Resource Exhaustion**: Large datasets could freeze the browser +4. **Dependency Failures**: Missing libraries or features caused complete breakdowns +5. **DOM Manipulation Risks**: Direct DOM access without safety checks +6. **Cascading Failures**: One component failure affecting others + +## Decision + +**We will implement the Robustness Principle as a comprehensive defensive programming strategy with multiple layers of protection throughout the JavaScript codebase, balanced with Fail Fast behavior in development mode to prevent difficult diagnosis and cascading errors.** + +## Alternatives Considered + +### Option 1: Robustness Principle (Selected) +**Approach**: Multiple defensive layers with graceful degradation +**Implementation**: Safe wrappers, input validation, error boundaries, resource limits + +### Option 2: Try-Catch Everything +**Approach**: Wrap all operations in try-catch blocks +**Implementation**: Granular exception handling without systematic approach + +### Option 3: Reactive Error Handling +**Approach**: Error handling through reactive programming patterns +**Implementation**: RxJS or similar libraries for error stream management + +### Option 4: Minimal Validation +**Approach**: Basic input checking with assumption of good data +**Implementation**: Simple null checks and basic validation + +## Decision Matrix + +| Criteria | Robustness Principle | Try-Catch All | Reactive Patterns | Minimal Validation | +|----------|---------------------|---------------|-------------------|-------------------| +| **Fault Tolerance** | โœ… Comprehensive | โš ๏ธ Inconsistent | โœ… Good | โŒ Poor | +| **Security Protection** | โœ… Multi-layered | โŒ Reactive only | โš ๏ธ Limited | โŒ Vulnerable | +| **Resource Management** | โœ… Proactive limits | โŒ No protection | โš ๏ธ Some control | โŒ No protection | +| **Code Maintainability** | โœ… Systematic | โŒ Scattered | โš ๏ธ Complex | โœ… Simple | +| **Performance Impact** | โš ๏ธ Moderate overhead | โš ๏ธ High overhead | โŒ Library weight | โœ… Minimal | +| **Developer Experience** | โœ… Clear patterns | โŒ Repetitive | โŒ Learning curve | โœ… Familiar | +| **Error Recovery** | โœ… Graceful fallbacks | โš ๏ธ Manual recovery | โœ… Automatic retry | โŒ System failure | + +## Balanced Implementation: Robustness + Fail Fast + +### Development vs Production Behavior + +**Development Mode (Fail Fast)**: +- Immediate exceptions on errors for fast debugging +- Strict validation with no silent failures +- Full error context and stack traces +- Activated on localhost, 127.0.0.1, or `?strict=true` + +**Production Mode (Robust)**: +- Graceful degradation and fallback behaviors +- Silent recovery with detailed logging +- User experience preservation +- Default behavior in production environments + +```javascript +const MARKITECT_STRICT_MODE = ( + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + window.location.search.includes('strict=true') || + window.markitectStrictMode === true +); +``` + +## Robustness Principle Implementation + +### Layer 1: Input Validation & Sanitization +**Purpose**: Prevent malicious or malformed data from entering the system + +```javascript +safeTextExtraction(element) { + if (!this.validateElement(element)) { + return ''; + } + + try { + const text = element.textContent || element.innerText || ''; + return this.sanitizeText(text.trim()); + } catch (error) { + console.warn('Text extraction failed:', error); + return ''; + } +} + +sanitizeText(text) { + if (typeof text !== 'string') return ''; + + const maxLength = 100000; // 100KB text limit + return text + .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars + .slice(0, maxLength); // Limit length +} +``` + +### Layer 2: Error Boundaries with Fallbacks +**Purpose**: Contain failures and provide alternative execution paths + +```javascript +safeOperation(operation, fallback = null, context = 'Unknown') { + try { + return operation(); + } catch (error) { + console.warn(`Operation failed in ${context}:`, error); + + // Fail Fast in development mode + if (MARKITECT_STRICT_MODE) { + console.error(`๐Ÿšจ STRICT MODE: Operation failed in ${context}`); + throw error; // Re-throw for immediate debugging + } + + // Robust handling in production + if (window.MarkitectDebugSystem) { + window.MarkitectDebugSystem.addMessage( + `Safe operation failed: ${error.message}`, + 'WARNING', + 'RobustnessSystem', + { context, eventType: 'ERROR' } + ); + } + + return typeof fallback === 'function' ? fallback() : fallback; + } +} +``` + +### Layer 3: Resource Limits & Timeout Protection +**Purpose**: Prevent resource exhaustion and infinite operations + +```javascript +// Element processing limits +const elements = this.safeQuerySelectorAll(selector); +const maxElements = 10000; // DoS protection +elements.slice(0, maxElements).forEach(processElement); + +// Operation timeouts +const timeout = setTimeout(() => { + if (this.isOperationRunning) { + console.warn('Operation timed out'); + this.cleanup(); + } +}, 30000); // 30 second safety timeout +``` + +### Layer 4: Graceful Degradation +**Purpose**: Maintain core functionality when non-essential features fail + +```javascript +// Dependency checking with fallbacks +initializeControl(controlClass, controlName, icon = '๐Ÿ”ง') { + if (!controlClass) { + this.safeLog(`${controlName} class not available, skipping`, 'WARNING'); + return null; + } + + try { + const instance = new controlClass(); + return instance.createControl() ? instance : null; + } catch (error) { + // Create minimal fallback for essential controls + if (controlName === 'StatusControl') { + return this.createFallbackControl(controlName, icon); + } + return null; + } +} +``` + +### Layer 5: Safe DOM Manipulation +**Purpose**: Protect against DOM-related failures and validate operations + +```javascript +safeQuerySelector(selector, parent = document) { + try { + if (!parent || !parent.querySelector) { + return null; + } + return parent.querySelector(selector); + } catch (error) { + console.warn(`Invalid selector: ${selector}`, error); + return null; + } +} + +validateElement(element) { + return element && + element.nodeType === Node.ELEMENT_NODE && + element.isConnected && + !element.closest('.control-panel'); // Avoid control elements +} +``` + +## Rationale + +### Why the Robustness Principle? + +1. **Systematic Approach**: Unlike ad-hoc try-catch blocks, provides consistent protection patterns +2. **Multiple Defense Layers**: Each layer catches different types of failures +3. **Proactive Protection**: Prevents problems before they occur rather than just reacting +4. **Maintainable Code**: Clear patterns and utility functions reduce repetition +5. **Production Ready**: Designed for real-world environments with unpredictable conditions +6. **Performance Conscious**: Adds protection without significant overhead + +### Why Not Try-Catch Everything? + +- **Maintenance Burden**: Scattered exception handling is hard to maintain +- **Inconsistent Coverage**: Easy to miss critical paths +- **Poor Error Recovery**: Just catching errors doesn't provide meaningful fallbacks +- **Performance Impact**: Exception handling has overhead when overused + +### Why Not Reactive Patterns? + +- **Complexity**: RxJS adds significant learning curve and bundle size +- **Overkill**: Our error handling needs don't require reactive streams +- **Library Dependency**: Adds external dependency for core functionality +- **Framework Lock-in**: Ties architecture to specific programming paradigm + +## Implementation Details + +### Core Protection Utilities + +```javascript +// Central error handling system +const RobustnessSystem = { + safeOperation(operation, fallback, context), + safeQuerySelector(selector, parent), + safeQuerySelectorAll(selector, parent), + validateElement(element), + sanitizeText(text), + safeTextExtraction(element) +}; +``` + +### Integration Pattern + +```javascript +// Before: Fragile operation +function processDocument() { + const stats = calculateStats(); // Could crash + updateUI(stats); // Could crash + saveToStorage(stats); // Could crash +} + +// After: Robust operation +function processDocument() { + const stats = this.safeOperation( + () => this.calculateStats(), + this.getDefaultStats(), + 'calculateStats' + ); + + this.safeOperation( + () => this.updateUI(stats), + null, + 'updateUI' + ); + + this.safeOperation( + () => this.saveToStorage(stats), + null, + 'saveToStorage' + ); +} +``` + +### Resource Protection Examples + +```javascript +// Memory limits +const characters = Math.min(sectionText.length, 1000000); // Cap at 1MB + +// Processing limits +elements.slice(0, maxElements).forEach(processElement); + +// Time limits +const timeout = setTimeout(cleanup, OPERATION_TIMEOUT); +``` + +## Consequences + +### Positive +- โœ… **System Stability**: Individual component failures don't crash the entire application +- โœ… **Security Hardening**: Multiple layers protect against various attack vectors +- โœ… **User Experience**: Graceful degradation maintains usability during failures +- โœ… **Developer Confidence**: Clear patterns reduce fear of production failures +- โœ… **Debugging Capability**: Detailed error context and logging +- โœ… **Maintenance Reduction**: Fewer emergency fixes for production issues + +### Negative +- โš ๏ธ **Performance Overhead**: Additional validation and error checking adds some cost +- โš ๏ธ **Code Complexity**: More defensive code requires more careful implementation +- โš ๏ธ **Initial Development Time**: Building robust systems takes longer upfront + +### Mitigation Strategies +- **Performance**: Use efficient validation techniques and avoid redundant checks +- **Complexity**: Provide clear utility functions and documentation +- **Development Time**: Treat as investment in reduced maintenance and debugging time + +## Testing Strategy + +### Robustness Testing Categories + +1. **Malicious Input Testing**: XSS attempts, oversized data, invalid formats +2. **Resource Exhaustion Testing**: Large datasets, memory pressure scenarios +3. **Dependency Failure Testing**: Missing libraries, network failures +4. **DOM Manipulation Edge Cases**: Invalid selectors, disconnected elements +5. **Timeout Scenarios**: Long-running operations, infinite loops +6. **Error Cascade Testing**: Multiple simultaneous failures + +### Automated Testing + +```javascript +// Example robustness test +describe('Robustness Principle', () => { + it('should handle malicious text input safely', () => { + const maliciousText = ''.repeat(10000); + const result = statusControl.safeTextExtraction({ textContent: maliciousText }); + + expect(result.length).toBeLessThan(100001); // Respects limits + expect(result).not.toContain(' - +
@@ -1109,11 +1128,29 @@ class CleanDocumentManager: // Always render content first (graceful degradation) document.addEventListener('DOMContentLoaded', function() {{ - console.log("Rendering content..."); + console.log("๐ŸŽฏ Rendering content in {('insert' if insert_mode else 'edit')} mode..."); - // Check if modular components are being used + // Initialize edit/insert capabilities first (always needed) + if ((typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) || + (typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE)) {{ + const mode = (typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE) ? 'insert' : 'edit'; + console.log(`๐Ÿš€ Initializing clean ${{mode}} capabilities...`); + try {{ + console.log("Creating clean editor instance..."); + initializeCleanEditor(); + if (mode === 'insert') {{ + console.log("โœ… Clean insert mode active - click any section to edit (headings 1-3 protected)"); + }} else {{ + console.log("โœ… Clean edit mode active - click any section to edit"); + }} + }} catch (error) {{ + console.error(`โŒ Clean ${{mode}} mode failed to initialize:`, error); + }} + }} + + // Check if modular components are being used for content rendering if (typeof SectionManager !== 'undefined') {{ - console.log("โœ“ Modular components detected - skipping direct content rendering"); + console.log("โœ“ Modular components detected - skipping fallback content rendering"); console.log("โœ“ Content will be rendered by modular architecture"); return; }} @@ -1129,7 +1166,6 @@ class CleanDocumentManager: const htmlWithTargetBlank = html.replace(/]*)>/g, ''); contentDiv.innerHTML = htmlWithTargetBlank; console.log("โœ“ Content rendered successfully"); - console.log('โœ“ Markdown rendered successfully'); }} catch (error) {{ contentDiv.innerHTML = '

Error rendering markdown: ' + error.message + '

'; console.error("Content rendered with errors"); @@ -1152,605 +1188,14 @@ class CleanDocumentManager: }} }} - // Step 2: Initialize edit/insert capabilities if enabled - if ((typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) || - (typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE)) {{ - const mode = (typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE) ? 'insert' : 'edit'; - console.log(`Initializing clean ${{mode}} capabilities...`); - try {{ - console.log("Creating clean editor instance..."); - initializeCleanEditor(); - if (mode === 'insert') {{ - console.log("โœ“ Clean insert mode active - click any section to edit (headings 1-3 protected)"); - }} else {{ - console.log("โœ“ Clean edit mode active - click any section to edit"); - }} - }} catch (error) {{ - console.error(`Clean ${{mode}} mode failed to initialize:`, error); - }} - }} - - // Step 3: Initialize document scroll indicators (always available) + // Step 3: Initialize scroll indicators try {{ initializeScrollIndicators(); }} catch (error) {{ console.error("Scroll indicators failed to initialize:", error); }} - - // Step 4: Define abstract Control class for UI controls - const Control = {{ - // Abstract control properties - element: null, - isExpanded: false, - isHeaderOnly: false, // New state for header-only mode - isDragging: false, - isResizing: false, // New state for resizing mode - dragOffset: {{ x: 0, y: 0 }}, - resizeStartSize: {{ width: 280, height: 'auto' }}, - originalPosition: {{ top: '80px', left: '20px' }}, - defaultSize: {{ width: 280, minWidth: 200, minHeight: 150 }}, - - // Configuration properties (to be overridden by subclasses) - config: {{ - icon: '?', - title: 'Control', - className: 'control', - defaultContent: 'Template only', - ariaLabel: 'Control', - position: 'w' // Default compass position: west (middle-left) - }}, - - // Compass positioning system (top-aligned for proper expansion) - compassPositions: {{ - // North positions (top) - 'n': {{ top: '20px', left: '50%', transform: 'translateX(-50%)' }}, - 'nne': {{ top: '20px', left: '65%', transform: 'translateX(-50%)' }}, - 'ne': {{ top: '20px', right: '20px' }}, - 'ene': {{ top: '80px', right: '20px' }}, // Top-aligned instead of center - - // East positions (right) - 'e': {{ top: '50vh', right: '20px', transform: 'translateY(-20px)' }}, // Anchor at icon level - 'ese': {{ top: 'calc(65vh - 20px)', right: '20px' }}, // Top-aligned - 'se': {{ bottom: '20px', right: '20px' }}, - 'sse': {{ bottom: '20px', right: '35%', transform: 'translateX(50%)' }}, - - // South positions (bottom) - 's': {{ bottom: '20px', left: '50%', transform: 'translateX(-50%)' }}, - 'ssw': {{ bottom: '20px', left: '35%', transform: 'translateX(-50%)' }}, - 'sw': {{ bottom: '20px', left: '20px' }}, - 'wsw': {{ bottom: '80px', left: '20px' }}, // Top-aligned instead of center - - // West positions (left) - top-aligned for proper expansion - 'w': {{ top: '50vh', left: '20px', transform: 'translateY(-20px)' }}, // Anchor at icon level - 'wnw': {{ top: '80px', left: '20px' }}, // Top-aligned instead of center - 'nw': {{ top: '20px', left: '20px' }}, - 'nnw': {{ top: '20px', left: '35%', transform: 'translateX(-50%)' }} - }}, - - // Get expansion direction based on compass position - getExpansionDirection: function() {{ - const pos = this.config.position; - const rightBorderPositions = ['ne', 'ene', 'e', 'ese', 'se']; - const bottomBorderPositions = ['sw', 'ssw', 's', 'sse', 'se']; - - return {{ - header: rightBorderPositions.includes(pos) ? 'left' : 'right', - body: bottomBorderPositions.includes(pos) ? 'up' : 'down' - }}; - }}, - - // Calculate position styles based on compass direction - getPositionStyles: function() {{ - const compassPos = this.compassPositions[this.config.position] || this.compassPositions['w']; - return {{ - position: 'fixed', - top: compassPos.top || 'auto', - right: compassPos.right || 'auto', - bottom: compassPos.bottom || 'auto', - left: compassPos.left || 'auto', - transform: compassPos.transform || 'none', - zIndex: 1000 - }}; - }}, - - // Abstract methods (to be implemented by subclasses) - buildContent: function() {{ - const content = this.element.querySelector('.control-content'); - content.innerHTML = `

${{this.config.defaultContent}}

`; - }}, - - // Concrete methods (shared by all controls) - createControl: function() {{ - console.log(`๐ŸŽ›๏ธ Creating ${{this.config.title}} control...`); - - this.element = document.createElement('div'); - this.element.className = this.config.className; - this.element.innerHTML = ` - - - `; - - // Position using compass direction - const positionStyles = this.getPositionStyles(); - this.element.style.cssText = ` - position: ${{positionStyles.position}}; - top: ${{positionStyles.top}}; - right: ${{positionStyles.right}}; - bottom: ${{positionStyles.bottom}}; - left: ${{positionStyles.left}}; - transform: ${{positionStyles.transform}}; - z-index: ${{positionStyles.zIndex}}; - background: rgba(255, 255, 255, 0.95); - border: 1px solid #e1e5e9; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - backdrop-filter: blur(8px); - width: 40px; - transition: all 0.3s ease; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - `; - - // Store original position for reset - this.originalPosition = {{ - top: positionStyles.top, - right: positionStyles.right, - bottom: positionStyles.bottom, - left: positionStyles.left, - transform: positionStyles.transform - }}; - - // Style toggle button - const toggleBtn = this.element.querySelector('.control-toggle'); - toggleBtn.style.cssText = ` - width: 100%; - height: 40px; - border: none; - background: transparent; - cursor: pointer; - font-size: 16px; - color: #666; - transition: color 0.2s ease; - `; - - // Handle click to build content on-demand - toggleBtn.addEventListener('click', () => {{ - if (this.isExpanded) {{ - this.collapse(); - }} else {{ - console.log(`๐ŸŽ›๏ธ ${{this.config.title}} toggle clicked - building content...`); - this.buildContent(); - }} - }}); - - // Close button handler - const closeBtn = this.element.querySelector('.control-close'); - closeBtn.addEventListener('click', () => {{ - this.collapse(); - }}); - - // Responsive behavior - window.addEventListener('resize', () => {{ - if (window.innerWidth <= 768) {{ - this.element.style.display = 'none'; - }} else {{ - this.element.style.display = ''; - }} - }}); - - document.body.appendChild(this.element); - - // Hide on mobile - if (window.innerWidth <= 768) {{ - this.element.style.display = 'none'; - }} - - console.log(`๐ŸŽ›๏ธ ${{this.config.title}} control created`); - }}, - - styleHeader: function() {{ - const header = this.element.querySelector('.control-header'); - - // Style the header to show icon, title, and close button in one line - // Match the height of the collapsed icon state (40px) - header.style.cssText = ` - display: flex; - align-items: center; - justify-content: space-between; - height: 40px; - padding: 0 1rem; - border-bottom: 1px solid #eee; - margin-bottom: 0; - `; - - const icon = header.querySelector('.control-icon'); - if (icon) {{ - icon.style.cssText = ` - font-size: 16px; - color: #666; - margin-right: 0.5rem; - cursor: grab; - user-select: none; - `; - - // Make icon draggable - this.setupDragHandlers(icon); - }} - - const title = header.querySelector('.control-title'); - if (title) {{ - title.style.cssText = ` - margin: 0; - font-size: 0.9rem; - font-weight: 600; - flex-grow: 1; - line-height: 1; - cursor: pointer; - user-select: none; - `; - - // Add click handler to toggle header-only mode - title.addEventListener('click', () => {{ - this.toggleHeaderOnly(); - }}); - }} - - const closeBtn = header.querySelector('.control-close'); - if (closeBtn) {{ - closeBtn.style.cssText = ` - background: none; - border: none; - font-size: 14px; - cursor: pointer; - color: #6c757d; - padding: 0; - width: 20px; - height: 20px; - display: none; - align-items: center; - justify-content: center; - transition: color 0.2s ease; - `; - }} - }}, - - styleContent: function() {{ - const content = this.element.querySelector('.control-content'); - const expansion = this.getExpansionDirection(); - - // Style the content area based on expansion direction - let contentStyles = ` - padding: 0.5rem; - overflow-y: auto; - `; - - if (expansion.body === 'up') {{ - // Body expands upward (for bottom border positions) - contentStyles += ` - max-height: calc(80vh - 40px); - `; - content.parentElement.style.flexDirection = 'column-reverse'; - }} else {{ - // Body expands downward (default) - contentStyles += ` - max-height: calc(80vh - 40px); - `; - content.parentElement.style.flexDirection = 'column'; - }} - - content.style.cssText = contentStyles; - }}, - - expand: function() {{ - this.isExpanded = true; - const panel = this.element.querySelector('.control-panel'); - const toggleBtn = this.element.querySelector('.control-toggle'); - - // Get expansion direction based on compass position - const expansion = this.getExpansionDirection(); - - // Apply expansion styling based on direction - if (expansion.header === 'left') {{ - // Header expands to the left (for right border positions) - this.element.style.width = '280px'; - this.element.style.transformOrigin = 'top right'; - }} else {{ - // Header expands to the right (default) - this.element.style.width = '280px'; - this.element.style.transformOrigin = 'top left'; - }} - - panel.style.display = 'block'; - toggleBtn.style.display = 'none'; - - this.styleHeader(); - this.styleContent(); - this.addResizeHandle(); - }}, - - collapse: function() {{ - this.isExpanded = false; - this.isHeaderOnly = false; // Reset header-only state - const panel = this.element.querySelector('.control-panel'); - const toggleBtn = this.element.querySelector('.control-toggle'); - panel.style.display = 'none'; - - // Reset size to default - this.element.style.width = '40px'; - this.element.style.height = 'auto'; - - // Remove resize handle - this.removeResizeHandle(); - - toggleBtn.style.display = 'block'; - - // Reset position to original compass location - this.element.style.top = this.originalPosition.top; - this.element.style.right = this.originalPosition.right; - this.element.style.bottom = this.originalPosition.bottom; - this.element.style.left = this.originalPosition.left; - this.element.style.transform = this.originalPosition.transform; - }}, - - toggleHeaderOnly: function() {{ - if (!this.isExpanded) {{ - // If collapsed, first expand normally - this.buildContent(); - return; - }} - - const content = this.element.querySelector('.control-content'); - - if (this.isHeaderOnly) {{ - // Show content area (go to full expanded mode) - this.isHeaderOnly = false; - content.style.display = 'block'; - console.log(`๐ŸŽ›๏ธ ${{this.config.title}} expanded to full view`); - }} else {{ - // Hide content area (go to header-only mode) - this.isHeaderOnly = true; - content.style.display = 'none'; - console.log(`๐ŸŽ›๏ธ ${{this.config.title}} collapsed to header only`); - }} - }}, - - setupDragHandlers: function(dragElement) {{ - dragElement.addEventListener('mousedown', (e) => {{ - this.isDragging = true; - const rect = this.element.getBoundingClientRect(); - const iconRect = dragElement.getBoundingClientRect(); - - // Calculate offset relative to the icon position, not the element - this.dragOffset.x = e.clientX - rect.left; - this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center - - dragElement.style.cursor = 'grabbing'; - - e.preventDefault(); - }}); - - document.addEventListener('mousemove', (e) => {{ - if (!this.isDragging || !this.isExpanded) return; - - const newX = e.clientX - this.dragOffset.x; - const newY = e.clientY - this.dragOffset.y; - - // Keep within viewport bounds - const maxX = window.innerWidth - this.element.offsetWidth; - const maxY = window.innerHeight - this.element.offsetHeight; - - const boundedX = Math.max(0, Math.min(newX, maxX)); - const boundedY = Math.max(0, Math.min(newY, maxY)); - - this.element.style.left = boundedX + 'px'; - this.element.style.top = boundedY + 'px'; - }}); - - document.addEventListener('mouseup', () => {{ - if (this.isDragging) {{ - this.isDragging = false; - dragElement.style.cursor = 'grab'; - }} - }}); - }}, - - // Add resize handle to expanded control - addResizeHandle: function() {{ - // Remove existing resize handle if any - this.removeResizeHandle(); - - const resizeHandle = document.createElement('div'); - resizeHandle.className = 'control-resize-handle'; - // Create small circle for resize handle - resizeHandle.innerHTML = ''; - resizeHandle.style.cssText = ` - position: absolute; - bottom: 2px; - right: 2px; - width: 8px; - height: 8px; - cursor: nw-resize; - display: none; - user-select: none; - z-index: 1001; - background: #6c757d; - border-radius: 50%; - opacity: 0.6; - transition: opacity 0.2s ease; - `; - - this.element.appendChild(resizeHandle); - this.setupResizeHandlers(resizeHandle); - this.setupHoverBehavior(); - }}, - - // Setup hover behavior for resize handle and close button - setupHoverBehavior: function() {{ - const resizeHandle = this.element.querySelector('.control-resize-handle'); - const closeBtn = this.element.querySelector('.control-close'); - - if (resizeHandle && closeBtn) {{ - // Show/hide on control hover - this.element.addEventListener('mouseenter', () => {{ - resizeHandle.style.display = 'flex'; - closeBtn.style.display = 'block'; - }}); - - this.element.addEventListener('mouseleave', () => {{ - resizeHandle.style.display = 'none'; - closeBtn.style.display = 'none'; - }}); - }} - }}, - - // Remove resize handle - removeResizeHandle: function() {{ - const existingHandle = this.element.querySelector('.control-resize-handle'); - if (existingHandle) {{ - existingHandle.remove(); - }} - }}, - - // Set up resize event handlers - setupResizeHandlers: function(resizeHandle) {{ - resizeHandle.addEventListener('mousedown', (e) => {{ - this.isResizing = true; - const rect = this.element.getBoundingClientRect(); - this.resizeStartSize = {{ - width: rect.width, - height: rect.height, - startX: e.clientX, - startY: e.clientY - }}; - - resizeHandle.style.cursor = 'nw-resize'; - resizeHandle.style.color = '#28a745'; - - e.preventDefault(); - e.stopPropagation(); // Prevent triggering drag - }}); - - document.addEventListener('mousemove', (e) => {{ - if (!this.isResizing || !this.isExpanded) return; - - const deltaX = e.clientX - this.resizeStartSize.startX; - const deltaY = e.clientY - this.resizeStartSize.startY; - - const newWidth = Math.max(this.defaultSize.minWidth, this.resizeStartSize.width + deltaX); - const newHeight = Math.max(this.defaultSize.minHeight, this.resizeStartSize.height + deltaY); - - // Check viewport bounds - const maxWidth = window.innerWidth - this.element.offsetLeft; - const maxHeight = window.innerHeight - this.element.offsetTop; - - const boundedWidth = Math.min(newWidth, maxWidth - 20); - const boundedHeight = Math.min(newHeight, maxHeight - 20); - - this.element.style.width = boundedWidth + 'px'; - this.element.style.height = boundedHeight + 'px'; - - // Ensure content areas resize properly - this.updateContentSize(); - }}); - - document.addEventListener('mouseup', () => {{ - if (this.isResizing) {{ - this.isResizing = false; - resizeHandle.style.cursor = 'nw-resize'; - resizeHandle.style.color = '#6c757d'; - }} - }}); - }}, - - // Update content area sizes during resize - updateContentSize: function() {{ - const content = this.element.querySelector('.control-content'); - if (content) {{ - // Adjust content height to fit the resized control - const headerHeight = 40; // Header is 40px - const padding = 16; // Account for padding - const controlHeight = this.element.offsetHeight; - const availableHeight = controlHeight - headerHeight - padding; - - content.style.maxHeight = Math.max(100, availableHeight) + 'px'; - }} - }} - }}; - - // Step 5: Initialize ContentsControl (new implementation based on Control class) - try {{ - const contentsControl = Object.create(Control); - - // Configure for contents navigation - contentsControl.config = {{ - icon: 'โ˜ฐ', - title: 'Contents', - className: 'contents-control', - defaultContent: 'No headings found', - ariaLabel: 'Document Navigation', - position: 'wnw' // West-north-west positioning - }}; - - // Override buildContent method for navigation functionality - contentsControl.buildContent = function() {{ - const content = this.element.querySelector('.control-content'); - - // Build navigation content from current DOM - const allHeadings = document.querySelectorAll('h1, h2, h3'); - // Filter out headings that contain "Contents" or similar navigation-related text - const headings = Array.from(allHeadings).filter(heading => {{ - const text = heading.textContent.trim().toLowerCase(); - return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation'); - }}); - console.log("๐Ÿ“‹ Found headings for navigation:", headings.length); - - if (headings.length === 0) {{ - content.innerHTML = '

No headings found

'; - }} else {{ - let navHtml = ''; - headings.forEach((heading, index) => {{ - if (!heading.id) {{ - heading.id = `heading-${{index + 1}}`; - }} - const level = parseInt(heading.tagName.substring(1)); - const indent = (level - 1) * 1; - navHtml += ` -
- ${{heading.textContent.trim()}} - - `; - }}); - content.innerHTML = navHtml; - }} - - // Show panel - this.expand(); - }}; - - // Initialize the ContentsControl - contentsControl.createControl(); - - // Make globally available for mobile collapse - window.contentsControl = contentsControl; - }} catch (error) {{ - console.error("ContentsControl failed to initialize:", error); - }} - }}); - // Handle CDN loading errors window.addEventListener('load', function() {{ if (window.markitectMarkedError) {{ console.error("CDN library failed to load - network or firewall blocking marked.js"); @@ -1760,10 +1205,108 @@ class CleanDocumentManager: """ + # Determine version string for template substitution + if version_info: + version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" + else: + version_str = "0.5.0.dev" + + # Replace template placeholders (same as static mode) + html_template = template_content.replace('{title}', title) + html_template = html_template.replace('{version}', version_str) + + # No {content} placeholder in edit mode - content is handled by JavaScript + return html_template + + else: + # Use external template for static viewing mode + template_path = Path(__file__).parent / 'templates' / 'document.html' + if not template_path.exists(): + # Fallback to a minimal template if external template not found + template_content = """ + + + + + {title} + + + +
{content}
+ +""" + else: + template_content = template_path.read_text(encoding='utf-8') + + # Determine version string + if version_info: + version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" + else: + version_str = "0.5.0.dev" + + # Convert markdown to HTML (basic conversion) + try: + import markdown + html_content = markdown.markdown(markdown_content_with_dogtag, extensions=['extra', 'codehilite', 'toc']) + except ImportError: + # Fallback: simple line breaks and basic formatting + html_content = markdown_content_with_dogtag.replace('\n\n', '

').replace('\n', '
') + html_content = f'

{html_content}

' + + # Replace template placeholders using safe string replacement + # This avoids conflicts with CSS curly braces + html_template = template_content.replace('{title}', title) + html_template = html_template.replace('{version}', version_str) + html_template = html_template.replace('{content}', html_content) + return html_template + def _should_fail_fast(self) -> bool: + """ + Determine if we should fail fast (development mode) or continue gracefully (production mode). + + Fail fast in: + - Development environments (localhost, 127.0.0.1) + - When strict mode is enabled via environment variable + - When running in test environments + + Continue gracefully in: + - Production environments + - When explicitly disabled via environment variable + """ + import os + + # Check environment variables first + strict_env = os.getenv('MARKITECT_STRICT_MODE', '').lower() + if strict_env in ('true', '1', 'yes', 'on'): + return True + if strict_env in ('false', '0', 'no', 'off'): + return False + + # Check if we're in a development environment + # This mimics the JavaScript strict mode detection + try: + import socket + hostname = socket.gethostname().lower() + if 'localhost' in hostname or hostname.startswith('127.') or 'dev' in hostname: + return True + except: + pass + + # Check for test environment indicators + if any(env in os.environ for env in ['PYTEST_CURRENT_TEST', 'CI', 'CONTINUOUS_INTEGRATION', 'TESTING']): + return True + + # Default to graceful handling in production + return False + + def _get_clean_editor_scripts_backup(self) -> str: + """Legacy method kept for reference - should not be used.""" + # This method contained embedded JavaScript that has been moved to external files + return "" + def _get_clean_editor_scripts(self) -> str: - """Load the modular editor JavaScript components from external files.""" + """Load the modular editor JavaScript components for edit/insert modes.""" from pathlib import Path # Define the modular components to load in order @@ -1796,1228 +1339,45 @@ class CleanDocumentManager: # Add initialization script to wire up the components initialization_script = """ // === Component Initialization === -document.addEventListener('DOMContentLoaded', function() { +function initializeCleanEditor() { + console.log('๐Ÿš€ Initializing Clean Editor Components...'); + // Create container for the markdown content const container = document.getElementById('markdown-content') || document.body; - // Initialize components - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - // Create document controls - documentControls.create(); - - // Define abstract Control class for UI controls (same as viewing mode) - const Control = { - // Abstract control properties - element: null, - isExpanded: false, - isHeaderOnly: false, // New state for header-only mode - isDragging: false, - isResizing: false, // New state for resizing mode - dragOffset: { x: 0, y: 0 }, - resizeStartSize: { width: 280, height: 'auto' }, - originalPosition: { top: '80px', left: '20px' }, - defaultSize: { width: 280, minWidth: 200, minHeight: 150 }, - - // Configuration properties (to be overridden by subclasses) - config: { - icon: '?', - title: 'Control', - className: 'control', - defaultContent: 'Template only', - ariaLabel: 'Control', - position: 'w' // Default compass position: west (middle-left) - }, - - // Compass positioning system (top-aligned for proper expansion) - compassPositions: { - // North positions (top) - 'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' }, - 'nne': { top: '20px', left: '65%', transform: 'translateX(-50%)' }, - 'ne': { top: '20px', right: '20px' }, - 'ene': { top: '80px', right: '20px' }, // Top-aligned instead of center - - // East positions (right) - 'e': { top: '50vh', right: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level - 'ese': { top: 'calc(65vh - 20px)', right: '20px' }, // Top-aligned - 'se': { bottom: '20px', right: '20px' }, - 'sse': { bottom: '20px', right: '35%', transform: 'translateX(50%)' }, - - // South positions (bottom) - 's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' }, - 'ssw': { bottom: '20px', left: '35%', transform: 'translateX(-50%)' }, - 'sw': { bottom: '20px', left: '20px' }, - 'wsw': { bottom: '80px', left: '20px' }, // Top-aligned instead of center - - // West positions (left) - top-aligned for proper expansion - 'w': { top: '50vh', left: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level - 'wnw': { top: '80px', left: '20px' }, // Top-aligned instead of center - 'nw': { top: '20px', left: '20px' }, - 'nnw': { top: '20px', left: '35%', transform: 'translateX(-50%)' } - }, - - // Get expansion direction based on compass position - getExpansionDirection: function() { - const pos = this.config.position; - const rightBorderPositions = ['ne', 'ene', 'e', 'ese', 'se']; - const bottomBorderPositions = ['sw', 'ssw', 's', 'sse', 'se']; - - return { - header: rightBorderPositions.includes(pos) ? 'left' : 'right', - body: bottomBorderPositions.includes(pos) ? 'up' : 'down' - }; - }, - - // Calculate position styles based on compass direction - getPositionStyles: function() { - const compassPos = this.compassPositions[this.config.position] || this.compassPositions['w']; - return { - position: 'fixed', - top: compassPos.top || 'auto', - right: compassPos.right || 'auto', - bottom: compassPos.bottom || 'auto', - left: compassPos.left || 'auto', - transform: compassPos.transform || 'none', - zIndex: 1001 - }; - }, - - // Abstract methods (to be implemented by subclasses) - buildContent: function() { - const content = this.element.querySelector('.control-content'); - content.innerHTML = `

${this.config.defaultContent}

`; - }, - - // Concrete methods (shared by all controls) - createControl: function() { - console.log(`๐ŸŽ›๏ธ Creating ${this.config.title} control...`); - - this.element = document.createElement('div'); - this.element.className = this.config.className; - this.element.innerHTML = ` - - - `; - - // Position using compass direction - const positionStyles = this.getPositionStyles(); - this.element.style.cssText = ` - position: ${positionStyles.position}; - top: ${positionStyles.top}; - right: ${positionStyles.right}; - bottom: ${positionStyles.bottom}; - left: ${positionStyles.left}; - transform: ${positionStyles.transform}; - z-index: ${positionStyles.zIndex}; - background: rgba(255, 255, 255, 0.95); - border: 1px solid #e1e5e9; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - backdrop-filter: blur(8px); - width: 40px; - transition: all 0.3s ease; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - `; - - // Store original position for reset - this.originalPosition = { - top: positionStyles.top, - right: positionStyles.right, - bottom: positionStyles.bottom, - left: positionStyles.left, - transform: positionStyles.transform - }; - - // Style toggle button - const toggleBtn = this.element.querySelector('.control-toggle'); - toggleBtn.style.cssText = ` - width: 100%; - height: 40px; - border: none; - background: transparent; - cursor: pointer; - font-size: 16px; - color: #666; - transition: color 0.2s ease; - `; - - // Handle click to build content on-demand - toggleBtn.addEventListener('click', () => { - if (this.isExpanded) { - this.collapse(); - } else { - console.log(`๐ŸŽ›๏ธ ${this.config.title} toggle clicked - building content...`); - this.buildContent(); - } - }); - - // Close button handler - const closeBtn = this.element.querySelector('.control-close'); - closeBtn.addEventListener('click', () => { - this.collapse(); - }); - - document.body.appendChild(this.element); - console.log(`๐ŸŽ›๏ธ ${this.config.title} control created`); - }, - - styleHeader: function() { - const header = this.element.querySelector('.control-header'); - - // Style the header to show icon, title, and close button in one line - // Match the height of the collapsed icon state (40px) - header.style.cssText = ` - display: flex; - align-items: center; - justify-content: space-between; - height: 40px; - padding: 0 1rem; - border-bottom: 1px solid #eee; - margin-bottom: 0; - `; - - const icon = header.querySelector('.control-icon'); - if (icon) { - icon.style.cssText = ` - font-size: 16px; - color: #666; - margin-right: 0.5rem; - cursor: grab; - user-select: none; - `; - - // Make icon draggable - this.setupDragHandlers(icon); - } - - const title = header.querySelector('.control-title'); - if (title) { - title.style.cssText = ` - margin: 0; - font-size: 0.9rem; - font-weight: 600; - flex-grow: 1; - line-height: 1; - cursor: pointer; - user-select: none; - `; - - // Add click handler to toggle header-only mode - title.addEventListener('click', () => { - this.toggleHeaderOnly(); - }); - } - - const closeBtn = header.querySelector('.control-close'); - if (closeBtn) { - closeBtn.style.cssText = ` - background: none; - border: none; - font-size: 14px; - cursor: pointer; - color: #6c757d; - padding: 0; - width: 20px; - height: 20px; - display: none; - align-items: center; - justify-content: center; - transition: color 0.2s ease; - `; - } - }, - - styleContent: function() { - const content = this.element.querySelector('.control-content'); - const expansion = this.getExpansionDirection(); - - // Style the content area based on expansion direction - let contentStyles = ` - padding: 0.5rem; - overflow-y: auto; - `; - - if (expansion.body === 'up') { - // Body expands upward (for bottom border positions) - contentStyles += ` - max-height: calc(80vh - 40px); - `; - content.parentElement.style.flexDirection = 'column-reverse'; - } else { - // Body expands downward (default) - contentStyles += ` - max-height: calc(80vh - 40px); - `; - content.parentElement.style.flexDirection = 'column'; - } - - content.style.cssText = contentStyles; - }, - - expand: function() { - this.isExpanded = true; - const panel = this.element.querySelector('.control-panel'); - const toggleBtn = this.element.querySelector('.control-toggle'); - - // Get expansion direction based on compass position - const expansion = this.getExpansionDirection(); - - // Apply expansion styling based on direction - if (expansion.header === 'left') { - // Header expands to the left (for right border positions) - this.element.style.width = '300px'; - this.element.style.transformOrigin = 'top right'; - } else { - // Header expands to the right (default) - this.element.style.width = '300px'; - this.element.style.transformOrigin = 'top left'; - } - - panel.style.display = 'block'; - toggleBtn.style.display = 'none'; - - this.styleHeader(); - this.styleContent(); - this.addResizeHandle(); - }, - - collapse: function() { - this.isExpanded = false; - this.isHeaderOnly = false; // Reset header-only state - const panel = this.element.querySelector('.control-panel'); - const toggleBtn = this.element.querySelector('.control-toggle'); - panel.style.display = 'none'; - - // Reset size to default - this.element.style.width = '40px'; - this.element.style.height = 'auto'; - - // Remove resize handle - this.removeResizeHandle(); - - toggleBtn.style.display = 'block'; - - // Reset position to original compass location - this.element.style.top = this.originalPosition.top; - this.element.style.right = this.originalPosition.right; - this.element.style.bottom = this.originalPosition.bottom; - this.element.style.left = this.originalPosition.left; - this.element.style.transform = this.originalPosition.transform; - }, - - toggleHeaderOnly: function() { - if (!this.isExpanded) { - // If collapsed, first expand normally - this.buildContent(); - return; - } - - const content = this.element.querySelector('.control-content'); - - if (this.isHeaderOnly) { - // Show content area (go to full expanded mode) - this.isHeaderOnly = false; - content.style.display = 'block'; - console.log(`๐ŸŽ›๏ธ ${this.config.title} expanded to full view`); - } else { - // Hide content area (go to header-only mode) - this.isHeaderOnly = true; - content.style.display = 'none'; - console.log(`๐ŸŽ›๏ธ ${this.config.title} collapsed to header only`); - } - }, - - setupDragHandlers: function(dragElement) { - dragElement.addEventListener('mousedown', (e) => { - this.isDragging = true; - const rect = this.element.getBoundingClientRect(); - const iconRect = dragElement.getBoundingClientRect(); - - // Calculate offset relative to the icon position, not the element - this.dragOffset.x = e.clientX - rect.left; - this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center - - dragElement.style.cursor = 'grabbing'; - - e.preventDefault(); - }); - - document.addEventListener('mousemove', (e) => { - if (!this.isDragging || !this.isExpanded) return; - - const newX = e.clientX - this.dragOffset.x; - const newY = e.clientY - this.dragOffset.y; - - // Keep within viewport bounds - const maxX = window.innerWidth - this.element.offsetWidth; - const maxY = window.innerHeight - this.element.offsetHeight; - - const boundedX = Math.max(0, Math.min(newX, maxX)); - const boundedY = Math.max(0, Math.min(newY, maxY)); - - this.element.style.left = boundedX + 'px'; - this.element.style.top = boundedY + 'px'; - }); - - document.addEventListener('mouseup', () => { - if (this.isDragging) { - this.isDragging = false; - dragElement.style.cursor = 'grab'; - } - }); - }, - - // Add resize handle to expanded control (same as viewing mode) - addResizeHandle: function() { - // Remove existing resize handle if any - this.removeResizeHandle(); - - const resizeHandle = document.createElement('div'); - resizeHandle.className = 'control-resize-handle'; - // Create small circle for resize handle - resizeHandle.innerHTML = ''; - resizeHandle.style.cssText = ` - position: absolute; - bottom: 2px; - right: 2px; - width: 8px; - height: 8px; - cursor: nw-resize; - display: none; - user-select: none; - z-index: 1001; - background: #6c757d; - border-radius: 50%; - opacity: 0.6; - transition: opacity 0.2s ease; - `; - - this.element.appendChild(resizeHandle); - this.setupResizeHandlers(resizeHandle); - this.setupHoverBehavior(); - }, - - // Remove resize handle - removeResizeHandle: function() { - const existingHandle = this.element.querySelector('.control-resize-handle'); - if (existingHandle) { - existingHandle.remove(); - } - }, - - // Set up resize event handlers - setupResizeHandlers: function(resizeHandle) { - resizeHandle.addEventListener('mousedown', (e) => { - this.isResizing = true; - const rect = this.element.getBoundingClientRect(); - this.resizeStartSize = { - width: rect.width, - height: rect.height, - startX: e.clientX, - startY: e.clientY - }; - - resizeHandle.style.cursor = 'nw-resize'; - resizeHandle.style.background = 'rgba(40, 167, 69, 0.9)'; - - e.preventDefault(); - e.stopPropagation(); // Prevent triggering drag - }); - - document.addEventListener('mousemove', (e) => { - if (!this.isResizing || !this.isExpanded) return; - - const deltaX = e.clientX - this.resizeStartSize.startX; - const deltaY = e.clientY - this.resizeStartSize.startY; - - const newWidth = Math.max(this.defaultSize.minWidth, this.resizeStartSize.width + deltaX); - const newHeight = Math.max(this.defaultSize.minHeight, this.resizeStartSize.height + deltaY); - - // Check viewport bounds - const maxWidth = window.innerWidth - this.element.offsetLeft; - const maxHeight = window.innerHeight - this.element.offsetTop; - - const boundedWidth = Math.min(newWidth, maxWidth - 20); - const boundedHeight = Math.min(newHeight, maxHeight - 20); - - this.element.style.width = boundedWidth + 'px'; - this.element.style.height = boundedHeight + 'px'; - - // Ensure content areas resize properly - this.updateContentSize(); - }); - - document.addEventListener('mouseup', () => { - if (this.isResizing) { - this.isResizing = false; - resizeHandle.style.cursor = 'nw-resize'; - resizeHandle.style.color = '#6c757d'; - } - }); - }, - - // Update content area sizes during resize - updateContentSize: function() { - const content = this.element.querySelector('.control-content'); - if (content) { - // Adjust content height to fit the resized control - const headerHeight = 40; // Header is 40px - const padding = 16; // Account for padding - const controlHeight = this.element.offsetHeight; - const availableHeight = controlHeight - headerHeight - padding; - - content.style.maxHeight = Math.max(100, availableHeight) + 'px'; - } - }, - - // Setup hover behavior for resize handle and close button - setupHoverBehavior: function() { - const resizeHandle = this.element.querySelector('.control-resize-handle'); - const closeBtn = this.element.querySelector('.control-close'); - - if (resizeHandle && closeBtn) { - // Show/hide on control hover - this.element.addEventListener('mouseenter', () => { - resizeHandle.style.display = 'flex'; - closeBtn.style.display = 'block'; - }); - - this.element.addEventListener('mouseleave', () => { - resizeHandle.style.display = 'none'; - closeBtn.style.display = 'none'; - }); - } - } - }; - - // Create ContentsControl for edit mode (new implementation based on Control class) try { - const contentsControl = Object.create(Control); + // Initialize components + const sectionManager = new SectionManager(); + const domRenderer = new DOMRenderer(sectionManager, container); + const debugPanel = new DebugPanel(); + const documentControls = new DocumentControls(); - // Configure for contents navigation in edit mode - contentsControl.config = { - icon: 'โ˜ฐ', - title: 'Contents', - className: 'contents-control edit-mode', - defaultContent: 'No headings found', - ariaLabel: 'Document Navigation', - position: 'wnw' // West-north-west positioning + // Create document controls + documentControls.create(); + + console.log('โœ“ Clean Editor initialized successfully'); + console.log('โœ“ Click on any section to start editing'); + + // Make components globally available for debugging + window.editorComponents = { + sectionManager, + domRenderer, + debugPanel, + documentControls }; - // Override buildContent method for navigation functionality - contentsControl.buildContent = function() { - const content = this.element.querySelector('.control-content'); - - // Build navigation content from current DOM - const allHeadings = document.querySelectorAll('h1, h2, h3'); - // Filter out headings that contain "Contents" or similar navigation-related text - const headings = Array.from(allHeadings).filter(heading => { - const text = heading.textContent.trim().toLowerCase(); - return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation'); - }); - console.log("๐Ÿ“‹ Found headings for navigation:", headings.length); - - if (headings.length === 0) { - content.innerHTML = '

No headings found

'; - } else { - let navHtml = ''; - headings.forEach((heading, index) => { - if (!heading.id) { - heading.id = `heading-${index + 1}`; - } - const level = parseInt(heading.tagName.substring(1)); - const indent = (level - 1) * 1; - navHtml += ` - - ${heading.textContent.trim()} - - `; - }); - content.innerHTML = navHtml; - } - - // Show panel - this.expand(); - }; - - // Initialize the ContentsControl - contentsControl.createControl(); - - // Make globally available for mobile collapse - window.contentsControl = contentsControl; - } catch (error) { - console.error("ContentsControl failed to initialize:", error); - } - - // Step 7: Initialize Independent Debug System - try { - // Create independent debug system using IndexedDB for persistence - window.MarkitectDebugSystem = { - db: null, - messages: [], - maxMessages: 1000, - isEnabled: true, - subscribers: [], - - // Selection and filtering system - selectionCriteria: { - includeDocumentEvents: true, - includeSystemEvents: false, - includeControlEvents: true, - includeEditingEvents: true, - includeNavigationEvents: false, - includedHeadings: new Set(), // Track which document headings to monitor - excludedSources: new Set(['ContentsControl', 'DocumentNavigator']) - }, - - // Initialize IndexedDB for persistence - async init() { - return new Promise((resolve, reject) => { - const request = indexedDB.open('MarkitectDebugDB', 1); - - request.onerror = () => reject(request.error); - request.onsuccess = () => { - this.db = request.result; - this.loadMessages().then(resolve); - }; - - request.onupgradeneeded = (e) => { - const db = e.target.result; - if (!db.objectStoreNames.contains('messages')) { - const store = db.createObjectStore('messages', { keyPath: 'id', autoIncrement: true }); - store.createIndex('timestamp', 'timestamp', { unique: false }); - store.createIndex('category', 'category', { unique: false }); - } - }; - }); - }, - - // Add a debug message with selection filtering - async addMessage(message, category = 'INFO', source = 'System', context = {}) { - // Check if this message should be included based on selection criteria - if (!this.shouldIncludeMessage(message, category, source, context)) { - return null; - } - - const messageObj = { - message: String(message), - category: category.toUpperCase(), - source: source, - context: context, - timestamp: new Date().toISOString(), - displayTime: new Date().toLocaleTimeString() - }; - - // Add to memory - this.messages.push(messageObj); - - // Keep only last maxMessages in memory - if (this.messages.length > this.maxMessages) { - this.messages = this.messages.slice(-this.maxMessages); - } - - // Persist to IndexedDB - if (this.db) { - try { - const transaction = this.db.transaction(['messages'], 'readwrite'); - const store = transaction.objectStore('messages'); - await store.add(messageObj); - } catch (e) { - console.warn('Failed to persist debug message:', e); - } - } - - // Notify subscribers - this.subscribers.forEach(callback => { - try { callback(messageObj); } catch (e) { console.error('Debug subscriber error:', e); } - }); - - return messageObj; - }, - - // Selection filtering logic - shouldIncludeMessage(message, category, source, context) { - if (!this.isEnabled) return false; - - // Check excluded sources - if (this.selectionCriteria.excludedSources.has(source)) { - return false; - } - - // Category-based filtering - const categoryChecks = { - 'DOCUMENT': () => this.selectionCriteria.includeDocumentEvents, - 'SYSTEM': () => this.selectionCriteria.includeSystemEvents, - 'CONTROL': () => this.selectionCriteria.includeControlEvents, - 'EDITING': () => this.selectionCriteria.includeEditingEvents, - 'NAVIGATION': () => this.selectionCriteria.includeNavigationEvents - }; - - // Check if we have a specific category filter - if (context.eventType && categoryChecks[context.eventType]) { - return categoryChecks[context.eventType](); - } - - // Document heading specific filtering - if (context.headingId) { - return this.selectionCriteria.includedHeadings.has(context.headingId) || - this.selectionCriteria.includedHeadings.size === 0; // Include all if none specifically selected - } - - // Default: include based on general settings - return category === 'ERROR' || // Always include errors - this.selectionCriteria.includeSystemEvents; - }, - - // Document structure awareness - scanDocumentStructure() { - const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); - const documentStructure = { - headings: [], - totalSections: 0 - }; - - headings.forEach((heading, index) => { - // Skip control headings (like Contents, Debug, etc.) - const text = heading.textContent.trim().toLowerCase(); - if (text.includes('contents') || text.includes('debug') || - text.includes('control') || text.includes('navigation')) { - return; - } - - if (!heading.id) { - heading.id = `document-heading-${index + 1}`; - } - - documentStructure.headings.push({ - id: heading.id, - text: heading.textContent.trim(), - level: parseInt(heading.tagName.substring(1)), - element: heading - }); - }); - - documentStructure.totalSections = documentStructure.headings.length; - this.documentStructure = documentStructure; - - // Auto-include all document headings for monitoring - this.selectionCriteria.includedHeadings.clear(); - documentStructure.headings.forEach(heading => { - this.selectionCriteria.includedHeadings.add(heading.id); - }); - - this.addMessage(`Document structure scanned: ${documentStructure.totalSections} sections found`, - 'INFO', 'DocumentScanner', { eventType: 'DOCUMENT', headings: documentStructure.headings }); - - return documentStructure; - }, - - // Selection management methods - includeHeading(headingId) { - this.selectionCriteria.includedHeadings.add(headingId); - }, - - excludeHeading(headingId) { - this.selectionCriteria.includedHeadings.delete(headingId); - }, - - includeSource(source) { - this.selectionCriteria.excludedSources.delete(source); - }, - - excludeSource(source) { - this.selectionCriteria.excludedSources.add(source); - }, - - // Get selection criteria for UI - getSelectionCriteria() { - return { - ...this.selectionCriteria, - includedHeadings: Array.from(this.selectionCriteria.includedHeadings), - excludedSources: Array.from(this.selectionCriteria.excludedSources) - }; - }, - - // Load messages from IndexedDB - async loadMessages() { - if (!this.db) return; - - try { - const transaction = this.db.transaction(['messages'], 'readonly'); - const store = transaction.objectStore('messages'); - const request = store.getAll(); - - request.onsuccess = () => { - this.messages = request.result.slice(-this.maxMessages); - console.log(`๐Ÿ“Š Loaded ${this.messages.length} debug messages from IndexedDB`); - }; - } catch (e) { - console.warn('Failed to load debug messages:', e); - } - }, - - // Clear all messages - async clearMessages() { - this.messages = []; - - if (this.db) { - try { - const transaction = this.db.transaction(['messages'], 'readwrite'); - const store = transaction.objectStore('messages'); - await store.clear(); - } catch (e) { - console.warn('Failed to clear debug messages from DB:', e); - } - } - - this.subscribers.forEach(callback => { - try { callback({ type: 'clear' }); } catch (e) { console.error('Debug subscriber error:', e); } - }); - }, - - // Subscribe to debug updates - subscribe(callback) { - this.subscribers.push(callback); - return () => { - const index = this.subscribers.indexOf(callback); - if (index > -1) this.subscribers.splice(index, 1); - }; - }, - - // Get messages (with filtering) - getMessages(category = null, limit = null) { - let filtered = this.messages; - - if (category) { - filtered = filtered.filter(msg => msg.category === category.toUpperCase()); - } - - if (limit) { - filtered = filtered.slice(-limit); - } - - return filtered; - }, - - // Get recent messages - getRecentMessages(count = 50) { - return this.messages.slice(-count); - }, - - // Export messages as JSON - exportMessages() { - return JSON.stringify(this.messages, null, 2); - } - }; - - // Initialize the debug system - window.MarkitectDebugSystem.init().then(() => { - console.log('๐Ÿ“Š Markitect Debug System initialized'); - - // Add initial message - window.MarkitectDebugSystem.addMessage('Markitect Debug System initialized', 'INFO', 'DebugSystem', {eventType: 'SYSTEM'}); - - }).catch(error => { - console.warn('๐Ÿ“Š Debug System initialization failed, using memory only:', error); - window.MarkitectDebugSystem.addMessage('Debug System initialized (memory only)', 'WARNING', 'DebugSystem', {eventType: 'SYSTEM'}); - }); - } catch (error) { - console.error("Debug System initialization failed:", error); + console.error('โŒ Clean Editor initialization failed:', error); + throw error; } +} - // Step 8: Initialize DebugControl (new implementation based on Control class) - try { - const debugControl = Object.create(Control); - - // Configure for debug functionality - debugControl.config = { - icon: '๐Ÿชฒ', - title: 'Debug', - className: 'debug-control', - defaultContent: 'Debug panel controls', - ariaLabel: 'Debug Control', - position: 'ese' // East-south-east positioning - }; - - // Override buildContent method for debug functionality - debugControl.buildContent = function() { - console.log("๐Ÿชฒ Building debug control content..."); - - try { - const content = this.element.querySelector('.control-content'); - if (!content) { - console.error("๐Ÿชฒ Debug control content element not found"); - return; - } - - // Build debug control panel with selection and filtering - content.innerHTML = ` -
- -
-

Debug Messages

-
- - -
-
- - -
-
Event Types:
-
- - - - -
-
- - -
- Debug System: - -
- - -
-
- Click ๐Ÿ“Š Scan to analyze document structure -
-
- - -
- - -
-
- `; - - console.log("๐Ÿชฒ Debug control HTML content built successfully"); - - // Note: Debug switch state is set directly in HTML (checked attribute) - // No need to call updateDebugStatus() since checkbox manages its own state - - // Set up periodic update of debug messages - try { - this.setupMessageUpdates(); - } catch (e) { - console.log("๐Ÿชฒ Message updates setup failed:", e); - } - - // Show panel and call expand - console.log("๐Ÿชฒ Calling expand..."); - this.expand(); - console.log("๐Ÿชฒ Expand called successfully"); - - } catch (error) { - console.error("๐Ÿชฒ Error in buildContent:", error); - // Fallback simple content - const content = this.element.querySelector('.control-content'); - if (content) { - content.innerHTML = `
Debug control error: ${error.message}
`; - this.expand(); - } - } - }; - - // Add method to update debug switch status (updated for new debug system) - debugControl.updateDebugStatus = function() { - const checkbox = this.element.querySelector('#debugToggle'); - const statusText = this.element.querySelector('#debugStatus'); - - if (checkbox && statusText) { - try { - if (window.MarkitectDebugSystem) { - // Use the new debug system's enabled state - const isEnabled = window.MarkitectDebugSystem.isEnabled; - checkbox.checked = isEnabled; - statusText.textContent = isEnabled ? 'ON' : 'OFF'; - statusText.style.color = isEnabled ? '#28a745' : '#dc3545'; - } else { - // Fallback when debug system is not available - checkbox.checked = false; - statusText.textContent = 'UNAVAILABLE'; - statusText.style.color = '#dc3545'; - } - } catch (e) { - console.log("๐Ÿชฒ Error updating debug status:", e); - checkbox.checked = false; - statusText.textContent = 'ERROR'; - statusText.style.color = '#dc3545'; - } - } - }; - - // Add method to setup message updates - debugControl.setupMessageUpdates = function() { - try { - // Update messages every 500ms when debug control is open - this.messageUpdateInterval = setInterval(() => { - if (this.isExpanded && !this.isHeaderOnly) { - this.updateMessages(); - // Note: updateDebugStatus() removed - checkbox manages its own state - } - }, 500); - console.log("๐Ÿชฒ Message update interval set up successfully"); - } catch (e) { - console.log("๐Ÿชฒ Failed to set up message updates:", e); - } - }; - - // Add method to update debug messages display using new system - debugControl.updateMessages = function() { - const container = this.element.querySelector('#debugMessagesContainer'); - if (!container) return; - - try { - if (window.MarkitectDebugSystem) { - const messages = window.MarkitectDebugSystem.getRecentMessages(50); - if (messages && messages.length > 0) { - // Show messages in reverse order (newest first) - const reversedMessages = [...messages].reverse(); - container.innerHTML = ` -
- ${messages.length} messages (newest first) | - Add Test -
- ${reversedMessages.map(msg => ` -
-
-
- [${msg.displayTime}] - ${msg.category}: - ${msg.message} -
- ${msg.source ? `${msg.source}` : ''} -
- ${msg.context && msg.context.headingId ? ` -
- ๐Ÿ“ Section: ${msg.context.headingId} -
- ` : ''} -
- `).join('')} - `; - } else { - container.innerHTML = ` -
- No debug messages yet
- -
- `; - } - } else { - container.innerHTML = ` -
- Debug system not initialized -
- `; - } - } catch (e) { - console.log("๐Ÿชฒ Error updating messages:", e); - container.innerHTML = ` -
- Error loading messages: ${e.message} -
- `; - } - }; - - // Add method to get message colors by type - debugControl.getMessageColor = function(type) { - const colors = { - 'ERROR': '#dc3545', - 'WARNING': '#fd7e14', - 'SUCCESS': '#28a745', - 'INFO': '#17a2b8', - 'DEBUG': '#6f42c1' - }; - return colors[type] || '#495057'; - }; - - // Override collapse to clean up intervals - const originalCollapse = debugControl.collapse; - debugControl.collapse = function() { - if (this.messageUpdateInterval) { - clearInterval(this.messageUpdateInterval); - this.messageUpdateInterval = null; - } - originalCollapse.call(this); - }; - - // Create and show the debug control - console.log("๐Ÿชฒ Creating debug control..."); - debugControl.createControl(); - console.log("๐Ÿชฒ Debug control created, element:", debugControl.element); - - // Make debug control globally accessible - window.debugControl = debugControl; - console.log("๐Ÿชฒ Debug control setup complete and globally accessible"); - } catch (error) { - console.error("DebugControl failed to initialize:", error); - } - - - // Wire up event handlers - documentControls.setEventHandlers({ - 'save-document': () => { - console.log('Save document clicked'); - try { - // Get current markdown content from section manager - const currentMarkdown = sectionManager.getDocumentMarkdown(); - - // Create filename with timestamp suffix following the established convention - const now = new Date(); - const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-'); - - // Extract original filename from config or use default - const originalFilename = window.editorConfig?.originalFilename || 'document'; - const editedFilename = `${originalFilename}-edited-${timestamp}.md`; - - // Create and download the file - const blob = new Blob([currentMarkdown], { type: 'text/markdown' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = editedFilename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - // Log success to debug system - window.MarkitectDebugSystem?.addMessage(`Document saved as: ${editedFilename}`, 'SUCCESS', 'DocumentSaver', {eventType: 'DOCUMENT'}); - console.log(`Document successfully saved as: ${editedFilename}`); - - } catch (error) { - window.MarkitectDebugSystem?.addMessage(`Save failed: ${error.message}`, 'ERROR', 'DocumentSaver', {eventType: 'DOCUMENT'}); - console.error('Save error:', error); - } - }, - 'reset-all': () => { - console.log('Reset all clicked'); - // Hide any open editors - domRenderer.hideCurrentEditor(); - // Reset all sections to original state - const allSections = Array.from(sectionManager.sections.values()); - allSections.forEach(section => { - section.resetToOriginal(); - }); - // Re-render all sections - domRenderer.renderAllSections(allSections); - window.MarkitectDebugSystem?.addMessage(`Reset all sections to original state`, 'INFO', 'SectionManager', {eventType: 'EDITING'}); - } - }); - - // Set up debug system integration with section-aware logging - sectionManager.on('sections-created', (data) => { - window.MarkitectDebugSystem?.addMessage(`Created ${data.count} sections`, 'INFO', 'SectionManager', {eventType: 'DOCUMENT'}); - }); - - sectionManager.on('edit-started', (data) => { - window.MarkitectDebugSystem?.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG', 'SectionManager', {eventType: 'EDITING', sectionId: data.sectionId}); - }); - - sectionManager.on('changes-accepted', (data) => { - window.MarkitectDebugSystem?.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS', 'SectionManager', {eventType: 'EDITING', sectionId: data.sectionId}); - // Re-render the section to show updated content - const section = sectionManager.sections.get(data.sectionId); - if (section) { - const sectionElement = domRenderer.findSectionElement(data.sectionId); - if (sectionElement) { - const newElement = domRenderer.renderSection(section); - sectionElement.parentNode.replaceChild(newElement, sectionElement); - window.MarkitectDebugSystem?.addMessage(`DOM updated for section: ${data.sectionId}`, 'INFO', 'DOMRenderer', {eventType: 'EDITING', sectionId: data.sectionId}); - } - } - }); - - sectionManager.on('changes-cancelled', (data) => { - window.MarkitectDebugSystem?.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING', 'SectionManager', {eventType: 'EDITING', sectionId: data.sectionId}); - }); - - // Initialize with markdown content - const markdownToRender = markdownContent || ''; - if (markdownToRender.trim()) { - const sections = sectionManager.createSectionsFromMarkdown(markdownToRender); - domRenderer.renderAllSections(sections); - window.MarkitectDebugSystem?.addMessage(`Initialized with ${sections.length} sections`, 'INFO', 'DocumentRenderer', {eventType: 'DOCUMENT'}); - } else { - window.MarkitectDebugSystem?.addMessage('No markdown content to initialize', 'WARNING', 'DocumentRenderer', {eventType: 'DOCUMENT'}); - } - - // Make components globally available for debugging - window.markitectComponents = { - sectionManager, - domRenderer, - debugPanel, - documentControls - }; - - console.log('Markitect modular editor initialized successfully'); -}); +function initializeScrollIndicators() { + // Placeholder for scroll indicators - can be implemented later + console.log('๐Ÿ“ Scroll indicators initialized'); +} """ combined_script.append(initialization_script) return '\n'.join(combined_script) + diff --git a/markitect/plugins/builtin/markdown_commands.py b/markitect/plugins/builtin/markdown_commands.py index 21018646..b2eda7e4 100644 --- a/markitect/plugins/builtin/markdown_commands.py +++ b/markitect/plugins/builtin/markdown_commands.py @@ -2165,12 +2165,11 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_ should_ship_assets = True - # Discover and ship assets if needed + # Ship markdown-referenced assets first if needed if should_ship_assets: if output_is_directory: # For directory output, ship to the same directory as the HTML file _ship_assets(input_path, output_path.parent, verbose, silent) - # For file output, we don't ship assets (shouldn't reach here anyway) # Initialize clean document manager from markitect.clean_document_manager import CleanDocumentManager @@ -2189,7 +2188,8 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_ image_max_height=final_image_max_height) if not silent: - click.echo(f"โœ“ Rendered with interactive editing capabilities to: {output_path}") + click.echo(f"โœ… Rendered with INTERACTIVE editing mode to: {output_path}") + click.echo(f" Edit mode is fully functional with interactive section editing.") if verbose: click.echo(f"Editor theme: {editor_theme}") @@ -2208,7 +2208,8 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_ image_max_height=final_image_max_height) if not silent: - click.echo(f"โœ“ Rendered with interactive insert capabilities to: {output_path}") + click.echo(f"โœ… Rendered with INTERACTIVE insert mode to: {output_path}") + click.echo(f" Insert mode is fully functional with protected heading editing.") if verbose: click.echo(f"Editor theme: {editor_theme}") @@ -2232,6 +2233,10 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_ click.echo(f"Theme: {theme or 'default'}") click.echo(f"CSS: {css or 'default'}") + # Ship HTML-referenced assets (JavaScript, CSS) after HTML generation + if should_ship_assets and output_is_directory and output_path.exists(): + _ship_html_assets(output_path, output_path.parent, verbose, silent) + except Exception as e: click.echo(f"Error rendering file: {e}", err=True) raise click.Abort() @@ -3721,3 +3726,128 @@ def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False, sile click.echo(f"Error shipping assets: {e}", err=True) +def _ship_html_assets(html_path: Path, output_dir: Path, verbose: bool = False, silent: bool = False): + """ + Ship (copy) assets referenced in HTML file to output directory. + + This function scans the generated HTML file for JavaScript and CSS references, + then copies those assets to the output directory for deployment. + + Args: + html_path: Path to the generated HTML file + output_dir: Directory where assets should be copied + verbose: Whether to print detailed output + silent: Whether to suppress non-essential output + """ + import shutil + import hashlib + from markitect.assets.discovery import discover_assets_from_html + + def get_file_hash(file_path): + """Get SHA-256 hash of file content for content comparison.""" + hash_sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_sha256.update(chunk) + return hash_sha256.hexdigest() + + try: + # Read the HTML content + html_content = html_path.read_text(encoding='utf-8') + + # Discover HTML assets (JavaScript, CSS) + # Use the project root as base path for resolving markitect/static/js paths + project_root = Path(__file__).parent.parent.parent.parent # Go up to project root (markitect/plugins/builtin/markdown_commands.py -> project_root) + assets = discover_assets_from_html(html_content, project_root) + + if not assets: + if verbose: + click.echo(" No HTML assets (JS/CSS) found to ship") + return + + shipped_count = 0 + skipped_count = 0 + missing_count = 0 + + if not silent: + click.echo(f"๐Ÿ“ฆ Shipping {len(assets)} HTML assets...") + + for asset_ref in assets: + # Skip URLs and broken assets + if asset_ref.asset_path.startswith(('http:', 'https:', 'mailto:', 'data:')): + continue + + if asset_ref.is_broken or not asset_ref.resolved_path: + missing_count += 1 + if verbose: + click.echo(f" โš  Missing HTML asset: {asset_ref.asset_path}", err=True) + continue + + # Determine output path (preserve relative directory structure) + clean_path = asset_ref.asset_path.lstrip('./') + dest_path = output_dir / clean_path + + # Create destination directory + dest_path.parent.mkdir(parents=True, exist_ok=True) + + # Check if we need to copy (smart comparison for cross-filesystem compatibility) + should_copy = True + if dest_path.exists(): + source_stat = asset_ref.resolved_path.stat() + dest_stat = dest_path.stat() + + # Detect if we're in a cross-filesystem scenario where timestamps might be unreliable + # Heuristics: different filesystems, or timestamps that don't make sense + is_cross_fs = ( + # Different device IDs suggests different filesystems + source_stat.st_dev != dest_stat.st_dev or + # Destination path starts with /mnt/ (common WSL Windows mount) + str(dest_path).startswith('/mnt/') or + # Very large timestamp differences (>1 hour) for same content suggest sync issues + abs(source_stat.st_mtime - dest_stat.st_mtime) > 3600 + ) + + if is_cross_fs: + # Use content-based comparison for cross-filesystem scenarios + if source_stat.st_size == dest_stat.st_size: + try: + source_hash = get_file_hash(asset_ref.resolved_path) + dest_hash = get_file_hash(dest_path) + + if source_hash == dest_hash: + should_copy = False + skipped_count += 1 + if verbose: + click.echo(f" โ†’ Content verified (cross-fs): {asset_ref.asset_path}") + # If hashes differ, should_copy remains True + except (OSError, IOError): + if verbose: + click.echo(f" โš  Could not verify content, will copy: {asset_ref.asset_path}") + pass + # If sizes differ, should_copy remains True + else: + # Use fast timestamp comparison for same-filesystem scenarios + if source_stat.st_mtime <= dest_stat.st_mtime and source_stat.st_size == dest_stat.st_size: + should_copy = False + skipped_count += 1 + if verbose: + click.echo(f" โ†’ Timestamp verified: {asset_ref.asset_path}") + # If timestamp suggests newer source or different size, should_copy remains True + + if should_copy: + shutil.copy2(asset_ref.resolved_path, dest_path) + shipped_count += 1 + if verbose: + click.echo(f" โœ“ Shipped HTML asset: {asset_ref.asset_path}") + + # Report results + if not silent: + click.echo(f"โœ“ Shipped {shipped_count} HTML assets, skipped {skipped_count} up-to-date") + if missing_count > 0: + click.echo(f" โš  {missing_count} HTML assets not found", err=True) + + except Exception as e: + if verbose: + click.echo(f"Error shipping HTML assets: {e}", err=True) + + diff --git a/markitect/static/css/controls.css b/markitect/static/css/controls.css new file mode 100644 index 00000000..9aac2bf6 --- /dev/null +++ b/markitect/static/css/controls.css @@ -0,0 +1,197 @@ +/** + * Control System CSS for Markitect + * Styles for positioning, interactions, and responsive behavior + */ + +/* Base control panel styles */ +.control-panel { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-size: 14px; + z-index: 1000; + user-select: none; +} + +.control-header { + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.control-header:hover { + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + transform: translateY(-1px); +} + +.control-content { + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + transition: all 0.3s ease; +} + +.control-content::-webkit-scrollbar { + width: 6px; +} + +.control-content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +.control-content::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +.control-content::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* Control-specific styles */ +.status-control .control-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border: 1px solid #dee2e6; +} + +.debug-control .control-header { + background: linear-gradient(135deg, #f8f9fa 0%, #fff3cd 100%); + border: 1px solid #ffeaa7; +} + +.contents-control .control-header { + background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); + border: 1px solid #2196f3; +} + +.edit-control .control-header { + background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%); + border: 1px solid #4caf50; +} + +/* Resize handle */ +.resize-handle { + transition: opacity 0.2s ease; +} + +.resize-handle:hover { + background: #495057 !important; + transform: scale(1.2); +} + +/* Button styles */ +.control-content button { + transition: all 0.2s ease; + font-weight: 500; + border: none; + cursor: pointer; +} + +.control-content button:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +.control-content button:active { + transform: translateY(0); + box-shadow: 0 1px 2px rgba(0,0,0,0.2); +} + +/* Footer styles */ +.control-footer { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +/* Responsive behavior */ +@media (max-width: 768px) { + .control-panel { + font-size: 12px; + } + + .control-content { + max-width: calc(100vw - 40px) !important; + max-height: calc(100vh - 120px) !important; + } + + /* Adjust positioning for mobile */ + .control-panel[style*="right: 20px"] { + right: 10px !important; + } + + .control-panel[style*="left: 20px"] { + left: 10px !important; + } + + .control-panel[style*="top: 20px"] { + top: 10px !important; + } + + .control-panel[style*="bottom: 20px"] { + bottom: 10px !important; + } +} + +@media (max-width: 480px) { + .control-content { + font-size: 0.7rem !important; + } + + .control-header { + padding: 0.4rem !important; + font-size: 0.8rem !important; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .control-panel { + color: #e9ecef; + } + + .control-header { + background: linear-gradient(135deg, #343a40 0%, #495057 100%) !important; + border-color: #6c757d !important; + } + + .control-content { + background: #2d3436 !important; + border-color: #6c757d !important; + color: #e9ecef; + } + + .control-footer { + background: #343a40 !important; + border-color: #6c757d !important; + } + + .control-content button { + border-color: #6c757d !important; + } +} + +/* Animation classes */ +.control-fade-in { + animation: controlFadeIn 0.3s ease-out; +} + +@keyframes controlFadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.control-slide-out { + animation: controlSlideOut 0.2s ease-in; +} + +@keyframes controlSlideOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } +} \ No newline at end of file diff --git a/markitect/static/js/controls/contents-control.js b/markitect/static/js/controls/contents-control.js new file mode 100644 index 00000000..843e3559 --- /dev/null +++ b/markitect/static/js/controls/contents-control.js @@ -0,0 +1,93 @@ +/** + * Contents Control - Displays document table of contents + * Implements the Robustness Principle with Fail Fast mode support + */ + +class ContentsControl { + constructor() { + this.control = Object.create(Control); + this.control.config = { + icon: 'โ˜ฐ', + title: 'Contents', + className: 'contents-control', + defaultContent: 'Click to view table of contents', + ariaLabel: 'Contents Control', + position: 'w' + }; + + // Bind methods to control + this.control.buildContent = () => { + const content = this.control.element.querySelector('.control-content'); + const headings = this.extractHeadings(); + + content.innerHTML = ` +
+

Table of Contents

+
+ ${headings.length > 0 ? + headings.map(heading => + `
+ + ${heading.text} + +
` + ).join('') : + '

No headings found in document

' + } +
+
+ ${headings.length} heading${headings.length !== 1 ? 's' : ''} found +
+
+ `; + this.control.isExpanded = true; + }; + + this.control.toggle = () => { + if (this.control.isExpanded) { + this.control.element.querySelector('.control-content').style.display = 'none'; + this.control.isExpanded = false; + } else { + this.control.buildContent(); + this.control.element.querySelector('.control-content').style.display = 'block'; + } + }; + } + + extractHeadings() { + const headings = []; + const elements = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); + + elements.forEach((heading, index) => { + const level = parseInt(heading.tagName.charAt(1)); + const text = heading.textContent || heading.innerText || ''; + let id = heading.id; + + // Generate ID if not present + if (!id) { + id = text.toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') || `heading-${index}`; + heading.id = id; + } + + headings.push({ + level: level, + text: text.trim(), + id: id + }); + }); + + return headings; + } + + createControl() { + return this.control.createControl(); + } +} + +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 new file mode 100644 index 00000000..aa7b0e65 --- /dev/null +++ b/markitect/static/js/controls/control-base.js @@ -0,0 +1,515 @@ +/** + * Base Control Class for Markitect UI Controls + * Provides common functionality for positioning, drag, resize, expand/collapse + * Supports Fail Fast strict mode for development + */ + +// Development mode detection (must match main.js) +const MARKITECT_STRICT_MODE = ( + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + window.location.search.includes('strict=true') || + window.markitectStrictMode === true +); + +const Control = { + // Default configuration + config: { + icon: '๐Ÿ”ง', + title: 'Control', + className: 'base-control', + defaultContent: 'Control content', + ariaLabel: 'Base Control', + position: 'w', // Default compass position: west (middle-left) + footer: null // If null, will use default Markitect copyright + }, + + // Utility functions for safe operations + safeOperation: function(operation, fallback = null, context = 'Unknown') { + try { + return operation(); + } catch (error) { + console.warn(`Control operation failed in ${context}:`, error); + + // Fail Fast in development mode + if (MARKITECT_STRICT_MODE) { + console.error(`๐Ÿšจ STRICT MODE: Control operation failed in ${context}`); + throw error; // Re-throw for immediate debugging + } + + if (window.MarkitectDebugSystem) { + window.MarkitectDebugSystem.addMessage( + `Safe operation failed: ${error.message}`, + 'WARNING', + 'Control', + { context, eventType: 'ERROR' } + ); + } + return typeof fallback === 'function' ? fallback() : fallback; + } + }, + + safeQuerySelector: function(selector, parent = document) { + try { + if (!parent || !parent.querySelector) { + return null; + } + return parent.querySelector(selector); + } catch (error) { + console.warn(`Invalid selector: ${selector}`, error); + return null; + } + }, + + safeQuerySelectorAll: function(selector, parent = document) { + try { + if (!parent || !parent.querySelectorAll) { + return []; + } + return Array.from(parent.querySelectorAll(selector)); + } catch (error) { + console.warn(`Invalid selector: ${selector}`, error); + return []; + } + }, + + // Version and default footer + getMarkitectVersion: function() { + return this.safeOperation(() => { + // Try to get version from various sources + if (window.markitectVersion) { + return window.markitectVersion; + } + + // Check for generator meta tag in document head + const generatorMeta = this.safeQuerySelector('meta[name="generator"]'); + if (generatorMeta) { + const content = generatorMeta.getAttribute('content'); + if (content && content.includes('Markitect')) { + // Extract version from generator content + // Expected formats: "Markitect 1.0.0" or "Markitect/1.0.0" or "Markitect v1.0.0" + const versionMatch = content.match(/Markitect[\s\/v]*([\d\.]+[\w\-\.]*)/i); + if (versionMatch && versionMatch[1]) { + return versionMatch[1]; + } + } + } + + // Fallback version with generation timestamp + const now = new Date(); + const timestamp = now.toISOString().slice(0, 19).replace('T', ' '); + return `Generated ${timestamp}`; + }, () => 'Unknown Version', 'getMarkitectVersion'); + }, + + getDefaultFooter: function() { + return `ยฉ Markitect ${this.getMarkitectVersion()}`; + }, + + getFooter: function() { + if (this.config.footer !== null) { + return this.config.footer; + } + return this.getDefaultFooter(); + }, + + // Compass positioning system (top-aligned for proper expansion) + compassPositions: { + 'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' }, + 'nne': { top: '40px', right: '120px' }, + 'ne': { top: '20px', right: '20px' }, + 'ene': { top: '80px', right: '20px' }, + 'e': { top: '50vh', right: '20px', transform: 'translateY(-20px)' }, + 'ese': { top: 'calc(50vh + 80px)', right: '20px', transform: 'translateY(-20px)' }, + 'se': { bottom: '20px', right: '20px' }, + 'sse': { bottom: '40px', right: '120px' }, + 's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' }, + 'ssw': { bottom: '40px', left: '120px' }, + 'sw': { bottom: '20px', left: '20px' }, + 'wsw': { bottom: '80px', left: '20px' }, + 'w': { top: '50vh', left: '20px', transform: 'translateY(-20px)' }, + 'wnw': { top: 'calc(50vh - 80px)', left: '20px', transform: 'translateY(-20px)' }, + 'nw': { top: '20px', left: '20px' }, + 'nnw': { top: '40px', left: '120px' } + }, + + // State management + isExpanded: false, + isDragging: false, + isResizing: false, + element: null, + + createControl: function() { + return this.safeOperation(() => { + console.log(`Creating ${this.config.title} control...`); + + // Validate configuration + if (!this.config || !this.config.title) { + throw new Error('Invalid control configuration'); + } + + // Ensure document.body exists + if (!document.body) { + throw new Error('Document body not available'); + } + + // Create main control element + this.element = document.createElement('div'); + this.element.className = `control-panel ${this.config.className || ''}`; + this.element.setAttribute('role', 'dialog'); + this.element.setAttribute('aria-label', this.config.ariaLabel || this.config.title); + + // Position the control using compass system + const position = this.compassPositions[this.config.position] || this.compassPositions['w']; + Object.assign(this.element.style, { + position: 'fixed', + zIndex: '1000', + ...position + }); + + // Build the control structure + this.buildControlStructure(); + + // Add to document + document.body.appendChild(this.element); + + console.log(`${this.config.title} control created and positioned at ${this.config.position}`); + return this.element; + }, () => { + console.error(`Failed to create ${this.config?.title || 'Unknown'} control`); + return null; + }, 'createControl'); + }, + + buildControlStructure: function() { + this.safeOperation(() => { + if (!this.element) { + throw new Error('Control element not available'); + } + + // Sanitize configuration values + const safeIcon = (this.config.icon || '๐Ÿ”ง').replace(/[<>"'&]/g, ''); + const safeTitle = (this.config.title || 'Control').replace(/[<>"'&]/g, ''); + const safeContent = (this.config.defaultContent || 'Control content').replace(/[<>]/g, ''); + + this.element.innerHTML = ` +
+
+ ${safeIcon} + ${safeTitle} +
+ +
+
+
+ ${safeContent} +
+ +
+ `; + + // Set up event listeners with error protection + this.safeOperation(() => this.setupEventListeners(), null, 'setupEventListeners'); + this.safeOperation(() => this.addResizeHandle(), null, 'addResizeHandle'); + this.safeOperation(() => this.addDragFunctionality(), null, 'addDragFunctionality'); + }, () => { + console.error('Failed to build control structure'); + if (this.element) { + this.element.innerHTML = '
Control failed to load
'; + } + }, 'buildControlStructure'); + }, + + setupEventListeners: function() { + const header = this.safeQuerySelector('.control-header', this.element); + const closeBtn = this.safeQuerySelector('.control-close', this.element); + + if (!header || !closeBtn) { + console.warn('Control header or close button not found'); + return; + } + + // Toggle expand/collapse on header click + header.addEventListener('click', (e) => { + this.safeOperation(() => { + e.stopPropagation(); + this.toggle(); + }, null, 'headerClick'); + }); + + // Close button + closeBtn.addEventListener('click', (e) => { + this.safeOperation(() => { + e.stopPropagation(); + this.collapse(); + }, null, 'closeClick'); + }); + + // Show/hide close button and resize handle on hover with bounds checking + this.element.addEventListener('mouseenter', () => { + this.safeOperation(() => { + if (this.isExpanded && closeBtn) { + closeBtn.style.display = 'flex'; + const resizeHandle = this.safeQuerySelector('.resize-handle', this.element); + if (resizeHandle) { + resizeHandle.style.display = 'block'; + } + } + }, null, 'mouseEnter'); + }); + + this.element.addEventListener('mouseleave', () => { + this.safeOperation(() => { + if (closeBtn) { + closeBtn.style.display = 'none'; + } + const resizeHandle = this.safeQuerySelector('.resize-handle', this.element); + if (resizeHandle) { + resizeHandle.style.display = 'none'; + } + }, null, 'mouseLeave'); + }); + }, + + addResizeHandle: function() { + const resizeHandle = document.createElement('div'); + resizeHandle.className = 'resize-handle'; + resizeHandle.innerHTML = ''; // Small circle via CSS + resizeHandle.style.cssText = ` + position: absolute; bottom: 2px; right: 2px; + width: 8px; height: 8px; cursor: nw-resize; + display: none; background: #6c757d; border-radius: 50%; + `; + + this.element.appendChild(resizeHandle); + + // Resize functionality + let startX, startY, startWidth, startHeight; + + resizeHandle.addEventListener('mousedown', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.isResizing = true; + + const content = this.element.querySelector('.control-content'); + const rect = content.getBoundingClientRect(); + + startX = e.clientX; + startY = e.clientY; + startWidth = rect.width; + startHeight = rect.height; + + document.addEventListener('mousemove', handleResize); + document.addEventListener('mouseup', stopResize); + }); + + const handleResize = (e) => { + if (!this.isResizing) return; + + const content = this.element.querySelector('.control-content'); + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + const newWidth = Math.max(200, startWidth + deltaX); + const newHeight = Math.max(100, startHeight + deltaY); + + content.style.width = `${newWidth}px`; + content.style.height = `${newHeight}px`; + }; + + const stopResize = () => { + this.isResizing = false; + document.removeEventListener('mousemove', handleResize); + document.removeEventListener('mouseup', stopResize); + }; + }, + + addDragFunctionality: function() { + const header = this.safeQuerySelector('.control-header', this.element); + if (!header) { + console.warn('Header not found for drag functionality'); + return; + } + + let startX, startY, startLeft, startTop, dragTimeout; + + header.addEventListener('mousedown', (e) => { + this.safeOperation(() => { + if (e.target.closest('.control-close')) return; + + // Clear any existing drag timeout + if (dragTimeout) { + clearTimeout(dragTimeout); + } + + this.isDragging = true; + const rect = this.element.getBoundingClientRect(); + + startX = e.clientX; + startY = e.clientY; + startLeft = rect.left; + startTop = rect.top; + + document.addEventListener('mousemove', handleDrag); + document.addEventListener('mouseup', stopDrag); + + // Safety timeout to prevent infinite dragging + dragTimeout = setTimeout(() => { + if (this.isDragging) { + console.warn('Drag operation timed out'); + stopDrag(); + } + }, 30000); // 30 second timeout + }, null, 'dragStart'); + }); + + const handleDrag = (e) => { + this.safeOperation(() => { + if (!this.isDragging || !this.element) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + // Constrain to viewport bounds + const viewportWidth = window.innerWidth || document.documentElement.clientWidth; + const viewportHeight = window.innerHeight || document.documentElement.clientHeight; + + const newLeft = Math.max(0, Math.min(viewportWidth - 100, startLeft + deltaX)); + const newTop = Math.max(0, Math.min(viewportHeight - 50, startTop + deltaY)); + + this.element.style.left = `${newLeft}px`; + this.element.style.top = `${newTop}px`; + this.element.style.right = 'auto'; + this.element.style.bottom = 'auto'; + this.element.style.transform = 'none'; + }, null, 'dragMove'); + }; + + const stopDrag = () => { + this.safeOperation(() => { + this.isDragging = false; + if (dragTimeout) { + clearTimeout(dragTimeout); + dragTimeout = null; + } + document.removeEventListener('mousemove', handleDrag); + document.removeEventListener('mouseup', stopDrag); + }, null, 'dragStop'); + }; + }, + + expand: function() { + this.safeOperation(() => { + if (this.isExpanded) return; + + const content = this.safeQuerySelector('.control-content', this.element); + const closeBtn = this.safeQuerySelector('.control-close', this.element); + + if (!content || !closeBtn) { + console.warn('Control content or close button not found for expansion'); + return; + } + + content.style.display = 'block'; + closeBtn.style.display = 'flex'; + this.isExpanded = true; + + // Style footer + this.styleFooter(); + + console.log(`${this.config.title || 'Unknown'} control expanded`); + }, null, 'expand'); + }, + + collapse: function() { + this.safeOperation(() => { + if (!this.isExpanded) return; + + const content = this.safeQuerySelector('.control-content', this.element); + const closeBtn = this.safeQuerySelector('.control-close', this.element); + const resizeHandle = this.safeQuerySelector('.resize-handle', this.element); + + if (content) { + content.style.display = 'none'; + content.style.width = ''; + content.style.height = ''; + } + if (closeBtn) { + closeBtn.style.display = 'none'; + } + if (resizeHandle) { + resizeHandle.style.display = 'none'; + } + this.isExpanded = false; + + console.log(`${this.config.title || 'Unknown'} control collapsed`); + }, null, 'collapse'); + }, + + toggle: function() { + this.safeOperation(() => { + if (this.isExpanded) { + this.collapse(); + } else { + if (this.buildContent) { + this.buildContent(); + } else { + this.expand(); + } + } + }, null, 'toggle'); + }, + + styleFooter: function() { + this.safeOperation(() => { + const footer = this.safeQuerySelector('.control-footer', this.element); + if (!footer) return; + + const footerText = this.getFooter(); + + if (footerText && footerText.trim()) { + // Sanitize footer text + const safeText = footerText.replace(/[<>"'&]/g, ''); + footer.textContent = safeText; + footer.style.cssText = ` + display: block; padding: 0.5rem; font-size: 0.7rem; + color: #6c757d; text-align: center; font-style: italic; + background: #f8f9fa; border-top: 1px solid #e9ecef; + border-radius: 0 0 6px 6px; + `; + } else { + footer.style.display = 'none'; + } + }, null, 'styleFooter'); + }, + + // Virtual method - should be overridden by specific controls + buildContent: function() { + this.safeOperation(() => { + console.log(`${this.config.title || 'Unknown'} control - buildContent should be overridden`); + this.expand(); + }, () => { + console.error('Failed to build content, expanding basic control'); + this.expand(); + }, 'buildContent'); + } +}; + +// Export for use in other modules +window.Control = Control; \ No newline at end of file diff --git a/markitect/static/js/controls/debug-control.js b/markitect/static/js/controls/debug-control.js new file mode 100644 index 00000000..33abadbf --- /dev/null +++ b/markitect/static/js/controls/debug-control.js @@ -0,0 +1,63 @@ +/** + * Debug Control - Displays debug information and system messages + * Implements the Robustness Principle with Fail Fast mode support + */ + +class DebugControl { + constructor() { + this.control = Object.create(Control); + this.control.config = { + icon: '๐Ÿชฒ', + title: 'Debug', + className: 'debug-control', + defaultContent: 'Click to view debug information', + ariaLabel: 'Debug Control', + position: 'w' + }; + + // Bind methods to control + this.control.buildContent = () => { + const content = this.control.element.querySelector('.control-content'); + const messages = window.MarkitectDebugSystem ? + window.MarkitectDebugSystem.getMessages() : []; + + content.innerHTML = ` +
+

Debug Messages

+
+ ${messages.length > 0 ? + messages.slice(-10).map(msg => + `
+ [${msg.category}] ${msg.component}: ${msg.message} +
${msg.displayTime}
+
` + ).join('') : + '

No debug messages yet

' + } +
+ +
+ `; + this.control.isExpanded = true; + }; + + this.control.toggle = () => { + if (this.control.isExpanded) { + this.control.element.querySelector('.control-content').style.display = 'none'; + this.control.isExpanded = false; + } else { + this.control.buildContent(); + this.control.element.querySelector('.control-content').style.display = 'block'; + } + }; + } + + createControl() { + return this.control.createControl(); + } +} + +window.DebugControl = DebugControl; \ No newline at end of file diff --git a/markitect/static/js/controls/edit-control.js b/markitect/static/js/controls/edit-control.js new file mode 100644 index 00000000..b87ebc70 --- /dev/null +++ b/markitect/static/js/controls/edit-control.js @@ -0,0 +1,70 @@ +/** + * Edit Control - Document editing tools and actions + * Implements the Robustness Principle with Fail Fast mode support + */ + +class EditControl { + constructor() { + this.control = Object.create(Control); + this.control.config = { + icon: 'โœ๏ธ', + title: 'Edit', + className: 'edit-control', + defaultContent: 'Document editing tools', + ariaLabel: 'Edit Control', + position: 'e' + }; + + // Bind methods to control + this.control.buildContent = () => { + const content = this.control.element.querySelector('.control-content'); + + content.innerHTML = ` +
+

Edit Tools

+ +
+ + + + + +
+ +
+ Page Info:
+ Title: ${document.title}
+ Words: ~${(document.body.textContent || '').split(/\\s+/).filter(w => w.length > 0).length}
+ Modified: ${document.lastModified} +
+
+ `; + this.control.isExpanded = true; + }; + + this.control.toggle = () => { + if (this.control.isExpanded) { + this.control.element.querySelector('.control-content').style.display = 'none'; + this.control.isExpanded = false; + } else { + this.control.buildContent(); + this.control.element.querySelector('.control-content').style.display = 'block'; + } + }; + } + + createControl() { + return this.control.createControl(); + } +} + +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 new file mode 100644 index 00000000..82232bc5 --- /dev/null +++ b/markitect/static/js/controls/status-control.js @@ -0,0 +1,616 @@ +/** + * Status Control - Document statistics and change tracking + */ +class StatusControl { + constructor() { + this.control = Object.create(Control); + + // Configure for status functionality + this.control.config = { + icon: '๐Ÿ“Š', + title: 'Status', + className: 'status-control', + defaultContent: 'Document statistics and changes', + ariaLabel: 'Status Control', + position: 'e', // East positioning + footer: `Updated ${new Date().toLocaleTimeString()}` + }; + + // Initialize change tracking + this.control.changeTracking = { + headings: new Set(), + sections: new Set(), + images: new Set(), + tables: new Set(), + lastScanTime: null, + initialCounts: { + headings: 0, + sections: 0, + images: 0, + tables: 0, + lines: 0, + words: 0, + characters: 0 + } + }; + + this.bindMethods(); + } + + bindMethods() { + // Bind utility functions + this.control.safeTextExtraction = this.safeTextExtraction.bind(this); + this.control.sanitizeText = this.sanitizeText.bind(this); + this.control.validateElement = this.validateElement.bind(this); + this.control.safeStatsOperation = this.safeStatsOperation.bind(this); + + // Bind existing methods + this.control.calculateStats = this.calculateStats.bind(this); + this.control.isContentSection = this.isContentSection.bind(this); + this.control.isContentTable = this.isContentTable.bind(this); + this.control.updateChangeTracking = this.updateChangeTracking.bind(this); + this.control.buildContent = this.buildContent.bind(this); + this.control.refreshStats = this.refreshStats.bind(this); + this.control.resetChangeTracking = this.resetChangeTracking.bind(this); + this.control.setupAutoRefresh = this.setupAutoRefresh.bind(this); + + // Override collapse to clean up intervals + const originalCollapse = this.control.collapse; + this.control.collapse = () => { + if (this.control.autoRefreshInterval) { + clearInterval(this.control.autoRefreshInterval); + this.control.autoRefreshInterval = null; + } + originalCollapse.call(this.control); + }; + } + + // Utility functions for safe operations + safeTextExtraction(element) { + if (!this.validateElement(element)) { + return ''; + } + + try { + const text = element.textContent || element.innerText || ''; + return this.sanitizeText(text.trim()); + } catch (error) { + console.warn('Text extraction failed:', error); + return ''; + } + } + + sanitizeText(text) { + if (typeof text !== 'string') { + return ''; + } + + // Remove potentially harmful characters and limit length + const maxLength = 100000; // 100KB text limit + const sanitized = text + .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars + .slice(0, maxLength); // Limit length + + return sanitized; + } + + validateElement(element) { + return element && + element.nodeType === Node.ELEMENT_NODE && + element.isConnected && + !element.closest('.control-panel'); // Avoid control elements + } + + safeStatsOperation(operation, fallback = 0, context = 'stats') { + try { + const result = operation(); + // Validate numeric results + return typeof result === 'number' && isFinite(result) ? result : fallback; + } catch (error) { + console.warn(`Stats operation failed in ${context}:`, error); + if (window.MarkitectDebugSystem) { + window.MarkitectDebugSystem.addMessage( + `Stats operation failed: ${error.message}`, + 'WARNING', + 'StatusControl', + { context, eventType: 'ERROR' } + ); + } + return fallback; + } + } + + calculateStats() { + const stats = { + headings: { total: 0, changed: 0 }, + sections: { total: 0, changed: 0 }, + images: { total: 0, changed: 0 }, + tables: { total: 0, changed: 0 }, + document: { lines: 0, words: 0, characters: 0 }, + sections_detail: { lines: 0, words: 0, characters: 0 }, + tables_detail: { lines: 0, words: 0, characters: 0 } + }; + + return this.safeStatsOperation(() => { + // Count headings (h1-h6, excluding control titles) + const headings = this.control.safeQuerySelectorAll('h1, h2, h3, h4, h5, h6'); + const maxElements = 10000; // Limit processing to prevent DoS + + headings.slice(0, maxElements).forEach(heading => { + if (!this.validateElement(heading)) return; + + const text = this.safeTextExtraction(heading).toLowerCase(); + // Skip control headings with enhanced filtering + const controlKeywords = ['contents', 'debug', 'status', 'control', 'menu', 'toolbar']; + const isControlHeading = controlKeywords.some(keyword => text.includes(keyword)); + + if (text.length > 0 && !isControlHeading) { + stats.headings.total++; + const fullText = this.safeTextExtraction(heading); + if (this.control.changeTracking.headings.has(fullText)) { + stats.headings.changed++; + } + } + }); + + // Count sections (content blocks excluding headings and table cells) + const sections = this.control.safeQuerySelectorAll('p, blockquote, pre, li, div'); + sections.slice(0, maxElements).forEach(section => { + if (this.isContentSection(section)) { + stats.sections.total++; + const sectionText = this.safeTextExtraction(section); + if (sectionText.length > 0) { + const lines = this.safeStatsOperation(() => sectionText.split('\n').length, 0, 'countLines'); + const words = this.safeStatsOperation(() => + sectionText.split(/\s+/).filter(w => w.length > 0).length, 0, 'countWords'); + const characters = Math.min(sectionText.length, 1000000); // Cap at 1MB + + stats.sections_detail.lines += lines; + stats.sections_detail.words += words; + stats.sections_detail.characters += characters; + + if (this.control.changeTracking.sections.has(sectionText)) { + stats.sections.changed++; + } + } + } + }); + + // Count tables as separate entities + const tables = this.control.safeQuerySelectorAll('table'); + tables.slice(0, maxElements).forEach(table => { + if (this.isContentTable(table)) { + stats.tables.total++; + const tableText = this.safeTextExtraction(table); + if (tableText.length > 0) { + const lines = this.safeStatsOperation(() => tableText.split('\n').length, 0, 'countTableLines'); + const words = this.safeStatsOperation(() => + tableText.split(/\s+/).filter(w => w.length > 0).length, 0, 'countTableWords'); + const characters = Math.min(tableText.length, 1000000); // Cap at 1MB + + stats.tables_detail.lines += lines; + stats.tables_detail.words += words; + stats.tables_detail.characters += characters; + + // Generate safer table identifier + const tableId = this.sanitizeText(table.id || + table.outerHTML.substring(0, 100).replace(/[<>"'&]/g, '')); + if (this.control.changeTracking.tables.has(tableId)) { + stats.tables.changed++; + } + } + } + }); + + // Count images with validation + const images = this.control.safeQuerySelectorAll('img'); + images.slice(0, maxElements).forEach(img => { + if (this.validateElement(img)) { + stats.images.total++; + // Safely extract and validate image source + const imgSrc = this.sanitizeText(img.src || img.getAttribute('src') || ''); + if (imgSrc && this.control.changeTracking.images.has(imgSrc)) { + stats.images.changed++; + } + } + }); + + // Calculate total document stats with protection + const bodyText = this.safeTextExtraction(document.body); + if (bodyText) { + const cleanText = bodyText.replace(/\s+/g, ' '); + stats.document.lines = this.safeStatsOperation(() => + bodyText.split('\n').length, 0, 'countDocLines'); + stats.document.words = this.safeStatsOperation(() => + cleanText.split(/\s+/).filter(w => w.length > 0).length, 0, 'countDocWords'); + stats.document.characters = Math.min(cleanText.length, 10000000); // Cap at 10MB + } + + return stats; + }, stats, 'calculateStats'); + } + + isContentSection(element) { + return this.safeStatsOperation(() => { + if (!this.validateElement(element)) { + return false; + } + + // Enhanced control detection with timeout protection + let current = element; + let depth = 0; + const maxDepth = 50; // Prevent infinite loops + + while (current && current !== document.body && depth < maxDepth) { + if (current.classList && ( + current.classList.contains('control-panel') || + current.classList.contains('control-content') || + current.classList.contains('control-header') || + current.className.includes('control') || + current.id?.includes('control') + )) { + return false; + } + current = current.parentElement; + depth++; + } + + // Skip if element is inside a table (tables are counted separately) + if (element.closest && element.closest('table')) { + return false; + } + + // Skip if element has no meaningful text content + const text = this.safeTextExtraction(element); + return text.length > 0 && text.length < 50000; // Reasonable size limit + }, false, 'isContentSection'); + } + + isContentTable(table) { + return this.safeStatsOperation(() => { + if (!this.validateElement(table) || table.tagName !== 'TABLE') { + return false; + } + + // Enhanced control detection with depth limiting + let current = table; + let depth = 0; + const maxDepth = 50; + + while (current && current !== document.body && depth < maxDepth) { + if (current.classList && ( + current.classList.contains('control-panel') || + current.classList.contains('control-content') || + current.classList.contains('control-header') || + current.className.includes('control') || + current.id?.includes('control') + )) { + return false; + } + current = current.parentElement; + depth++; + } + + // Check if table has meaningful content with limits + const text = this.safeTextExtraction(table); + return text.length > 0 && text.length < 100000; // Reasonable table size limit + }, false, 'isContentTable'); + } + + updateChangeTracking() { + const now = Date.now(); + + // Headings + const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); + headings.forEach(heading => { + const text = heading.textContent.trim(); + if (text && !text.toLowerCase().includes('control')) { + const changed = heading.dataset.lastModified && + (now - parseInt(heading.dataset.lastModified)) < 300000; // 5 minutes + if (changed) { + this.control.changeTracking.headings.add(text); + } + } + }); + + // Sections + const sections = document.querySelectorAll('p, blockquote, pre, li, div'); + sections.forEach(section => { + if (this.isContentSection(section)) { + const text = section.textContent.trim(); + if (text.length > 0) { + const changed = section.dataset.lastModified && + (now - parseInt(section.dataset.lastModified)) < 300000; // 5 minutes + if (changed) { + this.control.changeTracking.sections.add(text); + } + } + } + }); + + // Tables + const tables = document.querySelectorAll('table'); + tables.forEach(table => { + if (this.isContentTable(table)) { + const tableId = table.id || table.outerHTML.substring(0, 100); + const changed = table.dataset.lastModified && + (now - parseInt(table.dataset.lastModified)) < 300000; // 5 minutes + if (changed) { + this.control.changeTracking.tables.add(tableId); + } + } + }); + + // Images + const images = document.querySelectorAll('img'); + images.forEach(img => { + const src = img.src || img.getAttribute('src') || ''; + const changed = img.dataset.lastModified && + (now - parseInt(img.dataset.lastModified)) < 300000; // 5 minutes + if (changed && src) { + this.control.changeTracking.images.add(src); + } + }); + + this.control.changeTracking.lastScanTime = now; + } + + buildContent() { + this.control.safeOperation(() => { + console.log("๐Ÿ“Š Building status control content..."); + + const content = this.control.safeQuerySelector('.control-content', this.control.element); + if (!content) { + console.error("๐Ÿ“Š Status control content element not found"); + return; + } + + // Update tracking and calculate stats with timeout protection + const timeout = setTimeout(() => { + console.warn('Status content build operation timed out'); + }, 10000); // 10 second timeout + + this.updateChangeTracking(); + const stats = this.calculateStats(); + + clearTimeout(timeout); + + // Sanitize numeric values to prevent injection + const safeStats = { + document: { + lines: Math.max(0, Math.floor(stats.document.lines || 0)), + words: Math.max(0, Math.floor(stats.document.words || 0)), + characters: Math.max(0, Math.floor(stats.document.characters || 0)) + }, + headings: { + total: Math.max(0, Math.floor(stats.headings.total || 0)), + changed: Math.max(0, Math.floor(stats.headings.changed || 0)) + }, + sections: { + total: Math.max(0, Math.floor(stats.sections.total || 0)), + changed: Math.max(0, Math.floor(stats.sections.changed || 0)) + }, + sections_detail: { + lines: Math.max(0, Math.floor(stats.sections_detail.lines || 0)), + words: Math.max(0, Math.floor(stats.sections_detail.words || 0)), + characters: Math.max(0, Math.floor(stats.sections_detail.characters || 0)) + }, + tables: { + total: Math.max(0, Math.floor(stats.tables.total || 0)), + changed: Math.max(0, Math.floor(stats.tables.changed || 0)) + }, + tables_detail: { + lines: Math.max(0, Math.floor(stats.tables_detail.lines || 0)), + words: Math.max(0, Math.floor(stats.tables_detail.words || 0)), + characters: Math.max(0, Math.floor(stats.tables_detail.characters || 0)) + }, + images: { + total: Math.max(0, Math.floor(stats.images.total || 0)), + changed: Math.max(0, Math.floor(stats.images.changed || 0)) + } + }; + + // Use safe stats for display with proper escaping + content.innerHTML = ` +
+ +
+
๐Ÿ“„ Document
+
+ Lines: ${safeStats.document.lines.toLocaleString()} | Words: ${safeStats.document.words.toLocaleString()} | Chars: ${safeStats.document.characters.toLocaleString()} +
+
+ + +
+
+ ๐Ÿ“‹ Headings: ${safeStats.headings.total} + ${safeStats.headings.changed > 0 ? ` (+${safeStats.headings.changed})` : ''} +
+
+ + +
+
+ ๐Ÿ“„ Sections: ${safeStats.sections.total} + ${safeStats.sections.changed > 0 ? ` (+${safeStats.sections.changed})` : ''} +
+
+ Lines: ${safeStats.sections_detail.lines.toLocaleString()} | Words: ${safeStats.sections_detail.words.toLocaleString()} | Chars: ${safeStats.sections_detail.characters.toLocaleString()} +
+
+ + +
+
+ ๐Ÿ—‚๏ธ Tables: ${safeStats.tables.total} + ${safeStats.tables.changed > 0 ? ` (+${safeStats.tables.changed})` : ''} +
+
+ Lines: ${safeStats.tables_detail.lines.toLocaleString()} | Words: ${safeStats.tables_detail.words.toLocaleString()} | Chars: ${safeStats.tables_detail.characters.toLocaleString()} +
+
+ + +
+
+ ๐Ÿ–ผ๏ธ Images: ${safeStats.images.total} + ${safeStats.images.changed > 0 ? ` (+${safeStats.images.changed})` : ''} +
+
+ + +
+ + +
+ +
+ `; + + // Add safer event listeners instead of inline onclick + const refreshBtn = content.querySelector('#status-refresh-btn'); + const resetBtn = content.querySelector('#status-reset-btn'); + + if (refreshBtn) { + refreshBtn.addEventListener('click', () => { + this.control.safeOperation(() => { + if (window.statusControl && window.statusControl.refreshStats) { + window.statusControl.refreshStats(); + } + }, null, 'refreshButton'); + }); + } + + if (resetBtn) { + resetBtn.addEventListener('click', () => { + this.control.safeOperation(() => { + if (window.statusControl && window.statusControl.resetChangeTracking) { + window.statusControl.resetChangeTracking(); + } + }, null, 'resetButton'); + }); + } + + console.log("๐Ÿ“Š Status control content built successfully"); + + // Set up auto-refresh + this.setupAutoRefresh(); + + // Show panel and expand + this.control.expand(); + + }, () => { + console.error("๐Ÿ“Š Error in buildContent: Failed to build status control content"); + const content = this.control.safeQuerySelector('.control-content', this.control.element); + if (content) { + content.innerHTML = '
Status loading failed
'; + } + }, 'buildContent'); + } + + refreshStats() { + if (this.control.isExpanded) { + this.updateChangeTracking(); + // Update footer timestamp + this.control.config.footer = `Updated ${new Date().toLocaleTimeString()}`; + this.control.styleFooter(); + + const content = this.control.element.querySelector('.control-content'); + if (content) { + const stats = this.calculateStats(); + // Update the display without rebuilding entire content + this.buildContent(); + } + } + } + + resetChangeTracking() { + if (confirm('Reset all document changes? This will revert all sections to their original state.')) { + console.log('๐Ÿ“Š Resetting document changes...'); + + // Reset using available infrastructure + if (window.sectionManager && window.domRenderer) { + // Use the proper document management infrastructure + try { + // Hide any open editors + window.domRenderer.hideCurrentEditor(); + + // Reset all sections to original state + const allSections = Array.from(window.sectionManager.sections.values()); + allSections.forEach(section => { + section.resetToOriginal(); + }); + + // Re-render all sections + window.domRenderer.renderAllSections(allSections); + + console.log('๐Ÿ“Š Document reset successful'); + + // Add to debug system + if (window.MarkitectDebugSystem) { + window.MarkitectDebugSystem.addMessage( + `Document reset completed - ${allSections.length} sections restored`, + 'SUCCESS', + 'StatusControl', + { eventType: 'SYSTEM' } + ); + } + + } catch (error) { + console.error('๐Ÿ“Š Document reset failed:', error); + + if (window.MarkitectDebugSystem) { + window.MarkitectDebugSystem.addMessage( + `Document reset failed: ${error.message}`, + 'ERROR', + 'StatusControl', + { eventType: 'SYSTEM' } + ); + } + } + } else { + // Fallback to page reload if infrastructure not available + console.log('๐Ÿ“Š Document management infrastructure not available, using page reload'); + window.location.reload(); + } + + // Clear our own change tracking + this.control.changeTracking.headings.clear(); + this.control.changeTracking.sections.clear(); + this.control.changeTracking.images.clear(); + this.control.changeTracking.tables.clear(); + this.control.changeTracking.lastScanTime = Date.now(); + + // Refresh our display + this.refreshStats(); + } + } + + setupAutoRefresh() { + if (this.control.autoRefreshInterval) { + clearInterval(this.control.autoRefreshInterval); + } + + this.control.autoRefreshInterval = setInterval(() => { + if (this.control.isExpanded) { + this.refreshStats(); + } + }, 30000); // 30 seconds + } + + createControl() { + return this.control.createControl(); + } +} + +// Export for global access +window.StatusControl = StatusControl; \ No newline at end of file diff --git a/markitect/static/js/core/debug-system.js b/markitect/static/js/core/debug-system.js new file mode 100644 index 00000000..e9776da6 --- /dev/null +++ b/markitect/static/js/core/debug-system.js @@ -0,0 +1,290 @@ +/** + * Independent Debug System for Markitect + * Uses IndexedDB for persistence and provides selection-based filtering + */ +class MarkitectDebugSystem { + constructor() { + this.db = null; + this.messages = []; + this.maxMessages = 1000; + this.isEnabled = true; + this.subscribers = []; + + // Selection and filtering system + this.selectionCriteria = { + includeDocumentEvents: true, + includeSystemEvents: false, + includeControlEvents: true, + includeEditingEvents: true, + includeNavigationEvents: false, + includedHeadings: new Set(), // Track which document headings to monitor + excludedSources: new Set(['ContentsControl', 'DocumentNavigator']) + }; + + this.init(); + } + + // Initialize IndexedDB for persistence + async init() { + return new Promise((resolve, reject) => { + const request = indexedDB.open('MarkitectDebugDB', 1); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + this.db = request.result; + this.loadMessages().then(resolve); + }; + + request.onupgradeneeded = (e) => { + const db = e.target.result; + if (!db.objectStoreNames.contains('messages')) { + const store = db.createObjectStore('messages', { keyPath: 'id', autoIncrement: true }); + store.createIndex('timestamp', 'timestamp', { unique: false }); + store.createIndex('category', 'category', { unique: false }); + } + }; + }); + } + + // Add a debug message with selection filtering + async addMessage(message, category = 'INFO', source = 'System', context = {}) { + // Check if this message should be included based on selection criteria + if (!this.shouldIncludeMessage(message, category, source, context)) { + return null; + } + + const messageObj = { + timestamp: new Date().toISOString(), + message: String(message), + category: category.toUpperCase(), + source: String(source), + context: context || {}, + id: null // Will be set by IndexedDB + }; + + // Store in IndexedDB if available + if (this.db) { + try { + await this.saveMessage(messageObj); + } catch (error) { + console.warn('Failed to save debug message to IndexedDB:', error); + } + } + + // Store in memory + this.messages.unshift(messageObj); + + // Limit memory storage + if (this.messages.length > this.maxMessages) { + this.messages = this.messages.slice(0, this.maxMessages); + } + + // Notify subscribers + this.notifySubscribers(messageObj); + + // Console output for development + const consoleMethod = category.toLowerCase() === 'error' ? 'error' : + category.toLowerCase() === 'warning' ? 'warn' : 'log'; + console[consoleMethod](`[${source}] ${message}`, context); + + return messageObj; + } + + // Selection filtering logic + shouldIncludeMessage(message, category, source, context) { + if (!this.isEnabled) return false; + + const eventType = context.eventType || 'UNKNOWN'; + const criteria = this.selectionCriteria; + + // Check event type filters + switch (eventType.toUpperCase()) { + case 'DOCUMENT': + if (!criteria.includeDocumentEvents) return false; + break; + case 'SYSTEM': + if (!criteria.includeSystemEvents) return false; + break; + case 'CONTROL': + if (!criteria.includeControlEvents) return false; + break; + case 'EDITING': + if (!criteria.includeEditingEvents) return false; + break; + case 'NAVIGATION': + if (!criteria.includeNavigationEvents) return false; + break; + } + + // Check excluded sources + if (criteria.excludedSources.has(source)) { + return false; + } + + // Check heading-specific filtering + if (context.sectionId && criteria.includedHeadings.size > 0) { + const sectionElement = document.getElementById(context.sectionId); + if (sectionElement) { + const heading = sectionElement.querySelector('h1, h2, h3, h4, h5, h6'); + if (heading && !criteria.includedHeadings.has(heading.textContent.trim())) { + return false; + } + } + } + + return true; + } + + // Save message to IndexedDB + async saveMessage(messageObj) { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(['messages'], 'readwrite'); + const store = transaction.objectStore('messages'); + const request = store.add(messageObj); + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + // Load messages from IndexedDB + async loadMessages() { + if (!this.db) return []; + + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(['messages'], 'readonly'); + const store = transaction.objectStore('messages'); + const request = store.getAll(); + + request.onsuccess = () => { + this.messages = request.result.reverse(); // Most recent first + resolve(this.messages); + }; + request.onerror = () => reject(request.error); + }); + } + + // Clear all messages + async clearMessages() { + this.messages = []; + + if (this.db) { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(['messages'], 'readwrite'); + const store = transaction.objectStore('messages'); + const request = store.clear(); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + } + + // Get filtered messages + getMessages(filter = {}) { + let filteredMessages = [...this.messages]; + + if (filter.category) { + filteredMessages = filteredMessages.filter(msg => + msg.category.toLowerCase() === filter.category.toLowerCase() + ); + } + + if (filter.source) { + filteredMessages = filteredMessages.filter(msg => + msg.source.toLowerCase().includes(filter.source.toLowerCase()) + ); + } + + if (filter.since) { + const sinceDate = new Date(filter.since); + filteredMessages = filteredMessages.filter(msg => + new Date(msg.timestamp) >= sinceDate + ); + } + + if (filter.limit) { + filteredMessages = filteredMessages.slice(0, filter.limit); + } + + return filteredMessages; + } + + // Update selection criteria + updateSelectionCriteria(updates) { + Object.assign(this.selectionCriteria, updates); + this.notifySubscribers({ type: 'criteria-updated', criteria: this.selectionCriteria }); + } + + // Add heading to monitoring + addHeadingToMonitoring(headingText) { + this.selectionCriteria.includedHeadings.add(headingText); + } + + // Remove heading from monitoring + removeHeadingFromMonitoring(headingText) { + this.selectionCriteria.includedHeadings.delete(headingText); + } + + // Scan document for available headings + scanDocumentHeadings() { + const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); + return Array.from(headings) + .map(h => h.textContent.trim()) + .filter(text => text.length > 0 && !text.toLowerCase().includes('control')); + } + + // Subscribe to debug messages + subscribe(callback) { + this.subscribers.push(callback); + return () => { + const index = this.subscribers.indexOf(callback); + if (index > -1) { + this.subscribers.splice(index, 1); + } + }; + } + + // Notify all subscribers + notifySubscribers(message) { + this.subscribers.forEach(callback => { + try { + callback(message); + } catch (error) { + console.error('Debug subscriber error:', error); + } + }); + } + + // Toggle debug system + setEnabled(enabled) { + this.isEnabled = enabled; + this.addMessage( + `Debug system ${enabled ? 'enabled' : 'disabled'}`, + 'INFO', + 'DebugSystem', + { eventType: 'SYSTEM' } + ); + } + + // Get statistics + getStats() { + const stats = { + total: this.messages.length, + byCategory: {}, + bySource: {}, + enabled: this.isEnabled, + criteria: { ...this.selectionCriteria } + }; + + this.messages.forEach(msg => { + stats.byCategory[msg.category] = (stats.byCategory[msg.category] || 0) + 1; + stats.bySource[msg.source] = (stats.bySource[msg.source] || 0) + 1; + }); + + return stats; + } +} + +// Initialize and expose globally +window.MarkitectDebugSystem = new MarkitectDebugSystem(); \ No newline at end of file diff --git a/markitect/static/js/main.js b/markitect/static/js/main.js new file mode 100644 index 00000000..40f8c482 --- /dev/null +++ b/markitect/static/js/main.js @@ -0,0 +1,201 @@ +/** + * Main Markitect JavaScript Entry Point + * Initializes all controls and systems when document is ready + * Implements graceful degradation for missing dependencies + * Supports Fail Fast strict mode for development + */ + +// Development mode detection +const MARKITECT_STRICT_MODE = ( + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + window.location.search.includes('strict=true') || + window.markitectStrictMode === true +); + +// Utility functions for safe initialization +const MarkitectMain = { + // Safe dependency checking with timeout + checkDependencies: function() { + const dependencies = { + debugSystem: !!window.MarkitectDebugSystem, + control: !!window.Control, + statusControl: !!window.StatusControl, + debugControl: !!window.DebugControl, + contentsControl: !!window.ContentsControl, + editControl: !!window.EditControl + }; + + console.log('๐Ÿ“‹ Dependency check results:', dependencies); + return dependencies; + }, + + // Safe logging that works even without debug system + safeLog: function(message, level = 'INFO', component = 'Main', data = {}) { + console.log(`[${level}] ${component}: ${message}`); + + // In strict mode, throw on errors for immediate development feedback + if (MARKITECT_STRICT_MODE && level === 'ERROR') { + console.error(`๐Ÿšจ STRICT MODE: Throwing error for immediate diagnosis`); + throw new Error(`${component}: ${message}`); + } + + // Try to use debug system if available + if (window.MarkitectDebugSystem && window.MarkitectDebugSystem.addMessage) { + try { + window.MarkitectDebugSystem.addMessage(message, level, component, { ...data, eventType: 'SYSTEM' }); + } catch (error) { + console.warn('Debug system logging failed:', error); + if (MARKITECT_STRICT_MODE) { + throw error; // Fail fast in development + } + } + } + }, + + // Safe control initialization with fallbacks + initializeControl: function(controlClass, controlName, icon = '๐Ÿ”ง') { + const timeout = setTimeout(() => { + const message = `${controlName} initialization timed out`; + console.warn(message); + if (MARKITECT_STRICT_MODE) { + throw new Error(message); // Fail fast in development + } + }, 5000); + + try { + if (!controlClass) { + const message = `${controlName} class not available, skipping`; + this.safeLog(message, MARKITECT_STRICT_MODE ? 'ERROR' : 'WARNING'); + clearTimeout(timeout); + return null; + } + + const controlInstance = new controlClass(); + if (!controlInstance || typeof controlInstance.createControl !== 'function') { + throw new Error(`Invalid ${controlName} instance`); + } + + const element = controlInstance.createControl(); + if (!element) { + throw new Error(`${controlName} failed to create element`); + } + + clearTimeout(timeout); + this.safeLog(`${controlName} initialized successfully`, 'SUCCESS'); + return controlInstance; + + } catch (error) { + clearTimeout(timeout); + this.safeLog(`${controlName} initialization failed: ${error.message}`, 'ERROR'); + + // Create minimal fallback control if core Control class exists + if (window.Control && controlName === 'StatusControl') { + return this.createFallbackControl(controlName, icon); + } + + return null; + } + }, + + // Create minimal fallback control for essential controls + createFallbackControl: function(name, icon) { + try { + const fallback = Object.create(window.Control); + fallback.config = { + icon: icon, + title: `${name} (Fallback)`, + className: `${name.toLowerCase()}-fallback`, + defaultContent: `${name} is running in fallback mode due to initialization issues.`, + ariaLabel: `${name} Fallback Control`, + position: 'e' + }; + + const element = fallback.createControl(); + if (element) { + this.safeLog(`${name} fallback control created`, 'INFO'); + return { control: fallback }; + } + } catch (error) { + this.safeLog(`Fallback control creation failed: ${error.message}`, 'ERROR'); + } + return null; + }, + + // Main initialization with comprehensive error handling + initialize: function() { + this.safeLog('๐Ÿš€ Initializing Markitect controls and systems...', 'INFO'); + + // Check dependencies first + const deps = this.checkDependencies(); + + if (!deps.control) { + this.safeLog('โŒ Core Control system not available, cannot initialize UI controls', 'ERROR'); + return; + } + + const initializedControls = {}; + let successCount = 0; + let totalAttempts = 0; + + // Initialize controls with graceful degradation + const controlsToInit = [ + { class: window.StatusControl, name: 'StatusControl', key: 'statusControl', icon: '๐Ÿ“Š', essential: true }, + { class: window.DebugControl, name: 'DebugControl', key: 'debugControl', icon: '๐Ÿชฒ', essential: false }, + { class: window.ContentsControl, name: 'ContentsControl', key: 'contentsControl', icon: 'โ˜ฐ', essential: false }, + { class: window.EditControl, name: 'EditControl', key: 'editControl', icon: 'โœ๏ธ', essential: false } + ]; + + controlsToInit.forEach(({ class: controlClass, name, key, icon, essential }) => { + totalAttempts++; + const instance = this.initializeControl(controlClass, name, icon); + + if (instance) { + initializedControls[key] = instance.control || instance; + window[key] = initializedControls[key]; + successCount++; + } else if (essential) { + this.safeLog(`Essential control ${name} failed to initialize`, 'ERROR'); + } + }); + + // Report initialization results + const successRate = Math.round((successCount / totalAttempts) * 100); + if (successCount === totalAttempts) { + this.safeLog('โœ… All controls initialized successfully', 'SUCCESS'); + } else if (successCount > 0) { + this.safeLog(`โš ๏ธ Partial initialization: ${successCount}/${totalAttempts} controls (${successRate}%) initialized`, 'WARNING'); + } else { + this.safeLog('โŒ No controls could be initialized', 'ERROR'); + } + + // Set up global error handlers for runtime protection + this.setupErrorHandlers(); + + this.safeLog(`โœ… Markitect initialization complete (${successCount}/${totalAttempts} controls active)`, 'INFO'); + }, + + // Set up global error handlers + setupErrorHandlers: function() { + // Catch unhandled errors + window.addEventListener('error', (event) => { + this.safeLog(`Unhandled error: ${event.message} at ${event.filename}:${event.lineno}`, 'ERROR'); + }); + + // Catch unhandled promise rejections + window.addEventListener('unhandledrejection', (event) => { + this.safeLog(`Unhandled promise rejection: ${event.reason}`, 'ERROR'); + event.preventDefault(); // Prevent console spam + }); + } +}; + +// Initialize when DOM is ready with additional safety +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + setTimeout(() => MarkitectMain.initialize(), 100); // Brief delay for dependencies + }); +} else { + // DOM already loaded + setTimeout(() => MarkitectMain.initialize(), 100); +} \ No newline at end of file diff --git a/markitect/static/js/tests/test.md b/markitect/static/js/tests/test.md new file mode 100644 index 00000000..239c58bf --- /dev/null +++ b/markitect/static/js/tests/test.md @@ -0,0 +1,6 @@ +# Test Document + +This is a test document to check if UI controls appear in edit mode. + +## Section 1 +Some content here. diff --git a/markitect/static/js/tests/test_edit.html b/markitect/static/js/tests/test_edit.html new file mode 100644 index 00000000..8076d97e --- /dev/null +++ b/markitect/static/js/tests/test_edit.html @@ -0,0 +1,149 @@ + + + + + + + Test Document + + + + + + + + + + + +
+

Test Document

+

This is a test document to check if UI controls appear in edit mode.

+

Section 1

+

Some content here.

+
+

-- html from markdown by MarkiTect on 2025-11-11 23:42:23 by worsch

+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/markitect/templates/document.html b/markitect/templates/document.html new file mode 100644 index 00000000..fe2718db --- /dev/null +++ b/markitect/templates/document.html @@ -0,0 +1,147 @@ + + + + + + + {title} + + + + + + + + + + + +
+ {content} +
+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_complete.html b/test_complete.html new file mode 100644 index 00000000..a7431280 --- /dev/null +++ b/test_complete.html @@ -0,0 +1,191 @@ + + + + + + + Complete UI Test + + + + + + + + + + + +
+

Complete UI Test

+

This document tests the complete UI control system with all controls.

+

Content Section

+

This section has various content types to test the controls:

+

Lists

+ +

Code Example

+
console.log('Hello World');
+
+ +

Table

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureStatus
Status Controlโœ“ Working
Debug Controlโœ“ Working
Contents Controlโœ“ Working
Edit Controlโœ“ Working
+

Final Section

+

More content to test the table of contents functionality.

+
+

-- html from markdown by MarkiTect on 2025-11-11 23:46:11 by worsch

+
+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_guardrail_js.html b/test_guardrail_js.html new file mode 100644 index 00000000..357851d9 --- /dev/null +++ b/test_guardrail_js.html @@ -0,0 +1,145 @@ + + + + + + + Guardrail Principle Test - JavaScript Controls + + + +
+

Guardrail Principle Test Page

+ +
+

Test Section 1

+

This is a test paragraph to verify that the status control can properly count and analyze document content.

+

Another paragraph with some formatted text and emphasis.

+
+ +
+

Test Subsection with Table

+ + + + + + + + + + + + + + + + +
Column 1Column 2Column 3
Row 1, Cell 1Row 1, Cell 2Row 1, Cell 3
Row 2, Cell 1Row 2, Cell 2Row 2, Cell 3
+
+ +
+

Test with Images

+

Testing image counting (placeholder images):

+ Placeholder 1 + Placeholder 2 +
+ +
+

Test with Lists

+
    +
  • List item 1
  • +
  • List item 2 with inline code
  • +
  • List item 3
  • +
+ +
    +
  1. Ordered item 1
  2. +
  3. Ordered item 2
  4. +
+
+ +
+ This is a blockquote to test various content types that the status control should analyze. +
+ +

+// This is a code block
+function testFunction() {
+    return "Testing code block counting";
+}
+        
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_integration.html b/test_integration.html new file mode 100644 index 00000000..8603a47a --- /dev/null +++ b/test_integration.html @@ -0,0 +1,213 @@ + + + + + + + Integration Test Document + + + + + + + + + + + +
+

Integration Test Document

+

This document tests the JavaScript controls integration with the HTML output after the Guardrail Principle refactoring.

+

Recent Changes

+

Latest Commit (dbde13e)

+ +

Previous Commit (3839a67)

+ +

Test Content

+

Headers

+

This document contains various content types to test the status control functionality.

+

Subsection

+

Content in subsections should be properly counted.

+

Lists

+ +

Tables

+ + + + + + + + + + + + + + + + + + + + +
Column AColumn BColumn C
Row 1ARow 1BRow 1C
Row 2ARow 2BRow 2C
+

Code Block

+
def test_function():
+    return "This code block should be counted"
+
+ +

Blockquote

+
+

This is a blockquote that should be analyzed by the status control.

+
+

Expected Behavior

+

The JavaScript controls should: +1. Initialize successfully with proper error handling +2. Display accurate document statistics +3. Provide interactive drag/resize functionality +4. Work with the debug system integration +5. Handle errors gracefully per the Guardrail Principle

+

This test will verify that our external JavaScript files work correctly with the HTML template system.

+
+

-- html from markdown by MarkiTect on 2025-11-11 22:10:30 by worsch

+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_integration.md b/test_integration.md new file mode 100644 index 00000000..5c1916b7 --- /dev/null +++ b/test_integration.md @@ -0,0 +1,57 @@ +# Integration Test Document + +This document tests the JavaScript controls integration with the HTML output after the Guardrail Principle refactoring. + +## Recent Changes + +### Latest Commit (dbde13e) +- Enhanced control system with improved UI and debug functionality +- Added resize functionality to all controls with hover-only visibility +- Implemented small circle resize handles positioned in lower-right corner +- Added header-only toggle mode for space-efficient control management +- Created independent IndexedDB-based debug system with selection filtering + +### Previous Commit (3839a67) +- Fixed control positioning and drag behavior +- Updated compass positioning to be top-aligned instead of center-aligned +- Fixed drag offset calculation to maintain cursor position at icon +- Ensured expanded controls appear top-aligned with anchor position + +## Test Content + +### Headers +This document contains various content types to test the status control functionality. + +#### Subsection +Content in subsections should be properly counted. + +### Lists +- Item 1: Testing list counting +- Item 2: Multiple items +- Item 3: Final item + +### Tables +| Column A | Column B | Column C | +|----------|----------|----------| +| Row 1A | Row 1B | Row 1C | +| Row 2A | Row 2B | Row 2C | + +### Code Block +```python +def test_function(): + return "This code block should be counted" +``` + +### Blockquote +> This is a blockquote that should be analyzed by the status control. + +## Expected Behavior + +The JavaScript controls should: +1. Initialize successfully with proper error handling +2. Display accurate document statistics +3. Provide interactive drag/resize functionality +4. Work with the debug system integration +5. Handle errors gracefully per the Guardrail Principle + +This test will verify that our external JavaScript files work correctly with the HTML template system. \ No newline at end of file diff --git a/test_strict_mode.html b/test_strict_mode.html new file mode 100644 index 00000000..3d7c460c --- /dev/null +++ b/test_strict_mode.html @@ -0,0 +1,473 @@ + + + + + + + Strict Mode Test - Fail Fast + Robustness + + + + + +
+

Strict Mode Test - Fail Fast + Robustness Balance

+ +

This test page verifies that our Fail Fast strict mode works correctly in development while maintaining Robustness Principle protection in production.

+ +

Test Content

+ +

Document Statistics Test

+

This paragraph should be counted by the status control. It contains inline code and various formatting.

+ +

List Test

+ + +

Table Test

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureDevelopment ModeProduction Mode
Error HandlingFail Fast (throw errors)Graceful degradation
Missing DependenciesThrow error immediatelySkip with warning
Validation FailuresStop executionUse fallback values
+ +
+

Test Results

+
Loading tests...
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file