From 5c9e07eee05e4cf9ad58ef1f136c10c61fd2c045 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sun, 26 Oct 2025 08:14:23 +0400 Subject: [PATCH] ... --- server_webdav.py | 39 +++++++------ start.sh | 6 ++ static/css/layout.css | 28 +++++++++ static/js/app.js | 103 +++++++++++++-------------------- static/js/editor.js | 129 +++++++++++++++++++++++++++++++----------- static/js/ui-utils.js | 33 +++++------ templates/index.html | 15 +++-- 7 files changed, 213 insertions(+), 140 deletions(-) diff --git a/server_webdav.py b/server_webdav.py index bb955cc..b631b58 100755 --- a/server_webdav.py +++ b/server_webdav.py @@ -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'] diff --git a/start.sh b/start.sh index b4705fd..ea907c0 100755 --- a/start.sh +++ b/start.sh @@ -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 diff --git a/static/css/layout.css b/static/css/layout.css index 8004e0c..08ba397 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -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; +} \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index ed2c8e2..1a52ef8 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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; diff --git a/static/js/editor.js b/static/js/editor.js index 32a92ae..4059078 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -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(/
([\s\S]*?)<\/code><\/pre>/g, (match, code) => {
-            const id = 'mermaid-' + Math.random().toString(36).substr(2, 9);
-            return `
${code}
`; - }); - - this.previewElement.innerHTML = html; - - // Render mermaid diagrams - if (window.mermaid) { - window.mermaid.init(undefined, this.previewElement.querySelectorAll('.mermaid')); + if (!markdown || !markdown.trim()) { + previewDiv.innerHTML = ` +
+

Start typing to see preview...

+
+ `; + 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 = `
Preview engine not loaded.
`; + return; + } + let html = this.marked.parse(markdown); + + // Replace mermaid code blocks with div containers + html = html.replace( + /
([\s\S]*?)<\/code><\/pre>/g,
+                (match, code) => {
+                    const id = 'mermaid-' + Math.random().toString(36).substr(2, 9);
+                    return `
${code.trim()}
`; + } + ); + + 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 = ` + + `; } } @@ -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 diff --git a/static/js/ui-utils.js b/static/js/ui-utils.js index d083c47..afc5057 100644 --- a/static/js/ui-utils.js +++ b/static/js/ui-utils.js @@ -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(); } }); diff --git a/templates/index.html b/templates/index.html index 9f02fbd..b6f645c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -80,7 +80,6 @@
-

Preview

Start typing in the editor to see the preview

@@ -158,13 +157,13 @@ - - - - - - - + + + + + + + \ No newline at end of file