codekingpro/portable-devtools
115k
1(function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3 typeof define === 'function' && define.amd ? define(['exports'], factory) :
4 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Diff = {}));
5})(this, (function (exports) { 'use strict';
6
7 class Diff {
8 diff(oldStr, newStr,
9 // Type below is not accurate/complete - see above for full possibilities - but it compiles
10 options = {}) {
11 let callback;
12 if (typeof options === 'function') {
13 callback = options;
14 options = {};
15 }
16 else if ('callback' in options) {
17 callback = options.callback;
18 }
19 // Allow subclasses to massage the input prior to running
20 const oldString = this.castInput(oldStr, options);
21 const newString = this.castInput(newStr, options);
22 const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
23 const newTokens = this.removeEmpty(this.tokenize(newString, options));
24 return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
25 }
26 diffWithOptionsObj(oldTokens, newTokens, options, callback) {
27 var _a;
28 const done = (value) => {
29 value = this.postProcess(value, options);
30 if (callback) {
31 setTimeout(function () { callback(value); }, 0);
32 return undefined;
33 }
34 else {
35 return value;
36 }
37 };
38 const newLen = newTokens.length, oldLen = oldTokens.length;
39 let editLength = 1;
40 let maxEditLength = newLen + oldLen;
41 if (options.maxEditLength != null) {
42 maxEditLength = Math.min(maxEditLength, options.maxEditLength);
43 }
44 const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
45 const abortAfterTimestamp = Date.now() + maxExecutionTime;
46 const bestPath = [{ oldPos: -1, lastComponent: undefined }];
47 // Seed editLength = 0, i.e. the content starts with the same values
48 let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
49 if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
50 // Identity per the equality and tokenizer
51 return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
52 }
53 // Once we hit the right edge of the edit graph on some diagonal k, we can
54 // definitely reach the end of the edit graph in no more than k edits, so
55 // there's no point in considering any moves to diagonal k+1 any more (from
56 // which we're guaranteed to need at least k+1 more edits).
57 // Similarly, once we've reached the bottom of the edit graph, there's no
58 // point considering moves to lower diagonals.
59 // We record this fact by setting minDiagonalToConsider and
60 // maxDiagonalToConsider to some finite value once we've hit the edge of
61 // the edit graph.
62 // This optimization is not faithful to the original algorithm presented in
63 // Myers's paper, which instead pointlessly extends D-paths off the end of
64 // the edit graph - see page 7 of Myers's paper which notes this point
65 // explicitly and illustrates it with a diagram. This has major performance
66 // implications for some common scenarios. For instance, to compute a diff
67 // where the new text simply appends d characters on the end of the
68 // original text of length n, the true Myers algorithm will take O(n+d^2)
69 // time while this optimization needs only O(n+d) time.
70 let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
71 // Main worker method. checks all permutations of a given edit length for acceptance.
72 const execEditLength = () => {
73 for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
74 let basePath;
75 const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
76 if (removePath) {
77 // No one else is going to attempt to use this value, clear it
78 // @ts-expect-error - perf optimisation. This type-violating value will never be read.
79 bestPath[diagonalPath - 1] = undefined;
80 }
81 let canAdd = false;
82 if (addPath) {
83 // what newPos will be after we do an insertion:
84 const addPathNewPos = addPath.oldPos - diagonalPath;
85 canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
86 }
87 const canRemove = removePath && removePath.oldPos + 1 < oldLen;
88 if (!canAdd && !canRemove) {
89 // If this path is a terminal then prune
90 // @ts-expect-error - perf optimisation. This type-violating value will never be read.
91 bestPath[diagonalPath] = undefined;
92 continue;
93 }
94 // Select the diagonal that we want to branch from. We select the prior
95 // path whose position in the old string is the farthest from the origin
96 // and does not pass the bounds of the diff graph
97 if (!canRemove || (canAdd && removePath.oldPos < addPath.oldPos)) {
98 basePath = this.addToPath(addPath, true, false, 0, options);
99 }
100 else {
101 basePath = this.addToPath(removePath, false, true, 1, options);
102 }
103 newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
104 if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
105 // If we have hit the end of both strings, then we are done
106 return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
107 }
108 else {
109 bestPath[diagonalPath] = basePath;
110 if (basePath.oldPos + 1 >= oldLen) {
111 maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
112 }
113 if (newPos + 1 >= newLen) {
114 minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
115 }
116 }
117 }
118 editLength++;
119 };
120 // Performs the length of edit iteration. Is a bit fugly as this has to support the
121 // sync and async mode which is never fun. Loops over execEditLength until a value
122 // is produced, or until the edit length exceeds options.maxEditLength (if given),
123 // in which case it will return undefined.
124 if (callback) {
125 (function exec() {
126 setTimeout(function () {
127 if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
128 return callback(undefined);
129 }
130 if (!execEditLength()) {
131 exec();
132 }
133 }, 0);
134 }());
135 }
136 else {
137 while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
138 const ret = execEditLength();
139 if (ret) {
140 return ret;
141 }
142 }
143 }
144 }
145 addToPath(path, added, removed, oldPosInc, options) {
146 const last = path.lastComponent;
147 if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
148 return {
149 oldPos: path.oldPos + oldPosInc,
150 lastComponent: { count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent }
151 };
152 }
153 else {
154 return {
155 oldPos: path.oldPos + oldPosInc,
156 lastComponent: { count: 1, added: added, removed: removed, previousComponent: last }
157 };
158 }
159 }
160 extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
161 const newLen = newTokens.length, oldLen = oldTokens.length;
162 let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
163 while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
164 newPos++;
165 oldPos++;
166 commonCount++;
167 if (options.oneChangePerToken) {
168 basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
169 }
170 }
171 if (commonCount && !options.oneChangePerToken) {
172 basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
173 }
174 basePath.oldPos = oldPos;
175 return newPos;
176 }
177 equals(left, right, options) {
178 if (options.comparator) {
179 return options.comparator(left, right);
180 }
181 else {
182 return left === right
183 || (!!options.ignoreCase && left.toLowerCase() === right.toLowerCase());
184 }
185 }
186 removeEmpty(array) {
187 const ret = [];
188 for (let i = 0; i < array.length; i++) {
189 if (array[i]) {
190 ret.push(array[i]);
191 }
192 }
193 return ret;
194 }
195 // eslint-disable-next-line @typescript-eslint/no-unused-vars
196 castInput(value, options) {
197 return value;
198 }
199 // eslint-disable-next-line @typescript-eslint/no-unused-vars
200 tokenize(value, options) {
201 return Array.from(value);
202 }
203 join(chars) {
204 // Assumes ValueT is string, which is the case for most subclasses.
205 // When it's false, e.g. in diffArrays, this method needs to be overridden (e.g. with a no-op)
206 // Yes, the casts are verbose and ugly, because this pattern - of having the base class SORT OF
207 // assume tokens and values are strings, but not completely - is weird and janky.
208 return chars.join('');
209 }
210 postProcess(changeObjects,
211 // eslint-disable-next-line @typescript-eslint/no-unused-vars
212 options) {
213 return changeObjects;
214 }
215 get useLongestToken() {
216 return false;
217 }
218 buildValues(lastComponent, newTokens, oldTokens) {
219 // First we convert our linked list of components in reverse order to an
220 // array in the right order:
221 const components = [];
222 let nextComponent;
223 while (lastComponent) {
224 components.push(lastComponent);
225 nextComponent = lastComponent.previousComponent;
226 delete lastComponent.previousComponent;
227 lastComponent = nextComponent;
228 }
229 components.reverse();
230 const componentLen = components.length;
231 let componentPos = 0, newPos = 0, oldPos = 0;
232 for (; componentPos < componentLen; componentPos++) {
233 const component = components[componentPos];
234 if (!component.removed) {
235 if (!component.added && this.useLongestToken) {
236 let value = newTokens.slice(newPos, newPos + component.count);
237 value = value.map(function (value, i) {
238 const oldValue = oldTokens[oldPos + i];
239 return oldValue.length > value.length ? oldValue : value;
240 });
241 component.value = this.join(value);
242 }
243 else {
244 component.value = this.join(newTokens.slice(newPos, newPos + component.count));
245 }
246 newPos += component.count;
247 // Common case
248 if (!component.added) {
249 oldPos += component.count;
250 }
251 }
252 else {
253 component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
254 oldPos += component.count;
255 }
256 }
257 return components;
258 }
259 }
260
261 class CharacterDiff extends Diff {
262 }
263 const characterDiff = new CharacterDiff();
264 function diffChars(oldStr, newStr, options) {
265 return characterDiff.diff(oldStr, newStr, options);
266 }
267
268 function longestCommonPrefix(str1, str2) {
269 let i;
270 for (i = 0; i < str1.length && i < str2.length; i++) {
271 if (str1[i] != str2[i]) {
272 return str1.slice(0, i);
273 }
274 }
275 return str1.slice(0, i);
276 }
277 function longestCommonSuffix(str1, str2) {
278 let i;
279 // Unlike longestCommonPrefix, we need a special case to handle all scenarios
280 // where we return the empty string since str1.slice(-0) will return the
281 // entire string.
282 if (!str1 || !str2 || str1[str1.length - 1] != str2[str2.length - 1]) {
283 return '';
284 }
285 for (i = 0; i < str1.length && i < str2.length; i++) {
286 if (str1[str1.length - (i + 1)] != str2[str2.length - (i + 1)]) {
287 return str1.slice(-i);
288 }
289 }
290 return str1.slice(-i);
291 }
292 function replacePrefix(string, oldPrefix, newPrefix) {
293 if (string.slice(0, oldPrefix.length) != oldPrefix) {
294 throw Error(`string ${JSON.stringify(string)} doesn't start with prefix ${JSON.stringify(oldPrefix)}; this is a bug`);
295 }
296 return newPrefix + string.slice(oldPrefix.length);
297 }
298 function replaceSuffix(string, oldSuffix, newSuffix) {
299 if (!oldSuffix) {
300 return string + newSuffix;
301 }
302 if (string.slice(-oldSuffix.length) != oldSuffix) {
303 throw Error(`string ${JSON.stringify(string)} doesn't end with suffix ${JSON.stringify(oldSuffix)}; this is a bug`);
304 }
305 return string.slice(0, -oldSuffix.length) + newSuffix;
306 }
307 function removePrefix(string, oldPrefix) {
308 return replacePrefix(string, oldPrefix, '');
309 }
310 function removeSuffix(string, oldSuffix) {
311 return replaceSuffix(string, oldSuffix, '');
312 }
313 function maximumOverlap(string1, string2) {
314 return string2.slice(0, overlapCount(string1, string2));
315 }
316 // Nicked from https://stackoverflow.com/a/60422853/1709587
317 function overlapCount(a, b) {
318 // Deal with cases where the strings differ in length
319 let startA = 0;
320 if (a.length > b.length) {
321 startA = a.length - b.length;
322 }
323 let endB = b.length;
324 if (a.length < b.length) {
325 endB = a.length;
326 }
327 // Create a back-reference for each index
328 // that should be followed in case of a mismatch.
329 // We only need B to make these references:
330 const map = Array(endB);
331 let k = 0; // Index that lags behind j
332 map[0] = 0;
333 for (let j = 1; j < endB; j++) {
334 if (b[j] == b[k]) {
335 map[j] = map[k]; // skip over the same character (optional optimisation)
336 }
337 else {
338 map[j] = k;
339 }
340 while (k > 0 && b[j] != b[k]) {
341 k = map[k];
342 }
343 if (b[j] == b[k]) {
344 k++;
345 }
346 }
347 // Phase 2: use these references while iterating over A
348 k = 0;
349 for (let i = startA; i < a.length; i++) {
350 while (k > 0 && a[i] != b[k]) {
351 k = map[k];
352 }
353 if (a[i] == b[k]) {
354 k++;
355 }
356 }
357 return k;
358 }
359 /**
360 * Returns true if the string consistently uses Windows line endings.
361 */
362 function hasOnlyWinLineEndings(string) {
363 return string.includes('\r\n') && !string.startsWith('\n') && !string.match(/[^\r]\n/);
364 }
365 /**
366 * Returns true if the string consistently uses Unix line endings.
367 */
368 function hasOnlyUnixLineEndings(string) {
369 return !string.includes('\r\n') && string.includes('\n');
370 }
371 /**
372 * Split a string into segments using a word segmenter, merging consecutive
373 * segments if they are both whitespace segments. Whitespace segments can
374 * appear adjacent to one another for two reasons:
375 * - newlines always get their own segment
376 * - where a diacritic is attached to a whitespace character in the text, the
377 * segment ends after the diacritic, so e.g. " \u0300 " becomes two segments.
378 * This function therefore runs the segmenter's .segment() method and then
379 * merges consecutive segments of whitespace into a single part.
380 */
381 function segment(string, segmenter) {
382 const parts = [];
383 for (const segmentObj of Array.from(segmenter.segment(string))) {
384 const segment = segmentObj.segment;
385 if (parts.length && (/\s/).test(parts[parts.length - 1]) && (/\s/).test(segment)) {
386 parts[parts.length - 1] += segment;
387 }
388 else {
389 parts.push(segment);
390 }
391 }
392 return parts;
393 }
394 // The functions below take a `segmenter` argument so that, when called from
395 // diffWords when it is using a segmenter, they can use a notion of what
396 // constitutes "whitespace" that is consistent with the segmenter.
397 //
398 // USUALLY this will be identical to the result of the non-segmenter-based
399 // logic, but it differs in at least one case: when whitespace characters are
400 // modified by diacritics. A word segmenter considers these diacritics to be
401 // part of the whitespace, whereas our non-segmenter-based logic does not.
402 //
403 // Because the segmenter-based approach necessarily requires segmenting the
404 // entire string, we offer a leadingAndTrailingWs function to allow getting the
405 // whitespace prefix AND whitespace suffix with a single call to the segmenter,
406 // for efficiency's sake.
407 function trailingWs(string, segmenter) {
408 if (segmenter) {
409 return leadingAndTrailingWs(string, segmenter)[1];
410 }
411 // Yes, this looks overcomplicated and dumb - why not replace the whole function with
412 // return string.match(/\s*$/)[0]
413 // you ask? Because:
414 // 1. the trap described at https://markamery.com/blog/quadratic-time-regexes/ would mean doing
415 // this would cause this function to take O(n²) time in the worst case (specifically when
416 // there is a massive run of NON-TRAILING whitespace in `string`), and
417 // 2. the fix proposed in the same blog post, of using a negative lookbehind, is incompatible
418 // with old Safari versions that we'd like to not break if possible (see
419 // https://github.com/kpdecker/jsdiff/pull/550)
420 // It feels absurd to do this with an explicit loop instead of a regex, but I really can't see a
421 // better way that doesn't result in broken behaviour.
422 let i;
423 for (i = string.length - 1; i >= 0; i--) {
424 if (!string[i].match(/\s/)) {
425 break;
426 }
427 }
428 return string.substring(i + 1);
429 }
430 function leadingWs(string, segmenter) {
431 if (segmenter) {
432 return leadingAndTrailingWs(string, segmenter)[0];
433 }
434 // Thankfully the annoying considerations described in trailingWs don't apply here:
435 const match = string.match(/^\s*/);
436 return match ? match[0] : '';
437 }
438 function leadingAndTrailingWs(string, segmenter) {
439 if (!segmenter) {
440 return [leadingWs(string), trailingWs(string)];
441 }
442 if (segmenter.resolvedOptions().granularity != 'word') {
443 throw new Error('The segmenter passed must have a granularity of "word"');
444 }
445 const segments = segment(string, segmenter);
446 const firstSeg = segments[0];
447 const lastSeg = segments[segments.length - 1];
448 const head = (/\s/).test(firstSeg) ? firstSeg : '';
449 const tail = (/\s/).test(lastSeg) ? lastSeg : '';
450 return [head, tail];
451 }
452
453 // Based on https://en.wikipedia.org/wiki/Latin_script_in_Unicode
454 //
455 // Chars/ranges counted as "word" characters by this regex are as follows:
456 //
457 // + U+00AD Soft hyphen
458 // + 00C0–00FF (letters with diacritics from the Latin-1 Supplement), except:
459 // - U+00D7 × Multiplication sign
460 // - U+00F7 ÷ Division sign
461 // + Latin Extended-A, 0100–017F
462 // + Latin Extended-B, 0180–024F
463 // + IPA Extensions, 0250–02AF
464 // + Spacing Modifier Letters, 02B0–02FF, except:
465 // - U+02C7 ˇ ˇ Caron
466 // - U+02D8 ˘ ˘ Breve
467 // - U+02D9 ˙ ˙ Dot Above
468 // - U+02DA ˚ ˚ Ring Above
469 // - U+02DB ˛ ˛ Ogonek
470 // - U+02DC ˜ ˜ Small Tilde
471 // - U+02DD ˝ ˝ Double Acute Accent
472 // + Latin Extended Additional, 1E00–1EFF
473 const extendedWordChars = 'a-zA-Z0-9_\\u{AD}\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}';
474 // Each token is one of the following:
475 // - A punctuation mark plus the surrounding whitespace
476 // - A word plus the surrounding whitespace
477 // - Pure whitespace (but only in the special case where the entire text
478 // is just whitespace)
479 //
480 // We have to include surrounding whitespace in the tokens because the two
481 // alternative approaches produce horribly broken results:
482 // * If we just discard the whitespace, we can't fully reproduce the original
483 // text from the sequence of tokens and any attempt to render the diff will
484 // get the whitespace wrong.
485 // * If we have separate tokens for whitespace, then in a typical text every
486 // second token will be a single space character. But this often results in
487 // the optimal diff between two texts being a perverse one that preserves
488 // the spaces between words but deletes and reinserts actual common words.
489 // See https://github.com/kpdecker/jsdiff/issues/160#issuecomment-1866099640
490 // for an example.
491 //
492 // Keeping the surrounding whitespace of course has implications for .equals
493 // and .join, not just .tokenize.
494 // This regex does NOT fully implement the tokenization rules described above.
495 // Instead, it gives runs of whitespace their own "token". The tokenize method
496 // then handles stitching whitespace tokens onto adjacent word or punctuation
497 // tokens.
498 const tokenizeIncludingWhitespace = new RegExp(`[${extendedWordChars}]+|\\s+|[^${extendedWordChars}]`, 'ug');
499 class WordDiff extends Diff {
500 equals(left, right, options) {
501 if (options.ignoreCase) {
502 left = left.toLowerCase();
503 right = right.toLowerCase();
504 }
505 return left.trim() === right.trim();
506 }
507 tokenize(value, options = {}) {
508 let parts;
509 if (options.intlSegmenter) {
510 const segmenter = options.intlSegmenter;
511 if (segmenter.resolvedOptions().granularity != 'word') {
512 throw new Error('The segmenter passed must have a granularity of "word"');
513 }
514 // We want `parts` to be an array whose elements alternate between being
515 // pure whitespace and being pure non-whitespace. This is ALMOST what the
516 // segments returned by a word-based Intl.Segmenter already look like,
517 // but not quite - see explanation in the docs of our custom segment()
518 // function.
519 parts = segment(value, segmenter);
520 }
521 else {
522 parts = value.match(tokenizeIncludingWhitespace) || [];
523 }
524 const tokens = [];
525 let prevPart = null;
526 parts.forEach(part => {
527 if ((/\s/).test(part)) {
528 if (prevPart == null) {
529 tokens.push(part);
530 }
531 else {
532 tokens.push(tokens.pop() + part);
533 }
534 }
535 else if (prevPart != null && (/\s/).test(prevPart)) {
536 if (tokens[tokens.length - 1] == prevPart) {
537 tokens.push(tokens.pop() + part);
538 }
539 else {
540 tokens.push(prevPart + part);
541 }
542 }
543 else {
544 tokens.push(part);
545 }
546 prevPart = part;
547 });
548 return tokens;
549 }
550 join(tokens) {
551 // Tokens being joined here will always have appeared consecutively in the
552 // same text, so we can simply strip off the leading whitespace from all the
553 // tokens except the first (and except any whitespace-only tokens - but such
554 // a token will always be the first and only token anyway) and then join them
555 // and the whitespace around words and punctuation will end up correct.
556 return tokens.map((token, i) => {
557 if (i == 0) {
558 return token;
559 }
560 else {
561 return token.replace((/^\s+/), '');
562 }
563 }).join('');
564 }
565 postProcess(changes, options) {
566 if (!changes || options.oneChangePerToken) {
567 return changes;
568 }
569 let lastKeep = null;
570 // Change objects representing any insertion or deletion since the last
571 // "keep" change object. There can be at most one of each.
572 let insertion = null;
573 let deletion = null;
574 changes.forEach(change => {
575 if (change.added) {
576 insertion = change;
577 }
578 else if (change.removed) {
579 deletion = change;
580 }
581 else {
582 if (insertion || deletion) { // May be false at start of text
583 dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change, options.intlSegmenter);
584 }
585 lastKeep = change;
586 insertion = null;
587 deletion = null;
588 }
589 });
590 if (insertion || deletion) {
591 dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null, options.intlSegmenter);
592 }
593 return changes;
594 }
595 }
596 const wordDiff = new WordDiff();
597 function diffWords(oldStr, newStr, options) {
598 // This option has never been documented and never will be (it's clearer to
599 // just call `diffWordsWithSpace` directly if you need that behavior), but
600 // has existed in jsdiff for a long time, so we retain support for it here
601 // for the sake of backwards compatibility.
602 if ((options === null || options === void 0 ? void 0 : options.ignoreWhitespace) != null && !options.ignoreWhitespace) {
603 return diffWordsWithSpace(oldStr, newStr, options);
604 }
605 return wordDiff.diff(oldStr, newStr, options);
606 }
607 function dedupeWhitespaceInChangeObjects(startKeep, deletion, insertion, endKeep, segmenter) {
608 // Before returning, we tidy up the leading and trailing whitespace of the
609 // change objects to eliminate cases where trailing whitespace in one object
610 // is repeated as leading whitespace in the next.
611 // Below are examples of the outcomes we want here to explain the code.
612 // I=insert, K=keep, D=delete
613 // 1. diffing 'foo bar baz' vs 'foo baz'
614 // Prior to cleanup, we have K:'foo ' D:' bar ' K:' baz'
615 // After cleanup, we want: K:'foo ' D:'bar ' K:'baz'
616 //
617 // 2. Diffing 'foo bar baz' vs 'foo qux baz'
618 // Prior to cleanup, we have K:'foo ' D:' bar ' I:' qux ' K:' baz'
619 // After cleanup, we want K:'foo ' D:'bar' I:'qux' K:' baz'
620 //
621 // 3. Diffing 'foo\nbar baz' vs 'foo baz'
622 // Prior to cleanup, we have K:'foo ' D:'\nbar ' K:' baz'
623 // After cleanup, we want K'foo' D:'\nbar' K:' baz'
624 //
625 // 4. Diffing 'foo baz' vs 'foo\nbar baz'
626 // Prior to cleanup, we have K:'foo\n' I:'\nbar ' K:' baz'
627 // After cleanup, we ideally want K'foo' I:'\nbar' K:' baz'
628 // but don't actually manage this currently (the pre-cleanup change
629 // objects don't contain enough information to make it possible).
630 //
631 // 5. Diffing 'foo bar baz' vs 'foo baz'
632 // Prior to cleanup, we have K:'foo ' D:' bar ' K:' baz'
633 // After cleanup, we want K:'foo ' D:' bar ' K:'baz'
634 //
635 // Our handling is unavoidably imperfect in the case where there's a single
636 // indel between keeps and the whitespace has changed. For instance, consider
637 // diffing 'foo\tbar\nbaz' vs 'foo baz'. Unless we create an extra change
638 // object to represent the insertion of the space character (which isn't even
639 // a token), we have no way to avoid losing information about the texts'
640 // original whitespace in the result we return. Still, we do our best to
641 // output something that will look sensible if we e.g. print it with
642 // insertions in green and deletions in red.
643 // Between two "keep" change objects (or before the first or after the last
644 // change object), we can have either:
645 // * A "delete" followed by an "insert"
646 // * Just an "insert"
647 // * Just a "delete"
648 // We handle the three cases separately.
649 if (deletion && insertion) {
650 const [oldWsPrefix, oldWsSuffix] = leadingAndTrailingWs(deletion.value, segmenter);
651 const [newWsPrefix, newWsSuffix] = leadingAndTrailingWs(insertion.value, segmenter);
652 if (startKeep) {
653 const commonWsPrefix = longestCommonPrefix(oldWsPrefix, newWsPrefix);
654 startKeep.value = replaceSuffix(startKeep.value, newWsPrefix, commonWsPrefix);
655 deletion.value = removePrefix(deletion.value, commonWsPrefix);
656 insertion.value = removePrefix(insertion.value, commonWsPrefix);
657 }
658 if (endKeep) {
659 const commonWsSuffix = longestCommonSuffix(oldWsSuffix, newWsSuffix);
660 endKeep.value = replacePrefix(endKeep.value, newWsSuffix, commonWsSuffix);
661 deletion.value = removeSuffix(deletion.value, commonWsSuffix);
662 insertion.value = removeSuffix(insertion.value, commonWsSuffix);
663 }
664 }
665 else if (insertion) {
666 // The whitespaces all reflect what was in the new text rather than
667 // the old, so we essentially have no information about whitespace
668 // insertion or deletion. We just want to dedupe the whitespace.
669 // We do that by having each change object keep its trailing
670 // whitespace and deleting duplicate leading whitespace where
671 // present.
672 if (startKeep) {
673 const ws = leadingWs(insertion.value, segmenter);
674 insertion.value = insertion.value.substring(ws.length);
675 }
676 if (endKeep) {
677 const ws = leadingWs(endKeep.value, segmenter);
678 endKeep.value = endKeep.value.substring(ws.length);
679 }
680 // otherwise we've got a deletion and no insertion
681 }
682 else if (startKeep && endKeep) {
683 const newWsFull = leadingWs(endKeep.value, segmenter), [delWsStart, delWsEnd] = leadingAndTrailingWs(deletion.value, segmenter);
684 // Any whitespace that comes straight after startKeep in both the old and
685 // new texts, assign to startKeep and remove from the deletion.
686 const newWsStart = longestCommonPrefix(newWsFull, delWsStart);
687 deletion.value = removePrefix(deletion.value, newWsStart);
688 // Any whitespace that comes straight before endKeep in both the old and
689 // new texts, and hasn't already been assigned to startKeep, assign to
690 // endKeep and remove from the deletion.
691 const newWsEnd = longestCommonSuffix(removePrefix(newWsFull, newWsStart), delWsEnd);
692 deletion.value = removeSuffix(deletion.value, newWsEnd);
693 endKeep.value = replacePrefix(endKeep.value, newWsFull, newWsEnd);
694 // If there's any whitespace from the new text that HASN'T already been
695 // assigned, assign it to the start:
696 startKeep.value = replaceSuffix(startKeep.value, newWsFull, newWsFull.slice(0, newWsFull.length - newWsEnd.length));
697 }
698 else if (endKeep) {
699 // We are at the start of the text. Preserve all the whitespace on
700 // endKeep, and just remove whitespace from the end of deletion to the
701 // extent that it overlaps with the start of endKeep.
702 const endKeepWsPrefix = leadingWs(endKeep.value, segmenter);
703 const deletionWsSuffix = trailingWs(deletion.value, segmenter);
704 const overlap = maximumOverlap(deletionWsSuffix, endKeepWsPrefix);
705 deletion.value = removeSuffix(deletion.value, overlap);
706 }
707 else if (startKeep) {
708 // We are at the END of the text. Preserve all the whitespace on
709 // startKeep, and just remove whitespace from the start of deletion to
710 // the extent that it overlaps with the end of startKeep.
711 const startKeepWsSuffix = trailingWs(startKeep.value, segmenter);
712 const deletionWsPrefix = leadingWs(deletion.value, segmenter);
713 const overlap = maximumOverlap(startKeepWsSuffix, deletionWsPrefix);
714 deletion.value = removePrefix(deletion.value, overlap);
715 }
716 }
717 class WordsWithSpaceDiff extends Diff {
718 tokenize(value) {
719 // Slightly different to the tokenizeIncludingWhitespace regex used above in
720 // that this one treats each individual newline as a distinct token, rather
721 // than merging them into other surrounding whitespace. This was requested
722 // in https://github.com/kpdecker/jsdiff/issues/180 &
723 // https://github.com/kpdecker/jsdiff/issues/211
724 const regex = new RegExp(`(\\r?\\n)|[${extendedWordChars}]+|[^\\S\\n\\r]+|[^${extendedWordChars}]`, 'ug');
725 return value.match(regex) || [];
726 }
727 }
728 const wordsWithSpaceDiff = new WordsWithSpaceDiff();
729 function diffWordsWithSpace(oldStr, newStr, options) {
730 return wordsWithSpaceDiff.diff(oldStr, newStr, options);
731 }
732
733 function generateOptions(options, defaults) {
734 if (typeof options === 'function') {
735 defaults.callback = options;
736 }
737 else if (options) {
738 for (const name in options) {
739 /* istanbul ignore else */
740 if (Object.prototype.hasOwnProperty.call(options, name)) {
741 defaults[name] = options[name];
742 }
743 }
744 }
745 return defaults;
746 }
747
748 class LineDiff extends Diff {
749 constructor() {
750 super(...arguments);
751 this.tokenize = tokenize;
752 }
753 equals(left, right, options) {
754 // If we're ignoring whitespace, we need to normalise lines by stripping
755 // whitespace before checking equality. (This has an annoying interaction
756 // with newlineIsToken that requires special handling: if newlines get their
757 // own token, then we DON'T want to trim the *newline* tokens down to empty
758 // strings, since this would cause us to treat whitespace-only line content
759 // as equal to a separator between lines, which would be weird and
760 // inconsistent with the documented behavior of the options.)
761 if (options.ignoreWhitespace) {
762 if (!options.newlineIsToken || !left.includes('\n')) {
763 left = left.trim();
764 }
765 if (!options.newlineIsToken || !right.includes('\n')) {
766 right = right.trim();
767 }
768 }
769 else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
770 if (left.endsWith('\n')) {
771 left = left.slice(0, -1);
772 }
773 if (right.endsWith('\n')) {
774 right = right.slice(0, -1);
775 }
776 }
777 return super.equals(left, right, options);
778 }
779 }
780 const lineDiff = new LineDiff();
781 function diffLines(oldStr, newStr, options) {
782 return lineDiff.diff(oldStr, newStr, options);
783 }
784 function diffTrimmedLines(oldStr, newStr, options) {
785 options = generateOptions(options, { ignoreWhitespace: true });
786 return lineDiff.diff(oldStr, newStr, options);
787 }
788 // Exported standalone so it can be used from jsonDiff too.
789 function tokenize(value, options) {
790 if (options.stripTrailingCr) {
791 // remove one \r before \n to match GNU diff's --strip-trailing-cr behavior
792 value = value.replace(/\r\n/g, '\n');
793 }
794 const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
795 // Ignore the final empty token that occurs if the string ends with a new line
796 if (!linesAndNewlines[linesAndNewlines.length - 1]) {
797 linesAndNewlines.pop();
798 }
799 // Merge the content and line separators into single tokens
800 for (let i = 0; i < linesAndNewlines.length; i++) {
801 const line = linesAndNewlines[i];
802 if (i % 2 && !options.newlineIsToken) {
803 retLines[retLines.length - 1] += line;
804 }
805 else {
806 retLines.push(line);
807 }
808 }
809 return retLines;
810 }
811
812 function isSentenceEndPunct(char) {
813 return char == '.' || char == '!' || char == '?';
814 }
815 class SentenceDiff extends Diff {
816 tokenize(value) {
817 var _a;
818 // If in future we drop support for environments that don't support lookbehinds, we can replace
819 // this entire function with:
820 // return value.split(/(?<=[.!?])(\s+|$)/);
821 // but until then, for similar reasons to the trailingWs function in string.ts, we are forced
822 // to do this verbosely "by hand" instead of using a regex.
823 const result = [];
824 let tokenStartI = 0;
825 for (let i = 0; i < value.length; i++) {
826 if (i == value.length - 1) {
827 result.push(value.slice(tokenStartI));
828 break;
829 }
830 if (isSentenceEndPunct(value[i]) && value[i + 1].match(/\s/)) {
831 // We've hit a sentence break - i.e. a punctuation mark followed by whitespace.
832 // We now want to push TWO tokens to the result:
833 // 1. the sentence
834 result.push(value.slice(tokenStartI, i + 1));
835 // 2. the whitespace
836 i = tokenStartI = i + 1;
837 while ((_a = value[i + 1]) === null || _a === void 0 ? void 0 : _a.match(/\s/)) {
838 i++;
839 }
840 result.push(value.slice(tokenStartI, i + 1));
841 // Then the next token (a sentence) starts on the character after the whitespace.
842 // (It's okay if this is off the end of the string - then the outer loop will terminate
843 // here anyway.)
844 tokenStartI = i + 1;
845 }
846 }
847 return result;
848 }
849 }
850 const sentenceDiff = new SentenceDiff();
851 function diffSentences(oldStr, newStr, options) {
852 return sentenceDiff.diff(oldStr, newStr, options);
853 }
854
855 class CssDiff extends Diff {
856 tokenize(value) {
857 return value.split(/([{}:;,]|\s+)/);
858 }
859 }
860 const cssDiff = new CssDiff();
861 function diffCss(oldStr, newStr, options) {
862 return cssDiff.diff(oldStr, newStr, options);
863 }
864
865 class JsonDiff extends Diff {
866 constructor() {
867 super(...arguments);
868 this.tokenize = tokenize;
869 }
870 get useLongestToken() {
871 // Discriminate between two lines of pretty-printed, serialized JSON where one of them has a
872 // dangling comma and the other doesn't. Turns out including the dangling comma yields the nicest output:
873 return true;
874 }
875 castInput(value, options) {
876 const { undefinedReplacement, stringifyReplacer = (k, v) => typeof v === 'undefined' ? undefinedReplacement : v } = options;
877 return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), null, ' ');
878 }
879 equals(left, right, options) {
880 return super.equals(left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1'), options);
881 }
882 }
883 const jsonDiff = new JsonDiff();
884 function diffJson(oldStr, newStr, options) {
885 return jsonDiff.diff(oldStr, newStr, options);
886 }
887 // This function handles the presence of circular references by bailing out when encountering an
888 // object that is already on the "stack" of items being processed. Accepts an optional replacer
889 function canonicalize(obj, stack, replacementStack, replacer, key) {
890 stack = stack || [];
891 replacementStack = replacementStack || [];
892 if (replacer) {
893 obj = replacer(key === undefined ? '' : key, obj);
894 }
895 let i;
896 for (i = 0; i < stack.length; i += 1) {
897 if (stack[i] === obj) {
898 return replacementStack[i];
899 }
900 }
901 let canonicalizedObj;
902 if ('[object Array]' === Object.prototype.toString.call(obj)) {
903 stack.push(obj);
904 canonicalizedObj = new Array(obj.length);
905 replacementStack.push(canonicalizedObj);
906 for (i = 0; i < obj.length; i += 1) {
907 canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, String(i));
908 }
909 stack.pop();
910 replacementStack.pop();
911 return canonicalizedObj;
912 }
913 if (obj && obj.toJSON) {
914 obj = obj.toJSON();
915 }
916 if (typeof obj === 'object' && obj !== null) {
917 stack.push(obj);
918 canonicalizedObj = {};
919 replacementStack.push(canonicalizedObj);
920 const sortedKeys = [];
921 let key;
922 for (key in obj) {
923 /* istanbul ignore else */
924 if (Object.prototype.hasOwnProperty.call(obj, key)) {
925 sortedKeys.push(key);
926 }
927 }
928 sortedKeys.sort();
929 for (i = 0; i < sortedKeys.length; i += 1) {
930 key = sortedKeys[i];
931 canonicalizedObj[key] = canonicalize(obj[key], stack, replacementStack, replacer, key);
932 }
933 stack.pop();
934 replacementStack.pop();
935 }
936 else {
937 canonicalizedObj = obj;
938 }
939 return canonicalizedObj;
940 }
941
942 class ArrayDiff extends Diff {
943 tokenize(value) {
944 return value.slice();
945 }
946 join(value) {
947 return value;
948 }
949 removeEmpty(value) {
950 return value;
951 }
952 }
953 const arrayDiff = new ArrayDiff();
954 function diffArrays(oldArr, newArr, options) {
955 return arrayDiff.diff(oldArr, newArr, options);
956 }
957
958 function unixToWin(patch) {
959 if (Array.isArray(patch)) {
960 // It would be cleaner if instead of the line below we could just write
961 // return patch.map(unixToWin)
962 // but mysteriously TypeScript (v5.7.3 at the time of writing) does not like this and it will
963 // refuse to compile, thinking that unixToWin could then return StructuredPatch[][] and the
964 // result would be incompatible with the overload signatures.
965 // See bug report at https://github.com/microsoft/TypeScript/issues/61398.
966 return patch.map(p => unixToWin(p));
967 }
968 return Object.assign(Object.assign({}, patch), { hunks: patch.hunks.map(hunk => (Object.assign(Object.assign({}, hunk), { lines: hunk.lines.map((line, i) => {
969 var _a;
970 return (line.startsWith('\\') || line.endsWith('\r') || ((_a = hunk.lines[i + 1]) === null || _a === void 0 ? void 0 : _a.startsWith('\\')))
971 ? line
972 : line + '\r';
973 }) }))) });
974 }
975 function winToUnix(patch) {
976 if (Array.isArray(patch)) {
977 // (See comment above equivalent line in unixToWin)
978 return patch.map(p => winToUnix(p));
979 }
980 return Object.assign(Object.assign({}, patch), { hunks: patch.hunks.map(hunk => (Object.assign(Object.assign({}, hunk), { lines: hunk.lines.map(line => line.endsWith('\r') ? line.substring(0, line.length - 1) : line) }))) });
981 }
982 /**
983 * Returns true if the patch consistently uses Unix line endings (or only involves one line and has
984 * no line endings).
985 */
986 function isUnix(patch) {
987 if (!Array.isArray(patch)) {
988 patch = [patch];
989 }
990 return !patch.some(index => index.hunks.some(hunk => hunk.lines.some(line => !line.startsWith('\\') && line.endsWith('\r'))));
991 }
992 /**
993 * Returns true if the patch uses Windows line endings and only Windows line endings.
994 */
995 function isWin(patch) {
996 if (!Array.isArray(patch)) {
997 patch = [patch];
998 }
999 return patch.some(index => index.hunks.some(hunk => hunk.lines.some(line => line.endsWith('\r'))))
1000 && patch.every(index => index.hunks.every(hunk => hunk.lines.every((line, i) => { var _a; return line.startsWith('\\') || line.endsWith('\r') || ((_a = hunk.lines[i + 1]) === null || _a === void 0 ? void 0 : _a.startsWith('\\')); })));
1001 }
1002
1003 /**
1004 * Parses a patch into structured data, in the same structure returned by `structuredPatch`.
1005 *
1006 * @return a JSON object representation of the a patch, suitable for use with the `applyPatch` method.
1007 */
1008 function parsePatch(uniDiff) {
1009 const diffstr = uniDiff.split(/\n/), list = [];
1010 let i = 0;
1011 function parseIndex() {
1012 const index = {};
1013 list.push(index);
1014 // Parse diff metadata
1015 while (i < diffstr.length) {
1016 const line = diffstr[i];
1017 // File header found, end parsing diff metadata
1018 if ((/^(---|\+\+\+|@@)\s/).test(line)) {
1019 break;
1020 }
1021 // Try to parse the line as a diff header, like
1022 // Index: README.md
1023 // or
1024 // diff -r 9117c6561b0b -r 273ce12ad8f1 .hgignore
1025 // or
1026 // Index: something with multiple words
1027 // and extract the filename (or whatever else is used as an index name)
1028 // from the end (i.e. 'README.md', '.hgignore', or
1029 // 'something with multiple words' in the examples above).
1030 //
1031 // TODO: It seems awkward that we indiscriminately trim off trailing
1032 // whitespace here. Theoretically, couldn't that be meaningful -
1033 // e.g. if the patch represents a diff of a file whose name ends
1034 // with a space? Seems wrong to nuke it.
1035 // But this behaviour has been around since v2.2.1 in 2015, so if
1036 // it's going to change, it should be done cautiously and in a new
1037 // major release, for backwards-compat reasons.
1038 // -- ExplodingCabbage
1039 const headerMatch = (/^(?:Index:|diff(?: -r \w+)+)\s+/).exec(line);
1040 if (headerMatch) {
1041 index.index = line.substring(headerMatch[0].length).trim();
1042 }
1043 i++;
1044 }
1045 // Parse file headers if they are defined. Unified diff requires them, but
1046 // there's no technical issues to have an isolated hunk without file header
1047 parseFileHeader(index);
1048 parseFileHeader(index);
1049 // Parse hunks
1050 index.hunks = [];
1051 while (i < diffstr.length) {
1052 const line = diffstr[i];
1053 if ((/^(Index:\s|diff\s|---\s|\+\+\+\s|===================================================================)/).test(line)) {
1054 break;
1055 }
1056 else if ((/^@@/).test(line)) {
1057 index.hunks.push(parseHunk());
1058 }
1059 else if (line) {
1060 throw new Error('Unknown line ' + (i + 1) + ' ' + JSON.stringify(line));
1061 }
1062 else {
1063 i++;
1064 }
1065 }
1066 }
1067 // Parses the --- and +++ headers, if none are found, no lines
1068 // are consumed.
1069 function parseFileHeader(index) {
1070 const fileHeaderMatch = (/^(---|\+\+\+)\s+/).exec(diffstr[i]);
1071 if (fileHeaderMatch) {
1072 const prefix = fileHeaderMatch[1], data = diffstr[i].substring(3).trim().split('\t', 2), header = (data[1] || '').trim();
1073 let fileName = data[0].replace(/\\\\/g, '\\');
1074 if (fileName.startsWith('"') && fileName.endsWith('"')) {
1075 fileName = fileName.substr(1, fileName.length - 2);
1076 }
1077 if (prefix === '---') {
1078 index.oldFileName = fileName;
1079 index.oldHeader = header;
1080 }
1081 else {
1082 index.newFileName = fileName;
1083 index.newHeader = header;
1084 }
1085 i++;
1086 }
1087 }
1088 // Parses a hunk
1089 // This assumes that we are at the start of a hunk.
1090 function parseHunk() {
1091 var _a;
1092 const chunkHeaderIndex = i, chunkHeaderLine = diffstr[i++], chunkHeader = chunkHeaderLine.split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
1093 const hunk = {
1094 oldStart: +chunkHeader[1],
1095 oldLines: typeof chunkHeader[2] === 'undefined' ? 1 : +chunkHeader[2],
1096 newStart: +chunkHeader[3],
1097 newLines: typeof chunkHeader[4] === 'undefined' ? 1 : +chunkHeader[4],
1098 lines: []
1099 };
1100 // Unified Diff Format quirk: If the chunk size is 0,
1101 // the first number is one lower than one would expect.
1102 // https://www.artima.com/weblogs/viewpost.jsp?thread=164293
1103 if (hunk.oldLines === 0) {
1104 hunk.oldStart += 1;
1105 }
1106 if (hunk.newLines === 0) {
1107 hunk.newStart += 1;
1108 }
1109 let addCount = 0, removeCount = 0;
1110 for (; i < diffstr.length && (removeCount < hunk.oldLines || addCount < hunk.newLines || ((_a = diffstr[i]) === null || _a === void 0 ? void 0 : _a.startsWith('\\'))); i++) {
1111 const operation = (diffstr[i].length == 0 && i != (diffstr.length - 1)) ? ' ' : diffstr[i][0];
1112 if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') {
1113 hunk.lines.push(diffstr[i]);
1114 if (operation === '+') {
1115 addCount++;
1116 }
1117 else if (operation === '-') {
1118 removeCount++;
1119 }
1120 else if (operation === ' ') {
1121 addCount++;
1122 removeCount++;
1123 }
1124 }
1125 else {
1126 throw new Error(`Hunk at line ${chunkHeaderIndex + 1} contained invalid line ${diffstr[i]}`);
1127 }
1128 }
1129 // Handle the empty block count case
1130 if (!addCount && hunk.newLines === 1) {
1131 hunk.newLines = 0;
1132 }
1133 if (!removeCount && hunk.oldLines === 1) {
1134 hunk.oldLines = 0;
1135 }
1136 // Perform sanity checking
1137 if (addCount !== hunk.newLines) {
1138 throw new Error('Added line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
1139 }
1140 if (removeCount !== hunk.oldLines) {
1141 throw new Error('Removed line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
1142 }
1143 return hunk;
1144 }
1145 while (i < diffstr.length) {
1146 parseIndex();
1147 }
1148 return list;
1149 }
1150
1151 // Iterator that traverses in the range of [min, max], stepping
1152 // by distance from a given start position. I.e. for [0, 4], with
1153 // start of 2, this will iterate 2, 3, 1, 4, 0.
1154 function distanceIterator (start, minLine, maxLine) {
1155 let wantForward = true, backwardExhausted = false, forwardExhausted = false, localOffset = 1;
1156 return function iterator() {
1157 if (wantForward && !forwardExhausted) {
1158 if (backwardExhausted) {
1159 localOffset++;
1160 }
1161 else {
1162 wantForward = false;
1163 }
1164 // Check if trying to fit beyond text length, and if not, check it fits
1165 // after offset location (or desired location on first iteration)
1166 if (start + localOffset <= maxLine) {
1167 return start + localOffset;
1168 }
1169 forwardExhausted = true;
1170 }
1171 if (!backwardExhausted) {
1172 if (!forwardExhausted) {
1173 wantForward = true;
1174 }
1175 // Check if trying to fit before text beginning, and if not, check it fits
1176 // before offset location
1177 if (minLine <= start - localOffset) {
1178 return start - localOffset++;
1179 }
1180 backwardExhausted = true;
1181 return iterator();
1182 }
1183 // We tried to fit hunk before text beginning and beyond text length, then
1184 // hunk can't fit on the text. Return undefined
1185 return undefined;
1186 };
1187 }
1188
1189 /**
1190 * attempts to apply a unified diff patch.
1191 *
1192 * Hunks are applied first to last.
1193 * `applyPatch` first tries to apply the first hunk at the line number specified in the hunk header, and with all context lines matching exactly.
1194 * 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.
1195 * 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.
1196 * 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.
1197 *
1198 * Once a hunk is successfully fitted, the process begins again with the next hunk.
1199 * Regardless of `fuzzFactor`, later hunks must be applied later in the file than earlier hunks.
1200 *
