├── .gitignore ├── screenshots ├── chars.png ├── unified.png └── unified_no_color.png ├── bin └── disparity ├── test ├── file2.js ├── file1.js ├── chars.txt ├── unified_no_color.txt ├── chars_paths.txt ├── chars.html ├── unified.txt ├── unified_2.txt ├── unified.html └── runner.js ├── LICENSE.md ├── CHANGELOG.md ├── package.json ├── disparity-cli.js ├── disparity.d.ts ├── README.md └── disparity.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /screenshots/chars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millermedeiros/disparity/HEAD/screenshots/chars.png -------------------------------------------------------------------------------- /screenshots/unified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millermedeiros/disparity/HEAD/screenshots/unified.png -------------------------------------------------------------------------------- /screenshots/unified_no_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millermedeiros/disparity/HEAD/screenshots/unified_no_color.png -------------------------------------------------------------------------------- /bin/disparity: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var cli = require('../disparity-cli'); 4 | var args = cli.parse(process.argv.slice(2)); 5 | var code = cli.run(args, process.stdout, process.stderr); 6 | process.exit(code); 7 | -------------------------------------------------------------------------------- /test/file2.js: -------------------------------------------------------------------------------- 1 | var foo = "bar"; 2 | 3 | // some comment 4 | function dolor(){ amet() 5 | } 6 | 7 | // no changes until line 19 8 | var a = function() { 9 | return 'b'; 10 | }; 11 | 12 | // some other comment 13 | 14 | thisWontShowOnDiff(); 15 | thisShouldBeFirstLineOfLine19Diff(); 16 | // yeap this will show too 17 | 18 | dolor ( a); 19 | amet(); 20 | // EOF 21 | -------------------------------------------------------------------------------- /test/file1.js: -------------------------------------------------------------------------------- 1 | var foo = "bar"; 2 | 3 | // some comment 4 | function dolor(){ 5 | amet(); 6 | } 7 | 8 | // no changes until line 19 9 | var a = function() { 10 | return 'b'; 11 | }; 12 | 13 | // some other comment 14 | 15 | thisWontShowOnDiff(); 16 | thisShouldBeFirstLineOfLine19Diff(); 17 | // yeap this will show too 18 | 19 | dolor(a); 20 | amet(); 21 | // EOF 22 | -------------------------------------------------------------------------------- /test/chars.txt: -------------------------------------------------------------------------------- 1 | removed added 2 | 3 | 1 | var foo = "bar"; 4 | 2 | 5 | 3 | // some comment 6 | 4 | function dolor(){ 7 | 5 |  amet(); 8 | 6 | } 9 | 7 | 10 | 8 | // no changes until line 19 11 | 16 | thisShouldBeFirstLineOfLine19Diff(); 12 | 17 | // yeap this will show too 13 | 18 | 14 | 19 | dolor (a); 15 | 20 | amet(); 16 | 21 | // EOF 17 | -------------------------------------------------------------------------------- /test/unified_no_color.txt: -------------------------------------------------------------------------------- 1 | --- test/file1.js removed 2 | +++ test/file2.js added 3 | @@ -1,9 +1,8 @@ 4 | var foo = "bar"; 5 | 6 | // some comment 7 | -function dolor(){ 8 | - amet(); 9 | +function dolor(){ amet() 10 | } 11 | 12 | // no changes until line 19 13 | var a = function() { 14 | @@ -15,7 +14,7 @@ 15 | thisWontShowOnDiff(); 16 | thisShouldBeFirstLineOfLine19Diff(); 17 | // yeap this will show too 18 | 19 | -dolor(a); 20 | +dolor ( a); 21 | amet(); 22 | // EOF 23 | -------------------------------------------------------------------------------- /test/chars_paths.txt: -------------------------------------------------------------------------------- 1 | test/file1.js test/file2.js 2 | 3 | 1 | var foo = "bar"; 4 | 2 | 5 | 3 | // some comment 6 | 4 | function dolor(){ 7 | 5 |  amet(); 8 | 6 | } 9 | 7 | 10 | 8 | // no changes until line 19 11 | 16 | thisShouldBeFirstLineOfLine19Diff(); 12 | 17 | // yeap this will show too 13 | 18 | 14 | 19 | dolor (a); 15 | 20 | amet(); 16 | 21 | // EOF 17 | -------------------------------------------------------------------------------- /test/chars.html: -------------------------------------------------------------------------------- 1 | removed added 2 | 3 | 1 | var foo = "bar"; 4 | 2 | 5 | 3 | // some comment 6 | 4 | function dolor(){ 7 | 5 | amet(); 8 | 6 | } 9 | 7 | 10 | 8 | // no changes until line 19 11 | 16 | thisShouldBeFirstLineOfLine19Diff(); 12 | 17 | // yeap this will show too 13 | 18 | 14 | 19 | dolor (a); 15 | 20 | amet(); 16 | 21 | // EOF 17 | -------------------------------------------------------------------------------- /test/unified.txt: -------------------------------------------------------------------------------- 1 | --- removed 2 | +++ added 3 | @@ -1,9 +1,8 @@ 4 | var foo = "bar"; 5 | 6 | // some comment 7 | -function dolor(){ 8 | - amet(); 9 | +function dolor(){ amet() 10 | } 11 | 12 | // no changes until line 19 13 | var a = function() { 14 | @@ -15,7 +14,7 @@ 15 | thisWontShowOnDiff(); 16 | thisShouldBeFirstLineOfLine19Diff(); 17 | // yeap this will show too 18 | 19 | -dolor(a); 20 | +dolor ( a); 21 | amet(); 22 | // EOF 23 | -------------------------------------------------------------------------------- /test/unified_2.txt: -------------------------------------------------------------------------------- 1 | --- test/file1.js removed 2 | +++ test/file2.js added 3 | @@ -1,9 +1,8 @@ 4 | var foo = "bar"; 5 | 6 | // some comment 7 | -function dolor(){ 8 | - amet(); 9 | +function dolor(){ amet() 10 | } 11 | 12 | // no changes until line 19 13 | var a = function() { 14 | @@ -15,7 +14,7 @@ 15 | thisWontShowOnDiff(); 16 | thisShouldBeFirstLineOfLine19Diff(); 17 | // yeap this will show too 18 | 19 | -dolor(a); 20 | +dolor ( a); 21 | amet(); 22 | // EOF 23 | -------------------------------------------------------------------------------- /test/unified.html: -------------------------------------------------------------------------------- 1 | --- test/file1.js removed 2 | +++ test/file2.js added 3 | @@ -1,9 +1,8 @@ 4 | var foo = "bar"; 5 | 6 | // some comment 7 | -function dolor(){ 8 | - amet(); 9 | +function dolor(){ amet() 10 | } 11 | 12 | // no changes until line 19 13 | var a = function() { 14 | @@ -15,7 +14,7 @@ 15 | thisWontShowOnDiff(); 16 | thisShouldBeFirstLineOfLine19Diff(); 17 | // yeap this will show too 18 | 19 | -dolor(a); 20 | +dolor ( a); 21 | amet(); 22 | // EOF 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Miller Medeiros 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # disparity changelog 2 | 3 | ## v3.2.0 (2021/01/20) 4 | 5 | - Add support to `context` opt in `unified` function 6 | 7 | ## v3.1.0 8 | 9 | - Add type definitions for TypeScript support 10 | 11 | ## v3.0.0 (2019/11/06) 12 | 13 | - BREAKING: Only supports node >= 8 14 | - Fixed security issue with outdated dependencies 15 | 16 | ## v2.0.0 (2015/04/03) 17 | 18 | - change API so all methods have same signature (str1, str2, opts). 19 | - merge CLI options `--unified --no-color ` into `--unified-no-color`. 20 | - better error messages on the CLI. 21 | - show paths on chars diff as well (if supplied) or fallback to 22 | `disparity.removed` and `disparity.added`. 23 | 24 | ## v1.3.1 (2015/04/01) 25 | 26 | - fix line number alignment. 27 | 28 | ## v1.3.0 (2015/04/01) 29 | 30 | - add support for custom colors. 31 | - update unified color scheme. 32 | - simplify line splitting logic. 33 | - improve way strings are colorized (avoids `\n` and `\r` chars). 34 | 35 | ## v1.2.0 (2015/04/01) 36 | 37 | - add `unifiedNoColor` support. 38 | 39 | ## v1.1.0 (2015/04/01) 40 | 41 | - allow override of `removed` and `added` strings. 42 | 43 | ## v1.0.0 (2015/04/01) 44 | 45 | - initial release 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "disparity", 3 | "version": "3.2.0", 4 | "description": "Colorized string diff ideal for text/code that spans through multiple lines", 5 | "main": "disparity.js", 6 | "types": "disparity.d.ts", 7 | "bin": { 8 | "disparity": "bin/disparity" 9 | }, 10 | "scripts": { 11 | "test": "node test/runner.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/millermedeiros/disparity.git" 16 | }, 17 | "files": [ 18 | "bin", 19 | "disparity-cli.js", 20 | "disparity.d.ts", 21 | "disparity.js" 22 | ], 23 | "engines": { 24 | "node": ">=8" 25 | }, 26 | "keywords": [ 27 | "diff", 28 | "char", 29 | "unified", 30 | "multiline", 31 | "string", 32 | "color", 33 | "ansi", 34 | "terminal", 35 | "cli", 36 | "tty" 37 | ], 38 | "author": "Miller Medeiros ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/millermedeiros/disparity/issues" 42 | }, 43 | "homepage": "https://github.com/millermedeiros/disparity", 44 | "dependencies": { 45 | "ansi-styles": "^4.2.1", 46 | "diff": "^4.0.2" 47 | }, 48 | "jshintConfig": { 49 | "node": true, 50 | "eqnull": true 51 | }, 52 | "devDependencies": {} 53 | } 54 | -------------------------------------------------------------------------------- /disparity-cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var disparity = require('./disparity'); 4 | 5 | // decided to not use an external lib because we have very few options 6 | // and very basic logic 7 | exports.parse = function(argv) { 8 | var args = { 9 | help: argv.indexOf('--help') !== -1 || argv.indexOf('-h') !== -1 || !argv.length, 10 | version: argv.indexOf('--version') !== -1 || argv.indexOf('-v') !== -1, 11 | unified: argv.indexOf('-u') !== -1 || argv.indexOf('--unified') !== -1, 12 | unifiedNoColor: argv.indexOf('-x') !== -1 || argv.indexOf('--unified-no-color') !== -1, 13 | paths: argv.slice(-2).filter(nonArgs), 14 | errors: [] 15 | }; 16 | // default mode is "--chars" 17 | args.chars = !args.unified; 18 | 19 | var len = args.paths.length; 20 | if (!args.help && !args.version && len !== 2) { 21 | args.errors.push('Error: you should provide 2 file paths, found "' + len + '".'); 22 | } 23 | 24 | return args; 25 | }; 26 | 27 | function nonArgs(val) { 28 | // arguments starts with "-" so we ignore those 29 | return val.indexOf('-') !== 0; 30 | } 31 | 32 | exports.run = function(args, out, err) { 33 | out = out || process.stdout; 34 | err = err || process.stderr; 35 | 36 | if (args.help) { 37 | showHelp(out); 38 | return 0; 39 | } 40 | 41 | if (args.version) { 42 | out.write('disparity v' + require('./package.json').version + '\n'); 43 | return 0; 44 | } 45 | 46 | if (args.errors && args.errors.length) { 47 | args.errors.forEach(function(e) { 48 | err.write(e + '\n'); 49 | }); 50 | err.write('\n'); 51 | showHelp(out); 52 | return 1; 53 | } 54 | 55 | var fs = require('fs'); 56 | var f1 = fs.readFileSync(args.paths[0]).toString(); 57 | var f2 = fs.readFileSync(args.paths[1]).toString(); 58 | 59 | var method = 'chars'; 60 | if (args.unified) { 61 | method = 'unified'; 62 | } 63 | if (args.unifiedNoColor) { 64 | method = 'unifiedNoColor'; 65 | } 66 | 67 | // defaul to char diff 68 | out.write(disparity[method](f1, f2, { 69 | paths: args.paths 70 | })); 71 | return 0; 72 | }; 73 | 74 | function showHelp(out) { 75 | out.write([ 76 | 'disparity [OPTIONS] ', 77 | 'Colorized string diff.', 78 | '', 79 | 'Options:', 80 | ' -u, --unified Output unified diff.', 81 | ' -c, --chars Output char diff (default mode).', 82 | ' -v, --version Display current version.', 83 | ' -h, --help Display this help.', 84 | '' 85 | ].join('\n')); 86 | } 87 | -------------------------------------------------------------------------------- /disparity.d.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | /** 3 | * How many lines to display before/after a line that contains diffs 4 | * 5 | * @default 3 6 | */ 7 | context?: number; 8 | /** 9 | * File paths displayed just before the diff 10 | * 11 | * @default [disparity.removed, disparity.added] 12 | */ 13 | paths?: [string, string]; 14 | } 15 | 16 | /** 17 | * Diffs two blocks of text, comparing character by character 18 | * and returning a string with ansi color codes. 19 | * 20 | * Will return an empty string if `oldStr` equals `newStr`. 21 | * 22 | * @example ``` 23 | * var diff = disparity.chars(file1, file2); 24 | * console.log(diff); 25 | * ``` 26 | * 27 | * @param {string} oldStr 28 | * @param {string} newStr 29 | * @param {Options} opts 30 | * 31 | * @return {string} 32 | */ 33 | export function chars(oldStr: string, newStr: string, opts: Options): string; 34 | 35 | /** 36 | * Returns ansi colorized {@link https://en.wikipedia.org/wiki/Diff_utility#Unified_format unified diff}. 37 | * 38 | * Will return an empty string if `oldStr` equals `newStr`. 39 | * 40 | * @example ``` 41 | * var diff = disparity.unified(file1, file2, { 42 | * paths: ['test/file1.js', 'test/file2.js'] 43 | * }); 44 | * console.log(diff); 45 | * ``` 46 | * 47 | * @param {string} oldStr 48 | * @param {string} newStr 49 | * @param {Options} opts 50 | * 51 | * @return {string} 52 | */ 53 | export function unified(oldStr: string, newStr: string, opts: Options): string; 54 | 55 | /** 56 | * Returns ansi colorized {@link https://en.wikipedia.org/wiki/Diff_utility#Unified_format unified diff}. 57 | * Useful for terminals that {@link https://www.npmjs.com/package/supports-color don't support color}. 58 | * 59 | * Will return an empty string if `oldStr` equals `newStr`. 60 | * 61 | * @example ``` 62 | * var diff = disparity.unifiedNoColor(file1, file2, { 63 | * paths: ['test/file1.js', 'test/file2.js'] 64 | * }); 65 | * console.log(diff); 66 | * ``` 67 | * 68 | * @param {string} oldStr 69 | * @param {string} newStr 70 | * @param {Options} opts 71 | * 72 | * @return {string} 73 | */ 74 | export function unifiedNoColor( 75 | oldStr: string, 76 | newStr: string, 77 | opts: Options 78 | ): string; 79 | 80 | /** 81 | * The string used on diff headers to say that chars/lines were removed 82 | * 83 | * @default 'removed' 84 | */ 85 | export let removed: string; 86 | 87 | /** 88 | * The string used on the diff headers to say that chars/lines were added 89 | * 90 | * @default 'added' 91 | */ 92 | export let added: string; 93 | 94 | export interface DiffColor { 95 | open: string; 96 | close: string; 97 | } 98 | 99 | /** 100 | * Object containing references to all the colors used by disparity. 101 | */ 102 | export let colors: { 103 | added: DiffColor; 104 | charsAdded: DiffColor; 105 | charsRemoved: DiffColor; 106 | header: DiffColor; 107 | removed: DiffColor; 108 | section: DiffColor; 109 | }; 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # disparity 2 | 3 | Colorized string diff ideal for text/code that spans through multiple lines. 4 | 5 | This is basically just a wrapper around 6 | [diff](https://www.npmjs.com/package/diff) and 7 | [ansi-styles](https://www.npmjs.com/package/ansi-styles) + line numbers and 8 | omitting lines that don't have changes and/or that wouldn't help user identify 9 | the diff "context". 10 | 11 | We also replace some *invisible* chars to make it easier to understand what 12 | really changed from one file to another: 13 | 14 | - `\r` becomes `` 15 | - `\n` becomes `` 16 | - `\t` becomes `` 17 | 18 | Created mainly to be used by 19 | [esformatter](https://www.npmjs.com/package/esformatter) and other tools that 20 | might need to display a nice looking diff of source files. 21 | 22 | 23 | ## API 24 | 25 | ```js 26 | var disparity = require('disparity'); 27 | ``` 28 | 29 | ### chars(oldStr, newStr[, opts]):String 30 | 31 | Diffs two blocks of text, comparing character by character and returns 32 | a `String` with ansi color codes. 33 | 34 | ```js 35 | var diff = disparity.chars(file1, file2); 36 | console.log(diff); 37 | ``` 38 | 39 | Will return an empty string if `oldStr === newStr`; 40 | 41 | ```js 42 | // default options 43 | var opts = { 44 | // how many lines to display before/after a line that contains diffs 45 | context: 3, 46 | // file paths displayed just before the diff 47 | paths: [disparity.removed, disparity.added] 48 | }; 49 | ``` 50 | 51 | ![screenshot char diff](https://raw.githubusercontent.com/millermedeiros/disparity/master/screenshots/chars.png) 52 | 53 | ### unified(oldStr, newStr[, opts]):String 54 | 55 | Returns ansi colorized [unified 56 | diff](http://en.wikipedia.org/wiki/Diff_utility#Unified_format). 57 | 58 | Will return an empty string if `oldStr === newStr`; 59 | 60 | ```js 61 | var diff = disparity.unified(file1, file2, { 62 | paths: ['test/file1.js', 'test/file2.js'] 63 | }); 64 | console.log(diff); 65 | ``` 66 | 67 | ![screenshot unified diff](https://raw.githubusercontent.com/millermedeiros/disparity/master/screenshots/unified.png) 68 | 69 | ### unifiedNoColor(oldStr, newStr[, opts]):String 70 | 71 | Returns [unified diff](http://en.wikipedia.org/wiki/Diff_utility#Unified_format). 72 | Useful for terminals that [doesn't support colors](https://www.npmjs.com/package/supports-color). 73 | 74 | Will return an empty string if `oldStr === newStr`; 75 | 76 | ```js 77 | var diff = disparity.unifiedNoColor(file1, file2, { 78 | paths: ['test/file1.js', 'test/file2.js'] 79 | }); 80 | console.log(diff); 81 | ``` 82 | 83 | ![screenshot unified diff no color](https://raw.githubusercontent.com/millermedeiros/disparity/master/screenshots/unified_no_color.png) 84 | 85 | ### removed:String 86 | 87 | String used on the diff headers to say that chars/lines was removed. 88 | 89 | ```js 90 | // default value 91 | disparity.removed = 'removed'; 92 | ``` 93 | 94 | ### added:String 95 | 96 | String used on the diff headers to say that chars/lines was added. 97 | 98 | ```js 99 | // default value 100 | disparity.added = 'added'; 101 | ``` 102 | 103 | ### colors:Object 104 | 105 | Object containing references to all the colors used by disparity. 106 | 107 | If you want a different output than `ansi` (eg. HTML) you can replace the color 108 | values: 109 | 110 | ```js 111 | // wrap blocks into custom tags 112 | disparity.colors = { 113 | // chars diff 114 | charsRemoved: { open: '', close: '' }, 115 | charsAdded: { open: '', close: '' }, 116 | 117 | // unified diff 118 | removed: { open: '', close: '' }, 119 | added: { open: '', close: '' }, 120 | header: { open: '', close: '' }, 121 | section: { open: '', close: '' } 122 | }; 123 | ``` 124 | 125 | ## CLI 126 | 127 | `disparity` also have a command line interface: 128 | 129 | ``` 130 | disparity [OPTIONS] 131 | 132 | Options: 133 | -c, --chars Output char diff (default mode). 134 | -u, --unified Output unified diff. 135 | --unified-no-color Don't output colors. 136 | -v, --version Display current version. 137 | -h, --help Display this help. 138 | ``` 139 | 140 | PS: cli can only compare 2 external files at the moment, no `stdin` support. 141 | 142 | ## License 143 | 144 | Released under the MIT license. 145 | 146 | -------------------------------------------------------------------------------- /disparity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var stringDiff = require('diff'); 4 | var ansi = require('ansi-styles'); 5 | 6 | // --- 7 | 8 | exports.unified = unified; 9 | exports.unifiedNoColor = unifiedNoColor; 10 | exports.chars = chars; 11 | exports.removed = 'removed'; 12 | exports.added = 'added'; 13 | exports.colors = { 14 | charsRemoved: ansi.bgRed, 15 | charsAdded: ansi.bgGreen, 16 | removed: ansi.red, 17 | added: ansi.green, 18 | header: ansi.yellow, 19 | section: ansi.magenta, 20 | }; 21 | 22 | // --- 23 | 24 | function chars(str1, str2, opts) { 25 | if (str1 === str2) { 26 | return ''; 27 | } 28 | 29 | opts = opts || {}; 30 | 31 | // how many lines to add before/after the chars diff 32 | var context = opts.context != null ? opts.context : 3; 33 | 34 | var path1 = opts.paths && opts.paths[0] || exports.removed; 35 | var path2 = opts.paths && opts.paths[1] || exports.added; 36 | 37 | // text displayed before diff 38 | var header = colorize(path1, 'charsRemoved') + ' ' + 39 | colorize(path2, 'charsAdded') + '\n\n'; 40 | 41 | var changes = stringDiff.diffChars(str1, str2); 42 | var diff = changes.map(function(c) { 43 | var val = replaceInvisibleChars(c.value); 44 | if (c.removed) return colorize(val, 'charsRemoved'); 45 | if (c.added) return colorize(val, 'charsAdded'); 46 | return val; 47 | }).join(''); 48 | 49 | // this RegExp will include the '\n' char into the lines, easier to join() 50 | var lines = diff.split(/^/m); 51 | 52 | lines = addLineNumbers(lines); 53 | lines = removeLinesOutOfContext(lines, context); 54 | 55 | return header + lines.join(''); 56 | } 57 | 58 | function addLineNumbers(lines) { 59 | var nChars = lines.length.toString().length; 60 | return lines.map(function(line, i) { 61 | return alignRight(i + 1, nChars) + ' | ' + line; 62 | }); 63 | } 64 | 65 | function colorize(str, colorId) { 66 | var color = exports.colors[colorId]; 67 | // avoid highlighting the "\n" (would highlight till the end of the line) 68 | return str.replace(/[^\n\r]+/g, color.open + '$&' + color.close); 69 | } 70 | 71 | function replaceInvisibleChars(str) { 72 | return str 73 | .replace(/\t/g, '') 74 | .replace(/\r/g, '') 75 | .replace(/\n/g, '\n'); 76 | } 77 | 78 | function alignRight(val, nChars) { 79 | val = val.toString(); 80 | var diff = nChars - val.length; 81 | return diff ? (new Array(diff + 1)).join(' ') + val : val; 82 | } 83 | 84 | function removeLinesOutOfContext(lines, context) { 85 | // we cache the results since same line ends up being checked multiple times 86 | var diffMap = {}; 87 | var lastDiff = -Infinity; 88 | function hasDiff(line, i) { 89 | if (!(i in diffMap)) { 90 | diffMap[i] = hasCharDiff(line); 91 | } 92 | return diffMap[i]; 93 | } 94 | 95 | function hasDiffBefore(i) { 96 | return lastDiff + context >= i; 97 | } 98 | 99 | function hasDiffAfter(i) { 100 | var max = Math.min(i + context, lines.length - 1); 101 | var n = i; 102 | while (++n <= max) { 103 | if (hasDiff(lines[n], n)) return true; 104 | } 105 | return false; 106 | } 107 | 108 | return lines.filter(function(line, i, arr) { 109 | var has = hasDiff(line, i); 110 | if (has) { 111 | lastDiff = i; 112 | } 113 | return has || hasDiffBefore(i) || hasDiffAfter(i); 114 | }); 115 | } 116 | 117 | function hasCharDiff(line) { 118 | return line.indexOf(exports.colors.charsAdded.open) !== -1 || 119 | line.indexOf(exports.colors.charsRemoved.open) !== -1; 120 | } 121 | 122 | function unified(str1, str2, opts) { 123 | if (str2 === str1) { 124 | return ''; 125 | } 126 | 127 | var changes = unifiedNoColor(str1, str2, opts); 128 | // this RegExp will include all the `\n` chars into the lines, easier to join 129 | var lines = changes.split(/^/m); 130 | 131 | var start = colorize(lines.slice(0, 2).join(''), 'header'); 132 | var end = lines.slice(2).join('') 133 | .replace(/^\-.*/gm, colorize('$&', 'removed')) 134 | .replace(/^\+.*/gm, colorize('$&', 'added')) 135 | .replace(/^@@.+@@/gm, colorize('$&', 'section')); 136 | 137 | return start + end; 138 | } 139 | 140 | function unifiedNoColor(str1, str2, opts) { 141 | if (str2 === str1) { 142 | return ''; 143 | } 144 | 145 | opts = opts || {}; 146 | var path1 = opts.paths && opts.paths[0] || ''; 147 | var path2 = opts.paths && opts.paths[1] || path1; 148 | 149 | var changes = stringDiff.createPatch('', str1, str2, '', '', { context: opts.context }); 150 | 151 | // remove first 2 lines (header) 152 | changes = changes.replace(/^([^\n]+)\n([^\n]+)\n/m, ''); 153 | 154 | function appendPath(str, filePath, state) { 155 | var result = str; 156 | if (filePath) { 157 | result += ' ' + filePath; 158 | } 159 | if (state) { 160 | result += filePath ? '\t' : ' '; 161 | result += state; 162 | } 163 | return result; 164 | } 165 | 166 | changes = changes 167 | .replace(/^---.*/gm, appendPath('---', path1, exports.removed)) 168 | .replace(/^\+\+\+.*/gm, appendPath('+++', path2, exports.added)); 169 | 170 | return changes; 171 | } 172 | -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var disparity = require('../disparity'); 5 | var cli = require('../disparity-cli'); 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | 9 | function readFile(name) { 10 | var filePath = path.join(__dirname, name); 11 | return fs.readFileSync(filePath).toString(); 12 | } 13 | 14 | function compare(diff, expected, name) { 15 | if (diff !== expected) { 16 | // not using assert because it is easier to understand what is wrong 17 | process.stderr.write('disparity.' + name + '() failure!\n'); 18 | process.stderr.write('=== expected result:\n'); 19 | process.stderr.write(expected); 20 | process.stderr.write('\n=== actual result:\n'); 21 | process.stderr.write(diff); 22 | throw new Error('assertion error'); 23 | } 24 | } 25 | 26 | // setup 27 | // ===== 28 | 29 | var diff, expected; 30 | var file1 = readFile('file1.js'); 31 | var file2 = readFile('file2.js'); 32 | 33 | // chars 34 | // ===== 35 | 36 | diff = disparity.chars(file1, file2); 37 | expected = readFile('chars.txt'); 38 | compare(diff, expected, 'chars'); 39 | 40 | // unified 41 | // ======= 42 | 43 | diff = disparity.unified(file1, file2); 44 | expected = readFile('unified.txt'); 45 | compare(diff, expected, 'unified'); 46 | 47 | diff = disparity.unified(file1, file2, { 48 | paths: ['test/file1.js', 'test/file2.js'] 49 | }); 50 | expected = readFile('unified_2.txt'); 51 | compare(diff, expected, 'unified_2'); 52 | 53 | // unifiedNoColor 54 | // ============== 55 | 56 | diff = disparity.unifiedNoColor(file1, file2, { 57 | paths: ['test/file1.js', 'test/file2.js'] 58 | }); 59 | expected = readFile('unified_no_color.txt'); 60 | compare(diff, expected, 'unified_no_color'); 61 | 62 | // custom colors 63 | // ============= 64 | 65 | var _oldColors = disparity.colors; 66 | // wrap blocks into custom tags 67 | disparity.colors = { 68 | // chars diff 69 | charsRemoved: { open: '', close: '' }, 70 | charsAdded: { open: '', close: '' }, 71 | 72 | // unified diff 73 | removed: { open: '', close: '' }, 74 | added: { open: '', close: '' }, 75 | header: { open: '', close: '' }, 76 | section: { open: '', close: '' } 77 | }; 78 | 79 | diff = disparity.chars(file1, file2); 80 | expected = readFile('chars.html'); 81 | compare(diff, expected, 'chars.html'); 82 | 83 | diff = disparity.unified(file1, file2, { 84 | paths: ['test/file1.js', 'test/file2.js'] 85 | }); 86 | expected = readFile('unified.html'); 87 | compare(diff, expected, 'unified.html'); 88 | 89 | disparity.colors = _oldColors; 90 | 91 | // cli.parse 92 | // ========= 93 | 94 | var args = cli.parse([]); 95 | assert.ok(args.help, 'help'); 96 | assert.equal(args.errors.length, 0, 'error 1'); 97 | 98 | args = cli.parse(['--help']); 99 | assert.ok(args.help, 'help 2'); 100 | 101 | args = cli.parse(['-h']); 102 | assert.ok(args.help, 'help 3'); 103 | 104 | args = cli.parse(['-v']); 105 | assert.ok(args.version, 'version'); 106 | assert.equal(args.errors.length, 0, 'error 2'); 107 | 108 | args = cli.parse(['-u']); 109 | compare(args.errors[0], 'Error: you should provide 2 file paths, found "0".', 'error 3'); 110 | assert.equal(args.errors.length, 1, 'error 3.2'); 111 | 112 | args = cli.parse(['-u', 'foo.js', '--bar']); 113 | assert.ok(args.unified, '-u'); 114 | assert.equal(args.paths[0], 'foo.js'); 115 | // --bar should cause an error since it's invalid 116 | compare(args.errors[0], 'Error: you should provide 2 file paths, found "1".', 'error 4'); 117 | assert.equal(args.errors.length, 1, 'error 4.2'); 118 | 119 | args = cli.parse(['--unified', 'foo.js', 'bar.js']); 120 | assert.ok(args.unified, '--unified'); 121 | assert.equal(args.paths[0], 'foo.js'); 122 | assert.equal(args.paths[1], 'bar.js'); 123 | assert.equal(args.errors.length, 0, 'error 5'); 124 | 125 | args = cli.parse(['-c', 'foo.js', 'bar.js']); 126 | assert.ok(!args.unified, '!--unified'); 127 | assert.ok(args.chars, '-c'); 128 | 129 | args = cli.parse(['--chars', 'foo.js', 'bar.js']); 130 | assert.ok(!args.unified, '!--unified'); 131 | assert.ok(args.chars, '--chars'); 132 | 133 | args = cli.parse(['--unified-no-color', 'foo.js', 'bar.js']); 134 | assert.ok(!args.errors.length, '--unified-no-color errors'); 135 | assert.ok(args.unifiedNoColor, '--unified-no-color'); 136 | 137 | // cli.run 138 | // ======= 139 | 140 | function FakeStream(){ 141 | this.data = ''; 142 | this.write = function(data) { 143 | this.data += data; 144 | }; 145 | } 146 | 147 | var code; 148 | var out = new FakeStream(); 149 | var err = new FakeStream(); 150 | 151 | function run(args) { 152 | code = null; 153 | out.data = ''; 154 | err.data = ''; 155 | return cli.run(args, out, err); 156 | } 157 | 158 | code = run({ help: true }); 159 | assert.ok(!code, 'exit code'); 160 | assert.ok(out.data.length > 100, 'output help'); 161 | 162 | code = run({ version: true }); 163 | assert.ok(!code, 'exit code'); 164 | assert.equal(out.data, 'disparity v' + require('../package.json').version +'\n', 'version'); 165 | 166 | code = run({ errors: ['Error: foo bar'] }); 167 | assert.ok(code, 'exit code error'); 168 | assert.equal(err.data, 'Error: foo bar\n\n'); 169 | assert.ok(out.data.search('Options:')); 170 | 171 | code = run({ 172 | chars: true, 173 | paths: ['test/file1.js', 'test/file2.js'] 174 | }); 175 | expected = readFile('chars_paths.txt'); 176 | assert.ok(!code, 'exit code chars'); 177 | compare(out.data, expected, 'cli chars with paths'); 178 | assert.equal(err.data, ''); 179 | 180 | code = run({ 181 | unified: true, 182 | paths: ['test/file1.js', 'test/file2.js'] 183 | }); 184 | expected = readFile('unified_2.txt'); 185 | assert.ok(!code, 'exit code chars'); 186 | compare(out.data, expected, 'cli unified_2.txt'); 187 | assert.equal(err.data, ''); 188 | 189 | code = run({ 190 | unifiedNoColor: true, 191 | paths: ['test/file1.js', 'test/file2.js'] 192 | }); 193 | expected = readFile('unified_no_color.txt'); 194 | assert.ok(!code, 'exit code chars'); 195 | compare(out.data, expected, 'no color'); 196 | assert.equal(err.data, ''); 197 | --------------------------------------------------------------------------------