From e5757dd11a02d708c11fc8300bff625b4620d5b4 Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Mon, 29 Jun 2026 21:20:24 +0530 Subject: [PATCH] fix(markmap): normalize fences before splitting blocks in preview worker --- desktop-app/resources/js/preview-worker.js | 81 ++++++++++++++++++++-- preview-worker.js | 81 ++++++++++++++++++++-- 2 files changed, 152 insertions(+), 10 deletions(-) diff --git a/desktop-app/resources/js/preview-worker.js b/desktop-app/resources/js/preview-worker.js index 8455b0a7..989b3566 100644 --- a/desktop-app/resources/js/preview-worker.js +++ b/desktop-app/resources/js/preview-worker.js @@ -366,14 +366,17 @@ function configureMarked() { const krokiLanguages = { 'vega-lite': ['vegalite', 'Vega-Lite'], vegalite: ['vegalite', 'Vega-Lite'], - wavedrom: ['wavedrom', 'WaveDrom'], - markmap: ['markmap', 'Markmap'] + wavedrom: ['wavedrom', 'WaveDrom'] }; if (krokiLanguages[language]) { const [engine, label] = krokiLanguages[language]; const uniqueId = `${engine}-diagram-worker-${krokiIdCounter++}`; return renderDiagramShell(engine, 'kroki-container', 'kroki-diagram', uniqueId, code, label); } + if (language === 'markmap') { + const uniqueId = `markmap-diagram-worker-${krokiIdCounter++}`; + return renderDiagramShell('markmap', 'markmap-container', 'markmap-diagram', uniqueId, code, 'Markmap'); + } if (language === "math") { return `
$$\n${code}\n$$
\n`; @@ -400,6 +403,72 @@ function configureMarked() { return `${text}`; }; + function normalizeMarkmapFences(markdown) { + const lines = String(markdown || '').split(/\r?\n/); + const output = []; + let index = 0; + + while (index < lines.length) { + const opening = lines[index].match(/^([ \t]{0,3})(`{3,}|~{3,})([ \t]*)(.*)$/); + const info = opening ? opening[4].trim() : ''; + if (!opening || !/^markmap(?:\s|$)/i.test(info)) { + output.push(lines[index]); + index += 1; + continue; + } + + const indent = opening[1]; + const fence = opening[2]; + const marker = fence[0]; + const content = []; + let nestedFence = null; + let maxInnerFenceLength = fence.length; + let closeIndex = -1; + + for (let scan = index + 1; scan < lines.length; scan += 1) { + const line = lines[scan]; + const fenceMatch = line.match(/^[ \t]{0,3}(`{3,}|~{3,})([ \t]*.*)$/); + if (fenceMatch) { + const currentFence = fenceMatch[1]; + const currentMarker = currentFence[0]; + const tail = fenceMatch[2].trim(); + if (currentMarker === marker) { + maxInnerFenceLength = Math.max(maxInnerFenceLength, currentFence.length); + } + + if (nestedFence) { + if (currentMarker === nestedFence.marker && currentFence.length >= nestedFence.length && tail === '') { + nestedFence = null; + } + } else if (currentMarker === marker && currentFence.length >= fence.length && tail === '') { + closeIndex = scan; + break; + } else if (tail !== '') { + nestedFence = { + marker: currentMarker, + length: currentFence.length + }; + } + } + content.push(line); + } + + if (closeIndex === -1) { + output.push(lines[index]); + index += 1; + continue; + } + + const normalizedFence = marker.repeat(maxInnerFenceLength + 1); + output.push(`${indent}${normalizedFence}${opening[3]}${opening[4]}`); + output.push(...content); + output.push(`${indent}${normalizedFence}`); + index = closeIndex + 1; + } + + return output.join('\n'); + } + marked.use({ extensions: [ blockMathExtension, @@ -412,7 +481,8 @@ function configureMarked() { preprocess(markdown) { if (suppressFootnotePreprocess) return markdown; resetExtendedMarkdownState(); - const protectedMarkdown = markdown.replace(/\\\$/g, "$"); + const normalizedMarkdown = normalizeMarkmapFences(markdown); + const protectedMarkdown = normalizedMarkdown.replace(/\\\$/g, "$"); return applyFootnotes(extractFootnoteDefinitions(protectedMarkdown)); }, }, @@ -510,11 +580,12 @@ function splitMarkdownBlocks(markdown) { } function renderSegmentedMarkdown(markdown, options) { - if (!isSegmentedPreviewSafe(markdown)) { + const normalizedMarkdown = normalizeMarkmapFences(markdown); + if (!isSegmentedPreviewSafe(normalizedMarkdown)) { return { mode: "full-required", reason: "unsafe-markdown" }; } - const blocks = splitMarkdownBlocks(markdown); + const blocks = splitMarkdownBlocks(normalizedMarkdown); if (blocks.length < (options.minimumBlocks || 1)) { return { mode: "full-required", reason: "too-few-blocks" }; } diff --git a/preview-worker.js b/preview-worker.js index 8455b0a7..989b3566 100644 --- a/preview-worker.js +++ b/preview-worker.js @@ -366,14 +366,17 @@ function configureMarked() { const krokiLanguages = { 'vega-lite': ['vegalite', 'Vega-Lite'], vegalite: ['vegalite', 'Vega-Lite'], - wavedrom: ['wavedrom', 'WaveDrom'], - markmap: ['markmap', 'Markmap'] + wavedrom: ['wavedrom', 'WaveDrom'] }; if (krokiLanguages[language]) { const [engine, label] = krokiLanguages[language]; const uniqueId = `${engine}-diagram-worker-${krokiIdCounter++}`; return renderDiagramShell(engine, 'kroki-container', 'kroki-diagram', uniqueId, code, label); } + if (language === 'markmap') { + const uniqueId = `markmap-diagram-worker-${krokiIdCounter++}`; + return renderDiagramShell('markmap', 'markmap-container', 'markmap-diagram', uniqueId, code, 'Markmap'); + } if (language === "math") { return `
$$\n${code}\n$$
\n`; @@ -400,6 +403,72 @@ function configureMarked() { return `${text}`; }; + function normalizeMarkmapFences(markdown) { + const lines = String(markdown || '').split(/\r?\n/); + const output = []; + let index = 0; + + while (index < lines.length) { + const opening = lines[index].match(/^([ \t]{0,3})(`{3,}|~{3,})([ \t]*)(.*)$/); + const info = opening ? opening[4].trim() : ''; + if (!opening || !/^markmap(?:\s|$)/i.test(info)) { + output.push(lines[index]); + index += 1; + continue; + } + + const indent = opening[1]; + const fence = opening[2]; + const marker = fence[0]; + const content = []; + let nestedFence = null; + let maxInnerFenceLength = fence.length; + let closeIndex = -1; + + for (let scan = index + 1; scan < lines.length; scan += 1) { + const line = lines[scan]; + const fenceMatch = line.match(/^[ \t]{0,3}(`{3,}|~{3,})([ \t]*.*)$/); + if (fenceMatch) { + const currentFence = fenceMatch[1]; + const currentMarker = currentFence[0]; + const tail = fenceMatch[2].trim(); + if (currentMarker === marker) { + maxInnerFenceLength = Math.max(maxInnerFenceLength, currentFence.length); + } + + if (nestedFence) { + if (currentMarker === nestedFence.marker && currentFence.length >= nestedFence.length && tail === '') { + nestedFence = null; + } + } else if (currentMarker === marker && currentFence.length >= fence.length && tail === '') { + closeIndex = scan; + break; + } else if (tail !== '') { + nestedFence = { + marker: currentMarker, + length: currentFence.length + }; + } + } + content.push(line); + } + + if (closeIndex === -1) { + output.push(lines[index]); + index += 1; + continue; + } + + const normalizedFence = marker.repeat(maxInnerFenceLength + 1); + output.push(`${indent}${normalizedFence}${opening[3]}${opening[4]}`); + output.push(...content); + output.push(`${indent}${normalizedFence}`); + index = closeIndex + 1; + } + + return output.join('\n'); + } + marked.use({ extensions: [ blockMathExtension, @@ -412,7 +481,8 @@ function configureMarked() { preprocess(markdown) { if (suppressFootnotePreprocess) return markdown; resetExtendedMarkdownState(); - const protectedMarkdown = markdown.replace(/\\\$/g, "$"); + const normalizedMarkdown = normalizeMarkmapFences(markdown); + const protectedMarkdown = normalizedMarkdown.replace(/\\\$/g, "$"); return applyFootnotes(extractFootnoteDefinitions(protectedMarkdown)); }, }, @@ -510,11 +580,12 @@ function splitMarkdownBlocks(markdown) { } function renderSegmentedMarkdown(markdown, options) { - if (!isSegmentedPreviewSafe(markdown)) { + const normalizedMarkdown = normalizeMarkmapFences(markdown); + if (!isSegmentedPreviewSafe(normalizedMarkdown)) { return { mode: "full-required", reason: "unsafe-markdown" }; } - const blocks = splitMarkdownBlocks(markdown); + const blocks = splitMarkdownBlocks(normalizedMarkdown); if (blocks.length < (options.minimumBlocks || 1)) { return { mode: "full-required", reason: "too-few-blocks" }; }