MediaWiki  REL1_19
jsminplus.php
Go to the documentation of this file.
00001 <?php
00002 
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                                                                 // mutiple 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 carefull 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 concatted strings with same quotestyle
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 }