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