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