added callout support

This commit is contained in:
Gamosoft 2026-06-29 12:19:06 +02:00
parent 78c31ae99a
commit 581a95b1c8
4 changed files with 187 additions and 4 deletions

View File

@ -528,6 +528,36 @@ def generate_export_html(
color: var(--text-secondary, #6a737d); 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 ul,
.markdown-preview ol {{ .markdown-preview ol {{
padding-left: 2em; padding-left: 2em;
@ -767,9 +797,56 @@ def generate_export_html(
// Raw markdown content // Raw markdown content
const markdown = `{escaped_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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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(
'',
'<div class="callout callout-' + type + '">',
'<div class="callout-title"><span class="callout-icon" aria-hidden="true">' + icon + '</span><span class="callout-title-text">' + title + '</span></div>',
'<div class="callout-body">',
'',
bodyLines.join('\\n'),
'',
'</div>',
'</div>',
''
);
}}
processed = outLines.join('\\n')
.replace(/\\x00CODEBLOCK(\\d+)\\x00/g, function(_, idx) {{ return codeBlocks[parseInt(idx)]; }});
}}
// Render markdown with XSS sanitization // Render markdown with XSS sanitization
// DOMPurify strips scripts, iframes, and event handlers while allowing safe HTML/SVG // 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); const safeHtml = DOMPurify.sanitize(rawHtml);
document.getElementById('content').innerHTML = safeHtml; document.getElementById('content').innerHTML = safeHtml;

View File

@ -270,6 +270,34 @@ graph TD
📄 **See the [MERMAID](MERMAID.md) note for diagram examples and syntax reference.** 📄 **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 ## 📄 Note Templates
Create notes from reusable templates with dynamic placeholder replacement. Create notes from reusable templates with dynamic placeholder replacement.

View File

@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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(
'',
`<div class="callout callout-${type}">`,
`<div class="callout-title"><span class="callout-icon" aria-hidden="true">${icon}</span><span class="callout-title-text">${title}</span></div>`,
`<div class="callout-body">`,
'',
bodyLines.join('\n'),
'',
`</div>`,
`</div>`,
''
);
}
contentToRender = outLines.join('\n');
}
// Step 3: Restore code blocks // Step 3: Restore code blocks
contentToRender = contentToRender.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => { contentToRender = contentToRender.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {
return codeBlocks[parseInt(index)]; return codeBlocks[parseInt(index)];

View File

@ -545,6 +545,36 @@
color: var(--text-secondary); 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 */ /* Task lists */
.markdown-preview input[type="checkbox"] { .markdown-preview input[type="checkbox"] {
margin-right: 0.5rem; margin-right: 0.5rem;