Show coloured diffs in blogs

A quick Google search for JavaScript code to format context-diffs as coloured side-by-side diffs didn't turn up anything obvious (admittedly I haven't the slightest clue where to look for this sort of stuff on the 'net).

So I went and quickly wrote my own formatting function which formats a given context diff and returns a table-formatted side-by-side diff:

function formatDiff(textContent) {
    "use strict";

    function E(name, attrs) {
        var arg, elem = document.createElement(name), prop;

        for (prop in attrs) {
            elem.setAttribute(prop, attrs[prop]);
        }
        for (arg = 2; arg != arguments.length; ++arg) {
            elem.appendChild(arguments[arg]);
        }

        return elem;
    }

    var td = E.bind(undefined, "td");
    var th = E.bind(undefined, "th", {});
    var tr = E.bind(undefined, "tr", {});
    var T = document.createTextNode.bind(document);

    var res = E("table", {"class": "diff"});

    var sects = {"unchanged": [], "removed": [], "added": []};

    var lines = textContent.split("\n");
    lines.push("");
    lines.forEach(function (l) {
        var leftAttrs = {}, rightAttrs = {};

        var lineType = !l.length ? "eof"
                     : "--- " === l.substr(0, 4) ? "oldfile"
                     : "+++ " === l.substr(0, 4) ? "newfile"
                     : "@@ " === l.substr(0, 3) ? "range"
                     : "-" === l[0] ? "removed"
                     : "+" === l[0] ? "added"
                                    : "unchanged";

        /*
         * An "unchanged" section ends at a line that isn't "unchanged".
         */
        if (sects["unchanged"].length && "unchanged" !== lineType) {
            res.appendChild(tr(
                td({}, T(sects["unchanged"].join("\n"))),
                td({}, T(sects["unchanged"].join("\n")))
            ));

            sects["unchanged"].length = 0;
        }

        /*
         * An added/removed section ends
         *  o at a line that isn't "added" if there are "added" lines,
         *  o at a line that isn't "added" or "removed" if there are "removed" lines.
         */
        if (sects["added"].length && "added" !== lineType ||
            sects["removed"].length && "removed" !== lineType && "added" !== lineType)
        {
            if (sects["added"].length && sects["removed"].length) {
                leftAttrs["class"] = rightAttrs["class"] = "diff-changed";
            } else if (sects["removed"].length) {
                leftAttrs["class"] = "diff-removed";
            } else {
                rightAttrs["class"] = "diff-added";
            }

            res.appendChild(tr(
                td(leftAttrs, T(sects["removed"].join("\n"))),
                td(rightAttrs, T(sects["added"].join("\n")))
            ));

            sects["added"].length = sects["removed"].length = 0;
        }

        if ("oldfile" === lineType) {
            res.appendChild(tr(th(T(l.substring(4)))));
        } else if ("newfile" === lineType) {
            res.lastChild.appendChild(th(T(l.substring(4))));
        } else if ("range" === lineType) {
            var ds = td(
                {"class": "diff-section", "colspan": 2},
                T(l.split("@@").join(" "))
            );
            res.appendChild(tr(ds));
        } else if ("eof" !== lineType) {
            sects[lineType].push(l.substring(1));
        }
    });

    return res;
}

// Clone the HTML node collection before modifying the DOM.
var diffs = Array.prototype.slice.call(document.getElementsByClassName("diff"));
diffs.forEach(function (diff) {
    diff.replaceChild(formatDiff(diff.firstChild.textContent), diff.firstChild);
});
The code at the bottom reformats any elements of class diff: see it in action!

Here's a small unit test:

--- file1
+++ file2
@@ -0,+1 @@ something
 0
 1
-2
+2
+3
-3
-4
+4
-5
 6
+7
 8

There are already various source code highlighters out there so arguably this posting could be made to look a lot nicer. :-)

Comments