diff --git a/components.d.ts b/components.d.ts
index 3e65c3cc..1d51153b 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -121,6 +121,7 @@ declare module '@vue/runtime-core' {
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default']
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
+ MarkdownDiff: typeof import('./src/tools/markdown-diff/markdown-diff.vue')['default']
MarkdownToHtml: typeof import('./src/tools/markdown-to-html/markdown-to-html.vue')['default']
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
diff --git a/locales/de.yml b/locales/de.yml
index 0ccdb71d..954ad832 100644
--- a/locales/de.yml
+++ b/locales/de.yml
@@ -333,6 +333,9 @@ tools:
json-diff:
title: JSON-Unterschied
description: Vergleiche zwei JSON-Objekte und erhalte die Unterschiede zwischen ihnen.
+ markdown-diff:
+ title: Markdown-Unterschied
+ description: Vergleiche zwei Markdown-Dokumente und sieh die Unterschiede zwischen ihnen.
jwt-parser:
title: JWT-Parser
description: >-
diff --git a/locales/en.yml b/locales/en.yml
index d03d80d3..2cb385a8 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -289,6 +289,10 @@ tools:
title: JSON diff
description: Compare two JSON objects and get the differences between them.
+ markdown-diff:
+ title: Markdown diff
+ description: Compare two Markdown documents and see the differences between them.
+
jwt-parser:
title: JWT parser
description: Parse and decode your JSON Web Token (jwt) and display its content.
diff --git a/locales/es.yml b/locales/es.yml
index 14e2bb66..7f082cc6 100644
--- a/locales/es.yml
+++ b/locales/es.yml
@@ -70,3 +70,7 @@ tools:
measurement: Measurement
text: Text
data: Data
+
+ markdown-diff:
+ title: Markdown diff
+ description: Compare two Markdown documents and see the differences between them.
diff --git a/locales/fr.yml b/locales/fr.yml
index 86bb47d6..a63af6ff 100644
--- a/locales/fr.yml
+++ b/locales/fr.yml
@@ -65,6 +65,10 @@ tools:
text: Texte
data: Données
+ markdown-diff:
+ title: Markdown diff
+ description: Compare two Markdown documents and see the differences between them.
+
token-generator:
title: Générateur de token
description: >-
diff --git a/locales/no.yml b/locales/no.yml
index ba4f9e47..60c0e8ae 100644
--- a/locales/no.yml
+++ b/locales/no.yml
@@ -289,6 +289,10 @@ tools:
title: JSON diff
description: Sammenlign to JSON objekter og finn forskjellene mellom dem.
+ markdown-diff:
+ title: Markdown diff
+ description: Sammenlign to Markdown-dokumenter og vis forskjellene mellom dem.
+
jwt-parser:
title: JWT parser
description: Parse og dekode et JSON Web Token (jwt) og vis innholdet.
diff --git a/locales/pt.yml b/locales/pt.yml
index 5845eb2f..de217797 100644
--- a/locales/pt.yml
+++ b/locales/pt.yml
@@ -70,3 +70,7 @@ tools:
measurement: 'Medidas'
text: 'Texto'
data: 'Dados'
+
+ markdown-diff:
+ title: Markdown diff
+ description: Compare two Markdown documents and see the differences between them.
diff --git a/locales/uk.yml b/locales/uk.yml
index b0086226..5a5b60cc 100644
--- a/locales/uk.yml
+++ b/locales/uk.yml
@@ -70,3 +70,7 @@ tools:
measurement: Вимірювання
text: Текст
data: Дані
+
+ markdown-diff:
+ title: Markdown diff
+ description: Compare two Markdown documents and see the differences between them.
diff --git a/locales/vi.yml b/locales/vi.yml
index 59514cd7..e113c448 100644
--- a/locales/vi.yml
+++ b/locales/vi.yml
@@ -281,6 +281,10 @@ tools:
title: So sánh JSON
description: So sánh hai đối tượng JSON và lấy ra sự khác biệt giữa chúng.
+ markdown-diff:
+ title: Markdown diff
+ description: Compare two Markdown documents and see the differences between them.
+
jwt-parser:
title: Giải mã JWT
description: Giải mã và hiển thị nội dung của JSON Web Token (jwt).
diff --git a/locales/zh.yml b/locales/zh.yml
index 97968eb5..d3e3b72b 100644
--- a/locales/zh.yml
+++ b/locales/zh.yml
@@ -285,6 +285,9 @@ tools:
json-diff:
title: JSON 差异比较
description: 比较两个JSON对象并获得它们之间的差异。
+ markdown-diff:
+ title: Markdown diff
+ description: Compare two Markdown documents and see the differences between them.
jwt-parser:
title: JWT 解析器
description: 解析和解码JSON Web Token(jwt)并显示其内容。
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 388cfaf4..9cab99ec 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,6 +1,7 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
+import { tool as markdownDiff } from './markdown-diff';
import { tool as emailNormalizer } from './email-normalizer';
import { tool as asciiTextDrawer } from './ascii-text-drawer';
@@ -116,6 +117,7 @@ export const toolsByCategory: ToolCategory[] = [
xmlToJson,
jsonToXml,
markdownToHtml,
+ markdownDiff,
],
},
{
diff --git a/src/tools/markdown-diff/index.ts b/src/tools/markdown-diff/index.ts
new file mode 100644
index 00000000..48e7054d
--- /dev/null
+++ b/src/tools/markdown-diff/index.ts
@@ -0,0 +1,13 @@
+import { Markdown } from '@vicons/tabler';
+import { defineTool } from '../tool';
+import { translate } from '@/plugins/i18n.plugin';
+
+export const tool = defineTool({
+ name: translate('tools.markdown-diff.title'),
+ path: '/markdown-diff',
+ description: translate('tools.markdown-diff.description'),
+ keywords: ['markdown', 'diff', 'compare', 'difference', 'markdown diff', 'md', 'text'],
+ component: () => import('./markdown-diff.vue'),
+ icon: Markdown,
+ createdAt: new Date('2026-05-21'),
+});
diff --git a/src/tools/markdown-diff/markdown-diff.constants.test.ts b/src/tools/markdown-diff/markdown-diff.constants.test.ts
new file mode 100644
index 00000000..689692f4
--- /dev/null
+++ b/src/tools/markdown-diff/markdown-diff.constants.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, it } from 'vitest';
+import { modifiedMarkdown, originalMarkdown } from './markdown-diff.constants';
+
+describe('markdown-diff constants', () => {
+ it('provides different default Markdown documents', () => {
+ expect(originalMarkdown).toContain('# Release notes');
+ expect(originalMarkdown).toContain('| user | host | plugin |');
+ expect(modifiedMarkdown).toContain('# Release notes');
+ expect(modifiedMarkdown).toContain('```sql');
+ expect(modifiedMarkdown).not.toBe(originalMarkdown);
+ });
+});
diff --git a/src/tools/markdown-diff/markdown-diff.constants.ts b/src/tools/markdown-diff/markdown-diff.constants.ts
new file mode 100644
index 00000000..c0823ad0
--- /dev/null
+++ b/src/tools/markdown-diff/markdown-diff.constants.ts
@@ -0,0 +1,48 @@
+export const originalMarkdown = `# Release notes
+
+## Added
+
+- JSON export for reports
+- Keyboard shortcuts for navigation
+- Markdown table previews
+
+## Fixed
+
+- Preserve whitespace in code blocks
+
+## Plugin support
+
+| user | host | plugin |
+| --- | --- | --- |
+| mysql.infoschema | localhost | caching_sha2_password |
+| mysql.session | localhost | caching_sha2_password |
+| mysql.sys | localhost | caching_sha2_password |
+
+\`inline code\` stays readable in preview mode.
+`;
+
+export const modifiedMarkdown = `# Release notes
+
+## Added
+
+- JSON and CSV export for reports
+- Keyboard shortcuts for navigation
+- Dark mode support for charts
+- Markdown table previews
+
+## Fixed
+
+- Preserve whitespace in fenced code blocks
+
+## Plugin support
+
+| user | host | plugin |
+| --- | --- | --- |
+| mysql.session | localhost | caching_sha2_password |
+| mysql.sys | localhost | caching_sha2_password |
+
+\`\`\`sql
+select user, host, plugin
+from mysql.user;
+\`\`\`
+`;
diff --git a/src/tools/markdown-diff/markdown-diff.e2e.spec.ts b/src/tools/markdown-diff/markdown-diff.e2e.spec.ts
new file mode 100644
index 00000000..60b1c8e0
--- /dev/null
+++ b/src/tools/markdown-diff/markdown-diff.e2e.spec.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test';
+
+test.describe('Tool - Markdown diff', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/markdown-diff');
+ });
+
+ test('Has correct title', async ({ page }) => {
+ await expect(page).toHaveTitle('Markdown diff - IT Tools', { timeout: 15000 });
+ });
+
+ test('Diff editor is visible with two editable panes', async ({ page }) => {
+ await expect(page.getByTestId('markdown-diff-editor')).toBeVisible();
+
+ await expect(page.locator('.monaco-diff-editor')).toBeVisible();
+ await expect(page.locator('.monaco-editor textarea.inputarea')).toHaveCount(2);
+ });
+
+ test('Preview mode shows rendered source and modified Markdown', async ({ page }) => {
+ await page.getByTestId('preview').click();
+
+ await expect(page.getByTestId('markdown-preview')).toBeVisible();
+ await expect(page.getByTestId('source-markdown-preview')).toBeVisible();
+ await expect(page.getByTestId('modified-markdown-preview')).toBeVisible();
+ await expect(page.getByTestId('source-markdown-preview').locator('table')).toBeVisible();
+ await expect(page.getByTestId('modified-markdown-preview')).toContainText('caching_sha2_password');
+
+ await page.getByTestId('code').click();
+
+ await expect(page.getByTestId('markdown-diff-editor')).toBeVisible();
+ await expect(page.locator('.monaco-diff-editor')).toBeVisible();
+ });
+});
diff --git a/src/tools/markdown-diff/markdown-diff.vue b/src/tools/markdown-diff/markdown-diff.vue
new file mode 100644
index 00000000..64823649
--- /dev/null
+++ b/src/tools/markdown-diff/markdown-diff.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
diff --git a/src/ui/c-buttons-select/c-buttons-select.vue b/src/ui/c-buttons-select/c-buttons-select.vue
index a1ffa428..704e816b 100644
--- a/src/ui/c-buttons-select/c-buttons-select.vue
+++ b/src/ui/c-buttons-select/c-buttons-select.vue
@@ -51,7 +51,7 @@ function selectOption(option: CButtonSelectOption) {
:tooltip="option.tooltip"
>
(), { options: () => ({}) });
-const { options } = toRefs(props);
+const props = withDefaults(
+ defineProps<{
+ options?: monaco.editor.IDiffEditorOptions
+ original?: string
+ modified?: string
+ language?: string
+ height?: string
+ testId?: string
+ }>(),
+ {
+ options: () => ({}),
+ original: 'original text',
+ modified: 'modified text',
+ language: 'txt',
+ height: '600px',
+ testId: undefined,
+ },
+);
+const emit = defineEmits<{
+ 'update:original': [value: string]
+ 'update:modified': [value: string]
+}>();
+const { options, original, modified, language, height, testId } = toRefs(props);
const editorContainer = ref(null);
let editor: monaco.editor.IStandaloneDiffEditor | null = null;
+let originalModel: monaco.editor.ITextModel | null = null;
+let modifiedModel: monaco.editor.ITextModel | null = null;
+let modelListeners: monaco.IDisposable[] = [];
monaco.editor.defineTheme('it-tools-dark', {
base: 'vs-dark',
@@ -40,6 +64,28 @@ watch(
{ immediate: true, deep: true },
);
+watch(original, (value) => {
+ if (originalModel && originalModel.getValue() !== value) {
+ originalModel.setValue(value);
+ }
+});
+
+watch(modified, (value) => {
+ if (modifiedModel && modifiedModel.getValue() !== value) {
+ modifiedModel.setValue(value);
+ }
+});
+
+watch(language, (value) => {
+ if (originalModel) {
+ monaco.editor.setModelLanguage(originalModel, value);
+ }
+
+ if (modifiedModel) {
+ monaco.editor.setModelLanguage(modifiedModel, value);
+ }
+});
+
useResizeObserver(editorContainer, () => {
editor?.layout();
});
@@ -54,15 +100,31 @@ onMounted(() => {
minimap: {
enabled: false,
},
+ ...options.value,
});
+ originalModel = monaco.editor.createModel(original.value, language.value);
+ modifiedModel = monaco.editor.createModel(modified.value, language.value);
+
editor.setModel({
- original: monaco.editor.createModel('original text', 'txt'),
- modified: monaco.editor.createModel('modified text', 'txt'),
+ original: originalModel,
+ modified: modifiedModel,
});
+
+ modelListeners = [
+ originalModel.onDidChangeContent(() => emit('update:original', originalModel?.getValue() ?? '')),
+ modifiedModel.onDidChangeContent(() => emit('update:modified', modifiedModel?.getValue() ?? '')),
+ ];
+});
+
+onBeforeUnmount(() => {
+ modelListeners.forEach(listener => listener.dispose());
+ editor?.dispose();
+ originalModel?.dispose();
+ modifiedModel?.dispose();
});
-
+