splitDiff.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import styled from '@emotion/styled';
  2. import {Change, diffChars, diffLines, diffWords} from 'diff';
  3. const diffFnMap = {
  4. chars: diffChars,
  5. words: diffWords,
  6. lines: diffLines,
  7. } as const;
  8. type Props = {
  9. base: string;
  10. target: string;
  11. className?: string;
  12. type?: keyof typeof diffFnMap;
  13. };
  14. const SplitDiff = ({className, type = 'lines', base, target}: Props) => {
  15. const diffFn = diffFnMap[type];
  16. const baseLines = base.split('\n');
  17. const targetLines = target.split('\n');
  18. const [largerArray] =
  19. baseLines.length > targetLines.length
  20. ? [baseLines, targetLines]
  21. : [targetLines, baseLines];
  22. const results = largerArray.map((_line, index) =>
  23. diffFn(baseLines[index] || '', targetLines[index] || '', {newlineIsToken: true})
  24. );
  25. return (
  26. <SplitTable className={className} data-test-id="split-diff">
  27. <SplitBody>
  28. {results.map((line, j) => {
  29. const highlightAdded = line.find(result => result.added);
  30. const highlightRemoved = line.find(result => result.removed);
  31. return (
  32. <tr key={j}>
  33. <Cell isRemoved={highlightRemoved}>
  34. <Line>
  35. {line
  36. .filter(result => !result.added)
  37. .map((result, i) => (
  38. <Word key={i} isRemoved={result.removed}>
  39. {result.value}
  40. </Word>
  41. ))}
  42. </Line>
  43. </Cell>
  44. <Gap />
  45. <Cell isAdded={highlightAdded}>
  46. <Line>
  47. {line
  48. .filter(result => !result.removed)
  49. .map((result, i) => (
  50. <Word key={i} isAdded={result.added}>
  51. {result.value}
  52. </Word>
  53. ))}
  54. </Line>
  55. </Cell>
  56. </tr>
  57. );
  58. })}
  59. </SplitBody>
  60. </SplitTable>
  61. );
  62. };
  63. const SplitTable = styled('table')`
  64. table-layout: fixed;
  65. border-collapse: collapse;
  66. width: 100%;
  67. `;
  68. const SplitBody = styled('tbody')`
  69. font-family: ${p => p.theme.text.familyMono};
  70. font-size: ${p => p.theme.fontSizeSmall};
  71. `;
  72. const Cell = styled('td')<{isAdded?: Change; isRemoved?: Change}>`
  73. vertical-align: top;
  74. ${p => p.isRemoved && `background-color: ${p.theme.diff.removedRow}`};
  75. ${p => p.isAdded && `background-color: ${p.theme.diff.addedRow}`};
  76. `;
  77. const Gap = styled('td')`
  78. width: 20px;
  79. `;
  80. const Line = styled('div')`
  81. display: flex;
  82. flex-wrap: wrap;
  83. `;
  84. const Word = styled('span')<{isAdded?: boolean; isRemoved?: boolean}>`
  85. white-space: pre-wrap;
  86. word-break: break-all;
  87. ${p => p.isRemoved && `background-color: ${p.theme.diff.removed}`};
  88. ${p => p.isAdded && `background-color: ${p.theme.diff.added}`};
  89. `;
  90. export default SplitDiff;