mirror of
https://github.com/CorentinTh/it-tools.git
synced 2026-06-28 04:02:37 +00:00
Merge f0430b6116 into d505845f91
This commit is contained in:
commit
b79aa0aa92
18 changed files with 353 additions and 6 deletions
1
components.d.ts
vendored
1
components.d.ts
vendored
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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: >-
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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: >-
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -70,3 +70,7 @@ tools:
|
|||
measurement: Вимірювання
|
||||
text: Текст
|
||||
data: Дані
|
||||
|
||||
markdown-diff:
|
||||
title: Markdown diff
|
||||
description: Compare two Markdown documents and see the differences between them.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)并显示其内容。
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
13
src/tools/markdown-diff/index.ts
Normal file
13
src/tools/markdown-diff/index.ts
Normal 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'),
|
||||
});
|
||||
12
src/tools/markdown-diff/markdown-diff.constants.test.ts
Normal file
12
src/tools/markdown-diff/markdown-diff.constants.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
48
src/tools/markdown-diff/markdown-diff.constants.ts
Normal file
48
src/tools/markdown-diff/markdown-diff.constants.ts
Normal 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;
|
||||
\`\`\`
|
||||
`;
|
||||
33
src/tools/markdown-diff/markdown-diff.e2e.spec.ts
Normal file
33
src/tools/markdown-diff/markdown-diff.e2e.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
142
src/tools/markdown-diff/markdown-diff.vue
Normal file
142
src/tools/markdown-diff/markdown-diff.vue
Normal 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>
|
||||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue