splitDiff.tsx 2.8 KB

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