Compare commits
	
		
			1 Commits
		
	
	
		
			dragdrop_m
			...
			23a24d42e2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 23a24d42e2 | 
| @@ -111,19 +111,14 @@ | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| .tree-node { | ||||
|     cursor: move; | ||||
| } | ||||
|  | ||||
| /* Drag and drop */ | ||||
| .tree-node.dragging { | ||||
|     opacity: 0.5; | ||||
|     background-color: rgba(13, 110, 253, 0.1); | ||||
| } | ||||
|  | ||||
| .tree-node.drag-over { | ||||
|     background-color: rgba(13, 110, 253, 0.2) !important; | ||||
|     background-color: rgba(13, 110, 253, 0.2); | ||||
|     border: 1px dashed var(--link-color); | ||||
|     border-radius: 4px; | ||||
| } | ||||
|  | ||||
| /* Collection selector - Bootstrap styled */ | ||||
|   | ||||
| @@ -10,9 +10,7 @@ class MarkdownEditor { | ||||
|         this.filenameInput = document.getElementById(filenameInputId); | ||||
|         this.currentFile = null; | ||||
|         this.webdavClient = null; | ||||
|          | ||||
|         // Initialize macro processor AFTER webdavClient is set | ||||
|         this.macroProcessor = null; | ||||
|         this.macroProcessor = new MacroProcessor(null); // Will be set later | ||||
|          | ||||
|         this.initCodeMirror(); | ||||
|         this.initMarkdown(); | ||||
| @@ -90,8 +88,10 @@ class MarkdownEditor { | ||||
|     setWebDAVClient(client) { | ||||
|         this.webdavClient = client; | ||||
|          | ||||
|         // NOW initialize macro processor | ||||
|         this.macroProcessor = new MacroProcessor(client); | ||||
|         // Update macro processor with client | ||||
|         if (this.macroProcessor) { | ||||
|             this.macroProcessor.webdavClient = client; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -196,29 +196,38 @@ class MarkdownEditor { | ||||
|         const previewDiv = this.previewElement; | ||||
|  | ||||
|         if (!markdown || !markdown.trim()) { | ||||
|             previewDiv.innerHTML = `<div class="text-muted">Start typing...</div>`; | ||||
|             previewDiv.innerHTML = ` | ||||
|                 <div class="text-muted text-center mt-5"> | ||||
|                     <p>Start typing to see preview...</p> | ||||
|                 </div> | ||||
|             `; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             // Step 1: Process macros | ||||
|             console.log('[Editor] Processing macros...'); | ||||
|             let processedContent = markdown; | ||||
|              | ||||
|             if (this.macroProcessor) { | ||||
|                 const result = await this.macroProcessor.processMacros(markdown); | ||||
|                 processedContent = result.content; | ||||
|                 const processingResult = await this.macroProcessor.processMacros(markdown); | ||||
|                 processedContent = processingResult.content; | ||||
|                  | ||||
|                 if (result.errors.length > 0) { | ||||
|                     console.warn('[Editor] Macro errors:', result.errors); | ||||
|                 // Log errors if any | ||||
|                 if (processingResult.errors.length > 0) { | ||||
|                     console.warn('Macro processing errors:', processingResult.errors); | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             // Step 2: Parse markdown | ||||
|             console.log('[Editor] Parsing markdown...'); | ||||
|             // Step 2: Parse markdown to HTML | ||||
|             if (!this.marked) { | ||||
|                 console.error("Markdown parser (marked) not initialized."); | ||||
|                 previewDiv.innerHTML = `<div class="alert alert-danger">Preview engine not loaded.</div>`; | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             let html = this.marked.parse(processedContent); | ||||
|  | ||||
|             // Step 3: Handle mermaid | ||||
|             // Replace mermaid code blocks | ||||
|             html = html.replace( | ||||
|                 /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, | ||||
|                 (match, code) => { | ||||
| @@ -229,23 +238,35 @@ class MarkdownEditor { | ||||
|  | ||||
|             previewDiv.innerHTML = html; | ||||
|  | ||||
|             // Step 4: Syntax highlighting | ||||
|             // Apply syntax highlighting | ||||
|             const codeBlocks = previewDiv.querySelectorAll('pre code'); | ||||
|             codeBlocks.forEach(block => { | ||||
|                 const lang = Array.from(block.classList) | ||||
|                 const languageClass = Array.from(block.classList) | ||||
|                     .find(cls => cls.startsWith('language-')); | ||||
|                 if (lang && lang !== 'language-mermaid' && window.Prism) { | ||||
|                     window.Prism.highlightElement(block); | ||||
|                 if (languageClass && languageClass !== 'language-mermaid') { | ||||
|                     if (window.Prism) { | ||||
|                         window.Prism.highlightElement(block); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             // Step 5: Render mermaid | ||||
|             if (window.mermaid) { | ||||
|                 await window.mermaid.run(); | ||||
|             // 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('[Editor] Preview error:', error); | ||||
|             previewDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`; | ||||
|             console.error('Preview rendering error:', error); | ||||
|             previewDiv.innerHTML = ` | ||||
|                 <div class="alert alert-danger" role="alert"> | ||||
|                     <strong>Error rendering preview:</strong><br> | ||||
|                     ${error.message} | ||||
|                 </div> | ||||
|             `; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -16,102 +16,24 @@ class FileTree { | ||||
|     } | ||||
|      | ||||
|     setupEventListeners() { | ||||
|         // Click handler for tree nodes | ||||
|         this.container.addEventListener('click', (e) => { | ||||
|             console.log('Container clicked', e.target); | ||||
|             const node = e.target.closest('.tree-node'); | ||||
|             if (!node) return; | ||||
|              | ||||
|             console.log('Node found', node); | ||||
|             const path = node.dataset.path; | ||||
|             const isDir = node.dataset.isdir === 'true'; | ||||
|  | ||||
|             // If it's a directory, and the click was on the title, select the folder | ||||
|             if (isDir && e.target.classList.contains('tree-node-title')) { | ||||
|              | ||||
|             // The toggle is handled inside renderNodes now | ||||
|              | ||||
|             // Select node | ||||
|             if (isDir) { | ||||
|                 this.selectFolder(path); | ||||
|             } else if (!isDir) { // If it's a file, select the file | ||||
|             } else { | ||||
|                 this.selectFile(path); | ||||
|             } | ||||
|             // Clicks on the toggle are handled by the toggle's specific event listener | ||||
|         }); | ||||
|          | ||||
|         // DRAG AND DROP | ||||
|         this.container.addEventListener('dragstart', (e) => { | ||||
|             const node = e.target.closest('.tree-node'); | ||||
|             if (!node) return; | ||||
|              | ||||
|             const path = node.dataset.path; | ||||
|             const isDir = node.dataset.isdir === 'true'; | ||||
|              | ||||
|             console.log('[FileTree] Drag start:', path); | ||||
|              | ||||
|             e.dataTransfer.effectAllowed = 'move'; | ||||
|             e.dataTransfer.setData('text/plain', path); | ||||
|             e.dataTransfer.setData('application/json', JSON.stringify({ | ||||
|                 path, | ||||
|                 isDir, | ||||
|                 name: node.querySelector('.tree-node-title').textContent | ||||
|             })); | ||||
|              | ||||
|             node.classList.add('dragging'); | ||||
|             setTimeout(() => node.classList.remove('dragging'), 0); | ||||
|         }); | ||||
|          | ||||
|         this.container.addEventListener('dragover', (e) => { | ||||
|             const node = e.target.closest('.tree-node'); | ||||
|             if (!node) return; | ||||
|              | ||||
|             const isDir = node.dataset.isdir === 'true'; | ||||
|             if (!isDir) return; | ||||
|              | ||||
|             e.preventDefault(); | ||||
|             e.dataTransfer.dropEffect = 'move'; | ||||
|             node.classList.add('drag-over'); | ||||
|         }); | ||||
|          | ||||
|         this.container.addEventListener('dragleave', (e) => { | ||||
|             const node = e.target.closest('.tree-node'); | ||||
|             if (node) { | ||||
|                 node.classList.remove('drag-over'); | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         this.container.addEventListener('drop', async (e) => { | ||||
|             const targetNode = e.target.closest('.tree-node'); | ||||
|             if (!targetNode) return; | ||||
|              | ||||
|             e.preventDefault(); | ||||
|             e.stopPropagation(); | ||||
|              | ||||
|             const targetPath = targetNode.dataset.path; | ||||
|             const isDir = targetNode.dataset.isdir === 'true'; | ||||
|              | ||||
|             if (!isDir) { | ||||
|                 console.log('[FileTree] Target is not a directory'); | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             try { | ||||
|                 const data = JSON.parse(e.dataTransfer.getData('application/json')); | ||||
|                 const sourcePath = data.path; | ||||
|                 const sourceName = data.name; | ||||
|                  | ||||
|                 if (sourcePath === targetPath) { | ||||
|                     console.log('[FileTree] Source and target are same'); | ||||
|                     return; | ||||
|                 } | ||||
|                  | ||||
|                 const destPath = `${targetPath}/${sourceName}`.replace(/\/+/g, '/'); | ||||
|                  | ||||
|                 console.log('[FileTree] Moving:', sourcePath, '→', destPath); | ||||
|                  | ||||
|                 await this.webdavClient.move(sourcePath, destPath); | ||||
|                 await this.load(); | ||||
|                  | ||||
|                 showNotification(`Moved to ${targetNode.querySelector('.tree-node-title').textContent}`, 'success'); | ||||
|             } catch (error) { | ||||
|                 console.error('[FileTree] Drop error:', error); | ||||
|                 showNotification(`Failed to move: ${error.message}`, 'error'); | ||||
|             } finally { | ||||
|                 targetNode.classList.remove('drag-over'); | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         // Context menu | ||||
| @@ -120,9 +42,13 @@ class FileTree { | ||||
|             e.preventDefault(); | ||||
|  | ||||
|             if (node) { | ||||
|                 // Clicked on a node | ||||
|                 const path = node.dataset.path; | ||||
|                 const isDir = node.dataset.isdir === 'true'; | ||||
|                 window.showContextMenu(e.clientX, e.clientY, { path, isDir }); | ||||
|             } else if (e.target === this.container) { | ||||
|                 // Clicked on the empty space in the file tree container | ||||
|                 window.showContextMenu(e.clientX, e.clientY, { path: '', isDir: true }); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| @@ -165,8 +91,18 @@ class FileTree { | ||||
|                 nodeWrapper.appendChild(childContainer); | ||||
|  | ||||
|                 // Make toggle functional | ||||
|                 // The toggle functionality is already handled in renderNodes, no need to duplicate here. | ||||
|                 // Ensure the toggle's click event stops propagation to prevent the parent node's click from firing. | ||||
|                 const toggle = nodeElement.querySelector('.tree-node-toggle'); | ||||
|                 if (toggle) { | ||||
|                     toggle.addEventListener('click', (e) => { | ||||
|                         console.log('Toggle clicked', e.target); | ||||
|                         e.stopPropagation(); | ||||
|                         const isHidden = childContainer.style.display === 'none'; | ||||
|                         console.log('Is hidden?', isHidden); | ||||
|                         childContainer.style.display = isHidden ? 'block' : 'none'; | ||||
|                         toggle.innerHTML = isHidden ? '▼' : '▶'; | ||||
|                         toggle.classList.toggle('expanded'); | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             parentElement.appendChild(nodeWrapper); | ||||
|   | ||||
| @@ -5,7 +5,8 @@ | ||||
|  | ||||
| class MacroParser { | ||||
|     /** | ||||
|      * Extract macros with improved parsing | ||||
|      * Parse and extract all macros from content | ||||
|      * Returns array of { fullMatch, actor, method, params } | ||||
|      */ | ||||
|     static extractMacros(content) { | ||||
|         const macroRegex = /!!([\w.]+)\s*([\s\S]*?)(?=\n!!|\n#|$)/g; | ||||
| @@ -14,17 +15,17 @@ class MacroParser { | ||||
|          | ||||
|         while ((match = macroRegex.exec(content)) !== null) { | ||||
|             const fullMatch = match[0]; | ||||
|             const actionPart = match[1]; | ||||
|             const actionPart = match[1]; // e.g., "include" or "core.include" | ||||
|             const paramsPart = match[2]; | ||||
|              | ||||
|             // Parse action: "method" or "actor.method" | ||||
|             const [actor, method] = actionPart.includes('.') | ||||
|                 ? actionPart.split('.') | ||||
|                 : ['core', actionPart]; | ||||
|              | ||||
|             // Parse parameters from HeroScript-like syntax | ||||
|             const params = this.parseParams(paramsPart); | ||||
|              | ||||
|             console.log(`[MacroParser] Extracted: !!${actor}.${method}`, params); | ||||
|              | ||||
|             macros.push({ | ||||
|                 fullMatch: fullMatch.trim(), | ||||
|                 actor, | ||||
| @@ -39,13 +40,12 @@ class MacroParser { | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Parse HeroScript parameters with multiline support | ||||
|      * Supports: | ||||
|      *   key: 'value' | ||||
|      *   key: '''multiline value''' | ||||
|      *   key: | | ||||
|      *     multiline | ||||
|      *     value | ||||
|      * Parse HeroScript-style parameters | ||||
|      * key: value | ||||
|      * key: 'value with spaces' | ||||
|      * key: | | ||||
|      *   multiline | ||||
|      *   value | ||||
|      */ | ||||
|     static parseParams(paramsPart) { | ||||
|         const params = {}; | ||||
| @@ -54,95 +54,49 @@ class MacroParser { | ||||
|             return params; | ||||
|         } | ||||
|          | ||||
|         let lines = paramsPart.split('\n'); | ||||
|         let i = 0; | ||||
|         // Split by newlines but preserve multiline values | ||||
|         const lines = paramsPart.split('\n'); | ||||
|         let currentKey = null; | ||||
|         let currentValue = []; | ||||
|          | ||||
|         while (i < lines.length) { | ||||
|             const line = lines[i].trim(); | ||||
|         for (const line of lines) { | ||||
|             const trimmed = line.trim(); | ||||
|              | ||||
|             if (!line) { | ||||
|                 i++; | ||||
|                 continue; | ||||
|             } | ||||
|             if (!trimmed) continue; | ||||
|              | ||||
|             // Check for key: value pattern | ||||
|             if (line.includes(':')) { | ||||
|                 const colonIndex = line.indexOf(':'); | ||||
|                 const key = line.substring(0, colonIndex).trim(); | ||||
|                 let value = line.substring(colonIndex + 1).trim(); | ||||
|             // Check if this is a key: value line | ||||
|             if (trimmed.includes(':')) { | ||||
|                 // Save previous key-value | ||||
|                 if (currentKey) { | ||||
|                     params[currentKey] = currentValue.join('\n').trim(); | ||||
|                 } | ||||
|                  | ||||
|                 // Handle triple-quoted multiline | ||||
|                 if (value.startsWith("'''")) { | ||||
|                     value = value.substring(3); | ||||
|                     const valueLines = [value]; | ||||
|                     i++; | ||||
|                      | ||||
|                     while (i < lines.length) { | ||||
|                         const contentLine = lines[i]; | ||||
|                         if (contentLine.trim().endsWith("'''")) { | ||||
|                             valueLines.push(contentLine.trim().slice(0, -3)); | ||||
|                             break; | ||||
|                         } | ||||
|                         valueLines.push(contentLine); | ||||
|                         i++; | ||||
|                     } | ||||
|                      | ||||
|                     // Remove leading whitespace from multiline | ||||
|                     const processedValue = this.dedent(valueLines.join('\n')); | ||||
|                     params[key] = processedValue; | ||||
|                 } | ||||
|                 // Handle pipe multiline | ||||
|                 else if (value === '|') { | ||||
|                     const valueLines = []; | ||||
|                     i++; | ||||
|                      | ||||
|                     while (i < lines.length && lines[i].startsWith('\t')) { | ||||
|                         valueLines.push(lines[i].substring(1)); // Remove tab | ||||
|                         i++; | ||||
|                     } | ||||
|                     i--; // Back up one since loop will increment | ||||
|                      | ||||
|                     const processedValue = this.dedent(valueLines.join('\n')); | ||||
|                     params[key] = processedValue; | ||||
|                 } | ||||
|                 // Handle quoted value | ||||
|                 else if (value.startsWith("'") && value.endsWith("'")) { | ||||
|                     params[key] = value.slice(1, -1); | ||||
|                 } | ||||
|                 // Handle unquoted value | ||||
|                 else { | ||||
|                     params[key] = value; | ||||
|                 } | ||||
|                 const [key, ...valueParts] = trimmed.split(':'); | ||||
|                 currentKey = key.trim(); | ||||
|                 currentValue = [valueParts.join(':').trim()]; | ||||
|             } else if (currentKey) { | ||||
|                 // Continuation of multiline value | ||||
|                 currentValue.push(trimmed); | ||||
|             } | ||||
|              | ||||
|             i++; | ||||
|         } | ||||
|          | ||||
|         console.log(`[MacroParser] Parsed parameters:`, params); | ||||
|         // Save last key-value | ||||
|         if (currentKey) { | ||||
|             params[currentKey] = currentValue.join('\n').trim(); | ||||
|         } | ||||
|          | ||||
|         return params; | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Remove common leading whitespace from multiline strings | ||||
|      * Check if macro is valid | ||||
|      */ | ||||
|     static dedent(text) { | ||||
|         const lines = text.split('\n'); | ||||
|          | ||||
|         // Find minimum indentation | ||||
|         let minIndent = Infinity; | ||||
|         for (const line of lines) { | ||||
|             if (line.trim().length === 0) continue; | ||||
|             const indent = line.search(/\S/); | ||||
|             minIndent = Math.min(minIndent, indent); | ||||
|     static validateMacro(macro) { | ||||
|         if (!macro.actor || !macro.method) { | ||||
|             return { valid: false, error: 'Invalid macro format' }; | ||||
|         } | ||||
|          | ||||
|         if (minIndent === Infinity) minIndent = 0; | ||||
|          | ||||
|         // Remove common indentation | ||||
|         return lines | ||||
|             .map(line => line.slice(minIndent)) | ||||
|             .join('\n') | ||||
|             .trim(); | ||||
|         return { valid: true }; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,45 +6,53 @@ | ||||
| class MacroProcessor { | ||||
|     constructor(webdavClient) { | ||||
|         this.webdavClient = webdavClient; | ||||
|         this.macroRegistry = new MacroRegistry(); | ||||
|         this.includeStack = []; | ||||
|         this.faqItems = []; | ||||
|          | ||||
|         this.plugins = new Map(); | ||||
|         this.includeStack = []; // Track includes to detect cycles | ||||
|         this.registerDefaultPlugins(); | ||||
|         console.log('[MacroProcessor] Initialized'); | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Process all macros in markdown | ||||
|      * Register a macro plugin | ||||
|      * Plugin must implement: { canHandle(actor, method), process(macro, webdavClient) } | ||||
|      */ | ||||
|     registerPlugin(actor, method, plugin) { | ||||
|         const key = `${actor}.${method}`; | ||||
|         this.plugins.set(key, plugin); | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Process all macros in content | ||||
|      * Returns { success: boolean, content: string, errors: [] } | ||||
|      */ | ||||
|     async processMacros(content) { | ||||
|         console.log('[MacroProcessor] Processing content, length:', content.length); | ||||
|          | ||||
|         console.log('MacroProcessor: Starting macro processing for content:', content); | ||||
|         const macros = MacroParser.extractMacros(content); | ||||
|         console.log(`[MacroProcessor] Found ${macros.length} macros`); | ||||
|          | ||||
|         console.log('MacroProcessor: Extracted macros:', macros); | ||||
|         const errors = []; | ||||
|         let processedContent = content; | ||||
|         let faqOutput = ''; | ||||
|          | ||||
|         // Process in reverse to preserve positions | ||||
|         // Process macros in reverse order to preserve positions | ||||
|         for (let i = macros.length - 1; i >= 0; i--) { | ||||
|             const macro = macros[i]; | ||||
|             console.log(`[MacroProcessor] Processing macro ${i}:`, macro.actor, macro.method); | ||||
|             console.log('MacroProcessor: Processing macro:', macro); | ||||
|              | ||||
|             try { | ||||
|                 const result = await this.processMacro(macro); | ||||
|                 console.log('MacroProcessor: Macro processing result:', result); | ||||
|                  | ||||
|                 if (result.success) { | ||||
|                     console.log(`[MacroProcessor] Macro succeeded, replacing content`); | ||||
|                     // Replace macro with result | ||||
|                     processedContent = | ||||
|                         processedContent.substring(0, macro.start) + | ||||
|                         result.content + | ||||
|                         processedContent.substring(macro.end); | ||||
|                 } else { | ||||
|                     console.error(`[MacroProcessor] Macro failed:`, result.error); | ||||
|                     errors.push({ macro: macro.fullMatch, error: result.error }); | ||||
|                     errors.push({ | ||||
|                         macro: macro.fullMatch, | ||||
|                         error: result.error | ||||
|                     }); | ||||
|                      | ||||
|                     // Replace with error message | ||||
|                     const errorMsg = `\n\n⚠️ **Macro Error**: ${result.error}\n\n`; | ||||
|                     processedContent = | ||||
|                         processedContent.substring(0, macro.start) + | ||||
| @@ -52,10 +60,12 @@ class MacroProcessor { | ||||
|                         processedContent.substring(macro.end); | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 console.error(`[MacroProcessor] Macro exception:`, error); | ||||
|                 errors.push({ macro: macro.fullMatch, error: error.message }); | ||||
|                 errors.push({ | ||||
|                     macro: macro.fullMatch, | ||||
|                     error: error.message | ||||
|                 }); | ||||
|                  | ||||
|                 const errorMsg = `\n\n⚠️ **Macro Exception**: ${error.message}\n\n`; | ||||
|                 const errorMsg = `\n\n⚠️ **Macro Error**: ${error.message}\n\n`; | ||||
|                 processedContent = | ||||
|                     processedContent.substring(0, macro.start) + | ||||
|                     errorMsg + | ||||
| @@ -63,17 +73,7 @@ class MacroProcessor { | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Append FAQ if any FAQ macros were used | ||||
|         if (this.faqItems.length > 0) { | ||||
|             faqOutput = '\n\n---\n\n## FAQ\n\n'; | ||||
|             faqOutput += this.faqItems | ||||
|                 .map((item, idx) => `### ${idx + 1}. ${item.title}\n\n${item.response}`) | ||||
|                 .join('\n\n'); | ||||
|             processedContent += faqOutput; | ||||
|         } | ||||
|          | ||||
|         console.log('[MacroProcessor] Processing complete, errors:', errors.length); | ||||
|          | ||||
|         console.log('MacroProcessor: Final processed content:', processedContent); | ||||
|         return { | ||||
|             success: errors.length === 0, | ||||
|             content: processedContent, | ||||
| @@ -85,30 +85,37 @@ class MacroProcessor { | ||||
|      * Process single macro | ||||
|      */ | ||||
|     async processMacro(macro) { | ||||
|         const plugin = this.macroRegistry.resolve(macro.actor, macro.method); | ||||
|          | ||||
|         if (!plugin) { | ||||
|             return { | ||||
|                 success: false, | ||||
|                 error: `Unknown macro: !!${macro.actor}.${macro.method}` | ||||
|             }; | ||||
|         } | ||||
|          | ||||
|         const key = `${macro.actor}.${macro.method}`; | ||||
|         const plugin = this.plugins.get(key); | ||||
|  | ||||
|         // Check for circular includes | ||||
|         if (macro.method === 'include') { | ||||
|             const path = macro.params.path; | ||||
|             const path = macro.params.path || macro.params['']; | ||||
|             if (this.includeStack.includes(path)) { | ||||
|                 return { | ||||
|                     success: false, | ||||
|                     error: `Circular include: ${this.includeStack.join(' → ')} → ${path}` | ||||
|                     error: `Circular include detected: ${this.includeStack.join(' → ')} → ${path}` | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         if (!plugin) { | ||||
|             return { | ||||
|                 success: false, | ||||
|                 error: `Unknown macro: !!${key}` | ||||
|             }; | ||||
|         } | ||||
|          | ||||
|         // Validate macro | ||||
|         const validation = MacroParser.validateMacro(macro); | ||||
|         if (!validation.valid) { | ||||
|             return { success: false, error: validation.error }; | ||||
|         } | ||||
|          | ||||
|         // Execute plugin | ||||
|         try { | ||||
|             return await plugin.process(macro, this.webdavClient); | ||||
|         } catch (error) { | ||||
|             console.error('[MacroProcessor] Plugin error:', error); | ||||
|             return { | ||||
|                 success: false, | ||||
|                 error: `Plugin error: ${error.message}` | ||||
| @@ -121,12 +128,34 @@ class MacroProcessor { | ||||
|      */ | ||||
|     registerDefaultPlugins() { | ||||
|         // Include plugin | ||||
|         this.macroRegistry.register('core', 'include', new IncludePlugin(this)); | ||||
|          | ||||
|         // FAQ plugin | ||||
|         this.macroRegistry.register('core', 'faq', new FAQPlugin(this)); | ||||
|          | ||||
|         console.log('[MacroProcessor] Registered default plugins'); | ||||
|         this.registerPlugin('core', 'include', { | ||||
|             process: async (macro, webdavClient) => { | ||||
|                 const path = macro.params.path || macro.params['']; | ||||
|                  | ||||
|                 if (!path) { | ||||
|                     return { | ||||
|                         success: false, | ||||
|                         error: 'include macro requires "path" parameter' | ||||
|                     }; | ||||
|                 } | ||||
|                  | ||||
|                 try { | ||||
|                     // Add to include stack | ||||
|                     this.includeStack.push(path); | ||||
|                     const content = await webdavClient.includeFile(path); | ||||
|                     // Remove from include stack | ||||
|                     this.includeStack.pop(); | ||||
|                     return { success: true, content }; | ||||
|                 } catch (error) { | ||||
|                     // Remove from include stack on error | ||||
|                     this.includeStack.pop(); | ||||
|                     return { | ||||
|                         success: false, | ||||
|                         error: `Failed to include "${path}": ${error.message}` | ||||
|                     }; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,50 +0,0 @@ | ||||
| /** | ||||
|  * Macro System | ||||
|  * Generic plugin-based macro processor | ||||
|  */ | ||||
|  | ||||
| class MacroPlugin { | ||||
|     /** | ||||
|      * Base class for macro plugins | ||||
|      * Subclass and implement these methods: | ||||
|      * - canHandle(actor, method): boolean | ||||
|      * - process(macro, context): Promise<{ success, content, error }> | ||||
|      */ | ||||
|      | ||||
|     canHandle(actor, method) { | ||||
|         throw new Error('Must implement canHandle()'); | ||||
|     } | ||||
|      | ||||
|     async process(macro, context) { | ||||
|         throw new Error('Must implement process()'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| class MacroRegistry { | ||||
|     constructor() { | ||||
|         this.plugins = new Map(); | ||||
|         console.log('[MacroRegistry] Initializing macro registry'); | ||||
|     } | ||||
|      | ||||
|     register(actor, method, plugin) { | ||||
|         const key = `${actor}.${method}`; | ||||
|         this.plugins.set(key, plugin); | ||||
|         console.log(`[MacroRegistry] Registered plugin: ${key}`); | ||||
|     } | ||||
|      | ||||
|     resolve(actor, method) { | ||||
|         // Try exact match | ||||
|         let key = `${actor}.${method}`; | ||||
|         if (this.plugins.has(key)) { | ||||
|             console.log(`[MacroRegistry] Found plugin: ${key}`); | ||||
|             return this.plugins.get(key); | ||||
|         } | ||||
|          | ||||
|         // No plugin found | ||||
|         console.warn(`[MacroRegistry] No plugin found for: ${key}`); | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | ||||
| window.MacroRegistry = MacroRegistry; | ||||
| window.MacroPlugin = MacroPlugin; | ||||
| @@ -1,70 +0,0 @@ | ||||
| /** | ||||
|  * FAQ Plugin | ||||
|  * Creates FAQ entries that are collected and displayed at bottom of preview | ||||
|  *  | ||||
|  * Usage: | ||||
|  *   !!faq title: 'My Question' | ||||
|  *       response: ''' | ||||
|  *           This is the answer with **markdown** support. | ||||
|  *            | ||||
|  *           - Point 1 | ||||
|  *           - Point 2 | ||||
|  *       ''' | ||||
|  */ | ||||
| class FAQPlugin extends MacroPlugin { | ||||
|     constructor(processor) { | ||||
|         super(); | ||||
|         this.processor = processor; | ||||
|     } | ||||
|      | ||||
|     canHandle(actor, method) { | ||||
|         return actor === 'core' && method === 'faq'; | ||||
|     } | ||||
|      | ||||
|     async process(macro, webdavClient) { | ||||
|         const title = macro.params.title; | ||||
|         const response = macro.params.response; | ||||
|          | ||||
|         console.log('[FAQPlugin] Processing FAQ:', title); | ||||
|          | ||||
|         if (!title) { | ||||
|             console.error('[FAQPlugin] Missing title parameter'); | ||||
|             return { | ||||
|                 success: false, | ||||
|                 error: 'FAQ macro requires "title" parameter' | ||||
|             }; | ||||
|         } | ||||
|          | ||||
|         if (!response) { | ||||
|             console.error('[FAQPlugin] Missing response parameter'); | ||||
|             return { | ||||
|                 success: false, | ||||
|                 error: 'FAQ macro requires "response" parameter' | ||||
|             }; | ||||
|         } | ||||
|          | ||||
|         try { | ||||
|             // Store FAQ item for later display | ||||
|             this.processor.faqItems.push({ | ||||
|                 title: title.trim(), | ||||
|                 response: response.trim() | ||||
|             }); | ||||
|              | ||||
|             console.log('[FAQPlugin] FAQ item added, total:', this.processor.faqItems.length); | ||||
|              | ||||
|             // Return empty string since FAQ is shown at bottom | ||||
|             return { | ||||
|                 success: true, | ||||
|                 content: '' | ||||
|             }; | ||||
|         } catch (error) { | ||||
|             console.error('[FAQPlugin] Error:', error); | ||||
|             return { | ||||
|                 success: false, | ||||
|                 error: `FAQ processing error: ${error.message}` | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| window.FAQPlugin = FAQPlugin; | ||||
| @@ -1,97 +0,0 @@ | ||||
| /** | ||||
|  * Include Plugin | ||||
|  * Includes content from other files | ||||
|  *  | ||||
|  * Usage: | ||||
|  *   !!include path: 'myfile.md' | ||||
|  *   !!include path: 'collection:folder/file.md' | ||||
|  */ | ||||
| class IncludePlugin extends MacroPlugin { | ||||
|     constructor(processor) { | ||||
|         super(); | ||||
|         this.processor = processor; | ||||
|     } | ||||
|      | ||||
|     canHandle(actor, method) { | ||||
|         return actor === 'core' && method === 'include'; | ||||
|     } | ||||
|      | ||||
|     async process(macro, webdavClient) { | ||||
|         const path = macro.params.path; | ||||
|          | ||||
|         console.log('[IncludePlugin] Processing include:', path); | ||||
|          | ||||
|         if (!path) { | ||||
|             console.error('[IncludePlugin] Missing path parameter'); | ||||
|             return { | ||||
|                 success: false, | ||||
|                 error: 'Include macro requires "path" parameter' | ||||
|             }; | ||||
|         } | ||||
|          | ||||
|         try { | ||||
|             // Parse path format: "collection:path/to/file" or "path/to/file" | ||||
|             let targetCollection = webdavClient.currentCollection; | ||||
|             let targetPath = path; | ||||
|              | ||||
|             if (path.includes(':')) { | ||||
|                 [targetCollection, targetPath] = path.split(':', 2); | ||||
|                 console.log('[IncludePlugin] Using external collection:', targetCollection); | ||||
|             } else { | ||||
|                 console.log('[IncludePlugin] Using current collection:', targetCollection); | ||||
|             } | ||||
|              | ||||
|             // Check for circular includes | ||||
|             const fullPath = `${targetCollection}:${targetPath}`; | ||||
|             if (this.processor.includeStack.includes(fullPath)) { | ||||
|                 console.error('[IncludePlugin] Circular include detected'); | ||||
|                 return { | ||||
|                     success: false, | ||||
|                     error: `Circular include detected: ${this.processor.includeStack.join(' → ')} → ${fullPath}` | ||||
|                 }; | ||||
|             } | ||||
|              | ||||
|             // Add to include stack | ||||
|             this.processor.includeStack.push(fullPath); | ||||
|              | ||||
|             // Switch collection temporarily | ||||
|             const originalCollection = webdavClient.currentCollection; | ||||
|             webdavClient.setCollection(targetCollection); | ||||
|              | ||||
|             // Fetch file | ||||
|             console.log('[IncludePlugin] Fetching:', targetPath); | ||||
|             const content = await webdavClient.get(targetPath); | ||||
|              | ||||
|             // Restore collection | ||||
|             webdavClient.setCollection(originalCollection); | ||||
|              | ||||
|             // Remove from stack | ||||
|             this.processor.includeStack.pop(); | ||||
|              | ||||
|             console.log('[IncludePlugin] Include successful, length:', content.length); | ||||
|              | ||||
|             return { | ||||
|                 success: true, | ||||
|                 content: content | ||||
|             }; | ||||
|         } catch (error) { | ||||
|             console.error('[IncludePlugin] Error:', error); | ||||
|              | ||||
|             // Restore collection on error | ||||
|             if (webdavClient.currentCollection !== this.processor.webdavClient?.currentCollection) { | ||||
|                 webdavClient.setCollection(this.processor.webdavClient?.currentCollection); | ||||
|             } | ||||
|              | ||||
|             this.processor.includeStack = this.processor.includeStack.filter( | ||||
|                 item => !item.includes(path) | ||||
|             ); | ||||
|              | ||||
|             return { | ||||
|                 success: false, | ||||
|                 error: `Cannot include "${path}": ${error.message}` | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| window.IncludePlugin = IncludePlugin; | ||||
| @@ -186,12 +186,8 @@ | ||||
|     <script src="/static/js/file-tree-actions.js" defer></script> | ||||
|     <script src="/static/js/column-resizer.js" defer></script> | ||||
|     <script src="/static/js/app.js" defer></script> | ||||
|     <!-- Macro System --> | ||||
|     <script src="/static/js/macro-system.js" defer></script> | ||||
|     <script src="/static/js/plugins/include-plugin.js" defer></script> | ||||
|     <script src="/static/js/plugins/faq-plugin.js" defer></script> | ||||
|     <script src="/static/js/macro-parser.js" defer></script> | ||||
|     <script src="/static/js/macro-processor.js" defer></script> | ||||
|    <script src="/static/js/macro-parser.js" defer></script> | ||||
|    <script src="/static/js/macro-processor.js" defer></script> | ||||
| </body> | ||||
|  | ||||
| </html> | ||||
		Reference in New Issue
	
	Block a user