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∑ni=2n(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 += `