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