disparity
Version:
Colorized string diff ideal for text/code that spans through multiple lines
172 lines (138 loc) • 4.34 kB
JavaScript
;
var stringDiff = require('diff');
var ansi = require('ansi-styles');
// ---
exports.unified = unified;
exports.unifiedNoColor = unifiedNoColor;
exports.chars = chars;
exports.removed = 'removed';
exports.added = 'added';
exports.colors = {
charsRemoved: ansi.bgRed,
charsAdded: ansi.bgGreen,
removed: ansi.red,
added: ansi.green,
header: ansi.yellow,
section: ansi.magenta,
};
// ---
function chars(str1, str2, opts) {
if (str1 === str2) {
return '';
}
opts = opts || {};
// how many lines to add before/after the chars diff
var context = opts.context != null ? opts.context : 3;
var path1 = opts.paths && opts.paths[0] || exports.removed;
var path2 = opts.paths && opts.paths[1] || exports.added;
// text displayed before diff
var header = colorize(path1, 'charsRemoved') + ' ' +
colorize(path2, 'charsAdded') + '\n\n';
var changes = stringDiff.diffChars(str1, str2);
var diff = changes.map(function(c) {
var val = replaceInvisibleChars(c.value);
if (c.removed) return colorize(val, 'charsRemoved');
if (c.added) return colorize(val, 'charsAdded');
return val;
}).join('');
// this RegExp will include the '\n' char into the lines, easier to join()
var lines = diff.split(/^/m);
lines = addLineNumbers(lines);
lines = removeLinesOutOfContext(lines, context);
return header + lines.join('');
}
function addLineNumbers(lines) {
var nChars = lines.length.toString().length;
return lines.map(function(line, i) {
return alignRight(i + 1, nChars) + ' | ' + line;
});
}
function colorize(str, colorId) {
var color = exports.colors[colorId];
// avoid highlighting the "\n" (would highlight till the end of the line)
return str.replace(/[^\n\r]+/g, color.open + '$&' + color.close);
}
function replaceInvisibleChars(str) {
return str
.replace(/\t/g, '<tab>')
.replace(/\r/g, '<CR>')
.replace(/\n/g, '<LF>\n');
}
function alignRight(val, nChars) {
val = val.toString();
var diff = nChars - val.length;
return diff ? (new Array(diff + 1)).join(' ') + val : val;
}
function removeLinesOutOfContext(lines, context) {
// we cache the results since same line ends up being checked multiple times
var diffMap = {};
var lastDiff = -Infinity;
function hasDiff(line, i) {
if (!(i in diffMap)) {
diffMap[i] = hasCharDiff(line);
}
return diffMap[i];
}
function hasDiffBefore(i) {
return lastDiff + context >= i;
}
function hasDiffAfter(i) {
var max = Math.min(i + context, lines.length - 1);
var n = i;
while (++n <= max) {
if (hasDiff(lines[n], n)) return true;
}
return false;
}
return lines.filter(function(line, i, arr) {
var has = hasDiff(line, i);
if (has) {
lastDiff = i;
}
return has || hasDiffBefore(i) || hasDiffAfter(i);
});
}
function hasCharDiff(line) {
return line.indexOf(exports.colors.charsAdded.open) !== -1 ||
line.indexOf(exports.colors.charsRemoved.open) !== -1;
}
function unified(str1, str2, opts) {
if (str2 === str1) {
return '';
}
var changes = unifiedNoColor(str1, str2, opts);
// this RegExp will include all the `\n` chars into the lines, easier to join
var lines = changes.split(/^/m);
var start = colorize(lines.slice(0, 2).join(''), 'header');
var end = lines.slice(2).join('')
.replace(/^\-.*/gm, colorize('$&', 'removed'))
.replace(/^\+.*/gm, colorize('$&', 'added'))
.replace(/^@@.+@@/gm, colorize('$&', 'section'));
return start + end;
}
function unifiedNoColor(str1, str2, opts) {
if (str2 === str1) {
return '';
}
opts = opts || {};
var path1 = opts.paths && opts.paths[0] || '';
var path2 = opts.paths && opts.paths[1] || path1;
var changes = stringDiff.createPatch('', str1, str2, '', '', { context: opts.context });
// remove first 2 lines (header)
changes = changes.replace(/^([^\n]+)\n([^\n]+)\n/m, '');
function appendPath(str, filePath, state) {
var result = str;
if (filePath) {
result += ' ' + filePath;
}
if (state) {
result += filePath ? '\t' : ' ';
result += state;
}
return result;
}
changes = changes
.replace(/^---.*/gm, appendPath('---', path1, exports.removed))
.replace(/^\+\+\+.*/gm, appendPath('+++', path2, exports.added));
return changes;
}