PluginProbe
WP Super Minify • Minify, Compress and Cache HTML, CSS & JavaScript / trunk
WP Super Minify • Minify, Compress and Cache HTML, CSS & JavaScript vtrunk
trunk 1.0 1.1 1.2 1.3 1.3.1 1.3.2 1.4 1.5 1.5.1 1.6 2.0 2.0.1
wp-super-minify / includes / min / lib / JSMinPlus.php

JSMinPlus.php in WP Super Minify • Minify, Compress and Cache HTML, CSS & JavaScript trunk, at includes/min/lib/JSMinPlus.php

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