diff --git a/package.json b/package.json
index 49148be5..930edb92 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
"fuse.js": "^6.6.2",
"highlight.js": "^11.7.0",
"json5": "^2.2.3",
+ "jsondiffpatch-rc": "^0.4.2",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"mathjs": "^10.6.4",
@@ -68,6 +69,7 @@
"plausible-tracker": "^0.3.8",
"qrcode": "^1.5.1",
"randombytes": "^2.1.0",
+ "sanitize-html": "^2.10.0",
"sql-formatter": "^8.2.0",
"ts-pattern": "^4.2.2",
"ua-parser-js": "^1.0.35",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f0db977e..0d5e20fb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -70,6 +70,9 @@ dependencies:
json5:
specifier: ^2.2.3
version: 2.2.3
+ jsondiffpatch-rc:
+ specifier: ^0.4.2
+ version: 0.4.2
jwt-decode:
specifier: ^3.1.2
version: 3.1.2
@@ -106,6 +109,9 @@ dependencies:
randombytes:
specifier: ^2.1.0
version: 2.1.0
+ sanitize-html:
+ specifier: ^2.10.0
+ version: 2.10.0
sql-formatter:
specifier: ^8.2.0
version: 8.2.0
@@ -4039,6 +4045,10 @@ packages:
engines: {node: '>=8'}
dev: true
+ /diff-match-patch@1.0.5:
+ resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
+ dev: false
+
/dijkstrajs@1.0.2:
resolution: {integrity: sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==}
dev: false
@@ -4076,9 +4086,16 @@ packages:
entities: 2.2.0
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:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
- dev: true
/domexception@4.0.0:
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
@@ -4094,6 +4111,13 @@ packages:
domelementtype: 2.3.0
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:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
dependencies:
@@ -4102,6 +4126,14 @@ packages:
domhandler: 4.3.1
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:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
dependencies:
@@ -4184,6 +4216,11 @@ packages:
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
engines: {node: '>=0.12'}
+ /entities@4.4.0:
+ resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==}
+ engines: {node: '>=0.12'}
+ dev: false
+
/errno@0.1.8:
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
hasBin: true
@@ -5349,6 +5386,15 @@ packages:
entities: 3.0.1
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:
resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
engines: {node: '>= 6'}
@@ -5611,6 +5657,11 @@ packages:
isobject: 3.0.1
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:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
dev: true
@@ -5924,6 +5975,15 @@ packages:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
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:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies:
@@ -6698,6 +6758,10 @@ packages:
engines: {node: '>= 0.10'}
dev: true
+ /parse-srcset@1.0.2:
+ resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
+ dev: false
+
/parse5@6.0.1:
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
dev: true
@@ -7422,6 +7486,17 @@ packages:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
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:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
dev: true
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 5cb0cdcb..15dd003d 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 jsonDiff } from './json-diff';
import { tool as yamlToJson } from './yaml-to-json-converter';
import { tool as jsonToYaml } from './json-to-yaml-converter';
import { tool as ipv6UlaGenerator } from './ipv6-ula-generator';
@@ -102,6 +103,7 @@ export const toolsByCategory: ToolCategory[] = [
crontabGenerator,
jsonViewer,
jsonMinify,
+ jsonDiff,
sqlPrettify,
chmodCalculator,
dockerRunToDockerComposeConverter,
diff --git a/src/tools/json-diff/index.ts b/src/tools/json-diff/index.ts
new file mode 100644
index 00000000..815aceb5
--- /dev/null
+++ b/src/tools/json-diff/index.ts
@@ -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'),
+});
diff --git a/src/tools/json-diff/json-diff-result.vue b/src/tools/json-diff/json-diff-result.vue
new file mode 100644
index 00000000..26d06e40
--- /dev/null
+++ b/src/tools/json-diff/json-diff-result.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/json-diff/json-diff.e2e.spec.ts b/src/tools/json-diff/json-diff.e2e.spec.ts
new file mode 100644
index 00000000..60e0152d
--- /dev/null
+++ b/src/tools/json-diff/json-diff.e2e.spec.ts
@@ -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);
+ });
+});
diff --git a/src/tools/json-diff/json-diff.vue b/src/tools/json-diff/json-diff.vue
new file mode 100644
index 00000000..b6ee4f53
--- /dev/null
+++ b/src/tools/json-diff/json-diff.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/json-diff/sanitized-html.vue b/src/tools/json-diff/sanitized-html.vue
new file mode 100644
index 00000000..289eb2d8
--- /dev/null
+++ b/src/tools/json-diff/sanitized-html.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
diff --git a/src/tools/json-diff/styles.css b/src/tools/json-diff/styles.css
new file mode 100644
index 00000000..b7a6b96f
--- /dev/null
+++ b/src/tools/json-diff/styles.css
@@ -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;
+}