├── .eslintrc
├── .github
└── FUNDING.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── enzyme.ts
├── examples
└── src
│ ├── diff
│ ├── javascript
│ │ ├── new.rjs
│ │ └── old.rjs
│ ├── json
│ │ ├── new.json
│ │ └── old.json
│ └── xml
│ │ ├── new.xml
│ │ └── old.xml
│ ├── index.ejs
│ ├── index.tsx
│ └── style.scss
├── logo-standalone.png
├── logo.png
├── package.json
├── publish-examples.sh
├── src
├── compute-lines.ts
├── index.tsx
└── styles.ts
├── test
├── compute-lines-test.ts
└── react-diff-viewer-test.tsx
├── tsconfig.examples.json
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["airbnb-base", "plugin:@typescript-eslint/recommended"],
3 | "rules": {
4 | "no-tabs": "off",
5 | "@typescript-eslint/indent": ["error", 2],
6 | "max-len": ["error", {
7 | "code": 100
8 | }],
9 | "arrow-body-style": "off",
10 | },
11 | "settings": {
12 | "import/resolver": {
13 | "node": {
14 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
15 | }
16 | }
17 | },
18 | "env": {
19 | "mocha": true,
20 | "node": true,
21 | "browser": true,
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: https://www.buymeacoffee.com/cAHgxoB
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | lib
4 | *.log
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | examples
2 | test
3 | *.svg
4 | src
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: 12
3 | before_script: yarn build
4 | after_success: './publish-examples.sh'
5 | deploy:
6 | skip_cleanup: true
7 | provider: npm
8 | email: praneshpranesh@gmail.com
9 | api_key:
10 | secure: ytHogDV6wWZKY3eCgdJ5TFCz57llxTe6W1EuLBbg3TvHfpAmcL5XzT77T0lNTYY95XmABwGpf4qvqui1fXVoFqqSXVPSVt9I1aQ4gxBzxPKHGPV++wwgVOBfZErIlYPW4kcYJQvfemBDBfAMQS2u/0si5mZbeyVkvlnhHLnylsc1sJUGGnq9PJxwCagBd/DAhYYCU5d4a9psOwo6GnAwzs37rgVIb5oNhbtex0r1u5D4irweVz4V1TkwhMczw3v8t0d+I2REqjGkqLdKJOt9Q/Uqc7v34wIAy4Xng8zla7PKlzVNDlsA19en4DezJ/YRU3o6JxsMICRDyIRcwaebX2S7S35cif2gJRSF+LRCq2XdHvDAXPt6trQoeo9mSdq6+dL4/uYbhliUOyhzw84CRdY3+VrST1h7h8LAGODQzYJbQs20PkVL2527Bk+eS+tNCNpr/jSWG/iDO477j9jTGNVloF2IRHikbnZDFcF4ZrKE5RHEWSbQzyHkOOTgo5k+p6Doyv4jSgdLlnck2+f5n4Ocs/xh1c0UAphdW/mw/8lAv0+gpoL8f3rfN4AD5LYjjQ7xK5yO6RfIzsmAOqs7WCyY0ewXaPaP6Nz+Ne5Q2CKwVZiqj3qvd4o3aJodnquJ1iKzxbNHd/LFScEQxYSslyRluHrM+7DASPfccA7ePnE=
11 | on:
12 | tags: true
13 | repo: praneshr/react-diff-viewer
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Pranesh Ravi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | [](https://travis-ci.com/praneshr/react-diff-viewer)
8 | [](https://badge.fury.io/js/react-diff-viewer)
9 | [](https://github.com/praneshr/react-diff-viewer/blob/master/LICENSE)
10 |
11 | A simple and beautiful text diff viewer component made with [Diff](https://github.com/kpdecker/jsdiff) and [React](https://reactjs.org).
12 |
13 | Inspired from Github diff viewer, it includes features like split view, inline view, word diff, line highlight and more. It is highly customizable and it supports almost all languages.
14 |
15 | Check [here](https://github.com/praneshr/react-diff-viewer/tree/v2.0) for v2.0
16 |
17 | ## Install
18 |
19 | ```bash
20 | yarn add react-diff-viewer
21 |
22 | # or
23 |
24 | npm i react-diff-viewer
25 | ```
26 |
27 | ## Usage
28 |
29 | ```javascript
30 | import React, { PureComponent } from 'react';
31 | import ReactDiffViewer from 'react-diff-viewer';
32 |
33 | const oldCode = `
34 | const a = 10
35 | const b = 10
36 | const c = () => console.log('foo')
37 |
38 | if(a > 10) {
39 | console.log('bar')
40 | }
41 |
42 | console.log('done')
43 | `;
44 | const newCode = `
45 | const a = 10
46 | const boo = 10
47 |
48 | if(a === 10) {
49 | console.log('bar')
50 | }
51 | `;
52 |
53 | class Diff extends PureComponent {
54 | render = () => {
55 | return (
56 |
57 | );
58 | };
59 | }
60 | ```
61 |
62 | ## Props
63 |
64 | | Prop | Type | Default | Description |
65 | | ------------------------- | --------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
66 | | oldValue | `string` | `''` | Old value as string. |
67 | | newValue | `string` | `''` | New value as string. |
68 | | splitView | `boolean` | `true` | Switch between `unified` and `split` view. |
69 | | disableWordDiff | `boolean` | `false` | Show and hide word diff in a diff line. |
70 | | compareMethod | `DiffMethod` | `DiffMethod.CHARS` | JsDiff text diff method used for diffing strings. Check out the [guide](https://github.com/praneshr/react-diff-viewer/tree/v3.0.0#text-block-diff-comparison) to use different methods. |
71 | | hideLineNumbers | `boolean` | `false` | Show and hide line numbers. |
72 | | renderContent | `function` | `undefined` | Render Prop API to render code in the diff viewer. Helpful for [syntax highlighting](#syntax-highlighting) |
73 | | onLineNumberClick | `function` | `undefined` | Event handler for line number click. `(lineId: string) => void` |
74 | | highlightLines | `array[string]` | `[]` | List of lines to be highlighted. Works together with `onLineNumberClick`. Line number are prefixed with `L` and `R` for the left and right section of the diff viewer, respectively. For example, `L-20` means 20th line in the left pane. To highlight a range of line numbers, pass the prefixed line number as an array. For example, `[L-2, L-3, L-4, L-5]` will highlight the lines `2-5` in the left pane. |
75 | | showDiffOnly | `boolean` | `true` | Shows only the diffed lines and folds the unchanged lines |
76 | | extraLinesSurroundingDiff | `number` | `3` | Number of extra unchanged lines surrounding the diff. Works along with `showDiffOnly`. |
77 | | codeFoldMessageRenderer | `function` | `Expand {number} of lines ...` | Render Prop API to render code fold message. |
78 | | styles | `object` | `{}` | To override style variables and styles. Learn more about [overriding styles](#overriding-styles) |
79 | | useDarkTheme | `boolean` | `true` | To enable/disable dark theme. |
80 | | leftTitle | `string` | `undefined` | Column title for left section of the diff in split view. This will be used as the only title in inline view. |
81 | | rightTitle | `string` | `undefined` | Column title for right section of the diff in split view. This will be ignored in inline view. |
82 | | linesOffset | `number` | `0` | Number to start count code lines from. |
83 |
84 | ## Instance Methods
85 |
86 | `resetCodeBlocks()` - Resets the expanded code blocks to it's initial state. Return `true` on successful reset and `false` during unsuccessful reset.
87 |
88 | ## Syntax Highlighting
89 |
90 | Syntax highlighting is a bit tricky when combined with diff. Here, React Diff Viewer provides a simple render prop API to handle syntax highlighting. Use `renderContent(content: string) => JSX.Element` and your favorite syntax highlighting library to achieve this.
91 |
92 | An example using [Prism JS](https://prismjs.com)
93 |
94 | ```html
95 | // Load Prism CSS
96 |
99 |
100 | // Load Prism JS
101 |
102 | ```
103 |
104 | ```javascript
105 | import React, { PureComponent } from 'react';
106 | import ReactDiffViewer from 'react-diff-viewer';
107 |
108 | const oldCode = `
109 | const a = 10
110 | const b = 10
111 | const c = () => console.log('foo')
112 |
113 | if(a > 10) {
114 | console.log('bar')
115 | }
116 |
117 | console.log('done')
118 | `;
119 | const newCode = `
120 | const a = 10
121 | const boo = 10
122 |
123 | if(a === 10) {
124 | console.log('bar')
125 | }
126 | `;
127 |
128 | class Diff extends PureComponent {
129 | highlightSyntax = str => (
130 |
136 | );
137 |
138 | render = () => {
139 | return (
140 |
146 | );
147 | };
148 | }
149 | ```
150 |
151 | ## Text block diff comparison
152 |
153 | Different styles of text block diffing are possible by using the enums corresponding to variou JsDiff methods ([learn more](https://github.com/kpdecker/jsdiff/tree/v4.0.1#api)). The supported methods are as follows.
154 |
155 | ```javascript
156 | enum DiffMethod {
157 | CHARS = 'diffChars',
158 | WORDS = 'diffWords',
159 | WORDS_WITH_SPACE = 'diffWordsWithSpace',
160 | LINES = 'diffLines',
161 | TRIMMED_LINES = 'diffTrimmedLines',
162 | SENTENCES = 'diffSentences',
163 | CSS = 'diffCss',
164 | }
165 | ```
166 |
167 | ```javascript
168 | import React, { PureComponent } from 'react';
169 | import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer';
170 |
171 | const oldCode = `
172 | {
173 | "name": "Original name",
174 | "description": null
175 | }
176 | `;
177 | const newCode = `
178 | {
179 | "name": "My updated name",
180 | "description": "Brand new description",
181 | "status": "running"
182 | }
183 | `;
184 |
185 | class Diff extends PureComponent {
186 | render = () => {
187 | return (
188 |
194 | );
195 | };
196 | }
197 | ```
198 |
199 | ## Overriding Styles
200 |
201 | React Diff Viewer uses [emotion](https://emotion.sh/) for styling. It also offers a simple way to override styles and style variables. You can supply different variables for both light and dark themes. Styles will be common for both themes.
202 |
203 | Below are the default style variables and style object keys.
204 |
205 | ```javascript
206 |
207 | // Default variables and style keys
208 |
209 | const defaultStyles = {
210 | variables: {
211 | light: {
212 | diffViewerBackground: '#fff',
213 | diffViewerColor: '#212529',
214 | addedBackground: '#e6ffed',
215 | addedColor: '#24292e',
216 | removedBackground: '#ffeef0',
217 | removedColor: '#24292e',
218 | wordAddedBackground: '#acf2bd',
219 | wordRemovedBackground: '#fdb8c0',
220 | addedGutterBackground: '#cdffd8',
221 | removedGutterBackground: '#ffdce0',
222 | gutterBackground: '#f7f7f7',
223 | gutterBackgroundDark: '#f3f1f1',
224 | highlightBackground: '#fffbdd',
225 | highlightGutterBackground: '#fff5b1',
226 | codeFoldGutterBackground: '#dbedff',
227 | codeFoldBackground: '#f1f8ff',
228 | emptyLineBackground: '#fafbfc',
229 | gutterColor: '#212529',
230 | addedGutterColor: '#212529',
231 | removedGutterColor: '#212529',
232 | codeFoldContentColor: '#212529',
233 | diffViewerTitleBackground: '#fafbfc',
234 | diffViewerTitleColor: '#212529',
235 | diffViewerTitleBorderColor: '#eee',
236 | },
237 | dark: {
238 | diffViewerBackground: '#2e303c',
239 | diffViewerColor: '#FFF',
240 | addedBackground: '#044B53',
241 | addedColor: 'white',
242 | removedBackground: '#632F34',
243 | removedColor: 'white',
244 | wordAddedBackground: '#055d67',
245 | wordRemovedBackground: '#7d383f',
246 | addedGutterBackground: '#034148',
247 | removedGutterBackground: '#632b30',
248 | gutterBackground: '#2c2f3a',
249 | gutterBackgroundDark: '#262933',
250 | highlightBackground: '#2a3967',
251 | highlightGutterBackground: '#2d4077',
252 | codeFoldGutterBackground: '#21232b',
253 | codeFoldBackground: '#262831',
254 | emptyLineBackground: '#363946',
255 | gutterColor: '#464c67',
256 | addedGutterColor: '#8c8c8c',
257 | removedGutterColor: '#8c8c8c',
258 | codeFoldContentColor: '#555a7b',
259 | diffViewerTitleBackground: '#2f323e',
260 | diffViewerTitleColor: '#555a7b',
261 | diffViewerTitleBorderColor: '#353846',
262 | }
263 | },
264 | diffContainer?: {}, // style object
265 | diffRemoved?: {}, // style object
266 | diffAdded?: {}, // style object
267 | marker?: {}, // style object
268 | emptyGutter?: {}, // style object
269 | highlightedLine?: {}, // style object
270 | lineNumber?: {}, // style object
271 | highlightedGutter?: {}, // style object
272 | contentText?: {}, // style object
273 | gutter?: {}, // style object
274 | line?: {}, // style object
275 | wordDiff?: {}, // style object
276 | wordAdded?: {}, // style object
277 | wordRemoved?: {}, // style object
278 | codeFoldGutter?: {}, // style object
279 | codeFold?: {}, // style object
280 | emptyLine?: {}, // style object
281 | content?: {}, // style object
282 | titleBlock?: {}, // style object
283 | splitView?: {}, // style object
284 | }
285 | ```
286 |
287 | To override any style, just pass the new style object to the `styles` prop. New style will be computed using `Object.assign(default, override)`.
288 |
289 | For keys other than `variables`, the value can either be an object or string interpolation.
290 |
291 | ```javascript
292 | import React, { PureComponent } from 'react';
293 | import ReactDiffViewer from 'react-diff-viewer';
294 |
295 | const oldCode = `
296 | const a = 10
297 | const b = 10
298 | const c = () => console.log('foo')
299 |
300 | if(a > 10) {
301 | console.log('bar')
302 | }
303 |
304 | console.log('done')
305 | `;
306 | const newCode = `
307 | const a = 10
308 | const boo = 10
309 |
310 | if(a === 10) {
311 | console.log('bar')
312 | }
313 | `;
314 |
315 | class Diff extends PureComponent {
316 | highlightSyntax = str => (
317 |
323 | );
324 |
325 | render = () => {
326 | const newStyles = {
327 | variables: {
328 | dark: {
329 | highlightBackground: '#fefed5',
330 | highlightGutterBackground: '#ffcd3c',
331 | },
332 | },
333 | line: {
334 | padding: '10px 2px',
335 | '&:hover': {
336 | background: '#a26ea1',
337 | },
338 | },
339 | };
340 |
341 | return (
342 |
349 | );
350 | };
351 | }
352 | ```
353 |
354 | ## Local Development
355 |
356 | ```bash
357 | yarn install
358 | yarn build # or use yarn build:watch
359 | yarn start:examples
360 | ```
361 |
362 | Check package.json for more build scripts.
363 |
364 | ## License
365 |
366 | MIT
367 |
--------------------------------------------------------------------------------
/enzyme.ts:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme'
2 | import * as Adapter from 'enzyme-adapter-react-16'
3 | configure({ adapter: new Adapter() })
4 |
--------------------------------------------------------------------------------
/examples/src/diff/javascript/new.rjs:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var HtmlWebpackPlugin = require('html-webpack-plugin');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | var WriteFilePlugin = require('write-file-webpack-plugin');
6 | module.exports = {
7 | entry: [
8 | 'webpack/hot/dev-server',
9 | 'webpack-hot-middleware/client?reload=true',
10 | path.join(__dirname, 'app/main.js')
11 | ],
12 | output: {
13 | path: path.join(__dirname, '/dist/'),
14 | filename: '[name].js',
15 | publicPath: '/'
16 | },
17 | module: {
18 | loaders: [
19 | {
20 | test: /\.js?$/,
21 | loader: 'babel',
22 | exclude: /node_modules|lib/,
23 | },
24 | {
25 | test: /\.json?$/,
26 | loader: 'json'
27 | },
28 | {
29 | test: /\.css$/,
30 | loader: 'style!css?modules&localIdentName=[hash:base64:5]'
31 | },
32 | {
33 | test: /\.scss$/,
34 | loaders: [
35 | 'style?sourceMap',
36 | 'css?modules&importLoaders=1',
37 | 'sass?sourceMap'
38 | ],
39 | exclude: /node_modules|lib/
40 | },
41 | ],
42 | },
43 | plugins: [
44 | new WriteFilePlugin(),
45 | new ExtractTextPlugin('app.css', {
46 | allChunks: true
47 | }),
48 | new HtmlWebpackPlugin({
49 | template: 'app/index.tpl.html',
50 | inject: 'body',
51 | filename: 'index.html'
52 | }),
53 | new webpack.optimize.OccurenceOrderPlugin(),
54 | new webpack.HotModuleReplacementPlugin(),
55 | new webpack.NoErrorsPlugin(),
56 | new webpack.DefinePlugin({
57 | 'process.env.NODE_ENV': JSON.stringify('dev')
58 | })
59 | ],
60 | node: {
61 | fs: 'empty'
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/examples/src/diff/javascript/old.rjs:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var HtmlWebpackPlugin = require('html-webpack-plugin');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | var WriteFilePlugin = require('write-file-webpack-plugin');
6 | const devServer = {
7 | contentBase: path.resolve(__dirname, './app'),
8 | outputPath: path.join(__dirname, './dist'),
9 | colors: true,
10 | quiet: false,
11 | noInfo: false,
12 | publicPath: '/',
13 | historyApiFallback: false,
14 | host: '127.0.0.1',
15 | proxy:{
16 | '/graphql': {
17 | target: 'http://localhost:8080'
18 | }
19 | },
20 | port: 3000,
21 | hot: true
22 | };
23 | module.exports = {
24 | devtool: 'eval-source-map',
25 | debug: true,
26 | devServer: devServer,
27 | entry: [
28 | 'webpack/hot/dev-server',
29 | 'webpack-hot-middleware/client?reload=true',
30 | path.join(__dirname, 'app/main.js')
31 | ],
32 | output: {
33 | path: path.join(__dirname, '/dist/'),
34 | filename: '[name].js',
35 | publicPath: devServer.publicPath
36 | },
37 | module: {
38 | loaders: [
39 | {
40 | test: /\.js?$/,
41 | loader: 'babel',
42 | exclude: /node_modules|lib/,
43 | },
44 | {
45 | test: /\.json?$/,
46 | loader: 'json'
47 | },
48 | {
49 | test: /\.css$/,
50 | loader: 'style!css?modules&localIdentName=[name]---[local]---[hash:base64:5]'
51 | },
52 | {
53 | test: /\.scss$/,
54 | loaders: [
55 | 'style?sourceMap',
56 | 'css?modules&importLoaders=1&localIdentName=[name]--[local]',
57 | 'sass?sourceMap'
58 | ],
59 | exclude: /node_modules|lib/
60 | },
61 | ],
62 | },
63 | plugins: [
64 | new WriteFilePlugin(),
65 | new ExtractTextPlugin('app.css', {
66 | allChunks: true
67 | }),
68 | new HtmlWebpackPlugin({
69 | template: 'app/index.tpl.html',
70 | inject: 'body',
71 | filename: 'index.html'
72 | }),
73 | ],
74 | node: {
75 | fs: 'empty'
76 | }
77 | };
78 |
--------------------------------------------------------------------------------
/examples/src/diff/json/new.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@types/classnames": "^2.2.6",
4 | "@types/diff": "^3.5.1",
5 | "@types/react": "^16.4.14",
6 | "@types/react-dom": "^16.0.8",
7 | "@types/webpack": "^4.4.13",
8 | "css-loader": "^1.0.0",
9 | "html-webpack-plugin": "^3.2.0",
10 | "mini-css-extract-plugin": "^0.4.3",
11 | "node-sass": "^4.9.3",
12 | "react": "^16.5.2",
13 | "react-dom": "^16.5.2",
14 | "sass-loader": "^7.1.0",
15 | "ts-loader": "^5.2.1",
16 | "typescript": "^3.1.1",
17 | "webpack": "^4.20.2",
18 | "webpack-cli": "^3.1.1",
19 | "webpack-dev-server": "^3.1.9"
20 | },
21 | "name": "react-diff-viewer",
22 | "version": "1.0.0",
23 | "description": "Text diff viewer for React",
24 | "main": "lib/index.js",
25 | "repository": "git@github.com:praneshr/react-diff-viewer.git",
26 | "author": "Pranesh Ravi",
27 | "license": "BSD",
28 | "private": false,
29 | "scripts": {
30 | "build": "tsc --outDir lib/",
31 | "build:watch": "tsc --outDir lib/ -w",
32 | "start:examples": "webpack-dev-server --open --hot --inline",
33 | "start": "webpack-dev-server --open --hot --inline"
34 | },
35 | "dependencies": {
36 | "classnames": "^2.2.6"
37 | },
38 | "peerDependencies": {
39 | "react": "^16.5.2",
40 | "react-dom": "^16.5.2"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/src/diff/json/old.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@types/classnames": "^2.2.6",
4 | "@types/diff": "^3.5.1",
5 | "@types/react": "^16.4.14",
6 | "@types/react-dom": "^16.0.8",
7 | "@types/webpack": "^4.4.13",
8 | "css-loader": "^1.0.0",
9 | "html-webpack-plugin": "^3.2.0",
10 | "mini-css-extract-plugin": "^0.4.3",
11 | "node-sass": "^4.9.3",
12 | "react": "^16.5.2",
13 | "react-dom": "^16.5.2",
14 | "sass-loader": "^7.1.0",
15 | "ts-loader": "^5.2.1",
16 | "typescript": "^3.1.1",
17 | "webpack": "^4.20.2",
18 | "webpack-cli": "^3.1.1",
19 | "webpack-dev-server": "^3.1.9"
20 | },
21 | "name": "react-diff-viewer",
22 | "version": "1.0.0",
23 | "description": "Text diff viewer for React",
24 | "main": "lib/index.js",
25 | "repository": "git@github.com:praneshr/react-diff-viewer.git",
26 | "author": "Pranesh Ravi",
27 | "license": "MIT",
28 | "private": false,
29 | "scripts": {
30 | "build": "tsc --outDir lib/",
31 | "build:watch": "tsc --outDir lib/ -w",
32 | "start:examples": "webpack-dev-server --open --hot --inline"
33 | },
34 | "dependencies": {
35 | "classnames": "^2.2.6",
36 | "prop-types": "^15.6.2",
37 | "emotion": "^9.2.10",
38 | "diff": "^3.5.0"
39 | },
40 | "peerDependencies": {
41 | "react": "^16.5.2",
42 | "react-dom": "^16.5.2"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/examples/src/diff/xml/new.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | QWZ5671
8 | 39.95
9 |
10 | Red
11 | Burgundy
12 |
13 |
14 | Red
15 | Burgundy
16 |
17 |
18 |
19 | RRX9856
20 | 42.50
21 |
22 | Red
23 | Navy
24 | Burgundy
25 | Black
26 |
27 |
28 | Navy
29 | Black
30 |
31 |
32 | Burgundy
33 | Black
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/examples/src/diff/xml/old.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | QWZ5671
8 | 33
9 |
10 | Red
11 | Burgundy
12 |
13 |
14 |
15 | RRX9856
16 | 42.50
17 |
18 | Red
19 | Navy
20 | Burgundy
21 |
22 |
23 | Red
24 | Navy
25 | Burgundy
26 | Black
27 |
28 |
29 | Navy
30 | Black
31 |
32 |
33 | Burgundy
34 | Black
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/examples/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Diff Viewer
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/examples/src/index.tsx:
--------------------------------------------------------------------------------
1 | require('./style.scss');
2 | import * as React from 'react';
3 | import * as ReactDOM from 'react-dom';
4 |
5 | import ReactDiff, { DiffMethod } from '../../lib/index';
6 |
7 | const oldJs = require('./diff/javascript/old.rjs').default;
8 | const newJs = require('./diff/javascript/new.rjs').default;
9 |
10 | const logo = require('../../logo.png');
11 |
12 | interface ExampleState {
13 | splitView?: boolean;
14 | highlightLine?: string[];
15 | language?: string;
16 | enableSyntaxHighlighting?: boolean;
17 | compareMethod?: DiffMethod;
18 | }
19 |
20 | const P = (window as any).Prism;
21 |
22 | class Example extends React.Component<{}, ExampleState> {
23 | public constructor(props: any) {
24 | super(props);
25 | this.state = {
26 | highlightLine: [],
27 | enableSyntaxHighlighting: true,
28 | };
29 | }
30 |
31 | private onLineNumberClick = (
32 | id: string,
33 | e: React.MouseEvent,
34 | ): void => {
35 | let highlightLine = [id];
36 | if (e.shiftKey && this.state.highlightLine.length === 1) {
37 | const [dir, oldId] = this.state.highlightLine[0].split('-');
38 | const [newDir, newId] = id.split('-');
39 | if (dir === newDir) {
40 | highlightLine = [];
41 | const lowEnd = Math.min(Number(oldId), Number(newId));
42 | const highEnd = Math.max(Number(oldId), Number(newId));
43 | for (let i = lowEnd; i <= highEnd; i++) {
44 | highlightLine.push(`${dir}-${i}`);
45 | }
46 | }
47 | }
48 | this.setState({
49 | highlightLine,
50 | });
51 | };
52 |
53 | private syntaxHighlight = (str: string): any => {
54 | if (!str) return;
55 | const language = P.highlight(str, P.languages.javascript);
56 | return ;
57 | };
58 |
59 | public render(): JSX.Element {
60 |
61 | return (
62 |
63 |
64 |
65 |
66 |

67 |
68 |
69 | A simple and beautiful text diff viewer made with{' '}
70 |
71 | Diff{' '}
72 |
73 | and{' '}
74 |
75 | React.{' '}
76 |
77 | Featuring split view, inline view, word diff, line highlight and more.
78 |
79 |
86 |
87 |
88 |
99 |
100 |
106 |
107 | );
108 | }
109 | }
110 |
111 | ReactDOM.render(, document.getElementById('app'));
112 |
--------------------------------------------------------------------------------
/examples/src/style.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * GHColors theme by Avi Aryan (http://aviaryan.in)
3 | * Inspired by Github syntax coloring
4 | */
5 |
6 | code[class*="language-"],
7 | pre[class*="language-"] {
8 | color: #393A34;
9 | font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
10 | direction: ltr;
11 | text-align: left;
12 | white-space: pre;
13 | word-spacing: normal;
14 | word-break: normal;
15 | font-size: 0.95em;
16 | line-height: 1.2em;
17 | -moz-tab-size: 4;
18 | -o-tab-size: 4;
19 | tab-size: 4;
20 | -webkit-hyphens: none;
21 | -moz-hyphens: none;
22 | -ms-hyphens: none;
23 | hyphens: none;
24 | }
25 |
26 | pre[class*="language-"]::-moz-selection,
27 | pre[class*="language-"] ::-moz-selection,
28 | code[class*="language-"]::-moz-selection,
29 | code[class*="language-"] ::-moz-selection {
30 | background: #b3d4fc;
31 | }
32 |
33 | pre[class*="language-"]::selection,
34 | pre[class*="language-"] ::selection,
35 | code[class*="language-"]::selection,
36 | code[class*="language-"] ::selection {
37 | background: #b3d4fc;
38 | }
39 |
40 | /* Code blocks */
41 |
42 | pre[class*="language-"] {
43 | padding: 1em;
44 | margin: .5em 0;
45 | overflow: auto;
46 | border: 1px solid #dddddd;
47 | background-color: white;
48 | }
49 |
50 | :not(pre)>code[class*="language-"],
51 | pre[class*="language-"] {}
52 |
53 | /* Inline code */
54 |
55 | :not(pre)>code[class*="language-"] {
56 | padding: .2em;
57 | padding-top: 1px;
58 | padding-bottom: 1px;
59 | background: #f8f8f8;
60 | border: 1px solid #dddddd;
61 | }
62 |
63 | .token.comment,
64 | .token.prolog,
65 | .token.doctype,
66 | .token.cdata {
67 | color: #999988;
68 | font-style: italic;
69 | }
70 |
71 | .token.namespace {
72 | opacity: .7;
73 | }
74 |
75 | .token.string,
76 | .token.attr-value {
77 | color: #e3116c;
78 | }
79 |
80 | .token.punctuation,
81 | .token.operator {
82 | color: #3bf5d4;
83 | /* no highlight */
84 | }
85 |
86 | .token.entity,
87 | .token.url,
88 | .token.symbol,
89 | .token.number,
90 | .token.boolean,
91 | .token.variable,
92 | .token.constant,
93 | .token.property,
94 | .token.regex,
95 | .token.inserted {
96 | color: #36acaa;
97 | }
98 |
99 | .token.atrule,
100 | .token.keyword,
101 | .token.attr-name,
102 | .language-autohotkey .token.selector {
103 | color: #00a4db;
104 | }
105 |
106 | .token.function,
107 | .token.deleted,
108 | .language-autohotkey .token.tag {
109 | color: #069071;
110 | }
111 |
112 | .token.tag,
113 | .token.selector,
114 | .language-autohotkey .token.keyword {
115 | color: #ff9292;
116 | }
117 |
118 | .token.important,
119 | .token.function,
120 | .token.bold {
121 | font-weight: bold;
122 | }
123 |
124 | .token.italic {
125 | font-style: italic;
126 | }
127 |
128 | @import url('https://fonts.googleapis.com/css?family=Poppins:400,500,600,700,800');
129 |
130 | body {
131 | font-family: 'Poppins', sans-serif;
132 | background-color: #262831;
133 | }
134 |
135 | .react-diff-viewer-example {
136 | a {
137 | color: #125dec;
138 | text-decoration: none;
139 | }
140 |
141 | .banner {
142 | padding: 70px 15px;
143 | text-align: center;
144 |
145 | .img-container {
146 | text-align: center;
147 | margin: 100px auto 60px;
148 | max-width: 700px;
149 |
150 | img {
151 | width: 100%;
152 |
153 | &.mobile {
154 | display: none;
155 | }
156 | }
157 | }
158 |
159 | .cta {
160 | margin-top: 60px;
161 |
162 | a {
163 | &:last-child {
164 | button {
165 | margin-right: 0;
166 | }
167 | }
168 | }
169 |
170 | button {
171 | font-size: 14px;
172 | background: #125dec;
173 | border: none;
174 | cursor: pointer;
175 |
176 | &:focus {
177 | background: #125dec;
178 | }
179 | }
180 | }
181 |
182 | p {
183 | max-width: 700px;
184 | font-size: 18px;
185 | margin: 0 auto;
186 | color: #FFF;
187 | }
188 | }
189 |
190 | .controls {
191 | margin: 50px 15px 15px;
192 | display: flex;
193 | align-items: center;
194 | justify-content: space-between;
195 |
196 | label {
197 | margin-left: 30px;
198 | }
199 |
200 | select {
201 | background-color: transparent;
202 | padding: 5px 15px;
203 | border-radius: 4px;
204 | border: 2px solid #ddd;
205 | }
206 | }
207 |
208 | .radial {
209 | background: linear-gradient(180deg, #363946 0%, #262931 100%);
210 | position: absolute;
211 | width: 100%;
212 | height: 600px;
213 | left: 0;
214 | z-index: -1;
215 | }
216 |
217 | .diff-viewer {
218 | max-width: 90%;
219 | margin: 0 auto;
220 | border-radius: 8px;
221 | overflow-x: auto;
222 | overflow-y: hidden;
223 | white-space: nowrap;
224 | box-shadow: 0 0 30px #1c1e25;
225 |
226 | a {
227 | color: inherit;
228 | }
229 | }
230 |
231 | footer {
232 | margin: 40px 0;
233 | color: #FFF;
234 | text-align: center;
235 | }
236 | }
237 |
238 | @media (max-width: 1023px) {
239 | .react-diff-viewer-example {
240 | .banner {
241 | .img-container {
242 | img {
243 | width: 80%;
244 | }
245 | }
246 | }
247 |
248 | p {
249 | font-size: 16px;
250 | }
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/logo-standalone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tp1845/react-diff-viewer/d1f2619c434c4d3affb3b8560c0f9d276a23b127/logo-standalone.png
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tp1845/react-diff-viewer/d1f2619c434c4d3affb3b8560c0f9d276a23b127/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-diff-viewer",
3 | "version": "3.1.1",
4 | "private": false,
5 | "description": "A simple and beautiful text diff viewer component made with diff and React",
6 | "keywords": [
7 | "review",
8 | "code-review",
9 | "diff",
10 | "diff-viewer",
11 | "github",
12 | "react",
13 | "react-component",
14 | "ui"
15 | ],
16 | "repository": "git@github.com:praneshr/react-diff-viewer.git",
17 | "license": "MIT",
18 | "author": "Pranesh Ravi",
19 | "main": "lib/index",
20 | "typings": "lib/index",
21 | "scripts": {
22 | "build": "tsc --outDir lib/",
23 | "build:examples": "webpack --progress --colors",
24 | "build:watch": "tsc --outDir lib/ -w",
25 | "publish:examples": "NODE_ENV=production yarn build:examples && gh-pages -d examples/dist -r $GITHUB_REPO_URL",
26 | "publish:examples:local": "NODE_ENV=production yarn build:examples && gh-pages -d examples/dist",
27 | "start:examples": "webpack-dev-server --open --hot --inline",
28 | "test": "mocha --require ts-node/register --require enzyme.ts ./test/**",
29 | "test:watch": "mocha --require ts-node/register --require enzyme.ts --watch-extensions ts,tsx --watch ./test/**"
30 | },
31 | "dependencies": {
32 | "classnames": "^2.2.6",
33 | "create-emotion": "^10.0.14",
34 | "diff": "^4.0.1",
35 | "emotion": "^10.0.14",
36 | "memoize-one": "^5.0.4",
37 | "prop-types": "^15.6.2"
38 | },
39 | "devDependencies": {
40 | "@types/classnames": "^2.2.6",
41 | "@types/diff": "^4.0.2",
42 | "@types/enzyme": "^3.1.14",
43 | "@types/enzyme-adapter-react-16": "^1.0.3",
44 | "@types/expect": "^1.20.3",
45 | "@types/memoize-one": "^4.1.1",
46 | "@types/mocha": "^5.2.5",
47 | "@types/node": "^12.0.12",
48 | "@types/react": "^16.4.14",
49 | "@types/react-dom": "^16.0.8",
50 | "@types/webpack": "^4.4.13",
51 | "@typescript-eslint/eslint-plugin": "^1.11.0",
52 | "@typescript-eslint/parser": "^1.11.0",
53 | "css-loader": "^3.0.0",
54 | "enzyme": "^3.7.0",
55 | "enzyme-adapter-react-16": "^1.6.0",
56 | "eslint": "6.0.1",
57 | "eslint-config-airbnb": "17.1.1",
58 | "eslint-plugin-import": "^2.18.0",
59 | "eslint-plugin-jsx-a11y": "^6.2.3",
60 | "eslint-plugin-react": "^7.14.2",
61 | "expect": "^24.8.0",
62 | "favicons-webpack-plugin": "^0.0.9",
63 | "file-loader": "^4.0.0",
64 | "gh-pages": "^2.0.1",
65 | "html-webpack-plugin": "^3.2.0",
66 | "mini-css-extract-plugin": "^0.7.0",
67 | "mocha": "^6.1.4",
68 | "node-sass": "^7.0.0",
69 | "raw-loader": "^3.0.0",
70 | "react": "^16.5.2",
71 | "react-dom": "^16.5.2",
72 | "sass-loader": "^7.1.0",
73 | "spy": "^1.0.0",
74 | "ts-loader": "^6.0.4",
75 | "ts-node": "^8.3.0",
76 | "typescript": "^3.5.2",
77 | "webpack": "^4.20.2",
78 | "webpack-cli": "^3.1.1",
79 | "webpack-dev-server": "^3.1.9"
80 | },
81 | "peerDependencies": {
82 | "react": "^15.3.0 || ^16.0.0",
83 | "react-dom": "^15.3.0 || ^16.0.0"
84 | },
85 | "engines": {
86 | "node": ">= 8"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/publish-examples.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ -n "$TRAVIS_TAG" ]; then
4 | echo "Publishing examples to Github pages...."
5 | git config --global user.email "travis@travis-ci.org"
6 | git config --global user.name "Travis CI"
7 | yarn publish:examples
8 | else
9 | echo "Skipping examples deployment..."
10 | fi
11 |
--------------------------------------------------------------------------------
/src/compute-lines.ts:
--------------------------------------------------------------------------------
1 | import * as diff from 'diff';
2 |
3 | const jsDiff: { [key: string]: any } = diff;
4 |
5 | export enum DiffType {
6 | DEFAULT = 0,
7 | ADDED = 1,
8 | REMOVED = 2,
9 | }
10 |
11 | // See https://github.com/kpdecker/jsdiff/tree/v4.0.1#api for more info on the below JsDiff methods
12 | export enum DiffMethod {
13 | CHARS = 'diffChars',
14 | WORDS = 'diffWords',
15 | WORDS_WITH_SPACE = 'diffWordsWithSpace',
16 | LINES = 'diffLines',
17 | TRIMMED_LINES = 'diffTrimmedLines',
18 | SENTENCES = 'diffSentences',
19 | CSS = 'diffCss',
20 | }
21 |
22 | export interface DiffInformation {
23 | value?: string | DiffInformation[];
24 | lineNumber?: number;
25 | type?: DiffType;
26 | }
27 |
28 | export interface LineInformation {
29 | left?: DiffInformation;
30 | right?: DiffInformation;
31 | }
32 |
33 | export interface ComputedLineInformation {
34 | lineInformation: LineInformation[];
35 | diffLines: number[];
36 | }
37 |
38 | export interface ComputedDiffInformation {
39 | left?: DiffInformation[];
40 | right?: DiffInformation[];
41 | }
42 |
43 | // See https://github.com/kpdecker/jsdiff/tree/v4.0.1#change-objects for more info on JsDiff
44 | // Change Objects
45 | export interface JsDiffChangeObject {
46 | added?: boolean;
47 | removed?: boolean;
48 | value?: string;
49 | }
50 |
51 | /**
52 | * Splits diff text by new line and computes final list of diff lines based on
53 | * conditions.
54 | *
55 | * @param value Diff text from the js diff module.
56 | */
57 | const constructLines = (value: string): string[] => {
58 | const lines = value.split('\n');
59 | const isAllEmpty = lines.every((val): boolean => !val);
60 | if (isAllEmpty) {
61 | // This is to avoid added an extra new line in the UI.
62 | if (lines.length === 2) {
63 | return [];
64 | }
65 | lines.pop();
66 | return lines;
67 | }
68 |
69 | const lastLine = lines[lines.length - 1];
70 | const firstLine = lines[0];
71 | // Remove the first and last element if they are new line character. This is
72 | // to avoid addition of extra new line in the UI.
73 | if (!lastLine) {
74 | lines.pop();
75 | }
76 | if (!firstLine) {
77 | lines.shift();
78 | }
79 | return lines;
80 | };
81 |
82 | /**
83 | * Computes word diff information in the line.
84 | * [TODO]: Consider adding options argument for JsDiff text block comparison
85 | *
86 | * @param oldValue Old word in the line.
87 | * @param newValue New word in the line.
88 | * @param compareMethod JsDiff text diff method from https://github.com/kpdecker/jsdiff/tree/v4.0.1#api
89 | */
90 | const computeDiff = (
91 | oldValue: string,
92 | newValue: string,
93 | compareMethod: string = DiffMethod.CHARS,
94 | ): ComputedDiffInformation => {
95 | const diffArray: JsDiffChangeObject[] = jsDiff[compareMethod](
96 | oldValue,
97 | newValue,
98 | );
99 | const computedDiff: ComputedDiffInformation = {
100 | left: [],
101 | right: [],
102 | };
103 | diffArray.forEach(
104 | ({ added, removed, value }): DiffInformation => {
105 | const diffInformation: DiffInformation = {};
106 | if (added) {
107 | diffInformation.type = DiffType.ADDED;
108 | diffInformation.value = value;
109 | computedDiff.right.push(diffInformation);
110 | }
111 | if (removed) {
112 | diffInformation.type = DiffType.REMOVED;
113 | diffInformation.value = value;
114 | computedDiff.left.push(diffInformation);
115 | }
116 | if (!removed && !added) {
117 | diffInformation.type = DiffType.DEFAULT;
118 | diffInformation.value = value;
119 | computedDiff.right.push(diffInformation);
120 | computedDiff.left.push(diffInformation);
121 | }
122 | return diffInformation;
123 | },
124 | );
125 | return computedDiff;
126 | };
127 |
128 | /**
129 | * [TODO]: Think about moving common left and right value assignment to a
130 | * common place. Better readability?
131 | *
132 | * Computes line wise information based in the js diff information passed. Each
133 | * line contains information about left and right section. Left side denotes
134 | * deletion and right side denotes addition.
135 | *
136 | * @param oldString Old string to compare.
137 | * @param newString New string to compare with old string.
138 | * @param disableWordDiff Flag to enable/disable word diff.
139 | * @param compareMethod JsDiff text diff method from https://github.com/kpdecker/jsdiff/tree/v4.0.1#api
140 | * @param linesOffset line number to start counting from
141 | */
142 | const computeLineInformation = (
143 | oldString: string,
144 | newString: string,
145 | disableWordDiff: boolean = false,
146 | compareMethod: string = DiffMethod.CHARS,
147 | linesOffset: number = 0,
148 | ): ComputedLineInformation => {
149 | const diffArray = diff.diffLines(
150 | oldString.trimRight(),
151 | newString.trimRight(),
152 | {
153 | newlineIsToken: true,
154 | ignoreWhitespace: false,
155 | ignoreCase: false,
156 | },
157 | );
158 | let rightLineNumber = linesOffset;
159 | let leftLineNumber = linesOffset;
160 | let lineInformation: LineInformation[] = [];
161 | let counter = 0;
162 | const diffLines: number[] = [];
163 | const ignoreDiffIndexes: string[] = [];
164 | const getLineInformation = (
165 | value: string,
166 | diffIndex: number,
167 | added?: boolean,
168 | removed?: boolean,
169 | evaluateOnlyFirstLine?: boolean,
170 | ): LineInformation[] => {
171 | const lines = constructLines(value);
172 |
173 | return lines
174 | .map(
175 | (line: string, lineIndex): LineInformation => {
176 | const left: DiffInformation = {};
177 | const right: DiffInformation = {};
178 | if (
179 | ignoreDiffIndexes.includes(`${diffIndex}-${lineIndex}`) ||
180 | (evaluateOnlyFirstLine && lineIndex !== 0)
181 | ) {
182 | return undefined;
183 | }
184 | if (added || removed) {
185 | if (!diffLines.includes(counter)) {
186 | diffLines.push(counter);
187 | }
188 | if (removed) {
189 | leftLineNumber += 1;
190 | left.lineNumber = leftLineNumber;
191 | left.type = DiffType.REMOVED;
192 | left.value = line || ' ';
193 | // When the current line is of type REMOVED, check the next item in
194 | // the diff array whether it is of type ADDED. If true, the current
195 | // diff will be marked as both REMOVED and ADDED. Meaning, the
196 | // current line is a modification.
197 | const nextDiff = diffArray[diffIndex + 1];
198 | if (nextDiff && nextDiff.added) {
199 | const nextDiffLines = constructLines(nextDiff.value)[lineIndex];
200 | if (nextDiffLines) {
201 | const {
202 | value: rightValue,
203 | lineNumber,
204 | type,
205 | } = getLineInformation(
206 | nextDiff.value,
207 | diffIndex,
208 | true,
209 | false,
210 | true,
211 | )[0].right;
212 | // When identified as modification, push the next diff to ignore
213 | // list as the next value will be added in this line computation as
214 | // right and left values.
215 | ignoreDiffIndexes.push(`${diffIndex + 1}-${lineIndex}`);
216 | right.lineNumber = lineNumber;
217 | right.type = type;
218 | // Do word level diff and assign the corresponding values to the
219 | // left and right diff information object.
220 | if (disableWordDiff) {
221 | right.value = rightValue;
222 | } else {
223 | const computedDiff = computeDiff(
224 | line,
225 | rightValue as string,
226 | compareMethod,
227 | );
228 | right.value = computedDiff.right;
229 | left.value = computedDiff.left;
230 | }
231 | }
232 | }
233 | } else {
234 | rightLineNumber += 1;
235 | right.lineNumber = rightLineNumber;
236 | right.type = DiffType.ADDED;
237 | right.value = line;
238 | }
239 | } else {
240 | leftLineNumber += 1;
241 | rightLineNumber += 1;
242 |
243 | left.lineNumber = leftLineNumber;
244 | left.type = DiffType.DEFAULT;
245 | left.value = line;
246 | right.lineNumber = rightLineNumber;
247 | right.type = DiffType.DEFAULT;
248 | right.value = line;
249 | }
250 |
251 | counter += 1;
252 | return { right, left };
253 | },
254 | )
255 | .filter(Boolean);
256 | };
257 |
258 | diffArray.forEach(({ added, removed, value }: diff.Change, index): void => {
259 | lineInformation = [
260 | ...lineInformation,
261 | ...getLineInformation(value, index, added, removed),
262 | ];
263 | });
264 |
265 | return {
266 | lineInformation,
267 | diffLines,
268 | };
269 | };
270 |
271 | export { computeLineInformation };
272 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as PropTypes from 'prop-types';
3 | import cn from 'classnames';
4 |
5 | import {
6 | computeLineInformation,
7 | LineInformation,
8 | DiffInformation,
9 | DiffType,
10 | DiffMethod,
11 | } from './compute-lines';
12 | import computeStyles, {
13 | ReactDiffViewerStylesOverride,
14 | ReactDiffViewerStyles,
15 | } from './styles';
16 |
17 | // eslint-disable-next-line @typescript-eslint/no-var-requires
18 | const m = require('memoize-one');
19 |
20 | const memoize = m.default || m;
21 |
22 | export enum LineNumberPrefix {
23 | LEFT = 'L',
24 | RIGHT = 'R',
25 | }
26 |
27 | export interface ReactDiffViewerProps {
28 | // Old value to compare.
29 | oldValue: string;
30 | // New value to compare.
31 | newValue: string;
32 | // Enable/Disable split view.
33 | splitView?: boolean;
34 | // Set line Offset
35 | linesOffset?: number;
36 | // Enable/Disable word diff.
37 | disableWordDiff?: boolean;
38 | // JsDiff text diff method from https://github.com/kpdecker/jsdiff/tree/v4.0.1#api
39 | compareMethod?: DiffMethod;
40 | // Number of unmodified lines surrounding each line diff.
41 | extraLinesSurroundingDiff?: number;
42 | // Show/hide line number.
43 | hideLineNumbers?: boolean;
44 | // Show only diff between the two values.
45 | showDiffOnly?: boolean;
46 | // Render prop to format final string before displaying them in the UI.
47 | renderContent?: (source: string) => JSX.Element;
48 | // Render prop to format code fold message.
49 | codeFoldMessageRenderer?: (
50 | totalFoldedLines: number,
51 | leftStartLineNumber: number,
52 | rightStartLineNumber: number,
53 | ) => JSX.Element;
54 | // Event handler for line number click.
55 | onLineNumberClick?: (
56 | lineId: string,
57 | event: React.MouseEvent,
58 | ) => void;
59 | // Array of line ids to highlight lines.
60 | highlightLines?: string[];
61 | // Style overrides.
62 | styles?: ReactDiffViewerStylesOverride;
63 | // Use dark theme.
64 | useDarkTheme?: boolean;
65 | // Title for left column
66 | leftTitle?: string | JSX.Element;
67 | // Title for left column
68 | rightTitle?: string | JSX.Element;
69 | }
70 |
71 | export interface ReactDiffViewerState {
72 | // Array holding the expanded code folding.
73 | expandedBlocks?: number[];
74 | }
75 |
76 | class DiffViewer extends React.Component<
77 | ReactDiffViewerProps,
78 | ReactDiffViewerState
79 | > {
80 | private styles: ReactDiffViewerStyles;
81 |
82 | public static defaultProps: ReactDiffViewerProps = {
83 | oldValue: '',
84 | newValue: '',
85 | splitView: true,
86 | highlightLines: [],
87 | disableWordDiff: false,
88 | compareMethod: DiffMethod.CHARS,
89 | styles: {},
90 | hideLineNumbers: false,
91 | extraLinesSurroundingDiff: 3,
92 | showDiffOnly: true,
93 | useDarkTheme: false,
94 | linesOffset: 0,
95 | };
96 |
97 | public static propTypes = {
98 | oldValue: PropTypes.string.isRequired,
99 | newValue: PropTypes.string.isRequired,
100 | splitView: PropTypes.bool,
101 | disableWordDiff: PropTypes.bool,
102 | compareMethod: PropTypes.oneOf(Object.values(DiffMethod)),
103 | renderContent: PropTypes.func,
104 | onLineNumberClick: PropTypes.func,
105 | extraLinesSurroundingDiff: PropTypes.number,
106 | styles: PropTypes.object,
107 | hideLineNumbers: PropTypes.bool,
108 | showDiffOnly: PropTypes.bool,
109 | highlightLines: PropTypes.arrayOf(PropTypes.string),
110 | leftTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
111 | rightTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
112 | linesOffset: PropTypes.number,
113 | };
114 |
115 | public constructor(props: ReactDiffViewerProps) {
116 | super(props);
117 |
118 | this.state = {
119 | expandedBlocks: [],
120 | };
121 | }
122 |
123 | /**
124 | * Resets code block expand to the initial stage. Will be exposed to the parent component via
125 | * refs.
126 | */
127 | public resetCodeBlocks = (): boolean => {
128 | if (this.state.expandedBlocks.length > 0) {
129 | this.setState({
130 | expandedBlocks: [],
131 | });
132 | return true;
133 | }
134 | return false;
135 | };
136 |
137 | /**
138 | * Pushes the target expanded code block to the state. During the re-render,
139 | * this value is used to expand/fold unmodified code.
140 | */
141 | private onBlockExpand = (id: number): void => {
142 | const prevState = this.state.expandedBlocks.slice();
143 | prevState.push(id);
144 |
145 | this.setState({
146 | expandedBlocks: prevState,
147 | });
148 | };
149 |
150 | /**
151 | * Computes final styles for the diff viewer. It combines the default styles with the user
152 | * supplied overrides. The computed styles are cached with performance in mind.
153 | *
154 | * @param styles User supplied style overrides.
155 | */
156 | private computeStyles: (
157 | styles: ReactDiffViewerStylesOverride,
158 | useDarkTheme: boolean,
159 | ) => ReactDiffViewerStyles = memoize(computeStyles);
160 |
161 | /**
162 | * Returns a function with clicked line number in the closure. Returns an no-op function when no
163 | * onLineNumberClick handler is supplied.
164 | *
165 | * @param id Line id of a line.
166 | */
167 | private onLineNumberClickProxy = (id: string): any => {
168 | if (this.props.onLineNumberClick) {
169 | return (e: any): void => this.props.onLineNumberClick(id, e);
170 | }
171 | return (): void => {};
172 | };
173 |
174 | /**
175 | * Maps over the word diff and constructs the required React elements to show word diff.
176 | *
177 | * @param diffArray Word diff information derived from line information.
178 | * @param renderer Optional renderer to format diff words. Useful for syntax highlighting.
179 | */
180 | private renderWordDiff = (
181 | diffArray: DiffInformation[],
182 | renderer?: (chunk: string) => JSX.Element,
183 | ): JSX.Element[] => {
184 | return diffArray.map(
185 | (wordDiff, i): JSX.Element => {
186 | return (
187 |
193 | {renderer ? renderer(wordDiff.value as string) : wordDiff.value}
194 |
195 | );
196 | },
197 | );
198 | };
199 |
200 | /**
201 | * Maps over the line diff and constructs the required react elements to show line diff. It calls
202 | * renderWordDiff when encountering word diff. This takes care of both inline and split view line
203 | * renders.
204 | *
205 | * @param lineNumber Line number of the current line.
206 | * @param type Type of diff of the current line.
207 | * @param prefix Unique id to prefix with the line numbers.
208 | * @param value Content of the line. It can be a string or a word diff array.
209 | * @param additionalLineNumber Additional line number to be shown. Useful for rendering inline
210 | * diff view. Right line number will be passed as additionalLineNumber.
211 | * @param additionalPrefix Similar to prefix but for additional line number.
212 | */
213 | private renderLine = (
214 | lineNumber: number,
215 | type: DiffType,
216 | prefix: LineNumberPrefix,
217 | value: string | DiffInformation[],
218 | additionalLineNumber?: number,
219 | additionalPrefix?: LineNumberPrefix,
220 | ): JSX.Element => {
221 | const lineNumberTemplate = `${prefix}-${lineNumber}`;
222 | const additionalLineNumberTemplate = `${additionalPrefix}-${additionalLineNumber}`;
223 | const highlightLine =
224 | this.props.highlightLines.includes(lineNumberTemplate) ||
225 | this.props.highlightLines.includes(additionalLineNumberTemplate);
226 | const added = type === DiffType.ADDED;
227 | const removed = type === DiffType.REMOVED;
228 | let content;
229 | if (Array.isArray(value)) {
230 | content = this.renderWordDiff(value, this.props.renderContent);
231 | } else if (this.props.renderContent) {
232 | content = this.props.renderContent(value);
233 | } else {
234 | content = value;
235 | }
236 |
237 | return (
238 |
239 | {!this.props.hideLineNumbers && (
240 |
250 | {lineNumber}
251 | |
252 | )}
253 | {!this.props.splitView && !this.props.hideLineNumbers && (
254 |
265 | {additionalLineNumber}
266 | |
267 | )}
268 |
275 |
276 | {added && '+'}
277 | {removed && '-'}
278 |
279 | |
280 |
287 | {content}
288 | |
289 |
290 | );
291 | };
292 |
293 | /**
294 | * Generates lines for split view.
295 | *
296 | * @param obj Line diff information.
297 | * @param obj.left Life diff information for the left pane of the split view.
298 | * @param obj.right Life diff information for the right pane of the split view.
299 | * @param index React key for the lines.
300 | */
301 | private renderSplitView = (
302 | { left, right }: LineInformation,
303 | index: number,
304 | ): JSX.Element => {
305 | return (
306 |
307 | {this.renderLine(
308 | left.lineNumber,
309 | left.type,
310 | LineNumberPrefix.LEFT,
311 | left.value,
312 | )}
313 | {this.renderLine(
314 | right.lineNumber,
315 | right.type,
316 | LineNumberPrefix.RIGHT,
317 | right.value,
318 | )}
319 |
320 | );
321 | };
322 |
323 | /**
324 | * Generates lines for inline view.
325 | *
326 | * @param obj Line diff information.
327 | * @param obj.left Life diff information for the added section of the inline view.
328 | * @param obj.right Life diff information for the removed section of the inline view.
329 | * @param index React key for the lines.
330 | */
331 | public renderInlineView = (
332 | { left, right }: LineInformation,
333 | index: number,
334 | ): JSX.Element => {
335 | let content;
336 | if (left.type === DiffType.REMOVED && right.type === DiffType.ADDED) {
337 | return (
338 |
339 |
340 | {this.renderLine(
341 | left.lineNumber,
342 | left.type,
343 | LineNumberPrefix.LEFT,
344 | left.value,
345 | null,
346 | )}
347 |
348 |
349 | {this.renderLine(
350 | null,
351 | right.type,
352 | LineNumberPrefix.RIGHT,
353 | right.value,
354 | right.lineNumber,
355 | )}
356 |
357 |
358 | );
359 | }
360 | if (left.type === DiffType.REMOVED) {
361 | content = this.renderLine(
362 | left.lineNumber,
363 | left.type,
364 | LineNumberPrefix.LEFT,
365 | left.value,
366 | null,
367 | );
368 | }
369 | if (left.type === DiffType.DEFAULT) {
370 | content = this.renderLine(
371 | left.lineNumber,
372 | left.type,
373 | LineNumberPrefix.LEFT,
374 | left.value,
375 | right.lineNumber,
376 | LineNumberPrefix.RIGHT,
377 | );
378 | }
379 | if (right.type === DiffType.ADDED) {
380 | content = this.renderLine(
381 | null,
382 | right.type,
383 | LineNumberPrefix.RIGHT,
384 | right.value,
385 | right.lineNumber,
386 | );
387 | }
388 |
389 | return (
390 |
391 | {content}
392 |
393 | );
394 | };
395 |
396 | /**
397 | * Returns a function with clicked block number in the closure.
398 | *
399 | * @param id Cold fold block id.
400 | */
401 | private onBlockClickProxy = (id: number): any => (): void =>
402 | this.onBlockExpand(id);
403 |
404 | /**
405 | * Generates cold fold block. It also uses the custom message renderer when available to show
406 | * cold fold messages.
407 | *
408 | * @param num Number of skipped lines between two blocks.
409 | * @param blockNumber Code fold block id.
410 | * @param leftBlockLineNumber First left line number after the current code fold block.
411 | * @param rightBlockLineNumber First right line number after the current code fold block.
412 | */
413 | private renderSkippedLineIndicator = (
414 | num: number,
415 | blockNumber: number,
416 | leftBlockLineNumber: number,
417 | rightBlockLineNumber: number,
418 | ): JSX.Element => {
419 | const { hideLineNumbers, splitView } = this.props;
420 | const message = this.props.codeFoldMessageRenderer ? (
421 | this.props.codeFoldMessageRenderer(
422 | num,
423 | leftBlockLineNumber,
424 | rightBlockLineNumber,
425 | )
426 | ) : (
427 | Expand {num} lines ...
428 | );
429 | const content = (
430 |
431 |
432 | {message}
433 |
434 | |
435 | );
436 | const isUnifiedViewWithoutLineNumbers = !splitView && !hideLineNumbers;
437 | return (
438 |
441 | {!hideLineNumbers && | }
442 | |
447 |
448 | {/* Swap columns only for unified view without line numbers */}
449 | {isUnifiedViewWithoutLineNumbers ? (
450 |
451 | |
452 | {content}
453 |
454 | ) : (
455 |
456 | {content}
457 | |
458 |
459 | )}
460 |
461 | |
462 | |
463 |
464 | );
465 | };
466 |
467 | /**
468 | * Generates the entire diff view.
469 | */
470 | private renderDiff = (): JSX.Element[] => {
471 | const {
472 | oldValue,
473 | newValue,
474 | splitView,
475 | disableWordDiff,
476 | compareMethod,
477 | linesOffset,
478 | } = this.props;
479 | const { lineInformation, diffLines } = computeLineInformation(
480 | oldValue,
481 | newValue,
482 | disableWordDiff,
483 | compareMethod,
484 | linesOffset,
485 | );
486 | const extraLines =
487 | this.props.extraLinesSurroundingDiff < 0
488 | ? 0
489 | : this.props.extraLinesSurroundingDiff;
490 | let skippedLines: number[] = [];
491 | return lineInformation.map(
492 | (line: LineInformation, i: number): JSX.Element => {
493 | const diffBlockStart = diffLines[0];
494 | const currentPosition = diffBlockStart - i;
495 | if (this.props.showDiffOnly) {
496 | if (currentPosition === -extraLines) {
497 | skippedLines = [];
498 | diffLines.shift();
499 | }
500 | if (
501 | line.left.type === DiffType.DEFAULT &&
502 | (currentPosition > extraLines ||
503 | typeof diffBlockStart === 'undefined') &&
504 | !this.state.expandedBlocks.includes(diffBlockStart)
505 | ) {
506 | skippedLines.push(i + 1);
507 | if (i === lineInformation.length - 1 && skippedLines.length > 1) {
508 | return this.renderSkippedLineIndicator(
509 | skippedLines.length,
510 | diffBlockStart,
511 | line.left.lineNumber,
512 | line.right.lineNumber,
513 | );
514 | }
515 | return null;
516 | }
517 | }
518 |
519 | const diffNodes = splitView
520 | ? this.renderSplitView(line, i)
521 | : this.renderInlineView(line, i);
522 |
523 | if (currentPosition === extraLines && skippedLines.length > 0) {
524 | const { length } = skippedLines;
525 | skippedLines = [];
526 | return (
527 |
528 | {this.renderSkippedLineIndicator(
529 | length,
530 | diffBlockStart,
531 | line.left.lineNumber,
532 | line.right.lineNumber,
533 | )}
534 | {diffNodes}
535 |
536 | );
537 | }
538 | return diffNodes;
539 | },
540 | );
541 | };
542 |
543 | public render = (): JSX.Element => {
544 | const {
545 | oldValue,
546 | newValue,
547 | useDarkTheme,
548 | leftTitle,
549 | rightTitle,
550 | splitView,
551 | hideLineNumbers,
552 | } = this.props;
553 |
554 | if (typeof oldValue !== 'string' || typeof newValue !== 'string') {
555 | throw Error('"oldValue" and "newValue" should be strings');
556 | }
557 |
558 | this.styles = this.computeStyles(this.props.styles, useDarkTheme);
559 | const nodes = this.renderDiff();
560 | const colSpanOnSplitView = hideLineNumbers ? 2 : 3;
561 | const colSpanOnInlineView = hideLineNumbers ? 2 : 4;
562 |
563 | const title = (leftTitle || rightTitle) && (
564 |
565 |
568 | {leftTitle}
569 | |
570 | {splitView && (
571 |
572 | {rightTitle}
573 | |
574 | )}
575 |
576 | );
577 |
578 | return (
579 |
583 |
584 | {title}
585 | {nodes}
586 |
587 |
588 | );
589 | };
590 | }
591 |
592 | export default DiffViewer;
593 | export { ReactDiffViewerStylesOverride, DiffMethod };
594 |
--------------------------------------------------------------------------------
/src/styles.ts:
--------------------------------------------------------------------------------
1 | import { css, cx } from 'emotion';
2 | import { Interpolation } from 'create-emotion';
3 |
4 | export interface ReactDiffViewerStyles {
5 | diffContainer?: string;
6 | diffRemoved?: string;
7 | diffAdded?: string;
8 | line?: string;
9 | highlightedGutter?: string;
10 | contentText?: string;
11 | gutter?: string;
12 | highlightedLine?: string;
13 | lineNumber?: string;
14 | marker?: string;
15 | wordDiff?: string;
16 | wordAdded?: string;
17 | wordRemoved?: string;
18 | codeFoldGutter?: string;
19 | emptyGutter?: string;
20 | emptyLine?: string;
21 | codeFold?: string;
22 | titleBlock?: string;
23 | content?: string;
24 | splitView?: string;
25 | [key: string]: string | undefined;
26 | }
27 |
28 | export interface ReactDiffViewerStylesVariables {
29 | diffViewerBackground?: string;
30 | diffViewerTitleBackground?: string;
31 | diffViewerColor?: string;
32 | diffViewerTitleColor?: string;
33 | diffViewerTitleBorderColor?: string;
34 | addedBackground?: string;
35 | addedColor?: string;
36 | removedBackground?: string;
37 | removedColor?: string;
38 | wordAddedBackground?: string;
39 | wordRemovedBackground?: string;
40 | addedGutterBackground?: string;
41 | removedGutterBackground?: string;
42 | gutterBackground?: string;
43 | gutterBackgroundDark?: string;
44 | highlightBackground?: string;
45 | highlightGutterBackground?: string;
46 | codeFoldGutterBackground?: string;
47 | codeFoldBackground?: string;
48 | emptyLineBackground?: string;
49 | gutterColor?: string;
50 | addedGutterColor?: string;
51 | removedGutterColor?: string;
52 | codeFoldContentColor?: string;
53 | }
54 |
55 | export interface ReactDiffViewerStylesOverride {
56 | variables?: {
57 | dark?: ReactDiffViewerStylesVariables;
58 | light?: ReactDiffViewerStylesVariables;
59 | };
60 | diffContainer?: Interpolation;
61 | diffRemoved?: Interpolation;
62 | diffAdded?: Interpolation;
63 | marker?: Interpolation;
64 | emptyGutter?: Interpolation;
65 | highlightedLine?: Interpolation;
66 | lineNumber?: Interpolation;
67 | highlightedGutter?: Interpolation;
68 | contentText?: Interpolation;
69 | gutter?: Interpolation;
70 | line?: Interpolation;
71 | wordDiff?: Interpolation;
72 | wordAdded?: Interpolation;
73 | wordRemoved?: Interpolation;
74 | codeFoldGutter?: Interpolation;
75 | emptyLine?: Interpolation;
76 | content?: Interpolation;
77 | titleBlock?: Interpolation;
78 | splitView?: Interpolation;
79 | }
80 |
81 | export default (
82 | styleOverride: ReactDiffViewerStylesOverride,
83 | useDarkTheme = false,
84 | ): ReactDiffViewerStyles => {
85 | const { variables: overrideVariables = {}, ...styles } = styleOverride;
86 |
87 | const themeVariables = {
88 | light: {
89 | ...{
90 | diffViewerBackground: '#fff',
91 | diffViewerColor: '#212529',
92 | addedBackground: '#e6ffed',
93 | addedColor: '#24292e',
94 | removedBackground: '#ffeef0',
95 | removedColor: '#24292e',
96 | wordAddedBackground: '#acf2bd',
97 | wordRemovedBackground: '#fdb8c0',
98 | addedGutterBackground: '#cdffd8',
99 | removedGutterBackground: '#ffdce0',
100 | gutterBackground: '#f7f7f7',
101 | gutterBackgroundDark: '#f3f1f1',
102 | highlightBackground: '#fffbdd',
103 | highlightGutterBackground: '#fff5b1',
104 | codeFoldGutterBackground: '#dbedff',
105 | codeFoldBackground: '#f1f8ff',
106 | emptyLineBackground: '#fafbfc',
107 | gutterColor: '#212529',
108 | addedGutterColor: '#212529',
109 | removedGutterColor: '#212529',
110 | codeFoldContentColor: '#212529',
111 | diffViewerTitleBackground: '#fafbfc',
112 | diffViewerTitleColor: '#212529',
113 | diffViewerTitleBorderColor: '#eee',
114 | },
115 | ...(overrideVariables.light || {}),
116 | },
117 | dark: {
118 | ...{
119 | diffViewerBackground: '#2e303c',
120 | diffViewerColor: '#FFF',
121 | addedBackground: '#044B53',
122 | addedColor: 'white',
123 | removedBackground: '#632F34',
124 | removedColor: 'white',
125 | wordAddedBackground: '#055d67',
126 | wordRemovedBackground: '#7d383f',
127 | addedGutterBackground: '#034148',
128 | removedGutterBackground: '#632b30',
129 | gutterBackground: '#2c2f3a',
130 | gutterBackgroundDark: '#262933',
131 | highlightBackground: '#2a3967',
132 | highlightGutterBackground: '#2d4077',
133 | codeFoldGutterBackground: '#21232b',
134 | codeFoldBackground: '#262831',
135 | emptyLineBackground: '#363946',
136 | gutterColor: '#464c67',
137 | addedGutterColor: '#8c8c8c',
138 | removedGutterColor: '#8c8c8c',
139 | codeFoldContentColor: '#555a7b',
140 | diffViewerTitleBackground: '#2f323e',
141 | diffViewerTitleColor: '#555a7b',
142 | diffViewerTitleBorderColor: '#353846',
143 | },
144 | ...(overrideVariables.dark || {}),
145 | },
146 | };
147 |
148 | const variables = useDarkTheme ? themeVariables.dark : themeVariables.light;
149 |
150 | const content = css({
151 | width: '100%',
152 | label: 'content',
153 | });
154 |
155 | const splitView = css({
156 | [`.${content}`]: {
157 | width: '50%',
158 | },
159 | label: 'split-view',
160 | });
161 |
162 | const diffContainer = css({
163 | width: '100%',
164 | background: variables.diffViewerBackground,
165 | pre: {
166 | margin: 0,
167 | whiteSpace: 'pre-wrap',
168 | lineHeight: '25px',
169 | },
170 | label: 'diff-container',
171 | borderCollapse: 'collapse',
172 | });
173 |
174 | const codeFoldContent = css({
175 | color: variables.codeFoldContentColor,
176 | label: 'code-fold-content',
177 | });
178 |
179 | const contentText = css({
180 | color: variables.diffViewerColor,
181 | label: 'content-text',
182 | });
183 |
184 | const titleBlock = css({
185 | background: variables.diffViewerTitleBackground,
186 | padding: 10,
187 | borderBottom: `1px solid ${variables.diffViewerTitleBorderColor}`,
188 | label: 'title-block',
189 | ':last-child': {
190 | borderLeft: `1px solid ${variables.diffViewerTitleBorderColor}`,
191 | },
192 | [`.${contentText}`]: {
193 | color: variables.diffViewerTitleColor,
194 | },
195 | });
196 |
197 | const lineNumber = css({
198 | color: variables.gutterColor,
199 | label: 'line-number',
200 | });
201 |
202 | const diffRemoved = css({
203 | background: variables.removedBackground,
204 | color: variables.removedColor,
205 | pre: {
206 | color: variables.removedColor,
207 | },
208 | [`.${lineNumber}`]: {
209 | color: variables.removedGutterColor,
210 | },
211 | label: 'diff-removed',
212 | });
213 |
214 | const diffAdded = css({
215 | background: variables.addedBackground,
216 | color: variables.addedColor,
217 | pre: {
218 | color: variables.addedColor,
219 | },
220 | [`.${lineNumber}`]: {
221 | color: variables.addedGutterColor,
222 | },
223 | label: 'diff-added',
224 | });
225 |
226 | const wordDiff = css({
227 | padding: 2,
228 | display: 'inline-flex',
229 | borderRadius: 1,
230 | label: 'word-diff',
231 | });
232 |
233 | const wordAdded = css({
234 | background: variables.wordAddedBackground,
235 | label: 'word-added',
236 | });
237 |
238 | const wordRemoved = css({
239 | background: variables.wordRemovedBackground,
240 | label: 'word-removed',
241 | });
242 |
243 | const codeFoldGutter = css({
244 | backgroundColor: variables.codeFoldGutterBackground,
245 | label: 'code-fold-gutter',
246 | });
247 |
248 | const codeFold = css({
249 | backgroundColor: variables.codeFoldBackground,
250 | height: 40,
251 | fontSize: 14,
252 | fontWeight: 700,
253 | label: 'code-fold',
254 | a: {
255 | textDecoration: 'underline !important',
256 | cursor: 'pointer',
257 | pre: {
258 | display: 'inline',
259 | },
260 | },
261 | });
262 |
263 | const emptyLine = css({
264 | backgroundColor: variables.emptyLineBackground,
265 | label: 'empty-line',
266 | });
267 |
268 | const marker = css({
269 | width: 25,
270 | paddingLeft: 10,
271 | paddingRight: 10,
272 | userSelect: 'none',
273 | label: 'marker',
274 | [`&.${diffAdded}`]: {
275 | pre: {
276 | color: variables.addedColor,
277 | },
278 | },
279 | [`&.${diffRemoved}`]: {
280 | pre: {
281 | color: variables.removedColor,
282 | },
283 | },
284 | });
285 |
286 | const highlightedLine = css({
287 | background: variables.highlightBackground,
288 | label: 'highlighted-line',
289 | [`.${wordAdded}, .${wordRemoved}`]: {
290 | backgroundColor: 'initial',
291 | },
292 | });
293 |
294 | const highlightedGutter = css({
295 | label: 'highlighted-gutter',
296 | });
297 |
298 | const gutter = css({
299 | userSelect: 'none',
300 | minWidth: 50,
301 | padding: '0 10px',
302 | label: 'gutter',
303 | textAlign: 'right',
304 | background: variables.gutterBackground,
305 | '&:hover': {
306 | cursor: 'pointer',
307 | background: variables.gutterBackgroundDark,
308 | pre: {
309 | opacity: 1,
310 | },
311 | },
312 | pre: {
313 | opacity: 0.5,
314 | },
315 | [`&.${diffAdded}`]: {
316 | background: variables.addedGutterBackground,
317 | },
318 | [`&.${diffRemoved}`]: {
319 | background: variables.removedGutterBackground,
320 | },
321 | [`&.${highlightedGutter}`]: {
322 | background: variables.highlightGutterBackground,
323 | '&:hover': {
324 | background: variables.highlightGutterBackground,
325 | },
326 | },
327 | });
328 |
329 | const emptyGutter = css({
330 | '&:hover': {
331 | background: variables.gutterBackground,
332 | cursor: 'initial',
333 | },
334 | label: 'empty-gutter',
335 | });
336 |
337 | const line = css({
338 | verticalAlign: 'baseline',
339 | label: 'line',
340 | });
341 |
342 | const defaultStyles: any = {
343 | diffContainer,
344 | diffRemoved,
345 | diffAdded,
346 | splitView,
347 | marker,
348 | highlightedGutter,
349 | highlightedLine,
350 | gutter,
351 | line,
352 | wordDiff,
353 | wordAdded,
354 | wordRemoved,
355 | codeFoldGutter,
356 | codeFold,
357 | emptyGutter,
358 | emptyLine,
359 | lineNumber,
360 | contentText,
361 | content,
362 | codeFoldContent,
363 | titleBlock,
364 | };
365 |
366 | const computerOverrideStyles: ReactDiffViewerStyles = Object.keys(
367 | styles,
368 | ).reduce(
369 | (acc, key): ReactDiffViewerStyles => ({
370 | ...acc,
371 | ...{
372 | [key]: css((styles as any)[key]),
373 | },
374 | }),
375 | {},
376 | );
377 |
378 | return Object.keys(defaultStyles).reduce(
379 | (acc, key): ReactDiffViewerStyles => ({
380 | ...acc,
381 | ...{
382 | [key]: computerOverrideStyles[key]
383 | ? cx(defaultStyles[key], computerOverrideStyles[key])
384 | : defaultStyles[key],
385 | },
386 | }),
387 | {},
388 | );
389 | };
390 |
--------------------------------------------------------------------------------
/test/compute-lines-test.ts:
--------------------------------------------------------------------------------
1 | import * as expect from 'expect';
2 | import { computeLineInformation, DiffMethod } from '../src/compute-lines';
3 |
4 | describe('Testing compute lines utils', (): void => {
5 | it('Should it avoid trailing spaces', (): void => {
6 | const oldCode = `test
7 |
8 |
9 | `;
10 | const newCode = `test
11 |
12 | `;
13 |
14 | expect(computeLineInformation(oldCode, newCode))
15 | .toMatchObject({
16 | lineInformation: [
17 | {
18 | left: {
19 | lineNumber: 1,
20 | type: 0,
21 | value: 'test',
22 | },
23 | right: {
24 | lineNumber: 1,
25 | type: 0,
26 | value: 'test',
27 | },
28 | },
29 | ],
30 | diffLines: [],
31 | });
32 | });
33 |
34 | it('Should identify line addition', (): void => {
35 | const oldCode = 'test';
36 | const newCode = `test
37 | newLine`;
38 |
39 | expect(computeLineInformation(oldCode, newCode))
40 | .toMatchObject({
41 | lineInformation: [
42 | {
43 | right: {
44 | lineNumber: 1,
45 | type: 0,
46 | value: 'test',
47 | },
48 | left: {
49 | lineNumber: 1,
50 | type: 0,
51 | value: 'test',
52 | },
53 | },
54 | {
55 | right: {
56 | lineNumber: 2,
57 | type: 1,
58 | value: ' newLine',
59 | },
60 | left: {},
61 | },
62 | ],
63 | diffLines: [1],
64 | });
65 | });
66 |
67 | it('Should identify line deletion', (): void => {
68 | const oldCode = `test
69 | oldLine`;
70 | const newCode = 'test';
71 |
72 | expect(computeLineInformation(oldCode, newCode))
73 | .toMatchObject({
74 | lineInformation: [
75 | {
76 | right: {
77 | lineNumber: 1,
78 | type: 0,
79 | value: 'test',
80 | },
81 | left: {
82 | lineNumber: 1,
83 | type: 0,
84 | value: 'test',
85 | },
86 | },
87 | {
88 | right: {},
89 | left: {
90 | lineNumber: 2,
91 | type: 2,
92 | value: ' oldLine',
93 | },
94 | },
95 | ],
96 | diffLines: [1],
97 | });
98 | });
99 |
100 | it('Should identify line modification', (): void => {
101 | const oldCode = `test
102 | oldLine`;
103 | const newCode = `test
104 | newLine`;
105 |
106 | expect(computeLineInformation(oldCode, newCode, true))
107 | .toMatchObject({
108 | lineInformation: [
109 | {
110 | right: {
111 | lineNumber: 1,
112 | type: 0,
113 | value: 'test',
114 | },
115 | left: {
116 | lineNumber: 1,
117 | type: 0,
118 | value: 'test',
119 | },
120 | },
121 | {
122 | right: {
123 | lineNumber: 2,
124 | type: 1,
125 | value: ' newLine',
126 | },
127 | left: {
128 | lineNumber: 2,
129 | type: 2,
130 | value: ' oldLine',
131 | },
132 | },
133 | ],
134 | diffLines: [1],
135 | });
136 | });
137 |
138 | it('Should identify word diff', (): void => {
139 | const oldCode = `test
140 | oldLine`;
141 | const newCode = `test
142 | newLine`;
143 |
144 | expect(computeLineInformation(oldCode, newCode))
145 | .toMatchObject({
146 | lineInformation: [
147 | {
148 | right: {
149 | lineNumber: 1,
150 | type: 0,
151 | value: 'test',
152 | },
153 | left: {
154 | lineNumber: 1,
155 | type: 0,
156 | value: 'test',
157 | },
158 | },
159 | {
160 | right: {
161 | lineNumber: 2,
162 | type: 1,
163 | value: [
164 | {
165 | type: 0,
166 | value: ' ',
167 | },
168 | {
169 | type: 1,
170 | value: 'new',
171 | },
172 | {
173 | type: 0,
174 | value: 'Line',
175 | },
176 | ],
177 | },
178 | left: {
179 | lineNumber: 2,
180 | type: 2,
181 | value: [
182 | {
183 | type: 0,
184 | value: ' ',
185 | },
186 | {
187 | type: 2,
188 | value: 'old',
189 | },
190 | {
191 | type: 0,
192 | value: 'Line',
193 | },
194 | ],
195 | },
196 | },
197 | ],
198 | diffLines: [1],
199 | });
200 | });
201 |
202 | it('Should call "diffChars" jsDiff method when compareMethod is not provided', (): void => {
203 | const oldCode = 'Hello World';
204 | const newCode = `My Updated Name
205 | Also this info`;
206 |
207 | expect(computeLineInformation(oldCode, newCode))
208 | .toMatchObject({
209 | lineInformation: [
210 | {
211 | right: {
212 | lineNumber: 1,
213 | type: 1,
214 | value: [
215 | {
216 | type: 1,
217 | value: 'My Updat',
218 | },
219 | {
220 | type: 0,
221 | value: 'e',
222 | },
223 | {
224 | type: 1,
225 | value: 'd',
226 | },
227 | {
228 | type: 0,
229 | value: ' ',
230 | },
231 | {
232 | type: 1,
233 | value: 'Name',
234 | },
235 | ],
236 | },
237 | left: {
238 | lineNumber: 1,
239 | type: 2,
240 | value: [
241 | {
242 | type: 2,
243 | value: 'H',
244 | },
245 | {
246 | type: 0,
247 | value: 'e',
248 | },
249 | {
250 | type: 2,
251 | value: 'llo',
252 | },
253 | {
254 | type: 0,
255 | value: ' ',
256 | },
257 | {
258 | type: 2,
259 | value: 'World',
260 | },
261 | ],
262 | },
263 | },
264 | {
265 | right: {
266 | lineNumber: 2,
267 | type: 1,
268 | value: 'Also this info',
269 | },
270 | left: {},
271 | },
272 | ],
273 | diffLines: [
274 | 0,
275 | 2,
276 | ],
277 | });
278 | });
279 |
280 | it('Should call "diffWords" jsDiff method when a compareMethod IS provided', (): void => {
281 | const oldCode = 'Hello World';
282 | const newCode = `My Updated Name
283 | Also this info`;
284 |
285 | expect(computeLineInformation(oldCode, newCode, false, DiffMethod.WORDS))
286 | .toMatchObject({
287 | lineInformation: [
288 | {
289 | right: {
290 | lineNumber: 1,
291 | type: 1,
292 | value: [
293 | {
294 | type: 1,
295 | value: 'My',
296 | },
297 | {
298 | type: 0,
299 | value: ' ',
300 | },
301 | {
302 | type: 1,
303 | value: 'Updated Name',
304 | },
305 | ],
306 | },
307 | left: {
308 | lineNumber: 1,
309 | type: 2,
310 | value: [
311 | {
312 | type: 2,
313 | value: 'Hello',
314 | },
315 | {
316 | type: 0,
317 | value: ' ',
318 | },
319 | {
320 | type: 2,
321 | value: 'World',
322 | },
323 | ],
324 | },
325 | },
326 | {
327 | right: {
328 | lineNumber: 2,
329 | type: 1,
330 | value: 'Also this info',
331 | },
332 | left: {},
333 | },
334 | ],
335 | diffLines: [
336 | 0,
337 | 2,
338 | ],
339 | });
340 | });
341 |
342 | it('Should not call jsDiff method and not diff text when disableWordDiff is true', (): void => {
343 | const oldCode = 'Hello World';
344 | const newCode = `My Updated Name
345 | Also this info`;
346 |
347 | expect(computeLineInformation(oldCode, newCode, true))
348 | .toMatchObject({
349 | lineInformation: [
350 | {
351 | right: {
352 | lineNumber: 1,
353 | type: 1,
354 | value: 'My Updated Name',
355 | },
356 | left: {
357 | lineNumber: 1,
358 | type: 2,
359 | value: 'Hello World',
360 | },
361 | },
362 | {
363 | right: {
364 | lineNumber: 2,
365 | type: 1,
366 | value: 'Also this info',
367 | },
368 | left: {},
369 | },
370 | ],
371 | diffLines: [
372 | 0,
373 | 2,
374 | ],
375 | });
376 | });
377 |
378 | it('Should start line counting from offset', (): void => {
379 | const oldCode = 'Hello World';
380 | const newCode = `My Updated Name
381 | Also this info`;
382 |
383 | expect(computeLineInformation(oldCode, newCode, true, DiffMethod.WORDS, 5))
384 | .toMatchObject({
385 | lineInformation: [
386 | {
387 | right: {
388 | lineNumber: 6,
389 | type: 1,
390 | value: 'My Updated Name',
391 | },
392 | left: {
393 | lineNumber: 6,
394 | type: 2,
395 | value: 'Hello World',
396 | },
397 | },
398 | {
399 | right: {
400 | lineNumber: 7,
401 | type: 1,
402 | value: 'Also this info',
403 | },
404 | left: {},
405 | },
406 | ],
407 | diffLines: [
408 | 0,
409 | 2,
410 | ],
411 | });
412 | });
413 | });
414 |
--------------------------------------------------------------------------------
/test/react-diff-viewer-test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import * as React from 'react';
3 | import * as expect from 'expect';
4 |
5 | import DiffViewer from '../lib/index';
6 |
7 | const oldCode = `
8 | const a = 123
9 | const b = 456
10 | const c = 4556
11 | const d = 4566
12 | const e = () => {
13 | console.log('c')
14 | }
15 | `;
16 |
17 | const newCode = `
18 | const a = 123
19 | const b = 456
20 | const c = 4556
21 | const d = 4566
22 | const aa = 123
23 | const bb = 456
24 | `;
25 |
26 | describe('Testing react diff viewer', (): void => {
27 | it('It should render a table', (): void => {
28 | const node = shallow();
32 |
33 | expect(node.find('table').length).toEqual(1);
34 | });
35 |
36 | it('It should render diff lines in diff view', (): void => {
37 | const node = shallow();
41 |
42 | expect(node.find('table > tbody tr').length).toEqual(7);
43 | });
44 |
45 | it('It should render diff lines in inline view', (): void => {
46 | const node = shallow();
51 |
52 | expect(node.find('table > tbody tr').length).toEqual(9);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/tsconfig.examples.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "jsx": "react",
5 | "module": "es6",
6 | "moduleResolution": "node",
7 | "noImplicitAny": true,
8 | "target": "es5",
9 | "downlevelIteration": true,
10 | "lib": ["es2017", "dom"]
11 | },
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "jsx": "react",
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "noImplicitAny": true,
8 | "target": "es5",
9 | "declaration": true,
10 | "downlevelIteration": true,
11 | "lib": ["es2017", "dom"],
12 | "types": [
13 | "mocha",
14 | "node"
15 | ]
16 | },
17 | "include": ["./src/*", "enzyme.js"],
18 | "exclude": ["node_modules"],
19 | }
20 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const Css = require('mini-css-extract-plugin');
4 | const FavIconsWebpackPlugin = require('favicons-webpack-plugin');
5 |
6 | module.exports = {
7 | entry: {
8 | main: './examples/src/index.tsx',
9 | },
10 | mode: process.env.NODE_ENV === 'production' ?
11 | 'production' : 'development',
12 | resolve: {
13 | extensions: ['.jsx', '.tsx', '.ts', '.scss', '.css', '.js'],
14 | },
15 | output: {
16 | path: path.resolve(__dirname, 'examples/dist'),
17 | filename: '[name].js',
18 | },
19 | devServer: {
20 | contentBase: path.resolve(__dirname, 'examples/dist'),
21 | port: 8000,
22 | hot: true,
23 | },
24 | module: {
25 | rules: [{
26 | test: /\.tsx?$/,
27 | use: [{
28 | loader: 'ts-loader',
29 | options: {
30 | configFile: 'tsconfig.examples.json',
31 | },
32 | }],
33 | exclude: /node_modules/,
34 | },
35 | {
36 | test: /\.s?css$/,
37 | use: [
38 | Css.loader,
39 | 'css-loader',
40 | 'sass-loader',
41 | ],
42 | },
43 | {
44 | test: /\.xml|.rjs|.java/,
45 | use: 'raw-loader',
46 | },
47 | {
48 | test: /\.svg|.png/,
49 | use: 'file-loader',
50 | },
51 | ],
52 | },
53 | plugins: [
54 | new HtmlWebpackPlugin({
55 | template: './examples/src/index.ejs',
56 | }),
57 | new FavIconsWebpackPlugin('./logo-standalone.png'),
58 | new Css({
59 | filename: 'main.css',
60 | }),
61 | ],
62 | };
63 |
--------------------------------------------------------------------------------