diff.js 76 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674
  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. class Diff {
  7. diff(oldStr, newStr,
  8. // Type below is not accurate/complete - see above for full possibilities - but it compiles
  9. options = {}) {
  10. let callback;
  11. if (typeof options === 'function') {
  12. callback = options;
  13. options = {};
  14. }
  15. else if ('callback' in options) {
  16. callback = options.callback;
  17. }
  18. // Allow subclasses to massage the input prior to running
  19. const oldString = this.castInput(oldStr, options);
  20. const newString = this.castInput(newStr, options);
  21. const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
  22. const newTokens = this.removeEmpty(this.tokenize(newString, options));
  23. return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
  24. }
  25. diffWithOptionsObj(oldTokens, newTokens, options, callback) {
  26. var _a;
  27. const done = (value) => {
  28. value = this.postProcess(value, options);
  29. if (callback) {
  30. setTimeout(function () { callback(value); }, 0);
  31. return undefined;
  32. }
  33. else {
  34. return value;
  35. }
  36. };
  37. const newLen = newTokens.length, oldLen = oldTokens.length;
  38. let editLength = 1;
  39. let maxEditLength = newLen + oldLen;
  40. if (options.maxEditLength != null) {
  41. maxEditLength = Math.min(maxEditLength, options.maxEditLength);
  42. }
  43. const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
  44. const abortAfterTimestamp = Date.now() + maxExecutionTime;
  45. const bestPath = [{ oldPos: -1, lastComponent: undefined }];
  46. // Seed editLength = 0, i.e. the content starts with the same values
  47. let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
  48. if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
  49. // Identity per the equality and tokenizer
  50. return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
  51. }
  52. // Once we hit the right edge of the edit graph on some diagonal k, we can
  53. // definitely reach the end of the edit graph in no more than k edits, so
  54. // there's no point in considering any moves to diagonal k+1 any more (from
  55. // which we're guaranteed to need at least k+1 more edits).
  56. // Similarly, once we've reached the bottom of the edit graph, there's no
  57. // point considering moves to lower diagonals.
  58. // We record this fact by setting minDiagonalToConsider and
  59. // maxDiagonalToConsider to some finite value once we've hit the edge of
  60. // the edit graph.
  61. // This optimization is not faithful to the original algorithm presented in
  62. // Myers's paper, which instead pointlessly extends D-paths off the end of
  63. // the edit graph - see page 7 of Myers's paper which notes this point
  64. // explicitly and illustrates it with a diagram. This has major performance
  65. // implications for some common scenarios. For instance, to compute a diff
  66. // where the new text simply appends d characters on the end of the
  67. // original text of length n, the true Myers algorithm will take O(n+d^2)
  68. // time while this optimization needs only O(n+d) time.
  69. let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
  70. // Main worker method. checks all permutations of a given edit length for acceptance.
  71. const execEditLength = () => {
  72. for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
  73. let basePath;
  74. const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
  75. if (removePath) {
  76. // No one else is going to attempt to use this value, clear it
  77. // @ts-expect-error - perf optimisation. This type-violating value will never be read.
  78. bestPath[diagonalPath - 1] = undefined;
  79. }
  80. let canAdd = false;
  81. if (addPath) {
  82. // what newPos will be after we do an insertion:
  83. const addPathNewPos = addPath.oldPos - diagonalPath;
  84. canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
  85. }
  86. const canRemove = removePath && removePath.oldPos + 1 < oldLen;
  87. if (!canAdd && !canRemove) {
  88. // If this path is a terminal then prune
  89. // @ts-expect-error - perf optimisation. This type-violating value will never be read.
  90. bestPath[diagonalPath] = undefined;
  91. continue;
  92. }
  93. // Select the diagonal that we want to branch from. We select the prior
  94. // path whose position in the old string is the farthest from the origin
  95. // and does not pass the bounds of the diff graph
  96. if (!canRemove || (canAdd && removePath.oldPos < addPath.oldPos)) {
  97. basePath = this.addToPath(addPath, true, false, 0, options);
  98. }
  99. else {
  100. basePath = this.addToPath(removePath, false, true, 1, options);
  101. }
  102. newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
  103. if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
  104. // If we have hit the end of both strings, then we are done
  105. return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
  106. }
  107. else {
  108. bestPath[diagonalPath] = basePath;
  109. if (basePath.oldPos + 1 >= oldLen) {
  110. maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
  111. }
  112. if (newPos + 1 >= newLen) {
  113. minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
  114. }
  115. }
  116. }
  117. editLength++;
  118. };
  119. // Performs the length of edit iteration. Is a bit fugly as this has to support the
  120. // sync and async mode which is never fun. Loops over execEditLength until a value
  121. // is produced, or until the edit length exceeds options.maxEditLength (if given),
  122. // in which case it will return undefined.
  123. if (callback) {
  124. (function exec() {
  125. setTimeout(function () {
  126. if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
  127. return callback(undefined);
  128. }
  129. if (!execEditLength()) {
  130. exec();
  131. }
  132. }, 0);
  133. }());
  134. }
  135. else {
  136. while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
  137. const ret = execEditLength();
  138. if (ret) {
  139. return ret;
  140. }
  141. }
  142. }
  143. }
  144. addToPath(path, added, removed, oldPosInc, options) {
  145. const last = path.lastComponent;
  146. if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
  147. return {
  148. oldPos: path.oldPos + oldPosInc,
  149. lastComponent: { count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent }
  150. };
  151. }
  152. else {
  153. return {
  154. oldPos: path.oldPos + oldPosInc,
  155. lastComponent: { count: 1, added: added, removed: removed, previousComponent: last }
  156. };
  157. }
  158. }
  159. extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
  160. const newLen = newTokens.length, oldLen = oldTokens.length;
  161. let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
  162. while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
  163. newPos++;
  164. oldPos++;
  165. commonCount++;
  166. if (options.oneChangePerToken) {
  167. basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
  168. }
  169. }
  170. if (commonCount && !options.oneChangePerToken) {
  171. basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
  172. }
  173. basePath.oldPos = oldPos;
  174. return newPos;
  175. }
  176. equals(left, right, options) {
  177. if (options.comparator) {
  178. return options.comparator(left, right);
  179. }
  180. else {
  181. return left === right
  182. || (!!options.ignoreCase && left.toLowerCase() === right.toLowerCase());
  183. }
  184. }
  185. removeEmpty(array) {
  186. const ret = [];
  187. for (let i = 0; i < array.length; i++) {
  188. if (array[i]) {
  189. ret.push(array[i]);
  190. }
  191. }
  192. return ret;
  193. }
  194. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  195. castInput(value, options) {
  196. return value;
  197. }
  198. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  199. tokenize(value, options) {
  200. return Array.from(value);
  201. }
  202. join(chars) {
  203. // Assumes ValueT is string, which is the case for most subclasses.
  204. // When it's false, e.g. in diffArrays, this method needs to be overridden (e.g. with a no-op)
  205. // Yes, the casts are verbose and ugly, because this pattern - of having the base class SORT OF
  206. // assume tokens and values are strings, but not completely - is weird and janky.
  207. return chars.join('');
  208. }
  209. postProcess(changeObjects,
  210. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  211. options) {
  212. return changeObjects;
  213. }
  214. get useLongestToken() {
  215. return false;
  216. }
  217. buildValues(lastComponent, newTokens, oldTokens) {
  218. // First we convert our linked list of components in reverse order to an
  219. // array in the right order:
  220. const components = [];
  221. let nextComponent;
  222. while (lastComponent) {
  223. components.push(lastComponent);
  224. nextComponent = lastComponent.previousComponent;
  225. delete lastComponent.previousComponent;
  226. lastComponent = nextComponent;
  227. }
  228. components.reverse();
  229. const componentLen = components.length;
  230. let componentPos = 0, newPos = 0, oldPos = 0;
  231. for (; componentPos < componentLen; componentPos++) {
  232. const component = components[componentPos];
  233. if (!component.removed) {
  234. if (!component.added && this.useLongestToken) {
  235. let value = newTokens.slice(newPos, newPos + component.count);
  236. value = value.map(function (value, i) {
  237. const oldValue = oldTokens[oldPos + i];
  238. return oldValue.length > value.length ? oldValue : value;
  239. });
  240. component.value = this.join(value);
  241. }
  242. else {
  243. component.value = this.join(newTokens.slice(newPos, newPos + component.count));
  244. }
  245. newPos += component.count;
  246. // Common case
  247. if (!component.added) {
  248. oldPos += component.count;
  249. }
  250. }
  251. else {
  252. component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
  253. oldPos += component.count;
  254. }
  255. }
  256. return components;
  257. }
  258. }
  259. class CharacterDiff extends Diff {
  260. }
  261. const characterDiff = new CharacterDiff();
  262. function diffChars(oldStr, newStr, options) {
  263. return characterDiff.diff(oldStr, newStr, options);
  264. }
  265. function longestCommonPrefix(str1, str2) {
  266. let i;
  267. for (i = 0; i < str1.length && i < str2.length; i++) {
  268. if (str1[i] != str2[i]) {
  269. return str1.slice(0, i);
  270. }
  271. }
  272. return str1.slice(0, i);
  273. }
  274. function longestCommonSuffix(str1, str2) {
  275. let i;
  276. // Unlike longestCommonPrefix, we need a special case to handle all scenarios
  277. // where we return the empty string since str1.slice(-0) will return the
  278. // entire string.
  279. if (!str1 || !str2 || str1[str1.length - 1] != str2[str2.length - 1]) {
  280. return '';
  281. }
  282. for (i = 0; i < str1.length && i < str2.length; i++) {
  283. if (str1[str1.length - (i + 1)] != str2[str2.length - (i + 1)]) {
  284. return str1.slice(-i);
  285. }
  286. }
  287. return str1.slice(-i);
  288. }
  289. function replacePrefix(string, oldPrefix, newPrefix) {
  290. if (string.slice(0, oldPrefix.length) != oldPrefix) {
  291. throw Error(`string ${JSON.stringify(string)} doesn't start with prefix ${JSON.stringify(oldPrefix)}; this is a bug`);
  292. }
  293. return newPrefix + string.slice(oldPrefix.length);
  294. }
  295. function replaceSuffix(string, oldSuffix, newSuffix) {
  296. if (!oldSuffix) {
  297. return string + newSuffix;
  298. }
  299. if (string.slice(-oldSuffix.length) != oldSuffix) {
  300. throw Error(`string ${JSON.stringify(string)} doesn't end with suffix ${JSON.stringify(oldSuffix)}; this is a bug`);
  301. }
  302. return string.slice(0, -oldSuffix.length) + newSuffix;
  303. }
  304. function removePrefix(string, oldPrefix) {
  305. return replacePrefix(string, oldPrefix, '');
  306. }
  307. function removeSuffix(string, oldSuffix) {
  308. return replaceSuffix(string, oldSuffix, '');
  309. }
  310. function maximumOverlap(string1, string2) {
  311. return string2.slice(0, overlapCount(string1, string2));
  312. }
  313. // Nicked from https://stackoverflow.com/a/60422853/1709587
  314. function overlapCount(a, b) {
  315. // Deal with cases where the strings differ in length
  316. let startA = 0;
  317. if (a.length > b.length) {
  318. startA = a.length - b.length;
  319. }
  320. let endB = b.length;
  321. if (a.length < b.length) {
  322. endB = a.length;
  323. }
  324. // Create a back-reference for each index
  325. // that should be followed in case of a mismatch.
  326. // We only need B to make these references:
  327. const map = Array(endB);
  328. let k = 0; // Index that lags behind j
  329. map[0] = 0;
  330. for (let j = 1; j < endB; j++) {
  331. if (b[j] == b[k]) {
  332. map[j] = map[k]; // skip over the same character (optional optimisation)
  333. }
  334. else {
  335. map[j] = k;
  336. }
  337. while (k > 0 && b[j] != b[k]) {
  338. k = map[k];
  339. }
  340. if (b[j] == b[k]) {
  341. k++;
  342. }
  343. }
  344. // Phase 2: use these references while iterating over A
  345. k = 0;
  346. for (let i = startA; i < a.length; i++) {
  347. while (k > 0 && a[i] != b[k]) {
  348. k = map[k];
  349. }
  350. if (a[i] == b[k]) {
  351. k++;
  352. }
  353. }
  354. return k;
  355. }
  356. /**
  357. * Returns true if the string consistently uses Windows line endings.
  358. */
  359. function hasOnlyWinLineEndings(string) {
  360. return string.includes('\r\n') && !string.startsWith('\n') && !string.match(/[^\r]\n/);
  361. }
  362. /**
  363. * Returns true if the string consistently uses Unix line endings.
  364. */
  365. function hasOnlyUnixLineEndings(string) {
  366. return !string.includes('\r\n') && string.includes('\n');
  367. }
  368. function trailingWs(string) {
  369. // Yes, this looks overcomplicated and dumb - why not replace the whole function with
  370. // return string match(/\s*$/)[0]
  371. // you ask? Because:
  372. // 1. the trap described at https://markamery.com/blog/quadratic-time-regexes/ would mean doing
  373. // this would cause this function to take O(n²) time in the worst case (specifically when
  374. // there is a massive run of NON-TRAILING whitespace in `string`), and
  375. // 2. the fix proposed in the same blog post, of using a negative lookbehind, is incompatible
  376. // with old Safari versions that we'd like to not break if possible (see
  377. // https://github.com/kpdecker/jsdiff/pull/550)
  378. // It feels absurd to do this with an explicit loop instead of a regex, but I really can't see a
  379. // better way that doesn't result in broken behaviour.
  380. let i;
  381. for (i = string.length - 1; i >= 0; i--) {
  382. if (!string[i].match(/\s/)) {
  383. break;
  384. }
  385. }
  386. return string.substring(i + 1);
  387. }
  388. function leadingWs(string) {
  389. // Thankfully the annoying considerations described in trailingWs don't apply here:
  390. const match = string.match(/^\s*/);
  391. return match ? match[0] : '';
  392. }
  393. // Based on https://en.wikipedia.org/wiki/Latin_script_in_Unicode
  394. //
  395. // Ranges and exceptions:
  396. // Latin-1 Supplement, 0080–00FF
  397. // - U+00D7 × Multiplication sign
  398. // - U+00F7 ÷ Division sign
  399. // Latin Extended-A, 0100–017F
  400. // Latin Extended-B, 0180–024F
  401. // IPA Extensions, 0250–02AF
  402. // Spacing Modifier Letters, 02B0–02FF
  403. // - U+02C7 ˇ &#711; Caron
  404. // - U+02D8 ˘ &#728; Breve
  405. // - U+02D9 ˙ &#729; Dot Above
  406. // - U+02DA ˚ &#730; Ring Above
  407. // - U+02DB ˛ &#731; Ogonek
  408. // - U+02DC ˜ &#732; Small Tilde
  409. // - U+02DD ˝ &#733; Double Acute Accent
  410. // Latin Extended Additional, 1E00–1EFF
  411. const extendedWordChars = 'a-zA-Z0-9_\\u{C0}-\\u{FF}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}';
  412. // Each token is one of the following:
  413. // - A punctuation mark plus the surrounding whitespace
  414. // - A word plus the surrounding whitespace
  415. // - Pure whitespace (but only in the special case where this the entire text
  416. // is just whitespace)
  417. //
  418. // We have to include surrounding whitespace in the tokens because the two
  419. // alternative approaches produce horribly broken results:
  420. // * If we just discard the whitespace, we can't fully reproduce the original
  421. // text from the sequence of tokens and any attempt to render the diff will
  422. // get the whitespace wrong.
  423. // * If we have separate tokens for whitespace, then in a typical text every
  424. // second token will be a single space character. But this often results in
  425. // the optimal diff between two texts being a perverse one that preserves
  426. // the spaces between words but deletes and reinserts actual common words.
  427. // See https://github.com/kpdecker/jsdiff/issues/160#issuecomment-1866099640
  428. // for an example.
  429. //
  430. // Keeping the surrounding whitespace of course has implications for .equals
  431. // and .join, not just .tokenize.
  432. // This regex does NOT fully implement the tokenization rules described above.
  433. // Instead, it gives runs of whitespace their own "token". The tokenize method
  434. // then handles stitching whitespace tokens onto adjacent word or punctuation
  435. // tokens.
  436. const tokenizeIncludingWhitespace = new RegExp(`[${extendedWordChars}]+|\\s+|[^${extendedWordChars}]`, 'ug');
  437. class WordDiff extends Diff {
  438. equals(left, right, options) {
  439. if (options.ignoreCase) {
  440. left = left.toLowerCase();
  441. right = right.toLowerCase();
  442. }
  443. return left.trim() === right.trim();
  444. }
  445. tokenize(value, options = {}) {
  446. let parts;
  447. if (options.intlSegmenter) {
  448. const segmenter = options.intlSegmenter;
  449. if (segmenter.resolvedOptions().granularity != 'word') {
  450. throw new Error('The segmenter passed must have a granularity of "word"');
  451. }
  452. parts = Array.from(segmenter.segment(value), segment => segment.segment);
  453. }
  454. else {
  455. parts = value.match(tokenizeIncludingWhitespace) || [];
  456. }
  457. const tokens = [];
  458. let prevPart = null;
  459. parts.forEach(part => {
  460. if ((/\s/).test(part)) {
  461. if (prevPart == null) {
  462. tokens.push(part);
  463. }
  464. else {
  465. tokens.push(tokens.pop() + part);
  466. }
  467. }
  468. else if (prevPart != null && (/\s/).test(prevPart)) {
  469. if (tokens[tokens.length - 1] == prevPart) {
  470. tokens.push(tokens.pop() + part);
  471. }
  472. else {
  473. tokens.push(prevPart + part);
  474. }
  475. }
  476. else {
  477. tokens.push(part);
  478. }
  479. prevPart = part;
  480. });
  481. return tokens;
  482. }
  483. join(tokens) {
  484. // Tokens being joined here will always have appeared consecutively in the
  485. // same text, so we can simply strip off the leading whitespace from all the
  486. // tokens except the first (and except any whitespace-only tokens - but such
  487. // a token will always be the first and only token anyway) and then join them
  488. // and the whitespace around words and punctuation will end up correct.
  489. return tokens.map((token, i) => {
  490. if (i == 0) {
  491. return token;
  492. }
  493. else {
  494. return token.replace((/^\s+/), '');
  495. }
  496. }).join('');
  497. }
  498. postProcess(changes, options) {
  499. if (!changes || options.oneChangePerToken) {
  500. return changes;
  501. }
  502. let lastKeep = null;
  503. // Change objects representing any insertion or deletion since the last
  504. // "keep" change object. There can be at most one of each.
  505. let insertion = null;
  506. let deletion = null;
  507. changes.forEach(change => {
  508. if (change.added) {
  509. insertion = change;
  510. }
  511. else if (change.removed) {
  512. deletion = change;
  513. }
  514. else {
  515. if (insertion || deletion) { // May be false at start of text
  516. dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change);
  517. }
  518. lastKeep = change;
  519. insertion = null;
  520. deletion = null;
  521. }
  522. });
  523. if (insertion || deletion) {
  524. dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null);
  525. }
  526. return changes;
  527. }
  528. }
  529. const wordDiff = new WordDiff();
  530. function diffWords(oldStr, newStr, options) {
  531. // This option has never been documented and never will be (it's clearer to
  532. // just call `diffWordsWithSpace` directly if you need that behavior), but
  533. // has existed in jsdiff for a long time, so we retain support for it here
  534. // for the sake of backwards compatibility.
  535. if ((options === null || options === void 0 ? void 0 : options.ignoreWhitespace) != null && !options.ignoreWhitespace) {
  536. return diffWordsWithSpace(oldStr, newStr, options);
  537. }
  538. return wordDiff.diff(oldStr, newStr, options);
  539. }
  540. function dedupeWhitespaceInChangeObjects(startKeep, deletion, insertion, endKeep) {
  541. // Before returning, we tidy up the leading and trailing whitespace of the
  542. // change objects to eliminate cases where trailing whitespace in one object
  543. // is repeated as leading whitespace in the next.
  544. // Below are examples of the outcomes we want here to explain the code.
  545. // I=insert, K=keep, D=delete
  546. // 1. diffing 'foo bar baz' vs 'foo baz'
  547. // Prior to cleanup, we have K:'foo ' D:' bar ' K:' baz'
  548. // After cleanup, we want: K:'foo ' D:'bar ' K:'baz'
  549. //
  550. // 2. Diffing 'foo bar baz' vs 'foo qux baz'
  551. // Prior to cleanup, we have K:'foo ' D:' bar ' I:' qux ' K:' baz'
  552. // After cleanup, we want K:'foo ' D:'bar' I:'qux' K:' baz'
  553. //
  554. // 3. Diffing 'foo\nbar baz' vs 'foo baz'
  555. // Prior to cleanup, we have K:'foo ' D:'\nbar ' K:' baz'
  556. // After cleanup, we want K'foo' D:'\nbar' K:' baz'
  557. //
  558. // 4. Diffing 'foo baz' vs 'foo\nbar baz'
  559. // Prior to cleanup, we have K:'foo\n' I:'\nbar ' K:' baz'
  560. // After cleanup, we ideally want K'foo' I:'\nbar' K:' baz'
  561. // but don't actually manage this currently (the pre-cleanup change
  562. // objects don't contain enough information to make it possible).
  563. //
  564. // 5. Diffing 'foo bar baz' vs 'foo baz'
  565. // Prior to cleanup, we have K:'foo ' D:' bar ' K:' baz'
  566. // After cleanup, we want K:'foo ' D:' bar ' K:'baz'
  567. //
  568. // Our handling is unavoidably imperfect in the case where there's a single
  569. // indel between keeps and the whitespace has changed. For instance, consider
  570. // diffing 'foo\tbar\nbaz' vs 'foo baz'. Unless we create an extra change
  571. // object to represent the insertion of the space character (which isn't even
  572. // a token), we have no way to avoid losing information about the texts'
  573. // original whitespace in the result we return. Still, we do our best to
  574. // output something that will look sensible if we e.g. print it with
  575. // insertions in green and deletions in red.
  576. // Between two "keep" change objects (or before the first or after the last
  577. // change object), we can have either:
  578. // * A "delete" followed by an "insert"
  579. // * Just an "insert"
  580. // * Just a "delete"
  581. // We handle the three cases separately.
  582. if (deletion && insertion) {
  583. const oldWsPrefix = leadingWs(deletion.value);
  584. const oldWsSuffix = trailingWs(deletion.value);
  585. const newWsPrefix = leadingWs(insertion.value);
  586. const newWsSuffix = trailingWs(insertion.value);
  587. if (startKeep) {
  588. const commonWsPrefix = longestCommonPrefix(oldWsPrefix, newWsPrefix);
  589. startKeep.value = replaceSuffix(startKeep.value, newWsPrefix, commonWsPrefix);
  590. deletion.value = removePrefix(deletion.value, commonWsPrefix);
  591. insertion.value = removePrefix(insertion.value, commonWsPrefix);
  592. }
  593. if (endKeep) {
  594. const commonWsSuffix = longestCommonSuffix(oldWsSuffix, newWsSuffix);
  595. endKeep.value = replacePrefix(endKeep.value, newWsSuffix, commonWsSuffix);
  596. deletion.value = removeSuffix(deletion.value, commonWsSuffix);
  597. insertion.value = removeSuffix(insertion.value, commonWsSuffix);
  598. }
  599. }
  600. else if (insertion) {
  601. // The whitespaces all reflect what was in the new text rather than
  602. // the old, so we essentially have no information about whitespace
  603. // insertion or deletion. We just want to dedupe the whitespace.
  604. // We do that by having each change object keep its trailing
  605. // whitespace and deleting duplicate leading whitespace where
  606. // present.
  607. if (startKeep) {
  608. const ws = leadingWs(insertion.value);
  609. insertion.value = insertion.value.substring(ws.length);
  610. }
  611. if (endKeep) {
  612. const ws = leadingWs(endKeep.value);
  613. endKeep.value = endKeep.value.substring(ws.length);
  614. }
  615. // otherwise we've got a deletion and no insertion
  616. }
  617. else if (startKeep && endKeep) {
  618. const newWsFull = leadingWs(endKeep.value), delWsStart = leadingWs(deletion.value), delWsEnd = trailingWs(deletion.value);
  619. // Any whitespace that comes straight after startKeep in both the old and
  620. // new texts, assign to startKeep and remove from the deletion.
  621. const newWsStart = longestCommonPrefix(newWsFull, delWsStart);
  622. deletion.value = removePrefix(deletion.value, newWsStart);
  623. // Any whitespace that comes straight before endKeep in both the old and
  624. // new texts, and hasn't already been assigned to startKeep, assign to
  625. // endKeep and remove from the deletion.
  626. const newWsEnd = longestCommonSuffix(removePrefix(newWsFull, newWsStart), delWsEnd);
  627. deletion.value = removeSuffix(deletion.value, newWsEnd);
  628. endKeep.value = replacePrefix(endKeep.value, newWsFull, newWsEnd);
  629. // If there's any whitespace from the new text that HASN'T already been
  630. // assigned, assign it to the start:
  631. startKeep.value = replaceSuffix(startKeep.value, newWsFull, newWsFull.slice(0, newWsFull.length - newWsEnd.length));
  632. }
  633. else if (endKeep) {
  634. // We are at the start of the text. Preserve all the whitespace on
  635. // endKeep, and just remove whitespace from the end of deletion to the
  636. // extent that it overlaps with the start of endKeep.
  637. const endKeepWsPrefix = leadingWs(endKeep.value);
  638. const deletionWsSuffix = trailingWs(deletion.value);
  639. const overlap = maximumOverlap(deletionWsSuffix, endKeepWsPrefix);
  640. deletion.value = removeSuffix(deletion.value, overlap);
  641. }
  642. else if (startKeep) {
  643. // We are at the END of the text. Preserve all the whitespace on
  644. // startKeep, and just remove whitespace from the start of deletion to
  645. // the extent that it overlaps with the end of startKeep.
  646. const startKeepWsSuffix = trailingWs(startKeep.value);
  647. const deletionWsPrefix = leadingWs(deletion.value);
  648. const overlap = maximumOverlap(startKeepWsSuffix, deletionWsPrefix);
  649. deletion.value = removePrefix(deletion.value, overlap);
  650. }
  651. }
  652. class WordsWithSpaceDiff extends Diff {
  653. tokenize(value) {
  654. // Slightly different to the tokenizeIncludingWhitespace regex used above in
  655. // that this one treats each individual newline as a distinct tokens, rather
  656. // than merging them into other surrounding whitespace. This was requested
  657. // in https://github.com/kpdecker/jsdiff/issues/180 &
  658. // https://github.com/kpdecker/jsdiff/issues/211
  659. const regex = new RegExp(`(\\r?\\n)|[${extendedWordChars}]+|[^\\S\\n\\r]+|[^${extendedWordChars}]`, 'ug');
  660. return value.match(regex) || [];
  661. }
  662. }
  663. const wordsWithSpaceDiff = new WordsWithSpaceDiff();
  664. function diffWordsWithSpace(oldStr, newStr, options) {
  665. return wordsWithSpaceDiff.diff(oldStr, newStr, options);
  666. }
  667. function generateOptions(options, defaults) {
  668. if (typeof options === 'function') {
  669. defaults.callback = options;
  670. }
  671. else if (options) {
  672. for (const name in options) {
  673. /* istanbul ignore else */
  674. if (Object.prototype.hasOwnProperty.call(options, name)) {
  675. defaults[name] = options[name];
  676. }
  677. }
  678. }
  679. return defaults;
  680. }
  681. class LineDiff extends Diff {
  682. constructor() {
  683. super(...arguments);
  684. this.tokenize = tokenize;
  685. }
  686. equals(left, right, options) {
  687. // If we're ignoring whitespace, we need to normalise lines by stripping
  688. // whitespace before checking equality. (This has an annoying interaction
  689. // with newlineIsToken that requires special handling: if newlines get their
  690. // own token, then we DON'T want to trim the *newline* tokens down to empty
  691. // strings, since this would cause us to treat whitespace-only line content
  692. // as equal to a separator between lines, which would be weird and
  693. // inconsistent with the documented behavior of the options.)
  694. if (options.ignoreWhitespace) {
  695. if (!options.newlineIsToken || !left.includes('\n')) {
  696. left = left.trim();
  697. }
  698. if (!options.newlineIsToken || !right.includes('\n')) {
  699. right = right.trim();
  700. }
  701. }
  702. else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
  703. if (left.endsWith('\n')) {
  704. left = left.slice(0, -1);
  705. }
  706. if (right.endsWith('\n')) {
  707. right = right.slice(0, -1);
  708. }
  709. }
  710. return super.equals(left, right, options);
  711. }
  712. }
  713. const lineDiff = new LineDiff();
  714. function diffLines(oldStr, newStr, options) {
  715. return lineDiff.diff(oldStr, newStr, options);
  716. }
  717. function diffTrimmedLines(oldStr, newStr, options) {
  718. options = generateOptions(options, { ignoreWhitespace: true });
  719. return lineDiff.diff(oldStr, newStr, options);
  720. }
  721. // Exported standalone so it can be used from jsonDiff too.
  722. function tokenize(value, options) {
  723. if (options.stripTrailingCr) {
  724. // remove one \r before \n to match GNU diff's --strip-trailing-cr behavior
  725. value = value.replace(/\r\n/g, '\n');
  726. }
  727. const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
  728. // Ignore the final empty token that occurs if the string ends with a new line
  729. if (!linesAndNewlines[linesAndNewlines.length - 1]) {
  730. linesAndNewlines.pop();
  731. }
  732. // Merge the content and line separators into single tokens
  733. for (let i = 0; i < linesAndNewlines.length; i++) {
  734. const line = linesAndNewlines[i];
  735. if (i % 2 && !options.newlineIsToken) {
  736. retLines[retLines.length - 1] += line;
  737. }
  738. else {
  739. retLines.push(line);
  740. }
  741. }
  742. return retLines;
  743. }
  744. function isSentenceEndPunct(char) {
  745. return char == '.' || char == '!' || char == '?';
  746. }
  747. class SentenceDiff extends Diff {
  748. tokenize(value) {
  749. var _a;
  750. // If in future we drop support for environments that don't support lookbehinds, we can replace
  751. // this entire function with:
  752. // return value.split(/(?<=[.!?])(\s+|$)/);
  753. // but until then, for similar reasons to the trailingWs function in string.ts, we are forced
  754. // to do this verbosely "by hand" instead of using a regex.
  755. const result = [];
  756. let tokenStartI = 0;
  757. for (let i = 0; i < value.length; i++) {
  758. if (i == value.length - 1) {
  759. result.push(value.slice(tokenStartI));
  760. break;
  761. }
  762. if (isSentenceEndPunct(value[i]) && value[i + 1].match(/\s/)) {
  763. // We've hit a sentence break - i.e. a punctuation mark followed by whitespace.
  764. // We now want to push TWO tokens to the result:
  765. // 1. the sentence
  766. result.push(value.slice(tokenStartI, i + 1));
  767. // 2. the whitespace
  768. i = tokenStartI = i + 1;
  769. while ((_a = value[i + 1]) === null || _a === void 0 ? void 0 : _a.match(/\s/)) {
  770. i++;
  771. }
  772. result.push(value.slice(tokenStartI, i + 1));
  773. // Then the next token (a sentence) starts on the character after the whitespace.
  774. // (It's okay if this is off the end of the string - then the outer loop will terminate
  775. // here anyway.)
  776. tokenStartI = i + 1;
  777. }
  778. }
  779. return result;
  780. }
  781. }
  782. const sentenceDiff = new SentenceDiff();
  783. function diffSentences(oldStr, newStr, options) {
  784. return sentenceDiff.diff(oldStr, newStr, options);
  785. }
  786. class CssDiff extends Diff {
  787. tokenize(value) {
  788. return value.split(/([{}:;,]|\s+)/);
  789. }
  790. }
  791. const cssDiff = new CssDiff();
  792. function diffCss(oldStr, newStr, options) {
  793. return cssDiff.diff(oldStr, newStr, options);
  794. }
  795. class JsonDiff extends Diff {
  796. constructor() {
  797. super(...arguments);
  798. this.tokenize = tokenize;
  799. }
  800. get useLongestToken() {
  801. // Discriminate between two lines of pretty-printed, serialized JSON where one of them has a
  802. // dangling comma and the other doesn't. Turns out including the dangling comma yields the nicest output:
  803. return true;
  804. }
  805. castInput(value, options) {
  806. const { undefinedReplacement, stringifyReplacer = (k, v) => typeof v === 'undefined' ? undefinedReplacement : v } = options;
  807. return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), null, ' ');
  808. }
  809. equals(left, right, options) {
  810. return super.equals(left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1'), options);
  811. }
  812. }
  813. const jsonDiff = new JsonDiff();
  814. function diffJson(oldStr, newStr, options) {
  815. return jsonDiff.diff(oldStr, newStr, options);
  816. }
  817. // This function handles the presence of circular references by bailing out when encountering an
  818. // object that is already on the "stack" of items being processed. Accepts an optional replacer
  819. function canonicalize(obj, stack, replacementStack, replacer, key) {
  820. stack = stack || [];
  821. replacementStack = replacementStack || [];
  822. if (replacer) {
  823. obj = replacer(key === undefined ? '' : key, obj);
  824. }
  825. let i;
  826. for (i = 0; i < stack.length; i += 1) {
  827. if (stack[i] === obj) {
  828. return replacementStack[i];
  829. }
  830. }
  831. let canonicalizedObj;
  832. if ('[object Array]' === Object.prototype.toString.call(obj)) {
  833. stack.push(obj);
  834. canonicalizedObj = new Array(obj.length);
  835. replacementStack.push(canonicalizedObj);
  836. for (i = 0; i < obj.length; i += 1) {
  837. canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, String(i));
  838. }
  839. stack.pop();
  840. replacementStack.pop();
  841. return canonicalizedObj;
  842. }
  843. if (obj && obj.toJSON) {
  844. obj = obj.toJSON();
  845. }
  846. if (typeof obj === 'object' && obj !== null) {
  847. stack.push(obj);
  848. canonicalizedObj = {};
  849. replacementStack.push(canonicalizedObj);
  850. const sortedKeys = [];
  851. let key;
  852. for (key in obj) {
  853. /* istanbul ignore else */
  854. if (Object.prototype.hasOwnProperty.call(obj, key)) {
  855. sortedKeys.push(key);
  856. }
  857. }
  858. sortedKeys.sort();
  859. for (i = 0; i < sortedKeys.length; i += 1) {
  860. key = sortedKeys[i];
  861. canonicalizedObj[key] = canonicalize(obj[key], stack, replacementStack, replacer, key);
  862. }
  863. stack.pop();
  864. replacementStack.pop();
  865. }
  866. else {
  867. canonicalizedObj = obj;
  868. }
  869. return canonicalizedObj;
  870. }
  871. class ArrayDiff extends Diff {
  872. tokenize(value) {
  873. return value.slice();
  874. }
  875. join(value) {
  876. return value;
  877. }
  878. removeEmpty(value) {
  879. return value;
  880. }
  881. }
  882. const arrayDiff = new ArrayDiff();
  883. function diffArrays(oldArr, newArr, options) {
  884. return arrayDiff.diff(oldArr, newArr, options);
  885. }
  886. function unixToWin(patch) {
  887. if (Array.isArray(patch)) {
  888. // It would be cleaner if instead of the line below we could just write
  889. // return patch.map(unixToWin)
  890. // but mysteriously TypeScript (v5.7.3 at the time of writing) does not like this and it will
  891. // refuse to compile, thinking that unixToWin could then return StructuredPatch[][] and the
  892. // result would be incompatible with the overload signatures.
  893. // See bug report at https://github.com/microsoft/TypeScript/issues/61398.
  894. return patch.map(p => unixToWin(p));
  895. }
  896. return Object.assign(Object.assign({}, patch), { hunks: patch.hunks.map(hunk => (Object.assign(Object.assign({}, hunk), { lines: hunk.lines.map((line, i) => {
  897. var _a;
  898. return (line.startsWith('\\') || line.endsWith('\r') || ((_a = hunk.lines[i + 1]) === null || _a === void 0 ? void 0 : _a.startsWith('\\')))
  899. ? line
  900. : line + '\r';
  901. }) }))) });
  902. }
  903. function winToUnix(patch) {
  904. if (Array.isArray(patch)) {
  905. // (See comment above equivalent line in unixToWin)
  906. return patch.map(p => winToUnix(p));
  907. }
  908. 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) }))) });
  909. }
  910. /**
  911. * Returns true if the patch consistently uses Unix line endings (or only involves one line and has
  912. * no line endings).
  913. */
  914. function isUnix(patch) {
  915. if (!Array.isArray(patch)) {
  916. patch = [patch];
  917. }
  918. return !patch.some(index => index.hunks.some(hunk => hunk.lines.some(line => !line.startsWith('\\') && line.endsWith('\r'))));
  919. }
  920. /**
  921. * Returns true if the patch uses Windows line endings and only Windows line endings.
  922. */
  923. function isWin(patch) {
  924. if (!Array.isArray(patch)) {
  925. patch = [patch];
  926. }
  927. return patch.some(index => index.hunks.some(hunk => hunk.lines.some(line => line.endsWith('\r'))))
  928. && 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('\\')); })));
  929. }
  930. /**
  931. * Parses a patch into structured data, in the same structure returned by `structuredPatch`.
  932. *
  933. * @return a JSON object representation of the a patch, suitable for use with the `applyPatch` method.
  934. */
  935. function parsePatch(uniDiff) {
  936. const diffstr = uniDiff.split(/\n/), list = [];
  937. let i = 0;
  938. function parseIndex() {
  939. const index = {};
  940. list.push(index);
  941. // Parse diff metadata
  942. while (i < diffstr.length) {
  943. const line = diffstr[i];
  944. // File header found, end parsing diff metadata
  945. if ((/^(---|\+\+\+|@@)\s/).test(line)) {
  946. break;
  947. }
  948. // Diff index
  949. const header = (/^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/).exec(line);
  950. if (header) {
  951. index.index = header[1];
  952. }
  953. i++;
  954. }
  955. // Parse file headers if they are defined. Unified diff requires them, but
  956. // there's no technical issues to have an isolated hunk without file header
  957. parseFileHeader(index);
  958. parseFileHeader(index);
  959. // Parse hunks
  960. index.hunks = [];
  961. while (i < diffstr.length) {
  962. const line = diffstr[i];
  963. if ((/^(Index:\s|diff\s|---\s|\+\+\+\s|===================================================================)/).test(line)) {
  964. break;
  965. }
  966. else if ((/^@@/).test(line)) {
  967. index.hunks.push(parseHunk());
  968. }
  969. else if (line) {
  970. throw new Error('Unknown line ' + (i + 1) + ' ' + JSON.stringify(line));
  971. }
  972. else {
  973. i++;
  974. }
  975. }
  976. }
  977. // Parses the --- and +++ headers, if none are found, no lines
  978. // are consumed.
  979. function parseFileHeader(index) {
  980. const fileHeader = (/^(---|\+\+\+)\s+(.*)\r?$/).exec(diffstr[i]);
  981. if (fileHeader) {
  982. const data = fileHeader[2].split('\t', 2), header = (data[1] || '').trim();
  983. let fileName = data[0].replace(/\\\\/g, '\\');
  984. if ((/^".*"$/).test(fileName)) {
  985. fileName = fileName.substr(1, fileName.length - 2);
  986. }
  987. if (fileHeader[1] === '---') {
  988. index.oldFileName = fileName;
  989. index.oldHeader = header;
  990. }
  991. else {
  992. index.newFileName = fileName;
  993. index.newHeader = header;
  994. }
  995. i++;
  996. }
  997. }
  998. // Parses a hunk
  999. // This assumes that we are at the start of a hunk.
  1000. function parseHunk() {
  1001. var _a;
  1002. const chunkHeaderIndex = i, chunkHeaderLine = diffstr[i++], chunkHeader = chunkHeaderLine.split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
  1003. const hunk = {
  1004. oldStart: +chunkHeader[1],
  1005. oldLines: typeof chunkHeader[2] === 'undefined' ? 1 : +chunkHeader[2],
  1006. newStart: +chunkHeader[3],
  1007. newLines: typeof chunkHeader[4] === 'undefined' ? 1 : +chunkHeader[4],
  1008. lines: []
  1009. };
  1010. // Unified Diff Format quirk: If the chunk size is 0,
  1011. // the first number is one lower than one would expect.
  1012. // https://www.artima.com/weblogs/viewpost.jsp?thread=164293
  1013. if (hunk.oldLines === 0) {
  1014. hunk.oldStart += 1;
  1015. }
  1016. if (hunk.newLines === 0) {
  1017. hunk.newStart += 1;
  1018. }
  1019. let addCount = 0, removeCount = 0;
  1020. for (; i < diffstr.length && (removeCount < hunk.oldLines || addCount < hunk.newLines || ((_a = diffstr[i]) === null || _a === void 0 ? void 0 : _a.startsWith('\\'))); i++) {
  1021. const operation = (diffstr[i].length == 0 && i != (diffstr.length - 1)) ? ' ' : diffstr[i][0];
  1022. if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') {
  1023. hunk.lines.push(diffstr[i]);
  1024. if (operation === '+') {
  1025. addCount++;
  1026. }
  1027. else if (operation === '-') {
  1028. removeCount++;
  1029. }
  1030. else if (operation === ' ') {
  1031. addCount++;
  1032. removeCount++;
  1033. }
  1034. }
  1035. else {
  1036. throw new Error(`Hunk at line ${chunkHeaderIndex + 1} contained invalid line ${diffstr[i]}`);
  1037. }
  1038. }
  1039. // Handle the empty block count case
  1040. if (!addCount && hunk.newLines === 1) {
  1041. hunk.newLines = 0;
  1042. }
  1043. if (!removeCount && hunk.oldLines === 1) {
  1044. hunk.oldLines = 0;
  1045. }
  1046. // Perform sanity checking
  1047. if (addCount !== hunk.newLines) {
  1048. throw new Error('Added line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
  1049. }
  1050. if (removeCount !== hunk.oldLines) {
  1051. throw new Error('Removed line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
  1052. }
  1053. return hunk;
  1054. }
  1055. while (i < diffstr.length) {
  1056. parseIndex();
  1057. }
  1058. return list;
  1059. }
  1060. // Iterator that traverses in the range of [min, max], stepping
  1061. // by distance from a given start position. I.e. for [0, 4], with
  1062. // start of 2, this will iterate 2, 3, 1, 4, 0.
  1063. function distanceIterator (start, minLine, maxLine) {
  1064. let wantForward = true, backwardExhausted = false, forwardExhausted = false, localOffset = 1;
  1065. return function iterator() {
  1066. if (wantForward && !forwardExhausted) {
  1067. if (backwardExhausted) {
  1068. localOffset++;
  1069. }
  1070. else {
  1071. wantForward = false;
  1072. }
  1073. // Check if trying to fit beyond text length, and if not, check it fits
  1074. // after offset location (or desired location on first iteration)
  1075. if (start + localOffset <= maxLine) {
  1076. return start + localOffset;
  1077. }
  1078. forwardExhausted = true;
  1079. }
  1080. if (!backwardExhausted) {
  1081. if (!forwardExhausted) {
  1082. wantForward = true;
  1083. }
  1084. // Check if trying to fit before text beginning, and if not, check it fits
  1085. // before offset location
  1086. if (minLine <= start - localOffset) {
  1087. return start - localOffset++;
  1088. }
  1089. backwardExhausted = true;
  1090. return iterator();
  1091. }
  1092. // We tried to fit hunk before text beginning and beyond text length, then
  1093. // hunk can't fit on the text. Return undefined
  1094. return undefined;
  1095. };
  1096. }
  1097. /**
  1098. * attempts to apply a unified diff patch.
  1099. *
  1100. * Hunks are applied first to last.
  1101. * `applyPatch` first tries to apply the first hunk at the line number specified in the hunk header, and with all context lines matching exactly.
  1102. * 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.
  1103. * 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.
  1104. * 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.
  1105. *
  1106. * Once a hunk is successfully fitted, the process begins again with the next hunk.
  1107. * Regardless of `fuzzFactor`, later hunks must be applied later in the file than earlier hunks.
  1108. *
  1109. * If a hunk cannot be successfully fitted *anywhere* with fewer than `fuzzFactor` mismatches, `applyPatch` fails and returns `false`.
  1110. *
  1111. * 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.
  1112. * (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.)
  1113. *
  1114. * If the patch was applied successfully, returns a string containing the patched text.
  1115. * 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.
  1116. *
  1117. * @param patch a string diff or the output from the `parsePatch` or `structuredPatch` methods.
  1118. */
  1119. function applyPatch(source, patch, options = {}) {
  1120. let patches;
  1121. if (typeof patch === 'string') {
  1122. patches = parsePatch(patch);
  1123. }
  1124. else if (Array.isArray(patch)) {
  1125. patches = patch;
  1126. }
  1127. else {
  1128. patches = [patch];
  1129. }
  1130. if (patches.length > 1) {
  1131. throw new Error('applyPatch only works with a single input.');
  1132. }
  1133. return applyStructuredPatch(source, patches[0], options);
  1134. }
  1135. function applyStructuredPatch(source, patch, options = {}) {
  1136. if (options.autoConvertLineEndings || options.autoConvertLineEndings == null) {
  1137. if (hasOnlyWinLineEndings(source) && isUnix(patch)) {
  1138. patch = unixToWin(patch);
  1139. }
  1140. else if (hasOnlyUnixLineEndings(source) && isWin(patch)) {
  1141. patch = winToUnix(patch);
  1142. }
  1143. }
  1144. // Apply the diff to the input
  1145. const lines = source.split('\n'), hunks = patch.hunks, compareLine = options.compareLine || ((lineNumber, line, operation, patchContent) => line === patchContent), fuzzFactor = options.fuzzFactor || 0;
  1146. let minLine = 0;
  1147. if (fuzzFactor < 0 || !Number.isInteger(fuzzFactor)) {
  1148. throw new Error('fuzzFactor must be a non-negative integer');
  1149. }
  1150. // Special case for empty patch.
  1151. if (!hunks.length) {
  1152. return source;
  1153. }
  1154. // Before anything else, handle EOFNL insertion/removal. If the patch tells us to make a change
  1155. // to the EOFNL that is redundant/impossible - i.e. to remove a newline that's not there, or add a
  1156. // newline that already exists - then we either return false and fail to apply the patch (if
  1157. // fuzzFactor is 0) or simply ignore the problem and do nothing (if fuzzFactor is >0).
  1158. // If we do need to remove/add a newline at EOF, this will always be in the final hunk:
  1159. let prevLine = '', removeEOFNL = false, addEOFNL = false;
  1160. for (let i = 0; i < hunks[hunks.length - 1].lines.length; i++) {
  1161. const line = hunks[hunks.length - 1].lines[i];
  1162. if (line[0] == '\\') {
  1163. if (prevLine[0] == '+') {
  1164. removeEOFNL = true;
  1165. }
  1166. else if (prevLine[0] == '-') {
  1167. addEOFNL = true;
  1168. }
  1169. }
  1170. prevLine = line;
  1171. }
  1172. if (removeEOFNL) {
  1173. if (addEOFNL) {
  1174. // This means the final line gets changed but doesn't have a trailing newline in either the
  1175. // original or patched version. In that case, we do nothing if fuzzFactor > 0, and if
  1176. // fuzzFactor is 0, we simply validate that the source file has no trailing newline.
  1177. if (!fuzzFactor && lines[lines.length - 1] == '') {
  1178. return false;
  1179. }
  1180. }
  1181. else if (lines[lines.length - 1] == '') {
  1182. lines.pop();
  1183. }
  1184. else if (!fuzzFactor) {
  1185. return false;
  1186. }
  1187. }
  1188. else if (addEOFNL) {
  1189. if (lines[lines.length - 1] != '') {
  1190. lines.push('');
  1191. }
  1192. else if (!fuzzFactor) {
  1193. return false;
  1194. }
  1195. }
  1196. /**
  1197. * Checks if the hunk can be made to fit at the provided location with at most `maxErrors`
  1198. * insertions, substitutions, or deletions, while ensuring also that:
  1199. * - lines deleted in the hunk match exactly, and
  1200. * - wherever an insertion operation or block of insertion operations appears in the hunk, the
  1201. * immediately preceding and following lines of context match exactly
  1202. *
  1203. * `toPos` should be set such that lines[toPos] is meant to match hunkLines[0].
  1204. *
  1205. * If the hunk can be applied, returns an object with properties `oldLineLastI` and
  1206. * `replacementLines`. Otherwise, returns null.
  1207. */
  1208. function applyHunk(hunkLines, toPos, maxErrors, hunkLinesI = 0, lastContextLineMatched = true, patchedLines = [], patchedLinesLength = 0) {
  1209. let nConsecutiveOldContextLines = 0;
  1210. let nextContextLineMustMatch = false;
  1211. for (; hunkLinesI < hunkLines.length; hunkLinesI++) {
  1212. const hunkLine = hunkLines[hunkLinesI], operation = (hunkLine.length > 0 ? hunkLine[0] : ' '), content = (hunkLine.length > 0 ? hunkLine.substr(1) : hunkLine);
  1213. if (operation === '-') {
  1214. if (compareLine(toPos + 1, lines[toPos], operation, content)) {
  1215. toPos++;
  1216. nConsecutiveOldContextLines = 0;
  1217. }
  1218. else {
  1219. if (!maxErrors || lines[toPos] == null) {
  1220. return null;
  1221. }
  1222. patchedLines[patchedLinesLength] = lines[toPos];
  1223. return applyHunk(hunkLines, toPos + 1, maxErrors - 1, hunkLinesI, false, patchedLines, patchedLinesLength + 1);
  1224. }
  1225. }
  1226. if (operation === '+') {
  1227. if (!lastContextLineMatched) {
  1228. return null;
  1229. }
  1230. patchedLines[patchedLinesLength] = content;
  1231. patchedLinesLength++;
  1232. nConsecutiveOldContextLines = 0;
  1233. nextContextLineMustMatch = true;
  1234. }
  1235. if (operation === ' ') {
  1236. nConsecutiveOldContextLines++;
  1237. patchedLines[patchedLinesLength] = lines[toPos];
  1238. if (compareLine(toPos + 1, lines[toPos], operation, content)) {
  1239. patchedLinesLength++;
  1240. lastContextLineMatched = true;
  1241. nextContextLineMustMatch = false;
  1242. toPos++;
  1243. }
  1244. else {
  1245. if (nextContextLineMustMatch || !maxErrors) {
  1246. return null;
  1247. }
  1248. // Consider 3 possibilities in sequence:
  1249. // 1. lines contains a *substitution* not included in the patch context, or
  1250. // 2. lines contains an *insertion* not included in the patch context, or
  1251. // 3. lines contains a *deletion* not included in the patch context
  1252. // The first two options are of course only possible if the line from lines is non-null -
  1253. // i.e. only option 3 is possible if we've overrun the end of the old file.
  1254. return (lines[toPos] && (applyHunk(hunkLines, toPos + 1, maxErrors - 1, hunkLinesI + 1, false, patchedLines, patchedLinesLength + 1) || applyHunk(hunkLines, toPos + 1, maxErrors - 1, hunkLinesI, false, patchedLines, patchedLinesLength + 1)) || applyHunk(hunkLines, toPos, maxErrors - 1, hunkLinesI + 1, false, patchedLines, patchedLinesLength));
  1255. }
  1256. }
  1257. }
  1258. // Before returning, trim any unmodified context lines off the end of patchedLines and reduce
  1259. // toPos (and thus oldLineLastI) accordingly. This allows later hunks to be applied to a region
  1260. // that starts in this hunk's trailing context.
  1261. patchedLinesLength -= nConsecutiveOldContextLines;
  1262. toPos -= nConsecutiveOldContextLines;
  1263. patchedLines.length = patchedLinesLength;
  1264. return {
  1265. patchedLines,
  1266. oldLineLastI: toPos - 1
  1267. };
  1268. }
  1269. const resultLines = [];
  1270. // Search best fit offsets for each hunk based on the previous ones
  1271. let prevHunkOffset = 0;
  1272. for (let i = 0; i < hunks.length; i++) {
  1273. const hunk = hunks[i];
  1274. let hunkResult;
  1275. const maxLine = lines.length - hunk.oldLines + fuzzFactor;
  1276. let toPos;
  1277. for (let maxErrors = 0; maxErrors <= fuzzFactor; maxErrors++) {
  1278. toPos = hunk.oldStart + prevHunkOffset - 1;
  1279. const iterator = distanceIterator(toPos, minLine, maxLine);
  1280. for (; toPos !== undefined; toPos = iterator()) {
  1281. hunkResult = applyHunk(hunk.lines, toPos, maxErrors);
  1282. if (hunkResult) {
  1283. break;
  1284. }
  1285. }
  1286. if (hunkResult) {
  1287. break;
  1288. }
  1289. }
  1290. if (!hunkResult) {
  1291. return false;
  1292. }
  1293. // Copy everything from the end of where we applied the last hunk to the start of this hunk
  1294. for (let i = minLine; i < toPos; i++) {
  1295. resultLines.push(lines[i]);
  1296. }
  1297. // Add the lines produced by applying the hunk:
  1298. for (let i = 0; i < hunkResult.patchedLines.length; i++) {
  1299. const line = hunkResult.patchedLines[i];
  1300. resultLines.push(line);
  1301. }
  1302. // Set lower text limit to end of the current hunk, so next ones don't try
  1303. // to fit over already patched text
  1304. minLine = hunkResult.oldLineLastI + 1;
  1305. // Note the offset between where the patch said the hunk should've applied and where we
  1306. // applied it, so we can adjust future hunks accordingly:
  1307. prevHunkOffset = toPos + 1 - hunk.oldStart;
  1308. }
  1309. // Copy over the rest of the lines from the old text
  1310. for (let i = minLine; i < lines.length; i++) {
  1311. resultLines.push(lines[i]);
  1312. }
  1313. return resultLines.join('\n');
  1314. }
  1315. /**
  1316. * applies one or more patches.
  1317. *
  1318. * `patch` may be either an array of structured patch objects, or a string representing a patch in unified diff format (which may patch one or more files).
  1319. *
  1320. * This method will iterate over the contents of the patch and apply to data provided through callbacks. The general flow for each patch index is:
  1321. *
  1322. * - `options.loadFile(index, callback)` is called. The caller should then load the contents of the file and then pass that to the `callback(err, data)` callback. Passing an `err` will terminate further patch execution.
  1323. * - `options.patched(index, content, callback)` is called once the patch has been applied. `content` will be the return value from `applyPatch`. When it's ready, the caller should call `callback(err)` callback. Passing an `err` will terminate further patch execution.
  1324. *
  1325. * Once all patches have been applied or an error occurs, the `options.complete(err)` callback is made.
  1326. */
  1327. function applyPatches(uniDiff, options) {
  1328. const spDiff = typeof uniDiff === 'string' ? parsePatch(uniDiff) : uniDiff;
  1329. let currentIndex = 0;
  1330. function processIndex() {
  1331. const index = spDiff[currentIndex++];
  1332. if (!index) {
  1333. return options.complete();
  1334. }
  1335. options.loadFile(index, function (err, data) {
  1336. if (err) {
  1337. return options.complete(err);
  1338. }
  1339. const updatedContent = applyPatch(data, index, options);
  1340. options.patched(index, updatedContent, function (err) {
  1341. if (err) {
  1342. return options.complete(err);
  1343. }
  1344. processIndex();
  1345. });
  1346. });
  1347. }
  1348. processIndex();
  1349. }
  1350. function reversePatch(structuredPatch) {
  1351. if (Array.isArray(structuredPatch)) {
  1352. // (See comment in unixToWin for why we need the pointless-looking anonymous function here)
  1353. return structuredPatch.map(patch => reversePatch(patch)).reverse();
  1354. }
  1355. return Object.assign(Object.assign({}, structuredPatch), { oldFileName: structuredPatch.newFileName, oldHeader: structuredPatch.newHeader, newFileName: structuredPatch.oldFileName, newHeader: structuredPatch.oldHeader, hunks: structuredPatch.hunks.map(hunk => {
  1356. return {
  1357. oldLines: hunk.newLines,
  1358. oldStart: hunk.newStart,
  1359. newLines: hunk.oldLines,
  1360. newStart: hunk.oldStart,
  1361. lines: hunk.lines.map(l => {
  1362. if (l.startsWith('-')) {
  1363. return `+${l.slice(1)}`;
  1364. }
  1365. if (l.startsWith('+')) {
  1366. return `-${l.slice(1)}`;
  1367. }
  1368. return l;
  1369. })
  1370. };
  1371. }) });
  1372. }
  1373. function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
  1374. let optionsObj;
  1375. if (!options) {
  1376. optionsObj = {};
  1377. }
  1378. else if (typeof options === 'function') {
  1379. optionsObj = { callback: options };
  1380. }
  1381. else {
  1382. optionsObj = options;
  1383. }
  1384. if (typeof optionsObj.context === 'undefined') {
  1385. optionsObj.context = 4;
  1386. }
  1387. // We copy this into its own variable to placate TypeScript, which thinks
  1388. // optionsObj.context might be undefined in the callbacks below.
  1389. const context = optionsObj.context;
  1390. // @ts-expect-error (runtime check for something that is correctly a static type error)
  1391. if (optionsObj.newlineIsToken) {
  1392. throw new Error('newlineIsToken may not be used with patch-generation functions, only with diffing functions');
  1393. }
  1394. if (!optionsObj.callback) {
  1395. return diffLinesResultToPatch(diffLines(oldStr, newStr, optionsObj));
  1396. }
  1397. else {
  1398. const { callback } = optionsObj;
  1399. diffLines(oldStr, newStr, Object.assign(Object.assign({}, optionsObj), { callback: (diff) => {
  1400. const patch = diffLinesResultToPatch(diff);
  1401. // TypeScript is unhappy without the cast because it does not understand that `patch` may
  1402. // be undefined here only if `callback` is StructuredPatchCallbackAbortable:
  1403. callback(patch);
  1404. } }));
  1405. }
  1406. function diffLinesResultToPatch(diff) {
  1407. // STEP 1: Build up the patch with no "\ No newline at end of file" lines and with the arrays
  1408. // of lines containing trailing newline characters. We'll tidy up later...
  1409. if (!diff) {
  1410. return;
  1411. }
  1412. diff.push({ value: '', lines: [] }); // Append an empty value to make cleanup easier
  1413. function contextLines(lines) {
  1414. return lines.map(function (entry) { return ' ' + entry; });
  1415. }
  1416. const hunks = [];
  1417. let oldRangeStart = 0, newRangeStart = 0, curRange = [], oldLine = 1, newLine = 1;
  1418. for (let i = 0; i < diff.length; i++) {
  1419. const current = diff[i], lines = current.lines || splitLines(current.value);
  1420. current.lines = lines;
  1421. if (current.added || current.removed) {
  1422. // If we have previous context, start with that
  1423. if (!oldRangeStart) {
  1424. const prev = diff[i - 1];
  1425. oldRangeStart = oldLine;
  1426. newRangeStart = newLine;
  1427. if (prev) {
  1428. curRange = context > 0 ? contextLines(prev.lines.slice(-context)) : [];
  1429. oldRangeStart -= curRange.length;
  1430. newRangeStart -= curRange.length;
  1431. }
  1432. }
  1433. // Output our changes
  1434. for (const line of lines) {
  1435. curRange.push((current.added ? '+' : '-') + line);
  1436. }
  1437. // Track the updated file position
  1438. if (current.added) {
  1439. newLine += lines.length;
  1440. }
  1441. else {
  1442. oldLine += lines.length;
  1443. }
  1444. }
  1445. else {
  1446. // Identical context lines. Track line changes
  1447. if (oldRangeStart) {
  1448. // Close out any changes that have been output (or join overlapping)
  1449. if (lines.length <= context * 2 && i < diff.length - 2) {
  1450. // Overlapping
  1451. for (const line of contextLines(lines)) {
  1452. curRange.push(line);
  1453. }
  1454. }
  1455. else {
  1456. // end the range and output
  1457. const contextSize = Math.min(lines.length, context);
  1458. for (const line of contextLines(lines.slice(0, contextSize))) {
  1459. curRange.push(line);
  1460. }
  1461. const hunk = {
  1462. oldStart: oldRangeStart,
  1463. oldLines: (oldLine - oldRangeStart + contextSize),
  1464. newStart: newRangeStart,
  1465. newLines: (newLine - newRangeStart + contextSize),
  1466. lines: curRange
  1467. };
  1468. hunks.push(hunk);
  1469. oldRangeStart = 0;
  1470. newRangeStart = 0;
  1471. curRange = [];
  1472. }
  1473. }
  1474. oldLine += lines.length;
  1475. newLine += lines.length;
  1476. }
  1477. }
  1478. // Step 2: eliminate the trailing `\n` from each line of each hunk, and, where needed, add
  1479. // "\ No newline at end of file".
  1480. for (const hunk of hunks) {
  1481. for (let i = 0; i < hunk.lines.length; i++) {
  1482. if (hunk.lines[i].endsWith('\n')) {
  1483. hunk.lines[i] = hunk.lines[i].slice(0, -1);
  1484. }
  1485. else {
  1486. hunk.lines.splice(i + 1, 0, '\\ No newline at end of file');
  1487. i++; // Skip the line we just added, then continue iterating
  1488. }
  1489. }
  1490. }
  1491. return {
  1492. oldFileName: oldFileName, newFileName: newFileName,
  1493. oldHeader: oldHeader, newHeader: newHeader,
  1494. hunks: hunks
  1495. };
  1496. }
  1497. }
  1498. /**
  1499. * creates a unified diff patch.
  1500. * @param patch either a single structured patch object (as returned by `structuredPatch`) or an array of them (as returned by `parsePatch`)
  1501. */
  1502. function formatPatch(patch) {
  1503. if (Array.isArray(patch)) {
  1504. return patch.map(formatPatch).join('\n');
  1505. }
  1506. const ret = [];
  1507. if (patch.oldFileName == patch.newFileName) {
  1508. ret.push('Index: ' + patch.oldFileName);
  1509. }
  1510. ret.push('===================================================================');
  1511. ret.push('--- ' + patch.oldFileName + (typeof patch.oldHeader === 'undefined' ? '' : '\t' + patch.oldHeader));
  1512. ret.push('+++ ' + patch.newFileName + (typeof patch.newHeader === 'undefined' ? '' : '\t' + patch.newHeader));
  1513. for (let i = 0; i < patch.hunks.length; i++) {
  1514. const hunk = patch.hunks[i];
  1515. // Unified Diff Format quirk: If the chunk size is 0,
  1516. // the first number is one lower than one would expect.
  1517. // https://www.artima.com/weblogs/viewpost.jsp?thread=164293
  1518. if (hunk.oldLines === 0) {
  1519. hunk.oldStart -= 1;
  1520. }
  1521. if (hunk.newLines === 0) {
  1522. hunk.newStart -= 1;
  1523. }
  1524. ret.push('@@ -' + hunk.oldStart + ',' + hunk.oldLines
  1525. + ' +' + hunk.newStart + ',' + hunk.newLines
  1526. + ' @@');
  1527. for (const line of hunk.lines) {
  1528. ret.push(line);
  1529. }
  1530. }
  1531. return ret.join('\n') + '\n';
  1532. }
  1533. function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
  1534. if (typeof options === 'function') {
  1535. options = { callback: options };
  1536. }
  1537. if (!(options === null || options === void 0 ? void 0 : options.callback)) {
  1538. const patchObj = structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options);
  1539. if (!patchObj) {
  1540. return;
  1541. }
  1542. return formatPatch(patchObj);
  1543. }
  1544. else {
  1545. const { callback } = options;
  1546. structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, Object.assign(Object.assign({}, options), { callback: patchObj => {
  1547. if (!patchObj) {
  1548. callback(undefined);
  1549. }
  1550. else {
  1551. callback(formatPatch(patchObj));
  1552. }
  1553. } }));
  1554. }
  1555. }
  1556. function createPatch(fileName, oldStr, newStr, oldHeader, newHeader, options) {
  1557. return createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldHeader, newHeader, options);
  1558. }
  1559. /**
  1560. * Split `text` into an array of lines, including the trailing newline character (where present)
  1561. */
  1562. function splitLines(text) {
  1563. const hasTrailingNl = text.endsWith('\n');
  1564. const result = text.split('\n').map(line => line + '\n');
  1565. if (hasTrailingNl) {
  1566. result.pop();
  1567. }
  1568. else {
  1569. result.push(result.pop().slice(0, -1));
  1570. }
  1571. return result;
  1572. }
  1573. /**
  1574. * converts a list of change objects to the format returned by Google's [diff-match-patch](https://github.com/google/diff-match-patch) library
  1575. */
  1576. function convertChangesToDMP(changes) {
  1577. const ret = [];
  1578. let change, operation;
  1579. for (let i = 0; i < changes.length; i++) {
  1580. change = changes[i];
  1581. if (change.added) {
  1582. operation = 1;
  1583. }
  1584. else if (change.removed) {
  1585. operation = -1;
  1586. }
  1587. else {
  1588. operation = 0;
  1589. }
  1590. ret.push([operation, change.value]);
  1591. }
  1592. return ret;
  1593. }
  1594. /**
  1595. * converts a list of change objects to a serialized XML format
  1596. */
  1597. function convertChangesToXML(changes) {
  1598. const ret = [];
  1599. for (let i = 0; i < changes.length; i++) {
  1600. const change = changes[i];
  1601. if (change.added) {
  1602. ret.push('<ins>');
  1603. }
  1604. else if (change.removed) {
  1605. ret.push('<del>');
  1606. }
  1607. ret.push(escapeHTML(change.value));
  1608. if (change.added) {
  1609. ret.push('</ins>');
  1610. }
  1611. else if (change.removed) {
  1612. ret.push('</del>');
  1613. }
  1614. }
  1615. return ret.join('');
  1616. }
  1617. function escapeHTML(s) {
  1618. let n = s;
  1619. n = n.replace(/&/g, '&amp;');
  1620. n = n.replace(/</g, '&lt;');
  1621. n = n.replace(/>/g, '&gt;');
  1622. n = n.replace(/"/g, '&quot;');
  1623. return n;
  1624. }
  1625. exports.Diff = Diff;
  1626. exports.applyPatch = applyPatch;
  1627. exports.applyPatches = applyPatches;
  1628. exports.arrayDiff = arrayDiff;
  1629. exports.canonicalize = canonicalize;
  1630. exports.characterDiff = characterDiff;
  1631. exports.convertChangesToDMP = convertChangesToDMP;
  1632. exports.convertChangesToXML = convertChangesToXML;
  1633. exports.createPatch = createPatch;
  1634. exports.createTwoFilesPatch = createTwoFilesPatch;
  1635. exports.cssDiff = cssDiff;
  1636. exports.diffArrays = diffArrays;
  1637. exports.diffChars = diffChars;
  1638. exports.diffCss = diffCss;
  1639. exports.diffJson = diffJson;
  1640. exports.diffLines = diffLines;
  1641. exports.diffSentences = diffSentences;
  1642. exports.diffTrimmedLines = diffTrimmedLines;
  1643. exports.diffWords = diffWords;
  1644. exports.diffWordsWithSpace = diffWordsWithSpace;
  1645. exports.formatPatch = formatPatch;
  1646. exports.jsonDiff = jsonDiff;
  1647. exports.lineDiff = lineDiff;
  1648. exports.parsePatch = parsePatch;
  1649. exports.reversePatch = reversePatch;
  1650. exports.sentenceDiff = sentenceDiff;
  1651. exports.structuredPatch = structuredPatch;
  1652. exports.wordDiff = wordDiff;
  1653. exports.wordsWithSpaceDiff = wordsWithSpaceDiff;
  1654. }));