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