This commit is contained in:
2025-10-26 08:14:23 +04:00
parent 12b4685457
commit 5c9e07eee0
7 changed files with 213 additions and 140 deletions

View File

@@ -19,6 +19,8 @@ class MarkdownEditorApp:
"""Main application that wraps WsgiDAV and adds custom endpoints"""
def __init__(self, config_path="config.yaml"):
self.root_path = Path(__file__).parent.resolve()
os.chdir(self.root_path)
self.config = self.load_config(config_path)
self.collections = self.config.get('collections', {})
self.setup_collections()
@@ -72,21 +74,26 @@ class MarkdownEditorApp:
"""WSGI application entry point"""
path = environ.get('PATH_INFO', '')
method = environ.get('REQUEST_METHOD', '')
# Handle collection list endpoint
if path == '/fs/' and method == 'GET':
return self.handle_collections_list(environ, start_response)
# Handle static files
if path.startswith('/static/'):
return self.handle_static(environ, start_response)
# Handle root - serve index.html
# Root and index.html
if path == '/' or path == '/index.html':
return self.handle_index(environ, start_response)
# All other requests go to WebDAV
return self.webdav_app(environ, start_response)
# Static files
if path.startswith('/static/'):
return self.handle_static(environ, start_response)
# API for collections
if path == '/fs/' and method == 'GET':
return self.handle_collections_list(environ, start_response)
# All other /fs/ requests go to WebDAV
if path.startswith('/fs/'):
return self.webdav_app(environ, start_response)
# Fallback for anything else (shouldn't happen with correct linking)
start_response('404 Not Found', [('Content-Type', 'text/plain')])
return [b'Not Found']
def handle_collections_list(self, environ, start_response):
"""Return list of available collections"""
@@ -104,9 +111,9 @@ class MarkdownEditorApp:
def handle_static(self, environ, start_response):
"""Serve static files"""
path = environ.get('PATH_INFO', '')[1:] # Remove leading /
file_path = Path(path)
file_path = self.root_path / path
if not file_path.exists() or not file_path.is_file():
if not file_path.is_file():
start_response('404 Not Found', [('Content-Type', 'text/plain')])
return [b'File not found']
@@ -139,9 +146,9 @@ class MarkdownEditorApp:
def handle_index(self, environ, start_response):
"""Serve index.html"""
index_path = Path('templates/index.html')
index_path = self.root_path / 'templates' / 'index.html'
if not index_path.exists():
if not index_path.is_file():
start_response('404 Not Found', [('Content-Type', 'text/plain')])
return [b'index.html not found']

View File

@@ -1,5 +1,8 @@
#!/bin/bash
set -e
# Change to the script's directory to ensure relative paths work
cd "$(dirname "$0")"
echo "=============================================="
echo "Markdown Editor v3.0 - WebDAV Server"
echo "=============================================="
@@ -16,5 +19,8 @@ echo "Activating virtual environment..."
source .venv/bin/activate
echo "Installing dependencies..."
uv pip install wsgidav cheroot pyyaml
PORT=8004
echo "Checking for process on port $PORT..."
lsof -ti:$PORT | xargs -r kill -9
echo "Starting WebDAV server..."
python server_webdav.py

View File

@@ -132,3 +132,31 @@ html, body {
background: var(--text-secondary);
}
/* Preview Pane Styling */
#previewPane {
flex: 1 1 40%;
min-width: 250px;
max-width: 70%;
padding: 0;
overflow-y: auto;
overflow-x: hidden;
background-color: var(--bg-primary);
border-left: 1px solid var(--border-color);
}
#preview {
padding: 20px;
min-height: 100%;
overflow-wrap: break-word;
word-wrap: break-word;
}
#preview > p:first-child {
margin-top: 0;
}
#preview > h1:first-child,
#preview > h2:first-child {
margin-top: 0;
}

View File

@@ -12,6 +12,23 @@ let collectionSelector;
let clipboard = null;
let currentFilePath = null;
// Simple event bus
const eventBus = {
listeners: {},
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
},
dispatch(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
};
window.eventBus = eventBus;
// Initialize application
document.addEventListener('DOMContentLoaded', async () => {
// Initialize WebDAV client
@@ -26,7 +43,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// Initialize file tree
fileTree = new FileTree('fileTree', webdavClient);
fileTree.onFileSelect = async (item) => {
await loadFile(item.path);
await editor.loadFile(item.path);
};
// Initialize collection selector
@@ -39,6 +56,15 @@ document.addEventListener('DOMContentLoaded', async () => {
// Initialize editor
editor = new MarkdownEditor('editor', 'preview', 'filenameInput');
editor.setWebDAVClient(webdavClient);
// Add test content to verify preview works
setTimeout(() => {
if (!editor.editor.getValue()) {
editor.editor.setValue('# Welcome to Markdown Editor\n\nStart typing to see preview...\n');
editor.updatePreview();
}
}, 200);
// Setup editor drop handler
const editorDropHandler = new EditorDropHandler(
@@ -50,15 +76,15 @@ document.addEventListener('DOMContentLoaded', async () => {
// Setup button handlers
document.getElementById('newBtn').addEventListener('click', () => {
newFile();
editor.newFile();
});
document.getElementById('saveBtn').addEventListener('click', async () => {
await saveFile();
await editor.save();
});
document.getElementById('deleteBtn').addEventListener('click', async () => {
await deleteCurrentFile();
await editor.deleteFile();
});
// Setup context menu handlers
@@ -69,6 +95,13 @@ document.addEventListener('DOMContentLoaded', async () => {
// Initialize file tree actions manager
window.fileTreeActions = new FileTreeActions(webdavClient, fileTree, editor);
// Listen for file-saved event to reload file tree
window.eventBus.on('file-saved', async (path) => {
if (fileTree) {
await fileTree.load();
fileTree.selectNode(path);
}
});
});
// Listen for column resize events to refresh editor
@@ -81,66 +114,6 @@ window.addEventListener('column-resize', () => {
/**
* File Operations
*/
async function loadFile(path) {
try {
const content = await webdavClient.get(path);
editor.setValue(content);
document.getElementById('filenameInput').value = path;
currentFilePath = path;
showNotification('File loaded', 'success');
} catch (error) {
console.error('Failed to load file:', error);
showNotification('Failed to load file', 'error');
}
}
function newFile() {
editor.setValue('# New File\n\nStart typing...\n');
document.getElementById('filenameInput').value = '';
document.getElementById('filenameInput').focus();
currentFilePath = null;
showNotification('New file', 'info');
}
async function saveFile() {
const filename = document.getElementById('filenameInput').value.trim();
if (!filename) {
showNotification('Please enter a filename', 'warning');
return;
}
try {
const content = editor.getValue();
await webdavClient.put(filename, content);
currentFilePath = filename;
await fileTree.load();
showNotification('Saved', 'success');
} catch (error) {
console.error('Failed to save file:', error);
showNotification('Failed to save file', 'error');
}
}
async function deleteCurrentFile() {
if (!currentFilePath) {
showNotification('No file selected', 'warning');
return;
}
if (!confirm(`Delete ${currentFilePath}?`)) {
return;
}
try {
await webdavClient.delete(currentFilePath);
await fileTree.load();
newFile();
showNotification('Deleted', 'success');
} catch (error) {
console.error('Failed to delete file:', error);
showNotification('Failed to delete file', 'error');
}
}
/**
* Context Menu Handlers
@@ -166,7 +139,7 @@ async function handleContextAction(action, targetPath, isDir) {
switch (action) {
case 'open':
if (!isDir) {
await loadFile(targetPath);
await editor.loadFile(targetPath);
}
break;

View File

@@ -32,10 +32,15 @@ class MarkdownEditor {
}
});
// Update preview on change
this.editor.on('change', () => {
// Update preview on change with debouncing
this.editor.on('change', this.debounce(() => {
this.updatePreview();
});
}, 300));
// Initial preview render
setTimeout(() => {
this.updatePreview();
}, 100);
// Sync scroll
this.editor.on('scroll', () => {
@@ -47,17 +52,21 @@ class MarkdownEditor {
* Initialize markdown parser
*/
initMarkdown() {
this.marked = window.marked;
this.marked.setOptions({
breaks: true,
gfm: true,
highlight: (code, lang) => {
if (lang && window.Prism.languages[lang]) {
return window.Prism.highlight(code, window.Prism.languages[lang], lang);
if (window.marked) {
this.marked = window.marked;
this.marked.setOptions({
breaks: true,
gfm: true,
highlight: (code, lang) => {
if (lang && window.Prism.languages[lang]) {
return window.Prism.highlight(code, window.Prism.languages[lang], lang);
}
return code;
}
return code;
}
});
});
} else {
console.error('Marked library not found.');
}
}
/**
@@ -123,10 +132,9 @@ class MarkdownEditor {
window.showNotification('✅ Saved', 'success');
}
// Trigger file tree reload
if (window.fileTree) {
await window.fileTree.load();
window.fileTree.selectNode(path);
// Dispatch event to reload file tree
if (window.eventBus) {
window.eventBus.dispatch('file-saved', path);
}
} catch (error) {
console.error('Failed to save file:', error);
@@ -192,24 +200,66 @@ class MarkdownEditor {
*/
updatePreview() {
const markdown = this.editor.getValue();
let html = window.marked.parse(markdown);
const previewDiv = this.previewElement;
// Process mermaid diagrams
html = html.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, (match, code) => {
const id = 'mermaid-' + Math.random().toString(36).substr(2, 9);
return `<div class="mermaid" id="${id}">${code}</div>`;
});
this.previewElement.innerHTML = html;
// Render mermaid diagrams
if (window.mermaid) {
window.mermaid.init(undefined, this.previewElement.querySelectorAll('.mermaid'));
if (!markdown || !markdown.trim()) {
previewDiv.innerHTML = `
<div class="text-muted text-center mt-5">
<p>Start typing to see preview...</p>
</div>
`;
return;
}
// Highlight code blocks
if (window.Prism) {
window.Prism.highlightAllUnder(this.previewElement);
try {
// Parse markdown to HTML
if (!this.marked) {
console.error("Markdown parser (marked) not initialized.");
previewDiv.innerHTML = `<div class="alert alert-danger">Preview engine not loaded.</div>`;
return;
}
let html = this.marked.parse(markdown);
// Replace mermaid code blocks with div containers
html = html.replace(
/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
(match, code) => {
const id = 'mermaid-' + Math.random().toString(36).substr(2, 9);
return `<div class="mermaid" id="${id}">${code.trim()}</div>`;
}
);
previewDiv.innerHTML = html;
// Apply syntax highlighting to code blocks
const codeBlocks = previewDiv.querySelectorAll('pre code');
codeBlocks.forEach(block => {
const languageClass = Array.from(block.classList)
.find(cls => cls.startsWith('language-'));
if (languageClass && languageClass !== 'language-mermaid') {
if (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);
}
}
} catch (error) {
console.error('Preview rendering error:', error);
previewDiv.innerHTML = `
<div class="alert alert-danger" role="alert">
<strong>Error rendering preview:</strong><br>
${error.message}
</div>
`;
}
}
@@ -266,6 +316,21 @@ class MarkdownEditor {
setValue(content) {
this.editor.setValue(content);
}
/**
* Debounce function
*/
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
// Export for use in other modules

View File

@@ -92,29 +92,24 @@ function hideContextMenu() {
}
}
// Context menu item click handler
// Combined click handler for context menu and outside clicks
document.addEventListener('click', async (e) => {
const menuItem = e.target.closest('.context-menu-item');
if (!menuItem) {
hideContextMenu();
return;
}
const action = menuItem.dataset.action;
const menu = document.getElementById('contextMenu');
const targetPath = menu.dataset.targetPath;
const isDir = menu.dataset.targetIsDir === 'true';
hideContextMenu();
if (window.fileTreeActions) {
await window.fileTreeActions.execute(action, targetPath, isDir);
}
});
if (menuItem) {
// Handle context menu item click
const action = menuItem.dataset.action;
const menu = document.getElementById('contextMenu');
const targetPath = menu.dataset.targetPath;
const isDir = menu.dataset.targetIsDir === 'true';
// Hide on outside click
document.addEventListener('click', (e) => {
if (!e.target.closest('#contextMenu') && !e.target.closest('.tree-node')) {
hideContextMenu();
if (window.fileTreeActions) {
await window.fileTreeActions.execute(action, targetPath, isDir);
}
} else if (!e.target.closest('#contextMenu') && !e.target.closest('.tree-node')) {
// Hide on outside click
hideContextMenu();
}
});

View File

@@ -80,7 +80,6 @@
<!-- Preview Pane -->
<div class="col preview-pane" id="previewPane">
<h3>Preview</h3>
<div id="preview">
<p class="text-muted">Start typing in the editor to see the preview</p>
</div>
@@ -158,13 +157,13 @@
<!-- Mermaid for diagrams -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script src="/static/js/file-tree-actions.js"></script>
<script src="/static/js/webdav-client.js"></script>
<script src="/static/js/file-tree.js"></script>
<script src="/static/js/editor.js"></script>
<script src="/static/js/ui-utils.js"></script>
<script src="/static/js/app.js"></script>
<script src="/static/js/column-resizer.js"></script>
<script src="/static/js/webdav-client.js" defer></script>
<script src="/static/js/file-tree.js" defer></script>
<script src="/static/js/editor.js" defer></script>
<script src="/static/js/ui-utils.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/app.js" defer></script>
</body>
</html>