diff_strings.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', {
  3. value: true
  4. });
  5. exports.default = diffStrings;
  6. var _chalk = require('chalk');
  7. var _chalk2 = _interopRequireDefault(_chalk);
  8. var _diff = require('diff');
  9. var _constants = require('./constants.js');
  10. function _interopRequireDefault(obj) {
  11. return obj && obj.__esModule ? obj : {default: obj};
  12. }
  13. const DIFF_CONTEXT_DEFAULT = 5; // removed | added | equal
  14. // Given diff digit, return array which consists of:
  15. // if compared line is removed or added: corresponding original line
  16. // if compared line is equal: original received and expected lines
  17. /**
  18. * Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
  19. *
  20. * This source code is licensed under the MIT license found in the
  21. * LICENSE file in the root directory of this source tree.
  22. *
  23. *
  24. */
  25. // Given chunk, return diff character.
  26. const getDiffChar = chunk => (chunk.removed ? '-' : chunk.added ? '+' : ' ');
  27. // Given diff character in line of hunk or computed from properties of chunk.
  28. const getDiffDigit = char => (char === '-' ? -1 : char === '+' ? 1 : 0);
  29. // Color for text of line.
  30. const getColor = (digit, onlyIndentationChanged) => {
  31. if (digit === -1) {
  32. return _chalk2.default.green; // removed
  33. }
  34. if (digit === 1) {
  35. return _chalk2.default.red; // added
  36. }
  37. return onlyIndentationChanged ? _chalk2.default.cyan : _chalk2.default.dim;
  38. };
  39. // Do NOT color leading or trailing spaces if original lines are equal:
  40. // Background color for leading or trailing spaces.
  41. const getBgColor = (digit, onlyIndentationChanged) =>
  42. digit === 0 && !onlyIndentationChanged
  43. ? _chalk2.default.bgYellow
  44. : _chalk2.default.inverse;
  45. // ONLY trailing if expected value is snapshot or multiline string.
  46. const highlightTrailingSpaces = (line, bgColor) =>
  47. line.replace(/\s+$/, bgColor('$&'));
  48. // BOTH leading AND trailing if expected value is data structure.
  49. const highlightLeadingTrailingSpaces = (
  50. line,
  51. bgColor
  52. // If line consists of ALL spaces: highlight all of them.
  53. ) =>
  54. highlightTrailingSpaces(line, bgColor).replace(
  55. // If line has an ODD length of leading spaces: highlight only the LAST.
  56. /^(\s\s)*(\s)(?=[^\s])/,
  57. '$1' + bgColor('$2')
  58. );
  59. const getAnnotation = options =>
  60. _chalk2.default.green(
  61. '- ' + ((options && options.aAnnotation) || 'Expected')
  62. ) +
  63. '\n' +
  64. _chalk2.default.red('+ ' + ((options && options.bAnnotation) || 'Received')) +
  65. '\n\n';
  66. // Given string, return array of its lines.
  67. const splitIntoLines = string => {
  68. const lines = string.split('\n');
  69. if (lines.length !== 0 && lines[lines.length - 1] === '') {
  70. lines.pop();
  71. }
  72. return lines;
  73. };
  74. // Given diff character and compared line, return original line with colors.
  75. const formatLine = (char, lineCompared, getOriginal) => {
  76. const digit = getDiffDigit(char);
  77. if (getOriginal) {
  78. // Compared without indentation if expected value is data structure.
  79. const lineArray = getOriginal(digit);
  80. const lineOriginal = lineArray[0];
  81. const onlyIndentationChanged =
  82. digit === 0 && lineOriginal.length !== lineArray[1].length;
  83. return getColor(digit, onlyIndentationChanged)(
  84. char +
  85. ' ' +
  86. // Prepend indentation spaces from original to compared line.
  87. lineOriginal.slice(0, lineOriginal.length - lineCompared.length) +
  88. highlightLeadingTrailingSpaces(
  89. lineCompared,
  90. getBgColor(digit, onlyIndentationChanged)
  91. )
  92. );
  93. }
  94. // Format compared line when expected is snapshot or multiline string.
  95. return getColor(digit)(
  96. char + ' ' + highlightTrailingSpaces(lineCompared, getBgColor(digit))
  97. );
  98. };
  99. // Given original lines, return callback function
  100. // which given diff digit, returns array.
  101. const getterForChunks = original => {
  102. const linesExpected = splitIntoLines(original.a);
  103. const linesReceived = splitIntoLines(original.b);
  104. let iExpected = 0;
  105. let iReceived = 0;
  106. return digit => {
  107. if (digit === -1) {
  108. return [linesExpected[iExpected++]];
  109. }
  110. if (digit === 1) {
  111. return [linesReceived[iReceived++]];
  112. }
  113. // Because compared line is equal: original received and expected lines.
  114. return [linesReceived[iReceived++], linesExpected[iExpected++]];
  115. };
  116. };
  117. // jest --expand
  118. const formatChunks = (a, b, original) => {
  119. const chunks = (0, _diff.diffLines)(a, b);
  120. if (chunks.every(chunk => !chunk.removed && !chunk.added)) {
  121. return null;
  122. }
  123. const getOriginal = original && getterForChunks(original);
  124. return chunks
  125. .reduce((lines, chunk) => {
  126. const char = getDiffChar(chunk);
  127. splitIntoLines(chunk.value).forEach(line => {
  128. lines.push(formatLine(char, line, getOriginal));
  129. });
  130. return lines;
  131. }, [])
  132. .join('\n');
  133. };
  134. // Only show patch marks ("@@ ... @@") if the diff is big.
  135. // To determine this, we need to compare either the original string (a) to
  136. // `hunk.oldLines` or a new string to `hunk.newLines`.
  137. // If the `oldLinesCount` is greater than `hunk.oldLines`
  138. // we can be sure that at least 1 line has been "hidden".
  139. const shouldShowPatchMarks = (hunk, oldLinesCount) =>
  140. oldLinesCount > hunk.oldLines;
  141. const createPatchMark = hunk => {
  142. const markOld = `-${hunk.oldStart},${hunk.oldLines}`;
  143. const markNew = `+${hunk.newStart},${hunk.newLines}`;
  144. return _chalk2.default.yellow(`@@ ${markOld} ${markNew} @@`);
  145. };
  146. // Given original lines, return callback function which given indexes for hunk,
  147. // returns another callback function which given diff digit, returns array.
  148. const getterForHunks = original => {
  149. const linesExpected = splitIntoLines(original.a);
  150. const linesReceived = splitIntoLines(original.b);
  151. return (iExpected, iReceived) => digit => {
  152. if (digit === -1) {
  153. return [linesExpected[iExpected++]];
  154. }
  155. if (digit === 1) {
  156. return [linesReceived[iReceived++]];
  157. }
  158. // Because compared line is equal: original received and expected lines.
  159. return [linesReceived[iReceived++], linesExpected[iExpected++]];
  160. };
  161. };
  162. // jest --no-expand
  163. const formatHunks = (a, b, contextLines, original) => {
  164. const options = {
  165. context:
  166. typeof contextLines === 'number' && contextLines >= 0
  167. ? contextLines
  168. : DIFF_CONTEXT_DEFAULT
  169. };
  170. var _structuredPatch = (0, _diff.structuredPatch)(
  171. '',
  172. '',
  173. a,
  174. b,
  175. '',
  176. '',
  177. options
  178. );
  179. const hunks = _structuredPatch.hunks;
  180. if (hunks.length === 0) {
  181. return null;
  182. }
  183. const getter = original && getterForHunks(original);
  184. const oldLinesCount = (a.match(/\n/g) || []).length;
  185. return hunks
  186. .reduce((lines, hunk) => {
  187. if (shouldShowPatchMarks(hunk, oldLinesCount)) {
  188. lines.push(createPatchMark(hunk));
  189. }
  190. // Hunk properties are one-based but index args are zero-based.
  191. const getOriginal =
  192. getter && getter(hunk.oldStart - 1, hunk.newStart - 1);
  193. hunk.lines.forEach(line => {
  194. lines.push(formatLine(line[0], line.slice(1), getOriginal));
  195. });
  196. return lines;
  197. }, [])
  198. .join('\n');
  199. };
  200. function diffStrings(a, b, options, original) {
  201. // Because `formatHunks` and `formatChunks` ignore one trailing newline,
  202. // always append newline to strings:
  203. a += '\n';
  204. b += '\n';
  205. // `diff` uses the Myers LCS diff algorithm which runs in O(n+d^2) time
  206. // (where "d" is the edit distance) and can get very slow for large edit
  207. // distances. Mitigate the cost by switching to a lower-resolution diff
  208. // whenever linebreaks are involved.
  209. const result =
  210. options && options.expand === false
  211. ? formatHunks(a, b, options && options.contextLines, original)
  212. : formatChunks(a, b, original);
  213. return result === null
  214. ? _constants.NO_DIFF_MESSAGE
  215. : getAnnotation(options) + result;
  216. }