From c6a2f87641760ce62681f45fbe088a28b9f38a7e Mon Sep 17 00:00:00 2001 From: Steve Clay Date: Thu, 30 Sep 2010 04:31:58 +0000 Subject: [PATCH] Minify_CSS : remove charset at-rules by default & options cleanup Minify_HTML : speed patch (Issue 192) Minify_Controller_MinApp : better error logging (Issue 193) index.php : allow easier custom controller hacking --- min/config.php | 4 +- min/index.php | 12 +- min/lib/Minify/CSS.php | 44 +- min/lib/Minify/Controller/MinApp.php | 29 +- min/lib/Minify/HTML.php | 485 +++++++++--------- min_unit_tests/_test_files/css/styles.css | 2 + .../_test_files/html/before.min.html | 6 +- .../_test_files/html/before2.min.html | 6 +- .../_test_files/importProcessor/output.css | 2 + 9 files changed, 306 insertions(+), 284 deletions(-) diff --git a/min/config.php b/min/config.php index c436c86..8a3a890 100644 --- a/min/config.php +++ b/min/config.php @@ -14,14 +14,12 @@ * * If you want to use a custom error logger, set this to your logger * instance. Your object should have a method log(string $message). - * - * @todo cache system does not have error logging yet. */ $min_errorLogger = false; /** - * To allow debugging, you must set this option to true. + * To allow debug mode output, you must set this option to true. * * Once true, you can send the cookie minDebug to request debug mode output. The * cookie value should match the URIs you'd like to debug. E.g. to debug diff --git a/min/index.php b/min/index.php index 4b295ba..165c8d5 100644 --- a/min/index.php +++ b/min/index.php @@ -54,10 +54,9 @@ if ($min_errorLogger) { require_once 'Minify/Logger.php'; if (true === $min_errorLogger) { require_once 'FirePHP.php'; - Minify_Logger::setLogger(FirePHP::getInstance(true)); - } else { - Minify_Logger::setLogger($min_errorLogger); + $min_errorLogger = FirePHP::getInstance(true); } + Minify_Logger::setLogger($min_errorLogger); } // check for URI versioning @@ -70,7 +69,12 @@ if (isset($_GET['g'])) { } if (isset($_GET['f']) || isset($_GET['g'])) { // serve! - Minify::serve('MinApp', $min_serveOptions); + + if (! isset($min_serveController)) { + require 'Minify/Controller/MinApp.php'; + $min_serveController = new Minify_Controller_MinApp(); + } + Minify::serve($min_serveController, $min_serveOptions); } elseif ($min_enableBuilder) { header('Location: builder/'); diff --git a/min/lib/Minify/CSS.php b/min/lib/Minify/CSS.php index 2220cf2..49882e9 100644 --- a/min/lib/Minify/CSS.php +++ b/min/lib/Minify/CSS.php @@ -26,6 +26,8 @@ class Minify_CSS { * 'preserveComments': (default true) multi-line comments that begin * with "/*!" will be preserved with newlines before and after to * enhance readability. + * + * 'removeCharsets': (default true) remove all @charset at-rules * * 'prependRelativePath': (default null) if given, this string will be * prepended to all relative URIs in import/url declarations @@ -36,23 +38,37 @@ class Minify_CSS { * 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.: - * - * array('//symlink' => '/real/target/path') // unix - * array('//static' => 'D:\\staticStorage') // Windows + * '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 * + * + * 'docRoot': (default = $_SERVER['DOCUMENT_ROOT']) + * see Minify_CSS_UriRewriter::rewrite * * @return string */ public static function minify($css, $options = array()) { + $options = array_merge(array( + 'removeCharsets' => true, + 'preserveComments' => true, + 'currentDir' => null, + 'docRoot' => $_SERVER['DOCUMENT_ROOT'], + 'prependRelativePath' => null, + 'symlinks' => array(), + ), $options); + + if ($options['removeCharsets']) { + $css = preg_replace('/@charset[^;]+;\\s*/', '', $css); + } require_once 'Minify/CSS/Compressor.php'; - if (isset($options['preserveComments']) - && !$options['preserveComments']) { + if (! $options['preserveComments']) { $css = Minify_CSS_Compressor::process($css, $options); } else { require_once 'Minify/CommentPreserver.php'; @@ -62,16 +78,16 @@ class Minify_CSS { ,array($options) ); } - if (! isset($options['currentDir']) && ! isset($options['prependRelativePath'])) { + if (! $options['currentDir'] && ! $options['prependRelativePath']) { return $css; } require_once 'Minify/CSS/UriRewriter.php'; - if (isset($options['currentDir'])) { + if ($options['currentDir']) { return Minify_CSS_UriRewriter::rewrite( $css ,$options['currentDir'] - ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT'] - ,isset($options['symlinks']) ? $options['symlinks'] : array() + ,$options['docRoot'] + ,$options['symlinks'] ); } else { return Minify_CSS_UriRewriter::prepend( diff --git a/min/lib/Minify/Controller/MinApp.php b/min/lib/Minify/Controller/MinApp.php index ec7d8e0..b28eb39 100644 --- a/min/lib/Minify/Controller/MinApp.php +++ b/min/lib/Minify/Controller/MinApp.php @@ -65,11 +65,11 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { if (0 === strpos($file, '//')) { $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1); } - $file = realpath($file); - if ($file && is_file($file)) { - $sources[] = $this->_getFileSource($file, $cOptions); + $realpath = realpath($file); + if ($realpath && is_file($realpath)) { + $sources[] = $this->_getFileSource($realpath, $cOptions); } else { - $this->log("The path \"{$file}\" could not be found (or was not a file)"); + $this->log("The path \"{$file}\" (realpath \"{$realpath}\") could not be found (or was not a file)"); return $options; } } @@ -97,7 +97,7 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { // no "./" || preg_match('/(?:^|[^\\.])\\.\\//', $_GET['f']) ) { - $this->log("GET param 'f' invalid (see MinApp.php line 63)"); + $this->log("GET param 'f' was invalid"); return $options; } $ext = ".{$m[1]}"; @@ -109,7 +109,7 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { } $files = explode(',', $_GET['f']); if ($files != array_unique($files)) { - $this->log("Duplicate files specified"); + $this->log("Duplicate files were specified"); return $options; } if (isset($_GET['b'])) { @@ -120,7 +120,7 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { // valid base $base = "/{$_GET['b']}/"; } else { - $this->log("GET param 'b' invalid (see MinApp.php line 84)"); + $this->log("GET param 'b' was invalid"); return $options; } } else { @@ -134,25 +134,26 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { foreach ($files as $file) { $uri = $base . $file; $path = $_SERVER['DOCUMENT_ROOT'] . $uri; - $file = realpath($path); - if (false === $file || ! is_file($file)) { + $realpath = realpath($path); + if (false === $realpath || ! is_file($realpath)) { + $this->log("The path \"{$path}\" (realpath \"{$realpath}\") could not be found (or was not a file)"); if (! $missingUri) { $missingUri = $uri; continue; } else { - $this->log("At least two files missing: '$missingUri', '$uri'"); + $this->log("More than one file was missing: '$missingUri', '$uri'"); return $options; } } try { - parent::checkNotHidden($file); - parent::checkAllowDirs($file, $allowDirs, $uri); + parent::checkNotHidden($realpath); + parent::checkAllowDirs($realpath, $allowDirs, $uri); } catch (Exception $e) { $this->log($e->getMessage()); return $options; } - $sources[] = $this->_getFileSource($file, $cOptions); - $basenames[] = basename($file, $ext); + $sources[] = $this->_getFileSource($realpath, $cOptions); + $basenames[] = basename($realpath, $ext); } if ($this->selectionId) { $this->selectionId .= '_f='; diff --git a/min/lib/Minify/HTML.php b/min/lib/Minify/HTML.php index fb5c1e9..9bcf0ad 100644 --- a/min/lib/Minify/HTML.php +++ b/min/lib/Minify/HTML.php @@ -1,245 +1,240 @@ - - */ -class Minify_HTML { - - /** - * "Minify" an HTML page - * - * @param string $html - * - * @param array $options - * - * 'cssMinifier' : (optional) callback function to process content of STYLE - * elements. - * - * 'jsMinifier' : (optional) callback function to process content of SCRIPT - * elements. Note: the type attribute is ignored. - * - * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If - * unset, minify will sniff for an XHTML doctype. - * - * @return string - */ - public static function minify($html, $options = array()) { - $min = new Minify_HTML($html, $options); - return $min->process(); - } - - - /** - * Create a minifier object - * - * @param string $html - * - * @param array $options - * - * 'cssMinifier' : (optional) callback function to process content of STYLE - * elements. - * - * 'jsMinifier' : (optional) callback function to process content of SCRIPT - * elements. Note: the type attribute is ignored. - * - * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If - * unset, minify will sniff for an XHTML doctype. - * - * @return null - */ - public function __construct($html, $options = array()) - { - $this->_html = str_replace("\r\n", "\n", trim($html)); - if (isset($options['xhtml'])) { - $this->_isXhtml = (bool)$options['xhtml']; - } - if (isset($options['cssMinifier'])) { - $this->_cssMinifier = $options['cssMinifier']; - } - if (isset($options['jsMinifier'])) { - $this->_jsMinifier = $options['jsMinifier']; - } - } - - - /** - * Minify the markeup given in the constructor - * - * @return string - */ - public function process() - { - if ($this->_isXhtml === null) { - $this->_isXhtml = (false !== strpos($this->_html, '_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']); - $this->_placeholders = array(); - - // replace SCRIPTs (and minify) with placeholders - $this->_html = preg_replace_callback( - '/(\\s*)(]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i' - ,array($this, '_removeScriptCB') - ,$this->_html); - - // replace STYLEs (and minify) with placeholders - $this->_html = preg_replace_callback( - '/\\s*(]*?>)([\\s\\S]*?)<\\/style>\\s*/i' - ,array($this, '_removeStyleCB') - ,$this->_html); - - // remove HTML comments (not containing IE conditional comments). - $this->_html = preg_replace_callback( - '//' - ,array($this, '_commentCB') - ,$this->_html); - - // replace PREs with placeholders - $this->_html = preg_replace_callback('/\\s*(]*?>[\\s\\S]*?<\\/pre>)\\s*/i' - ,array($this, '_removePreCB') - ,$this->_html); - - // replace TEXTAREAs with placeholders - $this->_html = preg_replace_callback( - '/\\s*(]*?>[\\s\\S]*?<\\/textarea>)\\s*/i' - ,array($this, '_removeTextareaCB') - ,$this->_html); - - // trim each line. - // @todo take into account attribute values that span multiple lines. - $this->_html = preg_replace('/^\\s+|\\s+$/m', '', $this->_html); - - // remove ws around block/undisplayed elements - $this->_html = preg_replace('/\\s+(<\\/?(?:area|base(?:font)?|blockquote|body' - .'|caption|center|cite|col(?:group)?|dd|dir|div|dl|dt|fieldset|form' - .'|frame(?:set)?|h[1-6]|head|hr|html|legend|li|link|map|menu|meta' - .'|ol|opt(?:group|ion)|p|param|t(?:able|body|head|d|h||r|foot|itle)' - .'|ul)\\b[^>]*>)/i', '$1', $this->_html); - - // remove ws outside of all elements - $this->_html = preg_replace_callback( - '/>([^<]+)_html); - - // use newlines before 1st attribute in open tags (to limit line lengths) - $this->_html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $this->_html); - - // fill placeholders - $this->_html = str_replace( - array_keys($this->_placeholders) - ,array_values($this->_placeholders) - ,$this->_html - ); - return $this->_html; - } - - protected function _commentCB($m) - { - return (0 === strpos($m[1], '[') || false !== strpos($m[1], '_replacementHash . count($this->_placeholders) . '%'; - $this->_placeholders[$placeholder] = $content; - return $placeholder; - } - - protected $_isXhtml = null; - protected $_replacementHash = null; - protected $_placeholders = array(); - protected $_cssMinifier = null; - protected $_jsMinifier = null; - - protected function _outsideTagCB($m) - { - return '>' . preg_replace('/^\\s+|\\s+$/', ' ', $m[1]) . '<'; - } - - protected function _removePreCB($m) - { - return $this->_reservePlace($m[1]); - } - - protected function _removeTextareaCB($m) - { - return $this->_reservePlace($m[1]); - } - - protected function _removeStyleCB($m) - { - $openStyle = $m[1]; - $css = $m[2]; - // remove HTML comments - $css = preg_replace('/(?:^\\s*\\s*$)/', '', $css); - - // remove CDATA section markers - $css = $this->_removeCdata($css); - - // minify - $minifier = $this->_cssMinifier - ? $this->_cssMinifier - : 'trim'; - $css = call_user_func($minifier, $css); - - return $this->_reservePlace($this->_needsCdata($css) - ? "{$openStyle}/**/" - : "{$openStyle}{$css}" - ); - } - - protected function _removeScriptCB($m) - { - $openScript = $m[2]; - $js = $m[3]; - - // whitespace surrounding? preserve at least one space - $ws1 = ($m[1] === '') ? '' : ' '; - $ws2 = ($m[4] === '') ? '' : ' '; - - // remove HTML comments (and ending "//" if present) - $js = preg_replace('/(?:^\\s*\\s*$)/', '', $js); - - // remove CDATA section markers - $js = $this->_removeCdata($js); - - // minify - $minifier = $this->_jsMinifier - ? $this->_jsMinifier - : 'trim'; - $js = call_user_func($minifier, $js); - - return $this->_reservePlace($this->_needsCdata($js) - ? "{$ws1}{$openScript}/**/{$ws2}" - : "{$ws1}{$openScript}{$js}{$ws2}" - ); - } - - protected function _removeCdata($str) - { - return (false !== strpos($str, ''), '', $str) - : $str; - } - - protected function _needsCdata($str) - { - return ($this->_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str)); - } -} + + */ +class Minify_HTML { + + /** + * "Minify" an HTML page + * + * @param string $html + * + * @param array $options + * + * 'cssMinifier' : (optional) callback function to process content of STYLE + * elements. + * + * 'jsMinifier' : (optional) callback function to process content of SCRIPT + * elements. Note: the type attribute is ignored. + * + * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If + * unset, minify will sniff for an XHTML doctype. + * + * @return string + */ + public static function minify($html, $options = array()) { + $min = new Minify_HTML($html, $options); + return $min->process(); + } + + + /** + * Create a minifier object + * + * @param string $html + * + * @param array $options + * + * 'cssMinifier' : (optional) callback function to process content of STYLE + * elements. + * + * 'jsMinifier' : (optional) callback function to process content of SCRIPT + * elements. Note: the type attribute is ignored. + * + * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If + * unset, minify will sniff for an XHTML doctype. + * + * @return null + */ + public function __construct($html, $options = array()) + { + $this->_html = str_replace("\r\n", "\n", trim($html)); + if (isset($options['xhtml'])) { + $this->_isXhtml = (bool)$options['xhtml']; + } + if (isset($options['cssMinifier'])) { + $this->_cssMinifier = $options['cssMinifier']; + } + if (isset($options['jsMinifier'])) { + $this->_jsMinifier = $options['jsMinifier']; + } + } + + + /** + * Minify the markeup given in the constructor + * + * @return string + */ + public function process() + { + if ($this->_isXhtml === null) { + $this->_isXhtml = (false !== strpos($this->_html, '_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']); + $this->_placeholders = array(); + + // replace SCRIPTs (and minify) with placeholders + $this->_html = preg_replace_callback( + '/(\\s*)]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i' + ,array($this, '_removeScriptCB') + ,$this->_html); + + // replace STYLEs (and minify) with placeholders + $this->_html = preg_replace_callback( + '/\\s*]*>)([\\s\\S]*?)<\\/style>\\s*/i' + ,array($this, '_removeStyleCB') + ,$this->_html); + + // remove HTML comments (not containing IE conditional comments). + $this->_html = preg_replace_callback( + '//' + ,array($this, '_commentCB') + ,$this->_html); + + // replace PREs with placeholders + $this->_html = preg_replace_callback('/\\s*]*?>[\\s\\S]*?<\\/pre>)\\s*/i' + ,array($this, '_removePreCB') + ,$this->_html); + + // replace TEXTAREAs with placeholders + $this->_html = preg_replace_callback( + '/\\s*]*?>[\\s\\S]*?<\\/textarea>)\\s*/i' + ,array($this, '_removeTextareaCB') + ,$this->_html); + + // trim each line. + // @todo take into account attribute values that span multiple lines. + $this->_html = preg_replace('/^\\s+|\\s+$/m', '', $this->_html); + + // remove ws around block/undisplayed elements + $this->_html = preg_replace('/\\s+(<\\/?(?:area|base(?:font)?|blockquote|body' + .'|caption|center|cite|col(?:group)?|dd|dir|div|dl|dt|fieldset|form' + .'|frame(?:set)?|h[1-6]|head|hr|html|legend|li|link|map|menu|meta' + .'|ol|opt(?:group|ion)|p|param|t(?:able|body|head|d|h||r|foot|itle)' + .'|ul)\\b[^>]*>)/i', '$1', $this->_html); + + // remove ws outside of all elements + $this->_html = preg_replace( + '/>(\\s(?:\\s*))?([^<]+)(\\s(?:\s*))?$1$2$3<' + ,$this->_html); + + // use newlines before 1st attribute in open tags (to limit line lengths) + $this->_html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $this->_html); + + // fill placeholders + $this->_html = str_replace( + array_keys($this->_placeholders) + ,array_values($this->_placeholders) + ,$this->_html + ); + return $this->_html; + } + + protected function _commentCB($m) + { + return (0 === strpos($m[1], '[') || false !== strpos($m[1], '_replacementHash . count($this->_placeholders) . '%'; + $this->_placeholders[$placeholder] = $content; + return $placeholder; + } + + protected $_isXhtml = null; + protected $_replacementHash = null; + protected $_placeholders = array(); + protected $_cssMinifier = null; + protected $_jsMinifier = null; + + protected function _removePreCB($m) + { + return $this->_reservePlace("_reservePlace("\\s*$)/', '', $css); + + // remove CDATA section markers + $css = $this->_removeCdata($css); + + // minify + $minifier = $this->_cssMinifier + ? $this->_cssMinifier + : 'trim'; + $css = call_user_func($minifier, $css); + + return $this->_reservePlace($this->_needsCdata($css) + ? "{$openStyle}/**/" + : "{$openStyle}{$css}" + ); + } + + protected function _removeScriptCB($m) + { + $openScript = "\\s*$)/', '', $js); + + // remove CDATA section markers + $js = $this->_removeCdata($js); + + // minify + $minifier = $this->_jsMinifier + ? $this->_jsMinifier + : 'trim'; + $js = call_user_func($minifier, $js); + + return $this->_reservePlace($this->_needsCdata($js) + ? "{$ws1}{$openScript}/**/{$ws2}" + : "{$ws1}{$openScript}{$js}{$ws2}" + ); + } + + protected function _removeCdata($str) + { + return (false !== strpos($str, ''), '', $str) + : $str; + } + + protected function _needsCdata($str) + { + return ($this->_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str)); + } +} diff --git a/min_unit_tests/_test_files/css/styles.css b/min_unit_tests/_test_files/css/styles.css index bf46c0a..0c3a892 100644 --- a/min_unit_tests/_test_files/css/styles.css +++ b/min_unit_tests/_test_files/css/styles.css @@ -1,3 +1,5 @@ +@charset "utf-8"; + /* some CSS to try to exercise things in general */ @import url( /more.css ); diff --git a/min_unit_tests/_test_files/html/before.min.html b/min_unit_tests/_test_files/html/before.min.html index cb56d4f..6b1373f 100644 --- a/min_unit_tests/_test_files/html/before.min.html +++ b/min_unit_tests/_test_files/html/before.min.html @@ -20,10 +20,12 @@ rel="alternate" type="application/rss+xml" title="RSS" href="http://www.csszengarden.com/zengarden.xml" />

Browser != IE

+

Browser != IE

+title="Cascading Style Sheets">CSS
+Design
 	White  space  is  important   here!
 		

Browser != IE

+

Browser != IE

+title="Cascading Style Sheets">CSS
+Design
 	White  space  is  important   here!