3 * JSMinPlus version 1.4
5 * Minifies a javascript file using a javascript parser
7 * This implements a PHP port of Brendan Eich's Narcissus open source javascript engine (in javascript)
8 * References: http://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine)
9 * Narcissus sourcecode: http://mxr.mozilla.org/mozilla/source/js/narcissus/
10 * JSMinPlus weblog: http://crisp.tweakblogs.net/blog/cat/716
12 * Tino Zijdel <crisp@tweakers.net>
14 * Usage: $minified = JSMinPlus::minify($script [, $filename])
16 * Versionlog (see also changelog.txt):
17 * 23-07-2011 - remove dynamic creation of OP_* and KEYWORD_* defines and declare them on top
18 * reduce memory footprint by minifying by block-scope
19 * some small byte-saving and performance improvements
20 * 12-05-2009 - fixed hook:colon precedence, fixed empty body in loop and if-constructs
21 * 18-04-2009 - fixed crashbug in PHP 5.2.9 and several other bugfixes
22 * 12-04-2009 - some small bugfixes and performance improvements
23 * 09-04-2009 - initial open sourced version 1.0
25 * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip
30 /* ***** BEGIN LICENSE BLOCK *****
31 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
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/
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
43 * The Original Code is the Narcissus JavaScript engine.
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.
50 * Contributor(s): Tino Zijdel <crisp@tweakers.net>
51 * PHP port, modifications and minifier routine are (C) 2009-2011
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.
65 * ***** END LICENSE BLOCK ***** */
67 define('TOKEN_END', 1);
68 define('TOKEN_NUMBER', 2);
69 define('TOKEN_IDENTIFIER', 3);
70 define('TOKEN_STRING', 4);
71 define('TOKEN_REGEXP', 5);
72 define('TOKEN_NEWLINE', 6);
73 define('TOKEN_CONDCOMMENT_START', 7);
74 define('TOKEN_CONDCOMMENT_END', 8);
76 define('JS_SCRIPT', 100);
77 define('JS_BLOCK', 101);
78 define('JS_LABEL', 102);
79 define('JS_FOR_IN', 103);
80 define('JS_CALL', 104);
81 define('JS_NEW_WITH_ARGS', 105);
82 define('JS_INDEX', 106);
83 define('JS_ARRAY_INIT', 107);
84 define('JS_OBJECT_INIT', 108);
85 define('JS_PROPERTY_INIT', 109);
86 define('JS_GETTER', 110);
87 define('JS_SETTER', 111);
88 define('JS_GROUP', 112);
89 define('JS_LIST', 113);
91 define('JS_MINIFIED', 999);
93 define('DECLARED_FORM', 0);
94 define('EXPRESSED_FORM', 1);
95 define('STATEMENT_FORM', 2);
98 define('OP_SEMICOLON', ';');
99 define('OP_COMMA', ',');
100 define('OP_HOOK', '?');
101 define('OP_COLON', ':');
102 define('OP_OR', '||');
103 define('OP_AND', '&&');
104 define('OP_BITWISE_OR', '|');
105 define('OP_BITWISE_XOR', '^');
106 define('OP_BITWISE_AND', '&');
107 define('OP_STRICT_EQ', '===');
108 define('OP_EQ', '==');
109 define('OP_ASSIGN', '=');
110 define('OP_STRICT_NE', '!==');
111 define('OP_NE', '!=');
112 define('OP_LSH', '<<');
113 define('OP_LE', '<=');
114 define('OP_LT', '<');
115 define('OP_URSH', '>>>');
116 define('OP_RSH', '>>');
117 define('OP_GE', '>=');
118 define('OP_GT', '>');
119 define('OP_INCREMENT', '++');
120 define('OP_DECREMENT', '--');
121 define('OP_PLUS', '+');
122 define('OP_MINUS', '-');
123 define('OP_MUL', '*');
124 define('OP_DIV', '/');
125 define('OP_MOD', '%');
126 define('OP_NOT', '!');
127 define('OP_BITWISE_NOT', '~');
128 define('OP_DOT', '.');
129 define('OP_LEFT_BRACKET', '[');
130 define('OP_RIGHT_BRACKET', ']');
131 define('OP_LEFT_CURLY', '{');
132 define('OP_RIGHT_CURLY', '}');
133 define('OP_LEFT_PAREN', '(');
134 define('OP_RIGHT_PAREN', ')');
135 define('OP_CONDCOMMENT_END', '@*/');
137 define('OP_UNARY_PLUS', 'U+');
138 define('OP_UNARY_MINUS', 'U-');
141 define('KEYWORD_BREAK', 'break');
142 define('KEYWORD_CASE', 'case');
143 define('KEYWORD_CATCH', 'catch');
144 define('KEYWORD_CONST', 'const');
145 define('KEYWORD_CONTINUE', 'continue');
146 define('KEYWORD_DEBUGGER', 'debugger');
147 define('KEYWORD_DEFAULT', 'default');
148 define('KEYWORD_DELETE', 'delete');
149 define('KEYWORD_DO', 'do');
150 define('KEYWORD_ELSE', 'else');
151 define('KEYWORD_ENUM', 'enum');
152 define('KEYWORD_FALSE', 'false');
153 define('KEYWORD_FINALLY', 'finally');
154 define('KEYWORD_FOR', 'for');
155 define('KEYWORD_FUNCTION', 'function');
156 define('KEYWORD_IF', 'if');
157 define('KEYWORD_IN', 'in');
158 define('KEYWORD_INSTANCEOF', 'instanceof');
159 define('KEYWORD_NEW', 'new');
160 define('KEYWORD_NULL', 'null');
161 define('KEYWORD_RETURN', 'return');
162 define('KEYWORD_SWITCH', 'switch');
163 define('KEYWORD_THIS', 'this');
164 define('KEYWORD_THROW', 'throw');
165 define('KEYWORD_TRUE', 'true');
166 define('KEYWORD_TRY', 'try');
167 define('KEYWORD_TYPEOF', 'typeof');
168 define('KEYWORD_VAR', 'var');
169 define('KEYWORD_VOID', 'void');
170 define('KEYWORD_WHILE', 'while');
171 define('KEYWORD_WITH', 'with');
177 private $reserved = array(
178 'break', 'case', 'catch', 'continue', 'default', 'delete', 'do',
179 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof',
180 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var',
181 'void', 'while', 'with',
182 // Words reserved for future use
183 'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger',
184 'double', 'enum', 'export', 'extends', 'final', 'float', 'goto',
185 'implements', 'import', 'int', 'interface', 'long', 'native',
186 'package', 'private', 'protected', 'public', 'short', 'static',
187 'super', 'synchronized', 'throws', 'transient', 'volatile',
188 // These are not reserved, but should be taken into account
189 // in isValidIdentifier (See jslint source code)
190 'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined'
193 private function __construct()
195 $this->parser
= new JSParser($this);
198 public static function minify($js, $filename='')
202 // this is a singleton
204 $instance = new JSMinPlus();
206 return $instance->min($js, $filename);
209 private function min($js, $filename)
213 $n = $this->parser
->parse($js, $filename, 1);
214 return $this->parseTree($n);
218 echo $e->getMessage() . "\n";
224 public function parseTree($n, $noBlockGrouping = false)
235 // we do nothing yet with funDecls or varDecls
236 $noBlockGrouping = true;
240 $childs = $n->treeNodes
;
242 for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++
)
244 $type = $childs[$i]->type
;
245 $t = $this->parseTree($childs[$i]);
252 if ($type == KEYWORD_FUNCTION
&& $childs[$i]->functionForm
== DECLARED_FORM
)
254 // put declared functions on a new line
257 elseif ($type == KEYWORD_VAR
&& $type == $lastType)
259 // multiple var-statements can go into one
260 $t = ',' . substr($t, 4);
276 if ($c > 1 && !$noBlockGrouping)
282 case KEYWORD_FUNCTION
:
283 $s .= 'function' . ($n->name ?
' ' . $n->name
: '') . '(';
284 $params = $n->params
;
285 for ($i = 0, $j = count($params); $i < $j; $i++
)
286 $s .= ($i ?
',' : '') . $params[$i];
287 $s .= '){' . $this->parseTree($n->body
, true) . '}';
291 $s = 'if(' . $this->parseTree($n->condition
) . ')';
292 $thenPart = $this->parseTree($n->thenPart
);
293 $elsePart = $n->elsePart ?
$this->parseTree($n->elsePart
) : null;
295 // empty if-statement
301 // be careful and always make a block out of the thenPart; could be more optimized but is a lot of trouble
302 if ($thenPart != ';' && $thenPart[0] != '{')
303 $thenPart = '{' . $thenPart . '}';
305 $s .= $thenPart . 'else';
307 // we could check for more, but that hardly ever applies so go for performance
308 if ($elsePart[0] != '{')
320 $s = 'switch(' . $this->parseTree($n->discriminant
) . '){';
322 for ($i = 0, $j = count($cases); $i < $j; $i++
)
325 if ($case->type
== KEYWORD_CASE
)
326 $s .= 'case' . ($case->caseLabel
->type
!= TOKEN_STRING ?
' ' : '') . $this->parseTree($case->caseLabel
) . ':';
330 $statement = $this->parseTree($case->statements
, true);
334 // no terminator for last statement
343 $s = 'for(' . ($n->setup ?
$this->parseTree($n->setup
) : '')
344 . ';' . ($n->condition ?
$this->parseTree($n->condition
) : '')
345 . ';' . ($n->update ?
$this->parseTree($n->update
) : '') . ')';
347 $body = $this->parseTree($n->body
);
355 $s = 'while(' . $this->parseTree($n->condition
) . ')';
357 $body = $this->parseTree($n->body
);
365 $s = 'for(' . ($n->varDecl ?
$this->parseTree($n->varDecl
) : $this->parseTree($n->iterator
)) . ' in ' . $this->parseTree($n->object) . ')';
367 $body = $this->parseTree($n->body
);
375 $s = 'do{' . $this->parseTree($n->body
, true) . '}while(' . $this->parseTree($n->condition
) . ')';
379 case KEYWORD_CONTINUE
:
380 $s = $n->value
. ($n->label ?
' ' . $n->label
: '');
384 $s = 'try{' . $this->parseTree($n->tryBlock
, true) . '}';
385 $catchClauses = $n->catchClauses
;
386 for ($i = 0, $j = count($catchClauses); $i < $j; $i++
)
388 $t = $catchClauses[$i];
389 $s .= 'catch(' . $t->varName
. ($t->guard ?
' if ' . $this->parseTree($t->guard
) : '') . '){' . $this->parseTree($t->block
, true) . '}';
391 if ($n->finallyBlock
)
392 $s .= 'finally{' . $this->parseTree($n->finallyBlock
, true) . '}';
400 $t = $this->parseTree($n->value
);
403 if ($this->isWordChar($t[0]) ||
$t[0] == '\\')
412 $s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body
);
417 $s = $n->value
. ' ';
418 $childs = $n->treeNodes
;
419 for ($i = 0, $j = count($childs); $i < $j; $i++
)
422 $s .= ($i ?
',' : '') . $t->name
;
423 $u = $t->initializer
;
425 $s .= '=' . $this->parseTree($u);
430 case KEYWORD_INSTANCEOF
:
431 $left = $this->parseTree($n->treeNodes
[0]);
432 $right = $this->parseTree($n->treeNodes
[1]);
436 if ($this->isWordChar(substr($left, -1)))
441 if ($this->isWordChar($right[0]) ||
$right[0] == '\\')
449 $right = $this->parseTree($n->treeNodes
[0]);
453 if ($this->isWordChar($right[0]) ||
$right[0] == '\\')
460 $s = 'void(' . $this->parseTree($n->treeNodes
[0]) . ')';
463 case KEYWORD_DEBUGGER
:
464 throw new Exception('NOT IMPLEMENTED: DEBUGGER');
467 case TOKEN_CONDCOMMENT_START
:
468 case TOKEN_CONDCOMMENT_END
:
469 $s = $n->value
. ($n->type
== TOKEN_CONDCOMMENT_START ?
' ' : '');
470 $childs = $n->treeNodes
;
471 for ($i = 0, $j = count($childs); $i < $j; $i++
)
472 $s .= $this->parseTree($childs[$i]);
476 if ($expression = $n->expression
)
477 $s = $this->parseTree($expression);
481 $s = $n->label
. ':' . $this->parseTree($n->statement
);
485 $childs = $n->treeNodes
;
486 for ($i = 0, $j = count($childs); $i < $j; $i++
)
487 $s .= ($i ?
',' : '') . $this->parseTree($childs[$i]);
491 $s = $this->parseTree($n->treeNodes
[0]) . $n->value
. $this->parseTree($n->treeNodes
[1]);
495 $s = $this->parseTree($n->treeNodes
[0]) . '?' . $this->parseTree($n->treeNodes
[1]) . ':' . $this->parseTree($n->treeNodes
[2]);
498 case OP_OR
: case OP_AND
:
499 case OP_BITWISE_OR
: case OP_BITWISE_XOR
: case OP_BITWISE_AND
:
500 case OP_EQ
: case OP_NE
: case OP_STRICT_EQ
: case OP_STRICT_NE
:
501 case OP_LT
: case OP_LE
: case OP_GE
: case OP_GT
:
502 case OP_LSH
: case OP_RSH
: case OP_URSH
:
503 case OP_MUL
: case OP_DIV
: case OP_MOD
:
504 $s = $this->parseTree($n->treeNodes
[0]) . $n->type
. $this->parseTree($n->treeNodes
[1]);
509 $left = $this->parseTree($n->treeNodes
[0]);
510 $right = $this->parseTree($n->treeNodes
[1]);
512 switch ($n->treeNodes
[1]->type
)
520 $s = $left . $n->type
. ' ' . $right;
524 //combine concatenated strings with same quote style
525 if ($n->type
== OP_PLUS
&& substr($left, -1) == $right[0])
527 $s = substr($left, 0, -1) . substr($right, 1);
533 $s = $left . $n->type
. $right;
541 $s = $n->value
. $this->parseTree($n->treeNodes
[0]);
547 $s = $this->parseTree($n->treeNodes
[0]) . $n->value
;
549 $s = $n->value
. $this->parseTree($n->treeNodes
[0]);
553 $s = $this->parseTree($n->treeNodes
[0]) . '.' . $this->parseTree($n->treeNodes
[1]);
557 $s = $this->parseTree($n->treeNodes
[0]);
558 // See if we can replace named index with a dot saving 3 bytes
559 if ( $n->treeNodes
[0]->type
== TOKEN_IDENTIFIER
&&
560 $n->treeNodes
[1]->type
== TOKEN_STRING
&&
561 $this->isValidIdentifier(substr($n->treeNodes
[1]->value
, 1, -1))
563 $s .= '.' . substr($n->treeNodes
[1]->value
, 1, -1);
565 $s .= '[' . $this->parseTree($n->treeNodes
[1]) . ']';
569 $childs = $n->treeNodes
;
570 for ($i = 0, $j = count($childs); $i < $j; $i++
)
571 $s .= ($i ?
',' : '') . $this->parseTree($childs[$i]);
575 $s = $this->parseTree($n->treeNodes
[0]) . '(' . $this->parseTree($n->treeNodes
[1]) . ')';
579 case JS_NEW_WITH_ARGS
:
580 $s = 'new ' . $this->parseTree($n->treeNodes
[0]) . '(' . ($n->type
== JS_NEW_WITH_ARGS ?
$this->parseTree($n->treeNodes
[1]) : '') . ')';
585 $childs = $n->treeNodes
;
586 for ($i = 0, $j = count($childs); $i < $j; $i++
)
588 $s .= ($i ?
',' : '') . $this->parseTree($childs[$i]);
595 $childs = $n->treeNodes
;
596 for ($i = 0, $j = count($childs); $i < $j; $i++
)
601 if ($t->type
== JS_PROPERTY_INIT
)
603 // Ditch the quotes when the index is a valid identifier
604 if ( $t->treeNodes
[0]->type
== TOKEN_STRING
&&
605 $this->isValidIdentifier(substr($t->treeNodes
[0]->value
, 1, -1))
607 $s .= substr($t->treeNodes
[0]->value
, 1, -1);
609 $s .= $t->treeNodes
[0]->value
;
611 $s .= ':' . $this->parseTree($t->treeNodes
[1]);
615 $s .= $t->type
== JS_GETTER ?
'get' : 'set';
616 $s .= ' ' . $t->name
. '(';
617 $params = $t->params
;
618 for ($i = 0, $j = count($params); $i < $j; $i++
)
619 $s .= ($i ?
',' : '') . $params[$i];
620 $s .= '){' . $this->parseTree($t->body
, true) . '}';
628 if (preg_match('/^([1-9]+)(0{3,})$/', $s, $m))
629 $s = $m[1] . 'e' . strlen($m[2]);
632 case KEYWORD_NULL
: case KEYWORD_THIS
: case KEYWORD_TRUE
: case KEYWORD_FALSE
:
633 case TOKEN_IDENTIFIER
: case TOKEN_STRING
: case TOKEN_REGEXP
:
639 $n->treeNodes
[0]->type
,
641 JS_ARRAY_INIT
, JS_OBJECT_INIT
, JS_GROUP
,
642 TOKEN_NUMBER
, TOKEN_STRING
, TOKEN_REGEXP
, TOKEN_IDENTIFIER
,
643 KEYWORD_NULL
, KEYWORD_THIS
, KEYWORD_TRUE
, KEYWORD_FALSE
647 $s = $this->parseTree($n->treeNodes
[0]);
651 $s = '(' . $this->parseTree($n->treeNodes
[0]) . ')';
656 throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type
);
662 private function isValidIdentifier($string)
664 return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved
);
667 private function isWordChar($char)
669 return $char == '_' ||
$char == '$' ||
ctype_alnum($char);
678 private $opPrecedence = array(
681 '=' => 2, '?' => 2, ':' => 2,
682 // The above all have to have the same precedence, see bug 330975
688 '==' => 9, '!=' => 9, '===' => 9, '!==' => 9,
689 '<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10,
690 '<<' => 11, '>>' => 11, '>>>' => 11,
691 '+' => 12, '-' => 12,
692 '*' => 13, '/' => 13, '%' => 13,
693 'delete' => 14, 'void' => 14, 'typeof' => 14,
694 '!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14,
695 '++' => 15, '--' => 15,
698 JS_NEW_WITH_ARGS
=> 0, JS_INDEX
=> 0, JS_CALL
=> 0,
699 JS_ARRAY_INIT
=> 0, JS_OBJECT_INIT
=> 0, JS_GROUP
=> 0
702 private $opArity = array(
711 '==' => 2, '!=' => 2, '===' => 2, '!==' => 2,
712 '<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2,
713 '<<' => 2, '>>' => 2, '>>>' => 2,
715 '*' => 2, '/' => 2, '%' => 2,
716 'delete' => 1, 'void' => 1, 'typeof' => 1,
717 '!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1,
718 '++' => 1, '--' => 1,
721 JS_NEW_WITH_ARGS
=> 2, JS_INDEX
=> 2, JS_CALL
=> 2,
722 JS_ARRAY_INIT
=> 1, JS_OBJECT_INIT
=> 1, JS_GROUP
=> 1,
723 TOKEN_CONDCOMMENT_START
=> 1, TOKEN_CONDCOMMENT_END
=> 1
726 public function __construct($minifier=null)
728 $this->minifier
= $minifier;
729 $this->t
= new JSTokenizer();
732 public function parse($s, $f, $l)
734 // initialize tokenizer
735 $this->t
->init($s, $f, $l);
737 $x = new JSCompilerContext(false);
738 $n = $this->Script($x);
739 if (!$this->t
->isDone())
740 throw $this->t
->newSyntaxError('Syntax error');
745 private function Script($x)
747 $n = $this->Statements($x);
748 $n->type
= JS_SCRIPT
;
749 $n->funDecls
= $x->funDecls
;
750 $n->varDecls
= $x->varDecls
;
755 $n->value
= $this->minifier
->parseTree($n);
757 // clear tree from node to save memory
758 $n->treeNodes
= null;
762 $n->type
= JS_MINIFIED
;
768 private function Statements($x)
770 $n = new JSNode($this->t
, JS_BLOCK
);
771 array_push($x->stmtStack
, $n);
773 while (!$this->t
->isDone() && $this->t
->peek() != OP_RIGHT_CURLY
)
774 $n->addNode($this->Statement($x));
776 array_pop($x->stmtStack
);
781 private function Block($x)
783 $this->t
->mustMatch(OP_LEFT_CURLY
);
784 $n = $this->Statements($x);
785 $this->t
->mustMatch(OP_RIGHT_CURLY
);
790 private function Statement($x)
792 $tt = $this->t
->get();
795 // Cases for statements ending in a right curly return early, avoiding the
796 // common semicolon insertion magic after this switch.
799 case KEYWORD_FUNCTION
:
800 return $this->FunctionDefinition(
803 count($x->stmtStack
) > 1 ? STATEMENT_FORM
: DECLARED_FORM
808 $n = $this->Statements($x);
809 $this->t
->mustMatch(OP_RIGHT_CURLY
);
813 $n = new JSNode($this->t
);
814 $n->condition
= $this->ParenExpression($x);
815 array_push($x->stmtStack
, $n);
816 $n->thenPart
= $this->Statement($x);
817 $n->elsePart
= $this->t
->match(KEYWORD_ELSE
) ?
$this->Statement($x) : null;
818 array_pop($x->stmtStack
);
822 $n = new JSNode($this->t
);
823 $this->t
->mustMatch(OP_LEFT_PAREN
);
824 $n->discriminant
= $this->Expression($x);
825 $this->t
->mustMatch(OP_RIGHT_PAREN
);
827 $n->defaultIndex
= -1;
829 array_push($x->stmtStack
, $n);
831 $this->t
->mustMatch(OP_LEFT_CURLY
);
833 while (($tt = $this->t
->get()) != OP_RIGHT_CURLY
)
837 case KEYWORD_DEFAULT
:
838 if ($n->defaultIndex
>= 0)
839 throw $this->t
->newSyntaxError('More than one switch default');
842 $n2 = new JSNode($this->t
);
843 if ($tt == KEYWORD_DEFAULT
)
844 $n->defaultIndex
= count($n->cases
);
846 $n2->caseLabel
= $this->Expression($x, OP_COLON
);
849 throw $this->t
->newSyntaxError('Invalid switch case');
852 $this->t
->mustMatch(OP_COLON
);
853 $n2->statements
= new JSNode($this->t
, JS_BLOCK
);
854 while (($tt = $this->t
->peek()) != KEYWORD_CASE
&& $tt != KEYWORD_DEFAULT
&& $tt != OP_RIGHT_CURLY
)
855 $n2->statements
->addNode($this->Statement($x));
857 array_push($n->cases
, $n2);
860 array_pop($x->stmtStack
);
864 $n = new JSNode($this->t
);
866 $this->t
->mustMatch(OP_LEFT_PAREN
);
868 if (($tt = $this->t
->peek()) != OP_SEMICOLON
)
870 $x->inForLoopInit
= true;
871 if ($tt == KEYWORD_VAR ||
$tt == KEYWORD_CONST
)
874 $n2 = $this->Variables($x);
878 $n2 = $this->Expression($x);
880 $x->inForLoopInit
= false;
883 if ($n2 && $this->t
->match(KEYWORD_IN
))
885 $n->type
= JS_FOR_IN
;
886 if ($n2->type
== KEYWORD_VAR
)
888 if (count($n2->treeNodes
) != 1)
890 throw $this->t
->SyntaxError(
891 'Invalid for..in left-hand side',
897 // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name.
898 $n->iterator
= $n2->treeNodes
[0];
907 $n->object = $this->Expression($x);
911 $n->setup
= $n2 ?
$n2 : null;
912 $this->t
->mustMatch(OP_SEMICOLON
);
913 $n->condition
= $this->t
->peek() == OP_SEMICOLON ?
null : $this->Expression($x);
914 $this->t
->mustMatch(OP_SEMICOLON
);
915 $n->update
= $this->t
->peek() == OP_RIGHT_PAREN ?
null : $this->Expression($x);
918 $this->t
->mustMatch(OP_RIGHT_PAREN
);
919 $n->body
= $this->nest($x, $n);
923 $n = new JSNode($this->t
);
925 $n->condition
= $this->ParenExpression($x);
926 $n->body
= $this->nest($x, $n);
930 $n = new JSNode($this->t
);
932 $n->body
= $this->nest($x, $n, KEYWORD_WHILE
);
933 $n->condition
= $this->ParenExpression($x);
934 if (!$x->ecmaStrictMode
)
936 // <script language="JavaScript"> (without version hints) may need
937 // automatic semicolon insertion without a newline after do-while.
938 // See http://bugzilla.mozilla.org/show_bug.cgi?id=238945.
939 $this->t
->match(OP_SEMICOLON
);
945 case KEYWORD_CONTINUE
:
946 $n = new JSNode($this->t
);
948 if ($this->t
->peekOnSameLine() == TOKEN_IDENTIFIER
)
951 $n->label
= $this->t
->currentToken()->value
;
962 throw $this->t
->newSyntaxError('Label not found');
964 while ($ss[$i]->label
!= $label);
971 throw $this->t
->newSyntaxError('Invalid ' . $tt);
973 while (!$ss[$i]->isLoop
&& ($tt != KEYWORD_BREAK ||
$ss[$i]->type
!= KEYWORD_SWITCH
));
976 $n->target
= $ss[$i];
980 $n = new JSNode($this->t
);
981 $n->tryBlock
= $this->Block($x);
982 $n->catchClauses
= array();
984 while ($this->t
->match(KEYWORD_CATCH
))
986 $n2 = new JSNode($this->t
);
987 $this->t
->mustMatch(OP_LEFT_PAREN
);
988 $n2->varName
= $this->t
->mustMatch(TOKEN_IDENTIFIER
)->value
;
990 if ($this->t
->match(KEYWORD_IF
))
992 if ($x->ecmaStrictMode
)
993 throw $this->t
->newSyntaxError('Illegal catch guard');
995 if (count($n->catchClauses
) && !end($n->catchClauses
)->guard
)
996 throw $this->t
->newSyntaxError('Guarded catch after unguarded');
998 $n2->guard
= $this->Expression($x);
1005 $this->t
->mustMatch(OP_RIGHT_PAREN
);
1006 $n2->block
= $this->Block($x);
1007 array_push($n->catchClauses
, $n2);
1010 if ($this->t
->match(KEYWORD_FINALLY
))
1011 $n->finallyBlock
= $this->Block($x);
1013 if (!count($n->catchClauses
) && !$n->finallyBlock
)
1014 throw $this->t
->newSyntaxError('Invalid try statement');
1018 case KEYWORD_FINALLY
:
1019 throw $this->t
->newSyntaxError($tt +
' without preceding try');
1022 $n = new JSNode($this->t
);
1023 $n->value
= $this->Expression($x);
1026 case KEYWORD_RETURN
:
1027 if (!$x->inFunction
)
1028 throw $this->t
->newSyntaxError('Invalid return');
1030 $n = new JSNode($this->t
);
1031 $tt = $this->t
->peekOnSameLine();
1032 if ($tt != TOKEN_END
&& $tt != TOKEN_NEWLINE
&& $tt != OP_SEMICOLON
&& $tt != OP_RIGHT_CURLY
)
1033 $n->value
= $this->Expression($x);
1039 $n = new JSNode($this->t
);
1040 $n->object = $this->ParenExpression($x);
1041 $n->body
= $this->nest($x, $n);
1046 $n = $this->Variables($x);
1049 case TOKEN_CONDCOMMENT_START
:
1050 case TOKEN_CONDCOMMENT_END
:
1051 $n = new JSNode($this->t
);
1054 case KEYWORD_DEBUGGER
:
1055 $n = new JSNode($this->t
);
1060 $n = new JSNode($this->t
, OP_SEMICOLON
);
1061 $n->expression
= null;
1065 if ($tt == TOKEN_IDENTIFIER
)
1067 $this->t
->scanOperand
= false;
1068 $tt = $this->t
->peek();
1069 $this->t
->scanOperand
= true;
1070 if ($tt == OP_COLON
)
1072 $label = $this->t
->currentToken()->value
;
1073 $ss = $x->stmtStack
;
1074 for ($i = count($ss) - 1; $i >= 0; --$i)
1076 if ($ss[$i]->label
== $label)
1077 throw $this->t
->newSyntaxError('Duplicate label');
1081 $n = new JSNode($this->t
, JS_LABEL
);
1083 $n->statement
= $this->nest($x, $n);
1089 $n = new JSNode($this->t
, OP_SEMICOLON
);
1091 $n->expression
= $this->Expression($x);
1092 $n->end
= $n->expression
->end
;
1096 if ($this->t
->lineno
== $this->t
->currentToken()->lineno
)
1098 $tt = $this->t
->peekOnSameLine();
1099 if ($tt != TOKEN_END
&& $tt != TOKEN_NEWLINE
&& $tt != OP_SEMICOLON
&& $tt != OP_RIGHT_CURLY
)
1100 throw $this->t
->newSyntaxError('Missing ; before statement');
1103 $this->t
->match(OP_SEMICOLON
);
1108 private function FunctionDefinition($x, $requireName, $functionForm)
1110 $f = new JSNode($this->t
);
1112 if ($f->type
!= KEYWORD_FUNCTION
)
1113 $f->type
= ($f->value
== 'get') ? JS_GETTER
: JS_SETTER
;
1115 if ($this->t
->match(TOKEN_IDENTIFIER
))
1116 $f->name
= $this->t
->currentToken()->value
;
1117 elseif ($requireName)
1118 throw $this->t
->newSyntaxError('Missing function identifier');
1120 $this->t
->mustMatch(OP_LEFT_PAREN
);
1121 $f->params
= array();
1123 while (($tt = $this->t
->get()) != OP_RIGHT_PAREN
)
1125 if ($tt != TOKEN_IDENTIFIER
)
1126 throw $this->t
->newSyntaxError('Missing formal parameter');
1128 array_push($f->params
, $this->t
->currentToken()->value
);
1130 if ($this->t
->peek() != OP_RIGHT_PAREN
)
1131 $this->t
->mustMatch(OP_COMMA
);
1134 $this->t
->mustMatch(OP_LEFT_CURLY
);
1136 $x2 = new JSCompilerContext(true);
1137 $f->body
= $this->Script($x2);
1139 $this->t
->mustMatch(OP_RIGHT_CURLY
);
1140 $f->end
= $this->t
->currentToken()->end
;
1142 $f->functionForm
= $functionForm;
1143 if ($functionForm == DECLARED_FORM
)
1144 array_push($x->funDecls
, $f);
1149 private function Variables($x)
1151 $n = new JSNode($this->t
);
1155 $this->t
->mustMatch(TOKEN_IDENTIFIER
);
1157 $n2 = new JSNode($this->t
);
1158 $n2->name
= $n2->value
;
1160 if ($this->t
->match(OP_ASSIGN
))
1162 if ($this->t
->currentToken()->assignOp
)
1163 throw $this->t
->newSyntaxError('Invalid variable initialization');
1165 $n2->initializer
= $this->Expression($x, OP_COMMA
);
1168 $n2->readOnly
= $n->type
== KEYWORD_CONST
;
1171 array_push($x->varDecls
, $n2);
1173 while ($this->t
->match(OP_COMMA
));
1178 private function Expression($x, $stop=false)
1180 $operators = array();
1181 $operands = array();
1184 $bl = $x->bracketLevel
;
1185 $cl = $x->curlyLevel
;
1186 $pl = $x->parenLevel
;
1187 $hl = $x->hookLevel
;
1189 while (($tt = $this->t
->get()) != TOKEN_END
)
1192 $x->bracketLevel
== $bl &&
1193 $x->curlyLevel
== $cl &&
1194 $x->parenLevel
== $pl &&
1195 $x->hookLevel
== $hl
1198 // Stop only if tt matches the optional stop parameter, and that
1199 // token is not quoted by some kind of bracket.
1206 // NB: cannot be empty, Statement handled that.
1210 if ($this->t
->scanOperand
)
1213 while ( !empty($operators) &&
1214 $this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[$tt]
1216 $this->reduce($operators, $operands);
1218 array_push($operators, new JSNode($this->t
));
1221 $this->t
->scanOperand
= true;
1222 $n = $this->Expression($x);
1224 if (!$this->t
->match(OP_COLON
))
1228 array_push($operands, $n);
1235 throw $this->t
->newSyntaxError('Invalid label');
1239 if ($this->t
->scanOperand
)
1242 // Use >, not >=, for right-associative ASSIGN
1243 while ( !empty($operators) &&
1244 $this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[$tt]
1246 $this->reduce($operators, $operands);
1248 array_push($operators, new JSNode($this->t
));
1249 end($operands)->assignOp
= $this->t
->currentToken()->assignOp
;
1250 $this->t
->scanOperand
= true;
1254 // An in operator should not be parsed if we're parsing the head of
1255 // a for (...) loop, unless it is in the then part of a conditional
1256 // expression, or parenthesized somehow.
1257 if ($x->inForLoopInit
&& !$x->hookLevel
&&
1258 !$x->bracketLevel
&& !$x->curlyLevel
&&
1264 // A comma operator should not be parsed if we're parsing the then part
1265 // of a conditional expression unless it's parenthesized somehow.
1266 if ($tt == OP_COMMA
&& $x->hookLevel
&&
1267 !$x->bracketLevel
&& !$x->curlyLevel
&&
1271 // Treat comma as left-associative so reduce can fold left-heavy
1272 // COMMA trees into a single array.
1277 case OP_BITWISE_XOR
:
1278 case OP_BITWISE_AND
:
1279 case OP_EQ
: case OP_NE
: case OP_STRICT_EQ
: case OP_STRICT_NE
:
1280 case OP_LT
: case OP_LE
: case OP_GE
: case OP_GT
:
1281 case KEYWORD_INSTANCEOF
:
1282 case OP_LSH
: case OP_RSH
: case OP_URSH
:
1283 case OP_PLUS
: case OP_MINUS
:
1284 case OP_MUL
: case OP_DIV
: case OP_MOD
:
1286 if ($this->t
->scanOperand
)
1289 while ( !empty($operators) &&
1290 $this->opPrecedence
[end($operators)->type
] >= $this->opPrecedence
[$tt]
1292 $this->reduce($operators, $operands);
1296 $this->t
->mustMatch(TOKEN_IDENTIFIER
);
1297 array_push($operands, new JSNode($this->t
, OP_DOT
, array_pop($operands), new JSNode($this->t
)));
1301 array_push($operators, new JSNode($this->t
));
1302 $this->t
->scanOperand
= true;
1306 case KEYWORD_DELETE
: case KEYWORD_VOID
: case KEYWORD_TYPEOF
:
1307 case OP_NOT
: case OP_BITWISE_NOT
: case OP_UNARY_PLUS
: case OP_UNARY_MINUS
:
1309 if (!$this->t
->scanOperand
)
1312 array_push($operators, new JSNode($this->t
));
1315 case OP_INCREMENT
: case OP_DECREMENT
:
1316 if ($this->t
->scanOperand
)
1318 array_push($operators, new JSNode($this->t
)); // prefix increment or decrement
1322 // Don't cross a line boundary for postfix {in,de}crement.
1323 $t = $this->t
->tokens
[($this->t
->tokenIndex +
$this->t
->lookahead
- 1) & 3];
1324 if ($t && $t->lineno
!= $this->t
->lineno
)
1327 if (!empty($operators))
1329 // Use >, not >=, so postfix has higher precedence than prefix.
1330 while ($this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[$tt])
1331 $this->reduce($operators, $operands);
1334 $n = new JSNode($this->t
, $tt, array_pop($operands));
1336 array_push($operands, $n);
1340 case KEYWORD_FUNCTION
:
1341 if (!$this->t
->scanOperand
)
1344 array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM
));
1345 $this->t
->scanOperand
= false;
1348 case KEYWORD_NULL
: case KEYWORD_THIS
: case KEYWORD_TRUE
: case KEYWORD_FALSE
:
1349 case TOKEN_IDENTIFIER
: case TOKEN_NUMBER
: case TOKEN_STRING
: case TOKEN_REGEXP
:
1350 if (!$this->t
->scanOperand
)
1353 array_push($operands, new JSNode($this->t
));
1354 $this->t
->scanOperand
= false;
1357 case TOKEN_CONDCOMMENT_START
:
1358 case TOKEN_CONDCOMMENT_END
:
1359 if ($this->t
->scanOperand
)
1360 array_push($operators, new JSNode($this->t
));
1362 array_push($operands, new JSNode($this->t
));
1365 case OP_LEFT_BRACKET
:
1366 if ($this->t
->scanOperand
)
1368 // Array initialiser. Parse using recursive descent, as the
1369 // sub-grammar here is not an operator grammar.
1370 $n = new JSNode($this->t
, JS_ARRAY_INIT
);
1371 while (($tt = $this->t
->peek()) != OP_RIGHT_BRACKET
)
1373 if ($tt == OP_COMMA
)
1380 $n->addNode($this->Expression($x, OP_COMMA
));
1381 if (!$this->t
->match(OP_COMMA
))
1385 $this->t
->mustMatch(OP_RIGHT_BRACKET
);
1386 array_push($operands, $n);
1387 $this->t
->scanOperand
= false;
1391 // Property indexing operator.
1392 array_push($operators, new JSNode($this->t
, JS_INDEX
));
1393 $this->t
->scanOperand
= true;
1398 case OP_RIGHT_BRACKET
:
1399 if ($this->t
->scanOperand ||
$x->bracketLevel
== $bl)
1402 while ($this->reduce($operators, $operands)->type
!= JS_INDEX
)
1409 if (!$this->t
->scanOperand
)
1412 // Object initialiser. As for array initialisers (see above),
1413 // parse using recursive descent.
1415 $n = new JSNode($this->t
, JS_OBJECT_INIT
);
1416 while (!$this->t
->match(OP_RIGHT_CURLY
))
1420 $tt = $this->t
->get();
1421 $tv = $this->t
->currentToken()->value
;
1422 if (($tv == 'get' ||
$tv == 'set') && $this->t
->peek() == TOKEN_IDENTIFIER
)
1424 if ($x->ecmaStrictMode
)
1425 throw $this->t
->newSyntaxError('Illegal property accessor');
1427 $n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM
));
1433 case TOKEN_IDENTIFIER
:
1436 $id = new JSNode($this->t
);
1439 case OP_RIGHT_CURLY
:
1440 if ($x->ecmaStrictMode
)
1441 throw $this->t
->newSyntaxError('Illegal trailing ,');
1445 throw $this->t
->newSyntaxError('Invalid property name');
1448 $this->t
->mustMatch(OP_COLON
);
1449 $n->addNode(new JSNode($this->t
, JS_PROPERTY_INIT
, $id, $this->Expression($x, OP_COMMA
)));
1452 while ($this->t
->match(OP_COMMA
));
1454 $this->t
->mustMatch(OP_RIGHT_CURLY
);
1458 array_push($operands, $n);
1459 $this->t
->scanOperand
= false;
1463 case OP_RIGHT_CURLY
:
1464 if (!$this->t
->scanOperand
&& $x->curlyLevel
!= $cl)
1465 throw new Exception('PANIC: right curly botch');
1469 if ($this->t
->scanOperand
)
1471 array_push($operators, new JSNode($this->t
, JS_GROUP
));
1475 while ( !empty($operators) &&
1476 $this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[KEYWORD_NEW
]
1478 $this->reduce($operators, $operands);
1480 // Handle () now, to regularize the n-ary case for n > 0.
1481 // We must set scanOperand in case there are arguments and
1482 // the first one is a regexp or unary+/-.
1483 $n = end($operators);
1484 $this->t
->scanOperand
= true;
1485 if ($this->t
->match(OP_RIGHT_PAREN
))
1487 if ($n && $n->type
== KEYWORD_NEW
)
1489 array_pop($operators);
1490 $n->addNode(array_pop($operands));
1494 $n = new JSNode($this->t
, JS_CALL
, array_pop($operands), new JSNode($this->t
, JS_LIST
));
1497 array_push($operands, $n);
1498 $this->t
->scanOperand
= false;
1502 if ($n && $n->type
== KEYWORD_NEW
)
1503 $n->type
= JS_NEW_WITH_ARGS
;
1505 array_push($operators, new JSNode($this->t
, JS_CALL
));
1511 case OP_RIGHT_PAREN
:
1512 if ($this->t
->scanOperand ||
$x->parenLevel
== $pl)
1515 while (($tt = $this->reduce($operators, $operands)->type
) != JS_GROUP
&&
1516 $tt != JS_CALL
&& $tt != JS_NEW_WITH_ARGS
1522 if ($tt != JS_GROUP
)
1524 $n = end($operands);
1525 if ($n->treeNodes
[1]->type
!= OP_COMMA
)
1526 $n->treeNodes
[1] = new JSNode($this->t
, JS_LIST
, $n->treeNodes
[1]);
1528 $n->treeNodes
[1]->type
= JS_LIST
;
1534 // Automatic semicolon insertion means we may scan across a newline
1535 // and into the beginning of another statement. If so, break out of
1536 // the while loop and let the t.scanOperand logic handle errors.
1542 if ($x->hookLevel
!= $hl)
1543 throw $this->t
->newSyntaxError('Missing : in conditional expression');
1545 if ($x->parenLevel
!= $pl)
1546 throw $this->t
->newSyntaxError('Missing ) in parenthetical');
1548 if ($x->bracketLevel
!= $bl)
1549 throw $this->t
->newSyntaxError('Missing ] in index expression');
1551 if ($this->t
->scanOperand
)
1552 throw $this->t
->newSyntaxError('Missing operand');
1554 // Resume default mode, scanning for operands, not operators.
1555 $this->t
->scanOperand
= true;
1558 while (count($operators))
1559 $this->reduce($operators, $operands);
1561 return array_pop($operands);
1564 private function ParenExpression($x)
1566 $this->t
->mustMatch(OP_LEFT_PAREN
);
1567 $n = $this->Expression($x);
1568 $this->t
->mustMatch(OP_RIGHT_PAREN
);
1573 // Statement stack and nested statement handler.
1574 private function nest($x, $node, $end = false)
1576 array_push($x->stmtStack
, $node);
1577 $n = $this->statement($x);
1578 array_pop($x->stmtStack
);
1581 $this->t
->mustMatch($end);
1586 private function reduce(&$operators, &$operands)
1588 $n = array_pop($operators);
1590 $arity = $this->opArity
[$op];
1591 $c = count($operands);
1594 // Flatten left-associative trees
1597 $left = $operands[$c - 2];
1598 if ($left->type
== $op)
1600 $right = array_pop($operands);
1601 $left->addNode($right);
1608 // Always use push to add operands to n, to update start and end
1609 $a = array_splice($operands, $c - $arity);
1610 for ($i = 0; $i < $arity; $i++
)
1611 $n->addNode($a[$i]);
1613 // Include closing bracket or postfix operator in [start,end]
1614 $te = $this->t
->currentToken()->end
;
1618 array_push($operands, $n);
1624 class JSCompilerContext
1626 public $inFunction = false;
1627 public $inForLoopInit = false;
1628 public $ecmaStrictMode = false;
1629 public $bracketLevel = 0;
1630 public $curlyLevel = 0;
1631 public $parenLevel = 0;
1632 public $hookLevel = 0;
1634 public $stmtStack = array();
1635 public $funDecls = array();
1636 public $varDecls = array();
1638 public function __construct($inFunction)
1640 $this->inFunction
= $inFunction;
1652 public $treeNodes = array();
1653 public $funDecls = array();
1654 public $varDecls = array();
1656 public function __construct($t, $type=0)
1658 if ($token = $t->currentToken())
1660 $this->type
= $type ?
$type : $token->type
;
1661 $this->value
= $token->value
;
1662 $this->lineno
= $token->lineno
;
1663 $this->start
= $token->start
;
1664 $this->end
= $token->end
;
1668 $this->type
= $type;
1669 $this->lineno
= $t->lineno
;
1672 if (($numargs = func_num_args()) > 2)
1674 $args = func_get_args();
1675 for ($i = 2; $i < $numargs; $i++
)
1676 $this->addNode($args[$i]);
1680 // we don't want to bloat our object with all kind of specific properties, so we use overloading
1681 public function __set($name, $value)
1683 $this->$name = $value;
1686 public function __get($name)
1688 if (isset($this->$name))
1689 return $this->$name;
1694 public function addNode($node)
1698 if ($node->start
< $this->start
)
1699 $this->start
= $node->start
;
1700 if ($this->end
< $node->end
)
1701 $this->end
= $node->end
;
1704 $this->treeNodes
[] = $node;
1710 private $cursor = 0;
1713 public $tokens = array();
1714 public $tokenIndex = 0;
1715 public $lookahead = 0;
1716 public $scanNewlines = false;
1717 public $scanOperand = true;
1722 private $keywords = array(
1724 'case', 'catch', 'const', 'continue',
1725 'debugger', 'default', 'delete', 'do',
1727 'false', 'finally', 'for', 'function',
1728 'if', 'in', 'instanceof',
1732 'this', 'throw', 'true', 'try', 'typeof',
1737 private $opTypeNames = array(
1738 ';', ',', '?', ':', '||', '&&', '|', '^',
1739 '&', '===', '==', '=', '!==', '!=', '<<', '<=',
1740 '<', '>>>', '>>', '>=', '>', '++', '--', '+',
1741 '-', '*', '/', '%', '!', '~', '.', '[',
1742 ']', '{', '}', '(', ')', '@*/'
1745 private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
1748 public function __construct()
1750 $this->opRegExp
= '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames
)) . ')#';
1753 public function init($source, $filename = '', $lineno = 1)
1755 $this->source
= $source;
1756 $this->filename
= $filename ?
$filename : '[inline]';
1757 $this->lineno
= $lineno;
1760 $this->tokens
= array();
1761 $this->tokenIndex
= 0;
1762 $this->lookahead
= 0;
1763 $this->scanNewlines
= false;
1764 $this->scanOperand
= true;
1767 public function getInput($chunksize)
1770 return substr($this->source
, $this->cursor
, $chunksize);
1772 return substr($this->source
, $this->cursor
);
1775 public function isDone()
1777 return $this->peek() == TOKEN_END
;
1780 public function match($tt)
1782 return $this->get() == $tt ||
$this->unget();
1785 public function mustMatch($tt)
1787 if (!$this->match($tt))
1788 throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
1790 return $this->currentToken();
1793 public function peek()
1795 if ($this->lookahead
)
1797 $next = $this->tokens
[($this->tokenIndex +
$this->lookahead
) & 3];
1798 if ($this->scanNewlines
&& $next->lineno
!= $this->lineno
)
1799 $tt = TOKEN_NEWLINE
;
1812 public function peekOnSameLine()
1814 $this->scanNewlines
= true;
1815 $tt = $this->peek();
1816 $this->scanNewlines
= false;
1821 public function currentToken()
1823 if (!empty($this->tokens
))
1824 return $this->tokens
[$this->tokenIndex
];
1827 public function get($chunksize = 1000)
1829 while($this->lookahead
)
1832 $this->tokenIndex
= ($this->tokenIndex +
1) & 3;
1833 $token = $this->tokens
[$this->tokenIndex
];
1834 if ($token->type
!= TOKEN_NEWLINE ||
$this->scanNewlines
)
1835 return $token->type
;
1838 $conditional_comment = false;
1840 // strip whitespace and comments
1843 $input = $this->getInput($chunksize);
1845 // whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
1846 $re = $this->scanNewlines ?
'/^[ \r\t]+/' : '/^\s+/';
1847 if (preg_match($re, $input, $match))
1849 $spaces = $match[0];
1850 $spacelen = strlen($spaces);
1851 $this->cursor +
= $spacelen;
1852 if (!$this->scanNewlines
)
1853 $this->lineno +
= substr_count($spaces, "\n");
1855 if ($spacelen == $chunksize)
1856 continue; // complete chunk contained whitespace
1858 $input = $this->getInput($chunksize);
1859 if ($input == '' ||
$input[0] != '/')
1864 if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match))
1869 // retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
1874 // check if this is a conditional (JScript) comment
1875 if (!empty($match[1]))
1877 $match[0] = '/*' . $match[1];
1878 $conditional_comment = true;
1883 $this->cursor +
= strlen($match[0]);
1884 $this->lineno +
= substr_count($match[0], "\n");
1893 elseif ($conditional_comment)
1895 $tt = TOKEN_CONDCOMMENT_START
;
1903 if (($input[1] == 'x' ||
$input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match))
1910 case '1': case '2': case '3': case '4': case '5':
1911 case '6': case '7': case '8': case '9':
1912 // should always match
1913 preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match);
1918 if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match))
1925 return $this->get(null); // retry with a full chunk fetch
1927 throw $this->newSyntaxError('Unterminated string literal');
1932 if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match))
1939 return $this->get(null); // retry with a full chunk fetch
1941 throw $this->newSyntaxError('Unterminated string literal');
1946 if ($this->scanOperand
&& preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
1964 // should always match
1965 preg_match($this->opRegExp
, $input, $match);
1967 if (in_array($op, $this->assignOps
) && $input[strlen($op)] == '=')
1975 if ($this->scanOperand
)
1978 $tt = OP_UNARY_PLUS
;
1979 elseif ($op == OP_MINUS
)
1980 $tt = OP_UNARY_MINUS
;
1987 if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
2005 // these are all single
2006 $match = array($input[0]);
2011 // check end of conditional comment
2012 if (substr($input, 0, 3) == '@*/')
2014 $match = array('@*/');
2015 $tt = TOKEN_CONDCOMMENT_END
;
2018 throw $this->newSyntaxError('Illegal token');
2022 if ($this->scanNewlines
)
2024 $match = array("\n");
2025 $tt = TOKEN_NEWLINE
;
2028 throw $this->newSyntaxError('Illegal token');
2032 // Fast path for identifiers: word chars followed by whitespace or various other tokens.
2033 // Note we don't need to exclude digits in the first char, as they've already been found
2035 if (!preg_match('/^[$\w]+(?=[\s\/\|\^\&<>\+\-\*%=!.;,\?:~\[\]\{\}\(\)@])/', $input, $match))
2037 // Character classes per ECMA-262 edition 5.1 section 7.6
2038 // Per spec, must accept Unicode 3.0, *may* accept later versions.
2039 // We'll take whatever PCRE understands, which should be more recent.
2040 $identifierStartChars = "\\p{L}\\p{Nl}" . # UnicodeLetter
2043 $identifierPartChars = $identifierStartChars .
2044 "\\p{Mn}\\p{Mc}" . # UnicodeCombiningMark
2045 "\\p{Nd}" . # UnicodeDigit
2046 "\\p{Pc}"; # UnicodeConnectorPunctuation
2047 $unicodeEscape = "\\\\u[0-9A-F-a-f]{4}";
2048 $identifierRegex = "/^" .
2049 "(?:[$identifierStartChars]|$unicodeEscape)" .
2050 "(?:[$identifierPartChars]|$unicodeEscape)*" .
2052 if (preg_match($identifierRegex, $input, $match))
2054 if (strpos($match[0], '\\') !== false) {
2055 // Per ECMA-262 edition 5.1, section 7.6 escape sequences should behave as if they were
2056 // the original chars, but only within the boundaries of the identifier.
2057 $decoded = preg_replace_callback('/\\\\u([0-9A-Fa-f]{4})/',
2058 array(__CLASS__
, 'unicodeEscapeCallback'),
2061 // Since our original regex didn't de-escape the originals, we need to check for validity again.
2062 // No need to worry about token boundaries, as anything outside the identifier is illegal!
2063 if (!preg_match("/^[$identifierStartChars][$identifierPartChars]*$/u", $decoded)) {
2064 throw $this->newSyntaxError('Illegal token');
2067 // Per spec it _ought_ to work to use these escapes for keywords words as well...
2068 // but IE rejects them as invalid, while Firefox and Chrome treat them as identifiers
2069 // that don't match the keyword.
2070 if (in_array($decoded, $this->keywords
)) {
2071 throw $this->newSyntaxError('Illegal token');
2074 // TODO: save the decoded form for output?
2078 throw $this->newSyntaxError('Illegal token');
2080 $tt = in_array($match[0], $this->keywords
) ?
$match[0] : TOKEN_IDENTIFIER
;
2084 $this->tokenIndex
= ($this->tokenIndex +
1) & 3;
2086 if (!isset($this->tokens
[$this->tokenIndex
]))
2087 $this->tokens
[$this->tokenIndex
] = new JSToken();
2089 $token = $this->tokens
[$this->tokenIndex
];
2092 if ($tt == OP_ASSIGN
)
2093 $token->assignOp
= $op;
2095 $token->start
= $this->cursor
;
2097 $token->value
= $match[0];
2098 $this->cursor +
= strlen($match[0]);
2100 $token->end
= $this->cursor
;
2101 $token->lineno
= $this->lineno
;
2106 public function unget()
2108 if (++
$this->lookahead
== 4)
2109 throw $this->newSyntaxError('PANIC: too much lookahead!');
2111 $this->tokenIndex
= ($this->tokenIndex
- 1) & 3;
2114 public function newSyntaxError($m)
2116 return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename
. '\' on line ' . $this->lineno
);
2119 public static function unicodeEscapeCallback($m)
2121 return html_entity_decode('&#x' . $m[1]. ';', ENT_QUOTES
, 'UTF-8');