diff --git a/.gitignore b/.gitignore
index 6e326b1ba..fc7d79ae6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,8 @@ vendor
 .rvmrc
 .DS_Store
 lib/handlebars/compiler/parser.js
-dist/*.min.js
+/dist/
+/tmp/
 node_modules
 *.sublime-project
 *.sublime-workspace
diff --git a/.jshintrc b/.jshintrc
index f37931784..851cf78fa 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -21,18 +21,23 @@
         "stop",
         "ok",
         "strictEqual",
-        "module"
+        "module",
+
+        "describe",
+        "it",
+        "afterEach"
     ],
 
     "node" : true,
-    "es5" : true,
     "browser" : true,
+    "esnext": true,
 
     "boss" : true,
     "curly": false,
     "debug": false,
     "devel": false,
-    "eqeqeq": true,
+    "eqeqeq": false,
+    "eqnull": true,
     "evil": true,
     "forin": false,
     "immed": false,
diff --git a/.npmignore b/.npmignore
index 2f42c9f44..eeb1ceb5e 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,10 +1,22 @@
 .DS_Store
 .gitignore
 .rvmrc
+.jshintrc
+.travis.yml
+.rspec
 Gemfile
 Gemfile.lock
 Rakefile
+Gruntfile.js
+*.gemspec
+*.nuspec
 bench/*
+configurations/*
+components/*
+dist/cdnjs/*
+dist/components/*
 spec/*
 src/*
+tasks/*
+publish/*
 vendor/*
diff --git a/.rspec b/.rspec
deleted file mode 100644
index 62c58f049..000000000
--- a/.rspec
+++ /dev/null
@@ -1 +0,0 @@
--cfs
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..1e4e9651f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,27 @@
+language: node_js
+node_js:
+  - "0.8"
+  - "0.10"
+
+before_install:
+  - npm install -g grunt-cli
+
+script:
+  - grunt --stack default metrics publish:latest
+
+email:
+  on_failure: change
+  on_success: never
+env:
+  global:
+  - S3_BUCKET_NAME=builds.handlebarsjs.com
+  - secure: ! 'PJaukuvkBBsSDOqbIcNSSMgb96VVEaIt/eq9GPjXPeFbSd3hXgwhwVE62Lrq
+
+      tJO8BaUfX+PzpiQjEl4D5/KBmvlFZ057Hqmy0zmPOT5mDZfJe8Ja5zyvTMb+
+
+      KkCWN/tjAp8kawHojE04pn6jIpPdwXFnAYwPhaHbATFrmdt9fdg='
+  - secure: ! 'mBcGL2tnmiRujJdV/4fxrVd8E8wn6AW9IQKVcMv8tvOc7i5dOzZ39rpBKLuT
+
+      MRXDtMV1LyLiuKYb1pHj1IyeadEahcLYFfGygF4LG7Yzp4NWHtRzQ7Q8LXaJ
+
+      V7dXDboYCFkn2a8/Rtx1YSVh/sCONf5UoRC+MUIqrj4UiHN9r3s='
diff --git a/Gemfile b/Gemfile
deleted file mode 100644
index cf092ce28..000000000
--- a/Gemfile
+++ /dev/null
@@ -1,5 +0,0 @@
-source "http://rubygems.org"
-
-gem "rake"
-gem "therubyracer", ">= 0.9.8", "< 0.11"
-gem "rspec"
diff --git a/Gemfile.lock b/Gemfile.lock
deleted file mode 100644
index ce8338f4e..000000000
--- a/Gemfile.lock
+++ /dev/null
@@ -1,24 +0,0 @@
-GEM
-  remote: http://rubygems.org/
-  specs:
-    diff-lcs (1.1.3)
-    libv8 (3.3.10.4)
-    rake (10.0.3)
-    rspec (2.12.0)
-      rspec-core (~> 2.12.0)
-      rspec-expectations (~> 2.12.0)
-      rspec-mocks (~> 2.12.0)
-    rspec-core (2.12.2)
-    rspec-expectations (2.12.1)
-      diff-lcs (~> 1.1.3)
-    rspec-mocks (2.12.1)
-    therubyracer (0.10.2)
-      libv8 (~> 3.3.10)
-
-PLATFORMS
-  ruby
-
-DEPENDENCIES
-  rake
-  rspec
-  therubyracer (>= 0.9.8, < 0.11)
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 000000000..4fe9b0531
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,164 @@
+var childProcess = require('child_process');
+
+module.exports = function(grunt) {
+
+  grunt.initConfig({
+    pkg: grunt.file.readJSON('package.json'),
+
+    jshint: {
+      options: {
+        jshintrc: '.jshintrc',
+        force: true
+      },
+      files: [
+        'lib/**/!(parser).js'
+      ]
+    },
+
+    clean: ["dist", "lib/handlebars/compiler/parser.js"],
+
+    copy: {
+      dist: {
+        options: {
+          processContent: function(content, path) {
+            return grunt.template.process('/*!\n\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n@license\n*/\n')
+                + content;
+          }
+        },
+        files: [
+          {expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/'}
+        ]
+      },
+      cdnjs: {
+        files: [
+          {expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/cdnjs'}
+        ]
+      },
+      components: {
+        files: [
+          {expand: true, cwd: 'components/', src: ['**'], dest: 'dist/components'},
+          {expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/components'}
+        ]
+      }
+    },
+
+    transpile: {
+      amd: {
+        type: "amd",
+        anonymous: true,
+        files: [{
+          expand: true,
+          cwd: 'lib/',
+          src: '**/!(index).js',
+          dest: 'dist/amd/'
+        }]
+      },
+
+      cjs: {
+        type: 'cjs',
+        files: [{
+          expand: true,
+          cwd: 'lib/',
+          src: '**/!(index).js',
+          dest: 'dist/cjs/'
+        }]
+      }
+    },
+
+    packager: {
+      options: {
+        export: 'Handlebars'
+      },
+
+      global: {
+        files: [{
+          cwd: 'lib/',
+          expand: true,
+          src: ['handlebars*.js'],
+          dest: 'dist/'
+        }]
+      }
+    },
+    requirejs: {
+      options: {
+        optimize: "none",
+        baseUrl: "dist/amd/"
+      },
+      dist: {
+        options: {
+          name: "handlebars",
+          out: "dist/handlebars.amd.js"
+        }
+      },
+      runtime: {
+        options: {
+          name: "handlebars.runtime",
+          out: "dist/handlebars.runtime.amd.js"
+        }
+      }
+    },
+
+    uglify: {
+      options: {
+        mangle: true,
+        compress: true,
+        preserveComments: 'some'
+      },
+      dist: {
+        files: [{
+          cwd: 'dist/',
+          expand: true,
+          src: ['handlebars*.js'],
+          dest: 'dist/',
+          rename: function(dest, src) {
+            return dest + src.replace(/\.js$/, '.min.js');
+          }
+        }]
+      }
+    }
+  });
+
+  // Build a new version of the library
+  this.registerTask('build', "Builds a distributable version of the current project", [
+                    'jshint',
+                    'clean',
+                    'parser',
+                    'node',
+                    'globals']);
+
+  this.registerTask('amd', ['transpile:amd', 'requirejs']);
+  this.registerTask('node', ['transpile:cjs']);
+  this.registerTask('globals', ['packager-fork']);
+
+  this.registerTask('release', 'Build final packages', ['amd', 'uglify', 'copy:dist', 'copy:components', 'copy:cdnjs']);
+
+  // Load tasks from npm
+  grunt.loadNpmTasks('grunt-contrib-clean');
+  grunt.loadNpmTasks('grunt-contrib-copy');
+  grunt.loadNpmTasks('grunt-contrib-requirejs');
+  grunt.loadNpmTasks('grunt-contrib-jshint');
+  grunt.loadNpmTasks('grunt-contrib-uglify');
+  grunt.loadNpmTasks('grunt-es6-module-transpiler');
+
+  grunt.task.loadTasks('tasks');
+
+  grunt.registerTask('packager-fork', function() {
+    // Allows us to run the packager task out of process to work around the multiple
+    // traceur exec issues
+    grunt.util.spawn({grunt: true,  args: ['--stack', 'packager'], opts: {stdio: 'inherit'}}, this.async());
+  });
+  grunt.registerTask('test', function() {
+    var done = this.async();
+
+    var runner = childProcess.fork('./spec/env/runner', [], {stdio: 'inherit'});
+    runner.on('close', function(code) {
+      if (code != 0) {
+        grunt.fatal(code + ' tests failed');
+      }
+      done();
+    });
+  });
+  grunt.registerTask('bench', ['metrics']);
+
+  grunt.registerTask('default', ['build', 'test', 'release']);
+};
diff --git a/README.markdown b/README.markdown
index f775815e2..6ea94f8fb 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,5 +1,6 @@
 [![Build Status](https://travis-ci.org/wycats/handlebars.js.png?branch=master)](https://travis-ci.org/wycats/handlebars.js)
 
+
 Handlebars.js
 =============
 
@@ -11,13 +12,23 @@ keep the view and the code separated like we all know they should be.
 Checkout the official Handlebars docs site at
 [http://www.handlebarsjs.com](http://www.handlebarsjs.com).
 
-
 Installing
 ----------
 Installing Handlebars is easy. Simply download the package [from the
 official site](http://handlebarsjs.com/) and add it to your web pages
 (you should usually use the most recent version).
 
+Alternatively, if you prefer having the latest version of handlebars from
+the 'master' branch, passing builds of the 'master' branch are automatically
+published to S3. You may download the latest passing master build by grabbing
+a `handlebars-latest.js` file from the [builds page][builds-page]. When the
+build is published, it is also available as a `handlebars-gitSHA.js` file on
+the builds page if you need a version to refer to others.
+`handlebars-runtime.js` builds are also available.
+
+**Note**: The S3 builds page is provided as a convenience for the community,
+but you should not use it for hosting Handlebars in production.
+
 Usage
 -----
 In general, the syntax of Handlebars.js templates is a superset
@@ -108,7 +119,7 @@ To display data from descendant contexts, use the `.` character. So, for
 example, if your data were structured like:
 
 ```js
-var data = {"person": { "name": "Alan" }, company: {"name": "Rad, Inc." } };
+var data = {"person": { "name": "Alan" }, "company": {"name": "Rad, Inc." } };
 ```
 
 You could display the person's name from the top-level context with the
@@ -260,13 +271,20 @@ Precompile handlebar templates.
 Usage: handlebars template...
 
 Options:
-  -a, --amd        Create an AMD format function (allows loading with RequireJS)         [boolean]
-  -f, --output     Output File                                                           [string]
-  -k, --known      Known helpers                                                         [string]
-  -o, --knownOnly  Known helpers only                                                    [boolean]
-  -m, --min        Minimize output                                                       [boolean]
-  -s, --simple     Output template function only.                                        [boolean]
-  -r, --root       Template root. Base value that will be stripped from template names.  [string]
+  -a, --amd            Create an AMD format function (allows loading with RequireJS)          [boolean]
+  -f, --output         Output File                                                            [string]
+  -k, --known          Known helpers                                                          [string]
+  -o, --knownOnly      Known helpers only                                                     [boolean]
+  -m, --min            Minimize output                                                        [boolean]
+  -s, --simple         Output template function only.                                         [boolean]
+  -r, --root           Template root. Base value that will be stripped from template names.   [string]
+  -c, --commonjs       Exports CommonJS style, path to Handlebars module                      [string]
+  -h, --handlebarPath  Path to handlebar.js (only valid for amd-style)                        [string]
+  -n, --namespace      Template namespace                                                     [string]
+  -p, --partial        Compiling a partial template                                           [boolean]
+  -d, --data           Include data when compiling                                            [boolean]
+  -e, --extension      Template extension.                                                    [string]
+  -b, --bom            Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean]
 </pre>
 
 If using the precompiler's normal mode, the resulting templates will be
@@ -289,6 +307,8 @@ normal.
   helpers for size and speed.
 - When all helpers are known in advance the `--knownOnly` argument may be used
   to optimize all block helper references.
+- Implementations that do not use `@data` variables can improve performance of
+  iteration centric templates by specifying `{data: false}` in the compiler options.
 
 Supported Environments
 ----------------------
@@ -322,8 +342,7 @@ and we will have some benchmarks in the near future.
 Building
 --------
 
-To build handlebars, just run `rake release`, and you will get two files
-in the `dist` directory.
+To build handlebars, just run `grunt build`, and the build will output to the `dist` directory.
 
 
 Upgrading
@@ -337,47 +356,54 @@ Known Issues
 * Using a variable, helper, or partial named `class` causes errors in IE browsers. (Instead, use `className`)
 
 Handlebars in the Wild
------------------
+----------------------
+
+* [Assemble](http://assemble.io), by [@jonschlinkert](https://github.com/jonschlinkert)
+  and [@doowb](https://github.com/doowb), is a static site generator that uses Handlebars.js
+  as its template engine.
+* [CoSchedule](http://coschedule.com) An editorial calendar for WordPress that uses Handlebars.js
+* [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to
+  structure your views, also with automatic data binding support.
+* [handlebars_assets](http://github.com/leshill/handlebars_assets): A Rails Asset Pipeline gem
+  from Les Hill (@leshill).
+* [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library
+  with 100+ handlebars helpers.
+* [hbs](http://github.com/donpark/hbs): An Express.js view engine adapter for Handlebars.js,
+  from Don Park.
 * [jblotus](http://github.com/jblotus) created [http://tryhandlebarsjs.com](http://tryhandlebarsjs.com)
   for anyone who would like to try out Handlebars.js in their browser.
-* Don Park wrote an Express.js view engine adapter for Handlebars.js called
-  [hbs](http://github.com/donpark/hbs).
+* [jQuery plugin](http://71104.github.io/jquery-handlebars/): allows you to use
+  Handlebars.js with [jQuery](http://jquery.com/).
+* [Lumbar](http://walmartlabs.github.io/lumbar) provides easy module-based template management for
+  handlebars projects.
 * [sammy.js](http://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey,
   supports Handlebars.js as one of its template plugins.
 * [SproutCore](http://www.sproutcore.com) uses Handlebars.js as its main
   templating engine, extending it with automatic data binding support.
-* [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to
-  structure your views, also with automatic data binding support.
-* Les Hill (@leshill) wrote a Rails Asset Pipeline gem named
-  [handlebars_assets](http://github.com/leshill/handlebars_assets).
-* [Gist about Synchronous and asynchronous loading of external handlebars templates](https://gist.github.com/2287070)
-* [Lumbar](walmartlabs.github.io/lumbar) provides easy module-based template management for handlebars projects.
 * [YUI](http://yuilibrary.com/yui/docs/handlebars/) implements a port of handlebars
+* [Swag](https://github.com/elving/swag) by [@elving](https://github.com/elving) is a growing collection of helpers for handlebars.js. Give your handlebars.js templates some swag son!
+
+External Resources
+------------------
+
+* [Gist about Synchronous and asynchronous loading of external handlebars templates](https://gist.github.com/2287070)
 
 Have a project using Handlebars? Send us a [pull request](https://github.com/wycats/handlebars.js/pull/new/master)!
 
 Helping Out
 -----------
+
 To build Handlebars.js you'll need a few things installed.
 
 * Node.js
-* Ruby
-* therubyracer, for running tests - `gem install therubyracer`
-* rspec, for running tests - `gem install rspec`
+* [Grunt](http://gruntjs.com/getting-started)
 
-There's a Gemfile in the repo, so you can run `bundle` to install rspec
-and therubyracer if you've got bundler installed.
+Project dependencies may be installed via `npm install`.
 
-To build Handlebars.js from scratch, you'll want to run `rake compile`
+To build Handlebars.js from scratch, you'll want to run `grunt`
 in the root of the project. That will build Handlebars and output the
-results to the dist/ folder. To run tests, run `rake test`. You can also
-run our set of benchmarks with `rake bench`. Node tests can be run with
-`npm test` or `rake npm_test`. The default rake target will compile and
-run both test suites.
-
-Some environments, notably Windows, have issues running therubyracer. Under these
-envrionments the `rake compile` and `npm test` should be sufficient to test
-most handlebars functionality.
+results to the dist/ folder. To re-run tests, run `grunt test` or `npm test`.
+You can also run our set of benchmarks with `grunt bench`.
 
 If you notice any problems, please report them to the GitHub issue tracker at
 [http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues).
@@ -389,3 +415,4 @@ License
 -------
 Handlebars.js is released under the MIT license.
 
+[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/index.html
diff --git a/Rakefile b/Rakefile
deleted file mode 100644
index 794686f1e..000000000
--- a/Rakefile
+++ /dev/null
@@ -1,136 +0,0 @@
-require "rubygems"
-require "bundler/setup"
-
-def compile_parser
-  system "./node_modules/.bin/jison -m js src/handlebars.yy src/handlebars.l"
-  if $?.success?
-    File.open("lib/handlebars/compiler/parser.js", "w") do |file|
-      file.puts File.read("src/parser-prefix.js") + File.read("handlebars.js") + File.read("src/parser-suffix.js")
-    end
-
-    sh "rm handlebars.js"
-  else
-    fail "Failed to run Jison."
-  end
-end
-
-file "lib/handlebars/compiler/parser.js" => ["src/handlebars.yy","src/handlebars.l"] do
-  if File.exists?('./node_modules/jison')
-    compile_parser
-  else
-    puts "Jison is not installed. Trying `npm install jison`."
-    sh "npm install"
-    compile_parser
-  end
-end
-
-task :compile => "lib/handlebars/compiler/parser.js"
-
-desc "run the spec suite"
-task :spec => [:release] do
-  rc = system "rspec -cfs spec"
-  fail "rspec spec failed with exit code #{$?.exitstatus}" if (rc.nil? || ! rc || $?.exitstatus != 0)
-end
-
-desc "run the npm test suite"
-task :npm_test => [:release] do
-  rc = system "npm test"
-  fail "npm test failed with exit code #{$?.exitstatus}" if (rc.nil? || ! rc || $?.exitstatus != 0)
-end
-
-task :default => [:compile, :spec, :npm_test]
-
-def remove_exports(string)
-  match = string.match(%r{^// BEGIN\(BROWSER\)\n(.*)\n^// END\(BROWSER\)}m)
-  match ? match[1] : string
-end
-
-minimal_deps = %w(browser-prefix base compiler/parser compiler/base compiler/ast utils compiler/compiler runtime browser-suffix).map do |file|
-  "lib/handlebars/#{file}.js"
-end
-
-runtime_deps = %w(browser-prefix base utils runtime browser-suffix).map do |file|
-  "lib/handlebars/#{file}.js"
-end
-
-directory "dist"
-
-minimal_deps.unshift "dist"
-
-def build_for_task(task)
-  FileUtils.rm_rf("dist/*") if File.directory?("dist")
-  FileUtils.mkdir_p("dist")
-
-  contents = ["/*\n\n" + File.read('LICENSE') + "\n*/\n"]
-  task.prerequisites.each do |filename|
-    next if filename == "dist"
-
-    contents << "// #{filename}\n" + remove_exports(File.read(filename)) + ";"
-  end
-
-  File.open(task.name, "w") do |file|
-    file.puts contents.join("\n")
-  end
-end
-
-file "dist/handlebars.js" => minimal_deps do |task|
-  build_for_task(task)
-end
-
-file "dist/handlebars.runtime.js" => runtime_deps do |task|
-  build_for_task(task)
-end
-
-task :build => [:compile] do |task|
-  # Since the tests are dependent on this always rebuild.
-  Rake::Task["dist/handlebars.js"].execute
-end
-task :runtime => [:compile] do |task|
-  # Since the tests are dependent on this always rebuild.
-  Rake::Task["dist/handlebars.runtime.js"].execute
-end
-
-# Updates the various version numbers.
-task :version => [] do |task|
-  # TODO : Pull from package.json when the version numbers are synced
-  version = File.read("lib/handlebars/base.js").match(/Handlebars.VERSION = "(.*)";/)[1]
-
-  content = File.read("bower.json")
-  File.open("bower.json", "w") do |file|
-    file.puts content.gsub(/"version":.*/, "\"version\": \"#{version}\",")
-  end
-
-  content = File.read("handlebars.js.nuspec")
-  File.open("handlebars.js.nuspec", "w") do |file|
-    file.puts content.gsub(/<version>.*<\/version>/, "<version>#{version}</version>")
-  end
-end
-
-task :minify => [] do |task|
-  system "./node_modules/.bin/uglifyjs --comments -o dist/handlebars.min.js dist/handlebars.js"
-  system "./node_modules/.bin/uglifyjs --comments -o dist/handlebars.runtime.min.js dist/handlebars.runtime.js"
-end
-
-desc "build the build and runtime version of handlebars"
-task :release => [:version, :build, :runtime, :minify]
-
-directory "vendor"
-
-desc "benchmark against dust.js and mustache.js"
-task :bench => "vendor" do
-  require "open-uri"
-
-  #if File.directory?("vendor/coffee")
-    #system "cd vendor/coffee && git pull"
-  #else
-    #system "git clone git://github.com/jashkenas/coffee-script.git vendor/coffee"
-  #end
-
-  #if File.directory?("vendor/eco")
-    #system "cd vendor/eco && git pull && npm update"
-  #else
-    #system "git clone git://github.com/sstephenson/eco.git vendor/eco && cd vendor/eco && npm update"
-  #end
-
-  system "node bench/handlebars.js"
-end
diff --git a/bench/dist-size.js b/bench/dist-size.js
new file mode 100644
index 000000000..9e5fdc0f5
--- /dev/null
+++ b/bench/dist-size.js
@@ -0,0 +1,39 @@
+var _ = require('underscore'),
+    async = require('async'),
+    fs = require('fs'),
+    zlib = require('zlib');
+
+module.exports = function(grunt, callback) {
+  var distFiles = fs.readdirSync('dist'),
+      distSizes = {};
+
+  async.each(distFiles, function(file, callback) {
+      var content;
+      try {
+        content = fs.readFileSync('dist/' + file);
+      } catch (err) {
+        if (err.code === 'EISDIR') {
+          callback();
+          return;
+        } else {
+          throw err;
+        }
+      }
+
+      file = file.replace(/\.js/, '').replace(/\./g, '_');
+      distSizes[file] = content.length;
+
+      zlib.gzip(content, function(err, data) {
+        if (err) {
+          throw err;
+        }
+
+        distSizes[file + '_gz'] = data.length;
+        callback();
+      });
+    },
+    function() {
+      grunt.log.writeln('Distribution sizes: ' + JSON.stringify(distSizes, undefined, 2));
+      callback([distSizes]);
+    });
+};
diff --git a/bench/handlebars.js b/bench/handlebars.js
deleted file mode 100644
index c7f6c1058..000000000
--- a/bench/handlebars.js
+++ /dev/null
@@ -1,172 +0,0 @@
-var BenchWarmer = require("./benchwarmer");
-Handlebars = require("../lib/handlebars");
-
-var dust, Mustache, eco;
-
-try {
-  dust = require("dust");
-} catch (err) { /* NOP */ }
-
-try {
-  Mustache = require("mustache");
-} catch (err) { /* NOP */ }
-
-try {
-  var ecoExports = require("eco");
-  eco = function(str) {
-    return ecoExports(str);
-  }
-} catch (err) { /* NOP */ }
-
-var benchDetails = {
-  string: {
-    context: {},
-    handlebars: "Hello world",
-    dust: "Hello world",
-    mustache: "Hello world",
-    eco: "Hello world"
-  },
-  variables: {
-    context: {name: "Mick", count: 30},
-    handlebars:  "Hello {{name}}! You have {{count}} new messages.",
-    dust: "Hello {name}! You have {count} new messages.",
-    mustache: "Hello {{name}}! You have {{count}} new messages.",
-    eco: "Hello <%= @name %>! You have <%= @count %> new messages."
-  },
-  object: {
-    context:  { person: { name: "Larry", age: 45 } },
-    handlebars: "{{#with person}}{{name}}{{age}}{{/with}}",
-    dust: "{#person}{name}{age}{/person}",
-    mustache: "{{#person}}{{name}}{{age}}{{/person}}"
-  },
-  array: {
-    context:  { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
-    handlebars: "{{#each names}}{{name}}{{/each}}",
-    dust: "{#names}{name}{/names}",
-    mustache: "{{#names}}{{name}}{{/names}}",
-    eco: "<% for item in @names: %><%= item.name %><% end %>"
-  },
-  partial: {
-    context: { peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 1}] },
-    partials: {
-      mustache: { variables: "Hello {{name}}! You have {{count}} new messages." },
-      handlebars: { variables: "Hello {{name}}! You have {{count}} new messages." }
-    },
-    handlebars: "{{#each peeps}}{{>variables}}{{/each}}",
-    dust: "{#peeps}{>variables/}{/peeps}",
-    mustache: "{{#peeps}}{{>variables}}{{/peeps}}"
-  },
-  recursion: {
-    context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] },
-    partials: {
-      mustache: { recursion: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" },
-      handlebars: { recursion: "{{name}}{{#each kids}}{{>recursion}}{{/each}}" }
-    },
-    handlebars: "{{name}}{{#each kids}}{{>recursion}}{{/each}}",
-    dust: "{name}{#kids}{>recursion:./}{/kids}",
-    mustache: "{{name}}{{#kids}}{{>recursion}}{{/kids}}"
-  },
-  complex: {
-    handlebars: "<h1>{{header}}</h1>{{#if items}}<ul>{{#each items}}{{#if current}}" +
-                "<li><strong>{{name}}</strong></li>{{^}}" +
-                "<li><a href=\"{{url}}\">{{name}}</a></li>{{/if}}" +
-                "{{/each}}</ul>{{^}}<p>The list is empty.</p>{{/if}}",
-
-    dust:       "<h1>{header}</h1>\n"                             +
-                "{?items}\n"                                      +
-                "  <ul>\n"                                        +
-                "    {#items}\n"                                  +
-                "      {#current}\n"                              +
-                "        <li><strong>{name}</strong></li>\n"      +
-                "      {:else}\n"                                 +
-                "        <li><a href=\"{url}\">{name}</a></li>\n" +
-                "      {/current}\n"                              +
-                "    {/items}\n"                                  +
-                "  </ul>\n"                                       +
-                "{:else}\n"                                       +
-                "  <p>The list is empty.</p>\n"                   +
-                "{/items}",
-    context: {
-               header: function() {
-                 return "Colors";
-               },
-               items: [
-                 {name: "red", current: true, url: "#Red"},
-                 {name: "green", current: false, url: "#Green"},
-                 {name: "blue", current: false, url: "#Blue"}
-               ]
-             }
-  }
-
-};
-
-handlebarsTemplates = {};
-ecoTemplates = {};
-
-var warmer = new BenchWarmer();
-
-var makeSuite = function(name) {
-  warmer.suite(name, function(bench) {
-    var templateName = name;
-    var details = benchDetails[templateName];
-    var mustachePartials = details.partials && details.partials.mustache;
-    var mustacheSource = details.mustache;
-    var context = details.context;
-
-    var error = function() { throw new Error("EWOT"); };
-
-    if (dust) {
-      bench("dust", function() {
-        dust.render(templateName, context, function(err, out) { });
-      });
-    }
-
-    bench("handlebars", function() {
-      handlebarsTemplates[templateName](context);
-    });
-
-    if (eco) {
-      if(ecoTemplates[templateName]) {
-        bench("eco", function() {
-          ecoTemplates[templateName](context);
-        });
-      } else {
-        bench("eco", error);
-      }
-    }
-
-    if (Mustache && mustacheSource) {
-      bench("mustache", function() {
-        Mustache.to_html(mustacheSource, context, mustachePartials);
-      });
-    } else {
-      bench("mustache", error);
-    }
-  });
-}
-
-for(var name in benchDetails) {
-  if(benchDetails.hasOwnProperty(name)) {
-    if (dust) {
-      dust.loadSource(dust.compile(benchDetails[name].dust, name));
-    }
-    handlebarsTemplates[name] = Handlebars.compile(benchDetails[name].handlebars);
-
-    if (eco && benchDetails[name].eco) {
-      ecoTemplates[name] = eco(benchDetails[name].eco);
-    }
-
-    var partials = benchDetails[name].partials;
-    if(partials) {
-      for(var partialName in partials.handlebars) {
-        if(partials.handlebars.hasOwnProperty(partialName)) {
-          Handlebars.registerPartial(partialName, partials.handlebars[partialName]);
-        }
-      }
-    }
-
-    makeSuite(name);
-  }
-}
-
-warmer.bench();
diff --git a/bench/index.js b/bench/index.js
new file mode 100644
index 000000000..462b046f5
--- /dev/null
+++ b/bench/index.js
@@ -0,0 +1,14 @@
+var fs = require('fs');
+
+var metrics = fs.readdirSync(__dirname);
+metrics.forEach(function(metric) {
+  if (metric === 'index.js' || !/(.*)\.js$/.test(metric)) {
+    return;
+  }
+
+  var name = RegExp.$1;
+  metric = require('./' + name);
+  if (metric instanceof Function) {
+    module.exports[name] = metric;
+  }
+});
diff --git a/bench/precompile-size.js b/bench/precompile-size.js
new file mode 100644
index 000000000..66eacd025
--- /dev/null
+++ b/bench/precompile-size.js
@@ -0,0 +1,19 @@
+var _ = require('underscore'),
+    templates = require('./templates');
+
+module.exports = function(grunt, callback) {
+  // Deferring to here in case we have a build for parser, etc as part of this grunt exec
+  var Handlebars = require('../lib');
+
+  var templateSizes = {};
+  _.each(templates, function(info, template) {
+    var src = info.handlebars,
+        compiled = Handlebars.precompile(src, {}),
+        knownHelpers = Handlebars.precompile(src, {knownHelpersOnly: true, knownHelpers: info.helpers});
+
+    templateSizes[template] = compiled.length;
+    templateSizes['knownOnly_' + template] = knownHelpers.length;
+  });
+  grunt.log.writeln('Precompiled sizes: ' + JSON.stringify(templateSizes, undefined, 2));
+  callback([templateSizes]);
+};
diff --git a/bench/templates/arguments.js b/bench/templates/arguments.js
new file mode 100644
index 000000000..5480c8d8e
--- /dev/null
+++ b/bench/templates/arguments.js
@@ -0,0 +1,12 @@
+module.exports = {
+  helpers: {
+    foo: function(options) {
+      return '';
+    }
+  },
+  context: {
+    bar: true
+  },
+
+  handlebars: '{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}'
+};
diff --git a/bench/templates/array-each.js b/bench/templates/array-each.js
new file mode 100644
index 000000000..f1eb1e8e9
--- /dev/null
+++ b/bench/templates/array-each.js
@@ -0,0 +1,7 @@
+module.exports = {
+  context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
+  handlebars: "{{#each names}}{{name}}{{/each}}",
+  dust: "{#names}{name}{/names}",
+  mustache: "{{#names}}{{name}}{{/names}}",
+  eco: "<% for item in @names: %><%= item.name %><% end %>"
+};
diff --git a/bench/templates/array-mustache.js b/bench/templates/array-mustache.js
new file mode 100644
index 000000000..908f805ea
--- /dev/null
+++ b/bench/templates/array-mustache.js
@@ -0,0 +1,4 @@
+module.exports = {
+  context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
+  handlebars: "{{#names}}{{name}}{{/names}}"
+}
diff --git a/bench/templates/complex.dust b/bench/templates/complex.dust
new file mode 100644
index 000000000..ff453c2b8
--- /dev/null
+++ b/bench/templates/complex.dust
@@ -0,0 +1,14 @@
+<h1>{header}</h1>
+{?items}
+  <ul>
+    {#items}
+      {#current}
+        <li><strong>{name}</strong></li>
+      {:else}
+        <li><a href="{url}">{name}</a></li>
+      {/current}
+    {/items}
+  </ul>
+{:else}
+  <p>The list is empty.</p>
+{/items}
diff --git a/bench/templates/complex.eco b/bench/templates/complex.eco
new file mode 100644
index 000000000..42fb55b48
--- /dev/null
+++ b/bench/templates/complex.eco
@@ -0,0 +1,14 @@
+<h1><%= @header() %></h1>
+<% if @items.length: %>
+  <ul>
+    <% for item in @items: %>
+      <% if item.current: %>
+        <li><strong><%= item.name %></strong></li>
+      <% else: %>
+        <li><a href="<%= item.url %>"><%= item.name %></a></li>
+      <% end %>
+    <% end %>
+  </ul>
+<% else: %>
+  <p>The list is empty.</p>
+<% end %>
diff --git a/bench/templates/complex.handlebars b/bench/templates/complex.handlebars
new file mode 100644
index 000000000..4a5953445
--- /dev/null
+++ b/bench/templates/complex.handlebars
@@ -0,0 +1,14 @@
+<h1>{{header}}</h1>
+{{#if items}}
+  <ul>
+    {{#each items}}
+      {{#if current}}
+        <li><strong>{{name}}</strong></li>
+      {{^}}
+        <li><a href="{{url}}">{{name}}</a></li>
+      {{/if}}
+    {{/each}}
+  </ul>
+{{^}}
+  <p>The list is empty.</p>
+{{/if}}
diff --git a/bench/templates/complex.js b/bench/templates/complex.js
new file mode 100644
index 000000000..ddf361b59
--- /dev/null
+++ b/bench/templates/complex.js
@@ -0,0 +1,20 @@
+var fs = require('fs');
+
+module.exports = {
+  context: {
+    header: function() {
+      return "Colors";
+    },
+    hasItems: true,   // To make things fairer in mustache land due to no `{{if}}` construct on arrays
+    items: [
+      {name: "red", current: true, url: "#Red"},
+      {name: "green", current: false, url: "#Green"},
+      {name: "blue", current: false, url: "#Blue"}
+    ]
+  },
+
+  handlebars: fs.readFileSync(__dirname + '/complex.handlebars').toString(),
+  dust: fs.readFileSync(__dirname + '/complex.dust').toString(),
+  eco: fs.readFileSync(__dirname + '/complex.eco').toString(),
+  mustache: fs.readFileSync(__dirname + '/complex.mustache').toString()
+};
diff --git a/bench/templates/complex.mustache b/bench/templates/complex.mustache
new file mode 100644
index 000000000..9e425872f
--- /dev/null
+++ b/bench/templates/complex.mustache
@@ -0,0 +1,13 @@
+<h1>{{header}}</h1>
+{{#hasItems}}
+  <ul>
+    {{#items}}
+      {{#current}}
+        <li><strong>{{name}}</strong></li>
+      {{/current}}
+      {{^current}}
+        <li><a href="{{url}}">{{name}}</a></li>
+      {{/current}}
+    {{/items}}
+  </ul>
+{{/hasItems}}
diff --git a/bench/templates/data.js b/bench/templates/data.js
new file mode 100644
index 000000000..f532decd5
--- /dev/null
+++ b/bench/templates/data.js
@@ -0,0 +1,4 @@
+module.exports = {
+  context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
+  handlebars: "{{#each names}}{{@index}}{{name}}{{/each}}"
+}
diff --git a/bench/templates/index.js b/bench/templates/index.js
new file mode 100644
index 000000000..a718ea388
--- /dev/null
+++ b/bench/templates/index.js
@@ -0,0 +1,9 @@
+var fs = require('fs');
+
+var templates = fs.readdirSync(__dirname);
+templates.forEach(function(template) {
+  if (template === 'index.js' || !/(.*)\.js$/.test(template)) {
+    return;
+  }
+  module.exports[RegExp.$1] = require('./' + RegExp.$1);
+});
diff --git a/bench/templates/object-mustache.js b/bench/templates/object-mustache.js
new file mode 100644
index 000000000..52dbc26e3
--- /dev/null
+++ b/bench/templates/object-mustache.js
@@ -0,0 +1,4 @@
+module.exports = {
+  context: { person: { name: "Larry", age: 45 } },
+  handlebars: "{{#person}}{{name}}{{age}}{{/person}}"
+};
diff --git a/bench/templates/object.js b/bench/templates/object.js
new file mode 100644
index 000000000..fef127ada
--- /dev/null
+++ b/bench/templates/object.js
@@ -0,0 +1,7 @@
+module.exports = {
+  context: { person: { name: "Larry", age: 45 } },
+  handlebars: "{{#with person}}{{name}}{{age}}{{/with}}",
+  dust: "{#person}{name}{age}{/person}",
+  eco: "<%= @person.name %><%= @person.age %>",
+  mustache: "{{#person}}{{name}}{{age}}{{/person}}"
+};
diff --git a/bench/templates/partial-recursion.js b/bench/templates/partial-recursion.js
new file mode 100644
index 000000000..9d604fd02
--- /dev/null
+++ b/bench/templates/partial-recursion.js
@@ -0,0 +1,10 @@
+module.exports = {
+  context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] },
+  partials: {
+    mustache: { recursion: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" },
+    handlebars: { recursion: "{{name}}{{#each kids}}{{>recursion}}{{/each}}" }
+  },
+  handlebars: "{{name}}{{#each kids}}{{>recursion}}{{/each}}",
+  dust: "{name}{#kids}{>recursion:./}{/kids}",
+  mustache: "{{name}}{{#kids}}{{>recursion}}{{/kids}}"
+};
diff --git a/bench/templates/partial.js b/bench/templates/partial.js
new file mode 100644
index 000000000..a6e663156
--- /dev/null
+++ b/bench/templates/partial.js
@@ -0,0 +1,11 @@
+module.exports = {
+  context: { peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 1}] },
+  partials: {
+    mustache: { variables: "Hello {{name}}! You have {{count}} new messages." },
+    handlebars: { variables: "Hello {{name}}! You have {{count}} new messages." }
+  },
+
+  handlebars: "{{#each peeps}}{{>variables}}{{/each}}",
+  dust: "{#peeps}{>variables/}{/peeps}",
+  mustache: "{{#peeps}}{{>variables}}{{/peeps}}"
+};
diff --git a/bench/templates/paths.js b/bench/templates/paths.js
new file mode 100644
index 000000000..0a426dd83
--- /dev/null
+++ b/bench/templates/paths.js
@@ -0,0 +1,7 @@
+module.exports = {
+  context: { person: { name: "Larry", age: 45 } },
+  handlebars: "{{person.name}}{{person.age}}{{person.foo}}{{animal.age}}",
+  dust: "{person.name}{person.age}{person.foo}{animal.age}",
+  eco: "<%= @person.name %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>",
+  mustache: "{{person.name}}{{person.age}}{{person.foo}}{{animal.age}}"
+};
diff --git a/bench/templates/string.js b/bench/templates/string.js
new file mode 100644
index 000000000..335e37cf9
--- /dev/null
+++ b/bench/templates/string.js
@@ -0,0 +1,7 @@
+module.exports = {
+  context: {},
+  handlebars: "Hello world",
+  dust: "Hello world",
+  mustache: "Hello world",
+  eco: "Hello world"
+};
diff --git a/bench/templates/variables.js b/bench/templates/variables.js
new file mode 100644
index 000000000..d354238b1
--- /dev/null
+++ b/bench/templates/variables.js
@@ -0,0 +1,8 @@
+module.exports = {
+  context: {name: "Mick", count: 30},
+  handlebars:  "Hello {{name}}! You have {{count}} new messages.",
+  dust: "Hello {name}! You have {count} new messages.",
+  mustache: "Hello {{name}}! You have {{count}} new messages.",
+  eco: "Hello <%= @name %>! You have <%= @count %> new messages."
+};
+
diff --git a/bench/throughput.js b/bench/throughput.js
new file mode 100644
index 000000000..308446a09
--- /dev/null
+++ b/bench/throughput.js
@@ -0,0 +1,123 @@
+var _ = require('underscore'),
+    runner = require('./util/template-runner'),
+    templates = require('./templates'),
+
+    eco, dust, Handlebars, Mustache, eco;
+
+try {
+  dust = require("dustjs-linkedin");
+} catch (err) { /* NOP */ }
+
+try {
+  Mustache = require("mustache");
+} catch (err) { /* NOP */ }
+
+try {
+  eco = require("eco");
+} catch (err) { /* NOP */ }
+
+function error() {
+  throw new Error("EWOT");
+}
+
+function makeSuite(bench, name, template, handlebarsOnly) {
+  // Create aliases to minimize any impact from having to walk up the closure tree.
+  var templateName = name,
+
+      context = template.context,
+      partials = template.partials,
+
+      handlebarsOut,
+      dustOut,
+      ecoOut,
+      mustacheOut;
+
+  var handlebar = Handlebars.compile(template.handlebars, {data: false}),
+      options = {helpers: template.helpers};
+  _.each(template.partials && template.partials.handlebars, function(partial, name) {
+    Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false}));
+  });
+
+  handlebarsOut = handlebar(context, options);
+  bench("handlebars", function() {
+    handlebar(context, options);
+  });
+
+  if (handlebarsOnly) {
+    return;
+  }
+
+  if (dust) {
+    if (template.dust) {
+      dustOut = false;
+      dust.loadSource(dust.compile(template.dust, templateName));
+
+      dust.render(templateName, context, function(err, out) { dustOut = out; });
+
+      bench("dust", function() {
+        dust.render(templateName, context, function(err, out) { });
+      });
+    } else {
+      bench('dust', error);
+    }
+  }
+
+  if (eco) {
+    if (template.eco) {
+      var ecoTemplate = eco.compile(template.eco);
+
+      ecoOut = ecoTemplate(context);
+
+      bench("eco", function() {
+        ecoTemplate(context);
+      });
+    } else {
+      bench("eco", error);
+    }
+  }
+
+  if (Mustache) {
+    var mustacheSource = template.mustache,
+        mustachePartials = partials && partials.mustache;
+
+    if (mustacheSource) {
+      mustacheOut = Mustache.to_html(mustacheSource, context, mustachePartials);
+
+      bench("mustache", function() {
+        Mustache.to_html(mustacheSource, context, mustachePartials);
+      });
+    } else {
+      bench("mustache", error);
+    }
+  }
+
+  // Hack around whitespace until we have whitespace control
+  handlebarsOut = handlebarsOut.replace(/\s/g, '');
+  function compare(b, lang) {
+    if (b == null) {
+      return;
+    }
+
+    b = b.replace(/\s/g, '');
+
+    if (handlebarsOut !== b) {
+      throw new Error('Template output mismatch: ' + name
+            + '\n\nHandlebars: ' + handlebarsOut
+            + '\n\n' + lang + ': ' + b);
+    }
+  }
+
+  compare(dustOut, 'dust');
+  compare(ecoOut, 'eco');
+  compare(mustacheOut, 'mustache');
+}
+
+module.exports = function(grunt, callback) {
+  // Deferring load incase we are being run inline with the grunt build
+  Handlebars = require('../lib');
+
+  console.log('Execution Throughput');
+  runner(grunt, makeSuite, function(times, scaled) {
+    callback(scaled);
+  });
+};
diff --git a/bench/benchwarmer.js b/bench/util/benchwarmer.js
similarity index 50%
rename from bench/benchwarmer.js
rename to bench/util/benchwarmer.js
index 25a250619..7496a3e9e 100644
--- a/bench/benchwarmer.js
+++ b/bench/util/benchwarmer.js
@@ -1,10 +1,13 @@
-
-var Benchmark = require("benchmark");
+var _ = require('underscore'),
+    Benchmark = require("benchmark");
 
 var BenchWarmer = function(names) {
   this.benchmarks = [];
   this.currentBenches = [];
   this.names = [];
+  this.times = {};
+  this.minimum = Infinity;
+  this.maximum = -Infinity;
   this.errors = {};
 };
 
@@ -12,28 +15,11 @@ var print = require("sys").print;
 
 BenchWarmer.prototype = {
   winners: function(benches) {
-    var result = Benchmark.filter(benches, function(bench) { return bench.cycles; });
-
-    if (result.length > 1) {
-      result.sort(function(a, b) { return b.compare(a); });
-      first = result[0];
-      last  = result[result.length - 1];
-
-      var winners = [];
-
-      Benchmark.each(result, function(bench) {
-        if (bench.compare(first) === 0) {
-          winners.push(bench);
-        }
-      });
-
-      return winners;
-    } else {
-      return result;
-    }
+    return Benchmark.filter(benches, 'fastest');
   },
   suite: function(suite, fn) {
     this.suiteName = suite;
+    this.times[suite] = {};
     this.first = true;
 
     var self = this;
@@ -50,9 +36,7 @@ BenchWarmer.prototype = {
     var first = this.first, suiteName = this.suiteName, self = this;
     this.first = false;
 
-    var bench = new Benchmark(function() {
-      fn();
-    }, {
+    var bench = new Benchmark(fn, {
       name: this.suiteName + ": " + name,
       onComplete: function() {
         if(first) { self.startLine(suiteName); }
@@ -62,44 +46,49 @@ BenchWarmer.prototype = {
         self.errors[this.name] = this;
       }
     });
+    bench.suiteName = this.suiteName;
+    bench.benchName = name;
 
     this.benchmarks.push(bench);
   },
-  bench: function() {
-    var benchSize = 0, names = this.names, self = this, i, l;
 
-    for(i=0, l=names.length; i<l; i++) {
-      var name = names[i];
+  bench: function(callback) {
+    var self = this;
 
-      if(benchSize < name.length) { benchSize = name.length; }
-    }
+    this.printHeader('ops/msec', true);
 
-    this.nameSize = benchSize + 2;
-    this.benchSize = 20;
-    var horSize = 0;
+    Benchmark.invoke(this.benchmarks, {
+      name: "run",
+      onComplete: function() {
+        self.scaleTimes();
 
-    this.startLine("ops/msec");
-    horSize = horSize + "ops/msec    ".length;
-    for(i=0, l=names.length; i<l; i++) {
-      print(names[i] + new Array(this.benchSize - names[i].length + 1).join(" "));
-      horSize = horSize + this.benchSize;
-    }
+        self.startLine('');
 
-    print("WINNER(S)");
-    horSize = horSize + "WINNER(S)".length;
+        print('\n');
+        self.printHeader('scaled');
+        _.each(self.scaled, function(value, name) {
+          self.startLine(name);
 
-    print("\n" + new Array(horSize + 1).join("-"));
+          _.each(self.names, function(lang) {
+            self.writeValue(value[lang] || '');
+          });
+        });
+        print('\n');
 
-    Benchmark.invoke(this.benchmarks, {
-      name: "run",
-      onComplete: function() {
         var errors = false, prop, bench;
-        for(prop in self.errors) { if(self.errors.hasOwnProperty(prop)) { errors = true; break; } }
+        for(prop in self.errors) {
+          if (self.errors.hasOwnProperty(prop)
+              && self.errors[prop].error.message !== 'EWOT') {
+            errors = true;
+            break;
+          }
+        }
 
         if(errors) {
           print("\n\nErrors:\n");
           for(prop in self.errors) {
-            if(self.errors.hasOwnProperty(prop)) {
+            if (self.errors.hasOwnProperty(prop)
+                && self.errors[prop].error.message !== 'EWOT') {
               bench = self.errors[prop];
               print("\n" + bench.name + ":\n");
               print(bench.error.message);
@@ -110,11 +99,53 @@ BenchWarmer.prototype = {
             }
           }
         }
+
+        callback();
       }
     });
 
     print("\n");
   },
+
+  scaleTimes: function() {
+    var scaled = this.scaled = {};
+    _.each(this.times, function(times, name) {
+      var output = scaled[name] = {};
+
+      _.each(times, function(time, lang) {
+        output[lang] = ((time - this.minimum) / (this.maximum - this.minimum) * 100).toFixed(2);
+      }, this);
+    }, this);
+  },
+
+  printHeader: function(title, winners) {
+    var benchSize = 0, names = this.names, i, l;
+
+    for(i=0, l=names.length; i<l; i++) {
+      var name = names[i];
+
+      if(benchSize < name.length) { benchSize = name.length; }
+    }
+
+    this.nameSize = benchSize + 2;
+    this.benchSize = 20;
+    var horSize = 0;
+
+    this.startLine(title);
+    horSize = horSize + this.benchSize;
+    for(i=0, l=names.length; i<l; i++) {
+      this.writeValue(names[i]);
+      horSize = horSize + this.benchSize;
+    }
+
+    if (winners) {
+      print("WINNER(S)");
+      horSize = horSize + "WINNER(S)".length;
+    }
+
+    print("\n" + new Array(horSize + 1).join("-"));
+  },
+
   startLine: function(name) {
     var winners = Benchmark.map(this.winners(this.currentBenches), function(bench) {
       return bench.name.split(": ")[1];
@@ -124,22 +155,41 @@ BenchWarmer.prototype = {
 
     print(winners.join(", "));
     print("\n");
-    var padding = this.nameSize - name.length + 1;
-    name = name + new Array(padding).join(" ");
-    print(name);
+
+    if (name) {
+      this.writeValue(name);
+    }
   },
   writeBench: function(bench) {
     var out;
 
     if(!bench.error) {
       var count = bench.hz,
-          moe   = count * bench.stats.rme / 100;
+          moe   = count * bench.stats.rme / 100,
+          minimum,
+          maximum;
+
+      count = Math.round(count / 1000);
+      moe = Math.round(moe / 1000);
+      minimum = count - moe;
+      maximum = count + moe;
 
-      out = Math.round(count / 1000) + " ±" + Math.round(moe / 1000) + " (" + bench.cycles + ")";
+      out = count + " ±" + moe + " (" + bench.cycles + ")";
+
+      this.times[bench.suiteName][bench.benchName] = count;
+      this.minimum = Math.min(this.minimum, minimum);
+      this.maximum = Math.max(this.maximum, maximum);
     } else {
-      out = "E";
+      if (bench.error.message === 'EWOT') {
+        out = 'NA';
+      } else {
+        out = 'E';
+      }
     }
+    this.writeValue(out);
+  },
 
+  writeValue: function(out) {
     var padding = this.benchSize - out.length + 1;
     out = out + new Array(padding).join(" ");
     print(out);
diff --git a/bench/util/template-runner.js b/bench/util/template-runner.js
new file mode 100644
index 000000000..8863ccd6f
--- /dev/null
+++ b/bench/util/template-runner.js
@@ -0,0 +1,27 @@
+var _ = require('underscore'),
+    BenchWarmer = require('./benchwarmer'),
+    templates = require('../templates');
+
+module.exports = function(grunt, makeSuite, callback) {
+  var warmer = new BenchWarmer();
+
+  var handlebarsOnly = grunt.option('handlebars-only'),
+      grep = grunt.option('grep');
+  if (grep) {
+    grep = new RegExp(grep);
+  }
+
+  _.each(templates, function(template, name) {
+    if (!template.handlebars || (grep && !grep.test(name))) {
+      return;
+    }
+
+    warmer.suite(name, function(bench) {
+      makeSuite(bench, name, template, handlebarsOnly);
+    });
+  });
+
+  warmer.bench(function() {
+    callback && callback(warmer.times, warmer.scaled);
+  });
+};
diff --git a/bin/handlebars b/bin/handlebars
index 4835770f9..8ab6afda8 100755
--- a/bin/handlebars
+++ b/bin/handlebars
@@ -70,6 +70,11 @@ var optimist = require('optimist')
         'description': 'Template extension.',
         'alias': 'extension',
         'default': 'handlebars'
+      },
+      'b': {
+        'type': 'boolean',
+        'description': 'Removes the BOM (Byte Order Mark) from the beginning of the templates.',
+        'alias': 'bom'
       }
     })
 
@@ -122,7 +127,7 @@ extension = new RegExp('\\.' + extension + '$');
 var output = [];
 if (!argv.simple) {
   if (argv.amd) {
-    output.push('define([\'' + argv.handlebarPath + 'handlebars\'], function(Handlebars) {\n');
+    output.push('define([\'' + argv.handlebarPath + 'handlebars\'], function(Handlebars) {\n  Handlebars = Handlebars["default"];');
   } else if (argv.commonjs) {
     output.push('var Handlebars = require("' + argv.commonjs + '");');
   } else {
@@ -147,6 +152,10 @@ function processTemplate(template, root) {
     });
   } else {
     var data = fs.readFileSync(path, 'utf8');
+    
+    if (argv.bom && data.indexOf('\uFEFF') === 0) {
+      data = data.substring(1);
+    }
 
     var options = {
       knownHelpers: known,
diff --git a/bower.json b/bower.json
deleted file mode 100644
index 4b86a80a4..000000000
--- a/bower.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "name": "handlebars.js",
-  "version": "1.0.0",
-  "main": "dist/handlebars.js",
-  "ignore": [
-    "node_modules",
-    "components"
-  ]
-}
diff --git a/components/bower.json b/components/bower.json
new file mode 100644
index 000000000..ee9011dd5
--- /dev/null
+++ b/components/bower.json
@@ -0,0 +1,6 @@
+{
+  "name": "handlebars",
+  "version": "1.1.0",
+  "main": "handlebars.js",
+  "dependencies": {}
+}
diff --git a/components/component.json b/components/component.json
new file mode 100644
index 000000000..f74c83962
--- /dev/null
+++ b/components/component.json
@@ -0,0 +1,9 @@
+{
+  "name": "handlebars",
+  "repo": "components/handlebars.js",
+  "version": "1.0.0",
+  "main": "handlebars.js",
+  "scripts": [
+    "handlebars.js"
+  ]
+}
diff --git a/components/composer.json b/components/composer.json
new file mode 100644
index 000000000..c5dc4271f
--- /dev/null
+++ b/components/composer.json
@@ -0,0 +1,35 @@
+{
+    "name": "components/handlebars.js",
+    "description": "Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.",
+    "homepage": "http://handlebarsjs.com",
+    "license": "MIT",
+    "type": "component",
+    "keywords": [
+        "handlebars",
+        "mustache",
+        "html"
+    ],
+    "authors": [
+        {
+            "name": "Chris Wanstrath",
+            "homepage": "http://chriswanstrath.com"
+        }
+    ],
+    "require": {
+        "robloach/component-installer": "*"
+    },
+    "extra": {
+        "component": {
+            "name": "handlebars",
+            "scripts": [
+                "handlebars.js"
+            ],
+            "files": [
+                "handlebars.runtime.js"
+            ],
+            "shim": {
+                "exports": "Handlebars"
+            }
+        }
+    }
+}
diff --git a/handlebars-source.gemspec b/components/handlebars-source.gemspec
similarity index 83%
rename from handlebars-source.gemspec
rename to components/handlebars-source.gemspec
index 514e2b1e4..4313e9581 100644
--- a/handlebars-source.gemspec
+++ b/components/handlebars-source.gemspec
@@ -1,7 +1,7 @@
 # -*- encoding: utf-8 -*-
 require 'json'
 
-package = JSON.parse(File.read('package.json'))
+package = JSON.parse(File.read('bower.json'))
 
 Gem::Specification.new do |gem|
   gem.name          = "handlebars-source"
@@ -14,8 +14,8 @@ Gem::Specification.new do |gem|
   gem.version       = package["version"]
 
   gem.files = [
-    'dist/handlebars.js',
-    'dist/handlebars.runtime.js',
+    'handlebars.js',
+    'handlebars.runtime.js',
     'lib/handlebars/source.rb'
   ]
 end
diff --git a/handlebars.js.nuspec b/components/handlebars.js.nuspec
similarity index 85%
rename from handlebars.js.nuspec
rename to components/handlebars.js.nuspec
index 4515f2ebd..07d097c73 100644
--- a/handlebars.js.nuspec
+++ b/components/handlebars.js.nuspec
@@ -2,7 +2,7 @@
 <package>
 	<metadata>
 		<id>handlebars.js</id>
-		<version>1.0.0</version>
+		<version>1.1.0</version>
 		<authors>handlebars.js Authors</authors>
 		<licenseUrl>https://github.com/wycats/handlebars.js/blob/master/LICENSE</licenseUrl>
 		<projectUrl>https://github.com/wycats/handlebars.js/</projectUrl>
@@ -12,6 +12,6 @@
 		<tags>handlebars mustache template html</tags>
 	</metadata>
 	<files>
-		<file src="dist\handlebars.js" target="Content\Scripts" />
+		<file src="handlebars.js" target="Content\Scripts" />
 	</files>
 </package>
diff --git a/components/lib/handlebars/source.rb b/components/lib/handlebars/source.rb
new file mode 100644
index 000000000..4f3f8bfce
--- /dev/null
+++ b/components/lib/handlebars/source.rb
@@ -0,0 +1,11 @@
+module Handlebars
+  module Source
+    def self.bundled_path
+      File.expand_path("../../../handlebars.js", __FILE__)
+    end
+
+    def self.runtime_bundled_path
+      File.expand_path("../../../handlebars.runtime.js", __FILE__)
+    end
+  end
+end
diff --git a/dist/handlebars.js b/dist/handlebars.js
deleted file mode 100644
index c70f09d1d..000000000
--- a/dist/handlebars.js
+++ /dev/null
@@ -1,2278 +0,0 @@
-/*
-
-Copyright (C) 2011 by Yehuda Katz
-
-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.
-
-*/
-
-// lib/handlebars/browser-prefix.js
-var Handlebars = {};
-
-(function(Handlebars, undefined) {
-;
-// lib/handlebars/base.js
-
-Handlebars.VERSION = "1.0.0";
-Handlebars.COMPILER_REVISION = 4;
-
-Handlebars.REVISION_CHANGES = {
-  1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
-  2: '== 1.0.0-rc.3',
-  3: '== 1.0.0-rc.4',
-  4: '>= 1.0.0'
-};
-
-Handlebars.helpers  = {};
-Handlebars.partials = {};
-
-var toString = Object.prototype.toString,
-    functionType = '[object Function]',
-    objectType = '[object Object]';
-
-Handlebars.registerHelper = function(name, fn, inverse) {
-  if (toString.call(name) === objectType) {
-    if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); }
-    Handlebars.Utils.extend(this.helpers, name);
-  } else {
-    if (inverse) { fn.not = inverse; }
-    this.helpers[name] = fn;
-  }
-};
-
-Handlebars.registerPartial = function(name, str) {
-  if (toString.call(name) === objectType) {
-    Handlebars.Utils.extend(this.partials,  name);
-  } else {
-    this.partials[name] = str;
-  }
-};
-
-Handlebars.registerHelper('helperMissing', function(arg) {
-  if(arguments.length === 2) {
-    return undefined;
-  } else {
-    throw new Error("Missing helper: '" + arg + "'");
-  }
-});
-
-Handlebars.registerHelper('blockHelperMissing', function(context, options) {
-  var inverse = options.inverse || function() {}, fn = options.fn;
-
-  var type = toString.call(context);
-
-  if(type === functionType) { context = context.call(this); }
-
-  if(context === true) {
-    return fn(this);
-  } else if(context === false || context == null) {
-    return inverse(this);
-  } else if(type === "[object Array]") {
-    if(context.length > 0) {
-      return Handlebars.helpers.each(context, options);
-    } else {
-      return inverse(this);
-    }
-  } else {
-    return fn(context);
-  }
-});
-
-Handlebars.K = function() {};
-
-Handlebars.createFrame = Object.create || function(object) {
-  Handlebars.K.prototype = object;
-  var obj = new Handlebars.K();
-  Handlebars.K.prototype = null;
-  return obj;
-};
-
-Handlebars.logger = {
-  DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
-
-  methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'},
-
-  // can be overridden in the host environment
-  log: function(level, obj) {
-    if (Handlebars.logger.level <= level) {
-      var method = Handlebars.logger.methodMap[level];
-      if (typeof console !== 'undefined' && console[method]) {
-        console[method].call(console, obj);
-      }
-    }
-  }
-};
-
-Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); };
-
-Handlebars.registerHelper('each', function(context, options) {
-  var fn = options.fn, inverse = options.inverse;
-  var i = 0, ret = "", data;
-
-  var type = toString.call(context);
-  if(type === functionType) { context = context.call(this); }
-
-  if (options.data) {
-    data = Handlebars.createFrame(options.data);
-  }
-
-  if(context && typeof context === 'object') {
-    if(context instanceof Array){
-      for(var j = context.length; i<j; i++) {
-        if (data) { data.index = i; }
-        ret = ret + fn(context[i], { data: data });
-      }
-    } else {
-      for(var key in context) {
-        if(context.hasOwnProperty(key)) {
-          if(data) { data.key = key; }
-          ret = ret + fn(context[key], {data: data});
-          i++;
-        }
-      }
-    }
-  }
-
-  if(i === 0){
-    ret = inverse(this);
-  }
-
-  return ret;
-});
-
-Handlebars.registerHelper('if', function(conditional, options) {
-  var type = toString.call(conditional);
-  if(type === functionType) { conditional = conditional.call(this); }
-
-  if(!conditional || Handlebars.Utils.isEmpty(conditional)) {
-    return options.inverse(this);
-  } else {
-    return options.fn(this);
-  }
-});
-
-Handlebars.registerHelper('unless', function(conditional, options) {
-  return Handlebars.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn});
-});
-
-Handlebars.registerHelper('with', function(context, options) {
-  var type = toString.call(context);
-  if(type === functionType) { context = context.call(this); }
-
-  if (!Handlebars.Utils.isEmpty(context)) return options.fn(context);
-});
-
-Handlebars.registerHelper('log', function(context, options) {
-  var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
-  Handlebars.log(level, context);
-});
-;
-// lib/handlebars/compiler/parser.js
-/* Jison generated parser */
-var handlebars = (function(){
-var parser = {trace: function trace() { },
-yy: {},
-symbols_: {"error":2,"root":3,"program":4,"EOF":5,"simpleInverse":6,"statements":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"CLOSE_UNESCAPED":24,"OPEN_PARTIAL":25,"partialName":26,"params":27,"hash":28,"dataName":29,"param":30,"STRING":31,"INTEGER":32,"BOOLEAN":33,"hashSegments":34,"hashSegment":35,"ID":36,"EQUALS":37,"DATA":38,"pathSegments":39,"SEP":40,"$accept":0,"$end":1},
-terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"CLOSE_UNESCAPED",25:"OPEN_PARTIAL",31:"STRING",32:"INTEGER",33:"BOOLEAN",36:"ID",37:"EQUALS",38:"DATA",40:"SEP"},
-productions_: [0,[3,2],[4,2],[4,3],[4,2],[4,1],[4,1],[4,0],[7,1],[7,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[6,2],[17,3],[17,2],[17,2],[17,1],[17,1],[27,2],[27,1],[30,1],[30,1],[30,1],[30,1],[30,1],[28,1],[34,2],[34,1],[35,3],[35,3],[35,3],[35,3],[35,3],[26,1],[26,1],[26,1],[29,2],[21,1],[39,3],[39,1]],
-performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) {
-
-var $0 = $$.length - 1;
-switch (yystate) {
-case 1: return $$[$0-1]; 
-break;
-case 2: this.$ = new yy.ProgramNode([], $$[$0]); 
-break;
-case 3: this.$ = new yy.ProgramNode($$[$0-2], $$[$0]); 
-break;
-case 4: this.$ = new yy.ProgramNode($$[$0-1], []); 
-break;
-case 5: this.$ = new yy.ProgramNode($$[$0]); 
-break;
-case 6: this.$ = new yy.ProgramNode([], []); 
-break;
-case 7: this.$ = new yy.ProgramNode([]); 
-break;
-case 8: this.$ = [$$[$0]]; 
-break;
-case 9: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; 
-break;
-case 10: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1].inverse, $$[$0-1], $$[$0]); 
-break;
-case 11: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1], $$[$0-1].inverse, $$[$0]); 
-break;
-case 12: this.$ = $$[$0]; 
-break;
-case 13: this.$ = $$[$0]; 
-break;
-case 14: this.$ = new yy.ContentNode($$[$0]); 
-break;
-case 15: this.$ = new yy.CommentNode($$[$0]); 
-break;
-case 16: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]); 
-break;
-case 17: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]); 
-break;
-case 18: this.$ = $$[$0-1]; 
-break;
-case 19:
-    // Parsing out the '&' escape token at this level saves ~500 bytes after min due to the removal of one parser node.
-    this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2][2] === '&');
-  
-break;
-case 20: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], true); 
-break;
-case 21: this.$ = new yy.PartialNode($$[$0-1]); 
-break;
-case 22: this.$ = new yy.PartialNode($$[$0-2], $$[$0-1]); 
-break;
-case 23: 
-break;
-case 24: this.$ = [[$$[$0-2]].concat($$[$0-1]), $$[$0]]; 
-break;
-case 25: this.$ = [[$$[$0-1]].concat($$[$0]), null]; 
-break;
-case 26: this.$ = [[$$[$0-1]], $$[$0]]; 
-break;
-case 27: this.$ = [[$$[$0]], null]; 
-break;
-case 28: this.$ = [[$$[$0]], null]; 
-break;
-case 29: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; 
-break;
-case 30: this.$ = [$$[$0]]; 
-break;
-case 31: this.$ = $$[$0]; 
-break;
-case 32: this.$ = new yy.StringNode($$[$0]); 
-break;
-case 33: this.$ = new yy.IntegerNode($$[$0]); 
-break;
-case 34: this.$ = new yy.BooleanNode($$[$0]); 
-break;
-case 35: this.$ = $$[$0]; 
-break;
-case 36: this.$ = new yy.HashNode($$[$0]); 
-break;
-case 37: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; 
-break;
-case 38: this.$ = [$$[$0]]; 
-break;
-case 39: this.$ = [$$[$0-2], $$[$0]]; 
-break;
-case 40: this.$ = [$$[$0-2], new yy.StringNode($$[$0])]; 
-break;
-case 41: this.$ = [$$[$0-2], new yy.IntegerNode($$[$0])]; 
-break;
-case 42: this.$ = [$$[$0-2], new yy.BooleanNode($$[$0])]; 
-break;
-case 43: this.$ = [$$[$0-2], $$[$0]]; 
-break;
-case 44: this.$ = new yy.PartialNameNode($$[$0]); 
-break;
-case 45: this.$ = new yy.PartialNameNode(new yy.StringNode($$[$0])); 
-break;
-case 46: this.$ = new yy.PartialNameNode(new yy.IntegerNode($$[$0])); 
-break;
-case 47: this.$ = new yy.DataNode($$[$0]); 
-break;
-case 48: this.$ = new yy.IdNode($$[$0]); 
-break;
-case 49: $$[$0-2].push({part: $$[$0], separator: $$[$0-1]}); this.$ = $$[$0-2]; 
-break;
-case 50: this.$ = [{part: $$[$0]}]; 
-break;
-}
-},
-table: [{3:1,4:2,5:[2,7],6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],22:[1,14],23:[1,15],25:[1,16]},{1:[3]},{5:[1,17]},{5:[2,6],7:18,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,6],22:[1,14],23:[1,15],25:[1,16]},{5:[2,5],6:20,8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,5],22:[1,14],23:[1,15],25:[1,16]},{17:23,18:[1,22],21:24,29:25,36:[1,28],38:[1,27],39:26},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],25:[2,8]},{4:29,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],25:[1,16]},{4:30,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],25:[1,16]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],25:[2,12]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],25:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],25:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],25:[2,15]},{17:31,21:24,29:25,36:[1,28],38:[1,27],39:26},{17:32,21:24,29:25,36:[1,28],38:[1,27],39:26},{17:33,21:24,29:25,36:[1,28],38:[1,27],39:26},{21:35,26:34,31:[1,36],32:[1,37],36:[1,28],39:26},{1:[2,1]},{5:[2,2],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,2],22:[1,14],23:[1,15],25:[1,16]},{17:23,21:24,29:25,36:[1,28],38:[1,27],39:26},{5:[2,4],7:38,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,4],22:[1,14],23:[1,15],25:[1,16]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],25:[2,9]},{5:[2,23],14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],25:[2,23]},{18:[1,39]},{18:[2,27],21:44,24:[2,27],27:40,28:41,29:48,30:42,31:[1,45],32:[1,46],33:[1,47],34:43,35:49,36:[1,50],38:[1,27],39:26},{18:[2,28],24:[2,28]},{18:[2,48],24:[2,48],31:[2,48],32:[2,48],33:[2,48],36:[2,48],38:[2,48],40:[1,51]},{21:52,36:[1,28],39:26},{18:[2,50],24:[2,50],31:[2,50],32:[2,50],33:[2,50],36:[2,50],38:[2,50],40:[2,50]},{10:53,20:[1,54]},{10:55,20:[1,54]},{18:[1,56]},{18:[1,57]},{24:[1,58]},{18:[1,59],21:60,36:[1,28],39:26},{18:[2,44],36:[2,44]},{18:[2,45],36:[2,45]},{18:[2,46],36:[2,46]},{5:[2,3],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,3],22:[1,14],23:[1,15],25:[1,16]},{14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],25:[2,17]},{18:[2,25],21:44,24:[2,25],28:61,29:48,30:62,31:[1,45],32:[1,46],33:[1,47],34:43,35:49,36:[1,50],38:[1,27],39:26},{18:[2,26],24:[2,26]},{18:[2,30],24:[2,30],31:[2,30],32:[2,30],33:[2,30],36:[2,30],38:[2,30]},{18:[2,36],24:[2,36],35:63,36:[1,64]},{18:[2,31],24:[2,31],31:[2,31],32:[2,31],33:[2,31],36:[2,31],38:[2,31]},{18:[2,32],24:[2,32],31:[2,32],32:[2,32],33:[2,32],36:[2,32],38:[2,32]},{18:[2,33],24:[2,33],31:[2,33],32:[2,33],33:[2,33],36:[2,33],38:[2,33]},{18:[2,34],24:[2,34],31:[2,34],32:[2,34],33:[2,34],36:[2,34],38:[2,34]},{18:[2,35],24:[2,35],31:[2,35],32:[2,35],33:[2,35],36:[2,35],38:[2,35]},{18:[2,38],24:[2,38],36:[2,38]},{18:[2,50],24:[2,50],31:[2,50],32:[2,50],33:[2,50],36:[2,50],37:[1,65],38:[2,50],40:[2,50]},{36:[1,66]},{18:[2,47],24:[2,47],31:[2,47],32:[2,47],33:[2,47],36:[2,47],38:[2,47]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],25:[2,10]},{21:67,36:[1,28],39:26},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],25:[2,11]},{14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],25:[2,16]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],25:[2,19]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],25:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],25:[2,21]},{18:[1,68]},{18:[2,24],24:[2,24]},{18:[2,29],24:[2,29],31:[2,29],32:[2,29],33:[2,29],36:[2,29],38:[2,29]},{18:[2,37],24:[2,37],36:[2,37]},{37:[1,65]},{21:69,29:73,31:[1,70],32:[1,71],33:[1,72],36:[1,28],38:[1,27],39:26},{18:[2,49],24:[2,49],31:[2,49],32:[2,49],33:[2,49],36:[2,49],38:[2,49],40:[2,49]},{18:[1,74]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],25:[2,22]},{18:[2,39],24:[2,39],36:[2,39]},{18:[2,40],24:[2,40],36:[2,40]},{18:[2,41],24:[2,41],36:[2,41]},{18:[2,42],24:[2,42],36:[2,42]},{18:[2,43],24:[2,43],36:[2,43]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],25:[2,18]}],
-defaultActions: {17:[2,1]},
-parseError: function parseError(str, hash) {
-    throw new Error(str);
-},
-parse: function parse(input) {
-    var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1;
-    this.lexer.setInput(input);
-    this.lexer.yy = this.yy;
-    this.yy.lexer = this.lexer;
-    this.yy.parser = this;
-    if (typeof this.lexer.yylloc == "undefined")
-        this.lexer.yylloc = {};
-    var yyloc = this.lexer.yylloc;
-    lstack.push(yyloc);
-    var ranges = this.lexer.options && this.lexer.options.ranges;
-    if (typeof this.yy.parseError === "function")
-        this.parseError = this.yy.parseError;
-    function popStack(n) {
-        stack.length = stack.length - 2 * n;
-        vstack.length = vstack.length - n;
-        lstack.length = lstack.length - n;
-    }
-    function lex() {
-        var token;
-        token = self.lexer.lex() || 1;
-        if (typeof token !== "number") {
-            token = self.symbols_[token] || token;
-        }
-        return token;
-    }
-    var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected;
-    while (true) {
-        state = stack[stack.length - 1];
-        if (this.defaultActions[state]) {
-            action = this.defaultActions[state];
-        } else {
-            if (symbol === null || typeof symbol == "undefined") {
-                symbol = lex();
-            }
-            action = table[state] && table[state][symbol];
-        }
-        if (typeof action === "undefined" || !action.length || !action[0]) {
-            var errStr = "";
-            if (!recovering) {
-                expected = [];
-                for (p in table[state])
-                    if (this.terminals_[p] && p > 2) {
-                        expected.push("'" + this.terminals_[p] + "'");
-                    }
-                if (this.lexer.showPosition) {
-                    errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'";
-                } else {
-                    errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'");
-                }
-                this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected});
-            }
-        }
-        if (action[0] instanceof Array && action.length > 1) {
-            throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol);
-        }
-        switch (action[0]) {
-        case 1:
-            stack.push(symbol);
-            vstack.push(this.lexer.yytext);
-            lstack.push(this.lexer.yylloc);
-            stack.push(action[1]);
-            symbol = null;
-            if (!preErrorSymbol) {
-                yyleng = this.lexer.yyleng;
-                yytext = this.lexer.yytext;
-                yylineno = this.lexer.yylineno;
-                yyloc = this.lexer.yylloc;
-                if (recovering > 0)
-                    recovering--;
-            } else {
-                symbol = preErrorSymbol;
-                preErrorSymbol = null;
-            }
-            break;
-        case 2:
-            len = this.productions_[action[1]][1];
-            yyval.$ = vstack[vstack.length - len];
-            yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column};
-            if (ranges) {
-                yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]];
-            }
-            r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack);
-            if (typeof r !== "undefined") {
-                return r;
-            }
-            if (len) {
-                stack = stack.slice(0, -1 * len * 2);
-                vstack = vstack.slice(0, -1 * len);
-                lstack = lstack.slice(0, -1 * len);
-            }
-            stack.push(this.productions_[action[1]][0]);
-            vstack.push(yyval.$);
-            lstack.push(yyval._$);
-            newState = table[stack[stack.length - 2]][stack[stack.length - 1]];
-            stack.push(newState);
-            break;
-        case 3:
-            return true;
-        }
-    }
-    return true;
-}
-};
-/* Jison generated lexer */
-var lexer = (function(){
-var lexer = ({EOF:1,
-parseError:function parseError(str, hash) {
-        if (this.yy.parser) {
-            this.yy.parser.parseError(str, hash);
-        } else {
-            throw new Error(str);
-        }
-    },
-setInput:function (input) {
-        this._input = input;
-        this._more = this._less = this.done = false;
-        this.yylineno = this.yyleng = 0;
-        this.yytext = this.matched = this.match = '';
-        this.conditionStack = ['INITIAL'];
-        this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0};
-        if (this.options.ranges) this.yylloc.range = [0,0];
-        this.offset = 0;
-        return this;
-    },
-input:function () {
-        var ch = this._input[0];
-        this.yytext += ch;
-        this.yyleng++;
-        this.offset++;
-        this.match += ch;
-        this.matched += ch;
-        var lines = ch.match(/(?:\r\n?|\n).*/g);
-        if (lines) {
-            this.yylineno++;
-            this.yylloc.last_line++;
-        } else {
-            this.yylloc.last_column++;
-        }
-        if (this.options.ranges) this.yylloc.range[1]++;
-
-        this._input = this._input.slice(1);
-        return ch;
-    },
-unput:function (ch) {
-        var len = ch.length;
-        var lines = ch.split(/(?:\r\n?|\n)/g);
-
-        this._input = ch + this._input;
-        this.yytext = this.yytext.substr(0, this.yytext.length-len-1);
-        //this.yyleng -= len;
-        this.offset -= len;
-        var oldLines = this.match.split(/(?:\r\n?|\n)/g);
-        this.match = this.match.substr(0, this.match.length-1);
-        this.matched = this.matched.substr(0, this.matched.length-1);
-
-        if (lines.length-1) this.yylineno -= lines.length-1;
-        var r = this.yylloc.range;
-
-        this.yylloc = {first_line: this.yylloc.first_line,
-          last_line: this.yylineno+1,
-          first_column: this.yylloc.first_column,
-          last_column: lines ?
-              (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length:
-              this.yylloc.first_column - len
-          };
-
-        if (this.options.ranges) {
-            this.yylloc.range = [r[0], r[0] + this.yyleng - len];
-        }
-        return this;
-    },
-more:function () {
-        this._more = true;
-        return this;
-    },
-less:function (n) {
-        this.unput(this.match.slice(n));
-    },
-pastInput:function () {
-        var past = this.matched.substr(0, this.matched.length - this.match.length);
-        return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
-    },
-upcomingInput:function () {
-        var next = this.match;
-        if (next.length < 20) {
-            next += this._input.substr(0, 20-next.length);
-        }
-        return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, "");
-    },
-showPosition:function () {
-        var pre = this.pastInput();
-        var c = new Array(pre.length + 1).join("-");
-        return pre + this.upcomingInput() + "\n" + c+"^";
-    },
-next:function () {
-        if (this.done) {
-            return this.EOF;
-        }
-        if (!this._input) this.done = true;
-
-        var token,
-            match,
-            tempMatch,
-            index,
-            col,
-            lines;
-        if (!this._more) {
-            this.yytext = '';
-            this.match = '';
-        }
-        var rules = this._currentRules();
-        for (var i=0;i < rules.length; i++) {
-            tempMatch = this._input.match(this.rules[rules[i]]);
-            if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
-                match = tempMatch;
-                index = i;
-                if (!this.options.flex) break;
-            }
-        }
-        if (match) {
-            lines = match[0].match(/(?:\r\n?|\n).*/g);
-            if (lines) this.yylineno += lines.length;
-            this.yylloc = {first_line: this.yylloc.last_line,
-                           last_line: this.yylineno+1,
-                           first_column: this.yylloc.last_column,
-                           last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length};
-            this.yytext += match[0];
-            this.match += match[0];
-            this.matches = match;
-            this.yyleng = this.yytext.length;
-            if (this.options.ranges) {
-                this.yylloc.range = [this.offset, this.offset += this.yyleng];
-            }
-            this._more = false;
-            this._input = this._input.slice(match[0].length);
-            this.matched += match[0];
-            token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]);
-            if (this.done && this._input) this.done = false;
-            if (token) return token;
-            else return;
-        }
-        if (this._input === "") {
-            return this.EOF;
-        } else {
-            return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(),
-                    {text: "", token: null, line: this.yylineno});
-        }
-    },
-lex:function lex() {
-        var r = this.next();
-        if (typeof r !== 'undefined') {
-            return r;
-        } else {
-            return this.lex();
-        }
-    },
-begin:function begin(condition) {
-        this.conditionStack.push(condition);
-    },
-popState:function popState() {
-        return this.conditionStack.pop();
-    },
-_currentRules:function _currentRules() {
-        return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules;
-    },
-topState:function () {
-        return this.conditionStack[this.conditionStack.length-2];
-    },
-pushState:function begin(condition) {
-        this.begin(condition);
-    }});
-lexer.options = {};
-lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
-
-var YYSTATE=YY_START
-switch($avoiding_name_collisions) {
-case 0: yy_.yytext = "\\"; return 14; 
-break;
-case 1:
-                                   if(yy_.yytext.slice(-1) !== "\\") this.begin("mu");
-                                   if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu");
-                                   if(yy_.yytext) return 14;
-                                 
-break;
-case 2: return 14; 
-break;
-case 3:
-                                   if(yy_.yytext.slice(-1) !== "\\") this.popState();
-                                   if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1);
-                                   return 14;
-                                 
-break;
-case 4: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15; 
-break;
-case 5: return 25; 
-break;
-case 6: return 16; 
-break;
-case 7: return 20; 
-break;
-case 8: return 19; 
-break;
-case 9: return 19; 
-break;
-case 10: return 23; 
-break;
-case 11: return 22; 
-break;
-case 12: this.popState(); this.begin('com'); 
-break;
-case 13: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; 
-break;
-case 14: return 22; 
-break;
-case 15: return 37; 
-break;
-case 16: return 36; 
-break;
-case 17: return 36; 
-break;
-case 18: return 40; 
-break;
-case 19: /*ignore whitespace*/ 
-break;
-case 20: this.popState(); return 24; 
-break;
-case 21: this.popState(); return 18; 
-break;
-case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 31; 
-break;
-case 23: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 31; 
-break;
-case 24: return 38; 
-break;
-case 25: return 33; 
-break;
-case 26: return 33; 
-break;
-case 27: return 32; 
-break;
-case 28: return 36; 
-break;
-case 29: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 36; 
-break;
-case 30: return 'INVALID'; 
-break;
-case 31: return 5; 
-break;
-}
-};
-lexer.rules = [/^(?:\\\\(?=(\{\{)))/,/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[}\/ ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:-?[0-9]+(?=[}\s]))/,/^(?:[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/];
-lexer.conditions = {"mu":{"rules":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"INITIAL":{"rules":[0,1,2,31],"inclusive":true}};
-return lexer;})()
-parser.lexer = lexer;
-function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser;
-return new Parser;
-})();;
-// lib/handlebars/compiler/base.js
-
-Handlebars.Parser = handlebars;
-
-Handlebars.parse = function(input) {
-
-  // Just return if an already-compile AST was passed in.
-  if(input.constructor === Handlebars.AST.ProgramNode) { return input; }
-
-  Handlebars.Parser.yy = Handlebars.AST;
-  return Handlebars.Parser.parse(input);
-};
-;
-// lib/handlebars/compiler/ast.js
-Handlebars.AST = {};
-
-Handlebars.AST.ProgramNode = function(statements, inverse) {
-  this.type = "program";
-  this.statements = statements;
-  if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); }
-};
-
-Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) {
-  this.type = "mustache";
-  this.escaped = !unescaped;
-  this.hash = hash;
-
-  var id = this.id = rawParams[0];
-  var params = this.params = rawParams.slice(1);
-
-  // a mustache is an eligible helper if:
-  // * its id is simple (a single part, not `this` or `..`)
-  var eligibleHelper = this.eligibleHelper = id.isSimple;
-
-  // a mustache is definitely a helper if:
-  // * it is an eligible helper, and
-  // * it has at least one parameter or hash segment
-  this.isHelper = eligibleHelper && (params.length || hash);
-
-  // if a mustache is an eligible helper but not a definite
-  // helper, it is ambiguous, and will be resolved in a later
-  // pass or at runtime.
-};
-
-Handlebars.AST.PartialNode = function(partialName, context) {
-  this.type         = "partial";
-  this.partialName  = partialName;
-  this.context      = context;
-};
-
-Handlebars.AST.BlockNode = function(mustache, program, inverse, close) {
-  var verifyMatch = function(open, close) {
-    if(open.original !== close.original) {
-      throw new Handlebars.Exception(open.original + " doesn't match " + close.original);
-    }
-  };
-
-  verifyMatch(mustache.id, close);
-  this.type = "block";
-  this.mustache = mustache;
-  this.program  = program;
-  this.inverse  = inverse;
-
-  if (this.inverse && !this.program) {
-    this.isInverse = true;
-  }
-};
-
-Handlebars.AST.ContentNode = function(string) {
-  this.type = "content";
-  this.string = string;
-};
-
-Handlebars.AST.HashNode = function(pairs) {
-  this.type = "hash";
-  this.pairs = pairs;
-};
-
-Handlebars.AST.IdNode = function(parts) {
-  this.type = "ID";
-
-  var original = "",
-      dig = [],
-      depth = 0;
-
-  for(var i=0,l=parts.length; i<l; i++) {
-    var part = parts[i].part;
-    original += (parts[i].separator || '') + part;
-
-    if (part === ".." || part === "." || part === "this") {
-      if (dig.length > 0) { throw new Handlebars.Exception("Invalid path: " + original); }
-      else if (part === "..") { depth++; }
-      else { this.isScoped = true; }
-    }
-    else { dig.push(part); }
-  }
-
-  this.original = original;
-  this.parts    = dig;
-  this.string   = dig.join('.');
-  this.depth    = depth;
-
-  // an ID is simple if it only has one part, and that part is not
-  // `..` or `this`.
-  this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
-
-  this.stringModeValue = this.string;
-};
-
-Handlebars.AST.PartialNameNode = function(name) {
-  this.type = "PARTIAL_NAME";
-  this.name = name.original;
-};
-
-Handlebars.AST.DataNode = function(id) {
-  this.type = "DATA";
-  this.id = id;
-};
-
-Handlebars.AST.StringNode = function(string) {
-  this.type = "STRING";
-  this.original =
-    this.string =
-    this.stringModeValue = string;
-};
-
-Handlebars.AST.IntegerNode = function(integer) {
-  this.type = "INTEGER";
-  this.original =
-    this.integer = integer;
-  this.stringModeValue = Number(integer);
-};
-
-Handlebars.AST.BooleanNode = function(bool) {
-  this.type = "BOOLEAN";
-  this.bool = bool;
-  this.stringModeValue = bool === "true";
-};
-
-Handlebars.AST.CommentNode = function(comment) {
-  this.type = "comment";
-  this.comment = comment;
-};
-;
-// lib/handlebars/utils.js
-
-var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
-
-Handlebars.Exception = function(message) {
-  var tmp = Error.prototype.constructor.apply(this, arguments);
-
-  // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
-  for (var idx = 0; idx < errorProps.length; idx++) {
-    this[errorProps[idx]] = tmp[errorProps[idx]];
-  }
-};
-Handlebars.Exception.prototype = new Error();
-
-// Build out our basic SafeString type
-Handlebars.SafeString = function(string) {
-  this.string = string;
-};
-Handlebars.SafeString.prototype.toString = function() {
-  return this.string.toString();
-};
-
-var escape = {
-  "&": "&amp;",
-  "<": "&lt;",
-  ">": "&gt;",
-  '"': "&quot;",
-  "'": "&#x27;",
-  "`": "&#x60;"
-};
-
-var badChars = /[&<>"'`]/g;
-var possible = /[&<>"'`]/;
-
-var escapeChar = function(chr) {
-  return escape[chr] || "&amp;";
-};
-
-Handlebars.Utils = {
-  extend: function(obj, value) {
-    for(var key in value) {
-      if(value.hasOwnProperty(key)) {
-        obj[key] = value[key];
-      }
-    }
-  },
-
-  escapeExpression: function(string) {
-    // don't escape SafeStrings, since they're already safe
-    if (string instanceof Handlebars.SafeString) {
-      return string.toString();
-    } else if (string == null || string === false) {
-      return "";
-    }
-
-    // Force a string conversion as this will be done by the append regardless and
-    // the regex test will do this transparently behind the scenes, causing issues if
-    // an object's to string has escaped characters in it.
-    string = string.toString();
-
-    if(!possible.test(string)) { return string; }
-    return string.replace(badChars, escapeChar);
-  },
-
-  isEmpty: function(value) {
-    if (!value && value !== 0) {
-      return true;
-    } else if(toString.call(value) === "[object Array]" && value.length === 0) {
-      return true;
-    } else {
-      return false;
-    }
-  }
-};
-;
-// lib/handlebars/compiler/compiler.js
-
-/*jshint eqnull:true*/
-var Compiler = Handlebars.Compiler = function() {};
-var JavaScriptCompiler = Handlebars.JavaScriptCompiler = function() {};
-
-// the foundHelper register will disambiguate helper lookup from finding a
-// function in a context. This is necessary for mustache compatibility, which
-// requires that context functions in blocks are evaluated by blockHelperMissing,
-// and then proceed as if the resulting value was provided to blockHelperMissing.
-
-Compiler.prototype = {
-  compiler: Compiler,
-
-  disassemble: function() {
-    var opcodes = this.opcodes, opcode, out = [], params, param;
-
-    for (var i=0, l=opcodes.length; i<l; i++) {
-      opcode = opcodes[i];
-
-      if (opcode.opcode === 'DECLARE') {
-        out.push("DECLARE " + opcode.name + "=" + opcode.value);
-      } else {
-        params = [];
-        for (var j=0; j<opcode.args.length; j++) {
-          param = opcode.args[j];
-          if (typeof param === "string") {
-            param = "\"" + param.replace("\n", "\\n") + "\"";
-          }
-          params.push(param);
-        }
-        out.push(opcode.opcode + " " + params.join(" "));
-      }
-    }
-
-    return out.join("\n");
-  },
-  equals: function(other) {
-    var len = this.opcodes.length;
-    if (other.opcodes.length !== len) {
-      return false;
-    }
-
-    for (var i = 0; i < len; i++) {
-      var opcode = this.opcodes[i],
-          otherOpcode = other.opcodes[i];
-      if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) {
-        return false;
-      }
-      for (var j = 0; j < opcode.args.length; j++) {
-        if (opcode.args[j] !== otherOpcode.args[j]) {
-          return false;
-        }
-      }
-    }
-
-    len = this.children.length;
-    if (other.children.length !== len) {
-      return false;
-    }
-    for (i = 0; i < len; i++) {
-      if (!this.children[i].equals(other.children[i])) {
-        return false;
-      }
-    }
-
-    return true;
-  },
-
-  guid: 0,
-
-  compile: function(program, options) {
-    this.children = [];
-    this.depths = {list: []};
-    this.options = options;
-
-    // These changes will propagate to the other compiler components
-    var knownHelpers = this.options.knownHelpers;
-    this.options.knownHelpers = {
-      'helperMissing': true,
-      'blockHelperMissing': true,
-      'each': true,
-      'if': true,
-      'unless': true,
-      'with': true,
-      'log': true
-    };
-    if (knownHelpers) {
-      for (var name in knownHelpers) {
-        this.options.knownHelpers[name] = knownHelpers[name];
-      }
-    }
-
-    return this.program(program);
-  },
-
-  accept: function(node) {
-    return this[node.type](node);
-  },
-
-  program: function(program) {
-    var statements = program.statements, statement;
-    this.opcodes = [];
-
-    for(var i=0, l=statements.length; i<l; i++) {
-      statement = statements[i];
-      this[statement.type](statement);
-    }
-    this.isSimple = l === 1;
-
-    this.depths.list = this.depths.list.sort(function(a, b) {
-      return a - b;
-    });
-
-    return this;
-  },
-
-  compileProgram: function(program) {
-    var result = new this.compiler().compile(program, this.options);
-    var guid = this.guid++, depth;
-
-    this.usePartial = this.usePartial || result.usePartial;
-
-    this.children[guid] = result;
-
-    for(var i=0, l=result.depths.list.length; i<l; i++) {
-      depth = result.depths.list[i];
-
-      if(depth < 2) { continue; }
-      else { this.addDepth(depth - 1); }
-    }
-
-    return guid;
-  },
-
-  block: function(block) {
-    var mustache = block.mustache,
-        program = block.program,
-        inverse = block.inverse;
-
-    if (program) {
-      program = this.compileProgram(program);
-    }
-
-    if (inverse) {
-      inverse = this.compileProgram(inverse);
-    }
-
-    var type = this.classifyMustache(mustache);
-
-    if (type === "helper") {
-      this.helperMustache(mustache, program, inverse);
-    } else if (type === "simple") {
-      this.simpleMustache(mustache);
-
-      // now that the simple mustache is resolved, we need to
-      // evaluate it by executing `blockHelperMissing`
-      this.opcode('pushProgram', program);
-      this.opcode('pushProgram', inverse);
-      this.opcode('emptyHash');
-      this.opcode('blockValue');
-    } else {
-      this.ambiguousMustache(mustache, program, inverse);
-
-      // now that the simple mustache is resolved, we need to
-      // evaluate it by executing `blockHelperMissing`
-      this.opcode('pushProgram', program);
-      this.opcode('pushProgram', inverse);
-      this.opcode('emptyHash');
-      this.opcode('ambiguousBlockValue');
-    }
-
-    this.opcode('append');
-  },
-
-  hash: function(hash) {
-    var pairs = hash.pairs, pair, val;
-
-    this.opcode('pushHash');
-
-    for(var i=0, l=pairs.length; i<l; i++) {
-      pair = pairs[i];
-      val  = pair[1];
-
-      if (this.options.stringParams) {
-        if(val.depth) {
-          this.addDepth(val.depth);
-        }
-        this.opcode('getContext', val.depth || 0);
-        this.opcode('pushStringParam', val.stringModeValue, val.type);
-      } else {
-        this.accept(val);
-      }
-
-      this.opcode('assignToHash', pair[0]);
-    }
-    this.opcode('popHash');
-  },
-
-  partial: function(partial) {
-    var partialName = partial.partialName;
-    this.usePartial = true;
-
-    if(partial.context) {
-      this.ID(partial.context);
-    } else {
-      this.opcode('push', 'depth0');
-    }
-
-    this.opcode('invokePartial', partialName.name);
-    this.opcode('append');
-  },
-
-  content: function(content) {
-    this.opcode('appendContent', content.string);
-  },
-
-  mustache: function(mustache) {
-    var options = this.options;
-    var type = this.classifyMustache(mustache);
-
-    if (type === "simple") {
-      this.simpleMustache(mustache);
-    } else if (type === "helper") {
-      this.helperMustache(mustache);
-    } else {
-      this.ambiguousMustache(mustache);
-    }
-
-    if(mustache.escaped && !options.noEscape) {
-      this.opcode('appendEscaped');
-    } else {
-      this.opcode('append');
-    }
-  },
-
-  ambiguousMustache: function(mustache, program, inverse) {
-    var id = mustache.id,
-        name = id.parts[0],
-        isBlock = program != null || inverse != null;
-
-    this.opcode('getContext', id.depth);
-
-    this.opcode('pushProgram', program);
-    this.opcode('pushProgram', inverse);
-
-    this.opcode('invokeAmbiguous', name, isBlock);
-  },
-
-  simpleMustache: function(mustache) {
-    var id = mustache.id;
-
-    if (id.type === 'DATA') {
-      this.DATA(id);
-    } else if (id.parts.length) {
-      this.ID(id);
-    } else {
-      // Simplified ID for `this`
-      this.addDepth(id.depth);
-      this.opcode('getContext', id.depth);
-      this.opcode('pushContext');
-    }
-
-    this.opcode('resolvePossibleLambda');
-  },
-
-  helperMustache: function(mustache, program, inverse) {
-    var params = this.setupFullMustacheParams(mustache, program, inverse),
-        name = mustache.id.parts[0];
-
-    if (this.options.knownHelpers[name]) {
-      this.opcode('invokeKnownHelper', params.length, name);
-    } else if (this.options.knownHelpersOnly) {
-      throw new Error("You specified knownHelpersOnly, but used the unknown helper " + name);
-    } else {
-      this.opcode('invokeHelper', params.length, name);
-    }
-  },
-
-  ID: function(id) {
-    this.addDepth(id.depth);
-    this.opcode('getContext', id.depth);
-
-    var name = id.parts[0];
-    if (!name) {
-      this.opcode('pushContext');
-    } else {
-      this.opcode('lookupOnContext', id.parts[0]);
-    }
-
-    for(var i=1, l=id.parts.length; i<l; i++) {
-      this.opcode('lookup', id.parts[i]);
-    }
-  },
-
-  DATA: function(data) {
-    this.options.data = true;
-    if (data.id.isScoped || data.id.depth) {
-      throw new Handlebars.Exception('Scoped data references are not supported: ' + data.original);
-    }
-
-    this.opcode('lookupData');
-    var parts = data.id.parts;
-    for(var i=0, l=parts.length; i<l; i++) {
-      this.opcode('lookup', parts[i]);
-    }
-  },
-
-  STRING: function(string) {
-    this.opcode('pushString', string.string);
-  },
-
-  INTEGER: function(integer) {
-    this.opcode('pushLiteral', integer.integer);
-  },
-
-  BOOLEAN: function(bool) {
-    this.opcode('pushLiteral', bool.bool);
-  },
-
-  comment: function() {},
-
-  // HELPERS
-  opcode: function(name) {
-    this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) });
-  },
-
-  declare: function(name, value) {
-    this.opcodes.push({ opcode: 'DECLARE', name: name, value: value });
-  },
-
-  addDepth: function(depth) {
-    if(isNaN(depth)) { throw new Error("EWOT"); }
-    if(depth === 0) { return; }
-
-    if(!this.depths[depth]) {
-      this.depths[depth] = true;
-      this.depths.list.push(depth);
-    }
-  },
-
-  classifyMustache: function(mustache) {
-    var isHelper   = mustache.isHelper;
-    var isEligible = mustache.eligibleHelper;
-    var options    = this.options;
-
-    // if ambiguous, we can possibly resolve the ambiguity now
-    if (isEligible && !isHelper) {
-      var name = mustache.id.parts[0];
-
-      if (options.knownHelpers[name]) {
-        isHelper = true;
-      } else if (options.knownHelpersOnly) {
-        isEligible = false;
-      }
-    }
-
-    if (isHelper) { return "helper"; }
-    else if (isEligible) { return "ambiguous"; }
-    else { return "simple"; }
-  },
-
-  pushParams: function(params) {
-    var i = params.length, param;
-
-    while(i--) {
-      param = params[i];
-
-      if(this.options.stringParams) {
-        if(param.depth) {
-          this.addDepth(param.depth);
-        }
-
-        this.opcode('getContext', param.depth || 0);
-        this.opcode('pushStringParam', param.stringModeValue, param.type);
-      } else {
-        this[param.type](param);
-      }
-    }
-  },
-
-  setupMustacheParams: function(mustache) {
-    var params = mustache.params;
-    this.pushParams(params);
-
-    if(mustache.hash) {
-      this.hash(mustache.hash);
-    } else {
-      this.opcode('emptyHash');
-    }
-
-    return params;
-  },
-
-  // this will replace setupMustacheParams when we're done
-  setupFullMustacheParams: function(mustache, program, inverse) {
-    var params = mustache.params;
-    this.pushParams(params);
-
-    this.opcode('pushProgram', program);
-    this.opcode('pushProgram', inverse);
-
-    if(mustache.hash) {
-      this.hash(mustache.hash);
-    } else {
-      this.opcode('emptyHash');
-    }
-
-    return params;
-  }
-};
-
-var Literal = function(value) {
-  this.value = value;
-};
-
-JavaScriptCompiler.prototype = {
-  // PUBLIC API: You can override these methods in a subclass to provide
-  // alternative compiled forms for name lookup and buffering semantics
-  nameLookup: function(parent, name /* , type*/) {
-    if (/^[0-9]+$/.test(name)) {
-      return parent + "[" + name + "]";
-    } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
-      return parent + "." + name;
-    }
-    else {
-      return parent + "['" + name + "']";
-    }
-  },
-
-  appendToBuffer: function(string) {
-    if (this.environment.isSimple) {
-      return "return " + string + ";";
-    } else {
-      return {
-        appendToBuffer: true,
-        content: string,
-        toString: function() { return "buffer += " + string + ";"; }
-      };
-    }
-  },
-
-  initializeBuffer: function() {
-    return this.quotedString("");
-  },
-
-  namespace: "Handlebars",
-  // END PUBLIC API
-
-  compile: function(environment, options, context, asObject) {
-    this.environment = environment;
-    this.options = options || {};
-
-    Handlebars.log(Handlebars.logger.DEBUG, this.environment.disassemble() + "\n\n");
-
-    this.name = this.environment.name;
-    this.isChild = !!context;
-    this.context = context || {
-      programs: [],
-      environments: [],
-      aliases: { }
-    };
-
-    this.preamble();
-
-    this.stackSlot = 0;
-    this.stackVars = [];
-    this.registers = { list: [] };
-    this.compileStack = [];
-    this.inlineStack = [];
-
-    this.compileChildren(environment, options);
-
-    var opcodes = environment.opcodes, opcode;
-
-    this.i = 0;
-
-    for(l=opcodes.length; this.i<l; this.i++) {
-      opcode = opcodes[this.i];
-
-      if(opcode.opcode === 'DECLARE') {
-        this[opcode.name] = opcode.value;
-      } else {
-        this[opcode.opcode].apply(this, opcode.args);
-      }
-    }
-
-    return this.createFunctionContext(asObject);
-  },
-
-  nextOpcode: function() {
-    var opcodes = this.environment.opcodes;
-    return opcodes[this.i + 1];
-  },
-
-  eat: function() {
-    this.i = this.i + 1;
-  },
-
-  preamble: function() {
-    var out = [];
-
-    if (!this.isChild) {
-      var namespace = this.namespace;
-
-      var copies = "helpers = this.merge(helpers, " + namespace + ".helpers);";
-      if (this.environment.usePartial) { copies = copies + " partials = this.merge(partials, " + namespace + ".partials);"; }
-      if (this.options.data) { copies = copies + " data = data || {};"; }
-      out.push(copies);
-    } else {
-      out.push('');
-    }
-
-    if (!this.environment.isSimple) {
-      out.push(", buffer = " + this.initializeBuffer());
-    } else {
-      out.push("");
-    }
-
-    // track the last context pushed into place to allow skipping the
-    // getContext opcode when it would be a noop
-    this.lastContext = 0;
-    this.source = out;
-  },
-
-  createFunctionContext: function(asObject) {
-    var locals = this.stackVars.concat(this.registers.list);
-
-    if(locals.length > 0) {
-      this.source[1] = this.source[1] + ", " + locals.join(", ");
-    }
-
-    // Generate minimizer alias mappings
-    if (!this.isChild) {
-      for (var alias in this.context.aliases) {
-        if (this.context.aliases.hasOwnProperty(alias)) {
-          this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
-        }
-      }
-    }
-
-    if (this.source[1]) {
-      this.source[1] = "var " + this.source[1].substring(2) + ";";
-    }
-
-    // Merge children
-    if (!this.isChild) {
-      this.source[1] += '\n' + this.context.programs.join('\n') + '\n';
-    }
-
-    if (!this.environment.isSimple) {
-      this.source.push("return buffer;");
-    }
-
-    var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"];
-
-    for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
-      params.push("depth" + this.environment.depths.list[i]);
-    }
-
-    // Perform a second pass over the output to merge content when possible
-    var source = this.mergeSource();
-
-    if (!this.isChild) {
-      var revision = Handlebars.COMPILER_REVISION,
-          versions = Handlebars.REVISION_CHANGES[revision];
-      source = "this.compilerInfo = ["+revision+",'"+versions+"'];\n"+source;
-    }
-
-    if (asObject) {
-      params.push(source);
-
-      return Function.apply(this, params);
-    } else {
-      var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n  ' + source + '}';
-      Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n");
-      return functionSource;
-    }
-  },
-  mergeSource: function() {
-    // WARN: We are not handling the case where buffer is still populated as the source should
-    // not have buffer append operations as their final action.
-    var source = '',
-        buffer;
-    for (var i = 0, len = this.source.length; i < len; i++) {
-      var line = this.source[i];
-      if (line.appendToBuffer) {
-        if (buffer) {
-          buffer = buffer + '\n    + ' + line.content;
-        } else {
-          buffer = line.content;
-        }
-      } else {
-        if (buffer) {
-          source += 'buffer += ' + buffer + ';\n  ';
-          buffer = undefined;
-        }
-        source += line + '\n  ';
-      }
-    }
-    return source;
-  },
-
-  // [blockValue]
-  //
-  // On stack, before: hash, inverse, program, value
-  // On stack, after: return value of blockHelperMissing
-  //
-  // The purpose of this opcode is to take a block of the form
-  // `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and
-  // replace it on the stack with the result of properly
-  // invoking blockHelperMissing.
-  blockValue: function() {
-    this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
-
-    var params = ["depth0"];
-    this.setupParams(0, params);
-
-    this.replaceStack(function(current) {
-      params.splice(1, 0, current);
-      return "blockHelperMissing.call(" + params.join(", ") + ")";
-    });
-  },
-
-  // [ambiguousBlockValue]
-  //
-  // On stack, before: hash, inverse, program, value
-  // Compiler value, before: lastHelper=value of last found helper, if any
-  // On stack, after, if no lastHelper: same as [blockValue]
-  // On stack, after, if lastHelper: value
-  ambiguousBlockValue: function() {
-    this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
-
-    var params = ["depth0"];
-    this.setupParams(0, params);
-
-    var current = this.topStack();
-    params.splice(1, 0, current);
-
-    // Use the options value generated from the invocation
-    params[params.length-1] = 'options';
-
-    this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
-  },
-
-  // [appendContent]
-  //
-  // On stack, before: ...
-  // On stack, after: ...
-  //
-  // Appends the string value of `content` to the current buffer
-  appendContent: function(content) {
-    this.source.push(this.appendToBuffer(this.quotedString(content)));
-  },
-
-  // [append]
-  //
-  // On stack, before: value, ...
-  // On stack, after: ...
-  //
-  // Coerces `value` to a String and appends it to the current buffer.
-  //
-  // If `value` is truthy, or 0, it is coerced into a string and appended
-  // Otherwise, the empty string is appended
-  append: function() {
-    // Force anything that is inlined onto the stack so we don't have duplication
-    // when we examine local
-    this.flushInline();
-    var local = this.popStack();
-    this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
-    if (this.environment.isSimple) {
-      this.source.push("else { " + this.appendToBuffer("''") + " }");
-    }
-  },
-
-  // [appendEscaped]
-  //
-  // On stack, before: value, ...
-  // On stack, after: ...
-  //
-  // Escape `value` and append it to the buffer
-  appendEscaped: function() {
-    this.context.aliases.escapeExpression = 'this.escapeExpression';
-
-    this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
-  },
-
-  // [getContext]
-  //
-  // On stack, before: ...
-  // On stack, after: ...
-  // Compiler value, after: lastContext=depth
-  //
-  // Set the value of the `lastContext` compiler value to the depth
-  getContext: function(depth) {
-    if(this.lastContext !== depth) {
-      this.lastContext = depth;
-    }
-  },
-
-  // [lookupOnContext]
-  //
-  // On stack, before: ...
-  // On stack, after: currentContext[name], ...
-  //
-  // Looks up the value of `name` on the current context and pushes
-  // it onto the stack.
-  lookupOnContext: function(name) {
-    this.push(this.nameLookup('depth' + this.lastContext, name, 'context'));
-  },
-
-  // [pushContext]
-  //
-  // On stack, before: ...
-  // On stack, after: currentContext, ...
-  //
-  // Pushes the value of the current context onto the stack.
-  pushContext: function() {
-    this.pushStackLiteral('depth' + this.lastContext);
-  },
-
-  // [resolvePossibleLambda]
-  //
-  // On stack, before: value, ...
-  // On stack, after: resolved value, ...
-  //
-  // If the `value` is a lambda, replace it on the stack by
-  // the return value of the lambda
-  resolvePossibleLambda: function() {
-    this.context.aliases.functionType = '"function"';
-
-    this.replaceStack(function(current) {
-      return "typeof " + current + " === functionType ? " + current + ".apply(depth0) : " + current;
-    });
-  },
-
-  // [lookup]
-  //
-  // On stack, before: value, ...
-  // On stack, after: value[name], ...
-  //
-  // Replace the value on the stack with the result of looking
-  // up `name` on `value`
-  lookup: function(name) {
-    this.replaceStack(function(current) {
-      return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context');
-    });
-  },
-
-  // [lookupData]
-  //
-  // On stack, before: ...
-  // On stack, after: data[id], ...
-  //
-  // Push the result of looking up `id` on the current data
-  lookupData: function(id) {
-    this.push('data');
-  },
-
-  // [pushStringParam]
-  //
-  // On stack, before: ...
-  // On stack, after: string, currentContext, ...
-  //
-  // This opcode is designed for use in string mode, which
-  // provides the string value of a parameter along with its
-  // depth rather than resolving it immediately.
-  pushStringParam: function(string, type) {
-    this.pushStackLiteral('depth' + this.lastContext);
-
-    this.pushString(type);
-
-    if (typeof string === 'string') {
-      this.pushString(string);
-    } else {
-      this.pushStackLiteral(string);
-    }
-  },
-
-  emptyHash: function() {
-    this.pushStackLiteral('{}');
-
-    if (this.options.stringParams) {
-      this.register('hashTypes', '{}');
-      this.register('hashContexts', '{}');
-    }
-  },
-  pushHash: function() {
-    this.hash = {values: [], types: [], contexts: []};
-  },
-  popHash: function() {
-    var hash = this.hash;
-    this.hash = undefined;
-
-    if (this.options.stringParams) {
-      this.register('hashContexts', '{' + hash.contexts.join(',') + '}');
-      this.register('hashTypes', '{' + hash.types.join(',') + '}');
-    }
-    this.push('{\n    ' + hash.values.join(',\n    ') + '\n  }');
-  },
-
-  // [pushString]
-  //
-  // On stack, before: ...
-  // On stack, after: quotedString(string), ...
-  //
-  // Push a quoted version of `string` onto the stack
-  pushString: function(string) {
-    this.pushStackLiteral(this.quotedString(string));
-  },
-
-  // [push]
-  //
-  // On stack, before: ...
-  // On stack, after: expr, ...
-  //
-  // Push an expression onto the stack
-  push: function(expr) {
-    this.inlineStack.push(expr);
-    return expr;
-  },
-
-  // [pushLiteral]
-  //
-  // On stack, before: ...
-  // On stack, after: value, ...
-  //
-  // Pushes a value onto the stack. This operation prevents
-  // the compiler from creating a temporary variable to hold
-  // it.
-  pushLiteral: function(value) {
-    this.pushStackLiteral(value);
-  },
-
-  // [pushProgram]
-  //
-  // On stack, before: ...
-  // On stack, after: program(guid), ...
-  //
-  // Push a program expression onto the stack. This takes
-  // a compile-time guid and converts it into a runtime-accessible
-  // expression.
-  pushProgram: function(guid) {
-    if (guid != null) {
-      this.pushStackLiteral(this.programExpression(guid));
-    } else {
-      this.pushStackLiteral(null);
-    }
-  },
-
-  // [invokeHelper]
-  //
-  // On stack, before: hash, inverse, program, params..., ...
-  // On stack, after: result of helper invocation
-  //
-  // Pops off the helper's parameters, invokes the helper,
-  // and pushes the helper's return value onto the stack.
-  //
-  // If the helper is not found, `helperMissing` is called.
-  invokeHelper: function(paramSize, name) {
-    this.context.aliases.helperMissing = 'helpers.helperMissing';
-
-    var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
-    var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
-
-    this.push(helper.name + ' || ' + nonHelper);
-    this.replaceStack(function(name) {
-      return name + ' ? ' + name + '.call(' +
-          helper.callParams + ") " + ": helperMissing.call(" +
-          helper.helperMissingParams + ")";
-    });
-  },
-
-  // [invokeKnownHelper]
-  //
-  // On stack, before: hash, inverse, program, params..., ...
-  // On stack, after: result of helper invocation
-  //
-  // This operation is used when the helper is known to exist,
-  // so a `helperMissing` fallback is not required.
-  invokeKnownHelper: function(paramSize, name) {
-    var helper = this.setupHelper(paramSize, name);
-    this.push(helper.name + ".call(" + helper.callParams + ")");
-  },
-
-  // [invokeAmbiguous]
-  //
-  // On stack, before: hash, inverse, program, params..., ...
-  // On stack, after: result of disambiguation
-  //
-  // This operation is used when an expression like `{{foo}}`
-  // is provided, but we don't know at compile-time whether it
-  // is a helper or a path.
-  //
-  // This operation emits more code than the other options,
-  // and can be avoided by passing the `knownHelpers` and
-  // `knownHelpersOnly` flags at compile-time.
-  invokeAmbiguous: function(name, helperCall) {
-    this.context.aliases.functionType = '"function"';
-
-    this.pushStackLiteral('{}');    // Hash value
-    var helper = this.setupHelper(0, name, helperCall);
-
-    var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
-
-    var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
-    var nextStack = this.nextStack();
-
-    this.source.push('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }');
-    this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.apply(depth0) : ' + nextStack + '; }');
-  },
-
-  // [invokePartial]
-  //
-  // On stack, before: context, ...
-  // On stack after: result of partial invocation
-  //
-  // This operation pops off a context, invokes a partial with that context,
-  // and pushes the result of the invocation back.
-  invokePartial: function(name) {
-    var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"];
-
-    if (this.options.data) {
-      params.push("data");
-    }
-
-    this.context.aliases.self = "this";
-    this.push("self.invokePartial(" + params.join(", ") + ")");
-  },
-
-  // [assignToHash]
-  //
-  // On stack, before: value, hash, ...
-  // On stack, after: hash, ...
-  //
-  // Pops a value and hash off the stack, assigns `hash[key] = value`
-  // and pushes the hash back onto the stack.
-  assignToHash: function(key) {
-    var value = this.popStack(),
-        context,
-        type;
-
-    if (this.options.stringParams) {
-      type = this.popStack();
-      context = this.popStack();
-    }
-
-    var hash = this.hash;
-    if (context) {
-      hash.contexts.push("'" + key + "': " + context);
-    }
-    if (type) {
-      hash.types.push("'" + key + "': " + type);
-    }
-    hash.values.push("'" + key + "': (" + value + ")");
-  },
-
-  // HELPERS
-
-  compiler: JavaScriptCompiler,
-
-  compileChildren: function(environment, options) {
-    var children = environment.children, child, compiler;
-
-    for(var i=0, l=children.length; i<l; i++) {
-      child = children[i];
-      compiler = new this.compiler();
-
-      var index = this.matchExistingProgram(child);
-
-      if (index == null) {
-        this.context.programs.push('');     // Placeholder to prevent name conflicts for nested children
-        index = this.context.programs.length;
-        child.index = index;
-        child.name = 'program' + index;
-        this.context.programs[index] = compiler.compile(child, options, this.context);
-        this.context.environments[index] = child;
-      } else {
-        child.index = index;
-        child.name = 'program' + index;
-      }
-    }
-  },
-  matchExistingProgram: function(child) {
-    for (var i = 0, len = this.context.environments.length; i < len; i++) {
-      var environment = this.context.environments[i];
-      if (environment && environment.equals(child)) {
-        return i;
-      }
-    }
-  },
-
-  programExpression: function(guid) {
-    this.context.aliases.self = "this";
-
-    if(guid == null) {
-      return "self.noop";
-    }
-
-    var child = this.environment.children[guid],
-        depths = child.depths.list, depth;
-
-    var programParams = [child.index, child.name, "data"];
-
-    for(var i=0, l = depths.length; i<l; i++) {
-      depth = depths[i];
-
-      if(depth === 1) { programParams.push("depth0"); }
-      else { programParams.push("depth" + (depth - 1)); }
-    }
-
-    return (depths.length === 0 ? "self.program(" : "self.programWithDepth(") + programParams.join(", ") + ")";
-  },
-
-  register: function(name, val) {
-    this.useRegister(name);
-    this.source.push(name + " = " + val + ";");
-  },
-
-  useRegister: function(name) {
-    if(!this.registers[name]) {
-      this.registers[name] = true;
-      this.registers.list.push(name);
-    }
-  },
-
-  pushStackLiteral: function(item) {
-    return this.push(new Literal(item));
-  },
-
-  pushStack: function(item) {
-    this.flushInline();
-
-    var stack = this.incrStack();
-    if (item) {
-      this.source.push(stack + " = " + item + ";");
-    }
-    this.compileStack.push(stack);
-    return stack;
-  },
-
-  replaceStack: function(callback) {
-    var prefix = '',
-        inline = this.isInline(),
-        stack;
-
-    // If we are currently inline then we want to merge the inline statement into the
-    // replacement statement via ','
-    if (inline) {
-      var top = this.popStack(true);
-
-      if (top instanceof Literal) {
-        // Literals do not need to be inlined
-        stack = top.value;
-      } else {
-        // Get or create the current stack name for use by the inline
-        var name = this.stackSlot ? this.topStackName() : this.incrStack();
-
-        prefix = '(' + this.push(name) + ' = ' + top + '),';
-        stack = this.topStack();
-      }
-    } else {
-      stack = this.topStack();
-    }
-
-    var item = callback.call(this, stack);
-
-    if (inline) {
-      if (this.inlineStack.length || this.compileStack.length) {
-        this.popStack();
-      }
-      this.push('(' + prefix + item + ')');
-    } else {
-      // Prevent modification of the context depth variable. Through replaceStack
-      if (!/^stack/.test(stack)) {
-        stack = this.nextStack();
-      }
-
-      this.source.push(stack + " = (" + prefix + item + ");");
-    }
-    return stack;
-  },
-
-  nextStack: function() {
-    return this.pushStack();
-  },
-
-  incrStack: function() {
-    this.stackSlot++;
-    if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
-    return this.topStackName();
-  },
-  topStackName: function() {
-    return "stack" + this.stackSlot;
-  },
-  flushInline: function() {
-    var inlineStack = this.inlineStack;
-    if (inlineStack.length) {
-      this.inlineStack = [];
-      for (var i = 0, len = inlineStack.length; i < len; i++) {
-        var entry = inlineStack[i];
-        if (entry instanceof Literal) {
-          this.compileStack.push(entry);
-        } else {
-          this.pushStack(entry);
-        }
-      }
-    }
-  },
-  isInline: function() {
-    return this.inlineStack.length;
-  },
-
-  popStack: function(wrapped) {
-    var inline = this.isInline(),
-        item = (inline ? this.inlineStack : this.compileStack).pop();
-
-    if (!wrapped && (item instanceof Literal)) {
-      return item.value;
-    } else {
-      if (!inline) {
-        this.stackSlot--;
-      }
-      return item;
-    }
-  },
-
-  topStack: function(wrapped) {
-    var stack = (this.isInline() ? this.inlineStack : this.compileStack),
-        item = stack[stack.length - 1];
-
-    if (!wrapped && (item instanceof Literal)) {
-      return item.value;
-    } else {
-      return item;
-    }
-  },
-
-  quotedString: function(str) {
-    return '"' + str
-      .replace(/\\/g, '\\\\')
-      .replace(/"/g, '\\"')
-      .replace(/\n/g, '\\n')
-      .replace(/\r/g, '\\r')
-      .replace(/\u2028/g, '\\u2028')   // Per Ecma-262 7.3 + 7.8.4
-      .replace(/\u2029/g, '\\u2029') + '"';
-  },
-
-  setupHelper: function(paramSize, name, missingParams) {
-    var params = [];
-    this.setupParams(paramSize, params, missingParams);
-    var foundHelper = this.nameLookup('helpers', name, 'helper');
-
-    return {
-      params: params,
-      name: foundHelper,
-      callParams: ["depth0"].concat(params).join(", "),
-      helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
-    };
-  },
-
-  // the params and contexts arguments are passed in arrays
-  // to fill in
-  setupParams: function(paramSize, params, useRegister) {
-    var options = [], contexts = [], types = [], param, inverse, program;
-
-    options.push("hash:" + this.popStack());
-
-    inverse = this.popStack();
-    program = this.popStack();
-
-    // Avoid setting fn and inverse if neither are set. This allows
-    // helpers to do a check for `if (options.fn)`
-    if (program || inverse) {
-      if (!program) {
-        this.context.aliases.self = "this";
-        program = "self.noop";
-      }
-
-      if (!inverse) {
-       this.context.aliases.self = "this";
-        inverse = "self.noop";
-      }
-
-      options.push("inverse:" + inverse);
-      options.push("fn:" + program);
-    }
-
-    for(var i=0; i<paramSize; i++) {
-      param = this.popStack();
-      params.push(param);
-
-      if(this.options.stringParams) {
-        types.push(this.popStack());
-        contexts.push(this.popStack());
-      }
-    }
-
-    if (this.options.stringParams) {
-      options.push("contexts:[" + contexts.join(",") + "]");
-      options.push("types:[" + types.join(",") + "]");
-      options.push("hashContexts:hashContexts");
-      options.push("hashTypes:hashTypes");
-    }
-
-    if(this.options.data) {
-      options.push("data:data");
-    }
-
-    options = "{" + options.join(",") + "}";
-    if (useRegister) {
-      this.register('options', options);
-      params.push('options');
-    } else {
-      params.push(options);
-    }
-    return params.join(", ");
-  }
-};
-
-var reservedWords = (
-  "break else new var" +
-  " case finally return void" +
-  " catch for switch while" +
-  " continue function this with" +
-  " default if throw" +
-  " delete in try" +
-  " do instanceof typeof" +
-  " abstract enum int short" +
-  " boolean export interface static" +
-  " byte extends long super" +
-  " char final native synchronized" +
-  " class float package throws" +
-  " const goto private transient" +
-  " debugger implements protected volatile" +
-  " double import public let yield"
-).split(" ");
-
-var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
-
-for(var i=0, l=reservedWords.length; i<l; i++) {
-  compilerWords[reservedWords[i]] = true;
-}
-
-JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
-  if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) {
-    return true;
-  }
-  return false;
-};
-
-Handlebars.precompile = function(input, options) {
-  if (input == null || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) {
-    throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
-  }
-
-  options = options || {};
-  if (!('data' in options)) {
-    options.data = true;
-  }
-  var ast = Handlebars.parse(input);
-  var environment = new Compiler().compile(ast, options);
-  return new JavaScriptCompiler().compile(environment, options);
-};
-
-Handlebars.compile = function(input, options) {
-  if (input == null || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) {
-    throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
-  }
-
-  options = options || {};
-  if (!('data' in options)) {
-    options.data = true;
-  }
-  var compiled;
-  function compile() {
-    var ast = Handlebars.parse(input);
-    var environment = new Compiler().compile(ast, options);
-    var templateSpec = new JavaScriptCompiler().compile(environment, options, undefined, true);
-    return Handlebars.template(templateSpec);
-  }
-
-  // Template is only compiled on first use and cached after that point.
-  return function(context, options) {
-    if (!compiled) {
-      compiled = compile();
-    }
-    return compiled.call(this, context, options);
-  };
-};
-
-;
-// lib/handlebars/runtime.js
-
-Handlebars.VM = {
-  template: function(templateSpec) {
-    // Just add water
-    var container = {
-      escapeExpression: Handlebars.Utils.escapeExpression,
-      invokePartial: Handlebars.VM.invokePartial,
-      programs: [],
-      program: function(i, fn, data) {
-        var programWrapper = this.programs[i];
-        if(data) {
-          programWrapper = Handlebars.VM.program(i, fn, data);
-        } else if (!programWrapper) {
-          programWrapper = this.programs[i] = Handlebars.VM.program(i, fn);
-        }
-        return programWrapper;
-      },
-      merge: function(param, common) {
-        var ret = param || common;
-
-        if (param && common) {
-          ret = {};
-          Handlebars.Utils.extend(ret, common);
-          Handlebars.Utils.extend(ret, param);
-        }
-        return ret;
-      },
-      programWithDepth: Handlebars.VM.programWithDepth,
-      noop: Handlebars.VM.noop,
-      compilerInfo: null
-    };
-
-    return function(context, options) {
-      options = options || {};
-      var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
-
-      var compilerInfo = container.compilerInfo || [],
-          compilerRevision = compilerInfo[0] || 1,
-          currentRevision = Handlebars.COMPILER_REVISION;
-
-      if (compilerRevision !== currentRevision) {
-        if (compilerRevision < currentRevision) {
-          var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision],
-              compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision];
-          throw "Template was precompiled with an older version of Handlebars than the current runtime. "+
-                "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").";
-        } else {
-          // Use the embedded version info since the runtime doesn't know about this revision yet
-          throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+
-                "Please update your runtime to a newer version ("+compilerInfo[1]+").";
-        }
-      }
-
-      return result;
-    };
-  },
-
-  programWithDepth: function(i, fn, data /*, $depth */) {
-    var args = Array.prototype.slice.call(arguments, 3);
-
-    var program = function(context, options) {
-      options = options || {};
-
-      return fn.apply(this, [context, options.data || data].concat(args));
-    };
-    program.program = i;
-    program.depth = args.length;
-    return program;
-  },
-  program: function(i, fn, data) {
-    var program = function(context, options) {
-      options = options || {};
-
-      return fn(context, options.data || data);
-    };
-    program.program = i;
-    program.depth = 0;
-    return program;
-  },
-  noop: function() { return ""; },
-  invokePartial: function(partial, name, context, helpers, partials, data) {
-    var options = { helpers: helpers, partials: partials, data: data };
-
-    if(partial === undefined) {
-      throw new Handlebars.Exception("The partial " + name + " could not be found");
-    } else if(partial instanceof Function) {
-      return partial(context, options);
-    } else if (!Handlebars.compile) {
-      throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
-    } else {
-      partials[name] = Handlebars.compile(partial, {data: data !== undefined});
-      return partials[name](context, options);
-    }
-  }
-};
-
-Handlebars.template = Handlebars.VM.template;
-;
-// lib/handlebars/browser-suffix.js
-})(Handlebars);
-;
diff --git a/dist/handlebars.runtime.js b/dist/handlebars.runtime.js
deleted file mode 100644
index b9f4e4920..000000000
--- a/dist/handlebars.runtime.js
+++ /dev/null
@@ -1,362 +0,0 @@
-/*
-
-Copyright (C) 2011 by Yehuda Katz
-
-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.
-
-*/
-
-// lib/handlebars/browser-prefix.js
-var Handlebars = {};
-
-(function(Handlebars, undefined) {
-;
-// lib/handlebars/base.js
-
-Handlebars.VERSION = "1.0.0";
-Handlebars.COMPILER_REVISION = 4;
-
-Handlebars.REVISION_CHANGES = {
-  1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
-  2: '== 1.0.0-rc.3',
-  3: '== 1.0.0-rc.4',
-  4: '>= 1.0.0'
-};
-
-Handlebars.helpers  = {};
-Handlebars.partials = {};
-
-var toString = Object.prototype.toString,
-    functionType = '[object Function]',
-    objectType = '[object Object]';
-
-Handlebars.registerHelper = function(name, fn, inverse) {
-  if (toString.call(name) === objectType) {
-    if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); }
-    Handlebars.Utils.extend(this.helpers, name);
-  } else {
-    if (inverse) { fn.not = inverse; }
-    this.helpers[name] = fn;
-  }
-};
-
-Handlebars.registerPartial = function(name, str) {
-  if (toString.call(name) === objectType) {
-    Handlebars.Utils.extend(this.partials,  name);
-  } else {
-    this.partials[name] = str;
-  }
-};
-
-Handlebars.registerHelper('helperMissing', function(arg) {
-  if(arguments.length === 2) {
-    return undefined;
-  } else {
-    throw new Error("Missing helper: '" + arg + "'");
-  }
-});
-
-Handlebars.registerHelper('blockHelperMissing', function(context, options) {
-  var inverse = options.inverse || function() {}, fn = options.fn;
-
-  var type = toString.call(context);
-
-  if(type === functionType) { context = context.call(this); }
-
-  if(context === true) {
-    return fn(this);
-  } else if(context === false || context == null) {
-    return inverse(this);
-  } else if(type === "[object Array]") {
-    if(context.length > 0) {
-      return Handlebars.helpers.each(context, options);
-    } else {
-      return inverse(this);
-    }
-  } else {
-    return fn(context);
-  }
-});
-
-Handlebars.K = function() {};
-
-Handlebars.createFrame = Object.create || function(object) {
-  Handlebars.K.prototype = object;
-  var obj = new Handlebars.K();
-  Handlebars.K.prototype = null;
-  return obj;
-};
-
-Handlebars.logger = {
-  DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
-
-  methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'},
-
-  // can be overridden in the host environment
-  log: function(level, obj) {
-    if (Handlebars.logger.level <= level) {
-      var method = Handlebars.logger.methodMap[level];
-      if (typeof console !== 'undefined' && console[method]) {
-        console[method].call(console, obj);
-      }
-    }
-  }
-};
-
-Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); };
-
-Handlebars.registerHelper('each', function(context, options) {
-  var fn = options.fn, inverse = options.inverse;
-  var i = 0, ret = "", data;
-
-  var type = toString.call(context);
-  if(type === functionType) { context = context.call(this); }
-
-  if (options.data) {
-    data = Handlebars.createFrame(options.data);
-  }
-
-  if(context && typeof context === 'object') {
-    if(context instanceof Array){
-      for(var j = context.length; i<j; i++) {
-        if (data) { data.index = i; }
-        ret = ret + fn(context[i], { data: data });
-      }
-    } else {
-      for(var key in context) {
-        if(context.hasOwnProperty(key)) {
-          if(data) { data.key = key; }
-          ret = ret + fn(context[key], {data: data});
-          i++;
-        }
-      }
-    }
-  }
-
-  if(i === 0){
-    ret = inverse(this);
-  }
-
-  return ret;
-});
-
-Handlebars.registerHelper('if', function(conditional, options) {
-  var type = toString.call(conditional);
-  if(type === functionType) { conditional = conditional.call(this); }
-
-  if(!conditional || Handlebars.Utils.isEmpty(conditional)) {
-    return options.inverse(this);
-  } else {
-    return options.fn(this);
-  }
-});
-
-Handlebars.registerHelper('unless', function(conditional, options) {
-  return Handlebars.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn});
-});
-
-Handlebars.registerHelper('with', function(context, options) {
-  var type = toString.call(context);
-  if(type === functionType) { context = context.call(this); }
-
-  if (!Handlebars.Utils.isEmpty(context)) return options.fn(context);
-});
-
-Handlebars.registerHelper('log', function(context, options) {
-  var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
-  Handlebars.log(level, context);
-});
-;
-// lib/handlebars/utils.js
-
-var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
-
-Handlebars.Exception = function(message) {
-  var tmp = Error.prototype.constructor.apply(this, arguments);
-
-  // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
-  for (var idx = 0; idx < errorProps.length; idx++) {
-    this[errorProps[idx]] = tmp[errorProps[idx]];
-  }
-};
-Handlebars.Exception.prototype = new Error();
-
-// Build out our basic SafeString type
-Handlebars.SafeString = function(string) {
-  this.string = string;
-};
-Handlebars.SafeString.prototype.toString = function() {
-  return this.string.toString();
-};
-
-var escape = {
-  "&": "&amp;",
-  "<": "&lt;",
-  ">": "&gt;",
-  '"': "&quot;",
-  "'": "&#x27;",
-  "`": "&#x60;"
-};
-
-var badChars = /[&<>"'`]/g;
-var possible = /[&<>"'`]/;
-
-var escapeChar = function(chr) {
-  return escape[chr] || "&amp;";
-};
-
-Handlebars.Utils = {
-  extend: function(obj, value) {
-    for(var key in value) {
-      if(value.hasOwnProperty(key)) {
-        obj[key] = value[key];
-      }
-    }
-  },
-
-  escapeExpression: function(string) {
-    // don't escape SafeStrings, since they're already safe
-    if (string instanceof Handlebars.SafeString) {
-      return string.toString();
-    } else if (string == null || string === false) {
-      return "";
-    }
-
-    // Force a string conversion as this will be done by the append regardless and
-    // the regex test will do this transparently behind the scenes, causing issues if
-    // an object's to string has escaped characters in it.
-    string = string.toString();
-
-    if(!possible.test(string)) { return string; }
-    return string.replace(badChars, escapeChar);
-  },
-
-  isEmpty: function(value) {
-    if (!value && value !== 0) {
-      return true;
-    } else if(toString.call(value) === "[object Array]" && value.length === 0) {
-      return true;
-    } else {
-      return false;
-    }
-  }
-};
-;
-// lib/handlebars/runtime.js
-
-Handlebars.VM = {
-  template: function(templateSpec) {
-    // Just add water
-    var container = {
-      escapeExpression: Handlebars.Utils.escapeExpression,
-      invokePartial: Handlebars.VM.invokePartial,
-      programs: [],
-      program: function(i, fn, data) {
-        var programWrapper = this.programs[i];
-        if(data) {
-          programWrapper = Handlebars.VM.program(i, fn, data);
-        } else if (!programWrapper) {
-          programWrapper = this.programs[i] = Handlebars.VM.program(i, fn);
-        }
-        return programWrapper;
-      },
-      merge: function(param, common) {
-        var ret = param || common;
-
-        if (param && common) {
-          ret = {};
-          Handlebars.Utils.extend(ret, common);
-          Handlebars.Utils.extend(ret, param);
-        }
-        return ret;
-      },
-      programWithDepth: Handlebars.VM.programWithDepth,
-      noop: Handlebars.VM.noop,
-      compilerInfo: null
-    };
-
-    return function(context, options) {
-      options = options || {};
-      var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
-
-      var compilerInfo = container.compilerInfo || [],
-          compilerRevision = compilerInfo[0] || 1,
-          currentRevision = Handlebars.COMPILER_REVISION;
-
-      if (compilerRevision !== currentRevision) {
-        if (compilerRevision < currentRevision) {
-          var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision],
-              compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision];
-          throw "Template was precompiled with an older version of Handlebars than the current runtime. "+
-                "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").";
-        } else {
-          // Use the embedded version info since the runtime doesn't know about this revision yet
-          throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+
-                "Please update your runtime to a newer version ("+compilerInfo[1]+").";
-        }
-      }
-
-      return result;
-    };
-  },
-
-  programWithDepth: function(i, fn, data /*, $depth */) {
-    var args = Array.prototype.slice.call(arguments, 3);
-
-    var program = function(context, options) {
-      options = options || {};
-
-      return fn.apply(this, [context, options.data || data].concat(args));
-    };
-    program.program = i;
-    program.depth = args.length;
-    return program;
-  },
-  program: function(i, fn, data) {
-    var program = function(context, options) {
-      options = options || {};
-
-      return fn(context, options.data || data);
-    };
-    program.program = i;
-    program.depth = 0;
-    return program;
-  },
-  noop: function() { return ""; },
-  invokePartial: function(partial, name, context, helpers, partials, data) {
-    var options = { helpers: helpers, partials: partials, data: data };
-
-    if(partial === undefined) {
-      throw new Handlebars.Exception("The partial " + name + " could not be found");
-    } else if(partial instanceof Function) {
-      return partial(context, options);
-    } else if (!Handlebars.compile) {
-      throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
-    } else {
-      partials[name] = Handlebars.compile(partial, {data: data !== undefined});
-      return partials[name](context, options);
-    }
-  }
-};
-
-Handlebars.template = Handlebars.VM.template;
-;
-// lib/handlebars/browser-suffix.js
-})(Handlebars);
-;
diff --git a/lib/handlebars.js b/lib/handlebars.js
index f82ec3bad..4abdd4a1b 100644
--- a/lib/handlebars.js
+++ b/lib/handlebars.js
@@ -1,43 +1,30 @@
-var handlebars = require("./handlebars/base"),
+import Handlebars from "./handlebars.runtime";
 
-// Each of these augment the Handlebars object. No need to setup here.
-// (This is done to easily share code between commonjs and browse envs)
-  utils = require("./handlebars/utils"),
-  compiler = require("./handlebars/compiler"),
-  runtime = require("./handlebars/runtime");
+// Compiler imports
+module AST from "./handlebars/compiler/ast";
+import { parser as Parser, parse } from "./handlebars/compiler/base";
+import { Compiler, compile, precompile } from "./handlebars/compiler/compiler";
+import JavaScriptCompiler from "./handlebars/compiler/javascript-compiler";
 
+var _create = Handlebars.create;
 var create = function() {
-  var hb = handlebars.create();
+  var hb = _create();
 
-  utils.attach(hb);
-  compiler.attach(hb);
-  runtime.attach(hb);
+  hb.compile = function(input, options) {
+    return compile(input, options, hb);
+  };
+  hb.precompile = precompile;
+
+  hb.AST = AST;
+  hb.Compiler = Compiler;
+  hb.JavaScriptCompiler = JavaScriptCompiler;
+  hb.Parser = Parser;
+  hb.parse = parse;
 
   return hb;
 };
 
-var Handlebars = create();
+Handlebars = create();
 Handlebars.create = create;
 
-module.exports = Handlebars; // instantiate an instance
-
-// Publish a Node.js require() handler for .handlebars and .hbs files
-if (require.extensions) {
-  var extension = function(module, filename) {
-    var fs = require("fs");
-    var templateString = fs.readFileSync(filename, "utf8");
-    module.exports = Handlebars.compile(templateString);
-  };
-  require.extensions[".handlebars"] = extension;
-  require.extensions[".hbs"] = extension;
-}
-
-// BEGIN(BROWSER)
-
-// END(BROWSER)
-
-// USAGE:
-// var handlebars = require('handlebars');
-
-// var singleton = handlebars.Handlebars,
-//  local = handlebars.create();
+export default Handlebars;
diff --git a/lib/handlebars.runtime.js b/lib/handlebars.runtime.js
new file mode 100644
index 000000000..a59101919
--- /dev/null
+++ b/lib/handlebars.runtime.js
@@ -0,0 +1,30 @@
+module base from "./handlebars/base";
+
+// Each of these augment the Handlebars object. No need to setup here.
+// (This is done to easily share code between commonjs and browse envs)
+import SafeString from "./handlebars/safe-string";
+import Exception from "./handlebars/exception";
+module Utils from "./handlebars/utils";
+module runtime from "./handlebars/runtime";
+
+// For compatibility and usage outside of module systems, make the Handlebars object a namespace
+var create = function() {
+  var hb = new base.HandlebarsEnvironment();
+
+  Utils.extend(hb, base);
+  hb.SafeString = SafeString;
+  hb.Exception = Exception;
+  hb.Utils = Utils;
+
+  hb.VM = runtime;
+  hb.template = function(spec) {
+    return runtime.template(spec, hb);
+  };
+
+  return hb;
+};
+
+var Handlebars = create();
+Handlebars.create = create;
+
+export default Handlebars;
diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js
index 44a369c5e..236a3c272 100644
--- a/lib/handlebars/base.js
+++ b/lib/handlebars/base.js
@@ -1,166 +1,189 @@
-/*jshint eqnull: true */
+/*globals Exception, Utils */
+module Utils from "./utils";
+import Exception from "./exception";
 
-module.exports.create = function() {
+export var VERSION = "1.1.0";
+export var COMPILER_REVISION = 4;
 
-var Handlebars = {};
-
-// BEGIN(BROWSER)
-
-Handlebars.VERSION = "1.0.0";
-Handlebars.COMPILER_REVISION = 4;
-
-Handlebars.REVISION_CHANGES = {
+export var REVISION_CHANGES = {
   1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
   2: '== 1.0.0-rc.3',
   3: '== 1.0.0-rc.4',
   4: '>= 1.0.0'
 };
 
-Handlebars.helpers  = {};
-Handlebars.partials = {};
-
 var toString = Object.prototype.toString,
-    functionType = '[object Function]',
     objectType = '[object Object]';
 
-Handlebars.registerHelper = function(name, fn, inverse) {
-  if (toString.call(name) === objectType) {
-    if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); }
-    Handlebars.Utils.extend(this.helpers, name);
-  } else {
-    if (inverse) { fn.not = inverse; }
-    this.helpers[name] = fn;
-  }
+// Sourced from lodash
+// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt
+var isFunction = function(value) {
+  return typeof value === 'function';
 };
+// fallback for older versions of Chrome and Safari
+if (isFunction(/x/)) {
+  isFunction = function(value) {
+    return typeof value === 'function' && toString.call(value) === '[object Function]';
+  };
+}
+
+function isArray(value) {
+  return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
+}
+
+export function HandlebarsEnvironment(helpers, partials) {
+  this.helpers = helpers || {};
+  this.partials = partials || {};
+
+  registerDefaultHelpers(this);
+}
+
+HandlebarsEnvironment.prototype = {
+  constructor: HandlebarsEnvironment,
+
+  logger: logger,
+  log: log,
+
+  registerHelper: function(name, fn, inverse) {
+    if (toString.call(name) === objectType) {
+      if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); }
+      Utils.extend(this.helpers, name);
+    } else {
+      if (inverse) { fn.not = inverse; }
+      this.helpers[name] = fn;
+    }
+  },
 
-Handlebars.registerPartial = function(name, str) {
-  if (toString.call(name) === objectType) {
-    Handlebars.Utils.extend(this.partials,  name);
-  } else {
-    this.partials[name] = str;
+  registerPartial: function(name, str) {
+    if (toString.call(name) === objectType) {
+      Utils.extend(this.partials,  name);
+    } else {
+      this.partials[name] = str;
+    }
   }
 };
 
-Handlebars.registerHelper('helperMissing', function(arg) {
-  if(arguments.length === 2) {
-    return undefined;
-  } else {
-    throw new Error("Missing helper: '" + arg + "'");
-  }
-});
-
-Handlebars.registerHelper('blockHelperMissing', function(context, options) {
-  var inverse = options.inverse || function() {}, fn = options.fn;
+function registerDefaultHelpers(instance) {
+  instance.registerHelper('helperMissing', function(arg) {
+    if(arguments.length === 2) {
+      return undefined;
+    } else {
+      throw new Error("Missing helper: '" + arg + "'");
+    }
+  });
 
-  var type = toString.call(context);
+  instance.registerHelper('blockHelperMissing', function(context, options) {
+    var inverse = options.inverse || function() {}, fn = options.fn;
 
-  if(type === functionType) { context = context.call(this); }
+    if (isFunction(context)) { context = context.call(this); }
 
-  if(context === true) {
-    return fn(this);
-  } else if(context === false || context == null) {
-    return inverse(this);
-  } else if(type === "[object Array]") {
-    if(context.length > 0) {
-      return Handlebars.helpers.each(context, options);
-    } else {
+    if(context === true) {
+      return fn(this);
+    } else if(context === false || context == null) {
       return inverse(this);
+    } else if (isArray(context)) {
+      if(context.length > 0) {
+        return instance.helpers.each(context, options);
+      } else {
+        return inverse(this);
+      }
+    } else {
+      return fn(context);
     }
-  } else {
-    return fn(context);
-  }
-});
-
-Handlebars.K = function() {};
+  });
 
-Handlebars.createFrame = Object.create || function(object) {
-  Handlebars.K.prototype = object;
-  var obj = new Handlebars.K();
-  Handlebars.K.prototype = null;
-  return obj;
-};
+  instance.registerHelper('each', function(context, options) {
+    var fn = options.fn, inverse = options.inverse;
+    var i = 0, ret = "", data;
 
-Handlebars.logger = {
-  DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
+    if (isFunction(context)) { context = context.call(this); }
 
-  methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'},
+    if (options.data) {
+      data = createFrame(options.data);
+    }
 
-  // can be overridden in the host environment
-  log: function(level, obj) {
-    if (Handlebars.logger.level <= level) {
-      var method = Handlebars.logger.methodMap[level];
-      if (typeof console !== 'undefined' && console[method]) {
-        console[method].call(console, obj);
+    if(context && typeof context === 'object') {
+      if (isArray(context)) {
+        for(var j = context.length; i<j; i++) {
+          if (data) {
+            data.index = i;
+            data.first = (i === 0)
+            data.last  = (i === (context.length-1));
+          }
+          ret = ret + fn(context[i], { data: data });
+        }
+      } else {
+        for(var key in context) {
+          if(context.hasOwnProperty(key)) {
+            if(data) { data.key = key; }
+            ret = ret + fn(context[key], {data: data});
+            i++;
+          }
+        }
       }
     }
-  }
-};
-
-Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); };
 
-Handlebars.registerHelper('each', function(context, options) {
-  var fn = options.fn, inverse = options.inverse;
-  var i = 0, ret = "", data;
+    if(i === 0){
+      ret = inverse(this);
+    }
 
-  var type = toString.call(context);
-  if(type === functionType) { context = context.call(this); }
+    return ret;
+  });
 
-  if (options.data) {
-    data = Handlebars.createFrame(options.data);
-  }
+  instance.registerHelper('if', function(conditional, options) {
+    if (isFunction(conditional)) { conditional = conditional.call(this); }
 
-  if(context && typeof context === 'object') {
-    if(context instanceof Array){
-      for(var j = context.length; i<j; i++) {
-        if (data) { data.index = i; }
-        ret = ret + fn(context[i], { data: data });
-      }
+    // Default behavior is to render the positive path if the value is truthy and not empty.
+    // The `includeZero` option may be set to treat the condtional as purely not empty based on the
+    // behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative.
+    if ((!options.hash.includeZero && !conditional) || Utils.isEmpty(conditional)) {
+      return options.inverse(this);
     } else {
-      for(var key in context) {
-        if(context.hasOwnProperty(key)) {
-          if(data) { data.key = key; }
-          ret = ret + fn(context[key], {data: data});
-          i++;
-        }
-      }
+      return options.fn(this);
     }
-  }
+  });
 
-  if(i === 0){
-    ret = inverse(this);
-  }
-
-  return ret;
-});
+  instance.registerHelper('unless', function(conditional, options) {
+    return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash});
+  });
 
-Handlebars.registerHelper('if', function(conditional, options) {
-  var type = toString.call(conditional);
-  if(type === functionType) { conditional = conditional.call(this); }
+  instance.registerHelper('with', function(context, options) {
+    if (isFunction(context)) { context = context.call(this); }
 
-  if(!conditional || Handlebars.Utils.isEmpty(conditional)) {
-    return options.inverse(this);
-  } else {
-    return options.fn(this);
-  }
-});
+    if (!Utils.isEmpty(context)) return options.fn(context);
+  });
 
-Handlebars.registerHelper('unless', function(conditional, options) {
-  return Handlebars.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn});
-});
+  instance.registerHelper('log', function(context, options) {
+    var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
+    instance.log(level, context);
+  });
+}
 
-Handlebars.registerHelper('with', function(context, options) {
-  var type = toString.call(context);
-  if(type === functionType) { context = context.call(this); }
+export var logger = {
+  methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' },
 
-  if (!Handlebars.Utils.isEmpty(context)) return options.fn(context);
-});
+  // State enum
+  DEBUG: 0,
+  INFO: 1,
+  WARN: 2,
+  ERROR: 3,
+  level: 3,
 
-Handlebars.registerHelper('log', function(context, options) {
-  var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
-  Handlebars.log(level, context);
-});
+  // can be overridden in the host environment
+  log: function(level, obj) {
+    if (logger.level <= level) {
+      var method = logger.methodMap[level];
+      if (typeof console !== 'undefined' && console[method]) {
+        console[method].call(console, obj);
+      }
+    }
+  }
+};
 
-// END(BROWSER)
+export function log(level, obj) { logger.log(level, obj); }
 
-return Handlebars;
+export var createFrame = function(object) {
+  var obj = {};
+  Utils.extend(obj, object);
+  return obj;
 };
diff --git a/lib/handlebars/browser-prefix.js b/lib/handlebars/browser-prefix.js
deleted file mode 100644
index 138467d68..000000000
--- a/lib/handlebars/browser-prefix.js
+++ /dev/null
@@ -1,3 +0,0 @@
-var Handlebars = {};
-
-(function(Handlebars, undefined) {
diff --git a/lib/handlebars/browser-suffix.js b/lib/handlebars/browser-suffix.js
deleted file mode 100644
index 36c52c091..000000000
--- a/lib/handlebars/browser-suffix.js
+++ /dev/null
@@ -1 +0,0 @@
-})(Handlebars);
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js
index c99728ccd..f6229e299 100644
--- a/lib/handlebars/compiler/ast.js
+++ b/lib/handlebars/compiler/ast.js
@@ -1,18 +1,25 @@
-exports.attach = function(Handlebars) {
+import Exception from "../exception";
 
-// BEGIN(BROWSER)
-Handlebars.AST = {};
-
-Handlebars.AST.ProgramNode = function(statements, inverse) {
+export function ProgramNode(statements, inverseStrip, inverse) {
   this.type = "program";
   this.statements = statements;
-  if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); }
-};
+  this.strip = {};
+
+  if(inverse) {
+    this.inverse = new ProgramNode(inverse, inverseStrip);
+    this.strip.right = inverseStrip.left;
+  } else if (inverseStrip) {
+    this.strip.left = inverseStrip.right;
+  }
+}
 
-Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) {
+export function MustacheNode(rawParams, hash, open, strip) {
   this.type = "mustache";
-  this.escaped = !unescaped;
   this.hash = hash;
+  this.strip = strip;
+
+  var escapeFlag = open[3] || open[2];
+  this.escaped = escapeFlag !== '{' && escapeFlag !== '&';
 
   var id = this.id = rawParams[0];
   var params = this.params = rawParams.slice(1);
@@ -29,43 +36,49 @@ Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) {
   // if a mustache is an eligible helper but not a definite
   // helper, it is ambiguous, and will be resolved in a later
   // pass or at runtime.
-};
+}
 
-Handlebars.AST.PartialNode = function(partialName, context) {
+export function PartialNode(partialName, context, strip) {
   this.type         = "partial";
   this.partialName  = partialName;
   this.context      = context;
-};
+  this.strip = strip;
+}
 
-Handlebars.AST.BlockNode = function(mustache, program, inverse, close) {
-  var verifyMatch = function(open, close) {
-    if(open.original !== close.original) {
-      throw new Handlebars.Exception(open.original + " doesn't match " + close.original);
-    }
-  };
+export function BlockNode(mustache, program, inverse, close) {
+  if(mustache.id.original !== close.path.original) {
+    throw new Exception(mustache.id.original + " doesn't match " + close.path.original);
+  }
 
-  verifyMatch(mustache.id, close);
   this.type = "block";
   this.mustache = mustache;
   this.program  = program;
   this.inverse  = inverse;
 
-  if (this.inverse && !this.program) {
+  this.strip = {
+    left: mustache.strip.left,
+    right: close.strip.right
+  };
+
+  (program || inverse).strip.left = mustache.strip.right;
+  (inverse || program).strip.right = close.strip.left;
+
+  if (inverse && !program) {
     this.isInverse = true;
   }
-};
+}
 
-Handlebars.AST.ContentNode = function(string) {
+export function ContentNode(string) {
   this.type = "content";
   this.string = string;
-};
+}
 
-Handlebars.AST.HashNode = function(pairs) {
+export function HashNode(pairs) {
   this.type = "hash";
   this.pairs = pairs;
-};
+}
 
-Handlebars.AST.IdNode = function(parts) {
+export function IdNode(parts) {
   this.type = "ID";
 
   var original = "",
@@ -77,7 +90,7 @@ Handlebars.AST.IdNode = function(parts) {
     original += (parts[i].separator || '') + part;
 
     if (part === ".." || part === "." || part === "this") {
-      if (dig.length > 0) { throw new Handlebars.Exception("Invalid path: " + original); }
+      if (dig.length > 0) { throw new Exception("Invalid path: " + original); }
       else if (part === "..") { depth++; }
       else { this.isScoped = true; }
     }
@@ -94,45 +107,39 @@ Handlebars.AST.IdNode = function(parts) {
   this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
 
   this.stringModeValue = this.string;
-};
+}
 
-Handlebars.AST.PartialNameNode = function(name) {
+export function PartialNameNode(name) {
   this.type = "PARTIAL_NAME";
   this.name = name.original;
-};
+}
 
-Handlebars.AST.DataNode = function(id) {
+export function DataNode(id) {
   this.type = "DATA";
   this.id = id;
-};
+}
 
-Handlebars.AST.StringNode = function(string) {
+export function StringNode(string) {
   this.type = "STRING";
   this.original =
     this.string =
     this.stringModeValue = string;
-};
+}
 
-Handlebars.AST.IntegerNode = function(integer) {
+export function IntegerNode(integer) {
   this.type = "INTEGER";
   this.original =
     this.integer = integer;
   this.stringModeValue = Number(integer);
-};
+}
 
-Handlebars.AST.BooleanNode = function(bool) {
+export function BooleanNode(bool) {
   this.type = "BOOLEAN";
   this.bool = bool;
   this.stringModeValue = bool === "true";
-};
+}
 
-Handlebars.AST.CommentNode = function(comment) {
+export function CommentNode(comment) {
   this.type = "comment";
   this.comment = comment;
-};
-
-// END(BROWSER)
-
-return Handlebars;
-};
-
+}
diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js
index 759445154..d6cb06ee2 100644
--- a/lib/handlebars/compiler/base.js
+++ b/lib/handlebars/compiler/base.js
@@ -1,21 +1,12 @@
-var handlebars = require("./parser");
+import parser from "./parser";
+module AST from "./ast";
 
-exports.attach = function(Handlebars) {
-
-// BEGIN(BROWSER)
-
-Handlebars.Parser = handlebars;
-
-Handlebars.parse = function(input) {
+export { parser };
 
+export function parse(input) {
   // Just return if an already-compile AST was passed in.
-  if(input.constructor === Handlebars.AST.ProgramNode) { return input; }
-
-  Handlebars.Parser.yy = Handlebars.AST;
-  return Handlebars.Parser.parse(input);
-};
-
-// END(BROWSER)
+  if(input.constructor === AST.ProgramNode) { return input; }
 
-return Handlebars;
-};
+  parser.yy = AST;
+  return parser.parse(input);
+}
diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js
index 8bb1fc521..4f232ebce 100644
--- a/lib/handlebars/compiler/compiler.js
+++ b/lib/handlebars/compiler/compiler.js
@@ -1,14 +1,9 @@
-var compilerbase = require("./base");
+import Exception from "../exception";
+import { parse } from "./base";
+import JavaScriptCompiler from "./javascript-compiler";
+module AST from "./ast";
 
-exports.attach = function(Handlebars) {
-
-compilerbase.attach(Handlebars);
-
-// BEGIN(BROWSER)
-
-/*jshint eqnull:true*/
-var Compiler = Handlebars.Compiler = function() {};
-var JavaScriptCompiler = Handlebars.JavaScriptCompiler = function() {};
+export function Compiler() {}
 
 // the foundHelper register will disambiguate helper lookup from finding a
 // function in a context. This is necessary for mustache compatibility, which
@@ -41,6 +36,7 @@ Compiler.prototype = {
 
     return out.join("\n");
   },
+
   equals: function(other) {
     var len = this.opcodes.length;
     if (other.opcodes.length !== len) {
@@ -76,6 +72,7 @@ Compiler.prototype = {
   guid: 0,
 
   compile: function(program, options) {
+    this.opcodes = [];
     this.children = [];
     this.depths = {list: []};
     this.options = options;
@@ -97,20 +94,30 @@ Compiler.prototype = {
       }
     }
 
-    return this.program(program);
+    return this.accept(program);
   },
 
   accept: function(node) {
-    return this[node.type](node);
+    var strip = node.strip || {},
+        ret;
+    if (strip.left) {
+      this.opcode('strip');
+    }
+
+    ret = this[node.type](node);
+
+    if (strip.right) {
+      this.opcode('strip');
+    }
+
+    return ret;
   },
 
   program: function(program) {
-    var statements = program.statements, statement;
-    this.opcodes = [];
+    var statements = program.statements;
 
     for(var i=0, l=statements.length; i<l; i++) {
-      statement = statements[i];
-      this[statement.type](statement);
+      this.accept(statements[i]);
     }
     this.isSimple = l === 1;
 
@@ -302,7 +309,7 @@ Compiler.prototype = {
   DATA: function(data) {
     this.options.data = true;
     if (data.id.isScoped || data.id.depth) {
-      throw new Handlebars.Exception('Scoped data references are not supported: ' + data.original);
+      throw new Exception('Scoped data references are not supported: ' + data.original);
     }
 
     this.opcode('lookupData');
@@ -416,890 +423,46 @@ Compiler.prototype = {
   }
 };
 
-var Literal = function(value) {
-  this.value = value;
-};
-
-JavaScriptCompiler.prototype = {
-  // PUBLIC API: You can override these methods in a subclass to provide
-  // alternative compiled forms for name lookup and buffering semantics
-  nameLookup: function(parent, name /* , type*/) {
-    if (/^[0-9]+$/.test(name)) {
-      return parent + "[" + name + "]";
-    } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
-      return parent + "." + name;
-    }
-    else {
-      return parent + "['" + name + "']";
-    }
-  },
-
-  appendToBuffer: function(string) {
-    if (this.environment.isSimple) {
-      return "return " + string + ";";
-    } else {
-      return {
-        appendToBuffer: true,
-        content: string,
-        toString: function() { return "buffer += " + string + ";"; }
-      };
-    }
-  },
-
-  initializeBuffer: function() {
-    return this.quotedString("");
-  },
-
-  namespace: "Handlebars",
-  // END PUBLIC API
-
-  compile: function(environment, options, context, asObject) {
-    this.environment = environment;
-    this.options = options || {};
-
-    Handlebars.log(Handlebars.logger.DEBUG, this.environment.disassemble() + "\n\n");
-
-    this.name = this.environment.name;
-    this.isChild = !!context;
-    this.context = context || {
-      programs: [],
-      environments: [],
-      aliases: { }
-    };
-
-    this.preamble();
-
-    this.stackSlot = 0;
-    this.stackVars = [];
-    this.registers = { list: [] };
-    this.compileStack = [];
-    this.inlineStack = [];
-
-    this.compileChildren(environment, options);
-
-    var opcodes = environment.opcodes, opcode;
-
-    this.i = 0;
-
-    for(l=opcodes.length; this.i<l; this.i++) {
-      opcode = opcodes[this.i];
-
-      if(opcode.opcode === 'DECLARE') {
-        this[opcode.name] = opcode.value;
-      } else {
-        this[opcode.opcode].apply(this, opcode.args);
-      }
-    }
-
-    return this.createFunctionContext(asObject);
-  },
-
-  nextOpcode: function() {
-    var opcodes = this.environment.opcodes;
-    return opcodes[this.i + 1];
-  },
-
-  eat: function() {
-    this.i = this.i + 1;
-  },
-
-  preamble: function() {
-    var out = [];
-
-    if (!this.isChild) {
-      var namespace = this.namespace;
-
-      var copies = "helpers = this.merge(helpers, " + namespace + ".helpers);";
-      if (this.environment.usePartial) { copies = copies + " partials = this.merge(partials, " + namespace + ".partials);"; }
-      if (this.options.data) { copies = copies + " data = data || {};"; }
-      out.push(copies);
-    } else {
-      out.push('');
-    }
-
-    if (!this.environment.isSimple) {
-      out.push(", buffer = " + this.initializeBuffer());
-    } else {
-      out.push("");
-    }
-
-    // track the last context pushed into place to allow skipping the
-    // getContext opcode when it would be a noop
-    this.lastContext = 0;
-    this.source = out;
-  },
-
-  createFunctionContext: function(asObject) {
-    var locals = this.stackVars.concat(this.registers.list);
-
-    if(locals.length > 0) {
-      this.source[1] = this.source[1] + ", " + locals.join(", ");
-    }
-
-    // Generate minimizer alias mappings
-    if (!this.isChild) {
-      for (var alias in this.context.aliases) {
-        if (this.context.aliases.hasOwnProperty(alias)) {
-          this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
-        }
-      }
-    }
-
-    if (this.source[1]) {
-      this.source[1] = "var " + this.source[1].substring(2) + ";";
-    }
-
-    // Merge children
-    if (!this.isChild) {
-      this.source[1] += '\n' + this.context.programs.join('\n') + '\n';
-    }
-
-    if (!this.environment.isSimple) {
-      this.source.push("return buffer;");
-    }
-
-    var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"];
-
-    for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
-      params.push("depth" + this.environment.depths.list[i]);
-    }
-
-    // Perform a second pass over the output to merge content when possible
-    var source = this.mergeSource();
-
-    if (!this.isChild) {
-      var revision = Handlebars.COMPILER_REVISION,
-          versions = Handlebars.REVISION_CHANGES[revision];
-      source = "this.compilerInfo = ["+revision+",'"+versions+"'];\n"+source;
-    }
-
-    if (asObject) {
-      params.push(source);
-
-      return Function.apply(this, params);
-    } else {
-      var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n  ' + source + '}';
-      Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n");
-      return functionSource;
-    }
-  },
-  mergeSource: function() {
-    // WARN: We are not handling the case where buffer is still populated as the source should
-    // not have buffer append operations as their final action.
-    var source = '',
-        buffer;
-    for (var i = 0, len = this.source.length; i < len; i++) {
-      var line = this.source[i];
-      if (line.appendToBuffer) {
-        if (buffer) {
-          buffer = buffer + '\n    + ' + line.content;
-        } else {
-          buffer = line.content;
-        }
-      } else {
-        if (buffer) {
-          source += 'buffer += ' + buffer + ';\n  ';
-          buffer = undefined;
-        }
-        source += line + '\n  ';
-      }
-    }
-    return source;
-  },
-
-  // [blockValue]
-  //
-  // On stack, before: hash, inverse, program, value
-  // On stack, after: return value of blockHelperMissing
-  //
-  // The purpose of this opcode is to take a block of the form
-  // `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and
-  // replace it on the stack with the result of properly
-  // invoking blockHelperMissing.
-  blockValue: function() {
-    this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
-
-    var params = ["depth0"];
-    this.setupParams(0, params);
-
-    this.replaceStack(function(current) {
-      params.splice(1, 0, current);
-      return "blockHelperMissing.call(" + params.join(", ") + ")";
-    });
-  },
-
-  // [ambiguousBlockValue]
-  //
-  // On stack, before: hash, inverse, program, value
-  // Compiler value, before: lastHelper=value of last found helper, if any
-  // On stack, after, if no lastHelper: same as [blockValue]
-  // On stack, after, if lastHelper: value
-  ambiguousBlockValue: function() {
-    this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
-
-    var params = ["depth0"];
-    this.setupParams(0, params);
-
-    var current = this.topStack();
-    params.splice(1, 0, current);
-
-    // Use the options value generated from the invocation
-    params[params.length-1] = 'options';
-
-    this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
-  },
-
-  // [appendContent]
-  //
-  // On stack, before: ...
-  // On stack, after: ...
-  //
-  // Appends the string value of `content` to the current buffer
-  appendContent: function(content) {
-    this.source.push(this.appendToBuffer(this.quotedString(content)));
-  },
-
-  // [append]
-  //
-  // On stack, before: value, ...
-  // On stack, after: ...
-  //
-  // Coerces `value` to a String and appends it to the current buffer.
-  //
-  // If `value` is truthy, or 0, it is coerced into a string and appended
-  // Otherwise, the empty string is appended
-  append: function() {
-    // Force anything that is inlined onto the stack so we don't have duplication
-    // when we examine local
-    this.flushInline();
-    var local = this.popStack();
-    this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
-    if (this.environment.isSimple) {
-      this.source.push("else { " + this.appendToBuffer("''") + " }");
-    }
-  },
-
-  // [appendEscaped]
-  //
-  // On stack, before: value, ...
-  // On stack, after: ...
-  //
-  // Escape `value` and append it to the buffer
-  appendEscaped: function() {
-    this.context.aliases.escapeExpression = 'this.escapeExpression';
-
-    this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
-  },
-
-  // [getContext]
-  //
-  // On stack, before: ...
-  // On stack, after: ...
-  // Compiler value, after: lastContext=depth
-  //
-  // Set the value of the `lastContext` compiler value to the depth
-  getContext: function(depth) {
-    if(this.lastContext !== depth) {
-      this.lastContext = depth;
-    }
-  },
-
-  // [lookupOnContext]
-  //
-  // On stack, before: ...
-  // On stack, after: currentContext[name], ...
-  //
-  // Looks up the value of `name` on the current context and pushes
-  // it onto the stack.
-  lookupOnContext: function(name) {
-    this.push(this.nameLookup('depth' + this.lastContext, name, 'context'));
-  },
-
-  // [pushContext]
-  //
-  // On stack, before: ...
-  // On stack, after: currentContext, ...
-  //
-  // Pushes the value of the current context onto the stack.
-  pushContext: function() {
-    this.pushStackLiteral('depth' + this.lastContext);
-  },
-
-  // [resolvePossibleLambda]
-  //
-  // On stack, before: value, ...
-  // On stack, after: resolved value, ...
-  //
-  // If the `value` is a lambda, replace it on the stack by
-  // the return value of the lambda
-  resolvePossibleLambda: function() {
-    this.context.aliases.functionType = '"function"';
-
-    this.replaceStack(function(current) {
-      return "typeof " + current + " === functionType ? " + current + ".apply(depth0) : " + current;
-    });
-  },
-
-  // [lookup]
-  //
-  // On stack, before: value, ...
-  // On stack, after: value[name], ...
-  //
-  // Replace the value on the stack with the result of looking
-  // up `name` on `value`
-  lookup: function(name) {
-    this.replaceStack(function(current) {
-      return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context');
-    });
-  },
-
-  // [lookupData]
-  //
-  // On stack, before: ...
-  // On stack, after: data[id], ...
-  //
-  // Push the result of looking up `id` on the current data
-  lookupData: function(id) {
-    this.push('data');
-  },
-
-  // [pushStringParam]
-  //
-  // On stack, before: ...
-  // On stack, after: string, currentContext, ...
-  //
-  // This opcode is designed for use in string mode, which
-  // provides the string value of a parameter along with its
-  // depth rather than resolving it immediately.
-  pushStringParam: function(string, type) {
-    this.pushStackLiteral('depth' + this.lastContext);
-
-    this.pushString(type);
-
-    if (typeof string === 'string') {
-      this.pushString(string);
-    } else {
-      this.pushStackLiteral(string);
-    }
-  },
-
-  emptyHash: function() {
-    this.pushStackLiteral('{}');
-
-    if (this.options.stringParams) {
-      this.register('hashTypes', '{}');
-      this.register('hashContexts', '{}');
-    }
-  },
-  pushHash: function() {
-    this.hash = {values: [], types: [], contexts: []};
-  },
-  popHash: function() {
-    var hash = this.hash;
-    this.hash = undefined;
-
-    if (this.options.stringParams) {
-      this.register('hashContexts', '{' + hash.contexts.join(',') + '}');
-      this.register('hashTypes', '{' + hash.types.join(',') + '}');
-    }
-    this.push('{\n    ' + hash.values.join(',\n    ') + '\n  }');
-  },
-
-  // [pushString]
-  //
-  // On stack, before: ...
-  // On stack, after: quotedString(string), ...
-  //
-  // Push a quoted version of `string` onto the stack
-  pushString: function(string) {
-    this.pushStackLiteral(this.quotedString(string));
-  },
-
-  // [push]
-  //
-  // On stack, before: ...
-  // On stack, after: expr, ...
-  //
-  // Push an expression onto the stack
-  push: function(expr) {
-    this.inlineStack.push(expr);
-    return expr;
-  },
-
-  // [pushLiteral]
-  //
-  // On stack, before: ...
-  // On stack, after: value, ...
-  //
-  // Pushes a value onto the stack. This operation prevents
-  // the compiler from creating a temporary variable to hold
-  // it.
-  pushLiteral: function(value) {
-    this.pushStackLiteral(value);
-  },
-
-  // [pushProgram]
-  //
-  // On stack, before: ...
-  // On stack, after: program(guid), ...
-  //
-  // Push a program expression onto the stack. This takes
-  // a compile-time guid and converts it into a runtime-accessible
-  // expression.
-  pushProgram: function(guid) {
-    if (guid != null) {
-      this.pushStackLiteral(this.programExpression(guid));
-    } else {
-      this.pushStackLiteral(null);
-    }
-  },
-
-  // [invokeHelper]
-  //
-  // On stack, before: hash, inverse, program, params..., ...
-  // On stack, after: result of helper invocation
-  //
-  // Pops off the helper's parameters, invokes the helper,
-  // and pushes the helper's return value onto the stack.
-  //
-  // If the helper is not found, `helperMissing` is called.
-  invokeHelper: function(paramSize, name) {
-    this.context.aliases.helperMissing = 'helpers.helperMissing';
-
-    var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
-    var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
-
-    this.push(helper.name + ' || ' + nonHelper);
-    this.replaceStack(function(name) {
-      return name + ' ? ' + name + '.call(' +
-          helper.callParams + ") " + ": helperMissing.call(" +
-          helper.helperMissingParams + ")";
-    });
-  },
-
-  // [invokeKnownHelper]
-  //
-  // On stack, before: hash, inverse, program, params..., ...
-  // On stack, after: result of helper invocation
-  //
-  // This operation is used when the helper is known to exist,
-  // so a `helperMissing` fallback is not required.
-  invokeKnownHelper: function(paramSize, name) {
-    var helper = this.setupHelper(paramSize, name);
-    this.push(helper.name + ".call(" + helper.callParams + ")");
-  },
-
-  // [invokeAmbiguous]
-  //
-  // On stack, before: hash, inverse, program, params..., ...
-  // On stack, after: result of disambiguation
-  //
-  // This operation is used when an expression like `{{foo}}`
-  // is provided, but we don't know at compile-time whether it
-  // is a helper or a path.
-  //
-  // This operation emits more code than the other options,
-  // and can be avoided by passing the `knownHelpers` and
-  // `knownHelpersOnly` flags at compile-time.
-  invokeAmbiguous: function(name, helperCall) {
-    this.context.aliases.functionType = '"function"';
-
-    this.pushStackLiteral('{}');    // Hash value
-    var helper = this.setupHelper(0, name, helperCall);
-
-    var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
-
-    var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
-    var nextStack = this.nextStack();
-
-    this.source.push('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }');
-    this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.apply(depth0) : ' + nextStack + '; }');
-  },
-
-  // [invokePartial]
-  //
-  // On stack, before: context, ...
-  // On stack after: result of partial invocation
-  //
-  // This operation pops off a context, invokes a partial with that context,
-  // and pushes the result of the invocation back.
-  invokePartial: function(name) {
-    var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"];
-
-    if (this.options.data) {
-      params.push("data");
-    }
-
-    this.context.aliases.self = "this";
-    this.push("self.invokePartial(" + params.join(", ") + ")");
-  },
-
-  // [assignToHash]
-  //
-  // On stack, before: value, hash, ...
-  // On stack, after: hash, ...
-  //
-  // Pops a value and hash off the stack, assigns `hash[key] = value`
-  // and pushes the hash back onto the stack.
-  assignToHash: function(key) {
-    var value = this.popStack(),
-        context,
-        type;
-
-    if (this.options.stringParams) {
-      type = this.popStack();
-      context = this.popStack();
-    }
-
-    var hash = this.hash;
-    if (context) {
-      hash.contexts.push("'" + key + "': " + context);
-    }
-    if (type) {
-      hash.types.push("'" + key + "': " + type);
-    }
-    hash.values.push("'" + key + "': (" + value + ")");
-  },
-
-  // HELPERS
-
-  compiler: JavaScriptCompiler,
-
-  compileChildren: function(environment, options) {
-    var children = environment.children, child, compiler;
-
-    for(var i=0, l=children.length; i<l; i++) {
-      child = children[i];
-      compiler = new this.compiler();
-
-      var index = this.matchExistingProgram(child);
-
-      if (index == null) {
-        this.context.programs.push('');     // Placeholder to prevent name conflicts for nested children
-        index = this.context.programs.length;
-        child.index = index;
-        child.name = 'program' + index;
-        this.context.programs[index] = compiler.compile(child, options, this.context);
-        this.context.environments[index] = child;
-      } else {
-        child.index = index;
-        child.name = 'program' + index;
-      }
-    }
-  },
-  matchExistingProgram: function(child) {
-    for (var i = 0, len = this.context.environments.length; i < len; i++) {
-      var environment = this.context.environments[i];
-      if (environment && environment.equals(child)) {
-        return i;
-      }
-    }
-  },
-
-  programExpression: function(guid) {
-    this.context.aliases.self = "this";
-
-    if(guid == null) {
-      return "self.noop";
-    }
-
-    var child = this.environment.children[guid],
-        depths = child.depths.list, depth;
-
-    var programParams = [child.index, child.name, "data"];
-
-    for(var i=0, l = depths.length; i<l; i++) {
-      depth = depths[i];
-
-      if(depth === 1) { programParams.push("depth0"); }
-      else { programParams.push("depth" + (depth - 1)); }
-    }
-
-    return (depths.length === 0 ? "self.program(" : "self.programWithDepth(") + programParams.join(", ") + ")";
-  },
-
-  register: function(name, val) {
-    this.useRegister(name);
-    this.source.push(name + " = " + val + ";");
-  },
-
-  useRegister: function(name) {
-    if(!this.registers[name]) {
-      this.registers[name] = true;
-      this.registers.list.push(name);
-    }
-  },
-
-  pushStackLiteral: function(item) {
-    return this.push(new Literal(item));
-  },
-
-  pushStack: function(item) {
-    this.flushInline();
-
-    var stack = this.incrStack();
-    if (item) {
-      this.source.push(stack + " = " + item + ";");
-    }
-    this.compileStack.push(stack);
-    return stack;
-  },
-
-  replaceStack: function(callback) {
-    var prefix = '',
-        inline = this.isInline(),
-        stack;
-
-    // If we are currently inline then we want to merge the inline statement into the
-    // replacement statement via ','
-    if (inline) {
-      var top = this.popStack(true);
-
-      if (top instanceof Literal) {
-        // Literals do not need to be inlined
-        stack = top.value;
-      } else {
-        // Get or create the current stack name for use by the inline
-        var name = this.stackSlot ? this.topStackName() : this.incrStack();
-
-        prefix = '(' + this.push(name) + ' = ' + top + '),';
-        stack = this.topStack();
-      }
-    } else {
-      stack = this.topStack();
-    }
-
-    var item = callback.call(this, stack);
-
-    if (inline) {
-      if (this.inlineStack.length || this.compileStack.length) {
-        this.popStack();
-      }
-      this.push('(' + prefix + item + ')');
-    } else {
-      // Prevent modification of the context depth variable. Through replaceStack
-      if (!/^stack/.test(stack)) {
-        stack = this.nextStack();
-      }
-
-      this.source.push(stack + " = (" + prefix + item + ");");
-    }
-    return stack;
-  },
-
-  nextStack: function() {
-    return this.pushStack();
-  },
-
-  incrStack: function() {
-    this.stackSlot++;
-    if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
-    return this.topStackName();
-  },
-  topStackName: function() {
-    return "stack" + this.stackSlot;
-  },
-  flushInline: function() {
-    var inlineStack = this.inlineStack;
-    if (inlineStack.length) {
-      this.inlineStack = [];
-      for (var i = 0, len = inlineStack.length; i < len; i++) {
-        var entry = inlineStack[i];
-        if (entry instanceof Literal) {
-          this.compileStack.push(entry);
-        } else {
-          this.pushStack(entry);
-        }
-      }
-    }
-  },
-  isInline: function() {
-    return this.inlineStack.length;
-  },
-
-  popStack: function(wrapped) {
-    var inline = this.isInline(),
-        item = (inline ? this.inlineStack : this.compileStack).pop();
-
-    if (!wrapped && (item instanceof Literal)) {
-      return item.value;
-    } else {
-      if (!inline) {
-        this.stackSlot--;
-      }
-      return item;
-    }
-  },
-
-  topStack: function(wrapped) {
-    var stack = (this.isInline() ? this.inlineStack : this.compileStack),
-        item = stack[stack.length - 1];
-
-    if (!wrapped && (item instanceof Literal)) {
-      return item.value;
-    } else {
-      return item;
-    }
-  },
-
-  quotedString: function(str) {
-    return '"' + str
-      .replace(/\\/g, '\\\\')
-      .replace(/"/g, '\\"')
-      .replace(/\n/g, '\\n')
-      .replace(/\r/g, '\\r')
-      .replace(/\u2028/g, '\\u2028')   // Per Ecma-262 7.3 + 7.8.4
-      .replace(/\u2029/g, '\\u2029') + '"';
-  },
-
-  setupHelper: function(paramSize, name, missingParams) {
-    var params = [];
-    this.setupParams(paramSize, params, missingParams);
-    var foundHelper = this.nameLookup('helpers', name, 'helper');
-
-    return {
-      params: params,
-      name: foundHelper,
-      callParams: ["depth0"].concat(params).join(", "),
-      helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
-    };
-  },
-
-  // the params and contexts arguments are passed in arrays
-  // to fill in
-  setupParams: function(paramSize, params, useRegister) {
-    var options = [], contexts = [], types = [], param, inverse, program;
-
-    options.push("hash:" + this.popStack());
-
-    inverse = this.popStack();
-    program = this.popStack();
-
-    // Avoid setting fn and inverse if neither are set. This allows
-    // helpers to do a check for `if (options.fn)`
-    if (program || inverse) {
-      if (!program) {
-        this.context.aliases.self = "this";
-        program = "self.noop";
-      }
-
-      if (!inverse) {
-       this.context.aliases.self = "this";
-        inverse = "self.noop";
-      }
-
-      options.push("inverse:" + inverse);
-      options.push("fn:" + program);
-    }
-
-    for(var i=0; i<paramSize; i++) {
-      param = this.popStack();
-      params.push(param);
-
-      if(this.options.stringParams) {
-        types.push(this.popStack());
-        contexts.push(this.popStack());
-      }
-    }
-
-    if (this.options.stringParams) {
-      options.push("contexts:[" + contexts.join(",") + "]");
-      options.push("types:[" + types.join(",") + "]");
-      options.push("hashContexts:hashContexts");
-      options.push("hashTypes:hashTypes");
-    }
-
-    if(this.options.data) {
-      options.push("data:data");
-    }
-
-    options = "{" + options.join(",") + "}";
-    if (useRegister) {
-      this.register('options', options);
-      params.push('options');
-    } else {
-      params.push(options);
-    }
-    return params.join(", ");
-  }
-};
-
-var reservedWords = (
-  "break else new var" +
-  " case finally return void" +
-  " catch for switch while" +
-  " continue function this with" +
-  " default if throw" +
-  " delete in try" +
-  " do instanceof typeof" +
-  " abstract enum int short" +
-  " boolean export interface static" +
-  " byte extends long super" +
-  " char final native synchronized" +
-  " class float package throws" +
-  " const goto private transient" +
-  " debugger implements protected volatile" +
-  " double import public let yield"
-).split(" ");
-
-var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
-
-for(var i=0, l=reservedWords.length; i<l; i++) {
-  compilerWords[reservedWords[i]] = true;
-}
-
-JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
-  if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) {
-    return true;
-  }
-  return false;
-};
-
-Handlebars.precompile = function(input, options) {
-  if (input == null || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) {
-    throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
+export function precompile(input, options) {
+  if (input == null || (typeof input !== 'string' && input.constructor !== AST.ProgramNode)) {
+    throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
   }
 
   options = options || {};
   if (!('data' in options)) {
     options.data = true;
   }
-  var ast = Handlebars.parse(input);
+
+  var ast = parse(input);
   var environment = new Compiler().compile(ast, options);
   return new JavaScriptCompiler().compile(environment, options);
-};
+}
 
-Handlebars.compile = function(input, options) {
-  if (input == null || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) {
-    throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
+export function compile(input, options, env) {
+  if (input == null || (typeof input !== 'string' && input.constructor !== AST.ProgramNode)) {
+    throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
   }
 
   options = options || {};
+
   if (!('data' in options)) {
     options.data = true;
   }
+
   var compiled;
-  function compile() {
-    var ast = Handlebars.parse(input);
+
+  function compileInput() {
+    var ast = parse(input);
     var environment = new Compiler().compile(ast, options);
     var templateSpec = new JavaScriptCompiler().compile(environment, options, undefined, true);
-    return Handlebars.template(templateSpec);
+    return env.template(templateSpec);
   }
 
   // Template is only compiled on first use and cached after that point.
   return function(context, options) {
     if (!compiled) {
-      compiled = compile();
+      compiled = compileInput();
     }
     return compiled.call(this, context, options);
   };
-};
-
-
-// END(BROWSER)
-
-return Handlebars;
-
-};
-
-
+}
diff --git a/lib/handlebars/compiler/index.js b/lib/handlebars/compiler/index.js
deleted file mode 100644
index 6ed8d2190..000000000
--- a/lib/handlebars/compiler/index.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// Each of these module will augment the Handlebars object as it loads. No need to perform addition operations
-module.exports.attach = function(Handlebars) {
-
-var visitor = require("./visitor"),
-    printer = require("./printer"),
-    ast = require("./ast"),
-    compiler = require("./compiler");
-
-visitor.attach(Handlebars);
-printer.attach(Handlebars);
-ast.attach(Handlebars);
-compiler.attach(Handlebars);
-
-return Handlebars;
-
-};
diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js
new file mode 100644
index 000000000..57af1d449
--- /dev/null
+++ b/lib/handlebars/compiler/javascript-compiler.js
@@ -0,0 +1,887 @@
+import { COMPILER_REVISION, REVISION_CHANGES, log } from "../base";
+
+function Literal(value) {
+  this.value = value;
+}
+
+function JavaScriptCompiler() {}
+
+JavaScriptCompiler.prototype = {
+  // PUBLIC API: You can override these methods in a subclass to provide
+  // alternative compiled forms for name lookup and buffering semantics
+  nameLookup: function(parent, name /* , type*/) {
+    var wrap,
+        ret;
+    if (parent.indexOf('depth') === 0) {
+      wrap = true;
+    }
+
+    if (/^[0-9]+$/.test(name)) {
+      ret = parent + "[" + name + "]";
+    } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
+      ret = parent + "." + name;
+    }
+    else {
+      ret = parent + "['" + name + "']";
+    }
+
+    if (wrap) {
+      return '(' + parent + ' && ' + ret + ')';
+    } else {
+      return ret;
+    }
+  },
+
+  appendToBuffer: function(string) {
+    if (this.environment.isSimple) {
+      return "return " + string + ";";
+    } else {
+      return {
+        appendToBuffer: true,
+        content: string,
+        toString: function() { return "buffer += " + string + ";"; }
+      };
+    }
+  },
+
+  initializeBuffer: function() {
+    return this.quotedString("");
+  },
+
+  namespace: "Handlebars",
+  // END PUBLIC API
+
+  compile: function(environment, options, context, asObject) {
+    this.environment = environment;
+    this.options = options || {};
+
+    log('debug', this.environment.disassemble() + "\n\n");
+
+    this.name = this.environment.name;
+    this.isChild = !!context;
+    this.context = context || {
+      programs: [],
+      environments: [],
+      aliases: { }
+    };
+
+    this.preamble();
+
+    this.stackSlot = 0;
+    this.stackVars = [];
+    this.registers = { list: [] };
+    this.compileStack = [];
+    this.inlineStack = [];
+
+    this.compileChildren(environment, options);
+
+    var opcodes = environment.opcodes, opcode;
+
+    this.i = 0;
+
+    for(var l=opcodes.length; this.i<l; this.i++) {
+      opcode = opcodes[this.i];
+
+      if(opcode.opcode === 'DECLARE') {
+        this[opcode.name] = opcode.value;
+      } else {
+        this[opcode.opcode].apply(this, opcode.args);
+      }
+
+      // Reset the stripNext flag if it was not set by this operation.
+      if (opcode.opcode !== this.stripNext) {
+        this.stripNext = false;
+      }
+    }
+
+    // Flush any trailing content that might be pending.
+    this.pushSource('');
+
+    return this.createFunctionContext(asObject);
+  },
+
+  preamble: function() {
+    var out = [];
+
+    if (!this.isChild) {
+      var namespace = this.namespace;
+
+      var copies = "helpers = this.merge(helpers, " + namespace + ".helpers);";
+      if (this.environment.usePartial) { copies = copies + " partials = this.merge(partials, " + namespace + ".partials);"; }
+      if (this.options.data) { copies = copies + " data = data || {};"; }
+      out.push(copies);
+    } else {
+      out.push('');
+    }
+
+    if (!this.environment.isSimple) {
+      out.push(", buffer = " + this.initializeBuffer());
+    } else {
+      out.push("");
+    }
+
+    // track the last context pushed into place to allow skipping the
+    // getContext opcode when it would be a noop
+    this.lastContext = 0;
+    this.source = out;
+  },
+
+  createFunctionContext: function(asObject) {
+    var locals = this.stackVars.concat(this.registers.list);
+
+    if(locals.length > 0) {
+      this.source[1] = this.source[1] + ", " + locals.join(", ");
+    }
+
+    // Generate minimizer alias mappings
+    if (!this.isChild) {
+      for (var alias in this.context.aliases) {
+        if (this.context.aliases.hasOwnProperty(alias)) {
+          this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
+        }
+      }
+    }
+
+    if (this.source[1]) {
+      this.source[1] = "var " + this.source[1].substring(2) + ";";
+    }
+
+    // Merge children
+    if (!this.isChild) {
+      this.source[1] += '\n' + this.context.programs.join('\n') + '\n';
+    }
+
+    if (!this.environment.isSimple) {
+      this.pushSource("return buffer;");
+    }
+
+    var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"];
+
+    for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
+      params.push("depth" + this.environment.depths.list[i]);
+    }
+
+    // Perform a second pass over the output to merge content when possible
+    var source = this.mergeSource();
+
+    if (!this.isChild) {
+      var revision = COMPILER_REVISION,
+          versions = REVISION_CHANGES[revision];
+      source = "this.compilerInfo = ["+revision+",'"+versions+"'];\n"+source;
+    }
+
+    if (asObject) {
+      params.push(source);
+
+      return Function.apply(this, params);
+    } else {
+      var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n  ' + source + '}';
+      log('debug', functionSource + "\n\n");
+      return functionSource;
+    }
+  },
+  mergeSource: function() {
+    // WARN: We are not handling the case where buffer is still populated as the source should
+    // not have buffer append operations as their final action.
+    var source = '',
+        buffer;
+    for (var i = 0, len = this.source.length; i < len; i++) {
+      var line = this.source[i];
+      if (line.appendToBuffer) {
+        if (buffer) {
+          buffer = buffer + '\n    + ' + line.content;
+        } else {
+          buffer = line.content;
+        }
+      } else {
+        if (buffer) {
+          source += 'buffer += ' + buffer + ';\n  ';
+          buffer = undefined;
+        }
+        source += line + '\n  ';
+      }
+    }
+    return source;
+  },
+
+  // [blockValue]
+  //
+  // On stack, before: hash, inverse, program, value
+  // On stack, after: return value of blockHelperMissing
+  //
+  // The purpose of this opcode is to take a block of the form
+  // `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and
+  // replace it on the stack with the result of properly
+  // invoking blockHelperMissing.
+  blockValue: function() {
+    this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
+
+    var params = ["depth0"];
+    this.setupParams(0, params);
+
+    this.replaceStack(function(current) {
+      params.splice(1, 0, current);
+      return "blockHelperMissing.call(" + params.join(", ") + ")";
+    });
+  },
+
+  // [ambiguousBlockValue]
+  //
+  // On stack, before: hash, inverse, program, value
+  // Compiler value, before: lastHelper=value of last found helper, if any
+  // On stack, after, if no lastHelper: same as [blockValue]
+  // On stack, after, if lastHelper: value
+  ambiguousBlockValue: function() {
+    this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
+
+    var params = ["depth0"];
+    this.setupParams(0, params);
+
+    var current = this.topStack();
+    params.splice(1, 0, current);
+
+    // Use the options value generated from the invocation
+    params[params.length-1] = 'options';
+
+    this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
+  },
+
+  // [appendContent]
+  //
+  // On stack, before: ...
+  // On stack, after: ...
+  //
+  // Appends the string value of `content` to the current buffer
+  appendContent: function(content) {
+    if (this.pendingContent) {
+      content = this.pendingContent + content;
+    }
+    if (this.stripNext) {
+      content = content.replace(/^\s+/, '');
+    }
+
+    this.pendingContent = content;
+  },
+
+  // [strip]
+  //
+  // On stack, before: ...
+  // On stack, after: ...
+  //
+  // Removes any trailing whitespace from the prior content node and flags
+  // the next operation for stripping if it is a content node.
+  strip: function() {
+    if (this.pendingContent) {
+      this.pendingContent = this.pendingContent.replace(/\s+$/, '');
+    }
+    this.stripNext = 'strip';
+  },
+
+  // [append]
+  //
+  // On stack, before: value, ...
+  // On stack, after: ...
+  //
+  // Coerces `value` to a String and appends it to the current buffer.
+  //
+  // If `value` is truthy, or 0, it is coerced into a string and appended
+  // Otherwise, the empty string is appended
+  append: function() {
+    // Force anything that is inlined onto the stack so we don't have duplication
+    // when we examine local
+    this.flushInline();
+    var local = this.popStack();
+    this.pushSource("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
+    if (this.environment.isSimple) {
+      this.pushSource("else { " + this.appendToBuffer("''") + " }");
+    }
+  },
+
+  // [appendEscaped]
+  //
+  // On stack, before: value, ...
+  // On stack, after: ...
+  //
+  // Escape `value` and append it to the buffer
+  appendEscaped: function() {
+    this.context.aliases.escapeExpression = 'this.escapeExpression';
+
+    this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
+  },
+
+  // [getContext]
+  //
+  // On stack, before: ...
+  // On stack, after: ...
+  // Compiler value, after: lastContext=depth
+  //
+  // Set the value of the `lastContext` compiler value to the depth
+  getContext: function(depth) {
+    if(this.lastContext !== depth) {
+      this.lastContext = depth;
+    }
+  },
+
+  // [lookupOnContext]
+  //
+  // On stack, before: ...
+  // On stack, after: currentContext[name], ...
+  //
+  // Looks up the value of `name` on the current context and pushes
+  // it onto the stack.
+  lookupOnContext: function(name) {
+    this.push(this.nameLookup('depth' + this.lastContext, name, 'context'));
+  },
+
+  // [pushContext]
+  //
+  // On stack, before: ...
+  // On stack, after: currentContext, ...
+  //
+  // Pushes the value of the current context onto the stack.
+  pushContext: function() {
+    this.pushStackLiteral('depth' + this.lastContext);
+  },
+
+  // [resolvePossibleLambda]
+  //
+  // On stack, before: value, ...
+  // On stack, after: resolved value, ...
+  //
+  // If the `value` is a lambda, replace it on the stack by
+  // the return value of the lambda
+  resolvePossibleLambda: function() {
+    this.context.aliases.functionType = '"function"';
+
+    this.replaceStack(function(current) {
+      return "typeof " + current + " === functionType ? " + current + ".apply(depth0) : " + current;
+    });
+  },
+
+  // [lookup]
+  //
+  // On stack, before: value, ...
+  // On stack, after: value[name], ...
+  //
+  // Replace the value on the stack with the result of looking
+  // up `name` on `value`
+  lookup: function(name) {
+    this.replaceStack(function(current) {
+      return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context');
+    });
+  },
+
+  // [lookupData]
+  //
+  // On stack, before: ...
+  // On stack, after: data, ...
+  //
+  // Push the data lookup operator
+  lookupData: function() {
+    this.push('data');
+  },
+
+  // [pushStringParam]
+  //
+  // On stack, before: ...
+  // On stack, after: string, currentContext, ...
+  //
+  // This opcode is designed for use in string mode, which
+  // provides the string value of a parameter along with its
+  // depth rather than resolving it immediately.
+  pushStringParam: function(string, type) {
+    this.pushStackLiteral('depth' + this.lastContext);
+
+    this.pushString(type);
+
+    if (typeof string === 'string') {
+      this.pushString(string);
+    } else {
+      this.pushStackLiteral(string);
+    }
+  },
+
+  emptyHash: function() {
+    this.pushStackLiteral('{}');
+
+    if (this.options.stringParams) {
+      this.register('hashTypes', '{}');
+      this.register('hashContexts', '{}');
+    }
+  },
+  pushHash: function() {
+    this.hash = {values: [], types: [], contexts: []};
+  },
+  popHash: function() {
+    var hash = this.hash;
+    this.hash = undefined;
+
+    if (this.options.stringParams) {
+      this.register('hashContexts', '{' + hash.contexts.join(',') + '}');
+      this.register('hashTypes', '{' + hash.types.join(',') + '}');
+    }
+    this.push('{\n    ' + hash.values.join(',\n    ') + '\n  }');
+  },
+
+  // [pushString]
+  //
+  // On stack, before: ...
+  // On stack, after: quotedString(string), ...
+  //
+  // Push a quoted version of `string` onto the stack
+  pushString: function(string) {
+    this.pushStackLiteral(this.quotedString(string));
+  },
+
+  // [push]
+  //
+  // On stack, before: ...
+  // On stack, after: expr, ...
+  //
+  // Push an expression onto the stack
+  push: function(expr) {
+    this.inlineStack.push(expr);
+    return expr;
+  },
+
+  // [pushLiteral]
+  //
+  // On stack, before: ...
+  // On stack, after: value, ...
+  //
+  // Pushes a value onto the stack. This operation prevents
+  // the compiler from creating a temporary variable to hold
+  // it.
+  pushLiteral: function(value) {
+    this.pushStackLiteral(value);
+  },
+
+  // [pushProgram]
+  //
+  // On stack, before: ...
+  // On stack, after: program(guid), ...
+  //
+  // Push a program expression onto the stack. This takes
+  // a compile-time guid and converts it into a runtime-accessible
+  // expression.
+  pushProgram: function(guid) {
+    if (guid != null) {
+      this.pushStackLiteral(this.programExpression(guid));
+    } else {
+      this.pushStackLiteral(null);
+    }
+  },
+
+  // [invokeHelper]
+  //
+  // On stack, before: hash, inverse, program, params..., ...
+  // On stack, after: result of helper invocation
+  //
+  // Pops off the helper's parameters, invokes the helper,
+  // and pushes the helper's return value onto the stack.
+  //
+  // If the helper is not found, `helperMissing` is called.
+  invokeHelper: function(paramSize, name) {
+    this.context.aliases.helperMissing = 'helpers.helperMissing';
+
+    var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
+    var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
+
+    this.push(helper.name + ' || ' + nonHelper);
+    this.replaceStack(function(name) {
+      return name + ' ? ' + name + '.call(' +
+          helper.callParams + ") " + ": helperMissing.call(" +
+          helper.helperMissingParams + ")";
+    });
+  },
+
+  // [invokeKnownHelper]
+  //
+  // On stack, before: hash, inverse, program, params..., ...
+  // On stack, after: result of helper invocation
+  //
+  // This operation is used when the helper is known to exist,
+  // so a `helperMissing` fallback is not required.
+  invokeKnownHelper: function(paramSize, name) {
+    var helper = this.setupHelper(paramSize, name);
+    this.push(helper.name + ".call(" + helper.callParams + ")");
+  },
+
+  // [invokeAmbiguous]
+  //
+  // On stack, before: hash, inverse, program, params..., ...
+  // On stack, after: result of disambiguation
+  //
+  // This operation is used when an expression like `{{foo}}`
+  // is provided, but we don't know at compile-time whether it
+  // is a helper or a path.
+  //
+  // This operation emits more code than the other options,
+  // and can be avoided by passing the `knownHelpers` and
+  // `knownHelpersOnly` flags at compile-time.
+  invokeAmbiguous: function(name, helperCall) {
+    this.context.aliases.functionType = '"function"';
+
+    this.pushStackLiteral('{}');    // Hash value
+    var helper = this.setupHelper(0, name, helperCall);
+
+    var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
+
+    var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
+    var nextStack = this.nextStack();
+
+    this.pushSource('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }');
+    this.pushSource('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.call(' + helper.callParams + ') : ' + nextStack + '; }');
+  },
+
+  // [invokePartial]
+  //
+  // On stack, before: context, ...
+  // On stack after: result of partial invocation
+  //
+  // This operation pops off a context, invokes a partial with that context,
+  // and pushes the result of the invocation back.
+  invokePartial: function(name) {
+    var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"];
+
+    if (this.options.data) {
+      params.push("data");
+    }
+
+    this.context.aliases.self = "this";
+    this.push("self.invokePartial(" + params.join(", ") + ")");
+  },
+
+  // [assignToHash]
+  //
+  // On stack, before: value, hash, ...
+  // On stack, after: hash, ...
+  //
+  // Pops a value and hash off the stack, assigns `hash[key] = value`
+  // and pushes the hash back onto the stack.
+  assignToHash: function(key) {
+    var value = this.popStack(),
+        context,
+        type;
+
+    if (this.options.stringParams) {
+      type = this.popStack();
+      context = this.popStack();
+    }
+
+    var hash = this.hash;
+    if (context) {
+      hash.contexts.push("'" + key + "': " + context);
+    }
+    if (type) {
+      hash.types.push("'" + key + "': " + type);
+    }
+    hash.values.push("'" + key + "': (" + value + ")");
+  },
+
+  // HELPERS
+
+  compiler: JavaScriptCompiler,
+
+  compileChildren: function(environment, options) {
+    var children = environment.children, child, compiler;
+
+    for(var i=0, l=children.length; i<l; i++) {
+      child = children[i];
+      compiler = new this.compiler();
+
+      var index = this.matchExistingProgram(child);
+
+      if (index == null) {
+        this.context.programs.push('');     // Placeholder to prevent name conflicts for nested children
+        index = this.context.programs.length;
+        child.index = index;
+        child.name = 'program' + index;
+        this.context.programs[index] = compiler.compile(child, options, this.context);
+        this.context.environments[index] = child;
+      } else {
+        child.index = index;
+        child.name = 'program' + index;
+      }
+    }
+  },
+  matchExistingProgram: function(child) {
+    for (var i = 0, len = this.context.environments.length; i < len; i++) {
+      var environment = this.context.environments[i];
+      if (environment && environment.equals(child)) {
+        return i;
+      }
+    }
+  },
+
+  programExpression: function(guid) {
+    this.context.aliases.self = "this";
+
+    if(guid == null) {
+      return "self.noop";
+    }
+
+    var child = this.environment.children[guid],
+        depths = child.depths.list, depth;
+
+    var programParams = [child.index, child.name, "data"];
+
+    for(var i=0, l = depths.length; i<l; i++) {
+      depth = depths[i];
+
+      if(depth === 1) { programParams.push("depth0"); }
+      else { programParams.push("depth" + (depth - 1)); }
+    }
+
+    return (depths.length === 0 ? "self.program(" : "self.programWithDepth(") + programParams.join(", ") + ")";
+  },
+
+  register: function(name, val) {
+    this.useRegister(name);
+    this.pushSource(name + " = " + val + ";");
+  },
+
+  useRegister: function(name) {
+    if(!this.registers[name]) {
+      this.registers[name] = true;
+      this.registers.list.push(name);
+    }
+  },
+
+  pushStackLiteral: function(item) {
+    return this.push(new Literal(item));
+  },
+
+  pushSource: function(source) {
+    if (this.pendingContent) {
+      this.source.push(this.appendToBuffer(this.quotedString(this.pendingContent)));
+      this.pendingContent = undefined;
+    }
+
+    if (source) {
+      this.source.push(source);
+    }
+  },
+
+  pushStack: function(item) {
+    this.flushInline();
+
+    var stack = this.incrStack();
+    if (item) {
+      this.pushSource(stack + " = " + item + ";");
+    }
+    this.compileStack.push(stack);
+    return stack;
+  },
+
+  replaceStack: function(callback) {
+    var prefix = '',
+        inline = this.isInline(),
+        stack;
+
+    // If we are currently inline then we want to merge the inline statement into the
+    // replacement statement via ','
+    if (inline) {
+      var top = this.popStack(true);
+
+      if (top instanceof Literal) {
+        // Literals do not need to be inlined
+        stack = top.value;
+      } else {
+        // Get or create the current stack name for use by the inline
+        var name = this.stackSlot ? this.topStackName() : this.incrStack();
+
+        prefix = '(' + this.push(name) + ' = ' + top + '),';
+        stack = this.topStack();
+      }
+    } else {
+      stack = this.topStack();
+    }
+
+    var item = callback.call(this, stack);
+
+    if (inline) {
+      if (this.inlineStack.length || this.compileStack.length) {
+        this.popStack();
+      }
+      this.push('(' + prefix + item + ')');
+    } else {
+      // Prevent modification of the context depth variable. Through replaceStack
+      if (!/^stack/.test(stack)) {
+        stack = this.nextStack();
+      }
+
+      this.pushSource(stack + " = (" + prefix + item + ");");
+    }
+    return stack;
+  },
+
+  nextStack: function() {
+    return this.pushStack();
+  },
+
+  incrStack: function() {
+    this.stackSlot++;
+    if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
+    return this.topStackName();
+  },
+  topStackName: function() {
+    return "stack" + this.stackSlot;
+  },
+  flushInline: function() {
+    var inlineStack = this.inlineStack;
+    if (inlineStack.length) {
+      this.inlineStack = [];
+      for (var i = 0, len = inlineStack.length; i < len; i++) {
+        var entry = inlineStack[i];
+        if (entry instanceof Literal) {
+          this.compileStack.push(entry);
+        } else {
+          this.pushStack(entry);
+        }
+      }
+    }
+  },
+  isInline: function() {
+    return this.inlineStack.length;
+  },
+
+  popStack: function(wrapped) {
+    var inline = this.isInline(),
+        item = (inline ? this.inlineStack : this.compileStack).pop();
+
+    if (!wrapped && (item instanceof Literal)) {
+      return item.value;
+    } else {
+      if (!inline) {
+        this.stackSlot--;
+      }
+      return item;
+    }
+  },
+
+  topStack: function(wrapped) {
+    var stack = (this.isInline() ? this.inlineStack : this.compileStack),
+        item = stack[stack.length - 1];
+
+    if (!wrapped && (item instanceof Literal)) {
+      return item.value;
+    } else {
+      return item;
+    }
+  },
+
+  quotedString: function(str) {
+    return '"' + str
+      .replace(/\\/g, '\\\\')
+      .replace(/"/g, '\\"')
+      .replace(/\n/g, '\\n')
+      .replace(/\r/g, '\\r')
+      .replace(/\u2028/g, '\\u2028')   // Per Ecma-262 7.3 + 7.8.4
+      .replace(/\u2029/g, '\\u2029') + '"';
+  },
+
+  setupHelper: function(paramSize, name, missingParams) {
+    var params = [];
+    this.setupParams(paramSize, params, missingParams);
+    var foundHelper = this.nameLookup('helpers', name, 'helper');
+
+    return {
+      params: params,
+      name: foundHelper,
+      callParams: ["depth0"].concat(params).join(", "),
+      helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
+    };
+  },
+
+  // the params and contexts arguments are passed in arrays
+  // to fill in
+  setupParams: function(paramSize, params, useRegister) {
+    var options = [], contexts = [], types = [], param, inverse, program;
+
+    options.push("hash:" + this.popStack());
+
+    inverse = this.popStack();
+    program = this.popStack();
+
+    // Avoid setting fn and inverse if neither are set. This allows
+    // helpers to do a check for `if (options.fn)`
+    if (program || inverse) {
+      if (!program) {
+        this.context.aliases.self = "this";
+        program = "self.noop";
+      }
+
+      if (!inverse) {
+       this.context.aliases.self = "this";
+        inverse = "self.noop";
+      }
+
+      options.push("inverse:" + inverse);
+      options.push("fn:" + program);
+    }
+
+    for(var i=0; i<paramSize; i++) {
+      param = this.popStack();
+      params.push(param);
+
+      if(this.options.stringParams) {
+        types.push(this.popStack());
+        contexts.push(this.popStack());
+      }
+    }
+
+    if (this.options.stringParams) {
+      options.push("contexts:[" + contexts.join(",") + "]");
+      options.push("types:[" + types.join(",") + "]");
+      options.push("hashContexts:hashContexts");
+      options.push("hashTypes:hashTypes");
+    }
+
+    if(this.options.data) {
+      options.push("data:data");
+    }
+
+    options = "{" + options.join(",") + "}";
+    if (useRegister) {
+      this.register('options', options);
+      params.push('options');
+    } else {
+      params.push(options);
+    }
+    return params.join(", ");
+  }
+};
+
+var reservedWords = (
+  "break else new var" +
+  " case finally return void" +
+  " catch for switch while" +
+  " continue function this with" +
+  " default if throw" +
+  " delete in try" +
+  " do instanceof typeof" +
+  " abstract enum int short" +
+  " boolean export interface static" +
+  " byte extends long super" +
+  " char final native synchronized" +
+  " class float package throws" +
+  " const goto private transient" +
+  " debugger implements protected volatile" +
+  " double import public let yield"
+).split(" ");
+
+var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
+
+for(var i=0, l=reservedWords.length; i<l; i++) {
+  compilerWords[reservedWords[i]] = true;
+}
+
+JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
+  if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) {
+    return true;
+  }
+  return false;
+};
+
+export default JavaScriptCompiler;
diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js
index d9eb7a577..a35cdf408 100644
--- a/lib/handlebars/compiler/printer.js
+++ b/lib/handlebars/compiler/printer.js
@@ -1,15 +1,16 @@
-exports.attach = function(Handlebars) {
+import Visitor from "./visitor";
 
-// BEGIN(BROWSER)
+export function print(ast) {
+  return new PrintVisitor().accept(ast);
+}
 
-Handlebars.print = function(ast) {
-  return new Handlebars.PrintVisitor().accept(ast);
-};
+export function PrintVisitor() {
+  this.padding = 0;
+}
 
-Handlebars.PrintVisitor = function() { this.padding = 0; };
-Handlebars.PrintVisitor.prototype = new Handlebars.Visitor();
+PrintVisitor.prototype = new Visitor();
 
-Handlebars.PrintVisitor.prototype.pad = function(string, newline) {
+PrintVisitor.prototype.pad = function(string, newline) {
   var out = "";
 
   for(var i=0,l=this.padding; i<l; i++) {
@@ -22,7 +23,7 @@ Handlebars.PrintVisitor.prototype.pad = function(string, newline) {
   return out;
 };
 
-Handlebars.PrintVisitor.prototype.program = function(program) {
+PrintVisitor.prototype.program = function(program) {
   var out = "",
       statements = program.statements,
       inverse = program.inverse,
@@ -37,7 +38,7 @@ Handlebars.PrintVisitor.prototype.program = function(program) {
   return out;
 };
 
-Handlebars.PrintVisitor.prototype.block = function(block) {
+PrintVisitor.prototype.block = function(block) {
   var out = "";
 
   out = out + this.pad("BLOCK:");
@@ -62,7 +63,7 @@ Handlebars.PrintVisitor.prototype.block = function(block) {
   return out;
 };
 
-Handlebars.PrintVisitor.prototype.mustache = function(mustache) {
+PrintVisitor.prototype.mustache = function(mustache) {
   var params = mustache.params, paramStrings = [], hash;
 
   for(var i=0, l=params.length; i<l; i++) {
@@ -76,13 +77,13 @@ Handlebars.PrintVisitor.prototype.mustache = function(mustache) {
   return this.pad("{{ " + this.accept(mustache.id) + " " + params + hash + " }}");
 };
 
-Handlebars.PrintVisitor.prototype.partial = function(partial) {
+PrintVisitor.prototype.partial = function(partial) {
   var content = this.accept(partial.partialName);
   if(partial.context) { content = content + " " + this.accept(partial.context); }
   return this.pad("{{> " + content + " }}");
 };
 
-Handlebars.PrintVisitor.prototype.hash = function(hash) {
+PrintVisitor.prototype.hash = function(hash) {
   var pairs = hash.pairs;
   var joinedPairs = [], left, right;
 
@@ -95,19 +96,19 @@ Handlebars.PrintVisitor.prototype.hash = function(hash) {
   return "HASH{" + joinedPairs.join(", ") + "}";
 };
 
-Handlebars.PrintVisitor.prototype.STRING = function(string) {
+PrintVisitor.prototype.STRING = function(string) {
   return '"' + string.string + '"';
 };
 
-Handlebars.PrintVisitor.prototype.INTEGER = function(integer) {
+PrintVisitor.prototype.INTEGER = function(integer) {
   return "INTEGER{" + integer.integer + "}";
 };
 
-Handlebars.PrintVisitor.prototype.BOOLEAN = function(bool) {
+PrintVisitor.prototype.BOOLEAN = function(bool) {
   return "BOOLEAN{" + bool.bool + "}";
 };
 
-Handlebars.PrintVisitor.prototype.ID = function(id) {
+PrintVisitor.prototype.ID = function(id) {
   var path = id.parts.join("/");
   if(id.parts.length > 1) {
     return "PATH:" + path;
@@ -116,23 +117,19 @@ Handlebars.PrintVisitor.prototype.ID = function(id) {
   }
 };
 
-Handlebars.PrintVisitor.prototype.PARTIAL_NAME = function(partialName) {
+PrintVisitor.prototype.PARTIAL_NAME = function(partialName) {
     return "PARTIAL:" + partialName.name;
 };
 
-Handlebars.PrintVisitor.prototype.DATA = function(data) {
+PrintVisitor.prototype.DATA = function(data) {
   return "@" + this.accept(data.id);
 };
 
-Handlebars.PrintVisitor.prototype.content = function(content) {
+PrintVisitor.prototype.content = function(content) {
   return this.pad("CONTENT[ '" + content.string + "' ]");
 };
 
-Handlebars.PrintVisitor.prototype.comment = function(comment) {
+PrintVisitor.prototype.comment = function(comment) {
   return this.pad("{{! '" + comment.comment + "' }}");
 };
-// END(BROWSER)
-
-return Handlebars;
-};
 
diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js
index 5d0731407..6a0373e19 100644
--- a/lib/handlebars/compiler/visitor.js
+++ b/lib/handlebars/compiler/visitor.js
@@ -1,18 +1,11 @@
-exports.attach = function(Handlebars) {
+function Visitor() {}
 
-// BEGIN(BROWSER)
+Visitor.prototype = {
+  constructor: Visitor,
 
-Handlebars.Visitor = function() {};
-
-Handlebars.Visitor.prototype = {
   accept: function(object) {
     return this[object.type](object);
   }
 };
 
-// END(BROWSER)
-
-return Handlebars;
-};
-
-
+export default Visitor;
diff --git a/lib/handlebars/exception.js b/lib/handlebars/exception.js
new file mode 100644
index 000000000..6de9cfdb4
--- /dev/null
+++ b/lib/handlebars/exception.js
@@ -0,0 +1,15 @@
+
+var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
+
+function Exception(/* message */) {
+  var tmp = Error.prototype.constructor.apply(this, arguments);
+
+  // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
+  for (var idx = 0; idx < errorProps.length; idx++) {
+    this[errorProps[idx]] = tmp[errorProps[idx]];
+  }
+}
+
+Exception.prototype = new Error();
+
+export default Exception;
diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js
index 2e845c4cc..fe5fef4ab 100644
--- a/lib/handlebars/runtime.js
+++ b/lib/handlebars/runtime.js
@@ -1,106 +1,139 @@
-exports.attach = function(Handlebars) {
-
-// BEGIN(BROWSER)
-
-Handlebars.VM = {
-  template: function(templateSpec) {
-    // Just add water
-    var container = {
-      escapeExpression: Handlebars.Utils.escapeExpression,
-      invokePartial: Handlebars.VM.invokePartial,
-      programs: [],
-      program: function(i, fn, data) {
-        var programWrapper = this.programs[i];
-        if(data) {
-          programWrapper = Handlebars.VM.program(i, fn, data);
-        } else if (!programWrapper) {
-          programWrapper = this.programs[i] = Handlebars.VM.program(i, fn);
-        }
-        return programWrapper;
-      },
-      merge: function(param, common) {
-        var ret = param || common;
-
-        if (param && common) {
-          ret = {};
-          Handlebars.Utils.extend(ret, common);
-          Handlebars.Utils.extend(ret, param);
-        }
-        return ret;
-      },
-      programWithDepth: Handlebars.VM.programWithDepth,
-      noop: Handlebars.VM.noop,
-      compilerInfo: null
-    };
-
-    return function(context, options) {
-      options = options || {};
-      var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
-
-      var compilerInfo = container.compilerInfo || [],
-          compilerRevision = compilerInfo[0] || 1,
-          currentRevision = Handlebars.COMPILER_REVISION;
-
-      if (compilerRevision !== currentRevision) {
-        if (compilerRevision < currentRevision) {
-          var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision],
-              compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision];
-          throw "Template was precompiled with an older version of Handlebars than the current runtime. "+
-                "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").";
-        } else {
-          // Use the embedded version info since the runtime doesn't know about this revision yet
-          throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+
-                "Please update your runtime to a newer version ("+compilerInfo[1]+").";
-        }
-      }
+/*global Utils */
+module Utils from "./utils";
+import Exception from "./exception";
+import { COMPILER_REVISION, REVISION_CHANGES } from "./base";
+
+function checkRevision(compilerInfo) {
+  var compilerRevision = compilerInfo && compilerInfo[0] || 1,
+      currentRevision = COMPILER_REVISION;
+
+  if (compilerRevision !== currentRevision) {
+    if (compilerRevision < currentRevision) {
+      var runtimeVersions = REVISION_CHANGES[currentRevision],
+          compilerVersions = REVISION_CHANGES[compilerRevision];
+      throw new Error("Template was precompiled with an older version of Handlebars than the current runtime. "+
+            "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").");
+    } else {
+      // Use the embedded version info since the runtime doesn't know about this revision yet
+      throw new Error("Template was precompiled with a newer version of Handlebars than the current runtime. "+
+            "Please update your runtime to a newer version ("+compilerInfo[1]+").");
+    }
+  }
+}
 
-      return result;
-    };
-  },
+// TODO: Remove this line and break up compilePartial
 
-  programWithDepth: function(i, fn, data /*, $depth */) {
-    var args = Array.prototype.slice.call(arguments, 3);
+export function template(templateSpec, env) {
+  if (!env) {
+    throw new Error("No environment passed to template");
+  }
 
-    var program = function(context, options) {
-      options = options || {};
+  var invokePartialWrapper;
+  if (env.compile) {
+    invokePartialWrapper = function(partial, name, context, helpers, partials, data) {
+      // TODO : Check this for all inputs and the options handling (partial flag, etc). This feels
+      // like there should be a common exec path
+      var result = invokePartial.apply(this, arguments);
+      if (result) { return result; }
 
-      return fn.apply(this, [context, options.data || data].concat(args));
+      var options = { helpers: helpers, partials: partials, data: data };
+      partials[name] = env.compile(partial, { data: data !== undefined }, env);
+      return partials[name](context, options);
     };
-    program.program = i;
-    program.depth = args.length;
-    return program;
-  },
-  program: function(i, fn, data) {
-    var program = function(context, options) {
-      options = options || {};
-
-      return fn(context, options.data || data);
+  } else {
+    invokePartialWrapper = function(partial, name /* , context, helpers, partials, data */) {
+      var result = invokePartial.apply(this, arguments);
+      if (result) { return result; }
+      throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
     };
-    program.program = i;
-    program.depth = 0;
-    return program;
-  },
-  noop: function() { return ""; },
-  invokePartial: function(partial, name, context, helpers, partials, data) {
-    var options = { helpers: helpers, partials: partials, data: data };
-
-    if(partial === undefined) {
-      throw new Handlebars.Exception("The partial " + name + " could not be found");
-    } else if(partial instanceof Function) {
-      return partial(context, options);
-    } else if (!Handlebars.compile) {
-      throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
-    } else {
-      partials[name] = Handlebars.compile(partial, {data: data !== undefined});
-      return partials[name](context, options);
-    }
   }
-};
 
-Handlebars.template = Handlebars.VM.template;
-
-// END(BROWSER)
+  // Just add water
+  var container = {
+    escapeExpression: Utils.escapeExpression,
+    invokePartial: invokePartialWrapper,
+    programs: [],
+    program: function(i, fn, data) {
+      var programWrapper = this.programs[i];
+      if(data) {
+        programWrapper = program(i, fn, data);
+      } else if (!programWrapper) {
+        programWrapper = this.programs[i] = program(i, fn);
+      }
+      return programWrapper;
+    },
+    merge: function(param, common) {
+      var ret = param || common;
+
+      if (param && common && (param !== common)) {
+        ret = {};
+        Utils.extend(ret, common);
+        Utils.extend(ret, param);
+      }
+      return ret;
+    },
+    programWithDepth: programWithDepth,
+    noop: noop,
+    compilerInfo: null
+  };
+
+  return function(context, options) {
+    options = options || {};
+    var namespace = options.partial ? options : env,
+        helpers,
+        partials;
+
+    if (!options.partial) {
+      helpers = options.helpers;
+      partials = options.partials;
+    }
+    var result = templateSpec.call(
+          container,
+          namespace, context,
+          helpers,
+          partials,
+          options.data);
+
+    if (!options.partial) {
+      checkRevision(container.compilerInfo);
+    }
 
-return Handlebars;
+    return result;
+  };
+}
+
+export function programWithDepth(i, fn, data /*, $depth */) {
+  var args = Array.prototype.slice.call(arguments, 3);
+
+  var prog = function(context, options) {
+    options = options || {};
+
+    return fn.apply(this, [context, options.data || data].concat(args));
+  };
+  prog.program = i;
+  prog.depth = args.length;
+  return prog;
+}
+
+export function program(i, fn, data) {
+  var prog = function(context, options) {
+    options = options || {};
+
+    return fn(context, options.data || data);
+  };
+  prog.program = i;
+  prog.depth = 0;
+  return prog;
+}
+
+export function invokePartial(partial, name, context, helpers, partials, data) {
+  var options = { partial: true, helpers: helpers, partials: partials, data: data };
+
+  if(partial === undefined) {
+    throw new Exception("The partial " + name + " could not be found");
+  } else if(partial instanceof Function) {
+    return partial(context, options);
+  }
+}
 
-};
+export function noop() { return ""; }
diff --git a/lib/handlebars/safe-string.js b/lib/handlebars/safe-string.js
new file mode 100644
index 000000000..2ae49aa89
--- /dev/null
+++ b/lib/handlebars/safe-string.js
@@ -0,0 +1,10 @@
+// Build out our basic SafeString type
+function SafeString(string) {
+  this.string = string;
+}
+
+SafeString.prototype.toString = function() {
+  return "" + this.string;
+};
+
+export default SafeString;
diff --git a/lib/handlebars/source.rb b/lib/handlebars/source.rb
deleted file mode 100644
index f576885ae..000000000
--- a/lib/handlebars/source.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module Handlebars
-  module Source
-    def self.bundled_path
-      File.expand_path("../../../dist/handlebars.js", __FILE__)
-    end
-
-    def self.runtime_bundled_path
-      File.expand_path("../../../dist/handlebars.runtime.js", __FILE__)
-    end
-  end
-end
diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js
index 1e0e4c902..998c9ca6b 100644
--- a/lib/handlebars/utils.js
+++ b/lib/handlebars/utils.js
@@ -1,28 +1,6 @@
-exports.attach = function(Handlebars) {
+import SafeString from "./safe-string";
 
-var toString = Object.prototype.toString;
-
-// BEGIN(BROWSER)
-
-var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
-
-Handlebars.Exception = function(message) {
-  var tmp = Error.prototype.constructor.apply(this, arguments);
-
-  // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
-  for (var idx = 0; idx < errorProps.length; idx++) {
-    this[errorProps[idx]] = tmp[errorProps[idx]];
-  }
-};
-Handlebars.Exception.prototype = new Error();
-
-// Build out our basic SafeString type
-Handlebars.SafeString = function(string) {
-  this.string = string;
-};
-Handlebars.SafeString.prototype.toString = function() {
-  return this.string.toString();
-};
+var isArray = Array.isArray;
 
 var escape = {
   "&": "&amp;",
@@ -36,48 +14,41 @@ var escape = {
 var badChars = /[&<>"'`]/g;
 var possible = /[&<>"'`]/;
 
-var escapeChar = function(chr) {
+function escapeChar(chr) {
   return escape[chr] || "&amp;";
-};
+}
 
-Handlebars.Utils = {
-  extend: function(obj, value) {
-    for(var key in value) {
-      if(value.hasOwnProperty(key)) {
-        obj[key] = value[key];
-      }
-    }
-  },
-
-  escapeExpression: function(string) {
-    // don't escape SafeStrings, since they're already safe
-    if (string instanceof Handlebars.SafeString) {
-      return string.toString();
-    } else if (string == null || string === false) {
-      return "";
-    }
-
-    // Force a string conversion as this will be done by the append regardless and
-    // the regex test will do this transparently behind the scenes, causing issues if
-    // an object's to string has escaped characters in it.
-    string = string.toString();
-
-    if(!possible.test(string)) { return string; }
-    return string.replace(badChars, escapeChar);
-  },
-
-  isEmpty: function(value) {
-    if (!value && value !== 0) {
-      return true;
-    } else if(toString.call(value) === "[object Array]" && value.length === 0) {
-      return true;
-    } else {
-      return false;
+export function extend(obj, value) {
+  for(var key in value) {
+    if(value.hasOwnProperty(key)) {
+      obj[key] = value[key];
     }
   }
-};
-
-// END(BROWSER)
+}
+
+export function escapeExpression(string) {
+  // don't escape SafeStrings, since they're already safe
+  if (string instanceof SafeString) {
+    return string.toString();
+  } else if (!string && string !== 0) {
+    return "";
+  }
 
-return Handlebars;
-};
+  // Force a string conversion as this will be done by the append regardless and
+  // the regex test will do this transparently behind the scenes, causing issues if
+  // an object's to string has escaped characters in it.
+  string = "" + string;
+
+  if(!possible.test(string)) { return string; }
+  return string.replace(badChars, escapeChar);
+}
+
+export function isEmpty(value) {
+  if (!value && value !== 0) {
+    return true;
+  } else if (isArray(value) && value.length === 0) {
+    return true;
+  } else {
+    return false;
+  }
+}
diff --git a/lib/index.js b/lib/index.js
new file mode 100644
index 000000000..ca51d3a49
--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,25 @@
+// USAGE:
+// var handlebars = require('handlebars');
+
+// var local = handlebars.create();
+
+var handlebars = require('../dist/cjs/handlebars').default;
+
+handlebars.Visitor = require('../dist/cjs/handlebars/compiler/visitor').default;
+
+var printer = require('../dist/cjs/handlebars/compiler/printer');
+handlebars.PrintVisitor = printer.PrintVisitor;
+handlebars.print = printer.print;
+
+module.exports = handlebars;
+
+// Publish a Node.js require() handler for .handlebars and .hbs files
+if (typeof require !== 'undefined' && require.extensions) {
+  var extension = function(module, filename) {
+    var fs = require("fs");
+    var templateString = fs.readFileSync(filename, "utf8");
+    module.exports = handlebars.compile(templateString);
+  };
+  require.extensions[".handlebars"] = extension;
+  require.extensions[".hbs"] = extension;
+}
diff --git a/package.json b/package.json
index b469398f2..3e7595b39 100644
--- a/package.json
+++ b/package.json
@@ -1,35 +1,58 @@
 {
   "name": "handlebars",
-  "description": "Extension of the Mustache logicless template language",
-  "version": "1.0.12",
+  "barename": "handlebars",
+  "version": "1.1.0",
+  "description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
   "homepage": "http://www.handlebarsjs.com/",
   "keywords": [
-    "handlebars mustache template html"
+    "handlebars",
+    "mustache",
+    "template",
+    "html"
   ],
   "repository": {
     "type": "git",
-    "url": "git://github.com/wycats/handlebars.js.git"
+    "url": "https://github.com/wycats/handlebars.js.git"
   },
+  "author": "Yehuda Katz",
+  "license": "BSD",
+  "readmeFilename": "README.md",
   "engines": {
     "node": ">=0.4.7"
   },
   "dependencies": {
-    "optimist": "~0.3",
+    "optimist": "~0.3"
+  },
+  "optionalDependencies": {
     "uglify-js": "~2.3"
   },
   "devDependencies": {
+    "async": "~0.2.9",
+    "aws-sdk": "~1.5.0",
     "benchmark": "~1.0",
-    "dust": "~0.3",
-    "jison": "~0.3",
+    "dustjs-linkedin": "~2.0.2",
+    "eco": "~1.1.0-rc-3",
+    "grunt": "~0.4.1",
+    "grunt-contrib-clean": "~0.4.1",
+    "grunt-contrib-copy": "~0.4.1",
+    "grunt-contrib-jshint": "~0.6.3",
+    "grunt-contrib-requirejs": "~0.4.1",
+    "grunt-contrib-uglify": "~0.2.2",
+    "grunt-es6-module-transpiler": "joefiorini/grunt-es6-module-transpiler",
+    "es6-module-packager": "*",
+    "jison": "~0.3.0",
+    "keen.io": "0.0.3",
     "mocha": "*",
-    "mustache": "~0.7.2"
+    "mustache": "~0.7.2",
+    "semver": "~2.1.0",
+    "should": "~1.2.2",
+    "underscore": "~1.5.1"
   },
-  "main": "lib/handlebars.js",
+  "main": "lib/index.js",
   "bin": {
     "handlebars": "bin/handlebars"
   },
   "scripts": {
-    "test": "node_modules/.bin/mocha -u qunit spec/qunit_spec.js"
-  },
-  "optionalDependencies": {}
+    "test": "grunt"
+  }
 }
diff --git a/release-notes.md b/release-notes.md
index 8e6761944..6040f411f 100644
--- a/release-notes.md
+++ b/release-notes.md
@@ -1,7 +1,37 @@
 # Release Notes
 
 ## Development
-[Commits](https://github.com/wycats/handlebars.js/compare/v1.0.12...master)
+
+[Commits](https://github.com/wycats/handlebars.js/compare/v1.1.0...master)
+
+## v1.1.0 - November 3rd, 2013
+
+- [#628](https://github.com/wycats/handlebars.js/pull/628) - Convert code to ES6 modules ([@kpdecker](https://api.github.com/users/kpdecker))
+- [#336](https://github.com/wycats/handlebars.js/pull/336) - Add whitespace control syntax ([@kpdecker](https://api.github.com/users/kpdecker))
+- [#535](https://github.com/wycats/handlebars.js/pull/535) - Fix for probable JIT error under Safari ([@sorentwo](https://api.github.com/users/sorentwo))
+- [#483](https://github.com/wycats/handlebars.js/issues/483) - Add first and last @ vars to each helper ([@denniskuczynski](https://api.github.com/users/denniskuczynski))
+- [#557](https://github.com/wycats/handlebars.js/pull/557) - `\\{{foo}}` escaping only works in some situations ([@dmarcotte](https://api.github.com/users/dmarcotte))
+- [#552](https://github.com/wycats/handlebars.js/pull/552) - Added BOM removal flag. ([@blessenm](https://api.github.com/users/blessenm))
+- [#543](https://github.com/wycats/handlebars.js/pull/543) - publish passing master builds to s3 ([@fivetanley](https://api.github.com/users/fivetanley))
+
+- [#608](https://github.com/wycats/handlebars.js/issues/608) - Add `includeZero` flag to `if` conditional
+- [#498](https://github.com/wycats/handlebars.js/issues/498) - `Handlebars.compile` fails on empty string although a single blank works fine
+- [#599](https://github.com/wycats/handlebars.js/issues/599) - lambda helpers only receive options if used with arguments
+- [#592](https://github.com/wycats/handlebars.js/issues/592) - Optimize array and subprogram performance
+- [#571](https://github.com/wycats/handlebars.js/issues/571) - uglify upgrade breaks compatibility with older versions of node
+- [#587](https://github.com/wycats/handlebars.js/issues/587) - Partial inside partial breaks?
+
+
+Compatibility notes:
+- The project now includes separate artifacts for AMD, CommonJS, and global objects. 
+  - AMD: Users may load the bundled `handlebars.amd.js` or `handlebars.runtime.amd.js` files or load individual modules directly. AMD users should also note that the handlebars object is exposed via the `default` field on the imported object. This [gist](https://gist.github.com/wycats/7417be0dc361a69d5916) provides some discussion of possible compatibility shims.
+  - CommonJS/Node: Node loading occurs as normal via `require`
+  - Globals: The `handlebars.js` and `handlebars.runtime.js` files should behave in the same manner as the v1.0.12 / 1.0.0 release.
+- Build artifacts have been removed from the repository. [npm][npm], [components/handlebars.js][components], [cdnjs][cdnjs-lib], or the [builds page][builds-page] should now be used as the source of built artifacts. 
+- Context-stored helpers are now always passed the `options` hash. Previously no-argument helpers did not have this argument.
+
+
+[Commits](https://github.com/wycats/handlebars.js/compare/v1.0.12...v1.1.0)
 
 ## v1.0.12 / 1.0.0 - May 31 2013
 
@@ -21,6 +51,7 @@ Compatibility notes:
 - The parser is now stricter on `{{{`, requiring that the end token be `}}}`. Templates that do not
   follow this convention should add the additional brace value.
 - Code that relies on global the namespace being muted when custom helpers or partials are passed will need to explicitly pass an `undefined` value for any helpers that should not be available.
+- The compiler version has changed. Precompiled templates with 1.0.12 or higher must use the 1.0.0 or higher runtime.
 
 [Commits](https://github.com/wycats/handlebars.js/compare/v1.0.11...v1.0.12)
 
@@ -91,3 +122,8 @@ Use:
 ```js
 template(context, {helpers: helpers, partials: partials, data: data})
 ```
+
+[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/index.html
+[cdn-js]: http://cdnjs.com/libraries/handlebars.js/
+[components]: https://github.com/components/handlebars.js
+[npm]: https://npmjs.org/package/handlebars
diff --git a/spec/acceptance_spec.rb b/spec/acceptance_spec.rb
deleted file mode 100644
index 03b23c5de..000000000
--- a/spec/acceptance_spec.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-require "spec_helper"
-
-class TestContext
-  class TestModule
-    attr_reader :name, :tests
-
-    def initialize(name)
-      @name = name
-      @tests = []
-    end
-  end
-
-  attr_reader :modules
-
-  def initialize
-    @modules = []
-  end
-
-  def module(name)
-    @modules << TestModule.new(name)
-  end
-
-  def test(name, function)
-    @modules.last.tests << [name, function]
-  end
-end
-
-test_context = TestContext.new
-js_context = Handlebars::Spec::CONTEXT
-
-Module.new do
-  extend Test::Unit::Assertions
-
-  def self.js_backtrace(context)
-    begin
-      context.eval("throw")
-    rescue V8::JSError => e
-      return e.backtrace(:javascript)
-    end
-  end
-
-  js_context["p"] = proc do |this, str|
-    p str
-  end
-
-  js_context["ok"] = proc do |this, ok, message|
-    js_context["$$RSPEC1$$"] = ok
-
-    result = js_context.eval("!!$$RSPEC1$$")
-
-    message ||= "#{ok} was not truthy"
-
-    unless result
-      backtrace = js_backtrace(js_context)
-      message << "\n#{backtrace.join("\n")}"
-    end
-
-    assert result, message
-  end
-
-  js_context["equals"] = proc do |this, first, second, message|
-    js_context["$$RSPEC1$$"] = first
-    js_context["$$RSPEC2$$"] = second
-
-    result = js_context.eval("$$RSPEC1$$ == $$RSPEC2$$")
-
-    additional_message = "#{first.inspect} did not == #{second.inspect}"
-    message = message ? "#{message} (#{additional_message})" : additional_message
-
-    unless result
-      backtrace = js_backtrace(js_context)
-      message << "\n#{backtrace.join("\n")}"
-    end
-
-    assert result, message
-  end
-
-  js_context["equal"] = js_context["equals"]
-
-  js_context["suite"] = proc do |this, name|
-    test_context.module(name)
-  end
-
-  js_context["test"] = proc do |this, name, function|
-    test_context.test(name, function)
-  end
-
-  local = Regexp.escape(File.expand_path(Dir.pwd))
-  qunit_spec = File.expand_path("../qunit_spec.js", __FILE__)
-  js_context.load(qunit_spec.sub(/^#{local}\//, ''))
-end
-
-test_context.modules.each do |mod|
-  describe mod.name do
-    mod.tests.each do |name, function|
-      it name do
-        function.call
-      end
-    end
-  end
-end
diff --git a/spec/example_1.handlebars b/spec/artifacts/example_1.handlebars
similarity index 100%
rename from spec/example_1.handlebars
rename to spec/artifacts/example_1.handlebars
diff --git a/spec/example_2.hbs b/spec/artifacts/example_2.hbs
similarity index 100%
rename from spec/example_2.hbs
rename to spec/artifacts/example_2.hbs
diff --git a/spec/basic.js b/spec/basic.js
new file mode 100644
index 000000000..ee154a165
--- /dev/null
+++ b/spec/basic.js
@@ -0,0 +1,188 @@
+global.handlebarsEnv = null;
+
+beforeEach(function() {
+  global.handlebarsEnv = Handlebars.create();
+});
+
+describe("basic context", function() {
+  it("most basic", function() {
+    shouldCompileTo("{{foo}}", { foo: "foo" }, "foo");
+  });
+
+  it("escaping", function() {
+    shouldCompileTo("\\{{foo}}", { foo: "food" }, "{{foo}}");
+    shouldCompileTo("content \\{{foo}}", { foo: "food" }, "content {{foo}}");
+    shouldCompileTo("\\\\{{foo}}", { foo: "food" }, "\\food");
+    shouldCompileTo("content \\\\{{foo}}", { foo: "food" }, "content \\food");
+    shouldCompileTo("\\\\ {{foo}}", { foo: "food" }, "\\\\ food");
+  });
+
+  it("compiling with a basic context", function() {
+    shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
+                    "It works if all the required keys are provided");
+  });
+
+  it("compiling with an undefined context", function() {
+    shouldCompileTo("Goodbye\n{{cruel}}\n{{world.bar}}!", undefined, "Goodbye\n\n!");
+
+    shouldCompileTo("{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}", undefined, "Goodbye");
+  });
+
+  it("comments", function() {
+    shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
+      {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
+      "comments are ignored");
+  });
+
+  it("boolean", function() {
+    var string   = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!";
+    shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
+                    "booleans show the contents when true");
+
+    shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
+                    "booleans do not show the contents when false");
+  });
+
+  it("zeros", function() {
+    shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0},
+        "num1: 42, num2: 0");
+    shouldCompileTo("num: {{.}}", 0, "num: 0");
+    shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0");
+  });
+
+  it("newlines", function() {
+      shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest");
+      shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest");
+  });
+
+  it("escaping text", function() {
+    shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes");
+    shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored");
+    shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes");
+    shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes");
+    shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript");
+  });
+
+  it("escaping expressions", function() {
+   shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>',
+          "expressions with 3 handlebars aren't escaped");
+
+   shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
+          "expressions with {{& handlebars aren't escaped");
+
+   shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;',
+          "by default expressions should be escaped");
+
+   shouldCompileTo("{{awesome}}", {awesome: "Escaped, <b> looks like: &lt;b&gt;"}, 'Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;gt;',
+          "escaping should properly handle amperstands");
+  });
+
+  it("functions returning safestrings shouldn't be escaped", function() {
+    var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }};
+    shouldCompileTo("{{awesome}}", hash, '&\"\\<>',
+        "functions returning safestrings aren't escaped");
+  });
+
+  it("functions", function() {
+    shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome",
+                    "functions are called and render their output");
+    shouldCompileTo("{{awesome}}", {awesome: function() { return this.more; }, more:  "More awesome"}, "More awesome",
+                    "functions are bound to the context");
+  });
+
+  it("functions with context argument", function() {
+    shouldCompileTo("{{awesome frank}}",
+        {awesome: function(context) { return context; },
+          frank: "Frank"},
+        "Frank", "functions are called with context arguments");
+  });
+
+  it("block functions with context argument", function() {
+    shouldCompileTo("{{#awesome 1}}inner {{.}}{{/awesome}}",
+        {awesome: function(context, options) { return options.fn(context); }},
+        "inner 1", "block functions are called with context and options");
+  });
+
+  it("block functions without context argument", function() {
+    shouldCompileTo("{{#awesome}}inner{{/awesome}}",
+        {awesome: function(options) { return options.fn(this); }},
+        "inner", "block functions are called with options");
+  });
+
+
+  it("paths with hyphens", function() {
+    shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)");
+    shouldCompileTo("{{foo.foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)");
+    shouldCompileTo("{{foo/foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)");
+  });
+
+  it("nested paths", function() {
+    shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}},
+                    "Goodbye beautiful world!", "Nested paths access nested objects");
+  });
+
+  it("nested paths with empty string value", function() {
+    shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}},
+                    "Goodbye  world!", "Nested paths access nested objects with empty string");
+  });
+
+  it("literal paths", function() {
+    shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}},
+        "Goodbye beautiful world!", "Literal paths can be used");
+    shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}},
+        "Goodbye beautiful world!", "Literal paths can be used");
+  });
+
+  it('literal references', function() {
+    shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"},
+        "Goodbye beautiful world!", "Literal paths can be used");
+  });
+
+  it("that current context path ({{.}}) doesn't hit helpers", function() {
+    shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: ");
+  });
+
+  it("complex but empty paths", function() {
+    shouldCompileTo("{{person/name}}", {person: {name: null}}, "");
+    shouldCompileTo("{{person/name}}", {person: {}}, "");
+  });
+
+  it("this keyword in paths", function() {
+    var string = "{{#goodbyes}}{{this}}{{/goodbyes}}";
+    var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
+    shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE",
+      "This keyword in paths evaluates to current context");
+
+    string = "{{#hellos}}{{this/text}}{{/hellos}}";
+    hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
+    shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths");
+  });
+
+  it("this keyword nested inside path", function() {
+    var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}";
+    (function() {
+      CompilerContext.compile(string);
+    }).should.throw(Error);
+  });
+
+  it("this keyword in helpers", function() {
+    var helpers = {foo: function(value) {
+        return 'bar ' + value;
+    }};
+    var string = "{{#goodbyes}}{{foo this}}{{/goodbyes}}";
+    var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
+    shouldCompileTo(string, [hash, helpers], "bar goodbyebar Goodbyebar GOODBYE",
+      "This keyword in paths evaluates to current context");
+
+    string = "{{#hellos}}{{foo this/text}}{{/hellos}}";
+    hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
+    shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths");
+  });
+
+  it("this keyword nested inside helpers param", function() {
+    var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}";
+    (function() {
+      CompilerContext.compile(string);
+    }).should.throw(Error);
+  });
+});
diff --git a/spec/blocks.js b/spec/blocks.js
new file mode 100644
index 000000000..1880eb584
--- /dev/null
+++ b/spec/blocks.js
@@ -0,0 +1,86 @@
+/*global CompilerContext, shouldCompileTo */
+describe('blocks', function() {
+  it("array", function() {
+    var string   = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
+    var hash     = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+    shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
+                    "Arrays iterate over the contents when not empty");
+
+    shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
+                    "Arrays ignore the contents when empty");
+
+  });
+
+  it("array with @index", function() {
+    var string = "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!";
+    var hash   = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+
+    var template = CompilerContext.compile(string);
+    var result = template(hash);
+
+    equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
+  });
+
+  it("empty block", function() {
+    var string   = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!";
+    var hash     = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+    shouldCompileTo(string, hash, "cruel world!",
+                    "Arrays iterate over the contents when not empty");
+
+    shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
+                    "Arrays ignore the contents when empty");
+  });
+
+  it("block with complex lookup", function() {
+    var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}";
+    var hash     = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]};
+
+    shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
+                    "Templates can access variables in contexts up the stack with relative path syntax");
+  });
+
+  it("block with complex lookup using nested context", function() {
+    var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}";
+
+    (function() {
+      CompilerContext.compile(string);
+    }).should.throw(Error);
+  });
+
+  it("block with deep nested complex lookup", function() {
+    var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}";
+    var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
+
+    shouldCompileTo(string, hash, "Goodbye cruel OMG!");
+  });
+
+  describe('inverted sections', function() {
+    it("inverted sections with unset value", function() {
+      var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
+      var hash = {};
+      shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set.");
+    });
+
+    it("inverted section with false value", function() {
+      var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
+      var hash = {goodbyes: false};
+      shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false.");
+    });
+
+    it("inverted section with empty set", function() {
+      var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
+      var hash = {goodbyes: []};
+      shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set.");
+    });
+
+    it("block inverted sections", function() {
+      shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"},
+        "No people");
+    });
+
+    it("block inverted sections with empty arrays", function() {
+      shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []},
+        "No people");
+    });
+  });
+});
diff --git a/spec/builtins.js b/spec/builtins.js
new file mode 100644
index 000000000..401e789a9
--- /dev/null
+++ b/spec/builtins.js
@@ -0,0 +1,178 @@
+/*global CompilerContext, shouldCompileTo, compileWithPartials */
+describe('builtin helpers', function() {
+  describe('#if', function() {
+    it("if", function() {
+      var string   = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
+      shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
+                      "if with boolean argument shows the contents when true");
+      shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!",
+                      "if with string argument shows the contents");
+      shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
+                      "if with boolean argument does not show the contents when false");
+      shouldCompileTo(string, {world: "world"}, "cruel world!",
+                      "if with undefined does not show the contents");
+      shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!",
+                      "if with non-empty array shows the contents");
+      shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!",
+                      "if with empty array does not show the contents");
+      shouldCompileTo(string, {goodbye: 0, world: "world"}, "cruel world!",
+                      "if with zero does not show the contents");
+      shouldCompileTo("{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!",
+                      {goodbye: 0, world: "world"}, "GOODBYE cruel world!",
+                      "if with zero does not show the contents");
+    });
+
+    it("if with function argument", function() {
+      var string   = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
+      shouldCompileTo(string, {goodbye: function() {return true;}, world: "world"}, "GOODBYE cruel world!",
+                      "if with function shows the contents when function returns true");
+      shouldCompileTo(string, {goodbye: function() {return this.world;}, world: "world"}, "GOODBYE cruel world!",
+                      "if with function shows the contents when function returns string");
+      shouldCompileTo(string, {goodbye: function() {return false;}, world: "world"}, "cruel world!",
+                      "if with function does not show the contents when returns false");
+      shouldCompileTo(string, {goodbye: function() {return this.foo;}, world: "world"}, "cruel world!",
+                      "if with function does not show the contents when returns undefined");
+    });
+  });
+
+  describe('#with', function() {
+    it("with", function() {
+      var string = "{{#with person}}{{first}} {{last}}{{/with}}";
+      shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson");
+    });
+    it("with with function argument", function() {
+      var string = "{{#with person}}{{first}} {{last}}{{/with}}";
+      shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson");
+    });
+  });
+
+  describe('#each', function() {
+    beforeEach(function() {
+      handlebarsEnv.registerHelper('detectDataInsideEach', function(options) {
+        return options.data && options.data.exclaim;
+      });
+    });
+
+    it("each", function() {
+      var string   = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!";
+      var hash     = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+      shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
+                      "each with array argument iterates over the contents when not empty");
+      shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
+                      "each with array argument ignores the contents when empty");
+    });
+
+    it("each with an object and @key", function() {
+      var string   = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!";
+      var hash     = {goodbyes: {"<b>#1</b>": {text: "goodbye"}, 2: {text: "GOODBYE"}}, world: "world"};
+
+      // Object property iteration order is undefined according to ECMA spec,
+      // so we need to check both possible orders
+      // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop
+      var actual = compileWithPartials(string, hash);
+      var expected1 = "&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!";
+      var expected2 = "2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!";
+
+      (actual === expected1 || actual === expected2).should.equal(true, "each with object argument iterates over the contents when not empty");
+      shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
+                      "each with object argument ignores the contents when empty");
+    });
+
+    it("each with @index", function() {
+      var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!";
+      var hash   = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+
+      var template = CompilerContext.compile(string);
+      var result = template(hash);
+
+      equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
+    });
+
+    it("each with nested @index", function() {
+      var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!";
+      var hash   = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+
+      var template = CompilerContext.compile(string);
+      var result = template(hash);
+
+      equal(result, "0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!", "The @index variable is used");
+    });
+
+    it("each with @first", function() {
+      var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
+      var hash   = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+
+      var template = CompilerContext.compile(string);
+      var result = template(hash);
+
+      equal(result, "goodbye! cruel world!", "The @first variable is used");
+    });
+
+    it("each with nested @first", function() {
+      var string = "{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!";
+      var hash   = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+
+      var template = CompilerContext.compile(string);
+      var result = template(hash);
+
+      equal(result, "(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!", "The @first variable is used");
+    });
+
+    it("each with @last", function() {
+      var string = "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
+      var hash   = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+
+      var template = CompilerContext.compile(string);
+      var result = template(hash);
+
+      equal(result, "GOODBYE! cruel world!", "The @last variable is used");
+    });
+
+    it("each with nested @last", function() {
+      var string = "{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!";
+      var hash   = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+
+      var template = CompilerContext.compile(string);
+      var result = template(hash);
+
+      equal(result, "(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!", "The @last variable is used");
+    });
+
+    it("each with function argument", function() {
+      var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!";
+      var hash   = {goodbyes: function () { return [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];}, world: "world"};
+      shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
+                "each with array function argument iterates over the contents when not empty");
+      shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
+                "each with array function argument ignores the contents when empty");
+    });
+
+    it("data passed to helpers", function() {
+      var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}";
+      var hash = {letters: ['a', 'b', 'c']};
+
+      var template = CompilerContext.compile(string);
+      var result = template(hash, {
+        data: {
+          exclaim: '!'
+        }
+      });
+      equal(result, 'a!b!c!', 'should output data');
+    });
+
+  });
+
+  it("#log", function() {
+
+    var string = "{{log blah}}";
+    var hash   = { blah: "whee" };
+
+    var levelArg, logArg;
+    handlebarsEnv.log = function(level, arg){ levelArg = level, logArg = arg; };
+
+    shouldCompileTo(string, hash, "", "log should not display");
+    equals(1, levelArg, "should call log with 1");
+    equals("whee", logArg, "should call log with 'whee'");
+  });
+
+});
diff --git a/spec/data.js b/spec/data.js
new file mode 100644
index 000000000..cf9424e97
--- /dev/null
+++ b/spec/data.js
@@ -0,0 +1,239 @@
+/*global CompilerContext */
+describe('data', function() {
+  it("passing in data to a compiled function that expects data - works with helpers", function() {
+    var template = CompilerContext.compile("{{hello}}", {data: true});
+
+    var helpers = {
+      hello: function(options) {
+        return options.data.adjective + " "  + this.noun;
+      }
+    };
+
+    var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}});
+    equals("happy cat", result, "Data output by helper");
+  });
+
+  it("data can be looked up via @foo", function() {
+    var template = CompilerContext.compile("{{@hello}}");
+    var result = template({}, { data: { hello: "hello" } });
+    equals("hello", result, "@foo retrieves template data");
+  });
+
+  it("deep @foo triggers automatic top-level data", function() {
+    var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}');
+
+    var helpers = Handlebars.createFrame(handlebarsEnv.helpers);
+
+    helpers.let = function(options) {
+      var frame = Handlebars.createFrame(options.data);
+
+      for (var prop in options.hash) {
+        frame[prop] = options.hash[prop];
+      }
+      return options.fn(this, { data: frame });
+    };
+
+    var result = template({ foo: true }, { helpers: helpers });
+    equals("Hello world", result, "Automatic data was triggered");
+  });
+
+  it("parameter data can be looked up via @foo", function() {
+    var template = CompilerContext.compile("{{hello @world}}");
+    var helpers = {
+      hello: function(noun) {
+        return "Hello " + noun;
+      }
+    };
+
+    var result = template({}, { helpers: helpers, data: { world: "world" } });
+    equals("Hello world", result, "@foo as a parameter retrieves template data");
+  });
+
+  it("hash values can be looked up via @foo", function() {
+    var template = CompilerContext.compile("{{hello noun=@world}}");
+    var helpers = {
+      hello: function(options) {
+        return "Hello " + options.hash.noun;
+      }
+    };
+
+    var result = template({}, { helpers: helpers, data: { world: "world" } });
+    equals("Hello world", result, "@foo as a parameter retrieves template data");
+  });
+
+  it("nested parameter data can be looked up via @foo.bar", function() {
+    var template = CompilerContext.compile("{{hello @world.bar}}");
+    var helpers = {
+      hello: function(noun) {
+        return "Hello " + noun;
+      }
+    };
+
+    var result = template({}, { helpers: helpers, data: { world: {bar: "world" } } });
+    equals("Hello world", result, "@foo as a parameter retrieves template data");
+  });
+
+  it("nested parameter data does not fail with @world.bar", function() {
+    var template = CompilerContext.compile("{{hello @world.bar}}");
+    var helpers = {
+      hello: function(noun) {
+        return "Hello " + noun;
+      }
+    };
+
+    var result = template({}, { helpers: helpers, data: { foo: {bar: "world" } } });
+    equals("Hello undefined", result, "@foo as a parameter retrieves template data");
+  });
+
+  it("parameter data throws when using this scope references", function() {
+    var string = "{{#goodbyes}}{{text}} cruel {{@./name}}! {{/goodbyes}}";
+
+    (function() {
+      CompilerContext.compile(string);
+    }).should.throw(Error);
+  });
+
+  it("parameter data throws when using parent scope references", function() {
+    var string = "{{#goodbyes}}{{text}} cruel {{@../name}}! {{/goodbyes}}";
+
+    (function() {
+      CompilerContext.compile(string);
+    }).should.throw(Error);
+  });
+
+  it("parameter data throws when using complex scope references", function() {
+    var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}";
+
+    (function() {
+      CompilerContext.compile(string);
+    }).should.throw(Error);
+  });
+
+  it("data is inherited downstream", function() {
+    var template = CompilerContext.compile("{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}", { data: true });
+    var helpers = {
+      let: function(options) {
+        var frame = Handlebars.createFrame(options.data);
+        for (var prop in options.hash) {
+          frame[prop] = options.hash[prop];
+        }
+        return options.fn(this, {data: frame});
+      }
+    };
+
+    var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} });
+    equals("2hello world1", result, "data variables are inherited downstream");
+  });
+
+  it("passing in data to a compiled function that expects data - works with helpers in partials", function() {
+    var template = CompilerContext.compile("{{>my_partial}}", {data: true});
+
+    var partials = {
+      my_partial: CompilerContext.compile("{{hello}}", {data: true})
+    };
+
+    var helpers = {
+      hello: function(options) {
+        return options.data.adjective + " "  + this.noun;
+      }
+    };
+
+    var result = template({noun: "cat"}, {helpers: helpers, partials: partials, data: {adjective: "happy"}});
+    equals("happy cat", result, "Data output by helper inside partial");
+  });
+
+  it("passing in data to a compiled function that expects data - works with helpers and parameters", function() {
+    var template = CompilerContext.compile("{{hello world}}", {data: true});
+
+    var helpers = {
+      hello: function(noun, options) {
+        return options.data.adjective + " "  + noun + (this.exclaim ? "!" : "");
+      }
+    };
+
+    var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}});
+    equals("happy world!", result, "Data output by helper");
+  });
+
+  it("passing in data to a compiled function that expects data - works with block helpers", function() {
+    var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true});
+
+    var helpers = {
+      hello: function(options) {
+        return options.fn(this);
+      },
+      world: function(options) {
+        return options.data.adjective + " world" + (this.exclaim ? "!" : "");
+      }
+    };
+
+    var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}});
+    equals("happy world!", result, "Data output by helper");
+  });
+
+  it("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
+    var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
+
+    var helpers = {
+      hello: function(options) {
+        return options.fn({exclaim: "?"});
+      },
+      world: function(thing, options) {
+        return options.data.adjective + " " + thing + (this.exclaim || "");
+      }
+    };
+
+    var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
+    equals("happy world?", result, "Data output by helper");
+  });
+
+  it("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() {
+    var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
+
+    var helpers = {
+      hello: function(options) {
+        return options.data.accessData + " " + options.fn({exclaim: "?"});
+      },
+      world: function(thing, options) {
+        return options.data.adjective + " " + thing + (this.exclaim || "");
+      }
+    };
+
+    var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}});
+    equals("#win happy world?", result, "Data output by helper");
+  });
+
+  it("you can override inherited data when invoking a helper", function() {
+    var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true});
+
+    var helpers = {
+      hello: function(options) {
+        return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} });
+      },
+      world: function(thing, options) {
+        return options.data.adjective + " " + thing + (this.exclaim || "");
+      }
+    };
+
+    var result = template({exclaim: true, zomg: "planet"}, {helpers: helpers, data: {adjective: "happy"}});
+    equals("sad world?", result, "Overriden data output by helper");
+  });
+
+
+  it("you can override inherited data when invoking a helper with depth", function() {
+    var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
+
+    var helpers = {
+      hello: function(options) {
+        return options.fn({exclaim: "?"}, { data: {adjective: "sad"} });
+      },
+      world: function(thing, options) {
+        return options.data.adjective + " " + thing + (this.exclaim || "");
+      }
+    };
+
+    var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
+    equals("sad world?", result, "Overriden data output by helper");
+  });
+
+});
diff --git a/spec/env/browser.js b/spec/env/browser.js
new file mode 100644
index 000000000..9f69e74d7
--- /dev/null
+++ b/spec/env/browser.js
@@ -0,0 +1,23 @@
+/*global handlebarsEnv */
+require('./common');
+
+var _ = require('underscore'),
+    fs = require('fs'),
+    vm = require('vm');
+
+global.Handlebars = undefined;
+vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.js'), 'dist/handlebars.js');
+
+global.CompilerContext = {
+  compile: function(template, options) {
+    var templateSpec = handlebarsEnv.precompile(template, options);
+    return handlebarsEnv.template(safeEval(templateSpec));
+  },
+  compileWithPartial: function(template, options) {
+    return handlebarsEnv.compile(template, options);
+  }
+};
+
+function safeEval(templateSpec) {
+  return eval('(' + templateSpec + ')');
+}
diff --git a/spec/env/common.js b/spec/env/common.js
new file mode 100644
index 000000000..53ddd61ae
--- /dev/null
+++ b/spec/env/common.js
@@ -0,0 +1,28 @@
+global.should = require('should');
+
+global.shouldCompileTo = function(string, hashOrArray, expected, message) {
+  shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
+};
+
+global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) {
+  var result = compileWithPartials(string, hashOrArray, partials);
+  result.should.equal(expected, "'" + expected + "' should === '" + result + "': " + message);
+};
+
+global.compileWithPartials = function(string, hashOrArray, partials) {
+  var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary;
+  if(Object.prototype.toString.call(hashOrArray) === "[object Array]") {
+    ary = [];
+    ary.push(hashOrArray[0]);
+    ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
+  } else {
+    ary = [hashOrArray];
+  }
+
+  return template.apply(this, ary);
+};
+
+
+global.equals = global.equal = function(a, b, msg) {
+  a.should.equal(b, msg || '');
+};
diff --git a/spec/env/node.js b/spec/env/node.js
new file mode 100644
index 000000000..808c07ef4
--- /dev/null
+++ b/spec/env/node.js
@@ -0,0 +1,18 @@
+/*global handlebarsEnv */
+require('./common');
+
+global.Handlebars = require('../../lib');
+
+global.CompilerContext = {
+  compile: function(template, options) {
+    var templateSpec = handlebarsEnv.precompile(template, options);
+    return handlebarsEnv.template(safeEval(templateSpec));
+  },
+  compileWithPartial: function(template, options) {
+    return handlebarsEnv.compile(template, options);
+  }
+};
+
+function safeEval(templateSpec) {
+  return eval('(' + templateSpec + ')');
+}
diff --git a/spec/env/runner.js b/spec/env/runner.js
new file mode 100644
index 000000000..143d30023
--- /dev/null
+++ b/spec/env/runner.js
@@ -0,0 +1,42 @@
+var fs = require('fs'),
+    Mocha = require('mocha'),
+    path = require('path');
+
+var errors = 0,
+    testDir = path.dirname(__dirname),
+    grep = process.argv[2];
+
+var files = [ testDir + "/basic.js" ];
+
+var files = fs.readdirSync(testDir)
+      .filter(function(name) { return (/.*\.js$/).test(name); })
+      .map(function(name) { return testDir + '/' + name; });
+
+run('./node', function() {
+  run('./browser', function() {
+    run('./runtime', function() {
+      process.exit(errors);
+    });
+  });
+});
+
+
+function run(env, callback) {
+  var mocha = new Mocha();
+  mocha.ui('bdd');
+  mocha.files = files.slice();
+  if (grep) {
+    mocha.grep(grep);
+  }
+
+  files.forEach(function(name) {
+    delete require.cache[name];
+  });
+
+  console.log('Running env: ' + env);
+  require(env);
+  mocha.run(function(errorCount) {
+    errors += errorCount;
+    callback();
+  });
+}
diff --git a/spec/env/runtime.js b/spec/env/runtime.js
new file mode 100644
index 000000000..68e40b058
--- /dev/null
+++ b/spec/env/runtime.js
@@ -0,0 +1,29 @@
+/*global handlebarsEnv */
+require('./common');
+
+var _ = require('underscore'),
+    fs = require('fs'),
+    vm = require('vm');
+
+global.Handlebars = undefined;
+vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.runtime.js'), 'dist/handlebars.runtime.js');
+
+var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
+
+global.CompilerContext = {
+  compile: function(template, options) {
+    var templateSpec = compiler.precompile(template, options);
+    return handlebarsEnv.template(safeEval(templateSpec));
+  },
+  compileWithPartial: function(template, options) {
+    // Hack the compiler on to the environment for these specific tests
+    handlebarsEnv.compile = function(template, options) {
+      return compiler.compile(template, options, handlebarsEnv);
+    };
+    return handlebarsEnv.compile(template, options);
+  }
+};
+
+function safeEval(templateSpec) {
+  return eval('(' + templateSpec + ')');
+}
diff --git a/spec/helpers.js b/spec/helpers.js
new file mode 100644
index 000000000..c5ea574bc
--- /dev/null
+++ b/spec/helpers.js
@@ -0,0 +1,528 @@
+/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */
+describe('helpers', function() {
+  it("helper with complex lookup$", function() {
+    var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}";
+    var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
+    var helpers = {link: function(prefix) {
+      return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>";
+    }};
+    shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>");
+  });
+
+  it("helper block with complex lookup expression", function() {
+    var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}";
+    var hash = {name: "Alan"};
+    var helpers = {goodbyes: function(options) {
+      var out = "";
+      var byes = ["Goodbye", "goodbye", "GOODBYE"];
+      for (var i = 0,j = byes.length; i < j; i++) {
+        out += byes[i] + " " + options.fn(this) + "! ";
+      }
+      return out;
+    }};
+    shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
+  });
+
+  it("helper with complex lookup and nested template", function() {
+    var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
+    var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
+    var helpers = {link: function (prefix, options) {
+        return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>";
+    }};
+    shouldCompileToWithPartials(string, [hash, helpers], false, "<a href='/root/goodbye'>Goodbye</a>");
+  });
+
+  it("helper with complex lookup and nested template in VM+Compiler", function() {
+    var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
+    var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
+    var helpers = {link: function (prefix, options) {
+        return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>";
+    }};
+    shouldCompileToWithPartials(string, [hash, helpers], true, "<a href='/root/goodbye'>Goodbye</a>");
+  });
+
+  it("block helper", function() {
+    var string   = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
+    var template = CompilerContext.compile(string);
+
+    var result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}});
+    equal(result, "GOODBYE! cruel world!", "Block helper executed");
+  });
+
+  it("block helper staying in the same context", function() {
+    var string   = "{{#form}}<p>{{name}}</p>{{/form}}";
+    var template = CompilerContext.compile(string);
+
+    var result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "<form>" + options.fn(this) + "</form>"; } }});
+    equal(result, "<form><p>Yehuda</p></form>", "Block helper executed with current context");
+  });
+
+  it("block helper should have context in this", function() {
+    var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>";
+    var link = function(options) {
+      return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
+    };
+    var data = { "people": [
+      { "name": "Alan", "id": 1 },
+      { "name": "Yehuda", "id": 2 }
+    ]};
+
+    shouldCompileTo(source, [data, {link: link}], "<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>");
+  });
+
+  it("block helper for undefined value", function() {
+    shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, "");
+  });
+
+  it("block helper passing a new context", function() {
+    var string   = "{{#form yehuda}}<p>{{name}}</p>{{/form}}";
+    var template = CompilerContext.compile(string);
+
+    var result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}});
+    equal(result, "<form><p>Yehuda</p></form>", "Context variable resolved");
+  });
+
+  it("block helper passing a complex path context", function() {
+    var string   = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}";
+    var template = CompilerContext.compile(string);
+
+    var result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}});
+    equal(result, "<form><p>Harold</p></form>", "Complex path variable resolved");
+  });
+
+  it("nested block helpers", function() {
+    var string   = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}";
+    var template = CompilerContext.compile(string);
+
+    var result = template({
+      yehuda: {name: "Yehuda" }
+    }, {
+      helpers: {
+        link: function(options) { return "<a href='" + this.name + "'>" + options.fn(this) + "</a>"; },
+        form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }
+      }
+    });
+    equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>", "Both blocks executed");
+  });
+
+  it("block helper inverted sections", function() {
+    var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}";
+    var list = function(context, options) {
+      if (context.length > 0) {
+        var out = "<ul>";
+        for(var i = 0,j=context.length; i < j; i++) {
+          out += "<li>";
+          out += options.fn(context[i]);
+          out += "</li>";
+        }
+        out += "</ul>";
+        return out;
+      } else {
+        return "<p>" + options.inverse(this) + "</p>";
+      }
+    };
+
+    var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]};
+    var empty = {people: []};
+    var rootMessage = {
+      people: [],
+      message: "Nobody's here"
+    };
+
+    var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}";
+
+    // the meaning here may be kind of hard to catch, but list.not is always called,
+    // so we should see the output of both
+    shouldCompileTo(string, [hash, { list: list }], "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context");
+    shouldCompileTo(string, [empty, { list: list }], "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called");
+    shouldCompileTo(messageString, [rootMessage, { list: list }], "<p>Nobody&#x27;s here</p>", "the context of an inverse is the parent of the block");
+  });
+
+  describe("helpers hash", function() {
+    it("providing a helpers hash", function() {
+      shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!",
+                      "helpers hash is available");
+
+      shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}],
+                      "Goodbye cruel world!", "helpers hash is available inside other blocks");
+    });
+
+    it("in cases of conflict, helpers win", function() {
+      shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
+                      "helpers hash has precedence escaped expansion");
+      shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
+                      "helpers hash has precedence simple expansion");
+    });
+
+    it("the helpers hash is available is nested contexts", function() {
+      shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}",
+                    [{'outer': {'inner': {'unused':[]}}},  {'helper': function() { return 'helper'; }}], "helper",
+                    "helpers hash is available in nested contexts.");
+    });
+
+    it("the helper hash should augment the global hash", function() {
+      handlebarsEnv.registerHelper('test_helper', function() { return 'found it!'; });
+
+      shouldCompileTo(
+        "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", [
+          {cruel: "cruel"},
+          {world: function() { return "world!"; }}
+        ],
+        "found it! Goodbye cruel world!!");
+    });
+  });
+
+  it("Multiple global helper registration", function() {
+    var helpers = handlebarsEnv.helpers;
+    handlebarsEnv.helpers = {};
+
+    handlebarsEnv.registerHelper({
+      'if': helpers['if'],
+      world: function() { return "world!"; },
+      test_helper: function() { return 'found it!'; }
+    });
+
+    shouldCompileTo(
+      "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}",
+      [{cruel: "cruel"}],
+      "found it! Goodbye cruel world!!");
+  });
+
+  it("negative number literals work", function() {
+    var string   = 'Message: {{hello -12}}';
+    var hash     = {};
+    var helpers  = {hello: function(times) {
+      if(typeof times !== 'number') { times = "NaN"; }
+      return "Hello " + times + " times";
+    }};
+    shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal");
+  });
+
+  describe("String literal parameters", function() {
+    it("simple literals work", function() {
+      var string   = 'Message: {{hello "world" 12 true false}}';
+      var hash     = {};
+      var helpers  = {hello: function(param, times, bool1, bool2) {
+        if(typeof times !== 'number') { times = "NaN"; }
+        if(typeof bool1 !== 'boolean') { bool1 = "NaB"; }
+        if(typeof bool2 !== 'boolean') { bool2 = "NaB"; }
+        return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2;
+      }};
+      shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal");
+    });
+
+    it("using a quote in the middle of a parameter raises an error", function() {
+      (function() {
+        var string   = 'Message: {{hello wo"rld"}}';
+        CompilerContext.compile(string);
+      }).should.throw(Error);
+    });
+
+    it("escaping a String is possible", function(){
+      var string   = 'Message: {{{hello "\\"world\\""}}}';
+      var hash     = {};
+      var helpers = {hello: function(param) { return "Hello " + param; }};
+      shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal");
+    });
+
+    it("it works with ' marks", function() {
+      var string   = 'Message: {{{hello "Alan\'s world"}}}';
+      var hash     = {};
+      var helpers = {hello: function(param) { return "Hello " + param; }};
+      shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark");
+    });
+  });
+
+  it("negative number literals work", function() {
+    var string   = 'Message: {{hello -12}}';
+    var hash     = {};
+    var helpers  = {hello: function(times) {
+      if(typeof times !== 'number') { times = "NaN"; }
+      return "Hello " + times + " times";
+    }};
+    shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal");
+  });
+
+  describe("multiple parameters", function() {
+    it("simple multi-params work", function() {
+      var string   = 'Message: {{goodbye cruel world}}';
+      var hash     = {cruel: "cruel", world: "world"};
+      var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }};
+      shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params");
+    });
+
+    it("block multi-params work", function() {
+      var string   = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
+      var hash     = {cruel: "cruel", world: "world"};
+      var helpers = {goodbye: function(cruel, world, options) {
+        return options.fn({greeting: "Goodbye", adj: cruel, noun: world});
+      }};
+      shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params");
+    });
+  });
+  describe('hash', function() {
+    it("helpers can take an optional hash", function() {
+      var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}');
+
+      var helpers = {
+        goodbye: function(options) {
+          return "GOODBYE " + options.hash.cruel + " " + options.hash.world + " " + options.hash.times + " TIMES";
+        }
+      };
+
+      var context = {};
+
+      var result = template(context, {helpers: helpers});
+      equals(result, "GOODBYE CRUEL WORLD 12 TIMES", "Helper output hash");
+    });
+
+    it("helpers can take an optional hash with booleans", function() {
+      var helpers = {
+        goodbye: function(options) {
+          if (options.hash.print === true) {
+            return "GOODBYE " + options.hash.cruel + " " + options.hash.world;
+          } else if (options.hash.print === false) {
+            return "NOT PRINTING";
+          } else {
+            return "THIS SHOULD NOT HAPPEN";
+          }
+        }
+      };
+
+      var context = {};
+
+      var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}');
+      var result = template(context, {helpers: helpers});
+      equals(result, "GOODBYE CRUEL WORLD", "Helper output hash");
+
+      template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}');
+      result = template(context, {helpers: helpers});
+      equals(result, "NOT PRINTING", "Boolean helper parameter honored");
+    });
+
+    it("block helpers can take an optional hash", function() {
+      var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}');
+
+      var helpers = {
+        goodbye: function(options) {
+          return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
+        }
+      };
+
+      var result = template({}, {helpers: helpers});
+      equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output");
+    });
+
+    it("block helpers can take an optional hash with single quoted stings", function() {
+      var template = CompilerContext.compile("{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}");
+
+      var helpers = {
+        goodbye: function(options) {
+          return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
+        }
+      };
+
+      var result = template({}, {helpers: helpers});
+      equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output");
+    });
+
+    it("block helpers can take an optional hash with booleans", function() {
+      var helpers = {
+        goodbye: function(options) {
+          if (options.hash.print === true) {
+            return "GOODBYE " + options.hash.cruel + " " + options.fn(this);
+          } else if (options.hash.print === false) {
+            return "NOT PRINTING";
+          } else {
+            return "THIS SHOULD NOT HAPPEN";
+          }
+        }
+      };
+
+      var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}');
+      var result = template({}, {helpers: helpers});
+      equals(result, "GOODBYE CRUEL world", "Boolean hash parameter honored");
+
+      template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}');
+      result = template({}, {helpers: helpers});
+      equals(result, "NOT PRINTING", "Boolean hash parameter honored");
+    });
+  });
+
+  describe("helperMissing", function() {
+    it("if a context is not found, helperMissing is used", function() {
+      (function() {
+          var template = CompilerContext.compile("{{hello}} {{link_to world}}");
+          template({});
+      }).should.throw(/Missing helper: 'link_to'/);
+    });
+
+    it("if a context is not found, custom helperMissing is used", function() {
+      var string = "{{hello}} {{link_to world}}";
+      var context = { hello: "Hello", world: "world" };
+
+      var helpers = {
+        helperMissing: function(helper, context) {
+          if(helper === "link_to") {
+            return new Handlebars.SafeString("<a>" + context + "</a>");
+          }
+        }
+      };
+
+      shouldCompileTo(string, [context, helpers], "Hello <a>world</a>");
+    });
+  });
+
+  describe("knownHelpers", function() {
+    it("Known helper should render helper", function() {
+      var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}});
+
+      var result = template({}, {helpers: {hello: function() { return "foo"; }}});
+      equal(result, "foo", "'foo' should === '" + result);
+    });
+
+    it("Unknown helper in knownHelpers only mode should be passed as undefined", function() {
+      var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true});
+
+      var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}});
+      equal(result, "undefined", "'undefined' should === '" + result);
+    });
+    it("Builtin helpers available in knownHelpers only mode", function() {
+      var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true});
+
+      var result = template({});
+      equal(result, "bar", "'bar' should === '" + result);
+    });
+    it("Field lookup works in knownHelpers only mode", function() {
+      var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true});
+
+      var result = template({foo: 'bar'});
+      equal(result, "bar", "'bar' should === '" + result);
+    });
+    it("Conditional blocks work in knownHelpers only mode", function() {
+      var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true});
+
+      var result = template({foo: 'baz'});
+      equal(result, "bar", "'bar' should === '" + result);
+    });
+    it("Invert blocks work in knownHelpers only mode", function() {
+      var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true});
+
+      var result = template({foo: false});
+      equal(result, "bar", "'bar' should === '" + result);
+    });
+    it("Functions are bound to the context in knownHelpers only mode", function() {
+      var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true});
+      var result = template({foo: function() { return this.bar; }, bar: 'bar'});
+      equal(result, "bar", "'bar' should === '" + result);
+    });
+    it("Unknown helper call in knownHelpers only mode should throw", function() {
+      (function() {
+        CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true});
+      }).should.throw(Error);
+    });
+  });
+
+  describe("blockHelperMissing", function() {
+    it("lambdas are resolved by blockHelperMissing, not handlebars proper", function() {
+      var string = "{{#truthy}}yep{{/truthy}}";
+      var data = { truthy: function() { return true; } };
+      shouldCompileTo(string, data, "yep");
+    });
+    it("lambdas resolved by blockHelperMissing are bound to the context", function() {
+      var string = "{{#truthy}}yep{{/truthy}}";
+      var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } };
+      shouldCompileTo(string, boundData, "");
+    });
+  });
+
+  describe('name conflicts', function() {
+    it("helpers take precedence over same-named context properties", function() {
+      var template = CompilerContext.compile("{{goodbye}} {{cruel world}}");
+
+      var helpers = {
+        goodbye: function() {
+          return this.goodbye.toUpperCase();
+        },
+
+        cruel: function(world) {
+          return "cruel " + world.toUpperCase();
+        }
+      };
+
+      var context = {
+        goodbye: "goodbye",
+        world: "world"
+      };
+
+      var result = template(context, {helpers: helpers});
+      equals(result, "GOODBYE cruel WORLD", "Helper executed");
+    });
+
+    it("helpers take precedence over same-named context properties$", function() {
+      var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}");
+
+      var helpers = {
+        goodbye: function(options) {
+          return this.goodbye.toUpperCase() + options.fn(this);
+        },
+
+        cruel: function(world) {
+          return "cruel " + world.toUpperCase();
+        }
+      };
+
+      var context = {
+        goodbye: "goodbye",
+        world: "world"
+      };
+
+      var result = template(context, {helpers: helpers});
+      equals(result, "GOODBYE cruel WORLD", "Helper executed");
+    });
+
+    it("Scoped names take precedence over helpers", function() {
+      var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}");
+
+      var helpers = {
+        goodbye: function() {
+          return this.goodbye.toUpperCase();
+        },
+
+        cruel: function(world) {
+          return "cruel " + world.toUpperCase();
+        },
+      };
+
+      var context = {
+        goodbye: "goodbye",
+        world: "world"
+      };
+
+      var result = template(context, {helpers: helpers});
+      equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed");
+    });
+
+    it("Scoped names take precedence over block helpers", function() {
+      var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}");
+
+      var helpers = {
+        goodbye: function(options) {
+          return this.goodbye.toUpperCase() + options.fn(this);
+        },
+
+        cruel: function(world) {
+          return "cruel " + world.toUpperCase();
+        },
+      };
+
+      var context = {
+        goodbye: "goodbye",
+        world: "world"
+      };
+
+      var result = template(context, {helpers: helpers});
+      equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed");
+    });
+  });
+});
diff --git a/spec/parser.js b/spec/parser.js
new file mode 100644
index 000000000..3397105fd
--- /dev/null
+++ b/spec/parser.js
@@ -0,0 +1,173 @@
+describe('parser', function() {
+  if (!Handlebars.print) {
+    return;
+  }
+
+  function ast_for(template) {
+    var ast = Handlebars.parse(template);
+    return Handlebars.print(ast);
+  }
+
+  it('parses simple mustaches', function() {
+    ast_for('{{foo}}').should.equal("{{ ID:foo [] }}\n");
+    ast_for('{{foo?}}').should.equal("{{ ID:foo? [] }}\n");
+    ast_for('{{foo_}}').should.equal("{{ ID:foo_ [] }}\n");
+    ast_for('{{foo-}}').should.equal("{{ ID:foo- [] }}\n");
+    ast_for('{{foo:}}').should.equal("{{ ID:foo: [] }}\n");
+  });
+
+  it('parses simple mustaches with data', function() {
+    ast_for("{{@foo}}").should.equal("{{ @ID:foo [] }}\n");
+  });
+
+  it('parses mustaches with paths', function() {
+    ast_for("{{foo/bar}}").should.equal("{{ PATH:foo/bar [] }}\n");
+  });
+
+  it('parses mustaches with this/foo', function() {
+    ast_for("{{this/foo}}").should.equal("{{ ID:foo [] }}\n");
+  });
+
+  it('parses mustaches with - in a path', function() {
+    ast_for("{{foo-bar}}").should.equal("{{ ID:foo-bar [] }}\n");
+  });
+
+  it('parses mustaches with parameters', function() {
+    ast_for("{{foo bar}}").should.equal("{{ ID:foo [ID:bar] }}\n");
+  });
+
+  it('parses mustaches with string parameters', function() {
+    ast_for("{{foo bar \"baz\" }}").should.equal('{{ ID:foo [ID:bar, "baz"] }}\n');
+  });
+
+  it('parses mustaches with INTEGER parameters', function() {
+    ast_for("{{foo 1}}").should.equal("{{ ID:foo [INTEGER{1}] }}\n");
+  });
+
+  it('parses mustaches with BOOLEAN parameters', function() {
+    ast_for("{{foo true}}").should.equal("{{ ID:foo [BOOLEAN{true}] }}\n");
+    ast_for("{{foo false}}").should.equal("{{ ID:foo [BOOLEAN{false}] }}\n");
+  });
+
+  it('parses mutaches with DATA parameters', function() {
+    ast_for("{{foo @bar}}").should.equal("{{ ID:foo [@ID:bar] }}\n");
+  });
+
+  it('parses mustaches with hash arguments', function() {
+    ast_for("{{foo bar=baz}}").should.equal("{{ ID:foo [] HASH{bar=ID:baz} }}\n");
+    ast_for("{{foo bar=1}}").should.equal("{{ ID:foo [] HASH{bar=INTEGER{1}} }}\n");
+    ast_for("{{foo bar=true}}").should.equal("{{ ID:foo [] HASH{bar=BOOLEAN{true}} }}\n");
+    ast_for("{{foo bar=false}}").should.equal("{{ ID:foo [] HASH{bar=BOOLEAN{false}} }}\n");
+    ast_for("{{foo bar=@baz}}").should.equal("{{ ID:foo [] HASH{bar=@ID:baz} }}\n");
+
+    ast_for("{{foo bar=baz bat=bam}}").should.equal("{{ ID:foo [] HASH{bar=ID:baz, bat=ID:bam} }}\n");
+    ast_for("{{foo bar=baz bat=\"bam\"}}").should.equal('{{ ID:foo [] HASH{bar=ID:baz, bat="bam"} }}\n');
+
+    ast_for("{{foo bat='bam'}}").should.equal('{{ ID:foo [] HASH{bat="bam"} }}\n');
+
+    ast_for("{{foo omg bar=baz bat=\"bam\"}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam"} }}\n');
+    ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=INTEGER{1}} }}\n');
+    ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{true}} }}\n');
+    ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
+  });
+
+  it('parses contents followed by a mustache', function() {
+    ast_for("foo bar {{baz}}").should.equal("CONTENT[ \'foo bar \' ]\n{{ ID:baz [] }}\n");
+  });
+
+  it('parses a partial', function() {
+    ast_for("{{> foo }}").should.equal("{{> PARTIAL:foo }}\n");
+  });
+
+  it('parses a partial with context', function() {
+    ast_for("{{> foo bar}}").should.equal("{{> PARTIAL:foo ID:bar }}\n");
+  });
+
+  it('parses a partial with a complex name', function() {
+    ast_for("{{> shared/partial?.bar}}").should.equal("{{> PARTIAL:shared/partial?.bar }}\n");
+  });
+
+  it('parses a comment', function() {
+    ast_for("{{! this is a comment }}").should.equal("{{! ' this is a comment ' }}\n");
+  });
+
+  it('parses a multi-line comment', function() {
+    ast_for("{{!\nthis is a multi-line comment\n}}").should.equal("{{! \'\nthis is a multi-line comment\n\' }}\n");
+  });
+
+  it('parses an inverse section', function() {
+    ast_for("{{#foo}} bar {{^}} baz {{/foo}}").should.equal("BLOCK:\n  {{ ID:foo [] }}\n  PROGRAM:\n    CONTENT[ ' bar ' ]\n  {{^}}\n    CONTENT[ ' baz ' ]\n");
+  });
+
+  it('parses an inverse (else-style) section', function() {
+    ast_for("{{#foo}} bar {{else}} baz {{/foo}}").should.equal("BLOCK:\n  {{ ID:foo [] }}\n  PROGRAM:\n    CONTENT[ ' bar ' ]\n  {{^}}\n    CONTENT[ ' baz ' ]\n");
+  });
+
+  it('parses empty blocks', function() {
+    ast_for("{{#foo}}{{/foo}}").should.equal("BLOCK:\n  {{ ID:foo [] }}\n  PROGRAM:\n");
+  });
+
+  it('parses empty blocks with empty inverse section', function() {
+    ast_for("{{#foo}}{{^}}{{/foo}}").should.equal("BLOCK:\n  {{ ID:foo [] }}\n  PROGRAM:\n");
+  });
+
+  it('parses empty blocks with empty inverse (else-style) section', function() {
+    ast_for("{{#foo}}{{else}}{{/foo}}").should.equal("BLOCK:\n  {{ ID:foo [] }}\n  PROGRAM:\n");
+  });
+
+  it('parses non-empty blocks with empty inverse section', function() {
+    ast_for("{{#foo}} bar {{^}}{{/foo}}").should.equal("BLOCK:\n  {{ ID:foo [] }}\n  PROGRAM:\n    CONTENT[ ' bar ' ]\n  {{^}}\n");
+  });
+
+  it('parses non-empty blocks with empty inverse (else-style) section', function() {
+    ast_for("{{#foo}} bar {{else}}{{/foo}}").should.equal("BLOCK:\n  {{ ID:foo [] }}\n  PROGRAM:\n    CONTENT[ ' bar ' ]\n  {{^}}\n");
+  });
+
+  it('parses empty blocks with non-empty inverse section', function() {
+    ast_for("{{#foo}}{{^}} bar {{/foo}}").should.equal("BLOCK:\n  {{ ID:foo [] }}\n  PROGRAM:\n  {{^}}\n    CONTENT[ ' bar ' ]\n");
+  });
+
+  it('parses empty blocks with non-empty inverse (else-style) section', function() {
+    ast_for("{{#foo}}{{else}} bar {{/foo}}").should.equal("BLOCK:\n  {{ ID:foo [] }}\n  PROGRAM:\n  {{^}}\n    CONTENT[ ' bar ' ]\n");
+  });
+
+  it('parses a standalone inverse section', function() {
+    ast_for("{{^foo}}bar{{/foo}}").should.equal("BLOCK:\n  {{ ID:foo [] }}\n  {{^}}\n    CONTENT[ 'bar' ]\n");
+  });
+
+  it("raises if there's a Parse error", function() {
+    (function() {
+      ast_for("foo{{^}}bar");
+    }).should.throw(/Parse error on line 1/);
+    (function() {
+      ast_for("{{foo}");
+    }).should.throw(/Parse error on line 1/);
+    (function() {
+      ast_for("{{foo &}}");
+    }).should.throw(/Parse error on line 1/);
+    (function() {
+      ast_for("{{#goodbyes}}{{/hellos}}");
+    }).should.throw(/goodbyes doesn't match hellos/);
+  });
+
+  it('knows how to report the correct line number in errors', function() {
+    (function() {
+      ast_for("hello\nmy\n{{foo}");
+    }).should.throw(/Parse error on line 3/);
+    (function() {
+      ast_for("hello\n\nmy\n\n{{foo}");
+    }).should.throw(/Parse error on line 5/);
+  });
+
+  it('knows how to report the correct line number in errors when the first character is a newline', function() {
+    (function() {
+      ast_for("\n\nhello\n\nmy\n\n{{foo}");
+    }).should.throw(/Parse error on line 7/);
+  });
+
+  describe('externally compiled AST', function() {
+    it('can pass through an already-compiled AST', function() {
+      ast_for(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")])).should.equal("CONTENT[ \'Hello\' ]\n");
+    });
+  });
+});
diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb
deleted file mode 100644
index b25e88938..000000000
--- a/spec/parser_spec.rb
+++ /dev/null
@@ -1,426 +0,0 @@
-require "spec_helper"
-
-describe "Parser" do
-  let(:handlebars) { @context["Handlebars"] }
-
-  before(:all) do
-    @compiles = true
-  end
-
-  def root(&block)
-    ASTBuilder.build do
-      instance_eval(&block)
-    end
-  end
-
-  def ast_for(string)
-    ast = handlebars.parse(string)
-    handlebars.print(ast)
-  end
-
-  class ASTBuilder
-    def self.build(&block)
-      ret = new
-      ret.evaluate(&block)
-      ret.out
-    end
-
-    attr_reader :out
-
-    def initialize
-      @padding = 0
-      @out = ""
-    end
-
-    def evaluate(&block)
-      instance_eval(&block)
-    end
-
-    def pad(string)
-      @out << ("  " * @padding) + string + "\n"
-    end
-
-    def with_padding
-      @padding += 1
-      ret = yield
-      @padding -= 1
-      ret
-    end
-
-    def program
-      pad("PROGRAM:")
-      with_padding { yield }
-    end
-
-    def inverse
-      pad("{{^}}")
-      with_padding { yield }
-    end
-
-    def block
-      pad("BLOCK:")
-      with_padding { yield }
-    end
-
-    def inverted_block
-      pad("INVERSE:")
-      with_padding { yield }
-    end
-
-    def mustache(id, params = [], hash = nil)
-      hash = " #{hash}" if hash
-      pad("{{ #{id} [#{params.join(", ")}]#{hash} }}")
-    end
-
-    def partial(id, context = nil)
-      content = id.dup
-      content << " #{context}" if context
-      pad("{{> #{content} }}")
-    end
-
-    def comment(comment)
-      pad("{{! '#{comment}' }}")
-    end
-
-    def multiline_comment(comment)
-      pad("{{! '\n#{comment}\n' }}")
-    end
-
-    def content(string)
-      pad("CONTENT[ '#{string}' ]")
-    end
-
-    def string(string)
-      string.inspect
-    end
-
-    def integer(string)
-      "INTEGER{#{string}}"
-    end
-
-    def boolean(string)
-      "BOOLEAN{#{string}}"
-    end
-
-    def hash(*pairs)
-      "HASH{" + pairs.map {|k,v| "#{k}=#{v}" }.join(", ") + "}"
-    end
-
-    def id(id)
-      "ID:#{id}"
-    end
-
-    def data(id)
-      "@ID:#{id}"
-    end
-
-    def partial_name(name)
-      "PARTIAL:#{name}"
-    end
-
-    def path(*parts)
-      "PATH:#{parts.join("/")}"
-    end
-  end
-
-  it "parses simple mustaches" do
-    ast_for("{{foo}}").should == root { mustache id("foo") }
-    ast_for("{{foo?}}").should == root { mustache id("foo?") }
-    ast_for("{{foo_}}").should == root { mustache id("foo_") }
-    ast_for("{{foo-}}").should == root { mustache id("foo-") }
-    ast_for("{{foo:}}").should == root { mustache id("foo:") }
-  end
-
-  it "parses simple mustaches with data" do
-    ast_for("{{@foo}}").should == root { mustache data("foo") }
-  end
-
-  it "parses mustaches with paths" do
-    ast_for("{{foo/bar}}").should == root { mustache path("foo", "bar") }
-  end
-
-  it "parses mustaches with this/foo" do
-    ast_for("{{this/foo}}").should == root { mustache id("foo") }
-  end
-
-  it "parses mustaches with - in a path" do
-    ast_for("{{foo-bar}}").should == root { mustache id("foo-bar") }
-  end
-
-  it "parses mustaches with parameters" do
-    ast_for("{{foo bar}}").should == root { mustache id("foo"), [id("bar")] }
-  end
-
-  it "parses mustaches with hash arguments" do
-    ast_for("{{foo bar=baz}}").should == root do
-      mustache id("foo"), [], hash(["bar", id("baz")])
-    end
-
-    ast_for("{{foo bar=1}}").should == root do
-      mustache id("foo"), [], hash(["bar", integer("1")])
-    end
-
-    ast_for("{{foo bar=true}}").should == root do
-      mustache id("foo"), [], hash(["bar", boolean("true")])
-    end
-
-    ast_for("{{foo bar=false}}").should == root do
-      mustache id("foo"), [], hash(["bar", boolean("false")])
-    end
-
-    ast_for("{{foo bar=@baz}}").should == root do
-      mustache id("foo"), [], hash(["bar", data("baz")])
-    end
-
-    ast_for("{{foo bar=baz bat=bam}}").should == root do
-      mustache id("foo"), [], hash(["bar", "ID:baz"], ["bat", "ID:bam"])
-    end
-
-    ast_for("{{foo bar=baz bat=\"bam\"}}").should == root do
-      mustache id("foo"), [], hash(["bar", "ID:baz"], ["bat", "\"bam\""])
-    end
-
-    ast_for("{{foo bat='bam'}}").should == root do
-      mustache id("foo"), [], hash(["bat", "\"bam\""])
-    end
-
-    ast_for("{{foo omg bar=baz bat=\"bam\"}}").should == root do
-      mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")])
-    end
-
-    ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}").should == root do
-      mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", integer("1")])
-    end
-
-    ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}").should == root do
-      mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", boolean("true")])
-    end
-
-    ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}").should == root do
-      mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", boolean("false")])
-    end
-  end
-
-  it "parses mustaches with string parameters" do
-    ast_for("{{foo bar \"baz\" }}").should == root { mustache id("foo"), [id("bar"), string("baz")] }
-  end
-
-  it "parses mustaches with INTEGER parameters" do
-    ast_for("{{foo 1}}").should == root { mustache id("foo"), [integer("1")] }
-  end
-
-  it "parses mustaches with BOOLEAN parameters" do
-    ast_for("{{foo true}}").should == root { mustache id("foo"), [boolean("true")] }
-    ast_for("{{foo false}}").should == root { mustache id("foo"), [boolean("false")] }
-  end
-
-  it "parses mutaches with DATA parameters" do
-    ast_for("{{foo @bar}}").should == root { mustache id("foo"), [data("bar")] }
-  end
-
-  it "parses contents followed by a mustache" do
-    ast_for("foo bar {{baz}}").should == root do
-      content "foo bar "
-      mustache id("baz")
-    end
-  end
-
-  it "parses a partial" do
-    ast_for("{{> foo }}").should == root { partial partial_name("foo") }
-  end
-
-  it "parses a partial with context" do
-    ast_for("{{> foo bar}}").should == root { partial partial_name("foo"), id("bar") }
-  end
-
-  it "parses a partial with a complex name" do
-    ast_for("{{> shared/partial?.bar}}").should == root { partial partial_name("shared/partial?.bar") }
-  end
-
-  it "parses a comment" do
-    ast_for("{{! this is a comment }}").should == root do
-      comment " this is a comment "
-    end
-  end
-
-  it "parses a multi-line comment" do
-    ast_for("{{!\nthis is a multi-line comment\n}}").should == root do
-      multiline_comment "this is a multi-line comment"
-    end
-  end
-
-  it "parses an inverse section" do
-    ast_for("{{#foo}} bar {{^}} baz {{/foo}}").should == root do
-      block do
-        mustache id("foo")
-
-        program do
-          content " bar "
-        end
-
-        inverse do
-          content " baz "
-        end
-      end
-    end
-  end
-
-  it "parses an inverse ('else'-style) section" do
-    ast_for("{{#foo}} bar {{else}} baz {{/foo}}").should == root do
-      block do
-        mustache id("foo")
-
-        program do
-          content " bar "
-        end
-
-        inverse do
-          content " baz "
-        end
-      end
-    end
-  end
-
-  it "parses empty blocks" do
-    ast_for("{{#foo}}{{/foo}}").should == root do
-      block do
-        mustache id("foo")
-
-        program do
-          #  empty program
-        end
-      end
-    end
-  end
-
-  it "parses empty blocks with empty inverse section" do
-    ast_for("{{#foo}}{{^}}{{/foo}}").should == root do
-      block do
-        mustache id("foo")
-
-        program do
-          #  empty program
-        end
-
-        inverse do
-          #  empty inverse
-        end
-      end
-    end
-  end
-
-  it "parses empty blocks with empty inverse ('else'-style) section" do
-    ast_for("{{#foo}}{{else}}{{/foo}}").should == root do
-      block do
-        mustache id("foo")
-
-        program do
-          #  empty program
-        end
-
-        inverse do
-          #  empty inverse
-        end
-      end
-    end
-  end
-
-  it "parses non-empty blocks with empty inverse section" do
-    ast_for("{{#foo}} bar {{^}}{{/foo}}").should == root do
-      block do
-        mustache id("foo")
-
-        program do
-          content " bar "
-        end
-
-        inverse do
-          #  empty inverse
-        end
-      end
-    end
-  end
-
-  it "parses non-empty blocks with empty inverse ('else'-style) section" do
-    ast_for("{{#foo}} bar {{else}}{{/foo}}").should == root do
-      block do
-        mustache id("foo")
-
-        program do
-          content " bar "
-        end
-
-        inverse do
-          #  empty inverse
-        end
-      end
-    end
-  end
-
-  it "parses empty blocks with non-empty inverse section" do
-    ast_for("{{#foo}}{{^}} bar {{/foo}}").should == root do
-      block do
-        mustache id("foo")
-
-        program do
-          #  empty program
-        end
-
-        inverse do
-          content " bar "
-        end
-      end
-    end
-  end
-
-  it "parses empty blocks with non-empty inverse ('else'-style) section" do
-    ast_for("{{#foo}}{{else}} bar {{/foo}}").should == root do
-      block do
-        mustache id("foo")
-
-        program do
-          #  empty program
-        end
-
-        inverse do
-          content " bar "
-        end
-      end
-    end
-  end
-
-  it "parses a standalone inverse section" do
-    ast_for("{{^foo}}bar{{/foo}}").should == root do
-      block do
-        mustache id("foo")
-
-        inverse do
-          content "bar"
-        end
-      end
-    end
-  end
-
-  it "raises if there's a Parse error" do
-    lambda { ast_for("{{foo}") }.should   raise_error(V8::JSError, /Parse error on line 1/)
-    lambda { ast_for("{{foo &}}")}.should raise_error(V8::JSError, /Parse error on line 1/)
-    lambda { ast_for("{{#goodbyes}}{{/hellos}}") }.should raise_error(V8::JSError, /goodbyes doesn't match hellos/)
-  end
-
-  it "knows how to report the correct line number in errors" do
-    lambda { ast_for("hello\nmy\n{{foo}") }.should     raise_error(V8::JSError, /Parse error on line 3/m)
-    lambda { ast_for("hello\n\nmy\n\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 5/m)
-  end
-
-  it "knows how to report the correct line number in errors when the first character is a newline" do
-    lambda { ast_for("\n\nhello\n\nmy\n\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 7/m)
-  end
-
-  context "externally compiled AST" do
-    it "can pass through an already-compiled AST" do
-      ast_for(@context.eval('new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]);')).should == root do
-        content "Hello"
-      end
-    end
-  end
-end
diff --git a/spec/partials.js b/spec/partials.js
new file mode 100644
index 000000000..7ff9d3c25
--- /dev/null
+++ b/spec/partials.js
@@ -0,0 +1,126 @@
+/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */
+describe('partials', function() {
+  it("basic partials", function() {
+    var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
+    var partial = "{{name}} ({{url}}) ";
+    var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
+    shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
+                    "Basic partials output based on current context.");
+  });
+
+  it("partials with context", function() {
+    var string = "Dudes: {{>dude dudes}}";
+    var partial = "{{#this}}{{name}} ({{url}}) {{/this}}";
+    var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
+    shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
+                    "Partials can be passed a context");
+  });
+
+  it("partials with undefined context", function() {
+    var string = "Dudes: {{>dude dudes}}";
+    var partial = "{{foo}} Empty";
+    var hash = {};
+    shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes:  Empty");
+  });
+
+  it("partial in a partial", function() {
+    var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}";
+    var dude = "{{name}} {{> url}} ";
+    var url = "<a href='{{url}}'>{{url}}</a>";
+    var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
+    shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> ", "Partials are rendered inside of other partials");
+  });
+
+  it("rendering undefined partial throws an exception", function() {
+    (function() {
+        var template = CompilerContext.compile("{{> whatever}}");
+        template();
+    }).should.throw(Handlebars.Exception, 'The partial whatever could not be found');
+  });
+
+  it("rendering template partial in vm mode throws an exception", function() {
+    (function() {
+      var template = CompilerContext.compile("{{> whatever}}");
+      template();
+    }).should.throw(Handlebars.Exception, 'The partial whatever could not be found');
+  });
+
+  it("rendering function partial in vm mode", function() {
+    var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
+    var partial = function(context) {
+      return context.name + ' (' + context.url + ') ';
+    };
+    var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
+    shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
+                    "Function partials output based in VM.");
+  });
+
+  it("GH-14: a partial preceding a selector", function() {
+     var string = "Dudes: {{>dude}} {{another_dude}}";
+     var dude = "{{name}}";
+     var hash = {name:"Jeepers", another_dude:"Creepers"};
+     shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial");
+  });
+
+  it("Partials with slash paths", function() {
+    var string = "Dudes: {{> shared/dude}}";
+    var dude = "{{name}}";
+    var hash = {name:"Jeepers", another_dude:"Creepers"};
+    shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
+  });
+
+  it("Partials with slash and point paths", function() {
+    var string = "Dudes: {{> shared/dude.thing}}";
+    var dude = "{{name}}";
+    var hash = {name:"Jeepers", another_dude:"Creepers"};
+    shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing':dude}], true, "Dudes: Jeepers", "Partials can use literal with points in paths");
+  });
+
+  it("Global Partials", function() {
+    handlebarsEnv.registerPartial('global_test', '{{another_dude}}');
+
+    var string = "Dudes: {{> shared/dude}} {{> global_test}}";
+    var dude = "{{name}}";
+    var hash = {name:"Jeepers", another_dude:"Creepers"};
+    shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
+  });
+
+  it("Multiple partial registration", function() {
+    handlebarsEnv.registerPartial({
+      'shared/dude': '{{name}}',
+      global_test: '{{another_dude}}'
+    });
+
+    var string = "Dudes: {{> shared/dude}} {{> global_test}}";
+    var hash = {name:"Jeepers", another_dude:"Creepers"};
+    shouldCompileToWithPartials(string, [hash], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
+  });
+
+  it("Partials with integer path", function() {
+    var string = "Dudes: {{> 404}}";
+    var dude = "{{name}}";
+    var hash = {name:"Jeepers", another_dude:"Creepers"};
+    shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
+  });
+
+  it("Partials with complex path", function() {
+    var string = "Dudes: {{> 404/asdf?.bar}}";
+    var dude = "{{name}}";
+    var hash = {name:"Jeepers", another_dude:"Creepers"};
+    shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
+  });
+
+  it("Partials with escaped", function() {
+    var string = "Dudes: {{> [+404/asdf?.bar]}}";
+    var dude = "{{name}}";
+    var hash = {name:"Jeepers", another_dude:"Creepers"};
+    shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
+  });
+
+  it("Partials with string", function() {
+    var string = "Dudes: {{> \"+404/asdf?.bar\"}}";
+    var dude = "{{name}}";
+    var hash = {name:"Jeepers", another_dude:"Creepers"};
+    shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
+  });
+});
diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js
deleted file mode 100644
index 5d52e444b..000000000
--- a/spec/qunit_spec.js
+++ /dev/null
@@ -1,1657 +0,0 @@
-var Handlebars;
-if (!Handlebars) {
-  // Setup for Node package testing
-  Handlebars = require('../lib/handlebars');
-
-  var assert = require("assert"),
-
-      equal = assert.equal,
-      equals = assert.equal,
-      ok = assert.ok;
-
-  // Note that this doesn't have the same context separation as the rspec test.
-  // Both should be run for full acceptance of the two libary modes.
-  var CompilerContext = {
-    compile: function(template, options) {
-      var templateSpec = Handlebars.precompile(template, options);
-      return Handlebars.template(eval('(' + templateSpec + ')'));
-    },
-    compileWithPartial: function(template, options) {
-      return Handlebars.compile(template, options);
-    }
-  };
-} else {
-  var _equal = equal;
-  equals = equal = function(a, b, msg) {
-    // Allow exec with missing message params
-    _equal(a, b, msg || '');
-  };
-}
-
-suite("basic context");
-
-function shouldCompileTo(string, hashOrArray, expected, message) {
-  shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
-}
-
-function shouldCompileToWithPartials(string, hashOrArray, partials, expected, message) {
-  var result = compileWithPartials(string, hashOrArray, partials);
-  equal(result, expected, "'" + expected + "' should === '" + result + "': " + message);
-}
-
-function compileWithPartials(string, hashOrArray, partials) {
-  var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary;
-  if(Object.prototype.toString.call(hashOrArray) === "[object Array]") {
-    ary = [];
-    ary.push(hashOrArray[0]);
-    ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
-  } else {
-    ary = [hashOrArray];
-  }
-
-  return template.apply(this, ary);
-}
-
-function shouldThrow(fn, exception, message) {
-  var caught = false,
-      exType, exMessage;
-
-  if (exception instanceof Array) {
-    exType = exception[0];
-    exMessage = exception[1];
-  } else if (typeof exception === 'string') {
-    exType = Error;
-    exMessage = exception;
-  } else {
-    exType = exception;
-  }
-
-  try {
-    fn();
-  }
-  catch (e) {
-    if (e instanceof exType) {
-      if (!exMessage || e.message === exMessage) {
-        caught = true;
-      }
-    }
-  }
-
-  ok(caught, message || null);
-}
-
-test("most basic", function() {
-  shouldCompileTo("{{foo}}", { foo: "foo" }, "foo");
-});
-
-test("escaping", function() {
-  shouldCompileTo("\\{{foo}}", { foo: "food" }, "{{foo}}");
-  shouldCompileTo("\\\\{{foo}}", { foo: "food" }, "\\food");
-  shouldCompileTo("\\\\ {{foo}}", { foo: "food" }, "\\\\ food");
-});
-
-test("compiling with a basic context", function() {
-  shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
-                  "It works if all the required keys are provided");
-});
-
-test("comments", function() {
-  shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
-    {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
-    "comments are ignored");
-});
-
-test("boolean", function() {
-  var string   = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!";
-  shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
-                  "booleans show the contents when true");
-
-  shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
-                  "booleans do not show the contents when false");
-});
-
-test("zeros", function() {
-  shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0},
-      "num1: 42, num2: 0");
-	shouldCompileTo("num: {{.}}", 0, "num: 0");
-	shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0");
-});
-
-test("newlines", function() {
-    shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest");
-    shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest");
-});
-
-test("escaping text", function() {
-  shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes");
-  shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored");
-  shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes");
-  shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes");
-  shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript");
-});
-
-test("escaping expressions", function() {
- shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>',
-        "expressions with 3 handlebars aren't escaped");
-
- shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
-        "expressions with {{& handlebars aren't escaped");
-
- shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;',
-        "by default expressions should be escaped");
-
- shouldCompileTo("{{awesome}}", {awesome: "Escaped, <b> looks like: &lt;b&gt;"}, 'Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;gt;',
-        "escaping should properly handle amperstands");
-});
-
-test("functions returning safestrings shouldn't be escaped", function() {
-  var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }};
-  shouldCompileTo("{{awesome}}", hash, '&\"\\<>',
-      "functions returning safestrings aren't escaped");
-});
-
-test("functions", function() {
-  shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome",
-                  "functions are called and render their output");
-  shouldCompileTo("{{awesome}}", {awesome: function() { return this.more; }, more:  "More awesome"}, "More awesome",
-                  "functions are bound to the context");
-});
-
-test("functions with context argument", function() {
-  shouldCompileTo("{{awesome frank}}",
-      {awesome: function(context) { return context; },
-        frank: "Frank"},
-      "Frank", "functions are called with context arguments");
-});
-
-
-test("paths with hyphens", function() {
-  shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)");
-  shouldCompileTo("{{foo.foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)");
-  shouldCompileTo("{{foo/foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)");
-});
-
-test("nested paths", function() {
-  shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}},
-                  "Goodbye beautiful world!", "Nested paths access nested objects");
-});
-
-test("nested paths with empty string value", function() {
-  shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}},
-                  "Goodbye  world!", "Nested paths access nested objects with empty string");
-});
-
-test("literal paths", function() {
-  shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}},
-      "Goodbye beautiful world!", "Literal paths can be used");
-  shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}},
-      "Goodbye beautiful world!", "Literal paths can be used");
-});
-
-test('literal references', function() {
-  shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"},
-      "Goodbye beautiful world!", "Literal paths can be used");
-});
-
-test("that current context path ({{.}}) doesn't hit helpers", function() {
-	shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: ");
-});
-
-test("complex but empty paths", function() {
-  shouldCompileTo("{{person/name}}", {person: {name: null}}, "");
-  shouldCompileTo("{{person/name}}", {person: {}}, "");
-});
-
-test("this keyword in paths", function() {
-  var string = "{{#goodbyes}}{{this}}{{/goodbyes}}";
-  var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
-  shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE",
-    "This keyword in paths evaluates to current context");
-
-  string = "{{#hellos}}{{this/text}}{{/hellos}}";
-  hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
-  shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths");
-});
-
-test("this keyword nested inside path", function() {
-  var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}";
-  shouldThrow(function() {
-      CompilerContext.compile(string);
-    }, Error, "Should throw exception");
-});
-
-test("this keyword in helpers", function() {
-  var helpers = {foo: function(value) {
-      return 'bar ' + value;
-  }};
-  var string = "{{#goodbyes}}{{foo this}}{{/goodbyes}}";
-  var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
-  shouldCompileTo(string, [hash, helpers], "bar goodbyebar Goodbyebar GOODBYE",
-    "This keyword in paths evaluates to current context");
-
-  string = "{{#hellos}}{{foo this/text}}{{/hellos}}";
-  hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
-  shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths");
-});
-
-test("this keyword nested inside helpers param", function() {
-  var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}";
-  shouldThrow(function() {
-      CompilerContext.compile(string);
-    }, Error, "Should throw exception");
-});
-
-suite("inverted sections");
-
-test("inverted sections with unset value", function() {
-  var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
-  var hash = {};
-  shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set.");
-});
-
-test("inverted section with false value", function() {
-  var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
-  var hash = {goodbyes: false};
-  shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false.");
-});
-
-test("inverted section with empty set", function() {
-  var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
-  var hash = {goodbyes: []};
-  shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set.");
-});
-
-suite("blocks");
-
-test("array", function() {
-  var string   = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
-  var hash     = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
-  shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
-                  "Arrays iterate over the contents when not empty");
-
-  shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
-                  "Arrays ignore the contents when empty");
-
-});
-
-test("array with @index", function() {
-  var string = "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!";
-  var hash   = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
-
-  var template = CompilerContext.compile(string);
-  var result = template(hash);
-
-  equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
-});
-
-test("empty block", function() {
-  var string   = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!";
-  var hash     = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
-  shouldCompileTo(string, hash, "cruel world!",
-                  "Arrays iterate over the contents when not empty");
-
-  shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
-                  "Arrays ignore the contents when empty");
-});
-
-test("nested iteration", function() {
-
-});
-
-test("block with complex lookup", function() {
-  var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}";
-  var hash     = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]};
-
-  shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
-                  "Templates can access variables in contexts up the stack with relative path syntax");
-});
-
-test("block with complex lookup using nested context", function() {
-  var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}";
-
-  shouldThrow(function() {
-      CompilerContext.compile(string);
-    }, Error, "Should throw exception");
-});
-
-test("helper with complex lookup$", function() {
-  var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}";
-  var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
-  var helpers = {link: function(prefix) {
-    return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>";
-  }};
-  shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>");
-});
-
-test("helper block with complex lookup expression", function() {
-  var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}";
-  var hash = {name: "Alan"};
-  var helpers = {goodbyes: function(options) {
-		var out = "";
-		var byes = ["Goodbye", "goodbye", "GOODBYE"];
-		for (var i = 0,j = byes.length; i < j; i++) {
-			out += byes[i] + " " + options.fn(this) + "! ";
-		}
-    return out;
-  }};
-  shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
-});
-
-test("helper with complex lookup and nested template", function() {
-  var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
-  var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
-  var helpers = {link: function (prefix, options) {
-      return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>";
-  }};
-  shouldCompileToWithPartials(string, [hash, helpers], false, "<a href='/root/goodbye'>Goodbye</a>");
-});
-
-test("helper with complex lookup and nested template in VM+Compiler", function() {
-  var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
-  var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
-  var helpers = {link: function (prefix, options) {
-      return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>";
-  }};
-  shouldCompileToWithPartials(string, [hash, helpers], true, "<a href='/root/goodbye'>Goodbye</a>");
-});
-
-test("block with deep nested complex lookup", function() {
-  var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}";
-  var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
-
-  shouldCompileTo(string, hash, "Goodbye cruel OMG!");
-});
-
-test("block helper", function() {
-  var string   = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
-  var template = CompilerContext.compile(string);
-
-  var result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}});
-  equal(result, "GOODBYE! cruel world!", "Block helper executed");
-});
-
-test("block helper staying in the same context", function() {
-  var string   = "{{#form}}<p>{{name}}</p>{{/form}}";
-  var template = CompilerContext.compile(string);
-
-  var result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "<form>" + options.fn(this) + "</form>"; } }});
-  equal(result, "<form><p>Yehuda</p></form>", "Block helper executed with current context");
-});
-
-test("block helper should have context in this", function() {
-  var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>";
-  var link = function(options) {
-    return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
-  };
-  var data = { "people": [
-    { "name": "Alan", "id": 1 },
-    { "name": "Yehuda", "id": 2 }
-  ]};
-
-  shouldCompileTo(source, [data, {link: link}], "<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>");
-});
-
-test("block helper for undefined value", function() {
-	shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, "");
-});
-
-test("block helper passing a new context", function() {
-  var string   = "{{#form yehuda}}<p>{{name}}</p>{{/form}}";
-  var template = CompilerContext.compile(string);
-
-  var result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}});
-  equal(result, "<form><p>Yehuda</p></form>", "Context variable resolved");
-});
-
-test("block helper passing a complex path context", function() {
-  var string   = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}";
-  var template = CompilerContext.compile(string);
-
-  var result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}});
-  equal(result, "<form><p>Harold</p></form>", "Complex path variable resolved");
-});
-
-test("nested block helpers", function() {
-  var string   = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}";
-  var template = CompilerContext.compile(string);
-
-  var result = template({
-    yehuda: {name: "Yehuda" }
-  }, {
-    helpers: {
-      link: function(options) { return "<a href='" + this.name + "'>" + options.fn(this) + "</a>"; },
-      form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }
-    }
-  });
-  equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>", "Both blocks executed");
-});
-
-test("block inverted sections", function() {
-  shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"},
-    "No people");
-});
-
-test("block inverted sections with empty arrays", function() {
-  shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []},
-    "No people");
-});
-
-test("block helper inverted sections", function() {
-  var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}";
-  var list = function(context, options) {
-    if (context.length > 0) {
-      var out = "<ul>";
-      for(var i = 0,j=context.length; i < j; i++) {
-        out += "<li>";
-        out += options.fn(context[i]);
-        out += "</li>";
-      }
-      out += "</ul>";
-      return out;
-    } else {
-      return "<p>" + options.inverse(this) + "</p>";
-    }
-  };
-
-  var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]};
-  var empty = {people: []};
-  var rootMessage = {
-    people: [],
-    message: "Nobody's here"
-  };
-
-  var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}";
-
-  // the meaning here may be kind of hard to catch, but list.not is always called,
-  // so we should see the output of both
-  shouldCompileTo(string, [hash, { list: list }], "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context");
-  shouldCompileTo(string, [empty, { list: list }], "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called");
-  shouldCompileTo(messageString, [rootMessage, { list: list }], "<p>Nobody&#x27;s here</p>", "the context of an inverse is the parent of the block");
-});
-
-suite("helpers hash");
-
-test("providing a helpers hash", function() {
-  shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!",
-                  "helpers hash is available");
-
-  shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}],
-                  "Goodbye cruel world!", "helpers hash is available inside other blocks");
-});
-
-test("in cases of conflict, helpers win", function() {
-  shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
-                  "helpers hash has precedence escaped expansion");
-  shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
-                  "helpers hash has precedence simple expansion");
-});
-
-test("the helpers hash is available is nested contexts", function() {
-  shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}",
-                [{'outer': {'inner': {'unused':[]}}},  {'helper': function() { return 'helper'; }}], "helper",
-                "helpers hash is available in nested contexts.");
-});
-
-test("the helper hash should augment the global hash", function() {
-  Handlebars.registerHelper('test_helper', function() { return 'found it!'; });
-
-  shouldCompileTo(
-    "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", [
-      {cruel: "cruel"},
-      {world: function() { return "world!"; }}
-    ],
-    "found it! Goodbye cruel world!!");
-});
-
-test("Multiple global helper registration", function() {
-  var helpers = Handlebars.helpers;
-  try {
-    Handlebars.helpers = {};
-    Handlebars.registerHelper({
-      'if': helpers['if'],
-      world: function() { return "world!"; },
-      test_helper: function() { return 'found it!'; }
-    });
-
-    shouldCompileTo(
-      "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}",
-      [{cruel: "cruel"}],
-      "found it! Goodbye cruel world!!");
-  } finally {
-    if (helpers) {
-      Handlebars.helpers = helpers;
-    }
-  }
-});
-
-suite("partials");
-
-test("basic partials", function() {
-  var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
-  var partial = "{{name}} ({{url}}) ";
-  var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
-  shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
-                  "Basic partials output based on current context.");
-});
-
-test("partials with context", function() {
-  var string = "Dudes: {{>dude dudes}}";
-  var partial = "{{#this}}{{name}} ({{url}}) {{/this}}";
-  var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
-  shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
-                  "Partials can be passed a context");
-});
-
-test("partial in a partial", function() {
-  var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}";
-  var dude = "{{name}} {{> url}} ";
-  var url = "<a href='{{url}}'>{{url}}</a>";
-  var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
-  shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> ", "Partials are rendered inside of other partials");
-});
-
-test("rendering undefined partial throws an exception", function() {
-  shouldThrow(function() {
-      var template = CompilerContext.compile("{{> whatever}}");
-      template();
-    }, [Handlebars.Exception, 'The partial whatever could not be found'], "Should throw exception");
-});
-
-test("rendering template partial in vm mode throws an exception", function() {
-  shouldThrow(function() {
-      var template = CompilerContext.compile("{{> whatever}}");
-      template();
-    }, [Handlebars.Exception, 'The partial whatever could not be found'], "Should throw exception");
-});
-
-test("rendering function partial in vm mode", function() {
-  var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
-  var partial = function(context) {
-    return context.name + ' (' + context.url + ') ';
-  };
-  var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
-  shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
-                  "Function partials output based in VM.");
-});
-
-test("GH-14: a partial preceding a selector", function() {
-   var string = "Dudes: {{>dude}} {{another_dude}}";
-   var dude = "{{name}}";
-   var hash = {name:"Jeepers", another_dude:"Creepers"};
-   shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial");
-});
-
-test("Partials with slash paths", function() {
-	var string = "Dudes: {{> shared/dude}}";
-	var dude = "{{name}}";
-	var hash = {name:"Jeepers", another_dude:"Creepers"};
-  shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
-});
-
-test("Partials with slash and point paths", function() {
-  var string = "Dudes: {{> shared/dude.thing}}";
-  var dude = "{{name}}";
-  var hash = {name:"Jeepers", another_dude:"Creepers"};
-  shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing':dude}], true, "Dudes: Jeepers", "Partials can use literal with points in paths");
-});
-
-test("Global Partials", function() {
-  Handlebars.registerPartial('global_test', '{{another_dude}}');
-
-  var string = "Dudes: {{> shared/dude}} {{> global_test}}";
-  var dude = "{{name}}";
-  var hash = {name:"Jeepers", another_dude:"Creepers"};
-  shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
-});
-
-test("Multiple partial registration", function() {
-  Handlebars.registerPartial({
-    'shared/dude': '{{name}}',
-    global_test: '{{another_dude}}'
-  });
-
-  var string = "Dudes: {{> shared/dude}} {{> global_test}}";
-  var hash = {name:"Jeepers", another_dude:"Creepers"};
-  shouldCompileToWithPartials(string, [hash], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
-});
-
-test("Partials with integer path", function() {
-	var string = "Dudes: {{> 404}}";
-	var dude = "{{name}}";
-	var hash = {name:"Jeepers", another_dude:"Creepers"};
-  shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
-});
-
-test("Partials with complex path", function() {
-  var string = "Dudes: {{> 404/asdf?.bar}}";
-  var dude = "{{name}}";
-  var hash = {name:"Jeepers", another_dude:"Creepers"};
-  shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
-});
-
-test("Partials with escaped", function() {
-  var string = "Dudes: {{> [+404/asdf?.bar]}}";
-  var dude = "{{name}}";
-  var hash = {name:"Jeepers", another_dude:"Creepers"};
-  shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
-});
-
-test("Partials with string", function() {
-  var string = "Dudes: {{> \"+404/asdf?.bar\"}}";
-  var dude = "{{name}}";
-  var hash = {name:"Jeepers", another_dude:"Creepers"};
-  shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
-});
-
-suite("String literal parameters");
-
-test("simple literals work", function() {
-  var string   = 'Message: {{hello "world" 12 true false}}';
-  var hash     = {};
-  var helpers  = {hello: function(param, times, bool1, bool2) {
-    if(typeof times !== 'number') { times = "NaN"; }
-    if(typeof bool1 !== 'boolean') { bool1 = "NaB"; }
-    if(typeof bool2 !== 'boolean') { bool2 = "NaB"; }
-    return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2;
-  }};
-  shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal");
-});
-test("negative number literals work", function() {
-  var string   = 'Message: {{hello -12}}';
-  var hash     = {};
-  var helpers  = {hello: function(times) {
-    if(typeof times !== 'number') { times = "NaN"; }
-    return "Hello " + times + " times";
-  }};
-  shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal");
-});
-
-
-test("using a quote in the middle of a parameter raises an error", function() {
-  shouldThrow(function() {
-    var string   = 'Message: {{hello wo"rld"}}';
-    CompilerContext.compile(string);
-  }, Error, "should throw exception");
-});
-
-test("escaping a String is possible", function(){
-  var string   = 'Message: {{{hello "\\"world\\""}}}';
-  var hash     = {};
-  var helpers = {hello: function(param) { return "Hello " + param; }};
-  shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal");
-});
-
-test("it works with ' marks", function() {
-  var string   = 'Message: {{{hello "Alan\'s world"}}}';
-  var hash     = {};
-  var helpers = {hello: function(param) { return "Hello " + param; }};
-  shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark");
-});
-
-suite("multiple parameters");
-
-test("simple multi-params work", function() {
-  var string   = 'Message: {{goodbye cruel world}}';
-  var hash     = {cruel: "cruel", world: "world"};
-  var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }};
-  shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params");
-});
-
-test("block multi-params work", function() {
-  var string   = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
-  var hash     = {cruel: "cruel", world: "world"};
-  var helpers = {goodbye: function(cruel, world, options) {
-    return options.fn({greeting: "Goodbye", adj: cruel, noun: world});
-  }};
-  shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params");
-});
-
-suite("safestring");
-
-test("constructing a safestring from a string and checking its type", function() {
-  var safe = new Handlebars.SafeString("testing 1, 2, 3");
-  ok(safe instanceof Handlebars.SafeString, "SafeString is an instance of Handlebars.SafeString");
-  equal(safe, "testing 1, 2, 3", "SafeString is equivalent to its underlying string");
-});
-
-test("it should not escape SafeString properties", function() {
-  var name = new Handlebars.SafeString("<em>Sean O&#x27;Malley</em>");
-
-  shouldCompileTo('{{name}}', [{ name: name }], "<em>Sean O&#x27;Malley</em>");
-});
-
-suite("helperMissing");
-
-test("if a context is not found, helperMissing is used", function() {
-  shouldThrow(function() {
-      var template = CompilerContext.compile("{{hello}} {{link_to world}}");
-      template({});
-    }, [Error, "Missing helper: 'link_to'"], "Should throw exception");
-});
-
-test("if a context is not found, custom helperMissing is used", function() {
-  var string = "{{hello}} {{link_to world}}";
-  var context = { hello: "Hello", world: "world" };
-
-  var helpers = {
-    helperMissing: function(helper, context) {
-      if(helper === "link_to") {
-        return new Handlebars.SafeString("<a>" + context + "</a>");
-      }
-    }
-  };
-
-  shouldCompileTo(string, [context, helpers], "Hello <a>world</a>");
-});
-
-suite("knownHelpers");
-
-test("Known helper should render helper", function() {
-  var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}});
-
-  var result = template({}, {helpers: {hello: function() { return "foo"; }}});
-  equal(result, "foo", "'foo' should === '" + result);
-});
-
-test("Unknown helper in knownHelpers only mode should be passed as undefined", function() {
-  var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true});
-
-  var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}});
-  equal(result, "undefined", "'undefined' should === '" + result);
-});
-test("Builtin helpers available in knownHelpers only mode", function() {
-  var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true});
-
-  var result = template({});
-  equal(result, "bar", "'bar' should === '" + result);
-});
-test("Field lookup works in knownHelpers only mode", function() {
-  var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true});
-
-  var result = template({foo: 'bar'});
-  equal(result, "bar", "'bar' should === '" + result);
-});
-test("Conditional blocks work in knownHelpers only mode", function() {
-  var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true});
-
-  var result = template({foo: 'baz'});
-  equal(result, "bar", "'bar' should === '" + result);
-});
-test("Invert blocks work in knownHelpers only mode", function() {
-  var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true});
-
-  var result = template({foo: false});
-  equal(result, "bar", "'bar' should === '" + result);
-});
-test("Functions are bound to the context in knownHelpers only mode", function() {
-  var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true});
-  var result = template({foo: function() { return this.bar; }, bar: 'bar'});
-  equal(result, "bar", "'bar' should === '" + result);
-});
-test("Unknown helper call in knownHelpers only mode should throw", function() {
-  shouldThrow(function() {
-    CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true});
-  }, Error, 'specified knownHelpersOnly');
-});
-
-suite("blockHelperMissing");
-
-test("lambdas are resolved by blockHelperMissing, not handlebars proper", function() {
-  var string = "{{#truthy}}yep{{/truthy}}";
-  var data = { truthy: function() { return true; } };
-  shouldCompileTo(string, data, "yep");
-});
-test("lambdas resolved by blockHelperMissing are bound to the context", function() {
-  var string = "{{#truthy}}yep{{/truthy}}";
-  var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } };
-  shouldCompileTo(string, boundData, "");
-});
-
-var teardown;
-suite("built-in helpers", {
-  setup: function(){ teardown = null; },
-  teardown: function(){ if (teardown) { teardown(); } }
-});
-
-test("with", function() {
-  var string = "{{#with person}}{{first}} {{last}}{{/with}}";
-  shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson");
-});
-test("with with function argument", function() {
-  var string = "{{#with person}}{{first}} {{last}}{{/with}}";
-  shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson");
-});
-
-test("if", function() {
-  var string   = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
-  shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
-                  "if with boolean argument shows the contents when true");
-  shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!",
-                  "if with string argument shows the contents");
-  shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
-                  "if with boolean argument does not show the contents when false");
-  shouldCompileTo(string, {world: "world"}, "cruel world!",
-                  "if with undefined does not show the contents");
-  shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!",
-                  "if with non-empty array shows the contents");
-  shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!",
-                  "if with empty array does not show the contents");
-});
-
-test("if with function argument", function() {
-  var string   = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
-  shouldCompileTo(string, {goodbye: function() {return true;}, world: "world"}, "GOODBYE cruel world!",
-                  "if with function shows the contents when function returns true");
-  shouldCompileTo(string, {goodbye: function() {return this.world;}, world: "world"}, "GOODBYE cruel world!",
-                  "if with function shows the contents when function returns string");
-  shouldCompileTo(string, {goodbye: function() {return false;}, world: "world"}, "cruel world!",
-                  "if with function does not show the contents when returns false");
-  shouldCompileTo(string, {goodbye: function() {return this.foo;}, world: "world"}, "cruel world!",
-                  "if with function does not show the contents when returns undefined");
-});
-
-test("each", function() {
-  var string   = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!";
-  var hash     = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
-  shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
-                  "each with array argument iterates over the contents when not empty");
-  shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
-                  "each with array argument ignores the contents when empty");
-});
-
-test("each with an object and @key", function() {
-  var string   = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!";
-  var hash     = {goodbyes: {"<b>#1</b>": {text: "goodbye"}, 2: {text: "GOODBYE"}}, world: "world"};
-
-  // Object property iteration order is undefined according to ECMA spec,
-  // so we need to check both possible orders
-  // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop
-  var actual = compileWithPartials(string, hash);
-  var expected1 = "&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!";
-  var expected2 = "2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!";
-
-  ok(actual === expected1 || actual === expected2, "each with object argument iterates over the contents when not empty");
-  shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
-                  "each with object argument ignores the contents when empty");
-});
-
-test("each with @index", function() {
-  var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!";
-  var hash   = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
-
-  var template = CompilerContext.compile(string);
-  var result = template(hash);
-
-  equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
-});
-
-test("each with function argument", function() {
-  var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!";
-  var hash   = {goodbyes: function () { return [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];}, world: "world"};
-  shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
-            "each with array function argument iterates over the contents when not empty");
-  shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
-            "each with array function argument ignores the contents when empty");
-});
-
-test("data passed to helpers", function() {
-  var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}";
-  var hash = {letters: ['a', 'b', 'c']};
-
-  var template = CompilerContext.compile(string);
-  var result = template(hash, {
-    data: {
-      exclaim: '!'
-    }
-  });
-  equal(result, 'a!b!c!', 'should output data');
-});
-
-Handlebars.registerHelper('detectDataInsideEach', function(options) {
-  return options.data && options.data.exclaim;
-});
-
-test("log", function() {
-  var string = "{{log blah}}";
-  var hash   = { blah: "whee" };
-
-  var levelArg, logArg;
-  var originalLog = Handlebars.log;
-  Handlebars.log = function(level, arg){ levelArg = level, logArg = arg; };
-  teardown = function(){ Handlebars.log = originalLog; };
-
-  shouldCompileTo(string, hash, "", "log should not display");
-  equals(1, levelArg, "should call log with 1");
-  equals("whee", logArg, "should call log with 'whee'");
-});
-
-test("overriding property lookup", function() {
-
-});
-
-
-test("passing in data to a compiled function that expects data - works with helpers", function() {
-  var template = CompilerContext.compile("{{hello}}", {data: true});
-
-  var helpers = {
-    hello: function(options) {
-      return options.data.adjective + " "  + this.noun;
-    }
-  };
-
-  var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}});
-  equals("happy cat", result, "Data output by helper");
-});
-
-test("data can be looked up via @foo", function() {
-  var template = CompilerContext.compile("{{@hello}}");
-  var result = template({}, { data: { hello: "hello" } });
-  equals("hello", result, "@foo retrieves template data");
-});
-
-var objectCreate = Handlebars.createFrame;
-
-test("deep @foo triggers automatic top-level data", function() {
-  var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}');
-
-  var helpers = objectCreate(Handlebars.helpers);
-
-  helpers.let = function(options) {
-    var frame = Handlebars.createFrame(options.data);
-
-    for (var prop in options.hash) {
-      frame[prop] = options.hash[prop];
-    }
-    return options.fn(this, { data: frame });
-  };
-
-  var result = template({ foo: true }, { helpers: helpers });
-  equals("Hello world", result, "Automatic data was triggered");
-});
-
-test("parameter data can be looked up via @foo", function() {
-  var template = CompilerContext.compile("{{hello @world}}");
-  var helpers = {
-    hello: function(noun) {
-      return "Hello " + noun;
-    }
-  };
-
-  var result = template({}, { helpers: helpers, data: { world: "world" } });
-  equals("Hello world", result, "@foo as a parameter retrieves template data");
-});
-
-test("hash values can be looked up via @foo", function() {
-  var template = CompilerContext.compile("{{hello noun=@world}}");
-  var helpers = {
-    hello: function(options) {
-      return "Hello " + options.hash.noun;
-    }
-  };
-
-  var result = template({}, { helpers: helpers, data: { world: "world" } });
-  equals("Hello world", result, "@foo as a parameter retrieves template data");
-});
-
-test("nested parameter data can be looked up via @foo.bar", function() {
-  var template = CompilerContext.compile("{{hello @world.bar}}");
-  var helpers = {
-    hello: function(noun) {
-      return "Hello " + noun;
-    }
-  };
-
-  var result = template({}, { helpers: helpers, data: { world: {bar: "world" } } });
-  equals("Hello world", result, "@foo as a parameter retrieves template data");
-});
-
-test("nested parameter data does not fail with @world.bar", function() {
-  var template = CompilerContext.compile("{{hello @world.bar}}");
-  var helpers = {
-    hello: function(noun) {
-      return "Hello " + noun;
-    }
-  };
-
-  var result = template({}, { helpers: helpers, data: { foo: {bar: "world" } } });
-  equals("Hello undefined", result, "@foo as a parameter retrieves template data");
-});
-
-test("parameter data throws when using this scope references", function() {
-  var string = "{{#goodbyes}}{{text}} cruel {{@./name}}! {{/goodbyes}}";
-
-  shouldThrow(function() {
-      CompilerContext.compile(string);
-    }, Error, "Should throw exception");
-});
-
-test("parameter data throws when using parent scope references", function() {
-  var string = "{{#goodbyes}}{{text}} cruel {{@../name}}! {{/goodbyes}}";
-
-  shouldThrow(function() {
-      CompilerContext.compile(string);
-    }, Error, "Should throw exception");
-});
-
-test("parameter data throws when using complex scope references", function() {
-  var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}";
-
-  shouldThrow(function() {
-      CompilerContext.compile(string);
-    }, Error, "Should throw exception");
-});
-
-test("data is inherited downstream", function() {
-  var template = CompilerContext.compile("{{#let foo=bar.baz}}{{@foo}}{{/let}}", { data: true });
-  var helpers = {
-    let: function(options) {
-      for (var prop in options.hash) {
-        options.data[prop] = options.hash[prop];
-      }
-      return options.fn(this);
-    }
-  };
-
-  var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} });
-  equals("hello world", result, "data variables are inherited downstream");
-});
-
-test("passing in data to a compiled function that expects data - works with helpers in partials", function() {
-  var template = CompilerContext.compile("{{>my_partial}}", {data: true});
-
-  var partials = {
-    my_partial: CompilerContext.compile("{{hello}}", {data: true})
-  };
-
-  var helpers = {
-    hello: function(options) {
-      return options.data.adjective + " "  + this.noun;
-    }
-  };
-
-  var result = template({noun: "cat"}, {helpers: helpers, partials: partials, data: {adjective: "happy"}});
-  equals("happy cat", result, "Data output by helper inside partial");
-});
-
-test("passing in data to a compiled function that expects data - works with helpers and parameters", function() {
-  var template = CompilerContext.compile("{{hello world}}", {data: true});
-
-  var helpers = {
-    hello: function(noun, options) {
-      return options.data.adjective + " "  + noun + (this.exclaim ? "!" : "");
-    }
-  };
-
-  var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}});
-  equals("happy world!", result, "Data output by helper");
-});
-
-test("passing in data to a compiled function that expects data - works with block helpers", function() {
-  var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true});
-
-  var helpers = {
-    hello: function(options) {
-      return options.fn(this);
-    },
-    world: function(options) {
-      return options.data.adjective + " world" + (this.exclaim ? "!" : "");
-    }
-  };
-
-  var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}});
-  equals("happy world!", result, "Data output by helper");
-});
-
-test("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
-  var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
-
-  var helpers = {
-    hello: function(options) {
-      return options.fn({exclaim: "?"});
-    },
-    world: function(thing, options) {
-      return options.data.adjective + " " + thing + (this.exclaim || "");
-    }
-  };
-
-  var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
-  equals("happy world?", result, "Data output by helper");
-});
-
-test("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() {
-  var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
-
-  var helpers = {
-    hello: function(options) {
-      return options.data.accessData + " " + options.fn({exclaim: "?"});
-    },
-    world: function(thing, options) {
-      return options.data.adjective + " " + thing + (this.exclaim || "");
-    }
-  };
-
-  var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}});
-  equals("#win happy world?", result, "Data output by helper");
-});
-
-test("you can override inherited data when invoking a helper", function() {
-  var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true});
-
-  var helpers = {
-    hello: function(options) {
-      return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} });
-    },
-    world: function(thing, options) {
-      return options.data.adjective + " " + thing + (this.exclaim || "");
-    }
-  };
-
-  var result = template({exclaim: true, zomg: "planet"}, {helpers: helpers, data: {adjective: "happy"}});
-  equals("sad world?", result, "Overriden data output by helper");
-});
-
-
-test("you can override inherited data when invoking a helper with depth", function() {
-  var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
-
-  var helpers = {
-    hello: function(options) {
-      return options.fn({exclaim: "?"}, { data: {adjective: "sad"} });
-    },
-    world: function(thing, options) {
-      return options.data.adjective + " " + thing + (this.exclaim || "");
-    }
-  };
-
-  var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
-  equals("sad world?", result, "Overriden data output by helper");
-});
-
-test("helpers take precedence over same-named context properties", function() {
-  var template = CompilerContext.compile("{{goodbye}} {{cruel world}}");
-
-  var helpers = {
-    goodbye: function() {
-      return this.goodbye.toUpperCase();
-    },
-
-    cruel: function(world) {
-      return "cruel " + world.toUpperCase();
-    }
-  };
-
-  var context = {
-    goodbye: "goodbye",
-    world: "world"
-  };
-
-  var result = template(context, {helpers: helpers});
-  equals(result, "GOODBYE cruel WORLD", "Helper executed");
-});
-
-test("helpers take precedence over same-named context properties$", function() {
-  var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}");
-
-  var helpers = {
-    goodbye: function(options) {
-      return this.goodbye.toUpperCase() + options.fn(this);
-    },
-
-    cruel: function(world) {
-      return "cruel " + world.toUpperCase();
-    }
-  };
-
-  var context = {
-    goodbye: "goodbye",
-    world: "world"
-  };
-
-  var result = template(context, {helpers: helpers});
-  equals(result, "GOODBYE cruel WORLD", "Helper executed");
-});
-
-test("Scoped names take precedence over helpers", function() {
-  var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}");
-
-  var helpers = {
-    goodbye: function() {
-      return this.goodbye.toUpperCase();
-    },
-
-    cruel: function(world) {
-      return "cruel " + world.toUpperCase();
-    },
-  };
-
-  var context = {
-    goodbye: "goodbye",
-    world: "world"
-  };
-
-  var result = template(context, {helpers: helpers});
-  equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed");
-});
-
-test("Scoped names take precedence over block helpers", function() {
-  var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}");
-
-  var helpers = {
-    goodbye: function(options) {
-      return this.goodbye.toUpperCase() + options.fn(this);
-    },
-
-    cruel: function(world) {
-      return "cruel " + world.toUpperCase();
-    },
-  };
-
-  var context = {
-    goodbye: "goodbye",
-    world: "world"
-  };
-
-  var result = template(context, {helpers: helpers});
-  equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed");
-});
-
-test("helpers can take an optional hash", function() {
-  var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}');
-
-  var helpers = {
-    goodbye: function(options) {
-      return "GOODBYE " + options.hash.cruel + " " + options.hash.world + " " + options.hash.times + " TIMES";
-    }
-  };
-
-  var context = {};
-
-  var result = template(context, {helpers: helpers});
-  equals(result, "GOODBYE CRUEL WORLD 12 TIMES", "Helper output hash");
-});
-
-test("helpers can take an optional hash with booleans", function() {
-  var helpers = {
-    goodbye: function(options) {
-      if (options.hash.print === true) {
-        return "GOODBYE " + options.hash.cruel + " " + options.hash.world;
-      } else if (options.hash.print === false) {
-        return "NOT PRINTING";
-      } else {
-        return "THIS SHOULD NOT HAPPEN";
-      }
-    }
-  };
-
-  var context = {};
-
-  var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}');
-  var result = template(context, {helpers: helpers});
-  equals(result, "GOODBYE CRUEL WORLD", "Helper output hash");
-
-  template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}');
-  result = template(context, {helpers: helpers});
-  equals(result, "NOT PRINTING", "Boolean helper parameter honored");
-});
-
-test("block helpers can take an optional hash", function() {
-  var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}');
-
-  var helpers = {
-    goodbye: function(options) {
-      return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
-    }
-  };
-
-  var result = template({}, {helpers: helpers});
-  equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output");
-});
-
-test("block helpers can take an optional hash with single quoted stings", function() {
-  var template = CompilerContext.compile("{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}");
-
-  var helpers = {
-    goodbye: function(options) {
-      return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
-    }
-  };
-
-  var result = template({}, {helpers: helpers});
-  equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output");
-});
-
-test("block helpers can take an optional hash with booleans", function() {
-  var helpers = {
-    goodbye: function(options) {
-      if (options.hash.print === true) {
-        return "GOODBYE " + options.hash.cruel + " " + options.fn(this);
-      } else if (options.hash.print === false) {
-        return "NOT PRINTING";
-      } else {
-        return "THIS SHOULD NOT HAPPEN";
-      }
-    }
-  };
-
-  var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}');
-  var result = template({}, {helpers: helpers});
-  equals(result, "GOODBYE CRUEL world", "Boolean hash parameter honored");
-
-  template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}');
-  result = template({}, {helpers: helpers});
-  equals(result, "NOT PRINTING", "Boolean hash parameter honored");
-});
-
-
-test("arguments to helpers can be retrieved from options hash in string form", function() {
-  var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true});
-
-  var helpers = {
-    wycats: function(passiveVoice, noun) {
-      return "HELP ME MY BOSS " + passiveVoice + ' ' + noun;
-    }
-  };
-
-  var result = template({}, {helpers: helpers});
-
-  equals(result, "HELP ME MY BOSS is.a slave.driver", "String parameters output");
-});
-
-test("when using block form, arguments to helpers can be retrieved from options hash in string form", function() {
-  var template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true});
-
-  var helpers = {
-    wycats: function(passiveVoice, noun, options) {
-      return "HELP ME MY BOSS " + passiveVoice + ' ' +
-              noun + ': ' + options.fn(this);
-    }
-  };
-
-  var result = template({}, {helpers: helpers});
-
-  equals(result, "HELP ME MY BOSS is.a slave.driver: help :(", "String parameters output");
-});
-
-test("when inside a block in String mode, .. passes the appropriate context in the options hash", function() {
-  var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true});
-
-  var helpers = {
-    tomdale: function(desire, noun, options) {
-      return "STOP ME FROM READING HACKER NEWS I " +
-              options.contexts[0][desire] + " " + noun;
-    },
-
-    "with": function(context, options) {
-      return options.fn(options.contexts[0][context]);
-    }
-  };
-
-  var result = template({
-    dale: {},
-
-    need: 'need-a'
-  }, {helpers: helpers});
-
-  equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke", "Proper context variable output");
-});
-
-test("in string mode, information about the types is passed along", function() {
-  var template = CompilerContext.compile('{{tomdale "need" dad.joke true false}}', { stringParams: true });
-
-  var helpers = {
-    tomdale: function(desire, noun, trueBool, falseBool, options) {
-      equal(options.types[0], 'STRING', "the string type is passed");
-      equal(options.types[1], 'ID', "the expression type is passed");
-      equal(options.types[2], 'BOOLEAN', "the expression type is passed");
-      equal(desire, "need", "the string form is passed for strings");
-      equal(noun, "dad.joke", "the string form is passed for expressions");
-      equal(trueBool, true, "raw booleans are passed through");
-      equal(falseBool, false, "raw booleans are passed through");
-      return "Helper called";
-    }
-  };
-
-  var result = template({}, { helpers: helpers });
-  equal(result, "Helper called");
-});
-
-test("in string mode, hash parameters get type information", function() {
-  var template = CompilerContext.compile('{{tomdale he.says desire="need" noun=dad.joke bool=true}}', { stringParams: true });
-
-  var helpers = {
-    tomdale: function(exclamation, options) {
-      equal(exclamation, "he.says");
-      equal(options.types[0], "ID");
-
-      equal(options.hashTypes.desire, "STRING");
-      equal(options.hashTypes.noun, "ID");
-      equal(options.hashTypes.bool, "BOOLEAN");
-      equal(options.hash.desire, "need");
-      equal(options.hash.noun, "dad.joke");
-      equal(options.hash.bool, true);
-      return "Helper called";
-    }
-  };
-
-  var result = template({}, { helpers: helpers });
-  equal(result, "Helper called");
-});
-
-test("in string mode, hash parameters get context information", function() {
-  var template = CompilerContext.compile('{{#with dale}}{{tomdale he.says desire="need" noun=../dad/joke bool=true}}{{/with}}', { stringParams: true });
-
-  var context = {dale: {}};
-
-  var helpers = {
-    tomdale: function(exclamation, options) {
-      equal(exclamation, "he.says");
-      equal(options.types[0], "ID");
-
-      equal(options.contexts.length, 1);
-      equal(options.hashContexts.noun, context);
-      equal(options.hash.desire, "need");
-      equal(options.hash.noun, "dad.joke");
-      equal(options.hash.bool, true);
-      return "Helper called";
-    },
-    "with": function(context, options) {
-      return options.fn(options.contexts[0][context]);
-    }
-  };
-
-  var result = template(context, { helpers: helpers });
-  equal(result, "Helper called");
-});
-
-test("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() {
-  var template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true});
-
-  var helpers = {
-    tomdale: function(desire, noun, options) {
-      return "STOP ME FROM READING HACKER NEWS I " +
-              options.contexts[0][desire] + " " + noun + " " +
-              options.fn(this);
-    },
-
-    "with": function(context, options) {
-      return options.fn(options.contexts[0][context]);
-    }
-  };
-
-  var result = template({
-    dale: {},
-
-    need: 'need-a'
-  }, {helpers: helpers});
-
-  equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output");
-});
-
-suite("Regressions");
-
-test("GH-94: Cannot read property of undefined", function() {
-	var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]};
-	var string = "{{#books}}{{title}}{{author.name}}{{/books}}";
-	shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes",
-                  "Renders without an undefined property error");
-});
-
-test("GH-150: Inverted sections print when they shouldn't", function() {
-  var string = "{{^set}}not set{{/set}} :: {{#set}}set{{/set}}";
-
-  shouldCompileTo(string, {}, "not set :: ", "inverted sections run when property isn't present in context");
-  shouldCompileTo(string, {set: undefined}, "not set :: ", "inverted sections run when property is undefined");
-  shouldCompileTo(string, {set: false}, "not set :: ", "inverted sections run when property is false");
-  shouldCompileTo(string, {set: true}, " :: set", "inverted sections don't run when property is true");
-});
-
-test("Mustache man page", function() {
-  var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}";
-  var data = {
-    "name": "Chris",
-    "value": 10000,
-    "taxed_value": 10000 - (10000 * 0.4),
-    "in_ca": true
-  };
-
-  shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works");
-});
-
-test("GH-158: Using array index twice, breaks the template", function() {
-  var string = "{{arr.[0]}}, {{arr.[1]}}";
-  var data = { "arr": [1,2] };
-
-  shouldCompileTo(string, data, "1, 2", "it works as expected");
-});
-
-test("bug reported by @fat where lambdas weren't being properly resolved", function() {
-  var string = "<strong>This is a slightly more complicated {{thing}}.</strong>.\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n<ul>\n{{#things}}\n<li class={{className}}>{{word}}</li>\n{{/things}}</ul>.\n{{/hasThings}}\n{{^hasThings}}\n\n<small>Nothing to check out...</small>\n{{/hasThings}}";
-  var data = {
-    thing: function() {
-      return "blah";
-    },
-    things: [
-      {className: "one", word: "@fat"},
-      {className: "two", word: "@dhg"},
-      {className: "three", word:"@sayrer"}
-    ],
-    hasThings: function() {
-      return true;
-    }
-  };
-
-  var output = "<strong>This is a slightly more complicated blah.</strong>.\n\nCheck this out:\n\n<ul>\n\n<li class=one>@fat</li>\n\n<li class=two>@dhg</li>\n\n<li class=three>@sayrer</li>\n</ul>.\n\n";
-  shouldCompileTo(string, data, output);
-});
-
-test("Passing falsy values to Handlebars.compile throws an error", function() {
-  shouldThrow(function() {
-    CompilerContext.compile(null);
-  }, "You must pass a string or Handlebars AST to Handlebars.precompile. You passed null");
-});
-
-test("can pass through an already-compiled AST via compile/precompile", function() {
-  equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))(), 'Hello')
-});
-
-test('GH-408: Multiple loops fail', function() {
-  var context = [
-    { name: "John Doe", location: { city: "Chicago" } },
-    { name: "Jane Doe", location: { city: "New York"} }
-  ];
-
-  var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}');
-
-  var result = template(context);
-  equals(result, "John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe", 'It should output multiple times');
-});
-
-test('GS-428: Nested if else rendering', function() {
-  var succeedingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}}  {{#blk}} Expected {{/blk}} {{/inverse}}';
-  var failingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}';
-
-  var helpers = {
-    blk: function(block) { return block.fn(''); },
-    inverse: function(block) { return block.inverse(''); }
-  };
-
-  shouldCompileTo(succeedingTemplate, [{}, helpers], '   Expected  ');
-  shouldCompileTo(failingTemplate, [{}, helpers], '  Expected  ');
-});
-
-test('GH-458: Scoped this identifier', function() {
-  shouldCompileTo('{{./foo}}', {foo: 'bar'}, 'bar');
-});
-
-test('GH-375: Unicode line terminators', function() {
-  shouldCompileTo('\u2028', {}, '\u2028');
-});
-
-test('GH-534: Object prototype aliases', function() {
-  Object.prototype[0xD834] = true;
-
-  shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar');
-
-  delete Object.prototype[0xD834];
-});
-
-test('GH-437: Matching escaping', function() {
-  shouldThrow(function() {
-    CompilerContext.compile('{{{a}}');
-  }, Error);
-  shouldThrow(function() {
-    CompilerContext.compile('{{a}}}');
-  }, Error);
-});
-
-suite('Utils');
-
-test('escapeExpression', function() {
-  equal(Handlebars.Utils.escapeExpression('foo<&"\'>'), 'foo&lt;&amp;&quot;&#x27;&gt;');
-  equal(Handlebars.Utils.escapeExpression(new Handlebars.SafeString('foo<&"\'>')), 'foo<&"\'>');
-  equal(Handlebars.Utils.escapeExpression(''), '');
-  equal(Handlebars.Utils.escapeExpression(undefined), '');
-  equal(Handlebars.Utils.escapeExpression(null), '');
-  equal(Handlebars.Utils.escapeExpression(false), '');
-
-  equal(Handlebars.Utils.escapeExpression(0), '0');
-  equal(Handlebars.Utils.escapeExpression({}), {}.toString());
-  equal(Handlebars.Utils.escapeExpression([]), [].toString());
-});
-
-test('isEmpty', function() {
-  equal(Handlebars.Utils.isEmpty(undefined), true);
-  equal(Handlebars.Utils.isEmpty(null), true);
-  equal(Handlebars.Utils.isEmpty(false), true);
-  equal(Handlebars.Utils.isEmpty(''), true);
-  equal(Handlebars.Utils.isEmpty([]), true);
-
-  equal(Handlebars.Utils.isEmpty(0), false);
-  equal(Handlebars.Utils.isEmpty([1]), false);
-  equal(Handlebars.Utils.isEmpty('foo'), false);
-  equal(Handlebars.Utils.isEmpty({bar: 1}), false);
-});
-
-if (typeof(require) !== 'undefined') {
-  suite('Require');
-
-  test('Load .handlebars files with require()', function() {
-    var template = require("./example_1");
-    assert.deepEqual(template, require("./example_1.handlebars"));
-
-    var expected = 'foo\n';
-    var result = template({foo: "foo"});
-
-    equal(result, expected);
-  });
-
-  test('Load .hbs files with require()', function() {
-    var template = require("./example_2");
-    assert.deepEqual(template, require("./example_2.hbs"));
-
-    var expected = 'Hello, World!\n';
-    var result = template({name: "World"});
-
-    equal(result, expected);
-  });
-}
diff --git a/spec/regressions.js b/spec/regressions.js
new file mode 100644
index 000000000..44bde81b6
--- /dev/null
+++ b/spec/regressions.js
@@ -0,0 +1,119 @@
+/*global CompilerContext, shouldCompileTo */
+describe('Regressions', function() {
+  it("GH-94: Cannot read property of undefined", function() {
+    var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]};
+    var string = "{{#books}}{{title}}{{author.name}}{{/books}}";
+    shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes",
+                    "Renders without an undefined property error");
+  });
+
+  it("GH-150: Inverted sections print when they shouldn't", function() {
+    var string = "{{^set}}not set{{/set}} :: {{#set}}set{{/set}}";
+
+    shouldCompileTo(string, {}, "not set :: ", "inverted sections run when property isn't present in context");
+    shouldCompileTo(string, {set: undefined}, "not set :: ", "inverted sections run when property is undefined");
+    shouldCompileTo(string, {set: false}, "not set :: ", "inverted sections run when property is false");
+    shouldCompileTo(string, {set: true}, " :: set", "inverted sections don't run when property is true");
+  });
+
+  it("GH-158: Using array index twice, breaks the template", function() {
+    var string = "{{arr.[0]}}, {{arr.[1]}}";
+    var data = { "arr": [1,2] };
+
+    shouldCompileTo(string, data, "1, 2", "it works as expected");
+  });
+
+  it("bug reported by @fat where lambdas weren't being properly resolved", function() {
+    var string = "<strong>This is a slightly more complicated {{thing}}.</strong>.\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n<ul>\n{{#things}}\n<li class={{className}}>{{word}}</li>\n{{/things}}</ul>.\n{{/hasThings}}\n{{^hasThings}}\n\n<small>Nothing to check out...</small>\n{{/hasThings}}";
+    var data = {
+      thing: function() {
+        return "blah";
+      },
+      things: [
+        {className: "one", word: "@fat"},
+        {className: "two", word: "@dhg"},
+        {className: "three", word:"@sayrer"}
+      ],
+      hasThings: function() {
+        return true;
+      }
+    };
+
+    var output = "<strong>This is a slightly more complicated blah.</strong>.\n\nCheck this out:\n\n<ul>\n\n<li class=one>@fat</li>\n\n<li class=two>@dhg</li>\n\n<li class=three>@sayrer</li>\n</ul>.\n\n";
+    shouldCompileTo(string, data, output);
+  });
+
+  it('GH-408: Multiple loops fail', function() {
+    var context = [
+      { name: "John Doe", location: { city: "Chicago" } },
+      { name: "Jane Doe", location: { city: "New York"} }
+    ];
+
+    var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}');
+
+    var result = template(context);
+    equals(result, "John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe", 'It should output multiple times');
+  });
+
+  it('GS-428: Nested if else rendering', function() {
+    var succeedingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}}  {{#blk}} Expected {{/blk}} {{/inverse}}';
+    var failingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}';
+
+    var helpers = {
+      blk: function(block) { return block.fn(''); },
+      inverse: function(block) { return block.inverse(''); }
+    };
+
+    shouldCompileTo(succeedingTemplate, [{}, helpers], '   Expected  ');
+    shouldCompileTo(failingTemplate, [{}, helpers], '  Expected  ');
+  });
+
+  it('GH-458: Scoped this identifier', function() {
+    shouldCompileTo('{{./foo}}', {foo: 'bar'}, 'bar');
+  });
+
+  it('GH-375: Unicode line terminators', function() {
+    shouldCompileTo('\u2028', {}, '\u2028');
+  });
+
+  it('GH-534: Object prototype aliases', function() {
+    Object.prototype[0xD834] = true;
+
+    shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar');
+
+    delete Object.prototype[0xD834];
+  });
+
+  it('GH-437: Matching escaping', function() {
+    (function() {
+      CompilerContext.compile('{{{a}}');
+    }).should.throw(Error);
+    (function() {
+      CompilerContext.compile('{{a}}}');
+    }).should.throw(Error);
+  });
+
+  it("Mustache man page", function() {
+    var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}";
+    var data = {
+      "name": "Chris",
+      "value": 10000,
+      "taxed_value": 10000 - (10000 * 0.4),
+      "in_ca": true
+    };
+
+    shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works");
+  });
+
+  it("Passing falsy values to Handlebars.compile throws an error", function() {
+    (function() {
+      CompilerContext.compile(null);
+    }).should.throw("You must pass a string or Handlebars AST to Handlebars.precompile. You passed null");
+  });
+
+  if (Handlebars.AST) {
+    it("can pass through an already-compiled AST via compile/precompile", function() {
+      equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))(), 'Hello');
+    });
+  }
+});
diff --git a/spec/require.js b/spec/require.js
new file mode 100644
index 000000000..d750d7d3d
--- /dev/null
+++ b/spec/require.js
@@ -0,0 +1,23 @@
+if (typeof(require) !== 'undefined' && require.extensions[".handlebars"]) {
+  describe('Require', function() {
+    it('Load .handlebars files with require()', function() {
+      var template = require("./artifacts/example_1");
+      template.should.eql(require("./artifacts/example_1.handlebars"));
+
+      var expected = 'foo\n';
+      var result = template({foo: "foo"});
+
+      result.should.equal(expected);
+    });
+
+    it('Load .hbs files with require()', function() {
+      var template = require("./artifacts/example_2");
+      template.should.eql(require("./artifacts/example_2.hbs"));
+
+      var expected = 'Hello, World!\n';
+      var result = template({name: "World"});
+
+      result.should.equal(expected);
+    });
+  });
+}
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
deleted file mode 100644
index eb2f26afd..000000000
--- a/spec/spec_helper.rb
+++ /dev/null
@@ -1,132 +0,0 @@
-require "v8"
-
-# Monkey patches due to bugs in RubyRacer
-class V8::JSError
-  def initialize(try, to)
-    @to = to
-    begin
-      super(initialize_unsafe(try))
-    rescue Exception => e
-      # Original code does not make an Array here
-      @boundaries = [Boundary.new(:rbframes => e.backtrace)]
-      @value = e
-      super("BUG! please report. JSError#initialize failed!: #{e.message}")
-    end
-  end
-
-  def parse_js_frames(try)
-    raw = @to.rb(try.StackTrace())
-    if raw && !raw.empty?
-      raw.split("\n")[1..-1].tap do |frames|
-        # Original code uses strip!, and the frames are not guaranteed to be strippable
-        frames.each {|frame| frame.strip.chomp!(",")}
-      end
-    else
-      []
-    end
-  end
-end
-
-module Handlebars
-  module Spec
-    def self.js_backtrace(context)
-      begin
-        context.eval("throw")
-      rescue V8::JSError => e
-        return e.backtrace(:javascript)
-      end
-    end
-
-    def self.remove_exports(string)
-      match = string.match(%r{\A(.*?)^// BEGIN\(BROWSER\)\n(.*)\n^// END\(BROWSER\)(.*?)\Z}m)
-      prelines = match ? match[1].count("\n") + 1 : 0
-      ret = match ? match[2] : string
-      ("\n" * prelines) + ret
-    end
-
-    def self.load_helpers(context)
-      context["exports"] = nil
-
-      context["p"] = proc do |this, val|
-        p val if ENV["DEBUG_JS"]
-      end
-
-      context["puts"] = proc do |this, val|
-        puts val if ENV["DEBUG_JS"]
-      end
-
-      context["puts_node"] = proc do |this, val|
-        puts context["Handlebars"]["PrintVisitor"].new.accept(val)
-        puts
-      end
-
-      context["puts_caller"] = proc do
-        puts "BACKTRACE:"
-        puts Handlebars::Spec.js_backtrace(context)
-        puts
-      end
-    end
-
-    def self.js_load(context, file)
-      str = File.read(file)
-      context.eval(remove_exports(str), file)
-    end
-
-    CONTEXT = V8::Context.new
-    CONTEXT.instance_eval do |context|
-      Handlebars::Spec.load_helpers(context);
-
-      Handlebars::Spec.js_load(context, 'dist/handlebars.js');
-
-      context["CompilerContext"] = {}
-      CompilerContext = context["CompilerContext"]
-      CompilerContext["compile"] = proc do |this, *args|
-        template, options = args[0], args[1] || nil
-        templateSpec = COMPILE_CONTEXT["Handlebars"]["precompile"].call(template, options);
-        context["Handlebars"]["template"].call(context.eval("(#{templateSpec})"));
-      end
-      CompilerContext["compileWithPartial"] = proc do |this, *args|
-        template, options = args[0], args[1] || nil
-        context["Handlebars"]["compile"].call(template, options);
-      end
-    end
-
-    PARSER_CONTEXT = V8::Context.new
-    PARSER_CONTEXT.instance_eval do |context|
-      Handlebars::Spec.load_helpers(context);
-
-      Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/parser.js');
-    end
-
-    COMPILE_CONTEXT = V8::Context.new
-    COMPILE_CONTEXT.instance_eval do |context|
-      Handlebars::Spec.load_helpers(context);
-
-      Handlebars::Spec.js_load(context, 'dist/handlebars.js');
-      Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/visitor.js');
-      Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/printer.js');
-
-      context["Handlebars"]["logger"]["level"] = ENV["DEBUG_JS"] ? context["Handlebars"]["logger"][ENV["DEBUG_JS"]] : 4
-
-      context["Handlebars"]["logger"]["log"] = proc do |this, level, str|
-        logger_level = context["Handlebars"]["logger"]["level"].to_i
-
-        if logger_level <= level
-          puts str
-        end
-      end
-    end
-  end
-end
-
-
-require "test/unit/assertions"
-
-RSpec.configure do |config|
-  config.include Test::Unit::Assertions
-
-  # Each is required to allow classes to mark themselves as compiler tests
-  config.before(:each) do
-    @context = @compiles ? Handlebars::Spec::COMPILE_CONTEXT : Handlebars::Spec::CONTEXT
-  end
-end
diff --git a/spec/string-params.js b/spec/string-params.js
new file mode 100644
index 000000000..1ebb5838b
--- /dev/null
+++ b/spec/string-params.js
@@ -0,0 +1,145 @@
+describe('string params mode', function() {
+  it("arguments to helpers can be retrieved from options hash in string form", function() {
+    var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true});
+
+    var helpers = {
+      wycats: function(passiveVoice, noun) {
+        return "HELP ME MY BOSS " + passiveVoice + ' ' + noun;
+      }
+    };
+
+    var result = template({}, {helpers: helpers});
+
+    equals(result, "HELP ME MY BOSS is.a slave.driver", "String parameters output");
+  });
+
+  it("when using block form, arguments to helpers can be retrieved from options hash in string form", function() {
+    var template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true});
+
+    var helpers = {
+      wycats: function(passiveVoice, noun, options) {
+        return "HELP ME MY BOSS " + passiveVoice + ' ' +
+                noun + ': ' + options.fn(this);
+      }
+    };
+
+    var result = template({}, {helpers: helpers});
+
+    equals(result, "HELP ME MY BOSS is.a slave.driver: help :(", "String parameters output");
+  });
+
+  it("when inside a block in String mode, .. passes the appropriate context in the options hash", function() {
+    var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true});
+
+    var helpers = {
+      tomdale: function(desire, noun, options) {
+        return "STOP ME FROM READING HACKER NEWS I " +
+                options.contexts[0][desire] + " " + noun;
+      },
+
+      "with": function(context, options) {
+        return options.fn(options.contexts[0][context]);
+      }
+    };
+
+    var result = template({
+      dale: {},
+
+      need: 'need-a'
+    }, {helpers: helpers});
+
+    equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke", "Proper context variable output");
+  });
+
+  it("information about the types is passed along", function() {
+    var template = CompilerContext.compile('{{tomdale "need" dad.joke true false}}', { stringParams: true });
+
+    var helpers = {
+      tomdale: function(desire, noun, trueBool, falseBool, options) {
+        equal(options.types[0], 'STRING', "the string type is passed");
+        equal(options.types[1], 'ID', "the expression type is passed");
+        equal(options.types[2], 'BOOLEAN', "the expression type is passed");
+        equal(desire, "need", "the string form is passed for strings");
+        equal(noun, "dad.joke", "the string form is passed for expressions");
+        equal(trueBool, true, "raw booleans are passed through");
+        equal(falseBool, false, "raw booleans are passed through");
+        return "Helper called";
+      }
+    };
+
+    var result = template({}, { helpers: helpers });
+    equal(result, "Helper called");
+  });
+
+  it("hash parameters get type information", function() {
+    var template = CompilerContext.compile('{{tomdale he.says desire="need" noun=dad.joke bool=true}}', { stringParams: true });
+
+    var helpers = {
+      tomdale: function(exclamation, options) {
+        equal(exclamation, "he.says");
+        equal(options.types[0], "ID");
+
+        equal(options.hashTypes.desire, "STRING");
+        equal(options.hashTypes.noun, "ID");
+        equal(options.hashTypes.bool, "BOOLEAN");
+        equal(options.hash.desire, "need");
+        equal(options.hash.noun, "dad.joke");
+        equal(options.hash.bool, true);
+        return "Helper called";
+      }
+    };
+
+    var result = template({}, { helpers: helpers });
+    equal(result, "Helper called");
+  });
+
+  it("hash parameters get context information", function() {
+    var template = CompilerContext.compile('{{#with dale}}{{tomdale he.says desire="need" noun=../dad/joke bool=true}}{{/with}}', { stringParams: true });
+
+    var context = {dale: {}};
+
+    var helpers = {
+      tomdale: function(exclamation, options) {
+        equal(exclamation, "he.says");
+        equal(options.types[0], "ID");
+
+        equal(options.contexts.length, 1);
+        equal(options.hashContexts.noun, context);
+        equal(options.hash.desire, "need");
+        equal(options.hash.noun, "dad.joke");
+        equal(options.hash.bool, true);
+        return "Helper called";
+      },
+      "with": function(context, options) {
+        return options.fn(options.contexts[0][context]);
+      }
+    };
+
+    var result = template(context, { helpers: helpers });
+    equal(result, "Helper called");
+  });
+
+  it("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() {
+    var template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true});
+
+    var helpers = {
+      tomdale: function(desire, noun, options) {
+        return "STOP ME FROM READING HACKER NEWS I " +
+                options.contexts[0][desire] + " " + noun + " " +
+                options.fn(this);
+      },
+
+      "with": function(context, options) {
+        return options.fn(options.contexts[0][context]);
+      }
+    };
+
+    var result = template({
+      dale: {},
+
+      need: 'need-a'
+    }, {helpers: helpers});
+
+    equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output");
+  });
+});
diff --git a/spec/tokenizer.js b/spec/tokenizer.js
new file mode 100644
index 000000000..de981e427
--- /dev/null
+++ b/spec/tokenizer.js
@@ -0,0 +1,358 @@
+var should = require('should');
+
+should.Assertion.prototype.match_tokens = function(tokens) {
+  this.obj.forEach(function(value, index) {
+    value.name.should.equal(tokens[index]);
+  });
+};
+should.Assertion.prototype.be_token = function(name, text) {
+  this.obj.should.eql({name: name, text: text});
+};
+
+describe('Tokenizer', function() {
+  if (!Handlebars.Parser) {
+    return;
+  }
+
+  function tokenize(template) {
+    var parser = Handlebars.Parser,
+        lexer = parser.lexer;
+
+    lexer.setInput(template);
+    var out = [],
+        token;
+
+    while (token = lexer.lex()) {
+      var result = parser.terminals_[token] || token;
+      if (!result || result === 'EOF' || result === 'INVALID') {
+        break;
+      }
+      out.push({name: result, text: lexer.yytext});
+    }
+
+    return out;
+  }
+
+  it('tokenizes a simple mustache as "OPEN ID CLOSE"', function() {
+    var result = tokenize("{{foo}}");
+    result.should.match_tokens(['OPEN', 'ID', 'CLOSE']);
+    result[1].should.be_token("ID", "foo");
+  });
+
+  it('supports unescaping with &', function() {
+    var result = tokenize("{{&bar}}");
+    result.should.match_tokens(['OPEN', 'ID', 'CLOSE']);
+
+    result[0].should.be_token("OPEN", "{{&");
+    result[1].should.be_token("ID", "bar");
+  });
+
+  it('supports unescaping with {{{', function() {
+    var result = tokenize("{{{bar}}}");
+    result.should.match_tokens(['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']);
+
+    result[1].should.be_token("ID", "bar");
+  });
+
+  it('supports escaping delimiters', function() {
+    var result = tokenize("{{foo}} \\{{bar}} {{baz}}");
+    result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
+
+    result[3].should.be_token("CONTENT", " ");
+    result[4].should.be_token("CONTENT", "{{bar}} ");
+  });
+
+  it('supports escaping multiple delimiters', function() {
+    var result = tokenize("{{foo}} \\{{bar}} \\{{baz}}");
+    result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
+
+    result[3].should.be_token("CONTENT", " ");
+    result[4].should.be_token("CONTENT", "{{bar}} ");
+    result[5].should.be_token("CONTENT", "{{baz}}");
+  });
+
+  it('supports escaping a triple stash', function() {
+    var result = tokenize("{{foo}} \\{{{bar}}} {{baz}}");
+    result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
+
+    result[4].should.be_token("CONTENT", "{{{bar}}} ");
+  });
+
+  it('supports escaping escape character', function() {
+    var result = tokenize("{{foo}} \\\\{{bar}} {{baz}}");
+    result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
+
+    result[3].should.be_token("CONTENT", " \\");
+    result[5].should.be_token("ID", "bar");
+  });
+
+  it('supports escaping multiple escape characters', function() {
+    var result = tokenize("{{foo}} \\\\{{bar}} \\\\{{baz}}");
+      result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
+
+      result[3].should.be_token("CONTENT", " \\");
+      result[5].should.be_token("ID", "bar");
+      result[7].should.be_token("CONTENT", " \\");
+      result[9].should.be_token("ID", "baz");
+  });
+
+  it('supports mixed escaped delimiters and escaped escape characters', function() {
+    var result = tokenize("{{foo}} \\\\{{bar}} \\{{baz}}");
+    result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
+
+    result[3].should.be_token("CONTENT", " \\");
+    result[4].should.be_token("OPEN", "{{");
+    result[5].should.be_token("ID", "bar");
+    result[7].should.be_token("CONTENT", " ");
+    result[8].should.be_token("CONTENT", "{{baz}}");
+  });
+
+  it('supports escaped escape character on a triple stash', function() {
+    var result = tokenize("{{foo}} \\\\{{{bar}}} {{baz}}");
+    result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
+
+    result[3].should.be_token("CONTENT", " \\");
+    result[5].should.be_token("ID", "bar");
+  });
+
+  it('tokenizes a simple path', function() {
+    var result = tokenize("{{foo/bar}}");
+    result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
+  });
+
+  it('allows dot notation', function() {
+    var result = tokenize("{{foo.bar}}");
+    result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
+
+    tokenize("{{foo.bar.baz}}").should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
+  });
+
+  it('allows path literals with []', function() {
+    var result = tokenize("{{foo.[bar]}}");
+    result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
+  });
+
+  it('allows multiple path literals on a line with []', function() {
+    var result = tokenize("{{foo.[bar]}}{{foo.[baz]}}");
+    result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE', 'OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
+  });
+
+  it('tokenizes {{.}} as OPEN ID CLOSE', function() {
+    var result = tokenize("{{.}}");
+    result.should.match_tokens(['OPEN', 'ID', 'CLOSE']);
+  });
+
+  it('tokenizes a path as "OPEN (ID SEP)* ID CLOSE"', function() {
+    var result = tokenize("{{../foo/bar}}");
+    result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
+    result[1].should.be_token("ID", "..");
+  });
+
+  it('tokenizes a path with .. as a parent path', function() {
+    var result = tokenize("{{../foo.bar}}");
+    result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
+    result[1].should.be_token("ID", "..");
+  });
+
+  it('tokenizes a path with this/foo as OPEN ID SEP ID CLOSE', function() {
+    var result = tokenize("{{this/foo}}");
+    result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
+    result[1].should.be_token("ID", "this");
+    result[3].should.be_token("ID", "foo");
+  });
+
+  it('tokenizes a simple mustache with spaces as "OPEN ID CLOSE"', function() {
+    var result = tokenize("{{  foo  }}");
+    result.should.match_tokens(['OPEN', 'ID', 'CLOSE']);
+    result[1].should.be_token("ID", "foo");
+  });
+
+  it('tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"', function() {
+    var result = tokenize("{{  foo  \n   bar }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'CLOSE']);
+    result[1].should.be_token("ID", "foo");
+  });
+
+  it('tokenizes raw content as "CONTENT"', function() {
+    var result = tokenize("foo {{ bar }} baz");
+    result.should.match_tokens(['CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT']);
+    result[0].should.be_token("CONTENT", "foo ");
+    result[4].should.be_token("CONTENT", " baz");
+  });
+
+  it('tokenizes a partial as "OPEN_PARTIAL ID CLOSE"', function() {
+    var result = tokenize("{{> foo}}");
+    result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'CLOSE']);
+  });
+
+  it('tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"', function() {
+    var result = tokenize("{{> foo bar }}");
+    result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'ID', 'CLOSE']);
+  });
+
+  it('tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"', function() {
+    var result = tokenize("{{>foo}}");
+    result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'CLOSE']);
+  });
+
+  it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() {
+    var result = tokenize("{{>foo  }}");
+    result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'CLOSE']);
+  });
+
+  it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() {
+    var result = tokenize("{{>foo/bar.baz  }}");
+    result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
+  });
+
+  it('tokenizes a comment as "COMMENT"', function() {
+    var result = tokenize("foo {{! this is a comment }} bar {{ baz }}");
+    result.should.match_tokens(['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
+    result[1].should.be_token("COMMENT", " this is a comment ");
+  });
+
+  it('tokenizes a block comment as "COMMENT"', function() {
+    var result = tokenize("foo {{!-- this is a {{comment}} --}} bar {{ baz }}");
+    result.should.match_tokens(['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
+    result[1].should.be_token("COMMENT", " this is a {{comment}} ");
+  });
+
+  it('tokenizes a block comment with whitespace as "COMMENT"', function() {
+    var result = tokenize("foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}");
+    result.should.match_tokens(['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
+    result[1].should.be_token("COMMENT", " this is a\n{{comment}}\n");
+  });
+
+  it('tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE', function() {
+    var result = tokenize("{{#foo}}content{{/foo}}");
+    result.should.match_tokens(['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']);
+  });
+
+  it('tokenizes inverse sections as "OPEN_INVERSE CLOSE"', function() {
+    tokenize("{{^}}").should.match_tokens(['OPEN_INVERSE', 'CLOSE']);
+    tokenize("{{else}}").should.match_tokens(['OPEN_INVERSE', 'CLOSE']);
+    tokenize("{{ else }}").should.match_tokens(['OPEN_INVERSE', 'CLOSE']);
+  });
+
+  it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function() {
+    var result = tokenize("{{^foo}}");
+    result.should.match_tokens(['OPEN_INVERSE', 'ID', 'CLOSE']);
+    result[1].should.be_token("ID", "foo");
+  });
+
+  it('tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"', function() {
+    var result = tokenize("{{^ foo  }}");
+    result.should.match_tokens(['OPEN_INVERSE', 'ID', 'CLOSE']);
+    result[1].should.be_token("ID", "foo");
+  });
+
+  it('tokenizes mustaches with params as "OPEN ID ID ID CLOSE"', function() {
+    var result = tokenize("{{ foo bar baz }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'CLOSE']);
+    result[1].should.be_token("ID", "foo");
+    result[2].should.be_token("ID", "bar");
+    result[3].should.be_token("ID", "baz");
+  });
+
+  it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function() {
+    var result = tokenize("{{ foo bar \"baz\" }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
+    result[3].should.be_token("STRING", "baz");
+  });
+
+  it('tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"', function() {
+    var result = tokenize("{{ foo bar \'baz\' }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
+    result[3].should.be_token("STRING", "baz");
+  });
+
+  it('tokenizes String params with spaces inside as "STRING"', function() {
+    var result = tokenize("{{ foo bar \"baz bat\" }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
+    result[3].should.be_token("STRING", "baz bat");
+  });
+
+  it('tokenizes String params with escapes quotes as STRING', function() {
+    var result = tokenize('{{ foo "bar\\"baz" }}');
+    result.should.match_tokens(['OPEN', 'ID', 'STRING', 'CLOSE']);
+    result[2].should.be_token("STRING", 'bar"baz');
+  });
+
+  it('tokenizes String params using single quotes with escapes quotes as STRING', function() {
+    var result = tokenize("{{ foo 'bar\\'baz' }}");
+    result.should.match_tokens(['OPEN', 'ID', 'STRING', 'CLOSE']);
+    result[2].should.be_token("STRING", "bar'baz");
+  });
+
+  it('tokenizes numbers', function() {
+    var result = tokenize('{{ foo 1 }}');
+    result.should.match_tokens(['OPEN', 'ID', 'INTEGER', 'CLOSE']);
+    result[2].should.be_token("INTEGER", "1");
+
+    result = tokenize('{{ foo -1 }}');
+    result.should.match_tokens(['OPEN', 'ID', 'INTEGER', 'CLOSE']);
+    result[2].should.be_token("INTEGER", "-1");
+  });
+
+  it('tokenizes booleans', function() {
+    var result = tokenize('{{ foo true }}');
+    result.should.match_tokens(['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
+    result[2].should.be_token("BOOLEAN", "true");
+
+    result = tokenize('{{ foo false }}');
+    result.should.match_tokens(['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
+    result[2].should.be_token("BOOLEAN", "false");
+  });
+
+  it('tokenizes hash arguments', function() {
+    var result = tokenize("{{ foo bar=baz }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
+
+    result = tokenize("{{ foo bar baz=bat }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
+
+    result = tokenize("{{ foo bar baz=1 }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'INTEGER', 'CLOSE']);
+
+    result = tokenize("{{ foo bar baz=true }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
+
+    result = tokenize("{{ foo bar baz=false }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
+
+    result = tokenize("{{ foo bar\n  baz=bat }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
+
+    result = tokenize("{{ foo bar baz=\"bat\" }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
+
+    result = tokenize("{{ foo bar baz=\"bat\" bam=wot }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'ID', 'EQUALS', 'ID', 'CLOSE']);
+
+    result = tokenize("{{foo omg bar=baz bat=\"bam\"}}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
+    result[2].should.be_token("ID", "omg");
+  });
+
+  it('tokenizes special @ identifiers', function() {
+    var result = tokenize("{{ @foo }}");
+    result.should.match_tokens(['OPEN', 'DATA', 'ID', 'CLOSE']);
+    result[2].should.be_token("ID", "foo");
+
+    result = tokenize("{{ foo @bar }}");
+    result.should.match_tokens(['OPEN', 'ID', 'DATA', 'ID', 'CLOSE']);
+    result[3].should.be_token("ID", "bar");
+
+    result = tokenize("{{ foo bar=@baz }}");
+    result.should.match_tokens(['OPEN', 'ID', 'ID', 'EQUALS', 'DATA', 'ID', 'CLOSE']);
+    result[5].should.be_token("ID", "baz");
+  });
+
+  it('does not time out in a mustache with a single } followed by EOF', function() {
+    tokenize("{{foo}").should.match_tokens(['OPEN', 'ID']);
+  });
+
+  it('does not time out in a mustache when invalid ID characters are used', function() {
+    tokenize("{{foo & }}").should.match_tokens(['OPEN', 'ID']);
+  });
+});
diff --git a/spec/tokenizer_spec.rb b/spec/tokenizer_spec.rb
deleted file mode 100644
index 0a7c3f9db..000000000
--- a/spec/tokenizer_spec.rb
+++ /dev/null
@@ -1,322 +0,0 @@
-require "spec_helper"
-require "timeout"
-
-describe "Tokenizer" do
-  let(:parser) { Handlebars::Spec::PARSER_CONTEXT["handlebars"] }
-  let(:lexer) { Handlebars::Spec::PARSER_CONTEXT["handlebars"]["lexer"] }
-
-  Token = Struct.new(:name, :text)
-
-  def tokenize(string)
-    lexer.setInput(string)
-    out = []
-
-    while token = lexer.lex
-      # p token
-      result = parser.terminals_[token] || token
-      # p result
-      break if !result || result == "EOF" || result == "INVALID"
-      out << Token.new(result, lexer.yytext)
-    end
-
-    out
-  end
-
-  RSpec::Matchers.define :match_tokens do |tokens|
-    match do |result|
-      result.map(&:name).should == tokens
-    end
-  end
-
-  RSpec::Matchers.define :be_token do |name, string|
-    match do |token|
-      token.name.should == name
-      token.text.should == string
-    end
-  end
-
-  it "tokenizes a simple mustache as 'OPEN ID CLOSE'" do
-    result = tokenize("{{foo}}")
-    result.should match_tokens(%w(OPEN ID CLOSE))
-    result[1].should be_token("ID", "foo")
-  end
-
-  it "supports unescaping with &" do
-    result = tokenize("{{&bar}}")
-    result.should match_tokens(%w(OPEN ID CLOSE))
-
-    result[0].should be_token("OPEN", "{{&")
-    result[1].should be_token("ID", "bar")
-  end
-
-  it "supports unescaping with {{{" do
-    result = tokenize("{{{bar}}}")
-    result.should match_tokens(%w(OPEN_UNESCAPED ID CLOSE_UNESCAPED))
-
-    result[1].should be_token("ID", "bar")
-  end
-
-  it "supports escaping delimiters" do
-    result = tokenize("{{foo}} \\{{bar}} {{baz}}")
-    result.should match_tokens(%w(OPEN ID CLOSE CONTENT CONTENT OPEN ID CLOSE))
-
-    result[4].should be_token("CONTENT", "{{bar}} ")
-  end
-
-  it "supports escaping multiple delimiters" do
-    result = tokenize("{{foo}} \\{{bar}} \\{{baz}}")
-    result.should match_tokens(%w(OPEN ID CLOSE CONTENT CONTENT CONTENT))
-
-    result[3].should be_token("CONTENT", " ")
-    result[4].should be_token("CONTENT", "{{bar}} ")
-    result[5].should be_token("CONTENT", "{{baz}}")
-  end
-
-  it "supports escaping a triple stash" do
-    result = tokenize("{{foo}} \\{{{bar}}} {{baz}}")
-    result.should match_tokens(%w(OPEN ID CLOSE CONTENT CONTENT OPEN ID CLOSE))
-
-    result[4].should be_token("CONTENT", "{{{bar}}} ")
-  end
-
-  it "tokenizes a simple path" do
-    result = tokenize("{{foo/bar}}")
-    result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
-  end
-
-  it "allows dot notation" do
-    result = tokenize("{{foo.bar}}")
-    result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
-
-    tokenize("{{foo.bar.baz}}").should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
-  end
-
-  it "allows path literals with []" do
-    result = tokenize("{{foo.[bar]}}")
-    result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
-  end
-
-  it "allows multiple path literals on a line with []" do
-    result = tokenize("{{foo.[bar]}}{{foo.[baz]}}")
-    result.should match_tokens(%w(OPEN ID SEP ID CLOSE OPEN ID SEP ID CLOSE))
-  end
-
-  it "tokenizes {{.}} as OPEN ID CLOSE" do
-    result = tokenize("{{.}}")
-    result.should match_tokens(%w(OPEN ID CLOSE))
-  end
-
-  it "tokenizes a path as 'OPEN (ID SEP)* ID CLOSE'" do
-    result = tokenize("{{../foo/bar}}")
-    result.should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
-    result[1].should be_token("ID", "..")
-  end
-
-  it "tokenizes a path with .. as a parent path" do
-    result = tokenize("{{../foo.bar}}")
-    result.should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
-    result[1].should be_token("ID", "..")
-  end
-
-  it "tokenizes a path with this/foo as OPEN ID SEP ID CLOSE" do
-    result = tokenize("{{this/foo}}")
-    result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
-    result[1].should be_token("ID", "this")
-    result[3].should be_token("ID", "foo")
-  end
-
-  it "tokenizes a simple mustache with spaces as 'OPEN ID CLOSE'" do
-    result = tokenize("{{  foo  }}")
-    result.should match_tokens(%w(OPEN ID CLOSE))
-    result[1].should be_token("ID", "foo")
-  end
-
-  it "tokenizes a simple mustache with line breaks as 'OPEN ID ID CLOSE'" do
-    result = tokenize("{{  foo  \n   bar }}")
-    result.should match_tokens(%w(OPEN ID ID CLOSE))
-    result[1].should be_token("ID", "foo")
-  end
-
-  it "tokenizes raw content as 'CONTENT'" do
-    result = tokenize("foo {{ bar }} baz")
-    result.should match_tokens(%w(CONTENT OPEN ID CLOSE CONTENT))
-    result[0].should be_token("CONTENT", "foo ")
-    result[4].should be_token("CONTENT", " baz")
-  end
-
-  it "tokenizes a partial as 'OPEN_PARTIAL ID CLOSE'" do
-    result = tokenize("{{> foo}}")
-    result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
-  end
-
-  it "tokenizes a partial with context as 'OPEN_PARTIAL ID ID CLOSE'" do
-    result = tokenize("{{> foo bar }}")
-    result.should match_tokens(%w(OPEN_PARTIAL ID ID CLOSE))
-  end
-
-  it "tokenizes a partial without spaces as 'OPEN_PARTIAL ID CLOSE'" do
-    result = tokenize("{{>foo}}")
-    result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
-  end
-
-  it "tokenizes a partial space at the end as 'OPEN_PARTIAL ID CLOSE'" do
-    result = tokenize("{{>foo  }}")
-    result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
-  end
-
-  it "tokenizes a partial space at the end as 'OPEN_PARTIAL ID CLOSE'" do
-    result = tokenize("{{>foo/bar.baz  }}")
-    result.should match_tokens(%w(OPEN_PARTIAL ID SEP ID SEP ID CLOSE))
-  end
-
-  it "tokenizes a comment as 'COMMENT'" do
-    result = tokenize("foo {{! this is a comment }} bar {{ baz }}")
-    result.should match_tokens(%w(CONTENT COMMENT CONTENT OPEN ID CLOSE))
-    result[1].should be_token("COMMENT", " this is a comment ")
-  end
-
-  it "tokenizes a block comment as 'COMMENT'" do
-    result = tokenize("foo {{!-- this is a {{comment}} --}} bar {{ baz }}")
-    result.should match_tokens(%w(CONTENT COMMENT CONTENT OPEN ID CLOSE))
-    result[1].should be_token("COMMENT", " this is a {{comment}} ")
-  end
-
-  it "tokenizes a block comment with whitespace as 'COMMENT'" do
-    result = tokenize("foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}")
-    result.should match_tokens(%w(CONTENT COMMENT CONTENT OPEN ID CLOSE))
-    result[1].should be_token("COMMENT", " this is a\n{{comment}}\n")
-  end
-
-  it "tokenizes open and closing blocks as 'OPEN_BLOCK ID CLOSE ... OPEN_ENDBLOCK ID CLOSE'" do
-    result = tokenize("{{#foo}}content{{/foo}}")
-    result.should match_tokens(%w(OPEN_BLOCK ID CLOSE CONTENT OPEN_ENDBLOCK ID CLOSE))
-  end
-
-  it "tokenizes inverse sections as 'OPEN_INVERSE CLOSE'" do
-    tokenize("{{^}}").should match_tokens(%w(OPEN_INVERSE CLOSE))
-    tokenize("{{else}}").should match_tokens(%w(OPEN_INVERSE CLOSE))
-    tokenize("{{ else }}").should match_tokens(%w(OPEN_INVERSE CLOSE))
-  end
-
-  it "tokenizes inverse sections with ID as 'OPEN_INVERSE ID CLOSE'" do
-    result = tokenize("{{^foo}}")
-    result.should match_tokens(%w(OPEN_INVERSE ID CLOSE))
-    result[1].should be_token("ID", "foo")
-  end
-
-  it "tokenizes inverse sections with ID and spaces as 'OPEN_INVERSE ID CLOSE'" do
-    result = tokenize("{{^ foo  }}")
-    result.should match_tokens(%w(OPEN_INVERSE ID CLOSE))
-    result[1].should be_token("ID", "foo")
-  end
-
-  it "tokenizes mustaches with params as 'OPEN ID ID ID CLOSE'" do
-    result = tokenize("{{ foo bar baz }}")
-    result.should match_tokens(%w(OPEN ID ID ID CLOSE))
-    result[1].should be_token("ID", "foo")
-    result[2].should be_token("ID", "bar")
-    result[3].should be_token("ID", "baz")
-  end
-
-  it "tokenizes mustaches with String params as 'OPEN ID ID STRING CLOSE'" do
-    result = tokenize("{{ foo bar \"baz\" }}")
-    result.should match_tokens(%w(OPEN ID ID STRING CLOSE))
-    result[3].should be_token("STRING", "baz")
-  end
-
-  it "tokenizes mustaches with String params using single quotes as 'OPEN ID ID STRING CLOSE'" do
-    result = tokenize("{{ foo bar \'baz\' }}")
-    result.should match_tokens(%w(OPEN ID ID STRING CLOSE))
-    result[3].should be_token("STRING", "baz")
-  end
-
-  it "tokenizes String params with spaces inside as 'STRING'" do
-    result = tokenize("{{ foo bar \"baz bat\" }}")
-    result.should match_tokens(%w(OPEN ID ID STRING CLOSE))
-    result[3].should be_token("STRING", "baz bat")
-  end
-
-  it "tokenizes String params with escapes quotes as 'STRING'" do
-    result = tokenize(%|{{ foo "bar\\"baz" }}|)
-    result.should match_tokens(%w(OPEN ID STRING CLOSE))
-    result[2].should be_token("STRING", %{bar"baz})
-  end
-
-  it "tokenizes String params using single quotes with escapes quotes as 'STRING'" do
-    result = tokenize(%|{{ foo 'bar\\'baz' }}|)
-    result.should match_tokens(%w(OPEN ID STRING CLOSE))
-    result[2].should be_token("STRING", %{bar'baz})
-  end
-
-  it "tokenizes numbers" do
-    result = tokenize(%|{{ foo 1 }}|)
-    result.should match_tokens(%w(OPEN ID INTEGER CLOSE))
-    result[2].should be_token("INTEGER", "1")
-
-    result = tokenize(%|{{ foo -1 }}|)
-    result.should match_tokens(%w(OPEN ID INTEGER CLOSE))
-    result[2].should be_token("INTEGER", "-1")
-  end
-
-  it "tokenizes booleans" do
-    result = tokenize(%|{{ foo true }}|)
-    result.should match_tokens(%w(OPEN ID BOOLEAN CLOSE))
-    result[2].should be_token("BOOLEAN", "true")
-
-    result = tokenize(%|{{ foo false }}|)
-    result.should match_tokens(%w(OPEN ID BOOLEAN CLOSE))
-    result[2].should be_token("BOOLEAN", "false")
-  end
-
-  it "tokenizes hash arguments" do
-    result = tokenize("{{ foo bar=baz }}")
-    result.should match_tokens %w(OPEN ID ID EQUALS ID CLOSE)
-
-    result = tokenize("{{ foo bar baz=bat }}")
-    result.should match_tokens %w(OPEN ID ID ID EQUALS ID CLOSE)
-
-    result = tokenize("{{ foo bar baz=1 }}")
-    result.should match_tokens %w(OPEN ID ID ID EQUALS INTEGER CLOSE)
-
-    result = tokenize("{{ foo bar baz=true }}")
-    result.should match_tokens %w(OPEN ID ID ID EQUALS BOOLEAN CLOSE)
-
-    result = tokenize("{{ foo bar baz=false }}")
-    result.should match_tokens %w(OPEN ID ID ID EQUALS BOOLEAN CLOSE)
-
-    result = tokenize("{{ foo bar\n  baz=bat }}")
-    result.should match_tokens %w(OPEN ID ID ID EQUALS ID CLOSE)
-
-    result = tokenize("{{ foo bar baz=\"bat\" }}")
-    result.should match_tokens %w(OPEN ID ID ID EQUALS STRING CLOSE)
-
-    result = tokenize("{{ foo bar baz=\"bat\" bam=wot }}")
-    result.should match_tokens %w(OPEN ID ID ID EQUALS STRING ID EQUALS ID CLOSE)
-
-    result = tokenize("{{foo omg bar=baz bat=\"bam\"}}")
-    result.should match_tokens %w(OPEN ID ID ID EQUALS ID ID EQUALS STRING CLOSE)
-    result[2].should be_token("ID", "omg")
-  end
-
-  it "tokenizes special @ identifiers" do
-    result = tokenize("{{ @foo }}")
-    result.should match_tokens %w( OPEN DATA ID CLOSE )
-    result[2].should be_token("ID", "foo")
-
-    result = tokenize("{{ foo @bar }}")
-    result.should match_tokens %w( OPEN ID DATA ID CLOSE )
-    result[3].should be_token("ID", "bar")
-
-    result = tokenize("{{ foo bar=@baz }}")
-    result.should match_tokens %w( OPEN ID ID EQUALS DATA ID CLOSE )
-    result[5].should be_token("ID", "baz")
-  end
-
-  it "does not time out in a mustache with a single } followed by EOF" do
-    Timeout.timeout(1) { tokenize("{{foo}").should match_tokens(%w(OPEN ID)) }
-  end
-
-  it "does not time out in a mustache when invalid ID characters are used" do
-    Timeout.timeout(1) { tokenize("{{foo & }}").should match_tokens(%w(OPEN ID)) }
-  end
-end
diff --git a/spec/utils.js b/spec/utils.js
new file mode 100644
index 000000000..5eee69e06
--- /dev/null
+++ b/spec/utils.js
@@ -0,0 +1,57 @@
+/*global shouldCompileTo */
+
+describe('utils', function() {
+  describe('#SafeString', function() {
+    it("constructing a safestring from a string and checking its type", function() {
+      var safe = new Handlebars.SafeString("testing 1, 2, 3");
+      safe.should.be.instanceof(Handlebars.SafeString);
+      (safe == "testing 1, 2, 3").should.equal(true, "SafeString is equivalent to its underlying string");
+    });
+
+    it("it should not escape SafeString properties", function() {
+      var name = new Handlebars.SafeString("<em>Sean O&#x27;Malley</em>");
+
+      shouldCompileTo('{{name}}', [{ name: name }], "<em>Sean O&#x27;Malley</em>");
+    });
+  });
+
+  describe('#escapeExpression', function() {
+    it('shouhld escape html', function() {
+      Handlebars.Utils.escapeExpression('foo<&"\'>').should.equal('foo&lt;&amp;&quot;&#x27;&gt;');
+    });
+    it('should not escape SafeString', function() {
+      var string = new Handlebars.SafeString('foo<&"\'>');
+      Handlebars.Utils.escapeExpression(string).should.equal('foo<&"\'>');
+
+    });
+    it('should handle falsy', function() {
+      Handlebars.Utils.escapeExpression('').should.equal('');
+      Handlebars.Utils.escapeExpression(undefined).should.equal('');
+      Handlebars.Utils.escapeExpression(null).should.equal('');
+      Handlebars.Utils.escapeExpression(false).should.equal('');
+
+      Handlebars.Utils.escapeExpression(0).should.equal('0');
+    });
+    it('should handle empty objects', function() {
+      Handlebars.Utils.escapeExpression({}).should.equal({}.toString());
+      Handlebars.Utils.escapeExpression([]).should.equal([].toString());
+    });
+  });
+
+  describe('#isEmpty', function() {
+    it('should not be empty', function() {
+      Handlebars.Utils.isEmpty(undefined).should.equal(true);
+      Handlebars.Utils.isEmpty(null).should.equal(true);
+      Handlebars.Utils.isEmpty(false).should.equal(true);
+      Handlebars.Utils.isEmpty('').should.equal(true);
+      Handlebars.Utils.isEmpty([]).should.equal(true);
+    });
+
+    it('should be empty', function() {
+      Handlebars.Utils.isEmpty(0).should.equal(false);
+      Handlebars.Utils.isEmpty([1]).should.equal(false);
+      Handlebars.Utils.isEmpty('foo').should.equal(false);
+      Handlebars.Utils.isEmpty({bar: 1}).should.equal(false);
+    });
+  });
+});
diff --git a/spec/whitespace-control.js b/spec/whitespace-control.js
new file mode 100644
index 000000000..2088ed8b7
--- /dev/null
+++ b/spec/whitespace-control.js
@@ -0,0 +1,62 @@
+describe('whitespace control', function() {
+  it('should strip whitespace around mustache calls', function() {
+    var hash = {foo: 'bar<'};
+
+    shouldCompileTo(' {{~foo~}} ', hash, 'bar&lt;');
+    shouldCompileTo(' {{~foo}} ', hash, 'bar&lt; ');
+    shouldCompileTo(' {{foo~}} ', hash, ' bar&lt;');
+
+    shouldCompileTo(' {{~&foo~}} ', hash, 'bar<');
+    shouldCompileTo(' {{~{foo}~}} ', hash, 'bar<');
+  });
+
+  describe('blocks', function() {
+    it('should strip whitespace around simple block calls', function() {
+      var hash = {foo: 'bar<'};
+
+      shouldCompileTo(' {{~#if foo~}} bar {{~/if~}} ', hash, 'bar');
+      shouldCompileTo(' {{#if foo~}} bar {{/if~}} ', hash, ' bar ');
+      shouldCompileTo(' {{~#if foo}} bar {{~/if}} ', hash, ' bar ');
+      shouldCompileTo(' {{#if foo}} bar {{/if}} ', hash, '  bar  ');
+    });
+    it('should strip whitespace around inverse block calls', function() {
+      var hash = {};
+
+      shouldCompileTo(' {{~^if foo~}} bar {{~/if~}} ', hash, 'bar');
+      shouldCompileTo(' {{^if foo~}} bar {{/if~}} ', hash, ' bar ');
+      shouldCompileTo(' {{~^if foo}} bar {{~/if}} ', hash, ' bar ');
+      shouldCompileTo(' {{^if foo}} bar {{/if}} ', hash, '  bar  ');
+    });
+    it('should strip whitespace around complex block calls', function() {
+      var hash = {foo: 'bar<'};
+
+      shouldCompileTo('{{#if foo~}} bar {{~^~}} baz {{~/if}}', hash, 'bar');
+      shouldCompileTo('{{#if foo~}} bar {{^~}} baz {{/if}}', hash, 'bar ');
+      shouldCompileTo('{{#if foo}} bar {{~^~}} baz {{~/if}}', hash, ' bar');
+      shouldCompileTo('{{#if foo}} bar {{^~}} baz {{/if}}', hash, ' bar ');
+
+      shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'bar');
+
+      hash = {};
+
+      shouldCompileTo('{{#if foo~}} bar {{~^~}} baz {{~/if}}', hash, 'baz');
+      shouldCompileTo('{{#if foo}} bar {{~^~}} baz {{/if}}', hash, 'baz ');
+      shouldCompileTo('{{#if foo~}} bar {{~^}} baz {{~/if}}', hash, ' baz');
+      shouldCompileTo('{{#if foo~}} bar {{~^}} baz {{/if}}', hash, ' baz ');
+
+      shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'baz');
+    });
+  });
+
+  it('should strip whitespace around partials', function() {
+    shouldCompileToWithPartials('foo {{~> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foobar');
+    shouldCompileToWithPartials('foo {{> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar');
+    shouldCompileToWithPartials('foo {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar ');
+  });
+
+  it('should only strip whitespace once', function() {
+    var hash = {foo: 'bar'};
+
+    shouldCompileTo(' {{~foo~}} {{foo}} {{foo}} ', hash, 'barbar bar ');
+  });
+});
diff --git a/src/handlebars.l b/src/handlebars.l
index aa76eabd3..ddb7fe9ca 100644
--- a/src/handlebars.l
+++ b/src/handlebars.l
@@ -1,49 +1,19 @@
 
 %x mu emu com
 
-%%
+%{
 
-"\\\\"/("{{")                    { yytext = "\\"; return 'CONTENT'; }
-[^\x00]*?/("{{")                 {
-                                   if(yytext.slice(-1) !== "\\") this.begin("mu");
-                                   if(yytext.slice(-1) === "\\") yytext = yytext.substr(0,yyleng-1), this.begin("emu");
-                                   if(yytext) return 'CONTENT';
-                                 }
+function strip(start, end) {
+  return yytext = yytext.substr(start, yyleng-end);
+}
 
-[^\x00]+                         { return 'CONTENT'; }
+%}
 
-<emu>[^\x00]{2,}?/("{{"|<<EOF>>) {
-                                   if(yytext.slice(-1) !== "\\") this.popState();
-                                   if(yytext.slice(-1) === "\\") yytext = yytext.substr(0,yyleng-1);
-                                   return 'CONTENT';
-                                 }
+LEFT_STRIP    "~"
+RIGHT_STRIP   "~"
 
-<com>[\s\S]*?"--}}"              { yytext = yytext.substr(0, yyleng-4); this.popState(); return 'COMMENT'; }
-
-<mu>"{{>"                        { return 'OPEN_PARTIAL'; }
-<mu>"{{#"                        { return 'OPEN_BLOCK'; }
-<mu>"{{/"                        { return 'OPEN_ENDBLOCK'; }
-<mu>"{{^"                        { return 'OPEN_INVERSE'; }
-<mu>"{{"\s*"else"                { return 'OPEN_INVERSE'; }
-<mu>"{{{"                        { return 'OPEN_UNESCAPED'; }
-<mu>"{{&"                        { return 'OPEN'; }
-<mu>"{{!--"                      { this.popState(); this.begin('com'); }
-<mu>"{{!"[\s\S]*?"}}"            { yytext = yytext.substr(3,yyleng-5); this.popState(); return 'COMMENT'; }
-<mu>"{{"                         { return 'OPEN'; }
-
-<mu>"="                          { return 'EQUALS'; }
-<mu>"."/[}\/ ]                   { return 'ID'; }
-<mu>".."                         { return 'ID'; }
-<mu>[\/.]                        { return 'SEP'; }
-<mu>\s+                          { /*ignore whitespace*/ }
-<mu>"}}}"                        { this.popState(); return 'CLOSE_UNESCAPED'; }
-<mu>"}}"                         { this.popState(); return 'CLOSE'; }
-<mu>'"'("\\"["]|[^"])*'"'        { yytext = yytext.substr(1,yyleng-2).replace(/\\"/g,'"'); return 'STRING'; }
-<mu>"'"("\\"[']|[^'])*"'"        { yytext = yytext.substr(1,yyleng-2).replace(/\\'/g,"'"); return 'STRING'; }
-<mu>"@"                          { return 'DATA'; }
-<mu>"true"/[}\s]                 { return 'BOOLEAN'; }
-<mu>"false"/[}\s]                { return 'BOOLEAN'; }
-<mu>\-?[0-9]+/[}\s]              { return 'INTEGER'; }
+LOOKAHEAD           [=~}\s\/.]
+LITERAL_LOOKAHEAD   [~}\s]
 
 /*
 ID is the inverse of control characters.
@@ -54,10 +24,61 @@ Control characters ranges:
   [\[-\^`]      [, \, ], ^, `,                          Exceptions in range: _
   [\{-~]        {, |, }, ~
 */
-<mu>[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]   { return 'ID'; }
+ID    [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
+
+%%
+
+[^\x00]*?/("{{")                 {
+                                   if(yytext.slice(-2) === "\\\\") {
+                                     strip(0,1);
+                                     this.begin("mu");
+                                   } else if(yytext.slice(-1) === "\\") {
+                                     strip(0,1);
+                                     this.begin("emu");
+                                   } else {
+                                     this.begin("mu");
+                                   }
+                                   if(yytext) return 'CONTENT';
+                                 }
+
+[^\x00]+                         return 'CONTENT';
+
+<emu>[^\x00]{2,}?/("{{"|<<EOF>>) {
+                                   if(yytext.slice(-1) !== "\\") this.popState();
+                                   if(yytext.slice(-1) === "\\") strip(0,1);
+                                   return 'CONTENT';
+                                 }
+
+<com>[\s\S]*?"--}}"              strip(0,4); this.popState(); return 'COMMENT';
+
+<mu>"{{"{LEFT_STRIP}?">"         return 'OPEN_PARTIAL';
+<mu>"{{"{LEFT_STRIP}?"#"         return 'OPEN_BLOCK';
+<mu>"{{"{LEFT_STRIP}?"/"         return 'OPEN_ENDBLOCK';
+<mu>"{{"{LEFT_STRIP}?"^"         return 'OPEN_INVERSE';
+<mu>"{{"{LEFT_STRIP}?\s*"else"   return 'OPEN_INVERSE';
+<mu>"{{"{LEFT_STRIP}?"{"         return 'OPEN_UNESCAPED';
+<mu>"{{"{LEFT_STRIP}?"&"         return 'OPEN';
+<mu>"{{!--"                      this.popState(); this.begin('com');
+<mu>"{{!"[\s\S]*?"}}"            strip(3,5); this.popState(); return 'COMMENT';
+<mu>"{{"{LEFT_STRIP}?            return 'OPEN';
+
+<mu>"="                          return 'EQUALS';
+<mu>".."                         return 'ID';
+<mu>"."/{LOOKAHEAD}              return 'ID';
+<mu>[\/.]                        return 'SEP';
+<mu>\s+                          /*ignore whitespace*/
+<mu>"}"{RIGHT_STRIP}?"}}"        this.popState(); return 'CLOSE_UNESCAPED';
+<mu>{RIGHT_STRIP}?"}}"           this.popState(); return 'CLOSE';
+<mu>'"'("\\"["]|[^"])*'"'        yytext = strip(1,2).replace(/\\"/g,'"'); return 'STRING';
+<mu>"'"("\\"[']|[^'])*"'"        yytext = strip(1,2).replace(/\\'/g,"'"); return 'STRING';
+<mu>"@"                          return 'DATA';
+<mu>"true"/{LITERAL_LOOKAHEAD}   return 'BOOLEAN';
+<mu>"false"/{LITERAL_LOOKAHEAD}  return 'BOOLEAN';
+<mu>\-?[0-9]+/{LITERAL_LOOKAHEAD} return 'INTEGER';
 
-<mu>'['[^\]]*']'                 { yytext = yytext.substr(1, yyleng-2); return 'ID'; }
-<mu>.                            { return 'INVALID'; }
+<mu>{ID}                         return 'ID';
 
-<INITIAL,mu><<EOF>>              { return 'EOF'; }
+<mu>'['[^\]]*']'                 yytext = strip(1,2); return 'ID';
+<mu>.                            return 'INVALID';
 
+<INITIAL,mu><<EOF>>              return 'EOF';
diff --git a/src/handlebars.yy b/src/handlebars.yy
index 56b3b7040..0afd2cbbc 100644
--- a/src/handlebars.yy
+++ b/src/handlebars.yy
@@ -1,118 +1,112 @@
 %start root
 
+%ebnf
+
+%{
+
+function stripFlags(open, close) {
+  return {
+    left: open[2] === '~',
+    right: close[0] === '~' || close[1] === '~'
+  };
+}
+
+%}
+
 %%
 
 root
-  : program EOF { return $1; }
+  : statements EOF { return new yy.ProgramNode($1); }
   ;
 
 program
-  : simpleInverse statements { $$ = new yy.ProgramNode([], $2); }
-  | statements simpleInverse statements { $$ = new yy.ProgramNode($1, $3); }
-  | statements simpleInverse { $$ = new yy.ProgramNode($1, []); }
-  | statements { $$ = new yy.ProgramNode($1); }
-  | simpleInverse { $$ = new yy.ProgramNode([], []); }
-  | "" { $$ = new yy.ProgramNode([]); }
+  : simpleInverse statements -> new yy.ProgramNode([], $1, $2)
+  | statements simpleInverse statements -> new yy.ProgramNode($1, $2, $3)
+  | statements simpleInverse -> new yy.ProgramNode($1, $2, [])
+  | statements -> new yy.ProgramNode($1)
+  | simpleInverse -> new yy.ProgramNode([])
+  | "" -> new yy.ProgramNode([])
   ;
 
 statements
-  : statement { $$ = [$1]; }
+  : statement -> [$1]
   | statements statement { $1.push($2); $$ = $1; }
   ;
 
 statement
-  : openInverse program closeBlock { $$ = new yy.BlockNode($1, $2.inverse, $2, $3); }
-  | openBlock program closeBlock { $$ = new yy.BlockNode($1, $2, $2.inverse, $3); }
-  | mustache { $$ = $1; }
-  | partial { $$ = $1; }
-  | CONTENT { $$ = new yy.ContentNode($1); }
-  | COMMENT { $$ = new yy.CommentNode($1); }
+  : openInverse program closeBlock -> new yy.BlockNode($1, $2.inverse, $2, $3)
+  | openBlock program closeBlock -> new yy.BlockNode($1, $2, $2.inverse, $3)
+  | mustache -> $1
+  | partial -> $1
+  | CONTENT -> new yy.ContentNode($1)
+  | COMMENT -> new yy.CommentNode($1)
   ;
 
 openBlock
-  : OPEN_BLOCK inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); }
+  : OPEN_BLOCK inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
   ;
 
 openInverse
-  : OPEN_INVERSE inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); }
+  : OPEN_INVERSE inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
   ;
 
 closeBlock
-  : OPEN_ENDBLOCK path CLOSE { $$ = $2; }
+  : OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: stripFlags($1, $3)}
   ;
 
 mustache
-  : OPEN inMustache CLOSE {
-    // Parsing out the '&' escape token at this level saves ~500 bytes after min due to the removal of one parser node.
-    $$ = new yy.MustacheNode($2[0], $2[1], $1[2] === '&');
-  }
-  | OPEN_UNESCAPED inMustache CLOSE_UNESCAPED { $$ = new yy.MustacheNode($2[0], $2[1], true); }
+  // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node.
+  // This also allows for handler unification as all mustache node instances can utilize the same handler
+  : OPEN inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
+  | OPEN_UNESCAPED inMustache CLOSE_UNESCAPED -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
   ;
 
 
 partial
-  : OPEN_PARTIAL partialName CLOSE { $$ = new yy.PartialNode($2); }
-  | OPEN_PARTIAL partialName path CLOSE { $$ = new yy.PartialNode($2, $3); }
+  : OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3, stripFlags($1, $4))
   ;
 
 simpleInverse
-  : OPEN_INVERSE CLOSE { }
+  : OPEN_INVERSE CLOSE -> stripFlags($1, $2)
   ;
 
 inMustache
-  : path params hash { $$ = [[$1].concat($2), $3]; }
-  | path params { $$ = [[$1].concat($2), null]; }
-  | path hash { $$ = [[$1], $2]; }
-  | path { $$ = [[$1], null]; }
-  | dataName { $$ = [[$1], null]; }
-  ;
-
-params
-  : params param { $1.push($2); $$ = $1; }
-  | param { $$ = [$1]; }
+  : path param* hash? -> [[$1].concat($2), $3]
+  | dataName -> [[$1], null]
   ;
 
 param
-  : path { $$ = $1; }
-  | STRING { $$ = new yy.StringNode($1); }
-  | INTEGER { $$ = new yy.IntegerNode($1); }
-  | BOOLEAN { $$ = new yy.BooleanNode($1); }
-  | dataName { $$ = $1; }
+  : path -> $1
+  | STRING -> new yy.StringNode($1)
+  | INTEGER -> new yy.IntegerNode($1)
+  | BOOLEAN -> new yy.BooleanNode($1)
+  | dataName -> $1
   ;
 
 hash
-  : hashSegments { $$ = new yy.HashNode($1); }
-  ;
-
-hashSegments
-  : hashSegments hashSegment { $1.push($2); $$ = $1; }
-  | hashSegment { $$ = [$1]; }
+  : hashSegment+ -> new yy.HashNode($1)
   ;
 
 hashSegment
-  : ID EQUALS path { $$ = [$1, $3]; }
-  | ID EQUALS STRING { $$ = [$1, new yy.StringNode($3)]; }
-  | ID EQUALS INTEGER { $$ = [$1, new yy.IntegerNode($3)]; }
-  | ID EQUALS BOOLEAN { $$ = [$1, new yy.BooleanNode($3)]; }
-  | ID EQUALS dataName { $$ = [$1, $3]; }
+  : ID EQUALS param -> [$1, $3]
   ;
 
 partialName
-  : path { $$ = new yy.PartialNameNode($1); }
-  | STRING { $$ = new yy.PartialNameNode(new yy.StringNode($1)); }
-  | INTEGER { $$ = new yy.PartialNameNode(new yy.IntegerNode($1)); }
+  : path -> new yy.PartialNameNode($1)
+  | STRING -> new yy.PartialNameNode(new yy.StringNode($1))
+  | INTEGER -> new yy.PartialNameNode(new yy.IntegerNode($1))
   ;
 
 dataName
-  : DATA path { $$ = new yy.DataNode($2); }
+  : DATA path -> new yy.DataNode($2)
   ;
 
 path
-  : pathSegments { $$ = new yy.IdNode($1); }
+  : pathSegments -> new yy.IdNode($1)
   ;
 
 pathSegments
   : pathSegments SEP ID { $1.push({part: $3, separator: $2}); $$ = $1; }
-  | ID { $$ = [{part: $1}]; }
+  | ID -> [{part: $1}]
   ;
 
diff --git a/src/parser-prefix.js b/src/parser-prefix.js
deleted file mode 100644
index 42345d6f6..000000000
--- a/src/parser-prefix.js
+++ /dev/null
@@ -1 +0,0 @@
-// BEGIN(BROWSER)
diff --git a/src/parser-suffix.js b/src/parser-suffix.js
index fc8df1399..6e4aa20d6 100644
--- a/src/parser-suffix.js
+++ b/src/parser-suffix.js
@@ -1,4 +1 @@
-
-// END(BROWSER)
-
-module.exports = handlebars;
+export default handlebars;
diff --git a/tasks/metrics.js b/tasks/metrics.js
new file mode 100644
index 000000000..c4a202b15
--- /dev/null
+++ b/tasks/metrics.js
@@ -0,0 +1,63 @@
+var _ = require('underscore'),
+    async = require('async'),
+    git = require('./util/git'),
+    Keen = require('keen.io'),
+    metrics = require('../bench');
+
+module.exports = function(grunt) {
+  grunt.registerTask('metrics', function() {
+    var done = this.async(),
+        execName = grunt.option('name'),
+        events = {},
+
+        projectId = process.env.KEEN_PROJECTID,
+        writeKey = process.env.KEEN_WRITEKEY,
+        keen;
+
+    if (!execName && projectId && writeKey) {
+      keen = Keen.configure({
+        projectId: projectId,
+        writeKey: writeKey
+      });
+    }
+
+    async.each(_.keys(metrics), function(name, complete) {
+        if (/^_/.test(name) || (execName && name !== execName)) {
+          return complete();
+        }
+
+        metrics[name](grunt, function(data) {
+          events[name] = data;
+          complete();
+        });
+      },
+      function() {
+        if (!keen) {
+          return done();
+        }
+
+        emit(keen, events, function(err, res) {
+          if (err) {
+            throw err;
+          }
+
+          grunt.log.writeln('Metrics recorded.');
+          done();
+        });
+      });
+  });
+};
+function emit(keen, collections, callback) {
+  git.commitInfo(function(err, info) {
+    _.each(collections, function(collection) {
+      _.each(collection, function(event) {
+        if (info.tagName) {
+          event.tag = info.tagName;
+        }
+        event.sha = info.head;
+      });
+    });
+
+    keen.addEvents(collections, callback);
+  });
+}
diff --git a/tasks/packager.js b/tasks/packager.js
new file mode 100644
index 000000000..f01eef565
--- /dev/null
+++ b/tasks/packager.js
@@ -0,0 +1,13 @@
+module.exports = function(grunt) {
+  grunt.registerMultiTask('packager', 'Transpiles scripts written using ES6 to ES5.', function() {
+    // Execute in here to prevent traceur private var blowup
+    var Packager = require('es6-module-packager').default,
+        fs = require('fs');
+
+    var options = this.options();
+    this.files.forEach(function(file) {
+      var packager = new Packager(file.src[0], {export: options.export});
+      fs.writeFileSync(file.dest, packager.toLocals());
+    });
+  });
+};
diff --git a/tasks/parser.js b/tasks/parser.js
new file mode 100644
index 000000000..b1c1c0f23
--- /dev/null
+++ b/tasks/parser.js
@@ -0,0 +1,23 @@
+var childProcess = require('child_process');
+
+module.exports = function(grunt) {
+  grunt.registerTask('parser', 'Generate jison parser.', function() {
+    var done = this.async();
+
+    var child = childProcess.spawn('./node_modules/.bin/jison', ['-m', 'js', 'src/handlebars.yy', 'src/handlebars.l'], {stdio: 'inherit'});
+    child.on('exit', function(code) {
+      if (code != 0) {
+        grunt.fatal('Jison failure: ' + code);
+        done();
+        return;
+      }
+
+      var src = ['handlebars.js', 'src/parser-suffix.js'].map(grunt.file.read).join('');
+      grunt.file.delete('handlebars.js');
+
+      grunt.file.write('lib/handlebars/compiler/parser.js', src);
+      grunt.log.writeln('Parser "lib/handlebars/compiler/parser.js" created.');
+      done();
+    });
+  });
+};
diff --git a/tasks/publish.js b/tasks/publish.js
new file mode 100644
index 000000000..68b3157aa
--- /dev/null
+++ b/tasks/publish.js
@@ -0,0 +1,83 @@
+var _ = require('underscore'),
+    async = require('async'),
+    AWS = require('aws-sdk'),
+    git = require('./util/git'),
+    semver = require('semver');
+
+module.exports = function(grunt) {
+  grunt.registerTask('publish:latest', function() {
+    var done = this.async();
+
+    git.debug(function(remotes, branches) {
+      grunt.log.writeln('remotes: ' + remotes);
+      grunt.log.writeln('branches: ' + branches);
+
+      git.commitInfo(function(err, info) {
+        grunt.log.writeln('tag: ' + info.tagName);
+
+        if (info.isMaster) {
+          initSDK();
+
+          var files = ['-latest', '-' + info.head];
+          if (info.tagName && semver.valid(info.tagName)) {
+            files.push('-' + info.tagName);
+          }
+
+          publish(fileMap(files), done);
+        } else {
+          // Silently ignore for branches
+          done();
+        }
+      });
+    });
+  });
+  grunt.registerTask('publish:version', function() {
+    var done = this.async();
+    initSDK();
+
+    git.commitInfo(function(err, info) {
+      if (!info.tagName) {
+        throw new Error('The current commit must be tagged');
+      }
+      publish(fileMap(['-' + info.tagName]), done);
+    });
+  });
+
+  function initSDK() {
+    var bucket = process.env.S3_BUCKET_NAME,
+        key = process.env.S3_ACCESS_KEY_ID,
+        secret = process.env.S3_SECRET_ACCESS_KEY;
+
+    if (!bucket || !key || !secret) {
+      throw new Error('Missing S3 config values');
+    }
+
+    AWS.config.update({accessKeyId: key, secretAccessKey: secret});
+  }
+  function publish(files, callback) {
+    var s3 = new AWS.S3(),
+        bucket = process.env.S3_BUCKET_NAME;
+
+    async.forEach(_.keys(files), function(file, callback) {
+        var params = {Bucket: bucket, Key: file, Body: grunt.file.read(files[file])};
+        s3.putObject(params, function(err, data) {
+          if (err) {
+            throw err;
+          } else {
+            grunt.log.writeln('Published ' + file + ' to build server.');
+            callback();
+          }
+        });
+      },
+      callback);
+  }
+  function fileMap(suffixes) {
+    var map = {};
+    _.each(['handlebars.js', 'handlebars.min.js', 'handlebars.runtime.js', 'handlebars.runtime.min.js'], function(file) {
+      _.each(suffixes, function(suffix) {
+        map[file.replace(/\.js$/, suffix + '.js')] = 'dist/' + file;
+      });
+    });
+    return map;
+  }
+};
diff --git a/tasks/util/git.js b/tasks/util/git.js
new file mode 100644
index 000000000..dc57c91b6
--- /dev/null
+++ b/tasks/util/git.js
@@ -0,0 +1,100 @@
+var childProcess = require('child_process');
+
+module.exports = {
+  debug: function(callback) {
+    childProcess.exec('git remote -v', {}, function(err, remotes) {
+      if (err) {
+        throw new Error('git.remote: ' + err.message);
+      }
+
+      childProcess.exec('git branch -a', {}, function(err, branches) {
+        if (err) {
+          throw new Error('git.branch: ' + err.message);
+        }
+
+        callback(remotes, branches);
+      });
+    });
+  },
+  clean: function(callback) {
+    childProcess.exec('git diff-index --name-only HEAD --', {}, function(err, stdout) {
+      callback(undefined, !err && !stdout);
+    });
+  },
+
+  commitInfo: function(callback) {
+    module.exports.head(function(err, headSha) {
+      module.exports.master(function(err, masterSha) {
+        module.exports.tagName(function(err, tagName) {
+          callback(undefined, {
+            head: headSha,
+            master: masterSha,
+            tagName: tagName,
+            isMaster: headSha === masterSha
+          });
+        });
+      });
+    });
+  },
+
+  head: function(callback) {
+    childProcess.exec('git rev-parse --short HEAD', {}, function(err, stdout) {
+      if (err) {
+        throw new Error('git.head: ' + err.message);
+      }
+
+      callback(undefined, stdout.trim());
+    });
+  },
+  master: function(callback) {
+    childProcess.exec('git rev-parse --short origin/master', {}, function(err, stdout) {
+      // This will error if master was not checked out but in this case we know we are not master
+      // so we can ignore.
+      if (err && !/Needed a single revision/.test(err.message)) {
+        throw new Error('git.master: ' + err.message);
+      }
+
+      callback(undefined, stdout.trim());
+    });
+  },
+
+  add: function(path, callback) {
+    childProcess.exec('git add -f ' + path, {}, function(err, stdout) {
+      if (err) {
+        throw new Error('git.add: ' + err.message);
+      }
+
+      callback();
+    });
+  },
+  commit: function(name, callback) {
+    childProcess.exec('git commit --message=' + name, {}, function(err, stdout) {
+      if (err) {
+        throw new Error('git.commit: ' + err.message);
+      }
+
+      callback();
+    });
+  },
+  tag: function(name, callback) {
+    childProcess.exec('git tag -a --message=' + name + ' ' + name, {}, function(err, stdout, stderr) {
+      if (err) {
+        throw new Error('git.tag: ' + err.message);
+        throw err;
+      }
+
+      callback();
+    });
+  },
+  tagName: function(callback) {
+    childProcess.exec('git tag -l --points-at HEAD', {}, function(err, stdout) {
+      if (err) {
+        throw new Error('git.tagName: ' + err.message);
+      }
+
+      var tags = stdout.trim().split(/\n/),
+          versionTags = tags.filter(function(tag) { return /^v/.test(tag); });
+      callback(undefined, versionTags[0] || tags[0]);
+    });
+  }
+};
diff --git a/tasks/version.js b/tasks/version.js
new file mode 100644
index 000000000..c622a22fd
--- /dev/null
+++ b/tasks/version.js
@@ -0,0 +1,41 @@
+var async = require('async'),
+    git = require('./util/git'),
+    semver = require('semver');
+
+module.exports = function(grunt) {
+  grunt.registerTask('version', 'Updates the current release version', function() {
+    var done = this.async(),
+        pkg = grunt.config('pkg'),
+        version = grunt.option('ver');
+
+    if (!semver.valid(version)) {
+      throw new Error('Must provide a version number (Ex: --ver=1.0.0):\n\t' + version + '\n\n');
+    }
+
+    pkg.version = version;
+    grunt.config('pkg', pkg);
+
+    grunt.log.writeln('Updating to version ' + version);
+
+    async.each([
+        ['lib/handlebars/base.js', /var VERSION = "(.*)";/, 'var VERSION = "' + version + '";'],
+        ['components/bower.json', /"version":.*/, '"version": "' + version + '",'],
+        ['components/handlebars.js.nuspec', /<version>.*<\/version>/, '<version>' + version + '</version>']
+      ],
+      function(args, callback) {
+        replace.apply(undefined, args);
+        grunt.log.writeln('    - ' + args[0]);
+        git.add(args[0], callback);
+      },
+      function() {
+        grunt.task.run(['default']);
+        done();
+      });
+  });
+
+  function replace(path, regex, replace) {
+    var content = grunt.file.read(path);
+    content = content.replace(regex, replace);
+    grunt.file.write(path, content);
+  }
+};
