├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── RELEASE.md ├── index.d.ts ├── index.mjs ├── package.json └── test ├── LCS.test.js ├── diff3Merge.test.js ├── diff3MergeRegions.test.js ├── diffComm.test.js ├── diffIndices.test.js ├── diffPatch.test.js ├── merge.test.js ├── mergeDiff3.test.js └── mergeDigIn.test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 6, 8 | "sourceType": "module" 9 | }, 10 | "extends": [ 11 | "eslint:recommended" 12 | ], 13 | "rules": { 14 | "dot-notation": "error", 15 | "eqeqeq": ["error", "smart"], 16 | "indent": ["off", 4], 17 | "keyword-spacing": "error", 18 | "linebreak-style": ["error", "unix"], 19 | "no-caller": "error", 20 | "no-catch-shadow": "error", 21 | "no-console": "warn", 22 | "no-div-regex": "error", 23 | "no-extend-native": "error", 24 | "no-extra-bind": "error", 25 | "no-floating-decimal": "error", 26 | "no-implied-eval": "error", 27 | "no-invalid-this": "error", 28 | "no-iterator": "error", 29 | "no-labels": "error", 30 | "no-label-var": "error", 31 | "no-lone-blocks": "error", 32 | "no-loop-func": "error", 33 | "no-multi-str": "error", 34 | "no-native-reassign": "error", 35 | "no-new": "error", 36 | "no-new-func": "error", 37 | "no-new-wrappers": "error", 38 | "no-octal": "error", 39 | "no-octal-escape": "error", 40 | "no-process-env": "error", 41 | "no-proto": "error", 42 | "no-return-assign": "off", 43 | "no-script-url": "error", 44 | "no-self-compare": "error", 45 | "no-sequences": "error", 46 | "no-shadow": "off", 47 | "no-shadow-restricted-names": "error", 48 | "no-throw-literal": "error", 49 | "no-unneeded-ternary": "error", 50 | "no-unused-expressions": "error", 51 | "no-unexpected-multiline": "error", 52 | "no-unused-vars": "warn", 53 | "no-void": "error", 54 | "no-warning-comments": "warn", 55 | "no-with": "error", 56 | "no-use-before-define": ["off", "nofunc"], 57 | "semi": ["error", "always"], 58 | "semi-spacing": "error", 59 | "space-unary-ops": "error", 60 | "wrap-regex": "off", 61 | "quotes": ["error", "single"] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "npm" 7 | directory: "/" 8 | schedule: 9 | interval: "monthly" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | FORCE_COLOR: 2 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | node-version: ['18', '20', '22'] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm run all 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .esm-cache 3 | .vscode 4 | .coverage 5 | 6 | .nyc_output/ 7 | built/ 8 | coverage/ 9 | dist/ 10 | node_modules/ 11 | 12 | npm-debug.log 13 | lerna-debug.log 14 | package-lock.json 15 | yarn.lock 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # What's New 2 | 3 | **node-diff3** is an open source project. You can submit bug reports, help out, 4 | or learn more by visiting our project page on GitHub: :octocat: https://github.com/bhousel/node-diff3 5 | 6 | Please star our project on GitHub to show your support! ⭐️ 7 | 8 | _Breaking changes, which may affect downstream projects, are marked with a_ ⚠️ 9 | 10 | 17 | 18 | ## 3.1.2 19 | ##### 2022-Jun-06 20 | 21 | * Rearrange "default" export to end of list for Webpack ([#58]) 22 | 23 | [#58]: https://github.com/bhousel/node-diff3/issues/58 24 | 25 | 26 | ## 3.1.1 27 | ##### 2022-Jun-03 28 | 29 | * Fix exports property for Typescript declaration file ([#57]) 30 | 31 | [#57]: https://github.com/bhousel/node-diff3/issues/57 32 | 33 | 34 | ## 3.1.0 35 | ##### 2021-Sep-24 36 | 37 | * Add `sideEffects: false` to `package.json` so bundlers like webpack can treeshake 38 | * Remove the hardcoded `\n` from conflict boundaries ([#46], [#48]) 39 | * Users who want to view the results of a merge will probably do something like `console.log(result.join('\n'));`, so having extra `\n` in there is unhelpful. 40 | 41 | [#46]: https://github.com/bhousel/node-diff3/issues/46 42 | [#48]: https://github.com/bhousel/node-diff3/issues/48 43 | 44 | 45 | ## 3.0.0 46 | ##### 2021-Jun-26 47 | 48 | * ⚠️ Replace rollup with [esbuild](https://esbuild.github.io/) for super fast build speed. Package outputs are now: 49 | * `"module": "./index.mjs"` - ESM, modern JavaScript, works with `import` 50 | * `"main": "./dist/index.cjs"` - CJS bundle, modern JavaScript, works with `require()` 51 | * `"browser": "./dist/index.iife.js"` - IIFE bundle, modern JavaScript, works in browser ` 43 | 44 | … 45 | 51 | ``` 52 | 53 | 👉 This project uses modern JavaScript syntax for use in supported node versions and modern browsers. If you need support for legacy environments like ES5 or Internet Explorer, you'll need to build your own bundle with something like [Babel](https://babeljs.io/docs/en/index.html). 54 | 55 |   56 | 57 | ## API Reference 58 | 59 | * [3-way diff and merging](#3-way-diff-and-merging) 60 | * [diff3Merge](#diff3Merge) 61 | * [merge](#merge) 62 | * [mergeDiff3](#mergeDiff3) 63 | * [mergeDigIn](#mergeDigIn) 64 | * [diff3MergeRegions](#diff3MergeRegions) 65 | * [2-way diff and patching](#2-way-diff-and-patching) 66 | * [diffPatch](#diffPatch) 67 | * [patch](#patch) 68 | * [stripPatch](#stripPatch) 69 | * [invertPatch](#invertPatch) 70 | * [diffComm](#diffComm) 71 | * [diffIndices](#diffIndices) 72 | * [Longest Common Sequence (LCS)](#longest-common-sequence-lcs) 73 | * [LCS](#LCS) 74 | 75 |   76 | 77 | ### 3-way diff and merging 78 | 79 |   80 | 81 | # Diff3.diff3Merge(a, o, b, options) 82 | 83 | Performs a 3-way diff on buffers `o` (original), and `a` and `b` (changed). 84 | The buffers may be arrays or strings. If strings, they will be split into arrays on whitespace `/\s+/` by default. 85 | The returned result alternates between "ok" and "conflict" blocks. 86 | 87 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diff3Merge.test.js 88 | 89 | ```js 90 | const o = ['AA', 'ZZ', '00', 'M', '99']; 91 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 92 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 93 | const result = Diff3.diff3Merge(a, o, b); 94 | ``` 95 | 96 | Options may passed as an object: 97 | ```js 98 | { 99 | excludeFalseConflicts: true, 100 | stringSeparator: /\s+/ 101 | } 102 | ``` 103 | 104 | * `excludeFalseConflicts` - If both `a` and `b` contain an identical change from `o`, this is considered a "false" conflict. 105 | * `stringSeparator` - If inputs buffers are strings, this controls how to split the strings into arrays. The separator value may be a string or a regular expression, as it is just passed to [String.split()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split). 106 | 107 |   108 | 109 | # Diff3.merge(a, o, b, options) 110 | 111 | Passes arguments to [diff3Merge](#diff3Merge) to generate a diff3-style merge result. 112 | 113 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/merge.test.js 114 | 115 | ```js 116 | const r = Diff3.merge(a, o, b); 117 | const result = r.result; 118 | // [ 119 | // 'AA', 120 | // '<<<<<<<', 121 | // 'a', 122 | // 'b', 123 | // 'c', 124 | // '=======', 125 | // 'a', 126 | // 'd', 127 | // 'c', 128 | // '>>>>>>>', 129 | // 'ZZ', 130 | // '<<<<<<<', 131 | // 'new', 132 | // '00', 133 | // 'a', 134 | // 'a', 135 | // '=======', 136 | // '11', 137 | // '>>>>>>>', 138 | // 'M', 139 | // 'z', 140 | // 'z', 141 | // '99' 142 | // ] 143 | ``` 144 | 145 |   146 | 147 | # Diff3.mergeDiff3(a, o, b, options) 148 | 149 | Passes arguments to [diff3Merge](#diff3Merge) to generate a diff3-style merge result with original (similar to [git-diff3](https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging)). 150 | 151 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/mergeDiff3.test.js 152 | 153 | ```js 154 | const r = Diff3.mergeDiff3(a, o, b, { label: { a: 'a', o: 'o', b: 'b' } }); 155 | const result = r.result; 156 | // [ 157 | // 'AA', 158 | // '<<<<<<< a', 159 | // 'a', 160 | // 'b', 161 | // 'c', 162 | // '||||||| o', 163 | // '=======', 164 | // 'a', 165 | // 'd', 166 | // 'c', 167 | // '>>>>>>> b', 168 | // 'ZZ', 169 | // '<<<<<<< a', 170 | // 'new', 171 | // '00', 172 | // 'a', 173 | // 'a', 174 | // '||||||| o', 175 | // '00', 176 | // '=======', 177 | // '11', 178 | // '>>>>>>> b', 179 | // 'M', 180 | // 'z', 181 | // 'z', 182 | // '99' 183 | // ] 184 | ``` 185 | 186 | Extra options: 187 | ```js 188 | { 189 | // labels for conflict marker lines 190 | label: { 191 | a: 'a', 192 | o: 'o', 193 | b: 'b' 194 | }, 195 | } 196 | ``` 197 | 198 |   199 | 200 | # Diff3.mergeDigIn(a, o, b, options) 201 | 202 | Passes arguments to [diff3Merge](#diff3Merge) to generate a digin-style merge result. 203 | 204 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/mergeDigIn.test.js 205 | 206 |   207 | 208 | # Diff3.diff3MergeRegions(a, o, b) 209 | 210 | Low-level function used by [diff3Merge](#diff3Merge) to determine the stable and unstable regions between `a`, `o`, `b`. 211 | 212 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diff3MergeRegions.test.js 213 | 214 | 215 |   216 | 217 | ### 2-way diff and patching 218 | 219 |   220 | 221 | # Diff3.diffPatch(buffer1, buffer2) 222 | 223 | Performs a diff between arrays `buffer1` and `buffer2`. 224 | The returned `patch` result contains the information about the differing regions and can be applied to `buffer1` to yield `buffer2`. 225 | 226 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffPatch.test.js 227 | 228 | ```js 229 | const buffer1 = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 230 | const buffer2 = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 231 | const patch = Diff3.diffPatch(buffer1, buffer2); 232 | // `patch` contains the information needed to turn `buffer1` into `buffer2` 233 | ``` 234 | 235 |   236 | 237 | # Diff3.patch(buffer1, patch) 238 | 239 | Applies a patch to a buffer, returning a new buffer without modifying the original. 240 | 241 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffPatch.test.js 242 | 243 | ```js 244 | const result = Diff3.patch(buffer1, patch); 245 | // `result` contains a new arrray which is a copy of `buffer2` 246 | ``` 247 | 248 |   249 | 250 | # Diff3.stripPatch(patch) 251 | 252 | Strips some extra information from the patch, returning a new patch without modifying the original. 253 | The "stripped" patch can still patch `buffer1` -> `buffer2`, but can no longer be inverted. 254 | 255 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffPatch.test.js 256 | 257 | ```js 258 | const stripped = Diff3.stripPatch(patch); 259 | // `stripped` contains a copy of a patch but with the extra information removed 260 | ``` 261 | 262 |   263 | 264 | # Diff3.invertPatch(patch) 265 | 266 | Inverts the patch (for example to turn `buffer2` back into `buffer1`), returning a new patch without modifying the original. 267 | 268 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffPatch.test.js 269 | 270 | ```js 271 | const inverted = Diff3.invertPatch(patch); 272 | // `inverted` contains a copy of a patch to turn `buffer2` back into `buffer1` 273 | ``` 274 | 275 |   276 | 277 | # Diff3.diffComm(buffer1, buffer2) 278 | 279 | Returns a comm-style result of the differences between `buffer1` and `buffer2`. 280 | 281 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffComm.test.js 282 | 283 |   284 | 285 | # Diff3.diffIndices(buffer1, buffer2) 286 | 287 | Low-level function used by [diff3MergeRegions](#diff3MergeRegions) to determine differing regions between `buffer1` and `buffer2`. 288 | 289 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffIndices.test.js 290 | 291 | 292 |   293 | 294 | ### Longest Common Sequence (LCS) 295 | 296 |   297 | 298 | # Diff3.LCS(buffer1, buffer2) 299 | 300 | Low-level function used by other functions to find the LCS between `buffer1` and `buffer2`. 301 | Returns a result linked list chain containing the common sequence path. 302 | 303 | See also: 304 | * http://www.cs.dartmouth.edu/~doug/ 305 | * https://en.wikipedia.org/wiki/Longest_common_subsequence_problem 306 | 307 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/LCS.test.js 308 | 309 | 310 |   311 | 312 | ## License 313 | 314 | This project is available under the [MIT License](https://opensource.org/licenses/MIT). 315 | See the [LICENSE.md](LICENSE.md) file for more details. 316 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ## Release Checklist 2 | 3 | #### Update version, tag, and publish 4 | ```bash 5 | $ git checkout main 6 | $ npm install 7 | $ npm run test 8 | $ Update CHANGELOG 9 | $ Update version number in `package.json` 10 | $ git add . 11 | $ git commit -m 'vA.B.C' 12 | $ git tag vA.B.C 13 | $ git push origin main vA.B.C 14 | $ npm publish 15 | 16 | ``` 17 | * Open https://github.com/bhousel/node-diff3/tags 18 | * Click "Add Release Notes" and link to the CHANGELOG#version 19 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export interface ILCSResult { 2 | buffer1index: number; 3 | buffer2index: number; 4 | chain: null | ILCSResult; 5 | } 6 | 7 | /** 8 | * Expects two arrays, finds longest common sequence 9 | * @param {T[]} buffer1 10 | * @param {T[]} buffer2 11 | * @returns {ILCSResult} 12 | * @constructor 13 | */ 14 | export function LCS(buffer1: T[], buffer2: T[]): ILCSResult; 15 | 16 | export interface ICommResult { 17 | buffer1: T[]; 18 | buffer2: T[]; 19 | } 20 | 21 | /** 22 | * We apply the LCS to build a 'comm'-style picture of the 23 | * differences between buffer1 and buffer2. 24 | * @param {T[]} buffer1 25 | * @param {T[]} buffer2 26 | * @returns {Array>} 27 | */ 28 | export function diffComm(buffer1: T[], buffer2: T[]): ICommResult[]; 29 | 30 | export interface IDiffIndicesResult { 31 | buffer1: [number, number]; 32 | buffer1Content: T[]; 33 | buffer2: [number, number]; 34 | buffer2Content: T[]; 35 | } 36 | 37 | /** 38 | * We apply the LCS to give a simple representation of the 39 | * offsets and lengths of mismatched chunks in the input 40 | * buffers. This is used by diff3MergeRegions. 41 | * @param {T[]} buffer1 42 | * @param {T[]} buffer2 43 | * @returns {IDiffIndicesResult[]} 44 | */ 45 | export function diffIndices( 46 | buffer1: T[], 47 | buffer2: T[] 48 | ): IDiffIndicesResult[]; 49 | 50 | export interface IChunk { 51 | offset: number; 52 | length: number; 53 | chunk: T[]; 54 | } 55 | 56 | export interface IPatchRes { 57 | buffer1: IChunk; 58 | buffer2: IChunk; 59 | } 60 | 61 | /** 62 | * We apply the LCS to build a JSON representation of a 63 | * diff(1)-style patch. 64 | * @param {T[]} buffer1 65 | * @param {T[]} buffer2 66 | * @returns {IPatchRes[]} 67 | */ 68 | export function diffPatch(buffer1: T[], buffer2: T[]): IPatchRes[]; 69 | 70 | export function patch(buffer: T[], patch: IPatchRes[]): T[]; 71 | 72 | export interface IStableRegion { 73 | stable: true; 74 | buffer: 'a' | 'o' | 'b'; 75 | bufferStart: number; 76 | bufferLength: number; 77 | bufferContent: T[]; 78 | } 79 | 80 | export interface IUnstableRegion { 81 | stable: false; 82 | aStart: number; 83 | aLength: number; 84 | aContent: T[]; 85 | bStart: number; 86 | bLength: number; 87 | bContent: T[]; 88 | oStart: number; 89 | oLength: number; 90 | oContent: T[]; 91 | } 92 | 93 | export type IRegion = IStableRegion | IUnstableRegion; 94 | 95 | /** 96 | * Given three buffers, A, O, and B, where both A and B are 97 | * independently derived from O, returns a fairly complicated 98 | * internal representation of merge decisions it's taken. The 99 | * interested reader may wish to consult 100 | * 101 | * Sanjeev Khanna, Keshav Kunal, and Benjamin C. Pierce. 102 | * 'A Formal Investigation of ' In Arvind and Prasad, 103 | * editors, Foundations of Software Technology and Theoretical 104 | * Computer Science (FSTTCS), December 2007. 105 | * 106 | * (http://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf) 107 | * 108 | * @param {T[]} a 109 | * @param {T[]} o 110 | * @param {T[]} b 111 | * @returns {IRegion[]} 112 | */ 113 | export function diff3MergeRegions(a: T[], o: T[], b: T[]): IRegion[]; 114 | 115 | export interface MergeRegion { 116 | ok?: T[]; 117 | conflict?: { 118 | a: T[]; 119 | aIndex: number; 120 | b: T[]; 121 | bIndex: number; 122 | o: T[]; 123 | oIndex: number; 124 | }; 125 | } 126 | 127 | export interface MergeResult { 128 | conflict: boolean; 129 | result: string[]; 130 | } 131 | 132 | export interface IMergeOptions { 133 | excludeFalseConflicts?: boolean; 134 | stringSeparator?: string | RegExp; 135 | } 136 | 137 | /** 138 | * Applies the output of diff3MergeRegions to actually 139 | * construct the merged buffer; the returned result alternates 140 | * between 'ok' and 'conflict' blocks. 141 | * A "false conflict" is where `a` and `b` both change the same from `o` 142 | * 143 | * @param {string | T[]} a 144 | * @param {string | T[]} o 145 | * @param {string | T[]} b 146 | * @param {{excludeFalseConflicts: boolean; stringSeparator: RegExp}} options 147 | * @returns {MergeRegion[]} 148 | */ 149 | export function diff3Merge( 150 | a: string | T[], 151 | o: string | T[], 152 | b: string | T[], 153 | options?: IMergeOptions 154 | ): MergeRegion[]; 155 | 156 | export function merge( 157 | a: string | T[], 158 | o: string | T[], 159 | b: string | T[], 160 | options?: IMergeOptions 161 | ): MergeResult; 162 | 163 | export function mergeDiff3( 164 | a: string | T[], 165 | o: string | T[], 166 | b: string | T[], 167 | options?: IMergeOptions & { 168 | label?: { 169 | a?: string; 170 | o?: string; 171 | b?: string; 172 | } 173 | } 174 | ): MergeResult; 175 | 176 | export function mergeDigIn( 177 | a: string | T[], 178 | o: string | T[], 179 | b: string | T[], 180 | options?: IMergeOptions 181 | ): MergeResult; 182 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | export { 2 | LCS, 3 | diffComm, 4 | diffIndices, 5 | diffPatch, 6 | diff3MergeRegions, 7 | diff3Merge, 8 | mergeDiff3, 9 | merge, 10 | mergeDigIn, 11 | patch, 12 | stripPatch, 13 | invertPatch 14 | }; 15 | 16 | 17 | // Text diff algorithm following Hunt and McIlroy 1976. 18 | // J. W. Hunt and M. D. McIlroy, An algorithm for differential buffer 19 | // comparison, Bell Telephone Laboratories CSTR #41 (1976) 20 | // http://www.cs.dartmouth.edu/~doug/ 21 | // https://en.wikipedia.org/wiki/Longest_common_subsequence_problem 22 | // 23 | // Expects two arrays, finds longest common sequence 24 | function LCS(buffer1, buffer2) { 25 | 26 | let equivalenceClasses = {}; 27 | for (let j = 0; j < buffer2.length; j++) { 28 | const item = buffer2[j]; 29 | if (equivalenceClasses[item]) { 30 | equivalenceClasses[item].push(j); 31 | } else { 32 | equivalenceClasses[item] = [j]; 33 | } 34 | } 35 | 36 | const NULLRESULT = { buffer1index: -1, buffer2index: -1, chain: null }; 37 | let candidates = [NULLRESULT]; 38 | 39 | for (let i = 0; i < buffer1.length; i++) { 40 | const item = buffer1[i]; 41 | const buffer2indices = equivalenceClasses[item] || []; 42 | let r = 0; 43 | let c = candidates[0]; 44 | 45 | for (let jx = 0; jx < buffer2indices.length; jx++) { 46 | const j = buffer2indices[jx]; 47 | 48 | let s; 49 | for (s = r; s < candidates.length; s++) { 50 | if ((candidates[s].buffer2index < j) && ((s === candidates.length - 1) || (candidates[s + 1].buffer2index > j))) { 51 | break; 52 | } 53 | } 54 | 55 | if (s < candidates.length) { 56 | const newCandidate = { buffer1index: i, buffer2index: j, chain: candidates[s] }; 57 | if (r === candidates.length) { 58 | candidates.push(c); 59 | } else { 60 | candidates[r] = c; 61 | } 62 | r = s + 1; 63 | c = newCandidate; 64 | if (r === candidates.length) { 65 | break; // no point in examining further (j)s 66 | } 67 | } 68 | } 69 | 70 | candidates[r] = c; 71 | } 72 | 73 | // At this point, we know the LCS: it's in the reverse of the 74 | // linked-list through .chain of candidates[candidates.length - 1]. 75 | 76 | return candidates[candidates.length - 1]; 77 | } 78 | 79 | 80 | // We apply the LCS to build a 'comm'-style picture of the 81 | // differences between buffer1 and buffer2. 82 | function diffComm(buffer1, buffer2) { 83 | const lcs = LCS(buffer1, buffer2); 84 | let result = []; 85 | let tail1 = buffer1.length; 86 | let tail2 = buffer2.length; 87 | let common = {common: []}; 88 | 89 | function processCommon() { 90 | if (common.common.length) { 91 | common.common.reverse(); 92 | result.push(common); 93 | common = {common: []}; 94 | } 95 | } 96 | 97 | for (let candidate = lcs; candidate !== null; candidate = candidate.chain) { 98 | let different = {buffer1: [], buffer2: []}; 99 | 100 | while (--tail1 > candidate.buffer1index) { 101 | different.buffer1.push(buffer1[tail1]); 102 | } 103 | 104 | while (--tail2 > candidate.buffer2index) { 105 | different.buffer2.push(buffer2[tail2]); 106 | } 107 | 108 | if (different.buffer1.length || different.buffer2.length) { 109 | processCommon(); 110 | different.buffer1.reverse(); 111 | different.buffer2.reverse(); 112 | result.push(different); 113 | } 114 | 115 | if (tail1 >= 0) { 116 | common.common.push(buffer1[tail1]); 117 | } 118 | } 119 | 120 | processCommon(); 121 | 122 | result.reverse(); 123 | return result; 124 | } 125 | 126 | 127 | // We apply the LCS to give a simple representation of the 128 | // offsets and lengths of mismatched chunks in the input 129 | // buffers. This is used by diff3MergeRegions. 130 | function diffIndices(buffer1, buffer2) { 131 | const lcs = LCS(buffer1, buffer2); 132 | let result = []; 133 | let tail1 = buffer1.length; 134 | let tail2 = buffer2.length; 135 | 136 | for (let candidate = lcs; candidate !== null; candidate = candidate.chain) { 137 | const mismatchLength1 = tail1 - candidate.buffer1index - 1; 138 | const mismatchLength2 = tail2 - candidate.buffer2index - 1; 139 | tail1 = candidate.buffer1index; 140 | tail2 = candidate.buffer2index; 141 | 142 | if (mismatchLength1 || mismatchLength2) { 143 | result.push({ 144 | buffer1: [tail1 + 1, mismatchLength1], 145 | buffer1Content: buffer1.slice(tail1 + 1, tail1 + 1 + mismatchLength1), 146 | buffer2: [tail2 + 1, mismatchLength2], 147 | buffer2Content: buffer2.slice(tail2 + 1, tail2 + 1 + mismatchLength2) 148 | }); 149 | } 150 | } 151 | 152 | result.reverse(); 153 | return result; 154 | } 155 | 156 | 157 | // We apply the LCS to build a JSON representation of a 158 | // diff(1)-style patch. 159 | function diffPatch(buffer1, buffer2) { 160 | const lcs = LCS(buffer1, buffer2); 161 | let result = []; 162 | let tail1 = buffer1.length; 163 | let tail2 = buffer2.length; 164 | 165 | function chunkDescription(buffer, offset, length) { 166 | let chunk = []; 167 | for (let i = 0; i < length; i++) { 168 | chunk.push(buffer[offset + i]); 169 | } 170 | return { 171 | offset: offset, 172 | length: length, 173 | chunk: chunk 174 | }; 175 | } 176 | 177 | for (let candidate = lcs; candidate !== null; candidate = candidate.chain) { 178 | const mismatchLength1 = tail1 - candidate.buffer1index - 1; 179 | const mismatchLength2 = tail2 - candidate.buffer2index - 1; 180 | tail1 = candidate.buffer1index; 181 | tail2 = candidate.buffer2index; 182 | 183 | if (mismatchLength1 || mismatchLength2) { 184 | result.push({ 185 | buffer1: chunkDescription(buffer1, candidate.buffer1index + 1, mismatchLength1), 186 | buffer2: chunkDescription(buffer2, candidate.buffer2index + 1, mismatchLength2) 187 | }); 188 | } 189 | } 190 | 191 | result.reverse(); 192 | return result; 193 | } 194 | 195 | 196 | // Given three buffers, A, O, and B, where both A and B are 197 | // independently derived from O, returns a fairly complicated 198 | // internal representation of merge decisions it's taken. The 199 | // interested reader may wish to consult 200 | // 201 | // Sanjeev Khanna, Keshav Kunal, and Benjamin C. Pierce. 202 | // 'A Formal Investigation of ' In Arvind and Prasad, 203 | // editors, Foundations of Software Technology and Theoretical 204 | // Computer Science (FSTTCS), December 2007. 205 | // 206 | // (http://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf) 207 | // 208 | function diff3MergeRegions(a, o, b) { 209 | 210 | // "hunks" are array subsets where `a` or `b` are different from `o` 211 | // https://www.gnu.org/software/diffutils/manual/html_node/diff3-Hunks.html 212 | let hunks = []; 213 | function addHunk(h, ab) { 214 | hunks.push({ 215 | ab: ab, 216 | oStart: h.buffer1[0], 217 | oLength: h.buffer1[1], // length of o to remove 218 | abStart: h.buffer2[0], 219 | abLength: h.buffer2[1] // length of a/b to insert 220 | // abContent: (ab === 'a' ? a : b).slice(h.buffer2[0], h.buffer2[0] + h.buffer2[1]) 221 | }); 222 | } 223 | 224 | diffIndices(o, a).forEach(item => addHunk(item, 'a')); 225 | diffIndices(o, b).forEach(item => addHunk(item, 'b')); 226 | hunks.sort((x,y) => x.oStart - y.oStart); 227 | 228 | let results = []; 229 | let currOffset = 0; 230 | 231 | function advanceTo(endOffset) { 232 | if (endOffset > currOffset) { 233 | results.push({ 234 | stable: true, 235 | buffer: 'o', 236 | bufferStart: currOffset, 237 | bufferLength: endOffset - currOffset, 238 | bufferContent: o.slice(currOffset, endOffset) 239 | }); 240 | currOffset = endOffset; 241 | } 242 | } 243 | 244 | while (hunks.length) { 245 | let hunk = hunks.shift(); 246 | let regionStart = hunk.oStart; 247 | let regionEnd = hunk.oStart + hunk.oLength; 248 | let regionHunks = [hunk]; 249 | advanceTo(regionStart); 250 | 251 | // Try to pull next overlapping hunk into this region 252 | while (hunks.length) { 253 | const nextHunk = hunks[0]; 254 | const nextHunkStart = nextHunk.oStart; 255 | if (nextHunkStart > regionEnd) break; // no overlap 256 | 257 | regionEnd = Math.max(regionEnd, nextHunkStart + nextHunk.oLength); 258 | regionHunks.push(hunks.shift()); 259 | } 260 | 261 | if (regionHunks.length === 1) { 262 | // Only one hunk touches this region, meaning that there is no conflict here. 263 | // Either `a` or `b` is inserting into a region of `o` unchanged by the other. 264 | if (hunk.abLength > 0) { 265 | const buffer = (hunk.ab === 'a' ? a : b); 266 | results.push({ 267 | stable: true, 268 | buffer: hunk.ab, 269 | bufferStart: hunk.abStart, 270 | bufferLength: hunk.abLength, 271 | bufferContent: buffer.slice(hunk.abStart, hunk.abStart + hunk.abLength) 272 | }); 273 | } 274 | } else { 275 | // A true a/b conflict. Determine the bounds involved from `a`, `o`, and `b`. 276 | // Effectively merge all the `a` hunks into one giant hunk, then do the 277 | // same for the `b` hunks; then, correct for skew in the regions of `o` 278 | // that each side changed, and report appropriate spans for the three sides. 279 | let bounds = { 280 | a: [a.length, -1, o.length, -1], 281 | b: [b.length, -1, o.length, -1] 282 | }; 283 | while (regionHunks.length) { 284 | hunk = regionHunks.shift(); 285 | const oStart = hunk.oStart; 286 | const oEnd = oStart + hunk.oLength; 287 | const abStart = hunk.abStart; 288 | const abEnd = abStart + hunk.abLength; 289 | let b = bounds[hunk.ab]; 290 | b[0] = Math.min(abStart, b[0]); 291 | b[1] = Math.max(abEnd, b[1]); 292 | b[2] = Math.min(oStart, b[2]); 293 | b[3] = Math.max(oEnd, b[3]); 294 | } 295 | 296 | const aStart = bounds.a[0] + (regionStart - bounds.a[2]); 297 | const aEnd = bounds.a[1] + (regionEnd - bounds.a[3]); 298 | const bStart = bounds.b[0] + (regionStart - bounds.b[2]); 299 | const bEnd = bounds.b[1] + (regionEnd - bounds.b[3]); 300 | 301 | let result = { 302 | stable: false, 303 | aStart: aStart, 304 | aLength: aEnd - aStart, 305 | aContent: a.slice(aStart, aEnd), 306 | oStart: regionStart, 307 | oLength: regionEnd - regionStart, 308 | oContent: o.slice(regionStart, regionEnd), 309 | bStart: bStart, 310 | bLength: bEnd - bStart, 311 | bContent: b.slice(bStart, bEnd) 312 | }; 313 | results.push(result); 314 | } 315 | currOffset = regionEnd; 316 | } 317 | 318 | advanceTo(o.length); 319 | 320 | return results; 321 | } 322 | 323 | 324 | // Applies the output of diff3MergeRegions to actually 325 | // construct the merged buffer; the returned result alternates 326 | // between 'ok' and 'conflict' blocks. 327 | // A "false conflict" is where `a` and `b` both change the same from `o` 328 | function diff3Merge(a, o, b, options) { 329 | let defaults = { 330 | excludeFalseConflicts: true, 331 | stringSeparator: /\s+/ 332 | }; 333 | options = Object.assign(defaults, options); 334 | 335 | if (typeof a === 'string') a = a.split(options.stringSeparator); 336 | if (typeof o === 'string') o = o.split(options.stringSeparator); 337 | if (typeof b === 'string') b = b.split(options.stringSeparator); 338 | 339 | let results = []; 340 | const regions = diff3MergeRegions(a, o, b); 341 | 342 | let okBuffer = []; 343 | function flushOk() { 344 | if (okBuffer.length) { 345 | results.push({ ok: okBuffer }); 346 | } 347 | okBuffer = []; 348 | } 349 | 350 | function isFalseConflict(a, b) { 351 | if (a.length !== b.length) return false; 352 | for (let i = 0; i < a.length; i++) { 353 | if (a[i] !== b[i]) return false; 354 | } 355 | return true; 356 | } 357 | 358 | regions.forEach(region => { 359 | if (region.stable) { 360 | okBuffer.push(...region.bufferContent); 361 | } else { 362 | if (options.excludeFalseConflicts && isFalseConflict(region.aContent, region.bContent)) { 363 | okBuffer.push(...region.aContent); 364 | } else { 365 | flushOk(); 366 | results.push({ 367 | conflict: { 368 | a: region.aContent, 369 | aIndex: region.aStart, 370 | o: region.oContent, 371 | oIndex: region.oStart, 372 | b: region.bContent, 373 | bIndex: region.bStart 374 | } 375 | }); 376 | } 377 | } 378 | }); 379 | 380 | flushOk(); 381 | return results; 382 | } 383 | 384 | 385 | function mergeDiff3(a, o, b, options) { 386 | const defaults = { 387 | excludeFalseConflicts: true, 388 | stringSeparator: /\s+/, 389 | label: {} 390 | }; 391 | options = Object.assign(defaults, options); 392 | 393 | const aSection = '<<<<<<<' + (options.label.a ? ` ${options.label.a}` : ''); 394 | const oSection = '|||||||' + (options.label.o ? ` ${options.label.o}` : ''); 395 | const xSection = '======='; 396 | const bSection = '>>>>>>>' + (options.label.b ? ` ${options.label.b}` : ''); 397 | 398 | const regions = diff3Merge(a, o, b, options); 399 | let conflict = false; 400 | let result = []; 401 | 402 | regions.forEach(region => { 403 | if (region.ok) { 404 | result = result.concat(region.ok); 405 | } else if (region.conflict) { 406 | conflict = true; 407 | result = result.concat( 408 | [aSection], 409 | region.conflict.a, 410 | [oSection], 411 | region.conflict.o, 412 | [xSection], 413 | region.conflict.b, 414 | [bSection] 415 | ); 416 | } 417 | }); 418 | 419 | return { 420 | conflict: conflict, 421 | result: result 422 | }; 423 | } 424 | 425 | 426 | function merge(a, o, b, options) { 427 | const defaults = { 428 | excludeFalseConflicts: true, 429 | stringSeparator: /\s+/, 430 | label: {} 431 | }; 432 | options = Object.assign(defaults, options); 433 | 434 | const aSection = '<<<<<<<' + (options.label.a ? ` ${options.label.a}` : ''); 435 | const xSection = '======='; 436 | const bSection = '>>>>>>>' + (options.label.b ? ` ${options.label.b}` : ''); 437 | 438 | const regions = diff3Merge(a, o, b, options); 439 | let conflict = false; 440 | let result = []; 441 | 442 | regions.forEach(region => { 443 | if (region.ok) { 444 | result = result.concat(region.ok); 445 | } else if (region.conflict) { 446 | conflict = true; 447 | result = result.concat( 448 | [aSection], 449 | region.conflict.a, 450 | [xSection], 451 | region.conflict.b, 452 | [bSection] 453 | ); 454 | } 455 | }); 456 | 457 | return { 458 | conflict: conflict, 459 | result: result 460 | }; 461 | } 462 | 463 | 464 | function mergeDigIn(a, o, b, options) { 465 | const defaults = { 466 | excludeFalseConflicts: true, 467 | stringSeparator: /\s+/, 468 | label: {} 469 | }; 470 | options = Object.assign(defaults, options); 471 | 472 | const aSection = '<<<<<<<' + (options.label.a ? ` ${options.label.a}` : ''); 473 | const xSection = '======='; 474 | const bSection = '>>>>>>>' + (options.label.b ? ` ${options.label.b}` : ''); 475 | 476 | const regions = diff3Merge(a, o, b, options); 477 | let conflict = false; 478 | let result = []; 479 | 480 | regions.forEach(region => { 481 | if (region.ok) { 482 | result = result.concat(region.ok); 483 | } else { 484 | const c = diffComm(region.conflict.a, region.conflict.b); 485 | for (let j = 0; j < c.length; j++) { 486 | let inner = c[j]; 487 | if (inner.common) { 488 | result = result.concat(inner.common); 489 | } else { 490 | conflict = true; 491 | result = result.concat( 492 | [aSection], 493 | inner.buffer1, 494 | [xSection], 495 | inner.buffer2, 496 | [bSection] 497 | ); 498 | } 499 | } 500 | } 501 | }); 502 | 503 | return { 504 | conflict: conflict, 505 | result: result 506 | }; 507 | } 508 | 509 | 510 | // Applies a patch to a buffer. 511 | // Given buffer1 and buffer2, `patch(buffer1, diffPatch(buffer1, buffer2))` should give buffer2. 512 | function patch(buffer, patch) { 513 | let result = []; 514 | let currOffset = 0; 515 | 516 | function advanceTo(targetOffset) { 517 | while (currOffset < targetOffset) { 518 | result.push(buffer[currOffset]); 519 | currOffset++; 520 | } 521 | } 522 | 523 | for (let chunkIndex = 0; chunkIndex < patch.length; chunkIndex++) { 524 | let chunk = patch[chunkIndex]; 525 | advanceTo(chunk.buffer1.offset); 526 | for (let itemIndex = 0; itemIndex < chunk.buffer2.chunk.length; itemIndex++) { 527 | result.push(chunk.buffer2.chunk[itemIndex]); 528 | } 529 | currOffset += chunk.buffer1.length; 530 | } 531 | 532 | advanceTo(buffer.length); 533 | return result; 534 | } 535 | 536 | 537 | // Takes the output of diffPatch(), and removes extra information from it. 538 | // It can still be used by patch(), below, but can no longer be inverted. 539 | function stripPatch(patch) { 540 | return patch.map(chunk => ({ 541 | buffer1: { offset: chunk.buffer1.offset, length: chunk.buffer1.length }, 542 | buffer2: { chunk: chunk.buffer2.chunk } 543 | })); 544 | } 545 | 546 | 547 | // Takes the output of diffPatch(), and inverts the sense of it, so that it 548 | // can be applied to buffer2 to give buffer1 rather than the other way around. 549 | function invertPatch(patch) { 550 | return patch.map(chunk => ({ 551 | buffer1: chunk.buffer2, 552 | buffer2: chunk.buffer1 553 | })); 554 | } 555 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-diff3", 3 | "version": "3.1.2", 4 | "license": "MIT", 5 | "repository": "github:bhousel/node-diff3", 6 | "description": "A node.js module for text diffing and three-way-merge.", 7 | "contributors": [ 8 | "Bryan Housel (https://github.com/bhousel)" 9 | ], 10 | "keywords": [ 11 | "diff", 12 | "diff3", 13 | "diffutils", 14 | "gnu", 15 | "javascript", 16 | "merge", 17 | "nodejs", 18 | "patch" 19 | ], 20 | "files": [ 21 | "index.mjs", 22 | "index.d.ts", 23 | "dist/" 24 | ], 25 | "type": "module", 26 | "source": "./index.mjs", 27 | "types": "./index.d.ts", 28 | "main": "./dist/index.cjs", 29 | "module": "./index.mjs", 30 | "exports": { 31 | ".": { 32 | "import": { 33 | "types": "./index.d.ts", 34 | "default": "./index.mjs" 35 | }, 36 | "require": "./dist/index.cjs" 37 | } 38 | }, 39 | "scripts": { 40 | "all": "run-s clean test", 41 | "clean": "shx rm -rf dist", 42 | "build": "run-p build:**", 43 | "build:browser": "esbuild ./index.mjs --platform=browser --format=iife --global-name=Diff3 --bundle --sourcemap --outfile=./dist/index.iife.js", 44 | "build:cjs": "esbuild ./index.mjs --platform=node --format=cjs --sourcemap --outfile=./dist/index.cjs", 45 | "lint": "eslint index.mjs test/*.js", 46 | "test": "run-s build test:node", 47 | "test:node": "c8 node --test test/*.js" 48 | }, 49 | "devDependencies": { 50 | "c8": "^10.1.2", 51 | "esbuild": "^0.23.1", 52 | "eslint": "^9.10.0", 53 | "npm-run-all": "^4.1.5", 54 | "shx": "^0.3.4" 55 | }, 56 | "sideEffects": false, 57 | "publishConfig": { 58 | "access": "public" 59 | }, 60 | "engines": { 61 | "node": ">=18" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/LCS.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import * as Diff3 from '../index.mjs'; 4 | 5 | test('LCS', async t => { 6 | 7 | await t.test('returns the LCS of two arrays', t => { 8 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 9 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 10 | const result = Diff3.LCS(a, b); 11 | const NULLRESULT = { buffer1index: -1, buffer2index: -1, chain: null }; 12 | 13 | assert.deepEqual(result.buffer1index, 10); // '99' 14 | assert.deepEqual(result.buffer2index, 9); 15 | 16 | assert.deepEqual(result.chain.buffer1index, 9); // 'M' 17 | assert.deepEqual(result.chain.buffer2index, 6); 18 | 19 | assert.deepEqual(result.chain.chain.buffer1index, 4); // 'ZZ' 20 | assert.deepEqual(result.chain.chain.buffer2index, 4); 21 | 22 | assert.deepEqual(result.chain.chain.chain.buffer1index, 3); // 'c' 23 | assert.deepEqual(result.chain.chain.chain.buffer2index, 3); 24 | 25 | assert.deepEqual(result.chain.chain.chain.chain.buffer1index, 1); // 'a' 26 | assert.deepEqual(result.chain.chain.chain.chain.buffer2index, 1); 27 | 28 | assert.deepEqual(result.chain.chain.chain.chain.chain.buffer1index, 0); // 'AA' 29 | assert.deepEqual(result.chain.chain.chain.chain.chain.buffer2index, 0); 30 | assert.deepEqual(result.chain.chain.chain.chain.chain.chain, NULLRESULT); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/diff3Merge.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import * as Diff3 from '../index.mjs'; 4 | 5 | test('diff3Merge', async t => { 6 | 7 | await t.test('performs diff3 merge on arrays', t => { 8 | const o = ['AA', 'ZZ', '00', 'M', '99']; 9 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 10 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 11 | const result = Diff3.diff3Merge(a, o, b); 12 | 13 | /* 14 | AA 15 | <<<<<<< a 16 | a 17 | b 18 | c 19 | ||||||| o 20 | ======= 21 | a 22 | d 23 | c 24 | >>>>>>> b 25 | ZZ 26 | <<<<<<< a 27 | new 28 | 00 29 | a 30 | a 31 | ||||||| o 32 | 00 33 | ======= 34 | 11 35 | >>>>>>> b 36 | M 37 | z 38 | z 39 | 99 40 | */ 41 | 42 | assert.deepEqual(result[0].ok, ['AA']); 43 | assert.deepEqual(result[0].conflict, undefined); 44 | 45 | assert.deepEqual(result[1].ok, undefined); 46 | assert.deepEqual(result[1].conflict.o, []); 47 | assert.deepEqual(result[1].conflict.a, ['a', 'b', 'c']); 48 | assert.deepEqual(result[1].conflict.b, ['a', 'd', 'c']); 49 | 50 | assert.deepEqual(result[2].ok, ['ZZ']); 51 | assert.deepEqual(result[2].conflict, undefined); 52 | 53 | assert.deepEqual(result[3].ok, undefined); 54 | assert.deepEqual(result[3].conflict.o, ['00']); 55 | assert.deepEqual(result[3].conflict.a, ['new', '00', 'a', 'a']); 56 | assert.deepEqual(result[3].conflict.b, ['11']); 57 | 58 | assert.deepEqual(result[4].ok, ['M', 'z', 'z', '99']); 59 | assert.deepEqual(result[4].conflict, undefined); 60 | }); 61 | 62 | 63 | await t.test('strings split on whitespace by default to avoid surprises - issue #9', t => { 64 | const o = 'was touring'; 65 | const a = 'was here touring'; 66 | const b = 'was into touring'; 67 | const result = Diff3.diff3Merge(a, o, b); 68 | 69 | assert.deepEqual(result[0].ok, ['was']); 70 | assert.deepEqual(result[0].conflict, undefined); 71 | 72 | assert.deepEqual(result[1].ok, undefined); 73 | assert.deepEqual(result[1].conflict.o, []); 74 | assert.deepEqual(result[1].conflict.a, ['here']); 75 | assert.deepEqual(result[1].conflict.b, ['into']); 76 | 77 | assert.deepEqual(result[2].ok, ['touring']); 78 | assert.deepEqual(result[2].conflict, undefined); 79 | }); 80 | 81 | await t.test('strings can optionally split on given separator', t => { 82 | const o = 'new hampshire, new mexico, north carolina'; 83 | const a = 'new hampshire, new jersey, north carolina'; 84 | const b = 'new hampshire, new york, north carolina'; 85 | const result = Diff3.diff3Merge(a, o, b, { stringSeparator: /,\s+/ }); 86 | 87 | assert.deepEqual(result[0].ok, ['new hampshire']); 88 | assert.deepEqual(result[0].conflict, undefined); 89 | 90 | assert.deepEqual(result[1].ok, undefined); 91 | assert.deepEqual(result[1].conflict.o, ['new mexico']); 92 | assert.deepEqual(result[1].conflict.a, ['new jersey']); 93 | assert.deepEqual(result[1].conflict.b, ['new york']); 94 | 95 | assert.deepEqual(result[2].ok, ['north carolina']); 96 | assert.deepEqual(result[2].conflict, undefined); 97 | }); 98 | 99 | 100 | await t.test('excludes false conflicts by default', t => { 101 | const o = 'AA ZZ'; 102 | const a = 'AA a b c ZZ'; 103 | const b = 'AA a b c ZZ'; 104 | const result = Diff3.diff3Merge(a, o, b); 105 | 106 | assert.deepEqual(result[0].ok, ['AA', 'a', 'b', 'c', 'ZZ']); 107 | assert.deepEqual(result[0].conflict, undefined); 108 | }); 109 | 110 | 111 | await t.test('can include false conflicts with option', t => { 112 | const o = 'AA ZZ'; 113 | const a = 'AA a b c ZZ'; 114 | const b = 'AA a b c ZZ'; 115 | const result = Diff3.diff3Merge(a, o, b, { excludeFalseConflicts: false }); 116 | 117 | assert.deepEqual(result[0].ok, ['AA']); 118 | assert.deepEqual(result[0].conflict, undefined); 119 | 120 | assert.deepEqual(result[1].ok, undefined); 121 | assert.deepEqual(result[1].conflict.o, []); 122 | assert.deepEqual(result[1].conflict.a, ['a', 'b', 'c']); 123 | assert.deepEqual(result[1].conflict.b, ['a', 'b', 'c']); 124 | 125 | assert.deepEqual(result[2].ok, ['ZZ']); 126 | assert.deepEqual(result[2].conflict, undefined); 127 | }); 128 | 129 | 130 | await t.test('avoids improper hunk sorting - see openstreetmap/iD#3058', t => { 131 | const o = ['n4100522632', 'n4100697091', 'n4100697136', 'n4102671583', 'n4102671584', 'n4102671585', 'n4102671586', 'n4102671587', 'n4102671588', 'n4102677889', 'n4102677890', 'n4094374176']; 132 | const a = ['n4100522632', 'n4100697091', 'n4100697136', 'n-10000', 'n4102671583', 'n4102671584', 'n4102671585', 'n4102671586', 'n4102671587', 'n4102671588', 'n4102677889', 'n4102677890', 'n4094374176']; 133 | const b = ['n4100522632', 'n4100697091', 'n4100697136', 'n4102671583', 'n4102671584', 'n4102671585', 'n4102671586', 'n4102671587', 'n4102671588', 'n4102677889', 'n4105613618', 'n4102677890', 'n4105613617', 'n4094374176']; 134 | const expected = ['n4100522632', 'n4100697091', 'n4100697136', 'n-10000', 'n4102671583', 'n4102671584', 'n4102671585', 'n4102671586', 'n4102671587', 'n4102671588', 'n4102677889', 'n4105613618', 'n4102677890', 'n4105613617', 'n4094374176']; 135 | const result = Diff3.diff3Merge(a, o, b); 136 | 137 | assert.deepEqual(result[0].ok, expected); 138 | }); 139 | 140 | 141 | await t.test('yaml comparison - issue #46', t => { 142 | const o = `title: "title" 143 | description: "description"`; 144 | const a = `title: "title" 145 | description: "description changed"`; 146 | const b = `title: "title changed" 147 | description: "description"`; 148 | const result = Diff3.diff3Merge(a, o, b, { stringSeparator: /[\r\n]+/ }); 149 | 150 | assert.deepEqual(result[0].ok, undefined); 151 | assert.deepEqual(result[0].conflict.o, ['title: "title"', 'description: "description"']); 152 | assert.deepEqual(result[0].conflict.a, ['title: "title"', 'description: "description changed"']); 153 | assert.deepEqual(result[0].conflict.b, ['title: "title changed"', 'description: "description"']); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/diff3MergeRegions.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import * as Diff3 from '../index.mjs'; 4 | 5 | test('diff3MergeRegions', async t => { 6 | 7 | await t.test('returns results of 3-way diff from o,a,b arrays', t => { 8 | const o = ['AA', 'ZZ', '00', 'M', '99']; 9 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 10 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 11 | const result = Diff3.diff3MergeRegions(a, o, b); 12 | 13 | assert.deepEqual(result[0].stable, true); 14 | assert.deepEqual(result[0].buffer, 'o'); 15 | assert.deepEqual(result[0].bufferStart, 0); 16 | assert.deepEqual(result[0].bufferLength, 1); 17 | assert.deepEqual(result[0].bufferContent, ['AA']); 18 | 19 | assert.deepEqual(result[1].stable, false); 20 | assert.deepEqual(result[1].aStart, 1); 21 | assert.deepEqual(result[1].aLength, 3); 22 | assert.deepEqual(result[1].aContent, ['a', 'b', 'c']); 23 | assert.deepEqual(result[1].oStart, 1); 24 | assert.deepEqual(result[1].oLength, 0); 25 | assert.deepEqual(result[1].oContent, []); 26 | assert.deepEqual(result[1].bStart, 1); 27 | assert.deepEqual(result[1].bLength, 3); 28 | assert.deepEqual(result[1].bContent, ['a', 'd', 'c']); 29 | 30 | assert.deepEqual(result[2].stable, true); 31 | assert.deepEqual(result[2].buffer, 'o'); 32 | assert.deepEqual(result[2].bufferStart, 1); 33 | assert.deepEqual(result[2].bufferLength, 1); 34 | assert.deepEqual(result[2].bufferContent, ['ZZ']); 35 | 36 | assert.deepEqual(result[3].stable, false); 37 | assert.deepEqual(result[3].aStart, 5); 38 | assert.deepEqual(result[3].aLength, 4); 39 | assert.deepEqual(result[3].aContent, ['new', '00', 'a', 'a']); 40 | assert.deepEqual(result[3].oStart, 2); 41 | assert.deepEqual(result[3].oLength, 1); 42 | assert.deepEqual(result[3].oContent, ['00']); 43 | assert.deepEqual(result[3].bStart, 5); 44 | assert.deepEqual(result[3].bLength, 1); 45 | assert.deepEqual(result[3].bContent, ['11']); 46 | 47 | assert.deepEqual(result[4].stable, true); 48 | assert.deepEqual(result[4].buffer, 'o'); 49 | assert.deepEqual(result[4].bufferStart, 3); 50 | assert.deepEqual(result[4].bufferLength, 1); 51 | assert.deepEqual(result[4].bufferContent, ['M']); 52 | 53 | assert.deepEqual(result[5].stable, true); 54 | assert.deepEqual(result[5].buffer, 'b'); 55 | assert.deepEqual(result[5].bufferStart, 7); 56 | assert.deepEqual(result[5].bufferLength, 2); 57 | assert.deepEqual(result[5].bufferContent, ['z', 'z']); 58 | 59 | assert.deepEqual(result[6].stable, true); 60 | assert.deepEqual(result[6].buffer, 'o'); 61 | assert.deepEqual(result[6].bufferStart, 4); 62 | assert.deepEqual(result[6].bufferLength, 1); 63 | assert.deepEqual(result[6].bufferContent, ['99']); 64 | }); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /test/diffComm.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import * as Diff3 from '../index.mjs'; 4 | 5 | test('diffComm', async t => { 6 | 7 | await t.test('returns a comm-style diff of two arrays', t => { 8 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 9 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 10 | const result = Diff3.diffComm(a, b); 11 | 12 | assert.deepEqual(result[0].common, ['AA', 'a']); 13 | assert.deepEqual(result[0].buffer1, undefined); 14 | assert.deepEqual(result[0].buffer2, undefined); 15 | 16 | assert.deepEqual(result[1].common, undefined); 17 | assert.deepEqual(result[1].buffer1, ['b']); 18 | assert.deepEqual(result[1].buffer2, ['d']); 19 | 20 | assert.deepEqual(result[2].common, ['c', 'ZZ']); 21 | assert.deepEqual(result[2].buffer1, undefined); 22 | assert.deepEqual(result[2].buffer2, undefined); 23 | 24 | assert.deepEqual(result[3].common, undefined); 25 | assert.deepEqual(result[3].buffer1, ['new', '00', 'a', 'a']); 26 | assert.deepEqual(result[3].buffer2, ['11']); 27 | 28 | assert.deepEqual(result[4].common, ['M']); 29 | assert.deepEqual(result[4].buffer1, undefined); 30 | assert.deepEqual(result[4].buffer2, undefined); 31 | 32 | assert.deepEqual(result[5].common, undefined); 33 | assert.deepEqual(result[5].buffer1, []); 34 | assert.deepEqual(result[5].buffer2, ['z', 'z']); 35 | 36 | assert.deepEqual(result[6].common, ['99']); 37 | assert.deepEqual(result[6].buffer1, undefined); 38 | assert.deepEqual(result[6].buffer2, undefined); 39 | }); 40 | 41 | }); -------------------------------------------------------------------------------- /test/diffIndices.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import * as Diff3 from '../index.mjs'; 4 | 5 | test('diffIndices', async t => { 6 | 7 | await t.test('returns array indices for differing regions of two arrays', t => { 8 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 9 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 10 | const result = Diff3.diffIndices(a, b); 11 | 12 | assert.deepEqual(result[0].buffer1, [2, 1]); 13 | assert.deepEqual(result[0].buffer1Content, ['b']); 14 | assert.deepEqual(result[0].buffer2, [2, 1]); 15 | assert.deepEqual(result[0].buffer2Content, ['d']); 16 | 17 | assert.deepEqual(result[1].buffer1, [5, 4]); 18 | assert.deepEqual(result[1].buffer1Content, ['new', '00', 'a', 'a']); 19 | assert.deepEqual(result[1].buffer2, [5, 1]); 20 | assert.deepEqual(result[1].buffer2Content, ['11']); 21 | 22 | assert.deepEqual(result[2].buffer1, [10, 0]); 23 | assert.deepEqual(result[2].buffer1Content, []); 24 | assert.deepEqual(result[2].buffer2, [7, 2]); 25 | assert.deepEqual(result[2].buffer2Content, ['z', 'z']); 26 | }); 27 | 28 | }); -------------------------------------------------------------------------------- /test/diffPatch.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import * as Diff3 from '../index.mjs'; 4 | 5 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 6 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 7 | const a0 = a; 8 | const b0 = b; 9 | 10 | test('diffPatch', async t => { 11 | await t.test('returns a patch-style diff of two arrays', t => { 12 | const result = Diff3.diffPatch(a, b); 13 | 14 | assert.deepEqual(result[0].buffer1.offset, 2); 15 | assert.deepEqual(result[0].buffer1.length, 1); 16 | assert.deepEqual(result[0].buffer1.chunk, ['b']); 17 | assert.deepEqual(result[0].buffer2.offset, 2); 18 | assert.deepEqual(result[0].buffer2.length, 1); 19 | assert.deepEqual(result[0].buffer2.chunk, ['d']); 20 | 21 | assert.deepEqual(result[1].buffer1.offset, 5); 22 | assert.deepEqual(result[1].buffer1.length, 4); 23 | assert.deepEqual(result[1].buffer1.chunk, ['new', '00', 'a', 'a']); 24 | assert.deepEqual(result[1].buffer2.offset, 5); 25 | assert.deepEqual(result[1].buffer2.length, 1); 26 | assert.deepEqual(result[1].buffer2.chunk, ['11']); 27 | 28 | assert.deepEqual(result[2].buffer1.offset, 10); 29 | assert.deepEqual(result[2].buffer1.length, 0); 30 | assert.deepEqual(result[2].buffer1.chunk, []); 31 | assert.deepEqual(result[2].buffer2.offset, 7); 32 | assert.deepEqual(result[2].buffer2.length, 2); 33 | assert.deepEqual(result[2].buffer2.chunk, ['z', 'z']); 34 | }); 35 | 36 | await t.test('did not modify buffer1 or buffer2', t => { 37 | assert.equal(a0, a); 38 | assert.equal(b0, b); 39 | }); 40 | }); 41 | 42 | 43 | test('patch', async t => { 44 | await t.test('applies a patch against buffer1 to get buffer2', t => { 45 | const patch = Diff3.diffPatch(a, b); 46 | const result = Diff3.patch(a, patch); 47 | assert.deepEqual(result, b); 48 | }); 49 | 50 | await t.test('did not modify buffer1 or buffer2', t => { 51 | assert.equal(a0, a); 52 | assert.equal(b0, b); 53 | }); 54 | }); 55 | 56 | 57 | test('stripPatch', async t => { 58 | const patch = Diff3.diffPatch(a, b); 59 | const strip = Diff3.stripPatch(patch); 60 | 61 | await t.test('removes extra information from the diffPatch result', t => { 62 | assert.deepEqual(strip[0].buffer1.offset, 2); 63 | assert.deepEqual(strip[0].buffer1.length, 1); 64 | assert.deepEqual(strip[0].buffer1.chunk, undefined); 65 | assert.deepEqual(strip[0].buffer2.offset, undefined); 66 | assert.deepEqual(strip[0].buffer2.length, undefined); 67 | assert.deepEqual(strip[0].buffer2.chunk, ['d']); 68 | 69 | assert.deepEqual(strip[1].buffer1.offset, 5); 70 | assert.deepEqual(strip[1].buffer1.length, 4); 71 | assert.deepEqual(strip[1].buffer1.chunk, undefined); 72 | assert.deepEqual(strip[1].buffer2.offset, undefined); 73 | assert.deepEqual(strip[1].buffer2.length, undefined); 74 | assert.deepEqual(strip[1].buffer2.chunk, ['11']); 75 | 76 | assert.deepEqual(strip[2].buffer1.offset, 10); 77 | assert.deepEqual(strip[2].buffer1.length, 0); 78 | assert.deepEqual(strip[2].buffer1.chunk, undefined); 79 | assert.deepEqual(strip[2].buffer2.offset, undefined); 80 | assert.deepEqual(strip[2].buffer2.length, undefined); 81 | assert.deepEqual(strip[2].buffer2.chunk, ['z', 'z']); 82 | }); 83 | 84 | await t.test('applies a stripped patch against buffer1 to get buffer2', t => { 85 | const result = Diff3.patch(a, strip); 86 | assert.deepEqual(result, b); 87 | }); 88 | 89 | await t.test('did not modify the original patch', t => { 90 | assert.notEqual(patch, strip); 91 | assert.notEqual(patch[0], strip[0]); 92 | assert.notEqual(patch[1], strip[1]); 93 | assert.notEqual(patch[2], strip[2]); 94 | }); 95 | 96 | }); 97 | 98 | 99 | test('invertPatch', async t => { 100 | const patch = Diff3.diffPatch(a, b); 101 | const invert = Diff3.invertPatch(patch); 102 | 103 | await t.test('inverts the diffPatch result', t => { 104 | assert.deepEqual(invert[0].buffer2.offset, 2); 105 | assert.deepEqual(invert[0].buffer2.length, 1); 106 | assert.deepEqual(invert[0].buffer2.chunk, ['b']); 107 | assert.deepEqual(invert[0].buffer1.offset, 2); 108 | assert.deepEqual(invert[0].buffer1.length, 1); 109 | assert.deepEqual(invert[0].buffer1.chunk, ['d']); 110 | 111 | assert.deepEqual(invert[1].buffer2.offset, 5); 112 | assert.deepEqual(invert[1].buffer2.length, 4); 113 | assert.deepEqual(invert[1].buffer2.chunk, ['new', '00', 'a', 'a']); 114 | assert.deepEqual(invert[1].buffer1.offset, 5); 115 | assert.deepEqual(invert[1].buffer1.length, 1); 116 | assert.deepEqual(invert[1].buffer1.chunk, ['11']); 117 | 118 | assert.deepEqual(invert[2].buffer2.offset, 10); 119 | assert.deepEqual(invert[2].buffer2.length, 0); 120 | assert.deepEqual(invert[2].buffer2.chunk, []); 121 | assert.deepEqual(invert[2].buffer1.offset, 7); 122 | assert.deepEqual(invert[2].buffer1.length, 2); 123 | assert.deepEqual(invert[2].buffer1.chunk, ['z', 'z']); 124 | }); 125 | 126 | await t.test('applies an inverted patch against buffer2 to get buffer1', t => { 127 | const result = Diff3.patch(b, invert); 128 | assert.deepEqual(result, a); 129 | }); 130 | 131 | await t.test('did not modify the original patch', t => { 132 | assert.notEqual(patch, invert); 133 | assert.notEqual(patch[0], invert[0]); 134 | assert.notEqual(patch[1], invert[1]); 135 | assert.notEqual(patch[2], invert[2]); 136 | }); 137 | 138 | }); -------------------------------------------------------------------------------- /test/merge.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import * as Diff3 from '../index.mjs'; 4 | 5 | test('merge', async t => { 6 | 7 | await t.test('returns conflict: false if no conflicts', t => { 8 | const o = ['AA']; 9 | const a = ['AA']; 10 | const b = ['AA']; 11 | const expected = ['AA']; 12 | 13 | const r = Diff3.merge(a, o, b); 14 | assert.equal(r.conflict, false); 15 | assert.deepEqual(r.result, expected); 16 | }); 17 | 18 | 19 | await t.test('returns a diff3-style merge result', t => { 20 | const o = ['AA', 'ZZ', '00', 'M', '99']; 21 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 22 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 23 | const expected = [ 24 | 'AA', 25 | '<<<<<<<', 26 | 'a', 27 | 'b', 28 | 'c', 29 | '=======', 30 | 'a', 31 | 'd', 32 | 'c', 33 | '>>>>>>>', 34 | 'ZZ', 35 | '<<<<<<<', 36 | 'new', 37 | '00', 38 | 'a', 39 | 'a', 40 | '=======', 41 | '11', 42 | '>>>>>>>', 43 | 'M', 44 | 'z', 45 | 'z', 46 | '99' 47 | ]; 48 | 49 | const r = Diff3.merge(a, o, b); 50 | assert.equal(r.conflict, true); 51 | assert.deepEqual(r.result, expected); 52 | }); 53 | 54 | 55 | await t.test('yaml comparison - issue #46', t => { 56 | const o = `title: "title" 57 | description: "description"`; 58 | const a = `title: "title" 59 | description: "description changed"`; 60 | const b = `title: "title changed" 61 | description: "description"`; 62 | const expected = [ 63 | '<<<<<<<', 64 | 'title: "title"', 65 | 'description: "description changed"', 66 | '=======', 67 | 'title: "title changed"', 68 | 'description: "description"', 69 | '>>>>>>>' 70 | ]; 71 | 72 | const r = Diff3.merge(a, o, b, { stringSeparator: /[\r\n]+/ }); 73 | assert.equal(r.conflict, true); 74 | assert.deepEqual(r.result, expected); 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /test/mergeDiff3.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import * as Diff3 from '../index.mjs'; 4 | 5 | test('mergeDiff3', async t => { 6 | 7 | await t.test('returns conflict: false if no conflicts', t => { 8 | const o = ['AA']; 9 | const a = ['AA']; 10 | const b = ['AA']; 11 | const expected = ['AA']; 12 | 13 | const r = Diff3.mergeDiff3(a, o, b); 14 | assert.equal(r.conflict, false); 15 | assert.deepEqual(r.result, expected); 16 | }); 17 | 18 | 19 | await t.test('performs merge diff3 on arrays', t => { 20 | const o = ['AA', 'ZZ', '00', 'M', '99']; 21 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 22 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 23 | const expected = [ 24 | 'AA', 25 | '<<<<<<< a', 26 | 'a', 27 | 'b', 28 | 'c', 29 | '||||||| o', 30 | '=======', 31 | 'a', 32 | 'd', 33 | 'c', 34 | '>>>>>>> b', 35 | 'ZZ', 36 | '<<<<<<< a', 37 | 'new', 38 | '00', 39 | 'a', 40 | 'a', 41 | '||||||| o', 42 | '00', 43 | '=======', 44 | '11', 45 | '>>>>>>> b', 46 | 'M', 47 | 'z', 48 | 'z', 49 | '99' 50 | ]; 51 | 52 | const r = Diff3.mergeDiff3(a, o, b, { label: { a: 'a', o: 'o', b: 'b' } }); 53 | assert.equal(r.conflict, true); 54 | assert.deepEqual(r.result, expected); 55 | }); 56 | 57 | 58 | t.test('yaml comparison - issue #46', t => { 59 | const o = `title: "title" 60 | description: "description"`; 61 | const a = `title: "title" 62 | description: "description changed"`; 63 | const b = `title: "title changed" 64 | description: "description"`; 65 | const expected = [ 66 | '<<<<<<< a', 67 | 'title: "title"', 68 | 'description: "description changed"', 69 | '||||||| o', 70 | 'title: "title"', 71 | 'description: "description"', 72 | '=======', 73 | 'title: "title changed"', 74 | 'description: "description"', 75 | '>>>>>>> b' 76 | ]; 77 | 78 | const r = Diff3.mergeDiff3(a, o, b, { label: { a: 'a', o: 'o', b: 'b' }, stringSeparator: /[\r\n]+/ }); 79 | assert.equal(r.conflict, true); 80 | assert.deepEqual(r.result, expected); 81 | }); 82 | 83 | }); 84 | -------------------------------------------------------------------------------- /test/mergeDigIn.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import * as Diff3 from '../index.mjs'; 4 | 5 | test('mergeDigIn', async t => { 6 | 7 | await t.test('returns conflict: false if no conflicts', t => { 8 | const o = ['AA']; 9 | const a = ['AA']; 10 | const b = ['AA']; 11 | const expected = ['AA']; 12 | 13 | const r = Diff3.mergeDigIn(a, o, b); 14 | assert.equal(r.conflict, false); 15 | assert.deepEqual(r.result, expected); 16 | }); 17 | 18 | 19 | await t.test('returns a digin-style merge result', t => { 20 | const o = ['AA', 'ZZ', '00', 'M', '99']; 21 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99']; 22 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99']; 23 | const expected = [ 24 | 'AA', 25 | 'a', 26 | '<<<<<<<', 27 | 'b', 28 | '=======', 29 | 'd', 30 | '>>>>>>>', 31 | 'c', 32 | 'ZZ', 33 | '<<<<<<<', 34 | 'new', 35 | '00', 36 | 'a', 37 | 'a', 38 | '=======', 39 | '11', 40 | '>>>>>>>', 41 | 'M', 42 | 'z', 43 | 'z', 44 | '99' 45 | ]; 46 | 47 | const r = Diff3.mergeDigIn(a, o, b); 48 | assert.equal(r.conflict, true); 49 | assert.deepEqual(r.result, expected); 50 | }); 51 | 52 | }); 53 | --------------------------------------------------------------------------------