...
This commit is contained in:
5
start.sh
5
start.sh
@@ -19,5 +19,8 @@ echo "Activating virtual environment..."
|
|||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
echo "Installing dependencies..."
|
echo "Installing dependencies..."
|
||||||
uv pip install wsgidav cheroot pyyaml
|
uv pip install wsgidav cheroot pyyaml
|
||||||
echo "Starting WebDAV server on port $PORT..."
|
PORT=8004
|
||||||
|
echo "Checking for process on port $PORT..."
|
||||||
|
lsof -ti:$PORT | xargs -r kill -9
|
||||||
|
echo "Starting WebDAV server..."
|
||||||
python server_webdav.py
|
python server_webdav.py
|
||||||
|
|||||||
@@ -111,14 +111,19 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Drag and drop */
|
.tree-node {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-node.dragging {
|
.tree-node.dragging {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
background-color: rgba(13, 110, 253, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node.drag-over {
|
.tree-node.drag-over {
|
||||||
background-color: rgba(13, 110, 253, 0.2);
|
background-color: rgba(13, 110, 253, 0.2) !important;
|
||||||
border: 1px dashed var(--link-color);
|
border: 1px dashed var(--link-color);
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collection selector - Bootstrap styled */
|
/* Collection selector - Bootstrap styled */
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ class MarkdownEditor {
|
|||||||
this.filenameInput = document.getElementById(filenameInputId);
|
this.filenameInput = document.getElementById(filenameInputId);
|
||||||
this.currentFile = null;
|
this.currentFile = null;
|
||||||
this.webdavClient = null;
|
this.webdavClient = null;
|
||||||
this.macroProcessor = new MacroProcessor(null); // Will be set later
|
|
||||||
|
// Initialize macro processor AFTER webdavClient is set
|
||||||
|
this.macroProcessor = null;
|
||||||
|
|
||||||
this.initCodeMirror();
|
this.initCodeMirror();
|
||||||
this.initMarkdown();
|
this.initMarkdown();
|
||||||
@@ -88,10 +90,8 @@ class MarkdownEditor {
|
|||||||
setWebDAVClient(client) {
|
setWebDAVClient(client) {
|
||||||
this.webdavClient = client;
|
this.webdavClient = client;
|
||||||
|
|
||||||
// Update macro processor with client
|
// NOW initialize macro processor
|
||||||
if (this.macroProcessor) {
|
this.macroProcessor = new MacroProcessor(client);
|
||||||
this.macroProcessor.webdavClient = client;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,38 +196,29 @@ class MarkdownEditor {
|
|||||||
const previewDiv = this.previewElement;
|
const previewDiv = this.previewElement;
|
||||||
|
|
||||||
if (!markdown || !markdown.trim()) {
|
if (!markdown || !markdown.trim()) {
|
||||||
previewDiv.innerHTML = `
|
previewDiv.innerHTML = `<div class="text-muted">Start typing...</div>`;
|
||||||
<div class="text-muted text-center mt-5">
|
|
||||||
<p>Start typing to see preview...</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: Process macros
|
// Step 1: Process macros
|
||||||
|
console.log('[Editor] Processing macros...');
|
||||||
let processedContent = markdown;
|
let processedContent = markdown;
|
||||||
|
|
||||||
if (this.macroProcessor) {
|
if (this.macroProcessor) {
|
||||||
const processingResult = await this.macroProcessor.processMacros(markdown);
|
const result = await this.macroProcessor.processMacros(markdown);
|
||||||
processedContent = processingResult.content;
|
processedContent = result.content;
|
||||||
|
|
||||||
// Log errors if any
|
if (result.errors.length > 0) {
|
||||||
if (processingResult.errors.length > 0) {
|
console.warn('[Editor] Macro errors:', result.errors);
|
||||||
console.warn('Macro processing errors:', processingResult.errors);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Parse markdown to HTML
|
// Step 2: Parse markdown
|
||||||
if (!this.marked) {
|
console.log('[Editor] Parsing markdown...');
|
||||||
console.error("Markdown parser (marked) not initialized.");
|
|
||||||
previewDiv.innerHTML = `<div class="alert alert-danger">Preview engine not loaded.</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = this.marked.parse(processedContent);
|
let html = this.marked.parse(processedContent);
|
||||||
|
|
||||||
// Replace mermaid code blocks
|
// Step 3: Handle mermaid
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
|
/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
|
||||||
(match, code) => {
|
(match, code) => {
|
||||||
@@ -238,35 +229,23 @@ class MarkdownEditor {
|
|||||||
|
|
||||||
previewDiv.innerHTML = html;
|
previewDiv.innerHTML = html;
|
||||||
|
|
||||||
// Apply syntax highlighting
|
// Step 4: Syntax highlighting
|
||||||
const codeBlocks = previewDiv.querySelectorAll('pre code');
|
const codeBlocks = previewDiv.querySelectorAll('pre code');
|
||||||
codeBlocks.forEach(block => {
|
codeBlocks.forEach(block => {
|
||||||
const languageClass = Array.from(block.classList)
|
const lang = Array.from(block.classList)
|
||||||
.find(cls => cls.startsWith('language-'));
|
.find(cls => cls.startsWith('language-'));
|
||||||
if (languageClass && languageClass !== 'language-mermaid') {
|
if (lang && lang !== 'language-mermaid' && window.Prism) {
|
||||||
if (window.Prism) {
|
window.Prism.highlightElement(block);
|
||||||
window.Prism.highlightElement(block);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render mermaid diagrams
|
// Step 5: Render mermaid
|
||||||
const mermaidElements = previewDiv.querySelectorAll('.mermaid');
|
if (window.mermaid) {
|
||||||
if (mermaidElements.length > 0 && window.mermaid) {
|
await window.mermaid.run();
|
||||||
try {
|
|
||||||
window.mermaid.contentLoaded();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Mermaid rendering error:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Preview rendering error:', error);
|
console.error('[Editor] Preview error:', error);
|
||||||
previewDiv.innerHTML = `
|
previewDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
<strong>Error rendering preview:</strong><br>
|
|
||||||
${error.message}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,24 +16,102 @@ 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';
|
||||||
|
|
||||||
// The toggle is handled inside renderNodes now
|
// If it's a directory, and the click was on the title, select the folder
|
||||||
|
if (isDir && e.target.classList.contains('tree-node-title')) {
|
||||||
// Select node
|
|
||||||
if (isDir) {
|
|
||||||
this.selectFolder(path);
|
this.selectFolder(path);
|
||||||
} else {
|
} else if (!isDir) { // If it's a file, select the file
|
||||||
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
|
||||||
@@ -42,13 +120,9 @@ class FileTree {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
// Clicked on a node
|
|
||||||
const path = node.dataset.path;
|
const path = node.dataset.path;
|
||||||
const isDir = node.dataset.isdir === 'true';
|
const isDir = node.dataset.isdir === 'true';
|
||||||
window.showContextMenu(e.clientX, e.clientY, { path, isDir });
|
window.showContextMenu(e.clientX, e.clientY, { path, isDir });
|
||||||
} else if (e.target === this.container) {
|
|
||||||
// Clicked on the empty space in the file tree container
|
|
||||||
window.showContextMenu(e.clientX, e.clientY, { path: '', isDir: true });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,18 +165,8 @@ class FileTree {
|
|||||||
nodeWrapper.appendChild(childContainer);
|
nodeWrapper.appendChild(childContainer);
|
||||||
|
|
||||||
// Make toggle functional
|
// Make toggle functional
|
||||||
const toggle = nodeElement.querySelector('.tree-node-toggle');
|
// The toggle functionality is already handled in renderNodes, no need to duplicate here.
|
||||||
if (toggle) {
|
// Ensure the toggle's click event stops propagation to prevent the parent node's click from firing.
|
||||||
toggle.addEventListener('click', (e) => {
|
|
||||||
console.log('Toggle clicked', e.target);
|
|
||||||
e.stopPropagation();
|
|
||||||
const isHidden = childContainer.style.display === 'none';
|
|
||||||
console.log('Is hidden?', isHidden);
|
|
||||||
childContainer.style.display = isHidden ? 'block' : 'none';
|
|
||||||
toggle.innerHTML = isHidden ? '▼' : '▶';
|
|
||||||
toggle.classList.toggle('expanded');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parentElement.appendChild(nodeWrapper);
|
parentElement.appendChild(nodeWrapper);
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
|
|
||||||
class MacroParser {
|
class MacroParser {
|
||||||
/**
|
/**
|
||||||
* Parse and extract all macros from content
|
* Extract macros with improved parsing
|
||||||
* Returns array of { fullMatch, actor, method, params }
|
|
||||||
*/
|
*/
|
||||||
static extractMacros(content) {
|
static extractMacros(content) {
|
||||||
const macroRegex = /!!([\w.]+)\s*([\s\S]*?)(?=\n!!|\n#|$)/g;
|
const macroRegex = /!!([\w.]+)\s*([\s\S]*?)(?=\n!!|\n#|$)/g;
|
||||||
@@ -15,17 +14,17 @@ class MacroParser {
|
|||||||
|
|
||||||
while ((match = macroRegex.exec(content)) !== null) {
|
while ((match = macroRegex.exec(content)) !== null) {
|
||||||
const fullMatch = match[0];
|
const fullMatch = match[0];
|
||||||
const actionPart = match[1]; // e.g., "include" or "core.include"
|
const actionPart = match[1];
|
||||||
const paramsPart = match[2];
|
const paramsPart = match[2];
|
||||||
|
|
||||||
// Parse action: "method" or "actor.method"
|
|
||||||
const [actor, method] = actionPart.includes('.')
|
const [actor, method] = actionPart.includes('.')
|
||||||
? actionPart.split('.')
|
? actionPart.split('.')
|
||||||
: ['core', actionPart];
|
: ['core', actionPart];
|
||||||
|
|
||||||
// Parse parameters from HeroScript-like syntax
|
|
||||||
const params = this.parseParams(paramsPart);
|
const params = this.parseParams(paramsPart);
|
||||||
|
|
||||||
|
console.log(`[MacroParser] Extracted: !!${actor}.${method}`, params);
|
||||||
|
|
||||||
macros.push({
|
macros.push({
|
||||||
fullMatch: fullMatch.trim(),
|
fullMatch: fullMatch.trim(),
|
||||||
actor,
|
actor,
|
||||||
@@ -40,12 +39,13 @@ class MacroParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse HeroScript-style parameters
|
* Parse HeroScript parameters with multiline support
|
||||||
* key: value
|
* Supports:
|
||||||
* key: 'value with spaces'
|
* key: 'value'
|
||||||
* key: |
|
* key: '''multiline value'''
|
||||||
* multiline
|
* key: |
|
||||||
* value
|
* multiline
|
||||||
|
* value
|
||||||
*/
|
*/
|
||||||
static parseParams(paramsPart) {
|
static parseParams(paramsPart) {
|
||||||
const params = {};
|
const params = {};
|
||||||
@@ -54,49 +54,95 @@ class MacroParser {
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split by newlines but preserve multiline values
|
let lines = paramsPart.split('\n');
|
||||||
const lines = paramsPart.split('\n');
|
let i = 0;
|
||||||
let currentKey = null;
|
|
||||||
let currentValue = [];
|
|
||||||
|
|
||||||
for (const line of lines) {
|
while (i < lines.length) {
|
||||||
const trimmed = line.trim();
|
const line = lines[i].trim();
|
||||||
|
|
||||||
if (!trimmed) continue;
|
if (!line) {
|
||||||
|
i++;
|
||||||
// Check if this is a key: value line
|
continue;
|
||||||
if (trimmed.includes(':')) {
|
|
||||||
// Save previous key-value
|
|
||||||
if (currentKey) {
|
|
||||||
params[currentKey] = currentValue.join('\n').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
const [key, ...valueParts] = trimmed.split(':');
|
|
||||||
currentKey = key.trim();
|
|
||||||
currentValue = [valueParts.join(':').trim()];
|
|
||||||
} else if (currentKey) {
|
|
||||||
// Continuation of multiline value
|
|
||||||
currentValue.push(trimmed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for key: value pattern
|
||||||
|
if (line.includes(':')) {
|
||||||
|
const colonIndex = line.indexOf(':');
|
||||||
|
const key = line.substring(0, colonIndex).trim();
|
||||||
|
let value = line.substring(colonIndex + 1).trim();
|
||||||
|
|
||||||
|
// Handle triple-quoted multiline
|
||||||
|
if (value.startsWith("'''")) {
|
||||||
|
value = value.substring(3);
|
||||||
|
const valueLines = [value];
|
||||||
|
i++;
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
const contentLine = lines[i];
|
||||||
|
if (contentLine.trim().endsWith("'''")) {
|
||||||
|
valueLines.push(contentLine.trim().slice(0, -3));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
valueLines.push(contentLine);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove leading whitespace from multiline
|
||||||
|
const processedValue = this.dedent(valueLines.join('\n'));
|
||||||
|
params[key] = processedValue;
|
||||||
|
}
|
||||||
|
// Handle pipe multiline
|
||||||
|
else if (value === '|') {
|
||||||
|
const valueLines = [];
|
||||||
|
i++;
|
||||||
|
|
||||||
|
while (i < lines.length && lines[i].startsWith('\t')) {
|
||||||
|
valueLines.push(lines[i].substring(1)); // Remove tab
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
i--; // Back up one since loop will increment
|
||||||
|
|
||||||
|
const processedValue = this.dedent(valueLines.join('\n'));
|
||||||
|
params[key] = processedValue;
|
||||||
|
}
|
||||||
|
// Handle quoted value
|
||||||
|
else if (value.startsWith("'") && value.endsWith("'")) {
|
||||||
|
params[key] = value.slice(1, -1);
|
||||||
|
}
|
||||||
|
// Handle unquoted value
|
||||||
|
else {
|
||||||
|
params[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save last key-value
|
console.log(`[MacroParser] Parsed parameters:`, params);
|
||||||
if (currentKey) {
|
|
||||||
params[currentKey] = currentValue.join('\n').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if macro is valid
|
* Remove common leading whitespace from multiline strings
|
||||||
*/
|
*/
|
||||||
static validateMacro(macro) {
|
static dedent(text) {
|
||||||
if (!macro.actor || !macro.method) {
|
const lines = text.split('\n');
|
||||||
return { valid: false, error: 'Invalid macro format' };
|
|
||||||
|
// Find minimum indentation
|
||||||
|
let minIndent = Infinity;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim().length === 0) continue;
|
||||||
|
const indent = line.search(/\S/);
|
||||||
|
minIndent = Math.min(minIndent, indent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true };
|
if (minIndent === Infinity) minIndent = 0;
|
||||||
|
|
||||||
|
// Remove common indentation
|
||||||
|
return lines
|
||||||
|
.map(line => line.slice(minIndent))
|
||||||
|
.join('\n')
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,53 +6,45 @@
|
|||||||
class MacroProcessor {
|
class MacroProcessor {
|
||||||
constructor(webdavClient) {
|
constructor(webdavClient) {
|
||||||
this.webdavClient = webdavClient;
|
this.webdavClient = webdavClient;
|
||||||
this.plugins = new Map();
|
this.macroRegistry = new MacroRegistry();
|
||||||
this.includeStack = []; // Track includes to detect cycles
|
this.includeStack = [];
|
||||||
|
this.faqItems = [];
|
||||||
|
|
||||||
this.registerDefaultPlugins();
|
this.registerDefaultPlugins();
|
||||||
|
console.log('[MacroProcessor] Initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a macro plugin
|
* Process all macros in markdown
|
||||||
* Plugin must implement: { canHandle(actor, method), process(macro, webdavClient) }
|
|
||||||
*/
|
|
||||||
registerPlugin(actor, method, plugin) {
|
|
||||||
const key = `${actor}.${method}`;
|
|
||||||
this.plugins.set(key, plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process all macros in content
|
|
||||||
* Returns { success: boolean, content: string, errors: [] }
|
|
||||||
*/
|
*/
|
||||||
async processMacros(content) {
|
async processMacros(content) {
|
||||||
console.log('MacroProcessor: Starting macro processing for content:', content);
|
console.log('[MacroProcessor] Processing content, length:', content.length);
|
||||||
|
|
||||||
const macros = MacroParser.extractMacros(content);
|
const macros = MacroParser.extractMacros(content);
|
||||||
console.log('MacroProcessor: Extracted macros:', macros);
|
console.log(`[MacroProcessor] Found ${macros.length} macros`);
|
||||||
|
|
||||||
const errors = [];
|
const errors = [];
|
||||||
let processedContent = content;
|
let processedContent = content;
|
||||||
|
let faqOutput = '';
|
||||||
|
|
||||||
// Process macros in reverse order to preserve positions
|
// Process in reverse to preserve positions
|
||||||
for (let i = macros.length - 1; i >= 0; i--) {
|
for (let i = macros.length - 1; i >= 0; i--) {
|
||||||
const macro = macros[i];
|
const macro = macros[i];
|
||||||
console.log('MacroProcessor: Processing macro:', macro);
|
console.log(`[MacroProcessor] Processing macro ${i}:`, macro.actor, macro.method);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.processMacro(macro);
|
const result = await this.processMacro(macro);
|
||||||
console.log('MacroProcessor: Macro processing result:', result);
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Replace macro with result
|
console.log(`[MacroProcessor] Macro succeeded, replacing content`);
|
||||||
processedContent =
|
processedContent =
|
||||||
processedContent.substring(0, macro.start) +
|
processedContent.substring(0, macro.start) +
|
||||||
result.content +
|
result.content +
|
||||||
processedContent.substring(macro.end);
|
processedContent.substring(macro.end);
|
||||||
} else {
|
} else {
|
||||||
errors.push({
|
console.error(`[MacroProcessor] Macro failed:`, result.error);
|
||||||
macro: macro.fullMatch,
|
errors.push({ macro: macro.fullMatch, error: result.error });
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
|
|
||||||
// Replace with error message
|
|
||||||
const errorMsg = `\n\n⚠️ **Macro Error**: ${result.error}\n\n`;
|
const errorMsg = `\n\n⚠️ **Macro Error**: ${result.error}\n\n`;
|
||||||
processedContent =
|
processedContent =
|
||||||
processedContent.substring(0, macro.start) +
|
processedContent.substring(0, macro.start) +
|
||||||
@@ -60,12 +52,10 @@ class MacroProcessor {
|
|||||||
processedContent.substring(macro.end);
|
processedContent.substring(macro.end);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push({
|
console.error(`[MacroProcessor] Macro exception:`, error);
|
||||||
macro: macro.fullMatch,
|
errors.push({ macro: macro.fullMatch, error: error.message });
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorMsg = `\n\n⚠️ **Macro Error**: ${error.message}\n\n`;
|
const errorMsg = `\n\n⚠️ **Macro Exception**: ${error.message}\n\n`;
|
||||||
processedContent =
|
processedContent =
|
||||||
processedContent.substring(0, macro.start) +
|
processedContent.substring(0, macro.start) +
|
||||||
errorMsg +
|
errorMsg +
|
||||||
@@ -73,7 +63,17 @@ class MacroProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('MacroProcessor: Final processed content:', processedContent);
|
// Append FAQ if any FAQ macros were used
|
||||||
|
if (this.faqItems.length > 0) {
|
||||||
|
faqOutput = '\n\n---\n\n## FAQ\n\n';
|
||||||
|
faqOutput += this.faqItems
|
||||||
|
.map((item, idx) => `### ${idx + 1}. ${item.title}\n\n${item.response}`)
|
||||||
|
.join('\n\n');
|
||||||
|
processedContent += faqOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[MacroProcessor] Processing complete, errors:', errors.length);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: errors.length === 0,
|
success: errors.length === 0,
|
||||||
content: processedContent,
|
content: processedContent,
|
||||||
@@ -85,37 +85,30 @@ class MacroProcessor {
|
|||||||
* Process single macro
|
* Process single macro
|
||||||
*/
|
*/
|
||||||
async processMacro(macro) {
|
async processMacro(macro) {
|
||||||
const key = `${macro.actor}.${macro.method}`;
|
const plugin = this.macroRegistry.resolve(macro.actor, macro.method);
|
||||||
const plugin = this.plugins.get(key);
|
|
||||||
|
|
||||||
// Check for circular includes
|
|
||||||
if (macro.method === 'include') {
|
|
||||||
const path = macro.params.path || macro.params[''];
|
|
||||||
if (this.includeStack.includes(path)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Circular include detected: ${this.includeStack.join(' → ')} → ${path}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Unknown macro: !!${key}`
|
error: `Unknown macro: !!${macro.actor}.${macro.method}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate macro
|
// Check for circular includes
|
||||||
const validation = MacroParser.validateMacro(macro);
|
if (macro.method === 'include') {
|
||||||
if (!validation.valid) {
|
const path = macro.params.path;
|
||||||
return { success: false, error: validation.error };
|
if (this.includeStack.includes(path)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Circular include: ${this.includeStack.join(' → ')} → ${path}`
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute plugin
|
|
||||||
try {
|
try {
|
||||||
return await plugin.process(macro, this.webdavClient);
|
return await plugin.process(macro, this.webdavClient);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[MacroProcessor] Plugin error:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Plugin error: ${error.message}`
|
error: `Plugin error: ${error.message}`
|
||||||
@@ -128,34 +121,12 @@ class MacroProcessor {
|
|||||||
*/
|
*/
|
||||||
registerDefaultPlugins() {
|
registerDefaultPlugins() {
|
||||||
// Include plugin
|
// Include plugin
|
||||||
this.registerPlugin('core', 'include', {
|
this.macroRegistry.register('core', 'include', new IncludePlugin(this));
|
||||||
process: async (macro, webdavClient) => {
|
|
||||||
const path = macro.params.path || macro.params[''];
|
|
||||||
|
|
||||||
if (!path) {
|
// FAQ plugin
|
||||||
return {
|
this.macroRegistry.register('core', 'faq', new FAQPlugin(this));
|
||||||
success: false,
|
|
||||||
error: 'include macro requires "path" parameter'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
console.log('[MacroProcessor] Registered default plugins');
|
||||||
// Add to include stack
|
|
||||||
this.includeStack.push(path);
|
|
||||||
const content = await webdavClient.includeFile(path);
|
|
||||||
// Remove from include stack
|
|
||||||
this.includeStack.pop();
|
|
||||||
return { success: true, content };
|
|
||||||
} catch (error) {
|
|
||||||
// Remove from include stack on error
|
|
||||||
this.includeStack.pop();
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Failed to include "${path}": ${error.message}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
50
static/js/macro-system.js
Normal file
50
static/js/macro-system.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Macro System
|
||||||
|
* Generic plugin-based macro processor
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MacroPlugin {
|
||||||
|
/**
|
||||||
|
* Base class for macro plugins
|
||||||
|
* Subclass and implement these methods:
|
||||||
|
* - canHandle(actor, method): boolean
|
||||||
|
* - process(macro, context): Promise<{ success, content, error }>
|
||||||
|
*/
|
||||||
|
|
||||||
|
canHandle(actor, method) {
|
||||||
|
throw new Error('Must implement canHandle()');
|
||||||
|
}
|
||||||
|
|
||||||
|
async process(macro, context) {
|
||||||
|
throw new Error('Must implement process()');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MacroRegistry {
|
||||||
|
constructor() {
|
||||||
|
this.plugins = new Map();
|
||||||
|
console.log('[MacroRegistry] Initializing macro registry');
|
||||||
|
}
|
||||||
|
|
||||||
|
register(actor, method, plugin) {
|
||||||
|
const key = `${actor}.${method}`;
|
||||||
|
this.plugins.set(key, plugin);
|
||||||
|
console.log(`[MacroRegistry] Registered plugin: ${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(actor, method) {
|
||||||
|
// Try exact match
|
||||||
|
let key = `${actor}.${method}`;
|
||||||
|
if (this.plugins.has(key)) {
|
||||||
|
console.log(`[MacroRegistry] Found plugin: ${key}`);
|
||||||
|
return this.plugins.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No plugin found
|
||||||
|
console.warn(`[MacroRegistry] No plugin found for: ${key}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.MacroRegistry = MacroRegistry;
|
||||||
|
window.MacroPlugin = MacroPlugin;
|
||||||
70
static/js/plugins/faq-plugin.js
Normal file
70
static/js/plugins/faq-plugin.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* FAQ Plugin
|
||||||
|
* Creates FAQ entries that are collected and displayed at bottom of preview
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* !!faq title: 'My Question'
|
||||||
|
* response: '''
|
||||||
|
* This is the answer with **markdown** support.
|
||||||
|
*
|
||||||
|
* - Point 1
|
||||||
|
* - Point 2
|
||||||
|
* '''
|
||||||
|
*/
|
||||||
|
class FAQPlugin extends MacroPlugin {
|
||||||
|
constructor(processor) {
|
||||||
|
super();
|
||||||
|
this.processor = processor;
|
||||||
|
}
|
||||||
|
|
||||||
|
canHandle(actor, method) {
|
||||||
|
return actor === 'core' && method === 'faq';
|
||||||
|
}
|
||||||
|
|
||||||
|
async process(macro, webdavClient) {
|
||||||
|
const title = macro.params.title;
|
||||||
|
const response = macro.params.response;
|
||||||
|
|
||||||
|
console.log('[FAQPlugin] Processing FAQ:', title);
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
console.error('[FAQPlugin] Missing title parameter');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'FAQ macro requires "title" parameter'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
console.error('[FAQPlugin] Missing response parameter');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'FAQ macro requires "response" parameter'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Store FAQ item for later display
|
||||||
|
this.processor.faqItems.push({
|
||||||
|
title: title.trim(),
|
||||||
|
response: response.trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[FAQPlugin] FAQ item added, total:', this.processor.faqItems.length);
|
||||||
|
|
||||||
|
// Return empty string since FAQ is shown at bottom
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
content: ''
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[FAQPlugin] Error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `FAQ processing error: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.FAQPlugin = FAQPlugin;
|
||||||
97
static/js/plugins/include-plugin.js
Normal file
97
static/js/plugins/include-plugin.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Include Plugin
|
||||||
|
* Includes content from other files
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* !!include path: 'myfile.md'
|
||||||
|
* !!include path: 'collection:folder/file.md'
|
||||||
|
*/
|
||||||
|
class IncludePlugin extends MacroPlugin {
|
||||||
|
constructor(processor) {
|
||||||
|
super();
|
||||||
|
this.processor = processor;
|
||||||
|
}
|
||||||
|
|
||||||
|
canHandle(actor, method) {
|
||||||
|
return actor === 'core' && method === 'include';
|
||||||
|
}
|
||||||
|
|
||||||
|
async process(macro, webdavClient) {
|
||||||
|
const path = macro.params.path;
|
||||||
|
|
||||||
|
console.log('[IncludePlugin] Processing include:', path);
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
console.error('[IncludePlugin] Missing path parameter');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Include macro requires "path" parameter'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse path format: "collection:path/to/file" or "path/to/file"
|
||||||
|
let targetCollection = webdavClient.currentCollection;
|
||||||
|
let targetPath = path;
|
||||||
|
|
||||||
|
if (path.includes(':')) {
|
||||||
|
[targetCollection, targetPath] = path.split(':', 2);
|
||||||
|
console.log('[IncludePlugin] Using external collection:', targetCollection);
|
||||||
|
} else {
|
||||||
|
console.log('[IncludePlugin] Using current collection:', targetCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for circular includes
|
||||||
|
const fullPath = `${targetCollection}:${targetPath}`;
|
||||||
|
if (this.processor.includeStack.includes(fullPath)) {
|
||||||
|
console.error('[IncludePlugin] Circular include detected');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Circular include detected: ${this.processor.includeStack.join(' → ')} → ${fullPath}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to include stack
|
||||||
|
this.processor.includeStack.push(fullPath);
|
||||||
|
|
||||||
|
// Switch collection temporarily
|
||||||
|
const originalCollection = webdavClient.currentCollection;
|
||||||
|
webdavClient.setCollection(targetCollection);
|
||||||
|
|
||||||
|
// Fetch file
|
||||||
|
console.log('[IncludePlugin] Fetching:', targetPath);
|
||||||
|
const content = await webdavClient.get(targetPath);
|
||||||
|
|
||||||
|
// Restore collection
|
||||||
|
webdavClient.setCollection(originalCollection);
|
||||||
|
|
||||||
|
// Remove from stack
|
||||||
|
this.processor.includeStack.pop();
|
||||||
|
|
||||||
|
console.log('[IncludePlugin] Include successful, length:', content.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
content: content
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[IncludePlugin] Error:', error);
|
||||||
|
|
||||||
|
// Restore collection on error
|
||||||
|
if (webdavClient.currentCollection !== this.processor.webdavClient?.currentCollection) {
|
||||||
|
webdavClient.setCollection(this.processor.webdavClient?.currentCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processor.includeStack = this.processor.includeStack.filter(
|
||||||
|
item => !item.includes(path)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Cannot include "${path}": ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.IncludePlugin = IncludePlugin;
|
||||||
@@ -186,8 +186,12 @@
|
|||||||
<script src="/static/js/file-tree-actions.js" defer></script>
|
<script src="/static/js/file-tree-actions.js" defer></script>
|
||||||
<script src="/static/js/column-resizer.js" defer></script>
|
<script src="/static/js/column-resizer.js" defer></script>
|
||||||
<script src="/static/js/app.js" defer></script>
|
<script src="/static/js/app.js" defer></script>
|
||||||
<script src="/static/js/macro-parser.js" defer></script>
|
<!-- Macro System -->
|
||||||
<script src="/static/js/macro-processor.js" defer></script>
|
<script src="/static/js/macro-system.js" defer></script>
|
||||||
|
<script src="/static/js/plugins/include-plugin.js" defer></script>
|
||||||
|
<script src="/static/js/plugins/faq-plugin.js" defer></script>
|
||||||
|
<script src="/static/js/macro-parser.js" defer></script>
|
||||||
|
<script src="/static/js/macro-processor.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user