diff --git a/.gitignore b/.gitignore index 21b6d99a4..b78c4327d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist yarn-error.log .vscode .nyc_output +/src/**/*.js \ No newline at end of file diff --git a/README.md b/README.md index e14adb8a3..68bdb6b2c 100644 --- a/README.md +++ b/README.md @@ -194,8 +194,8 @@ For even more customisation of the diffing behavior, you can create a `new Diff. * `tokenize(value, options)`: used to convert each of `oldString` and `newString` (after they've gone through `castInput`) to an array of tokens. Defaults to returning `value.split('')` (returning an array of individual characters). * `removeEmpty(array)`: called on the arrays of tokens returned by `tokenize` and can be used to modify them. Defaults to stripping out falsey tokens, such as empty strings. `diffArrays` overrides this to simply return the `array`, which means that falsey values like empty strings can be handled like any other token by `diffArrays`. * `equals(left, right, options)`: called to determine if two tokens (one from the old string, one from the new string) should be considered equal. Defaults to comparing them with `===`. -* `join(tokens)`: gets called with an array of consecutive tokens that have either all been added, all been removed, or are all common. Needs to join them into a single value that can be used as the `value` property of the [change object](#change-objects) for these tokens. Defaults to simply returning `tokens.join('')`. -* `postProcess(changeObjects)`: gets called at the end of the algorithm with the [change objects](#change-objects) produced, and can do final cleanups on them. Defaults to simply returning `changeObjects` unchanged. +* `join(tokens)`: gets called with an array of consecutive tokens that have either all been added, all been removed, or are all common. Needs to join them into a single value that can be used as the `value` property of the [change object](#change-objects) for these tokens. Defaults to simply returning `tokens.join('')` (and therefore by default will error out if your tokens are not strings; differs that support non-string tokens like `diffArrays` should override it to be a no-op to fix this). +* `postProcess(changeObjects, options)`: gets called at the end of the algorithm with the [change objects](#change-objects) produced, and can do final cleanups on them. Defaults to simply returning `changeObjects` unchanged. ### Change Objects Many of the methods above return change objects. These objects consist of the following fields: diff --git a/eslint.config.mjs b/eslint.config.mjs index ddc7551c8..357400d35 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,176 +1,157 @@ -import globals from "globals"; -import babelParser from "@babel/eslint-parser"; - -export default [ - { - languageOptions: { - globals: { - ...globals.browser, - }, - - parser: babelParser, - }, - - rules: { - // Possible Errors // - //-----------------// - "comma-dangle": [2, "never"], - "no-cond-assign": [2, "except-parens"], - "no-console": 1, // Allow for debugging - "no-constant-condition": 2, - "no-control-regex": 2, - "no-debugger": 1, // Allow for debugging - "no-dupe-args": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-empty": 2, - "no-empty-character-class": 2, - "no-ex-assign": 2, - "no-extra-boolean-cast": 2, - "no-extra-parens": [2, "functions"], - "no-extra-semi": 2, - "no-func-assign": 2, - "no-invalid-regexp": 2, - "no-irregular-whitespace": 2, - "no-negated-in-lhs": 2, - "no-obj-calls": 2, - "no-regex-spaces": 2, - "no-unreachable": 1, // Optimizer and coverage will handle/highlight this and can be useful for debugging - "use-isnan": 2, - "valid-typeof": 2, - - // Best Practices // - //----------------// - curly: 2, - "default-case": 1, - - "dot-notation": [2, { - allowKeywords: false, - }], - - "guard-for-in": 1, - "no-alert": 2, - "no-caller": 2, - "no-div-regex": 1, - "no-eval": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-fallthrough": 2, - "no-floating-decimal": 2, - "no-implied-eval": 2, - "no-iterator": 2, - "no-labels": 2, - "no-lone-blocks": 2, - "no-multi-spaces": 2, - "no-multi-str": 1, - "no-native-reassign": 2, - "no-new": 2, - "no-new-func": 2, - "no-new-wrappers": 2, - "no-octal": 2, - "no-octal-escape": 2, - "no-process-env": 2, - "no-proto": 2, - "no-redeclare": 2, - "no-return-assign": 2, - "no-script-url": 2, - "no-self-compare": 2, - "no-sequences": 2, - "no-throw-literal": 2, - "no-unused-expressions": 2, - "no-warning-comments": 1, - "no-with": 2, - radix: 2, - "wrap-iife": 2, - - // Variables // - //-----------// - "no-catch-shadow": 2, - "no-delete-var": 2, - "no-label-var": 2, - "no-undef": 2, - "no-undef-init": 2, - - "no-unused-vars": [2, { - vars: "all", - args: "after-used", - }], - - "no-use-before-define": [2, "nofunc"], - - // Node.js // - //---------// - - // Stylistic // - //-----------// - "brace-style": [2, "1tbs", { - allowSingleLine: true, - }], +// @ts-check - camelcase: 2, - - "comma-spacing": [2, { - before: false, - after: true, - }], - - "comma-style": [2, "last"], - "consistent-this": [1, "self"], - "eol-last": 2, - "func-style": [2, "declaration"], - - "key-spacing": [2, { - beforeColon: false, - afterColon: true, - }], - - "new-cap": 2, - "new-parens": 2, - "no-array-constructor": 2, - "no-lonely-if": 2, - "no-mixed-spaces-and-tabs": 2, - "no-nested-ternary": 1, - "no-new-object": 2, - "no-spaced-func": 2, - "no-trailing-spaces": 2, - - "quote-props": [2, "as-needed", { - keywords: true, - }], - - quotes: [2, "single", "avoid-escape"], - semi: 2, - - "semi-spacing": [2, { - before: false, - after: true, - }], - - "space-before-blocks": [2, "always"], +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import globals from "globals"; - "space-before-function-paren": [2, { - anonymous: "never", - named: "never", - }], +export default tseslint.config( + { + ignores: [ + "**/*", // ignore everything... + "!src/**/", "!src/**/*.ts", // ... except our TypeScript source files... + "!test/**/", "!test/**/*.js", // ... and our tests + ], + }, + eslint.configs.recommended, + tseslint.configs.recommended, + { + languageOptions: { + globals: { + ...globals.browser, + }, + }, - "space-in-parens": [2, "never"], - "space-infix-ops": 2, - "space-unary-ops": 2, - "spaced-comment": [2, "always"], - "wrap-regex": 1, - "no-var": 2, - }, + rules: { + // Possible Errors // + //-----------------// + "comma-dangle": [2, "never"], + "no-console": 1, // Allow for debugging + "no-debugger": 1, // Allow for debugging + "no-extra-parens": [2, "functions"], + "no-extra-semi": 2, + "no-negated-in-lhs": 2, + "no-unreachable": 1, // Optimizer and coverage will handle/highlight this and can be useful for debugging + + // Best Practices // + //----------------// + curly: 2, + "default-case": 1, + "dot-notation": [2, { + allowKeywords: false, + }], + "guard-for-in": 1, + "no-alert": 2, + "no-caller": 2, + "no-div-regex": 1, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-floating-decimal": 2, + "no-implied-eval": 2, + "no-iterator": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-multi-spaces": 2, + "no-multi-str": 1, + "no-native-reassign": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-octal-escape": 2, + "no-process-env": 2, + "no-proto": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-throw-literal": 2, + "no-unused-expressions": 2, + "no-warning-comments": 1, + radix: 2, + "wrap-iife": 2, + + // Variables // + //-----------// + "no-catch-shadow": 2, + "no-label-var": 2, + "no-undef-init": 2, + + // Node.js // + //---------// + + // Stylistic // + //-----------// + "brace-style": [2, "1tbs", { + allowSingleLine: true, + }], + camelcase: 2, + "comma-spacing": [2, { + before: false, + after: true, + }], + "comma-style": [2, "last"], + "consistent-this": [1, "self"], + "eol-last": 2, + "func-style": [2, "declaration"], + "key-spacing": [2, { + beforeColon: false, + afterColon: true, + }], + "new-cap": 2, + "new-parens": 2, + "no-array-constructor": 2, + "no-lonely-if": 2, + "no-mixed-spaces-and-tabs": 2, + "no-nested-ternary": 1, + "no-new-object": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "quote-props": [2, "as-needed", { + keywords: true, + }], + quotes: [2, "single", "avoid-escape"], + semi: 2, + "semi-spacing": [2, { + before: false, + after: true, + }], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, { + anonymous: "never", + named: "never", + }], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": 2, + "spaced-comment": [2, "always"], + "wrap-regex": 1, + "no-var": 2, + + // Typescript // + //------------// + "@typescript-eslint/no-explicit-any": 0, // Very strict rule, incompatible with our code + + // We use these intentionally - e.g. + // export interface DiffCssOptions extends CommonDiffOptions {} + // for the options argument to diffCss which currently takes no options beyond the ones + // common to all diffFoo functions. Doing this allows consistency (one options interface per + // diffFoo function) and future-proofs against the API having to change in future if we add a + // non-common option to one of these functions. + "@typescript-eslint/no-empty-object-type": [2, {allowInterfaces: 'with-single-extends'}], + }, + }, + { + files: ['test/**/*.js'], + languageOptions: { + globals: { + ...globals.node, + ...globals.mocha, + }, + }, + rules: { + "no-unused-expressions": 0, // Needs disabling to support Chai `.to.be.undefined` etc syntax + "@typescript-eslint/no-unused-expressions": 0, // (as above) + "no-use-before-define": [2, "nofunc"], // Useful rule but broken for TypeScript code }, - { - files: ['test/**/*.js'], - languageOptions: { - globals: { - ...globals.node, - ...globals.mocha, - }, - }, - rules: { - "no-unused-expressions": 0, // Needs disabling to support Chai `.to.be.undefined` etc syntax - }, - } -]; \ No newline at end of file + } +); \ No newline at end of file diff --git a/package.json b/package.json index 8699cf55e..53533f4d7 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,8 @@ }, "scripts": { "clean": "rm -rf lib/ dist/ coverage/ .nyc_output/", - "lint": "yarn eslint 'src/**/*.js' 'test/**/*.js'", - "build": "yarn lint && yarn run-babel && yarn run-rollup && yarn run-uglify", + "lint": "yarn eslint", + "build": "yarn tsc src/*.ts src/**/*.ts --module es2015 --moduleResolution bundler --target es2022 && yarn run-babel && yarn run-rollup && yarn run-uglify", "test": "nyc yarn _test", "_test": "yarn build && cross-env NODE_ENV=test yarn run-mocha", "run-babel": "babel --out-dir lib --source-maps=inline src", @@ -74,12 +74,16 @@ "nyc": "^17.1.0", "rollup": "^4.34.8", "rollup-plugin-babel": "^4.4.0", + "typescript": "^5.7.3", "uglify-js": "^3.19.3", "webpack": "^5.98.0", "webpack-dev-server": "^5.2.0" }, "optionalDependencies": {}, - "dependencies": {}, + "dependencies": { + "@eslint/js": "^9.22.0", + "typescript-eslint": "^8.26.1" + }, "nyc": { "require": [ "@babel/register" diff --git a/src/convert/dmp.js b/src/convert/dmp.ts similarity index 62% rename from src/convert/dmp.js rename to src/convert/dmp.ts index b411dc2de..05e82d285 100644 --- a/src/convert/dmp.js +++ b/src/convert/dmp.ts @@ -1,7 +1,11 @@ +import {ChangeObject} from '../types'; + +type DmpOperation = 1 | 0 | -1; + // See: http://code.google.com/p/google-diff-match-patch/wiki/API -export function convertChangesToDMP(changes) { - let ret = [], - change, +export function convertChangesToDMP(changes: ChangeObject[]): [DmpOperation, ValueT][] { + const ret = []; + let change, operation; for (let i = 0; i < changes.length; i++) { change = changes[i]; diff --git a/src/convert/xml.js b/src/convert/xml.ts similarity index 70% rename from src/convert/xml.js rename to src/convert/xml.ts index 34aa46366..ab65f8ffd 100644 --- a/src/convert/xml.js +++ b/src/convert/xml.ts @@ -1,7 +1,9 @@ -export function convertChangesToXML(changes) { - let ret = []; +import {ChangeObject} from '../types'; + +export function convertChangesToXML(changes: ChangeObject[]): string { + const ret = []; for (let i = 0; i < changes.length; i++) { - let change = changes[i]; + const change = changes[i]; if (change.added) { ret.push(''); } else if (change.removed) { @@ -19,7 +21,7 @@ export function convertChangesToXML(changes) { return ret.join(''); } -function escapeHTML(s) { +function escapeHTML(s: string): string { let n = s; n = n.replace(/&/g, '&'); n = n.replace(/> { + protected tokenize(value: Array) { + return value.slice(); + } + + protected join(value: Array) { + return value; + } + + protected removeEmpty(value: Array) { + return value; + } +} + +export const arrayDiff = new ArrayDiff(); + +export function diffArrays( + oldArr: any[], + newArr: any[], + options: (DiffArraysOptions & CallbackOption) | DiffCallback +): undefined; +export function diffArrays( + oldArr: any[], + newArr: any[], + options: DiffArraysOptions +): ChangeObject[]; +export function diffArrays( + oldArr: any[], + newArr: any[], + options +): undefined | ChangeObject[] { + return arrayDiff.diff(oldArr, newArr, options); +} diff --git a/src/diff/base.js b/src/diff/base.js deleted file mode 100644 index d58b5797e..000000000 --- a/src/diff/base.js +++ /dev/null @@ -1,264 +0,0 @@ -export default function Diff() {} - -Diff.prototype = { - diff(oldString, newString, options = {}) { - let callback = options.callback; - if (typeof options === 'function') { - callback = options; - options = {}; - } - - let self = this; - - function done(value) { - value = self.postProcess(value, options); - if (callback) { - setTimeout(function() { callback(value); }, 0); - return true; - } else { - return value; - } - } - - // Allow subclasses to massage the input prior to running - oldString = this.castInput(oldString, options); - newString = this.castInput(newString, options); - - oldString = this.removeEmpty(this.tokenize(oldString, options)); - newString = this.removeEmpty(this.tokenize(newString, options)); - - let newLen = newString.length, oldLen = oldString.length; - let editLength = 1; - let maxEditLength = newLen + oldLen; - if(options.maxEditLength != null) { - maxEditLength = Math.min(maxEditLength, options.maxEditLength); - } - const maxExecutionTime = options.timeout ?? Infinity; - const abortAfterTimestamp = Date.now() + maxExecutionTime; - - let bestPath = [{ oldPos: -1, lastComponent: undefined }]; - - // Seed editLength = 0, i.e. the content starts with the same values - let newPos = this.extractCommon(bestPath[0], newString, oldString, 0, options); - if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) { - // Identity per the equality and tokenizer - return done(buildValues(self, bestPath[0].lastComponent, newString, oldString, self.useLongestToken)); - } - - // Once we hit the right edge of the edit graph on some diagonal k, we can - // definitely reach the end of the edit graph in no more than k edits, so - // there's no point in considering any moves to diagonal k+1 any more (from - // which we're guaranteed to need at least k+1 more edits). - // Similarly, once we've reached the bottom of the edit graph, there's no - // point considering moves to lower diagonals. - // We record this fact by setting minDiagonalToConsider and - // maxDiagonalToConsider to some finite value once we've hit the edge of - // the edit graph. - // This optimization is not faithful to the original algorithm presented in - // Myers's paper, which instead pointlessly extends D-paths off the end of - // the edit graph - see page 7 of Myers's paper which notes this point - // explicitly and illustrates it with a diagram. This has major performance - // implications for some common scenarios. For instance, to compute a diff - // where the new text simply appends d characters on the end of the - // original text of length n, the true Myers algorithm will take O(n+d^2) - // time while this optimization needs only O(n+d) time. - let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity; - - // Main worker method. checks all permutations of a given edit length for acceptance. - function execEditLength() { - for ( - let diagonalPath = Math.max(minDiagonalToConsider, -editLength); - diagonalPath <= Math.min(maxDiagonalToConsider, editLength); - diagonalPath += 2 - ) { - let basePath; - let removePath = bestPath[diagonalPath - 1], - addPath = bestPath[diagonalPath + 1]; - if (removePath) { - // No one else is going to attempt to use this value, clear it - bestPath[diagonalPath - 1] = undefined; - } - - let canAdd = false; - if (addPath) { - // what newPos will be after we do an insertion: - const addPathNewPos = addPath.oldPos - diagonalPath; - canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen; - } - - let canRemove = removePath && removePath.oldPos + 1 < oldLen; - if (!canAdd && !canRemove) { - // If this path is a terminal then prune - bestPath[diagonalPath] = undefined; - continue; - } - - // Select the diagonal that we want to branch from. We select the prior - // path whose position in the old string is the farthest from the origin - // and does not pass the bounds of the diff graph - if (!canRemove || (canAdd && removePath.oldPos < addPath.oldPos)) { - basePath = self.addToPath(addPath, true, false, 0, options); - } else { - basePath = self.addToPath(removePath, false, true, 1, options); - } - - newPos = self.extractCommon(basePath, newString, oldString, diagonalPath, options); - - if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) { - // If we have hit the end of both strings, then we are done - return done(buildValues(self, basePath.lastComponent, newString, oldString, self.useLongestToken)); - } else { - bestPath[diagonalPath] = basePath; - if (basePath.oldPos + 1 >= oldLen) { - maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1); - } - if (newPos + 1 >= newLen) { - minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1); - } - } - } - - editLength++; - } - - // Performs the length of edit iteration. Is a bit fugly as this has to support the - // sync and async mode which is never fun. Loops over execEditLength until a value - // is produced, or until the edit length exceeds options.maxEditLength (if given), - // in which case it will return undefined. - if (callback) { - (function exec() { - setTimeout(function() { - if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) { - return callback(); - } - - if (!execEditLength()) { - exec(); - } - }, 0); - }()); - } else { - while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) { - let ret = execEditLength(); - if (ret) { - return ret; - } - } - } - }, - - addToPath(path, added, removed, oldPosInc, options) { - let last = path.lastComponent; - if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) { - return { - oldPos: path.oldPos + oldPosInc, - lastComponent: {count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent } - }; - } else { - return { - oldPos: path.oldPos + oldPosInc, - lastComponent: {count: 1, added: added, removed: removed, previousComponent: last } - }; - } - }, - extractCommon(basePath, newString, oldString, diagonalPath, options) { - let newLen = newString.length, - oldLen = oldString.length, - oldPos = basePath.oldPos, - newPos = oldPos - diagonalPath, - - commonCount = 0; - while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldString[oldPos + 1], newString[newPos + 1], options)) { - newPos++; - oldPos++; - commonCount++; - if (options.oneChangePerToken) { - basePath.lastComponent = {count: 1, previousComponent: basePath.lastComponent, added: false, removed: false}; - } - } - - if (commonCount && !options.oneChangePerToken) { - basePath.lastComponent = {count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false}; - } - - basePath.oldPos = oldPos; - return newPos; - }, - - equals(left, right, options) { - if (options.comparator) { - return options.comparator(left, right); - } else { - return left === right - || (options.ignoreCase && left.toLowerCase() === right.toLowerCase()); - } - }, - removeEmpty(array) { - let ret = []; - for (let i = 0; i < array.length; i++) { - if (array[i]) { - ret.push(array[i]); - } - } - return ret; - }, - castInput(value) { - return value; - }, - tokenize(value) { - return Array.from(value); - }, - join(chars) { - return chars.join(''); - }, - postProcess(changeObjects) { - return changeObjects; - } -}; - -function buildValues(diff, lastComponent, newString, oldString, useLongestToken) { - // First we convert our linked list of components in reverse order to an - // array in the right order: - const components = []; - let nextComponent; - while (lastComponent) { - components.push(lastComponent); - nextComponent = lastComponent.previousComponent; - delete lastComponent.previousComponent; - lastComponent = nextComponent; - } - components.reverse(); - - let componentPos = 0, - componentLen = components.length, - newPos = 0, - oldPos = 0; - - for (; componentPos < componentLen; componentPos++) { - let component = components[componentPos]; - if (!component.removed) { - if (!component.added && useLongestToken) { - let value = newString.slice(newPos, newPos + component.count); - value = value.map(function(value, i) { - let oldValue = oldString[oldPos + i]; - return oldValue.length > value.length ? oldValue : value; - }); - - component.value = diff.join(value); - } else { - component.value = diff.join(newString.slice(newPos, newPos + component.count)); - } - newPos += component.count; - - // Common case - if (!component.added) { - oldPos += component.count; - } - } else { - component.value = diff.join(oldString.slice(oldPos, oldPos + component.count)); - oldPos += component.count; - } - } - - return components; -} diff --git a/src/diff/base.ts b/src/diff/base.ts new file mode 100644 index 000000000..68bd8f212 --- /dev/null +++ b/src/diff/base.ts @@ -0,0 +1,349 @@ +import {ChangeObject, DiffOptionsWithoutCallback, DiffOptionsWithCallback, DiffCallback} from '../types'; + +/** + * Like a ChangeObject, but with no value and an extra `previousComponent` property. + * A linked list of these (linked via `.previousComponent`) is used internally in the code below to + * keep track of the state of the diffing algorithm, but gets converted to an array of + * ChangeObjects before being returned to the caller. + */ +interface DraftChangeObject { + added: boolean; + removed: boolean; + count: number; + previousComponent?: DraftChangeObject; + + // Only added in buildValues: + value?: any; +} + +interface Path { + oldPos: number; + lastComponent?: DraftChangeObject +} + +// TODO: Types are wrong for case with a callback + +export default class Diff< + TokenT, + ValueT extends Iterable = Iterable +> { + diff( + oldString: ValueT, + newString: ValueT, + options: DiffCallback | DiffOptionsWithCallback + ): undefined + diff( + oldString: ValueT, + newString: ValueT, + options: DiffOptionsWithoutCallback + ): ChangeObject[] + diff( + oldString: ValueT, + newString: ValueT, + options: DiffCallback | DiffOptionsWithCallback | DiffOptionsWithoutCallback = {} + ): ChangeObject[] | undefined { + let callback: DiffCallback | undefined; + if (typeof options === 'function') { + callback = options; + options = {}; + } else if ('callback' in options) { + callback = options.callback; + } + // Allow subclasses to massage the input prior to running + oldString = this.castInput(oldString, options); + newString = this.castInput(newString, options); + + const oldTokens = this.removeEmpty(this.tokenize(oldString, options)); + const newTokens = this.removeEmpty(this.tokenize(newString, options)); + + return this.diffWithOptionsObj(oldTokens, newTokens, options, callback); + } + + private diffWithOptionsObj( + oldTokens: TokenT[], + newTokens: TokenT[], + options: DiffOptionsWithoutCallback | DiffOptionsWithCallback, + callback: DiffCallback | undefined + ): ChangeObject[] | undefined { + const done = (value) => { + value = this.postProcess(value, options); + if (callback) { + setTimeout(function() { callback(value); }, 0); + return true; + } else { + return value; + } + }; + + const newLen = newTokens.length, oldLen = oldTokens.length; + let editLength = 1; + let maxEditLength = newLen + oldLen; + if(options.maxEditLength != null) { + maxEditLength = Math.min(maxEditLength, options.maxEditLength); + } + const maxExecutionTime = options.timeout ?? Infinity; + const abortAfterTimestamp = Date.now() + maxExecutionTime; + + const bestPath = [{ oldPos: -1, lastComponent: undefined }]; + + // Seed editLength = 0, i.e. the content starts with the same values + let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options); + if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) { + // Identity per the equality and tokenizer + return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens)); + } + + // Once we hit the right edge of the edit graph on some diagonal k, we can + // definitely reach the end of the edit graph in no more than k edits, so + // there's no point in considering any moves to diagonal k+1 any more (from + // which we're guaranteed to need at least k+1 more edits). + // Similarly, once we've reached the bottom of the edit graph, there's no + // point considering moves to lower diagonals. + // We record this fact by setting minDiagonalToConsider and + // maxDiagonalToConsider to some finite value once we've hit the edge of + // the edit graph. + // This optimization is not faithful to the original algorithm presented in + // Myers's paper, which instead pointlessly extends D-paths off the end of + // the edit graph - see page 7 of Myers's paper which notes this point + // explicitly and illustrates it with a diagram. This has major performance + // implications for some common scenarios. For instance, to compute a diff + // where the new text simply appends d characters on the end of the + // original text of length n, the true Myers algorithm will take O(n+d^2) + // time while this optimization needs only O(n+d) time. + let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity; + + // Main worker method. checks all permutations of a given edit length for acceptance. + const execEditLength = () => { + for ( + let diagonalPath = Math.max(minDiagonalToConsider, -editLength); + diagonalPath <= Math.min(maxDiagonalToConsider, editLength); + diagonalPath += 2 + ) { + let basePath; + const removePath = bestPath[diagonalPath - 1], + addPath = bestPath[diagonalPath + 1]; + if (removePath) { + // @ts-expect-error (Nothing will ever read this element; we're doing this to free memory) + bestPath[diagonalPath - 1] = undefined; + } + + let canAdd = false; + if (addPath) { + // what newPos will be after we do an insertion: + const addPathNewPos = addPath.oldPos - diagonalPath; + canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen; + } + + const canRemove = removePath && removePath.oldPos + 1 < oldLen; + if (!canAdd && !canRemove) { + // @ts-expect-error (Nothing will ever read this element; we're doing this to free memory) + bestPath[diagonalPath] = undefined; + continue; + } + + // Select the diagonal that we want to branch from. We select the prior + // path whose position in the old string is the farthest from the origin + // and does not pass the bounds of the diff graph + if (!canRemove || (canAdd && removePath.oldPos < addPath.oldPos)) { + basePath = this.addToPath(addPath, true, false, 0, options); + } else { + basePath = this.addToPath(removePath, false, true, 1, options); + } + + newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options); + + if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) { + // If we have hit the end of both strings, then we are done + return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)); + } else { + bestPath[diagonalPath] = basePath; + if (basePath.oldPos + 1 >= oldLen) { + maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1); + } + if (newPos + 1 >= newLen) { + minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1); + } + } + } + + editLength++; + }; + + // Performs the length of edit iteration. Is a bit fugly as this has to support the + // sync and async mode which is never fun. Loops over execEditLength until a value + // is produced, or until the edit length exceeds options.maxEditLength (if given), + // in which case it will return undefined. + if (callback) { + (function exec() { + setTimeout(function() { + if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) { + return callback(); + } + + if (!execEditLength()) { + exec(); + } + }, 0); + }()); + } else { + while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) { + const ret = execEditLength(); + if (ret) { + return ret; + } + } + } + } + + private addToPath( + path: Path, + added: boolean, + removed: boolean, + oldPosInc: number, + options: DiffOptionsWithoutCallback + ): Path { + const last = path.lastComponent; + if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) { + return { + oldPos: path.oldPos + oldPosInc, + lastComponent: {count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent } + }; + } else { + return { + oldPos: path.oldPos + oldPosInc, + lastComponent: {count: 1, added: added, removed: removed, previousComponent: last } + }; + } + } + + private extractCommon( + basePath: Path, + newTokens: TokenT[], + oldTokens: TokenT[], + diagonalPath: number, + options: DiffOptionsWithoutCallback + ): number { + const newLen = newTokens.length, + oldLen = oldTokens.length; + let oldPos = basePath.oldPos, + newPos = oldPos - diagonalPath, + commonCount = 0; + + while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) { + newPos++; + oldPos++; + commonCount++; + if (options.oneChangePerToken) { + basePath.lastComponent = {count: 1, previousComponent: basePath.lastComponent, added: false, removed: false}; + } + } + + if (commonCount && !options.oneChangePerToken) { + basePath.lastComponent = {count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false}; + } + + basePath.oldPos = oldPos; + return newPos; + } + + protected equals(left: TokenT, right: TokenT, options: DiffOptionsWithoutCallback): boolean { + if (options.comparator) { + return options.comparator(left, right); + } else { + return left === right + || (!!options.ignoreCase && (left as string).toLowerCase() === (right as string).toLowerCase()); + } + } + + protected removeEmpty(array: TokenT[]): TokenT[] { + const ret: TokenT[] = []; + for (let i = 0; i < array.length; i++) { + if (array[i]) { + ret.push(array[i]); + } + } + return ret; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected castInput(value: ValueT, options: DiffOptionsWithoutCallback): ValueT { + return value; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected tokenize(value: ValueT, options: DiffOptionsWithoutCallback): TokenT[] { + return Array.from(value); + } + + protected join(chars: TokenT[]): ValueT { + // Assumes ValueT is string, which is the case for most subclasses. + // When it's false, e.g. in diffArrays, this method needs to be overridden (e.g. with a no-op) + // Yes, the casts are verbose and ugly, because this pattern - of having the base class SORT OF + // assume tokens and values are strings, but not completely - is weird and janky. + return (chars as string[]).join('') as unknown as ValueT; + } + + protected postProcess( + changeObjects: ChangeObject[], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options: DiffOptionsWithoutCallback + ): ChangeObject[] { + return changeObjects; + } + + protected get useLongestToken(): boolean { + return false; + } + + private buildValues( + lastComponent: DraftChangeObject | undefined, + newTokens: TokenT[], + oldTokens: TokenT[] + ): ChangeObject[] { + // First we convert our linked list of components in reverse order to an + // array in the right order: + const components: DraftChangeObject[] = []; + let nextComponent; + while (lastComponent) { + components.push(lastComponent); + nextComponent = lastComponent.previousComponent; + delete lastComponent.previousComponent; + lastComponent = nextComponent; + } + components.reverse(); + + const componentLen = components.length; + let componentPos = 0, + newPos = 0, + oldPos = 0; + + for (; componentPos < componentLen; componentPos++) { + const component = components[componentPos]; + if (!component.removed) { + if (!component.added && this.useLongestToken) { + let value = newTokens.slice(newPos, newPos + component.count); + value = value.map(function(value, i) { + const oldValue = oldTokens[oldPos + i]; + return (oldValue as string).length > (value as string).length ? oldValue : value; + }); + + component.value = this.join(value); + } else { + component.value = this.join(newTokens.slice(newPos, newPos + component.count)); + } + newPos += component.count; + + // Common case + if (!component.added) { + oldPos += component.count; + } + } else { + component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count)); + oldPos += component.count; + } + } + + return components as ChangeObject[]; + } +} + diff --git a/src/diff/character.js b/src/diff/character.js deleted file mode 100644 index e9f17b111..000000000 --- a/src/diff/character.js +++ /dev/null @@ -1,4 +0,0 @@ -import Diff from './base'; - -export const characterDiff = new Diff(); -export function diffChars(oldStr, newStr, options) { return characterDiff.diff(oldStr, newStr, options); } diff --git a/src/diff/character.ts b/src/diff/character.ts new file mode 100644 index 000000000..67d70dcad --- /dev/null +++ b/src/diff/character.ts @@ -0,0 +1,16 @@ +import Diff from './base'; +import {CallbackOption, DiffCallback, ChangeObject, DiffCharsOptions} from '../types'; + +class CharacterDiff extends Diff {} + +export const characterDiff = new CharacterDiff(); + +export function diffChars( + oldStr: string, + newStr: string, + options: (DiffCharsOptions & CallbackOption) | DiffCallback +): undefined +export function diffChars(oldStr: string, newStr: string, options: DiffCharsOptions): ChangeObject[]; +export function diffChars(oldStr: string, newStr: string, options): undefined | ChangeObject[] { + return characterDiff.diff(oldStr, newStr, options); +} diff --git a/src/diff/css.js b/src/diff/css.js deleted file mode 100644 index e2e445a60..000000000 --- a/src/diff/css.js +++ /dev/null @@ -1,8 +0,0 @@ -import Diff from './base'; - -export const cssDiff = new Diff(); -cssDiff.tokenize = function(value) { - return value.split(/([{}:;,]|\s+)/); -}; - -export function diffCss(oldStr, newStr, callback) { return cssDiff.diff(oldStr, newStr, callback); } diff --git a/src/diff/css.ts b/src/diff/css.ts new file mode 100644 index 000000000..eda81cfa6 --- /dev/null +++ b/src/diff/css.ts @@ -0,0 +1,20 @@ +import Diff from './base'; +import { CallbackOption, ChangeObject, DiffCallback, DiffCssOptions } from '../types'; + +class CssDiff extends Diff { + protected tokenize(value: string) { + return value.split(/([{}:;,]|\s+)/); + } +} + +export const cssDiff = new CssDiff(); + +export function diffCss( + oldStr: string, + newStr: string, + options: (DiffCssOptions & CallbackOption) | DiffCallback +): undefined +export function diffCss(oldStr: string, newStr: string, options: DiffCssOptions): ChangeObject[]; +export function diffCss(oldStr: string, newStr: string, options): undefined | ChangeObject[] { + return cssDiff.diff(oldStr, newStr, options); +} diff --git a/src/diff/json.js b/src/diff/json.js deleted file mode 100644 index 301d1a946..000000000 --- a/src/diff/json.js +++ /dev/null @@ -1,80 +0,0 @@ -import Diff from './base'; -import {lineDiff} from './line'; - -export const jsonDiff = new Diff(); -// Discriminate between two lines of pretty-printed, serialized JSON where one of them has a -// dangling comma and the other doesn't. Turns out including the dangling comma yields the nicest output: -jsonDiff.useLongestToken = true; - -jsonDiff.tokenize = lineDiff.tokenize; -jsonDiff.castInput = function(value, options) { - const {undefinedReplacement, stringifyReplacer = (k, v) => typeof v === 'undefined' ? undefinedReplacement : v} = options; - - return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, ' '); -}; -jsonDiff.equals = function(left, right, options) { - return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1'), options); -}; - -export function diffJson(oldObj, newObj, options) { return jsonDiff.diff(oldObj, newObj, options); } - -// This function handles the presence of circular references by bailing out when encountering an -// object that is already on the "stack" of items being processed. Accepts an optional replacer -export function canonicalize(obj, stack, replacementStack, replacer, key) { - stack = stack || []; - replacementStack = replacementStack || []; - - if (replacer) { - obj = replacer(key, obj); - } - - let i; - - for (i = 0; i < stack.length; i += 1) { - if (stack[i] === obj) { - return replacementStack[i]; - } - } - - let canonicalizedObj; - - if ('[object Array]' === Object.prototype.toString.call(obj)) { - stack.push(obj); - canonicalizedObj = new Array(obj.length); - replacementStack.push(canonicalizedObj); - for (i = 0; i < obj.length; i += 1) { - canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key); - } - stack.pop(); - replacementStack.pop(); - return canonicalizedObj; - } - - if (obj && obj.toJSON) { - obj = obj.toJSON(); - } - - if (typeof obj === 'object' && obj !== null) { - stack.push(obj); - canonicalizedObj = {}; - replacementStack.push(canonicalizedObj); - let sortedKeys = [], - key; - for (key in obj) { - /* istanbul ignore else */ - if (Object.prototype.hasOwnProperty.call(obj, key)) { - sortedKeys.push(key); - } - } - sortedKeys.sort(); - for (i = 0; i < sortedKeys.length; i += 1) { - key = sortedKeys[i]; - canonicalizedObj[key] = canonicalize(obj[key], stack, replacementStack, replacer, key); - } - stack.pop(); - replacementStack.pop(); - } else { - canonicalizedObj = obj; - } - return canonicalizedObj; -} diff --git a/src/diff/json.ts b/src/diff/json.ts new file mode 100644 index 000000000..e36850449 --- /dev/null +++ b/src/diff/json.ts @@ -0,0 +1,98 @@ +import Diff from './base'; +import { CallbackOption, ChangeObject, DiffCallback, DiffJsonOptions } from '../types'; +import {lineDiff} from './line'; + +class JsonDiff extends Diff { + protected get useLongestToken() { + // Discriminate between two lines of pretty-printed, serialized JSON where one of them has a + // dangling comma and the other doesn't. Turns out including the dangling comma yields the nicest output: + return true; + } + + protected tokenize = lineDiff.tokenize; + + protected castInput(value: string, options: DiffJsonOptions) { + const {undefinedReplacement, stringifyReplacer = (k, v) => typeof v === 'undefined' ? undefinedReplacement : v} = options; + + return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, ' '); + } + + protected equals(left: string, right: string, options: DiffJsonOptions) { + return super.equals(left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1'), options); + } +} + +const jsonDiff = new JsonDiff(); + + +export function diffJson( + oldStr: string, + newStr: string, + options: (DiffJsonOptions & CallbackOption) | DiffCallback +): undefined +export function diffJson(oldStr: string, newStr: string, options: DiffJsonOptions): ChangeObject[]; +export function diffJson(oldStr: string, newStr: string, options): undefined | ChangeObject[] { + return jsonDiff.diff(oldStr, newStr, options); +} + + +// This function handles the presence of circular references by bailing out when encountering an +// object that is already on the "stack" of items being processed. Accepts an optional replacer +export function canonicalize(obj: any, stack: Array | null, replacementStack: Array | null, replacer: (string, any) => any, key?: string) { + stack = stack || []; + replacementStack = replacementStack || []; + + if (replacer) { + obj = replacer(key, obj); + } + + let i; + + for (i = 0; i < stack.length; i += 1) { + if (stack[i] === obj) { + return replacementStack[i]; + } + } + + let canonicalizedObj; + + if ('[object Array]' === Object.prototype.toString.call(obj)) { + stack.push(obj); + canonicalizedObj = new Array(obj.length); + replacementStack.push(canonicalizedObj); + for (i = 0; i < obj.length; i += 1) { + canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key); + } + stack.pop(); + replacementStack.pop(); + return canonicalizedObj; + } + + if (obj && obj.toJSON) { + obj = obj.toJSON(); + } + + if (typeof obj === 'object' && obj !== null) { + stack.push(obj); + canonicalizedObj = {}; + replacementStack.push(canonicalizedObj); + const sortedKeys = []; + let key; + for (key in obj) { + /* istanbul ignore else */ + if (Object.prototype.hasOwnProperty.call(obj, key)) { + sortedKeys.push(key); + } + } + sortedKeys.sort(); + for (i = 0; i < sortedKeys.length; i += 1) { + key = sortedKeys[i]; + canonicalizedObj[key] = canonicalize(obj[key], stack, replacementStack, replacer, key); + } + stack.pop(); + replacementStack.pop(); + } else { + canonicalizedObj = obj; + } + return canonicalizedObj; +} diff --git a/src/diff/line.js b/src/diff/line.js deleted file mode 100644 index 5d313013f..000000000 --- a/src/diff/line.js +++ /dev/null @@ -1,70 +0,0 @@ -import Diff from './base'; -import {generateOptions} from '../util/params'; - -export const lineDiff = new Diff(); -lineDiff.tokenize = function(value, options) { - if(options.stripTrailingCr) { - // remove one \r before \n to match GNU diff's --strip-trailing-cr behavior - value = value.replace(/\r\n/g, '\n'); - } - - let retLines = [], - linesAndNewlines = value.split(/(\n|\r\n)/); - - // Ignore the final empty token that occurs if the string ends with a new line - if (!linesAndNewlines[linesAndNewlines.length - 1]) { - linesAndNewlines.pop(); - } - - // Merge the content and line separators into single tokens - for (let i = 0; i < linesAndNewlines.length; i++) { - let line = linesAndNewlines[i]; - - if (i % 2 && !options.newlineIsToken) { - retLines[retLines.length - 1] += line; - } else { - retLines.push(line); - } - } - - return retLines; -}; - -lineDiff.equals = function(left, right, options) { - // If we're ignoring whitespace, we need to normalise lines by stripping - // whitespace before checking equality. (This has an annoying interaction - // with newlineIsToken that requires special handling: if newlines get their - // own token, then we DON'T want to trim the *newline* tokens down to empty - // strings, since this would cause us to treat whitespace-only line content - // as equal to a separator between lines, which would be weird and - // inconsistent with the documented behavior of the options.) - if (options.ignoreWhitespace) { - if (!options.newlineIsToken || !left.includes('\n')) { - left = left.trim(); - } - if (!options.newlineIsToken || !right.includes('\n')) { - right = right.trim(); - } - } else if (options.ignoreNewlineAtEof && !options.newlineIsToken) { - if (left.endsWith('\n')) { - left = left.slice(0, -1); - } - if (right.endsWith('\n')) { - right = right.slice(0, -1); - } - } - return Diff.prototype.equals.call(this, left, right, options); -}; - -export function diffLines(oldStr, newStr, callback) { return lineDiff.diff(oldStr, newStr, callback); } - -// Kept for backwards compatibility. This is a rather arbitrary wrapper method -// that just calls `diffLines` with `ignoreWhitespace: true`. It's confusing to -// have two ways to do exactly the same thing in the API, so we no longer -// document this one (library users should explicitly use `diffLines` with -// `ignoreWhitespace: true` instead) but we keep it around to maintain -// compatibility with code that used old versions. -export function diffTrimmedLines(oldStr, newStr, callback) { - let options = generateOptions(callback, {ignoreWhitespace: true}); - return lineDiff.diff(oldStr, newStr, options); -} diff --git a/src/diff/line.ts b/src/diff/line.ts new file mode 100644 index 000000000..d908c8778 --- /dev/null +++ b/src/diff/line.ts @@ -0,0 +1,90 @@ +import Diff from './base'; +import { CallbackOption, ChangeObject, DiffCallback, DiffLinesOptions } from '../types'; +import {generateOptions} from '../util/params'; + + +class LineDiff extends Diff { + // public so it can be copied by jsonDiff + public tokenize(value: string, options: DiffLinesOptions) { + if(options.stripTrailingCr) { + // remove one \r before \n to match GNU diff's --strip-trailing-cr behavior + value = value.replace(/\r\n/g, '\n'); + } + + const retLines = [], + linesAndNewlines = value.split(/(\n|\r\n)/); + + // Ignore the final empty token that occurs if the string ends with a new line + if (!linesAndNewlines[linesAndNewlines.length - 1]) { + linesAndNewlines.pop(); + } + + // Merge the content and line separators into single tokens + for (let i = 0; i < linesAndNewlines.length; i++) { + const line = linesAndNewlines[i]; + + if (i % 2 && !options.newlineIsToken) { + retLines[retLines.length - 1] += line; + } else { + retLines.push(line); + } + } + + return retLines; + } + + protected equals(left: string, right: string, options: DiffLinesOptions) { + // If we're ignoring whitespace, we need to normalise lines by stripping + // whitespace before checking equality. (This has an annoying interaction + // with newlineIsToken that requires special handling: if newlines get their + // own token, then we DON'T want to trim the *newline* tokens down to empty + // strings, since this would cause us to treat whitespace-only line content + // as equal to a separator between lines, which would be weird and + // inconsistent with the documented behavior of the options.) + if (options.ignoreWhitespace) { + if (!options.newlineIsToken || !left.includes('\n')) { + left = left.trim(); + } + if (!options.newlineIsToken || !right.includes('\n')) { + right = right.trim(); + } + } else if (options.ignoreNewlineAtEof && !options.newlineIsToken) { + if (left.endsWith('\n')) { + left = left.slice(0, -1); + } + if (right.endsWith('\n')) { + right = right.slice(0, -1); + } + } + return super.equals(left, right, options); + } +} + +export const lineDiff = new LineDiff(); + +export function diffLines( + oldStr: string, + newStr: string, + options: (DiffLinesOptions & CallbackOption) | DiffCallback +): undefined +export function diffLines(oldStr: string, newStr: string, options: DiffLinesOptions): ChangeObject[]; +export function diffLines(oldStr: string, newStr: string, options): undefined | ChangeObject[] { + return lineDiff.diff(oldStr, newStr, options); +} + +// Kept for backwards compatibility. This is a rather arbitrary wrapper method +// that just calls `diffLines` with `ignoreWhitespace: true`. It's confusing to +// have two ways to do exactly the same thing in the API, so we no longer +// document this one (library users should explicitly use `diffLines` with +// `ignoreWhitespace: true` instead) but we keep it around to maintain +// compatibility with code that used old versions. +export function diffTrimmedLines( + oldStr: string, + newStr: string, + options: (DiffLinesOptions & CallbackOption) | DiffCallback +): undefined +export function diffTrimmedLines(oldStr: string, newStr: string, options: DiffLinesOptions): ChangeObject[]; +export function diffTrimmedLines(oldStr: string, newStr: string, options): undefined | ChangeObject[] { + options = generateOptions(options, {ignoreWhitespace: true}); + return lineDiff.diff(oldStr, newStr, options); +} diff --git a/src/diff/sentence.js b/src/diff/sentence.js deleted file mode 100644 index fe699b6dd..000000000 --- a/src/diff/sentence.js +++ /dev/null @@ -1,9 +0,0 @@ -import Diff from './base'; - - -export const sentenceDiff = new Diff(); -sentenceDiff.tokenize = function(value) { - return value.split(/(?<=[.!?])(\s+|$)/); -}; - -export function diffSentences(oldStr, newStr, callback) { return sentenceDiff.diff(oldStr, newStr, callback); } diff --git a/src/diff/sentence.ts b/src/diff/sentence.ts new file mode 100644 index 000000000..5ba5a37ba --- /dev/null +++ b/src/diff/sentence.ts @@ -0,0 +1,20 @@ +import Diff from './base'; +import { CallbackOption, ChangeObject, DiffCallback, DiffSentencesOptions } from '../types'; + +class SentenceDiff extends Diff { + protected tokenize(value: string) { + return value.split(/(?<=[.!?])(\s+|$)/); + } +} + +export const sentenceDiff = new SentenceDiff(); + +export function diffSentences( + oldStr: string, + newStr: string, + options: (DiffSentencesOptions & CallbackOption) | DiffCallback +): undefined +export function diffSentences(oldStr: string, newStr: string, options: DiffSentencesOptions): ChangeObject[]; +export function diffSentences(oldStr: string, newStr: string, options): undefined | ChangeObject[] { + return sentenceDiff.diff(oldStr, newStr, options); +} diff --git a/src/diff/word.js b/src/diff/word.ts similarity index 67% rename from src/diff/word.js rename to src/diff/word.ts index 3bd534ed4..79b889be0 100644 --- a/src/diff/word.js +++ b/src/diff/word.ts @@ -1,4 +1,5 @@ import Diff from './base'; +import { CallbackOption, ChangeObject, DiffCallback, DiffWordsOptions } from '../types'; import { longestCommonPrefix, longestCommonSuffix, replacePrefix, replaceSuffix, removePrefix, removeSuffix, maximumOverlap, leadingWs, trailingWs } from '../util/string'; // Based on https://en.wikipedia.org/wiki/Latin_script_in_Unicode @@ -48,96 +49,106 @@ const extendedWordChars = 'a-zA-Z0-9_\\u{C0}-\\u{FF}\\u{D8}-\\u{F6}\\u{F8}-\\u{2 // tokens. const tokenizeIncludingWhitespace = new RegExp(`[${extendedWordChars}]+|\\s+|[^${extendedWordChars}]`, 'ug'); -export const wordDiff = new Diff(); -wordDiff.equals = function(left, right, options) { - if (options.ignoreCase) { - left = left.toLowerCase(); - right = right.toLowerCase(); - } - - return left.trim() === right.trim(); -}; -wordDiff.tokenize = function(value, options = {}) { - let parts; - if (options.intlSegmenter) { - if (options.intlSegmenter.resolvedOptions().granularity != 'word') { - throw new Error('The segmenter passed must have a granularity of "word"'); +class WordDiff extends Diff { + protected equals(left: string, right: string, options: DiffWordsOptions) { + if (options.ignoreCase) { + left = left.toLowerCase(); + right = right.toLowerCase(); } - parts = Array.from(options.intlSegmenter.segment(value), segment => segment.segment); - } else { - parts = value.match(tokenizeIncludingWhitespace) || []; + + return left.trim() === right.trim(); } - const tokens = []; - let prevPart = null; - parts.forEach(part => { - if ((/\s/).test(part)) { - if (prevPart == null) { - tokens.push(part); - } else { - tokens.push(tokens.pop() + part); - } - } else if ((/\s/).test(prevPart)) { - if (tokens[tokens.length - 1] == prevPart) { - tokens.push(tokens.pop() + part); - } else { - tokens.push(prevPart + part); + + protected tokenize(value: string, options: DiffWordsOptions = {}) { + let parts; + if (options.intlSegmenter) { + if (options.intlSegmenter.resolvedOptions().granularity != 'word') { + throw new Error('The segmenter passed must have a granularity of "word"'); } + parts = Array.from(options.intlSegmenter.segment(value), segment => segment.segment); } else { - tokens.push(part); + parts = value.match(tokenizeIncludingWhitespace) || []; } + const tokens: string[] = []; + let prevPart = null; + parts.forEach(part => { + if ((/\s/).test(part)) { + if (prevPart == null) { + tokens.push(part); + } else { + tokens.push(tokens.pop() + part); + } + } else if (prevPart != null && (/\s/).test(prevPart)) { + if (tokens[tokens.length - 1] == prevPart) { + tokens.push(tokens.pop() + part); + } else { + tokens.push(prevPart + part); + } + } else { + tokens.push(part); + } - prevPart = part; - }); - return tokens; -}; - -wordDiff.join = function(tokens) { - // Tokens being joined here will always have appeared consecutively in the - // same text, so we can simply strip off the leading whitespace from all the - // tokens except the first (and except any whitespace-only tokens - but such - // a token will always be the first and only token anyway) and then join them - // and the whitespace around words and punctuation will end up correct. - return tokens.map((token, i) => { - if (i == 0) { - return token; - } else { - return token.replace((/^\s+/), ''); - } - }).join(''); -}; + prevPart = part; + }); + return tokens; + } -wordDiff.postProcess = function(changes, options) { - if (!changes || options.oneChangePerToken) { - return changes; + protected join(tokens) { + // Tokens being joined here will always have appeared consecutively in the + // same text, so we can simply strip off the leading whitespace from all the + // tokens except the first (and except any whitespace-only tokens - but such + // a token will always be the first and only token anyway) and then join them + // and the whitespace around words and punctuation will end up correct. + return tokens.map((token, i) => { + if (i == 0) { + return token; + } else { + return token.replace((/^\s+/), ''); + } + }).join(''); } - let lastKeep = null; - // Change objects representing any insertion or deletion since the last - // "keep" change object. There can be at most one of each. - let insertion = null; - let deletion = null; - changes.forEach(change => { - if (change.added) { - insertion = change; - } else if (change.removed) { - deletion = change; - } else { - if (insertion || deletion) { // May be false at start of text - dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change); + protected postProcess(changes, options) { + if (!changes || options.oneChangePerToken) { + return changes; + } + + let lastKeep = null; + // Change objects representing any insertion or deletion since the last + // "keep" change object. There can be at most one of each. + let insertion = null; + let deletion = null; + changes.forEach(change => { + if (change.added) { + insertion = change; + } else if (change.removed) { + deletion = change; + } else { + if (insertion || deletion) { // May be false at start of text + dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change); + } + lastKeep = change; + insertion = null; + deletion = null; } - lastKeep = change; - insertion = null; - deletion = null; + }); + if (insertion || deletion) { + dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null); } - }); - if (insertion || deletion) { - dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null); + return changes; } - return changes; -}; +} + +export const wordDiff = new WordDiff(); -export function diffWords(oldStr, newStr, options) { +export function diffWords( + oldStr: string, + newStr: string, + options: (DiffWordsOptions & CallbackOption) | DiffCallback +): undefined +export function diffWords(oldStr: string, newStr: string, options: DiffWordsOptions): ChangeObject[]; +export function diffWords(oldStr: string, newStr: string, options): undefined | ChangeObject[] { // This option has never been documented and never will be (it's clearer to // just call `diffWordsWithSpace` directly if you need that behavior), but // has existed in jsdiff for a long time, so we retain support for it here @@ -273,16 +284,25 @@ function dedupeWhitespaceInChangeObjects(startKeep, deletion, insertion, endKeep } -export const wordWithSpaceDiff = new Diff(); -wordWithSpaceDiff.tokenize = function(value) { - // Slightly different to the tokenizeIncludingWhitespace regex used above in - // that this one treats each individual newline as a distinct tokens, rather - // than merging them into other surrounding whitespace. This was requested - // in https://github.com/kpdecker/jsdiff/issues/180 & - // https://github.com/kpdecker/jsdiff/issues/211 - const regex = new RegExp(`(\\r?\\n)|[${extendedWordChars}]+|[^\\S\\n\\r]+|[^${extendedWordChars}]`, 'ug'); - return value.match(regex) || []; -}; -export function diffWordsWithSpace(oldStr, newStr, options) { - return wordWithSpaceDiff.diff(oldStr, newStr, options); +class WordsWithSpaceDiff extends Diff { + protected tokenize(value: string) { + // Slightly different to the tokenizeIncludingWhitespace regex used above in + // that this one treats each individual newline as a distinct tokens, rather + // than merging them into other surrounding whitespace. This was requested + // in https://github.com/kpdecker/jsdiff/issues/180 & + // https://github.com/kpdecker/jsdiff/issues/211 + const regex = new RegExp(`(\\r?\\n)|[${extendedWordChars}]+|[^\\S\\n\\r]+|[^${extendedWordChars}]`, 'ug'); + return value.match(regex) || []; + } +} + +export const wordsWithSpaceDiff = new WordsWithSpaceDiff(); +export function diffWordsWithSpace( + oldStr: string, + newStr: string, + options: (DiffWordsOptions & CallbackOption) | DiffCallback +): undefined +export function diffWordsWithSpace(oldStr: string, newStr: string, options: DiffWordsOptions): ChangeObject[]; +export function diffWordsWithSpace(oldStr: string, newStr: string, options): undefined | ChangeObject[] { + return wordsWithSpaceDiff.diff(oldStr, newStr, options); } diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/patch/apply.js b/src/patch/apply.ts similarity index 78% rename from src/patch/apply.js rename to src/patch/apply.ts index 612e0658b..2d9ed4cd4 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.ts @@ -2,35 +2,54 @@ import {hasOnlyWinLineEndings, hasOnlyUnixLineEndings} from '../util/string'; import {isWin, isUnix, unixToWin, winToUnix} from './line-endings'; import {parsePatch} from './parse'; import distanceIterator from '../util/distance-iterator'; +import { StructuredPatch } from '../types'; -export function applyPatch(source, uniDiff, options = {}) { +export interface ApplyPatchOptions { + fuzzFactor?: number, + autoConvertLineEndings?: boolean, + compareLine?: (lineNumber: number, line: string, operation: string, patchContent: string) => boolean, +} + +export function applyPatch( + source: string, + uniDiff: string | StructuredPatch | [StructuredPatch], + options: ApplyPatchOptions = {} +): string | boolean { + let patches: StructuredPatch[]; if (typeof uniDiff === 'string') { - uniDiff = parsePatch(uniDiff); + patches = parsePatch(uniDiff); + } else if (Array.isArray(uniDiff)) { + patches = uniDiff; + } else { + patches = [uniDiff]; } - if (Array.isArray(uniDiff)) { - if (uniDiff.length > 1) { - throw new Error('applyPatch only works with a single input.'); - } - - uniDiff = uniDiff[0]; + if (patches.length > 1) { + throw new Error('applyPatch only works with a single input.'); } + return applyStructuredPatch(source, patches[0], options); +} + +function applyStructuredPatch( + source: string, + patch: StructuredPatch, + options: ApplyPatchOptions = {} +): string | boolean { if (options.autoConvertLineEndings || options.autoConvertLineEndings == null) { - if (hasOnlyWinLineEndings(source) && isUnix(uniDiff)) { - uniDiff = unixToWin(uniDiff); - } else if (hasOnlyUnixLineEndings(source) && isWin(uniDiff)) { - uniDiff = winToUnix(uniDiff); + if (hasOnlyWinLineEndings(source) && isUnix(patch)) { + patch = unixToWin(patch); + } else if (hasOnlyUnixLineEndings(source) && isWin(patch)) { + patch = winToUnix(patch); } } // Apply the diff to the input - let lines = source.split('\n'), - hunks = uniDiff.hunks, - - compareLine = options.compareLine || ((lineNumber, line, operation, patchContent) => line === patchContent), - fuzzFactor = options.fuzzFactor || 0, - minLine = 0; + const lines = source.split('\n'), + hunks = patch.hunks, + compareLine = options.compareLine || ((lineNumber, line, operation, patchContent) => line === patchContent), + fuzzFactor = options.fuzzFactor || 0; + let minLine = 0; if (fuzzFactor < 0 || !Number.isInteger(fuzzFactor)) { throw new Error('fuzzFactor must be a non-negative integer'); @@ -94,18 +113,18 @@ export function applyPatch(source, uniDiff, options = {}) { * `replacementLines`. Otherwise, returns null. */ function applyHunk( - hunkLines, - toPos, - maxErrors, - hunkLinesI = 0, - lastContextLineMatched = true, - patchedLines = [], - patchedLinesLength = 0 + hunkLines: string[], + toPos: number, + maxErrors: number, + hunkLinesI: number = 0, + lastContextLineMatched: boolean = true, + patchedLines: string[] = [], + patchedLinesLength: number = 0 ) { let nConsecutiveOldContextLines = 0; let nextContextLineMustMatch = false; for (; hunkLinesI < hunkLines.length; hunkLinesI++) { - let hunkLine = hunkLines[hunkLinesI], + const hunkLine = hunkLines[hunkLinesI], operation = (hunkLine.length > 0 ? hunkLine[0] : ' '), content = (hunkLine.length > 0 ? hunkLine.substr(1) : hunkLine); @@ -204,18 +223,18 @@ export function applyPatch(source, uniDiff, options = {}) { }; } - const resultLines = []; + const resultLines: string[] = []; // Search best fit offsets for each hunk based on the previous ones let prevHunkOffset = 0; for (let i = 0; i < hunks.length; i++) { const hunk = hunks[i]; let hunkResult; - let maxLine = lines.length - hunk.oldLines + fuzzFactor; + const maxLine = lines.length - hunk.oldLines + fuzzFactor; let toPos; for (let maxErrors = 0; maxErrors <= fuzzFactor; maxErrors++) { toPos = hunk.oldStart + prevHunkOffset - 1; - let iterator = distanceIterator(toPos, minLine, maxLine); + const iterator = distanceIterator(toPos, minLine, maxLine); for (; toPos !== undefined; toPos = iterator()) { hunkResult = applyHunk(hunk.lines, toPos, maxErrors); if (hunkResult) { @@ -259,15 +278,21 @@ export function applyPatch(source, uniDiff, options = {}) { return resultLines.join('\n'); } +export interface ApplyPatchesOptions extends ApplyPatchOptions { + loadFile: (index: StructuredPatch, callback: (err: any, data: string) => void) => void, + patched: (index: StructuredPatch, content: string, callback: (err: any) => void) => void, + complete: (err?: any) => void, +} + // Wrapper that supports multiple file patches via callbacks. -export function applyPatches(uniDiff, options) { +export function applyPatches(uniDiff: string | StructuredPatch[], options): void { if (typeof uniDiff === 'string') { uniDiff = parsePatch(uniDiff); } let currentIndex = 0; - function processIndex() { - let index = uniDiff[currentIndex++]; + function processIndex(): void { + const index = uniDiff[currentIndex++]; if (!index) { return options.complete(); } @@ -277,7 +302,7 @@ export function applyPatches(uniDiff, options) { return options.complete(err); } - let updatedContent = applyPatch(data, index, options); + const updatedContent = applyPatch(data, index, options); options.patched(index, updatedContent, function(err) { if (err) { return options.complete(err); diff --git a/src/patch/line-endings.js b/src/patch/line-endings.js deleted file mode 100644 index d1907b47a..000000000 --- a/src/patch/line-endings.js +++ /dev/null @@ -1,62 +0,0 @@ -export function unixToWin(patch) { - if (Array.isArray(patch)) { - return patch.map(unixToWin); - } - - return { - ...patch, - hunks: patch.hunks.map(hunk => ({ - ...hunk, - lines: hunk.lines.map( - (line, i) => - (line.startsWith('\\') || line.endsWith('\r') || hunk.lines[i + 1]?.startsWith('\\')) - ? line - : line + '\r' - ) - })) - }; -} - -export function winToUnix(patch) { - if (Array.isArray(patch)) { - return patch.map(winToUnix); - } - - return { - ...patch, - hunks: patch.hunks.map(hunk => ({ - ...hunk, - lines: hunk.lines.map(line => line.endsWith('\r') ? line.substring(0, line.length - 1) : line) - })) - }; -} - -/** - * Returns true if the patch consistently uses Unix line endings (or only involves one line and has - * no line endings). - */ -export function isUnix(patch) { - if (!Array.isArray(patch)) { patch = [patch]; } - return !patch.some( - index => index.hunks.some( - hunk => hunk.lines.some( - line => !line.startsWith('\\') && line.endsWith('\r') - ) - ) - ); -} - -/** - * Returns true if the patch uses Windows line endings and only Windows line endings. - */ -export function isWin(patch) { - if (!Array.isArray(patch)) { patch = [patch]; } - return patch.some(index => index.hunks.some(hunk => hunk.lines.some(line => line.endsWith('\r')))) - && patch.every( - index => index.hunks.every( - hunk => hunk.lines.every( - (line, i) => line.startsWith('\\') || line.endsWith('\r') || hunk.lines[i + 1]?.startsWith('\\') - ) - ) - ); -} diff --git a/src/patch/line-endings.ts b/src/patch/line-endings.ts new file mode 100644 index 000000000..64f65e024 --- /dev/null +++ b/src/patch/line-endings.ts @@ -0,0 +1,77 @@ +import { StructuredPatch } from '../types'; + +export function unixToWin(patch: StructuredPatch): StructuredPatch; +export function unixToWin(patches: StructuredPatch[]): StructuredPatch[]; +export function unixToWin(patch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[]; +export function unixToWin(patch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[] { + if (Array.isArray(patch)) { + // It would be cleaner if instead of the line below we could just write + // return patch.map(unixToWin) + // but mysteriously TypeScript (v5.7.3 at the time of writing) does not like this and it will + // refuse to compile, thinking that unixToWin could then return StructuredPatch[][] and the + // result would be incompatible with the overload signatures. + // See bug report at https://github.com/microsoft/TypeScript/issues/61398. + return patch.map(p => unixToWin(p)); + } + + return { + ...patch, + hunks: patch.hunks.map(hunk => ({ + ...hunk, + lines: hunk.lines.map( + (line, i) => + (line.startsWith('\\') || line.endsWith('\r') || hunk.lines[i + 1]?.startsWith('\\')) + ? line + : line + '\r' + ) + })) + }; +} + +export function winToUnix(patch: StructuredPatch): StructuredPatch; +export function winToUnix(patches: StructuredPatch[]): StructuredPatch[]; +export function winToUnix(patch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[]; +export function winToUnix(patch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[] { + if (Array.isArray(patch)) { + // (See comment above equivalent line in unixToWin) + return patch.map(p => winToUnix(p)); + } + + return { + ...patch, + hunks: patch.hunks.map(hunk => ({ + ...hunk, + lines: hunk.lines.map(line => line.endsWith('\r') ? line.substring(0, line.length - 1) : line) + })) + }; +} + +/** + * Returns true if the patch consistently uses Unix line endings (or only involves one line and has + * no line endings). + */ +export function isUnix(patch: StructuredPatch | StructuredPatch[]): boolean { + if (!Array.isArray(patch)) { patch = [patch]; } + return !patch.some( + index => index.hunks.some( + hunk => hunk.lines.some( + line => !line.startsWith('\\') && line.endsWith('\r') + ) + ) + ); +} + +/** + * Returns true if the patch uses Windows line endings and only Windows line endings. + */ +export function isWin(patch: StructuredPatch | StructuredPatch[]): boolean { + if (!Array.isArray(patch)) { patch = [patch]; } + return patch.some(index => index.hunks.some(hunk => hunk.lines.some(line => line.endsWith('\r')))) + && patch.every( + index => index.hunks.every( + hunk => hunk.lines.every( + (line, i) => line.startsWith('\\') || line.endsWith('\r') || hunk.lines[i + 1]?.startsWith('\\') + ) + ) + ); +} diff --git a/src/patch/parse.js b/src/patch/parse.ts similarity index 80% rename from src/patch/parse.js rename to src/patch/parse.ts index 61334b23b..965ccba26 100755 --- a/src/patch/parse.js +++ b/src/patch/parse.ts @@ -1,23 +1,25 @@ -export function parsePatch(uniDiff) { - let diffstr = uniDiff.split(/\n/), - list = [], - i = 0; +import { StructuredPatch } from '../types'; + +export function parsePatch(uniDiff: string): StructuredPatch[] { + const diffstr = uniDiff.split(/\n/), + list: Partial[] = []; + let i = 0; function parseIndex() { - let index = {}; + const index: Partial = {}; list.push(index); // Parse diff metadata while (i < diffstr.length) { - let line = diffstr[i]; + const line = diffstr[i]; // File header found, end parsing diff metadata - if ((/^(\-\-\-|\+\+\+|@@)\s/).test(line)) { + if ((/^(---|\+\+\+|@@)\s/).test(line)) { break; } // Diff index - let header = (/^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/).exec(line); + const header = (/^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/).exec(line); if (header) { index.index = header[1]; } @@ -34,8 +36,8 @@ export function parsePatch(uniDiff) { index.hunks = []; while (i < diffstr.length) { - let line = diffstr[i]; - if ((/^(Index:\s|diff\s|\-\-\-\s|\+\+\+\s|===================================================================)/).test(line)) { + const line = diffstr[i]; + if ((/^(Index:\s|diff\s|---\s|\+\+\+\s|===================================================================)/).test(line)) { break; } else if ((/^@@/).test(line)) { index.hunks.push(parseHunk()); @@ -52,7 +54,7 @@ export function parsePatch(uniDiff) { function parseFileHeader(index) { const fileHeader = (/^(---|\+\+\+)\s+(.*)\r?$/).exec(diffstr[i]); if (fileHeader) { - let keyPrefix = fileHeader[1] === '---' ? 'old' : 'new'; + const keyPrefix = fileHeader[1] === '---' ? 'old' : 'new'; const data = fileHeader[2].split('\t', 2); let fileName = data[0].replace(/\\\\/g, '\\'); if ((/^".*"$/).test(fileName)) { @@ -68,11 +70,11 @@ export function parsePatch(uniDiff) { // Parses a hunk // This assumes that we are at the start of a hunk. function parseHunk() { - let chunkHeaderIndex = i, + const chunkHeaderIndex = i, chunkHeaderLine = diffstr[i++], chunkHeader = chunkHeaderLine.split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/); - let hunk = { + const hunk = { oldStart: +chunkHeader[1], oldLines: typeof chunkHeader[2] === 'undefined' ? 1 : +chunkHeader[2], newStart: +chunkHeader[3], @@ -97,7 +99,7 @@ export function parsePatch(uniDiff) { i < diffstr.length && (removeCount < hunk.oldLines || addCount < hunk.newLines || diffstr[i]?.startsWith('\\')); i++ ) { - let operation = (diffstr[i].length == 0 && i != (diffstr.length - 1)) ? ' ' : diffstr[i][0]; + const operation = (diffstr[i].length == 0 && i != (diffstr.length - 1)) ? ' ' : diffstr[i][0]; if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') { hunk.lines.push(diffstr[i]); @@ -137,5 +139,5 @@ export function parsePatch(uniDiff) { parseIndex(); } - return list; + return list as StructuredPatch[]; } diff --git a/src/patch/reverse.js b/src/patch/reverse.ts similarity index 51% rename from src/patch/reverse.js rename to src/patch/reverse.ts index e839eebaa..e56affe54 100644 --- a/src/patch/reverse.js +++ b/src/patch/reverse.ts @@ -1,6 +1,12 @@ -export function reversePatch(structuredPatch) { +import { StructuredPatch } from '../types'; + +export function reversePatch(structuredPatch: StructuredPatch): StructuredPatch; +export function reversePatch(structuredPatch: StructuredPatch[]): StructuredPatch[]; +export function reversePatch(structuredPatch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[]; +export function reversePatch(structuredPatch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[] { if (Array.isArray(structuredPatch)) { - return structuredPatch.map(reversePatch).reverse(); + // (See comment in unixToWin for why we need the pointless-looking anonymous function here) + return structuredPatch.map(patch => reversePatch(patch)).reverse(); } return { diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..53d50b151 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,84 @@ +export interface ChangeObject { + value: ValueT; + added: boolean; + removed: boolean; + count: number; +} + +export interface CommonDiffOptions { + maxEditLength?: number, + timeout?: number, + oneChangePerToken?: boolean, +} + +export interface DiffArraysOptions extends CommonDiffOptions { + comparator?: (a: any, b: any) => boolean, +} + +export interface DiffCharsOptions extends CommonDiffOptions { + ignoreCase?: boolean, +} + +export interface DiffLinesOptions extends CommonDiffOptions { + stripTrailingCr?: boolean, + newlineIsToken?: boolean, + ignoreNewlineAtEof?: boolean, + ignoreWhitespace?: boolean, // TODO: This is SORT OF supported by diffWords. What to do? +} + +export interface DiffWordsOptions extends CommonDiffOptions { + ignoreCase?: boolean + intlSegmenter?: Intl.Segmenter, +} + +export interface DiffSentencesOptions extends CommonDiffOptions {} + +export interface DiffJsonOptions extends CommonDiffOptions { + undefinedReplacement?: any, + stringifyReplacer?: (k: string, v: any) => any, +} + +export interface DiffCssOptions extends CommonDiffOptions {} + + +export interface CallbackOption { + callback: DiffCallback, +} + +/** + * Note that this contains the union of ALL options accepted by any of the built-in diffing + * functions. The README notes which options are usable which functions. Using an option with a + * diffing function that doesn't support it might yield unreasonable results. + */ +export type DiffOptionsWithoutCallback = + DiffArraysOptions & + DiffCharsOptions & + DiffWordsOptions & + DiffLinesOptions & + DiffJsonOptions; + +/** + * This is a distinct type from DiffOptionsWithoutCallback so that we can have different overloads + * with different return types depending upon whether a callback option is given (and thus whether + * we are running in async or sync mode). + */ +export type DiffOptionsWithCallback = DiffOptionsWithoutCallback & CallbackOption; + +export type DiffCallback = (result?: ChangeObject[]) => void; + +export interface StructuredPatch { + oldFileName: string, + newFileName: string, + oldHeader: string, + newHeader: string, + hunks: StructuredPatchHunk[], + index?: string, +} + +export interface StructuredPatchHunk { + oldStart: number, + oldLines: number, + newStart: number, + newLines: number, + lines: string[], +} diff --git a/src/util/array.js b/src/util/array.ts similarity index 67% rename from src/util/array.js rename to src/util/array.ts index 5de3cab9d..602714aab 100644 --- a/src/util/array.js +++ b/src/util/array.ts @@ -1,4 +1,4 @@ -export function arrayEqual(a, b) { +export function arrayEqual(a: any[], b: any[]): boolean { if (a.length !== b.length) { return false; } @@ -6,7 +6,7 @@ export function arrayEqual(a, b) { return arrayStartsWith(a, b); } -export function arrayStartsWith(array, start) { +export function arrayStartsWith(array: any[], start: any[]): boolean { if (start.length > array.length) { return false; } diff --git a/src/util/distance-iterator.js b/src/util/distance-iterator.ts similarity index 87% rename from src/util/distance-iterator.js rename to src/util/distance-iterator.ts index 1ffe6413d..e2fe316cc 100644 --- a/src/util/distance-iterator.js +++ b/src/util/distance-iterator.ts @@ -1,13 +1,13 @@ // Iterator that traverses in the range of [min, max], stepping // by distance from a given start position. I.e. for [0, 4], with // start of 2, this will iterate 2, 3, 1, 4, 0. -export default function(start, minLine, maxLine) { +export default function(start: number, minLine: number, maxLine: number): () => number | undefined { let wantForward = true, backwardExhausted = false, forwardExhausted = false, localOffset = 1; - return function iterator() { + return function iterator(): number | undefined { if (wantForward && !forwardExhausted) { if (backwardExhausted) { localOffset++; @@ -41,5 +41,6 @@ export default function(start, minLine, maxLine) { // We tried to fit hunk before text beginning and beyond text length, then // hunk can't fit on the text. Return undefined + return undefined; }; } diff --git a/src/util/params.js b/src/util/params.js deleted file mode 100644 index 07e03e59e..000000000 --- a/src/util/params.js +++ /dev/null @@ -1,13 +0,0 @@ -export function generateOptions(options, defaults) { - if (typeof options === 'function') { - defaults.callback = options; - } else if (options) { - for (let name in options) { - /* istanbul ignore else */ - if (options.hasOwnProperty(name)) { - defaults[name] = options[name]; - } - } - } - return defaults; -} diff --git a/src/util/params.ts b/src/util/params.ts new file mode 100644 index 000000000..89bf6e24b --- /dev/null +++ b/src/util/params.ts @@ -0,0 +1,19 @@ +import {DiffCallback, DiffOptionsWithCallback, DiffOptionsWithoutCallback} from '../types'; + + +export function generateOptions( + options: DiffOptionsWithoutCallback | DiffCallback, + defaults: DiffOptionsWithoutCallback +): DiffOptionsWithoutCallback | DiffOptionsWithCallback { + if (typeof options === 'function') { + (defaults as DiffOptionsWithCallback).callback = options; + } else if (options) { + for (const name in options) { + /* istanbul ignore else */ + if (Object.prototype.hasOwnProperty.call(options, name)) { + defaults[name] = options[name]; + } + } + } + return defaults; +} diff --git a/src/util/string.js b/src/util/string.ts similarity index 79% rename from src/util/string.js rename to src/util/string.ts index 80cf54786..9be0603ec 100644 --- a/src/util/string.js +++ b/src/util/string.ts @@ -1,4 +1,4 @@ -export function longestCommonPrefix(str1, str2) { +export function longestCommonPrefix(str1: string, str2: string): string { let i; for (i = 0; i < str1.length && i < str2.length; i++) { if (str1[i] != str2[i]) { @@ -8,7 +8,7 @@ export function longestCommonPrefix(str1, str2) { return str1.slice(0, i); } -export function longestCommonSuffix(str1, str2) { +export function longestCommonSuffix(str1: string, str2: string): string { let i; // Unlike longestCommonPrefix, we need a special case to handle all scenarios @@ -26,14 +26,14 @@ export function longestCommonSuffix(str1, str2) { return str1.slice(-i); } -export function replacePrefix(string, oldPrefix, newPrefix) { +export function replacePrefix(string: string, oldPrefix: string, newPrefix: string): string { if (string.slice(0, oldPrefix.length) != oldPrefix) { throw Error(`string ${JSON.stringify(string)} doesn't start with prefix ${JSON.stringify(oldPrefix)}; this is a bug`); } return newPrefix + string.slice(oldPrefix.length); } -export function replaceSuffix(string, oldSuffix, newSuffix) { +export function replaceSuffix(string: string, oldSuffix: string, newSuffix: string): string { if (!oldSuffix) { return string + newSuffix; } @@ -44,20 +44,20 @@ export function replaceSuffix(string, oldSuffix, newSuffix) { return string.slice(0, -oldSuffix.length) + newSuffix; } -export function removePrefix(string, oldPrefix) { +export function removePrefix(string: string, oldPrefix: string): string { return replacePrefix(string, oldPrefix, ''); } -export function removeSuffix(string, oldSuffix) { +export function removeSuffix(string: string, oldSuffix: string): string { return replaceSuffix(string, oldSuffix, ''); } -export function maximumOverlap(string1, string2) { +export function maximumOverlap(string1: string, string2: string): string { return string2.slice(0, overlapCount(string1, string2)); } // Nicked from https://stackoverflow.com/a/60422853/1709587 -function overlapCount(a, b) { +function overlapCount(a: string, b: string): number { // Deal with cases where the strings differ in length let startA = 0; if (a.length > b.length) { startA = a.length - b.length; } @@ -66,7 +66,7 @@ function overlapCount(a, b) { // Create a back-reference for each index // that should be followed in case of a mismatch. // We only need B to make these references: - let map = Array(endB); + const map = Array(endB); let k = 0; // Index that lags behind j map[0] = 0; for (let j = 1; j < endB; j++) { @@ -91,18 +91,18 @@ function overlapCount(a, b) { /** * Returns true if the string consistently uses Windows line endings. */ -export function hasOnlyWinLineEndings(string) { +export function hasOnlyWinLineEndings(string: string): boolean { return string.includes('\r\n') && !string.startsWith('\n') && !string.match(/[^\r]\n/); } /** * Returns true if the string consistently uses Unix line endings. */ -export function hasOnlyUnixLineEndings(string) { +export function hasOnlyUnixLineEndings(string: string): boolean { return !string.includes('\r\n') && string.includes('\n'); } -export function trailingWs(string) { +export function trailingWs(string: string): string { // Yes, this looks overcomplicated and dumb - why not replace the whole function with // return string match(/\s*$/)[0] // you ask? Because: @@ -123,7 +123,8 @@ export function trailingWs(string) { return string.substring(i + 1); } -export function leadingWs(string) { +export function leadingWs(string: string): string { // Thankfully the annoying considerations described in trailingWs don't apply here: - return string.match(/^\s*/)[0]; + const match = string.match(/^\s*/); + return match ? match[0] : ''; } diff --git a/test/diff/json.js b/test/diff/json.js index e7aae0bec..3587d84fe 100644 --- a/test/diff/json.js +++ b/test/diff/json.js @@ -117,25 +117,25 @@ describe('diff/json', function() { describe('#canonicalize', function() { it('should put the keys in canonical order', function() { - expect(getKeys(canonicalize({b: 456, a: 123}))).to.eql(['a', 'b']); + expect(Object.keys(canonicalize({b: 456, a: 123}))).to.eql(['a', 'b']); }); it('should dive into nested objects', function() { const canonicalObj = canonicalize({b: 456, a: {d: 123, c: 456}}); - expect(getKeys(canonicalObj.a)).to.eql(['c', 'd']); + expect(Object.keys(canonicalObj.a)).to.eql(['c', 'd']); }); it('should dive into nested arrays', function() { const canonicalObj = canonicalize({b: 456, a: [789, {d: 123, c: 456}]}); - expect(getKeys(canonicalObj.a[1])).to.eql(['c', 'd']); + expect(Object.keys(canonicalObj.a[1])).to.eql(['c', 'd']); }); it('should handle circular references correctly', function() { const obj = {b: 456}; obj.a = obj; const canonicalObj = canonicalize(obj); - expect(getKeys(canonicalObj)).to.eql(['a', 'b']); - expect(getKeys(canonicalObj.a)).to.eql(['a', 'b']); + expect(Object.keys(canonicalObj)).to.eql(['a', 'b']); + expect(Object.keys(canonicalObj.a)).to.eql(['a', 'b']); }); it('should accept a custom JSON.stringify() replacer function', function() { @@ -144,8 +144,8 @@ describe('diff/json', function() { {a: /foo/} )).to.eql([ { count: 1, value: '{\n', removed: false, added: false }, - { count: 1, value: ' \"a\": 123\n', added: false, removed: true }, - { count: 1, value: ' \"a\": {}\n', added: true, removed: false }, + { count: 1, value: ' "a": 123\n', added: false, removed: true }, + { count: 1, value: ' "a": {}\n', added: true, removed: false }, { count: 1, value: '}', removed: false, added: false } ]); @@ -155,8 +155,8 @@ describe('diff/json', function() { {stringifyReplacer: (k, v) => v instanceof RegExp ? v.toString() : v} )).to.eql([ { count: 1, value: '{\n', removed: false, added: false }, - { count: 1, value: ' \"a\": 123\n', added: false, removed: true }, - { count: 1, value: ' \"a\": "/foo/gi"\n', added: true, removed: false }, + { count: 1, value: ' "a": 123\n', added: false, removed: true }, + { count: 1, value: ' "a": "/foo/gi"\n', added: true, removed: false }, { count: 1, value: '}', removed: false, added: false } ]); @@ -166,8 +166,8 @@ describe('diff/json', function() { {stringifyReplacer: (k, v) => v instanceof Error ? `${v.name}: ${v.message}` : v} )).to.eql([ { count: 1, value: '{\n', removed: false, added: false }, - { count: 1, value: ' \"a\": 123\n', added: false, removed: true }, - { count: 1, value: ' \"a\": "Error: ohaider"\n', added: true, removed: false }, + { count: 1, value: ' "a": 123\n', added: false, removed: true }, + { count: 1, value: ' "a": "Error: ohaider"\n', added: true, removed: false }, { count: 1, value: '}', removed: false, added: false } ]); @@ -177,8 +177,8 @@ describe('diff/json', function() { {stringifyReplacer: (k, v) => v instanceof Error ? `${v.name}: ${v.message}` : v} )).to.eql([ { count: 1, value: '{\n', removed: false, added: false }, - { count: 1, value: ' \"a\": 123\n', added: false, removed: true }, - { count: 3, value: ' \"a\": [\n "Error: ohaider"\n ]\n', added: true, removed: false }, + { count: 1, value: ' "a": 123\n', added: false, removed: true }, + { count: 3, value: ' "a": [\n "Error: ohaider"\n ]\n', added: true, removed: false }, { count: 1, value: '}', removed: false, added: false } ]); }); @@ -193,20 +193,10 @@ describe('diff/json', function() { }).not.to['throw'](); expect(diff).to.eql([ { count: 1, value: '{\n', removed: false, added: false }, - { count: 1, value: ' \"a\": 123\n', removed: true, added: false }, - { count: 1, value: ' \"b\": 456\n', removed: false, added: true }, + { count: 1, value: ' "a": 123\n', removed: true, added: false }, + { count: 1, value: ' "b": 456\n', removed: false, added: true }, { count: 1, value: '}', removed: false, added: false } ]); }); }); }); - -function getKeys(obj) { - const keys = []; - for (let key in obj) { - if (obj.hasOwnProperty(key)) { - keys.push(key); - } - } - return keys; -} diff --git a/test/patch/create.js b/test/patch/create.js index 1d1772ab8..0a43b18e3 100644 --- a/test/patch/create.js +++ b/test/patch/create.js @@ -686,7 +686,7 @@ describe('patch/create', function() { + '+line\n' + '\\ No newline at end of file\n'; - const diffResult = createPatch('testFileName', 'line \n\ line', 'line\n\line', undefined, undefined, {ignoreWhitespace: false}); + const diffResult = createPatch('testFileName', 'line \n line', 'line\nline', undefined, undefined, {ignoreWhitespace: false}); expect(diffResult).to.equal(expectedResult); }); @@ -697,7 +697,7 @@ describe('patch/create', function() { + '--- testFileName\n' + '+++ testFileName\n'; - const diffResult = createPatch('testFileName', 'line \n\ line', 'line\n\line', undefined, undefined, {ignoreWhitespace: true}); + const diffResult = createPatch('testFileName', 'line \n line', 'line\nline', undefined, undefined, {ignoreWhitespace: true}); expect(diffResult).to.equal(expectedResult); }); }); @@ -778,9 +778,9 @@ describe('patch/create', function() { + '--- foo\n' + '+++ bar\n' + '@@ -1,2 +1,2 @@\n' - + '\-line\n' - + '\+line\r\n' - + '\ line\n' + + '-line\n' + + '+line\r\n' + + ' line\n' + '\\ No newline at end of file\n'; expect(createTwoFilesPatch( 'foo', diff --git a/yarn.lock b/yarn.lock index 201e96e18..c7e776124 100644 --- a/yarn.lock +++ b/yarn.lock @@ -829,14 +829,14 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.5.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz#b0fc7e06d0c94f801537fd4237edc2706d3b8e4c" integrity sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w== dependencies: eslint-visitor-keys "^3.4.3" -"@eslint-community/regexpp@^4.12.1": +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": version "4.12.1" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== @@ -877,7 +877,7 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.22.0": +"@eslint/js@9.22.0", "@eslint/js@^9.22.0": version "9.22.0" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.22.0.tgz#4ff53649ded7cbce90b444b494c234137fa1aa3d" integrity sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ== @@ -1028,6 +1028,27 @@ dependencies: eslint-scope "5.1.1" +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -1320,6 +1341,87 @@ dependencies: "@types/node" "*" +"@typescript-eslint/eslint-plugin@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz#3e48eb847924161843b092c87a9b65176b53782f" + integrity sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.26.1" + "@typescript-eslint/type-utils" "8.26.1" + "@typescript-eslint/utils" "8.26.1" + "@typescript-eslint/visitor-keys" "8.26.1" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^2.0.1" + +"@typescript-eslint/parser@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.1.tgz#0e2f915a497519fc43f52cf2ecbfa607ff56f72e" + integrity sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ== + dependencies: + "@typescript-eslint/scope-manager" "8.26.1" + "@typescript-eslint/types" "8.26.1" + "@typescript-eslint/typescript-estree" "8.26.1" + "@typescript-eslint/visitor-keys" "8.26.1" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz#5e6ad0ac258ccf79462e91c3f43a3f1f7f31a6cc" + integrity sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg== + dependencies: + "@typescript-eslint/types" "8.26.1" + "@typescript-eslint/visitor-keys" "8.26.1" + +"@typescript-eslint/type-utils@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz#462f0bae09de72ac6e8e1af2ebe588c23224d7f8" + integrity sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg== + dependencies: + "@typescript-eslint/typescript-estree" "8.26.1" + "@typescript-eslint/utils" "8.26.1" + debug "^4.3.4" + ts-api-utils "^2.0.1" + +"@typescript-eslint/types@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.26.1.tgz#d5978721670cff263348d5062773389231a64132" + integrity sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ== + +"@typescript-eslint/typescript-estree@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz#eb0e4ce31753683d83be53441a409fd5f0b34afd" + integrity sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA== + dependencies: + "@typescript-eslint/types" "8.26.1" + "@typescript-eslint/visitor-keys" "8.26.1" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.0.1" + +"@typescript-eslint/utils@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.26.1.tgz#54cc58469955f25577f659753b71a0e117a0539f" + integrity sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.26.1" + "@typescript-eslint/types" "8.26.1" + "@typescript-eslint/typescript-estree" "8.26.1" + +"@typescript-eslint/visitor-keys@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz#c5267fcc82795cf10280363023837deacad2647c" + integrity sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg== + dependencies: + "@typescript-eslint/types" "8.26.1" + eslint-visitor-keys "^4.2.0" + "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" @@ -2482,6 +2584,17 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -2497,6 +2610,13 @@ fast-uri@^3.0.1: resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + faye-websocket@^0.11.3: version "0.11.4" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" @@ -2728,6 +2848,13 @@ get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" @@ -2735,13 +2862,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" @@ -2796,6 +2916,11 @@ graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -2921,7 +3046,7 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -ignore@^5.2.0: +ignore@^5.2.0, ignore@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== @@ -3486,12 +3611,17 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.2: +micromatch@^4.0.2, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -3984,6 +4114,11 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -4134,6 +4269,11 @@ retry@^0.13.1: resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + rfdc@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" @@ -4194,6 +4334,13 @@ run-applescript@^7.0.0: resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -4251,7 +4398,7 @@ semver@^6.0.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.3, semver@^7.5.4: +semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: version "7.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== @@ -4681,6 +4828,11 @@ tree-dump@^1.0.1: resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac" integrity sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ== +ts-api-utils@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.1.tgz#660729385b625b939aaa58054f45c058f33f10cd" + integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w== + tslib@^2.0.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" @@ -4718,6 +4870,20 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript-eslint@^8.26.1: + version "8.26.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.26.1.tgz#d17a638a7543bc535157b83cdf5876513c71493b" + integrity sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg== + dependencies: + "@typescript-eslint/eslint-plugin" "8.26.1" + "@typescript-eslint/parser" "8.26.1" + "@typescript-eslint/utils" "8.26.1" + +typescript@^5.7.3: + version "5.8.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4" + integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ== + ua-parser-js@^0.7.30: version "0.7.40" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.40.tgz#c87d83b7bb25822ecfa6397a0da5903934ea1562"