diff --git a/GUARDRAILS.md b/GUARDRAILS.md new file mode 100644 index 00000000..0d996e9a --- /dev/null +++ b/GUARDRAILS.md @@ -0,0 +1,81 @@ +# Development Guardrails + +## JavaScript Code Principles + +### 1. No Inline JavaScript in Python +**NEVER write JavaScript code directly from Python code** + +❌ **Wrong:** +```python +script = f""" +function myFunction() {{ + console.log("Hello {name}"); +}} +""" +``` + +✅ **Correct:** +```python +# Load from external files only +components = [ + 'js/core/section-manager.js', + 'js/components/debug-panel.js', + 'js/components/document-controls.js' +] +``` + +### 2. Why This Rule Exists +- **Quoting Problems**: String escaping in Python corrupts JavaScript +- **Syntax Errors**: Template literals and complex JS break when embedded +- **Maintainability**: JS code should be in .js files for proper tooling +- **Architecture**: Follows the established modular component system + +### 3. Proper Approach +1. Create separate `.js` files in `markitect/static/js/components/` +2. Load them via `_get_clean_editor_scripts()` +3. Wire up components in the initialization script only + +## Testing and Validation + +### 1. Always Validate Generated HTML +- Check that HTML files actually render content +- Validate JavaScript syntax before deployment +- Test both viewing and editing modes + +### 2. Detect JavaScript Errors Programmatically +- Run syntax validation on generated JS +- Check for common error patterns +- Fail fast when JS is malformed + +### 3. Manual Testing Backup +- If automated checks pass but functionality fails +- Open generated HTML in browser +- Check console for runtime errors +- Report specific error messages + +## Architecture Principles + +### 1. Separation of Concerns +- Python: File generation, template management +- JavaScript: UI components, interaction logic +- HTML: Structure and content only + +### 2. Modular Component System +- Each UI component in separate file +- Lazy loading where appropriate +- Clear dependency management + +### 3. Error Handling +- Graceful degradation when components fail +- Clear error messages for debugging +- Fallback modes when possible + +## Breaking These Rules + +If you find yourself writing JavaScript in Python strings: +1. **STOP** - Step back and reconsider +2. Create a proper component file instead +3. Use the existing component loading system +4. Add validation to catch the issue early + +These guardrails exist because we've seen the problems when they're violated. \ No newline at end of file diff --git a/markitect/clean_document_manager.py b/markitect/clean_document_manager.py index 1a829030..b3270667 100644 --- a/markitect/clean_document_manager.py +++ b/markitect/clean_document_manager.py @@ -1176,6 +1176,149 @@ class CleanDocumentManager: }} catch (error) {{ console.error("Scroll indicators failed to initialize:", error); }} + + // Step 4: Initialize DocumentNavigator (lazy loading for all modes) + try {{ + const documentNavigator = {{ + navElement: null, + isExpanded: false, + + createControl: function() {{ + console.log("📋 Creating DocumentNavigator control for view mode..."); + + this.navElement = document.createElement('nav'); + this.navElement.className = 'document-navigator'; + this.navElement.innerHTML = ` + + + `; + + // Position on left side following UI convention + this.navElement.style.cssText = ` + position: fixed; + top: 80px; + left: 20px; + z-index: 1000; + 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: width 0.3s ease; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + `; + + // Style toggle button + const toggleBtn = this.navElement.querySelector('.navigator-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 navigation on-demand + toggleBtn.addEventListener('click', () => {{ + console.log("📋 Navigator toggle clicked - building navigation..."); + this.buildNavigation(); + }}); + + // Close button handler + const closeBtn = this.navElement.querySelector('.navigator-close'); + closeBtn.addEventListener('click', () => {{ + this.collapse(); + }}); + + // Responsive behavior + window.addEventListener('resize', () => {{ + if (window.innerWidth <= 768) {{ + this.navElement.style.display = 'none'; + }} else {{ + this.navElement.style.display = ''; + }} + }}); + + document.body.appendChild(this.navElement); + + // Hide on mobile + if (window.innerWidth <= 768) {{ + this.navElement.style.display = 'none'; + }} + + console.log("📋 DocumentNavigator control created"); + }}, + + buildNavigation: function() {{ + const panel = this.navElement.querySelector('.navigator-panel'); + const content = this.navElement.querySelector('.navigator-content'); + + // Build navigation content from current DOM + const headings = document.querySelectorAll('h1, h2, h3'); + 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(); + }}, + + expand: function() {{ + this.isExpanded = true; + const panel = this.navElement.querySelector('.navigator-panel'); + this.navElement.style.width = '280px'; + panel.style.display = 'block'; + }}, + + collapse: function() {{ + this.isExpanded = false; + const panel = this.navElement.querySelector('.navigator-panel'); + panel.style.display = 'none'; + this.navElement.style.width = '40px'; + }} + }}; + + // Initialize the DocumentNavigator control + documentNavigator.createControl(); + + // Make globally available for mobile collapse + window.documentNavigator = documentNavigator; + }} catch (error) {{ + console.error("DocumentNavigator failed to initialize:", error); + }} }}); // Handle CDN loading errors @@ -1237,6 +1380,127 @@ document.addEventListener('DOMContentLoaded', function() { // Create document controls documentControls.create(); + // Create DocumentNavigator for edit mode (lazy loading) + const documentNavigator = { + navElement: null, + isExpanded: false, + + createControl: function() { + console.log("📋 Creating DocumentNavigator control for edit mode..."); + + this.navElement = document.createElement('nav'); + this.navElement.className = 'document-navigator edit-mode'; + this.navElement.innerHTML = ` + + + `; + + // Position on left side following UI convention + this.navElement.style.cssText = ` + position: fixed; + top: 80px; + left: 20px; + z-index: 1001; + 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: width 0.3s ease; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + `; + + // Style toggle button + const toggleBtn = this.navElement.querySelector('.navigator-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 navigation on-demand + toggleBtn.addEventListener('click', () => { + console.log("📋 Navigator toggle clicked - building navigation..."); + this.buildNavigation(); + }); + + // Close button handler + const closeBtn = this.navElement.querySelector('.navigator-close'); + closeBtn.addEventListener('click', () => { + this.collapse(); + }); + + document.body.appendChild(this.navElement); + console.log("📋 DocumentNavigator control created"); + }, + + buildNavigation: function() { + const panel = this.navElement.querySelector('.navigator-panel'); + const content = this.navElement.querySelector('.navigator-content'); + + // Build navigation content from current DOM + const headings = document.querySelectorAll('h1, h2, h3'); + 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(); + }, + + expand: function() { + this.isExpanded = true; + const panel = this.navElement.querySelector('.navigator-panel'); + this.navElement.style.width = '300px'; + panel.style.display = 'block'; + }, + + collapse: function() { + this.isExpanded = false; + const panel = this.navElement.querySelector('.navigator-panel'); + panel.style.display = 'none'; + this.navElement.style.width = '40px'; + } + }; + + // Initialize the DocumentNavigator control + documentNavigator.createControl(); + // Wire up event handlers documentControls.setEventHandlers({ 'save-document': () => { diff --git a/tools/validate_js.py b/tools/validate_js.py new file mode 100755 index 00000000..72229dc2 --- /dev/null +++ b/tools/validate_js.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +JavaScript Validation Tool + +Extracts JavaScript from HTML files and validates syntax. +Detects common issues like "Uncaught SyntaxError: unexpected token" +""" + +import re +import subprocess +import sys +import tempfile +from pathlib import Path + + +def extract_javascript_from_html(html_file): + """Extract all JavaScript code from an HTML file.""" + try: + content = Path(html_file).read_text(encoding='utf-8') + except Exception as e: + print(f"❌ Failed to read {html_file}: {e}") + return [] + + # Find all ' + scripts = re.findall(script_pattern, content, re.DOTALL | re.IGNORECASE) + + # Filter out empty scripts and external script tags + js_blocks = [] + for script in scripts: + script = script.strip() + if script and not script.startswith('http'): # Skip external scripts + js_blocks.append(script) + + return js_blocks + + +def validate_javascript_syntax(js_code): + """Validate JavaScript syntax using Node.js.""" + try: + # Create temporary JS file + with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f: + f.write(js_code) + temp_file = f.name + + # Try to parse with node --check + result = subprocess.run( + ['node', '--check', temp_file], + capture_output=True, + text=True + ) + + # Clean up + Path(temp_file).unlink() + + if result.returncode == 0: + return True, "✓ Syntax OK" + else: + return False, f"❌ Syntax Error: {result.stderr.strip()}" + + except FileNotFoundError: + # Fallback: Try to detect obvious syntax errors + return validate_javascript_basic(js_code) + except Exception as e: + return False, f"❌ Validation failed: {e}" + + +def validate_javascript_basic(js_code): + """Basic JavaScript syntax validation without Node.js.""" + errors = [] + + # Check for common syntax issues + if js_code.count('{') != js_code.count('}'): + errors.append("Mismatched curly braces") + + if js_code.count('(') != js_code.count(')'): + errors.append("Mismatched parentheses") + + if js_code.count('[') != js_code.count(']'): + errors.append("Mismatched square brackets") + + # Check for unescaped quotes in strings + if re.search(r'["\']\s*["\']\s*["\']\s*["\']', js_code): + errors.append("Possible quote escaping issue") + + # Check for Python-style string formatting leftover + if '${' in js_code and '"}' in js_code: + errors.append("Possible Python string template leftover") + + if errors: + return False, f"❌ Potential issues: {', '.join(errors)}" + else: + return True, "✓ Basic validation passed (Node.js not available)" + + +def validate_html_js(html_file): + """Validate all JavaScript in an HTML file.""" + print(f"🔍 Validating JavaScript in: {html_file}") + + js_blocks = extract_javascript_from_html(html_file) + + if not js_blocks: + print("⚠️ No JavaScript found in HTML file") + return True + + print(f"📝 Found {len(js_blocks)} JavaScript blocks") + + all_valid = True + for i, js_block in enumerate(js_blocks, 1): + print(f"\n--- Script Block {i} ({len(js_block)} chars) ---") + + # Show first few lines for context + lines = js_block.split('\n')[:3] + preview = '\n'.join(lines) + if len(lines) < len(js_block.split('\n')): + preview += "\n..." + print(f"Preview:\n{preview}") + + is_valid, message = validate_javascript_syntax(js_block) + print(message) + + if not is_valid: + all_valid = False + # Show more context around potential error + if 'line' in message.lower(): + try: + line_num = re.search(r'line (\d+)', message, re.IGNORECASE) + if line_num: + line_no = int(line_num.group(1)) + lines = js_block.split('\n') + start = max(0, line_no - 3) + end = min(len(lines), line_no + 2) + print("\nContext around error:") + for j in range(start, end): + marker = ">>> " if j == line_no - 1 else " " + print(f"{marker}{j+1:3}: {lines[j]}") + except: + pass + + print(f"\n{'✅' if all_valid else '❌'} Overall result: {'All JavaScript is valid' if all_valid else 'JavaScript validation failed'}") + return all_valid + + +def main(): + if len(sys.argv) != 2: + print("Usage: python validate_js.py ") + print("Example: python validate_js.py /tmp/test-edit.html") + sys.exit(1) + + html_file = sys.argv[1] + + if not Path(html_file).exists(): + print(f"❌ File not found: {html_file}") + sys.exit(1) + + success = validate_html_js(html_file) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() \ No newline at end of file