2 // @codingStandardsIgnoreFile 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: http://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine)
10 * Narcissus sourcecode: http://mxr.mozilla.org/mozilla/source/js/narcissus/
11 * JSMinPlus weblog: http://crisp.tweakblogs.net/blog/cat/716
13 * Tino Zijdel <crisp@tweakers.net>
15 * Usage: $minified = JSMinPlus::minify($script [, $filename])
17 * Versionlog (see also changelog.txt):
18 * 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 http://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
));
977 $n->target
= $ss[$i];
981 $n = new JSNode($this->t
);
982 $n->tryBlock
= $this->Block($x);
983 $n->catchClauses
= array();
985 while ($this->t
->match(KEYWORD_CATCH
))
987 $n2 = new JSNode($this->t
);
988 $this->t
->mustMatch(OP_LEFT_PAREN
);
989 $n2->varName
= $this->t
->mustMatch(TOKEN_IDENTIFIER
)->value
;
991 if ($this->t
->match(KEYWORD_IF
))
993 if ($x->ecmaStrictMode
)
994 throw $this->t
->newSyntaxError('Illegal catch guard');
996 if (count($n->catchClauses
) && !end($n->catchClauses
)->guard
)
997 throw $this->t
->newSyntaxError('Guarded catch after unguarded');
999 $n2->guard
= $this->Expression($x);
1006 $this->t
->mustMatch(OP_RIGHT_PAREN
);
1007 $n2->block
= $this->Block($x);
1008 array_push($n->catchClauses
, $n2);
1011 if ($this->t
->match(KEYWORD_FINALLY
))
1012 $n->finallyBlock
= $this->Block($x);
1014 if (!count($n->catchClauses
) && !$n->finallyBlock
)
1015 throw $this->t
->newSyntaxError('Invalid try statement');
1019 case KEYWORD_FINALLY
:
1020 throw $this->t
->newSyntaxError($tt +
' without preceding try');
1023 $n = new JSNode($this->t
);
1024 $n->value
= $this->Expression($x);
1027 case KEYWORD_RETURN
:
1028 if (!$x->inFunction
)
1029 throw $this->t
->newSyntaxError('Invalid return');
1031 $n = new JSNode($this->t
);
1032 $tt = $this->t
->peekOnSameLine();
1033 if ($tt != TOKEN_END
&& $tt != TOKEN_NEWLINE
&& $tt != OP_SEMICOLON
&& $tt != OP_RIGHT_CURLY
)
1034 $n->value
= $this->Expression($x);
1040 $n = new JSNode($this->t
);
1041 $n->object = $this->ParenExpression($x);
1042 $n->body
= $this->nest($x, $n);
1047 $n = $this->Variables($x);
1050 case TOKEN_CONDCOMMENT_START
:
1051 case TOKEN_CONDCOMMENT_END
:
1052 $n = new JSNode($this->t
);
1055 case KEYWORD_DEBUGGER
:
1056 $n = new JSNode($this->t
);
1061 $n = new JSNode($this->t
, OP_SEMICOLON
);
1062 $n->expression
= null;
1066 if ($tt == TOKEN_IDENTIFIER
)
1068 $this->t
->scanOperand
= false;
1069 $tt = $this->t
->peek();
1070 $this->t
->scanOperand
= true;
1071 if ($tt == OP_COLON
)
1073 $label = $this->t
->currentToken()->value
;
1074 $ss = $x->stmtStack
;
1075 for ($i = count($ss) - 1; $i >= 0; --$i)
1077 if ($ss[$i]->label
== $label)
1078 throw $this->t
->newSyntaxError('Duplicate label');
1082 $n = new JSNode($this->t
, JS_LABEL
);
1084 $n->statement
= $this->nest($x, $n);
1090 $n = new JSNode($this->t
, OP_SEMICOLON
);
1092 $n->expression
= $this->Expression($x);
1093 $n->end
= $n->expression
->end
;
1097 if ($this->t
->lineno
== $this->t
->currentToken()->lineno
)
1099 $tt = $this->t
->peekOnSameLine();
1100 if ($tt != TOKEN_END
&& $tt != TOKEN_NEWLINE
&& $tt != OP_SEMICOLON
&& $tt != OP_RIGHT_CURLY
)
1101 throw $this->t
->newSyntaxError('Missing ; before statement');
1104 $this->t
->match(OP_SEMICOLON
);
1109 private function FunctionDefinition($x, $requireName, $functionForm)
1111 $f = new JSNode($this->t
);
1113 if ($f->type
!= KEYWORD_FUNCTION
)
1114 $f->type
= ($f->value
== 'get') ? JS_GETTER
: JS_SETTER
;
1116 if ($this->t
->match(TOKEN_IDENTIFIER
))
1117 $f->name
= $this->t
->currentToken()->value
;
1118 elseif ($requireName)
1119 throw $this->t
->newSyntaxError('Missing function identifier');
1121 $this->t
->mustMatch(OP_LEFT_PAREN
);
1122 $f->params
= array();
1124 while (($tt = $this->t
->get()) != OP_RIGHT_PAREN
)
1126 if ($tt != TOKEN_IDENTIFIER
)
1127 throw $this->t
->newSyntaxError('Missing formal parameter');
1129 array_push($f->params
, $this->t
->currentToken()->value
);
1131 if ($this->t
->peek() != OP_RIGHT_PAREN
)
1132 $this->t
->mustMatch(OP_COMMA
);
1135 $this->t
->mustMatch(OP_LEFT_CURLY
);
1137 $x2 = new JSCompilerContext(true);
1138 $f->body
= $this->Script($x2);
1140 $this->t
->mustMatch(OP_RIGHT_CURLY
);
1141 $f->end
= $this->t
->currentToken()->end
;
1143 $f->functionForm
= $functionForm;
1144 if ($functionForm == DECLARED_FORM
)
1145 array_push($x->funDecls
, $f);
1150 private function Variables($x)
1152 $n = new JSNode($this->t
);
1156 $this->t
->mustMatch(TOKEN_IDENTIFIER
);
1158 $n2 = new JSNode($this->t
);
1159 $n2->name
= $n2->value
;
1161 if ($this->t
->match(OP_ASSIGN
))
1163 if ($this->t
->currentToken()->assignOp
)
1164 throw $this->t
->newSyntaxError('Invalid variable initialization');
1166 $n2->initializer
= $this->Expression($x, OP_COMMA
);
1169 $n2->readOnly
= $n->type
== KEYWORD_CONST
;
1172 array_push($x->varDecls
, $n2);
1174 while ($this->t
->match(OP_COMMA
));
1179 private function Expression($x, $stop=false)
1181 $operators = array();
1182 $operands = array();
1185 $bl = $x->bracketLevel
;
1186 $cl = $x->curlyLevel
;
1187 $pl = $x->parenLevel
;
1188 $hl = $x->hookLevel
;
1190 while (($tt = $this->t
->get()) != TOKEN_END
)
1193 $x->bracketLevel
== $bl &&
1194 $x->curlyLevel
== $cl &&
1195 $x->parenLevel
== $pl &&
1196 $x->hookLevel
== $hl
1199 // Stop only if tt matches the optional stop parameter, and that
1200 // token is not quoted by some kind of bracket.
1207 // NB: cannot be empty, Statement handled that.
1211 if ($this->t
->scanOperand
)
1214 while ( !empty($operators) &&
1215 $this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[$tt]
1217 $this->reduce($operators, $operands);
1219 array_push($operators, new JSNode($this->t
));
1222 $this->t
->scanOperand
= true;
1223 $n = $this->Expression($x);
1225 if (!$this->t
->match(OP_COLON
))
1229 array_push($operands, $n);
1236 throw $this->t
->newSyntaxError('Invalid label');
1240 if ($this->t
->scanOperand
)
1243 // Use >, not >=, for right-associative ASSIGN
1244 while ( !empty($operators) &&
1245 $this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[$tt]
1247 $this->reduce($operators, $operands);
1249 array_push($operators, new JSNode($this->t
));
1250 end($operands)->assignOp
= $this->t
->currentToken()->assignOp
;
1251 $this->t
->scanOperand
= true;
1255 // An in operator should not be parsed if we're parsing the head of
1256 // a for (...) loop, unless it is in the then part of a conditional
1257 // expression, or parenthesized somehow.
1258 if ($x->inForLoopInit
&& !$x->hookLevel
&&
1259 !$x->bracketLevel
&& !$x->curlyLevel
&&
1265 // A comma operator should not be parsed if we're parsing the then part
1266 // of a conditional expression unless it's parenthesized somehow.
1267 if ($tt == OP_COMMA
&& $x->hookLevel
&&
1268 !$x->bracketLevel
&& !$x->curlyLevel
&&
1272 // Treat comma as left-associative so reduce can fold left-heavy
1273 // COMMA trees into a single array.
1278 case OP_BITWISE_XOR
:
1279 case OP_BITWISE_AND
:
1280 case OP_EQ
: case OP_NE
: case OP_STRICT_EQ
: case OP_STRICT_NE
:
1281 case OP_LT
: case OP_LE
: case OP_GE
: case OP_GT
:
1282 case KEYWORD_INSTANCEOF
:
1283 case OP_LSH
: case OP_RSH
: case OP_URSH
:
1284 case OP_PLUS
: case OP_MINUS
:
1285 case OP_MUL
: case OP_DIV
: case OP_MOD
:
1287 if ($this->t
->scanOperand
)
1290 while ( !empty($operators) &&
1291 $this->opPrecedence
[end($operators)->type
] >= $this->opPrecedence
[$tt]
1293 $this->reduce($operators, $operands);
1297 $this->t
->mustMatch(TOKEN_IDENTIFIER
);
1298 array_push($operands, new JSNode($this->t
, OP_DOT
, array_pop($operands), new JSNode($this->t
)));
1302 array_push($operators, new JSNode($this->t
));
1303 $this->t
->scanOperand
= true;
1307 case KEYWORD_DELETE
: case KEYWORD_VOID
: case KEYWORD_TYPEOF
:
1308 case OP_NOT
: case OP_BITWISE_NOT
: case OP_UNARY_PLUS
: case OP_UNARY_MINUS
:
1310 if (!$this->t
->scanOperand
)
1313 array_push($operators, new JSNode($this->t
));
1316 case OP_INCREMENT
: case OP_DECREMENT
:
1317 if ($this->t
->scanOperand
)
1319 array_push($operators, new JSNode($this->t
)); // prefix increment or decrement
1323 // Don't cross a line boundary for postfix {in,de}crement.
1324 $t = $this->t
->tokens
[($this->t
->tokenIndex +
$this->t
->lookahead
- 1) & 3];
1325 if ($t && $t->lineno
!= $this->t
->lineno
)
1328 if (!empty($operators))
1330 // Use >, not >=, so postfix has higher precedence than prefix.
1331 while ($this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[$tt])
1332 $this->reduce($operators, $operands);
1335 $n = new JSNode($this->t
, $tt, array_pop($operands));
1337 array_push($operands, $n);
1341 case KEYWORD_FUNCTION
:
1342 if (!$this->t
->scanOperand
)
1345 array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM
));
1346 $this->t
->scanOperand
= false;
1349 case KEYWORD_NULL
: case KEYWORD_THIS
: case KEYWORD_TRUE
: case KEYWORD_FALSE
:
1350 case TOKEN_IDENTIFIER
: case TOKEN_NUMBER
: case TOKEN_STRING
: case TOKEN_REGEXP
:
1351 if (!$this->t
->scanOperand
)
1354 array_push($operands, new JSNode($this->t
));
1355 $this->t
->scanOperand
= false;
1358 case TOKEN_CONDCOMMENT_START
:
1359 case TOKEN_CONDCOMMENT_END
:
1360 if ($this->t
->scanOperand
)
1361 array_push($operators, new JSNode($this->t
));
1363 array_push($operands, new JSNode($this->t
));
1366 case OP_LEFT_BRACKET
:
1367 if ($this->t
->scanOperand
)
1369 // Array initialiser. Parse using recursive descent, as the
1370 // sub-grammar here is not an operator grammar.
1371 $n = new JSNode($this->t
, JS_ARRAY_INIT
);
1372 while (($tt = $this->t
->peek()) != OP_RIGHT_BRACKET
)
1374 if ($tt == OP_COMMA
)
1381 $n->addNode($this->Expression($x, OP_COMMA
));
1382 if (!$this->t
->match(OP_COMMA
))
1386 $this->t
->mustMatch(OP_RIGHT_BRACKET
);
1387 array_push($operands, $n);
1388 $this->t
->scanOperand
= false;
1392 // Property indexing operator.
1393 array_push($operators, new JSNode($this->t
, JS_INDEX
));
1394 $this->t
->scanOperand
= true;
1399 case OP_RIGHT_BRACKET
:
1400 if ($this->t
->scanOperand ||
$x->bracketLevel
== $bl)
1403 while ($this->reduce($operators, $operands)->type
!= JS_INDEX
)
1410 if (!$this->t
->scanOperand
)
1413 // Object initialiser. As for array initialisers (see above),
1414 // parse using recursive descent.
1416 $n = new JSNode($this->t
, JS_OBJECT_INIT
);
1417 while (!$this->t
->match(OP_RIGHT_CURLY
))
1421 $tt = $this->t
->get();
1422 $tv = $this->t
->currentToken()->value
;
1423 if (($tv == 'get' ||
$tv == 'set') && $this->t
->peek() == TOKEN_IDENTIFIER
)
1425 if ($x->ecmaStrictMode
)
1426 throw $this->t
->newSyntaxError('Illegal property accessor');
1428 $n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM
));
1434 case TOKEN_IDENTIFIER
:
1437 $id = new JSNode($this->t
);
1440 case OP_RIGHT_CURLY
:
1441 if ($x->ecmaStrictMode
)
1442 throw $this->t
->newSyntaxError('Illegal trailing ,');
1446 throw $this->t
->newSyntaxError('Invalid property name');
1449 $this->t
->mustMatch(OP_COLON
);
1450 $n->addNode(new JSNode($this->t
, JS_PROPERTY_INIT
, $id, $this->Expression($x, OP_COMMA
)));
1453 while ($this->t
->match(OP_COMMA
));
1455 $this->t
->mustMatch(OP_RIGHT_CURLY
);
1459 array_push($operands, $n);
1460 $this->t
->scanOperand
= false;
1464 case OP_RIGHT_CURLY
:
1465 if (!$this->t
->scanOperand
&& $x->curlyLevel
!= $cl)
1466 throw new Exception('PANIC: right curly botch');
1470 if ($this->t
->scanOperand
)
1472 array_push($operators, new JSNode($this->t
, JS_GROUP
));
1476 while ( !empty($operators) &&
1477 $this->opPrecedence
[end($operators)->type
] > $this->opPrecedence
[KEYWORD_NEW
]
1479 $this->reduce($operators, $operands);
1481 // Handle () now, to regularize the n-ary case for n > 0.
1482 // We must set scanOperand in case there are arguments and
1483 // the first one is a regexp or unary+/-.
1484 $n = end($operators);
1485 $this->t
->scanOperand
= true;
1486 if ($this->t
->match(OP_RIGHT_PAREN
))
1488 if ($n && $n->type
== KEYWORD_NEW
)
1490 array_pop($operators);
1491 $n->addNode(array_pop($operands));
1495 $n = new JSNode($this->t
, JS_CALL
, array_pop($operands), new JSNode($this->t
, JS_LIST
));
1498 array_push($operands, $n);
1499 $this->t
->scanOperand
= false;
1503 if ($n && $n->type
== KEYWORD_NEW
)
1504 $n->type
= JS_NEW_WITH_ARGS
;
1506 array_push($operators, new JSNode($this->t
, JS_CALL
));
1512 case OP_RIGHT_PAREN
:
1513 if ($this->t
->scanOperand ||
$x->parenLevel
== $pl)
1516 while (($tt = $this->reduce($operators, $operands)->type
) != JS_GROUP
&&
1517 $tt != JS_CALL
&& $tt != JS_NEW_WITH_ARGS
1523 if ($tt != JS_GROUP
)
1525 $n = end($operands);
1526 if ($n->treeNodes
[1]->type
!= OP_COMMA
)
1527 $n->treeNodes
[1] = new JSNode($this->t
, JS_LIST
, $n->treeNodes
[1]);
1529 $n->treeNodes
[1]->type
= JS_LIST
;
1535 // Automatic semicolon insertion means we may scan across a newline
1536 // and into the beginning of another statement. If so, break out of
1537 // the while loop and let the t.scanOperand logic handle errors.
1543 if ($x->hookLevel
!= $hl)
1544 throw $this->t
->newSyntaxError('Missing : in conditional expression');
1546 if ($x->parenLevel
!= $pl)
1547 throw $this->t
->newSyntaxError('Missing ) in parenthetical');
1549 if ($x->bracketLevel
!= $bl)
1550 throw $this->t
->newSyntaxError('Missing ] in index expression');
1552 if ($this->t
->scanOperand
)
1553 throw $this->t
->newSyntaxError('Missing operand');
1555 // Resume default mode, scanning for operands, not operators.
1556 $this->t
->scanOperand
= true;
1559 while (count($operators))
1560 $this->reduce($operators, $operands);
1562 return array_pop($operands);
1565 private function ParenExpression($x)
1567 $this->t
->mustMatch(OP_LEFT_PAREN
);
1568 $n = $this->Expression($x);
1569 $this->t
->mustMatch(OP_RIGHT_PAREN
);
1574 // Statement stack and nested statement handler.
1575 private function nest($x, $node, $end = false)
1577 array_push($x->stmtStack
, $node);
1578 $n = $this->statement($x);
1579 array_pop($x->stmtStack
);
1582 $this->t
->mustMatch($end);
1587 private function reduce(&$operators, &$operands)
1589 $n = array_pop($operators);
1591 $arity = $this->opArity
[$op];
1592 $c = count($operands);
1595 // Flatten left-associative trees
1598 $left = $operands[$c - 2];
1599 if ($left->type
== $op)
1601 $right = array_pop($operands);
1602 $left->addNode($right);
1609 // Always use push to add operands to n, to update start and end
1610 $a = array_splice($operands, $c - $arity);
1611 for ($i = 0; $i < $arity; $i++
)
1612 $n->addNode($a[$i]);
1614 // Include closing bracket or postfix operator in [start,end]
1615 $te = $this->t
->currentToken()->end
;
1619 array_push($operands, $n);
1625 class JSCompilerContext
1627 public $inFunction = false;
1628 public $inForLoopInit = false;
1629 public $ecmaStrictMode = false;
1630 public $bracketLevel = 0;
1631 public $curlyLevel = 0;
1632 public $parenLevel = 0;
1633 public $hookLevel = 0;
1635 public $stmtStack = array();
1636 public $funDecls = array();
1637 public $varDecls = array();
1639 public function __construct($inFunction)
1641 $this->inFunction
= $inFunction;
1653 public $treeNodes = array();
1654 public $funDecls = array();
1655 public $varDecls = array();
1657 public function __construct($t, $type=0)
1659 if ($token = $t->currentToken())
1661 $this->type
= $type ?
$type : $token->type
;
1662 $this->value
= $token->value
;
1663 $this->lineno
= $token->lineno
;
1664 $this->start
= $token->start
;
1665 $this->end
= $token->end
;
1669 $this->type
= $type;
1670 $this->lineno
= $t->lineno
;
1673 if (($numargs = func_num_args()) > 2)
1675 $args = func_get_args();
1676 for ($i = 2; $i < $numargs; $i++
)
1677 $this->addNode($args[$i]);
1681 // we don't want to bloat our object with all kind of specific properties, so we use overloading
1682 public function __set($name, $value)
1684 $this->$name = $value;
1687 public function __get($name)
1689 if (isset($this->$name))
1690 return $this->$name;
1695 public function addNode($node)
1699 if ($node->start
< $this->start
)
1700 $this->start
= $node->start
;
1701 if ($this->end
< $node->end
)
1702 $this->end
= $node->end
;
1705 $this->treeNodes
[] = $node;
1711 private $cursor = 0;
1714 public $tokens = array();
1715 public $tokenIndex = 0;
1716 public $lookahead = 0;
1717 public $scanNewlines = false;
1718 public $scanOperand = true;
1723 private $keywords = array(
1725 'case', 'catch', 'const', 'continue',
1726 'debugger', 'default', 'delete', 'do',
1728 'false', 'finally', 'for', 'function',
1729 'if', 'in', 'instanceof',
1733 'this', 'throw', 'true', 'try', 'typeof',
1738 private $opTypeNames = array(
1739 ';', ',', '?', ':', '||', '&&', '|', '^',
1740 '&', '===', '==', '=', '!==', '!=', '<<', '<=',
1741 '<', '>>>', '>>', '>=', '>', '++', '--', '+',
1742 '-', '*', '/', '%', '!', '~', '.', '[',
1743 ']', '{', '}', '(', ')', '@*/'
1746 private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
1749 public function __construct()
1751 $this->opRegExp
= '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames
)) . ')#';
1754 public function init($source, $filename = '', $lineno = 1)
1756 $this->source
= $source;
1757 $this->filename
= $filename ?
$filename : '[inline]';
1758 $this->lineno
= $lineno;
1761 $this->tokens
= array();
1762 $this->tokenIndex
= 0;
1763 $this->lookahead
= 0;
1764 $this->scanNewlines
= false;
1765 $this->scanOperand
= true;
1768 public function getInput($chunksize)
1771 return substr($this->source
, $this->cursor
, $chunksize);
1773 return substr($this->source
, $this->cursor
);
1776 public function isDone()
1778 return $this->peek() == TOKEN_END
;
1781 public function match($tt)
1783 return $this->get() == $tt ||
$this->unget();
1786 public function mustMatch($tt)
1788 if (!$this->match($tt))
1789 throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
1791 return $this->currentToken();
1794 public function peek()
1796 if ($this->lookahead
)
1798 $next = $this->tokens
[($this->tokenIndex +
$this->lookahead
) & 3];
1799 if ($this->scanNewlines
&& $next->lineno
!= $this->lineno
)
1800 $tt = TOKEN_NEWLINE
;
1813 public function peekOnSameLine()
1815 $this->scanNewlines
= true;
1816 $tt = $this->peek();
1817 $this->scanNewlines
= false;
1822 public function currentToken()
1824 if (!empty($this->tokens
))
1825 return $this->tokens
[$this->tokenIndex
];
1828 public function get($chunksize = 1000)
1830 while($this->lookahead
)
1833 $this->tokenIndex
= ($this->tokenIndex +
1) & 3;
1834 $token = $this->tokens
[$this->tokenIndex
];
1835 if ($token->type
!= TOKEN_NEWLINE ||
$this->scanNewlines
)
1836 return $token->type
;
1839 $conditional_comment = false;
1841 // strip whitespace and comments
1844 $input = $this->getInput($chunksize);
1846 // whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
1847 $re = $this->scanNewlines ?
'/^[ \r\t]+/' : '/^\s+/';
1848 if (preg_match($re, $input, $match))
1850 $spaces = $match[0];
1851 $spacelen = strlen($spaces);
1852 $this->cursor +
= $spacelen;
1853 if (!$this->scanNewlines
)
1854 $this->lineno +
= substr_count($spaces, "\n");
1856 if ($spacelen == $chunksize)
1857 continue; // complete chunk contained whitespace
1859 $input = $this->getInput($chunksize);
1860 if ($input == '' ||
$input[0] != '/')
1865 if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match))
1870 // retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
1875 // check if this is a conditional (JScript) comment
1876 if (!empty($match[1]))
1878 $match[0] = '/*' . $match[1];
1879 $conditional_comment = true;
1884 $this->cursor +
= strlen($match[0]);
1885 $this->lineno +
= substr_count($match[0], "\n");
1894 elseif ($conditional_comment)
1896 $tt = TOKEN_CONDCOMMENT_START
;
1904 if (($input[1] == 'x' ||
$input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match))
1911 case '1': case '2': case '3': case '4': case '5':
1912 case '6': case '7': case '8': case '9':
1913 // should always match
1914 preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match);
1919 if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match))
1926 return $this->get(null); // retry with a full chunk fetch
1928 throw $this->newSyntaxError('Unterminated string literal');
1933 if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match))
1940 return $this->get(null); // retry with a full chunk fetch
1942 throw $this->newSyntaxError('Unterminated string literal');
1947 if ($this->scanOperand
&& preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
1965 // should always match
1966 preg_match($this->opRegExp
, $input, $match);
1968 if (in_array($op, $this->assignOps
) && $input[strlen($op)] == '=')
1976 if ($this->scanOperand
)
1979 $tt = OP_UNARY_PLUS
;
1980 elseif ($op == OP_MINUS
)
1981 $tt = OP_UNARY_MINUS
;
1988 if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
2006 // these are all single
2007 $match = array($input[0]);
2012 // check end of conditional comment
2013 if (substr($input, 0, 3) == '@*/')
2015 $match = array('@*/');
2016 $tt = TOKEN_CONDCOMMENT_END
;
2019 throw $this->newSyntaxError('Illegal token');
2023 if ($this->scanNewlines
)
2025 $match = array("\n");
2026 $tt = TOKEN_NEWLINE
;
2029 throw $this->newSyntaxError('Illegal token');
2033 // Fast path for identifiers: word chars followed by whitespace or various other tokens.
2034 // Note we don't need to exclude digits in the first char, as they've already been found
2036 if (!preg_match('/^[$\w]+(?=[\s\/\|\^\&<>\+\-\*%=!.;,\?:~\[\]\{\}\(\)@])/', $input, $match))
2038 // Character classes per ECMA-262 edition 5.1 section 7.6
2039 // Per spec, must accept Unicode 3.0, *may* accept later versions.
2040 // We'll take whatever PCRE understands, which should be more recent.
2041 $identifierStartChars = "\\p{L}\\p{Nl}" . # UnicodeLetter
2044 $identifierPartChars = $identifierStartChars .
2045 "\\p{Mn}\\p{Mc}" . # UnicodeCombiningMark
2046 "\\p{Nd}" . # UnicodeDigit
2047 "\\p{Pc}"; # UnicodeConnectorPunctuation
2048 $unicodeEscape = "\\\\u[0-9A-F-a-f]{4}";
2049 $identifierRegex = "/^" .
2050 "(?:[$identifierStartChars]|$unicodeEscape)" .
2051 "(?:[$identifierPartChars]|$unicodeEscape)*" .
2053 if (preg_match($identifierRegex, $input, $match))
2055 if (strpos($match[0], '\\') !== false) {
2056 // Per ECMA-262 edition 5.1, section 7.6 escape sequences should behave as if they were
2057 // the original chars, but only within the boundaries of the identifier.
2058 $decoded = preg_replace_callback('/\\\\u([0-9A-Fa-f]{4})/',
2059 array(__CLASS__
, 'unicodeEscapeCallback'),
2062 // Since our original regex didn't de-escape the originals, we need to check for validity again.
2063 // No need to worry about token boundaries, as anything outside the identifier is illegal!
2064 if (!preg_match("/^[$identifierStartChars][$identifierPartChars]*$/u", $decoded)) {
2065 throw $this->newSyntaxError('Illegal token');
2068 // Per spec it _ought_ to work to use these escapes for keywords words as well...
2069 // but IE rejects them as invalid, while Firefox and Chrome treat them as identifiers
2070 // that don't match the keyword.
2071 if (in_array($decoded, $this->keywords
)) {
2072 throw $this->newSyntaxError('Illegal token');
2075 // TODO: save the decoded form for output?
2079 throw $this->newSyntaxError('Illegal token');
2081 $tt = in_array($match[0], $this->keywords
) ?
$match[0] : TOKEN_IDENTIFIER
;
2085 $this->tokenIndex
= ($this->tokenIndex +
1) & 3;
2087 if (!isset($this->tokens
[$this->tokenIndex
]))
2088 $this->tokens
[$this->tokenIndex
] = new JSToken();
2090 $token = $this->tokens
[$this->tokenIndex
];
2093 if ($tt == OP_ASSIGN
)
2094 $token->assignOp
= $op;
2096 $token->start
= $this->cursor
;
2098 $token->value
= $match[0];
2099 $this->cursor +
= strlen($match[0]);
2101 $token->end
= $this->cursor
;
2102 $token->lineno
= $this->lineno
;
2107 public function unget()
2109 if (++
$this->lookahead
== 4)
2110 throw $this->newSyntaxError('PANIC: too much lookahead!');
2112 $this->tokenIndex
= ($this->tokenIndex
- 1) & 3;
2115 public function newSyntaxError($m)
2117 return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename
. '\' on line ' . $this->lineno
);
2120 public static function unicodeEscapeCallback($m)
2122 return html_entity_decode('&#x' . $m[1]. ';', ENT_QUOTES
, 'UTF-8');