MediaWiki  REL1_22
jsminplus.php
Go to the documentation of this file.
00001 <?php
00030 /* ***** BEGIN LICENSE BLOCK *****
00031  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
00032  *
00033  * The contents of this file are subject to the Mozilla Public License Version
00034  * 1.1 (the "License"); you may not use this file except in compliance with
00035  * the License. You may obtain a copy of the License at
00036  * http://www.mozilla.org/MPL/
00037  *
00038  * Software distributed under the License is distributed on an "AS IS" basis,
00039  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
00040  * for the specific language governing rights and limitations under the
00041  * License.
00042  *
00043  * The Original Code is the Narcissus JavaScript engine.
00044  *
00045  * The Initial Developer of the Original Code is
00046  * Brendan Eich <[email protected]>.
00047  * Portions created by the Initial Developer are Copyright (C) 2004
00048  * the Initial Developer. All Rights Reserved.
00049  *
00050  * Contributor(s): Tino Zijdel <[email protected]>
00051  * PHP port, modifications and minifier routine are (C) 2009-2011
00052  *
00053  * Alternatively, the contents of this file may be used under the terms of
00054  * either the GNU General Public License Version 2 or later (the "GPL"), or
00055  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
00056  * in which case the provisions of the GPL or the LGPL are applicable instead
00057  * of those above. If you wish to allow use of your version of this file only
00058  * under the terms of either the GPL or the LGPL, and not to allow others to
00059  * use your version of this file under the terms of the MPL, indicate your
00060  * decision by deleting the provisions above and replace them with the notice
00061  * and other provisions required by the GPL or the LGPL. If you do not delete
00062  * the provisions above, a recipient may use your version of this file under
00063  * the terms of any one of the MPL, the GPL or the LGPL.
00064  *
00065  * ***** END LICENSE BLOCK ***** */
00066 
00067 define('TOKEN_END', 1);
00068 define('TOKEN_NUMBER', 2);
00069 define('TOKEN_IDENTIFIER', 3);
00070 define('TOKEN_STRING', 4);
00071 define('TOKEN_REGEXP', 5);
00072 define('TOKEN_NEWLINE', 6);
00073 define('TOKEN_CONDCOMMENT_START', 7);
00074 define('TOKEN_CONDCOMMENT_END', 8);
00075 
00076 define('JS_SCRIPT', 100);
00077 define('JS_BLOCK', 101);
00078 define('JS_LABEL', 102);
00079 define('JS_FOR_IN', 103);
00080 define('JS_CALL', 104);
00081 define('JS_NEW_WITH_ARGS', 105);
00082 define('JS_INDEX', 106);
00083 define('JS_ARRAY_INIT', 107);
00084 define('JS_OBJECT_INIT', 108);
00085 define('JS_PROPERTY_INIT', 109);
00086 define('JS_GETTER', 110);
00087 define('JS_SETTER', 111);
00088 define('JS_GROUP', 112);
00089 define('JS_LIST', 113);
00090 
00091 define('JS_MINIFIED', 999);
00092 
00093 define('DECLARED_FORM', 0);
00094 define('EXPRESSED_FORM', 1);
00095 define('STATEMENT_FORM', 2);
00096 
00097 /* Operators */
00098 define('OP_SEMICOLON', ';');
00099 define('OP_COMMA', ',');
00100 define('OP_HOOK', '?');
00101 define('OP_COLON', ':');
00102 define('OP_OR', '||');
00103 define('OP_AND', '&&');
00104 define('OP_BITWISE_OR', '|');
00105 define('OP_BITWISE_XOR', '^');
00106 define('OP_BITWISE_AND', '&');
00107 define('OP_STRICT_EQ', '===');
00108 define('OP_EQ', '==');
00109 define('OP_ASSIGN', '=');
00110 define('OP_STRICT_NE', '!==');
00111 define('OP_NE', '!=');
00112 define('OP_LSH', '<<');
00113 define('OP_LE', '<=');
00114 define('OP_LT', '<');
00115 define('OP_URSH', '>>>');
00116 define('OP_RSH', '>>');
00117 define('OP_GE', '>=');
00118 define('OP_GT', '>');
00119 define('OP_INCREMENT', '++');
00120 define('OP_DECREMENT', '--');
00121 define('OP_PLUS', '+');
00122 define('OP_MINUS', '-');
00123 define('OP_MUL', '*');
00124 define('OP_DIV', '/');
00125 define('OP_MOD', '%');
00126 define('OP_NOT', '!');
00127 define('OP_BITWISE_NOT', '~');
00128 define('OP_DOT', '.');
00129 define('OP_LEFT_BRACKET', '[');
00130 define('OP_RIGHT_BRACKET', ']');
00131 define('OP_LEFT_CURLY', '{');
00132 define('OP_RIGHT_CURLY', '}');
00133 define('OP_LEFT_PAREN', '(');
00134 define('OP_RIGHT_PAREN', ')');
00135 define('OP_CONDCOMMENT_END', '@*/');
00136 
00137 define('OP_UNARY_PLUS', 'U+');
00138 define('OP_UNARY_MINUS', 'U-');
00139 
00140 /* Keywords */
00141 define('KEYWORD_BREAK', 'break');
00142 define('KEYWORD_CASE', 'case');
00143 define('KEYWORD_CATCH', 'catch');
00144 define('KEYWORD_CONST', 'const');
00145 define('KEYWORD_CONTINUE', 'continue');
00146 define('KEYWORD_DEBUGGER', 'debugger');
00147 define('KEYWORD_DEFAULT', 'default');
00148 define('KEYWORD_DELETE', 'delete');
00149 define('KEYWORD_DO', 'do');
00150 define('KEYWORD_ELSE', 'else');
00151 define('KEYWORD_ENUM', 'enum');
00152 define('KEYWORD_FALSE', 'false');
00153 define('KEYWORD_FINALLY', 'finally');
00154 define('KEYWORD_FOR', 'for');
00155 define('KEYWORD_FUNCTION', 'function');
00156 define('KEYWORD_IF', 'if');
00157 define('KEYWORD_IN', 'in');
00158 define('KEYWORD_INSTANCEOF', 'instanceof');
00159 define('KEYWORD_NEW', 'new');
00160 define('KEYWORD_NULL', 'null');
00161 define('KEYWORD_RETURN', 'return');
00162 define('KEYWORD_SWITCH', 'switch');
00163 define('KEYWORD_THIS', 'this');
00164 define('KEYWORD_THROW', 'throw');
00165 define('KEYWORD_TRUE', 'true');
00166 define('KEYWORD_TRY', 'try');
00167 define('KEYWORD_TYPEOF', 'typeof');
00168 define('KEYWORD_VAR', 'var');
00169 define('KEYWORD_VOID', 'void');
00170 define('KEYWORD_WHILE', 'while');
00171 define('KEYWORD_WITH', 'with');
00172 
00173 
00174 class JSMinPlus
00175 {
00176     private $parser;
00177     private $reserved = array(
00178         'break', 'case', 'catch', 'continue', 'default', 'delete', 'do',
00179         'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof',
00180         'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var',
00181         'void', 'while', 'with',
00182         // Words reserved for future use
00183         'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger',
00184         'double', 'enum', 'export', 'extends', 'final', 'float', 'goto',
00185         'implements', 'import', 'int', 'interface', 'long', 'native',
00186         'package', 'private', 'protected', 'public', 'short', 'static',
00187         'super', 'synchronized', 'throws', 'transient', 'volatile',
00188         // These are not reserved, but should be taken into account
00189         // in isValidIdentifier (See jslint source code)
00190         'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined'
00191     );
00192 
00193     private function __construct()
00194     {
00195         $this->parser = new JSParser($this);
00196     }
00197 
00198     public static function minify($js, $filename='')
00199     {
00200         static $instance;
00201 
00202         // this is a singleton
00203         if(!$instance)
00204             $instance = new JSMinPlus();
00205 
00206         return $instance->min($js, $filename);
00207     }
00208 
00209     private function min($js, $filename)
00210     {
00211         try
00212         {
00213             $n = $this->parser->parse($js, $filename, 1);
00214             return $this->parseTree($n);
00215         }
00216         catch(Exception $e)
00217         {
00218             echo $e->getMessage() . "\n";
00219         }
00220 
00221         return false;
00222     }
00223 
00224     public function parseTree($n, $noBlockGrouping = false)
00225     {
00226         $s = '';
00227 
00228         switch ($n->type)
00229         {
00230             case JS_MINIFIED:
00231                 $s = $n->value;
00232             break;
00233 
00234             case JS_SCRIPT:
00235                 // we do nothing yet with funDecls or varDecls
00236                 $noBlockGrouping = true;
00237             // FALL THROUGH
00238 
00239             case JS_BLOCK:
00240                 $childs = $n->treeNodes;
00241                 $lastType = 0;
00242                 for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++)
00243                 {
00244                     $type = $childs[$i]->type;
00245                     $t = $this->parseTree($childs[$i]);
00246                     if (strlen($t))
00247                     {
00248                         if ($c)
00249                         {
00250                             $s = rtrim($s, ';');
00251 
00252                             if ($type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM)
00253                             {
00254                                 // put declared functions on a new line
00255                                 $s .= "\n";
00256                             }
00257                             elseif ($type == KEYWORD_VAR && $type == $lastType)
00258                             {
00259                                 // multiple var-statements can go into one
00260                                 $t = ',' . substr($t, 4);
00261                             }
00262                             else
00263                             {
00264                                 // add terminator
00265                                 $s .= ';';
00266                             }
00267                         }
00268 
00269                         $s .= $t;
00270 
00271                         $c++;
00272                         $lastType = $type;
00273                     }
00274                 }
00275 
00276                 if ($c > 1 && !$noBlockGrouping)
00277                 {
00278                     $s = '{' . $s . '}';
00279                 }
00280             break;
00281 
00282             case KEYWORD_FUNCTION:
00283                 $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '(';
00284                 $params = $n->params;
00285                 for ($i = 0, $j = count($params); $i < $j; $i++)
00286                     $s .= ($i ? ',' : '') . $params[$i];
00287                 $s .= '){' . $this->parseTree($n->body, true) . '}';
00288             break;
00289 
00290             case KEYWORD_IF:
00291                 $s = 'if(' . $this->parseTree($n->condition) . ')';
00292                 $thenPart = $this->parseTree($n->thenPart);
00293                 $elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null;
00294 
00295                 // empty if-statement
00296                 if ($thenPart == '')
00297                     $thenPart = ';';
00298 
00299                 if ($elsePart)
00300                 {
00301                     // be careful and always make a block out of the thenPart; could be more optimized but is a lot of trouble
00302                     if ($thenPart != ';' && $thenPart[0] != '{')
00303                         $thenPart = '{' . $thenPart . '}';
00304 
00305                     $s .= $thenPart . 'else';
00306 
00307                     // we could check for more, but that hardly ever applies so go for performance
00308                     if ($elsePart[0] != '{')
00309                         $s .= ' ';
00310 
00311                     $s .= $elsePart;
00312                 }
00313                 else
00314                 {
00315                     $s .= $thenPart;
00316                 }
00317             break;
00318 
00319             case KEYWORD_SWITCH:
00320                 $s = 'switch(' . $this->parseTree($n->discriminant) . '){';
00321                 $cases = $n->cases;
00322                 for ($i = 0, $j = count($cases); $i < $j; $i++)
00323                 {
00324                     $case = $cases[$i];
00325                     if ($case->type == KEYWORD_CASE)
00326                         $s .= 'case' . ($case->caseLabel->type != TOKEN_STRING ? ' ' : '') . $this->parseTree($case->caseLabel) . ':';
00327                     else
00328                         $s .= 'default:';
00329 
00330                     $statement = $this->parseTree($case->statements, true);
00331                     if ($statement)
00332                     {
00333                         $s .= $statement;
00334                         // no terminator for last statement
00335                         if ($i + 1 < $j)
00336                             $s .= ';';
00337                     }
00338                 }
00339                 $s .= '}';
00340             break;
00341 
00342             case KEYWORD_FOR:
00343                 $s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '')
00344                     . ';' . ($n->condition ? $this->parseTree($n->condition) : '')
00345                     . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')';
00346 
00347                 $body  = $this->parseTree($n->body);
00348                 if ($body == '')
00349                     $body = ';';
00350 
00351                 $s .= $body;
00352             break;
00353 
00354             case KEYWORD_WHILE:
00355                 $s = 'while(' . $this->parseTree($n->condition) . ')';
00356 
00357                 $body  = $this->parseTree($n->body);
00358                 if ($body == '')
00359                     $body = ';';
00360 
00361                 $s .= $body;
00362             break;
00363 
00364             case JS_FOR_IN:
00365                 $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')';
00366 
00367                 $body  = $this->parseTree($n->body);
00368                 if ($body == '')
00369                     $body = ';';
00370 
00371                 $s .= $body;
00372             break;
00373 
00374             case KEYWORD_DO:
00375                 $s = 'do{' . $this->parseTree($n->body, true) . '}while(' . $this->parseTree($n->condition) . ')';
00376             break;
00377 
00378             case KEYWORD_BREAK:
00379             case KEYWORD_CONTINUE:
00380                 $s = $n->value . ($n->label ? ' ' . $n->label : '');
00381             break;
00382 
00383             case KEYWORD_TRY:
00384                 $s = 'try{' . $this->parseTree($n->tryBlock, true) . '}';
00385                 $catchClauses = $n->catchClauses;
00386                 for ($i = 0, $j = count($catchClauses); $i < $j; $i++)
00387                 {
00388                     $t = $catchClauses[$i];
00389                     $s .= 'catch(' . $t->varName . ($t->guard ? ' if ' . $this->parseTree($t->guard) : '') . '){' . $this->parseTree($t->block, true) . '}';
00390                 }
00391                 if ($n->finallyBlock)
00392                     $s .= 'finally{' . $this->parseTree($n->finallyBlock, true) . '}';
00393             break;
00394 
00395             case KEYWORD_THROW:
00396             case KEYWORD_RETURN:
00397                 $s = $n->type;
00398                 if ($n->value)
00399                 {
00400                     $t = $this->parseTree($n->value);
00401                     if (strlen($t))
00402                     {
00403                         if ($this->isWordChar($t[0]) || $t[0] == '\\')
00404                             $s .= ' ';
00405 
00406                         $s .= $t;
00407                     }
00408                 }
00409             break;
00410 
00411             case KEYWORD_WITH:
00412                 $s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
00413             break;
00414 
00415             case KEYWORD_VAR:
00416             case KEYWORD_CONST:
00417                 $s = $n->value . ' ';
00418                 $childs = $n->treeNodes;
00419                 for ($i = 0, $j = count($childs); $i < $j; $i++)
00420                 {
00421                     $t = $childs[$i];
00422                     $s .= ($i ? ',' : '') . $t->name;
00423                     $u = $t->initializer;
00424                     if ($u)
00425                         $s .= '=' . $this->parseTree($u);
00426                 }
00427             break;
00428 
00429             case KEYWORD_IN:
00430             case KEYWORD_INSTANCEOF:
00431                 $left = $this->parseTree($n->treeNodes[0]);
00432                 $right = $this->parseTree($n->treeNodes[1]);
00433 
00434                 $s = $left;
00435 
00436                 if ($this->isWordChar(substr($left, -1)))
00437                     $s .= ' ';
00438 
00439                 $s .= $n->type;
00440 
00441                 if ($this->isWordChar($right[0]) || $right[0] == '\\')
00442                     $s .= ' ';
00443 
00444                 $s .= $right;
00445             break;
00446 
00447             case KEYWORD_DELETE:
00448             case KEYWORD_TYPEOF:
00449                 $right = $this->parseTree($n->treeNodes[0]);
00450 
00451                 $s = $n->type;
00452 
00453                 if ($this->isWordChar($right[0]) || $right[0] == '\\')
00454                     $s .= ' ';
00455 
00456                 $s .= $right;
00457             break;
00458 
00459             case KEYWORD_VOID:
00460                 $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')';
00461             break;
00462 
00463             case KEYWORD_DEBUGGER:
00464                 throw new Exception('NOT IMPLEMENTED: DEBUGGER');
00465             break;
00466 
00467             case TOKEN_CONDCOMMENT_START:
00468             case TOKEN_CONDCOMMENT_END:
00469                 $s = $n->value . ($n->type == TOKEN_CONDCOMMENT_START ? ' ' : '');
00470                 $childs = $n->treeNodes;
00471                 for ($i = 0, $j = count($childs); $i < $j; $i++)
00472                     $s .= $this->parseTree($childs[$i]);
00473             break;
00474 
00475             case OP_SEMICOLON:
00476                 if ($expression = $n->expression)
00477                     $s = $this->parseTree($expression);
00478             break;
00479 
00480             case JS_LABEL:
00481                 $s = $n->label . ':' . $this->parseTree($n->statement);
00482             break;
00483 
00484             case OP_COMMA:
00485                 $childs = $n->treeNodes;
00486                 for ($i = 0, $j = count($childs); $i < $j; $i++)
00487                     $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
00488             break;
00489 
00490             case OP_ASSIGN:
00491                 $s = $this->parseTree($n->treeNodes[0]) . $n->value . $this->parseTree($n->treeNodes[1]);
00492             break;
00493 
00494             case OP_HOOK:
00495                 $s = $this->parseTree($n->treeNodes[0]) . '?' . $this->parseTree($n->treeNodes[1]) . ':' . $this->parseTree($n->treeNodes[2]);
00496             break;
00497 
00498             case OP_OR: case OP_AND:
00499             case OP_BITWISE_OR: case OP_BITWISE_XOR: case OP_BITWISE_AND:
00500             case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
00501             case OP_LT: case OP_LE: case OP_GE: case OP_GT:
00502             case OP_LSH: case OP_RSH: case OP_URSH:
00503             case OP_MUL: case OP_DIV: case OP_MOD:
00504                 $s = $this->parseTree($n->treeNodes[0]) . $n->type . $this->parseTree($n->treeNodes[1]);
00505             break;
00506 
00507             case OP_PLUS:
00508             case OP_MINUS:
00509                 $left = $this->parseTree($n->treeNodes[0]);
00510                 $right = $this->parseTree($n->treeNodes[1]);
00511 
00512                 switch ($n->treeNodes[1]->type)
00513                 {
00514                     case OP_PLUS:
00515                     case OP_MINUS:
00516                     case OP_INCREMENT:
00517                     case OP_DECREMENT:
00518                     case OP_UNARY_PLUS:
00519                     case OP_UNARY_MINUS:
00520                         $s = $left . $n->type . ' ' . $right;
00521                     break;
00522 
00523                     case TOKEN_STRING:
00524                         //combine concatenated strings with same quote style
00525                         if ($n->type == OP_PLUS && substr($left, -1) == $right[0])
00526                         {
00527                             $s = substr($left, 0, -1) . substr($right, 1);
00528                             break;
00529                         }
00530                     // FALL THROUGH
00531 
00532                     default:
00533                         $s = $left . $n->type . $right;
00534                 }
00535             break;
00536 
00537             case OP_NOT:
00538             case OP_BITWISE_NOT:
00539             case OP_UNARY_PLUS:
00540             case OP_UNARY_MINUS:
00541                 $s = $n->value . $this->parseTree($n->treeNodes[0]);
00542             break;
00543 
00544             case OP_INCREMENT:
00545             case OP_DECREMENT:
00546                 if ($n->postfix)
00547                     $s = $this->parseTree($n->treeNodes[0]) . $n->value;
00548                 else
00549                     $s = $n->value . $this->parseTree($n->treeNodes[0]);
00550             break;
00551 
00552             case OP_DOT:
00553                 $s = $this->parseTree($n->treeNodes[0]) . '.' . $this->parseTree($n->treeNodes[1]);
00554             break;
00555 
00556             case JS_INDEX:
00557                 $s = $this->parseTree($n->treeNodes[0]);
00558                 // See if we can replace named index with a dot saving 3 bytes
00559                 if (    $n->treeNodes[0]->type == TOKEN_IDENTIFIER &&
00560                     $n->treeNodes[1]->type == TOKEN_STRING &&
00561                     $this->isValidIdentifier(substr($n->treeNodes[1]->value, 1, -1))
00562                 )
00563                     $s .= '.' . substr($n->treeNodes[1]->value, 1, -1);
00564                 else
00565                     $s .= '[' . $this->parseTree($n->treeNodes[1]) . ']';
00566             break;
00567 
00568             case JS_LIST:
00569                 $childs = $n->treeNodes;
00570                 for ($i = 0, $j = count($childs); $i < $j; $i++)
00571                     $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
00572             break;
00573 
00574             case JS_CALL:
00575                 $s = $this->parseTree($n->treeNodes[0]) . '(' . $this->parseTree($n->treeNodes[1]) . ')';
00576             break;
00577 
00578             case KEYWORD_NEW:
00579             case JS_NEW_WITH_ARGS:
00580                 $s = 'new ' . $this->parseTree($n->treeNodes[0]) . '(' . ($n->type == JS_NEW_WITH_ARGS ? $this->parseTree($n->treeNodes[1]) : '') . ')';
00581             break;
00582 
00583             case JS_ARRAY_INIT:
00584                 $s = '[';
00585                 $childs = $n->treeNodes;
00586                 for ($i = 0, $j = count($childs); $i < $j; $i++)
00587                 {
00588                     $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
00589                 }
00590                 $s .= ']';
00591             break;
00592 
00593             case JS_OBJECT_INIT:
00594                 $s = '{';
00595                 $childs = $n->treeNodes;
00596                 for ($i = 0, $j = count($childs); $i < $j; $i++)
00597                 {
00598                     $t = $childs[$i];
00599                     if ($i)
00600                         $s .= ',';
00601                     if ($t->type == JS_PROPERTY_INIT)
00602                     {
00603                         // Ditch the quotes when the index is a valid identifier
00604                         if (    $t->treeNodes[0]->type == TOKEN_STRING &&
00605                             $this->isValidIdentifier(substr($t->treeNodes[0]->value, 1, -1))
00606                         )
00607                             $s .= substr($t->treeNodes[0]->value, 1, -1);
00608                         else
00609                             $s .= $t->treeNodes[0]->value;
00610 
00611                         $s .= ':' . $this->parseTree($t->treeNodes[1]);
00612                     }
00613                     else
00614                     {
00615                         $s .= $t->type == JS_GETTER ? 'get' : 'set';
00616                         $s .= ' ' . $t->name . '(';
00617                         $params = $t->params;
00618                         for ($i = 0, $j = count($params); $i < $j; $i++)
00619                             $s .= ($i ? ',' : '') . $params[$i];
00620                         $s .= '){' . $this->parseTree($t->body, true) . '}';
00621                     }
00622                 }
00623                 $s .= '}';
00624             break;
00625 
00626             case TOKEN_NUMBER:
00627                 $s = $n->value;
00628                 if (preg_match('/^([1-9]+)(0{3,})$/', $s, $m))
00629                     $s = $m[1] . 'e' . strlen($m[2]);
00630             break;
00631 
00632             case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
00633             case TOKEN_IDENTIFIER: case TOKEN_STRING: case TOKEN_REGEXP:
00634                 $s = $n->value;
00635             break;
00636 
00637             case JS_GROUP:
00638                 if (in_array(
00639                     $n->treeNodes[0]->type,
00640                     array(
00641                         JS_ARRAY_INIT, JS_OBJECT_INIT, JS_GROUP,
00642                         TOKEN_NUMBER, TOKEN_STRING, TOKEN_REGEXP, TOKEN_IDENTIFIER,
00643                         KEYWORD_NULL, KEYWORD_THIS, KEYWORD_TRUE, KEYWORD_FALSE
00644                     )
00645                 ))
00646                 {
00647                     $s = $this->parseTree($n->treeNodes[0]);
00648                 }
00649                 else
00650                 {
00651                     $s = '(' . $this->parseTree($n->treeNodes[0]) . ')';
00652                 }
00653             break;
00654 
00655             default:
00656                 throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type);
00657         }
00658 
00659         return $s;
00660     }
00661 
00662     private function isValidIdentifier($string)
00663     {
00664         return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved);
00665     }
00666 
00667     private function isWordChar($char)
00668     {
00669         return $char == '_' || $char == '$' || ctype_alnum($char);
00670     }
00671 }
00672 
00673 class JSParser
00674 {
00675     private $t;
00676     private $minifier;
00677 
00678     private $opPrecedence = array(
00679         ';' => 0,
00680         ',' => 1,
00681         '=' => 2, '?' => 2, ':' => 2,
00682         // The above all have to have the same precedence, see bug 330975
00683         '||' => 4,
00684         '&&' => 5,
00685         '|' => 6,
00686         '^' => 7,
00687         '&' => 8,
00688         '==' => 9, '!=' => 9, '===' => 9, '!==' => 9,
00689         '<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10,
00690         '<<' => 11, '>>' => 11, '>>>' => 11,
00691         '+' => 12, '-' => 12,
00692         '*' => 13, '/' => 13, '%' => 13,
00693         'delete' => 14, 'void' => 14, 'typeof' => 14,
00694         '!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14,
00695         '++' => 15, '--' => 15,
00696         'new' => 16,
00697         '.' => 17,
00698         JS_NEW_WITH_ARGS => 0, JS_INDEX => 0, JS_CALL => 0,
00699         JS_ARRAY_INIT => 0, JS_OBJECT_INIT => 0, JS_GROUP => 0
00700     );
00701 
00702     private $opArity = array(
00703         ',' => -2,
00704         '=' => 2,
00705         '?' => 3,
00706         '||' => 2,
00707         '&&' => 2,
00708         '|' => 2,
00709         '^' => 2,
00710         '&' => 2,
00711         '==' => 2, '!=' => 2, '===' => 2, '!==' => 2,
00712         '<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2,
00713         '<<' => 2, '>>' => 2, '>>>' => 2,
00714         '+' => 2, '-' => 2,
00715         '*' => 2, '/' => 2, '%' => 2,
00716         'delete' => 1, 'void' => 1, 'typeof' => 1,
00717         '!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1,
00718         '++' => 1, '--' => 1,
00719         'new' => 1,
00720         '.' => 2,
00721         JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2,
00722         JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1,
00723         TOKEN_CONDCOMMENT_START => 1, TOKEN_CONDCOMMENT_END => 1
00724     );
00725 
00726     public function __construct($minifier=null)
00727     {
00728         $this->minifier = $minifier;
00729         $this->t = new JSTokenizer();
00730     }
00731 
00732     public function parse($s, $f, $l)
00733     {
00734         // initialize tokenizer
00735         $this->t->init($s, $f, $l);
00736 
00737         $x = new JSCompilerContext(false);
00738         $n = $this->Script($x);
00739         if (!$this->t->isDone())
00740             throw $this->t->newSyntaxError('Syntax error');
00741 
00742         return $n;
00743     }
00744 
00745     private function Script($x)
00746     {
00747         $n = $this->Statements($x);
00748         $n->type = JS_SCRIPT;
00749         $n->funDecls = $x->funDecls;
00750         $n->varDecls = $x->varDecls;
00751 
00752         // minify by scope
00753         if ($this->minifier)
00754         {
00755             $n->value = $this->minifier->parseTree($n);
00756 
00757             // clear tree from node to save memory
00758             $n->treeNodes = null;
00759             $n->funDecls = null;
00760             $n->varDecls = null;
00761 
00762             $n->type = JS_MINIFIED;
00763         }
00764 
00765         return $n;
00766     }
00767 
00768     private function Statements($x)
00769     {
00770         $n = new JSNode($this->t, JS_BLOCK);
00771         array_push($x->stmtStack, $n);
00772 
00773         while (!$this->t->isDone() && $this->t->peek() != OP_RIGHT_CURLY)
00774             $n->addNode($this->Statement($x));
00775 
00776         array_pop($x->stmtStack);
00777 
00778         return $n;
00779     }
00780 
00781     private function Block($x)
00782     {
00783         $this->t->mustMatch(OP_LEFT_CURLY);
00784         $n = $this->Statements($x);
00785         $this->t->mustMatch(OP_RIGHT_CURLY);
00786 
00787         return $n;
00788     }
00789 
00790     private function Statement($x)
00791     {
00792         $tt = $this->t->get();
00793         $n2 = null;
00794 
00795         // Cases for statements ending in a right curly return early, avoiding the
00796         // common semicolon insertion magic after this switch.
00797         switch ($tt)
00798         {
00799             case KEYWORD_FUNCTION:
00800                 return $this->FunctionDefinition(
00801                     $x,
00802                     true,
00803                     count($x->stmtStack) > 1 ? STATEMENT_FORM : DECLARED_FORM
00804                 );
00805             break;
00806 
00807             case OP_LEFT_CURLY:
00808                 $n = $this->Statements($x);
00809                 $this->t->mustMatch(OP_RIGHT_CURLY);
00810             return $n;
00811 
00812             case KEYWORD_IF:
00813                 $n = new JSNode($this->t);
00814                 $n->condition = $this->ParenExpression($x);
00815                 array_push($x->stmtStack, $n);
00816                 $n->thenPart = $this->Statement($x);
00817                 $n->elsePart = $this->t->match(KEYWORD_ELSE) ? $this->Statement($x) : null;
00818                 array_pop($x->stmtStack);
00819             return $n;
00820 
00821             case KEYWORD_SWITCH:
00822                 $n = new JSNode($this->t);
00823                 $this->t->mustMatch(OP_LEFT_PAREN);
00824                 $n->discriminant = $this->Expression($x);
00825                 $this->t->mustMatch(OP_RIGHT_PAREN);
00826                 $n->cases = array();
00827                 $n->defaultIndex = -1;
00828 
00829                 array_push($x->stmtStack, $n);
00830 
00831                 $this->t->mustMatch(OP_LEFT_CURLY);
00832 
00833                 while (($tt = $this->t->get()) != OP_RIGHT_CURLY)
00834                 {
00835                     switch ($tt)
00836                     {
00837                         case KEYWORD_DEFAULT:
00838                             if ($n->defaultIndex >= 0)
00839                                 throw $this->t->newSyntaxError('More than one switch default');
00840                             // FALL THROUGH
00841                         case KEYWORD_CASE:
00842                             $n2 = new JSNode($this->t);
00843                             if ($tt == KEYWORD_DEFAULT)
00844                                 $n->defaultIndex = count($n->cases);
00845                             else
00846                                 $n2->caseLabel = $this->Expression($x, OP_COLON);
00847                                 break;
00848                         default:
00849                             throw $this->t->newSyntaxError('Invalid switch case');
00850                     }
00851 
00852                     $this->t->mustMatch(OP_COLON);
00853                     $n2->statements = new JSNode($this->t, JS_BLOCK);
00854                     while (($tt = $this->t->peek()) != KEYWORD_CASE && $tt != KEYWORD_DEFAULT && $tt != OP_RIGHT_CURLY)
00855                         $n2->statements->addNode($this->Statement($x));
00856 
00857                     array_push($n->cases, $n2);
00858                 }
00859 
00860                 array_pop($x->stmtStack);
00861             return $n;
00862 
00863             case KEYWORD_FOR:
00864                 $n = new JSNode($this->t);
00865                 $n->isLoop = true;
00866                 $this->t->mustMatch(OP_LEFT_PAREN);
00867 
00868                 if (($tt = $this->t->peek()) != OP_SEMICOLON)
00869                 {
00870                     $x->inForLoopInit = true;
00871                     if ($tt == KEYWORD_VAR || $tt == KEYWORD_CONST)
00872                     {
00873                         $this->t->get();
00874                         $n2 = $this->Variables($x);
00875                     }
00876                     else
00877                     {
00878                         $n2 = $this->Expression($x);
00879                     }
00880                     $x->inForLoopInit = false;
00881                 }
00882 
00883                 if ($n2 && $this->t->match(KEYWORD_IN))
00884                 {
00885                     $n->type = JS_FOR_IN;
00886                     if ($n2->type == KEYWORD_VAR)
00887                     {
00888                         if (count($n2->treeNodes) != 1)
00889                         {
00890                             throw $this->t->SyntaxError(
00891                                 'Invalid for..in left-hand side',
00892                                 $this->t->filename,
00893                                 $n2->lineno
00894                             );
00895                         }
00896 
00897                         // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name.
00898                         $n->iterator = $n2->treeNodes[0];
00899                         $n->varDecl = $n2;
00900                     }
00901                     else
00902                     {
00903                         $n->iterator = $n2;
00904                         $n->varDecl = null;
00905                     }
00906 
00907                     $n->object = $this->Expression($x);
00908                 }
00909                 else
00910                 {
00911                     $n->setup = $n2 ? $n2 : null;
00912                     $this->t->mustMatch(OP_SEMICOLON);
00913                     $n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x);
00914                     $this->t->mustMatch(OP_SEMICOLON);
00915                     $n->update = $this->t->peek() == OP_RIGHT_PAREN ? null : $this->Expression($x);
00916                 }
00917 
00918                 $this->t->mustMatch(OP_RIGHT_PAREN);
00919                 $n->body = $this->nest($x, $n);
00920             return $n;
00921 
00922             case KEYWORD_WHILE:
00923                     $n = new JSNode($this->t);
00924                     $n->isLoop = true;
00925                     $n->condition = $this->ParenExpression($x);
00926                     $n->body = $this->nest($x, $n);
00927             return $n;
00928 
00929             case KEYWORD_DO:
00930                 $n = new JSNode($this->t);
00931                 $n->isLoop = true;
00932                 $n->body = $this->nest($x, $n, KEYWORD_WHILE);
00933                 $n->condition = $this->ParenExpression($x);
00934                 if (!$x->ecmaStrictMode)
00935                 {
00936                     // <script language="JavaScript"> (without version hints) may need
00937                     // automatic semicolon insertion without a newline after do-while.
00938                     // See http://bugzilla.mozilla.org/show_bug.cgi?id=238945.
00939                     $this->t->match(OP_SEMICOLON);
00940                     return $n;
00941                 }
00942             break;
00943 
00944             case KEYWORD_BREAK:
00945             case KEYWORD_CONTINUE:
00946                 $n = new JSNode($this->t);
00947 
00948                 if ($this->t->peekOnSameLine() == TOKEN_IDENTIFIER)
00949                 {
00950                     $this->t->get();
00951                     $n->label = $this->t->currentToken()->value;
00952                 }
00953 
00954                 $ss = $x->stmtStack;
00955                 $i = count($ss);
00956                 $label = $n->label;
00957                 if ($label)
00958                 {
00959                     do
00960                     {
00961                         if (--$i < 0)
00962                             throw $this->t->newSyntaxError('Label not found');
00963                     }
00964                     while ($ss[$i]->label != $label);
00965                 }
00966                 else
00967                 {
00968                     do
00969                     {
00970                         if (--$i < 0)
00971                             throw $this->t->newSyntaxError('Invalid ' . $tt);
00972                     }
00973                     while (!$ss[$i]->isLoop && ($tt != KEYWORD_BREAK || $ss[$i]->type != KEYWORD_SWITCH));
00974                 }
00975 
00976                 $n->target = $ss[$i];
00977             break;
00978 
00979             case KEYWORD_TRY:
00980                 $n = new JSNode($this->t);
00981                 $n->tryBlock = $this->Block($x);
00982                 $n->catchClauses = array();
00983 
00984                 while ($this->t->match(KEYWORD_CATCH))
00985                 {
00986                     $n2 = new JSNode($this->t);
00987                     $this->t->mustMatch(OP_LEFT_PAREN);
00988                     $n2->varName = $this->t->mustMatch(TOKEN_IDENTIFIER)->value;
00989 
00990                     if ($this->t->match(KEYWORD_IF))
00991                     {
00992                         if ($x->ecmaStrictMode)
00993                             throw $this->t->newSyntaxError('Illegal catch guard');
00994 
00995                         if (count($n->catchClauses) && !end($n->catchClauses)->guard)
00996                             throw $this->t->newSyntaxError('Guarded catch after unguarded');
00997 
00998                         $n2->guard = $this->Expression($x);
00999                     }
01000                     else
01001                     {
01002                         $n2->guard = null;
01003                     }
01004 
01005                     $this->t->mustMatch(OP_RIGHT_PAREN);
01006                     $n2->block = $this->Block($x);
01007                     array_push($n->catchClauses, $n2);
01008                 }
01009 
01010                 if ($this->t->match(KEYWORD_FINALLY))
01011                     $n->finallyBlock = $this->Block($x);
01012 
01013                 if (!count($n->catchClauses) && !$n->finallyBlock)
01014                     throw $this->t->newSyntaxError('Invalid try statement');
01015             return $n;
01016 
01017             case KEYWORD_CATCH:
01018             case KEYWORD_FINALLY:
01019                 throw $this->t->newSyntaxError($tt + ' without preceding try');
01020 
01021             case KEYWORD_THROW:
01022                 $n = new JSNode($this->t);
01023                 $n->value = $this->Expression($x);
01024             break;
01025 
01026             case KEYWORD_RETURN:
01027                 if (!$x->inFunction)
01028                     throw $this->t->newSyntaxError('Invalid return');
01029 
01030                 $n = new JSNode($this->t);
01031                 $tt = $this->t->peekOnSameLine();
01032                 if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
01033                     $n->value = $this->Expression($x);
01034                 else
01035                     $n->value = null;
01036             break;
01037 
01038             case KEYWORD_WITH:
01039                 $n = new JSNode($this->t);
01040                 $n->object = $this->ParenExpression($x);
01041                 $n->body = $this->nest($x, $n);
01042             return $n;
01043 
01044             case KEYWORD_VAR:
01045             case KEYWORD_CONST:
01046                     $n = $this->Variables($x);
01047             break;
01048 
01049             case TOKEN_CONDCOMMENT_START:
01050             case TOKEN_CONDCOMMENT_END:
01051                 $n = new JSNode($this->t);
01052             return $n;
01053 
01054             case KEYWORD_DEBUGGER:
01055                 $n = new JSNode($this->t);
01056             break;
01057 
01058             case TOKEN_NEWLINE:
01059             case OP_SEMICOLON:
01060                 $n = new JSNode($this->t, OP_SEMICOLON);
01061                 $n->expression = null;
01062             return $n;
01063 
01064             default:
01065                 if ($tt == TOKEN_IDENTIFIER)
01066                 {
01067                     $this->t->scanOperand = false;
01068                     $tt = $this->t->peek();
01069                     $this->t->scanOperand = true;
01070                     if ($tt == OP_COLON)
01071                     {
01072                         $label = $this->t->currentToken()->value;
01073                         $ss = $x->stmtStack;
01074                         for ($i = count($ss) - 1; $i >= 0; --$i)
01075                         {
01076                             if ($ss[$i]->label == $label)
01077                                 throw $this->t->newSyntaxError('Duplicate label');
01078                         }
01079 
01080                         $this->t->get();
01081                         $n = new JSNode($this->t, JS_LABEL);
01082                         $n->label = $label;
01083                         $n->statement = $this->nest($x, $n);
01084 
01085                         return $n;
01086                     }
01087                 }
01088 
01089                 $n = new JSNode($this->t, OP_SEMICOLON);
01090                 $this->t->unget();
01091                 $n->expression = $this->Expression($x);
01092                 $n->end = $n->expression->end;
01093             break;
01094         }
01095 
01096         if ($this->t->lineno == $this->t->currentToken()->lineno)
01097         {
01098             $tt = $this->t->peekOnSameLine();
01099             if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
01100                 throw $this->t->newSyntaxError('Missing ; before statement');
01101         }
01102 
01103         $this->t->match(OP_SEMICOLON);
01104 
01105         return $n;
01106     }
01107 
01108     private function FunctionDefinition($x, $requireName, $functionForm)
01109     {
01110         $f = new JSNode($this->t);
01111 
01112         if ($f->type != KEYWORD_FUNCTION)
01113             $f->type = ($f->value == 'get') ? JS_GETTER : JS_SETTER;
01114 
01115         if ($this->t->match(TOKEN_IDENTIFIER))
01116             $f->name = $this->t->currentToken()->value;
01117         elseif ($requireName)
01118             throw $this->t->newSyntaxError('Missing function identifier');
01119 
01120         $this->t->mustMatch(OP_LEFT_PAREN);
01121             $f->params = array();
01122 
01123         while (($tt = $this->t->get()) != OP_RIGHT_PAREN)
01124         {
01125             if ($tt != TOKEN_IDENTIFIER)
01126                 throw $this->t->newSyntaxError('Missing formal parameter');
01127 
01128             array_push($f->params, $this->t->currentToken()->value);
01129 
01130             if ($this->t->peek() != OP_RIGHT_PAREN)
01131                 $this->t->mustMatch(OP_COMMA);
01132         }
01133 
01134         $this->t->mustMatch(OP_LEFT_CURLY);
01135 
01136         $x2 = new JSCompilerContext(true);
01137         $f->body = $this->Script($x2);
01138 
01139         $this->t->mustMatch(OP_RIGHT_CURLY);
01140         $f->end = $this->t->currentToken()->end;
01141 
01142         $f->functionForm = $functionForm;
01143         if ($functionForm == DECLARED_FORM)
01144             array_push($x->funDecls, $f);
01145 
01146         return $f;
01147     }
01148 
01149     private function Variables($x)
01150     {
01151         $n = new JSNode($this->t);
01152 
01153         do
01154         {
01155             $this->t->mustMatch(TOKEN_IDENTIFIER);
01156 
01157             $n2 = new JSNode($this->t);
01158             $n2->name = $n2->value;
01159 
01160             if ($this->t->match(OP_ASSIGN))
01161             {
01162                 if ($this->t->currentToken()->assignOp)
01163                     throw $this->t->newSyntaxError('Invalid variable initialization');
01164 
01165                 $n2->initializer = $this->Expression($x, OP_COMMA);
01166             }
01167 
01168             $n2->readOnly = $n->type == KEYWORD_CONST;
01169 
01170             $n->addNode($n2);
01171             array_push($x->varDecls, $n2);
01172         }
01173         while ($this->t->match(OP_COMMA));
01174 
01175         return $n;
01176     }
01177 
01178     private function Expression($x, $stop=false)
01179     {
01180         $operators = array();
01181         $operands = array();
01182         $n = false;
01183 
01184         $bl = $x->bracketLevel;
01185         $cl = $x->curlyLevel;
01186         $pl = $x->parenLevel;
01187         $hl = $x->hookLevel;
01188 
01189         while (($tt = $this->t->get()) != TOKEN_END)
01190         {
01191             if ($tt == $stop &&
01192                 $x->bracketLevel == $bl &&
01193                 $x->curlyLevel == $cl &&
01194                 $x->parenLevel == $pl &&
01195                 $x->hookLevel == $hl
01196             )
01197             {
01198                 // Stop only if tt matches the optional stop parameter, and that
01199                 // token is not quoted by some kind of bracket.
01200                 break;
01201             }
01202 
01203             switch ($tt)
01204             {
01205                 case OP_SEMICOLON:
01206                     // NB: cannot be empty, Statement handled that.
01207                     break 2;
01208 
01209                 case OP_HOOK:
01210                     if ($this->t->scanOperand)
01211                         break 2;
01212 
01213                     while ( !empty($operators) &&
01214                         $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]
01215                     )
01216                         $this->reduce($operators, $operands);
01217 
01218                     array_push($operators, new JSNode($this->t));
01219 
01220                     ++$x->hookLevel;
01221                     $this->t->scanOperand = true;
01222                     $n = $this->Expression($x);
01223 
01224                     if (!$this->t->match(OP_COLON))
01225                         break 2;
01226 
01227                     --$x->hookLevel;
01228                     array_push($operands, $n);
01229                 break;
01230 
01231                 case OP_COLON:
01232                     if ($x->hookLevel)
01233                         break 2;
01234 
01235                     throw $this->t->newSyntaxError('Invalid label');
01236                 break;
01237 
01238                 case OP_ASSIGN:
01239                     if ($this->t->scanOperand)
01240                         break 2;
01241 
01242                     // Use >, not >=, for right-associative ASSIGN
01243                     while ( !empty($operators) &&
01244                         $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]
01245                     )
01246                         $this->reduce($operators, $operands);
01247 
01248                     array_push($operators, new JSNode($this->t));
01249                     end($operands)->assignOp = $this->t->currentToken()->assignOp;
01250                     $this->t->scanOperand = true;
01251                 break;
01252 
01253                 case KEYWORD_IN:
01254                     // An in operator should not be parsed if we're parsing the head of
01255                     // a for (...) loop, unless it is in the then part of a conditional
01256                     // expression, or parenthesized somehow.
01257                     if ($x->inForLoopInit && !$x->hookLevel &&
01258                         !$x->bracketLevel && !$x->curlyLevel &&
01259                         !$x->parenLevel
01260                     )
01261                         break 2;
01262                 // FALL THROUGH
01263                 case OP_COMMA:
01264                     // A comma operator should not be parsed if we're parsing the then part
01265                     // of a conditional expression unless it's parenthesized somehow.
01266                     if ($tt == OP_COMMA && $x->hookLevel &&
01267                         !$x->bracketLevel && !$x->curlyLevel &&
01268                         !$x->parenLevel
01269                     )
01270                         break 2;
01271                 // Treat comma as left-associative so reduce can fold left-heavy
01272                 // COMMA trees into a single array.
01273                 // FALL THROUGH
01274                 case OP_OR:
01275                 case OP_AND:
01276                 case OP_BITWISE_OR:
01277                 case OP_BITWISE_XOR:
01278                 case OP_BITWISE_AND:
01279                 case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
01280                 case OP_LT: case OP_LE: case OP_GE: case OP_GT:
01281                 case KEYWORD_INSTANCEOF:
01282                 case OP_LSH: case OP_RSH: case OP_URSH:
01283                 case OP_PLUS: case OP_MINUS:
01284                 case OP_MUL: case OP_DIV: case OP_MOD:
01285                 case OP_DOT:
01286                     if ($this->t->scanOperand)
01287                         break 2;
01288 
01289                     while ( !empty($operators) &&
01290                         $this->opPrecedence[end($operators)->type] >= $this->opPrecedence[$tt]
01291                     )
01292                         $this->reduce($operators, $operands);
01293 
01294                     if ($tt == OP_DOT)
01295                     {
01296                         $this->t->mustMatch(TOKEN_IDENTIFIER);
01297                         array_push($operands, new JSNode($this->t, OP_DOT, array_pop($operands), new JSNode($this->t)));
01298                     }
01299                     else
01300                     {
01301                         array_push($operators, new JSNode($this->t));
01302                         $this->t->scanOperand = true;
01303                     }
01304                 break;
01305 
01306                 case KEYWORD_DELETE: case KEYWORD_VOID: case KEYWORD_TYPEOF:
01307                 case OP_NOT: case OP_BITWISE_NOT: case OP_UNARY_PLUS: case OP_UNARY_MINUS:
01308                 case KEYWORD_NEW:
01309                     if (!$this->t->scanOperand)
01310                         break 2;
01311 
01312                     array_push($operators, new JSNode($this->t));
01313                 break;
01314 
01315                 case OP_INCREMENT: case OP_DECREMENT:
01316                     if ($this->t->scanOperand)
01317                     {
01318                         array_push($operators, new JSNode($this->t));  // prefix increment or decrement
01319                     }
01320                     else
01321                     {
01322                         // Don't cross a line boundary for postfix {in,de}crement.
01323                         $t = $this->t->tokens[($this->t->tokenIndex + $this->t->lookahead - 1) & 3];
01324                         if ($t && $t->lineno != $this->t->lineno)
01325                             break 2;
01326 
01327                         if (!empty($operators))
01328                         {
01329                             // Use >, not >=, so postfix has higher precedence than prefix.
01330                             while ($this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt])
01331                                 $this->reduce($operators, $operands);
01332                         }
01333 
01334                         $n = new JSNode($this->t, $tt, array_pop($operands));
01335                         $n->postfix = true;
01336                         array_push($operands, $n);
01337                     }
01338                 break;
01339 
01340                 case KEYWORD_FUNCTION:
01341                     if (!$this->t->scanOperand)
01342                         break 2;
01343 
01344                     array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM));
01345                     $this->t->scanOperand = false;
01346                 break;
01347 
01348                 case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
01349                 case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
01350                     if (!$this->t->scanOperand)
01351                         break 2;
01352 
01353                     array_push($operands, new JSNode($this->t));
01354                     $this->t->scanOperand = false;
01355                 break;
01356 
01357                 case TOKEN_CONDCOMMENT_START:
01358                 case TOKEN_CONDCOMMENT_END:
01359                     if ($this->t->scanOperand)
01360                         array_push($operators, new JSNode($this->t));
01361                     else
01362                         array_push($operands, new JSNode($this->t));
01363                 break;
01364 
01365                 case OP_LEFT_BRACKET:
01366                     if ($this->t->scanOperand)
01367                     {
01368                         // Array initialiser.  Parse using recursive descent, as the
01369                         // sub-grammar here is not an operator grammar.
01370                         $n = new JSNode($this->t, JS_ARRAY_INIT);
01371                         while (($tt = $this->t->peek()) != OP_RIGHT_BRACKET)
01372                         {
01373                             if ($tt == OP_COMMA)
01374                             {
01375                                 $this->t->get();
01376                                 $n->addNode(null);
01377                                 continue;
01378                             }
01379 
01380                             $n->addNode($this->Expression($x, OP_COMMA));
01381                             if (!$this->t->match(OP_COMMA))
01382                                 break;
01383                         }
01384 
01385                         $this->t->mustMatch(OP_RIGHT_BRACKET);
01386                         array_push($operands, $n);
01387                         $this->t->scanOperand = false;
01388                     }
01389                     else
01390                     {
01391                         // Property indexing operator.
01392                         array_push($operators, new JSNode($this->t, JS_INDEX));
01393                         $this->t->scanOperand = true;
01394                         ++$x->bracketLevel;
01395                     }
01396                 break;
01397 
01398                 case OP_RIGHT_BRACKET:
01399                     if ($this->t->scanOperand || $x->bracketLevel == $bl)
01400                         break 2;
01401 
01402                     while ($this->reduce($operators, $operands)->type != JS_INDEX)
01403                         continue;
01404 
01405                     --$x->bracketLevel;
01406                 break;
01407 
01408                 case OP_LEFT_CURLY:
01409                     if (!$this->t->scanOperand)
01410                         break 2;
01411 
01412                     // Object initialiser.  As for array initialisers (see above),
01413                     // parse using recursive descent.
01414                     ++$x->curlyLevel;
01415                     $n = new JSNode($this->t, JS_OBJECT_INIT);
01416                     while (!$this->t->match(OP_RIGHT_CURLY))
01417                     {
01418                         do
01419                         {
01420                             $tt = $this->t->get();
01421                             $tv = $this->t->currentToken()->value;
01422                             if (($tv == 'get' || $tv == 'set') && $this->t->peek() == TOKEN_IDENTIFIER)
01423                             {
01424                                 if ($x->ecmaStrictMode)
01425                                     throw $this->t->newSyntaxError('Illegal property accessor');
01426 
01427                                 $n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM));
01428                             }
01429                             else
01430                             {
01431                                 switch ($tt)
01432                                 {
01433                                     case TOKEN_IDENTIFIER:
01434                                     case TOKEN_NUMBER:
01435                                     case TOKEN_STRING:
01436                                         $id = new JSNode($this->t);
01437                                     break;
01438 
01439                                     case OP_RIGHT_CURLY:
01440                                         if ($x->ecmaStrictMode)
01441                                             throw $this->t->newSyntaxError('Illegal trailing ,');
01442                                     break 3;
01443 
01444                                     default:
01445                                         throw $this->t->newSyntaxError('Invalid property name');
01446                                 }
01447 
01448                                 $this->t->mustMatch(OP_COLON);
01449                                 $n->addNode(new JSNode($this->t, JS_PROPERTY_INIT, $id, $this->Expression($x, OP_COMMA)));
01450                             }
01451                         }
01452                         while ($this->t->match(OP_COMMA));
01453 
01454                         $this->t->mustMatch(OP_RIGHT_CURLY);
01455                         break;
01456                     }
01457 
01458                     array_push($operands, $n);
01459                     $this->t->scanOperand = false;
01460                     --$x->curlyLevel;
01461                 break;
01462 
01463                 case OP_RIGHT_CURLY:
01464                     if (!$this->t->scanOperand && $x->curlyLevel != $cl)
01465                         throw new Exception('PANIC: right curly botch');
01466                 break 2;
01467 
01468                 case OP_LEFT_PAREN:
01469                     if ($this->t->scanOperand)
01470                     {
01471                         array_push($operators, new JSNode($this->t, JS_GROUP));
01472                     }
01473                     else
01474                     {
01475                         while ( !empty($operators) &&
01476                             $this->opPrecedence[end($operators)->type] > $this->opPrecedence[KEYWORD_NEW]
01477                         )
01478                             $this->reduce($operators, $operands);
01479 
01480                         // Handle () now, to regularize the n-ary case for n > 0.
01481                         // We must set scanOperand in case there are arguments and
01482                         // the first one is a regexp or unary+/-.
01483                         $n = end($operators);
01484                         $this->t->scanOperand = true;
01485                         if ($this->t->match(OP_RIGHT_PAREN))
01486                         {
01487                             if ($n && $n->type == KEYWORD_NEW)
01488                             {
01489                                 array_pop($operators);
01490                                 $n->addNode(array_pop($operands));
01491                             }
01492                             else
01493                             {
01494                                 $n = new JSNode($this->t, JS_CALL, array_pop($operands), new JSNode($this->t, JS_LIST));
01495                             }
01496 
01497                             array_push($operands, $n);
01498                             $this->t->scanOperand = false;
01499                             break;
01500                         }
01501 
01502                         if ($n && $n->type == KEYWORD_NEW)
01503                             $n->type = JS_NEW_WITH_ARGS;
01504                         else
01505                             array_push($operators, new JSNode($this->t, JS_CALL));
01506                     }
01507 
01508                     ++$x->parenLevel;
01509                 break;
01510 
01511                 case OP_RIGHT_PAREN:
01512                     if ($this->t->scanOperand || $x->parenLevel == $pl)
01513                         break 2;
01514 
01515                     while (($tt = $this->reduce($operators, $operands)->type) != JS_GROUP &&
01516                         $tt != JS_CALL && $tt != JS_NEW_WITH_ARGS
01517                     )
01518                     {
01519                         continue;
01520                     }
01521 
01522                     if ($tt != JS_GROUP)
01523                     {
01524                         $n = end($operands);
01525                         if ($n->treeNodes[1]->type != OP_COMMA)
01526                             $n->treeNodes[1] = new JSNode($this->t, JS_LIST, $n->treeNodes[1]);
01527                         else
01528                             $n->treeNodes[1]->type = JS_LIST;
01529                     }
01530 
01531                     --$x->parenLevel;
01532                 break;
01533 
01534                 // Automatic semicolon insertion means we may scan across a newline
01535                 // and into the beginning of another statement.  If so, break out of
01536                 // the while loop and let the t.scanOperand logic handle errors.
01537                 default:
01538                     break 2;
01539             }
01540         }
01541 
01542         if ($x->hookLevel != $hl)
01543             throw $this->t->newSyntaxError('Missing : in conditional expression');
01544 
01545         if ($x->parenLevel != $pl)
01546             throw $this->t->newSyntaxError('Missing ) in parenthetical');
01547 
01548         if ($x->bracketLevel != $bl)
01549             throw $this->t->newSyntaxError('Missing ] in index expression');
01550 
01551         if ($this->t->scanOperand)
01552             throw $this->t->newSyntaxError('Missing operand');
01553 
01554         // Resume default mode, scanning for operands, not operators.
01555         $this->t->scanOperand = true;
01556         $this->t->unget();
01557 
01558         while (count($operators))
01559             $this->reduce($operators, $operands);
01560 
01561         return array_pop($operands);
01562     }
01563 
01564     private function ParenExpression($x)
01565     {
01566         $this->t->mustMatch(OP_LEFT_PAREN);
01567         $n = $this->Expression($x);
01568         $this->t->mustMatch(OP_RIGHT_PAREN);
01569 
01570         return $n;
01571     }
01572 
01573     // Statement stack and nested statement handler.
01574     private function nest($x, $node, $end = false)
01575     {
01576         array_push($x->stmtStack, $node);
01577         $n = $this->statement($x);
01578         array_pop($x->stmtStack);
01579 
01580         if ($end)
01581             $this->t->mustMatch($end);
01582 
01583         return $n;
01584     }
01585 
01586     private function reduce(&$operators, &$operands)
01587     {
01588         $n = array_pop($operators);
01589         $op = $n->type;
01590         $arity = $this->opArity[$op];
01591         $c = count($operands);
01592         if ($arity == -2)
01593         {
01594             // Flatten left-associative trees
01595             if ($c >= 2)
01596             {
01597                 $left = $operands[$c - 2];
01598                 if ($left->type == $op)
01599                 {
01600                     $right = array_pop($operands);
01601                     $left->addNode($right);
01602                     return $left;
01603                 }
01604             }
01605             $arity = 2;
01606         }
01607 
01608         // Always use push to add operands to n, to update start and end
01609         $a = array_splice($operands, $c - $arity);
01610         for ($i = 0; $i < $arity; $i++)
01611             $n->addNode($a[$i]);
01612 
01613         // Include closing bracket or postfix operator in [start,end]
01614         $te = $this->t->currentToken()->end;
01615         if ($n->end < $te)
01616             $n->end = $te;
01617 
01618         array_push($operands, $n);
01619 
01620         return $n;
01621     }
01622 }
01623 
01624 class JSCompilerContext
01625 {
01626     public $inFunction = false;
01627     public $inForLoopInit = false;
01628     public $ecmaStrictMode = false;
01629     public $bracketLevel = 0;
01630     public $curlyLevel = 0;
01631     public $parenLevel = 0;
01632     public $hookLevel = 0;
01633 
01634     public $stmtStack = array();
01635     public $funDecls = array();
01636     public $varDecls = array();
01637 
01638     public function __construct($inFunction)
01639     {
01640         $this->inFunction = $inFunction;
01641     }
01642 }
01643 
01644 class JSNode
01645 {
01646     private $type;
01647     private $value;
01648     private $lineno;
01649     private $start;
01650     private $end;
01651 
01652     public $treeNodes = array();
01653     public $funDecls = array();
01654     public $varDecls = array();
01655 
01656     public function __construct($t, $type=0)
01657     {
01658         if ($token = $t->currentToken())
01659         {
01660             $this->type = $type ? $type : $token->type;
01661             $this->value = $token->value;
01662             $this->lineno = $token->lineno;
01663             $this->start = $token->start;
01664             $this->end = $token->end;
01665         }
01666         else
01667         {
01668             $this->type = $type;
01669             $this->lineno = $t->lineno;
01670         }
01671 
01672         if (($numargs = func_num_args()) > 2)
01673         {
01674             $args = func_get_args();
01675             for ($i = 2; $i < $numargs; $i++)
01676                 $this->addNode($args[$i]);
01677         }
01678     }
01679 
01680     // we don't want to bloat our object with all kind of specific properties, so we use overloading
01681     public function __set($name, $value)
01682     {
01683         $this->$name = $value;
01684     }
01685 
01686     public function __get($name)
01687     {
01688         if (isset($this->$name))
01689             return $this->$name;
01690 
01691         return null;
01692     }
01693 
01694     public function addNode($node)
01695     {
01696         if ($node !== null)
01697         {
01698             if ($node->start < $this->start)
01699                 $this->start = $node->start;
01700             if ($this->end < $node->end)
01701                 $this->end = $node->end;
01702         }
01703 
01704         $this->treeNodes[] = $node;
01705     }
01706 }
01707 
01708 class JSTokenizer
01709 {
01710     private $cursor = 0;
01711     private $source;
01712 
01713     public $tokens = array();
01714     public $tokenIndex = 0;
01715     public $lookahead = 0;
01716     public $scanNewlines = false;
01717     public $scanOperand = true;
01718 
01719     public $filename;
01720     public $lineno;
01721 
01722     private $keywords = array(
01723         'break',
01724         'case', 'catch', 'const', 'continue',
01725         'debugger', 'default', 'delete', 'do',
01726         'else', 'enum',
01727         'false', 'finally', 'for', 'function',
01728         'if', 'in', 'instanceof',
01729         'new', 'null',
01730         'return',
01731         'switch',
01732         'this', 'throw', 'true', 'try', 'typeof',
01733         'var', 'void',
01734         'while', 'with'
01735     );
01736 
01737     private $opTypeNames = array(
01738         ';', ',', '?', ':', '||', '&&', '|', '^',
01739         '&', '===', '==', '=', '!==', '!=', '<<', '<=',
01740         '<', '>>>', '>>', '>=', '>', '++', '--', '+',
01741         '-', '*', '/', '%', '!', '~', '.', '[',
01742         ']', '{', '}', '(', ')', '@*/'
01743     );
01744 
01745     private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
01746     private $opRegExp;
01747 
01748     public function __construct()
01749     {
01750         $this->opRegExp = '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames)) . ')#';
01751     }
01752 
01753     public function init($source, $filename = '', $lineno = 1)
01754     {
01755         $this->source = $source;
01756         $this->filename = $filename ? $filename : '[inline]';
01757         $this->lineno = $lineno;
01758 
01759         $this->cursor = 0;
01760         $this->tokens = array();
01761         $this->tokenIndex = 0;
01762         $this->lookahead = 0;
01763         $this->scanNewlines = false;
01764         $this->scanOperand = true;
01765     }
01766 
01767     public function getInput($chunksize)
01768     {
01769         if ($chunksize)
01770             return substr($this->source, $this->cursor, $chunksize);
01771 
01772         return substr($this->source, $this->cursor);
01773     }
01774 
01775     public function isDone()
01776     {
01777         return $this->peek() == TOKEN_END;
01778     }
01779 
01780     public function match($tt)
01781     {
01782         return $this->get() == $tt || $this->unget();
01783     }
01784 
01785     public function mustMatch($tt)
01786     {
01787             if (!$this->match($tt))
01788             throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
01789 
01790         return $this->currentToken();
01791     }
01792 
01793     public function peek()
01794     {
01795         if ($this->lookahead)
01796         {
01797             $next = $this->tokens[($this->tokenIndex + $this->lookahead) & 3];
01798             if ($this->scanNewlines && $next->lineno != $this->lineno)
01799                 $tt = TOKEN_NEWLINE;
01800             else
01801                 $tt = $next->type;
01802         }
01803         else
01804         {
01805             $tt = $this->get();
01806             $this->unget();
01807         }
01808 
01809         return $tt;
01810     }
01811 
01812     public function peekOnSameLine()
01813     {
01814         $this->scanNewlines = true;
01815         $tt = $this->peek();
01816         $this->scanNewlines = false;
01817 
01818         return $tt;
01819     }
01820 
01821     public function currentToken()
01822     {
01823         if (!empty($this->tokens))
01824             return $this->tokens[$this->tokenIndex];
01825     }
01826 
01827     public function get($chunksize = 1000)
01828     {
01829         while($this->lookahead)
01830         {
01831             $this->lookahead--;
01832             $this->tokenIndex = ($this->tokenIndex + 1) & 3;
01833             $token = $this->tokens[$this->tokenIndex];
01834             if ($token->type != TOKEN_NEWLINE || $this->scanNewlines)
01835                 return $token->type;
01836         }
01837 
01838         $conditional_comment = false;
01839 
01840         // strip whitespace and comments
01841         while(true)
01842         {
01843             $input = $this->getInput($chunksize);
01844 
01845             // whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
01846             $re = $this->scanNewlines ? '/^[ \r\t]+/' : '/^\s+/';
01847             if (preg_match($re, $input, $match))
01848             {
01849                 $spaces = $match[0];
01850                 $spacelen = strlen($spaces);
01851                 $this->cursor += $spacelen;
01852                 if (!$this->scanNewlines)
01853                     $this->lineno += substr_count($spaces, "\n");
01854 
01855                 if ($spacelen == $chunksize)
01856                     continue; // complete chunk contained whitespace
01857 
01858                 $input = $this->getInput($chunksize);
01859                 if ($input == '' || $input[0] != '/')
01860                     break;
01861             }
01862 
01863             // Comments
01864             if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match))
01865             {
01866                 if (!$chunksize)
01867                     break;
01868 
01869                 // retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
01870                 $chunksize = null;
01871                 continue;
01872             }
01873 
01874             // check if this is a conditional (JScript) comment
01875             if (!empty($match[1]))
01876             {
01877                 $match[0] = '/*' . $match[1];
01878                 $conditional_comment = true;
01879                 break;
01880             }
01881             else
01882             {
01883                 $this->cursor += strlen($match[0]);
01884                 $this->lineno += substr_count($match[0], "\n");
01885             }
01886         }
01887 
01888         if ($input == '')
01889         {
01890             $tt = TOKEN_END;
01891             $match = array('');
01892         }
01893         elseif ($conditional_comment)
01894         {
01895             $tt = TOKEN_CONDCOMMENT_START;
01896         }
01897         else
01898         {
01899             switch ($input[0])
01900             {
01901                 case '0':
01902                     // hexadecimal
01903                     if (($input[1] == 'x' || $input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match))
01904                     {
01905                         $tt = TOKEN_NUMBER;
01906                         break;
01907                     }
01908                 // FALL THROUGH
01909 
01910                 case '1': case '2': case '3': case '4': case '5':
01911                 case '6': case '7': case '8': case '9':
01912                     // should always match
01913                     preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match);
01914                     $tt = TOKEN_NUMBER;
01915                 break;
01916 
01917                 case "'":
01918                     if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match))
01919                     {
01920                         $tt = TOKEN_STRING;
01921                     }
01922                     else
01923                     {
01924                         if ($chunksize)
01925                             return $this->get(null); // retry with a full chunk fetch
01926 
01927                         throw $this->newSyntaxError('Unterminated string literal');
01928                     }
01929                 break;
01930 
01931                 case '"':
01932                     if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match))
01933                     {
01934                         $tt = TOKEN_STRING;
01935                     }
01936                     else
01937                     {
01938                         if ($chunksize)
01939                             return $this->get(null); // retry with a full chunk fetch
01940 
01941                         throw $this->newSyntaxError('Unterminated string literal');
01942                     }
01943                 break;
01944 
01945                 case '/':
01946                     if ($this->scanOperand && preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
01947                     {
01948                         $tt = TOKEN_REGEXP;
01949                         break;
01950                     }
01951                 // FALL THROUGH
01952 
01953                 case '|':
01954                 case '^':
01955                 case '&':
01956                 case '<':
01957                 case '>':
01958                 case '+':
01959                 case '-':
01960                 case '*':
01961                 case '%':
01962                 case '=':
01963                 case '!':
01964                     // should always match
01965                     preg_match($this->opRegExp, $input, $match);
01966                     $op = $match[0];
01967                     if (in_array($op, $this->assignOps) && $input[strlen($op)] == '=')
01968                     {
01969                         $tt = OP_ASSIGN;
01970                         $match[0] .= '=';
01971                     }
01972                     else
01973                     {
01974                         $tt = $op;
01975                         if ($this->scanOperand)
01976                         {
01977                             if ($op == OP_PLUS)
01978                                 $tt = OP_UNARY_PLUS;
01979                             elseif ($op == OP_MINUS)
01980                                 $tt = OP_UNARY_MINUS;
01981                         }
01982                         $op = null;
01983                     }
01984                 break;
01985 
01986                 case '.':
01987                     if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
01988                     {
01989                         $tt = TOKEN_NUMBER;
01990                         break;
01991                     }
01992                 // FALL THROUGH
01993 
01994                 case ';':
01995                 case ',':
01996                 case '?':
01997                 case ':':
01998                 case '~':
01999                 case '[':
02000                 case ']':
02001                 case '{':
02002                 case '}':
02003                 case '(':
02004                 case ')':
02005                     // these are all single
02006                     $match = array($input[0]);
02007                     $tt = $input[0];
02008                 break;
02009 
02010                 case '@':
02011                     // check end of conditional comment
02012                     if (substr($input, 0, 3) == '@*/')
02013                     {
02014                         $match = array('@*/');
02015                         $tt = TOKEN_CONDCOMMENT_END;
02016                     }
02017                     else
02018                         throw $this->newSyntaxError('Illegal token');
02019                 break;
02020 
02021                 case "\n":
02022                     if ($this->scanNewlines)
02023                     {
02024                         $match = array("\n");
02025                         $tt = TOKEN_NEWLINE;
02026                     }
02027                     else
02028                         throw $this->newSyntaxError('Illegal token');
02029                 break;
02030 
02031                 default:
02032                     // Fast path for identifiers: word chars followed by whitespace or various other tokens.
02033                     // Note we don't need to exclude digits in the first char, as they've already been found
02034                     // above.
02035                     if (!preg_match('/^[$\w]+(?=[\s\/\|\^\&<>\+\-\*%=!.;,\?:~\[\]\{\}\(\)@])/', $input, $match))
02036                     {
02037                         // Character classes per ECMA-262 edition 5.1 section 7.6
02038                         // Per spec, must accept Unicode 3.0, *may* accept later versions.
02039                         // We'll take whatever PCRE understands, which should be more recent.
02040                         $identifierStartChars = "\\p{L}\\p{Nl}" .  # UnicodeLetter
02041                                                 "\$" .
02042                                                 "_";
02043                         $identifierPartChars  = $identifierStartChars .
02044                                                 "\\p{Mn}\\p{Mc}" . # UnicodeCombiningMark
02045                                                 "\\p{Nd}" .        # UnicodeDigit
02046                                                 "\\p{Pc}";         # UnicodeConnectorPunctuation
02047                         $unicodeEscape = "\\\\u[0-9A-F-a-f]{4}";
02048                         $identifierRegex = "/^" .
02049                                            "(?:[$identifierStartChars]|$unicodeEscape)" .
02050                                            "(?:[$identifierPartChars]|$unicodeEscape)*" .
02051                                            "/uS";
02052                         if (preg_match($identifierRegex, $input, $match))
02053                         {
02054                             if (strpos($match[0], '\\') !== false) {
02055                                 // Per ECMA-262 edition 5.1, section 7.6 escape sequences should behave as if they were
02056                                 // the original chars, but only within the boundaries of the identifier.
02057                                 $decoded = preg_replace_callback('/\\\\u([0-9A-Fa-f]{4})/',
02058                                         array(__CLASS__, 'unicodeEscapeCallback'),
02059                                         $match[0]);
02060 
02061                                 // Since our original regex didn't de-escape the originals, we need to check for validity again.
02062                                 // No need to worry about token boundaries, as anything outside the identifier is illegal!
02063                                 if (!preg_match("/^[$identifierStartChars][$identifierPartChars]*$/u", $decoded)) {
02064                                     throw $this->newSyntaxError('Illegal token');
02065                                 }
02066 
02067                                 // Per spec it _ought_ to work to use these escapes for keywords words as well...
02068                                 // but IE rejects them as invalid, while Firefox and Chrome treat them as identifiers
02069                                 // that don't match the keyword.
02070                                 if (in_array($decoded, $this->keywords)) {
02071                                     throw $this->newSyntaxError('Illegal token');
02072                                 }
02073 
02074                                 // TODO: save the decoded form for output?
02075                             }
02076                         }
02077                         else
02078                             throw $this->newSyntaxError('Illegal token');
02079                     }
02080                     $tt = in_array($match[0], $this->keywords) ? $match[0] : TOKEN_IDENTIFIER;
02081             }
02082         }
02083 
02084         $this->tokenIndex = ($this->tokenIndex + 1) & 3;
02085 
02086         if (!isset($this->tokens[$this->tokenIndex]))
02087             $this->tokens[$this->tokenIndex] = new JSToken();
02088 
02089         $token = $this->tokens[$this->tokenIndex];
02090         $token->type = $tt;
02091 
02092         if ($tt == OP_ASSIGN)
02093             $token->assignOp = $op;
02094 
02095         $token->start = $this->cursor;
02096 
02097         $token->value = $match[0];
02098         $this->cursor += strlen($match[0]);
02099 
02100         $token->end = $this->cursor;
02101         $token->lineno = $this->lineno;
02102 
02103         return $tt;
02104     }
02105 
02106     public function unget()
02107     {
02108         if (++$this->lookahead == 4)
02109             throw $this->newSyntaxError('PANIC: too much lookahead!');
02110 
02111         $this->tokenIndex = ($this->tokenIndex - 1) & 3;
02112     }
02113 
02114     public function newSyntaxError($m)
02115     {
02116         return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename . '\' on line ' . $this->lineno);
02117     }
02118 
02119     public static function unicodeEscapeCallback($m)
02120     {
02121         return html_entity_decode('&#x' . $m[1]. ';', ENT_QUOTES, 'UTF-8');
02122     }
02123 }
02124 
02125 class JSToken
02126 {
02127     public $type;
02128     public $value;
02129     public $start;
02130     public $end;
02131     public $lineno;
02132     public $assignOp;
02133 }