diff_strings.js 7.3 KB

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