4 * JSMinPlus version 1.1
6 * Minifies a javascript file using a javascript parser
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
13 * Tino Zijdel <crisp@tweakers.net>
15 * Usage: $minified = JSMinPlus::minify($script [, $filename])
17 * Versionlog (see also changelog.txt):
18 * 12-04-2009 - some small bugfixes and performance improvements
19 * 09-04-2009 - initial open sourced version 1.0
21 * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip
25 /* ***** BEGIN LICENSE BLOCK *****
26 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
28 * The contents of this file are subject to the Mozilla Public License Version
29 * 1.1 (the "License"); you may not use this file except in compliance with
30 * the License. You may obtain a copy of the License at
31 * http://www.mozilla.org/MPL/
33 * Software distributed under the License is distributed on an "AS IS" basis,
34 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
35 * for the specific language governing rights and limitations under the
38 * The Original Code is the Narcissus JavaScript engine.
40 * The Initial Developer of the Original Code is
41 * Brendan Eich <brendan@mozilla.org>.
42 * Portions created by the Initial Developer are Copyright (C) 2004
43 * the Initial Developer. All Rights Reserved.
45 * Contributor(s): Tino Zijdel <crisp@tweakers.net>
46 * PHP port, modifications and minifier routine are (C) 2009
48 * Alternatively, the contents of this file may be used under the terms of
49 * either the GNU General Public License Version 2 or later (the "GPL"), or
50 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
51 * in which case the provisions of the GPL or the LGPL are applicable instead
52 * of those above. If you wish to allow use of your version of this file only
53 * under the terms of either the GPL or the LGPL, and not to allow others to
54 * use your version of this file under the terms of the MPL, indicate your
55 * decision by deleting the provisions above and replace them with the notice
56 * and other provisions required by the GPL or the LGPL. If you do not delete
57 * the provisions above, a recipient may use your version of this file under
58 * the terms of any one of the MPL, the GPL or the LGPL.
60 * ***** END LICENSE BLOCK ***** */
62 define('TOKEN_END', 1);
63 define('TOKEN_NUMBER', 2);
64 define('TOKEN_IDENTIFIER', 3);
65 define('TOKEN_STRING', 4);
66 define('TOKEN_REGEXP', 5);
67 define('TOKEN_NEWLINE', 6);
68 define('TOKEN_CONDCOMMENT_MULTILINE', 7);
70 define('JS_SCRIPT', 100);
71 define('JS_BLOCK', 101);
72 define('JS_LABEL', 102);
73 define('JS_FOR_IN', 103);
74 define('JS_CALL', 104);
75 define('JS_NEW_WITH_ARGS', 105);
76 define('JS_INDEX', 106);
77 define('JS_ARRAY_INIT', 107);
78 define('JS_OBJECT_INIT', 108);
79 define('JS_PROPERTY_INIT', 109);
80 define('JS_GETTER', 110);
81 define('JS_SETTER', 111);
82 define('JS_GROUP', 112);
83 define('JS_LIST', 113);
85 define('DECLARED_FORM', 0);
86 define('EXPRESSED_FORM', 1);
87 define('STATEMENT_FORM', 2);
92 private $reserved = array(
93 'break', 'case', 'catch', 'continue', 'default', 'delete', 'do',
94 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof',
95 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var',
96 'void', 'while', 'with',
97 // Words reserved for future use
98 'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger',
99 'double', 'enum', 'export', 'extends', 'final', 'float', 'goto',
100 'implements', 'import', 'int', 'interface', 'long', 'native',
101 'package', 'private', 'protected', 'public', 'short', 'static',
102 'super', 'synchronized', 'throws', 'transient', 'volatile',
103 // These are not reserved, but should be taken into account
104 // in isValidIdentifier (See jslint source code)
105 'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined'
108 private function __construct()
110 $this->parser = new JSParser();
113 public static function minify($js, $filename='')
117 // this is a singleton
119 $instance = new JSMinPlus();
121 return $instance->min($js, $filename);
124 private function min($js, $filename)
128 $n = $this->parser->parse($js, $filename, 1);
129 return $this->parseTree($n);
133 echo $e->getMessage() . "\n";
139 private function parseTree($n, $noBlockGrouping = false)
145 case KEYWORD_FUNCTION:
146 $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '(';
147 $params = $n->params;
148 for ($i = 0, $j = count($params); $i < $j; $i++)
149 $s .= ($i ? ',' : '') . $params[$i];
150 $s .= '){' . $this->parseTree($n->body, true) . '}';
154 // we do nothing with funDecls or varDecls
155 $noBlockGrouping = true;
158 $childs = $n->treeNodes;
159 for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++)
161 $t = $this->parseTree($childs[$i]);
166 if ($childs[$i]->type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM)
167 $s .= "\n"; // put declared functions on a new line
178 if ($c > 1 && !$noBlockGrouping)
185 $s = 'if(' . $this->parseTree($n->condition) . ')';
186 $thenPart = $this->parseTree($n->thenPart);
187 $elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null;
189 // quite a rancid hack to see if we should enclose the thenpart in brackets
190 if ($thenPart[0] != '{')
192 if (strpos($thenPart, 'if(') !== false)
193 $thenPart = '{' . $thenPart . '}';
204 if ($elsePart[0] != '{')
212 $s = 'switch(' . $this->parseTree($n->discriminant) . '){';
214 for ($i = 0, $j = count($cases); $i < $j; $i++)
217 if ($case->type == KEYWORD_CASE)
218 $s .= 'case' . ($case->caseLabel->type != TOKEN_STRING ? ' ' : '') . $this->parseTree($case->caseLabel) . ':';
222 $statement = $this->parseTree($case->statements);
224 $s .= $statement . ';';
226 $s = rtrim($s, ';') . '}';
230 $s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '')
231 . ';' . ($n->condition ? $this->parseTree($n->condition) : '')
232 . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')'
233 . $this->parseTree($n->body);
237 $s = 'while(' . $this->parseTree($n->condition) . ')' . $this->parseTree($n->body);
241 $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
245 $s = 'do{' . $this->parseTree($n->body, true) . '}while(' . $this->parseTree($n->condition) . ')';
249 case KEYWORD_CONTINUE:
250 $s = $n->value . ($n->label ? ' ' . $n->label : '');
254 $s = 'try{' . $this->parseTree($n->tryBlock, true) . '}';
255 $catchClauses = $n->catchClauses;
256 for ($i = 0, $j = count($catchClauses); $i < $j; $i++)
258 $t = $catchClauses[$i];
259 $s .= 'catch(' . $t->varName . ($t->guard ? ' if ' . $this->parseTree($t->guard) : '') . '){' . $this->parseTree($t->block, true) . '}';
261 if ($n->finallyBlock)
262 $s .= 'finally{' . $this->parseTree($n->finallyBlock, true) . '}';
266 $s = 'throw ' . $this->parseTree($n->exception);
270 $s = 'return' . ($n->value ? ' ' . $this->parseTree($n->value) : '');
274 $s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
279 $s = $n->value . ' ';
280 $childs = $n->treeNodes;
281 for ($i = 0, $j = count($childs); $i < $j; $i++)
284 $s .= ($i ? ',' : '') . $t->name;
285 $u = $t->initializer;
287 $s .= '=' . $this->parseTree($u);
291 case KEYWORD_DEBUGGER:
292 throw new Exception('NOT IMPLEMENTED: DEBUGGER');
295 case TOKEN_CONDCOMMENT_MULTILINE:
296 $s = $n->value . ' ';
297 $childs = $n->treeNodes;
298 for ($i = 0, $j = count($childs); $i < $j; $i++)
299 $s .= $this->parseTree($childs[$i]);
303 if ($expression = $n->expression)
304 $s = $this->parseTree($expression);
308 $s = $n->label . ':' . $this->parseTree($n->statement);
312 $childs = $n->treeNodes;
313 for ($i = 0, $j = count($childs); $i < $j; $i++)
314 $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
318 $s = $this->parseTree($n->treeNodes[0]) . $n->value . $this->parseTree($n->treeNodes[1]);
322 $s = $this->parseTree($n->treeNodes[0]) . '?' . $this->parseTree($n->treeNodes[1]) . ':' . $this->parseTree($n->treeNodes[2]);
325 case OP_OR: case OP_AND:
326 case OP_BITWISE_OR: case OP_BITWISE_XOR: case OP_BITWISE_AND:
327 case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
328 case OP_LT: case OP_LE: case OP_GE: case OP_GT:
329 case OP_LSH: case OP_RSH: case OP_URSH:
330 case OP_MUL: case OP_DIV: case OP_MOD:
331 $s = $this->parseTree($n->treeNodes[0]) . $n->type . $this->parseTree($n->treeNodes[1]);
336 $s = $this->parseTree($n->treeNodes[0]) . $n->type;
337 $nextTokenType = $n->treeNodes[1]->type;
338 if ( $nextTokenType == OP_PLUS || $nextTokenType == OP_MINUS ||
339 $nextTokenType == OP_INCREMENT || $nextTokenType == OP_DECREMENT ||
340 $nextTokenType == OP_UNARY_PLUS || $nextTokenType == OP_UNARY_MINUS
343 $s .= $this->parseTree($n->treeNodes[1]);
347 $s = $this->parseTree($n->treeNodes[0]) . ' in ' . $this->parseTree($n->treeNodes[1]);
350 case KEYWORD_INSTANCEOF:
351 $s = $this->parseTree($n->treeNodes[0]) . ' instanceof ' . $this->parseTree($n->treeNodes[1]);
355 $s = 'delete ' . $this->parseTree($n->treeNodes[0]);
359 $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')';
363 $s = 'typeof ' . $this->parseTree($n->treeNodes[0]);
370 $s = $n->value . $this->parseTree($n->treeNodes[0]);
376 $s = $this->parseTree($n->treeNodes[0]) . $n->value;
378 $s = $n->value . $this->parseTree($n->treeNodes[0]);
382 $s = $this->parseTree($n->treeNodes[0]) . '.' . $this->parseTree($n->treeNodes[1]);
386 $s = $this->parseTree($n->treeNodes[0]);
387 // See if we can replace named index with a dot saving 3 bytes
388 if ( $n->treeNodes[0]->type == TOKEN_IDENTIFIER &&
389 $n->treeNodes[1]->type == TOKEN_STRING &&
390 $this->isValidIdentifier(substr($n->treeNodes[1]->value, 1, -1))
392 $s .= '.' . substr($n->treeNodes[1]->value, 1, -1);
394 $s .= '[' . $this->parseTree($n->treeNodes[1]) . ']';
398 $childs = $n->treeNodes;
399 for ($i = 0, $j = count($childs); $i < $j; $i++)
400 $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
404 $s = $this->parseTree($n->treeNodes[0]) . '(' . $this->parseTree($n->treeNodes[1]) . ')';
408 case JS_NEW_WITH_ARGS:
409 $s = 'new ' . $this->parseTree($n->treeNodes[0]) . '(' . ($n->type == JS_NEW_WITH_ARGS ? $this->parseTree($n->treeNodes[1]) : '') . ')';
414 $childs = $n->treeNodes;
415 for ($i = 0, $j = count($childs); $i < $j; $i++)
417 $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
424 $childs = $n->treeNodes;
425 for ($i = 0, $j = count($childs); $i < $j; $i++)
430 if ($t->type == JS_PROPERTY_INIT)
432 // Ditch the quotes when the index is a valid identifier
433 if ( $t->treeNodes[0]->type == TOKEN_STRING &&
434 $this->isValidIdentifier(substr($t->treeNodes[0]->value, 1, -1))
436 $s .= substr($t->treeNodes[0]->value, 1, -1);
438 $s .= $t->treeNodes[0]->value;
440 $s .= ':' . $this->parseTree($t->treeNodes[1]);
444 $s .= $t->type == JS_GETTER ? 'get' : 'set';
445 $s .= ' ' . $t->name . '(';
446 $params = $t->params;
447 for ($i = 0, $j = count($params); $i < $j; $i++)
448 $s .= ($i ? ',' : '') . $params[$i];
449 $s .= '){' . $this->parseTree($t->body, true) . '}';
455 case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
456 case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
461 $s = '(' . $this->parseTree($n->treeNodes[0]) . ')';
465 throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type);
471 private function isValidIdentifier($string)
473 return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved);
481 private $opPrecedence = array(
484 '=' => 2, '?' => 2, ':' => 2,
485 // The above all have to have the same precedence, see bug 330975.
491 '==' => 9, '!=' => 9, '===' => 9, '!==' => 9,
492 '<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10,
493 '<<' => 11, '>>' => 11, '>>>' => 11,
494 '+' => 12, '-' => 12,
495 '*' => 13, '/' => 13, '%' => 13,
496 'delete' => 14, 'void' => 14, 'typeof' => 14,
497 '!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14,
498 '++' => 15, '--' => 15,
501 JS_NEW_WITH_ARGS => 0, JS_INDEX => 0, JS_CALL => 0,
502 JS_ARRAY_INIT => 0, JS_OBJECT_INIT => 0, JS_GROUP => 0
505 private $opArity = array(
514 '==' => 2, '!=' => 2, '===' => 2, '!==' => 2,
515 '<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2,
516 '<<' => 2, '>>' => 2, '>>>' => 2,
518 '*' => 2, '/' => 2, '%' => 2,
519 'delete' => 1, 'void' => 1, 'typeof' => 1,
520 '!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1,
521 '++' => 1, '--' => 1,
524 JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2,
525 JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1,
526 TOKEN_CONDCOMMENT_MULTILINE => 1
529 public function __construct()
531 $this->t = new JSTokenizer();
534 public function parse($s, $f, $l)
536 // initialize tokenizer
537 $this->t->init($s, $f, $l);
539 $x = new JSCompilerContext(false);
540 $n = $this->Script($x);
541 if (!$this->t->isDone())
542 throw $this->t->newSyntaxError('Syntax error');
547 private function Script($x)
549 $n = $this->Statements($x);
550 $n->type = JS_SCRIPT;
551 $n->funDecls = $x->funDecls;
552 $n->varDecls = $x->varDecls;
557 private function Statements($x)
559 $n = new JSNode($this->t, JS_BLOCK);
560 array_push($x->stmtStack, $n);
562 while (!$this->t->isDone() && $this->t->peek() != OP_RIGHT_CURLY)
563 $n->addNode($this->Statement($x));
565 array_pop($x->stmtStack);
570 private function Block($x)
572 $this->t->mustMatch(OP_LEFT_CURLY);
573 $n = $this->Statements($x);
574 $this->t->mustMatch(OP_RIGHT_CURLY);
579 private function Statement($x)
581 $tt = $this->t->get();
584 // Cases for statements ending in a right curly return early, avoiding the
585 // common semicolon insertion magic after this switch.
588 case KEYWORD_FUNCTION:
589 return $this->FunctionDefinition(
592 count($x->stmtStack) > 1 ? STATEMENT_FORM : DECLARED_FORM
597 $n = $this->Statements($x);
598 $this->t->mustMatch(OP_RIGHT_CURLY);
602 $n = new JSNode($this->t);
603 $n->condition = $this->ParenExpression($x);
604 array_push($x->stmtStack, $n);
605 $n->thenPart = $this->Statement($x);
606 $n->elsePart = $this->t->match(KEYWORD_ELSE) ? $this->Statement($x) : null;
607 array_pop($x->stmtStack);
611 $n = new JSNode($this->t);
612 $this->t->mustMatch(OP_LEFT_PAREN);
613 $n->discriminant = $this->Expression($x);
614 $this->t->mustMatch(OP_RIGHT_PAREN);
616 $n->defaultIndex = -1;
618 array_push($x->stmtStack, $n);
620 $this->t->mustMatch(OP_LEFT_CURLY);
622 while (($tt = $this->t->get()) != OP_RIGHT_CURLY)
626 case KEYWORD_DEFAULT:
627 if ($n->defaultIndex >= 0)
628 throw $this->t->newSyntaxError('More than one switch default');
631 $n2 = new JSNode($this->t);
632 if ($tt == KEYWORD_DEFAULT)
633 $n->defaultIndex = count($n->cases);
635 $n2->caseLabel = $this->Expression($x, OP_COLON);
638 throw $this->t->newSyntaxError('Invalid switch case');
641 $this->t->mustMatch(OP_COLON);
642 $n2->statements = new JSNode($this->t, JS_BLOCK);
643 while (($tt = $this->t->peek()) != KEYWORD_CASE && $tt != KEYWORD_DEFAULT && $tt != OP_RIGHT_CURLY)
644 $n2->statements->addNode($this->Statement($x));
646 array_push($n->cases, $n2);
649 array_pop($x->stmtStack);
653 $n = new JSNode($this->t);
655 $this->t->mustMatch(OP_LEFT_PAREN);
657 if (($tt = $this->t->peek()) != OP_SEMICOLON)
659 $x->inForLoopInit = true;
660 if ($tt == KEYWORD_VAR || $tt == KEYWORD_CONST)
663 $n2 = $this->Variables($x);
667 $n2 = $this->Expression($x);
669 $x->inForLoopInit = false;
672 if ($n2 && $this->t->match(KEYWORD_IN))
674 $n->type = JS_FOR_IN;
675 if ($n2->type == KEYWORD_VAR)
677 if (count($n2->treeNodes) != 1)
679 throw $this->t->SyntaxError(
680 'Invalid for..in left-hand side',
686 // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name.
687 $n->iterator = $n2->treeNodes[0];
696 $n->object = $this->Expression($x);
700 $n->setup = $n2 ? $n2 : null;
701 $this->t->mustMatch(OP_SEMICOLON);
702 $n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x);
703 $this->t->mustMatch(OP_SEMICOLON);
704 $n->update = $this->t->peek() == OP_RIGHT_PAREN ? null : $this->Expression($x);
707 $this->t->mustMatch(OP_RIGHT_PAREN);
708 $n->body = $this->nest($x, $n);
712 $n = new JSNode($this->t);
714 $n->condition = $this->ParenExpression($x);
715 $n->body = $this->nest($x, $n);
719 $n = new JSNode($this->t);
721 $n->body = $this->nest($x, $n, KEYWORD_WHILE);
722 $n->condition = $this->ParenExpression($x);
723 if (!$x->ecmaStrictMode)
725 // <script language="JavaScript"> (without version hints) may need
726 // automatic semicolon insertion without a newline after do-while.
727 // See http://bugzilla.mozilla.org/show_bug.cgi?id=238945.
728 $this->t->match(OP_SEMICOLON);
734 case KEYWORD_CONTINUE:
735 $n = new JSNode($this->t);
737 if ($this->t->peekOnSameLine() == TOKEN_IDENTIFIER)
740 $n->label = $this->t->currentToken()->value;
751 throw $this->t->newSyntaxError('Label not found');
753 while ($ss[$i]->label != $label);
760 throw $this->t->newSyntaxError('Invalid ' . $tt);
762 while (!$ss[$i]->isLoop && ($tt != KEYWORD_BREAK || $ss[$i]->type != KEYWORD_SWITCH));
765 $n->target = $ss[$i];
769 $n = new JSNode($this->t);
770 $n->tryBlock = $this->Block($x);
771 $n->catchClauses = array();
773 while ($this->t->match(KEYWORD_CATCH))
775 $n2 = new JSNode($this->t);
776 $this->t->mustMatch(OP_LEFT_PAREN);
777 $n2->varName = $this->t->mustMatch(TOKEN_IDENTIFIER)->value;
779 if ($this->t->match(KEYWORD_IF))
781 if ($x->ecmaStrictMode)
782 throw $this->t->newSyntaxError('Illegal catch guard');
784 if (count($n->catchClauses) && !end($n->catchClauses)->guard)
785 throw $this->t->newSyntaxError('Guarded catch after unguarded');
787 $n2->guard = $this->Expression($x);
794 $this->t->mustMatch(OP_RIGHT_PAREN);
795 $n2->block = $this->Block($x);
796 array_push($n->catchClauses, $n2);
799 if ($this->t->match(KEYWORD_FINALLY))
800 $n->finallyBlock = $this->Block($x);
802 if (!count($n->catchClauses) && !$n->finallyBlock)
803 throw $this->t->newSyntaxError('Invalid try statement');
807 case KEYWORD_FINALLY:
808 throw $this->t->newSyntaxError($tt + ' without preceding try');
811 $n = new JSNode($this->t);
812 $n->exception = $this->Expression($x);
817 throw $this->t->newSyntaxError('Invalid return');
819 $n = new JSNode($this->t);
820 $tt = $this->t->peekOnSameLine();
821 if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
822 $n->value = $this->Expression($x);
828 $n = new JSNode($this->t);
829 $n->object = $this->ParenExpression($x);
830 $n->body = $this->nest($x, $n);
835 $n = $this->Variables($x);
838 case TOKEN_CONDCOMMENT_MULTILINE:
839 $n = new JSNode($this->t);
842 case KEYWORD_DEBUGGER:
843 $n = new JSNode($this->t);
848 $n = new JSNode($this->t, OP_SEMICOLON);
849 $n->expression = null;
853 if ($tt == TOKEN_IDENTIFIER)
855 $this->t->scanOperand = false;
856 $tt = $this->t->peek();
857 $this->t->scanOperand = true;
860 $label = $this->t->currentToken()->value;
862 for ($i = count($ss) - 1; $i >= 0; --$i)
864 if ($ss[$i]->label == $label)
865 throw $this->t->newSyntaxError('Duplicate label');
869 $n = new JSNode($this->t, JS_LABEL);
871 $n->statement = $this->nest($x, $n);
877 $n = new JSNode($this->t, OP_SEMICOLON);
879 $n->expression = $this->Expression($x);
880 $n->end = $n->expression->end;
884 if ($this->t->lineno == $this->t->currentToken()->lineno)
886 $tt = $this->t->peekOnSameLine();
887 if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
888 throw $this->t->newSyntaxError('Missing ; before statement');
891 $this->t->match(OP_SEMICOLON);
896 private function FunctionDefinition($x, $requireName, $functionForm)
898 $f = new JSNode($this->t);
900 if ($f->type != KEYWORD_FUNCTION)
901 $f->type = ($f->value == 'get') ? JS_GETTER : JS_SETTER;
903 if ($this->t->match(TOKEN_IDENTIFIER))
904 $f->name = $this->t->currentToken()->value;
905 elseif ($requireName)
906 throw $this->t->newSyntaxError('Missing function identifier');
908 $this->t->mustMatch(OP_LEFT_PAREN);
909 $f->params = array();
911 while (($tt = $this->t->get()) != OP_RIGHT_PAREN)
913 if ($tt != TOKEN_IDENTIFIER)
914 throw $this->t->newSyntaxError('Missing formal parameter');
916 array_push($f->params, $this->t->currentToken()->value);
918 if ($this->t->peek() != OP_RIGHT_PAREN)
919 $this->t->mustMatch(OP_COMMA);
922 $this->t->mustMatch(OP_LEFT_CURLY);
924 $x2 = new JSCompilerContext(true);
925 $f->body = $this->Script($x2);
927 $this->t->mustMatch(OP_RIGHT_CURLY);
928 $f->end = $this->t->currentToken()->end;
930 $f->functionForm = $functionForm;
931 if ($functionForm == DECLARED_FORM)
932 array_push($x->funDecls, $f);
937 private function Variables($x)
939 $n = new JSNode($this->t);
943 $this->t->mustMatch(TOKEN_IDENTIFIER);
945 $n2 = new JSNode($this->t);
946 $n2->name = $n2->value;
948 if ($this->t->match(OP_ASSIGN))
950 if ($this->t->currentToken()->assignOp)
951 throw $this->t->newSyntaxError('Invalid variable initialization');
953 $n2->initializer = $this->Expression($x, OP_COMMA);
956 $n2->readOnly = $n->type == KEYWORD_CONST;
959 array_push($x->varDecls, $n2);
961 while ($this->t->match(OP_COMMA));
966 private function Expression($x, $stop=false)
968 $operators = array();
972 $bl = $x->bracketLevel;
973 $cl = $x->curlyLevel;
974 $pl = $x->parenLevel;
977 while (($tt = $this->t->get()) != TOKEN_END)
980 $x->bracketLevel == $bl &&
981 $x->curlyLevel == $cl &&
982 $x->parenLevel == $pl &&
986 // Stop only if tt matches the optional stop parameter, and that
987 // token is not quoted by some kind of bracket.
994 // NB: cannot be empty, Statement handled that.
1000 if ($this->t->scanOperand)
1003 // Use >, not >=, for right-associative ASSIGN and HOOK/COLON.
1004 while ( !empty($operators) &&
1005 ( $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] ||
1006 ($tt == OP_COLON && end($operators)->type == OP_ASSIGN)
1009 $this->reduce($operators, $operands);
1011 if ($tt == OP_COLON)
1013 $n = end($operators);
1014 if ($n->type != OP_HOOK)
1015 throw $this->t->newSyntaxError('Invalid label');
1021 array_push($operators, new JSNode($this->t));
1022 if ($tt == OP_ASSIGN)
1023 end($operands)->assignOp = $this->t->currentToken()->assignOp;
1028 $this->t->scanOperand = true;
1032 // An in operator should not be parsed if we're parsing the head of
1033 // a for (...) loop, unless it is in the then part of a conditional
1034 // expression, or parenthesized somehow.
1035 if ($x->inForLoopInit && !$x->hookLevel &&
1036 !$x->bracketLevel && !$x->curlyLevel &&
1044 // Treat comma as left-associative so reduce can fold left-heavy
1045 // COMMA trees into a single array.
1050 case OP_BITWISE_XOR:
1051 case OP_BITWISE_AND:
1052 case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
1053 case OP_LT: case OP_LE: case OP_GE: case OP_GT:
1054 case KEYWORD_INSTANCEOF:
1055 case OP_LSH: case OP_RSH: case OP_URSH:
1056 case OP_PLUS: case OP_MINUS:
1057 case OP_MUL: case OP_DIV: case OP_MOD:
1059 if ($this->t->scanOperand)
1062 while ( !empty($operators) &&
1063 $this->opPrecedence[end($operators)->type] >= $this->opPrecedence[$tt]
1065 $this->reduce($operators, $operands);
1069 $this->t->mustMatch(TOKEN_IDENTIFIER);
1070 array_push($operands, new JSNode($this->t, OP_DOT, array_pop($operands), new JSNode($this->t)));
1074 array_push($operators, new JSNode($this->t));
1075 $this->t->scanOperand = true;
1079 case KEYWORD_DELETE: case KEYWORD_VOID: case KEYWORD_TYPEOF:
1080 case OP_NOT: case OP_BITWISE_NOT: case OP_UNARY_PLUS: case OP_UNARY_MINUS:
1082 if (!$this->t->scanOperand)
1085 array_push($operators, new JSNode($this->t));
1088 case OP_INCREMENT: case OP_DECREMENT:
1089 if ($this->t->scanOperand)
1091 array_push($operators, new JSNode($this->t)); // prefix increment or decrement
1095 // Don't cross a line boundary for postfix {in,de}crement.
1096 $t = $this->t->tokens[($this->t->tokenIndex + $this->t->lookahead - 1) & 3];
1097 if ($t && $t->lineno != $this->t->lineno)
1100 if (!empty($operators))
1102 // Use >, not >=, so postfix has higher precedence than prefix.
1103 while ($this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt])
1104 $this->reduce($operators, $operands);
1107 $n = new JSNode($this->t, $tt, array_pop($operands));
1109 array_push($operands, $n);
1113 case KEYWORD_FUNCTION:
1114 if (!$this->t->scanOperand)
1117 array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM));
1118 $this->t->scanOperand = false;
1121 case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
1122 case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
1123 if (!$this->t->scanOperand)
1126 array_push($operands, new JSNode($this->t));
1127 $this->t->scanOperand = false;
1130 case TOKEN_CONDCOMMENT_MULTILINE:
1131 if ($this->t->scanOperand)
1132 array_push($operators, new JSNode($this->t));
1134 array_push($operands, new JSNode($this->t));
1137 case OP_LEFT_BRACKET:
1138 if ($this->t->scanOperand)
1140 // Array initialiser. Parse using recursive descent, as the
1141 // sub-grammar here is not an operator grammar.
1142 $n = new JSNode($this->t, JS_ARRAY_INIT);
1143 while (($tt = $this->t->peek()) != OP_RIGHT_BRACKET)
1145 if ($tt == OP_COMMA)
1152 $n->addNode($this->Expression($x, OP_COMMA));
1153 if (!$this->t->match(OP_COMMA))
1157 $this->t->mustMatch(OP_RIGHT_BRACKET);
1158 array_push($operands, $n);
1159 $this->t->scanOperand = false;
1163 // Property indexing operator.
1164 array_push($operators, new JSNode($this->t, JS_INDEX));
1165 $this->t->scanOperand = true;
1170 case OP_RIGHT_BRACKET:
1171 if ($this->t->scanOperand || $x->bracketLevel == $bl)
1174 while ($this->reduce($operators, $operands)->type != JS_INDEX)
1181 if (!$this->t->scanOperand)
1184 // Object initialiser. As for array initialisers (see above),
1185 // parse using recursive descent.
1187 $n = new JSNode($this->t, JS_OBJECT_INIT);
1188 while (!$this->t->match(OP_RIGHT_CURLY))
1192 $tt = $this->t->get();
1193 $tv = $this->t->currentToken()->value;
1194 if (($tv == 'get' || $tv == 'set') && $this->t->peek() == TOKEN_IDENTIFIER)
1196 if ($x->ecmaStrictMode)
1197 throw $this->t->newSyntaxError('Illegal property accessor');
1199 $n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM));
1205 case TOKEN_IDENTIFIER:
1208 $id = new JSNode($this->t);
1211 case OP_RIGHT_CURLY:
1212 if ($x->ecmaStrictMode)
1213 throw $this->t->newSyntaxError('Illegal trailing ,');
1217 throw $this->t->newSyntaxError('Invalid property name');
1220 $this->t->mustMatch(OP_COLON);
1221 $n->addNode(new JSNode($this->t, JS_PROPERTY_INIT, $id, $this->Expression($x, OP_COMMA)));
1224 while ($this->t->match(OP_COMMA));
1226 $this->t->mustMatch(OP_RIGHT_CURLY);
1230 array_push($operands, $n);
1231 $this->t->scanOperand = false;
1235 case OP_RIGHT_CURLY:
1236 if (!$this->t->scanOperand && $x->curlyLevel != $cl)
1237 throw new Exception('PANIC: right curly botch');
1241 if ($this->t->scanOperand)
1243 array_push($operators, new JSNode($this->t, JS_GROUP));
1247 while ( !empty($operators) &&
1248 $this->opPrecedence[end($operators)->type] > $this->opPrecedence[KEYWORD_NEW]
1250 $this->reduce($operators, $operands);
1252 // Handle () now, to regularize the n-ary case for n > 0.
1253 // We must set scanOperand in case there are arguments and
1254 // the first one is a regexp or unary+/-.
1255 $n = end($operators);
1256 $this->t->scanOperand = true;
1257 if ($this->t->match(OP_RIGHT_PAREN))
1259 if ($n && $n->type == KEYWORD_NEW)
1261 array_pop($operators);
1262 $n->addNode(array_pop($operands));
1266 $n = new JSNode($this->t, JS_CALL, array_pop($operands), new JSNode($this->t, JS_LIST));
1269 array_push($operands, $n);
1270 $this->t->scanOperand = false;
1274 if ($n && $n->type == KEYWORD_NEW)
1275 $n->type = JS_NEW_WITH_ARGS;
1277 array_push($operators, new JSNode($this->t, JS_CALL));
1283 case OP_RIGHT_PAREN:
1284 if ($this->t->scanOperand || $x->parenLevel == $pl)
1287 while (($tt = $this->reduce($operators, $operands)->type) != JS_GROUP &&
1288 $tt != JS_CALL && $tt != JS_NEW_WITH_ARGS
1294 if ($tt != JS_GROUP)
1296 $n = end($operands);
1297 if ($n->treeNodes[1]->type != OP_COMMA)
1298 $n->treeNodes[1] = new JSNode($this->t, JS_LIST, $n->treeNodes[1]);
1300 $n->treeNodes[1]->type = JS_LIST;
1306 // Automatic semicolon insertion means we may scan across a newline
1307 // and into the beginning of another statement. If so, break out of
1308 // the while loop and let the t.scanOperand logic handle errors.
1314 if ($x->hookLevel != $hl)
1315 throw $this->t->newSyntaxError('Missing : after ?');
1317 if ($x->parenLevel != $pl)
1318 throw $this->t->newSyntaxError('Missing ) in parenthetical');
1320 if ($x->bracketLevel != $bl)
1321 throw $this->t->newSyntaxError('Missing ] in index expression');
1323 if ($this->t->scanOperand)
1324 throw $this->t->newSyntaxError('Missing operand');
1326 // Resume default mode, scanning for operands, not operators.
1327 $this->t->scanOperand = true;
1330 while (count($operators))
1331 $this->reduce($operators, $operands);
1333 return array_pop($operands);
1336 private function ParenExpression($x)
1338 $this->t->mustMatch(OP_LEFT_PAREN);
1339 $n = $this->Expression($x);
1340 $this->t->mustMatch(OP_RIGHT_PAREN);
1345 // Statement stack and nested statement handler.
1346 private function nest($x, $node, $end = false)
1348 array_push($x->stmtStack, $node);
1349 $n = $this->statement($x);
1350 array_pop($x->stmtStack);
1353 $this->t->mustMatch($end);
1358 private function reduce(&$operators, &$operands)
1360 $n = array_pop($operators);
1362 $arity = $this->opArity[$op];
1363 $c = count($operands);
1366 // Flatten left-associative trees
1369 $left = $operands[$c - 2];
1370 if ($left->type == $op)
1372 $right = array_pop($operands);
1373 $left->addNode($right);
1380 // Always use push to add operands to n, to update start and end
1381 $a = array_splice($operands, $c - $arity);
1382 for ($i = 0; $i < $arity; $i++)
1383 $n->addNode($a[$i]);
1385 // Include closing bracket or postfix operator in [start,end]
1386 $te = $this->t->currentToken()->end;
1390 array_push($operands, $n);
1396 class JSCompilerContext
1398 public $inFunction = false;
1399 public $inForLoopInit = false;
1400 public $ecmaStrictMode = false;
1401 public $bracketLevel = 0;
1402 public $curlyLevel = 0;
1403 public $parenLevel = 0;
1404 public $hookLevel = 0;
1406 public $stmtStack = array();
1407 public $funDecls = array();
1408 public $varDecls = array();
1410 public function __construct($inFunction)
1412 $this->inFunction = $inFunction;
1424 public $treeNodes = array();
1425 public $funDecls = array();
1426 public $varDecls = array();
1428 public function __construct($t, $type=0)
1430 if ($token = $t->currentToken())
1432 $this->type = $type ? $type : $token->type;
1433 $this->value = $token->value;
1434 $this->lineno = $token->lineno;
1435 $this->start = $token->start;
1436 $this->end = $token->end;
1440 $this->type = $type;
1441 $this->lineno = $t->lineno;
1444 if (($numargs = func_num_args()) > 2)
1446 $args = func_get_args();;
1447 for ($i = 2; $i < $numargs; $i++)
1448 $this->addNode($args[$i]);
1452 // we don't want to bloat our object with all kind of specific properties, so we use overloading
1453 public function __set($name, $value)
1455 $this->$name = $value;
1458 public function __get($name)
1460 if (isset($this->$name))
1461 return $this->$name;
1466 public function addNode($node)
1468 $this->treeNodes[] = $node;
1474 private $cursor = 0;
1477 public $tokens = array();
1478 public $tokenIndex = 0;
1479 public $lookahead = 0;
1480 public $scanNewlines = false;
1481 public $scanOperand = true;
1486 private $keywords = array(
1488 'case', 'catch', 'const', 'continue',
1489 'debugger', 'default', 'delete', 'do',
1491 'false', 'finally', 'for', 'function',
1492 'if', 'in', 'instanceof',
1496 'this', 'throw', 'true', 'try', 'typeof',
1501 private $opTypeNames = array(
1508 '|' => 'BITWISE_OR',
1509 '^' => 'BITWISE_XOR',
1510 '&' => 'BITWISE_AND',
1511 '===' => 'STRICT_EQ',
1514 '!==' => 'STRICT_NE',
1523 '++' => 'INCREMENT',
1524 '--' => 'DECREMENT',
1531 '~' => 'BITWISE_NOT',
1533 '[' => 'LEFT_BRACKET',
1534 ']' => 'RIGHT_BRACKET',
1535 '{' => 'LEFT_CURLY',
1536 '}' => 'RIGHT_CURLY',
1537 '(' => 'LEFT_PAREN',
1538 ')' => 'RIGHT_PAREN',
1539 '@*/' => 'CONDCOMMENT_END'
1542 private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
1545 public function __construct()
1547 $this->opRegExp = '#^(' . implode('|', array_map('preg_quote', array_keys($this->opTypeNames))) . ')#';
1549 // this is quite a hidden yet convenient place to create the defines for operators and keywords
1550 foreach ($this->opTypeNames as $operand => $name)
1551 define('OP_' . $name, $operand);
1553 define('OP_UNARY_PLUS', 'U+');
1554 define('OP_UNARY_MINUS', 'U-');
1556 foreach ($this->keywords as $keyword)
1557 define('KEYWORD_' . strtoupper($keyword), $keyword);
1560 public function init($source, $filename = '', $lineno = 1)
1562 $this->source = $source;
1563 $this->filename = $filename ? $filename : '[inline]';
1564 $this->lineno = $lineno;
1567 $this->tokens = array();
1568 $this->tokenIndex = 0;
1569 $this->lookahead = 0;
1570 $this->scanNewlines = false;
1571 $this->scanOperand = true;
1574 public function getInput($chunksize)
1577 return substr($this->source, $this->cursor, $chunksize);
1579 return substr($this->source, $this->cursor);
1582 public function isDone()
1584 return $this->peek() == TOKEN_END;
1587 public function match($tt)
1589 return $this->get() == $tt || $this->unget();
1592 public function mustMatch($tt)
1594 if (!$this->match($tt))
1595 throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
1597 return $this->currentToken();
1600 public function peek()
1602 if ($this->lookahead)
1604 $next = $this->tokens[($this->tokenIndex + $this->lookahead) & 3];
1605 if ($this->scanNewlines && $next->lineno != $this->lineno)
1606 $tt = TOKEN_NEWLINE;
1619 public function peekOnSameLine()
1621 $this->scanNewlines = true;
1622 $tt = $this->peek();
1623 $this->scanNewlines = false;
1628 public function currentToken()
1630 if (!empty($this->tokens))
1631 return $this->tokens[$this->tokenIndex];
1634 public function get($chunksize = 1000)
1636 while($this->lookahead)
1639 $this->tokenIndex = ($this->tokenIndex + 1) & 3;
1640 $token = $this->tokens[$this->tokenIndex];
1641 if ($token->type != TOKEN_NEWLINE || $this->scanNewlines)
1642 return $token->type;
1645 $conditional_comment = false;
1647 // strip whitespace and comments
1650 $input = $this->getInput($chunksize);
1652 // whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
1653 $re = $this->scanNewlines ? '/^[ \r\t]+/' : '/^\s+/';
1654 if (preg_match($re, $input, $match))
1656 $spaces = $match[0];
1657 $spacelen = strlen($spaces);
1658 $this->cursor += $spacelen;
1659 if (!$this->scanNewlines)
1660 $this->lineno += substr_count($spaces, "\n");
1662 if ($spacelen == $chunksize)
1663 continue; // complete chunk contained whitespace
1665 $input = $this->getInput($chunksize);
1666 if ($input == '' || $input[0] != '/')
1671 if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?(?:.|\n)*?\*\/|\/.*)/', $input, $match))
1676 // retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
1681 // check if this is a conditional (JScript) comment
1682 if (!empty($match[1]))
1684 //$match[0] = '/*' . $match[1];
1685 $conditional_comment = true;
1690 $this->cursor += strlen($match[0]);
1691 $this->lineno += substr_count($match[0], "\n");
1700 elseif ($conditional_comment)
1702 $tt = TOKEN_CONDCOMMENT_MULTILINE;
1708 case '0': case '1': case '2': case '3': case '4':
1709 case '5': case '6': case '7': case '8': case '9':
1710 if (preg_match('/^\d+\.\d*(?:[eE][-+]?\d+)?|^\d+(?:\.\d*)?[eE][-+]?\d+/', $input, $match))
1714 elseif (preg_match('/^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/', $input, $match))
1716 // this should always match because of \d+
1723 if (preg_match('/^"(?:\\\\(?:.|\r?\n)|[^\\\\"\r\n])*"|^\'(?:\\\\(?:.|\r?\n)|[^\\\\\'\r\n])*\'/', $input, $match))
1730 return $this->get(null); // retry with a full chunk fetch
1732 throw $this->newSyntaxError('Unterminated string literal');
1737 if ($this->scanOperand && preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
1755 // should always match
1756 preg_match($this->opRegExp, $input, $match);
1758 if (in_array($op, $this->assignOps) && $input[strlen($op)] == '=')
1766 if ($this->scanOperand)
1769 $tt = OP_UNARY_PLUS;
1770 elseif ($op == OP_MINUS)
1771 $tt = OP_UNARY_MINUS;
1778 if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
1796 // these are all single
1797 $match = array($input[0]);
1802 throw $this->newSyntaxError('Illegal token');
1806 if ($this->scanNewlines)
1808 $match = array("\n");
1809 $tt = TOKEN_NEWLINE;
1812 throw $this->newSyntaxError('Illegal token');
1816 // FIXME: add support for unicode and unicode escape sequence \uHHHH
1817 if (preg_match('/^[$\w]+/', $input, $match))
1819 $tt = in_array($match[0], $this->keywords) ? $match[0] : TOKEN_IDENTIFIER;
1822 throw $this->newSyntaxError('Illegal token');
1826 $this->tokenIndex = ($this->tokenIndex + 1) & 3;
1828 if (!isset($this->tokens[$this->tokenIndex]))
1829 $this->tokens[$this->tokenIndex] = new JSToken();
1831 $token = $this->tokens[$this->tokenIndex];
1834 if ($tt == OP_ASSIGN)
1835 $token->assignOp = $op;
1837 $token->start = $this->cursor;
1839 $token->value = $match[0];
1840 $this->cursor += strlen($match[0]);
1842 $token->end = $this->cursor;
1843 $token->lineno = $this->lineno;
1848 public function unget()
1850 if (++$this->lookahead == 4)
1851 throw $this->newSyntaxError('PANIC: too much lookahead!');
1853 $this->tokenIndex = ($this->tokenIndex - 1) & 3;
1856 public function newSyntaxError($m)
1858 return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename . '\' on line ' . $this->lineno);