Compare commits
	
		
			1 Commits
		
	
	
		
			dragdrop_m
			...
			23a24d42e2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 23a24d42e2 | 
@@ -111,19 +111,14 @@
 | 
				
			|||||||
    display: none;
 | 
					    display: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tree-node {
 | 
					/* Drag and drop */
 | 
				
			||||||
    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) !important;
 | 
					    background-color: rgba(13, 110, 253, 0.2);
 | 
				
			||||||
    border: 1px dashed var(--link-color);
 | 
					    border: 1px dashed var(--link-color);
 | 
				
			||||||
    border-radius: 4px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Collection selector - Bootstrap styled */
 | 
					/* Collection selector - Bootstrap styled */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,9 +10,7 @@ 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();
 | 
				
			||||||
@@ -90,8 +88,10 @@ class MarkdownEditor {
 | 
				
			|||||||
    setWebDAVClient(client) {
 | 
					    setWebDAVClient(client) {
 | 
				
			||||||
        this.webdavClient = client;
 | 
					        this.webdavClient = client;
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // NOW initialize macro processor
 | 
					        // Update macro processor with client
 | 
				
			||||||
        this.macroProcessor = new MacroProcessor(client);
 | 
					        if (this.macroProcessor) {
 | 
				
			||||||
 | 
					            this.macroProcessor.webdavClient = client;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -196,29 +196,38 @@ class MarkdownEditor {
 | 
				
			|||||||
        const previewDiv = this.previewElement;
 | 
					        const previewDiv = this.previewElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!markdown || !markdown.trim()) {
 | 
					        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;
 | 
					            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 result = await this.macroProcessor.processMacros(markdown);
 | 
					                const processingResult = await this.macroProcessor.processMacros(markdown);
 | 
				
			||||||
                processedContent = result.content;
 | 
					                processedContent = processingResult.content;
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                if (result.errors.length > 0) {
 | 
					                // Log errors if any
 | 
				
			||||||
                    console.warn('[Editor] Macro errors:', result.errors);
 | 
					                if (processingResult.errors.length > 0) {
 | 
				
			||||||
 | 
					                    console.warn('Macro processing errors:', processingResult.errors);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // Step 2: Parse markdown
 | 
					            // Step 2: Parse markdown to HTML
 | 
				
			||||||
            console.log('[Editor] Parsing markdown...');
 | 
					            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);
 | 
					            let html = this.marked.parse(processedContent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Step 3: Handle mermaid
 | 
					            // Replace mermaid code blocks
 | 
				
			||||||
            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) => {
 | 
				
			||||||
@@ -229,23 +238,35 @@ class MarkdownEditor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            previewDiv.innerHTML = html;
 | 
					            previewDiv.innerHTML = html;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Step 4: Syntax highlighting
 | 
					            // Apply syntax highlighting
 | 
				
			||||||
            const codeBlocks = previewDiv.querySelectorAll('pre code');
 | 
					            const codeBlocks = previewDiv.querySelectorAll('pre code');
 | 
				
			||||||
            codeBlocks.forEach(block => {
 | 
					            codeBlocks.forEach(block => {
 | 
				
			||||||
                const lang = Array.from(block.classList)
 | 
					                const languageClass = Array.from(block.classList)
 | 
				
			||||||
                    .find(cls => cls.startsWith('language-'));
 | 
					                    .find(cls => cls.startsWith('language-'));
 | 
				
			||||||
                if (lang && lang !== 'language-mermaid' && window.Prism) {
 | 
					                if (languageClass && languageClass !== 'language-mermaid') {
 | 
				
			||||||
 | 
					                    if (window.Prism) {
 | 
				
			||||||
                        window.Prism.highlightElement(block);
 | 
					                        window.Prism.highlightElement(block);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Step 5: Render mermaid
 | 
					            // Render mermaid diagrams
 | 
				
			||||||
            if (window.mermaid) {
 | 
					            const mermaidElements = previewDiv.querySelectorAll('.mermaid');
 | 
				
			||||||
                await window.mermaid.run();
 | 
					            if (mermaidElements.length > 0 && window.mermaid) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    window.mermaid.contentLoaded();
 | 
				
			||||||
 | 
					                } catch (error) {
 | 
				
			||||||
 | 
					                    console.warn('Mermaid rendering error:', error);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            console.error('[Editor] Preview error:', error);
 | 
					            console.error('Preview rendering error:', error);
 | 
				
			||||||
            previewDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
 | 
					            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() {
 | 
					    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 path = node.dataset.path;
 | 
				
			||||||
            const isDir = node.dataset.isdir === 'true';
 | 
					            const isDir = node.dataset.isdir === 'true';
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // If it's a directory, and the click was on the title, select the folder
 | 
					            // The toggle is handled inside renderNodes now
 | 
				
			||||||
            if (isDir && e.target.classList.contains('tree-node-title')) {
 | 
					            
 | 
				
			||||||
 | 
					            // Select node
 | 
				
			||||||
 | 
					            if (isDir) {
 | 
				
			||||||
                this.selectFolder(path);
 | 
					                this.selectFolder(path);
 | 
				
			||||||
            } else if (!isDir) { // If it's a file, select the file
 | 
					            } else {
 | 
				
			||||||
                this.selectFile(path);
 | 
					                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
 | 
					        // Context menu
 | 
				
			||||||
@@ -120,9 +42,13 @@ 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 });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -165,8 +91,18 @@ class FileTree {
 | 
				
			|||||||
                nodeWrapper.appendChild(childContainer);
 | 
					                nodeWrapper.appendChild(childContainer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Make toggle functional
 | 
					                // Make toggle functional
 | 
				
			||||||
                // The toggle functionality is already handled in renderNodes, no need to duplicate here.
 | 
					                const toggle = nodeElement.querySelector('.tree-node-toggle');
 | 
				
			||||||
                // Ensure the toggle's click event stops propagation to prevent the parent node's click from firing.
 | 
					                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);
 | 
					            parentElement.appendChild(nodeWrapper);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class MacroParser {
 | 
					class MacroParser {
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Extract macros with improved parsing
 | 
					     * Parse and extract all macros from content
 | 
				
			||||||
 | 
					     * 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;
 | 
				
			||||||
@@ -14,17 +15,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];
 | 
					            const actionPart = match[1]; // e.g., "include" or "core.include"
 | 
				
			||||||
            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,
 | 
				
			||||||
@@ -39,10 +40,9 @@ class MacroParser {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Parse HeroScript parameters with multiline support
 | 
					     * Parse HeroScript-style parameters
 | 
				
			||||||
     * Supports:
 | 
					     * key: value
 | 
				
			||||||
     *   key: 'value'
 | 
					     * key: 'value with spaces'
 | 
				
			||||||
     *   key: '''multiline value'''
 | 
					 | 
				
			||||||
     * key: |
 | 
					     * key: |
 | 
				
			||||||
     *   multiline
 | 
					     *   multiline
 | 
				
			||||||
     *   value
 | 
					     *   value
 | 
				
			||||||
@@ -54,95 +54,49 @@ class MacroParser {
 | 
				
			|||||||
            return params;
 | 
					            return params;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        let lines = paramsPart.split('\n');
 | 
					        // Split by newlines but preserve multiline values
 | 
				
			||||||
        let i = 0;
 | 
					        const lines = paramsPart.split('\n');
 | 
				
			||||||
 | 
					        let currentKey = null;
 | 
				
			||||||
 | 
					        let currentValue = [];
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        while (i < lines.length) {
 | 
					        for (const line of lines) {
 | 
				
			||||||
            const line = lines[i].trim();
 | 
					            const trimmed = line.trim();
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            if (!line) {
 | 
					            if (!trimmed) continue;
 | 
				
			||||||
                i++;
 | 
					            
 | 
				
			||||||
                continue;
 | 
					            // Check if this is a key: value line
 | 
				
			||||||
 | 
					            if (trimmed.includes(':')) {
 | 
				
			||||||
 | 
					                // Save previous key-value
 | 
				
			||||||
 | 
					                if (currentKey) {
 | 
				
			||||||
 | 
					                    params[currentKey] = currentValue.join('\n').trim();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
            // Check for key: value pattern
 | 
					                const [key, ...valueParts] = trimmed.split(':');
 | 
				
			||||||
            if (line.includes(':')) {
 | 
					                currentKey = key.trim();
 | 
				
			||||||
                const colonIndex = line.indexOf(':');
 | 
					                currentValue = [valueParts.join(':').trim()];
 | 
				
			||||||
                const key = line.substring(0, colonIndex).trim();
 | 
					            } else if (currentKey) {
 | 
				
			||||||
                let value = line.substring(colonIndex + 1).trim();
 | 
					                // Continuation of multiline value
 | 
				
			||||||
                
 | 
					                currentValue.push(trimmed);
 | 
				
			||||||
                // 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
 | 
				
			||||||
 | 
					        if (currentKey) {
 | 
				
			||||||
 | 
					            params[currentKey] = currentValue.join('\n').trim();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        console.log(`[MacroParser] Parsed parameters:`, params);
 | 
					 | 
				
			||||||
        return params;
 | 
					        return params;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Remove common leading whitespace from multiline strings
 | 
					     * Check if macro is valid
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    static dedent(text) {
 | 
					    static validateMacro(macro) {
 | 
				
			||||||
        const lines = text.split('\n');
 | 
					        if (!macro.actor || !macro.method) {
 | 
				
			||||||
        
 | 
					            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);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if (minIndent === Infinity) minIndent = 0;
 | 
					        return { valid: true };
 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Remove common indentation
 | 
					 | 
				
			||||||
        return lines
 | 
					 | 
				
			||||||
            .map(line => line.slice(minIndent))
 | 
					 | 
				
			||||||
            .join('\n')
 | 
					 | 
				
			||||||
            .trim();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,45 +6,53 @@
 | 
				
			|||||||
class MacroProcessor {
 | 
					class MacroProcessor {
 | 
				
			||||||
    constructor(webdavClient) {
 | 
					    constructor(webdavClient) {
 | 
				
			||||||
        this.webdavClient = webdavClient;
 | 
					        this.webdavClient = webdavClient;
 | 
				
			||||||
        this.macroRegistry = new MacroRegistry();
 | 
					        this.plugins = new Map();
 | 
				
			||||||
        this.includeStack = [];
 | 
					        this.includeStack = []; // Track includes to detect cycles
 | 
				
			||||||
        this.faqItems = [];
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        this.registerDefaultPlugins();
 | 
					        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) {
 | 
					    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);
 | 
					        const macros = MacroParser.extractMacros(content);
 | 
				
			||||||
        console.log(`[MacroProcessor] Found ${macros.length} macros`);
 | 
					        console.log('MacroProcessor: Extracted macros:', macros);
 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        const errors = [];
 | 
					        const errors = [];
 | 
				
			||||||
        let processedContent = content;
 | 
					        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--) {
 | 
					        for (let i = macros.length - 1; i >= 0; i--) {
 | 
				
			||||||
            const macro = macros[i];
 | 
					            const macro = macros[i];
 | 
				
			||||||
            console.log(`[MacroProcessor] Processing macro ${i}:`, macro.actor, macro.method);
 | 
					            console.log('MacroProcessor: Processing macro:', macro);
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            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) {
 | 
				
			||||||
                    console.log(`[MacroProcessor] Macro succeeded, replacing content`);
 | 
					                    // Replace macro with result
 | 
				
			||||||
                    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 {
 | 
				
			||||||
                    console.error(`[MacroProcessor] Macro failed:`, result.error);
 | 
					                    errors.push({
 | 
				
			||||||
                    errors.push({ macro: macro.fullMatch, error: result.error });
 | 
					                        macro: macro.fullMatch,
 | 
				
			||||||
 | 
					                        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) +
 | 
				
			||||||
@@ -52,10 +60,12 @@ class MacroProcessor {
 | 
				
			|||||||
                        processedContent.substring(macro.end);
 | 
					                        processedContent.substring(macro.end);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } catch (error) {
 | 
					            } catch (error) {
 | 
				
			||||||
                console.error(`[MacroProcessor] Macro exception:`, error);
 | 
					                errors.push({
 | 
				
			||||||
                errors.push({ macro: macro.fullMatch, error: error.message });
 | 
					                    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 =
 | 
				
			||||||
                    processedContent.substring(0, macro.start) +
 | 
					                    processedContent.substring(0, macro.start) +
 | 
				
			||||||
                    errorMsg +
 | 
					                    errorMsg +
 | 
				
			||||||
@@ -63,17 +73,7 @@ class MacroProcessor {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Append FAQ if any FAQ macros were used
 | 
					        console.log('MacroProcessor: Final processed content:', processedContent);
 | 
				
			||||||
        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,30 +85,37 @@ class MacroProcessor {
 | 
				
			|||||||
     * Process single macro
 | 
					     * Process single macro
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    async processMacro(macro) {
 | 
					    async processMacro(macro) {
 | 
				
			||||||
        const plugin = this.macroRegistry.resolve(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 || 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: !!${macro.actor}.${macro.method}`
 | 
					                error: `Unknown macro: !!${key}`
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Check for circular includes
 | 
					        // Validate macro
 | 
				
			||||||
        if (macro.method === 'include') {
 | 
					        const validation = MacroParser.validateMacro(macro);
 | 
				
			||||||
            const path = macro.params.path;
 | 
					        if (!validation.valid) {
 | 
				
			||||||
            if (this.includeStack.includes(path)) {
 | 
					            return { success: false, error: validation.error };
 | 
				
			||||||
                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}`
 | 
				
			||||||
@@ -121,12 +128,34 @@ class MacroProcessor {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    registerDefaultPlugins() {
 | 
					    registerDefaultPlugins() {
 | 
				
			||||||
        // Include plugin
 | 
					        // Include plugin
 | 
				
			||||||
        this.macroRegistry.register('core', 'include', new IncludePlugin(this));
 | 
					        this.registerPlugin('core', 'include', {
 | 
				
			||||||
 | 
					            process: async (macro, webdavClient) => {
 | 
				
			||||||
 | 
					                const path = macro.params.path || macro.params[''];
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
        // FAQ plugin
 | 
					                if (!path) {
 | 
				
			||||||
        this.macroRegistry.register('core', 'faq', new FAQPlugin(this));
 | 
					                    return {
 | 
				
			||||||
 | 
					                        success: false,
 | 
				
			||||||
 | 
					                        error: 'include macro requires "path" parameter'
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
        console.log('[MacroProcessor] Registered default plugins');
 | 
					                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,10 +186,6 @@
 | 
				
			|||||||
    <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>
 | 
				
			||||||
    <!-- 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-parser.js" defer></script>
 | 
				
			||||||
   <script src="/static/js/macro-processor.js" defer></script>
 | 
					   <script src="/static/js/macro-processor.js" defer></script>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user