mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-09 07:35:01 -04:00
feat(json-diff): add new tool to get the diff of two given JSONs
This commit is contained in:
parent
7d7cc99866
commit
80af4a3eea
9 changed files with 559 additions and 1 deletions
|
@ -56,6 +56,7 @@
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
|
"jsondiffpatch-rc": "^0.4.2",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mathjs": "^10.6.4",
|
"mathjs": "^10.6.4",
|
||||||
|
@ -68,6 +69,7 @@
|
||||||
"plausible-tracker": "^0.3.8",
|
"plausible-tracker": "^0.3.8",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"randombytes": "^2.1.0",
|
"randombytes": "^2.1.0",
|
||||||
|
"sanitize-html": "^2.10.0",
|
||||||
"sql-formatter": "^8.2.0",
|
"sql-formatter": "^8.2.0",
|
||||||
"ts-pattern": "^4.2.2",
|
"ts-pattern": "^4.2.2",
|
||||||
"ua-parser-js": "^1.0.35",
|
"ua-parser-js": "^1.0.35",
|
||||||
|
|
77
pnpm-lock.yaml
generated
77
pnpm-lock.yaml
generated
|
@ -70,6 +70,9 @@ dependencies:
|
||||||
json5:
|
json5:
|
||||||
specifier: ^2.2.3
|
specifier: ^2.2.3
|
||||||
version: 2.2.3
|
version: 2.2.3
|
||||||
|
jsondiffpatch-rc:
|
||||||
|
specifier: ^0.4.2
|
||||||
|
version: 0.4.2
|
||||||
jwt-decode:
|
jwt-decode:
|
||||||
specifier: ^3.1.2
|
specifier: ^3.1.2
|
||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
|
@ -106,6 +109,9 @@ dependencies:
|
||||||
randombytes:
|
randombytes:
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
|
sanitize-html:
|
||||||
|
specifier: ^2.10.0
|
||||||
|
version: 2.10.0
|
||||||
sql-formatter:
|
sql-formatter:
|
||||||
specifier: ^8.2.0
|
specifier: ^8.2.0
|
||||||
version: 8.2.0
|
version: 8.2.0
|
||||||
|
@ -4039,6 +4045,10 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/diff-match-patch@1.0.5:
|
||||||
|
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dijkstrajs@1.0.2:
|
/dijkstrajs@1.0.2:
|
||||||
resolution: {integrity: sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==}
|
resolution: {integrity: sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -4076,9 +4086,16 @@ packages:
|
||||||
entities: 2.2.0
|
entities: 2.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/dom-serializer@2.0.0:
|
||||||
|
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
entities: 4.4.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/domelementtype@2.3.0:
|
/domelementtype@2.3.0:
|
||||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/domexception@4.0.0:
|
/domexception@4.0.0:
|
||||||
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
|
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
|
||||||
|
@ -4094,6 +4111,13 @@ packages:
|
||||||
domelementtype: 2.3.0
|
domelementtype: 2.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/domhandler@5.0.3:
|
||||||
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/domutils@2.8.0:
|
/domutils@2.8.0:
|
||||||
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
|
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4102,6 +4126,14 @@ packages:
|
||||||
domhandler: 4.3.1
|
domhandler: 4.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/domutils@3.0.1:
|
||||||
|
resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==}
|
||||||
|
dependencies:
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dot-case@3.0.4:
|
/dot-case@3.0.4:
|
||||||
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4184,6 +4216,11 @@ packages:
|
||||||
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
|
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
/entities@4.4.0:
|
||||||
|
resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/errno@0.1.8:
|
/errno@0.1.8:
|
||||||
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
|
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -5349,6 +5386,15 @@ packages:
|
||||||
entities: 3.0.1
|
entities: 3.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/htmlparser2@8.0.2:
|
||||||
|
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.0.1
|
||||||
|
entities: 4.4.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/http-proxy-agent@4.0.1:
|
/http-proxy-agent@4.0.1:
|
||||||
resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
|
resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -5611,6 +5657,11 @@ packages:
|
||||||
isobject: 3.0.1
|
isobject: 3.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/is-plain-object@5.0.0:
|
||||||
|
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/is-potential-custom-element-name@1.0.1:
|
/is-potential-custom-element-name@1.0.1:
|
||||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -5924,6 +5975,15 @@ packages:
|
||||||
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/jsondiffpatch-rc@0.4.2:
|
||||||
|
resolution: {integrity: sha512-Y1qBHcinsSX6E24KvYEAzybrmhnyy/eg2uhablTW76oKrdY0nYeWoXRlMCvTXG0Nv/zlzGwAfb3mxg1JzitLug==}
|
||||||
|
engines: {node: '>=8.17.0'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
diff-match-patch: 1.0.5
|
||||||
|
dev: false
|
||||||
|
bundledDependencies: []
|
||||||
|
|
||||||
/jsonfile@6.1.0:
|
/jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6698,6 +6758,10 @@ packages:
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/parse-srcset@1.0.2:
|
||||||
|
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/parse5@6.0.1:
|
/parse5@6.0.1:
|
||||||
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
|
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -7422,6 +7486,17 @@ packages:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/sanitize-html@2.10.0:
|
||||||
|
resolution: {integrity: sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ==}
|
||||||
|
dependencies:
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
escape-string-regexp: 4.0.0
|
||||||
|
htmlparser2: 8.0.2
|
||||||
|
is-plain-object: 5.0.0
|
||||||
|
parse-srcset: 1.0.2
|
||||||
|
postcss: 8.4.21
|
||||||
|
dev: false
|
||||||
|
|
||||||
/sax@1.2.4:
|
/sax@1.2.4:
|
||||||
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
|
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { tool as base64FileConverter } from './base64-file-converter';
|
import { tool as base64FileConverter } from './base64-file-converter';
|
||||||
import { tool as base64StringConverter } from './base64-string-converter';
|
import { tool as base64StringConverter } from './base64-string-converter';
|
||||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||||
|
import { tool as jsonDiff } from './json-diff';
|
||||||
import { tool as yamlToJson } from './yaml-to-json-converter';
|
import { tool as yamlToJson } from './yaml-to-json-converter';
|
||||||
import { tool as jsonToYaml } from './json-to-yaml-converter';
|
import { tool as jsonToYaml } from './json-to-yaml-converter';
|
||||||
import { tool as ipv6UlaGenerator } from './ipv6-ula-generator';
|
import { tool as ipv6UlaGenerator } from './ipv6-ula-generator';
|
||||||
|
@ -102,6 +103,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||||
crontabGenerator,
|
crontabGenerator,
|
||||||
jsonViewer,
|
jsonViewer,
|
||||||
jsonMinify,
|
jsonMinify,
|
||||||
|
jsonDiff,
|
||||||
sqlPrettify,
|
sqlPrettify,
|
||||||
chmodCalculator,
|
chmodCalculator,
|
||||||
dockerRunToDockerComposeConverter,
|
dockerRunToDockerComposeConverter,
|
||||||
|
|
12
src/tools/json-diff/index.ts
Normal file
12
src/tools/json-diff/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { ArrowsShuffle } from '@vicons/tabler';
|
||||||
|
import { defineTool } from '../tool';
|
||||||
|
|
||||||
|
export const tool = defineTool({
|
||||||
|
name: 'JSON diff',
|
||||||
|
path: '/json-diff',
|
||||||
|
description: 'Compares two given JSONs and build a visual comparison of them',
|
||||||
|
keywords: ['json', 'diff', 'visual'],
|
||||||
|
component: () => import('./json-diff.vue'),
|
||||||
|
icon: ArrowsShuffle,
|
||||||
|
createdAt: new Date('2023-04-12'),
|
||||||
|
});
|
70
src/tools/json-diff/json-diff-result.vue
Normal file
70
src/tools/json-diff/json-diff-result.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<div style="flex: 0 0 100%">
|
||||||
|
<n-space style="margin: 0 auto; max-width: 600px" justify="center">
|
||||||
|
<n-form-item label="Show unchanged :" label-placement="left" label-width="160">
|
||||||
|
<n-switch v-model:value="showUnchanged" :disabled="!validInput" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex: 0 0 100%">
|
||||||
|
<n-space style="margin: 0 auto; max-width: 600px" justify="center">
|
||||||
|
<n-card title="Diff result" data-test-id="result">
|
||||||
|
<div ref="result">
|
||||||
|
<SanitizedHtml :html="diffResult" />
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, toRefs } from 'vue';
|
||||||
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
|
import JSON5 from 'json5';
|
||||||
|
import { DiffPatcher, formatters } from 'jsondiffpatch-rc';
|
||||||
|
import SanitizedHtml from './sanitized-html.vue';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
const result = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
showHideUnchanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ left: string; right: string }>(), { left: '', right: '' });
|
||||||
|
const { left, right } = toRefs(props);
|
||||||
|
const showUnchanged = ref(true);
|
||||||
|
|
||||||
|
const leftJson = computed(() => withDefaultOnError(() => JSON5.parse(left.value), ''));
|
||||||
|
const rightJson = computed(() => withDefaultOnError(() => JSON5.parse(right.value), ''));
|
||||||
|
const diffResult = computed(() => {
|
||||||
|
if (!validInput.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffPatcher = new DiffPatcher({
|
||||||
|
objectHash: function (obj, index) {
|
||||||
|
if (typeof obj._id !== 'undefined') {
|
||||||
|
return obj._id;
|
||||||
|
}
|
||||||
|
if (typeof obj.id !== 'undefined') {
|
||||||
|
return obj.id;
|
||||||
|
}
|
||||||
|
return '$$index:' + index;
|
||||||
|
},
|
||||||
|
arrays: { detectMove: true, includeValueOnMove: true },
|
||||||
|
});
|
||||||
|
const delta = diffPatcher.diff(leftJson.value, rightJson.value);
|
||||||
|
return delta === undefined ? 'both JSONs are identical' : formatters.html.format(delta, leftJson.value);
|
||||||
|
});
|
||||||
|
const validInput = computed(() => leftJson.value !== '' && rightJson.value !== '');
|
||||||
|
|
||||||
|
watch([diffResult, showUnchanged], () => showHideUnchanged(), { immediate: true });
|
||||||
|
|
||||||
|
function showHideUnchanged() {
|
||||||
|
if (result.value) {
|
||||||
|
formatters.html.showUnchanged(showUnchanged.value, result.value, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
28
src/tools/json-diff/json-diff.e2e.spec.ts
Normal file
28
src/tools/json-diff/json-diff.e2e.spec.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Tool - Json diff', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/json-diff');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Has correct title', async ({ page }) => {
|
||||||
|
await expect(page).toHaveTitle('JSON diff - IT Tools');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Compare two identical JSONs with corresponding result message', async ({ page }) => {
|
||||||
|
const json = '{"foo":"bar","list":["item",{"key":"value"}]}';
|
||||||
|
await page.getByTestId('leftJson').fill(json);
|
||||||
|
await page.getByTestId('rightJson').fill(json);
|
||||||
|
|
||||||
|
const generatedResult = await page.getByTestId('result').innerText();
|
||||||
|
|
||||||
|
expect(generatedResult.trim()).toContain('both JSONs are identical');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Compare two different JSONs with corresponding result message', async ({ page }) => {
|
||||||
|
await page.getByTestId('leftJson').fill('{"foo":"bar","list":["item","item2",{"key":"value"}]}');
|
||||||
|
await page.getByTestId('rightJson').fill('{"foo":"bar","list":["item",{"key":"value"}]}');
|
||||||
|
|
||||||
|
await expect(page.getByTestId('result').getByRole('listitem')).toHaveCount(6);
|
||||||
|
});
|
||||||
|
});
|
146
src/tools/json-diff/json-diff.vue
Normal file
146
src/tools/json-diff/json-diff.vue
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
<template>
|
||||||
|
<n-form-item
|
||||||
|
label="Your first json"
|
||||||
|
:feedback="leftJsonValidation.message"
|
||||||
|
:validation-status="leftJsonValidation.status"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="rawLeftJson"
|
||||||
|
placeholder="Paste your first json here..."
|
||||||
|
type="textarea"
|
||||||
|
rows="20"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
:input-props="{ 'data-test-id': 'leftJson' }"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item
|
||||||
|
label="Your json to compare"
|
||||||
|
:feedback="rightJsonValidation.message"
|
||||||
|
:validation-status="rightJsonValidation.status"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="rawRightJson"
|
||||||
|
placeholder="Paste your json to compare here..."
|
||||||
|
type="textarea"
|
||||||
|
rows="20"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
:input-props="{ 'data-test-id': 'rightJson' }"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<JsonDiffResult :left="rawLeftJson" :right="rawRightJson" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useStorage } from '@vueuse/core';
|
||||||
|
import { useValidation } from '@/composable/validation';
|
||||||
|
import JSON5 from 'json5';
|
||||||
|
import JsonDiffResult from './json-diff-result.vue';
|
||||||
|
|
||||||
|
const rawLeftJson = useStorage(
|
||||||
|
'json-compare:left-json',
|
||||||
|
`{
|
||||||
|
"Actors": [
|
||||||
|
{
|
||||||
|
"name": "Tom Cruise",
|
||||||
|
"age": 56,
|
||||||
|
"Born At": "Syracuse, NY",
|
||||||
|
"Birthdate": "July 3, 1962",
|
||||||
|
"photo": "https://jsonformatter.org/img/tom-cruise.jpg",
|
||||||
|
"wife": null,
|
||||||
|
"weight": 67.5,
|
||||||
|
"hasChildren": true,
|
||||||
|
"hasGreyHair": false,
|
||||||
|
"children": [
|
||||||
|
"Suri",
|
||||||
|
"Isabella Jane",
|
||||||
|
"Connor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Robert Downey Jr.",
|
||||||
|
"age": 53,
|
||||||
|
"Born At": "New York City, NY",
|
||||||
|
"Birthdate": "April 4, 1965",
|
||||||
|
"photo": "https://jsonformatter.org/img/Robert-Downey-Jr.jpg",
|
||||||
|
"wife": "Susan Downey",
|
||||||
|
"weight": 77.1,
|
||||||
|
"hasChildren": true,
|
||||||
|
"hasGreyHair": false,
|
||||||
|
"children": [
|
||||||
|
"Indio Falconer",
|
||||||
|
"Avri Roel",
|
||||||
|
"Exton Elias"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
const rawRightJson = useStorage(
|
||||||
|
'json-compare:right-json',
|
||||||
|
`{
|
||||||
|
"Actors": [
|
||||||
|
{
|
||||||
|
"name": "Tom Cruise",
|
||||||
|
"age": 56,
|
||||||
|
"Born At": "Syracuse, NY",
|
||||||
|
"Birthdate": "July 3, 1962",
|
||||||
|
"photo": "https://jsonformatter.org/img/tom-cruise.jpg",
|
||||||
|
"wife": null,
|
||||||
|
"weight": 57.7,
|
||||||
|
"hasChildren": true,
|
||||||
|
"hasGreyHair": false,
|
||||||
|
"children": [
|
||||||
|
"Connor",
|
||||||
|
"Suri",
|
||||||
|
"Isabella Jane"
|
||||||
|
],
|
||||||
|
"favoriteFood": "Spaghetti"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Robert Downey Sr.",
|
||||||
|
"age": 53,
|
||||||
|
"Born At": "New York City, NY",
|
||||||
|
"Birthdate": "April 4, 1965",
|
||||||
|
"photo": "https://jsonformatter.org/img/Robert-Downey-Jr.jpg",
|
||||||
|
"weight": 77.1,
|
||||||
|
"hasChildren": true,
|
||||||
|
"hasGreyHair": false,
|
||||||
|
"children": [
|
||||||
|
"Indio Falconer",
|
||||||
|
"Avri Roel",
|
||||||
|
"Exton Elias"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const leftJsonValidation = useValidation({
|
||||||
|
source: rawLeftJson,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
validator: (v) => v === '' || JSON5.parse(v),
|
||||||
|
message: 'Provided JSON is not valid.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const rightJsonValidation = useValidation({
|
||||||
|
source: rawRightJson,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
validator: (v) => v === '' || JSON5.parse(v),
|
||||||
|
message: 'Provided JSON is not valid.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
39
src/tools/json-diff/sanitized-html.vue
Normal file
39
src/tools/json-diff/sanitized-html.vue
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<span ref="block"></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
import { Ref, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const block = ref() as Ref<HTMLSpanElement>;
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ html: string }>(), { html: undefined });
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
allowedTags: ['div', 'ul', 'li', 'pre'],
|
||||||
|
allowedAttributes: {
|
||||||
|
div: ['class'],
|
||||||
|
ul: ['class'],
|
||||||
|
li: ['class', 'data-key'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateContent = () => {
|
||||||
|
if (block.value) {
|
||||||
|
block.value.innerHTML = sanitizeHtml(props.html, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onUpdateContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.html as string | undefined,
|
||||||
|
() => {
|
||||||
|
onUpdateContent();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
184
src/tools/json-diff/styles.css
Normal file
184
src/tools/json-diff/styles.css
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
:root {
|
||||||
|
--jsdiff-color_fg: #888888ff;
|
||||||
|
--jsdiff-color_fg_location: #bbbbbbff;
|
||||||
|
--jsdiff-color_bg_added: #066f1988;
|
||||||
|
--jsdiff-color_bg_deleted: #ab060988;
|
||||||
|
--jsdiff-color_bg_moved: #ffffbbff;
|
||||||
|
--jsdiff-font_family: monospace;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-delta {
|
||||||
|
font-family: var(--jsdiff-font_family);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 0 12px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-delta pre {
|
||||||
|
font-family: var(--jsdiff-font_family);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-delta ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0 0 0 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
ul.jsondiffpatch-delta {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0 0 0 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-added .jsondiffpatch-property-name {
|
||||||
|
background: var(--jsdiff-color_bg_added);
|
||||||
|
}
|
||||||
|
.jsondiffpatch-added .jsondiffpatch-value pre {
|
||||||
|
background: var(--jsdiff-color_bg_added);
|
||||||
|
}
|
||||||
|
.jsondiffpatch-modified .jsondiffpatch-right-value {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-modified .jsondiffpatch-right-value pre {
|
||||||
|
background: var(--jsdiff-color_bg_added);
|
||||||
|
}
|
||||||
|
.jsondiffpatch-modified .jsondiffpatch-left-value pre {
|
||||||
|
background: var(--jsdiff-color_bg_deleted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-modified >.jsondiffpatch-left-value pre:after {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
.jsondiffpatch-modified .jsondiffpatch-value {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-textdiff-added {
|
||||||
|
background: var(--jsdiff-color_bg_added);
|
||||||
|
}
|
||||||
|
.jsondiffpatch-deleted .jsondiffpatch-property-name {
|
||||||
|
background: var(--jsdiff-color_bg_deleted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-deleted pre {
|
||||||
|
background: var(--jsdiff-color_bg_deleted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-textdiff-deleted {
|
||||||
|
background: var(--jsdiff-color_bg_deleted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged {
|
||||||
|
color: var(--jsdiff-color_fg);
|
||||||
|
transition: all 0.5s;
|
||||||
|
-webkit-transition: all 0.5s;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-movedestination {
|
||||||
|
color: var(--jsdiff-color_fg);
|
||||||
|
}
|
||||||
|
.jsondiffpatch-movedestination >.jsondiffpatch-value {
|
||||||
|
transition: all 0.5s;
|
||||||
|
-webkit-transition: all 0.5s;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged-showing .jsondiffpatch-unchanged {
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged-showing .jsondiffpatch-movedestination >.jsondiffpatch-value {
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged-showing .jsondiffpatch-arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged-hidden .jsondiffpatch-unchanged {
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination >.jsondiffpatch-value {
|
||||||
|
max-height: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination >.jsondiffpatch-value {
|
||||||
|
display: block;
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged-hiding .jsondiffpatch-unchanged {
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged-hiding .jsondiffpatch-arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged-visible .jsondiffpatch-unchanged {
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-unchanged-visible .jsondiffpatch-movedestination >.jsondiffpatch-value {
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-value {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-value pre:after {
|
||||||
|
content: ',';
|
||||||
|
}
|
||||||
|
.jsondiffpatch-property-name {
|
||||||
|
display: inline-block;
|
||||||
|
padding-right: 5px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-property-name:after {
|
||||||
|
content: ': ';
|
||||||
|
}
|
||||||
|
.jsondiffpatch-child-node-type-array >.jsondiffpatch-property-name:after {
|
||||||
|
content: ': [';
|
||||||
|
}
|
||||||
|
.jsondiffpatch-child-node-type-array:after {
|
||||||
|
content: '],';
|
||||||
|
}
|
||||||
|
div.jsondiffpatch-child-node-type-array:before {
|
||||||
|
content: '[';
|
||||||
|
}
|
||||||
|
div.jsondiffpatch-child-node-type-array:after {
|
||||||
|
content: ']';
|
||||||
|
}
|
||||||
|
.jsondiffpatch-child-node-type-object >.jsondiffpatch-property-name:after {
|
||||||
|
content: ': {';
|
||||||
|
}
|
||||||
|
.jsondiffpatch-child-node-type-object:after {
|
||||||
|
content: '},';
|
||||||
|
}
|
||||||
|
div.jsondiffpatch-child-node-type-object:before {
|
||||||
|
content: '{';
|
||||||
|
}
|
||||||
|
div.jsondiffpatch-child-node-type-object:after {
|
||||||
|
content: '}';
|
||||||
|
}
|
||||||
|
li:last-child >.jsondiffpatch-value pre:after {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
.jsondiffpatch-moved .jsondiffpatch-value {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-moved .jsondiffpatch-moved-destination {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--jsdiff-color_bg_moved);
|
||||||
|
color: var(--jsdiff-color_fg);
|
||||||
|
}
|
||||||
|
.jsondiffpatch-moved .jsondiffpatch-moved-destination:before {
|
||||||
|
content: ' => ';
|
||||||
|
}
|
||||||
|
ul.jsondiffpatch-textdiff {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-textdiff-location {
|
||||||
|
color: var(--jsdiff-color_fg_location);
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-textdiff-line {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.jsondiffpatch-textdiff-line-number:after {
|
||||||
|
content: ',';
|
||||||
|
}
|
||||||
|
.jsondiffpatch-error {
|
||||||
|
background: red;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue