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%;'" > - +
+
+ +