From 62625b88979bd2d813a124a2ac9a8d3db69c549a Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Tue, 9 Dec 2025 15:33:49 +0100 Subject: [PATCH 1/5] added theme-aware basic syntax highlight for editor --- frontend/app.js | 92 ++++++++++++++++++++++ frontend/index.html | 186 +++++++++++++++++++++++++++++++------------- 2 files changed, 223 insertions(+), 55 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index 636e6a3..a8aa2a9 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -61,6 +61,10 @@ function noteApp() { currentTheme: 'light', availableThemes: [], + // Syntax highlighting + syntaxHighlightEnabled: false, + syntaxHighlightTimeout: null, + // Icon rail / panel state activePanel: 'files', // 'files', 'search', 'tags', 'settings' @@ -297,6 +301,7 @@ function noteApp() { this.loadEditorWidth(); this.loadViewMode(); this.loadTagsExpanded(); + this.loadSyntaxHighlightSetting(); // Parse URL and load specific note if provided this.loadNoteFromURL(); @@ -515,6 +520,93 @@ function noteApp() { await this.applyTheme(themeId); }, + // Syntax highlighting toggle + toggleSyntaxHighlight() { + this.syntaxHighlightEnabled = !this.syntaxHighlightEnabled; + localStorage.setItem('syntaxHighlightEnabled', this.syntaxHighlightEnabled); + if (this.syntaxHighlightEnabled) { + this.updateSyntaxHighlight(); + } + }, + + loadSyntaxHighlightSetting() { + try { + const saved = localStorage.getItem('syntaxHighlightEnabled'); + this.syntaxHighlightEnabled = saved === 'true'; + } catch (error) { + console.error('Error loading syntax highlight setting:', error); + } + }, + + // Update syntax highlight overlay (debounced, called on input) + updateSyntaxHighlight() { + if (!this.syntaxHighlightEnabled) return; + + clearTimeout(this.syntaxHighlightTimeout); + this.syntaxHighlightTimeout = setTimeout(() => { + const overlay = document.getElementById('syntax-overlay'); + if (overlay) { + overlay.innerHTML = this.highlightMarkdown(this.noteContent); + } + }, 50); // 50ms debounce + }, + + // Sync overlay scroll with textarea + syncOverlayScroll() { + const textarea = document.getElementById('note-editor'); + const overlay = document.getElementById('syntax-overlay'); + if (textarea && overlay) { + overlay.scrollTop = textarea.scrollTop; + overlay.scrollLeft = textarea.scrollLeft; + } + }, + + // Highlight markdown syntax + highlightMarkdown(text) { + if (!text) return ''; + + // Escape HTML first + let html = this.escapeHtml(text); + + // Frontmatter (must be at start) + html = html.replace(/^(---\n[\s\S]*?\n---)/m, '$1'); + + // Code blocks (before other patterns to avoid conflicts) + html = html.replace(/(```[\s\S]*?```)/g, '$1'); + + // Headings + html = html.replace(/^(#{1,6})\s(.*)$/gm, '$1 $2'); + + // Bold (must come before italic) + html = html.replace(/\*\*([^*]+)\*\*/g, '**$1**'); + html = html.replace(/__([^_]+)__/g, '__$1__'); + + // Italic + html = html.replace(/(?*$1*'); + html = html.replace(/(?_$1_'); + + // Inline code + html = html.replace(/`([^`\n]+)`/g, '`$1`'); + + // Wikilinks [[...]] + html = html.replace(/\[\[([^\]]+)\]\]/g, '[[$1]]'); + + // Links [text](url) + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$1]($2)'); + + // Lists + html = html.replace(/^(\s*)([-*+])\s/gm, '$1$2 '); + html = html.replace(/^(\s*)(\d+\.)\s/gm, '$1$2 '); + + // Blockquotes + html = html.replace(/^(>.*)$/gm, '$1'); + + // Horizontal rules + html = html.replace(/^([-*_]{3,})$/gm, '$1'); + + return html; + }, + // Apply theme to document async applyTheme(themeId) { // Load theme CSS from file diff --git a/frontend/index.html b/frontend/index.html index 5fe84a7..a2f9ec0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -438,6 +438,52 @@ color: var(--text-primary) !important; } + /* Syntax Highlighting Overlay */ + .editor-wrapper { + position: relative; + flex: 1; + display: flex; + min-height: 0; + } + .editor-wrapper .editor-textarea { + position: relative; + z-index: 1; + } + .editor-wrapper.syntax-highlight .editor-textarea { + color: transparent !important; + caret-color: var(--text-primary); + background: transparent !important; + } + .syntax-overlay { + position: absolute; + inset: 0; + padding: 1.5rem; + pointer-events: none; + white-space: pre-wrap; + word-wrap: break-word; + overflow: hidden; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: inherit; + line-height: inherit; + tab-size: 2; + color: var(--text-primary); + background-color: var(--bg-primary); + z-index: 0; + } + /* Syntax highlight colors using theme variables */ + .syntax-overlay .md-heading { color: var(--accent-primary); font-weight: 600; } + .syntax-overlay .md-bold { color: var(--text-primary); font-weight: 700; } + .syntax-overlay .md-italic { color: var(--text-secondary); font-style: italic; } + .syntax-overlay .md-code { color: var(--accent-secondary, var(--accent-primary)); background: var(--bg-tertiary); padding: 1px 3px; border-radius: 3px; } + .syntax-overlay .md-link { color: var(--link-color, var(--accent-primary)); } + .syntax-overlay .md-link-url { color: var(--text-tertiary); } + .syntax-overlay .md-list { color: var(--accent-primary); } + .syntax-overlay .md-blockquote { color: var(--text-tertiary); font-style: italic; } + .syntax-overlay .md-hr { color: var(--border-primary); } + .syntax-overlay .md-wikilink { color: var(--link-internal, var(--accent-primary)); } + .syntax-overlay .md-frontmatter { color: var(--text-tertiary); } + .syntax-overlay .md-codeblock { color: var(--text-secondary); background: var(--bg-tertiary); } + /* Link drop zone cursor */ .editor-textarea.link-drop-target { cursor: copy !important; @@ -984,7 +1030,7 @@ > - + + - + - +
- - + + + - - - + + + +
@@ -1248,9 +1294,9 @@ >
-
- -
+ + + @@ -1806,19 +1870,31 @@ style="min-height: 0;" :style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'" > - +
+
+ +
From 598901e629060ba729430790683a8edded50e268 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Wed, 10 Dec 2025 09:10:58 +0100 Subject: [PATCH 2/5] fix for some blocks --- frontend/app.js | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index a8aa2a9..4d25641 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -568,11 +568,28 @@ function noteApp() { // Escape HTML first let html = this.escapeHtml(text); - // Frontmatter (must be at start) - html = html.replace(/^(---\n[\s\S]*?\n---)/m, '$1'); + // Store code blocks and inline code with placeholders to protect from other patterns + const codePlaceholders = []; - // Code blocks (before other patterns to avoid conflicts) - html = html.replace(/(```[\s\S]*?```)/g, '$1'); + // Frontmatter (must be at start) - protect it + html = html.replace(/^(---\n[\s\S]*?\n---)/m, (match) => { + codePlaceholders.push('' + match + ''); + return `\x00CODE${codePlaceholders.length - 1}\x00`; + }); + + // Code blocks - protect them + html = html.replace(/(```[\s\S]*?```)/g, (match) => { + codePlaceholders.push('' + match + ''); + return `\x00CODE${codePlaceholders.length - 1}\x00`; + }); + + // Inline code - protect it + html = html.replace(/`([^`\n]+)`/g, (match) => { + codePlaceholders.push('' + match + ''); + return `\x00CODE${codePlaceholders.length - 1}\x00`; + }); + + // Now apply other patterns (they won't match inside protected code) // Headings html = html.replace(/^(#{1,6})\s(.*)$/gm, '$1 $2'); @@ -585,9 +602,6 @@ function noteApp() { html = html.replace(/(?*$1*'); html = html.replace(/(?_$1_'); - // Inline code - html = html.replace(/`([^`\n]+)`/g, '`$1`'); - // Wikilinks [[...]] html = html.replace(/\[\[([^\]]+)\]\]/g, '[[$1]]'); @@ -604,6 +618,9 @@ function noteApp() { // Horizontal rules html = html.replace(/^([-*_]{3,})$/gm, '$1'); + // Restore protected code blocks + html = html.replace(/\x00CODE(\d+)\x00/g, (match, index) => codePlaceholders[parseInt(index)]); + return html; }, From b97eb70287147f731caca50e73815c6ac6d099c9 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Wed, 10 Dec 2025 09:17:25 +0100 Subject: [PATCH 3/5] added hack to use notes with extension .md --- frontend/app.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/app.js b/frontend/app.js index 4d25641..8e8aff3 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1910,7 +1910,14 @@ function noteApp() { // Load note from URL path loadNoteFromURL() { // Get path from URL (e.g., /folder/note or /note) - const path = window.location.pathname; + let path = window.location.pathname; + + // Strip .md extension if present (for MKdocs/Zensical integration) + if (path.toLowerCase().endsWith('.md')) { + path = path.slice(0, -3); + // Update URL bar to show clean path without .md + window.history.replaceState(null, '', path); + } // Skip if root path or static assets if (path === '/' || path.startsWith('/static/') || path.startsWith('/api/')) { From 670a6b22d751df9d0ccf6c48e2f2166e70b4c27e Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Wed, 10 Dec 2025 09:41:54 +0100 Subject: [PATCH 4/5] prevent double initialization (Alpine.js may call x-init twice in some cases) --- frontend/app.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/app.js b/frontend/app.js index 8e8aff3..308924e 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -281,6 +281,10 @@ function noteApp() { // Initialize app async init() { + // Prevent double initialization (Alpine.js may call x-init twice in some cases) + if (window.__noteapp_initialized) return; + window.__noteapp_initialized = true; + // Store global reference for native event handlers in x-html content window.$root = this; From 646b0acc4719575207b3dc45005795f30df210af Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Wed, 10 Dec 2025 09:45:12 +0100 Subject: [PATCH 5/5] fixed bug not showing slected theme in dropdown --- frontend/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/index.html b/frontend/index.html index a2f9ec0..527e5ff 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1422,7 +1422,7 @@ style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary); cursor: pointer;" >