1<?php 2 3/** 4 * JSMinPlus version 1.4 5 * 6 * Minifies a javascript file using a javascript parser 7 * 8 * This implements a PHP port of Brendan Eich's Narcissus open source javascript engine (in javascript) 9 * References: http://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine) 10 * Narcissus sourcecode: http://mxr.mozilla.org/mozilla/source/js/narcissus/ 11 * JSMinPlus weblog: http://crisp.tweakblogs.net/blog/cat/716 12 * 13 * Tino Zijdel <crisp@tweakers.net> 14 * 15 * Usage: $minified = JSMinPlus::minify($script [, $filename]) 16 * 17 * Versionlog (see also changelog.txt): 18 * 23-07-2011 - remove dynamic creation of OP_* and KEYWORD_* defines and declare them on top 19 * reduce memory footprint by minifying by block-scope 20 * some small byte-saving and performance improvements 21 * 12-05-2009 - fixed hook:colon precedence, fixed empty body in loop and if-constructs 22 * 18-04-2009 - fixed crashbug in PHP 5.2.9 and several other bugfixes 23 * 12-04-2009 - some small bugfixes and performance improvements 24 * 09-04-2009 - initial open sourced version 1.0 25 * 26 * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip 27 * 28 */ 29 30/* ***** BEGIN LICENSE BLOCK ***** 31 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 32 * 33 * The contents of this file are subject to the Mozilla Public License Version 34 * 1.1 (the "License"); you may not use this file except in compliance with 35 * the License. You may obtain a copy of the License at 36 * http://www.mozilla.org/MPL/ 37 * 38 * Software distributed under the License is distributed on an "AS IS" basis, 39 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 40 * for the specific language governing rights and limitations under the 41 * License. 42 * 43 * The Original Code is the Narcissus JavaScript engine. 44 * 45 * The Initial Developer of the Original Code is 46 * Brendan Eich <brendan@mozilla.org>. 47 * Portions created by the Initial Developer are Copyright (C) 2004 48 * the Initial Developer. All Rights Reserved. 49 * 50 * Contributor(s): Tino Zijdel <crisp@tweakers.net> 51 * PHP port, modifications and minifier routine are (C) 2009-2011 52 * 53 * Alternatively, the contents of this file may be used under the terms of 54 * either the GNU General Public License Version 2 or later (the "GPL"), or 55 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 56 * in which case the provisions of the GPL or the LGPL are applicable instead 57 * of those above. If you wish to allow use of your version of this file only 58 * under the terms of either the GPL or the LGPL, and not to allow others to 59 * use your version of this file under the terms of the MPL, indicate your 60 * decision by deleting the provisions above and replace them with the notice 61 * and other provisions required by the GPL or the LGPL. If you do not delete 62 * the provisions above, a recipient may use your version of this file under 63 * the terms of any one of the MPL, the GPL or the LGPL. 64 * 65 * ***** END LICENSE BLOCK ***** */ 66 67define('TOKEN_END', 1); 68define('TOKEN_NUMBER', 2); 69define('TOKEN_IDENTIFIER', 3); 70define('TOKEN_STRING', 4); 71define('TOKEN_REGEXP', 5); 72define('TOKEN_NEWLINE', 6); 73define('TOKEN_CONDCOMMENT_START', 7); 74define('TOKEN_CONDCOMMENT_END', 8); 75 76define('JS_SCRIPT', 100); 77define('JS_BLOCK', 101); 78define('JS_LABEL', 102); 79define('JS_FOR_IN', 103); 80define('JS_CALL', 104); 81define('JS_NEW_WITH_ARGS', 105); 82define('JS_INDEX', 106); 83define('JS_ARRAY_INIT', 107); 84define('JS_OBJECT_INIT', 108); 85define('JS_PROPERTY_INIT', 109); 86define('JS_GETTER', 110); 87define('JS_SETTER', 111); 88define('JS_GROUP', 112); 89define('JS_LIST', 113); 90 91define('JS_MINIFIED', 999); 92 93define('DECLARED_FORM', 0); 94define('EXPRESSED_FORM', 1); 95define('STATEMENT_FORM', 2); 96 97/* Operators */ 98define('OP_SEMICOLON', ';'); 99define('OP_COMMA', ','); 100define('OP_HOOK', '?'); 101define('OP_COLON', ':'); 102define('OP_OR', '||'); 103define('OP_AND', '&&'); 104define('OP_BITWISE_OR', '|'); 105define('OP_BITWISE_XOR', '^'); 106define('OP_BITWISE_AND', '&'); 107define('OP_STRICT_EQ', '==='); 108define('OP_EQ', '=='); 109define('OP_ASSIGN', '='); 110define('OP_STRICT_NE', '!=='); 111define('OP_NE', '!='); 112define('OP_LSH', '<<'); 113define('OP_LE', '<='); 114define('OP_LT', '<'); 115define('OP_URSH', '>>>'); 116define('OP_RSH', '>>'); 117define('OP_GE', '>='); 118define('OP_GT', '>'); 119define('OP_INCREMENT', '++'); 120define('OP_DECREMENT', '--'); 121define('OP_PLUS', '+'); 122define('OP_MINUS', '-'); 123define('OP_MUL', '*'); 124define('OP_DIV', '/'); 125define('OP_MOD', '%'); 126define('OP_NOT', '!'); 127define('OP_BITWISE_NOT', '~'); 128define('OP_DOT', '.'); 129define('OP_LEFT_BRACKET', '['); 130define('OP_RIGHT_BRACKET', ']'); 131define('OP_LEFT_CURLY', '{'); 132define('OP_RIGHT_CURLY', '}'); 133define('OP_LEFT_PAREN', '('); 134define('OP_RIGHT_PAREN', ')'); 135define('OP_CONDCOMMENT_END', '@*/'); 136 137define('OP_UNARY_PLUS', 'U+'); 138define('OP_UNARY_MINUS', 'U-'); 139 140/* Keywords */ 141define('KEYWORD_BREAK', 'break'); 142define('KEYWORD_CASE', 'case'); 143define('KEYWORD_CATCH', 'catch'); 144define('KEYWORD_CONST', 'const'); 145define('KEYWORD_CONTINUE', 'continue'); 146define('KEYWORD_DEBUGGER', 'debugger'); 147define('KEYWORD_DEFAULT', 'default'); 148define('KEYWORD_DELETE', 'delete'); 149define('KEYWORD_DO', 'do'); 150define('KEYWORD_ELSE', 'else'); 151define('KEYWORD_ENUM', 'enum'); 152define('KEYWORD_FALSE', 'false'); 153define('KEYWORD_FINALLY', 'finally'); 154define('KEYWORD_FOR', 'for'); 155define('KEYWORD_FUNCTION', 'function'); 156define('KEYWORD_IF', 'if'); 157define('KEYWORD_IN', 'in'); 158define('KEYWORD_INSTANCEOF', 'instanceof'); 159define('KEYWORD_NEW', 'new'); 160define('KEYWORD_NULL', 'null'); 161define('KEYWORD_RETURN', 'return'); 162define('KEYWORD_SWITCH', 'switch'); 163define('KEYWORD_THIS', 'this'); 164define('KEYWORD_THROW', 'throw'); 165define('KEYWORD_TRUE', 'true'); 166define('KEYWORD_TRY', 'try'); 167define('KEYWORD_TYPEOF', 'typeof'); 168define('KEYWORD_VAR', 'var'); 169define('KEYWORD_VOID', 'void'); 170define('KEYWORD_WHILE', 'while'); 171define('KEYWORD_WITH', 'with'); 172 173/** 174 * @deprecated 2.3 This will be removed in Minify 3.0 175 */ 176class JSMinPlus 177{ 178 private $parser; 179 private $reserved = array( 180 'break', 'case', 'catch', 'continue', 'default', 'delete', 'do', 181 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof', 182 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 183 'void', 'while', 'with', 184 // Words reserved for future use 185 'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger', 186 'double', 'enum', 'export', 'extends', 'final', 'float', 'goto', 187 'implements', 'import', 'int', 'interface', 'long', 'native', 188 'package', 'private', 'protected', 'public', 'short', 'static', 189 'super', 'synchronized', 'throws', 'transient', 'volatile', 190 // These are not reserved, but should be taken into account 191 // in isValidIdentifier (See jslint source code) 192 'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined' 193 ); 194 195 private function __construct() 196 { 197 $this->parser = new JSParser($this); 198 } 199 200 public static function minify($js, $filename='') 201 { 202 trigger_error(__CLASS__ . ' is deprecated. This will be removed in Minify 3.0', E_USER_DEPRECATED); 203 204 static $instance; 205 206 // this is a singleton 207 if(!$instance) 208 $instance = new JSMinPlus(); 209 210 return $instance->min($js, $filename); 211 } 212 213 private function min($js, $filename) 214 { 215 try 216 { 217 $n = $this->parser->parse($js, $filename, 1); 218 return $this->parseTree($n); 219 } 220 catch(Exception $e) 221 { 222 echo $e->getMessage() . "\n"; 223 } 224 225 return false; 226 } 227 228 public function parseTree($n, $noBlockGrouping = false) 229 { 230 $s = ''; 231 232 switch ($n->type) 233 { 234 case JS_MINIFIED: 235 $s = $n->value; 236 break; 237 238 case JS_SCRIPT: 239 // we do nothing yet with funDecls or varDecls 240 $noBlockGrouping = true; 241 // FALL THROUGH 242 243 case JS_BLOCK: 244 $childs = $n->treeNodes; 245 $lastType = 0; 246 for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++) 247 { 248 $type = $childs[$i]->type; 249 $t = $this->parseTree($childs[$i]); 250 if (strlen($t)) 251 { 252 if ($c) 253 { 254 $s = rtrim($s, ';'); 255 256 if ($type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM) 257 { 258 // put declared functions on a new line 259 $s .= "\n"; 260 } 261 elseif ($type == KEYWORD_VAR && $type == $lastType) 262 { 263 // mutiple var-statements can go into one 264 $t = ',' . substr($t, 4); 265 } 266 else 267 { 268 // add terminator 269 $s .= ';'; 270 } 271 } 272 273 $s .= $t; 274 275 $c++; 276 $lastType = $type; 277 } 278 } 279 280 if ($c > 1 && !$noBlockGrouping) 281 { 282 $s = '{' . $s . '}'; 283 } 284 break; 285 286 case KEYWORD_FUNCTION: 287 $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '('; 288 $params = $n->params; 289 for ($i = 0, $j = count($params); $i < $j; $i++) 290 $s .= ($i ? ',' : '') . $params[$i]; 291 $s .= '){' . $this->parseTree($n->body, true) . '}'; 292 break; 293 294 case KEYWORD_IF: 295 $s = 'if(' . $this->parseTree($n->condition) . ')'; 296 $thenPart = $this->parseTree($n->thenPart); 297 $elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null; 298 299 // empty if-statement 300 if ($thenPart == '') 301 $thenPart = ';'; 302 303 if ($elsePart) 304 { 305 // be carefull and always make a block out of the thenPart; could be more optimized but is a lot of trouble 306 if ($thenPart != ';' && $thenPart[0] != '{') 307 $thenPart = '{' . $thenPart . '}'; 308 309 $s .= $thenPart . 'else'; 310 311 // we could check for more, but that hardly ever applies so go for performance 312 if ($elsePart[0] != '{') 313 $s .= ' '; 314 315 $s .= $elsePart; 316 } 317 else 318 { 319 $s .= $thenPart; 320 } 321 break; 322 323 case KEYWORD_SWITCH: 324 $s = 'switch(' . $this->parseTree($n->discriminant) . '){'; 325 $cases = $n->cases; 326 for ($i = 0, $j = count($cases); $i < $j; $i++) 327 { 328 $case = $cases[$i]; 329 if ($case->type == KEYWORD_CASE) 330 $s .= 'case' . ($case->caseLabel->type != TOKEN_STRING ? ' ' : '') . $this->parseTree($case->caseLabel) . ':'; 331 else 332 $s .= 'default:'; 333 334 $statement = $this->parseTree($case->statements, true); 335 if ($statement) 336 { 337 $s .= $statement; 338 // no terminator for last statement 339 if ($i + 1 < $j) 340 $s .= ';'; 341 } 342 } 343 $s .= '}'; 344 break; 345 346 case KEYWORD_FOR: 347 $s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '') 348 . ';' . ($n->condition ? $this->parseTree($n->condition) : '') 349 . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')'; 350 351 $body = $this->parseTree($n->body); 352 if ($body == '') 353 $body = ';'; 354 355 $s .= $body; 356 break; 357 358 case KEYWORD_WHILE: 359 $s = 'while(' . $this->parseTree($n->condition) . ')'; 360 361 $body = $this->parseTree($n->body); 362 if ($body == '') 363 $body = ';'; 364 365 $s .= $body; 366 break; 367 368 case JS_FOR_IN: 369 $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')'; 370 371 $body = $this->parseTree($n->body); 372 if ($body == '') 373 $body = ';'; 374 375 $s .= $body; 376 break; 377 378 case KEYWORD_DO: 379 $s = 'do{' . $this->parseTree($n->body, true) . '}while(' . $this->parseTree($n->condition) . ')'; 380 break; 381 382 case KEYWORD_BREAK: 383 case KEYWORD_CONTINUE: 384 $s = $n->value . ($n->label ? ' ' . $n->label : ''); 385 break; 386 387 case KEYWORD_TRY: 388 $s = 'try{' . $this->parseTree($n->tryBlock, true) . '}'; 389 $catchClauses = $n->catchClauses; 390 for ($i = 0, $j = count($catchClauses); $i < $j; $i++) 391 { 392 $t = $catchClauses[$i]; 393 $s .= 'catch(' . $t->varName . ($t->guard ? ' if ' . $this->parseTree($t->guard) : '') . '){' . $this->parseTree($t->block, true) . '}'; 394 } 395 if ($n->finallyBlock) 396 $s .= 'finally{' . $this->parseTree($n->finallyBlock, true) . '}'; 397 break; 398 399 case KEYWORD_THROW: 400 case KEYWORD_RETURN: 401 $s = $n->type; 402 if ($n->value) 403 { 404 $t = $this->parseTree($n->value); 405 if (strlen($t)) 406 { 407 if ($this->isWordChar($t[0]) || $t[0] == '\\') 408 $s .= ' '; 409 410 $s .= $t; 411 } 412 } 413 break; 414 415 case KEYWORD_WITH: 416 $s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body); 417 break; 418 419 case KEYWORD_VAR: 420 case KEYWORD_CONST: 421 $s = $n->value . ' '; 422 $childs = $n->treeNodes; 423 for ($i = 0, $j = count($childs); $i < $j; $i++) 424 { 425 $t = $childs[$i]; 426 $s .= ($i ? ',' : '') . $t->name; 427 $u = $t->initializer; 428 if ($u) 429 $s .= '=' . $this->parseTree($u); 430 } 431 break; 432 433 case KEYWORD_IN: 434 case KEYWORD_INSTANCEOF: 435 $left = $this->parseTree($n->treeNodes[0]); 436 $right = $this->parseTree($n->treeNodes[1]); 437 438 $s = $left; 439 440 if ($this->isWordChar(substr($left, -1))) 441 $s .= ' '; 442 443 $s .= $n->type; 444 445 if ($this->isWordChar($right[0]) || $right[0] == '\\') 446 $s .= ' '; 447 448 $s .= $right; 449 break; 450 451 case KEYWORD_DELETE: 452 case KEYWORD_TYPEOF: 453 $right = $this->parseTree($n->treeNodes[0]); 454 455 $s = $n->type; 456 457 if ($this->isWordChar($right[0]) || $right[0] == '\\') 458 $s .= ' '; 459 460 $s .= $right; 461 break; 462 463 case KEYWORD_VOID: 464 $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')'; 465 break; 466 467 case KEYWORD_DEBUGGER: 468 throw new Exception('NOT IMPLEMENTED: DEBUGGER'); 469 break; 470 471 case TOKEN_CONDCOMMENT_START: 472 case TOKEN_CONDCOMMENT_END: 473 $s = $n->value . ($n->type == TOKEN_CONDCOMMENT_START ? ' ' : ''); 474 $childs = $n->treeNodes; 475 for ($i = 0, $j = count($childs); $i < $j; $i++) 476 $s .= $this->parseTree($childs[$i]); 477 break; 478 479 case OP_SEMICOLON: 480 if ($expression = $n->expression) 481 $s = $this->parseTree($expression); 482 break; 483 484 case JS_LABEL: 485 $s = $n->label . ':' . $this->parseTree($n->statement); 486 break; 487 488 case OP_COMMA: 489 $childs = $n->treeNodes; 490 for ($i = 0, $j = count($childs); $i < $j; $i++) 491 $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]); 492 break; 493 494 case OP_ASSIGN: 495 $s = $this->parseTree($n->treeNodes[0]) . $n->value . $this->parseTree($n->treeNodes[1]); 496 break; 497 498 case OP_HOOK: 499 $s = $this->parseTree($n->treeNodes[0]) . '?' . $this->parseTree($n->treeNodes[1]) . ':' . $this->parseTree($n->treeNodes[2]); 500 break; 501 502 case OP_OR: case OP_AND: 503 case OP_BITWISE_OR: case OP_BITWISE_XOR: case OP_BITWISE_AND: 504 case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE: 505 case OP_LT: case OP_LE: case OP_GE: case OP_GT: 506 case OP_LSH: case OP_RSH: case OP_URSH: 507 case OP_MUL: case OP_DIV: case OP_MOD: 508 $s = $this->parseTree($n->treeNodes[0]) . $n->type . $this->parseTree($n->treeNodes[1]); 509 break; 510 511 case OP_PLUS: 512 case OP_MINUS: 513 $left = $this->parseTree($n->treeNodes[0]); 514 $right = $this->parseTree($n->treeNodes[1]); 515 516 switch ($n->treeNodes[1]->type) 517 { 518 case OP_PLUS: 519 case OP_MINUS: 520 case OP_INCREMENT: 521 case OP_DECREMENT: 522 case OP_UNARY_PLUS: 523 case OP_UNARY_MINUS: 524 $s = $left . $n->type . ' ' . $right; 525 break; 526 527 case TOKEN_STRING: 528 //combine concatted strings with same quotestyle 529 if ($n->type == OP_PLUS && substr($left, -1) == $right[0]) 530 { 531 $s = substr($left, 0, -1) . substr($right, 1); 532 break; 533 } 534 // FALL THROUGH 535 536 default: 537 $s = $left . $n->type . $right; 538 } 539 break; 540 541 case OP_NOT: 542 case OP_BITWISE_NOT: 543 case OP_UNARY_PLUS: 544 case OP_UNARY_MINUS: 545 $s = $n->value . $this->parseTree($n->treeNodes[0]); 546 break; 547 548 case OP_INCREMENT: 549 case OP_DECREMENT: 550 if ($n->postfix) 551 $s = $this->parseTree($n->treeNodes[0]) . $n->value; 552 else 553 $s = $n->value . $this->parseTree($n->treeNodes[0]); 554 break; 555 556 case OP_DOT: 557 $s = $this->parseTree($n->treeNodes[0]) . '.' . $this->parseTree($n->treeNodes[1]); 558 break; 559 560 case JS_INDEX: 561 $s = $this->parseTree($n->treeNodes[0]); 562 // See if we can replace named index with a dot saving 3 bytes 563 if ( $n->treeNodes[0]->type == TOKEN_IDENTIFIER && 564 $n->treeNodes[1]->type == TOKEN_STRING && 565 $this->isValidIdentifier(substr($n->treeNodes[1]->value, 1, -1)) 566 ) 567 $s .= '.' . substr($n->treeNodes[1]->value, 1, -1); 568 else 569 $s .= '[' . $this->parseTree($n->treeNodes[1]) . ']'; 570 break; 571 572 case JS_LIST: 573 $childs = $n->treeNodes; 574 for ($i = 0, $j = count($childs); $i < $j; $i++) 575 $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]); 576 break; 577 578 case JS_CALL: 579 $s = $this->parseTree($n->treeNodes[0]) . '(' . $this->parseTree($n->treeNodes[1]) . ')'; 580 break; 581 582 case KEYWORD_NEW: 583 case JS_NEW_WITH_ARGS: 584 $s = 'new ' . $this->parseTree($n->treeNodes[0]) . '(' . ($n->type == JS_NEW_WITH_ARGS ? $this->parseTree($n->treeNodes[1]) : '') . ')'; 585 break; 586 587 case JS_ARRAY_INIT: 588 $s = '['; 589 $childs = $n->treeNodes; 590 for ($i = 0, $j = count($childs); $i < $j; $i++) 591 { 592 $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]); 593 } 594 $s .= ']'; 595 break; 596 597 case JS_OBJECT_INIT: 598 $s = '{'; 599 $childs = $n->treeNodes; 600 for ($i = 0, $j = count($childs); $i < $j; $i++) 601 { 602 $t = $childs[$i]; 603 if ($i) 604 $s .= ','; 605 if ($t->type == JS_PROPERTY_INIT) 606 { 607 // Ditch the quotes when the index is a valid identifier 608 if ( $t->treeNodes[0]->type == TOKEN_STRING && 609 $this->isValidIdentifier(substr($t->treeNodes[0]->value, 1, -1)) 610 ) 611 $s .= substr($t->treeNodes[0]->value, 1, -1); 612 else 613 $s .= $t->treeNodes[0]->value; 614 615 $s .= ':' . $this->parseTree($t->treeNodes[1]); 616 } 617 else 618 { 619 $s .= $t->type == JS_GETTER ? 'get' : 'set'; 620 $s .= ' ' . $t->name . '('; 621 $params = $t->params; 622 for ($i = 0, $j = count($params); $i < $j; $i++) 623 $s .= ($i ? ',' : '') . $params[$i]; 624 $s .= '){' . $this->parseTree($t->body, true) . '}'; 625 } 626 } 627 $s .= '}'; 628 break; 629 630 case TOKEN_NUMBER: 631 $s = $n->value; 632 if (preg_match('/^([1-9]+)(0{3,})$/', $s, $m)) 633 $s = $m[1] . 'e' . strlen($m[2]); 634 break; 635 636 case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE: 637 case TOKEN_IDENTIFIER: case TOKEN_STRING: case TOKEN_REGEXP: 638 $s = $n->value; 639 break; 640 641 case JS_GROUP: 642 if (in_array( 643 $n->treeNodes[0]->type, 644 array( 645 JS_ARRAY_INIT, JS_OBJECT_INIT, JS_GROUP, 646 TOKEN_NUMBER, TOKEN_STRING, TOKEN_REGEXP, TOKEN_IDENTIFIER, 647 KEYWORD_NULL, KEYWORD_THIS, KEYWORD_TRUE, KEYWORD_FALSE 648 ) 649 )) 650 { 651 $s = $this->parseTree($n->treeNodes[0]); 652 } 653 else 654 { 655 $s = '(' . $this->parseTree($n->treeNodes[0]) . ')'; 656 } 657 break; 658 659 default: 660 throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type); 661 } 662 663 return $s; 664 } 665 666 private function isValidIdentifier($string) 667 { 668 return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved); 669 } 670 671 private function isWordChar($char) 672 { 673 return $char == '_' || $char == '$' || ctype_alnum($char); 674 } 675} 676 677class JSParser 678{ 679 private $t; 680 private $minifier; 681 682 private $opPrecedence = array( 683 ';' => 0, 684 ',' => 1, 685 '=' => 2, '?' => 2, ':' => 2, 686 // The above all have to have the same precedence, see bug 330975 687 '||' => 4, 688 '&&' => 5, 689 '|' => 6, 690 '^' => 7, 691 '&' => 8, 692 '==' => 9, '!=' => 9, '===' => 9, '!==' => 9, 693 '<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10, 694 '<<' => 11, '>>' => 11, '>>>' => 11, 695 '+' => 12, '-' => 12, 696 '*' => 13, '/' => 13, '%' => 13, 697 'delete' => 14, 'void' => 14, 'typeof' => 14, 698 '!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14, 699 '++' => 15, '--' => 15, 700 'new' => 16, 701 '.' => 17, 702 JS_NEW_WITH_ARGS => 0, JS_INDEX => 0, JS_CALL => 0, 703 JS_ARRAY_INIT => 0, JS_OBJECT_INIT => 0, JS_GROUP => 0 704 ); 705 706 private $opArity = array( 707 ',' => -2, 708 '=' => 2, 709 '?' => 3, 710 '||' => 2, 711 '&&' => 2, 712 '|' => 2, 713 '^' => 2, 714 '&' => 2, 715 '==' => 2, '!=' => 2, '===' => 2, '!==' => 2, 716 '<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2, 717 '<<' => 2, '>>' => 2, '>>>' => 2, 718 '+' => 2, '-' => 2, 719 '*' => 2, '/' => 2, '%' => 2, 720 'delete' => 1, 'void' => 1, 'typeof' => 1, 721 '!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1, 722 '++' => 1, '--' => 1, 723 'new' => 1, 724 '.' => 2, 725 JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2, 726 JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1, 727 TOKEN_CONDCOMMENT_START => 1, TOKEN_CONDCOMMENT_END => 1 728 ); 729 730 public function __construct($minifier=null) 731 { 732 $this->minifier = $minifier; 733 $this->t = new JSTokenizer(); 734 } 735 736 public function parse($s, $f, $l) 737 { 738 // initialize tokenizer 739 $this->t->init($s, $f, $l); 740 741 $x = new JSCompilerContext(false); 742 $n = $this->Script($x); 743 if (!$this->t->isDone()) 744 throw $this->t->newSyntaxError('Syntax error'); 745 746 return $n; 747 } 748 749 private function Script($x) 750 { 751 $n = $this->Statements($x); 752 $n->type = JS_SCRIPT; 753 $n->funDecls = $x->funDecls; 754 $n->varDecls = $x->varDecls; 755 756 // minify by scope 757 if ($this->minifier) 758 { 759 $n->value = $this->minifier->parseTree($n); 760 761 // clear tree from node to save memory 762 $n->treeNodes = null; 763 $n->funDecls = null; 764 $n->varDecls = null; 765 766 $n->type = JS_MINIFIED; 767 } 768 769 return $n; 770 } 771 772 private function Statements($x) 773 { 774 $n = new JSNode($this->t, JS_BLOCK); 775 array_push($x->stmtStack, $n); 776 777 while (!$this->t->isDone() && $this->t->peek() != OP_RIGHT_CURLY) 778 $n->addNode($this->Statement($x)); 779 780 array_pop($x->stmtStack); 781 782 return $n; 783 } 784 785 private function Block($x) 786 { 787 $this->t->mustMatch(OP_LEFT_CURLY); 788 $n = $this->Statements($x); 789 $this->t->mustMatch(OP_RIGHT_CURLY); 790 791 return $n; 792 } 793 794 private function Statement($x) 795 { 796 $tt = $this->t->get(); 797 $n2 = null; 798 799 // Cases for statements ending in a right curly return early, avoiding the 800 // common semicolon insertion magic after this switch. 801 switch ($tt) 802 { 803 case KEYWORD_FUNCTION: 804 return $this->FunctionDefinition( 805 $x, 806 true, 807 count($x->stmtStack) > 1 ? STATEMENT_FORM : DECLARED_FORM 808 ); 809 break; 810 811 case OP_LEFT_CURLY: 812 $n = $this->Statements($x); 813 $this->t->mustMatch(OP_RIGHT_CURLY); 814 return $n; 815 816 case KEYWORD_IF: 817 $n = new JSNode($this->t); 818 $n->condition = $this->ParenExpression($x); 819 array_push($x->stmtStack, $n); 820 $n->thenPart = $this->Statement($x); 821 $n->elsePart = $this->t->match(KEYWORD_ELSE) ? $this->Statement($x) : null; 822 array_pop($x->stmtStack); 823 return $n; 824 825 case KEYWORD_SWITCH: 826 $n = new JSNode($this->t); 827 $this->t->mustMatch(OP_LEFT_PAREN); 828 $n->discriminant = $this->Expression($x); 829 $this->t->mustMatch(OP_RIGHT_PAREN); 830 $n->cases = array(); 831 $n->defaultIndex = -1; 832 833 array_push($x->stmtStack, $n); 834 835 $this->t->mustMatch(OP_LEFT_CURLY); 836 837 while (($tt = $this->t->get()) != OP_RIGHT_CURLY) 838 { 839 switch ($tt) 840 { 841 case KEYWORD_DEFAULT: 842 if ($n->defaultIndex >= 0) 843 throw $this->t->newSyntaxError('More than one switch default'); 844 // FALL THROUGH 845 case KEYWORD_CASE: 846 $n2 = new JSNode($this->t); 847 if ($tt == KEYWORD_DEFAULT) 848 $n->defaultIndex = count($n->cases); 849 else 850 $n2->caseLabel = $this->Expression($x, OP_COLON); 851 break; 852 default: 853 throw $this->t->newSyntaxError('Invalid switch case'); 854 } 855 856 $this->t->mustMatch(OP_COLON); 857 $n2->statements = new JSNode($this->t, JS_BLOCK); 858 while (($tt = $this->t->peek()) != KEYWORD_CASE && $tt != KEYWORD_DEFAULT && $tt != OP_RIGHT_CURLY) 859 $n2->statements->addNode($this->Statement($x)); 860 861 array_push($n->cases, $n2); 862 } 863 864 array_pop($x->stmtStack); 865 return $n; 866 867 case KEYWORD_FOR: 868 $n = new JSNode($this->t); 869 $n->isLoop = true; 870 $this->t->mustMatch(OP_LEFT_PAREN); 871 872 if (($tt = $this->t->peek()) != OP_SEMICOLON) 873 { 874 $x->inForLoopInit = true; 875 if ($tt == KEYWORD_VAR || $tt == KEYWORD_CONST) 876 { 877 $this->t->get(); 878 $n2 = $this->Variables($x); 879 } 880 else 881 { 882 $n2 = $this->Expression($x); 883 } 884 $x->inForLoopInit = false; 885 } 886 887 if ($n2 && $this->t->match(KEYWORD_IN)) 888 { 889 $n->type = JS_FOR_IN; 890 if ($n2->type == KEYWORD_VAR) 891 { 892 if (count($n2->treeNodes) != 1) 893 { 894 throw $this->t->SyntaxError( 895 'Invalid for..in left-hand side', 896 $this->t->filename, 897 $n2->lineno 898 ); 899 } 900 901 // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name. 902 $n->iterator = $n2->treeNodes[0]; 903 $n->varDecl = $n2; 904 } 905 else 906 { 907 $n->iterator = $n2; 908 $n->varDecl = null; 909 } 910 911 $n->object = $this->Expression($x); 912 } 913 else 914 { 915 $n->setup = $n2 ? $n2 : null; 916 $this->t->mustMatch(OP_SEMICOLON); 917 $n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x); 918 $this->t->mustMatch(OP_SEMICOLON); 919 $n->update = $this->t->peek() == OP_RIGHT_PAREN ? null : $this->Expression($x); 920 } 921 922 $this->t->mustMatch(OP_RIGHT_PAREN); 923 $n->body = $this->nest($x, $n); 924 return $n; 925 926 case KEYWORD_WHILE: 927 $n = new JSNode($this->t); 928 $n->isLoop = true; 929 $n->condition = $this->ParenExpression($x); 930 $n->body = $this->nest($x, $n); 931 return $n; 932 933 case KEYWORD_DO: 934 $n = new JSNode($this->t); 935 $n->isLoop = true; 936 $n->body = $this->nest($x, $n, KEYWORD_WHILE); 937 $n->condition = $this->ParenExpression($x); 938 if (!$x->ecmaStrictMode) 939 { 940 // <script language="JavaScript"> (without version hints) may need 941 // automatic semicolon insertion without a newline after do-while. 942 // See http://bugzilla.mozilla.org/show_bug.cgi?id=238945. 943 $this->t->match(OP_SEMICOLON); 944 return $n; 945 } 946 break; 947 948 case KEYWORD_BREAK: 949 case KEYWORD_CONTINUE: 950 $n = new JSNode($this->t); 951 952 if ($this->t->peekOnSameLine() == TOKEN_IDENTIFIER) 953 { 954 $this->t->get(); 955 $n->label = $this->t->currentToken()->value; 956 } 957 958 $ss = $x->stmtStack; 959 $i = count($ss); 960 $label = $n->label; 961 if ($label) 962 { 963 do 964 { 965 if (--$i < 0) 966 throw $this->t->newSyntaxError('Label not found'); 967 } 968 while ($ss[$i]->label != $label); 969 } 970 else 971 { 972 do 973 { 974 if (--$i < 0) 975 throw $this->t->newSyntaxError('Invalid ' . $tt); 976 } 977 while (!$ss[$i]->isLoop && ($tt != KEYWORD_BREAK || $ss[$i]->type != KEYWORD_SWITCH)); 978 } 979 980 $n->target = $ss[$i]; 981 break; 982 983 case KEYWORD_TRY: 984 $n = new JSNode($this->t); 985 $n->tryBlock = $this->Block($x); 986 $n->catchClauses = array(); 987 988 while ($this->t->match(KEYWORD_CATCH)) 989 { 990 $n2 = new JSNode($this->t); 991 $this->t->mustMatch(OP_LEFT_PAREN); 992 $n2->varName = $this->t->mustMatch(TOKEN_IDENTIFIER)->value; 993 994 if ($this->t->match(KEYWORD_IF)) 995 { 996 if ($x->ecmaStrictMode) 997 throw $this->t->newSyntaxError('Illegal catch guard'); 998 999 if (count($n->catchClauses) && !end($n->catchClauses)->guard) 1000 throw $this->t->newSyntaxError('Guarded catch after unguarded'); 1001 1002 $n2->guard = $this->Expression($x); 1003 } 1004 else 1005 { 1006 $n2->guard = null; 1007 } 1008 1009 $this->t->mustMatch(OP_RIGHT_PAREN); 1010 $n2->block = $this->Block($x); 1011 array_push($n->catchClauses, $n2); 1012 } 1013 1014 if ($this->t->match(KEYWORD_FINALLY)) 1015 $n->finallyBlock = $this->Block($x); 1016 1017 if (!count($n->catchClauses) && !$n->finallyBlock) 1018 throw $this->t->newSyntaxError('Invalid try statement'); 1019 return $n; 1020 1021 case KEYWORD_CATCH: 1022 case KEYWORD_FINALLY: 1023 throw $this->t->newSyntaxError($tt + ' without preceding try'); 1024 1025 case KEYWORD_THROW: 1026 $n = new JSNode($this->t); 1027 $n->value = $this->Expression($x); 1028 break; 1029 1030 case KEYWORD_RETURN: 1031 if (!$x->inFunction) 1032 throw $this->t->newSyntaxError('Invalid return'); 1033 1034 $n = new JSNode($this->t); 1035 $tt = $this->t->peekOnSameLine(); 1036 if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY) 1037 $n->value = $this->Expression($x); 1038 else 1039 $n->value = null; 1040 break; 1041 1042 case KEYWORD_WITH: 1043 $n = new JSNode($this->t); 1044 $n->object = $this->ParenExpression($x); 1045 $n->body = $this->nest($x, $n); 1046 return $n; 1047 1048 case KEYWORD_VAR: 1049 case KEYWORD_CONST: 1050 $n = $this->Variables($x); 1051 break; 1052 1053 case TOKEN_CONDCOMMENT_START: 1054 case TOKEN_CONDCOMMENT_END: 1055 $n = new JSNode($this->t); 1056 return $n; 1057 1058 case KEYWORD_DEBUGGER: 1059 $n = new JSNode($this->t); 1060 break; 1061 1062 case TOKEN_NEWLINE: 1063 case OP_SEMICOLON: 1064 $n = new JSNode($this->t, OP_SEMICOLON); 1065 $n->expression = null; 1066 return $n; 1067 1068 default: 1069 if ($tt == TOKEN_IDENTIFIER) 1070 { 1071 $this->t->scanOperand = false; 1072 $tt = $this->t->peek(); 1073 $this->t->scanOperand = true; 1074 if ($tt == OP_COLON) 1075 { 1076 $label = $this->t->currentToken()->value; 1077 $ss = $x->stmtStack; 1078 for ($i = count($ss) - 1; $i >= 0; --$i) 1079 { 1080 if ($ss[$i]->label == $label) 1081 throw $this->t->newSyntaxError('Duplicate label'); 1082 } 1083 1084 $this->t->get(); 1085 $n = new JSNode($this->t, JS_LABEL); 1086 $n->label = $label; 1087 $n->statement = $this->nest($x, $n); 1088 1089 return $n; 1090 } 1091 } 1092 1093 $n = new JSNode($this->t, OP_SEMICOLON); 1094 $this->t->unget(); 1095 $n->expression = $this->Expression($x); 1096 $n->end = $n->expression->end; 1097 break; 1098 } 1099 1100 if ($this->t->lineno == $this->t->currentToken()->lineno) 1101 { 1102 $tt = $this->t->peekOnSameLine(); 1103 if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY) 1104 throw $this->t->newSyntaxError('Missing ; before statement'); 1105 } 1106 1107 $this->t->match(OP_SEMICOLON); 1108 1109 return $n; 1110 } 1111 1112 private function FunctionDefinition($x, $requireName, $functionForm) 1113 { 1114 $f = new JSNode($this->t); 1115 1116 if ($f->type != KEYWORD_FUNCTION) 1117 $f->type = ($f->value == 'get') ? JS_GETTER : JS_SETTER; 1118 1119 if ($this->t->match(TOKEN_IDENTIFIER)) 1120 $f->name = $this->t->currentToken()->value; 1121 elseif ($requireName) 1122 throw $this->t->newSyntaxError('Missing function identifier'); 1123 1124 $this->t->mustMatch(OP_LEFT_PAREN); 1125 $f->params = array(); 1126 1127 while (($tt = $this->t->get()) != OP_RIGHT_PAREN) 1128 { 1129 if ($tt != TOKEN_IDENTIFIER) 1130 throw $this->t->newSyntaxError('Missing formal parameter'); 1131 1132 array_push($f->params, $this->t->currentToken()->value); 1133 1134 if ($this->t->peek() != OP_RIGHT_PAREN) 1135 $this->t->mustMatch(OP_COMMA); 1136 } 1137 1138 $this->t->mustMatch(OP_LEFT_CURLY); 1139 1140 $x2 = new JSCompilerContext(true); 1141 $f->body = $this->Script($x2); 1142 1143 $this->t->mustMatch(OP_RIGHT_CURLY); 1144 $f->end = $this->t->currentToken()->end; 1145 1146 $f->functionForm = $functionForm; 1147 if ($functionForm == DECLARED_FORM) 1148 array_push($x->funDecls, $f); 1149 1150 return $f; 1151 } 1152 1153 private function Variables($x) 1154 { 1155 $n = new JSNode($this->t); 1156 1157 do 1158 { 1159 $this->t->mustMatch(TOKEN_IDENTIFIER); 1160 1161 $n2 = new JSNode($this->t); 1162 $n2->name = $n2->value; 1163 1164 if ($this->t->match(OP_ASSIGN)) 1165 { 1166 if ($this->t->currentToken()->assignOp) 1167 throw $this->t->newSyntaxError('Invalid variable initialization'); 1168 1169 $n2->initializer = $this->Expression($x, OP_COMMA); 1170 } 1171 1172 $n2->readOnly = $n->type == KEYWORD_CONST; 1173 1174 $n->addNode($n2); 1175 array_push($x->varDecls, $n2); 1176 } 1177 while ($this->t->match(OP_COMMA)); 1178 1179 return $n; 1180 } 1181 1182 private function Expression($x, $stop=false) 1183 { 1184 $operators = array(); 1185 $operands = array(); 1186 $n = false; 1187 1188 $bl = $x->bracketLevel; 1189 $cl = $x->curlyLevel; 1190 $pl = $x->parenLevel; 1191 $hl = $x->hookLevel; 1192 1193 while (($tt = $this->t->get()) != TOKEN_END) 1194 { 1195 if ($tt == $stop && 1196 $x->bracketLevel == $bl && 1197 $x->curlyLevel == $cl && 1198 $x->parenLevel == $pl && 1199 $x->hookLevel == $hl 1200 ) 1201 { 1202 // Stop only if tt matches the optional stop parameter, and that 1203 // token is not quoted by some kind of bracket. 1204 break; 1205 } 1206 1207 switch ($tt) 1208 { 1209 case OP_SEMICOLON: 1210 // NB: cannot be empty, Statement handled that. 1211 break 2; 1212 1213 case OP_HOOK: 1214 if ($this->t->scanOperand) 1215 break 2; 1216 1217 while ( !empty($operators) && 1218 $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] 1219 ) 1220 $this->reduce($operators, $operands); 1221 1222 array_push($operators, new JSNode($this->t)); 1223 1224 ++$x->hookLevel; 1225 $this->t->scanOperand = true; 1226 $n = $this->Expression($x); 1227 1228 if (!$this->t->match(OP_COLON)) 1229 break 2; 1230 1231 --$x->hookLevel; 1232 array_push($operands, $n); 1233 break; 1234 1235 case OP_COLON: 1236 if ($x->hookLevel) 1237 break 2; 1238 1239 throw $this->t->newSyntaxError('Invalid label'); 1240 break; 1241 1242 case OP_ASSIGN: 1243 if ($this->t->scanOperand) 1244 break 2; 1245 1246 // Use >, not >=, for right-associative ASSIGN 1247 while ( !empty($operators) && 1248 $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] 1249 ) 1250 $this->reduce($operators, $operands); 1251 1252 array_push($operators, new JSNode($this->t)); 1253 end($operands)->assignOp = $this->t->currentToken()->assignOp; 1254 $this->t->scanOperand = true; 1255 break; 1256 1257 case KEYWORD_IN: 1258 // An in operator should not be parsed if we're parsing the head of 1259 // a for (...) loop, unless it is in the then part of a conditional 1260 // expression, or parenthesized somehow. 1261 if ($x->inForLoopInit && !$x->hookLevel && 1262 !$x->bracketLevel && !$x->curlyLevel && 1263 !$x->parenLevel 1264 ) 1265 break 2; 1266 // FALL THROUGH 1267 case OP_COMMA: 1268 // A comma operator should not be parsed if we're parsing the then part 1269 // of a conditional expression unless it's parenthesized somehow. 1270 if ($tt == OP_COMMA && $x->hookLevel && 1271 !$x->bracketLevel && !$x->curlyLevel && 1272 !$x->parenLevel 1273 ) 1274 break 2; 1275 // Treat comma as left-associative so reduce can fold left-heavy 1276 // COMMA trees into a single array. 1277 // FALL THROUGH 1278 case OP_OR: 1279 case OP_AND: 1280 case OP_BITWISE_OR: 1281 case OP_BITWISE_XOR: 1282 case OP_BITWISE_AND: 1283 case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE: 1284 case OP_LT: case OP_LE: case OP_GE: case OP_GT: 1285 case KEYWORD_INSTANCEOF: 1286 case OP_LSH: case OP_RSH: case OP_URSH: 1287 case OP_PLUS: case OP_MINUS: 1288 case OP_MUL: case OP_DIV: case OP_MOD: 1289 case OP_DOT: 1290 if ($this->t->scanOperand) 1291 break 2; 1292 1293 while ( !empty($operators) && 1294 $this->opPrecedence[end($operators)->type] >= $this->opPrecedence[$tt] 1295 ) 1296 $this->reduce($operators, $operands); 1297 1298 if ($tt == OP_DOT) 1299 { 1300 $this->t->mustMatch(TOKEN_IDENTIFIER); 1301 array_push($operands, new JSNode($this->t, OP_DOT, array_pop($operands), new JSNode($this->t))); 1302 } 1303 else 1304 { 1305 array_push($operators, new JSNode($this->t)); 1306 $this->t->scanOperand = true; 1307 } 1308 break; 1309 1310 case KEYWORD_DELETE: case KEYWORD_VOID: case KEYWORD_TYPEOF: 1311 case OP_NOT: case OP_BITWISE_NOT: case OP_UNARY_PLUS: case OP_UNARY_MINUS: 1312 case KEYWORD_NEW: 1313 if (!$this->t->scanOperand) 1314 break 2; 1315 1316 array_push($operators, new JSNode($this->t)); 1317 break; 1318 1319 case OP_INCREMENT: case OP_DECREMENT: 1320 if ($this->t->scanOperand) 1321 { 1322 array_push($operators, new JSNode($this->t)); // prefix increment or decrement 1323 } 1324 else 1325 { 1326 // Don't cross a line boundary for postfix {in,de}crement. 1327 $t = $this->t->tokens[($this->t->tokenIndex + $this->t->lookahead - 1) & 3]; 1328 if ($t && $t->lineno != $this->t->lineno) 1329 break 2; 1330 1331 if (!empty($operators)) 1332 { 1333 // Use >, not >=, so postfix has higher precedence than prefix. 1334 while ($this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]) 1335 $this->reduce($operators, $operands); 1336 } 1337 1338 $n = new JSNode($this->t, $tt, array_pop($operands)); 1339 $n->postfix = true; 1340 array_push($operands, $n); 1341 } 1342 break; 1343 1344 case KEYWORD_FUNCTION: 1345 if (!$this->t->scanOperand) 1346 break 2; 1347 1348 array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM)); 1349 $this->t->scanOperand = false; 1350 break; 1351 1352 case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE: 1353 case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP: 1354 if (!$this->t->scanOperand) 1355 break 2; 1356 1357 array_push($operands, new JSNode($this->t)); 1358 $this->t->scanOperand = false; 1359 break; 1360 1361 case TOKEN_CONDCOMMENT_START: 1362 case TOKEN_CONDCOMMENT_END: 1363 if ($this->t->scanOperand) 1364 array_push($operators, new JSNode($this->t)); 1365 else 1366 array_push($operands, new JSNode($this->t)); 1367 break; 1368 1369 case OP_LEFT_BRACKET: 1370 if ($this->t->scanOperand) 1371 { 1372 // Array initialiser. Parse using recursive descent, as the 1373 // sub-grammar here is not an operator grammar. 1374 $n = new JSNode($this->t, JS_ARRAY_INIT); 1375 while (($tt = $this->t->peek()) != OP_RIGHT_BRACKET) 1376 { 1377 if ($tt == OP_COMMA) 1378 { 1379 $this->t->get(); 1380 $n->addNode(null); 1381 continue; 1382 } 1383 1384 $n->addNode($this->Expression($x, OP_COMMA)); 1385 if (!$this->t->match(OP_COMMA)) 1386 break; 1387 } 1388 1389 $this->t->mustMatch(OP_RIGHT_BRACKET); 1390 array_push($operands, $n); 1391 $this->t->scanOperand = false; 1392 } 1393 else 1394 { 1395 // Property indexing operator. 1396 array_push($operators, new JSNode($this->t, JS_INDEX)); 1397 $this->t->scanOperand = true; 1398 ++$x->bracketLevel; 1399 } 1400 break; 1401 1402 case OP_RIGHT_BRACKET: 1403 if ($this->t->scanOperand || $x->bracketLevel == $bl) 1404 break 2; 1405 1406 while ($this->reduce($operators, $operands)->type != JS_INDEX) 1407 continue; 1408 1409 --$x->bracketLevel; 1410 break; 1411 1412 case OP_LEFT_CURLY: 1413 if (!$this->t->scanOperand) 1414 break 2; 1415 1416 // Object initialiser. As for array initialisers (see above), 1417 // parse using recursive descent. 1418 ++$x->curlyLevel; 1419 $n = new JSNode($this->t, JS_OBJECT_INIT); 1420 while (!$this->t->match(OP_RIGHT_CURLY)) 1421 { 1422 do 1423 { 1424 $tt = $this->t->get(); 1425 $tv = $this->t->currentToken()->value; 1426 if (($tv == 'get' || $tv == 'set') && $this->t->peek() == TOKEN_IDENTIFIER) 1427 { 1428 if ($x->ecmaStrictMode) 1429 throw $this->t->newSyntaxError('Illegal property accessor'); 1430 1431 $n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM)); 1432 } 1433 else 1434 { 1435 switch ($tt) 1436 { 1437 case TOKEN_IDENTIFIER: 1438 case TOKEN_NUMBER: 1439 case TOKEN_STRING: 1440 $id = new JSNode($this->t); 1441 break; 1442 1443 case OP_RIGHT_CURLY: 1444 if ($x->ecmaStrictMode) 1445 throw $this->t->newSyntaxError('Illegal trailing ,'); 1446 break 3; 1447 1448 default: 1449 throw $this->t->newSyntaxError('Invalid property name'); 1450 } 1451 1452 $this->t->mustMatch(OP_COLON); 1453 $n->addNode(new JSNode($this->t, JS_PROPERTY_INIT, $id, $this->Expression($x, OP_COMMA))); 1454 } 1455 } 1456 while ($this->t->match(OP_COMMA)); 1457 1458 $this->t->mustMatch(OP_RIGHT_CURLY); 1459 break; 1460 } 1461 1462 array_push($operands, $n); 1463 $this->t->scanOperand = false; 1464 --$x->curlyLevel; 1465 break; 1466 1467 case OP_RIGHT_CURLY: 1468 if (!$this->t->scanOperand && $x->curlyLevel != $cl) 1469 throw new Exception('PANIC: right curly botch'); 1470 break 2; 1471 1472 case OP_LEFT_PAREN: 1473 if ($this->t->scanOperand) 1474 { 1475 array_push($operators, new JSNode($this->t, JS_GROUP)); 1476 } 1477 else 1478 { 1479 while ( !empty($operators) && 1480 $this->opPrecedence[end($operators)->type] > $this->opPrecedence[KEYWORD_NEW] 1481 ) 1482 $this->reduce($operators, $operands); 1483 1484 // Handle () now, to regularize the n-ary case for n > 0. 1485 // We must set scanOperand in case there are arguments and 1486 // the first one is a regexp or unary+/-. 1487 $n = end($operators); 1488 $this->t->scanOperand = true; 1489 if ($this->t->match(OP_RIGHT_PAREN)) 1490 { 1491 if ($n && $n->type == KEYWORD_NEW) 1492 { 1493 array_pop($operators); 1494 $n->addNode(array_pop($operands)); 1495 } 1496 else 1497 { 1498 $n = new JSNode($this->t, JS_CALL, array_pop($operands), new JSNode($this->t, JS_LIST)); 1499 } 1500 1501 array_push($operands, $n); 1502 $this->t->scanOperand = false; 1503 break; 1504 } 1505 1506 if ($n && $n->type == KEYWORD_NEW) 1507 $n->type = JS_NEW_WITH_ARGS; 1508 else 1509 array_push($operators, new JSNode($this->t, JS_CALL)); 1510 } 1511 1512 ++$x->parenLevel; 1513 break; 1514 1515 case OP_RIGHT_PAREN: 1516 if ($this->t->scanOperand || $x->parenLevel == $pl) 1517 break 2; 1518 1519 while (($tt = $this->reduce($operators, $operands)->type) != JS_GROUP && 1520 $tt != JS_CALL && $tt != JS_NEW_WITH_ARGS 1521 ) 1522 { 1523 continue; 1524 } 1525 1526 if ($tt != JS_GROUP) 1527 { 1528 $n = end($operands); 1529 if ($n->treeNodes[1]->type != OP_COMMA) 1530 $n->treeNodes[1] = new JSNode($this->t, JS_LIST, $n->treeNodes[1]); 1531 else 1532 $n->treeNodes[1]->type = JS_LIST; 1533 } 1534 1535 --$x->parenLevel; 1536 break; 1537 1538 // Automatic semicolon insertion means we may scan across a newline 1539 // and into the beginning of another statement. If so, break out of 1540 // the while loop and let the t.scanOperand logic handle errors. 1541 default: 1542 break 2; 1543 } 1544 } 1545 1546 if ($x->hookLevel != $hl) 1547 throw $this->t->newSyntaxError('Missing : in conditional expression'); 1548 1549 if ($x->parenLevel != $pl) 1550 throw $this->t->newSyntaxError('Missing ) in parenthetical'); 1551 1552 if ($x->bracketLevel != $bl) 1553 throw $this->t->newSyntaxError('Missing ] in index expression'); 1554 1555 if ($this->t->scanOperand) 1556 throw $this->t->newSyntaxError('Missing operand'); 1557 1558 // Resume default mode, scanning for operands, not operators. 1559 $this->t->scanOperand = true; 1560 $this->t->unget(); 1561 1562 while (count($operators)) 1563 $this->reduce($operators, $operands); 1564 1565 return array_pop($operands); 1566 } 1567 1568 private function ParenExpression($x) 1569 { 1570 $this->t->mustMatch(OP_LEFT_PAREN); 1571 $n = $this->Expression($x); 1572 $this->t->mustMatch(OP_RIGHT_PAREN); 1573 1574 return $n; 1575 } 1576 1577 // Statement stack and nested statement handler. 1578 private function nest($x, $node, $end = false) 1579 { 1580 array_push($x->stmtStack, $node); 1581 $n = $this->statement($x); 1582 array_pop($x->stmtStack); 1583 1584 if ($end) 1585 $this->t->mustMatch($end); 1586 1587 return $n; 1588 } 1589 1590 private function reduce(&$operators, &$operands) 1591 { 1592 $n = array_pop($operators); 1593 $op = $n->type; 1594 $arity = $this->opArity[$op]; 1595 $c = count($operands); 1596 if ($arity == -2) 1597 { 1598 // Flatten left-associative trees 1599 if ($c >= 2) 1600 { 1601 $left = $operands[$c - 2]; 1602 if ($left->type == $op) 1603 { 1604 $right = array_pop($operands); 1605 $left->addNode($right); 1606 return $left; 1607 } 1608 } 1609 $arity = 2; 1610 } 1611 1612 // Always use push to add operands to n, to update start and end 1613 $a = array_splice($operands, $c - $arity); 1614 for ($i = 0; $i < $arity; $i++) 1615 $n->addNode($a[$i]); 1616 1617 // Include closing bracket or postfix operator in [start,end] 1618 $te = $this->t->currentToken()->end; 1619 if ($n->end < $te) 1620 $n->end = $te; 1621 1622 array_push($operands, $n); 1623 1624 return $n; 1625 } 1626} 1627 1628class JSCompilerContext 1629{ 1630 public $inFunction = false; 1631 public $inForLoopInit = false; 1632 public $ecmaStrictMode = false; 1633 public $bracketLevel = 0; 1634 public $curlyLevel = 0; 1635 public $parenLevel = 0; 1636 public $hookLevel = 0; 1637 1638 public $stmtStack = array(); 1639 public $funDecls = array(); 1640 public $varDecls = array(); 1641 1642 public function __construct($inFunction) 1643 { 1644 $this->inFunction = $inFunction; 1645 } 1646} 1647 1648class JSNode 1649{ 1650 private $type; 1651 private $value; 1652 private $lineno; 1653 private $start; 1654 private $end; 1655 1656 public $treeNodes = array(); 1657 public $funDecls = array(); 1658 public $varDecls = array(); 1659 1660 public function __construct($t, $type=0) 1661 { 1662 if ($token = $t->currentToken()) 1663 { 1664 $this->type = $type ? $type : $token->type; 1665 $this->value = $token->value; 1666 $this->lineno = $token->lineno; 1667 $this->start = $token->start; 1668 $this->end = $token->end; 1669 } 1670 else 1671 { 1672 $this->type = $type; 1673 $this->lineno = $t->lineno; 1674 } 1675 1676 if (($numargs = func_num_args()) > 2) 1677 { 1678 $args = func_get_args(); 1679 for ($i = 2; $i < $numargs; $i++) 1680 $this->addNode($args[$i]); 1681 } 1682 } 1683 1684 // we don't want to bloat our object with all kind of specific properties, so we use overloading 1685 public function __set($name, $value) 1686 { 1687 $this->$name = $value; 1688 } 1689 1690 public function __get($name) 1691 { 1692 if (isset($this->$name)) 1693 return $this->$name; 1694 1695 return null; 1696 } 1697 1698 public function addNode($node) 1699 { 1700 if ($node !== null) 1701 { 1702 if ($node->start < $this->start) 1703 $this->start = $node->start; 1704 if ($this->end < $node->end) 1705 $this->end = $node->end; 1706 } 1707 1708 $this->treeNodes[] = $node; 1709 } 1710} 1711 1712class JSTokenizer 1713{ 1714 private $cursor = 0; 1715 private $source; 1716 1717 public $tokens = array(); 1718 public $tokenIndex = 0; 1719 public $lookahead = 0; 1720 public $scanNewlines = false; 1721 public $scanOperand = true; 1722 1723 public $filename; 1724 public $lineno; 1725 1726 private $keywords = array( 1727 'break', 1728 'case', 'catch', 'const', 'continue', 1729 'debugger', 'default', 'delete', 'do', 1730 'else', 'enum', 1731 'false', 'finally', 'for', 'function', 1732 'if', 'in', 'instanceof', 1733 'new', 'null', 1734 'return', 1735 'switch', 1736 'this', 'throw', 'true', 'try', 'typeof', 1737 'var', 'void', 1738 'while', 'with' 1739 ); 1740 1741 private $opTypeNames = array( 1742 ';', ',', '?', ':', '||', '&&', '|', '^', 1743 '&', '===', '==', '=', '!==', '!=', '<<', '<=', 1744 '<', '>>>', '>>', '>=', '>', '++', '--', '+', 1745 '-', '*', '/', '%', '!', '~', '.', '[', 1746 ']', '{', '}', '(', ')', '@*/' 1747 ); 1748 1749 private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%'); 1750 private $opRegExp; 1751 1752 public function __construct() 1753 { 1754 $this->opRegExp = '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames)) . ')#'; 1755 } 1756 1757 public function init($source, $filename = '', $lineno = 1) 1758 { 1759 $this->source = $source; 1760 $this->filename = $filename ? $filename : '[inline]'; 1761 $this->lineno = $lineno; 1762 1763 $this->cursor = 0; 1764 $this->tokens = array(); 1765 $this->tokenIndex = 0; 1766 $this->lookahead = 0; 1767 $this->scanNewlines = false; 1768 $this->scanOperand = true; 1769 } 1770 1771 public function getInput($chunksize) 1772 { 1773 if ($chunksize) 1774 return substr($this->source, $this->cursor, $chunksize); 1775 1776 return substr($this->source, $this->cursor); 1777 } 1778 1779 public function isDone() 1780 { 1781 return $this->peek() == TOKEN_END; 1782 } 1783 1784 public function match($tt) 1785 { 1786 return $this->get() == $tt || $this->unget(); 1787 } 1788 1789 public function mustMatch($tt) 1790 { 1791 if (!$this->match($tt)) 1792 throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected'); 1793 1794 return $this->currentToken(); 1795 } 1796 1797 public function peek() 1798 { 1799 if ($this->lookahead) 1800 { 1801 $next = $this->tokens[($this->tokenIndex + $this->lookahead) & 3]; 1802 if ($this->scanNewlines && $next->lineno != $this->lineno) 1803 $tt = TOKEN_NEWLINE; 1804 else 1805 $tt = $next->type; 1806 } 1807 else 1808 { 1809 $tt = $this->get(); 1810 $this->unget(); 1811 } 1812 1813 return $tt; 1814 } 1815 1816 public function peekOnSameLine() 1817 { 1818 $this->scanNewlines = true; 1819 $tt = $this->peek(); 1820 $this->scanNewlines = false; 1821 1822 return $tt; 1823 } 1824 1825 public function currentToken() 1826 { 1827 if (!empty($this->tokens)) 1828 return $this->tokens[$this->tokenIndex]; 1829 } 1830 1831 public function get($chunksize = 1000) 1832 { 1833 while($this->lookahead) 1834 { 1835 $this->lookahead--; 1836 $this->tokenIndex = ($this->tokenIndex + 1) & 3; 1837 $token = $this->tokens[$this->tokenIndex]; 1838 if ($token->type != TOKEN_NEWLINE || $this->scanNewlines) 1839 return $token->type; 1840 } 1841 1842 $conditional_comment = false; 1843 1844 // strip whitespace and comments 1845 while(true) 1846 { 1847 $input = $this->getInput($chunksize); 1848 1849 // whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!) 1850 $re = $this->scanNewlines ? '/^[ \r\t]+/' : '/^\s+/'; 1851 if (preg_match($re, $input, $match)) 1852 { 1853 $spaces = $match[0]; 1854 $spacelen = strlen($spaces); 1855 $this->cursor += $spacelen; 1856 if (!$this->scanNewlines) 1857 $this->lineno += substr_count($spaces, "\n"); 1858 1859 if ($spacelen == $chunksize) 1860 continue; // complete chunk contained whitespace 1861 1862 $input = $this->getInput($chunksize); 1863 if ($input == '' || $input[0] != '/') 1864 break; 1865 } 1866 1867 // Comments 1868 if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match)) 1869 { 1870 if (!$chunksize) 1871 break; 1872 1873 // retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment) 1874 $chunksize = null; 1875 continue; 1876 } 1877 1878 // check if this is a conditional (JScript) comment 1879 if (!empty($match[1])) 1880 { 1881 $match[0] = '/*' . $match[1]; 1882 $conditional_comment = true; 1883 break; 1884 } 1885 else 1886 { 1887 $this->cursor += strlen($match[0]); 1888 $this->lineno += substr_count($match[0], "\n"); 1889 } 1890 } 1891 1892 if ($input == '') 1893 { 1894 $tt = TOKEN_END; 1895 $match = array(''); 1896 } 1897 elseif ($conditional_comment) 1898 { 1899 $tt = TOKEN_CONDCOMMENT_START; 1900 } 1901 else 1902 { 1903 switch ($input[0]) 1904 { 1905 case '0': 1906 // hexadecimal 1907 if (($input[1] == 'x' || $input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match)) 1908 { 1909 $tt = TOKEN_NUMBER; 1910 break; 1911 } 1912 // FALL THROUGH 1913 1914 case '1': case '2': case '3': case '4': case '5': 1915 case '6': case '7': case '8': case '9': 1916 // should always match 1917 preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match); 1918 $tt = TOKEN_NUMBER; 1919 break; 1920 1921 case "'": 1922 if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match)) 1923 { 1924 $tt = TOKEN_STRING; 1925 } 1926 else 1927 { 1928 if ($chunksize) 1929 return $this->get(null); // retry with a full chunk fetch 1930 1931 throw $this->newSyntaxError('Unterminated string literal'); 1932 } 1933 break; 1934 1935 case '"': 1936 if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match)) 1937 { 1938 $tt = TOKEN_STRING; 1939 } 1940 else 1941 { 1942 if ($chunksize) 1943 return $this->get(null); // retry with a full chunk fetch 1944 1945 throw $this->newSyntaxError('Unterminated string literal'); 1946 } 1947 break; 1948 1949 case '/': 1950 if ($this->scanOperand && preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match)) 1951 { 1952 $tt = TOKEN_REGEXP; 1953 break; 1954 } 1955 // FALL THROUGH 1956 1957 case '|': 1958 case '^': 1959 case '&': 1960 case '<': 1961 case '>': 1962 case '+': 1963 case '-': 1964 case '*': 1965 case '%': 1966 case '=': 1967 case '!': 1968 // should always match 1969 preg_match($this->opRegExp, $input, $match); 1970 $op = $match[0]; 1971 if (in_array($op, $this->assignOps) && $input[strlen($op)] == '=') 1972 { 1973 $tt = OP_ASSIGN; 1974 $match[0] .= '='; 1975 } 1976 else 1977 { 1978 $tt = $op; 1979 if ($this->scanOperand) 1980 { 1981 if ($op == OP_PLUS) 1982 $tt = OP_UNARY_PLUS; 1983 elseif ($op == OP_MINUS) 1984 $tt = OP_UNARY_MINUS; 1985 } 1986 $op = null; 1987 } 1988 break; 1989 1990 case '.': 1991 if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match)) 1992 { 1993 $tt = TOKEN_NUMBER; 1994 break; 1995 } 1996 // FALL THROUGH 1997 1998 case ';': 1999 case ',': 2000 case '?': 2001 case ':': 2002 case '~': 2003 case '[': 2004 case ']': 2005 case '{': 2006 case '}': 2007 case '(': 2008 case ')': 2009 // these are all single 2010 $match = array($input[0]); 2011 $tt = $input[0]; 2012 break; 2013 2014 case '@': 2015 // check end of conditional comment 2016 if (substr($input, 0, 3) == '@*/') 2017 { 2018 $match = array('@*/'); 2019 $tt = TOKEN_CONDCOMMENT_END; 2020 } 2021 else 2022 throw $this->newSyntaxError('Illegal token'); 2023 break; 2024 2025 case "\n": 2026 if ($this->scanNewlines) 2027 { 2028 $match = array("\n"); 2029 $tt = TOKEN_NEWLINE; 2030 } 2031 else 2032 throw $this->newSyntaxError('Illegal token'); 2033 break; 2034 2035 default: 2036 // FIXME: add support for unicode and unicode escape sequence \uHHHH 2037 if (preg_match('/^[$\w]+/', $input, $match)) 2038 { 2039 $tt = in_array($match[0], $this->keywords) ? $match[0] : TOKEN_IDENTIFIER; 2040 } 2041 else 2042 throw $this->newSyntaxError('Illegal token'); 2043 } 2044 } 2045 2046 $this->tokenIndex = ($this->tokenIndex + 1) & 3; 2047 2048 if (!isset($this->tokens[$this->tokenIndex])) 2049 $this->tokens[$this->tokenIndex] = new JSToken(); 2050 2051 $token = $this->tokens[$this->tokenIndex]; 2052 $token->type = $tt; 2053 2054 if ($tt == OP_ASSIGN) 2055 $token->assignOp = $op; 2056 2057 $token->start = $this->cursor; 2058 2059 $token->value = $match[0]; 2060 $this->cursor += strlen($match[0]); 2061 2062 $token->end = $this->cursor; 2063 $token->lineno = $this->lineno; 2064 2065 return $tt; 2066 } 2067 2068 public function unget() 2069 { 2070 if (++$this->lookahead == 4) 2071 throw $this->newSyntaxError('PANIC: too much lookahead!'); 2072 2073 $this->tokenIndex = ($this->tokenIndex - 1) & 3; 2074 } 2075 2076 public function newSyntaxError($m) 2077 { 2078 return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename . '\' on line ' . $this->lineno); 2079 } 2080} 2081 2082class JSToken 2083{ 2084 public $type; 2085 public $value; 2086 public $start; 2087 public $end; 2088 public $lineno; 2089 public $assignOp; 2090} 2091