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