Compare commits
	
		
			1 Commits
		
	
	
		
			23a24d42e2
			...
			dragdrop_m
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8750e0af39 | 
							
								
								
									
										5
									
								
								start.sh
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								start.sh
									
									
									
									
									
								
							| @@ -19,5 +19,8 @@ echo "Activating virtual environment..." | |||||||
| source .venv/bin/activate | source .venv/bin/activate | ||||||
| echo "Installing dependencies..." | echo "Installing dependencies..." | ||||||
| uv pip install wsgidav cheroot pyyaml | uv pip install wsgidav cheroot pyyaml | ||||||
| echo "Starting WebDAV server on port $PORT..." | PORT=8004 | ||||||
|  | echo "Checking for process on port $PORT..." | ||||||
|  | lsof -ti:$PORT | xargs -r kill -9 | ||||||
|  | echo "Starting WebDAV server..." | ||||||
| python server_webdav.py | python server_webdav.py | ||||||
|   | |||||||
| @@ -111,14 +111,19 @@ | |||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* Drag and drop */ | .tree-node { | ||||||
|  |     cursor: move; | ||||||
|  | } | ||||||
|  |  | ||||||
| .tree-node.dragging { | .tree-node.dragging { | ||||||
|     opacity: 0.5; |     opacity: 0.5; | ||||||
|  |     background-color: rgba(13, 110, 253, 0.1); | ||||||
| } | } | ||||||
|  |  | ||||||
| .tree-node.drag-over { | .tree-node.drag-over { | ||||||
|     background-color: rgba(13, 110, 253, 0.2); |     background-color: rgba(13, 110, 253, 0.2) !important; | ||||||
|     border: 1px dashed var(--link-color); |     border: 1px dashed var(--link-color); | ||||||
|  |     border-radius: 4px; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* Collection selector - Bootstrap styled */ | /* Collection selector - Bootstrap styled */ | ||||||
|   | |||||||
| @@ -10,7 +10,9 @@ class MarkdownEditor { | |||||||
|         this.filenameInput = document.getElementById(filenameInputId); |         this.filenameInput = document.getElementById(filenameInputId); | ||||||
|         this.currentFile = null; |         this.currentFile = null; | ||||||
|         this.webdavClient = null; |         this.webdavClient = null; | ||||||
|         this.macroProcessor = new MacroProcessor(null); // Will be set later |          | ||||||
|  |         // Initialize macro processor AFTER webdavClient is set | ||||||
|  |         this.macroProcessor = null; | ||||||
|          |          | ||||||
|         this.initCodeMirror(); |         this.initCodeMirror(); | ||||||
|         this.initMarkdown(); |         this.initMarkdown(); | ||||||
| @@ -88,10 +90,8 @@ class MarkdownEditor { | |||||||
|     setWebDAVClient(client) { |     setWebDAVClient(client) { | ||||||
|         this.webdavClient = client; |         this.webdavClient = client; | ||||||
|          |          | ||||||
|         // Update macro processor with client |         // NOW initialize macro processor | ||||||
|         if (this.macroProcessor) { |         this.macroProcessor = new MacroProcessor(client); | ||||||
|             this.macroProcessor.webdavClient = client; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -196,38 +196,29 @@ class MarkdownEditor { | |||||||
|         const previewDiv = this.previewElement; |         const previewDiv = this.previewElement; | ||||||
|  |  | ||||||
|         if (!markdown || !markdown.trim()) { |         if (!markdown || !markdown.trim()) { | ||||||
|             previewDiv.innerHTML = ` |             previewDiv.innerHTML = `<div class="text-muted">Start typing...</div>`; | ||||||
|                 <div class="text-muted text-center mt-5"> |  | ||||||
|                     <p>Start typing to see preview...</p> |  | ||||||
|                 </div> |  | ||||||
|             `; |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             // Step 1: Process macros |             // Step 1: Process macros | ||||||
|  |             console.log('[Editor] Processing macros...'); | ||||||
|             let processedContent = markdown; |             let processedContent = markdown; | ||||||
|              |              | ||||||
|             if (this.macroProcessor) { |             if (this.macroProcessor) { | ||||||
|                 const processingResult = await this.macroProcessor.processMacros(markdown); |                 const result = await this.macroProcessor.processMacros(markdown); | ||||||
|                 processedContent = processingResult.content; |                 processedContent = result.content; | ||||||
|                  |                  | ||||||
|                 // Log errors if any |                 if (result.errors.length > 0) { | ||||||
|                 if (processingResult.errors.length > 0) { |                     console.warn('[Editor] Macro errors:', result.errors); | ||||||
|                     console.warn('Macro processing errors:', processingResult.errors); |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             // Step 2: Parse markdown to HTML |             // Step 2: Parse markdown | ||||||
|             if (!this.marked) { |             console.log('[Editor] Parsing markdown...'); | ||||||
|                 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); |             let html = this.marked.parse(processedContent); | ||||||
|  |  | ||||||
|             // Replace mermaid code blocks |             // Step 3: Handle mermaid | ||||||
|             html = html.replace( |             html = html.replace( | ||||||
|                 /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, |                 /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, | ||||||
|                 (match, code) => { |                 (match, code) => { | ||||||
| @@ -238,35 +229,23 @@ class MarkdownEditor { | |||||||
|  |  | ||||||
|             previewDiv.innerHTML = html; |             previewDiv.innerHTML = html; | ||||||
|  |  | ||||||
|             // Apply syntax highlighting |             // Step 4: Syntax highlighting | ||||||
|             const codeBlocks = previewDiv.querySelectorAll('pre code'); |             const codeBlocks = previewDiv.querySelectorAll('pre code'); | ||||||
|             codeBlocks.forEach(block => { |             codeBlocks.forEach(block => { | ||||||
|                 const languageClass = Array.from(block.classList) |                 const lang = Array.from(block.classList) | ||||||
|                     .find(cls => cls.startsWith('language-')); |                     .find(cls => cls.startsWith('language-')); | ||||||
|                 if (languageClass && languageClass !== 'language-mermaid') { |                 if (lang && lang !== 'language-mermaid' && window.Prism) { | ||||||
|                     if (window.Prism) { |                     window.Prism.highlightElement(block); | ||||||
|                         window.Prism.highlightElement(block); |  | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             // Render mermaid diagrams |             // Step 5: Render mermaid | ||||||
|             const mermaidElements = previewDiv.querySelectorAll('.mermaid'); |             if (window.mermaid) { | ||||||
|             if (mermaidElements.length > 0 && window.mermaid) { |                 await window.mermaid.run(); | ||||||
|                 try { |  | ||||||
|                     window.mermaid.contentLoaded(); |  | ||||||
|                 } catch (error) { |  | ||||||
|                     console.warn('Mermaid rendering error:', error); |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             console.error('Preview rendering error:', error); |             console.error('[Editor] Preview error:', error); | ||||||
|             previewDiv.innerHTML = ` |             previewDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`; | ||||||
|                 <div class="alert alert-danger" role="alert"> |  | ||||||
|                     <strong>Error rendering preview:</strong><br> |  | ||||||
|                     ${error.message} |  | ||||||
|                 </div> |  | ||||||
|             `; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,23 +16,101 @@ class FileTree { | |||||||
|     } |     } | ||||||
|      |      | ||||||
|     setupEventListeners() { |     setupEventListeners() { | ||||||
|         // Click handler for tree nodes |  | ||||||
|         this.container.addEventListener('click', (e) => { |         this.container.addEventListener('click', (e) => { | ||||||
|             console.log('Container clicked', e.target); |  | ||||||
|             const node = e.target.closest('.tree-node'); |             const node = e.target.closest('.tree-node'); | ||||||
|             if (!node) return; |             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')) { | ||||||
|  |                 this.selectFolder(path); | ||||||
|  |             } else if (!isDir) { // If it's a file, select the file | ||||||
|  |                 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 path = node.dataset.path; | ||||||
|             const isDir = node.dataset.isdir === 'true'; |             const isDir = node.dataset.isdir === 'true'; | ||||||
|              |              | ||||||
|             // The toggle is handled inside renderNodes now |             console.log('[FileTree] Drag start:', path); | ||||||
|              |              | ||||||
|             // Select node |             e.dataTransfer.effectAllowed = 'move'; | ||||||
|             if (isDir) { |             e.dataTransfer.setData('text/plain', path); | ||||||
|                 this.selectFolder(path); |             e.dataTransfer.setData('application/json', JSON.stringify({ | ||||||
|             } else { |                 path, | ||||||
|                 this.selectFile(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'); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|          |          | ||||||
| @@ -42,13 +120,9 @@ class FileTree { | |||||||
|             e.preventDefault(); |             e.preventDefault(); | ||||||
|  |  | ||||||
|             if (node) { |             if (node) { | ||||||
|                 // Clicked on a node |  | ||||||
|                 const path = node.dataset.path; |                 const path = node.dataset.path; | ||||||
|                 const isDir = node.dataset.isdir === 'true'; |                 const isDir = node.dataset.isdir === 'true'; | ||||||
|                 window.showContextMenu(e.clientX, e.clientY, { path, isDir }); |                 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 }); |  | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| @@ -91,18 +165,8 @@ class FileTree { | |||||||
|                 nodeWrapper.appendChild(childContainer); |                 nodeWrapper.appendChild(childContainer); | ||||||
|  |  | ||||||
|                 // Make toggle functional |                 // Make toggle functional | ||||||
|                 const toggle = nodeElement.querySelector('.tree-node-toggle'); |                 // The toggle functionality is already handled in renderNodes, no need to duplicate here. | ||||||
|                 if (toggle) { |                 // Ensure the toggle's click event stops propagation to prevent the parent node's click from firing. | ||||||
|                     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); |             parentElement.appendChild(nodeWrapper); | ||||||
|   | |||||||
| @@ -5,8 +5,7 @@ | |||||||
|  |  | ||||||
| class MacroParser { | class MacroParser { | ||||||
|     /** |     /** | ||||||
|      * Parse and extract all macros from content |      * Extract macros with improved parsing | ||||||
|      * Returns array of { fullMatch, actor, method, params } |  | ||||||
|      */ |      */ | ||||||
|     static extractMacros(content) { |     static extractMacros(content) { | ||||||
|         const macroRegex = /!!([\w.]+)\s*([\s\S]*?)(?=\n!!|\n#|$)/g; |         const macroRegex = /!!([\w.]+)\s*([\s\S]*?)(?=\n!!|\n#|$)/g; | ||||||
| @@ -15,17 +14,17 @@ class MacroParser { | |||||||
|          |          | ||||||
|         while ((match = macroRegex.exec(content)) !== null) { |         while ((match = macroRegex.exec(content)) !== null) { | ||||||
|             const fullMatch = match[0]; |             const fullMatch = match[0]; | ||||||
|             const actionPart = match[1]; // e.g., "include" or "core.include" |             const actionPart = match[1]; | ||||||
|             const paramsPart = match[2]; |             const paramsPart = match[2]; | ||||||
|              |              | ||||||
|             // Parse action: "method" or "actor.method" |  | ||||||
|             const [actor, method] = actionPart.includes('.') |             const [actor, method] = actionPart.includes('.') | ||||||
|                 ? actionPart.split('.') |                 ? actionPart.split('.') | ||||||
|                 : ['core', actionPart]; |                 : ['core', actionPart]; | ||||||
|              |              | ||||||
|             // Parse parameters from HeroScript-like syntax |  | ||||||
|             const params = this.parseParams(paramsPart); |             const params = this.parseParams(paramsPart); | ||||||
|              |              | ||||||
|  |             console.log(`[MacroParser] Extracted: !!${actor}.${method}`, params); | ||||||
|  |              | ||||||
|             macros.push({ |             macros.push({ | ||||||
|                 fullMatch: fullMatch.trim(), |                 fullMatch: fullMatch.trim(), | ||||||
|                 actor, |                 actor, | ||||||
| @@ -40,12 +39,13 @@ class MacroParser { | |||||||
|     } |     } | ||||||
|      |      | ||||||
|     /** |     /** | ||||||
|      * Parse HeroScript-style parameters |      * Parse HeroScript parameters with multiline support | ||||||
|      * key: value |      * Supports: | ||||||
|      * key: 'value with spaces' |      *   key: 'value' | ||||||
|      * key: | |      *   key: '''multiline value''' | ||||||
|      *   multiline |      *   key: | | ||||||
|      *   value |      *     multiline | ||||||
|  |      *     value | ||||||
|      */ |      */ | ||||||
|     static parseParams(paramsPart) { |     static parseParams(paramsPart) { | ||||||
|         const params = {}; |         const params = {}; | ||||||
| @@ -54,49 +54,95 @@ class MacroParser { | |||||||
|             return params; |             return params; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         // Split by newlines but preserve multiline values |         let lines = paramsPart.split('\n'); | ||||||
|         const lines = paramsPart.split('\n'); |         let i = 0; | ||||||
|         let currentKey = null; |  | ||||||
|         let currentValue = []; |  | ||||||
|          |          | ||||||
|         for (const line of lines) { |         while (i < lines.length) { | ||||||
|             const trimmed = line.trim(); |             const line = lines[i].trim(); | ||||||
|              |              | ||||||
|             if (!trimmed) continue; |             if (!line) { | ||||||
|              |                 i++; | ||||||
|             // Check if this is a key: value line |                 continue; | ||||||
|             if (trimmed.includes(':')) { |  | ||||||
|                 // Save previous key-value |  | ||||||
|                 if (currentKey) { |  | ||||||
|                     params[currentKey] = currentValue.join('\n').trim(); |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 const [key, ...valueParts] = trimmed.split(':'); |  | ||||||
|                 currentKey = key.trim(); |  | ||||||
|                 currentValue = [valueParts.join(':').trim()]; |  | ||||||
|             } else if (currentKey) { |  | ||||||
|                 // Continuation of multiline value |  | ||||||
|                 currentValue.push(trimmed); |  | ||||||
|             } |             } | ||||||
|  |              | ||||||
|  |             // 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(); | ||||||
|  |                  | ||||||
|  |                 // 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; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             i++; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         // Save last key-value |         console.log(`[MacroParser] Parsed parameters:`, params); | ||||||
|         if (currentKey) { |  | ||||||
|             params[currentKey] = currentValue.join('\n').trim(); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         return params; |         return params; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     /** |     /** | ||||||
|      * Check if macro is valid |      * Remove common leading whitespace from multiline strings | ||||||
|      */ |      */ | ||||||
|     static validateMacro(macro) { |     static dedent(text) { | ||||||
|         if (!macro.actor || !macro.method) { |         const lines = text.split('\n'); | ||||||
|             return { valid: false, error: 'Invalid macro format' }; |          | ||||||
|  |         // 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); | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         return { valid: true }; |         if (minIndent === Infinity) minIndent = 0; | ||||||
|  |          | ||||||
|  |         // Remove common indentation | ||||||
|  |         return lines | ||||||
|  |             .map(line => line.slice(minIndent)) | ||||||
|  |             .join('\n') | ||||||
|  |             .trim(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,53 +6,45 @@ | |||||||
| class MacroProcessor { | class MacroProcessor { | ||||||
|     constructor(webdavClient) { |     constructor(webdavClient) { | ||||||
|         this.webdavClient = webdavClient; |         this.webdavClient = webdavClient; | ||||||
|         this.plugins = new Map(); |         this.macroRegistry = new MacroRegistry(); | ||||||
|         this.includeStack = []; // Track includes to detect cycles |         this.includeStack = []; | ||||||
|  |         this.faqItems = []; | ||||||
|  |          | ||||||
|         this.registerDefaultPlugins(); |         this.registerDefaultPlugins(); | ||||||
|  |         console.log('[MacroProcessor] Initialized'); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     /** |     /** | ||||||
|      * Register a macro plugin |      * Process all macros in markdown | ||||||
|      * 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) { |     async processMacros(content) { | ||||||
|         console.log('MacroProcessor: Starting macro processing for content:', content); |         console.log('[MacroProcessor] Processing content, length:', content.length); | ||||||
|  |          | ||||||
|         const macros = MacroParser.extractMacros(content); |         const macros = MacroParser.extractMacros(content); | ||||||
|         console.log('MacroProcessor: Extracted macros:', macros); |         console.log(`[MacroProcessor] Found ${macros.length} macros`); | ||||||
|  |          | ||||||
|         const errors = []; |         const errors = []; | ||||||
|         let processedContent = content; |         let processedContent = content; | ||||||
|  |         let faqOutput = ''; | ||||||
|          |          | ||||||
|         // Process macros in reverse order to preserve positions |         // Process in reverse to preserve positions | ||||||
|         for (let i = macros.length - 1; i >= 0; i--) { |         for (let i = macros.length - 1; i >= 0; i--) { | ||||||
|             const macro = macros[i]; |             const macro = macros[i]; | ||||||
|             console.log('MacroProcessor: Processing macro:', macro); |             console.log(`[MacroProcessor] Processing macro ${i}:`, macro.actor, macro.method); | ||||||
|              |              | ||||||
|             try { |             try { | ||||||
|                 const result = await this.processMacro(macro); |                 const result = await this.processMacro(macro); | ||||||
|                 console.log('MacroProcessor: Macro processing result:', result); |  | ||||||
|                  |                  | ||||||
|                 if (result.success) { |                 if (result.success) { | ||||||
|                     // Replace macro with result |                     console.log(`[MacroProcessor] Macro succeeded, replacing content`); | ||||||
|                     processedContent = |                     processedContent = | ||||||
|                         processedContent.substring(0, macro.start) + |                         processedContent.substring(0, macro.start) + | ||||||
|                         result.content + |                         result.content + | ||||||
|                         processedContent.substring(macro.end); |                         processedContent.substring(macro.end); | ||||||
|                 } else { |                 } else { | ||||||
|                     errors.push({ |                     console.error(`[MacroProcessor] Macro failed:`, result.error); | ||||||
|                         macro: macro.fullMatch, |                     errors.push({ macro: macro.fullMatch, error: result.error }); | ||||||
|                         error: result.error |  | ||||||
|                     }); |  | ||||||
|                      |                      | ||||||
|                     // Replace with error message |  | ||||||
|                     const errorMsg = `\n\n⚠️ **Macro Error**: ${result.error}\n\n`; |                     const errorMsg = `\n\n⚠️ **Macro Error**: ${result.error}\n\n`; | ||||||
|                     processedContent = |                     processedContent = | ||||||
|                         processedContent.substring(0, macro.start) + |                         processedContent.substring(0, macro.start) + | ||||||
| @@ -60,12 +52,10 @@ class MacroProcessor { | |||||||
|                         processedContent.substring(macro.end); |                         processedContent.substring(macro.end); | ||||||
|                 } |                 } | ||||||
|             } catch (error) { |             } catch (error) { | ||||||
|                 errors.push({ |                 console.error(`[MacroProcessor] Macro exception:`, error); | ||||||
|                     macro: macro.fullMatch, |                 errors.push({ macro: macro.fullMatch, error: error.message }); | ||||||
|                     error: error.message |  | ||||||
|                 }); |  | ||||||
|                  |                  | ||||||
|                 const errorMsg = `\n\n⚠️ **Macro Error**: ${error.message}\n\n`; |                 const errorMsg = `\n\n⚠️ **Macro Exception**: ${error.message}\n\n`; | ||||||
|                 processedContent = |                 processedContent = | ||||||
|                     processedContent.substring(0, macro.start) + |                     processedContent.substring(0, macro.start) + | ||||||
|                     errorMsg + |                     errorMsg + | ||||||
| @@ -73,7 +63,17 @@ class MacroProcessor { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         console.log('MacroProcessor: Final processed content:', processedContent); |         // 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); | ||||||
|  |          | ||||||
|         return { |         return { | ||||||
|             success: errors.length === 0, |             success: errors.length === 0, | ||||||
|             content: processedContent, |             content: processedContent, | ||||||
| @@ -85,37 +85,30 @@ class MacroProcessor { | |||||||
|      * Process single macro |      * Process single macro | ||||||
|      */ |      */ | ||||||
|     async processMacro(macro) { |     async processMacro(macro) { | ||||||
|         const key = `${macro.actor}.${macro.method}`; |         const plugin = this.macroRegistry.resolve(macro.actor, macro.method); | ||||||
|         const plugin = this.plugins.get(key); |  | ||||||
|  |  | ||||||
|         // Check for circular includes |  | ||||||
|         if (macro.method === 'include') { |  | ||||||
|             const path = macro.params.path || macro.params['']; |  | ||||||
|             if (this.includeStack.includes(path)) { |  | ||||||
|                 return { |  | ||||||
|                     success: false, |  | ||||||
|                     error: `Circular include detected: ${this.includeStack.join(' → ')} → ${path}` |  | ||||||
|                 }; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|          |          | ||||||
|         if (!plugin) { |         if (!plugin) { | ||||||
|             return { |             return { | ||||||
|                 success: false, |                 success: false, | ||||||
|                 error: `Unknown macro: !!${key}` |                 error: `Unknown macro: !!${macro.actor}.${macro.method}` | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         // Validate macro |         // Check for circular includes | ||||||
|         const validation = MacroParser.validateMacro(macro); |         if (macro.method === 'include') { | ||||||
|         if (!validation.valid) { |             const path = macro.params.path; | ||||||
|             return { success: false, error: validation.error }; |             if (this.includeStack.includes(path)) { | ||||||
|  |                 return { | ||||||
|  |                     success: false, | ||||||
|  |                     error: `Circular include: ${this.includeStack.join(' → ')} → ${path}` | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         // Execute plugin |  | ||||||
|         try { |         try { | ||||||
|             return await plugin.process(macro, this.webdavClient); |             return await plugin.process(macro, this.webdavClient); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|  |             console.error('[MacroProcessor] Plugin error:', error); | ||||||
|             return { |             return { | ||||||
|                 success: false, |                 success: false, | ||||||
|                 error: `Plugin error: ${error.message}` |                 error: `Plugin error: ${error.message}` | ||||||
| @@ -128,34 +121,12 @@ class MacroProcessor { | |||||||
|      */ |      */ | ||||||
|     registerDefaultPlugins() { |     registerDefaultPlugins() { | ||||||
|         // Include plugin |         // Include plugin | ||||||
|         this.registerPlugin('core', 'include', { |         this.macroRegistry.register('core', 'include', new IncludePlugin(this)); | ||||||
|             process: async (macro, webdavClient) => { |          | ||||||
|                 const path = macro.params.path || macro.params['']; |         // FAQ plugin | ||||||
|                  |         this.macroRegistry.register('core', 'faq', new FAQPlugin(this)); | ||||||
|                 if (!path) { |          | ||||||
|                     return { |         console.log('[MacroProcessor] Registered default plugins'); | ||||||
|                         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}` |  | ||||||
|                     }; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										50
									
								
								static/js/macro-system.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								static/js/macro-system.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | /** | ||||||
|  |  * 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; | ||||||
							
								
								
									
										70
									
								
								static/js/plugins/faq-plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								static/js/plugins/faq-plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | /** | ||||||
|  |  * 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; | ||||||
							
								
								
									
										97
									
								
								static/js/plugins/include-plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								static/js/plugins/include-plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | /** | ||||||
|  |  * 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,8 +186,12 @@ | |||||||
|     <script src="/static/js/file-tree-actions.js" defer></script> |     <script src="/static/js/file-tree-actions.js" defer></script> | ||||||
|     <script src="/static/js/column-resizer.js" defer></script> |     <script src="/static/js/column-resizer.js" defer></script> | ||||||
|     <script src="/static/js/app.js" defer></script> |     <script src="/static/js/app.js" defer></script> | ||||||
|    <script src="/static/js/macro-parser.js" defer></script> |     <!-- Macro System --> | ||||||
|    <script src="/static/js/macro-processor.js" defer></script> |     <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> | ||||||
| </body> | </body> | ||||||
|  |  | ||||||
| </html> | </html> | ||||||
		Reference in New Issue
	
	Block a user