* * Usage: $minified = JSMinPlus::minify($script [, $filename]) * * Versionlog (see also changelog.txt): * 23-07-2011 - remove dynamic creation of OP_* and KEYWORD_* defines and declare them on top * reduce memory footprint by minifying by block-scope * some small byte-saving and performance improvements * 12-05-2009 - fixed hook:colon precedence, fixed empty body in loop and if-constructs * 18-04-2009 - fixed crashbug in PHP 5.2.9 and several other bugfixes * 12-04-2009 - some small bugfixes and performance improvements * 09-04-2009 - initial open sourced version 1.0 * * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip * */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is the Narcissus JavaScript engine. * * The Initial Developer of the Original Code is * Brendan Eich . * Portions created by the Initial Developer are Copyright (C) 2004 * the Initial Developer. All Rights Reserved. * * Contributor(s): Tino Zijdel * PHP port, modifications and minifier routine are (C) 2009-2011 * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ class JSParser { /** * * @var \Phacil\Framework\ECompress\Helpers\JSMinPlus\JSTokenizer */ private $t; private $minifier; private $opPrecedence = array( ';' => 0, ',' => 1, '=' => 2, '?' => 2, ':' => 2, // The above all have to have the same precedence, see bug 330975 '||' => 4, '&&' => 5, '|' => 6, '^' => 7, '&' => 8, '==' => 9, '!=' => 9, '===' => 9, '!==' => 9, '<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10, '<<' => 11, '>>' => 11, '>>>' => 11, '+' => 12, '-' => 12, '*' => 13, '/' => 13, '%' => 13, 'delete' => 14, 'void' => 14, 'typeof' => 14, '!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14, '++' => 15, '--' => 15, 'new' => 16, '.' => 17, JS_NEW_WITH_ARGS => 0, JS_INDEX => 0, JS_CALL => 0, JS_ARRAY_INIT => 0, JS_OBJECT_INIT => 0, JS_GROUP => 0 ); private $opArity = array( ',' => -2, '=' => 2, '?' => 3, '||' => 2, '&&' => 2, '|' => 2, '^' => 2, '&' => 2, '==' => 2, '!=' => 2, '===' => 2, '!==' => 2, '<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2, '<<' => 2, '>>' => 2, '>>>' => 2, '+' => 2, '-' => 2, '*' => 2, '/' => 2, '%' => 2, 'delete' => 1, 'void' => 1, 'typeof' => 1, '!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1, '++' => 1, '--' => 1, 'new' => 1, '.' => 2, JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2, JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1, TOKEN_CONDCOMMENT_START => 1, TOKEN_CONDCOMMENT_END => 1 ); public function __construct($minifier=null) { $this->minifier = $minifier; $this->t = new JSTokenizer(); } public function parse($s, $f, $l) { // initialize tokenizer $this->t->init($s, $f, $l); $x = new JSCompilerContext(false); $n = $this->Script($x); if (!$this->t->isDone()) throw $this->t->newSyntaxError('Syntax error'); return $n; } private function Script($x) { $n = $this->Statements($x); $n->type = JS_SCRIPT; $n->funDecls = $x->funDecls; $n->varDecls = $x->varDecls; // minify by scope if ($this->minifier) { $n->value = $this->minifier->parseTree($n); // clear tree from node to save memory $n->treeNodes = null; $n->funDecls = null; $n->varDecls = null; $n->type = JS_MINIFIED; } return $n; } private function Statements($x) { $n = new JSNode($this->t, JS_BLOCK); array_push($x->stmtStack, $n); while (!$this->t->isDone() && $this->t->peek() != OP_RIGHT_CURLY) $n->addNode($this->Statement($x)); array_pop($x->stmtStack); return $n; } private function Block($x) { $this->t->mustMatch(OP_LEFT_CURLY); $n = $this->Statements($x); $this->t->mustMatch(OP_RIGHT_CURLY); return $n; } private function Statement($x) { $tt = $this->t->get(); $n2 = null; // Cases for statements ending in a right curly return early, avoiding the // common semicolon insertion magic after this switch. switch ($tt) { case KEYWORD_FUNCTION: return $this->FunctionDefinition( $x, true, count($x->stmtStack) > 1 ? STATEMENT_FORM : DECLARED_FORM ); break; case OP_LEFT_CURLY: $n = $this->Statements($x); $this->t->mustMatch(OP_RIGHT_CURLY); return $n; case KEYWORD_IF: $n = new JSNode($this->t); $n->condition = $this->ParenExpression($x); array_push($x->stmtStack, $n); $n->thenPart = $this->Statement($x); $n->elsePart = $this->t->match(KEYWORD_ELSE) ? $this->Statement($x) : null; array_pop($x->stmtStack); return $n; case KEYWORD_SWITCH: $n = new JSNode($this->t); $this->t->mustMatch(OP_LEFT_PAREN); $n->discriminant = $this->Expression($x); $this->t->mustMatch(OP_RIGHT_PAREN); $n->cases = array(); $n->defaultIndex = -1; array_push($x->stmtStack, $n); $this->t->mustMatch(OP_LEFT_CURLY); while (($tt = $this->t->get()) != OP_RIGHT_CURLY) { switch ($tt) { case KEYWORD_DEFAULT: if ($n->defaultIndex >= 0) throw $this->t->newSyntaxError('More than one switch default'); // FALL THROUGH case KEYWORD_CASE: $n2 = new JSNode($this->t); if ($tt == KEYWORD_DEFAULT) $n->defaultIndex = count($n->cases); else $n2->caseLabel = $this->Expression($x, OP_COLON); break; default: throw $this->t->newSyntaxError('Invalid switch case'); } $this->t->mustMatch(OP_COLON); $n2->statements = new JSNode($this->t, JS_BLOCK); while (($tt = $this->t->peek()) != KEYWORD_CASE && $tt != KEYWORD_DEFAULT && $tt != OP_RIGHT_CURLY) $n2->statements->addNode($this->Statement($x)); array_push($n->cases, $n2); } array_pop($x->stmtStack); return $n; case KEYWORD_FOR: $n = new JSNode($this->t); $n->isLoop = true; $this->t->mustMatch(OP_LEFT_PAREN); if (($tt = $this->t->peek()) != OP_SEMICOLON) { $x->inForLoopInit = true; if ($tt == KEYWORD_VAR || $tt == KEYWORD_CONST) { $this->t->get(); $n2 = $this->Variables($x); } else { $n2 = $this->Expression($x); } $x->inForLoopInit = false; } if ($n2 && $this->t->match(KEYWORD_IN)) { $n->type = JS_FOR_IN; if ($n2->type == KEYWORD_VAR) { if (count($n2->treeNodes) != 1) { throw $this->t->newSyntaxError( 'Invalid for..in left-hand side', $this->t->filename, $n2->lineno ); } // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name. $n->iterator = $n2->treeNodes[0]; $n->varDecl = $n2; } else { $n->iterator = $n2; $n->varDecl = null; } $n->object = $this->Expression($x); } else { $n->setup = $n2 ? $n2 : null; $this->t->mustMatch(OP_SEMICOLON); $n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x); $this->t->mustMatch(OP_SEMICOLON); $n->update = $this->t->peek() == OP_RIGHT_PAREN ? null : $this->Expression($x); } $this->t->mustMatch(OP_RIGHT_PAREN); $n->body = $this->nest($x, $n); return $n; case KEYWORD_WHILE: $n = new JSNode($this->t); $n->isLoop = true; $n->condition = $this->ParenExpression($x); $n->body = $this->nest($x, $n); return $n; case KEYWORD_DO: $n = new JSNode($this->t); $n->isLoop = true; $n->body = $this->nest($x, $n, KEYWORD_WHILE); $n->condition = $this->ParenExpression($x); if (!$x->ecmaStrictMode) { //