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