Compare commits

...

1 Commits

Author SHA1 Message Date
8750e0af39 ... 2025-10-26 10:52:27 +04:00
10 changed files with 482 additions and 193 deletions

View File

@@ -19,5 +19,8 @@ echo "Activating virtual environment..."
source .venv/bin/activate source .venv/bin/activate
echo "Installing dependencies..." echo "Installing dependencies..."
uv pip install wsgidav cheroot pyyaml uv pip install wsgidav cheroot pyyaml
echo "Starting WebDAV server on port $PORT..." PORT=8004
echo "Checking for process on port $PORT..."
lsof -ti:$PORT | xargs -r kill -9
echo "Starting WebDAV server..."
python server_webdav.py python server_webdav.py

View File

@@ -111,14 +111,19 @@
display: none; display: none;
} }
/* Drag and drop */ .tree-node {
cursor: move;
}
.tree-node.dragging { .tree-node.dragging {
opacity: 0.5; opacity: 0.5;
background-color: rgba(13, 110, 253, 0.1);
} }
.tree-node.drag-over { .tree-node.drag-over {
background-color: rgba(13, 110, 253, 0.2); background-color: rgba(13, 110, 253, 0.2) !important;
border: 1px dashed var(--link-color); border: 1px dashed var(--link-color);
border-radius: 4px;
} }
/* Collection selector - Bootstrap styled */ /* Collection selector - Bootstrap styled */

View File

@@ -10,7 +10,9 @@ class MarkdownEditor {
this.filenameInput = document.getElementById(filenameInputId); this.filenameInput = document.getElementById(filenameInputId);
this.currentFile = null; this.currentFile = null;
this.webdavClient = null; this.webdavClient = null;
this.macroProcessor = new MacroProcessor(null); // Will be set later
// Initialize macro processor AFTER webdavClient is set
this.macroProcessor = null;
this.initCodeMirror(); this.initCodeMirror();
this.initMarkdown(); this.initMarkdown();
@@ -88,10 +90,8 @@ class MarkdownEditor {
setWebDAVClient(client) { setWebDAVClient(client) {
this.webdavClient = client; this.webdavClient = client;
// Update macro processor with client // NOW initialize macro processor
if (this.macroProcessor) { this.macroProcessor = new MacroProcessor(client);
this.macroProcessor.webdavClient = client;
}
} }
/** /**
@@ -196,38 +196,29 @@ class MarkdownEditor {
const previewDiv = this.previewElement; const previewDiv = this.previewElement;
if (!markdown || !markdown.trim()) { if (!markdown || !markdown.trim()) {
previewDiv.innerHTML = ` previewDiv.innerHTML = `<div class="text-muted">Start typing...</div>`;
<div class="text-muted text-center mt-5">
<p>Start typing to see preview...</p>
</div>
`;
return; return;
} }
try { try {
// Step 1: Process macros // Step 1: Process macros
console.log('[Editor] Processing macros...');
let processedContent = markdown; let processedContent = markdown;
if (this.macroProcessor) { if (this.macroProcessor) {
const processingResult = await this.macroProcessor.processMacros(markdown); const result = await this.macroProcessor.processMacros(markdown);
processedContent = processingResult.content; processedContent = result.content;
// Log errors if any if (result.errors.length > 0) {
if (processingResult.errors.length > 0) { console.warn('[Editor] Macro errors:', result.errors);
console.warn('Macro processing errors:', processingResult.errors);
} }
} }
// Step 2: Parse markdown to HTML // Step 2: Parse markdown
if (!this.marked) { console.log('[Editor] Parsing markdown...');
console.error("Markdown parser (marked) not initialized.");
previewDiv.innerHTML = `<div class="alert alert-danger">Preview engine not loaded.</div>`;
return;
}
let html = this.marked.parse(processedContent); let html = this.marked.parse(processedContent);
// Replace mermaid code blocks // Step 3: Handle mermaid
html = html.replace( html = html.replace(
/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
(match, code) => { (match, code) => {
@@ -238,35 +229,23 @@ class MarkdownEditor {
previewDiv.innerHTML = html; previewDiv.innerHTML = html;
// Apply syntax highlighting // Step 4: Syntax highlighting
const codeBlocks = previewDiv.querySelectorAll('pre code'); const codeBlocks = previewDiv.querySelectorAll('pre code');
codeBlocks.forEach(block => { codeBlocks.forEach(block => {
const languageClass = Array.from(block.classList) const lang = Array.from(block.classList)
.find(cls => cls.startsWith('language-')); .find(cls => cls.startsWith('language-'));
if (languageClass && languageClass !== 'language-mermaid') { if (lang && lang !== 'language-mermaid' && window.Prism) {
if (window.Prism) { window.Prism.highlightElement(block);
window.Prism.highlightElement(block);
}
} }
}); });
// Render mermaid diagrams // Step 5: Render mermaid
const mermaidElements = previewDiv.querySelectorAll('.mermaid'); if (window.mermaid) {
if (mermaidElements.length > 0 && window.mermaid) { await window.mermaid.run();
try {
window.mermaid.contentLoaded();
} catch (error) {
console.warn('Mermaid rendering error:', error);
}
} }
} catch (error) { } catch (error) {
console.error('Preview rendering error:', error); console.error('[Editor] Preview error:', error);
previewDiv.innerHTML = ` previewDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
<div class="alert alert-danger" role="alert">
<strong>Error rendering preview:</strong><br>
${error.message}
</div>
`;
} }
} }

View File

@@ -16,23 +16,101 @@ class FileTree {
} }
setupEventListeners() { setupEventListeners() {
// Click handler for tree nodes
this.container.addEventListener('click', (e) => { this.container.addEventListener('click', (e) => {
console.log('Container clicked', e.target);
const node = e.target.closest('.tree-node'); const node = e.target.closest('.tree-node');
if (!node) return; if (!node) return;
console.log('Node found', node); const path = node.dataset.path;
const isDir = node.dataset.isdir === 'true';
// If it's a directory, and the click was on the title, select the folder
if (isDir && e.target.classList.contains('tree-node-title')) {
this.selectFolder(path);
} else if (!isDir) { // If it's a file, select the file
this.selectFile(path);
}
// Clicks on the toggle are handled by the toggle's specific event listener
});
// DRAG AND DROP
this.container.addEventListener('dragstart', (e) => {
const node = e.target.closest('.tree-node');
if (!node) return;
const path = node.dataset.path; const path = node.dataset.path;
const isDir = node.dataset.isdir === 'true'; const isDir = node.dataset.isdir === 'true';
// The toggle is handled inside renderNodes now console.log('[FileTree] Drag start:', path);
// Select node e.dataTransfer.effectAllowed = 'move';
if (isDir) { e.dataTransfer.setData('text/plain', path);
this.selectFolder(path); e.dataTransfer.setData('application/json', JSON.stringify({
} else { path,
this.selectFile(path); isDir,
name: node.querySelector('.tree-node-title').textContent
}));
node.classList.add('dragging');
setTimeout(() => node.classList.remove('dragging'), 0);
});
this.container.addEventListener('dragover', (e) => {
const node = e.target.closest('.tree-node');
if (!node) return;
const isDir = node.dataset.isdir === 'true';
if (!isDir) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
node.classList.add('drag-over');
});
this.container.addEventListener('dragleave', (e) => {
const node = e.target.closest('.tree-node');
if (node) {
node.classList.remove('drag-over');
}
});
this.container.addEventListener('drop', async (e) => {
const targetNode = e.target.closest('.tree-node');
if (!targetNode) return;
e.preventDefault();
e.stopPropagation();
const targetPath = targetNode.dataset.path;
const isDir = targetNode.dataset.isdir === 'true';
if (!isDir) {
console.log('[FileTree] Target is not a directory');
return;
}
try {
const data = JSON.parse(e.dataTransfer.getData('application/json'));
const sourcePath = data.path;
const sourceName = data.name;
if (sourcePath === targetPath) {
console.log('[FileTree] Source and target are same');
return;
}
const destPath = `${targetPath}/${sourceName}`.replace(/\/+/g, '/');
console.log('[FileTree] Moving:', sourcePath, '→', destPath);
await this.webdavClient.move(sourcePath, destPath);
await this.load();
showNotification(`Moved to ${targetNode.querySelector('.tree-node-title').textContent}`, 'success');
} catch (error) {
console.error('[FileTree] Drop error:', error);
showNotification(`Failed to move: ${error.message}`, 'error');
} finally {
targetNode.classList.remove('drag-over');
} }
}); });
@@ -42,13 +120,9 @@ class FileTree {
e.preventDefault(); e.preventDefault();
if (node) { if (node) {
// Clicked on a node
const path = node.dataset.path; const path = node.dataset.path;
const isDir = node.dataset.isdir === 'true'; const isDir = node.dataset.isdir === 'true';
window.showContextMenu(e.clientX, e.clientY, { path, isDir }); window.showContextMenu(e.clientX, e.clientY, { path, isDir });
} else if (e.target === this.container) {
// Clicked on the empty space in the file tree container
window.showContextMenu(e.clientX, e.clientY, { path: '', isDir: true });
} }
}); });
} }
@@ -91,18 +165,8 @@ class FileTree {
nodeWrapper.appendChild(childContainer); nodeWrapper.appendChild(childContainer);
// Make toggle functional // Make toggle functional
const toggle = nodeElement.querySelector('.tree-node-toggle'); // The toggle functionality is already handled in renderNodes, no need to duplicate here.
if (toggle) { // Ensure the toggle's click event stops propagation to prevent the parent node's click from firing.
toggle.addEventListener('click', (e) => {
console.log('Toggle clicked', e.target);
e.stopPropagation();
const isHidden = childContainer.style.display === 'none';
console.log('Is hidden?', isHidden);
childContainer.style.display = isHidden ? 'block' : 'none';
toggle.innerHTML = isHidden ? '▼' : '▶';
toggle.classList.toggle('expanded');
});
}
} }
parentElement.appendChild(nodeWrapper); parentElement.appendChild(nodeWrapper);

View File

@@ -5,8 +5,7 @@
class MacroParser { class MacroParser {
/** /**
* Parse and extract all macros from content * Extract macros with improved parsing
* Returns array of { fullMatch, actor, method, params }
*/ */
static extractMacros(content) { static extractMacros(content) {
const macroRegex = /!!([\w.]+)\s*([\s\S]*?)(?=\n!!|\n#|$)/g; const macroRegex = /!!([\w.]+)\s*([\s\S]*?)(?=\n!!|\n#|$)/g;
@@ -15,17 +14,17 @@ class MacroParser {
while ((match = macroRegex.exec(content)) !== null) { while ((match = macroRegex.exec(content)) !== null) {
const fullMatch = match[0]; const fullMatch = match[0];
const actionPart = match[1]; // e.g., "include" or "core.include" const actionPart = match[1];
const paramsPart = match[2]; const paramsPart = match[2];
// Parse action: "method" or "actor.method"
const [actor, method] = actionPart.includes('.') const [actor, method] = actionPart.includes('.')
? actionPart.split('.') ? actionPart.split('.')
: ['core', actionPart]; : ['core', actionPart];
// Parse parameters from HeroScript-like syntax
const params = this.parseParams(paramsPart); const params = this.parseParams(paramsPart);
console.log(`[MacroParser] Extracted: !!${actor}.${method}`, params);
macros.push({ macros.push({
fullMatch: fullMatch.trim(), fullMatch: fullMatch.trim(),
actor, actor,
@@ -40,12 +39,13 @@ class MacroParser {
} }
/** /**
* Parse HeroScript-style parameters * Parse HeroScript parameters with multiline support
* key: value * Supports:
* key: 'value with spaces' * key: 'value'
* key: | * key: '''multiline value'''
* multiline * key: |
* value * multiline
* value
*/ */
static parseParams(paramsPart) { static parseParams(paramsPart) {
const params = {}; const params = {};
@@ -54,49 +54,95 @@ class MacroParser {
return params; return params;
} }
// Split by newlines but preserve multiline values let lines = paramsPart.split('\n');
const lines = paramsPart.split('\n'); let i = 0;
let currentKey = null;
let currentValue = [];
for (const line of lines) { while (i < lines.length) {
const trimmed = line.trim(); const line = lines[i].trim();
if (!trimmed) continue; if (!line) {
i++;
// Check if this is a key: value line continue;
if (trimmed.includes(':')) {
// Save previous key-value
if (currentKey) {
params[currentKey] = currentValue.join('\n').trim();
}
const [key, ...valueParts] = trimmed.split(':');
currentKey = key.trim();
currentValue = [valueParts.join(':').trim()];
} else if (currentKey) {
// Continuation of multiline value
currentValue.push(trimmed);
} }
// Check for key: value pattern
if (line.includes(':')) {
const colonIndex = line.indexOf(':');
const key = line.substring(0, colonIndex).trim();
let value = line.substring(colonIndex + 1).trim();
// Handle triple-quoted multiline
if (value.startsWith("'''")) {
value = value.substring(3);
const valueLines = [value];
i++;
while (i < lines.length) {
const contentLine = lines[i];
if (contentLine.trim().endsWith("'''")) {
valueLines.push(contentLine.trim().slice(0, -3));
break;
}
valueLines.push(contentLine);
i++;
}
// Remove leading whitespace from multiline
const processedValue = this.dedent(valueLines.join('\n'));
params[key] = processedValue;
}
// Handle pipe multiline
else if (value === '|') {
const valueLines = [];
i++;
while (i < lines.length && lines[i].startsWith('\t')) {
valueLines.push(lines[i].substring(1)); // Remove tab
i++;
}
i--; // Back up one since loop will increment
const processedValue = this.dedent(valueLines.join('\n'));
params[key] = processedValue;
}
// Handle quoted value
else if (value.startsWith("'") && value.endsWith("'")) {
params[key] = value.slice(1, -1);
}
// Handle unquoted value
else {
params[key] = value;
}
}
i++;
} }
// Save last key-value console.log(`[MacroParser] Parsed parameters:`, params);
if (currentKey) {
params[currentKey] = currentValue.join('\n').trim();
}
return params; return params;
} }
/** /**
* Check if macro is valid * Remove common leading whitespace from multiline strings
*/ */
static validateMacro(macro) { static dedent(text) {
if (!macro.actor || !macro.method) { const lines = text.split('\n');
return { valid: false, error: 'Invalid macro format' };
// Find minimum indentation
let minIndent = Infinity;
for (const line of lines) {
if (line.trim().length === 0) continue;
const indent = line.search(/\S/);
minIndent = Math.min(minIndent, indent);
} }
return { valid: true }; if (minIndent === Infinity) minIndent = 0;
// Remove common indentation
return lines
.map(line => line.slice(minIndent))
.join('\n')
.trim();
} }
} }

View File

@@ -6,53 +6,45 @@
class MacroProcessor { class MacroProcessor {
constructor(webdavClient) { constructor(webdavClient) {
this.webdavClient = webdavClient; this.webdavClient = webdavClient;
this.plugins = new Map(); this.macroRegistry = new MacroRegistry();
this.includeStack = []; // Track includes to detect cycles this.includeStack = [];
this.faqItems = [];
this.registerDefaultPlugins(); this.registerDefaultPlugins();
console.log('[MacroProcessor] Initialized');
} }
/** /**
* Register a macro plugin * Process all macros in markdown
* Plugin must implement: { canHandle(actor, method), process(macro, webdavClient) }
*/
registerPlugin(actor, method, plugin) {
const key = `${actor}.${method}`;
this.plugins.set(key, plugin);
}
/**
* Process all macros in content
* Returns { success: boolean, content: string, errors: [] }
*/ */
async processMacros(content) { async processMacros(content) {
console.log('MacroProcessor: Starting macro processing for content:', content); console.log('[MacroProcessor] Processing content, length:', content.length);
const macros = MacroParser.extractMacros(content); const macros = MacroParser.extractMacros(content);
console.log('MacroProcessor: Extracted macros:', macros); console.log(`[MacroProcessor] Found ${macros.length} macros`);
const errors = []; const errors = [];
let processedContent = content; let processedContent = content;
let faqOutput = '';
// Process macros in reverse order to preserve positions // Process in reverse to preserve positions
for (let i = macros.length - 1; i >= 0; i--) { for (let i = macros.length - 1; i >= 0; i--) {
const macro = macros[i]; const macro = macros[i];
console.log('MacroProcessor: Processing macro:', macro); console.log(`[MacroProcessor] Processing macro ${i}:`, macro.actor, macro.method);
try { try {
const result = await this.processMacro(macro); const result = await this.processMacro(macro);
console.log('MacroProcessor: Macro processing result:', result);
if (result.success) { if (result.success) {
// Replace macro with result console.log(`[MacroProcessor] Macro succeeded, replacing content`);
processedContent = processedContent =
processedContent.substring(0, macro.start) + processedContent.substring(0, macro.start) +
result.content + result.content +
processedContent.substring(macro.end); processedContent.substring(macro.end);
} else { } else {
errors.push({ console.error(`[MacroProcessor] Macro failed:`, result.error);
macro: macro.fullMatch, errors.push({ macro: macro.fullMatch, error: result.error });
error: result.error
});
// Replace with error message
const errorMsg = `\n\n⚠️ **Macro Error**: ${result.error}\n\n`; const errorMsg = `\n\n⚠️ **Macro Error**: ${result.error}\n\n`;
processedContent = processedContent =
processedContent.substring(0, macro.start) + processedContent.substring(0, macro.start) +
@@ -60,12 +52,10 @@ class MacroProcessor {
processedContent.substring(macro.end); processedContent.substring(macro.end);
} }
} catch (error) { } catch (error) {
errors.push({ console.error(`[MacroProcessor] Macro exception:`, error);
macro: macro.fullMatch, errors.push({ macro: macro.fullMatch, error: error.message });
error: error.message
});
const errorMsg = `\n\n⚠️ **Macro Error**: ${error.message}\n\n`; const errorMsg = `\n\n⚠️ **Macro Exception**: ${error.message}\n\n`;
processedContent = processedContent =
processedContent.substring(0, macro.start) + processedContent.substring(0, macro.start) +
errorMsg + errorMsg +
@@ -73,7 +63,17 @@ class MacroProcessor {
} }
} }
console.log('MacroProcessor: Final processed content:', processedContent); // Append FAQ if any FAQ macros were used
if (this.faqItems.length > 0) {
faqOutput = '\n\n---\n\n## FAQ\n\n';
faqOutput += this.faqItems
.map((item, idx) => `### ${idx + 1}. ${item.title}\n\n${item.response}`)
.join('\n\n');
processedContent += faqOutput;
}
console.log('[MacroProcessor] Processing complete, errors:', errors.length);
return { return {
success: errors.length === 0, success: errors.length === 0,
content: processedContent, content: processedContent,
@@ -85,37 +85,30 @@ class MacroProcessor {
* Process single macro * Process single macro
*/ */
async processMacro(macro) { async processMacro(macro) {
const key = `${macro.actor}.${macro.method}`; const plugin = this.macroRegistry.resolve(macro.actor, macro.method);
const plugin = this.plugins.get(key);
// Check for circular includes
if (macro.method === 'include') {
const path = macro.params.path || macro.params[''];
if (this.includeStack.includes(path)) {
return {
success: false,
error: `Circular include detected: ${this.includeStack.join(' → ')}${path}`
};
}
}
if (!plugin) { if (!plugin) {
return { return {
success: false, success: false,
error: `Unknown macro: !!${key}` error: `Unknown macro: !!${macro.actor}.${macro.method}`
}; };
} }
// Validate macro // Check for circular includes
const validation = MacroParser.validateMacro(macro); if (macro.method === 'include') {
if (!validation.valid) { const path = macro.params.path;
return { success: false, error: validation.error }; if (this.includeStack.includes(path)) {
return {
success: false,
error: `Circular include: ${this.includeStack.join(' → ')}${path}`
};
}
} }
// Execute plugin
try { try {
return await plugin.process(macro, this.webdavClient); return await plugin.process(macro, this.webdavClient);
} catch (error) { } catch (error) {
console.error('[MacroProcessor] Plugin error:', error);
return { return {
success: false, success: false,
error: `Plugin error: ${error.message}` error: `Plugin error: ${error.message}`
@@ -128,34 +121,12 @@ class MacroProcessor {
*/ */
registerDefaultPlugins() { registerDefaultPlugins() {
// Include plugin // Include plugin
this.registerPlugin('core', 'include', { this.macroRegistry.register('core', 'include', new IncludePlugin(this));
process: async (macro, webdavClient) => {
const path = macro.params.path || macro.params['']; // FAQ plugin
this.macroRegistry.register('core', 'faq', new FAQPlugin(this));
if (!path) {
return { console.log('[MacroProcessor] Registered default plugins');
success: false,
error: 'include macro requires "path" parameter'
};
}
try {
// Add to include stack
this.includeStack.push(path);
const content = await webdavClient.includeFile(path);
// Remove from include stack
this.includeStack.pop();
return { success: true, content };
} catch (error) {
// Remove from include stack on error
this.includeStack.pop();
return {
success: false,
error: `Failed to include "${path}": ${error.message}`
};
}
}
});
} }
} }

50
static/js/macro-system.js Normal file
View File

@@ -0,0 +1,50 @@
/**
* Macro System
* Generic plugin-based macro processor
*/
class MacroPlugin {
/**
* Base class for macro plugins
* Subclass and implement these methods:
* - canHandle(actor, method): boolean
* - process(macro, context): Promise<{ success, content, error }>
*/
canHandle(actor, method) {
throw new Error('Must implement canHandle()');
}
async process(macro, context) {
throw new Error('Must implement process()');
}
}
class MacroRegistry {
constructor() {
this.plugins = new Map();
console.log('[MacroRegistry] Initializing macro registry');
}
register(actor, method, plugin) {
const key = `${actor}.${method}`;
this.plugins.set(key, plugin);
console.log(`[MacroRegistry] Registered plugin: ${key}`);
}
resolve(actor, method) {
// Try exact match
let key = `${actor}.${method}`;
if (this.plugins.has(key)) {
console.log(`[MacroRegistry] Found plugin: ${key}`);
return this.plugins.get(key);
}
// No plugin found
console.warn(`[MacroRegistry] No plugin found for: ${key}`);
return null;
}
}
window.MacroRegistry = MacroRegistry;
window.MacroPlugin = MacroPlugin;

View File

@@ -0,0 +1,70 @@
/**
* FAQ Plugin
* Creates FAQ entries that are collected and displayed at bottom of preview
*
* Usage:
* !!faq title: 'My Question'
* response: '''
* This is the answer with **markdown** support.
*
* - Point 1
* - Point 2
* '''
*/
class FAQPlugin extends MacroPlugin {
constructor(processor) {
super();
this.processor = processor;
}
canHandle(actor, method) {
return actor === 'core' && method === 'faq';
}
async process(macro, webdavClient) {
const title = macro.params.title;
const response = macro.params.response;
console.log('[FAQPlugin] Processing FAQ:', title);
if (!title) {
console.error('[FAQPlugin] Missing title parameter');
return {
success: false,
error: 'FAQ macro requires "title" parameter'
};
}
if (!response) {
console.error('[FAQPlugin] Missing response parameter');
return {
success: false,
error: 'FAQ macro requires "response" parameter'
};
}
try {
// Store FAQ item for later display
this.processor.faqItems.push({
title: title.trim(),
response: response.trim()
});
console.log('[FAQPlugin] FAQ item added, total:', this.processor.faqItems.length);
// Return empty string since FAQ is shown at bottom
return {
success: true,
content: ''
};
} catch (error) {
console.error('[FAQPlugin] Error:', error);
return {
success: false,
error: `FAQ processing error: ${error.message}`
};
}
}
}
window.FAQPlugin = FAQPlugin;

View File

@@ -0,0 +1,97 @@
/**
* Include Plugin
* Includes content from other files
*
* Usage:
* !!include path: 'myfile.md'
* !!include path: 'collection:folder/file.md'
*/
class IncludePlugin extends MacroPlugin {
constructor(processor) {
super();
this.processor = processor;
}
canHandle(actor, method) {
return actor === 'core' && method === 'include';
}
async process(macro, webdavClient) {
const path = macro.params.path;
console.log('[IncludePlugin] Processing include:', path);
if (!path) {
console.error('[IncludePlugin] Missing path parameter');
return {
success: false,
error: 'Include macro requires "path" parameter'
};
}
try {
// Parse path format: "collection:path/to/file" or "path/to/file"
let targetCollection = webdavClient.currentCollection;
let targetPath = path;
if (path.includes(':')) {
[targetCollection, targetPath] = path.split(':', 2);
console.log('[IncludePlugin] Using external collection:', targetCollection);
} else {
console.log('[IncludePlugin] Using current collection:', targetCollection);
}
// Check for circular includes
const fullPath = `${targetCollection}:${targetPath}`;
if (this.processor.includeStack.includes(fullPath)) {
console.error('[IncludePlugin] Circular include detected');
return {
success: false,
error: `Circular include detected: ${this.processor.includeStack.join(' → ')}${fullPath}`
};
}
// Add to include stack
this.processor.includeStack.push(fullPath);
// Switch collection temporarily
const originalCollection = webdavClient.currentCollection;
webdavClient.setCollection(targetCollection);
// Fetch file
console.log('[IncludePlugin] Fetching:', targetPath);
const content = await webdavClient.get(targetPath);
// Restore collection
webdavClient.setCollection(originalCollection);
// Remove from stack
this.processor.includeStack.pop();
console.log('[IncludePlugin] Include successful, length:', content.length);
return {
success: true,
content: content
};
} catch (error) {
console.error('[IncludePlugin] Error:', error);
// Restore collection on error
if (webdavClient.currentCollection !== this.processor.webdavClient?.currentCollection) {
webdavClient.setCollection(this.processor.webdavClient?.currentCollection);
}
this.processor.includeStack = this.processor.includeStack.filter(
item => !item.includes(path)
);
return {
success: false,
error: `Cannot include "${path}": ${error.message}`
};
}
}
}
window.IncludePlugin = IncludePlugin;

View File

@@ -186,8 +186,12 @@
<script src="/static/js/file-tree-actions.js" defer></script> <script src="/static/js/file-tree-actions.js" defer></script>
<script src="/static/js/column-resizer.js" defer></script> <script src="/static/js/column-resizer.js" defer></script>
<script src="/static/js/app.js" defer></script> <script src="/static/js/app.js" defer></script>
<script src="/static/js/macro-parser.js" defer></script> <!-- Macro System -->
<script src="/static/js/macro-processor.js" defer></script> <script src="/static/js/macro-system.js" defer></script>
<script src="/static/js/plugins/include-plugin.js" defer></script>
<script src="/static/js/plugins/faq-plugin.js" defer></script>
<script src="/static/js/macro-parser.js" defer></script>
<script src="/static/js/macro-processor.js" defer></script>
</body> </body>
</html> </html>