hello, world
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ef5f69b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.swp
+node_modules/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..568956a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,15 @@
+TEST_TIMEOUT = 2000
+TEST_REPORTER = spec
+
+lib/difflib.js: src/difflib.coffee
+	@coffee -c -o lib src
+
+test:
+	@NODE_ENV=test \
+		node_modules/.bin/mocha \
+			--ui qunit \
+			--require should \
+			--timeout $(TEST_TIMEOUT) \
+			--reporter $(TEST_REPORTER) 
+
+.PHONY: test
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..825fc9a
--- /dev/null
+++ b/index.js
@@ -0,0 +1 @@
+module.exports = require('./lib/difflib');
diff --git a/lib/difflib.js b/lib/difflib.js
new file mode 100644
index 0000000..8c64b99
--- /dev/null
+++ b/lib/difflib.js
@@ -0,0 +1,565 @@
+
+/*
+Module difflib -- helpers for computing deltas between objects.
+
+Function get_close_matches(word, possibilities, n=3, cutoff=0.6):
+    Use SequenceMatcher to return list of the best "good enough" matches.
+
+Function context_diff(a, b):
+    For two lists of strings, return a delta in context diff format.
+
+Function ndiff(a, b):
+    Return a delta: the difference between `a` and `b` (lists of strings).
+
+Function restore(delta, which):
+    Return one of the two sequences that generated an ndiff delta.
+
+Function unified_diff(a, b):
+    For two lists of strings, return a delta in unified diff format.
+
+Class SequenceMatcher:
+    A flexible class for comparing pairs of sequences of any type.
+
+Class Differ:
+    For producing human-readable deltas from sequences of lines of text.
+
+Class HtmlDiff:
+    For producing HTML side by side comparison with change highlights.
+*/
+
+(function() {
+  var Differ, Heap, SequenceMatcher, arrayCmp, assert, calculateRatio, exports, floor, getCloseMatches, log, max, min, _countLeading;
+
+  floor = Math.floor, max = Math.max, min = Math.min;
+
+  Heap = require('heap').Heap;
+
+  assert = require('assert');
+
+  log = console.log.bind(console);
+
+  calculateRatio = function(matches, length) {
+    if (length) {
+      return 2.0 * matches / length;
+    } else {
+      return 1.0;
+    }
+  };
+
+  arrayCmp = function(a, b) {
+    var i, _ref;
+    for (i = 0, _ref = max(a.length, b.length); 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) {
+      if (a[i] < b[i]) return -1;
+      if (a[i] > b[i]) return 1;
+    }
+    return 0;
+  };
+
+  SequenceMatcher = (function() {
+
+    function SequenceMatcher(isjunk, a, b, autojunk) {
+      this.isjunk = isjunk;
+      if (a == null) a = '';
+      if (b == null) b = '';
+      this.autojunk = autojunk != null ? autojunk : true;
+      this.a = this.b = null;
+      this.setSeqs(a, b);
+    }
+
+    /* 
+    Set the two sequences to be compared.
+    */
+
+    SequenceMatcher.prototype.setSeqs = function(a, b) {
+      this.setSeq1(a);
+      return this.setSeq2(b);
+    };
+
+    /* 
+    Set the first sequence to be compared. 
+    The second sequence to be compared is not changed.
+    */
+
+    SequenceMatcher.prototype.setSeq1 = function(a) {
+      if (a === this.a) return;
+      this.a = a;
+      return this.matchingBlocks = this.opcodes = null;
+    };
+
+    /*
+      Set the second sequence to be compared. 
+      The first sequence to be compared is not changed.
+    */
+
+    SequenceMatcher.prototype.setSeq2 = function(b) {
+      if (b === this.b) return;
+      this.b = b;
+      this.matchingBlocks = this.opcodes = null;
+      this.fullbcount = null;
+      return this._chainB();
+    };
+
+    SequenceMatcher.prototype._chainB = function() {
+      var b, b2j, elt, i, idxs, indices, isjunk, junk, n, ntest, popular, _i, _len, _len2, _ref;
+      b = this.b;
+      this.b2j = b2j = {};
+      for (i = 0, _len = b.length; i < _len; i++) {
+        elt = b[i];
+        indices = elt in b2j ? b2j[elt] : b2j[elt] = [];
+        indices.push(i);
+      }
+      junk = {};
+      isjunk = this.isjunk;
+      if (isjunk) {
+        _ref = Object.keys(b2j);
+        for (_i = 0, _len2 = _ref.length; _i < _len2; _i++) {
+          elt = _ref[_i];
+          if (isjunk(elt)) {
+            junk[elt] = true;
+            delete b2j[elt];
+          }
+        }
+      }
+      popular = {};
+      n = b.length;
+      if (this.autojunk && n >= 200) {
+        ntest = floor(n / 100) + 1;
+        for (elt in b2j) {
+          idxs = b2j[elt];
+          if (idxs.length > ntest) {
+            popular[elt] = true;
+            delete b2j[elt];
+          }
+        }
+      }
+      this.isbjunk = function(b) {
+        return b in junk;
+      };
+      return this.isbpopular = function(b) {
+        return b in popular;
+      };
+    };
+
+    /* 
+    Find longest matching block in a[alo:ahi] and b[blo:bhi].
+    */
+
+    SequenceMatcher.prototype.findLongestMatch = function(alo, ahi, blo, bhi) {
+      var a, b, b2j, besti, bestj, bestsize, i, isbjunk, j, j2len, k, newj2len, _i, _len, _ref, _ref2, _ref3, _ref4, _ref5, _ref6;
+      _ref = [this.a, this.b, this.b2j, this.isbjunk], a = _ref[0], b = _ref[1], b2j = _ref[2], isbjunk = _ref[3];
+      _ref2 = [alo, blo, 0], besti = _ref2[0], bestj = _ref2[1], bestsize = _ref2[2];
+      j2len = {};
+      for (i = alo; alo <= ahi ? i < ahi : i > ahi; alo <= ahi ? i++ : i--) {
+        newj2len = {};
+        _ref3 = (a[i] in b2j ? b2j[a[i]] : []);
+        for (_i = 0, _len = _ref3.length; _i < _len; _i++) {
+          j = _ref3[_i];
+          if (j < blo) continue;
+          if (j >= bhi) break;
+          k = newj2len[j] = (j2len[j - 1] || 0) + 1;
+          if (k > bestsize) {
+            _ref4 = [i - k + 1, j - k + 1, k], besti = _ref4[0], bestj = _ref4[1], bestsize = _ref4[2];
+          }
+        }
+        j2len = newj2len;
+      }
+      while (besti > alo && bestj > blo && !isbjunk(b[bestj - 1]) && a[besti - 1] === b[bestj - 1]) {
+        _ref5 = [besti - 1, bestj - 1, bestsize + 1], besti = _ref5[0], bestj = _ref5[1], bestsize = _ref5[2];
+      }
+      while (besti + bestsize < ahi && bestj + bestsize < bhi && !isbjunk(b[bestj + bestsize]) && a[besti + bestsize] === b[bestj + bestsize]) {
+        bestsize++;
+      }
+      while (besti > alo && bestj > blo && isbjunk(b[bestj - 1]) && a[besti - 1] === b[bestj - 1]) {
+        _ref6 = [besti - 1, bestj - 1, bestsize + 1], besti = _ref6[0], bestj = _ref6[1], bestsize = _ref6[2];
+      }
+      while (besti + bestsize < ahi && bestj + bestsize < bhi && isbjunk(b[bestj + bestsize]) && a[besti + bestsize] === b[bestj + bestsize]) {
+        bestsize++;
+      }
+      return [besti, bestj, bestsize];
+    };
+
+    /*
+      Return list of matches describing matching subsequences.
+    */
+
+    SequenceMatcher.prototype.getMatchingBlocks = function() {
+      var ahi, alo, bhi, blo, i, i1, i2, j, j1, j2, k, k1, k2, la, lb, matchingBlocks, nonAdjacent, queue, x, _i, _len, _ref, _ref2, _ref3, _ref4, _ref5;
+      if (this.matchingBlocks) return this.matchingBlocks;
+      _ref = [this.a.length, this.b.length], la = _ref[0], lb = _ref[1];
+      queue = [[0, la, 0, lb]];
+      matchingBlocks = [];
+      while (queue.length) {
+        _ref2 = queue.pop(), alo = _ref2[0], ahi = _ref2[1], blo = _ref2[2], bhi = _ref2[3];
+        _ref3 = x = this.findLongestMatch(alo, ahi, blo, bhi), i = _ref3[0], j = _ref3[1], k = _ref3[2];
+        if (k) {
+          matchingBlocks.push(x);
+          if (alo < i && blo < j) queue.push([alo, i, blo, j]);
+          if (i + k < ahi && j + k < bhi) queue.push([i + k, ahi, j + k, bhi]);
+        }
+      }
+      matchingBlocks.sort(arrayCmp);
+      i1 = j1 = k1 = 0;
+      nonAdjacent = [];
+      for (_i = 0, _len = matchingBlocks.length; _i < _len; _i++) {
+        _ref4 = matchingBlocks[_i], i2 = _ref4[0], j2 = _ref4[1], k2 = _ref4[2];
+        if (i1 + k1 === i2 && j1 + k1 === j2) {
+          k1 += k2;
+        } else {
+          if (k1) nonAdjacent.push([i1, j1, k1]);
+          _ref5 = [i2, j2, k2], i1 = _ref5[0], j1 = _ref5[1], k1 = _ref5[2];
+        }
+      }
+      if (k1) nonAdjacent.push([i1, j1, k1]);
+      nonAdjacent.push([la, lb, 0]);
+      return this.matchingBlocks = nonAdjacent;
+    };
+
+    /* 
+    Return list of 5-tuples describing how to turn a into b
+    */
+
+    SequenceMatcher.prototype.getOpcodes = function() {
+      var ai, answer, bj, i, j, size, tag, _i, _len, _ref, _ref2, _ref3;
+      if (this.opcodes) return this.opcodes;
+      i = j = 0;
+      this.opcodes = answer = [];
+      _ref = this.getMatchingBlocks();
+      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+        _ref2 = _ref[_i], ai = _ref2[0], bj = _ref2[1], size = _ref2[2];
+        tag = '';
+        if (i < ai && j < bj) {
+          tag = 'replace';
+        } else if (i < ai) {
+          tag = 'delete';
+        } else if (j < bj) {
+          tag = 'insert';
+        }
+        if (tag) answer.push([tag, i, ai, j, bj]);
+        _ref3 = [ai + size, bj + size], i = _ref3[0], j = _ref3[1];
+        if (size) answer.push(['equal', ai, i, bj, j]);
+      }
+      return answer;
+    };
+
+    /* 
+    Isolate change clusters by eliminating ranges with no changes.
+    XXX: not match
+    */
+
+    SequenceMatcher.prototype.getGroupedOpcodes = function(n) {
+      var codes, group, groups, i1, i2, j1, j2, nn, tag, _i, _len, _ref, _ref2, _ref3, _ref4;
+      if (n == null) n = 3;
+      codes = this.getOpcodes();
+      if (!codes.length) codes = [['equal', 0, 1, 0, 1]];
+      if (codes[0][0] === 'equal') {
+        _ref = codes[0], tag = _ref[0], i1 = _ref[1], i2 = _ref[2], j1 = _ref[3], j2 = _ref[4];
+        codes[0] = [tag, max(i1, i2 - n), i2, max(j1, j2 - n), j2];
+      }
+      if (codes[codes.length - 1][0] === 'equal') {
+        _ref2 = codes[codes.length - 1], tag = _ref2[0], i1 = _ref2[1], i2 = _ref2[2], j1 = _ref2[3], j2 = _ref2[4];
+        codes[codes.length - 1] = [tag, i1, min(i2, i1 + n), j1, min(j2, j1 + n)];
+      }
+      nn = n + n;
+      groups = [];
+      group = [];
+      for (_i = 0, _len = codes.length; _i < _len; _i++) {
+        _ref3 = codes[_i], tag = _ref3[0], i1 = _ref3[1], i2 = _ref3[2], j1 = _ref3[3], j2 = _ref3[4];
+        if (tag === 'equal' && i2 - i1 > nn) {
+          group.push([tag, i1, min(i2, i1 + n), j1, min(j2, j1 + n)]);
+          groups.push(group);
+          group = [];
+          _ref4 = [max(i1, i2 - n), max(j1, j2 - n)], i1 = _ref4[0], j1 = _ref4[1];
+        }
+        group.push([tag, i1, i2, j1, j2]);
+      }
+      if (group.length && !(group.length === 1 && group[0][0] === 'equal')) {
+        groups.push(group);
+      }
+      return groups;
+    };
+
+    SequenceMatcher.prototype.ratio = function() {
+      var matches;
+      matches = this.getMatchingBlocks().reduce((function(sum, match) {
+        return sum += match[2];
+      }), 0);
+      return calculateRatio(matches, this.a.length + this.b.length);
+    };
+
+    SequenceMatcher.prototype.quickRatio = function() {
+      var avail, elt, fullbcount, matches, numb, _i, _j, _len, _len2, _ref, _ref2;
+      if (!this.fullbcount) {
+        this.fullbcount = fullbcount = {};
+        _ref = this.b;
+        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+          elt = _ref[_i];
+          fullbcount[elt] = (fullbcount[elt] || 0) + 1;
+        }
+      }
+      fullbcount = this.fullbcount;
+      avail = {};
+      matches = 0;
+      _ref2 = this.a;
+      for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
+        elt = _ref2[_j];
+        if (elt in avail) {
+          numb = avail[elt];
+        } else {
+          numb = fullbcount[elt] || 0;
+        }
+        avail[elt] = numb - 1;
+        if (numb > 0) matches++;
+      }
+      return calculateRatio(matches, this.a.length + this.b.length);
+    };
+
+    SequenceMatcher.prototype.realQuickRatio = function() {
+      var la, lb, _ref;
+      _ref = [this.a.length, this.b.length], la = _ref[0], lb = _ref[1];
+      return calculateRatio(min(la, lb), la + lb);
+    };
+
+    return SequenceMatcher;
+
+  })();
+
+  getCloseMatches = function(word, possibilities, n, cutoff) {
+    var result, s, score, x, _i, _j, _len, _len2, _ref, _results;
+    if (n == null) n = 3;
+    if (cutoff == null) cutoff = 0.6;
+    if (!(n > 0)) throw new Error("n must be > 0: (" + n + ")");
+    if (!((0.0 <= cutoff && cutoff <= 1.0))) {
+      throw new Error("cutoff must be in [0.0, 1.0]: (" + cutoff + ")");
+    }
+    result = [];
+    s = new SequenceMatcher();
+    s.setSeq2(word);
+    for (_i = 0, _len = possibilities.length; _i < _len; _i++) {
+      x = possibilities[_i];
+      s.setSeq1(x);
+      if (s.realQuickRatio() >= cutoff && s.quickRatio() >= cutoff && s.ratio() >= cutoff) {
+        result.push([s.ratio(), x]);
+      }
+    }
+    result = Heap.nlargest(n, result);
+    _results = [];
+    for (_j = 0, _len2 = result.length; _j < _len2; _j++) {
+      _ref = result[_j], score = _ref[0], x = _ref[1];
+      _results.push(x);
+    }
+    return _results;
+  };
+
+  /*
+  Return number of `ch` characters at the start of `line`.
+  */
+
+  _countLeading = function(line, ch) {
+    var i, n, _ref;
+    _ref = [0, line.length], i = _ref[0], n = _ref[1];
+    while (i < n && line[i] === ch) {
+      i++;
+    }
+    return i;
+  };
+
+  Differ = (function() {
+
+    function Differ(linejunk, charjunk) {
+      this.linejunk = linejunk;
+      this.charjunk = charjunk;
+    }
+
+    Differ.prototype.compare = function(a, b) {
+      var ahi, alo, bhi, blo, cruncher, g, line, lines, tag, _i, _j, _len, _len2, _ref, _ref2;
+      cruncher = new SequenceMatcher(this.linejunk, a, b);
+      lines = [];
+      _ref = cruncher.getOpcodes();
+      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+        _ref2 = _ref[_i], tag = _ref2[0], alo = _ref2[1], ahi = _ref2[2], blo = _ref2[3], bhi = _ref2[4];
+        switch (tag) {
+          case 'replace':
+            g = this._fancyReplace(a, alo, ahi, b, blo, bhi);
+            break;
+          case 'delete':
+            g = this._dump('-', a, alo, ahi);
+            break;
+          case 'insert':
+            g = this._dump('+', b, blo, bhi);
+            break;
+          case 'equal':
+            g = this._dump(' ', a, alo, ahi);
+            break;
+          default:
+            throw new Error("unknow tag (" + tag + ")");
+        }
+        for (_j = 0, _len2 = g.length; _j < _len2; _j++) {
+          line = g[_j];
+          lines.push(line);
+        }
+      }
+      return lines;
+    };
+
+    Differ.prototype._dump = function(tag, x, lo, hi) {
+      var i, _results;
+      _results = [];
+      for (i = lo; lo <= hi ? i < hi : i > hi; lo <= hi ? i++ : i--) {
+        _results.push("" + tag + " " + x[i]);
+      }
+      return _results;
+    };
+
+    Differ.prototype._plainReplace = function(a, alo, ahi, b, blo, bhi) {
+      var first, g, line, lines, second, _i, _j, _len, _len2, _ref;
+      assert(alo < ahi && blo < bhi);
+      if (bhi - blo < ahi - alo) {
+        first = this._dump('+', b, blo, bhi);
+        second = this._dump('-', a, alo, ahi);
+      } else {
+        first = this._dump('-', a, alo, ahi);
+        second = this._dump('+', b, blo, bhi);
+      }
+      lines = [];
+      _ref = [first, second];
+      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+        g = _ref[_i];
+        for (_j = 0, _len2 = g.length; _j < _len2; _j++) {
+          line = g[_j];
+          lines.push(line);
+        }
+      }
+      return lines;
+    };
+
+    Differ.prototype._fancyReplace = function(a, alo, ahi, b, blo, bhi) {
+      var aelt, ai, ai1, ai2, atags, belt, bestRatio, besti, bestj, bj, bj1, bj2, btags, cruncher, cutoff, eqi, eqj, i, j, la, lb, line, lines, tag, _i, _j, _k, _l, _len, _len2, _len3, _len4, _len5, _m, _ref, _ref10, _ref11, _ref12, _ref13, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9;
+      _ref = [0.74, 0.75], bestRatio = _ref[0], cutoff = _ref[1];
+      cruncher = new SequenceMatcher(this.charjunk);
+      _ref2 = [null, null], eqi = _ref2[0], eqj = _ref2[1];
+      lines = [];
+      for (j = blo; blo <= bhi ? j < bhi : j > bhi; blo <= bhi ? j++ : j--) {
+        bj = b[j];
+        cruncher.setSeq2(bj);
+        for (i = alo; alo <= ahi ? i < ahi : i > ahi; alo <= ahi ? i++ : i--) {
+          ai = a[i];
+          if (ai === bj) {
+            if (eqi === null) _ref3 = [i, j], eqi = _ref3[0], eqj = _ref3[1];
+            continue;
+          }
+          cruncher.setSeq1(ai);
+          if (cruncher.realQuickRatio() > bestRatio && cruncher.quickRatio() > bestRatio && cruncher.ratio() > bestRatio) {
+            _ref4 = [cruncher.ratio(), i, j], bestRatio = _ref4[0], besti = _ref4[1], bestj = _ref4[2];
+          }
+        }
+      }
+      if (bestRatio < cutoff) {
+        if (eqi === null) {
+          _ref5 = this._plainReplace(a, alo, ahi, b, blo, bhi);
+          for (_i = 0, _len = _ref5.length; _i < _len; _i++) {
+            line = _ref5[_i];
+            lines.push(line);
+          }
+          return lines;
+        }
+        _ref6 = [eqi, eqj, 1.0], besti = _ref6[0], bestj = _ref6[1], bestRatio = _ref6[2];
+      } else {
+        eqi = null;
+      }
+      _ref7 = this._fancyHelper(a, alo, besti, b, blo, bestj);
+      for (_j = 0, _len2 = _ref7.length; _j < _len2; _j++) {
+        line = _ref7[_j];
+        lines.push(line);
+      }
+      _ref8 = [a[besti], b[bestj]], aelt = _ref8[0], belt = _ref8[1];
+      if (eqi === null) {
+        atags = btags = '';
+        cruncher.setSeqs(aelt, belt);
+        _ref9 = cruncher.getOpcodes();
+        for (_k = 0, _len3 = _ref9.length; _k < _len3; _k++) {
+          _ref10 = _ref9[_k], tag = _ref10[0], ai1 = _ref10[1], ai2 = _ref10[2], bj1 = _ref10[3], bj2 = _ref10[4];
+          _ref11 = [ai2 - ai1, bj2 - bj1], la = _ref11[0], lb = _ref11[1];
+          switch (tag) {
+            case 'replace':
+              atags += Array(la + 1).join('^');
+              btags += Array(lb + 1).join('^');
+              break;
+            case 'delete':
+              atags += Array(la + 1).join('-');
+              break;
+            case 'insert':
+              btags += Array(lb + 1).join('+');
+              break;
+            case 'equal':
+              atags += Array(la + 1).join(' ');
+              btags += Array(lb + 1).join(' ');
+              break;
+            default:
+              throw new Error("unknow tag (" + tag + ")");
+          }
+        }
+        _ref12 = this._qformat(aelt, belt, atags, btags);
+        for (_l = 0, _len4 = _ref12.length; _l < _len4; _l++) {
+          line = _ref12[_l];
+          lines.push(line);
+        }
+      } else {
+        lines.push('  ' + aelt);
+      }
+      _ref13 = this._fancyHelper(a, besti + 1, ahi, b, bestj + 1, bhi);
+      for (_m = 0, _len5 = _ref13.length; _m < _len5; _m++) {
+        line = _ref13[_m];
+        lines.push(line);
+      }
+      return lines;
+    };
+
+    Differ.prototype._fancyHelper = function(a, alo, ahi, b, blo, bhi) {
+      var g;
+      g = [];
+      if (alo < ahi) {
+        if (blo < bhi) {
+          g = this._fancyReplace(a, alo, ahi, b, blo, bhi);
+        } else {
+          g = this._dump('-', a, alo, ahi);
+        }
+      } else if (blo < bhi) {
+        g = this._dump('+', b, blo, bhi);
+      }
+      return g;
+    };
+
+    Differ.prototype._qformat = function(aline, bline, atags, btags) {
+      var common, lines;
+      lines = [];
+      common = min(_countLeading(aline, '\t'), _countLeading(bline, '\t'));
+      common = min(common, _countLeading(atags.slice(0, common), ' '));
+      common = min(common, _countLeading(btags.slice(0, common), ' '));
+      atags = atags.slice(common).trimRight();
+      btags = btags.slice(common).trimRight();
+      lines.push('- ' + aline);
+      if (atags.length) {
+        lines.push("? " + (Array(common + 1).join('\t')) + atags + "\n");
+      }
+      lines.push('+ ' + bline);
+      if (btags.length) {
+        lines.push("? " + (Array(common + 1).join('\t')) + btags + "\n");
+      }
+      return lines;
+    };
+
+    return Differ;
+
+  })();
+
+  exports = (typeof module !== "undefined" && module !== null ? module.exports : void 0) || (window.difflib = {});
+
+  exports.SequenceMatcher = SequenceMatcher;
+
+  exports.getCloseMatches = getCloseMatches;
+
+  exports._countLeading = _countLeading;
+
+  exports.Differ = Differ;
+
+}).call(this);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f8ff169
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+    "name"            : "difflib"
+  , "version"         : "0.1.0"
+  , "description"     : ""
+  , "homepage"        : "https://github.com/qiao/difflib.js"
+  , "keywords"        : ["diff"]
+  , "author"          : "Xueqiao Xu <xueqiaoxu@gmail.com>"
+  , "main"            : "./index.js"
+  , "dependencies"    : {}
+  , "devDependencies" : {
+      "coffee-script"   : ">= 1.2.0"
+    , "mocha"           : ">= 0.14.0"
+    , "should"          : ">= 0.6.0"
+  }
+  , "repository"      : {
+      "type"          : "git"
+    , "url"           : "git://github.com/qiao/difflib.js.git"
+  }
+  , "licenses"        : [{
+      "type"          :  "PSF"
+    , "url"           : "http://docs.python.org/license.html"
+  }]
+}
diff --git a/src/difflib.coffee b/src/difflib.coffee
new file mode 100644
index 0000000..82cb148
--- /dev/null
+++ b/src/difflib.coffee
@@ -0,0 +1,425 @@
+###
+Module difflib -- helpers for computing deltas between objects.
+
+Function get_close_matches(word, possibilities, n=3, cutoff=0.6):
+    Use SequenceMatcher to return list of the best "good enough" matches.
+
+Function context_diff(a, b):
+    For two lists of strings, return a delta in context diff format.
+
+Function ndiff(a, b):
+    Return a delta: the difference between `a` and `b` (lists of strings).
+
+Function restore(delta, which):
+    Return one of the two sequences that generated an ndiff delta.
+
+Function unified_diff(a, b):
+    For two lists of strings, return a delta in unified diff format.
+
+Class SequenceMatcher:
+    A flexible class for comparing pairs of sequences of any type.
+
+Class Differ:
+    For producing human-readable deltas from sequences of lines of text.
+
+Class HtmlDiff:
+    For producing HTML side by side comparison with change highlights.
+###
+
+
+{floor, max, min} = Math
+{Heap} = require('heap')
+assert = require('assert')
+log = console.log.bind(console)
+
+calculateRatio = (matches, length) ->
+  if length then (2.0 * matches / length) else 1.0
+
+arrayCmp = (a, b) ->
+  for i in [0...max(a.length, b.length)]
+    return -1 if a[i] < b[i]
+    return 1 if a[i] > b[i]
+  0
+      
+
+class SequenceMatcher
+  constructor: (@isjunk, a='', b='', @autojunk=true) ->
+    @a = @b = null
+    @setSeqs(a, b)
+
+  ### 
+  Set the two sequences to be compared. 
+  ###
+  setSeqs: (a, b) ->
+    @setSeq1(a)
+    @setSeq2(b)
+
+  ### 
+  Set the first sequence to be compared. 
+  The second sequence to be compared is not changed.
+  ###
+  setSeq1: (a) ->
+    return if a is @a
+    @a = a
+    @matchingBlocks = @opcodes = null
+
+
+  ###
+  Set the second sequence to be compared. 
+  The first sequence to be compared is not changed.
+  ###
+  setSeq2: (b) ->
+    return if b is @b
+    @b = b
+    @matchingBlocks = @opcodes = null
+    @fullbcount = null
+    @_chainB()
+
+  _chainB: ->
+    b = @b
+    @b2j = b2j = {}
+
+    for elt, i in b
+      indices = if elt of b2j then b2j[elt] else b2j[elt] = []
+      indices.push(i)
+
+    junk = {}
+    isjunk = @isjunk
+    if isjunk
+      for elt in Object.keys(b2j)
+        if isjunk(elt)
+          junk[elt] = true
+          delete b2j[elt]
+
+    popular = {}
+    n = b.length
+    if @autojunk and n >= 200
+      ntest = floor(n / 100) + 1
+      for elt, idxs of b2j
+        if idxs.length > ntest
+          popular[elt] = true
+          delete b2j[elt]
+
+    @isbjunk = (b) -> b of junk
+    @isbpopular = (b) -> b of popular
+
+  ### 
+  Find longest matching block in a[alo:ahi] and b[blo:bhi].  
+  ###
+  findLongestMatch: (alo, ahi, blo, bhi) ->
+    [a, b, b2j, isbjunk] = [@a, @b, @b2j, @isbjunk]
+    [besti, bestj, bestsize] = [alo, blo, 0]
+
+    j2len = {}
+    for i in [alo...ahi]
+      newj2len = {}
+      for j in (if a[i] of b2j then b2j[a[i]] else [])
+        continue if j < blo
+        break if j >= bhi
+        k = newj2len[j] = (j2len[j-1] or 0) + 1
+        if k > bestsize
+          [besti, bestj, bestsize] = [i-k+1,j-k+1,k]
+      j2len = newj2len
+
+    while besti > alo and bestj > blo and
+        not isbjunk(b[bestj-1]) and
+        a[besti-1] is b[bestj-1]
+      [besti, bestj, bestsize] = [besti-1, bestj-1, bestsize+1]
+    while besti+bestsize < ahi and bestj+bestsize < bhi and
+        not isbjunk(b[bestj+bestsize]) and
+        a[besti+bestsize] is b[bestj+bestsize]
+      bestsize++
+
+    while besti > alo and bestj > blo and
+        isbjunk(b[bestj-1]) and
+        a[besti-1] is b[bestj-1]
+      [besti,bestj,bestsize] = [besti-1, bestj-1, bestsize+1]
+    while besti+bestsize < ahi and bestj+bestsize < bhi and
+        isbjunk(b[bestj+bestsize]) and
+        a[besti+bestsize] is b[bestj+bestsize]
+      bestsize++
+
+    [besti, bestj, bestsize]
+
+  ###
+  Return list of matches describing matching subsequences.
+  ###
+  getMatchingBlocks: ->
+    return @matchingBlocks if @matchingBlocks
+    [la, lb] = [@a.length, @b.length]
+
+    queue = [[0, la, 0, lb]]
+    matchingBlocks = []
+    while queue.length
+      [alo, ahi, blo, bhi] = queue.pop()
+      [i, j, k] = x = @findLongestMatch(alo, ahi, blo, bhi)
+
+      if k
+        matchingBlocks.push(x)
+        if alo < i and blo < j
+          queue.push([alo, i, blo, j])
+        if i+k < ahi and j+k < bhi
+          queue.push([i+k, ahi, j+k, bhi])
+    matchingBlocks.sort(arrayCmp)
+
+    i1 = j1 = k1 = 0
+    nonAdjacent = []
+    for [i2, j2, k2] in matchingBlocks
+      if i1 + k1 is i2 and j1 + k1 is j2
+        k1 += k2
+      else
+        if k1
+          nonAdjacent.push([i1, j1, k1])
+        [i1, j1, k1] = [i2, j2, k2]
+    if k1
+      nonAdjacent.push([i1, j1, k1])
+
+    nonAdjacent.push([la, lb, 0])
+    @matchingBlocks = nonAdjacent
+
+  ### 
+  Return list of 5-tuples describing how to turn a into b 
+  ###
+  getOpcodes: ->
+    return @opcodes if @opcodes
+    i = j = 0
+    @opcodes = answer = []
+    for [ai, bj, size] in @getMatchingBlocks()
+      tag = ''
+      if i < ai and j < bj
+        tag = 'replace'
+      else if i < ai
+        tag = 'delete'
+      else if j < bj
+        tag = 'insert'
+      if tag
+        answer.push([tag, i, ai, j, bj])
+      [i, j] = [ai+size, bj+size]
+
+      if size
+        answer.push(['equal', ai, i, bj, j])
+    answer
+
+  ### 
+  Isolate change clusters by eliminating ranges with no changes.
+  XXX: not match
+  ###
+  getGroupedOpcodes: (n=3) ->
+    codes = @getOpcodes()
+    unless codes.length
+      codes = [['equal', 0, 1, 0, 1]]
+    if codes[0][0] is 'equal'
+      [tag, i1, i2, j1, j2] = codes[0]
+      codes[0] = [tag, max(i1, i2-n), i2, max(j1, j2-n), j2]
+    if codes[codes.length-1][0] is 'equal'
+      [tag, i1, i2, j1, j2] = codes[codes.length-1]
+      codes[codes.length-1] = [tag, i1, min(i2, i1+n), j1, min(j2, j1+n)]
+
+    nn = n + n
+    groups = []
+    group = []
+    for [tag, i1, i2, j1, j2] in codes
+      if tag is 'equal' and i2-i1 > nn
+        group.push([tag, i1, min(i2, i1+n), j1, min(j2, j1+n)])
+        groups.push(group)
+        group = []
+        [i1, j1] = [max(i1, i2-n), max(j1, j2-n)]
+      group.push([tag, i1, i2, j1, j2])
+    if group.length and not (group.length is 1 and group[0][0] is 'equal')
+      groups.push(group)
+    groups
+
+  ratio: ->
+    matches = @getMatchingBlocks().reduce ((sum, match) ->
+      sum += match[2]
+    ), 0
+    calculateRatio(matches, @a.length + @b.length)
+
+  quickRatio: ->
+    unless @fullbcount
+      @fullbcount = fullbcount = {}
+      for elt in @b
+        fullbcount[elt] = (fullbcount[elt] or 0) + 1
+
+    fullbcount = @fullbcount
+    avail = {}
+    matches = 0
+    for elt in @a
+      if elt of avail
+        numb = avail[elt]
+      else
+        numb = fullbcount[elt] or 0
+      avail[elt] = numb - 1
+      if numb > 0
+        matches++
+    calculateRatio(matches, @a.length + @b.length)
+
+  realQuickRatio: ->
+    [la, lb] = [@a.length, @b.length]
+    calculateRatio(min(la, lb), la + lb)
+
+getCloseMatches = (word, possibilities, n=3, cutoff=0.6) ->
+  unless n > 0
+    throw new Error("n must be > 0: (#{n})")
+  unless 0.0 <= cutoff <= 1.0
+    throw new Error("cutoff must be in [0.0, 1.0]: (#{cutoff})")
+  result = []
+  s = new SequenceMatcher()
+  s.setSeq2(word)
+  for x in possibilities
+    s.setSeq1(x)
+    if s.realQuickRatio() >= cutoff and
+        s.quickRatio() >= cutoff and
+        s.ratio() >= cutoff
+      result.push([s.ratio(), x])
+
+  result = Heap.nlargest(n, result)
+  (x for [score, x] in result)
+
+###
+Return number of `ch` characters at the start of `line`.
+###
+_countLeading = (line, ch) ->
+  [i, n] = [0, line.length]
+  while i < n and line[i] is ch
+    i++
+  i
+
+
+class Differ
+  constructor: (@linejunk, @charjunk) ->
+
+  compare: (a, b) ->
+    cruncher = new SequenceMatcher(@linejunk, a, b)
+    lines = []
+    for [tag, alo, ahi, blo, bhi] in cruncher.getOpcodes()
+      switch tag
+        when 'replace'
+          g = @_fancyReplace(a, alo, ahi, b, blo, bhi)
+        when 'delete'
+          g = @_dump('-', a, alo, ahi)
+        when 'insert'
+          g = @_dump('+', b, blo, bhi)
+        when 'equal'
+          g = @_dump(' ', a, alo, ahi)
+        else
+          throw new Error("unknow tag (#{tag})")
+      lines.push(line) for line in g
+    lines
+
+  _dump: (tag, x, lo, hi) ->
+    ("#{tag} #{x[i]}" for i in [lo...hi])
+
+  _plainReplace: (a, alo, ahi, b, blo, bhi) ->
+    assert(alo < ahi and blo < bhi)
+    if bhi - blo < ahi - alo
+      first  = @_dump('+', b, blo, bhi)
+      second = @_dump('-', a, alo, ahi)
+    else
+      first  = @_dump('-', a, alo, ahi)
+      second = @_dump('+', b, blo, bhi)
+
+    lines = []
+    lines.push(line) for line in g for g in [first, second]
+    lines
+
+  _fancyReplace: (a, alo, ahi, b, blo, bhi) ->
+    [bestRatio, cutoff] = [0.74, 0.75]
+    cruncher = new SequenceMatcher(@charjunk)
+    [eqi, eqj] = [null, null]
+    lines = []
+
+    for j in [blo...bhi]
+      bj = b[j]
+      cruncher.setSeq2(bj)
+      for i in [alo...ahi]
+        ai = a[i]
+        if ai is bj
+          if eqi is null
+            [eqi, eqj] = [i, j]
+          continue
+        cruncher.setSeq1(ai)
+
+        if cruncher.realQuickRatio() > bestRatio and
+            cruncher.quickRatio() > bestRatio and
+            cruncher.ratio() > bestRatio
+          [bestRatio, besti, bestj] = [cruncher.ratio(), i, j]
+
+    if bestRatio < cutoff
+      if eqi is null
+        for line in @_plainReplace(a, alo, ahi, b, blo, bhi)
+          lines.push(line)
+        return lines
+      [besti, bestj, bestRatio] = [eqi, eqj, 1.0]
+    else
+      eqi = null
+
+    for line in @_fancyHelper(a, alo, besti, b, blo, bestj)
+      lines.push(line)
+
+    [aelt, belt] = [a[besti], b[bestj]]
+    if eqi is null
+      atags = btags = ''
+      cruncher.setSeqs(aelt, belt)
+      for [tag, ai1, ai2, bj1, bj2] in cruncher.getOpcodes()
+        [la, lb] = [ai2 - ai1, bj2 - bj1]
+        switch tag
+          when 'replace'
+            atags += Array(la+1).join('^')
+            btags += Array(lb+1).join('^')
+          when 'delete'
+            atags += Array(la+1).join('-')
+          when 'insert'
+            btags += Array(lb+1).join('+')
+          when 'equal'
+            atags += Array(la+1).join(' ')
+            btags += Array(lb+1).join(' ')
+          else
+            throw new Error("unknow tag (#{tag})")
+      for line in @_qformat(aelt, belt, atags, btags)
+        lines.push(line)
+    else
+      lines.push('  ' + aelt)
+
+    for line in @_fancyHelper(a, besti+1, ahi, b, bestj+1, bhi)
+      lines.push(line)
+
+    lines
+
+  _fancyHelper: (a, alo, ahi, b, blo, bhi) ->
+    g = []
+    if alo < ahi
+      if blo < bhi
+        g = @_fancyReplace(a, alo, ahi, b, blo, bhi)
+      else
+        g = @_dump('-', a, alo, ahi)
+    else if blo < bhi
+      g = @_dump('+', b, blo, bhi)
+    g
+
+  _qformat: (aline, bline, atags, btags) ->
+    lines = []
+
+    common = min(_countLeading(aline, '\t'),
+                 _countLeading(bline, '\t'))
+    common = min(common, _countLeading(atags[0...common], ' '))
+    common = min(common, _countLeading(btags[0...common], ' '))
+    atags = atags[common..].trimRight()
+    btags = btags[common..].trimRight()
+
+    lines.push('- ' + aline)
+    if atags.length
+      lines.push("? #{Array(common+1).join('\t')}#{atags}\n")
+
+    lines.push('+ ' + bline)
+    if btags.length
+      lines.push("? #{Array(common+1).join('\t')}#{btags}\n")
+    lines
+
+
+
+exports = module?.exports or (window.difflib = {})
+exports.SequenceMatcher = SequenceMatcher
+exports.getCloseMatches = getCloseMatches
+exports._countLeading   = _countLeading
+exports.Differ          = Differ
diff --git a/test/Differ.coffee b/test/Differ.coffee
new file mode 100644
index 0000000..cbd1dfe
--- /dev/null
+++ b/test/Differ.coffee
@@ -0,0 +1,39 @@
+{SequenceMatcher, Differ} = require '..'
+
+suite 'Differ'
+
+test '#_qformat', ->
+  d = new Differ
+  results = d._qformat('\tabcDefghiJkl\n', '\tabcdefGhijkl\n',
+                       '  ^ ^  ^      ',   '  ^ ^  ^      ')
+  results.should.eql [
+    '- \tabcDefghiJkl\n',
+    '? \t ^ ^  ^\n',
+    '+ \tabcdefGhijkl\n',
+    '? \t ^ ^  ^\n'
+  ]
+
+test '#_fancyReplace', ->
+  d = new Differ
+  d._fancyReplace(['abcDefghiJkl\n'], 0, 1,
+                  ['abcdefGhijkl\n'], 0, 1).should.eql [
+    '- abcDefghiJkl\n',
+    '?    ^  ^  ^\n',
+    '+ abcdefGhijkl\n',
+    '?    ^  ^  ^\n'
+  ]
+
+test '#compare', ->
+  s = new Differ
+  s.compare(['one\n', 'two\n', 'three\n'],
+            ['ore\n', 'tree\n', 'emu\n']).should.eql [
+    '- one\n',
+    '?  ^\n',
+    '+ ore\n',
+    '?  ^\n',
+    '- two\n',
+    '- three\n',
+    '?  -\n',
+    '+ tree\n',
+    '+ emu\n'
+  ]
diff --git a/test/SequenceMatcher.coffee b/test/SequenceMatcher.coffee
new file mode 100644
index 0000000..b034280
--- /dev/null
+++ b/test/SequenceMatcher.coffee
@@ -0,0 +1,108 @@
+{SequenceMatcher} = require '..'
+
+suite 'SequenceMatcher'
+
+test '#setSeqs', ->
+  s = new SequenceMatcher()
+  s.setSeqs('abcd', 'bcde')
+  s.ratio().should.eql 0.75
+
+test '#setSeq1', ->
+  s = new SequenceMatcher(null, 'abcd', 'bcde')
+  s.ratio().should.eql 0.75
+  s.setSeq1('bcde')
+  s.ratio().should.eql 1.0
+
+test '#setSeq2', ->
+  s = new SequenceMatcher(null, 'abcd', 'bcde')
+  s.ratio().should.eql 0.75
+  s.setSeq2('abcd')
+  s.ratio().should.eql 1.0
+
+test '#findLongestMatch', ->
+  isjunk = (x) -> x is ' '
+  s = new SequenceMatcher(isjunk, ' abcd', 'abcd abcd')
+  m = s.findLongestMatch(0, 5, 0, 9)
+  m.should.eql [1, 0, 4]
+
+  s = new SequenceMatcher(null, 'ab', 'c')
+  m = s.findLongestMatch(0, 2, 0, 1)
+  m.should.eql [0, 0, 0]
+
+test '#getMatchingBlocks', ->
+  s = new SequenceMatcher(null, 'abxcd', 'abcd')
+  ms = s.getMatchingBlocks()
+  ms.should.eql [[0, 0, 2], [3, 2, 2], [5, 4, 0]]
+
+  isjunk = (x) -> x is ' '
+  s = new SequenceMatcher(isjunk,
+                          'private Thread currentThread;',
+                          'private volatile Thread currentThread;')
+  s.getMatchingBlocks().should.eql [ [0, 0, 8], [8, 17, 21], [29, 38, 0] ]
+
+test '#getOpcodes', ->
+  s = new SequenceMatcher(null, 'qabxcd', 'abycdf')
+  s.getOpcodes().should.eql [
+     [ 'delete'  , 0 , 1 , 0 , 0 ] ,
+     [ 'equal'   , 1 , 3 , 0 , 2 ] ,
+     [ 'replace' , 3 , 4 , 2 , 3 ] ,
+     [ 'equal'   , 4 , 6 , 3 , 5 ] ,
+     [ 'insert'  , 6 , 6 , 5 , 6 ]
+  ]
+
+  isjunk = (x) -> x is ' '
+  s = new SequenceMatcher(isjunk,
+                          'private Thread currentThread;',
+                          'private volatile Thread currentThread;')
+
+  s.getOpcodes().should.eql [
+    ['equal', 0, 8, 0, 8],
+    ['insert', 8, 8, 8, 17],
+    ['equal', 8, 29, 17, 38]
+  ]
+
+test '#getGroupedOpcodes', ->
+  a = [1...40].map(String)
+  b = a.slice()
+  b[8...8] = 'i'
+  b[20] += 'x'
+  b[23...28] = []
+  b[30] += 'y'
+  s = new SequenceMatcher(null, a, b)
+  s.getGroupedOpcodes().should.eql [
+    [
+      [ 'equal'  , 5 , 8  , 5 , 8 ],
+      [ 'insert' , 8 , 8  , 8 , 9 ],
+      [ 'equal'  , 8 , 11 , 9 , 12 ]
+    ],
+    [
+      [ 'equal'   , 16 , 19 , 17 , 20 ],
+      [ 'replace' , 19 , 20 , 20 , 21 ],
+      [ 'equal'   , 20 , 22 , 21 , 23 ],
+      [ 'delete'  , 22 , 27 , 23 , 23 ],
+      [ 'equal'   , 27 , 30 , 23 , 26 ]
+    ],
+    [
+      [ 'equal'   , 31 , 34 , 27 , 30 ],
+      [ 'replace' , 34 , 35 , 30 , 31 ],
+      [ 'equal'   , 35 , 38 , 31 , 34 ]
+    ]
+  ]
+
+test '#ratio', ->
+  s = new SequenceMatcher(null, 'abcd', 'bcde')
+  s.ratio().should.equal 0.75
+
+  isjunk = (x) -> x is ' '
+  s = new SequenceMatcher(isjunk,
+                          'private Thread currentThread;',
+                          'private volatile Thread currentThread;')
+  s.ratio().toPrecision(3).should.eql '0.866'
+
+test '#quickRatio', ->
+  s = new SequenceMatcher(null, 'abcd', 'bcde')
+  s.quickRatio().should.equal 0.75
+
+test '#realQuickRatio', ->
+  s = new SequenceMatcher(null, 'abcd', 'bcde')
+  s.realQuickRatio().should.equal 1.0
diff --git a/test/global.coffee b/test/global.coffee
new file mode 100644
index 0000000..2664b24
--- /dev/null
+++ b/test/global.coffee
@@ -0,0 +1,15 @@
+{getCloseMatches, _countLeading} = require '..'
+
+suite 'global'
+
+test '.getCloseMatches', ->
+ getCloseMatches('appel', ['ape', 'apple', 'peach', 'puppy'])
+   .should.eql ['apple', 'ape']
+ 
+ KEYWORDS = require('coffee-script').RESERVED
+ getCloseMatches('wheel', KEYWORDS).should.eql ['when', 'while']
+ getCloseMatches('accost', KEYWORDS).should.eql ['const']
+
+test '._countLeading', ->
+  _countLeading('   abc', ' ').should.eql 3
+