/** * Editor Module * Handles CodeMirror editor and markdown preview */ class MarkdownEditor { constructor(editorId, previewId, filenameInputId) { this.editorElement = document.getElementById(editorId); this.previewElement = document.getElementById(previewId); this.filenameInput = document.getElementById(filenameInputId); this.currentFile = null; this.webdavClient = null; this.macroProcessor = new MacroProcessor(null); // Will be set later this.initCodeMirror(); this.initMarkdown(); this.initMermaid(); } /** * Initialize CodeMirror */ initCodeMirror() { this.editor = CodeMirror(this.editorElement, { mode: 'markdown', theme: 'monokai', lineNumbers: true, lineWrapping: true, autofocus: true, extraKeys: { 'Ctrl-S': () => this.save(), 'Cmd-S': () => this.save() } }); // Update preview on change with debouncing this.editor.on('change', this.debounce(() => { this.updatePreview(); }, 300)); // Initial preview render setTimeout(() => { this.updatePreview(); }, 100); // Sync scroll this.editor.on('scroll', () => { this.syncScroll(); }); } /** * Initialize markdown parser */ initMarkdown() { if (window.marked) { this.marked = window.marked; this.marked.setOptions({ breaks: true, gfm: true, highlight: (code, lang) => { if (lang && window.Prism.languages[lang]) { return window.Prism.highlight(code, window.Prism.languages[lang], lang); } return code; } }); } else { console.error('Marked library not found.'); } } /** * Initialize Mermaid */ initMermaid() { if (window.mermaid) { window.mermaid.initialize({ startOnLoad: false, theme: document.body.classList.contains('dark-mode') ? 'dark' : 'default' }); } } /** * Set WebDAV client */ setWebDAVClient(client) { this.webdavClient = client; // Update macro processor with client if (this.macroProcessor) { this.macroProcessor.webdavClient = client; } } /** * Load file */ async loadFile(path) { try { const content = await this.webdavClient.get(path); this.currentFile = path; this.filenameInput.value = path; this.editor.setValue(content); this.updatePreview(); if (window.showNotification) { window.showNotification(`Loaded ${path}`, 'info'); } } catch (error) { console.error('Failed to load file:', error); if (window.showNotification) { window.showNotification('Failed to load file', 'danger'); } } } /** * Save file */ async save() { const path = this.filenameInput.value.trim(); if (!path) { if (window.showNotification) { window.showNotification('Please enter a filename', 'warning'); } return; } const content = this.editor.getValue(); try { await this.webdavClient.put(path, content); this.currentFile = path; if (window.showNotification) { window.showNotification('✅ Saved', 'success'); } // Dispatch event to reload file tree if (window.eventBus) { window.eventBus.dispatch('file-saved', path); } } catch (error) { console.error('Failed to save file:', error); if (window.showNotification) { window.showNotification('Failed to save file', 'danger'); } } } /** * Create new file */ newFile() { this.currentFile = null; this.filenameInput.value = ''; this.filenameInput.focus(); this.editor.setValue('# New File\n\nStart typing...\n'); this.updatePreview(); if (window.showNotification) { window.showNotification('Enter filename and start typing', 'info'); } } /** * Delete current file */ async deleteFile() { if (!this.currentFile) { window.showNotification('No file selected', 'warning'); return; } const confirmed = await window.ConfirmationManager.confirm(`Are you sure you want to delete ${this.currentFile}?`, 'Delete File'); if (confirmed) { try { await this.webdavClient.delete(this.currentFile); window.showNotification(`Deleted ${this.currentFile}`, 'success'); this.newFile(); window.eventBus.dispatch('file-deleted'); } catch (error) { console.error('Failed to delete file:', error); window.showNotification('Failed to delete file', 'danger'); } } } /** * Update preview */ async updatePreview() { const markdown = this.editor.getValue(); const previewDiv = this.previewElement; if (!markdown || !markdown.trim()) { previewDiv.innerHTML = `

Start typing to see preview...

`; return; } try { // Step 1: Process macros let processedContent = markdown; if (this.macroProcessor) { const processingResult = await this.macroProcessor.processMacros(markdown); processedContent = processingResult.content; // Log errors if any if (processingResult.errors.length > 0) { console.warn('Macro processing errors:', processingResult.errors); } } // Step 2: Parse markdown to HTML if (!this.marked) { console.error("Markdown parser (marked) not initialized."); previewDiv.innerHTML = `
Preview engine not loaded.
`; return; } let html = this.marked.parse(processedContent); // Replace mermaid code blocks html = html.replace( /
([\s\S]*?)<\/code><\/pre>/g,
                (match, code) => {
                    const id = 'mermaid-' + Math.random().toString(36).substr(2, 9);
                    return `
${code.trim()}
`; } ); previewDiv.innerHTML = html; // Apply syntax highlighting const codeBlocks = previewDiv.querySelectorAll('pre code'); codeBlocks.forEach(block => { const languageClass = Array.from(block.classList) .find(cls => cls.startsWith('language-')); if (languageClass && languageClass !== 'language-mermaid') { if (window.Prism) { window.Prism.highlightElement(block); } } }); // Render mermaid diagrams const mermaidElements = previewDiv.querySelectorAll('.mermaid'); if (mermaidElements.length > 0 && window.mermaid) { try { window.mermaid.contentLoaded(); } catch (error) { console.warn('Mermaid rendering error:', error); } } } catch (error) { console.error('Preview rendering error:', error); previewDiv.innerHTML = ` `; } } /** * Sync scroll between editor and preview */ syncScroll() { const scrollInfo = this.editor.getScrollInfo(); const scrollPercent = scrollInfo.top / (scrollInfo.height - scrollInfo.clientHeight); const previewHeight = this.previewElement.scrollHeight - this.previewElement.clientHeight; this.previewElement.scrollTop = previewHeight * scrollPercent; } /** * Handle image upload */ async uploadImage(file) { try { const filename = await this.webdavClient.uploadImage(file); const imageUrl = `/fs/${this.webdavClient.currentCollection}/images/${filename}`; const markdown = `![${file.name}](${imageUrl})`; // Insert at cursor this.editor.replaceSelection(markdown); if (window.showNotification) { window.showNotification('Image uploaded', 'success'); } } catch (error) { console.error('Failed to upload image:', error); if (window.showNotification) { window.showNotification('Failed to upload image', 'danger'); } } } /** * Get editor content */ getValue() { return this.editor.getValue(); } insertAtCursor(text) { const doc = this.editor.getDoc(); const cursor = doc.getCursor(); doc.replaceRange(text, cursor); } /** * Set editor content */ setValue(content) { this.editor.setValue(content); } /** * Debounce function */ debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } } // Export for use in other modules window.MarkdownEditor = MarkdownEditor;