From b2fd6574ded5f95d3cd666cb95e52a7ac74e1a23 Mon Sep 17 00:00:00 2001
From: phine-apps <91043289+phine-apps@users.noreply.github.com>
Date: Tue, 11 Aug 2026 14:52:58 +0900
Subject: [PATCH] chore(release): prepare v1.5.0 release with Visual Conflict
Resolver, HTML export, and bug fixes
---
CHANGELOG.md | 3 +-
README.md | 7 +-
fixtures/expected/comprehensive.html | 45 ++--
src/conflictEditorProvider.ts | 186 +--------------
src/exportHtml.ts | 222 ++++++++++++++++++
src/extension.ts | 26 +-
src/markdown/conflictParser.ts | 202 ++++++++++++++++
src/markdown/mermaidDiff.ts | 2 +-
src/markdown/structuralDiff.ts | 74 +++---
src/markdown/webviewTemplate.ts | 13 +-
src/test/unit/checkbox_regression.test.ts | 13 +
src/test/unit/conflictParser.test.ts | 33 +++
.../unit/consolidate_wrapped_items.test.ts | 6 +
src/test/unit/exportHtml.test.ts | 156 ++++++++++++
src/test/unit/markdownDiff.test.ts | 62 +++++
src/test/unit/mermaidDiff.test.ts | 45 ++++
...nsive-v1-v2-split-light-chromium-linux.png | Bin 580678 -> 568772 bytes
17 files changed, 830 insertions(+), 265 deletions(-)
create mode 100644 src/exportHtml.ts
create mode 100644 src/test/unit/exportHtml.test.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa3b885..6ed439d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,12 +2,11 @@
All notable changes to **Rich Markdown Diff** will be documented in this file.
-## [1.5.0] - 2026-08-DD
+## [1.5.0] - 2026-08-11
### Added in 1.5.0
- **Visual Conflict Resolver**: Interactive 3-way merge editor for resolving Git merge conflicts (`<<<<<<<`, `=======`, `>>>>>>>`) directly in rendered Markdown.
-- **Semantic Mermaid Diff**: Element-level diff highlighting for Mermaid flowcharts with dynamic color-coding and ghost outlines.
- **HTML Export**: Export rendered side-by-side or inline diffs to standalone HTML files via the `Export Rendered Diff to HTML` command.
- **Custom Diff Colors & View Preferences**: Added settings for custom insertion/deletion highlight colors and default view/folding modes.
- **Committed Revision Diffs**: Render visual diffs for committed revisions from Git history and the editor context menu.
diff --git a/README.md b/README.md
index 2e12ece..5ee187f 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,6 @@ A professional VS Code extension for visual Markdown comparison. Compare rendere
- **Visual Diff**: Compare rendered HTML side-by-side or inline instead of raw source code.
- ⚔️ **Visual Conflict Resolver**: Resolve Git merge conflicts (`<<<<<<<`, `|||||||`, `=======`, `>>>>>>>`) in rendered Markdown with an interactive 3-Way Merge View (`Accept Mine`, `Accept Theirs`, `Accept Both`).
-- 📊 **Semantic Mermaid Diff**: Element-level diff highlighting for Mermaid flowcharts. Added, deleted, and modified nodes and edges are dynamically color-coded with ghost outlines for deleted elements.
- **Git Integration**: Compare changes directly from the VS Code Source Control view (Workspace, Staged, or HEAD).
- **Git Blame**: Hover over any line in the diff view to instantly see its commit author and history.
- **Interactive Image Diff**: Compare visual changes in images using "Swipe" and "Onion Skin" comparison modes.
@@ -23,7 +22,7 @@ A professional VS Code extension for visual Markdown comparison. Compare rendere
| **MDX / Custom** | Native diffing for MDX, Docusaurus, and Astro Starlight components (Tabs, Steps, Badges, Cards, and Admonitions). |
| **Marp** | Render and diff slide decks with full theme support. |
| **Math** | High-quality KaTeX rendering for formulas. |
-| **Mermaid** | Flowcharts with element-level diff highlighting, plus sequence diagrams and Gantt charts with VS Code theme-aware rendering. |
+| **Mermaid** | Flowcharts, sequence diagrams, and Gantt charts with VS Code theme-aware rendering. |
| **Obsidian** | Native support for Tags (`#tag`) and Transclusions (`![[link]]`). |
| **Alerts** | GitHub-style `[!NOTE]`, `[!WARNING]`, etc. |
| **Structure** | Robust diffing for Tables, Nested Lists, Footnotes, and block-level math/code changes. |
@@ -66,8 +65,8 @@ The following default shortcuts are available when the visual diff panel is acti
| Shortcut (Mac) | Shortcut (Windows/Linux) | Command | Description |
| --- | --- | --- | --- |
| `F7` / `Shift+F7` | `F7` / `Shift+F7` | Next / Previous Change | Jump to the next or previous diff highlight. |
-| `Alt+F7` | `Alt+F7` | Toggle Inline View | Toggle between Side-by-Side and Inline diff layouts. |
-| `Alt+F8` | `Alt+F8` | Toggle Fold Unchanged | Fold or unfold unchanged Markdown blocks. |
+| `Option+F7` | `Alt+F7` | Toggle Inline View | Toggle between Side-by-Side and Inline diff layouts. |
+| `Option+F8` | `Alt+F8` | Toggle Fold Unchanged | Fold or unfold unchanged Markdown blocks. |
| `Cmd+Shift+C` | `Ctrl+Shift+C` | Compare with Clipboard | Compare active Markdown document with clipboard content. |
## Technical Details
diff --git a/fixtures/expected/comprehensive.html b/fixtures/expected/comprehensive.html
index e960560..e4f76b2 100644
--- a/fixtures/expected/comprehensive.html
+++ b/fixtures/expected/comprehensive.html
@@ -26,7 +26,8 @@
function greet (name ) {
+function greet (name ) {
+
console .log (`Hello, ${name} ! Welcome! ` );
}
@@ -86,7 +87,17 @@ Math (KaTeX)
M834 80h400000v40h-400000z">
New equation added:
∑ i = 1 n i = n ( n + 1 ) 2 \sum_{i=1}^{n} i = \frac{n(n+1)}{2}
i = 1 ∑ n i = 2 n ( n + 1 )
Mermaid Diagrams
-
+graph TD;
+ A-->B;
+ B-->C;
+ C-->D;
+
+
Math (KaTeX)
E --> H[End]
F --> H
G --> H
-">graph TD
+">
+graph TD
A[Start] --> B{Decision}
B -- Yes --> C[Process One]
B -- No --> D[Process Two]
@@ -107,32 +119,7 @@
Math (KaTeX)
F --> H
G --> H
-%% Ghost definitions for deleted elements
- A--["A--"]
- B--["B--"]
- C--["C--"]
- C -.-> D
-
-%% Dynamic Diff Styles
- style A fill:#e6ffec,stroke:#22863a,stroke-width:2px;
- style E fill:#e6ffec,stroke:#22863a,stroke-width:2px;
- style F fill:#e6ffec,stroke:#22863a,stroke-width:2px;
- style G fill:#e6ffec,stroke:#22863a,stroke-width:2px;
- style H fill:#e6ffec,stroke:#22863a,stroke-width:2px;
- style A-- fill:#ffeef0,stroke:#d73a49,stroke-width:1px,stroke-dasharray:5 5,opacity:0.75;
- style B-- fill:#ffeef0,stroke:#d73a49,stroke-width:1px,stroke-dasharray:5 5,opacity:0.75;
- style C-- fill:#ffeef0,stroke:#d73a49,stroke-width:1px,stroke-dasharray:5 5,opacity:0.75;
- style B fill:#fffdef,stroke:#b08800,stroke-width:2px;
- style C fill:#fffdef,stroke:#b08800,stroke-width:2px;
- style D fill:#fffdef,stroke:#b08800,stroke-width:2px;
- linkStyle 2 stroke:#22863a,stroke-width:2px;
- linkStyle 3 stroke:#22863a,stroke-width:2px;
- linkStyle 4 stroke:#22863a,stroke-width:2px;
- linkStyle 5 stroke:#22863a,stroke-width:2px;
- linkStyle 6 stroke:#22863a,stroke-width:2px;
- linkStyle 7 stroke:#22863a,stroke-width:2px;
- linkStyle 8 stroke:#22863a,stroke-width:2px;
- linkStyle 9 stroke:#d73a49,stroke-width:1px,stroke-dasharray:3 3,opacity:0.75;GitHub Alerts
+ GitHub Alerts
Note
This is a note alert with updated content .
Warning
This is a warning alert.
diff --git a/src/conflictEditorProvider.ts b/src/conflictEditorProvider.ts
index cbb4ffb..f990255 100644
--- a/src/conflictEditorProvider.ts
+++ b/src/conflictEditorProvider.ts
@@ -6,9 +6,13 @@
import * as crypto from "crypto";
import * as vscode from "vscode";
-import { parseConflictBlocks, reconstructDocument, DocBlock } from "./markdown/conflictParser";
-import { MarkdownDiffProvider } from "./markdownDiff";
-import { escapeHtml } from "./markdown/sanitizer";
+import {
+ parseConflictBlocks,
+ reconstructDocument,
+ DocBlock,
+ getConflictResolverShellHtml,
+ renderConflictBlocks,
+} from "./markdown/conflictParser";
export class ConflictEditorProvider implements vscode.CustomTextEditorProvider {
public static readonly viewType = "rich-markdown-diff.conflictResolver";
@@ -113,183 +117,15 @@ export class ConflictEditorProvider implements vscode.CustomTextEditorProvider {
* Returns the static shell HTML: styles + script only, no content.
* This is set on the webview exactly once and never replaced afterwards.
*/
- private getShellHtml(nonce: string): string {
- return `
-
-
-
-
-
-
-
Visual Conflict Resolver
-
-
-
-
-
-
-
- `;
+ public getShellHtml(nonce: string): string {
+ return getConflictResolverShellHtml(nonce);
}
/**
* Renders blocks to an HTML string for injection into #content via postMessage.
* Does NOT include // wrappers.
*/
- private renderBlocksContent(blocks: DocBlock[]): string {
- const diffProvider = new MarkdownDiffProvider();
- let bodyContent = "";
-
- for (const block of blocks) {
- if (block.type === "common") {
- const { html } = diffProvider.computeDiff(block.text, block.text);
- bodyContent += `
${html}
`;
- } else {
- const safeMineLabel = escapeHtml(block.mineLabel || "Current (Mine)");
- const safeTheirsLabel = escapeHtml(block.theirsLabel || "Incoming (Theirs)");
- const safeBlockId = escapeHtml(block.id);
-
- // Compute Mine vs Theirs diff to show highlighted changes
- const { html: mineVsTheirsHtml } = diffProvider.computeDiff(block.mine, block.theirs);
- const { html: mineHtml } = diffProvider.computeDiff(block.mine, block.mine);
- const { html: theirsHtml } = diffProvider.computeDiff(block.theirs, block.theirs);
-
- bodyContent += `
-
-
-
-
Diff Preview (Mine → Theirs)
-
${mineVsTheirsHtml}
-
-
-
-
Mine (${safeMineLabel})
-
${mineHtml}
-
-
-
Theirs (${safeTheirsLabel})
-
${theirsHtml}
-
-
-
- `;
- }
- }
-
- return bodyContent;
+ public renderBlocksContent(blocks: DocBlock[]): string {
+ return renderConflictBlocks(blocks);
}
}
diff --git a/src/exportHtml.ts b/src/exportHtml.ts
new file mode 100644
index 0000000..c4e1eb4
--- /dev/null
+++ b/src/exportHtml.ts
@@ -0,0 +1,222 @@
+import * as fs from "fs";
+import * as path from "path";
+
+/**
+ * Maps common file extensions to their corresponding MIME types.
+ */
+const MIME_TYPES: Record
= {
+ ".svg": "image/svg+xml",
+ ".png": "image/png",
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".gif": "image/gif",
+ ".webp": "image/webp",
+ ".bmp": "image/bmp",
+ ".ico": "image/x-icon",
+ ".avif": "image/avif",
+ ".woff": "font/woff",
+ ".woff2": "font/woff2",
+ ".ttf": "font/ttf",
+ ".otf": "font/otf",
+ ".eot": "application/vnd.ms-fontobject",
+};
+
+/**
+ * Extracts a local filesystem path from a VS Code Webview URI or file URI string.
+ *
+ * Supported formats:
+ * - https://file%2B.vscode-resource.vscode-cdn.net/...
+ * - https://file+.vscode-resource.vscode-cdn.net/...
+ * - vscode-resource://...
+ * - vscode-file://vscode-app/...
+ * - file://...
+ */
+export function extractLocalFsPath(uriStr: string): string | undefined {
+ if (!uriStr) {
+ return undefined;
+ }
+
+ let decoded = uriStr.trim();
+
+ // Strip query and hash if present
+ const queryIndex = decoded.search(/[?#]/);
+ if (queryIndex !== -1) {
+ decoded = decoded.slice(0, queryIndex);
+ }
+
+ const vscodeResourceCdnMatch = decoded.match(/^https:\/\/(?:file%2B|file\+)\.vscode-resource\.vscode-cdn\.net\/(.*)$/i);
+ if (vscodeResourceCdnMatch) {
+ let rawPath = decodeURIComponent(vscodeResourceCdnMatch[1]);
+ // If Windows drive letter without leading slash: e.g. "c:/path/to" or "/c:/path/to"
+ if (/^\/?[a-zA-Z]:[\\/]/.test(rawPath)) {
+ rawPath = rawPath.replace(/^\//, "");
+ } else if (!rawPath.startsWith("/")) {
+ rawPath = "/" + rawPath;
+ }
+ return rawPath;
+ }
+
+ const vscodeFileMatch = decoded.match(/^vscode-file:\/\/vscode-app\/(.*)$/i);
+ if (vscodeFileMatch) {
+ let rawPath = decodeURIComponent(vscodeFileMatch[1]);
+ if (/^\/?[a-zA-Z]:[\\/]/.test(rawPath)) {
+ rawPath = rawPath.replace(/^\//, "");
+ } else if (!rawPath.startsWith("/")) {
+ rawPath = "/" + rawPath;
+ }
+ return rawPath;
+ }
+
+ const vscodeResourceMatch = decoded.match(/^vscode-resource:(?:\/\/file)?\/(.*)$/i);
+ if (vscodeResourceMatch) {
+ let rawPath = decodeURIComponent(vscodeResourceMatch[1]);
+ if (/^\/?[a-zA-Z]:[\\/]/.test(rawPath)) {
+ rawPath = rawPath.replace(/^\//, "");
+ } else if (!rawPath.startsWith("/")) {
+ rawPath = "/" + rawPath;
+ }
+ return rawPath;
+ }
+
+ const fileMatch = decoded.match(/^file:\/\/(.*)$/i);
+ if (fileMatch) {
+ let rawPath = decodeURIComponent(fileMatch[1]);
+ // file:///c:/... or file:///Users/...
+ if (/^\/[a-zA-Z]:[\\/]/.test(rawPath)) {
+ rawPath = rawPath.slice(1);
+ }
+ return rawPath;
+ }
+
+ // Already a local absolute path
+ if (path.isAbsolute(decoded)) {
+ return decoded;
+ }
+
+ return undefined;
+}
+
+/**
+ * Returns MIME type based on file extension.
+ */
+export function getMimeType(filePath: string): string {
+ const ext = path.extname(filePath).toLowerCase();
+ return MIME_TYPES[ext] || "application/octet-stream";
+}
+
+/**
+ * Default file reader using node fs promises.
+ */
+async function defaultReadFile(fsPath: string): Promise {
+ return await fs.promises.readFile(fsPath);
+}
+
+export interface PrepareExportHtmlOptions {
+ readFile?: (fsPath: string) => Promise;
+ mermaidCdn?: string;
+ hljsLightCdn?: string;
+ hljsDarkCdn?: string;
+ katexFontCdnBase?: string;
+}
+
+/**
+ * Processes VS Code webview HTML to produce a self-contained, browser-compatible HTML document.
+ * Inlines local images as Base64 Data URIs and replaces local scripts/styles with CDN equivalents.
+ */
+export async function prepareExportHtml(
+ rawHtml: string,
+ options: PrepareExportHtmlOptions = {},
+): Promise {
+ const readFile = options.readFile || defaultReadFile;
+ const mermaidCdn =
+ options.mermaidCdn ||
+ "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js";
+ const hljsLightCdn =
+ options.hljsLightCdn ||
+ "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css";
+ const hljsDarkCdn =
+ options.hljsDarkCdn ||
+ "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css";
+ const katexFontCdnBase =
+ options.katexFontCdnBase ||
+ "https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/fonts";
+
+ let html = rawHtml;
+
+ // 1. Replace Highlight.js and Mermaid CDN scripts/styles
+ html = html
+ .replace(
+ /href="[^"]*github\.min\.css[^"]*"/i,
+ `href="${hljsLightCdn}"`,
+ )
+ .replace(
+ /href="[^"]*github-dark\.min\.css[^"]*"/i,
+ `href="${hljsDarkCdn}"`,
+ )
+ .replace(
+ /src="[^"]*mermaid\.min\.js[^"]*"/i,
+ `src="${mermaidCdn}"`,
+ );
+
+ // 2. Adjust Content-Security-Policy for standalone browser execution
+ // Replace strict webview CSP with a safe standalone CSP allowing CDN and data: URIs
+ const standaloneCsp = ` `;
+ html = html.replace(
+ / ]*>/i,
+ standaloneCsp,
+ );
+
+ // 3. Find and replace all local image URLs and asset URLs with Base64 Data URIs
+ // Targets:
+ // -
+ // - CSS url(...) inside style tags
+ const uriRegex = /(?:src=["']([^"']+)["']|url\((['"]?)([^'")]+)\2\))/gi;
+ const matches: string[] = [];
+ let m: RegExpExecArray | null;
+
+ while ((m = uriRegex.exec(html)) !== null) {
+ const candidateUri = m[1] || m[3];
+ if (candidateUri && !candidateUri.startsWith("data:")) {
+ matches.push(candidateUri);
+ }
+ }
+
+ // Deduplicate URIs to process
+ const uniqueUris = Array.from(new Set(matches));
+ const uriToDataMap = new Map();
+
+ for (const uri of uniqueUris) {
+ // Check if this is a KaTeX font file that can be replaced by KaTeX CDN or inlined
+ if (uri.includes("/media/katex/fonts/")) {
+ const fontFilename = path.basename(uri.split("?")[0]);
+ if (fontFilename) {
+ // Replace with CDN font URL
+ uriToDataMap.set(uri, `${katexFontCdnBase}/${fontFilename}`);
+ continue;
+ }
+ }
+
+ const fsPath = extractLocalFsPath(uri);
+ if (!fsPath) {
+ continue;
+ }
+
+ try {
+ const fileBytes = await readFile(fsPath);
+ const mime = getMimeType(fsPath);
+ const base64 = Buffer.from(fileBytes).toString("base64");
+ const dataUri = `data:${mime};base64,${base64}`;
+ uriToDataMap.set(uri, dataUri);
+ } catch (e) {
+ console.warn(`[rich-markdown-diff] Failed to inline asset for export: ${fsPath}`, e);
+ }
+ }
+
+ // Replace URIs in HTML
+ for (const [origUri, replacement] of uriToDataMap.entries()) {
+ // Escape for literal replace
+ html = html.split(origUri).join(replacement);
+ }
+
+ return html;
+}
diff --git a/src/extension.ts b/src/extension.ts
index 3bc4f5a..d73f51f 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -45,6 +45,7 @@ import {
} from "./commandTarget";
import * as path from "path";
import * as l10n from "@vscode/l10n";
+import { prepareExportHtml } from "./exportHtml";
/**
* Escapes HTML special characters to prevent XSS in webview content.
@@ -1177,25 +1178,12 @@ export function activate(context: vscode.ExtensionContext) {
}
try {
- let htmlContent = activePanel.webview.html;
-
- const mermaidCdn = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js";
- const hljsLightCdn = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css";
- const hljsDarkCdn = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css";
-
- htmlContent = htmlContent
- .replace(
- /href="[^"]*github\.min\.css[^"]*"/i,
- `href="${hljsLightCdn}"`
- )
- .replace(
- /href="[^"]*github-dark\.min\.css[^"]*"/i,
- `href="${hljsDarkCdn}"`
- )
- .replace(
- /src="[^"]*mermaid\.min\.js[^"]*"/i,
- `src="${mermaidCdn}"`
- );
+ const rawHtml = activePanel.webview.html;
+ const htmlContent = await prepareExportHtml(rawHtml, {
+ readFile: async (fsPath: string) => {
+ return await vscode.workspace.fs.readFile(vscode.Uri.file(fsPath));
+ },
+ });
await vscode.workspace.fs.writeFile(
saveUri,
diff --git a/src/markdown/conflictParser.ts b/src/markdown/conflictParser.ts
index c436dfb..9633b13 100644
--- a/src/markdown/conflictParser.ts
+++ b/src/markdown/conflictParser.ts
@@ -4,6 +4,9 @@
* Copyright (c) 2026 Rich Markdown Diff Authors
*/
+import { MarkdownDiffProvider } from "../markdownDiff";
+import { escapeHtml } from "./sanitizer";
+
export interface CommonBlock {
type: "common";
text: string;
@@ -24,6 +27,205 @@ export interface ConflictBlock {
export type DocBlock = CommonBlock | ConflictBlock;
+/**
+ * Returns the static shell HTML: styles + script only, no content.
+ * This is set on the webview exactly once and never replaced afterwards.
+ */
+export function getConflictResolverShellHtml(nonce: string): string {
+ return `
+
+
+
+
+
+
+ Visual Conflict Resolver
+
+
+
+
+
+
+
+ `;
+}
+
+/**
+ * Renders blocks to an HTML string for injection into #content via postMessage.
+ * Does NOT include // wrappers.
+ */
+export function renderConflictBlocks(blocks: DocBlock[]): string {
+ const diffProvider = new MarkdownDiffProvider();
+ let bodyContent = "";
+
+ for (const block of blocks) {
+ if (block.type === "common") {
+ const { html } = diffProvider.computeDiff(block.text, block.text);
+ bodyContent += `${html}
`;
+ } else {
+ const safeMineLabel = escapeHtml(block.mineLabel || "Current (Mine)");
+ const safeTheirsLabel = escapeHtml(block.theirsLabel || "Incoming (Theirs)");
+ const safeBlockId = escapeHtml(block.id);
+
+ // Compute Mine vs Theirs diff to show highlighted changes
+ const { html: mineVsTheirsHtml } = diffProvider.computeDiff(block.mine, block.theirs);
+ const { html: mineHtml } = diffProvider.computeDiff(block.mine, block.mine);
+ const { html: theirsHtml } = diffProvider.computeDiff(block.theirs, block.theirs);
+
+ bodyContent += `
+
+
+
+
Diff Preview (Mine → Theirs)
+
${mineVsTheirsHtml}
+
+
+
+
Mine (${safeMineLabel})
+
${mineHtml}
+
+
+
Theirs (${safeTheirsLabel})
+
${theirsHtml}
+
+
+
+ `;
+ }
+ }
+
+ return bodyContent;
+}
+
/**
* Parses markdown text containing Git conflict markers (<<<<<<<, |||||||, =======, >>>>>>>)
* into a sequence of Common and Conflict blocks.
diff --git a/src/markdown/mermaidDiff.ts b/src/markdown/mermaidDiff.ts
index 193894e..530ab6c 100644
--- a/src/markdown/mermaidDiff.ts
+++ b/src/markdown/mermaidDiff.ts
@@ -130,7 +130,7 @@ export function parseMermaidNodes(code: string): Map {
continue;
}
- const nodeRegex = /\b([a-zA-Z0-9_-]+)(?:\(\((?:["']([^"']+)["']|([^\)]+))\)\)|\[\[(?:["']([^"']+)["']|([^\]]+))\]\]|\[(?:["']([^"']+)["']|([^\]]+))\]|\((?:["']([^"']+)["']|([^\)]+))\)|\{(?:["']([^"']+)["']|([^\}]+))\}|>([^\\]]+)\])?/g;
+ const nodeRegex = /\b([a-zA-Z0-9_]+(?:-[a-zA-Z0-9_]+)*)(?:\(\((?:["']([^"']+)["']|([^\)]+))\)\)|\[\[(?:["']([^"']+)["']|([^\]]+))\]\]|\[(?:["']([^"']+)["']|([^\]]+))\]|\((?:["']([^"']+)["']|([^\)]+))\)|\{(?:["']([^"']+)["']|([^\}]+))\}|>([^\\]]+)\])?/g;
let match: RegExpExecArray | null;
while ((match = nodeRegex.exec(trimmed)) !== null) {
diff --git a/src/markdown/structuralDiff.ts b/src/markdown/structuralDiff.ts
index 13338a4..1864c1b 100644
--- a/src/markdown/structuralDiff.ts
+++ b/src/markdown/structuralDiff.ts
@@ -25,8 +25,6 @@
import * as crypto from "crypto";
import { diffTables } from "./tableDiff";
import { findClosing } from "./domUtils";
-import { computeMermaidDiff } from "./mermaidDiff";
-import { escapeHtml } from "./sanitizer";
/**
* Main entrance for computing granular HTML diffs.
@@ -144,7 +142,10 @@ export function executeWithFullPipeline(
}
// 5. Restore checkboxes
- restored = restoreCheckboxes(restored, cbTokensNew);
+ restored = restoreCheckboxes(restored, {
+ ...cbTokensOld,
+ ...cbTokensNew,
+ });
// Apply post-restoration refinements (like list Ghost items)
if (!options.skipRefinement) {
@@ -548,7 +549,17 @@ export function balanceDiffTags(html: string): string {
}
export function cleanupUnbalancedDiffTags(html: string): string {
- let result = balanceDiffTags(html);
+ // Protect pre/code blocks from having whitespace/newline diff tags stripped
+ const preBlocks: string[] = [];
+ const maskedHtml = html.replace(/]*>[\s\S]*?<\/pre>/gi, (match) => {
+ // Inside , only remove strictly empty diff tags with no content at all
+ const cleanedPre = match.replace(/<(ins|del)[^>]*><\/\1>/gi, "");
+ const token = `__PRE_CLEANUP_${preBlocks.length}_TOKEN__`;
+ preBlocks.push(cleanedPre);
+ return token;
+ });
+
+ let result = balanceDiffTags(maskedHtml);
// Fix cases like which happen if htmldiff gets confused by fragments
result = result.replace(/\s*<\/del>\s*<\/ins>/gi, "");
@@ -560,6 +571,11 @@ export function cleanupUnbalancedDiffTags(html: string): string {
// Remove empty diff tags
result = result.replace(/<(ins|del)[^>]*>\s*<\/\1>/gi, "");
+ // Restore protected pre blocks
+ for (let i = 0; i < preBlocks.length; i++) {
+ result = result.replace(`__PRE_CLEANUP_${i}_TOKEN__`, () => preBlocks[i]);
+ }
+
return result;
}
@@ -716,11 +732,16 @@ export function consolidateWrappedItems(html: string): string {
return html.replace(
/<(h[1-6]|p|blockquote)([^>]*)>\s*<(ins|del)[^>]*>\s*([\s\S]*?)\s*<\/\3>\s*<\/\1>/gi,
(match, tag, attrs, type, content) => {
- const diffClass = type === "ins" ? "diffins" : "diffdel";
// Ensure we don't wrap twice
if (match.includes("diff-block")) {
return match;
}
+ // Safety guard: ensure content does not contain additional unclosed/nested diff tags
+ // which indicates multiple diff tags inside the block rather than the entire block being wrapped
+ if (/<(ins|del)\b/i.test(content) || /<\/(ins|del)>/i.test(content)) {
+ return match;
+ }
+ const diffClass = type === "ins" ? "diffins" : "diffdel";
return `<${type} class="${diffClass}"><${tag}${attrs}>${content}${tag}>${type}>`;
},
);
@@ -1538,6 +1559,16 @@ export function refineBlockDiffs(
},
);
+ const tableRegex =
+ /(]*>\s*(]*>[\s\S]*?<\/table>)\s*<\/del>)\s*(]*>\s*(]*>[\s\S]*?<\/table>)\s*<\/ins>)/gi;
+
+ resultHtml = resultHtml.replace(
+ tableRegex,
+ (match, delBlock, oldInner, insBlock, newInner) => {
+ return diffTables(oldInner, newInner, execute);
+ },
+ );
+
const genericBlockRegex =
/(]*>\s*<([a-z1-6]+)(?:\s+[^>]*)?>([\s\S]*?)<\/\2>\s*<\/del>)\s*(]*>\s*<([a-z1-6]+)(?:\s+[^>]*)?>([\s\S]*?)<\/\5>\s*<\/ins>)/gi;
@@ -1548,9 +1579,9 @@ export function refineBlockDiffs(
return match;
}
- // Special handling for pre blocks to ensure we don't break syntax highlighting
- // if it's already highlighted.
- if (delTag.toLowerCase() === "pre") {
+ // Special handling for pre and table blocks to ensure we don't break syntax highlighting
+ // or bypass structured table diffing.
+ if (delTag.toLowerCase() === "pre" || delTag.toLowerCase() === "table") {
return match;
}
@@ -1562,28 +1593,13 @@ export function refineBlockDiffs(
attributesMatch && attributesMatch[1] ? attributesMatch[1] : "";
// EXCEPTION: Do not re-diff the inside of specialized blocks like Mermaid or GitHub Alerts.
- // For Mermaid: Re-diffing injects / tags that break their specific parsers.
+ // For Mermaid: Preserve separate (original) and (modified) blocks for split/inline view.
// For Alerts: It often causes redundant nesting (double vertical bars).
if (
/class=["'][^"']*(?:mermaid|markdown-alert|katex)[^"']*["']/i.test(
attributes,
)
) {
- if (/class=["'][^"']*mermaid[^"']*["']/i.test(attributes)) {
- const unescapeHtml = (str: string) =>
- str
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/"/g, '"')
- .replace(/'/g, "'")
- .replace(/&/g, "&");
-
- const oldCode = unescapeHtml(delInner.replace(/<[^>]+>/g, "").trim());
- const newCode = unescapeHtml(insInner.replace(/<[^>]+>/g, "").trim());
- const diffMermaid = computeMermaidDiff(oldCode, newCode);
- const escapedDiff = escapeHtml(diffMermaid);
- return `<${insTag}${attributes}>${escapedDiff}${insTag}>`;
- }
return match;
}
@@ -1627,16 +1643,6 @@ export function refineBlockDiffs(
},
);
- const tableRegex =
- /(]*>\s*(]*>[\s\S]*?<\/table>)\s*<\/del>)\s*(]*>\s*(]*>[\s\S]*?<\/table>)\s*<\/ins>)/gi;
-
- resultHtml = resultHtml.replace(
- tableRegex,
- (match, delBlock, oldInner, insBlock, newInner) => {
- return diffTables(oldInner, newInner, execute);
- },
- );
-
const imageRegex =
/(?:]*)>\s*)?(]*>\s*( ]*>)\s*<\/del>)\s*(]*>\s*( ]*>)\s*<\/ins>)(?:\s*<\/p>)?/gi;
diff --git a/src/markdown/webviewTemplate.ts b/src/markdown/webviewTemplate.ts
index d91f4e5..ce78805 100644
--- a/src/markdown/webviewTemplate.ts
+++ b/src/markdown/webviewTemplate.ts
@@ -1008,6 +1008,10 @@ export function getWebviewContent(
.task-list-item {
position: relative;
}
+ .task-list-item p {
+ display: inline;
+ margin: 0;
+ }
.task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
@@ -3679,7 +3683,14 @@ export function getWebviewContent(
if (!activeHeading) {
// Before first heading
- container.innerHTML = '' + t("Top") + ' ';
+ container.innerHTML = '';
+ const topItem = document.createElement('span');
+ topItem.className = 'breadcrumb-item';
+ topItem.textContent = t("Top");
+ topItem.onclick = () => {
+ pane.scrollTop = 0;
+ };
+ container.appendChild(topItem);
return;
}
diff --git a/src/test/unit/checkbox_regression.test.ts b/src/test/unit/checkbox_regression.test.ts
index f38046f..ea7f63d 100644
--- a/src/test/unit/checkbox_regression.test.ts
+++ b/src/test/unit/checkbox_regression.test.ts
@@ -70,4 +70,17 @@ describe("Checkbox Regression Test", () => {
assert.strictEqual(restored.includes(' '), true, "Should restore token despite mangled casing");
assert.strictEqual(restored.includes('zChEcKbOxZ'), false, "Token string should be gone");
});
+
+ it('should preserve text and checkbox integrity when comparing tight and loose task lists', () => {
+ const oldMd = `- [ ] Task A\n- [x] Task B\n- [ ] Task C`;
+ const newMd = `- [x] Task A (Checked)\n\n- [ ] Task C`;
+
+ const { html: diffHtml } = provider.computeDiff(oldMd, newMd);
+
+ // Verify Task A text is NOT wrapped inside an ins tag along with p
+ assert.strictEqual(/]*>
wrapper");
+ assert.ok(diffHtml.includes("Task A"), "Task A text must be present in diff HTML");
+ assert.ok(diffHtml.includes("Task C"), "Task C text must be present in diff HTML");
+ });
});
+
diff --git a/src/test/unit/conflictParser.test.ts b/src/test/unit/conflictParser.test.ts
index fec9aba..1b89f3d 100644
--- a/src/test/unit/conflictParser.test.ts
+++ b/src/test/unit/conflictParser.test.ts
@@ -2,6 +2,8 @@ import * as assert from "assert";
import {
parseConflictBlocks,
reconstructDocument,
+ renderConflictBlocks,
+ getConflictResolverShellHtml,
} from "../../markdown/conflictParser";
describe("Conflict Parser", () => {
@@ -112,4 +114,35 @@ Unclosed mine content`;
assert.strictEqual(blocks[1].type, "common");
assert.ok(blocks[1].text.includes("Unclosed mine content"));
});
+
+ it("should render conflict buttons with data attributes and no inline onclick handlers", () => {
+ const text = `<<<<<<< HEAD
+Mine content
+=======
+Theirs content
+>>>>>>> feature`;
+ const blocks = parseConflictBlocks(text);
+
+ const html = renderConflictBlocks(blocks);
+
+ // Verify buttons have .btn-resolve and data attributes
+ assert.ok(html.includes('class="btn-resolve"'), "Buttons should have btn-resolve class");
+ assert.ok(html.includes('data-block-id="conflict-1"'), "Buttons should have data-block-id attribute");
+ assert.ok(html.includes('data-choice="mine"'), "Accept Mine button should have data-choice='mine'");
+ assert.ok(html.includes('data-choice="theirs"'), "Accept Theirs button should have data-choice='theirs'");
+ assert.ok(html.includes('data-choice="both"'), "Accept Both button should have data-choice='both'");
+
+ // Crucial: Ensure NO inline onclick attributes are generated (blocked by CSP)
+ assert.strictEqual(html.includes("onclick="), false, "HTML must not contain inline onclick attributes");
+ });
+
+ it("should generate shell HTML with CSP nonce and event delegation for .btn-resolve", () => {
+ const nonce = "test-nonce-12345";
+
+ const shellHtml = getConflictResolverShellHtml(nonce);
+
+ assert.ok(shellHtml.includes(`script-src 'nonce-${nonce}'`), "Shell HTML should include CSP nonce");
+ assert.ok(shellHtml.includes(".btn-resolve"), "Shell HTML script should delegate clicks on .btn-resolve");
+ assert.ok(shellHtml.includes("resolveConflict"), "Shell HTML should define resolveConflict function");
+ });
});
diff --git a/src/test/unit/consolidate_wrapped_items.test.ts b/src/test/unit/consolidate_wrapped_items.test.ts
index d85d548..5545e02 100644
--- a/src/test/unit/consolidate_wrapped_items.test.ts
+++ b/src/test/unit/consolidate_wrapped_items.test.ts
@@ -12,4 +12,10 @@ describe("consolidateWrappedItems bug", () => {
assert.strictEqual(result.includes('
outside of if inside ");
assert.strictEqual(result, input, "Should leave inside ");
});
+ it("should NOT move diff tags outside when block contains multiple diff tags", () => {
+ const input = ' Task A (Checked)
';
+ const result = consolidateWrappedItems(input);
+ assert.strictEqual(result, input, "Should leave multiple tags intact without broken wrapping");
+ });
});
+
diff --git a/src/test/unit/exportHtml.test.ts b/src/test/unit/exportHtml.test.ts
new file mode 100644
index 0000000..806dfa7
--- /dev/null
+++ b/src/test/unit/exportHtml.test.ts
@@ -0,0 +1,156 @@
+import * as assert from "assert";
+import {
+ extractLocalFsPath,
+ getMimeType,
+ prepareExportHtml,
+} from "../../exportHtml";
+
+describe("Export HTML Module", () => {
+ describe("extractLocalFsPath", () => {
+ it("should extract fsPath from vscode-resource.vscode-cdn.net (percent-encoded +)", () => {
+ const uri = "https://file%2B.vscode-resource.vscode-cdn.net/Users/test/image.png";
+ assert.strictEqual(extractLocalFsPath(uri), "/Users/test/image.png");
+ });
+
+ it("should extract fsPath from vscode-resource.vscode-cdn.net (+)", () => {
+ const uri = "https://file+.vscode-resource.vscode-cdn.net/Users/test/image.svg";
+ assert.strictEqual(extractLocalFsPath(uri), "/Users/test/image.svg");
+ });
+
+ it("should extract Windows fsPath correctly", () => {
+ const uri = "https://file%2B.vscode-resource.vscode-cdn.net/c%3A/Users/test/image.png";
+ assert.strictEqual(extractLocalFsPath(uri), "c:/Users/test/image.png");
+ });
+
+ it("should extract fsPath from vscode-file:// URL", () => {
+ const uri = "vscode-file://vscode-app/Users/test/image.webp";
+ assert.strictEqual(extractLocalFsPath(uri), "/Users/test/image.webp");
+ });
+
+ it("should extract fsPath from vscode-resource: URL", () => {
+ const uri = "vscode-resource:/Users/test/image.gif";
+ assert.strictEqual(extractLocalFsPath(uri), "/Users/test/image.gif");
+ });
+
+ it("should extract fsPath from file:// URL", () => {
+ const uri = "file:///Users/test/image.png";
+ assert.strictEqual(extractLocalFsPath(uri), "/Users/test/image.png");
+ });
+
+ it("should ignore query parameters and hashes", () => {
+ const uri = "https://file%2B.vscode-resource.vscode-cdn.net/Users/test/image.png?v=123#frag";
+ assert.strictEqual(extractLocalFsPath(uri), "/Users/test/image.png");
+ });
+
+ it("should return undefined for non-file URLs", () => {
+ assert.strictEqual(extractLocalFsPath("https://example.com/image.png"), undefined);
+ assert.strictEqual(extractLocalFsPath("data:image/png;base64,..."), undefined);
+ });
+ });
+
+ describe("getMimeType", () => {
+ it("should identify SVG, PNG, JPEG, GIF, WebP", () => {
+ assert.strictEqual(getMimeType("test.svg"), "image/svg+xml");
+ assert.strictEqual(getMimeType("test.png"), "image/png");
+ assert.strictEqual(getMimeType("test.jpg"), "image/jpeg");
+ assert.strictEqual(getMimeType("test.jpeg"), "image/jpeg");
+ assert.strictEqual(getMimeType("test.gif"), "image/gif");
+ assert.strictEqual(getMimeType("test.webp"), "image/webp");
+ assert.strictEqual(getMimeType("test.woff2"), "font/woff2");
+ });
+ });
+
+ describe("prepareExportHtml", () => {
+ it("should replace local image URIs with Base64 Data URIs", async () => {
+ const mockHtml = `
+
+
+
+
+
+
+
+
+
+`;
+
+ const svgContent = ` `;
+ const pngContent = Buffer.from("fake-png-bytes");
+
+ const mockReadFile = async (filePath: string): Promise => {
+ if (filePath.endsWith(".svg")) {
+ return Buffer.from(svgContent, "utf8");
+ }
+ if (filePath.endsWith(".png")) {
+ return pngContent;
+ }
+ throw new Error("File not found");
+ };
+
+ const result = await prepareExportHtml(mockHtml, { readFile: mockReadFile });
+
+ // Should contain Base64 data URIs
+ const expectedSvgBase64 = Buffer.from(svgContent, "utf8").toString("base64");
+ const expectedPngBase64 = pngContent.toString("base64");
+
+ assert.ok(
+ result.includes(`src="data:image/svg+xml;base64,${expectedSvgBase64}"`),
+ "Should replace SVG src with Base64 data URI",
+ );
+ assert.ok(
+ result.includes(`src="data:image/png;base64,${expectedPngBase64}"`),
+ "Should replace PNG src with Base64 data URI",
+ );
+ // CSP should be relaxed for standalone
+ assert.ok(!result.includes("default-src 'none'"), "Should replace strict webview CSP");
+ assert.ok(result.includes("default-src 'self'"), "Should have standalone CSP");
+ });
+
+ it("should replace KaTeX font URIs with CDN URLs", async () => {
+ const mockHtml = `
+`;
+
+ const result = await prepareExportHtml(mockHtml, {
+ readFile: async () => {
+ throw new Error("Should use CDN for katex fonts");
+ },
+ });
+
+ assert.ok(
+ result.includes("https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/fonts/KaTeX_Main-Regular.woff2"),
+ "Should replace local KaTeX font with KaTeX CDN font URL",
+ );
+ });
+
+ it("should replace Highlight.js and Mermaid URIs with CDN equivalents", async () => {
+ const mockHtml = `
+
+
+`;
+
+ const result = await prepareExportHtml(mockHtml);
+
+ assert.ok(result.includes("https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"));
+ assert.ok(result.includes("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css"));
+ assert.ok(result.includes("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css"));
+ });
+
+ it("should gracefully handle missing local image files without crashing", async () => {
+ const mockHtml = ` `;
+
+ const result = await prepareExportHtml(mockHtml, {
+ readFile: async () => {
+ throw new Error("File not found on disk");
+ },
+ });
+
+ // Should keep original URL if reading fails
+ assert.ok(result.includes("https://file%2B.vscode-resource.vscode-cdn.net/Users/missing/image.png"));
+ });
+ });
+});
diff --git a/src/test/unit/markdownDiff.test.ts b/src/test/unit/markdownDiff.test.ts
index f4b049d..1cbcd65 100644
--- a/src/test/unit/markdownDiff.test.ts
+++ b/src/test/unit/markdownDiff.test.ts
@@ -104,6 +104,18 @@ describe("MarkdownDiffProvider", () => {
assert.ok(diff.includes("graph TD;"), "Should contain mermaid content");
});
+ it("should generate separate del (original) and ins (modified) blocks for modified mermaid diagrams", () => {
+ const oldMd = "```mermaid\ngraph TD;\nA-->B;\nB-->C;\n```";
+ const newMd = "```mermaid\ngraph TD\nA[Start] --> B{Decision}\nB --> C[Process One]\nB --> D[Process Two]\n```";
+
+ const { html: diff } = provider.computeDiff(oldMd, newMd);
+ assert.ok(diff.includes('class="mermaid"'), "Should contain mermaid class");
+ assert.ok(diff.includes(" {
const oldMd = "";
const newMd = "";
@@ -337,6 +349,41 @@ describe("MarkdownDiffProvider", () => {
);
});
+ it("should accurately align columns and cell changes when column count changes", () => {
+ const table3Cols = [
+ "| ID | Name | Role |",
+ "| :-- | :--- | :--- |",
+ "| 1 | Alice | Admin |",
+ "| 2 | Bob | User |",
+ "| 3 | Charlie | Guest |",
+ ].join("\n");
+
+ const table4Cols = [
+ "| ID | Name | Role | Status |",
+ "| :-- | :--- | :--- | :----- |",
+ "| 1 | Alice | Administrator | Active |",
+ "| 2 | Bob | User | Inactive |",
+ "| 3 | Charlie | Guest | Active |",
+ "| 4 | Dave | User | Active |",
+ ].join("\n");
+
+ // 1. 3 cols -> 4 cols (Column inserted)
+ const { html: diff3to4 } = provider.computeDiff(table3Cols, table4Cols);
+ assert.ok(diff3to4.includes("diff-col-ins"), "Inserted 4th column should have diff-col-ins class");
+ assert.ok(
+ diff3to4.includes("AdminAdministrator "),
+ "Role cell should show inline modification between Admin and Administrator",
+ );
+
+ // 2. 4 cols -> 3 cols (Column deleted in reverse diff)
+ const { html: diff4to3 } = provider.computeDiff(table4Cols, table3Cols);
+ assert.ok(diff4to3.includes("diff-col-del"), "Deleted 4th column should have diff-col-del class");
+ assert.ok(
+ diff4to3.includes("AdministratorAdmin "),
+ "Role cell in reverse diff should align Administrator -> Admin in the Role column, not drift to Status column",
+ );
+ });
+
it("should include explicit table styling in the webview", () => {
const webviewContent = provider.getWebviewContent(
"",
@@ -1311,6 +1358,21 @@ describe("MarkdownDiffProvider", () => {
);
});
+ it("should preserve newlines when appending code with blank lines inside code blocks", () => {
+ const oldMd = '```javascript\nfunction farewell(name) {\n console.log(`Goodbye, ${name}!`);\n}\n```';
+ const newMd = '```javascript\nfunction farewell(name) {\n console.log(`Goodbye, ${name}!`);\n}\n\nfunction good_morning(name) {\n console.log(`Hello, ${name}!`);\n}\n```';
+ const { html: diff } = provider.computeDiff(oldMd, newMd);
+
+ assert.ok(
+ /]*>\s*\n\s*<\/ins>/.test(diff) || diff.includes("\n\n ") || diff.includes("\n\n"),
+ "Diff should preserve newlines between functions in code blocks",
+ );
+ assert.ok(
+ !diff.includes("}function good_morning") && !diff.includes("} {
const { html: diff } = provider.computeDiff(
"Above\n\n---\n\nBelow",
diff --git a/src/test/unit/mermaidDiff.test.ts b/src/test/unit/mermaidDiff.test.ts
index c2ae1f4..e6d29c0 100644
--- a/src/test/unit/mermaidDiff.test.ts
+++ b/src/test/unit/mermaidDiff.test.ts
@@ -178,4 +178,49 @@ describe("Mermaid Semantic Diff", () => {
assert.ok(diff.includes("linkStyle 0 stroke:#d73a49"), "Edge 0 should be styled as deleted");
assert.ok(diff.includes("linkStyle 1 stroke:#d73a49"), "Edge 1 should be styled as deleted");
});
+
+ // --- MERMAID-03 regression tests ---
+
+ it("[MERMAID-03] should not consume arrow hyphens as part of node IDs (A-->B;)", () => {
+ const code = `
+ graph TD;
+ A-->B;
+ B-->C;
+ C-->D;
+ `;
+ const nodes = parseMermaidNodes(code);
+ assert.strictEqual(nodes.size, 4, "Should detect exactly 4 nodes (A, B, C, D)");
+ assert.ok(nodes.has("A"), "Node A must be present");
+ assert.ok(nodes.has("B"), "Node B must be present");
+ assert.ok(nodes.has("C"), "Node C must be present");
+ assert.ok(nodes.has("D"), "Node D must be present");
+ assert.ok(!nodes.has("A--"), "A-- must NOT be recognized as a node");
+ assert.ok(!nodes.has("B--"), "B-- must NOT be recognized as a node");
+ assert.ok(!nodes.has("C--"), "C-- must NOT be recognized as a node");
+ });
+
+ it("[MERMAID-03] should correctly compute diff between compact arrow diagrams and modified diagrams", () => {
+ const oldCode = `
+ graph TD;
+ A-->B;
+ B-->C;
+ C-->D;
+ `;
+ const newCode = `
+ graph TD
+ A[Start] --> B{Decision}
+ B -- Yes --> C[Process One]
+ B -- No --> D[Process Two]
+ C --> E[Branch A]
+ C --> F[Branch B]
+ E --> H[End]
+ F --> H
+ G --> H
+ `;
+ const diff = computeMermaidDiff(oldCode, newCode);
+ assert.ok(!diff.includes("A--"), "Should not include bogus A-- node");
+ assert.ok(diff.includes("style E fill:#e6ffec"), "Should style added node E as inserted");
+ assert.ok(diff.includes("style A fill:#fffdef"), "Should style modified node A as modified");
+ assert.ok(diff.includes("C -.-> D"), "Should include ghost edge for removed C-->D");
+ });
});
diff --git a/src/test/visual/__screenshots__/vrt.test.js-snapshots/comprehensive-v1-v2-split-light-chromium-linux.png b/src/test/visual/__screenshots__/vrt.test.js-snapshots/comprehensive-v1-v2-split-light-chromium-linux.png
index 2e53feaa51626960f898987ff4f630ff5d740ee2..ade4a8f47b2df8cebe90175b72a060fb97ee2088 100644
GIT binary patch
literal 568772
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