From f5567a71b3fd2d34056752e5657e0c95f3fadd49 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Wed, 26 Nov 2025 11:28:09 +0100 Subject: [PATCH 1/9] added templating system --- backend/main.py | 96 ++++++++++++++++++++++++++++++++++ backend/utils.py | 105 ++++++++++++++++++++++++++++++++++++++ documentation/FEATURES.md | 44 ++++++++++++++++ frontend/app.js | 72 ++++++++++++++++++++++++++ frontend/index.html | 97 ++++++++++++++++++++++++++++++++++- 5 files changed, 413 insertions(+), 1 deletion(-) diff --git a/backend/main.py b/backend/main.py index 8b2539e..37869f9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -38,6 +38,9 @@ from .utils import ( validate_path_security, get_all_tags, get_notes_by_tag, + get_templates, + get_template_content, + apply_template_placeholders, ) from .plugins import PluginManager from .themes import get_available_themes, get_theme_css @@ -777,6 +780,99 @@ async def get_notes_by_tag_endpoint(tag_name: str): raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to get notes by tag")) +# --- Template Endpoints --- + +@api_router.get("/templates") +async def list_templates(): + """ + List all available templates from _templates folder. + + Returns: + List of template metadata + """ + try: + templates = get_templates(config['storage']['notes_dir']) + return {"templates": templates} + except Exception as e: + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to list templates")) + + +@api_router.get("/templates/{template_name}") +async def get_template(template_name: str): + """ + Get content of a specific template. + + Args: + template_name: Name of the template (without .md extension) + + Returns: + Template name and content + """ + try: + content = get_template_content(config['storage']['notes_dir'], template_name) + + if content is None: + raise HTTPException(status_code=404, detail="Template not found") + + return { + "name": template_name, + "content": content + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to get template")) + + +@api_router.post("/templates/create-note") +@limiter.limit("60/minute") +async def create_note_from_template(request: Request, data: dict): + """ + Create a new note from a template with placeholder replacement. + + Args: + data: Dictionary containing templateName and notePath + + Returns: + Success status, path, and created content + """ + try: + template_name = data.get('templateName', '') + note_path = data.get('notePath', '') + + if not template_name or not note_path: + raise HTTPException(status_code=400, detail="Template name and note path required") + + # Get template content + template_content = get_template_content(config['storage']['notes_dir'], template_name) + + if template_content is None: + raise HTTPException(status_code=404, detail="Template not found") + + # Apply placeholder replacements + final_content = apply_template_placeholders(template_content, note_path) + + # Save the note + success = save_note(config['storage']['notes_dir'], note_path, final_content) + + if not success: + raise HTTPException(status_code=500, detail="Failed to create note from template") + + # Run plugin hooks + plugin_manager.run_hook('on_note_create', note_path=note_path, initial_content=final_content) + + return { + "success": True, + "path": note_path, + "message": "Note created from template successfully", + "content": final_content + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to create note from template")) + + # --- Notes Endpoints --- @api_router.get("/notes") diff --git a/backend/utils.py b/backend/utils.py index 8459805..cd69ff3 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -647,3 +647,108 @@ def get_notes_by_tag(notes_dir: str, tag: str) -> List[Dict]: return matching_notes + +# ============================================================================ +# Template Functions +# ============================================================================ + +def get_templates(notes_dir: str) -> List[Dict]: + """ + Get all templates from the _templates folder. + + Args: + notes_dir: Base notes directory + + Returns: + List of template metadata (name, path, modified) + """ + templates = [] + templates_path = Path(notes_dir) / "_templates" + + if not templates_path.exists(): + return templates + + try: + for template_file in templates_path.glob("*.md"): + try: + stat = template_file.stat() + templates.append({ + "name": template_file.stem, + "path": str(template_file.relative_to(notes_dir).as_posix()), + "modified": datetime.fromtimestamp(stat.st_mtime).isoformat() + }) + except Exception as e: + print(f"Error reading template {template_file}: {e}") + continue + except Exception as e: + print(f"Error accessing templates directory: {e}") + + return sorted(templates, key=lambda x: x['name']) + + +def get_template_content(notes_dir: str, template_name: str) -> Optional[str]: + """ + Get the content of a specific template. + + Args: + notes_dir: Base notes directory + template_name: Name of the template (without .md extension) + + Returns: + Template content or None if not found + """ + template_path = Path(notes_dir) / "_templates" / f"{template_name}.md" + + if not template_path.exists(): + return None + + # Security check: ensure template is within notes directory + if not validate_path_security(notes_dir, template_path): + print(f"Security: Template path is outside notes directory: {template_path}") + return None + + try: + with open(template_path, 'r', encoding='utf-8') as f: + return f.read() + except Exception as e: + print(f"Error reading template {template_name}: {e}") + return None + + +def apply_template_placeholders(content: str, note_path: str) -> str: + """ + Replace template placeholders with actual values. + + Supported placeholders: + {{date}} - Current date (YYYY-MM-DD) + {{time}} - Current time (HH:MM:SS) + {{datetime}} - Current datetime (YYYY-MM-DD HH:MM:SS) + {{timestamp}} - Unix timestamp + {{title}} - Note name without extension + {{folder}} - Parent folder name + + Args: + content: Template content with placeholders + note_path: Path of the note being created + + Returns: + Content with placeholders replaced + """ + now = datetime.now() + note = Path(note_path) + + replacements = { + '{{date}}': now.strftime('%Y-%m-%d'), + '{{time}}': now.strftime('%H:%M:%S'), + '{{datetime}}': now.strftime('%Y-%m-%d %H:%M:%S'), + '{{timestamp}}': str(int(now.timestamp())), + '{{title}}': note.stem, + '{{folder}}': note.parent.name if str(note.parent) != '.' else 'Root', + } + + result = content + for placeholder, value in replacements.items(): + result = result.replace(placeholder, value) + + return result + diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md index 7509ef7..8685a02 100644 --- a/documentation/FEATURES.md +++ b/documentation/FEATURES.md @@ -7,6 +7,7 @@ - **Three view modes**: Edit, Split, Preview - **Auto-save** - Never lose your work - **Undo/Redo** - Ctrl+Z / Ctrl+Y support +- **Note templates** - Create notes from templates with dynamic placeholders - **Syntax highlighting** for code blocks (50+ languages) - **Copy code blocks** - One-click copy button on hover - **LaTeX/Math rendering** - Beautiful mathematical equations with MathJax (see [MATHJAX.md](MATHJAX.md)) @@ -162,6 +163,49 @@ graph TD ๐Ÿ“„ **See the [MERMAID](MERMAID.md) note for diagram examples and syntax reference.** +## ๐Ÿ“„ Note Templates + +Create notes from reusable templates with dynamic placeholder replacement. + +### Creating Templates +1. Create markdown files in `data/_templates/` folder +2. Use placeholders for dynamic content +3. Templates appear in the "New from Template" menu + +### Available Placeholders +- `{{date}}` - Current date (YYYY-MM-DD) +- `{{time}}` - Current time (HH:MM:SS) +- `{{datetime}}` - Current date and time +- `{{timestamp}}` - Unix timestamp +- `{{title}}` - Note name (without extension) +- `{{folder}}` - Parent folder name + +### Example Template +```markdown +--- +tags: [meeting] +date: {{date}} +--- + +# {{title}} + +**Created:** {{datetime}} + +## Notes + +``` + +### Using Templates +1. Click the "New" dropdown button +2. Select "New from Template" +3. Choose a template and enter a note name +4. The new note will be created with placeholders replaced + +### Built-in Templates +- **meeting-notes** - Template for meeting notes +- **daily-journal** - Daily journal with morning goals and evening reflection +- **project-plan** - Project planning template with objectives and timeline + ## โšก Keyboard Shortcuts ### General diff --git a/frontend/app.js b/frontend/app.js index 6e3bc15..49364ef 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -100,6 +100,12 @@ function noteApp() { // Dropdown state showNewDropdown: false, + // Template state + showTemplateModal: false, + availableTemplates: [], + selectedTemplate: '', + newTemplateNoteName: '', + // Homepage state selectedHomepageFolder: '', _homepageCache: { @@ -258,6 +264,7 @@ function noteApp() { await this.loadThemes(); await this.initTheme(); await this.loadNotes(); + await this.loadTemplates(); await this.checkStatsPlugin(); this.loadSidebarWidth(); this.loadEditorWidth(); @@ -591,6 +598,71 @@ function noteApp() { this.applyFilters(); }, + // ======================================================================== + // Template Methods + // ======================================================================== + + // Load available templates from _templates folder + async loadTemplates() { + try { + const response = await fetch('/api/templates'); + const data = await response.json(); + this.availableTemplates = data.templates || []; + } catch (error) { + ErrorHandler.handle('load templates', error, false); // Don't show alert, templates are optional + } + }, + + // Create a new note from a template + async createNoteFromTemplate() { + if (!this.selectedTemplate || !this.newTemplateNoteName.trim()) { + return; + } + + try { + // Determine the note path based on current context + let notePath = this.newTemplateNoteName.trim(); + if (!notePath.endsWith('.md')) { + notePath += '.md'; + } + + // If we're in a folder on the homepage, create in that folder + if (this.selectedHomepageFolder) { + notePath = `${this.selectedHomepageFolder}/${notePath}`; + } + + // Create note from template + const response = await fetch('/api/templates/create-note', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + templateName: this.selectedTemplate, + notePath: notePath + }) + }); + + if (!response.ok) { + const error = await response.json(); + alert(error.detail || 'Failed to create note from template'); + return; + } + + const data = await response.json(); + + // Close modal and reset state + this.showTemplateModal = false; + this.selectedTemplate = ''; + this.newTemplateNoteName = ''; + + // Reload notes and open the new note + await this.loadNotes(); + await this.loadNote(data.path); + + } catch (error) { + ErrorHandler.handle('create note from template', error); + } + }, + // Clear all tag filters clearTagFilters() { this.selectedTags = []; diff --git a/frontend/index.html b/frontend/index.html index 0e890c5..319e40c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -944,7 +944,17 @@ > ๐Ÿ“ New Folder - + + @@ -1618,6 +1628,91 @@ + +
+
+ +

Create from Template

+ + +
+ + +
+ + +
+ + +
+ + +
+ Available placeholders:
+ {{date}}, + {{time}}, + {{datetime}}, + {{title}}, + {{folder}} +
+ + +
+ No templates found. Create templates in the _templates folder. +
+ + +
+ + +
+
+
+ From 3bc63178dca81258a9d2cfd1e93ebb9583665261 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Wed, 26 Nov 2025 11:30:33 +0100 Subject: [PATCH 2/9] escape key support --- frontend/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/index.html b/frontend/index.html index 319e40c..e528671 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1631,6 +1631,7 @@
From 54df891e945fcc65639609758115b7f3b29481a9 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Wed, 26 Nov 2025 11:40:20 +0100 Subject: [PATCH 3/9] added hamburger menu to landing page --- frontend/index.html | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index e528671..54114cb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -615,6 +615,12 @@ } /* Mobile Responsive Styles */ + /* Z-index hierarchy (for reference): + - Mobile sidebar: 1000 + - Mobile overlay: 999 + - Template modal: 1100 (must be above sidebar) + - Dropdown menu: 50 (below everything) + */ @media (max-width: 768px) { /* Hide sidebar by default on mobile */ .mobile-sidebar { @@ -946,7 +952,7 @@ New Folder +
+