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