From 581a95b1c8f4a27f33bc6daa2139e01214daabff Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Mon, 29 Jun 2026 12:19:06 +0200 Subject: [PATCH 1/2] added callout support --- backend/export.py | 83 +++++++++++++++++++++++++++++++++++++-- documentation/FEATURES.md | 28 +++++++++++++ frontend/app.js | 48 ++++++++++++++++++++++ frontend/index.html | 32 ++++++++++++++- 4 files changed, 187 insertions(+), 4 deletions(-) diff --git a/backend/export.py b/backend/export.py index e8ae206..a5d45de 100644 --- a/backend/export.py +++ b/backend/export.py @@ -527,7 +527,37 @@ def generate_export_html( border-left: 4px solid var(--accent-primary, #0366d6); color: var(--text-secondary, #6a737d); }} - + + /* Callouts / admonitions (GitHub-style: > [!NOTE], > [!TIP], etc.) + Mirrors the in-app preview styles so exported/shared notes match. + Hard-coded GitHub palette with rgba backgrounds blends with any theme. */ + .markdown-preview .callout {{ + margin: 1rem 0; + padding: 0.75rem 1rem; + border-left: 4px solid var(--callout-color, var(--accent-primary, #0366d6)); + border-radius: 0.375rem; + background: var(--callout-bg, var(--bg-secondary, #f6f8fa)); + }} + .markdown-preview .callout-title {{ + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + color: var(--callout-color, var(--accent-primary, #0366d6)); + margin-bottom: 0.25rem; + }} + .markdown-preview .callout-icon {{ + font-size: 1.1em; + line-height: 1; + }} + .markdown-preview .callout-body > :first-child {{ margin-top: 0; }} + .markdown-preview .callout-body > :last-child {{ margin-bottom: 0; }} + .markdown-preview .callout-note {{ --callout-color: #0969da; --callout-bg: rgba(9, 105, 218, 0.08); }} + .markdown-preview .callout-tip {{ --callout-color: #1a7f37; --callout-bg: rgba(26, 127, 55, 0.08); }} + .markdown-preview .callout-important {{ --callout-color: #8250df; --callout-bg: rgba(130, 80, 223, 0.08); }} + .markdown-preview .callout-warning {{ --callout-color: #9a6700; --callout-bg: rgba(154, 103, 0, 0.08); }} + .markdown-preview .callout-caution {{ --callout-color: #d1242f; --callout-bg: rgba(209, 36, 47, 0.08); }} + .markdown-preview ul, .markdown-preview ol {{ padding-left: 2em; @@ -766,10 +796,57 @@ def generate_export_html( // Raw markdown content const markdown = `{escaped_content}`; - + + // Convert GitHub/Obsidian-style callouts (admonitions) before marked parses. + // Mirrors the in-app preview preprocessor so exported/shared notes match. + // Code blocks are protected with placeholders so callout syntax inside + // fenced/inline code is preserved verbatim. Only the 5 GitHub-canonical + // types are recognised; unknown types fall through to plain blockquotes. + let processed; + {{ + const CALLOUT_RE = /^>\\s*\\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\]\\s*(.*)$/i; + const CALLOUT_ICONS = {{ note: 'â„šī¸', tip: '💡', important: '❗', warning: 'âš ī¸', caution: '🛑' }}; + const CALLOUT_TITLES = {{ note: 'Note', tip: 'Tip', important: 'Important', warning: 'Warning', caution: 'Caution' }}; + const escapeAttr = (s) => s.replace(/&/g, '&').replace(//g, '>'); + const codeBlocks = []; + processed = markdown + .replace(/```[\\s\\S]*?```/g, function(m) {{ codeBlocks.push(m); return '\\x00CODEBLOCK' + (codeBlocks.length - 1) + '\\x00'; }}) + .replace(/`[^`]+`/g, function(m) {{ codeBlocks.push(m); return '\\x00CODEBLOCK' + (codeBlocks.length - 1) + '\\x00'; }}); + const srcLines = processed.split('\\n'); + const outLines = []; + let li = 0; + while (li < srcLines.length) {{ + const m = srcLines[li].match(CALLOUT_RE); + if (!m) {{ outLines.push(srcLines[li]); li++; continue; }} + const type = m[1].toLowerCase(); + const title = escapeAttr((m[2] || '').trim() || CALLOUT_TITLES[type]); + const icon = CALLOUT_ICONS[type]; + const bodyLines = []; + li++; + while (li < srcLines.length && srcLines[li].startsWith('>')) {{ + bodyLines.push(srcLines[li].replace(/^>\\s?/, '')); + li++; + }} + outLines.push( + '', + '
', + '
' + title + '
', + '
', + '', + bodyLines.join('\\n'), + '', + '
', + '
', + '' + ); + }} + processed = outLines.join('\\n') + .replace(/\\x00CODEBLOCK(\\d+)\\x00/g, function(_, idx) {{ return codeBlocks[parseInt(idx)]; }}); + }} + // Render markdown with XSS sanitization // DOMPurify strips scripts, iframes, and event handlers while allowing safe HTML/SVG - const rawHtml = marked.parse(markdown); + const rawHtml = marked.parse(processed); const safeHtml = DOMPurify.sanitize(rawHtml); document.getElementById('content').innerHTML = safeHtml; diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md index ea4c453..bde2392 100644 --- a/documentation/FEATURES.md +++ b/documentation/FEATURES.md @@ -270,6 +270,34 @@ graph TD 📄 **See the [MERMAID](MERMAID.md) note for diagram examples and syntax reference.** +## đŸ’Ŧ Callouts / Admonitions + +GitHub-style callouts highlight important information in your notes. They render as colored, bordered blocks in the preview pane and are fully compatible with notes imported from GitHub or Obsidian. + +### Syntax + +Start a blockquote with `> [!TYPE]` on its own line, then continue with the body: + +```markdown +> [!NOTE] +> Useful information that users should know, even when skimming. + +> [!TIP] Custom title +> Helpful advice for doing things better or more easily. +``` + +The first line can include an optional custom title after the type. Inner markdown (bold, links, lists, code) is fully parsed. + +### Supported Types + +- `[!NOTE]` - General information (blue) +- `[!TIP]` - Helpful advice (green) +- `[!IMPORTANT]` - Crucial information (purple) +- `[!WARNING]` - Urgent caution required (amber) +- `[!CAUTION]` - Negative consequences if ignored (red) + +Unknown types fall through to a normal blockquote, so existing notes are never broken. + ## 📄 Note Templates Create notes from reusable templates with dynamic placeholder replacement. diff --git a/frontend/app.js b/frontend/app.js index a3a6ee1..3d29186 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -5811,6 +5811,54 @@ function noteApp() { } ); + // Step 2c: Convert GitHub/Obsidian-style callouts (admonitions). + // Runs while code blocks are still placeholders so callout syntax + // inside fenced/inline code is preserved verbatim. The body is + // emitted as a raw HTML block surrounded by blank lines so marked + // still parses inner markdown (bold, links, lists, etc.) per CommonMark. + // Only the 5 GitHub-canonical types are recognised; unknown types + // (e.g. `> [!CUSTOM]`) fall through to plain blockquotes. + { + const CALLOUT_RE = /^>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*(.*)$/i; + const CALLOUT_ICONS = { note: 'â„šī¸', tip: '💡', important: '❗', warning: 'âš ī¸', caution: '🛑' }; + const CALLOUT_TITLES = { note: 'Note', tip: 'Tip', important: 'Important', warning: 'Warning', caution: 'Caution' }; + const escapeAttr = (s) => s.replace(/&/g, '&').replace(//g, '>'); + + const srcLines = contentToRender.split('\n'); + const outLines = []; + let li = 0; + while (li < srcLines.length) { + const m = srcLines[li].match(CALLOUT_RE); + if (!m) { + outLines.push(srcLines[li]); + li++; + continue; + } + const type = m[1].toLowerCase(); + const title = escapeAttr((m[2] || '').trim() || CALLOUT_TITLES[type]); + const icon = CALLOUT_ICONS[type]; + const bodyLines = []; + li++; + while (li < srcLines.length && srcLines[li].startsWith('>')) { + bodyLines.push(srcLines[li].replace(/^>\s?/, '')); + li++; + } + outLines.push( + '', + `
`, + `
${title}
`, + `
`, + '', + bodyLines.join('\n'), + '', + `
`, + `
`, + '' + ); + } + contentToRender = outLines.join('\n'); + } + // Step 3: Restore code blocks contentToRender = contentToRender.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => { return codeBlocks[parseInt(index)]; diff --git a/frontend/index.html b/frontend/index.html index 699df78..fcff80e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -544,7 +544,37 @@ font-size: 0.75rem; color: var(--text-secondary); } - + + /* Callouts / admonitions (GitHub-style: > [!NOTE], > [!TIP], etc.) + Colors are hard-coded GitHub palette with rgba backgrounds so the + tint blends with any theme bg without needing per-theme overrides. */ + .markdown-preview .callout { + margin: 1rem 0; + padding: 0.75rem 1rem; + border-left: 4px solid var(--callout-color, var(--accent-primary)); + border-radius: 0.375rem; + background: var(--callout-bg, var(--bg-secondary)); + } + .markdown-preview .callout-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + color: var(--callout-color, var(--accent-primary)); + margin-bottom: 0.25rem; + } + .markdown-preview .callout-icon { + font-size: 1.1em; + line-height: 1; + } + .markdown-preview .callout-body > :first-child { margin-top: 0; } + .markdown-preview .callout-body > :last-child { margin-bottom: 0; } + .markdown-preview .callout-note { --callout-color: #0969da; --callout-bg: rgba(9, 105, 218, 0.08); } + .markdown-preview .callout-tip { --callout-color: #1a7f37; --callout-bg: rgba(26, 127, 55, 0.08); } + .markdown-preview .callout-important { --callout-color: #8250df; --callout-bg: rgba(130, 80, 223, 0.08); } + .markdown-preview .callout-warning { --callout-color: #9a6700; --callout-bg: rgba(154, 103, 0, 0.08); } + .markdown-preview .callout-caution { --callout-color: #d1242f; --callout-bg: rgba(209, 36, 47, 0.08); } + /* Task lists */ .markdown-preview input[type="checkbox"] { margin-right: 0.5rem; From da748caf42538e8b6d7add44e5de374e9d7719e5 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Mon, 29 Jun 2026 13:07:00 +0200 Subject: [PATCH 2/2] double-click issue --- backend/export.py | 10 ++------- frontend/app.js | 50 ++++++++++++++++++++++++++++++--------------- frontend/index.html | 5 ++--- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/backend/export.py b/backend/export.py index a5d45de..cea3fd2 100644 --- a/backend/export.py +++ b/backend/export.py @@ -528,9 +528,7 @@ def generate_export_html( color: var(--text-secondary, #6a737d); }} - /* Callouts / admonitions (GitHub-style: > [!NOTE], > [!TIP], etc.) - Mirrors the in-app preview styles so exported/shared notes match. - Hard-coded GitHub palette with rgba backgrounds blends with any theme. */ + /* Callouts — mirror the in-app preview. */ .markdown-preview .callout {{ margin: 1rem 0; padding: 0.75rem 1rem; @@ -797,11 +795,7 @@ def generate_export_html( // Raw markdown content const markdown = `{escaped_content}`; - // Convert GitHub/Obsidian-style callouts (admonitions) before marked parses. - // Mirrors the in-app preview preprocessor so exported/shared notes match. - // Code blocks are protected with placeholders so callout syntax inside - // fenced/inline code is preserved verbatim. Only the 5 GitHub-canonical - // types are recognised; unknown types fall through to plain blockquotes. + // GitHub-style callouts — mirrors the in-app preview preprocessor. let processed; {{ const CALLOUT_RE = /^>\\s*\\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\]\\s*(.*)$/i; diff --git a/frontend/app.js b/frontend/app.js index 3d29186..5ff7d7d 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -5190,6 +5190,21 @@ function noteApp() { }, // Markdown formatting helpers + // Trim trailing whitespace from double-click word selections. + // No-op on browsers that already exclude it (Firefox, Safari). + trimSelectionTrailingSpace(event) { + const editor = event && event.target; + if (!editor) return; + const start = editor.selectionStart; + let end = editor.selectionEnd; + if (start >= end) return; + const text = editor.value; + while (end > start && /\s/.test(text.charAt(end - 1))) end--; + if (end !== editor.selectionEnd) { + editor.setSelectionRange(start, end); + } + }, + wrapSelection(before, after, placeholder) { const editor = document.getElementById('note-editor'); if (!editor) return; @@ -5197,23 +5212,29 @@ function noteApp() { const start = editor.selectionStart; const end = editor.selectionEnd; const selectedText = this.noteContent.substring(start, end); - const textToWrap = selectedText || placeholder; + + // Push edge whitespace OUTSIDE inline wrappers — "**house **" is + // invalid per CommonMark. Block wrappers (with \n) keep it intact. + const isInline = !before.includes('\n') && !after.includes('\n'); + let leading = '', trailing = '', core = selectedText; + if (isInline && selectedText) { + const m = selectedText.match(/^(\s*)([\s\S]*?)(\s*)$/); + leading = m[1]; + core = m[2]; + trailing = m[3]; + } + const textToWrap = core || placeholder; // Build the new text - const newText = before + textToWrap + after; + const newText = leading + before + textToWrap + after + trailing; // Update content this.noteContent = this.noteContent.substring(0, start) + newText + this.noteContent.substring(end); - // Set cursor position (select the wrapped text or placeholder) this.$nextTick(() => { - if (selectedText) { - // If text was selected, keep it selected (inside the wrapper) - editor.setSelectionRange(start + before.length, start + before.length + selectedText.length); - } else { - // If no text selected, select the placeholder - editor.setSelectionRange(start + before.length, start + before.length + placeholder.length); - } + const coreStart = start + leading.length + before.length; + const coreEnd = coreStart + textToWrap.length; + editor.setSelectionRange(coreStart, coreEnd); editor.focus(); }); @@ -5811,13 +5832,8 @@ function noteApp() { } ); - // Step 2c: Convert GitHub/Obsidian-style callouts (admonitions). - // Runs while code blocks are still placeholders so callout syntax - // inside fenced/inline code is preserved verbatim. The body is - // emitted as a raw HTML block surrounded by blank lines so marked - // still parses inner markdown (bold, links, lists, etc.) per CommonMark. - // Only the 5 GitHub-canonical types are recognised; unknown types - // (e.g. `> [!CUSTOM]`) fall through to plain blockquotes. + // Step 2c: GitHub-style callouts. Runs inside the code-protection + // window. Unknown types fall through to plain blockquotes. { const CALLOUT_RE = /^>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*(.*)$/i; const CALLOUT_ICONS = { note: 'â„šī¸', tip: '💡', important: '❗', warning: 'âš ī¸', caution: '🛑' }; diff --git a/frontend/index.html b/frontend/index.html index fcff80e..a167237 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -545,9 +545,7 @@ color: var(--text-secondary); } - /* Callouts / admonitions (GitHub-style: > [!NOTE], > [!TIP], etc.) - Colors are hard-coded GitHub palette with rgba backgrounds so the - tint blends with any theme bg without needing per-theme overrides. */ + /* Callouts — GitHub-style admonitions (> [!NOTE], etc.) */ .markdown-preview .callout { margin: 1rem 0; padding: 0.75rem 1rem; @@ -3061,6 +3059,7 @@ @scroll="syncOverlayScroll()" @keydown.enter="handleEditorEnterKey($event)" @keydown.tab="handleTabKey($event)" + @dblclick="trimSelectionTrailingSpace($event)" @drop="onEditorDrop($event)" @dragover.prevent="onEditorDragOver($event)" @dragenter="onEditorDragEnter($event)"