1<?php
2// phpcs:ignoreFile -- File external to MediaWiki. Ignore coding conventions checks.
3/**
4 * JSMinPlus version 1.4
5 *
6 * Minifies a javascript file using a javascript parser
7 *
8 * This implements a PHP port of Brendan Eich's Narcissus open source javascript engine (in javascript)
9 * References: https://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine)
10 * Narcissus sourcecode: https://mxr.mozilla.org/mozilla/source/js/narcissus/
11 * JSMinPlus weblog: https://crisp.tweakblogs.net/blog/cat/716
12 *
13 * Tino Zijdel <crisp@tweakers.net>
14 *
15 * Usage: $minified = JSMinPlus::minify($script [, $filename])
16 *
17 * Versionlog (see also changelog.txt):
18 * 23-07-2011 - remove dynamic creation of OP_* and KEYWORD_* defines and declare them on top
19 *              reduce memory footprint by minifying by block-scope
20 *              some small byte-saving and performance improvements
21 * 12-05-2009 - fixed hook:colon precedence, fixed empty body in loop and if-constructs
22 * 18-04-2009 - fixed crashbug in PHP 5.2.9 and several other bugfixes
23 * 12-04-2009 - some small bugfixes and performance improvements
24 * 09-04-2009 - initial open sourced version 1.0
25 *
26 * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip
27 *
28 * @file
29 */
30
31/* ***** BEGIN LICENSE BLOCK *****
32 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
33 *
34 * The contents of this file are subject to the Mozilla Public License Version
35 * 1.1 (the "License"); you may not use this file except in compliance with
36 * the License. You may obtain a copy of the License at
37 * http://www.mozilla.org/MPL/
38 *
39 * Software distributed under the License is distributed on an "AS IS" basis,
40 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
41 * for the specific language governing rights and limitations under the
42 * License.
43 *
44 * The Original Code is the Narcissus JavaScript engine.
45 *
46 * The Initial Developer of the Original Code is
47 * Brendan Eich <brendan@mozilla.org>.
48 * Portions created by the Initial Developer are Copyright (C) 2004
49 * the Initial Developer. All Rights Reserved.
50 *
51 * Contributor(s): Tino Zijdel <crisp@tweakers.net>
52 * PHP port, modifications and minifier routine are (C) 2009-2011
53 *
54 * Alternatively, the contents of this file may be used under the terms of
55 * either the GNU General Public License Version 2 or later (the "GPL"), or
56 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
57 * in which case the provisions of the GPL or the LGPL are applicable instead
58 * of those above. If you wish to allow use of your version of this file only
59 * under the terms of either the GPL or the LGPL, and not to allow others to
60 * use your version of this file under the terms of the MPL, indicate your
61 * decision by deleting the provisions above and replace them with the notice
62 * and other provisions required by the GPL or the LGPL. If you do not delete
63 * the provisions above, a recipient may use your version of this file under
64 * the terms of any one of the MPL, the GPL or the LGPL.
65 *
66 * ***** END LICENSE BLOCK ***** */
67
68define('TOKEN_END', 1);
69define('TOKEN_NUMBER', 2);
70define('TOKEN_IDENTIFIER', 3);
71define('TOKEN_STRING', 4);
72define('TOKEN_REGEXP', 5);
73define('TOKEN_NEWLINE', 6);
74define('TOKEN_CONDCOMMENT_START', 7);
75define('TOKEN_CONDCOMMENT_END', 8);
76
77define('JS_SCRIPT', 100);
78define('JS_BLOCK', 101);
79define('JS_LABEL', 102);
80define('JS_FOR_IN', 103);
81define('JS_CALL', 104);
82define('JS_NEW_WITH_ARGS', 105);
83define('JS_INDEX', 106);
84define('JS_ARRAY_INIT', 107);
85define('JS_OBJECT_INIT', 108);
86define('JS_PROPERTY_INIT', 109);
87define('JS_GETTER', 110);
88define('JS_SETTER', 111);
89define('JS_GROUP', 112);
90define('JS_LIST', 113);
91
92define('JS_MINIFIED', 999);
93
94define('DECLARED_FORM', 0);
95define('EXPRESSED_FORM', 1);
96define('STATEMENT_FORM', 2);
97
98/* Operators */
99define('OP_SEMICOLON', ';');
100define('OP_COMMA', ',');
101define('OP_HOOK', '?');
102define('OP_COLON', ':');
103define('OP_OR', '||');
104define('OP_AND', '&&');
105define('OP_BITWISE_OR', '|');
106define('OP_BITWISE_XOR', '^');
107define('OP_BITWISE_AND', '&');
108define('OP_STRICT_EQ', '===');
109define('OP_EQ', '==');
110define('OP_ASSIGN', '=');
111define('OP_STRICT_NE', '!==');
112define('OP_NE', '!=');
113define('OP_LSH', '<<');
114define('OP_LE', '<=');
115define('OP_LT', '<');
116define('OP_URSH', '>>>');
117define('OP_RSH', '>>');
118define('OP_GE', '>=');
119define('OP_GT', '>');
120define('OP_INCREMENT', '++');
121define('OP_DECREMENT', '--');
122define('OP_PLUS', '+');
123define('OP_MINUS', '-');
124define('OP_MUL', '*');
125define('OP_DIV', '/');
126define('OP_MOD', '%');
127define('OP_NOT', '!');
128define('OP_BITWISE_NOT', '~');
129define('OP_DOT', '.');
130define('OP_LEFT_BRACKET', '[');
131define('OP_RIGHT_BRACKET', ']');
132define('OP_LEFT_CURLY', '{');
133define('OP_RIGHT_CURLY', '}');
134define('OP_LEFT_PAREN', '(');
135define('OP_RIGHT_PAREN', ')');
136define('OP_CONDCOMMENT_END', '@*/');
137
138define('OP_UNARY_PLUS', 'U+');
139define('OP_UNARY_MINUS', 'U-');
140
141/* Keywords */
142define('KEYWORD_BREAK', 'break');
143define('KEYWORD_CASE', 'case');
144define('KEYWORD_CATCH', 'catch');
145define('KEYWORD_CONST', 'const');
146define('KEYWORD_CONTINUE', 'continue');
147define('KEYWORD_DEBUGGER', 'debugger');
148define('KEYWORD_DEFAULT', 'default');
149define('KEYWORD_DELETE', 'delete');
150define('KEYWORD_DO', 'do');
151define('KEYWORD_ELSE', 'else');
152define('KEYWORD_ENUM', 'enum');
153define('KEYWORD_FALSE', 'false');
154define('KEYWORD_FINALLY', 'finally');
155define('KEYWORD_FOR', 'for');
156define('KEYWORD_FUNCTION', 'function');
157define('KEYWORD_IF', 'if');
158define('KEYWORD_IN', 'in');
159define('KEYWORD_INSTANCEOF', 'instanceof');
160define('KEYWORD_NEW', 'new');
161define('KEYWORD_NULL', 'null');
162define('KEYWORD_RETURN', 'return');
163define('KEYWORD_SWITCH', 'switch');
164define('KEYWORD_THIS', 'this');
165define('KEYWORD_THROW', 'throw');
166define('KEYWORD_TRUE', 'true');
167define('KEYWORD_TRY', 'try');
168define('KEYWORD_TYPEOF', 'typeof');
169define('KEYWORD_VAR', 'var');
170define('KEYWORD_VOID', 'void');
171define('KEYWORD_WHILE', 'while');
172define('KEYWORD_WITH', 'with');
173
174
175class 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
674class 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 ?: 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						$tt = $this->t->get();
1296						if (!$this->isKeyword($tt) && $tt !== TOKEN_IDENTIFIER)
1297							throw $this->t->newSyntaxError("Unexpected token; token identifier or keyword expected.");
1298
1299						array_push($operands, new JSNode($this->t, OP_DOT, array_pop($operands), new JSNode($this->t)));
1300					}
1301					else
1302					{
1303						array_push($operators, new JSNode($this->t));
1304						$this->t->scanOperand = true;
1305					}
1306				break;
1307
1308				case KEYWORD_DELETE: case KEYWORD_VOID: case KEYWORD_TYPEOF:
1309				case OP_NOT: case OP_BITWISE_NOT: case OP_UNARY_PLUS: case OP_UNARY_MINUS:
1310				case KEYWORD_NEW:
1311					if (!$this->t->scanOperand)
1312						break 2;
1313
1314					array_push($operators, new JSNode($this->t));
1315				break;
1316
1317				case OP_INCREMENT: case OP_DECREMENT:
1318					if ($this->t->scanOperand)
1319					{
1320						array_push($operators, new JSNode($this->t));  // prefix increment or decrement
1321					}
1322					else
1323					{
1324						// Don't cross a line boundary for postfix {in,de}crement.
1325						$t = $this->t->tokens[($this->t->tokenIndex + $this->t->lookahead - 1) & 3];
1326						if ($t && $t->lineno != $this->t->lineno)
1327							break 2;
1328
1329						if (!empty($operators))
1330						{
1331							// Use >, not >=, so postfix has higher precedence than prefix.
1332							while ($this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt])
1333								$this->reduce($operators, $operands);
1334						}
1335
1336						$n = new JSNode($this->t, $tt, array_pop($operands));
1337						$n->postfix = true;
1338						array_push($operands, $n);
1339					}
1340				break;
1341
1342				case KEYWORD_FUNCTION:
1343					if (!$this->t->scanOperand)
1344						break 2;
1345
1346					array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM));
1347					$this->t->scanOperand = false;
1348				break;
1349
1350				case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
1351				case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
1352					if (!$this->t->scanOperand)
1353						break 2;
1354
1355					array_push($operands, new JSNode($this->t));
1356					$this->t->scanOperand = false;
1357				break;
1358
1359				case TOKEN_CONDCOMMENT_START:
1360				case TOKEN_CONDCOMMENT_END:
1361					if ($this->t->scanOperand)
1362						array_push($operators, new JSNode($this->t));
1363					else
1364						array_push($operands, new JSNode($this->t));
1365				break;
1366
1367				case OP_LEFT_BRACKET:
1368					if ($this->t->scanOperand)
1369					{
1370						// Array initialiser.  Parse using recursive descent, as the
1371						// sub-grammar here is not an operator grammar.
1372						$n = new JSNode($this->t, JS_ARRAY_INIT);
1373						while (($tt = $this->t->peek()) != OP_RIGHT_BRACKET)
1374						{
1375							if ($tt == OP_COMMA)
1376							{
1377								$this->t->get();
1378								$n->addNode(null);
1379								continue;
1380							}
1381
1382							$n->addNode($this->Expression($x, OP_COMMA));
1383							if (!$this->t->match(OP_COMMA))
1384								break;
1385						}
1386
1387						$this->t->mustMatch(OP_RIGHT_BRACKET);
1388						array_push($operands, $n);
1389						$this->t->scanOperand = false;
1390					}
1391					else
1392					{
1393						// Property indexing operator.
1394						array_push($operators, new JSNode($this->t, JS_INDEX));
1395						$this->t->scanOperand = true;
1396						++$x->bracketLevel;
1397					}
1398				break;
1399
1400				case OP_RIGHT_BRACKET:
1401					if ($this->t->scanOperand || $x->bracketLevel == $bl)
1402						break 2;
1403
1404					while ($this->reduce($operators, $operands)->type != JS_INDEX)
1405						continue;
1406
1407					--$x->bracketLevel;
1408				break;
1409
1410				case OP_LEFT_CURLY:
1411					if (!$this->t->scanOperand)
1412						break 2;
1413
1414					// Object initialiser.  As for array initialisers (see above),
1415					// parse using recursive descent.
1416					++$x->curlyLevel;
1417					$n = new JSNode($this->t, JS_OBJECT_INIT);
1418					while (!$this->t->match(OP_RIGHT_CURLY))
1419					{
1420						do
1421						{
1422							$tt = $this->t->get();
1423							$tv = $this->t->currentToken()->value;
1424							if (($tv == 'get' || $tv == 'set') && $this->t->peek() == TOKEN_IDENTIFIER)
1425							{
1426								if ($x->ecmaStrictMode)
1427									throw $this->t->newSyntaxError('Illegal property accessor');
1428
1429								$n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM));
1430							}
1431							else
1432							{
1433								// Accept keywords as property names by treating
1434								// them similarly with identifiers
1435								if ($this->isKeyword($tt))
1436									$tt = TOKEN_IDENTIFIER;
1437
1438								switch ($tt)
1439								{
1440									case TOKEN_IDENTIFIER:
1441									case TOKEN_NUMBER:
1442									case TOKEN_STRING:
1443										$id = new JSNode($this->t);
1444									break;
1445
1446									case OP_RIGHT_CURLY:
1447										if ($x->ecmaStrictMode)
1448											throw $this->t->newSyntaxError('Illegal trailing ,');
1449									break 3;
1450
1451									default:
1452										throw $this->t->newSyntaxError('Invalid property name');
1453								}
1454
1455								$this->t->mustMatch(OP_COLON);
1456								$n->addNode(new JSNode($this->t, JS_PROPERTY_INIT, $id, $this->Expression($x, OP_COMMA)));
1457							}
1458						}
1459						while ($this->t->match(OP_COMMA));
1460
1461						$this->t->mustMatch(OP_RIGHT_CURLY);
1462						break;
1463					}
1464
1465					array_push($operands, $n);
1466					$this->t->scanOperand = false;
1467					--$x->curlyLevel;
1468				break;
1469
1470				case OP_RIGHT_CURLY:
1471					if (!$this->t->scanOperand && $x->curlyLevel != $cl)
1472						throw new Exception('PANIC: right curly botch');
1473				break 2;
1474
1475				case OP_LEFT_PAREN:
1476					if ($this->t->scanOperand)
1477					{
1478						array_push($operators, new JSNode($this->t, JS_GROUP));
1479					}
1480					else
1481					{
1482						while (	!empty($operators) &&
1483							$this->opPrecedence[end($operators)->type] > $this->opPrecedence[KEYWORD_NEW]
1484						)
1485							$this->reduce($operators, $operands);
1486
1487						// Handle () now, to regularize the n-ary case for n > 0.
1488						// We must set scanOperand in case there are arguments and
1489						// the first one is a regexp or unary+/-.
1490						$n = end($operators);
1491						$this->t->scanOperand = true;
1492						if ($this->t->match(OP_RIGHT_PAREN))
1493						{
1494							if ($n && $n->type == KEYWORD_NEW)
1495							{
1496								array_pop($operators);
1497								$n->addNode(array_pop($operands));
1498							}
1499							else
1500							{
1501								$n = new JSNode($this->t, JS_CALL, array_pop($operands), new JSNode($this->t, JS_LIST));
1502							}
1503
1504							array_push($operands, $n);
1505							$this->t->scanOperand = false;
1506							break;
1507						}
1508
1509						if ($n && $n->type == KEYWORD_NEW)
1510							$n->type = JS_NEW_WITH_ARGS;
1511						else
1512							array_push($operators, new JSNode($this->t, JS_CALL));
1513					}
1514
1515					++$x->parenLevel;
1516				break;
1517
1518				case OP_RIGHT_PAREN:
1519					if ($this->t->scanOperand || $x->parenLevel == $pl)
1520						break 2;
1521
1522					while (($tt = $this->reduce($operators, $operands)->type) != JS_GROUP &&
1523						$tt != JS_CALL && $tt != JS_NEW_WITH_ARGS
1524					)
1525					{
1526						continue;
1527					}
1528
1529					if ($tt != JS_GROUP)
1530					{
1531						$n = end($operands);
1532						if ($n->treeNodes[1]->type != OP_COMMA)
1533							$n->treeNodes[1] = new JSNode($this->t, JS_LIST, $n->treeNodes[1]);
1534						else
1535							$n->treeNodes[1]->type = JS_LIST;
1536					}
1537
1538					--$x->parenLevel;
1539				break;
1540
1541				// Automatic semicolon insertion means we may scan across a newline
1542				// and into the beginning of another statement.  If so, break out of
1543				// the while loop and let the t.scanOperand logic handle errors.
1544				default:
1545					break 2;
1546			}
1547		}
1548
1549		if ($x->hookLevel != $hl)
1550			throw $this->t->newSyntaxError('Missing : in conditional expression');
1551
1552		if ($x->parenLevel != $pl)
1553			throw $this->t->newSyntaxError('Missing ) in parenthetical');
1554
1555		if ($x->bracketLevel != $bl)
1556			throw $this->t->newSyntaxError('Missing ] in index expression');
1557
1558		if ($this->t->scanOperand)
1559			throw $this->t->newSyntaxError('Missing operand');
1560
1561		// Resume default mode, scanning for operands, not operators.
1562		$this->t->scanOperand = true;
1563		$this->t->unget();
1564
1565		while (count($operators))
1566			$this->reduce($operators, $operands);
1567
1568		return array_pop($operands);
1569	}
1570
1571	private function ParenExpression($x)
1572	{
1573		$this->t->mustMatch(OP_LEFT_PAREN);
1574		$n = $this->Expression($x);
1575		$this->t->mustMatch(OP_RIGHT_PAREN);
1576
1577		return $n;
1578	}
1579
1580	// Statement stack and nested statement handler.
1581	private function nest($x, $node, $end = false)
1582	{
1583		array_push($x->stmtStack, $node);
1584		$n = $this->Statement($x);
1585		array_pop($x->stmtStack);
1586
1587		if ($end)
1588			$this->t->mustMatch($end);
1589
1590		return $n;
1591	}
1592
1593	private function reduce(&$operators, &$operands)
1594	{
1595		$n = array_pop($operators);
1596		$op = $n->type;
1597		$arity = $this->opArity[$op];
1598		$c = count($operands);
1599		if ($arity == -2)
1600		{
1601			// Flatten left-associative trees
1602			if ($c >= 2)
1603			{
1604				$left = $operands[$c - 2];
1605				if ($left->type == $op)
1606				{
1607					$right = array_pop($operands);
1608					$left->addNode($right);
1609					return $left;
1610				}
1611			}
1612			$arity = 2;
1613		}
1614
1615		// Always use push to add operands to n, to update start and end
1616		$a = array_splice($operands, $c - $arity);
1617		for ($i = 0; $i < $arity; $i++)
1618			$n->addNode($a[$i]);
1619
1620		// Include closing bracket or postfix operator in [start,end]
1621		$te = $this->t->currentToken()->end;
1622		if ($n->end < $te)
1623			$n->end = $te;
1624
1625		array_push($operands, $n);
1626
1627		return $n;
1628	}
1629
1630	private function isKeyword($tt)
1631	{
1632		switch ($tt) {
1633			case KEYWORD_BREAK:
1634			case KEYWORD_CASE:
1635			case KEYWORD_CATCH:
1636			case KEYWORD_CONST:
1637			case KEYWORD_CONTINUE:
1638			case KEYWORD_DEBUGGER:
1639			case KEYWORD_DEFAULT:
1640			case KEYWORD_DELETE:
1641			case KEYWORD_DO:
1642			case KEYWORD_ELSE:
1643			case KEYWORD_ENUM:
1644			case KEYWORD_FALSE:
1645			case KEYWORD_FINALLY:
1646			case KEYWORD_FOR:
1647			case KEYWORD_FUNCTION:
1648			case KEYWORD_IF:
1649			case KEYWORD_IN:
1650			case KEYWORD_INSTANCEOF:
1651			case KEYWORD_NEW:
1652			case KEYWORD_NULL:
1653			case KEYWORD_RETURN:
1654			case KEYWORD_SWITCH:
1655			case KEYWORD_THIS:
1656			case KEYWORD_THROW:
1657			case KEYWORD_TRUE:
1658			case KEYWORD_TRY:
1659			case KEYWORD_TYPEOF:
1660			case KEYWORD_VAR:
1661			case KEYWORD_VOID:
1662			case KEYWORD_WHILE:
1663			case KEYWORD_WITH:
1664				return true;
1665			default:
1666				return false;
1667		}
1668	}
1669}
1670
1671class JSCompilerContext
1672{
1673	public $inFunction = false;
1674	public $inForLoopInit = false;
1675	public $ecmaStrictMode = false;
1676	public $bracketLevel = 0;
1677	public $curlyLevel = 0;
1678	public $parenLevel = 0;
1679	public $hookLevel = 0;
1680
1681	public $stmtStack = array();
1682	public $funDecls = array();
1683	public $varDecls = array();
1684
1685	public function __construct($inFunction)
1686	{
1687		$this->inFunction = $inFunction;
1688	}
1689}
1690
1691class JSNode
1692{
1693	private $type;
1694	private $value;
1695	private $lineno;
1696	private $start;
1697	private $end;
1698
1699	public $treeNodes = array();
1700	public $funDecls = array();
1701	public $varDecls = array();
1702
1703	public function __construct($t, $type=0, ...$nodes)
1704	{
1705		if ($token = $t->currentToken())
1706		{
1707			$this->type = $type ?: $token->type;
1708			$this->value = $token->value;
1709			$this->lineno = $token->lineno;
1710			$this->start = $token->start;
1711			$this->end = $token->end;
1712		}
1713		else
1714		{
1715			$this->type = $type;
1716			$this->lineno = $t->lineno;
1717		}
1718
1719		foreach($nodes as $node)
1720		{
1721			$this->addNode($node);
1722		}
1723	}
1724
1725	// we don't want to bloat our object with all kind of specific properties, so we use overloading
1726	public function __set($name, $value)
1727	{
1728		$this->$name = $value;
1729	}
1730
1731	public function __get($name)
1732	{
1733		if (isset($this->$name))
1734			return $this->$name;
1735
1736		return null;
1737	}
1738
1739	public function addNode($node)
1740	{
1741		if ($node !== null)
1742		{
1743			if ($node->start < $this->start)
1744				$this->start = $node->start;
1745			if ($this->end < $node->end)
1746				$this->end = $node->end;
1747		}
1748
1749		$this->treeNodes[] = $node;
1750	}
1751}
1752
1753class JSTokenizer
1754{
1755	private $cursor = 0;
1756	private $source;
1757
1758	public $tokens = array();
1759	public $tokenIndex = 0;
1760	public $lookahead = 0;
1761	public $scanNewlines = false;
1762	public $scanOperand = true;
1763
1764	public $filename;
1765	public $lineno;
1766
1767	private $keywords = array(
1768		'break',
1769		'case', 'catch', 'const', 'continue',
1770		'debugger', 'default', 'delete', 'do',
1771		'else', 'enum',
1772		'false', 'finally', 'for', 'function',
1773		'if', 'in', 'instanceof',
1774		'new', 'null',
1775		'return',
1776		'switch',
1777		'this', 'throw', 'true', 'try', 'typeof',
1778		'var', 'void',
1779		'while', 'with'
1780	);
1781
1782	private $opTypeNames = array(
1783		';', ',', '?', ':', '||', '&&', '|', '^',
1784		'&', '===', '==', '=', '!==', '!=', '<<', '<=',
1785		'<', '>>>', '>>', '>=', '>', '++', '--', '+',
1786		'-', '*', '/', '%', '!', '~', '.', '[',
1787		']', '{', '}', '(', ')', '@*/'
1788	);
1789
1790	private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
1791	private $opRegExp;
1792
1793	public function __construct()
1794	{
1795		$this->opRegExp = '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames)) . ')#';
1796	}
1797
1798	public function init($source, $filename = '', $lineno = 1)
1799	{
1800		$this->source = $source;
1801		$this->filename = $filename ?: '[inline]';
1802		$this->lineno = $lineno;
1803
1804		$this->cursor = 0;
1805		$this->tokens = array();
1806		$this->tokenIndex = 0;
1807		$this->lookahead = 0;
1808		$this->scanNewlines = false;
1809		$this->scanOperand = true;
1810	}
1811
1812	public function getInput($chunksize)
1813	{
1814		if ($chunksize)
1815			return substr($this->source, $this->cursor, $chunksize);
1816
1817		return substr($this->source, $this->cursor);
1818	}
1819
1820	public function isDone()
1821	{
1822		return $this->peek() == TOKEN_END;
1823	}
1824
1825	public function match($tt)
1826	{
1827		return $this->get() == $tt || $this->unget();
1828	}
1829
1830	public function mustMatch($tt)
1831	{
1832	        if (!$this->match($tt))
1833			throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
1834
1835		return $this->currentToken();
1836	}
1837
1838	public function peek()
1839	{
1840		if ($this->lookahead)
1841		{
1842			$next = $this->tokens[($this->tokenIndex + $this->lookahead) & 3];
1843			if ($this->scanNewlines && $next->lineno != $this->lineno)
1844				$tt = TOKEN_NEWLINE;
1845			else
1846				$tt = $next->type;
1847		}
1848		else
1849		{
1850			$tt = $this->get();
1851			$this->unget();
1852		}
1853
1854		return $tt;
1855	}
1856
1857	public function peekOnSameLine()
1858	{
1859		$this->scanNewlines = true;
1860		$tt = $this->peek();
1861		$this->scanNewlines = false;
1862
1863		return $tt;
1864	}
1865
1866	public function currentToken()
1867	{
1868		if (!empty($this->tokens))
1869			return $this->tokens[$this->tokenIndex];
1870	}
1871
1872	public function get($chunksize = 1000)
1873	{
1874		while($this->lookahead)
1875		{
1876			$this->lookahead--;
1877			$this->tokenIndex = ($this->tokenIndex + 1) & 3;
1878			$token = $this->tokens[$this->tokenIndex];
1879			if ($token->type != TOKEN_NEWLINE || $this->scanNewlines)
1880				return $token->type;
1881		}
1882
1883		$conditional_comment = false;
1884
1885		// strip whitespace and comments
1886		while(true)
1887		{
1888			$input = $this->getInput($chunksize);
1889
1890			// whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
1891			$re = $this->scanNewlines ? '/^[ \r\t]+/' : '/^\s+/';
1892			if (preg_match($re, $input, $match))
1893			{
1894				$spaces = $match[0];
1895				$spacelen = strlen($spaces);
1896				$this->cursor += $spacelen;
1897				if (!$this->scanNewlines)
1898					$this->lineno += substr_count($spaces, "\n");
1899
1900				if ($spacelen == $chunksize)
1901					continue; // complete chunk contained whitespace
1902
1903				$input = $this->getInput($chunksize);
1904				if ($input == '' || $input[0] != '/')
1905					break;
1906			}
1907
1908			// Comments
1909			if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match))
1910			{
1911				if (!$chunksize)
1912					break;
1913
1914				// retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
1915				$chunksize = null;
1916				continue;
1917			}
1918
1919			// check if this is a conditional (JScript) comment
1920			if (!empty($match[1]))
1921			{
1922				$match[0] = '/*' . $match[1];
1923				$conditional_comment = true;
1924				break;
1925			}
1926			else
1927			{
1928				$this->cursor += strlen($match[0]);
1929				$this->lineno += substr_count($match[0], "\n");
1930			}
1931		}
1932
1933		if ($input == '')
1934		{
1935			$tt = TOKEN_END;
1936			$match = array('');
1937		}
1938		elseif ($conditional_comment)
1939		{
1940			$tt = TOKEN_CONDCOMMENT_START;
1941		}
1942		else
1943		{
1944			switch ($input[0])
1945			{
1946				case '0':
1947					// hexadecimal
1948					if (($input[1] == 'x' || $input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match))
1949					{
1950						$tt = TOKEN_NUMBER;
1951						break;
1952					}
1953				// FALL THROUGH
1954
1955				case '1': case '2': case '3': case '4': case '5':
1956				case '6': case '7': case '8': case '9':
1957					// should always match
1958					preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match);
1959					$tt = TOKEN_NUMBER;
1960				break;
1961
1962				case "'":
1963					if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match))
1964					{
1965						$tt = TOKEN_STRING;
1966					}
1967					else
1968					{
1969						if ($chunksize)
1970							return $this->get(null); // retry with a full chunk fetch
1971
1972						throw $this->newSyntaxError('Unterminated string literal');
1973					}
1974				break;
1975
1976				case '"':
1977					if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match))
1978					{
1979						$tt = TOKEN_STRING;
1980					}
1981					else
1982					{
1983						if ($chunksize)
1984							return $this->get(null); // retry with a full chunk fetch
1985
1986						throw $this->newSyntaxError('Unterminated string literal');
1987					}
1988				break;
1989
1990				case '/':
1991					if ($this->scanOperand && preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
1992					{
1993						$tt = TOKEN_REGEXP;
1994						break;
1995					}
1996				// FALL THROUGH
1997
1998				case '|':
1999				case '^':
2000				case '&':
2001				case '<':
2002				case '>':
2003				case '+':
2004				case '-':
2005				case '*':
2006				case '%':
2007				case '=':
2008				case '!':
2009					// should always match
2010					preg_match($this->opRegExp, $input, $match);
2011					$op = $match[0];
2012					if (in_array($op, $this->assignOps) && $input[strlen($op)] == '=')
2013					{
2014						$tt = OP_ASSIGN;
2015						$match[0] .= '=';
2016					}
2017					else
2018					{
2019						$tt = $op;
2020						if ($this->scanOperand)
2021						{
2022							if ($op == OP_PLUS)
2023								$tt = OP_UNARY_PLUS;
2024							elseif ($op == OP_MINUS)
2025								$tt = OP_UNARY_MINUS;
2026						}
2027						$op = null;
2028					}
2029				break;
2030
2031				case '.':
2032					if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
2033					{
2034						$tt = TOKEN_NUMBER;
2035						break;
2036					}
2037				// FALL THROUGH
2038
2039				case ';':
2040				case ',':
2041				case '?':
2042				case ':':
2043				case '~':
2044				case '[':
2045				case ']':
2046				case '{':
2047				case '}':
2048				case '(':
2049				case ')':
2050					// these are all single
2051					$match = array($input[0]);
2052					$tt = $input[0];
2053				break;
2054
2055				case '@':
2056					// check end of conditional comment
2057					if (substr($input, 0, 3) == '@*/')
2058					{
2059						$match = array('@*/');
2060						$tt = TOKEN_CONDCOMMENT_END;
2061					}
2062					else
2063						throw $this->newSyntaxError('Illegal token');
2064				break;
2065
2066				case "\n":
2067					if ($this->scanNewlines)
2068					{
2069						$match = array("\n");
2070						$tt = TOKEN_NEWLINE;
2071					}
2072					else
2073						throw $this->newSyntaxError('Illegal token');
2074				break;
2075
2076				default:
2077					// Fast path for identifiers: word chars followed by whitespace or various other tokens.
2078					// Note we don't need to exclude digits in the first char, as they've already been found
2079					// above.
2080					if (!preg_match('/^[$\w]+(?=[\s\/\|\^\&<>\+\-\*%=!.;,\?:~\[\]\{\}\(\)@])/', $input, $match))
2081					{
2082						// Character classes per ECMA-262 edition 5.1 section 7.6
2083						// Per spec, must accept Unicode 3.0, *may* accept later versions.
2084						// We'll take whatever PCRE understands, which should be more recent.
2085						$identifierStartChars = "\\p{L}\\p{Nl}" .  # UnicodeLetter
2086						                        "\$" .
2087						                        "_";
2088						$identifierPartChars  = $identifierStartChars .
2089						                        "\\p{Mn}\\p{Mc}" . # UnicodeCombiningMark
2090						                        "\\p{Nd}" .        # UnicodeDigit
2091						                        "\\p{Pc}";         # UnicodeConnectorPunctuation
2092						$unicodeEscape = "\\\\u[0-9A-F-a-f]{4}";
2093						$identifierRegex = "/^" .
2094						                   "(?:[$identifierStartChars]|$unicodeEscape)" .
2095						                   "(?:[$identifierPartChars]|$unicodeEscape)*" .
2096						                   "/uS";
2097						if (preg_match($identifierRegex, $input, $match))
2098						{
2099							if (strpos($match[0], '\\') !== false) {
2100								// Per ECMA-262 edition 5.1, section 7.6 escape sequences should behave as if they were
2101								// the original chars, but only within the boundaries of the identifier.
2102								$decoded = preg_replace_callback('/\\\\u([0-9A-Fa-f]{4})/',
2103										array(__CLASS__, 'unicodeEscapeCallback'),
2104										$match[0]);
2105
2106								// Since our original regex didn't de-escape the originals, we need to check for validity again.
2107								// No need to worry about token boundaries, as anything outside the identifier is illegal!
2108								if (!preg_match("/^[$identifierStartChars][$identifierPartChars]*$/u", $decoded)) {
2109									throw $this->newSyntaxError('Illegal token');
2110								}
2111
2112								// Per spec it _ought_ to work to use these escapes for keywords words as well...
2113								// but IE rejects them as invalid, while Firefox and Chrome treat them as identifiers
2114								// that don't match the keyword.
2115								if (in_array($decoded, $this->keywords)) {
2116									throw $this->newSyntaxError('Illegal token');
2117								}
2118
2119								// TODO: save the decoded form for output?
2120							}
2121						}
2122						else
2123							throw $this->newSyntaxError('Illegal token');
2124					}
2125					$tt = in_array($match[0], $this->keywords) ? $match[0] : TOKEN_IDENTIFIER;
2126			}
2127		}
2128
2129		$this->tokenIndex = ($this->tokenIndex + 1) & 3;
2130
2131		if (!isset($this->tokens[$this->tokenIndex]))
2132			$this->tokens[$this->tokenIndex] = new JSToken();
2133
2134		$token = $this->tokens[$this->tokenIndex];
2135		$token->type = $tt;
2136
2137		if ($tt == OP_ASSIGN)
2138			$token->assignOp = $op;
2139
2140		$token->start = $this->cursor;
2141
2142		$token->value = $match[0];
2143		$this->cursor += strlen($match[0]);
2144
2145		$token->end = $this->cursor;
2146		$token->lineno = $this->lineno;
2147
2148		return $tt;
2149	}
2150
2151	public function unget()
2152	{
2153		if (++$this->lookahead == 4)
2154			throw $this->newSyntaxError('PANIC: too much lookahead!');
2155
2156		$this->tokenIndex = ($this->tokenIndex - 1) & 3;
2157	}
2158
2159	public function newSyntaxError($m)
2160	{
2161		return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename . '\' on line ' . $this->lineno);
2162	}
2163
2164	public static function unicodeEscapeCallback($m)
2165	{
2166		return html_entity_decode('&#x' . $m[1]. ';', ENT_QUOTES, 'UTF-8');
2167	}
2168}
2169
2170class JSToken
2171{
2172	public $type;
2173	public $value;
2174	public $start;
2175	public $end;
2176	public $lineno;
2177	public $assignOp;
2178}
2179