]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/libs/jsminplus.php
MediaWiki 1.30.2-scripts
[autoinstalls/mediawiki.git] / includes / libs / jsminplus.php
1 <?php
2 // @codingStandardsIgnoreFile 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 }