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 function trailingWs(string) {
372 // Yes, this looks overcomplicated and dumb - why not replace the whole function with
373 // return string.match(/\s*$/)[0]
374 // you ask? Because:
375 // 1. the trap described at https://markamery.com/blog/quadratic-time-regexes/ would mean doing
376 // this would cause this function to take O(n²) time in the worst case (specifically when
377 // there is a massive run of NON-TRAILING whitespace in `string`), and
378 // 2. the fix proposed in the same blog post, of using a negative lookbehind, is incompatible
379 // with old Safari versions that we'd like to not break if possible (see
380 // https://github.com/kpdecker/jsdiff/pull/550)
381 // It feels absurd to do this with an explicit loop instead of a regex, but I really can't see a
382 // better way that doesn't result in broken behaviour.
383 let i;
384 for (i = string.length - 1; i >= 0; i--) {
385 if (!string[i].match(/\s/)) {
386 break;
387 }
388 }
389 return string.substring(i + 1);
390 }
391 function leadingWs(string) {
392 // Thankfully the annoying considerations described in trailingWs don't apply here:
393 const match = string.match(/^\s*/);
394 return match ? match[0] : '';
395 }
396
397 // Based on https://en.wikipedia.org/wiki/Latin_script_in_Unicode
398 //
399 // Chars/ranges counted as "word" characters by this regex are as follows:
400 //
401 // + U+00AD Soft hyphen
402 // + 00C0–00FF (letters with diacritics from the Latin-1 Supplement), except:
403 // - U+00D7 × Multiplication sign
404 // - U+00F7 ÷ Division sign
405 // + Latin Extended-A, 0100–017F
406 // + Latin Extended-B, 0180–024F
407 // + IPA Extensions, 0250–02AF
408 // + Spacing Modifier Letters, 02B0–02FF, except:
409 // - U+02C7 ˇ ˇ Caron
410 // - U+02D8 ˘ ˘ Breve
411 // - U+02D9 ˙ ˙ Dot Above
412 // - U+02DA ˚ ˚ Ring Above
413 // - U+02DB ˛ ˛ Ogonek
414 // - U+02DC ˜ ˜ Small Tilde
415 // - U+02DD ˝ ˝ Double Acute Accent
416 // + Latin Extended Additional, 1E00–1EFF
417 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}';
418 // Each token is one of the following:
419 // - A punctuation mark plus the surrounding whitespace
420 // - A word plus the surrounding whitespace
421 // - Pure whitespace (but only in the special case where the entire text
422 // is just whitespace)
423 //
424 // We have to include surrounding whitespace in the tokens because the two
425 // alternative approaches produce horribly broken results:
426 // * If we just discard the whitespace, we can't fully reproduce the original
427 // text from the sequence of tokens and any attempt to render the diff will
428 // get the whitespace wrong.
429 // * If we have separate tokens for whitespace, then in a typical text every
430 // second token will be a single space character. But this often results in
431 // the optimal diff between two texts being a perverse one that preserves
432 // the spaces between words but deletes and reinserts actual common words.
433 // See https://github.com/kpdecker/jsdiff/issues/160#issuecomment-1866099640
434 // for an example.
435 //
436 // Keeping the surrounding whitespace of course has implications for .equals
437 // and .join, not just .tokenize.
438 // This regex does NOT fully implement the tokenization rules described above.
439 // Instead, it gives runs of whitespace their own "token". The tokenize method
440 // then handles stitching whitespace tokens onto adjacent word or punctuation
441 // tokens.
442 const tokenizeIncludingWhitespace = new RegExp(`[${extendedWordChars}]+|\\s+|[^${extendedWordChars}]`, 'ug');
443 class WordDiff extends Diff {
444 equals(left, right, options) {
445 if (options.ignoreCase) {
446 left = left.toLowerCase();
447 right = right.toLowerCase();
448 }
449 return left.trim() === right.trim();
450 }
451 tokenize(value, options = {}) {
452 let parts;
453 if (options.intlSegmenter) {
454 const segmenter = options.intlSegmenter;
455 if (segmenter.resolvedOptions().granularity != 'word') {
456 throw new Error('The segmenter passed must have a granularity of "word"');
457 }
458 // We want `parts` to be an array whose elements alternate between being
459 // pure whitespace and being pure non-whitespace. This is ALMOST what the
460 // segments returned by a word-based Intl.Segmenter already look like,
461 // and therefore we can ALMOST get what we want by simply doing...
462 // parts = Array.from(segmenter.segment(value), segment => segment.segment);
463 // ... but not QUITE, because there's of one annoying special case: every
464 // newline character gets its own segment, instead of sharing a segment
465 // with other surrounding whitespace. We therefore need to manually merge
466 // consecutive segments of whitespace into a single part:
467 parts = [];
468 for (const segmentObj of Array.from(segmenter.segment(value))) {
469 const segment = segmentObj.segment;
470 if (parts.length && (/\s/).test(parts[parts.length - 1]) && (/\s/).test(segment)) {
471 parts[parts.length - 1] += segment;
472 }
473 else {
474 parts.push(segment);
475 }
476 }
477 }
478 else {
479 parts = value.match(tokenizeIncludingWhitespace) || [];
480 }
481 const tokens = [];
482 let prevPart = null;
483 parts.forEach(part => {
484 if ((/\s/).test(part)) {
485 if (prevPart == null) {
486 tokens.push(part);
487 }
488 else {
489 tokens.push(tokens.pop() + part);
490 }
491 }
492 else if (prevPart != null && (/\s/).test(prevPart)) {
493 if (tokens[tokens.length - 1] == prevPart) {
494 tokens.push(tokens.pop() + part);
495 }
496 else {
497 tokens.push(prevPart + part);
498 }
499 }
500 else {
501 tokens.push(part);
502 }
503 prevPart = part;
504 });
505 return tokens;
506 }
507 join(tokens) {
508 // Tokens being joined here will always have appeared consecutively in the
509 // same text, so we can simply strip off the leading whitespace from all the
510 // tokens except the first (and except any whitespace-only tokens - but such
511 // a token will always be the first and only token anyway) and then join them
512 // and the whitespace around words and punctuation will end up correct.
513 return tokens.map((token, i) => {
514 if (i == 0) {
515 return token;
516 }
517 else {
518 return token.replace((/^\s+/), '');
519 }
520 }).join('');
521 }
522 postProcess(changes, options) {
523 if (!changes || options.oneChangePerToken) {
524 return changes;
525 }
526 let lastKeep = null;
527 // Change objects representing any insertion or deletion since the last
528 // "keep" change object. There can be at most one of each.
529 let insertion = null;
530 let deletion = null;
531 changes.forEach(change => {
532 if (change.added) {
533 insertion = change;
534 }
535 else if (change.removed) {
536 deletion = change;
537 }
538 else {
539 if (insertion || deletion) { // May be false at start of text
540 dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change);
541 }
542 lastKeep = change;
543 insertion = null;
544 deletion = null;
545 }
546 });
547 if (insertion || deletion) {
548 dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null);
549 }
550 return changes;
551 }
552 }
553 const wordDiff = new WordDiff();
554 function diffWords(oldStr, newStr, options) {
555 // This option has never been documented and never will be (it's clearer to
556 // just call `diffWordsWithSpace` directly if you need that behavior), but
557 // has existed in jsdiff for a long time, so we retain support for it here
558 // for the sake of backwards compatibility.
559 if ((options === null || options === void 0 ? void 0 : options.ignoreWhitespace) != null && !options.ignoreWhitespace) {
560 return diffWordsWithSpace(oldStr, newStr, options);
561 }
562 return wordDiff.diff(oldStr, newStr, options);
563 }
564 function dedupeWhitespaceInChangeObjects(startKeep, deletion, insertion, endKeep) {
565 // Before returning, we tidy up the leading and trailing whitespace of the
566 // change objects to eliminate cases where trailing whitespace in one object
567 // is repeated as leading whitespace in the next.
568 // Below are examples of the outcomes we want here to explain the code.
569 // I=insert, K=keep, D=delete
570 // 1. diffing 'foo bar baz' vs 'foo baz'
571 // Prior to cleanup, we have K:'foo ' D:' bar ' K:' baz'
572 // After cleanup, we want: K:'foo ' D:'bar ' K:'baz'
573 //
574 // 2. Diffing 'foo bar baz' vs 'foo qux baz'
575 // Prior to cleanup, we have K:'foo ' D:' bar ' I:' qux ' K:' baz'
576 // After cleanup, we want K:'foo ' D:'bar' I:'qux' K:' baz'
577 //
578 // 3. Diffing 'foo\nbar baz' vs 'foo baz'
579 // Prior to cleanup, we have K:'foo ' D:'\nbar ' K:' baz'
580 // After cleanup, we want K'foo' D:'\nbar' K:' baz'
581 //
582 // 4. Diffing 'foo baz' vs 'foo\nbar baz'
583 // Prior to cleanup, we have K:'foo\n' I:'\nbar ' K:' baz'
584 // After cleanup, we ideally want K'foo' I:'\nbar' K:' baz'
585 // but don't actually manage this currently (the pre-cleanup change
586 // objects don't contain enough information to make it possible).
587 //
588 // 5. Diffing 'foo bar baz' vs 'foo baz'
589 // Prior to cleanup, we have K:'foo ' D:' bar ' K:' baz'
590 // After cleanup, we want K:'foo ' D:' bar ' K:'baz'
591 //
592 // Our handling is unavoidably imperfect in the case where there's a single
593 // indel between keeps and the whitespace has changed. For instance, consider
594 // diffing 'foo\tbar\nbaz' vs 'foo baz'. Unless we create an extra change
595 // object to represent the insertion of the space character (which isn't even
596 // a token), we have no way to avoid losing information about the texts'
597 // original whitespace in the result we return. Still, we do our best to
598 // output something that will look sensible if we e.g. print it with
599 // insertions in green and deletions in red.
600 // Between two "keep" change objects (or before the first or after the last
601 // change object), we can have either:
602 // * A "delete" followed by an "insert"
603 // * Just an "insert"
604 // * Just a "delete"
605 // We handle the three cases separately.
606 if (deletion && insertion) {
607 const oldWsPrefix = leadingWs(deletion.value);
608 const oldWsSuffix = trailingWs(deletion.value);
609 const newWsPrefix = leadingWs(insertion.value);
610 const newWsSuffix = trailingWs(insertion.value);
611 if (startKeep) {
612 const commonWsPrefix = longestCommonPrefix(oldWsPrefix, newWsPrefix);
613 startKeep.value = replaceSuffix(startKeep.value, newWsPrefix, commonWsPrefix);
614 deletion.value = removePrefix(deletion.value, commonWsPrefix);
615 insertion.value = removePrefix(insertion.value, commonWsPrefix);
616 }
617 if (endKeep) {
618 const commonWsSuffix = longestCommonSuffix(oldWsSuffix, newWsSuffix);
619 endKeep.value = replacePrefix(endKeep.value, newWsSuffix, commonWsSuffix);
620 deletion.value = removeSuffix(deletion.value, commonWsSuffix);
621 insertion.value = removeSuffix(insertion.value, commonWsSuffix);
622 }
623 }
624 else if (insertion) {
625 // The whitespaces all reflect what was in the new text rather than
626 // the old, so we essentially have no information about whitespace
627 // insertion or deletion. We just want to dedupe the whitespace.
628 // We do that by having each change object keep its trailing
629 // whitespace and deleting duplicate leading whitespace where
630 // present.
631 if (startKeep) {
632 const ws = leadingWs(insertion.value);
633 insertion.value = insertion.value.substring(ws.length);
634 }
635 if (endKeep) {
636 const ws = leadingWs(endKeep.value);
637 endKeep.value = endKeep.value.substring(ws.length);
638 }
639 // otherwise we've got a deletion and no insertion
640 }
641 else if (startKeep && endKeep) {
642 const newWsFull = leadingWs(endKeep.value), delWsStart = leadingWs(deletion.value), delWsEnd = trailingWs(deletion.value);
643 // Any whitespace that comes straight after startKeep in both the old and
644 // new texts, assign to startKeep and remove from the deletion.
645 const newWsStart = longestCommonPrefix(newWsFull, delWsStart);
646 deletion.value = removePrefix(deletion.value, newWsStart);
647 // Any whitespace that comes straight before endKeep in both the old and
648 // new texts, and hasn't already been assigned to startKeep, assign to
649 // endKeep and remove from the deletion.
650 const newWsEnd = longestCommonSuffix(removePrefix(newWsFull, newWsStart), delWsEnd);
651 deletion.value = removeSuffix(deletion.value, newWsEnd);
652 endKeep.value = replacePrefix(endKeep.value, newWsFull, newWsEnd);
653 // If there's any whitespace from the new text that HASN'T already been
654 // assigned, assign it to the start:
655 startKeep.value = replaceSuffix(startKeep.value, newWsFull, newWsFull.slice(0, newWsFull.length - newWsEnd.length));
656 }
657 else if (endKeep) {
658 // We are at the start of the text. Preserve all the whitespace on
659 // endKeep, and just remove whitespace from the end of deletion to the
660 // extent that it overlaps with the start of endKeep.
661 const endKeepWsPrefix = leadingWs(endKeep.value);
662 const deletionWsSuffix = trailingWs(deletion.value);
663 const overlap = maximumOverlap(deletionWsSuffix, endKeepWsPrefix);
664 deletion.value = removeSuffix(deletion.value, overlap);
665 }
666 else if (startKeep) {
667 // We are at the END of the text. Preserve all the whitespace on
668 // startKeep, and just remove whitespace from the start of deletion to
669 // the extent that it overlaps with the end of startKeep.
670 const startKeepWsSuffix = trailingWs(startKeep.value);
671 const deletionWsPrefix = leadingWs(deletion.value);
672 const overlap = maximumOverlap(startKeepWsSuffix, deletionWsPrefix);
673 deletion.value = removePrefix(deletion.value, overlap);
674 }
675 }
676 class WordsWithSpaceDiff extends Diff {
677 tokenize(value) {
678 // Slightly different to the tokenizeIncludingWhitespace regex used above in
679 // that this one treats each individual newline as a distinct token, rather
680 // than merging them into other surrounding whitespace. This was requested
681 // in https://github.com/kpdecker/jsdiff/issues/180 &
682 // https://github.com/kpdecker/jsdiff/issues/211
683 const regex = new RegExp(`(\\r?\\n)|[${extendedWordChars}]+|[^\\S\\n\\r]+|[^${extendedWordChars}]`, 'ug');
684 return value.match(regex) || [];
685 }
686 }
687 const wordsWithSpaceDiff = new WordsWithSpaceDiff();
688 function diffWordsWithSpace(oldStr, newStr, options) {
689 return wordsWithSpaceDiff.diff(oldStr, newStr, options);
690 }
691
692 function generateOptions(options, defaults) {
693 if (typeof options === 'function') {
694 defaults.callback = options;
695 }
696 else if (options) {
697 for (const name in options) {
698 /* istanbul ignore else */
699 if (Object.prototype.hasOwnProperty.call(options, name)) {
700 defaults[name] = options[name];
701 }
702 }
703 }
704 return defaults;
705 }
706
707 class LineDiff extends Diff {
708 constructor() {
709 super(...arguments);
710 this.tokenize = tokenize;
711 }
712 equals(left, right, options) {
713 // If we're ignoring whitespace, we need to normalise lines by stripping
714 // whitespace before checking equality. (This has an annoying interaction
715 // with newlineIsToken that requires special handling: if newlines get their
716 // own token, then we DON'T want to trim the *newline* tokens down to empty
717 // strings, since this would cause us to treat whitespace-only line content
718 // as equal to a separator between lines, which would be weird and
719 // inconsistent with the documented behavior of the options.)
720 if (options.ignoreWhitespace) {
721 if (!options.newlineIsToken || !left.includes('\n')) {
722 left = left.trim();
723 }
724 if (!options.newlineIsToken || !right.includes('\n')) {
725 right = right.trim();
726 }
727 }
728 else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
729 if (left.endsWith('\n')) {
730 left = left.slice(0, -1);
731 }
732 if (right.endsWith('\n')) {
733 right = right.slice(0, -1);
734 }
735 }
736 return super.equals(left, right, options);
737 }
738 }
739 const lineDiff = new LineDiff();
740 function diffLines(oldStr, newStr, options) {
741 return lineDiff.diff(oldStr, newStr, options);
742 }
743 function diffTrimmedLines(oldStr, newStr, options) {
744 options = generateOptions(options, { ignoreWhitespace: true });
745 return lineDiff.diff(oldStr, newStr, options);
746 }
747 // Exported standalone so it can be used from jsonDiff too.
748 function tokenize(value, options) {
749 if (options.stripTrailingCr) {
750 // remove one \r before \n to match GNU diff's --strip-trailing-cr behavior
751 value = value.replace(/\r\n/g, '\n');
752 }
753 const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
754 // Ignore the final empty token that occurs if the string ends with a new line
755 if (!linesAndNewlines[linesAndNewlines.length - 1]) {
756 linesAndNewlines.pop();
757 }
758 // Merge the content and line separators into single tokens
759 for (let i = 0; i < linesAndNewlines.length; i++) {
760 const line = linesAndNewlines[i];
761 if (i % 2 && !options.newlineIsToken) {
762 retLines[retLines.length - 1] += line;
763 }
764 else {
765 retLines.push(line);
766 }
767 }
768 return retLines;
769 }
770
771 function isSentenceEndPunct(char) {
772 return char == '.' || char == '!' || char == '?';
773 }
774 class SentenceDiff extends Diff {
775 tokenize(value) {
776 var _a;
777 // If in future we drop support for environments that don't support lookbehinds, we can replace
778 // this entire function with:
779 // return value.split(/(?<=[.!?])(\s+|$)/);
780 // but until then, for similar reasons to the trailingWs function in string.ts, we are forced
781 // to do this verbosely "by hand" instead of using a regex.
782 const result = [];
783 let tokenStartI = 0;
784 for (let i = 0; i < value.length; i++) {
785 if (i == value.length - 1) {
786 result.push(value.slice(tokenStartI));
787 break;
788 }
789 if (isSentenceEndPunct(value[i]) && value[i + 1].match(/\s/)) {
790 // We've hit a sentence break - i.e. a punctuation mark followed by whitespace.
791 // We now want to push TWO tokens to the result:
792 // 1. the sentence
793 result.push(value.slice(tokenStartI, i + 1));
794 // 2. the whitespace
795 i = tokenStartI = i + 1;
796 while ((_a = value[i + 1]) === null || _a === void 0 ? void 0 : _a.match(/\s/)) {
797 i++;
798 }
799 result.push(value.slice(tokenStartI, i + 1));
800 // Then the next token (a sentence) starts on the character after the whitespace.
801 // (It's okay if this is off the end of the string - then the outer loop will terminate
802 // here anyway.)
803 tokenStartI = i + 1;
804 }
805 }
806 return result;
807 }
808 }
809 const sentenceDiff = new SentenceDiff();
810 function diffSentences(oldStr, newStr, options) {
811 return sentenceDiff.diff(oldStr, newStr, options);
812 }
813
814 class CssDiff extends Diff {
815 tokenize(value) {
816 return value.split(/([{}:;,]|\s+)/);
817 }
818 }
819 const cssDiff = new CssDiff();
820 function diffCss(oldStr, newStr, options) {
821 return cssDiff.diff(oldStr, newStr, options);
822 }
823
824 class JsonDiff extends Diff {
825 constructor() {
826 super(...arguments);
827 this.tokenize = tokenize;
828 }
829 get useLongestToken() {
830 // Discriminate between two lines of pretty-printed, serialized JSON where one of them has a
831 // dangling comma and the other doesn't. Turns out including the dangling comma yields the nicest output:
832 return true;
833 }
834 castInput(value, options) {
835 const { undefinedReplacement, stringifyReplacer = (k, v) => typeof v === 'undefined' ? undefinedReplacement : v } = options;
836 return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), null, ' ');
837 }
838 equals(left, right, options) {
839 return super.equals(left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1'), options);
840 }
841 }
842 const jsonDiff = new JsonDiff();
843 function diffJson(oldStr, newStr, options) {
844 return jsonDiff.diff(oldStr, newStr, options);
845 }
846 // This function handles the presence of circular references by bailing out when encountering an
847 // object that is already on the "stack" of items being processed. Accepts an optional replacer
848 function canonicalize(obj, stack, replacementStack, replacer, key) {
849 stack = stack || [];
850 replacementStack = replacementStack || [];
851 if (replacer) {
852 obj = replacer(key === undefined ? '' : key, obj);
853 }
854 let i;
855 for (i = 0; i < stack.length; i += 1) {
856 if (stack[i] === obj) {
857 return replacementStack[i];
858 }
859 }
860 let canonicalizedObj;
861 if ('[object Array]' === Object.prototype.toString.call(obj)) {
862 stack.push(obj);
863 canonicalizedObj = new Array(obj.length);
864 replacementStack.push(canonicalizedObj);
865 for (i = 0; i < obj.length; i += 1) {
866 canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, String(i));
867 }
868 stack.pop();
869 replacementStack.pop();
870 return canonicalizedObj;
871 }
872 if (obj && obj.toJSON) {
873 obj = obj.toJSON();
874 }
875 if (typeof obj === 'object' && obj !== null) {
876 stack.push(obj);
877 canonicalizedObj = {};
878 replacementStack.push(canonicalizedObj);
879 const sortedKeys = [];
880 let key;
881 for (key in obj) {
882 /* istanbul ignore else */
883 if (Object.prototype.hasOwnProperty.call(obj, key)) {
884 sortedKeys.push(key);
885 }
886 }
887 sortedKeys.sort();
888 for (i = 0; i < sortedKeys.length; i += 1) {
889 key = sortedKeys[i];
890 canonicalizedObj[key] = canonicalize(obj[key], stack, replacementStack, replacer, key);
891 }
892 stack.pop();
893 replacementStack.pop();
894 }
895 else {
896 canonicalizedObj = obj;
897 }
898 return canonicalizedObj;
899 }
900
901 class ArrayDiff extends Diff {
902 tokenize(value) {
903 return value.slice();
904 }
905 join(value) {
906 return value;
907 }
908 removeEmpty(value) {
909 return value;
910 }
911 }
912 const arrayDiff = new ArrayDiff();
913 function diffArrays(oldArr, newArr, options) {
914 return arrayDiff.diff(oldArr, newArr, options);
915 }
916
917 function unixToWin(patch) {
918 if (Array.isArray(patch)) {
919 // It would be cleaner if instead of the line below we could just write
920 // return patch.map(unixToWin)
921 // but mysteriously TypeScript (v5.7.3 at the time of writing) does not like this and it will
922 // refuse to compile, thinking that unixToWin could then return StructuredPatch[][] and the
923 // result would be incompatible with the overload signatures.
924 // See bug report at https://github.com/microsoft/TypeScript/issues/61398.
925 return patch.map(p => unixToWin(p));
926 }
927 return Object.assign(Object.assign({}, patch), { hunks: patch.hunks.map(hunk => (Object.assign(Object.assign({}, hunk), { lines: hunk.lines.map((line, i) => {
928 var _a;
929 return (line.startsWith('\\') || line.endsWith('\r') || ((_a = hunk.lines[i + 1]) === null || _a === void 0 ? void 0 : _a.startsWith('\\')))
930 ? line
931 : line + '\r';
932 }) }))) });
933 }
934 function winToUnix(patch) {
935 if (Array.isArray(patch)) {
936 // (See comment above equivalent line in unixToWin)
937 return patch.map(p => winToUnix(p));
938 }
939 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) }))) });
940 }
941 /**
942 * Returns true if the patch consistently uses Unix line endings (or only involves one line and has
943 * no line endings).
944 */
945 function isUnix(patch) {
946 if (!Array.isArray(patch)) {
947 patch = [patch];
948 }
949 return !patch.some(index => index.hunks.some(hunk => hunk.lines.some(line => !line.startsWith('\\') && line.endsWith('\r'))));
950 }
951 /**
952 * Returns true if the patch uses Windows line endings and only Windows line endings.
953 */
954 function isWin(patch) {
955 if (!Array.isArray(patch)) {
956 patch = [patch];
957 }
958 return patch.some(index => index.hunks.some(hunk => hunk.lines.some(line => line.endsWith('\r'))))
959 && 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('\\')); })));
960 }
961
962 /**
963 * Parses a patch into structured data, in the same structure returned by `structuredPatch`.
964 *
965 * @return a JSON object representation of the a patch, suitable for use with the `applyPatch` method.
966 */
967 function parsePatch(uniDiff) {
968 const diffstr = uniDiff.split(/\n/), list = [];
969 let i = 0;
970 function parseIndex() {
971 const index = {};
972 list.push(index);
973 // Parse diff metadata
974 while (i < diffstr.length) {
975 const line = diffstr[i];
976 // File header found, end parsing diff metadata
977 if ((/^(---|\+\+\+|@@)\s/).test(line)) {
978 break;
979 }
980 // Try to parse the line as a diff header, like
981 // Index: README.md
982 // or
983 // diff -r 9117c6561b0b -r 273ce12ad8f1 .hgignore
984 // or
985 // Index: something with multiple words
986 // and extract the filename (or whatever else is used as an index name)
987 // from the end (i.e. 'README.md', '.hgignore', or
988 // 'something with multiple words' in the examples above).
989 //
990 // TODO: It seems awkward that we indiscriminately trim off trailing
991 // whitespace here. Theoretically, couldn't that be meaningful -
992 // e.g. if the patch represents a diff of a file whose name ends
993 // with a space? Seems wrong to nuke it.
994 // But this behaviour has been around since v2.2.1 in 2015, so if
995 // it's going to change, it should be done cautiously and in a new
996 // major release, for backwards-compat reasons.
997 // -- ExplodingCabbage
998 const headerMatch = (/^(?:Index:|diff(?: -r \w+)+)\s+/).exec(line);
999 if (headerMatch) {
1000 index.index = line.substring(headerMatch[0].length).trim();
1001 }
1002 i++;
1003 }
1004 // Parse file headers if they are defined. Unified diff requires them, but
1005 // there's no technical issues to have an isolated hunk without file header
1006 parseFileHeader(index);
1007 parseFileHeader(index);
1008 // Parse hunks
1009 index.hunks = [];
1010 while (i < diffstr.length) {
1011 const line = diffstr[i];
1012 if ((/^(Index:\s|diff\s|---\s|\+\+\+\s|===================================================================)/).test(line)) {
1013 break;
1014 }
1015 else if ((/^@@/).test(line)) {
1016 index.hunks.push(parseHunk());
1017 }
1018 else if (line) {
1019 throw new Error('Unknown line ' + (i + 1) + ' ' + JSON.stringify(line));
1020 }
1021 else {
1022 i++;
1023 }
1024 }
1025 }
1026 // Parses the --- and +++ headers, if none are found, no lines
1027 // are consumed.
1028 function parseFileHeader(index) {
1029 const fileHeaderMatch = (/^(---|\+\+\+)\s+/).exec(diffstr[i]);
1030 if (fileHeaderMatch) {
1031 const prefix = fileHeaderMatch[1], data = diffstr[i].substring(3).trim().split('\t', 2), header = (data[1] || '').trim();
1032 let fileName = data[0].replace(/\\\\/g, '\\');
1033 if (fileName.startsWith('"') && fileName.endsWith('"')) {
1034 fileName = fileName.substr(1, fileName.length - 2);
1035 }
1036 if (prefix === '---') {
1037 index.oldFileName = fileName;
1038 index.oldHeader = header;
1039 }
1040 else {
1041 index.newFileName = fileName;
1042 index.newHeader = header;
1043 }
1044 i++;
1045 }
1046 }
1047 // Parses a hunk
1048 // This assumes that we are at the start of a hunk.
1049 function parseHunk() {
1050 var _a;
1051 const chunkHeaderIndex = i, chunkHeaderLine = diffstr[i++], chunkHeader = chunkHeaderLine.split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
1052 const hunk = {
1053 oldStart: +chunkHeader[1],
1054 oldLines: typeof chunkHeader[2] === 'undefined' ? 1 : +chunkHeader[2],
1055 newStart: +chunkHeader[3],
1056 newLines: typeof chunkHeader[4] === 'undefined' ? 1 : +chunkHeader[4],
1057 lines: []
1058 };
1059 // Unified Diff Format quirk: If the chunk size is 0,
1060 // the first number is one lower than one would expect.
1061 // https://www.artima.com/weblogs/viewpost.jsp?thread=164293
1062 if (hunk.oldLines === 0) {
1063 hunk.oldStart += 1;
1064 }
1065 if (hunk.newLines === 0) {
1066 hunk.newStart += 1;
1067 }
1068 let addCount = 0, removeCount = 0;
1069 for (; i < diffstr.length && (removeCount < hunk.oldLines || addCount < hunk.newLines || ((_a = diffstr[i]) === null || _a === void 0 ? void 0 : _a.startsWith('\\'))); i++) {
1070 const operation = (diffstr[i].length == 0 && i != (diffstr.length - 1)) ? ' ' : diffstr[i][0];
1071 if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') {
1072 hunk.lines.push(diffstr[i]);
1073 if (operation === '+') {
1074 addCount++;
1075 }
1076 else if (operation === '-') {
1077 removeCount++;
1078 }
1079 else if (operation === ' ') {
1080 addCount++;
1081 removeCount++;
1082 }
1083 }
1084 else {
1085 throw new Error(`Hunk at line ${chunkHeaderIndex + 1} contained invalid line ${diffstr[i]}`);
1086 }
1087 }
1088 // Handle the empty block count case
1089 if (!addCount && hunk.newLines === 1) {
1090 hunk.newLines = 0;
1091 }
1092 if (!removeCount && hunk.oldLines === 1) {
1093 hunk.oldLines = 0;
1094 }
1095 // Perform sanity checking
1096 if (addCount !== hunk.newLines) {
1097 throw new Error('Added line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
1098 }
1099 if (removeCount !== hunk.oldLines) {
1100 throw new Error('Removed line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
1101 }
1102 return hunk;
1103 }
1104 while (i < diffstr.length) {
1105 parseIndex();
1106 }
1107 return list;
1108 }
1109
1110 // Iterator that traverses in the range of [min, max], stepping
1111 // by distance from a given start position. I.e. for [0, 4], with
1112 // start of 2, this will iterate 2, 3, 1, 4, 0.
1113 function distanceIterator (start, minLine, maxLine) {
1114 let wantForward = true, backwardExhausted = false, forwardExhausted = false, localOffset = 1;
1115 return function iterator() {
1116 if (wantForward && !forwardExhausted) {
1117 if (backwardExhausted) {
1118 localOffset++;
1119 }
1120 else {
1121 wantForward = false;
1122 }
1123 // Check if trying to fit beyond text length, and if not, check it fits
1124 // after offset location (or desired location on first iteration)
1125 if (start + localOffset <= maxLine) {
1126 return start + localOffset;
1127 }
1128 forwardExhausted = true;
1129 }
1130 if (!backwardExhausted) {
1131 if (!forwardExhausted) {
1132 wantForward = true;
1133 }
1134 // Check if trying to fit before text beginning, and if not, check it fits
1135 // before offset location
1136 if (minLine <= start - localOffset) {
1137 return start - localOffset++;
1138 }
1139 backwardExhausted = true;
1140 return iterator();
1141 }
1142 // We tried to fit hunk before text beginning and beyond text length, then
1143 // hunk can't fit on the text. Return undefined
1144 return undefined;
1145 };
1146 }
1147
1148 /**
1149 * attempts to apply a unified diff patch.
1150 *
1151 * Hunks are applied first to last.
1152 * `applyPatch` first tries to apply the first hunk at the line number specified in the hunk header, and with all context lines matching exactly.
1153 * 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.
1154 * 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.
1155 * 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.
1156 *
1157 * Once a hunk is successfully fitted, the process begins again with the next hunk.
1158 * Regardless of `fuzzFactor`, later hunks must be applied later in the file than earlier hunks.
1159 *
1160 * If a hunk cannot be successfully fitted *anywhere* with fewer than `fuzzFactor` mismatches, `applyPatch` fails and returns `false`.
1161 *
1162 * If a hunk is successfully fitted but not at the line number specified by the hunk header, all subsequent hunks have their target line number adjusted accordingly.
1163 * (e.g. if the first hunk is applied 10 lines below where the hunk header said it should fit, `applyPatch` will *start* looking for somewhere to apply the second hunk 10 lines below where its hunk header says it goes.)
1164 *
1165 * If the patch was applied successfully, returns a string containing the patched text.
1166 * If the patch could not be applied (because some hunks in the patch couldn't be fitted to the text in `source`), `applyPatch` returns false.
1167 *
1168 * @param patch a string diff or the output from the `parsePatch` or `structuredPatch` methods.
1169 */
1170 function applyPatch(source, patch, options = {}) {
1171 let patches;
1172 if (typeof patch === 'string') {
1173 patches = parsePatch(patch);
1174 }
1175 else if (Array.isArray(patch)) {
1176 patches = patch;
1177 }
1178 else {
1179 patches = [patch];
1180 }
1181 if (patches.length > 1) {
1182 throw new Error('applyPatch only works with a single input.');
1183 }
1184 return applyStructuredPatch(source, patches[0], options);
1185 }
1186 function applyStructuredPatch(source, patch, options = {}) {
1187 if (options.autoConvertLineEndings || options.autoConvertLineEndings == null) {
1188 if (hasOnlyWinLineEndings(source) && isUnix(patch)) {
1189 patch = unixToWin(patch);
1190 }
1191 else if (hasOnlyUnixLineEndings(source) && isWin(patch)) {
1192 patch = winToUnix(patch);
1193 }
1194 }
1195 // Apply the diff to the input
1196 const lines = source.split('\n'), hunks = patch.hunks, compareLine = options.compareLine || ((lineNumber, line, operation, patchContent) => line === patchContent), fuzzFactor = options.fuzzFactor || 0;
1197 let minLine = 0;
1198 if (fuzzFactor < 0 || !Number.isInteger(fuzzFactor)) {
1199 throw new Error('fuzzFactor must be a non-negative integer');
1200 }
