1<?php 2// phpcs:ignoreFile -- 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: https://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine) 10 * Narcissus sourcecode: https://mxr.mozilla.org/mozilla/source/js/narcissus/ 11 * JSMinPlus weblog: https://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 * @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 <brendan@mozilla.org>. 48 * Portions created by the Initial Developer are Copyright (C) 2004 49 * the Initial Developer. All Rights Reserved. 50 * 51 * Contributor(s): Tino Zijdel <crisp@tweakers.net> 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 68define('TOKEN_END', 1); 69define('TOKEN_NUMBER', 2); 70define('TOKEN_IDENTIFIER', 3); 71define('TOKEN_STRING', 4); 72define('TOKEN_REGEXP', 5); 73define('TOKEN_NEWLINE', 6); 74define('TOKEN_CONDCOMMENT_START', 7); 75define('TOKEN_CONDCOMMENT_END', 8); 76 77define('JS_SCRIPT', 100); 78define('JS_BLOCK', 101); 79define('JS_LABEL', 102); 80define('JS_FOR_IN', 103); 81define('JS_CALL', 104); 82define('JS_NEW_WITH_ARGS', 105); 83define('JS_INDEX', 106); 84define('JS_ARRAY_INIT', 107); 85define('JS_OBJECT_INIT', 108); 86define('JS_PROPERTY_INIT', 109); 87define('JS_GETTER', 110); 88define('JS_SETTER', 111); 89define('JS_GROUP', 112); 90define('JS_LIST', 113); 91 92define('JS_MINIFIED', 999); 93 94define('DECLARED_FORM', 0); 95define('EXPRESSED_FORM', 1); 96define('STATEMENT_FORM', 2); 97 98/* Operators */ 99define('OP_SEMICOLON', ';'); 100define('OP_COMMA', ','); 101define('OP_HOOK', '?'); 102define('OP_COLON', ':'); 103define('OP_OR', '||'); 104define('OP_AND', '&&'); 105define('OP_BITWISE_OR', '|'); 106define('OP_BITWISE_XOR', '^'); 107define('OP_BITWISE_AND', '&'); 108define('OP_STRICT_EQ', '==='); 109define('OP_EQ', '=='); 110define('OP_ASSIGN', '='); 111define('OP_STRICT_NE', '!=='); 112define('OP_NE', '!='); 113define('OP_LSH', '<<'); 114define('OP_LE', '<='); 115define('OP_LT', '<'); 116define('OP_URSH', '>>>'); 117define('OP_RSH', '>>'); 118define('OP_GE', '>='); 119define('OP_GT', '>'); 120define('OP_INCREMENT', '++'); 121define('OP_DECREMENT', '--'); 122define('OP_PLUS', '+'); 123define('OP_MINUS', '-'); 124define('OP_MUL', '*'); 125define('OP_DIV', '/'); 126define('OP_MOD', '%'); 127define('OP_NOT', '!'); 128define('OP_BITWISE_NOT', '~'); 129define('OP_DOT', '.'); 130define('OP_LEFT_BRACKET', '['); 131define('OP_RIGHT_BRACKET', ']'); 132define('OP_LEFT_CURLY', '{'); 133define('OP_RIGHT_CURLY', '}'); 134define('OP_LEFT_PAREN', '('); 135define('OP_RIGHT_PAREN', ')'); 136define('OP_CONDCOMMENT_END', '@*/'); 137 138define('OP_UNARY_PLUS', 'U+'); 139define('OP_UNARY_MINUS', 'U-'); 140 141/* Keywords */ 142define('KEYWORD_BREAK', 'break'); 143define('KEYWORD_CASE', 'case'); 144define('KEYWORD_CATCH', 'catch'); 145define('KEYWORD_CONST', 'const'); 146define('KEYWORD_CONTINUE', 'continue'); 147define('KEYWORD_DEBUGGER', 'debugger'); 148define('KEYWORD_DEFAULT', 'default'); 149define('KEYWORD_DELETE', 'delete'); 150define('KEYWORD_DO', 'do'); 151define('KEYWORD_ELSE', 'else'); 152define('KEYWORD_ENUM', 'enum'); 153define('KEYWORD_FALSE', 'false'); 154define('KEYWORD_FINALLY', 'finally'); 155define('KEYWORD_FOR', 'for'); 156define('KEYWORD_FUNCTION', 'function'); 157define('KEYWORD_IF', 'if'); 158define('KEYWORD_IN', 'in'); 159define('KEYWORD_INSTANCEOF', 'instanceof'); 160define('KEYWORD_NEW', 'new'); 161define('KEYWORD_NULL', 'null'); 162define('KEYWORD_RETURN', 'return'); 163define('KEYWORD_SWITCH', 'switch'); 164define('KEYWORD_THIS', 'this'); 165define('KEYWORD_THROW', 'throw'); 166define('KEYWORD_TRUE', 'true'); 167define('KEYWORD_TRY', 'try'); 168define('KEYWORD_TYPEOF', 'typeof'); 169define('KEYWORD_VAR', 'var'); 170define('KEYWORD_VOID', 'void'); 171define('KEYWORD_WHILE', 'while'); 172define('KEYWORD_WITH', 'with'); 173 174 175class 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 674class 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 ?: 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 https://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 break; 977 978 case KEYWORD_TRY: 979 $n = new JSNode($this->t); 980 $n->tryBlock = $this->Block($x); 981 $n->catchClauses = array(); 982 983 while ($this->t->match(KEYWORD_CATCH)) 984 { 985 $n2 = new JSNode($this->t); 986 $this->t->mustMatch(OP_LEFT_PAREN); 987 $n2->varName = $this->t->mustMatch(TOKEN_IDENTIFIER)->value; 988 989 if ($this->t->match(KEYWORD_IF)) 990 { 991 if ($x->ecmaStrictMode) 992 throw $this->t->newSyntaxError('Illegal catch guard'); 993 994 if (count($n->catchClauses) && !end($n->catchClauses)->guard) 995 throw $this->t->newSyntaxError('Guarded catch after unguarded'); 996 997 $n2->guard = $this->Expression($x); 998 } 999 else 1000 { 1001 $n2->guard = null; 1002 } 1003 1004 $this->t->mustMatch(OP_RIGHT_PAREN); 1005 $n2->block = $this->Block($x); 1006 array_push($n->catchClauses, $n2); 1007 } 1008 1009 if ($this->t->match(KEYWORD_FINALLY)) 1010 $n->finallyBlock = $this->Block($x); 1011 1012 if (!count($n->catchClauses) && !$n->finallyBlock) 1013 throw $this->t->newSyntaxError('Invalid try statement'); 1014 return $n; 1015 1016 case KEYWORD_CATCH: 1017 case KEYWORD_FINALLY: 1018 throw $this->t->newSyntaxError($tt . ' without preceding try'); 1019 1020 case KEYWORD_THROW: 1021 $n = new JSNode($this->t); 1022 $n->value = $this->Expression($x); 1023 break; 1024 1025 case KEYWORD_RETURN: 1026 if (!$x->inFunction) 1027 throw $this->t->newSyntaxError('Invalid return'); 1028 1029 $n = new JSNode($this->t); 1030 $tt = $this->t->peekOnSameLine(); 1031 if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY) 1032 $n->value = $this->Expression($x); 1033 else 1034 $n->value = null; 1035 break; 1036 1037 case KEYWORD_WITH: 1038 $n = new JSNode($this->t); 1039 $n->object = $this->ParenExpression($x); 1040 $n->body = $this->nest($x, $n); 1041 return $n; 1042 1043 case KEYWORD_VAR: 1044 case KEYWORD_CONST: 1045 $n = $this->Variables($x); 1046 break; 1047 1048 case TOKEN_CONDCOMMENT_START: 1049 case TOKEN_CONDCOMMENT_END: 1050 $n = new JSNode($this->t); 1051 return $n; 1052 1053 case KEYWORD_DEBUGGER: 1054 $n = new JSNode($this->t); 1055 break; 1056 1057 case TOKEN_NEWLINE: 1058 case OP_SEMICOLON: 1059 $n = new JSNode($this->t, OP_SEMICOLON); 1060 $n->expression = null; 1061 return $n; 1062 1063 default: 1064 if ($tt == TOKEN_IDENTIFIER) 1065 { 1066 $this->t->scanOperand = false; 1067 $tt = $this->t->peek(); 1068 $this->t->scanOperand = true; 1069 if ($tt == OP_COLON) 1070 { 1071 $label = $this->t->currentToken()->value; 1072 $ss = $x->stmtStack; 1073 for ($i = count($ss) - 1; $i >= 0; --$i) 1074 { 1075 if ($ss[$i]->label == $label) 1076 throw $this->t->newSyntaxError('Duplicate label'); 1077 } 1078 1079 $this->t->get(); 1080 $n = new JSNode($this->t, JS_LABEL); 1081 $n->label = $label; 1082 $n->statement = $this->nest($x, $n); 1083 1084 return $n; 1085 } 1086 } 1087 1088 $n = new JSNode($this->t, OP_SEMICOLON); 1089 $this->t->unget(); 1090 $n->expression = $this->Expression($x); 1091 $n->end = $n->expression->end; 1092 break; 1093 } 1094 1095 if ($this->t->lineno == $this->t->currentToken()->lineno) 1096 { 1097 $tt = $this->t->peekOnSameLine(); 1098 if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY) 1099 throw $this->t->newSyntaxError('Missing ; before statement'); 1100 } 1101 1102 $this->t->match(OP_SEMICOLON); 1103 1104 return $n; 1105 } 1106 1107 private function FunctionDefinition($x, $requireName, $functionForm) 1108 { 1109 $f = new JSNode($this->t); 1110 1111 if ($f->type != KEYWORD_FUNCTION) 1112 $f->type = ($f->value == 'get') ? JS_GETTER : JS_SETTER; 1113 1114 if ($this->t->match(TOKEN_IDENTIFIER)) 1115 $f->name = $this->t->currentToken()->value; 1116 elseif ($requireName) 1117 throw $this->t->newSyntaxError('Missing function identifier'); 1118 1119 $this->t->mustMatch(OP_LEFT_PAREN); 1120 $f->params = array(); 1121 1122 while (($tt = $this->t->get()) != OP_RIGHT_PAREN) 1123 { 1124 if ($tt != TOKEN_IDENTIFIER) 1125 throw $this->t->newSyntaxError('Missing formal parameter'); 1126 1127 array_push($f->params, $this->t->currentToken()->value); 1128 1129 if ($this->t->peek() != OP_RIGHT_PAREN) 1130 $this->t->mustMatch(OP_COMMA); 1131 } 1132 1133 $this->t->mustMatch(OP_LEFT_CURLY); 1134 1135 $x2 = new JSCompilerContext(true); 1136 $f->body = $this->Script($x2); 1137 1138 $this->t->mustMatch(OP_RIGHT_CURLY); 1139 $f->end = $this->t->currentToken()->end; 1140 1141 $f->functionForm = $functionForm; 1142 if ($functionForm == DECLARED_FORM) 1143 array_push($x->funDecls, $f); 1144 1145 return $f; 1146 } 1147 1148 private function Variables($x) 1149 { 1150 $n = new JSNode($this->t); 1151 1152 do 1153 { 1154 $this->t->mustMatch(TOKEN_IDENTIFIER); 1155 1156 $n2 = new JSNode($this->t); 1157 $n2->name = $n2->value; 1158 1159 if ($this->t->match(OP_ASSIGN)) 1160 { 1161 if ($this->t->currentToken()->assignOp) 1162 throw $this->t->newSyntaxError('Invalid variable initialization'); 1163 1164 $n2->initializer = $this->Expression($x, OP_COMMA); 1165 } 1166 1167 $n2->readOnly = $n->type == KEYWORD_CONST; 1168 1169 $n->addNode($n2); 1170 array_push($x->varDecls, $n2); 1171 } 1172 while ($this->t->match(OP_COMMA)); 1173 1174 return $n; 1175 } 1176 1177 private function Expression($x, $stop=false) 1178 { 1179 $operators = array(); 1180 $operands = array(); 1181 $n = false; 1182 1183 $bl = $x->bracketLevel; 1184 $cl = $x->curlyLevel; 1185 $pl = $x->parenLevel; 1186 $hl = $x->hookLevel; 1187 1188 while (($tt = $this->t->get()) != TOKEN_END) 1189 { 1190 if ($tt == $stop && 1191 $x->bracketLevel == $bl && 1192 $x->curlyLevel == $cl && 1193 $x->parenLevel == $pl && 1194 $x->hookLevel == $hl 1195 ) 1196 { 1197 // Stop only if tt matches the optional stop parameter, and that 1198 // token is not quoted by some kind of bracket. 1199 break; 1200 } 1201 1202 switch ($tt) 1203 { 1204 case OP_SEMICOLON: 1205 // NB: cannot be empty, Statement handled that. 1206 break 2; 1207 1208 case OP_HOOK: 1209 if ($this->t->scanOperand) 1210 break 2; 1211 1212 while ( !empty($operators) && 1213 $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] 1214 ) 1215 $this->reduce($operators, $operands); 1216 1217 array_push($operators, new JSNode($this->t)); 1218 1219 ++$x->hookLevel; 1220 $this->t->scanOperand = true; 1221 $n = $this->Expression($x); 1222 1223 if (!$this->t->match(OP_COLON)) 1224 break 2; 1225 1226 --$x->hookLevel; 1227 array_push($operands, $n); 1228 break; 1229 1230 case OP_COLON: 1231 if ($x->hookLevel) 1232 break 2; 1233 1234 throw $this->t->newSyntaxError('Invalid label'); 1235 break; 1236 1237 case OP_ASSIGN: 1238 if ($this->t->scanOperand) 1239 break 2; 1240 1241 // Use >, not >=, for right-associative ASSIGN 1242 while ( !empty($operators) && 1243 $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] 1244 ) 1245 $this->reduce($operators, $operands); 1246 1247 array_push($operators, new JSNode($this->t)); 1248 end($operands)->assignOp = $this->t->currentToken()->assignOp; 1249 $this->t->scanOperand = true; 1250 break; 1251 1252 case KEYWORD_IN: 1253 // An in operator should not be parsed if we're parsing the head of 1254 // a for (...) loop, unless it is in the then part of a conditional 1255 // expression, or parenthesized somehow. 1256 if ($x->inForLoopInit && !$x->hookLevel && 1257 !$x->bracketLevel && !$x->curlyLevel && 1258 !$x->parenLevel 1259 ) 1260 break 2; 1261 // FALL THROUGH 1262 case OP_COMMA: 1263 // A comma operator should not be parsed if we're parsing the then part 1264 // of a conditional expression unless it's parenthesized somehow. 1265 if ($tt == OP_COMMA && $x->hookLevel && 1266 !$x->bracketLevel && !$x->curlyLevel && 1267 !$x->parenLevel 1268 ) 1269 break 2; 1270 // Treat comma as left-associative so reduce can fold left-heavy 1271 // COMMA trees into a single array. 1272 // FALL THROUGH 1273 case OP_OR: 1274 case OP_AND: 1275 case OP_BITWISE_OR: 1276 case OP_BITWISE_XOR: 1277 case OP_BITWISE_AND: 1278 case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE: 1279 case OP_LT: case OP_LE: case OP_GE: case OP_GT: 1280 case KEYWORD_INSTANCEOF: 1281 case OP_LSH: case OP_RSH: case OP_URSH: 1282 case OP_PLUS: case OP_MINUS: 1283 case OP_MUL: case OP_DIV: case OP_MOD: 1284 case OP_DOT: 1285 if ($this->t->scanOperand) 1286 break 2; 1287 1288 while ( !empty($operators) && 1289 $this->opPrecedence[end($operators)->type] >= $this->opPrecedence[$tt] 1290 ) 1291 $this->reduce($operators, $operands); 1292 1293 if ($tt == OP_DOT) 1294 { 1295 $tt = $this->t->get(); 1296 if (!$this->isKeyword($tt) && $tt !== TOKEN_IDENTIFIER) 1297 throw $this->t->newSyntaxError("Unexpected token; token identifier or keyword expected."); 1298 1299 array_push($operands, new JSNode($this->t, OP_DOT, array_pop($operands), new JSNode($this->t))); 1300 } 1301 else 1302 { 1303 array_push($operators, new JSNode($this->t)); 1304 $this->t->scanOperand = true; 1305 } 1306 break; 1307 1308 case KEYWORD_DELETE: case KEYWORD_VOID: case KEYWORD_TYPEOF: 1309 case OP_NOT: case OP_BITWISE_NOT: case OP_UNARY_PLUS: case OP_UNARY_MINUS: 1310 case KEYWORD_NEW: 1311 if (!$this->t->scanOperand) 1312 break 2; 1313 1314 array_push($operators, new JSNode($this->t)); 1315 break; 1316 1317 case OP_INCREMENT: case OP_DECREMENT: 1318 if ($this->t->scanOperand) 1319 { 1320 array_push($operators, new JSNode($this->t)); // prefix increment or decrement 1321 } 1322 else 1323 { 1324 // Don't cross a line boundary for postfix {in,de}crement. 1325 $t = $this->t->tokens[($this->t->tokenIndex + $this->t->lookahead - 1) & 3]; 1326 if ($t && $t->lineno != $this->t->lineno) 1327 break 2; 1328 1329 if (!empty($operators)) 1330 { 1331 // Use >, not >=, so postfix has higher precedence than prefix. 1332 while ($this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]) 1333 $this->reduce($operators, $operands); 1334 } 1335 1336 $n = new JSNode($this->t, $tt, array_pop($operands)); 1337 $n->postfix = true; 1338 array_push($operands, $n); 1339 } 1340 break; 1341 1342 case KEYWORD_FUNCTION: 1343 if (!$this->t->scanOperand) 1344 break 2; 1345 1346 array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM)); 1347 $this->t->scanOperand = false; 1348 break; 1349 1350 case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE: 1351 case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP: 1352 if (!$this->t->scanOperand) 1353 break 2; 1354 1355 array_push($operands, new JSNode($this->t)); 1356 $this->t->scanOperand = false; 1357 break; 1358 1359 case TOKEN_CONDCOMMENT_START: 1360 case TOKEN_CONDCOMMENT_END: 1361 if ($this->t->scanOperand) 1362 array_push($operators, new JSNode($this->t)); 1363 else 1364 array_push($operands, new JSNode($this->t)); 1365 break; 1366 1367 case OP_LEFT_BRACKET: 1368 if ($this->t->scanOperand) 1369 { 1370 // Array initialiser. Parse using recursive descent, as the 1371 // sub-grammar here is not an operator grammar. 1372 $n = new JSNode($this->t, JS_ARRAY_INIT); 1373 while (($tt = $this->t->peek()) != OP_RIGHT_BRACKET) 1374 { 1375 if ($tt == OP_COMMA) 1376 { 1377 $this->t->get(); 1378 $n->addNode(null); 1379 continue; 1380 } 1381 1382 $n->addNode($this->Expression($x, OP_COMMA)); 1383 if (!$this->t->match(OP_COMMA)) 1384 break; 1385 } 1386 1387 $this->t->mustMatch(OP_RIGHT_BRACKET); 1388 array_push($operands, $n); 1389 $this->t->scanOperand = false; 1390 } 1391 else 1392 { 1393 // Property indexing operator. 1394 array_push($operators, new JSNode($this->t, JS_INDEX)); 1395 $this->t->scanOperand = true; 1396 ++$x->bracketLevel; 1397 } 1398 break; 1399 1400 case OP_RIGHT_BRACKET: 1401 if ($this->t->scanOperand || $x->bracketLevel == $bl) 1402 break 2; 1403 1404 while ($this->reduce($operators, $operands)->type != JS_INDEX) 1405 continue; 1406 1407 --$x->bracketLevel; 1408 break; 1409 1410 case OP_LEFT_CURLY: 1411 if (!$this->t->scanOperand) 1412 break 2; 1413 1414 // Object initialiser. As for array initialisers (see above), 1415 // parse using recursive descent. 1416 ++$x->curlyLevel; 1417 $n = new JSNode($this->t, JS_OBJECT_INIT); 1418 while (!$this->t->match(OP_RIGHT_CURLY)) 1419 { 1420 do 1421 { 1422 $tt = $this->t->get(); 1423 $tv = $this->t->currentToken()->value; 1424 if (($tv == 'get' || $tv == 'set') && $this->t->peek() == TOKEN_IDENTIFIER) 1425 { 1426 if ($x->ecmaStrictMode) 1427 throw $this->t->newSyntaxError('Illegal property accessor'); 1428 1429 $n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM)); 1430 } 1431 else 1432 { 1433 // Accept keywords as property names by treating 1434 // them similarly with identifiers 1435 if ($this->isKeyword($tt)) 1436 $tt = TOKEN_IDENTIFIER; 1437 1438 switch ($tt) 1439 { 1440 case TOKEN_IDENTIFIER: 1441 case TOKEN_NUMBER: 1442 case TOKEN_STRING: 1443 $id = new JSNode($this->t); 1444 break; 1445 1446 case OP_RIGHT_CURLY: 1447 if ($x->ecmaStrictMode) 1448 throw $this->t->newSyntaxError('Illegal trailing ,'); 1449 break 3; 1450 1451 default: 1452 throw $this->t->newSyntaxError('Invalid property name'); 1453 } 1454 1455 $this->t->mustMatch(OP_COLON); 1456 $n->addNode(new JSNode($this->t, JS_PROPERTY_INIT, $id, $this->Expression($x, OP_COMMA))); 1457 } 1458 } 1459 while ($this->t->match(OP_COMMA)); 1460 1461 $this->t->mustMatch(OP_RIGHT_CURLY); 1462 break; 1463 } 1464 1465 array_push($operands, $n); 1466 $this->t->scanOperand = false; 1467 --$x->curlyLevel; 1468 break; 1469 1470 case OP_RIGHT_CURLY: 1471 if (!$this->t->scanOperand && $x->curlyLevel != $cl) 1472 throw new Exception('PANIC: right curly botch'); 1473 break 2; 1474 1475 case OP_LEFT_PAREN: 1476 if ($this->t->scanOperand) 1477 { 1478 array_push($operators, new JSNode($this->t, JS_GROUP)); 1479 } 1480 else 1481 { 1482 while ( !empty($operators) && 1483 $this->opPrecedence[end($operators)->type] > $this->opPrecedence[KEYWORD_NEW] 1484 ) 1485 $this->reduce($operators, $operands); 1486 1487 // Handle () now, to regularize the n-ary case for n > 0. 1488 // We must set scanOperand in case there are arguments and 1489 // the first one is a regexp or unary+/-. 1490 $n = end($operators); 1491 $this->t->scanOperand = true; 1492 if ($this->t->match(OP_RIGHT_PAREN)) 1493 { 1494 if ($n && $n->type == KEYWORD_NEW) 1495 { 1496 array_pop($operators); 1497 $n->addNode(array_pop($operands)); 1498 } 1499 else 1500 { 1501 $n = new JSNode($this->t, JS_CALL, array_pop($operands), new JSNode($this->t, JS_LIST)); 1502 } 1503 1504 array_push($operands, $n); 1505 $this->t->scanOperand = false; 1506 break; 1507 } 1508 1509 if ($n && $n->type == KEYWORD_NEW) 1510 $n->type = JS_NEW_WITH_ARGS; 1511 else 1512 array_push($operators, new JSNode($this->t, JS_CALL)); 1513 } 1514 1515 ++$x->parenLevel; 1516 break; 1517 1518 case OP_RIGHT_PAREN: 1519 if ($this->t->scanOperand || $x->parenLevel == $pl) 1520 break 2; 1521 1522 while (($tt = $this->reduce($operators, $operands)->type) != JS_GROUP && 1523 $tt != JS_CALL && $tt != JS_NEW_WITH_ARGS 1524 ) 1525 { 1526 continue; 1527 } 1528 1529 if ($tt != JS_GROUP) 1530 { 1531 $n = end($operands); 1532 if ($n->treeNodes[1]->type != OP_COMMA) 1533 $n->treeNodes[1] = new JSNode($this->t, JS_LIST, $n->treeNodes[1]); 1534 else 1535 $n->treeNodes[1]->type = JS_LIST; 1536 } 1537 1538 --$x->parenLevel; 1539 break; 1540 1541 // Automatic semicolon insertion means we may scan across a newline 1542 // and into the beginning of another statement. If so, break out of 1543 // the while loop and let the t.scanOperand logic handle errors. 1544 default: 1545 break 2; 1546 } 1547 } 1548 1549 if ($x->hookLevel != $hl) 1550 throw $this->t->newSyntaxError('Missing : in conditional expression'); 1551 1552 if ($x->parenLevel != $pl) 1553 throw $this->t->newSyntaxError('Missing ) in parenthetical'); 1554 1555 if ($x->bracketLevel != $bl) 1556 throw $this->t->newSyntaxError('Missing ] in index expression'); 1557 1558 if ($this->t->scanOperand) 1559 throw $this->t->newSyntaxError('Missing operand'); 1560 1561 // Resume default mode, scanning for operands, not operators. 1562 $this->t->scanOperand = true; 1563 $this->t->unget(); 1564 1565 while (count($operators)) 1566 $this->reduce($operators, $operands); 1567 1568 return array_pop($operands); 1569 } 1570 1571 private function ParenExpression($x) 1572 { 1573 $this->t->mustMatch(OP_LEFT_PAREN); 1574 $n = $this->Expression($x); 1575 $this->t->mustMatch(OP_RIGHT_PAREN); 1576 1577 return $n; 1578 } 1579 1580 // Statement stack and nested statement handler. 1581 private function nest($x, $node, $end = false) 1582 { 1583 array_push($x->stmtStack, $node); 1584 $n = $this->Statement($x); 1585 array_pop($x->stmtStack); 1586 1587 if ($end) 1588 $this->t->mustMatch($end); 1589 1590 return $n; 1591 } 1592 1593 private function reduce(&$operators, &$operands) 1594 { 1595 $n = array_pop($operators); 1596 $op = $n->type; 1597 $arity = $this->opArity[$op]; 1598 $c = count($operands); 1599 if ($arity == -2) 1600 { 1601 // Flatten left-associative trees 1602 if ($c >= 2) 1603 { 1604 $left = $operands[$c - 2]; 1605 if ($left->type == $op) 1606 { 1607 $right = array_pop($operands); 1608 $left->addNode($right); 1609 return $left; 1610 } 1611 } 1612 $arity = 2; 1613 } 1614 1615 // Always use push to add operands to n, to update start and end 1616 $a = array_splice($operands, $c - $arity); 1617 for ($i = 0; $i < $arity; $i++) 1618 $n->addNode($a[$i]); 1619 1620 // Include closing bracket or postfix operator in [start,end] 1621 $te = $this->t->currentToken()->end; 1622 if ($n->end < $te) 1623 $n->end = $te; 1624 1625 array_push($operands, $n); 1626 1627 return $n; 1628 } 1629 1630 private function isKeyword($tt) 1631 { 1632 switch ($tt) { 1633 case KEYWORD_BREAK: 1634 case KEYWORD_CASE: 1635 case KEYWORD_CATCH: 1636 case KEYWORD_CONST: 1637 case KEYWORD_CONTINUE: 1638 case KEYWORD_DEBUGGER: 1639 case KEYWORD_DEFAULT: 1640 case KEYWORD_DELETE: 1641 case KEYWORD_DO: 1642 case KEYWORD_ELSE: 1643 case KEYWORD_ENUM: 1644 case KEYWORD_FALSE: 1645 case KEYWORD_FINALLY: 1646 case KEYWORD_FOR: 1647 case KEYWORD_FUNCTION: 1648 case KEYWORD_IF: 1649 case KEYWORD_IN: 1650 case KEYWORD_INSTANCEOF: 1651 case KEYWORD_NEW: 1652 case KEYWORD_NULL: 1653 case KEYWORD_RETURN: 1654 case KEYWORD_SWITCH: 1655 case KEYWORD_THIS: 1656 case KEYWORD_THROW: 1657 case KEYWORD_TRUE: 1658 case KEYWORD_TRY: 1659 case KEYWORD_TYPEOF: 1660 case KEYWORD_VAR: 1661 case KEYWORD_VOID: 1662 case KEYWORD_WHILE: 1663 case KEYWORD_WITH: 1664 return true; 1665 default: 1666 return false; 1667 } 1668 } 1669} 1670 1671class JSCompilerContext 1672{ 1673 public $inFunction = false; 1674 public $inForLoopInit = false; 1675 public $ecmaStrictMode = false; 1676 public $bracketLevel = 0; 1677 public $curlyLevel = 0; 1678 public $parenLevel = 0; 1679 public $hookLevel = 0; 1680 1681 public $stmtStack = array(); 1682 public $funDecls = array(); 1683 public $varDecls = array(); 1684 1685 public function __construct($inFunction) 1686 { 1687 $this->inFunction = $inFunction; 1688 } 1689} 1690 1691class JSNode 1692{ 1693 private $type; 1694 private $value; 1695 private $lineno; 1696 private $start; 1697 private $end; 1698 1699 public $treeNodes = array(); 1700 public $funDecls = array(); 1701 public $varDecls = array(); 1702 1703 public function __construct($t, $type=0, ...$nodes) 1704 { 1705 if ($token = $t->currentToken()) 1706 { 1707 $this->type = $type ?: $token->type; 1708 $this->value = $token->value; 1709 $this->lineno = $token->lineno; 1710 $this->start = $token->start; 1711 $this->end = $token->end; 1712 } 1713 else 1714 { 1715 $this->type = $type; 1716 $this->lineno = $t->lineno; 1717 } 1718 1719 foreach($nodes as $node) 1720 { 1721 $this->addNode($node); 1722 } 1723 } 1724 1725 // we don't want to bloat our object with all kind of specific properties, so we use overloading 1726 public function __set($name, $value) 1727 { 1728 $this->$name = $value; 1729 } 1730 1731 public function __get($name) 1732 { 1733 if (isset($this->$name)) 1734 return $this->$name; 1735 1736 return null; 1737 } 1738 1739 public function addNode($node) 1740 { 1741 if ($node !== null) 1742 { 1743 if ($node->start < $this->start) 1744 $this->start = $node->start; 1745 if ($this->end < $node->end) 1746 $this->end = $node->end; 1747 } 1748 1749 $this->treeNodes[] = $node; 1750 } 1751} 1752 1753class JSTokenizer 1754{ 1755 private $cursor = 0; 1756 private $source; 1757 1758 public $tokens = array(); 1759 public $tokenIndex = 0; 1760 public $lookahead = 0; 1761 public $scanNewlines = false; 1762 public $scanOperand = true; 1763 1764 public $filename; 1765 public $lineno; 1766 1767 private $keywords = array( 1768 'break', 1769 'case', 'catch', 'const', 'continue', 1770 'debugger', 'default', 'delete', 'do', 1771 'else', 'enum', 1772 'false', 'finally', 'for', 'function', 1773 'if', 'in', 'instanceof', 1774 'new', 'null', 1775 'return', 1776 'switch', 1777 'this', 'throw', 'true', 'try', 'typeof', 1778 'var', 'void', 1779 'while', 'with' 1780 ); 1781 1782 private $opTypeNames = array( 1783 ';', ',', '?', ':', '||', '&&', '|', '^', 1784 '&', '===', '==', '=', '!==', '!=', '<<', '<=', 1785 '<', '>>>', '>>', '>=', '>', '++', '--', '+', 1786 '-', '*', '/', '%', '!', '~', '.', '[', 1787 ']', '{', '}', '(', ')', '@*/' 1788 ); 1789 1790 private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%'); 1791 private $opRegExp; 1792 1793 public function __construct() 1794 { 1795 $this->opRegExp = '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames)) . ')#'; 1796 } 1797 1798 public function init($source, $filename = '', $lineno = 1) 1799 { 1800 $this->source = $source; 1801 $this->filename = $filename ?: '[inline]'; 1802 $this->lineno = $lineno; 1803 1804 $this->cursor = 0; 1805 $this->tokens = array(); 1806 $this->tokenIndex = 0; 1807 $this->lookahead = 0; 1808 $this->scanNewlines = false; 1809 $this->scanOperand = true; 1810 } 1811 1812 public function getInput($chunksize) 1813 { 1814 if ($chunksize) 1815 return substr($this->source, $this->cursor, $chunksize); 1816 1817 return substr($this->source, $this->cursor); 1818 } 1819 1820 public function isDone() 1821 { 1822 return $this->peek() == TOKEN_END; 1823 } 1824 1825 public function match($tt) 1826 { 1827 return $this->get() == $tt || $this->unget(); 1828 } 1829 1830 public function mustMatch($tt) 1831 { 1832 if (!$this->match($tt)) 1833 throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected'); 1834 1835 return $this->currentToken(); 1836 } 1837 1838 public function peek() 1839 { 1840 if ($this->lookahead) 1841 { 1842 $next = $this->tokens[($this->tokenIndex + $this->lookahead) & 3]; 1843 if ($this->scanNewlines && $next->lineno != $this->lineno) 1844 $tt = TOKEN_NEWLINE; 1845 else 1846 $tt = $next->type; 1847 } 1848 else 1849 { 1850 $tt = $this->get(); 1851 $this->unget(); 1852 } 1853 1854 return $tt; 1855 } 1856 1857 public function peekOnSameLine() 1858 { 1859 $this->scanNewlines = true; 1860 $tt = $this->peek(); 1861 $this->scanNewlines = false; 1862 1863 return $tt; 1864 } 1865 1866 public function currentToken() 1867 { 1868 if (!empty($this->tokens)) 1869 return $this->tokens[$this->tokenIndex]; 1870 } 1871 1872 public function get($chunksize = 1000) 1873 { 1874 while($this->lookahead) 1875 { 1876 $this->lookahead--; 1877 $this->tokenIndex = ($this->tokenIndex + 1) & 3; 1878 $token = $this->tokens[$this->tokenIndex]; 1879 if ($token->type != TOKEN_NEWLINE || $this->scanNewlines) 1880 return $token->type; 1881 } 1882 1883 $conditional_comment = false; 1884 1885 // strip whitespace and comments 1886 while(true) 1887 { 1888 $input = $this->getInput($chunksize); 1889 1890 // whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!) 1891 $re = $this->scanNewlines ? '/^[ \r\t]+/' : '/^\s+/'; 1892 if (preg_match($re, $input, $match)) 1893 { 1894 $spaces = $match[0]; 1895 $spacelen = strlen($spaces); 1896 $this->cursor += $spacelen; 1897 if (!$this->scanNewlines) 1898 $this->lineno += substr_count($spaces, "\n"); 1899 1900 if ($spacelen == $chunksize) 1901 continue; // complete chunk contained whitespace 1902 1903 $input = $this->getInput($chunksize); 1904 if ($input == '' || $input[0] != '/') 1905 break; 1906 } 1907 1908 // Comments 1909 if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match)) 1910 { 1911 if (!$chunksize) 1912 break; 1913 1914 // retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment) 1915 $chunksize = null; 1916 continue; 1917 } 1918 1919 // check if this is a conditional (JScript) comment 1920 if (!empty($match[1])) 1921 { 1922 $match[0] = '/*' . $match[1]; 1923 $conditional_comment = true; 1924 break; 1925 } 1926 else 1927 { 1928 $this->cursor += strlen($match[0]); 1929 $this->lineno += substr_count($match[0], "\n"); 1930 } 1931 } 1932 1933 if ($input == '') 1934 { 1935 $tt = TOKEN_END; 1936 $match = array(''); 1937 } 1938 elseif ($conditional_comment) 1939 { 1940 $tt = TOKEN_CONDCOMMENT_START; 1941 } 1942 else 1943 { 1944 switch ($input[0]) 1945 { 1946 case '0': 1947 // hexadecimal 1948 if (($input[1] == 'x' || $input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match)) 1949 { 1950 $tt = TOKEN_NUMBER; 1951 break; 1952 } 1953 // FALL THROUGH 1954 1955 case '1': case '2': case '3': case '4': case '5': 1956 case '6': case '7': case '8': case '9': 1957 // should always match 1958 preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match); 1959 $tt = TOKEN_NUMBER; 1960 break; 1961 1962 case "'": 1963 if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match)) 1964 { 1965 $tt = TOKEN_STRING; 1966 } 1967 else 1968 { 1969 if ($chunksize) 1970 return $this->get(null); // retry with a full chunk fetch 1971 1972 throw $this->newSyntaxError('Unterminated string literal'); 1973 } 1974 break; 1975 1976 case '"': 1977 if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match)) 1978 { 1979 $tt = TOKEN_STRING; 1980 } 1981 else 1982 { 1983 if ($chunksize) 1984 return $this->get(null); // retry with a full chunk fetch 1985 1986 throw $this->newSyntaxError('Unterminated string literal'); 1987 } 1988 break; 1989 1990 case '/': 1991 if ($this->scanOperand && preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match)) 1992 { 1993 $tt = TOKEN_REGEXP; 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 // should always match 2010 preg_match($this->opRegExp, $input, $match); 2011 $op = $match[0]; 2012 if (in_array($op, $this->assignOps) && $input[strlen($op)] == '=') 2013 { 2014 $tt = OP_ASSIGN; 2015 $match[0] .= '='; 2016 } 2017 else 2018 { 2019 $tt = $op; 2020 if ($this->scanOperand) 2021 { 2022 if ($op == OP_PLUS) 2023 $tt = OP_UNARY_PLUS; 2024 elseif ($op == OP_MINUS) 2025 $tt = OP_UNARY_MINUS; 2026 } 2027 $op = null; 2028 } 2029 break; 2030 2031 case '.': 2032 if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match)) 2033 { 2034 $tt = TOKEN_NUMBER; 2035 break; 2036 } 2037 // FALL THROUGH 2038 2039 case ';': 2040 case ',': 2041 case '?': 2042 case ':': 2043 case '~': 2044 case '[': 2045 case ']': 2046 case '{': 2047 case '}': 2048 case '(': 2049 case ')': 2050 // these are all single 2051 $match = array($input[0]); 2052 $tt = $input[0]; 2053 break; 2054 2055 case '@': 2056 // check end of conditional comment 2057 if (substr($input, 0, 3) == '@*/') 2058 { 2059 $match = array('@*/'); 2060 $tt = TOKEN_CONDCOMMENT_END; 2061 } 2062 else 2063 throw $this->newSyntaxError('Illegal token'); 2064 break; 2065 2066 case "\n": 2067 if ($this->scanNewlines) 2068 { 2069 $match = array("\n"); 2070 $tt = TOKEN_NEWLINE; 2071 } 2072 else 2073 throw $this->newSyntaxError('Illegal token'); 2074 break; 2075 2076 default: 2077 // Fast path for identifiers: word chars followed by whitespace or various other tokens. 2078 // Note we don't need to exclude digits in the first char, as they've already been found 2079 // above. 2080 if (!preg_match('/^[$\w]+(?=[\s\/\|\^\&<>\+\-\*%=!.;,\?:~\[\]\{\}\(\)@])/', $input, $match)) 2081 { 2082 // Character classes per ECMA-262 edition 5.1 section 7.6 2083 // Per spec, must accept Unicode 3.0, *may* accept later versions. 2084 // We'll take whatever PCRE understands, which should be more recent. 2085 $identifierStartChars = "\\p{L}\\p{Nl}" . # UnicodeLetter 2086 "\$" . 2087 "_"; 2088 $identifierPartChars = $identifierStartChars . 2089 "\\p{Mn}\\p{Mc}" . # UnicodeCombiningMark 2090 "\\p{Nd}" . # UnicodeDigit 2091 "\\p{Pc}"; # UnicodeConnectorPunctuation 2092 $unicodeEscape = "\\\\u[0-9A-F-a-f]{4}"; 2093 $identifierRegex = "/^" . 2094 "(?:[$identifierStartChars]|$unicodeEscape)" . 2095 "(?:[$identifierPartChars]|$unicodeEscape)*" . 2096 "/uS"; 2097 if (preg_match($identifierRegex, $input, $match)) 2098 { 2099 if (strpos($match[0], '\\') !== false) { 2100 // Per ECMA-262 edition 5.1, section 7.6 escape sequences should behave as if they were 2101 // the original chars, but only within the boundaries of the identifier. 2102 $decoded = preg_replace_callback('/\\\\u([0-9A-Fa-f]{4})/', 2103 array(__CLASS__, 'unicodeEscapeCallback'), 2104 $match[0]); 2105 2106 // Since our original regex didn't de-escape the originals, we need to check for validity again. 2107 // No need to worry about token boundaries, as anything outside the identifier is illegal! 2108 if (!preg_match("/^[$identifierStartChars][$identifierPartChars]*$/u", $decoded)) { 2109 throw $this->newSyntaxError('Illegal token'); 2110 } 2111 2112 // Per spec it _ought_ to work to use these escapes for keywords words as well... 2113 // but IE rejects them as invalid, while Firefox and Chrome treat them as identifiers 2114 // that don't match the keyword. 2115 if (in_array($decoded, $this->keywords)) { 2116 throw $this->newSyntaxError('Illegal token'); 2117 } 2118 2119 // TODO: save the decoded form for output? 2120 } 2121 } 2122 else 2123 throw $this->newSyntaxError('Illegal token'); 2124 } 2125 $tt = in_array($match[0], $this->keywords) ? $match[0] : TOKEN_IDENTIFIER; 2126 } 2127 } 2128 2129 $this->tokenIndex = ($this->tokenIndex + 1) & 3; 2130 2131 if (!isset($this->tokens[$this->tokenIndex])) 2132 $this->tokens[$this->tokenIndex] = new JSToken(); 2133 2134 $token = $this->tokens[$this->tokenIndex]; 2135 $token->type = $tt; 2136 2137 if ($tt == OP_ASSIGN) 2138 $token->assignOp = $op; 2139 2140 $token->start = $this->cursor; 2141 2142 $token->value = $match[0]; 2143 $this->cursor += strlen($match[0]); 2144 2145 $token->end = $this->cursor; 2146 $token->lineno = $this->lineno; 2147 2148 return $tt; 2149 } 2150 2151 public function unget() 2152 { 2153 if (++$this->lookahead == 4) 2154 throw $this->newSyntaxError('PANIC: too much lookahead!'); 2155 2156 $this->tokenIndex = ($this->tokenIndex - 1) & 3; 2157 } 2158 2159 public function newSyntaxError($m) 2160 { 2161 return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename . '\' on line ' . $this->lineno); 2162 } 2163 2164 public static function unicodeEscapeCallback($m) 2165 { 2166 return html_entity_decode('&#x' . $m[1]. ';', ENT_QUOTES, 'UTF-8'); 2167 } 2168} 2169 2170class JSToken 2171{ 2172 public $type; 2173 public $value; 2174 public $start; 2175 public $end; 2176 public $lineno; 2177 public $assignOp; 2178} 2179