diff --git a/.babelrc b/.babelrc index 9375ee59..f45b63d4 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,6 @@ { - "presets": [["@babel/preset-env", { "targets": "defaults" }]] + "presets": [ + ["@babel/preset-env", { "targets": "defaults" }], + "@babel/preset-typescript" + ] } diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c780564d..ae317de9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,5 +19,6 @@ jobs: - run: npm ci - run: npm run build - run: npm run test + - run: npm run type-check - run: npm run lint - run: npm run format-check diff --git a/docs/test/index.html b/docs/test/index.html deleted file mode 100644 index 199975b6..00000000 --- a/docs/test/index.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - jsondiffpatch tests - - - - - - - -

jsondiffpatch tests

- -
- - - diff --git a/package-lock.json b/package-lock.json index 5950ac10..9c03069f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@babel/core": "^7.22.10", "@babel/eslint-parser": "^7.22.10", "@babel/preset-env": "^7.22.10", + "@babel/preset-typescript": "^7.22.11", "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-commonjs": "^25.0.4", "@rollup/plugin-node-resolve": "^15.2.0", @@ -30,7 +31,8 @@ "jest": "^29.6.2", "prettier": "^3.0.2", "rollup": "^3.28.0", - "rollup-plugin-visualizer": "^5.9.2" + "rollup-plugin-visualizer": "^5.9.2", + "typescript": "~5.1.6" }, "engines": { "node": ">=8.17.0" @@ -255,9 +257,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz", - "integrity": "sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz", + "integrity": "sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -1308,12 +1310,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", - "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.11.tgz", + "integrity": "sha512-o2+bg7GDS60cJMgz9jWqRUsWkMzLCxp+jFDeDUT5sjRlAxcJWZ2ylNdI7QQ2+CH5hWu7OnN+Cv3htt7AkSf96g==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.9", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -1660,6 +1662,24 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.11.tgz", + "integrity": "sha512-0E4/L+7gfvHub7wsbTv03oRtD69X31LByy44fGmFzbZScpupFByMcgCJ0VbBTkzyjSJKuRoGN8tcijOWKTmqOA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-typescript": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.22.10", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", @@ -1831,6 +1851,25 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/preset-typescript": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.22.11.tgz", + "integrity": "sha512-tWY5wyCZYBGY7IlalfKI1rLiGlIfnwsRHZqlky0HVv8qviwQ1Uo/05M6+s+TcTCVa6Bmoo2uJW5TMFX6Wa4qVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.11", + "@babel/plugin-transform-typescript": "^7.22.11" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/regjsgen": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", @@ -8004,7 +8043,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 15e727a2..f12e0ed9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "browser": "dist/jsondiffpatch.umd.js", "main": "dist/jsondiffpatch.cjs.js", "module": "dist/jsondiffpatch.esm.js", - "types": "./dist/index", + "types": "dist/index.d.ts", "files": [ "dist", "bin" @@ -23,6 +23,7 @@ "test": "jest --coverage", "format": "prettier . --write", "format-check": "prettier . --check", + "type-check": "tsc", "prepack": "npm run build", "prepublishOnly": "npm run test && npm run lint" }, @@ -43,6 +44,7 @@ "@babel/core": "^7.22.10", "@babel/eslint-parser": "^7.22.10", "@babel/preset-env": "^7.22.10", + "@babel/preset-typescript": "^7.22.11", "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-commonjs": "^25.0.4", "@rollup/plugin-node-resolve": "^15.2.0", @@ -54,7 +56,8 @@ "jest": "^29.6.2", "prettier": "^3.0.2", "rollup": "^3.28.0", - "rollup-plugin-visualizer": "^5.9.2" + "rollup-plugin-visualizer": "^5.9.2", + "typescript": "~5.1.6" }, "license": "MIT", "engines": { diff --git a/rollup.config.mjs b/rollup.config.mjs index eed79c89..48208a7d 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -9,12 +9,11 @@ import { visualizer } from 'rollup-plugin-visualizer'; import pkg from './package.json' assert { type: 'json' }; -const copySrcFileToDist = copyFromFolderToDist('src'); const copyDocsFileToDist = copyFromFolderToDist('docs'); export default [ { - input: 'src/main.js', + input: 'src/index.ts', external: ['chalk'], output: { name: pkg.name, @@ -32,13 +31,14 @@ export default [ babel({ exclude: 'node_modules/**', babelHelpers: 'bundled', + extensions: ['.js', '.jsx', '.es6', '.es', '.mjs', '.ts'], }), - resolve(), // so Rollup can find node modules - commonjs(), // so Rollup can convert node modules to ES modules + resolve({ extensions: ['.mjs', '.js', '.json', '.node', '.ts'] }), + commonjs(), ], }, { - input: 'src/main.js', + input: 'src/index.ts', external: ['chalk', 'diff-match-patch'], output: { name: pkg.name, @@ -61,13 +61,14 @@ export default [ babel({ exclude: 'node_modules/**', babelHelpers: 'bundled', + extensions: ['.js', '.jsx', '.es6', '.es', '.mjs', '.ts'], }), - resolve(), // so Rollup can find node modules - commonjs(), // so Rollup can convert node modules to ES modules + resolve({ extensions: ['.mjs', '.js', '.json', '.node', '.ts'] }), + commonjs(), ], }, { - input: 'src/main.js', + input: 'src/index.ts', external: [ // external node modules 'diff-match-patch', @@ -77,8 +78,9 @@ export default [ babel({ exclude: 'node_modules/**', babelHelpers: 'bundled', + extensions: ['.js', '.jsx', '.es6', '.es', '.mjs', '.ts'], }), - copySrcFileToDist('index.d.ts'), + resolve({ extensions: ['.mjs', '.js', '.json', '.node', '.ts'] }), copyDocsFileToDist('formatters-styles/annotated.css'), copyDocsFileToDist('formatters-styles/html.css'), ], diff --git a/src/clone.js b/src/clone.ts similarity index 59% rename from src/clone.js rename to src/clone.ts index 3d4d5310..d5fc22e6 100644 --- a/src/clone.js +++ b/src/clone.ts @@ -1,21 +1,16 @@ -const isArray = - typeof Array.isArray === 'function' - ? Array.isArray - : (a) => a instanceof Array; - -function cloneRegExp(re) { - const regexMatch = /^\/(.*)\/([gimyu]*)$/.exec(re.toString()); +function cloneRegExp(re: RegExp) { + const regexMatch = /^\/(.*)\/([gimyu]*)$/.exec(re.toString())!; return new RegExp(regexMatch[1], regexMatch[2]); } -export default function clone(arg) { +export default function clone(arg: unknown): unknown { if (typeof arg !== 'object') { return arg; } if (arg === null) { return null; } - if (isArray(arg)) { + if (Array.isArray(arg)) { return arg.map(clone); } if (arg instanceof Date) { @@ -27,7 +22,9 @@ export default function clone(arg) { const cloned = {}; for (const name in arg) { if (Object.prototype.hasOwnProperty.call(arg, name)) { - cloned[name] = clone(arg[name]); + (cloned as { [name: string]: unknown })[name] = clone( + (arg as { [name: string]: unknown })[name], + ); } } return cloned; diff --git a/src/contexts/context.js b/src/contexts/context.ts similarity index 58% rename from src/contexts/context.js rename to src/contexts/context.ts index f31abd2d..bc18b730 100644 --- a/src/contexts/context.js +++ b/src/contexts/context.ts @@ -1,7 +1,21 @@ import Pipe from '../pipe'; +import { Options } from '../processor'; -export default class Context { - setResult(result) { +export default class Context { + result?: TResult; + hasResult?: boolean; + exiting?: boolean; + nextPipe?: string | Pipe; + parent?: this; + childName?: string | number; + root?: this; + options?: Options; + children?: this[]; + nextAfterChildren?: this | null; + next?: this | null; + pipe?: string; + + setResult(result: TResult) { this.result = result; this.hasResult = true; return this; @@ -12,7 +26,9 @@ export default class Context { return this; } - switchTo(next, pipe) { + switchTo(next: string | Pipe): this; + switchTo(next: this, pipe?: string | Pipe): this; + switchTo(next: string | Pipe | this, pipe?: string | Pipe) { if (typeof next === 'string' || next instanceof Pipe) { this.nextPipe = next; } else { @@ -24,7 +40,7 @@ export default class Context { return this; } - push(child, name) { + push(child: this, name: string | number) { child.parent = this; if (typeof name !== 'undefined') { child.childName = name; diff --git a/src/contexts/diff.js b/src/contexts/diff.js deleted file mode 100644 index 5cd82435..00000000 --- a/src/contexts/diff.js +++ /dev/null @@ -1,29 +0,0 @@ -import Context from './context'; -import defaultClone from '../clone'; - -class DiffContext extends Context { - constructor(left, right) { - super(); - this.left = left; - this.right = right; - this.pipe = 'diff'; - } - - setResult(result) { - if (this.options.cloneDiffValues && typeof result === 'object') { - const clone = - typeof this.options.cloneDiffValues === 'function' - ? this.options.cloneDiffValues - : defaultClone; - if (typeof result[0] === 'object') { - result[0] = clone(result[0]); - } - if (typeof result[1] === 'object') { - result[1] = clone(result[1]); - } - } - return Context.prototype.setResult.apply(this, arguments); - } -} - -export default DiffContext; diff --git a/src/contexts/diff.ts b/src/contexts/diff.ts new file mode 100644 index 00000000..d903adf8 --- /dev/null +++ b/src/contexts/diff.ts @@ -0,0 +1,65 @@ +import Context from './context'; +import defaultClone from '../clone'; + +export type AddedDelta = [unknown]; +export type ModifiedDelta = [unknown, unknown]; +export type DeletedDelta = [unknown, 0, 0]; + +export interface ObjectDelta { + [property: string]: Delta; +} + +export interface ArrayDelta { + _t: 'a'; + [index: `${number}` | `_${number}`]: Delta; +} + +export type MovedDelta = [unknown, number, 3]; + +export type TextDiffDelta = [string, 0, 2]; + +export type Delta = + | AddedDelta + | ModifiedDelta + | DeletedDelta + | ObjectDelta + | ArrayDelta + | MovedDelta + | TextDiffDelta + | undefined; + +class DiffContext extends Context { + left: unknown; + right: unknown; + pipe: 'diff'; + + leftType?: string; + rightType?: string; + leftIsArray?: boolean; + rightIsArray?: boolean; + + constructor(left: unknown, right: unknown) { + super(); + this.left = left; + this.right = right; + this.pipe = 'diff'; + } + + setResult(result: Delta) { + if (this.options!.cloneDiffValues && typeof result === 'object') { + const clone = + typeof this.options!.cloneDiffValues === 'function' + ? this.options!.cloneDiffValues + : defaultClone; + if (typeof result[0] === 'object') { + result[0] = clone(result[0]); + } + if (typeof result[1] === 'object') { + result[1] = clone(result[1]); + } + } + return super.setResult(result); + } +} + +export default DiffContext; diff --git a/src/contexts/patch.js b/src/contexts/patch.js deleted file mode 100644 index f6d55722..00000000 --- a/src/contexts/patch.js +++ /dev/null @@ -1,12 +0,0 @@ -import Context from './context'; - -class PatchContext extends Context { - constructor(left, delta) { - super(); - this.left = left; - this.delta = delta; - this.pipe = 'patch'; - } -} - -export default PatchContext; diff --git a/src/contexts/patch.ts b/src/contexts/patch.ts new file mode 100644 index 00000000..a67f35f5 --- /dev/null +++ b/src/contexts/patch.ts @@ -0,0 +1,18 @@ +import Context from './context'; +import { Delta } from './diff'; + +class PatchContext extends Context { + left: unknown; + delta: Delta; + pipe: 'patch'; + nested?: boolean; + + constructor(left: unknown, delta: Delta) { + super(); + this.left = left; + this.delta = delta; + this.pipe = 'patch'; + } +} + +export default PatchContext; diff --git a/src/contexts/reverse.js b/src/contexts/reverse.js deleted file mode 100644 index d49d6cb9..00000000 --- a/src/contexts/reverse.js +++ /dev/null @@ -1,11 +0,0 @@ -import Context from './context'; - -class ReverseContext extends Context { - constructor(delta) { - super(); - this.delta = delta; - this.pipe = 'reverse'; - } -} - -export default ReverseContext; diff --git a/src/contexts/reverse.ts b/src/contexts/reverse.ts new file mode 100644 index 00000000..2c6d6708 --- /dev/null +++ b/src/contexts/reverse.ts @@ -0,0 +1,17 @@ +import Context from './context'; +import { Delta } from './diff'; + +class ReverseContext extends Context { + delta: Delta; + pipe: 'reverse'; + nested?: boolean; + newName?: string; + + constructor(delta: Delta) { + super(); + this.delta = delta; + this.pipe = 'reverse'; + } +} + +export default ReverseContext; diff --git a/src/date-reviver.js b/src/date-reviver.ts similarity index 89% rename from src/date-reviver.js rename to src/date-reviver.ts index c4d74ed2..4830ed37 100644 --- a/src/date-reviver.js +++ b/src/date-reviver.ts @@ -1,5 +1,5 @@ // use as 2nd parameter for JSON.parse to revive Date instances -export default function dateReviver(key, value) { +export default function dateReviver(key: string, value: unknown) { let parts; if (typeof value === 'string') { // eslint-disable-next-line max-len diff --git a/src/diff-match-patch.d.ts b/src/diff-match-patch.d.ts new file mode 100644 index 00000000..c635fc76 --- /dev/null +++ b/src/diff-match-patch.d.ts @@ -0,0 +1,128 @@ +declare module 'diff-match-patch' { + namespace diff_match_patch { + type Diff = [number, string]; + + interface patch_obj { + diffs: Diff[]; + start1: number | null; + start2: number | null; + length1: number; + length2: number; + } + } + + class diff_match_patch { + Diff_Timeout: number; + Diff_EditCost: number; + Match_Threshold: number; + Match_Distance: number; + Patch_DeleteThreshold: number; + Patch_Margin: number; + Match_MaxBits: number; + + diff_main( + text1: string, + text2: string, + opt_checklines?: boolean, + opt_deadline?: number, + ): diff_match_patch.Diff[]; + + diff_bisect_( + text1: string, + text2: string, + deadline: number, + ): diff_match_patch.Diff[]; + + diff_linesToChars_( + text1: string, + text2: string, + ): { chars1: string; chars2: string; lineArray: string[] }; + + diff_charsToLines_( + diffs: diff_match_patch.Diff[], + lineArray: string[], + ): void; + + diff_commonPrefix(text1: string, text2: string): number; + + diff_commonSuffix(text1: string, text2: string): number; + + diff_commonOverlap_(text1: string, text2: string): number; + + diff_halfMatch_(text1: string, text2: string): string[]; + + diff_cleanupSemantic(diffs: diff_match_patch.Diff[]): void; + + diff_cleanupSemanticLossless(diffs: diff_match_patch.Diff[]): void; + + diff_cleanupEfficiency(diffs: diff_match_patch.Diff[]): void; + + diff_cleanupMerge(diffs: diff_match_patch.Diff[]): void; + + diff_xIndex(diffs: diff_match_patch.Diff[], loc: number): number; + + diff_prettyHtml(diffs: diff_match_patch.Diff[]): string; + + diff_text1(diffs: diff_match_patch.Diff[]): string; + + diff_text2(diffs: diff_match_patch.Diff[]): string; + + diff_levenshtein(diffs: diff_match_patch.Diff[]): number; + + diff_toDelta(diffs: diff_match_patch.Diff[]): string; + + diff_fromDelta(text1: string, delta: string): diff_match_patch.Diff[]; + + match_main(text: string, pattern: string, loc: number): number; + + match_bitap_(text: string, pattern: string, loc: number): number; + + match_alphabet_(pattern: string): { [char: string]: number }; + + patch_addContext_( + patch: typeof diff_match_patch.patch_obj, + text: string, + ): void; + + patch_make( + a: string, + opt_b: string | diff_match_patch.Diff[], + ): Array; + patch_make( + a: diff_match_patch.Diff[], + ): Array; + patch_make( + a: string, + opt_b: string, + opt_c: diff_match_patch.Diff[], + ): Array; + + patch_deepCopy( + patches: Array, + ): Array; + + patch_apply( + patches: Array, + text: string, + ): [string, boolean[]]; + + patch_addPadding(patches: Array): string; + + patch_splitMax(patches: Array): void; + + patch_fromText(text: string): Array; + + patch_toText(patches: Array): string; + + static patch_obj: { + new (): diff_match_patch.patch_obj; + }; + + static diff_match_patch: typeof diff_match_patch; + static DIFF_DELETE: -1; + static DIFF_INSERT: 1; + static DIFF_EQUAL: 0; + } + + export = diff_match_patch; +} diff --git a/src/diffpatcher.js b/src/diffpatcher.ts similarity index 70% rename from src/diffpatcher.js rename to src/diffpatcher.ts index 660232d7..8eb51722 100644 --- a/src/diffpatcher.js +++ b/src/diffpatcher.ts @@ -1,6 +1,6 @@ -import Processor from './processor'; +import Processor, { Options } from './processor'; import Pipe from './pipe'; -import DiffContext from './contexts/diff'; +import DiffContext, { Delta } from './contexts/diff'; import PatchContext from './contexts/patch'; import ReverseContext from './contexts/reverse'; import clone from './clone'; @@ -12,10 +12,12 @@ import * as dates from './filters/dates'; import * as texts from './filters/texts'; class DiffPatcher { - constructor(options) { + processor: Processor; + + constructor(options?: Options) { this.processor = new Processor(options); this.processor.pipe( - new Pipe('diff') + new Pipe('diff') .append( nested.collectChildrenDiffFilter, trivial.diffFilter, @@ -24,10 +26,10 @@ class DiffPatcher { nested.objectsDiffFilter, arrays.diffFilter, ) - .shouldHaveResult(), + .shouldHaveResult()!, ); this.processor.pipe( - new Pipe('patch') + new Pipe('patch') .append( nested.collectChildrenPatchFilter, arrays.collectChildrenPatchFilter, @@ -36,10 +38,10 @@ class DiffPatcher { nested.patchFilter, arrays.patchFilter, ) - .shouldHaveResult(), + .shouldHaveResult()!, ); this.processor.pipe( - new Pipe('reverse') + new Pipe('reverse') .append( nested.collectChildrenReverseFilter, arrays.collectChildrenReverseFilter, @@ -48,31 +50,31 @@ class DiffPatcher { nested.reverseFilter, arrays.reverseFilter, ) - .shouldHaveResult(), + .shouldHaveResult()!, ); } - options(...args) { - return this.processor.options(...args); + options(options: Options): Options { + return this.processor.options(options); } - diff(left, right) { + diff(left: unknown, right: unknown): Delta { return this.processor.process(new DiffContext(left, right)); } - patch(left, delta) { + patch(left: unknown, delta: Delta): unknown { return this.processor.process(new PatchContext(left, delta)); } - reverse(delta) { + reverse(delta: Delta): Delta { return this.processor.process(new ReverseContext(delta)); } - unpatch(right, delta) { + unpatch(right: unknown, delta: Delta): unknown { return this.patch(right, this.reverse(delta)); } - clone(value) { + clone(value: unknown): unknown { return clone(value); } } diff --git a/src/filters/arrays.js b/src/filters/arrays.ts similarity index 64% rename from src/filters/arrays.js rename to src/filters/arrays.ts index 477d26f9..ddcca88e 100644 --- a/src/filters/arrays.js +++ b/src/filters/arrays.ts @@ -1,30 +1,24 @@ -import DiffContext from '../contexts/diff'; +import DiffContext, { + AddedDelta, + ArrayDelta, + DeletedDelta, + Delta, + MovedDelta, +} from '../contexts/diff'; import PatchContext from '../contexts/patch'; import ReverseContext from '../contexts/reverse'; import lcs from './lcs'; +import { Filter } from '../pipe'; const ARRAY_MOVE = 3; -const isArray = - typeof Array.isArray === 'function' - ? Array.isArray - : (a) => a instanceof Array; - -const arrayIndexOf = - typeof Array.prototype.indexOf === 'function' - ? (array, item) => array.indexOf(item) - : (array, item) => { - const length = array.length; - for (let i = 0; i < length; i++) { - if (array[i] === item) { - return i; - } - } - return -1; - }; - -function arraysHaveMatchByRef(array1, array2, len1, len2) { +function arraysHaveMatchByRef( + array1: unknown[], + array2: unknown[], + len1: number, + len2: number, +) { for (let index1 = 0; index1 < len1; index1++) { const val1 = array1[index1]; for (let index2 = 0; index2 < len2; index2++) { @@ -36,7 +30,20 @@ function arraysHaveMatchByRef(array1, array2, len1, len2) { } } -function matchItems(array1, array2, index1, index2, context) { +export interface MatchContext { + objectHash: ((item: object, index?: number) => string) | undefined; + matchByPosition: boolean | undefined; + hashCache1?: string[]; + hashCache2?: string[]; +} + +function matchItems( + array1: unknown[], + array2: unknown[], + index1: number, + index2: number, + context: MatchContext, +) { const value1 = array1[index1]; const value2 = array2[index2]; if (value1 === value2) { @@ -56,10 +63,10 @@ function matchItems(array1, array2, index1, index2, context) { context.hashCache1 = context.hashCache1 || []; hash1 = context.hashCache1[index1]; if (typeof hash1 === 'undefined') { - context.hashCache1[index1] = hash1 = objectHash(value1, index1); + context.hashCache1[index1] = hash1 = objectHash(value1!, index1); } } else { - hash1 = objectHash(value1); + hash1 = objectHash(value1!); } if (typeof hash1 === 'undefined') { return false; @@ -68,10 +75,10 @@ function matchItems(array1, array2, index1, index2, context) { context.hashCache2 = context.hashCache2 || []; hash2 = context.hashCache2[index2]; if (typeof hash2 === 'undefined') { - context.hashCache2[index2] = hash2 = objectHash(value2, index2); + context.hashCache2[index2] = hash2 = objectHash(value2!, index2); } } else { - hash2 = objectHash(value2); + hash2 = objectHash(value2!); } if (typeof hash2 === 'undefined') { return false; @@ -79,12 +86,14 @@ function matchItems(array1, array2, index1, index2, context) { return hash1 === hash2; } -export const diffFilter = function arraysDiffFilter(context) { +export const diffFilter: Filter = function arraysDiffFilter( + context, +) { if (!context.leftIsArray) { return; } - const matchContext = { + const matchContext: MatchContext = { objectHash: context.options && context.options.objectHash, matchByPosition: context.options && context.options.matchByPosition, }; @@ -93,8 +102,8 @@ export const diffFilter = function arraysDiffFilter(context) { let index; let index1; let index2; - const array1 = context.left; - const array2 = context.right; + const array1 = context.left as unknown[]; + const array2 = context.right as unknown[]; const len1 = array1.length; const len2 = array2.length; @@ -121,7 +130,10 @@ export const diffFilter = function arraysDiffFilter(context) { matchItems(array1, array2, commonHead, commonHead, matchContext) ) { index = commonHead; - child = new DiffContext(context.left[index], context.right[index]); + child = new DiffContext( + (context.left as unknown[])[index], + (context.right as unknown[])[index], + ); context.push(child, index); commonHead++; } @@ -139,11 +151,20 @@ export const diffFilter = function arraysDiffFilter(context) { ) { index1 = len1 - 1 - commonTail; index2 = len2 - 1 - commonTail; - child = new DiffContext(context.left[index1], context.right[index2]); + child = new DiffContext( + (context.left as unknown[])[index1], + (context.right as unknown[])[index2], + ); context.push(child, index2); commonTail++; } - let result; + let result: + | { + _t: 'a'; + [index: `${number}`]: AddedDelta; + [index: `_${number}`]: DeletedDelta; + } + | undefined; if (commonHead + commonTail === len1) { if (len1 === len2) { // arrays are identical @@ -155,7 +176,7 @@ export const diffFilter = function arraysDiffFilter(context) { _t: 'a', }; for (index = commonHead; index < len2 - commonTail; index++) { - result[index] = [array2[index]]; + result[`${index}`] = [array2[index]]; } context.setResult(result).exit(); return; @@ -184,7 +205,7 @@ export const diffFilter = function arraysDiffFilter(context) { _t: 'a', }; for (index = commonHead; index < len1 - commonTail; index++) { - if (arrayIndexOf(seq.indices1, index - commonHead) < 0) { + if (seq.indices1.indexOf(index - commonHead) < 0) { // removed result[`_${index}`] = [array1[index], 0, 0]; removedItems.push(index); @@ -210,7 +231,7 @@ export const diffFilter = function arraysDiffFilter(context) { const removedItemsLength = removedItems.length; for (index = commonHead; index < len2 - commonTail; index++) { - const indexOnArray2 = arrayIndexOf(seq.indices2, index - commonHead); + const indexOnArray2 = seq.indices2.indexOf(index - commonHead); if (indexOnArray2 < 0) { // added, try to match with a removed item and register as position move let isMove = false; @@ -239,8 +260,8 @@ export const diffFilter = function arraysDiffFilter(context) { index2 = index; child = new DiffContext( - context.left[index1], - context.right[index2], + (context.left as unknown[])[index1], + (context.right as unknown[])[index2], ); context.push(child, index2); removedItems.splice(removeItemIndex1, 1); @@ -251,13 +272,16 @@ export const diffFilter = function arraysDiffFilter(context) { } if (!isMove) { // added - result[index] = [array2[index]]; + result[`${index}`] = [array2[index]]; } } else { // match, do inner diff index1 = seq.indices1[indexOnArray2] + commonHead; index2 = seq.indices2[indexOnArray2] + commonHead; - child = new DiffContext(context.left[index1], context.right[index2]); + child = new DiffContext( + (context.left as unknown[])[index1], + (context.right as unknown[])[index2], + ); context.push(child, index2); } } @@ -267,55 +291,67 @@ export const diffFilter = function arraysDiffFilter(context) { diffFilter.filterName = 'arrays'; const compare = { - numerically(a, b) { + numerically(a: number, b: number) { return a - b; }, - numericallyBy(name) { - return (a, b) => a[name] - b[name]; + numericallyBy( + name: { [K in keyof T]: T[K] extends number ? K : never }[keyof T], + ) { + return (a: T, b: T) => (a[name] as number) - (b[name] as number); }, }; -export const patchFilter = function nestedPatchFilter(context) { +interface ToInsert { + index: number; + value: unknown; +} + +export const patchFilter: Filter = function nestedPatchFilter( + context, +) { if (!context.nested) { return; } - if (context.delta._t !== 'a') { + if ((context.delta as ArrayDelta)._t !== 'a') { return; } let index; let index1; - const delta = context.delta; - const array = context.left; + const delta = context.delta as ArrayDelta; + const array = context.left as unknown[]; // first, separate removals, insertions and modifications - let toRemove = []; - let toInsert = []; + let toRemove: number[] = []; + let toInsert: ToInsert[] = []; const toModify = []; for (index in delta) { if (index !== '_t') { if (index[0] === '_') { // removed item from original array - if (delta[index][2] === 0 || delta[index][2] === ARRAY_MOVE) { + if ( + delta[index as `_${number}`]![2] === 0 || + delta[index as `_${number}`]![2] === ARRAY_MOVE + ) { toRemove.push(parseInt(index.slice(1), 10)); } else { throw new Error( 'only removal or move can be applied at original array indices,' + - ` invalid diff type: ${delta[index][2]}`, + ` invalid diff type: ${delta[index as `_${number}`]![2]}`, ); } } else { - if (delta[index].length === 1) { + if ((delta[index as `${number}`]! as unknown[]).length === 1) { // added item at new array toInsert.push({ index: parseInt(index, 10), - value: delta[index][0], + value: delta[index as `${number}`]![0], }); } else { // modified item at new array toModify.push({ index: parseInt(index, 10), - delta: delta[index], + delta: delta[index as `${number}`]!, }); } } @@ -326,12 +362,12 @@ export const patchFilter = function nestedPatchFilter(context) { toRemove = toRemove.sort(compare.numerically); for (index = toRemove.length - 1; index >= 0; index--) { index1 = toRemove[index]; - const indexDiff = delta[`_${index1}`]; + const indexDiff = delta[`_${index1}`]!; const removedValue = array.splice(index1, 1)[0]; if (indexDiff[2] === ARRAY_MOVE) { // reinsert later toInsert.push({ - index: indexDiff[1], + index: (indexDiff as MovedDelta)[1], value: removedValue, }); } @@ -352,7 +388,7 @@ export const patchFilter = function nestedPatchFilter(context) { for (index = 0; index < toModifyLength; index++) { const modification = toModify[index]; child = new PatchContext( - context.left[modification.index], + (context.left as unknown[])[modification.index], modification.delta, ); context.push(child, modification.index); @@ -367,69 +403,75 @@ export const patchFilter = function nestedPatchFilter(context) { }; patchFilter.filterName = 'arrays'; -export const collectChildrenPatchFilter = function collectChildrenPatchFilter( - context, -) { - if (!context || !context.children) { - return; - } - if (context.delta._t !== 'a') { - return; - } - const length = context.children.length; - let child; - for (let index = 0; index < length; index++) { - child = context.children[index]; - context.left[child.childName] = child.result; - } - context.setResult(context.left).exit(); -}; +export const collectChildrenPatchFilter: Filter = + function collectChildrenPatchFilter(context) { + if (!context || !context.children) { + return; + } + if ((context.delta as ArrayDelta)._t !== 'a') { + return; + } + const length = context.children.length; + let child; + for (let index = 0; index < length; index++) { + child = context.children[index]; + (context.left as unknown[])[child.childName as number] = child.result; + } + context.setResult(context.left).exit(); + }; collectChildrenPatchFilter.filterName = 'arraysCollectChildren'; -export const reverseFilter = function arraysReverseFilter(context) { - if (!context.nested) { - if (context.delta[2] === ARRAY_MOVE) { - context.newName = `_${context.delta[1]}`; - context - .setResult([ - context.delta[0], - parseInt(context.childName.substr(1), 10), - ARRAY_MOVE, - ]) - .exit(); +export const reverseFilter: Filter = + function arraysReverseFilter(context) { + if (!context.nested) { + if (context.delta![2] === ARRAY_MOVE) { + context.newName = `_${context.delta![1]}`; + context + .setResult([ + context.delta![0], + parseInt((context.childName as string).substr(1), 10), + ARRAY_MOVE, + ]) + .exit(); + } + return; } - return; - } - if (context.delta._t !== 'a') { - return; - } - let name; - let child; - for (name in context.delta) { - if (name === '_t') { - continue; + if ((context.delta as ArrayDelta)._t !== 'a') { + return; } - child = new ReverseContext(context.delta[name]); - context.push(child, name); - } - context.exit(); -}; + let name; + let child; + for (name in context.delta) { + if (name === '_t') { + continue; + } + child = new ReverseContext( + (context.delta as ArrayDelta)[name as `${number}` | `_${number}`], + ); + context.push(child, name); + } + context.exit(); + }; reverseFilter.filterName = 'arrays'; -const reverseArrayDeltaIndex = (delta, index, itemDelta) => { +const reverseArrayDeltaIndex = ( + delta: ArrayDelta, + index: string | number, + itemDelta: Delta, +) => { if (typeof index === 'string' && index[0] === '_') { return parseInt(index.substr(1), 10); - } else if (isArray(itemDelta) && itemDelta[2] === 0) { + } else if (Array.isArray(itemDelta) && itemDelta[2] === 0) { return `_${index}`; } let reverseIndex = +index; for (const deltaIndex in delta) { - const deltaItem = delta[deltaIndex]; - if (isArray(deltaItem)) { + const deltaItem = delta[deltaIndex as `${number}` | `_${number}`]; + if (Array.isArray(deltaItem)) { if (deltaItem[2] === ARRAY_MOVE) { const moveFromIndex = parseInt(deltaIndex.substr(1), 10); - const moveToIndex = deltaItem[1]; + const moveToIndex = (deltaItem as MovedDelta)[1]; if (moveToIndex === +index) { return moveFromIndex; } @@ -446,7 +488,10 @@ const reverseArrayDeltaIndex = (delta, index, itemDelta) => { if (deleteIndex <= reverseIndex) { reverseIndex++; } - } else if (deltaItem.length === 1 && deltaIndex <= reverseIndex) { + } else if ( + deltaItem.length === 1 && + parseInt(deltaIndex, 10) <= reverseIndex + ) { reverseIndex--; } } @@ -455,33 +500,35 @@ const reverseArrayDeltaIndex = (delta, index, itemDelta) => { return reverseIndex; }; -export function collectChildrenReverseFilter(context) { +export const collectChildrenReverseFilter: Filter = ( + context, +) => { if (!context || !context.children) { return; } - if (context.delta._t !== 'a') { + if ((context.delta as ArrayDelta)._t !== 'a') { return; } const length = context.children.length; let child; - const delta = { + const delta: ArrayDelta = { _t: 'a', }; for (let index = 0; index < length; index++) { child = context.children[index]; - let name = child.newName; + let name: string | number | undefined = child.newName; if (typeof name === 'undefined') { name = reverseArrayDeltaIndex( - context.delta, - child.childName, + context.delta as ArrayDelta, + child.childName!, child.result, ); } - if (delta[name] !== child.result) { - delta[name] = child.result; + if (delta[name as `${number}` | `_${number}`] !== child.result) { + delta[name as `${number}` | `_${number}`] = child.result; } } context.setResult(delta).exit(); -} +}; collectChildrenReverseFilter.filterName = 'arraysCollectChildren'; diff --git a/src/filters/dates.js b/src/filters/dates.ts similarity index 74% rename from src/filters/dates.js rename to src/filters/dates.ts index fc1f8aba..015151e6 100644 --- a/src/filters/dates.js +++ b/src/filters/dates.ts @@ -1,4 +1,9 @@ -export const diffFilter = function datesDiffFilter(context) { +import type { Filter } from '../pipe'; +import DiffContext from '../contexts/diff'; + +export const diffFilter: Filter = function datesDiffFilter( + context, +) { if (context.left instanceof Date) { if (context.right instanceof Date) { if (context.left.getTime() !== context.right.getTime()) { diff --git a/src/filters/lcs.js b/src/filters/lcs.ts similarity index 55% rename from src/filters/lcs.js rename to src/filters/lcs.ts index c61100bf..65fe65f0 100644 --- a/src/filters/lcs.js +++ b/src/filters/lcs.ts @@ -6,17 +6,43 @@ reference: http://en.wikipedia.org/wiki/Longest_common_subsequence_problem */ -const defaultMatch = function (array1, array2, index1, index2) { +import { MatchContext } from './arrays'; + +const defaultMatch = function ( + array1: unknown[], + array2: unknown[], + index1: number, + index2: number, +) { return array1[index1] === array2[index2]; }; -const lengthMatrix = function (array1, array2, match, context) { +const lengthMatrix = function ( + array1: unknown[], + array2: unknown[], + match: ( + array1: unknown[], + array2: unknown[], + index1: number, + index2: number, + context: MatchContext, + ) => boolean | undefined, + context: MatchContext, +) { const len1 = array1.length; const len2 = array2.length; let x, y; // initialize empty matrix of len1+1 x len2+1 - const matrix = new Array(len1 + 1); + const matrix: number[][] & { + match?: ( + array1: unknown[], + array2: unknown[], + index1: number, + index2: number, + context: MatchContext, + ) => boolean | undefined; + } = new Array(len1 + 1); for (x = 0; x < len1 + 1; x++) { matrix[x] = new Array(len2 + 1); for (y = 0; y < len2 + 1; y++) { @@ -37,17 +63,36 @@ const lengthMatrix = function (array1, array2, match, context) { return matrix; }; -const backtrack = function (matrix, array1, array2, context) { +interface Subsequence { + sequence: unknown[]; + indices1: number[]; + indices2: number[]; +} + +const backtrack = function ( + matrix: number[][] & { + match?: ( + array1: unknown[], + array2: unknown[], + index1: number, + index2: number, + context: MatchContext, + ) => boolean | undefined; + }, + array1: unknown[], + array2: unknown[], + context: MatchContext, +) { let index1 = array1.length; let index2 = array2.length; - const subsequence = { + const subsequence: Subsequence = { sequence: [], indices1: [], indices2: [], }; while (index1 !== 0 && index2 !== 0) { - const sameLetter = matrix.match( + const sameLetter = matrix.match!( array1, array2, index1 - 1, @@ -73,7 +118,18 @@ const backtrack = function (matrix, array1, array2, context) { return subsequence; }; -const get = function (array1, array2, match, context) { +const get = function ( + array1: unknown[], + array2: unknown[], + match: ( + array1: unknown[], + array2: unknown[], + index1: number, + index2: number, + context: MatchContext, + ) => boolean | undefined, + context: MatchContext, +) { const innerContext = context || {}; const matrix = lengthMatrix( array1, @@ -81,11 +137,7 @@ const get = function (array1, array2, match, context) { match || defaultMatch, innerContext, ); - const result = backtrack(matrix, array1, array2, innerContext); - if (typeof array1 === 'string' && typeof array2 === 'string') { - result.sequence = result.sequence.join(''); - } - return result; + return backtrack(matrix, array1, array2, innerContext); }; export default { diff --git a/src/filters/nested.js b/src/filters/nested.js deleted file mode 100644 index 3c1e6d5d..00000000 --- a/src/filters/nested.js +++ /dev/null @@ -1,144 +0,0 @@ -import DiffContext from '../contexts/diff'; -import PatchContext from '../contexts/patch'; -import ReverseContext from '../contexts/reverse'; - -export function collectChildrenDiffFilter(context) { - if (!context || !context.children) { - return; - } - const length = context.children.length; - let child; - let result = context.result; - for (let index = 0; index < length; index++) { - child = context.children[index]; - if (typeof child.result === 'undefined') { - continue; - } - result = result || {}; - result[child.childName] = child.result; - } - if (result && context.leftIsArray) { - result._t = 'a'; - } - context.setResult(result).exit(); -} -collectChildrenDiffFilter.filterName = 'collectChildren'; - -export function objectsDiffFilter(context) { - if (context.leftIsArray || context.leftType !== 'object') { - return; - } - - let name; - let child; - const propertyFilter = context.options.propertyFilter; - for (name in context.left) { - if (!Object.prototype.hasOwnProperty.call(context.left, name)) { - continue; - } - if (propertyFilter && !propertyFilter(name, context)) { - continue; - } - child = new DiffContext(context.left[name], context.right[name]); - context.push(child, name); - } - for (name in context.right) { - if (!Object.prototype.hasOwnProperty.call(context.right, name)) { - continue; - } - if (propertyFilter && !propertyFilter(name, context)) { - continue; - } - if (typeof context.left[name] === 'undefined') { - child = new DiffContext(undefined, context.right[name]); - context.push(child, name); - } - } - - if (!context.children || context.children.length === 0) { - context.setResult(undefined).exit(); - return; - } - context.exit(); -} -objectsDiffFilter.filterName = 'objects'; - -export const patchFilter = function nestedPatchFilter(context) { - if (!context.nested) { - return; - } - if (context.delta._t) { - return; - } - let name; - let child; - for (name in context.delta) { - child = new PatchContext(context.left[name], context.delta[name]); - context.push(child, name); - } - context.exit(); -}; -patchFilter.filterName = 'objects'; - -export const collectChildrenPatchFilter = function collectChildrenPatchFilter( - context, -) { - if (!context || !context.children) { - return; - } - if (context.delta._t) { - return; - } - const length = context.children.length; - let child; - for (let index = 0; index < length; index++) { - child = context.children[index]; - if ( - Object.prototype.hasOwnProperty.call(context.left, child.childName) && - child.result === undefined - ) { - delete context.left[child.childName]; - } else if (context.left[child.childName] !== child.result) { - context.left[child.childName] = child.result; - } - } - context.setResult(context.left).exit(); -}; -collectChildrenPatchFilter.filterName = 'collectChildren'; - -export const reverseFilter = function nestedReverseFilter(context) { - if (!context.nested) { - return; - } - if (context.delta._t) { - return; - } - let name; - let child; - for (name in context.delta) { - child = new ReverseContext(context.delta[name]); - context.push(child, name); - } - context.exit(); -}; -reverseFilter.filterName = 'objects'; - -export function collectChildrenReverseFilter(context) { - if (!context || !context.children) { - return; - } - if (context.delta._t) { - return; - } - const length = context.children.length; - let child; - const delta = {}; - for (let index = 0; index < length; index++) { - child = context.children[index]; - if (delta[child.childName] !== child.result) { - delta[child.childName] = child.result; - } - } - context.setResult(delta).exit(); -} -collectChildrenReverseFilter.filterName = 'collectChildren'; diff --git a/src/filters/nested.ts b/src/filters/nested.ts new file mode 100644 index 00000000..0f5558b5 --- /dev/null +++ b/src/filters/nested.ts @@ -0,0 +1,164 @@ +import DiffContext, { ArrayDelta, ObjectDelta } from '../contexts/diff'; +import PatchContext from '../contexts/patch'; +import ReverseContext from '../contexts/reverse'; +import { Filter } from '../pipe'; + +export const collectChildrenDiffFilter: Filter = (context) => { + if (!context || !context.children) { + return; + } + const length = context.children.length; + let child; + let result = context.result; + for (let index = 0; index < length; index++) { + child = context.children[index]; + if (typeof child.result === 'undefined') { + continue; + } + result = result || {}; + (result as ObjectDelta)[child.childName!] = child.result; + } + if (result && context.leftIsArray) { + (result as ArrayDelta)._t = 'a'; + } + context.setResult(result).exit(); +}; +collectChildrenDiffFilter.filterName = 'collectChildren'; + +export const objectsDiffFilter: Filter = (context) => { + if (context.leftIsArray || context.leftType !== 'object') { + return; + } + + let name; + let child; + const propertyFilter = context.options!.propertyFilter; + for (name in context.left as object) { + if (!Object.prototype.hasOwnProperty.call(context.left, name)) { + continue; + } + if (propertyFilter && !propertyFilter(name, context)) { + continue; + } + child = new DiffContext( + (context.left as { [name: string]: unknown })[name], + (context.right as { [name: string]: unknown })[name], + ); + context.push(child, name); + } + for (name in context.right as object) { + if (!Object.prototype.hasOwnProperty.call(context.right, name)) { + continue; + } + if (propertyFilter && !propertyFilter(name, context)) { + continue; + } + if ( + typeof (context.left as { [name: string]: unknown })[name] === 'undefined' + ) { + child = new DiffContext( + undefined, + (context.right as { [name: string]: unknown })[name], + ); + context.push(child, name); + } + } + + if (!context.children || context.children.length === 0) { + context.setResult(undefined).exit(); + return; + } + context.exit(); +}; +objectsDiffFilter.filterName = 'objects'; + +export const patchFilter: Filter = function nestedPatchFilter( + context, +) { + if (!context.nested) { + return; + } + if ((context.delta as ArrayDelta)._t) { + return; + } + let name; + let child; + for (name in context.delta) { + child = new PatchContext( + (context.left as { [name: string]: unknown })[name], + (context.delta as ObjectDelta)[name], + ); + context.push(child, name); + } + context.exit(); +}; +patchFilter.filterName = 'objects'; + +export const collectChildrenPatchFilter: Filter = + function collectChildrenPatchFilter(context) { + if (!context || !context.children) { + return; + } + if ((context.delta as ArrayDelta)._t) { + return; + } + const length = context.children.length; + let child; + for (let index = 0; index < length; index++) { + child = context.children[index]; + if ( + Object.prototype.hasOwnProperty.call(context.left, child.childName!) && + child.result === undefined + ) { + delete (context.left as { [name: string]: unknown })[child.childName!]; + } else if ( + (context.left as { [name: string]: unknown })[child.childName!] !== + child.result + ) { + (context.left as { [name: string]: unknown })[child.childName!] = + child.result; + } + } + context.setResult(context.left).exit(); + }; +collectChildrenPatchFilter.filterName = 'collectChildren'; + +export const reverseFilter: Filter = + function nestedReverseFilter(context) { + if (!context.nested) { + return; + } + if ((context.delta as ArrayDelta)._t) { + return; + } + let name; + let child; + for (name in context.delta) { + child = new ReverseContext((context.delta as ObjectDelta)[name]); + context.push(child, name); + } + context.exit(); + }; +reverseFilter.filterName = 'objects'; + +export const collectChildrenReverseFilter: Filter = ( + context, +) => { + if (!context || !context.children) { + return; + } + if ((context.delta as ArrayDelta)._t) { + return; + } + const length = context.children.length; + let child; + const delta: ObjectDelta = {}; + for (let index = 0; index < length; index++) { + child = context.children[index]; + if (delta[child.childName!] !== child.result) { + delta[child.childName!] = child.result; + } + } + context.setResult(delta).exit(); +}; +collectChildrenReverseFilter.filterName = 'collectChildren'; diff --git a/src/filters/texts.js b/src/filters/texts.ts similarity index 57% rename from src/filters/texts.js rename to src/filters/texts.ts index b7b92c38..1d7117ac 100644 --- a/src/filters/texts.js +++ b/src/filters/texts.ts @@ -1,22 +1,35 @@ /* global diff_match_patch */ import dmp from 'diff-match-patch'; +import { Filter } from '../pipe'; +import DiffContext, { TextDiffDelta } from '../contexts/diff'; +import PatchContext from '../contexts/patch'; +import ReverseContext from '../contexts/reverse'; + +declare global { + const diff_match_patch: typeof dmp | undefined; +} + +interface DiffPatch { + diff: (txt1: string, txt2: string) => string; + patch: (txt1: string, string: string) => string; +} const TEXT_DIFF = 2; const DEFAULT_MIN_LENGTH = 60; -let cachedDiffPatch = null; +let cachedDiffPatch: DiffPatch | null = null; -const getDiffMatchPatch = function (required) { +const getDiffMatchPatch = function (required?: boolean) { /* jshint camelcase: false */ if (!cachedDiffPatch) { - let instance; + let instance: dmp | null | undefined; /* eslint-disable camelcase, new-cap */ if (typeof diff_match_patch !== 'undefined') { // already loaded, probably a browser instance = typeof diff_match_patch === 'function' ? new diff_match_patch() - : new diff_match_patch.diff_match_patch(); + : new (diff_match_patch as typeof dmp).diff_match_patch(); } else if (dmp) { try { instance = dmp && new dmp(); @@ -29,23 +42,27 @@ const getDiffMatchPatch = function (required) { if (!required) { return null; } - const error = new Error('text diff_match_patch library not found'); + const error: Error & { diff_match_patch_not_found?: boolean } = new Error( + 'text diff_match_patch library not found', + ); // eslint-disable-next-line camelcase error.diff_match_patch_not_found = true; throw error; } cachedDiffPatch = { diff: function (txt1, txt2) { - return instance.patch_toText(instance.patch_make(txt1, txt2)); + return instance!.patch_toText(instance!.patch_make(txt1, txt2)); }, patch: function (txt1, patch) { - const results = instance.patch_apply( - instance.patch_fromText(patch), + const results = instance!.patch_apply( + instance!.patch_fromText(patch), txt1, ); for (let i = 0; i < results[1].length; i++) { if (!results[1][i]) { - const error = new Error('text patch failed'); + const error: Error & { textPatchFailed?: boolean } = new Error( + 'text patch failed', + ); error.textPatchFailed = true; } } @@ -56,7 +73,9 @@ const getDiffMatchPatch = function (required) { return cachedDiffPatch; }; -export const diffFilter = function textsDiffFilter(context) { +export const diffFilter: Filter = function textsDiffFilter( + context, +) { if (context.leftType !== 'string') { return; } @@ -65,7 +84,10 @@ export const diffFilter = function textsDiffFilter(context) { context.options.textDiff && context.options.textDiff.minLength) || DEFAULT_MIN_LENGTH; - if (context.left.length < minLength || context.right.length < minLength) { + if ( + (context.left as string).length < minLength || + (context.right as string).length < minLength + ) { context.setResult([context.left, context.right]).exit(); return; } @@ -78,25 +100,37 @@ export const diffFilter = function textsDiffFilter(context) { return; } const diff = diffMatchPatch.diff; - context.setResult([diff(context.left, context.right), 0, TEXT_DIFF]).exit(); + context + .setResult([ + diff(context.left as string, context.right as string), + 0, + TEXT_DIFF, + ]) + .exit(); }; diffFilter.filterName = 'texts'; -export const patchFilter = function textsPatchFilter(context) { +export const patchFilter: Filter = function textsPatchFilter( + context, +) { if (context.nested) { return; } - if (context.delta[2] !== TEXT_DIFF) { + if (context.delta![2] !== TEXT_DIFF) { return; } // text-diff, use a text-patch algorithm - const patch = getDiffMatchPatch(true).patch; - context.setResult(patch(context.left, context.delta[0])).exit(); + const patch = getDiffMatchPatch(true)!.patch; + context + .setResult( + patch(context.left as string, (context.delta as TextDiffDelta)[0]), + ) + .exit(); }; patchFilter.filterName = 'texts'; -const textDeltaReverse = function (delta) { +const textDeltaReverse = function (delta: string) { let i; let l; let line; @@ -109,7 +143,7 @@ const textDeltaReverse = function (delta) { line = lines[i]; const lineStart = line.slice(0, 1); if (lineStart === '@') { - header = headerRegex.exec(line); + header = headerRegex.exec(line)!; lineHeader = i; // fix header @@ -138,15 +172,22 @@ const textDeltaReverse = function (delta) { return lines.join('\n'); }; -export const reverseFilter = function textsReverseFilter(context) { - if (context.nested) { - return; - } - if (context.delta[2] !== TEXT_DIFF) { - return; - } +export const reverseFilter: Filter = + function textsReverseFilter(context) { + if (context.nested) { + return; + } + if (context.delta![2] !== TEXT_DIFF) { + return; + } - // text-diff, use a text-diff algorithm - context.setResult([textDeltaReverse(context.delta[0]), 0, TEXT_DIFF]).exit(); -}; + // text-diff, use a text-diff algorithm + context + .setResult([ + textDeltaReverse((context.delta as TextDiffDelta)[0]), + 0, + TEXT_DIFF, + ]) + .exit(); + }; reverseFilter.filterName = 'texts'; diff --git a/src/filters/trivial.js b/src/filters/trivial.js deleted file mode 100644 index 1d05c6ce..00000000 --- a/src/filters/trivial.js +++ /dev/null @@ -1,114 +0,0 @@ -const isArray = - typeof Array.isArray === 'function' - ? Array.isArray - : function (a) { - return a instanceof Array; - }; - -export const diffFilter = function trivialMatchesDiffFilter(context) { - if (context.left === context.right) { - context.setResult(undefined).exit(); - return; - } - if (typeof context.left === 'undefined') { - if (typeof context.right === 'function') { - throw new Error('functions are not supported'); - } - context.setResult([context.right]).exit(); - return; - } - if (typeof context.right === 'undefined') { - context.setResult([context.left, 0, 0]).exit(); - return; - } - if ( - typeof context.left === 'function' || - typeof context.right === 'function' - ) { - throw new Error('functions are not supported'); - } - context.leftType = context.left === null ? 'null' : typeof context.left; - context.rightType = context.right === null ? 'null' : typeof context.right; - if (context.leftType !== context.rightType) { - context.setResult([context.left, context.right]).exit(); - return; - } - if (context.leftType === 'boolean' || context.leftType === 'number') { - context.setResult([context.left, context.right]).exit(); - return; - } - if (context.leftType === 'object') { - context.leftIsArray = isArray(context.left); - } - if (context.rightType === 'object') { - context.rightIsArray = isArray(context.right); - } - if (context.leftIsArray !== context.rightIsArray) { - context.setResult([context.left, context.right]).exit(); - return; - } - - if (context.left instanceof RegExp) { - if (context.right instanceof RegExp) { - context - .setResult([context.left.toString(), context.right.toString()]) - .exit(); - } else { - context.setResult([context.left, context.right]).exit(); - } - } -}; -diffFilter.filterName = 'trivial'; - -export const patchFilter = function trivialMatchesPatchFilter(context) { - if (typeof context.delta === 'undefined') { - context.setResult(context.left).exit(); - return; - } - context.nested = !isArray(context.delta); - if (context.nested) { - return; - } - if (context.delta.length === 1) { - context.setResult(context.delta[0]).exit(); - return; - } - if (context.delta.length === 2) { - if (context.left instanceof RegExp) { - const regexArgs = /^\/(.*)\/([gimyu]+)$/.exec(context.delta[1]); - if (regexArgs) { - context.setResult(new RegExp(regexArgs[1], regexArgs[2])).exit(); - return; - } - } - context.setResult(context.delta[1]).exit(); - return; - } - if (context.delta.length === 3 && context.delta[2] === 0) { - context.setResult(undefined).exit(); - } -}; -patchFilter.filterName = 'trivial'; - -export const reverseFilter = function trivialReferseFilter(context) { - if (typeof context.delta === 'undefined') { - context.setResult(context.delta).exit(); - return; - } - context.nested = !isArray(context.delta); - if (context.nested) { - return; - } - if (context.delta.length === 1) { - context.setResult([context.delta[0], 0, 0]).exit(); - return; - } - if (context.delta.length === 2) { - context.setResult([context.delta[1], context.delta[0]]).exit(); - return; - } - if (context.delta.length === 3 && context.delta[2] === 0) { - context.setResult([context.delta[0]]).exit(); - } -}; -reverseFilter.filterName = 'trivial'; diff --git a/src/filters/trivial.ts b/src/filters/trivial.ts new file mode 100644 index 00000000..81faa971 --- /dev/null +++ b/src/filters/trivial.ts @@ -0,0 +1,117 @@ +import { Filter } from '../pipe'; +import DiffContext from '../contexts/diff'; +import PatchContext from '../contexts/patch'; +import ReverseContext from '../contexts/reverse'; + +export const diffFilter: Filter = + function trivialMatchesDiffFilter(context) { + if (context.left === context.right) { + context.setResult(undefined).exit(); + return; + } + if (typeof context.left === 'undefined') { + if (typeof context.right === 'function') { + throw new Error('functions are not supported'); + } + context.setResult([context.right]).exit(); + return; + } + if (typeof context.right === 'undefined') { + context.setResult([context.left, 0, 0]).exit(); + return; + } + if ( + typeof context.left === 'function' || + typeof context.right === 'function' + ) { + throw new Error('functions are not supported'); + } + context.leftType = context.left === null ? 'null' : typeof context.left; + context.rightType = context.right === null ? 'null' : typeof context.right; + if (context.leftType !== context.rightType) { + context.setResult([context.left, context.right]).exit(); + return; + } + if (context.leftType === 'boolean' || context.leftType === 'number') { + context.setResult([context.left, context.right]).exit(); + return; + } + if (context.leftType === 'object') { + context.leftIsArray = Array.isArray(context.left); + } + if (context.rightType === 'object') { + context.rightIsArray = Array.isArray(context.right); + } + if (context.leftIsArray !== context.rightIsArray) { + context.setResult([context.left, context.right]).exit(); + return; + } + + if (context.left instanceof RegExp) { + if (context.right instanceof RegExp) { + context + .setResult([context.left.toString(), context.right.toString()]) + .exit(); + } else { + context.setResult([context.left, context.right]).exit(); + } + } + }; +diffFilter.filterName = 'trivial'; + +export const patchFilter: Filter = + function trivialMatchesPatchFilter(context) { + if (typeof context.delta === 'undefined') { + context.setResult(context.left).exit(); + return; + } + context.nested = !Array.isArray(context.delta); + if (context.nested) { + return; + } + if ((context.delta as unknown[]).length === 1) { + context.setResult(context.delta[0]).exit(); + return; + } + if ((context.delta as unknown[]).length === 2) { + if (context.left instanceof RegExp) { + const regexArgs = /^\/(.*)\/([gimyu]+)$/.exec( + context.delta[1] as string, + ); + if (regexArgs) { + context.setResult(new RegExp(regexArgs[1], regexArgs[2])).exit(); + return; + } + } + context.setResult(context.delta[1]).exit(); + return; + } + if ((context.delta as unknown[]).length === 3 && context.delta[2] === 0) { + context.setResult(undefined).exit(); + } + }; +patchFilter.filterName = 'trivial'; + +export const reverseFilter: Filter = + function trivialReferseFilter(context) { + if (typeof context.delta === 'undefined') { + context.setResult(context.delta).exit(); + return; + } + context.nested = !Array.isArray(context.delta); + if (context.nested) { + return; + } + if ((context.delta as unknown[]).length === 1) { + context.setResult([context.delta[0], 0, 0]).exit(); + return; + } + if ((context.delta as unknown[]).length === 2) { + context.setResult([context.delta[1], context.delta[0]]).exit(); + return; + } + if ((context.delta as unknown[]).length === 3 && context.delta[2] === 0) { + context.setResult([context.delta[0]]).exit(); + } + }; +reverseFilter.filterName = 'trivial'; diff --git a/src/formatters/annotated.js b/src/formatters/annotated.ts similarity index 50% rename from src/formatters/annotated.js rename to src/formatters/annotated.ts index 155cc832..66ce3563 100644 --- a/src/formatters/annotated.js +++ b/src/formatters/annotated.ts @@ -1,12 +1,33 @@ -import BaseFormatter from './base'; +import BaseFormatter, { + BaseFormatterContext, + DeltaType, + NodeType, +} from './base'; +import { + AddedDelta, + ArrayDelta, + DeletedDelta, + Delta, + ModifiedDelta, + MovedDelta, + ObjectDelta, + TextDiffDelta, +} from '../contexts/diff'; -class AnnotatedFormatter extends BaseFormatter { +interface AnnotatedFormatterContext extends BaseFormatterContext { + indent: (levels?: number) => void; + indentLevel?: number; + indentPad: string; + row: (json: string, htmlNote?: string) => void; +} + +class AnnotatedFormatter extends BaseFormatter { constructor() { super(); this.includeMoveDestinations = false; } - prepareContext(context) { + prepareContext(context: Partial) { super.prepareContext(context); context.indent = function (levels) { this.indentLevel = @@ -14,25 +35,28 @@ class AnnotatedFormatter extends BaseFormatter { this.indentPad = new Array(this.indentLevel + 1).join('  '); }; context.row = (json, htmlNote) => { - context.out( + context.out!( '' + '
',
       );
-      context.out(context.indentPad);
-      context.out('
');
-      context.out(json);
-      context.out('
'); - context.out(htmlNote); - context.out('
'); + context.out!(context.indentPad!); + context.out!('
');
+      context.out!(json);
+      context.out!('
'); + context.out!(htmlNote!); + context.out!('
'); }; } - typeFormattterErrorFormatter(context, err) { + typeFormattterErrorFormatter( + context: AnnotatedFormatterContext, + err: unknown, + ) { context.row('', `
${err}
`); } - formatTextDiffString(context, value) { + formatTextDiffString(context: AnnotatedFormatterContext, value: string) { const lines = this.parseTextDiff(value); context.out('
    '); for (let i = 0, l = lines.length; i < l; i++) { @@ -57,7 +81,11 @@ class AnnotatedFormatter extends BaseFormatter { context.out('
'); } - rootBegin(context, type, nodeType) { + rootBegin( + context: AnnotatedFormatterContext, + type: DeltaType, + nodeType: NodeType, + ) { context.out(''); if (type === 'node') { context.row('{'); @@ -71,7 +99,7 @@ class AnnotatedFormatter extends BaseFormatter { } } - rootEnd(context, type) { + rootEnd(context: AnnotatedFormatterContext, type: DeltaType) { if (type === 'node') { context.indent(-1); context.row('}'); @@ -79,7 +107,13 @@ class AnnotatedFormatter extends BaseFormatter { context.out('
'); } - nodeBegin(context, key, leftKey, type, nodeType) { + nodeBegin( + context: AnnotatedFormatterContext, + key: string, + leftKey: string | number, + type: DeltaType, + nodeType: NodeType, + ) { context.row(`"${key}": {`); if (type === 'node') { context.indent(); @@ -92,7 +126,14 @@ class AnnotatedFormatter extends BaseFormatter { } } - nodeEnd(context, key, leftKey, type, nodeType, isLast) { + nodeEnd( + context: AnnotatedFormatterContext, + key: string, + leftKey: string | number, + type: DeltaType, + nodeType: NodeType, + isLast: boolean, + ) { if (type === 'node') { context.indent(-1); } @@ -106,19 +147,95 @@ class AnnotatedFormatter extends BaseFormatter { format_movedestination() {} - format_node(context, delta, left) { + format_node( + context: AnnotatedFormatterContext, + delta: ObjectDelta | ArrayDelta, + left: unknown, + ) { // recurse this.formatDeltaChildren(context, delta, left); } + + // TODO Forward more arguments + + format_added( + context: AnnotatedFormatterContext, + delta: AddedDelta, + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) { + formatAnyChange.call(this, context, delta, left, key, leftKey); + } + + format_modified( + context: AnnotatedFormatterContext, + delta: ModifiedDelta, + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) { + formatAnyChange.call(this, context, delta, left, key, leftKey); + } + + format_deleted( + context: AnnotatedFormatterContext, + delta: DeletedDelta, + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) { + formatAnyChange.call(this, context, delta, left, key, leftKey); + } + + format_moved( + context: AnnotatedFormatterContext, + delta: MovedDelta, + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) { + formatAnyChange.call(this, context, delta, left, key, leftKey); + } + + format_textdiff( + context: AnnotatedFormatterContext, + delta: TextDiffDelta, + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) { + formatAnyChange.call(this, context, delta, left, key, leftKey); + } } /* eslint-enable camelcase */ -const wrapPropertyName = (name) => +const wrapPropertyName = (name: string) => `
"${name}"
`; -const deltaAnnotations = { - added(delta, left, key, leftKey) { +interface DeltaTypeAnnotationsMap { + added: AddedDelta; + modified: ModifiedDelta; + deleted: DeletedDelta; + moved: MovedDelta; + textdiff: TextDiffDelta; +} + +const deltaAnnotations: { + [DeltaType in keyof DeltaTypeAnnotationsMap]: ( + delta: DeltaTypeAnnotationsMap[DeltaType], + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) => string; +} = { + added( + delta: AddedDelta, + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) { const formatLegend = '
([newValue])
'; if (typeof leftKey === 'undefined') { return `new value${formatLegend}`; @@ -128,7 +245,12 @@ const deltaAnnotations = { } return `add property ${wrapPropertyName(leftKey)}${formatLegend}`; }, - modified(delta, left, key, leftKey) { + modified( + delta: ModifiedDelta, + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) { const formatLegend = '
([previousValue, newValue])
'; if (typeof leftKey === 'undefined') { return `modify value${formatLegend}`; @@ -138,7 +260,12 @@ const deltaAnnotations = { } return `modify property ${wrapPropertyName(leftKey)}${formatLegend}`; }, - deleted(delta, left, key, leftKey) { + deleted( + delta: DeletedDelta, + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) { const formatLegend = '
([previousValue, 0, 0])
'; if (typeof leftKey === 'undefined') { return `delete value${formatLegend}`; @@ -148,14 +275,24 @@ const deltaAnnotations = { } return `delete property ${wrapPropertyName(leftKey)}${formatLegend}`; }, - moved(delta, left, key, leftKey) { + moved( + delta: MovedDelta, + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) { return ( 'move from ' + `index ${leftKey} to index ${delta[1]}` ); }, - textdiff(delta, left, key, leftKey) { + textdiff( + delta: TextDiffDelta, + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, + ) { const location = typeof leftKey === 'undefined' ? '' @@ -169,12 +306,19 @@ const deltaAnnotations = { }, }; -const formatAnyChange = function (context, delta) { - const deltaType = this.getDeltaType(delta); +const formatAnyChange = function < + TDeltaType extends keyof DeltaTypeAnnotationsMap, +>( + this: AnnotatedFormatter, + context: AnnotatedFormatterContext, + delta: DeltaTypeAnnotationsMap[TDeltaType], + left: unknown, + key: string | undefined, + leftKey: string | number | undefined, +) { + const deltaType = this.getDeltaType(delta) as TDeltaType; const annotator = deltaAnnotations[deltaType]; - const htmlNote = - annotator && - annotator.apply(annotator, Array.prototype.slice.call(arguments, 1)); + const htmlNote = annotator && annotator(delta, left, key, leftKey); let json = JSON.stringify(delta, null, 2); if (deltaType === 'textdiff') { // split text diffs lines @@ -185,21 +329,11 @@ const formatAnyChange = function (context, delta) { context.indent(-1); }; -/* eslint-disable camelcase */ -AnnotatedFormatter.prototype.format_added = formatAnyChange; -AnnotatedFormatter.prototype.format_modified = formatAnyChange; -AnnotatedFormatter.prototype.format_deleted = formatAnyChange; -AnnotatedFormatter.prototype.format_moved = formatAnyChange; -AnnotatedFormatter.prototype.format_textdiff = formatAnyChange; -/* eslint-enable camelcase */ - -/* jshint camelcase: true */ - export default AnnotatedFormatter; -let defaultInstance; +let defaultInstance: AnnotatedFormatter | undefined; -export function format(delta, left) { +export function format(delta: Delta, left: unknown) { if (!defaultInstance) { defaultInstance = new AnnotatedFormatter(); } diff --git a/src/formatters/base.js b/src/formatters/base.js deleted file mode 100644 index abf90028..00000000 --- a/src/formatters/base.js +++ /dev/null @@ -1,264 +0,0 @@ -const isArray = - typeof Array.isArray === 'function' - ? Array.isArray - : (a) => a instanceof Array; - -const getObjectKeys = - typeof Object.keys === 'function' - ? (obj) => Object.keys(obj) - : (obj) => { - const names = []; - for (const property in obj) { - if (Object.prototype.hasOwnProperty.call(obj, property)) { - names.push(property); - } - } - return names; - }; - -const trimUnderscore = (str) => { - if (str.substr(0, 1) === '_') { - return str.slice(1); - } - return str; -}; - -const arrayKeyToSortNumber = (key) => { - if (key === '_t') { - return -1; - } else { - if (key.substr(0, 1) === '_') { - return parseInt(key.slice(1), 10); - } else { - return parseInt(key, 10) + 0.1; - } - } -}; - -const arrayKeyComparer = (key1, key2) => - arrayKeyToSortNumber(key1) - arrayKeyToSortNumber(key2); - -class BaseFormatter { - format(delta, left) { - const context = {}; - this.prepareContext(context); - this.recurse(context, delta, left); - return this.finalize(context); - } - - prepareContext(context) { - context.buffer = []; - context.out = function (...args) { - this.buffer.push(...args); - }; - } - - typeFormattterNotFound(context, deltaType) { - throw new Error(`cannot format delta type: ${deltaType}`); - } - - typeFormattterErrorFormatter(context, err) { - return err.toString(); - } - - finalize({ buffer }) { - if (isArray(buffer)) { - return buffer.join(''); - } - } - - recurse(context, delta, left, key, leftKey, movedFrom, isLast) { - const useMoveOriginHere = delta && movedFrom; - const leftValue = useMoveOriginHere ? movedFrom.value : left; - - if (typeof delta === 'undefined' && typeof key === 'undefined') { - return undefined; - } - - const type = this.getDeltaType(delta, movedFrom); - const nodeType = - type === 'node' ? (delta._t === 'a' ? 'array' : 'object') : ''; - - if (typeof key !== 'undefined') { - this.nodeBegin(context, key, leftKey, type, nodeType, isLast); - } else { - this.rootBegin(context, type, nodeType); - } - - let typeFormattter; - try { - typeFormattter = - this[`format_${type}`] || this.typeFormattterNotFound(context, type); - typeFormattter.call( - this, - context, - delta, - leftValue, - key, - leftKey, - movedFrom, - ); - } catch (err) { - this.typeFormattterErrorFormatter( - context, - err, - delta, - leftValue, - key, - leftKey, - movedFrom, - ); - if (typeof console !== 'undefined' && console.error) { - console.error(err.stack); - } - } - - if (typeof key !== 'undefined') { - this.nodeEnd(context, key, leftKey, type, nodeType, isLast); - } else { - this.rootEnd(context, type, nodeType); - } - } - - formatDeltaChildren(context, delta, left) { - const self = this; - this.forEachDeltaKey(delta, left, (key, leftKey, movedFrom, isLast) => { - self.recurse( - context, - delta[key], - left ? left[leftKey] : undefined, - key, - leftKey, - movedFrom, - isLast, - ); - }); - } - - forEachDeltaKey(delta, left, fn) { - const keys = getObjectKeys(delta); - const arrayKeys = delta._t === 'a'; - const moveDestinations = {}; - let name; - if (typeof left !== 'undefined') { - for (name in left) { - if (Object.prototype.hasOwnProperty.call(left, name)) { - if ( - typeof delta[name] === 'undefined' && - (!arrayKeys || typeof delta[`_${name}`] === 'undefined') - ) { - keys.push(name); - } - } - } - } - // look for move destinations - for (name in delta) { - if (Object.prototype.hasOwnProperty.call(delta, name)) { - const value = delta[name]; - if (isArray(value) && value[2] === 3) { - moveDestinations[value[1].toString()] = { - key: name, - value: left && left[parseInt(name.substr(1))], - }; - if (this.includeMoveDestinations !== false) { - if ( - typeof left === 'undefined' && - typeof delta[value[1]] === 'undefined' - ) { - keys.push(value[1].toString()); - } - } - } - } - } - if (arrayKeys) { - keys.sort(arrayKeyComparer); - } else { - keys.sort(); - } - for (let index = 0, length = keys.length; index < length; index++) { - const key = keys[index]; - if (arrayKeys && key === '_t') { - continue; - } - const leftKey = arrayKeys - ? typeof key === 'number' - ? key - : parseInt(trimUnderscore(key), 10) - : key; - const isLast = index === length - 1; - fn(key, leftKey, moveDestinations[leftKey], isLast); - } - } - - getDeltaType(delta, movedFrom) { - if (typeof delta === 'undefined') { - if (typeof movedFrom !== 'undefined') { - return 'movedestination'; - } - return 'unchanged'; - } - if (isArray(delta)) { - if (delta.length === 1) { - return 'added'; - } - if (delta.length === 2) { - return 'modified'; - } - if (delta.length === 3 && delta[2] === 0) { - return 'deleted'; - } - if (delta.length === 3 && delta[2] === 2) { - return 'textdiff'; - } - if (delta.length === 3 && delta[2] === 3) { - return 'moved'; - } - } else if (typeof delta === 'object') { - return 'node'; - } - return 'unknown'; - } - - parseTextDiff(value) { - const output = []; - const lines = value.split('\n@@ '); - for (let i = 0, l = lines.length; i < l; i++) { - const line = lines[i]; - const lineOutput = { - pieces: [], - }; - const location = /^(?:@@ )?[-+]?(\d+),(\d+)/.exec(line).slice(1); - lineOutput.location = { - line: location[0], - chr: location[1], - }; - const pieces = line.split('\n').slice(1); - for ( - let pieceIndex = 0, piecesLength = pieces.length; - pieceIndex < piecesLength; - pieceIndex++ - ) { - const piece = pieces[pieceIndex]; - if (!piece.length) { - continue; - } - const pieceOutput = { - type: 'context', - }; - if (piece.substr(0, 1) === '+') { - pieceOutput.type = 'added'; - } else if (piece.substr(0, 1) === '-') { - pieceOutput.type = 'deleted'; - } - pieceOutput.text = piece.slice(1); - lineOutput.pieces.push(pieceOutput); - } - output.push(lineOutput); - } - return output; - } -} - -export default BaseFormatter; diff --git a/src/formatters/base.ts b/src/formatters/base.ts new file mode 100644 index 00000000..020bb223 --- /dev/null +++ b/src/formatters/base.ts @@ -0,0 +1,476 @@ +import { + AddedDelta, + ArrayDelta, + DeletedDelta, + Delta, + ModifiedDelta, + MovedDelta, + ObjectDelta, + TextDiffDelta, +} from '../contexts/diff'; + +const trimUnderscore = (str: string) => { + if (str.substr(0, 1) === '_') { + return str.slice(1); + } + return str; +}; + +const arrayKeyToSortNumber = (key: string) => { + if (key === '_t') { + return -1; + } else { + if (key.substr(0, 1) === '_') { + return parseInt(key.slice(1), 10); + } else { + return parseInt(key, 10) + 0.1; + } + } +}; + +const arrayKeyComparer = (key1: string, key2: string) => + arrayKeyToSortNumber(key1) - arrayKeyToSortNumber(key2); + +export interface BaseFormatterContext { + buffer: string[]; + out: (...args: string[]) => void; +} + +export type DeltaType = + | 'movedestination' + | 'unchanged' + | 'added' + | 'modified' + | 'deleted' + | 'textdiff' + | 'moved' + | 'node' + | 'unknown'; + +interface DeltaTypeMap { + movedestination: undefined; + unchanged: undefined; + added: AddedDelta; + modified: ModifiedDelta; + deleted: DeletedDelta; + textdiff: TextDiffDelta; + moved: MovedDelta; + node: ObjectDelta | ArrayDelta; +} + +export type NodeType = 'array' | 'object' | ''; + +interface MoveDestination { + key: string; + value: unknown; +} + +interface LineOutputPiece { + type: 'context' | 'added' | 'deleted'; + text: string; +} + +interface LineOutputLocation { + line: string; + chr: string; +} + +interface LineOutput { + pieces: LineOutputPiece[]; + location: LineOutputLocation; +} + +type FormatMehodMap = {}; + +type Formatter = { + [TDeltaType in keyof DeltaTypeMap as `format_${keyof DeltaTypeMap}`]: ( + context: TContext, + delta: DeltaTypeMap[TDeltaType], + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ) => void; +}; + +abstract class BaseFormatter< + TContext extends BaseFormatterContext, + TFormatted extends unknown = string | undefined, +> { + includeMoveDestinations?: boolean; + + format(delta: Delta, left: unknown): TFormatted { + const context: Partial = {}; + this.prepareContext(context); + this.recurse(context as TContext, delta, left); + return this.finalize(context as TContext) as TFormatted; + } + + prepareContext(context: Partial) { + context.buffer = []; + context.out = function (...args) { + this.buffer!.push(...args); + }; + } + + typeFormattterNotFound(context: TContext, deltaType: 'unknown'): never { + throw new Error(`cannot format delta type: ${deltaType}`); + } + + typeFormattterErrorFormatter( + context: TContext, + err: unknown, + delta: Delta, + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ) {} + + finalize({ buffer }: TContext) { + if (Array.isArray(buffer)) { + return buffer.join(''); + } + } + + recurse( + context: TContext, + delta: DeltaTypeMap[TDeltaType], + left: unknown, + key?: string, + leftKey?: string | number, + movedFrom?: MoveDestination, + isLast?: boolean, + ) { + const useMoveOriginHere = delta && movedFrom; + const leftValue = useMoveOriginHere ? movedFrom.value : left; + + if (typeof delta === 'undefined' && typeof key === 'undefined') { + return undefined; + } + + const type = this.getDeltaType(delta, movedFrom); + const nodeType = + type === 'node' + ? (delta as ArrayDelta)._t === 'a' + ? 'array' + : 'object' + : ''; + + if (typeof key !== 'undefined') { + this.nodeBegin(context, key, leftKey!, type, nodeType, isLast!); + } else { + this.rootBegin(context, type, nodeType); + } + + let typeFormattter: + | (( + context: TContext, + delta: DeltaTypeMap[TDeltaType], + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ) => void) + | undefined; + try { + typeFormattter = + type !== 'unknown' + ? (this as Formatter)[`format_${type}`] + : this.typeFormattterNotFound(context, type); + typeFormattter.call( + this, + context, + delta, + leftValue, + key, + leftKey, + movedFrom, + ); + } catch (err) { + this.typeFormattterErrorFormatter( + context, + err, + delta, + leftValue, + key, + leftKey, + movedFrom, + ); + if (typeof console !== 'undefined' && console.error) { + console.error((err as Error).stack); + } + } + + if (typeof key !== 'undefined') { + this.nodeEnd(context, key, leftKey!, type, nodeType, isLast!); + } else { + this.rootEnd(context, type, nodeType); + } + } + + formatDeltaChildren( + context: TContext, + delta: ObjectDelta | ArrayDelta, + left: unknown, + ) { + const self = this; + this.forEachDeltaKey(delta, left, (key, leftKey, movedFrom, isLast) => { + self.recurse( + context, + (delta as ObjectDelta)[key], + left + ? (left as { [key: string | number]: unknown })[leftKey] + : undefined, + key, + leftKey, + movedFrom, + isLast, + ); + }); + } + + forEachDeltaKey( + delta: Delta, + left: unknown, + fn: ( + key: string, + leftKey: string | number, + moveDestination: MoveDestination, + isLast: boolean, + ) => void, + ) { + const keys = Object.keys(delta!); + const arrayKeys = (delta as ArrayDelta)._t === 'a'; + const moveDestinations: { [destinationIndex: string]: MoveDestination } = + {}; + let name; + if (typeof left !== 'undefined') { + for (name in left) { + if (Object.prototype.hasOwnProperty.call(left, name)) { + if ( + typeof (delta as ObjectDelta)[name] === 'undefined' && + (!arrayKeys || + typeof (delta as ArrayDelta)[`_${name as unknown as number}`] === + 'undefined') + ) { + keys.push(name); + } + } + } + } + // look for move destinations + for (name in delta) { + if (Object.prototype.hasOwnProperty.call(delta, name)) { + const value = (delta as ObjectDelta)[name]; + if (Array.isArray(value) && value[2] === 3) { + moveDestinations[value[1]!.toString()] = { + key: name, + value: left && (left as unknown[])[parseInt(name.substr(1))], + }; + if (this.includeMoveDestinations !== false) { + if ( + typeof left === 'undefined' && + typeof (delta as ObjectDelta)[value[1] as string] === 'undefined' + ) { + keys.push((value[1] as string).toString()); + } + } + } + } + } + if (arrayKeys) { + keys.sort(arrayKeyComparer); + } else { + keys.sort(); + } + for (let index = 0, length = keys.length; index < length; index++) { + const key = keys[index]; + if (arrayKeys && key === '_t') { + continue; + } + const leftKey = arrayKeys + ? typeof key === 'number' + ? key + : parseInt(trimUnderscore(key), 10) + : key; + const isLast = index === length - 1; + fn(key, leftKey, moveDestinations[leftKey], isLast); + } + } + + getDeltaType(delta: Delta, movedFrom?: MoveDestination | undefined) { + if (typeof delta === 'undefined') { + if (typeof movedFrom !== 'undefined') { + return 'movedestination'; + } + return 'unchanged'; + } + if (Array.isArray(delta)) { + if (delta.length === 1) { + return 'added'; + } + if (delta.length === 2) { + return 'modified'; + } + if (delta.length === 3 && delta[2] === 0) { + return 'deleted'; + } + if (delta.length === 3 && delta[2] === 2) { + return 'textdiff'; + } + if (delta.length === 3 && delta[2] === 3) { + return 'moved'; + } + } else if (typeof delta === 'object') { + return 'node'; + } + return 'unknown'; + } + + parseTextDiff(value: string) { + const output = []; + const lines = value.split('\n@@ '); + for (let i = 0, l = lines.length; i < l; i++) { + const line = lines[i]; + const lineOutput: { + pieces: LineOutputPiece[]; + location?: LineOutputLocation; + } = { + pieces: [], + }; + const location = /^(?:@@ )?[-+]?(\d+),(\d+)/.exec(line)!.slice(1); + lineOutput.location = { + line: location[0], + chr: location[1], + }; + const pieces = line.split('\n').slice(1); + for ( + let pieceIndex = 0, piecesLength = pieces.length; + pieceIndex < piecesLength; + pieceIndex++ + ) { + const piece = pieces[pieceIndex]; + if (!piece.length) { + continue; + } + const pieceOutput: Partial = { + type: 'context', + }; + if (piece.substr(0, 1) === '+') { + pieceOutput.type = 'added'; + } else if (piece.substr(0, 1) === '-') { + pieceOutput.type = 'deleted'; + } + pieceOutput.text = piece.slice(1); + lineOutput.pieces.push(pieceOutput as LineOutputPiece); + } + output.push(lineOutput as LineOutput); + } + return output; + } + + abstract rootBegin( + context: TContext, + type: DeltaType, + nodeType: NodeType, + ): void; + + abstract rootEnd( + context: TContext, + type: DeltaType, + nodeType: NodeType, + ): void; + + abstract nodeBegin( + context: TContext, + key: string, + leftKey: string | number, + type: DeltaType, + nodeType: NodeType, + isLast: boolean, + ): void; + + abstract nodeEnd( + context: TContext, + key: string, + leftKey: string | number, + type: DeltaType, + nodeType: NodeType, + isLast: boolean, + ): void; + + abstract format_unchanged( + context: TContext, + delta: undefined, + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ): void; + + abstract format_movedestination( + context: TContext, + delta: undefined, + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ): void; + + abstract format_node( + context: TContext, + delta: ObjectDelta | ArrayDelta, + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ): void; + + abstract format_added( + context: TContext, + delta: AddedDelta, + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ): void; + + abstract format_modified( + context: TContext, + delta: ModifiedDelta, + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ): void; + + abstract format_deleted( + context: TContext, + delta: DeletedDelta, + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ): void; + + abstract format_moved( + context: TContext, + delta: MovedDelta, + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ): void; + + abstract format_textdiff( + context: TContext, + delta: TextDiffDelta, + leftValue: unknown, + key: string | undefined, + leftKey: string | number | undefined, + movedFrom: MoveDestination | undefined, + ): void; +} + +export default BaseFormatter; diff --git a/src/formatters/console.js b/src/formatters/console.ts similarity index 58% rename from src/formatters/console.js rename to src/formatters/console.ts index 81e2b77e..2d42c88f 100644 --- a/src/formatters/console.js +++ b/src/formatters/console.ts @@ -1,9 +1,27 @@ import chalk from 'chalk'; -import BaseFormatter from './base'; +import BaseFormatter, { + BaseFormatterContext, + DeltaType, + NodeType, +} from './base'; +import { + AddedDelta, + ArrayDelta, + DeletedDelta, + Delta, + ModifiedDelta, + MovedDelta, + ObjectDelta, + TextDiffDelta, +} from '../contexts/diff'; -function chalkColor(name) { +interface Color { + (text: string): string; +} + +function chalkColor(name: string): Color { return ( - (chalk && chalk[name]) || + (chalk && chalk[name as typeof chalk.ForegroundColor]) || function (...args) { return args; } @@ -20,22 +38,32 @@ const colors = { textDiffLine: chalkColor('gray'), }; -class ConsoleFormatter extends BaseFormatter { +interface ConsoleFormatterContext extends BaseFormatterContext { + indent: (levels?: number) => void; + indentLevel?: number; + indentPad: string; + outLine: () => void; + color?: Color[]; + pushColor: (color: Color) => void; + popColor: () => void; +} + +class ConsoleFormatter extends BaseFormatter { constructor() { super(); this.includeMoveDestinations = false; } - prepareContext(context) { + prepareContext(context: Partial) { super.prepareContext(context); context.indent = function (levels) { this.indentLevel = (this.indentLevel || 0) + (typeof levels === 'undefined' ? 1 : levels); this.indentPad = new Array(this.indentLevel + 1).join(' '); - this.outLine(); + this.outLine!(); }; context.outLine = function () { - this.buffer.push(`\n${this.indentPad || ''}`); + this.buffer!.push(`\n${this.indentPad || ''}`); }; context.out = function (...args) { for (let i = 0, l = args.length; i < l; i++) { @@ -44,7 +72,7 @@ class ConsoleFormatter extends BaseFormatter { if (this.color && this.color[0]) { text = this.color[0](text); } - this.buffer.push(text); + this.buffer!.push(text); } }; context.pushColor = function (color) { @@ -57,17 +85,17 @@ class ConsoleFormatter extends BaseFormatter { }; } - typeFormattterErrorFormatter(context, err) { + typeFormattterErrorFormatter(context: ConsoleFormatterContext, err: unknown) { context.pushColor(colors.error); context.out(`[ERROR]${err}`); context.popColor(); } - formatValue(context, value) { + formatValue(context: ConsoleFormatterContext, value: unknown) { context.out(JSON.stringify(value, null, 2)); } - formatTextDiffString(context, value) { + formatTextDiffString(context: ConsoleFormatterContext, value: string) { const lines = this.parseTextDiff(value); context.indent(); for (let i = 0, l = lines.length; i < l; i++) { @@ -82,7 +110,7 @@ class ConsoleFormatter extends BaseFormatter { pieceIndex++ ) { const piece = pieces[pieceIndex]; - context.pushColor(colors[piece.type]); + context.pushColor(colors[piece.type as keyof typeof colors]); context.out(piece.text); context.popColor(); } @@ -93,15 +121,23 @@ class ConsoleFormatter extends BaseFormatter { context.indent(-1); } - rootBegin(context, type, nodeType) { - context.pushColor(colors[type]); + rootBegin( + context: ConsoleFormatterContext, + type: DeltaType, + nodeType: NodeType, + ) { + context.pushColor(colors[type as keyof typeof colors]); if (type === 'node') { context.out(nodeType === 'array' ? '[' : '{'); context.indent(); } } - rootEnd(context, type, nodeType) { + rootEnd( + context: ConsoleFormatterContext, + type: DeltaType, + nodeType: NodeType, + ) { if (type === 'node') { context.indent(-1); context.out(nodeType === 'array' ? ']' : '}'); @@ -109,8 +145,14 @@ class ConsoleFormatter extends BaseFormatter { context.popColor(); } - nodeBegin(context, key, leftKey, type, nodeType) { - context.pushColor(colors[type]); + nodeBegin( + context: ConsoleFormatterContext, + key: string, + leftKey: string | number, + type: DeltaType, + nodeType: NodeType, + ) { + context.pushColor(colors[type as keyof typeof colors]); context.out(`${leftKey}: `); if (type === 'node') { context.out(nodeType === 'array' ? '[' : '{'); @@ -118,7 +160,14 @@ class ConsoleFormatter extends BaseFormatter { } } - nodeEnd(context, key, leftKey, type, nodeType, isLast) { + nodeEnd( + context: ConsoleFormatterContext, + key: string, + leftKey: string | number, + type: DeltaType, + nodeType: NodeType, + isLast: boolean, + ) { if (type === 'node') { context.indent(-1); context.out(nodeType === 'array' ? ']' : `}${isLast ? '' : ','}`); @@ -132,30 +181,42 @@ class ConsoleFormatter extends BaseFormatter { /* jshint camelcase: false */ /* eslint-disable camelcase */ - format_unchanged(context, delta, left) { + format_unchanged( + context: ConsoleFormatterContext, + delta: undefined, + left: unknown, + ) { if (typeof left === 'undefined') { return; } this.formatValue(context, left); } - format_movedestination(context, delta, left) { + format_movedestination( + context: ConsoleFormatterContext, + delta: undefined, + left: unknown, + ) { if (typeof left === 'undefined') { return; } this.formatValue(context, left); } - format_node(context, delta, left) { + format_node( + context: ConsoleFormatterContext, + delta: ObjectDelta | ArrayDelta, + left: unknown, + ) { // recurse this.formatDeltaChildren(context, delta, left); } - format_added(context, delta) { + format_added(context: ConsoleFormatterContext, delta: AddedDelta) { this.formatValue(context, delta[0]); } - format_modified(context, delta) { + format_modified(context: ConsoleFormatterContext, delta: ModifiedDelta) { context.pushColor(colors.deleted); this.formatValue(context, delta[0]); context.popColor(); @@ -165,15 +226,15 @@ class ConsoleFormatter extends BaseFormatter { context.popColor(); } - format_deleted(context, delta) { + format_deleted(context: ConsoleFormatterContext, delta: DeletedDelta) { this.formatValue(context, delta[0]); } - format_moved(context, delta) { + format_moved(context: ConsoleFormatterContext, delta: MovedDelta) { context.out(`==> ${delta[1]}`); } - format_textdiff(context, delta) { + format_textdiff(context: ConsoleFormatterContext, delta: TextDiffDelta) { this.formatTextDiffString(context, delta[0]); } } @@ -184,15 +245,15 @@ class ConsoleFormatter extends BaseFormatter { export default ConsoleFormatter; -let defaultInstance; +let defaultInstance: ConsoleFormatter | undefined; -export const format = (delta, left) => { +export const format = (delta: Delta, left: unknown) => { if (!defaultInstance) { defaultInstance = new ConsoleFormatter(); } return defaultInstance.format(delta, left); }; -export function log(delta, left) { +export function log(delta: Delta, left: unknown) { console.log(format(delta, left)); } diff --git a/src/formatters/html.js b/src/formatters/html.ts similarity index 72% rename from src/formatters/html.js rename to src/formatters/html.ts index fb837fce..09ba5d37 100644 --- a/src/formatters/html.js +++ b/src/formatters/html.ts @@ -1,15 +1,33 @@ -import BaseFormatter from './base'; +import BaseFormatter, { + BaseFormatterContext, + DeltaType, + NodeType, +} from './base'; +import { + AddedDelta, + ArrayDelta, + DeletedDelta, + Delta, + ModifiedDelta, + MovedDelta, + ObjectDelta, + TextDiffDelta, +} from '../contexts/diff'; -class HtmlFormatter extends BaseFormatter { - typeFormattterErrorFormatter(context, err) { +interface HtmlFormatterContext extends BaseFormatterContext { + hasArrows?: boolean; +} + +class HtmlFormatter extends BaseFormatter { + typeFormattterErrorFormatter(context: HtmlFormatterContext, err: unknown) { context.out(`
${err}
`); } - formatValue(context, value) { + formatValue(context: HtmlFormatterContext, value: unknown) { context.out(`
${htmlEscape(JSON.stringify(value, null, 2))}
`); } - formatTextDiffString(context, value) { + formatTextDiffString(context: HtmlFormatterContext, value: string) { const lines = this.parseTextDiff(value); context.out('
    '); for (let i = 0, l = lines.length; i < l; i++) { @@ -24,7 +42,6 @@ class HtmlFormatter extends BaseFormatter { pieceIndex < piecesLength; pieceIndex++ ) { - /* global decodeURI */ const piece = pieces[pieceIndex]; context.out( `${htmlEscape( @@ -37,14 +54,18 @@ class HtmlFormatter extends BaseFormatter { context.out('
'); } - rootBegin(context, type, nodeType) { + rootBegin( + context: HtmlFormatterContext, + type: DeltaType, + nodeType: NodeType, + ) { const nodeClass = `jsondiffpatch-${type}${ nodeType ? ` jsondiffpatch-child-node-type-${nodeType}` : '' }`; context.out(`
`); } - rootEnd(context) { + rootEnd(context: HtmlFormatterContext) { context.out( `
${ context.hasArrows @@ -55,7 +76,13 @@ class HtmlFormatter extends BaseFormatter { ); } - nodeBegin(context, key, leftKey, type, nodeType) { + nodeBegin( + context: HtmlFormatterContext, + key: string, + leftKey: string | number, + type: DeltaType, + nodeType: NodeType, + ) { const nodeClass = `jsondiffpatch-${type}${ nodeType ? ` jsondiffpatch-child-node-type-${nodeType}` : '' }`; @@ -65,14 +92,18 @@ class HtmlFormatter extends BaseFormatter { ); } - nodeEnd(context) { + nodeEnd(context: HtmlFormatterContext) { context.out(''); } /* jshint camelcase: false */ /* eslint-disable camelcase */ - format_unchanged(context, delta, left) { + format_unchanged( + context: HtmlFormatterContext, + delta: undefined, + left: unknown, + ) { if (typeof left === 'undefined') { return; } @@ -81,7 +112,11 @@ class HtmlFormatter extends BaseFormatter { context.out(''); } - format_movedestination(context, delta, left) { + format_movedestination( + context: HtmlFormatterContext, + delta: undefined, + left: unknown, + ) { if (typeof left === 'undefined') { return; } @@ -90,7 +125,11 @@ class HtmlFormatter extends BaseFormatter { context.out(''); } - format_node(context, delta, left) { + format_node( + context: HtmlFormatterContext, + delta: ObjectDelta | ArrayDelta, + left: unknown, + ) { // recurse const nodeType = delta._t === 'a' ? 'array' : 'object'; context.out( @@ -100,13 +139,13 @@ class HtmlFormatter extends BaseFormatter { context.out(''); } - format_added(context, delta) { + format_added(context: HtmlFormatterContext, delta: AddedDelta) { context.out('
'); this.formatValue(context, delta[0]); context.out('
'); } - format_modified(context, delta) { + format_modified(context: HtmlFormatterContext, delta: ModifiedDelta) { context.out('
'); this.formatValue(context, delta[0]); context.out( @@ -116,13 +155,13 @@ class HtmlFormatter extends BaseFormatter { context.out('
'); } - format_deleted(context, delta) { + format_deleted(context: HtmlFormatterContext, delta: DeletedDelta) { context.out('
'); this.formatValue(context, delta[0]); context.out('
'); } - format_moved(context, delta) { + format_moved(context: HtmlFormatterContext, delta: MovedDelta) { context.out('
'); this.formatValue(context, delta[0]); context.out( @@ -153,14 +192,14 @@ class HtmlFormatter extends BaseFormatter { context.hasArrows = true; } - format_textdiff(context, delta) { + format_textdiff(context: HtmlFormatterContext, delta: TextDiffDelta) { context.out('
'); this.formatTextDiffString(context, delta[0]); context.out('
'); } } -function htmlEscape(text) { +function htmlEscape(text: string) { let html = text; const replacements = [ [/&/g, '&'], @@ -168,24 +207,33 @@ function htmlEscape(text) { [/>/g, '>'], [/'/g, '''], [/"/g, '"'], - ]; + ] as const; for (let i = 0; i < replacements.length; i++) { html = html.replace(replacements[i][0], replacements[i][1]); } return html; } -const adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows(nodeArg) { +const adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows( + nodeArg?: Element, +) { const node = nodeArg || document; - const getElementText = ({ textContent, innerText }) => + const getElementText = ({ textContent, innerText }: HTMLDivElement) => textContent || innerText; - const eachByQuery = (el, query, fn) => { + const eachByQuery = ( + el: Element | Document, + query: string, + fn: (element: HTMLElement) => void, + ) => { const elems = el.querySelectorAll(query); for (let i = 0, l = elems.length; i < l; i++) { - fn(elems[i]); + fn(elems[i] as HTMLDivElement); } }; - const eachChildren = ({ children }, fn) => { + const eachChildren = ( + { children }: ParentNode, + fn: (child: Element, index: number) => void, + ) => { for (let i = 0, l = children.length; i < l; i++) { fn(children[i], i); } @@ -194,18 +242,18 @@ const adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows(nodeArg) { node, '.jsondiffpatch-arrow', ({ parentNode, children, style }) => { - const arrowParent = parentNode; - const svg = children[0]; - const path = svg.children[1]; + const arrowParent = parentNode as HTMLElement; + const svg = children[0] as SVGSVGElement; + const path = svg.children[1] as SVGPathElement; svg.style.display = 'none'; const destination = getElementText( - arrowParent.querySelector('.jsondiffpatch-moved-destination'), + arrowParent.querySelector('.jsondiffpatch-moved-destination')!, ); - const container = arrowParent.parentNode; - let destinationElem; + const container = arrowParent.parentNode!; + let destinationElem: HTMLElement | undefined; eachChildren(container, (child) => { if (child.getAttribute('data-key') === destination) { - destinationElem = child; + destinationElem = child as HTMLElement; } }); if (!destinationElem) { @@ -213,7 +261,7 @@ const adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows(nodeArg) { } try { const distance = destinationElem.offsetTop - arrowParent.offsetTop; - svg.setAttribute('height', Math.abs(distance) + 6); + svg.setAttribute('height', `${Math.abs(distance) + 6}`); style.top = `${-8 + (distance > 0 ? 0 : distance)}px`; const curve = distance > 0 @@ -229,7 +277,11 @@ const adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows(nodeArg) { /* jshint camelcase: true */ /* eslint-enable camelcase */ -export const showUnchanged = (show, node, delay) => { +export const showUnchanged = ( + show?: boolean, + node?: Element, + delay?: number, +) => { const el = node || document.body; const prefix = 'jsondiffpatch-unchanged-'; const classes = { @@ -283,13 +335,14 @@ export const showUnchanged = (show, node, delay) => { }, delay); }; -export const hideUnchanged = (node, delay) => showUnchanged(false, node, delay); +export const hideUnchanged = (node?: Element, delay?: number) => + showUnchanged(false, node, delay); export default HtmlFormatter; -let defaultInstance; +let defaultInstance: HtmlFormatter | undefined; -export function format(delta, left) { +export function format(delta: Delta, left: unknown) { if (!defaultInstance) { defaultInstance = new HtmlFormatter(); } diff --git a/src/formatters/index.js b/src/formatters/index.ts similarity index 100% rename from src/formatters/index.js rename to src/formatters/index.ts diff --git a/src/formatters/jsonpatch.js b/src/formatters/jsonpatch.js deleted file mode 100644 index ebc28664..00000000 --- a/src/formatters/jsonpatch.js +++ /dev/null @@ -1,180 +0,0 @@ -import BaseFormatter from './base'; - -const OPERATIONS = { - add: 'add', - remove: 'remove', - replace: 'replace', - move: 'move', -}; - -class JSONFormatter extends BaseFormatter { - constructor() { - super(); - this.includeMoveDestinations = true; - } - - prepareContext(context) { - super.prepareContext(context); - context.result = []; - context.path = []; - context.pushCurrentOp = function (obj) { - const { op, value } = obj; - const val = { - op, - path: this.currentPath(), - }; - if (typeof value !== 'undefined') { - val.value = value; - } - this.result.push(val); - }; - - context.pushMoveOp = function (to) { - const from = this.currentPath(); - this.result.push({ - op: OPERATIONS.move, - from, - path: this.toPath(to), - }); - }; - - context.currentPath = function () { - return `/${this.path.join('/')}`; - }; - - context.toPath = function (toPath) { - const to = this.path.slice(); - to[to.length - 1] = toPath; - return `/${to.join('/')}`; - }; - } - - typeFormattterErrorFormatter(context, err) { - context.out(`[ERROR] ${err}`); - } - - rootBegin() {} - rootEnd() {} - - nodeBegin({ path }, key, leftKey) { - path.push(leftKey); - } - - nodeEnd({ path }) { - path.pop(); - } - - /* jshint camelcase: false */ - /* eslint-disable camelcase */ - - format_unchanged() {} - - format_movedestination() {} - - format_node(context, delta, left) { - this.formatDeltaChildren(context, delta, left); - } - - format_added(context, delta) { - context.pushCurrentOp({ op: OPERATIONS.add, value: delta[0] }); - } - - format_modified(context, delta) { - context.pushCurrentOp({ op: OPERATIONS.replace, value: delta[1] }); - } - - format_deleted(context) { - context.pushCurrentOp({ op: OPERATIONS.remove }); - } - - format_moved(context, delta) { - const to = delta[1]; - context.pushMoveOp(to); - } - - format_textdiff() { - throw new Error('Not implemented'); - } - - format(delta, left) { - const context = {}; - this.prepareContext(context); - this.recurse(context, delta, left); - return context.result; - } -} - -/* jshint camelcase: true */ -/* eslint-enable camelcase */ - -export default JSONFormatter; - -const last = (arr) => arr[arr.length - 1]; - -const sortBy = (arr, pred) => { - arr.sort(pred); - return arr; -}; - -const compareByIndexDesc = (indexA, indexB) => { - const lastA = parseInt(indexA, 10); - const lastB = parseInt(indexB, 10); - if (!(isNaN(lastA) || isNaN(lastB))) { - return lastB - lastA; - } else { - return 0; - } -}; - -const opsByDescendingOrder = (removeOps) => - sortBy(removeOps, (a, b) => { - const splitA = a.path.split('/'); - const splitB = b.path.split('/'); - if (splitA.length !== splitB.length) { - return splitA.length - splitB.length; - } else { - return compareByIndexDesc(last(splitA), last(splitB)); - } - }); - -export const partitionOps = (arr, fns) => { - const initArr = Array(fns.length + 1) - .fill() - .map(() => []); - return arr - .map((item) => { - let position = fns.map((fn) => fn(item)).indexOf(true); - if (position < 0) { - position = fns.length; - } - return { item, position }; - }) - .reduce((acc, item) => { - acc[item.position].push(item.item); - return acc; - }, initArr); -}; -const isMoveOp = ({ op }) => op === 'move'; -const isRemoveOp = ({ op }) => op === 'remove'; - -const reorderOps = (diff) => { - const [moveOps, removedOps, restOps] = partitionOps(diff, [ - isMoveOp, - isRemoveOp, - ]); - const removeOpsReverse = opsByDescendingOrder(removedOps); - return [...removeOpsReverse, ...moveOps, ...restOps]; -}; - -let defaultInstance; - -export const format = (delta, left) => { - if (!defaultInstance) { - defaultInstance = new JSONFormatter(); - } - return reorderOps(defaultInstance.format(delta, left)); -}; - -export const log = (delta, left) => { - console.log(format(delta, left)); -}; diff --git a/src/formatters/jsonpatch.ts b/src/formatters/jsonpatch.ts new file mode 100644 index 00000000..375c4e62 --- /dev/null +++ b/src/formatters/jsonpatch.ts @@ -0,0 +1,237 @@ +import BaseFormatter, { BaseFormatterContext } from './base'; +import { + AddedDelta, + ArrayDelta, + Delta, + ModifiedDelta, + MovedDelta, + ObjectDelta, +} from '../contexts/diff'; + +const OPERATIONS = { + add: 'add', + remove: 'remove', + replace: 'replace', + move: 'move', +} as const; + +interface AddOp { + op: 'add'; + path: string; + value: unknown; +} + +interface RemoveOp { + op: 'remove'; + path: string; +} + +interface ReplaceOp { + op: 'replace'; + path: string; + value: unknown; +} + +interface MoveOp { + op: 'move'; + from: string; + path: string; +} + +type Op = AddOp | RemoveOp | ReplaceOp | MoveOp; + +interface JSONFormatterContext extends BaseFormatterContext { + result: Op[]; + path: Array; + pushCurrentOp: ( + obj: + | { op: 'add'; value: unknown } + | { op: 'replace'; value: unknown } + | { op: 'remove' }, + ) => void; + pushMoveOp: (to: number) => void; + currentPath: () => string; + toPath: (toPath: number) => string; +} + +class JSONFormatter extends BaseFormatter { + constructor() { + super(); + this.includeMoveDestinations = true; + } + + prepareContext(context: Partial) { + super.prepareContext(context); + context.result = []; + context.path = []; + context.pushCurrentOp = function (obj) { + if (obj.op === 'add' || obj.op === 'replace') { + this.result!.push({ + op: obj.op, + path: this.currentPath!(), + value: obj.value, + }); + } else if (obj.op === 'remove') { + this.result!.push({ op: obj.op, path: this.currentPath!() }); + } else { + obj satisfies never; + } + }; + + context.pushMoveOp = function (to) { + const from = this.currentPath!(); + this.result!.push({ + op: OPERATIONS.move, + from, + path: this.toPath!(to), + }); + }; + + context.currentPath = function () { + return `/${this.path!.join('/')}`; + }; + + context.toPath = function (toPath) { + const to = this.path!.slice(); + to[to.length - 1] = toPath; + return `/${to.join('/')}`; + }; + } + + typeFormattterErrorFormatter(context: JSONFormatterContext, err: unknown) { + context.out(`[ERROR] ${err}`); + } + + rootBegin() {} + rootEnd() {} + + nodeBegin( + { path }: JSONFormatterContext, + key: string, + leftKey: string | number, + ) { + path.push(leftKey); + } + + nodeEnd({ path }: JSONFormatterContext) { + path.pop(); + } + + /* jshint camelcase: false */ + /* eslint-disable camelcase */ + + format_unchanged() {} + + format_movedestination() {} + + format_node( + context: JSONFormatterContext, + delta: ObjectDelta | ArrayDelta, + left: unknown, + ) { + this.formatDeltaChildren(context, delta, left); + } + + format_added(context: JSONFormatterContext, delta: AddedDelta) { + context.pushCurrentOp({ op: OPERATIONS.add, value: delta[0] }); + } + + format_modified(context: JSONFormatterContext, delta: ModifiedDelta) { + context.pushCurrentOp({ op: OPERATIONS.replace, value: delta[1] }); + } + + format_deleted(context: JSONFormatterContext) { + context.pushCurrentOp({ op: OPERATIONS.remove }); + } + + format_moved(context: JSONFormatterContext, delta: MovedDelta) { + const to = delta[1]; + context.pushMoveOp(to); + } + + format_textdiff() { + throw new Error('Not implemented'); + } + + format(delta: Delta, left: unknown) { + const context: Partial = {}; + this.prepareContext(context); + this.recurse(context as JSONFormatterContext, delta, left); + return (context as JSONFormatterContext).result; + } +} + +/* jshint camelcase: true */ +/* eslint-enable camelcase */ + +export default JSONFormatter; + +const last = (arr: T[]): T => arr[arr.length - 1]; + +const sortBy = (arr: T[], pred: (a: T, b: T) => number) => { + arr.sort(pred); + return arr; +}; + +const compareByIndexDesc = (indexA: string, indexB: string) => { + const lastA = parseInt(indexA, 10); + const lastB = parseInt(indexB, 10); + if (!(isNaN(lastA) || isNaN(lastB))) { + return lastB - lastA; + } else { + return 0; + } +}; + +const opsByDescendingOrder = (removeOps: Op[]) => + sortBy(removeOps, (a, b) => { + const splitA = a.path.split('/'); + const splitB = b.path.split('/'); + if (splitA.length !== splitB.length) { + return splitA.length - splitB.length; + } else { + return compareByIndexDesc(last(splitA), last(splitB)); + } + }); + +export const partitionOps = (arr: Op[], fns: Array<(op: Op) => boolean>) => { + const initArr: Op[][] = Array(fns.length + 1) + .fill(undefined) + .map(() => []); + return arr + .map((item) => { + let position = fns.map((fn) => fn(item)).indexOf(true); + if (position < 0) { + position = fns.length; + } + return { item, position }; + }) + .reduce((acc, item) => { + acc[item.position].push(item.item); + return acc; + }, initArr); +}; +const isMoveOp = ({ op }: Op) => op === 'move'; +const isRemoveOp = ({ op }: Op) => op === 'remove'; + +const reorderOps = (diff: Op[]) => { + const [moveOps, removedOps, restOps] = partitionOps(diff, [ + isMoveOp, + isRemoveOp, + ]); + const removeOpsReverse = opsByDescendingOrder(removedOps); + return [...removeOpsReverse, ...moveOps, ...restOps]; +}; + +let defaultInstance: JSONFormatter | undefined; + +export const format = (delta: Delta, left: unknown) => { + if (!defaultInstance) { + defaultInstance = new JSONFormatter(); + } + return reorderOps(defaultInstance.format(delta, left)); +}; + +export const log = (delta: Delta, left: unknown) => { + console.log(format(delta, left)); +}; diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index 715ce30d..00000000 --- a/src/index.d.ts +++ /dev/null @@ -1,203 +0,0 @@ -export interface Formatter { - format(delta: Delta, original: any): string; -} - -export interface HtmlFormatter extends Formatter { - /** - * Set whether to show or hide unchanged parts of a diff. - * @param show Whether to show unchanged parts - * @param node The root element the diff is contained within. (Default: body) - * @param delay Transition time in ms. (Default: no transition) - */ - showUnchanged(show: boolean, node?: Element | null, delay?: number): void; - - /** - * An alias for showUnchanged(false, ...) - * @param node The root element the diff is contained within (Default: body) - * @param delay Transition time in ms. (Default: no transition) - */ - hideUnchanged(node?: Element | null, delay?: number): void; -} - -export interface Delta { - [key: string]: any; - [key: number]: any; -} - -export class Context { - nested: boolean; - exiting?: boolean; - options: Config; - parent?: PatchContext; - childName?: string; - children?: PatchContext[]; - root?: PatchContext; - next?: PatchContext; - nextAfterChildren?: PatchContext; - hasResult: boolean; - setResult(result: any): Context; - exit(): Context; -} - -export class PatchContext extends Context { - pipe: 'patch'; - left: any; - delta: Delta; -} - -export class DiffContext extends Context { - pipe: 'diff'; - left: any; - right: any; -} - -export class ReverseContext extends Context { - pipe: 'reverse'; - delta: Delta; -} - -type FilterContext = PatchContext | DiffContext | ReverseContext; - -/** - * A plugin which can modify the diff(), patch() or reverse() operations - */ -export interface Filter { - /** - * A function which is called at each stage of the operation and can update the context to modify the result - * @param context The current state of the operation - */ - (context: TContext): void; - - /** - * A unique name which can be used to insert other filters before/after, or remove/replace this filter - */ - filterName: string; -} - -/** - * A collection of Filters run on each diff(), patch() or reverse() operation - */ -export class Pipe { - /** - * Append one or more filters to the existing list - */ - append(...filters: Filter[]): void; - - /** - * Prepend one or more filters to the existing list - */ - prepend(...filters: Filter[]): void; - - /** - * Add one ore more filters after the specified filter - * @param filterName The name of the filter to insert before - * @param filters Filters to be inserted - */ - after(filterName: string, ...filters: Filter[]): void; - - /** - * Add one ore more filters before the specified filter - * @param filterName The name of the filter to insert before - * @param filters Filters to be inserted - */ - before(filterName: string, ...filters: Filter[]): void; - - /** - * Replace the specified filter with one ore more filters - * @param filterName The name of the filter to replace - * @param filters Filters to be inserted - */ - replace(filterName: string, ...filters: Filter[]): void; - - /** - * Remove the filter with the specified name - * @param filterName The name of the filter to remove - */ - remove(filterName: string): void; - - /** - * Remove all filters from this pipe - */ - clear(): void; - - /** - * Return array of ordered filter names for this pipe - */ - list(): void; -} - -export class Processor { - constructor(options?: Config); - - pipes: { - patch: Pipe; - diff: Pipe; - reverse: Pipe; - }; -} - -export interface Config { - // used to match objects when diffing arrays, by default only === operator is used - objectHash?: (item: any, index: number) => string; - - arrays?: { - // default true, detect items moved inside the array (otherwise they will be registered as remove+add) - detectMove: boolean; - // default false, the value of items moved is not included in deltas - includeValueOnMove: boolean; - }; - - textDiff?: { - // default 60, minimum string length (left and right sides) to use text diff algorythm: google-diff-match-patch - minLength: number; - }; - - /** - * this optional function can be specified to ignore object properties (eg. volatile data) - * @param name property name, present in either context.left or context.right objects - * @param context the diff context (has context.left and context.right objects) - */ - /** - * - */ - propertyFilter?: (name: string, context: DiffContext) => boolean; - - /** - * default false. if true, values in the obtained delta will be cloned (using jsondiffpatch.clone by default), - * to ensure delta keeps no references to left or right objects. this becomes useful if you're diffing and patching - * the same objects multiple times without serializing deltas. - * - * instead of true, a function can be specified here to provide a custom clone(value) - */ - cloneDiffValues?: boolean | ((value: any) => any); -} - -export class DiffPatcher { - constructor(options?: Config); - - processor: Processor; - - clone: (value: any) => any; - diff: (left: any, right: any) => Delta | undefined; - patch: (left: any, delta: Delta) => any; - reverse: (delta: Delta) => Delta | undefined; - unpatch: (right: any, delta: Delta) => any; -} - -export const create: (options?: any) => DiffPatcher; - -export const formatters: { - annotated: Formatter; - console: Formatter; - html: HtmlFormatter; - jsonpatch: Formatter; -}; - -export const console: Formatter; - -export const dateReviver: (key: string, value: any) => any; - -export const diff: (left: any, right: any) => Delta | undefined; -export const patch: (left: any, delta: Delta) => any; -export const reverse: (delta: Delta) => Delta | undefined; -export const unpatch: (right: any, delta: Delta) => any; diff --git a/src/main.js b/src/index.ts similarity index 50% rename from src/main.js rename to src/index.ts index 6ae18423..b3a35017 100644 --- a/src/main.js +++ b/src/index.ts @@ -1,5 +1,7 @@ import DiffPatcher from './diffpatcher'; import dateReviver from './date-reviver'; +import { Options } from './processor'; +import { Delta } from './contexts/diff'; export { DiffPatcher, dateReviver }; @@ -7,43 +9,43 @@ export * as formatters from './formatters/index'; export * as console from './formatters/console'; -export function create(options) { +export function create(options?: Options) { return new DiffPatcher(options); } -let defaultInstance; +let defaultInstance: DiffPatcher; -export function diff() { +export function diff(left: unknown, right: unknown) { if (!defaultInstance) { defaultInstance = new DiffPatcher(); } - return defaultInstance.diff.apply(defaultInstance, arguments); + return defaultInstance.diff(left, right); } -export function patch() { +export function patch(left: unknown, delta: Delta) { if (!defaultInstance) { defaultInstance = new DiffPatcher(); } - return defaultInstance.patch.apply(defaultInstance, arguments); + return defaultInstance.patch(left, delta); } -export function unpatch() { +export function unpatch(right: unknown, delta: Delta) { if (!defaultInstance) { defaultInstance = new DiffPatcher(); } - return defaultInstance.unpatch.apply(defaultInstance, arguments); + return defaultInstance.unpatch(right, delta); } -export function reverse() { +export function reverse(delta: Delta) { if (!defaultInstance) { defaultInstance = new DiffPatcher(); } - return defaultInstance.reverse.apply(defaultInstance, arguments); + return defaultInstance.reverse(delta); } -export function clone() { +export function clone(value: unknown) { if (!defaultInstance) { defaultInstance = new DiffPatcher(); } - return defaultInstance.clone.apply(defaultInstance, arguments); + return defaultInstance.clone(value); } diff --git a/src/pipe.js b/src/pipe.ts similarity index 64% rename from src/pipe.js rename to src/pipe.ts index 460634c4..119bf5bd 100644 --- a/src/pipe.js +++ b/src/pipe.ts @@ -1,10 +1,24 @@ -class Pipe { - constructor(name) { +import Context from './contexts/context'; +import Processor from './processor'; + +export interface Filter { + (context: TContext): void; + filterName: string; +} + +class Pipe> { + name?: string; + filters: Filter[]; + processor?: Processor; + debug?: boolean; + resultCheck?: ((context: TContext) => void) | null; + + constructor(name: string) { this.name = name; this.filters = []; } - process(input) { + process(input: TContext) { if (!this.processor) { throw new Error('add this pipe to a processor before using it'); } @@ -27,21 +41,21 @@ class Pipe { } } - log(msg) { + log(msg: string) { console.log(`[jsondiffpatch] ${this.name} pipe, ${msg}`); } - append(...args) { + append(...args: Filter[]) { this.filters.push(...args); return this; } - prepend(...args) { + prepend(...args: Filter[]) { this.filters.unshift(...args); return this; } - indexOf(filterName) { + indexOf(filterName: string) { if (!filterName) { throw new Error('a filter name is required'); } @@ -58,40 +72,25 @@ class Pipe { return this.filters.map((f) => f.filterName); } - after(filterName) { + after(filterName: string, ...params: Filter[]) { const index = this.indexOf(filterName); - const params = Array.prototype.slice.call(arguments, 1); - if (!params.length) { - throw new Error('a filter is required'); - } - params.unshift(index + 1, 0); - Array.prototype.splice.apply(this.filters, params); + this.filters.splice(index + 1, 0, ...params); return this; } - before(filterName) { + before(filterName: string, ...params: Filter[]) { const index = this.indexOf(filterName); - const params = Array.prototype.slice.call(arguments, 1); - if (!params.length) { - throw new Error('a filter is required'); - } - params.unshift(index, 0); - Array.prototype.splice.apply(this.filters, params); + this.filters.splice(index, 0, ...params); return this; } - replace(filterName) { + replace(filterName: string, ...params: Filter[]) { const index = this.indexOf(filterName); - const params = Array.prototype.slice.call(arguments, 1); - if (!params.length) { - throw new Error('a filter is required'); - } - params.unshift(index, 1); - Array.prototype.splice.apply(this.filters, params); + this.filters.splice(index, 1, ...params); return this; } - remove(filterName) { + remove(filterName: string) { const index = this.indexOf(filterName); this.filters.splice(index, 1); return this; @@ -102,7 +101,7 @@ class Pipe { return this; } - shouldHaveResult(should) { + shouldHaveResult(should?: boolean) { if (should === false) { this.resultCheck = null; return; @@ -114,7 +113,9 @@ class Pipe { this.resultCheck = (context) => { if (!context.hasResult) { console.log(context); - const error = new Error(`${pipe.name} failed`); + const error: Error & { noResult?: boolean } = new Error( + `${pipe.name} failed`, + ); error.noResult = true; throw error; } diff --git a/src/processor.js b/src/processor.js deleted file mode 100644 index ccc0ae6b..00000000 --- a/src/processor.js +++ /dev/null @@ -1,65 +0,0 @@ -class Processor { - constructor(options) { - this.selfOptions = options || {}; - this.pipes = {}; - } - - options(options) { - if (options) { - this.selfOptions = options; - } - return this.selfOptions; - } - - pipe(name, pipeArg) { - let pipe = pipeArg; - if (typeof name === 'string') { - if (typeof pipe === 'undefined') { - return this.pipes[name]; - } else { - this.pipes[name] = pipe; - } - } - if (name && name.name) { - pipe = name; - if (pipe.processor === this) { - return pipe; - } - this.pipes[pipe.name] = pipe; - } - pipe.processor = this; - return pipe; - } - - process(input, pipe) { - let context = input; - context.options = this.options(); - let nextPipe = pipe || input.pipe || 'default'; - let lastPipe; - let lastContext; - while (nextPipe) { - if (typeof context.nextAfterChildren !== 'undefined') { - // children processed and coming back to parent - context.next = context.nextAfterChildren; - context.nextAfterChildren = null; - } - - if (typeof nextPipe === 'string') { - nextPipe = this.pipe(nextPipe); - } - nextPipe.process(context); - lastContext = context; - lastPipe = nextPipe; - nextPipe = null; - if (context) { - if (context.next) { - context = context.next; - nextPipe = lastContext.nextPipe || context.pipe || lastPipe; - } - } - } - return context.hasResult ? context.result : undefined; - } -} - -export default Processor; diff --git a/src/processor.ts b/src/processor.ts new file mode 100644 index 00000000..77eacc1c --- /dev/null +++ b/src/processor.ts @@ -0,0 +1,93 @@ +import Pipe from './pipe'; +import Context from './contexts/context'; +import DiffContext from './contexts/diff'; + +export interface Options { + objectHash?: (item: object, index?: number) => string; + matchByPosition?: boolean; + arrays?: { + detectMove?: boolean; + includeValueOnMove?: boolean; + }; + textDiff?: { + minLength?: number; + }; + propertyFilter?: (name: string, context: DiffContext) => boolean; + cloneDiffValues?: boolean | ((value: unknown) => unknown); +} + +class Processor { + selfOptions: Options; + pipes: { [pipeName: string]: Pipe> | undefined }; + + constructor(options?: Options) { + this.selfOptions = options || {}; + this.pipes = {}; + } + + options(options?: Options) { + if (options) { + this.selfOptions = options; + } + return this.selfOptions; + } + + pipe>( + name: string | Pipe, + pipeArg?: Pipe, + ) { + let pipe = pipeArg; + if (typeof name === 'string') { + if (typeof pipe === 'undefined') { + return this.pipes[name]!; + } else { + this.pipes[name] = pipe as Pipe>; + } + } + if (name && (name as Pipe).name) { + pipe = name as Pipe; + if (pipe.processor === this) { + return pipe; + } + this.pipes[pipe.name!] = pipe as Pipe>; + } + pipe!.processor = this; + return pipe!; + } + + process>( + input: TContext, + pipe?: Pipe, + ): TContext['result'] | undefined { + let context = input; + context.options = this.options(); + let nextPipe: Pipe | string | null = + pipe || input.pipe || 'default'; + let lastPipe; + let lastContext; + while (nextPipe) { + if (typeof context.nextAfterChildren !== 'undefined') { + // children processed and coming back to parent + context.next = context.nextAfterChildren; + context.nextAfterChildren = null; + } + + if (typeof nextPipe === 'string') { + nextPipe = this.pipe(nextPipe); + } + (nextPipe as Pipe).process(context); + lastContext = context; + lastPipe = nextPipe; + nextPipe = null; + if (context) { + if (context.next) { + context = context.next; + nextPipe = lastContext.nextPipe || context.pipe || lastPipe; + } + } + } + return context.hasResult ? context.result : undefined; + } +} + +export default Processor; diff --git a/test/index.spec.js b/test/index.spec.js index 306bdfc8..4bf7c683 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,4 +1,4 @@ -import * as jsondiffpatch from '../src/main'; +import * as jsondiffpatch from '../src'; import lcs from '../src/filters/lcs'; import examples from './examples/diffpatch'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..7daae30d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2016", + "module": "Node16", + "noEmit": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": false + } +}