diff.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. /**
  2. * This library modifies the diff-patch-match library by Neil Fraser
  3. * by removing the patch and match functionality and certain advanced
  4. * options in the diff function. The original license is as follows:
  5. *
  6. * ===
  7. *
  8. * Diff Match and Patch
  9. *
  10. * Copyright 2006 Google Inc.
  11. * http://code.google.com/p/google-diff-match-patch/
  12. *
  13. * Licensed under the Apache License, Version 2.0 (the "License");
  14. * you may not use this file except in compliance with the License.
  15. * You may obtain a copy of the License at
  16. *
  17. * http://www.apache.org/licenses/LICENSE-2.0
  18. *
  19. * Unless required by applicable law or agreed to in writing, software
  20. * distributed under the License is distributed on an "AS IS" BASIS,
  21. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  22. * See the License for the specific language governing permissions and
  23. * limitations under the License.
  24. */
  25. /**
  26. * The data structure representing a diff is an array of tuples:
  27. * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
  28. * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
  29. */
  30. var DIFF_DELETE = -1;
  31. var DIFF_INSERT = 1;
  32. var DIFF_EQUAL = 0;
  33. /**
  34. * Find the differences between two texts. Simplifies the problem by stripping
  35. * any common prefix or suffix off the texts before diffing.
  36. * @param {string} text1 Old string to be diffed.
  37. * @param {string} text2 New string to be diffed.
  38. * @param {Int} cursor_pos Expected edit position in text1 (optional)
  39. * @return {Array} Array of diff tuples.
  40. */
  41. function diff_main(text1, text2, cursor_pos) {
  42. // Check for equality (speedup).
  43. if (text1 == text2) {
  44. if (text1) {
  45. return [[DIFF_EQUAL, text1]];
  46. }
  47. return [];
  48. }
  49. // Check cursor_pos within bounds
  50. if (cursor_pos < 0 || text1.length < cursor_pos) {
  51. cursor_pos = null;
  52. }
  53. // Trim off common prefix (speedup).
  54. var commonlength = diff_commonPrefix(text1, text2);
  55. var commonprefix = text1.substring(0, commonlength);
  56. text1 = text1.substring(commonlength);
  57. text2 = text2.substring(commonlength);
  58. // Trim off common suffix (speedup).
  59. commonlength = diff_commonSuffix(text1, text2);
  60. var commonsuffix = text1.substring(text1.length - commonlength);
  61. text1 = text1.substring(0, text1.length - commonlength);
  62. text2 = text2.substring(0, text2.length - commonlength);
  63. // Compute the diff on the middle block.
  64. var diffs = diff_compute_(text1, text2);
  65. // Restore the prefix and suffix.
  66. if (commonprefix) {
  67. diffs.unshift([DIFF_EQUAL, commonprefix]);
  68. }
  69. if (commonsuffix) {
  70. diffs.push([DIFF_EQUAL, commonsuffix]);
  71. }
  72. diff_cleanupMerge(diffs);
  73. if (cursor_pos != null) {
  74. diffs = fix_cursor(diffs, cursor_pos);
  75. }
  76. diffs = fix_emoji(diffs);
  77. return diffs;
  78. };
  79. /**
  80. * Find the differences between two texts. Assumes that the texts do not
  81. * have any common prefix or suffix.
  82. * @param {string} text1 Old string to be diffed.
  83. * @param {string} text2 New string to be diffed.
  84. * @return {Array} Array of diff tuples.
  85. */
  86. function diff_compute_(text1, text2) {
  87. var diffs;
  88. if (!text1) {
  89. // Just add some text (speedup).
  90. return [[DIFF_INSERT, text2]];
  91. }
  92. if (!text2) {
  93. // Just delete some text (speedup).
  94. return [[DIFF_DELETE, text1]];
  95. }
  96. var longtext = text1.length > text2.length ? text1 : text2;
  97. var shorttext = text1.length > text2.length ? text2 : text1;
  98. var i = longtext.indexOf(shorttext);
  99. if (i != -1) {
  100. // Shorter text is inside the longer text (speedup).
  101. diffs = [[DIFF_INSERT, longtext.substring(0, i)],
  102. [DIFF_EQUAL, shorttext],
  103. [DIFF_INSERT, longtext.substring(i + shorttext.length)]];
  104. // Swap insertions for deletions if diff is reversed.
  105. if (text1.length > text2.length) {
  106. diffs[0][0] = diffs[2][0] = DIFF_DELETE;
  107. }
  108. return diffs;
  109. }
  110. if (shorttext.length == 1) {
  111. // Single character string.
  112. // After the previous speedup, the character can't be an equality.
  113. return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
  114. }
  115. // Check to see if the problem can be split in two.
  116. var hm = diff_halfMatch_(text1, text2);
  117. if (hm) {
  118. // A half-match was found, sort out the return data.
  119. var text1_a = hm[0];
  120. var text1_b = hm[1];
  121. var text2_a = hm[2];
  122. var text2_b = hm[3];
  123. var mid_common = hm[4];
  124. // Send both pairs off for separate processing.
  125. var diffs_a = diff_main(text1_a, text2_a);
  126. var diffs_b = diff_main(text1_b, text2_b);
  127. // Merge the results.
  128. return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b);
  129. }
  130. return diff_bisect_(text1, text2);
  131. };
  132. /**
  133. * Find the 'middle snake' of a diff, split the problem in two
  134. * and return the recursively constructed diff.
  135. * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
  136. * @param {string} text1 Old string to be diffed.
  137. * @param {string} text2 New string to be diffed.
  138. * @return {Array} Array of diff tuples.
  139. * @private
  140. */
  141. function diff_bisect_(text1, text2) {
  142. // Cache the text lengths to prevent multiple calls.
  143. var text1_length = text1.length;
  144. var text2_length = text2.length;
  145. var max_d = Math.ceil((text1_length + text2_length) / 2);
  146. var v_offset = max_d;
  147. var v_length = 2 * max_d;
  148. var v1 = new Array(v_length);
  149. var v2 = new Array(v_length);
  150. // Setting all elements to -1 is faster in Chrome & Firefox than mixing
  151. // integers and undefined.
  152. for (var x = 0; x < v_length; x++) {
  153. v1[x] = -1;
  154. v2[x] = -1;
  155. }
  156. v1[v_offset + 1] = 0;
  157. v2[v_offset + 1] = 0;
  158. var delta = text1_length - text2_length;
  159. // If the total number of characters is odd, then the front path will collide
  160. // with the reverse path.
  161. var front = (delta % 2 != 0);
  162. // Offsets for start and end of k loop.
  163. // Prevents mapping of space beyond the grid.
  164. var k1start = 0;
  165. var k1end = 0;
  166. var k2start = 0;
  167. var k2end = 0;
  168. for (var d = 0; d < max_d; d++) {
  169. // Walk the front path one step.
  170. for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
  171. var k1_offset = v_offset + k1;
  172. var x1;
  173. if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) {
  174. x1 = v1[k1_offset + 1];
  175. } else {
  176. x1 = v1[k1_offset - 1] + 1;
  177. }
  178. var y1 = x1 - k1;
  179. while (x1 < text1_length && y1 < text2_length &&
  180. text1.charAt(x1) == text2.charAt(y1)) {
  181. x1++;
  182. y1++;
  183. }
  184. v1[k1_offset] = x1;
  185. if (x1 > text1_length) {
  186. // Ran off the right of the graph.
  187. k1end += 2;
  188. } else if (y1 > text2_length) {
  189. // Ran off the bottom of the graph.
  190. k1start += 2;
  191. } else if (front) {
  192. var k2_offset = v_offset + delta - k1;
  193. if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) {
  194. // Mirror x2 onto top-left coordinate system.
  195. var x2 = text1_length - v2[k2_offset];
  196. if (x1 >= x2) {
  197. // Overlap detected.
  198. return diff_bisectSplit_(text1, text2, x1, y1);
  199. }
  200. }
  201. }
  202. }
  203. // Walk the reverse path one step.
  204. for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
  205. var k2_offset = v_offset + k2;
  206. var x2;
  207. if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) {
  208. x2 = v2[k2_offset + 1];
  209. } else {
  210. x2 = v2[k2_offset - 1] + 1;
  211. }
  212. var y2 = x2 - k2;
  213. while (x2 < text1_length && y2 < text2_length &&
  214. text1.charAt(text1_length - x2 - 1) ==
  215. text2.charAt(text2_length - y2 - 1)) {
  216. x2++;
  217. y2++;
  218. }
  219. v2[k2_offset] = x2;
  220. if (x2 > text1_length) {
  221. // Ran off the left of the graph.
  222. k2end += 2;
  223. } else if (y2 > text2_length) {
  224. // Ran off the top of the graph.
  225. k2start += 2;
  226. } else if (!front) {
  227. var k1_offset = v_offset + delta - k2;
  228. if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) {
  229. var x1 = v1[k1_offset];
  230. var y1 = v_offset + x1 - k1_offset;
  231. // Mirror x2 onto top-left coordinate system.
  232. x2 = text1_length - x2;
  233. if (x1 >= x2) {
  234. // Overlap detected.
  235. return diff_bisectSplit_(text1, text2, x1, y1);
  236. }
  237. }
  238. }
  239. }
  240. }
  241. // Diff took too long and hit the deadline or
  242. // number of diffs equals number of characters, no commonality at all.
  243. return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
  244. };
  245. /**
  246. * Given the location of the 'middle snake', split the diff in two parts
  247. * and recurse.
  248. * @param {string} text1 Old string to be diffed.
  249. * @param {string} text2 New string to be diffed.
  250. * @param {number} x Index of split point in text1.
  251. * @param {number} y Index of split point in text2.
  252. * @return {Array} Array of diff tuples.
  253. */
  254. function diff_bisectSplit_(text1, text2, x, y) {
  255. var text1a = text1.substring(0, x);
  256. var text2a = text2.substring(0, y);
  257. var text1b = text1.substring(x);
  258. var text2b = text2.substring(y);
  259. // Compute both diffs serially.
  260. var diffs = diff_main(text1a, text2a);
  261. var diffsb = diff_main(text1b, text2b);
  262. return diffs.concat(diffsb);
  263. };
  264. /**
  265. * Determine the common prefix of two strings.
  266. * @param {string} text1 First string.
  267. * @param {string} text2 Second string.
  268. * @return {number} The number of characters common to the start of each
  269. * string.
  270. */
  271. function diff_commonPrefix(text1, text2) {
  272. // Quick check for common null cases.
  273. if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) {
  274. return 0;
  275. }
  276. // Binary search.
  277. // Performance analysis: http://neil.fraser.name/news/2007/10/09/
  278. var pointermin = 0;
  279. var pointermax = Math.min(text1.length, text2.length);
  280. var pointermid = pointermax;
  281. var pointerstart = 0;
  282. while (pointermin < pointermid) {
  283. if (text1.substring(pointerstart, pointermid) ==
  284. text2.substring(pointerstart, pointermid)) {
  285. pointermin = pointermid;
  286. pointerstart = pointermin;
  287. } else {
  288. pointermax = pointermid;
  289. }
  290. pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
  291. }
  292. return pointermid;
  293. };
  294. /**
  295. * Determine the common suffix of two strings.
  296. * @param {string} text1 First string.
  297. * @param {string} text2 Second string.
  298. * @return {number} The number of characters common to the end of each string.
  299. */
  300. function diff_commonSuffix(text1, text2) {
  301. // Quick check for common null cases.
  302. if (!text1 || !text2 ||
  303. text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) {
  304. return 0;
  305. }
  306. // Binary search.
  307. // Performance analysis: http://neil.fraser.name/news/2007/10/09/
  308. var pointermin = 0;
  309. var pointermax = Math.min(text1.length, text2.length);
  310. var pointermid = pointermax;
  311. var pointerend = 0;
  312. while (pointermin < pointermid) {
  313. if (text1.substring(text1.length - pointermid, text1.length - pointerend) ==
  314. text2.substring(text2.length - pointermid, text2.length - pointerend)) {
  315. pointermin = pointermid;
  316. pointerend = pointermin;
  317. } else {
  318. pointermax = pointermid;
  319. }
  320. pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
  321. }
  322. return pointermid;
  323. };
  324. /**
  325. * Do the two texts share a substring which is at least half the length of the
  326. * longer text?
  327. * This speedup can produce non-minimal diffs.
  328. * @param {string} text1 First string.
  329. * @param {string} text2 Second string.
  330. * @return {Array.<string>} Five element Array, containing the prefix of
  331. * text1, the suffix of text1, the prefix of text2, the suffix of
  332. * text2 and the common middle. Or null if there was no match.
  333. */
  334. function diff_halfMatch_(text1, text2) {
  335. var longtext = text1.length > text2.length ? text1 : text2;
  336. var shorttext = text1.length > text2.length ? text2 : text1;
  337. if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
  338. return null; // Pointless.
  339. }
  340. /**
  341. * Does a substring of shorttext exist within longtext such that the substring
  342. * is at least half the length of longtext?
  343. * Closure, but does not reference any external variables.
  344. * @param {string} longtext Longer string.
  345. * @param {string} shorttext Shorter string.
  346. * @param {number} i Start index of quarter length substring within longtext.
  347. * @return {Array.<string>} Five element Array, containing the prefix of
  348. * longtext, the suffix of longtext, the prefix of shorttext, the suffix
  349. * of shorttext and the common middle. Or null if there was no match.
  350. * @private
  351. */
  352. function diff_halfMatchI_(longtext, shorttext, i) {
  353. // Start with a 1/4 length substring at position i as a seed.
  354. var seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
  355. var j = -1;
  356. var best_common = '';
  357. var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b;
  358. while ((j = shorttext.indexOf(seed, j + 1)) != -1) {
  359. var prefixLength = diff_commonPrefix(longtext.substring(i),
  360. shorttext.substring(j));
  361. var suffixLength = diff_commonSuffix(longtext.substring(0, i),
  362. shorttext.substring(0, j));
  363. if (best_common.length < suffixLength + prefixLength) {
  364. best_common = shorttext.substring(j - suffixLength, j) +
  365. shorttext.substring(j, j + prefixLength);
  366. best_longtext_a = longtext.substring(0, i - suffixLength);
  367. best_longtext_b = longtext.substring(i + prefixLength);
  368. best_shorttext_a = shorttext.substring(0, j - suffixLength);
  369. best_shorttext_b = shorttext.substring(j + prefixLength);
  370. }
  371. }
  372. if (best_common.length * 2 >= longtext.length) {
  373. return [best_longtext_a, best_longtext_b,
  374. best_shorttext_a, best_shorttext_b, best_common];
  375. } else {
  376. return null;
  377. }
  378. }
  379. // First check if the second quarter is the seed for a half-match.
  380. var hm1 = diff_halfMatchI_(longtext, shorttext,
  381. Math.ceil(longtext.length / 4));
  382. // Check again based on the third quarter.
  383. var hm2 = diff_halfMatchI_(longtext, shorttext,
  384. Math.ceil(longtext.length / 2));
  385. var hm;
  386. if (!hm1 && !hm2) {
  387. return null;
  388. } else if (!hm2) {
  389. hm = hm1;
  390. } else if (!hm1) {
  391. hm = hm2;
  392. } else {
  393. // Both matched. Select the longest.
  394. hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
  395. }
  396. // A half-match was found, sort out the return data.
  397. var text1_a, text1_b, text2_a, text2_b;
  398. if (text1.length > text2.length) {
  399. text1_a = hm[0];
  400. text1_b = hm[1];
  401. text2_a = hm[2];
  402. text2_b = hm[3];
  403. } else {
  404. text2_a = hm[0];
  405. text2_b = hm[1];
  406. text1_a = hm[2];
  407. text1_b = hm[3];
  408. }
  409. var mid_common = hm[4];
  410. return [text1_a, text1_b, text2_a, text2_b, mid_common];
  411. };
  412. /**
  413. * Reorder and merge like edit sections. Merge equalities.
  414. * Any edit section can move as long as it doesn't cross an equality.
  415. * @param {Array} diffs Array of diff tuples.
  416. */
  417. function diff_cleanupMerge(diffs) {
  418. diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end.
  419. var pointer = 0;
  420. var count_delete = 0;
  421. var count_insert = 0;
  422. var text_delete = '';
  423. var text_insert = '';
  424. var commonlength;
  425. while (pointer < diffs.length) {
  426. switch (diffs[pointer][0]) {
  427. case DIFF_INSERT:
  428. count_insert++;
  429. text_insert += diffs[pointer][1];
  430. pointer++;
  431. break;
  432. case DIFF_DELETE:
  433. count_delete++;
  434. text_delete += diffs[pointer][1];
  435. pointer++;
  436. break;
  437. case DIFF_EQUAL:
  438. // Upon reaching an equality, check for prior redundancies.
  439. if (count_delete + count_insert > 1) {
  440. if (count_delete !== 0 && count_insert !== 0) {
  441. // Factor out any common prefixies.
  442. commonlength = diff_commonPrefix(text_insert, text_delete);
  443. if (commonlength !== 0) {
  444. if ((pointer - count_delete - count_insert) > 0 &&
  445. diffs[pointer - count_delete - count_insert - 1][0] ==
  446. DIFF_EQUAL) {
  447. diffs[pointer - count_delete - count_insert - 1][1] +=
  448. text_insert.substring(0, commonlength);
  449. } else {
  450. diffs.splice(0, 0, [DIFF_EQUAL,
  451. text_insert.substring(0, commonlength)]);
  452. pointer++;
  453. }
  454. text_insert = text_insert.substring(commonlength);
  455. text_delete = text_delete.substring(commonlength);
  456. }
  457. // Factor out any common suffixies.
  458. commonlength = diff_commonSuffix(text_insert, text_delete);
  459. if (commonlength !== 0) {
  460. diffs[pointer][1] = text_insert.substring(text_insert.length -
  461. commonlength) + diffs[pointer][1];
  462. text_insert = text_insert.substring(0, text_insert.length -
  463. commonlength);
  464. text_delete = text_delete.substring(0, text_delete.length -
  465. commonlength);
  466. }
  467. }
  468. // Delete the offending records and add the merged ones.
  469. if (count_delete === 0) {
  470. diffs.splice(pointer - count_insert,
  471. count_delete + count_insert, [DIFF_INSERT, text_insert]);
  472. } else if (count_insert === 0) {
  473. diffs.splice(pointer - count_delete,
  474. count_delete + count_insert, [DIFF_DELETE, text_delete]);
  475. } else {
  476. diffs.splice(pointer - count_delete - count_insert,
  477. count_delete + count_insert, [DIFF_DELETE, text_delete],
  478. [DIFF_INSERT, text_insert]);
  479. }
  480. pointer = pointer - count_delete - count_insert +
  481. (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1;
  482. } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) {
  483. // Merge this equality with the previous one.
  484. diffs[pointer - 1][1] += diffs[pointer][1];
  485. diffs.splice(pointer, 1);
  486. } else {
  487. pointer++;
  488. }
  489. count_insert = 0;
  490. count_delete = 0;
  491. text_delete = '';
  492. text_insert = '';
  493. break;
  494. }
  495. }
  496. if (diffs[diffs.length - 1][1] === '') {
  497. diffs.pop(); // Remove the dummy entry at the end.
  498. }
  499. // Second pass: look for single edits surrounded on both sides by equalities
  500. // which can be shifted sideways to eliminate an equality.
  501. // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
  502. var changes = false;
  503. pointer = 1;
  504. // Intentionally ignore the first and last element (don't need checking).
  505. while (pointer < diffs.length - 1) {
  506. if (diffs[pointer - 1][0] == DIFF_EQUAL &&
  507. diffs[pointer + 1][0] == DIFF_EQUAL) {
  508. // This is a single edit surrounded by equalities.
  509. if (diffs[pointer][1].substring(diffs[pointer][1].length -
  510. diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) {
  511. // Shift the edit over the previous equality.
  512. diffs[pointer][1] = diffs[pointer - 1][1] +
  513. diffs[pointer][1].substring(0, diffs[pointer][1].length -
  514. diffs[pointer - 1][1].length);
  515. diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
  516. diffs.splice(pointer - 1, 1);
  517. changes = true;
  518. } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) ==
  519. diffs[pointer + 1][1]) {
  520. // Shift the edit over the next equality.
  521. diffs[pointer - 1][1] += diffs[pointer + 1][1];
  522. diffs[pointer][1] =
  523. diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
  524. diffs[pointer + 1][1];
  525. diffs.splice(pointer + 1, 1);
  526. changes = true;
  527. }
  528. }
  529. pointer++;
  530. }
  531. // If shifts were made, the diff needs reordering and another shift sweep.
  532. if (changes) {
  533. diff_cleanupMerge(diffs);
  534. }
  535. };
  536. var diff = diff_main;
  537. diff.INSERT = DIFF_INSERT;
  538. diff.DELETE = DIFF_DELETE;
  539. diff.EQUAL = DIFF_EQUAL;
  540. module.exports = diff;
  541. /*
  542. * Modify a diff such that the cursor position points to the start of a change:
  543. * E.g.
  544. * cursor_normalize_diff([[DIFF_EQUAL, 'abc']], 1)
  545. * => [1, [[DIFF_EQUAL, 'a'], [DIFF_EQUAL, 'bc']]]
  546. * cursor_normalize_diff([[DIFF_INSERT, 'new'], [DIFF_DELETE, 'xyz']], 2)
  547. * => [2, [[DIFF_INSERT, 'new'], [DIFF_DELETE, 'xy'], [DIFF_DELETE, 'z']]]
  548. *
  549. * @param {Array} diffs Array of diff tuples
  550. * @param {Int} cursor_pos Suggested edit position. Must not be out of bounds!
  551. * @return {Array} A tuple [cursor location in the modified diff, modified diff]
  552. */
  553. function cursor_normalize_diff (diffs, cursor_pos) {
  554. if (cursor_pos === 0) {
  555. return [DIFF_EQUAL, diffs];
  556. }
  557. for (var current_pos = 0, i = 0; i < diffs.length; i++) {
  558. var d = diffs[i];
  559. if (d[0] === DIFF_DELETE || d[0] === DIFF_EQUAL) {
  560. var next_pos = current_pos + d[1].length;
  561. if (cursor_pos === next_pos) {
  562. return [i + 1, diffs];
  563. } else if (cursor_pos < next_pos) {
  564. // copy to prevent side effects
  565. diffs = diffs.slice();
  566. // split d into two diff changes
  567. var split_pos = cursor_pos - current_pos;
  568. var d_left = [d[0], d[1].slice(0, split_pos)];
  569. var d_right = [d[0], d[1].slice(split_pos)];
  570. diffs.splice(i, 1, d_left, d_right);
  571. return [i + 1, diffs];
  572. } else {
  573. current_pos = next_pos;
  574. }
  575. }
  576. }
  577. throw new Error('cursor_pos is out of bounds!')
  578. }
  579. /*
  580. * Modify a diff such that the edit position is "shifted" to the proposed edit location (cursor_position).
  581. *
  582. * Case 1)
  583. * Check if a naive shift is possible:
  584. * [0, X], [ 1, Y] -> [ 1, Y], [0, X] (if X + Y === Y + X)
  585. * [0, X], [-1, Y] -> [-1, Y], [0, X] (if X + Y === Y + X) - holds same result
  586. * Case 2)
  587. * Check if the following shifts are possible:
  588. * [0, 'pre'], [ 1, 'prefix'] -> [ 1, 'pre'], [0, 'pre'], [ 1, 'fix']
  589. * [0, 'pre'], [-1, 'prefix'] -> [-1, 'pre'], [0, 'pre'], [-1, 'fix']
  590. * ^ ^
  591. * d d_next
  592. *
  593. * @param {Array} diffs Array of diff tuples
  594. * @param {Int} cursor_pos Suggested edit position. Must not be out of bounds!
  595. * @return {Array} Array of diff tuples
  596. */
  597. function fix_cursor (diffs, cursor_pos) {
  598. var norm = cursor_normalize_diff(diffs, cursor_pos);
  599. var ndiffs = norm[1];
  600. var cursor_pointer = norm[0];
  601. var d = ndiffs[cursor_pointer];
  602. var d_next = ndiffs[cursor_pointer + 1];
  603. if (d == null) {
  604. // Text was deleted from end of original string,
  605. // cursor is now out of bounds in new string
  606. return diffs;
  607. } else if (d[0] !== DIFF_EQUAL) {
  608. // A modification happened at the cursor location.
  609. // This is the expected outcome, so we can return the original diff.
  610. return diffs;
  611. } else {
  612. if (d_next != null && d[1] + d_next[1] === d_next[1] + d[1]) {
  613. // Case 1)
  614. // It is possible to perform a naive shift
  615. ndiffs.splice(cursor_pointer, 2, d_next, d)
  616. return merge_tuples(ndiffs, cursor_pointer, 2)
  617. } else if (d_next != null && d_next[1].indexOf(d[1]) === 0) {
  618. // Case 2)
  619. // d[1] is a prefix of d_next[1]
  620. // We can assume that d_next[0] !== 0, since d[0] === 0
  621. // Shift edit locations..
  622. ndiffs.splice(cursor_pointer, 2, [d_next[0], d[1]], [0, d[1]]);
  623. var suffix = d_next[1].slice(d[1].length);
  624. if (suffix.length > 0) {
  625. ndiffs.splice(cursor_pointer + 2, 0, [d_next[0], suffix]);
  626. }
  627. return merge_tuples(ndiffs, cursor_pointer, 3)
  628. } else {
  629. // Not possible to perform any modification
  630. return diffs;
  631. }
  632. }
  633. }
  634. /*
  635. * Check diff did not split surrogate pairs.
  636. * Ex. [0, '\uD83D'], [-1, '\uDC36'], [1, '\uDC2F'] -> [-1, '\uD83D\uDC36'], [1, '\uD83D\uDC2F']
  637. * '\uD83D\uDC36' === '🐶', '\uD83D\uDC2F' === '🐯'
  638. *
  639. * @param {Array} diffs Array of diff tuples
  640. * @return {Array} Array of diff tuples
  641. */
  642. function fix_emoji (diffs) {
  643. var compact = false;
  644. var starts_with_pair_end = function(str) {
  645. return str.charCodeAt(0) >= 0xDC00 && str.charCodeAt(0) <= 0xDFFF;
  646. }
  647. var ends_with_pair_start = function(str) {
  648. return str.charCodeAt(str.length-1) >= 0xD800 && str.charCodeAt(str.length-1) <= 0xDBFF;
  649. }
  650. for (var i = 2; i < diffs.length; i += 1) {
  651. if (diffs[i-2][0] === DIFF_EQUAL && ends_with_pair_start(diffs[i-2][1]) &&
  652. diffs[i-1][0] === DIFF_DELETE && starts_with_pair_end(diffs[i-1][1]) &&
  653. diffs[i][0] === DIFF_INSERT && starts_with_pair_end(diffs[i][1])) {
  654. compact = true;
  655. diffs[i-1][1] = diffs[i-2][1].slice(-1) + diffs[i-1][1];
  656. diffs[i][1] = diffs[i-2][1].slice(-1) + diffs[i][1];
  657. diffs[i-2][1] = diffs[i-2][1].slice(0, -1);
  658. }
  659. }
  660. if (!compact) {
  661. return diffs;
  662. }
  663. var fixed_diffs = [];
  664. for (var i = 0; i < diffs.length; i += 1) {
  665. if (diffs[i][1].length > 0) {
  666. fixed_diffs.push(diffs[i]);
  667. }
  668. }
  669. return fixed_diffs;
  670. }
  671. /*
  672. * Try to merge tuples with their neigbors in a given range.
  673. * E.g. [0, 'a'], [0, 'b'] -> [0, 'ab']
  674. *
  675. * @param {Array} diffs Array of diff tuples.
  676. * @param {Int} start Position of the first element to merge (diffs[start] is also merged with diffs[start - 1]).
  677. * @param {Int} length Number of consecutive elements to check.
  678. * @return {Array} Array of merged diff tuples.
  679. */
  680. function merge_tuples (diffs, start, length) {
  681. // Check from (start-1) to (start+length).
  682. for (var i = start + length - 1; i >= 0 && i >= start - 1; i--) {
  683. if (i + 1 < diffs.length) {
  684. var left_d = diffs[i];
  685. var right_d = diffs[i+1];
  686. if (left_d[0] === right_d[1]) {
  687. diffs.splice(i, 2, [left_d[0], left_d[1] + right_d[1]]);
  688. }
  689. }
  690. }
  691. return diffs;
  692. }