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 ?
$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 $this->t
->mustMatch(TOKEN_IDENTIFIER
);
1296 array_push($operands, new JSNode($this->t
, OP_DOT
, array_pop($operands), new JSNode($this->t
)));
1300 array_push($operators, new JSNode($this->t
));
1301 $this->t
->scanOperand
= true;
1305 case KEYWORD_DELETE
: case KEYWORD_VOID
: case KEYWORD_TYPEOF
:
1306 case OP_NOT
: case OP_BITWISE_NOT
: case OP_UNARY_PLUS
: case OP_UNARY_MINUS
:
1308 if (!$this->t
->scanOperand
)
1311 array_push($operators, new JSNode($this->t
));
1314 case OP_INCREMENT
: case OP_DECREMENT
:
1315 if ($this->t
->scanOperand
)
1317 array_push($operators, new JSNode($this->t
)); // prefix increment or decrement
1321 // Don't cross a line boundary for postfix {in,de}crement.
1322 $t = $this->t
->tokens
[($this->t
->tokenIndex +
$this->t
->lookahead
- 1) & 3];
1323 if ($t && $t->lineno
!= $this->t
->lineno
)
1326 if (!empty($operators))
1328 // Use >, not >=, so postfix has higher precedence than prefix.
1329 while ($this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[$tt])
1330 $this->reduce($operators, $operands);
1333 $n = new JSNode($this->t
, $tt, array_pop($operands));
1335 array_push($operands, $n);
1339 case KEYWORD_FUNCTION
:
1340 if (!$this->t
->scanOperand
)
1343 array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM
));
1344 $this->t
->scanOperand
= false;
1347 case KEYWORD_NULL
: case KEYWORD_THIS
: case KEYWORD_TRUE
: case KEYWORD_FALSE
:
1348 case TOKEN_IDENTIFIER
: case TOKEN_NUMBER
: case TOKEN_STRING
: case TOKEN_REGEXP
:
1349 if (!$this->t
->scanOperand
)
1352 array_push($operands, new JSNode($this->t
));
1353 $this->t
->scanOperand
= false;
1356 case TOKEN_CONDCOMMENT_START
:
1357 case TOKEN_CONDCOMMENT_END
:
1358 if ($this->t
->scanOperand
)
1359 array_push($operators, new JSNode($this->t
));
1361 array_push($operands, new JSNode($this->t
));
1364 case OP_LEFT_BRACKET
:
1365 if ($this->t
->scanOperand
)
1367 // Array initialiser. Parse using recursive descent, as the
1368 // sub-grammar here is not an operator grammar.
1369 $n = new JSNode($this->t
, JS_ARRAY_INIT
);
1370 while (($tt = $this->t
->peek()) != OP_RIGHT_BRACKET
)
1372 if ($tt == OP_COMMA
)
1379 $n->addNode($this->Expression($x, OP_COMMA
));
1380 if (!$this->t
->match(OP_COMMA
))
1384 $this->t
->mustMatch(OP_RIGHT_BRACKET
);
1385 array_push($operands, $n);
1386 $this->t
->scanOperand
= false;
1390 // Property indexing operator.
1391 array_push($operators, new JSNode($this->t
, JS_INDEX
));
1392 $this->t
->scanOperand
= true;
1397 case OP_RIGHT_BRACKET
:
1398 if ($this->t
->scanOperand ||
$x->bracketLevel
== $bl)
1401 while ($this->reduce($operators, $operands)->type
!= JS_INDEX
)
1408 if (!$this->t
->scanOperand
)
1411 // Object initialiser. As for array initialisers (see above),
1412 // parse using recursive descent.
1414 $n = new JSNode($this->t
, JS_OBJECT_INIT
);
1415 while (!$this->t
->match(OP_RIGHT_CURLY
))
1419 $tt = $this->t
->get();
1420 $tv = $this->t
->currentToken()->value
;
1421 if (($tv == 'get' ||
$tv == 'set') && $this->t
->peek() == TOKEN_IDENTIFIER
)
1423 if ($x->ecmaStrictMode
)
1424 throw $this->t
->newSyntaxError('Illegal property accessor');
1426 $n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM
));
1432 case TOKEN_IDENTIFIER
:
1435 $id = new JSNode($this->t
);
1438 case OP_RIGHT_CURLY
:
1439 if ($x->ecmaStrictMode
)
1440 throw $this->t
->newSyntaxError('Illegal trailing ,');
1444 throw $this->t
->newSyntaxError('Invalid property name');
1447 $this->t
->mustMatch(OP_COLON
);
1448 $n->addNode(new JSNode($this->t
, JS_PROPERTY_INIT
, $id, $this->Expression($x, OP_COMMA
)));
1451 while ($this->t
->match(OP_COMMA
));
1453 $this->t
->mustMatch(OP_RIGHT_CURLY
);
1457 array_push($operands, $n);
1458 $this->t
->scanOperand
= false;
1462 case OP_RIGHT_CURLY
:
1463 if (!$this->t
->scanOperand
&& $x->curlyLevel
!= $cl)
1464 throw new Exception('PANIC: right curly botch');
1468 if ($this->t
->scanOperand
)
1470 array_push($operators, new JSNode($this->t
, JS_GROUP
));
1474 while ( !empty($operators) &&
1475 $this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[KEYWORD_NEW
]
1477 $this->reduce($operators, $operands);
1479 // Handle () now, to regularize the n-ary case for n > 0.
1480 // We must set scanOperand in case there are arguments and
1481 // the first one is a regexp or unary+/-.
1482 $n = end($operators);
1483 $this->t
->scanOperand
= true;
1484 if ($this->t
->match(OP_RIGHT_PAREN
))
1486 if ($n && $n->type
== KEYWORD_NEW
)
1488 array_pop($operators);
1489 $n->addNode(array_pop($operands));
1493 $n = new JSNode($this->t
, JS_CALL
, array_pop($operands), new JSNode($this->t
, JS_LIST
));
1496 array_push($operands, $n);
1497 $this->t
->scanOperand
= false;
1501 if ($n && $n->type
== KEYWORD_NEW
)
1502 $n->type
= JS_NEW_WITH_ARGS
;
1504 array_push($operators, new JSNode($this->t
, JS_CALL
));
1510 case OP_RIGHT_PAREN
:
1511 if ($this->t
->scanOperand ||
$x->parenLevel
== $pl)
1514 while (($tt = $this->reduce($operators, $operands)->type
) != JS_GROUP
&&
1515 $tt != JS_CALL
&& $tt != JS_NEW_WITH_ARGS
1521 if ($tt != JS_GROUP
)
1523 $n = end($operands);
1524 if ($n->treeNodes
[1]->type
!= OP_COMMA
)
1525 $n->treeNodes
[1] = new JSNode($this->t
, JS_LIST
, $n->treeNodes
[1]);
1527 $n->treeNodes
[1]->type
= JS_LIST
;
1533 // Automatic semicolon insertion means we may scan across a newline
1534 // and into the beginning of another statement. If so, break out of
1535 // the while loop and let the t.scanOperand logic handle errors.
1541 if ($x->hookLevel
!= $hl)
1542 throw $this->t
->newSyntaxError('Missing : in conditional expression');
1544 if ($x->parenLevel
!= $pl)
1545 throw $this->t
->newSyntaxError('Missing ) in parenthetical');
1547 if ($x->bracketLevel
!= $bl)
1548 throw $this->t
->newSyntaxError('Missing ] in index expression');
1550 if ($this->t
->scanOperand
)
1551 throw $this->t
->newSyntaxError('Missing operand');
1553 // Resume default mode, scanning for operands, not operators.
1554 $this->t
->scanOperand
= true;
1557 while (count($operators))
1558 $this->reduce($operators, $operands);
1560 return array_pop($operands);
1563 private function ParenExpression($x)
1565 $this->t
->mustMatch(OP_LEFT_PAREN
);
1566 $n = $this->Expression($x);
1567 $this->t
->mustMatch(OP_RIGHT_PAREN
);
1572 // Statement stack and nested statement handler.
1573 private function nest($x, $node, $end = false)
1575 array_push($x->stmtStack
, $node);
1576 $n = $this->statement($x);
1577 array_pop($x->stmtStack
);
1580 $this->t
->mustMatch($end);
1585 private function reduce(&$operators, &$operands)
1587 $n = array_pop($operators);
1589 $arity = $this->opArity
[$op];
1590 $c = count($operands);
1593 // Flatten left-associative trees
1596 $left = $operands[$c - 2];
1597 if ($left->type
== $op)
1599 $right = array_pop($operands);
1600 $left->addNode($right);
1607 // Always use push to add operands to n, to update start and end
1608 $a = array_splice($operands, $c - $arity);
1609 for ($i = 0; $i < $arity; $i++
)
1610 $n->addNode($a[$i]);
1612 // Include closing bracket or postfix operator in [start,end]
1613 $te = $this->t
->currentToken()->end
;
1617 array_push($operands, $n);
1623 class JSCompilerContext
1625 public $inFunction = false;
1626 public $inForLoopInit = false;
1627 public $ecmaStrictMode = false;
1628 public $bracketLevel = 0;
1629 public $curlyLevel = 0;
1630 public $parenLevel = 0;
1631 public $hookLevel = 0;
1633 public $stmtStack = array();
1634 public $funDecls = array();
1635 public $varDecls = array();
1637 public function __construct($inFunction)
1639 $this->inFunction
= $inFunction;
1651 public $treeNodes = array();
1652 public $funDecls = array();
1653 public $varDecls = array();
1655 public function __construct($t, $type=0)
1657 if ($token = $t->currentToken())
1659 $this->type
= $type ?
$type : $token->type
;
1660 $this->value
= $token->value
;
1661 $this->lineno
= $token->lineno
;
1662 $this->start
= $token->start
;
1663 $this->end
= $token->end
;
1667 $this->type
= $type;
1668 $this->lineno
= $t->lineno
;
1671 if (($numargs = func_num_args()) > 2)
1673 $args = func_get_args();
1674 for ($i = 2; $i < $numargs; $i++
)
1675 $this->addNode($args[$i]);
1679 // we don't want to bloat our object with all kind of specific properties, so we use overloading
1680 public function __set($name, $value)
1682 $this->$name = $value;
1685 public function __get($name)
1687 if (isset($this->$name))
1688 return $this->$name;
1693 public function addNode($node)
1697 if ($node->start
< $this->start
)
1698 $this->start
= $node->start
;
1699 if ($this->end
< $node->end
)
1700 $this->end
= $node->end
;
1703 $this->treeNodes
[] = $node;
1709 private $cursor = 0;
1712 public $tokens = array();
1713 public $tokenIndex = 0;
1714 public $lookahead = 0;
1715 public $scanNewlines = false;
1716 public $scanOperand = true;
1721 private $keywords = array(
1723 'case', 'catch', 'const', 'continue',
1724 'debugger', 'default', 'delete', 'do',
1726 'false', 'finally', 'for', 'function',
1727 'if', 'in', 'instanceof',
1731 'this', 'throw', 'true', 'try', 'typeof',
1736 private $opTypeNames = array(
1737 ';', ',', '?', ':', '||', '&&', '|', '^',
1738 '&', '===', '==', '=', '!==', '!=', '<<', '<=',
1739 '<', '>>>', '>>', '>=', '>', '++', '--', '+',
1740 '-', '*', '/', '%', '!', '~', '.', '[',
1741 ']', '{', '}', '(', ')', '@*/'
1744 private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
1747 public function __construct()
1749 $this->opRegExp
= '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames
)) . ')#';
1752 public function init($source, $filename = '', $lineno = 1)
1754 $this->source
= $source;
1755 $this->filename
= $filename ?
$filename : '[inline]';
1756 $this->lineno
= $lineno;
1759 $this->tokens
= array();
1760 $this->tokenIndex
= 0;
1761 $this->lookahead
= 0;
1762 $this->scanNewlines
= false;
1763 $this->scanOperand
= true;
1766 public function getInput($chunksize)
1769 return substr($this->source
, $this->cursor
, $chunksize);
1771 return substr($this->source
, $this->cursor
);
1774 public function isDone()
1776 return $this->peek() == TOKEN_END
;
1779 public function match($tt)
1781 return $this->get() == $tt ||
$this->unget();
1784 public function mustMatch($tt)
1786 if (!$this->match($tt))
1787 throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
1789 return $this->currentToken();
1792 public function peek()
1794 if ($this->lookahead
)
1796 $next = $this->tokens
[($this->tokenIndex +
$this->lookahead
) & 3];
1797 if ($this->scanNewlines
&& $next->lineno
!= $this->lineno
)
1798 $tt = TOKEN_NEWLINE
;
1811 public function peekOnSameLine()
1813 $this->scanNewlines
= true;
1814 $tt = $this->peek();
1815 $this->scanNewlines
= false;
1820 public function currentToken()
1822 if (!empty($this->tokens
))
1823 return $this->tokens
[$this->tokenIndex
];
1826 public function get($chunksize = 1000)
1828 while($this->lookahead
)
1831 $this->tokenIndex
= ($this->tokenIndex +
1) & 3;
1832 $token = $this->tokens
[$this->tokenIndex
];
1833 if ($token->type
!= TOKEN_NEWLINE ||
$this->scanNewlines
)
1834 return $token->type
;
1837 $conditional_comment = false;
1839 // strip whitespace and comments
1842 $input = $this->getInput($chunksize);
1844 // whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
1845 $re = $this->scanNewlines ?
'/^[ \r\t]+/' : '/^\s+/';
1846 if (preg_match($re, $input, $match))
1848 $spaces = $match[0];
1849 $spacelen = strlen($spaces);
1850 $this->cursor +
= $spacelen;
1851 if (!$this->scanNewlines
)
1852 $this->lineno +
= substr_count($spaces, "\n");
1854 if ($spacelen == $chunksize)
1855 continue; // complete chunk contained whitespace
1857 $input = $this->getInput($chunksize);
1858 if ($input == '' ||
$input[0] != '/')
1863 if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match))
1868 // retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
1873 // check if this is a conditional (JScript) comment
1874 if (!empty($match[1]))
1876 $match[0] = '/*' . $match[1];
1877 $conditional_comment = true;
1882 $this->cursor +
= strlen($match[0]);
1883 $this->lineno +
= substr_count($match[0], "\n");
1892 elseif ($conditional_comment)
1894 $tt = TOKEN_CONDCOMMENT_START
;
1902 if (($input[1] == 'x' ||
$input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match))
1909 case '1': case '2': case '3': case '4': case '5':
1910 case '6': case '7': case '8': case '9':
1911 // should always match
1912 preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match);
1917 if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match))
1924 return $this->get(null); // retry with a full chunk fetch
1926 throw $this->newSyntaxError('Unterminated string literal');
1931 if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match))
1938 return $this->get(null); // retry with a full chunk fetch
1940 throw $this->newSyntaxError('Unterminated string literal');
1945 if ($this->scanOperand
&& preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
1963 // should always match
1964 preg_match($this->opRegExp
, $input, $match);
1966 if (in_array($op, $this->assignOps
) && $input[strlen($op)] == '=')
1974 if ($this->scanOperand
)
1977 $tt = OP_UNARY_PLUS
;
1978 elseif ($op == OP_MINUS
)
1979 $tt = OP_UNARY_MINUS
;
1986 if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
2004 // these are all single
2005 $match = array($input[0]);
2010 // check end of conditional comment
2011 if (substr($input, 0, 3) == '@*/')
2013 $match = array('@*/');
2014 $tt = TOKEN_CONDCOMMENT_END
;
2017 throw $this->newSyntaxError('Illegal token');
2021 if ($this->scanNewlines
)
2023 $match = array("\n");
2024 $tt = TOKEN_NEWLINE
;
2027 throw $this->newSyntaxError('Illegal token');
2031 // Fast path for identifiers: word chars followed by whitespace or various other tokens.
2032 // Note we don't need to exclude digits in the first char, as they've already been found
2034 if (!preg_match('/^[$\w]+(?=[\s\/\|\^\&<>\+\-\*%=!.;,\?:~\[\]\{\}\(\)@])/', $input, $match))
2036 // Character classes per ECMA-262 edition 5.1 section 7.6
2037 // Per spec, must accept Unicode 3.0, *may* accept later versions.
2038 // We'll take whatever PCRE understands, which should be more recent.
2039 $identifierStartChars = "\\p{L}\\p{Nl}" . # UnicodeLetter
2042 $identifierPartChars = $identifierStartChars .
2043 "\\p{Mn}\\p{Mc}" . # UnicodeCombiningMark
2044 "\\p{Nd}" . # UnicodeDigit
2045 "\\p{Pc}"; # UnicodeConnectorPunctuation
2046 $unicodeEscape = "\\\\u[0-9A-F-a-f]{4}";
2047 $identifierRegex = "/^" .
2048 "(?:[$identifierStartChars]|$unicodeEscape)" .
2049 "(?:[$identifierPartChars]|$unicodeEscape)*" .
2051 if (preg_match($identifierRegex, $input, $match))
2053 if (strpos($match[0], '\\') !== false) {
2054 // Per ECMA-262 edition 5.1, section 7.6 escape sequences should behave as if they were
2055 // the original chars, but only within the boundaries of the identifier.
2056 $decoded = preg_replace_callback('/\\\\u([0-9A-Fa-f]{4})/',
2057 array(__CLASS__
, 'unicodeEscapeCallback'),
2060 // Since our original regex didn't de-escape the originals, we need to check for validity again.
2061 // No need to worry about token boundaries, as anything outside the identifier is illegal!
2062 if (!preg_match("/^[$identifierStartChars][$identifierPartChars]*$/u", $decoded)) {
2063 throw $this->newSyntaxError('Illegal token');
2066 // Per spec it _ought_ to work to use these escapes for keywords words as well...
2067 // but IE rejects them as invalid, while Firefox and Chrome treat them as identifiers
2068 // that don't match the keyword.
2069 if (in_array($decoded, $this->keywords
)) {
2070 throw $this->newSyntaxError('Illegal token');
2073 // TODO: save the decoded form for output?
2077 throw $this->newSyntaxError('Illegal token');
2079 $tt = in_array($match[0], $this->keywords
) ?
$match[0] : TOKEN_IDENTIFIER
;
2083 $this->tokenIndex
= ($this->tokenIndex +
1) & 3;
2085 if (!isset($this->tokens
[$this->tokenIndex
]))
2086 $this->tokens
[$this->tokenIndex
] = new JSToken();
2088 $token = $this->tokens
[$this->tokenIndex
];
2091 if ($tt == OP_ASSIGN
)
2092 $token->assignOp
= $op;
2094 $token->start
= $this->cursor
;
2096 $token->value
= $match[0];
2097 $this->cursor +
= strlen($match[0]);
2099 $token->end
= $this->cursor
;
2100 $token->lineno
= $this->lineno
;
2105 public function unget()
2107 if (++
$this->lookahead
== 4)
2108 throw $this->newSyntaxError('PANIC: too much lookahead!');
2110 $this->tokenIndex
= ($this->tokenIndex
- 1) & 3;
2113 public function newSyntaxError($m)
2115 return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename
. '\' on line ' . $this->lineno
);
2118 public static function unicodeEscapeCallback($m)
2120 return html_entity_decode('&#x' . $m[1]. ';', ENT_QUOTES
, 'UTF-8');