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(
+ '',
+ '
',
+ '
' + icon + '' + 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(
+ '',
+ ``,
+ `
${icon}${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;