PageRenderTime 55ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/html/lib/diffHighlighter.js

https://github.com/rowanj/gitx
JavaScript | 512 lines | 445 code | 41 blank | 26 comment | 97 complexity | 8b756bea769903f950e6ca9b3652f31a MD5 | raw file
  1. // If we run from a Safari instance, we don't
  2. // have a Controller object. Instead, we fake it by
  3. // using the console
  4. if (typeof Controller == 'undefined') {
  5. Controller = console;
  6. Controller.log_ = console.log;
  7. }
  8. var toggleDiff = function(id)
  9. {
  10. var content = document.getElementById('content_' + id);
  11. if (content) {
  12. var collapsed = (content.style.display == 'none');
  13. if (collapsed) {
  14. content.style.display = 'box';
  15. jQuery(content).fadeTo('slow', 1).slideDown();
  16. } else {
  17. jQuery(content).fadeTo('fast', 0).slideUp('fast', function () {content.style.display = 'none'});
  18. }
  19. var title = document.getElementById('title_' + id);
  20. if (title) {
  21. if (collapsed) {
  22. title.classList.remove('collapsed');
  23. title.classList.add('expanded');
  24. }
  25. else {
  26. title.classList.add('collapsed');
  27. title.classList.remove('expanded');
  28. }
  29. }
  30. }
  31. }
  32. var highlightDiff = function(diff, element, callbacks) {
  33. if (!diff || diff == "")
  34. return;
  35. if (!callbacks)
  36. callbacks = {};
  37. var start = new Date().getTime();
  38. element.className = "diff"
  39. var content = diff.escapeHTML();
  40. var file_index = 0;
  41. var startname = "";
  42. var endname = "";
  43. var line1 = "";
  44. var line2 = "";
  45. var diffContent = "";
  46. var finalContent = "";
  47. var lines = content.split('\n');
  48. var binary = false;
  49. var mode_change = false;
  50. var old_mode = "";
  51. var new_mode = "";
  52. var linkToTop = "<div class=\"top-link\"><a href=\"#\">Top</a></div>";
  53. var hunk_start_line_1 = -1;
  54. var hunk_start_line_2 = -1;
  55. var header = false;
  56. var finishContent = function()
  57. {
  58. if (!file_index)
  59. {
  60. file_index++;
  61. return;
  62. }
  63. if (callbacks["newfile"])
  64. callbacks["newfile"](startname, endname, "file_index_" + (file_index - 1), mode_change, old_mode, new_mode);
  65. var title = startname;
  66. var binaryname = endname;
  67. if (endname == "/dev/null") {
  68. binaryname = startname;
  69. title = startname;
  70. }
  71. else if (startname == "/dev/null")
  72. title = endname;
  73. else if (startname != endname)
  74. title = startname + " renamed to " + endname;
  75. if (binary && endname == "/dev/null") { // in cases of a deleted binary file, there is no diff/file to display
  76. line1 = "";
  77. line2 = "";
  78. diffContent = "";
  79. file_index++;
  80. startname = "";
  81. endname = "";
  82. return; // so printing the filename in the file-list is enough
  83. }
  84. if (diffContent != "" || binary) {
  85. finalContent += '<div class="file" id="file_index_' + (file_index - 1) + '">' +
  86. '<div id="title_' + title + '" class="expanded fileHeader"><a href="javascript:toggleDiff(\'' + title + '\');">' + title + '</a></div>';
  87. }
  88. if (!binary && (diffContent != "")) {
  89. finalContent += '<div id="content_' + title + '" class="diffContent">' +
  90. '<div class="lineno">' + line1 + "</div>" +
  91. '<div class="lineno">' + line2 + "</div>" +
  92. '<div class="lines">' + postProcessDiffContents(diffContent).replace(/\t/g, " ") + "</div>" +
  93. '</div>';
  94. }
  95. else {
  96. if (binary) {
  97. if (callbacks["binaryFile"])
  98. finalContent += callbacks["binaryFile"](binaryname);
  99. else
  100. finalContent += '<div id="content_' + title + '">Binary file differs</div>';
  101. }
  102. }
  103. if (diffContent != "" || binary)
  104. finalContent += '</div>' + linkToTop;
  105. line1 = "";
  106. line2 = "";
  107. diffContent = "";
  108. file_index++;
  109. startname = "";
  110. endname = "";
  111. }
  112. for (var lineno = 0, lindex = 0; lineno < lines.length; lineno++) {
  113. var l = lines[lineno];
  114. var firstChar = l.charAt(0);
  115. if (firstChar == "d" && l.charAt(1) == "i") { // "diff", i.e. new file, we have to reset everything
  116. header = true; // diff always starts with a header
  117. finishContent(); // Finish last file
  118. binary = false;
  119. mode_change = false;
  120. if(match = l.match(/^diff --git (a\/)+(.*) (b\/)+(.*)$/)) { // there are cases when we need to capture filenames from
  121. startname = match[2]; // the diff line, like with mode-changes.
  122. endname = match[4]; // this can get overwritten later if there is a diff or if
  123. } // the file is binary
  124. continue;
  125. }
  126. if (header) {
  127. if (firstChar == "n") {
  128. if (l.match(/^new file mode .*$/))
  129. startname = "/dev/null";
  130. if (match = l.match(/^new mode (.*)$/)) {
  131. mode_change = true;
  132. new_mode = match[1];
  133. }
  134. continue;
  135. }
  136. if (firstChar == "o") {
  137. if (match = l.match(/^old mode (.*)$/)) {
  138. mode_change = true;
  139. old_mode = match[1];
  140. }
  141. continue;
  142. }
  143. if (firstChar == "d") {
  144. if (l.match(/^deleted file mode .*$/))
  145. endname = "/dev/null";
  146. continue;
  147. }
  148. if (firstChar == "-") {
  149. if (match = l.match(/^--- (a\/)?(.*)$/))
  150. startname = match[2];
  151. continue;
  152. }
  153. if (firstChar == "+") {
  154. if (match = l.match(/^\+\+\+ (b\/)?(.*)$/))
  155. endname = match[2];
  156. continue;
  157. }
  158. // If it is a complete rename, we don't know the name yet
  159. // We can figure this out from the 'rename from.. rename to.. thing
  160. if (firstChar == 'r')
  161. {
  162. if (match = l.match(/^rename (from|to) (.*)$/))
  163. {
  164. if (match[1] == "from")
  165. startname = match[2];
  166. else
  167. endname = match[2];
  168. }
  169. continue;
  170. }
  171. if (firstChar == "B") // "Binary files .. and .. differ"
  172. {
  173. binary = true;
  174. // We might not have a diff from the binary file if it's new.
  175. // So, we use a regex to figure that out
  176. if (match = l.match(/^Binary files (a\/)?(.*) and (b\/)?(.*) differ$/))
  177. {
  178. startname = match[2];
  179. endname = match[4];
  180. }
  181. }
  182. // Finish the header
  183. if (firstChar == "@")
  184. header = false;
  185. else
  186. continue;
  187. }
  188. sindex = "index=" + lindex.toString() + " ";
  189. if (firstChar == "+") {
  190. line1 += "\n";
  191. line2 += ++hunk_start_line_2 + "\n";
  192. diffContent += "<div " + sindex + "class='addline'>" + l + "</div>";
  193. } else if (firstChar == "-") {
  194. line1 += ++hunk_start_line_1 + "\n";
  195. line2 += "\n";
  196. diffContent += "<div " + sindex + "class='delline'>" + l + "</div>";
  197. } else if (firstChar == "@") {
  198. if (header) {
  199. header = false;
  200. }
  201. if (m = l.match(/@@ \-([0-9]+),?\d* \+(\d+),?\d* @@/))
  202. {
  203. hunk_start_line_1 = parseInt(m[1]) - 1;
  204. hunk_start_line_2 = parseInt(m[2]) - 1;
  205. }
  206. line1 += "...\n";
  207. line2 += "...\n";
  208. diffContent += "<div " + sindex + "class='hunkheader'>" + l + "</div>";
  209. } else if (firstChar == " ") {
  210. line1 += ++hunk_start_line_1 + "\n";
  211. line2 += ++hunk_start_line_2 + "\n";
  212. diffContent += "<div " + sindex + "class='noopline'>" + l + "</div>";
  213. }
  214. lindex++;
  215. }
  216. finishContent();
  217. // This takes about 7ms
  218. element.innerHTML = finalContent;
  219. // TODO: Replace this with a performance pref call
  220. if (false)
  221. Controller.log_("Total time:" + (new Date().getTime() - start));
  222. }
  223. var highlightTrailingWhitespace = function (l) {
  224. // Highlight trailing whitespace
  225. l = l.replace(/(\s+)(<\/ins>)?$/, '<span class="whitespace">$1</span>$2');
  226. return l;
  227. }
  228. var mergeInsDel = function (html) {
  229. return html
  230. .replace(/^<\/(ins|del)>|<(ins|del)>$/g,'')
  231. .replace(/<\/(ins|del)><\1>/g,'');
  232. }
  233. var postProcessDiffContents = function(diffContent) {
  234. var $ = jQuery;
  235. var diffEl = $(diffContent);
  236. var dumbEl = $('<div/>');
  237. var newContent = "";
  238. var oldEls = [];
  239. var newEls = [];
  240. var flushBuffer = function () {
  241. if (oldEls.length || newEls.length) {
  242. var buffer = "";
  243. if (!oldEls.length || !newEls.length) {
  244. // hunk only contains additions OR deletions, so there is no need
  245. // to do any inline-diff. just keep the elements as they are
  246. buffer = $.map(oldEls.length ? oldEls : newEls, function (e) {
  247. var prefix = e.text().substring(0,1),
  248. text = inlinediff.escape(e.text().substring(1)),
  249. tag = prefix=='+' ? 'ins' : 'del',
  250. html = prefix+'<'+tag+'>'+(prefix == "+" ? highlightTrailingWhitespace(text) : text)+'</'+tag+'>';
  251. e.html(html);
  252. return dumbEl.html(e).html();
  253. }).join("");
  254. }
  255. else {
  256. // hunk contains additions AND deletions. so we create an inline diff
  257. // of all the old and new lines together and merge the result back to buffer
  258. var mapFn = function (e) { return e.text().substring(1).replace(/\r?\n|\r/g,''); };
  259. var oldText = $.map(oldEls, mapFn).join("\n");
  260. var newText = $.map(newEls, mapFn).join("\n");
  261. var diffResult = inlinediff.diffString3(oldText,newText);
  262. diffLines = (diffResult[1] + "\n" + diffResult[2]).split(/\n/g);
  263. buffer = $.map(oldEls, function (e, i) {
  264. var di = i;
  265. e.html("-"+mergeInsDel(diffLines[di]));
  266. return dumbEl.html(e).html();
  267. }).join("") + $.map(newEls, function (e, i) {
  268. var di = i + oldEls.length;
  269. var line = mergeInsDel(highlightTrailingWhitespace(diffLines[di]));
  270. e.html("+"+line);
  271. return dumbEl.html(e).html();
  272. }).join("");
  273. }
  274. newContent+= buffer;
  275. oldEls = [];
  276. newEls = [];
  277. }
  278. };
  279. diffEl.each(function (i, e) {
  280. e = $(e);
  281. var isAdd = e.is(".addline");
  282. var isDel = e.is(".delline");
  283. var text = e.text();
  284. var html = dumbEl.html(e).html();
  285. if (isAdd) {
  286. newEls.push(e);
  287. }
  288. else if (isDel) {
  289. oldEls.push(e);
  290. }
  291. else {
  292. flushBuffer();
  293. newContent+= html;
  294. }
  295. });
  296. flushBuffer();
  297. return newContent;
  298. }
  299. /*
  300. * Javascript Diff Algorithm
  301. * By John Resig (http://ejohn.org/)
  302. * Modified by Chu Alan "sprite"
  303. * Adapted for GitX by Mathias Leppich http://github.com/muhqu
  304. *
  305. * Released under the MIT license.
  306. *
  307. * More Info:
  308. * http://ejohn.org/projects/javascript-diff-algorithm/
  309. */
  310. var inlinediff = (function () {
  311. return {
  312. diffString: diffString,
  313. diffString3: diffString3,
  314. escape: escape
  315. };
  316. function escape(s) {
  317. var n = s;
  318. n = n.replace(/&/g, "&amp;");
  319. n = n.replace(/</g, "&lt;");
  320. n = n.replace(/>/g, "&gt;");
  321. n = n.replace(/"/g, "&quot;");
  322. return n;
  323. }
  324. function diffString( o, n ) {
  325. o = o.replace(/\s+$/, '');
  326. n = n.replace(/\s+$/, '');
  327. var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/) );
  328. var str = "";
  329. var oSpace = o.match(/\s+/g);
  330. if (oSpace == null) {
  331. oSpace = ["\n"];
  332. } else {
  333. oSpace.push("\n");
  334. }
  335. var nSpace = n.match(/\s+/g);
  336. if (nSpace == null) {
  337. nSpace = ["\n"];
  338. } else {
  339. nSpace.push("\n");
  340. }
  341. if (out.n.length == 0) {
  342. for (var i = 0; i < out.o.length; i++) {
  343. str += '<del>' + escape(out.o[i]) + oSpace[i] + "</del>";
  344. }
  345. } else {
  346. if (out.n[0].text == null) {
  347. for (n = 0; n < out.o.length && out.o[n].text == null; n++) {
  348. str += '<del>' + escape(out.o[n]) + oSpace[n] + "</del>";
  349. }
  350. }
  351. for ( var i = 0; i < out.n.length; i++ ) {
  352. if (out.n[i].text == null) {
  353. str += '<ins>' + escape(out.n[i]) + nSpace[i] + "</ins>";
  354. } else {
  355. var pre = "";
  356. for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
  357. pre += '<del>' + escape(out.o[n]) + oSpace[n] + "</del>";
  358. }
  359. str += escape(out.n[i].text) + nSpace[i] + pre;
  360. }
  361. }
  362. }
  363. return str;
  364. }
  365. function whitespaceAwareTokenize(n) {
  366. return n !== "" && n.match(/\n| *[\-><!=]+ *|[ \t]+|[<$&#ยง%]\w+|\w+|\W/g) || [];
  367. }
  368. function tag(t,c) {
  369. if (t === "") return escape(c);
  370. return c==="" ? '' : '<'+t+'>'+escape(c)+'</'+t+'>';
  371. }
  372. function diffString3( o, n ) {
  373. var out = diff(whitespaceAwareTokenize(o), whitespaceAwareTokenize(n));
  374. var ac = [], ao = [], an = [];
  375. if (out.n.length == 0) {
  376. for (var i = 0; i < out.o.length; i++) {
  377. ac.push(tag('del',out.o[i]));
  378. ao.push(tag('del',out.o[i]));
  379. }
  380. } else {
  381. if (out.n[0].text == null) {
  382. for (n = 0; n < out.o.length && out.o[n].text == null; n++) {
  383. ac.push(tag('del',out.o[n]));
  384. }
  385. }
  386. var added = 0;
  387. for ( var i = 0; i < out.o.length; i++ ) {
  388. if (out.o[i].text == null) {
  389. ao.push(tag('del',out.o[i])); added++;
  390. } else {
  391. var moved = (i - out.o[i].row - added);
  392. ao.push(tag((moved>0) ? 'del' : '',out.o[i].text));
  393. }
  394. }
  395. var removed = 0;
  396. for ( var i = 0; i < out.n.length; i++ ) {
  397. if (out.n[i].text == null) {
  398. ac.push(tag('ins',out.n[i]));
  399. an.push(tag('ins',out.n[i]));
  400. } else {
  401. var moved = (i - out.n[i].row + removed);
  402. an.push(tag((moved<0)?'ins':'', out.n[i].text));
  403. ac.push(escape(out.n[i].text));
  404. for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
  405. ac.push(tag('del',out.o[n])); removed++;
  406. }
  407. }
  408. }
  409. }
  410. return [
  411. ac.join(""), // anotated combined additions and deletions
  412. ao.join(""), // old with highlighted deletions
  413. an.join("") // new with highlighted additions
  414. ];
  415. }
  416. function diff( o, n ) {
  417. var ns = {}, os = {}, k = null, i = 0;
  418. for ( var i = 0; i < n.length; i++ ) {
  419. k = '"' + n[i]; // prefix keys with a quote to not collide with Object's internal keys, e.g. '__proto__' or 'constructor'
  420. if ( ns[k] === undefined )
  421. ns[k] = { rows: [], o: null };
  422. ns[k].rows.push( i );
  423. }
  424. for ( var i = 0; i < o.length; i++ ) {
  425. k = '"' + o[i]
  426. if ( os[k] === undefined )
  427. os[k] = { rows: [], n: null };
  428. os[k].rows.push( i );
  429. }
  430. for ( var k in ns ) {
  431. if ( ns[k].rows.length == 1 && os[k] !== undefined && os[k].rows.length == 1 ) {
  432. n[ ns[k].rows[0] ] = { text: n[ ns[k].rows[0] ], row: os[k].rows[0] };
  433. o[ os[k].rows[0] ] = { text: o[ os[k].rows[0] ], row: ns[k].rows[0] };
  434. }
  435. }
  436. for ( var i = 0; i < n.length - 1; i++ ) {
  437. if ( n[i].text != null && n[i+1].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null &&
  438. n[i+1] == o[ n[i].row + 1 ] ) {
  439. n[i+1] = { text: n[i+1], row: n[i].row + 1 };
  440. o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 };
  441. }
  442. }
  443. for ( var i = n.length - 1; i > 0; i-- ) {
  444. if ( n[i].text != null && n[i-1].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null &&
  445. n[i-1] == o[ n[i].row - 1 ] ) {
  446. n[i-1] = { text: n[i-1], row: n[i].row - 1 };
  447. o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1 };
  448. }
  449. }
  450. return { o: o, n: n };
  451. }
  452. })();