code:
<?php

/**
 * JSMinPlus version 1.1
 *
 * Minifies a javascript file using a javascript parser
 *
 * This implements a PHP port of Brendan Eich's Narcissus open source javascript engine (in javascript)
 * References: http://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine)
 * Narcissus sourcecode: http://mxr.mozilla.org/mozilla/source/js/narcissus/
 * JSMinPlus weblog: http://crisp.tweakblogs.net/blog/cat/716
 *
 * Tino Zijdel <crisp@tweakers.net>
 *
 * Usage: $minified = JSMinPlus::minify($script [, $filename])
 *
 * Versionlog (see also changelog.txt):
 * 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 <brendan@mozilla.org>.
 * Portions created by the Initial Developer are Copyright (C) 2004
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s): Tino Zijdel <crisp@tweakers.net>
 * PHP port, modifications and minifier routine are (C) 2009
 *
 * 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 ***** */

define('TOKEN_END', 1);
define('TOKEN_NUMBER', 2);
define('TOKEN_IDENTIFIER', 3);
define('TOKEN_STRING', 4);
define('TOKEN_REGEXP', 5);
define('TOKEN_NEWLINE', 6);
define('TOKEN_CONDCOMMENT_MULTILINE', 7);

define('JS_SCRIPT', 100);
define('JS_BLOCK', 101);
define('JS_LABEL', 102);
define('JS_FOR_IN', 103);
define('JS_CALL', 104);
define('JS_NEW_WITH_ARGS', 105);
define('JS_INDEX', 106);
define('JS_ARRAY_INIT', 107);
define('JS_OBJECT_INIT', 108);
define('JS_PROPERTY_INIT', 109);
define('JS_GETTER', 110);
define('JS_SETTER', 111);
define('JS_GROUP', 112);
define('JS_LIST', 113);

define('DECLARED_FORM', 0);
define('EXPRESSED_FORM', 1);
define('STATEMENT_FORM', 2);

class JSMinPlus
{
	private $parser;
	private $reserved = array(
		'break', 'case', 'catch', 'continue', 'default', 'delete', 'do',
		'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof',
		'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var',
		'void', 'while', 'with',
		// Words reserved for future use
		'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger',
		'double', 'enum', 'export', 'extends', 'final', 'float', 'goto',
		'implements', 'import', 'int', 'interface', 'long', 'native',
		'package', 'private', 'protected', 'public', 'short', 'static',
		'super', 'synchronized', 'throws', 'transient', 'volatile',
		// These are not reserved, but should be taken into account
		// in isValidIdentifier (See jslint source code)
		'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined'
	);

	private function __construct()
	{
		$this->parser = new JSParser();
	}

	public static function minify($js, $filename='')
	{
		static $instance;

		// this is a singleton
		if(!$instance)
			$instance = new JSMinPlus();

		return $instance->min($js, $filename);
	}

	private function min($js, $filename)
	{
		try
		{
			$n = $this->parser->parse($js, $filename, 1);
			return $this->parseTree($n);
		}
		catch(Exception $e)
		{
			echo $e->getMessage() . "\n";
		}

		return false;
	}

	private function parseTree($n, $noBlockGrouping = false)
	{
		$s = '';

		switch ($n->type)
		{
			case KEYWORD_FUNCTION:
				$s .= 'function' . ($n->name ? ' ' . $n->name : '') . '(';
				$params = $n->params;
				for ($i = 0, $j = count($params); $i < $j; $i++)
					$s .= ($i ? ',' : '') . $params[$i];
				$s .= '){' . $this->parseTree($n->body, true) . '}';
			break;

			case JS_SCRIPT:
				// we do nothing with funDecls or varDecls
				$noBlockGrouping = true;
			// fall through
			case JS_BLOCK:
				$childs = $n->treeNodes;
				for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++)
				{
					$t = $this->parseTree($childs[$i]);
					if (strlen($t))
					{
						if ($c)
						{
							if ($childs[$i]->type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM)
								$s .= "\n"; // put declared functions on a new line
							else
								$s .= ';';
						}

						$s .= $t;

						$c++;
					}
				}

				if ($c > 1 && !$noBlockGrouping)
				{
					$s = '{' . $s . '}';
				}
			break;

			case KEYWORD_IF:
				$s = 'if(' . $this->parseTree($n->condition) . ')';
				$thenPart = $this->parseTree($n->thenPart);
				$elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null;

				// quite a rancid hack to see if we should enclose the thenpart in brackets
				if ($thenPart[0] != '{')
				{
					if (strpos($thenPart, 'if(') !== false)
						$thenPart = '{' . $thenPart . '}';
					elseif ($elsePart)
						$thenPart .= ';';
				}

				$s .= $thenPart;

				if ($elsePart)
				{
					$s .= 'else';

					if ($elsePart[0] != '{')
						$s .= ' ';

					$s .= $elsePart;
				}
			break;

			case KEYWORD_SWITCH:
				$s = 'switch(' . $this->parseTree($n->discriminant) . '){';
				$cases = $n->cases;
				for ($i = 0, $j = count($cases); $i < $j; $i++)
				{
					$case = $cases[$i];
					if ($case->type == KEYWORD_CASE)
						$s .= 'case' . ($case->caseLabel->type != TOKEN_STRING ? ' ' : '') . $this->parseTree($case->caseLabel) . ':';
					else
						$s .= 'default:';

					$statement = $this->parseTree($case->statements);
					if ($statement)
						$s .= $statement . ';';
				}
				$s = rtrim($s, ';') . '}';
			break;

			case KEYWORD_FOR:
				$s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '')
					. ';' . ($n->condition ? $this->parseTree($n->condition) : '')
					. ';' . ($n->update ? $this->parseTree($n->update) : '') . ')'
					. $this->parseTree($n->body);
			break;

			case KEYWORD_WHILE:
				$s = 'while(' . $this->parseTree($n->condition) . ')' . $this->parseTree($n->body);
			break;

			case JS_FOR_IN:
				$s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
			break;

			case KEYWORD_DO:
				$s = 'do{' . $this->parseTree($n->body, true) . '}while(' . $this->parseTree($n->condition) . ')';
			break;

			case KEYWORD_BREAK:
			case KEYWORD_CONTINUE:
				$s = $n->value . ($n->label ? ' ' . $n->label : '');
			break;

			case KEYWORD_TRY:
				$s = 'try{' . $this->parseTree($n->tryBlock, true) . '}';
				$catchClauses = $n->catchClauses;
				for ($i = 0, $j = count($catchClauses); $i < $j; $i++)
				{
					$t = $catchClauses[$i];
					$s .= 'catch(' . $t->varName . ($t->guard ? ' if ' . $this->parseTree($t->guard) : '') . '){' . $this->parseTree($t->block, true) . '}';
				}
				if ($n->finallyBlock)
					$s .= 'finally{' . $this->parseTree($n->finallyBlock, true) . '}';
			break;

			case KEYWORD_THROW:
				$s = 'throw ' . $this->parseTree($n->exception);
			break;

			case KEYWORD_RETURN:
				$s = 'return' . ($n->value ? ' ' . $this->parseTree($n->value) : '');
			break;

			case KEYWORD_WITH:
				$s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
			break;

			case KEYWORD_VAR:
			case KEYWORD_CONST:
				$s = $n->value . ' ';
				$childs = $n->treeNodes;
				for ($i = 0, $j = count($childs); $i < $j; $i++)
				{
					$t = $childs[$i];
					$s .= ($i ? ',' : '') . $t->name;
					$u = $t->initializer;
					if ($u)
						$s .= '=' . $this->parseTree($u);
				}
			break;

			case KEYWORD_DEBUGGER:
				throw new Exception('NOT IMPLEMENTED: DEBUGGER');
			break;

			case TOKEN_CONDCOMMENT_MULTILINE:
				$s = $n->value . ' ';
				$childs = $n->treeNodes;
				for ($i = 0, $j = count($childs); $i < $j; $i++)
					$s .= $this->parseTree($childs[$i]);
			break;

			case OP_SEMICOLON:
				if ($expression = $n->expression)
					$s = $this->parseTree($expression);
			break;

			case JS_LABEL:
				$s = $n->label . ':' . $this->parseTree($n->statement);
			break;

			case OP_COMMA:
				$childs = $n->treeNodes;
				for ($i = 0, $j = count($childs); $i < $j; $i++)
					$s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
			break;

			case OP_ASSIGN:
				$s = $this->parseTree($n->treeNodes[0]) . $n->value . $this->parseTree($n->treeNodes[1]);
			break;

			case OP_HOOK:
				$s = $this->parseTree($n->treeNodes[0]) . '?' . $this->parseTree($n->treeNodes[1]) . ':' . $this->parseTree($n->treeNodes[2]);
			break;

			case OP_OR: case OP_AND:
			case OP_BITWISE_OR: case OP_BITWISE_XOR: case OP_BITWISE_AND:
			case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
			case OP_LT: case OP_LE: case OP_GE: case OP_GT:
			case OP_LSH: case OP_RSH: case OP_URSH:
			case OP_MUL: case OP_DIV: case OP_MOD:
				$s = $this->parseTree($n->treeNodes[0]) . $n->type . $this->parseTree($n->treeNodes[1]);
			break;

			case OP_PLUS:
			case OP_MINUS:
				$s = $this->parseTree($n->treeNodes[0]) . $n->type;
				$nextTokenType = $n->treeNodes[1]->type;
				if (	$nextTokenType == OP_PLUS || $nextTokenType == OP_MINUS ||
					$nextTokenType == OP_INCREMENT || $nextTokenType == OP_DECREMENT ||
					$nextTokenType == OP_UNARY_PLUS || $nextTokenType == OP_UNARY_MINUS
				)
					$s .= ' ';
				$s .= $this->parseTree($n->treeNodes[1]);
			break;

			case KEYWORD_IN:
				$s = $this->parseTree($n->treeNodes[0]) . ' in ' . $this->parseTree($n->treeNodes[1]);
			break;

			case KEYWORD_INSTANCEOF:
				$s = $this->parseTree($n->treeNodes[0]) . ' instanceof ' . $this->parseTree($n->treeNodes[1]);
			break;

			case KEYWORD_DELETE:
				$s = 'delete ' . $this->parseTree($n->treeNodes[0]);
			break;

			case KEYWORD_VOID:
				$s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')';
			break;

			case KEYWORD_TYPEOF:
				$s = 'typeof ' . $this->parseTree($n->treeNodes[0]);
			break;

			case OP_NOT:
			case OP_BITWISE_NOT:
			case OP_UNARY_PLUS:
			case OP_UNARY_MINUS:
				$s = $n->value . $this->parseTree($n->treeNodes[0]);
			break;

			case OP_INCREMENT:
			case OP_DECREMENT:
				if ($n->postfix)
					$s = $this->parseTree($n->treeNodes[0]) . $n->value;
				else
					$s = $n->value . $this->parseTree($n->treeNodes[0]);
			break;

			case OP_DOT:
				$s = $this->parseTree($n->treeNodes[0]) . '.' . $this->parseTree($n->treeNodes[1]);
			break;

			case JS_INDEX:
				$s = $this->parseTree($n->treeNodes[0]);
				// See if we can replace named index with a dot saving 3 bytes
				if (	$n->treeNodes[0]->type == TOKEN_IDENTIFIER &&
					$n->treeNodes[1]->type == TOKEN_STRING &&
					$this->isValidIdentifier(substr($n->treeNodes[1]->value, 1, -1))
				)
					$s .= '.' . substr($n->treeNodes[1]->value, 1, -1);
				else
					$s .= '[' . $this->parseTree($n->treeNodes[1]) . ']';
			break;

			case JS_LIST:
				$childs = $n->treeNodes;
				for ($i = 0, $j = count($childs); $i < $j; $i++)
					$s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
			break;

			case JS_CALL:
				$s = $this->parseTree($n->treeNodes[0]) . '(' . $this->parseTree($n->treeNodes[1]) . ')';
			break;

			case KEYWORD_NEW:
			case JS_NEW_WITH_ARGS:
				$s = 'new ' . $this->parseTree($n->treeNodes[0]) . '(' . ($n->type == JS_NEW_WITH_ARGS ? $this->parseTree($n->treeNodes[1]) : '') . ')';
			break;

			case JS_ARRAY_INIT:
				$s = '[';
				$childs = $n->treeNodes;
				for ($i = 0, $j = count($childs); $i < $j; $i++)
				{
					$s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
				}
				$s .= ']';
			break;

			case JS_OBJECT_INIT:
				$s = '{';
				$childs = $n->treeNodes;
				for ($i = 0, $j = count($childs); $i < $j; $i++)
				{
					$t = $childs[$i];
					if ($i)
						$s .= ',';
					if ($t->type == JS_PROPERTY_INIT)
					{
						// Ditch the quotes when the index is a valid identifier
						if (	$t->treeNodes[0]->type == TOKEN_STRING &&
							$this->isValidIdentifier(substr($t->treeNodes[0]->value, 1, -1))
						)
							$s .= substr($t->treeNodes[0]->value, 1, -1);
						else
							$s .= $t->treeNodes[0]->value;

						$s .= ':' . $this->parseTree($t->treeNodes[1]);
					}
					else
					{
						$s .= $t->type == JS_GETTER ? 'get' : 'set';
						$s .= ' ' . $t->name . '(';
						$params = $t->params;
						for ($i = 0, $j = count($params); $i < $j; $i++)
							$s .= ($i ? ',' : '') . $params[$i];
						$s .= '){' . $this->parseTree($t->body, true) . '}';
					}
				}
				$s .= '}';
			break;

			case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
			case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
				$s = $n->value;
			break;

			case JS_GROUP:
				$s = '(' . $this->parseTree($n->treeNodes[0]) . ')';
			break;

			default:
				throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type);
		}

		return $s;
	}

	private function isValidIdentifier($string)
	{
		return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved);
	}
}

class JSParser
{
	private $t;

	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_MULTILINE => 1
	);

	public function __construct()
	{
		$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;

		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->SyntaxError(
								'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)
				{
					// <script language="JavaScript"> (without version hints) may need
					// automatic semicolon insertion without a newline after do-while.
					// See http://bugzilla.mozilla.org/show_bug.cgi?id=238945.
					$this->t->match(OP_SEMICOLON);
					return $n;
				}
			break;

			case KEYWORD_BREAK:
			case KEYWORD_CONTINUE:
				$n = new JSNode($this->t);

				if ($this->t->peekOnSameLine() == TOKEN_IDENTIFIER)
				{
					$this->t->get();
					$n->label = $this->t->currentToken()->value;
				}

				$ss = $x->stmtStack;
				$i = count($ss);
				$label = $n->label;
				if ($label)
				{
					do
					{
						if (--$i < 0)
							throw $this->t->newSyntaxError('Label not found');
					}
					while ($ss[$i]->label != $label);
				}
				else
				{
					do
					{
						if (--$i < 0)
							throw $this->t->newSyntaxError('Invalid ' . $tt);
					}
					while (!$ss[$i]->isLoop && ($tt != KEYWORD_BREAK || $ss[$i]->type != KEYWORD_SWITCH));
				}

				$n->target = $ss[$i];
			break;

			case KEYWORD_TRY:
				$n = new JSNode($this->t);
				$n->tryBlock = $this->Block($x);
				$n->catchClauses = array();

				while ($this->t->match(KEYWORD_CATCH))
				{
					$n2 = new JSNode($this->t);
					$this->t->mustMatch(OP_LEFT_PAREN);
					$n2->varName = $this->t->mustMatch(TOKEN_IDENTIFIER)->value;

					if ($this->t->match(KEYWORD_IF))
					{
						if ($x->ecmaStrictMode)
							throw $this->t->newSyntaxError('Illegal catch guard');

						if (count($n->catchClauses) && !end($n->catchClauses)->guard)
							throw $this->t->newSyntaxError('Guarded catch after unguarded');

						$n2->guard = $this->Expression($x);
					}
					else
					{
						$n2->guard = null;
					}

					$this->t->mustMatch(OP_RIGHT_PAREN);
					$n2->block = $this->Block($x);
					array_push($n->catchClauses, $n2);
				}

				if ($this->t->match(KEYWORD_FINALLY))
					$n->finallyBlock = $this->Block($x);

				if (!count($n->catchClauses) && !$n->finallyBlock)
					throw $this->t->newSyntaxError('Invalid try statement');
			return $n;

			case KEYWORD_CATCH:
			case KEYWORD_FINALLY:
				throw $this->t->newSyntaxError($tt + ' without preceding try');

			case KEYWORD_THROW:
				$n = new JSNode($this->t);
				$n->exception = $this->Expression($x);
			break;

			case KEYWORD_RETURN:
				if (!$x->inFunction)
					throw $this->t->newSyntaxError('Invalid return');

				$n = new JSNode($this->t);
				$tt = $this->t->peekOnSameLine();
				if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
					$n->value = $this->Expression($x);
				else
					$n->value = null;
			break;

			case KEYWORD_WITH:
				$n = new JSNode($this->t);
				$n->object = $this->ParenExpression($x);
				$n->body = $this->nest($x, $n);
			return $n;

			case KEYWORD_VAR:
			case KEYWORD_CONST:
			        $n = $this->Variables($x);
			break;

			case TOKEN_CONDCOMMENT_MULTILINE:
				$n = new JSNode($this->t);
			return $n;

			case KEYWORD_DEBUGGER:
				$n = new JSNode($this->t);
			break;

			case TOKEN_NEWLINE:
			case OP_SEMICOLON:
				$n = new JSNode($this->t, OP_SEMICOLON);
				$n->expression = null;
			return $n;

			default:
				if ($tt == TOKEN_IDENTIFIER)
				{
					$this->t->scanOperand = false;
					$tt = $this->t->peek();
					$this->t->scanOperand = true;
					if ($tt == OP_COLON)
					{
						$label = $this->t->currentToken()->value;
						$ss = $x->stmtStack;
						for ($i = count($ss) - 1; $i >= 0; --$i)
						{
							if ($ss[$i]->label == $label)
								throw $this->t->newSyntaxError('Duplicate label');
						}

						$this->t->get();
						$n = new JSNode($this->t, JS_LABEL);
						$n->label = $label;
						$n->statement = $this->nest($x, $n);

						return $n;
					}
				}

				$n = new JSNode($this->t, OP_SEMICOLON);
				$this->t->unget();
				$n->expression = $this->Expression($x);
				$n->end = $n->expression->end;
			break;
		}

		if ($this->t->lineno == $this->t->currentToken()->lineno)
		{
			$tt = $this->t->peekOnSameLine();
			if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
				throw $this->t->newSyntaxError('Missing ; before statement');
		}

		$this->t->match(OP_SEMICOLON);

		return $n;
	}

	private function FunctionDefinition($x, $requireName, $functionForm)
	{
		$f = new JSNode($this->t);

		if ($f->type != KEYWORD_FUNCTION)
			$f->type = ($f->value == 'get') ? JS_GETTER : JS_SETTER;

		if ($this->t->match(TOKEN_IDENTIFIER))
			$f->name = $this->t->currentToken()->value;
		elseif ($requireName)
			throw $this->t->newSyntaxError('Missing function identifier');

		$this->t->mustMatch(OP_LEFT_PAREN);
			$f->params = array();

		while (($tt = $this->t->get()) != OP_RIGHT_PAREN)
		{
			if ($tt != TOKEN_IDENTIFIER)
				throw $this->t->newSyntaxError('Missing formal parameter');

			array_push($f->params, $this->t->currentToken()->value);

			if ($this->t->peek() != OP_RIGHT_PAREN)
				$this->t->mustMatch(OP_COMMA);
		}

		$this->t->mustMatch(OP_LEFT_CURLY);

		$x2 = new JSCompilerContext(true);
		$f->body = $this->Script($x2);

		$this->t->mustMatch(OP_RIGHT_CURLY);
		$f->end = $this->t->currentToken()->end;

		$f->functionForm = $functionForm;
		if ($functionForm == DECLARED_FORM)
			array_push($x->funDecls, $f);

		return $f;
	}

	private function Variables($x)
	{
		$n = new JSNode($this->t);

		do
		{
			$this->t->mustMatch(TOKEN_IDENTIFIER);

			$n2 = new JSNode($this->t);
			$n2->name = $n2->value;

			if ($this->t->match(OP_ASSIGN))
			{
				if ($this->t->currentToken()->assignOp)
					throw $this->t->newSyntaxError('Invalid variable initialization');

				$n2->initializer = $this->Expression($x, OP_COMMA);
			}

			$n2->readOnly = $n->type == KEYWORD_CONST;

			$n->addNode($n2);
			array_push($x->varDecls, $n2);
		}
		while ($this->t->match(OP_COMMA));

		return $n;
	}

	private function Expression($x, $stop=false)
	{
		$operators = array();
		$operands = array();
		$n = false;

		$bl = $x->bracketLevel;
		$cl = $x->curlyLevel;
		$pl = $x->parenLevel;
		$hl = $x->hookLevel;

		while (($tt = $this->t->get()) != TOKEN_END)
		{
			if ($tt == $stop &&
				$x->bracketLevel == $bl &&
				$x->curlyLevel == $cl &&
				$x->parenLevel == $pl &&
				$x->hookLevel == $hl
			)
			{
				// Stop only if tt matches the optional stop parameter, and that
				// token is not quoted by some kind of bracket.
				break;
			}

			switch ($tt)
			{
				case OP_SEMICOLON:
					// NB: cannot be empty, Statement handled that.
					break 2;

				case OP_ASSIGN:
				case OP_HOOK:
				case OP_COLON:
					if ($this->t->scanOperand)
						break 2;

					// Use >, not >=, for right-associative ASSIGN and HOOK/COLON.
					while (	!empty($operators) &&
						(	$this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] ||
							($tt == OP_COLON && end($operators)->type == OP_ASSIGN)
						)
					)
						$this->reduce($operators, $operands);

					if ($tt == OP_COLON)
					{
						$n = end($operators);
						if ($n->type != OP_HOOK)
							throw $this->t->newSyntaxError('Invalid label');

						--$x->hookLevel;
					}
					else
					{
						array_push($operators, new JSNode($this->t));
						if ($tt == OP_ASSIGN)
							end($operands)->assignOp = $this->t->currentToken()->assignOp;
						else
							++$x->hookLevel;
					}

					$this->t->scanOperand = true;
				break;

				case KEYWORD_IN:
					// An in operator should not be parsed if we're parsing the head of
					// a for (...) loop, unless it is in the then part of a conditional
					// expression, or parenthesized somehow.
					if ($x->inForLoopInit && !$x->hookLevel &&
						!$x->bracketLevel && !$x->curlyLevel &&
						!$x->parenLevel
					)
					{
						break 2;
					}
				// FALL THROUGH
				case OP_COMMA:
					// Treat comma as left-associative so reduce can fold left-heavy
					// COMMA trees into a single array.
					// FALL THROUGH
				case OP_OR:
				case OP_AND:
				case OP_BITWISE_OR:
				case OP_BITWISE_XOR:
				case OP_BITWISE_AND:
				case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
				case OP_LT: case OP_LE: case OP_GE: case OP_GT:
				case KEYWORD_INSTANCEOF:
				case OP_LSH: case OP_RSH: case OP_URSH:
				case OP_PLUS: case OP_MINUS:
				case OP_MUL: case OP_DIV: case OP_MOD:
				case OP_DOT:
					if ($this->t->scanOperand)
						break 2;

					while (	!empty($operators) &&
						$this->opPrecedence[end($operators)->type] >= $this->opPrecedence[$tt]
					)
						$this->reduce($operators, $operands);

					if ($tt == OP_DOT)
					{
						$this->t->mustMatch(TOKEN_IDENTIFIER);
						array_push($operands, new JSNode($this->t, OP_DOT, array_pop($operands), new JSNode($this->t)));
					}
					else
					{
						array_push($operators, new JSNode($this->t));
						$this->t->scanOperand = true;
					}
				break;

				case KEYWORD_DELETE: case KEYWORD_VOID: case KEYWORD_TYPEOF:
				case OP_NOT: case OP_BITWISE_NOT: case OP_UNARY_PLUS: case OP_UNARY_MINUS:
				case KEYWORD_NEW:
					if (!$this->t->scanOperand)
						break 2;

					array_push($operators, new JSNode($this->t));
				break;

				case OP_INCREMENT: case OP_DECREMENT:
					if ($this->t->scanOperand)
					{
						array_push($operators, new JSNode($this->t));  // prefix increment or decrement
					}
					else
					{
						// Don't cross a line boundary for postfix {in,de}crement.
						$t = $this->t->tokens[($this->t->tokenIndex + $this->t->lookahead - 1) & 3];
						if ($t && $t->lineno != $this->t->lineno)
							break 2;

						if (!empty($operators))
						{
							// Use >, not >=, so postfix has higher precedence than prefix.
							while ($this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt])
								$this->reduce($operators, $operands);
						}

						$n = new JSNode($this->t, $tt, array_pop($operands));
						$n->postfix = true;
						array_push($operands, $n);
					}
				break;

				case KEYWORD_FUNCTION:
					if (!$this->t->scanOperand)
						break 2;

					array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM));
					$this->t->scanOperand = false;
				break;

				case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
				case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
					if (!$this->t->scanOperand)
						break 2;

					array_push($operands, new JSNode($this->t));
					$this->t->scanOperand = false;
				break;

				case TOKEN_CONDCOMMENT_MULTILINE:
					if ($this->t->scanOperand)
						array_push($operators, new JSNode($this->t));
					else
						array_push($operands, new JSNode($this->t));
				break;

				case OP_LEFT_BRACKET:
					if ($this->t->scanOperand)
					{
						// Array initialiser.  Parse using recursive descent, as the
						// sub-grammar here is not an operator grammar.
						$n = new JSNode($this->t, JS_ARRAY_INIT);
						while (($tt = $this->t->peek()) != OP_RIGHT_BRACKET)
						{
							if ($tt == OP_COMMA)
							{
								$this->t->get();
								$n->addNode(null);
								continue;
							}

							$n->addNode($this->Expression($x, OP_COMMA));
							if (!$this->t->match(OP_COMMA))
								break;
						}

						$this->t->mustMatch(OP_RIGHT_BRACKET);
						array_push($operands, $n);
						$this->t->scanOperand = false;
					}
					else
					{
						// Property indexing operator.
						array_push($operators, new JSNode($this->t, JS_INDEX));
						$this->t->scanOperand = true;
						++$x->bracketLevel;
					}
				break;

				case OP_RIGHT_BRACKET:
					if ($this->t->scanOperand || $x->bracketLevel == $bl)
						break 2;

					while ($this->reduce($operators, $operands)->type != JS_INDEX)
						continue;

					--$x->bracketLevel;
				break;

				case OP_LEFT_CURLY:
					if (!$this->t->scanOperand)
						break 2;

					// Object initialiser.  As for array initialisers (see above),
					// parse using recursive descent.
					++$x->curlyLevel;
					$n = new JSNode($this->t, JS_OBJECT_INIT);
					while (!$this->t->match(OP_RIGHT_CURLY))
					{
						do
						{
							$tt = $this->t->get();
							$tv = $this->t->currentToken()->value;
							if (($tv == 'get' || $tv == 'set') && $this->t->peek() == TOKEN_IDENTIFIER)
							{
								if ($x->ecmaStrictMode)
									throw $this->t->newSyntaxError('Illegal property accessor');

								$n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM));
							}
							else
							{
								switch ($tt)
								{
									case TOKEN_IDENTIFIER:
									case TOKEN_NUMBER:
									case TOKEN_STRING:
										$id = new JSNode($this->t);
									break;

									case OP_RIGHT_CURLY:
										if ($x->ecmaStrictMode)
											throw $this->t->newSyntaxError('Illegal trailing ,');
									break 3;

									default:
										throw $this->t->newSyntaxError('Invalid property name');
								}

								$this->t->mustMatch(OP_COLON);
								$n->addNode(new JSNode($this->t, JS_PROPERTY_INIT, $id, $this->Expression($x, OP_COMMA)));
							}
						}
						while ($this->t->match(OP_COMMA));

						$this->t->mustMatch(OP_RIGHT_CURLY);
						break;
					}

					array_push($operands, $n);
					$this->t->scanOperand = false;
					--$x->curlyLevel;
				break;

				case OP_RIGHT_CURLY:
					if (!$this->t->scanOperand && $x->curlyLevel != $cl)
						throw new Exception('PANIC: right curly botch');
				break 2;

				case OP_LEFT_PAREN:
					if ($this->t->scanOperand)
					{
						array_push($operators, new JSNode($this->t, JS_GROUP));
					}
					else
					{
						while (	!empty($operators) &&
							$this->opPrecedence[end($operators)->type] > $this->opPrecedence[KEYWORD_NEW]
						)
							$this->reduce($operators, $operands);

						// Handle () now, to regularize the n-ary case for n > 0.
						// We must set scanOperand in case there are arguments and
						// the first one is a regexp or unary+/-.
						$n = end($operators);
						$this->t->scanOperand = true;
						if ($this->t->match(OP_RIGHT_PAREN))
						{
							if ($n && $n->type == KEYWORD_NEW)
							{
								array_pop($operators);
								$n->addNode(array_pop($operands));
							}
							else
							{
								$n = new JSNode($this->t, JS_CALL, array_pop($operands), new JSNode($this->t, JS_LIST));
							}

							array_push($operands, $n);
							$this->t->scanOperand = false;
							break;
						}

						if ($n && $n->type == KEYWORD_NEW)
							$n->type = JS_NEW_WITH_ARGS;
						else
							array_push($operators, new JSNode($this->t, JS_CALL));
					}

					++$x->parenLevel;
				break;

				case OP_RIGHT_PAREN:
					if ($this->t->scanOperand || $x->parenLevel == $pl)
						break 2;

					while (($tt = $this->reduce($operators, $operands)->type) != JS_GROUP &&
						$tt != JS_CALL && $tt != JS_NEW_WITH_ARGS
					)
					{
						continue;
					}

					if ($tt != JS_GROUP)
					{
						$n = end($operands);
						if ($n->treeNodes[1]->type != OP_COMMA)
							$n->treeNodes[1] = new JSNode($this->t, JS_LIST, $n->treeNodes[1]);
						else
							$n->treeNodes[1]->type = JS_LIST;
					}

					--$x->parenLevel;
				break;

				// Automatic semicolon insertion means we may scan across a newline
				// and into the beginning of another statement.  If so, break out of
				// the while loop and let the t.scanOperand logic handle errors.
				default:
					break 2;
			}
		}

		if ($x->hookLevel != $hl)
			throw $this->t->newSyntaxError('Missing : after ?');

		if ($x->parenLevel != $pl)
			throw $this->t->newSyntaxError('Missing ) in parenthetical');

		if ($x->bracketLevel != $bl)
			throw $this->t->newSyntaxError('Missing ] in index expression');

		if ($this->t->scanOperand)
			throw $this->t->newSyntaxError('Missing operand');

		// Resume default mode, scanning for operands, not operators.
		$this->t->scanOperand = true;
		$this->t->unget();

		while (count($operators))
			$this->reduce($operators, $operands);

		return array_pop($operands);
	}

	private function ParenExpression($x)
	{
		$this->t->mustMatch(OP_LEFT_PAREN);
		$n = $this->Expression($x);
		$this->t->mustMatch(OP_RIGHT_PAREN);

		return $n;
	}

	// Statement stack and nested statement handler.
	private function nest($x, $node, $end = false)
	{
		array_push($x->stmtStack, $node);
		$n = $this->statement($x);
		array_pop($x->stmtStack);

		if ($end)
			$this->t->mustMatch($end);

		return $n;
	}

	private function reduce(&$operators, &$operands)
	{
		$n = array_pop($operators);
		$op = $n->type;
		$arity = $this->opArity[$op];
		$c = count($operands);
		if ($arity == -2)
		{
			// Flatten left-associative trees
			if ($c >= 2)
			{
				$left = $operands[$c - 2];
				if ($left->type == $op)
				{
					$right = array_pop($operands);
					$left->addNode($right);
					return $left;
				}
			}
			$arity = 2;
		}

		// Always use push to add operands to n, to update start and end
		$a = array_splice($operands, $c - $arity);
		for ($i = 0; $i < $arity; $i++)
			$n->addNode($a[$i]);

		// Include closing bracket or postfix operator in [start,end]
		$te = $this->t->currentToken()->end;
		if ($n->end < $te)
			$n->end = $te;

		array_push($operands, $n);

		return $n;
	}
}

class JSCompilerContext
{
	public $inFunction = false;
	public $inForLoopInit = false;
	public $ecmaStrictMode = false;
	public $bracketLevel = 0;
	public $curlyLevel = 0;
	public $parenLevel = 0;
	public $hookLevel = 0;

	public $stmtStack = array();
	public $funDecls = array();
	public $varDecls = array();

	public function __construct($inFunction)
	{
		$this->inFunction = $inFunction;
	}
}

class JSNode
{
	private $type;
	private $value;
	private $lineno;
	private $start;
	private $end;

	public $treeNodes = array();
	public $funDecls = array();
	public $varDecls = array();

	public function __construct($t, $type=0)
	{
		if ($token = $t->currentToken())
		{
			$this->type = $type ? $type : $token->type;
			$this->value = $token->value;
			$this->lineno = $token->lineno;
			$this->start = $token->start;
			$this->end = $token->end;
		}
		else
		{
			$this->type = $type;
			$this->lineno = $t->lineno;
		}

		if (($numargs = func_num_args()) > 2)
		{
			$args = func_get_args();;
			for ($i = 2; $i < $numargs; $i++)
				$this->addNode($args[$i]);
		}
	}

	// we don't want to bloat our object with all kind of specific properties, so we use overloading
	public function __set($name, $value)
	{
		$this->$name = $value;
	}

	public function __get($name)
	{
		if (isset($this->$name))
			return $this->$name;

		return null;
	}

	public function addNode($node)
	{
		$this->treeNodes[] = $node;
	}
}

class JSTokenizer
{
	private $cursor = 0;
	private $source;

	public $tokens = array();
	public $tokenIndex = 0;
	public $lookahead = 0;
	public $scanNewlines = false;
	public $scanOperand = true;

	public $filename;
	public $lineno;

	private $keywords = array(
		'break',
		'case', 'catch', 'const', 'continue',
		'debugger', 'default', 'delete', 'do',
		'else', 'enum',
		'false', 'finally', 'for', 'function',
		'if', 'in', 'instanceof',
		'new', 'null',
		'return',
		'switch',
		'this', 'throw', 'true', 'try', 'typeof',
		'var', 'void',
		'while', 'with'
	);

	private $opTypeNames = array(
		';'	=> 'SEMICOLON',
		','	=> 'COMMA',
		'?'	=> 'HOOK',
		':'	=> 'COLON',
		'||'	=> 'OR',
		'&&'	=> 'AND',
		'|'	=> 'BITWISE_OR',
		'^'	=> 'BITWISE_XOR',
		'&'	=> 'BITWISE_AND',
		'==='	=> 'STRICT_EQ',
		'=='	=> 'EQ',
		'='	=> 'ASSIGN',
		'!=='	=> 'STRICT_NE',
		'!='	=> 'NE',
		'<<'	=> 'LSH',
		'<='	=> 'LE',
		'<'	=> 'LT',
		'>>>'	=> 'URSH',
		'>>'	=> 'RSH',
		'>='	=> 'GE',
		'>'	=> 'GT',
		'++'	=> 'INCREMENT',
		'--'	=> 'DECREMENT',
		'+'	=> 'PLUS',
		'-'	=> 'MINUS',
		'*'	=> 'MUL',
		'/'	=> 'DIV',
		'%'	=> 'MOD',
		'!'	=> 'NOT',
		'~'	=> 'BITWISE_NOT',
		'.'	=> 'DOT',
		'['	=> 'LEFT_BRACKET',
		']'	=> 'RIGHT_BRACKET',
		'{'	=> 'LEFT_CURLY',
		'}'	=> 'RIGHT_CURLY',
		'('	=> 'LEFT_PAREN',
		')'	=> 'RIGHT_PAREN',
		'@*/'	=> 'CONDCOMMENT_END'
	);

	private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
	private $opRegExp;

	public function __construct()
	{
		$this->opRegExp = '#^(' . implode('|', array_map('preg_quote', array_keys($this->opTypeNames))) . ')#';

		// this is quite a hidden yet convenient place to create the defines for operators and keywords
		foreach ($this->opTypeNames as $operand => $name)
			define('OP_' . $name, $operand);

		define('OP_UNARY_PLUS', 'U+');
		define('OP_UNARY_MINUS', 'U-');

		foreach ($this->keywords as $keyword)
			define('KEYWORD_' . strtoupper($keyword), $keyword);
	}

	public function init($source, $filename = '', $lineno = 1)
	{
		$this->source = $source;
		$this->filename = $filename ? $filename : '[inline]';
		$this->lineno = $lineno;

		$this->cursor = 0;
		$this->tokens = array();
		$this->tokenIndex = 0;
		$this->lookahead = 0;
		$this->scanNewlines = false;
		$this->scanOperand = true;
	}

	public function getInput($chunksize)
	{
		if ($chunksize)
			return substr($this->source, $this->cursor, $chunksize);

		return substr($this->source, $this->cursor);
	}

	public function isDone()
	{
		return $this->peek() == TOKEN_END;
	}

	public function match($tt)
	{
		return $this->get() == $tt || $this->unget();
	}

	public function mustMatch($tt)
	{
	        if (!$this->match($tt))
			throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');

		return $this->currentToken();
	}

	public function peek()
	{
		if ($this->lookahead)
		{
			$next = $this->tokens[($this->tokenIndex + $this->lookahead) & 3];
			if ($this->scanNewlines && $next->lineno != $this->lineno)
				$tt = TOKEN_NEWLINE;
			else
				$tt = $next->type;
		}
		else
		{
			$tt = $this->get();
			$this->unget();
		}

		return $tt;
	}

	public function peekOnSameLine()
	{
		$this->scanNewlines = true;
		$tt = $this->peek();
		$this->scanNewlines = false;

		return $tt;
	}

	public function currentToken()
	{
		if (!empty($this->tokens))
			return $this->tokens[$this->tokenIndex];
	}

	public function get($chunksize = 1000)
	{
		while($this->lookahead)
		{
			$this->lookahead--;
			$this->tokenIndex = ($this->tokenIndex + 1) & 3;
			$token = $this->tokens[$this->tokenIndex];
			if ($token->type != TOKEN_NEWLINE || $this->scanNewlines)
				return $token->type;
		}

		$conditional_comment = false;

		// strip whitespace and comments
		while(true)
		{
			$input = $this->getInput($chunksize);

			// whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
			$re = $this->scanNewlines ? '/^[ \r\t]+/' : '/^\s+/';
			if (preg_match($re, $input, $match))
			{
				$spaces = $match[0];
				$spacelen = strlen($spaces);
				$this->cursor += $spacelen;
				if (!$this->scanNewlines)
					$this->lineno += substr_count($spaces, "\n");

				if ($spacelen == $chunksize)
					continue; // complete chunk contained whitespace

				$input = $this->getInput($chunksize);
				if ($input == '' || $input[0] != '/')
					break;
			}

			// Comments
			if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?(?:.|\n)*?\*\/|\/.*)/', $input, $match))
			{
				if (!$chunksize)
					break;

				// retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
				$chunksize = null;
				continue;
			}

			// check if this is a conditional (JScript) comment
			if (!empty($match[1]))
			{
				//$match[0] = '/*' . $match[1];
				$conditional_comment = true;
				break;
			}
			else
			{
				$this->cursor += strlen($match[0]);
				$this->lineno += substr_count($match[0], "\n");
			}
		}

		if ($input == '')
		{
			$tt = TOKEN_END;
			$match = array('');
		}
		elseif ($conditional_comment)
		{
			$tt = TOKEN_CONDCOMMENT_MULTILINE;
		}
		else
		{
			switch ($input[0])
			{
				case '0': case '1': case '2': case '3': case '4':
				case '5': case '6': case '7': case '8': case '9':
					if (preg_match('/^\d+\.\d*(?:[eE][-+]?\d+)?|^\d+(?:\.\d*)?[eE][-+]?\d+/', $input, $match))
					{
						$tt = TOKEN_NUMBER;
					}
					elseif (preg_match('/^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/', $input, $match))
					{
						// this should always match because of \d+
						$tt = TOKEN_NUMBER;
					}
				break;

				case '"':
				case "'":
					if (preg_match('/^"(?:\\\\(?:.|\r?\n)|[^\\\\"\r\n])*"|^\'(?:\\\\(?:.|\r?\n)|[^\\\\\'\r\n])*\'/', $input, $match))
					{
						$tt = TOKEN_STRING;
					}
					else
					{
						if ($chunksize)
							return $this->get(null); // retry with a full chunk fetch

						throw $this->newSyntaxError('Unterminated string literal');
					}
				break;

				case '/':
					if ($this->scanOperand && preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
					{
						$tt = TOKEN_REGEXP;
						break;
					}
				// fall through

				case '|':
				case '^':
				case '&':
				case '<':
				case '>':
				case '+':
				case '-':
				case '*':
				case '%':
				case '=':
				case '!':
					// should always match
					preg_match($this->opRegExp, $input, $match);
					$op = $match[0];
					if (in_array($op, $this->assignOps) && $input[strlen($op)] == '=')
					{
						$tt = OP_ASSIGN;
						$match[0] .= '=';
					}
					else
					{
						$tt = $op;
						if ($this->scanOperand)
						{
							if ($op == OP_PLUS)
								$tt = OP_UNARY_PLUS;
							elseif ($op == OP_MINUS)
								$tt = OP_UNARY_MINUS;
						}
						$op = null;
					}
				break;

				case '.':
					if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
					{
						$tt = TOKEN_NUMBER;
						break;
					}
				// fall through

				case ';':
				case ',':
				case '?':
				case ':':
				case '~':
				case '[':
				case ']':
				case '{':
				case '}':
				case '(':
				case ')':
					// these are all single
					$match = array($input[0]);
					$tt = $input[0];
				break;

				case '@':
					throw $this->newSyntaxError('Illegal token');
				break;

				case "\n":
					if ($this->scanNewlines)
					{
						$match = array("\n");
						$tt = TOKEN_NEWLINE;
					}
					else
						throw $this->newSyntaxError('Illegal token');
				break;

				default:
					// FIXME: add support for unicode and unicode escape sequence \uHHHH
					if (preg_match('/^[$\w]+/', $input, $match))
					{
						$tt = in_array($match[0], $this->keywords) ? $match[0] : TOKEN_IDENTIFIER;
					}
					else
						throw $this->newSyntaxError('Illegal token');
			}
		}

		$this->tokenIndex = ($this->tokenIndex + 1) & 3;

		if (!isset($this->tokens[$this->tokenIndex]))
			$this->tokens[$this->tokenIndex] = new JSToken();

		$token = $this->tokens[$this->tokenIndex];
		$token->type = $tt;

		if ($tt == OP_ASSIGN)
			$token->assignOp = $op;

		$token->start = $this->cursor;

		$token->value = $match[0];
		$this->cursor += strlen($match[0]);

		$token->end = $this->cursor;
		$token->lineno = $this->lineno;

		return $tt;
	}

	public function unget()
	{
		if (++$this->lookahead == 4)
			throw $this->newSyntaxError('PANIC: too much lookahead!');

		$this->tokenIndex = ($this->tokenIndex - 1) & 3;
	}

	public function newSyntaxError($m)
	{
		return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename . '\' on line ' . $this->lineno);
	}
}

class JSToken
{
	public $type;
	public $value;
	public $start;
	public $end;
	public $lineno;
	public $assignOp;
}

?>


code:
<?php
/**
 * Class Minify
 * @package Minify
 */

/**
 * Minify_Source
 */
require_once 'Minify/Source.php';

/**
 * Minify - Combines, minifies, and caches JavaScript and CSS files on demand.
 *
 * See README for usage instructions (for now).
 *
 * This library was inspired by {@link mailto:flashkot@mail.ru jscsscomp by Maxim Martynyuk}
 * and by the article {@link http://www.hunlock.com/blogs/Supercharged_Javascript "Supercharged JavaScript" by Patrick Hunlock}.
 *
 * Requires PHP 5.1.0.
 * Tested on PHP 5.1.6.
 *
 * @package Minify
 * @author Ryan Grove <ryan@wonko.com>
 * @author Stephen Clay <steve@mrclay.org>
 * @copyright 2008 Ryan Grove, Stephen Clay. All rights reserved.
 * @license http://opensource.org/licenses/bsd-license.php  New BSD License
 * @link http://code.google.com/p/minify/
 */
class Minify {

    const VERSION = '2.1.3';
    const TYPE_CSS = 'text/css';
    const TYPE_HTML = 'text/html';
    // there is some debate over the ideal JS Content-Type, but this is the
    // Apache default and what Yahoo! uses..
    const TYPE_JS = 'application/x-javascript';

    /**
     * How many hours behind are the file modification times of uploaded files?
     *
     * If you upload files from Windows to a non-Windows server, Windows may report
     * incorrect mtimes for the files. Immediately after modifying and uploading a
     * file, use the touch command to update the mtime on the server. If the mtime
     * jumps ahead by a number of hours, set this variable to that number. If the mtime
     * moves back, this should not be needed.
     *
     * @var int $uploaderHoursBehind
     */
    public static $uploaderHoursBehind = 0;

    /**
     * If this string is not empty AND the serve() option 'bubbleCssImports' is
     * NOT set, then serve() will check CSS files for @import declarations that
     * appear too late in the combined stylesheet. If found, serve() will prepend
     * the output with this warning.
     *
     * @var string $importWarning
     */
    public static $importWarning = "/* See http://code.google.com/p/minify/wiki/CommonProblems#@imports_can_appear_in_invalid_locations_in_combined_CSS_files */\n";

    /**
     * Specify a cache object (with identical interface as Minify_Cache_File) or
     * a path to use with Minify_Cache_File.
     *
     * If not called, Minify will not use a cache and, for each 200 response, will
     * need to recombine files, minify and encode the output.
     *
     * @param mixed $cache object with identical interface as Minify_Cache_File or
     * a directory path, or null to disable caching. (default = '')
     *
     * @param bool $fileLocking (default = true) This only applies if the first
     * parameter is a string.
     *
     * @return null
     */
    public static function setCache($cache = '', $fileLocking = true)
    {
        if (is_string($cache)) {
            require_once 'Minify/Cache/File.php';
            self::$_cache = new Minify_Cache_File($cache, $fileLocking);
        } else {
            self::$_cache = $cache;
        }
    }

    /**
     * Serve a request for a minified file.
     *
     * Here are the available options and defaults in the base controller:
     *
     * 'isPublic' : send "public" instead of "private" in Cache-Control
     * headers, allowing shared caches to cache the output. (default true)
     *
     * 'quiet' : set to true to have serve() return an array rather than sending
     * any headers/output (default false)
     *
     * 'encodeOutput' : set to false to disable content encoding, and not send
     * the Vary header (default true)
     *
     * 'encodeMethod' : generally you should let this be determined by
     * HTTP_Encoder (leave null), but you can force a particular encoding
     * to be returned, by setting this to 'gzip' or '' (no encoding)
     *
     * 'encodeLevel' : level of encoding compression (0 to 9, default 9)
     *
     * 'contentTypeCharset' : appended to the Content-Type header sent. Set to a falsey
     * value to remove. (default 'utf-8')
     *
     * 'maxAge' : set this to the number of seconds the client should use its cache
     * before revalidating with the server. This sets Cache-Control: max-age and the
     * Expires header. Unlike the old 'setExpires' setting, this setting will NOT
     * prevent conditional GETs. Note this has nothing to do with server-side caching.
     *
     * 'rewriteCssUris' : If true, serve() will automatically set the 'currentDir'
     * minifier option to enable URI rewriting in CSS files (default true)
     *
     * 'bubbleCssImports' : If true, all @import declarations in combined CSS
     * files will be move to the top. Note this may alter effective CSS values
     * due to a change in order. (default false)
     *
     * 'debug' : set to true to minify all sources with the 'Lines' controller, which
     * eases the debugging of combined files. This also prevents 304 responses.
     * @see Minify_Lines::minify()
     *
     * 'minifiers' : to override Minify's default choice of minifier function for
     * a particular content-type, specify your callback under the key of the
     * content-type:
     *
     * // call customCssMinifier($css) for all CSS minification
     * $options['minifiers'][Minify::TYPE_CSS] = 'customCssMinifier';
     *
     * // don't minify Javascript at all
     * $options['minifiers'][Minify::TYPE_JS] = '';
     *
     *
     * 'minifierOptions' : to send options to the minifier function, specify your options
     * under the key of the content-type. E.g. To send the CSS minifier an option:
     *
     * // give CSS minifier array('optionName' => 'optionValue') as 2nd argument
     * $options['minifierOptions'][Minify::TYPE_CSS]['optionName'] = 'optionValue';
     *
     *
     * 'contentType' : (optional) this is only needed if your file extension is not
     * js/css/html. The given content-type will be sent regardless of source file
     * extension, so this should not be used in a Groups config with other
     * Javascript/CSS files.
     *
     * Any controller options are documented in that controller's setupSources() method.
     *
     * @param mixed instance of subclass of Minify_Controller_Base or string name of
     * controller. E.g. 'Files'
     *
     * @param array $options controller/serve options
     *
     * @return mixed null, or, if the 'quiet' option is set to true, an array
     * with keys "success" (bool), "statusCode" (int), "content" (string), and
     * "headers" (array).
     */
    public static function serve($controller, $options = array())
    {
        if (is_string($controller)) {
            // make $controller into object
            $class = 'Minify_Controller_' . $controller;
            if (! class_exists($class, false)) {
                require_once "Minify/Controller/"
                    . str_replace('_', '/', $controller) . ".php";
            }
            $controller = new $class();
        }

        // set up controller sources and mix remaining options with
        // controller defaults
        $options = $controller->setupSources($options);
        $options = $controller->analyzeSources($options);
        self::$_options = $controller->mixInDefaultOptions($options);

        // check request validity
        if (! $controller->sources) {
            // invalid request!
            if (! self::$_options['quiet']) {
                header(self::$_options['badRequestHeader']);
                echo self::$_options['badRequestHeader'];
                return;
            } else {
                list(,$statusCode) = explode(' ', self::$_options['badRequestHeader']);
                return array(
                    'success' => false
                    ,'statusCode' => (int)$statusCode
                    ,'content' => ''
                    ,'headers' => array()
                );
            }
        }

        self::$_controller = $controller;

        if (self::$_options['debug']) {
            self::_setupDebug($controller->sources);
            self::$_options['maxAge'] = 0;
        }

        // determine encoding
        if (self::$_options['encodeOutput']) {
            if (self::$_options['encodeMethod'] !== null) {
                // controller specifically requested this
                $contentEncoding = self::$_options['encodeMethod'];
            } else {
                // sniff request header
                require_once 'HTTP/Encoder.php';
                // depending on what the client accepts, $contentEncoding may be
                // 'x-gzip' while our internal encodeMethod is 'gzip'. Calling
                // getAcceptedEncoding(false, false) leaves out compress and deflate as options.
                list(self::$_options['encodeMethod'], $contentEncoding) = HTTP_Encoder::getAcceptedEncoding(false, false);
            }
        } else {
            self::$_options['encodeMethod'] = ''; // identity (no encoding)
        }

        // check client cache
        require_once 'HTTP/ConditionalGet.php';
        $cgOptions = array(
            'lastModifiedTime' => self::$_options['lastModifiedTime']
            ,'isPublic' => self::$_options['isPublic']
            ,'encoding' => self::$_options['encodeMethod']
        );
        if (self::$_options['maxAge'] > 0) {
            $cgOptions['maxAge'] = self::$_options['maxAge'];
        }
        $cg = new HTTP_ConditionalGet($cgOptions);
        if ($cg->cacheIsValid) {
            // client's cache is valid
            if (! self::$_options['quiet']) {
                $cg->sendHeaders();
                return;
            } else {
                return array(
                    'success' => true
                    ,'statusCode' => 304
                    ,'content' => ''
                    ,'headers' => $cg->getHeaders()
                );
            }
        } else {
            // client will need output
            $headers = $cg->getHeaders();
            unset($cg);
        }

        if (self::$_options['contentType'] === self::TYPE_CSS
            && self::$_options['rewriteCssUris']) {
            reset($controller->sources);
            while (list($key, $source) = each($controller->sources)) {
                if ($source->filepath
                    && !isset($source->minifyOptions['currentDir'])
                    && !isset($source->minifyOptions['prependRelativePath'])
                ) {
                    $source->minifyOptions['currentDir'] = dirname($source->filepath);
                }
            }
        }

        // check server cache
        if (null !== self::$_cache) {
            // using cache
            // the goal is to use only the cache methods to sniff the length and
            // output the content, as they do not require ever loading the file into
            // memory.
            $cacheId = 'minify_' . self::_getCacheId();
            $fullCacheId = (self::$_options['encodeMethod'])
                ? $cacheId . '.gz'
                : $cacheId;
            // check cache for valid entry
            $cacheIsReady = self::$_cache->isValid($fullCacheId, self::$_options['lastModifiedTime']);
            if ($cacheIsReady) {
                $cacheContentLength = self::$_cache->getSize($fullCacheId);
            } else {
                // generate & cache content
                $content = self::_combineMinify();
                self::$_cache->store($cacheId, $content);
                if (function_exists('gzencode')) {
                    self::$_cache->store($cacheId . '.gz', gzencode($content, self::$_options['encodeLevel']));
                }
            }
        } else {
            // no cache
            $cacheIsReady = false;
            $content = self::_combineMinify();
        }
        if (! $cacheIsReady && self::$_options['encodeMethod']) {
            // still need to encode
            $content = gzencode($content, self::$_options['encodeLevel']);
        }

        // add headers
        $headers['Content-Length'] = $cacheIsReady
            ? $cacheContentLength
            : strlen($content);
        $headers['Content-Type'] = self::$_options['contentTypeCharset']
            ? self::$_options['contentType'] . '; charset=' . self::$_options['contentTypeCharset']
            : self::$_options['contentType'];
        if (self::$_options['encodeMethod'] !== '') {
            $headers['Content-Encoding'] = $contentEncoding;
        }
        if (self::$_options['encodeOutput']) {
            $headers['Vary'] = 'Accept-Encoding';
        }

        if (! self::$_options['quiet']) {
            // output headers & content
            foreach ($headers as $name => $val) {
                header($name . ': ' . $val);
            }
            if ($cacheIsReady) {
                self::$_cache->display($fullCacheId);
            } else {
                echo $content;
            }
        } else {
            return array(
                'success' => true
                ,'statusCode' => 200
                ,'content' => $cacheIsReady
                    ? self::$_cache->fetch($fullCacheId)
                    : $content
                ,'headers' => $headers
            );
        }
    }

    /**
     * Return combined minified content for a set of sources
     *
     * No internal caching will be used and the content will not be HTTP encoded.
     *
     * @param array $sources array of filepaths and/or Minify_Source objects
     *
     * @param array $options (optional) array of options for serve. By default
     * these are already set: quiet = true, encodeMethod = '', lastModifiedTime = 0.
     *
     * @return string
     */
    public static function combine($sources, $options = array())
    {
        $cache = self::$_cache;
        self::$_cache = null;
        $options = array_merge(array(
            'files' => (array)$sources
            ,'quiet' => true
            ,'encodeMethod' => ''
            ,'lastModifiedTime' => 0
        ), $options);
        $out = self::serve('Files', $options);
        self::$_cache = $cache;
        return $out['content'];
    }

    /**
     * On IIS, create $_SERVER['DOCUMENT_ROOT']
     *
     * @param bool $unsetPathInfo (default false) if true, $_SERVER['PATH_INFO']
     * will be unset (it is inconsistent with Apache's setting)
     *
     * @return null
     */
    public static function setDocRoot($unsetPathInfo = false)
    {
        if (isset($_SERVER['SERVER_SOFTWARE'])
            && 0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/')
        ) {
            $_SERVER['DOCUMENT_ROOT'] = rtrim(substr(
                $_SERVER['PATH_TRANSLATED']
                ,0
                ,strlen($_SERVER['PATH_TRANSLATED']) - strlen($_SERVER['SCRIPT_NAME'])
            ), '\\');
            if ($unsetPathInfo) {
                unset($_SERVER['PATH_INFO']);
            }
            require_once 'Minify/Logger.php';
            Minify_Logger::log("setDocRoot() set DOCUMENT_ROOT to \"{$_SERVER['DOCUMENT_ROOT']}\"");
        }
    }

    /**
     * @var mixed Minify_Cache_* object or null (i.e. no server cache is used)
     */
    private static $_cache = null;

    /**
     * @var Minify_Controller active controller for current request
     */
    protected static $_controller = null;

    /**
     * @var array options for current request
     */
    protected static $_options = null;

    /**
     * Set up sources to use Minify_Lines
     *
     * @param array $sources Minify_Source instances
     *
     * @return null
     */
    protected static function _setupDebug($sources)
    {
        foreach ($sources as $source) {
            $source->minifier = array('Minify_Lines', 'minify');
            $id = $source->getId();
            $source->minifyOptions = array(
                'id' => (is_file($id) ? basename($id) : $id)
            );
        }
    }

    /**
     * Combines sources and minifies the result.
     *
     * @return string
     */
    protected static function _combineMinify()
    {
        $type = self::$_options['contentType']; // ease readability

        // when combining scripts, make sure all statements separated and
        // trailing single line comment is terminated
        $implodeSeparator = ($type === self::TYPE_JS)
            ? "\n;"
            : '';
        // allow the user to pass a particular array of options to each
        // minifier (designated by type). source objects may still override
        // these
        $defaultOptions = isset(self::$_options['minifierOptions'][$type])
            ? self::$_options['minifierOptions'][$type]
            : array();
        // if minifier not set, default is no minification. source objects
        // may still override this
        $defaultMinifier = isset(self::$_options['minifiers'][$type])
            ? self::$_options['minifiers'][$type]
            : false;

        if (Minify_Source::haveNoMinifyPrefs(self::$_controller->sources)) {
            // all source have same options/minifier, better performance
            // to combine, then minify once
            foreach (self::$_controller->sources as $source) {
                $pieces[] = $source->getContent();
            }
            $content = implode($implodeSeparator, $pieces);
            if ($defaultMinifier) {
                self::$_controller->loadMinifier($defaultMinifier);
                $content = call_user_func($defaultMinifier, $content, $defaultOptions);
            }
        } else {
            // minify each source with its own options and minifier, then combine
            foreach (self::$_controller->sources as $source) {
                // allow the source to override our minifier and options
                $minifier = (null !== $source->minifier)
                    ? $source->minifier
                    : $defaultMinifier;
                $options = (null !== $source->minifyOptions)
                    ? array_merge($defaultOptions, $source->minifyOptions)
                    : $defaultOptions;
                if ($minifier) {
                    self::$_controller->loadMinifier($minifier);
                    // get source content and minify it
                    $pieces[] = call_user_func($minifier, $source->getContent(), $options);
                } else {
                    $pieces[] = $source->getContent();
                }
            }
            $content = implode($implodeSeparator, $pieces);
        }

        if ($type === self::TYPE_CSS && false !== strpos($content, '@import')) {
            $content = self::_handleCssImports($content);
        }

        // do any post-processing (esp. for editing build URIs)
        if (self::$_options['postprocessorRequire']) {
            require_once self::$_options['postprocessorRequire'];
        }
        if (self::$_options['postprocessor']) {
            $content = call_user_func(self::$_options['postprocessor'], $content, $type);
        }
        return $content;
    }

    /**
     * Make a unique cache id for for this request.
     *
     * Any settings that could affect output are taken into consideration
     *
     * @return string
     */
    protected static function _getCacheId()
    {
        return md5(serialize(array(
            Minify_Source::getDigest(self::$_controller->sources)
            ,self::$_options['minifiers']
            ,self::$_options['minifierOptions']
            ,self::$_options['postprocessor']
            ,self::$_options['bubbleCssImports']
        )));
    }

    /**
     * Bubble CSS @imports to the top or prepend a warning if an
     * @import is detected not at the top.
     */
    protected static function _handleCssImports($css)
    {
        if (self::$_options['bubbleCssImports']) {
            // bubble CSS imports
            preg_match_all('/@import.*?;/', $css, $imports);
            $css = implode('', $imports[0]) . preg_replace('/@import.*?;/', '', $css);
        } else if ('' !== self::$importWarning) {
            // remove comments so we don't mistake { in a comment as a block
            $noCommentCss = preg_replace('@/\\*[\\s\\S]*?\\*/@', '', $css);
            $lastImportPos = strrpos($noCommentCss, '@import');
            $firstBlockPos = strpos($noCommentCss, '{');
            if (false !== $lastImportPos
                && false !== $firstBlockPos
                && $firstBlockPos < $lastImportPos
            ) {
                // { appears before @import : prepend warning
                $css = self::$importWarning . $css;
            }
        }
        return $css;
    }
}

license: mit


code:
<?php
/**
 * Class Minify_Build
 * @package Minify
 */

require_once 'Minify/Source.php';

/**
 * Maintain a single last modification time for a group of Minify sources to
 * allow use of far off Expires headers in Minify.
 *
 *
 * // in config file
 * $groupSources = array(
 *   'js' => array('file1.js', 'file2.js')
 *   ,'css' => array('file1.css', 'file2.css', 'file3.css')
 * )
 *
 * // during HTML generation
 * $jsBuild = new Minify_Build($groupSources['js']);
 * $cssBuild = new Minify_Build($groupSources['css']);
 *
 * $script = "";
 * $link = "<link rel='stylesheet' type='text/css' href='"
 *     . $cssBuild->uri('/min.php/css') . "'>";
 *
 * // in min.php
 * Minify::serve('Groups', array(
 *   'groups' => $groupSources
 *   ,'setExpires' => (time() + 86400 * 365)
 * ));
 *
 *
 * @package Minify
 * @author Stephen Clay <steve@mrclay.org>
 */
class Minify_Build {

    /**
     * Last modification time of all files in the build
     *
     * @var int
     */
    public $lastModified = 0;

    /**
     * String to use as ampersand in uri(). Set this to '&' if
     * you are not HTML-escaping URIs.
     *
     * @var string
     */
    public static $ampersand = '&';

    /**
     * Get a time-stamped URI
     *
     *
     * echo $b->uri('/site.js');
     * // outputs "/site.js?1678242"
     *
     * echo $b->uri('/scriptaculous.js?load=effects');
     * // outputs "/scriptaculous.js?load=effects&amp1678242"
     *
     *
     * @param string $uri
     * @param boolean $forceAmpersand (default = false) Force the use of ampersand to
     * append the timestamp to the URI.
     * @return string
     */
    public function uri($uri, $forceAmpersand = false) {
        $sep = ($forceAmpersand || strpos($uri, '?') !== false)
            ? self::$ampersand
            : '?';
        return "{$uri}{$sep}{$this->lastModified}";
    }

	/**
     * Create a build object
     *
     * @param array $sources array of Minify_Source objects and/or file paths
     *
     * @return null
     */
    public function __construct($sources)
    {
        $max = 0;
        foreach ((array)$sources as $source) {
            if ($source instanceof Minify_Source) {
                $max = max($max, $source->lastModified);
            } elseif (is_string($source)) {
                if (0 === strpos($source, '//')) {
                    $source = $_SERVER['DOCUMENT_ROOT'] . substr($source, 1);
                }
                if (is_file($source)) {
                    $max = max($max, filemtime($source));
                }
            }
        }
        $this->lastModified = $max;
    }
}


code:
<?php
/**
 * Class Minify_CSS
 * @package Minify
 */

/**
 * Minify CSS
 *
 * This class uses Minify_CSS_Compressor and Minify_CSS_UriRewriter to
 * minify CSS and rewrite relative URIs.
 *
 * @package Minify
 * @author Stephen Clay <steve@mrclay.org>
 * @author http://code.google.com/u/1stvamp/ (Issue 64 patch)
 */
class Minify_CSS {

    /**
     * Minify a CSS string
     *
     * @param string $css
     *
     * @param array $options available options:
     *
     * 'preserveComments': (default true) multi-line comments that begin
     * with "/*!" will be preserved with newlines before and after to
     * enhance readability.
     *
     * 'prependRelativePath': (default null) if given, this string will be
     * prepended to all relative URIs in import/url declarations
     *
     * 'currentDir': (default null) if given, this is assumed to be the
     * directory of the current CSS file. Using this, minify will rewrite
     * all relative URIs in import/url declarations to correctly point to
     * the desired files. For this to work, the files *must* exist and be
     * visible by the PHP process.
     *
     * 'symlinks': (default = array()) If the CSS file is stored in
     * a symlink-ed directory, provide an array of link paths to
     * target paths, where the link paths are within the document root. Because
     * paths need to be normalized for this to work, use "//" to substitute
     * the doc root in the link paths (the array keys). E.g.:
     * <code>
     * array('//symlink' => '/real/target/path') // unix
     * array('//static' => 'D:\\staticStorage')  // Windows
     * </code>
     *
     * @return string
     */
    public static function minify($css, $options = array())
    {
        require_once 'Minify/CSS/Compressor.php';
        if (isset($options['preserveComments'])
            && !$options['preserveComments']) {
            $css = Minify_CSS_Compressor::process($css, $options);
        } else {
            require_once 'Minify/CommentPreserver.php';
            $css = Minify_CommentPreserver::process(
                $css
                ,array('Minify_CSS_Compressor', 'process')
                ,array($options)
            );
        }
        if (! isset($options['currentDir']) && ! isset($options['prependRelativePath'])) {
            return $css;
        }
        require_once 'Minify/CSS/UriRewriter.php';
        if (isset($options['currentDir'])) {
            return Minify_CSS_UriRewriter::rewrite(
                $css
                ,$options['currentDir']
                ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT']
                ,isset($options['symlinks']) ? $options['symlinks'] : array()
            );
        } else {
            return Minify_CSS_UriRewriter::prepend(
                $css
                ,$options['prependRelativePath']
            );
        }
    }
}


code:
<?php
/**
 * Class Minify_CSS_Compressor
 * @package Minify
 */

/**
 * Compress CSS
 *
 * This is a heavy regex-based removal of whitespace, unnecessary
 * comments and tokens, and some CSS value minimization, where practical.
 * Many steps have been taken to avoid breaking comment-based hacks,
 * including the ie5/mac filter (and its inversion), but expect tricky
 * hacks involving comment tokens in 'content' value strings to break
 * minimization badly. A test suite is available.
 *
 * @package Minify
 * @author Stephen Clay <steve@mrclay.org>
 * @author http://code.google.com/u/1stvamp/ (Issue 64 patch)
 */
class Minify_CSS_Compressor {

    /**
     * Minify a CSS string
     *
     * @param string $css
     *
     * @param array $options (currently ignored)
     *
     * @return string
     */
    public static function process($css, $options = array())
    {
        $obj = new Minify_CSS_Compressor($options);
        return $obj->_process($css);
    }

    /**
     * @var array options
     */
    protected $_options = null;

    /**
     * @var bool Are we "in" a hack?
     *
     * I.e. are some browsers targetted until the next comment?
     */
    protected $_inHack = false;

    /**
     * Constructor
     *
     * @param array $options (currently ignored)
     *
     * @return null
     */
    private function __construct($options) {
        $this->_options = $options;
    }

    /**
     * Minify a CSS string
     *
     * @param string $css
     *
     * @return string
     */
    protected function _process($css)
    {
        $css = str_replace("\r\n", "\n", $css);

        // preserve empty comment after '>'
        // http://www.webdevout.net/css-hacks#in_css-selectors
        $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);

        // preserve empty comment between property and value
        // http://css-discuss.incutio.com/?page=BoxModelHack
        $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
        $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);

        // apply callback to all valid comments (and strip out surrounding ws
        $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@'
            ,array($this, '_commentCB'), $css);

        // remove ws around { } and last semicolon in declaration block
        $css = preg_replace('/\\s*{\\s*/', '{', $css);
        $css = preg_replace('/;?\\s*}\\s*/', '}', $css);

        // remove ws surrounding semicolons
        $css = preg_replace('/\\s*;\\s*/', ';', $css);

        // remove ws around urls
        $css = preg_replace('/
                url\\(      # url(
                \\s*
                ([^\\)]+?)  # 1 = the URL (really just a bunch of non right parenthesis)
                \\s*
                \\)         # )
            /x', 'url($1)', $css);

        // remove ws between rules and colons
        $css = preg_replace('/
                \\s*
                ([{;])              # 1 = beginning of block or rule separator
                \\s*
                ([\\*_]?[\\w\\-]+)  # 2 = property (and maybe IE filter)
                \\s*
                :
                \\s*
                (\\b|[#\'"])        # 3 = first character of a value
            /x', '$1$2:$3', $css);

        // remove ws in selectors
        $css = preg_replace_callback('/
                (?:              # non-capture
                    \\s*
                    [^~>+,\\s]+  # selector part
                    \\s*
                    [,>+~]       # combinators
                )+
                \\s*
                [^~>+,\\s]+      # selector part
                {                # open declaration block
            /x'
            ,array($this, '_selectorsCB'), $css);

        // minimize hex colors
        $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i'
            , '$1#$2$3$4$5', $css);

        // remove spaces between font families
        $css = preg_replace_callback('/font-family:([^;}]+)([;}])/'
            ,array($this, '_fontFamilyCB'), $css);

        $css = preg_replace('/@import\\s+url/', '@import url', $css);

        // replace any ws involving newlines with a single newline
        $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);

        // separate common descendent selectors w/ newlines (to limit line lengths)
        $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);

        // Use newline after 1st numeric value (to limit line lengths).
        $css = preg_replace('/
            ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
            \\s+
            /x'
            ,"$1\n", $css);

        // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
        $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);

        return trim($css);
    }

    /**
     * Replace what looks like a set of selectors
     *
     * @param array $m regex matches
     *
     * @return string
     */
    protected function _selectorsCB($m)
    {
        // remove ws around the combinators
        return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
    }

    /**
     * Process a comment and return a replacement
     *
     * @param array $m regex matches
     *
     * @return string
     */
    protected function _commentCB($m)
    {
        $hasSurroundingWs = (trim($m[0]) !== $m[1]);
        $m = $m[1];
        // $m is the comment content w/o the surrounding tokens,
        // but the return value will replace the entire comment.
        if ($m === 'keep') {
            return '/**/';
        }
        if ($m === '" "') {
            // component of http://tantek.com/CSS/Examples/midpass.html
            return '/*" "*/';
        }
        if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
            // component of http://tantek.com/CSS/Examples/midpass.html
            return '/*";}}/* */';
        }
        if ($this->_inHack) {
            // inversion: feeding only to one browser
            if (preg_match('@
                    ^/               # comment started like /*/
                    \\s*
                    (\\S[\\s\\S]+?)  # has at least some non-ws content
                    \\s*
                    /\\*             # ends like /*/ or /**/
                @x', $m, $n)) {
                // end hack mode after this comment, but preserve the hack and comment content
                $this->_inHack = false;
                return "/*/{$n[1]}/**/";
            }
        }
        if (substr($m, -1) === '\\') { // comment ends like \*/
            // begin hack mode and preserve hack
            $this->_inHack = true;
            return '/*\\*/';
        }
        if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
            // begin hack mode and preserve hack
            $this->_inHack = true;
            return '/*/*/';
        }
        if ($this->_inHack) {
            // a regular comment ends hack mode but should be preserved
            $this->_inHack = false;
            return '/**/';
        }
        // Issue 107: if there's any surrounding whitespace, it may be important, so
        // replace the comment with a single space
        return $hasSurroundingWs // remove all other comments
            ? ' '
            : '';
    }

    /**
     * Process a font-family listing and return a replacement
     *
     * @param array $m regex matches
     *
     * @return string
     */
    protected function _fontFamilyCB($m)
    {
        $m[1] = preg_replace('/
                \\s*
                (
                    "[^"]+"      # 1 = family in double qutoes
                    |\'[^\']+\'  # or 1 = family in single quotes
                    |[\\w\\-]+   # or 1 = unquoted family
                )
                \\s*
            /x', '$1', $m[1]);
        return 'font-family:' . $m[1] . $m[2];
    }
}


code:
<?php
/**
 * Class Minify_CSS_UriRewriter
 * @package Minify
 */

/**
 * Rewrite file-relative URIs as root-relative in CSS files
 *
 * @package Minify
 * @author Stephen Clay <steve@mrclay.org>
 */
class Minify_CSS_UriRewriter {

    /**
     * Defines which class to call as part of callbacks, change this
     * if you extend Minify_CSS_UriRewriter
     * @var string
     */
    protected static $className = 'Minify_CSS_UriRewriter';

    /**
     * rewrite() and rewriteRelative() append debugging information here
     * @var string
     */
    public static $debugText = '';

    /**
     * Rewrite file relative URIs as root relative in CSS files
     *
     * @param string $css
     *
     * @param string $currentDir The directory of the current CSS file.
     *
     * @param string $docRoot The document root of the web site in which
     * the CSS file resides (default = $_SERVER['DOCUMENT_ROOT']).
     *
     * @param array $symlinks (default = array()) If the CSS file is stored in
     * a symlink-ed directory, provide an array of link paths to
     * target paths, where the link paths are within the document root. Because
     * paths need to be normalized for this to work, use "//" to substitute
     * the doc root in the link paths (the array keys). E.g.:
     *
     * array('//symlink' => '/real/target/path') // unix
     * array('//static' => 'D:\\staticStorage')  // Windows
     *
     *
     * @return string
     */
    public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = array())
    {
        self::$_docRoot = self::_realpath(
            $docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT']
        );
        self::$_currentDir = self::_realpath($currentDir);
        self::$_symlinks = array();

        // normalize symlinks
        foreach ($symlinks as $link => $target) {
            $link = ($link === '//')
                ? self::$_docRoot
                : str_replace('//', self::$_docRoot . '/', $link);
            $link = strtr($link, '/', DIRECTORY_SEPARATOR);
            self::$_symlinks[$link] = self::_realpath($target);
        }

        self::$debugText .= "docRoot    : " . self::$_docRoot . "\n"
                          . "currentDir : " . self::$_currentDir . "\n";
        if (self::$_symlinks) {
            self::$debugText .= "symlinks : " . var_export(self::$_symlinks, 1) . "\n";
        }
        self::$debugText .= "\n";

        $css = self::_trimUrls($css);

        // rewrite
        $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/'
            ,array(self::$className, '_processUriCB'), $css);
        $css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
            ,array(self::$className, '_processUriCB'), $css);

        return $css;
    }

    /**
     * Prepend a path to relative URIs in CSS files
     *
     * @param string $css
     *
     * @param string $path The path to prepend.
     *
     * @return string
     */
    public static function prepend($css, $path)
    {
        self::$_prependPath = $path;

        $css = self::_trimUrls($css);

        // append
        $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/'
            ,array(self::$className, '_processUriCB'), $css);
        $css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
            ,array(self::$className, '_processUriCB'), $css);

        self::$_prependPath = null;
        return $css;
    }

    /**
     * @var string directory of this stylesheet
     */
    private static $_currentDir = '';

    /**
     * @var string DOC_ROOT
     */
    private static $_docRoot = '';

    /**
     * @var array directory replacements to map symlink targets back to their
     * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath'
     */
    private static $_symlinks = array();

    /**
     * @var string path to prepend
     */
    private static $_prependPath = null;

    private static function _trimUrls($css)
    {
        return preg_replace('/
            url\\(      # url(
            \\s*
            ([^\\)]+?)  # 1 = URI (assuming does not contain ")")
            \\s*
            \\)         # )
        /x', 'url($1)', $css);
    }

    private static function _processUriCB($m)
    {
        // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
        $isImport = ($m[0][0] === '@');
        // determine URI and the quote character (if any)
        if ($isImport) {
            $quoteChar = $m[1];
            $uri = $m[2];
        } else {
            // $m[1] is either quoted or not
            $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"')
                ? $m[1][0]
                : '';
            $uri = ($quoteChar === '')
                ? $m[1]
                : substr($m[1], 1, strlen($m[1]) - 2);
        }
        // analyze URI
        if ('/' !== $uri[0]                  // root-relative
            && false === strpos($uri, '//')  // protocol (non-data)
            && 0 !== strpos($uri, 'data:')   // data protocol
        ) {
            // URI is file-relative: rewrite depending on options
            $uri = (self::$_prependPath !== null)
                ? (self::$_prependPath . $uri)
                : self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks);
        }
        return $isImport
            ? "@import {$quoteChar}{$uri}{$quoteChar}"
            : "url({$quoteChar}{$uri}{$quoteChar})";
    }

    /**
     * Rewrite a file relative URI as root relative
     *
     *
     * Minify_CSS_UriRewriter::rewriteRelative(
     *       '../img/hello.gif'
     *     , '/home/user/www/css'  // path of CSS file
     *     , '/home/user/www'      // doc root
     * );
     * // returns '/img/hello.gif'
     *
     * // example where static files are stored in a symlinked directory
     * Minify_CSS_UriRewriter::rewriteRelative(
     *       'hello.gif'
     *     , '/var/staticFiles/theme'
     *     , '/home/user/www'
     *     , array('/home/user/www/static' => '/var/staticFiles')
     * );
     * // returns '/static/theme/hello.gif'
     *
     *
     * @param string $uri file relative URI
     *
     * @param string $realCurrentDir realpath of the current file's directory.
     *
     * @param string $realDocRoot realpath of the site document root.
     *
     * @param array $symlinks (default = array()) If the file is stored in
     * a symlink-ed directory, provide an array of link paths to
     * real target paths, where the link paths "appear" to be within the document
     * root. E.g.:
     *
     * array('/home/foo/www/not/real/path' => '/real/target/path') // unix
     * array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path')  // Windows
     *
     *
     * @return string
     */
    public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = array())
    {
        // prepend path with current dir separator (OS-independent)
        $path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR)
            . DIRECTORY_SEPARATOR . strtr($uri, '/', DIRECTORY_SEPARATOR);

        self::$debugText .= "file-relative URI  : {$uri}\n"
                          . "path prepended     : {$path}\n";

        // "unresolve" a symlink back to doc root
        foreach ($symlinks as $link => $target) {
            if (0 === strpos($path, $target)) {
                // replace $target with $link
                $path = $link . substr($path, strlen($target));

                self::$debugText .= "symlink unresolved : {$path}\n";

                break;
            }
        }
        // strip doc root
        $path = substr($path, strlen($realDocRoot));

        self::$debugText .= "docroot stripped   : {$path}\n";

        // fix to root-relative URI

        $uri = strtr($path, '/\\', '//');

        // remove /./ and /../ where possible
        $uri = str_replace('/./', '/', $uri);
        // inspired by patch from Oleg Cherniy
        do {
            $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed);
        } while ($changed);

        self::$debugText .= "traversals removed : {$uri}\n\n";

        return $uri;
    }

    /**
     * Get realpath with any trailing slash removed. If realpath() fails,
     * just remove the trailing slash.
     *
     * @param string $path
     *
     * @return mixed path with no trailing slash
     */
    protected static function _realpath($path)
    {
        $realPath = realpath($path);
        if ($realPath !== false) {
            $path = $realPath;
        }
        return rtrim($path, '/\\');
    }
}


code:
<?php
/**
 * Class Minify_Cache_APC
 * @package Minify
 */

/**
 * APC-based cache class for Minify
 *
 * <code>
 * Minify::setCache(new Minify_Cache_APC());
 * </code>
 *
 * @package Minify
 * @author Chris Edwards
 **/
class Minify_Cache_APC {

    /**
     * Create a Minify_Cache_APC object, to be passed to
     * Minify::setCache().
     *
     *
     * @param int $expire seconds until expiration (default = 0
     * meaning the item will not get an expiration date)
     *
     * @return null
     */
    public function __construct($expire = 0)
    {
        $this->_exp = $expire;
    }

    /**
     * Write data to cache.
     *
     * @param string $id cache id
     *
     * @param string $data
     *
     * @return bool success
     */
    public function store($id, $data)
    {
        return apc_store($id, "{$_SERVER['REQUEST_TIME']}|{$data}", $this->_exp);
    }

    /**
     * Get the size of a cache entry
     *
     * @param string $id cache id
     *
     * @return int size in bytes
     */
    public function getSize($id)
    {
        return $this->_fetch($id)
            ? strlen($this->_data)
            : false;
    }

    /**
     * Does a valid cache entry exist?
     *
     * @param string $id cache id
     *
     * @param int $srcMtime mtime of the original source file(s)
     *
     * @return bool exists
     */
    public function isValid($id, $srcMtime)
    {
        return ($this->_fetch($id) && ($this->_lm >= $srcMtime));
    }

    /**
     * Send the cached content to output
     *
     * @param string $id cache id
     */
    public function display($id)
    {
        echo $this->_fetch($id)
            ? $this->_data
            : '';
    }

    /**
     * Fetch the cached content
     *
     * @param string $id cache id
     *
     * @return string
     */
    public function fetch($id)
    {
        return $this->_fetch($id)
            ? $this->_data
            : '';
    }

    private $_exp = null;

    // cache of most recently fetched id
    private $_lm = null;
    private $_data = null;
    private $_id = null;

    /**
     * Fetch data and timestamp from apc, store in instance
     *
     * @param string $id
     *
     * @return bool success
     */
    private function _fetch($id)
    {
        if ($this->_id === $id) {
            return true;
        }
        $ret = apc_fetch($id);
        if (false === $ret) {
            $this->_id = null;
            return false;
        }
        list($this->_lm, $this->_data) = explode('|', $ret, 2);
        $this->_id = $id;
        return true;
    }
}


code:
<?php
/**
 * Class Minify_Cache_File
 * @package Minify
 */

class Minify_Cache_File {

    public function __construct($path = '', $fileLocking = false)
    {
        if (! $path) {
            require_once 'Solar/Dir.php';
            $path = rtrim(Solar_Dir::tmp(), DIRECTORY_SEPARATOR);
        }
        $this->_locking = $fileLocking;
        $this->_path = $path;
    }

    /**
     * Write data to cache.
     *
     * @param string $id cache id (e.g. a filename)
     *
     * @param string $data
     *
     * @return bool success
     */
    public function store($id, $data)
    {
        $flag = $this->_locking
            ? LOCK_EX
            : null;
        if (is_file($this->_path . '/' . $id)) {
            @unlink($this->_path . '/' . $id);
        }
        if (! @file_put_contents($this->_path . '/' . $id, $data, $flag)) {
            return false;
        }
        // write control
        if ($data !== $this->fetch($id)) {
            @unlink($file);
            return false;
        }
        return true;
    }

    /**
     * Get the size of a cache entry
     *
     * @param string $id cache id (e.g. a filename)
     *
     * @return int size in bytes
     */
    public function getSize($id)
    {
        return filesize($this->_path . '/' . $id);
    }

    /**
     * Does a valid cache entry exist?
     *
     * @param string $id cache id (e.g. a filename)
     *
     * @param int $srcMtime mtime of the original source file(s)
     *
     * @return bool exists
     */
    public function isValid($id, $srcMtime)
    {
        $file = $this->_path . '/' . $id;
        return (is_file($file) && (filemtime($file) >= $srcMtime));
    }

    /**
     * Send the cached content to output
     *
     * @param string $id cache id (e.g. a filename)
     */
    public function display($id)
    {
        if ($this->_locking) {
            $fp = fopen($this->_path . '/' . $id, 'rb');
            flock($fp, LOCK_SH);
            fpassthru($fp);
            flock($fp, LOCK_UN);
            fclose($fp);
        } else {
            readfile($this->_path . '/' . $id);
        }
    }

	/**
     * Fetch the cached content
     *
     * @param string $id cache id (e.g. a filename)
     *
     * @return string
     */
    public function fetch($id)
    {
        if ($this->_locking) {
            $fp = fopen($this->_path . '/' . $id, 'rb');
            flock($fp, LOCK_SH);
            $ret = stream_get_contents($fp);
            flock($fp, LOCK_UN);
            fclose($fp);
            return $ret;
        } else {
            return file_get_contents($this->_path . '/' . $id);
        }
    }

    /**
     * Fetch the cache path used
     *
     * @return string
     */
    public function getPath()
    {
        return $this->_path;
    }

    private $_path = null;
    private $_locking = null;
}


code:
<?php
/**
 * Class Minify_Cache_Memcache
 * @package Minify
 */

/**
 * Memcache-based cache class for Minify
 *
 * <code>
 * // fall back to disk caching if memcache can't connect
 * $memcache = new Memcache;
 * if ($memcache->connect('localhost', 11211)) {
 *     Minify::setCache(new Minify_Cache_Memcache($memcache));
 * } else {
 *     Minify::setCache();
 * }
 * </code>
 **/
class Minify_Cache_Memcache {

    /**
     * Create a Minify_Cache_Memcache object, to be passed to
     * Minify::setCache().
     *
     * @param Memcache $memcache already-connected instance
     *
     * @param int $expire seconds until expiration (default = 0
     * meaning the item will not get an expiration date)
     *
     * @return null
     */
    public function __construct($memcache, $expire = 0)
    {
        $this->_mc = $memcache;
        $this->_exp = $expire;
    }

    /**
     * Write data to cache.
     *
     * @param string $id cache id
     *
     * @param string $data
     *
     * @return bool success
     */
    public function store($id, $data)
    {
        return $this->_mc->set($id, "{$_SERVER['REQUEST_TIME']}|{$data}", 0, $this->_exp);
    }

    /**
     * Get the size of a cache entry
     *
     * @param string $id cache id
     *
     * @return int size in bytes
     */
    public function getSize($id)
    {
        return $this->_fetch($id)
            ? strlen($this->_data)
            : false;
    }

    /**
     * Does a valid cache entry exist?
     *
     * @param string $id cache id
     *
     * @param int $srcMtime mtime of the original source file(s)
     *
     * @return bool exists
     */
    public function isValid($id, $srcMtime)
    {
        return ($this->_fetch($id) && ($this->_lm >= $srcMtime));
    }

    /**
     * Send the cached content to output
     *
     * @param string $id cache id
     */
    public function display($id)
    {
        echo $this->_fetch($id)
            ? $this->_data
            : '';
    }

	/**
     * Fetch the cached content
     *
     * @param string $id cache id
     *
     * @return string
     */
    public function fetch($id)
    {
        return $this->_fetch($id)
            ? $this->_data
            : '';
    }

    private $_mc = null;
    private $_exp = null;

    // cache of most recently fetched id
    private $_lm = null;
    private $_data = null;
    private $_id = null;

	/**
     * Fetch data and timestamp from memcache, store in instance
     *
     * @param string $id
     *
     * @return bool success
     */
    private function _fetch($id)
    {
        if ($this->_id === $id) {
            return true;
        }
        $ret = $this->_mc->get($id);
        if (false === $ret) {
            $this->_id = null;
            return false;
        }
        list($this->_lm, $this->_data) = explode('|', $ret, 2);
        $this->_id = $id;
        return true;
    }
}