diff --git a/src/markdown/marpRenderer.ts b/src/markdown/marpRenderer.ts index c00c085..05f549b 100644 --- a/src/markdown/marpRenderer.ts +++ b/src/markdown/marpRenderer.ts @@ -45,21 +45,22 @@ export async function loadMarp() { */ export function cleanMarpHtml(html: string): { cleaned: string; scripts: string[] } { const scripts: string[] = []; - const cleaned = html.replace( - /)<[^<]*)*<\/script>/gi, - (match) => { - const content = match.replace(/^/i, "").replace(/<\/script>$/i, ""); - if (content.trim()) { - scripts.push(content); - } - return ""; - }, - ); - - let fullyCleaned = cleaned.replace( - /)<[^<]*)*<\/style>/gi, - "", - ); + const scriptRegex = /<\s*script\b[^>]*>([\s\S]*?)<\/\s*script\b[^>]*>/gi; + let m: RegExpExecArray | null; + while ((m = scriptRegex.exec(html)) !== null) { + const content = m[1].trim(); + if (content) { + scripts.push(content); + } + } + + let fullyCleaned = html; + let prevCleaned: string; + const tagRemoveRegex = /<\s*(?:script|style)\b[^>]*>[\s\S]*?<\/\s*(?:script|style)\b[^>]*>/gi; + do { + prevCleaned = fullyCleaned; + fullyCleaned = fullyCleaned.replace(tagRemoveRegex, ""); + } while (fullyCleaned !== prevCleaned); // Strip data-line from SVGs and Sections to fix Marp slide offsets and Quick Edit targeting fullyCleaned = fullyCleaned.replace(/<(svg|section)\b[^>]*\sdata-line="[^"]*"[^>]*>/gi, (match) => { diff --git a/src/markdown/structuralDiff.ts b/src/markdown/structuralDiff.ts index a71183a..9cf4f02 100644 --- a/src/markdown/structuralDiff.ts +++ b/src/markdown/structuralDiff.ts @@ -1373,7 +1373,7 @@ export function refineBlockDiffs( ); const footnoteBundleRegex = - /]*>\s*((?:]*>[\s\S]*?<\/li>\s*)+)<\/del>\s*]*>\s*((?:]*>[\s\S]*?<\/li>\s*)+)<\/ins>/gi; + /]*)>\s*([\s\S]*?)\s*<\/del>\s*]*)>\s*([\s\S]*?)\s*<\/ins>/gi; const footnoteItemRegex = /]*>[\s\S]*?<\/li>/gi; const getFootnoteId = (itemHtml: string) => { const id = itemHtml.match(/\bid=["']([^"']+)["']/i)?.[1] ?? null; @@ -1382,7 +1382,27 @@ export function refineBlockDiffs( resultHtml = resultHtml.replace( footnoteBundleRegex, - (match, oldBundle, newBundle) => { + (match, delAttrs, oldBundle, insAttrs, newBundle) => { + // Must not match list container change diffs or non-li bundles + if ( + delAttrs.includes("diff-list-container-change") || + insAttrs.includes("diff-list-container-change") + ) { + return match; + } + const trimmedOld = oldBundle.trim(); + const trimmedNew = newBundle.trim(); + if ( + !trimmedOld.startsWith("") || + !trimmedNew.startsWith("") || + /<(?:ul|ol|dl|table|pre|h[1-6])\b/i.test(oldBundle) || + /<(?:ul|ol|dl|table|pre|h[1-6])\b/i.test(newBundle) + ) { + return match; + } + const oldFootnotes = oldBundle.match(footnoteItemRegex) || []; const newFootnotes = newBundle.match(footnoteItemRegex) || []; @@ -1967,19 +1987,27 @@ export function stripDataLineAttributes(html: string): string { export function wrapHeadingPrefixes(html: string): string { return html.replace( - /(]*>)((?:\s*(?:<(?:del|ins)[^>]*>)?\s*[\d\.\[\]]+\s*(?:<\/(?:del|ins)>)?\s*)+(?:\]\s*)?(?=\S))/gi, - (match, tag, prefix) => { - if (!/<(ins|del)\b/.test(prefix)) { + /(]*>)([\s\S]*?)(<\/h[1-6]>)/gi, + (match, openTag, innerContent, closeTag) => { + const prefixMatch = innerContent.match( + /^((?:[0-9.\[\]\s]|<(?:del|ins)\b[^>]*>[0-9.\[\]\s]*<\/(?:del|ins)\b[^>]*>)+)(?=\S)/i, + ); + if (!prefixMatch) { + return match; + } + const prefix = prefixMatch[1]; + if (!/<(?:ins|del)\b/i.test(prefix)) { return match; } - const openIns = (prefix.match(//g) || []).length; - const openDel = (prefix.match(//g) || []).length; + const openIns = (prefix.match(//gi) || []).length; + const openDel = (prefix.match(//gi) || []).length; if (openIns !== closeIns || openDel !== closeDel) { return match; } - return tag + '' + prefix + ""; + const rest = innerContent.slice(prefix.length); + return `${openTag}${prefix}${rest}${closeTag}`; }, ); } diff --git a/src/test/runTest.ts b/src/test/runTest.ts index c6c3369..f0a1652 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -77,6 +77,9 @@ async function main() { await fs.mkdir(userDataDir, { recursive: true }); await fs.mkdir(extensionsDir, { recursive: true }); + // Prevent Electron from running as Node CLI when running inside VS Code / sub-shells + delete process.env.ELECTRON_RUN_AS_NODE; + // Download VS Code, unzip it and run the integration test await runTests({ extensionDevelopmentPath, diff --git a/src/test/unit/markdownDiff.test.ts b/src/test/unit/markdownDiff.test.ts index cb4a81b..5f3cfd3 100644 --- a/src/test/unit/markdownDiff.test.ts +++ b/src/test/unit/markdownDiff.test.ts @@ -519,7 +519,7 @@ describe("MarkdownDiffProvider", () => { const scriptBlocks = Array.from( webviewContent.matchAll( - /([\s\S]*?)<\/script>/g, + /<\s*script\b[^>]*>([\s\S]*?)<\/\s*script\b[^>]*>/gi, ), (match) => match[1], );