From d21e20916308f26e0cdee3e7461f40fb03ba8161 Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Fri, 1 Dec 2023 09:15:44 -0500 Subject: [PATCH 1/9] Make it a monorepo and convert demos to TypeScript --- .github/workflows/CI.yml | 9 + .npmignore | 0 Makefile | 11 - README.md | 4 +- .../console-demo/demo.ts | 29 +- demos/console-demo/package.json | 15 + demos/console-demo/tsconfig.json | 109 ++ demos/html-demo/demo.ts | 843 ++++++++ {docs/demo => demos/html-demo}/index.html | 6 +- demos/html-demo/package.json | 15 + {docs/demo => demos/html-demo}/style.css | 0 demos/html-demo/tsconfig.json | 109 ++ demos/numeric-plugin-demo/numeric-plugin.ts | 139 ++ demos/numeric-plugin-demo/package.json | 15 + demos/numeric-plugin-demo/tsconfig.json | 109 ++ docs/demo/demo.js | 721 ------- docs/demo/numeric-plugin.js | 113 -- docs/index.html | 1 - package-lock.json | 1718 +++++++++++------ package.json | 70 +- .babelrc => packages/jsondiffpatch/.babelrc | 0 .../jsondiffpatch/.editorconfig | 0 .../jsondiffpatch/.eslintignore | 0 .../jsondiffpatch/.eslintrc.cjs | 0 .../jsondiffpatch/bin}/jsondiffpatch | 0 .../jsondiffpatch/jest.config.cjs | 0 packages/jsondiffpatch/package.json | 66 + .../jsondiffpatch/rollup.config.mjs | 46 +- {src => packages/jsondiffpatch/src}/clone.ts | 0 .../jsondiffpatch/src}/contexts/context.ts | 0 .../jsondiffpatch/src}/contexts/diff.ts | 0 .../jsondiffpatch/src}/contexts/patch.ts | 0 .../jsondiffpatch/src}/contexts/reverse.ts | 0 .../jsondiffpatch/src}/date-reviver.ts | 0 .../jsondiffpatch/src}/diff-match-patch.d.ts | 0 .../jsondiffpatch/src}/diffpatcher.ts | 0 .../jsondiffpatch/src}/filters/arrays.ts | 0 .../jsondiffpatch/src}/filters/dates.ts | 0 .../jsondiffpatch/src}/filters/lcs.ts | 0 .../jsondiffpatch/src}/filters/nested.ts | 0 .../jsondiffpatch/src}/filters/texts.ts | 0 .../jsondiffpatch/src}/filters/trivial.ts | 0 .../src}/formatters/annotated.ts | 0 .../jsondiffpatch/src}/formatters/base.ts | 0 .../jsondiffpatch/src}/formatters/console.ts | 0 .../jsondiffpatch/src}/formatters/html.ts | 2 +- .../jsondiffpatch/src}/formatters/index.ts | 0 .../src}/formatters/jsonpatch.ts | 0 .../src/formatters/styles}/annotated.css | 0 .../src/formatters/styles}/html.css | 0 {src => packages/jsondiffpatch/src}/main.ts | 0 {src => packages/jsondiffpatch/src}/pipe.ts | 0 .../jsondiffpatch/src}/processor.ts | 0 {src => packages/jsondiffpatch/src}/types.ts | 0 .../jsondiffpatch/test}/examples/diffpatch.ts | 0 .../jsondiffpatch/test}/index.spec.ts | 0 .../jsondiffpatch/test}/tsconfig.json | 0 .../jsondiffpatch/tsconfig.json | 0 58 files changed, 2604 insertions(+), 1546 deletions(-) delete mode 100644 .npmignore delete mode 100644 Makefile rename docs/demo/consoledemo.js => demos/console-demo/demo.ts (88%) create mode 100644 demos/console-demo/package.json create mode 100644 demos/console-demo/tsconfig.json create mode 100644 demos/html-demo/demo.ts rename {docs/demo => demos/html-demo}/index.html (97%) create mode 100644 demos/html-demo/package.json rename {docs/demo => demos/html-demo}/style.css (100%) create mode 100644 demos/html-demo/tsconfig.json create mode 100644 demos/numeric-plugin-demo/numeric-plugin.ts create mode 100644 demos/numeric-plugin-demo/package.json create mode 100644 demos/numeric-plugin-demo/tsconfig.json delete mode 100644 docs/demo/demo.js delete mode 100644 docs/demo/numeric-plugin.js delete mode 100644 docs/index.html rename .babelrc => packages/jsondiffpatch/.babelrc (100%) rename .editorconfig => packages/jsondiffpatch/.editorconfig (100%) rename .eslintignore => packages/jsondiffpatch/.eslintignore (100%) rename .eslintrc.cjs => packages/jsondiffpatch/.eslintrc.cjs (100%) rename {bin => packages/jsondiffpatch/bin}/jsondiffpatch (100%) mode change 100755 => 100644 rename jest.config.cjs => packages/jsondiffpatch/jest.config.cjs (100%) create mode 100644 packages/jsondiffpatch/package.json rename rollup.config.mjs => packages/jsondiffpatch/rollup.config.mjs (76%) rename {src => packages/jsondiffpatch/src}/clone.ts (100%) rename {src => packages/jsondiffpatch/src}/contexts/context.ts (100%) rename {src => packages/jsondiffpatch/src}/contexts/diff.ts (100%) rename {src => packages/jsondiffpatch/src}/contexts/patch.ts (100%) rename {src => packages/jsondiffpatch/src}/contexts/reverse.ts (100%) rename {src => packages/jsondiffpatch/src}/date-reviver.ts (100%) rename {src => packages/jsondiffpatch/src}/diff-match-patch.d.ts (100%) rename {src => packages/jsondiffpatch/src}/diffpatcher.ts (100%) rename {src => packages/jsondiffpatch/src}/filters/arrays.ts (100%) rename {src => packages/jsondiffpatch/src}/filters/dates.ts (100%) rename {src => packages/jsondiffpatch/src}/filters/lcs.ts (100%) rename {src => packages/jsondiffpatch/src}/filters/nested.ts (100%) rename {src => packages/jsondiffpatch/src}/filters/texts.ts (100%) rename {src => packages/jsondiffpatch/src}/filters/trivial.ts (100%) rename {src => packages/jsondiffpatch/src}/formatters/annotated.ts (100%) rename {src => packages/jsondiffpatch/src}/formatters/base.ts (100%) rename {src => packages/jsondiffpatch/src}/formatters/console.ts (100%) rename {src => packages/jsondiffpatch/src}/formatters/html.ts (99%) rename {src => packages/jsondiffpatch/src}/formatters/index.ts (100%) rename {src => packages/jsondiffpatch/src}/formatters/jsonpatch.ts (100%) rename {docs/formatters-styles => packages/jsondiffpatch/src/formatters/styles}/annotated.css (100%) rename {docs/formatters-styles => packages/jsondiffpatch/src/formatters/styles}/html.css (100%) rename {src => packages/jsondiffpatch/src}/main.ts (100%) rename {src => packages/jsondiffpatch/src}/pipe.ts (100%) rename {src => packages/jsondiffpatch/src}/processor.ts (100%) rename {src => packages/jsondiffpatch/src}/types.ts (100%) rename {test => packages/jsondiffpatch/test}/examples/diffpatch.ts (100%) rename {test => packages/jsondiffpatch/test}/index.spec.ts (100%) rename {test => packages/jsondiffpatch/test}/tsconfig.json (100%) rename tsconfig.json => packages/jsondiffpatch/tsconfig.json (100%) 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/.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..a01a712a 100644 --- a/README.md +++ b/README.md @@ -275,12 +275,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 88% rename from docs/demo/consoledemo.js rename to demos/console-demo/demo.ts index 759ae77e..3bc4f274 100644 --- a/docs/demo/consoledemo.js +++ b/demos/console-demo/demo.ts @@ -1,12 +1,35 @@ -const jsondiffpatch = require('../../dist/jsondiffpatch.cjs.js'); +import jsondiffpatch from 'jsondiffpatch'; 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;' + diff --git a/demos/console-demo/package.json b/demos/console-demo/package.json new file mode 100644 index 00000000..006b089c --- /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.2.2" + } +} diff --git a/demos/console-demo/tsconfig.json b/demos/console-demo/tsconfig.json new file mode 100644 index 00000000..2d25bccc --- /dev/null +++ b/demos/console-demo/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "node16", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./build/", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": false /* Skip type checking all .d.ts files. */ + } +} diff --git a/demos/html-demo/demo.ts b/demos/html-demo/demo.ts new file mode 100644 index 00000000..b8998468 --- /dev/null +++ b/demos/html-demo/demo.ts @@ -0,0 +1,843 @@ +import * as jsondiffpatch from 'jsondiffpatch'; +import Editor = CodeMirror.Editor; + +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: { _id?: string; id?: string; name?: string }, + index, + ) { + if (typeof obj._id !== 'undefined') { + return obj._id; + } + if (typeof obj.id !== 'undefined') { + return obj.id; + } + if (typeof obj.name !== 'undefined') { + return obj.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?: 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 = jsondiffpatch.formatters.html.format( + delta, + left, + )!; + if ( + !(document.getElementById('showunchanged') as HTMLInputElement) + .checked + ) { + jsondiffpatch.formatters.html.hideUnchanged(); + } + dom.runScriptTags(visualdiff); + break; + case 'annotated': + annotateddiff.innerHTML = + jsondiffpatch.formatters.annotated.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 () { + jsondiffpatch.formatters.html.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 97% rename from docs/demo/index.html rename to demos/html-demo/index.html index 9f05a001..26290627 100644 --- a/docs/demo/index.html +++ b/demos/html-demo/index.html @@ -24,10 +24,6 @@ type="text/css" media="screen" /> - + @@ -250,18 +240,20 @@ var jsondiffpatchInstance = require('jsondiffpatch').create({

- From ca72c43350d4962cc839b7c94ef81f2f24ad56fb Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Sat, 2 Dec 2023 16:26:16 -0500 Subject: [PATCH 9/9] Format --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6519d868..124c86a9 100644 --- a/README.md +++ b/README.md @@ -241,19 +241,23 @@ const jsondiffpatchInstance = jsondiffpatch.create({