Enhancements for highlighting

1. Fixed a bug that prevented highlighting when more lines were deleted
from a replacement block than added

2. Added similarity search feature to match similar lines for
highlighting.

3. Highlighting: Support for matching "ins" and "del" markers to allow
for a "change" marker

4. Bugfix: All lines are now split into prefix and content spans
This commit is contained in:
Wolfgang Illmeyer 2015-11-25 17:37:26 +01:00
parent 8b9c2473b2
commit 7335c3bab3
6 changed files with 307 additions and 79 deletions

View file

@ -14,6 +14,8 @@ Diff to Html generates pretty HTML diffs from git diff output.
* Code syntax highlight * Code syntax highlight
* Line similarity matching
## Online Example ## Online Example
> Go to [Diff2HTML](http://rtfpessoa.github.io/diff2html/) > Go to [Diff2HTML](http://rtfpessoa.github.io/diff2html/)
@ -48,6 +50,8 @@ The HTML output accepts a Javascript object with configuration. Possible options
- `inputFormat`: the format of the input data: `'diff'` or `'json'`, default is `'diff'` - `inputFormat`: the format of the input data: `'diff'` or `'json'`, default is `'diff'`
- `outputFormat`: the format of the output data: `'line-by-line'` or `'side-by-side'`, default is `'line-by-line'` - `outputFormat`: the format of the output data: `'line-by-line'` or `'side-by-side'`, default is `'line-by-line'`
- `showFiles`: show a file list before the diff: `true` or `false`, default is `false` - `showFiles`: show a file list before the diff: `true` or `false`, default is `false`
- `matchWords`: matches changed words in diff output lines: `true` or `false`, default is `false`
- `matchWordsThreshold`: similarity threshold for word matching, default is 0.25
## Syntax Highlight ## Syntax Highlight

View file

@ -256,6 +256,19 @@
clear: both; clear: both;
} }
.d2h-del.d2h-change, .d2h-ins.d2h-change {
background-color: #ffc;
}
ins.d2h-change, del.d2h-change {
background-color: #fad771;
}
.d2h-file-diff .d2h-del.d2h-change {
background-color: #fae1af;
}
.d2h-file-diff .d2h-ins.d2h-change {
background-color: #faedaf;
}
/* CSS only show/hide */ /* CSS only show/hide */
.d2h-show { .d2h-show {
display: none; display: none;

View file

@ -10,6 +10,7 @@
var diffParser = require('./diff-parser.js').DiffParser; var diffParser = require('./diff-parser.js').DiffParser;
var printerUtils = require('./printer-utils.js').PrinterUtils; var printerUtils = require('./printer-utils.js').PrinterUtils;
var utils = require('./utils.js').Utils; var utils = require('./utils.js').Utils;
var Rematch = require('./rematch.js').Rematch;
function LineByLinePrinter() { function LineByLinePrinter() {
} }
@ -51,6 +52,12 @@
'</div>\n'; '</div>\n';
}; };
var matcher=Rematch.rematch(function(a,b) {
var amod = a.content.substr(1),
bmod = b.content.substr(1);
return Rematch.distance(amod, bmod);
});
function generateFileHtml(file, config) { function generateFileHtml(file, config) {
return file.blocks.map(function(block) { return file.blocks.map(function(block) {
@ -63,27 +70,18 @@
var oldLines = []; var oldLines = [];
var newLines = []; var newLines = [];
var processedOldLines = []; function processChangeBlock() {
var processedNewLines = []; var matches = matcher(oldLines, newLines);
matches.forEach(function(match){
for (var i = 0; i < block.lines.length; i++) { var oldLines = match[0];
var line = block.lines[i]; var newLines = match[1];
var escapedLine = utils.escape(line.content); var processedOldLines = [];
var processedNewLines = [];
if (line.type == diffParser.LINE_TYPE.CONTEXT && !oldLines.length && !newLines.length) {
lines += generateLineHtml(line.type, line.oldNumber, line.newNumber, escapedLine);
} else if (line.type == diffParser.LINE_TYPE.INSERTS && !oldLines.length && !newLines.length) {
lines += generateLineHtml(line.type, line.oldNumber, line.newNumber, escapedLine);
} else if (line.type == diffParser.LINE_TYPE.DELETES && !newLines.length) {
oldLines.push(line);
} else if (line.type == diffParser.LINE_TYPE.INSERTS && oldLines.length > newLines.length) {
newLines.push(line);
} else {
var j = 0; var j = 0;
var oldLine, newLine; var oldLine, newLine,
common = Math.min(oldLines.length, newLines.length),
if (oldLines.length === newLines.length) { max = Math.max(oldLines.length, newLines.length);
for (j = 0; j < oldLines.length; j++) { for (j = 0; j < common; j++) {
oldLine = oldLines[j]; oldLine = oldLines[j];
newLine = newLines[j]; newLine = newLines[j];
@ -91,27 +89,46 @@
var diff = printerUtils.diffHighlight(oldLine.content, newLine.content, config); var diff = printerUtils.diffHighlight(oldLine.content, newLine.content, config);
processedOldLines += processedOldLines +=
generateLineHtml(oldLine.type, oldLine.oldNumber, oldLine.newNumber, generateLineHtml(oldLine.type + ' d2h-change', oldLine.oldNumber, oldLine.newNumber,
diff.first.line, diff.first.prefix); diff.first.line, diff.first.prefix);
processedNewLines += processedNewLines +=
generateLineHtml(newLine.type, newLine.oldNumber, newLine.newNumber, generateLineHtml(newLine.type + ' d2h-change', newLine.oldNumber, newLine.newNumber,
diff.second.line, diff.second.prefix); diff.second.line, diff.second.prefix);
} }
lines += processedOldLines + processedNewLines; lines += processedOldLines + processedNewLines;
} else { lines += processLines(oldLines.slice(common), newLines.slice(common));
lines += processLines(oldLines, newLines);
}
oldLines = []; processedOldLines = [];
newLines = []; processedNewLines = [];
processedOldLines = []; });
processedNewLines = []; oldLines = [];
i--; newLines = [];
}
for (var i = 0; i < block.lines.length; i++) {
var line = block.lines[i];
var escapedLine = utils.escape(line.content);
if ( line.type !== diffParser.LINE_TYPE.INSERTS &&
(newLines.length > 0 || (line.type !== diffParser.LINE_TYPE.DELETES && oldLines > 0))) {
processChangeBlock();
}
if (line.type == diffParser.LINE_TYPE.CONTEXT) {
lines += generateLineHtml(line.type, line.oldNumber, line.newNumber, escapedLine);
} else if (line.type == diffParser.LINE_TYPE.INSERTS && !oldLines.length) {
lines += generateLineHtml(line.type, line.oldNumber, line.newNumber, escapedLine);
} else if (line.type == diffParser.LINE_TYPE.DELETES) {
oldLines.push(line);
} else if (line.type == diffParser.LINE_TYPE.INSERTS && !!oldLines.length) {
newLines.push(line);
} else {
console.error('unknown state in html line-by-line generator');
processChangeBlock();
} }
} }
lines += processLines(oldLines, newLines); processChangeBlock();
return lines; return lines;
}).join('\n'); }).join('\n');

View file

@ -9,6 +9,7 @@
var jsDiff = require('diff'); var jsDiff = require('diff');
var utils = require('./utils.js').Utils; var utils = require('./utils.js').Utils;
var Rematch = require('./rematch.js').Rematch;
function PrinterUtils() { function PrinterUtils() {
} }
@ -69,12 +70,42 @@
var highlightedLine = ''; var highlightedLine = '';
var changedWords = [];
if (!config.charByChar && config.matchWords) {
var treshold = 0.25;
if (typeof(config.matchWordsThreshold) !== "undefined") {
treshold = config.matchWordsThreshold;
}
var matcher = Rematch.rematch(function(a, b) {
var amod = a.value,
bmod = b.value,
result = Rematch.distance(amod, bmod);
return result;
});
var removed = diff.filter(function isRemoved(element){
return element.removed;
});
var added = diff.filter(function isAdded(element){
return element.added;
});
var chunks = matcher(added, removed);
chunks = chunks.forEach(function(chunk){
if(chunk[0].length === 1 && chunk[1].length === 1) {
var dist = Rematch.distance(chunk[0][0].value, chunk[1][0].value)
if (dist < treshold) {
changedWords.push(chunk[0][0]);
changedWords.push(chunk[1][0]);
}
}
});
}
diff.forEach(function(part) { diff.forEach(function(part) {
var addClass = changedWords.indexOf(part) > -1 ? ' class="d2h-change"' : '';
var elemType = part.added ? 'ins' : part.removed ? 'del' : null; var elemType = part.added ? 'ins' : part.removed ? 'del' : null;
var escapedValue = utils.escape(part.value); var escapedValue = utils.escape(part.value);
if (elemType !== null) { if (elemType !== null) {
highlightedLine += '<' + elemType + '>' + escapedValue + '</' + elemType + '>'; highlightedLine += '<' + elemType + addClass + '>' + escapedValue + '</' + elemType + '>';
} else { } else {
highlightedLine += escapedValue; highlightedLine += escapedValue;
} }
@ -97,11 +128,11 @@
} }
function removeIns(line) { function removeIns(line) {
return line.replace(/(<ins>((.|\n)*?)<\/ins>)/g, ''); return line.replace(/(<ins[^>]*>((.|\n)*?)<\/ins>)/g, '');
} }
function removeDel(line) { function removeDel(line) {
return line.replace(/(<del>((.|\n)*?)<\/del>)/g, ''); return line.replace(/(<del[^>]*>((.|\n)*?)<\/del>)/g, '');
} }
module.exports['PrinterUtils'] = new PrinterUtils(); module.exports['PrinterUtils'] = new PrinterUtils();

134
src/rematch.js Normal file
View file

@ -0,0 +1,134 @@
/*
*
* Rematch (rematch.js)
* Matching two sequences of objects by similarity
* Author: W. Illmeyer, Nexxar GmbH
*
*/
(function(ctx, undefined) {
var Rematch = {};
Rematch.arrayToString = function arrayToString(a) {
if (Object.prototype.toString.apply(a,[]) === "[object Array]") {
return "[" + a.map(arrayToString).join(", ") + "]";
} else {
return a;
}
}
/*
Copyright (c) 2011 Andrei Mackenzie
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
function levenshtein(a, b){
if(a.length == 0) return b.length;
if(b.length == 0) return a.length;
var matrix = [];
// increment along the first column of each row
var i;
for(i = 0; i <= b.length; i++){
matrix[i] = [i];
}
// increment each column in the first row
var j;
for(j = 0; j <= a.length; j++){
matrix[0][j] = j;
}
// Fill in the rest of the matrix
for(i = 1; i <= b.length; i++){
for(j = 1; j <= a.length; j++){
if(b.charAt(i-1) == a.charAt(j-1)){
matrix[i][j] = matrix[i-1][j-1];
} else {
matrix[i][j] = Math.min(matrix[i-1][j-1] + 1, // substitution
Math.min(matrix[i][j-1] + 1, // insertion
matrix[i-1][j] + 1)); // deletion
}
}
}
return matrix[b.length][a.length];
}
Rematch.levenshtein = levenshtein;
Rematch.distance = function distance(x,y) {
x=x.trim();
y=y.trim();
var lev = levenshtein(x,y),
score = lev / (x.length + y.length);
return score;
}
Rematch.rematch = function rematch(distanceFunction) {
function findBestMatch(a, b, cache) {
var cachecount = 0;
for(var key in cache) {
cachecount++;
}
var bestMatchDist = Infinity;
var bestMatch;
for (var i = 0; i < a.length; ++i) {
for (var j = 0; j < b.length; ++j) {
var cacheKey = JSON.stringify([a[i], b[j]]);
var md;
if (cache.hasOwnProperty(cacheKey)) {
md = cache[cacheKey];
} else {
md = distanceFunction(a[i], b[j]);
cache[cacheKey] = md;
}
if (md < bestMatchDist) {
bestMatchDist = md;
bestMatch = { indexA: i, indexB: j, score: bestMatchDist };
}
}
}
return bestMatch;
}
function group(a, b, level, cache) {
if (typeof(cache)==="undefined") {
cache = {};
}
var minLength = Math.min(a.length, b.length);
var bm = findBestMatch(a,b, cache);
if (!level) {
level = 0;
}
if (!bm || (a.length + b.length < 3)) {
return [[a, b]];
}
var a1 = a.slice(0, bm.indexA),
b1 = b.slice(0, bm.indexB),
aMatch = [a[bm.indexA]],
bMatch = [b[bm.indexB]],
tailA = bm.indexA + 1,
tailB = bm.indexB + 1,
a2 = a.slice(tailA),
b2 = b.slice(tailB);
var group1 = group(a1, b1, level+1, cache);
var groupMatch = group(aMatch, bMatch, level+1, cache);
var group2 = group(a2, b2, level+1, cache);
var result = groupMatch;
if (bm.indexA > 0 || bm.indexB > 0) {
result = group1.concat(result);
}
if (a.length > tailA || b.length > tailB ) {
result = result.concat(group2);
}
return result;
}
return group;
}
module.exports['Rematch'] = Rematch;
})(this);

View file

@ -10,6 +10,7 @@
var diffParser = require('./diff-parser.js').DiffParser; var diffParser = require('./diff-parser.js').DiffParser;
var printerUtils = require('./printer-utils.js').PrinterUtils; var printerUtils = require('./printer-utils.js').PrinterUtils;
var utils = require('./utils.js').Utils; var utils = require('./utils.js').Utils;
var Rematch = require('./rematch.js').Rematch;
function SideBySidePrinter() { function SideBySidePrinter() {
} }
@ -62,6 +63,12 @@
'</div>\n'; '</div>\n';
}; };
var matcher=Rematch.rematch(function(a,b) {
var amod = a.content.substr(1),
bmod = b.content.substr(1);
return Rematch.distance(amod, bmod);
});
function generateSideBySideFileHtml(file, config) { function generateSideBySideFileHtml(file, config) {
var fileHtml = {}; var fileHtml = {};
fileHtml.left = ''; fileHtml.left = '';
@ -87,57 +94,68 @@
var oldLines = []; var oldLines = [];
var newLines = []; var newLines = [];
var tmpHtml = ''; function processChangeBlock() {
var matches = matcher(oldLines, newLines);
matches.forEach(function(match){
var oldLines = match[0];
var newLines = match[1];
var tmpHtml;
var j = 0;
var oldLine, newLine,
common = Math.min(oldLines.length, newLines.length),
max = Math.max(oldLines.length, newLines.length);
for (j = 0; j < common; j++) {
oldLine = oldLines[j];
newLine = newLines[j];
config.isCombined = file.isCombined;
var diff = printerUtils.diffHighlight(oldLine.content, newLine.content, config);
fileHtml.left +=
generateSingleLineHtml(oldLine.type + ' d2h-change', oldLine.oldNumber,
diff.first.line, diff.first.prefix);
fileHtml.right +=
generateSingleLineHtml(newLine.type + ' d2h-change', newLine.newNumber,
diff.second.line, diff.second.prefix);
}
if (max > common) {
var oldSlice = oldLines.slice(common),
newSlice = newLines.slice(common);
tmpHtml = processLines(oldLines.slice(common), newLines.slice(common));
fileHtml.left += tmpHtml.left;
fileHtml.right += tmpHtml.right;
}
});
oldLines = [];
newLines = [];
}
for (var i = 0; i < block.lines.length; i++) { for (var i = 0; i < block.lines.length; i++) {
var line = block.lines[i]; var line = block.lines[i];
var escapedLine = utils.escape(line.content); var prefix = line[0];
var escapedLine = utils.escape(line.content.substr(1));
if (line.type == diffParser.LINE_TYPE.CONTEXT && !oldLines.length && !newLines.length) { if ( line.type !== diffParser.LINE_TYPE.INSERTS &&
fileHtml.left += generateSingleLineHtml(line.type, line.oldNumber, escapedLine); (newLines.length > 0 || (line.type !== diffParser.LINE_TYPE.DELETES && oldLines > 0))) {
fileHtml.right += generateSingleLineHtml(line.type, line.newNumber, escapedLine); processChangeBlock();
} else if (line.type == diffParser.LINE_TYPE.INSERTS && !oldLines.length && !newLines.length) { }
if (line.type == diffParser.LINE_TYPE.CONTEXT) {
fileHtml.left += generateSingleLineHtml(line.type, line.oldNumber, escapedLine, prefix);
fileHtml.right += generateSingleLineHtml(line.type, line.newNumber, escapedLine, prefix);
} else if (line.type == diffParser.LINE_TYPE.INSERTS && !oldLines.length) {
fileHtml.left += generateSingleLineHtml(diffParser.LINE_TYPE.CONTEXT, '', '', ''); fileHtml.left += generateSingleLineHtml(diffParser.LINE_TYPE.CONTEXT, '', '', '');
fileHtml.right += generateSingleLineHtml(line.type, line.newNumber, escapedLine); fileHtml.right += generateSingleLineHtml(line.type, line.newNumber, escapedLine, prefix);
} else if (line.type == diffParser.LINE_TYPE.DELETES && !newLines.length) { } else if (line.type == diffParser.LINE_TYPE.DELETES) {
oldLines.push(line); oldLines.push(line);
} else if (line.type == diffParser.LINE_TYPE.INSERTS && oldLines.length > newLines.length) { } else if (line.type == diffParser.LINE_TYPE.INSERTS && !!oldLines.length) {
newLines.push(line); newLines.push(line);
} else { } else {
var j = 0; console.error('unknown state in html side-by-side generator');
var oldLine, newLine; processChangeBlock();
if (oldLines.length === newLines.length) {
for (j = 0; j < oldLines.length; j++) {
oldLine = oldLines[j];
newLine = newLines[j];
config.isCombined = file.isCombined;
var diff = printerUtils.diffHighlight(oldLine.content, newLine.content, config);
fileHtml.left +=
generateSingleLineHtml(oldLine.type, oldLine.oldNumber,
diff.first.line, diff.first.prefix);
fileHtml.right +=
generateSingleLineHtml(newLine.type, newLine.newNumber,
diff.second.line, diff.second.prefix);
}
} else {
tmpHtml = processLines(oldLines, newLines);
fileHtml.left += tmpHtml.left;
fileHtml.right += tmpHtml.right;
}
oldLines = [];
newLines = [];
i--;
} }
} }
tmpHtml = processLines(oldLines, newLines); processChangeBlock();
fileHtml.left += tmpHtml.left;
fileHtml.right += tmpHtml.right;
}); });
return fileHtml; return fileHtml;
@ -152,16 +170,27 @@
for (j = 0; j < maxLinesNumber; j++) { for (j = 0; j < maxLinesNumber; j++) {
var oldLine = oldLines[j]; var oldLine = oldLines[j];
var newLine = newLines[j]; var newLine = newLines[j];
var oldContent;
var newContent;
var oldPrefix;
var newPrefix;
if (oldLine) {
oldContent = utils.escape(oldLine.content.substr(1));
oldPrefix = oldLine.content[0];
}
if (newLine) {
newContent = utils.escape(newLine.content.substr(1));
newPrefix = newLine.content[0];
}
if (oldLine && newLine) { if (oldLine && newLine) {
fileHtml.left += generateSingleLineHtml(oldLine.type, oldLine.oldNumber, utils.escape(oldLine.content)); fileHtml.left += generateSingleLineHtml(oldLine.type, oldLine.oldNumber, oldContent, oldPrefix);
fileHtml.right += generateSingleLineHtml(newLine.type, newLine.newNumber, utils.escape(newLine.content)); fileHtml.right += generateSingleLineHtml(newLine.type, newLine.newNumber, newContent, newPrefix);
} else if (oldLine) { } else if (oldLine) {
fileHtml.left += generateSingleLineHtml(oldLine.type, oldLine.oldNumber, utils.escape(oldLine.content)); fileHtml.left += generateSingleLineHtml(oldLine.type, oldLine.oldNumber, oldContent, oldPrefix);
fileHtml.right += generateSingleLineHtml(diffParser.LINE_TYPE.CONTEXT, '', '', ''); fileHtml.right += generateSingleLineHtml(diffParser.LINE_TYPE.CONTEXT, '', '', '');
} else if (newLine) { } else if (newLine) {
fileHtml.left += generateSingleLineHtml(diffParser.LINE_TYPE.CONTEXT, '', '', ''); fileHtml.left += generateSingleLineHtml(diffParser.LINE_TYPE.CONTEXT, '', '', '');
fileHtml.right += generateSingleLineHtml(newLine.type, newLine.newNumber, utils.escape(newLine.content)); fileHtml.right += generateSingleLineHtml(newLine.type, newLine.newNumber, newContent, newPrefix);
} else { } else {
console.error('How did it get here?'); console.error('How did it get here?');
} }