From 02de95b87b36a541f0739e91ca5f0fa74a17e30c Mon Sep 17 00:00:00 2001
From: phine-apps <91043289+phine-apps@users.noreply.github.com>
Date: Mon, 10 Aug 2026 22:34:28 +0900
Subject: [PATCH] feat: add Visual Conflict Resolver, Semantic Mermaid Diff,
and v1.5.0 updates
---
CHANGELOG.md | 20 ++
README.md | 27 +-
fixtures/expected/comprehensive.html | 42 ++-
package.json | 14 +-
pnpm-lock.yaml | 10 +-
src/conflictEditorProvider.ts | 295 ++++++++++++++++
src/extension.ts | 2 +
src/markdown/conflictParser.ts | 168 +++++++++
src/markdown/mermaidDiff.ts | 323 ++++++++++++++++++
src/markdown/structuralDiff.ts | 17 +
src/test/unit/conflictParser.test.ts | 115 +++++++
src/test/unit/mermaidDiff.test.ts | 181 ++++++++++
...nsive-v1-v2-split-light-chromium-linux.png | Bin 576431 -> 580678 bytes
13 files changed, 1190 insertions(+), 24 deletions(-)
create mode 100644 src/conflictEditorProvider.ts
create mode 100644 src/markdown/conflictParser.ts
create mode 100644 src/markdown/mermaidDiff.ts
create mode 100644 src/test/unit/conflictParser.test.ts
create mode 100644 src/test/unit/mermaidDiff.test.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a26d69..fa3b885 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
All notable changes to **Rich Markdown Diff** will be documented in this file.
+## [1.5.0] - 2026-08-DD
+
+### 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.
+
+### Fixed in 1.5.0
+
+- **Stability & Edge Cases**:
+ - Fixed frontmatter parser error when metadata keys include `hasOwnProperty`.
+ - Fixed MDX `` tag counting inside `` blocks.
+ - Resolved table diff `TypeError` on rows with fewer columns than the header.
+ - Added support for 64-character SHA-256 commit hashes in Git Blame.
+ - Fixed line number data-attribute injection when HTML tags contain `>`.
+ - Hardened validation for webview edit message payloads.
+
## [1.4.0] - 2026-07-19
### Added in 1.4.0
diff --git a/README.md b/README.md
index 195ab81..2e12ece 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,8 @@ A professional VS Code extension for visual Markdown comparison. Compare rendere
## Key Features
- **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.
@@ -21,7 +23,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, sequence diagrams, and Gantt charts with VS Code theme-aware rendering. |
+| **Mermaid** | Flowcharts with element-level diff highlighting, plus 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. |
@@ -29,6 +31,7 @@ A professional VS Code extension for visual Markdown comparison. Compare rendere
## Use Cases
+- ⚔️ **Merge Conflicts**: Resolve Git merge conflicts in Markdown docs visually rather than inspecting raw syntax markers.
- 🖼️ **Marp Presentations**: Verify slide layout and theme changes visually.
- 📚 **Knowledge Bases**: Review changes in foam/wiki notes with wikilinks and footnotes.
- 🌐 **Modern Tech Docs**: Catch rendering issues in MDX, Docusaurus, and Astro Starlight components.
@@ -42,12 +45,30 @@ A professional VS Code extension for visual Markdown comparison. Compare rendere
Select two `.md` files in the Explorer, right-click, and select **Show Markdown Diff**.
### 2. Git & SCM
-Click the **Show Markdown Diff** icon next to any modified Markdown file in the Source Control view.
+- **Working Tree & Staged**: Click the **Show Markdown Diff** icon next to any modified Markdown file in the Source Control view.
+- **Committed Revisions**: When viewing committed file diffs from Git history or the Git Graph, click the **Show Markdown Diff** icon in the editor title bar or right-click inside the diff editor and choose **Show Rich Markdown Diff**.

### 3. Clipboard Comparison
-Right-click in any Markdown editor and choose **Compare with Clipboard**.
+Right-click in any Markdown editor and choose **Compare with Clipboard** (or press `Cmd+Shift+C` / `Ctrl+Shift+C`).
+
+### 4. Visual Conflict Resolver
+Right-click any Markdown file containing Git conflict markers, choose **Open With...**, and select **Markdown Visual Conflict Resolver**. Click **Accept Mine**, **Accept Theirs**, or **Accept Both** on any conflict block to resolve it directly.
+
+### 5. Export to HTML
+Click the **Export to HTML** icon in the diff view toolbar, or run **Export Rendered Diff to HTML** from the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) to generate a standalone HTML document.
+
+## Keyboard Shortcuts
+
+The following default shortcuts are available when the visual diff panel is active:
+
+| 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. |
+| `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 40207e5..e960560 100644
--- a/fixtures/expected/comprehensive.html
+++ b/fixtures/expected/comprehensive.html
@@ -86,17 +86,7 @@ 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]
@@ -118,7 +107,32 @@
Math (KaTeX)
F --> H
G --> H
-GitHub Alerts
+%% 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
Note
This is a note alert with updated content .
Warning
This is a warning alert.
diff --git a/package.json b/package.json
index ba3cd70..5ff59b0 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"publisher": "phine-apps",
"displayName": "Rich Markdown Diff",
"description": "Professional visual Markdown diff with Math, Mermaid, and Git integration for better document reviews.",
- "version": "1.4.0",
+ "version": "1.5.0",
"icon": "images/icon.png",
"author": "phine-apps",
"license": "MIT",
@@ -313,6 +313,16 @@
}
],
"priority": "option"
+ },
+ {
+ "viewType": "rich-markdown-diff.conflictResolver",
+ "displayName": "Markdown Visual Conflict Resolver",
+ "selector": [
+ {
+ "filenamePattern": "*.md"
+ }
+ ],
+ "priority": "option"
}
]
},
@@ -348,7 +358,7 @@
"@typescript-eslint/eslint-plugin": "^8.63.0",
"@typescript-eslint/parser": "^8.63.0",
"@vscode/l10n-dev": "^0.0.35",
- "@vscode/test-electron": "^3.0.0",
+ "@vscode/test-electron": "^3.1.0",
"@vscode/vsce": "^3.9.2",
"esbuild": "^0.28.1",
"eslint": "^10.7.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3f7012d..afc8ab3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -88,8 +88,8 @@ importers:
specifier: ^0.0.35
version: 0.0.35
'@vscode/test-electron':
- specifier: ^3.0.0
- version: 3.0.0
+ specifier: ^3.1.0
+ version: 3.1.0
'@vscode/vsce':
specifier: ^3.9.2
version: 3.9.2
@@ -795,8 +795,8 @@ packages:
'@vscode/l10n@0.0.18':
resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==}
- '@vscode/test-electron@3.0.0':
- resolution: {integrity: sha512-TY5mC7aAjxSLDXsyjhrG8cJHgc/HLdiE5lvtW7hABYQrY24Qwozzr5UoO3HiuAM4Hzz4b7K/eZlwrCILj94CcA==}
+ '@vscode/test-electron@3.1.0':
+ resolution: {integrity: sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==}
engines: {node: '>=22'}
'@vscode/vsce-sign-alpine-arm64@2.0.6':
@@ -3289,7 +3289,7 @@ snapshots:
'@vscode/l10n@0.0.18': {}
- '@vscode/test-electron@3.0.0':
+ '@vscode/test-electron@3.1.0':
dependencies:
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
diff --git a/src/conflictEditorProvider.ts b/src/conflictEditorProvider.ts
new file mode 100644
index 0000000..cbb4ffb
--- /dev/null
+++ b/src/conflictEditorProvider.ts
@@ -0,0 +1,295 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2026 Rich Markdown Diff Authors
+ */
+
+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";
+
+export class ConflictEditorProvider implements vscode.CustomTextEditorProvider {
+ public static readonly viewType = "rich-markdown-diff.conflictResolver";
+
+ constructor(private readonly context: vscode.ExtensionContext) {}
+
+ public static register(context: vscode.ExtensionContext): vscode.Disposable {
+ const provider = new ConflictEditorProvider(context);
+ return vscode.window.registerCustomEditorProvider(
+ ConflictEditorProvider.viewType,
+ provider,
+ {
+ webviewOptions: {
+ retainContextWhenHidden: true,
+ },
+ supportsMultipleEditorsPerDocument: false,
+ }
+ );
+ }
+
+ public async resolveCustomTextEditor(
+ document: vscode.TextDocument,
+ webviewPanel: vscode.WebviewPanel,
+ _token: vscode.CancellationToken
+ ): Promise
{
+ webviewPanel.webview.options = {
+ enableScripts: true,
+ localResourceRoots: [
+ vscode.Uri.joinPath(this.context.extensionUri, "media"),
+ ],
+ };
+
+ // [SEC-01] Generate a per-session nonce for CSP (same approach as webviewTemplate.ts)
+ const nonce = crypto.randomBytes(16).toString("hex");
+
+ // [UX-01] Set the shell HTML only once. Subsequent updates are sent via postMessage
+ // to avoid a full Webview reload, which previously caused scroll-position race conditions.
+ webviewPanel.webview.html = this.getShellHtml(nonce);
+
+ const sendUpdate = () => {
+ const text = document.getText();
+ const blocks = parseConflictBlocks(text);
+ const content = this.renderBlocksContent(blocks);
+ webviewPanel.webview.postMessage({ command: "update", content });
+ };
+
+ const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument((e) => {
+ if (e.document.uri.toString() === document.uri.toString()) {
+ sendUpdate();
+ }
+ });
+
+ webviewPanel.onDidDispose(() => {
+ changeDocumentSubscription.dispose();
+ });
+
+ // [SEC-02] Allowlist for valid conflict resolution choices
+ const VALID_CHOICES = ["mine", "theirs", "both"] as const;
+ type ValidChoice = typeof VALID_CHOICES[number];
+
+ webviewPanel.webview.onDidReceiveMessage(async (message) => {
+ if (message.command === "resolveConflict") {
+ const { blockId, choice } = message;
+
+ // Validate choice against allowlist before applying
+ if (typeof choice !== "string" || !VALID_CHOICES.includes(choice as ValidChoice)) {
+ console.warn(`[ConflictEditorProvider] Rejected invalid choice: ${String(choice)}`);
+ return;
+ }
+
+ const text = document.getText();
+ const blocks = parseConflictBlocks(text);
+
+ const targetBlock = blocks.find(
+ (b) => b.type === "conflict" && b.id === blockId
+ );
+ if (targetBlock && targetBlock.type === "conflict") {
+ targetBlock.choice = choice as ValidChoice;
+ const newText = reconstructDocument(blocks);
+
+ const edit = new vscode.WorkspaceEdit();
+ const fullRange = new vscode.Range(
+ document.positionAt(0),
+ document.positionAt(text.length)
+ );
+ edit.replace(document.uri, fullRange, newText);
+ await vscode.workspace.applyEdit(edit);
+ }
+ } else if (message.command === "ready") {
+ // [UX-01] Webview signals that it is ready to receive content.
+ // Send the initial content only after the Webview JS is fully initialised,
+ // so the scroll-restore logic runs AFTER the DOM is populated.
+ sendUpdate();
+ }
+ });
+
+ // Do NOT call sendUpdate() here. The Webview JS fires "ready" once mounted,
+ // which triggers sendUpdate() at the correct time.
+ }
+
+ /**
+ * 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
+
+
+
+
+
+
+
+ `;
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/src/extension.ts b/src/extension.ts
index eaae151..3bc4f5a 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -23,6 +23,7 @@
*/
import * as vscode from "vscode";
+import { ConflictEditorProvider } from "./conflictEditorProvider";
import { MarkdownDiffProvider } from "./markdownDiff";
import {
describeComparisonSide,
@@ -1054,6 +1055,7 @@ export function activate(context: vscode.ExtensionContext) {
);
context.subscriptions.push(
+ ConflictEditorProvider.register(context),
vscode.window.onDidChangeActiveTextEditor((editor) => {
void updateRenderedDiffContext(editor);
}),
diff --git a/src/markdown/conflictParser.ts b/src/markdown/conflictParser.ts
new file mode 100644
index 0000000..c436dfb
--- /dev/null
+++ b/src/markdown/conflictParser.ts
@@ -0,0 +1,168 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2026 Rich Markdown Diff Authors
+ */
+
+export interface CommonBlock {
+ type: "common";
+ text: string;
+}
+
+export interface ConflictBlock {
+ type: "conflict";
+ id: string;
+ mine: string;
+ theirs: string;
+ base?: string;
+ mineLabel?: string;
+ theirsLabel?: string;
+ startLine: number;
+ endLine: number;
+ choice?: "mine" | "theirs" | "both";
+}
+
+export type DocBlock = CommonBlock | ConflictBlock;
+
+/**
+ * Parses markdown text containing Git conflict markers (<<<<<<<, |||||||, =======, >>>>>>>)
+ * into a sequence of Common and Conflict blocks.
+ */
+export function parseConflictBlocks(content: string): DocBlock[] {
+ const lines = content.split(/\r?\n/);
+ const blocks: DocBlock[] = [];
+
+ let currentCommonLines: string[] = [];
+ let inConflict = false;
+
+ let conflictIdCounter = 1;
+ let conflictMineLines: string[] = [];
+ let conflictBaseLines: string[] = [];
+ let conflictTheirsLines: string[] = [];
+ let mineLabel = "";
+ let conflictStartLine = 0;
+ let section: "mine" | "base" | "theirs" = "mine";
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ // Git conflict markers start with <<<<<<<, |||||||, =======, >>>>>>>
+ if (line.startsWith("<<<<<<<")) {
+ if (currentCommonLines.length > 0) {
+ blocks.push({
+ type: "common",
+ text: currentCommonLines.join("\n"),
+ });
+ currentCommonLines = [];
+ }
+ inConflict = true;
+ section = "mine";
+ conflictStartLine = i;
+ mineLabel = line.substring(7).trim() || "Current (Mine)";
+ conflictMineLines = [];
+ conflictBaseLines = [];
+ conflictTheirsLines = [];
+ continue;
+ }
+
+ if (inConflict && line.startsWith("|||||||")) {
+ section = "base";
+ continue;
+ }
+
+ if (inConflict && line.startsWith("=======")) {
+ section = "theirs";
+ continue;
+ }
+
+ if (inConflict && line.startsWith(">>>>>>>")) {
+ const theirsLabel = line.substring(7).trim() || "Incoming (Theirs)";
+ blocks.push({
+ type: "conflict",
+ id: `conflict-${conflictIdCounter++}`,
+ mine: conflictMineLines.join("\n"),
+ theirs: conflictTheirsLines.join("\n"),
+ base: conflictBaseLines.length > 0 ? conflictBaseLines.join("\n") : undefined,
+ mineLabel,
+ theirsLabel,
+ startLine: conflictStartLine,
+ endLine: i,
+ });
+ inConflict = false;
+ continue;
+ }
+
+ if (inConflict) {
+ if (section === "base") {
+ conflictBaseLines.push(line);
+ } else if (section === "theirs") {
+ conflictTheirsLines.push(line);
+ } else {
+ conflictMineLines.push(line);
+ }
+ } else {
+ currentCommonLines.push(line);
+ }
+ }
+
+ // Safely rescue unclosed conflict marker
+ if (inConflict) {
+ const unclosedLines: string[] = [
+ `<<<<<<< ${mineLabel}`,
+ ...conflictMineLines,
+ ];
+ if (conflictBaseLines.length > 0) {
+ unclosedLines.push("|||||||");
+ unclosedLines.push(...conflictBaseLines);
+ }
+ if (section === "theirs") {
+ unclosedLines.push("=======");
+ unclosedLines.push(...conflictTheirsLines);
+ }
+ currentCommonLines.push(...unclosedLines);
+ }
+
+ if (currentCommonLines.length > 0) {
+ blocks.push({
+ type: "common",
+ text: currentCommonLines.join("\n"),
+ });
+ }
+
+ return blocks;
+}
+
+/**
+ * Reconstructs document text based on conflict choices made by the user.
+ */
+export function reconstructDocument(blocks: DocBlock[]): string {
+ const resultLines: string[] = [];
+
+ for (const block of blocks) {
+ if (block.type === "common") {
+ resultLines.push(block.text);
+ } else {
+ if (block.choice === "mine") {
+ resultLines.push(block.mine);
+ } else if (block.choice === "theirs") {
+ resultLines.push(block.theirs);
+ } else if (block.choice === "both") {
+ resultLines.push(block.mine);
+ resultLines.push(block.theirs);
+ } else {
+ // Unresolved: keep conflict markers intact
+ resultLines.push(`<<<<<<< ${block.mineLabel || "HEAD"}`);
+ resultLines.push(block.mine);
+ if (block.base !== undefined) {
+ resultLines.push("|||||||");
+ resultLines.push(block.base);
+ }
+ resultLines.push("=======");
+ resultLines.push(block.theirs);
+ resultLines.push(`>>>>>>> ${block.theirsLabel || "Incoming"}`);
+ }
+ }
+ }
+
+ return resultLines.join("\n");
+}
diff --git a/src/markdown/mermaidDiff.ts b/src/markdown/mermaidDiff.ts
new file mode 100644
index 0000000..193894e
--- /dev/null
+++ b/src/markdown/mermaidDiff.ts
@@ -0,0 +1,323 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2026 Rich Markdown Diff Authors
+ */
+
+export interface MermaidDiffOptions {
+ insertedColor?: string;
+ deletedColor?: string;
+}
+
+export interface MermaidNode {
+ id: string;
+ label?: string;
+ raw: string;
+}
+
+export interface MermaidEdge {
+ from: string;
+ to: string;
+ label?: string;
+ raw: string;
+}
+
+/**
+ * Checks if the given Mermaid code is a Flowchart / Graph diagram.
+ */
+export function isFlowchartMermaid(code: string): boolean {
+ const lines = code.split(/\r?\n/);
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith("%%")) {
+ continue;
+ }
+ return /^(?:graph|flowchart)\b/i.test(trimmed);
+ }
+ return false;
+}
+
+/**
+ * Parses edge connections from a Flowchart / Graph Mermaid code block.
+ */
+export function parseMermaidEdges(code: string): MermaidEdge[] {
+ const edges: MermaidEdge[] = [];
+ const lines = code.split(/\r?\n/);
+
+ // Match arrows:
+ // 1. A -->|label| B
+ // 2. A -- label --> B or A -- "label" --> B
+ // 3. A --> B or A --- B or A ==> B or A -.- B
+ // The optional shape group (?:[(\[{](?:[^)\]}>]|"[^"]*")*[)\]}>])? after the node ID
+ // allows shaped nodes like A["Start"] --> B to be captured correctly.
+ const SHAPE_GROUP = `(?:(?:\\(\\(|\\[\\[|[([{>])(?:[^)\\]}>]|"[^"]*")*(?:\\)\\)|\\]\\]|[)\\]}>]))?`;
+ const edgeRegex = new RegExp(
+ `\\b([a-zA-Z0-9_-]+)${SHAPE_GROUP}\\s*` +
+ `(?:(?:---|-->|==>|-\\.-)\\s*(?:\\|([^|]+)\\|)?\\s*` +
+ `|--\\s*(?:["']([^"']+)["']|([a-zA-Z0-9_\\s]+))\\s*-->\\s*)` +
+ `([a-zA-Z0-9_-]+)`,
+ "g"
+ );
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ // [MERMAID-01] Skip directive lines and subgraph/end keywords to avoid false edge matches
+ if (
+ !trimmed ||
+ trimmed.startsWith("%%") ||
+ trimmed.startsWith("style") ||
+ trimmed.startsWith("linkStyle") ||
+ trimmed.startsWith("subgraph") ||
+ trimmed === "end"
+ ) {
+ continue;
+ }
+
+ // Reset lastIndex explicitly at the start of each line.
+ // The chain-notation rewind (below) adjusts lastIndex mid-line intentionally,
+ // but it must not bleed into the next line's processing.
+ edgeRegex.lastIndex = 0;
+
+ let match: RegExpExecArray | null;
+ while ((match = edgeRegex.exec(trimmed)) !== null) {
+ const from = match[1];
+ const label = match[2] || match[3] || match[4];
+ const to = match[5];
+ if (from && to && !["graph", "flowchart", "subgraph", "end"].includes(from)) {
+ edges.push({ from, to, label: label ? label.trim() : undefined, raw: match[0] });
+ // [MERMAID-01] Rewind lastIndex to allow the 'to' node to serve as the 'from' of the
+ // next edge in chain notation (e.g. A --> B --> C produces both A→B and B→C).
+ // This is safe because the rewound position is always ahead of match.index.
+ edgeRegex.lastIndex = match.index + match[0].length - to.length;
+ }
+ }
+ }
+
+ return edges;
+}
+
+/**
+ * Parses node IDs, labels, and raw node strings from a Flowchart / Graph Mermaid code block.
+ */
+export function parseMermaidNodes(code: string): Map {
+ const nodes = new Map();
+ const lines = code.split(/\r?\n/);
+
+ const edges = parseMermaidEdges(code);
+ const edgeLabelWords = new Set();
+ for (const edge of edges) {
+ if (edge.label) {
+ edge.label.split(/\s+/).forEach((w) => edgeLabelWords.add(w));
+ }
+ }
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ // [MERMAID-01] Skip directive lines. Crucially, skip `subgraph myGroup[Label]` lines
+ // to prevent the group name from being mistaken for a node definition.
+ if (
+ !trimmed ||
+ trimmed.startsWith("%%") ||
+ trimmed.startsWith("graph") ||
+ trimmed.startsWith("flowchart") ||
+ trimmed.startsWith("style") ||
+ trimmed.startsWith("linkStyle") ||
+ trimmed.startsWith("classDef") ||
+ trimmed.startsWith("class ") ||
+ trimmed.startsWith("subgraph") ||
+ trimmed === "end"
+ ) {
+ continue;
+ }
+
+ const nodeRegex = /\b([a-zA-Z0-9_-]+)(?:\(\((?:["']([^"']+)["']|([^\)]+))\)\)|\[\[(?:["']([^"']+)["']|([^\]]+))\]\]|\[(?:["']([^"']+)["']|([^\]]+))\]|\((?:["']([^"']+)["']|([^\)]+))\)|\{(?:["']([^"']+)["']|([^\}]+))\}|>([^\\]]+)\])?/g;
+
+ let match: RegExpExecArray | null;
+ while ((match = nodeRegex.exec(trimmed)) !== null) {
+ const id = match[1];
+ if (["graph", "flowchart", "subgraph", "end", "style", "linkStyle", "classDef", "class", "TD", "LR", "BT", "RL", "TB"].includes(id)) {
+ continue;
+ }
+
+ const hasExplicitShape = Boolean(match[2] || match[3] || match[4] || match[5] || match[6] || match[7] || match[8] || match[9] || match[10] || match[11] || match[12]);
+
+ if (!hasExplicitShape && edgeLabelWords.has(id)) {
+ continue;
+ }
+
+ const label = match[2] || match[3] || match[4] || match[5] || match[6] || match[7] || match[8] || match[9] || match[10] || match[11] || match[12] || id;
+ if (!nodes.has(id)) {
+ nodes.set(id, { id, label, raw: match[0] });
+ }
+ }
+ }
+
+ return nodes;
+}
+
+/**
+ * Computes semantic diff between two Mermaid diagrams (flowcharts) and injects dynamic diff styles.
+ */
+export function computeMermaidDiff(
+ oldCode: string,
+ newCode: string,
+ options: MermaidDiffOptions = {}
+): string {
+ // Base colors — defined early so empty-side paths can reuse them
+ const insFill = options.insertedColor || "#e6ffec";
+ const insStroke = "#22863a";
+ const delFill = options.deletedColor || "#ffeef0";
+ const delStroke = "#d73a49";
+ const modFill = "#fffdef";
+ const modStroke = "#b08800";
+
+ // [MERMAID-02] When one side is empty, style ALL elements on the non-empty side
+ // (all-inserted green or all-deleted red) rather than returning the code unstyled.
+ if (!oldCode.trim()) {
+ if (!isFlowchartMermaid(newCode)) {
+ return newCode;
+ }
+ const newNodes = parseMermaidNodes(newCode);
+ const newEdges = parseMermaidEdges(newCode);
+ let result = newCode.trim();
+ const styleLines = ["\n%% Dynamic Diff Styles (all inserted)"];
+ for (const id of newNodes.keys()) {
+ styleLines.push(` style ${id} fill:${insFill},stroke:${insStroke},stroke-width:2px;`);
+ }
+ newEdges.forEach((_edge, idx) => {
+ styleLines.push(` linkStyle ${idx} stroke:${insStroke},stroke-width:2px;`);
+ });
+ if (styleLines.length > 1) { result += "\n" + styleLines.join("\n"); }
+ return result;
+ }
+ if (!newCode.trim()) {
+ if (!isFlowchartMermaid(oldCode)) {
+ return oldCode;
+ }
+ const oldNodes = parseMermaidNodes(oldCode);
+ const oldEdges = parseMermaidEdges(oldCode);
+ let result = oldCode.trim();
+ const ghostLines = ["\n%% Ghost definitions for deleted elements (all deleted)"];
+ const styleLines = ["\n%% Dynamic Diff Styles (all deleted)"];
+ for (const [id, node] of oldNodes) {
+ const labelStr = node.label ? `["${node.label}"]` : "";
+ ghostLines.push(` ${id}${labelStr}`);
+ styleLines.push(
+ ` style ${id} fill:${delFill},stroke:${delStroke},stroke-width:1px,stroke-dasharray:5 5,opacity:0.75;`
+ );
+ }
+ oldEdges.forEach((_edge, idx) => {
+ styleLines.push(
+ ` linkStyle ${idx} stroke:${delStroke},stroke-width:1px,stroke-dasharray:3 3,opacity:0.75;`
+ );
+ });
+ if (ghostLines.length > 1) { result += "\n" + ghostLines.join("\n"); }
+ if (styleLines.length > 1) { result += "\n" + styleLines.join("\n"); }
+ return result;
+ }
+
+ // Guard: Only process flowcharts / graph diagrams
+ if (!isFlowchartMermaid(newCode) && !isFlowchartMermaid(oldCode)) {
+ return newCode;
+ }
+
+ const oldNodes = parseMermaidNodes(oldCode);
+ const newNodes = parseMermaidNodes(newCode);
+
+ const oldEdges = parseMermaidEdges(oldCode);
+ const newEdges = parseMermaidEdges(newCode);
+
+ const addedNodeIds: string[] = [];
+ const removedNodeIds: string[] = [];
+ const modifiedNodeIds: string[] = [];
+
+ for (const [id, newNode] of newNodes) {
+ if (!oldNodes.has(id)) {
+ addedNodeIds.push(id);
+ } else {
+ const oldNode = oldNodes.get(id)!;
+ if (oldNode.label !== newNode.label) {
+ modifiedNodeIds.push(id);
+ }
+ }
+ }
+
+ for (const id of oldNodes.keys()) {
+ if (!newNodes.has(id)) {
+ removedNodeIds.push(id);
+ }
+ }
+
+ // (Colors already defined above)
+
+ let resultMermaid = newCode.trim();
+
+ // 1. Append ghost definitions for deleted nodes & edges
+ const ghostLines: string[] = [];
+ if (removedNodeIds.length > 0) {
+ ghostLines.push("\n%% Ghost definitions for deleted elements");
+ for (const id of removedNodeIds) {
+ const oldNode = oldNodes.get(id)!;
+ const labelStr = oldNode.label ? `["${oldNode.label}"]` : "";
+ ghostLines.push(` ${id}${labelStr}`);
+ }
+ }
+
+ // Check deleted edges
+ const removedEdges: MermaidEdge[] = [];
+ for (const oldEdge of oldEdges) {
+ const isPresentInNew = newEdges.some(
+ (e) => e.from === oldEdge.from && e.to === oldEdge.to
+ );
+ if (!isPresentInNew) {
+ removedEdges.push(oldEdge);
+ ghostLines.push(` ${oldEdge.from} -.-> ${oldEdge.to}`);
+ }
+ }
+
+ if (ghostLines.length > 0) {
+ resultMermaid += "\n" + ghostLines.join("\n");
+ }
+
+ // 2. Inject Node Styles
+ const styleLines: string[] = ["\n%% Dynamic Diff Styles"];
+
+ for (const id of addedNodeIds) {
+ styleLines.push(
+ ` style ${id} fill:${insFill},stroke:${insStroke},stroke-width:2px;`
+ );
+ }
+
+ for (const id of removedNodeIds) {
+ styleLines.push(
+ ` style ${id} fill:${delFill},stroke:${delStroke},stroke-width:1px,stroke-dasharray:5 5,opacity:0.75;`
+ );
+ }
+
+ for (const id of modifiedNodeIds) {
+ styleLines.push(
+ ` style ${id} fill:${modFill},stroke:${modStroke},stroke-width:2px;`
+ );
+ }
+
+ // 3. Inject Link Styles (Edges)
+ newEdges.forEach((edge, idx) => {
+ const isAdded = !oldEdges.some((e) => e.from === edge.from && e.to === edge.to);
+ if (isAdded) {
+ styleLines.push(` linkStyle ${idx} stroke:${insStroke},stroke-width:2px;`);
+ }
+ });
+
+ removedEdges.forEach((edge, offsetIdx) => {
+ const totalIdx = newEdges.length + offsetIdx;
+ styleLines.push(
+ ` linkStyle ${totalIdx} stroke:${delStroke},stroke-width:1px,stroke-dasharray:3 3,opacity:0.75;`
+ );
+ });
+
+ if (styleLines.length > 1) {
+ resultMermaid += "\n" + styleLines.join("\n");
+ }
+
+ return resultMermaid;
+}
diff --git a/src/markdown/structuralDiff.ts b/src/markdown/structuralDiff.ts
index c97a07e..13338a4 100644
--- a/src/markdown/structuralDiff.ts
+++ b/src/markdown/structuralDiff.ts
@@ -25,6 +25,8 @@
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.
@@ -1567,6 +1569,21 @@ export function refineBlockDiffs(
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;
}
diff --git a/src/test/unit/conflictParser.test.ts b/src/test/unit/conflictParser.test.ts
new file mode 100644
index 0000000..fec9aba
--- /dev/null
+++ b/src/test/unit/conflictParser.test.ts
@@ -0,0 +1,115 @@
+import * as assert from "assert";
+import {
+ parseConflictBlocks,
+ reconstructDocument,
+} from "../../markdown/conflictParser";
+
+describe("Conflict Parser", () => {
+ it("should parse normal document without conflict markers as single common block", () => {
+ const text = "Hello World\nThis is normal text.";
+ const blocks = parseConflictBlocks(text);
+ assert.strictEqual(blocks.length, 1);
+ assert.strictEqual(blocks[0].type, "common");
+ if (blocks[0].type === "common") {
+ assert.strictEqual(blocks[0].text, text);
+ }
+ });
+
+ it("should parse document with single conflict marker into common and conflict blocks", () => {
+ const text = `Line 1
+<<<<<<< HEAD
+Mine content
+=======
+Theirs content
+>>>>>>> feature
+Line 2`;
+
+ const blocks = parseConflictBlocks(text);
+ assert.strictEqual(blocks.length, 3);
+ assert.strictEqual(blocks[0].type, "common");
+ assert.strictEqual(blocks[1].type, "conflict");
+ assert.strictEqual(blocks[2].type, "common");
+
+ if (blocks[1].type === "conflict") {
+ assert.strictEqual(blocks[1].mine, "Mine content");
+ assert.strictEqual(blocks[1].theirs, "Theirs content");
+ assert.strictEqual(blocks[1].mineLabel, "HEAD");
+ assert.strictEqual(blocks[1].theirsLabel, "feature");
+ }
+ });
+
+ it("should reconstruct document correctly when choices are made", () => {
+ const text = `Line 1
+<<<<<<< HEAD
+Mine content
+=======
+Theirs content
+>>>>>>> feature
+Line 2`;
+
+ const blocks = parseConflictBlocks(text);
+ if (blocks[1].type === "conflict") {
+ blocks[1].choice = "mine";
+ }
+
+ const reconstructedMine = reconstructDocument(blocks);
+ assert.strictEqual(reconstructedMine, "Line 1\nMine content\nLine 2");
+
+ if (blocks[1].type === "conflict") {
+ blocks[1].choice = "theirs";
+ }
+ const reconstructedTheirs = reconstructDocument(blocks);
+ assert.strictEqual(reconstructedTheirs, "Line 1\nTheirs content\nLine 2");
+ });
+
+ it("should parse conflict markers inside code blocks", () => {
+ const text = `\`\`\`typescript
+<<<<<<< HEAD
+const x = 1;
+=======
+const x = 2;
+>>>>>>> main
+\`\`\``;
+
+ const blocks = parseConflictBlocks(text);
+ assert.strictEqual(blocks.length, 3);
+ assert.strictEqual(blocks[0].type, "common");
+ assert.strictEqual(blocks[1].type, "conflict");
+ assert.strictEqual(blocks[2].type, "common");
+ if (blocks[1].type === "conflict") {
+ assert.strictEqual(blocks[1].mine, "const x = 1;");
+ assert.strictEqual(blocks[1].theirs, "const x = 2;");
+ }
+ });
+
+ it("should support diff3 conflict marker format with base section", () => {
+ const text = `<<<<<<< HEAD
+Mine content
+||||||| merged common ancestors
+Base content
+=======
+Theirs content
+>>>>>>> branch`;
+
+ const blocks = parseConflictBlocks(text);
+ assert.strictEqual(blocks.length, 1);
+ assert.strictEqual(blocks[0].type, "conflict");
+ if (blocks[0].type === "conflict") {
+ assert.strictEqual(blocks[0].mine, "Mine content");
+ assert.strictEqual(blocks[0].base, "Base content");
+ assert.strictEqual(blocks[0].theirs, "Theirs content");
+ }
+ });
+
+ it("should safely rescue unclosed conflict marker at EOF", () => {
+ const text = `Start
+<<<<<<< HEAD
+Unclosed mine content`;
+
+ const blocks = parseConflictBlocks(text);
+ assert.strictEqual(blocks.length, 2);
+ assert.strictEqual(blocks[0].type, "common");
+ assert.strictEqual(blocks[1].type, "common");
+ assert.ok(blocks[1].text.includes("Unclosed mine content"));
+ });
+});
diff --git a/src/test/unit/mermaidDiff.test.ts b/src/test/unit/mermaidDiff.test.ts
new file mode 100644
index 0000000..c2ae1f4
--- /dev/null
+++ b/src/test/unit/mermaidDiff.test.ts
@@ -0,0 +1,181 @@
+import * as assert from "assert";
+import {
+ computeMermaidDiff,
+ parseMermaidNodes,
+ parseMermaidEdges,
+} from "../../markdown/mermaidDiff";
+
+describe("Mermaid Semantic Diff", () => {
+ it("should parse nodes correctly", () => {
+ const code = `
+ graph TD
+ A["Start"] --> B(Process)
+ C((End))
+ `;
+ const nodes = parseMermaidNodes(code);
+ assert.strictEqual(nodes.size, 3);
+ assert.strictEqual(nodes.get("A")?.label, "Start");
+ assert.strictEqual(nodes.get("B")?.label, "Process");
+ assert.strictEqual(nodes.get("C")?.label, "End");
+ });
+
+ it("should parse edges correctly", () => {
+ const code = `
+ graph TD
+ A --> B
+ B --> C
+ `;
+ const edges = parseMermaidEdges(code);
+ assert.strictEqual(edges.length, 2);
+ assert.strictEqual(edges[0].from, "A");
+ assert.strictEqual(edges[0].to, "B");
+ assert.strictEqual(edges[1].from, "B");
+ assert.strictEqual(edges[1].to, "C");
+ });
+
+ it("should inject inserted node styles when node is added", () => {
+ const oldCode = `
+ graph TD
+ A --> B
+ `;
+ const newCode = `
+ graph TD
+ A --> B
+ B --> C["New Node"]
+ `;
+ const diff = computeMermaidDiff(oldCode, newCode);
+ assert.ok(diff.includes("style C fill:#e6ffec"), "Should style node C as inserted");
+ assert.ok(diff.includes("linkStyle 1 stroke:#22863a"), "Should style edge 1 as inserted link");
+ });
+
+ it("should inject ghost definition and deleted node style when node is removed", () => {
+ const oldCode = `
+ graph TD
+ A --> B
+ B --> C["Old Node"]
+ `;
+ const newCode = `
+ graph TD
+ A --> B
+ `;
+ const diff = computeMermaidDiff(oldCode, newCode);
+ assert.ok(diff.includes("C[\"Old Node\"]"), "Should include ghost node definition for C");
+ assert.ok(diff.includes("style C fill:#ffeef0"), "Should style node C as deleted");
+ assert.ok(diff.includes("linkStyle 1 stroke:#d73a49"), "Should style deleted link");
+ });
+
+ it("should highlight modified node when node label changes", () => {
+ const oldCode = `
+ graph TD
+ A["Original Step"] --> B
+ `;
+ const newCode = `
+ graph TD
+ A["Updated Step"] --> B
+ `;
+ const diff = computeMermaidDiff(oldCode, newCode);
+ assert.ok(diff.includes("style A fill:#fffdef"), "Should style node A as modified");
+ });
+
+ it("should not treat edge labels as node identifiers", () => {
+ const code = `
+ graph TD
+ A --> B
+ B -- Yes --> C
+ B -- No --> D
+ `;
+ const nodes = parseMermaidNodes(code);
+ assert.strictEqual(nodes.size, 4, "Should only detect A, B, C, D as nodes");
+ assert.ok(nodes.has("A"));
+ assert.ok(nodes.has("B"));
+ assert.ok(nodes.has("C"));
+ assert.ok(nodes.has("D"));
+ assert.ok(!nodes.has("Yes"), "Yes should not be treated as a node");
+ assert.ok(!nodes.has("No"), "No should not be treated as a node");
+ });
+
+ it("should skip style injection for non-flowchart diagrams (e.g. sequenceDiagram)", () => {
+ const oldCode = `
+ sequenceDiagram
+ Alice->>Bob: Hello Bob
+ `;
+ const newCode = `
+ sequenceDiagram
+ Alice->>Bob: Hello Bob
+ Bob-->>Alice: Hi Alice
+ `;
+ const diff = computeMermaidDiff(oldCode, newCode);
+ assert.strictEqual(diff.trim(), newCode.trim(), "Should return unmodified new code for sequenceDiagram");
+ });
+
+ // --- MERMAID-01 regression tests ---
+
+ it("[MERMAID-01] should NOT register subgraph group name as a node", () => {
+ // 'myGroup' appears on the `subgraph myGroup[...]` line and must not be
+ // treated as a diagram node, even though it has an explicit bracket shape.
+ const code = `
+ graph TD
+ subgraph myGroup[My Group]
+ A --> B
+ end
+ `;
+ const nodes = parseMermaidNodes(code);
+ assert.ok(!nodes.has("myGroup"), "subgraph group ID must not be registered as a node");
+ assert.ok(nodes.has("A"), "A should be registered as a node");
+ assert.ok(nodes.has("B"), "B should be registered as a node");
+ });
+
+ it("[MERMAID-01] should parse all edges in chain notation (A --> B --> C)", () => {
+ // A single line `A --> B --> C` encodes two edges: A→B and B→C.
+ // The edge parser must rewind its index so the shared node B is captured
+ // as both the 'to' of the first edge and the 'from' of the second.
+ const code = `
+ graph LR
+ A --> B --> C
+ `;
+ const edges = parseMermaidEdges(code);
+ assert.strictEqual(edges.length, 2, "Chain A-->B-->C should produce 2 edges");
+ assert.ok(
+ edges.some((e) => e.from === "A" && e.to === "B"),
+ "Edge A→B must be present"
+ );
+ assert.ok(
+ edges.some((e) => e.from === "B" && e.to === "C"),
+ "Edge B→C must be present"
+ );
+ });
+
+ // --- MERMAID-02 regression tests ---
+
+ it("[MERMAID-02] should style all nodes/edges as inserted when oldCode is empty", () => {
+ // When the Mermaid block is brand new (no previous version), all elements
+ // should be highlighted green rather than returned unstyled.
+ const newCode = `
+ graph TD
+ A["Start"] --> B
+ B --> C["End"]
+ `;
+ const diff = computeMermaidDiff("", newCode);
+ assert.ok(diff.includes("style A fill:#e6ffec"), "Node A should be styled as inserted");
+ assert.ok(diff.includes("style B fill:#e6ffec"), "Node B should be styled as inserted");
+ assert.ok(diff.includes("style C fill:#e6ffec"), "Node C should be styled as inserted");
+ assert.ok(diff.includes("linkStyle 0 stroke:#22863a"), "Edge 0 should be styled as inserted");
+ assert.ok(diff.includes("linkStyle 1 stroke:#22863a"), "Edge 1 should be styled as inserted");
+ });
+
+ it("[MERMAID-02] should style all nodes/edges as deleted when newCode is empty", () => {
+ // When the Mermaid block is entirely removed, all elements from the old
+ // diagram should be highlighted red with ghost definitions.
+ const oldCode = `
+ graph TD
+ A["Start"] --> B
+ B --> C["End"]
+ `;
+ const diff = computeMermaidDiff(oldCode, "");
+ assert.ok(diff.includes("style A fill:#ffeef0"), "Node A should be styled as deleted");
+ assert.ok(diff.includes("style B fill:#ffeef0"), "Node B should be styled as deleted");
+ assert.ok(diff.includes("style C fill:#ffeef0"), "Node C should be styled as deleted");
+ 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");
+ });
+});
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 3113e1cae73b5e8a5d63f892044aad4584eae640..2e53feaa51626960f898987ff4f630ff5d740ee2 100644
GIT binary patch
literal 580678
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