├── .babelrc ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── examples ├── node_example.js └── web_example.html ├── images ├── node_example.png └── web_example.png ├── karma.conf.js ├── package.json ├── release-notes.md ├── rollup.config.mjs ├── runtime.js ├── src ├── convert │ ├── dmp.ts │ └── xml.ts ├── diff │ ├── array.ts │ ├── base.ts │ ├── character.ts │ ├── css.ts │ ├── json.ts │ ├── line.ts │ ├── sentence.ts │ └── word.ts ├── index.ts ├── patch │ ├── apply.ts │ ├── create.ts │ ├── line-endings.ts │ ├── parse.ts │ └── reverse.ts ├── types.ts └── util │ ├── array.ts │ ├── distance-iterator.ts │ ├── params.ts │ └── string.ts ├── test-d ├── diffCharsOverloads.test-d.ts └── originalDefinitelyTypedTests.test-d.ts ├── test ├── convert │ └── dmp.js ├── diff │ ├── array.js │ ├── character.js │ ├── css.js │ ├── json.js │ ├── line.js │ ├── sentence.js │ └── word.js ├── index.js ├── patch │ ├── apply.js │ ├── create.js │ ├── line-endings.js │ ├── parse.js │ └── reverse.js └── util │ └── string.js ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": "inline", 3 | "presets": ["@babel/preset-env"], 4 | "env": { 5 | "test": { 6 | "plugins": [ 7 | "istanbul" 8 | ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | libesm 4 | libcjs 5 | dist 6 | yarn-error.log 7 | .vscode 8 | .nyc_output 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslint.config.mjs 3 | .gitignore 4 | .npmignore 5 | .vscode 6 | .nyc_output 7 | test-d 8 | components 9 | coverage 10 | examples 11 | images 12 | index.html 13 | karma.conf.js 14 | rollup.config.mjs 15 | runtime.js 16 | src 17 | tasks 18 | test 19 | tsconfig.json 20 | yarn-error.log 21 | yarn.lock 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Building and testing 2 | 3 | ``` 4 | yarn 5 | yarn test 6 | ``` 7 | 8 | To run tests in a *browser* (for instance to test compatibility with Firefox, with Safari, or with old browser versions), run `yarn karma start`, then open http://localhost:9876/ in the browser you want to test in. Results of the test run will appear in the terminal where `yarn karma start` is running. 9 | 10 | If you notice any problems, please report them to the GitHub issue tracker at 11 | [http://github.com/kpdecker/jsdiff/issues](http://github.com/kpdecker/jsdiff/issues). 12 | 13 | ## Releasing 14 | 15 | Run a test in Firefox via the procedure above before releasing. 16 | 17 | A full release may be completed by first updating the `"version"` property in package.json, then running the following: 18 | 19 | ``` 20 | yarn clean 21 | yarn build 22 | yarn publish 23 | ``` 24 | 25 | After releasing, remember to: 26 | * commit the `package.json` change and push it to GitHub 27 | * create a new version tag on GitHub 28 | * update `diff.js` on the `gh-pages` branch to the latest built version from the `dist/` folder. 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2009-2015, Kevin Decker 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | import globals from "globals"; 6 | 7 | export default tseslint.config( 8 | { 9 | ignores: [ 10 | "**/*", // ignore everything... 11 | "!src/**/", "!src/**/*.ts", // ... except our TypeScript source files... 12 | "!test/**/", "!test/**/*.js", // ... and our tests 13 | ], 14 | }, 15 | eslint.configs.recommended, 16 | tseslint.configs.recommended, 17 | { 18 | files: ['src/**/*.ts'], 19 | languageOptions: { 20 | parserOptions: { 21 | projectService: true, 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | extends: [tseslint.configs.recommendedTypeChecked], 26 | rules: { 27 | // Not sure if these actually serve a purpose, but they provide a way to enforce SOME of what 28 | // would be imposed by having "verbatimModuleSyntax": true in our tsconfig.json without 29 | // actually doing that. 30 | "@typescript-eslint/consistent-type-imports": 2, 31 | "@typescript-eslint/consistent-type-exports": 2, 32 | 33 | // Things from the recommendedTypeChecked shared config that are disabled simply because they 34 | // caused lots of errors in our existing code when tried. Plausibly useful to turn on if 35 | // possible and somebody fancies doing the work: 36 | "@typescript-eslint/no-unsafe-argument": 0, 37 | "@typescript-eslint/no-unsafe-assignment": 0, 38 | "@typescript-eslint/no-unsafe-call": 0, 39 | "@typescript-eslint/no-unsafe-member-access": 0, 40 | "@typescript-eslint/no-unsafe-return": 0, 41 | } 42 | }, 43 | { 44 | languageOptions: { 45 | globals: { 46 | ...globals.browser, 47 | }, 48 | }, 49 | 50 | rules: { 51 | // Possible Errors // 52 | //-----------------// 53 | "comma-dangle": [2, "never"], 54 | "no-console": 1, // Allow for debugging 55 | "no-debugger": 1, // Allow for debugging 56 | "no-extra-parens": [2, "functions"], 57 | "no-extra-semi": 2, 58 | "no-negated-in-lhs": 2, 59 | "no-unreachable": 1, // Optimizer and coverage will handle/highlight this and can be useful for debugging 60 | 61 | // Best Practices // 62 | //----------------// 63 | curly: 2, 64 | "default-case": 1, 65 | "dot-notation": [2, { 66 | allowKeywords: false, 67 | }], 68 | "guard-for-in": 1, 69 | "no-alert": 2, 70 | "no-caller": 2, 71 | "no-div-regex": 1, 72 | "no-eval": 2, 73 | "no-extend-native": 2, 74 | "no-extra-bind": 2, 75 | "no-floating-decimal": 2, 76 | "no-implied-eval": 2, 77 | "no-iterator": 2, 78 | "no-labels": 2, 79 | "no-lone-blocks": 2, 80 | "no-multi-spaces": 2, 81 | "no-multi-str": 1, 82 | "no-native-reassign": 2, 83 | "no-new": 2, 84 | "no-new-func": 2, 85 | "no-new-wrappers": 2, 86 | "no-octal-escape": 2, 87 | "no-process-env": 2, 88 | "no-proto": 2, 89 | "no-return-assign": 2, 90 | "no-script-url": 2, 91 | "no-self-compare": 2, 92 | "no-sequences": 2, 93 | "no-throw-literal": 2, 94 | "no-unused-expressions": 2, 95 | "no-warning-comments": 1, 96 | radix: 2, 97 | "wrap-iife": 2, 98 | 99 | // Variables // 100 | //-----------// 101 | "no-catch-shadow": 2, 102 | "no-label-var": 2, 103 | "no-undef-init": 2, 104 | 105 | // Node.js // 106 | //---------// 107 | 108 | // Stylistic // 109 | //-----------// 110 | "brace-style": [2, "1tbs", { 111 | allowSingleLine: true, 112 | }], 113 | camelcase: 2, 114 | "comma-spacing": [2, { 115 | before: false, 116 | after: true, 117 | }], 118 | "comma-style": [2, "last"], 119 | "consistent-this": [1, "self"], 120 | "eol-last": 2, 121 | "func-style": [2, "declaration"], 122 | "key-spacing": [2, { 123 | beforeColon: false, 124 | afterColon: true, 125 | }], 126 | "new-cap": 2, 127 | "new-parens": 2, 128 | "no-array-constructor": 2, 129 | "no-lonely-if": 2, 130 | "no-mixed-spaces-and-tabs": 2, 131 | "no-nested-ternary": 1, 132 | "no-new-object": 2, 133 | "no-spaced-func": 2, 134 | "no-trailing-spaces": 2, 135 | "quote-props": [2, "as-needed", { 136 | keywords: true, 137 | }], 138 | quotes: [2, "single", "avoid-escape"], 139 | semi: 2, 140 | "semi-spacing": [2, { 141 | before: false, 142 | after: true, 143 | }], 144 | "space-before-blocks": [2, "always"], 145 | "space-before-function-paren": [2, { 146 | anonymous: "never", 147 | named: "never", 148 | }], 149 | "space-in-parens": [2, "never"], 150 | "space-infix-ops": 2, 151 | "space-unary-ops": 2, 152 | "spaced-comment": [2, "always"], 153 | "wrap-regex": 1, 154 | "no-var": 2, 155 | 156 | // Typescript // 157 | //------------// 158 | "@typescript-eslint/no-explicit-any": 0, // Very strict rule, incompatible with our code 159 | 160 | // We use these intentionally - e.g. 161 | // export interface DiffCssOptions extends CommonDiffOptions {} 162 | // for the options argument to diffCss which currently takes no options beyond the ones 163 | // common to all diffFoo functions. Doing this allows consistency (one options interface per 164 | // diffFoo function) and future-proofs against the API having to change in future if we add a 165 | // non-common option to one of these functions. 166 | "@typescript-eslint/no-empty-object-type": [2, {allowInterfaces: 'with-single-extends'}], 167 | }, 168 | }, 169 | { 170 | files: ['test/**/*.js'], 171 | languageOptions: { 172 | globals: { 173 | ...globals.node, 174 | ...globals.mocha, 175 | }, 176 | }, 177 | rules: { 178 | "no-unused-expressions": 0, // Needs disabling to support Chai `.to.be.undefined` etc syntax 179 | "@typescript-eslint/no-unused-expressions": 0, // (as above) 180 | }, 181 | } 182 | ); 183 | -------------------------------------------------------------------------------- /examples/node_example.js: -------------------------------------------------------------------------------- 1 | require('colors'); 2 | const {diffChars} = require('diff'); 3 | 4 | const one = 'beep boop'; 5 | const other = 'beep boob blah'; 6 | 7 | const diff = diffChars(one, other); 8 | 9 | diff.forEach((part) => { 10 | // green for additions, red for deletions 11 | let text = part.added ? part.value.bgGreen : 12 | part.removed ? part.value.bgRed : 13 | part.value; 14 | process.stderr.write(text); 15 | }); 16 | 17 | console.log(); 18 | -------------------------------------------------------------------------------- /examples/web_example.html: -------------------------------------------------------------------------------- 1 | 4 |

 5 | 
 6 | 
30 | 
33 | 


--------------------------------------------------------------------------------
/images/node_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kpdecker/jsdiff/329f85b09018f5cf55000a58786163561e037bd2/images/node_example.png


--------------------------------------------------------------------------------
/images/web_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kpdecker/jsdiff/329f85b09018f5cf55000a58786163561e037bd2/images/web_example.png


--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
 1 | export default function(config) {
 2 |   config.set({
 3 |     basePath: '',
 4 | 
 5 |     frameworks: ['mocha'],
 6 | 
 7 |     files: [
 8 |       'test/**/*.js'
 9 |     ],
10 |     preprocessors: {
11 |       'test/**/*.js': ['webpack', 'sourcemap']
12 |     },
13 | 
14 |     webpack: {
15 |       devtool: 'eval',
16 |       module: {
17 |         rules: [
18 |           {
19 |             test: /\.jsx?$/,
20 |             exclude: /node_modules/,
21 |             loader: 'babel-loader'
22 |           }
23 |         ]
24 |       }
25 |     },
26 | 
27 |     reporters: ['mocha'],
28 | 
29 |     port: 9876,
30 |     colors: true,
31 |     logLevel: config.LOG_INFO,
32 |     autoWatch: true,
33 |     singleRun: false,
34 |   });
35 | };
36 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "name": "diff",
  3 |   "version": "8.0.2",
  4 |   "description": "A JavaScript text diff implementation.",
  5 |   "keywords": [
  6 |     "diff",
  7 |     "jsdiff",
  8 |     "compare",
  9 |     "patch",
 10 |     "text",
 11 |     "json",
 12 |     "css",
 13 |     "javascript"
 14 |   ],
 15 |   "maintainers": [
 16 |     "Kevin Decker  (http://incaseofstairs.com)",
 17 |     "Mark Amery "
 18 |   ],
 19 |   "bugs": {
 20 |     "email": "kpdecker@gmail.com",
 21 |     "url": "http://github.com/kpdecker/jsdiff/issues"
 22 |   },
 23 |   "license": "BSD-3-Clause",
 24 |   "repository": {
 25 |     "type": "git",
 26 |     "url": "https://github.com/kpdecker/jsdiff.git"
 27 |   },
 28 |   "engines": {
 29 |     "node": ">=0.3.1"
 30 |   },
 31 |   "main": "./libcjs/index.js",
 32 |   "module": "./libesm/index.js",
 33 |   "browser": "./dist/diff.js",
 34 |   "unpkg": "./dist/diff.js",
 35 |   "exports": {
 36 |     ".": {
 37 |       "import": {
 38 |         "types": "./libesm/index.d.ts",
 39 |         "default": "./libesm/index.js"
 40 |       },
 41 |       "require": {
 42 |         "types": "./libcjs/index.d.ts",
 43 |         "default": "./libcjs/index.js"
 44 |       }
 45 |     },
 46 |     "./package.json": "./package.json",
 47 |     "./lib/*.js": {
 48 |       "import": {
 49 |         "types": "./libesm/*.d.ts",
 50 |         "default": "./libesm/*.js"
 51 |       },
 52 |       "require": {
 53 |         "types": "./libcjs/*.d.ts",
 54 |         "default": "./libcjs/*.js"
 55 |       }
 56 |     },
 57 |     "./lib/": {
 58 |       "import": {
 59 |         "types": "./libesm/",
 60 |         "default": "./libesm/"
 61 |       },
 62 |       "require": {
 63 |         "types": "./libcjs/",
 64 |         "default": "./libcjs/"
 65 |       }
 66 |     }
 67 |   },
 68 |   "type": "module",
 69 |   "types": "libcjs/index.d.ts",
 70 |   "scripts": {
 71 |     "clean": "rm -rf libcjs/ libesm/ dist/ coverage/ .nyc_output/",
 72 |     "lint": "yarn eslint",
 73 |     "build": "yarn lint && yarn generate-esm && yarn generate-cjs && yarn check-types && yarn run-rollup && yarn run-uglify",
 74 |     "generate-cjs": "yarn tsc --module commonjs --outDir libcjs && node --eval \"fs.writeFileSync('libcjs/package.json', JSON.stringify({type:'commonjs',sideEffects:false}))\"",
 75 |     "generate-esm": "yarn tsc --module nodenext --outDir libesm --target es6 && node --eval \"fs.writeFileSync('libesm/package.json', JSON.stringify({type:'module',sideEffects:false}))\"",
 76 |     "check-types": "yarn run-tsd && yarn run-attw",
 77 |     "test": "nyc yarn _test",
 78 |     "_test": "yarn build && cross-env NODE_ENV=test yarn run-mocha",
 79 |     "run-attw": "yarn attw --pack --entrypoints . && yarn attw --pack --entrypoints lib/diff/word.js --profile node16",
 80 |     "run-tsd": "yarn tsd --typings libesm/ && yarn tsd --files test-d/",
 81 |     "run-rollup": "rollup -c rollup.config.mjs",
 82 |     "run-uglify": "uglifyjs dist/diff.js -c -o dist/diff.min.js",
 83 |     "run-mocha": "mocha --require ./runtime 'test/**/*.js'"
 84 |   },
 85 |   "devDependencies": {
 86 |     "@arethetypeswrong/cli": "^0.17.4",
 87 |     "@babel/core": "^7.26.9",
 88 |     "@babel/preset-env": "^7.26.9",
 89 |     "@babel/register": "^7.25.9",
 90 |     "@colors/colors": "^1.6.0",
 91 |     "@eslint/js": "^9.25.1",
 92 |     "babel-loader": "^10.0.0",
 93 |     "babel-plugin-istanbul": "^7.0.0",
 94 |     "chai": "^5.2.0",
 95 |     "cross-env": "^7.0.3",
 96 |     "eslint": "^9.25.1",
 97 |     "globals": "^16.0.0",
 98 |     "karma": "^6.4.4",
 99 |     "karma-mocha": "^2.0.1",
100 |     "karma-mocha-reporter": "^2.2.5",
101 |     "karma-sourcemap-loader": "^0.4.0",
102 |     "karma-webpack": "^5.0.1",
103 |     "mocha": "^11.1.0",
104 |     "nyc": "^17.1.0",
105 |     "rollup": "^4.40.1",
106 |     "tsd": "^0.32.0",
107 |     "typescript": "^5.8.3",
108 |     "typescript-eslint": "^8.31.0",
109 |     "uglify-js": "^3.19.3",
110 |     "webpack": "^5.99.7",
111 |     "webpack-dev-server": "^5.2.1"
112 |   },
113 |   "optionalDependencies": {},
114 |   "dependencies": {},
115 |   "nyc": {
116 |     "require": [
117 |       "@babel/register"
118 |     ],
119 |     "reporter": [
120 |       "lcov",
121 |       "text"
122 |     ],
123 |     "sourceMap": false,
124 |     "instrument": false,
125 |     "check-coverage": true,
126 |     "branches": 100,
127 |     "lines": 100,
128 |     "functions": 100,
129 |     "statements": 100
130 |   }
131 | }
132 | 


--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
 1 | import pkg from './package.json' with { type: 'json' };
 2 | 
 3 | export default [
 4 |   // browser-friendly UMD build
 5 |   {
 6 |     input: 'libesm/index.js',
 7 |     output: [
 8 |       {
 9 |         name: 'Diff',
10 |         format: 'umd',
11 |         file: "./dist/diff.js"
12 |       }
13 |     ]
14 |   }
15 | ];
16 | 


--------------------------------------------------------------------------------
/runtime.js:
--------------------------------------------------------------------------------
1 | require('@babel/register')({
2 |   ignore: ['libcjs', 'libesm', 'node_modules']
3 | });
4 | 


--------------------------------------------------------------------------------
/src/convert/dmp.ts:
--------------------------------------------------------------------------------
 1 | import type {ChangeObject} from '../types.js';
 2 | 
 3 | type DmpOperation = 1 | 0 | -1;
 4 | 
 5 | /**
 6 |  * converts a list of change objects to the format returned by Google's [diff-match-patch](https://github.com/google/diff-match-patch) library
 7 |  */
 8 | export function convertChangesToDMP(changes: ChangeObject[]): [DmpOperation, ValueT][] {
 9 |   const ret: [DmpOperation, ValueT][] = [];
10 |   let change,
11 |       operation: DmpOperation;
12 |   for (let i = 0; i < changes.length; i++) {
13 |     change = changes[i];
14 |     if (change.added) {
15 |       operation = 1;
16 |     } else if (change.removed) {
17 |       operation = -1;
18 |     } else {
19 |       operation = 0;
20 |     }
21 | 
22 |     ret.push([operation, change.value]);
23 |   }
24 |   return ret;
25 | }
26 | 


--------------------------------------------------------------------------------
/src/convert/xml.ts:
--------------------------------------------------------------------------------
 1 | import type {ChangeObject} from '../types.js';
 2 | 
 3 | /**
 4 |  * converts a list of change objects to a serialized XML format
 5 |  */
 6 | export function convertChangesToXML(changes: ChangeObject[]): string {
 7 |   const ret = [];
 8 |   for (let i = 0; i < changes.length; i++) {
 9 |     const change = changes[i];
10 |     if (change.added) {
11 |       ret.push('');
12 |     } else if (change.removed) {
13 |       ret.push('');
14 |     }
15 | 
16 |     ret.push(escapeHTML(change.value));
17 | 
18 |     if (change.added) {
19 |       ret.push('');
20 |     } else if (change.removed) {
21 |       ret.push('');
22 |     }
23 |   }
24 |   return ret.join('');
25 | }
26 | 
27 | function escapeHTML(s: string): string {
28 |   let n = s;
29 |   n = n.replace(/&/g, '&');
30 |   n = n.replace(//g, '>');
32 |   n = n.replace(/"/g, '"');
33 | 
34 |   return n;
35 | }
36 | 


--------------------------------------------------------------------------------
/src/diff/array.ts:
--------------------------------------------------------------------------------
 1 | import Diff from './base.js';
 2 | import type {ChangeObject, DiffArraysOptionsNonabortable, CallbackOptionNonabortable, DiffArraysOptionsAbortable, DiffCallbackNonabortable, CallbackOptionAbortable} from '../types.js';
 3 | 
 4 | class ArrayDiff extends Diff> {
 5 |   tokenize(value: Array) {
 6 |     return value.slice();
 7 |   }
 8 | 
 9 |   join(value: Array) {
10 |     return value;
11 |   }
12 | 
13 |   removeEmpty(value: Array) {
14 |     return value;
15 |   }
16 | }
17 | 
18 | export const arrayDiff = new ArrayDiff();
19 | 
20 | /**
21 |  * diffs two arrays of tokens, comparing each item for strict equality (===).
22 |  * @returns a list of change objects.
23 |  */
24 | export function diffArrays(
25 |   oldArr: T[],
26 |   newArr: T[],
27 |   options: DiffCallbackNonabortable
28 | ): undefined;
29 | export function diffArrays(
30 |   oldArr: T[],
31 |   newArr: T[],
32 |   options: DiffArraysOptionsAbortable & CallbackOptionAbortable
33 | ): undefined
34 | export function diffArrays(
35 |   oldArr: T[],
36 |   newArr: T[],
37 |   options: DiffArraysOptionsNonabortable & CallbackOptionNonabortable
38 | ): undefined
39 | export function diffArrays(
40 |   oldArr: T[],
41 |   newArr: T[],
42 |   options: DiffArraysOptionsAbortable
43 | ): ChangeObject[] | undefined
44 | export function diffArrays(
45 |   oldArr: T[],
46 |   newArr: T[],
47 |   options?: DiffArraysOptionsNonabortable
48 | ): ChangeObject[]
49 | export function diffArrays(
50 |   oldArr: T[],
51 |   newArr: T[],
52 |   options?: any
53 | ): undefined | ChangeObject[] {
54 |   return arrayDiff.diff(oldArr, newArr, options);
55 | }
56 | 


--------------------------------------------------------------------------------
/src/diff/base.ts:
--------------------------------------------------------------------------------
  1 | import type {ChangeObject, AllDiffOptions, AbortableDiffOptions, DiffCallbackNonabortable, CallbackOptionAbortable, CallbackOptionNonabortable, DiffCallbackAbortable, TimeoutOption, MaxEditLengthOption} from '../types.js';
  2 | 
  3 | /**
  4 |  * Like a ChangeObject, but with no value and an extra `previousComponent` property.
  5 |  * A linked list of these (linked via `.previousComponent`) is used internally in the code below to
  6 |  * keep track of the state of the diffing algorithm, but gets converted to an array of
  7 |  * ChangeObjects before being returned to the caller.
  8 |  */
  9 | interface DraftChangeObject {
 10 |     added: boolean;
 11 |     removed: boolean;
 12 |     count: number;
 13 |     previousComponent?: DraftChangeObject;
 14 | 
 15 |     // Only added in buildValues:
 16 |     value?: any;
 17 | }
 18 | 
 19 | interface Path {
 20 |   oldPos: number;
 21 |   lastComponent: DraftChangeObject | undefined
 22 | }
 23 | 
 24 | export default class Diff<
 25 |   TokenT,
 26 |   ValueT extends Iterable = Iterable,
 27 |   InputValueT = ValueT
 28 | > {
 29 |   diff(
 30 |     oldStr: InputValueT,
 31 |     newStr: InputValueT,
 32 |     options: DiffCallbackNonabortable
 33 |   ): undefined;
 34 |   diff(
 35 |     oldStr: InputValueT,
 36 |     newStr: InputValueT,
 37 |     options: AllDiffOptions & AbortableDiffOptions & CallbackOptionAbortable
 38 |   ): undefined
 39 |   diff(
 40 |     oldStr: InputValueT,
 41 |     newStr: InputValueT,
 42 |     options: AllDiffOptions & CallbackOptionNonabortable
 43 |   ): undefined
 44 |   diff(
 45 |     oldStr: InputValueT,
 46 |     newStr: InputValueT,
 47 |     options: AllDiffOptions & AbortableDiffOptions
 48 |   ): ChangeObject[] | undefined
 49 |   diff(
 50 |     oldStr: InputValueT,
 51 |     newStr: InputValueT,
 52 |     options?: AllDiffOptions
 53 |   ): ChangeObject[]
 54 |   diff(
 55 |     oldStr: InputValueT,
 56 |     newStr: InputValueT,
 57 |     // Type below is not accurate/complete - see above for full possibilities - but it compiles
 58 |     options: DiffCallbackNonabortable | AllDiffOptions & Partial> = {}
 59 |   ): ChangeObject[] | undefined {
 60 |     let callback: DiffCallbackAbortable | DiffCallbackNonabortable | undefined;
 61 |     if (typeof options === 'function') {
 62 |       callback = options;
 63 |       options = {};
 64 |     } else if ('callback' in options) {
 65 |       callback = options.callback;
 66 |     }
 67 |     // Allow subclasses to massage the input prior to running
 68 |     const oldString = this.castInput(oldStr, options);
 69 |     const newString = this.castInput(newStr, options);
 70 | 
 71 |     const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
 72 |     const newTokens = this.removeEmpty(this.tokenize(newString, options));
 73 | 
 74 |     return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
 75 |   }
 76 | 
 77 |   private diffWithOptionsObj(
 78 |     oldTokens: TokenT[],
 79 |     newTokens: TokenT[],
 80 |     options: AllDiffOptions & Partial & Partial,
 81 |     callback: DiffCallbackAbortable | DiffCallbackNonabortable | undefined
 82 |   ): ChangeObject[] | undefined {
 83 |     const done = (value: ChangeObject[]) => {
 84 |       value = this.postProcess(value, options);
 85 |       if (callback) {
 86 |         setTimeout(function() { callback(value); }, 0);
 87 |         return undefined;
 88 |       } else {
 89 |         return value;
 90 |       }
 91 |     };
 92 | 
 93 |     const newLen = newTokens.length, oldLen = oldTokens.length;
 94 |     let editLength = 1;
 95 |     let maxEditLength = newLen + oldLen;
 96 |     if(options.maxEditLength != null) {
 97 |       maxEditLength = Math.min(maxEditLength, options.maxEditLength);
 98 |     }
 99 |     const maxExecutionTime = options.timeout ?? Infinity;
100 |     const abortAfterTimestamp = Date.now() + maxExecutionTime;
101 | 
102 |     const bestPath: Path[] = [{ oldPos: -1, lastComponent: undefined }];
103 | 
104 |     // Seed editLength = 0, i.e. the content starts with the same values
105 |     let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
106 |     if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
107 |       // Identity per the equality and tokenizer
108 |       return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
109 |     }
110 | 
111 |     // Once we hit the right edge of the edit graph on some diagonal k, we can
112 |     // definitely reach the end of the edit graph in no more than k edits, so
113 |     // there's no point in considering any moves to diagonal k+1 any more (from
114 |     // which we're guaranteed to need at least k+1 more edits).
115 |     // Similarly, once we've reached the bottom of the edit graph, there's no
116 |     // point considering moves to lower diagonals.
117 |     // We record this fact by setting minDiagonalToConsider and
118 |     // maxDiagonalToConsider to some finite value once we've hit the edge of
119 |     // the edit graph.
120 |     // This optimization is not faithful to the original algorithm presented in
121 |     // Myers's paper, which instead pointlessly extends D-paths off the end of
122 |     // the edit graph - see page 7 of Myers's paper which notes this point
123 |     // explicitly and illustrates it with a diagram. This has major performance
124 |     // implications for some common scenarios. For instance, to compute a diff
125 |     // where the new text simply appends d characters on the end of the
126 |     // original text of length n, the true Myers algorithm will take O(n+d^2)
127 |     // time while this optimization needs only O(n+d) time.
128 |     let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
129 | 
130 |     // Main worker method. checks all permutations of a given edit length for acceptance.
131 |     const execEditLength = () => {
132 |       for (
133 |         let diagonalPath = Math.max(minDiagonalToConsider, -editLength);
134 |         diagonalPath <= Math.min(maxDiagonalToConsider, editLength);
135 |         diagonalPath += 2
136 |       ) {
137 |         let basePath;
138 |         const removePath = bestPath[diagonalPath - 1],
139 |               addPath = bestPath[diagonalPath + 1];
140 |         if (removePath) {
141 |           // No one else is going to attempt to use this value, clear it
142 |           // @ts-expect-error - perf optimisation. This type-violating value will never be read.
143 |           bestPath[diagonalPath - 1] = undefined;
144 |         }
145 | 
146 |         let canAdd = false;
147 |         if (addPath) {
148 |           // what newPos will be after we do an insertion:
149 |           const addPathNewPos = addPath.oldPos - diagonalPath;
150 |           canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
151 |         }
152 | 
153 |         const canRemove = removePath && removePath.oldPos + 1 < oldLen;
154 |         if (!canAdd && !canRemove) {
155 |           // If this path is a terminal then prune
156 |           // @ts-expect-error - perf optimisation. This type-violating value will never be read.
157 |           bestPath[diagonalPath] = undefined;
158 |           continue;
159 |         }
160 | 
161 |         // Select the diagonal that we want to branch from. We select the prior
162 |         // path whose position in the old string is the farthest from the origin
163 |         // and does not pass the bounds of the diff graph
164 |         if (!canRemove || (canAdd && removePath.oldPos < addPath.oldPos)) {
165 |           basePath = this.addToPath(addPath, true, false, 0, options);
166 |         } else {
167 |           basePath = this.addToPath(removePath, false, true, 1, options);
168 |         }
169 | 
170 |         newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
171 | 
172 |         if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
173 |           // If we have hit the end of both strings, then we are done
174 |           return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
175 |         } else {
176 |           bestPath[diagonalPath] = basePath;
177 |           if (basePath.oldPos + 1 >= oldLen) {
178 |             maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
179 |           }
180 |           if (newPos + 1 >= newLen) {
181 |             minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
182 |           }
183 |         }
184 |       }
185 | 
186 |       editLength++;
187 |     };
188 | 
189 |     // Performs the length of edit iteration. Is a bit fugly as this has to support the
190 |     // sync and async mode which is never fun. Loops over execEditLength until a value
191 |     // is produced, or until the edit length exceeds options.maxEditLength (if given),
192 |     // in which case it will return undefined.
193 |     if (callback) {
194 |       (function exec() {
195 |         setTimeout(function() {
196 |           if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
197 |             return (callback as DiffCallbackAbortable)(undefined);
198 |           }
199 | 
200 |           if (!execEditLength()) {
201 |             exec();
202 |           }
203 |         }, 0);
204 |       }());
205 |     } else {
206 |       while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
207 |         const ret = execEditLength();
208 |         if (ret) {
209 |           return ret as ChangeObject[];
210 |         }
211 |       }
212 |     }
213 |   }
214 | 
215 |   private addToPath(
216 |     path: Path,
217 |     added: boolean,
218 |     removed: boolean,
219 |     oldPosInc: number,
220 |     options: AllDiffOptions
221 |   ): Path {
222 |     const last = path.lastComponent;
223 |     if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
224 |       return {
225 |         oldPos: path.oldPos + oldPosInc,
226 |         lastComponent: {count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent }
227 |       };
228 |     } else {
229 |       return {
230 |         oldPos: path.oldPos + oldPosInc,
231 |         lastComponent: {count: 1, added: added, removed: removed, previousComponent: last }
232 |       };
233 |     }
234 |   }
235 | 
236 |   private extractCommon(
237 |     basePath: Path,
238 |     newTokens: TokenT[],
239 |     oldTokens: TokenT[],
240 |     diagonalPath: number,
241 |     options: AllDiffOptions
242 |   ): number {
243 |     const newLen = newTokens.length,
244 |           oldLen = oldTokens.length;
245 |     let oldPos = basePath.oldPos,
246 |         newPos = oldPos - diagonalPath,
247 |         commonCount = 0;
248 | 
249 |     while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
250 |       newPos++;
251 |       oldPos++;
252 |       commonCount++;
253 |       if (options.oneChangePerToken) {
254 |         basePath.lastComponent = {count: 1, previousComponent: basePath.lastComponent, added: false, removed: false};
255 |       }
256 |     }
257 | 
258 |     if (commonCount && !options.oneChangePerToken) {
259 |       basePath.lastComponent = {count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false};
260 |     }
261 | 
262 |     basePath.oldPos = oldPos;
263 |     return newPos;
264 |   }
265 | 
266 |   equals(left: TokenT, right: TokenT, options: AllDiffOptions): boolean {
267 |     if (options.comparator) {
268 |       return options.comparator(left, right);
269 |     } else {
270 |       return left === right
271 |         || (!!options.ignoreCase && (left as string).toLowerCase() === (right as string).toLowerCase());
272 |     }
273 |   }
274 | 
275 |   removeEmpty(array: TokenT[]): TokenT[] {
276 |     const ret: TokenT[] = [];
277 |     for (let i = 0; i < array.length; i++) {
278 |       if (array[i]) {
279 |         ret.push(array[i]);
280 |       }
281 |     }
282 |     return ret;
283 |   }
284 | 
285 |   // eslint-disable-next-line @typescript-eslint/no-unused-vars
286 |   castInput(value: InputValueT, options: AllDiffOptions): ValueT {
287 |     return value as unknown as ValueT;
288 |   }
289 | 
290 |   // eslint-disable-next-line @typescript-eslint/no-unused-vars
291 |   tokenize(value: ValueT, options: AllDiffOptions): TokenT[] {
292 |     return Array.from(value);
293 |   }
294 | 
295 |   join(chars: TokenT[]): ValueT {
296 |     // Assumes ValueT is string, which is the case for most subclasses.
297 |     // When it's false, e.g. in diffArrays, this method needs to be overridden (e.g. with a no-op)
298 |     // Yes, the casts are verbose and ugly, because this pattern - of having the base class SORT OF
299 |     // assume tokens and values are strings, but not completely - is weird and janky.
300 |     return (chars as string[]).join('') as unknown as ValueT;
301 |   }
302 | 
303 |   postProcess(
304 |     changeObjects: ChangeObject[],
305 |     // eslint-disable-next-line @typescript-eslint/no-unused-vars
306 |     options: AllDiffOptions
307 |   ): ChangeObject[] {
308 |     return changeObjects;
309 |   }
310 | 
311 |   get useLongestToken(): boolean {
312 |     return false;
313 |   }
314 | 
315 |   private buildValues(
316 |     lastComponent: DraftChangeObject | undefined,
317 |     newTokens: TokenT[],
318 |     oldTokens: TokenT[]
319 |   ): ChangeObject[] {
320 |     // First we convert our linked list of components in reverse order to an
321 |     // array in the right order:
322 |     const components: DraftChangeObject[] = [];
323 |     let nextComponent;
324 |     while (lastComponent) {
325 |       components.push(lastComponent);
326 |       nextComponent = lastComponent.previousComponent;
327 |       delete lastComponent.previousComponent;
328 |       lastComponent = nextComponent;
329 |     }
330 |     components.reverse();
331 | 
332 |     const componentLen = components.length;
333 |     let componentPos = 0,
334 |         newPos = 0,
335 |         oldPos = 0;
336 | 
337 |     for (; componentPos < componentLen; componentPos++) {
338 |       const component = components[componentPos];
339 |       if (!component.removed) {
340 |         if (!component.added && this.useLongestToken) {
341 |           let value = newTokens.slice(newPos, newPos + component.count);
342 |           value = value.map(function(value, i) {
343 |             const oldValue = oldTokens[oldPos + i];
344 |             return (oldValue as string).length > (value as string).length ? oldValue : value;
345 |           });
346 | 
347 |           component.value = this.join(value);
348 |         } else {
349 |           component.value = this.join(newTokens.slice(newPos, newPos + component.count));
350 |         }
351 |         newPos += component.count;
352 | 
353 |         // Common case
354 |         if (!component.added) {
355 |           oldPos += component.count;
356 |         }
357 |       } else {
358 |         component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
359 |         oldPos += component.count;
360 |       }
361 |     }
362 | 
363 |     return components as ChangeObject[];
364 |   }
365 | }
366 | 
367 | 


--------------------------------------------------------------------------------
/src/diff/character.ts:
--------------------------------------------------------------------------------
 1 | import Diff from './base.js';
 2 | import type { ChangeObject, CallbackOptionAbortable, CallbackOptionNonabortable, DiffCallbackNonabortable, DiffCharsOptionsAbortable, DiffCharsOptionsNonabortable} from '../types.js';
 3 | 
 4 | class CharacterDiff extends Diff {}
 5 | 
 6 | export const characterDiff = new CharacterDiff();
 7 | 
 8 | /**
 9 |  * diffs two blocks of text, treating each character as a token.
10 |  *
11 |  * ("Characters" here means Unicode code points - the elements you get when you loop over a string with a `for ... of ...` loop.)
12 |  *
13 |  * @returns a list of change objects.
14 |  */
15 | export function diffChars(
16 |   oldStr: string,
17 |   newStr: string,
18 |   options: DiffCallbackNonabortable
19 | ): undefined;
20 | export function diffChars(
21 |   oldStr: string,
22 |   newStr: string,
23 |   options: DiffCharsOptionsAbortable & CallbackOptionAbortable
24 | ): undefined
25 | export function diffChars(
26 |   oldStr: string,
27 |   newStr: string,
28 |   options: DiffCharsOptionsNonabortable & CallbackOptionNonabortable
29 | ): undefined
30 | export function diffChars(
31 |   oldStr: string,
32 |   newStr: string,
33 |   options: DiffCharsOptionsAbortable
34 | ): ChangeObject[] | undefined
35 | export function diffChars(
36 |   oldStr: string,
37 |   newStr: string,
38 |   options?: DiffCharsOptionsNonabortable
39 | ): ChangeObject[]
40 | export function diffChars(
41 |   oldStr: string,
42 |   newStr: string,
43 |   options?: any
44 | ): undefined | ChangeObject[] {
45 |   return characterDiff.diff(oldStr, newStr, options);
46 | }
47 | 


--------------------------------------------------------------------------------
/src/diff/css.ts:
--------------------------------------------------------------------------------
 1 | import Diff from './base.js';
 2 | import type { ChangeObject, CallbackOptionAbortable, CallbackOptionNonabortable, DiffCallbackNonabortable, DiffCssOptionsAbortable, DiffCssOptionsNonabortable} from '../types.js';
 3 | 
 4 | class CssDiff extends Diff {
 5 |   tokenize(value: string) {
 6 |     return value.split(/([{}:;,]|\s+)/);
 7 |   }
 8 | }
 9 | 
10 | export const cssDiff = new CssDiff();
11 | 
12 | /**
13 |  * diffs two blocks of text, comparing CSS tokens.
14 |  *
15 |  * @returns a list of change objects.
16 |  */
17 | export function diffCss(
18 |   oldStr: string,
19 |   newStr: string,
20 |   options: DiffCallbackNonabortable
21 | ): undefined;
22 | export function diffCss(
23 |   oldStr: string,
24 |   newStr: string,
25 |   options: DiffCssOptionsAbortable & CallbackOptionAbortable
26 | ): undefined
27 | export function diffCss(
28 |   oldStr: string,
29 |   newStr: string,
30 |   options: DiffCssOptionsNonabortable & CallbackOptionNonabortable
31 | ): undefined
32 | export function diffCss(
33 |   oldStr: string,
34 |   newStr: string,
35 |   options: DiffCssOptionsAbortable
36 | ): ChangeObject[] | undefined
37 | export function diffCss(
38 |   oldStr: string,
39 |   newStr: string,
40 |   options?: DiffCssOptionsNonabortable
41 | ): ChangeObject[]
42 | export function diffCss(oldStr: string, newStr: string, options?: any): undefined | ChangeObject[] {
43 |   return cssDiff.diff(oldStr, newStr, options);
44 | }
45 | 


--------------------------------------------------------------------------------
/src/diff/json.ts:
--------------------------------------------------------------------------------
  1 | import Diff from './base.js';
  2 | import type { ChangeObject, CallbackOptionAbortable, CallbackOptionNonabortable, DiffCallbackNonabortable, DiffJsonOptionsAbortable, DiffJsonOptionsNonabortable} from '../types.js';
  3 | import { tokenize } from './line.js';
  4 | 
  5 | class JsonDiff extends Diff {
  6 |   get useLongestToken() {
  7 |     // Discriminate between two lines of pretty-printed, serialized JSON where one of them has a
  8 |     // dangling comma and the other doesn't. Turns out including the dangling comma yields the nicest output:
  9 |     return true;
 10 |   }
 11 | 
 12 |   tokenize = tokenize;
 13 | 
 14 |   castInput(value: string | object, options: DiffJsonOptionsNonabortable | DiffJsonOptionsAbortable) {
 15 |     const {undefinedReplacement, stringifyReplacer = (k, v) => typeof v === 'undefined' ? undefinedReplacement : v} = options;
 16 | 
 17 |     return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), null, '  ');
 18 |   }
 19 | 
 20 |   equals(left: string, right: string, options: DiffJsonOptionsNonabortable | DiffJsonOptionsAbortable) {
 21 |     return super.equals(left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1'), options);
 22 |   }
 23 | }
 24 | 
 25 | export const jsonDiff = new JsonDiff();
 26 | 
 27 | /**
 28 |  * diffs two JSON-serializable objects by first serializing them to prettily-formatted JSON and then treating each line of the JSON as a token.
 29 |  * Object properties are ordered alphabetically in the serialized JSON, so the order of properties in the objects being compared doesn't affect the result.
 30 |  *
 31 |  * @returns a list of change objects.
 32 |  */
 33 | export function diffJson(
 34 |   oldStr: string | object,
 35 |   newStr: string | object,
 36 |   options: DiffCallbackNonabortable
 37 | ): undefined;
 38 | export function diffJson(
 39 |   oldStr: string | object,
 40 |   newStr: string | object,
 41 |   options: DiffJsonOptionsAbortable & CallbackOptionAbortable
 42 | ): undefined
 43 | export function diffJson(
 44 |   oldStr: string | object,
 45 |   newStr: string | object,
 46 |   options: DiffJsonOptionsNonabortable & CallbackOptionNonabortable
 47 | ): undefined
 48 | export function diffJson(
 49 |   oldStr: string | object,
 50 |   newStr: string | object,
 51 |   options: DiffJsonOptionsAbortable
 52 | ): ChangeObject[] | undefined
 53 | export function diffJson(
 54 |   oldStr: string | object,
 55 |   newStr: string | object,
 56 |   options?: DiffJsonOptionsNonabortable
 57 | ): ChangeObject[]
 58 | export function diffJson(oldStr: string | object, newStr: string | object, options?: any): undefined | ChangeObject[] {
 59 |   return jsonDiff.diff(oldStr, newStr, options);
 60 | }
 61 | 
 62 | 
 63 | // This function handles the presence of circular references by bailing out when encountering an
 64 | // object that is already on the "stack" of items being processed. Accepts an optional replacer
 65 | export function canonicalize(
 66 |   obj: any,
 67 |   stack: Array | null, replacementStack: Array | null,
 68 |   replacer: (k: string, v: any) => any,
 69 |   key?: string
 70 | ) {
 71 |   stack = stack || [];
 72 |   replacementStack = replacementStack || [];
 73 | 
 74 |   if (replacer) {
 75 |     obj = replacer(key === undefined ? '' : key, obj);
 76 |   }
 77 | 
 78 |   let i;
 79 | 
 80 |   for (i = 0; i < stack.length; i += 1) {
 81 |     if (stack[i] === obj) {
 82 |       return replacementStack[i];
 83 |     }
 84 |   }
 85 | 
 86 |   let canonicalizedObj: any;
 87 | 
 88 |   if ('[object Array]' === Object.prototype.toString.call(obj)) {
 89 |     stack.push(obj);
 90 |     canonicalizedObj = new Array(obj.length);
 91 |     replacementStack.push(canonicalizedObj);
 92 |     for (i = 0; i < obj.length; i += 1) {
 93 |       canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, String(i));
 94 |     }
 95 |     stack.pop();
 96 |     replacementStack.pop();
 97 |     return canonicalizedObj;
 98 |   }
 99 | 
100 |   if (obj && obj.toJSON) {
101 |     obj = obj.toJSON();
102 |   }
103 | 
104 |   if (typeof obj === 'object' && obj !== null) {
105 |     stack.push(obj);
106 |     canonicalizedObj = {};
107 |     replacementStack.push(canonicalizedObj);
108 |     const sortedKeys = [];
109 |     let key;
110 |     for (key in obj) {
111 |       /* istanbul ignore else */
112 |       if (Object.prototype.hasOwnProperty.call(obj, key)) {
113 |         sortedKeys.push(key);
114 |       }
115 |     }
116 |     sortedKeys.sort();
117 |     for (i = 0; i < sortedKeys.length; i += 1) {
118 |       key = sortedKeys[i];
119 |       canonicalizedObj[key] = canonicalize(obj[key], stack, replacementStack, replacer, key);
120 |     }
121 |     stack.pop();
122 |     replacementStack.pop();
123 |   } else {
124 |     canonicalizedObj = obj;
125 |   }
126 |   return canonicalizedObj;
127 | }
128 | 


--------------------------------------------------------------------------------
/src/diff/line.ts:
--------------------------------------------------------------------------------
  1 | import Diff from './base.js';
  2 | import type { ChangeObject, CallbackOptionAbortable, CallbackOptionNonabortable, DiffCallbackNonabortable, DiffLinesOptionsAbortable, DiffLinesOptionsNonabortable} from '../types.js';
  3 | import {generateOptions} from '../util/params.js';
  4 | 
  5 | class LineDiff extends Diff {
  6 |   tokenize = tokenize;
  7 | 
  8 |   equals(left: string, right: string, options: DiffLinesOptionsAbortable | DiffLinesOptionsNonabortable) {
  9 |     // If we're ignoring whitespace, we need to normalise lines by stripping
 10 |     // whitespace before checking equality. (This has an annoying interaction
 11 |     // with newlineIsToken that requires special handling: if newlines get their
 12 |     // own token, then we DON'T want to trim the *newline* tokens down to empty
 13 |     // strings, since this would cause us to treat whitespace-only line content
 14 |     // as equal to a separator between lines, which would be weird and
 15 |     // inconsistent with the documented behavior of the options.)
 16 |     if (options.ignoreWhitespace) {
 17 |       if (!options.newlineIsToken || !left.includes('\n')) {
 18 |         left = left.trim();
 19 |       }
 20 |       if (!options.newlineIsToken || !right.includes('\n')) {
 21 |         right = right.trim();
 22 |       }
 23 |     } else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
 24 |       if (left.endsWith('\n')) {
 25 |         left = left.slice(0, -1);
 26 |       }
 27 |       if (right.endsWith('\n')) {
 28 |         right = right.slice(0, -1);
 29 |       }
 30 |     }
 31 |     return super.equals(left, right, options);
 32 |   }
 33 | }
 34 | 
 35 | export const lineDiff = new LineDiff();
 36 | 
 37 | /**
 38 |  * diffs two blocks of text, treating each line as a token.
 39 |  * @returns a list of change objects.
 40 |  */
 41 | export function diffLines(
 42 |   oldStr: string,
 43 |   newStr: string,
 44 |   options: DiffCallbackNonabortable
 45 | ): undefined;
 46 | export function diffLines(
 47 |   oldStr: string,
 48 |   newStr: string,
 49 |   options: DiffLinesOptionsAbortable & CallbackOptionAbortable
 50 | ): undefined
 51 | export function diffLines(
 52 |   oldStr: string,
 53 |   newStr: string,
 54 |   options: DiffLinesOptionsNonabortable & CallbackOptionNonabortable
 55 | ): undefined
 56 | export function diffLines(
 57 |   oldStr: string,
 58 |   newStr: string,
 59 |   options: DiffLinesOptionsAbortable
 60 | ): ChangeObject[] | undefined
 61 | export function diffLines(
 62 |   oldStr: string,
 63 |   newStr: string,
 64 |   options?: DiffLinesOptionsNonabortable
 65 | ): ChangeObject[]
 66 | export function diffLines(oldStr: string, newStr: string, options?: any): undefined | ChangeObject[] {
 67 |   return lineDiff.diff(oldStr, newStr, options);
 68 | }
 69 | 
 70 | // Kept for backwards compatibility. This is a rather arbitrary wrapper method
 71 | // that just calls `diffLines` with `ignoreWhitespace: true`. It's confusing to
 72 | // have two ways to do exactly the same thing in the API, so we no longer
 73 | // document this one (library users should explicitly use `diffLines` with
 74 | // `ignoreWhitespace: true` instead) but we keep it around to maintain
 75 | // compatibility with code that used old versions.
 76 | export function diffTrimmedLines(
 77 |   oldStr: string,
 78 |   newStr: string,
 79 |   options: DiffCallbackNonabortable
 80 | ): undefined;
 81 | export function diffTrimmedLines(
 82 |   oldStr: string,
 83 |   newStr: string,
 84 |   options: DiffLinesOptionsAbortable & CallbackOptionAbortable
 85 | ): undefined
 86 | export function diffTrimmedLines(
 87 |   oldStr: string,
 88 |   newStr: string,
 89 |   options: DiffLinesOptionsNonabortable & CallbackOptionNonabortable
 90 | ): undefined
 91 | export function diffTrimmedLines(
 92 |   oldStr: string,
 93 |   newStr: string,
 94 |   options: DiffLinesOptionsAbortable
 95 | ): ChangeObject[] | undefined
 96 | export function diffTrimmedLines(
 97 |   oldStr: string,
 98 |   newStr: string,
 99 |   options?: DiffLinesOptionsNonabortable
100 | ): ChangeObject[]
101 | export function diffTrimmedLines(oldStr: string, newStr: string, options?: any): undefined | ChangeObject[] {
102 |   options = generateOptions(options, {ignoreWhitespace: true});
103 |   return lineDiff.diff(oldStr, newStr, options);
104 | }
105 | 
106 | // Exported standalone so it can be used from jsonDiff too.
107 | export function tokenize(value: string, options: DiffLinesOptionsAbortable | DiffLinesOptionsNonabortable) {
108 |   if(options.stripTrailingCr) {
109 |     // remove one \r before \n to match GNU diff's --strip-trailing-cr behavior
110 |     value = value.replace(/\r\n/g, '\n');
111 |   }
112 | 
113 |   const retLines = [],
114 |       linesAndNewlines = value.split(/(\n|\r\n)/);
115 | 
116 |   // Ignore the final empty token that occurs if the string ends with a new line
117 |   if (!linesAndNewlines[linesAndNewlines.length - 1]) {
118 |     linesAndNewlines.pop();
119 |   }
120 | 
121 |   // Merge the content and line separators into single tokens
122 |   for (let i = 0; i < linesAndNewlines.length; i++) {
123 |     const line = linesAndNewlines[i];
124 | 
125 |     if (i % 2 && !options.newlineIsToken) {
126 |       retLines[retLines.length - 1] += line;
127 |     } else {
128 |       retLines.push(line);
129 |     }
130 |   }
131 | 
132 |   return retLines;
133 | }
134 | 


--------------------------------------------------------------------------------
/src/diff/sentence.ts:
--------------------------------------------------------------------------------
 1 | import Diff from './base.js';
 2 | import type {
 3 |   ChangeObject,
 4 |   CallbackOptionAbortable,
 5 |   CallbackOptionNonabortable,
 6 |   DiffCallbackNonabortable,
 7 |   DiffSentencesOptionsAbortable,
 8 |   DiffSentencesOptionsNonabortable
 9 | } from '../types.js';
10 | 
11 | function isSentenceEndPunct(char: string) {
12 |   return char == '.' || char == '!' || char == '?';
13 | }
14 | 
15 | class SentenceDiff extends Diff {
16 |   tokenize(value: string) {
17 |     // If in future we drop support for environments that don't support lookbehinds, we can replace
18 |     // this entire function with:
19 |     //     return value.split(/(?<=[.!?])(\s+|$)/);
20 |     // but until then, for similar reasons to the trailingWs function in string.ts, we are forced
21 |     // to do this verbosely "by hand" instead of using a regex.
22 |     const result = [];
23 |     let tokenStartI = 0;
24 |     for (let i = 0; i < value.length; i++) {
25 |       if (i == value.length - 1) {
26 |         result.push(value.slice(tokenStartI));
27 |         break;
28 |       }
29 | 
30 |       if (isSentenceEndPunct(value[i]) && value[i + 1].match(/\s/)) {
31 |         // We've hit a sentence break - i.e. a punctuation mark followed by whitespace.
32 |         // We now want to push TWO tokens to the result:
33 |         // 1. the sentence
34 |         result.push(value.slice(tokenStartI, i + 1));
35 | 
36 |         // 2. the whitespace
37 |         i = tokenStartI = i + 1;
38 |         while (value[i + 1]?.match(/\s/)) {
39 |           i++;
40 |         }
41 |         result.push(value.slice(tokenStartI, i + 1));
42 | 
43 |         // Then the next token (a sentence) starts on the character after the whitespace.
44 |         // (It's okay if this is off the end of the string - then the outer loop will terminate
45 |         // here anyway.)
46 |         tokenStartI = i + 1;
47 |       }
48 |     }
49 | 
50 |     return result;
51 |   }
52 | }
53 | 
54 | export const sentenceDiff = new SentenceDiff();
55 | 
56 | /**
57 |  * diffs two blocks of text, treating each sentence, and the whitespace between each pair of sentences, as a token.
58 |  * The characters `.`, `!`, and `?`, when followed by whitespace, are treated as marking the end of a sentence; nothing else besides the end of the string is considered to mark a sentence end.
59 |  *
60 |  * (For more sophisticated detection of sentence breaks, including support for non-English punctuation, consider instead tokenizing with an [`Intl.Segmenter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter) with `granularity: 'sentence'` and passing the result to `diffArrays`.)
61 |  *
62 |  * @returns a list of change objects.
63 |  */
64 | export function diffSentences(
65 |   oldStr: string,
66 |   newStr: string,
67 |   options: DiffCallbackNonabortable
68 | ): undefined;
69 | export function diffSentences(
70 |   oldStr: string,
71 |   newStr: string,
72 |   options: DiffSentencesOptionsAbortable & CallbackOptionAbortable
73 | ): undefined
74 | export function diffSentences(
75 |   oldStr: string,
76 |   newStr: string,
77 |   options: DiffSentencesOptionsNonabortable & CallbackOptionNonabortable
78 | ): undefined
79 | export function diffSentences(
80 |   oldStr: string,
81 |   newStr: string,
82 |   options: DiffSentencesOptionsAbortable
83 | ): ChangeObject[] | undefined
84 | export function diffSentences(
85 |   oldStr: string,
86 |   newStr: string,
87 |   options?: DiffSentencesOptionsNonabortable
88 | ): ChangeObject[]
89 | export function diffSentences(oldStr: string, newStr: string, options?: any): undefined | ChangeObject[] {
90 |   return sentenceDiff.diff(oldStr, newStr, options);
91 | }
92 | 


--------------------------------------------------------------------------------
/src/diff/word.ts:
--------------------------------------------------------------------------------
  1 | import Diff from './base.js';
  2 | import type { ChangeObject, CallbackOptionAbortable, CallbackOptionNonabortable, DiffCallbackNonabortable, DiffWordsOptionsAbortable, DiffWordsOptionsNonabortable} from '../types.js';
  3 | import { longestCommonPrefix, longestCommonSuffix, replacePrefix, replaceSuffix, removePrefix, removeSuffix, maximumOverlap, leadingWs, trailingWs } from '../util/string.js';
  4 | 
  5 | // Based on https://en.wikipedia.org/wiki/Latin_script_in_Unicode
  6 | //
  7 | // Ranges and exceptions:
  8 | // Latin-1 Supplement, 0080–00FF
  9 | //  - U+00D7  × Multiplication sign
 10 | //  - U+00F7  ÷ Division sign
 11 | // Latin Extended-A, 0100–017F
 12 | // Latin Extended-B, 0180–024F
 13 | // IPA Extensions, 0250–02AF
 14 | // Spacing Modifier Letters, 02B0–02FF
 15 | //  - U+02C7  ˇ ˇ  Caron
 16 | //  - U+02D8  ˘ ˘  Breve
 17 | //  - U+02D9  ˙ ˙  Dot Above
 18 | //  - U+02DA  ˚ ˚  Ring Above
 19 | //  - U+02DB  ˛ ˛  Ogonek
 20 | //  - U+02DC  ˜ ˜  Small Tilde
 21 | //  - U+02DD  ˝ ˝  Double Acute Accent
 22 | // Latin Extended Additional, 1E00–1EFF
 23 | const extendedWordChars = 'a-zA-Z0-9_\\u{C0}-\\u{FF}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}';
 24 | 
 25 | // Each token is one of the following:
 26 | // - A punctuation mark plus the surrounding whitespace
 27 | // - A word plus the surrounding whitespace
 28 | // - Pure whitespace (but only in the special case where the entire text
 29 | //   is just whitespace)
 30 | //
 31 | // We have to include surrounding whitespace in the tokens because the two
 32 | // alternative approaches produce horribly broken results:
 33 | // * If we just discard the whitespace, we can't fully reproduce the original
 34 | //   text from the sequence of tokens and any attempt to render the diff will
 35 | //   get the whitespace wrong.
 36 | // * If we have separate tokens for whitespace, then in a typical text every
 37 | //   second token will be a single space character. But this often results in
 38 | //   the optimal diff between two texts being a perverse one that preserves
 39 | //   the spaces between words but deletes and reinserts actual common words.
 40 | //   See https://github.com/kpdecker/jsdiff/issues/160#issuecomment-1866099640
 41 | //   for an example.
 42 | //
 43 | // Keeping the surrounding whitespace of course has implications for .equals
 44 | // and .join, not just .tokenize.
 45 | 
 46 | // This regex does NOT fully implement the tokenization rules described above.
 47 | // Instead, it gives runs of whitespace their own "token". The tokenize method
 48 | // then handles stitching whitespace tokens onto adjacent word or punctuation
 49 | // tokens.
 50 | const tokenizeIncludingWhitespace = new RegExp(`[${extendedWordChars}]+|\\s+|[^${extendedWordChars}]`, 'ug');
 51 | 
 52 | 
 53 | class WordDiff extends Diff {
 54 |   equals(left: string, right: string, options: DiffWordsOptionsAbortable | DiffWordsOptionsNonabortable) {
 55 |     if (options.ignoreCase) {
 56 |       left = left.toLowerCase();
 57 |       right = right.toLowerCase();
 58 |     }
 59 | 
 60 |     return left.trim() === right.trim();
 61 |   }
 62 | 
 63 |   tokenize(value: string, options: DiffWordsOptionsAbortable | DiffWordsOptionsNonabortable = {}) {
 64 |     let parts;
 65 |     if (options.intlSegmenter) {
 66 |       const segmenter: Intl.Segmenter = options.intlSegmenter;
 67 |       if (segmenter.resolvedOptions().granularity != 'word') {
 68 |         throw new Error('The segmenter passed must have a granularity of "word"');
 69 |       }
 70 |       parts = Array.from(segmenter.segment(value), segment => segment.segment);
 71 |     } else {
 72 |       parts = value.match(tokenizeIncludingWhitespace) || [];
 73 |     }
 74 |     const tokens: string[] = [];
 75 |     let prevPart: string | null = null;
 76 |     parts.forEach(part => {
 77 |       if ((/\s/).test(part)) {
 78 |         if (prevPart == null) {
 79 |           tokens.push(part);
 80 |         } else {
 81 |           tokens.push(tokens.pop() + part);
 82 |         }
 83 |       } else if (prevPart != null && (/\s/).test(prevPart)) {
 84 |         if (tokens[tokens.length - 1] == prevPart) {
 85 |           tokens.push(tokens.pop() + part);
 86 |         } else {
 87 |           tokens.push(prevPart + part);
 88 |         }
 89 |       } else {
 90 |         tokens.push(part);
 91 |       }
 92 | 
 93 |       prevPart = part;
 94 |     });
 95 |     return tokens;
 96 |   }
 97 | 
 98 |   join(tokens: string[]) {
 99 |     // Tokens being joined here will always have appeared consecutively in the
100 |     // same text, so we can simply strip off the leading whitespace from all the
101 |     // tokens except the first (and except any whitespace-only tokens - but such
102 |     // a token will always be the first and only token anyway) and then join them
103 |     // and the whitespace around words and punctuation will end up correct.
104 |     return tokens.map((token, i) => {
105 |       if (i == 0) {
106 |         return token;
107 |       } else {
108 |         return token.replace((/^\s+/), '');
109 |       }
110 |     }).join('');
111 |   }
112 | 
113 |   postProcess(changes: ChangeObject[], options: any) {
114 |     if (!changes || options.oneChangePerToken) {
115 |       return changes;
116 |     }
117 | 
118 |     let lastKeep: ChangeObject | null = null;
119 |     // Change objects representing any insertion or deletion since the last
120 |     // "keep" change object. There can be at most one of each.
121 |     let insertion: ChangeObject | null = null;
122 |     let deletion: ChangeObject | null = null;
123 |     changes.forEach(change => {
124 |       if (change.added) {
125 |         insertion = change;
126 |       } else if (change.removed) {
127 |         deletion = change;
128 |       } else {
129 |         if (insertion || deletion) { // May be false at start of text
130 |           dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change);
131 |         }
132 |         lastKeep = change;
133 |         insertion = null;
134 |         deletion = null;
135 |       }
136 |     });
137 |     if (insertion || deletion) {
138 |       dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null);
139 |     }
140 |     return changes;
141 |   }
142 | }
143 | 
144 | export const wordDiff = new WordDiff();
145 | 
146 | /**
147 |  * diffs two blocks of text, treating each word and each punctuation mark as a token.
148 |  * Whitespace is ignored when computing the diff (but preserved as far as possible in the final change objects).
149 |  *
150 |  * @returns a list of change objects.
151 |  */
152 | export function diffWords(
153 |   oldStr: string,
154 |   newStr: string,
155 |   options: DiffCallbackNonabortable
156 | ): undefined;
157 | export function diffWords(
158 |   oldStr: string,
159 |   newStr: string,
160 |   options: DiffWordsOptionsAbortable & CallbackOptionAbortable
161 | ): undefined
162 | export function diffWords(
163 |   oldStr: string,
164 |   newStr: string,
165 |   options: DiffWordsOptionsNonabortable & CallbackOptionNonabortable
166 | ): undefined
167 | export function diffWords(
168 |   oldStr: string,
169 |   newStr: string,
170 |   options: DiffWordsOptionsAbortable
171 | ): ChangeObject[] | undefined
172 | export function diffWords(
173 |   oldStr: string,
174 |   newStr: string,
175 |   options?: DiffWordsOptionsNonabortable
176 | ): ChangeObject[]
177 | export function diffWords(oldStr: string, newStr: string, options?: any): undefined | ChangeObject[] {
178 |   // This option has never been documented and never will be (it's clearer to
179 |   // just call `diffWordsWithSpace` directly if you need that behavior), but
180 |   // has existed in jsdiff for a long time, so we retain support for it here
181 |   // for the sake of backwards compatibility.
182 |   if (options?.ignoreWhitespace != null && !options.ignoreWhitespace) {
183 |     return diffWordsWithSpace(oldStr, newStr, options);
184 |   }
185 | 
186 |   return wordDiff.diff(oldStr, newStr, options);
187 | }
188 | 
189 | function dedupeWhitespaceInChangeObjects(
190 |   startKeep: ChangeObject | null,
191 |   deletion: ChangeObject | null,
192 |   insertion: ChangeObject | null,
193 |   endKeep: ChangeObject | null
194 | ) {
195 |   // Before returning, we tidy up the leading and trailing whitespace of the
196 |   // change objects to eliminate cases where trailing whitespace in one object
197 |   // is repeated as leading whitespace in the next.
198 |   // Below are examples of the outcomes we want here to explain the code.
199 |   // I=insert, K=keep, D=delete
200 |   // 1. diffing 'foo bar baz' vs 'foo baz'
201 |   //    Prior to cleanup, we have K:'foo ' D:' bar ' K:' baz'
202 |   //    After cleanup, we want:   K:'foo ' D:'bar ' K:'baz'
203 |   //
204 |   // 2. Diffing 'foo bar baz' vs 'foo qux baz'
205 |   //    Prior to cleanup, we have K:'foo ' D:' bar ' I:' qux ' K:' baz'
206 |   //    After cleanup, we want K:'foo ' D:'bar' I:'qux' K:' baz'
207 |   //
208 |   // 3. Diffing 'foo\nbar baz' vs 'foo baz'
209 |   //    Prior to cleanup, we have K:'foo ' D:'\nbar ' K:' baz'
210 |   //    After cleanup, we want K'foo' D:'\nbar' K:' baz'
211 |   //
212 |   // 4. Diffing 'foo baz' vs 'foo\nbar baz'
213 |   //    Prior to cleanup, we have K:'foo\n' I:'\nbar ' K:' baz'
214 |   //    After cleanup, we ideally want K'foo' I:'\nbar' K:' baz'
215 |   //    but don't actually manage this currently (the pre-cleanup change
216 |   //    objects don't contain enough information to make it possible).
217 |   //
218 |   // 5. Diffing 'foo   bar baz' vs 'foo  baz'
219 |   //    Prior to cleanup, we have K:'foo  ' D:'   bar ' K:'  baz'
220 |   //    After cleanup, we want K:'foo  ' D:' bar ' K:'baz'
221 |   //
222 |   // Our handling is unavoidably imperfect in the case where there's a single
223 |   // indel between keeps and the whitespace has changed. For instance, consider
224 |   // diffing 'foo\tbar\nbaz' vs 'foo baz'. Unless we create an extra change
225 |   // object to represent the insertion of the space character (which isn't even
226 |   // a token), we have no way to avoid losing information about the texts'
227 |   // original whitespace in the result we return. Still, we do our best to
228 |   // output something that will look sensible if we e.g. print it with
229 |   // insertions in green and deletions in red.
230 | 
231 |   // Between two "keep" change objects (or before the first or after the last
232 |   // change object), we can have either:
233 |   // * A "delete" followed by an "insert"
234 |   // * Just an "insert"
235 |   // * Just a "delete"
236 |   // We handle the three cases separately.
237 |   if (deletion && insertion) {
238 |     const oldWsPrefix = leadingWs(deletion.value);
239 |     const oldWsSuffix = trailingWs(deletion.value);
240 |     const newWsPrefix = leadingWs(insertion.value);
241 |     const newWsSuffix = trailingWs(insertion.value);
242 | 
243 |     if (startKeep) {
244 |       const commonWsPrefix = longestCommonPrefix(oldWsPrefix, newWsPrefix);
245 |       startKeep.value = replaceSuffix(startKeep.value, newWsPrefix, commonWsPrefix);
246 |       deletion.value = removePrefix(deletion.value, commonWsPrefix);
247 |       insertion.value = removePrefix(insertion.value, commonWsPrefix);
248 |     }
249 |     if (endKeep) {
250 |       const commonWsSuffix = longestCommonSuffix(oldWsSuffix, newWsSuffix);
251 |       endKeep.value = replacePrefix(endKeep.value, newWsSuffix, commonWsSuffix);
252 |       deletion.value = removeSuffix(deletion.value, commonWsSuffix);
253 |       insertion.value = removeSuffix(insertion.value, commonWsSuffix);
254 |     }
255 |   } else if (insertion) {
256 |     // The whitespaces all reflect what was in the new text rather than
257 |     // the old, so we essentially have no information about whitespace
258 |     // insertion or deletion. We just want to dedupe the whitespace.
259 |     // We do that by having each change object keep its trailing
260 |     // whitespace and deleting duplicate leading whitespace where
261 |     // present.
262 |     if (startKeep) {
263 |       const ws = leadingWs(insertion.value);
264 |       insertion.value = insertion.value.substring(ws.length);
265 |     }
266 |     if (endKeep) {
267 |       const ws = leadingWs(endKeep.value);
268 |       endKeep.value = endKeep.value.substring(ws.length);
269 |     }
270 |   // otherwise we've got a deletion and no insertion
271 |   } else if (startKeep && endKeep) {
272 |     const newWsFull = leadingWs(endKeep.value),
273 |         delWsStart = leadingWs(deletion!.value),
274 |         delWsEnd = trailingWs(deletion!.value);
275 | 
276 |     // Any whitespace that comes straight after startKeep in both the old and
277 |     // new texts, assign to startKeep and remove from the deletion.
278 |     const newWsStart = longestCommonPrefix(newWsFull, delWsStart);
279 |     deletion!.value = removePrefix(deletion!.value, newWsStart);
280 | 
281 |     // Any whitespace that comes straight before endKeep in both the old and
282 |     // new texts, and hasn't already been assigned to startKeep, assign to
283 |     // endKeep and remove from the deletion.
284 |     const newWsEnd = longestCommonSuffix(
285 |       removePrefix(newWsFull, newWsStart),
286 |       delWsEnd
287 |     );
288 |     deletion!.value = removeSuffix(deletion!.value, newWsEnd);
289 |     endKeep.value = replacePrefix(endKeep.value, newWsFull, newWsEnd);
290 | 
291 |     // If there's any whitespace from the new text that HASN'T already been
292 |     // assigned, assign it to the start:
293 |     startKeep.value = replaceSuffix(
294 |       startKeep.value,
295 |       newWsFull,
296 |       newWsFull.slice(0, newWsFull.length - newWsEnd.length)
297 |     );
298 |   } else if (endKeep) {
299 |     // We are at the start of the text. Preserve all the whitespace on
300 |     // endKeep, and just remove whitespace from the end of deletion to the
301 |     // extent that it overlaps with the start of endKeep.
302 |     const endKeepWsPrefix = leadingWs(endKeep.value);
303 |     const deletionWsSuffix = trailingWs(deletion!.value);
304 |     const overlap = maximumOverlap(deletionWsSuffix, endKeepWsPrefix);
305 |     deletion!.value = removeSuffix(deletion!.value, overlap);
306 |   } else if (startKeep) {
307 |     // We are at the END of the text. Preserve all the whitespace on
308 |     // startKeep, and just remove whitespace from the start of deletion to
309 |     // the extent that it overlaps with the end of startKeep.
310 |     const startKeepWsSuffix = trailingWs(startKeep.value);
311 |     const deletionWsPrefix = leadingWs(deletion!.value);
312 |     const overlap = maximumOverlap(startKeepWsSuffix, deletionWsPrefix);
313 |     deletion!.value = removePrefix(deletion!.value, overlap);
314 |   }
315 | }
316 | 
317 | 
318 | class WordsWithSpaceDiff extends Diff {
319 |   tokenize(value: string) {
320 |     // Slightly different to the tokenizeIncludingWhitespace regex used above in
321 |     // that this one treats each individual newline as a distinct token, rather
322 |     // than merging them into other surrounding whitespace. This was requested
323 |     // in https://github.com/kpdecker/jsdiff/issues/180 &
324 |     //    https://github.com/kpdecker/jsdiff/issues/211
325 |     const regex = new RegExp(`(\\r?\\n)|[${extendedWordChars}]+|[^\\S\\n\\r]+|[^${extendedWordChars}]`, 'ug');
326 |     return value.match(regex) || [];
327 |   }
328 | }
329 | 
330 | export const wordsWithSpaceDiff = new WordsWithSpaceDiff();
331 | 
332 | /**
333 |  * diffs two blocks of text, treating each word, punctuation mark, newline, or run of (non-newline) whitespace as a token.
334 |  * @returns a list of change objects
335 |  */
336 | export function diffWordsWithSpace(
337 |   oldStr: string,
338 |   newStr: string,
339 |   options: DiffCallbackNonabortable
340 | ): undefined;
341 | export function diffWordsWithSpace(
342 |   oldStr: string,
343 |   newStr: string,
344 |   options: DiffWordsOptionsAbortable & CallbackOptionAbortable
345 | ): undefined
346 | export function diffWordsWithSpace(
347 |   oldStr: string,
348 |   newStr: string,
349 |   options: DiffWordsOptionsNonabortable & CallbackOptionNonabortable
350 | ): undefined
351 | export function diffWordsWithSpace(
352 |   oldStr: string,
353 |   newStr: string,
354 |   options: DiffWordsOptionsAbortable
355 | ): ChangeObject[] | undefined
356 | export function diffWordsWithSpace(
357 |   oldStr: string,
358 |   newStr: string,
359 |   options?: DiffWordsOptionsNonabortable
360 | ): ChangeObject[]
361 | export function diffWordsWithSpace(oldStr: string, newStr: string, options?: any): undefined | ChangeObject[] {
362 |   return wordsWithSpaceDiff.diff(oldStr, newStr, options);
363 | }
364 | 


--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
  1 | /* See LICENSE file for terms of use */
  2 | 
  3 | /*
  4 |  * Text diff implementation.
  5 |  *
  6 |  * This library supports the following APIs:
  7 |  * Diff.diffChars: Character by character diff
  8 |  * Diff.diffWords: Word (as defined by \b regex) diff which ignores whitespace
  9 |  * Diff.diffLines: Line based diff
 10 |  *
 11 |  * Diff.diffCss: Diff targeted at CSS content
 12 |  *
 13 |  * These methods are based on the implementation proposed in
 14 |  * "An O(ND) Difference Algorithm and its Variations" (Myers, 1986).
 15 |  * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927
 16 |  */
 17 | import Diff from './diff/base.js';
 18 | import {diffChars, characterDiff} from './diff/character.js';
 19 | import {diffWords, diffWordsWithSpace, wordDiff, wordsWithSpaceDiff} from './diff/word.js';
 20 | import {diffLines, diffTrimmedLines, lineDiff} from './diff/line.js';
 21 | import {diffSentences, sentenceDiff} from './diff/sentence.js';
 22 | 
 23 | import {diffCss, cssDiff} from './diff/css.js';
 24 | import {diffJson, canonicalize, jsonDiff} from './diff/json.js';
 25 | 
 26 | import {diffArrays, arrayDiff} from './diff/array.js';
 27 | 
 28 | import {applyPatch, applyPatches} from './patch/apply.js';
 29 | import type {ApplyPatchOptions, ApplyPatchesOptions} from './patch/apply.js';
 30 | import {parsePatch} from './patch/parse.js';
 31 | import {reversePatch} from './patch/reverse.js';
 32 | import {
 33 |   structuredPatch,
 34 |   createTwoFilesPatch,
 35 |   createPatch,
 36 |   formatPatch
 37 | } from './patch/create.js';
 38 | import type {
 39 |   StructuredPatchOptionsAbortable,
 40 |   StructuredPatchOptionsNonabortable,
 41 |   CreatePatchOptionsAbortable,
 42 |   CreatePatchOptionsNonabortable
 43 | } from './patch/create.js';
 44 | 
 45 | import {convertChangesToDMP} from './convert/dmp.js';
 46 | import {convertChangesToXML} from './convert/xml.js';
 47 | import type {
 48 |   ChangeObject,
 49 |   Change,
 50 |   DiffArraysOptionsAbortable,
 51 |   DiffArraysOptionsNonabortable,
 52 |   DiffCharsOptionsAbortable,
 53 |   DiffCharsOptionsNonabortable,
 54 |   DiffLinesOptionsAbortable,
 55 |   DiffLinesOptionsNonabortable,
 56 |   DiffWordsOptionsAbortable,
 57 |   DiffWordsOptionsNonabortable,
 58 |   DiffSentencesOptionsAbortable,
 59 |   DiffSentencesOptionsNonabortable,
 60 |   DiffJsonOptionsAbortable,
 61 |   DiffJsonOptionsNonabortable,
 62 |   DiffCssOptionsAbortable,
 63 |   DiffCssOptionsNonabortable,
 64 |   StructuredPatch,
 65 |   StructuredPatchHunk
 66 | } from './types.js';
 67 | 
 68 | export {
 69 |   Diff,
 70 | 
 71 |   diffChars,
 72 |   characterDiff,
 73 |   diffWords,
 74 |   wordDiff,
 75 |   diffWordsWithSpace,
 76 |   wordsWithSpaceDiff,
 77 |   diffLines,
 78 |   lineDiff,
 79 |   diffTrimmedLines,
 80 |   diffSentences,
 81 |   sentenceDiff,
 82 |   diffCss,
 83 |   cssDiff,
 84 |   diffJson,
 85 |   jsonDiff,
 86 |   diffArrays,
 87 |   arrayDiff,
 88 | 
 89 |   structuredPatch,
 90 |   createTwoFilesPatch,
 91 |   createPatch,
 92 |   formatPatch,
 93 |   applyPatch,
 94 |   applyPatches,
 95 |   parsePatch,
 96 |   reversePatch,
 97 |   convertChangesToDMP,
 98 |   convertChangesToXML,
 99 |   canonicalize
100 | };
101 | 
102 | export type {
103 |   ChangeObject,
104 |   Change,
105 |   DiffArraysOptionsAbortable,
106 |   DiffArraysOptionsNonabortable,
107 |   DiffCharsOptionsAbortable,
108 |   DiffCharsOptionsNonabortable,
109 |   DiffLinesOptionsAbortable,
110 |   DiffLinesOptionsNonabortable,
111 |   DiffWordsOptionsAbortable,
112 |   DiffWordsOptionsNonabortable,
113 |   DiffSentencesOptionsAbortable,
114 |   DiffSentencesOptionsNonabortable,
115 |   DiffJsonOptionsAbortable,
116 |   DiffJsonOptionsNonabortable,
117 |   DiffCssOptionsAbortable,
118 |   DiffCssOptionsNonabortable,
119 |   StructuredPatch,
120 |   StructuredPatchHunk,
121 | 
122 |   ApplyPatchOptions,
123 |   ApplyPatchesOptions,
124 | 
125 |   StructuredPatchOptionsAbortable,
126 |   StructuredPatchOptionsNonabortable,
127 |   CreatePatchOptionsAbortable,
128 |   CreatePatchOptionsNonabortable
129 | };
130 | 


--------------------------------------------------------------------------------
/src/patch/apply.ts:
--------------------------------------------------------------------------------
  1 | import {hasOnlyWinLineEndings, hasOnlyUnixLineEndings} from '../util/string.js';
  2 | import {isWin, isUnix, unixToWin, winToUnix} from './line-endings.js';
  3 | import {parsePatch} from './parse.js';
  4 | import distanceIterator from '../util/distance-iterator.js';
  5 | import type { StructuredPatch } from '../types.js';
  6 | 
  7 | export interface ApplyPatchOptions {
  8 |   /**
  9 |    * Maximum Levenshtein distance (in lines deleted, added, or subtituted) between the context shown in a patch hunk and the lines found in the file.
 10 |    * @default 0
 11 |    */
 12 |   fuzzFactor?: number,
 13 |   /**
 14 |    * If `true`, and if the file to be patched consistently uses different line endings to the patch (i.e. either the file always uses Unix line endings while the patch uses Windows ones, or vice versa), then `applyPatch` will behave as if the line endings in the patch were the same as those in the source file.
 15 |    * (If `false`, the patch will usually fail to apply in such circumstances since lines deleted in the patch won't be considered to match those in the source file.)
 16 |    * @default true
 17 |    */
 18 |   autoConvertLineEndings?: boolean,
 19 |   /**
 20 |    * Callback used to compare to given lines to determine if they should be considered equal when patching.
 21 |    * Defaults to strict equality but may be overridden to provide fuzzier comparison.
 22 |    * Should return false if the lines should be rejected.
 23 |    */
 24 |   compareLine?: (lineNumber: number, line: string, operation: string, patchContent: string) => boolean,
 25 | }
 26 | 
 27 | interface ApplyHunkReturnType {
 28 |   patchedLines: string[];
 29 |   oldLineLastI: number;
 30 | }
 31 | 
 32 | /**
 33 |  * attempts to apply a unified diff patch.
 34 |  *
 35 |  * Hunks are applied first to last.
 36 |  * `applyPatch` first tries to apply the first hunk at the line number specified in the hunk header, and with all context lines matching exactly.
 37 |  * If that fails, it tries scanning backwards and forwards, one line at a time, to find a place to apply the hunk where the context lines match exactly.
 38 |  * If that still fails, and `fuzzFactor` is greater than zero, it increments the maximum number of mismatches (missing, extra, or changed context lines) that there can be between the hunk context and a region where we are trying to apply the patch such that the hunk will still be considered to match.
 39 |  * Regardless of `fuzzFactor`, lines to be deleted in the hunk *must* be present for a hunk to match, and the context lines *immediately* before and after an insertion must match exactly.
 40 |  *
 41 |  * Once a hunk is successfully fitted, the process begins again with the next hunk.
 42 |  * Regardless of `fuzzFactor`, later hunks must be applied later in the file than earlier hunks.
 43 |  *
 44 |  * If a hunk cannot be successfully fitted *anywhere* with fewer than `fuzzFactor` mismatches, `applyPatch` fails and returns `false`.
 45 |  *
 46 |  * If a hunk is successfully fitted but not at the line number specified by the hunk header, all subsequent hunks have their target line number adjusted accordingly.
 47 |  * (e.g. if the first hunk is applied 10 lines below where the hunk header said it should fit, `applyPatch` will *start* looking for somewhere to apply the second hunk 10 lines below where its hunk header says it goes.)
 48 |  *
 49 |  * If the patch was applied successfully, returns a string containing the patched text.
 50 |  * If the patch could not be applied (because some hunks in the patch couldn't be fitted to the text in `source`), `applyPatch` returns false.
 51 |  *
 52 |  * @param patch a string diff or the output from the `parsePatch` or `structuredPatch` methods.
 53 |  */
 54 | export function applyPatch(
 55 |   source: string,
 56 |   patch: string | StructuredPatch | [StructuredPatch],
 57 |   options: ApplyPatchOptions = {}
 58 | ): string | false {
 59 |   let patches: StructuredPatch[];
 60 |   if (typeof patch === 'string') {
 61 |     patches = parsePatch(patch);
 62 |   } else if (Array.isArray(patch)) {
 63 |     patches = patch;
 64 |   } else {
 65 |     patches = [patch];
 66 |   }
 67 | 
 68 |   if (patches.length > 1) {
 69 |     throw new Error('applyPatch only works with a single input.');
 70 |   }
 71 | 
 72 |   return applyStructuredPatch(source, patches[0], options);
 73 | }
 74 | 
 75 | function applyStructuredPatch(
 76 |   source: string,
 77 |   patch: StructuredPatch,
 78 |   options: ApplyPatchOptions = {}
 79 | ): string | false {
 80 |   if (options.autoConvertLineEndings || options.autoConvertLineEndings == null) {
 81 |     if (hasOnlyWinLineEndings(source) && isUnix(patch)) {
 82 |       patch = unixToWin(patch);
 83 |     } else if (hasOnlyUnixLineEndings(source) && isWin(patch)) {
 84 |       patch = winToUnix(patch);
 85 |     }
 86 |   }
 87 | 
 88 |   // Apply the diff to the input
 89 |   const lines = source.split('\n'),
 90 |         hunks = patch.hunks,
 91 |         compareLine = options.compareLine || ((lineNumber, line, operation, patchContent) => line === patchContent),
 92 |         fuzzFactor = options.fuzzFactor || 0;
 93 |   let minLine = 0;
 94 | 
 95 |   if (fuzzFactor < 0 || !Number.isInteger(fuzzFactor)) {
 96 |     throw new Error('fuzzFactor must be a non-negative integer');
 97 |   }
 98 | 
 99 |   // Special case for empty patch.
100 |   if (!hunks.length) {
101 |     return source;
102 |   }
103 | 
104 |   // Before anything else, handle EOFNL insertion/removal. If the patch tells us to make a change
105 |   // to the EOFNL that is redundant/impossible - i.e. to remove a newline that's not there, or add a
106 |   // newline that already exists - then we either return false and fail to apply the patch (if
107 |   // fuzzFactor is 0) or simply ignore the problem and do nothing (if fuzzFactor is >0).
108 |   // If we do need to remove/add a newline at EOF, this will always be in the final hunk:
109 |   let prevLine = '',
110 |       removeEOFNL = false,
111 |       addEOFNL = false;
112 |   for (let i = 0; i < hunks[hunks.length - 1].lines.length; i++) {
113 |     const line = hunks[hunks.length - 1].lines[i];
114 |     if (line[0] == '\\') {
115 |       if (prevLine[0] == '+') {
116 |         removeEOFNL = true;
117 |       } else if (prevLine[0] == '-') {
118 |         addEOFNL = true;
119 |       }
120 |     }
121 |     prevLine = line;
122 |   }
123 |   if (removeEOFNL) {
124 |     if (addEOFNL) {
125 |       // This means the final line gets changed but doesn't have a trailing newline in either the
126 |       // original or patched version. In that case, we do nothing if fuzzFactor > 0, and if
127 |       // fuzzFactor is 0, we simply validate that the source file has no trailing newline.
128 |       if (!fuzzFactor && lines[lines.length - 1] == '') {
129 |         return false;
130 |       }
131 |     } else if (lines[lines.length - 1] == '') {
132 |       lines.pop();
133 |     } else if (!fuzzFactor) {
134 |       return false;
135 |     }
136 |   } else if (addEOFNL) {
137 |     if (lines[lines.length - 1] != '') {
138 |       lines.push('');
139 |     } else if (!fuzzFactor) {
140 |       return false;
141 |     }
142 |   }
143 | 
144 |   /**
145 |    * Checks if the hunk can be made to fit at the provided location with at most `maxErrors`
146 |    * insertions, substitutions, or deletions, while ensuring also that:
147 |    * - lines deleted in the hunk match exactly, and
148 |    * - wherever an insertion operation or block of insertion operations appears in the hunk, the
149 |    *   immediately preceding and following lines of context match exactly
150 |    *
151 |    * `toPos` should be set such that lines[toPos] is meant to match hunkLines[0].
152 |    *
153 |    * If the hunk can be applied, returns an object with properties `oldLineLastI` and
154 |    * `replacementLines`. Otherwise, returns null.
155 |    */
156 |   function applyHunk(
157 |     hunkLines: string[],
158 |     toPos: number,
159 |     maxErrors: number,
160 |     hunkLinesI: number = 0,
161 |     lastContextLineMatched: boolean = true,
162 |     patchedLines: string[] = [],
163 |     patchedLinesLength: number = 0
164 |   ): ApplyHunkReturnType | null {
165 |     let nConsecutiveOldContextLines = 0;
166 |     let nextContextLineMustMatch = false;
167 |     for (; hunkLinesI < hunkLines.length; hunkLinesI++) {
168 |       const hunkLine = hunkLines[hunkLinesI],
169 |           operation = (hunkLine.length > 0 ? hunkLine[0] : ' '),
170 |           content = (hunkLine.length > 0 ? hunkLine.substr(1) : hunkLine);
171 | 
172 |       if (operation === '-') {
173 |         if (compareLine(toPos + 1, lines[toPos], operation, content)) {
174 |           toPos++;
175 |           nConsecutiveOldContextLines = 0;
176 |         } else {
177 |           if (!maxErrors || lines[toPos] == null) {
178 |             return null;
179 |           }
180 |           patchedLines[patchedLinesLength] = lines[toPos];
181 |           return applyHunk(
182 |             hunkLines,
183 |             toPos + 1,
184 |             maxErrors - 1,
185 |             hunkLinesI,
186 |             false,
187 |             patchedLines,
188 |             patchedLinesLength + 1
189 |           );
190 |         }
191 |       }
192 | 
193 |       if (operation === '+') {
194 |         if (!lastContextLineMatched) {
195 |           return null;
196 |         }
197 |         patchedLines[patchedLinesLength] = content;
198 |         patchedLinesLength++;
199 |         nConsecutiveOldContextLines = 0;
200 |         nextContextLineMustMatch = true;
201 |       }
202 | 
203 |       if (operation === ' ') {
204 |         nConsecutiveOldContextLines++;
205 |         patchedLines[patchedLinesLength] = lines[toPos];
206 |         if (compareLine(toPos + 1, lines[toPos], operation, content)) {
207 |           patchedLinesLength++;
208 |           lastContextLineMatched = true;
209 |           nextContextLineMustMatch = false;
210 |           toPos++;
211 |         } else {
212 |           if (nextContextLineMustMatch || !maxErrors) {
213 |             return null;
214 |           }
215 | 
216 |           // Consider 3 possibilities in sequence:
217 |           // 1. lines contains a *substitution* not included in the patch context, or
218 |           // 2. lines contains an *insertion* not included in the patch context, or
219 |           // 3. lines contains a *deletion* not included in the patch context
220 |           // The first two options are of course only possible if the line from lines is non-null -
221 |           // i.e. only option 3 is possible if we've overrun the end of the old file.
222 |           return (
223 |             lines[toPos] && (
224 |               applyHunk(
225 |                 hunkLines,
226 |                 toPos + 1,
227 |                 maxErrors - 1,
228 |                 hunkLinesI + 1,
229 |                 false,
230 |                 patchedLines,
231 |                 patchedLinesLength + 1
232 |               ) || applyHunk(
233 |                 hunkLines,
234 |                 toPos + 1,
235 |                 maxErrors - 1,
236 |                 hunkLinesI,
237 |                 false,
238 |                 patchedLines,
239 |                 patchedLinesLength + 1
240 |               )
241 |             ) || applyHunk(
242 |               hunkLines,
243 |               toPos,
244 |               maxErrors - 1,
245 |               hunkLinesI + 1,
246 |               false,
247 |               patchedLines,
248 |               patchedLinesLength
249 |             )
250 |           );
251 |         }
252 |       }
253 |     }
254 | 
255 |     // Before returning, trim any unmodified context lines off the end of patchedLines and reduce
256 |     // toPos (and thus oldLineLastI) accordingly. This allows later hunks to be applied to a region
257 |     // that starts in this hunk's trailing context.
258 |     patchedLinesLength -= nConsecutiveOldContextLines;
259 |     toPos -= nConsecutiveOldContextLines;
260 |     patchedLines.length = patchedLinesLength;
261 |     return {
262 |       patchedLines,
263 |       oldLineLastI: toPos - 1
264 |     };
265 |   }
266 | 
267 |   const resultLines: string[] = [];
268 | 
269 |   // Search best fit offsets for each hunk based on the previous ones
270 |   let prevHunkOffset = 0;
271 |   for (let i = 0; i < hunks.length; i++) {
272 |     const hunk = hunks[i];
273 |     let hunkResult;
274 |     const maxLine = lines.length - hunk.oldLines + fuzzFactor;
275 |     let toPos: number | undefined;
276 |     for (let maxErrors = 0; maxErrors <= fuzzFactor; maxErrors++) {
277 |       toPos = hunk.oldStart + prevHunkOffset - 1;
278 |       const iterator = distanceIterator(toPos, minLine, maxLine);
279 |       for (; toPos !== undefined; toPos = iterator()) {
280 |         hunkResult = applyHunk(hunk.lines, toPos, maxErrors);
281 |         if (hunkResult) {
282 |           break;
283 |         }
284 |       }
285 |       if (hunkResult) {
286 |         break;
287 |       }
288 |     }
289 | 
290 |     if (!hunkResult) {
291 |       return false;
292 |     }
293 | 
294 |     // Copy everything from the end of where we applied the last hunk to the start of this hunk
295 |     for (let i = minLine; i < toPos!; i++) {
296 |       resultLines.push(lines[i]);
297 |     }
298 | 
299 |     // Add the lines produced by applying the hunk:
300 |     for (let i = 0; i < hunkResult.patchedLines.length; i++) {
301 |       const line = hunkResult.patchedLines[i];
302 |       resultLines.push(line);
303 |     }
304 | 
305 |     // Set lower text limit to end of the current hunk, so next ones don't try
306 |     // to fit over already patched text
307 |     minLine = hunkResult.oldLineLastI + 1;
308 | 
309 |     // Note the offset between where the patch said the hunk should've applied and where we
310 |     // applied it, so we can adjust future hunks accordingly:
311 |     prevHunkOffset = toPos! + 1 - hunk.oldStart;
312 |   }
313 | 
314 |   // Copy over the rest of the lines from the old text
315 |   for (let i = minLine; i < lines.length; i++) {
316 |     resultLines.push(lines[i]);
317 |   }
318 | 
319 |   return resultLines.join('\n');
320 | }
321 | 
322 | export interface ApplyPatchesOptions extends ApplyPatchOptions {
323 |   loadFile: (index: StructuredPatch, callback: (err: any, data: string) => void) => void,
324 |   patched: (index: StructuredPatch, content: string | false, callback: (err: any) => void) => void,
325 |   complete: (err?: any) => void,
326 | }
327 | 
328 | /**
329 |  * applies one or more patches.
330 |  *
331 |  * `patch` may be either an array of structured patch objects, or a string representing a patch in unified diff format (which may patch one or more files).
332 |  *
333 |  * This method will iterate over the contents of the patch and apply to data provided through callbacks. The general flow for each patch index is:
334 |  *
335 |  * - `options.loadFile(index, callback)` is called. The caller should then load the contents of the file and then pass that to the `callback(err, data)` callback. Passing an `err` will terminate further patch execution.
336 |  * - `options.patched(index, content, callback)` is called once the patch has been applied. `content` will be the return value from `applyPatch`. When it's ready, the caller should call `callback(err)` callback. Passing an `err` will terminate further patch execution.
337 |  *
338 |  * Once all patches have been applied or an error occurs, the `options.complete(err)` callback is made.
339 |  */
340 | export function applyPatches(uniDiff: string | StructuredPatch[], options: ApplyPatchesOptions): void {
341 |   const spDiff: StructuredPatch[] = typeof uniDiff === 'string' ? parsePatch(uniDiff) : uniDiff;
342 | 
343 |   let currentIndex = 0;
344 |   function processIndex(): void {
345 |     const index = spDiff[currentIndex++];
346 |     if (!index) {
347 |       return options.complete();
348 |     }
349 | 
350 |     options.loadFile(index, function(err: any, data: string) {
351 |       if (err) {
352 |         return options.complete(err);
353 |       }
354 | 
355 |       const updatedContent = applyPatch(data, index, options);
356 |       options.patched(index, updatedContent, function(err: any) {
357 |         if (err) {
358 |           return options.complete(err);
359 |         }
360 | 
361 |         processIndex();
362 |       });
363 |     });
364 |   }
365 |   processIndex();
366 | }
367 | 


--------------------------------------------------------------------------------
/src/patch/line-endings.ts:
--------------------------------------------------------------------------------
 1 | import type { StructuredPatch } from '../types.js';
 2 | 
 3 | export function unixToWin(patch: StructuredPatch): StructuredPatch;
 4 | export function unixToWin(patches: StructuredPatch[]): StructuredPatch[];
 5 | export function unixToWin(patch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[];
 6 | export function unixToWin(patch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[] {
 7 |   if (Array.isArray(patch)) {
 8 |     // It would be cleaner if instead of the line below we could just write
 9 |     //     return patch.map(unixToWin)
10 |     // but mysteriously TypeScript (v5.7.3 at the time of writing) does not like this and it will
11 |     // refuse to compile, thinking that unixToWin could then return StructuredPatch[][] and the
12 |     // result would be incompatible with the overload signatures.
13 |     // See bug report at https://github.com/microsoft/TypeScript/issues/61398.
14 |     return patch.map(p => unixToWin(p));
15 |   }
16 | 
17 |   return {
18 |     ...patch,
19 |     hunks: patch.hunks.map(hunk => ({
20 |       ...hunk,
21 |       lines: hunk.lines.map(
22 |         (line, i) =>
23 |           (line.startsWith('\\') || line.endsWith('\r') || hunk.lines[i + 1]?.startsWith('\\'))
24 |             ? line
25 |             : line + '\r'
26 |       )
27 |     }))
28 |   };
29 | }
30 | 
31 | export function winToUnix(patch: StructuredPatch): StructuredPatch;
32 | export function winToUnix(patches: StructuredPatch[]): StructuredPatch[];
33 | export function winToUnix(patch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[];
34 | export function winToUnix(patch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[] {
35 |   if (Array.isArray(patch)) {
36 |     // (See comment above equivalent line in unixToWin)
37 |     return patch.map(p => winToUnix(p));
38 |   }
39 | 
40 |   return {
41 |     ...patch,
42 |     hunks: patch.hunks.map(hunk => ({
43 |       ...hunk,
44 |       lines: hunk.lines.map(line => line.endsWith('\r') ? line.substring(0, line.length - 1) : line)
45 |     }))
46 |   };
47 | }
48 | 
49 | /**
50 |  * Returns true if the patch consistently uses Unix line endings (or only involves one line and has
51 |  * no line endings).
52 |  */
53 | export function isUnix(patch: StructuredPatch | StructuredPatch[]): boolean {
54 |   if (!Array.isArray(patch)) { patch = [patch]; }
55 |   return !patch.some(
56 |     index => index.hunks.some(
57 |       hunk => hunk.lines.some(
58 |         line => !line.startsWith('\\') && line.endsWith('\r')
59 |       )
60 |     )
61 |   );
62 | }
63 | 
64 | /**
65 |  * Returns true if the patch uses Windows line endings and only Windows line endings.
66 |  */
67 | export function isWin(patch: StructuredPatch | StructuredPatch[]): boolean {
68 |   if (!Array.isArray(patch)) { patch = [patch]; }
69 |   return patch.some(index => index.hunks.some(hunk => hunk.lines.some(line => line.endsWith('\r'))))
70 |     && patch.every(
71 |       index => index.hunks.every(
72 |         hunk => hunk.lines.every(
73 |           (line, i) => line.startsWith('\\') || line.endsWith('\r') || hunk.lines[i + 1]?.startsWith('\\')
74 |         )
75 |       )
76 |     );
77 | }
78 | 


--------------------------------------------------------------------------------
/src/patch/parse.ts:
--------------------------------------------------------------------------------
  1 | import type { StructuredPatch } from '../types.js';
  2 | 
  3 | /**
  4 |  * Parses a patch into structured data, in the same structure returned by `structuredPatch`.
  5 |  *
  6 |  * @return a JSON object representation of the a patch, suitable for use with the `applyPatch` method.
  7 |  */
  8 | export function parsePatch(uniDiff: string): StructuredPatch[] {
  9 |   const diffstr = uniDiff.split(/\n/),
 10 |         list: Partial[] = [];
 11 |   let i = 0;
 12 | 
 13 |   function parseIndex() {
 14 |     const index: Partial = {};
 15 |     list.push(index);
 16 | 
 17 |     // Parse diff metadata
 18 |     while (i < diffstr.length) {
 19 |       const line = diffstr[i];
 20 | 
 21 |       // File header found, end parsing diff metadata
 22 |       if ((/^(---|\+\+\+|@@)\s/).test(line)) {
 23 |         break;
 24 |       }
 25 | 
 26 |       // Diff index
 27 |       const header = (/^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/).exec(line);
 28 |       if (header) {
 29 |         index.index = header[1];
 30 |       }
 31 | 
 32 |       i++;
 33 |     }
 34 | 
 35 |     // Parse file headers if they are defined. Unified diff requires them, but
 36 |     // there's no technical issues to have an isolated hunk without file header
 37 |     parseFileHeader(index);
 38 |     parseFileHeader(index);
 39 | 
 40 |     // Parse hunks
 41 |     index.hunks = [];
 42 | 
 43 |     while (i < diffstr.length) {
 44 |       const line = diffstr[i];
 45 |       if ((/^(Index:\s|diff\s|---\s|\+\+\+\s|===================================================================)/).test(line)) {
 46 |         break;
 47 |       } else if ((/^@@/).test(line)) {
 48 |         index.hunks.push(parseHunk());
 49 |       } else if (line) {
 50 |         throw new Error('Unknown line ' + (i + 1) + ' ' + JSON.stringify(line));
 51 |       } else {
 52 |         i++;
 53 |       }
 54 |     }
 55 |   }
 56 | 
 57 |   // Parses the --- and +++ headers, if none are found, no lines
 58 |   // are consumed.
 59 |   function parseFileHeader(index: Partial) {
 60 |     const fileHeader = (/^(---|\+\+\+)\s+(.*)\r?$/).exec(diffstr[i]);
 61 |     if (fileHeader) {
 62 |       const data = fileHeader[2].split('\t', 2),
 63 |             header = (data[1] || '').trim();
 64 |       let fileName = data[0].replace(/\\\\/g, '\\');
 65 |       if ((/^".*"$/).test(fileName)) {
 66 |         fileName = fileName.substr(1, fileName.length - 2);
 67 |       }
 68 |       if (fileHeader[1] === '---') {
 69 |         index.oldFileName = fileName;
 70 |         index.oldHeader = header;
 71 |       } else {
 72 |         index.newFileName = fileName;
 73 |         index.newHeader = header;
 74 |       }
 75 | 
 76 |       i++;
 77 |     }
 78 |   }
 79 | 
 80 |   // Parses a hunk
 81 |   // This assumes that we are at the start of a hunk.
 82 |   function parseHunk() {
 83 |     const chunkHeaderIndex = i,
 84 |         chunkHeaderLine = diffstr[i++],
 85 |         chunkHeader = chunkHeaderLine.split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
 86 | 
 87 |     const hunk = {
 88 |       oldStart: +chunkHeader[1],
 89 |       oldLines: typeof chunkHeader[2] === 'undefined' ? 1 : +chunkHeader[2],
 90 |       newStart: +chunkHeader[3],
 91 |       newLines: typeof chunkHeader[4] === 'undefined' ? 1 : +chunkHeader[4],
 92 |       lines: [] as string[]
 93 |     };
 94 | 
 95 |     // Unified Diff Format quirk: If the chunk size is 0,
 96 |     // the first number is one lower than one would expect.
 97 |     // https://www.artima.com/weblogs/viewpost.jsp?thread=164293
 98 |     if (hunk.oldLines === 0) {
 99 |       hunk.oldStart += 1;
100 |     }
101 |     if (hunk.newLines === 0) {
102 |       hunk.newStart += 1;
103 |     }
104 | 
105 |     let addCount = 0,
106 |         removeCount = 0;
107 |     for (
108 |       ;
109 |       i < diffstr.length && (removeCount < hunk.oldLines || addCount < hunk.newLines || diffstr[i]?.startsWith('\\'));
110 |       i++
111 |     ) {
112 |       const operation = (diffstr[i].length == 0 && i != (diffstr.length - 1)) ? ' ' : diffstr[i][0];
113 |       if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') {
114 |         hunk.lines.push(diffstr[i]);
115 | 
116 |         if (operation === '+') {
117 |           addCount++;
118 |         } else if (operation === '-') {
119 |           removeCount++;
120 |         } else if (operation === ' ') {
121 |           addCount++;
122 |           removeCount++;
123 |         }
124 |       } else {
125 |         throw new Error(`Hunk at line ${chunkHeaderIndex + 1} contained invalid line ${diffstr[i]}`);
126 |       }
127 |     }
128 | 
129 |     // Handle the empty block count case
130 |     if (!addCount && hunk.newLines === 1) {
131 |       hunk.newLines = 0;
132 |     }
133 |     if (!removeCount && hunk.oldLines === 1) {
134 |       hunk.oldLines = 0;
135 |     }
136 | 
137 |     // Perform sanity checking
138 |     if (addCount !== hunk.newLines) {
139 |       throw new Error('Added line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
140 |     }
141 |     if (removeCount !== hunk.oldLines) {
142 |       throw new Error('Removed line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
143 |     }
144 | 
145 |     return hunk;
146 |   }
147 | 
148 |   while (i < diffstr.length) {
149 |     parseIndex();
150 |   }
151 | 
152 |   return list as StructuredPatch[];
153 | }
154 | 


--------------------------------------------------------------------------------
/src/patch/reverse.ts:
--------------------------------------------------------------------------------
 1 | import type { StructuredPatch } from '../types.js';
 2 | 
 3 | /**
 4 |  * @param patch either a single structured patch object (as returned by `structuredPatch`) or an array of them (as returned by `parsePatch`).
 5 |  * @returns a new structured patch which when applied will undo the original `patch`.
 6 |  */
 7 | export function reversePatch(structuredPatch: StructuredPatch): StructuredPatch;
 8 | export function reversePatch(structuredPatch: StructuredPatch[]): StructuredPatch[];
 9 | export function reversePatch(structuredPatch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[];
10 | export function reversePatch(structuredPatch: StructuredPatch | StructuredPatch[]): StructuredPatch | StructuredPatch[] {
11 |   if (Array.isArray(structuredPatch)) {
12 |     // (See comment in unixToWin for why we need the pointless-looking anonymous function here)
13 |     return structuredPatch.map(patch => reversePatch(patch)).reverse();
14 |   }
15 | 
16 |   return {
17 |     ...structuredPatch,
18 |     oldFileName: structuredPatch.newFileName,
19 |     oldHeader: structuredPatch.newHeader,
20 |     newFileName: structuredPatch.oldFileName,
21 |     newHeader: structuredPatch.oldHeader,
22 |     hunks: structuredPatch.hunks.map(hunk => {
23 |       return {
24 |         oldLines: hunk.newLines,
25 |         oldStart: hunk.newStart,
26 |         newLines: hunk.oldLines,
27 |         newStart: hunk.oldStart,
28 |         lines: hunk.lines.map(l => {
29 |           if (l.startsWith('-')) { return `+${l.slice(1)}`; }
30 |           if (l.startsWith('+')) { return `-${l.slice(1)}`; }
31 |           return l;
32 |         })
33 |       };
34 |     })
35 |   };
36 | }
37 | 


--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
  1 | export interface ChangeObject {
  2 |   /**
  3 |    * The concatenated content of all the tokens represented by this change object - i.e. generally the text that is either added, deleted, or common, as a single string.
  4 |    * In cases where tokens are considered common but are non-identical (e.g. because an option like `ignoreCase` or a custom `comparator` was used), the value from the *new* string will be provided here.
  5 |    */
  6 |   value: ValueT;
  7 |   /**
  8 |    * true if the value was inserted into the new string, otherwise false
  9 |    */
 10 |   added: boolean;
 11 |   /**
 12 |    * true if the value was removed from the old string, otherwise false
 13 |    */
 14 |   removed: boolean;
 15 |   /**
 16 |    * How many tokens (e.g. chars for `diffChars`, lines for `diffLines`) the value in the change object consists of
 17 |    */
 18 |   count: number;
 19 | }
 20 | 
 21 | // Name "Change" is used here for consistency with the previous type definitions from
 22 | // DefinitelyTyped. I would *guess* this is probably the single most common type for people to
 23 | // explicitly reference by name in their own code, so keeping its name consistent is valuable even
 24 | // though the names of many other types are inconsistent with the old DefinitelyTyped names.
 25 | export type Change = ChangeObject;
 26 | export type ArrayChange = ChangeObject;
 27 | 
 28 | export interface CommonDiffOptions {
 29 |   /**
 30 |    * If `true`, the array of change objects returned will contain one change object per token (e.g. one per line if calling `diffLines`), instead of runs of consecutive tokens that are all added / all removed / all conserved being combined into a single change object.
 31 |    */
 32 |   oneChangePerToken?: boolean,
 33 | }
 34 | 
 35 | export interface TimeoutOption {
 36 |   /**
 37 |    * A number of milliseconds after which the diffing algorithm will abort and return `undefined`.
 38 |    * Supported by the same functions as `maxEditLength`.
 39 |    */
 40 |   timeout: number;
 41 | }
 42 | 
 43 | export interface MaxEditLengthOption {
 44 |   /**
 45 |    * A number specifying the maximum edit distance to consider between the old and new texts.
 46 |    * You can use this to limit the computational cost of diffing large, very different texts by giving up early if the cost will be huge.
 47 |    * This option can be passed either to diffing functions (`diffLines`, `diffChars`, etc) or to patch-creation function (`structuredPatch`, `createPatch`, etc), all of which will indicate that the max edit length was reached by returning `undefined` instead of whatever they'd normally return.
 48 |    */
 49 |   maxEditLength: number;
 50 | }
 51 | 
 52 | export type AbortableDiffOptions = TimeoutOption | MaxEditLengthOption;
 53 | 
 54 | export type DiffCallbackNonabortable = (result: ChangeObject[]) => void;
 55 | export type DiffCallbackAbortable = (result: ChangeObject[] | undefined) => void;
 56 | 
 57 | export interface CallbackOptionNonabortable {
 58 |   /**
 59 |    * If provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated.
 60 |    * The value of the `callback` option should be a function and will be passed the computed diff or patch as its first argument.
 61 |    */
 62 |   callback: DiffCallbackNonabortable
 63 | }
 64 | export interface CallbackOptionAbortable {
 65 |   /**
 66 |    * If provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated.
 67 |    * The value of the `callback` option should be a function and will be passed the computed diff or patch as its first argument.
 68 |    */
 69 |   callback: DiffCallbackAbortable
 70 | }
 71 | 
 72 | interface DiffArraysOptions extends CommonDiffOptions {
 73 |   comparator?: (a: T, b: T) => boolean,
 74 | }
 75 | export interface DiffArraysOptionsNonabortable extends DiffArraysOptions {
 76 |   /**
 77 |    * If provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated.
 78 |    * The value of the `callback` option should be a function and will be passed the computed diff or patch as its first argument.
 79 |    */
 80 |   callback?: DiffCallbackNonabortable
 81 | }
 82 | export type DiffArraysOptionsAbortable = DiffArraysOptions & AbortableDiffOptions & Partial>
 83 | 
 84 | 
 85 | interface DiffCharsOptions extends CommonDiffOptions {
 86 |   /**
 87 |    * If `true`, the uppercase and lowercase forms of a character are considered equal.
 88 |    * @default false
 89 |    */
 90 |   ignoreCase?: boolean;
 91 | }
 92 | export interface DiffCharsOptionsNonabortable extends DiffCharsOptions {
 93 |   /**
 94 |    * If provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated.
 95 |    * The value of the `callback` option should be a function and will be passed the computed diff or patch as its first argument.
 96 |    */
 97 |   callback?: DiffCallbackNonabortable
 98 | }
 99 | export type DiffCharsOptionsAbortable = DiffCharsOptions & AbortableDiffOptions & Partial>
100 | 
101 | interface DiffLinesOptions extends CommonDiffOptions {
102 |   /**
103 |    * `true` to remove all trailing CR (`\r`) characters before performing the diff.
104 |    * This helps to get a useful diff when diffing UNIX text files against Windows text files.
105 |    * @default false
106 |    */
107 |   stripTrailingCr?: boolean,
108 |   /**
109 |    * `true` to treat the newline character at the end of each line as its own token.
110 |    * This allows for changes to the newline structure to occur independently of the line content and to be treated as such.
111 |    * In general this is the more human friendly form of `diffLines`; the default behavior with this option turned off is better suited for patches and other computer friendly output.
112 |    *
113 |    * Note that while using `ignoreWhitespace` in combination with `newlineIsToken` is not an error, results may not be as expected.
114 |    * With `ignoreWhitespace: true` and `newlineIsToken: false`, changing a completely empty line to contain some spaces is treated as a non-change, but with `ignoreWhitespace: true` and `newlineIsToken: true`, it is treated as an insertion.
115 |    * This is because the content of a completely blank line is not a token at all in `newlineIsToken` mode.
116 |    *
117 |    * @default false
118 |    */
119 |   newlineIsToken?: boolean,
120 |   /**
121 |    * `true` to ignore a missing newline character at the end of the last line when comparing it to other lines.
122 |    * (By default, the line `'b\n'` in text `'a\nb\nc'` is not considered equal to the line `'b'` in text `'a\nb'`; this option makes them be considered equal.)
123 |    * Ignored if `ignoreWhitespace` or `newlineIsToken` are also true.
124 |    * @default false
125 |    */
126 |   ignoreNewlineAtEof?: boolean,
127 |   /**
128 |    * `true` to ignore leading and trailing whitespace characters when checking if two lines are equal.
129 |    * @default false
130 |    */
131 |   ignoreWhitespace?: boolean,
132 | }
133 | export interface DiffLinesOptionsNonabortable extends DiffLinesOptions {
134 |   /**
135 |    * If provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated.
136 |    * The value of the `callback` option should be a function and will be passed the computed diff or patch as its first argument.
137 |    */
138 |   callback?: DiffCallbackNonabortable
139 | }
140 | export type DiffLinesOptionsAbortable = DiffLinesOptions & AbortableDiffOptions & Partial>
141 | 
142 | 
143 | interface DiffWordsOptions extends CommonDiffOptions {
144 |   /**
145 |    * Same as in `diffChars`.
146 |    * @default false
147 |    */
148 |   ignoreCase?: boolean
149 | 
150 |   /**
151 |    * An optional [`Intl.Segmenter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter) object (which must have a `granularity` of `'word'`) for `diffWords` to use to split the text into words.
152 |    *
153 |    * Note that this is (deliberately) incorrectly typed as `any` to avoid users whose `lib` & `target` settings in tsconfig.json are older than es2022 getting type errors when they build about `Intl.Segmenter` not existing.
154 |    * This is kind of ugly, since it makes the type declarations worse for users who genuinely use this feature, but seemed worth it to avoid the majority of the library's users (who probably do not use this particular option) getting confusing errors and being forced to change their `lib` to es2022 (even if their own code doesn't use any es2022 functions).
155 |    *
156 |    * By default, `diffWords` does not use an `Intl.Segmenter`, just some regexes for splitting text into words. This will tend to give worse results than `Intl.Segmenter` would, but ensures the results are consistent across environments; `Intl.Segmenter` behaviour is only loosely specced and the implementations in browsers could in principle change dramatically in future. If you want to use `diffWords` with an `Intl.Segmenter` but ensure it behaves the same whatever environment you run it in, use an `Intl.Segmenter` polyfill instead of the JavaScript engine's native `Intl.Segmenter` implementation.
157 |    *
158 |    * Using an `Intl.Segmenter` should allow better word-level diffing of non-English text than the default behaviour. For instance, `Intl.Segmenter`s can generally identify via built-in dictionaries which sequences of adjacent Chinese characters form words, allowing word-level diffing of Chinese. By specifying a language when instantiating the segmenter (e.g. `new Intl.Segmenter('sv', {granularity: 'word'})`) you can also support language-specific rules, like treating Swedish's colon separated contractions (like *k:a* for *kyrka*) as single words; by default this would be seen as two words separated by a colon.
159 |    */
160 |   intlSegmenter?: any,
161 | }
162 | export interface DiffWordsOptionsNonabortable extends DiffWordsOptions {
163 |   /**
164 |    * If provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated.
165 |    * The value of the `callback` option should be a function and will be passed the computed diff or patch as its first argument.
166 |    */
167 |   callback?: DiffCallbackNonabortable
168 | }
169 | export type DiffWordsOptionsAbortable = DiffWordsOptions & AbortableDiffOptions & Partial>
170 | 
171 | 
172 | interface DiffSentencesOptions extends CommonDiffOptions {}
173 | export interface DiffSentencesOptionsNonabortable extends DiffSentencesOptions {
174 |   /**
175 |    * If provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated.
176 |    * The value of the `callback` option should be a function and will be passed the computed diff or patch as its first argument.
177 |    */
178 |   callback?: DiffCallbackNonabortable
179 | }
180 | export type DiffSentencesOptionsAbortable = DiffSentencesOptions & AbortableDiffOptions & Partial>
181 | 
182 | 
183 | interface DiffJsonOptions extends CommonDiffOptions {
184 |   /**
185 |    * A value to replace `undefined` with. Ignored if a `stringifyReplacer` is provided.
186 |    */
187 |   undefinedReplacement?: any,
188 |   /**
189 |    * A custom replacer function.
190 |    * Operates similarly to the `replacer` parameter to [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter), but must be a function.
191 |    */
192 |   stringifyReplacer?: (k: string, v: any) => any,
193 | }
194 | export interface DiffJsonOptionsNonabortable extends DiffJsonOptions {
195 |   /**
196 |    * If provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated.
197 |    * The value of the `callback` option should be a function and will be passed the computed diff or patch as its first argument.
198 |    */
199 |   callback?: DiffCallbackNonabortable
200 | }
201 | export type DiffJsonOptionsAbortable = DiffJsonOptions & AbortableDiffOptions & Partial>
202 | 
203 | 
204 | interface DiffCssOptions extends CommonDiffOptions {}
205 | export interface DiffCssOptionsNonabortable extends DiffCssOptions {
206 |   /**
207 |    * If provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated.
208 |    * The value of the `callback` option should be a function and will be passed the computed diff or patch as its first argument.
209 |    */
210 |   callback?: DiffCallbackNonabortable
211 | }
212 | export type DiffCssOptionsAbortable = DiffCssOptions & AbortableDiffOptions & Partial>
213 | 
214 | 
215 | /**
216 |  * Note that this contains the union of ALL options accepted by any of the built-in diffing
217 |  * functions. The README notes which options are usable which functions. Using an option with a
218 |  * diffing function that doesn't support it might yield unreasonable results.
219 |  */
220 | export type AllDiffOptions =
221 |   DiffArraysOptions &
222 |   DiffCharsOptions &
223 |   DiffWordsOptions &
224 |   DiffLinesOptions &
225 |   DiffJsonOptions;
226 | 
227 | export interface StructuredPatch {
228 |   oldFileName: string,
229 |   newFileName: string,
230 |   oldHeader: string | undefined,
231 |   newHeader: string | undefined,
232 |   hunks: StructuredPatchHunk[],
233 |   index?: string,
234 | }
235 | 
236 | export interface StructuredPatchHunk {
237 |   oldStart: number,
238 |   oldLines: number,
239 |   newStart: number,
240 |   newLines: number,
241 |   lines: string[],
242 | }
243 | 


--------------------------------------------------------------------------------
/src/util/array.ts:
--------------------------------------------------------------------------------
 1 | export function arrayEqual(a: any[], b: any[]): boolean {
 2 |   if (a.length !== b.length) {
 3 |     return false;
 4 |   }
 5 | 
 6 |   return arrayStartsWith(a, b);
 7 | }
 8 | 
 9 | export function arrayStartsWith(array: any[], start: any[]): boolean {
10 |   if (start.length > array.length) {
11 |     return false;
12 |   }
13 | 
14 |   for (let i = 0; i < start.length; i++) {
15 |     if (start[i] !== array[i]) {
16 |       return false;
17 |     }
18 |   }
19 | 
20 |   return true;
21 | }
22 | 


--------------------------------------------------------------------------------
/src/util/distance-iterator.ts:
--------------------------------------------------------------------------------
 1 | // Iterator that traverses in the range of [min, max], stepping
 2 | // by distance from a given start position. I.e. for [0, 4], with
 3 | // start of 2, this will iterate 2, 3, 1, 4, 0.
 4 | export default function(start: number, minLine: number, maxLine: number): () => number | undefined {
 5 |   let wantForward = true,
 6 |       backwardExhausted = false,
 7 |       forwardExhausted = false,
 8 |       localOffset = 1;
 9 | 
10 |   return function iterator(): number | undefined {
11 |     if (wantForward && !forwardExhausted) {
12 |       if (backwardExhausted) {
13 |         localOffset++;
14 |       } else {
15 |         wantForward = false;
16 |       }
17 | 
18 |       // Check if trying to fit beyond text length, and if not, check it fits
19 |       // after offset location (or desired location on first iteration)
20 |       if (start + localOffset <= maxLine) {
21 |         return start + localOffset;
22 |       }
23 | 
24 |       forwardExhausted = true;
25 |     }
26 | 
27 |     if (!backwardExhausted) {
28 |       if (!forwardExhausted) {
29 |         wantForward = true;
30 |       }
31 | 
32 |       // Check if trying to fit before text beginning, and if not, check it fits
33 |       // before offset location
34 |       if (minLine <= start - localOffset) {
35 |         return start - localOffset++;
36 |       }
37 | 
38 |       backwardExhausted = true;
39 |       return iterator();
40 |     }
41 | 
42 |     // We tried to fit hunk before text beginning and beyond text length, then
43 |     // hunk can't fit on the text. Return undefined
44 |     return undefined;
45 |   };
46 | }
47 | 


--------------------------------------------------------------------------------
/src/util/params.ts:
--------------------------------------------------------------------------------
 1 | export function generateOptions(
 2 |   options: {[key: string]: any} | ((_: unknown) => void),
 3 |   defaults: any
 4 | ): object {
 5 |   if (typeof options === 'function') {
 6 |     defaults.callback = options;
 7 |   } else if (options) {
 8 |     for (const name in options) {
 9 |       /* istanbul ignore else */
10 |       if (Object.prototype.hasOwnProperty.call(options, name)) {
11 |         defaults[name] = options[name];
12 |       }
13 |     }
14 |   }
15 |   return defaults;
16 | }
17 | 


--------------------------------------------------------------------------------
/src/util/string.ts:
--------------------------------------------------------------------------------
  1 | export function longestCommonPrefix(str1: string, str2: string): string {
  2 |   let i;
  3 |   for (i = 0; i < str1.length && i < str2.length; i++) {
  4 |     if (str1[i] != str2[i]) {
  5 |       return str1.slice(0, i);
  6 |     }
  7 |   }
  8 |   return str1.slice(0, i);
  9 | }
 10 | 
 11 | export function longestCommonSuffix(str1: string, str2: string): string {
 12 |   let i;
 13 | 
 14 |   // Unlike longestCommonPrefix, we need a special case to handle all scenarios
 15 |   // where we return the empty string since str1.slice(-0) will return the
 16 |   // entire string.
 17 |   if (!str1 || !str2 || str1[str1.length - 1] != str2[str2.length - 1]) {
 18 |     return '';
 19 |   }
 20 | 
 21 |   for (i = 0; i < str1.length && i < str2.length; i++) {
 22 |     if (str1[str1.length - (i + 1)] != str2[str2.length - (i + 1)]) {
 23 |       return str1.slice(-i);
 24 |     }
 25 |   }
 26 |   return str1.slice(-i);
 27 | }
 28 | 
 29 | export function replacePrefix(string: string, oldPrefix: string, newPrefix: string): string {
 30 |   if (string.slice(0, oldPrefix.length) != oldPrefix) {
 31 |     throw Error(`string ${JSON.stringify(string)} doesn't start with prefix ${JSON.stringify(oldPrefix)}; this is a bug`);
 32 |   }
 33 |   return newPrefix + string.slice(oldPrefix.length);
 34 | }
 35 | 
 36 | export function replaceSuffix(string: string, oldSuffix: string, newSuffix: string): string {
 37 |   if (!oldSuffix) {
 38 |     return string + newSuffix;
 39 |   }
 40 | 
 41 |   if (string.slice(-oldSuffix.length) != oldSuffix) {
 42 |     throw Error(`string ${JSON.stringify(string)} doesn't end with suffix ${JSON.stringify(oldSuffix)}; this is a bug`);
 43 |   }
 44 |   return string.slice(0, -oldSuffix.length) + newSuffix;
 45 | }
 46 | 
 47 | export function removePrefix(string: string, oldPrefix: string): string {
 48 |   return replacePrefix(string, oldPrefix, '');
 49 | }
 50 | 
 51 | export function removeSuffix(string: string, oldSuffix: string): string {
 52 |   return replaceSuffix(string, oldSuffix, '');
 53 | }
 54 | 
 55 | export function maximumOverlap(string1: string, string2: string): string {
 56 |   return string2.slice(0, overlapCount(string1, string2));
 57 | }
 58 | 
 59 | // Nicked from https://stackoverflow.com/a/60422853/1709587
 60 | function overlapCount(a: string, b: string): number {
 61 |   // Deal with cases where the strings differ in length
 62 |   let startA = 0;
 63 |   if (a.length > b.length) { startA = a.length - b.length; }
 64 |   let endB = b.length;
 65 |   if (a.length < b.length) { endB = a.length; }
 66 |   // Create a back-reference for each index
 67 |   //   that should be followed in case of a mismatch.
 68 |   //   We only need B to make these references:
 69 |   const map = Array(endB);
 70 |   let k = 0; // Index that lags behind j
 71 |   map[0] = 0;
 72 |   for (let j = 1; j < endB; j++) {
 73 |       if (b[j] == b[k]) {
 74 |           map[j] = map[k]; // skip over the same character (optional optimisation)
 75 |       } else {
 76 |           map[j] = k;
 77 |       }
 78 |       while (k > 0 && b[j] != b[k]) { k = map[k]; }
 79 |       if (b[j] == b[k]) { k++; }
 80 |   }
 81 |   // Phase 2: use these references while iterating over A
 82 |   k = 0;
 83 |   for (let i = startA; i < a.length; i++) {
 84 |       while (k > 0 && a[i] != b[k]) { k = map[k]; }
 85 |       if (a[i] == b[k]) { k++; }
 86 |   }
 87 |   return k;
 88 | }
 89 | 
 90 | 
 91 | /**
 92 |  * Returns true if the string consistently uses Windows line endings.
 93 |  */
 94 | export function hasOnlyWinLineEndings(string: string): boolean {
 95 |   return string.includes('\r\n') && !string.startsWith('\n') && !string.match(/[^\r]\n/);
 96 | }
 97 | 
 98 | /**
 99 |  * Returns true if the string consistently uses Unix line endings.
100 |  */
101 | export function hasOnlyUnixLineEndings(string: string): boolean {
102 |   return !string.includes('\r\n') && string.includes('\n');
103 | }
104 | 
105 | export function trailingWs(string: string): string {
106 |   // Yes, this looks overcomplicated and dumb - why not replace the whole function with
107 |   //     return string.match(/\s*$/)[0]
108 |   // you ask? Because:
109 |   // 1. the trap described at https://markamery.com/blog/quadratic-time-regexes/ would mean doing
110 |   //    this would cause this function to take O(n²) time in the worst case (specifically when
111 |   //    there is a massive run of NON-TRAILING whitespace in `string`), and
112 |   // 2. the fix proposed in the same blog post, of using a negative lookbehind, is incompatible
113 |   //    with old Safari versions that we'd like to not break if possible (see
114 |   //    https://github.com/kpdecker/jsdiff/pull/550)
115 |   // It feels absurd to do this with an explicit loop instead of a regex, but I really can't see a
116 |   // better way that doesn't result in broken behaviour.
117 |   let i;
118 |   for (i = string.length - 1; i >= 0; i--) {
119 |     if (!string[i].match(/\s/)) {
120 |       break;
121 |     }
122 |   }
123 |   return string.substring(i + 1);
124 | }
125 | 
126 | export function leadingWs(string: string): string {
127 |   // Thankfully the annoying considerations described in trailingWs don't apply here:
128 |   const match = string.match(/^\s*/);
129 |   return match ? match[0] : '';
130 | }
131 | 


--------------------------------------------------------------------------------
/test-d/diffCharsOverloads.test-d.ts:
--------------------------------------------------------------------------------
 1 | import {expectType} from 'tsd';
 2 | import {ChangeObject, diffChars} from '../libesm/index.js';
 3 | 
 4 | const result1 = diffChars('foo', 'bar', {ignoreCase: true});
 5 | expectType[]>(result1);
 6 | 
 7 | const result2 = diffChars('foo', 'bar');
 8 | expectType[]>(result2);
 9 | 
10 | const result3 = diffChars('foo', 'bar', {timeout: 100});
11 | expectType[] | undefined>(result3);
12 | 
13 | const result4 = diffChars('foo', 'bar', {maxEditLength: 100});
14 | expectType[] | undefined>(result4);
15 | 
16 | const result5 = diffChars('foo', 'bar', cbResult => {
17 |     expectType[]>(cbResult)
18 | });
19 | expectType(result5);
20 | 
21 | const result6 = diffChars('foo', 'bar', {
22 |     callback: cbResult => {
23 |         expectType[]>(cbResult);
24 |     }
25 | });
26 | expectType(result6);
27 | 
28 | const result7 = diffChars('foo', 'bar', {
29 |     timeout: 100,
30 |     callback: cbResult => {
31 |         expectType[] | undefined>(cbResult)
32 |     }
33 | });
34 | expectType(result7);
35 | 


--------------------------------------------------------------------------------
/test-d/originalDefinitelyTypedTests.test-d.ts:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * This file was copied from
  3 |  * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/diff/diff-tests.ts
  4 |  * then tweaked to work with tsd.
  5 |  */
  6 | 
  7 | import {expectType} from 'tsd';
  8 | import Diff, { Change, StructuredPatch } from "../libesm/index.js";
  9 | 
 10 | const one = "beep boop";
 11 | const other = "beep boob blah";
 12 | 
 13 | let changes = Diff.diffChars(one, other);
 14 | examineChanges(changes);
 15 | 
 16 | expectType(Diff.diffChars(one, other, {
 17 |     callback: (value) => {
 18 |         expectType(value);
 19 |     },
 20 | }));
 21 | expectType(Diff.diffChars(one, other, (value) => {
 22 |     expectType(value);
 23 | }));
 24 | Diff.diffWords("吾輩は猫である。名前はまだ無い。", "吾輩は猫である。名前はたぬき。", {
 25 |     intlSegmenter: new Intl.Segmenter("ja-JP", { granularity: "word" }),
 26 | });
 27 | expectType(
 28 |     Diff.diffLines(
 29 |         "line\nold value\nline",
 30 |         "line\nnew value\nline",
 31 |         {
 32 |             stripTrailingCr: true,
 33 |             ignoreNewlineAtEof: true,
 34 |             maxEditLength: 1,
 35 |             oneChangePerToken: true,
 36 |         },
 37 |     )
 38 | );
 39 | expectType(
 40 |     Diff.createPatch("filename", "A", "a", undefined, undefined, {
 41 |         callback: (value) => {
 42 |             expectType(value);
 43 |         },
 44 |     })
 45 | );
 46 | 
 47 | const diffArraysResult = Diff.diffArrays(["a", "b", "c"], ["a", "c", "d"]);
 48 | diffArraysResult.forEach(result => {
 49 |     expectType(result.added);
 50 |     expectType(result.removed);
 51 |     expectType(result.value);
 52 |     expectType(result.count);
 53 | });
 54 | 
 55 | interface DiffObj {
 56 |     value: number;
 57 | }
 58 | const a: DiffObj = { value: 0 };
 59 | const b: DiffObj = { value: 1 };
 60 | const c: DiffObj = { value: 2 };
 61 | const d: DiffObj = { value: 3 };
 62 | const arrayOptions: Diff.DiffArraysOptionsNonabortable = {
 63 |     comparator: (left, right) => {
 64 |         return left.value === right.value;
 65 |     },
 66 | };
 67 | const arrayChanges = Diff.diffArrays([a, b, c], [a, b, d], arrayOptions);
 68 | arrayChanges.forEach(result => {
 69 |     expectType(result.added)
 70 |     expectType(result.removed)
 71 |     expectType(result.value)
 72 |     expectType(result.count)
 73 | });
 74 | 
 75 | // --------------------------
 76 | 
 77 | class LineDiffWithoutWhitespace extends Diff.Diff {
 78 |     tokenize(value: string): any {
 79 |         return value.split(/^/m);
 80 |     }
 81 | 
 82 |     equals(left: string, right: string): boolean {
 83 |         return left.trim() === right.trim();
 84 |     }
 85 | }
 86 | 
 87 | const obj = new LineDiffWithoutWhitespace();
 88 | changes = obj.diff(one, other);
 89 | examineChanges(changes);
 90 | 
 91 | function examineChanges(diff: Diff.Change[]) {
 92 |     diff.forEach(part => {
 93 |         expectType(part.added);
 94 |         expectType(part.removed);
 95 |         expectType(part.value);
 96 |         expectType(part.count);
 97 |     });
 98 | }
 99 | 
100 | function verifyPatchMethods(oldStr: string, newStr: string, uniDiff: Diff.StructuredPatch) {
101 |     const verifyPatch = Diff.parsePatch(
102 |         Diff.createTwoFilesPatch("oldFile.ts", "newFile.ts", oldStr, newStr, "old", "new", {
103 |             context: 1,
104 |             stripTrailingCr: true,
105 |         }),
106 |     );
107 | 
108 |     if (
109 |         JSON.stringify(verifyPatch[0], Object.keys(verifyPatch[0]).sort())
110 |             !== JSON.stringify(uniDiff, Object.keys(uniDiff).sort())
111 |     ) {
112 |         throw new Error("Patch did not match uniDiff");
113 |     }
114 | }
115 | function verifyApplyMethods(oldStr: string, newStr: string, uniDiffStr: string) {
116 |     const uniDiff = Diff.parsePatch(uniDiffStr)[0];
117 |     const verifyApply = [Diff.applyPatch(oldStr, uniDiff), Diff.applyPatch(oldStr, [uniDiff])];
118 |     const options: Diff.ApplyPatchesOptions = {
119 |         loadFile(index, callback) {
120 |             expectType(index);
121 |             callback(undefined, one);
122 |         },
123 |         patched(index, content) {
124 |             expectType(index); 
125 |             if (content !== false) {
126 |                 verifyApply.push(content);
127 |             }
128 |         },
129 |         complete(err) {
130 |             if (err) {
131 |                 throw err;
132 |             }
133 | 
134 |             verifyApply.forEach(result => {
135 |                 if (result !== newStr) {
136 |                     throw new Error("Result did not match newStr");
137 |                 }
138 |             });
139 |         },
140 |         compareLine(_, line, operator, patchContent) {
141 |             if (operator === " ") {
142 |                 return true;
143 |             }
144 |             return line === patchContent;
145 |         },
146 |         fuzzFactor: 0,
147 |     };
148 |     Diff.applyPatches([uniDiff], options);
149 |     Diff.applyPatches(uniDiffStr, options);
150 | }
151 | 
152 | const uniDiffPatch = Diff.structuredPatch("oldFile.ts", "newFile.ts", one, other, "old", "new", {
153 |     context: 1,
154 | });
155 | verifyPatchMethods(one, other, uniDiffPatch);
156 | 
157 | const formatted: string = Diff.formatPatch(uniDiffPatch);
158 | 
159 | const uniDiffStr = Diff.createPatch("file.ts", one, other, "old", "new", { context: 1 });
160 | verifyApplyMethods(one, other, uniDiffStr);
161 | 
162 | const file1 = "line1\nline2\nline3\nline4\n";
163 | const file2 = "line1\nline2\nline5\nline4\n";
164 | const patch = Diff.structuredPatch("file1", "file2", file1, file2);
165 | expectType(patch);
166 | const reversedPatch = Diff.reversePatch(patch);
167 | expectType(reversedPatch)
168 | const verifyPatch = Diff.parsePatch(
169 |     Diff.createTwoFilesPatch("oldFile.ts", "newFile.ts", "old content", "new content", "old", "new", {
170 |         context: 1,
171 |     }),
172 | );
173 | expectType(verifyPatch)
174 | 
175 | const wordDiff = new Diff.Diff();
176 | wordDiff.equals = function(left, right, options) {
177 |     if (options.ignoreWhitespace) {
178 |         if (!options.newlineIsToken || !left.includes("\n")) {
179 |             left = left.trim();
180 |         }
181 |         if (!options.newlineIsToken || !right.includes("\n")) {
182 |             right = right.trim();
183 |         }
184 |     }
185 |     return Diff.Diff.prototype.equals.call(this, left, right, options);
186 | };
187 | 


--------------------------------------------------------------------------------
/test/convert/dmp.js:
--------------------------------------------------------------------------------
 1 | import {convertChangesToDMP} from '../../libesm/convert/dmp.js';
 2 | import {diffChars} from '../../libesm/diff/character.js';
 3 | 
 4 | import {expect} from 'chai';
 5 | 
 6 | describe('convertToDMP', function() {
 7 |   it('should output diff-match-patch format', function() {
 8 |     const diffResult = diffChars('New Value  ', 'New  ValueMoreData ');
 9 | 
10 |     expect(convertChangesToDMP(diffResult)).to.eql([[0, 'New '], [1, ' '], [0, 'Value'], [1, 'MoreData'], [0, ' '], [-1, ' ']]);
11 |   });
12 | });
13 | 


--------------------------------------------------------------------------------
/test/diff/array.js:
--------------------------------------------------------------------------------
  1 | import {diffArrays} from '../../libesm/diff/array.js';
  2 | 
  3 | import {expect} from 'chai';
  4 | 
  5 | describe('diff/array', function() {
  6 |   describe('#diffArrays', function() {
  7 |     it('Should diff arrays', function() {
  8 |       const a = {a: 0}, b = {b: 1}, c = {c: 2};
  9 |       const diffResult = diffArrays([a, b, c], [a, c, b]);
 10 |       expect(diffResult).to.deep.equals([
 11 |           {count: 1, value: [a], removed: false, added: false},
 12 |           {count: 1, value: [b], removed: true, added: false},
 13 |           {count: 1, value: [c], removed: false, added: false},
 14 |           {count: 1, value: [b], removed: false, added: true}
 15 |       ]);
 16 |     });
 17 |     it('should diff falsey values', function() {
 18 |       const a = false;
 19 |       const b = 0;
 20 |       const c = '';
 21 |       // Example sequences from Myers 1986
 22 |       const arrayA = [a, b, c, a, b, b, a];
 23 |       const arrayB = [c, b, a, b, a, c];
 24 |       const diffResult = diffArrays(arrayA, arrayB);
 25 |       expect(diffResult).to.deep.equals([
 26 |         {count: 2, value: [a, b], removed: true, added: false},
 27 |         {count: 1, value: [c], removed: false, added: false},
 28 |         {count: 1, value: [b], removed: false, added: true},
 29 |         {count: 2, value: [a, b], removed: false, added: false},
 30 |         {count: 1, value: [b], removed: true, added: false},
 31 |         {count: 1, value: [a], removed: false, added: false},
 32 |         {count: 1, value: [c], removed: false, added: true}
 33 |       ]);
 34 |     });
 35 |     describe('anti-aliasing', function() {
 36 |       // Test apparent contract that no chunk value is ever an input argument.
 37 |       const value = [0, 1, 2];
 38 |       const expected = [
 39 |         {count: value.length, value: value, removed: false, added: false}
 40 |       ];
 41 | 
 42 |       const input = value.slice();
 43 |       const diffResult = diffArrays(input, input);
 44 |       it('returns correct deep result for identical inputs', function() {
 45 |         expect(diffResult).to.deep.equals(expected);
 46 |       });
 47 |       it('does not return the input array', function() {
 48 |         expect(diffResult[0].value).to.not.equal(input);
 49 |       });
 50 | 
 51 |       const input1 = value.slice();
 52 |       const input2 = value.slice();
 53 |       const diffResult2 = diffArrays(input1, input2);
 54 |       it('returns correct deep result for equivalent inputs', function() {
 55 |         expect(diffResult2).to.deep.equals(expected);
 56 |       });
 57 |       it('does not return the first input array', function() {
 58 |         expect(diffResult2[0].value).to.not.equal(input1);
 59 |       });
 60 |       it('does not return the second input array', function() {
 61 |         expect(diffResult2[0].value).to.not.equal(input2);
 62 |       });
 63 |     });
 64 |     it('Should diff arrays with comparator', function() {
 65 |       const a = {a: 0}, b = {a: 1}, c = {a: 2}, d = {a: 3};
 66 |       function comparator(left, right) {
 67 |         return left.a === right.a;
 68 |       }
 69 |       const diffResult = diffArrays([a, b, c], [a, b, d], { comparator: comparator });
 70 |       expect(diffResult).to.deep.equals([
 71 |           {count: 2, value: [a, b], removed: false, added: false},
 72 |           {count: 1, value: [c], removed: true, added: false},
 73 |           {count: 1, value: [d], removed: false, added: true}
 74 |       ]);
 75 |     });
 76 |     it('Should pass old/new tokens as the left/right comparator args respectively', function() {
 77 |       diffArrays(
 78 |         ['a', 'b', 'c'],
 79 |         ['x', 'y', 'z'],
 80 |         {
 81 |           comparator: function(left, right) {
 82 |             expect(left).to.be.oneOf(['a', 'b', 'c']);
 83 |             expect(right).to.be.oneOf(['x', 'y', 'z']);
 84 |             return left === right;
 85 |           }
 86 |         }
 87 |       );
 88 |     });
 89 |     it('Should terminate early if execution time exceeds `timeout` ms', function() {
 90 |       // To test this, we also pass a comparator that hot sleeps as a way to
 91 |       // artificially slow down execution so we reach the timeout.
 92 |       function comparator(left, right) {
 93 |         const start = Date.now();
 94 |         // Hot-sleep for 10ms
 95 |         while (Date.now() < start + 10) {
 96 |           // Do nothing
 97 |         }
 98 |         return left === right;
 99 |       }
100 | 
101 |       // It will require 14 comparisons (140ms) to diff these arrays:
102 |       const arr1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
103 |       const arr2 = ['a', 'b', 'c', 'd', 'x', 'y', 'z'];
104 | 
105 |       // So with a timeout of 50ms, we are guaranteed failure:
106 |       expect(diffArrays(arr1, arr2, {comparator, timeout: 50})).to.be.undefined;
107 | 
108 |       // But with a longer timeout, we expect success:
109 |       expect(diffArrays(arr1, arr2, {comparator, timeout: 1000})).not.to.be.undefined;
110 |     });
111 |   });
112 | });
113 | 


--------------------------------------------------------------------------------
/test/diff/character.js:
--------------------------------------------------------------------------------
 1 | import {diffChars} from '../../libesm/diff/character.js';
 2 | import {convertChangesToXML} from '../../libesm/convert/xml.js';
 3 | 
 4 | import {expect} from 'chai';
 5 | 
 6 | describe('diff/character', function() {
 7 |   describe('#diffChars', function() {
 8 |     it('Should diff chars', function() {
 9 |       const diffResult = diffChars('Old Value.', 'New ValueMoreData.');
10 |       expect(convertChangesToXML(diffResult)).to.equal('OldNew ValueMoreData.');
11 |     });
12 | 
13 |     describe('oneChangePerToken option', function() {
14 |       it('emits one change per character', function() {
15 |         const diffResult = diffChars('Old Value.', 'New ValueMoreData.', {oneChangePerToken: true});
16 |         expect(diffResult.length).to.equal(21);
17 |         expect(convertChangesToXML(diffResult)).to.equal('OldNew ValueMoreData.');
18 |       });
19 | 
20 |       it('correctly handles the case where the texts are identical', function() {
21 |         const diffResult = diffChars('foo bar baz qux', 'foo bar baz qux', {oneChangePerToken: true});
22 |         expect(diffResult).to.deep.equal(
23 |           ['f', 'o', 'o', ' ', 'b', 'a', 'r', ' ', 'b', 'a', 'z', ' ', 'q', 'u', 'x'].map(
24 |             char => ({value: char, count: 1, added: false, removed: false})
25 |           )
26 |         );
27 |       });
28 |     });
29 | 
30 |     it('should treat a code point that consists of two UTF-16 code units as a single character, not two', function() {
31 |       const diffResult = diffChars('𝟘𝟙𝟚𝟛', '𝟘𝟙𝟚𝟜𝟝𝟞');
32 |       expect(diffResult.length).to.equal(3);
33 |       expect(diffResult[2].count).to.equal(3);
34 |       expect(convertChangesToXML(diffResult)).to.equal('𝟘𝟙𝟚𝟛𝟜𝟝𝟞');
35 |     });
36 | 
37 |     describe('case insensitivity', function() {
38 |       it("is considered when there's no difference", function() {
39 |         const diffResult = diffChars('New Value.', 'New value.', {ignoreCase: true});
40 |         expect(convertChangesToXML(diffResult)).to.equal('New value.');
41 |       });
42 | 
43 |       it("is considered when there's a difference", function() {
44 |         const diffResult = diffChars('New Values.', 'New value.', {ignoreCase: true});
45 |         expect(convertChangesToXML(diffResult)).to.equal('New values.');
46 |       });
47 |     });
48 | 
49 |     it('should not be susceptible to race conditions in async mode when called with different options', function(done) {
50 |       // (regression test for https://github.com/kpdecker/jsdiff/issues/477)
51 |       diffChars('wibblywobbly', 'WIBBLYWOBBLY', {ignoreCase: false, callback: (diffResult) => {
52 |         expect(convertChangesToXML(diffResult)).to.equal('wibblywobblyWIBBLYWOBBLY');
53 |         done();
54 |       }});
55 | 
56 |       // Historically, doing this while async execution of the previous
57 |       // diffChars call was ongoing would overwrite this.options and make the
58 |       // ongoing diff become case-insensitive partway through execution.
59 |       diffChars('whatever', 'whatever', {ignoreCase: true});
60 |       diffChars('whatever', 'whatever', {ignoreCase: true, callback: () => {}});
61 |     });
62 | 
63 |     it('should return undefined when called in async mode', function() {
64 |       expect(diffChars('whatever', 'whatever', {callback: () => {}})).to.be.undefined;
65 |       expect(diffChars('whatever', 'whatever else', {callback: () => {}})).to.be.undefined;
66 |     });
67 |   });
68 | });
69 | 


--------------------------------------------------------------------------------
/test/diff/css.js:
--------------------------------------------------------------------------------
 1 | import {diffCss} from '../../libesm/diff/css.js';
 2 | import {convertChangesToXML} from '../../libesm/convert/xml.js';
 3 | 
 4 | import {expect} from 'chai';
 5 | 
 6 | describe('diff/css', function() {
 7 |   describe('#diffCss', function() {
 8 |     it('should diff css', function() {
 9 |       const diffResult = diffCss(
10 |         '.test,#value .test{margin-left:50px;margin-right:-40px}',
11 |         '.test2, #value2 .test {\nmargin-top:50px;\nmargin-right:-400px;\n}');
12 |       expect(convertChangesToXML(diffResult)).to.equal(
13 |         '.test.test2,#value #value2 .test {'
14 |         + 'margin-left\nmargin-top:50px;\n'
15 |         + 'margin-right:-40px-400px;\n}');
16 |     });
17 |   });
18 | });
19 | 


--------------------------------------------------------------------------------
/test/diff/json.js:
--------------------------------------------------------------------------------
  1 | import {diffJson, canonicalize} from '../../libesm/diff/json.js';
  2 | import {convertChangesToXML} from '../../libesm/convert/xml.js';
  3 | 
  4 | import {expect} from 'chai';
  5 | 
  6 | describe('diff/json', function() {
  7 |   describe('#diffJson', function() {
  8 |     it('should accept objects', function() {
  9 |       expect(diffJson(
 10 |         {a: 123, b: 456, c: 789},
 11 |         {a: 123, b: 456}
 12 |       )).to.eql([
 13 |         { count: 3, value: '{\n  "a": 123,\n  "b": 456,\n', removed: false, added: false },
 14 |         { count: 1, value: '  "c": 789\n', added: false, removed: true },
 15 |         { count: 1, value: '}', removed: false, added: false }
 16 |       ]);
 17 |     });
 18 | 
 19 |     it('should accept objects with different order', function() {
 20 |       expect(diffJson(
 21 |         {a: 123, b: 456, c: 789},
 22 |         {b: 456, a: 123}
 23 |       )).to.eql([
 24 |         { count: 3, value: '{\n  "a": 123,\n  "b": 456,\n', removed: false, added: false },
 25 |         { count: 1, value: '  "c": 789\n', added: false, removed: true },
 26 |         { count: 1, value: '}', removed: false, added: false }
 27 |       ]);
 28 |     });
 29 | 
 30 |     it('should accept objects with nested structures', function() {
 31 |       expect(diffJson(
 32 |         {a: 123, b: 456, c: [1, 2, {foo: 'bar'}, 4]},
 33 |         {a: 123, b: 456, c: [1, {foo: 'bar'}, 4]}
 34 |       )).to.eql([
 35 |         { count: 5, value: '{\n  "a": 123,\n  "b": 456,\n  "c": [\n    1,\n', removed: false, added: false },
 36 |         { count: 1, value: '    2,\n', added: false, removed: true },
 37 |         { count: 6, value: '    {\n      "foo": "bar"\n    },\n    4\n  ]\n}', removed: false, added: false }
 38 |       ]);
 39 |     });
 40 | 
 41 |     it('should accept dates', function() {
 42 |       expect(diffJson(
 43 |         {a: new Date(123), b: new Date(456), c: new Date(789)},
 44 |         {a: new Date(124), b: new Date(456)}
 45 |       )).to.eql([
 46 |         { count: 1, value: '{\n', removed: false, added: false },
 47 |         { count: 1, value: '  "a": "1970-01-01T00:00:00.123Z",\n', added: false, removed: true },
 48 |         { count: 1, value: '  "a": "1970-01-01T00:00:00.124Z",\n', added: true, removed: false },
 49 |         { count: 1, value: '  "b": "1970-01-01T00:00:00.456Z",\n', removed: false, added: false },
 50 |         { count: 1, value: '  "c": "1970-01-01T00:00:00.789Z"\n', added: false, removed: true },
 51 |         { count: 1, value: '}', removed: false, added: false }
 52 |       ]);
 53 |     });
 54 | 
 55 |     it('should accept undefined keys', function() {
 56 |       expect(diffJson(
 57 |         {a: 123, b: 456, c: null},
 58 |         {a: 123, b: 456}
 59 |       )).to.eql([
 60 |         { count: 3, value: '{\n  "a": 123,\n  "b": 456,\n', removed: false, added: false },
 61 |         { count: 1, value: '  "c": null\n', added: false, removed: true },
 62 |         { count: 1, value: '}', removed: false, added: false }
 63 |       ]);
 64 |       expect(diffJson(
 65 |         {a: 123, b: 456, c: undefined},
 66 |         {a: 123, b: 456}
 67 |       )).to.eql([
 68 |         { count: 4, value: '{\n  "a": 123,\n  "b": 456\n}', removed: false, added: false }
 69 |       ]);
 70 |       expect(diffJson(
 71 |         {a: 123, b: 456, c: undefined},
 72 |         {a: 123, b: 456},
 73 |         {undefinedReplacement: null}
 74 |       )).to.eql([
 75 |         { count: 3, value: '{\n  "a": 123,\n  "b": 456,\n', removed: false, added: false },
 76 |         { count: 1, value: '  "c": null\n', added: false, removed: true },
 77 |         { count: 1, value: '}', removed: false, added: false }
 78 |       ]);
 79 |     });
 80 | 
 81 |     it('should accept already stringified JSON', function() {
 82 |       expect(diffJson(
 83 |         JSON.stringify({a: 123, b: 456, c: 789}, undefined, '  '),
 84 |         JSON.stringify({a: 123, b: 456}, undefined, '  ')
 85 |       )).to.eql([
 86 |         { count: 3, value: '{\n  "a": 123,\n  "b": 456,\n', removed: false, added: false },
 87 |         { count: 1, value: '  "c": 789\n', added: false, removed: true },
 88 |         { count: 1, value: '}', removed: false, added: false }
 89 |       ]);
 90 |     });
 91 | 
 92 |     it('should ignore trailing comma on the previous line when the property has been removed', function() {
 93 |       const diffResult = diffJson(
 94 |         {a: 123, b: 456, c: 789},
 95 |         {a: 123, b: 456});
 96 |       expect(convertChangesToXML(diffResult)).to.equal('{\n  "a": 123,\n  "b": 456,\n  "c": 789\n}');
 97 |     });
 98 | 
 99 |     it('should ignore the missing trailing comma on the last line when a property has been added after it', function() {
100 |       const diffResult = diffJson(
101 |         {a: 123, b: 456},
102 |         {a: 123, b: 456, c: 789});
103 |       expect(convertChangesToXML(diffResult)).to.equal('{\n  "a": 123,\n  "b": 456,\n  "c": 789\n}');
104 |     });
105 | 
106 |     it('should throw an error if one of the objects being diffed has a circular reference', function() {
107 |       const circular = {foo: 123};
108 |       circular.bar = circular;
109 |       expect(function() {
110 |         diffJson(
111 |           circular,
112 |           {foo: 123, bar: {}}
113 |         );
114 |       }).to['throw'](/circular|cyclic/i);
115 |     });
116 |   });
117 | 
118 |   describe('#canonicalize', function() {
119 |     it('should put the keys in canonical order', function() {
120 |       expect(Object.keys(canonicalize({b: 456, a: 123}))).to.eql(['a', 'b']);
121 |     });
122 | 
123 |     it('should dive into nested objects', function() {
124 |       const canonicalObj = canonicalize({b: 456, a: {d: 123, c: 456}});
125 |       expect(Object.keys(canonicalObj.a)).to.eql(['c', 'd']);
126 |     });
127 | 
128 |     it('should dive into nested arrays', function() {
129 |       const canonicalObj = canonicalize({b: 456, a: [789, {d: 123, c: 456}]});
130 |       expect(Object.keys(canonicalObj.a[1])).to.eql(['c', 'd']);
131 |     });
132 | 
133 |     it('should handle circular references correctly', function() {
134 |       const obj = {b: 456};
135 |       obj.a = obj;
136 |       const canonicalObj = canonicalize(obj);
137 |       expect(Object.keys(canonicalObj)).to.eql(['a', 'b']);
138 |       expect(Object.keys(canonicalObj.a)).to.eql(['a', 'b']);
139 |     });
140 | 
141 |     it('should accept a custom JSON.stringify() replacer function', function() {
142 |       expect(diffJson(
143 |         {a: 123},
144 |         {a: /foo/}
145 |       )).to.eql([
146 |         { count: 1, value: '{\n', removed: false, added: false },
147 |         { count: 1, value: '  "a": 123\n', added: false, removed: true },
148 |         { count: 1, value: '  "a": {}\n', added: true, removed: false },
149 |         { count: 1, value: '}', removed: false, added: false }
150 |       ]);
151 | 
152 |       expect(diffJson(
153 |         {a: 123},
154 |         {a: /foo/gi},
155 |         {stringifyReplacer: (k, v) => v instanceof RegExp ? v.toString() : v}
156 |       )).to.eql([
157 |         { count: 1, value: '{\n', removed: false, added: false },
158 |         { count: 1, value: '  "a": 123\n', added: false, removed: true },
159 |         { count: 1, value: '  "a": "/foo/gi"\n', added: true, removed: false },
160 |         { count: 1, value: '}', removed: false, added: false }
161 |       ]);
162 | 
163 |       expect(diffJson(
164 |         {a: 123},
165 |         {a: new Error('ohaider')},
166 |         {stringifyReplacer: (k, v) => v instanceof Error ? `${v.name}: ${v.message}` : v}
167 |       )).to.eql([
168 |         { count: 1, value: '{\n', removed: false, added: false },
169 |         { count: 1, value: '  "a": 123\n', added: false, removed: true },
170 |         { count: 1, value: '  "a": "Error: ohaider"\n', added: true, removed: false },
171 |         { count: 1, value: '}', removed: false, added: false }
172 |       ]);
173 | 
174 |       expect(diffJson(
175 |         {a: 123},
176 |         {a: [new Error('ohaider')]},
177 |         {stringifyReplacer: (k, v) => v instanceof Error ? `${v.name}: ${v.message}` : v}
178 |       )).to.eql([
179 |         { count: 1, value: '{\n', removed: false, added: false },
180 |         { count: 1, value: '  "a": 123\n', added: false, removed: true },
181 |         { count: 3, value: '  "a": [\n    "Error: ohaider"\n  ]\n', added: true, removed: false },
182 |         { count: 1, value: '}', removed: false, added: false }
183 |       ]);
184 |     });
185 | 
186 |     it('should only run each value through stringifyReplacer once', function() {
187 |       expect(
188 |         diffJson(
189 |           {foo: '123ab'},
190 |           {foo: '123xy'},
191 |           {stringifyReplacer: (k, v) => typeof v === 'string' ? v.slice(0, v.length - 1) : v}
192 |         )
193 |       ).to.deep.equal(
194 |         [
195 |           { count: 1, value: '{\n', removed: false, added: false },
196 |           { count: 1, value: '  "foo": "123a"\n', added: false, removed: true },
197 |           { count: 1, value: '  "foo": "123x"\n', added: true, removed: false },
198 |           { count: 1, value: '}', removed: false, added: false }
199 |         ]
200 |       );
201 |     });
202 | 
203 |     it("should pass the same 'key' values to the replacer as JSON.stringify would", function() {
204 |       const calls = [],
205 |             obj1 = {a: ['q', 'r', 's', {t: []}]},
206 |             obj2 = {a: ['x', 'y', 'z', {bla: []}]};
207 |       diffJson(
208 |         obj1,
209 |         obj2,
210 |         {stringifyReplacer: (k, v) => {
211 |           calls.push([k, v]);
212 |           return v;
213 |         }}
214 |       );
215 | 
216 |       // We run the same objects through JSON.stringify just to make unambiguous when reading this
217 |       // test that we're checking for the same key/value pairs that JSON.stringify would pass to
218 |       // the replacer.
219 |       const jsonStringifyCalls = [];
220 |       JSON.stringify(
221 |         obj1,
222 |         (k, v) => {
223 |           jsonStringifyCalls.push([k, v]);
224 |           return v;
225 |         }
226 |       );
227 |       JSON.stringify(
228 |         obj2,
229 |         (k, v) => {
230 |           jsonStringifyCalls.push([k, v]);
231 |           return v;
232 |         }
233 |       );
234 | 
235 |       expect(jsonStringifyCalls).to.deep.equal([
236 |         ['', {a: ['q', 'r', 's', {t: []}]}],
237 |         ['a', ['q', 'r', 's', {t: []}]],
238 |         ['0', 'q'],
239 |         ['1', 'r'],
240 |         ['2', 's'],
241 |         ['3', {t: []}],
242 |         ['t', []],
243 |         ['', {a: ['x', 'y', 'z', {bla: []}]}],
244 |         ['a', ['x', 'y', 'z', {bla: []}]],
245 |         ['0', 'x'],
246 |         ['1', 'y'],
247 |         ['2', 'z'],
248 |         ['3', {bla: []}],
249 |         ['bla', []]
250 |       ]);
251 | 
252 |       expect(calls).to.deep.equal(jsonStringifyCalls);
253 |     });
254 | 
255 |     it("doesn't throw on Object.create(null)", function() {
256 |       let diff;
257 |       expect(function() {
258 |         diff = diffJson(
259 |           Object.assign(Object.create(null), {a: 123}),
260 |           {b: 456}
261 |         );
262 |       }).not.to['throw']();
263 |       expect(diff).to.eql([
264 |         { count: 1, value: '{\n', removed: false, added: false },
265 |         { count: 1, value: '  "a": 123\n', removed: true, added: false },
266 |         { count: 1, value: '  "b": 456\n', removed: false, added: true },
267 |         { count: 1, value: '}', removed: false, added: false }
268 |       ]);
269 |     });
270 |   });
271 | });
272 | 


--------------------------------------------------------------------------------
/test/diff/line.js:
--------------------------------------------------------------------------------
  1 | import {diffLines, diffTrimmedLines} from '../../libesm/diff/line.js';
  2 | import {convertChangesToXML} from '../../libesm/convert/xml.js';
  3 | 
  4 | import {expect} from 'chai';
  5 | 
  6 | describe('diff/line', function() {
  7 |   // Line Diff
  8 |   describe('#diffLines', function() {
  9 |     it('should diff lines', function() {
 10 |       const diffResult = diffLines(
 11 |         'line\nold value\nline',
 12 |         'line\nnew value\nline');
 13 |       expect(convertChangesToXML(diffResult)).to.equal('line\nold value\nnew value\nline');
 14 |     });
 15 |     it('should treat identical lines as equal', function() {
 16 |       const diffResult = diffLines(
 17 |         'line\nvalue\nline',
 18 |         'line\nvalue\nline');
 19 |       expect(convertChangesToXML(diffResult)).to.equal('line\nvalue\nline');
 20 |     });
 21 | 
 22 |     it('should handle leading and trailing whitespace', function() {
 23 |       const diffResult = diffLines(
 24 |         'line\nvalue \nline',
 25 |         'line\nvalue\nline');
 26 |       expect(convertChangesToXML(diffResult)).to.equal('line\nvalue \nvalue\nline');
 27 |     });
 28 | 
 29 |     it('should handle windows line endings', function() {
 30 |       const diffResult = diffLines(
 31 |         'line\r\nold value \r\nline',
 32 |         'line\r\nnew value\r\nline');
 33 |       expect(convertChangesToXML(diffResult)).to.equal('line\r\nold value \r\nnew value\r\nline');
 34 |     });
 35 | 
 36 |     it('should handle empty lines', function() {
 37 |       const diffResult = diffLines(
 38 |         'line\n\nold value \n\nline',
 39 |         'line\n\nnew value\n\nline');
 40 |       expect(convertChangesToXML(diffResult)).to.equal('line\n\nold value \nnew value\n\nline');
 41 |     });
 42 | 
 43 |     it('should handle empty input', function() {
 44 |       const diffResult = diffLines(
 45 |         'line\n\nold value \n\nline',
 46 |         '');
 47 |       expect(convertChangesToXML(diffResult)).to.equal('line\n\nold value \n\nline');
 48 |     });
 49 | 
 50 |     it('Should prefer to do deletions before insertions, like Unix diff does', function() {
 51 |       const diffResult = diffLines('a\nb\nc\nd\n', 'a\nc\nb\nd\n');
 52 | 
 53 |       // There are two possible diffs with equal edit distance here; either we
 54 |       // can delete the "b" and insert it again later, or we can insert a "c"
 55 |       // before the "b" and then delete the original "c" later.
 56 |       // For consistency with the convention of other diff tools, we want to
 57 |       // prefer the diff where we delete and then later insert over the one
 58 |       // where we insert and then later delete.
 59 |       expect(convertChangesToXML(diffResult)).to.equal('a\nb\nc\nb\nd\n');
 60 | 
 61 |       const diffResult2 = diffLines('a\nc\nb\nd\n', 'a\nb\nc\nd\n');
 62 |       expect(convertChangesToXML(diffResult2)).to.equal('a\nc\nb\nc\nd\n');
 63 |     });
 64 | 
 65 |     describe('given options.maxEditLength', function() {
 66 |       it('terminates early', function() {
 67 |         const diffResult = diffLines(
 68 |           'line\nold value\nline',
 69 |           'line\nnew value\nline', { maxEditLength: 1 });
 70 |         expect(diffResult).to.be.undefined;
 71 |       });
 72 |       it('terminates early - async', function(done) {
 73 |         function callback(diffResult) {
 74 |           expect(diffResult).to.be.undefined;
 75 |           done();
 76 |         }
 77 |         diffLines(
 78 |           'line\nold value\nline',
 79 |           'line\nnew value\nline', { callback, maxEditLength: 1 });
 80 |       });
 81 |     });
 82 | 
 83 |     describe('given options.maxEditLength === 0', function() {
 84 |       it('returns normally if the strings are identical', function() {
 85 |         const diffResult = diffLines(
 86 |           'foo\nbar\nbaz\nqux\n',
 87 |           'foo\nbar\nbaz\nqux\n',
 88 |           { maxEditLength: 0 }
 89 |         );
 90 |         expect(convertChangesToXML(diffResult)).to.equal('foo\nbar\nbaz\nqux\n');
 91 |       });
 92 | 
 93 |       it('terminates early if there is even a single change', function() {
 94 |         const diffResult = diffLines(
 95 |           'foo\nbar\nbaz\nqux\n',
 96 |           'fox\nbar\nbaz\nqux\n',
 97 |           { maxEditLength: 0 }
 98 |         );
 99 |         expect(diffResult).to.be.undefined;
100 |       });
101 |     });
102 |   });
103 | 
104 |   describe('oneChangePerToken option', function() {
105 |     it('emits one change per line', function() {
106 |       const diffResult = diffLines(
107 |         'foo\nbar\nbaz\nqux\n',
108 |         'fox\nbar\nbaz\nqux\n',
109 |         { oneChangePerToken: true }
110 |       );
111 |       expect(diffResult).to.deep.equal(
112 |         [
113 |           {value: 'foo\n', count: 1, added: false, removed: true},
114 |           {value: 'fox\n', count: 1, added: true, removed: false},
115 |           {value: 'bar\n', count: 1, added: false, removed: false},
116 |           {value: 'baz\n', count: 1, added: false, removed: false},
117 |           {value: 'qux\n', count: 1, added: false, removed: false}
118 |         ]
119 |       );
120 |     });
121 |   });
122 | 
123 |   // Trimmed Line Diff
124 |   describe('#TrimmedLineDiff', function() {
125 |     it('should diff lines', function() {
126 |       const diffResult = diffTrimmedLines(
127 |         'line\nold value\nline',
128 |         'line\nnew value\nline');
129 |       expect(convertChangesToXML(diffResult)).to.equal('line\nold value\nnew value\nline');
130 |     });
131 |     it('should treat identical lines as equal', function() {
132 |       const diffResult = diffTrimmedLines(
133 |         'line\nvalue\nline',
134 |         'line\nvalue\nline');
135 |       expect(convertChangesToXML(diffResult)).to.equal('line\nvalue\nline');
136 |     });
137 | 
138 |     it('should ignore leading and trailing whitespace', function() {
139 |       const diffResult1 = diffTrimmedLines(
140 |         'line\nvalue \nline',
141 |         'line\nvalue\nline');
142 |       expect(convertChangesToXML(diffResult1)).to.equal('line\nvalue\nline');
143 | 
144 |       const diffResult2 = diffTrimmedLines(
145 |         'line\nvalue\nline',
146 |         'line\nvalue \nline');
147 |       expect(convertChangesToXML(diffResult2)).to.equal('line\nvalue \nline');
148 | 
149 |       const diffResult3 = diffTrimmedLines(
150 |         'line\n value\nline',
151 |         'line\nvalue\nline');
152 |       expect(convertChangesToXML(diffResult3)).to.equal('line\nvalue\nline');
153 | 
154 |       const diffResult4 = diffTrimmedLines(
155 |         'line\nvalue\nline',
156 |         'line\n value\nline');
157 |       expect(convertChangesToXML(diffResult4)).to.equal('line\n value\nline');
158 |     });
159 | 
160 |     it('should not ignore the insertion or deletion of lines of whitespace at the end', function() {
161 |       const finalChange = diffLines('foo\nbar\n', 'foo\nbar\n  \n  \n  \n', {ignoreWhitespace: true}).pop();
162 |       expect(finalChange.count).to.equal(3);
163 |       expect(finalChange.added).to.equal(true);
164 | 
165 |       const finalChange2 = diffLines('foo\nbar\n\n', 'foo\nbar\n', {ignoreWhitespace: true}).pop();
166 |       expect(finalChange2.removed).to.equal(true);
167 |     });
168 | 
169 |     it('should keep leading and trailing whitespace in the output', function() {
170 |       function stringify(value) {
171 |         return JSON.stringify(value, null, 2);
172 |       }
173 |       const diffResult = diffTrimmedLines(
174 |       stringify([10, 20, 30]),
175 |       stringify({ data: [10, 42, 30] }));
176 |       expect(diffResult.filter(change => !change.removed).map(change => change.value).join('')).to.equal(`{
177 |   "data": [
178 |     10,
179 |     42,
180 |     30
181 |   ]
182 | }`);
183 |       expect(convertChangesToXML(diffResult)).to.equal([
184 |           '[\n',
185 |           '{\n',
186 |           '  "data": [\n',
187 |           '    10,\n',
188 |           '  20,\n',
189 |           '    42,\n',
190 |           '    30\n',
191 |           '  ]\n',
192 |           '}'
193 |       ].join('').replace(/"/g, '"'));
194 |     });
195 | 
196 |     it('should not consider adding whitespace to an empty line an insertion', function() {
197 |       const diffResult = diffTrimmedLines('foo\n\nbar', 'foo\n \nbar');
198 |       expect(convertChangesToXML(diffResult)).to.equal('foo\n \nbar');
199 |     });
200 | 
201 |     it('should handle windows line endings', function() {
202 |       const diffResult = diffTrimmedLines(
203 |         'line\r\nold value \r\nline',
204 |         'line\r\nnew value\r\nline');
205 |       expect(convertChangesToXML(diffResult)).to.equal('line\r\nold value \r\nnew value\r\nline');
206 |     });
207 | 
208 |     it('should be compatible with newlineIsToken', function() {
209 |       const diffResult = diffTrimmedLines(
210 |         'line1\nline2\n   \nline4\n \n',
211 |         'line1\nline2\n\n\nline4\n   \n',
212 |         {newlineIsToken: true}
213 |       );
214 |       expect(convertChangesToXML(diffResult)).to.equal('line1\nline2\n   \n\nline4\n   \n');
215 |     });
216 | 
217 |     it('supports async mode by passing a function as the options argument', function(done) {
218 |       diffTrimmedLines(
219 |         'line\r\nold value \r\nline',
220 |         'line \r\nnew value\r\nline',
221 |         function(diffResult) {
222 |           expect(convertChangesToXML(diffResult)).to.equal(
223 |             'line \r\nold value \r\nnew value\r\nline'
224 |           );
225 |           done();
226 |         }
227 |       );
228 |     });
229 |   });
230 | 
231 |   describe('#diffLinesNL', function() {
232 |     expect(diffLines('restaurant', 'restaurant\n', {newlineIsToken: true})).to.eql([
233 |       {value: 'restaurant', count: 1, added: false, removed: false},
234 |       {value: '\n', count: 1, added: true, removed: false}
235 |     ]);
236 |     expect(diffLines('restaurant', 'restaurant\nhello', {newlineIsToken: true})).to.eql([
237 |       {value: 'restaurant', count: 1, added: false, removed: false},
238 |       {value: '\nhello', count: 2, added: true, removed: false}
239 |     ]);
240 |   });
241 | 
242 |   describe('Strip trailing CR', function() {
243 |     expect(diffLines('line\nline', 'line\r\nline', {stripTrailingCr: true})).to.eql([
244 |       {value: 'line\nline', count: 2, added: false, removed: false}
245 |     ]);
246 |   });
247 | 
248 |   it('ignores absence of newline on the last line of file wrt equality when ignoreNewlineAtEof', function() {
249 |     // Example taken directly from https://github.com/kpdecker/jsdiff/issues/324
250 |     expect(diffLines('a\nb\nc', 'a\nb')).to.eql(
251 |       [
252 |         { count: 1, added: false, removed: false, value: 'a\n' },
253 |         { count: 2, added: false, removed: true, value: 'b\nc' },
254 |         { count: 1, added: true, removed: false, value: 'b' }
255 |       ]
256 |     );
257 |     expect(diffLines('a\nb\nc', 'a\nb', { ignoreNewlineAtEof: true })).to.eql(
258 |       [
259 |         { count: 2, added: false, removed: false, value: 'a\nb' },
260 |         { count: 1, added: false, removed: true, value: 'c' }
261 |       ]
262 |     );
263 |     expect(diffLines('a\nb', 'a\nb\nc', { ignoreNewlineAtEof: true })).to.eql(
264 |       [
265 |         { count: 2, added: false, removed: false, value: 'a\nb\n' },
266 |         { count: 1, added: true, removed: false, value: 'c' }
267 |       ]
268 |     );
269 |   });
270 | });
271 | 


--------------------------------------------------------------------------------
/test/diff/sentence.js:
--------------------------------------------------------------------------------
 1 | import {diffSentences, sentenceDiff} from '../../libesm/diff/sentence.js';
 2 | import {convertChangesToXML} from '../../libesm/convert/xml.js';
 3 | 
 4 | import {expect} from 'chai';
 5 | 
 6 | describe('diff/sentence', function() {
 7 |   describe('tokenize', function() {
 8 |     it('should split on whitespace after a punctuation mark, and keep the whitespace as a token', function() {
 9 |       expect(sentenceDiff.removeEmpty(sentenceDiff.tokenize(''))).to.eql([]);
10 | 
11 |       expect(sentenceDiff.removeEmpty(sentenceDiff.tokenize(
12 |           'Foo bar baz! Qux wibbly wobbly bla? \n\tYayayaya!Yayayaya!Ya! Yes!!!!! Blub'
13 |       ))).to.eql([
14 |         'Foo bar baz!',
15 |         ' ',
16 |         'Qux wibbly wobbly bla?',
17 |         ' \n\t',
18 |         'Yayayaya!Yayayaya!Ya!',
19 |         ' ',
20 |         'Yes!!!!!',
21 |         ' ',
22 |         'Blub'
23 |       ]);
24 | 
25 |       expect(sentenceDiff.removeEmpty(sentenceDiff.tokenize(
26 |         '! Hello there.'
27 |       ))).to.eql([
28 |         '!',
29 |         ' ',
30 |         'Hello there.'
31 |       ]);
32 | 
33 |       expect(sentenceDiff.removeEmpty(sentenceDiff.tokenize(
34 |         '    foo bar baz.'
35 |       ))).to.eql([
36 |         '    foo bar baz.'
37 |       ]);
38 |     });
39 |   });
40 | 
41 |   describe('#diffSentences', function() {
42 |     it('Should diff Sentences', function() {
43 |       const diffResult = diffSentences('New Value.', 'New ValueMoreData.');
44 |       expect(convertChangesToXML(diffResult)).to.equal('New Value.New ValueMoreData.');
45 |     });
46 | 
47 |     it('should diff only the last sentence', function() {
48 |       const diffResult = diffSentences('Here im. Rock you like old man.', 'Here im. Rock you like hurricane.');
49 |       expect(convertChangesToXML(diffResult)).to.equal('Here im. Rock you like old man.Rock you like hurricane.');
50 |     });
51 |   });
52 | });
53 | 


--------------------------------------------------------------------------------
/test/diff/word.js:
--------------------------------------------------------------------------------
  1 | import {wordDiff, diffWords, diffWordsWithSpace} from '../../libesm/diff/word.js';
  2 | import {convertChangesToXML} from '../../libesm/convert/xml.js';
  3 | 
  4 | import {expect} from 'chai';
  5 | 
  6 | describe('WordDiff', function() {
  7 |   describe('#tokenize', function() {
  8 |     it('should give each word & punctuation mark its own token, including leading and trailing whitespace', function() {
  9 |       expect(
 10 |         wordDiff.tokenize(
 11 |           'foo bar baz jurídica wir üben    bla\t\t \txyzáxyz  \n\n\n  animá-los\r\n\r\n(wibbly wobbly)().'
 12 |         )
 13 |       ).to.deep.equal([
 14 |         'foo ',
 15 |         ' bar ',
 16 |         ' baz ',
 17 |         ' jurídica ',
 18 |         ' wir ',
 19 |         ' üben    ',
 20 |         '    bla\t\t \t',
 21 |         '\t\t \txyzáxyz  \n\n\n  ',
 22 |         '  \n\n\n  animá',
 23 |         '-',
 24 |         'los\r\n\r\n',
 25 |         '\r\n\r\n(',
 26 |         'wibbly ',
 27 |         ' wobbly',
 28 |         ')',
 29 |         '(',
 30 |         ')',
 31 |         '.'
 32 |       ]);
 33 |     });
 34 | 
 35 |     // Test for bug reported at https://github.com/kpdecker/jsdiff/issues/553
 36 |     it('should treat numbers as part of a word if not separated by whitespace or punctuation', () => {
 37 |       expect(
 38 |         wordDiff.tokenize(
 39 |           'Tea Too, also known as T2, had revenue of 57m AUD in 2012-13.'
 40 |         )
 41 |       ).to.deep.equal([
 42 |         'Tea ',
 43 |         ' Too',
 44 |         ', ',
 45 |         ' also ',
 46 |         ' known ',
 47 |         ' as ',
 48 |         ' T2',
 49 |         ', ',
 50 |         ' had ',
 51 |         ' revenue ',
 52 |         ' of ',
 53 |         ' 57m ',
 54 |         ' AUD ',
 55 |         ' in ',
 56 |         ' 2012',
 57 |         '-',
 58 |         '13',
 59 |         '.'
 60 |       ]);
 61 |     });
 62 |   });
 63 | 
 64 |   describe('#diffWords', function() {
 65 |     it("should ignore whitespace changes between tokens that aren't added or deleted", function() {
 66 |       const diffResult = diffWords('New    Value', 'New \n \t Value');
 67 |       expect(convertChangesToXML(diffResult)).to.equal('New \n \t Value');
 68 |     });
 69 | 
 70 |     describe('whitespace changes that border inserted/deleted tokens should be included in the diff as far as is possible...', function() {
 71 |       it('(add+del at end of text)', function() {
 72 |         const diffResult = diffWords('New Value  ', 'New  ValueMoreData ');
 73 |         expect(convertChangesToXML(diffResult)).to.equal('New Value   ValueMoreData ');
 74 |       });
 75 | 
 76 |       it('(add+del in middle of text)', function() {
 77 |         const diffResult = diffWords('New Value End', 'New  ValueMoreData End');
 78 |         expect(convertChangesToXML(diffResult)).to.equal('New Value ValueMoreData End');
 79 |       });
 80 | 
 81 |       it('(add+del at start of text)', function() {
 82 |         const diffResult = diffWords('\tValue End', ' ValueMoreData   End');
 83 |         expect(convertChangesToXML(diffResult)).to.equal('\tValue ValueMoreData   End');
 84 |       });
 85 | 
 86 |       it('(add at start of text)', function() {
 87 |         const diffResult = diffWords('\t Value', 'More  Value');
 88 |         // Preferable would be:
 89 |         // 'More  Value'
 90 |         // But this is hard to achieve without adding something like the
 91 |         // .oldValue property I contemplate in
 92 |         // https://github.com/kpdecker/jsdiff/pull/219#issuecomment-1858246490
 93 |         // to change objects returned by the base diffing algorithm. The CO
 94 |         // cleanup done by diffWords simply doesn't have enough information to
 95 |         // return the ideal result otherwise.
 96 |         expect(convertChangesToXML(diffResult)).to.equal('More  Value');
 97 |       });
 98 | 
 99 |       it('(del at start of text)', function() {
100 |         const diffResult = diffWords('More  Value', '\t Value');
101 |         expect(convertChangesToXML(diffResult)).to.equal('More  \t Value');
102 |       });
103 | 
104 |       it('(add in middle of text)', function() {
105 |         const diffResult = diffWords('Even Value', 'Even    More    Value');
106 |         // Preferable would be:
107 |         // 'Even    More    Value'
108 |         // But this is hard to achieve without adding something like the
109 |         // .oldValue property I contemplate in
110 |         // https://github.com/kpdecker/jsdiff/pull/219#issuecomment-1858246490
111 |         // to change objects returned by the base diffing algorithm. The CO
112 |         // cleanup done by diffWords simply doesn't have enough information to
113 |         // return the ideal result otherwise.
114 |         expect(convertChangesToXML(diffResult)).to.equal('Even    More    Value');
115 |       });
116 | 
117 |       it('(del in middle of text)', function() {
118 |         const diffResult = diffWords('Even    More    Value', 'Even Value');
119 |         expect(convertChangesToXML(diffResult)).to.equal('Even    More    Value');
120 | 
121 |         // Rules around how to split up the whitespace between the start and
122 |         // end "keep" change objects are subtle, as shown by the three examples
123 |         // below:
124 |         const diffResult2 = diffWords('foo\nbar baz', 'foo baz');
125 |         expect(convertChangesToXML(diffResult2)).to.equal('foo\nbar baz');
126 | 
127 |         const diffResult3 = diffWords('foo bar baz', 'foo baz');
128 |         expect(convertChangesToXML(diffResult3)).to.equal('foo bar baz');
129 | 
130 |         const diffResult4 = diffWords('foo\nbar baz', 'foo\n baz');
131 |         expect(convertChangesToXML(diffResult4)).to.equal('foo\nbar baz');
132 |       });
133 | 
134 |       it('(add at end of text)', function() {
135 |         const diffResult = diffWords('Foo\n', 'Foo Bar\n');
136 |         // Preferable would be:
137 |         // 'Foo Bar\n'
138 |         // But this is hard to achieve without adding something like the
139 |         // .oldValue property I contemplate in
140 |         // https://github.com/kpdecker/jsdiff/pull/219#issuecomment-1858246490
141 |         // to change objects returned by the base diffing algorithm. The CO
142 |         // cleanup done by diffWords simply doesn't have enough information to
143 |         // return the ideal result otherwise.
144 |         expect(convertChangesToXML(diffResult)).to.equal('Foo Bar\n');
145 |       });
146 | 
147 |       it('(del at end of text)', function() {
148 |         const diffResult = diffWords('Foo   Bar', 'Foo ');
149 |         expect(convertChangesToXML(diffResult)).to.equal('Foo   Bar');
150 |       });
151 |     });
152 | 
153 |     it('should skip postprocessing of change objects in one-change-object-per-token mode', function() {
154 |       const diffResult = diffWords('Foo Bar', 'Foo Baz', {oneChangePerToken: true});
155 |       expect(convertChangesToXML(diffResult)).to.equal(
156 |         'Foo  Bar Baz'
157 |       );
158 |     });
159 | 
160 |     it('should respect options.ignoreCase', function() {
161 |       const diffResult = diffWords('foo bar baz', 'FOO BAR QUX', {ignoreCase: true});
162 |       expect(convertChangesToXML(diffResult)).to.equal(
163 |         'FOO BAR bazQUX'
164 |       );
165 |     });
166 | 
167 |     it('should treat punctuation characters as tokens', function() {
168 |       let diffResult = diffWords('New:Value:Test', 'New,Value,More,Data ');
169 |       expect(convertChangesToXML(diffResult)).to.equal('New:,Value:Test,More,Data ');
170 |     });
171 | 
172 |     // Diff without changes
173 |     it('should handle identity', function() {
174 |       const diffResult = diffWords('New Value', 'New Value');
175 |       expect(convertChangesToXML(diffResult)).to.equal('New Value');
176 |     });
177 |     it('should handle empty', function() {
178 |       const diffResult = diffWords('', '');
179 |       expect(convertChangesToXML(diffResult)).to.equal('');
180 |     });
181 |     it('should diff has identical content', function() {
182 |       const diffResult = diffWords('New Value', 'New  Value');
183 |       expect(convertChangesToXML(diffResult)).to.equal('New  Value');
184 |     });
185 | 
186 |     // Empty diffs
187 |     it('should diff empty new content', function() {
188 |       const diffResult = diffWords('New Value', '');
189 |       expect(diffResult.length).to.equal(1);
190 |       expect(convertChangesToXML(diffResult)).to.equal('New Value');
191 |     });
192 |     it('should diff empty old content', function() {
193 |       const diffResult = diffWords('', 'New Value');
194 |       expect(convertChangesToXML(diffResult)).to.equal('New Value');
195 |     });
196 | 
197 |     it('should include count with identity cases', function() {
198 |       expect(diffWords('foo', 'foo')).to.eql([{value: 'foo', count: 1, removed: false, added: false}]);
199 |       expect(diffWords('foo bar', 'foo bar')).to.eql([{value: 'foo bar', count: 2, removed: false, added: false}]);
200 |     });
201 |     it('should include count with empty cases', function() {
202 |       expect(diffWords('foo', '')).to.eql([{value: 'foo', count: 1, added: false, removed: true}]);
203 |       expect(diffWords('foo bar', '')).to.eql([{value: 'foo bar', count: 2, added: false, removed: true}]);
204 | 
205 |       expect(diffWords('', 'foo')).to.eql([{value: 'foo', count: 1, added: true, removed: false}]);
206 |       expect(diffWords('', 'foo bar')).to.eql([{value: 'foo bar', count: 2, added: true, removed: false}]);
207 |     });
208 | 
209 |     it('should ignore whitespace', function() {
210 |       expect(diffWords('hase igel fuchs', 'hase igel fuchs')).to.eql([{ count: 3, value: 'hase igel fuchs', removed: false, added: false }]);
211 |       expect(diffWords('hase igel fuchs', 'hase igel fuchs\n')).to.eql([{ count: 3, value: 'hase igel fuchs\n', removed: false, added: false }]);
212 |       expect(diffWords('hase igel fuchs\n', 'hase igel fuchs')).to.eql([{ count: 3, value: 'hase igel fuchs', removed: false, added: false }]);
213 |       expect(diffWords('hase igel fuchs', 'hase igel\nfuchs')).to.eql([{ count: 3, value: 'hase igel\nfuchs', removed: false, added: false }]);
214 |       expect(diffWords('hase igel\nfuchs', 'hase igel fuchs')).to.eql([{ count: 3, value: 'hase igel fuchs', removed: false, added: false }]);
215 |     });
216 | 
217 |     it('should diff with only whitespace', function() {
218 |       let diffResult = diffWords('', ' ');
219 |       expect(convertChangesToXML(diffResult)).to.equal(' ');
220 | 
221 |       diffResult = diffWords(' ', '');
222 |       expect(convertChangesToXML(diffResult)).to.equal(' ');
223 |     });
224 | 
225 |     it('should support async mode', function(done) {
226 |       diffWords('New    Value', 'New \n \t Value', function(diffResult) {
227 |         expect(convertChangesToXML(diffResult)).to.equal('New \n \t Value');
228 |         done();
229 |       });
230 |     });
231 | 
232 |     it('calls #diffWordsWithSpace if you pass ignoreWhitespace: false', function() {
233 |       const diffResult = diffWords(
234 |         'foo bar',
235 |         'foo\tbar',
236 |         {ignoreWhitespace: false}
237 |       );
238 |       expect(convertChangesToXML(diffResult)).to.equal('foo \tbar');
239 |     });
240 | 
241 |     it('supports tokenizing with an Intl.Segmenter', () => {
242 |       // Example 1: Diffing Chinese text with no spaces.
243 |       // a. "She (她) has (有) many (很多) tables (桌子)"
244 |       // b. "Mei (梅) has (有) many (很多) sons (儿子)"
245 |       // We want to see that diffWords will get the word counts right and won't try to treat the
246 |       // trailing 子 as common to both texts (since it's part of a different word each time).
247 |       const chineseSegmenter = new Intl.Segmenter('zh', {granularity: 'word'});
248 |       const diffResult = diffWords('她有很多桌子。', '梅有很多儿子。', {intlSegmenter: chineseSegmenter});
249 |       expect(diffResult).to.deep.equal([
250 |         { count: 1, added: false, removed: true, value: '她' },
251 |         { count: 1, added: true, removed: false, value: '梅' },
252 |         { count: 2, added: false, removed: false, value: '有很多' },
253 |         { count: 1, added: false, removed: true, value: '桌子' },
254 |         { count: 1, added: true, removed: false, value: '儿子' },
255 |         { count: 1, added: false, removed: false, value: '。' }
256 |       ]);
257 | 
258 |       // Example 2: Should understand that a colon in the middle of a word is not a word break in
259 |       // Finnish (see https://stackoverflow.com/a/76402021/1709587)
260 |       const finnishSegmenter = new Intl.Segmenter('fi', {granularity: 'word'});
261 |       expect(convertChangesToXML(diffWords(
262 |         'USA:n nykyinen presidentti',
263 |         'USA ja sen presidentti',
264 |         {intlSegmenter: finnishSegmenter}
265 |       ))).to.equal('USA:n nykyinenUSA ja sen presidentti');
266 | 
267 |       // Example 3: Some English text, including contractions, long runs of arbitrary space,
268 |       // and punctuation, and using case insensitive mode, just to show all normal behaviour of
269 |       // diffWords still works with a segmenter
270 |       const englishSegmenter = new Intl.Segmenter('en', {granularity: 'word'});
271 |       expect(convertChangesToXML(diffWords(
272 |         "There wasn't time \n \t   for all that. He thought...",
273 |         "There isn't time \n \t   left for all that, he thinks.",
274 |         {intlSegmenter: englishSegmenter, ignoreCase: true}
275 |       ))).to.equal(
276 |         "There wasn'tisn't time \n \t   left "
277 |         + 'for all that., he thoughtthinks...'
278 |       );
279 |     });
280 | 
281 |     it('rejects attempts to use a non-word Intl.Segmenter', () => {
282 |       const segmenter = new Intl.Segmenter('en', {granularity: 'grapheme'});
283 |       expect(() => {
284 |         diffWords('foo', 'bar', {intlSegmenter: segmenter});
285 |       }).to['throw']('The segmenter passed must have a granularity of "word"');
286 |     });
287 |   });
288 | 
289 |   describe('#diffWordsWithSpace', function() {
290 |     it('should diff whitespace', function() {
291 |       const diffResult = diffWordsWithSpace('New Value', 'New  ValueMoreData');
292 |       expect(convertChangesToXML(diffResult)).to.equal('New Value  ValueMoreData');
293 |     });
294 | 
295 |     it('should diff multiple whitespace values', function() {
296 |       const diffResult = diffWordsWithSpace('New Value  ', 'New  ValueMoreData ');
297 |       expect(convertChangesToXML(diffResult)).to.equal('New Value  ValueMoreData ');
298 |     });
299 | 
300 |     it('should inserts values in parenthesis', function() {
301 |       const diffResult = diffWordsWithSpace('()', '(word)');
302 |       expect(convertChangesToXML(diffResult)).to.equal('(word)');
303 |     });
304 | 
305 |     it('should inserts values in brackets', function() {
306 |       const diffResult = diffWordsWithSpace('[]', '[word]');
307 |       expect(convertChangesToXML(diffResult)).to.equal('[word]');
308 |     });
309 | 
310 |     it('should inserts values in curly braces', function() {
311 |       const diffResult = diffWordsWithSpace('{}', '{word}');
312 |       expect(convertChangesToXML(diffResult)).to.equal('{word}');
313 |     });
314 | 
315 |     it('should inserts values in quotes', function() {
316 |       const diffResult = diffWordsWithSpace("''", "'word'");
317 |       expect(convertChangesToXML(diffResult)).to.equal("'word'");
318 |     });
319 | 
320 |     it('should inserts values in double quotes', function() {
321 |       const diffResult = diffWordsWithSpace('""', '"word"');
322 |       expect(convertChangesToXML(diffResult)).to.equal('"word"');
323 |     });
324 | 
325 |     it('should treat newline as separate token (issues #180, #211)', function() {
326 |       // #180
327 |       const diffResult1 = diffWordsWithSpace('foo\nbar', 'foo\n\n\nbar');
328 |       expect(convertChangesToXML(diffResult1)).to.equal('foo\n\n\nbar');
329 |       // #211
330 |       const diffResult2 = diffWordsWithSpace('A\n\nB\n', 'A\nB\n');
331 |       expect(convertChangesToXML(diffResult2)).to.equal('A\n\nB\n');
332 |       // Windows-style newlines should also get a single token
333 |       const diffResult3 = diffWordsWithSpace('foo\r\nbar', 'foo  \r\n\r\n\r\nbar');
334 |       expect(convertChangesToXML(diffResult3)).to.equal('foo  \r\n\r\n\r\nbar');
335 |       const diffResult4 = diffWordsWithSpace('A\r\n\r\nB\r\n', 'A\r\nB\r\n');
336 |       expect(convertChangesToXML(diffResult4)).to.equal('A\r\n\r\nB\r\n');
337 |     });
338 | 
339 |     it('should perform async operations', function(done) {
340 |       diffWordsWithSpace('New Value  ', 'New  ValueMoreData ', function(diffResult) {
341 |         expect(convertChangesToXML(diffResult)).to.equal('New Value  ValueMoreData ');
342 |         done();
343 |       });
344 |     });
345 | 
346 |     // With without anchor (the Heckel algorithm error case)
347 |     it('should diff when there is no anchor value', function() {
348 |       const diffResult = diffWordsWithSpace('New Value New Value', 'Value Value New New');
349 |       expect(convertChangesToXML(diffResult)).to.equal('NewValue Value New ValueNew');
350 |     });
351 | 
352 |     it('should handle empty', function() {
353 |       const diffResult = diffWordsWithSpace('', '');
354 |       expect(convertChangesToXML(diffResult)).to.equal('');
355 |     });
356 | 
357 |     describe('case insensitivity', function() {
358 |       it("is considered when there's a difference", function() {
359 |         const diffResult = diffWordsWithSpace('new value', 'New  ValueMoreData', {ignoreCase: true});
360 |         expect(convertChangesToXML(diffResult)).to.equal('New value  ValueMoreData');
361 |       });
362 | 
363 |       it("is considered when there's no difference", function() {
364 |         const diffResult = diffWordsWithSpace('new value', 'New Value', {ignoreCase: true});
365 |         expect(convertChangesToXML(diffResult)).to.equal('New Value');
366 |       });
367 |     });
368 |   });
369 | });
370 | 


--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
 1 | import * as Diff from 'diff';
 2 | 
 3 | import {expect} from 'chai';
 4 | 
 5 | describe('root exports', function() {
 6 |   it('should export APIs', function() {
 7 |     expect(Diff.Diff).to.exist;
 8 | 
 9 |     expect(Diff.diffChars).to.exist;
10 |     expect(Diff.diffWords).to.exist;
11 |     expect(Diff.diffWordsWithSpace).to.exist;
12 |     expect(Diff.diffLines).to.exist;
13 |     expect(Diff.diffTrimmedLines).to.exist;
14 |     expect(Diff.diffSentences).to.exist;
15 | 
16 |     expect(Diff.diffCss).to.exist;
17 |     expect(Diff.diffJson).to.exist;
18 | 
19 |     expect(Diff.diffArrays).to.exist;
20 | 
21 |     expect(Diff.structuredPatch).to.exist;
22 |     expect(Diff.createTwoFilesPatch).to.exist;
23 |     expect(Diff.createPatch).to.exist;
24 |     expect(Diff.applyPatch).to.exist;
25 |     expect(Diff.applyPatches).to.exist;
26 |     expect(Diff.parsePatch).to.exist;
27 |     expect(Diff.convertChangesToDMP).to.exist;
28 |     expect(Diff.convertChangesToXML).to.exist;
29 |     expect(Diff.canonicalize).to.exist;
30 |   });
31 | });
32 | 


--------------------------------------------------------------------------------
/test/patch/line-endings.js:
--------------------------------------------------------------------------------
  1 | import {parsePatch} from '../../libesm/patch/parse.js';
  2 | import {formatPatch} from '../../libesm/patch/create.js';
  3 | import {winToUnix, unixToWin, isWin, isUnix} from '../../libesm/patch/line-endings.js';
  4 | 
  5 | import {expect} from 'chai';
  6 | 
  7 | describe('unixToWin and winToUnix', function() {
  8 |   it('should convert line endings in a patch between Unix-style and Windows-style', function() {
  9 |     const patch = parsePatch(
 10 |       'Index: test\n'
 11 |       + '===================================================================\n'
 12 |       + '--- test\theader1\n'
 13 |       + '+++ test\theader2\n'
 14 |       + '@@ -1,3 +1,4 @@\n'
 15 |       + ' line2\n'
 16 |       + ' line3\r\n'
 17 |       + '+line4\r\n'
 18 |       + ' line5\n'
 19 |     );
 20 | 
 21 |     const winPatch = unixToWin(patch);
 22 |     expect(formatPatch(winPatch)).to.equal(
 23 |       'Index: test\n'
 24 |       + '===================================================================\n'
 25 |       + '--- test\theader1\n'
 26 |       + '+++ test\theader2\n'
 27 |       + '@@ -1,3 +1,4 @@\n'
 28 |       + ' line2\r\n'
 29 |       + ' line3\r\n'
 30 |       + '+line4\r\n'
 31 |       + ' line5\r\n'
 32 |     );
 33 | 
 34 |     const unixPatch = winToUnix(winPatch);
 35 |     expect(formatPatch(unixPatch)).to.equal(
 36 |       'Index: test\n'
 37 |       + '===================================================================\n'
 38 |       + '--- test\theader1\n'
 39 |       + '+++ test\theader2\n'
 40 |       + '@@ -1,3 +1,4 @@\n'
 41 |       + ' line2\n'
 42 |       + ' line3\n'
 43 |       + '+line4\n'
 44 |       + ' line5\n'
 45 |     );
 46 | 
 47 |     expect(formatPatch(winToUnix(patch))).to.equal(formatPatch(unixPatch));
 48 |   });
 49 | 
 50 |   it('should not introduce \\r on the last line if there was no newline at EOF', () => {
 51 |     const patch = parsePatch(
 52 |       'Index: test\n'
 53 |       + '===================================================================\n'
 54 |       + '--- test\theader1\n'
 55 |       + '+++ test\theader2\n'
 56 |       + '@@ -1,2 +1,3 @@\n'
 57 |       + ' line2\n'
 58 |       + ' line3\n'
 59 |       + '+line4\n'
 60 |       + '\\ No newline at end of file\n'
 61 |     );
 62 | 
 63 |     const winPatch = unixToWin(patch);
 64 |     expect(formatPatch(winPatch)).to.equal(
 65 |       'Index: test\n'
 66 |       + '===================================================================\n'
 67 |       + '--- test\theader1\n'
 68 |       + '+++ test\theader2\n'
 69 |       + '@@ -1,2 +1,3 @@\n'
 70 |       + ' line2\r\n'
 71 |       + ' line3\r\n'
 72 |       + '+line4\n'
 73 |       + '\\ No newline at end of file\n'
 74 |     );
 75 |   });
 76 | });
 77 | 
 78 | describe('isWin', () => {
 79 |   it('should return true if all lines end with CRLF', () => {
 80 |     const patch = parsePatch(
 81 |       'Index: test\n'
 82 |       + '===================================================================\n'
 83 |       + '--- test\theader1\n'
 84 |       + '+++ test\theader2\n'
 85 |       + '@@ -1,2 +1,3 @@\n'
 86 |       + ' line2\r\n'
 87 |       + ' line3\r\n'
 88 |       + '+line4\r\n'
 89 |     );
 90 |     expect(isWin(patch)).to.equal(true);
 91 |   });
 92 | 
 93 |   it('should return false if a line ends with a LF without a CR', () => {
 94 |     const patch = parsePatch(
 95 |       'Index: test\n'
 96 |       + '===================================================================\n'
 97 |       + '--- test\theader1\n'
 98 |       + '+++ test\theader2\n'
 99 |       + '@@ -1,2 +1,3 @@\n'
100 |       + ' line2\r\n'
101 |       + ' line3\r\n'
102 |       + '+line4\n'
103 |     );
104 |     expect(isWin(patch)).to.equal(false);
105 |   });
106 | 
107 |   it('should still return true if only the last line in a file is missing a CR and there is a no newline at EOF indicator', () => {
108 |     const patch = parsePatch(
109 |       'Index: test\n'
110 |       + '===================================================================\n'
111 |       + '--- test\theader1\n'
112 |       + '+++ test\theader2\n'
113 |       + '@@ -1,2 +1,3 @@\n'
114 |       + ' line2\r\n'
115 |       + ' line3\r\n'
116 |       + '+line4\n'
117 |       + '\\ No newline at end of file\n'
118 |     );
119 |     expect(isWin(patch)).to.equal(true);
120 |   });
121 | });
122 | 
123 | describe('isUnix', () => {
124 |   it('should return false if some lines end with CRLF', () => {
125 |     const patch = parsePatch(
126 |       'Index: test\n'
127 |       + '===================================================================\n'
128 |       + '--- test\theader1\n'
129 |       + '+++ test\theader2\n'
130 |       + '@@ -1,2 +1,3 @@\n'
131 |       + ' line2\r\n'
132 |       + ' line3\n'
133 |       + '+line4\r\n'
134 |     );
135 |     expect(isUnix(patch)).to.equal(false);
136 |   });
137 | 
138 |   it('should return true if no lines end with CRLF', () => {
139 |     const patch = parsePatch(
140 |       'Index: test\n'
141 |       + '===================================================================\n'
142 |       + '--- test\theader1\n'
143 |       + '+++ test\theader2\n'
144 |       + '@@ -1,2 +1,3 @@\n'
145 |       + ' line2\n'
146 |       + ' line3\n'
147 |       + '+line4\n'
148 |     );
149 |     expect(isUnix(patch)).to.equal(true);
150 |   });
151 | });
152 | 


--------------------------------------------------------------------------------
/test/patch/parse.js:
--------------------------------------------------------------------------------
  1 | import {parsePatch} from '../../libesm/patch/parse.js';
  2 | import {createPatch} from '../../libesm/patch/create.js';
  3 | 
  4 | import {expect} from 'chai';
  5 | 
  6 | describe('patch/parse', function() {
  7 |   describe('#parse', function() {
  8 |     it('should parse basic patches', function() {
  9 |       expect(parsePatch(
 10 | `@@ -1,3 +1,4 @@
 11 |  line2
 12 |  line3
 13 | +line4
 14 |  line5`))
 15 |         .to.eql([{
 16 |           hunks: [
 17 |             {
 18 |               oldStart: 1, oldLines: 3,
 19 |               newStart: 1, newLines: 4,
 20 |               lines: [
 21 |                 ' line2',
 22 |                 ' line3',
 23 |                 '+line4',
 24 |                 ' line5'
 25 |               ]
 26 |             }
 27 |           ]
 28 |         }]);
 29 |     });
 30 |     it('should parse single line hunks', function() {
 31 |       expect(parsePatch(
 32 | `@@ -1 +1 @@
 33 | -line3
 34 | +line4`))
 35 |         .to.eql([{
 36 |           hunks: [
 37 |             {
 38 |               oldStart: 1, oldLines: 1,
 39 |               newStart: 1, newLines: 1,
 40 |               lines: [
 41 |                 '-line3',
 42 |                 '+line4'
 43 |               ]
 44 |             }
 45 |           ]
 46 |         }]);
 47 |     });
 48 |     it('should parse multiple hunks', function() {
 49 |       expect(parsePatch(
 50 | `@@ -1,3 +1,4 @@
 51 |  line2
 52 |  line3
 53 | +line4
 54 |  line5
 55 | @@ -4,4 +1,3 @@
 56 |  line2
 57 |  line3
 58 | -line4
 59 |  line5`))
 60 |         .to.eql([{
 61 |           hunks: [
 62 |             {
 63 |               oldStart: 1, oldLines: 3,
 64 |               newStart: 1, newLines: 4,
 65 |               lines: [
 66 |                 ' line2',
 67 |                 ' line3',
 68 |                 '+line4',
 69 |                 ' line5'
 70 |               ]
 71 |             },
 72 |             {
 73 |               oldStart: 4, oldLines: 4,
 74 |               newStart: 1, newLines: 3,
 75 |               lines: [
 76 |                 ' line2',
 77 |                 ' line3',
 78 |                 '-line4',
 79 |                 ' line5'
 80 |               ]
 81 |             }
 82 |           ]
 83 |         }]);
 84 |     });
 85 |     it('should parse single index patches', function() {
 86 |       expect(parsePatch(
 87 | `Index: test
 88 | ===================================================================
 89 | --- from\theader1
 90 | +++ to\theader2
 91 | @@ -1,3 +1,4 @@
 92 |  line2
 93 |  line3
 94 | +line4
 95 |  line5`))
 96 |         .to.eql([{
 97 |           index: 'test',
 98 |           oldFileName: 'from',
 99 |           oldHeader: 'header1',
100 |           newFileName: 'to',
101 |           newHeader: 'header2',
102 |           hunks: [
103 |             {
104 |               oldStart: 1, oldLines: 3,
105 |               newStart: 1, newLines: 4,
106 |               lines: [
107 |                 ' line2',
108 |                 ' line3',
109 |                 '+line4',
110 |                 ' line5'
111 |               ]
112 |             }
113 |           ]
114 |         }]);
115 |     });
116 |     it('should parse multiple index files', function() {
117 |       expect(parsePatch(
118 | `Index: test
119 | ===================================================================
120 | --- from\theader1
121 | +++ to\theader2
122 | @@ -1,3 +1,4 @@
123 |  line2
124 |  line3
125 | +line4
126 |  line5
127 | Index: test2
128 | ===================================================================
129 | --- from\theader1
130 | +++ to\theader2
131 | @@ -1,3 +1,4 @@
132 |  line2
133 |  line3
134 | +line4
135 |  line5`))
136 |         .to.eql([{
137 |           index: 'test',
138 |           oldFileName: 'from',
139 |           oldHeader: 'header1',
140 |           newFileName: 'to',
141 |           newHeader: 'header2',
142 |           hunks: [
143 |             {
144 |               oldStart: 1, oldLines: 3,
145 |               newStart: 1, newLines: 4,
146 |               lines: [
147 |                 ' line2',
148 |                 ' line3',
149 |                 '+line4',
150 |                 ' line5'
151 |               ]
152 |             }
153 |           ]
154 |         }, {
155 |           index: 'test2',
156 |           oldFileName: 'from',
157 |           oldHeader: 'header1',
158 |           newFileName: 'to',
159 |           newHeader: 'header2',
160 |           hunks: [
161 |             {
162 |               oldStart: 1, oldLines: 3,
163 |               newStart: 1, newLines: 4,
164 |               lines: [
165 |                 ' line2',
166 |                 ' line3',
167 |                 '+line4',
168 |                 ' line5'
169 |               ]
170 |             }
171 |           ]
172 |         }]);
173 |     });
174 | 
175 |     it('should parse multiple files without the Index line', function() {
176 |       expect(parsePatch(
177 | `--- from\theader1
178 | +++ to\theader2
179 | @@ -1,3 +1,4 @@
180 |  line2
181 |  line3
182 | +line4
183 |  line5
184 | --- from\theader1
185 | +++ to\theader2
186 | @@ -1,3 +1,4 @@
187 |  line2
188 |  line3
189 | +line4
190 |  line5`))
191 |         .to.eql([{
192 |           oldFileName: 'from',
193 |           oldHeader: 'header1',
194 |           newFileName: 'to',
195 |           newHeader: 'header2',
196 |           hunks: [
197 |             {
198 |               oldStart: 1, oldLines: 3,
199 |               newStart: 1, newLines: 4,
200 |               lines: [
201 |                 ' line2',
202 |                 ' line3',
203 |                 '+line4',
204 |                 ' line5'
205 |               ]
206 |             }
207 |           ]
208 |         }, {
209 |           oldFileName: 'from',
210 |           oldHeader: 'header1',
211 |           newFileName: 'to',
212 |           newHeader: 'header2',
213 |           hunks: [
214 |             {
215 |               oldStart: 1, oldLines: 3,
216 |               newStart: 1, newLines: 4,
217 |               lines: [
218 |                 ' line2',
219 |                 ' line3',
220 |                 '+line4',
221 |                 ' line5'
222 |               ]
223 |             }
224 |           ]
225 |         }]);
226 |     });
227 | 
228 |  it('should parse patches with filenames having quotes and back slashes', function() {
229 |       expect(parsePatch(
230 | `Index: test
231 | ===================================================================
232 | --- "from\\\\a\\\\file\\\\with\\\\quotes\\\\and\\\\backslash"\theader1
233 | +++ "to\\\\a\\\\file\\\\with\\\\quotes\\\\and\\\\backslash"\theader2
234 | @@ -1,3 +1,4 @@
235 |  line2
236 |  line3
237 | +line4
238 |  line5`))
239 |         .to.eql([{
240 |           index: 'test',
241 |           oldFileName: 'from\\a\\file\\with\\quotes\\and\\backslash',
242 |           oldHeader: 'header1',
243 |           newFileName: 'to\\a\\file\\with\\quotes\\and\\backslash',
244 |           newHeader: 'header2',
245 |           hunks: [
246 |             {
247 |               oldStart: 1, oldLines: 3,
248 |               newStart: 1, newLines: 4,
249 |               lines: [
250 |                 ' line2',
251 |                 ' line3',
252 |                 '+line4',
253 |                 ' line5'
254 |               ]
255 |             }
256 |           ]
257 |         }]);
258 |     });
259 | 
260 |     it('should note added EOFNL', function() {
261 |       expect(parsePatch(
262 | `@@ -1,1 +0,0 @@
263 | -line5
264 | \\ No newline at end of file`))
265 |         .to.eql([{
266 |           hunks: [
267 |             {
268 |               oldStart: 1, oldLines: 1,
269 |               newStart: 1, newLines: 0,
270 |               lines: [
271 |                 '-line5',
272 |                 '\\ No newline at end of file'
273 |               ]
274 |             }
275 |           ]
276 |         }]);
277 |     });
278 |     it('should note removed EOFNL', function() {
279 |       expect(parsePatch(
280 | `@@ -0,0 +1 @@
281 | +line5
282 | \\ No newline at end of file`))
283 |         .to.eql([{
284 |           hunks: [
285 |             {
286 |               oldStart: 1, oldLines: 0,
287 |               newStart: 1, newLines: 1,
288 |               lines: [
289 |                 '+line5',
290 |                 '\\ No newline at end of file'
291 |               ]
292 |             }
293 |           ]
294 |         }]);
295 |     });
296 |     it('should ignore context no EOFNL', function() {
297 |       expect(parsePatch(
298 | `@@ -1 +1,2 @@
299 | +line4
300 |  line5
301 | \\ No newline at end of file`))
302 |         .to.eql([{
303 |           hunks: [
304 |             {
305 |               oldStart: 1, oldLines: 1,
306 |               newStart: 1, newLines: 2,
307 |               lines: [
308 |                 '+line4',
309 |                 ' line5',
310 |                 '\\ No newline at end of file'
311 |               ]
312 |             }
313 |           ]
314 |         }]);
315 |     });
316 | 
317 |     it('should perform sanity checks on line numbers', function() {
318 |       parsePatch('@@ -1 +1 @@');
319 | 
320 |       expect(function() {
321 |         parsePatch('@@ -1 +1,4 @@');
322 |       }).to['throw']('Added line count did not match for hunk at line 1');
323 |       expect(function() {
324 |         parsePatch('@@ -1,4 +1 @@');
325 |       }).to['throw']('Removed line count did not match for hunk at line 1');
326 |     });
327 | 
328 |     it('should not throw on invalid input', function() {
329 |       expect(parsePatch('blit\nblat\nIndex: foo\nfoo'))
330 |           .to.eql([{
331 |             hunks: [],
332 |             index: 'foo'
333 |           }]);
334 |     });
335 |     it('should throw on invalid input', function() {
336 |       expect(function() {
337 |         parsePatch('Index: foo\n+++ bar\nblah');
338 |       }).to['throw'](/Unknown line 3 "blah"/);
339 |     });
340 | 
341 |     it('should handle OOM case', function() {
342 |       parsePatch('Index: \n===================================================================\n--- \n+++ \n@@ -1,1 +1,2 @@\n-1\n\\ No newline at end of file\n+1\n+2\n');
343 |     });
344 | 
345 |     it('should treat vertical tabs like ordinary characters', function() {
346 |       // Patch below was generated as follows:
347 |       // 1. From Node, run:
348 |       //      fs.writeFileSync("foo", "foo\nbar\vbar\nbaz\nqux")
349 |       //      fs.writeFileSync("bar", "foo\nbarry\vbarry\nbaz\nqux")
350 |       // 2. From shell, run:
351 |       //      diff -u foo bar > diff.txt
352 |       // 3. From Node, run
353 |       //      fs.readFileSync("diff.txt")
354 |       //    and copy the string literal you get.
355 |       // This patch illustrates how the Unix `diff` and `patch` tools handle
356 |       // characters like vertical tabs - namely, they simply treat them as
357 |       // ordinary characters, NOT as line breaks. JsDiff used to treat them as
358 |       // line breaks but this breaks its parsing of patches like this one; this
359 |       // test was added to demonstrate the brokenness and prevent us from
360 |       // reintroducing it.
361 |       const patch = '--- foo\t2023-12-20 16:11:20.908225554 +0000\n' +
362 |       '+++ bar\t2023-12-20 16:11:34.391473579 +0000\n' +
363 |       '@@ -1,4 +1,4 @@\n' +
364 |       ' foo\n' +
365 |       '-bar\x0Bbar\n' +
366 |       '+barry\x0Bbarry\n' +
367 |       ' baz\n' +
368 |       ' qux\n' +
369 |       '\\ No newline at end of file\n';
370 | 
371 |       expect(parsePatch(patch)).to.eql([
372 |         {
373 |           oldFileName: 'foo',
374 |           oldHeader: '2023-12-20 16:11:20.908225554 +0000',
375 |           newFileName: 'bar',
376 |           newHeader: '2023-12-20 16:11:34.391473579 +0000',
377 |           hunks: [
378 |             {
379 |               oldStart: 1,
380 |               oldLines: 4,
381 |               newStart: 1,
382 |               newLines: 4,
383 |               lines: [
384 |                 ' foo',
385 |                 '-bar\x0Bbar',
386 |                 '+barry\x0Bbarry',
387 |                 ' baz',
388 |                 ' qux',
389 |                 '\\ No newline at end of file'
390 |               ]
391 |             }
392 |           ]
393 |         }
394 |       ]);
395 |     });
396 | 
397 |     it('should treat vertical tabs in a way consistent with createPatch', function() {
398 |       // This is basically the same as the test above, but this time we create
399 |       // the patch USING JsDiff instead of testing one created with Unix diff
400 |       const patch = createPatch('foo', 'foo\nbar\vbar\nbaz\nqux', 'foo\nbarry\vbarry\nbaz\nqux');
401 | 
402 |       expect(parsePatch(patch)).to.eql([
403 |         {
404 |           oldFileName: 'foo',
405 |           oldHeader: '',
406 |           newFileName: 'foo',
407 |           newHeader: '',
408 |           index: 'foo',
409 |           hunks: [
410 |             {
411 |               oldStart: 1,
412 |               oldLines: 4,
413 |               newStart: 1,
414 |               newLines: 4,
415 |               lines: [
416 |                 ' foo',
417 |                 '-bar\x0Bbar',
418 |                 '+barry\x0Bbarry',
419 |                 ' baz',
420 |                 ' qux',
421 |                 '\\ No newline at end of file'
422 |               ]
423 |             }
424 |           ]
425 |         }
426 |       ]);
427 |     });
428 | 
429 |     it('should tolerate patches with extra trailing newlines after hunks', () => {
430 |       // Regression test for https://github.com/kpdecker/jsdiff/issues/524
431 |       // Not only are these considered valid by GNU patch, but jsdiff's own formatPatch method
432 |       // emits patches like this, which jsdiff used to then be unable to parse!
433 |       const patchStr = `--- foo\t2024-06-14 22:16:31.444276792 +0100
434 | +++ bar\t2024-06-14 22:17:14.910611219 +0100
435 | @@ -1,7 +1,7 @@
436 |  first
437 |  second
438 |  third
439 | -fourth
440 | -fifth
441 | +vierte
442 | +fünfte
443 |  sixth
444 |  seventh
445 | 
446 | `;
447 |       expect(parsePatch(patchStr)).to.eql([{
448 |         oldFileName: 'foo',
449 |         oldHeader: '2024-06-14 22:16:31.444276792 +0100',
450 |         newFileName: 'bar',
451 |         newHeader: '2024-06-14 22:17:14.910611219 +0100',
452 |         hunks: [
453 |           {
454 |             oldStart: 1,
455 |             oldLines: 7,
456 |             newStart: 1,
457 |             newLines: 7,
458 |             lines: [
459 |               ' first',
460 |               ' second',
461 |               ' third',
462 |               '-fourth',
463 |               '-fifth',
464 |               '+vierte',
465 |               '+fünfte',
466 |               ' sixth',
467 |               ' seventh'
468 |             ]
469 |           }
470 |         ]
471 |       }]);
472 |     });
473 | 
474 |     it("shouldn't be caught out by removal/addition of lines starting with -- or ++", () => {
475 |       // The patch below is a valid patch generated by diffing this file, foo:
476 | 
477 |       // first
478 |       // second
479 |       // third
480 |       // -- bla
481 |       // fifth
482 |       // sixth
483 | 
484 |       // against this file, bar:
485 | 
486 |       // first
487 |       // second
488 |       // third
489 |       // ++ bla
490 |       // fifth
491 |       // sixth
492 |       // seventh
493 | 
494 |       // with the command `diff -u0 foo bar`. (All lines in `foo` and `bar` have no leading
495 |       // whitespace and a trailing LF.)
496 | 
497 |       // This is effectively an adversarial example meant to catch out a parser that tries to
498 |       // detect the end of a file in a multi-file diff by looking for lines starting with '---',
499 |       // '+++', and then '@@'. jsdiff used to do this. However, as this example illustrates, it is
500 |       // unsound, since the '---' and '+++' lines might actually just represent the deletion and
501 |       // insertion of lines starting with '--' and '++'. The only way to disambiguate these
502 |       // interpretations is to heed the line counts in the @@ hunk headers; you *cannot* reliably
503 |       // determine where a hunk or file ends in a unified diff patch without heeding those line
504 |       // counts.
505 | 
506 |       const patchStr = `--- foo\t2024-06-14 21:57:04.341065736 +0100
507 | +++ bar\t2024-06-14 22:00:57.988080321 +0100
508 | @@ -4 +4 @@
509 | --- bla
510 | +++ bla
511 | @@ -6,0 +7 @@
512 | +seventh
513 | `;
514 | 
515 |       expect(parsePatch(patchStr)).to.eql([{
516 |         oldFileName: 'foo',
517 |         oldHeader: '2024-06-14 21:57:04.341065736 +0100',
518 |         newFileName: 'bar',
519 |         newHeader: '2024-06-14 22:00:57.988080321 +0100',
520 |         hunks: [
521 |           { oldStart: 4, oldLines: 1, newStart: 4, newLines: 1, lines: ['--- bla', '+++ bla'] },
522 |           { oldStart: 7, oldLines: 0, newStart: 7, newLines: 1, lines: ['+seventh'] }
523 |         ]
524 |       }]);
525 |     });
526 | 
527 |     it('should emit an error if a hunk contains an invalid line', () => {
528 |       // Within a hunk, every line must either start with '+' (insertion), '-' (deletion),
529 |       // ' ' (context line, i.e. not deleted nor inserted) or a backslash (for
530 |       // '\\ No newline at end of file' lines). Seeing anything else before the end of the hunk is
531 |       // an error.
532 | 
533 |       const patchStr = `Index: test
534 | ===================================================================
535 | --- from\theader1
536 | +++ to\theader2
537 | @@ -1,3 +1,4 @@
538 |  line2
539 | line3
540 | +line4
541 |  line5`;
542 | 
543 |       // eslint-disable-next-line dot-notation
544 |       expect(() => {parsePatch(patchStr);}).to.throw('Hunk at line 5 contained invalid line line3');
545 |     });
546 |   });
547 | });
548 | 


--------------------------------------------------------------------------------
/test/patch/reverse.js:
--------------------------------------------------------------------------------
 1 | import {applyPatch} from '../../libesm/patch/apply.js';
 2 | import {structuredPatch, formatPatch} from '../../libesm/patch/create.js';
 3 | import {reversePatch} from '../../libesm/patch/reverse.js';
 4 | import {parsePatch} from '../../libesm/patch/parse.js';
 5 | 
 6 | import {expect} from 'chai';
 7 | 
 8 | describe('patch/reverse', function() {
 9 |   describe('#reversePatch', function() {
10 |     it('should output a patch that is the inverse of the provided patch', function() {
11 |       const file1 = 'line1\nline2\nline3\nline4\n';
12 |       const file2 = 'line1\nline2\nline5\nline4\n';
13 |       const patch = structuredPatch('file1', 'file2', file1, file2);
14 |       const reversedPatch = reversePatch(patch);
15 |       expect(formatPatch(reversedPatch)).to.equal(
16 |         '===================================================================\n'
17 |         + '--- file2\n'
18 |         + '+++ file1\n'
19 |         + '@@ -1,4 +1,4 @@\n'
20 |         + ' line1\n'
21 |         + ' line2\n'
22 |         + '+line3\n'
23 |         + '-line5\n'
24 |         + ' line4\n'
25 |       );
26 |       expect(applyPatch(file2, reversedPatch)).to.equal(file1);
27 |     });
28 | 
29 |     it('should support taking an array of structured patches, as output by parsePatch', function() {
30 |       const patch = parsePatch(
31 |         'diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md\n' +
32 |         'index 20b807a..4a96aff 100644\n' +
33 |         '--- a/CONTRIBUTING.md\n' +
34 |         '+++ b/CONTRIBUTING.md\n' +
35 |         '@@ -2,6 +2,8 @@\n' +
36 |         ' \n' +
37 |         ' ## Pull Requests\n' +
38 |         ' \n' +
39 |         '+bla bla bla\n' +
40 |         '+\n' +
41 |         ' We also accept [pull requests][pull-request]!\n' +
42 |         ' \n' +
43 |         ' Generally we like to see pull requests that\n' +
44 |         'diff --git a/README.md b/README.md\n' +
45 |         'index 06eebfa..40919a6 100644\n' +
46 |         '--- a/README.md\n' +
47 |         '+++ b/README.md\n' +
48 |         '@@ -1,5 +1,7 @@\n' +
49 |         ' # jsdiff\n' +
50 |         ' \n' +
51 |         '+foo\n' +
52 |         '+\n' +
53 |         ' [![Build Status](https://secure.travis-ci.org/kpdecker/jsdiff.svg)](http://travis-ci.org/kpdecker/jsdiff)\n' +
54 |         ' [![Sauce Test Status](https://saucelabs.com/buildstatus/jsdiff)](https://saucelabs.com/u/jsdiff)\n' +
55 |         ' \n' +
56 |         "@@ -225,3 +227,5 @@ jsdiff deviates from the published algorithm in a couple of ways that don't affe\n" +
57 |         ' \n' +
58 |         " * jsdiff keeps track of the diff for each diagonal using a linked list of change objects for each diagonal, rather than the historical array of furthest-reaching D-paths on each diagonal contemplated on page 8 of Myers's paper.\n" +
59 |         ' * jsdiff skips considering diagonals where the furthest-reaching D-path would go off the edge of the edit graph. This dramatically reduces the time cost (from quadratic to linear) in cases where the new text just appends or truncates content at the end of the old text.\n' +
60 |         '+\n' +
61 |         '+bar\n'
62 |       );
63 |       expect(formatPatch(reversePatch(patch))).to.equal(
64 |         '===================================================================\n' +
65 |         '--- b/README.md\t\n' +
66 |         '+++ a/README.md\t\n' +
67 |         '@@ -1,7 +1,5 @@\n' +
68 |         ' # jsdiff\n' +
69 |         ' \n' +
70 |         '-foo\n' +
71 |         '-\n' +
72 |         ' [![Build Status](https://secure.travis-ci.org/kpdecker/jsdiff.svg)](http://travis-ci.org/kpdecker/jsdiff)\n' +
73 |         ' [![Sauce Test Status](https://saucelabs.com/buildstatus/jsdiff)](https://saucelabs.com/u/jsdiff)\n' +
74 |         ' \n' +
75 |         '@@ -227,5 +225,3 @@\n' +
76 |         ' \n' +
77 |         " * jsdiff keeps track of the diff for each diagonal using a linked list of change objects for each diagonal, rather than the historical array of furthest-reaching D-paths on each diagonal contemplated on page 8 of Myers's paper.\n" +
78 |         ' * jsdiff skips considering diagonals where the furthest-reaching D-path would go off the edge of the edit graph. This dramatically reduces the time cost (from quadratic to linear) in cases where the new text just appends or truncates content at the end of the old text.\n' +
79 |         '-\n' +
80 |         '-bar\n' +
81 |         '\n' +
82 |         '===================================================================\n' +
83 |         '--- b/CONTRIBUTING.md\t\n' +
84 |         '+++ a/CONTRIBUTING.md\t\n' +
85 |         '@@ -2,8 +2,6 @@\n' +
86 |         ' \n' +
87 |         ' ## Pull Requests\n' +
88 |         ' \n' +
89 |         '-bla bla bla\n' +
90 |         '-\n' +
91 |         ' We also accept [pull requests][pull-request]!\n' +
92 |         ' \n' +
93 |         ' Generally we like to see pull requests that\n'
94 |       );
95 |     });
96 |   });
97 | });
98 | 


--------------------------------------------------------------------------------
/test/util/string.js:
--------------------------------------------------------------------------------
 1 | import {longestCommonPrefix, longestCommonSuffix, replacePrefix, replaceSuffix, removePrefix, removeSuffix, maximumOverlap} from '../../libesm/util/string.js';
 2 | import {expect} from 'chai';
 3 | 
 4 | describe('#longestCommonPrefix', function() {
 5 |   it('finds the longest common prefix', function() {
 6 |     expect(longestCommonPrefix('food', 'foolish')).to.equal('foo');
 7 |     expect(longestCommonPrefix('foolish', 'food')).to.equal('foo');
 8 |     expect(longestCommonPrefix('foolish', 'foo')).to.equal('foo');
 9 |     expect(longestCommonPrefix('foo', 'foolish')).to.equal('foo');
10 |     expect(longestCommonPrefix('foo', '')).to.equal('');
11 |     expect(longestCommonPrefix('', 'foo')).to.equal('');
12 |     expect(longestCommonPrefix('', '')).to.equal('');
13 |     expect(longestCommonPrefix('foo', 'bar')).to.equal('');
14 |   });
15 | });
16 | 
17 | describe('#longestCommonSuffix', function() {
18 |   it('finds the longest common suffix', function() {
19 |     expect(longestCommonSuffix('bumpy', 'grumpy')).to.equal('umpy');
20 |     expect(longestCommonSuffix('grumpy', 'bumpy')).to.equal('umpy');
21 |     expect(longestCommonSuffix('grumpy', 'umpy')).to.equal('umpy');
22 |     expect(longestCommonSuffix('umpy', 'grumpy')).to.equal('umpy');
23 |     expect(longestCommonSuffix('foo', '')).to.equal('');
24 |     expect(longestCommonSuffix('', 'foo')).to.equal('');
25 |     expect(longestCommonSuffix('', '')).to.equal('');
26 |     expect(longestCommonSuffix('foo', 'bar')).to.equal('');
27 |   });
28 | });
29 | 
30 | describe('#replacePrefix', function() {
31 |   it('replaces a prefix on a string with a different prefix', function() {
32 |     expect((replacePrefix('food', 'foo', 'gla'))).to.equal('glad');
33 |     expect((replacePrefix('food', '', 'good '))).to.equal('good food');
34 |   });
35 | 
36 |   it("throws if the prefix to remove isn't present", function() {
37 |     // eslint-disable-next-line dot-notation
38 |     expect(() => replacePrefix('food', 'drin', 'goo')).to.throw();
39 |   });
40 | });
41 | 
42 | describe('#replaceSuffix', function() {
43 |   it('replaces a suffix on a string with a different suffix', function() {
44 |     expect((replaceSuffix('bangle', 'gle', 'jo'))).to.equal('banjo');
45 |     expect((replaceSuffix('bun', '', 'gle'))).to.equal('bungle');
46 |   });
47 | 
48 |   it("throws if the suffix to remove isn't present", function() {
49 |     // eslint-disable-next-line dot-notation
50 |     expect(() => replaceSuffix('food', 'ool', 'ondle')).to.throw();
51 |   });
52 | });
53 | 
54 | describe('#removePrefix', function() {
55 |   it('removes a prefix', function() {
56 |     expect(removePrefix('inconceivable', 'in')).to.equal('conceivable');
57 |     expect(removePrefix('inconceivable', '')).to.equal('inconceivable');
58 |     expect(removePrefix('inconceivable', 'inconceivable')).to.equal('');
59 |   });
60 | 
61 |   it("throws if the prefix to remove isn't present", function() {
62 |     // eslint-disable-next-line dot-notation
63 |     expect(() => removePrefix('food', 'dr')).to.throw();
64 |   });
65 | });
66 | 
67 | describe('#removeSuffix', function() {
68 |   it('removes a suffix', function() {
69 |     expect(removeSuffix('counterfactual', 'factual')).to.equal('counter');
70 |     expect(removeSuffix('counterfactual', '')).to.equal('counterfactual');
71 |     expect(removeSuffix('counterfactual', 'counterfactual')).to.equal('');
72 |   });
73 | 
74 |   it("throws if the suffix to remove isn't present", function() {
75 |     // eslint-disable-next-line dot-notation
76 |     expect(() => removeSuffix('food', 'dr')).to.throw();
77 |   });
78 | });
79 | 
80 | describe('#maximumOverlap', function() {
81 |   it('finds the maximum overlap between the end of one string and the start of the other', function() {
82 |     expect(maximumOverlap('qwertyuiop', 'uiopasdfgh')).to.equal('uiop');
83 |     expect(maximumOverlap('qwertyuiop', 'qwertyuiop')).to.equal('qwertyuiop');
84 |     expect(maximumOverlap('qwertyuiop', 'asdfghjkl')).to.equal('');
85 |     expect(maximumOverlap('qwertyuiop', '')).to.equal('');
86 |     expect(maximumOverlap('uiopasdfgh', 'qwertyuiop')).to.equal('');
87 |     expect(maximumOverlap('x   ', '  x')).to.equal('  ');
88 |     expect(maximumOverlap('', '')).to.equal('');
89 |   });
90 | });
91 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "include": ["src/*.ts", "src/**/*.ts"],
 3 |     "compilerOptions": {
 4 |         "rootDir": "src/",
 5 |         "target": "es5",
 6 |         "lib": [
 7 |             "es2022"
 8 |         ],
 9 |         "declaration": true,
10 |         "skipLibCheck": true,
11 | 
12 |         // Options below enabled per recommendation at
13 |         // https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html#im-writing-a-library
14 |         "strict": true,
15 |         "declarationMap": true,
16 |         // The same docs page recommends setting
17 |         // "verbatimModuleSyntax": true,
18 |         // but this totally breaks the CJS build and a footnote observes that "Any configuration
19 |         // that produces both an ESM and a CJS output from the same source file is fundamentally 
20 |         // incompatible with" this setting. I don't really know WTF the authors of those docs were
21 |         // thinking, because the recommendation to turn this setting on is from the section
22 |         // specifically for "a library author" who wants to ensure their code works "under all
23 |         // possible library consumer compilation settings" - i.e. a person who is essentially 100%
24 |         // guaranteed to be running both an ESM and CJS build. So how can the recommendation to
25 |         // turn this setting on possible be even remotely sane? Beats me. ¯\_(ツ)_/¯
26 |         // I've done the best I can by using the @typescript-eslint/consistent-type-imports and
27 |         // @typescript-eslint/consistent-type-exports ESLint rules to enforce SOME of what this
28 |         // setting would have enforced, though I dunno if I'm enforcing the bits that motivated the
29 |         // recommendation to turn it on.
30 |     }
31 | }
32 | 


--------------------------------------------------------------------------------