2 // phpcs:ignoreFile -- File external to MediaWiki. Ignore coding conventions checks.
4 * JSMinPlus version 1.4
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: https://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine)
10 * Narcissus sourcecode: https://mxr.mozilla.org/mozilla/source/js/narcissus/
11 * JSMinPlus weblog: https://crisp.tweakblogs.net/blog/cat/716
13 * Tino Zijdel <crisp@tweakers.net>
15 * Usage: $minified = JSMinPlus::minify($script [, $filename])
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
26 * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip
31 /* ***** BEGIN LICENSE BLOCK *****
32 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
34 * The contents of this file are subject to the Mozilla Public License Version
35 * 1.1 (the "License"); you may not use this file except in compliance with
36 * the License. You may obtain a copy of the License at
37 * http://www.mozilla.org/MPL/
39 * Software distributed under the License is distributed on an "AS IS" basis,
40 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
41 * for the specific language governing rights and limitations under the
44 * The Original Code is the Narcissus JavaScript engine.
46 * The Initial Developer of the Original Code is
47 * Brendan Eich <brendan@mozilla.org>.
48 * Portions created by the Initial Developer are Copyright (C) 2004
49 * the Initial Developer. All Rights Reserved.
51 * Contributor(s): Tino Zijdel <crisp@tweakers.net>
52 * PHP port, modifications and minifier routine are (C) 2009-2011
54 * Alternatively, the contents of this file may be used under the terms of
55 * either the GNU General Public License Version 2 or later (the "GPL"), or
56 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
57 * in which case the provisions of the GPL or the LGPL are applicable instead
58 * of those above. If you wish to allow use of your version of this file only
59 * under the terms of either the GPL or the LGPL, and not to allow others to
60 * use your version of this file under the terms of the MPL, indicate your
61 * decision by deleting the provisions above and replace them with the notice
62 * and other provisions required by the GPL or the LGPL. If you do not delete
63 * the provisions above, a recipient may use your version of this file under
64 * the terms of any one of the MPL, the GPL or the LGPL.
66 * ***** END LICENSE BLOCK ***** */
68 define('TOKEN_END', 1);
69 define('TOKEN_NUMBER', 2);
70 define('TOKEN_IDENTIFIER', 3);
71 define('TOKEN_STRING', 4);
72 define('TOKEN_REGEXP', 5);
73 define('TOKEN_NEWLINE', 6);
74 define('TOKEN_CONDCOMMENT_START', 7);
75 define('TOKEN_CONDCOMMENT_END', 8);
77 define('JS_SCRIPT', 100);
78 define('JS_BLOCK', 101);
79 define('JS_LABEL', 102);
80 define('JS_FOR_IN', 103);
81 define('JS_CALL', 104);
82 define('JS_NEW_WITH_ARGS', 105);
83 define('JS_INDEX', 106);
84 define('JS_ARRAY_INIT', 107);
85 define('JS_OBJECT_INIT', 108);
86 define('JS_PROPERTY_INIT', 109);
87 define('JS_GETTER', 110);
88 define('JS_SETTER', 111);
89 define('JS_GROUP', 112);
90 define('JS_LIST', 113);
92 define('JS_MINIFIED', 999);
94 define('DECLARED_FORM', 0);
95 define('EXPRESSED_FORM', 1);
96 define('STATEMENT_FORM', 2);
99 define('OP_SEMICOLON', ';');
100 define('OP_COMMA', ',');
101 define('OP_HOOK', '?');
102 define('OP_COLON', ':');
103 define('OP_OR', '||');
104 define('OP_AND', '&&');
105 define('OP_BITWISE_OR', '|');
106 define('OP_BITWISE_XOR', '^');
107 define('OP_BITWISE_AND', '&');
108 define('OP_STRICT_EQ', '===');
109 define('OP_EQ', '==');
110 define('OP_ASSIGN', '=');
111 define('OP_STRICT_NE', '!==');
112 define('OP_NE', '!=');
113 define('OP_LSH', '<<');
114 define('OP_LE', '<=');
115 define('OP_LT', '<');
116 define('OP_URSH', '>>>');
117 define('OP_RSH', '>>');
118 define('OP_GE', '>=');
119 define('OP_GT', '>');
120 define('OP_INCREMENT', '++');
121 define('OP_DECREMENT', '--');
122 define('OP_PLUS', '+');
123 define('OP_MINUS', '-');
124 define('OP_MUL', '*');
125 define('OP_DIV', '/');
126 define('OP_MOD', '%');
127 define('OP_NOT', '!');
128 define('OP_BITWISE_NOT', '~');
129 define('OP_DOT', '.');
130 define('OP_LEFT_BRACKET', '[');
131 define('OP_RIGHT_BRACKET', ']');
132 define('OP_LEFT_CURLY', '{');
133 define('OP_RIGHT_CURLY', '}');
134 define('OP_LEFT_PAREN', '(');
135 define('OP_RIGHT_PAREN', ')');
136 define('OP_CONDCOMMENT_END', '@*/');
138 define('OP_UNARY_PLUS', 'U+');
139 define('OP_UNARY_MINUS', 'U-');
142 define('KEYWORD_BREAK', 'break');
143 define('KEYWORD_CASE', 'case');
144 define('KEYWORD_CATCH', 'catch');
145 define('KEYWORD_CONST', 'const');
146 define('KEYWORD_CONTINUE', 'continue');
147 define('KEYWORD_DEBUGGER', 'debugger');
148 define('KEYWORD_DEFAULT', 'default');
149 define('KEYWORD_DELETE', 'delete');
150 define('KEYWORD_DO', 'do');
151 define('KEYWORD_ELSE', 'else');
152 define('KEYWORD_ENUM', 'enum');
153 define('KEYWORD_FALSE', 'false');
154 define('KEYWORD_FINALLY', 'finally');
155 define('KEYWORD_FOR', 'for');
156 define('KEYWORD_FUNCTION', 'function');
157 define('KEYWORD_IF', 'if');
158 define('KEYWORD_IN', 'in');
159 define('KEYWORD_INSTANCEOF', 'instanceof');
160 define('KEYWORD_NEW', 'new');
161 define('KEYWORD_NULL', 'null');
162 define('KEYWORD_RETURN', 'return');
163 define('KEYWORD_SWITCH', 'switch');
164 define('KEYWORD_THIS', 'this');
165 define('KEYWORD_THROW', 'throw');
166 define('KEYWORD_TRUE', 'true');
167 define('KEYWORD_TRY', 'try');
168 define('KEYWORD_TYPEOF', 'typeof');
169 define('KEYWORD_VAR', 'var');
170 define('KEYWORD_VOID', 'void');
171 define('KEYWORD_WHILE', 'while');
172 define('KEYWORD_WITH', 'with');
178 private $reserved = array(
179 'break', 'case', 'catch', 'continue', 'default', 'delete', 'do',
180 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof',
181 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var',
182 'void', 'while', 'with',
183 // Words reserved for future use
184 'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger',
185 'double', 'enum', 'export', 'extends', 'final', 'float', 'goto',
186 'implements', 'import', 'int', 'interface', 'long', 'native',
187 'package', 'private', 'protected', 'public', 'short', 'static',
188 'super', 'synchronized', 'throws', 'transient', 'volatile',
189 // These are not reserved, but should be taken into account
190 // in isValidIdentifier (See jslint source code)
191 'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined'
194 private function __construct()
196 $this->parser
= new JSParser($this);
199 public static function minify($js, $filename='')
203 // this is a singleton
205 $instance = new JSMinPlus();
207 return $instance->min($js, $filename);
210 private function min($js, $filename)
214 $n = $this->parser
->parse($js, $filename, 1);
215 return $this->parseTree($n);
219 echo $e->getMessage() . "\n";
225 public function parseTree($n, $noBlockGrouping = false)
236 // we do nothing yet with funDecls or varDecls
237 $noBlockGrouping = true;
241 $childs = $n->treeNodes
;
243 for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++
)
245 $type = $childs[$i]->type
;
246 $t = $this->parseTree($childs[$i]);
253 if ($type == KEYWORD_FUNCTION
&& $childs[$i]->functionForm
== DECLARED_FORM
)
255 // put declared functions on a new line
258 elseif ($type == KEYWORD_VAR
&& $type == $lastType)
260 // multiple var-statements can go into one
261 $t = ',' . substr($t, 4);
277 if ($c > 1 && !$noBlockGrouping)
283 case KEYWORD_FUNCTION
:
284 $s .= 'function' . ($n->name ?
' ' . $n->name
: '') . '(';
285 $params = $n->params
;
286 for ($i = 0, $j = count($params); $i < $j; $i++
)
287 $s .= ($i ?
',' : '') . $params[$i];
288 $s .= '){' . $this->parseTree($n->body
, true) . '}';
292 $s = 'if(' . $this->parseTree($n->condition
) . ')';
293 $thenPart = $this->parseTree($n->thenPart
);
294 $elsePart = $n->elsePart ?
$this->parseTree($n->elsePart
) : null;
296 // empty if-statement
302 // be careful and always make a block out of the thenPart; could be more optimized but is a lot of trouble
303 if ($thenPart != ';' && $thenPart[0] != '{')
304 $thenPart = '{' . $thenPart . '}';
306 $s .= $thenPart . 'else';
308 // we could check for more, but that hardly ever applies so go for performance
309 if ($elsePart[0] != '{')
321 $s = 'switch(' . $this->parseTree($n->discriminant
) . '){';
323 for ($i = 0, $j = count($cases); $i < $j; $i++
)
326 if ($case->type
== KEYWORD_CASE
)
327 $s .= 'case' . ($case->caseLabel
->type
!= TOKEN_STRING ?
' ' : '') . $this->parseTree($case->caseLabel
) . ':';
331 $statement = $this->parseTree($case->statements
, true);
335 // no terminator for last statement
344 $s = 'for(' . ($n->setup ?
$this->parseTree($n->setup
) : '')
345 . ';' . ($n->condition ?
$this->parseTree($n->condition
) : '')
346 . ';' . ($n->update ?
$this->parseTree($n->update
) : '') . ')';
348 $body = $this->parseTree($n->body
);
356 $s = 'while(' . $this->parseTree($n->condition
) . ')';
358 $body = $this->parseTree($n->body
);
366 $s = 'for(' . ($n->varDecl ?
$this->parseTree($n->varDecl
) : $this->parseTree($n->iterator
)) . ' in ' . $this->parseTree($n->object) . ')';
368 $body = $this->parseTree($n->body
);
376 $s = 'do{' . $this->parseTree($n->body
, true) . '}while(' . $this->parseTree($n->condition
) . ')';
380 case KEYWORD_CONTINUE
:
381 $s = $n->value
. ($n->label ?
' ' . $n->label
: '');
385 $s = 'try{' . $this->parseTree($n->tryBlock
, true) . '}';
386 $catchClauses = $n->catchClauses
;
387 for ($i = 0, $j = count($catchClauses); $i < $j; $i++
)
389 $t = $catchClauses[$i];
390 $s .= 'catch(' . $t->varName
. ($t->guard ?
' if ' . $this->parseTree($t->guard
) : '') . '){' . $this->parseTree($t->block
, true) . '}';
392 if ($n->finallyBlock
)
393 $s .= 'finally{' . $this->parseTree($n->finallyBlock
, true) . '}';
401 $t = $this->parseTree($n->value
);
404 if ($this->isWordChar($t[0]) ||
$t[0] == '\\')
413 $s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body
);
418 $s = $n->value
. ' ';
419 $childs = $n->treeNodes
;
420 for ($i = 0, $j = count($childs); $i < $j; $i++
)
423 $s .= ($i ?
',' : '') . $t->name
;
424 $u = $t->initializer
;
426 $s .= '=' . $this->parseTree($u);
431 case KEYWORD_INSTANCEOF
:
432 $left = $this->parseTree($n->treeNodes
[0]);
433 $right = $this->parseTree($n->treeNodes
[1]);
437 if ($this->isWordChar(substr($left, -1)))
442 if ($this->isWordChar($right[0]) ||
$right[0] == '\\')
450 $right = $this->parseTree($n->treeNodes
[0]);
454 if ($this->isWordChar($right[0]) ||
$right[0] == '\\')
461 $s = 'void(' . $this->parseTree($n->treeNodes
[0]) . ')';
464 case KEYWORD_DEBUGGER
:
465 throw new Exception('NOT IMPLEMENTED: DEBUGGER');
468 case TOKEN_CONDCOMMENT_START
:
469 case TOKEN_CONDCOMMENT_END
:
470 $s = $n->value
. ($n->type
== TOKEN_CONDCOMMENT_START ?
' ' : '');
471 $childs = $n->treeNodes
;
472 for ($i = 0, $j = count($childs); $i < $j; $i++
)
473 $s .= $this->parseTree($childs[$i]);
477 if ($expression = $n->expression
)
478 $s = $this->parseTree($expression);
482 $s = $n->label
. ':' . $this->parseTree($n->statement
);
486 $childs = $n->treeNodes
;
487 for ($i = 0, $j = count($childs); $i < $j; $i++
)
488 $s .= ($i ?
',' : '') . $this->parseTree($childs[$i]);
492 $s = $this->parseTree($n->treeNodes
[0]) . $n->value
. $this->parseTree($n->treeNodes
[1]);
496 $s = $this->parseTree($n->treeNodes
[0]) . '?' . $this->parseTree($n->treeNodes
[1]) . ':' . $this->parseTree($n->treeNodes
[2]);
499 case OP_OR
: case OP_AND
:
500 case OP_BITWISE_OR
: case OP_BITWISE_XOR
: case OP_BITWISE_AND
:
501 case OP_EQ
: case OP_NE
: case OP_STRICT_EQ
: case OP_STRICT_NE
:
502 case OP_LT
: case OP_LE
: case OP_GE
: case OP_GT
:
503 case OP_LSH
: case OP_RSH
: case OP_URSH
:
504 case OP_MUL
: case OP_DIV
: case OP_MOD
:
505 $s = $this->parseTree($n->treeNodes
[0]) . $n->type
. $this->parseTree($n->treeNodes
[1]);
510 $left = $this->parseTree($n->treeNodes
[0]);
511 $right = $this->parseTree($n->treeNodes
[1]);
513 switch ($n->treeNodes
[1]->type
)
521 $s = $left . $n->type
. ' ' . $right;
525 //combine concatenated strings with same quote style
526 if ($n->type
== OP_PLUS
&& substr($left, -1) == $right[0])
528 $s = substr($left, 0, -1) . substr($right, 1);
534 $s = $left . $n->type
. $right;
542 $s = $n->value
. $this->parseTree($n->treeNodes
[0]);
548 $s = $this->parseTree($n->treeNodes
[0]) . $n->value
;
550 $s = $n->value
. $this->parseTree($n->treeNodes
[0]);
554 $s = $this->parseTree($n->treeNodes
[0]) . '.' . $this->parseTree($n->treeNodes
[1]);
558 $s = $this->parseTree($n->treeNodes
[0]);
559 // See if we can replace named index with a dot saving 3 bytes
560 if ( $n->treeNodes
[0]->type
== TOKEN_IDENTIFIER
&&
561 $n->treeNodes
[1]->type
== TOKEN_STRING
&&
562 $this->isValidIdentifier(substr($n->treeNodes
[1]->value
, 1, -1))
564 $s .= '.' . substr($n->treeNodes
[1]->value
, 1, -1);
566 $s .= '[' . $this->parseTree($n->treeNodes
[1]) . ']';
570 $childs = $n->treeNodes
;
571 for ($i = 0, $j = count($childs); $i < $j; $i++
)
572 $s .= ($i ?
',' : '') . $this->parseTree($childs[$i]);
576 $s = $this->parseTree($n->treeNodes
[0]) . '(' . $this->parseTree($n->treeNodes
[1]) . ')';
580 case JS_NEW_WITH_ARGS
:
581 $s = 'new ' . $this->parseTree($n->treeNodes
[0]) . '(' . ($n->type
== JS_NEW_WITH_ARGS ?
$this->parseTree($n->treeNodes
[1]) : '') . ')';
586 $childs = $n->treeNodes
;
587 for ($i = 0, $j = count($childs); $i < $j; $i++
)
589 $s .= ($i ?
',' : '') . $this->parseTree($childs[$i]);
596 $childs = $n->treeNodes
;
597 for ($i = 0, $j = count($childs); $i < $j; $i++
)
602 if ($t->type
== JS_PROPERTY_INIT
)
604 // Ditch the quotes when the index is a valid identifier
605 if ( $t->treeNodes
[0]->type
== TOKEN_STRING
&&
606 $this->isValidIdentifier(substr($t->treeNodes
[0]->value
, 1, -1))
608 $s .= substr($t->treeNodes
[0]->value
, 1, -1);
610 $s .= $t->treeNodes
[0]->value
;
612 $s .= ':' . $this->parseTree($t->treeNodes
[1]);
616 $s .= $t->type
== JS_GETTER ?
'get' : 'set';
617 $s .= ' ' . $t->name
. '(';
618 $params = $t->params
;
619 for ($i = 0, $j = count($params); $i < $j; $i++
)
620 $s .= ($i ?
',' : '') . $params[$i];
621 $s .= '){' . $this->parseTree($t->body
, true) . '}';
629 if (preg_match('/^([1-9]+)(0{3,})$/', $s, $m))
630 $s = $m[1] . 'e' . strlen($m[2]);
633 case KEYWORD_NULL
: case KEYWORD_THIS
: case KEYWORD_TRUE
: case KEYWORD_FALSE
:
634 case TOKEN_IDENTIFIER
: case TOKEN_STRING
: case TOKEN_REGEXP
:
640 $n->treeNodes
[0]->type
,
642 JS_ARRAY_INIT
, JS_OBJECT_INIT
, JS_GROUP
,
643 TOKEN_NUMBER
, TOKEN_STRING
, TOKEN_REGEXP
, TOKEN_IDENTIFIER
,
644 KEYWORD_NULL
, KEYWORD_THIS
, KEYWORD_TRUE
, KEYWORD_FALSE
648 $s = $this->parseTree($n->treeNodes
[0]);
652 $s = '(' . $this->parseTree($n->treeNodes
[0]) . ')';
657 throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type
);
663 private function isValidIdentifier($string)
665 return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved
);
668 private function isWordChar($char)
670 return $char == '_' ||
$char == '$' ||
ctype_alnum($char);
679 private $opPrecedence = array(
682 '=' => 2, '?' => 2, ':' => 2,
683 // The above all have to have the same precedence, see bug 330975
689 '==' => 9, '!=' => 9, '===' => 9, '!==' => 9,
690 '<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10,
691 '<<' => 11, '>>' => 11, '>>>' => 11,
692 '+' => 12, '-' => 12,
693 '*' => 13, '/' => 13, '%' => 13,
694 'delete' => 14, 'void' => 14, 'typeof' => 14,
695 '!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14,
696 '++' => 15, '--' => 15,
699 JS_NEW_WITH_ARGS
=> 0, JS_INDEX
=> 0, JS_CALL
=> 0,
700 JS_ARRAY_INIT
=> 0, JS_OBJECT_INIT
=> 0, JS_GROUP
=> 0
703 private $opArity = array(
712 '==' => 2, '!=' => 2, '===' => 2, '!==' => 2,
713 '<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2,
714 '<<' => 2, '>>' => 2, '>>>' => 2,
716 '*' => 2, '/' => 2, '%' => 2,
717 'delete' => 1, 'void' => 1, 'typeof' => 1,
718 '!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1,
719 '++' => 1, '--' => 1,
722 JS_NEW_WITH_ARGS
=> 2, JS_INDEX
=> 2, JS_CALL
=> 2,
723 JS_ARRAY_INIT
=> 1, JS_OBJECT_INIT
=> 1, JS_GROUP
=> 1,
724 TOKEN_CONDCOMMENT_START
=> 1, TOKEN_CONDCOMMENT_END
=> 1
727 public function __construct($minifier=null)
729 $this->minifier
= $minifier;
730 $this->t
= new JSTokenizer();
733 public function parse($s, $f, $l)
735 // initialize tokenizer
736 $this->t
->init($s, $f, $l);
738 $x = new JSCompilerContext(false);
739 $n = $this->Script($x);
740 if (!$this->t
->isDone())
741 throw $this->t
->newSyntaxError('Syntax error');
746 private function Script($x)
748 $n = $this->Statements($x);
749 $n->type
= JS_SCRIPT
;
750 $n->funDecls
= $x->funDecls
;
751 $n->varDecls
= $x->varDecls
;
756 $n->value
= $this->minifier
->parseTree($n);
758 // clear tree from node to save memory
759 $n->treeNodes
= null;
763 $n->type
= JS_MINIFIED
;
769 private function Statements($x)
771 $n = new JSNode($this->t
, JS_BLOCK
);
772 array_push($x->stmtStack
, $n);
774 while (!$this->t
->isDone() && $this->t
->peek() != OP_RIGHT_CURLY
)
775 $n->addNode($this->Statement($x));
777 array_pop($x->stmtStack
);
782 private function Block($x)
784 $this->t
->mustMatch(OP_LEFT_CURLY
);
785 $n = $this->Statements($x);
786 $this->t
->mustMatch(OP_RIGHT_CURLY
);
791 private function Statement($x)
793 $tt = $this->t
->get();
796 // Cases for statements ending in a right curly return early, avoiding the
797 // common semicolon insertion magic after this switch.
800 case KEYWORD_FUNCTION
:
801 return $this->FunctionDefinition(
804 count($x->stmtStack
) > 1 ? STATEMENT_FORM
: DECLARED_FORM
809 $n = $this->Statements($x);
810 $this->t
->mustMatch(OP_RIGHT_CURLY
);
814 $n = new JSNode($this->t
);
815 $n->condition
= $this->ParenExpression($x);
816 array_push($x->stmtStack
, $n);
817 $n->thenPart
= $this->Statement($x);
818 $n->elsePart
= $this->t
->match(KEYWORD_ELSE
) ?
$this->Statement($x) : null;
819 array_pop($x->stmtStack
);
823 $n = new JSNode($this->t
);
824 $this->t
->mustMatch(OP_LEFT_PAREN
);
825 $n->discriminant
= $this->Expression($x);
826 $this->t
->mustMatch(OP_RIGHT_PAREN
);
828 $n->defaultIndex
= -1;
830 array_push($x->stmtStack
, $n);
832 $this->t
->mustMatch(OP_LEFT_CURLY
);
834 while (($tt = $this->t
->get()) != OP_RIGHT_CURLY
)
838 case KEYWORD_DEFAULT
:
839 if ($n->defaultIndex
>= 0)
840 throw $this->t
->newSyntaxError('More than one switch default');
843 $n2 = new JSNode($this->t
);
844 if ($tt == KEYWORD_DEFAULT
)
845 $n->defaultIndex
= count($n->cases
);
847 $n2->caseLabel
= $this->Expression($x, OP_COLON
);
850 throw $this->t
->newSyntaxError('Invalid switch case');
853 $this->t
->mustMatch(OP_COLON
);
854 $n2->statements
= new JSNode($this->t
, JS_BLOCK
);
855 while (($tt = $this->t
->peek()) != KEYWORD_CASE
&& $tt != KEYWORD_DEFAULT
&& $tt != OP_RIGHT_CURLY
)
856 $n2->statements
->addNode($this->Statement($x));
858 array_push($n->cases
, $n2);
861 array_pop($x->stmtStack
);
865 $n = new JSNode($this->t
);
867 $this->t
->mustMatch(OP_LEFT_PAREN
);
869 if (($tt = $this->t
->peek()) != OP_SEMICOLON
)
871 $x->inForLoopInit
= true;
872 if ($tt == KEYWORD_VAR ||
$tt == KEYWORD_CONST
)
875 $n2 = $this->Variables($x);
879 $n2 = $this->Expression($x);
881 $x->inForLoopInit
= false;
884 if ($n2 && $this->t
->match(KEYWORD_IN
))
886 $n->type
= JS_FOR_IN
;
887 if ($n2->type
== KEYWORD_VAR
)
889 if (count($n2->treeNodes
) != 1)
891 throw $this->t
->SyntaxError(
892 'Invalid for..in left-hand side',
898 // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name.
899 $n->iterator
= $n2->treeNodes
[0];
908 $n->object = $this->Expression($x);
912 $n->setup
= $n2 ?
: null;
913 $this->t
->mustMatch(OP_SEMICOLON
);
914 $n->condition
= $this->t
->peek() == OP_SEMICOLON ?
null : $this->Expression($x);
915 $this->t
->mustMatch(OP_SEMICOLON
);
916 $n->update
= $this->t
->peek() == OP_RIGHT_PAREN ?
null : $this->Expression($x);
919 $this->t
->mustMatch(OP_RIGHT_PAREN
);
920 $n->body
= $this->nest($x, $n);
924 $n = new JSNode($this->t
);
926 $n->condition
= $this->ParenExpression($x);
927 $n->body
= $this->nest($x, $n);
931 $n = new JSNode($this->t
);
933 $n->body
= $this->nest($x, $n, KEYWORD_WHILE
);
934 $n->condition
= $this->ParenExpression($x);
935 if (!$x->ecmaStrictMode
)
937 // <script language="JavaScript"> (without version hints) may need
938 // automatic semicolon insertion without a newline after do-while.
939 // See https://bugzilla.mozilla.org/show_bug.cgi?id=238945.
940 $this->t
->match(OP_SEMICOLON
);
946 case KEYWORD_CONTINUE
:
947 $n = new JSNode($this->t
);
949 if ($this->t
->peekOnSameLine() == TOKEN_IDENTIFIER
)
952 $n->label
= $this->t
->currentToken()->value
;
963 throw $this->t
->newSyntaxError('Label not found');
965 while ($ss[$i]->label
!= $label);
972 throw $this->t
->newSyntaxError('Invalid ' . $tt);
974 while (!$ss[$i]->isLoop
&& ($tt != KEYWORD_BREAK ||
$ss[$i]->type
!= KEYWORD_SWITCH
));
979 $n = new JSNode($this->t
);
980 $n->tryBlock
= $this->Block($x);
981 $n->catchClauses
= array();
983 while ($this->t
->match(KEYWORD_CATCH
))
985 $n2 = new JSNode($this->t
);
986 $this->t
->mustMatch(OP_LEFT_PAREN
);
987 $n2->varName
= $this->t
->mustMatch(TOKEN_IDENTIFIER
)->value
;
989 if ($this->t
->match(KEYWORD_IF
))
991 if ($x->ecmaStrictMode
)
992 throw $this->t
->newSyntaxError('Illegal catch guard');
994 if (count($n->catchClauses
) && !end($n->catchClauses
)->guard
)
995 throw $this->t
->newSyntaxError('Guarded catch after unguarded');
997 $n2->guard
= $this->Expression($x);
1004 $this->t
->mustMatch(OP_RIGHT_PAREN
);
1005 $n2->block
= $this->Block($x);
1006 array_push($n->catchClauses
, $n2);
1009 if ($this->t
->match(KEYWORD_FINALLY
))
1010 $n->finallyBlock
= $this->Block($x);
1012 if (!count($n->catchClauses
) && !$n->finallyBlock
)
1013 throw $this->t
->newSyntaxError('Invalid try statement');
1017 case KEYWORD_FINALLY
:
1018 throw $this->t
->newSyntaxError($tt . ' without preceding try');
1021 $n = new JSNode($this->t
);
1022 $n->value
= $this->Expression($x);
1025 case KEYWORD_RETURN
:
1026 if (!$x->inFunction
)
1027 throw $this->t
->newSyntaxError('Invalid return');
1029 $n = new JSNode($this->t
);
1030 $tt = $this->t
->peekOnSameLine();
1031 if ($tt != TOKEN_END
&& $tt != TOKEN_NEWLINE
&& $tt != OP_SEMICOLON
&& $tt != OP_RIGHT_CURLY
)
1032 $n->value
= $this->Expression($x);
1038 $n = new JSNode($this->t
);
1039 $n->object = $this->ParenExpression($x);
1040 $n->body
= $this->nest($x, $n);
1045 $n = $this->Variables($x);
1048 case TOKEN_CONDCOMMENT_START
:
1049 case TOKEN_CONDCOMMENT_END
:
1050 $n = new JSNode($this->t
);
1053 case KEYWORD_DEBUGGER
:
1054 $n = new JSNode($this->t
);
1059 $n = new JSNode($this->t
, OP_SEMICOLON
);
1060 $n->expression
= null;
1064 if ($tt == TOKEN_IDENTIFIER
)
1066 $this->t
->scanOperand
= false;
1067 $tt = $this->t
->peek();
1068 $this->t
->scanOperand
= true;
1069 if ($tt == OP_COLON
)
1071 $label = $this->t
->currentToken()->value
;
1072 $ss = $x->stmtStack
;
1073 for ($i = count($ss) - 1; $i >= 0; --$i)
1075 if ($ss[$i]->label
== $label)
1076 throw $this->t
->newSyntaxError('Duplicate label');
1080 $n = new JSNode($this->t
, JS_LABEL
);
1082 $n->statement
= $this->nest($x, $n);
1088 $n = new JSNode($this->t
, OP_SEMICOLON
);
1090 $n->expression
= $this->Expression($x);
1091 $n->end
= $n->expression
->end
;
1095 if ($this->t
->lineno
== $this->t
->currentToken()->lineno
)
1097 $tt = $this->t
->peekOnSameLine();
1098 if ($tt != TOKEN_END
&& $tt != TOKEN_NEWLINE
&& $tt != OP_SEMICOLON
&& $tt != OP_RIGHT_CURLY
)
1099 throw $this->t
->newSyntaxError('Missing ; before statement');
1102 $this->t
->match(OP_SEMICOLON
);
1107 private function FunctionDefinition($x, $requireName, $functionForm)
1109 $f = new JSNode($this->t
);
1111 if ($f->type
!= KEYWORD_FUNCTION
)
1112 $f->type
= ($f->value
== 'get') ? JS_GETTER
: JS_SETTER
;
1114 if ($this->t
->match(TOKEN_IDENTIFIER
))
1115 $f->name
= $this->t
->currentToken()->value
;
1116 elseif ($requireName)
1117 throw $this->t
->newSyntaxError('Missing function identifier');
1119 $this->t
->mustMatch(OP_LEFT_PAREN
);
1120 $f->params
= array();
1122 while (($tt = $this->t
->get()) != OP_RIGHT_PAREN
)
1124 if ($tt != TOKEN_IDENTIFIER
)
1125 throw $this->t
->newSyntaxError('Missing formal parameter');
1127 array_push($f->params
, $this->t
->currentToken()->value
);
1129 if ($this->t
->peek() != OP_RIGHT_PAREN
)
1130 $this->t
->mustMatch(OP_COMMA
);
1133 $this->t
->mustMatch(OP_LEFT_CURLY
);
1135 $x2 = new JSCompilerContext(true);
1136 $f->body
= $this->Script($x2);
1138 $this->t
->mustMatch(OP_RIGHT_CURLY
);
1139 $f->end
= $this->t
->currentToken()->end
;
1141 $f->functionForm
= $functionForm;
1142 if ($functionForm == DECLARED_FORM
)
1143 array_push($x->funDecls
, $f);
1148 private function Variables($x)
1150 $n = new JSNode($this->t
);
1154 $this->t
->mustMatch(TOKEN_IDENTIFIER
);
1156 $n2 = new JSNode($this->t
);
1157 $n2->name
= $n2->value
;
1159 if ($this->t
->match(OP_ASSIGN
))
1161 if ($this->t
->currentToken()->assignOp
)
1162 throw $this->t
->newSyntaxError('Invalid variable initialization');
1164 $n2->initializer
= $this->Expression($x, OP_COMMA
);
1167 $n2->readOnly
= $n->type
== KEYWORD_CONST
;
1170 array_push($x->varDecls
, $n2);
1172 while ($this->t
->match(OP_COMMA
));
1177 private function Expression($x, $stop=false)
1179 $operators = array();
1180 $operands = array();
1183 $bl = $x->bracketLevel
;
1184 $cl = $x->curlyLevel
;
1185 $pl = $x->parenLevel
;
1186 $hl = $x->hookLevel
;
1188 while (($tt = $this->t
->get()) != TOKEN_END
)
1191 $x->bracketLevel
== $bl &&
1192 $x->curlyLevel
== $cl &&
1193 $x->parenLevel
== $pl &&
1194 $x->hookLevel
== $hl
1197 // Stop only if tt matches the optional stop parameter, and that
1198 // token is not quoted by some kind of bracket.
1205 // NB: cannot be empty, Statement handled that.
1209 if ($this->t
->scanOperand
)
1212 while ( !empty($operators) &&
1213 $this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[$tt]
1215 $this->reduce($operators, $operands);
1217 array_push($operators, new JSNode($this->t
));
1220 $this->t
->scanOperand
= true;
1221 $n = $this->Expression($x);
1223 if (!$this->t
->match(OP_COLON
))
1227 array_push($operands, $n);
1234 throw $this->t
->newSyntaxError('Invalid label');
1238 if ($this->t
->scanOperand
)
1241 // Use >, not >=, for right-associative ASSIGN
1242 while ( !empty($operators) &&
1243 $this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[$tt]
1245 $this->reduce($operators, $operands);
1247 array_push($operators, new JSNode($this->t
));
1248 end($operands)->assignOp
= $this->t
->currentToken()->assignOp
;
1249 $this->t
->scanOperand
= true;
1253 // An in operator should not be parsed if we're parsing the head of
1254 // a for (...) loop, unless it is in the then part of a conditional
1255 // expression, or parenthesized somehow.
1256 if ($x->inForLoopInit
&& !$x->hookLevel
&&
1257 !$x->bracketLevel
&& !$x->curlyLevel
&&
1263 // A comma operator should not be parsed if we're parsing the then part
1264 // of a conditional expression unless it's parenthesized somehow.
1265 if ($tt == OP_COMMA
&& $x->hookLevel
&&
1266 !$x->bracketLevel
&& !$x->curlyLevel
&&
1270 // Treat comma as left-associative so reduce can fold left-heavy
1271 // COMMA trees into a single array.
1276 case OP_BITWISE_XOR
:
1277 case OP_BITWISE_AND
:
1278 case OP_EQ
: case OP_NE
: case OP_STRICT_EQ
: case OP_STRICT_NE
:
1279 case OP_LT
: case OP_LE
: case OP_GE
: case OP_GT
:
1280 case KEYWORD_INSTANCEOF
:
1281 case OP_LSH
: case OP_RSH
: case OP_URSH
:
1282 case OP_PLUS
: case OP_MINUS
:
1283 case OP_MUL
: case OP_DIV
: case OP_MOD
:
1285 if ($this->t
->scanOperand
)
1288 while ( !empty($operators) &&
1289 $this->opPrecedence
[end($operators)->type
] >= $this->opPrecedence
[$tt]
1291 $this->reduce($operators, $operands);
1295 $tt = $this->t
->get();
1296 if (!$this->isKeyword($tt) && $tt !== TOKEN_IDENTIFIER
)
1297 throw $this->t
->newSyntaxError("Unexpected token; token identifier or keyword expected.");
1299 array_push($operands, new JSNode($this->t
, OP_DOT
, array_pop($operands), new JSNode($this->t
)));
1303 array_push($operators, new JSNode($this->t
));
1304 $this->t
->scanOperand
= true;
1308 case KEYWORD_DELETE
: case KEYWORD_VOID
: case KEYWORD_TYPEOF
:
1309 case OP_NOT
: case OP_BITWISE_NOT
: case OP_UNARY_PLUS
: case OP_UNARY_MINUS
:
1311 if (!$this->t
->scanOperand
)
1314 array_push($operators, new JSNode($this->t
));
1317 case OP_INCREMENT
: case OP_DECREMENT
:
1318 if ($this->t
->scanOperand
)
1320 array_push($operators, new JSNode($this->t
)); // prefix increment or decrement
1324 // Don't cross a line boundary for postfix {in,de}crement.
1325 $t = $this->t
->tokens
[($this->t
->tokenIndex +
$this->t
->lookahead
- 1) & 3];
1326 if ($t && $t->lineno
!= $this->t
->lineno
)
1329 if (!empty($operators))
1331 // Use >, not >=, so postfix has higher precedence than prefix.
1332 while ($this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[$tt])
1333 $this->reduce($operators, $operands);
1336 $n = new JSNode($this->t
, $tt, array_pop($operands));
1338 array_push($operands, $n);
1342 case KEYWORD_FUNCTION
:
1343 if (!$this->t
->scanOperand
)
1346 array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM
));
1347 $this->t
->scanOperand
= false;
1350 case KEYWORD_NULL
: case KEYWORD_THIS
: case KEYWORD_TRUE
: case KEYWORD_FALSE
:
1351 case TOKEN_IDENTIFIER
: case TOKEN_NUMBER
: case TOKEN_STRING
: case TOKEN_REGEXP
:
1352 if (!$this->t
->scanOperand
)
1355 array_push($operands, new JSNode($this->t
));
1356 $this->t
->scanOperand
= false;
1359 case TOKEN_CONDCOMMENT_START
:
1360 case TOKEN_CONDCOMMENT_END
:
1361 if ($this->t
->scanOperand
)
1362 array_push($operators, new JSNode($this->t
));
1364 array_push($operands, new JSNode($this->t
));
1367 case OP_LEFT_BRACKET
:
1368 if ($this->t
->scanOperand
)
1370 // Array initialiser. Parse using recursive descent, as the
1371 // sub-grammar here is not an operator grammar.
1372 $n = new JSNode($this->t
, JS_ARRAY_INIT
);
1373 while (($tt = $this->t
->peek()) != OP_RIGHT_BRACKET
)
1375 if ($tt == OP_COMMA
)
1382 $n->addNode($this->Expression($x, OP_COMMA
));
1383 if (!$this->t
->match(OP_COMMA
))
1387 $this->t
->mustMatch(OP_RIGHT_BRACKET
);
1388 array_push($operands, $n);
1389 $this->t
->scanOperand
= false;
1393 // Property indexing operator.
1394 array_push($operators, new JSNode($this->t
, JS_INDEX
));
1395 $this->t
->scanOperand
= true;
1400 case OP_RIGHT_BRACKET
:
1401 if ($this->t
->scanOperand ||
$x->bracketLevel
== $bl)
1404 while ($this->reduce($operators, $operands)->type
!= JS_INDEX
)
1411 if (!$this->t
->scanOperand
)
1414 // Object initialiser. As for array initialisers (see above),
1415 // parse using recursive descent.
1417 $n = new JSNode($this->t
, JS_OBJECT_INIT
);
1418 while (!$this->t
->match(OP_RIGHT_CURLY
))
1422 $tt = $this->t
->get();
1423 $tv = $this->t
->currentToken()->value
;
1424 if (($tv == 'get' ||
$tv == 'set') && $this->t
->peek() == TOKEN_IDENTIFIER
)
1426 if ($x->ecmaStrictMode
)
1427 throw $this->t
->newSyntaxError('Illegal property accessor');
1429 $n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM
));
1433 // Accept keywords as property names by treating
1434 // them similarly with identifiers
1435 if ($this->isKeyword($tt))
1436 $tt = TOKEN_IDENTIFIER
;
1440 case TOKEN_IDENTIFIER
:
1443 $id = new JSNode($this->t
);
1446 case OP_RIGHT_CURLY
:
1447 if ($x->ecmaStrictMode
)
1448 throw $this->t
->newSyntaxError('Illegal trailing ,');
1452 throw $this->t
->newSyntaxError('Invalid property name');
1455 $this->t
->mustMatch(OP_COLON
);
1456 $n->addNode(new JSNode($this->t
, JS_PROPERTY_INIT
, $id, $this->Expression($x, OP_COMMA
)));
1459 while ($this->t
->match(OP_COMMA
));
1461 $this->t
->mustMatch(OP_RIGHT_CURLY
);
1465 array_push($operands, $n);
1466 $this->t
->scanOperand
= false;
1470 case OP_RIGHT_CURLY
:
1471 if (!$this->t
->scanOperand
&& $x->curlyLevel
!= $cl)
1472 throw new Exception('PANIC: right curly botch');
1476 if ($this->t
->scanOperand
)
1478 array_push($operators, new JSNode($this->t
, JS_GROUP
));
1482 while ( !empty($operators) &&
1483 $this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[KEYWORD_NEW
]
1485 $this->reduce($operators, $operands);
1487 // Handle () now, to regularize the n-ary case for n > 0.
1488 // We must set scanOperand in case there are arguments and
1489 // the first one is a regexp or unary+/-.
1490 $n = end($operators);
1491 $this->t
->scanOperand
= true;
1492 if ($this->t
->match(OP_RIGHT_PAREN
))
1494 if ($n && $n->type
== KEYWORD_NEW
)
1496 array_pop($operators);
1497 $n->addNode(array_pop($operands));
1501 $n = new JSNode($this->t
, JS_CALL
, array_pop($operands), new JSNode($this->t
, JS_LIST
));
1504 array_push($operands, $n);
1505 $this->t
->scanOperand
= false;
1509 if ($n && $n->type
== KEYWORD_NEW
)
1510 $n->type
= JS_NEW_WITH_ARGS
;
1512 array_push($operators, new JSNode($this->t
, JS_CALL
));
1518 case OP_RIGHT_PAREN
:
1519 if ($this->t
->scanOperand ||
$x->parenLevel
== $pl)
1522 while (($tt = $this->reduce($operators, $operands)->type
) != JS_GROUP
&&
1523 $tt != JS_CALL
&& $tt != JS_NEW_WITH_ARGS
1529 if ($tt != JS_GROUP
)
1531 $n = end($operands);
1532 if ($n->treeNodes
[1]->type
!= OP_COMMA
)
1533 $n->treeNodes
[1] = new JSNode($this->t
, JS_LIST
, $n->treeNodes
[1]);
1535 $n->treeNodes
[1]->type
= JS_LIST
;
1541 // Automatic semicolon insertion means we may scan across a newline
1542 // and into the beginning of another statement. If so, break out of
1543 // the while loop and let the t.scanOperand logic handle errors.
1549 if ($x->hookLevel
!= $hl)
1550 throw $this->t
->newSyntaxError('Missing : in conditional expression');
1552 if ($x->parenLevel
!= $pl)
1553 throw $this->t
->newSyntaxError('Missing ) in parenthetical');
1555 if ($x->bracketLevel
!= $bl)
1556 throw $this->t
->newSyntaxError('Missing ] in index expression');
1558 if ($this->t
->scanOperand
)
1559 throw $this->t
->newSyntaxError('Missing operand');
1561 // Resume default mode, scanning for operands, not operators.
1562 $this->t
->scanOperand
= true;
1565 while (count($operators))
1566 $this->reduce($operators, $operands);
1568 return array_pop($operands);
1571 private function ParenExpression($x)
1573 $this->t
->mustMatch(OP_LEFT_PAREN
);
1574 $n = $this->Expression($x);
1575 $this->t
->mustMatch(OP_RIGHT_PAREN
);
1580 // Statement stack and nested statement handler.
1581 private function nest($x, $node, $end = false)
1583 array_push($x->stmtStack
, $node);
1584 $n = $this->statement($x);
1585 array_pop($x->stmtStack
);
1588 $this->t
->mustMatch($end);
1593 private function reduce(&$operators, &$operands)
1595 $n = array_pop($operators);
1597 $arity = $this->opArity
[$op];
1598 $c = count($operands);
1601 // Flatten left-associative trees
1604 $left = $operands[$c - 2];
1605 if ($left->type
== $op)
1607 $right = array_pop($operands);
1608 $left->addNode($right);
1615 // Always use push to add operands to n, to update start and end
1616 $a = array_splice($operands, $c - $arity);
1617 for ($i = 0; $i < $arity; $i++
)
1618 $n->addNode($a[$i]);
1620 // Include closing bracket or postfix operator in [start,end]
1621 $te = $this->t
->currentToken()->end
;
1625 array_push($operands, $n);
1630 private function isKeyword($tt)
1637 case KEYWORD_CONTINUE
:
1638 case KEYWORD_DEBUGGER
:
1639 case KEYWORD_DEFAULT
:
1640 case KEYWORD_DELETE
:
1645 case KEYWORD_FINALLY
:
1647 case KEYWORD_FUNCTION
:
1650 case KEYWORD_INSTANCEOF
:
1653 case KEYWORD_RETURN
:
1654 case KEYWORD_SWITCH
:
1659 case KEYWORD_TYPEOF
:
1671 class JSCompilerContext
1673 public $inFunction = false;
1674 public $inForLoopInit = false;
1675 public $ecmaStrictMode = false;
1676 public $bracketLevel = 0;
1677 public $curlyLevel = 0;
1678 public $parenLevel = 0;
1679 public $hookLevel = 0;
1681 public $stmtStack = array();
1682 public $funDecls = array();
1683 public $varDecls = array();
1685 public function __construct($inFunction)
1687 $this->inFunction
= $inFunction;
1699 public $treeNodes = array();
1700 public $funDecls = array();
1701 public $varDecls = array();
1703 public function __construct($t, $type=0)
1705 if ($token = $t->currentToken())
1707 $this->type
= $type ?
: $token->type
;
1708 $this->value
= $token->value
;
1709 $this->lineno
= $token->lineno
;
1710 $this->start
= $token->start
;
1711 $this->end
= $token->end
;
1715 $this->type
= $type;
1716 $this->lineno
= $t->lineno
;
1719 if (($numargs = func_num_args()) > 2)
1721 $args = func_get_args();
1722 for ($i = 2; $i < $numargs; $i++
)
1723 $this->addNode($args[$i]);
1727 // we don't want to bloat our object with all kind of specific properties, so we use overloading
1728 public function __set($name, $value)
1730 $this->$name = $value;
1733 public function __get($name)
1735 if (isset($this->$name))
1736 return $this->$name;
1741 public function addNode($node)
1745 if ($node->start
< $this->start
)
1746 $this->start
= $node->start
;
1747 if ($this->end
< $node->end
)
1748 $this->end
= $node->end
;
1751 $this->treeNodes
[] = $node;
1757 private $cursor = 0;
1760 public $tokens = array();
1761 public $tokenIndex = 0;
1762 public $lookahead = 0;
1763 public $scanNewlines = false;
1764 public $scanOperand = true;
1769 private $keywords = array(
1771 'case', 'catch', 'const', 'continue',
1772 'debugger', 'default', 'delete', 'do',
1774 'false', 'finally', 'for', 'function',
1775 'if', 'in', 'instanceof',
1779 'this', 'throw', 'true', 'try', 'typeof',
1784 private $opTypeNames = array(
1785 ';', ',', '?', ':', '||', '&&', '|', '^',
1786 '&', '===', '==', '=', '!==', '!=', '<<', '<=',
1787 '<', '>>>', '>>', '>=', '>', '++', '--', '+',
1788 '-', '*', '/', '%', '!', '~', '.', '[',
1789 ']', '{', '}', '(', ')', '@*/'
1792 private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
1795 public function __construct()
1797 $this->opRegExp
= '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames
)) . ')#';
1800 public function init($source, $filename = '', $lineno = 1)
1802 $this->source
= $source;
1803 $this->filename
= $filename ?
: '[inline]';
1804 $this->lineno
= $lineno;
1807 $this->tokens
= array();
1808 $this->tokenIndex
= 0;
1809 $this->lookahead
= 0;
1810 $this->scanNewlines
= false;
1811 $this->scanOperand
= true;
1814 public function getInput($chunksize)
1817 return substr($this->source
, $this->cursor
, $chunksize);
1819 return substr($this->source
, $this->cursor
);
1822 public function isDone()
1824 return $this->peek() == TOKEN_END
;
1827 public function match($tt)
1829 return $this->get() == $tt ||
$this->unget();
1832 public function mustMatch($tt)
1834 if (!$this->match($tt))
1835 throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
1837 return $this->currentToken();
1840 public function peek()
1842 if ($this->lookahead
)
1844 $next = $this->tokens
[($this->tokenIndex +
$this->lookahead
) & 3];
1845 if ($this->scanNewlines
&& $next->lineno
!= $this->lineno
)
1846 $tt = TOKEN_NEWLINE
;
1859 public function peekOnSameLine()
1861 $this->scanNewlines
= true;
1862 $tt = $this->peek();
1863 $this->scanNewlines
= false;
1868 public function currentToken()
1870 if (!empty($this->tokens
))
1871 return $this->tokens
[$this->tokenIndex
];
1874 public function get($chunksize = 1000)
1876 while($this->lookahead
)
1879 $this->tokenIndex
= ($this->tokenIndex +
1) & 3;
1880 $token = $this->tokens
[$this->tokenIndex
];
1881 if ($token->type
!= TOKEN_NEWLINE ||
$this->scanNewlines
)
1882 return $token->type
;
1885 $conditional_comment = false;
1887 // strip whitespace and comments
1890 $input = $this->getInput($chunksize);
1892 // whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
1893 $re = $this->scanNewlines ?
'/^[ \r\t]+/' : '/^\s+/';
1894 if (preg_match($re, $input, $match))
1896 $spaces = $match[0];
1897 $spacelen = strlen($spaces);
1898 $this->cursor +
= $spacelen;
1899 if (!$this->scanNewlines
)
1900 $this->lineno +
= substr_count($spaces, "\n");
1902 if ($spacelen == $chunksize)
1903 continue; // complete chunk contained whitespace
1905 $input = $this->getInput($chunksize);
1906 if ($input == '' ||
$input[0] != '/')
1911 if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match))
1916 // retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
1921 // check if this is a conditional (JScript) comment
1922 if (!empty($match[1]))
1924 $match[0] = '/*' . $match[1];
1925 $conditional_comment = true;
1930 $this->cursor +
= strlen($match[0]);
1931 $this->lineno +
= substr_count($match[0], "\n");
1940 elseif ($conditional_comment)
1942 $tt = TOKEN_CONDCOMMENT_START
;
1950 if (($input[1] == 'x' ||
$input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match))
1957 case '1': case '2': case '3': case '4': case '5':
1958 case '6': case '7': case '8': case '9':
1959 // should always match
1960 preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match);
1965 if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match))
1972 return $this->get(null); // retry with a full chunk fetch
1974 throw $this->newSyntaxError('Unterminated string literal');
1979 if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match))
1986 return $this->get(null); // retry with a full chunk fetch
1988 throw $this->newSyntaxError('Unterminated string literal');
1993 if ($this->scanOperand
&& preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
2011 // should always match
2012 preg_match($this->opRegExp
, $input, $match);
2014 if (in_array($op, $this->assignOps
) && $input[strlen($op)] == '=')
2022 if ($this->scanOperand
)
2025 $tt = OP_UNARY_PLUS
;
2026 elseif ($op == OP_MINUS
)
2027 $tt = OP_UNARY_MINUS
;
2034 if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
2052 // these are all single
2053 $match = array($input[0]);
2058 // check end of conditional comment
2059 if (substr($input, 0, 3) == '@*/')
2061 $match = array('@*/');
2062 $tt = TOKEN_CONDCOMMENT_END
;
2065 throw $this->newSyntaxError('Illegal token');
2069 if ($this->scanNewlines
)
2071 $match = array("\n");
2072 $tt = TOKEN_NEWLINE
;
2075 throw $this->newSyntaxError('Illegal token');
2079 // Fast path for identifiers: word chars followed by whitespace or various other tokens.
2080 // Note we don't need to exclude digits in the first char, as they've already been found
2082 if (!preg_match('/^[$\w]+(?=[\s\/\|\^\&<>\+\-\*%=!.;,\?:~\[\]\{\}\(\)@])/', $input, $match))
2084 // Character classes per ECMA-262 edition 5.1 section 7.6
2085 // Per spec, must accept Unicode 3.0, *may* accept later versions.
2086 // We'll take whatever PCRE understands, which should be more recent.
2087 $identifierStartChars = "\\p{L}\\p{Nl}" . # UnicodeLetter
2090 $identifierPartChars = $identifierStartChars .
2091 "\\p{Mn}\\p{Mc}" . # UnicodeCombiningMark
2092 "\\p{Nd}" . # UnicodeDigit
2093 "\\p{Pc}"; # UnicodeConnectorPunctuation
2094 $unicodeEscape = "\\\\u[0-9A-F-a-f]{4}";
2095 $identifierRegex = "/^" .
2096 "(?:[$identifierStartChars]|$unicodeEscape)" .
2097 "(?:[$identifierPartChars]|$unicodeEscape)*" .
2099 if (preg_match($identifierRegex, $input, $match))
2101 if (strpos($match[0], '\\') !== false) {
2102 // Per ECMA-262 edition 5.1, section 7.6 escape sequences should behave as if they were
2103 // the original chars, but only within the boundaries of the identifier.
2104 $decoded = preg_replace_callback('/\\\\u([0-9A-Fa-f]{4})/',
2105 array(__CLASS__
, 'unicodeEscapeCallback'),
2108 // Since our original regex didn't de-escape the originals, we need to check for validity again.
2109 // No need to worry about token boundaries, as anything outside the identifier is illegal!
2110 if (!preg_match("/^[$identifierStartChars][$identifierPartChars]*$/u", $decoded)) {
2111 throw $this->newSyntaxError('Illegal token');
2114 // Per spec it _ought_ to work to use these escapes for keywords words as well...
2115 // but IE rejects them as invalid, while Firefox and Chrome treat them as identifiers
2116 // that don't match the keyword.
2117 if (in_array($decoded, $this->keywords
)) {
2118 throw $this->newSyntaxError('Illegal token');
2121 // TODO: save the decoded form for output?
2125 throw $this->newSyntaxError('Illegal token');
2127 $tt = in_array($match[0], $this->keywords
) ?
$match[0] : TOKEN_IDENTIFIER
;
2131 $this->tokenIndex
= ($this->tokenIndex +
1) & 3;
2133 if (!isset($this->tokens
[$this->tokenIndex
]))
2134 $this->tokens
[$this->tokenIndex
] = new JSToken();
2136 $token = $this->tokens
[$this->tokenIndex
];
2139 if ($tt == OP_ASSIGN
)
2140 $token->assignOp
= $op;
2142 $token->start
= $this->cursor
;
2144 $token->value
= $match[0];
2145 $this->cursor +
= strlen($match[0]);
2147 $token->end
= $this->cursor
;
2148 $token->lineno
= $this->lineno
;
2153 public function unget()
2155 if (++
$this->lookahead
== 4)
2156 throw $this->newSyntaxError('PANIC: too much lookahead!');
2158 $this->tokenIndex
= ($this->tokenIndex
- 1) & 3;
2161 public function newSyntaxError($m)
2163 return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename
. '\' on line ' . $this->lineno
);
2166 public static function unicodeEscapeCallback($m)
2168 return html_entity_decode('&#x' . $m[1]. ';', ENT_QUOTES
, 'UTF-8');