diff --git a/.babelrc b/.babelrc deleted file mode 100644 index f45b63d4..00000000 --- a/.babelrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "presets": [ - ["@babel/preset-env", { "targets": "defaults" }], - "@babel/preset-typescript" - ] -} diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c780564d..2a83397e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,6 +18,15 @@ jobs: cache: 'npm' - run: npm ci - run: npm run build + working-directory: ./packages/jsondiffpatch - run: npm run test + working-directory: ./packages/jsondiffpatch - run: npm run lint + working-directory: ./packages/jsondiffpatch - run: npm run format-check + - run: npm run start + working-directory: ./demos/console-demo + - run: npm run build + working-directory: ./demos/html-demo + - run: npm run start + working-directory: ./demos/numeric-plugin-demo diff --git a/.gitignore b/.gitignore index 7b45a8be..e1a8a21a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,10 +12,11 @@ lib-cov pids logs results -coverage +coverage .nyc_output -dist +dist build +lib npm-debug.log -.idea/ +.idea/ diff --git a/.npmignore b/.npmignore deleted file mode 100644 index e69de29b..00000000 diff --git a/Makefile b/Makefile deleted file mode 100644 index dcc1b7ac..00000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -build: node_modules - npm run build -clean: - rm -rf dist - rm -rf coverage -test: node_modules - npm run test -node_modules: - npm install - -.PHONY: test build dist diff --git a/README.md b/README.md index 7e4c9c9f..124c86a9 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Diff & patch JavaScript objects ## **[Live Demo](http://benjamine.github.io/jsondiffpatch/demo/index.html)** - min+gzipped ~ 16KB -- browser and server (`/dist` folder with bundles for UMD, commonjs, or ES modules) +- browser and server (ESM-only) - (optionally) uses [google-diff-match-patch](http://code.google.com/p/google-diff-match-patch/) for long text diffs (diff at character level) - smart array diffing using [LCS](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem), **_IMPORTANT NOTE:_** to match objects inside an array you must provide an `objectHash` function (this is how objects are matched, otherwise a dumb match by position is used). For more details, check [Array diff documentation](docs/arrays.md) - reverse a delta @@ -31,19 +31,14 @@ Diff & patch JavaScript objects ## Supported platforms -- Any modern browser and IE8+ - -[![Testling Status](https://ci.testling.com/benjamine/jsondiffpatch.png)](https://ci.testling.com/benjamine/jsondiffpatch) - -And you can test your current browser visiting the [test page](http://benjamine.github.io/jsondiffpatch/test/index.html). - -- Node.js [![Build Status](https://secure.travis-ci.org/benjamine/jsondiffpatch.svg)](http://travis-ci.org/benjamine/jsondiffpatch) v8+ +- Any browser that supports ES6 +- Node.js 18, 20+ ## Usage ```javascript // sample data -var country = { +const country = { name: 'Argentina', capital: 'Buenos Aires', independence: new Date(1816, 6, 9), @@ -51,14 +46,14 @@ var country = { }; // clone country, using dateReviver for Date objects -var country2 = JSON.parse(JSON.stringify(country), jsondiffpatch.dateReviver); +const country2 = JSON.parse(JSON.stringify(country), jsondiffpatch.dateReviver); // make some changes country2.name = 'Republica Argentina'; country2.population = 41324992; delete country2.capital; -var delta = jsondiffpatch.diff(country, country2); +const delta = jsondiffpatch.diff(country, country2); assertSame(delta, { name: ['Argentina', 'Republica Argentina'], // old value, new value @@ -70,10 +65,10 @@ assertSame(delta, { jsondiffpatch.patch(country, delta); // reverse diff -var reverseDelta = jsondiffpatch.reverse(delta); +const reverseDelta = jsondiffpatch.reverse(delta); // also country2 can be return to original value with: jsondiffpatch.unpatch(country2, delta); -var delta2 = jsondiffpatch.diff(country, country2); +const delta2 = jsondiffpatch.diff(country, country2); assert(delta2 === undefined); // undefined => no difference ``` @@ -82,7 +77,7 @@ Array diffing: ```javascript // sample data -var country = { +const country = { name: 'Argentina', cities: [ { @@ -109,7 +104,7 @@ var country = { }; // clone country -var country2 = JSON.parse(JSON.stringify(country)); +const country2 = JSON.parse(JSON.stringify(country)); // delete Cordoba country.cities.splice(1, 1); @@ -120,18 +115,18 @@ country.cities.splice(4, 0, { }); // modify Rosario, and move it -var rosario = country.cities.splice(1, 1)[0]; +const rosario = country.cities.splice(1, 1)[0]; rosario.population += 1234; country.cities.push(rosario); // create a configured instance, match objects by name -var diffpatcher = jsondiffpatch.create({ +const diffpatcher = jsondiffpatch.create({ objectHash: function (obj) { return obj.name; }, }); -var delta = diffpatcher.diff(country, country2); +const delta = diffpatcher.diff(country, country2); assertSame(delta, { cities: { @@ -165,7 +160,7 @@ assertSame(delta, { }); ``` -For more example cases (nested objects or arrays, long text diffs) check `test/examples/` +For more example cases (nested objects or arrays, long text diffs) check `packages/jsondiffpatch/test/examples/` If you want to understand deltas, see [delta format documentation](docs/deltas.md) @@ -180,20 +175,19 @@ npm install jsondiffpatch ``` ```js -var jsondiffpatch = require('jsondiffpatch'); -var jsondiffpatchInstance = jsondiffpatch.create(options); +import * as jsondiffpatch from 'jsondiffpatch'; +const jsondiffpatchInstance = jsondiffpatch.create(options); ``` -Some properties are available only from static main module (e.g. formatters, console), so we need to keep the reference to it if we want to use them. - ### browser -In a browser, you could load directly a bundle in `/dist`, eg. `/dist/jsondiffpatch.umd.js`. +In a browser, you can load a bundle using a tool like [esm.sh](https://esm.sh) or [Skypack](https://www.skypack.dev). ## Options ```javascript -var jsondiffpatchInstance = require('jsondiffpatch').create({ +import * as jsondiffpatch from 'jsondiffpatch'; +const jsondiffpatchInstance = jsondiffpatch.create({ // used to match objects when diffing arrays, by default only === operator is used objectHash: function (obj) { // this function is used only to when objects are not equal by ref @@ -230,19 +224,15 @@ var jsondiffpatchInstance = require('jsondiffpatch').create({ - @@ -250,18 +240,24 @@ var jsondiffpatchInstance = require('jsondiffpatch').create({

- @@ -275,12 +271,12 @@ For more details check [Formatters documentation](docs/formatters.md) ```sh # diff two json files, colored output (using chalk lib) -./node_modules/.bin/jsondiffpatch ./left.json ./right.json +./node_modules/.bin/jsondiffpatch ./docs/demo/left.json ./docs/demo/right.json # or install globally npm install -g jsondiffpatch -jsondiffpatch ./demo/left.json ./demo/right.json +jsondiffpatch ./docs/demo/left.json ./docs/demo/right.json ``` ![console_demo!](docs/demo/consoledemo.png) diff --git a/docs/demo/consoledemo.js b/demos/console-demo/demo.ts similarity index 86% rename from docs/demo/consoledemo.js rename to demos/console-demo/demo.ts index 759ae77e..82026fb5 100644 --- a/docs/demo/consoledemo.js +++ b/demos/console-demo/demo.ts @@ -1,12 +1,36 @@ -const jsondiffpatch = require('../../dist/jsondiffpatch.cjs.js'); +import * as jsondiffpatch from 'jsondiffpatch'; +import * as consoleFormatter from 'jsondiffpatch/formatters/console'; const instance = jsondiffpatch.create({ objectHash: function (obj) { - return obj._id || obj.id || obj.name || JSON.stringify(obj); + const objRecord = obj as Record; + return ( + objRecord._id || + objRecord.id || + objRecord.name || + JSON.stringify(objRecord) + ); }, }); -const data = { +interface Data { + name: string; + summary: string; + surface?: number; + timezone: number[]; + demographics: { population: number; largestCities: string[] }; + languages: string[]; + countries: { + name: string; + capital?: string; + independence?: Date; + unasur: boolean; + population?: number; + }[]; + spanishName?: string; +} + +const data: Data = { name: 'South America', summary: 'South America (Spanish: América del Sur, Sudamérica or Suramérica;' + @@ -158,4 +182,4 @@ data.demographics.population += 2342; const right = data; const delta = instance.diff(left, right); -jsondiffpatch.console.log(delta); +consoleFormatter.log(delta); diff --git a/demos/console-demo/package.json b/demos/console-demo/package.json new file mode 100644 index 00000000..e0c068b6 --- /dev/null +++ b/demos/console-demo/package.json @@ -0,0 +1,15 @@ +{ + "name": "console-demo", + "type": "module", + "version": "1.0.0", + "scripts": { + "start": "npm run build && node build/demo.js", + "build": "tsc" + }, + "dependencies": { + "jsondiffpatch": "^0.5.0" + }, + "devDependencies": { + "typescript": "~5.3.2" + } +} diff --git a/demos/console-demo/tsconfig.json b/demos/console-demo/tsconfig.json new file mode 100644 index 00000000..b0afd955 --- /dev/null +++ b/demos/console-demo/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["es6"], + "module": "node16", + "outDir": "build", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": false + } +} diff --git a/demos/html-demo/demo.ts b/demos/html-demo/demo.ts new file mode 100644 index 00000000..bc6e08d7 --- /dev/null +++ b/demos/html-demo/demo.ts @@ -0,0 +1,841 @@ +import * as jsondiffpatch from 'jsondiffpatch'; +import * as annotatedFormatter from 'jsondiffpatch/formatters/annotated'; +import * as htmlFormatter from 'jsondiffpatch/formatters/html'; + +import 'jsondiffpatch/formatters/styles/html.css'; +import 'jsondiffpatch/formatters/styles/annotated.css'; + +declare namespace CodeMirror { + function fromTextArea( + host: HTMLTextAreaElement, + options?: EditorConfiguration, + ): Editor; + + interface EditorConfiguration { + mode?: string; + json?: boolean; + readOnly?: boolean; + } + + interface Editor { + getValue(): string; + setValue(content: string): void; + on(eventName: 'change', handler: () => void): void; + } +} + +interface Continent { + name: string; + summary: string; + surface?: number; + timezone: [number, number]; + demographics: { population: number; largestCities: string[] }; + languages: string[]; + countries: Country[]; + spanishName?: string; +} + +interface Country { + name: string; + capital?: string; + independence?: Date; + unasur: boolean; + population?: number; +} + +const getExampleJson = function () { + const data: Continent = { + name: 'South America', + summary: + 'South America (Spanish: América del Sur, Sudamérica or \n' + + 'Suramérica; Portuguese: América do Sul; Quechua and Aymara: \n' + + 'Urin Awya Yala; Guarani: Ñembyamérika; Dutch: Zuid-Amerika; \n' + + 'French: Amérique du Sud) is a continent situated in the \n' + + 'Western Hemisphere, mostly in the Southern Hemisphere, with \n' + + 'a relatively small portion in the Northern Hemisphere. \n' + + 'The continent is also considered a subcontinent of the \n' + + 'Americas.[2][3] It is bordered on the west by the Pacific \n' + + 'Ocean and on the north and east by the Atlantic Ocean; \n' + + 'North America and the Caribbean Sea lie to the northwest. \n' + + 'It includes twelve countries: Argentina, Bolivia, Brazil, \n' + + 'Chile, Colombia, Ecuador, Guyana, Paraguay, Peru, Suriname, \n' + + 'Uruguay, and Venezuela. The South American nations that \n' + + 'border the Caribbean Sea—including Colombia, Venezuela, \n' + + 'Guyana, Suriname, as well as French Guiana, which is an \n' + + 'overseas region of France—are also known as Caribbean South \n' + + 'America. South America has an area of 17,840,000 square \n' + + 'kilometers (6,890,000 sq mi). Its population as of 2005 \n' + + 'has been estimated at more than 371,090,000. South America \n' + + 'ranks fourth in area (after Asia, Africa, and North America) \n' + + 'and fifth in population (after Asia, Africa, Europe, and \n' + + 'North America). The word America was coined in 1507 by \n' + + 'cartographers Martin Waldseemüller and Matthias Ringmann, \n' + + 'after Amerigo Vespucci, who was the first European to \n' + + 'suggest that the lands newly discovered by Europeans were \n' + + 'not India, but a New World unknown to Europeans.', + + surface: 17840000, + timezone: [-4, -2], + demographics: { + population: 385742554, + largestCities: [ + 'São Paulo', + 'Buenos Aires', + 'Rio de Janeiro', + 'Lima', + 'Bogotá', + ], + }, + languages: [ + 'spanish', + 'portuguese', + 'english', + 'dutch', + 'french', + 'quechua', + 'guaraní', + 'aimara', + 'mapudungun', + ], + countries: [ + { + name: 'Argentina', + capital: 'Buenos Aires', + independence: new Date(1816, 6, 9), + unasur: true, + }, + { + name: 'Bolivia', + capital: 'La Paz', + independence: new Date(1825, 7, 6), + unasur: true, + }, + { + name: 'Brazil', + capital: 'Brasilia', + independence: new Date(1822, 8, 7), + unasur: true, + }, + { + name: 'Chile', + capital: 'Santiago', + independence: new Date(1818, 1, 12), + unasur: true, + }, + { + name: 'Colombia', + capital: 'Bogotá', + independence: new Date(1810, 6, 20), + unasur: true, + }, + { + name: 'Ecuador', + capital: 'Quito', + independence: new Date(1809, 7, 10), + unasur: true, + }, + { + name: 'Guyana', + capital: 'Georgetown', + independence: new Date(1966, 4, 26), + unasur: true, + }, + { + name: 'Paraguay', + capital: 'Asunción', + independence: new Date(1811, 4, 14), + unasur: true, + }, + { + name: 'Peru', + capital: 'Lima', + independence: new Date(1821, 6, 28), + unasur: true, + }, + { + name: 'Suriname', + capital: 'Paramaribo', + independence: new Date(1975, 10, 25), + unasur: true, + }, + { + name: 'Uruguay', + capital: 'Montevideo', + independence: new Date(1825, 7, 25), + unasur: true, + }, + { + name: 'Venezuela', + capital: 'Caracas', + independence: new Date(1811, 6, 5), + unasur: true, + }, + ], + }; + + const json = [JSON.stringify(data, null, 2)]; + + data.summary = data.summary + .replace('Brazil', 'Brasil') + .replace('also known as', 'a.k.a.'); + data.languages[2] = 'inglés'; + data.countries.pop(); + data.countries.pop(); + data.countries[0].capital = 'Rawson'; + data.countries.push({ + name: 'Antártida', + unasur: false, + }); + + // modify and move + data.countries[4].population = 42888594; + data.countries.splice(11, 0, data.countries.splice(4, 1)[0]); + + data.countries.splice(2, 0, data.countries.splice(7, 1)[0]); + + delete data.surface; + data.spanishName = 'Sudamérica'; + data.demographics.population += 2342; + + json.push(JSON.stringify(data, null, 2)); + + return json; +}; + +const instance = jsondiffpatch.create({ + objectHash: function (obj, index) { + const objRecord = obj as Record; + if (typeof objRecord._id !== 'undefined') { + return objRecord._id; + } + if (typeof objRecord.id !== 'undefined') { + return objRecord.id; + } + if (typeof objRecord.name !== 'undefined') { + return objRecord.name; + } + return '$$index:' + index; + }, +}); + +const dom = { + addClass: function (el: HTMLElement, className: string) { + if (el.classList) { + el.classList.add(className); + } else { + el.className += ' ' + className; + } + }, + removeClass: function (el: HTMLElement, className: string) { + if (el.classList) { + el.classList.remove(className); + } else { + el.className = el.className.replace( + new RegExp( + '(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', + 'gi', + ), + ' ', + ); + } + }, + text: function (el: HTMLElement, text: string) { + if (typeof el.textContent !== 'undefined') { + if (typeof text === 'undefined') { + return el.textContent; + } + el.textContent = text; + } else { + if (typeof text === 'undefined') { + return el.innerText; + } + el.innerText = text; + } + }, + getJson: function ( + url: string, + callback: (error: Error | string | null, data?: unknown) => void, + ) { + let request: XMLHttpRequest | null = new XMLHttpRequest(); + request.open('GET', url, true); + request.onreadystatechange = function () { + if (this.readyState === 4) { + let data; + try { + data = JSON.parse(this.responseText, jsondiffpatch.dateReviver); + } catch (parseError) { + return callback('parse error: ' + parseError); + } + if (this.status >= 200 && this.status < 400) { + callback(null, data); + } else { + callback(new Error('request failed'), data); + } + } + }; + request.send(); + request = null; + }, + runScriptTags: function (el: HTMLElement) { + const scripts = el.querySelectorAll('script'); + for (let i = 0; i < scripts.length; i++) { + const s = scripts[i]; + // eslint-disable-next-line no-eval + eval(s.innerHTML); + } + }, +}; + +const trim = function (str: string) { + return str.replace(/^\s+|\s+$/g, ''); +}; + +class JsonArea { + element: HTMLTextAreaElement; + container: HTMLElement; + editor?: CodeMirror.Editor; + + constructor(element: HTMLTextAreaElement) { + this.element = element; + this.container = element.parentNode as HTMLElement; + const self = this; + const prettifyButton = this.container.querySelector( + '.prettyfy', + ) as HTMLElement; + if (prettifyButton) { + prettifyButton.addEventListener('click', function () { + self.prettyfy(); + }); + } + } + + error = (err: unknown) => { + const errorElement = this.container.querySelector('.error-message')!; + if (!err) { + dom.removeClass(this.container, 'json-error'); + errorElement.innerHTML = ''; + return; + } + errorElement.innerHTML = err + ''; + dom.addClass(this.container, 'json-error'); + }; + + getValue = () => { + if (!this.editor) { + return this.element.value; + } + return this.editor.getValue(); + }; + + parse = () => { + const txt = trim(this.getValue()); + try { + this.error(false); + if ( + /^\d+(.\d+)?(e[+-]?\d+)?$/i.test(txt) || + /^(true|false)$/.test(txt) || + /^["].*["]$/.test(txt) || + /^[{[](.|\n)*[}\]]$/.test(txt) + ) { + return JSON.parse(txt, jsondiffpatch.dateReviver); + } + return this.getValue(); + } catch (err) { + this.error(err); + throw err; + } + }; + + setValue = (value: string) => { + if (!this.editor) { + this.element.value = value; + return; + } + this.editor.setValue(value); + }; + + prettyfy = () => { + const value = this.parse(); + const prettyJson = + typeof value === 'string' ? value : JSON.stringify(value, null, 2); + this.setValue(prettyJson); + }; + + /* global CodeMirror */ + makeEditor = (readOnly?: boolean) => { + if (typeof CodeMirror === 'undefined') { + return; + } + this.editor = CodeMirror.fromTextArea(this.element, { + mode: 'javascript', + json: true, + readOnly, + }); + if (!readOnly) { + this.editor.on('change', compare); + } + }; +} + +const areas = { + left: new JsonArea( + document.getElementById('json-input-left') as HTMLTextAreaElement, + ), + right: new JsonArea( + document.getElementById('json-input-right') as HTMLTextAreaElement, + ), + delta: new JsonArea( + document.getElementById('json-delta') as HTMLTextAreaElement, + ), +}; + +const compare = function () { + let left, right, error; + document.getElementById('results')!.style.display = 'none'; + try { + left = areas.left.parse(); + } catch (err) { + error = err; + } + try { + right = areas.right.parse(); + } catch (err) { + error = err; + } + areas.delta.error(false); + if (error) { + areas.delta.setValue(''); + return; + } + const selectedType = getSelectedDeltaType(); + const visualdiff = document.getElementById('visualdiff')!; + const annotateddiff = document.getElementById('annotateddiff')!; + const jsondifflength = document.getElementById('jsondifflength')!; + try { + const delta = instance.diff(left, right); + + if (typeof delta === 'undefined') { + switch (selectedType) { + case 'visual': + visualdiff.innerHTML = 'no diff'; + break; + case 'annotated': + annotateddiff.innerHTML = 'no diff'; + break; + case 'json': + areas.delta.setValue('no diff'); + jsondifflength.innerHTML = '0'; + break; + } + } else { + switch (selectedType) { + case 'visual': + visualdiff.innerHTML = htmlFormatter.format(delta, left)!; + if ( + !(document.getElementById('showunchanged') as HTMLInputElement) + .checked + ) { + htmlFormatter.hideUnchanged(); + } + dom.runScriptTags(visualdiff); + break; + case 'annotated': + annotateddiff.innerHTML = annotatedFormatter.format(delta)!; + break; + case 'json': + areas.delta.setValue(JSON.stringify(delta, null, 2)); + jsondifflength.innerHTML = + Math.round(JSON.stringify(delta).length / 102.4) / 10.0 + ''; + break; + } + } + } catch (err) { + jsondifflength.innerHTML = '0'; + visualdiff.innerHTML = ''; + annotateddiff.innerHTML = ''; + areas.delta.setValue(''); + areas.delta.error(err); + if (typeof console !== 'undefined' && console.error) { + console.error(err); + console.error((err as Error).stack); + } + } + document.getElementById('results')!.style.display = ''; +}; + +areas.left.makeEditor(); +areas.right.makeEditor(); + +areas.left.element.addEventListener('change', compare); +areas.right.element.addEventListener('change', compare); +areas.left.element.addEventListener('keyup', compare); +areas.right.element.addEventListener('keyup', compare); + +const getSelectedDeltaType = function () { + if ( + (document.getElementById('show-delta-type-visual') as HTMLInputElement) + .checked + ) { + return 'visual'; + } + if ( + (document.getElementById('show-delta-type-annotated') as HTMLInputElement) + .checked + ) { + return 'annotated'; + } + if ( + (document.getElementById('show-delta-type-json') as HTMLInputElement) + .checked + ) { + return 'json'; + } +}; + +const showSelectedDeltaType = function () { + const type = getSelectedDeltaType(); + document.getElementById('delta-panel-visual')!.style.display = + type === 'visual' ? '' : 'none'; + document.getElementById('delta-panel-annotated')!.style.display = + type === 'annotated' ? '' : 'none'; + document.getElementById('delta-panel-json')!.style.display = + type === 'json' ? '' : 'none'; + compare(); +}; + +document + .getElementById('show-delta-type-visual')! + .addEventListener('click', showSelectedDeltaType); +document + .getElementById('show-delta-type-annotated')! + .addEventListener('click', showSelectedDeltaType); +document + .getElementById('show-delta-type-json')! + .addEventListener('click', showSelectedDeltaType); + +document.getElementById('swap')!.addEventListener('click', function () { + const leftValue = areas.left.getValue(); + areas.left.setValue(areas.right.getValue()); + areas.right.setValue(leftValue); + compare(); +}); + +document.getElementById('clear')!.addEventListener('click', function () { + areas.left.setValue(''); + areas.right.setValue(''); + compare(); +}); + +document + .getElementById('showunchanged')! + .addEventListener('change', function () { + htmlFormatter.showUnchanged( + (document.getElementById('showunchanged') as HTMLInputElement).checked, + null, + 800, + ); + }); + +document.addEventListener('DOMContentLoaded', function () { + setTimeout(compare); +}); + +interface DataObject { + name?: string; + content?: string; + fullname?: string; +} + +interface Data { + url?: string; + description?: string; + left?: DataObject | string; + right?: DataObject | string; + error?: unknown; +} + +interface Load { + data: (dataArg?: Data) => void; + gist: (this: Load, id: string) => void; + leftright: ( + this: Load, + descriptionArg: string | undefined, + leftValueArg: string, + rightValueArg: string, + ) => void; + key: (key: string) => void; +} + +const load: Load = { + data: function (dataArg) { + const data = dataArg || {}; + dom.text(document.getElementById('description')!, data.description || ''); + if (data.url && trim(data.url).substring(0, 10) !== 'javascript') { + document.getElementById('external-link')!.setAttribute('href', data.url); + document.getElementById('external-link')!.style.display = ''; + } else { + document.getElementById('external-link')!.style.display = 'none'; + } + const leftValue = data.left + ? (data.left as DataObject).content || (data.left as string) + : ''; + areas.left.setValue(leftValue); + const rightValue = data.right + ? (data.right as DataObject).content || (data.right as string) + : ''; + areas.right.setValue(rightValue); + + dom.text( + document.getElementById('json-panel-left')!.querySelector('h2')!, + (data.left && (data.left as DataObject).name) || 'left.json', + ); + dom.text( + document.getElementById('json-panel-right')!.querySelector('h2')!, + (data.right && (data.right as DataObject).name) || 'right.json', + ); + + document + .getElementById('json-panel-left')! + .querySelector('h2')! + .setAttribute( + 'title', + (data.left && (data.left as DataObject).fullname) || '', + ); + document + .getElementById('json-panel-right')! + .querySelector('h2')! + .setAttribute( + 'title', + (data.right && (data.right as DataObject).fullname) || '', + ); + + if (data.error) { + areas.left.setValue('ERROR LOADING: ' + data.error); + areas.right.setValue(''); + } + }, + + gist: function (id) { + dom.getJson('https://api.github.com/gists/' + id, function (error, data) { + interface GistError { + message?: string; + } + + if (error) { + const gistError = data as GistError; + const message = + error + (gistError && gistError.message ? gistError.message : ''); + load.data({ + error: message, + }); + return; + } + + interface GistData { + files: Record< + string, + { language: string; filename: string; content: string } + >; + html_url: string; + description: string; + } + + const gistData = data as GistData; + + const filenames = []; + for (const filename in gistData.files) { + const file = gistData.files[filename]; + if (file.language === 'JSON') { + filenames.push(filename); + } + } + filenames.sort(); + const files = [ + gistData.files[filenames[0]], + gistData.files[filenames[1]], + ]; + /* jshint camelcase: false */ + load.data({ + url: gistData.html_url, + description: gistData.description, + left: { + name: files[0].filename, + content: files[0].content, + }, + right: { + name: files[1].filename, + content: files[1].content, + }, + }); + }); + }, + + leftright: function (descriptionArg, leftValueArg, rightValueArg) { + try { + const description = decodeURIComponent(descriptionArg || ''); + const leftValue = decodeURIComponent(leftValueArg); + const rightValue = decodeURIComponent(rightValueArg); + const urlmatch = /https?:\/\/.*\/([^/]+\.json)(?:[?#].*)?/; + const dataLoaded: { + description: string; + left: DataObject; + right: DataObject; + } = { + description, + left: {}, + right: {}, + }; + const loadIfReady = function () { + if ( + typeof dataLoaded.left.content !== 'undefined' && + typeof dataLoaded.right.content !== 'undefined' + ) { + load.data(dataLoaded); + } + }; + if (urlmatch.test(leftValue)) { + dataLoaded.left.name = urlmatch.exec(leftValue)![1]; + dataLoaded.left.fullname = leftValue; + dom.getJson(leftValue, function (error, data) { + if (error) { + dataLoaded.left.content = + error + + (data && (data as { message?: string }).message + ? (data as { message: string }).message + : ''); + } else { + dataLoaded.left.content = JSON.stringify(data, null, 2); + } + loadIfReady(); + }); + } else { + dataLoaded.left.content = leftValue; + } + if (urlmatch.test(rightValue)) { + dataLoaded.right.name = urlmatch.exec(rightValue)![1]; + dataLoaded.right.fullname = rightValue; + dom.getJson(rightValue, function (error, data) { + if (error) { + dataLoaded.right.content = + error + + (data && (data as { message?: string }).message + ? (data as { message: string }).message + : ''); + } else { + dataLoaded.right.content = JSON.stringify(data, null, 2); + } + loadIfReady(); + }); + } else { + dataLoaded.right.content = rightValue; + } + loadIfReady(); + } catch (err) { + load.data({ + error: err, + }); + } + }, + + key: function (key: string) { + const matchers = { + gist: /^(?:https?:\/\/)?(?:gist\.github\.com\/)?(?:[\w0-9\-a-f]+\/)?([0-9a-f]+)$/i, + leftright: /^(?:desc=(.*)?&)?left=(.*)&right=(.*)&?$/i, + }; + for (const loader in matchers) { + const match = matchers[loader as keyof typeof matchers].exec(key); + if (match) { + return ( + load[loader as keyof typeof matchers] as ( + this: Load, + ...args: string[] + ) => void + ).apply(load, match.slice(1)); + } + } + load.data({ + error: 'unsupported source: ' + key, + }); + }, +}; + +const urlQuery = /^[^?]*\?([^#]+)/.exec(document.location.href); +if (urlQuery) { + load.key(urlQuery[1]); +} else { + const exampleJson = getExampleJson(); + load.data({ + left: exampleJson[0], + right: exampleJson[1], + }); +} + +(document.getElementById('examples') as HTMLSelectElement).addEventListener( + 'change', + function () { + const example = trim(this.value); + switch (example) { + case 'text': { + const exampleJson = getExampleJson(); + load.data({ + left: { + name: 'left.txt', + content: JSON.parse(exampleJson[0]).summary, + }, + right: { + name: 'right.txt', + content: JSON.parse(exampleJson[1]).summary, + }, + }); + break; + } + case 'gist': + document.location = '?benjamine/9188826'; + break; + case 'moving': + document.location = + '?desc=moving%20around&left=' + + encodeURIComponent( + JSON.stringify([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + ) + + '&right=' + + encodeURIComponent( + JSON.stringify([10, 0, 1, 7, 2, 4, 5, 6, 88, 9, 3]), + ); + break; + case 'query': + document.location = + '?desc=encoded%20in%20url&left=' + + /* jshint quotmark: false */ + encodeURIComponent( + JSON.stringify({ + "don't": 'abuse', + with: ['large', 'urls'], + }), + ) + + '&right=' + + encodeURIComponent( + JSON.stringify({ + "don't": 'use', + with: ['>', 2, 'KB urls'], + }), + ); + break; + case 'urls': + document.location = + '?desc=http%20raw%20file%20urls&left=' + + encodeURIComponent( + 'https://rawgithub.com/benjamine/JsonDiffPatch/' + + 'c83e942971c627f61ef874df3cfdd50a95f1c5a2/package.json', + ) + + '&right=' + + encodeURIComponent( + 'https://rawgithub.com/benjamine/JsonDiffPatch/master/package.json', + ); + break; + default: + document.location = '?'; + break; + } + }, +); diff --git a/docs/demo/index.html b/demos/html-demo/index.html similarity index 94% rename from docs/demo/index.html rename to demos/html-demo/index.html index 9f05a001..1446be7d 100644 --- a/docs/demo/index.html +++ b/demos/html-demo/index.html @@ -8,13 +8,7 @@ - @@ -24,10 +18,6 @@ type="text/css" media="screen" /> - +