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