diff --git a/start.sh b/start.sh index b3d5849..ea907c0 100755 --- a/start.sh +++ b/start.sh @@ -19,5 +19,8 @@ echo "Activating virtual environment..." source .venv/bin/activate echo "Installing dependencies..." 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 diff --git a/static/css/file-tree.css b/static/css/file-tree.css index 13cbf87..216c835 100644 --- a/static/css/file-tree.css +++ b/static/css/file-tree.css @@ -111,14 +111,19 @@ display: none; } -/* Drag and drop */ +.tree-node { + cursor: move; +} + .tree-node.dragging { opacity: 0.5; + background-color: rgba(13, 110, 253, 0.1); } .tree-node.drag-over { - background-color: rgba(13, 110, 253, 0.2); + background-color: rgba(13, 110, 253, 0.2) !important; border: 1px dashed var(--link-color); + border-radius: 4px; } /* Collection selector - Bootstrap styled */ diff --git a/static/js/editor.js b/static/js/editor.js index c7042ca..d3bc3eb 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -10,7 +10,9 @@ class MarkdownEditor { this.filenameInput = document.getElementById(filenameInputId); this.currentFile = 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.initMarkdown(); @@ -88,10 +90,8 @@ class MarkdownEditor { setWebDAVClient(client) { this.webdavClient = client; - // Update macro processor with client - if (this.macroProcessor) { - this.macroProcessor.webdavClient = client; - } + // NOW initialize macro processor + this.macroProcessor = new MacroProcessor(client); } /** @@ -196,38 +196,29 @@ class MarkdownEditor { const previewDiv = this.previewElement; if (!markdown || !markdown.trim()) { - previewDiv.innerHTML = ` -
Start typing to see preview...
-([\s\S]*?)<\/code><\/pre>/g,
(match, code) => {
@@ -238,35 +229,23 @@ class MarkdownEditor {
previewDiv.innerHTML = html;
- // Apply syntax highlighting
+ // Step 4: Syntax highlighting
const codeBlocks = previewDiv.querySelectorAll('pre code');
codeBlocks.forEach(block => {
- const languageClass = Array.from(block.classList)
+ const lang = Array.from(block.classList)
.find(cls => cls.startsWith('language-'));
- if (languageClass && languageClass !== 'language-mermaid') {
- if (window.Prism) {
- window.Prism.highlightElement(block);
- }
+ if (lang && lang !== 'language-mermaid' && window.Prism) {
+ window.Prism.highlightElement(block);
}
});
- // Render mermaid diagrams
- const mermaidElements = previewDiv.querySelectorAll('.mermaid');
- if (mermaidElements.length > 0 && window.mermaid) {
- try {
- window.mermaid.contentLoaded();
- } catch (error) {
- console.warn('Mermaid rendering error:', error);
- }
+ // Step 5: Render mermaid
+ if (window.mermaid) {
+ await window.mermaid.run();
}
} catch (error) {
- console.error('Preview rendering error:', error);
- previewDiv.innerHTML = `
-
- Error rendering preview:
- ${error.message}
-
- `;
+ console.error('[Editor] Preview error:', error);
+ previewDiv.innerHTML = `Error: ${error.message}`;
}
}
diff --git a/static/js/file-tree.js b/static/js/file-tree.js
index 29a3fd6..e53f67f 100644
--- a/static/js/file-tree.js
+++ b/static/js/file-tree.js
@@ -16,23 +16,101 @@ class FileTree {
}
setupEventListeners() {
- // Click handler for tree nodes
this.container.addEventListener('click', (e) => {
- console.log('Container clicked', e.target);
const node = e.target.closest('.tree-node');
if (!node) return;
- console.log('Node found', node);
+ const path = node.dataset.path;
+ const isDir = node.dataset.isdir === 'true';
+
+ // If it's a directory, and the click was on the title, select the folder
+ if (isDir && e.target.classList.contains('tree-node-title')) {
+ 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 isDir = node.dataset.isdir === 'true';
- // The toggle is handled inside renderNodes now
+ console.log('[FileTree] Drag start:', path);
- // Select node
- if (isDir) {
- this.selectFolder(path);
- } else {
- this.selectFile(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');
}
});
@@ -42,13 +120,9 @@ class FileTree {
e.preventDefault();
if (node) {
- // Clicked on a node
const path = node.dataset.path;
const isDir = node.dataset.isdir === 'true';
window.showContextMenu(e.clientX, e.clientY, { path, isDir });
- } else if (e.target === this.container) {
- // Clicked on the empty space in the file tree container
- window.showContextMenu(e.clientX, e.clientY, { path: '', isDir: true });
}
});
}
@@ -91,18 +165,8 @@ class FileTree {
nodeWrapper.appendChild(childContainer);
// Make toggle functional
- const toggle = nodeElement.querySelector('.tree-node-toggle');
- if (toggle) {
- toggle.addEventListener('click', (e) => {
- console.log('Toggle clicked', e.target);
- e.stopPropagation();
- const isHidden = childContainer.style.display === 'none';
- console.log('Is hidden?', isHidden);
- childContainer.style.display = isHidden ? 'block' : 'none';
- toggle.innerHTML = isHidden ? '▼' : '▶';
- toggle.classList.toggle('expanded');
- });
- }
+ // The toggle functionality is already handled in renderNodes, no need to duplicate here.
+ // Ensure the toggle's click event stops propagation to prevent the parent node's click from firing.
}
parentElement.appendChild(nodeWrapper);
diff --git a/static/js/macro-parser.js b/static/js/macro-parser.js
index dd8488e..62c8f37 100644
--- a/static/js/macro-parser.js
+++ b/static/js/macro-parser.js
@@ -5,8 +5,7 @@
class MacroParser {
/**
- * Parse and extract all macros from content
- * Returns array of { fullMatch, actor, method, params }
+ * Extract macros with improved parsing
*/
static extractMacros(content) {
const macroRegex = /!!([\w.]+)\s*([\s\S]*?)(?=\n!!|\n#|$)/g;
@@ -15,17 +14,17 @@ class MacroParser {
while ((match = macroRegex.exec(content)) !== null) {
const fullMatch = match[0];
- const actionPart = match[1]; // e.g., "include" or "core.include"
+ const actionPart = match[1];
const paramsPart = match[2];
- // Parse action: "method" or "actor.method"
const [actor, method] = actionPart.includes('.')
? actionPart.split('.')
: ['core', actionPart];
- // Parse parameters from HeroScript-like syntax
const params = this.parseParams(paramsPart);
+ console.log(`[MacroParser] Extracted: !!${actor}.${method}`, params);
+
macros.push({
fullMatch: fullMatch.trim(),
actor,
@@ -40,12 +39,13 @@ class MacroParser {
}
/**
- * Parse HeroScript-style parameters
- * key: value
- * key: 'value with spaces'
- * key: |
- * multiline
- * value
+ * Parse HeroScript parameters with multiline support
+ * Supports:
+ * key: 'value'
+ * key: '''multiline value'''
+ * key: |
+ * multiline
+ * value
*/
static parseParams(paramsPart) {
const params = {};
@@ -54,49 +54,95 @@ class MacroParser {
return params;
}
- // Split by newlines but preserve multiline values
- const lines = paramsPart.split('\n');
- let currentKey = null;
- let currentValue = [];
+ let lines = paramsPart.split('\n');
+ let i = 0;
- for (const line of lines) {
- const trimmed = line.trim();
+ while (i < lines.length) {
+ const line = lines[i].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);
+ if (!line) {
+ i++;
+ continue;
}
+
+ // Check for key: value pattern
+ if (line.includes(':')) {
+ const colonIndex = line.indexOf(':');
+ const key = line.substring(0, colonIndex).trim();
+ let value = line.substring(colonIndex + 1).trim();
+
+ // 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;
}
/**
- * Check if macro is valid
+ * Remove common leading whitespace from multiline strings
*/
- static validateMacro(macro) {
- if (!macro.actor || !macro.method) {
- return { valid: false, error: 'Invalid macro format' };
+ static dedent(text) {
+ const lines = text.split('\n');
+
+ // Find minimum indentation
+ let minIndent = Infinity;
+ for (const line of lines) {
+ if (line.trim().length === 0) continue;
+ const indent = line.search(/\S/);
+ minIndent = Math.min(minIndent, indent);
}
- return { valid: true };
+ if (minIndent === Infinity) minIndent = 0;
+
+ // Remove common indentation
+ return lines
+ .map(line => line.slice(minIndent))
+ .join('\n')
+ .trim();
}
}
diff --git a/static/js/macro-processor.js b/static/js/macro-processor.js
index 3a7174d..0b6dad7 100644
--- a/static/js/macro-processor.js
+++ b/static/js/macro-processor.js
@@ -6,53 +6,45 @@
class MacroProcessor {
constructor(webdavClient) {
this.webdavClient = webdavClient;
- this.plugins = new Map();
- this.includeStack = []; // Track includes to detect cycles
+ this.macroRegistry = new MacroRegistry();
+ this.includeStack = [];
+ this.faqItems = [];
+
this.registerDefaultPlugins();
+ console.log('[MacroProcessor] Initialized');
}
/**
- * 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: [] }
+ * Process all macros in markdown
*/
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);
- console.log('MacroProcessor: Extracted macros:', macros);
+ console.log(`[MacroProcessor] Found ${macros.length} macros`);
+
const errors = [];
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--) {
const macro = macros[i];
- console.log('MacroProcessor: Processing macro:', macro);
+ console.log(`[MacroProcessor] Processing macro ${i}:`, macro.actor, macro.method);
try {
const result = await this.processMacro(macro);
- console.log('MacroProcessor: Macro processing result:', result);
if (result.success) {
- // Replace macro with result
+ console.log(`[MacroProcessor] Macro succeeded, replacing content`);
processedContent =
processedContent.substring(0, macro.start) +
result.content +
processedContent.substring(macro.end);
} else {
- errors.push({
- macro: macro.fullMatch,
- error: result.error
- });
+ console.error(`[MacroProcessor] Macro failed:`, result.error);
+ errors.push({ macro: macro.fullMatch, error: result.error });
- // Replace with error message
const errorMsg = `\n\n⚠️ **Macro Error**: ${result.error}\n\n`;
processedContent =
processedContent.substring(0, macro.start) +
@@ -60,12 +52,10 @@ class MacroProcessor {
processedContent.substring(macro.end);
}
} catch (error) {
- errors.push({
- macro: macro.fullMatch,
- error: error.message
- });
+ console.error(`[MacroProcessor] Macro exception:`, error);
+ errors.push({ macro: macro.fullMatch, 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.substring(0, macro.start) +
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 {
success: errors.length === 0,
content: processedContent,
@@ -85,37 +85,30 @@ class MacroProcessor {
* 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}`
- };
- }
- }
+ const plugin = this.macroRegistry.resolve(macro.actor, macro.method);
if (!plugin) {
return {
success: false,
- error: `Unknown macro: !!${key}`
+ error: `Unknown macro: !!${macro.actor}.${macro.method}`
};
}
- // Validate macro
- const validation = MacroParser.validateMacro(macro);
- if (!validation.valid) {
- return { success: false, error: validation.error };
+ // Check for circular includes
+ if (macro.method === 'include') {
+ const path = macro.params.path;
+ if (this.includeStack.includes(path)) {
+ return {
+ success: false,
+ error: `Circular include: ${this.includeStack.join(' → ')} → ${path}`
+ };
+ }
}
- // Execute plugin
try {
return await plugin.process(macro, this.webdavClient);
} catch (error) {
+ console.error('[MacroProcessor] Plugin error:', error);
return {
success: false,
error: `Plugin error: ${error.message}`
@@ -128,34 +121,12 @@ class MacroProcessor {
*/
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}`
- };
- }
- }
- });
+ this.macroRegistry.register('core', 'include', new IncludePlugin(this));
+
+ // FAQ plugin
+ this.macroRegistry.register('core', 'faq', new FAQPlugin(this));
+
+ console.log('[MacroProcessor] Registered default plugins');
}
}
diff --git a/static/js/macro-system.js b/static/js/macro-system.js
new file mode 100644
index 0000000..42d1d45
--- /dev/null
+++ b/static/js/macro-system.js
@@ -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;
\ No newline at end of file
diff --git a/static/js/plugins/faq-plugin.js b/static/js/plugins/faq-plugin.js
new file mode 100644
index 0000000..a9094f6
--- /dev/null
+++ b/static/js/plugins/faq-plugin.js
@@ -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;
\ No newline at end of file
diff --git a/static/js/plugins/include-plugin.js b/static/js/plugins/include-plugin.js
new file mode 100644
index 0000000..0768050
--- /dev/null
+++ b/static/js/plugins/include-plugin.js
@@ -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;
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
index b59e6f1..e182066 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -186,8 +186,12 @@
-
-
+
+
+
+
+
+