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