This commit is contained in:
nimza 2026-05-22 11:37:08 +00:00 committed by GitHub
commit b79aa0aa92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 353 additions and 6 deletions

1
components.d.ts vendored
View file

@ -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']

View file

@ -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: >-

View file

@ -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.

View file

@ -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.

View file

@ -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: >-

View file

@ -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.

View file

@ -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.

View file

@ -70,3 +70,7 @@ tools:
measurement: Вимірювання
text: Текст
data: Дані
markdown-diff:
title: Markdown diff
description: Compare two Markdown documents and see the differences between them.

View file

@ -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).

View file

@ -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 Tokenjwt并显示其内容。

View file

@ -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,
],
},
{

View file

@ -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'),
});

View file

@ -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);
});
});

View file

@ -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;
\`\`\`
`;

View file

@ -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();
});
});

View file

@ -0,0 +1,142 @@
<template>
<div class="markdown-diff-tool">
<div flex justify-center mb-4>
<c-buttons-select
v-model:value="mode"
:options="modeOptions"
size="small"
/>
</div>
<c-card v-if="mode === 'code'" w-full important:flex-1 important:pa-0>
<c-diff-editor
v-model:original="sourceMarkdown"
v-model:modified="targetMarkdown"
test-id="markdown-diff-editor"
language="markdown"
height="clamp(620px, 72vh, 820px)"
/>
</c-card>
<c-card v-else data-test-id="markdown-preview" w-full>
<div class="markdown-preview-grid">
<section>
<h3>Source Markdown</h3>
<div class="markdown-preview-pane" data-test-id="source-markdown-preview">
<c-markdown :markdown="sourceMarkdown" />
</div>
</section>
<section>
<h3>Modified Markdown</h3>
<div class="markdown-preview-pane" data-test-id="modified-markdown-preview">
<c-markdown :markdown="targetMarkdown" />
</div>
</section>
</div>
</c-card>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { modifiedMarkdown, originalMarkdown } from './markdown-diff.constants';
type MarkdownDiffMode = 'code' | 'preview';
const mode = ref<MarkdownDiffMode>('code');
const sourceMarkdown = ref(originalMarkdown);
const targetMarkdown = ref(modifiedMarkdown);
const modeOptions: Array<{ label: string; value: MarkdownDiffMode }> = [
{ label: 'Code', value: 'code' },
{ label: 'Preview', value: 'preview' },
];
</script>
<style lang="less" scoped>
.markdown-preview-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
section {
min-width: 0;
}
h3 {
margin: 0 0 12px;
font-size: 18px;
font-weight: 600;
}
}
.markdown-diff-tool {
width: 100%;
max-width: 1200px;
flex: 1 1 1200px !important;
}
.markdown-preview-pane {
min-height: clamp(620px, 72vh, 820px);
padding: 18px;
border: 1px solid #d8dee8;
border-radius: 8px;
overflow: auto;
}
:deep(.markdown-preview-pane) {
line-height: 1.65;
}
:deep(.markdown-preview-pane > div > *:first-child) {
margin-top: 0;
}
:deep(.markdown-preview-pane > div > *:last-child) {
margin-bottom: 0;
}
:deep(.markdown-preview-pane table) {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
:deep(.markdown-preview-pane th),
:deep(.markdown-preview-pane td) {
padding: 10px 12px;
border: 1px solid #cfd6e2;
text-align: left;
}
:deep(.markdown-preview-pane th) {
font-weight: 600;
background: rgb(248 250 252);
}
:deep(.markdown-preview-pane code) {
padding: 2px 5px;
border-radius: 4px;
background: rgb(241 245 249);
}
:deep(.markdown-preview-pane pre) {
padding: 14px;
border-radius: 6px;
overflow: auto;
background: rgb(15 23 42);
}
:deep(.markdown-preview-pane pre code) {
padding: 0;
color: rgb(226 232 240);
background: transparent;
}
@media (max-width: 900px) {
.markdown-preview-grid {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -51,7 +51,7 @@ function selectOption(option: CButtonSelectOption<T>) {
:tooltip="option.tooltip"
>
<c-button
:test-id="option.value"
:data-test-id="option.value"
:size="size"
:type="option.value === value ? 'primary' : 'default'"
@click="selectOption(option)"

View file

@ -2,11 +2,35 @@
import * as monaco from 'monaco-editor';
import { useStyleStore } from '@/stores/style.store';
const props = withDefaults(defineProps<{ options?: monaco.editor.IDiffEditorOptions }>(), { 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<HTMLElement | null>(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();
});
</script>
<template>
<div ref="editorContainer" h-600px />
<div ref="editorContainer" :data-test-id="testId" :style="{ height }" />
</template>