...
This commit is contained in:
		| @@ -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'] | ||||
|          | ||||
|   | ||||
							
								
								
									
										6
									
								
								start.sh
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								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 | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
							
								
								
									
										103
									
								
								static/js/app.js
									
									
									
									
									
								
							
							
						
						
									
										103
									
								
								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; | ||||
|              | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| }); | ||||
|   | ||||
| @@ -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> | ||||
		Reference in New Issue
	
	Block a user