[Pkg-javascript-commits] [node-recast] 01/03: Imported Upstream version 0.10.32

Julien Puydt julien.puydt at laposte.net
Tue Oct 20 06:13:36 UTC 2015


This is an automated email from the git hooks/post-receive script.

jpuydt-guest pushed a commit to branch master
in repository node-recast.

commit af1f28024344818c4136289c18db330f8b6cb6af
Author: Julien Puydt <julien.puydt at laposte.net>
Date:   Sun Oct 18 16:59:09 2015 +0200

    Imported Upstream version 0.10.32
---
 .editorconfig             |    3 +
 .gitignore                |    1 +
 .npmignore                |    2 +
 .travis.yml               |   10 +
 LICENSE                   |   20 +
 README.md                 |  142 ++++
 example/add-braces        |   44 ++
 example/generic-identity  |   17 +
 example/identity          |    8 +
 example/to-while          |   84 +++
 lib/comments.js           |  350 +++++++++
 lib/fast-path.js          |  472 ++++++++++++
 lib/lines.js              |  854 ++++++++++++++++++++++
 lib/mapping.js            |  277 +++++++
 lib/options.js            |   95 +++
 lib/parser.js             |  142 ++++
 lib/patcher.js            |  480 ++++++++++++
 lib/printer.js            | 1763 +++++++++++++++++++++++++++++++++++++++++++++
 lib/types.js              |    5 +
 lib/util.js               |  150 ++++
 main.js                   |   99 +++
 package.json              |   39 +
 test/comments.js          |  626 ++++++++++++++++
 test/data/backbone.js     | 1578 ++++++++++++++++++++++++++++++++++++++++
 test/data/empty.js        |    0
 test/data/regexp-props.js |    6 +
 test/es6tests.js          |  294 ++++++++
 test/identity.js          |   33 +
 test/jsx.js               |   47 ++
 test/lines.js             |  559 ++++++++++++++
 test/mapping.js           |  211 ++++++
 test/parens.js            |  273 +++++++
 test/parser.js            |  147 ++++
 test/patcher.js           |  194 +++++
 test/perf.js              |   32 +
 test/printer.js           | 1129 +++++++++++++++++++++++++++++
 test/syntax.js            |   54 ++
 test/type-syntax.js       |   95 +++
 test/visit.js             |  138 ++++
 39 files changed, 10473 insertions(+)

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..9541a24
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,3 @@
+[*.js]
+indent_style = space
+indent_size = 4
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..07e6e47
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/node_modules
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..e216ae5
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,2 @@
+/node_modules
+/test
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..80d1fad
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,10 @@
+language: node_js
+node_js:
+  - "iojs"
+  - "0.12"
+  - "0.11"
+  - "0.10"
+  - "0.8"
+
+# Allow Travis tests to run in containers.
+sudo: false
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..527755b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2012 Ben Newman <bn at cs.stanford.edu>
+
+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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a98ce06
--- /dev/null
+++ b/README.md
@@ -0,0 +1,142 @@
+# recast, _v_. [![Build Status](https://travis-ci.org/benjamn/recast.png?branch=master)](https://travis-ci.org/benjamn/recast)
+1. to give (a metal object) a different form by melting it down and reshaping it.
+1. to form, fashion, or arrange again.
+1. to remodel or reconstruct (a literary work, document, sentence, etc.).
+1. to supply (a theater or opera work) with a new cast.
+
+Installation
+---
+
+From NPM:
+
+    npm install recast
+    
+From GitHub:
+
+    cd path/to/node_modules
+    git clone git://github.com/benjamn/recast.git
+    cd recast
+    npm install .
+
+Usage
+---
+
+In less poetic terms, Recast exposes two essential interfaces, one for parsing JavaScript code (`require("recast").parse`) and the other for reprinting modified syntax trees (`require("recast").print`).
+
+Here's a simple but non-trivial example of how you might use `.parse` and `.print`:
+```js
+var recast = require("recast");
+
+// Let's turn this function declaration into a variable declaration.
+var code = [
+    "function add(a, b) {",
+    "  return a +",
+    "    // Weird formatting, huh?",
+    "    b;",
+    "}"
+].join("\n");
+
+// Parse the code using an interface similar to require("esprima").parse.
+var ast = recast.parse(code);
+```
+Now do *whatever* you want to `ast`. Really, anything at all!
+
+See [ast-types](https://github.com/benjamn/ast-types) (especially the [def/core.js](https://github.com/benjamn/ast-types/blob/master/def/core.js)) module for a thorough overview of the `ast` api.
+```js
+// Grab a reference to the function declaration we just parsed.
+var add = ast.program.body[0];
+
+// Make sure it's a FunctionDeclaration (optional).
+var n = recast.types.namedTypes;
+n.FunctionDeclaration.assert(add);
+
+// If you choose to use recast.builders to construct new AST nodes, all builder
+// arguments will be dynamically type-checked against the Mozilla Parser API.
+var b = recast.types.builders;
+
+// This kind of manipulation should seem familiar if you've used Esprima or the
+// Mozilla Parser API before.
+ast.program.body[0] = b.variableDeclaration("var", [
+    b.variableDeclarator(add.id, b.functionExpression(
+        null, // Anonymize the function expression.
+        add.params,
+        add.body
+    ))
+]);
+
+// Just for fun, because addition is commutative:
+add.params.push(add.params.shift());
+```
+When you finish manipulating the AST, let `recast.print` work its magic:
+```js
+var output = recast.print(ast).code;
+```
+The `output` string now looks exactly like this, weird formatting and all:
+```js
+var add = function(b, a) {
+  return a +
+    // Weird formatting, huh?
+    b;
+}
+```
+The magic of Recast is that it reprints only those parts of the syntax tree that you modify. In other words, the following identity is guaranteed:
+```js
+recast.print(recast.parse(source)).code === source
+```
+Whenever Recast cannot reprint a modified node using the orginal source code, it falls back to using a generic pretty printer. So the worst that can happen is that your changes trigger some harmless reformatting of your code.
+
+If you really don't care about preserving the original formatting, you can access the pretty printer directly:
+```js
+var output = recast.prettyPrint(ast, { tabWidth: 2 }).code;
+```
+And here's the exact `output`:
+```js
+var add = function(b, a) {
+  return a + b;
+}
+```
+Note that the weird formatting was discarded, yet the behavior and abstract structure of the code remain the same.
+
+Source maps
+---
+
+One of the coolest consequences of tracking and reusing original source code during reprinting is that it's pretty easy to generate a high-resolution mapping between the original code and the generated code—completely automatically!
+
+With every `slice`, `join`, and re-`indent`-ation, the reprinting process maintains exact knowledge of which character sequences are original, and where in the original source they came from.
+
+All you have to think about is how to manipulate the syntax tree, and Recast will give you a [source map](https://github.com/mozilla/source-map) in exchange for specifying the names of your source file(s) and the desired name of the map:
+```js
+var result = recast.print(transform(recast.parse(source, {
+  sourceFileName: "source.js"
+})), {
+  sourceMapName: "map.json"
+});
+    
+console.log(result.code); // Resulting string of code.
+console.log(result.map); // JSON source map.
+
+var SourceMapConsumer = require("source-map").SourceMapConsumer;
+var smc = new SourceMapConsumer(result.map);
+console.log(smc.originalPositionFor({
+  line: 3,
+  column: 15
+})); // { source: 'source.js',
+     //   line: 2,
+     //   column: 10,
+     //   name: null }
+```
+
+Note that you are free to mix and match syntax trees parsed from different source files, and the resulting source map will automatically keep track of the separate file origins for you.
+
+Note also that the source maps generated by Recast are character-by-character maps, so meaningful identifier names are not recorded at this time. This approach leads to higher-resolution debugging in modern browsers, at the expense of somewhat larger map sizes. Striking the perfect balance here is an area for future exploration, but such improvements will not require any breaking changes to the interface demonstrated above.
+
+Motivation
+---
+
+The more code you have, the harder it becomes to make big, sweeping changes quickly and confidently. Even if you trust yourself not to make too many mistakes, and no matter how proficient you are with your text editor, changing tens of thousands of lines of code takes precious, non-refundable time.
+
+Is there a better way? Not always! When a task requires you to alter the semantics of many different pieces of code in subtly different ways, your brain inevitably becomes the bottleneck, and there is little hope of completely automating the process. Your best bet is to plan carefully, buckle down, and get it right the first time. Love it or loathe it, that's the way programming goes sometimes.
+
+What I hope to eliminate are the brain-wasting tasks, the tasks that are bottlenecked by keystrokes, the tasks that can be expressed as operations on the _syntactic structure_ of your code. Specifically, my goal is to make it possible for you to run your code through a parser, manipulate the abstract syntax tree directly, subject only to the constraints of your imagination, and then automatically translate those modifications back into source code, without upsetting the formatting of unm [...]
+
+And here's the best part: when you're done running a Recast script, if you're not completely satisfied with the results, blow them away with `git reset --hard`, tweak the script, and just run it again. Change your mind as many times as you like. Instead of typing yourself into a nasty case of [RSI](http://en.wikipedia.org/wiki/Repetitive_strain_injury), gaze upon your new wells of free time and ask yourself: what next?
diff --git a/example/add-braces b/example/add-braces
new file mode 100755
index 0000000..0c6901d
--- /dev/null
+++ b/example/add-braces
@@ -0,0 +1,44 @@
+#!/usr/bin/env node
+
+var recast = require("recast");
+var types = recast.types;
+var n = types.namedTypes;
+var b = types.builders;
+
+require("recast").run(function(ast, callback) {
+    recast.visit(ast, {
+        visitIfStatement: function(path) {
+            var stmt = path.node;
+            stmt.consequent = fix(stmt.consequent);
+
+            var alt = stmt.alternate;
+            if (!n.IfStatement.check(alt)) {
+                stmt.alternate = fix(alt);
+            }
+
+            this.traverse(path);
+        },
+
+        visitWhileStatement: visitLoop,
+        visitForStatement: visitLoop,
+        visitForInStatement: visitLoop
+    });
+
+    callback(ast);
+});
+
+function visitLoop(path) {
+    var loop = path.node;
+    loop.body = fix(loop.body);
+    this.traverse(path);
+}
+
+function fix(clause) {
+    if (clause) {
+        if (!n.BlockStatement.check(clause)) {
+            clause = b.blockStatement([clause]);
+        }
+    }
+
+    return clause;
+}
diff --git a/example/generic-identity b/example/generic-identity
new file mode 100755
index 0000000..a8364c3
--- /dev/null
+++ b/example/generic-identity
@@ -0,0 +1,17 @@
+#!/usr/bin/env node
+
+// This script should reprint the contents of the given file without
+// reusing the original source, but with identical AST structure.
+
+var recast = require("recast");
+
+recast.run(function(ast, callback) {
+    recast.visit(ast, {
+        visitNode: function(path) {
+            this.traverse(path);
+            path.node.original = null;
+        }
+    });
+
+    callback(ast);
+});
diff --git a/example/identity b/example/identity
new file mode 100755
index 0000000..62981b8
--- /dev/null
+++ b/example/identity
@@ -0,0 +1,8 @@
+#!/usr/bin/env node
+
+// This script should echo the contents of the given file without
+// modification.
+
+require("recast").run(function(ast, callback) {
+    callback(ast);
+});
diff --git a/example/to-while b/example/to-while
new file mode 100755
index 0000000..9922222
--- /dev/null
+++ b/example/to-while
@@ -0,0 +1,84 @@
+#!/usr/bin/env node
+
+// This script converts for and do-while loops into equivalent while loops.
+// Note that for-in statements are left unmodified, as they do not have a
+// simple analogy to while loops. Also note that labeled continue statements
+// are not correctly handled at this point, and will trigger an assertion
+// failure if encountered.
+
+var assert = require("assert");
+var recast = require("recast");
+var types = recast.types;
+var n = types.namedTypes;
+var b = types.builders;
+
+recast.run(function(ast, callback) {
+    recast.visit(ast, {
+        visitForStatement: function(path) {
+            var fst = path.node;
+
+            path.replace(
+                fst.init,
+                b.whileStatement(
+                    fst.test,
+                    insertBeforeLoopback(fst, fst.update)
+                )
+            );
+
+            this.traverse(path);
+        },
+
+        visitDoWhileStatement: function(path) {
+            var dwst = path.node;
+            return b.whileStatement(
+                b.literal(true),
+                insertBeforeLoopback(
+                    dwst,
+                    b.ifStatement(
+                        dwst.test,
+                        b.breakStatement()
+                    )
+                )
+            );
+        }
+    });
+
+    callback(ast);
+});
+
+function insertBeforeLoopback(loop, toInsert) {
+    var body = loop.body;
+
+    if (!n.Statement.check(toInsert)) {
+        toInsert = b.expressionStatement(toInsert);
+    }
+
+    if (n.BlockStatement.check(body)) {
+        body.body.push(toInsert);
+    } else {
+        body = b.blockStatement([body, toInsert]);
+        loop.body = body;
+    }
+
+    recast.visit(body, {
+        visitContinueStatement: function(path) {
+            var cst = path.node;
+
+            assert.equal(
+                cst.label, null,
+                "Labeled continue statements are not yet supported."
+            );
+
+            path.replace(toInsert, path.node);
+            return false;
+        },
+
+        // Do not descend into nested loops.
+        visitWhileStatement: function() {},
+        visitForStatement: function() {},
+        visitForInStatement: function() {},
+        visitDoWhileStatement: function() {}
+    });
+
+    return body;
+}
diff --git a/lib/comments.js b/lib/comments.js
new file mode 100644
index 0000000..df1d383
--- /dev/null
+++ b/lib/comments.js
@@ -0,0 +1,350 @@
+var assert = require("assert");
+var types = require("./types");
+var n = types.namedTypes;
+var isArray = types.builtInTypes.array;
+var isObject = types.builtInTypes.object;
+var linesModule = require("./lines");
+var fromString = linesModule.fromString;
+var Lines = linesModule.Lines;
+var concat = linesModule.concat;
+var util = require("./util");
+var comparePos = util.comparePos;
+var childNodesCacheKey = require("private").makeUniqueKey();
+
+// TODO Move a non-caching implementation of this function into ast-types,
+// and implement a caching wrapper function here.
+function getSortedChildNodes(node, resultArray) {
+    if (!node) {
+        return;
+    }
+
+    // The .loc checks below are sensitive to some of the problems that
+    // are fixed by this utility function. Specifically, if it decides to
+    // set node.loc to null, indicating that the node's .loc information
+    // is unreliable, then we don't want to add node to the resultArray.
+    util.fixFaultyLocations(node);
+
+    if (resultArray) {
+        if (n.Node.check(node) &&
+            n.SourceLocation.check(node.loc)) {
+            // This reverse insertion sort almost always takes constant
+            // time because we almost always (maybe always?) append the
+            // nodes in order anyway.
+            for (var i = resultArray.length - 1; i >= 0; --i) {
+                if (comparePos(resultArray[i].loc.end,
+                               node.loc.start) <= 0) {
+                    break;
+                }
+            }
+            resultArray.splice(i + 1, 0, node);
+            return;
+        }
+    } else if (node[childNodesCacheKey]) {
+        return node[childNodesCacheKey];
+    }
+
+    var names;
+    if (isArray.check(node)) {
+        names = Object.keys(node);
+    } else if (isObject.check(node)) {
+        names = types.getFieldNames(node);
+    } else {
+        return;
+    }
+
+    if (!resultArray) {
+        Object.defineProperty(node, childNodesCacheKey, {
+            value: resultArray = [],
+            enumerable: false
+        });
+    }
+
+    for (var i = 0, nameCount = names.length; i < nameCount; ++i) {
+        getSortedChildNodes(node[names[i]], resultArray);
+    }
+
+    return resultArray;
+}
+
+// As efficiently as possible, decorate the comment object with
+// .precedingNode, .enclosingNode, and/or .followingNode properties, at
+// least one of which is guaranteed to be defined.
+function decorateComment(node, comment) {
+    var childNodes = getSortedChildNodes(node);
+
+    // Time to dust off the old binary search robes and wizard hat.
+    var left = 0, right = childNodes.length;
+    while (left < right) {
+        var middle = (left + right) >> 1;
+        var child = childNodes[middle];
+
+        if (comparePos(child.loc.start, comment.loc.start) <= 0 &&
+            comparePos(comment.loc.end, child.loc.end) <= 0) {
+            // The comment is completely contained by this child node.
+            decorateComment(comment.enclosingNode = child, comment);
+            return; // Abandon the binary search at this level.
+        }
+
+        if (comparePos(child.loc.end, comment.loc.start) <= 0) {
+            // This child node falls completely before the comment.
+            // Because we will never consider this node or any nodes
+            // before it again, this node must be the closest preceding
+            // node we have encountered so far.
+            var precedingNode = child;
+            left = middle + 1;
+            continue;
+        }
+
+        if (comparePos(comment.loc.end, child.loc.start) <= 0) {
+            // This child node falls completely after the comment.
+            // Because we will never consider this node or any nodes after
+            // it again, this node must be the closest following node we
+            // have encountered so far.
+            var followingNode = child;
+            right = middle;
+            continue;
+        }
+
+        throw new Error("Comment location overlaps with node location");
+    }
+
+    if (precedingNode) {
+        comment.precedingNode = precedingNode;
+    }
+
+    if (followingNode) {
+        comment.followingNode = followingNode;
+    }
+}
+
+exports.attach = function(comments, ast, lines) {
+    if (!isArray.check(comments)) {
+        return;
+    }
+
+    var tiesToBreak = [];
+
+    comments.forEach(function(comment) {
+        comment.loc.lines = lines;
+        decorateComment(ast, comment);
+
+        var pn = comment.precedingNode;
+        var en = comment.enclosingNode;
+        var fn = comment.followingNode;
+
+        if (pn && fn) {
+            var tieCount = tiesToBreak.length;
+            if (tieCount > 0) {
+                var lastTie = tiesToBreak[tieCount - 1];
+
+                assert.strictEqual(
+                    lastTie.precedingNode === comment.precedingNode,
+                    lastTie.followingNode === comment.followingNode
+                );
+
+                if (lastTie.followingNode !== comment.followingNode) {
+                    breakTies(tiesToBreak, lines);
+                }
+            }
+
+            tiesToBreak.push(comment);
+
+        } else if (pn) {
+            // No contest: we have a trailing comment.
+            breakTies(tiesToBreak, lines);
+            addTrailingComment(pn, comment);
+
+        } else if (fn) {
+            // No contest: we have a leading comment.
+            breakTies(tiesToBreak, lines);
+            addLeadingComment(fn, comment);
+
+        } else if (en) {
+            // The enclosing node has no child nodes at all, so what we
+            // have here is a dangling comment, e.g. [/* crickets */].
+            breakTies(tiesToBreak, lines);
+            addDanglingComment(en, comment);
+
+        } else {
+            throw new Error("AST contains no nodes at all?");
+        }
+    });
+
+    breakTies(tiesToBreak, lines);
+
+    comments.forEach(function(comment) {
+        // These node references were useful for breaking ties, but we
+        // don't need them anymore, and they create cycles in the AST that
+        // may lead to infinite recursion if we don't delete them here.
+        delete comment.precedingNode;
+        delete comment.enclosingNode;
+        delete comment.followingNode;
+    });
+};
+
+function breakTies(tiesToBreak, lines) {
+    var tieCount = tiesToBreak.length;
+    if (tieCount === 0) {
+        return;
+    }
+
+    var pn = tiesToBreak[0].precedingNode;
+    var fn = tiesToBreak[0].followingNode;
+    var gapEndPos = fn.loc.start;
+
+    // Iterate backwards through tiesToBreak, examining the gaps
+    // between the tied comments. In order to qualify as leading, a
+    // comment must be separated from fn by an unbroken series of
+    // whitespace-only gaps (or other comments).
+    for (var indexOfFirstLeadingComment = tieCount;
+         indexOfFirstLeadingComment > 0;
+         --indexOfFirstLeadingComment) {
+        var comment = tiesToBreak[indexOfFirstLeadingComment - 1];
+        assert.strictEqual(comment.precedingNode, pn);
+        assert.strictEqual(comment.followingNode, fn);
+
+        var gap = lines.sliceString(comment.loc.end, gapEndPos);
+        if (/\S/.test(gap)) {
+            // The gap string contained something other than whitespace.
+            break;
+        }
+
+        gapEndPos = comment.loc.start;
+    }
+
+    while (indexOfFirstLeadingComment <= tieCount &&
+           (comment = tiesToBreak[indexOfFirstLeadingComment]) &&
+           // If the comment is a //-style comment and indented more
+           // deeply than the node itself, reconsider it as trailing.
+           comment.type === "Line" &&
+           comment.loc.start.column > fn.loc.start.column) {
+        ++indexOfFirstLeadingComment;
+    }
+
+    tiesToBreak.forEach(function(comment, i) {
+        if (i < indexOfFirstLeadingComment) {
+            addTrailingComment(pn, comment);
+        } else {
+            addLeadingComment(fn, comment);
+        }
+    });
+
+    tiesToBreak.length = 0;
+}
+
+function addCommentHelper(node, comment) {
+    var comments = node.comments || (node.comments = []);
+    comments.push(comment);
+}
+
+function addLeadingComment(node, comment) {
+    comment.leading = true;
+    comment.trailing = false;
+    addCommentHelper(node, comment);
+}
+
+function addDanglingComment(node, comment) {
+    comment.leading = false;
+    comment.trailing = false;
+    addCommentHelper(node, comment);
+}
+
+function addTrailingComment(node, comment) {
+    comment.leading = false;
+    comment.trailing = true;
+    addCommentHelper(node, comment);
+}
+
+function printLeadingComment(commentPath, print) {
+    var comment = commentPath.getValue();
+    n.Comment.assert(comment);
+
+    var loc = comment.loc;
+    var lines = loc && loc.lines;
+    var parts = [print(commentPath)];
+
+    if (comment.trailing) {
+        // When we print trailing comments as leading comments, we don't
+        // want to bring any trailing spaces along.
+        parts.push("\n");
+
+    } else if (lines instanceof Lines) {
+        var trailingSpace = lines.slice(
+            loc.end,
+            lines.skipSpaces(loc.end)
+        );
+
+        if (trailingSpace.length === 1) {
+            // If the trailing space contains no newlines, then we want to
+            // preserve it exactly as we found it.
+            parts.push(trailingSpace);
+        } else {
+            // If the trailing space contains newlines, then replace it
+            // with just that many newlines, with all other spaces removed.
+            parts.push(new Array(trailingSpace.length).join("\n"));
+        }
+
+    } else {
+        parts.push("\n");
+    }
+
+    return concat(parts);
+}
+
+function printTrailingComment(commentPath, print) {
+    var comment = commentPath.getValue(commentPath);
+    n.Comment.assert(comment);
+
+    var loc = comment.loc;
+    var lines = loc && loc.lines;
+    var parts = [];
+
+    if (lines instanceof Lines) {
+        var fromPos = lines.skipSpaces(loc.start, true) || lines.firstPos();
+        var leadingSpace = lines.slice(fromPos, loc.start);
+
+        if (leadingSpace.length === 1) {
+            // If the leading space contains no newlines, then we want to
+            // preserve it exactly as we found it.
+            parts.push(leadingSpace);
+        } else {
+            // If the leading space contains newlines, then replace it
+            // with just that many newlines, sans all other spaces.
+            parts.push(new Array(leadingSpace.length).join("\n"));
+        }
+    }
+
+    parts.push(print(commentPath));
+
+    return concat(parts);
+}
+
+exports.printComments = function(path, print) {
+    var value = path.getValue();
+    var innerLines = print(path);
+    var comments = n.Node.check(value) &&
+        types.getFieldValue(value, "comments");
+
+    if (!comments || comments.length === 0) {
+        return innerLines;
+    }
+
+    var leadingParts = [];
+    var trailingParts = [innerLines];
+
+    path.each(function(commentPath) {
+        var comment = commentPath.getValue();
+        var leading = types.getFieldValue(comment, "leading");
+        var trailing = types.getFieldValue(comment, "trailing");
+
+        if (leading || (trailing && comment.type !== "Block")) {
+            leadingParts.push(printLeadingComment(commentPath, print));
+        } else if (trailing) {
+            assert.strictEqual(comment.type, "Block");
+            trailingParts.push(printTrailingComment(commentPath, print));
+        }
+    }, "comments");
+
+    leadingParts.push.apply(leadingParts, trailingParts);
+    return concat(leadingParts);
+};
diff --git a/lib/fast-path.js b/lib/fast-path.js
new file mode 100644
index 0000000..1d97ab1
--- /dev/null
+++ b/lib/fast-path.js
@@ -0,0 +1,472 @@
+var assert = require("assert");
+var types = require("./types");
+var n = types.namedTypes;
+var Node = n.Node;
+var isArray = types.builtInTypes.array;
+var isNumber = types.builtInTypes.number;
+
+function FastPath(value) {
+    assert.ok(this instanceof FastPath);
+    this.stack = [value];
+}
+
+var FPp = FastPath.prototype;
+module.exports = FastPath;
+
+// Static convenience function for coercing a value to a FastPath.
+FastPath.from = function(obj) {
+    if (obj instanceof FastPath) {
+        // Return a defensive copy of any existing FastPath instances.
+        return obj.copy();
+    }
+
+    if (obj instanceof types.NodePath) {
+        // For backwards compatibility, unroll NodePath instances into
+        // lightweight FastPath [..., name, value] stacks.
+        var copy = Object.create(FastPath.prototype);
+        var stack = [obj.value];
+        for (var pp; (pp = obj.parentPath); obj = pp)
+            stack.push(obj.name, pp.value);
+        copy.stack = stack.reverse();
+        return copy;
+    }
+
+    // Otherwise use obj as the value of the new FastPath instance.
+    return new FastPath(obj);
+};
+
+FPp.copy = function copy() {
+    var copy = Object.create(FastPath.prototype);
+    copy.stack = this.stack.slice(0);
+    return copy;
+};
+
+// The name of the current property is always the penultimate element of
+// this.stack, and always a String.
+FPp.getName = function getName() {
+    var s = this.stack;
+    var len = s.length;
+    if (len > 1) {
+        return s[len - 2];
+    }
+    // Since the name is always a string, null is a safe sentinel value to
+    // return if we do not know the name of the (root) value.
+    return null;
+};
+
+// The value of the current property is always the final element of
+// this.stack.
+FPp.getValue = function getValue() {
+    var s = this.stack;
+    return s[s.length - 1];
+};
+
+function getNodeHelper(path, count) {
+    var s = path.stack;
+
+    for (var i = s.length - 1; i >= 0; i -= 2) {
+        var value = s[i];
+        if (n.Node.check(value) && --count < 0) {
+            return value;
+        }
+    }
+
+    return null;
+}
+
+FPp.getNode = function getNode(count) {
+    return getNodeHelper(this, ~~count);
+};
+
+FPp.getParentNode = function getParentNode(count) {
+    return getNodeHelper(this, ~~count + 1);
+};
+
+// The length of the stack can be either even or odd, depending on whether
+// or not we have a name for the root value. The difference between the
+// index of the root value and the index of the final value is always
+// even, though, which allows us to return the root value in constant time
+// (i.e. without iterating backwards through the stack).
+FPp.getRootValue = function getRootValue() {
+    var s = this.stack;
+    if (s.length % 2 === 0) {
+        return s[1];
+    }
+    return s[0];
+};
+
+// Temporarily push properties named by string arguments given after the
+// callback function onto this.stack, then call the callback with a
+// reference to this (modified) FastPath object. Note that the stack will
+// be restored to its original state after the callback is finished, so it
+// is probably a mistake to retain a reference to the path.
+FPp.call = function call(callback/*, name1, name2, ... */) {
+    var s = this.stack;
+    var origLen = s.length;
+    var value = s[origLen - 1];
+    var argc = arguments.length;
+    for (var i = 1; i < argc; ++i) {
+        var name = arguments[i];
+        value = value[name];
+        s.push(name, value);
+    }
+    var result = callback(this);
+    s.length = origLen;
+    return result;
+};
+
+// Similar to FastPath.prototype.call, except that the value obtained by
+// accessing this.getValue()[name1][name2]... should be array-like. The
+// callback will be called with a reference to this path object for each
+// element of the array.
+FPp.each = function each(callback/*, name1, name2, ... */) {
+    var s = this.stack;
+    var origLen = s.length;
+    var value = s[origLen - 1];
+    var argc = arguments.length;
+
+    for (var i = 1; i < argc; ++i) {
+        var name = arguments[i];
+        value = value[name];
+        s.push(name, value);
+    }
+
+    for (var i = 0; i < value.length; ++i) {
+        if (i in value) {
+            s.push(i, value[i]);
+            // If the callback needs to know the value of i, call
+            // path.getName(), assuming path is the parameter name.
+            callback(this);
+            s.length -= 2;
+        }
+    }
+
+    s.length = origLen;
+};
+
+// Similar to FastPath.prototype.each, except that the results of the
+// callback function invocations are stored in an array and returned at
+// the end of the iteration.
+FPp.map = function map(callback/*, name1, name2, ... */) {
+    var s = this.stack;
+    var origLen = s.length;
+    var value = s[origLen - 1];
+    var argc = arguments.length;
+
+    for (var i = 1; i < argc; ++i) {
+        var name = arguments[i];
+        value = value[name];
+        s.push(name, value);
+    }
+
+    var result = new Array(value.length);
+
+    for (var i = 0; i < value.length; ++i) {
+        if (i in value) {
+            s.push(i, value[i]);
+            result[i] = callback(this, i);
+            s.length -= 2;
+        }
+    }
+
+    s.length = origLen;
+
+    return result;
+};
+
+// Inspired by require("ast-types").NodePath.prototype.needsParens, but
+// more efficient because we're iterating backwards through a stack.
+FPp.needsParens = function(assumeExpressionContext) {
+    var parent = this.getParentNode();
+    if (!parent) {
+        return false;
+    }
+
+    var name = this.getName();
+    var node = this.getNode();
+
+    // If the value of this path is some child of a Node and not a Node
+    // itself, then it doesn't need parentheses. Only Node objects (in
+    // fact, only Expression nodes) need parentheses.
+    if (this.getValue() !== node) {
+        return false;
+    }
+
+    // Only expressions need parentheses.
+    if (!n.Expression.check(node)) {
+        return false;
+    }
+
+    // Identifiers never need parentheses.
+    if (node.type === "Identifier") {
+        return false;
+    }
+
+    if (parent.type === "ParenthesizedExpression") {
+        return false;
+    }
+
+    switch (node.type) {
+    case "UnaryExpression":
+    case "SpreadElement":
+    case "SpreadProperty":
+        return parent.type === "MemberExpression"
+            && name === "object"
+            && parent.object === node;
+
+    case "BinaryExpression":
+    case "LogicalExpression":
+        switch (parent.type) {
+        case "CallExpression":
+            return name === "callee"
+                && parent.callee === node;
+
+        case "UnaryExpression":
+        case "SpreadElement":
+        case "SpreadProperty":
+            return true;
+
+        case "MemberExpression":
+            return name === "object"
+                && parent.object === node;
+
+        case "BinaryExpression":
+        case "LogicalExpression":
+            var po = parent.operator;
+            var pp = PRECEDENCE[po];
+            var no = node.operator;
+            var np = PRECEDENCE[no];
+
+            if (pp > np) {
+                return true;
+            }
+
+            if (pp === np && name === "right") {
+                assert.strictEqual(parent.right, node);
+                return true;
+            }
+
+        default:
+            return false;
+        }
+
+    case "SequenceExpression":
+        switch (parent.type) {
+        case "ForStatement":
+            // Although parentheses wouldn't hurt around sequence
+            // expressions in the head of for loops, traditional style
+            // dictates that e.g. i++, j++ should not be wrapped with
+            // parentheses.
+            return false;
+
+        case "ExpressionStatement":
+            return name !== "expression";
+
+        default:
+            // Otherwise err on the side of overparenthesization, adding
+            // explicit exceptions above if this proves overzealous.
+            return true;
+        }
+
+    case "YieldExpression":
+        switch (parent.type) {
+        case "BinaryExpression":
+        case "LogicalExpression":
+        case "UnaryExpression":
+        case "SpreadElement":
+        case "SpreadProperty":
+        case "CallExpression":
+        case "MemberExpression":
+        case "NewExpression":
+        case "ConditionalExpression":
+        case "YieldExpression":
+            return true;
+
+        default:
+            return false;
+        }
+
+    case "Literal":
+        return parent.type === "MemberExpression"
+            && isNumber.check(node.value)
+            && name === "object"
+            && parent.object === node;
+
+    case "AssignmentExpression":
+    case "ConditionalExpression":
+        switch (parent.type) {
+        case "UnaryExpression":
+        case "SpreadElement":
+        case "SpreadProperty":
+        case "BinaryExpression":
+        case "LogicalExpression":
+            return true;
+
+        case "CallExpression":
+            return name === "callee"
+                && parent.callee === node;
+
+        case "ConditionalExpression":
+            return name === "test"
+                && parent.test === node;
+
+        case "MemberExpression":
+            return name === "object"
+                && parent.object === node;
+
+        default:
+            return false;
+        }
+
+    case "ArrowFunctionExpression":
+        return isBinary(parent);
+
+    case "ObjectExpression":
+        if (parent.type === "ArrowFunctionExpression" &&
+            name === "body") {
+            return true;
+        }
+
+    default:
+        if (parent.type === "NewExpression" &&
+            name === "callee" &&
+            parent.callee === node) {
+            return containsCallExpression(node);
+        }
+    }
+
+    if (assumeExpressionContext !== true &&
+        !this.canBeFirstInStatement() &&
+        this.firstInStatement())
+        return true;
+
+    return false;
+};
+
+function isBinary(node) {
+    return n.BinaryExpression.check(node)
+        || n.LogicalExpression.check(node);
+}
+
+function isUnaryLike(node) {
+    return n.UnaryExpression.check(node)
+        // I considered making SpreadElement and SpreadProperty subtypes
+        // of UnaryExpression, but they're not really Expression nodes.
+        || (n.SpreadElement && n.SpreadElement.check(node))
+        || (n.SpreadProperty && n.SpreadProperty.check(node));
+}
+
+var PRECEDENCE = {};
+[["||"],
+ ["&&"],
+ ["|"],
+ ["^"],
+ ["&"],
+ ["==", "===", "!=", "!=="],
+ ["<", ">", "<=", ">=", "in", "instanceof"],
+ [">>", "<<", ">>>"],
+ ["+", "-"],
+ ["*", "/", "%"]
+].forEach(function(tier, i) {
+    tier.forEach(function(op) {
+        PRECEDENCE[op] = i;
+    });
+});
+
+function containsCallExpression(node) {
+    if (n.CallExpression.check(node)) {
+        return true;
+    }
+
+    if (isArray.check(node)) {
+        return node.some(containsCallExpression);
+    }
+
+    if (n.Node.check(node)) {
+        return types.someField(node, function(name, child) {
+            return containsCallExpression(child);
+        });
+    }
+
+    return false;
+}
+
+FPp.canBeFirstInStatement = function() {
+    var node = this.getNode();
+    return !n.FunctionExpression.check(node)
+        && !n.ObjectExpression.check(node);
+};
+
+FPp.firstInStatement = function() {
+    var s = this.stack;
+    var parentName, parent;
+    var childName, child;
+
+    for (var i = s.length - 1; i >= 0; i -= 2) {
+        if (n.Node.check(s[i])) {
+            childName = parentName;
+            child = parent;
+            parentName = s[i - 1];
+            parent = s[i];
+        }
+
+        if (!parent || !child) {
+            continue;
+        }
+
+        if (n.BlockStatement.check(parent) &&
+            parentName === "body" &&
+            childName === 0) {
+            assert.strictEqual(parent.body[0], child);
+            return true;
+        }
+
+        if (n.ExpressionStatement.check(parent) &&
+            childName === "expression") {
+            assert.strictEqual(parent.expression, child);
+            return true;
+        }
+
+        if (n.SequenceExpression.check(parent) &&
+            parentName === "expressions" &&
+            childName === 0) {
+            assert.strictEqual(parent.expressions[0], child);
+            continue;
+        }
+
+        if (n.CallExpression.check(parent) &&
+            childName === "callee") {
+            assert.strictEqual(parent.callee, child);
+            continue;
+        }
+
+        if (n.MemberExpression.check(parent) &&
+            childName === "object") {
+            assert.strictEqual(parent.object, child);
+            continue;
+        }
+
+        if (n.ConditionalExpression.check(parent) &&
+            childName === "test") {
+            assert.strictEqual(parent.test, child);
+            continue;
+        }
+
+        if (isBinary(parent) &&
+            childName === "left") {
+            assert.strictEqual(parent.left, child);
+            continue;
+        }
+
+        if (n.UnaryExpression.check(parent) &&
+            !parent.prefix &&
+            childName === "argument") {
+            assert.strictEqual(parent.argument, child);
+            continue;
+        }
+
+        return false;
+    }
+
+    return true;
+};
diff --git a/lib/lines.js b/lib/lines.js
new file mode 100644
index 0000000..e084f8f
--- /dev/null
+++ b/lib/lines.js
@@ -0,0 +1,854 @@
+var assert = require("assert");
+var sourceMap = require("source-map");
+var normalizeOptions = require("./options").normalize;
+var secretKey = require("private").makeUniqueKey();
+var types = require("./types");
+var isString = types.builtInTypes.string;
+var comparePos = require("./util").comparePos;
+var Mapping = require("./mapping");
+
+// Goals:
+// 1. Minimize new string creation.
+// 2. Keep (de)identation O(lines) time.
+// 3. Permit negative indentations.
+// 4. Enforce immutability.
+// 5. No newline characters.
+
+function getSecret(lines) {
+    return lines[secretKey];
+}
+
+function Lines(infos, sourceFileName) {
+    assert.ok(this instanceof Lines);
+    assert.ok(infos.length > 0);
+
+    if (sourceFileName) {
+        isString.assert(sourceFileName);
+    } else {
+        sourceFileName = null;
+    }
+
+    Object.defineProperty(this, secretKey, {
+        value: {
+            infos: infos,
+            mappings: [],
+            name: sourceFileName,
+            cachedSourceMap: null
+        }
+    });
+
+    if (sourceFileName) {
+        getSecret(this).mappings.push(new Mapping(this, {
+            start: this.firstPos(),
+            end: this.lastPos()
+        }));
+    }
+}
+
+// Exposed for instanceof checks. The fromString function should be used
+// to create new Lines objects.
+exports.Lines = Lines;
+var Lp = Lines.prototype;
+
+// These properties used to be assigned to each new object in the Lines
+// constructor, but we can more efficiently stuff them into the secret and
+// let these lazy accessors compute their values on-the-fly.
+Object.defineProperties(Lp, {
+    length: {
+        get: function() {
+            return getSecret(this).infos.length;
+        }
+    },
+
+    name: {
+        get: function() {
+            return getSecret(this).name;
+        }
+    }
+});
+
+function copyLineInfo(info) {
+    return {
+        line: info.line,
+        indent: info.indent,
+        sliceStart: info.sliceStart,
+        sliceEnd: info.sliceEnd
+    };
+}
+
+var fromStringCache = {};
+var hasOwn = fromStringCache.hasOwnProperty;
+var maxCacheKeyLen = 10;
+
+function countSpaces(spaces, tabWidth) {
+    var count = 0;
+    var len = spaces.length;
+
+    for (var i = 0; i < len; ++i) {
+        switch (spaces.charCodeAt(i)) {
+        case 9: // '\t'
+            assert.strictEqual(typeof tabWidth, "number");
+            assert.ok(tabWidth > 0);
+
+            var next = Math.ceil(count / tabWidth) * tabWidth;
+            if (next === count) {
+                count += tabWidth;
+            } else {
+                count = next;
+            }
+
+            break;
+
+        case 11: // '\v'
+        case 12: // '\f'
+        case 13: // '\r'
+        case 0xfeff: // zero-width non-breaking space
+            // These characters contribute nothing to indentation.
+            break;
+
+        case 32: // ' '
+        default: // Treat all other whitespace like ' '.
+            count += 1;
+            break;
+        }
+    }
+
+    return count;
+}
+exports.countSpaces = countSpaces;
+
+var leadingSpaceExp = /^\s*/;
+
+// As specified here: http://www.ecma-international.org/ecma-262/6.0/#sec-line-terminators
+var lineTerminatorSeqExp =
+    /\u000D\u000A|\u000D(?!\u000A)|\u000A|\u2028|\u2029/;
+
+/**
+ * @param {Object} options - Options object that configures printing.
+ */
+function fromString(string, options) {
+    if (string instanceof Lines)
+        return string;
+
+    string += "";
+
+    var tabWidth = options && options.tabWidth;
+    var tabless = string.indexOf("\t") < 0;
+    var cacheable = !options && tabless && (string.length <= maxCacheKeyLen);
+
+    assert.ok(tabWidth || tabless, "No tab width specified but encountered tabs in string\n" + string);
+
+    if (cacheable && hasOwn.call(fromStringCache, string))
+        return fromStringCache[string];
+
+    var lines = new Lines(string.split(lineTerminatorSeqExp).map(function(line) {
+        var spaces = leadingSpaceExp.exec(line)[0];
+        return {
+            line: line,
+            indent: countSpaces(spaces, tabWidth),
+            sliceStart: spaces.length,
+            sliceEnd: line.length
+        };
+    }), normalizeOptions(options).sourceFileName);
+
+    if (cacheable)
+        fromStringCache[string] = lines;
+
+    return lines;
+}
+exports.fromString = fromString;
+
+function isOnlyWhitespace(string) {
+    return !/\S/.test(string);
+}
+
+Lp.toString = function(options) {
+    return this.sliceString(this.firstPos(), this.lastPos(), options);
+};
+
+Lp.getSourceMap = function(sourceMapName, sourceRoot) {
+    if (!sourceMapName) {
+        // Although we could make up a name or generate an anonymous
+        // source map, instead we assume that any consumer who does not
+        // provide a name does not actually want a source map.
+        return null;
+    }
+
+    var targetLines = this;
+
+    function updateJSON(json) {
+        json = json || {};
+
+        isString.assert(sourceMapName);
+        json.file = sourceMapName;
+
+        if (sourceRoot) {
+            isString.assert(sourceRoot);
+            json.sourceRoot = sourceRoot;
+        }
+
+        return json;
+    }
+
+    var secret = getSecret(targetLines);
+    if (secret.cachedSourceMap) {
+        // Since Lines objects are immutable, we can reuse any source map
+        // that was previously generated. Nevertheless, we return a new
+        // JSON object here to protect the cached source map from outside
+        // modification.
+        return updateJSON(secret.cachedSourceMap.toJSON());
+    }
+
+    var smg = new sourceMap.SourceMapGenerator(updateJSON());
+    var sourcesToContents = {};
+
+    secret.mappings.forEach(function(mapping) {
+        var sourceCursor = mapping.sourceLines.skipSpaces(
+            mapping.sourceLoc.start
+        ) || mapping.sourceLines.lastPos();
+
+        var targetCursor = targetLines.skipSpaces(
+            mapping.targetLoc.start
+        ) || targetLines.lastPos();
+
+        while (comparePos(sourceCursor, mapping.sourceLoc.end) < 0 &&
+               comparePos(targetCursor, mapping.targetLoc.end) < 0) {
+
+            var sourceChar = mapping.sourceLines.charAt(sourceCursor);
+            var targetChar = targetLines.charAt(targetCursor);
+            assert.strictEqual(sourceChar, targetChar);
+
+            var sourceName = mapping.sourceLines.name;
+
+            // Add mappings one character at a time for maximum resolution.
+            smg.addMapping({
+                source: sourceName,
+                original: { line: sourceCursor.line,
+                            column: sourceCursor.column },
+                generated: { line: targetCursor.line,
+                             column: targetCursor.column }
+            });
+
+            if (!hasOwn.call(sourcesToContents, sourceName)) {
+                var sourceContent = mapping.sourceLines.toString();
+                smg.setSourceContent(sourceName, sourceContent);
+                sourcesToContents[sourceName] = sourceContent;
+            }
+
+            targetLines.nextPos(targetCursor, true);
+            mapping.sourceLines.nextPos(sourceCursor, true);
+        }
+    });
+
+    secret.cachedSourceMap = smg;
+
+    return smg.toJSON();
+};
+
+Lp.bootstrapCharAt = function(pos) {
+    assert.strictEqual(typeof pos, "object");
+    assert.strictEqual(typeof pos.line, "number");
+    assert.strictEqual(typeof pos.column, "number");
+
+    var line = pos.line,
+        column = pos.column,
+        strings = this.toString().split(lineTerminatorSeqExp),
+        string = strings[line - 1];
+
+    if (typeof string === "undefined")
+        return "";
+
+    if (column === string.length &&
+        line < strings.length)
+        return "\n";
+
+    if (column >= string.length)
+        return "";
+
+    return string.charAt(column);
+};
+
+Lp.charAt = function(pos) {
+    assert.strictEqual(typeof pos, "object");
+    assert.strictEqual(typeof pos.line, "number");
+    assert.strictEqual(typeof pos.column, "number");
+
+    var line = pos.line,
+        column = pos.column,
+        secret = getSecret(this),
+        infos = secret.infos,
+        info = infos[line - 1],
+        c = column;
+
+    if (typeof info === "undefined" || c < 0)
+        return "";
+
+    var indent = this.getIndentAt(line);
+    if (c < indent)
+        return " ";
+
+    c += info.sliceStart - indent;
+
+    if (c === info.sliceEnd &&
+        line < this.length)
+        return "\n";
+
+    if (c >= info.sliceEnd)
+        return "";
+
+    return info.line.charAt(c);
+};
+
+Lp.stripMargin = function(width, skipFirstLine) {
+    if (width === 0)
+        return this;
+
+    assert.ok(width > 0, "negative margin: " + width);
+
+    if (skipFirstLine && this.length === 1)
+        return this;
+
+    var secret = getSecret(this);
+
+    var lines = new Lines(secret.infos.map(function(info, i) {
+        if (info.line && (i > 0 || !skipFirstLine)) {
+            info = copyLineInfo(info);
+            info.indent = Math.max(0, info.indent - width);
+        }
+        return info;
+    }));
+
+    if (secret.mappings.length > 0) {
+        var newMappings = getSecret(lines).mappings;
+        assert.strictEqual(newMappings.length, 0);
+        secret.mappings.forEach(function(mapping) {
+            newMappings.push(mapping.indent(width, skipFirstLine, true));
+        });
+    }
+
+    return lines;
+};
+
+Lp.indent = function(by) {
+    if (by === 0)
+        return this;
+
+    var secret = getSecret(this);
+
+    var lines = new Lines(secret.infos.map(function(info) {
+        if (info.line) {
+            info = copyLineInfo(info);
+            info.indent += by;
+        }
+        return info
+    }));
+
+    if (secret.mappings.length > 0) {
+        var newMappings = getSecret(lines).mappings;
+        assert.strictEqual(newMappings.length, 0);
+        secret.mappings.forEach(function(mapping) {
+            newMappings.push(mapping.indent(by));
+        });
+    }
+
+    return lines;
+};
+
+Lp.indentTail = function(by) {
+    if (by === 0)
+        return this;
+
+    if (this.length < 2)
+        return this;
+
+    var secret = getSecret(this);
+
+    var lines = new Lines(secret.infos.map(function(info, i) {
+        if (i > 0 && info.line) {
+            info = copyLineInfo(info);
+            info.indent += by;
+        }
+
+        return info;
+    }));
+
+    if (secret.mappings.length > 0) {
+        var newMappings = getSecret(lines).mappings;
+        assert.strictEqual(newMappings.length, 0);
+        secret.mappings.forEach(function(mapping) {
+            newMappings.push(mapping.indent(by, true));
+        });
+    }
+
+    return lines;
+};
+
+Lp.getIndentAt = function(line) {
+    assert.ok(line >= 1, "no line " + line + " (line numbers start from 1)");
+    var secret = getSecret(this),
+        info = secret.infos[line - 1];
+    return Math.max(info.indent, 0);
+};
+
+Lp.guessTabWidth = function() {
+    var secret = getSecret(this);
+    if (hasOwn.call(secret, "cachedTabWidth")) {
+        return secret.cachedTabWidth;
+    }
+
+    var counts = []; // Sparse array.
+    var lastIndent = 0;
+
+    for (var line = 1, last = this.length; line <= last; ++line) {
+        var info = secret.infos[line - 1];
+        var sliced = info.line.slice(info.sliceStart, info.sliceEnd);
+
+        // Whitespace-only lines don't tell us much about the likely tab
+        // width of this code.
+        if (isOnlyWhitespace(sliced)) {
+            continue;
+        }
+
+        var diff = Math.abs(info.indent - lastIndent);
+        counts[diff] = ~~counts[diff] + 1;
+        lastIndent = info.indent;
+    }
+
+    var maxCount = -1;
+    var result = 2;
+
+    for (var tabWidth = 1;
+         tabWidth < counts.length;
+         tabWidth += 1) {
+        if (hasOwn.call(counts, tabWidth) &&
+            counts[tabWidth] > maxCount) {
+            maxCount = counts[tabWidth];
+            result = tabWidth;
+        }
+    }
+
+    return secret.cachedTabWidth = result;
+};
+
+Lp.isOnlyWhitespace = function() {
+    return isOnlyWhitespace(this.toString());
+};
+
+Lp.isPrecededOnlyByWhitespace = function(pos) {
+    var secret = getSecret(this);
+    var info = secret.infos[pos.line - 1];
+    var indent = Math.max(info.indent, 0);
+
+    var diff = pos.column - indent;
+    if (diff <= 0) {
+        // If pos.column does not exceed the indentation amount, then
+        // there must be only whitespace before it.
+        return true;
+    }
+
+    var start = info.sliceStart;
+    var end = Math.min(start + diff, info.sliceEnd);
+    var prefix = info.line.slice(start, end);
+
+    return isOnlyWhitespace(prefix);
+};
+
+Lp.getLineLength = function(line) {
+    var secret = getSecret(this),
+        info = secret.infos[line - 1];
+    return this.getIndentAt(line) + info.sliceEnd - info.sliceStart;
+};
+
+Lp.nextPos = function(pos, skipSpaces) {
+    var l = Math.max(pos.line, 0),
+        c = Math.max(pos.column, 0);
+
+    if (c < this.getLineLength(l)) {
+        pos.column += 1;
+
+        return skipSpaces
+            ? !!this.skipSpaces(pos, false, true)
+            : true;
+    }
+
+    if (l < this.length) {
+        pos.line += 1;
+        pos.column = 0;
+
+        return skipSpaces
+            ? !!this.skipSpaces(pos, false, true)
+            : true;
+    }
+
+    return false;
+};
+
+Lp.prevPos = function(pos, skipSpaces) {
+    var l = pos.line,
+        c = pos.column;
+
+    if (c < 1) {
+        l -= 1;
+
+        if (l < 1)
+            return false;
+
+        c = this.getLineLength(l);
+
+    } else {
+        c = Math.min(c - 1, this.getLineLength(l));
+    }
+
+    pos.line = l;
+    pos.column = c;
+
+    return skipSpaces
+        ? !!this.skipSpaces(pos, true, true)
+        : true;
+};
+
+Lp.firstPos = function() {
+    // Trivial, but provided for completeness.
+    return { line: 1, column: 0 };
+};
+
+Lp.lastPos = function() {
+    return {
+        line: this.length,
+        column: this.getLineLength(this.length)
+    };
+};
+
+Lp.skipSpaces = function(pos, backward, modifyInPlace) {
+    if (pos) {
+        pos = modifyInPlace ? pos : {
+            line: pos.line,
+            column: pos.column
+        };
+    } else if (backward) {
+        pos = this.lastPos();
+    } else {
+        pos = this.firstPos();
+    }
+
+    if (backward) {
+        while (this.prevPos(pos)) {
+            if (!isOnlyWhitespace(this.charAt(pos)) &&
+                this.nextPos(pos)) {
+                return pos;
+            }
+        }
+
+        return null;
+
+    } else {
+        while (isOnlyWhitespace(this.charAt(pos))) {
+            if (!this.nextPos(pos)) {
+                return null;
+            }
+        }
+
+        return pos;
+    }
+};
+
+Lp.trimLeft = function() {
+    var pos = this.skipSpaces(this.firstPos(), false, true);
+    return pos ? this.slice(pos) : emptyLines;
+};
+
+Lp.trimRight = function() {
+    var pos = this.skipSpaces(this.lastPos(), true, true);
+    return pos ? this.slice(this.firstPos(), pos) : emptyLines;
+};
+
+Lp.trim = function() {
+    var start = this.skipSpaces(this.firstPos(), false, true);
+    if (start === null)
+        return emptyLines;
+
+    var end = this.skipSpaces(this.lastPos(), true, true);
+    assert.notStrictEqual(end, null);
+
+    return this.slice(start, end);
+};
+
+Lp.eachPos = function(callback, startPos, skipSpaces) {
+    var pos = this.firstPos();
+
+    if (startPos) {
+        pos.line = startPos.line,
+        pos.column = startPos.column
+    }
+
+    if (skipSpaces && !this.skipSpaces(pos, false, true)) {
+        return; // Encountered nothing but spaces.
+    }
+
+    do callback.call(this, pos);
+    while (this.nextPos(pos, skipSpaces));
+};
+
+Lp.bootstrapSlice = function(start, end) {
+    var strings = this.toString().split(
+        lineTerminatorSeqExp
+    ).slice(
+        start.line - 1,
+        end.line
+    );
+
+    strings.push(strings.pop().slice(0, end.column));
+    strings[0] = strings[0].slice(start.column);
+
+    return fromString(strings.join("\n"));
+};
+
+Lp.slice = function(start, end) {
+    if (!end) {
+        if (!start) {
+            // The client seems to want a copy of this Lines object, but
+            // Lines objects are immutable, so it's perfectly adequate to
+            // return the same object.
+            return this;
+        }
+
+        // Slice to the end if no end position was provided.
+        end = this.lastPos();
+    }
+
+    var secret = getSecret(this);
+    var sliced = secret.infos.slice(start.line - 1, end.line);
+
+    if (start.line === end.line) {
+        sliced[0] = sliceInfo(sliced[0], start.column, end.column);
+    } else {
+        assert.ok(start.line < end.line);
+        sliced[0] = sliceInfo(sliced[0], start.column);
+        sliced.push(sliceInfo(sliced.pop(), 0, end.column));
+    }
+
+    var lines = new Lines(sliced);
+
+    if (secret.mappings.length > 0) {
+        var newMappings = getSecret(lines).mappings;
+        assert.strictEqual(newMappings.length, 0);
+        secret.mappings.forEach(function(mapping) {
+            var sliced = mapping.slice(this, start, end);
+            if (sliced) {
+                newMappings.push(sliced);
+            }
+        }, this);
+    }
+
+    return lines;
+};
+
+function sliceInfo(info, startCol, endCol) {
+    var sliceStart = info.sliceStart;
+    var sliceEnd = info.sliceEnd;
+    var indent = Math.max(info.indent, 0);
+    var lineLength = indent + sliceEnd - sliceStart;
+
+    if (typeof endCol === "undefined") {
+        endCol = lineLength;
+    }
+
+    startCol = Math.max(startCol, 0);
+    endCol = Math.min(endCol, lineLength);
+    endCol = Math.max(endCol, startCol);
+
+    if (endCol < indent) {
+        indent = endCol;
+        sliceEnd = sliceStart;
+    } else {
+        sliceEnd -= lineLength - endCol;
+    }
+
+    lineLength = endCol;
+    lineLength -= startCol;
+
+    if (startCol < indent) {
+        indent -= startCol;
+    } else {
+        startCol -= indent;
+        indent = 0;
+        sliceStart += startCol;
+    }
+
+    assert.ok(indent >= 0);
+    assert.ok(sliceStart <= sliceEnd);
+    assert.strictEqual(lineLength, indent + sliceEnd - sliceStart);
+
+    if (info.indent === indent &&
+        info.sliceStart === sliceStart &&
+        info.sliceEnd === sliceEnd) {
+        return info;
+    }
+
+    return {
+        line: info.line,
+        indent: indent,
+        sliceStart: sliceStart,
+        sliceEnd: sliceEnd
+    };
+}
+
+Lp.bootstrapSliceString = function(start, end, options) {
+    return this.slice(start, end).toString(options);
+};
+
+Lp.sliceString = function(start, end, options) {
+    if (!end) {
+        if (!start) {
+            // The client seems to want a copy of this Lines object, but
+            // Lines objects are immutable, so it's perfectly adequate to
+            // return the same object.
+            return this;
+        }
+
+        // Slice to the end if no end position was provided.
+        end = this.lastPos();
+    }
+
+    options = normalizeOptions(options);
+
+    var infos = getSecret(this).infos;
+    var parts = [];
+    var tabWidth = options.tabWidth;
+
+    for (var line = start.line; line <= end.line; ++line) {
+        var info = infos[line - 1];
+
+        if (line === start.line) {
+            if (line === end.line) {
+                info = sliceInfo(info, start.column, end.column);
+            } else {
+                info = sliceInfo(info, start.column);
+            }
+        } else if (line === end.line) {
+            info = sliceInfo(info, 0, end.column);
+        }
+
+        var indent = Math.max(info.indent, 0);
+
+        var before = info.line.slice(0, info.sliceStart);
+        if (options.reuseWhitespace &&
+            isOnlyWhitespace(before) &&
+            countSpaces(before, options.tabWidth) === indent) {
+            // Reuse original spaces if the indentation is correct.
+            parts.push(info.line.slice(0, info.sliceEnd));
+            continue;
+        }
+
+        var tabs = 0;
+        var spaces = indent;
+
+        if (options.useTabs) {
+            tabs = Math.floor(indent / tabWidth);
+            spaces -= tabs * tabWidth;
+        }
+
+        var result = "";
+
+        if (tabs > 0) {
+            result += new Array(tabs + 1).join("\t");
+        }
+
+        if (spaces > 0) {
+            result += new Array(spaces + 1).join(" ");
+        }
+
+        result += info.line.slice(info.sliceStart, info.sliceEnd);
+
+        parts.push(result);
+    }
+
+    return parts.join(options.lineTerminator);
+};
+
+Lp.isEmpty = function() {
+    return this.length < 2 && this.getLineLength(1) < 1;
+};
+
+Lp.join = function(elements) {
+    var separator = this;
+    var separatorSecret = getSecret(separator);
+    var infos = [];
+    var mappings = [];
+    var prevInfo;
+
+    function appendSecret(secret) {
+        if (secret === null)
+            return;
+
+        if (prevInfo) {
+            var info = secret.infos[0];
+            var indent = new Array(info.indent + 1).join(" ");
+            var prevLine = infos.length;
+            var prevColumn = Math.max(prevInfo.indent, 0) +
+                prevInfo.sliceEnd - prevInfo.sliceStart;
+
+            prevInfo.line = prevInfo.line.slice(
+                0, prevInfo.sliceEnd) + indent + info.line.slice(
+                    info.sliceStart, info.sliceEnd);
+
+            prevInfo.sliceEnd = prevInfo.line.length;
+
+            if (secret.mappings.length > 0) {
+                secret.mappings.forEach(function(mapping) {
+                    mappings.push(mapping.add(prevLine, prevColumn));
+                });
+            }
+
+        } else if (secret.mappings.length > 0) {
+            mappings.push.apply(mappings, secret.mappings);
+        }
+
+        secret.infos.forEach(function(info, i) {
+            if (!prevInfo || i > 0) {
+                prevInfo = copyLineInfo(info);
+                infos.push(prevInfo);
+            }
+        });
+    }
+
+    function appendWithSeparator(secret, i) {
+        if (i > 0)
+            appendSecret(separatorSecret);
+        appendSecret(secret);
+    }
+
+    elements.map(function(elem) {
+        var lines = fromString(elem);
+        if (lines.isEmpty())
+            return null;
+        return getSecret(lines);
+    }).forEach(separator.isEmpty()
+               ? appendSecret
+               : appendWithSeparator);
+
+    if (infos.length < 1)
+        return emptyLines;
+
+    var lines = new Lines(infos);
+
+    getSecret(lines).mappings = mappings;
+
+    return lines;
+};
+
+exports.concat = function(elements) {
+    return emptyLines.join(elements);
+};
+
+Lp.concat = function(other) {
+    var args = arguments,
+        list = [this];
+    list.push.apply(list, args);
+    assert.strictEqual(list.length, args.length + 1);
+    return emptyLines.join(list);
+};
+
+// The emptyLines object needs to be created all the way down here so that
+// Lines.prototype will be fully populated.
+var emptyLines = fromString("");
diff --git a/lib/mapping.js b/lib/mapping.js
new file mode 100644
index 0000000..1d469f3
--- /dev/null
+++ b/lib/mapping.js
@@ -0,0 +1,277 @@
+var assert = require("assert");
+var types = require("./types");
+var isString = types.builtInTypes.string;
+var isNumber = types.builtInTypes.number;
+var SourceLocation = types.namedTypes.SourceLocation;
+var Position = types.namedTypes.Position;
+var linesModule = require("./lines");
+var comparePos = require("./util").comparePos;
+
+function Mapping(sourceLines, sourceLoc, targetLoc) {
+    assert.ok(this instanceof Mapping);
+    assert.ok(sourceLines instanceof linesModule.Lines);
+    SourceLocation.assert(sourceLoc);
+
+    if (targetLoc) {
+        // In certain cases it's possible for targetLoc.{start,end}.column
+        // values to be negative, which technically makes them no longer
+        // valid SourceLocation nodes, so we need to be more forgiving.
+        assert.ok(
+            isNumber.check(targetLoc.start.line) &&
+            isNumber.check(targetLoc.start.column) &&
+            isNumber.check(targetLoc.end.line) &&
+            isNumber.check(targetLoc.end.column)
+        );
+    } else {
+        // Assume identity mapping if no targetLoc specified.
+        targetLoc = sourceLoc;
+    }
+
+    Object.defineProperties(this, {
+        sourceLines: { value: sourceLines },
+        sourceLoc: { value: sourceLoc },
+        targetLoc: { value: targetLoc }
+    });
+}
+
+var Mp = Mapping.prototype;
+module.exports = Mapping;
+
+Mp.slice = function(lines, start, end) {
+    assert.ok(lines instanceof linesModule.Lines);
+    Position.assert(start);
+
+    if (end) {
+        Position.assert(end);
+    } else {
+        end = lines.lastPos();
+    }
+
+    var sourceLines = this.sourceLines;
+    var sourceLoc = this.sourceLoc;
+    var targetLoc = this.targetLoc;
+
+    function skip(name) {
+        var sourceFromPos = sourceLoc[name];
+        var targetFromPos = targetLoc[name];
+        var targetToPos = start;
+
+        if (name === "end") {
+            targetToPos = end;
+        } else {
+            assert.strictEqual(name, "start");
+        }
+
+        return skipChars(
+            sourceLines, sourceFromPos,
+            lines, targetFromPos, targetToPos
+        );
+    }
+
+    if (comparePos(start, targetLoc.start) <= 0) {
+        if (comparePos(targetLoc.end, end) <= 0) {
+            targetLoc = {
+                start: subtractPos(targetLoc.start, start.line, start.column),
+                end: subtractPos(targetLoc.end, start.line, start.column)
+            };
+
+            // The sourceLoc can stay the same because the contents of the
+            // targetLoc have not changed.
+
+        } else if (comparePos(end, targetLoc.start) <= 0) {
+            return null;
+
+        } else {
+            sourceLoc = {
+                start: sourceLoc.start,
+                end: skip("end")
+            };
+
+            targetLoc = {
+                start: subtractPos(targetLoc.start, start.line, start.column),
+                end: subtractPos(end, start.line, start.column)
+            };
+        }
+
+    } else {
+        if (comparePos(targetLoc.end, start) <= 0) {
+            return null;
+        }
+
+        if (comparePos(targetLoc.end, end) <= 0) {
+            sourceLoc = {
+                start: skip("start"),
+                end: sourceLoc.end
+            };
+
+            targetLoc = {
+                // Same as subtractPos(start, start.line, start.column):
+                start: { line: 1, column: 0 },
+                end: subtractPos(targetLoc.end, start.line, start.column)
+            };
+
+        } else {
+            sourceLoc = {
+                start: skip("start"),
+                end: skip("end")
+            };
+
+            targetLoc = {
+                // Same as subtractPos(start, start.line, start.column):
+                start: { line: 1, column: 0 },
+                end: subtractPos(end, start.line, start.column)
+            };
+        }
+    }
+
+    return new Mapping(this.sourceLines, sourceLoc, targetLoc);
+};
+
+Mp.add = function(line, column) {
+    return new Mapping(this.sourceLines, this.sourceLoc, {
+        start: addPos(this.targetLoc.start, line, column),
+        end: addPos(this.targetLoc.end, line, column)
+    });
+};
+
+function addPos(toPos, line, column) {
+    return {
+        line: toPos.line + line - 1,
+        column: (toPos.line === 1)
+            ? toPos.column + column
+            : toPos.column
+    };
+}
+
+Mp.subtract = function(line, column) {
+    return new Mapping(this.sourceLines, this.sourceLoc, {
+        start: subtractPos(this.targetLoc.start, line, column),
+        end: subtractPos(this.targetLoc.end, line, column)
+    });
+};
+
+function subtractPos(fromPos, line, column) {
+    return {
+        line: fromPos.line - line + 1,
+        column: (fromPos.line === line)
+            ? fromPos.column - column
+            : fromPos.column
+    };
+}
+
+Mp.indent = function(by, skipFirstLine, noNegativeColumns) {
+    if (by === 0) {
+        return this;
+    }
+
+    var targetLoc = this.targetLoc;
+    var startLine = targetLoc.start.line;
+    var endLine = targetLoc.end.line;
+
+    if (skipFirstLine && startLine === 1 && endLine === 1) {
+        return this;
+    }
+
+    targetLoc = {
+        start: targetLoc.start,
+        end: targetLoc.end
+    };
+
+    if (!skipFirstLine || startLine > 1) {
+        var startColumn = targetLoc.start.column + by;
+        targetLoc.start = {
+            line: startLine,
+            column: noNegativeColumns
+                ? Math.max(0, startColumn)
+                : startColumn
+        };
+    }
+
+    if (!skipFirstLine || endLine > 1) {
+        var endColumn = targetLoc.end.column + by;
+        targetLoc.end = {
+            line: endLine,
+            column: noNegativeColumns
+                ? Math.max(0, endColumn)
+                : endColumn
+        };
+    }
+
+    return new Mapping(this.sourceLines, this.sourceLoc, targetLoc);
+};
+
+function skipChars(
+    sourceLines, sourceFromPos,
+    targetLines, targetFromPos, targetToPos
+) {
+    assert.ok(sourceLines instanceof linesModule.Lines);
+    assert.ok(targetLines instanceof linesModule.Lines);
+    Position.assert(sourceFromPos);
+    Position.assert(targetFromPos);
+    Position.assert(targetToPos);
+
+    var targetComparison = comparePos(targetFromPos, targetToPos);
+    if (targetComparison === 0) {
+        // Trivial case: no characters to skip.
+        return sourceFromPos;
+    }
+
+    if (targetComparison < 0) {
+        // Skipping forward.
+
+        var sourceCursor = sourceLines.skipSpaces(sourceFromPos);
+        var targetCursor = targetLines.skipSpaces(targetFromPos);
+
+        var lineDiff = targetToPos.line - targetCursor.line;
+        sourceCursor.line += lineDiff;
+        targetCursor.line += lineDiff;
+
+        if (lineDiff > 0) {
+            // If jumping to later lines, reset columns to the beginnings
+            // of those lines.
+            sourceCursor.column = 0;
+            targetCursor.column = 0;
+        } else {
+            assert.strictEqual(lineDiff, 0);
+        }
+
+        while (comparePos(targetCursor, targetToPos) < 0 &&
+               targetLines.nextPos(targetCursor, true)) {
+            assert.ok(sourceLines.nextPos(sourceCursor, true));
+            assert.strictEqual(
+                sourceLines.charAt(sourceCursor),
+                targetLines.charAt(targetCursor)
+            );
+        }
+
+    } else {
+        // Skipping backward.
+
+        var sourceCursor = sourceLines.skipSpaces(sourceFromPos, true);
+        var targetCursor = targetLines.skipSpaces(targetFromPos, true);
+
+        var lineDiff = targetToPos.line - targetCursor.line;
+        sourceCursor.line += lineDiff;
+        targetCursor.line += lineDiff;
+
+        if (lineDiff < 0) {
+            // If jumping to earlier lines, reset columns to the ends of
+            // those lines.
+            sourceCursor.column = sourceLines.getLineLength(sourceCursor.line);
+            targetCursor.column = targetLines.getLineLength(targetCursor.line);
+        } else {
+            assert.strictEqual(lineDiff, 0);
+        }
+
+        while (comparePos(targetToPos, targetCursor) < 0 &&
+               targetLines.prevPos(targetCursor, true)) {
+            assert.ok(sourceLines.prevPos(sourceCursor, true));
+            assert.strictEqual(
+                sourceLines.charAt(sourceCursor),
+                targetLines.charAt(targetCursor)
+            );
+        }
+    }
+
+    return sourceCursor;
+}
diff --git a/lib/options.js b/lib/options.js
new file mode 100644
index 0000000..c8eb04d
--- /dev/null
+++ b/lib/options.js
@@ -0,0 +1,95 @@
+var defaults = {
+    // If you want to use a different branch of esprima, or any other
+    // module that supports a .parse function, pass that module object to
+    // recast.parse as options.esprima.
+    esprima: require("esprima-fb"),
+
+    // Number of spaces the pretty-printer should use per tab for
+    // indentation. If you do not pass this option explicitly, it will be
+    // (quite reliably!) inferred from the original code.
+    tabWidth: 4,
+
+    // If you really want the pretty-printer to use tabs instead of
+    // spaces, make this option true.
+    useTabs: false,
+
+    // The reprinting code leaves leading whitespace untouched unless it
+    // has to reindent a line, or you pass false for this option.
+    reuseWhitespace: true,
+
+    // Override this option to use a different line terminator, e.g. \r\n.
+    lineTerminator: require("os").EOL,
+
+    // Some of the pretty-printer code (such as that for printing function
+    // parameter lists) makes a valiant attempt to prevent really long
+    // lines. You can adjust the limit by changing this option; however,
+    // there is no guarantee that line length will fit inside this limit.
+    wrapColumn: 74, // Aspirational for now.
+
+    // Pass a string as options.sourceFileName to recast.parse to tell the
+    // reprinter to keep track of reused code so that it can construct a
+    // source map automatically.
+    sourceFileName: null,
+
+    // Pass a string as options.sourceMapName to recast.print, and
+    // (provided you passed options.sourceFileName earlier) the
+    // PrintResult of recast.print will have a .map property for the
+    // generated source map.
+    sourceMapName: null,
+
+    // If provided, this option will be passed along to the source map
+    // generator as a root directory for relative source file paths.
+    sourceRoot: null,
+
+    // If you provide a source map that was generated from a previous call
+    // to recast.print as options.inputSourceMap, the old source map will
+    // be composed with the new source map.
+    inputSourceMap: null,
+
+    // If you want esprima to generate .range information (recast only
+    // uses .loc internally), pass true for this option.
+    range: false,
+
+    // If you want esprima not to throw exceptions when it encounters
+    // non-fatal errors, keep this option true.
+    tolerant: true,
+
+    // If you want to override the quotes used in string literals, specify
+    // either "single", "double", or "auto" here ("auto" will select the one
+    // which results in the shorter literal)
+    // Otherwise, the input marks will be preserved
+    quote: null,
+
+    // If you want to print trailing commas in object literals,
+    // array expressions, functions calls and function definitions pass true
+    // for this option.
+    trailingComma: false,
+}, hasOwn = defaults.hasOwnProperty;
+
+// Copy options and fill in default values.
+exports.normalize = function(options) {
+    options = options || defaults;
+
+    function get(key) {
+        return hasOwn.call(options, key)
+            ? options[key]
+            : defaults[key];
+    }
+
+    return {
+        tabWidth: +get("tabWidth"),
+        useTabs: !!get("useTabs"),
+        reuseWhitespace: !!get("reuseWhitespace"),
+        lineTerminator: get("lineTerminator"),
+        wrapColumn: Math.max(get("wrapColumn"), 0),
+        sourceFileName: get("sourceFileName"),
+        sourceMapName: get("sourceMapName"),
+        sourceRoot: get("sourceRoot"),
+        inputSourceMap: get("inputSourceMap"),
+        esprima: get("esprima"),
+        range: get("range"),
+        tolerant: get("tolerant"),
+        quote: get("quote"),
+        trailingComma: get("trailingComma"),
+    };
+};
diff --git a/lib/parser.js b/lib/parser.js
new file mode 100644
index 0000000..ce40743
--- /dev/null
+++ b/lib/parser.js
@@ -0,0 +1,142 @@
+var assert = require("assert");
+var types = require("./types");
+var n = types.namedTypes;
+var b = types.builders;
+var isObject = types.builtInTypes.object;
+var isArray = types.builtInTypes.array;
+var isFunction = types.builtInTypes.function;
+var Patcher = require("./patcher").Patcher;
+var normalizeOptions = require("./options").normalize;
+var fromString = require("./lines").fromString;
+var attachComments = require("./comments").attach;
+var util = require("./util");
+
+exports.parse = function parse(source, options) {
+    options = normalizeOptions(options);
+
+    var lines = fromString(source, options);
+
+    var sourceWithoutTabs = lines.toString({
+        tabWidth: options.tabWidth,
+        reuseWhitespace: false,
+        useTabs: false
+    });
+
+    var comments = [];
+    var program = options.esprima.parse(sourceWithoutTabs, {
+        loc: true,
+        locations: true,
+        range: options.range,
+        comment: true,
+        onComment: comments,
+        tolerant: options.tolerant,
+        ecmaVersion: 6,
+        sourceType: 'module'
+    });
+
+    program.loc = program.loc || {
+        start: lines.firstPos(),
+        end: lines.lastPos()
+    };
+
+    program.loc.lines = lines;
+    program.loc.indent = 0;
+
+    // Expand the Program node's .loc to include all comments, since
+    // typically its .loc.start and .loc.end will coincide with those of
+    // the first and last statements, respectively, excluding any comments
+    // that fall outside that region.
+    var trueProgramLoc = util.getTrueLoc(program, lines);
+    program.loc.start = trueProgramLoc.start;
+    program.loc.end = trueProgramLoc.end;
+
+    if (program.comments) {
+        comments = program.comments;
+        delete program.comments;
+    }
+
+    // In order to ensure we reprint leading and trailing program
+    // comments, wrap the original Program node with a File node.
+    var file = b.file(program);
+    file.loc = {
+        lines: lines,
+        indent: 0,
+        start: lines.firstPos(),
+        end: lines.lastPos()
+    };
+
+    // Passing file.program here instead of just file means that initial
+    // comments will be attached to program.body[0] instead of program.
+    attachComments(
+        comments,
+        program.body.length ? file.program : file,
+        lines
+    );
+
+    // Return a copy of the original AST so that any changes made may be
+    // compared to the original.
+    return new TreeCopier(lines).copy(file);
+};
+
+function TreeCopier(lines) {
+    assert.ok(this instanceof TreeCopier);
+    this.lines = lines;
+    this.indent = 0;
+}
+
+var TCp = TreeCopier.prototype;
+
+TCp.copy = function(node) {
+    if (isArray.check(node)) {
+        return node.map(this.copy, this);
+    }
+
+    if (!isObject.check(node)) {
+        return node;
+    }
+
+    util.fixFaultyLocations(node);
+
+    var copy = Object.create(Object.getPrototypeOf(node), {
+        original: { // Provide a link from the copy to the original.
+            value: node,
+            configurable: false,
+            enumerable: false,
+            writable: true
+        }
+    });
+
+    var loc = node.loc;
+    var oldIndent = this.indent;
+    var newIndent = oldIndent;
+
+    if (loc) {
+        // When node is a comment, we set node.loc.indent to
+        // node.loc.start.column so that, when/if we print the comment by
+        // itself, we can strip that much whitespace from the left margin
+        // of the comment. This only really matters for multiline Block
+        // comments, but it doesn't hurt for Line comments.
+        if (node.type === "Block" || node.type === "Line" ||
+            this.lines.isPrecededOnlyByWhitespace(loc.start)) {
+            newIndent = this.indent = loc.start.column;
+        }
+
+        loc.lines = this.lines;
+        loc.indent = newIndent;
+    }
+
+    var keys = Object.keys(node);
+    var keyCount = keys.length;
+    for (var i = 0; i < keyCount; ++i) {
+        var key = keys[i];
+        if (key === "loc") {
+            copy[key] = node[key];
+        } else {
+            copy[key] = this.copy(node[key]);
+        }
+    }
+
+    this.indent = oldIndent;
+
+    return copy;
+};
diff --git a/lib/patcher.js b/lib/patcher.js
new file mode 100644
index 0000000..b9eb986
--- /dev/null
+++ b/lib/patcher.js
@@ -0,0 +1,480 @@
+var assert = require("assert");
+var linesModule = require("./lines");
+var types = require("./types");
+var getFieldValue = types.getFieldValue;
+var Printable = types.namedTypes.Printable;
+var Expression = types.namedTypes.Expression;
+var SourceLocation = types.namedTypes.SourceLocation;
+var util = require("./util");
+var comparePos = util.comparePos;
+var FastPath = require("./fast-path");
+var isObject = types.builtInTypes.object;
+var isArray = types.builtInTypes.array;
+var isString = types.builtInTypes.string;
+var riskyAdjoiningCharExp = /[0-9a-z_$]/i;
+
+function Patcher(lines) {
+    assert.ok(this instanceof Patcher);
+    assert.ok(lines instanceof linesModule.Lines);
+
+    var self = this,
+        replacements = [];
+
+    self.replace = function(loc, lines) {
+        if (isString.check(lines))
+            lines = linesModule.fromString(lines);
+
+        replacements.push({
+            lines: lines,
+            start: loc.start,
+            end: loc.end
+        });
+    };
+
+    self.get = function(loc) {
+        // If no location is provided, return the complete Lines object.
+        loc = loc || {
+            start: { line: 1, column: 0 },
+            end: { line: lines.length,
+                   column: lines.getLineLength(lines.length) }
+        };
+
+        var sliceFrom = loc.start,
+            toConcat = [];
+
+        function pushSlice(from, to) {
+            assert.ok(comparePos(from, to) <= 0);
+            toConcat.push(lines.slice(from, to));
+        }
+
+        replacements.sort(function(a, b) {
+            return comparePos(a.start, b.start);
+        }).forEach(function(rep) {
+            if (comparePos(sliceFrom, rep.start) > 0) {
+                // Ignore nested replacement ranges.
+            } else {
+                pushSlice(sliceFrom, rep.start);
+                toConcat.push(rep.lines);
+                sliceFrom = rep.end;
+            }
+        });
+
+        pushSlice(sliceFrom, loc.end);
+
+        return linesModule.concat(toConcat);
+    };
+}
+exports.Patcher = Patcher;
+
+var Pp = Patcher.prototype;
+
+Pp.tryToReprintComments = function(newNode, oldNode, print) {
+    var patcher = this;
+
+    if (!newNode.comments &&
+        !oldNode.comments) {
+        // We were (vacuously) able to reprint all the comments!
+        return true;
+    }
+
+    var newPath = FastPath.from(newNode);
+    var oldPath = FastPath.from(oldNode);
+
+    newPath.stack.push("comments", getSurroundingComments(newNode));
+    oldPath.stack.push("comments", getSurroundingComments(oldNode));
+
+    var reprints = [];
+    var ableToReprintComments =
+        findArrayReprints(newPath, oldPath, reprints);
+
+    // No need to pop anything from newPath.stack or oldPath.stack, since
+    // newPath and oldPath are fresh local variables.
+
+    if (ableToReprintComments && reprints.length > 0) {
+        reprints.forEach(function(reprint) {
+            var oldComment = reprint.oldPath.getValue();
+            assert.ok(oldComment.leading || oldComment.trailing);
+            patcher.replace(
+                oldComment.loc,
+                // Comments can't have .comments, so it doesn't matter
+                // whether we print with comments or without.
+                print(reprint.newPath).indentTail(oldComment.loc.indent)
+            );
+        });
+    }
+
+    return ableToReprintComments;
+};
+
+// Get all comments that are either leading or trailing, ignoring any
+// comments that occur inside node.loc. Returns an empty array for nodes
+// with no leading or trailing comments.
+function getSurroundingComments(node) {
+    var result = [];
+    if (node.comments &&
+        node.comments.length > 0) {
+        node.comments.forEach(function(comment) {
+            if (comment.leading || comment.trailing) {
+                result.push(comment);
+            }
+        });
+    }
+    return result;
+}
+
+Pp.deleteComments = function(node) {
+    if (!node.comments) {
+        return;
+    }
+
+    var patcher = this;
+
+    node.comments.forEach(function(comment) {
+        if (comment.leading) {
+            // Delete leading comments along with any trailing whitespace
+            // they might have.
+            patcher.replace({
+                start: comment.loc.start,
+                end: node.loc.lines.skipSpaces(
+                    comment.loc.end, false, false)
+            }, "");
+
+        } else if (comment.trailing) {
+            // Delete trailing comments along with any leading whitespace
+            // they might have.
+            patcher.replace({
+                start: node.loc.lines.skipSpaces(
+                    comment.loc.start, true, false),
+                end: comment.loc.end
+            }, "");
+        }
+    });
+};
+
+exports.getReprinter = function(path) {
+    assert.ok(path instanceof FastPath);
+
+    // Make sure that this path refers specifically to a Node, rather than
+    // some non-Node subproperty of a Node.
+    var node = path.getValue();
+    if (!Printable.check(node))
+        return;
+
+    var orig = node.original;
+    var origLoc = orig && orig.loc;
+    var lines = origLoc && origLoc.lines;
+    var reprints = [];
+
+    if (!lines || !findReprints(path, reprints))
+        return;
+
+    return function(print) {
+        var patcher = new Patcher(lines);
+
+        reprints.forEach(function(reprint) {
+            var newNode = reprint.newPath.getValue();
+            var oldNode = reprint.oldPath.getValue();
+
+            SourceLocation.assert(oldNode.loc, true);
+
+            var needToPrintNewPathWithComments =
+                !patcher.tryToReprintComments(newNode, oldNode, print)
+
+            if (needToPrintNewPathWithComments) {
+                // Since we were not able to preserve all leading/trailing
+                // comments, we delete oldNode's comments, print newPath
+                // with comments, and then patch the resulting lines where
+                // oldNode used to be.
+                patcher.deleteComments(oldNode);
+            }
+
+            var pos = util.copyPos(oldNode.loc.start);
+            var needsLeadingSpace = lines.prevPos(pos) &&
+                riskyAdjoiningCharExp.test(lines.charAt(pos));
+
+            var newLines = print(
+                reprint.newPath,
+                needToPrintNewPathWithComments
+            ).indentTail(oldNode.loc.indent);
+
+            var needsTrailingSpace =
+                riskyAdjoiningCharExp.test(lines.charAt(oldNode.loc.end));
+
+            // If we try to replace the argument of a ReturnStatement like
+            // return"asdf" with e.g. a literal null expression, we run
+            // the risk of ending up with returnnull, so we need to add an
+            // extra leading space in situations where that might
+            // happen. Likewise for "asdf"in obj. See #170.
+            if (needsLeadingSpace || needsTrailingSpace) {
+                var newParts = [];
+                needsLeadingSpace && newParts.push(" ");
+                newParts.push(newLines);
+                needsTrailingSpace && newParts.push(" ");
+                newLines = linesModule.concat(newParts);
+            }
+
+            patcher.replace(oldNode.loc, newLines);
+        });
+
+        // Recall that origLoc is the .loc of an ancestor node that is
+        // guaranteed to contain all the reprinted nodes and comments.
+        return patcher.get(origLoc).indentTail(-orig.loc.indent);
+    };
+};
+
+function findReprints(newPath, reprints) {
+    var newNode = newPath.getValue();
+    Printable.assert(newNode);
+
+    var oldNode = newNode.original;
+    Printable.assert(oldNode);
+
+    assert.deepEqual(reprints, []);
+
+    if (newNode.type !== oldNode.type) {
+        return false;
+    }
+
+    var oldPath = new FastPath(oldNode);
+    var canReprint = findChildReprints(newPath, oldPath, reprints);
+
+    if (!canReprint) {
+        // Make absolutely sure the calling code does not attempt to reprint
+        // any nodes.
+        reprints.length = 0;
+    }
+
+    return canReprint;
+}
+
+function findAnyReprints(newPath, oldPath, reprints) {
+    var newNode = newPath.getValue();
+    var oldNode = oldPath.getValue();
+
+    if (newNode === oldNode)
+        return true;
+
+    if (isArray.check(newNode))
+        return findArrayReprints(newPath, oldPath, reprints);
+
+    if (isObject.check(newNode))
+        return findObjectReprints(newPath, oldPath, reprints);
+
+    return false;
+}
+
+function findArrayReprints(newPath, oldPath, reprints) {
+    var newNode = newPath.getValue();
+    var oldNode = oldPath.getValue();
+    isArray.assert(newNode);
+    var len = newNode.length;
+
+    if (!(isArray.check(oldNode) &&
+          oldNode.length === len))
+        return false;
+
+    for (var i = 0; i < len; ++i) {
+        newPath.stack.push(i, newNode[i]);
+        oldPath.stack.push(i, oldNode[i]);
+        var canReprint = findAnyReprints(newPath, oldPath, reprints);
+        newPath.stack.length -= 2;
+        oldPath.stack.length -= 2;
+        if (!canReprint) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+function findObjectReprints(newPath, oldPath, reprints) {
+    var newNode = newPath.getValue();
+    isObject.assert(newNode);
+
+    if (newNode.original === null) {
+        // If newNode.original node was set to null, reprint the node.
+        return false;
+    }
+
+    var oldNode = oldPath.getValue();
+    if (!isObject.check(oldNode))
+        return false;
+
+    if (Printable.check(newNode)) {
+        if (!Printable.check(oldNode)) {
+            return false;
+        }
+
+        // Here we need to decide whether the reprinted code for newNode
+        // is appropriate for patching into the location of oldNode.
+
+        if (newNode.type === oldNode.type) {
+            var childReprints = [];
+
+            if (findChildReprints(newPath, oldPath, childReprints)) {
+                reprints.push.apply(reprints, childReprints);
+            } else if (oldNode.loc) {
+                // If we have no .loc information for oldNode, then we
+                // won't be able to reprint it.
+                reprints.push({
+                    oldPath: oldPath.copy(),
+                    newPath: newPath.copy()
+                });
+            } else {
+                return false;
+            }
+
+            return true;
+        }
+
+        if (Expression.check(newNode) &&
+            Expression.check(oldNode) &&
+            // If we have no .loc information for oldNode, then we won't
+            // be able to reprint it.
+            oldNode.loc) {
+
+            // If both nodes are subtypes of Expression, then we should be
+            // able to fill the location occupied by the old node with
+            // code printed for the new node with no ill consequences.
+            reprints.push({
+                oldPath: oldPath.copy(),
+                newPath: newPath.copy()
+            });
+
+            return true;
+        }
+
+        // The nodes have different types, and at least one of the types
+        // is not a subtype of the Expression type, so we cannot safely
+        // assume the nodes are syntactically interchangeable.
+        return false;
+    }
+
+    return findChildReprints(newPath, oldPath, reprints);
+}
+
+// This object is reused in hasOpeningParen and hasClosingParen to avoid
+// having to allocate a temporary object.
+var reusablePos = { line: 1, column: 0 };
+var nonSpaceExp = /\S/;
+
+function hasOpeningParen(oldPath) {
+    var oldNode = oldPath.getValue();
+    var loc = oldNode.loc;
+    var lines = loc && loc.lines;
+
+    if (lines) {
+        var pos = reusablePos;
+        pos.line = loc.start.line;
+        pos.column = loc.start.column;
+
+        while (lines.prevPos(pos)) {
+            var ch = lines.charAt(pos);
+
+            if (ch === "(") {
+                // If we found an opening parenthesis but it occurred before
+                // the start of the original subtree for this reprinting, then
+                // we must not return true for hasOpeningParen(oldPath).
+                return comparePos(oldPath.getRootValue().loc.start, pos) <= 0;
+            }
+
+            if (nonSpaceExp.test(ch)) {
+                return false;
+            }
+        }
+    }
+
+    return false;
+}
+
+function hasClosingParen(oldPath) {
+    var oldNode = oldPath.getValue();
+    var loc = oldNode.loc;
+    var lines = loc && loc.lines;
+
+    if (lines) {
+        var pos = reusablePos;
+        pos.line = loc.end.line;
+        pos.column = loc.end.column;
+
+        do {
+            var ch = lines.charAt(pos);
+
+            if (ch === ")") {
+                // If we found a closing parenthesis but it occurred after the
+                // end of the original subtree for this reprinting, then we
+                // must not return true for hasClosingParen(oldPath).
+                return comparePos(pos, oldPath.getRootValue().loc.end) <= 0;
+            }
+
+            if (nonSpaceExp.test(ch)) {
+                return false;
+            }
+
+        } while (lines.nextPos(pos));
+    }
+
+    return false;
+}
+
+function hasParens(oldPath) {
+    // This logic can technically be fooled if the node has parentheses
+    // but there are comments intervening between the parentheses and the
+    // node. In such cases the node will be harmlessly wrapped in an
+    // additional layer of parentheses.
+    return hasOpeningParen(oldPath) && hasClosingParen(oldPath);
+}
+
+function findChildReprints(newPath, oldPath, reprints) {
+    var newNode = newPath.getValue();
+    var oldNode = oldPath.getValue();
+
+    isObject.assert(newNode);
+    isObject.assert(oldNode);
+
+    if (newNode.original === null) {
+        // If newNode.original node was set to null, reprint the node.
+        return false;
+    }
+
+    // If this type of node cannot come lexically first in its enclosing
+    // statement (e.g. a function expression or object literal), and it
+    // seems to be doing so, then the only way we can ignore this problem
+    // and save ourselves from falling back to the pretty printer is if an
+    // opening parenthesis happens to precede the node.  For example,
+    // (function(){ ... }()); does not need to be reprinted, even though
+    // the FunctionExpression comes lexically first in the enclosing
+    // ExpressionStatement and fails the hasParens test, because the
+    // parent CallExpression passes the hasParens test. If we relied on
+    // the path.needsParens() && !hasParens(oldNode) check below, the
+    // absence of a closing parenthesis after the FunctionExpression would
+    // trigger pretty-printing unnecessarily.
+    if (!newPath.canBeFirstInStatement() &&
+        newPath.firstInStatement() &&
+        !hasOpeningParen(oldPath))
+        return false;
+
+    // If this node needs parentheses and will not be wrapped with
+    // parentheses when reprinted, then return false to skip reprinting
+    // and let it be printed generically.
+    if (newPath.needsParens(true) && !hasParens(oldPath)) {
+        return false;
+    }
+
+    for (var k in util.getUnionOfKeys(newNode, oldNode)) {
+        if (k === "loc")
+            continue;
+
+        newPath.stack.push(k, types.getFieldValue(newNode, k));
+        oldPath.stack.push(k, types.getFieldValue(oldNode, k));
+        var canReprint = findAnyReprints(newPath, oldPath, reprints);
+        newPath.stack.length -= 2;
+        oldPath.stack.length -= 2;
+
+        if (!canReprint) {
+            return false;
+        }
+    }
+
+    return true;
+}
diff --git a/lib/printer.js b/lib/printer.js
new file mode 100644
index 0000000..53ea319
--- /dev/null
+++ b/lib/printer.js
@@ -0,0 +1,1763 @@
+var assert = require("assert");
+var sourceMap = require("source-map");
+var printComments = require("./comments").printComments;
+var linesModule = require("./lines");
+var fromString = linesModule.fromString;
+var concat = linesModule.concat;
+var normalizeOptions = require("./options").normalize;
+var getReprinter = require("./patcher").getReprinter;
+var types = require("./types");
+var namedTypes = types.namedTypes;
+var isString = types.builtInTypes.string;
+var isObject = types.builtInTypes.object;
+var FastPath = require("./fast-path");
+var util = require("./util");
+
+function PrintResult(code, sourceMap) {
+    assert.ok(this instanceof PrintResult);
+
+    isString.assert(code);
+    this.code = code;
+
+    if (sourceMap) {
+        isObject.assert(sourceMap);
+        this.map = sourceMap;
+    }
+}
+
+var PRp = PrintResult.prototype;
+var warnedAboutToString = false;
+
+PRp.toString = function() {
+    if (!warnedAboutToString) {
+        console.warn(
+            "Deprecation warning: recast.print now returns an object with " +
+            "a .code property. You appear to be treating the object as a " +
+            "string, which might still work but is strongly discouraged."
+        );
+
+        warnedAboutToString = true;
+    }
+
+    return this.code;
+};
+
+var emptyPrintResult = new PrintResult("");
+
+function Printer(originalOptions) {
+    assert.ok(this instanceof Printer);
+
+    var explicitTabWidth = originalOptions && originalOptions.tabWidth;
+    var options = normalizeOptions(originalOptions);
+    assert.notStrictEqual(options, originalOptions);
+
+    // It's common for client code to pass the same options into both
+    // recast.parse and recast.print, but the Printer doesn't need (and
+    // can be confused by) options.sourceFileName, so we null it out.
+    options.sourceFileName = null;
+
+    function printWithComments(path) {
+        assert.ok(path instanceof FastPath);
+        return printComments(path, print);
+    }
+
+    function print(path, includeComments) {
+        if (includeComments)
+            return printWithComments(path);
+
+        assert.ok(path instanceof FastPath);
+
+        if (!explicitTabWidth) {
+            var oldTabWidth = options.tabWidth;
+            var loc = path.getNode().loc;
+            if (loc && loc.lines && loc.lines.guessTabWidth) {
+                options.tabWidth = loc.lines.guessTabWidth();
+                var lines = maybeReprint(path);
+                options.tabWidth = oldTabWidth;
+                return lines;
+            }
+        }
+
+        return maybeReprint(path);
+    }
+
+    function maybeReprint(path) {
+        var reprinter = getReprinter(path);
+        if (reprinter)
+            return maybeAddParens(path, reprinter(print));
+        return printRootGenerically(path);
+    }
+
+    // Print the root node generically, but then resume reprinting its
+    // children non-generically.
+    function printRootGenerically(path) {
+        return genericPrint(path, options, printWithComments);
+    }
+
+    // Print the entire AST generically.
+    function printGenerically(path) {
+        return genericPrint(path, options, printGenerically);
+    }
+
+    this.print = function(ast) {
+        if (!ast) {
+            return emptyPrintResult;
+        }
+
+        var lines = print(FastPath.from(ast), true);
+
+        return new PrintResult(
+            lines.toString(options),
+            util.composeSourceMaps(
+                options.inputSourceMap,
+                lines.getSourceMap(
+                    options.sourceMapName,
+                    options.sourceRoot
+                )
+            )
+        );
+    };
+
+    this.printGenerically = function(ast) {
+        if (!ast) {
+            return emptyPrintResult;
+        }
+
+        var path = FastPath.from(ast);
+        var oldReuseWhitespace = options.reuseWhitespace;
+
+        // Do not reuse whitespace (or anything else, for that matter)
+        // when printing generically.
+        options.reuseWhitespace = false;
+
+        // TODO Allow printing of comments?
+        var pr = new PrintResult(printGenerically(path).toString(options));
+        options.reuseWhitespace = oldReuseWhitespace;
+        return pr;
+    };
+}
+
+exports.Printer = Printer;
+
+function maybeAddParens(path, lines) {
+    return path.needsParens() ? concat(["(", lines, ")"]) : lines;
+}
+
+function genericPrint(path, options, printPath) {
+    assert.ok(path instanceof FastPath);
+    return maybeAddParens(path, genericPrintNoParens(path, options, printPath));
+}
+
+function genericPrintNoParens(path, options, print) {
+    var n = path.getValue();
+
+    if (!n) {
+        return fromString("");
+    }
+
+    if (typeof n === "string") {
+        return fromString(n, options);
+    }
+
+    namedTypes.Printable.assert(n);
+
+    switch (n.type) {
+    case "File":
+        return path.call(print, "program");
+
+    case "Program":
+        return path.call(function(bodyPath) {
+            return printStatementSequence(bodyPath, options, print);
+        }, "body");
+
+    case "Noop": // Babel extension.
+    case "EmptyStatement":
+        return fromString("");
+
+    case "ExpressionStatement":
+        return concat([path.call(print, "expression"), ";"]);
+
+    case "ParenthesizedExpression": // Babel extension.
+        return concat(["(", path.call(print, "expression"), ")"]);
+
+    case "BinaryExpression":
+    case "LogicalExpression":
+    case "AssignmentExpression":
+        return fromString(" ").join([
+            path.call(print, "left"),
+            n.operator,
+            path.call(print, "right")
+        ]);
+
+    case "AssignmentPattern":
+        return concat([
+            path.call(print, "left"),
+            "=",
+            path.call(print, "right")
+        ]);
+
+    case "MemberExpression":
+        var parts = [path.call(print, "object")];
+
+        var property = path.call(print, "property");
+        if (n.computed) {
+            parts.push("[", property, "]");
+        } else {
+            parts.push(".", property);
+        }
+
+        return concat(parts);
+
+    case "MetaProperty":
+        return concat([
+            path.call(print, "meta"),
+            ".",
+            path.call(print, "property")
+        ]);
+
+    case "BindExpression":
+        var parts = [];
+
+        if (n.object) {
+            parts.push(path.call(print, "object"));
+        }
+
+        parts.push("::", path.call(print, "callee"));
+
+        return concat(parts);
+
+    case "Path":
+        return fromString(".").join(n.body);
+
+    case "Identifier":
+        return concat([
+            fromString(n.name, options),
+            path.call(print, "typeAnnotation")
+        ]);
+
+    case "SpreadElement":
+    case "SpreadElementPattern":
+    case "SpreadProperty":
+    case "SpreadPropertyPattern":
+    case "RestElement":
+        return concat(["...", path.call(print, "argument")]);
+
+    case "FunctionDeclaration":
+    case "FunctionExpression":
+        var parts = [];
+
+        if (n.async)
+            parts.push("async ");
+
+        parts.push("function");
+
+        if (n.generator)
+            parts.push("*");
+
+        if (n.id) {
+            parts.push(
+                " ",
+                path.call(print, "id"),
+                path.call(print, "typeParameters")
+            );
+        }
+
+        parts.push(
+            "(",
+            printFunctionParams(path, options, print),
+            ")",
+            path.call(print, "returnType"),
+            " ",
+            path.call(print, "body")
+        );
+
+        return concat(parts);
+
+    case "ArrowFunctionExpression":
+        var parts = [];
+
+        if (n.async)
+            parts.push("async ");
+
+        if (
+            n.params.length === 1 &&
+            !n.rest &&
+            n.params[0].type !== 'SpreadElementPattern' &&
+            n.params[0].type !== 'RestElement'
+        ) {
+            parts.push(path.call(print, "params", 0));
+        } else {
+            parts.push(
+                "(",
+                printFunctionParams(path, options, print),
+                ")"
+            );
+        }
+
+        parts.push(" => ", path.call(print, "body"));
+
+        return concat(parts);
+
+    case "MethodDefinition":
+        var parts = [];
+
+        if (n.static) {
+            parts.push("static ");
+        }
+
+        parts.push(printMethod(path, options, print));
+
+        return concat(parts);
+
+    case "YieldExpression":
+        var parts = ["yield"];
+
+        if (n.delegate)
+            parts.push("*");
+
+        if (n.argument)
+            parts.push(" ", path.call(print, "argument"));
+
+        return concat(parts);
+
+    case "AwaitExpression":
+        var parts = ["await"];
+
+        if (n.all)
+            parts.push("*");
+
+        if (n.argument)
+            parts.push(" ", path.call(print, "argument"));
+
+        return concat(parts);
+
+    case "ModuleDeclaration":
+        var parts = ["module", path.call(print, "id")];
+
+        if (n.source) {
+            assert.ok(!n.body);
+            parts.push("from", path.call(print, "source"));
+        } else {
+            parts.push(path.call(print, "body"));
+        }
+
+        return fromString(" ").join(parts);
+
+    case "ImportSpecifier":
+        var parts = [];
+
+        if (n.imported) {
+            parts.push(path.call(print, "imported"));
+            if (n.local &&
+                n.local.name !== n.imported.name) {
+                parts.push(" as ", path.call(print, "local"));
+            }
+        } else if (n.id) {
+            parts.push(path.call(print, "id"));
+            if (n.name) {
+                parts.push(" as ", path.call(print, "name"));
+            }
+        }
+
+        return concat(parts);
+
+    case "ExportSpecifier":
+        var parts = [];
+
+        if (n.local) {
+            parts.push(path.call(print, "local"));
+            if (n.exported &&
+                n.exported.name !== n.local.name) {
+                parts.push(" as ", path.call(print, "exported"));
+            }
+        } else if (n.id) {
+            parts.push(path.call(print, "id"));
+            if (n.name) {
+                parts.push(" as ", path.call(print, "name"));
+            }
+        }
+
+        return concat(parts);
+
+    case "ExportBatchSpecifier":
+        return fromString("*");
+
+    case "ImportNamespaceSpecifier":
+        var parts = ["* as "];
+        if (n.local) {
+            parts.push(path.call(print, "local"));
+        } else if (n.id) {
+            parts.push(path.call(print, "id"));
+        }
+        return concat(parts);
+
+    case "ImportDefaultSpecifier":
+        if (n.local) {
+            return path.call(print, "local");
+        }
+        return path.call(print, "id");
+
+    case "ExportDeclaration":
+        var parts = ["export"];
+
+        if (n["default"]) {
+            parts.push(" default");
+
+        } else if (n.specifiers &&
+                   n.specifiers.length > 0) {
+
+            if (n.specifiers.length === 1 &&
+                n.specifiers[0].type === "ExportBatchSpecifier") {
+                parts.push(" *");
+            } else {
+                parts.push(
+                    " { ",
+                    fromString(", ").join(path.map(print, "specifiers")),
+                    " }"
+                );
+            }
+
+            if (n.source)
+                parts.push(" from ", path.call(print, "source"));
+
+            parts.push(";");
+
+            return concat(parts);
+        }
+
+        if (n.declaration) {
+            var decLines = path.call(print, "declaration");
+            parts.push(" ", decLines);
+            if (lastNonSpaceCharacter(decLines) !== ";") {
+                parts.push(";");
+            }
+        }
+
+        return concat(parts);
+
+    case "ExportDefaultDeclaration":
+        return concat([
+            "export default ",
+            path.call(print, "declaration")
+        ]);
+
+    case "ExportNamedDeclaration":
+        var parts = ["export "];
+
+        if (n.declaration) {
+            parts.push(path.call(print, "declaration"));
+        }
+
+        if (n.specifiers &&
+            n.specifiers.length > 0) {
+            parts.push(
+                n.declaration ? ", {" : "{",
+                fromString(", ").join(path.map(print, "specifiers")),
+                "}"
+            );
+        }
+
+        if (n.source) {
+            parts.push(" from ", path.call(print, "source"));
+        }
+
+        return concat(parts);
+
+    case "ExportAllDeclaration":
+        var parts = ["export *"];
+
+        if (n.exported) {
+            parts.push(" as ", path.call(print, "exported"));
+        }
+
+        return concat([
+            " from ",
+            path.call(print, "source")
+        ]);
+
+    case "ExportNamespaceSpecifier":
+        return concat(["* as ", path.call(print, "exported")]);
+
+    case "ExportDefaultSpecifier":
+        return path.call(print, "exported");
+
+    case "ImportDeclaration":
+        var parts = ["import "];
+
+        if (n.importKind && n.importKind !== "value") {
+            parts.push(n.importKind + " ");
+        }
+
+        if (n.specifiers &&
+            n.specifiers.length > 0) {
+
+            var foundImportSpecifier = false;
+
+            path.each(function(specifierPath) {
+                var i = specifierPath.getName();
+                if (i > 0) {
+                    parts.push(", ");
+                }
+
+                var value = specifierPath.getValue();
+
+                if (namedTypes.ImportDefaultSpecifier.check(value) ||
+                    namedTypes.ImportNamespaceSpecifier.check(value)) {
+                    assert.strictEqual(foundImportSpecifier, false);
+                } else {
+                    namedTypes.ImportSpecifier.assert(value);
+                    if (!foundImportSpecifier) {
+                        foundImportSpecifier = true;
+                        parts.push("{");
+                    }
+                }
+
+                parts.push(print(specifierPath));
+            }, "specifiers");
+
+            if (foundImportSpecifier) {
+                parts.push("}");
+            }
+
+            parts.push(" from ");
+        }
+
+        parts.push(path.call(print, "source"), ";");
+
+        return concat(parts);
+
+    case "BlockStatement":
+        var naked = path.call(function(bodyPath) {
+            return printStatementSequence(bodyPath, options, print);
+        }, "body");
+
+        if (naked.isEmpty()) {
+            return fromString("{}");
+        }
+
+        return concat([
+            "{\n",
+            naked.indent(options.tabWidth),
+            "\n}"
+        ]);
+
+    case "ReturnStatement":
+        var parts = ["return"];
+
+        if (n.argument) {
+            var argLines = path.call(print, "argument");
+            if (argLines.length > 1 &&
+                (namedTypes.XJSElement &&
+                 namedTypes.XJSElement.check(n.argument) ||
+                 namedTypes.JSXElement &&
+                 namedTypes.JSXElement.check(n.argument))) {
+                parts.push(
+                    " (\n",
+                    argLines.indent(options.tabWidth),
+                    "\n)"
+                );
+            } else {
+                parts.push(" ", argLines);
+            }
+        }
+
+        parts.push(";");
+
+        return concat(parts);
+
+    case "CallExpression":
+        return concat([
+            path.call(print, "callee"),
+            printArgumentsList(path, options, print)
+        ]);
+
+    case "ObjectExpression":
+    case "ObjectPattern":
+    case "ObjectTypeAnnotation":
+        var allowBreak = false;
+        var isTypeAnnotation = n.type === "ObjectTypeAnnotation";
+        var separator = isTypeAnnotation ? ';' : ',';
+        var fields = [];
+
+        if (isTypeAnnotation) {
+            fields.push("indexers", "callProperties");
+        }
+
+        fields.push("properties");
+
+        var len = 0;
+        fields.forEach(function(field) {
+            len += n[field].length;
+        });
+
+        var oneLine = (isTypeAnnotation && len === 1) || len === 0;
+        var parts = [oneLine ? "{" : "{\n"];
+
+        var i = 0;
+        fields.forEach(function(field) {
+            path.each(function(childPath) {
+                var lines = print(childPath);
+
+                if (!oneLine) {
+                    lines = lines.indent(options.tabWidth);
+                }
+
+                var multiLine = !isTypeAnnotation && lines.length > 1;
+                if (multiLine && allowBreak) {
+                    // Similar to the logic for BlockStatement.
+                    parts.push("\n");
+                }
+
+                parts.push(lines);
+
+                if (i < len - 1) {
+                    // Add an extra line break if the previous object property
+                    // had a multi-line value.
+                    parts.push(separator + (multiLine ? "\n\n" : "\n"));
+                    allowBreak = !multiLine;
+                } else if (len !== 1 && isTypeAnnotation) {
+                    parts.push(separator);
+                } else if (options.trailingComma) {
+                    parts.push(separator);
+                }
+                i++;
+            }, field);
+        });
+
+        parts.push(oneLine ? "}" : "\n}");
+
+        return concat(parts);
+
+    case "PropertyPattern":
+        return concat([
+            path.call(print, "key"),
+            ": ",
+            path.call(print, "pattern")
+        ]);
+
+    case "Property": // Non-standard AST node type.
+        if (n.method || n.kind === "get" || n.kind === "set") {
+            return printMethod(path, options, print);
+        }
+
+        var parts = [];
+
+        if (n.decorators) {
+            path.each(function(decoratorPath) {
+                parts.push(print(decoratorPath), "\n");
+            }, "decorators");
+        }
+
+        var key = path.call(print, "key");
+        if (n.computed) {
+            parts.push("[", key, "]");
+        } else {
+            parts.push(key);
+        }
+
+        if (! n.shorthand) {
+            parts.push(": ", path.call(print, "value"));
+        }
+
+        return concat(parts);
+
+    case "Decorator":
+        return concat(["@", path.call(print, "expression")]);
+
+    case "ArrayExpression":
+    case "ArrayPattern":
+        var elems = n.elements,
+            len = elems.length;
+
+        var printed = path.map(print, "elements");
+        var joined = fromString(", ").join(printed);
+        var oneLine = joined.getLineLength(1) <= options.wrapColumn;
+        var parts = [oneLine ? "[" : "[\n"];
+
+        path.each(function(elemPath) {
+            var i = elemPath.getName();
+            var elem = elemPath.getValue();
+            if (!elem) {
+                // If the array expression ends with a hole, that hole
+                // will be ignored by the interpreter, but if it ends with
+                // two (or more) holes, we need to write out two (or more)
+                // commas so that the resulting code is interpreted with
+                // both (all) of the holes.
+                parts.push(",");
+            } else {
+                var lines = printed[i];
+                if (oneLine) {
+                    if (i > 0)
+                        parts.push(" ");
+                } else {
+                    lines = lines.indent(options.tabWidth);
+                }
+                parts.push(lines);
+                if (i < len - 1 || (!oneLine && options.trailingComma))
+                    parts.push(",");
+                if (!oneLine)
+                    parts.push("\n");
+            }
+        }, "elements");
+
+        parts.push("]");
+
+        return concat(parts);
+
+    case "SequenceExpression":
+        return fromString(", ").join(path.map(print, "expressions"));
+
+    case "ThisExpression":
+        return fromString("this");
+
+    case "Super":
+        return fromString("super");
+
+    case "Literal":
+        if (typeof n.value !== "string")
+            return fromString(n.value, options);
+
+        return fromString(nodeStr(n.value, options), options);
+
+    case "ModuleSpecifier":
+        if (n.local) {
+            throw new Error(
+                "The ESTree ModuleSpecifier type should be abstract"
+            );
+        }
+
+        // The Esprima ModuleSpecifier type is just a string-valued
+        // Literal identifying the imported-from module.
+        return fromString(nodeStr(n.value, options), options);
+
+    case "UnaryExpression":
+        var parts = [n.operator];
+        if (/[a-z]$/.test(n.operator))
+            parts.push(" ");
+        parts.push(path.call(print, "argument"));
+        return concat(parts);
+
+    case "UpdateExpression":
+        var parts = [path.call(print, "argument"), n.operator];
+
+        if (n.prefix)
+            parts.reverse();
+
+        return concat(parts);
+
+    case "ConditionalExpression":
+        return concat([
+            "(", path.call(print, "test"),
+            " ? ", path.call(print, "consequent"),
+            " : ", path.call(print, "alternate"), ")"
+        ]);
+
+    case "NewExpression":
+        var parts = ["new ", path.call(print, "callee")];
+        var args = n.arguments;
+        if (args) {
+            parts.push(printArgumentsList(path, options, print));
+        }
+
+        return concat(parts);
+
+    case "VariableDeclaration":
+        var parts = [n.kind, " "];
+        var maxLen = 0;
+        var printed = path.map(function(childPath) {
+            var lines = print(childPath);
+            maxLen = Math.max(lines.length, maxLen);
+            return lines;
+        }, "declarations");
+
+        if (maxLen === 1) {
+            parts.push(fromString(", ").join(printed));
+        } else if (printed.length > 1 ) {
+            parts.push(
+                fromString(",\n").join(printed)
+                    .indentTail(n.kind.length + 1)
+            );
+        } else {
+            parts.push(printed[0]);
+        }
+
+        // We generally want to terminate all variable declarations with a
+        // semicolon, except when they are children of for loops.
+        var parentNode = path.getParentNode();
+        if (!namedTypes.ForStatement.check(parentNode) &&
+            !namedTypes.ForInStatement.check(parentNode) &&
+            !(namedTypes.ForOfStatement &&
+              namedTypes.ForOfStatement.check(parentNode))) {
+            parts.push(";");
+        }
+
+        return concat(parts);
+
+    case "VariableDeclarator":
+        return n.init ? fromString(" = ").join([
+            path.call(print, "id"),
+            path.call(print, "init")
+        ]) : path.call(print, "id");
+
+    case "WithStatement":
+        return concat([
+            "with (",
+            path.call(print, "object"),
+            ") ",
+            path.call(print, "body")
+        ]);
+
+    case "IfStatement":
+        var con = adjustClause(path.call(print, "consequent"), options),
+            parts = ["if (", path.call(print, "test"), ")", con];
+
+        if (n.alternate)
+            parts.push(
+                endsWithBrace(con) ? " else" : "\nelse",
+                adjustClause(path.call(print, "alternate"), options));
+
+        return concat(parts);
+
+    case "ForStatement":
+        // TODO Get the for (;;) case right.
+        var init = path.call(print, "init"),
+            sep = init.length > 1 ? ";\n" : "; ",
+            forParen = "for (",
+            indented = fromString(sep).join([
+                init,
+                path.call(print, "test"),
+                path.call(print, "update")
+            ]).indentTail(forParen.length),
+            head = concat([forParen, indented, ")"]),
+            clause = adjustClause(path.call(print, "body"), options),
+            parts = [head];
+
+        if (head.length > 1) {
+            parts.push("\n");
+            clause = clause.trimLeft();
+        }
+
+        parts.push(clause);
+
+        return concat(parts);
+
+    case "WhileStatement":
+        return concat([
+            "while (",
+            path.call(print, "test"),
+            ")",
+            adjustClause(path.call(print, "body"), options)
+        ]);
+
+    case "ForInStatement":
+        // Note: esprima can't actually parse "for each (".
+        return concat([
+            n.each ? "for each (" : "for (",
+            path.call(print, "left"),
+            " in ",
+            path.call(print, "right"),
+            ")",
+            adjustClause(path.call(print, "body"), options)
+        ]);
+
+    case "ForOfStatement":
+        return concat([
+            "for (",
+            path.call(print, "left"),
+            " of ",
+            path.call(print, "right"),
+            ")",
+            adjustClause(path.call(print, "body"), options)
+        ]);
+
+    case "DoWhileStatement":
+        var doBody = concat([
+            "do",
+            adjustClause(path.call(print, "body"), options)
+        ]), parts = [doBody];
+
+        if (endsWithBrace(doBody))
+            parts.push(" while");
+        else
+            parts.push("\nwhile");
+
+        parts.push(" (", path.call(print, "test"), ");");
+
+        return concat(parts);
+
+    case "DoExpression":
+        var statements = path.call(function(bodyPath) {
+            return printStatementSequence(bodyPath, options, print);
+        }, "body");
+
+        return concat([
+            "do {\n",
+            statements.indent(options.tabWidth),
+            "\n}"
+        ]);
+
+    case "BreakStatement":
+        var parts = ["break"];
+        if (n.label)
+            parts.push(" ", path.call(print, "label"));
+        parts.push(";");
+        return concat(parts);
+
+    case "ContinueStatement":
+        var parts = ["continue"];
+        if (n.label)
+            parts.push(" ", path.call(print, "label"));
+        parts.push(";");
+        return concat(parts);
+
+    case "LabeledStatement":
+        return concat([
+            path.call(print, "label"),
+            ":\n",
+            path.call(print, "body")
+        ]);
+
+    case "TryStatement":
+        var parts = [
+            "try ",
+            path.call(print, "block")
+        ];
+
+        if (n.handler) {
+            parts.push(" ", path.call(print, "handler"));
+        } else if (n.handlers) {
+            path.each(function(handlerPath) {
+                parts.push(" ", print(handlerPath));
+            }, "handlers");
+        }
+
+        if (n.finalizer) {
+            parts.push(" finally ", path.call(print, "finalizer"));
+        }
+
+        return concat(parts);
+
+    case "CatchClause":
+        var parts = ["catch (", path.call(print, "param")];
+
+        if (n.guard)
+            // Note: esprima does not recognize conditional catch clauses.
+            parts.push(" if ", path.call(print, "guard"));
+
+        parts.push(") ", path.call(print, "body"));
+
+        return concat(parts);
+
+    case "ThrowStatement":
+        return concat(["throw ", path.call(print, "argument"), ";"]);
+
+    case "SwitchStatement":
+        return concat([
+            "switch (",
+            path.call(print, "discriminant"),
+            ") {\n",
+            fromString("\n").join(path.map(print, "cases")),
+            "\n}"
+        ]);
+
+        // Note: ignoring n.lexical because it has no printing consequences.
+
+    case "SwitchCase":
+        var parts = [];
+
+        if (n.test)
+            parts.push("case ", path.call(print, "test"), ":");
+        else
+            parts.push("default:");
+
+        if (n.consequent.length > 0) {
+            parts.push("\n", path.call(function(consequentPath) {
+                return printStatementSequence(consequentPath, options, print);
+            }, "consequent").indent(options.tabWidth));
+        }
+
+        return concat(parts);
+
+    case "DebuggerStatement":
+        return fromString("debugger;");
+
+    // JSX extensions below.
+
+    case "XJSAttribute":
+    case "JSXAttribute":
+        var parts = [path.call(print, "name")];
+        if (n.value)
+            parts.push("=", path.call(print, "value"));
+        return concat(parts);
+
+    case "XJSIdentifier":
+    case "JSXIdentifier":
+        return fromString(n.name, options);
+
+    case "XJSNamespacedName":
+    case "JSXNamespacedName":
+        return fromString(":").join([
+            path.call(print, "namespace"),
+            path.call(print, "name")
+        ]);
+
+    case "XJSMemberExpression":
+    case "JSXMemberExpression":
+        return fromString(".").join([
+            path.call(print, "object"),
+            path.call(print, "property")
+        ]);
+
+    case "XJSSpreadAttribute":
+    case "JSXSpreadAttribute":
+        return concat(["{...", path.call(print, "argument"), "}"]);
+
+    case "XJSExpressionContainer":
+    case "JSXExpressionContainer":
+        return concat(["{", path.call(print, "expression"), "}"]);
+
+    case "XJSElement":
+    case "JSXElement":
+        var openingLines = path.call(print, "openingElement");
+
+        if (n.openingElement.selfClosing) {
+            assert.ok(!n.closingElement);
+            return openingLines;
+        }
+
+        var childLines = concat(
+            path.map(function(childPath) {
+                var child = childPath.getValue();
+
+                if (namedTypes.Literal.check(child) &&
+                    typeof child.value === "string") {
+                    if (/\S/.test(child.value)) {
+                        return child.value.replace(/^\s+|\s+$/g, "");
+                    } else if (/\n/.test(child.value)) {
+                        return "\n";
+                    }
+                }
+
+                return print(childPath);
+            }, "children")
+        ).indentTail(options.tabWidth);
+
+        var closingLines = path.call(print, "closingElement");
+
+        return concat([
+            openingLines,
+            childLines,
+            closingLines
+        ]);
+
+    case "XJSOpeningElement":
+    case "JSXOpeningElement":
+        var parts = ["<", path.call(print, "name")];
+        var attrParts = [];
+
+        path.each(function(attrPath) {
+            attrParts.push(" ", print(attrPath));
+        }, "attributes");
+
+        var attrLines = concat(attrParts);
+
+        var needLineWrap = (
+            attrLines.length > 1 ||
+            attrLines.getLineLength(1) > options.wrapColumn
+        );
+
+        if (needLineWrap) {
+            attrParts.forEach(function(part, i) {
+                if (part === " ") {
+                    assert.strictEqual(i % 2, 0);
+                    attrParts[i] = "\n";
+                }
+            });
+
+            attrLines = concat(attrParts).indentTail(options.tabWidth);
+        }
+
+        parts.push(attrLines, n.selfClosing ? " />" : ">");
+
+        return concat(parts);
+
+    case "XJSClosingElement":
+    case "JSXClosingElement":
+        return concat(["</", path.call(print, "name"), ">"]);
+
+    case "XJSText":
+    case "JSXText":
+        return fromString(n.value, options);
+
+    case "XJSEmptyExpression":
+    case "JSXEmptyExpression":
+        return fromString("");
+
+    case "TypeAnnotatedIdentifier":
+        return concat([
+            path.call(print, "annotation"),
+            " ",
+            path.call(print, "identifier")
+        ]);
+
+    case "ClassBody":
+        if (n.body.length === 0) {
+            return fromString("{}");
+        }
+
+        return concat([
+            "{\n",
+            path.call(function(bodyPath) {
+                return printStatementSequence(bodyPath, options, print);
+            }, "body").indent(options.tabWidth),
+            "\n}"
+        ]);
+
+    case "ClassPropertyDefinition":
+        var parts = ["static ", path.call(print, "definition")];
+        if (!namedTypes.MethodDefinition.check(n.definition))
+            parts.push(";");
+        return concat(parts);
+
+    case "ClassProperty":
+        var parts = [];
+        if (n.static)
+            parts.push("static ");
+
+        parts.push(path.call(print, "key"));
+        if (n.typeAnnotation)
+            parts.push(path.call(print, "typeAnnotation"));
+
+        if (n.value)
+            parts.push(" = ", path.call(print, "value"));
+
+        parts.push(";");
+        return concat(parts);
+
+    case "ClassDeclaration":
+    case "ClassExpression":
+        var parts = ["class"];
+
+        if (n.id) {
+            parts.push(
+                " ",
+                path.call(print, "id"),
+                path.call(print, "typeParameters")
+            );
+        }
+
+        if (n.superClass) {
+            parts.push(
+                " extends ",
+                path.call(print, "superClass"),
+                path.call(print, "superTypeParameters")
+            );
+        }
+
+        if (n["implements"]) {
+            parts.push(
+                " implements ",
+                fromString(", ").join(path.map(print, "implements"))
+            );
+        }
+
+        parts.push(" ", path.call(print, "body"));
+
+        return concat(parts);
+
+    case "TemplateElement":
+        return fromString(n.value.raw, options);
+
+    case "TemplateLiteral":
+        var expressions = path.map(print, "expressions");
+        var parts = ["`"];
+
+        path.each(function(childPath) {
+            var i = childPath.getName();
+            parts.push(print(childPath));
+            if (i < expressions.length) {
+                parts.push("${", expressions[i], "}");
+            }
+        }, "quasis");
+
+        parts.push("`");
+
+        return concat(parts);
+
+    case "TaggedTemplateExpression":
+        return concat([
+            path.call(print, "tag"),
+            path.call(print, "quasi")
+        ]);
+
+    // These types are unprintable because they serve as abstract
+    // supertypes for other (printable) types.
+    case "Node":
+    case "Printable":
+    case "SourceLocation":
+    case "Position":
+    case "Statement":
+    case "Function":
+    case "Pattern":
+    case "Expression":
+    case "Declaration":
+    case "Specifier":
+    case "NamedSpecifier":
+    case "Comment": // Supertype of Block and Line.
+    case "MemberTypeAnnotation": // Flow
+    case "TupleTypeAnnotation": // Flow
+    case "Type": // Flow
+        throw new Error("unprintable type: " + JSON.stringify(n.type));
+
+    case "CommentBlock": // Babel block comment.
+    case "Block": // Esprima block comment.
+        return concat(["/*", fromString(n.value, options), "*/"]);
+
+    case "CommentLine": // Babel line comment.
+    case "Line": // Esprima line comment.
+        return concat(["//", fromString(n.value, options)]);
+
+    // Type Annotations for Facebook Flow, typically stripped out or
+    // transformed away before printing.
+    case "TypeAnnotation":
+        var parts = [];
+
+        if (n.typeAnnotation) {
+            if (n.typeAnnotation.type !== "FunctionTypeAnnotation") {
+                parts.push(": ");
+            }
+            parts.push(path.call(print, "typeAnnotation"));
+            return concat(parts);
+        }
+
+        return fromString("");
+
+    case "AnyTypeAnnotation":
+        return fromString("any", options);
+
+    case "MixedTypeAnnotation":
+        return fromString("mixed", options);
+
+    case "ArrayTypeAnnotation":
+        return concat([
+            path.call(print, "elementType"),
+            "[]"
+        ]);
+
+    case "BooleanTypeAnnotation":
+        return fromString("boolean", options);
+
+    case "BooleanLiteralTypeAnnotation":
+        assert.strictEqual(typeof n.value, "boolean");
+        return fromString("" + n.value, options);
+
+    case "DeclareClass":
+        return concat([
+            fromString("declare class ", options),
+            path.call(print, "id"),
+            " ",
+            path.call(print, "body"),
+        ]);
+
+    case "DeclareFunction":
+        return concat([
+            fromString("declare function ", options),
+            path.call(print, "id"),
+            ";"
+        ]);
+
+    case "DeclareModule":
+        return concat([
+            fromString("declare module ", options),
+            path.call(print, "id"),
+            " ",
+            path.call(print, "body"),
+        ]);
+
+    case "DeclareVariable":
+        return concat([
+            fromString("declare var ", options),
+            path.call(print, "id"),
+            ";"
+        ]);
+
+    case "FunctionTypeAnnotation":
+        // FunctionTypeAnnotation is ambiguous:
+        // declare function(a: B): void; OR
+        // var A: (a: B) => void;
+        var parts = [];
+        var parent = path.getParentNode(0);
+        var isArrowFunctionTypeAnnotation = !(
+            namedTypes.ObjectTypeCallProperty.check(parent) ||
+            namedTypes.DeclareFunction.check(path.getParentNode(2))
+        );
+
+        var needsColon =
+            isArrowFunctionTypeAnnotation &&
+            !namedTypes.FunctionTypeParam.check(parent);
+
+        if (needsColon) {
+            parts.push(": ");
+        }
+
+        parts.push(
+            "(",
+            fromString(", ").join(path.map(print, "params")),
+            ")"
+        );
+
+        // The returnType is not wrapped in a TypeAnnotation, so the colon
+        // needs to be added separately.
+        if (n.returnType) {
+            parts.push(
+                isArrowFunctionTypeAnnotation ? " => " : ": ",
+                path.call(print, "returnType")
+            );
+        }
+
+        return concat(parts);
+
+    case "FunctionTypeParam":
+        return concat([
+            path.call(print, "name"),
+            ": ",
+            path.call(print, "typeAnnotation"),
+        ]);
+
+    case "GenericTypeAnnotation":
+        return concat([
+            path.call(print, "id"),
+            path.call(print, "typeParameters")
+        ]);
+
+    case "InterfaceDeclaration":
+        var parts = [
+            fromString("interface ", options),
+            path.call(print, "id"),
+            path.call(print, "typeParameters"),
+            " "
+        ];
+
+        if (n["extends"]) {
+            parts.push(
+                "extends ",
+                fromString(", ").join(path.map(print, "extends"))
+            );
+        }
+
+        parts.push(" ", path.call(print, "body"));
+
+        return concat(parts);
+
+    case "ClassImplements":
+    case "InterfaceExtends":
+        return concat([
+            path.call(print, "id"),
+            path.call(print, "typeParameters")
+        ]);
+
+    case "IntersectionTypeAnnotation":
+        return fromString(" & ").join(path.map(print, "types"));
+
+    case "NullableTypeAnnotation":
+        return concat([
+            "?",
+            path.call(print, "typeAnnotation")
+        ]);
+
+    case "NumberTypeAnnotation":
+        return fromString("number", options);
+
+    case "ObjectTypeCallProperty":
+        return path.call(print, "value");
+
+    case "ObjectTypeIndexer":
+        return concat([
+            "[",
+            path.call(print, "id"),
+            ": ",
+            path.call(print, "key"),
+            "]: ",
+            path.call(print, "value")
+        ]);
+
+    case "ObjectTypeProperty":
+        return concat([
+            path.call(print, "key"),
+            ": ",
+            path.call(print, "value")
+        ]);
+
+    case "QualifiedTypeIdentifier":
+        return concat([
+            path.call(print, "qualification"),
+            ".",
+            path.call(print, "id")
+        ]);
+
+    case "StringLiteralTypeAnnotation":
+        return fromString(nodeStr(n.value, options), options);
+
+    case "NumberLiteralTypeAnnotation":
+        assert.strictEqual(typeof n.value, "number");
+        return fromString("" + n.value, options);
+
+    case "StringTypeAnnotation":
+        return fromString("string", options);
+
+    case "TypeAlias":
+        return concat([
+            "type ",
+            path.call(print, "id"),
+            " = ",
+            path.call(print, "right"),
+            ";"
+        ]);
+
+    case "TypeCastExpression":
+        return concat([
+            "(",
+            path.call(print, "expression"),
+            path.call(print, "typeAnnotation"),
+            ")"
+        ]);
+
+    case "TypeParameterDeclaration":
+    case "TypeParameterInstantiation":
+        return concat([
+            "<",
+            fromString(", ").join(path.map(print, "params")),
+            ">"
+        ]);
+
+    case "TypeofTypeAnnotation":
+        return concat([
+            fromString("typeof ", options),
+            path.call(print, "argument")
+        ]);
+
+    case "UnionTypeAnnotation":
+        return fromString(" | ").join(path.map(print, "types"));
+
+    case "VoidTypeAnnotation":
+        return fromString("void", options);
+
+    // Unhandled types below. If encountered, nodes of these types should
+    // be either left alone or desugared into AST types that are fully
+    // supported by the pretty-printer.
+    case "ClassHeritage": // TODO
+    case "ComprehensionBlock": // TODO
+    case "ComprehensionExpression": // TODO
+    case "Glob": // TODO
+    case "GeneratorExpression": // TODO
+    case "LetStatement": // TODO
+    case "LetExpression": // TODO
+    case "GraphExpression": // TODO
+    case "GraphIndexExpression": // TODO
+
+    // XML types that nobody cares about or needs to print.
+    case "XMLDefaultDeclaration":
+    case "XMLAnyName":
+    case "XMLQualifiedIdentifier":
+    case "XMLFunctionQualifiedIdentifier":
+    case "XMLAttributeSelector":
+    case "XMLFilterExpression":
+    case "XML":
+    case "XMLElement":
+    case "XMLList":
+    case "XMLEscape":
+    case "XMLText":
+    case "XMLStartTag":
+    case "XMLEndTag":
+    case "XMLPointTag":
+    case "XMLName":
+    case "XMLAttribute":
+    case "XMLCdata":
+    case "XMLComment":
+    case "XMLProcessingInstruction":
+    default:
+        debugger;
+        throw new Error("unknown type: " + JSON.stringify(n.type));
+    }
+
+    return p;
+}
+
+function printStatementSequence(path, options, print) {
+    var inClassBody =
+        namedTypes.ClassBody &&
+        namedTypes.ClassBody.check(path.getParentNode());
+
+    var filtered = [];
+    var sawComment = false;
+    var sawStatement = false;
+
+    path.each(function(stmtPath) {
+        var i = stmtPath.getName();
+        var stmt = stmtPath.getValue();
+
+        // Just in case the AST has been modified to contain falsy
+        // "statements," it's safer simply to skip them.
+        if (!stmt) {
+            return;
+        }
+
+        // Skip printing EmptyStatement nodes to avoid leaving stray
+        // semicolons lying around.
+        if (stmt.type === "EmptyStatement") {
+            return;
+        }
+
+        if (namedTypes.Comment.check(stmt)) {
+            // The pretty printer allows a dangling Comment node to act as
+            // a Statement when the Comment can't be attached to any other
+            // non-Comment node in the tree.
+            sawComment = true;
+        } else if (!inClassBody) {
+            namedTypes.Statement.assert(stmt);
+            sawStatement = true;
+        }
+
+        // We can't hang onto stmtPath outside of this function, because
+        // it's just a reference to a mutable FastPath object, so we have
+        // to go ahead and print it here.
+        filtered.push({
+            node: stmt,
+            printed: print(stmtPath)
+        });
+    });
+
+    if (sawComment) {
+        assert.strictEqual(
+            sawStatement, false,
+            "Comments may appear as statements in otherwise empty statement " +
+                "lists, but may not coexist with non-Comment nodes."
+        );
+    }
+
+    var prevTrailingSpace = null;
+    var len = filtered.length;
+    var parts = [];
+
+    filtered.forEach(function(info, i) {
+        var printed = info.printed;
+        var stmt = info.node;
+        var multiLine = printed.length > 1;
+        var notFirst = i > 0;
+        var notLast = i < len - 1;
+        var leadingSpace;
+        var trailingSpace;
+        var lines = stmt && stmt.loc && stmt.loc.lines;
+        var trueLoc = lines && options.reuseWhitespace &&
+            util.getTrueLoc(stmt, lines);
+
+        if (notFirst) {
+            if (trueLoc) {
+                var beforeStart = lines.skipSpaces(trueLoc.start, true);
+                var beforeStartLine = beforeStart ? beforeStart.line : 1;
+                var leadingGap = trueLoc.start.line - beforeStartLine;
+                leadingSpace = Array(leadingGap + 1).join("\n");
+            } else {
+                leadingSpace = multiLine ? "\n\n" : "\n";
+            }
+        } else {
+            leadingSpace = "";
+        }
+
+        if (notLast) {
+            if (trueLoc) {
+                var afterEnd = lines.skipSpaces(trueLoc.end);
+                var afterEndLine = afterEnd ? afterEnd.line : lines.length;
+                var trailingGap = afterEndLine - trueLoc.end.line;
+                trailingSpace = Array(trailingGap + 1).join("\n");
+            } else {
+                trailingSpace = multiLine ? "\n\n" : "\n";
+            }
+        } else {
+            trailingSpace = "";
+        }
+
+        parts.push(
+            maxSpace(prevTrailingSpace, leadingSpace),
+            printed
+        );
+
+        if (notLast) {
+            prevTrailingSpace = trailingSpace;
+        } else if (trailingSpace) {
+            parts.push(trailingSpace);
+        }
+    });
+
+    return concat(parts);
+}
+
+function maxSpace(s1, s2) {
+    if (!s1 && !s2) {
+        return fromString("");
+    }
+
+    if (!s1) {
+        return fromString(s2);
+    }
+
+    if (!s2) {
+        return fromString(s1);
+    }
+
+    var spaceLines1 = fromString(s1);
+    var spaceLines2 = fromString(s2);
+
+    if (spaceLines2.length > spaceLines1.length) {
+        return spaceLines2;
+    }
+
+    return spaceLines1;
+}
+
+function printMethod(path, options, print) {
+    var node = path.getNode();
+    var kind = node.kind;
+    var parts = [];
+
+    namedTypes.FunctionExpression.assert(node.value);
+
+    if (node.decorators) {
+        path.each(function(decoratorPath) {
+            parts.push(print(decoratorPath), "\n");
+        }, "decorators");
+    }
+
+    if (node.value.async) {
+        parts.push("async ");
+    }
+
+    if (!kind || kind === "init" || kind === "method" || kind === "constructor") {
+        if (node.value.generator) {
+            parts.push("*");
+        }
+    } else {
+        assert.ok(kind === "get" || kind === "set");
+        parts.push(kind, " ");
+    }
+
+    var key = path.call(print, "key");
+    if (node.computed) {
+        key = concat(["[", key, "]"]);
+    }
+
+    parts.push(
+        key,
+        path.call(print, "value", "typeParameters"),
+        "(",
+        path.call(function(valuePath) {
+            return printFunctionParams(valuePath, options, print);
+        }, "value"),
+        ")",
+        path.call(print, "value", "returnType"),
+        " ",
+        path.call(print, "value", "body")
+    );
+
+    return concat(parts);
+}
+
+function printArgumentsList(path, options, print) {
+    var printed = path.map(print, "arguments");
+
+    var joined = fromString(", ").join(printed);
+    if (joined.getLineLength(1) > options.wrapColumn) {
+        joined = fromString(",\n").join(printed);
+        return concat([
+            "(\n",
+            joined.indent(options.tabWidth),
+            options.trailingComma ? ",\n)" : "\n)"
+        ]);
+    }
+
+    return concat(["(", joined, ")"]);
+}
+
+function printFunctionParams(path, options, print) {
+    var fun = path.getValue();
+    namedTypes.Function.assert(fun);
+
+    var printed = path.map(print, "params");
+
+    if (fun.defaults) {
+        path.each(function(defExprPath) {
+            var i = defExprPath.getName();
+            var p = printed[i];
+            if (p && defExprPath.getValue()) {
+                printed[i] = concat([p, "=", print(defExprPath)]);
+            }
+        }, "defaults");
+    }
+
+    if (fun.rest) {
+        printed.push(concat(["...", path.call(print, "rest")]));
+    }
+
+    var joined = fromString(", ").join(printed);
+    if (joined.length > 1 ||
+        joined.getLineLength(1) > options.wrapColumn) {
+        joined = fromString(",\n").join(printed);
+        if (options.trailingComma && !fun.rest) {
+            joined = concat([joined, ",\n"]);
+        }
+        return concat(["\n", joined.indent(options.tabWidth)]);
+    }
+
+    return joined;
+}
+
+function adjustClause(clause, options) {
+    if (clause.length > 1)
+        return concat([" ", clause]);
+
+    return concat([
+        "\n",
+        maybeAddSemicolon(clause).indent(options.tabWidth)
+    ]);
+}
+
+function lastNonSpaceCharacter(lines) {
+    var pos = lines.lastPos();
+    do {
+        var ch = lines.charAt(pos);
+        if (/\S/.test(ch))
+            return ch;
+    } while (lines.prevPos(pos));
+}
+
+function endsWithBrace(lines) {
+    return lastNonSpaceCharacter(lines) === "}";
+}
+
+function swapQuotes(str) {
+    return str.replace(/['"]/g, function(m) {
+        return m === '"' ? '\'' : '"';
+    });
+}
+
+function nodeStr(str, options) {
+    isString.assert(str);
+    switch (options.quote) {
+    case "auto":
+        var double = JSON.stringify(str);
+        var single = swapQuotes(JSON.stringify(swapQuotes(str)));
+        return double.length > single.length ? single : double;
+    case "single":
+        return swapQuotes(JSON.stringify(swapQuotes(str)));
+    case "double":
+    default:
+        return JSON.stringify(str);
+    }
+}
+
+function maybeAddSemicolon(lines) {
+    var eoc = lastNonSpaceCharacter(lines);
+    if (!eoc || "\n};".indexOf(eoc) < 0)
+        return concat([lines, ";"]);
+    return lines;
+}
diff --git a/lib/types.js b/lib/types.js
new file mode 100644
index 0000000..4446f07
--- /dev/null
+++ b/lib/types.js
@@ -0,0 +1,5 @@
+// This module was originally created so that Recast could add its own
+// custom types to the AST type system (in particular, the File type), but
+// those types are now incorporated into ast-types, so this module doesn't
+// have much to do anymore. Still, it might prove useful in the future.
+module.exports = require("ast-types");
diff --git a/lib/util.js b/lib/util.js
new file mode 100644
index 0000000..6af3980
--- /dev/null
+++ b/lib/util.js
@@ -0,0 +1,150 @@
+var assert = require("assert");
+var types = require("./types");
+var getFieldValue = types.getFieldValue;
+var n = types.namedTypes;
+var sourceMap = require("source-map");
+var SourceMapConsumer = sourceMap.SourceMapConsumer;
+var SourceMapGenerator = sourceMap.SourceMapGenerator;
+var hasOwn = Object.prototype.hasOwnProperty;
+
+function getUnionOfKeys() {
+    var result = {};
+    var argc = arguments.length;
+    for (var i = 0; i < argc; ++i) {
+        var keys = Object.keys(arguments[i]);
+        var keyCount = keys.length;
+        for (var j = 0; j < keyCount; ++j) {
+            result[keys[j]] = true;
+        }
+    }
+    return result;
+}
+exports.getUnionOfKeys = getUnionOfKeys;
+
+function comparePos(pos1, pos2) {
+    return (pos1.line - pos2.line) || (pos1.column - pos2.column);
+}
+exports.comparePos = comparePos;
+
+function copyPos(pos) {
+    return {
+        line: pos.line,
+        column: pos.column
+    };
+}
+exports.copyPos = copyPos;
+
+exports.composeSourceMaps = function(formerMap, latterMap) {
+    if (formerMap) {
+        if (!latterMap) {
+            return formerMap;
+        }
+    } else {
+        return latterMap || null;
+    }
+
+    var smcFormer = new SourceMapConsumer(formerMap);
+    var smcLatter = new SourceMapConsumer(latterMap);
+    var smg = new SourceMapGenerator({
+        file: latterMap.file,
+        sourceRoot: latterMap.sourceRoot
+    });
+
+    var sourcesToContents = {};
+
+    smcLatter.eachMapping(function(mapping) {
+        var origPos = smcFormer.originalPositionFor({
+            line: mapping.originalLine,
+            column: mapping.originalColumn
+        });
+
+        var sourceName = origPos.source;
+        if (sourceName === null) {
+            return;
+        }
+
+        smg.addMapping({
+            source: sourceName,
+            original: copyPos(origPos),
+            generated: {
+                line: mapping.generatedLine,
+                column: mapping.generatedColumn
+            },
+            name: mapping.name
+        });
+
+        var sourceContent = smcFormer.sourceContentFor(sourceName);
+        if (sourceContent && !hasOwn.call(sourcesToContents, sourceName)) {
+            sourcesToContents[sourceName] = sourceContent;
+            smg.setSourceContent(sourceName, sourceContent);
+        }
+    });
+
+    return smg.toJSON();
+};
+
+exports.getTrueLoc = function(node, lines) {
+    // It's possible that node is newly-created (not parsed by Esprima),
+    // in which case it probably won't have a .loc property (or an
+    // .original property for that matter). That's fine; we'll just
+    // pretty-print it as usual.
+    if (!node.loc) {
+        return null;
+    }
+
+    var start = node.loc.start;
+    var end = node.loc.end;
+
+    // If the node has any comments, their locations might contribute to
+    // the true start/end positions of the node.
+    if (node.comments) {
+        node.comments.forEach(function(comment) {
+            if (comment.loc) {
+                if (comparePos(comment.loc.start, start) < 0) {
+                    start = comment.loc.start;
+                }
+
+                if (comparePos(end, comment.loc.end) < 0) {
+                    end = comment.loc.end;
+                }
+            }
+        });
+    }
+
+    return {
+        // Finally, trim any leading or trailing whitespace from the true
+        // location of the node.
+        start: lines.skipSpaces(start, false, false),
+        end: lines.skipSpaces(end, true, false)
+    };
+};
+
+exports.fixFaultyLocations = function(node) {
+    if ((n.MethodDefinition && n.MethodDefinition.check(node)) ||
+        (n.Property.check(node) && (node.method || node.shorthand))) {
+        // If the node is a MethodDefinition or a .method or .shorthand
+        // Property, then the location information stored in
+        // node.value.loc is very likely untrustworthy (just the {body}
+        // part of a method, or nothing in the case of shorthand
+        // properties), so we null out that information to prevent
+        // accidental reuse of bogus source code during reprinting.
+        node.value.loc = null;
+
+        if (n.FunctionExpression.check(node.value)) {
+            // FunctionExpression method values should be anonymous,
+            // because their .id fields are ignored anyway.
+            node.value.id = null;
+        }
+    }
+
+    var loc = node.loc;
+    if (loc) {
+        if (loc.start.line < 1) {
+            loc.start.line = 1;
+        }
+
+        if (loc.end.line < 1) {
+            loc.end.line = 1;
+        }
+    }
+};
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..60d5ea6
--- /dev/null
+++ b/main.js
@@ -0,0 +1,99 @@
+var types = require("./lib/types");
+var parse = require("./lib/parser").parse;
+var Printer = require("./lib/printer").Printer;
+
+function print(node, options) {
+    return new Printer(options).print(node);
+}
+
+function prettyPrint(node, options) {
+    return new Printer(options).printGenerically(node);
+}
+
+function run(transformer, options) {
+    return runFile(process.argv[2], transformer, options);
+}
+
+function runFile(path, transformer, options) {
+    require("fs").readFile(path, "utf-8", function(err, code) {
+        if (err) {
+            console.error(err);
+            return;
+        }
+
+        runString(code, transformer, options);
+    });
+}
+
+function defaultWriteback(output) {
+    process.stdout.write(output);
+}
+
+function runString(code, transformer, options) {
+    var writeback = options && options.writeback || defaultWriteback;
+    transformer(parse(code, options), function(node) {
+        writeback(print(node, options).code);
+    });
+}
+
+Object.defineProperties(exports, {
+    /**
+     * Parse a string of code into an augmented syntax tree suitable for
+     * arbitrary modification and reprinting.
+     */
+    parse: {
+        enumerable: true,
+        value: parse
+    },
+
+    /**
+     * Traverse and potentially modify an abstract syntax tree using a
+     * convenient visitor syntax:
+     *
+     *   recast.visit(ast, {
+     *     names: [],
+     *     visitIdentifier: function(path) {
+     *       var node = path.value;
+     *       this.visitor.names.push(node.name);
+     *       this.traverse(path);
+     *     }
+     *   });
+     */
+    visit: {
+        enumerable: true,
+        value: types.visit
+    },
+
+    /**
+     * Reprint a modified syntax tree using as much of the original source
+     * code as possible.
+     */
+    print: {
+        enumerable: true,
+        value: print
+    },
+
+    /**
+     * Print without attempting to reuse any original source code.
+     */
+    prettyPrint: {
+        enumerable: false,
+        value: prettyPrint
+    },
+
+    /**
+     * Customized version of require("ast-types").
+     */
+    types: {
+        enumerable: false,
+        value: types
+    },
+
+    /**
+     * Convenient command-line interface (see e.g. example/add-braces).
+     */
+    run: {
+        enumerable: false,
+        value: run
+    }
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7b37e88
--- /dev/null
+++ b/package.json
@@ -0,0 +1,39 @@
+{
+  "author": "Ben Newman <bn at cs.stanford.edu>",
+  "name": "recast",
+  "description": "JavaScript syntax tree transformer, nondestructive pretty-printer, and automatic source map generator",
+  "keywords": [
+    "ast",
+    "rewriting",
+    "refactoring",
+    "codegen",
+    "syntax",
+    "transformation",
+    "parsing",
+    "pretty-printing"
+  ],
+  "version": "0.10.32",
+  "homepage": "http://github.com/benjamn/recast",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/benjamn/recast.git"
+  },
+  "license": "MIT",
+  "main": "main.js",
+  "scripts": {
+    "test": "node ./node_modules/mocha/bin/mocha --reporter spec --full-trace",
+    "debug": "node ./node_modules/mocha/bin/mocha --debug-brk --reporter spec"
+  },
+  "dependencies": {
+    "esprima-fb": "~15001.1001.0-dev-harmony-fb",
+    "source-map": "~0.4.4",
+    "private": "~0.1.5",
+    "ast-types": "0.8.11"
+  },
+  "devDependencies": {
+    "mocha": "~2.2.5"
+  },
+  "engines": {
+    "node": ">= 0.8"
+  }
+}
diff --git a/test/comments.js b/test/comments.js
new file mode 100644
index 0000000..e7e2272
--- /dev/null
+++ b/test/comments.js
@@ -0,0 +1,626 @@
+var recast = require("../main");
+var n = recast.types.namedTypes;
+var b = recast.types.builders;
+var Printer = require("../lib/printer").Printer;
+var fromString = require("../lib/lines").fromString;
+var assert = require("assert");
+var printer = new Printer;
+
+var annotated = [
+    "function dup(/* string */ s,",
+    "             /* int */ n) /* string */",
+    "{",
+    "  // Use an array full of holes.",
+    "  return Array(n + /*",
+    "                    * off-by-*/ 1).join(s);",
+    "}"
+];
+
+describe("comments", function() {
+    it("attachment and reprinting", function() {
+        var code = annotated.join("\n");
+        var ast = recast.parse(code);
+
+        var dup = ast.program.body[0];
+        n.FunctionDeclaration.assert(dup);
+        assert.strictEqual(dup.id.name, "dup");
+
+        // More of a basic sanity test than a comment test.
+        assert.strictEqual(recast.print(ast).code, code);
+        assert.strictEqual(recast.print(ast.program).code, code);
+        assert.strictEqual(recast.print(dup).code, code);
+
+        assert.strictEqual(
+            recast.print(dup.params[0]).code,
+            "/* string */ s"
+        );
+
+        assert.strictEqual(
+            recast.print(dup.params[1]).code,
+            "/* int */ n"
+        );
+
+        assert.strictEqual(
+            recast.print(dup.body).code,
+            ["/* string */"].concat(annotated.slice(2)).join("\n")
+        );
+
+        var retStmt = dup.body.body[0];
+        n.ReturnStatement.assert(retStmt);
+
+        var indented = annotated.slice(3, 6).join("\n");
+        var flush = fromString(indented).indent(-2);
+
+        assert.strictEqual(
+            recast.print(retStmt).code,
+            flush.toString()
+        );
+
+        var join = retStmt.argument;
+        n.CallExpression.assert(join);
+
+        var one = join.callee.object.arguments[0].right;
+        n.Literal.assert(one);
+        assert.strictEqual(one.value, 1);
+        assert.strictEqual(recast.print(one).code, [
+            "/*",
+            " * off-by-*/ 1"
+        ].join("\n"));
+    });
+
+    var trailing = [
+        "Foo.prototype = {",
+        "// Copyright (c) 2013 Ben Newman <bn at cs.stanford.edu>",
+        "",
+        "  /**",
+        "   * Leading comment.",
+        "   */",
+        "  constructor: Foo, // Important for instanceof",
+        "                    // to work in all browsers.",
+        '  bar: "baz", // Just in case we need it.',
+        "  qux: { // Here is an object literal.",
+        "    zxcv: 42",
+        "    // Put more properties here when you think of them.",
+        "  } // There was an object literal...",
+        "    // ... and here I am continuing this comment.",
+        "",
+        "};"
+    ];
+
+    var trailingExpected = [
+        "Foo.prototype = {",
+        "  // Copyright (c) 2013 Ben Newman <bn at cs.stanford.edu>",
+        "",
+        "  /**",
+        "   * Leading comment.",
+        "   */",
+        "  // Important for instanceof",
+        "  // to work in all browsers.",
+        "  constructor: Foo,",
+        "",
+        "  // Just in case we need it.",
+        '  bar: "baz",',
+        "",
+        "  // There was an object literal...",
+        "  // ... and here I am continuing this comment.",
+        "  qux: {",
+        "    // Here is an object literal.",
+        "    // Put more properties here when you think of them.",
+        "    zxcv: 42,",
+        "",
+        "    asdf: 43",
+        "  },",
+        "",
+        '  extra: "property"',
+        "};"
+    ];
+
+    it("TrailingComments", function() {
+        var code = trailing.join("\n");
+        var ast = recast.parse(code);
+        assert.strictEqual(recast.print(ast).code, code);
+
+        // Drop all original source information to force reprinting.
+        recast.visit(ast, {
+            visitNode: function(path) {
+                this.traverse(path);
+                path.value.original = null;
+            }
+        });
+
+        var assign = ast.program.body[0].expression;
+        n.AssignmentExpression.assert(assign);
+
+        var props = assign.right.properties;
+        n.Property.arrayOf().assert(props);
+
+        props.push(b.property(
+            "init",
+            b.identifier("extra"),
+            b.literal("property")
+        ));
+
+        var quxVal = props[2].value;
+        n.ObjectExpression.assert(quxVal);
+        quxVal.properties.push(b.property(
+            "init",
+            b.identifier("asdf"),
+            b.literal(43)
+        ));
+
+        var actual = recast.print(ast, { tabWidth: 2 }).code;
+        var expected = trailingExpected.join("\n");
+
+        // Check semantic equivalence:
+        recast.types.astNodesAreEquivalent.assert(
+            ast,
+            recast.parse(actual)
+        );
+
+        assert.strictEqual(actual, expected);
+    });
+
+    var bodyTrailing = [
+        "module.exports = {};",
+        "/**",
+        " * Trailing comment.",
+        " */"
+    ];
+
+    var bodyTrailingExpected = [
+        "module.exports = {};",
+        "/**",
+        " * Trailing comment.",
+        " */"
+    ];
+
+    it("BodyTrailingComments", function() {
+        var code = bodyTrailing.join("\n");
+        var ast = recast.parse(code);
+
+        // Drop all original source information to force reprinting.
+        recast.visit(ast, {
+            visitNode: function(path) {
+                this.traverse(path);
+                path.value.original = null;
+            }
+        });
+
+        var actual = recast.print(ast, { tabWidth: 2 }).code;
+        var expected = bodyTrailingExpected.join("\n");
+
+        assert.strictEqual(actual, expected);
+    });
+
+    var paramTrailing = [
+        "function foo(bar, baz /* = null */) {",
+        "  assert.strictEqual(baz, null);",
+        "}"
+    ];
+
+    var paramTrailingExpected = [
+        "function foo(zxcv, bar, baz /* = null */) {",
+        "  assert.strictEqual(baz, null);",
+        "}"
+    ];
+
+    it("ParamTrailingComments", function() {
+        var code = paramTrailing.join("\n");
+        var ast = recast.parse(code);
+
+        var func = ast.program.body[0];
+        n.FunctionDeclaration.assert(func);
+
+        func.params.unshift(b.identifier("zxcv"));
+
+        var actual = recast.print(ast, { tabWidth: 2 }).code;
+        var expected = paramTrailingExpected.join("\n");
+
+        assert.strictEqual(actual, expected);
+    });
+
+    var protoAssign = [
+        "A.prototype.foo = function() {",
+        "  return this.bar();",
+        "}", // Lack of semicolon screws up location info.
+        "",
+        "// Comment about the bar method.",
+        "A.prototype.bar = function() {",
+        "  return this.foo();",
+        "}"
+    ];
+
+    it("ProtoAssignComment", function() {
+        var code = protoAssign.join("\n");
+        var ast = recast.parse(code);
+
+        var foo = ast.program.body[0];
+        var bar = ast.program.body[1];
+
+        n.ExpressionStatement.assert(foo);
+        n.ExpressionStatement.assert(bar);
+
+        assert.strictEqual(foo.expression.left.property.name, "foo");
+        assert.strictEqual(bar.expression.left.property.name, "bar");
+
+        assert.ok(!foo.comments);
+        assert.ok(bar.comments);
+        assert.strictEqual(bar.comments.length, 1);
+
+        var barComment = bar.comments[0];
+        assert.strictEqual(barComment.leading, true);
+        assert.strictEqual(barComment.trailing, false);
+
+        assert.strictEqual(
+            barComment.value,
+            " Comment about the bar method."
+        );
+    });
+
+    var conciseMethods = [
+        "var obj = {",
+        "  a(/*before*/ param) {},",
+        "  b(param /*after*/) {},",
+        "  c(param) /*body*/ {}",
+        "};",
+    ];
+
+    it("should correctly attach to concise methods", function() {
+        var code = conciseMethods.join("\n");
+        var ast = recast.parse(code);
+
+        var objExpr = ast.program.body[0].declarations[0].init;
+        n.ObjectExpression.assert(objExpr);
+
+        var a = objExpr.properties[0];
+        n.Identifier.assert(a.key);
+        assert.strictEqual(a.key.name, "a");
+
+        var aComments = a.value.params[0].comments;
+        assert.strictEqual(aComments.length, 1);
+
+        var aComment = aComments[0];
+        assert.strictEqual(aComment.leading, true);
+        assert.strictEqual(aComment.trailing, false);
+        assert.strictEqual(aComment.type, "Block");
+        assert.strictEqual(aComment.value, "before");
+
+        assert.strictEqual(
+            recast.print(a).code,
+            "a(/*before*/ param) {}"
+        );
+
+        var b = objExpr.properties[1];
+        n.Identifier.assert(b.key);
+        assert.strictEqual(b.key.name, "b");
+
+        var bComments = b.value.params[0].comments;
+        assert.strictEqual(bComments.length, 1);
+
+        var bComment = bComments[0];
+        assert.strictEqual(bComment.leading, false);
+        assert.strictEqual(bComment.trailing, true);
+        assert.strictEqual(bComment.type, "Block");
+        assert.strictEqual(bComment.value, "after");
+
+        assert.strictEqual(
+            recast.print(b).code,
+            "b(param /*after*/) {}"
+        );
+
+        var c = objExpr.properties[2];
+        n.Identifier.assert(c.key);
+        assert.strictEqual(c.key.name, "c");
+
+        var cComments = c.value.body.comments;
+        assert.strictEqual(cComments.length, 1);
+
+        var cComment = cComments[0];
+        assert.strictEqual(cComment.leading, true);
+        assert.strictEqual(cComment.trailing, false);
+        assert.strictEqual(cComment.type, "Block");
+        assert.strictEqual(cComment.value, "body");
+
+        assert.strictEqual(
+            recast.print(c).code,
+            "c(param) /*body*/ {}"
+        );
+    });
+
+    it("should attach comments as configurable", function() {
+        // Given
+        var simpleCommentedCode = [
+            "// A comment",
+            "var obj = {",
+            "};",
+        ];
+        var code = simpleCommentedCode.join("\n");
+        var ast = recast.parse(code);
+
+        // When
+        Object.defineProperty(ast.program, 'comments', {
+            value: undefined,
+            enumerable: false
+        });
+
+        // Then
+        // An exception will be thrown if `comments` aren't configurable.
+    });
+
+    it("should be reprinted when modified", function() {
+        var code = [
+            "foo;",
+            "// bar",
+            "bar;"
+        ].join("\n");
+
+        var ast = recast.parse(code);
+
+        var comments = ast.program.body[1].comments;
+        assert.strictEqual(comments.length, 1);
+        var comment = comments[0];
+        assert.strictEqual(comment.type, "Line");
+        assert.strictEqual(comment.value, " bar");
+
+        comment.value = " barbara";
+        assert.strictEqual(recast.print(ast).code, [
+            "foo;",
+            "// barbara",
+            "bar;"
+        ].join("\n"));
+
+        ast.program.body[0].comments = comments;
+        delete ast.program.body[1].comments;
+        assert.strictEqual(recast.print(ast).code, [
+            "// barbara",
+            "foo;",
+            "bar;"
+        ].join("\n"));
+
+        ast.program.body[0] = b.blockStatement([
+            ast.program.body[0]
+        ]);
+        assert.strictEqual(recast.print(ast).code, [
+            "{",
+            "  // barbara",
+            "  foo;",
+            "}",
+            "",
+            "bar;"
+        ].join("\n"));
+
+        var comment = ast.program.body[0].body[0].comments[0];
+        comment.type = "Block";
+        assert.strictEqual(recast.print(ast).code, [
+            "{",
+            "  /* barbara*/",
+            "  foo;",
+            "}",
+            "",
+            "bar;"
+        ].join("\n"));
+
+        comment.value += "\n * babar\n ";
+        assert.strictEqual(recast.print(ast).code, [
+            "{",
+            "  /* barbara",
+            "   * babar",
+            "   */",
+            "  foo;",
+            "}",
+            "",
+            "bar;"
+        ].join("\n"));
+
+        ast.program.body[1].comments = [comment];
+        assert.strictEqual(recast.print(ast).code, [
+            "{",
+            "  /* barbara",
+            "   * babar",
+            "   */",
+            "  foo;",
+            "}",
+            "",
+            "/* barbara",
+            " * babar",
+            " */",
+            "bar;"
+        ].join("\n"));
+
+        delete ast.program.body[0].body[0].comments;
+        ast.program.comments = [b.line(" program comment")];
+        assert.strictEqual(recast.print(ast).code, [
+            "// program comment",
+            "{",
+            "  foo;",
+            "}",
+            "",
+            "/* barbara",
+            " * babar",
+            " */",
+            "bar;"
+        ].join("\n"));
+
+        ast.program.body.push(
+            ast.program.body.shift()
+        );
+        assert.strictEqual(recast.print(ast).code, [
+            "// program comment",
+            "/* barbara",
+            " * babar",
+            " */",
+            "bar;",
+            "",
+            "{",
+            "  foo;",
+            "}"
+        ].join("\n"));
+
+        recast.visit(ast, {
+            visitNode: function(path) {
+                delete path.value.comments;
+                this.traverse(path);
+            }
+        });
+        assert.strictEqual(recast.print(ast).code, [
+            "bar;",
+            "",
+            "{",
+            "  foo;",
+            "}"
+        ].join("\n"));
+
+        ast.program.body[1] = ast.program.body[1].body[0];
+        assert.strictEqual(recast.print(ast).code, [
+            "bar;",
+            "foo;"
+        ].join("\n"));
+    });
+
+    it("should preserve stray non-comment syntax", function() {
+        var code = [
+            "[",
+            "  foo",
+            "  , /* comma */",
+            "  /* hole */",
+            "  , /* comma */",
+            "  bar",
+            "]"
+        ].join("\n");
+
+        var ast = recast.parse(code);
+        assert.strictEqual(recast.print(ast).code, code);
+
+        var elems = ast.program.body[0].expression.elements;
+        elems[0].comments.push(b.line(" line comment", true, false));
+        assert.strictEqual(recast.print(ast).code, [
+            "[",
+            "  // line comment",
+            "  foo /* comma */",
+            "  /* hole */",
+            "  ,",
+            "  , /* comma */",
+            "  bar",
+            "]"
+        ].join("\n"));
+    });
+
+    it("should be reprinted even if dangling", function() {
+        var code = [
+            "[/*dangling*/] // array literal"
+        ].join("\n");
+
+        var ast = recast.parse(code);
+        var array = ast.program.body[0].expression;
+        var danglingComment = array.comments[0];
+        var trailingComment = array.comments[1];
+
+        assert.strictEqual(danglingComment.leading, false);
+        assert.strictEqual(danglingComment.trailing, false);
+
+        assert.strictEqual(trailingComment.leading, false);
+        assert.strictEqual(trailingComment.trailing, true);
+
+        danglingComment.value = " neither leading nor trailing ";
+        assert.strictEqual(recast.print(ast).code, [
+            "[/* neither leading nor trailing */] // array literal"
+        ].join("\n"));
+
+        trailingComment.value = " trailing";
+        assert.strictEqual(recast.print(ast).code, [
+            "[/* neither leading nor trailing */] // trailing"
+        ].join("\n"));
+
+        // Unfortuantely altering the elements of the array leads to
+        // reprinting which blows away the dangling comment.
+        array.elements.push(b.literal(1));
+        assert.strictEqual(
+            recast.print(ast).code,
+            "[1] // trailing"
+        );
+    });
+
+    it("should attach to program.body[0] instead of program", function() {
+        var code = [
+            "// comment 1",
+            "var a;",
+            "// comment 2",
+            "var b;",
+            "if (true) {",
+            "  // comment 3",
+            "  var c;",
+            "}"
+        ].join('\n');
+
+        var ast = recast.parse(code);
+
+        assert.ok(!ast.program.comments);
+
+        var aDecl = ast.program.body[0];
+        n.VariableDeclaration.assert(aDecl);
+        assert.strictEqual(aDecl.comments.length, 1);
+        assert.strictEqual(aDecl.comments[0].leading, true);
+        assert.strictEqual(aDecl.comments[0].trailing, false);
+        assert.strictEqual(aDecl.comments[0].value, " comment 1");
+
+        var bDecl = ast.program.body[1];
+        n.VariableDeclaration.assert(bDecl);
+        assert.strictEqual(bDecl.comments.length, 1);
+        assert.strictEqual(bDecl.comments[0].leading, true);
+        assert.strictEqual(bDecl.comments[0].trailing, false);
+        assert.strictEqual(bDecl.comments[0].value, " comment 2");
+
+        var cDecl = ast.program.body[2].consequent.body[0];
+        n.VariableDeclaration.assert(cDecl);
+        assert.strictEqual(cDecl.comments.length, 1);
+        assert.strictEqual(cDecl.comments[0].leading, true);
+        assert.strictEqual(cDecl.comments[0].trailing, false);
+        assert.strictEqual(cDecl.comments[0].value, " comment 3");
+    });
+
+    it("should not collapse multi line function definitions", function() {
+        var code = [
+            "var obj = {",
+            "  a(",
+            "    /*before*/ param",
+            "  ) /*after*/ {",
+            "  },",
+            "};",
+        ].join('\n');
+
+        var ast = recast.parse(code);
+        var printer = new Printer({
+            tabWidth: 2
+        });
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            code
+        );
+    });
+
+    it("should be pretty-printable in illegal positions", function() {
+        var code = [
+            "var sum = function /*anonymous*/(/*...args*/) /*int*/ {",
+            "  // TODO",
+            "};"
+        ].join("\n");
+
+        var ast = recast.parse(code);
+        var funExp = ast.program.body[0].declarations[0].init;
+        n.FunctionExpression.assert(funExp);
+
+        funExp.original = null;
+
+        var comments = funExp.body.comments;
+        assert.strictEqual(comments.length, 4);
+        funExp.id = comments.shift();
+        funExp.params.push(comments.shift());
+        funExp.body.body.push(comments.pop());
+
+        assert.strictEqual(
+            recast.print(ast).code,
+            code
+        );
+    });
+});
diff --git a/test/data/backbone.js b/test/data/backbone.js
new file mode 100644
index 0000000..e09fa28
--- /dev/null
+++ b/test/data/backbone.js
@@ -0,0 +1,1578 @@
+//     Backbone.js 1.0.0
+
+//     (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc.
+//     (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+//     Backbone may be freely distributed under the MIT license.
+//     For all details and documentation:
+//     http://backbonejs.org
+
+(function(){
+
+  // Initial Setup
+  // -------------
+
+  // Save a reference to the global object (`window` in the browser, `exports`
+  // on the server).
+  var root = this;
+
+  // Save the previous value of the `Backbone` variable, so that it can be
+  // restored later on, if `noConflict` is used.
+  var previousBackbone = root.Backbone;
+
+  // Create local references to array methods we'll want to use later.
+  var array = [];
+  var push = array.push;
+  var slice = array.slice;
+  var splice = array.splice;
+
+  // The top-level namespace. All public Backbone classes and modules will
+  // be attached to this. Exported for both the browser and the server.
+  var Backbone;
+  if (typeof exports !== 'undefined') {
+    Backbone = exports;
+  } else {
+    Backbone = root.Backbone = {};
+  }
+
+  // Current version of the library. Keep in sync with `package.json`.
+  Backbone.VERSION = '1.0.0';
+
+  // Require Underscore, if we're on the server, and it's not already present.
+  var _ = root._;
+  if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
+
+  // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
+  // the `$` variable.
+  Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;
+
+  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
+  // to its previous owner. Returns a reference to this Backbone object.
+  Backbone.noConflict = function() {
+    root.Backbone = previousBackbone;
+    return this;
+  };
+
+  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
+  // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
+  // set a `X-Http-Method-Override` header.
+  Backbone.emulateHTTP = false;
+
+  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
+  // `application/json` requests ... will encode the body as
+  // `application/x-www-form-urlencoded` instead and will send the model in a
+  // form param named `model`.
+  Backbone.emulateJSON = false;
+
+  // Backbone.Events
+  // ---------------
+
+  // A module that can be mixed in to *any object* in order to provide it with
+  // custom events. You may bind with `on` or remove with `off` callback
+  // functions to an event; `trigger`-ing an event fires all callbacks in
+  // succession.
+  //
+  //     var object = {};
+  //     _.extend(object, Backbone.Events);
+  //     object.on('expand', function(){ alert('expanded'); });
+  //     object.trigger('expand');
+  //
+  var Events = Backbone.Events = {
+
+    // Bind an event to a `callback` function. Passing `"all"` will bind
+    // the callback to all events fired.
+    on: function(name, callback, context) {
+      if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
+      this._events || (this._events = {});
+      var events = this._events[name] || (this._events[name] = []);
+      events.push({callback: callback, context: context, ctx: context || this});
+      return this;
+    },
+
+    // Bind an event to only be triggered a single time. After the first time
+    // the callback is invoked, it will be removed.
+    once: function(name, callback, context) {
+      if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
+      var self = this;
+      var once = _.once(function() {
+        self.off(name, once);
+        callback.apply(this, arguments);
+      });
+      once._callback = callback;
+      return this.on(name, once, context);
+    },
+
+    // Remove one or many callbacks. If `context` is null, removes all
+    // callbacks with that function. If `callback` is null, removes all
+    // callbacks for the event. If `name` is null, removes all bound
+    // callbacks for all events.
+    off: function(name, callback, context) {
+      var retain, ev, events, names, i, l, j, k;
+      if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
+      if (!name && !callback && !context) {
+        this._events = {};
+        return this;
+      }
+
+      names = name ? [name] : _.keys(this._events);
+      for (i = 0, l = names.length; i < l; i++) {
+        name = names[i];
+        if (events = this._events[name]) {
+          this._events[name] = retain = [];
+          if (callback || context) {
+            for (j = 0, k = events.length; j < k; j++) {
+              ev = events[j];
+              if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
+                  (context && context !== ev.context)) {
+                retain.push(ev);
+              }
+            }
+          }
+          if (!retain.length) delete this._events[name];
+        }
+      }
+
+      return this;
+    },
+
+    // Trigger one or many events, firing all bound callbacks. Callbacks are
+    // passed the same arguments as `trigger` is, apart from the event name
+    // (unless you're listening on `"all"`, which will cause your callback to
+    // receive the true name of the event as the first argument).
+    trigger: function(name) {
+      if (!this._events) return this;
+      var args = slice.call(arguments, 1);
+      if (!eventsApi(this, 'trigger', name, args)) return this;
+      var events = this._events[name];
+      var allEvents = this._events.all;
+      if (events) triggerEvents(events, args);
+      if (allEvents) triggerEvents(allEvents, arguments);
+      return this;
+    },
+
+    // Tell this object to stop listening to either specific events ... or
+    // to every object it's currently listening to.
+    stopListening: function(obj, name, callback) {
+      var listeners = this._listeners;
+      if (!listeners) return this;
+      var deleteListener = !name && !callback;
+      if (typeof name === 'object') callback = this;
+      if (obj) (listeners = {})[obj._listenerId] = obj;
+      for (var id in listeners) {
+        listeners[id].off(name, callback, this);
+        if (deleteListener) delete this._listeners[id];
+      }
+      return this;
+    }
+
+  };
+
+  // Regular expression used to split event strings.
+  var eventSplitter = /\s+/;
+
+  // Implement fancy features of the Events API such as multiple event
+  // names `"change blur"` and jQuery-style event maps `{change: action}`
+  // in terms of the existing API.
+  var eventsApi = function(obj, action, name, rest) {
+    if (!name) return true;
+
+    // Handle event maps.
+    if (typeof name === 'object') {
+      for (var key in name) {
+        obj[action].apply(obj, [key, name[key]].concat(rest));
+      }
+      return false;
+    }
+
+    // Handle space separated event names.
+    if (eventSplitter.test(name)) {
+      var names = name.split(eventSplitter);
+      for (var i = 0, l = names.length; i < l; i++) {
+        obj[action].apply(obj, [names[i]].concat(rest));
+      }
+      return false;
+    }
+
+    return true;
+  };
+
+  // A difficult-to-believe, but optimized internal dispatch function for
+  // triggering events. Tries to keep the usual cases speedy (most internal
+  // Backbone events have 3 arguments).
+  var triggerEvents = function(events, args) {
+    var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
+    switch (args.length) {
+      case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
+      case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
+      case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
+      case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
+      default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
+    }
+  };
+
+  var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
+
+  // Inversion-of-control versions of `on` and `once`. Tell *this* object to
+  // listen to an event in another object ... keeping track of what it's
+  // listening to.
+  _.each(listenMethods, function(implementation, method) {
+    Events[method] = function(obj, name, callback) {
+      var listeners = this._listeners || (this._listeners = {});
+      var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
+      listeners[id] = obj;
+      if (typeof name === 'object') callback = this;
+      obj[implementation](name, callback, this);
+      return this;
+    };
+  });
+
+  // Aliases for backwards compatibility.
+  Events.bind   = Events.on;
+  Events.unbind = Events.off;
+
+  // Allow the `Backbone` object to serve as a global event bus, for folks who
+  // want global "pubsub" in a convenient place.
+  _.extend(Backbone, Events);
+
+  // Backbone.Model
+  // --------------
+
+  // Backbone **Models** are the basic data object in the framework --
+  // frequently representing a row in a table in a database on your server.
+  // A discrete chunk of data and a bunch of useful, related methods for
+  // performing computations and transformations on that data.
+
+  // Create a new model with the specified attributes. A client id (`cid`)
+  // is automatically generated and assigned for you.
+  var Model = Backbone.Model = function(attributes, options) {
+    var defaults;
+    var attrs = attributes || {};
+    options || (options = {});
+    this.cid = _.uniqueId('c');
+    this.attributes = {};
+    if (options.collection) this.collection = options.collection;
+    if (options.parse) attrs = this.parse(attrs, options) || {};
+    options._attrs || (options._attrs = attrs);
+    if (defaults = _.result(this, 'defaults')) {
+      attrs = _.defaults({}, attrs, defaults);
+    }
+    this.set(attrs, options);
+    this.changed = {};
+    this.initialize.apply(this, arguments);
+  };
+
+  // Attach all inheritable methods to the Model prototype.
+  _.extend(Model.prototype, Events, {
+
+    // A hash of attributes whose current and previous value differ.
+    changed: null,
+
+    // The value returned during the last failed validation.
+    validationError: null,
+
+    // The default name for the JSON `id` attribute is `"id"`. MongoDB and
+    // CouchDB users may want to set this to `"_id"`.
+    idAttribute: 'id',
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // Return a copy of the model's `attributes` object.
+    toJSON: function(options) {
+      return _.clone(this.attributes);
+    },
+
+    // Proxy `Backbone.sync` by default -- but override this if you need
+    // custom syncing semantics for *this* particular model.
+    sync: function() {
+      return Backbone.sync.apply(this, arguments);
+    },
+
+    // Get the value of an attribute.
+    get: function(attr) {
+      return this.attributes[attr];
+    },
+
+    // Get the HTML-escaped value of an attribute.
+    escape: function(attr) {
+      return _.escape(this.get(attr));
+    },
+
+    // Returns `true` if the attribute contains a value that is not null
+    // or undefined.
+    has: function(attr) {
+      return this.get(attr) != null;
+    },
+
+    // Set a hash of model attributes on the object, firing `"change"`. This is
+    // the core primitive operation of a model, updating the data and notifying
+    // anyone who needs to know about the change in state. The heart of the beast.
+    set: function(key, val, options) {
+      var attr, attrs, unset, changes, silent, changing, prev, current;
+      if (key == null) return this;
+
+      // Handle both `"key", value` and `{key: value}` -style arguments.
+      if (typeof key === 'object') {
+        attrs = key;
+        options = val;
+      } else {
+        (attrs = {})[key] = val;
+      }
+
+      options || (options = {});
+
+      // Run validation.
+      if (!this._validate(attrs, options)) return false;
+
+      // Extract attributes and options.
+      unset           = options.unset;
+      silent          = options.silent;
+      changes         = [];
+      changing        = this._changing;
+      this._changing  = true;
+
+      if (!changing) {
+        this._previousAttributes = _.clone(this.attributes);
+        this.changed = {};
+      }
+      current = this.attributes, prev = this._previousAttributes;
+
+      // Check for changes of `id`.
+      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
+
+      // For each `set` attribute, update or delete the current value.
+      for (attr in attrs) {
+        val = attrs[attr];
+        if (!_.isEqual(current[attr], val)) changes.push(attr);
+        if (!_.isEqual(prev[attr], val)) {
+          this.changed[attr] = val;
+        } else {
+          delete this.changed[attr];
+        }
+        unset ? delete current[attr] : current[attr] = val;
+      }
+
+      // Trigger all relevant attribute changes.
+      if (!silent) {
+        if (changes.length) this._pending = true;
+        for (var i = 0, l = changes.length; i < l; i++) {
+          this.trigger('change:' + changes[i], this, current[changes[i]], options);
+        }
+      }
+
+      // You might be wondering why there's a `while` loop here. Changes can
+      // be recursively nested within `"change"` events.
+      if (changing) return this;
+      if (!silent) {
+        while (this._pending) {
+          this._pending = false;
+          this.trigger('change', this, options);
+        }
+      }
+      this._pending = false;
+      this._changing = false;
+      return this;
+    },
+
+    // Remove an attribute from the model, firing `"change"`. `unset` is a noop
+    // if the attribute doesn't exist.
+    unset: function(attr, options) {
+      return this.set(attr, void 0, _.extend({}, options, {unset: true}));
+    },
+
+    // Clear all attributes on the model, firing `"change"`.
+    clear: function(options) {
+      var attrs = {};
+      for (var key in this.attributes) attrs[key] = void 0;
+      return this.set(attrs, _.extend({}, options, {unset: true}));
+    },
+
+    // Determine if the model has changed since the last `"change"` event.
+    // If you specify an attribute name, determine if that attribute has changed.
+    hasChanged: function(attr) {
+      if (attr == null) return !_.isEmpty(this.changed);
+      return _.has(this.changed, attr);
+    },
+
+    // Return an object containing all the attributes that have changed, or
+    // false if there are no changed attributes. Useful for determining what
+    // parts of a view need to be updated and/or what attributes need to be
+    // persisted to the server. Unset attributes will be set to undefined.
+    // You can also pass an attributes object to diff against the model,
+    // determining if there *would be* a change.
+    changedAttributes: function(diff) {
+      if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
+      var val, changed = false;
+      var old = this._changing ? this._previousAttributes : this.attributes;
+      for (var attr in diff) {
+        if (_.isEqual(old[attr], (val = diff[attr]))) continue;
+        (changed || (changed = {}))[attr] = val;
+      }
+      return changed;
+    },
+
+    // Get the previous value of an attribute, recorded at the time the last
+    // `"change"` event was fired.
+    previous: function(attr) {
+      if (attr == null || !this._previousAttributes) return null;
+      return this._previousAttributes[attr];
+    },
+
+    // Get all of the attributes of the model at the time of the previous
+    // `"change"` event.
+    previousAttributes: function() {
+      return _.clone(this._previousAttributes);
+    },
+
+    // Fetch the model from the server. If the server's representation of the
+    // model differs from its current attributes, they will be overridden,
+    // triggering a `"change"` event.
+    fetch: function(options) {
+      options = options ? _.clone(options) : {};
+      if (options.parse === void 0) options.parse = true;
+      var model = this;
+      var success = options.success;
+      options.success = function(resp) {
+        if (!model.set(model.parse(resp, options), options)) return false;
+        if (success) success(model, resp, options);
+        model.trigger('sync', model, resp, options);
+      };
+      wrapError(this, options);
+      return this.sync('read', this, options);
+    },
+
+    // Set a hash of model attributes, and sync the model to the server.
+    // If the server returns an attributes hash that differs, the model's
+    // state will be `set` again.
+    save: function(key, val, options) {
+      var attrs, method, xhr, attributes = this.attributes;
+
+      // Handle both `"key", value` and `{key: value}` -style arguments.
+      if (key == null || typeof key === 'object') {
+        attrs = key;
+        options = val;
+      } else {
+        (attrs = {})[key] = val;
+      }
+
+      options = _.extend({validate: true}, options);
+
+      // If we're not waiting and attributes exist, save acts as
+      // `set(attr).save(null, opts)` with validation. Otherwise, check if
+      // the model will be valid when the attributes, if any, are set.
+      if (attrs && !options.wait) {
+        if (!this.set(attrs, options)) return false;
+      } else {
+        if (!this._validate(attrs, options)) return false;
+      }
+
+      // Set temporary attributes if `{wait: true}`.
+      if (attrs && options.wait) {
+        this.attributes = _.extend({}, attributes, attrs);
+      }
+
+      // After a successful server-side save, the client is (optionally)
+      // updated with the server-side state.
+      if (options.parse === void 0) options.parse = true;
+      var model = this;
+      var success = options.success;
+      options.success = function(resp) {
+        // Ensure attributes are restored during synchronous saves.
+        model.attributes = attributes;
+        var serverAttrs = model.parse(resp, options);
+        if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
+        if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
+          return false;
+        }
+        if (success) success(model, resp, options);
+        model.trigger('sync', model, resp, options);
+      };
+      wrapError(this, options);
+
+      method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
+      if (method === 'patch') options.attrs = attrs;
+      xhr = this.sync(method, this, options);
+
+      // Restore attributes.
+      if (attrs && options.wait) this.attributes = attributes;
+
+      return xhr;
+    },
+
+    // Destroy this model on the server if it was already persisted.
+    // Optimistically removes the model from its collection, if it has one.
+    // If `wait: true` is passed, waits for the server to respond before removal.
+    destroy: function(options) {
+      options = options ? _.clone(options) : {};
+      var model = this;
+      var success = options.success;
+
+      var destroy = function() {
+        model.trigger('destroy', model, model.collection, options);
+      };
+
+      options.success = function(resp) {
+        if (options.wait || model.isNew()) destroy();
+        if (success) success(model, resp, options);
+        if (!model.isNew()) model.trigger('sync', model, resp, options);
+      };
+
+      if (this.isNew()) {
+        options.success();
+        return false;
+      }
+      wrapError(this, options);
+
+      var xhr = this.sync('delete', this, options);
+      if (!options.wait) destroy();
+      return xhr;
+    },
+
+    // Default URL for the model's representation on the server -- if you're
+    // using Backbone's restful methods, override this to change the endpoint
+    // that will be called.
+    url: function() {
+      var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
+      if (this.isNew()) return base;
+      return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
+    },
+
+    // **parse** converts a response into the hash of attributes to be `set` on
+    // the model. The default implementation is just to pass the response along.
+    parse: function(resp, options) {
+      return resp;
+    },
+
+    // Create a new model with identical attributes to this one.
+    clone: function() {
+      return new this.constructor(this.attributes);
+    },
+
+    // A model is new if it has never been saved to the server, and lacks an id.
+    isNew: function() {
+      return this.id == null;
+    },
+
+    // Check if the model is currently in a valid state.
+    isValid: function(options) {
+      return this._validate({}, _.extend(options || {}, { validate: true }));
+    },
+
+    // Run validation against the next complete set of model attributes,
+    // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
+    _validate: function(attrs, options) {
+      if (!options.validate || !this.validate) return true;
+      attrs = _.extend({}, this.attributes, attrs);
+      var error = this.validationError = this.validate(attrs, options) || null;
+      if (!error) return true;
+      this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));
+      return false;
+    }
+
+  });
+
+  // Underscore methods that we want to implement on the Model.
+  var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
+
+  // Mix in each Underscore method as a proxy to `Model#attributes`.
+  _.each(modelMethods, function(method) {
+    Model.prototype[method] = function() {
+      var args = slice.call(arguments);
+      args.unshift(this.attributes);
+      return _[method].apply(_, args);
+    };
+  });
+
+  // Backbone.Collection
+  // -------------------
+
+  // If models tend to represent a single row of data, a Backbone Collection is
+  // more analagous to a table full of data ... or a small slice or page of that
+  // table, or a collection of rows that belong together for a particular reason
+  // -- all of the messages in this particular folder, all of the documents
+  // belonging to this particular author, and so on. Collections maintain
+  // indexes of their models, both in order, and for lookup by `id`.
+
+  // Create a new **Collection**, perhaps to contain a specific type of `model`.
+  // If a `comparator` is specified, the Collection will maintain
+  // its models in sort order, as they're added and removed.
+  var Collection = Backbone.Collection = function(models, options) {
+    options || (options = {});
+    if (options.model) this.model = options.model;
+    if (options.comparator !== void 0) this.comparator = options.comparator;
+    this._reset();
+    this.initialize.apply(this, arguments);
+    if (models) this.reset(models, _.extend({silent: true}, options));
+  };
+
+  // Default options for `Collection#set`.
+  var setOptions = {add: true, remove: true, merge: true};
+  var addOptions = {add: true, remove: false};
+
+  // Define the Collection's inheritable methods.
+  _.extend(Collection.prototype, Events, {
+
+    // The default model for a collection is just a **Backbone.Model**.
+    // This should be overridden in most cases.
+    model: Model,
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // The JSON representation of a Collection is an array of the
+    // models' attributes.
+    toJSON: function(options) {
+      return this.map(function(model){ return model.toJSON(options); });
+    },
+
+    // Proxy `Backbone.sync` by default.
+    sync: function() {
+      return Backbone.sync.apply(this, arguments);
+    },
+
+    // Add a model, or list of models to the set.
+    add: function(models, options) {
+      return this.set(models, _.extend({merge: false}, options, addOptions));
+    },
+
+    // Remove a model, or a list of models from the set.
+    remove: function(models, options) {
+      models = _.isArray(models) ? models.slice() : [models];
+      options || (options = {});
+      var i, l, index, model;
+      for (i = 0, l = models.length; i < l; i++) {
+        model = this.get(models[i]);
+        if (!model) continue;
+        delete this._byId[model.id];
+        delete this._byId[model.cid];
+        index = this.indexOf(model);
+        this.models.splice(index, 1);
+        this.length--;
+        if (!options.silent) {
+          options.index = index;
+          model.trigger('remove', model, this, options);
+        }
+        this._removeReference(model);
+      }
+      return this;
+    },
+
+    // Update a collection by `set`-ing a new list of models, adding new ones,
+    // removing models that are no longer present, and merging models that
+    // already exist in the collection, as necessary. Similar to **Model#set**,
+    // the core operation for updating the data contained by the collection.
+    set: function(models, options) {
+      options = _.defaults({}, options, setOptions);
+      if (options.parse) models = this.parse(models, options);
+      if (!_.isArray(models)) models = models ? [models] : [];
+      var i, l, model, attrs, existing, sort;
+      var at = options.at;
+      var sortable = this.comparator && (at == null) && options.sort !== false;
+      var sortAttr = _.isString(this.comparator) ? this.comparator : null;
+      var toAdd = [], toRemove = [], modelMap = {};
+      var add = options.add, merge = options.merge, remove = options.remove;
+      var order = !sortable && add && remove ? [] : false;
+
+      // Turn bare objects into model references, and prevent invalid models
+      // from being added.
+      for (i = 0, l = models.length; i < l; i++) {
+        if (!(model = this._prepareModel(attrs = models[i], options))) continue;
+
+        // If a duplicate is found, prevent it from being added and
+        // optionally merge it into the existing model.
+        if (existing = this.get(model)) {
+          if (remove) modelMap[existing.cid] = true;
+          if (merge) {
+            attrs = attrs === model ? model.attributes : options._attrs;
+            existing.set(attrs, options);
+            if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
+          }
+
+        // This is a new model, push it to the `toAdd` list.
+        } else if (add) {
+          toAdd.push(model);
+
+          // Listen to added models' events, and index models for lookup by
+          // `id` and by `cid`.
+          model.on('all', this._onModelEvent, this);
+          this._byId[model.cid] = model;
+          if (model.id != null) this._byId[model.id] = model;
+        }
+        if (order) order.push(existing || model);
+        delete options._attrs;
+      }
+
+      // Remove nonexistent models if appropriate.
+      if (remove) {
+        for (i = 0, l = this.length; i < l; ++i) {
+          if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
+        }
+        if (toRemove.length) this.remove(toRemove, options);
+      }
+
+      // See if sorting is needed, update `length` and splice in new models.
+      if (toAdd.length || (order && order.length)) {
+        if (sortable) sort = true;
+        this.length += toAdd.length;
+        if (at != null) {
+          splice.apply(this.models, [at, 0].concat(toAdd));
+        } else {
+          if (order) this.models.length = 0;
+          push.apply(this.models, order || toAdd);
+        }
+      }
+
+      // Silently sort the collection if appropriate.
+      if (sort) this.sort({silent: true});
+
+      if (options.silent) return this;
+
+      // Trigger `add` events.
+      for (i = 0, l = toAdd.length; i < l; i++) {
+        (model = toAdd[i]).trigger('add', model, this, options);
+      }
+
+      // Trigger `sort` if the collection was sorted.
+      if (sort || (order && order.length)) this.trigger('sort', this, options);
+      return this;
+    },
+
+    // When you have more items than you want to add or remove individually,
+    // you can reset the entire set with a new list of models, without firing
+    // any granular `add` or `remove` events. Fires `reset` when finished.
+    // Useful for bulk operations and optimizations.
+    reset: function(models, options) {
+      options || (options = {});
+      for (var i = 0, l = this.models.length; i < l; i++) {
+        this._removeReference(this.models[i]);
+      }
+      options.previousModels = this.models;
+      this._reset();
+      this.add(models, _.extend({silent: true}, options));
+      if (!options.silent) this.trigger('reset', this, options);
+      return this;
+    },
+
+    // Add a model to the end of the collection.
+    push: function(model, options) {
+      model = this._prepareModel(model, options);
+      this.add(model, _.extend({at: this.length}, options));
+      return model;
+    },
+
+    // Remove a model from the end of the collection.
+    pop: function(options) {
+      var model = this.at(this.length - 1);
+      this.remove(model, options);
+      return model;
+    },
+
+    // Add a model to the beginning of the collection.
+    unshift: function(model, options) {
+      model = this._prepareModel(model, options);
+      this.add(model, _.extend({at: 0}, options));
+      return model;
+    },
+
+    // Remove a model from the beginning of the collection.
+    shift: function(options) {
+      var model = this.at(0);
+      this.remove(model, options);
+      return model;
+    },
+
+    // Slice out a sub-array of models from the collection.
+    slice: function() {
+      return slice.apply(this.models, arguments);
+    },
+
+    // Get a model from the set by id.
+    get: function(obj) {
+      if (obj == null) return void 0;
+      return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj];
+    },
+
+    // Get the model at the given index.
+    at: function(index) {
+      return this.models[index];
+    },
+
+    // Return models with matching attributes. Useful for simple cases of
+    // `filter`.
+    where: function(attrs, first) {
+      if (_.isEmpty(attrs)) return first ? void 0 : [];
+      return this[first ? 'find' : 'filter'](function(model) {
+        for (var key in attrs) {
+          if (attrs[key] !== model.get(key)) return false;
+        }
+        return true;
+      });
+    },
+
+    // Return the first model with matching attributes. Useful for simple cases
+    // of `find`.
+    findWhere: function(attrs) {
+      return this.where(attrs, true);
+    },
+
+    // Force the collection to re-sort itself. You don't need to call this under
+    // normal circumstances, as the set will maintain sort order as each item
+    // is added.
+    sort: function(options) {
+      if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
+      options || (options = {});
+
+      // Run sort based on type of `comparator`.
+      if (_.isString(this.comparator) || this.comparator.length === 1) {
+        this.models = this.sortBy(this.comparator, this);
+      } else {
+        this.models.sort(_.bind(this.comparator, this));
+      }
+
+      if (!options.silent) this.trigger('sort', this, options);
+      return this;
+    },
+
+    // Figure out the smallest index at which a model should be inserted so as
+    // to maintain order.
+    sortedIndex: function(model, value, context) {
+      value || (value = this.comparator);
+      var iterator = _.isFunction(value) ? value : function(model) {
+        return model.get(value);
+      };
+      return _.sortedIndex(this.models, model, iterator, context);
+    },
+
+    // Pluck an attribute from each model in the collection.
+    pluck: function(attr) {
+      return _.invoke(this.models, 'get', attr);
+    },
+
+    // Fetch the default set of models for this collection, resetting the
+    // collection when they arrive. If `reset: true` is passed, the response
+    // data will be passed through the `reset` method instead of `set`.
+    fetch: function(options) {
+      options = options ? _.clone(options) : {};
+      if (options.parse === void 0) options.parse = true;
+      var success = options.success;
+      var collection = this;
+      options.success = function(resp) {
+        var method = options.reset ? 'reset' : 'set';
+        collection[method](resp, options);
+        if (success) success(collection, resp, options);
+        collection.trigger('sync', collection, resp, options);
+      };
+      wrapError(this, options);
+      return this.sync('read', this, options);
+    },
+
+    // Create a new instance of a model in this collection. Add the model to the
+    // collection immediately, unless `wait: true` is passed, in which case we
+    // wait for the server to agree.
+    create: function(model, options) {
+      options = options ? _.clone(options) : {};
+      if (!(model = this._prepareModel(model, options))) return false;
+      if (!options.wait) this.add(model, options);
+      var collection = this;
+      var success = options.success;
+      options.success = function(model, resp, options) {
+        if (options.wait) collection.add(model, options);
+        if (success) success(model, resp, options);
+      };
+      model.save(null, options);
+      return model;
+    },
+
+    // **parse** converts a response into a list of models to be added to the
+    // collection. The default implementation is just to pass it through.
+    parse: function(resp, options) {
+      return resp;
+    },
+
+    // Create a new collection with an identical list of models as this one.
+    clone: function() {
+      return new this.constructor(this.models);
+    },
+
+    // Private method to reset all internal state. Called when the collection
+    // is first initialized or reset.
+    _reset: function() {
+      this.length = 0;
+      this.models = [];
+      this._byId  = {};
+    },
+
+    // Prepare a hash of attributes (or other model) to be added to this
+    // collection.
+    _prepareModel: function(attrs, options) {
+      if (attrs instanceof Model) {
+        if (!attrs.collection) attrs.collection = this;
+        return attrs;
+      }
+      options || (options = {});
+      options.collection = this;
+      var model = new this.model(attrs, options);
+      if (!model.validationError) return model;
+      this.trigger('invalid', this, attrs, options);
+      return false;
+    },
+
+    // Internal method to sever a model's ties to a collection.
+    _removeReference: function(model) {
+      if (this === model.collection) delete model.collection;
+      model.off('all', this._onModelEvent, this);
+    },
+
+    // Internal method called every time a model in the set fires an event.
+    // Sets need to update their indexes when models change ids. All other
+    // events simply proxy through. "add" and "remove" events that originate
+    // in other collections are ignored.
+    _onModelEvent: function(event, model, collection, options) {
+      if ((event === 'add' || event === 'remove') && collection !== this) return;
+      if (event === 'destroy') this.remove(model, options);
+      if (model && event === 'change:' + model.idAttribute) {
+        delete this._byId[model.previous(model.idAttribute)];
+        if (model.id != null) this._byId[model.id] = model;
+      }
+      this.trigger.apply(this, arguments);
+    }
+
+  });
+
+  // Underscore methods that we want to implement on the Collection.
+  // 90% of the core usefulness of Backbone Collections is actually implemented
+  // right here:
+  var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
+    'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
+    'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
+    'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
+    'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
+    'lastIndexOf', 'isEmpty', 'chain'];
+
+  // Mix in each Underscore method as a proxy to `Collection#models`.
+  _.each(methods, function(method) {
+    Collection.prototype[method] = function() {
+      var args = slice.call(arguments);
+      args.unshift(this.models);
+      return _[method].apply(_, args);
+    };
+  });
+
+  // Underscore methods that take a property name as an argument.
+  var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
+
+  // Use attributes instead of properties.
+  _.each(attributeMethods, function(method) {
+    Collection.prototype[method] = function(value, context) {
+      var iterator = _.isFunction(value) ? value : function(model) {
+        return model.get(value);
+      };
+      return _[method](this.models, iterator, context);
+    };
+  });
+
+  // Backbone.View
+  // -------------
+
+  // Backbone Views are almost more convention than they are actual code. A View
+  // is simply a JavaScript object that represents a logical chunk of UI in the
+  // DOM. This might be a single item, an entire list, a sidebar or panel, or
+  // even the surrounding frame which wraps your whole app. Defining a chunk of
+  // UI as a **View** allows you to define your DOM events declaratively, without
+  // having to worry about render order ... and makes it easy for the view to
+  // react to specific changes in the state of your models.
+
+  // Options with special meaning *(e.g. model, collection, id, className)* are
+  // attached directly to the view.  See `viewOptions` for an exhaustive
+  // list.
+
+  // Creating a Backbone.View creates its initial element outside of the DOM,
+  // if an existing element is not provided...
+  var View = Backbone.View = function(options) {
+    this.cid = _.uniqueId('view');
+    options || (options = {});
+    _.extend(this, _.pick(options, viewOptions));
+    this._ensureElement();
+    this.initialize.apply(this, arguments);
+    this.delegateEvents();
+  };
+
+  // Cached regex to split keys for `delegate`.
+  var delegateEventSplitter = /^(\S+)\s*(.*)$/;
+
+  // List of view options to be merged as properties.
+  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
+
+  // Set up all inheritable **Backbone.View** properties and methods.
+  _.extend(View.prototype, Events, {
+
+    // The default `tagName` of a View's element is `"div"`.
+    tagName: 'div',
+
+    // jQuery delegate for element lookup, scoped to DOM elements within the
+    // current view. This should be prefered to global lookups where possible.
+    $: function(selector) {
+      return this.$el.find(selector);
+    },
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // **render** is the core function that your view should override, in order
+    // to populate its element (`this.el`), with the appropriate HTML. The
+    // convention is for **render** to always return `this`.
+    render: function() {
+      return this;
+    },
+
+    // Remove this view by taking the element out of the DOM, and removing any
+    // applicable Backbone.Events listeners.
+    remove: function() {
+      this.$el.remove();
+      this.stopListening();
+      return this;
+    },
+
+    // Change the view's element (`this.el` property), including event
+    // re-delegation.
+    setElement: function(element, delegate) {
+      if (this.$el) this.undelegateEvents();
+      this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
+      this.el = this.$el[0];
+      if (delegate !== false) this.delegateEvents();
+      return this;
+    },
+
+    // Set callbacks, where `this.events` is a hash of
+    //
+    // *{"event selector": "callback"}*
+    //
+    //     {
+    //       'mousedown .title':  'edit',
+    //       'click .button':     'save'
+    //       'click .open':       function(e) { ... }
+    //     }
+    //
+    // pairs. Callbacks will be bound to the view, with `this` set properly.
+    // Uses event delegation for efficiency.
+    // Omitting the selector binds the event to `this.el`.
+    // This only works for delegate-able events: not `focus`, `blur`, and
+    // not `change`, `submit`, and `reset` in Internet Explorer.
+    delegateEvents: function(events) {
+      if (!(events || (events = _.result(this, 'events')))) return this;
+      this.undelegateEvents();
+      for (var key in events) {
+        var method = events[key];
+        if (!_.isFunction(method)) method = this[events[key]];
+        if (!method) continue;
+
+        var match = key.match(delegateEventSplitter);
+        var eventName = match[1], selector = match[2];
+        method = _.bind(method, this);
+        eventName += '.delegateEvents' + this.cid;
+        if (selector === '') {
+          this.$el.on(eventName, method);
+        } else {
+          this.$el.on(eventName, selector, method);
+        }
+      }
+      return this;
+    },
+
+    // Clears all callbacks previously bound to the view with `delegateEvents`.
+    // You usually don't need to use this, but may wish to if you have multiple
+    // Backbone views attached to the same DOM element.
+    undelegateEvents: function() {
+      this.$el.off('.delegateEvents' + this.cid);
+      return this;
+    },
+
+    // Ensure that the View has a DOM element to render into.
+    // If `this.el` is a string, pass it through `$()`, take the first
+    // matching element, and re-assign it to `el`. Otherwise, create
+    // an element from the `id`, `className` and `tagName` properties.
+    _ensureElement: function() {
+      if (!this.el) {
+        var attrs = _.extend({}, _.result(this, 'attributes'));
+        if (this.id) attrs.id = _.result(this, 'id');
+        if (this.className) attrs['class'] = _.result(this, 'className');
+        var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
+        this.setElement($el, false);
+      } else {
+        this.setElement(_.result(this, 'el'), false);
+      }
+    }
+
+  });
+
+  // Backbone.sync
+  // -------------
+
+  // Override this function to change the manner in which Backbone persists
+  // models to the server. You will be passed the type of request, and the
+  // model in question. By default, makes a RESTful Ajax request
+  // to the model's `url()`. Some possible customizations could be:
+  //
+  // * Use `setTimeout` to batch rapid-fire updates into a single request.
+  // * Send up the models as XML instead of JSON.
+  // * Persist models via WebSockets instead of Ajax.
+  //
+  // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
+  // as `POST`, with a `_method` parameter containing the true HTTP method,
+  // as well as all requests with the body as `application/x-www-form-urlencoded`
+  // instead of `application/json` with the model in a param named `model`.
+  // Useful when interfacing with server-side languages like **PHP** that make
+  // it difficult to read the body of `PUT` requests.
+  Backbone.sync = function(method, model, options) {
+    var type = methodMap[method];
+
+    // Default options, unless specified.
+    _.defaults(options || (options = {}), {
+      emulateHTTP: Backbone.emulateHTTP,
+      emulateJSON: Backbone.emulateJSON
+    });
+
+    // Default JSON-request options.
+    var params = {type: type, dataType: 'json'};
+
+    // Ensure that we have a URL.
+    if (!options.url) {
+      params.url = _.result(model, 'url') || urlError();
+    }
+
+    // Ensure that we have the appropriate request data.
+    if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
+      params.contentType = 'application/json';
+      params.data = JSON.stringify(options.attrs || model.toJSON(options));
+    }
+
+    // For older servers, emulate JSON by encoding the request into an HTML-form.
+    if (options.emulateJSON) {
+      params.contentType = 'application/x-www-form-urlencoded';
+      params.data = params.data ? {model: params.data} : {};
+    }
+
+    // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
+    // And an `X-HTTP-Method-Override` header.
+    if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
+      params.type = 'POST';
+      if (options.emulateJSON) params.data._method = type;
+      var beforeSend = options.beforeSend;
+      options.beforeSend = function(xhr) {
+        xhr.setRequestHeader('X-HTTP-Method-Override', type);
+        if (beforeSend) return beforeSend.apply(this, arguments);
+      };
+    }
+
+    // Don't process data on a non-GET request.
+    if (params.type !== 'GET' && !options.emulateJSON) {
+      params.processData = false;
+    }
+
+    // If we're sending a `PATCH` request, and we're in an old Internet Explorer
+    // that still has ActiveX enabled by default, override jQuery to use that
+    // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
+    if (params.type === 'PATCH' && noXhrPatch) {
+      params.xhr = function() {
+        return new ActiveXObject("Microsoft.XMLHTTP");
+      };
+    }
+
+    // Make the request, allowing the user to override any Ajax options.
+    var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
+    model.trigger('request', model, xhr, options);
+    return xhr;
+  };
+
+  var noXhrPatch = typeof window !== 'undefined' && !!window.ActiveXObject && !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
+
+  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
+  var methodMap = {
+    'create': 'POST',
+    'update': 'PUT',
+    'patch':  'PATCH',
+    'delete': 'DELETE',
+    'read':   'GET'
+  };
+
+  // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
+  // Override this if you'd like to use a different library.
+  Backbone.ajax = function() {
+    return Backbone.$.ajax.apply(Backbone.$, arguments);
+  };
+
+  // Backbone.Router
+  // ---------------
+
+  // Routers map faux-URLs to actions, and fire events when routes are
+  // matched. Creating a new one sets its `routes` hash, if not set statically.
+  var Router = Backbone.Router = function(options) {
+    options || (options = {});
+    if (options.routes) this.routes = options.routes;
+    this._bindRoutes();
+    this.initialize.apply(this, arguments);
+  };
+
+  // Cached regular expressions for matching named param parts and splatted
+  // parts of route strings.
+  var optionalParam = /\((.*?)\)/g;
+  var namedParam    = /(\(\?)?:\w+/g;
+  var splatParam    = /\*\w+/g;
+  var escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;
+
+  // Set up all inheritable **Backbone.Router** properties and methods.
+  _.extend(Router.prototype, Events, {
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // Manually bind a single named route to a callback. For example:
+    //
+    //     this.route('search/:query/p:num', 'search', function(query, num) {
+    //       ...
+    //     });
+    //
+    route: function(route, name, callback) {
+      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
+      if (_.isFunction(name)) {
+        callback = name;
+        name = '';
+      }
+      if (!callback) callback = this[name];
+      var router = this;
+      Backbone.history.route(route, function(fragment) {
+        var args = router._extractParameters(route, fragment);
+        callback && callback.apply(router, args);
+        router.trigger.apply(router, ['route:' + name].concat(args));
+        router.trigger('route', name, args);
+        Backbone.history.trigger('route', router, name, args);
+      });
+      return this;
+    },
+
+    // Simple proxy to `Backbone.history` to save a fragment into the history.
+    navigate: function(fragment, options) {
+      Backbone.history.navigate(fragment, options);
+      return this;
+    },
+
+    // Bind all defined routes to `Backbone.history`. We have to reverse the
+    // order of the routes here to support behavior where the most general
+    // routes can be defined at the bottom of the route map.
+    _bindRoutes: function() {
+      if (!this.routes) return;
+      this.routes = _.result(this, 'routes');
+      var route, routes = _.keys(this.routes);
+      while ((route = routes.pop()) != null) {
+        this.route(route, this.routes[route]);
+      }
+    },
+
+    // Convert a route string into a regular expression, suitable for matching
+    // against the current location hash.
+    _routeToRegExp: function(route) {
+      route = route.replace(escapeRegExp, '\\$&')
+                   .replace(optionalParam, '(?:$1)?')
+                   .replace(namedParam, function(match, optional){
+                     return optional ? match : '([^\/]+)';
+                   })
+                   .replace(splatParam, '(.*?)');
+      return new RegExp('^' + route + '$');
+    },
+
+    // Given a route, and a URL fragment that it matches, return the array of
+    // extracted decoded parameters. Empty or unmatched parameters will be
+    // treated as `null` to normalize cross-browser behavior.
+    _extractParameters: function(route, fragment) {
+      var params = route.exec(fragment).slice(1);
+      return _.map(params, function(param) {
+        return param ? decodeURIComponent(param) : null;
+      });
+    }
+
+  });
+
+  // Backbone.History
+  // ----------------
+
+  // Handles cross-browser history management, based on either
+  // [pushState](http://diveintohtml5.info/history.html) and real URLs, or
+  // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
+  // and URL fragments. If the browser supports neither (old IE, natch),
+  // falls back to polling.
+  var History = Backbone.History = function() {
+    this.handlers = [];
+    _.bindAll(this, 'checkUrl');
+
+    // Ensure that `History` can be used outside of the browser.
+    if (typeof window !== 'undefined') {
+      this.location = window.location;
+      this.history = window.history;
+    }
+  };
+
+  // Cached regex for stripping a leading hash/slash and trailing space.
+  var routeStripper = /^[#\/]|\s+$/g;
+
+  // Cached regex for stripping leading and trailing slashes.
+  var rootStripper = /^\/+|\/+$/g;
+
+  // Cached regex for detecting MSIE.
+  var isExplorer = /msie [\w.]+/;
+
+  // Cached regex for removing a trailing slash.
+  var trailingSlash = /\/$/;
+
+  // Has the history handling already been started?
+  History.started = false;
+
+  // Set up all inheritable **Backbone.History** properties and methods.
+  _.extend(History.prototype, Events, {
+
+    // The default interval to poll for hash changes, if necessary, is
+    // twenty times a second.
+    interval: 50,
+
+    // Gets the true hash value. Cannot use location.hash directly due to bug
+    // in Firefox where location.hash will always be decoded.
+    getHash: function(window) {
+      var match = (window || this).location.href.match(/#(.*)$/);
+      return match ? match[1] : '';
+    },
+
+    // Get the cross-browser normalized URL fragment, either from the URL,
+    // the hash, or the override.
+    getFragment: function(fragment, forcePushState) {
+      if (fragment == null) {
+        if (this._hasPushState || !this._wantsHashChange || forcePushState) {
+          fragment = this.location.pathname;
+          var root = this.root.replace(trailingSlash, '');
+          if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
+        } else {
+          fragment = this.getHash();
+        }
+      }
+      return fragment.replace(routeStripper, '');
+    },
+
+    // Start the hash change handling, returning `true` if the current URL matches
+    // an existing route, and `false` otherwise.
+    start: function(options) {
+      if (History.started) throw new Error("Backbone.history has already been started");
+      History.started = true;
+
+      // Figure out the initial configuration. Do we need an iframe?
+      // Is pushState desired ... is it available?
+      this.options          = _.extend({}, {root: '/'}, this.options, options);
+      this.root             = this.options.root;
+      this._wantsHashChange = this.options.hashChange !== false;
+      this._wantsPushState  = !!this.options.pushState;
+      this._hasPushState    = !!(this.options.pushState && this.history && this.history.pushState);
+      var fragment          = this.getFragment();
+      var docMode           = document.documentMode;
+      var oldIE             = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
+
+      // Normalize root to always include a leading and trailing slash.
+      this.root = ('/' + this.root + '/').replace(rootStripper, '/');
+
+      if (oldIE && this._wantsHashChange) {
+        this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
+        this.navigate(fragment);
+      }
+
+      // Depending on whether we're using pushState or hashes, and whether
+      // 'onhashchange' is supported, determine how we check the URL state.
+      if (this._hasPushState) {
+        Backbone.$(window).on('popstate', this.checkUrl);
+      } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
+        Backbone.$(window).on('hashchange', this.checkUrl);
+      } else if (this._wantsHashChange) {
+        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
+      }
+
+      // Determine if we need to change the base url, for a pushState link
+      // opened by a non-pushState browser.
+      this.fragment = fragment;
+      var loc = this.location;
+      var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
+
+      // Transition from hashChange to pushState or vice versa if both are
+      // requested.
+      if (this._wantsHashChange && this._wantsPushState) {
+
+        // If we've started off with a route from a `pushState`-enabled
+        // browser, but we're currently in a browser that doesn't support it...
+        if (!this._hasPushState && !atRoot) {
+          this.fragment = this.getFragment(null, true);
+          this.location.replace(this.root + this.location.search + '#' + this.fragment);
+          // Return immediately as browser will do redirect to new url
+          return true;
+
+        // Or if we've started out with a hash-based route, but we're currently
+        // in a browser where it could be `pushState`-based instead...
+        } else if (this._hasPushState && atRoot && loc.hash) {
+          this.fragment = this.getHash().replace(routeStripper, '');
+          this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
+        }
+
+      }
+
+      if (!this.options.silent) return this.loadUrl();
+    },
+
+    // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
+    // but possibly useful for unit testing Routers.
+    stop: function() {
+      Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
+      clearInterval(this._checkUrlInterval);
+      History.started = false;
+    },
+
+    // Add a route to be tested when the fragment changes. Routes added later
+    // may override previous routes.
+    route: function(route, callback) {
+      this.handlers.unshift({route: route, callback: callback});
+    },
+
+    // Checks the current URL to see if it has changed, and if it has,
+    // calls `loadUrl`, normalizing across the hidden iframe.
+    checkUrl: function(e) {
+      var current = this.getFragment();
+      if (current === this.fragment && this.iframe) {
+        current = this.getFragment(this.getHash(this.iframe));
+      }
+      if (current === this.fragment) return false;
+      if (this.iframe) this.navigate(current);
+      this.loadUrl();
+    },
+
+    // Attempt to load the current URL fragment. If a route succeeds with a
+    // match, returns `true`. If no defined routes matches the fragment,
+    // returns `false`.
+    loadUrl: function(fragmentOverride) {
+      var fragment = this.fragment = this.getFragment(fragmentOverride);
+      var matched = _.any(this.handlers, function(handler) {
+        if (handler.route.test(fragment)) {
+          handler.callback(fragment);
+          return true;
+        }
+      });
+      return matched;
+    },
+
+    // Save a fragment into the hash history, or replace the URL state if the
+    // 'replace' option is passed. You are responsible for properly URL-encoding
+    // the fragment in advance.
+    //
+    // The options object can contain `trigger: true` if you wish to have the
+    // route callback be fired (not usually desirable), or `replace: true`, if
+    // you wish to modify the current URL without adding an entry to the history.
+    navigate: function(fragment, options) {
+      if (!History.started) return false;
+      if (!options || options === true) options = {trigger: options};
+      fragment = this.getFragment(fragment || '');
+      if (this.fragment === fragment) return;
+      this.fragment = fragment;
+      var url = this.root + fragment;
+
+      // If pushState is available, we use it to set the fragment as a real URL.
+      if (this._hasPushState) {
+        this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
+
+      // If hash changes haven't been explicitly disabled, update the hash
+      // fragment to store history.
+      } else if (this._wantsHashChange) {
+        this._updateHash(this.location, fragment, options.replace);
+        if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
+          // Opening and closing the iframe tricks IE7 and earlier to push a
+          // history entry on hash-tag change.  When replace is true, we don't
+          // want this.
+          if(!options.replace) this.iframe.document.open().close();
+          this._updateHash(this.iframe.location, fragment, options.replace);
+        }
+
+      // If you've told us that you explicitly don't want fallback hashchange-
+      // based history, then `navigate` becomes a page refresh.
+      } else {
+        return this.location.assign(url);
+      }
+      if (options.trigger) return this.loadUrl(fragment);
+    },
+
+    // Update the hash location, either replacing the current entry, or adding
+    // a new one to the browser history.
+    _updateHash: function(location, fragment, replace) {
+      if (replace) {
+        var href = location.href.replace(/(javascript:|#).*$/, '');
+        location.replace(href + '#' + fragment);
+      } else {
+        // Some browsers require that `hash` contains a leading #.
+        location.hash = '#' + fragment;
+      }
+    }
+
+  });
+
+  // Create the default Backbone.history.
+  Backbone.history = new History;
+
+  // Helpers
+  // -------
+
+  // Helper function to correctly set up the prototype chain, for subclasses.
+  // Similar to `goog.inherits`, but uses a hash of prototype properties and
+  // class properties to be extended.
+  var extend = function(protoProps, staticProps) {
+    var parent = this;
+    var child;
+
+    // The constructor function for the new subclass is either defined by you
+    // (the "constructor" property in your `extend` definition), or defaulted
+    // by us to simply call the parent's constructor.
+    if (protoProps && _.has(protoProps, 'constructor')) {
+      child = protoProps.constructor;
+    } else {
+      child = function(){ return parent.apply(this, arguments); };
+    }
+
+    // Add static properties to the constructor function, if supplied.
+    _.extend(child, parent, staticProps);
+
+    // Set the prototype chain to inherit from `parent`, without calling
+    // `parent`'s constructor function.
+    var Surrogate = function(){ this.constructor = child; };
+    Surrogate.prototype = parent.prototype;
+    child.prototype = new Surrogate;
+
+    // Add prototype properties (instance properties) to the subclass,
+    // if supplied.
+    if (protoProps) _.extend(child.prototype, protoProps);
+
+    // Set a convenience property in case the parent's prototype is needed
+    // later.
+    child.__super__ = parent.prototype;
+
+    return child;
+  };
+
+  // Set up inheritance for the model, collection, router, view and history.
+  Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
+
+  // Throw an error when a URL is needed, and none is supplied.
+  var urlError = function() {
+    throw new Error('A "url" property or function must be specified');
+  };
+
+  // Wrap an optional error callback with a fallback error event.
+  var wrapError = function(model, options) {
+    var error = options.error;
+    options.error = function(resp) {
+      if (error) error(model, resp, options);
+      model.trigger('error', model, resp, options);
+    };
+  };
+
+}).call(this);
diff --git a/test/data/empty.js b/test/data/empty.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/data/regexp-props.js b/test/data/regexp-props.js
new file mode 100644
index 0000000..8806ad6
--- /dev/null
+++ b/test/data/regexp-props.js
@@ -0,0 +1,6 @@
+// Taken from underscore.js, version 1.4.2, line 1073.
+_.templateSettings = {
+    evaluate    : /<%([\s\S]+?)%>/g,
+    interpolate : /<%=([\s\S]+?)%>/g,
+    escape      : /<%-([\s\S]+?)%>/g // this line parsed oddly
+};
diff --git a/test/es6tests.js b/test/es6tests.js
new file mode 100644
index 0000000..12e2614
--- /dev/null
+++ b/test/es6tests.js
@@ -0,0 +1,294 @@
+var assert = require("assert");
+var parse = require("../lib/parser").parse;
+var Printer = require("../lib/printer").Printer;
+var types = require("../lib/types");
+var n = types.namedTypes;
+var b = types.builders;
+
+describe("ES6 Compatability", function() {
+    function convertShorthandMethod() {
+        var printer = new Printer({ tabWidth: 2 });
+
+        var code = [
+            "var name='test-name';",
+            "var shorthandObj = {",
+            "  name,",
+            "  func() { return 'value'; }",
+            "};"
+        ].join("\n");
+
+        var ast = parse(code);
+        n.VariableDeclaration.assert(ast.program.body[1]);
+
+        var shorthandObjDec = ast.program.body[1].declarations[0].init;
+        var methodDecProperty = shorthandObjDec.properties[1];
+        var newES5MethodProperty = b.property(
+            methodDecProperty.kind,
+            methodDecProperty.key,
+            methodDecProperty.value,
+            false,
+            false
+        );
+
+        var correctMethodProperty = b.property(
+            methodDecProperty.kind,
+            methodDecProperty.key,
+            b.functionExpression(
+                methodDecProperty.value.id,
+                methodDecProperty.value.params,
+                methodDecProperty.value.body,
+                methodDecProperty.value.generator,
+                methodDecProperty.value.expression
+            ),
+            false,
+            false
+        );
+
+        assert.strictEqual(
+            printer.print(newES5MethodProperty).code,
+            printer.print(correctMethodProperty).code
+        );
+    }
+
+    it("correctly converts from a shorthand method to ES5 function",
+       convertShorthandMethod);
+
+    function respectDestructuringAssignment() {
+        var printer = new Printer({ tabWidth: 2 });
+        var code = 'var {a} = {};';
+        var ast = parse(code);
+        n.VariableDeclaration.assert(ast.program.body[0]);
+        assert.strictEqual(printer.print(ast).code, code);
+    }
+
+    it("respects destructuring assignments",
+       respectDestructuringAssignment);
+});
+
+describe("import/export syntax", function() {
+    var printer = new Printer({ tabWidth: 2 });
+
+    function check(source) {
+        var ast1 = parse(source);
+        var ast2 = parse(printer.printGenerically(ast1).code);
+        types.astNodesAreEquivalent.assert(ast1, ast2);
+    }
+
+    it("should parse and print import statements correctly", function() {
+        check("import foo from 'foo'");
+
+        // default imports
+        check("import foo from 'foo';");
+        check("import {default as foo} from 'foo';");
+
+        // named imports
+        check("import {bar} from 'foo';");
+        check("import {bar, baz} from 'foo';");
+        check("import {bar as baz} from 'foo';");
+        check("import {bar as baz, xyz} from 'foo';");
+
+        // glob imports
+        check("import * as foo from 'foo';");
+
+        // mixing imports
+        check("import foo, {baz as xyz} from 'foo';");
+        check("import foo, * as bar from 'foo';");
+
+        // just import
+        check("import 'foo';");
+    });
+
+    it("should parse and print export statements correctly", function() {
+        // default exports
+        check("export default 42;");
+        check("export default {};");
+        check("export default [];");
+        check("export default foo;");
+        check("export default function () {};");
+        check("export default class {};");
+        check("export default function foo () {};");
+        check("export default class foo {};");
+
+        // variables exports
+        check("export var foo = 1;");
+        check("export var foo = function () {};");
+        check("export var bar;"); // lazy initialization
+        check("export let foo = 2;");
+        check("export let bar;"); // lazy initialization
+        check("export const foo = 3;");
+        check("export function foo () {};");
+        check("export class foo {};");
+
+        // named exports
+        check("export {foo};");
+        check("export {foo, bar};");
+        check("export {foo as bar};");
+        check("export {foo as default};");
+        check("export {foo as default, bar};");
+
+        // exports from
+        check("export * from 'foo';");
+        check("export {foo} from 'foo';");
+        check("export {foo, bar} from 'foo';");
+        check("export {foo as bar} from 'foo';");
+        check("export {foo as default} from 'foo';");
+        check("export {foo as default, bar} from 'foo';");
+        check("export {default} from 'foo';");
+        check("export {default as foo} from 'foo';");
+    });
+
+    it("should forbid invalid import/export syntax", function() {
+        function checkInvalid(source, expectedMessage) {
+            try {
+                parse(source);
+                throw new Error("Parsing should have failed: " +
+                                JSON.stringify(source));
+            } catch (err) {
+                assert.strictEqual(err.message, "Line 1: " + expectedMessage);
+            }
+        }
+
+        // const variables must have an initializer
+        checkInvalid(
+            "export const bar;",
+            "Const must be initialized"
+        );
+
+        // Unexpected token identifier, invalid named export syntax
+        checkInvalid(
+            "export foo;",
+            "Unexpected identifier"
+        );
+
+        // Unexpected token (, use a function declaration instead
+        checkInvalid(
+            "export function () {}",
+            "Unexpected token ("
+        );
+
+        // Unexpected token default
+        checkInvalid(
+            "export function default () {}",
+            "Unexpected token default"
+        );
+
+        // Missing from after import
+        checkInvalid(
+            "import foo;",
+            "Unexpected token ;"
+        );
+
+        // Missing from after import
+        checkInvalid(
+            "import { foo, bar };",
+            "Unexpected token ;"
+        );
+
+        // Invalid module specifier
+        checkInvalid(
+            "import foo from bar;",
+            "Invalid module specifier"
+        );
+
+        // Unexpected token default
+        checkInvalid(
+            "import default from 'foo';",
+            "Unexpected token default"
+        );
+
+        // Unexpected token from
+        checkInvalid(
+            "export default from 'foo';",
+            "Unexpected token from"
+        );
+
+        // Missing from after export
+        checkInvalid(
+            "export {default};",
+            "Unexpected token ;"
+        );
+
+        // Missing from after export
+        checkInvalid(
+            "export *;",
+            "Unexpected token ;"
+        );
+
+        // Missing from after import
+        checkInvalid(
+            "import {default as foo};",
+            "Unexpected token ;"
+        );
+
+        // Missing as after import *
+        checkInvalid(
+            "import * from 'foo';",
+            "Missing as after import *"
+        );
+
+        // Unexpected token =
+        checkInvalid(
+            "export default = 42;",
+            "Unexpected token ="
+        );
+
+        // Unexpected token default
+        checkInvalid(
+            "import {bar as default} from 'foo';",
+            "Unexpected token default"
+        );
+
+        // Unexpected token ,
+        checkInvalid(
+            "import foo, * as bar, {baz as xyz} from 'foo';",
+            "Unexpected token ,"
+        );
+
+        // Unexpected token ,
+        checkInvalid(
+            "import {bar}, foo from 'foo';",
+            "Unexpected token ,"
+        );
+
+        // Unexpected token ,
+        checkInvalid(
+            "import {bar}, * as foo from 'foo';",
+            "Unexpected token ,"
+        );
+
+        // Unexpected token ,
+        checkInvalid(
+            "import foo, {bar}, foo from 'foo';",
+            "Unexpected token ,"
+        );
+
+        // Unexpected token ,
+        checkInvalid(
+            "import {bar}, {foo} from 'foo';",
+            "Unexpected token ,"
+        );
+
+        // Unexpected token ,
+        checkInvalid(
+            "import * as bar, {baz as xyz} from 'foo';",
+            "Unexpected token ,"
+        );
+    });
+
+    it("should pretty-print template strings with backticks", function() {
+        var code = [
+            'var noun = "fool";',
+            'var s = `I am a ${noun}`;',
+            'var t = tag`You said: ${s}!`;'
+        ].join("\n");
+
+        var ast = parse(code);
+
+        assert.strictEqual(
+            new Printer({
+                tabWidth: 2
+            }).printGenerically(ast).code,
+            code
+        );
+    });
+});
diff --git a/test/identity.js b/test/identity.js
new file mode 100644
index 0000000..ebe6fb9
--- /dev/null
+++ b/test/identity.js
@@ -0,0 +1,33 @@
+var assert = require("assert");
+var fs = require("fs");
+var path = require("path");
+var types = require("../lib/types");
+var main = require("../main");
+
+function testFile(path) {
+    fs.readFile(path, "utf-8", function(err, source) {
+        assert.equal(err, null);
+        assert.strictEqual(typeof source, "string");
+
+        var ast = main.parse(source);
+        types.astNodesAreEquivalent.assert(ast.original, ast);
+        var code = main.print(ast).code;
+        assert.strictEqual(source, code);
+    });
+}
+
+function addTest(name) {
+    it(name, function() {
+        testFile(path.join(__dirname, "..", name + ".js"));
+    });
+}
+
+describe("identity", function() {
+    // Add more tests here as need be.
+    addTest("test/data/regexp-props");
+    addTest("test/data/empty");
+    addTest("test/data/backbone");
+    addTest("test/lines");
+    addTest("lib/lines");
+    addTest("lib/printer");
+});
diff --git a/test/jsx.js b/test/jsx.js
new file mode 100644
index 0000000..7205b9e
--- /dev/null
+++ b/test/jsx.js
@@ -0,0 +1,47 @@
+var parse = require("../lib/parser").parse;
+var Printer = require("../lib/printer").Printer;
+var types = require("../lib/types");
+
+describe("JSX Compatability", function() {
+    var printer = new Printer({ tabWidth: 2 });
+
+    function check(source) {
+        var ast1 = parse(source);
+        var ast2 = parse(printer.printGenerically(ast1).code);
+        types.astNodesAreEquivalent.assert(ast1, ast2);
+    }
+
+    it("should parse and print attribute comments", function() {
+        check("<b /* comment */ />");
+        check("<b /* multi\nline\ncomment */ />");
+    });
+
+    it("should parse and print child comments", function() {
+        check("<b>{/* comment */}</b>");
+        check("<b>{/* multi\nline\ncomment */}</b>");
+    });
+
+    it("should parse and print literal attributes", function() {
+        check("<b className=\"hello\" />");
+    });
+
+    it("should parse and print expression attributes", function() {
+        check("<b className={classes} />");
+    });
+
+    it("should parse and print chidren", function() {
+        check("<label><input /></label>");
+    });
+
+    it("should parse and print literal chidren", function() {
+        check("<b>hello world</b>");
+    });
+
+    it("should parse and print expression children", function() {
+        check("<b>{this.props.user.name}</b>");
+    });
+
+    it("should parse and print namespaced elements", function() {
+        check("<Foo.Bar />");
+    });
+});
diff --git a/test/lines.js b/test/lines.js
new file mode 100644
index 0000000..3087b8d
--- /dev/null
+++ b/test/lines.js
@@ -0,0 +1,559 @@
+var assert = require("assert");
+var fs = require("fs");
+var path = require("path");
+var linesModule = require("../lib/lines");
+var fromString = linesModule.fromString;
+var concat = linesModule.concat;
+
+function check(a, b) {
+    assert.strictEqual(a.toString(), b.toString());
+}
+
+describe("lines", function() {
+
+    describe('line terminators', function() {
+        var source = [
+            'foo;',
+            'bar;',
+        ];
+
+        var terminators = [
+            '\u000A',
+            '\u000D',
+            '\u2028',
+            '\u2029',
+            '\u000D\u000A',
+        ];
+        terminators.forEach(function(t) {
+            it('can handle ' + escape(t) + ' as line terminator', function() {
+                var lines = fromString(source.join(t));
+                assert.strictEqual(lines.length, 2);
+                assert.strictEqual(lines.getLineLength(1), 4);
+            });
+        });
+    });
+
+
+    it("FromString", function() {
+        function checkIsCached(s) {
+            assert.strictEqual(fromString(s), fromString(s));
+            check(fromString(s), s);
+        }
+
+        checkIsCached("");
+        checkIsCached(",");
+        checkIsCached("\n");
+        checkIsCached("this");
+        checkIsCached(", ");
+        checkIsCached(": ");
+
+        var longer = "This is a somewhat longer string that we do not want to cache.";
+        assert.notStrictEqual(
+            fromString(longer),
+            fromString(longer));
+
+        // Since Lines objects are immutable, if one is passed to fromString,
+        // we can return it as-is without having to make a defensive copy.
+        var longerLines = fromString(longer);
+        assert.strictEqual(fromString(longerLines), longerLines);
+    });
+
+    it("ToString", function() {
+        var code = arguments.callee + "",
+            lines = fromString(code);
+
+        check(lines, code);
+        check(lines.indentTail(5)
+                   .indentTail(-7)
+                   .indentTail(2),
+              code);
+    });
+
+    function testEachPosHelper(lines, code) {
+        var lengths = [];
+
+        check(lines, code);
+
+        var chars = [];
+        var emptyCount = 0;
+
+        function iterator(pos) {
+            var ch = lines.charAt(pos);
+            if (ch === "")
+                emptyCount += 1;
+            chars.push(ch);
+        }
+
+        lines.eachPos(iterator, null);
+
+        // The character at the position just past the end (as returned by
+        // lastPos) should be the only empty string.
+        assert.strictEqual(emptyCount, 1);
+
+        var joined = chars.join("");
+        assert.strictEqual(joined.length, code.length);
+        assert.strictEqual(joined, code);
+
+        var withoutSpaces = code.replace(/\s+/g, "");
+        chars.length = emptyCount = 0;
+        lines.eachPos(iterator, null, true); // Skip spaces this time.
+        assert.strictEqual(emptyCount, 0);
+        joined = chars.join("");
+        assert.strictEqual(joined.length, withoutSpaces.length);
+        assert.strictEqual(joined, withoutSpaces);
+    }
+
+    it("EachPos", function() {
+        // Function.prototype.toString uses \r\n line endings on non-*NIX
+        // systems, so normalize those to \n characters.
+        var code = (arguments.callee + "").replace(/\r\n/g, "\n");
+        var lines = fromString(code);
+
+        testEachPosHelper(lines, code);
+
+        lines = lines.indentTail(5);
+        testEachPosHelper(lines, lines.toString());
+
+        lines = lines.indentTail(-9);
+        testEachPosHelper(lines, lines.toString());
+
+        lines = lines.indentTail(4);
+        testEachPosHelper(lines, code);
+    });
+
+    it("CharAt", function() {
+        // Function.prototype.toString uses \r\n line endings on non-*NIX
+        // systems, so normalize those to \n characters.
+        var code = (arguments.callee + "").replace(/\r\n/g, "\n");
+        var lines = fromString(code);
+
+        function compare(pos) {
+            assert.strictEqual(
+                lines.charAt(pos),
+                lines.bootstrapCharAt(pos));
+        }
+
+        lines.eachPos(compare);
+
+        // Try a bunch of crazy positions to verify equivalence for
+        // out-of-bounds input positions.
+        fromString(exports.testBasic).eachPos(compare);
+
+        var original = fromString("  ab\n  c"),
+            indented = original.indentTail(4),
+            reference = fromString("  ab\n      c");
+
+        function compareIndented(pos) {
+            var c = indented.charAt(pos);
+            check(c, reference.charAt(pos));
+            check(c, indented.bootstrapCharAt(pos));
+            check(c, reference.bootstrapCharAt(pos));
+        }
+
+        indented.eachPos(compareIndented);
+
+        indented = indented.indentTail(-4);
+        reference = original;
+
+        indented.eachPos(compareIndented);
+    });
+
+    it("Concat", function() {
+        var strings = ["asdf", "zcxv", "qwer"],
+            lines = fromString(strings.join("\n")),
+            indented = lines.indentTail(4);
+
+        assert.strictEqual(lines.length, 3);
+
+        check(indented, strings.join("\n    "));
+
+        assert.strictEqual(5, concat([lines, indented]).length);
+        assert.strictEqual(5, concat([indented, lines]).length);
+
+        check(concat([lines, indented]),
+              lines.toString() + indented.toString());
+
+        check(concat([lines, indented]).indentTail(4),
+              strings.join("\n    ") +
+              strings.join("\n        "));
+
+        check(concat([indented, lines]),
+              strings.join("\n    ") + lines.toString());
+
+        check(concat([lines, indented]),
+              lines.concat(indented));
+
+        check(concat([indented, lines]),
+              indented.concat(lines));
+
+        check(concat([]), fromString(""));
+        assert.strictEqual(concat([]), fromString(""));
+
+        check(fromString(" ").join([
+            fromString("var"),
+            fromString("foo")
+        ]), fromString("var foo"));
+
+        check(fromString(" ").join(["var", "foo"]),
+              fromString("var foo"));
+
+        check(concat([
+            fromString("var"),
+            fromString(" "),
+            fromString("foo")
+        ]), fromString("var foo"));
+
+        check(concat(["var", " ", "foo"]),
+              fromString("var foo"));
+
+        check(concat([
+            fromString("debugger"), ";"
+        ]), fromString("debugger;"));
+    });
+
+    it("Empty", function() {
+        function c(s) {
+            var lines = fromString(s);
+            check(lines, s);
+            assert.strictEqual(
+                lines.isEmpty(),
+                s.length === 0);
+
+            assert.ok(lines.trimLeft().isEmpty());
+            assert.ok(lines.trimRight().isEmpty());
+            assert.ok(lines.trim().isEmpty());
+        }
+
+        c("");
+        c(" ");
+        c("    ");
+        c(" \n");
+        c("\n ");
+        c(" \n ");
+        c("\n \n ");
+        c(" \n\n ");
+        c(" \n \n ");
+        c(" \n \n\n");
+    });
+
+    it("SingleLine", function() {
+        var string = "asdf",
+            line = fromString(string);
+
+        check(line, string);
+        check(line.indentTail(4), string);
+        check(line.indentTail(-4), string);
+
+        // Single-line Lines objects are completely unchanged by indentTail.
+        assert.strictEqual(line.indentTail(10), line);
+
+        // Multi-line Lines objects are altered by indentTail, but only if the
+        // amount of the indentation is non-zero.
+        var twice = line.concat("\n", line);
+        assert.notStrictEqual(twice.indentTail(10), twice);
+        assert.strictEqual(twice.indentTail(0), twice);
+
+        check(line.concat(line), string + string);
+        check(line.indentTail(4).concat(line), string + string);
+        check(line.concat(line.indentTail(4)), string + string);
+        check(line.indentTail(8).concat(line.indentTail(4)), string + string);
+
+        line.eachPos(function(start) {
+            line.eachPos(function(end) {
+                check(line.slice(start, end),
+                      string.slice(start.column, end.column));
+            }, start);
+        });
+    });
+
+    it("Slice", function() {
+        var code = arguments.callee + "",
+            lines = fromString(code);
+        checkAllSlices(lines);
+    });
+
+    function checkAllSlices(lines) {
+        lines.eachPos(function(start) {
+            lines.eachPos(function(end) {
+                check(lines.slice(start, end),
+                      lines.bootstrapSlice(start, end));
+                check(lines.sliceString(start, end),
+                      lines.bootstrapSliceString(start, end));
+            }, start);
+        });
+    }
+
+    function getSourceLocation(lines) {
+        return { start: lines.firstPos(),
+                 end: lines.lastPos() };
+    }
+
+    it("GetSourceLocation", function() {
+        var code = arguments.callee + "",
+            lines = fromString(code);
+
+        function verify(indent) {
+            var indented = lines.indentTail(indent),
+                loc = getSourceLocation(indented),
+                string = indented.toString(),
+                strings = string.split("\n"),
+                lastLine = strings[strings.length - 1];
+
+            assert.strictEqual(loc.end.line, strings.length);
+            assert.strictEqual(loc.end.column, lastLine.length);
+
+            assert.deepEqual(loc, getSourceLocation(
+                indented.slice(loc.start, loc.end)));
+        }
+
+        verify(0);
+        verify(4);
+        verify(-4);
+    });
+
+    it("Trim", function() {
+        var string = "  xxx \n ";
+        var options = { tabWidth: 4 };
+        var lines = fromString(string);
+
+        function test(string) {
+            var lines = fromString(string, options);
+            check(lines.trimLeft(), fromString(string.replace(/^\s+/, ""), options));
+            check(lines.trimRight(), fromString(string.replace(/\s+$/, ""), options));
+            check(lines.trim(), fromString(string.replace(/^\s+|\s+$/g, ""), options));
+        }
+
+        test("");
+        test(" ");
+        test("  xxx \n ");
+        test("  xxx");
+        test("xxx  ");
+        test("\nx\nx\nx\n");
+        test("\t\nx\nx\nx\n\t\n");
+        test("xxx");
+    });
+
+    it("NoIndentEmptyLines", function() {
+        var lines = fromString("a\n\nb"),
+            indented = lines.indent(4),
+            tailIndented = lines.indentTail(5);
+
+        check(indented, fromString("    a\n\n    b"));
+        check(tailIndented, fromString("a\n\n     b"));
+
+        check(indented.indent(-4), lines);
+        check(tailIndented.indent(-5), lines);
+    });
+
+    it("CountSpaces", function() {
+        var count = linesModule.countSpaces;
+
+        assert.strictEqual(count(""), 0);
+        assert.strictEqual(count(" "), 1);
+        assert.strictEqual(count("  "), 2);
+        assert.strictEqual(count("   "), 3);
+
+        function check(s, tabWidth, result) {
+            assert.strictEqual(count(s, tabWidth), result);
+        }
+
+        check("", 2, 0);
+        check("", 3, 0);
+        check("", 4, 0);
+
+        check(" ", 2, 1);
+        check("\t", 2, 2);
+        check("\t\t", 2, 4);
+        check(" \t\t", 2, 4);
+        check(" \t \t", 2, 4);
+        check("  \t \t", 2, 6);
+        check("  \t  \t", 2, 8);
+        check(" \t   \t", 2, 6);
+        check("   \t \t", 2, 6);
+
+        check(" ", 3, 1);
+        check("\t", 3, 3);
+        check("\t\t", 3, 6);
+        check(" \t\t", 3, 6);
+        check(" \t \t", 3, 6);
+        check("  \t \t", 3, 6);
+        check("  \t  \t", 3, 6);
+        check(" \t   \t", 3, 9);
+        check("   \t \t", 3, 9);
+
+        check("\t\t\t   ", 2, 9);
+        check("\t\t\t   ", 3, 12);
+        check("\t\t\t   ", 4, 15);
+
+        check("\r", 4, 0);
+        check("\r ", 4, 1);
+        check(" \r ", 4, 2);
+        check(" \r\r ", 4, 2);
+    });
+
+    it("IndentWithTabs", function() {
+        var tabWidth = 4;
+        var tabOpts = { tabWidth: tabWidth, useTabs: true };
+        var noTabOpts = { tabWidth: tabWidth, useTabs: false };
+
+        var code = [
+            "function f() {",
+            "\treturn this;",
+            "}"
+        ].join("\n");
+
+        function checkUnchanged(lines, code) {
+            check(lines.toString(tabOpts), code);
+            check(lines.toString(noTabOpts), code);
+            check(lines.indent(3).indent(-5).indent(2).toString(tabOpts), code);
+            check(lines.indent(-3).indent(4).indent(-1).toString(noTabOpts), code);
+        }
+
+        var lines = fromString(code, tabOpts);
+        checkUnchanged(lines, code);
+
+        check(lines.indent(1).toString(tabOpts), [
+            " function f() {",
+            "\t return this;",
+            " }"
+        ].join("\n"));
+
+        check(lines.indent(tabWidth).toString(tabOpts), [
+            "\tfunction f() {",
+            "\t\treturn this;",
+            "\t}"
+        ].join("\n"));
+
+        check(lines.indent(1).toString(noTabOpts), [
+            " function f() {",
+            "     return this;",
+            " }"
+        ].join("\n"));
+
+        check(lines.indent(tabWidth).toString(noTabOpts), [
+            "    function f() {",
+            "        return this;",
+            "    }"
+        ].join("\n"));
+
+        var funkyCode = [
+            " function g() { \t ",
+            " \t\t  return this;  ",
+            "\t} "
+        ].join("\n");
+
+        var funky = fromString(funkyCode, tabOpts);
+        checkUnchanged(funky, funkyCode);
+
+        check(funky.indent(1).toString(tabOpts), [
+            "  function g() { \t ",
+            "\t\t   return this;  ",
+            "\t } "
+        ].join("\n"));
+
+        check(funky.indent(2).toString(tabOpts), [
+            "   function g() { \t ",
+            "\t\t\treturn this;  ",
+            "\t  } "
+        ].join("\n"));
+
+        check(funky.indent(1).toString(noTabOpts), [
+            "  function g() { \t ",
+            "           return this;  ",
+            "     } "
+        ].join("\n"));
+
+        check(funky.indent(2).toString(noTabOpts), [
+            "   function g() { \t ",
+            "            return this;  ",
+            "      } "
+        ].join("\n"));
+
+        // Test that '\v' characters are ignored for the purposes of indentation,
+        // but preserved when printing untouched lines.
+        code = [
+            "\vfunction f() {\v",
+            " \v   return \vthis;\v",
+            "\v} \v "
+        ].join("\n");
+
+        lines = fromString(code, tabOpts);
+
+        checkUnchanged(lines, code);
+
+        check(lines.indent(4).toString(noTabOpts), [
+            "    function f() {\v",
+            "        return \vthis;\v",
+            "    } \v "
+        ].join("\n"));
+
+        check(lines.indent(5).toString(tabOpts), [
+            "\t function f() {\v",
+            "\t\t return \vthis;\v",
+            "\t } \v "
+        ].join("\n"));
+    });
+
+    it("GuessTabWidth", function(done) {
+        var lines = fromString(arguments.callee + "");
+        assert.strictEqual(lines.guessTabWidth(), 4);
+
+        lines = fromString([
+            "function identity(x) {",
+            "  return x;",
+            "}"
+        ].join("\n"));
+        assert.strictEqual(lines.guessTabWidth(), 2);
+        assert.strictEqual(lines.indent(5).guessTabWidth(), 2);
+        assert.strictEqual(lines.indent(-4).guessTabWidth(), 2);
+
+        fs.readFile(__filename, "utf-8", function(err, source) {
+            assert.equal(err, null);
+            assert.strictEqual(fromString(source).guessTabWidth(), 4);
+
+            fs.readFile(path.join(
+                __dirname,
+                "..",
+                "package.json"
+            ), "utf-8", function(err, source) {
+                assert.equal(err, null);
+                assert.strictEqual(fromString(source).guessTabWidth(), 2);
+
+                done();
+            });
+        });
+    });
+
+    it("ExoticWhitespace", function() {
+        var source = "";
+        var spacePattern = /^\s+$/;
+
+        for (var i = 0; i < 0xffff; ++i) {
+            var ch = String.fromCharCode(i);
+            if (spacePattern.test(ch)) {
+                source += ch;
+            }
+        }
+
+        source += "x";
+
+        var options = { tabWidth: 4 };
+        var lines = fromString(source, options);
+
+        assert.strictEqual(lines.length, 5);
+        assert.strictEqual(lines.getLineLength(1), options.tabWidth);
+        assert.strictEqual(lines.getIndentAt(1), options.tabWidth);
+
+        assert.strictEqual(lines.slice({
+            line: 5,
+            column: lines.getLineLength(5) - 1
+        }).toString(options), "x");
+
+        assert.ok(spacePattern.test(
+            lines.slice(lines.firstPos(), {
+                line: 5,
+                column: lines.getLineLength(5) - 1
+            }).toString(options)
+        ));
+    });
+});
diff --git a/test/mapping.js b/test/mapping.js
new file mode 100644
index 0000000..a3a63c1
--- /dev/null
+++ b/test/mapping.js
@@ -0,0 +1,211 @@
+var assert = require("assert");
+var sourceMap = require("source-map");
+var recast = require("..");
+var types = require("../lib/types");
+var n = types.namedTypes;
+var b = types.builders;
+var NodePath = types.NodePath;
+var fromString = require("../lib/lines").fromString;
+var parse = require("../lib/parser").parse;
+var Printer = require("../lib/printer").Printer;
+var Mapping = require("../lib/mapping");
+
+describe("source maps", function() {
+    it("should generate correct mappings", function() {
+        var code = [
+            "function foo(bar) {",
+            "  return 1 + bar;",
+            "}"
+        ].join("\n");
+
+        var lines = fromString(code);
+        var ast = parse(code, {
+            sourceFileName: "source.js"
+        });
+
+        var path = new NodePath(ast);
+        var returnPath = path.get("program", "body", 0, "body", "body", 0);
+        n.ReturnStatement.assert(returnPath.value);
+
+        var leftPath = returnPath.get("argument", "left");
+        var leftValue = leftPath.value;
+        var rightPath = returnPath.get("argument", "right");
+
+        leftPath.replace(rightPath.value);
+        rightPath.replace(leftValue);
+
+        var sourceRoot = "path/to/source/root";
+        var printed = new Printer({
+            sourceMapName: "source.map.json",
+            sourceRoot: sourceRoot
+        }).print(ast);
+
+        assert.ok(printed.map);
+
+        assert.strictEqual(
+            printed.map.file,
+            "source.map.json"
+        );
+
+        assert.strictEqual(
+            printed.map.sourceRoot,
+            sourceRoot
+        );
+
+        var smc = new sourceMap.SourceMapConsumer(printed.map);
+
+        function check(origLine, origCol, genLine, genCol, lastColumn) {
+            assert.deepEqual(smc.originalPositionFor({
+                line: genLine,
+                column: genCol
+            }), {
+                source: sourceRoot + "/source.js",
+                line: origLine,
+                column: origCol,
+                name: null
+            });
+
+            assert.deepEqual(smc.generatedPositionFor({
+                source: sourceRoot + "/source.js",
+                line: origLine,
+                column: origCol
+            }), {
+                line: genLine,
+                column: genCol,
+                lastColumn: lastColumn
+            });
+        }
+
+        check(1, 0, 1, 0, null); // function
+        check(1, 18, 1, 18, null); // {
+        check(2, 2, 2, 2, null); // return
+        check(2, 13, 2, 9, null); // bar
+        check(2, 9, 2, 15, null); // 1
+        check(2, 16, 2, 16, null); // ;
+        check(3, 0, 3, 0, null); // }
+    });
+
+    it("should compose with inputSourceMap", function() {
+        function addUseStrict(ast) {
+            return recast.visit(ast, {
+                visitFunction: function(path) {
+                    path.get("body", "body").unshift(
+                        b.expressionStatement(b.literal("use strict"))
+                    );
+                    this.traverse(path);
+                }
+            });
+        }
+
+        function stripConsole(ast) {
+            return recast.visit(ast, {
+                visitCallExpression: function(path) {
+                    var node = path.value;
+                    if (n.MemberExpression.check(node.callee) &&
+                        n.Identifier.check(node.callee.object) &&
+                        node.callee.object.name === "console") {
+                        n.ExpressionStatement.assert(path.parent.node);
+                        path.parent.replace();
+                        return false;
+                    }
+                }
+            });
+        }
+
+        var code = [
+            "function add(a, b) {",
+            "  var sum = a + b;",
+            "  console.log(a, b);",
+            "  return sum;",
+            "}"
+        ].join("\n");
+
+        var ast = parse(code, {
+            sourceFileName: "original.js"
+        });
+
+        var useStrictResult = new Printer({
+            sourceMapName: "useStrict.map.json"
+        }).print(addUseStrict(ast));
+
+        var useStrictAst = parse(useStrictResult.code, {
+            sourceFileName: "useStrict.js"
+        });
+
+        var oneStepResult = new Printer({
+            sourceMapName: "oneStep.map.json"
+        }).print(stripConsole(ast));
+
+        var twoStepResult = new Printer({
+            sourceMapName: "twoStep.map.json",
+            inputSourceMap: useStrictResult.map
+        }).print(stripConsole(useStrictAst));
+
+        assert.strictEqual(
+            oneStepResult.code,
+            twoStepResult.code
+        );
+
+        var smc1 = new sourceMap.SourceMapConsumer(oneStepResult.map);
+        var smc2 = new sourceMap.SourceMapConsumer(twoStepResult.map);
+
+        smc1.eachMapping(function(mapping) {
+            var pos = {
+                line: mapping.generatedLine,
+                column: mapping.generatedColumn
+            };
+
+            var orig1 = smc1.originalPositionFor(pos);
+            var orig2 = smc2.originalPositionFor(pos);
+
+            // The composition of the source maps generated separately from
+            // the two transforms should be equivalent to the source map
+            // generated from the composition of the two transforms.
+            assert.deepEqual(orig1, orig2);
+
+            // Make sure the two-step source map refers back to the original
+            // source instead of the intermediate source.
+            assert.strictEqual(orig2.source, "original.js");
+        });
+    });
+
+    it("should work when a child node becomes null", function() {
+        // https://github.com/facebook/regenerator/issues/103
+        var code = [
+            "for (var i = 0; false; i++)",
+            "  log(i);"
+        ].join("\n");
+        var ast = parse(code);
+        var path = new NodePath(ast);
+
+        var updatePath = path.get("program", "body", 0, "update");
+        n.UpdateExpression.assert(updatePath.value);
+
+        updatePath.replace(null);
+
+        var printed = new Printer().print(ast);
+        assert.strictEqual(printed.code, [
+            "for (var i = 0; false; )",
+            "  log(i);"
+        ].join("\n"));
+    });
+
+    it("should tolerate programs that become empty", function() {
+        var source = "foo();";
+        var ast = recast.parse(source, {
+            sourceFileName: "foo.js"
+        });
+
+        assert.strictEqual(ast.program.body.length, 1);
+        ast.program.body.length = 0;
+
+        var result = recast.print(ast, {
+            sourceMapName: "foo.map.json"
+        });
+
+        assert.strictEqual(result.map.file, "foo.map.json");
+        assert.deepEqual(result.map.sources, []);
+        assert.deepEqual(result.map.names, []);
+        assert.strictEqual(result.map.mappings, "");
+    });
+});
diff --git a/test/parens.js b/test/parens.js
new file mode 100644
index 0000000..2c22722
--- /dev/null
+++ b/test/parens.js
@@ -0,0 +1,273 @@
+var assert = require("assert");
+var esprima = require("esprima-fb");
+var parse = require("../lib/parser").parse;
+var Printer = require("../lib/printer").Printer;
+var NodePath = require("ast-types").NodePath;
+var types = require("../lib/types");
+var n = types.namedTypes;
+var b = types.builders;
+var printer = new Printer;
+
+function parseExpression(expr) {
+    var ast = esprima.parse(expr);
+    n.Program.assert(ast);
+    ast = ast.body[0];
+    if (n.ExpressionStatement.check(ast))
+        return ast.expression;
+    return ast;
+}
+
+function check(expr) {
+    var ast1 = parseExpression(expr);
+    var printed = printer.printGenerically(ast1).code;
+    try {
+        var ast2 = parseExpression(printed);
+    } finally {
+        types.astNodesAreEquivalent.assert(ast1, ast2);
+    }
+}
+
+var operators = [
+    "==", "!=", "===", "!==",
+    "<", "<=", ">", ">=",
+    "<<", ">>", ">>>",
+    "+", "-", "*", "/", "%",
+    "&", // TODO Missing from the Parser API.
+    "|", "^", "in",
+    "instanceof",
+    "&&", "||"
+];
+
+describe("parens", function() {
+    it("Arithmetic", function() {
+        check("1 - 2");
+        check("  2 +2 ");
+
+        operators.forEach(function(op1) {
+            operators.forEach(function(op2) {
+                check("(a " + op1 + " b) " + op2 + " c");
+                check("a " + op1 + " (b " + op2 + " c)");
+            });
+        });
+    });
+
+    it("Unary", function() {
+        check("(-a).b");
+        check("(+a).b");
+        check("(!a).b");
+        check("(~a).b");
+        check("(typeof a).b");
+        check("(void a).b");
+        check("(delete a.b).c");
+    });
+
+    it("Binary", function() {
+        check("(a && b)()");
+        check("typeof (a && b)");
+        check("(a && b)[c]");
+        check("(a && b).c");
+    });
+
+    it("Sequence", function() {
+        check("(a, b)()");
+        check("a(b, (c, d), e)");
+        check("!(a, b)");
+        check("a + (b, c) + d");
+        check("var a = (1, 2), b = a + a;");
+        check("(a, { b: 2 }).b");
+        check("[a, (b, c), d]");
+        check("({ a: (1, 2) }).a");
+        check("(a, b) ? (a = 1, b = 2) : (c = 3)");
+        check("a = (1, 2)");
+    });
+
+    it("NewExpression", function() {
+        check("new (a.b())");
+        check("new (a.b())(c)");
+        check("new a.b(c)");
+        check("+new Date");
+        check("(new Date).getTime()");
+        check("new a");
+        check("(new a)(b)");
+        check("(new (a.b(c))(d))(e)");
+        check("(new Date)['getTime']()");
+        check('(new Date)["getTime"]()');
+    });
+
+    it("Numbers", function() {
+        check("(1).foo");
+        check("(-1).foo");
+        check("+0");
+        check("NaN.foo");
+        check("(-Infinity).foo");
+    });
+
+    it("Assign", function() {
+        check("!(a = false)");
+        check("a + (b = 2) + c");
+        check("(a = fn)()");
+        check("(a = b) ? c : d");
+        check("(a = b)[c]");
+        check("(a = b).c");
+    });
+
+    it("Function", function() {
+        check("a(function(){}.bind(this))");
+        check("(function(){}).apply(this, arguments)");
+        check("function f() { (function(){}).call(this) }");
+        check("while (true) { (function(){}).call(this) }");
+        check("() => ({a:1,b:2})");
+        check("(x, y={z:1}) => x + y.z");
+        check("a || ((x, y={z:1}) => x + y.z)");
+    });
+
+    it("ObjectLiteral", function() {
+        check("a({b:c(d)}.b)");
+        check("({a:b(c)}).a");
+    });
+
+    it("ReprintedParens", function() {
+        var code = "a(function g(){}.call(this));";
+        var ast1 = parse(code);
+        var body = ast1.program.body;
+
+        // Copy the function from a position where it does not need
+        // parentheses to a position where it does need parentheses.
+        body.push(b.expressionStatement(
+            body[0].expression.arguments[0]));
+
+        var generic = printer.printGenerically(ast1).code;
+        var ast2 = parse(generic);
+        types.astNodesAreEquivalent.assert(ast1, ast2);
+
+        var reprint = printer.print(ast1).code;
+        var ast3 = parse(reprint);
+        types.astNodesAreEquivalent.assert(ast1, ast3);
+
+        body.shift();
+        reprint = printer.print(ast1).code;
+        var ast4 = parse(reprint);
+        assert.strictEqual(ast4.program.body.length, 1);
+        var callExp = ast4.program.body[0].expression;
+        n.CallExpression.assert(callExp);
+        n.MemberExpression.assert(callExp.callee);
+        n.FunctionExpression.assert(callExp.callee.object);
+        types.astNodesAreEquivalent.assert(ast1, ast4);
+
+        var objCode = "({ foo: 42 }.foo);";
+        var objAst = parse(objCode);
+        var memExp = objAst.program.body[0].expression;
+        n.MemberExpression.assert(memExp);
+        n.ObjectExpression.assert(memExp.object);
+        n.Identifier.assert(memExp.property);
+        assert.strictEqual(memExp.property.name, "foo");
+        var blockStmt = b.blockStatement([b.expressionStatement(memExp)]);
+        reprint = printer.print(blockStmt).code;
+        types.astNodesAreEquivalent.assert(
+            blockStmt,
+            parse(reprint).program.body[0]
+        );
+    });
+
+    it("don't reparenthesize valid IIFEs", function() {
+        var iifeCode = "(function     spaces   () {        }.call()  )  ;";
+        var iifeAst = parse(iifeCode);
+        var iifeReprint = printer.print(iifeAst).code;
+        assert.strictEqual(iifeReprint, iifeCode);
+    });
+
+    it("don't reparenthesize valid object literals", function() {
+        var objCode = "(  {    foo   :  42}.  foo )  ;";
+        var objAst = parse(objCode);
+        var objReprint = printer.print(objAst).code;
+        assert.strictEqual(objReprint, objCode);
+    });
+
+    it("NegatedLoopCondition", function() {
+        var ast = parse([
+            "for (var i = 0; i < 10; ++i) {",
+            "  console.log(i);",
+            "}"
+        ].join("\n"))
+
+        var loop = ast.program.body[0];
+        var test = loop.test;
+        var negation = b.unaryExpression("!", test);
+
+        assert.strictEqual(
+            printer.print(negation).code,
+            "!(i < 10)"
+        );
+
+        loop.test = negation;
+
+        assert.strictEqual(printer.print(ast).code, [
+            "for (var i = 0; !(i < 10); ++i) {",
+            "  console.log(i);",
+            "}"
+        ].join("\n"));
+    });
+
+    it("MisleadingExistingParens", function() {
+        var ast = parse([
+            // The key === "oyez" expression appears to have parentheses
+            // already, but those parentheses won't help us when we negate the
+            // condition with a !.
+            'if (key === "oyez") {',
+            "  throw new Error(key);",
+            "}"
+        ].join("\n"));
+
+        var ifStmt = ast.program.body[0];
+        ifStmt.test = b.unaryExpression("!", ifStmt.test);
+
+        var binaryPath = new NodePath(ast).get(
+            "program", "body", 0, "test", "argument");
+
+        assert.ok(binaryPath.needsParens());
+
+        assert.strictEqual(printer.print(ifStmt).code, [
+            'if (!(key === "oyez")) {',
+            "  throw new Error(key);",
+            "}"
+        ].join("\n"));
+    });
+
+    it("DiscretionaryParens", function() {
+        var code = [
+            "if (info.line && (i > 0 || !skipFirstLine)) {",
+            "  info = copyLineInfo(info);",
+            "}"
+        ].join("\n");
+
+        var ast = parse(code);
+
+        var rightPath = new NodePath(ast).get(
+            "program", "body", 0, "test", "right");
+
+        assert.ok(rightPath.needsParens());
+        assert.strictEqual(printer.print(ast).code, code);
+    });
+
+    it("should not be added to multiline boolean expressions", function() {
+        var code = [
+            "function foo() {",
+            "  return !(",
+            "    a &&",
+            "    b &&",
+            "    c",
+            "  );",
+            "}"
+        ].join("\n");
+
+        var ast = parse(code);
+        var printer = new Printer({
+            tabWidth: 2
+        });
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            code
+        );
+    });
+});
diff --git a/test/parser.js b/test/parser.js
new file mode 100644
index 0000000..d1b2d75
--- /dev/null
+++ b/test/parser.js
@@ -0,0 +1,147 @@
+var assert = require("assert");
+var parse = require("../lib/parser").parse;
+var getReprinter = require("../lib/patcher").getReprinter;
+var Printer = require("../lib/printer").Printer;
+var printComments = require("../lib/comments").printComments;
+var linesModule = require("../lib/lines");
+var fromString = linesModule.fromString;
+var concat = linesModule.concat;
+var types = require("../lib/types");
+var namedTypes = types.namedTypes;
+var FastPath = require("../lib/fast-path");
+
+// Esprima seems unable to handle unnamed top-level functions, so declare
+// test functions with names and then export them later.
+
+describe("parser", function() {
+    it("Parser", function testParser(done) {
+        var code = testParser + "";
+        var ast = parse(code);
+
+        namedTypes.File.assert(ast);
+        assert.ok(getReprinter(FastPath.from(ast)));
+
+        var funDecl = ast.program.body[0],
+            funBody = funDecl.body;
+
+        namedTypes.FunctionDeclaration.assert(funDecl);
+        namedTypes.BlockStatement.assert(funBody);
+        assert.ok(getReprinter(FastPath.from(funBody)));
+
+        var lastStatement = funBody.body.pop(),
+            doneCall = lastStatement.expression;
+
+        assert.ok(!getReprinter(FastPath.from(funBody)));
+        assert.ok(getReprinter(FastPath.from(ast)));
+
+        funBody.body.push(lastStatement);
+        assert.ok(getReprinter(FastPath.from(funBody)));
+
+        assert.strictEqual(doneCall.callee.name, "done");
+
+        assert.strictEqual(lastStatement.comments.length, 2);
+
+        var firstComment = lastStatement.comments[0];
+        assert.strictEqual(firstComment.type, "Line");
+        assert.strictEqual(firstComment.leading, true);
+        assert.strictEqual(firstComment.trailing, false);
+        assert.strictEqual(
+            firstComment.value,
+            " Make sure done() remains the final statement in this function,"
+        );
+
+        var secondComment = lastStatement.comments[1];
+        assert.strictEqual(secondComment.type, "Line");
+        assert.strictEqual(secondComment.leading, true);
+        assert.strictEqual(secondComment.trailing, false);
+        assert.strictEqual(
+            secondComment.value,
+            " or the above assertions will probably fail."
+        );
+
+        // Make sure done() remains the final statement in this function,
+        // or the above assertions will probably fail.
+        done();
+    });
+
+    it("LocationFixer", function() {
+        var code = [
+            "function foo() {",
+            "    a()",
+            "    b()",
+            "}"
+        ].join("\n");
+        var ast = parse(code);
+        var printer = new Printer;
+
+        types.visit(ast, {
+            visitFunctionDeclaration: function(path) {
+                path.node.body.body.reverse();
+                this.traverse(path);
+            }
+        });
+
+        var altered = code
+            .replace("a()", "xxx")
+            .replace("b()", "a()")
+            .replace("xxx", "b()");
+
+        assert.strictEqual(altered, printer.print(ast).code);
+    });
+
+    it("TabHandling", function() {
+        function check(code, tabWidth) {
+            var lines = fromString(code, { tabWidth: tabWidth });
+            assert.strictEqual(lines.length, 1);
+
+            types.visit(parse(code, { tabWidth: tabWidth }), {
+                check: function(s, loc) {
+                    var sliced = lines.slice(loc.start, loc.end);
+                    assert.strictEqual(s + "", sliced.toString());
+                },
+
+                visitIdentifier: function(path) {
+                    var ident = path.node;
+                    this.check(ident.name, ident.loc);
+                    this.traverse(path);
+                },
+
+                visitLiteral: function(path) {
+                    var lit = path.node;
+                    this.check(lit.value, lit.loc);
+                    this.traverse(path);
+                }
+            });
+        }
+
+        for (var tabWidth = 1; tabWidth <= 8; ++tabWidth) {
+            check("\t\ti = 10;", tabWidth);
+            check("\t\ti \t= 10;", tabWidth);
+            check("\t\ti \t=\t 10;", tabWidth);
+            check("\t \ti \t=\t 10;", tabWidth);
+            check("\t \ti \t=\t 10;\t", tabWidth);
+            check("\t \ti \t=\t 10;\t ", tabWidth);
+        }
+    });
+
+    it("AlternateEsprima", function() {
+        var types = require("../lib/types");
+        var b = types.builders;
+        var esprima = {
+            parse: function(code) {
+                var program = b.program([
+                    b.expressionStatement(b.identifier("surprise"))
+                ]);
+                program.comments = [];
+                return program;
+            }
+        };
+        var ast = parse("ignored", { esprima: esprima });
+        var printer = new Printer;
+
+        types.namedTypes.File.assert(ast, true);
+        assert.strictEqual(
+            printer.printGenerically(ast).code,
+            "surprise;");
+    });
+});
diff --git a/test/patcher.js b/test/patcher.js
new file mode 100644
index 0000000..27a5e0b
--- /dev/null
+++ b/test/patcher.js
@@ -0,0 +1,194 @@
+var assert = require("assert");
+var recast = require("..");
+var types = require("../lib/types");
+var n = types.namedTypes;
+var b = types.builders;
+var patcherModule = require("../lib/patcher");
+var getReprinter = patcherModule.getReprinter;
+var Patcher = patcherModule.Patcher;
+var fromString = require("../lib/lines").fromString;
+var parse = require("../lib/parser").parse;
+var FastPath = require("../lib/fast-path");
+
+var code = [
+    "// file comment",
+    "exports.foo({",
+    "    // some comment",
+    "    bar: 42,",
+    "    baz: this",
+    "});"
+];
+
+function loc(sl, sc, el, ec) {
+    return {
+        start: { line: sl, column: sc },
+        end: { line: el, column: ec }
+    };
+}
+
+describe("patcher", function() {
+    it("Patcher", function() {
+        var lines = fromString(code.join("\n")),
+            patcher = new Patcher(lines),
+            selfLoc = loc(5, 9, 5, 13);
+
+        assert.strictEqual(patcher.get(selfLoc).toString(), "this");
+
+        patcher.replace(selfLoc, "self");
+
+        assert.strictEqual(patcher.get(selfLoc).toString(), "self");
+
+        var got = patcher.get().toString();
+        assert.strictEqual(got, code.join("\n").replace("this", "self"));
+
+        // Make sure comments are preserved.
+        assert.ok(got.indexOf("// some") >= 0);
+
+        var oyezLoc = loc(2, 12, 6, 1),
+            beforeOyez = patcher.get(oyezLoc).toString();
+        assert.strictEqual(beforeOyez.indexOf("exports"), -1);
+        assert.ok(beforeOyez.indexOf("comment") >= 0);
+
+        patcher.replace(oyezLoc, "oyez");
+
+        assert.strictEqual(patcher.get().toString(), [
+            "// file comment",
+            "exports.foo(oyez);"
+        ].join("\n"));
+
+        // "Reset" the patcher.
+        patcher = new Patcher(lines);
+        patcher.replace(oyezLoc, "oyez");
+        patcher.replace(selfLoc, "self");
+
+        assert.strictEqual(patcher.get().toString(), [
+            "// file comment",
+            "exports.foo(oyez);"
+        ].join("\n"));
+    });
+
+    var trickyCode = [
+        "    function",
+        "      foo(bar,",
+        "  baz) {",
+        "        qux();",
+        "    }"
+    ].join("\n");
+
+    it("GetIndent", function() {
+        function check(indent) {
+            var lines = fromString(trickyCode).indent(indent);
+            var file = parse(lines.toString());
+            var reprinter = FastPath.from(file).call(function(bodyPath) {
+                return getReprinter(bodyPath);
+            }, "program", "body", 0, "body");
+
+            var reprintedLines = reprinter(function(path) {
+                assert.ok(false, "should not have called print function");
+            });
+
+            assert.strictEqual(reprintedLines.length, 3);
+            assert.strictEqual(reprintedLines.getIndentAt(1), 0);
+            assert.strictEqual(reprintedLines.getIndentAt(2), 4);
+            assert.strictEqual(reprintedLines.getIndentAt(3), 0);
+            assert.strictEqual(reprintedLines.toString(), [
+                "{",
+                "    qux();",
+                "}"
+            ].join("\n"));
+        }
+
+        for (var indent = -4; indent <= 4; ++indent) {
+            check(indent);
+        }
+    });
+
+    it("should patch return/throw/etc. arguments correctly", function() {
+        var strAST = parse('return"foo"');
+        var returnStmt = strAST.program.body[0];
+        n.ReturnStatement.assert(returnStmt);
+        assert.strictEqual(
+            recast.print(strAST).code,
+            'return"foo"'
+        );
+
+        returnStmt.argument = b.literal(null);
+        assert.strictEqual(
+            recast.print(strAST).code,
+            "return null" // Instead of returnnull.
+        );
+
+        var arrAST = parse("throw[1,2,3]");
+        var throwStmt = arrAST.program.body[0];
+        n.ThrowStatement.assert(throwStmt);
+        assert.strictEqual(
+            recast.print(arrAST).code,
+            "throw[1,2,3]"
+        );
+
+        throwStmt.argument = b.literal(false);
+        assert.strictEqual(
+            recast.print(arrAST).code,
+            "throw false" // Instead of throwfalse.
+        );
+
+        var inAST = parse('"foo"in bar');
+        var inExpr = inAST.program.body[0].expression;
+
+        n.BinaryExpression.assert(inExpr);
+        assert.strictEqual(inExpr.operator, "in");
+
+        n.Literal.assert(inExpr.left);
+        assert.strictEqual(inExpr.left.value, "foo");
+
+        assert.strictEqual(
+            recast.print(inAST).code,
+            '"foo"in bar'
+        );
+
+        inExpr.left = b.identifier("x");
+        assert.strictEqual(
+            recast.print(inAST).code,
+            "x in bar" // Instead of xin bar.
+        );
+    });
+
+    it("should not add spaces to the beginnings of lines", function() {
+        var twoLineCode = [
+            "return",      // Because of ASI rules, these two lines will
+            '"use strict"' // parse as separate statements.
+        ].join("\n");
+
+        var twoLineAST = parse(twoLineCode);
+
+        assert.strictEqual(twoLineAST.program.body.length, 2);
+        var useStrict = twoLineAST.program.body[1];
+        n.ExpressionStatement.assert(useStrict);
+        n.Literal.assert(useStrict.expression);
+        assert.strictEqual(useStrict.expression.value, "use strict");
+
+        assert.strictEqual(
+            recast.print(twoLineAST).code,
+            twoLineCode
+        );
+
+        useStrict.expression = b.identifier("sloppy");
+
+        var withSloppyIdentifier = recast.print(twoLineAST).code;
+        assert.strictEqual(withSloppyIdentifier, [
+            "return",
+            "sloppy" // The key is that no space should be added to the
+                     // beginning of this line.
+        ].join("\n"));
+
+        twoLineAST.program.body[1] = b.expressionStatement(
+            b.callExpression(b.identifier("foo"), [])
+        );
+
+        var withFooCall = recast.print(twoLineAST).code;
+        assert.strictEqual(withFooCall, [
+            "return",
+            "foo()"
+        ].join("\n"));
+    });
+});
diff --git a/test/perf.js b/test/perf.js
new file mode 100644
index 0000000..00554e6
--- /dev/null
+++ b/test/perf.js
@@ -0,0 +1,32 @@
+var path = require("path");
+var fs = require("fs");
+var recast = require("..");
+
+var source = fs.readFileSync(
+    path.join(__dirname, "data", "backbone.js"),
+    "utf8"
+);
+
+var start = +new Date;
+var ast = recast.parse(source);
+var types = Object.create(null);
+
+var parseTime = new Date - start;
+console.log("parse", parseTime, "ms");
+
+recast.visit(ast, {
+    visitNode: function(path) {
+        types[path.value.type] = true;
+        this.traverse(path);
+    }
+});
+
+var visitTime = new Date - start - parseTime;
+console.log("visit", visitTime, "ms");
+
+recast.prettyPrint(ast).code;
+
+var printTime = new Date - start - visitTime - parseTime;
+console.log("print", printTime, "ms");
+
+console.log("total", new Date - start, "ms");
diff --git a/test/printer.js b/test/printer.js
new file mode 100644
index 0000000..fa4ad8b
--- /dev/null
+++ b/test/printer.js
@@ -0,0 +1,1129 @@
+var assert = require("assert");
+var recast = require("..");
+var parse = require("../lib/parser").parse;
+var Printer = require("../lib/printer").Printer;
+var n = require("../lib/types").namedTypes;
+var b = require("../lib/types").builders;
+var fromString = require("../lib/lines").fromString;
+
+describe("printer", function() {
+    it("Printer", function testPrinter(done) {
+        var code = testPrinter + "";
+        var ast = parse(code);
+        var printer = new Printer;
+
+        assert.strictEqual(typeof printer.print, "function");
+        assert.strictEqual(printer.print(null).code, "");
+
+        var string = printer.printGenerically(ast).code;
+        assert.ok(string.indexOf("done();") > 0);
+
+        string = printer.print(ast).code;
+
+        // TODO
+
+        assert.ok(string.indexOf("// TODO") > 0);
+
+        done();
+    });
+
+    var uselessSemicolons = [
+        'function a() {',
+        '  return "a";',
+        '};',
+        '',
+        'function b() {',
+        '  return "b";',
+        '};'
+    ].join("\n");
+
+    it("EmptyStatements", function() {
+        var ast = parse(uselessSemicolons);
+        var printer = new Printer({ tabWidth: 2 });
+
+        var reprinted = printer.print(ast).code;
+        assert.strictEqual(typeof reprinted, "string");
+        assert.strictEqual(reprinted, uselessSemicolons);
+
+        var generic = printer.printGenerically(ast).code;
+        var withoutTrailingSemicolons = uselessSemicolons.replace(/\};/g, "}");
+        assert.strictEqual(typeof generic, "string");
+        assert.strictEqual(generic, withoutTrailingSemicolons);
+    });
+
+    var importantSemicolons = [
+        "var a = {};", // <--- this trailing semi-colon is very important
+        "(function() {})();"
+    ].join("\n");
+
+    it("IffeAfterVariableDeclarationEndingInObjectLiteral", function() {
+        var ast = parse(importantSemicolons);
+        var printer = new Printer({ tabWidth: 2 });
+
+        var reprinted = printer.printGenerically(ast).code;
+        assert.strictEqual(typeof reprinted, "string");
+        assert.strictEqual(reprinted, importantSemicolons);
+    });
+
+    var arrayExprWithTrailingComma = '[1, 2,];';
+    var arrayExprWithoutTrailingComma = '[1, 2];';
+
+    it("ArrayExpressionWithTrailingComma", function() {
+        var ast = parse(arrayExprWithTrailingComma);
+        var printer = new Printer({ tabWidth: 2 });
+
+        var body = ast.program.body;
+        var arrayExpr = body[0].expression;
+        n.ArrayExpression.assert(arrayExpr);
+
+        // This causes the array expression to be reprinted.
+        var arrayExprOrig = arrayExpr.original;
+        arrayExpr.original = null;
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            arrayExprWithoutTrailingComma
+        );
+
+        arrayExpr.original = arrayExprOrig;
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            arrayExprWithTrailingComma
+        );
+    });
+
+    var arrayExprWithHoles = '[,,];';
+
+    it("ArrayExpressionWithHoles", function() {
+        var ast = parse(arrayExprWithHoles);
+        var printer = new Printer({ tabWidth: 2 });
+
+        var body = ast.program.body;
+        var arrayExpr = body[0].expression;
+        n.ArrayExpression.assert(arrayExpr);
+
+        // This causes the array expression to be reprinted.
+        var arrayExprOrig = arrayExpr.original;
+        arrayExpr.original = null;
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            arrayExprWithHoles
+        );
+
+        arrayExpr.original = arrayExprOrig;
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            arrayExprWithHoles
+        );
+    });
+
+    var objectExprWithTrailingComma = '({x: 1, y: 2,});';
+    var objectExprWithoutTrailingComma = '({\n  x: 1,\n  y: 2\n});';
+
+    it("ArrayExpressionWithTrailingComma", function() {
+        var ast = parse(objectExprWithTrailingComma);
+        var printer = new Printer({ tabWidth: 2 });
+
+        var body = ast.program.body;
+        var objectExpr = body[0].expression;
+        n.ObjectExpression.assert(objectExpr);
+
+        // This causes the array expression to be reprinted.
+        var objectExprOrig = objectExpr.original;
+        objectExpr.original = null;
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            objectExprWithoutTrailingComma
+        );
+
+        objectExpr.original = objectExprOrig;
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            objectExprWithTrailingComma
+        );
+    });
+
+    var switchCase = [
+        "switch (test) {",
+        "  default:",
+        "  case a: break",
+        "",
+        "  case b:",
+        "    break;",
+        "}",
+    ].join("\n");
+
+    var switchCaseReprinted = [
+        "if (test) {",
+        "  switch (test) {",
+        "  default:",
+        "  case a: break",
+        "  case b:",
+        "    break;",
+        "  }",
+        "}"
+    ].join("\n");
+
+    var switchCaseGeneric = [
+        "if (test) {",
+        "  switch (test) {",
+        "  default:",
+        "  case a:",
+        "    break;",
+        "  case b:",
+        "    break;",
+        "  }",
+        "}"
+    ].join("\n");
+
+    it("SwitchCase", function() {
+        var ast = parse(switchCase);
+        var printer = new Printer({ tabWidth: 2 });
+
+        var body = ast.program.body;
+        var switchStmt = body[0];
+        n.SwitchStatement.assert(switchStmt);
+
+        // This causes the switch statement to be reprinted.
+        switchStmt.original = null;
+
+        body[0] = b.ifStatement(
+            b.identifier("test"),
+            b.blockStatement([
+                switchStmt
+            ])
+        );
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            switchCaseReprinted
+        );
+
+        assert.strictEqual(
+            printer.printGenerically(ast).code,
+            switchCaseGeneric
+        );
+    });
+
+    var tryCatch = [
+        "try {",
+        "  a();",
+        "} catch (e) {",
+        "  b(e);",
+        "}"
+    ].join("\n");
+
+    it("IndentTryCatch", function() {
+        var ast = parse(tryCatch);
+        var printer = new Printer({ tabWidth: 2 });
+        var body = ast.program.body;
+        var tryStmt = body[0];
+        n.TryStatement.assert(tryStmt);
+
+        // Force reprinting.
+        assert.strictEqual(printer.printGenerically(ast).code, tryCatch);
+    });
+
+    var classBody = [
+        "class A {",
+        "  foo(x) { return x }",
+        "  bar(y) { this.foo(y); }",
+        "  baz(x, y) {",
+        "    this.foo(x);",
+        "    this.bar(y);",
+        "  }",
+        "}"
+    ];
+
+    var classBodyExpected = [
+        "class A {",
+        "  foo(x) { return x }",
+        "  bar(y) { this.foo(y); }",
+        "  baz(x, y) {",
+        "    this.foo(x);",
+        "    this.bar(y);",
+        "  }",
+        "  foo(x) { return x }",
+        "}"
+    ];
+
+    it("MethodPrinting", function() {
+        var code = classBody.join("\n");
+        try {
+            var ast = parse(code);
+        } catch (e) {
+            // ES6 not supported, silently finish
+            return;
+        }
+        var printer = new Printer({ tabWidth: 2 });
+        var cb = ast.program.body[0].body;
+        n.ClassBody.assert(cb);
+
+        // Trigger reprinting of the class body.
+        cb.body.push(cb.body[0]);
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            classBodyExpected.join("\n")
+        );
+    });
+
+    var multiLineParams = [
+        "function f(/* first",
+        "              xxx",
+        "              param */ a,",
+        "  // other params",
+        "  b, c, // see?",
+        "  d) {",
+        "  return a + b + c + d;",
+        "}"
+    ];
+
+    var multiLineParamsExpected = [
+        "function f(",
+        "  /* first",
+        "     xxx",
+        "     param */ a,",
+        "  // other params",
+        "  b,",
+        "  // see?",
+        "  c,",
+        "  d) {",
+        "  return a + b + c + d;",
+        "}"
+    ];
+
+    it("MultiLineParams", function() {
+        var code = multiLineParams.join("\n");
+        var ast = parse(code);
+        var printer = new Printer({ tabWidth: 2 });
+
+        recast.visit(ast, {
+            visitNode: function(path) {
+                path.value.original = null;
+                this.traverse(path);
+            }
+        });
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            multiLineParamsExpected.join("\n")
+        );
+    });
+
+    it("SimpleVarPrinting", function() {
+        var printer = new Printer({ tabWidth: 2 });
+        var varDecl = b.variableDeclaration("var", [
+            b.variableDeclarator(b.identifier("x"), null),
+            b.variableDeclarator(b.identifier("y"), null),
+            b.variableDeclarator(b.identifier("z"), null)
+        ]);
+
+        assert.strictEqual(
+            printer.print(b.program([varDecl])).code,
+            "var x, y, z;"
+        );
+
+        var z = varDecl.declarations.pop();
+        varDecl.declarations.pop();
+        varDecl.declarations.push(z);
+
+        assert.strictEqual(
+            printer.print(b.program([varDecl])).code,
+            "var x, z;"
+        );
+    });
+
+    it("MultiLineVarPrinting", function() {
+        var printer = new Printer({ tabWidth: 2 });
+        var varDecl = b.variableDeclaration("var", [
+            b.variableDeclarator(b.identifier("x"), null),
+            b.variableDeclarator(
+                b.identifier("y"),
+                b.objectExpression([
+                    b.property("init", b.identifier("why"), b.literal("not"))
+                ])
+            ),
+            b.variableDeclarator(b.identifier("z"), null)
+        ]);
+
+        assert.strictEqual(printer.print(b.program([varDecl])).code, [
+            "var x,",
+            "    y = {",
+            "      why: \"not\"",
+            "    },",
+            "    z;"
+        ].join("\n"));
+    });
+
+    it("ForLoopPrinting", function() {
+        var printer = new Printer({ tabWidth: 2 });
+        var loop = b.forStatement(
+            b.variableDeclaration("var", [
+                b.variableDeclarator(b.identifier("i"), b.literal(0))
+            ]),
+            b.binaryExpression("<", b.identifier("i"), b.literal(3)),
+            b.updateExpression("++", b.identifier("i"), /* prefix: */ false),
+            b.expressionStatement(
+                b.callExpression(b.identifier("log"), [b.identifier("i")])
+            )
+        );
+
+        assert.strictEqual(
+            printer.print(loop).code,
+            "for (var i = 0; i < 3; i++)\n" +
+            "  log(i);"
+        );
+    });
+
+    it("EmptyForLoopPrinting", function() {
+        var printer = new Printer({ tabWidth: 2 });
+        var loop = b.forStatement(
+            b.variableDeclaration("var", [
+                b.variableDeclarator(b.identifier("i"), b.literal(0))
+            ]),
+            b.binaryExpression("<", b.identifier("i"), b.literal(3)),
+            b.updateExpression("++", b.identifier("i"), /* prefix: */ false),
+            b.emptyStatement()
+        );
+
+        assert.strictEqual(
+            printer.print(loop).code,
+            "for (var i = 0; i < 3; i++)\n" +
+            "  ;"
+        );
+    });
+
+    it("ForInLoopPrinting", function() {
+        var printer = new Printer({ tabWidth: 2 });
+        var loop = b.forInStatement(
+            b.variableDeclaration("var", [
+                b.variableDeclarator(b.identifier("key"), null)
+            ]),
+            b.identifier("obj"),
+            b.expressionStatement(
+                b.callExpression(b.identifier("log"), [b.identifier("key")])
+            ),
+            /* each: */ false
+        );
+
+        assert.strictEqual(
+            printer.print(loop).code,
+            "for (var key in obj)\n" +
+            "  log(key);"
+        );
+    });
+
+    it("GuessTabWidth", function() {
+        var code = [
+            "function identity(x) {",
+            "  return x;",
+            "}"
+        ].join("\n");
+
+        var guessedTwo = [
+            "function identity(x) {",
+            "  log(x);",
+            "  return x;",
+            "}"
+        ].join("\n");
+
+        var explicitFour = [
+            "function identity(x) {",
+            "    log(x);",
+            "    return x;",
+            "}"
+        ].join("\n");
+
+        var ast = parse(code);
+
+        var funDecl = ast.program.body[0];
+        n.FunctionDeclaration.assert(funDecl);
+
+        var funBody = funDecl.body.body;
+
+        funBody.unshift(
+            b.expressionStatement(
+                b.callExpression(
+                    b.identifier("log"),
+                    funDecl.params
+                )
+            )
+        );
+
+        assert.strictEqual(
+            new Printer().print(ast).code,
+            guessedTwo
+        );
+
+        assert.strictEqual(
+            new Printer({
+                tabWidth: 4
+            }).print(ast).code,
+            explicitFour
+        );
+    });
+
+    it("FunctionDefaultsAndRest", function() {
+        var printer = new Printer();
+        var funExpr = b.functionExpression(
+            b.identifier('a'),
+            [b.identifier('b'), b.identifier('c')],
+            b.blockStatement([]),
+            false,
+            false,
+            false,
+            undefined
+        );
+
+        funExpr.defaults = [undefined, b.literal(1)];
+        funExpr.rest = b.identifier('d');
+
+        assert.strictEqual(
+            printer.print(funExpr).code,
+            "function a(b, c=1, ...d) {}"
+        );
+
+        var arrowFunExpr = b.arrowFunctionExpression(
+            [b.identifier('b'), b.identifier('c')],
+            b.blockStatement([]),
+            false,
+            false,
+            false,
+            undefined);
+
+        arrowFunExpr.defaults = [undefined, b.literal(1)];
+        arrowFunExpr.rest = b.identifier('d');
+
+        assert.strictEqual(
+            printer.print(arrowFunExpr).code,
+            "(b, c=1, ...d) => {}"
+        );
+    });
+
+    it("generically prints parsed code and generated code the same way", function() {
+        var printer = new Printer();
+        var ast = b.program([
+            b.expressionStatement(b.literal(1)),
+            b.expressionStatement(b.literal(2))
+        ]);
+
+        assert.strictEqual(
+            printer.printGenerically(parse("1; 2;")).code,
+            printer.printGenerically(ast).code
+        );
+    });
+
+    it("ExportDeclaration semicolons", function() {
+        var printer = new Printer();
+        var code = "export var foo = 42;";
+        var ast = parse(code);
+
+        assert.strictEqual(printer.print(ast).code, code);
+        assert.strictEqual(printer.printGenerically(ast).code, code);
+
+        code = "export var foo = 42";
+        ast = parse(code);
+
+        assert.strictEqual(printer.print(ast).code, code);
+        assert.strictEqual(printer.printGenerically(ast).code, code + ";");
+
+        code = "export function foo() {}";
+        ast = parse(code);
+
+        assert.strictEqual(printer.print(ast).code, code);
+        assert.strictEqual(printer.printGenerically(ast).code, code + ";");
+
+        code = "export function foo() {};";
+        ast = parse(code);
+
+        assert.strictEqual(printer.print(ast).code, code);
+        assert.strictEqual(printer.printGenerically(ast).code, code);
+    });
+
+    var stmtListSpaces = [
+        "",
+        "var x = 1;",
+        "",
+        "",
+        "// y summation",
+        "var y = x + 1;",
+        "var z = x + y;",
+        "// after z",
+        "",
+        "console.log(x, y, z);",
+        "",
+        ""
+    ].join("\n");
+
+    var stmtListSpacesExpected = [
+        "",
+        "debugger;",
+        "var x = 1;",
+        "",
+        "",
+        "// y summation",
+        "var y = x + 1;",
+        "debugger;",
+        "var z = x + y;",
+        "// after z",
+        "",
+        "console.log(x, y, z);",
+        "",
+        "debugger;",
+        "",
+        ""
+    ].join("\n");
+
+    it("Statement list whitespace reuse", function() {
+        var ast = parse(stmtListSpaces);
+        var printer = new Printer({ tabWidth: 2 });
+        var debugStmt = b.expressionStatement(b.identifier("debugger"));
+
+        ast.program.body.splice(2, 0, debugStmt);
+        ast.program.body.unshift(debugStmt);
+        ast.program.body.push(debugStmt);
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            stmtListSpacesExpected
+        );
+
+        var funDecl = b.functionDeclaration(
+            b.identifier("foo"),
+            [],
+            b.blockStatement(ast.program.body)
+        );
+
+        var linesModule = require("../lib/lines");
+
+        assert.strictEqual(
+            printer.print(funDecl).code,
+            linesModule.concat([
+                "function foo() {\n",
+                linesModule.fromString(
+                    stmtListSpacesExpected.replace(/^\s+|\s+$/g, "")
+                ).indent(2),
+                "\n}"
+            ]).toString()
+        );
+    });
+
+    it("should print static methods with the static keyword", function() {
+        var printer = new Printer({ tabWidth: 4 });
+        var ast = parse([
+            "class A {",
+            "  static foo() {}",
+            "}"
+        ].join("\n"));
+
+        var classBody = ast.program.body[0].body;
+        n.ClassBody.assert(classBody);
+
+        var foo = classBody.body[0];
+        n.MethodDefinition.assert(foo);
+
+        classBody.body.push(foo);
+
+        foo.key.name = "formerlyFoo";
+
+        assert.strictEqual(printer.print(ast).code, [
+            "class A {",
+            "    static formerlyFoo() {}",
+            "    static formerlyFoo() {}",
+            "}"
+        ].join("\n"));
+    });
+
+    it("should print string literals with the specified delimiter", function() {
+        var ast = parse([
+            "var obj = {",
+            "    \"foo's\": 'bar',",
+            "    '\"bar\\'s\"': /regex/m",
+            "};"
+        ].join("\n"));
+
+        var variableDeclaration = ast.program.body[0];
+        n.VariableDeclaration.assert(variableDeclaration);
+
+        var printer = new Printer({ quote: "single" });
+        assert.strictEqual(printer.printGenerically(ast).code, [
+            "var obj = {",
+            "    'foo\\'s': 'bar',",
+            "    '\"bar\\'s\"': /regex/m",
+            "};"
+        ].join("\n"));
+
+        var printer2 = new Printer({ quote: "double" });
+        assert.strictEqual(printer2.printGenerically(ast).code, [
+            "var obj = {",
+            "    \"foo's\": \"bar\",",
+            '    "\\"bar\'s\\"": /regex/m',
+            "};"
+        ].join("\n"));
+
+        var printer3 = new Printer({ quote: "auto" });
+        assert.strictEqual(printer3.printGenerically(ast).code, [
+            "var obj = {",
+            '    "foo\'s": "bar",',
+            '    \'"bar\\\'s"\': /regex/m',
+            "};"
+        ].join("\n"));
+    });
+
+    it("should print block comments at head of class once", function() {
+        // Given.
+        var ast = parse([
+            "/**",
+            " * This class was in an IIFE and returned an instance of itself.",
+            " */",
+            "function SimpleClass() {",
+            "};"
+        ].join("\n"));
+
+        var classIdentifier = b.identifier('SimpleClass');
+        var exportsExpression = b.memberExpression(b.identifier('module'), b.identifier('exports'), false);
+        var assignmentExpression = b.assignmentExpression('=', exportsExpression, classIdentifier);
+        var exportStatement = b.expressionStatement(assignmentExpression);
+
+        ast.program.body.push(exportStatement);
+
+        // When.
+        var printedClass = new Printer().print(ast).code;
+
+        // Then.
+        assert.strictEqual(printedClass, [
+            "/**",
+            " * This class was in an IIFE and returned an instance of itself.",
+            " */",
+            "function SimpleClass() {",
+            "}",
+            "module.exports = SimpleClass;"
+        ].join("\n"));
+    });
+
+    it("should support computed properties", function() {
+        var code = [
+            'class A {',
+            '  ["a"]() {}',
+            '  [ID("b")]() {}',
+            '  [0]() {}',
+            '  [ID(1)]() {}',
+            '  get ["a"]() {}',
+            '  get [ID("b")]() {}',
+            '  get [0]() {}',
+            '  get [ID(1)]() {}',
+            '  set ["a"](x) {}',
+            '  set [ID("b")](x) {}',
+            '  set [0](x) {}',
+            '  set [ID(1)](x) {}',
+            '  static ["a"]() {}',
+            '  static [ID("b")]() {}',
+            '  static [0]() {}',
+            '  static [ID(1)]() {}',
+            '  static get ["a"]() {}',
+            '  static get [ID("b")]() {}',
+            '  static get [0]() {}',
+            '  static get [ID(1)]() {}',
+            '  static set ["a"](x) {}',
+            '  static set [ID("b")](x) {}',
+            '  static set [0](x) {}',
+            '  static set [ID(1)](x) {}',
+            '}'
+        ].join("\n");
+
+        var ast = parse(code);
+
+        var printer = new Printer({
+            tabWidth: 2
+        });
+
+        assert.strictEqual(
+            printer.printGenerically(ast).code,
+            code
+        );
+
+        var code = [
+            'var obj = {',
+            '  ["a"]: 1,',
+            '  [ID("b")]: 2,',
+            '  [0]: 3,',
+            '  [ID(1)]: 4,',
+            '  ["a"]() {},',
+            '  [ID("b")]() {},',
+            '  [0]() {},',
+            '  [ID(1)]() {},',
+            '  get ["a"]() {},',
+            '  get [ID("b")]() {},',
+            '  get [0]() {},',
+            '  get [ID(1)]() {},',
+            '  set ["a"](x) {},',
+            '  set [ID("b")](x) {},',
+            '  set [0](x) {},',
+            '  set [ID(1)](x) {}',
+            '};'
+        ].join("\n");
+
+        ast = parse(code);
+
+        assert.strictEqual(
+            printer.printGenerically(ast).code,
+            code
+        );
+
+        ast = parse([
+            "var o = {",
+            "  // This foo will become a computed method name.",
+            "  foo() { return bar }",
+            "};"
+        ].join("\n"));
+
+        var objExpr = ast.program.body[0].declarations[0].init;
+        n.ObjectExpression.assert(objExpr);
+
+        assert.strictEqual(objExpr.properties[0].computed, false);
+        objExpr.properties[0].computed = true;
+        objExpr.properties[0].kind = "get";
+
+        assert.strictEqual(recast.print(ast).code, [
+            "var o = {",
+            "  // This foo will become a computed method name.",
+            "  get [foo]() { return bar }",
+            "};"
+        ].join("\n"));
+    });
+
+    it("prints trailing commas in object literals", function() {
+        var code = [
+            "({",
+            "  foo: bar,",
+            "  bar: foo,",
+            "});"
+        ].join("\n");
+
+        var ast = parse(code);
+
+        var printer = new Printer({
+            tabWidth: 2,
+            trailingComma: true,
+        });
+
+        var pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+    });
+
+    it("prints trailing commas in function calls", function() {
+        var code = [
+            "call(",
+            "  1,",
+            "  2,",
+            ");"
+        ].join("\n");
+
+        var ast = parse(code);
+
+        var printer = new Printer({
+            tabWidth: 2,
+            wrapColumn: 1,
+            trailingComma: true,
+        });
+
+        var pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+    });
+
+    it("prints trailing commas in array expressions", function() {
+        var code = [
+            "[",
+            "  1,",
+            "  2,",
+            "];"
+        ].join("\n");
+
+        var ast = parse(code);
+
+        var printer = new Printer({
+            tabWidth: 2,
+            wrapColumn: 1,
+            trailingComma: true,
+        });
+
+        var pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+    });
+
+    it("prints trailing commas in function definitions", function() {
+        var code = [
+            "function foo(",
+            "  a,",
+            "  b,",
+            ") {}"
+        ].join("\n");
+
+        var ast = parse(code);
+
+        var printer = new Printer({
+            tabWidth: 2,
+            wrapColumn: 1,
+            trailingComma: true,
+        });
+
+        var pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+    });
+
+    it("should support AssignmentPattern and RestElement", function() {
+        var code = [
+            "function foo(a, [b, c]=d(a), ...[e, f, ...rest]) {",
+            "  return [a, b, c, e, f, rest];",
+            "}"
+        ].join("\n");
+
+        var ast = parse(code);
+        var printer = new Printer({
+            tabWidth: 2
+        });
+
+        var pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+    });
+
+    it("adds parenthesis around spread patterns", function() {
+        var code = "(...rest) => rest;";
+
+        var ast = b.program([
+            b.expressionStatement(b.arrowFunctionExpression(
+                [b.spreadElementPattern(b.identifier('rest'))],
+                b.identifier('rest'),
+                false
+            ))
+        ]);
+
+        var printer = new Printer({
+            tabWidth: 2
+        });
+
+        var pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+
+        // Print RestElement the same way
+        ast = b.program([
+            b.expressionStatement(b.arrowFunctionExpression(
+                [b.restElement(b.identifier('rest'))],
+                b.identifier('rest'),
+                false
+            ))
+        ]);
+
+        pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+
+        // Do the same for the `rest` field.
+        var arrowFunction = b.arrowFunctionExpression(
+            [],
+            b.identifier('rest'),
+            false
+        );
+        arrowFunction.rest = b.identifier('rest');
+        ast = b.program([
+            b.expressionStatement(arrowFunction)
+        ]);
+
+        pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+    });
+
+    it("prints ClassProperty correctly", function() {
+        var code = [
+            "class A {",
+            "  foo: Type = Bar;",
+            "}",
+        ].join("\n");
+
+        var ast = b.program([
+            b.classDeclaration(
+                b.identifier('A'),
+                b.classBody([
+                    b.classProperty(
+                        b.identifier('foo'),
+                        b.identifier('Bar'),
+                        b.typeAnnotation(
+                            b.genericTypeAnnotation(b.identifier('Type'), null)
+                        )
+                    )
+                ])
+            )
+        ]);
+
+        var printer = new Printer({
+            tabWidth: 2
+        });
+
+        var pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+    });
+
+    it("prints static ClassProperty correctly", function() {
+        var code = [
+            "class A {",
+            "  static foo = Bar;",
+            "}",
+        ].join("\n");
+
+        var ast = b.program([
+            b.classDeclaration(
+                b.identifier('A'),
+                b.classBody([
+                    b.classProperty(
+                        b.identifier('foo'),
+                        b.identifier('Bar'),
+                        null,
+                        true
+                    )
+                ])
+            )
+        ]);
+
+        var printer = new Printer({
+            tabWidth: 2
+        });
+
+        var pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+    });
+
+    it("prints template expressions correctly", function() {
+        var code = [
+            "graphql`query`;",
+        ].join("\n");
+
+        var ast = b.program([
+            b.taggedTemplateStatement(
+                b.identifier('graphql'),
+                b.templateLiteral(
+                    [b.templateElement({cooked: 'query', raw: 'query'}, false)],
+                    []
+                )
+            )
+        ]);
+
+        var printer = new Printer({
+            tabWidth: 2
+        });
+
+        var pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+
+        code = [
+            "graphql`query${foo.getQuery()}field${bar}`;",
+        ].join("\n");
+
+        ast = b.program([
+            b.taggedTemplateStatement(
+                b.identifier('graphql'),
+                b.templateLiteral(
+                    [
+                        b.templateElement(
+                            {cooked: 'query', raw: 'query'},
+                            false
+                        ),
+                        b.templateElement(
+                            {cooked: 'field', raw: 'field'},
+                            false
+                        ),
+                        b.templateElement(
+                            {cooked: '', raw: ''},
+                            true
+                        ),
+                    ],
+                    [
+                        b.callExpression(
+                            b.memberExpression(
+                                b.identifier('foo'),
+                                b.identifier('getQuery'),
+                                false
+                            ),
+                            []
+                        ),
+                        b.identifier('bar')
+                    ]
+                )
+            )
+        ]);
+
+        pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+
+        code = [
+            "graphql`",
+            "  query {",
+            "    ${foo.getQuery()},",
+            "    field,",
+            "    ${bar},",
+            "  }",
+            "`;",
+        ].join("\n");
+
+        ast = parse(code);
+        pretty = printer.printGenerically(ast).code;
+        assert.strictEqual(pretty, code);
+    });
+
+    it("preserves newlines at the beginning/end of files", function() {
+        var code = [
+            "",
+            "f();",
+            ""
+        ].join("\n");
+
+        var lines = fromString(code);
+        var ast = parse(code, {
+            esprima: {
+                parse: function(source, options) {
+                    var program = require("esprima-fb").parse(source, options);
+                    n.Program.assert(program);
+                    // Expand ast.program.loc to include any
+                    // leading/trailing whitespace, to simulate the
+                    // behavior of some parsers, e.g. babel-core.
+                    lines.skipSpaces(program.loc.start, true, true);
+                    lines.skipSpaces(program.loc.end, false, true);
+                    return program;
+                }
+            }
+        });
+
+        ast.program.body.unshift(b.debuggerStatement());
+
+        var printer = new Printer({
+            tabWidth: 2
+        });
+
+        assert.strictEqual(printer.print(ast).code, [
+            "",
+            "debugger;",
+            "f();",
+            ""
+        ].join("\n"));
+    });
+
+    it("respects options.lineTerminator", function() {
+        var lines = [
+            "var first = 1;",
+            "var second = 2;"
+        ];
+        var code = lines.join("\n");
+        var ast = parse(code);
+
+        assert.strictEqual(
+            new Printer({
+                lineTerminator: "\r\n"
+            }).print(ast).code,
+            lines.join("\r\n")
+        );
+    });
+});
diff --git a/test/syntax.js b/test/syntax.js
new file mode 100644
index 0000000..7cd9e73
--- /dev/null
+++ b/test/syntax.js
@@ -0,0 +1,54 @@
+var assert = require("assert");
+var fs = require("fs");
+var path = require("path");
+var types = require("../lib/types");
+var parse = require("../lib/parser").parse;
+
+describe("syntax", function() {
+    // Make sure we handle all possible node types in Syntax, and no additional
+    // types that are not present in Syntax.
+    it("Completeness", function(done) {
+        var printer = path.join(__dirname, "../lib/printer.js");
+
+        fs.readFile(printer, "utf-8", function(err, data) {
+            assert.ok(!err);
+
+            var ast = parse(data);
+            assert.ok(ast);
+
+            var typeNames = {};
+            types.visit(ast, {
+                visitFunctionDeclaration: function(path) {
+                    var decl = path.node;
+                    if (types.namedTypes.Identifier.check(decl.id) &&
+                        decl.id.name === "genericPrintNoParens") {
+                        this.traverse(path, {
+                            visitSwitchCase: function(path) {
+                                var test = path.node.test;
+                                if (test &&
+                                    test.type === "Literal" &&
+                                    typeof test.value === "string") {
+                                    var name = test.value;
+                                    typeNames[name] = name;
+                                }
+                                return false;
+                            }
+                        });
+                    } else {
+                        this.traverse(path);
+                    }
+                }
+            });
+
+            for (var name in types.namedTypes) {
+                if (types.namedTypes.hasOwnProperty(name)) {
+                    assert.ok(typeNames.hasOwnProperty(name), "unhandled type: " + name);
+                    assert.strictEqual(name, typeNames[name]);
+                    delete typeNames[name];
+                }
+            }
+
+            done();
+        });
+    });
+});
diff --git a/test/type-syntax.js b/test/type-syntax.js
new file mode 100644
index 0000000..6c7bde9
--- /dev/null
+++ b/test/type-syntax.js
@@ -0,0 +1,95 @@
+var assert = require("assert");
+var parse = require("../lib/parser").parse;
+var Printer = require("../lib/printer").Printer;
+var types = require("../lib/types");
+var n = types.namedTypes;
+var b = types.builders;
+
+describe("type syntax", function() {
+    var printer = new Printer({ tabWidth: 2, quote: 'single' });
+
+    function check(source) {
+        var ast1 = parse(source);
+        var code = printer.printGenerically(ast1).code;
+        var ast2 = parse(code);
+        types.astNodesAreEquivalent.assert(ast1, ast2);
+        assert.strictEqual(source, code);
+    }
+
+    it("should parse and print type annotations correctly", function() {
+        // Import type annotations
+        check("import type foo from 'foo';");
+        check("import typeof foo from 'foo';");
+
+        // Scalar type annotations
+        check("var a: number;");
+        check("var a: number = 5;");
+
+        check("var a: any;");
+        check("var a: boolean;");
+        check("var a: string;");
+        check("var a: 'foo';");
+        check("var a: void;");
+
+        // Nullable
+        check("var a: ?number;");
+
+        // Unions & Intersections
+        check("var a: number | string | boolean = 26;");
+        check("var a: number & string & boolean = 26;");
+
+        // Types
+        check("var a: A = 5;");
+        // TODO!?
+        check("var a: typeof A;");
+
+        // Type aliases
+        check("type A = B;");
+        check("type A = B.C;");
+
+        // Generic
+        check("var a: Array<Foo>;");
+        check("var a: number[];");
+
+        // Return types
+        check("function a(): number {}");
+        check("var a: () => X = fn;");
+
+        // Object
+        check("var a: {\n  b: number;\n  x: {y: A};\n};");
+        check("var b: {[key: string]: number};")
+        check("var c: {(): number};")
+        check("var d: {\n  [key: string]: A;\n  [key: number]: B;\n  (): C;\n  a: D;\n};")
+
+        // Casts
+        check("(1 + 1: number);");
+
+        // Declare
+        check("declare var A: string;");
+
+        check("declare function foo(c: C): void;");
+        check("declare function foo(c: C, b: B): void;");
+        check("declare function foo(c: (e: Event) => void, b: B): void;");
+        check("declare class C {x: string}");
+        check("declare module M {\n  declare function foo(c: C): void;\n}");
+
+        // Classes
+        check("class A {\n  a: number;\n}");
+        check("class A {\n  foo(a: number): string {}\n}");
+        check("class A {\n  static foo(a: number): string {}\n}");
+
+        // Type parameters
+        check("class A<T> {}");
+        check("class A<X, Y> {}");
+        check("class A<X> extends B<Y> {}");
+        check("function a<T>(y: Y<T>): T {}");
+        check("class A {\n  foo<T>(a: number): string {}\n}");
+
+        // Interfaces
+        check("interface A<X> extends B<A>, C {a: number}");
+        check("class A extends B implements C<T>, Y {}");
+
+        // Bounded polymorphism
+        check("class A<T: number> {}");
+    });
+});
diff --git a/test/visit.js b/test/visit.js
new file mode 100644
index 0000000..0f42a06
--- /dev/null
+++ b/test/visit.js
@@ -0,0 +1,138 @@
+var assert = require("assert");
+var types = require("../lib/types");
+var namedTypes = types.namedTypes;
+var builders = types.builders;
+var parse = require("../lib/parser").parse;
+var Printer = require("../lib/printer").Printer;
+
+var lines = [
+    "// file comment",
+    "exports.foo({",
+    "    // some comment",
+    "    bar: 42,",
+    "    baz: this",
+    "});"
+];
+
+describe("types.visit", function() {
+    it("replacement", function() {
+        var source = lines.join("\n");
+        var printer = new Printer;
+        var ast = parse(source);
+        var withThis = printer.print(ast).code;
+        var thisExp = /\bthis\b/g;
+
+        assert.ok(thisExp.test(withThis));
+
+        types.visit(ast, {
+            visitThisExpression: function() {
+                return builders.identifier("self");
+            }
+        });
+
+        assert.strictEqual(
+            printer.print(ast).code,
+            withThis.replace(thisExp, "self")
+        );
+
+        var propNames = [];
+        var methods = {
+            visitProperty: function(path) {
+                var key = path.node.key;
+                propNames.push(key.value || key.name);
+                this.traverse(path);
+            }
+        };
+
+        types.visit(ast, methods);
+        assert.deepEqual(propNames, ["bar", "baz"]);
+
+        types.visit(ast, {
+            visitProperty: function(path) {
+                if (namedTypes.Identifier.check(path.node.value) &&
+                    path.node.value.name === "self") {
+                    path.replace();
+                    return false;
+                }
+
+                this.traverse(path);
+            }
+        });
+
+        propNames.length = 0;
+
+        types.visit(ast, methods);
+        assert.deepEqual(propNames, ["bar"]);
+    });
+
+    it("reindent", function() {
+        var lines = [
+            "a(b(c({",
+            "    m: d(function() {",
+            "        if (e('y' + 'z'))",
+            "            f(42).h()",
+            "                 .i()",
+            "                 .send();",
+            "        g(8);",
+            "    })",
+            "})));"
+        ];
+
+        var altered = [
+            "a(xxx(function() {",
+            "    if (e('y' > 'z'))",
+            "        f(42).h()",
+            "             .i()",
+            "             .send();",
+            "    g(8);",
+            "}, c(function() {",
+            "    if (e('y' > 'z'))",
+            "        f(42).h()",
+            "             .i()",
+            "             .send();",
+            "    g(8);",
+            "})));"
+        ];
+
+        var source = lines.join("\n");
+        var ast = parse(source);
+        var printer = new Printer;
+
+        var funExpr;
+        types.visit(ast, {
+            visitFunctionExpression: function(path) {
+                assert.strictEqual(typeof funExpr, "undefined");
+                funExpr = path.node;
+                this.traverse(path);
+            },
+
+            visitBinaryExpression: function(path) {
+                path.node.operator = ">";
+                this.traverse(path);
+            }
+        });
+
+        namedTypes.FunctionExpression.assert(funExpr);
+
+        types.visit(ast, {
+            visitCallExpression: function(path) {
+                this.traverse(path);
+                var expr = path.node;
+                if (namedTypes.Identifier.check(expr.callee) &&
+                    expr.callee.name === "b") {
+                    expr.callee.name = "xxx";
+                    expr["arguments"].unshift(funExpr);
+                }
+            },
+
+            visitObjectExpression: function() {
+                return funExpr;
+            }            
+        });
+
+        assert.strictEqual(
+            altered.join("\n"),
+            printer.print(ast).code
+        );
+    });
+});

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-recast.git



More information about the Pkg-javascript-commits mailing list