Version 1
[yaffs-website] / web / modules / contrib / advagg / advagg_js_minify / jsminplus.inc
diff --git a/web/modules/contrib/advagg/advagg_js_minify/jsminplus.inc b/web/modules/contrib/advagg/advagg_js_minify/jsminplus.inc
new file mode 100644 (file)
index 0000000..f3346d2
--- /dev/null
@@ -0,0 +1,2319 @@
+<?php
+// @codingStandardsIgnoreFile
+// @ignore comment_docblock_file:file
+// @ignore style_curly_braces:file
+// @ignore style_string_spacing:file
+// @ignore style_else_spacing:file
+// @ignore comment_comment_docblock_missing:file
+// @ignore comment_comment_eg:file
+// @ignore production_code:file
+// @ignore druplart_unary:file
+// @ignore style_uppercase_constants:file
+// @ignore comment_comment_space:file
+// @ignore druplart_conditional_assignment:file
+// @ignore style_paren_spacing:file
+// @ignore style_no_tabs:file
+// @ignore :file
+
+/**
+ * JSMinPlus version 1.4
+ *
+ * Minifies a javascript file using a javascript parser
+ *
+ * This implements a PHP port of Brendan Eich's Narcissus open source javascript engine (in javascript)
+ * References: http://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine)
+ * Narcissus sourcecode: http://mxr.mozilla.org/mozilla/source/js/narcissus/
+ * JSMinPlus weblog: http://crisp.tweakblogs.net/blog/cat/716
+ *
+ * Tino Zijdel <crisp@tweakers.net>
+ *
+ * Usage: $minified = JSMinPlus::minify($script [, $filename])
+ *
+ * Versionlog (see also changelog.txt):
+ * 23-07-2011 - remove dynamic creation of OP_* and KEYWORD_* defines and declare them on top
+ *              reduce memory footprint by minifying by block-scope
+ *              some small byte-saving and performance improvements
+ * 12-05-2009 - fixed hook:colon precedence, fixed empty body in loop and if-constructs
+ * 18-04-2009 - fixed crashbug in PHP 5.2.9 and several other bugfixes
+ * 12-04-2009 - some small bugfixes and performance improvements
+ * 09-04-2009 - initial open sourced version 1.0
+ *
+ * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip
+ *
+ * @file
+ */
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Narcissus JavaScript engine.
+ *
+ * The Initial Developer of the Original Code is
+ * Brendan Eich <brendan@mozilla.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2004
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s): Tino Zijdel <crisp@tweakers.net>
+ * PHP port, modifications and minifier routine are (C) 2009-2011
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('TOKEN_END', 1);
+define('TOKEN_NUMBER', 2);
+define('TOKEN_IDENTIFIER', 3);
+define('TOKEN_STRING', 4);
+define('TOKEN_REGEXP', 5);
+define('TOKEN_NEWLINE', 6);
+define('TOKEN_CONDCOMMENT_START', 7);
+define('TOKEN_CONDCOMMENT_END', 8);
+
+define('JS_SCRIPT', 100);
+define('JS_BLOCK', 101);
+define('JS_LABEL', 102);
+define('JS_FOR_IN', 103);
+define('JS_CALL', 104);
+define('JS_NEW_WITH_ARGS', 105);
+define('JS_INDEX', 106);
+define('JS_ARRAY_INIT', 107);
+define('JS_OBJECT_INIT', 108);
+define('JS_PROPERTY_INIT', 109);
+define('JS_GETTER', 110);
+define('JS_SETTER', 111);
+define('JS_GROUP', 112);
+define('JS_LIST', 113);
+
+define('JS_MINIFIED', 999);
+
+define('DECLARED_FORM', 0);
+define('EXPRESSED_FORM', 1);
+define('STATEMENT_FORM', 2);
+
+/* Operators */
+define('OP_SEMICOLON', ';');
+define('OP_COMMA', ',');
+define('OP_HOOK', '?');
+define('OP_COLON', ':');
+define('OP_OR', '||');
+define('OP_AND', '&&');
+define('OP_BITWISE_OR', '|');
+define('OP_BITWISE_XOR', '^');
+define('OP_BITWISE_AND', '&');
+define('OP_STRICT_EQ', '===');
+define('OP_EQ', '==');
+define('OP_ASSIGN', '=');
+define('OP_STRICT_NE', '!==');
+define('OP_NE', '!=');
+define('OP_LSH', '<<');
+define('OP_LE', '<=');
+define('OP_LT', '<');
+define('OP_URSH', '>>>');
+define('OP_RSH', '>>');
+define('OP_GE', '>=');
+define('OP_GT', '>');
+define('OP_INCREMENT', '++');
+define('OP_DECREMENT', '--');
+define('OP_PLUS', '+');
+define('OP_MINUS', '-');
+define('OP_MUL', '*');
+define('OP_DIV', '/');
+define('OP_MOD', '%');
+define('OP_NOT', '!');
+define('OP_BITWISE_NOT', '~');
+define('OP_DOT', '.');
+define('OP_LEFT_BRACKET', '[');
+define('OP_RIGHT_BRACKET', ']');
+define('OP_LEFT_CURLY', '{');
+define('OP_RIGHT_CURLY', '}');
+define('OP_LEFT_PAREN', '(');
+define('OP_RIGHT_PAREN', ')');
+define('OP_CONDCOMMENT_END', '@*/');
+
+define('OP_UNARY_PLUS', 'U+');
+define('OP_UNARY_MINUS', 'U-');
+
+/* Keywords */
+define('KEYWORD_BREAK', 'break');
+define('KEYWORD_CASE', 'case');
+define('KEYWORD_CATCH', 'catch');
+define('KEYWORD_CONST', 'const');
+define('KEYWORD_CONTINUE', 'continue');
+define('KEYWORD_DEBUGGER', 'debugger');
+define('KEYWORD_DEFAULT', 'default');
+define('KEYWORD_DELETE', 'delete');
+define('KEYWORD_DO', 'do');
+define('KEYWORD_ELSE', 'else');
+define('KEYWORD_ENUM', 'enum');
+define('KEYWORD_FALSE', 'false');
+define('KEYWORD_FINALLY', 'finally');
+define('KEYWORD_FOR', 'for');
+define('KEYWORD_FUNCTION', 'function');
+define('KEYWORD_IF', 'if');
+define('KEYWORD_IN', 'in');
+define('KEYWORD_INSTANCEOF', 'instanceof');
+define('KEYWORD_NEW', 'new');
+define('KEYWORD_NULL', 'null');
+define('KEYWORD_RETURN', 'return');
+define('KEYWORD_SWITCH', 'switch');
+define('KEYWORD_THIS', 'this');
+define('KEYWORD_THROW', 'throw');
+define('KEYWORD_TRUE', 'true');
+define('KEYWORD_TRY', 'try');
+define('KEYWORD_TYPEOF', 'typeof');
+define('KEYWORD_VAR', 'var');
+define('KEYWORD_VOID', 'void');
+define('KEYWORD_WHILE', 'while');
+define('KEYWORD_WITH', 'with');
+
+
+class JSMinPlus {
+  private $parser;
+  private $reserved = array(
+    'break',
+    'case',
+    'catch',
+    'continue',
+    'default',
+    'delete',
+    'do',
+    'else',
+    'finally',
+    'for',
+    'function',
+    'if',
+    'in',
+    'instanceof',
+    'new',
+    'return',
+    'switch',
+    'this',
+    'throw',
+    'try',
+    'typeof',
+    'var',
+    'void',
+    'while',
+    'with',
+    // Words reserved for future use
+    'abstract',
+    'boolean',
+    'byte',
+    'char',
+    'class',
+    'const',
+    'debugger',
+    'double',
+    'enum',
+    'export',
+    'extends',
+    'final',
+    'float',
+    'goto',
+    'implements',
+    'import',
+    'int',
+    'interface',
+    'long',
+    'native',
+    'package',
+    'private',
+    'protected',
+    'public',
+    'short',
+    'static',
+    'super',
+    'synchronized',
+    'throws',
+    'transient',
+    'volatile',
+    // These are not reserved, but should be taken into account
+    // in isValidIdentifier (See jslint source code)
+    'arguments',
+    'eval',
+    'true',
+    'false',
+    'Infinity',
+    'NaN',
+    'null',
+    'undefined',
+  );
+
+  private function __construct() {
+    $this->parser = new JSParser($this);
+  }
+
+  public static function minify($js, $filename = '') {
+    static $instance;
+
+    // this is a singleton
+    if (!$instance) {
+      $instance = new JSMinPlus();
+    }
+
+    return $instance->min($js, $filename);
+  }
+
+  private function min($js, $filename) {
+    try {
+      $n = $this->parser->parse($js, $filename, 1);
+      return $this->parseTree($n);
+    }
+    catch (Exception $e) {
+      echo $e->getMessage() . "\n";
+    }
+
+    return false;
+  }
+
+  public function parseTree($n, $noBlockGrouping = false) {
+    $s = '';
+
+    switch ($n->type) {
+      case JS_MINIFIED:
+        $s = $n->value;
+        break;
+
+      case JS_SCRIPT:
+        // we do nothing yet with funDecls or varDecls
+        $noBlockGrouping = true;
+        // FALL THROUGH
+
+      case JS_BLOCK:
+        $childs = $n->treeNodes;
+        $lastType = 0;
+        for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++) {
+          $type = $childs[$i]->type;
+          $t = $this->parseTree($childs[$i]);
+          if (strlen($t)) {
+            if ($c) {
+              $s = rtrim($s, ';');
+
+              if ($type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM) {
+                // put declared functions on a new line
+                $s .= "\n";
+              }
+              elseif ($type == KEYWORD_VAR && $type == $lastType) {
+                // multiple var-statements can go into one
+                $t = ',' . substr($t, 4);
+              }
+              else {
+                // add terminator
+                $s .= ';';
+              }
+            }
+
+            $s .= $t;
+
+            $c++;
+            $lastType = $type;
+          }
+        }
+
+        if ($c > 1 && !$noBlockGrouping) {
+          $s = '{' . $s . '}';
+        }
+        break;
+
+      case KEYWORD_FUNCTION:
+        $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '(';
+        $params = $n->params;
+        for ($i = 0, $j = count($params); $i < $j; $i++) {
+          $s .= ($i ? ',' : '') . $params[$i];
+        }
+        $s .= '){' . $this->parseTree($n->body, true) . '}';
+        break;
+
+      case KEYWORD_IF:
+        $s = 'if(' . $this->parseTree($n->condition) . ')';
+        $thenPart = $this->parseTree($n->thenPart);
+        $elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null;
+
+        // empty if-statement
+        if ($thenPart == '') {
+          $thenPart = ';';
+        }
+
+        if ($elsePart) {
+          // be careful and always make a block out of the thenPart; could be more optimized but is a lot of trouble
+          if ($thenPart != ';' && $thenPart[0] != '{') {
+            $thenPart = '{' . $thenPart . '}';
+          }
+
+          $s .= $thenPart . 'else';
+
+          // we could check for more, but that hardly ever applies so go for performance
+          if ($elsePart[0] != '{') {
+            $s .= ' ';
+          }
+
+          $s .= $elsePart;
+        }
+        else {
+          $s .= $thenPart;
+        }
+        break;
+
+      case KEYWORD_SWITCH:
+        $s = 'switch(' . $this->parseTree($n->discriminant) . '){';
+        $cases = $n->cases;
+        for ($i = 0, $j = count($cases); $i < $j; $i++) {
+          $case = $cases[$i];
+          if ($case->type == KEYWORD_CASE) {
+            $s .= 'case' . ($case->caseLabel->type != TOKEN_STRING ? ' ' : '') . $this->parseTree($case->caseLabel) . ':';
+          }
+          else {
+            $s .= 'default:';
+          }
+
+          $statement = $this->parseTree($case->statements, true);
+          if ($statement) {
+            $s .= $statement;
+            // no terminator for last statement
+            if ($i + 1 < $j) {
+              $s .= ';';
+            }
+          }
+        }
+        $s .= '}';
+        break;
+
+      case KEYWORD_FOR:
+        $s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '')
+                                       . ';' . ($n->condition ? $this->parseTree($n->condition) : '')
+                                       . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')';
+
+        $body  = $this->parseTree($n->body);
+        if ($body == '') {
+          $body = ';';
+        }
+
+        $s .= $body;
+        break;
+
+      case KEYWORD_WHILE:
+        $s = 'while(' . $this->parseTree($n->condition) . ')';
+
+        $body  = $this->parseTree($n->body);
+        if ($body == '') {
+          $body = ';';
+        }
+
+        $s .= $body;
+        break;
+
+      case JS_FOR_IN:
+        $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')';
+
+        $body  = $this->parseTree($n->body);
+        if ($body == '') {
+          $body = ';';
+        }
+
+        $s .= $body;
+        break;
+
+      case KEYWORD_DO:
+        $s = 'do{' . $this->parseTree($n->body, true) . '}while(' . $this->parseTree($n->condition) . ')';
+        break;
+
+      case KEYWORD_BREAK:
+      case KEYWORD_CONTINUE:
+        $s = $n->value . ($n->label ? ' ' . $n->label : '');
+        break;
+
+      case KEYWORD_TRY:
+        $s = 'try{' . $this->parseTree($n->tryBlock, true) . '}';
+        $catchClauses = $n->catchClauses;
+        for ($i = 0, $j = count($catchClauses); $i < $j; $i++) {
+          $t = $catchClauses[$i];
+          $s .= 'catch(' . $t->varName . ($t->guard ? ' if ' . $this->parseTree($t->guard) : '') . '){' . $this->parseTree($t->block, true) . '}';
+        }
+        if ($n->finallyBlock) {
+          $s .= 'finally{' . $this->parseTree($n->finallyBlock, true) . '}';
+        }
+        break;
+
+      case KEYWORD_THROW:
+      case KEYWORD_RETURN:
+        $s = $n->type;
+        if ($n->value) {
+          $t = $this->parseTree($n->value);
+          if (strlen($t)) {
+            if ($this->isWordChar($t[0]) || $t[0] == '\\') {
+              $s .= ' ';
+            }
+
+            $s .= $t;
+          }
+        }
+        break;
+
+      case KEYWORD_WITH:
+        $s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
+        break;
+
+      case KEYWORD_VAR:
+      case KEYWORD_CONST:
+        $s = $n->value . ' ';
+        $childs = $n->treeNodes;
+        for ($i = 0, $j = count($childs); $i < $j; $i++) {
+          $t = $childs[$i];
+          $s .= ($i ? ',' : '') . $t->name;
+          $u = $t->initializer;
+          if ($u) {
+            $s .= '=' . $this->parseTree($u);
+          }
+        }
+        break;
+
+      case KEYWORD_IN:
+      case KEYWORD_INSTANCEOF:
+        $left = $this->parseTree($n->treeNodes[0]);
+        $right = $this->parseTree($n->treeNodes[1]);
+
+        $s = $left;
+
+        if ($this->isWordChar(substr($left, -1))) {
+          $s .= ' ';
+        }
+
+        $s .= $n->type;
+
+        if ($this->isWordChar($right[0]) || $right[0] == '\\') {
+          $s .= ' ';
+        }
+
+        $s .= $right;
+        break;
+
+      case KEYWORD_DELETE:
+      case KEYWORD_TYPEOF:
+        $right = $this->parseTree($n->treeNodes[0]);
+
+        $s = $n->type;
+
+        if ($this->isWordChar($right[0]) || $right[0] == '\\') {
+          $s .= ' ';
+        }
+
+        $s .= $right;
+        break;
+
+      case KEYWORD_VOID:
+        $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')';
+        break;
+
+      case KEYWORD_DEBUGGER:
+        throw new Exception('NOT IMPLEMENTED: DEBUGGER');
+        break;
+
+      case TOKEN_CONDCOMMENT_START:
+      case TOKEN_CONDCOMMENT_END:
+        $s = $n->value . ($n->type == TOKEN_CONDCOMMENT_START ? ' ' : '');
+        $childs = $n->treeNodes;
+        for ($i = 0, $j = count($childs); $i < $j; $i++) {
+          $s .= $this->parseTree($childs[$i]);
+        }
+        break;
+
+      case OP_SEMICOLON:
+        if ($expression = $n->expression) {
+          $s = $this->parseTree($expression);
+        }
+        break;
+
+      case JS_LABEL:
+        $s = $n->label . ':' . $this->parseTree($n->statement);
+        break;
+
+      case OP_COMMA:
+        $childs = $n->treeNodes;
+        for ($i = 0, $j = count($childs); $i < $j; $i++) {
+          $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
+        }
+        break;
+
+      case OP_ASSIGN:
+        $s = $this->parseTree($n->treeNodes[0]) . $n->value . $this->parseTree($n->treeNodes[1]);
+        break;
+
+      case OP_HOOK:
+        $s = $this->parseTree($n->treeNodes[0]) . '?' . $this->parseTree($n->treeNodes[1]) . ':' . $this->parseTree($n->treeNodes[2]);
+        break;
+
+      case OP_OR:
+      case OP_AND:
+      case OP_BITWISE_OR:
+      case OP_BITWISE_XOR:
+      case OP_BITWISE_AND:
+      case OP_EQ:
+      case OP_NE:
+      case OP_STRICT_EQ:
+      case OP_STRICT_NE:
+      case OP_LT:
+      case OP_LE:
+      case OP_GE:
+      case OP_GT:
+      case OP_LSH:
+      case OP_RSH:
+      case OP_URSH:
+      case OP_MUL:
+      case OP_DIV:
+      case OP_MOD:
+        $s = $this->parseTree($n->treeNodes[0]) . $n->type . $this->parseTree($n->treeNodes[1]);
+        break;
+
+      case OP_PLUS:
+      case OP_MINUS:
+        $left = $this->parseTree($n->treeNodes[0]);
+        $right = $this->parseTree($n->treeNodes[1]);
+
+        switch ($n->treeNodes[1]->type) {
+          case OP_PLUS:
+          case OP_MINUS:
+          case OP_INCREMENT:
+          case OP_DECREMENT:
+          case OP_UNARY_PLUS:
+          case OP_UNARY_MINUS:
+            $s = $left . $n->type . ' ' . $right;
+            break;
+
+          case TOKEN_STRING:
+            //combine concatenated strings with same quote style
+            if ($n->type == OP_PLUS && substr($left, -1) == $right[0]) {
+              $s = substr($left, 0, -1) . substr($right, 1);
+              break;
+            }
+            // FALL THROUGH
+
+          default:
+            $s = $left . $n->type . $right;
+        }
+        break;
+
+      case OP_NOT:
+      case OP_BITWISE_NOT:
+      case OP_UNARY_PLUS:
+      case OP_UNARY_MINUS:
+        $s = $n->value . $this->parseTree($n->treeNodes[0]);
+        break;
+
+      case OP_INCREMENT:
+      case OP_DECREMENT:
+        if ($n->postfix) {
+          $s = $this->parseTree($n->treeNodes[0]) . $n->value;
+        }
+        else {
+          $s = $n->value . $this->parseTree($n->treeNodes[0]);
+        }
+        break;
+
+      case OP_DOT:
+        $s = $this->parseTree($n->treeNodes[0]) . '.' . $this->parseTree($n->treeNodes[1]);
+        break;
+
+      case JS_INDEX:
+        $s = $this->parseTree($n->treeNodes[0]);
+        // See if we can replace named index with a dot saving 3 bytes
+        if (   $n->treeNodes[0]->type == TOKEN_IDENTIFIER &&
+                                       $n->treeNodes[1]->type == TOKEN_STRING &&
+                                       $this->isValidIdentifier(substr($n->treeNodes[1]->value, 1, -1))
+                               ) {
+          $s .= '.' . substr($n->treeNodes[1]->value, 1, -1);
+        }
+        else {
+          $s .= '[' . $this->parseTree($n->treeNodes[1]) . ']';
+        }
+        break;
+
+      case JS_LIST:
+        $childs = $n->treeNodes;
+        for ($i = 0, $j = count($childs); $i < $j; $i++) {
+          $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
+        }
+        break;
+
+      case JS_CALL:
+        $s = $this->parseTree($n->treeNodes[0]) . '(' . $this->parseTree($n->treeNodes[1]) . ')';
+        break;
+
+      case KEYWORD_NEW:
+      case JS_NEW_WITH_ARGS:
+        $s = 'new ' . $this->parseTree($n->treeNodes[0]) . '(' . ($n->type == JS_NEW_WITH_ARGS ? $this->parseTree($n->treeNodes[1]) : '') . ')';
+        break;
+
+      case JS_ARRAY_INIT:
+        $s = '[';
+        $childs = $n->treeNodes;
+        for ($i = 0, $j = count($childs); $i < $j; $i++) {
+          $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
+        }
+        $s .= ']';
+        break;
+
+      case JS_OBJECT_INIT:
+        $s = '{';
+        $childs = $n->treeNodes;
+        for ($i = 0, $j = count($childs); $i < $j; $i++) {
+          $t = $childs[$i];
+          if ($i) {
+            $s .= ',';
+          }
+          if ($t->type == JS_PROPERTY_INIT) {
+            // Ditch the quotes when the index is a valid identifier
+            if (       $t->treeNodes[0]->type == TOKEN_STRING &&
+                                                       $this->isValidIdentifier(substr($t->treeNodes[0]->value, 1, -1))
+                                               ) {
+              $s .= substr($t->treeNodes[0]->value, 1, -1);
+            }
+            else {
+              $s .= $t->treeNodes[0]->value;
+            }
+
+            $s .= ':' . $this->parseTree($t->treeNodes[1]);
+          }
+          else {
+            $s .= $t->type == JS_GETTER ? 'get' : 'set';
+            $s .= ' ' . $t->name . '(';
+            $params = $t->params;
+            for ($i = 0, $j = count($params); $i < $j; $i++) {
+              $s .= ($i ? ',' : '') . $params[$i];
+            }
+            $s .= '){' . $this->parseTree($t->body, true) . '}';
+          }
+        }
+        $s .= '}';
+        break;
+
+      case TOKEN_NUMBER:
+        $s = $n->value;
+        if (preg_match('/^([1-9]+)(0{3,})$/', $s, $m)) {
+          $s = $m[1] . 'e' . strlen($m[2]);
+        }
+        break;
+
+      case KEYWORD_NULL:
+      case KEYWORD_THIS:
+      case KEYWORD_TRUE:
+      case KEYWORD_FALSE:
+      case TOKEN_IDENTIFIER:
+      case TOKEN_STRING:
+      case TOKEN_REGEXP:
+        $s = $n->value;
+        break;
+
+      case JS_GROUP:
+        if (in_array(
+                                       $n->treeNodes[0]->type,
+                                       array(
+          JS_ARRAY_INIT,
+          JS_OBJECT_INIT,
+          JS_GROUP,
+          TOKEN_NUMBER,
+          TOKEN_STRING,
+          TOKEN_REGEXP,
+          TOKEN_IDENTIFIER,
+          KEYWORD_NULL,
+          KEYWORD_THIS,
+          KEYWORD_TRUE,
+          KEYWORD_FALSE,
+        )
+                               )) {
+          $s = $this->parseTree($n->treeNodes[0]);
+        }
+        else {
+          $s = '(' . $this->parseTree($n->treeNodes[0]) . ')';
+        }
+        break;
+
+      default:
+        throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type);
+    }
+
+    return $s;
+  }
+
+  private function isValidIdentifier($string) {
+    return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved);
+  }
+
+  private function isWordChar($char) {
+    return $char == '_' || $char == '$' || ctype_alnum($char);
+  }
+}
+
+class JSParser {
+  private $t;
+  private $minifier;
+
+  private $opPrecedence = array(
+    ';' => 0,
+    ',' => 1,
+    '=' => 2,
+    '?' => 2,
+    ':' => 2,
+    // The above all have to have the same precedence, see bug 330975
+    '||' => 4,
+    '&&' => 5,
+    '|' => 6,
+    '^' => 7,
+    '&' => 8,
+    '==' => 9,
+    '!=' => 9,
+    '===' => 9,
+    '!==' => 9,
+    '<' => 10,
+    '<=' => 10,
+    '>=' => 10,
+    '>' => 10,
+    'in' => 10,
+    'instanceof' => 10,
+    '<<' => 11,
+    '>>' => 11,
+    '>>>' => 11,
+    '+' => 12,
+    '-' => 12,
+    '*' => 13,
+    '/' => 13,
+    '%' => 13,
+    'delete' => 14,
+    'void' => 14,
+    'typeof' => 14,
+    '!' => 14,
+    '~' => 14,
+    'U+' => 14,
+    'U-' => 14,
+    '++' => 15,
+    '--' => 15,
+    'new' => 16,
+    '.' => 17,
+    JS_NEW_WITH_ARGS => 0,
+    JS_INDEX => 0,
+    JS_CALL => 0,
+    JS_ARRAY_INIT => 0,
+    JS_OBJECT_INIT => 0,
+    JS_GROUP => 0,
+  );
+
+  private $opArity = array(
+    ',' => -2,
+    '=' => 2,
+    '?' => 3,
+    '||' => 2,
+    '&&' => 2,
+    '|' => 2,
+    '^' => 2,
+    '&' => 2,
+    '==' => 2,
+    '!=' => 2,
+    '===' => 2,
+    '!==' => 2,
+    '<' => 2,
+    '<=' => 2,
+    '>=' => 2,
+    '>' => 2,
+    'in' => 2,
+    'instanceof' => 2,
+    '<<' => 2,
+    '>>' => 2,
+    '>>>' => 2,
+    '+' => 2,
+    '-' => 2,
+    '*' => 2,
+    '/' => 2,
+    '%' => 2,
+    'delete' => 1,
+    'void' => 1,
+    'typeof' => 1,
+    '!' => 1,
+    '~' => 1,
+    'U+' => 1,
+    'U-' => 1,
+    '++' => 1,
+    '--' => 1,
+    'new' => 1,
+    '.' => 2,
+    JS_NEW_WITH_ARGS => 2,
+    JS_INDEX => 2,
+    JS_CALL => 2,
+    JS_ARRAY_INIT => 1,
+    JS_OBJECT_INIT => 1,
+    JS_GROUP => 1,
+    TOKEN_CONDCOMMENT_START => 1,
+    TOKEN_CONDCOMMENT_END => 1,
+  );
+
+  public function __construct($minifier = null) {
+    $this->minifier = $minifier;
+    $this->t = new JSTokenizer();
+  }
+
+  public function parse($s, $f, $l) {
+    // initialize tokenizer
+    $this->t->init($s, $f, $l);
+
+    $x = new JSCompilerContext(false);
+    $n = $this->Script($x);
+    if (!$this->t->isDone()) {
+      throw $this->t->newSyntaxError('Syntax error');
+    }
+
+    return $n;
+  }
+
+  private function Script($x) {
+    $n = $this->Statements($x);
+    $n->type = JS_SCRIPT;
+    $n->funDecls = $x->funDecls;
+    $n->varDecls = $x->varDecls;
+
+    // minify by scope
+    if ($this->minifier) {
+      $n->value = $this->minifier->parseTree($n);
+
+      // clear tree from node to save memory
+      $n->treeNodes = null;
+      $n->funDecls = null;
+      $n->varDecls = null;
+
+      $n->type = JS_MINIFIED;
+    }
+
+    return $n;
+  }
+
+  private function Statements($x) {
+    $n = new JSNode($this->t, JS_BLOCK);
+    array_push($x->stmtStack, $n);
+
+    while (!$this->t->isDone() && $this->t->peek() != OP_RIGHT_CURLY) {
+      $n->addNode($this->Statement($x));
+    }
+
+    array_pop($x->stmtStack);
+
+    return $n;
+  }
+
+  private function Block($x) {
+    $this->t->mustMatch(OP_LEFT_CURLY);
+    $n = $this->Statements($x);
+    $this->t->mustMatch(OP_RIGHT_CURLY);
+
+    return $n;
+  }
+
+  private function Statement($x) {
+    $tt = $this->t->get();
+    $n2 = null;
+
+    // Cases for statements ending in a right curly return early, avoiding the
+    // common semicolon insertion magic after this switch.
+    switch ($tt) {
+      case KEYWORD_FUNCTION:
+        return $this->FunctionDefinition(
+                                       $x,
+                                       true,
+                                       count($x->stmtStack) > 1 ? STATEMENT_FORM : DECLARED_FORM
+                               );
+        break;
+
+      case OP_LEFT_CURLY:
+        $n = $this->Statements($x);
+        $this->t->mustMatch(OP_RIGHT_CURLY);
+        return $n;
+
+      case KEYWORD_IF:
+        $n = new JSNode($this->t);
+        $n->condition = $this->ParenExpression($x);
+        array_push($x->stmtStack, $n);
+        $n->thenPart = $this->Statement($x);
+        $n->elsePart = $this->t->match(KEYWORD_ELSE) ? $this->Statement($x) : null;
+        array_pop($x->stmtStack);
+        return $n;
+
+      case KEYWORD_SWITCH:
+        $n = new JSNode($this->t);
+        $this->t->mustMatch(OP_LEFT_PAREN);
+        $n->discriminant = $this->Expression($x);
+        $this->t->mustMatch(OP_RIGHT_PAREN);
+        $n->cases = array();
+        $n->defaultIndex = -1;
+
+        array_push($x->stmtStack, $n);
+
+        $this->t->mustMatch(OP_LEFT_CURLY);
+
+        while (($tt = $this->t->get()) != OP_RIGHT_CURLY) {
+          switch ($tt) {
+            case KEYWORD_DEFAULT:
+              if ($n->defaultIndex >= 0) {
+                throw $this->t->newSyntaxError('More than one switch default');
+              }
+              // FALL THROUGH
+            case KEYWORD_CASE:
+              $n2 = new JSNode($this->t);
+              if ($tt == KEYWORD_DEFAULT) {
+                $n->defaultIndex = count($n->cases);
+              }
+              else {
+                $n2->caseLabel = $this->Expression($x, OP_COLON);
+              }
+              break;
+            default:
+              throw $this->t->newSyntaxError('Invalid switch case');
+          }
+
+          $this->t->mustMatch(OP_COLON);
+          $n2->statements = new JSNode($this->t, JS_BLOCK);
+          while (($tt = $this->t->peek()) != KEYWORD_CASE && $tt != KEYWORD_DEFAULT && $tt != OP_RIGHT_CURLY) {
+            $n2->statements->addNode($this->Statement($x));
+          }
+
+          array_push($n->cases, $n2);
+        }
+
+        array_pop($x->stmtStack);
+        return $n;
+
+      case KEYWORD_FOR:
+        $n = new JSNode($this->t);
+        $n->isLoop = true;
+        $this->t->mustMatch(OP_LEFT_PAREN);
+
+        if (($tt = $this->t->peek()) != OP_SEMICOLON) {
+          $x->inForLoopInit = true;
+          if ($tt == KEYWORD_VAR || $tt == KEYWORD_CONST) {
+            $this->t->get();
+            $n2 = $this->Variables($x);
+          }
+          else {
+            $n2 = $this->Expression($x);
+          }
+          $x->inForLoopInit = false;
+        }
+
+        if ($n2 && $this->t->match(KEYWORD_IN)) {
+          $n->type = JS_FOR_IN;
+          if ($n2->type == KEYWORD_VAR) {
+            if (count($n2->treeNodes) != 1) {
+              throw $this->t->SyntaxError(
+                                                               'Invalid for..in left-hand side',
+                                                               $this->t->filename,
+                                                               $n2->lineno
+                                                       );
+            }
+
+            // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name.
+            $n->iterator = $n2->treeNodes[0];
+            $n->varDecl = $n2;
+          }
+          else {
+            $n->iterator = $n2;
+            $n->varDecl = null;
+          }
+
+          $n->object = $this->Expression($x);
+        }
+        else {
+          $n->setup = $n2 ? $n2 : null;
+          $this->t->mustMatch(OP_SEMICOLON);
+          $n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x);
+          $this->t->mustMatch(OP_SEMICOLON);
+          $n->update = $this->t->peek() == OP_RIGHT_PAREN ? null : $this->Expression($x);
+        }
+
+        $this->t->mustMatch(OP_RIGHT_PAREN);
+        $n->body = $this->nest($x, $n);
+        return $n;
+
+      case KEYWORD_WHILE:
+        $n = new JSNode($this->t);
+        $n->isLoop = true;
+        $n->condition = $this->ParenExpression($x);
+        $n->body = $this->nest($x, $n);
+        return $n;
+
+      case KEYWORD_DO:
+        $n = new JSNode($this->t);
+        $n->isLoop = true;
+        $n->body = $this->nest($x, $n, KEYWORD_WHILE);
+        $n->condition = $this->ParenExpression($x);
+        if (!$x->ecmaStrictMode) {
+          // <script language="JavaScript"> (without version hints) may need
+          // automatic semicolon insertion without a newline after do-while.
+          // See http://bugzilla.mozilla.org/show_bug.cgi?id=238945.
+          $this->t->match(OP_SEMICOLON);
+          return $n;
+        }
+        break;
+
+      case KEYWORD_BREAK:
+      case KEYWORD_CONTINUE:
+        $n = new JSNode($this->t);
+
+        if ($this->t->peekOnSameLine() == TOKEN_IDENTIFIER) {
+          $this->t->get();
+          $n->label = $this->t->currentToken()->value;
+        }
+
+        $ss = $x->stmtStack;
+        $i = count($ss);
+        $label = $n->label;
+        if ($label) {
+          do {
+            if (--$i < 0) {
+              throw $this->t->newSyntaxError('Label not found');
+            }
+          } while ($ss[$i]->label != $label);
+        }
+        else {
+          do {
+            if (--$i < 0) {
+              throw $this->t->newSyntaxError('Invalid ' . $tt);
+            }
+          } while (!$ss[$i]->isLoop && ($tt != KEYWORD_BREAK || $ss[$i]->type != KEYWORD_SWITCH));
+        }
+
+        $n->target = $ss[$i];
+        break;
+
+      case KEYWORD_TRY:
+        $n = new JSNode($this->t);
+        $n->tryBlock = $this->Block($x);
+        $n->catchClauses = array();
+
+        while ($this->t->match(KEYWORD_CATCH)) {
+          $n2 = new JSNode($this->t);
+          $this->t->mustMatch(OP_LEFT_PAREN);
+          $n2->varName = $this->t->mustMatch(TOKEN_IDENTIFIER)->value;
+
+          if ($this->t->match(KEYWORD_IF)) {
+            if ($x->ecmaStrictMode) {
+              throw $this->t->newSyntaxError('Illegal catch guard');
+            }
+
+            if (count($n->catchClauses) && !end($n->catchClauses)->guard) {
+              throw $this->t->newSyntaxError('Guarded catch after unguarded');
+            }
+
+            $n2->guard = $this->Expression($x);
+          }
+          else {
+            $n2->guard = null;
+          }
+
+          $this->t->mustMatch(OP_RIGHT_PAREN);
+          $n2->block = $this->Block($x);
+          array_push($n->catchClauses, $n2);
+        }
+
+        if ($this->t->match(KEYWORD_FINALLY)) {
+          $n->finallyBlock = $this->Block($x);
+        }
+
+        if (!count($n->catchClauses) && !$n->finallyBlock) {
+          throw $this->t->newSyntaxError('Invalid try statement');
+        }
+        return $n;
+
+      case KEYWORD_CATCH:
+      case KEYWORD_FINALLY:
+        throw $this->t->newSyntaxError($tt . ' without preceding try');
+
+      case KEYWORD_THROW:
+        $n = new JSNode($this->t);
+        $n->value = $this->Expression($x);
+        break;
+
+      case KEYWORD_RETURN:
+        if (!$x->inFunction) {
+          throw $this->t->newSyntaxError('Invalid return');
+        }
+
+        $n = new JSNode($this->t);
+        $tt = $this->t->peekOnSameLine();
+        if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY) {
+          $n->value = $this->Expression($x);
+        }
+        else {
+          $n->value = null;
+        }
+        break;
+
+      case KEYWORD_WITH:
+        $n = new JSNode($this->t);
+        $n->object = $this->ParenExpression($x);
+        $n->body = $this->nest($x, $n);
+        return $n;
+
+      case KEYWORD_VAR:
+      case KEYWORD_CONST:
+        $n = $this->Variables($x);
+        break;
+
+      case TOKEN_CONDCOMMENT_START:
+      case TOKEN_CONDCOMMENT_END:
+        $n = new JSNode($this->t);
+        return $n;
+
+      case KEYWORD_DEBUGGER:
+        $n = new JSNode($this->t);
+        break;
+
+      case TOKEN_NEWLINE:
+      case OP_SEMICOLON:
+        $n = new JSNode($this->t, OP_SEMICOLON);
+        $n->expression = null;
+        return $n;
+
+      default:
+        if ($tt == TOKEN_IDENTIFIER) {
+          $this->t->scanOperand = false;
+          $tt = $this->t->peek();
+          $this->t->scanOperand = true;
+          if ($tt == OP_COLON) {
+            $label = $this->t->currentToken()->value;
+            $ss = $x->stmtStack;
+            for ($i = count($ss) - 1; $i >= 0; --$i) {
+              if ($ss[$i]->label == $label) {
+                throw $this->t->newSyntaxError('Duplicate label');
+              }
+            }
+
+            $this->t->get();
+            $n = new JSNode($this->t, JS_LABEL);
+            $n->label = $label;
+            $n->statement = $this->nest($x, $n);
+
+            return $n;
+          }
+        }
+
+        $n = new JSNode($this->t, OP_SEMICOLON);
+        $this->t->unget();
+        $n->expression = $this->Expression($x);
+        $n->end = $n->expression->end;
+        break;
+    }
+
+    if ($this->t->lineno == $this->t->currentToken()->lineno) {
+      $tt = $this->t->peekOnSameLine();
+      if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY) {
+        throw $this->t->newSyntaxError('Missing ; before statement');
+      }
+    }
+
+    $this->t->match(OP_SEMICOLON);
+
+    return $n;
+  }
+
+  private function FunctionDefinition($x, $requireName, $functionForm) {
+    $f = new JSNode($this->t);
+
+    if ($f->type != KEYWORD_FUNCTION) {
+      $f->type = ($f->value == 'get') ? JS_GETTER : JS_SETTER;
+    }
+
+    if ($this->t->match(TOKEN_IDENTIFIER)) {
+      $f->name = $this->t->currentToken()->value;
+    }
+    elseif ($requireName) {
+      throw $this->t->newSyntaxError('Missing function identifier');
+    }
+
+    $this->t->mustMatch(OP_LEFT_PAREN);
+    $f->params = array();
+
+    while (($tt = $this->t->get()) != OP_RIGHT_PAREN) {
+      if ($tt != TOKEN_IDENTIFIER) {
+        throw $this->t->newSyntaxError('Missing formal parameter');
+      }
+
+      array_push($f->params, $this->t->currentToken()->value);
+
+      if ($this->t->peek() != OP_RIGHT_PAREN) {
+        $this->t->mustMatch(OP_COMMA);
+      }
+    }
+
+    $this->t->mustMatch(OP_LEFT_CURLY);
+
+    $x2 = new JSCompilerContext(true);
+    $f->body = $this->Script($x2);
+
+    $this->t->mustMatch(OP_RIGHT_CURLY);
+    $f->end = $this->t->currentToken()->end;
+
+    $f->functionForm = $functionForm;
+    if ($functionForm == DECLARED_FORM) {
+      array_push($x->funDecls, $f);
+    }
+
+    return $f;
+  }
+
+  private function Variables($x) {
+    $n = new JSNode($this->t);
+
+    do {
+      $this->t->mustMatch(TOKEN_IDENTIFIER);
+
+      $n2 = new JSNode($this->t);
+      $n2->name = $n2->value;
+
+      if ($this->t->match(OP_ASSIGN)) {
+        if ($this->t->currentToken()->assignOp) {
+          throw $this->t->newSyntaxError('Invalid variable initialization');
+        }
+
+        $n2->initializer = $this->Expression($x, OP_COMMA);
+      }
+
+      $n2->readOnly = $n->type == KEYWORD_CONST;
+
+      $n->addNode($n2);
+      array_push($x->varDecls, $n2);
+    } while ($this->t->match(OP_COMMA));
+
+    return $n;
+  }
+
+  private function Expression($x, $stop = false) {
+    $operators = array();
+    $operands = array();
+    $n = false;
+
+    $bl = $x->bracketLevel;
+    $cl = $x->curlyLevel;
+    $pl = $x->parenLevel;
+    $hl = $x->hookLevel;
+
+    while (($tt = $this->t->get()) != TOKEN_END) {
+      if ($tt == $stop &&
+                               $x->bracketLevel == $bl &&
+                               $x->curlyLevel == $cl &&
+                               $x->parenLevel == $pl &&
+                               $x->hookLevel == $hl
+                       ) {
+        // Stop only if tt matches the optional stop parameter, and that
+        // token is not quoted by some kind of bracket.
+        break;
+      }
+
+      switch ($tt) {
+        case OP_SEMICOLON:
+          // NB: cannot be empty, Statement handled that.
+          break 2;
+
+        case OP_HOOK:
+          if ($this->t->scanOperand) {
+            break 2;
+          }
+
+          while (      !empty($operators) &&
+                                               $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]
+                                       ) {
+            $this->reduce($operators, $operands);
+          }
+
+          array_push($operators, new JSNode($this->t));
+
+          ++$x->hookLevel;
+          $this->t->scanOperand = true;
+          $n = $this->Expression($x);
+
+          if (!$this->t->match(OP_COLON)) {
+            break 2;
+          }
+
+          --$x->hookLevel;
+          array_push($operands, $n);
+          break;
+
+        case OP_COLON:
+          if ($x->hookLevel) {
+            break 2;
+          }
+
+          throw $this->t->newSyntaxError('Invalid label');
+          break;
+
+        case OP_ASSIGN:
+          if ($this->t->scanOperand) {
+            break 2;
+          }
+
+          // Use >, not >=, for right-associative ASSIGN
+          while (      !empty($operators) &&
+                                               $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]
+                                       ) {
+            $this->reduce($operators, $operands);
+          }
+
+          array_push($operators, new JSNode($this->t));
+          end($operands)->assignOp = $this->t->currentToken()->assignOp;
+          $this->t->scanOperand = true;
+          break;
+
+        case KEYWORD_IN:
+          // An in operator should not be parsed if we're parsing the head of
+          // a for (...) loop, unless it is in the then part of a conditional
+          // expression, or parenthesized somehow.
+          if ($x->inForLoopInit && !$x->hookLevel &&
+                                               !$x->bracketLevel && !$x->curlyLevel &&
+                                               !$x->parenLevel
+                                       ) {
+            break 2;
+          }
+          // FALL THROUGH
+        case OP_COMMA:
+          // A comma operator should not be parsed if we're parsing the then part
+          // of a conditional expression unless it's parenthesized somehow.
+          if ($tt == OP_COMMA && $x->hookLevel &&
+                                               !$x->bracketLevel && !$x->curlyLevel &&
+                                               !$x->parenLevel
+                                       ) {
+            break 2;
+          }
+          // Treat comma as left-associative so reduce can fold left-heavy
+          // COMMA trees into a single array.
+          // FALL THROUGH
+        case OP_OR:
+        case OP_AND:
+        case OP_BITWISE_OR:
+        case OP_BITWISE_XOR:
+        case OP_BITWISE_AND:
+        case OP_EQ:
+        case OP_NE:
+        case OP_STRICT_EQ:
+        case OP_STRICT_NE:
+        case OP_LT:
+        case OP_LE:
+        case OP_GE:
+        case OP_GT:
+        case KEYWORD_INSTANCEOF:
+        case OP_LSH:
+        case OP_RSH:
+        case OP_URSH:
+        case OP_PLUS:
+        case OP_MINUS:
+        case OP_MUL:
+        case OP_DIV:
+        case OP_MOD:
+        case OP_DOT:
+          if ($this->t->scanOperand) {
+            break 2;
+          }
+
+          while (      !empty($operators) &&
+                                               $this->opPrecedence[end($operators)->type] >= $this->opPrecedence[$tt]
+                                       ) {
+            $this->reduce($operators, $operands);
+          }
+
+          if ($tt == OP_DOT) {
+            $this->t->mustMatch(TOKEN_IDENTIFIER);
+            array_push($operands, new JSNode($this->t, OP_DOT, array_pop($operands), new JSNode($this->t)));
+          }
+          else {
+            array_push($operators, new JSNode($this->t));
+            $this->t->scanOperand = true;
+          }
+          break;
+
+        case KEYWORD_DELETE:
+        case KEYWORD_VOID:
+        case KEYWORD_TYPEOF:
+        case OP_NOT:
+        case OP_BITWISE_NOT:
+        case OP_UNARY_PLUS:
+        case OP_UNARY_MINUS:
+        case KEYWORD_NEW:
+          if (!$this->t->scanOperand) {
+            break 2;
+          }
+
+          array_push($operators, new JSNode($this->t));
+          break;
+
+        case OP_INCREMENT:
+        case OP_DECREMENT:
+          if ($this->t->scanOperand) {
+            array_push($operators, new JSNode($this->t)); // prefix increment or decrement
+          }
+          else {
+            // Don't cross a line boundary for postfix {in,de}crement.
+            $t = $this->t->tokens[($this->t->tokenIndex + $this->t->lookahead - 1) & 3];
+            if ($t && $t->lineno != $this->t->lineno) {
+              break 2;
+            }
+
+            if (!empty($operators)) {
+              // Use >, not >=, so postfix has higher precedence than prefix.
+              while ($this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]) {
+                $this->reduce($operators, $operands);
+              }
+            }
+
+            $n = new JSNode($this->t, $tt, array_pop($operands));
+            $n->postfix = true;
+            array_push($operands, $n);
+          }
+          break;
+
+        case KEYWORD_FUNCTION:
+          if (!$this->t->scanOperand) {
+            break 2;
+          }
+
+          array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM));
+          $this->t->scanOperand = false;
+          break;
+
+        case KEYWORD_NULL:
+        case KEYWORD_THIS:
+        case KEYWORD_TRUE:
+        case KEYWORD_FALSE:
+        case TOKEN_IDENTIFIER:
+        case TOKEN_NUMBER:
+        case TOKEN_STRING:
+        case TOKEN_REGEXP:
+          if (!$this->t->scanOperand) {
+            break 2;
+          }
+
+          array_push($operands, new JSNode($this->t));
+          $this->t->scanOperand = false;
+          break;
+
+        case TOKEN_CONDCOMMENT_START:
+        case TOKEN_CONDCOMMENT_END:
+          if ($this->t->scanOperand) {
+            array_push($operators, new JSNode($this->t));
+          }
+          else {
+            array_push($operands, new JSNode($this->t));
+          }
+          break;
+
+        case OP_LEFT_BRACKET:
+          if ($this->t->scanOperand) {
+            // Array initialiser.  Parse using recursive descent, as the
+            // sub-grammar here is not an operator grammar.
+            $n = new JSNode($this->t, JS_ARRAY_INIT);
+            while (($tt = $this->t->peek()) != OP_RIGHT_BRACKET) {
+              if ($tt == OP_COMMA) {
+                $this->t->get();
+                $n->addNode(null);
+                continue;
+              }
+
+              $n->addNode($this->Expression($x, OP_COMMA));
+              if (!$this->t->match(OP_COMMA)) {
+                break;
+              }
+            }
+
+            $this->t->mustMatch(OP_RIGHT_BRACKET);
+            array_push($operands, $n);
+            $this->t->scanOperand = false;
+          }
+          else {
+            // Property indexing operator.
+            array_push($operators, new JSNode($this->t, JS_INDEX));
+            $this->t->scanOperand = true;
+            ++$x->bracketLevel;
+          }
+          break;
+
+        case OP_RIGHT_BRACKET:
+          if ($this->t->scanOperand || $x->bracketLevel == $bl) {
+            break 2;
+          }
+
+          while ($this->reduce($operators, $operands)->type != JS_INDEX) {
+            continue;
+          }
+
+          --$x->bracketLevel;
+          break;
+
+        case OP_LEFT_CURLY:
+          if (!$this->t->scanOperand) {
+            break 2;
+          }
+
+          // Object initialiser.  As for array initialisers (see above),
+          // parse using recursive descent.
+          ++$x->curlyLevel;
+          $n = new JSNode($this->t, JS_OBJECT_INIT);
+          while (!$this->t->match(OP_RIGHT_CURLY)) {
+            do {
+              $tt = $this->t->get();
+              $tv = $this->t->currentToken()->value;
+              if (($tv == 'get' || $tv == 'set') && $this->t->peek() == TOKEN_IDENTIFIER) {
+                if ($x->ecmaStrictMode) {
+                  throw $this->t->newSyntaxError('Illegal property accessor');
+                }
+
+                $n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM));
+              }
+              else {
+                switch ($tt) {
+                  case TOKEN_IDENTIFIER:
+                  case TOKEN_NUMBER:
+                  case TOKEN_STRING:
+                    $id = new JSNode($this->t);
+                    break;
+
+                  case OP_RIGHT_CURLY:
+                    if ($x->ecmaStrictMode) {
+                      throw $this->t->newSyntaxError('Illegal trailing ,');
+                    }
+                    break 3;
+
+                  default:
+                    throw $this->t->newSyntaxError('Invalid property name');
+                }
+
+                $this->t->mustMatch(OP_COLON);
+                $n->addNode(new JSNode($this->t, JS_PROPERTY_INIT, $id, $this->Expression($x, OP_COMMA)));
+              }
+            } while ($this->t->match(OP_COMMA));
+
+            $this->t->mustMatch(OP_RIGHT_CURLY);
+            break;
+          }
+
+          array_push($operands, $n);
+          $this->t->scanOperand = false;
+          --$x->curlyLevel;
+          break;
+
+        case OP_RIGHT_CURLY:
+          if (!$this->t->scanOperand && $x->curlyLevel != $cl) {
+            throw new Exception('PANIC: right curly botch');
+          }
+          break 2;
+
+        case OP_LEFT_PAREN:
+          if ($this->t->scanOperand) {
+            array_push($operators, new JSNode($this->t, JS_GROUP));
+          }
+          else {
+            while (    !empty($operators) &&
+                                                       $this->opPrecedence[end($operators)->type] > $this->opPrecedence[KEYWORD_NEW]
+                                               ) {
+              $this->reduce($operators, $operands);
+            }
+
+            // Handle () now, to regularize the n-ary case for n > 0.
+            // We must set scanOperand in case there are arguments and
+            // the first one is a regexp or unary+/-.
+            $n = end($operators);
+            $this->t->scanOperand = true;
+            if ($this->t->match(OP_RIGHT_PAREN)) {
+              if ($n && $n->type == KEYWORD_NEW) {
+                array_pop($operators);
+                $n->addNode(array_pop($operands));
+              }
+              else {
+                $n = new JSNode($this->t, JS_CALL, array_pop($operands), new JSNode($this->t, JS_LIST));
+              }
+
+              array_push($operands, $n);
+              $this->t->scanOperand = false;
+              break;
+            }
+
+            if ($n && $n->type == KEYWORD_NEW) {
+              $n->type = JS_NEW_WITH_ARGS;
+            }
+            else {
+              array_push($operators, new JSNode($this->t, JS_CALL));
+            }
+          }
+
+          ++$x->parenLevel;
+          break;
+
+        case OP_RIGHT_PAREN:
+          if ($this->t->scanOperand || $x->parenLevel == $pl) {
+            break 2;
+          }
+
+          while (($tt = $this->reduce($operators, $operands)->type) != JS_GROUP &&
+                                               $tt != JS_CALL && $tt != JS_NEW_WITH_ARGS
+                                       ) {
+            continue;
+          }
+
+          if ($tt != JS_GROUP) {
+            $n = end($operands);
+            if ($n->treeNodes[1]->type != OP_COMMA) {
+              $n->treeNodes[1] = new JSNode($this->t, JS_LIST, $n->treeNodes[1]);
+            }
+            else {
+              $n->treeNodes[1]->type = JS_LIST;
+            }
+          }
+
+          --$x->parenLevel;
+          break;
+
+          // Automatic semicolon insertion means we may scan across a newline
+          // and into the beginning of another statement.  If so, break out of
+          // the while loop and let the t.scanOperand logic handle errors.
+        default:
+          break 2;
+      }
+    }
+
+    if ($x->hookLevel != $hl) {
+      throw $this->t->newSyntaxError('Missing : in conditional expression');
+    }
+
+    if ($x->parenLevel != $pl) {
+      throw $this->t->newSyntaxError('Missing ) in parenthetical');
+    }
+
+    if ($x->bracketLevel != $bl) {
+      throw $this->t->newSyntaxError('Missing ] in index expression');
+    }
+
+    if ($this->t->scanOperand) {
+      throw $this->t->newSyntaxError('Missing operand');
+    }
+
+    // Resume default mode, scanning for operands, not operators.
+    $this->t->scanOperand = true;
+    $this->t->unget();
+
+    while (count($operators)) {
+      $this->reduce($operators, $operands);
+    }
+
+    return array_pop($operands);
+  }
+
+  private function ParenExpression($x) {
+    $this->t->mustMatch(OP_LEFT_PAREN);
+    $n = $this->Expression($x);
+    $this->t->mustMatch(OP_RIGHT_PAREN);
+
+    return $n;
+  }
+
+  // Statement stack and nested statement handler.
+  private function nest($x, $node, $end = false) {
+    array_push($x->stmtStack, $node);
+    $n = $this->statement($x);
+    array_pop($x->stmtStack);
+
+    if ($end) {
+      $this->t->mustMatch($end);
+    }
+
+    return $n;
+  }
+
+  private function reduce(&$operators, &$operands) {
+    $n = array_pop($operators);
+    $op = $n->type;
+    $arity = $this->opArity[$op];
+    $c = count($operands);
+    if ($arity == -2) {
+      // Flatten left-associative trees
+      if ($c >= 2) {
+        $left = $operands[$c - 2];
+        if ($left->type == $op) {
+          $right = array_pop($operands);
+          $left->addNode($right);
+          return $left;
+        }
+      }
+      $arity = 2;
+    }
+
+    // Always use push to add operands to n, to update start and end
+    $a = array_splice($operands, $c - $arity);
+    for ($i = 0; $i < $arity; $i++) {
+      $n->addNode($a[$i]);
+    }
+
+    // Include closing bracket or postfix operator in [start,end]
+    $te = $this->t->currentToken()->end;
+    if ($n->end < $te) {
+      $n->end = $te;
+    }
+
+    array_push($operands, $n);
+
+    return $n;
+  }
+}
+
+class JSCompilerContext {
+  public $inFunction = false;
+  public $inForLoopInit = false;
+  public $ecmaStrictMode = false;
+  public $bracketLevel = 0;
+  public $curlyLevel = 0;
+  public $parenLevel = 0;
+  public $hookLevel = 0;
+
+  public $stmtStack = array();
+  public $funDecls = array();
+  public $varDecls = array();
+
+  public function __construct($inFunction) {
+    $this->inFunction = $inFunction;
+  }
+}
+
+class JSNode {
+  private $type;
+  private $value;
+  private $lineno;
+  private $start;
+  private $end;
+
+  public $treeNodes = array();
+  public $funDecls = array();
+  public $varDecls = array();
+
+  public function __construct($t, $type = 0) {
+    if ($token = $t->currentToken()) {
+      $this->type = $type ? $type : $token->type;
+      $this->value = $token->value;
+      $this->lineno = $token->lineno;
+      $this->start = $token->start;
+      $this->end = $token->end;
+    }
+    else {
+      $this->type = $type;
+      $this->lineno = $t->lineno;
+    }
+
+    if (($numargs = func_num_args()) > 2) {
+      $args = func_get_args();
+      for ($i = 2; $i < $numargs; $i++) {
+        $this->addNode($args[$i]);
+      }
+    }
+  }
+
+  // we don't want to bloat our object with all kind of specific properties, so we use overloading
+  public function __set($name, $value) {
+    $this->$name = $value;
+  }
+
+  public function __get($name) {
+    if (isset($this->$name)) {
+      return $this->$name;
+    }
+
+    return null;
+  }
+
+  public function addNode($node) {
+    if ($node !== null) {
+      if ($node->start < $this->start) {
+        $this->start = $node->start;
+      }
+      if ($this->end < $node->end) {
+        $this->end = $node->end;
+      }
+    }
+
+    $this->treeNodes[] = $node;
+  }
+}
+
+class JSTokenizer {
+  private $cursor = 0;
+  private $source;
+
+  public $tokens = array();
+  public $tokenIndex = 0;
+  public $lookahead = 0;
+  public $scanNewlines = false;
+  public $scanOperand = true;
+
+  public $filename;
+  public $lineno;
+
+  private $keywords = array(
+    'break',
+    'case',
+    'catch',
+    'const',
+    'continue',
+    'debugger',
+    'default',
+    'delete',
+    'do',
+    'else',
+    'enum',
+    'false',
+    'finally',
+    'for',
+    'function',
+    'if',
+    'in',
+    'instanceof',
+    'new',
+    'null',
+    'return',
+    'switch',
+    'this',
+    'throw',
+    'true',
+    'try',
+    'typeof',
+    'var',
+    'void',
+    'while',
+    'with',
+  );
+
+  private $opTypeNames = array(
+    ';',
+    ',',
+    '?',
+    ':',
+    '||',
+    '&&',
+    '|',
+    '^',
+    '&',
+    '===',
+    '==',
+    '=',
+    '!==',
+    '!=',
+    '<<',
+    '<=',
+    '<',
+    '>>>',
+    '>>',
+    '>=',
+    '>',
+    '++',
+    '--',
+    '+',
+    '-',
+    '*',
+    '/',
+    '%',
+    '!',
+    '~',
+    '.',
+    '[',
+    ']',
+    '{',
+    '}',
+    '(',
+    ')',
+    '@*/',
+  );
+
+  private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
+  private $opRegExp;
+
+  public function __construct() {
+    $this->opRegExp = '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames)) . ')#';
+  }
+
+  public function init($source, $filename = '', $lineno = 1) {
+    $this->source = $source;
+    $this->filename = $filename ? $filename : '[inline]';
+    $this->lineno = $lineno;
+
+    $this->cursor = 0;
+    $this->tokens = array();
+    $this->tokenIndex = 0;
+    $this->lookahead = 0;
+    $this->scanNewlines = false;
+    $this->scanOperand = true;
+  }
+
+  public function getInput($chunksize) {
+    if ($chunksize) {
+      return substr($this->source, $this->cursor, $chunksize);
+    }
+
+    return substr($this->source, $this->cursor);
+  }
+
+  public function isDone() {
+    return $this->peek() == TOKEN_END;
+  }
+
+  public function match($tt) {
+    return $this->get() == $tt || $this->unget();
+  }
+
+  public function mustMatch($tt) {
+    if (!$this->match($tt)) {
+      throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
+    }
+
+    return $this->currentToken();
+  }
+
+  public function peek() {
+    if ($this->lookahead) {
+      $next = $this->tokens[($this->tokenIndex + $this->lookahead) & 3];
+      if ($this->scanNewlines && $next->lineno != $this->lineno) {
+        $tt = TOKEN_NEWLINE;
+      }
+      else {
+        $tt = $next->type;
+      }
+    }
+    else {
+      $tt = $this->get();
+      $this->unget();
+    }
+
+    return $tt;
+  }
+
+  public function peekOnSameLine() {
+    $this->scanNewlines = true;
+    $tt = $this->peek();
+    $this->scanNewlines = false;
+
+    return $tt;
+  }
+
+  public function currentToken() {
+    if (!empty($this->tokens)) {
+      return $this->tokens[$this->tokenIndex];
+    }
+  }
+
+  public function get($chunksize = 1000) {
+    while ($this->lookahead) {
+      $this->lookahead--;
+      $this->tokenIndex = ($this->tokenIndex + 1) & 3;
+      $token = $this->tokens[$this->tokenIndex];
+      if ($token->type != TOKEN_NEWLINE || $this->scanNewlines) {
+        return $token->type;
+      }
+    }
+
+    $conditional_comment = false;
+
+    // strip whitespace and comments
+    while (true) {
+      $input = $this->getInput($chunksize);
+
+      // whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
+      $re = $this->scanNewlines ? '/^[ \r\t]+/' : '/^\s+/';
+      if (preg_match($re, $input, $match)) {
+        $spaces = $match[0];
+        $spacelen = strlen($spaces);
+        $this->cursor += $spacelen;
+        if (!$this->scanNewlines) {
+          $this->lineno += substr_count($spaces, "\n");
+        }
+
+        if ($spacelen == $chunksize) {
+          continue; // complete chunk contained whitespace
+        }
+
+        $input = $this->getInput($chunksize);
+        if ($input == '' || $input[0] != '/') {
+          break;
+        }
+      }
+
+      // Comments
+      if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match)) {
+        if (!$chunksize) {
+          break;
+        }
+
+        // retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
+        $chunksize = null;
+        continue;
+      }
+
+      // check if this is a conditional (JScript) comment
+      if (!empty($match[1])) {
+        $match[0] = '/*' . $match[1];
+        $conditional_comment = true;
+        break;
+      }
+      else {
+        $this->cursor += strlen($match[0]);
+        $this->lineno += substr_count($match[0], "\n");
+      }
+    }
+
+    if ($input == '') {
+      $tt = TOKEN_END;
+      $match = array('');
+    }
+    elseif ($conditional_comment) {
+      $tt = TOKEN_CONDCOMMENT_START;
+    }
+    else {
+      switch ($input[0]) {
+        case '0':
+          // hexadecimal
+          if (($input[1] == 'x' || $input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match)) {
+            $tt = TOKEN_NUMBER;
+            break;
+          }
+          // FALL THROUGH
+
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+        case '6':
+        case '7':
+        case '8':
+        case '9':
+          // should always match
+          preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match);
+          $tt = TOKEN_NUMBER;
+          break;
+
+        case "'":
+          if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match)) {
+            $tt = TOKEN_STRING;
+          }
+          else {
+            if ($chunksize) {
+              return $this->get(null); // retry with a full chunk fetch
+            }
+
+            throw $this->newSyntaxError('Unterminated string literal');
+          }
+          break;
+
+        case '"':
+          if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match)) {
+            $tt = TOKEN_STRING;
+          }
+          else {
+            if ($chunksize) {
+              return $this->get(null); // retry with a full chunk fetch
+            }
+
+            throw $this->newSyntaxError('Unterminated string literal');
+          }
+          break;
+
+        case '/':
+          if ($this->scanOperand && preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match)) {
+            $tt = TOKEN_REGEXP;
+            break;
+          }
+          // FALL THROUGH
+
+        case '|':
+        case '^':
+        case '&':
+        case '<':
+        case '>':
+        case '+':
+        case '-':
+        case '*':
+        case '%':
+        case '=':
+        case '!':
+          // should always match
+          preg_match($this->opRegExp, $input, $match);
+          $op = $match[0];
+          if (in_array($op, $this->assignOps) && $input[strlen($op)] == '=') {
+            $tt = OP_ASSIGN;
+            $match[0] .= '=';
+          }
+          else {
+            $tt = $op;
+            if ($this->scanOperand) {
+              if ($op == OP_PLUS) {
+                $tt = OP_UNARY_PLUS;
+              }
+              elseif ($op == OP_MINUS) {
+                $tt = OP_UNARY_MINUS;
+              }
+            }
+            $op = null;
+          }
+          break;
+
+        case '.':
+          if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match)) {
+            $tt = TOKEN_NUMBER;
+            break;
+          }
+          // FALL THROUGH
+
+        case ';':
+        case ',':
+        case '?':
+        case ':':
+        case '~':
+        case '[':
+        case ']':
+        case '{':
+        case '}':
+        case '(':
+        case ')':
+          // these are all single
+          $match = array($input[0]);
+          $tt = $input[0];
+          break;
+
+        case '@':
+          // check end of conditional comment
+          if (substr($input, 0, 3) == '@*/') {
+            $match = array('@*/');
+            $tt = TOKEN_CONDCOMMENT_END;
+          }
+          else {
+            throw $this->newSyntaxError('Illegal token');
+          }
+          break;
+
+        case "\n":
+          if ($this->scanNewlines) {
+            $match = array("\n");
+            $tt = TOKEN_NEWLINE;
+          }
+          else {
+            throw $this->newSyntaxError('Illegal token');
+          }
+          break;
+
+        default:
+          // Fast path for identifiers: word chars followed by whitespace or various other tokens.
+          // Note we don't need to exclude digits in the first char, as they've already been found
+          // above.
+          if (!preg_match('/^[$\w]+(?=[\s\/\|\^\&<>\+\-\*%=!.;,\?:~\[\]\{\}\(\)@])/', $input, $match)) {
+            // Character classes per ECMA-262 edition 5.1 section 7.6
+            // Per spec, must accept Unicode 3.0, *may* accept later versions.
+            // We'll take whatever PCRE understands, which should be more recent.
+            $identifierStartChars = "\\p{L}\\p{Nl}" .  # UnicodeLetter
+                                    "\$" .
+                                    "_";
+            $identifierPartChars  = $identifierStartChars .
+                                    "\\p{Mn}\\p{Mc}" . # UnicodeCombiningMark
+                                    "\\p{Nd}" .        # UnicodeDigit
+                                    "\\p{Pc}";         # UnicodeConnectorPunctuation
+            $unicodeEscape = "\\\\u[0-9A-F-a-f]{4}";
+            $identifierRegex = "/^" .
+                               "(?:[$identifierStartChars]|$unicodeEscape)" .
+                               "(?:[$identifierPartChars]|$unicodeEscape)*" .
+                               "/uS";
+            if (preg_match($identifierRegex, $input, $match)) {
+              if (strpos($match[0], '\\') !== false) {
+                // Per ECMA-262 edition 5.1, section 7.6 escape sequences should behave as if they were
+                // the original chars, but only within the boundaries of the identifier.
+                $decoded = preg_replace_callback('/\\\\u([0-9A-Fa-f]{4})/',
+                    array(__CLASS__, 'unicodeEscapeCallback'),
+                    $match[0]);
+
+                // Since our original regex didn't de-escape the originals, we need to check for validity again.
+                // No need to worry about token boundaries, as anything outside the identifier is illegal!
+                if (!preg_match("/^[$identifierStartChars][$identifierPartChars]*$/u", $decoded)) {
+                  throw $this->newSyntaxError('Illegal token');
+                }
+
+                // Per spec it _ought_ to work to use these escapes for keywords words as well...
+                // but IE rejects them as invalid, while Firefox and Chrome treat them as identifiers
+                // that don't match the keyword.
+                if (in_array($decoded, $this->keywords)) {
+                  throw $this->newSyntaxError('Illegal token');
+                }
+
+                // TODO: save the decoded form for output?
+              }
+            }
+            else {
+              throw $this->newSyntaxError('Illegal token');
+            }
+          }
+          $tt = in_array($match[0], $this->keywords) ? $match[0] : TOKEN_IDENTIFIER;
+      }
+    }
+
+    $this->tokenIndex = ($this->tokenIndex + 1) & 3;
+
+    if (!isset($this->tokens[$this->tokenIndex])) {
+      $this->tokens[$this->tokenIndex] = new JSToken();
+    }
+
+    $token = $this->tokens[$this->tokenIndex];
+    $token->type = $tt;
+
+    if ($tt == OP_ASSIGN) {
+      $token->assignOp = $op;
+    }
+
+    $token->start = $this->cursor;
+
+    $token->value = $match[0];
+    $this->cursor += strlen($match[0]);
+
+    $token->end = $this->cursor;
+    $token->lineno = $this->lineno;
+
+    return $tt;
+  }
+
+  public function unget() {
+    if (++$this->lookahead == 4) {
+      throw $this->newSyntaxError('PANIC: too much lookahead!');
+    }
+
+    $this->tokenIndex = ($this->tokenIndex - 1) & 3;
+  }
+
+  public function newSyntaxError($m) {
+    return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename . '\' on line ' . $this->lineno);
+  }
+
+  public static function unicodeEscapeCallback($m) {
+    return html_entity_decode('&#x' . $m[1]. ';', ENT_QUOTES, 'UTF-8');
+  }
+}
+
+class JSToken {
+  public $type;
+  public $value;
+  public $start;
+  public $end;
+  public $lineno;
+  public $assignOp;
+}