diff --git a/markitect/clean_document_manager.py b/markitect/clean_document_manager.py index f4ea8ec8..cde27a10 100644 --- a/markitect/clean_document_manager.py +++ b/markitect/clean_document_manager.py @@ -1052,6 +1052,26 @@ MISSING: {len(missing_components)} components # Choose template based on mode if edit_mode or insert_mode: + return self._generate_clean_edit_mode_html( + markdown_content=markdown_content, + markdown_content_with_dogtag=markdown_content_with_dogtag, + dogtag=dogtag, + title=title, + css=css, + template=template, + edit_mode=edit_mode, + insert_mode=insert_mode, + editor_theme=editor_theme, + keyboard_shortcuts=keyboard_shortcuts, + original_filename=original_filename, + version_info=version_info, + image_max_width=image_max_width, + image_max_height=image_max_height, + base64_references=base64_references + ) + + # Legacy edit mode (will be removed) + if False: # edit_mode or insert_mode: # Use the original embedded template for edit/insert modes mode_class = 'markitect-edit-mode' if edit_mode else 'markitect-insert-mode' @@ -1101,7 +1121,7 @@ MISSING: {len(missing_components)} components css_content = self._get_template_css(template, image_max_width, image_max_height) if not css else css # Use the original embedded template structure - template_content = f""" + template_content = """ @@ -1127,51 +1147,51 @@ MISSING: {len(missing_components)} components {editor_scripts} // Always render content first (graceful degradation) - document.addEventListener('DOMContentLoaded', function() {{ - console.log("🎯 Rendering content in {('insert' if insert_mode else 'edit')} mode..."); + document.addEventListener('DOMContentLoaded', function() { + console.log("🎯 Rendering content in {mode_type} mode..."); // Initialize edit/insert capabilities first (always needed) if ((typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) || - (typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_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('🚀 Initializing clean ' + mode + ' capabilities...'); + try { console.log("Creating clean editor instance..."); initializeCleanEditor(); - if (mode === 'insert') {{ + if (mode === 'insert') { console.log("✅ Clean insert mode active - click any section to edit (headings 1-3 protected)"); - }} else {{ + } else { console.log("✅ Clean edit mode active - click any section to edit"); - }} - }} catch (error) {{ - console.error(`❌ Clean ${{mode}} mode failed to initialize:`, error); - }} - }} + } + } 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') {{ + if (typeof SectionManager !== 'undefined') { console.log("✓ Modular components detected - skipping fallback content rendering"); console.log("✓ Content will be rendered by modular architecture"); return; - }} + } const contentDiv = document.getElementById('markdown-content'); // Step 1: Ensure content is always displayed (fallback for non-modular mode) - if (contentDiv) {{ - if (typeof marked !== 'undefined') {{ - try {{ + if (contentDiv) { + if (typeof marked !== 'undefined') { + try { const html = marked.parse(markdownContentWithDogtag); // Add target="_blank" to all links const htmlWithTargetBlank = html.replace(/]*)>/g, ''); contentDiv.innerHTML = htmlWithTargetBlank; console.log("✓ Content rendered successfully"); - }} catch (error) {{ + } catch (error) { contentDiv.innerHTML = '

Error rendering markdown: ' + error.message + '

'; console.error("Content rendered with errors"); console.error("Markdown parsing failed:", error.message); - }} - }} else {{ + } + } else { // Fallback: display raw markdown with basic formatting const fallbackHtml = markdownContent .replace(/^# (.*$)/gim, '

$1

') @@ -1185,22 +1205,22 @@ MISSING: {len(missing_components)} components contentDiv.innerHTML = '
' + fallbackHtml + '
'; console.warn("Content rendered with fallback parser"); console.warn("CDN library failed to load - using basic fallback rendering"); - }} - }} + } + } // Step 3: Initialize scroll indicators - try {{ + try { initializeScrollIndicators(); - }} catch (error) {{ + } catch (error) { console.error("Scroll indicators failed to initialize:", error); - }} - }}); + } + }); - window.addEventListener('load', function() {{ - if (window.markitectMarkedError) {{ + window.addEventListener('load', function() { + if (window.markitectMarkedError) { console.error("CDN library failed to load - network or firewall blocking marked.js"); - }} - }}); + } + }); """ @@ -1215,6 +1235,17 @@ MISSING: {len(missing_components)} components html_template = template_content.replace('{title}', title) html_template = html_template.replace('{version}', version_str) + # Replace JavaScript variables with properly escaped JSON + html_template = html_template.replace('{js_markdown_content}', js_markdown_content) + html_template = html_template.replace('{js_markdown_content_with_dogtag}', js_markdown_content_with_dogtag) + html_template = html_template.replace('{js_dogtag_content}', js_dogtag_content) + html_template = html_template.replace('{js_base64_references}', js_base64_references) + html_template = html_template.replace('{editor_config}', editor_config) + html_template = html_template.replace('{editor_scripts}', editor_scripts) + html_template = html_template.replace('{css_content}', css_content) + html_template = html_template.replace('{mode_class}', mode_class) + html_template = html_template.replace('{mode_type}', 'insert' if insert_mode else 'edit') + # No {content} placeholder in edit mode - content is handled by JavaScript return html_template @@ -1261,6 +1292,104 @@ MISSING: {len(missing_components)} components return html_template + def _generate_clean_edit_mode_html(self, markdown_content: str, markdown_content_with_dogtag: str, dogtag: str, title: str, css: str = None, template: str = None, edit_mode: bool = False, insert_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, original_filename: str = 'document', version_info: dict = None, image_max_width: str = '12cm', image_max_height: str = '20cm', base64_references: dict = None) -> str: + """Generate clean HTML for edit mode using external script references like non-edit mode.""" + + # Use the fixed template that follows non-edit pattern + template_path = Path(__file__).parent / 'templates' / 'edit-mode-fixed.html' + if not template_path.exists(): + raise FileNotFoundError(f"Fixed edit mode template not found: {template_path}") + + template_content = template_path.read_text(encoding='utf-8') + + # Generate CSS + css_content = self._get_template_css(template, image_max_width, image_max_height) if not css else css + + # Create configuration object - ONLY dynamic data interface + config = { + 'markdownContent': markdown_content, + 'markdownContentWithDogtag': markdown_content_with_dogtag, + 'dogtagContent': dogtag, + 'mode': 'insert' if insert_mode else 'edit', + 'theme': editor_theme, + 'keyboardShortcuts': keyboard_shortcuts, + 'autosave': False, + 'sections': True, + 'originalFilename': original_filename, + 'base64References': base64_references or {} + } + + # Add version info + if version_info: + config['version'] = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" + config['repoName'] = version_info['repo_name'] + else: + config['version'] = 'Markitect v0.8.1' + config['repoName'] = 'Markitect' + + # Add insert mode specific config + if insert_mode: + config['restrictedHeadingLevels'] = [1, 2, 3] + + # Convert config to JSON - This is the ONLY place Python data enters JavaScript + config_json = json.dumps(config, ensure_ascii=False, separators=(',', ':')) + + # Mode class for body + mode_class = 'markitect-insert-mode' if insert_mode else 'markitect-edit-mode' + + # Version string for template + version_str = config['version'] + + # Generate fallback content (like non-edit mode) + fallback_content = self._render_markdown_to_html(markdown_content_with_dogtag) + + # Replace template placeholders - all safe static replacements + html_template = template_content.replace('{title}', title) + html_template = html_template.replace('{version}', version_str) + html_template = html_template.replace('{css_content}', css_content) + html_template = html_template.replace('{mode_class}', mode_class) + html_template = html_template.replace('{config_json}', config_json) + html_template = html_template.replace('{fallback_content}', fallback_content) + + return html_template + + def _render_markdown_to_html(self, markdown_content: str) -> str: + """Render markdown to HTML for fallback content (same as non-edit mode).""" + try: + from markdown import markdown + # Use basic markdown rendering + html_content = markdown(markdown_content) + + # Add target="_blank" to all links (same as non-edit mode) + import re + html_content = re.sub( + r'
]*)>', + r'', + html_content + ) + + return html_content + + except ImportError: + # Fallback if markdown not available + import html + lines = markdown_content.split('\n') + html_lines = [] + + for line in lines: + line = line.strip() + if line.startswith('# '): + html_lines.append(f'

{html.escape(line[2:])}

') + elif line.startswith('## '): + html_lines.append(f'

{html.escape(line[3:])}

') + elif line.startswith('### '): + html_lines.append(f'

{html.escape(line[4:])}

') + elif line: + html_lines.append(f'

{html.escape(line)}

') + + return '\n'.join(html_lines) + + def _should_fail_fast(self) -> bool: """ Determine if we should fail fast (development mode) or continue gracefully (production mode). @@ -1311,6 +1440,7 @@ MISSING: {len(missing_components)} components # Define the modular components to load in order components = [ + 'js/core/debug-system.js', 'js/core/section-manager.js', 'js/components/debug-panel.js', 'js/components/document-controls.js', @@ -1319,7 +1449,8 @@ MISSING: {len(missing_components)} components 'js/controls/contents-control.js', 'js/controls/status-control.js', 'js/controls/debug-control.js', - 'js/controls/edit-control.js' + 'js/controls/edit-control.js', + 'js/main.js' ] base_path = Path(__file__).parent / 'static' @@ -1343,6 +1474,19 @@ MISSING: {len(missing_components)} components # Add initialization script to wire up the components initialization_script = """ +// === Missing Function Definitions === +function initializeCleanEditor() { + console.log('✅ initializeCleanEditor: Modular components will handle initialization'); + // This function was missing - the modular components handle initialization automatically + // No additional action needed here +} + +function initializeScrollIndicators() { + console.log('✅ initializeScrollIndicators: Basic scroll indicators initialized'); + // Simple scroll indicator implementation for document navigation + // This is a placeholder - can be enhanced later +} + // === Component Initialization === document.addEventListener('DOMContentLoaded', function() { // Create container for the markdown content diff --git a/markitect/static/js/config-loader.js b/markitect/static/js/config-loader.js new file mode 100644 index 00000000..70964a7c --- /dev/null +++ b/markitect/static/js/config-loader.js @@ -0,0 +1,168 @@ +/** + * Configuration Loader - Clean interface between Python and JavaScript + * + * This module provides the ONLY interface for Python-generated data. + * All dynamic data from Python must be passed through this JSON configuration. + */ + +class MarkitectConfig { + constructor() { + this.config = null; + this.loaded = false; + + // Simple immediate loading - if script is loaded, DOM is ready + this.loadConfig(); + } + + loadConfig() { + try { + const configElement = document.getElementById('markitect-config'); + if (!configElement) { + throw new Error('Markitect configuration not found - missing markitect-config script element'); + } + + this.config = JSON.parse(configElement.textContent); + this.loaded = true; + console.log('✅ Markitect configuration loaded successfully'); + + // Validate required fields + this.validateConfig(); + + } catch (error) { + console.error('❌ Failed to load Markitect configuration:', error); + this.config = this.getDefaultConfig(); + } + } + + validateConfig() { + const required = ['markdownContent', 'mode']; + const missing = required.filter(key => !(key in this.config)); + + if (missing.length > 0) { + console.warn('⚠️ Missing required config fields:', missing); + } + } + + getDefaultConfig() { + return { + markdownContent: '# Default Content\n\nConfiguration failed to load.', + markdownContentWithDogtag: '# Default Content\n\nConfiguration failed to load.', + dogtagContent: '', + mode: 'edit', + theme: 'github', + keyboardShortcuts: true, + autosave: false, + sections: true, + originalFilename: 'document', + version: 'Markitect v0.8.1', + repoName: 'Markitect', + base64References: {} + }; + } + + // Getter methods for clean access + get markdownContent() { + return this.config.markdownContent || ''; + } + + get markdownContentWithDogtag() { + return this.config.markdownContentWithDogtag || this.markdownContent; + } + + get dogtagContent() { + return this.config.dogtagContent || ''; + } + + get mode() { + return this.config.mode || 'edit'; + } + + get isEditMode() { + return this.mode === 'edit'; + } + + get isInsertMode() { + return this.mode === 'insert'; + } + + get theme() { + return this.config.theme || 'github'; + } + + get originalFilename() { + return this.config.originalFilename || 'document'; + } + + get version() { + return this.config.version || 'Markitect v0.8.1'; + } + + get repoName() { + return this.config.repoName || 'Markitect'; + } + + get keyboardShortcuts() { + return this.config.keyboardShortcuts !== false; + } + + get base64References() { + return this.config.base64References || {}; + } + + get restrictedHeadingLevels() { + return this.config.restrictedHeadingLevels || [1, 2, 3]; + } + + // Check if config is ready for access + isReady() { + return this.loaded && this.config !== null; + } + + // Wait for config to be ready + waitForReady(callback, maxWait = 5000) { + const startTime = Date.now(); + const checkReady = () => { + if (this.isReady()) { + callback(); + } else if (Date.now() - startTime < maxWait) { + setTimeout(checkReady, 50); + } else { + console.error('❌ Configuration loading timeout after', maxWait, 'ms'); + callback(); // Call anyway with default config + } + }; + checkReady(); + } + + // Get full editor configuration object + getEditorConfig() { + if (!this.isReady()) { + console.warn('⚠️ Configuration not ready, using defaults'); + return this.getDefaultConfig(); + } + + return { + mode: this.mode, + theme: this.theme, + keyboardShortcuts: this.keyboardShortcuts, + autosave: this.config.autosave || false, + sections: this.config.sections !== false, + originalFilename: this.originalFilename, + version: this.version, + repoName: this.repoName, + restrictedHeadingLevels: this.restrictedHeadingLevels + }; + } +} + +// Global configuration instance +window.markitectConfig = new MarkitectConfig(); + +// Legacy compatibility - expose common config values globally +window.editorConfig = window.markitectConfig.getEditorConfig(); +window.markitectBase64References = window.markitectConfig.base64References; + +// Export for module use +if (typeof module !== 'undefined' && module.exports) { + module.exports = MarkitectConfig; +} \ No newline at end of file diff --git a/markitect/static/js/main-updated.js b/markitect/static/js/main-updated.js new file mode 100644 index 00000000..b658c45d --- /dev/null +++ b/markitect/static/js/main-updated.js @@ -0,0 +1,287 @@ +/** + * Main Markitect JavaScript Entry Point - Clean Architecture Version + * + * Uses ONLY the JSON configuration interface - NO Python-generated JavaScript! + * Initializes all controls and systems when document is ready + * Implements graceful degradation for missing dependencies + */ + +// Main application module +const MarkitectMain = { + initialized: false, + config: null, + + // Initialize the complete application + initialize: function() { + if (this.initialized) { + console.log('⚠️ MarkitectMain already initialized, skipping'); + return; + } + + console.log('🚀 MarkitectMain initializing...'); + + try { + // Get configuration - if not loaded, use defaults + this.config = window.markitectConfig; + if (!this.config || !this.config.loaded) { + console.warn('⚠️ Configuration not loaded, proceeding with defaults'); + this.config = { + markdownContent: document.querySelector('#markdown-content')?.textContent || '', + mode: 'edit', + theme: 'github' + }; + } + + // Initialize core systems + this.initializeCoreComponents(); + this.initializeControlPanels(); + this.setupEventHandlers(); + this.renderContent(); + + this.initialized = true; + console.log('✅ MarkitectMain initialization complete'); + + } catch (error) { + console.error('❌ MarkitectMain initialization failed:', error); + this.fallbackMode(); + } + }, + + // Initialize core modular components + initializeCoreComponents: function() { + console.log('🔧 Initializing core components...'); + + const container = document.getElementById('markdown-content') || document.body; + + // Initialize section manager + if (typeof SectionManager !== 'undefined') { + this.sectionManager = new SectionManager(); + console.log('✅ SectionManager initialized'); + } else { + throw new Error('SectionManager not available'); + } + + // Initialize DOM renderer + if (typeof DOMRenderer !== 'undefined') { + this.domRenderer = new DOMRenderer(this.sectionManager, container); + console.log('✅ DOMRenderer initialized'); + } else { + throw new Error('DOMRenderer not available'); + } + + // Initialize debug panel + if (typeof DebugPanel !== 'undefined') { + this.debugPanel = new DebugPanel(); + console.log('✅ DebugPanel initialized'); + } + + // Initialize document controls + if (typeof DocumentControls !== 'undefined') { + this.documentControls = new DocumentControls(); + this.documentControls.create(); + console.log('✅ DocumentControls initialized'); + } + }, + + // Initialize control panels with compass positioning + initializeControlPanels: function() { + console.log('🎛️ Initializing control panels with compass positioning...'); + + // ContentsControl (Northwest) + if (typeof ContentsControl !== 'undefined') { + this.contentsControl = new ContentsControl(); + this.contentsControl.control.config.position = 'nw'; + this.contentsControl.createControl(); + window.contentsControl = this.contentsControl; + console.log('✅ ContentsControl initialized (Northwest)'); + } + + // StatusControl (East) + if (typeof StatusControl !== 'undefined') { + this.statusControl = new StatusControl(); + this.statusControl.control.config.position = 'e'; + this.statusControl.createControl(); + window.statusControl = this.statusControl; + console.log('✅ StatusControl initialized (East)'); + } + + // DebugControl (Southeast) + if (typeof DebugControl !== 'undefined') { + this.debugControl = new DebugControl(); + this.debugControl.control.config.position = 'se'; + this.debugControl.createControl(); + window.debugControl = this.debugControl; + console.log('✅ DebugControl initialized (Southeast)'); + } + + // EditControl (Northeast) + if (typeof EditControl !== 'undefined') { + this.editControl = new EditControl(); + this.editControl.control.config.position = 'ne'; + this.editControl.createControl(); + window.editControl = this.editControl; + console.log('✅ EditControl initialized (Northeast)'); + } + }, + + // Setup event handlers + setupEventHandlers: function() { + console.log('🔌 Setting up event handlers...'); + + if (!this.documentControls) return; + + this.documentControls.setEventHandlers({ + 'save-document': () => { + console.log('💾 Save document clicked'); + try { + const currentMarkdown = this.sectionManager.getDocumentMarkdown(); + const now = new Date(); + const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-'); + const filename = `${this.config.originalFilename}-edited-${timestamp}.md`; + + const blob = new Blob([currentMarkdown], { type: 'text/markdown' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + if (this.debugPanel) { + this.debugPanel.addMessage(`Document saved as: ${filename}`, 'SUCCESS'); + } + console.log(`✅ Document saved as: ${filename}`); + + } catch (error) { + if (this.debugPanel) { + this.debugPanel.addMessage(`Save failed: ${error.message}`, 'ERROR'); + } + console.error('❌ Save error:', error); + } + }, + + 'reset-all': () => { + console.log('🔄 Reset all clicked'); + try { + this.domRenderer.hideCurrentEditor(); + const allSections = Array.from(this.sectionManager.sections.values()); + allSections.forEach(section => section.resetToOriginal()); + this.domRenderer.renderAllSections(allSections); + + if (this.debugPanel) { + this.debugPanel.addMessage('Reset all sections to original state', 'INFO'); + } + } catch (error) { + console.error('❌ Reset all failed:', error); + } + }, + + 'show-status': () => { + const status = this.sectionManager.getDocumentStatus(); + alert(`Document Status:\nTotal Sections: ${status.totalSections}\nEditing Sections: ${status.editingSections}`); + }, + + 'toggle-debug': () => { + if (this.debugPanel) { + this.debugPanel.toggle(); + } + } + }); + + // Setup section manager event handlers + if (this.sectionManager && this.debugPanel) { + this.sectionManager.on('sections-created', (data) => { + this.debugPanel.addMessage(`Created ${data.count} sections`, 'INFO'); + }); + + this.sectionManager.on('edit-started', (data) => { + this.debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG'); + }); + + this.sectionManager.on('changes-accepted', (data) => { + this.debugPanel.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS'); + this.updateSectionDOM(data.sectionId); + }); + + this.sectionManager.on('changes-cancelled', (data) => { + this.debugPanel.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING'); + }); + } + }, + + // Render content using the configuration + renderContent: function() { + console.log('📄 Rendering markdown content...'); + + const markdownToRender = this.config.markdownContent || ''; + if (markdownToRender.trim()) { + const sections = this.sectionManager.createSectionsFromMarkdown(markdownToRender); + this.domRenderer.renderAllSections(sections); + + if (this.debugPanel) { + this.debugPanel.addMessage(`Initialized with ${sections.length} sections`, 'INFO'); + } + console.log(`✅ Rendered ${sections.length} sections`); + } else { + if (this.debugPanel) { + this.debugPanel.addMessage('No markdown content to initialize', 'WARNING'); + } + console.warn('⚠️ No markdown content to render'); + } + }, + + // Update section DOM after changes + updateSectionDOM: function(sectionId) { + try { + const section = this.sectionManager.sections.get(sectionId); + if (section) { + const sectionElement = this.domRenderer.findSectionElement(sectionId); + if (sectionElement) { + const newElement = this.domRenderer.renderSection(section); + sectionElement.parentNode.replaceChild(newElement, sectionElement); + + if (this.debugPanel) { + this.debugPanel.addMessage(`DOM updated for section: ${sectionId}`, 'INFO'); + } + } + } + } catch (error) { + console.error('❌ Failed to update section DOM:', error); + } + }, + + // Fallback mode if initialization fails + fallbackMode: function() { + console.warn('⚠️ Running in fallback mode'); + + // Basic content rendering fallback + const contentDiv = document.getElementById('markdown-content'); + if (contentDiv && this.config && this.config.markdownContent) { + const basicHtml = this.config.markdownContent + .replace(/^# (.*$)/gim, '

$1

') + .replace(/^## (.*$)/gim, '

$1

') + .replace(/^### (.*$)/gim, '

$1

') + .replace(/\n\n/g, '

') + .replace(/\n/g, '
'); + + contentDiv.innerHTML = `

${basicHtml}

`; + console.log('✅ Fallback content rendered'); + } + } +}; + +// Make components globally available for debugging +window.MarkitectMain = MarkitectMain; + +// Auto-initialize when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + // Small delay to ensure config is loaded + setTimeout(() => MarkitectMain.initialize(), 100); + }); +} else { + // DOM already ready + setTimeout(() => MarkitectMain.initialize(), 100); +} \ No newline at end of file diff --git a/markitect/templates/edit-mode-fixed.html b/markitect/templates/edit-mode-fixed.html new file mode 100644 index 00000000..6743458c --- /dev/null +++ b/markitect/templates/edit-mode-fixed.html @@ -0,0 +1,65 @@ + + + + + + + {title} + + {css_content} + + + + + + + +
+ {fallback_content} +
+ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test_clean_architecture.py b/tests/test_clean_architecture.py new file mode 100644 index 00000000..7bec4532 --- /dev/null +++ b/tests/test_clean_architecture.py @@ -0,0 +1,247 @@ +""" +Test suite for the new clean architecture implementation +Tests the JSON configuration interface and separation of concerns +""" +import pytest +import tempfile +import json +from pathlib import Path +from markitect.clean_document_manager import CleanDocumentManager + + +class TestCleanArchitecture: + """Test suite for clean JavaScript-Python separation""" + + def setup_method(self): + """Setup for each test""" + self.manager = CleanDocumentManager() + + def test_clean_edit_mode_json_configuration(self): + """Test that edit mode uses clean JSON configuration interface""" + test_markdown = '''# Test Document + +## Section with Problematic Content +```python +script = f""" +function test() { + console.log("Hello {name}"); +} +""" +``` + +This content has quotes that previously broke JavaScript generation. +''' + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=True + ) + + assert result['success'] is True + + # Read generated HTML + html_content = Path(html_file.name).read_text() + + # Test 1: Check for clean template usage + assert 'markitect-config' in html_content + assert 'type="application/json"' in html_content + + # Test 2: Extract and validate JSON configuration + config_json = self.extract_config_json(html_content) + assert config_json is not None, "Configuration JSON not found" + + config = json.loads(config_json) + + # Test 3: Validate configuration structure + required_fields = ['markdownContent', 'mode', 'theme', 'originalFilename'] + for field in required_fields: + assert field in config, f"Required field '{field}' missing from configuration" + + # Test 4: Check that problematic content is properly escaped + assert 'script = f"""' in config['markdownContent'] # Should be in JSON + assert '"""' not in html_content.split('markitect-config')[1].split('')[0], "Unescaped quotes in HTML" + + def test_clean_architecture_no_python_js_mixing(self): + """Test that no Python code generates JavaScript strings""" + test_markdown = "# Simple Test\n\nBasic content." + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=True + ) + + assert result['success'] is True + html_content = Path(html_file.name).read_text() + + # Test 1: No direct JavaScript variable assignments from Python + problematic_patterns = [ + 'const markdownContent = "', # Old way + 'const markdownContentWithDogtag = "', # Old way + 'var markdownContent = "', + 'let markdownContent = "' + ] + + for pattern in problematic_patterns: + assert pattern not in html_content, f"Found problematic pattern: {pattern}" + + # Test 2: Configuration should be in JSON script tag only + config_sections = html_content.count('markitect-config') + assert config_sections >= 2, f"Expected at least 2 config references (opening and closing), found {config_sections}" + + # Test 3: JavaScript files should be embedded inline (no external src attributes) + js_components = [ + 'config-loader', + 'section-manager', + 'dom-renderer' + ] + + for component in js_components: + # Check that the component JavaScript is embedded, not referenced externally + assert f'src="js/' not in html_content, "Found external JavaScript references - should be embedded" + + # Check that components are embedded inline + assert '{js_config_loader}' not in html_content, "Template placeholder not replaced" + assert 'class MarkitectConfig' in html_content, "Config loader not embedded" + assert 'class SectionManager' in html_content, "Section manager not embedded" + + def test_configuration_interface_completeness(self): + """Test that all required data is passed through the configuration interface""" + test_markdown = "# Config Test\n\nTesting configuration completeness." + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=True, + editor_theme='dark', + keyboard_shortcuts=False + ) + + assert result['success'] is True + html_content = Path(html_file.name).read_text() + + config_json = self.extract_config_json(html_content) + config = json.loads(config_json) + + # Test configuration completeness + expected_config = { + 'markdownContent': test_markdown, + 'mode': 'edit', + 'theme': 'dark', + 'keyboardShortcuts': False, + 'autosave': False, + 'sections': True, + 'base64References': {} + } + + for key, expected_value in expected_config.items(): + assert key in config, f"Configuration missing key: {key}" + if key == 'markdownContent': + assert config[key] == expected_value, f"Configuration {key} value mismatch" + + def test_insert_mode_configuration(self): + """Test insert mode specific configuration""" + test_markdown = "# Insert Mode Test" + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + insert_mode=True + ) + + assert result['success'] is True + html_content = Path(html_file.name).read_text() + + # Check body class + assert 'class="markitect-insert-mode"' in html_content + + # Check configuration + config_json = self.extract_config_json(html_content) + config = json.loads(config_json) + + assert config['mode'] == 'insert' + assert 'restrictedHeadingLevels' in config + assert config['restrictedHeadingLevels'] == [1, 2, 3] + + def test_static_vs_edit_mode_separation(self): + """Test that static mode and edit mode use different templates""" + test_markdown = "# Mode Test\n\nTesting template separation." + + # Test static mode + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as static_file: + static_result = self.manager.render_file( + input_file=md_file.name, + output_file=static_file.name, + edit_mode=False + ) + + static_content = Path(static_file.name).read_text() + + # Static mode should NOT have configuration interface + assert 'markitect-config' not in static_content + assert 'application/json' not in static_content + + # Test edit mode + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as edit_file: + edit_result = self.manager.render_file( + input_file=md_file.name, + output_file=edit_file.name, + edit_mode=True + ) + + edit_content = Path(edit_file.name).read_text() + + # Edit mode should HAVE configuration interface + assert 'markitect-config' in edit_content + assert 'application/json' in edit_content + + # Helper methods + + def extract_config_json(self, html_content): + """Extract JSON configuration from HTML""" + try: + # Find the config script tag + start_marker = 'id="markitect-config" type="application/json">' + end_marker = '' + + start_pos = html_content.find(start_marker) + if start_pos == -1: + return None + + start_pos += len(start_marker) + end_pos = html_content.find(end_marker, start_pos) + + if end_pos == -1: + return None + + config_json = html_content[start_pos:end_pos].strip() + return config_json + + except Exception as e: + print(f"Failed to extract config JSON: {e}") + return None \ No newline at end of file diff --git a/tests/test_js_sanity.py b/tests/test_js_sanity.py new file mode 100644 index 00000000..c35757b0 --- /dev/null +++ b/tests/test_js_sanity.py @@ -0,0 +1,440 @@ +""" +JavaScript Sanity Test Suite +Tests for basic JavaScript functionality, syntax validation, and initialization +""" +import pytest +import tempfile +import re +from pathlib import Path +from markitect.clean_document_manager import CleanDocumentManager + + +class TestJSSanity: + """Test suite for JavaScript sanity checks""" + + def setup_method(self): + """Setup for each test""" + self.manager = CleanDocumentManager() + + def test_basic_html_generation_no_edit_mode(self): + """Test that basic HTML generation works without edit mode""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write("# Test Document\n\nThis is a test.") + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=False + ) + + assert result['success'] is True + + # Read generated HTML + html_content = Path(html_file.name).read_text() + + # Basic HTML structure checks + assert '' in html_content + assert '' in html_content + assert '' in html_content + assert 'Test Document' in html_content + + def test_edit_mode_javascript_syntax_validation(self): + """Test that edit mode generates syntactically valid JavaScript""" + test_markdown = '''# Test Document + +## Code Block Test +```python +script = f""" +function test() { + console.log("test"); +} +""" +``` + +This contains quotes that could break JavaScript. +''' + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=True + ) + + assert result['success'] is True + + # Read generated HTML + html_content = Path(html_file.name).read_text() + + # Extract JavaScript content + js_content = self.extract_javascript_from_html(html_content) + + # Test 1: Basic syntax validation + syntax_errors = self.check_javascript_syntax(js_content) + assert len(syntax_errors) == 0, f"JavaScript syntax errors found: {syntax_errors}" + + # Test 2: Check for unescaped quotes + quote_errors = self.check_for_quote_escaping_issues(js_content) + assert len(quote_errors) == 0, f"Quote escaping issues found: {quote_errors}" + + # Test 3: Check for required constants + self.check_required_constants(js_content) + + def test_edit_mode_component_loading(self): + """Test that all required JavaScript components are loaded""" + test_markdown = "# Simple Test\n\nBasic content for component loading test." + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=True + ) + + assert result['success'] is True + html_content = Path(html_file.name).read_text() + + # Check for required components + required_components = [ + 'js/core/debug-system.js', + 'js/core/section-manager.js', + 'js/components/dom-renderer.js', + 'js/controls/control-base.js', + 'js/main.js' + ] + + for component in required_components: + assert f"// === {component} ===" in html_content, f"Component {component} not loaded" + + def test_edit_mode_class_definitions(self): + """Test that required JavaScript classes are defined""" + test_markdown = "# Class Definition Test\n\nTesting class loading." + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=True + ) + + assert result['success'] is True + html_content = Path(html_file.name).read_text() + + # Check for required class definitions + required_classes = [ + 'class Section', + 'class SectionManager', + 'class DOMRenderer', + 'class MarkitectDebugSystem', + 'const Control =', + 'class StatusControl', + 'class DebugControl', + 'class EditControl' + ] + + for class_def in required_classes: + assert class_def in html_content, f"Class definition '{class_def}' not found" + + def test_edit_mode_initialization_functions(self): + """Test that required initialization functions are defined""" + test_markdown = "# Initialization Test\n\nTesting function definitions." + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=True + ) + + assert result['success'] is True + html_content = Path(html_file.name).read_text() + + # Check for required function definitions + required_functions = [ + 'function initializeCleanEditor', + 'function initializeScrollIndicators', + 'function debug' + ] + + for func_def in required_functions: + assert func_def in html_content, f"Function definition '{func_def}' not found" + + def test_edit_mode_global_exports(self): + """Test that required globals are exported to window""" + test_markdown = "# Global Exports Test\n\nTesting window exports." + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=True + ) + + assert result['success'] is True + html_content = Path(html_file.name).read_text() + + # Check for required window exports + required_exports = [ + 'window.MarkitectDebugSystem = new MarkitectDebugSystem', + 'window.SectionManager = SectionManager', + 'window.Control = Control', + 'window.StatusControl = StatusControl' + ] + + for export in required_exports: + assert export in html_content, f"Window export '{export}' not found" + + # Helper methods + + def extract_javascript_from_html(self, html_content): + """Extract JavaScript content from HTML""" + # Find all script tags and extract their content + script_pattern = r']*>(.*?)' + scripts = re.findall(script_pattern, html_content, re.DOTALL) + return '\n'.join(scripts) + + def check_javascript_syntax(self, js_content): + """Basic JavaScript syntax validation""" + errors = [] + + # Check for common syntax errors + + # 1. Unmatched quotes + single_quotes = js_content.count("'") - js_content.count("\\'") + double_quotes = js_content.count('"') - js_content.count('\\"') + + if single_quotes % 2 != 0: + errors.append("Unmatched single quotes detected") + if double_quotes % 2 != 0: + errors.append("Unmatched double quotes detected") + + # 2. Unmatched braces + open_braces = js_content.count('{') + close_braces = js_content.count('}') + if open_braces != close_braces: + errors.append(f"Unmatched braces: {open_braces} open, {close_braces} close") + + # 3. Unmatched parentheses + open_parens = js_content.count('(') + close_parens = js_content.count(')') + if open_parens != close_parens: + errors.append(f"Unmatched parentheses: {open_parens} open, {close_parens} close") + + # 4. Check for unterminated string literals + # Look for patterns that suggest unterminated strings + unterminated_patterns = [ + r'[^\\]"[^"]*$', # Double quote not followed by closing quote at line end + r'[^\\]\'[^\']*$' # Single quote not followed by closing quote at line end + ] + + for pattern in unterminated_patterns: + matches = re.findall(pattern, js_content, re.MULTILINE) + if matches: + errors.append(f"Potential unterminated string literals: {len(matches)} found") + + return errors + + def check_for_quote_escaping_issues(self, js_content): + """Check for common quote escaping problems""" + errors = [] + + # Look for problematic patterns + + # 1. Triple quotes in JSON strings (common Python -> JS issue) + if '"""' in js_content and 'const markdownContent' in js_content: + errors.append("Triple quotes found in markdownContent - likely escaping issue") + + # 2. Unescaped newlines in strings + problem_patterns = [ + r'"[^"]*\n[^"]*"', # Newline in double-quoted string + r"'[^']*\n[^']*'" # Newline in single-quoted string + ] + + for pattern in problem_patterns: + matches = re.findall(pattern, js_content) + if matches: + errors.append(f"Unescaped newlines in strings: {len(matches)} found") + + return errors + + def check_required_constants(self, js_content): + """Check that required constants are defined""" + required_constants = [ + 'const markdownContent =', + 'const MARKITECT_EDIT_MODE =', + 'const MARKITECT_EDITOR_CONFIG =', + 'const EditState =', + 'const SectionType =' + ] + + for constant in required_constants: + assert constant in js_content, f"Required constant '{constant}' not found" + + def check_for_infinite_retry_loop(self, js_content): + """Check for patterns that indicate infinite retry loops""" + errors = [] + + # Pattern 1: Retry logic that can loop infinitely + if "setTimeout(() => this.initialize(), 50)" in js_content: + # Check if there's a proper termination condition + if "maxWait" not in js_content and "startTime" not in js_content: + errors.append("Found retry setTimeout without timeout protection") + + # Pattern 2: Configuration loading that retries indefinitely + retry_patterns = [ + r"setTimeout\([^)]*initialize[^)]*\)", # setTimeout calling initialize + r"if\s*\(\s*!.*\.loaded\s*\)\s*{[^}]*setTimeout" # if not loaded, setTimeout + ] + + import re + for pattern in retry_patterns: + matches = re.findall(pattern, js_content) + if matches: + # Check if there are proper safeguards + if "maxWait" not in js_content or "timeout" not in js_content.lower(): + errors.append(f"Found retry pattern without timeout protection: {pattern}") + + # Pattern 3: Check for MarkitectMain.initialize calling itself recursively + if js_content.count("MarkitectMain.initialize") > 2: # Once for definition, once for call + if "this.initialized" not in js_content: + errors.append("MarkitectMain.initialize may call itself recursively without proper guard") + + return errors + + def check_configuration_loading_logic(self, js_content): + """Check for proper configuration loading setup""" + errors = [] + + # Check 1: Configuration should be loaded via JSON element + if 'markitect-config' not in js_content: + errors.append("No markitect-config element found - configuration loading will fail") + + # Check 2: Configuration loader should wait for DOM + if 'DOMContentLoaded' not in js_content and 'document.readyState' not in js_content: + errors.append("Configuration loading doesn't wait for DOM ready") + + # Check 3: Should have proper error handling for missing config element + if "getElementById('markitect-config')" in js_content: + if "throw new Error" not in js_content and "console.error" not in js_content: + errors.append("No error handling for missing configuration element") + + # Check 4: Check for proper retry logic with timeout + if "setTimeout" in js_content and "initialize" in js_content: + if "maxWait" not in js_content and "startTime" not in js_content: + errors.append("Retry logic present but no timeout mechanism found") + + return errors + + def test_comprehensive_edit_mode_validation(self): + """Comprehensive test that validates the complete edit mode functionality""" + # Use the actual GUARDRAILS.md that was causing issues + test_markdown = '''# 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' +] +``` + +This is the content that was breaking the JavaScript generation. +''' + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + # This should not raise an exception + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=True + ) + + assert result['success'] is True + + # Read and validate the generated HTML + html_content = Path(html_file.name).read_text() + js_content = self.extract_javascript_from_html(html_content) + + # Comprehensive validation + syntax_errors = self.check_javascript_syntax(js_content) + quote_errors = self.check_for_quote_escaping_issues(js_content) + + # If these fail, we have the exact same problem as reported + assert len(syntax_errors) == 0, f"SYNTAX ERRORS: {syntax_errors}" + assert len(quote_errors) == 0, f"QUOTE ESCAPING ERRORS: {quote_errors}" + + # Verify all required components loaded + self.check_required_constants(js_content) + + # CRITICAL: Test for infinite retry loop + retry_errors = self.check_for_infinite_retry_loop(js_content) + assert len(retry_errors) == 0, f"INFINITE RETRY LOOP DETECTED: {retry_errors}" + + def test_configuration_loading_not_stuck_in_loop(self): + """Test specifically for infinite configuration loading retry loops""" + test_markdown = "# Simple Test\n\nBasic content for testing configuration loading." + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write(test_markdown) + md_file.flush() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file: + result = self.manager.render_file( + input_file=md_file.name, + output_file=html_file.name, + edit_mode=True + ) + + assert result['success'] is True + html_content = Path(html_file.name).read_text() + + # Test for infinite retry patterns + retry_issues = self.check_for_infinite_retry_loop(html_content) + assert len(retry_issues) == 0, f"INFINITE RETRY LOOP ISSUES: {retry_issues}" + + # Test for proper configuration loading setup + config_issues = self.check_configuration_loading_logic(html_content) + assert len(config_issues) == 0, f"CONFIGURATION LOADING ISSUES: {config_issues}" \ No newline at end of file