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; +}