...
This commit is contained in:
		| @@ -1,3 +1,10 @@ | ||||
| # New File | ||||
|  | ||||
| Start typing... | ||||
| # test | ||||
|  | ||||
| - 1 | ||||
| - 2 | ||||
|  | ||||
| [2025 SeaweedFS Intro Slides.pdf](/notes/2025 SeaweedFS Intro Slides.pdf) | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -6,4 +6,4 @@ | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| !!include path:test2.md | ||||
|   | ||||
							
								
								
									
										12
									
								
								collections/notes/ttt/test2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								collections/notes/ttt/test2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
|  | ||||
| ## test2 | ||||
|  | ||||
| - something | ||||
| - another thing | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -53,7 +53,7 @@ class MarkdownEditorApp: | ||||
|          | ||||
|         config = { | ||||
|             'host': self.config['server']['host'], | ||||
|             'port': self.config['server']['port'], | ||||
|             'port': int(os.environ.get('PORT', self.config['server']['port'])), | ||||
|             'provider_mapping': provider_mapping, | ||||
|             'verbose': self.config['webdav'].get('verbose', 1), | ||||
|             'logging': { | ||||
| @@ -179,7 +179,7 @@ def main(): | ||||
|      | ||||
|     # Get server config | ||||
|     host = app.config['server']['host'] | ||||
|     port = app.config['server']['port'] | ||||
|     port = int(os.environ.get('PORT', app.config['server']['port'])) | ||||
|      | ||||
|     print(f"\nServer starting on http://{host}:{port}") | ||||
|     print(f"\nAvailable collections:") | ||||
|   | ||||
							
								
								
									
										5
									
								
								start.sh
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								start.sh
									
									
									
									
									
								
							| @@ -19,8 +19,5 @@ echo "Activating virtual environment..." | ||||
| source .venv/bin/activate | ||||
| echo "Installing dependencies..." | ||||
| uv pip install wsgidav cheroot pyyaml | ||||
| PORT=8004 | ||||
| echo "Checking for process on port $PORT..." | ||||
| lsof -ti:$PORT | xargs -r kill -9 | ||||
| echo "Starting WebDAV server..." | ||||
| echo "Starting WebDAV server on port $PORT..." | ||||
| python server_webdav.py | ||||
|   | ||||
| @@ -10,6 +10,7 @@ class MarkdownEditor { | ||||
|         this.filenameInput = document.getElementById(filenameInputId); | ||||
|         this.currentFile = null; | ||||
|         this.webdavClient = null; | ||||
|         this.macroProcessor = new MacroProcessor(null); // Will be set later | ||||
|          | ||||
|         this.initCodeMirror(); | ||||
|         this.initMarkdown(); | ||||
| @@ -86,6 +87,11 @@ class MarkdownEditor { | ||||
|      */ | ||||
|     setWebDAVClient(client) { | ||||
|         this.webdavClient = client; | ||||
|          | ||||
|         // Update macro processor with client | ||||
|         if (this.macroProcessor) { | ||||
|             this.macroProcessor.webdavClient = client; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -185,7 +191,7 @@ class MarkdownEditor { | ||||
|     /** | ||||
|      * Update preview | ||||
|      */ | ||||
|     updatePreview() { | ||||
|     async updatePreview() { | ||||
|         const markdown = this.editor.getValue(); | ||||
|         const previewDiv = this.previewElement; | ||||
|  | ||||
| @@ -199,15 +205,29 @@ class MarkdownEditor { | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             // Parse markdown to HTML | ||||
|             // 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 = `<div class="alert alert-danger">Preview engine not loaded.</div>`; | ||||
|                 return; | ||||
|             } | ||||
|             let html = this.marked.parse(markdown); | ||||
|              | ||||
|             let html = this.marked.parse(processedContent); | ||||
|  | ||||
|             // Replace mermaid code blocks with div containers | ||||
|             // Replace mermaid code blocks | ||||
|             html = html.replace( | ||||
|                 /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, | ||||
|                 (match, code) => { | ||||
| @@ -218,7 +238,7 @@ class MarkdownEditor { | ||||
|  | ||||
|             previewDiv.innerHTML = html; | ||||
|  | ||||
|             // Apply syntax highlighting to code blocks | ||||
|             // Apply syntax highlighting | ||||
|             const codeBlocks = previewDiv.querySelectorAll('pre code'); | ||||
|             codeBlocks.forEach(block => { | ||||
|                 const languageClass = Array.from(block.classList) | ||||
|   | ||||
| @@ -11,6 +11,37 @@ class FileTreeActions { | ||||
|         this.clipboard = null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validate and sanitize filename/folder name | ||||
|      * Returns { valid: boolean, sanitized: string, message: string } | ||||
|      */ | ||||
|     validateFileName(name, isFolder = false) { | ||||
|         const type = isFolder ? 'folder' : 'file'; | ||||
|          | ||||
|         if (!name || name.trim().length === 0) { | ||||
|             return { valid: false, message: `${type} name cannot be empty` }; | ||||
|         } | ||||
|          | ||||
|         // Check for invalid characters | ||||
|         const validPattern = /^[a-z0-9_]+(\.[a-z0-9_]+)*$/; | ||||
|          | ||||
|         if (!validPattern.test(name)) { | ||||
|             const sanitized = name | ||||
|                 .toLowerCase() | ||||
|                 .replace(/[^a-z0-9_.]/g, '_') | ||||
|                 .replace(/_+/g, '_') | ||||
|                 .replace(/^_+|_+$/g, ''); | ||||
|              | ||||
|             return { | ||||
|                 valid: false, | ||||
|                 sanitized, | ||||
|                 message: `Invalid characters in ${type} name. Only lowercase letters, numbers, and underscores allowed.\n\nSuggestion: "${sanitized}"` | ||||
|             }; | ||||
|         } | ||||
|          | ||||
|         return { valid: true, sanitized: name, message: '' }; | ||||
|     } | ||||
|  | ||||
|     async execute(action, targetPath, isDirectory) { | ||||
|         const handler = this.actions[action]; | ||||
|         if (!handler) { | ||||
| @@ -35,25 +66,62 @@ class FileTreeActions { | ||||
|  | ||||
|         'new-file': async function(path, isDir) { | ||||
|             if (!isDir) return; | ||||
|             const filename = await this.showInputDialog('Enter filename:', 'new-file.md'); | ||||
|             if (filename) { | ||||
|              | ||||
|             await this.showInputDialog('Enter filename (lowercase, underscore only):', 'new_file.md', async (filename) => { | ||||
|                 if (!filename) return; | ||||
|                  | ||||
|                 const validation = this.validateFileName(filename, false); | ||||
|                  | ||||
|                 if (!validation.valid) { | ||||
|                     showNotification(validation.message, 'warning'); | ||||
|                      | ||||
|                     // Ask if user wants to use sanitized version | ||||
|                     if (validation.sanitized) { | ||||
|                         if (await this.showConfirmDialog('Use sanitized name?', `${filename} → ${validation.sanitized}`)) { | ||||
|                             filename = validation.sanitized; | ||||
|                         } else { | ||||
|                             return; | ||||
|                         } | ||||
|                     } else { | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 const fullPath = `${path}/${filename}`.replace(/\/+/g, '/'); | ||||
|                 await this.webdavClient.put(fullPath, '# New File\n\n'); | ||||
|                 await this.fileTree.load(); | ||||
|                 showNotification(`Created ${filename}`, 'success'); | ||||
|                 await this.editor.loadFile(fullPath); | ||||
|             } | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         'new-folder': async function(path, isDir) { | ||||
|             if (!isDir) return; | ||||
|             const foldername = await this.showInputDialog('Enter folder name:', 'new-folder'); | ||||
|             if (foldername) { | ||||
|              | ||||
|             await this.showInputDialog('Enter folder name (lowercase, underscore only):', 'new_folder', async (foldername) => { | ||||
|                 if (!foldername) return; | ||||
|                  | ||||
|                 const validation = this.validateFileName(foldername, true); | ||||
|                  | ||||
|                 if (!validation.valid) { | ||||
|                     showNotification(validation.message, 'warning'); | ||||
|                      | ||||
|                     if (validation.sanitized) { | ||||
|                         if (await this.showConfirmDialog('Use sanitized name?', `${foldername} → ${validation.sanitized}`)) { | ||||
|                             foldername = validation.sanitized; | ||||
|                         } else { | ||||
|                             return; | ||||
|                         } | ||||
|                     } else { | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 const fullPath = `${path}/${foldername}`.replace(/\/+/g, '/'); | ||||
|                 await this.webdavClient.mkcol(fullPath); | ||||
|                 await this.fileTree.load(); | ||||
|                 showNotification(`Created folder ${foldername}`, 'success'); | ||||
|             } | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         rename: async function(path, isDir) { | ||||
| @@ -140,27 +208,36 @@ class FileTreeActions { | ||||
|     }; | ||||
|  | ||||
|     // Modern dialog implementations | ||||
|     async showInputDialog(title, placeholder = '') { | ||||
|     async showInputDialog(title, placeholder = '', callback) { | ||||
|         return new Promise((resolve) => { | ||||
|             const dialog = this.createInputDialog(title, placeholder); | ||||
|             const input = dialog.querySelector('input'); | ||||
|             const confirmBtn = dialog.querySelector('.btn-primary'); | ||||
|             const cancelBtn = dialog.querySelector('.btn-secondary'); | ||||
|  | ||||
|             const cleanup = () => { | ||||
|             const cleanup = (value) => { | ||||
|                 const modalInstance = bootstrap.Modal.getInstance(dialog); | ||||
|                 if (modalInstance) { | ||||
|                     modalInstance.hide(); | ||||
|                 } | ||||
|                 dialog.remove(); | ||||
|                 const backdrop = document.querySelector('.modal-backdrop'); | ||||
|                 if (backdrop) backdrop.remove(); | ||||
|                 document.body.classList.remove('modal-open'); | ||||
|                 resolve(value); | ||||
|                 if (callback) callback(value); | ||||
|             }; | ||||
|  | ||||
|             confirmBtn.onclick = () => { | ||||
|                 resolve(input.value.trim()); | ||||
|                 cleanup(); | ||||
|                 cleanup(input.value.trim()); | ||||
|             }; | ||||
|  | ||||
|             cancelBtn.onclick = () => { | ||||
|                 cleanup(null); | ||||
|             }; | ||||
|  | ||||
|             dialog.addEventListener('hidden.bs.modal', () => { | ||||
|                 resolve(null); | ||||
|                 cleanup(); | ||||
|                 cleanup(null); | ||||
|             }); | ||||
|  | ||||
|             input.onkeypress = (e) => { | ||||
| @@ -175,26 +252,35 @@ class FileTreeActions { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async showConfirmDialog(title, message = '') { | ||||
|     async showConfirmDialog(title, message = '', callback) { | ||||
|         return new Promise((resolve) => { | ||||
|             const dialog = this.createConfirmDialog(title, message); | ||||
|             const confirmBtn = dialog.querySelector('.btn-danger'); | ||||
|             const cancelBtn = dialog.querySelector('.btn-secondary'); | ||||
|  | ||||
|             const cleanup = () => { | ||||
|             const cleanup = (value) => { | ||||
|                 const modalInstance = bootstrap.Modal.getInstance(dialog); | ||||
|                 if (modalInstance) { | ||||
|                     modalInstance.hide(); | ||||
|                 } | ||||
|                 dialog.remove(); | ||||
|                 const backdrop = document.querySelector('.modal-backdrop'); | ||||
|                 if (backdrop) backdrop.remove(); | ||||
|                 document.body.classList.remove('modal-open'); | ||||
|                 resolve(value); | ||||
|                 if (callback) callback(value); | ||||
|             }; | ||||
|  | ||||
|             confirmBtn.onclick = () => { | ||||
|                 resolve(true); | ||||
|                 cleanup(); | ||||
|                 cleanup(true); | ||||
|             }; | ||||
|  | ||||
|             cancelBtn.onclick = () => { | ||||
|                 cleanup(false); | ||||
|             }; | ||||
|  | ||||
|             dialog.addEventListener('hidden.bs.modal', () => { | ||||
|                 resolve(false); | ||||
|                 cleanup(); | ||||
|                 cleanup(false); | ||||
|             }); | ||||
|  | ||||
|             document.body.appendChild(dialog); | ||||
|   | ||||
							
								
								
									
										103
									
								
								static/js/macro-parser.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								static/js/macro-parser.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| /** | ||||
|  * Macro Parser and Processor | ||||
|  * Parses HeroScript-style macros from markdown content | ||||
|  */ | ||||
|  | ||||
| class MacroParser { | ||||
|     /** | ||||
|      * 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; | ||||
|         const macros = []; | ||||
|         let match; | ||||
|          | ||||
|         while ((match = macroRegex.exec(content)) !== null) { | ||||
|             const fullMatch = match[0]; | ||||
|             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); | ||||
|              | ||||
|             macros.push({ | ||||
|                 fullMatch: fullMatch.trim(), | ||||
|                 actor, | ||||
|                 method, | ||||
|                 params, | ||||
|                 start: match.index, | ||||
|                 end: match.index + fullMatch.length | ||||
|             }); | ||||
|         } | ||||
|          | ||||
|         return macros; | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Parse HeroScript-style parameters | ||||
|      * key: value | ||||
|      * key: 'value with spaces' | ||||
|      * key: | | ||||
|      *   multiline | ||||
|      *   value | ||||
|      */ | ||||
|     static parseParams(paramsPart) { | ||||
|         const params = {}; | ||||
|          | ||||
|         if (!paramsPart || !paramsPart.trim()) { | ||||
|             return params; | ||||
|         } | ||||
|          | ||||
|         // Split by newlines but preserve multiline values | ||||
|         const lines = paramsPart.split('\n'); | ||||
|         let currentKey = null; | ||||
|         let currentValue = []; | ||||
|          | ||||
|         for (const line of lines) { | ||||
|             const trimmed = line.trim(); | ||||
|              | ||||
|             if (!trimmed) continue; | ||||
|              | ||||
|             // Check if this is a key: value line | ||||
|             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); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Save last key-value | ||||
|         if (currentKey) { | ||||
|             params[currentKey] = currentValue.join('\n').trim(); | ||||
|         } | ||||
|          | ||||
|         return params; | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Check if macro is valid | ||||
|      */ | ||||
|     static validateMacro(macro) { | ||||
|         if (!macro.actor || !macro.method) { | ||||
|             return { valid: false, error: 'Invalid macro format' }; | ||||
|         } | ||||
|          | ||||
|         return { valid: true }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| window.MacroParser = MacroParser; | ||||
							
								
								
									
										162
									
								
								static/js/macro-processor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								static/js/macro-processor.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,162 @@ | ||||
| /** | ||||
|  * Macro Processor | ||||
|  * Handles macro execution and result rendering | ||||
|  */ | ||||
|  | ||||
| class MacroProcessor { | ||||
|     constructor(webdavClient) { | ||||
|         this.webdavClient = webdavClient; | ||||
|         this.plugins = new Map(); | ||||
|         this.includeStack = []; // Track includes to detect cycles | ||||
|         this.registerDefaultPlugins(); | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 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: Starting macro processing for content:', content); | ||||
|         const macros = MacroParser.extractMacros(content); | ||||
|         console.log('MacroProcessor: Extracted macros:', macros); | ||||
|         const errors = []; | ||||
|         let processedContent = content; | ||||
|          | ||||
|         // 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:', macro); | ||||
|              | ||||
|             try { | ||||
|                 const result = await this.processMacro(macro); | ||||
|                 console.log('MacroProcessor: Macro processing result:', result); | ||||
|                  | ||||
|                 if (result.success) { | ||||
|                     // Replace macro with result | ||||
|                     processedContent = | ||||
|                         processedContent.substring(0, macro.start) + | ||||
|                         result.content + | ||||
|                         processedContent.substring(macro.end); | ||||
|                 } else { | ||||
|                     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) + | ||||
|                         errorMsg + | ||||
|                         processedContent.substring(macro.end); | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 errors.push({ | ||||
|                     macro: macro.fullMatch, | ||||
|                     error: error.message | ||||
|                 }); | ||||
|                  | ||||
|                 const errorMsg = `\n\n⚠️ **Macro Error**: ${error.message}\n\n`; | ||||
|                 processedContent = | ||||
|                     processedContent.substring(0, macro.start) + | ||||
|                     errorMsg + | ||||
|                     processedContent.substring(macro.end); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         console.log('MacroProcessor: Final processed content:', processedContent); | ||||
|         return { | ||||
|             success: errors.length === 0, | ||||
|             content: processedContent, | ||||
|             errors | ||||
|         }; | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Process single macro | ||||
|      */ | ||||
|     async processMacro(macro) { | ||||
|         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 || macro.params['']; | ||||
|             if (this.includeStack.includes(path)) { | ||||
|                 return { | ||||
|                     success: false, | ||||
|                     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) { | ||||
|             return { | ||||
|                 success: false, | ||||
|                 error: `Plugin error: ${error.message}` | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Register default plugins | ||||
|      */ | ||||
|     registerDefaultPlugins() { | ||||
|         // Include plugin | ||||
|         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}` | ||||
|                     }; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| window.MacroProcessor = MacroProcessor; | ||||
| @@ -162,6 +162,31 @@ class WebDAVClient { | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     async includeFile(path) { | ||||
|         try { | ||||
|             // Parse path: "collection:path/to/file" or "path/to/file" | ||||
|             let targetCollection = this.currentCollection; | ||||
|             let targetPath = path; | ||||
|              | ||||
|             if (path.includes(':')) { | ||||
|                 [targetCollection, targetPath] = path.split(':'); | ||||
|             } | ||||
|              | ||||
|             // Temporarily switch collection | ||||
|             const originalCollection = this.currentCollection; | ||||
|             this.currentCollection = targetCollection; | ||||
|              | ||||
|             const content = await this.get(targetPath); | ||||
|              | ||||
|             // Restore collection | ||||
|             this.currentCollection = originalCollection; | ||||
|              | ||||
|             return content; | ||||
|         } catch (error) { | ||||
|             throw new Error(`Cannot include file "${path}": ${error.message}`); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     parseMultiStatus(xml) { | ||||
|         const parser = new DOMParser(); | ||||
|         const doc = parser.parseFromString(xml, 'text/xml'); | ||||
| @@ -231,6 +256,7 @@ class WebDAVClient { | ||||
|             } else { | ||||
|                 root.push(node); | ||||
|             } | ||||
|              | ||||
|         }); | ||||
|          | ||||
|         return root; | ||||
|   | ||||
| @@ -186,6 +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> | ||||
|    <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