1
0
mirror of https://github.com/mrclay/minify.git synced 2025-04-21 04:41:54 +02:00

Perf update for JSmin (Minify_Javascript), + phpDocs for public HTTP_* class APIs, minor Minify fix, private members changed to protected to allow easier subclassing.

This commit is contained in:
Steve Clay 2008-03-03 16:23:39 +00:00
parent 7a3d7129b4
commit e8ac1dc8d0
9 changed files with 304 additions and 161 deletions

View File

@ -32,49 +32,43 @@
*/
class HTTP_ConditionalGet {
private $headers = array();
private $lmTime = null;
private $etag = null;
/**
* Does the client have a valid copy of the requested resource?
*
* You'll want to check this after instantiating the object. If true, do
* not send content, just call sendHeaders() if you haven't already.
*
* @var bool
*/
public $cacheIsValid = null;
public function getHeaders() {
return $this->headers;
}
/**
* Depending on the PHP config, PHP will buffer all output and set
* Content-Length for you. If it doesn't, or you flush() while sending data,
* you'll want to call this to let the client know up front.
* @param array $spec options
*
* 'isPublic': (bool) if true, the Cache-Control header will contain
* "public", allowing proxy caches to cache the content. Otherwise
* "private" will be sent, allowing only browsers to cache. (default false)
*
* 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers
* will be sent with content. This is recommended.
*
* 'contentHash': (string) if given, only the ETag header can be sent with
* content (only HTTP1.1 clients can conditionally GET). The given string
* should be short with no quote characters and always change when the
* resource changes (recommend md5()). This is not needed/used if
* lastModifiedTime is given.
*
* 'invalidate': (bool) if true, the client cache will be considered invalid
* without testing. Effectively this disables conditional GET.
* (default false)
*
* 'setExpires': (mixed) set this to a timestamp or GMT date to send an
* Expires header with the content instead of ETag/Last-Modified. If given,
* Conditional GETs will not be performed, but the public/private
* Cache-Control header will still be sent. (default null)
*
* @return null
*/
public function setContentLength($bytes) {
return $this->headers['Content-Length'] = $bytes;
}
public function sendHeaders() {
$headers = $this->headers;
if (array_key_exists('_responseCode', $headers)) {
header($headers['_responseCode']);
unset($headers['_responseCode']);
}
foreach ($headers as $name => $val) {
header($name . ': ' . $val);
}
}
private function setEtag($hash, $scope) {
$this->etag = '"' . $hash
. substr($scope, 0, 3)
. '"';
$this->headers['ETag'] = $this->etag;
}
private function setLastModified($time) {
$this->lmTime = (int)$time;
$this->headers['Last-Modified'] = self::gmtdate($time);
}
// TODO: allow custom Cache-Control directives, but offer pre-configured
// "modes" for common cache models
public function __construct($spec) {
$scope = (isset($spec['isPublic']) && $spec['isPublic'])
? 'public'
@ -84,7 +78,7 @@ class HTTP_ConditionalGet {
if (is_numeric($spec['setExpires'])) {
$spec['setExpires'] = self::gmtdate($spec['setExpires']);
}
$this->headers = array(
$this->_headers = array(
'Cache-Control' => $scope
,'Expires' => $spec['setExpires']
);
@ -93,41 +87,114 @@ class HTTP_ConditionalGet {
}
if (isset($spec['lastModifiedTime'])) {
// base both headers on time
$this->setLastModified($spec['lastModifiedTime']);
$this->setEtag($spec['lastModifiedTime'], $scope);
$this->_setLastModified($spec['lastModifiedTime']);
$this->_setEtag($spec['lastModifiedTime'], $scope);
} else {
// hope to use ETag
if (isset($spec['contentHash'])) {
$this->setEtag($spec['contentHash'], $scope);
$this->_setEtag($spec['contentHash'], $scope);
}
}
$this->headers['Cache-Control'] = "max-age=0, {$scope}, must-revalidate";
$this->_headers['Cache-Control'] = "max-age=0, {$scope}, must-revalidate";
// invalidate cache if disabled, otherwise check
$this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate'])
? false
: $this->isCacheValid();
: $this->_isCacheValid();
}
/**
* Get array of output headers to be sent
*
* In the case of 304 responses, this array will only contain the response
* code header: array('_responseCode' => 'HTTP/1.0 304 Not Modified')
*
* Otherwise something like:
* <code>
* array(
* 'Cache-Control' => 'max-age=0, public, must-revalidate'
* ,'ETag' => '"foobar"'
* )
* </code>
*
* @return array
*/
public function getHeaders() {
return $this->_headers;
}
/**
* Set the Content-Length header in bytes
*
* With most PHP configs, as long as you don't flush() output, this method
* is not needed and PHP will buffer all output and set Content-Length for
* you. Otherwise you'll want to call this to let the client know up front.
*
* @param int $bytes
*
* @return int copy of input $bytes
*/
public function setContentLength($bytes) {
return $this->_headers['Content-Length'] = $bytes;
}
/**
* Send headers
*
* @see getHeaders()
*
* Note this doesn't "clear" the headers. Calling sendHeaders() will
* call header() again (but probably have not effect) and getHeaders() will
* still return the headers.
*
* @return null
*/
public function sendHeaders() {
$headers = $this->_headers;
if (array_key_exists('_responseCode', $headers)) {
header($headers['_responseCode']);
unset($headers['_responseCode']);
}
foreach ($headers as $name => $val) {
header($name . ': ' . $val);
}
}
protected $_headers = array();
protected $_lmTime = null;
protected $_etag = null;
protected function _setEtag($hash, $scope) {
$this->_etag = '"' . $hash
. substr($scope, 0, 3)
. '"';
$this->_headers['ETag'] = $this->_etag;
}
protected function _setLastModified($time) {
$this->_lmTime = (int)$time;
$this->_headers['Last-Modified'] = self::gmtdate($time);
}
/**
* Determine validity of client cache and queue 304 header if valid
*/
private function isCacheValid()
protected function _isCacheValid()
{
if (null === $this->etag) {
if (null === $this->_etag) {
// ETag was our backup, so we know we don't have lmTime either
return false;
}
$isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified());
if ($isValid) {
// overwrite headers, only need 304
$this->headers = array(
$this->_headers = array(
'_responseCode' => 'HTTP/1.0 304 Not Modified'
);
}
return $isValid;
}
private function resourceMatchedEtag() {
protected function resourceMatchedEtag() {
if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
return false;
}
@ -136,14 +203,14 @@ class HTTP_ConditionalGet {
: $_SERVER['HTTP_IF_NONE_MATCH'];
$cachedEtags = split(',', $cachedEtagList);
foreach ($cachedEtags as $cachedEtag) {
if (trim($cachedEtag) == $this->etag) {
if (trim($cachedEtag) == $this->_etag) {
return true;
}
}
return false;
}
private function resourceNotModified() {
protected function resourceNotModified() {
if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
return false;
}
@ -154,11 +221,10 @@ class HTTP_ConditionalGet {
// IE has tacked on extra data to this header, strip it
$ifModifiedSince = substr($ifModifiedSince, 0, $semicolon);
}
return ($ifModifiedSince == self::gmtdate($this->lmTime));
return ($ifModifiedSince == self::gmtdate($this->_lmTime));
}
private static function gmtdate($ts) {
protected static function gmtdate($ts) {
return gmdate('D, d M Y H:i:s \G\M\T', $ts);
}
}

View File

@ -104,7 +104,7 @@ class HTTP_ConditionalGet_Build {
file_put_contents($file, "{$this->lastModified}|{$nextCheck}");
}
private static function _scan($max, $path, $options)
protected static function _scan($max, $path, $options)
{
$d = dir($path);
while (false !== ($entry = $d->read())) {

View File

@ -23,70 +23,134 @@
*/
class HTTP_Encoder {
/**
* Default compression level for zlib operations
*
* This level is used if encode() is not given a $compressionLevel
*
* @var int
*/
public static $compressionLevel = 6;
private static $clientEncodeMethod = null;
private $content = '';
private $headers = array();
private $encodeMethod = array('', '');
/**
* Get an HTTP Encoder object
*
* @param array $spec options
*
* 'content': (string required) content to be encoded
*
* 'type': (string) if set, the Content-Type header will have this value.
*
* 'method: (string) only set this if you are forcing a particular encoding
* method. If not set, the best method will be chosen by getAcceptedEncoding()
* The available methods are 'gzip', 'deflate', 'compress', and '' (no
* encoding)
*
* @return null
*/
public function __construct($spec) {
if (isset($spec['content'])) {
$this->content = $spec['content'];
}
$this->headers['Content-Length'] = strlen($this->content);
$this->_content = $spec['content'];
$this->_headers['Content-Length'] = (string)strlen($this->_content);
if (isset($spec['type'])) {
$this->headers['Content-Type'] = $spec['type'];
$this->_headers['Content-Type'] = $spec['type'];
}
if (self::$clientEncodeMethod === null) {
self::$clientEncodeMethod = self::getAcceptedEncoding();
if (self::$_clientEncodeMethod === null) {
self::$_clientEncodeMethod = self::getAcceptedEncoding();
}
if (isset($spec['method'])
&& in_array($spec['method'], array('gzip', 'deflate', 'compress', '')))
{
$this->encodeMethod = array($spec['method'], $spec['method']);
$this->_encodeMethod = array($spec['method'], $spec['method']);
} else {
$this->encodeMethod = self::$clientEncodeMethod;
$this->_encodeMethod = self::$_clientEncodeMethod;
}
}
/**
* Get content in current form
*
* Call after encode() for encoded content.
*
* return string
*/
public function getContent() {
return $this->content;
return $this->_content;
}
/**
* Get array of output headers to be sent
*
* E.g.
* <code>
* array(
* 'Content-Length' => '615'
* ,'Content-Encoding' => 'x-gzip'
* ,'Vary' => 'Accept-Encoding'
* )
* </code>
*
* @return array
*/
public function getHeaders() {
return $this->headers;
return $this->_headers;
}
/**
* Send the file and headers (encoded or not)
* Send output headers
*
* You must call this before headers are sent and it probably cannot be
* used in conjunction with zlib output buffering / mod_gzip. Errors are
* not handled purposefully.
*
* @see getHeaders()
*
* @return null
*/
public function sendHeaders() {
foreach ($this->_headers as $name => $val) {
header($name . ': ' . $val);
}
}
/**
* Send output headers and content
*
* A shortcut for sendHeaders() and echo getContent()
*
* You must call this before headers are sent and it probably cannot be
* used in conjunction with zlib output buffering / mod_gzip. Errors are
* not handled purposefully.
*
* @return null
*/
public function sendAll() {
$this->sendHeaders();
echo $this->content;
echo $this->_content;
}
/**
* Send just the headers
* Determine the client's best encoding method from the HTTP Accept-Encoding
* header.
*
* If no Accept-Encoding header is set, or the browser is IE before v6 SP2,
* this will return ('', ''), the "identity" encoding.
*
* A syntax-aware scan is done of the Accept-Encoding, so the method must
* be non 0. The methods are favored in order of gzip, deflate, then
* compress.
*
* Note: this value is cached internally for the entire PHP execution
*
* @return array two values, 1st is the actual encoding method, 2nd is the
* alias of that method to use in the Content-Encoding header (some browsers
* call gzip "x-gzip" etc.)
*/
public function sendHeaders() {
foreach ($this->headers as $name => $val) {
header($name . ': ' . $val);
}
}
// returns array(encoding, encoding to use in Content-Encoding header)
// eg. array('gzip', 'x-gzip')
public static function getAcceptedEncoding() {
if (self::$clientEncodeMethod !== null) {
return self::$clientEncodeMethod;
if (self::$_clientEncodeMethod !== null) {
return self::$_clientEncodeMethod;
}
if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])
|| self::isBuggyIe())
|| self::_isBuggyIe())
{
return array('', '');
}
@ -104,37 +168,55 @@ class HTTP_Encoder {
}
/**
* If conditionsEncode the content
* @return bool success
* Encode (compress) the content
*
* If the encode method is '' (none) or compression level is 0, or the 'zlib'
* extension isn't loaded, we return false.
*
* Then the appropriate gz_* function is called to compress the content. If
* this fails, false is returned.
*
* If successful, the Content-Length header is updated, and Content-Encoding
* and Vary headers are added.
*
* @param int $compressionLevel given to zlib functions. If not given, the
* class default will be used.
*
* @return bool success true if the content was actually compressed
*/
public function encode($compressionLevel = null) {
if (null === $compressionLevel) {
$compressionLevel = self::$compressionLevel;
}
if ('' === $this->encodeMethod[0]
if ('' === $this->_encodeMethod[0]
|| ($compressionLevel == 0)
|| !extension_loaded('zlib'))
{
return false;
}
if ($this->encodeMethod[0] === 'gzip') {
$encoded = gzencode($this->content, $compressionLevel);
} elseif ($this->encodeMethod[0] === 'deflate') {
$encoded = gzdeflate($this->content, $compressionLevel);
if ($this->_encodeMethod[0] === 'gzip') {
$encoded = gzencode($this->_content, $compressionLevel);
} elseif ($this->_encodeMethod[0] === 'deflate') {
$encoded = gzdeflate($this->_content, $compressionLevel);
} else {
$encoded = gzcompress($this->content, $compressionLevel);
$encoded = gzcompress($this->_content, $compressionLevel);
}
if (false === $encoded) {
return false;
}
$this->headers['Content-Length'] = strlen($encoded);
$this->headers['Content-Encoding'] = $this->encodeMethod[1];
$this->headers['Vary'] = 'Accept-Encoding';
$this->content = $encoded;
$this->_headers['Content-Length'] = strlen($encoded);
$this->_headers['Content-Encoding'] = $this->_encodeMethod[1];
$this->_headers['Vary'] = 'Accept-Encoding';
$this->_content = $encoded;
return true;
}
private static function isBuggyIe()
protected static $_clientEncodeMethod = null;
protected $content = '';
protected $headers = array();
protected $encodeMethod = array('', '');
protected static function _isBuggyIe()
{
if (strstr($_SERVER['HTTP_USER_AGENT'], 'Opera')
|| !preg_match('/^Mozilla\/4\.0 \(compatible; MSIE ([0-9]\.[0-9])/i', $_SERVER['HTTP_USER_AGENT'], $m))

View File

@ -146,16 +146,15 @@ class Minify {
$cg = new HTTP_ConditionalGet($cgOptions);
if ($cg->cacheIsValid) {
// client's cache is valid
if (self::$_options['quiet']) {
return array(
'success' => true
,'statusCode' => 304
,'content' => ''
,'headers' => array()
);
} else {
if (! self::$_options['quiet']) {
$cg->sendHeaders();
}
}
return array(
'success' => true
,'statusCode' => 304
,'content' => ''
,'headers' => array()
);
}
// client will need output
$headers = $cg->getHeaders();
@ -216,22 +215,22 @@ class Minify {
/**
* @var mixed null if disk cache is not to be used
*/
private static $_cachePath = null;
protected static $_cachePath = null;
/**
* @var Minify_Controller active controller for current request
*/
private static $_controller = null;
protected static $_controller = null;
/**
* @var array options for current request
*/
private static $_options = null;
protected static $_options = null;
/**
* @var Cache_Lite_File cache obj for current request
*/
private static $_cache = null;
protected static $_cache = null;
@ -247,7 +246,7 @@ class Minify {
*
* @return string minified, encoded content
*/
private static function _fetchContent($encodeMethod)
protected static function _fetchContent($encodeMethod)
{
$cacheId = self::_getCacheId(self::$_controller->sources, self::$_options)
. $encodeMethod;
@ -278,7 +277,7 @@ class Minify {
*
* @return null
*/
private static function _setupCache() {
protected static function _setupCache() {
// until the patch is rolled into PEAR, we'll provide the
// class in our package
require_once dirname(__FILE__) . '/Cache/Lite/File.php';
@ -297,7 +296,7 @@ class Minify {
*
* @return string
*/
private static function _combineMinify() {
protected static function _combineMinify() {
$type = self::$_options['contentType']; // ease readability
// when combining scripts, make sure all statements separated
@ -366,7 +365,7 @@ class Minify {
*
* @return string
*/
private static function _encode($content)
protected static function _encode($content)
{
if (self::$_options['encodeMethod'] === ''
|| ! self::$_options['encodeOutput']) {
@ -389,7 +388,7 @@ class Minify {
*
* @return string
*/
private static function _getCacheId() {
protected static function _getCacheId() {
return md5(serialize(array(
Minify_Source::getDigest(self::$_controller->sources)
,self::$_options['minifiers']

View File

@ -87,17 +87,17 @@ class Minify_CSS {
*
* I.e. are some browsers targetted until the next comment?
*/
private static $_inHack = false;
protected static $_inHack = false;
/**
* @var string string to be prepended to relative URIs
*/
private static $_tempPrepend = '';
protected static $_tempPrepend = '';
/**
* @var string path of this stylesheet for rewriting purposes
*/
private static $_tempCurrentPath = '';
protected static $_tempCurrentPath = '';
/**
* Process what looks like a comment and return a replacement
@ -106,7 +106,7 @@ class Minify_CSS {
*
* @return string
*/
private static function _commentCB($m)
protected static function _commentCB($m)
{
$m = $m[1];
// $m is everything after the opening tokens and before the closing tokens
@ -148,12 +148,12 @@ class Minify_CSS {
*
* @return string
*/
private static function _selectorsCB($m)
protected static function _selectorsCB($m)
{
return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
}
private static function _urlCB($m)
protected static function _urlCB($m)
{
$isImport = (0 === strpos($m[0], '@import'));
if ($isImport) {

View File

@ -54,7 +54,7 @@ class Minify_Controller_Page extends Minify_Controller_Base {
return $options;
}
private $_loadCssJsMinifiers = false;
protected $_loadCssJsMinifiers = false;
/**
* @see Minify_Controller_Base::loadMinifier()

View File

@ -84,21 +84,21 @@ class Minify_HTML {
return $html;
}
private static $_isXhtml = false;
private static $_replacementHash = null;
private static $_pres = array();
private static $_scripts = array();
private static $_styles = array();
private static $_cssMinifier = null;
private static $_jsMinifier = null;
protected static $_isXhtml = false;
protected static $_replacementHash = null;
protected static $_pres = array();
protected static $_scripts = array();
protected static $_styles = array();
protected static $_cssMinifier = null;
protected static $_jsMinifier = null;
private static function _removePreCB($m)
protected static function _removePreCB($m)
{
self::$_pres[] = $m[1];
return self::$_replacementHash . 'PRE' . count(self::$_pres);
}
private static function _removeStyleCB($m)
protected static function _removeStyleCB($m)
{
$openStyle = $m[1];
$css = $m[2];
@ -123,7 +123,7 @@ class Minify_HTML {
return self::$_replacementHash . 'STYLE' . count(self::$_styles);
}
private static function _removeScriptCB($m)
protected static function _removeScriptCB($m)
{
$openScript = $m[1];
$js = $m[2];
@ -147,14 +147,14 @@ class Minify_HTML {
return self::$_replacementHash . 'SCRIPT' . count(self::$_scripts);
}
private static function _removeCdata($str)
protected static function _removeCdata($str)
{
return (false !== strpos($str, '<![CDATA['))
? str_replace(array('<![CDATA[', ']]>'), '', $str)
: $str;
}
private static function _needsCdata($str)
protected static function _needsCdata($str)
{
return (self::$_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str));
}

View File

@ -39,9 +39,9 @@
* @package Minify_Javascript
* @author Ryan Grove <ryan@wonko.com>
* @copyright 2002 Douglas Crockford <douglas@crockford.com> (JSMin.c)
* @copyright 2007 Ryan Grove <ryan@wonko.com> (PHP port)
* @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
* @license http://opensource.org/licenses/mit-license.php MIT License
* @version 1.1.0 (2007-06-01)
* @version 1.1.1 (2008-03-03)
* @link http://code.google.com/p/jsmin-php/
*/
@ -55,7 +55,7 @@ class Minify_Javascript {
private $inputIndex = 0;
private $inputLength = 0;
private $lookAhead = null;
private $output = array();
private $output = '';
// -- Public Static Methods --------------------------------------------------
@ -74,15 +74,15 @@ class Minify_Javascript {
private function action($d) {
switch($d) {
case 1:
$this->output[] = $this->a;
$this->output .= $this->a;
case 2:
$this->a = $this->b;
if ($this->a === "'" || $this->a === '"') {
for (;;) {
$this->output[] = $this->a;
$this->a = $this->get();
$this->output .= $this->a;
$this->a = $this->get();
if ($this->a === $this->b) {
break;
@ -93,8 +93,8 @@ class Minify_Javascript {
}
if ($this->a === '\\') {
$this->output[] = $this->a;
$this->a = $this->get();
$this->output .= $this->a;
$this->a = $this->get();
}
}
}
@ -107,25 +107,23 @@ class Minify_Javascript {
$this->a === ':' || $this->a === '[' || $this->a === '!' ||
$this->a === '&' || $this->a === '|' || $this->a === '?')) {
$this->output[] = $this->a;
$this->output[] = $this->b;
$this->output .= $this->a;
$this->output .= $this->b;
for (;;) {
$this->a = $this->get();
if ($this->a === '/') {
break;
}
elseif ($this->a === '\\') {
$this->output[] = $this->a;
$this->a = $this->get();
}
elseif (ord($this->a) <= self::ORD_LF) {
} elseif ($this->a === '\\') {
$this->output .= $this->a;
$this->a = $this->get();
} elseif (ord($this->a) <= self::ORD_LF) {
throw new Minify_JavascriptException('Unterminated regular expression '.
'literal.');
}
$this->output[] = $this->a;
$this->output .= $this->a;
}
$this->b = $this->next();
@ -141,8 +139,7 @@ class Minify_Javascript {
if ($this->inputIndex < $this->inputLength) {
$c = $this->input[$this->inputIndex];
$this->inputIndex += 1;
}
else {
} else {
$c = null;
}
}
@ -167,8 +164,7 @@ class Minify_Javascript {
case ' ':
if (self::isAlphaNum($this->b)) {
$this->action(1);
}
else {
} else {
$this->action(2);
}
break;
@ -237,7 +233,7 @@ class Minify_Javascript {
}
}
return implode('', $this->output);
return $this->output;
}
private function next() {

View File

@ -129,8 +129,8 @@ class Minify_Source {
return 'text/plain';
}
private $_content = null;
private $_filepath = null;
private $_id = null;
protected $_content = null;
protected $_filepath = null;
protected $_id = null;
}