diff --git a/.eslintignore b/.eslintignore
index 714d84019..424ad0b0e 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -12,7 +12,6 @@ node_modules
 .nyc_output
 
 # Generated files
-lib/handlebars/compiler/parser.js
 /coverage/
 /dist/
 /integration-testing/*/dist/
diff --git a/.gitignore b/.gitignore
index 97e44f885..7bccd17e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,8 +13,7 @@ node_modules
 .nyc_output
 
 # Generated files
-lib/handlebars/compiler/parser.js
 /coverage/
 /dist/
 /integration-testing/*/dist/
-/spec/tmp/*
\ No newline at end of file
+/spec/tmp/*
diff --git a/.prettierignore b/.prettierignore
index 724620669..f160a7f51 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -12,7 +12,6 @@ node_modules
 .nyc_output
 
 # Generated files
-lib/handlebars/compiler/parser.js
 /coverage/
 /dist/
 /integration-testing/*/dist/
diff --git a/Gruntfile.js b/Gruntfile.js
index 8b9538320..deacdc432 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -3,12 +3,7 @@ module.exports = function(grunt) {
   grunt.initConfig({
     pkg: grunt.file.readJSON('package.json'),
 
-    clean: [
-      'tmp',
-      'dist',
-      'lib/handlebars/compiler/parser.js',
-      'integration-testing/**/node_modules'
-    ],
+    clean: ['tmp', 'dist', 'integration-testing/**/node_modules'],
 
     copy: {
       dist: {
@@ -198,7 +193,7 @@ module.exports = function(grunt) {
   this.registerTask(
     'build',
     'Builds a distributable version of the current project',
-    ['parser', 'node', 'globals']
+    ['node', 'globals']
   );
 
   this.registerTask('node', ['babel:cjs']);
diff --git a/lib/handlebars.js b/lib/handlebars.js
index c4f8fc781..64a288c35 100644
--- a/lib/handlebars.js
+++ b/lib/handlebars.js
@@ -1,15 +1,16 @@
+import {
+  parser as Parser,
+  parse,
+  parseWithoutProcessing,
+  Visitor
+} from '@handlebars/parser';
+
 import runtime from './handlebars.runtime';
 
 // Compiler imports
 import AST from './handlebars/compiler/ast';
-import {
-  parser as Parser,
-  parse,
-  parseWithoutProcessing
-} from './handlebars/compiler/base';
 import { Compiler, compile, precompile } from './handlebars/compiler/compiler';
 import JavaScriptCompiler from './handlebars/compiler/javascript-compiler';
-import Visitor from './handlebars/compiler/visitor';
 
 import noConflict from './handlebars/no-conflict';
 
diff --git a/lib/handlebars.runtime.js b/lib/handlebars.runtime.js
index 3d05b5448..6b6270b2f 100644
--- a/lib/handlebars.runtime.js
+++ b/lib/handlebars.runtime.js
@@ -1,9 +1,9 @@
+import { Exception } from '@handlebars/parser';
 import * as 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';
 import * as Utils from './handlebars/utils';
 import * as runtime from './handlebars/runtime';
 
diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js
index 753c9a466..de4f18d43 100644
--- a/lib/handlebars/base.js
+++ b/lib/handlebars/base.js
@@ -1,5 +1,5 @@
+import { Exception } from '@handlebars/parser';
 import { createFrame, extend, toString } from './utils';
-import Exception from './exception';
 import { registerDefaultHelpers } from './helpers';
 import { registerDefaultDecorators } from './decorators';
 import logger from './logger';
diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js
deleted file mode 100644
index 1dd5af1a4..000000000
--- a/lib/handlebars/compiler/base.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import parser from './parser';
-import WhitespaceControl from './whitespace-control';
-import * as Helpers from './helpers';
-import { extend } from '../utils';
-
-export { parser };
-
-let yy = {};
-extend(yy, Helpers);
-
-export function parseWithoutProcessing(input, options) {
-  // Just return if an already-compiled AST was passed in.
-  if (input.type === 'Program') {
-    return input;
-  }
-
-  parser.yy = yy;
-
-  // Altering the shared object here, but this is ok as parser is a sync operation
-  yy.locInfo = function(locInfo) {
-    return new yy.SourceLocation(options && options.srcName, locInfo);
-  };
-
-  let ast = parser.parse(input);
-
-  return ast;
-}
-
-export function parse(input, options) {
-  let ast = parseWithoutProcessing(input, options);
-  let strip = new WhitespaceControl(options);
-
-  return strip.accept(ast);
-}
diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js
index d759dd4d3..83a13ab51 100644
--- a/lib/handlebars/compiler/compiler.js
+++ b/lib/handlebars/compiler/compiler.js
@@ -1,6 +1,6 @@
 /* eslint-disable new-cap */
 
-import Exception from '../exception';
+import { Exception } from '@handlebars/parser';
 import { isArray, indexOf, extend } from '../utils';
 import AST from './ast';
 
diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js
deleted file mode 100644
index 27033a1d5..000000000
--- a/lib/handlebars/compiler/helpers.js
+++ /dev/null
@@ -1,219 +0,0 @@
-import Exception from '../exception';
-
-function validateClose(open, close) {
-  close = close.path ? close.path.original : close;
-
-  if (open.path.original !== close) {
-    let errorNode = { loc: open.path.loc };
-
-    throw new Exception(
-      open.path.original + " doesn't match " + close,
-      errorNode
-    );
-  }
-}
-
-export function SourceLocation(source, locInfo) {
-  this.source = source;
-  this.start = {
-    line: locInfo.first_line,
-    column: locInfo.first_column
-  };
-  this.end = {
-    line: locInfo.last_line,
-    column: locInfo.last_column
-  };
-}
-
-export function id(token) {
-  if (/^\[.*\]$/.test(token)) {
-    return token.substring(1, token.length - 1);
-  } else {
-    return token;
-  }
-}
-
-export function stripFlags(open, close) {
-  return {
-    open: open.charAt(2) === '~',
-    close: close.charAt(close.length - 3) === '~'
-  };
-}
-
-export function stripComment(comment) {
-  return comment.replace(/^\{\{~?!-?-?/, '').replace(/-?-?~?\}\}$/, '');
-}
-
-export function preparePath(data, parts, loc) {
-  loc = this.locInfo(loc);
-
-  let original = data ? '@' : '',
-    dig = [],
-    depth = 0;
-
-  for (let i = 0, l = parts.length; i < l; i++) {
-    let part = parts[i].part,
-      // If we have [] syntax then we do not treat path references as operators,
-      // i.e. foo.[this] resolves to approximately context.foo['this']
-      isLiteral = parts[i].original !== part;
-    original += (parts[i].separator || '') + part;
-
-    if (!isLiteral && (part === '..' || part === '.' || part === 'this')) {
-      if (dig.length > 0) {
-        throw new Exception('Invalid path: ' + original, { loc });
-      } else if (part === '..') {
-        depth++;
-      }
-    } else {
-      dig.push(part);
-    }
-  }
-
-  return {
-    type: 'PathExpression',
-    data,
-    depth,
-    parts: dig,
-    original,
-    loc
-  };
-}
-
-export function prepareMustache(path, params, hash, open, strip, locInfo) {
-  // Must use charAt to support IE pre-10
-  let escapeFlag = open.charAt(3) || open.charAt(2),
-    escaped = escapeFlag !== '{' && escapeFlag !== '&';
-
-  let decorator = /\*/.test(open);
-  return {
-    type: decorator ? 'Decorator' : 'MustacheStatement',
-    path,
-    params,
-    hash,
-    escaped,
-    strip,
-    loc: this.locInfo(locInfo)
-  };
-}
-
-export function prepareRawBlock(openRawBlock, contents, close, locInfo) {
-  validateClose(openRawBlock, close);
-
-  locInfo = this.locInfo(locInfo);
-  let program = {
-    type: 'Program',
-    body: contents,
-    strip: {},
-    loc: locInfo
-  };
-
-  return {
-    type: 'BlockStatement',
-    path: openRawBlock.path,
-    params: openRawBlock.params,
-    hash: openRawBlock.hash,
-    program,
-    openStrip: {},
-    inverseStrip: {},
-    closeStrip: {},
-    loc: locInfo
-  };
-}
-
-export function prepareBlock(
-  openBlock,
-  program,
-  inverseAndProgram,
-  close,
-  inverted,
-  locInfo
-) {
-  if (close && close.path) {
-    validateClose(openBlock, close);
-  }
-
-  let decorator = /\*/.test(openBlock.open);
-
-  program.blockParams = openBlock.blockParams;
-
-  let inverse, inverseStrip;
-
-  if (inverseAndProgram) {
-    if (decorator) {
-      throw new Exception(
-        'Unexpected inverse block on decorator',
-        inverseAndProgram
-      );
-    }
-
-    if (inverseAndProgram.chain) {
-      inverseAndProgram.program.body[0].closeStrip = close.strip;
-    }
-
-    inverseStrip = inverseAndProgram.strip;
-    inverse = inverseAndProgram.program;
-  }
-
-  if (inverted) {
-    inverted = inverse;
-    inverse = program;
-    program = inverted;
-  }
-
-  return {
-    type: decorator ? 'DecoratorBlock' : 'BlockStatement',
-    path: openBlock.path,
-    params: openBlock.params,
-    hash: openBlock.hash,
-    program,
-    inverse,
-    openStrip: openBlock.strip,
-    inverseStrip,
-    closeStrip: close && close.strip,
-    loc: this.locInfo(locInfo)
-  };
-}
-
-export function prepareProgram(statements, loc) {
-  if (!loc && statements.length) {
-    const firstLoc = statements[0].loc,
-      lastLoc = statements[statements.length - 1].loc;
-
-    /* istanbul ignore else */
-    if (firstLoc && lastLoc) {
-      loc = {
-        source: firstLoc.source,
-        start: {
-          line: firstLoc.start.line,
-          column: firstLoc.start.column
-        },
-        end: {
-          line: lastLoc.end.line,
-          column: lastLoc.end.column
-        }
-      };
-    }
-  }
-
-  return {
-    type: 'Program',
-    body: statements,
-    strip: {},
-    loc: loc
-  };
-}
-
-export function preparePartialBlock(open, program, close, locInfo) {
-  validateClose(open, close);
-
-  return {
-    type: 'PartialBlockStatement',
-    name: open.path,
-    params: open.params,
-    hash: open.hash,
-    program,
-    openStrip: open.strip,
-    closeStrip: close && close.strip,
-    loc: this.locInfo(locInfo)
-  };
-}
diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js
index 7b48e9a3f..47c498e12 100644
--- a/lib/handlebars/compiler/javascript-compiler.js
+++ b/lib/handlebars/compiler/javascript-compiler.js
@@ -1,5 +1,5 @@
+import { Exception } from '@handlebars/parser';
 import { COMPILER_REVISION, REVISION_CHANGES } from '../base';
-import Exception from '../exception';
 import { isArray } from '../utils';
 import CodeGen from './code-gen';
 
diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js
deleted file mode 100644
index e087806cd..000000000
--- a/lib/handlebars/compiler/printer.js
+++ /dev/null
@@ -1,178 +0,0 @@
-/* eslint-disable new-cap */
-import Visitor from './visitor';
-
-export function print(ast) {
-  return new PrintVisitor().accept(ast);
-}
-
-export function PrintVisitor() {
-  this.padding = 0;
-}
-
-PrintVisitor.prototype = new Visitor();
-
-PrintVisitor.prototype.pad = function(string) {
-  let out = '';
-
-  for (let i = 0, l = this.padding; i < l; i++) {
-    out += '  ';
-  }
-
-  out += string + '\n';
-  return out;
-};
-
-PrintVisitor.prototype.Program = function(program) {
-  let out = '',
-    body = program.body,
-    i,
-    l;
-
-  if (program.blockParams) {
-    let blockParams = 'BLOCK PARAMS: [';
-    for (i = 0, l = program.blockParams.length; i < l; i++) {
-      blockParams += ' ' + program.blockParams[i];
-    }
-    blockParams += ' ]';
-    out += this.pad(blockParams);
-  }
-
-  for (i = 0, l = body.length; i < l; i++) {
-    out += this.accept(body[i]);
-  }
-
-  this.padding--;
-
-  return out;
-};
-
-PrintVisitor.prototype.MustacheStatement = function(mustache) {
-  return this.pad('{{ ' + this.SubExpression(mustache) + ' }}');
-};
-PrintVisitor.prototype.Decorator = function(mustache) {
-  return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}');
-};
-
-PrintVisitor.prototype.BlockStatement = PrintVisitor.prototype.DecoratorBlock = function(
-  block
-) {
-  let out = '';
-
-  out += this.pad(
-    (block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:'
-  );
-  this.padding++;
-  out += this.pad(this.SubExpression(block));
-  if (block.program) {
-    out += this.pad('PROGRAM:');
-    this.padding++;
-    out += this.accept(block.program);
-    this.padding--;
-  }
-  if (block.inverse) {
-    if (block.program) {
-      this.padding++;
-    }
-    out += this.pad('{{^}}');
-    this.padding++;
-    out += this.accept(block.inverse);
-    this.padding--;
-    if (block.program) {
-      this.padding--;
-    }
-  }
-  this.padding--;
-
-  return out;
-};
-
-PrintVisitor.prototype.PartialStatement = function(partial) {
-  let content = 'PARTIAL:' + partial.name.original;
-  if (partial.params[0]) {
-    content += ' ' + this.accept(partial.params[0]);
-  }
-  if (partial.hash) {
-    content += ' ' + this.accept(partial.hash);
-  }
-  return this.pad('{{> ' + content + ' }}');
-};
-PrintVisitor.prototype.PartialBlockStatement = function(partial) {
-  let content = 'PARTIAL BLOCK:' + partial.name.original;
-  if (partial.params[0]) {
-    content += ' ' + this.accept(partial.params[0]);
-  }
-  if (partial.hash) {
-    content += ' ' + this.accept(partial.hash);
-  }
-
-  content += ' ' + this.pad('PROGRAM:');
-  this.padding++;
-  content += this.accept(partial.program);
-  this.padding--;
-
-  return this.pad('{{> ' + content + ' }}');
-};
-
-PrintVisitor.prototype.ContentStatement = function(content) {
-  return this.pad("CONTENT[ '" + content.value + "' ]");
-};
-
-PrintVisitor.prototype.CommentStatement = function(comment) {
-  return this.pad("{{! '" + comment.value + "' }}");
-};
-
-PrintVisitor.prototype.SubExpression = function(sexpr) {
-  let params = sexpr.params,
-    paramStrings = [],
-    hash;
-
-  for (let i = 0, l = params.length; i < l; i++) {
-    paramStrings.push(this.accept(params[i]));
-  }
-
-  params = '[' + paramStrings.join(', ') + ']';
-
-  hash = sexpr.hash ? ' ' + this.accept(sexpr.hash) : '';
-
-  return this.accept(sexpr.path) + ' ' + params + hash;
-};
-
-PrintVisitor.prototype.PathExpression = function(id) {
-  let path = id.parts.join('/');
-  return (id.data ? '@' : '') + 'PATH:' + path;
-};
-
-PrintVisitor.prototype.StringLiteral = function(string) {
-  return '"' + string.value + '"';
-};
-
-PrintVisitor.prototype.NumberLiteral = function(number) {
-  return 'NUMBER{' + number.value + '}';
-};
-
-PrintVisitor.prototype.BooleanLiteral = function(bool) {
-  return 'BOOLEAN{' + bool.value + '}';
-};
-
-PrintVisitor.prototype.UndefinedLiteral = function() {
-  return 'UNDEFINED';
-};
-
-PrintVisitor.prototype.NullLiteral = function() {
-  return 'NULL';
-};
-
-PrintVisitor.prototype.Hash = function(hash) {
-  let pairs = hash.pairs,
-    joinedPairs = [];
-
-  for (let i = 0, l = pairs.length; i < l; i++) {
-    joinedPairs.push(this.accept(pairs[i]));
-  }
-
-  return 'HASH{' + joinedPairs.join(', ') + '}';
-};
-PrintVisitor.prototype.HashPair = function(pair) {
-  return pair.key + '=' + this.accept(pair.value);
-};
-/* eslint-enable new-cap */
diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js
deleted file mode 100644
index 76bb01f12..000000000
--- a/lib/handlebars/compiler/visitor.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import Exception from '../exception';
-
-function Visitor() {
-  this.parents = [];
-}
-
-Visitor.prototype = {
-  constructor: Visitor,
-  mutating: false,
-
-  // Visits a given value. If mutating, will replace the value if necessary.
-  acceptKey: function(node, name) {
-    let value = this.accept(node[name]);
-    if (this.mutating) {
-      // Hacky sanity check: This may have a few false positives for type for the helper
-      // methods but will generally do the right thing without a lot of overhead.
-      if (value && !Visitor.prototype[value.type]) {
-        throw new Exception(
-          'Unexpected node type "' +
-            value.type +
-            '" found when accepting ' +
-            name +
-            ' on ' +
-            node.type
-        );
-      }
-      node[name] = value;
-    }
-  },
-
-  // Performs an accept operation with added sanity check to ensure
-  // required keys are not removed.
-  acceptRequired: function(node, name) {
-    this.acceptKey(node, name);
-
-    if (!node[name]) {
-      throw new Exception(node.type + ' requires ' + name);
-    }
-  },
-
-  // Traverses a given array. If mutating, empty respnses will be removed
-  // for child elements.
-  acceptArray: function(array) {
-    for (let i = 0, l = array.length; i < l; i++) {
-      this.acceptKey(array, i);
-
-      if (!array[i]) {
-        array.splice(i, 1);
-        i--;
-        l--;
-      }
-    }
-  },
-
-  accept: function(object) {
-    if (!object) {
-      return;
-    }
-
-    /* istanbul ignore next: Sanity code */
-    if (!this[object.type]) {
-      throw new Exception('Unknown type: ' + object.type, object);
-    }
-
-    if (this.current) {
-      this.parents.unshift(this.current);
-    }
-    this.current = object;
-
-    let ret = this[object.type](object);
-
-    this.current = this.parents.shift();
-
-    if (!this.mutating || ret) {
-      return ret;
-    } else if (ret !== false) {
-      return object;
-    }
-  },
-
-  Program: function(program) {
-    this.acceptArray(program.body);
-  },
-
-  MustacheStatement: visitSubExpression,
-  Decorator: visitSubExpression,
-
-  BlockStatement: visitBlock,
-  DecoratorBlock: visitBlock,
-
-  PartialStatement: visitPartial,
-  PartialBlockStatement: function(partial) {
-    visitPartial.call(this, partial);
-
-    this.acceptKey(partial, 'program');
-  },
-
-  ContentStatement: function(/* content */) {},
-  CommentStatement: function(/* comment */) {},
-
-  SubExpression: visitSubExpression,
-
-  PathExpression: function(/* path */) {},
-
-  StringLiteral: function(/* string */) {},
-  NumberLiteral: function(/* number */) {},
-  BooleanLiteral: function(/* bool */) {},
-  UndefinedLiteral: function(/* literal */) {},
-  NullLiteral: function(/* literal */) {},
-
-  Hash: function(hash) {
-    this.acceptArray(hash.pairs);
-  },
-  HashPair: function(pair) {
-    this.acceptRequired(pair, 'value');
-  }
-};
-
-function visitSubExpression(mustache) {
-  this.acceptRequired(mustache, 'path');
-  this.acceptArray(mustache.params);
-  this.acceptKey(mustache, 'hash');
-}
-function visitBlock(block) {
-  visitSubExpression.call(this, block);
-
-  this.acceptKey(block, 'program');
-  this.acceptKey(block, 'inverse');
-}
-function visitPartial(partial) {
-  this.acceptRequired(partial, 'name');
-  this.acceptArray(partial.params);
-  this.acceptKey(partial, 'hash');
-}
-
-export default Visitor;
diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js
deleted file mode 100644
index c437bf013..000000000
--- a/lib/handlebars/compiler/whitespace-control.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import Visitor from './visitor';
-
-function WhitespaceControl(options = {}) {
-  this.options = options;
-}
-WhitespaceControl.prototype = new Visitor();
-
-WhitespaceControl.prototype.Program = function(program) {
-  const doStandalone = !this.options.ignoreStandalone;
-
-  let isRoot = !this.isRootSeen;
-  this.isRootSeen = true;
-
-  let body = program.body;
-  for (let i = 0, l = body.length; i < l; i++) {
-    let current = body[i],
-      strip = this.accept(current);
-
-    if (!strip) {
-      continue;
-    }
-
-    let _isPrevWhitespace = isPrevWhitespace(body, i, isRoot),
-      _isNextWhitespace = isNextWhitespace(body, i, isRoot),
-      openStandalone = strip.openStandalone && _isPrevWhitespace,
-      closeStandalone = strip.closeStandalone && _isNextWhitespace,
-      inlineStandalone =
-        strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace;
-
-    if (strip.close) {
-      omitRight(body, i, true);
-    }
-    if (strip.open) {
-      omitLeft(body, i, true);
-    }
-
-    if (doStandalone && inlineStandalone) {
-      omitRight(body, i);
-
-      if (omitLeft(body, i)) {
-        // If we are on a standalone node, save the indent info for partials
-        if (current.type === 'PartialStatement') {
-          // Pull out the whitespace from the final line
-          current.indent = /([ \t]+$)/.exec(body[i - 1].original)[1];
-        }
-      }
-    }
-    if (doStandalone && openStandalone) {
-      omitRight((current.program || current.inverse).body);
-
-      // Strip out the previous content node if it's whitespace only
-      omitLeft(body, i);
-    }
-    if (doStandalone && closeStandalone) {
-      // Always strip the next node
-      omitRight(body, i);
-
-      omitLeft((current.inverse || current.program).body);
-    }
-  }
-
-  return program;
-};
-
-WhitespaceControl.prototype.BlockStatement = WhitespaceControl.prototype.DecoratorBlock = WhitespaceControl.prototype.PartialBlockStatement = function(
-  block
-) {
-  this.accept(block.program);
-  this.accept(block.inverse);
-
-  // Find the inverse program that is involed with whitespace stripping.
-  let program = block.program || block.inverse,
-    inverse = block.program && block.inverse,
-    firstInverse = inverse,
-    lastInverse = inverse;
-
-  if (inverse && inverse.chained) {
-    firstInverse = inverse.body[0].program;
-
-    // Walk the inverse chain to find the last inverse that is actually in the chain.
-    while (lastInverse.chained) {
-      lastInverse = lastInverse.body[lastInverse.body.length - 1].program;
-    }
-  }
-
-  let strip = {
-    open: block.openStrip.open,
-    close: block.closeStrip.close,
-
-    // Determine the standalone candiacy. Basically flag our content as being possibly standalone
-    // so our parent can determine if we actually are standalone
-    openStandalone: isNextWhitespace(program.body),
-    closeStandalone: isPrevWhitespace((firstInverse || program).body)
-  };
-
-  if (block.openStrip.close) {
-    omitRight(program.body, null, true);
-  }
-
-  if (inverse) {
-    let inverseStrip = block.inverseStrip;
-
-    if (inverseStrip.open) {
-      omitLeft(program.body, null, true);
-    }
-
-    if (inverseStrip.close) {
-      omitRight(firstInverse.body, null, true);
-    }
-    if (block.closeStrip.open) {
-      omitLeft(lastInverse.body, null, true);
-    }
-
-    // Find standalone else statments
-    if (
-      !this.options.ignoreStandalone &&
-      isPrevWhitespace(program.body) &&
-      isNextWhitespace(firstInverse.body)
-    ) {
-      omitLeft(program.body);
-      omitRight(firstInverse.body);
-    }
-  } else if (block.closeStrip.open) {
-    omitLeft(program.body, null, true);
-  }
-
-  return strip;
-};
-
-WhitespaceControl.prototype.Decorator = WhitespaceControl.prototype.MustacheStatement = function(
-  mustache
-) {
-  return mustache.strip;
-};
-
-WhitespaceControl.prototype.PartialStatement = WhitespaceControl.prototype.CommentStatement = function(
-  node
-) {
-  /* istanbul ignore next */
-  let strip = node.strip || {};
-  return {
-    inlineStandalone: true,
-    open: strip.open,
-    close: strip.close
-  };
-};
-
-function isPrevWhitespace(body, i, isRoot) {
-  if (i === undefined) {
-    i = body.length;
-  }
-
-  // Nodes that end with newlines are considered whitespace (but are special
-  // cased for strip operations)
-  let prev = body[i - 1],
-    sibling = body[i - 2];
-  if (!prev) {
-    return isRoot;
-  }
-
-  if (prev.type === 'ContentStatement') {
-    return (sibling || !isRoot ? /\r?\n\s*?$/ : /(^|\r?\n)\s*?$/).test(
-      prev.original
-    );
-  }
-}
-function isNextWhitespace(body, i, isRoot) {
-  if (i === undefined) {
-    i = -1;
-  }
-
-  let next = body[i + 1],
-    sibling = body[i + 2];
-  if (!next) {
-    return isRoot;
-  }
-
-  if (next.type === 'ContentStatement') {
-    return (sibling || !isRoot ? /^\s*?\r?\n/ : /^\s*?(\r?\n|$)/).test(
-      next.original
-    );
-  }
-}
-
-// Marks the node to the right of the position as omitted.
-// I.e. {{foo}}' ' will mark the ' ' node as omitted.
-//
-// If i is undefined, then the first child will be marked as such.
-//
-// If multiple is truthy then all whitespace will be stripped out until non-whitespace
-// content is met.
-function omitRight(body, i, multiple) {
-  let current = body[i == null ? 0 : i + 1];
-  if (
-    !current ||
-    current.type !== 'ContentStatement' ||
-    (!multiple && current.rightStripped)
-  ) {
-    return;
-  }
-
-  let original = current.value;
-  current.value = current.value.replace(
-    multiple ? /^\s+/ : /^[ \t]*\r?\n?/,
-    ''
-  );
-  current.rightStripped = current.value !== original;
-}
-
-// Marks the node to the left of the position as omitted.
-// I.e. ' '{{foo}} will mark the ' ' node as omitted.
-//
-// If i is undefined then the last child will be marked as such.
-//
-// If multiple is truthy then all whitespace will be stripped out until non-whitespace
-// content is met.
-function omitLeft(body, i, multiple) {
-  let current = body[i == null ? body.length - 1 : i - 1];
-  if (
-    !current ||
-    current.type !== 'ContentStatement' ||
-    (!multiple && current.leftStripped)
-  ) {
-    return;
-  }
-
-  // We omit the last node if it's whitespace only and not preceded by a non-content node.
-  let original = current.value;
-  current.value = current.value.replace(multiple ? /\s+$/ : /[ \t]+$/, '');
-  current.leftStripped = current.value !== original;
-  return current.leftStripped;
-}
-
-export default WhitespaceControl;
diff --git a/lib/handlebars/exception.js b/lib/handlebars/exception.js
deleted file mode 100644
index b2fdc00bc..000000000
--- a/lib/handlebars/exception.js
+++ /dev/null
@@ -1,68 +0,0 @@
-const errorProps = [
-  'description',
-  'fileName',
-  'lineNumber',
-  'endLineNumber',
-  'message',
-  'name',
-  'number',
-  'stack'
-];
-
-function Exception(message, node) {
-  let loc = node && node.loc,
-    line,
-    endLineNumber,
-    column,
-    endColumn;
-
-  if (loc) {
-    line = loc.start.line;
-    endLineNumber = loc.end.line;
-    column = loc.start.column;
-    endColumn = loc.end.column;
-
-    message += ' - ' + line + ':' + column;
-  }
-
-  let tmp = Error.prototype.constructor.call(this, message);
-
-  // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
-  for (let idx = 0; idx < errorProps.length; idx++) {
-    this[errorProps[idx]] = tmp[errorProps[idx]];
-  }
-
-  /* istanbul ignore else */
-  if (Error.captureStackTrace) {
-    Error.captureStackTrace(this, Exception);
-  }
-
-  try {
-    if (loc) {
-      this.lineNumber = line;
-      this.endLineNumber = endLineNumber;
-
-      // Work around issue under safari where we can't directly set the column value
-      /* istanbul ignore next */
-      if (Object.defineProperty) {
-        Object.defineProperty(this, 'column', {
-          value: column,
-          enumerable: true
-        });
-        Object.defineProperty(this, 'endColumn', {
-          value: endColumn,
-          enumerable: true
-        });
-      } else {
-        this.column = column;
-        this.endColumn = endColumn;
-      }
-    }
-  } catch (nop) {
-    /* Ignore if the browser is very particular */
-  }
-}
-
-Exception.prototype = new Error();
-
-export default Exception;
diff --git a/lib/handlebars/helpers/each.js b/lib/handlebars/helpers/each.js
index 25651f453..33d19190d 100644
--- a/lib/handlebars/helpers/each.js
+++ b/lib/handlebars/helpers/each.js
@@ -1,5 +1,5 @@
+import { Exception } from '@handlebars/parser';
 import { createFrame, isArray, isFunction } from '../utils';
-import Exception from '../exception';
 
 export default function(instance) {
   instance.registerHelper('each', function(context, options) {
diff --git a/lib/handlebars/helpers/helper-missing.js b/lib/handlebars/helpers/helper-missing.js
index f8869e9c9..6daf44253 100644
--- a/lib/handlebars/helpers/helper-missing.js
+++ b/lib/handlebars/helpers/helper-missing.js
@@ -1,4 +1,4 @@
-import Exception from '../exception';
+import { Exception } from '@handlebars/parser';
 
 export default function(instance) {
   instance.registerHelper('helperMissing', function(/* [args, ]options */) {
diff --git a/lib/handlebars/helpers/if.js b/lib/handlebars/helpers/if.js
index 55241ecb7..db06df262 100644
--- a/lib/handlebars/helpers/if.js
+++ b/lib/handlebars/helpers/if.js
@@ -1,5 +1,5 @@
+import { Exception } from '@handlebars/parser';
 import { isEmpty, isFunction } from '../utils';
-import Exception from '../exception';
 
 export default function(instance) {
   instance.registerHelper('if', function(conditional, options) {
diff --git a/lib/handlebars/helpers/with.js b/lib/handlebars/helpers/with.js
index 3394b7ae6..63559be5c 100644
--- a/lib/handlebars/helpers/with.js
+++ b/lib/handlebars/helpers/with.js
@@ -1,5 +1,5 @@
+import { Exception } from '@handlebars/parser';
 import { isEmpty, isFunction } from '../utils';
-import Exception from '../exception';
 
 export default function(instance) {
   instance.registerHelper('with', function(context, options) {
diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js
index cb3ff3d04..eb31bfa52 100644
--- a/lib/handlebars/runtime.js
+++ b/lib/handlebars/runtime.js
@@ -1,5 +1,5 @@
+import { Exception } from '@handlebars/parser';
 import * as Utils from './utils';
-import Exception from './exception';
 import {
   COMPILER_REVISION,
   createFrame,
diff --git a/lib/index.js b/lib/index.js
index 0383c02f7..06547f1db 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -6,9 +6,9 @@
 
 var handlebars = require('../dist/cjs/handlebars')['default'];
 
-var printer = require('../dist/cjs/handlebars/compiler/printer');
-handlebars.PrintVisitor = printer.PrintVisitor;
-handlebars.print = printer.print;
+var parser = require('@handlebars/parser');
+handlebars.PrintVisitor = parser.PrintVisitor;
+handlebars.print = parser.print;
 
 module.exports = handlebars;
 
diff --git a/nyc.config.js b/nyc.config.js
index f516eb2b4..3f52f089e 100644
--- a/nyc.config.js
+++ b/nyc.config.js
@@ -4,6 +4,6 @@ module.exports = {
   lines: 100,
   functions: 100,
   statements: 100,
-  exclude: ['**/spec/**', '**/handlebars/compiler/parser.js'],
+  exclude: ['**/spec/**'],
   reporter: 'html'
 };
diff --git a/package-lock.json b/package-lock.json
index 699777bdf..835fffde9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -166,6 +166,11 @@
         }
       }
     },
+    "@handlebars/parser": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@handlebars/parser/-/parser-1.1.0.tgz",
+      "integrity": "sha512-rR7tJoSwJ2eooOpYGxGGW95sLq6GXUaS1UtWvN7pei6n2/okYvCGld9vsUTvkl2migxbkszsycwtMf/GEc1k1A=="
+    },
     "@knappi/grunt-saucelabs": {
       "version": "9.0.2",
       "resolved": "https://registry.npmjs.org/@knappi/grunt-saucelabs/-/grunt-saucelabs-9.0.2.tgz",
diff --git a/package.json b/package.json
index 296074952..5e2d1033c 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
     "node": ">=10"
   },
   "dependencies": {
+    "@handlebars/parser": "^1.1.0",
     "neo-async": "^2.6.0",
     "source-map": "^0.6.1",
     "yargs": "^15.3.1"
diff --git a/spec/ast.js b/spec/ast.js
index 1f4146ae0..d31752048 100644
--- a/spec/ast.js
+++ b/spec/ast.js
@@ -181,251 +181,4 @@ describe('ast', function() {
       testColumns(chainInverseNode.inverse.body[0].inverse, 10, 11, 8, 0);
     });
   });
-
-  describe('whitespace control', function() {
-    describe('parse', function() {
-      it('mustache', function() {
-        var ast = Handlebars.parse('  {{~comment~}} ');
-
-        equals(ast.body[0].value, '');
-        equals(ast.body[2].value, '');
-      });
-
-      it('block statements', function() {
-        var ast = Handlebars.parse(' {{# comment~}} \nfoo\n {{~/comment}}');
-
-        equals(ast.body[0].value, '');
-        equals(ast.body[1].program.body[0].value, 'foo');
-      });
-    });
-
-    describe('parseWithoutProcessing', function() {
-      it('mustache', function() {
-        var ast = Handlebars.parseWithoutProcessing('  {{~comment~}} ');
-
-        equals(ast.body[0].value, '  ');
-        equals(ast.body[2].value, ' ');
-      });
-
-      it('block statements', function() {
-        var ast = Handlebars.parseWithoutProcessing(
-          ' {{# comment~}} \nfoo\n {{~/comment}}'
-        );
-
-        equals(ast.body[0].value, ' ');
-        equals(ast.body[1].program.body[0].value, ' \nfoo\n ');
-      });
-    });
-  });
-
-  describe('standalone flags', function() {
-    describe('mustache', function() {
-      it('does not mark mustaches as standalone', function() {
-        var ast = Handlebars.parse('  {{comment}} ');
-        equals(!!ast.body[0].value, true);
-        equals(!!ast.body[2].value, true);
-      });
-    });
-    describe('blocks - parseWithoutProcessing', function() {
-      it('block mustaches', function() {
-        var ast = Handlebars.parseWithoutProcessing(
-            ' {{# comment}} \nfoo\n {{else}} \n  bar \n  {{/comment}} '
-          ),
-          block = ast.body[1];
-
-        equals(ast.body[0].value, ' ');
-
-        equals(block.program.body[0].value, ' \nfoo\n ');
-        equals(block.inverse.body[0].value, ' \n  bar \n  ');
-
-        equals(ast.body[2].value, ' ');
-      });
-      it('initial block mustaches', function() {
-        var ast = Handlebars.parseWithoutProcessing(
-            '{{# comment}} \nfoo\n {{/comment}}'
-          ),
-          block = ast.body[0];
-
-        equals(block.program.body[0].value, ' \nfoo\n ');
-      });
-      it('mustaches with children', function() {
-        var ast = Handlebars.parseWithoutProcessing(
-            '{{# comment}} \n{{foo}}\n {{/comment}}'
-          ),
-          block = ast.body[0];
-
-        equals(block.program.body[0].value, ' \n');
-        equals(block.program.body[1].path.original, 'foo');
-        equals(block.program.body[2].value, '\n ');
-      });
-      it('nested block mustaches', function() {
-        var ast = Handlebars.parseWithoutProcessing(
-            '{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n  bar \n  {{/comment}} \n{{/foo}}'
-          ),
-          body = ast.body[0].program.body,
-          block = body[1];
-
-        equals(body[0].value, ' \n');
-
-        equals(block.program.body[0].value, ' \nfoo\n ');
-        equals(block.inverse.body[0].value, ' \n  bar \n  ');
-      });
-      it('column 0 block mustaches', function() {
-        var ast = Handlebars.parseWithoutProcessing(
-            'test\n{{# comment}} \nfoo\n {{else}} \n  bar \n  {{/comment}} '
-          ),
-          block = ast.body[1];
-
-        equals(ast.body[0].omit, undefined);
-
-        equals(block.program.body[0].value, ' \nfoo\n ');
-        equals(block.inverse.body[0].value, ' \n  bar \n  ');
-
-        equals(ast.body[2].value, ' ');
-      });
-    });
-    describe('blocks', function() {
-      it('marks block mustaches as standalone', function() {
-        var ast = Handlebars.parse(
-            ' {{# comment}} \nfoo\n {{else}} \n  bar \n  {{/comment}} '
-          ),
-          block = ast.body[1];
-
-        equals(ast.body[0].value, '');
-
-        equals(block.program.body[0].value, 'foo\n');
-        equals(block.inverse.body[0].value, '  bar \n');
-
-        equals(ast.body[2].value, '');
-      });
-      it('marks initial block mustaches as standalone', function() {
-        var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'),
-          block = ast.body[0];
-
-        equals(block.program.body[0].value, 'foo\n');
-      });
-      it('marks mustaches with children as standalone', function() {
-        var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'),
-          block = ast.body[0];
-
-        equals(block.program.body[0].value, '');
-        equals(block.program.body[1].path.original, 'foo');
-        equals(block.program.body[2].value, '\n');
-      });
-      it('marks nested block mustaches as standalone', function() {
-        var ast = Handlebars.parse(
-            '{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n  bar \n  {{/comment}} \n{{/foo}}'
-          ),
-          body = ast.body[0].program.body,
-          block = body[1];
-
-        equals(body[0].value, '');
-
-        equals(block.program.body[0].value, 'foo\n');
-        equals(block.inverse.body[0].value, '  bar \n');
-
-        equals(body[0].value, '');
-      });
-      it('does not mark nested block mustaches as standalone', function() {
-        var ast = Handlebars.parse(
-            '{{#foo}} {{# comment}} \nfoo\n {{else}} \n  bar \n  {{/comment}} {{/foo}}'
-          ),
-          body = ast.body[0].program.body,
-          block = body[1];
-
-        equals(body[0].omit, undefined);
-
-        equals(block.program.body[0].value, ' \nfoo\n');
-        equals(block.inverse.body[0].value, '  bar \n  ');
-
-        equals(body[0].omit, undefined);
-      });
-      it('does not mark nested initial block mustaches as standalone', function() {
-        var ast = Handlebars.parse(
-            '{{#foo}}{{# comment}} \nfoo\n {{else}} \n  bar \n  {{/comment}}{{/foo}}'
-          ),
-          body = ast.body[0].program.body,
-          block = body[0];
-
-        equals(block.program.body[0].value, ' \nfoo\n');
-        equals(block.inverse.body[0].value, '  bar \n  ');
-
-        equals(body[0].omit, undefined);
-      });
-
-      it('marks column 0 block mustaches as standalone', function() {
-        var ast = Handlebars.parse(
-            'test\n{{# comment}} \nfoo\n {{else}} \n  bar \n  {{/comment}} '
-          ),
-          block = ast.body[1];
-
-        equals(ast.body[0].omit, undefined);
-
-        equals(block.program.body[0].value, 'foo\n');
-        equals(block.inverse.body[0].value, '  bar \n');
-
-        equals(ast.body[2].value, '');
-      });
-    });
-    describe('partials - parseWithoutProcessing', function() {
-      it('simple partial', function() {
-        var ast = Handlebars.parseWithoutProcessing('{{> partial }} ');
-        equals(ast.body[1].value, ' ');
-      });
-      it('indented partial', function() {
-        var ast = Handlebars.parseWithoutProcessing('  {{> partial }} ');
-        equals(ast.body[0].value, '  ');
-        equals(ast.body[1].indent, '');
-        equals(ast.body[2].value, ' ');
-      });
-    });
-    describe('partials', function() {
-      it('marks partial as standalone', function() {
-        var ast = Handlebars.parse('{{> partial }} ');
-        equals(ast.body[1].value, '');
-      });
-      it('marks indented partial as standalone', function() {
-        var ast = Handlebars.parse('  {{> partial }} ');
-        equals(ast.body[0].value, '');
-        equals(ast.body[1].indent, '  ');
-        equals(ast.body[2].value, '');
-      });
-      it('marks those around content as not standalone', function() {
-        var ast = Handlebars.parse('a{{> partial }}');
-        equals(ast.body[0].omit, undefined);
-
-        ast = Handlebars.parse('{{> partial }}a');
-        equals(ast.body[1].omit, undefined);
-      });
-    });
-    describe('comments - parseWithoutProcessing', function() {
-      it('simple comment', function() {
-        var ast = Handlebars.parseWithoutProcessing('{{! comment }} ');
-        equals(ast.body[1].value, ' ');
-      });
-      it('indented comment', function() {
-        var ast = Handlebars.parseWithoutProcessing('  {{! comment }} ');
-        equals(ast.body[0].value, '  ');
-        equals(ast.body[2].value, ' ');
-      });
-    });
-    describe('comments', function() {
-      it('marks comment as standalone', function() {
-        var ast = Handlebars.parse('{{! comment }} ');
-        equals(ast.body[1].value, '');
-      });
-      it('marks indented comment as standalone', function() {
-        var ast = Handlebars.parse('  {{! comment }} ');
-        equals(ast.body[0].value, '');
-        equals(ast.body[2].value, '');
-      });
-      it('marks those around content as not standalone', function() {
-        var ast = Handlebars.parse('a{{! comment }}');
-        equals(ast.body[0].omit, undefined);
-
-        ast = Handlebars.parse('{{! comment }}a');
-        equals(ast.body[1].omit, undefined);
-      });
-    });
-  });
 });
diff --git a/spec/env/runtime.js b/spec/env/runtime.js
index 99b5bf785..2d5007a2e 100644
--- a/spec/env/runtime.js
+++ b/spec/env/runtime.js
@@ -22,7 +22,7 @@ vm.runInThisContext(
   filename
 );
 
-var parse = require('../../dist/cjs/handlebars/compiler/base').parse;
+var parse = require('@handlebars/parser').parse;
 var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
 var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler');
 
diff --git a/spec/parser.js b/spec/parser.js
deleted file mode 100644
index 2aa7e39ff..000000000
--- a/spec/parser.js
+++ /dev/null
@@ -1,455 +0,0 @@
-describe('parser', function() {
-  if (!Handlebars.print) {
-    return;
-  }
-
-  function astFor(template) {
-    var ast = Handlebars.parse(template);
-    return Handlebars.print(ast);
-  }
-
-  it('parses simple mustaches', function() {
-    equals(astFor('{{123}}'), '{{ NUMBER{123} [] }}\n');
-    equals(astFor('{{"foo"}}'), '{{ "foo" [] }}\n');
-    equals(astFor('{{false}}'), '{{ BOOLEAN{false} [] }}\n');
-    equals(astFor('{{true}}'), '{{ BOOLEAN{true} [] }}\n');
-    equals(astFor('{{foo}}'), '{{ PATH:foo [] }}\n');
-    equals(astFor('{{foo?}}'), '{{ PATH:foo? [] }}\n');
-    equals(astFor('{{foo_}}'), '{{ PATH:foo_ [] }}\n');
-    equals(astFor('{{foo-}}'), '{{ PATH:foo- [] }}\n');
-    equals(astFor('{{foo:}}'), '{{ PATH:foo: [] }}\n');
-  });
-
-  it('parses simple mustaches with data', function() {
-    equals(astFor('{{@foo}}'), '{{ @PATH:foo [] }}\n');
-  });
-
-  it('parses simple mustaches with data paths', function() {
-    equals(astFor('{{@../foo}}'), '{{ @PATH:foo [] }}\n');
-  });
-
-  it('parses mustaches with paths', function() {
-    equals(astFor('{{foo/bar}}'), '{{ PATH:foo/bar [] }}\n');
-  });
-
-  it('parses mustaches with this/foo', function() {
-    equals(astFor('{{this/foo}}'), '{{ PATH:foo [] }}\n');
-  });
-
-  it('parses mustaches with - in a path', function() {
-    equals(astFor('{{foo-bar}}'), '{{ PATH:foo-bar [] }}\n');
-  });
-  it('parses mustaches with escaped [] in a path', function() {
-    equals(astFor('{{[foo[\\]]}}'), '{{ PATH:foo[] [] }}\n');
-  });
-  it('parses escaped \\\\ in path', function() {
-    equals(astFor('{{[foo\\\\]}}'), '{{ PATH:foo\\ [] }}\n');
-  });
-
-  it('parses mustaches with parameters', function() {
-    equals(astFor('{{foo bar}}'), '{{ PATH:foo [PATH:bar] }}\n');
-  });
-
-  it('parses mustaches with string parameters', function() {
-    equals(astFor('{{foo bar "baz" }}'), '{{ PATH:foo [PATH:bar, "baz"] }}\n');
-  });
-
-  it('parses mustaches with NUMBER parameters', function() {
-    equals(astFor('{{foo 1}}'), '{{ PATH:foo [NUMBER{1}] }}\n');
-  });
-
-  it('parses mustaches with BOOLEAN parameters', function() {
-    equals(astFor('{{foo true}}'), '{{ PATH:foo [BOOLEAN{true}] }}\n');
-    equals(astFor('{{foo false}}'), '{{ PATH:foo [BOOLEAN{false}] }}\n');
-  });
-
-  it('parses mustaches with undefined and null paths', function() {
-    equals(astFor('{{undefined}}'), '{{ UNDEFINED [] }}\n');
-    equals(astFor('{{null}}'), '{{ NULL [] }}\n');
-  });
-  it('parses mustaches with undefined and null parameters', function() {
-    equals(
-      astFor('{{foo undefined null}}'),
-      '{{ PATH:foo [UNDEFINED, NULL] }}\n'
-    );
-  });
-
-  it('parses mustaches with DATA parameters', function() {
-    equals(astFor('{{foo @bar}}'), '{{ PATH:foo [@PATH:bar] }}\n');
-  });
-
-  it('parses mustaches with hash arguments', function() {
-    equals(astFor('{{foo bar=baz}}'), '{{ PATH:foo [] HASH{bar=PATH:baz} }}\n');
-    equals(astFor('{{foo bar=1}}'), '{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n');
-    equals(
-      astFor('{{foo bar=true}}'),
-      '{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n'
-    );
-    equals(
-      astFor('{{foo bar=false}}'),
-      '{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n'
-    );
-    equals(
-      astFor('{{foo bar=@baz}}'),
-      '{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n'
-    );
-
-    equals(
-      astFor('{{foo bar=baz bat=bam}}'),
-      '{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n'
-    );
-    equals(
-      astFor('{{foo bar=baz bat="bam"}}'),
-      '{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n'
-    );
-
-    equals(astFor("{{foo bat='bam'}}"), '{{ PATH:foo [] HASH{bat="bam"} }}\n');
-
-    equals(
-      astFor('{{foo omg bar=baz bat="bam"}}'),
-      '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n'
-    );
-    equals(
-      astFor('{{foo omg bar=baz bat="bam" baz=1}}'),
-      '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n'
-    );
-    equals(
-      astFor('{{foo omg bar=baz bat="bam" baz=true}}'),
-      '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n'
-    );
-    equals(
-      astFor('{{foo omg bar=baz bat="bam" baz=false}}'),
-      '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n'
-    );
-  });
-
-  it('parses contents followed by a mustache', function() {
-    equals(
-      astFor('foo bar {{baz}}'),
-      "CONTENT[ 'foo bar ' ]\n{{ PATH:baz [] }}\n"
-    );
-  });
-
-  it('parses a partial', function() {
-    equals(astFor('{{> foo }}'), '{{> PARTIAL:foo }}\n');
-    equals(astFor('{{> "foo" }}'), '{{> PARTIAL:foo }}\n');
-    equals(astFor('{{> 1 }}'), '{{> PARTIAL:1 }}\n');
-  });
-
-  it('parses a partial with context', function() {
-    equals(astFor('{{> foo bar}}'), '{{> PARTIAL:foo PATH:bar }}\n');
-  });
-
-  it('parses a partial with hash', function() {
-    equals(
-      astFor('{{> foo bar=bat}}'),
-      '{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n'
-    );
-  });
-
-  it('parses a partial with context and hash', function() {
-    equals(
-      astFor('{{> foo bar bat=baz}}'),
-      '{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n'
-    );
-  });
-
-  it('parses a partial with a complex name', function() {
-    equals(
-      astFor('{{> shared/partial?.bar}}'),
-      '{{> PARTIAL:shared/partial?.bar }}\n'
-    );
-  });
-
-  it('parsers partial blocks', function() {
-    equals(
-      astFor('{{#> foo}}bar{{/foo}}'),
-      "{{> PARTIAL BLOCK:foo PROGRAM:\n  CONTENT[ 'bar' ]\n }}\n"
-    );
-  });
-  it('should handle parser block mismatch', function() {
-    shouldThrow(
-      function() {
-        astFor('{{#> goodbyes}}{{/hellos}}');
-      },
-      Error,
-      /goodbyes doesn't match hellos/
-    );
-  });
-  it('parsers partial blocks with arguments', function() {
-    equals(
-      astFor('{{#> foo context hash=value}}bar{{/foo}}'),
-      "{{> PARTIAL BLOCK:foo PATH:context HASH{hash=PATH:value} PROGRAM:\n  CONTENT[ 'bar' ]\n }}\n"
-    );
-  });
-
-  it('parses a comment', function() {
-    equals(
-      astFor('{{! this is a comment }}'),
-      "{{! ' this is a comment ' }}\n"
-    );
-  });
-
-  it('parses a multi-line comment', function() {
-    equals(
-      astFor('{{!\nthis is a multi-line comment\n}}'),
-      "{{! '\nthis is a multi-line comment\n' }}\n"
-    );
-  });
-
-  it('parses an inverse section', function() {
-    equals(
-      astFor('{{#foo}} bar {{^}} baz {{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  PROGRAM:\n    CONTENT[ ' bar ' ]\n  {{^}}\n    CONTENT[ ' baz ' ]\n"
-    );
-  });
-
-  it('parses an inverse (else-style) section', function() {
-    equals(
-      astFor('{{#foo}} bar {{else}} baz {{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  PROGRAM:\n    CONTENT[ ' bar ' ]\n  {{^}}\n    CONTENT[ ' baz ' ]\n"
-    );
-  });
-
-  it('parses multiple inverse sections', function() {
-    equals(
-      astFor('{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  PROGRAM:\n    CONTENT[ ' bar ' ]\n  {{^}}\n    BLOCK:\n      PATH:if [PATH:bar]\n      PROGRAM:\n      {{^}}\n        CONTENT[ ' baz ' ]\n"
-    );
-  });
-
-  it('parses empty blocks', function() {
-    equals(astFor('{{#foo}}{{/foo}}'), 'BLOCK:\n  PATH:foo []\n  PROGRAM:\n');
-  });
-
-  it('parses empty blocks with empty inverse section', function() {
-    equals(
-      astFor('{{#foo}}{{^}}{{/foo}}'),
-      'BLOCK:\n  PATH:foo []\n  PROGRAM:\n  {{^}}\n'
-    );
-  });
-
-  it('parses empty blocks with empty inverse (else-style) section', function() {
-    equals(
-      astFor('{{#foo}}{{else}}{{/foo}}'),
-      'BLOCK:\n  PATH:foo []\n  PROGRAM:\n  {{^}}\n'
-    );
-  });
-
-  it('parses non-empty blocks with empty inverse section', function() {
-    equals(
-      astFor('{{#foo}} bar {{^}}{{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  PROGRAM:\n    CONTENT[ ' bar ' ]\n  {{^}}\n"
-    );
-  });
-
-  it('parses non-empty blocks with empty inverse (else-style) section', function() {
-    equals(
-      astFor('{{#foo}} bar {{else}}{{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  PROGRAM:\n    CONTENT[ ' bar ' ]\n  {{^}}\n"
-    );
-  });
-
-  it('parses empty blocks with non-empty inverse section', function() {
-    equals(
-      astFor('{{#foo}}{{^}} bar {{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  PROGRAM:\n  {{^}}\n    CONTENT[ ' bar ' ]\n"
-    );
-  });
-
-  it('parses empty blocks with non-empty inverse (else-style) section', function() {
-    equals(
-      astFor('{{#foo}}{{else}} bar {{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  PROGRAM:\n  {{^}}\n    CONTENT[ ' bar ' ]\n"
-    );
-  });
-
-  it('parses a standalone inverse section', function() {
-    equals(
-      astFor('{{^foo}}bar{{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  {{^}}\n    CONTENT[ 'bar' ]\n"
-    );
-  });
-  it('throws on old inverse section', function() {
-    shouldThrow(function() {
-      astFor('{{else foo}}bar{{/foo}}');
-    }, Error);
-  });
-
-  it('parses block with block params', function() {
-    equals(
-      astFor('{{#foo as |bar baz|}}content{{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  PROGRAM:\n    BLOCK PARAMS: [ bar baz ]\n    CONTENT[ 'content' ]\n"
-    );
-  });
-
-  it('parses inverse block with block params', function() {
-    equals(
-      astFor('{{^foo as |bar baz|}}content{{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  {{^}}\n    BLOCK PARAMS: [ bar baz ]\n    CONTENT[ 'content' ]\n"
-    );
-  });
-  it('parses chained inverse block with block params', function() {
-    equals(
-      astFor('{{#foo}}{{else foo as |bar baz|}}content{{/foo}}'),
-      "BLOCK:\n  PATH:foo []\n  PROGRAM:\n  {{^}}\n    BLOCK:\n      PATH:foo []\n      PROGRAM:\n        BLOCK PARAMS: [ bar baz ]\n        CONTENT[ 'content' ]\n"
-    );
-  });
-  it("raises if there's a Parse error", function() {
-    shouldThrow(
-      function() {
-        astFor('foo{{^}}bar');
-      },
-      Error,
-      /Parse error on line 1/
-    );
-    shouldThrow(
-      function() {
-        astFor('{{foo}');
-      },
-      Error,
-      /Parse error on line 1/
-    );
-    shouldThrow(
-      function() {
-        astFor('{{foo &}}');
-      },
-      Error,
-      /Parse error on line 1/
-    );
-    shouldThrow(
-      function() {
-        astFor('{{#goodbyes}}{{/hellos}}');
-      },
-      Error,
-      /goodbyes doesn't match hellos/
-    );
-
-    shouldThrow(
-      function() {
-        astFor('{{{{goodbyes}}}} {{{{/hellos}}}}');
-      },
-      Error,
-      /goodbyes doesn't match hellos/
-    );
-  });
-
-  it('should handle invalid paths', function() {
-    shouldThrow(
-      function() {
-        astFor('{{foo/../bar}}');
-      },
-      Error,
-      /Invalid path: foo\/\.\. - 1:2/
-    );
-    shouldThrow(
-      function() {
-        astFor('{{foo/./bar}}');
-      },
-      Error,
-      /Invalid path: foo\/\. - 1:2/
-    );
-    shouldThrow(
-      function() {
-        astFor('{{foo/this/bar}}');
-      },
-      Error,
-      /Invalid path: foo\/this - 1:2/
-    );
-  });
-
-  it('knows how to report the correct line number in errors', function() {
-    shouldThrow(
-      function() {
-        astFor('hello\nmy\n{{foo}');
-      },
-      Error,
-      /Parse error on line 3/
-    );
-    shouldThrow(
-      function() {
-        astFor('hello\n\nmy\n\n{{foo}');
-      },
-      Error,
-      /Parse error on line 5/
-    );
-  });
-
-  it('knows how to report the correct line number in errors when the first character is a newline', function() {
-    shouldThrow(
-      function() {
-        astFor('\n\nhello\n\nmy\n\n{{foo}');
-      },
-      Error,
-      /Parse error on line 7/
-    );
-  });
-
-  describe('externally compiled AST', function() {
-    it('can pass through an already-compiled AST', function() {
-      equals(
-        astFor({
-          type: 'Program',
-          body: [{ type: 'ContentStatement', value: 'Hello' }]
-        }),
-        "CONTENT[ 'Hello' ]\n"
-      );
-    });
-  });
-
-  describe('directives', function() {
-    it('should parse block directives', function() {
-      equals(
-        astFor('{{#* foo}}{{/foo}}'),
-        'DIRECTIVE BLOCK:\n  PATH:foo []\n  PROGRAM:\n'
-      );
-    });
-    it('should parse directives', function() {
-      equals(astFor('{{* foo}}'), '{{ DIRECTIVE PATH:foo [] }}\n');
-    });
-    it('should fail if directives have inverse', function() {
-      shouldThrow(
-        function() {
-          astFor('{{#* foo}}{{^}}{{/foo}}');
-        },
-        Error,
-        /Unexpected inverse/
-      );
-    });
-  });
-
-  it('GH1024 - should track program location properly', function() {
-    var p = Handlebars.parse(
-      '\n' +
-        '  {{#if foo}}\n' +
-        '    {{bar}}\n' +
-        '       {{else}}    {{baz}}\n' +
-        '\n' +
-        '     {{/if}}\n' +
-        '    '
-    );
-
-    // We really need a deep equals but for now this should be stable...
-    equals(
-      JSON.stringify(p.loc),
-      JSON.stringify({
-        start: { line: 1, column: 0 },
-        end: { line: 7, column: 4 }
-      })
-    );
-    equals(
-      JSON.stringify(p.body[1].program.loc),
-      JSON.stringify({
-        start: { line: 2, column: 13 },
-        end: { line: 4, column: 7 }
-      })
-    );
-    equals(
-      JSON.stringify(p.body[1].inverse.loc),
-      JSON.stringify({
-        start: { line: 4, column: 15 },
-        end: { line: 6, column: 5 }
-      })
-    );
-  });
-});
diff --git a/spec/visitor.js b/spec/visitor.js
deleted file mode 100644
index 2bec356fe..000000000
--- a/spec/visitor.js
+++ /dev/null
@@ -1,164 +0,0 @@
-describe('Visitor', function() {
-  if (!Handlebars.Visitor || !Handlebars.print) {
-    return;
-  }
-
-  it('should provide coverage', function() {
-    // Simply run the thing and make sure it does not fail and that all of the
-    // stub methods are executed
-    var visitor = new Handlebars.Visitor();
-    visitor.accept(
-      Handlebars.parse(
-        '{{foo}}{{#foo (bar 1 "1" true undefined null) foo=@data}}{{!comment}}{{> bar }} {{/foo}}'
-      )
-    );
-    visitor.accept(Handlebars.parse('{{#> bar }} {{/bar}}'));
-    visitor.accept(Handlebars.parse('{{#* bar }} {{/bar}}'));
-    visitor.accept(Handlebars.parse('{{* bar }}'));
-  });
-
-  it('should traverse to stubs', function() {
-    var visitor = new Handlebars.Visitor();
-
-    visitor.StringLiteral = function(string) {
-      equal(string.value, '2');
-    };
-    visitor.NumberLiteral = function(number) {
-      equal(number.value, 1);
-    };
-    visitor.BooleanLiteral = function(bool) {
-      equal(bool.value, true);
-
-      equal(this.parents.length, 3);
-      equal(this.parents[0].type, 'SubExpression');
-      equal(this.parents[1].type, 'BlockStatement');
-      equal(this.parents[2].type, 'Program');
-    };
-    visitor.PathExpression = function(id) {
-      equal(/(foo\.)?bar$/.test(id.original), true);
-    };
-    visitor.ContentStatement = function(content) {
-      equal(content.value, ' ');
-    };
-    visitor.CommentStatement = function(comment) {
-      equal(comment.value, 'comment');
-    };
-
-    visitor.accept(
-      Handlebars.parse(
-        '{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}'
-      )
-    );
-  });
-
-  describe('mutating', function() {
-    describe('fields', function() {
-      it('should replace value', function() {
-        var visitor = new Handlebars.Visitor();
-
-        visitor.mutating = true;
-        visitor.StringLiteral = function(string) {
-          return { type: 'NumberLiteral', value: 42, loc: string.loc };
-        };
-
-        var ast = Handlebars.parse('{{foo foo="foo"}}');
-        visitor.accept(ast);
-        equals(
-          Handlebars.print(ast),
-          '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'
-        );
-      });
-      it('should treat undefined resonse as identity', function() {
-        var visitor = new Handlebars.Visitor();
-        visitor.mutating = true;
-
-        var ast = Handlebars.parse('{{foo foo=42}}');
-        visitor.accept(ast);
-        equals(
-          Handlebars.print(ast),
-          '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'
-        );
-      });
-      it('should remove false responses', function() {
-        var visitor = new Handlebars.Visitor();
-
-        visitor.mutating = true;
-        visitor.Hash = function() {
-          return false;
-        };
-
-        var ast = Handlebars.parse('{{foo foo=42}}');
-        visitor.accept(ast);
-        equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n');
-      });
-      it('should throw when removing required values', function() {
-        shouldThrow(
-          function() {
-            var visitor = new Handlebars.Visitor();
-
-            visitor.mutating = true;
-            visitor.PathExpression = function() {
-              return false;
-            };
-
-            var ast = Handlebars.parse('{{foo 42}}');
-            visitor.accept(ast);
-          },
-          Handlebars.Exception,
-          'MustacheStatement requires path'
-        );
-      });
-      it('should throw when returning non-node responses', function() {
-        shouldThrow(
-          function() {
-            var visitor = new Handlebars.Visitor();
-
-            visitor.mutating = true;
-            visitor.PathExpression = function() {
-              return {};
-            };
-
-            var ast = Handlebars.parse('{{foo 42}}');
-            visitor.accept(ast);
-          },
-          Handlebars.Exception,
-          'Unexpected node type "undefined" found when accepting path on MustacheStatement'
-        );
-      });
-    });
-    describe('arrays', function() {
-      it('should replace value', function() {
-        var visitor = new Handlebars.Visitor();
-
-        visitor.mutating = true;
-        visitor.StringLiteral = function(string) {
-          return { type: 'NumberLiteral', value: 42, loc: string.locInfo };
-        };
-
-        var ast = Handlebars.parse('{{foo "foo"}}');
-        visitor.accept(ast);
-        equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n');
-      });
-      it('should treat undefined resonse as identity', function() {
-        var visitor = new Handlebars.Visitor();
-        visitor.mutating = true;
-
-        var ast = Handlebars.parse('{{foo 42}}');
-        visitor.accept(ast);
-        equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n');
-      });
-      it('should remove false responses', function() {
-        var visitor = new Handlebars.Visitor();
-
-        visitor.mutating = true;
-        visitor.NumberLiteral = function() {
-          return false;
-        };
-
-        var ast = Handlebars.parse('{{foo 42}}');
-        visitor.accept(ast);
-        equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n');
-      });
-    });
-  });
-});
diff --git a/src/handlebars.l b/src/handlebars.l
deleted file mode 100644
index fbf208b48..000000000
--- a/src/handlebars.l
+++ /dev/null
@@ -1,126 +0,0 @@
-
-%x mu emu com raw
-
-%{
-
-function strip(start, end) {
-  return yytext = yytext.substring(start, yyleng - end + start);
-}
-
-%}
-
-LEFT_STRIP    "~"
-RIGHT_STRIP   "~"
-
-LOOKAHEAD           [=~}\s\/.)|]
-LITERAL_LOOKAHEAD   [~}\s)]
-
-/*
-ID is the inverse of control characters.
-Control characters ranges:
-  [\s]          Whitespace
-  [!"#%-,\./]   !, ", #, %, &, ', (, ), *, +, ,, ., /,  Exceptions in range: $, -
-  [;->@]        ;, <, =, >, @,                          Exceptions in range: :, ?
-  [\[-\^`]      [, \, ], ^, `,                          Exceptions in range: _
-  [\{-~]        {, |, }, ~
-*/
-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';
-
-// marks CONTENT up to the next mustache or escaped mustache
-<emu>[^\x00]{2,}?/("{{"|"\\{{"|"\\\\{{"|<<EOF>>) {
-                                   this.popState();
-                                   return 'CONTENT';
-                                 }
-
-// nested raw block will create stacked 'raw' condition
-<raw>"{{{{"/[^/]                 this.begin('raw'); return 'CONTENT';
-<raw>"{{{{/"[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]"}}}}" {
-                                  this.popState();
-                                  // Should be using `this.topState()` below, but it currently
-                                  // returns the second top instead of the first top. Opened an
-                                  // issue about it at https://github.com/zaach/jison/issues/291
-                                  if (this.conditionStack[this.conditionStack.length-1] === 'raw') {
-                                    return 'CONTENT';
-                                  } else {
-                                    strip(5, 9);
-                                    return 'END_RAW_BLOCK';
-                                  }
-                                 }
-<raw>[^\x00]+?/("{{{{")          { return 'CONTENT'; }
-
-<com>[\s\S]*?"--"{RIGHT_STRIP}?"}}" {
-  this.popState();
-  return 'COMMENT';
-}
-
-<mu>"("                          return 'OPEN_SEXPR';
-<mu>")"                          return 'CLOSE_SEXPR';
-
-<mu>"{{{{"                       { return 'OPEN_RAW_BLOCK'; }
-<mu>"}}}}"                       {
-                                  this.popState();
-                                  this.begin('raw');
-                                  return 'CLOSE_RAW_BLOCK';
-                                 }
-<mu>"{{"{LEFT_STRIP}?">"         return 'OPEN_PARTIAL';
-<mu>"{{"{LEFT_STRIP}?"#>"        return 'OPEN_PARTIAL_BLOCK';
-<mu>"{{"{LEFT_STRIP}?"#""*"?     return 'OPEN_BLOCK';
-<mu>"{{"{LEFT_STRIP}?"/"         return 'OPEN_ENDBLOCK';
-<mu>"{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}"        this.popState(); return 'INVERSE';
-<mu>"{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}"  this.popState(); return 'INVERSE';
-<mu>"{{"{LEFT_STRIP}?"^"         return 'OPEN_INVERSE';
-<mu>"{{"{LEFT_STRIP}?\s*"else"   return 'OPEN_INVERSE_CHAIN';
-<mu>"{{"{LEFT_STRIP}?"{"         return 'OPEN_UNESCAPED';
-<mu>"{{"{LEFT_STRIP}?"&"         return 'OPEN';
-<mu>"{{"{LEFT_STRIP}?"!--" {
-  this.unput(yytext);
-  this.popState();
-  this.begin('com');
-}
-<mu>"{{"{LEFT_STRIP}?"!"[\s\S]*?"}}" {
-  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>"undefined"/{LITERAL_LOOKAHEAD} return 'UNDEFINED';
-<mu>"null"/{LITERAL_LOOKAHEAD}   return 'NULL';
-<mu>\-?[0-9]+(?:\.[0-9]+)?/{LITERAL_LOOKAHEAD} return 'NUMBER';
-<mu>"as"\s+"|"                   return 'OPEN_BLOCK_PARAMS';
-<mu>"|"                          return 'CLOSE_BLOCK_PARAMS';
-
-<mu>{ID}                         return 'ID';
-
-<mu>'['('\\]'|[^\]])*']'         yytext = yytext.replace(/\\([\\\]])/g,'$1'); return 'ID';
-<mu>.                            return 'INVALID';
-
-<INITIAL,mu><<EOF>>              return 'EOF';
diff --git a/src/handlebars.yy b/src/handlebars.yy
deleted file mode 100644
index cab04c61a..000000000
--- a/src/handlebars.yy
+++ /dev/null
@@ -1,166 +0,0 @@
-%start root
-
-%ebnf
-
-%%
-
-root
-  : program EOF { return $1; }
-  ;
-
-program
-  : statement* -> yy.prepareProgram($1)
-  ;
-
-statement
-  : mustache -> $1
-  | block -> $1
-  | rawBlock -> $1
-  | partial -> $1
-  | partialBlock -> $1
-  | content -> $1
-  | COMMENT {
-    $$ = {
-      type: 'CommentStatement',
-      value: yy.stripComment($1),
-      strip: yy.stripFlags($1, $1),
-      loc: yy.locInfo(@$)
-    };
-  };
-
-content
-  : CONTENT {
-    $$ = {
-      type: 'ContentStatement',
-      original: $1,
-      value: $1,
-      loc: yy.locInfo(@$)
-    };
-  };
-
-rawBlock
-  : openRawBlock content* END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$)
-  ;
-
-openRawBlock
-  : OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK -> { path: $2, params: $3, hash: $4 }
-  ;
-
-block
-  : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$)
-  | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$)
-  ;
-
-openBlock
-  : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { open: $1, path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
-  ;
-
-openInverse
-  : OPEN_INVERSE helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
-  ;
-
-openInverseChain
-  : OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
-  ;
-
-inverseAndProgram
-  : INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 }
-  ;
-
-inverseChain
-  : openInverseChain program inverseChain? {
-    var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$),
-        program = yy.prepareProgram([inverse], $2.loc);
-    program.chained = true;
-
-    $$ = { strip: $1.strip, program: program, chain: true };
-  }
-  | inverseAndProgram -> $1
-  ;
-
-closeBlock
-  : OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)}
-  ;
-
-mustache
-  // 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 helperName param* hash? CLOSE -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$)
-  | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$)
-  ;
-
-partial
-  : OPEN_PARTIAL partialName param* hash? CLOSE {
-    $$ = {
-      type: 'PartialStatement',
-      name: $2,
-      params: $3,
-      hash: $4,
-      indent: '',
-      strip: yy.stripFlags($1, $5),
-      loc: yy.locInfo(@$)
-    };
-  }
-  ;
-partialBlock
-  : openPartialBlock program closeBlock -> yy.preparePartialBlock($1, $2, $3, @$)
-  ;
-openPartialBlock
-  : OPEN_PARTIAL_BLOCK partialName param* hash? CLOSE -> { path: $2, params: $3, hash: $4, strip: yy.stripFlags($1, $5) }
-  ;
-
-param
-  : helperName -> $1
-  | sexpr -> $1
-  ;
-
-sexpr
-  : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR {
-    $$ = {
-      type: 'SubExpression',
-      path: $2,
-      params: $3,
-      hash: $4,
-      loc: yy.locInfo(@$)
-    };
-  };
-
-hash
-  : hashSegment+ -> {type: 'Hash', pairs: $1, loc: yy.locInfo(@$)}
-  ;
-
-hashSegment
-  : ID EQUALS param -> {type: 'HashPair', key: yy.id($1), value: $3, loc: yy.locInfo(@$)}
-  ;
-
-blockParams
-  : OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS -> yy.id($2)
-  ;
-
-helperName
-  : path -> $1
-  | dataName -> $1
-  | STRING -> {type: 'StringLiteral', value: $1, original: $1, loc: yy.locInfo(@$)}
-  | NUMBER -> {type: 'NumberLiteral', value: Number($1), original: Number($1), loc: yy.locInfo(@$)}
-  | BOOLEAN -> {type: 'BooleanLiteral', value: $1 === 'true', original: $1 === 'true', loc: yy.locInfo(@$)}
-  | UNDEFINED -> {type: 'UndefinedLiteral', original: undefined, value: undefined, loc: yy.locInfo(@$)}
-  | NULL -> {type: 'NullLiteral', original: null, value: null, loc: yy.locInfo(@$)}
-  ;
-
-partialName
-  : helperName -> $1
-  | sexpr -> $1
-  ;
-
-dataName
-  : DATA pathSegments -> yy.preparePath(true, $2, @$)
-  ;
-
-path
-  : pathSegments -> yy.preparePath(false, $1, @$)
-  ;
-
-pathSegments
-  : pathSegments SEP ID { $1.push({part: yy.id($3), original: $3, separator: $2}); $$ = $1; }
-  | ID -> [{part: yy.id($1), original: $1}]
-  ;
diff --git a/src/parser-prefix.js b/src/parser-prefix.js
deleted file mode 100644
index d9ed04116..000000000
--- a/src/parser-prefix.js
+++ /dev/null
@@ -1 +0,0 @@
-// File ignored in coverage tests via setting in .istanbul.yml
diff --git a/src/parser-suffix.js b/src/parser-suffix.js
deleted file mode 100644
index 6e4aa20d6..000000000
--- a/src/parser-suffix.js
+++ /dev/null
@@ -1 +0,0 @@
-export default handlebars;
diff --git a/tasks/parser.js b/tasks/parser.js
deleted file mode 100644
index 252b8c267..000000000
--- a/tasks/parser.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const { execFileWithInheritedOutput } = require('./util/exec-file');
-const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
-
-const OUTPUT_FILE = 'lib/handlebars/compiler/parser.js';
-
-module.exports = function(grunt) {
-  const registerAsyncTask = createRegisterAsyncTaskFn(grunt);
-
-  registerAsyncTask('parser', async () => {
-    await runJison();
-    combineWithPrefixAndSuffix();
-    grunt.log.writeln(`Parser "${OUTPUT_FILE}" created.`);
-  });
-
-  async function runJison() {
-    await execFileWithInheritedOutput('jison', [
-      '-m',
-      'js',
-      'src/handlebars.yy',
-      'src/handlebars.l'
-    ]);
-  }
-
-  function combineWithPrefixAndSuffix() {
-    const combinedParserSourceCode =
-      grunt.file.read('src/parser-prefix.js') +
-      grunt.file.read('handlebars.js') +
-      grunt.file.read('src/parser-suffix.js');
-
-    grunt.file.write(OUTPUT_FILE, combinedParserSourceCode);
-    grunt.file.delete('handlebars.js');
-  }
-};
diff --git a/types/index.d.ts b/types/index.d.ts
index 3f2f8b792..4275c50f4 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -13,6 +13,12 @@
  * https://github.com/DefinitelyTyped/DefinitelyTyped/commits/1ce60bdc07f10e0b076778c6c953271c072bc894/types/handlebars/index.d.ts
  */
 // TypeScript Version: 2.3
+import {
+    parse,
+    parseWithoutProcessing,
+    ParseOptions,
+    AST
+} from '@handlebars/parser';
 
 declare namespace Handlebars {
   export interface TemplateDelegate<T = any> {
@@ -50,10 +56,7 @@ declare namespace Handlebars {
       [key: string]: HelperDelegate;
   }
 
-  export interface ParseOptions {
-      srcName?: string;
-      ignoreStandalone?: boolean;
-  }
+  export { parse, parseWithoutProcessing, ParseOptions };
 
   export function registerHelper(name: string, fn: HelperDelegate): void;
   export function registerHelper(name: HelperDeclareSpec): void;
@@ -71,8 +74,7 @@ declare namespace Handlebars {
   export function createFrame(object: any): any;
   export function blockParams(obj: any[], ids: any[]): any[];
   export function log(level: number, obj: any): void;
-  export function parse(input: string, options?: ParseOptions): hbs.AST.Program;
-  export function parseWithoutProcessing(input: string, options?: ParseOptions): hbs.AST.Program;
+
   export function compile<T = any>(input: any, options?: CompileOptions): HandlebarsTemplateDelegate<T>;
   export function precompile(input: any, options?: PrecompileOptions): TemplateSpecification;
   export function template<T = any>(precompilation: TemplateSpecification): HandlebarsTemplateDelegate<T>;
@@ -127,7 +129,7 @@ declare namespace Handlebars {
       export const helpers: hbs.AST.helpers;
   }
 
-  interface ICompiler {
+  export interface ICompiler {
       accept(node: hbs.AST.Node): void;
       Program(program: hbs.AST.Program): void;
       BlockStatement(block: hbs.AST.BlockStatement): void;
@@ -191,25 +193,25 @@ declare namespace Handlebars {
 /**
 * Implement this interface on your MVW/MVVM/MVC views such as Backbone.View
 **/
-interface HandlebarsTemplatable {
+export interface HandlebarsTemplatable {
   template: HandlebarsTemplateDelegate;
 }
 
 // NOTE: for backward compatibility of this typing
-type HandlebarsTemplateDelegate<T = any> = Handlebars.TemplateDelegate<T>;
+export type HandlebarsTemplateDelegate<T = any> = Handlebars.TemplateDelegate<T>;
 
-interface HandlebarsTemplates {
+export interface HandlebarsTemplates {
   [index: string]: HandlebarsTemplateDelegate;
 }
 
-interface TemplateSpecification {
+export interface TemplateSpecification {
 
 }
 
 // for backward compatibility of this typing
-type RuntimeOptions = Handlebars.RuntimeOptions;
+export type RuntimeOptions = Handlebars.RuntimeOptions;
 
-interface CompileOptions {
+export interface CompileOptions {
   data?: boolean;
   compat?: boolean;
   knownHelpers?: KnownHelpers;
@@ -222,11 +224,11 @@ interface CompileOptions {
   explicitPartialContext?: boolean;
 }
 
-type KnownHelpers = {
+export type KnownHelpers = {
   [name in BuiltinHelperName | CustomHelperName]: boolean;
 };
 
-type BuiltinHelperName =
+export type BuiltinHelperName =
   "helperMissing"|
   "blockHelperMissing"|
   "each"|
@@ -236,21 +238,23 @@ type BuiltinHelperName =
   "log"|
   "lookup";
 
-type CustomHelperName = string;
+export type CustomHelperName = string;
 
-interface PrecompileOptions extends CompileOptions {
+export interface PrecompileOptions extends CompileOptions {
   srcName?: string;
   destName?: string;
 }
 
-declare namespace hbs {
+export namespace hbs {
   // for backward compatibility of this typing
-  type SafeString = Handlebars.SafeString;
+  export type SafeString = Handlebars.SafeString;
+
+  export type Utils = typeof Handlebars.Utils;
 
-  type Utils = typeof Handlebars.Utils;
+  export { AST }
 }
 
-interface Logger {
+export interface Logger {
   DEBUG: number;
   INFO: number;
   WARN: number;
@@ -262,161 +266,10 @@ interface Logger {
   log(level: number, obj: string): void;
 }
 
-type CompilerInfo = [number/* revision */, string /* versions */];
-
-declare namespace hbs {
-  namespace AST {
-      interface Node {
-          type: string;
-          loc: SourceLocation;
-      }
-
-      interface SourceLocation {
-          source: string;
-          start: Position;
-          end: Position;
-      }
-
-      interface Position {
-          line: number;
-          column: number;
-      }
-
-      interface Program extends Node {
-          body: Statement[];
-          blockParams: string[];
-      }
-
-      interface Statement extends Node {}
-
-      interface MustacheStatement extends Statement {
-          type: 'MustacheStatement';
-          path: PathExpression | Literal;
-          params: Expression[];
-          hash: Hash;
-          escaped: boolean;
-          strip: StripFlags;
-      }
-
-      interface Decorator extends MustacheStatement { }
-
-      interface BlockStatement extends Statement {
-          type: 'BlockStatement';
-          path: PathExpression;
-          params: Expression[];
-          hash: Hash;
-          program: Program;
-          inverse: Program;
-          openStrip: StripFlags;
-          inverseStrip: StripFlags;
-          closeStrip: StripFlags;
-      }
-
-      interface DecoratorBlock extends BlockStatement { }
-
-      interface PartialStatement extends Statement {
-          type: 'PartialStatement';
-          name: PathExpression | SubExpression;
-          params: Expression[];
-          hash: Hash;
-          indent: string;
-          strip: StripFlags;
-      }
-
-      interface PartialBlockStatement extends Statement {
-          type: 'PartialBlockStatement';
-          name: PathExpression | SubExpression;
-          params: Expression[];
-          hash: Hash;
-          program: Program;
-          openStrip: StripFlags;
-          closeStrip: StripFlags;
-      }
-
-      interface ContentStatement extends Statement {
-          type: 'ContentStatement';
-          value: string;
-          original: StripFlags;
-      }
-
-      interface CommentStatement extends Statement {
-          type: 'CommentStatement';
-          value: string;
-          strip: StripFlags;
-      }
-
-      interface Expression extends Node {}
-
-      interface SubExpression extends Expression {
-          type: 'SubExpression';
-          path: PathExpression;
-          params: Expression[];
-          hash: Hash;
-      }
-
-      interface PathExpression extends Expression {
-          type: 'PathExpression';
-          data: boolean;
-          depth: number;
-          parts: string[];
-          original: string;
-      }
-
-      interface Literal extends Expression {}
-      interface StringLiteral extends Literal {
-          type: 'StringLiteral';
-          value: string;
-          original: string;
-      }
-
-      interface BooleanLiteral extends Literal {
-          type: 'BooleanLiteral';
-          value: boolean;
-          original: boolean;
-      }
-
-      interface NumberLiteral extends Literal {
-          type: 'NumberLiteral';
-          value: number;
-          original: number;
-      }
-
-      interface UndefinedLiteral extends Literal {
-          type: 'UndefinedLiteral';
-	  }
-
-      interface NullLiteral extends Literal {
-          type: 'NullLiteral';
-	  }
-
-      interface Hash extends Node {
-          type: 'Hash';
-          pairs: HashPair[];
-      }
-
-      interface HashPair extends Node {
-          type: 'HashPair';
-          key: string;
-          value: Expression;
-      }
-
-      interface StripFlags {
-          open: boolean;
-          close: boolean;
-      }
-
-      interface helpers {
-          helperExpression(node: Node): boolean;
-          scopeId(path: PathExpression): boolean;
-          simpleId(path: PathExpression): boolean;
-      }
-  }
-}
-
-declare module "handlebars" {
-  export = Handlebars;
-}
+export type CompilerInfo = [number/* revision */, string /* versions */];
 
 declare module "handlebars/runtime" {
   export = Handlebars;
 }
+
+export default Handlebars;
