Merge pull request #251 from pablon/fix/callouts-nested-codeblock
fix(callouts): render nested code blocks inside GFM/GLFM alerts
This commit is contained in:
commit
a4342cf1cb
|
|
@ -809,23 +809,44 @@ def generate_export_html(
|
||||||
// Raw markdown content
|
// Raw markdown content
|
||||||
const markdown = `{escaped_content}`;
|
const markdown = `{escaped_content}`;
|
||||||
|
|
||||||
// GitHub-style callouts — mirrors the in-app preview preprocessor.
|
// GFM/GLFM callouts. Runs BEFORE code-block extraction so fences
|
||||||
|
// nested in a callout blockquote lose their `> ` prefix on the closer
|
||||||
|
// — otherwise the restored block sits inside <div class="callout-body">
|
||||||
|
// without a valid CommonMark fence closer and runs unclosed.
|
||||||
|
// Fence-aware so literal `> [!TIPO]` inside a top-level code block
|
||||||
|
// is not misread. Mirrors the in-app preview preprocessor.
|
||||||
let processed;
|
let processed;
|
||||||
{{
|
{{
|
||||||
const CALLOUT_RE = /^>\\s*\\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\]\\s*(.*)$/i;
|
const CALLOUT_RE = /^>\\s*\\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\]\\s*(.*)$/i;
|
||||||
const CALLOUT_ICONS = {{ note: 'ℹ️', tip: '💡', important: '❗', warning: '⚠️', caution: '🛑' }};
|
const CALLOUT_ICONS = {{ note: 'ℹ️', tip: '💡', important: '❗', warning: '⚠️', caution: '🛑' }};
|
||||||
const CALLOUT_TITLES = {{ note: 'Note', tip: 'Tip', important: 'Important', warning: 'Warning', caution: 'Caution' }};
|
const CALLOUT_TITLES = {{ note: 'Note', tip: 'Tip', important: 'Important', warning: 'Warning', caution: 'Caution' }};
|
||||||
|
const FENCE_OPEN_RE = /^\\s{{0,3}}(`{{3,}}|~{{3,}})/;
|
||||||
const escapeAttr = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
const escapeAttr = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
const codeBlocks = [];
|
|
||||||
processed = markdown
|
const srcLines = markdown.split('\\n');
|
||||||
.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 = [];
|
const outLines = [];
|
||||||
let li = 0;
|
let li = 0;
|
||||||
|
let fenceChar = null;
|
||||||
|
let fenceLen = 0;
|
||||||
while (li < srcLines.length) {{
|
while (li < srcLines.length) {{
|
||||||
const m = srcLines[li].match(CALLOUT_RE);
|
const line = srcLines[li];
|
||||||
if (!m) {{ outLines.push(srcLines[li]); li++; continue; }}
|
if (fenceChar) {{
|
||||||
|
outLines.push(line);
|
||||||
|
const closeRe = new RegExp('^\\\\s{{0,3}}' + (fenceChar === '`' ? '`' : '~') + '{{' + fenceLen + ',}}\\\\s*$');
|
||||||
|
if (closeRe.test(line)) {{ fenceChar = null; fenceLen = 0; }}
|
||||||
|
li++;
|
||||||
|
continue;
|
||||||
|
}}
|
||||||
|
const fenceOpen = line.match(FENCE_OPEN_RE);
|
||||||
|
if (fenceOpen) {{
|
||||||
|
fenceChar = fenceOpen[1][0];
|
||||||
|
fenceLen = fenceOpen[1].length;
|
||||||
|
outLines.push(line);
|
||||||
|
li++;
|
||||||
|
continue;
|
||||||
|
}}
|
||||||
|
const m = line.match(CALLOUT_RE);
|
||||||
|
if (!m) {{ outLines.push(line); li++; continue; }}
|
||||||
const type = m[1].toLowerCase();
|
const type = m[1].toLowerCase();
|
||||||
const title = escapeAttr((m[2] || '').trim() || CALLOUT_TITLES[type]);
|
const title = escapeAttr((m[2] || '').trim() || CALLOUT_TITLES[type]);
|
||||||
const icon = CALLOUT_ICONS[type];
|
const icon = CALLOUT_ICONS[type];
|
||||||
|
|
@ -848,8 +869,8 @@ def generate_export_html(
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
processed = outLines.join('\\n')
|
processed = outLines.join('\\n');
|
||||||
.replace(/\\x00CODEBLOCK(\\d+)\\x00/g, function(_, idx) {{ return codeBlocks[parseInt(idx)]; }});
|
|
||||||
// Escape same-line block-starters after a task marker so they
|
// Escape same-line block-starters after a task marker so they
|
||||||
// render as literal text. Matches Obsidian. Issue #247.
|
// render as literal text. Matches Obsidian. Issue #247.
|
||||||
processed = processed.replace(
|
processed = processed.replace(
|
||||||
|
|
|
||||||
117
frontend/app.js
117
frontend/app.js
|
|
@ -5835,6 +5835,80 @@ function noteApp() {
|
||||||
// BUT we need to protect code blocks first to avoid converting [[text]] inside code
|
// BUT we need to protect code blocks first to avoid converting [[text]] inside code
|
||||||
const self = this; // Reference for closure
|
const self = this; // Reference for closure
|
||||||
|
|
||||||
|
// Step 0: GFM/GLFM callouts. Runs BEFORE code-block extraction so
|
||||||
|
// fences nested in a callout blockquote lose their `> ` prefix on
|
||||||
|
// the closer — otherwise CommonMark won't see a valid fence closer
|
||||||
|
// once restored inside <div class="callout-body"> and the block
|
||||||
|
// runs unclosed. Fence-aware so `> [!TIPO]` literal inside a
|
||||||
|
// top-level code block is not misread as a callout.
|
||||||
|
{
|
||||||
|
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' };
|
||||||
|
// Fence opener at 0-3 spaces indent (CommonMark). Captured
|
||||||
|
// group holds the fence chars so the closer must match char + length.
|
||||||
|
const FENCE_OPEN_RE = /^\s{0,3}(`{3,}|~{3,})/;
|
||||||
|
const escapeAttr = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
|
||||||
|
const srcLines = contentToRender.split('\n');
|
||||||
|
const outLines = [];
|
||||||
|
let li = 0;
|
||||||
|
let fenceChar = null; // '`' or '~' when inside a top-level fence
|
||||||
|
let fenceLen = 0;
|
||||||
|
while (li < srcLines.length) {
|
||||||
|
const line = srcLines[li];
|
||||||
|
|
||||||
|
// Inside a top-level fence: pass through opaquely.
|
||||||
|
if (fenceChar) {
|
||||||
|
outLines.push(line);
|
||||||
|
const closeRe = new RegExp('^\\s{0,3}' + (fenceChar === '`' ? '`' : '~') + '{' + fenceLen + ',}\\s*$');
|
||||||
|
if (closeRe.test(line)) { fenceChar = null; fenceLen = 0; }
|
||||||
|
li++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fenceOpen = line.match(FENCE_OPEN_RE);
|
||||||
|
if (fenceOpen) {
|
||||||
|
fenceChar = fenceOpen[1][0];
|
||||||
|
fenceLen = fenceOpen[1].length;
|
||||||
|
outLines.push(line);
|
||||||
|
li++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = line.match(CALLOUT_RE);
|
||||||
|
if (!m) {
|
||||||
|
outLines.push(line);
|
||||||
|
li++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const type = m[1].toLowerCase();
|
||||||
|
const title = escapeAttr((m[2] || '').trim() || CALLOUT_TITLES[type]);
|
||||||
|
const icon = CALLOUT_ICONS[type];
|
||||||
|
const bodyLines = [];
|
||||||
|
li++;
|
||||||
|
// Fence lines inside the callout also start with `>`, so
|
||||||
|
// they get absorbed and stripped, leaving a clean fence.
|
||||||
|
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 1: Temporarily replace code blocks and inline code with placeholders
|
// Step 1: Temporarily replace code blocks and inline code with placeholders
|
||||||
const codeBlocks = [];
|
const codeBlocks = [];
|
||||||
// Protect fenced code blocks (```...```)
|
// Protect fenced code blocks (```...```)
|
||||||
|
|
@ -5920,48 +5994,7 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 2c: GitHub-style callouts. Runs inside the code-protection
|
// (Callouts handled in Step 0, before code-block extraction.)
|
||||||
// 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: '🛑' };
|
|
||||||
const CALLOUT_TITLES = { note: 'Note', tip: 'Tip', important: 'Important', warning: 'Warning', caution: 'Caution' };
|
|
||||||
const escapeAttr = (s) => s.replace(/&/g, '&').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(
|
|
||||||
'',
|
|
||||||
`<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');
|
|
||||||
}
|
|
||||||
|
|
||||||
contentToRender = contentToRender.replace(
|
contentToRender = contentToRender.replace(
|
||||||
/^(\s*[-*+]\s+\[[xX ]\]\s+)(\d+)\.(\s)/gm,
|
/^(\s*[-*+]\s+\[[xX ]\]\s+)(\d+)\.(\s)/gm,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue