diff --git a/HISTORY.txt b/HISTORY.txt index 95a46c8..94606ed 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -1,5 +1,21 @@ Minify Release History +Version 2.1.4 + * Option to minify JS with Closure Compiler API w/ JSMin failover + * Cookie/bookmarklet-based debug mode. No HTML editing! + * Allows 1 file to be missing w/o complete failure + * Combine multiple groups and files in single URI + * More useful HTML helpers for writing versioned URIs + * More detailed error logging, including minifier exceptions + * Builder offers more helpful messages/PHP environment warnings + * Bypass minification based on filename pattern. e.g. foo.min.js / foo-min.css + * JSMin won't choke on common Closure compiler syntaxes (i+ ++j) + * Better caching in IE6 + * Cache ids are influenced by group/file names + * Debug mode for Javascript doesn't break on common XPath strings (Prototype 1.6) + * Removed annoying maxFiles limit + * mbstring.func_overload usage is safer + Version 2.1.3 * HTTP fixes * ETag generation now valid (different when gzipped) diff --git a/README.txt b/README.txt index 3899b99..5d2e232 100644 --- a/README.txt +++ b/README.txt @@ -8,6 +8,14 @@ and tell clients to cache the file for a period of time. More info: http://code.google.com/p/minify/ +WORDPRESS USER? + +These WP plugins integrate Minify into WordPress's style and script hooks to +get you set up faster. + http://wordpress.org/extend/plugins/wp-minify/ + http://wordpress.org/extend/plugins/w3-total-cache/ + + UPGRADING See UPGRADING.txt for instructions. @@ -16,12 +24,14 @@ See UPGRADING.txt for instructions. INSTALLATION AND USAGE: 1. Place the /min/ directory as a child of your DOCUMENT_ROOT -directory: i.e. you will have: /home/user/www/public_html/min +directory: i.e. you will have: /home/user/www/min 2. Open http://yourdomain/min/ in a web browser. This will forward you to the Minify URI Builder application, which will help you quickly start using Minify to serve content on your site. +See the User Guide: http://code.google.com/p/minify/wiki/UserGuide + UNIT TESTING: @@ -36,12 +46,6 @@ components with more verbose output.) 3. Remove /min_unit_tests/ from your DOCUMENT_ROOT when you are done. -EXTRAS: - -The min_extras folder contains files for benchmarking using Apache ab on Windows -and a couple single-use tools. DO NOT place this on your production server. - - FILE ENCODINGS Minify *should* work fine with files encoded in UTF-8 or other 8-bit diff --git a/min/.htaccess b/min/.htaccess index 42f13eb..06c1161 100644 --- a/min/.htaccess +++ b/min/.htaccess @@ -1,4 +1,13 @@ RewriteEngine on -RewriteRule ^([a-z]=.*) index.php?$1 [L,NE] - \ No newline at end of file + +# You may need RewriteBase on some servers +#RewriteBase /min + +# rewrite URLs like "/min/f=..." to "/min/?f=..." +RewriteRule ^([bfg]=.*) index.php?$1 [L,NE] + + +# In case AddOutputFilterByType has been added +SetEnv no-gzip + diff --git a/min/README.txt b/min/README.txt index a7cf774..7e6fdb1 100644 --- a/min/README.txt +++ b/min/README.txt @@ -66,14 +66,14 @@ to the /js and /themes/default directories, use: $min_serveOptions['minApp']['allowDirs'] = array('//js', '//themes/default'); -GROUPS: FASTER PERFORMANCE AND BETTER URLS +GROUPS: NICER URLS -For the best performance, edit groupsConfig.php to pre-specify groups of files +For nicer URLs, edit groupsConfig.php to pre-specify groups of files to be combined under preset keys. E.g., here's an example configuration in groupsConfig.php: -return array( - 'js' => array('//js/Class.js', '//js/email.js') +return array( + 'js' => array('//js/Class.js', '//js/email.js') ); This pre-selects the following files to be combined under the key "js": @@ -82,7 +82,7 @@ This pre-selects the following files to be combined under the key "js": You can now serve these files with this simple URL: http://example.com/min/?g=js - + GROUPS: SPECIFYING FILES OUTSIDE THE DOC_ROOT @@ -99,6 +99,14 @@ return array( ); +COMBINE MULTIPLE GROUPS AND FILES IN ONE URL + +E.g.: http://example.com/min/?g=js&f=more/scripts.js + +Separate group keys with commas: + http://example.com/min/?g=baseCss,css1&f=moreStyles.css + + FAR-FUTURE EXPIRES HEADERS Minify can send far-future (one year) Expires headers. To enable this you must @@ -106,15 +114,20 @@ add a number to the querystring (e.g. /min/?g=js&1234 or /min/f=file.js&1234) and alter it whenever a source file is changed. If you have a build process you can use a build/source control revision number. -If you serve files as a group, you can use the utility function Minify_groupUri() -to get a "versioned" Minify URI for use in your HTML. E.g.: +You can alternately use the utility function Minify_getUri() to get a "versioned" +Minify URI for use in your HTML. E.g.: "; +$jsUri = Minify_getUri('js'); // a key in groupsConfig.php +echo ""; + +$cssUri = Minify_getUri(array( + '//css/styles1.css' + ,'//css/styles2.css' +)); // a list of files +echo ""; DEBUG MODE diff --git a/min/builder/_index.js b/min/builder/_index.js index 8e5313a..ca2d497 100644 --- a/min/builder/_index.js +++ b/min/builder/_index.js @@ -1,3 +1,6 @@ +/*! + * Minify URI Builder + */ var MUB = { _uid : 0 ,_minRoot : '/min/?' @@ -197,11 +200,12 @@ var MUB = { * Runs on DOMready */ ,init : function () { + $('#jsDidntLoad').remove(); $('#app').show(); $('#sources').html(''); $('#add button').click(MUB.addButtonClick); // make easier to copy text out of - $('#uriHtml, #groupConfig').click(function () { + $('#uriHtml, #groupConfig, #symlinkOpt').click(function () { this.select(); }).focus(function () { this.select(); @@ -222,10 +226,9 @@ var MUB = { return false; }).attr({title:'Add file +'}); } else { - // copy bookmarklet code into href - var bmUri = location.pathname.replace(/\/[^\/]*$/, '/bm.js').substr(1); + // setup bookmarklet 1 $.ajax({ - url : '../?f=' + bmUri + url : '../?f=' + location.pathname.replace(/\/[^\/]*$/, '/bm.js').substr(1) ,success : function (code) { $('#bm')[0].href = code .replace('%BUILDER_URL%', location.href) @@ -236,7 +239,15 @@ var MUB = { $.browser.msie && $('#getBm p:last').append(' Sorry, not supported in MSIE!'); MUB.addButtonClick(); } + // setup bookmarklet 2 + $.ajax({ + url : '../?f=' + location.pathname.replace(/\/[^\/]*$/, '/bm2.js').substr(1) + ,success : function (code) { + $('#bm2')[0].href = code.replace(/\n/g, ' '); + } + ,dataType : 'text' + }); MUB.checkRewrite(); } }; -window.onload = MUB.init; \ No newline at end of file +$(MUB.init); \ No newline at end of file diff --git a/min/builder/bm2.js b/min/builder/bm2.js new file mode 100644 index 0000000..db92172 --- /dev/null +++ b/min/builder/bm2.js @@ -0,0 +1,15 @@ +javascript:(function(){ + var d = document + ,c = d.cookie + ,m = c.match(/\bminDebug=([^; ]+)/) + ,v = m ? decodeURIComponent(m[1]) : '' + ,p = prompt('Debug Minify URIs on ' + location.hostname + ' which contain:' + + '\n(empty for none, space = OR)', v) + ; + if (p === null) return; + p = p.replace(/^\s+|\s+$/, ''); + v = (p === '') + ? 'minDebug=; expires=Fri, 27 Jul 2001 02:47:11 UTC; path=/' + : 'minDebug=' + encodeURIComponent(p) + '; path=/'; + d.cookie = v; +})(); \ No newline at end of file diff --git a/min/builder/index.php b/min/builder/index.php index 1b20982..deb0fea 100644 --- a/min/builder/index.php +++ b/min/builder/index.php @@ -8,6 +8,20 @@ if (phpversion() < 5) { $encodeOutput = (function_exists('gzdeflate') && !ini_get('zlib.output_compression')); +// recommend $min_symlinks setting for Apache UserDir +$symlinkOption = ''; +if (0 === strpos($_SERVER["SERVER_SOFTWARE"], 'Apache/') + && preg_match('@^/\\~(\\w+)/@', $_SERVER['REQUEST_URI'], $m) +) { + $userDir = DIRECTORY_SEPARATOR . $m[1] . DIRECTORY_SEPARATOR; + if (false !== strpos(__FILE__, $userDir)) { + $sm = array(); + $sm["//~{$m[1]}"] = dirname(dirname(__FILE__)); + $array = str_replace('array (', 'array(', var_export($sm, 1)); + $symlinkOption = "\$min_symlinks = $array;"; + } +} + require dirname(__FILE__) . '/../config.php'; if (! $min_enableBuilder) { @@ -15,13 +29,34 @@ if (! $min_enableBuilder) { exit(); } +$setIncludeSuccess = set_include_path(dirname(__FILE__) . '/../lib' . PATH_SEPARATOR . get_include_path()); +// we do it this way because we want the builder to work after the user corrects +// include_path. (set_include_path returning FALSE is OK). +try { + require_once 'Minify/Cache/File.php'; +} catch (Exception $e) { + if (! $setIncludeSuccess) { + echo "Minify: set_include_path() failed. You may need to set your include_path " + ."outside of PHP code, e.g., in php.ini."; + } else { + echo $e->getMessage(); + } + exit(); +} +require 'Minify.php'; + +$cachePathCode = ''; +if (! isset($min_cachePath) && ! function_exists('sys_get_temp_dir')) { + $detectedTmp = Minify_Cache_File::tmp(); + $cachePathCode = "\$min_cachePath = " . var_export($detectedTmp, 1) . ';'; +} + ob_start(); ?> - - - - Minify URI Builder - - +.topWarning a {color:#fff;} +#jsDidntLoad {display:none;} + + + +
Note: It looks like you're running Minify in a user + directory. You may need the following option in /min/config.php to have URIs + correctly rewritten in CSS output: +
+
+ - -

Note: Please set $min_cachePath -in /min/config.php to improve performance.

+

Uh Oh. Minify was unable to + serve Javascript for this app. To troubleshoot this, + enable FirePHP debugging + and request the Minify URL directly. Hopefully the + FirePHP console will report the cause of the error. +

+ + +

Note: was discovered as a usable temp directory.
To + slightly improve performance you can hardcode this in /min/config.php: +

Note: Your webserver does not seem to @@ -107,20 +160,33 @@ in your list, and move any others to the top of the first file in your list

If you desire, you can use Minify URIs in imports and they will not be touched by Minify. E.g. @import "/min/?g=css2";

+

Debug Mode

+

When /min/config.php has $min_allowDebugFlag = true; + you can get debug output by appending &debug to a Minify URL, or + by sending the cookie minDebug=<match>, where <match> + should be a string in the Minify URIs you'd like to debug. This bookmarklet will allow you to + set this cookie.

+

Minify Debug (right-click, add to bookmarks)

+
-

Need help? Search or post to the Minify discussion list.

-

This app is minified :) view -source

+

Need help? Check the wiki, + or post to the discussion + list.

+

Powered by Minify

- - - + - - + ob_get_contents() - ,'id' => __FILE__ - ,'lastModifiedTime' => max( - // regenerate cache if either of these change - filemtime(__FILE__) - ,filemtime(dirname(__FILE__) . '/../config.php') - ) - ,'minifyAll' => true - ,'encodeOutput' => $encodeOutput -); -ob_end_clean(); - -set_include_path(dirname(__FILE__) . '/../lib' . PATH_SEPARATOR . get_include_path()); - -require 'Minify.php'; - +// setup Minify if (0 === stripos(PHP_OS, 'win')) { Minify::setDocRoot(); // we may be on IIS } -Minify::setCache(isset($min_cachePath) ? $min_cachePath : null); +Minify::setCache( + isset($min_cachePath) ? $min_cachePath : '' + ,$min_cacheFileLocking +); Minify::$uploaderHoursBehind = $min_uploaderHoursBehind; -Minify::serve('Page', $serveOpts); +Minify::serve('Page', array( + 'content' => $content + ,'id' => __FILE__ + ,'lastModifiedTime' => max( + // regenerate cache if any of these change + filemtime(__FILE__) + ,filemtime(dirname(__FILE__) . '/../config.php') + ,filemtime(dirname(__FILE__) . '/../lib/Minify.php') + ) + ,'minifyAll' => true + ,'encodeOutput' => $encodeOutput +)); diff --git a/min/builder/test.php b/min/builder/test.php new file mode 100644 index 0000000..08fb498 --- /dev/null +++ b/min/builder/test.php @@ -0,0 +1,43 @@ +realpath(DOCUMENT_ROOT) failed. You may need " + . "to set \$min_documentRoot manually (hopefully realpath() is not " + . "broken in your environment).

"; + } + if (0 !== strpos(realpath(__FILE__), realpath($_SERVER['DOCUMENT_ROOT']))) { + echo "

DOCUMENT_ROOT doesn't contain this file. You may " + . " need to set \$min_documentRoot manually

"; + } + if (isset($_SERVER['SUBDOMAIN_DOCUMENT_ROOT'])) { + echo "

\$_SERVER['SUBDOMAIN_DOCUMENT_ROOT'] is set. " + . "You may need to set \$min_documentRoot to this in config.php

"; + } + +} + +//*/ \ No newline at end of file diff --git a/min/config.php b/min/config.php index 76199a0..8a3a890 100644 --- a/min/config.php +++ b/min/config.php @@ -1,33 +1,38 @@ array('//js/file1.js', '//js/file2.js'), // 'css' => array('//css/file1.css', '//css/file2.css'), - - // custom source example - /*'js2' => array( - dirname(__FILE__) . '/../min_unit_tests/_test_files/js/before.js', - // do NOT process this file - new Minify_Source(array( - 'filepath' => dirname(__FILE__) . '/../min_unit_tests/_test_files/js/before.js', - 'minifier' => create_function('$a', 'return $a;') - )) - ),//*/ - - /*'js3' => array( - dirname(__FILE__) . '/../min_unit_tests/_test_files/js/before.js', - // do NOT process this file - new Minify_Source(array( - 'filepath' => dirname(__FILE__) . '/../min_unit_tests/_test_files/js/before.js', - 'minifier' => array('Minify_Packer', 'minify') - )) - ),//*/ ); \ No newline at end of file diff --git a/min/index.php b/min/index.php index 51c3525..165c8d5 100644 --- a/min/index.php +++ b/min/index.php @@ -30,19 +30,33 @@ if ($min_documentRoot) { } $min_serveOptions['minifierOptions']['text/css']['symlinks'] = $min_symlinks; +// auto-add targets to allowDirs +foreach ($min_symlinks as $uri => $target) { + $min_serveOptions['minApp']['allowDirs'][] = $target; +} -if ($min_allowDebugFlag && isset($_GET['debug'])) { - $min_serveOptions['debug'] = true; +if ($min_allowDebugFlag) { + if (! empty($_COOKIE['minDebug'])) { + foreach (preg_split('/\\s+/', $_COOKIE['minDebug']) as $debugUri) { + if (false !== strpos($_SERVER['REQUEST_URI'], $debugUri)) { + $min_serveOptions['debug'] = true; + break; + } + } + } + // allow GET to override + if (isset($_GET['debug'])) { + $min_serveOptions['debug'] = true; + } } 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 @@ -55,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/'); @@ -63,4 +82,4 @@ if (isset($_GET['f']) || isset($_GET['g'])) { } else { header("Location: /"); exit(); -} +} \ No newline at end of file diff --git a/min/lib/HTTP/ConditionalGet.php b/min/lib/HTTP/ConditionalGet.php index 823db05..158f73a 100644 --- a/min/lib/HTTP/ConditionalGet.php +++ b/min/lib/HTTP/ConditionalGet.php @@ -75,9 +75,8 @@ class HTTP_ConditionalGet { /** * @param array $spec options * - * 'isPublic': (bool) if true, the Cache-Control header will contain - * "public", allowing proxies to cache the content. Otherwise "private" will - * be sent, allowing only browser caching. (default false) + * 'isPublic': (bool) if false, the Cache-Control header will contain + * "private", allowing only browser caching. (default false) * * 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers * will be sent with content. This is recommended. @@ -150,7 +149,10 @@ class HTTP_ConditionalGet { } elseif (isset($spec['contentHash'])) { // Use the hash as the ETag $this->_setEtag($spec['contentHash'] . $etagAppend, $scope); } - $this->_headers['Cache-Control'] = "max-age={$maxAge}, {$scope}"; + $privacy = ($scope === 'private') + ? ', private' + : ''; + $this->_headers['Cache-Control'] = "max-age={$maxAge}{$privacy}"; // invalidate cache if disabled, otherwise check $this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate']) ? false @@ -209,7 +211,9 @@ class HTTP_ConditionalGet { { $headers = $this->_headers; if (array_key_exists('_responseCode', $headers)) { - header($headers['_responseCode']); + // FastCGI environments require 3rd arg to header() to be set + list(, $code) = explode(' ', $headers['_responseCode'], 3); + header($headers['_responseCode'], true, $code); unset($headers['_responseCode']); } foreach ($headers as $name => $val) { @@ -332,12 +336,9 @@ class HTTP_ConditionalGet { if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { return false; } - $ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE']; - if (false !== ($semicolon = strrpos($ifModifiedSince, ';'))) { - // IE has tacked on extra data to this header, strip it - $ifModifiedSince = substr($ifModifiedSince, 0, $semicolon); - } - if ($ifModifiedSince == self::gmtDate($this->_lmTime)) { + // strip off IE's extra data (semicolon) + list($ifModifiedSince) = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE'], 2); + if (strtotime($ifModifiedSince) >= $this->_lmTime) { // Apache 2.2's behavior. If there was no ETag match, send the // non-encoded version of the ETag value. $this->_headers['ETag'] = $this->normalizeEtag($this->_etag); diff --git a/min/lib/HTTP/Encoder.php b/min/lib/HTTP/Encoder.php index 66c2678..49bedd7 100644 --- a/min/lib/HTTP/Encoder.php +++ b/min/lib/HTTP/Encoder.php @@ -33,11 +33,11 @@ * * * For more control over headers, use getHeaders() and getData() and send your - * own output. - * - * Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate, - * and gzcompress functions for gzip, deflate, and compress-encoding - * respectively. + * own output. + * + * Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate, + * and gzcompress functions for gzip, deflate, and compress-encoding + * respectively. * * @package Minify * @subpackage HTTP @@ -59,7 +59,7 @@ class HTTP_Encoder { * * @var bool */ - public static $encodeToIe6 = false; + public static $encodeToIe6 = true; /** @@ -88,10 +88,15 @@ class HTTP_Encoder { * * @return null */ - public function __construct($spec) + public function __construct($spec) { + $this->_useMbStrlen = (function_exists('mb_strlen') + && (ini_get('mbstring.func_overload') !== '') + && ((int)ini_get('mbstring.func_overload') & 2)); $this->_content = $spec['content']; - $this->_headers['Content-Length'] = (string)strlen($this->_content); + $this->_headers['Content-Length'] = $this->_useMbStrlen + ? (string)mb_strlen($this->_content, '8bit') + : (string)strlen($this->_content); if (isset($spec['type'])) { $this->_headers['Content-Type'] = $spec['type']; } @@ -111,7 +116,7 @@ class HTTP_Encoder { * * return string */ - public function getContent() + public function getContent() { return $this->_content; } @@ -130,7 +135,7 @@ class HTTP_Encoder { * * @return array */ - public function getHeaders() + public function getHeaders() { return $this->_headers; } @@ -146,7 +151,7 @@ class HTTP_Encoder { * * @return null */ - public function sendHeaders() + public function sendHeaders() { foreach ($this->_headers as $name => $val) { header($name . ': ' . $val); @@ -164,7 +169,7 @@ class HTTP_Encoder { * * @return null */ - public function sendAll() + public function sendAll() { $this->sendHeaders(); echo $this->_content; @@ -181,21 +186,21 @@ class HTTP_Encoder { * be non 0. The methods are favored in order of gzip, deflate, then * compress. Deflate is always smallest and generally faster, but is * rarely sent by servers, so client support could be buggier. - * + * * @param bool $allowCompress allow the older compress encoding * - * @param bool $allowDeflate allow the more recent deflate encoding + * @param bool $allowDeflate allow the more recent deflate encoding * * @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 static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true) + public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true) { // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html if (! isset($_SERVER['HTTP_ACCEPT_ENCODING']) - || self::_isBuggyIe()) + || self::isBuggyIe()) { return array('', ''); } @@ -244,16 +249,18 @@ class HTTP_Encoder { * this fails, false is returned. * * The header "Vary: Accept-Encoding" is added. If encoding is successful, - * the Content-Length header is updated, and Content-Encoding is also added. - * + * the Content-Length header is updated, and Content-Encoding is also 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) + public function encode($compressionLevel = null) { - $this->_headers['Vary'] = 'Accept-Encoding'; + if (! self::isBuggyIe()) { + $this->_headers['Vary'] = 'Accept-Encoding'; + } if (null === $compressionLevel) { $compressionLevel = self::$compressionLevel; } @@ -262,9 +269,9 @@ class HTTP_Encoder { || !extension_loaded('zlib')) { return false; - } - if ($this->_encodeMethod[0] === 'deflate') { - $encoded = gzdeflate($this->_content, $compressionLevel); + } + if ($this->_encodeMethod[0] === 'deflate') { + $encoded = gzdeflate($this->_content, $compressionLevel); } elseif ($this->_encodeMethod[0] === 'gzip') { $encoded = gzencode($this->_content, $compressionLevel); } else { @@ -273,7 +280,9 @@ class HTTP_Encoder { if (false === $encoded) { return false; } - $this->_headers['Content-Length'] = strlen($encoded); + $this->_headers['Content-Length'] = $this->_useMbStrlen + ? (string)mb_strlen($encoded, '8bit') + : (string)strlen($encoded); $this->_headers['Content-Encoding'] = $this->_encodeMethod[1]; $this->_content = $encoded; return true; @@ -285,7 +294,7 @@ class HTTP_Encoder { * This is a convenience method for common use of the class * * @param string $content - * + * * @param int $compressionLevel given to zlib functions. If not given, the * class default will be used. * @@ -296,20 +305,18 @@ class HTTP_Encoder { if (null === $compressionLevel) { $compressionLevel = self::$compressionLevel; } - $he = new HTTP_Encoder(array('content' => $content)); - $ret = $he->encode($compressionLevel); + $he = new HTTP_Encoder(array('content' => $content)); + $ret = $he->encode($compressionLevel); $he->sendAll(); return $ret; - } - - protected $_content = ''; - protected $_headers = array(); - protected $_encodeMethod = array('', ''); + } /** - * Is the browser an IE version earlier than 6 SP2? + * Is the browser an IE version earlier than 6 SP2? + * + * @return bool */ - protected static function _isBuggyIe() + public static function isBuggyIe() { $ua = $_SERVER['HTTP_USER_AGENT']; // quick escape for non-IEs @@ -318,9 +325,14 @@ class HTTP_Encoder { return false; } // no regex = faaast - $version = (float)substr($ua, 30); + $version = (float)substr($ua, 30); return self::$encodeToIe6 ? ($version < 6 || ($version == 6 && false === strpos($ua, 'SV1'))) : ($version < 7); } + + protected $_content = ''; + protected $_headers = array(); + protected $_encodeMethod = array('', ''); + protected $_useMbStrlen = false; } diff --git a/min/lib/JSMin.php b/min/lib/JSMin.php index 770e1c6..011eca3 100644 --- a/min/lib/JSMin.php +++ b/min/lib/JSMin.php @@ -1,12 +1,16 @@ + * $minifiedJs = JSMin::minify($js); + * * * This is a direct port of jsmin.c to PHP with a few PHP performance tweaks and * modifications to preserve some comments (see below). Also, rather than using * stdin/stdout, JSMin::minify() accepts a string as input and returns another * string as output. - * + * * Comments containing IE conditional compilation are preserved, as are multi-line * comments that begin with "/*!" (for documentation purposes). In the latter case * newlines are inserted around the comment to enhance readability. @@ -56,7 +60,7 @@ class JSMin { const ACTION_KEEP_A = 1; const ACTION_DELETE_A = 2; const ACTION_DELETE_A_B = 3; - + protected $a = "\n"; protected $b = ''; protected $input = ''; @@ -64,9 +68,9 @@ class JSMin { protected $inputLength = 0; protected $lookAhead = null; protected $output = ''; - + /** - * Minify Javascript + * Minify Javascript. * * @param string $js Javascript to be minified * @return string @@ -76,16 +80,22 @@ class JSMin { $jsmin = new JSMin($js); return $jsmin->min(); } - + /** - * Setup process + * @param string $input */ public function __construct($input) { - $this->input = str_replace("\r\n", "\n", $input); - $this->inputLength = strlen($this->input); + $this->input = $input; + // look out for syntax like "++ +" and "- ++" + $p = '\\+'; + $m = '\\-'; + if (preg_match("/([$p$m])(?:\\1 [$p$m]| (?:$p$p|$m$m))/", $input)) { + // likely pre-minified and would be broken by JSMin + $this->output = $input; + } } - + /** * Perform minification, return result */ @@ -94,8 +104,17 @@ class JSMin { if ($this->output !== '') { // min already run return $this->output; } + + $mbIntEnc = null; + if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + $this->input = str_replace("\r\n", "\n", $this->input); + $this->inputLength = strlen($this->input); + $this->action(self::ACTION_DELETE_A_B); - + while ($this->a !== null) { // determine next command $command = self::ACTION_KEEP_A; // default @@ -106,13 +125,16 @@ class JSMin { } elseif ($this->a === "\n") { if ($this->b === ' ') { $command = self::ACTION_DELETE_A_B; - } elseif (false === strpos('{[(+-', $this->b) - && ! $this->isAlphaNum($this->b)) { + // in case of mbstring.func_overload & 2, must check for null b, + // otherwise mb_strpos will give WARNING + } elseif ($this->b === null + || (false === strpos('{[(+-', $this->b) + && ! $this->isAlphaNum($this->b))) { $command = self::ACTION_DELETE_A; } } elseif (! $this->isAlphaNum($this->a)) { if ($this->b === ' ' - || ($this->b === "\n" + || ($this->b === "\n" && (false === strpos('}])+-"\'', $this->a)))) { $command = self::ACTION_DELETE_A_B; } @@ -120,9 +142,13 @@ class JSMin { $this->action($command); } $this->output = trim($this->output); + + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return $this->output; } - + /** * ACTION_KEEP_A = Output A. Copy B to A. Get the next B. * ACTION_DELETE_A = Copy B to A. Get the next B. @@ -146,7 +172,8 @@ class JSMin { } if (ord($this->a) <= self::ORD_LF) { throw new JSMin_UnterminatedStringException( - 'Unterminated String: ' . var_export($str, true)); + "JSMin: Unterminated String at byte " + . $this->inputIndex . ": {$str}"); } $str .= $this->a; if ($this->a === '\\') { @@ -173,7 +200,8 @@ class JSMin { $pattern .= $this->a; } elseif (ord($this->a) <= self::ORD_LF) { throw new JSMin_UnterminatedRegExpException( - 'Unterminated RegExp: '. var_export($pattern, true)); + "JSMin: Unterminated RegExp at byte " + . $this->inputIndex .": {$pattern}"); } $this->output .= $this->a; } @@ -182,7 +210,7 @@ class JSMin { // end case ACTION_DELETE_A_B } } - + protected function isRegexpLiteral() { if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing @@ -207,7 +235,7 @@ class JSMin { } return false; } - + /** * Get next char. Convert ctrl char to space. */ @@ -231,7 +259,7 @@ class JSMin { } return $c; } - + /** * Get next char. If is ctrl character, translate to a space or newline. */ @@ -240,7 +268,7 @@ class JSMin { $this->lookAhead = $this->get(); return $this->lookAhead; } - + /** * Is $c a letter, digit, underscore, dollar sign, escape, or non-ASCII? */ @@ -248,7 +276,7 @@ class JSMin { { return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126); } - + protected function singleLineComment() { $comment = ''; @@ -264,7 +292,7 @@ class JSMin { } } } - + protected function multipleLineComment() { $this->get(); @@ -276,7 +304,7 @@ class JSMin { $this->get(); // if comment preserved by YUI Compressor if (0 === strpos($comment, '!')) { - return "\n/*" . substr($comment, 1) . "*/\n"; + return "\n/*!" . substr($comment, 1) . "*/\n"; } // if IE conditional comment if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { @@ -285,12 +313,14 @@ class JSMin { return ' '; } } elseif ($get === null) { - throw new JSMin_UnterminatedCommentException('Unterminated Comment: ' . var_export('/*' . $comment, true)); + throw new JSMin_UnterminatedCommentException( + "JSMin: Unterminated comment at byte " + . $this->inputIndex . ": /*{$comment}"); } $comment .= $get; } } - + /** * Get the next character, skipping over comments. * Some comments may be preserved. diff --git a/min/lib/Minify.php b/min/lib/Minify.php index 2c0ca34..d70e32d 100644 --- a/min/lib/Minify.php +++ b/min/lib/Minify.php @@ -29,12 +29,13 @@ require_once 'Minify/Source.php'; */ class Minify { - const VERSION = '2.1.3'; + const VERSION = '2.1.4'; 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'; + const URL_DEBUG = 'http://code.google.com/p/minify/wiki/Debugging'; /** * How many hours behind are the file modification times of uploaded files? @@ -179,9 +180,7 @@ class Minify { if (! $controller->sources) { // invalid request! if (! self::$_options['quiet']) { - header(self::$_options['badRequestHeader']); - echo self::$_options['badRequestHeader']; - return; + self::_errorExit(self::$_options['badRequestHeader'], self::URL_DEBUG); } else { list(,$statusCode) = explode(' ', self::$_options['badRequestHeader']); return array( @@ -202,6 +201,7 @@ class Minify { // determine encoding if (self::$_options['encodeOutput']) { + $sendVary = true; if (self::$_options['encodeMethod'] !== null) { // controller specifically requested this $contentEncoding = self::$_options['encodeMethod']; @@ -212,6 +212,7 @@ class Minify { // '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); + $sendVary = ! HTTP_Encoder::isBuggyIe(); } } else { self::$_options['encodeMethod'] = ''; // identity (no encoding) @@ -226,6 +227,8 @@ class Minify { ); if (self::$_options['maxAge'] > 0) { $cgOptions['maxAge'] = self::$_options['maxAge']; + } elseif (self::$_options['debug']) { + $cgOptions['invalidate'] = true; } $cg = new HTTP_ConditionalGet($cgOptions); if ($cg->cacheIsValid) { @@ -250,7 +253,7 @@ class Minify { if (self::$_options['contentType'] === self::TYPE_CSS && self::$_options['rewriteCssUris']) { reset($controller->sources); - while (list($key, $source) = each($controller->sources)) { + foreach($controller->sources as $key => $source) { if ($source->filepath && !isset($source->minifyOptions['currentDir']) && !isset($source->minifyOptions['prependRelativePath']) @@ -261,12 +264,12 @@ class Minify { } // check server cache - if (null !== self::$_cache) { + if (null !== self::$_cache && ! self::$_options['debug']) { // 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(); + $cacheId = self::_getCacheId(); $fullCacheId = (self::$_options['encodeMethod']) ? $cacheId . '.gz' : $cacheId; @@ -276,7 +279,15 @@ class Minify { $cacheContentLength = self::$_cache->getSize($fullCacheId); } else { // generate & cache content - $content = self::_combineMinify(); + try { + $content = self::_combineMinify(); + } catch (Exception $e) { + self::$_controller->log($e->getMessage()); + if (! self::$_options['quiet']) { + self::_errorExit(self::$_options['errorHeader'], self::URL_DEBUG); + } + throw $e; + } self::$_cache->store($cacheId, $content); if (function_exists('gzencode')) { self::$_cache->store($cacheId . '.gz', gzencode($content, self::$_options['encodeLevel'])); @@ -285,7 +296,15 @@ class Minify { } else { // no cache $cacheIsReady = false; - $content = self::_combineMinify(); + try { + $content = self::_combineMinify(); + } catch (Exception $e) { + self::$_controller->log($e->getMessage()); + if (! self::$_options['quiet']) { + self::_errorExit(self::$_options['errorHeader'], self::URL_DEBUG); + } + throw $e; + } } if (! $cacheIsReady && self::$_options['encodeMethod']) { // still need to encode @@ -295,14 +314,17 @@ class Minify { // add headers $headers['Content-Length'] = $cacheIsReady ? $cacheContentLength - : strlen($content); + : ((function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($content, '8bit') + : 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']) { + if (self::$_options['encodeOutput'] && $sendVary) { $headers['Vary'] = 'Accept-Encoding'; } @@ -369,9 +391,9 @@ class Minify { && 0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/') ) { $_SERVER['DOCUMENT_ROOT'] = rtrim(substr( - $_SERVER['PATH_TRANSLATED'] + $_SERVER['SCRIPT_FILENAME'] ,0 - ,strlen($_SERVER['PATH_TRANSLATED']) - strlen($_SERVER['SCRIPT_NAME']) + ,strlen($_SERVER['SCRIPT_FILENAME']) - strlen($_SERVER['SCRIPT_NAME']) ), '\\'); if ($unsetPathInfo) { unset($_SERVER['PATH_INFO']); @@ -396,6 +418,20 @@ class Minify { */ protected static $_options = null; + protected static function _errorExit($header, $url) + { + $url = htmlspecialchars($url, ENT_QUOTES); + list(,$h1) = explode(' ', $header, 2); + $h1 = htmlspecialchars($h1); + // FastCGI environments require 3rd arg to header() to be set + list(, $code) = explode(' ', $header, 3); + header($header, true, $code); + header('Content-Type: text/html; charset=utf-8'); + echo "

$h1

"; + echo "

Please see $url.

"; + exit(); + } + /** * Set up sources to use Minify_Lines * @@ -440,37 +476,31 @@ class Minify { ? 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) { + // minify each source with its own options and minifier, then combine. + // Here we used to combine all first but this was probably + // bad for PCRE performance, esp. in CSS. + 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 + try { + $pieces[] = call_user_func($minifier, $source->getContent(), $options); + } catch (Exception $e) { + throw new Exception("Exception in " . $source->getId() . + ": " . $e->getMessage()); + } + } else { $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); } + $content = implode($implodeSeparator, $pieces); if ($type === self::TYPE_CSS && false !== strpos($content, '@import')) { $content = self::_handleCssImports($content); @@ -491,17 +521,23 @@ class Minify { * * Any settings that could affect output are taken into consideration * + * @param string $prefix + * * @return string */ - protected static function _getCacheId() + protected static function _getCacheId($prefix = 'minify') { - return md5(serialize(array( + $name = preg_replace('/[^a-zA-Z0-9\\.=_,]/', '', self::$_controller->selectionId); + $name = preg_replace('/\\.+/', '.', $name); + $name = substr($name, 0, 200 - 34 - strlen($prefix)); + $md5 = md5(serialize(array( Minify_Source::getDigest(self::$_controller->sources) ,self::$_options['minifiers'] ,self::$_options['minifierOptions'] ,self::$_options['postprocessor'] ,self::$_options['bubbleCssImports'] ))); + return "{$prefix}_{$name}_{$md5}"; } /** @@ -512,7 +548,7 @@ class Minify { { if (self::$_options['bubbleCssImports']) { // bubble CSS imports - preg_match_all('/@import.*?;/', $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 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/CSS/Compressor.php b/min/lib/Minify/CSS/Compressor.php index a348286..f6fb034 100644 --- a/min/lib/Minify/CSS/Compressor.php +++ b/min/lib/Minify/CSS/Compressor.php @@ -108,7 +108,7 @@ class Minify_CSS_Compressor { \\s* : \\s* - (\\b|[#\'"]) # 3 = first character of a value + (\\b|[#\'"-]) # 3 = first character of a value /x', '$1$2:$3', $css); // remove ws in selectors @@ -236,15 +236,16 @@ class Minify_CSS_Compressor { */ 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]; + // Issue 210: must not eliminate WS between words in unquoted families + $pieces = preg_split('/(\'[^\']+\'|"[^"]+")/', $m[1], null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $out = 'font-family:'; + while (null !== ($piece = array_shift($pieces))) { + if ($piece[0] !== '"' && $piece[0] !== "'") { + $piece = preg_replace('/\\s+/', ' ', $piece); + $piece = preg_replace('/\\s?,\\s?/', ',', $piece); + } + $out .= $piece; + } + return $out . $m[2]; } } diff --git a/min/lib/Minify/CSS/UriRewriter.php b/min/lib/Minify/CSS/UriRewriter.php index 824c6bb..1404371 100644 --- a/min/lib/Minify/CSS/UriRewriter.php +++ b/min/lib/Minify/CSS/UriRewriter.php @@ -12,13 +12,6 @@ */ 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 @@ -26,7 +19,7 @@ class Minify_CSS_UriRewriter { public static $debugText = ''; /** - * Rewrite file relative URIs as root relative in CSS files + * In CSS content, rewrite file relative URIs as root relative * * @param string $css * @@ -83,7 +76,7 @@ class Minify_CSS_UriRewriter { } /** - * Prepend a path to relative URIs in CSS files + * In CSS content, prepend a path to relative URIs * * @param string $css * @@ -107,73 +100,8 @@ class Minify_CSS_UriRewriter { 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 + * Get a root relative URI from a file relative URI * * * Minify_CSS_UriRewriter::rewriteRelative( @@ -219,16 +147,16 @@ class Minify_CSS_UriRewriter { 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 + // "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"; + self::$debugText .= "symlink unresolved : {$path}\n"; - break; - } + break; + } } // strip doc root $path = substr($path, strlen($realDocRoot)); @@ -239,18 +167,35 @@ class Minify_CSS_UriRewriter { $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); + $uri = self::removeDots($uri); self::$debugText .= "traversals removed : {$uri}\n\n"; return $uri; } + + /** + * Remove instances of "./" and "../" where possible from a root-relative URI + * @param string $uri + * @return string + */ + public static function removeDots($uri) + { + $uri = str_replace('/./', '/', $uri); + // inspired by patch from Oleg Cherniy + do { + $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed); + } while ($changed); + return $uri; + } + /** + * 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'; + /** * Get realpath with any trailing slash removed. If realpath() fails, * just remove the trailing slash. @@ -267,4 +212,79 @@ class Minify_CSS_UriRewriter { } return rtrim($path, '/\\'); } + + /** + * @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 + if (self::$_prependPath === null) { + $uri = self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks); + } else { + $uri = self::$_prependPath . $uri; + if ($uri[0] === '/') { + $root = ''; + $rootRelative = $uri; + $uri = $root . self::removeDots($rootRelative); + } elseif (preg_match('@^((https?\:)?//([^/]+))/@', $uri, $m) && (false !== strpos($m[3], '.'))) { + $root = $m[1]; + $rootRelative = substr($uri, strlen($root)); + $uri = $root . self::removeDots($rootRelative); + } + } + } + return $isImport + ? "@import {$quoteChar}{$uri}{$quoteChar}" + : "url({$quoteChar}{$uri}{$quoteChar})"; + } } diff --git a/min/lib/Minify/Cache/APC.php b/min/lib/Minify/Cache/APC.php index ca84d29..24ab046 100644 --- a/min/lib/Minify/Cache/APC.php +++ b/min/lib/Minify/Cache/APC.php @@ -54,9 +54,12 @@ class Minify_Cache_APC { */ public function getSize($id) { - return $this->_fetch($id) - ? strlen($this->_data) - : false; + if (! $this->_fetch($id)) { + return false; + } + return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($this->_data, '8bit') + : strlen($this->_data); } /** diff --git a/min/lib/Minify/Cache/File.php b/min/lib/Minify/Cache/File.php index 8744a7e..a45ba85 100644 --- a/min/lib/Minify/Cache/File.php +++ b/min/lib/Minify/Cache/File.php @@ -9,13 +9,12 @@ class Minify_Cache_File { public function __construct($path = '', $fileLocking = false) { if (! $path) { - require_once 'Solar/Dir.php'; - $path = rtrim(Solar_Dir::tmp(), DIRECTORY_SEPARATOR); - } + $path = self::tmp(); + } $this->_locking = $fileLocking; $this->_path = $path; } - + /** * Write data to cache. * @@ -27,20 +26,23 @@ class Minify_Cache_File { */ 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; + $flag = $this->_locking + ? LOCK_EX + : null; + $file = $this->_path . '/' . $id; + if (is_file($file)) { + @unlink($file); + } + if (! @file_put_contents($file, $data, $flag)) { + $this->_log("Minify_Cache_File: Write failed to '$file'"); + return false; + } + // write control + if ($data !== $this->fetch($id)) { + @unlink($file); + $this->_log("Minify_Cache_File: Post-write read failed for '$file'"); + return false; } - // write control - if ($data !== $this->fetch($id)) { - @unlink($file); - return false; - } return true; } @@ -78,15 +80,15 @@ class Minify_Cache_File { */ 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); - } + 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); + } } /** @@ -98,15 +100,15 @@ class Minify_Cache_File { */ 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); + 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); } } @@ -119,7 +121,79 @@ class Minify_Cache_File { { return $this->_path; } + + /** + * Get a usable temp directory + * + * Adapted from Solar/Dir.php + * @author Paul M. Jones + * @license http://opensource.org/licenses/bsd-license.php BSD + * @link http://solarphp.com/trac/core/browser/trunk/Solar/Dir.php + * + * @return string + */ + public static function tmp() + { + static $tmp = null; + if (! $tmp) { + $tmp = function_exists('sys_get_temp_dir') + ? sys_get_temp_dir() + : self::_tmp(); + $tmp = rtrim($tmp, DIRECTORY_SEPARATOR); + } + return $tmp; + } + + /** + * Returns the OS-specific directory for temporary files + * + * @author Paul M. Jones + * @license http://opensource.org/licenses/bsd-license.php BSD + * @link http://solarphp.com/trac/core/browser/trunk/Solar/Dir.php + * + * @return string + */ + protected static function _tmp() + { + // non-Windows system? + if (strtolower(substr(PHP_OS, 0, 3)) != 'win') { + $tmp = empty($_ENV['TMPDIR']) ? getenv('TMPDIR') : $_ENV['TMPDIR']; + if ($tmp) { + return $tmp; + } else { + return '/tmp'; + } + } + // Windows 'TEMP' + $tmp = empty($_ENV['TEMP']) ? getenv('TEMP') : $_ENV['TEMP']; + if ($tmp) { + return $tmp; + } + // Windows 'TMP' + $tmp = empty($_ENV['TMP']) ? getenv('TMP') : $_ENV['TMP']; + if ($tmp) { + return $tmp; + } + // Windows 'windir' + $tmp = empty($_ENV['windir']) ? getenv('windir') : $_ENV['windir']; + if ($tmp) { + return $tmp; + } + // final fallback for Windows + return getenv('SystemRoot') . '\\temp'; + } + + /** + * Send message to the Minify logger + * @param string $msg + * @return null + */ + protected function _log($msg) + { + require_once 'Minify/Logger.php'; + Minify_Logger::log($msg); + } private $_path = null; - private $_locking = null; + private $_locking = null; } diff --git a/min/lib/Minify/Cache/Memcache.php b/min/lib/Minify/Cache/Memcache.php index 2b81e7a..72bf454 100644 --- a/min/lib/Minify/Cache/Memcache.php +++ b/min/lib/Minify/Cache/Memcache.php @@ -60,9 +60,12 @@ class Minify_Cache_Memcache { */ public function getSize($id) { - return $this->_fetch($id) - ? strlen($this->_data) - : false; + if (! $this->_fetch($id)) { + return false; + } + return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($this->_data, '8bit') + : strlen($this->_data); } /** diff --git a/min/lib/Minify/Cache/ZendPlatform.php b/min/lib/Minify/Cache/ZendPlatform.php new file mode 100644 index 0000000..3130d69 --- /dev/null +++ b/min/lib/Minify/Cache/ZendPlatform.php @@ -0,0 +1,142 @@ + + * Minify::setCache(new Minify_Cache_ZendPlatform()); + * + * + * @package Minify + * @author Patrick van Dissel + */ +class Minify_Cache_ZendPlatform { + + + /** + * Create a Minify_Cache_ZendPlatform 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 output_cache_put($id, "{$_SERVER['REQUEST_TIME']}|{$data}"); + } + + + /** + * 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) + { + $ret = ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + return $ret; + } + + + /** + * 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 ZendPlatform, store in instance + * + * @param string $id + * + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $ret = output_cache_get($id, $this->_exp); + if (false === $ret) { + $this->_id = null; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} diff --git a/min/lib/Minify/CommentPreserver.php b/min/lib/Minify/CommentPreserver.php index f56eb34..7a359bf 100644 --- a/min/lib/Minify/CommentPreserver.php +++ b/min/lib/Minify/CommentPreserver.php @@ -30,8 +30,7 @@ class Minify_CommentPreserver { * Process a string outside of C-style comments that begin with "/*!" * * On each non-empty string outside these comments, the given processor - * function will be called. The first "!" will be removed from the - * preserved comments, and the comments will be surrounded by + * function will be called. The comments will be surrounded by * Minify_CommentPreserver::$preprend and Minify_CommentPreserver::$append. * * @param string $content @@ -65,7 +64,7 @@ class Minify_CommentPreserver { * @param string $in input * * @return array 3 elements are returned. If a YUI comment is found, the - * 2nd element is the comment and the 1st and 2nd are the surrounding + * 2nd element is the comment and the 1st and 3rd are the surrounding * strings. If no comment is found, the entire string is returned as the * 1st element and the other two are false. */ @@ -79,7 +78,7 @@ class Minify_CommentPreserver { } $ret = array( substr($in, 0, $start) - ,self::$prepend . '/*' . substr($in, $start + 3, $end - $start - 1) . self::$append + ,self::$prepend . '/*!' . substr($in, $start + 3, $end - $start - 1) . self::$append ); $endChars = (strlen($in) - $end - 2); $ret[] = (0 === $endChars) diff --git a/min/lib/Minify/Controller/Base.php b/min/lib/Minify/Controller/Base.php index 84889b3..724dd8d 100644 --- a/min/lib/Minify/Controller/Base.php +++ b/min/lib/Minify/Controller/Base.php @@ -52,9 +52,10 @@ abstract class Minify_Controller_Base { ,'quiet' => false // serve() will send headers and output ,'debug' => false - // if you override this, the response code MUST be directly after + // if you override these, the response codes MUST be directly after // the first space. ,'badRequestHeader' => 'HTTP/1.0 400 Bad Request' + ,'errorHeader' => 'HTTP/1.0 500 Internal Server Error' // callback function to see/modify content of all sources ,'postprocessor' => null @@ -117,6 +118,8 @@ abstract class Minify_Controller_Base { * be in subdirectories of these directories. * * @return bool file is safe + * + * @deprecated use checkAllowDirs, checkNotHidden instead */ public static function _fileIsSafe($file, $safeDirs) { @@ -134,7 +137,28 @@ abstract class Minify_Controller_Base { list($revExt) = explode('.', strrev($base)); return in_array(strrev($revExt), array('js', 'css', 'html', 'txt')); } + + public static function checkAllowDirs($file, $allowDirs, $uri) + { + foreach ((array)$allowDirs as $allowDir) { + if (strpos($file, $allowDir) === 0) { + return true; + } + } + throw new Exception("File '$file' is outside \$allowDirs. If the path is" + . " resolved via an alias/symlink, look into the \$min_symlinks option." + . " E.g. \$min_symlinks['/" . dirname($uri) . "'] = '" . dirname($file) . "';"); + } + + public static function checkNotHidden($file) + { + $b = basename($file); + if (0 === strpos($b, '.')) { + throw new Exception("Filename '$b' starts with period (may be hidden)"); + } + } + /** * @var array instances of Minify_Source, which provide content and * any individual minification needs. @@ -143,6 +167,15 @@ abstract class Minify_Controller_Base { */ public $sources = array(); + /** + * The setupSources() method may choose to set this, making it easier to + * recognize a particular set of sources/settings in the cache folder. It + * will be filtered and truncated to make the final cache id <= 250 bytes. + * + * @var string short name to place inside cache id + */ + public $selectionId = ''; + /** * Mix in default controller options with user-given options * @@ -195,7 +228,7 @@ abstract class Minify_Controller_Base { * @param string $msg * @return null */ - protected function log($msg) { + public function log($msg) { require_once 'Minify/Logger.php'; Minify_Logger::log($msg); } diff --git a/min/lib/Minify/Controller/MinApp.php b/min/lib/Minify/Controller/MinApp.php index 9582d29..986ef2c 100644 --- a/min/lib/Minify/Controller/MinApp.php +++ b/min/lib/Minify/Controller/MinApp.php @@ -28,64 +28,86 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { 'allowDirs' => '//' ,'groupsOnly' => false ,'groups' => array() - ,'maxFiles' => 10 + ,'noMinPattern' => '@[-\\.]min\\.(?:js|css)$@i' // matched against basename ) ,(isset($options['minApp']) ? $options['minApp'] : array()) ); unset($options['minApp']); $sources = array(); + $this->selectionId = ''; + $missingUri = ''; + if (isset($_GET['g'])) { - // try groups - if (! isset($cOptions['groups'][$_GET['g']])) { - $this->log("A group configuration for \"{$_GET['g']}\" was not set"); + // add group(s) + $this->selectionId .= 'g=' . $_GET['g']; + $keys = explode(',', $_GET['g']); + if ($keys != array_unique($keys)) { + $this->log("Duplicate group key found."); return $options; } - - $files = $cOptions['groups'][$_GET['g']]; - // if $files is a single object, casting will break it - if (is_object($files)) { - $files = array($files); - } elseif (! is_array($files)) { - $files = (array)$files; - } - foreach ($files as $file) { - if ($file instanceof Minify_Source) { - $sources[] = $file; - continue; - } - if (0 === strpos($file, '//')) { - $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1); - } - $file = realpath($file); - if (is_file($file)) { - $sources[] = new Minify_Source(array( - 'filepath' => $file - )); - } else { - $this->log("The path \"{$file}\" could not be found (or was not a file)"); + foreach (explode(',', $_GET['g']) as $key) { + if (! isset($cOptions['groups'][$key])) { + $this->log("A group configuration for \"{$key}\" was not found"); return $options; } + $files = $cOptions['groups'][$key]; + // if $files is a single object, casting will break it + if (is_object($files)) { + $files = array($files); + } elseif (! is_array($files)) { + $files = (array)$files; + } + foreach ($files as $file) { + if ($file instanceof Minify_Source) { + $sources[] = $file; + continue; + } + if (0 === strpos($file, '//')) { + $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1); + } + $realpath = realpath($file); + if ($realpath && is_file($realpath)) { + $sources[] = $this->_getFileSource($realpath, $cOptions); + } else { + $this->log("The path \"{$file}\" (realpath \"{$realpath}\") could not be found (or was not a file)"); + return $options; + } + } + if ($sources) { + try { + $this->checkType($sources[0]); + } catch (Exception $e) { + $this->log($e->getMessage()); + return $options; + } + } } - } elseif (! $cOptions['groupsOnly'] && isset($_GET['f'])) { + } + if (! $cOptions['groupsOnly'] && isset($_GET['f'])) { // try user files // The following restrictions are to limit the URLs that minify will - // respond to. Ideally there should be only one way to reference a file. + // respond to. if (// verify at least one file, files are single comma separated, // and are all same extension - ! preg_match('/^[^,]+\\.(css|js)(?:,[^,]+\\.\\1)*$/', $_GET['f']) + ! preg_match('/^[^,]+\\.(css|js)(?:,[^,]+\\.\\1)*$/', $_GET['f'], $m) // no "//" || strpos($_GET['f'], '//') !== false // no "\" || strpos($_GET['f'], '\\') !== false - // 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]}"; + try { + $this->checkType($m[1]); + } catch (Exception $e) { + $this->log($e->getMessage()); return $options; } $files = explode(',', $_GET['f']); - if (count($files) > $cOptions['maxFiles'] || $files != array_unique($files)) { - $this->log("Too many or duplicate files specified"); + if ($files != array_unique($files)) { + $this->log("Duplicate files were specified"); return $options; } if (isset($_GET['b'])) { @@ -96,7 +118,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 { @@ -106,27 +128,82 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { foreach ((array)$cOptions['allowDirs'] as $allowDir) { $allowDirs[] = realpath(str_replace('//', $_SERVER['DOCUMENT_ROOT'] . '/', $allowDir)); } + $basenames = array(); // just for cache id foreach ($files as $file) { - $path = $_SERVER['DOCUMENT_ROOT'] . $base . $file; - $file = realpath($path); - if (false === $file) { - $this->log("Path \"{$path}\" failed realpath()"); - return $options; - } elseif (! parent::_fileIsSafe($file, $allowDirs)) { - $this->log("Path \"{$path}\" failed Minify_Controller_Base::_fileIsSafe()"); - return $options; - } else { - $sources[] = new Minify_Source(array( - 'filepath' => $file - )); + $uri = $base . $file; + $path = $_SERVER['DOCUMENT_ROOT'] . $uri; + $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("More than one file was missing: '$missingUri', '$uri'"); + return $options; + } } + try { + parent::checkNotHidden($realpath); + parent::checkAllowDirs($realpath, $allowDirs, $uri); + } catch (Exception $e) { + $this->log($e->getMessage()); + return $options; + } + $sources[] = $this->_getFileSource($realpath, $cOptions); + $basenames[] = basename($realpath, $ext); } + if ($this->selectionId) { + $this->selectionId .= '_f='; + } + $this->selectionId .= implode(',', $basenames) . $ext; } if ($sources) { + if ($missingUri) { + array_unshift($sources, new Minify_Source(array( + 'id' => 'missingFile' + ,'lastModified' => 0 + ,'content' => "/* Minify: missing file '" . ltrim($missingUri, '/') . "' */\n" + ,'minifier' => '' + ))); + } $this->sources = $sources; } else { $this->log("No sources to serve"); } return $options; } + + protected function _getFileSource($file, $cOptions) + { + $spec['filepath'] = $file; + if ($cOptions['noMinPattern'] + && preg_match($cOptions['noMinPattern'], basename($file))) { + $spec['minifier'] = ''; + } + return new Minify_Source($spec); + } + + protected $_type = null; + + /* + * Make sure that only source files of a single type are registered + */ + public function checkType($sourceOrExt) + { + if ($sourceOrExt === 'js') { + $type = Minify::TYPE_JS; + } elseif ($sourceOrExt === 'css') { + $type = Minify::TYPE_CSS; + } elseif ($sourceOrExt->contentType !== null) { + $type = $sourceOrExt->contentType; + } else { + return; + } + if ($this->_type === null) { + $this->_type = $type; + } elseif ($this->_type !== $type) { + throw new Exception('Content-Type mismatch'); + } + } } diff --git a/min/lib/Minify/Controller/Page.php b/min/lib/Minify/Controller/Page.php index fa4599a..de471e1 100644 --- a/min/lib/Minify/Controller/Page.php +++ b/min/lib/Minify/Controller/Page.php @@ -40,14 +40,19 @@ class Minify_Controller_Page extends Minify_Controller_Base { $sourceSpec = array( 'filepath' => $options['file'] ); + $f = $options['file']; } else { // strip controller options $sourceSpec = array( 'content' => $options['content'] ,'id' => $options['id'] ); + $f = $options['id']; unset($options['content'], $options['id']); } + // something like "builder,index.php" or "directory,file.html" + $this->selectionId = strtr(substr($f, 1 + strlen(dirname(dirname($f)))), '/\\', ',,'); + if (isset($options['minifyAll'])) { // this will be the 2nd argument passed to Minify_HTML::minify() $sourceSpec['minifyOptions'] = array( diff --git a/min/lib/Minify/Controller/Version1.php b/min/lib/Minify/Controller/Version1.php index 1861aab..5279d36 100644 --- a/min/lib/Minify/Controller/Version1.php +++ b/min/lib/Minify/Controller/Version1.php @@ -7,7 +7,7 @@ require_once 'Minify/Controller/Base.php'; /** - * Controller class for emulating version 1 of minify.php + * Controller class for emulating version 1 of minify.php (mostly a proof-of-concept) * * * Minify::serve('Version1'); diff --git a/min/lib/Minify/HTML.php b/min/lib/Minify/HTML.php index fb5c1e9..e9453ff 100644 --- a/min/lib/Minify/HTML.php +++ b/min/lib/Minify/HTML.php @@ -1,245 +1,246 @@ - - */ -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 + ); + // issue 229: multi-pass to catch scripts that didn't get replaced in textareas + $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/lib/Minify/HTML/Helper.php b/min/lib/Minify/HTML/Helper.php new file mode 100644 index 0000000..807fdc9 --- /dev/null +++ b/min/lib/Minify/HTML/Helper.php @@ -0,0 +1,193 @@ + + */ +class Minify_HTML_Helper { + public $rewriteWorks = true; + public $minAppUri = '/min'; + public $groupsConfigFile = ''; + + /* + * Get an HTML-escaped Minify URI for a group or set of files + * + * @param mixed $keyOrFiles a group key or array of filepaths/URIs + * @param array $opts options: + * 'farExpires' : (default true) append a modified timestamp for cache revving + * 'debug' : (default false) append debug flag + * 'charset' : (default 'UTF-8') for htmlspecialchars + * 'minAppUri' : (default '/min') URI of min directory + * 'rewriteWorks' : (default true) does mod_rewrite work in min app? + * 'groupsConfigFile' : specify if different + * @return string + */ + public static function getUri($keyOrFiles, $opts = array()) + { + $opts = array_merge(array( // default options + 'farExpires' => true + ,'debug' => false + ,'charset' => 'UTF-8' + ,'minAppUri' => '/min' + ,'rewriteWorks' => true + ,'groupsConfigFile' => '' + ), $opts); + $h = new self; + $h->minAppUri = $opts['minAppUri']; + $h->rewriteWorks = $opts['rewriteWorks']; + $h->groupsConfigFile = $opts['groupsConfigFile']; + if (is_array($keyOrFiles)) { + $h->setFiles($keyOrFiles, $opts['farExpires']); + } else { + $h->setGroup($keyOrFiles, $opts['farExpires']); + } + $uri = $h->getRawUri($opts['farExpires'], $opts['debug']); + return htmlspecialchars($uri, ENT_QUOTES, $opts['charset']); + } + + /* + * Get non-HTML-escaped URI to minify the specified files + */ + public function getRawUri($farExpires = true, $debug = false) + { + $path = rtrim($this->minAppUri, '/') . '/'; + if (! $this->rewriteWorks) { + $path .= '?'; + } + if (null === $this->_groupKey) { + // @todo: implement shortest uri + $path = self::_getShortestUri($this->_filePaths, $path); + } else { + $path .= "g=" . $this->_groupKey; + } + if ($debug) { + $path .= "&debug"; + } elseif ($farExpires && $this->_lastModified) { + $path .= "&" . $this->_lastModified; + } + return $path; + } + + public function setFiles($files, $checkLastModified = true) + { + $this->_groupKey = null; + if ($checkLastModified) { + $this->_lastModified = self::getLastModified($files); + } + // normalize paths like in /min/f= + foreach ($files as $k => $file) { + if (0 === strpos($file, '//')) { + $file = substr($file, 2); + } elseif (0 === strpos($file, '/') + || 1 === strpos($file, ':\\')) { + $file = substr($file, strlen($_SERVER['DOCUMENT_ROOT']) + 1); + } + $file = strtr($file, '\\', '/'); + $files[$k] = $file; + } + $this->_filePaths = $files; + } + + public function setGroup($key, $checkLastModified = true) + { + $this->_groupKey = $key; + if ($checkLastModified) { + if (! $this->groupsConfigFile) { + $this->groupsConfigFile = dirname(dirname(dirname(dirname(__FILE__)))) . '/groupsConfig.php'; + } + if (is_file($this->groupsConfigFile)) { + $gc = (require $this->groupsConfigFile); + if (isset($gc[$key])) { + $this->_lastModified = self::getLastModified($gc[$key]); + } + } + } + } + + public static function getLastModified($sources, $lastModified = 0) + { + $max = $lastModified; + foreach ((array)$sources as $source) { + if (is_object($source) && isset($source->lastModified)) { + $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)); + } + } + } + return $max; + } + + protected $_groupKey = null; // if present, URI will be like g=... + protected $_filePaths = array(); + protected $_lastModified = null; + + + /** + * In a given array of strings, find the character they all have at + * a particular index + * + * @param array $arr array of strings + * @param int $pos index to check + * @return mixed a common char or '' if any do not match + */ + protected static function _getCommonCharAtPos($arr, $pos) { + $l = count($arr); + $c = $arr[0][$pos]; + if ($c === '' || $l === 1) + return $c; + for ($i = 1; $i < $l; ++$i) + if ($arr[$i][$pos] !== $c) + return ''; + return $c; + } + + /** + * Get the shortest URI to minify the set of source files + * + * @param array $paths root-relative URIs of files + * @param string $minRoot root-relative URI of the "min" application + */ + protected static function _getShortestUri($paths, $minRoot = '/min/') { + $pos = 0; + $base = ''; + $c; + while (true) { + $c = self::_getCommonCharAtPos($paths, $pos); + if ($c === '') { + break; + } else { + $base .= $c; + } + ++$pos; + } + $base = preg_replace('@[^/]+$@', '', $base); + $uri = $minRoot . 'f=' . implode(',', $paths); + + if (substr($base, -1) === '/') { + // we have a base dir! + $basedPaths = $paths; + $l = count($paths); + for ($i = 0; $i < $l; ++$i) { + $basedPaths[$i] = substr($paths[$i], strlen($base)); + } + $base = substr($base, 0, strlen($base) - 1); + $bUri = $minRoot . 'b=' . $base . '&f=' . implode(',', $basedPaths); + + $uri = strlen($uri) < strlen($bUri) + ? $uri + : $bUri; + } + return $uri; + } +} diff --git a/min/lib/Minify/JS/ClosureCompiler.php b/min/lib/Minify/JS/ClosureCompiler.php new file mode 100644 index 0000000..66cc6db --- /dev/null +++ b/min/lib/Minify/JS/ClosureCompiler.php @@ -0,0 +1,116 @@ + + * + * @todo can use a stream wrapper to unit test this? + */ +class Minify_JS_ClosureCompiler { + const URL = 'http://closure-compiler.appspot.com/compile'; + + /** + * Minify Javascript code via HTTP request to the Closure Compiler API + * + * @param string $js input code + * @param array $options unused at this point + * @return string + */ + public static function minify($js, array $options = array()) + { + $obj = new self($options); + return $obj->min($js); + } + + /** + * + * @param array $options + * + * fallbackFunc : default array($this, 'fallback'); + */ + public function __construct(array $options = array()) + { + $this->_fallbackFunc = isset($options['fallbackMinifier']) + ? $options['fallbackMinifier'] + : array($this, '_fallback'); + } + + public function min($js) + { + $postBody = $this->_buildPostBody($js); + $bytes = (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($postBody, '8bit') + : strlen($postBody); + if ($bytes > 200000) { + throw new Minify_JS_ClosureCompiler_Exception( + 'POST content larger than 200000 bytes' + ); + } + $response = $this->_getResponse($postBody); + if (preg_match('/^Error\(\d\d?\):/', $response)) { + if (is_callable($this->_fallbackFunc)) { + $response = "/* Received errors from Closure Compiler API:\n$response" + . "\n(Using fallback minifier)\n*/\n"; + $response .= call_user_func($this->_fallbackFunc, $js); + } else { + throw new Minify_JS_ClosureCompiler_Exception($response); + } + } + if ($response === '') { + $errors = $this->_getResponse($this->_buildPostBody($js, true)); + throw new Minify_JS_ClosureCompiler_Exception($errors); + } + return $response; + } + + protected $_fallbackFunc = null; + + protected function _getResponse($postBody) + { + $contents = file_get_contents(self::URL, false, stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => $postBody, + 'max_redirects' => 0, + 'timeout' => 15, + ) + ))); + if (false === $contents) { + throw new Minify_JS_ClosureCompiler_Exception( + "No HTTP response from server" + ); + } + return trim($contents); + } + + protected function _buildPostBody($js, $returnErrors = false) + { + return http_build_query(array( + 'js_code' => $js, + 'output_info' => ($returnErrors ? 'errors' : 'compiled_code'), + 'output_format' => 'text', + 'compilation_level' => 'SIMPLE_OPTIMIZATIONS' + ), null, '&'); + } + + /** + * Default fallback function if CC API fails + * @param string $js + * @return string + */ + protected function _fallback($js) + { + require_once 'JSMin.php'; + return JSMin::minify($js); + } +} + +class Minify_JS_ClosureCompiler_Exception extends Exception {} diff --git a/min/lib/Minify/Lines.php b/min/lib/Minify/Lines.php index 6f94fb6..ca8afa1 100644 --- a/min/lib/Minify/Lines.php +++ b/min/lib/Minify/Lines.php @@ -26,9 +26,9 @@ class Minify_Lines { * * 'id': (optional) string to identify file. E.g. file name/path * - * '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 + * '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, and prepend a comment with debugging information about * this process. * @@ -40,10 +40,16 @@ class Minify_Lines { ? $options['id'] : ''; $content = str_replace("\r\n", "\n", $content); + + // Hackily rewrite strings with XPath expressions that are + // likely to throw off our dumb parser (for Prototype 1.6.1). + $content = str_replace('"/*"', '"/"+"*"', $content); + $content = preg_replace('@([\'"])(\\.?//?)\\*@', '$1$2$1+$1*', $content); + $lines = explode("\n", $content); $numLines = count($lines); // determine left padding - $padTo = strlen($numLines); + $padTo = strlen((string) $numLines); // e.g. 103 lines = 3 digits $inComment = false; $i = 0; $newLines = array(); @@ -64,7 +70,7 @@ class Minify_Lines { $content = Minify_CSS_UriRewriter::rewrite( $content ,$options['currentDir'] - ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT'] + ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT'] ,isset($options['symlinks']) ? $options['symlinks'] : array() ); $content = "/* Minify_CSS_UriRewriter::\$debugText\n\n" @@ -82,7 +88,7 @@ class Minify_Lines { * * @param bool $inComment was the parser in a comment at the * beginning of the line? - * + * * @return bool */ private static function _eolInComment($line, $inComment) diff --git a/min/lib/Minify/Logger.php b/min/lib/Minify/Logger.php index 7844eea..8eb72f4 100644 --- a/min/lib/Minify/Logger.php +++ b/min/lib/Minify/Logger.php @@ -9,6 +9,8 @@ * * @package Minify * @author Stephen Clay + * + * @todo lose this singleton! pass log object in Minify::serve and distribute to others */ class Minify_Logger { diff --git a/min/lib/Minify/YUI/CssCompressor.java b/min/lib/Minify/YUI/CssCompressor.java new file mode 100644 index 0000000..ed699b8 --- /dev/null +++ b/min/lib/Minify/YUI/CssCompressor.java @@ -0,0 +1,291 @@ +/* + * YUI Compressor + * Author: Julien Lecomte - http://www.julienlecomte.net/ + * Author: Isaac Schlueter - http://foohack.com/ + * Author: Stoyan Stefanov - http://phpied.com/ + * Copyright (c) 2009 Yahoo! Inc. All rights reserved. + * The copyrights embodied in the content of this file are licensed + * by Yahoo! Inc. under the BSD (revised) open source license. + */ + +package com.yahoo.platform.yui.compressor; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import java.util.ArrayList; + +public class CssCompressor { + + private StringBuffer srcsb = new StringBuffer(); + + public CssCompressor(Reader in) throws IOException { + // Read the stream... + int c; + while ((c = in.read()) != -1) { + srcsb.append((char) c); + } + } + + public void compress(Writer out, int linebreakpos) + throws IOException { + + Pattern p; + Matcher m; + String css = srcsb.toString(); + StringBuffer sb = new StringBuffer(css); + + int startIndex = 0; + int endIndex = 0; + int i = 0; + int max = 0; + ArrayList preservedTokens = new ArrayList(0); + ArrayList comments = new ArrayList(0); + String token; + int totallen = css.length(); + String placeholder; + + + // collect all comment blocks... + while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) { + endIndex = sb.indexOf("*/", startIndex + 2); + if (endIndex < 0) { + endIndex = totallen; + } + + token = sb.substring(startIndex + 2, endIndex); + comments.add(token); + sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___"); + startIndex += 2; + } + css = sb.toString(); + + // preserve strings so their content doesn't get accidentally minified + sb = new StringBuffer(); + p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')"); + m = p.matcher(css); + while (m.find()) { + token = m.group(); + char quote = token.charAt(0); + token = token.substring(1, token.length() - 1); + + // maybe the string contains a comment-like substring? + // one, maybe more? put'em back then + if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { + for (i = 0, max = comments.size(); i < max; i += 1) { + token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments.get(i).toString()); + } + } + + // minify alpha opacity in filter strings + token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); + + preservedTokens.add(token); + String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote; + m.appendReplacement(sb, preserver); + } + m.appendTail(sb); + css = sb.toString(); + + + // strings are safe, now wrestle the comments + for (i = 0, max = comments.size(); i < max; i += 1) { + + token = comments.get(i).toString(); + placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; + + // ! in the first position of the comment means preserve + // so push to the preserved tokens while stripping the ! + if (token.startsWith("!")) { + preservedTokens.add(token); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + continue; + } + + // \ in the last position looks like hack for Mac/IE5 + // shorten that to /*\*/ and the next one to /**/ + if (token.endsWith("\\")) { + preservedTokens.add("\\"); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + i = i + 1; // attn: advancing the loop + preservedTokens.add(""); + css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + continue; + } + + // keep empty comments after child selectors (IE7 hack) + // e.g. html >/**/ body + if (token.length() == 0) { + startIndex = css.indexOf(placeholder); + if (startIndex > 2) { + if (css.charAt(startIndex - 3) == '>') { + preservedTokens.add(""); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + } + } + } + + // in all other cases kill the comment + css = css.replace("/*" + placeholder + "*/", ""); + } + + + // Normalize all whitespace strings to single spaces. Easier to work with that way. + css = css.replaceAll("\\s+", " "); + + // Remove the spaces before the things that should not have spaces before them. + // But, be careful not to turn "p :link {...}" into "p:link{...}" + // Swap out any pseudo-class colons with the token, and then swap back. + sb = new StringBuffer(); + p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)"); + m = p.matcher(css); + while (m.find()) { + String s = m.group(); + s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); + s = s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\\$", "\\\\\\$" ); + m.appendReplacement(sb, s); + } + m.appendTail(sb); + css = sb.toString(); + // Remove spaces before the things that should not have spaces before them. + css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1"); + // bring back the colon + css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":"); + + // retain space for special IE6 cases + css = css.replaceAll(":first\\-(line|letter)(\\{|,)", ":first-$1 $2"); + + // no space after the end of a preserved comment + css = css.replaceAll("\\*/ ", "*/"); + + // If there is a @charset, then only allow one, and push to the top of the file. + css = css.replaceAll("^(.*)(@charset \"[^\"]*\";)", "$2$1"); + css = css.replaceAll("^(\\s*@charset [^;]+;\\s*)+", "$1"); + + // Put the space back in some cases, to support stuff like + // @media screen and (-webkit-min-device-pixel-ratio:0){ + css = css.replaceAll("\\band\\(", "and ("); + + // Remove the spaces after the things that should not have spaces after them. + css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1"); + + // remove unnecessary semicolons + css = css.replaceAll(";+}", "}"); + + // Replace 0(px,em,%) with 0. + css = css.replaceAll("([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", "$1$2"); + + // Replace 0 0 0 0; with 0. + css = css.replaceAll(":0 0 0 0(;|})", ":0$1"); + css = css.replaceAll(":0 0 0(;|})", ":0$1"); + css = css.replaceAll(":0 0(;|})", ":0$1"); + + + // Replace background-position:0; with background-position:0 0; + // same for transform-origin + sb = new StringBuffer(); + p = Pattern.compile("(?i)(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})"); + m = p.matcher(css); + while (m.find()) { + m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2)); + } + m.appendTail(sb); + css = sb.toString(); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2"); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)"); + m = p.matcher(css); + sb = new StringBuffer(); + while (m.find()) { + String[] rgbcolors = m.group(1).split(","); + StringBuffer hexcolor = new StringBuffer("#"); + for (i = 0; i < rgbcolors.length; i++) { + int val = Integer.parseInt(rgbcolors[i]); + if (val < 16) { + hexcolor.append("0"); + } + hexcolor.append(Integer.toHexString(val)); + } + m.appendReplacement(sb, hexcolor.toString()); + } + m.appendTail(sb); + css = sb.toString(); + + // Shorten colors from #AABBCC to #ABC. Note that we want to make sure + // the color is not preceded by either ", " or =. Indeed, the property + // filter: chroma(color="#FFFFFF"); + // would become + // filter: chroma(color="#FFF"); + // which makes the filter break in IE. + p = Pattern.compile("([^\"'=\\s])(\\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])"); + m = p.matcher(css); + sb = new StringBuffer(); + while (m.find()) { + // Test for AABBCC pattern + if (m.group(3).equalsIgnoreCase(m.group(4)) && + m.group(5).equalsIgnoreCase(m.group(6)) && + m.group(7).equalsIgnoreCase(m.group(8))) { + m.appendReplacement(sb, (m.group(1) + m.group(2) + "#" + m.group(3) + m.group(5) + m.group(7)).toLowerCase()); + } else { + m.appendReplacement(sb, m.group().toLowerCase()); + } + } + m.appendTail(sb); + css = sb.toString(); + + // border: none -> border:0 + sb = new StringBuffer(); + p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|})"); + m = p.matcher(css); + while (m.find()) { + m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2)); + } + m.appendTail(sb); + css = sb.toString(); + + // shorter opacity IE filter + css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); + + // Remove empty rules. + css = css.replaceAll("[^\\}\\{/;]+\\{\\}", ""); + + if (linebreakpos >= 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + i = 0; + int linestartpos = 0; + sb = new StringBuffer(css); + while (i < sb.length()) { + char c = sb.charAt(i++); + if (c == '}' && i - linestartpos > linebreakpos) { + sb.insert(i, '\n'); + linestartpos = i; + } + } + + css = sb.toString(); + } + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + css = css.replaceAll(";;+", ";"); + + // restore preserved comments and strings + for(i = 0, max = preservedTokens.size(); i < max; i++) { + css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString()); + } + + // Trim the final string (for any leading or trailing white spaces) + css = css.trim(); + + // Write the output... + out.write(css); + } +} \ No newline at end of file diff --git a/min/lib/Minify/YUI/CssCompressor.php b/min/lib/Minify/YUI/CssCompressor.php new file mode 100644 index 0000000..5ae2185 --- /dev/null +++ b/min/lib/Minify/YUI/CssCompressor.php @@ -0,0 +1,171 @@ ++\\(\\)\\],])@", "$1", $css); + $css = str_replace("___PSEUDOCLASSCOLON___", ":", $css); + + // Remove the spaces after the things that should not have spaces after them. + $css = preg_replace("@([!{}:;>+\\(\\[,])\\s+@", "$1", $css); + + // Add the semicolon where it's missing. + $css = preg_replace("@([^;\\}])}@", "$1;}", $css); + + // Replace 0(px,em,%) with 0. + $css = preg_replace("@([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)@", "$1$2", $css); + + // Replace 0 0 0 0; with 0. + $css = str_replace(":0 0 0 0;", ":0;", $css); + $css = str_replace(":0 0 0;", ":0;", $css); + $css = str_replace(":0 0;", ":0;", $css); + + // Replace background-position:0; with background-position:0 0; + $css = str_replace("background-position:0;", "background-position:0 0;", $css); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + $css = preg_replace("@(:|\\s)0+\\.(\\d+)@", "$1.$2", $css); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + $css = preg_replace_callback("@rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)@", array($this, '_shortenRgbCB'), $css); + + // Shorten colors from #AABBCC to #ABC. Note that we want to make sure + // the color is not preceded by either ", " or =. Indeed, the property + // filter: chroma(color="#FFFFFF"); + // would become + // filter: chroma(color="#FFF"); + // which makes the filter break in IE. + $css = preg_replace_callback("@([^\"'=\\s])(\\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])@", array($this, '_shortenHexCB'), $css); + + // Remove empty rules. + $css = preg_replace("@[^\\}]+\\{;\\}@", "", $css); + + $linebreakpos = isset($this->_options['linebreakpos']) + ? $this->_options['linebreakpos'] + : 0; + + if ($linebreakpos > 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + $i = 0; + $linestartpos = 0; + $sb = $css; + + // make sure strlen returns byte count + $mbIntEnc = null; + if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + $sbLength = strlen($css); + while ($i < $sbLength) { + $c = $sb[$i++]; + if ($c === '}' && $i - $linestartpos > $linebreakpos) { + $sb = substr_replace($sb, "\n", $i, 0); + $sbLength++; + $linestartpos = $i; + } + } + $css = $sb; + + // undo potential mb_encoding change + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } + } + + // Replace the pseudo class for the Box Model Hack + $css = str_replace("___PSEUDOCLASSBMH___", "\"\\\\\"}\\\\\"\"", $css); + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + $css = preg_replace("@;;+@", ";", $css); + + // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/ + $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css); + + // Trim the final string (for any leading or trailing white spaces) + $css = trim($css); + + return $css; + } + + protected function _removeSpacesCB($m) + { + return str_replace(':', '___PSEUDOCLASSCOLON___', $m[0]); + } + + protected function _shortenRgbCB($m) + { + $rgbcolors = explode(',', $m[1]); + $hexcolor = '#'; + for ($i = 0; $i < count($rgbcolors); $i++) { + $val = round($rgbcolors[$i]); + if ($val < 16) { + $hexcolor .= '0'; + } + $hexcolor .= dechex($val); + } + return $hexcolor; + } + + protected function _shortenHexCB($m) + { + // Test for AABBCC pattern + if ((strtolower($m[3])===strtolower($m[4])) && + (strtolower($m[5])===strtolower($m[6])) && + (strtolower($m[7])===strtolower($m[8]))) { + return $m[1] . $m[2] . "#" . $m[3] . $m[5] . $m[7]; + } else { + return $m[0]; + } + } +} \ No newline at end of file diff --git a/min/lib/Minify/YUI/Java/Matcher.php b/min/lib/Minify/YUI/Java/Matcher.php new file mode 100644 index 0000000..c504896 --- /dev/null +++ b/min/lib/Minify/YUI/Java/Matcher.php @@ -0,0 +1,81 @@ +_subject = $subject; + preg_match_all($pattern, $subject, $this->_matches, PREG_OFFSET_CAPTURE); + } + + /** + * + * @return bool + */ + public function find() + { + $this->_match = current($this->_matches); + if ($this->_match) { + next($this->_matches); + return true; + } + return false; + } + + public function group($group = 0) + { + return $this->_match[0][$group]; + } + + public function start() + { + return $this->_match[1]; + } + + public function end() + { + return $this->_match[1] + strlen($this->_match[0][0]); + } + + public function appendReplacement(Minify_YUI_Java_String $string, $replacement) + { + $length = $this->start() - $this->_appendPosition; + $string->append(substr($this->_subject, $this->_appendPosition, $length)); + + $i = 0; + $newReplacement = ''; + $next = ''; + $length = strlen($replacement); + while ($i < $length) { + $curr = $replacement[$i]; + $next = ($i === ($length - 1)) ? '' : $replacement[$i + 1]; + if ($curr === '\\' && $next === '$') { + $newReplacement .= '$'; + $i += 2; + continue; + } + if ($curr === '$' && is_numeric($next) && isset($this->_match[0][(int) $next])) { + $newReplacement .= $this->_match[0][(int) $next]; + $i += 2; + continue; + } + $newReplacement .= $curr; + $i++; + } + + $string->append($newReplacement); + $this->_appendPosition = $this->end(); + } + + public function appendTail(Minify_YUI_Java_String $string) + { + $string->append(substr($this->_subject, $this->_appendPosition)); + } +} \ No newline at end of file diff --git a/min/lib/Minify/YUI/Java/String.php b/min/lib/Minify/YUI/Java/String.php new file mode 100644 index 0000000..63a0c37 --- /dev/null +++ b/min/lib/Minify/YUI/Java/String.php @@ -0,0 +1,64 @@ +content = $content; + } + + public function replace($target, $replacement) + { + return new self(str_replace($target, $replacement, $this->content)); + } + + public function replaceAll($regex, $replacement) + { + $pattern = '/' . str_replace('/', '\/', $regex) . '/'; + return new self(preg_replace($pattern, $replacement, $this->content)); + } + + /** + * Return position (in bytes) of string found or -1 (not FALSE!) + * @param string $needle + * @param int $offset + * @return int + */ + public function indexOf($needle, $offset = 0) { + $pos = strpos($this->content, $needle, $offset); + return ($pos === false) + ? -1 + : $pos; + } + + /** + * Get number of bytes (not characters) in string + * @return int + */ + public function length() + { + return strlen($this->content); + } + + public function toString() + { + return $this->content; + } + + public function append($str) + { + $this->content .= $str; + } +} \ No newline at end of file diff --git a/min/lib/Minify/YUICompressor.php b/min/lib/Minify/YUICompressor.php index 7cb61ad..cd93b0e 100644 --- a/min/lib/Minify/YUICompressor.php +++ b/min/lib/Minify/YUICompressor.php @@ -110,7 +110,7 @@ class Minify_YUICompressor { ); $cmd = self::$javaExecutable . ' -jar ' . escapeshellarg(self::$jarFile) . " --type {$type}" - . (preg_match('/^[a-zA-Z\\-]+$/', $o['charset']) + . (preg_match('/^[\\da-zA-Z0-9\\-]+$/', $o['charset']) ? " --charset {$o['charset']}" : '') . (is_numeric($o['line-break']) && $o['line-break'] >= 0 @@ -128,11 +128,14 @@ class Minify_YUICompressor { private static function _prepare() { - if (! is_file(self::$jarFile) - || ! is_dir(self::$tempDir) - || ! is_writable(self::$tempDir) - ) { - throw new Exception('Minify_YUICompressor : $jarFile and $tempDir must be set.'); + if (! is_file(self::$jarFile)) { + throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not a valid file.'); + } + if (! is_dir(self::$tempDir)) { + throw new Exception('Minify_YUICompressor : $tempDir('.self::$tempDir.') is not a valid direcotry.'); + } + if (! is_writable(self::$tempDir)) { + throw new Exception('Minify_YUICompressor : $tempDir('.self::$tempDir.') is not writable.'); } } } diff --git a/min/lib/Solar/Dir.php b/min/lib/Solar/Dir.php deleted file mode 100644 index 37f7169..0000000 --- a/min/lib/Solar/Dir.php +++ /dev/null @@ -1,199 +0,0 @@ - - * - * @license http://opensource.org/licenses/bsd-license.php BSD - * - * @version $Id: Dir.php 2926 2007-11-09 16:25:44Z pmjones $ - * - */ -class Solar_Dir { - - /** - * - * The OS-specific temporary directory location. - * - * @var string - * - */ - protected static $_tmp; - - /** - * - * Hack for [[php::is_dir() | ]] that checks the include_path. - * - * Use this to see if a directory exists anywhere in the include_path. - * - * {{code: php - * $dir = Solar_Dir::exists('path/to/dir') - * if ($dir) { - * $files = scandir($dir); - * } else { - * echo "Not found in the include-path."; - * } - * }} - * - * @param string $dir Check for this directory in the include_path. - * - * @return mixed If the directory exists in the include_path, returns the - * absolute path; if not, returns boolean false. - * - */ - public static function exists($dir) - { - // no file requested? - $dir = trim($dir); - if (! $dir) { - return false; - } - - // using an absolute path for the file? - // dual check for Unix '/' and Windows '\', - // or Windows drive letter and a ':'. - $abs = ($dir[0] == '/' || $dir[0] == '\\' || $dir[1] == ':'); - if ($abs && is_dir($dir)) { - return $dir; - } - - // using a relative path on the file - $path = explode(PATH_SEPARATOR, ini_get('include_path')); - foreach ($path as $base) { - // strip Unix '/' and Windows '\' - $target = rtrim($base, '\\/') . DIRECTORY_SEPARATOR . $dir; - if (is_dir($target)) { - return $target; - } - } - - // never found it - return false; - } - - /** - * - * "Fixes" a directory string for the operating system. - * - * Use slashes anywhere you need a directory separator. Then run the - * string through fixdir() and the slashes will be converted to the - * proper separator (for example '\' on Windows). - * - * Always adds a final trailing separator. - * - * @param string $dir The directory string to 'fix'. - * - * @return string The "fixed" directory string. - * - */ - public static function fix($dir) - { - $dir = str_replace('/', DIRECTORY_SEPARATOR, $dir); - return rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - } - - /** - * - * Convenience method for dirname() and higher-level directories. - * - * @param string $file Get the dirname() of this file. - * - * @param int $up Move up in the directory structure this many - * times, default 0. - * - * @return string The dirname() of the file. - * - */ - public static function name($file, $up = 0) - { - $dir = dirname($file); - while ($up --) { - $dir = dirname($dir); - } - return $dir; - } - - /** - * - * Returns the OS-specific directory for temporary files. - * - * @param string $sub Add this subdirectory to the returned temporary - * directory name. - * - * @return string The temporary directory path. - * - */ - public static function tmp($sub = '') - { - // find the tmp dir if needed - if (! Solar_Dir::$_tmp) { - - // use the system if we can - if (function_exists('sys_get_temp_dir')) { - $tmp = sys_get_temp_dir(); - } else { - $tmp = Solar_Dir::_tmp(); - } - - // remove trailing separator and save - Solar_Dir::$_tmp = rtrim($tmp, DIRECTORY_SEPARATOR); - } - - // do we have a subdirectory request? - $sub = trim($sub); - if ($sub) { - // remove leading and trailing separators, and force exactly - // one trailing separator - $sub = trim($sub, DIRECTORY_SEPARATOR) - . DIRECTORY_SEPARATOR; - } - - return Solar_Dir::$_tmp . DIRECTORY_SEPARATOR . $sub; - } - - /** - * - * Returns the OS-specific temporary directory location. - * - * @return string The temp directory path. - * - */ - protected static function _tmp() - { - // non-Windows system? - if (strtolower(substr(PHP_OS, 0, 3)) != 'win') { - $tmp = empty($_ENV['TMPDIR']) ? getenv('TMPDIR') : $_ENV['TMPDIR']; - if ($tmp) { - return $tmp; - } else { - return '/tmp'; - } - } - - // Windows 'TEMP' - $tmp = empty($_ENV['TEMP']) ? getenv('TEMP') : $_ENV['TEMP']; - if ($tmp) { - return $tmp; - } - - // Windows 'TMP' - $tmp = empty($_ENV['TMP']) ? getenv('TMP') : $_ENV['TMP']; - if ($tmp) { - return $tmp; - } - - // Windows 'windir' - $tmp = empty($_ENV['windir']) ? getenv('windir') : $_ENV['windir']; - if ($tmp) { - return $tmp; - } - - // final fallback for Windows - return getenv('SystemRoot') . '\\temp'; - } -} \ No newline at end of file diff --git a/min/utils.php b/min/utils.php index c735941..a18d7ad 100644 --- a/min/utils.php +++ b/min/utils.php @@ -1,90 +1,74 @@ - * - * + * + * + * * * - * If you do not want ampersands as HTML entities, set Minify_Build::$ampersand = "&" - * before using this function. - * - * @param string $group a key from groupsConfig.php - * @param boolean $forceAmpersand (default false) Set to true if the RewriteRule - * directives in .htaccess are functional. This will remove the "?" from URIs, making them - * more cacheable by proxies. + * @param mixed $keyOrFiles a group key or array of file paths/URIs + * @param array $opts options: + * 'farExpires' : (default true) append a modified timestamp for cache revving + * 'debug' : (default false) append debug flag + * 'charset' : (default 'UTF-8') for htmlspecialchars + * 'minAppUri' : (default '/min') URI of min directory + * 'rewriteWorks' : (default true) does mod_rewrite work in min app? + * 'groupsConfigFile' : specify if different * @return string - */ -function Minify_groupUri($group, $forceAmpersand = false) -{ - $path = $forceAmpersand - ? "/g={$group}" - : "/?g={$group}"; - return _Minify_getBuild($group)->uri( - '/' . basename(dirname(__FILE__)) . $path - ,$forceAmpersand - ); -} - - -/** - * Get the last modification time of the source js/css files used by Minify to - * build the page. - * - * If you're caching the output of Minify_groupUri(), you'll want to rebuild - * the cache if it's older than this timestamp. - * - * - * // simplistic HTML cache system - * $file = '/path/to/cache/file'; - * if (! file_exists($file) || filemtime($file) < Minify_groupsMtime(array('js', 'css'))) { - * // (re)build cache - * $page = buildPage(); // this calls Minify_groupUri() for js and css - * file_put_contents($file, $page); - * echo $page; - * exit(); - * } - * readfile($file); - * - * - * @param array $groups an array of keys from groupsConfig.php - * @return int Unix timestamp of the latest modification - */ -function Minify_groupsMtime($groups) -{ - $max = 0; - foreach ((array)$groups as $group) { - $max = max($max, _Minify_getBuild($group)->lastModified); - } - return $max; -} - -/** - * @param string $group a key from groupsConfig.php - * @return Minify_Build - * @private */ -function _Minify_getBuild($group) +function Minify_getUri($keyOrFiles, $opts = array()) { - static $builds = array(); - static $gc = false; - if (false === $gc) { - $gc = (require dirname(__FILE__) . '/groupsConfig.php'); - } - if (! isset($builds[$group])) { - $builds[$group] = new Minify_Build($gc[$group]); - } - return $builds[$group]; + return Minify_HTML_Helper::getUri($keyOrFiles, $opts); +} + + +/** + * Get the last modification time of several source js/css files. If you're + * caching the output of Minify_getUri(), you might want to know if one of the + * dependent source files has changed so you can update the HTML. + * + * Since this makes a bunch of stat() calls, you might not want to check this + * on every request. + * + * @param array $keysAndFiles group keys and/or file paths/URIs. + * @return int latest modification time of all given keys/files + */ +function Minify_mtime($keysAndFiles, $groupsConfigFile = null) +{ + $gc = null; + if (! $groupsConfigFile) { + $groupsConfigFile = dirname(__FILE__) . '/groupsConfig.php'; + } + $sources = array(); + foreach ($keysAndFiles as $keyOrFile) { + if (is_object($keyOrFile) + || 0 === strpos($keyOrFile, '/') + || 1 === strpos($keyOrFile, ':\\')) { + // a file/source obj + $sources[] = $keyOrFile; + } else { + if (! $gc) { + $gc = (require $groupsConfigFile); + } + foreach ($gc[$keyOrFile] as $source) { + $sources[] = $source; + } + } + } + return Minify_HTML_Helper::getLastModified($sources); } diff --git a/min_unit_tests/.htaccess b/min_unit_tests/.htaccess new file mode 100644 index 0000000..4e05c4c --- /dev/null +++ b/min_unit_tests/.htaccess @@ -0,0 +1,4 @@ + +# In case AddOutputFilterByType has been added +SetEnv no-gzip + diff --git a/min_unit_tests/_inc.php b/min_unit_tests/_inc.php index bf416d4..711009a 100644 --- a/min_unit_tests/_inc.php +++ b/min_unit_tests/_inc.php @@ -24,7 +24,7 @@ if ($min_errorLogger && true !== $min_errorLogger) { // custom logger error_reporting(E_ALL | E_STRICT); ini_set('display_errors', 1); -header('Content-Type: text/plain'); +header('Content-Type: text/plain;charset=utf-8'); $thisDir = dirname(__FILE__); @@ -47,4 +47,17 @@ function assertTrue($test, $message) return (bool)$test; } +/** + * Get number of bytes in a string regardless of mbstring.func_overload + * + * @param string $str + * @return int + */ +function countBytes($str) +{ + return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($str, '8bit') + : strlen($str); +} + ob_start(); \ No newline at end of file diff --git a/min_unit_tests/_test_files/css/comments.min.css b/min_unit_tests/_test_files/css/comments.min.css index 3ea5c61..e0414a7 100644 --- a/min_unit_tests/_test_files/css/comments.min.css +++ b/min_unit_tests/_test_files/css/comments.min.css @@ -1,3 +1,3 @@ -/* YUI Compressor style comments are preserved */ +/*! YUI Compressor style comments are preserved */ body{background:#fff url(/path/to/image.gif) repeat-y} \ No newline at end of file diff --git a/min_unit_tests/_test_files/css/issue210.css b/min_unit_tests/_test_files/css/issue210.css new file mode 100644 index 0000000..842195b --- /dev/null +++ b/min_unit_tests/_test_files/css/issue210.css @@ -0,0 +1 @@ +.ui-widget { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; } \ No newline at end of file diff --git a/min_unit_tests/_test_files/css/issue210.min.css b/min_unit_tests/_test_files/css/issue210.min.css new file mode 100644 index 0000000..8a1869d --- /dev/null +++ b/min_unit_tests/_test_files/css/issue210.min.css @@ -0,0 +1 @@ +.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif} \ No newline at end of file diff --git a/min_unit_tests/_test_files/css/styles.css b/min_unit_tests/_test_files/css/styles.css index bf46c0a..ed0bcd4 100644 --- a/min_unit_tests/_test_files/css/styles.css +++ b/min_unit_tests/_test_files/css/styles.css @@ -1,9 +1,11 @@ +@charset "utf-8"; + /* some CSS to try to exercise things in general */ @import url( /more.css ); body, td, th { - font-family: Verdana , "Bitstream Vera Sans" , sans-serif ; + font-family: Verdana , "Bitstream Vera Sans" , Arial Narrow, sans-serif ; font-size : 12px; } diff --git a/min_unit_tests/_test_files/css/styles.min.css b/min_unit_tests/_test_files/css/styles.min.css index a823b05..a71063d 100644 --- a/min_unit_tests/_test_files/css/styles.min.css +++ b/min_unit_tests/_test_files/css/styles.min.css @@ -1,3 +1,3 @@ -@import url(/more.css);body,td,th{font-family:Verdana,"Bitstream Vera Sans",sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px +@import url(/more.css);body,td,th{font-family:Verdana,"Bitstream Vera Sans",Arial Narrow,sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px solid #0f7}div#content h1+p{padding-top:0;margin-top:0}@media all and (min-width: 640px){#media-queries-1{background-color:#0f0}}@media screen and (max-width: 2000px){#media-queries-2{background-color:#0f0}} \ No newline at end of file diff --git a/min_unit_tests/_test_files/css/vladmirated.min.css b/min_unit_tests/_test_files/css/vladmirated.min.css index eaffaee..a59e500 100644 --- a/min_unit_tests/_test_files/css/vladmirated.min.css +++ b/min_unit_tests/_test_files/css/vladmirated.min.css @@ -25,7 +25,7 @@ p{padding:0px;margin:0px;padding-bottom:5px}#post_nav{text-align:left;padding-bo a{text-decoration:none;font-family:Verdana;font-size:12px;color:#108eed}#post_nav a:hover, .post_nav_2 p a:hover{text-decoration:underline;color:#FF8000}#rightcol #about{padding-bottom:10px}#rightcol #r_news input{color:#333;font-size:12px}#header{height:200px;width:100%;background-image:url('images/header_bg.jpg');background-repeat:x-repeat}#header -img{float:right;margin-right: -3px;z-index:100}.tags{text-transform:lowercase;color:#333;font-family:arial;font-size:12px;border-top:2px dotted #EEE;width:300px;padding-top:20px;padding-bottom:0px;margin-top:0px;padding-left:20px;padding-right:20px}.tags +img{float:right;margin-right:-3px;z-index:100}.tags{text-transform:lowercase;color:#333;font-family:arial;font-size:12px;border-top:2px dotted #EEE;width:300px;padding-top:20px;padding-bottom:0px;margin-top:0px;padding-left:20px;padding-right:20px}.tags a{color:#108eed}.tags p{text-align:left;margin:0px;padding:0px}blockquote strong{font-family:verdana;display:block;margin-top:10px;color:#F00;font-style:italic;text-align:right}blockquote{margin:0px;background-color:#eee;border:2px diff --git a/min_unit_tests/_test_files/css_uriRewriter/exp_prepend.css b/min_unit_tests/_test_files/css_uriRewriter/exp_prepend.css new file mode 100644 index 0000000..87b7ecb --- /dev/null +++ b/min_unit_tests/_test_files/css_uriRewriter/exp_prepend.css @@ -0,0 +1,14 @@ +@import "http://cnd.com/A/B/foo.css"; +@import 'http://cnd.com/A/B/bar/foo.css' print; +@import 'http://cnd.com/A/bar/foo.css' print; +@import 'http://cnd.com/foo.css' print; +@import '/css/foo.css'; /* abs, should not alter */ +@import 'http://foo.com/css/foo.css'; /* abs, should not alter */ +@import url(http://cnd.com/A/foo.css) tv, projection; +@import url("/css/foo.css"); /* abs, should not alter */ +@import url(/css2/foo.css); /* abs, should not alter */ +@import url(data:image/gif;base64,AAAA); /* data, should not alter */ +foo {background:url('http://cnd.com/A/B/bar/foo.png')} +foo {background:url('http://foo.com/css/foo.css');} /* abs, should not alter */ +foo {background:url("//foo.com/css/foo.css");} /* protocol relative, should not alter */ +foo {background:url(data:image/gif;base64,AAAA);} /* data, should not alter */ \ No newline at end of file diff --git a/min_unit_tests/_test_files/css_uriRewriter/exp_prepend2.css b/min_unit_tests/_test_files/css_uriRewriter/exp_prepend2.css new file mode 100644 index 0000000..ceea4ec --- /dev/null +++ b/min_unit_tests/_test_files/css_uriRewriter/exp_prepend2.css @@ -0,0 +1,14 @@ +@import "//cnd.com/A/B/foo.css"; +@import '//cnd.com/A/B/bar/foo.css' print; +@import '//cnd.com/A/bar/foo.css' print; +@import '//cnd.com/foo.css' print; +@import '/css/foo.css'; /* abs, should not alter */ +@import 'http://foo.com/css/foo.css'; /* abs, should not alter */ +@import url(//cnd.com/A/foo.css) tv, projection; +@import url("/css/foo.css"); /* abs, should not alter */ +@import url(/css2/foo.css); /* abs, should not alter */ +@import url(data:image/gif;base64,AAAA); /* data, should not alter */ +foo {background:url('//cnd.com/A/B/bar/foo.png')} +foo {background:url('http://foo.com/css/foo.css');} /* abs, should not alter */ +foo {background:url("//foo.com/css/foo.css");} /* protocol relative, should not alter */ +foo {background:url(data:image/gif;base64,AAAA);} /* data, should not alter */ \ No newline at end of file diff --git a/min_unit_tests/_test_files/html/before.html b/min_unit_tests/_test_files/html/before.html index c22cc8f..3b06d18 100644 --- a/min_unit_tests/_test_files/html/before.html +++ b/min_unit_tests/_test_files/html/before.html @@ -78,7 +78,7 @@ css hack {
@@ -90,7 +90,7 @@ Design
 		
+1234567890 \ No newline at end of file diff --git a/min_unit_tests/_test_files/html/before.min.html b/min_unit_tests/_test_files/html/before.min.html index 58ba2bb..7ec603d 100644 --- a/min_unit_tests/_test_files/html/before.min.html +++ b/min_unit_tests/_test_files/html/before.min.html @@ -8,7 +8,7 @@ name="robots" content="all" />css Zen Garden: The Beauty in CSS Design</t is.mac=is.ua.indexOf('mac')>=0;if(is.ua.indexOf('opera')>=0){is.ie=is.ns=false;is.opera=true;} if(is.ua.indexOf('gecko')>=0){is.ie=is.ns=false;is.gecko=true;}</script> <script type="text/javascript">/*<![CDATA[*/var i=0;while(++i<10) {}/*]]>*/</script> <script type="text/javascript">i=1;</script> <script type="text/javascript">/*<![CDATA[*/(i<1);/*]]>*/</script> <!--[if IE 6]><style type="text/css">/*<![CDATA[*/ -/* copyright: you'll need CDATA for this < & */ +/*! copyright: you'll need CDATA for this < & */ body{background:white}/*]]>*/</style><![endif]--><style type="text/css" title="currentStyle" media="screen">@import "/001/001.css";/*\*/css hack{}/**/ /*/*/css hack{}/**/css @@ -20,10 +20,12 @@ rel="alternate" type="application/rss+xml" title="RSS" href="http://www.csszengarden.com/zengarden.xml" /></head><body -id="css-zen-garden"> <!--[if !IE]>--><p>Browser != IE</p><!--<![endif]--><div +id="css-zen-garden"> +<!--[if !IE]>--><p>Browser != IE</p><!--<![endif]--><div id="container"><div id="pageHeader"><h1><span>css Zen Garden</span></h1><h2><span>The Beauty of <acronym -title="Cascading Style Sheets">CSS</acronym> Design</span></h2></div><pre> +title="Cascading Style Sheets">CSS</acronym> +Design</span></h2></div><pre> White space is important here! </pre><div id="quickSummary"><p @@ -33,4 +35,4 @@ class="p2"><span>Download the sample <a href="/zengarden-sample.html" title="This page's source HTML code, not to be modified.">html file</a> and <a href="/zengarden-sample.css" title="This page's sample CSS, the file you may modify.">css file</a></span></p></div><textarea name="comment" id="comment" rows="6" class="maxwidth" cols="80">66666 -1234567890</textarea></div></body></html> \ No newline at end of file +1234567890<script>var Hello='world';</script></textarea></div></body></html> \ No newline at end of file diff --git a/min_unit_tests/_test_files/html/before2.min.html b/min_unit_tests/_test_files/html/before2.min.html index ae28465..0e1680f 100644 --- a/min_unit_tests/_test_files/html/before2.min.html +++ b/min_unit_tests/_test_files/html/before2.min.html @@ -8,7 +8,7 @@ name="robots" content="all"><title>css Zen Garden: The Beauty in CSS Design</tit is.mac=is.ua.indexOf('mac')>=0;if(is.ua.indexOf('opera')>=0){is.ie=is.ns=false;is.opera=true;} if(is.ua.indexOf('gecko')>=0){is.ie=is.ns=false;is.gecko=true;}</script> <script type="text/javascript">var i=0;while(++i<10) {}</script> <script type="text/javascript">i=1;</script> <script type="text/javascript">(i<1);</script> <!--[if IE 6]><style type="text/css"> -/* copyright: you'll need CDATA for this < & */ +/*! copyright: you'll need CDATA for this < & */ body{background:white}</style><![endif]--><style type="text/css" title="currentStyle" media="screen">@import "/001/001.css";/*\*/css hack{}/**/ /*/*/css hack{}/**/css @@ -20,10 +20,12 @@ rel="alternate" type="application/rss+xml" title="RSS" href="http://www.csszengarden.com/zengarden.xml"></head><body -id="css-zen-garden"> <!--[if !IE]>--><p>Browser != IE</p><!--<![endif]--><div +id="css-zen-garden"> +<!--[if !IE]>--><p>Browser != IE</p><!--<![endif]--><div id="container"><div id="pageHeader"><h1><span>css Zen Garden</span></h1><h2><span>The Beauty of <acronym -title="Cascading Style Sheets">CSS</acronym> Design</span></h2></div><pre> +title="Cascading Style Sheets">CSS</acronym> +Design</span></h2></div><pre> White space is important here! </pre><div id="quickSummary"><p diff --git a/min_unit_tests/_test_files/htmlHelper_groupsConfig.php b/min_unit_tests/_test_files/htmlHelper_groupsConfig.php new file mode 100644 index 0000000..7f5153d --- /dev/null +++ b/min_unit_tests/_test_files/htmlHelper_groupsConfig.php @@ -0,0 +1,8 @@ +<?php + +return array( + 'css' => array( + '//_test_files/css/paths_prepend.css' + ,'//_test_files/css/styles.css' + ) +); diff --git a/min_unit_tests/_test_files/importProcessor/output.css b/min_unit_tests/_test_files/importProcessor/output.css index 1e4358c..a652c99 100644 --- a/min_unit_tests/_test_files/importProcessor/output.css +++ b/min_unit_tests/_test_files/importProcessor/output.css @@ -1,10 +1,12 @@ @media screen { +@charset "utf-8"; + /* some CSS to try to exercise things in general */ @import url(/more.css); body, td, th { - font-family: Verdana , "Bitstream Vera Sans" , sans-serif ; + font-family: Verdana , "Bitstream Vera Sans" , Arial Narrow, sans-serif ; font-size : 12px; } diff --git a/min_unit_tests/_test_files/js/before.min.js b/min_unit_tests/_test_files/js/before.min.js index 6f06c6e..e89ccff 100644 --- a/min_unit_tests/_test_files/js/before.min.js +++ b/min_unit_tests/_test_files/js/before.min.js @@ -1,10 +1,10 @@ -/* is.js +/*! is.js (c) 2001 Douglas Crockford 2001 June 3 */ var is={ie:navigator.appName=='Microsoft Internet Explorer',java:navigator.javaEnabled(),ns:navigator.appName=='Netscape',ua:navigator.userAgent.toLowerCase(),version:parseFloat(navigator.appVersion.substr(21))||parseFloat(navigator.appVersion),win:navigator.platform=='Win32'} -/** +/*!* * preserve this comment, too */ is.mac=is.ua.indexOf('mac')>=0;if(is.ua.indexOf('opera')>=0){is.ie=is.ns=false;is.opera=true;} diff --git a/min_unit_tests/_test_files/js/issue132.js b/min_unit_tests/_test_files/js/issue132.js new file mode 100644 index 0000000..4ccfb15 --- /dev/null +++ b/min_unit_tests/_test_files/js/issue132.js @@ -0,0 +1,7 @@ +// from jQuery tablesorter +ts.addParser({ + id: "currency", + is: function(s) { + return /^[£$€?.]/.test(s); + }, +}); diff --git a/min_unit_tests/_test_files/js/issue132.min.js b/min_unit_tests/_test_files/js/issue132.min.js new file mode 100644 index 0000000..7215187 --- /dev/null +++ b/min_unit_tests/_test_files/js/issue132.min.js @@ -0,0 +1 @@ +ts.addParser({id:"currency",is:function(s){return /^[£$€?.]/.test(s);},}); \ No newline at end of file diff --git a/min_unit_tests/_test_files/js/issue141.min.js b/min_unit_tests/_test_files/js/issue141.min.js new file mode 100644 index 0000000..10aa56e --- /dev/null +++ b/min_unit_tests/_test_files/js/issue141.min.js @@ -0,0 +1,3 @@ +// The MinApp controller should cause this file to not be minified +// since the basename of the filepath matches the default noMinPattern + diff --git a/min_unit_tests/_test_files/js/issue144.js b/min_unit_tests/_test_files/js/issue144.js new file mode 100644 index 0000000..41b72f1 --- /dev/null +++ b/min_unit_tests/_test_files/js/issue144.js @@ -0,0 +1,2 @@ +// JSMin should not alter this file +if(!a.id)a.id="dp"+ ++this.uuid; diff --git a/min_unit_tests/_test_files/js/issue144.min.js b/min_unit_tests/_test_files/js/issue144.min.js new file mode 100644 index 0000000..41b72f1 --- /dev/null +++ b/min_unit_tests/_test_files/js/issue144.min.js @@ -0,0 +1,2 @@ +// JSMin should not alter this file +if(!a.id)a.id="dp"+ ++this.uuid; diff --git a/min_unit_tests/_test_files/minify/QueryString.js b/min_unit_tests/_test_files/minify/QueryString.js index e56a244..d926d64 100644 --- a/min_unit_tests/_test_files/minify/QueryString.js +++ b/min_unit_tests/_test_files/minify/QueryString.js @@ -1,4 +1,4 @@ -var MrClay = window.MrClay || {}; +var MrClay = window.MrClay || {}; /** * Simplified access to/manipulation of the query string diff --git a/min_unit_tests/_test_files/minify/email.js b/min_unit_tests/_test_files/minify/email.js index 761062d..b725379 100644 --- a/min_unit_tests/_test_files/minify/email.js +++ b/min_unit_tests/_test_files/minify/email.js @@ -1,4 +1,4 @@ -// http://mrclay.org/ +// http://mrclay.org/ (function(){ var reMailto = /^mailto:my_name_is_(\S+)_and_the_domain_is_(\S+)$/, diff --git a/min_unit_tests/_test_files/minify/issue143.js b/min_unit_tests/_test_files/minify/issue143.js new file mode 100644 index 0000000..d726a73 --- /dev/null +++ b/min_unit_tests/_test_files/minify/issue143.js @@ -0,0 +1,6 @@ +/* + * This file is to intentionally throw a JSMin exception + */ +function HelloWorld() { + return /regexp; +} diff --git a/min_unit_tests/_test_files/minify/lines_bugs.js b/min_unit_tests/_test_files/minify/lines_bugs.js index 3215540..cf56b58 100644 --- a/min_unit_tests/_test_files/minify/lines_bugs.js +++ b/min_unit_tests/_test_files/minify/lines_bugs.js @@ -1,2 +1,10 @@ -var triggerBug = {_default: "*/*"}; -var essentialFunctionality = true; +// sections from Prototype 1.6.1 +var xpath = ".//*[local-name()='ul' or local-name()='UL']" + + "//*[local-name()='li' or local-name()='LI']"; +this.matcher = ['.//*']; +xpath = { + descendant: "//*", + child: "/*", + f: 0 +}; +document._getElementsByXPath('.//*' + cond, element); \ No newline at end of file diff --git a/min_unit_tests/_test_files/minify/lines_output.js b/min_unit_tests/_test_files/minify/lines_output.js index 424308c..0c7a63a 100644 --- a/min_unit_tests/_test_files/minify/lines_output.js +++ b/min_unit_tests/_test_files/minify/lines_output.js @@ -29,9 +29,16 @@ ; /* lines_bugs.js */ -/* 1 */ var triggerBug = {_default: "*/*"}; -/* 2 */ var essentialFunctionality = true; -/* 3 */ +/* 1 */ // sections from Prototype 1.6.1 +/* 2 */ var xpath = ".//"+"*[local-name()='ul' or local-name()='UL']" + +/* 3 */ "//"+"*[local-name()='li' or local-name()='LI']"; +/* 4 */ this.matcher = ['.//'+'*']; +/* 5 */ xpath = { +/* 6 */ descendant: "//"+"*", +/* 7 */ child: "/"+"*", +/* 8 */ f: 0 +/* 9 */ }; +/* 10 */ document._getElementsByXPath('.//'+'*' + cond, element); ; /* QueryString.js */ diff --git a/min_unit_tests/_test_files/minify/minified.css b/min_unit_tests/_test_files/minify/minified.css index 200b60f..4798bde 100644 --- a/min_unit_tests/_test_files/minify/minified.css +++ b/min_unit_tests/_test_files/minify/minified.css @@ -1,5 +1,5 @@ -@import url(/more.css);body,td,th{font-family:Verdana,"Bitstream Vera Sans",sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px +@import url(/more.css);body,td,th{font-family:Verdana,"Bitstream Vera Sans",Arial Narrow,sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px solid #0f7}div#content h1+p{padding-top:0;margin-top:0}@media all and (min-width: 640px){#media-queries-1{background-color:#0f0}}@media screen and (max-width: 2000px){#media-queries-2{background-color:#0f0}} -/* YUI Compressor style comments are preserved */ +/*! YUI Compressor style comments are preserved */ body{background:#fff url(/path/to/image.gif) repeat-y} \ No newline at end of file diff --git a/min_unit_tests/_test_files/minify/minified.js b/min_unit_tests/_test_files/minify/minified.js index 5a490ed..3883520 100644 --- a/min_unit_tests/_test_files/minify/minified.js +++ b/min_unit_tests/_test_files/minify/minified.js @@ -1,5 +1,6 @@ (function(){var -reMailto=/^mailto:my_name_is_(\S+)_and_the_domain_is_(\S+)$/,reRemoveTitleIf=/^my name is/,oo=window.onload,fixHrefs=function(){var i=0,l,m;while(l=document.links[i++]){if(m=l.href.match(reMailto)){l.href='mailto:'+m[1]+'@'+m[2];if(reRemoveTitleIf.test(l.title)){l.title='';}}}};window.onload=function(){oo&&oo();fixHrefs();};})();;var MrClay=window.MrClay||{};MrClay.QueryString=function(){var parse=function(str){var assignments=str.split('&'),obj={},propValue;for(var i=0,l=assignments.length;i<l;++i){propValue=assignments[i].split('=');if(propValue.length>2||-1!=propValue[0].indexOf('+')||propValue[0]==''){continue;} +reMailto=/^mailto:my_name_is_(\S+)_and_the_domain_is_(\S+)$/,reRemoveTitleIf=/^my name is/,oo=window.onload,fixHrefs=function(){var i=0,l,m;while(l=document.links[i++]){if(m=l.href.match(reMailto)){l.href='mailto:'+m[1]+'@'+m[2];if(reRemoveTitleIf.test(l.title)){l.title='';}}}};window.onload=function(){oo&&oo();fixHrefs();};})(); +;var MrClay=window.MrClay||{};MrClay.QueryString=function(){var parse=function(str){var assignments=str.split('&'),obj={},propValue;for(var i=0,l=assignments.length;i<l;++i){propValue=assignments[i].split('=');if(propValue.length>2||-1!=propValue[0].indexOf('+')||propValue[0]==''){continue;} if(propValue.length==1){propValue[1]=propValue[0];} obj[unescape(propValue[0])]=unescape(propValue[1].replace(/\+/g,' '));} return obj;};function construct_(spec){spec=spec||window;if(typeof spec=='object'){this.window=spec;spec=spec.location.search.substr(1);}else{this.window=window;} diff --git a/min_unit_tests/_test_files/yuic/README b/min_unit_tests/_test_files/yuic/README new file mode 100644 index 0000000..12f9be7 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/README @@ -0,0 +1,6 @@ +To add a test: + +1. Create a "blah.css" or "blah.js" file. +2. Create a "blah.css.min" or "blah.js.min" file, containing the expected minified output. + +That's all! \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/background-position.css b/min_unit_tests/_test_files/yuic/background-position.css new file mode 100644 index 0000000..4cdff82 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/background-position.css @@ -0,0 +1,2 @@ +a {background-position: 0 0 0 0;} +b {BACKGROUND-POSITION: 0 0;} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/background-position.css.min b/min_unit_tests/_test_files/yuic/background-position.css.min new file mode 100644 index 0000000..0895e1a --- /dev/null +++ b/min_unit_tests/_test_files/yuic/background-position.css.min @@ -0,0 +1 @@ +a{background-position:0 0}b{background-position:0 0} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/border-none.css b/min_unit_tests/_test_files/yuic/border-none.css new file mode 100644 index 0000000..29f9cba --- /dev/null +++ b/min_unit_tests/_test_files/yuic/border-none.css @@ -0,0 +1,5 @@ +a { + border: none; +} +b {BACKGROUND:none} +s {border-top: none;} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/border-none.css.min b/min_unit_tests/_test_files/yuic/border-none.css.min new file mode 100644 index 0000000..1ed1b65 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/border-none.css.min @@ -0,0 +1 @@ +a{border:0}b{background:0}s{border-top:0} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/box-model-hack.css b/min_unit_tests/_test_files/yuic/box-model-hack.css new file mode 100644 index 0000000..c00e32f --- /dev/null +++ b/min_unit_tests/_test_files/yuic/box-model-hack.css @@ -0,0 +1,9 @@ +#elem { + width: 100px; + voice-family: "\"}\""; + voice-family:inherit; + width: 200px; +} +html>body #elem { + width: 200px; +} diff --git a/min_unit_tests/_test_files/yuic/box-model-hack.css.min b/min_unit_tests/_test_files/yuic/box-model-hack.css.min new file mode 100644 index 0000000..3340179 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/box-model-hack.css.min @@ -0,0 +1 @@ +#elem{width:100px;voice-family:"\"}\"";voice-family:inherit;width:200px}html>body #elem{width:200px} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/bug2527974.css b/min_unit_tests/_test_files/yuic/bug2527974.css new file mode 100644 index 0000000..b3bc2c8 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/bug2527974.css @@ -0,0 +1,10 @@ +/* this file contains no css, it exists purely to put the revision number into the + combined css before uploading it to SiteManager. The exclaimation at the start + of the comment informs yuicompressor not to strip the comment out */ + +/*! $LastChangedRevision: 81 $ $LastChangedDate: 2009-05-27 17:41:02 +0100 (Wed, 27 May 2009) $ */ + +body { + yo: cats; +} +ul[id$=foo] label:hover {yo: yo;} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/bug2527974.css.min b/min_unit_tests/_test_files/yuic/bug2527974.css.min new file mode 100644 index 0000000..00cc007 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/bug2527974.css.min @@ -0,0 +1 @@ +/*! $LastChangedRevision: 81 $ $LastChangedDate: 2009-05-27 17:41:02 +0100 (Wed, 27 May 2009) $ */body{yo:cats}ul[id$=foo] label:hover{yo:yo} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/bug2527991.css b/min_unit_tests/_test_files/yuic/bug2527991.css new file mode 100644 index 0000000..d4c80ff --- /dev/null +++ b/min_unit_tests/_test_files/yuic/bug2527991.css @@ -0,0 +1,19 @@ +@media screen and/*!YUI-Compresser */(-webkit-min-device-pixel-ratio:0) { + a{ + b: 1; + } +} + + +@media screen and/*! */ /*! */(-webkit-min-device-pixel-ratio:0) { + a{ + b: 1; + } +} + + +@media -webkit-min-device-pixel-ratio:0 { + a{ + b: 1; + } +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/bug2527991.css.min b/min_unit_tests/_test_files/yuic/bug2527991.css.min new file mode 100644 index 0000000..965755a --- /dev/null +++ b/min_unit_tests/_test_files/yuic/bug2527991.css.min @@ -0,0 +1 @@ +@media screen and/*!YUI-Compresser */(-webkit-min-device-pixel-ratio:0){a{b:1}}@media screen and/*! *//*! */(-webkit-min-device-pixel-ratio:0){a{b:1}}@media -webkit-min-device-pixel-ratio:0{a{b:1}} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/bug2527998.css b/min_unit_tests/_test_files/yuic/bug2527998.css new file mode 100644 index 0000000..9c6c00e --- /dev/null +++ b/min_unit_tests/_test_files/yuic/bug2527998.css @@ -0,0 +1,4 @@ +/*! special */ +body { + +} diff --git a/min_unit_tests/_test_files/yuic/bug2527998.css.min b/min_unit_tests/_test_files/yuic/bug2527998.css.min new file mode 100644 index 0000000..7fabf8a --- /dev/null +++ b/min_unit_tests/_test_files/yuic/bug2527998.css.min @@ -0,0 +1 @@ +/*! special */ \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/bug2528034.css b/min_unit_tests/_test_files/yuic/bug2528034.css new file mode 100644 index 0000000..c315cb1 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/bug2528034.css @@ -0,0 +1,5 @@ +a[href$="/test/"] span:first-child { b:1; } +a[href$="/test/"] span:first-child { } + + + diff --git a/min_unit_tests/_test_files/yuic/bug2528034.css.min b/min_unit_tests/_test_files/yuic/bug2528034.css.min new file mode 100644 index 0000000..1543777 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/bug2528034.css.min @@ -0,0 +1 @@ +a[href$="/test/"] span:first-child{b:1} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/charset-media.css b/min_unit_tests/_test_files/yuic/charset-media.css new file mode 100644 index 0000000..bd02f38 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/charset-media.css @@ -0,0 +1,9 @@ +/* re: 2495387 */ +@charset 'utf-8'; +@media all { +body { +} +body { +background-color: gold; +} +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/charset-media.css.min b/min_unit_tests/_test_files/yuic/charset-media.css.min new file mode 100644 index 0000000..dcaf49d --- /dev/null +++ b/min_unit_tests/_test_files/yuic/charset-media.css.min @@ -0,0 +1 @@ +@charset 'utf-8';@media all{body{background-color:gold}} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/color.css b/min_unit_tests/_test_files/yuic/color.css new file mode 100644 index 0000000..8d69639 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/color.css @@ -0,0 +1,7 @@ +.color { + me: rgb(123, 123, 123); + impressed: #ffeedd; + filter: chroma(color="#FFFFFF"); + background: none repeat scroll 0 0 rgb(255, 0,0); + alpha: rgba(1, 2, 3, 4); +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/color.css.min b/min_unit_tests/_test_files/yuic/color.css.min new file mode 100644 index 0000000..c45ebba --- /dev/null +++ b/min_unit_tests/_test_files/yuic/color.css.min @@ -0,0 +1 @@ +.color{me:#7b7b7b;impressed:#fed;filter:chroma(color="#FFFFFF");background:none repeat scroll 0 0 #f00;alpha:rgba(1,2,3,4)} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/comment.css b/min_unit_tests/_test_files/yuic/comment.css new file mode 100644 index 0000000..7073b9e --- /dev/null +++ b/min_unit_tests/_test_files/yuic/comment.css @@ -0,0 +1,3 @@ +html >/**/ body p { + color: blue; +} diff --git a/min_unit_tests/_test_files/yuic/comment.css.min b/min_unit_tests/_test_files/yuic/comment.css.min new file mode 100644 index 0000000..b280371 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/comment.css.min @@ -0,0 +1 @@ +html>/**/body p{color:blue} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/concat-charset.css b/min_unit_tests/_test_files/yuic/concat-charset.css new file mode 100644 index 0000000..87ca565 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/concat-charset.css @@ -0,0 +1,15 @@ +/* This is invalid CSS, but frequently happens as a result of concatenation. */ +@charset "utf-8"; +#foo { + border-width:1px; +} +/* +Note that this is erroneous! +The actual CSS file can only have a single charset. +However, this is the job of the author/application. +The compressor should not get involved. +*/ +@charset "another one"; +#bar { + border-width:10px; +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/concat-charset.css.min b/min_unit_tests/_test_files/yuic/concat-charset.css.min new file mode 100644 index 0000000..73e8d3b --- /dev/null +++ b/min_unit_tests/_test_files/yuic/concat-charset.css.min @@ -0,0 +1 @@ +@charset "utf-8";#foo{border-width:1px}#bar{border-width:10px} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/decimals.css b/min_unit_tests/_test_files/yuic/decimals.css new file mode 100644 index 0000000..9593979 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/decimals.css @@ -0,0 +1,3 @@ +::selection { + margin: 0.6px 0.333pt 1.2em 8.8cm; +} diff --git a/min_unit_tests/_test_files/yuic/decimals.css.min b/min_unit_tests/_test_files/yuic/decimals.css.min new file mode 100644 index 0000000..4dadedc --- /dev/null +++ b/min_unit_tests/_test_files/yuic/decimals.css.min @@ -0,0 +1 @@ +::selection{margin:.6px .333pt 1.2em 8.8cm} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/dollar-header.css b/min_unit_tests/_test_files/yuic/dollar-header.css new file mode 100644 index 0000000..43999c4 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/dollar-header.css @@ -0,0 +1,7 @@ +/*! +$Header: /temp/dirname/filename.css 3 2/02/08 3:37p JSmith $ +*/ + +foo { + bar: baz +} diff --git a/min_unit_tests/_test_files/yuic/dollar-header.css.min b/min_unit_tests/_test_files/yuic/dollar-header.css.min new file mode 100644 index 0000000..9308100 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/dollar-header.css.min @@ -0,0 +1,3 @@ +/*! +$Header: /temp/dirname/filename.css 3 2/02/08 3:37p JSmith $ +*/foo{bar:baz} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/font-face.css b/min_unit_tests/_test_files/yuic/font-face.css new file mode 100644 index 0000000..4b6956c --- /dev/null +++ b/min_unit_tests/_test_files/yuic/font-face.css @@ -0,0 +1,6 @@ +@font-face { + font-family: 'gzipper'; + src: url(yanone.eot); + src: local('gzipper'), + url(yanone.ttf) format('truetype'); +} diff --git a/min_unit_tests/_test_files/yuic/font-face.css.min b/min_unit_tests/_test_files/yuic/font-face.css.min new file mode 100644 index 0000000..3a1077c --- /dev/null +++ b/min_unit_tests/_test_files/yuic/font-face.css.min @@ -0,0 +1 @@ +@font-face{font-family:'gzipper';src:url(yanone.eot);src:local('gzipper'),url(yanone.ttf) format('truetype')} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/ie5mac.css b/min_unit_tests/_test_files/yuic/ie5mac.css new file mode 100644 index 0000000..e4d5204 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/ie5mac.css @@ -0,0 +1,5 @@ +/* Ignore the next rule in IE mac \*/ +.selector { + color: khaki; +} +/* Stop ignoring in IE mac */ diff --git a/min_unit_tests/_test_files/yuic/ie5mac.css.min b/min_unit_tests/_test_files/yuic/ie5mac.css.min new file mode 100644 index 0000000..f90df41 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/ie5mac.css.min @@ -0,0 +1 @@ +/*\*/.selector{color:khaki}/**/ \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/media-empty-class.css b/min_unit_tests/_test_files/yuic/media-empty-class.css new file mode 100644 index 0000000..d2f22d5 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/media-empty-class.css @@ -0,0 +1,16 @@ +/*! preserved */ +emptiness {} + +@import "another.css"; +/* I'm empty - delete me */ +empty { ;} + +@media print { + .noprint { display: none; } +} + +@media screen { + /* this rule should be removed, not simply minified.*/ + .breakme {} + .printonly { display: none; } +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/media-empty-class.css.min b/min_unit_tests/_test_files/yuic/media-empty-class.css.min new file mode 100644 index 0000000..0350c7f --- /dev/null +++ b/min_unit_tests/_test_files/yuic/media-empty-class.css.min @@ -0,0 +1 @@ +/*! preserved */@import "another.css";@media print{.noprint{display:none}}@media screen{.printonly{display:none}} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/media-multi.css b/min_unit_tests/_test_files/yuic/media-multi.css new file mode 100644 index 0000000..c589771 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/media-multi.css @@ -0,0 +1,3 @@ +@media only all and (max-width:50em), only all and (max-device-width:800px), only all and (max-width:780px) { + some-css : here +} diff --git a/min_unit_tests/_test_files/yuic/media-multi.css.min b/min_unit_tests/_test_files/yuic/media-multi.css.min new file mode 100644 index 0000000..57b52f7 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/media-multi.css.min @@ -0,0 +1 @@ +@media only all and (max-width:50em),only all and (max-device-width:800px),only all and (max-width:780px){some-css:here} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/media-test.css b/min_unit_tests/_test_files/yuic/media-test.css new file mode 100644 index 0000000..af118ff --- /dev/null +++ b/min_unit_tests/_test_files/yuic/media-test.css @@ -0,0 +1,3 @@ +@media screen and (-webkit-min-device-pixel-ratio:0) { + some-css : here +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/media-test.css.min b/min_unit_tests/_test_files/yuic/media-test.css.min new file mode 100644 index 0000000..0e7168e --- /dev/null +++ b/min_unit_tests/_test_files/yuic/media-test.css.min @@ -0,0 +1 @@ +@media screen and (-webkit-min-device-pixel-ratio:0){some-css:here} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/opacity-filter.css b/min_unit_tests/_test_files/yuic/opacity-filter.css new file mode 100644 index 0000000..60deca7 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/opacity-filter.css @@ -0,0 +1,14 @@ +/* example from https://developer.mozilla.org/en/CSS/opacity */ +pre { /* make the box translucent (80% opaque) */ + border: solid red; + opacity: 0.8; /* Firefox, Safari(WebKit), Opera */ + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; /* IE 8 */ + filter: PROGID:DXImageTransform.Microsoft.Alpha(Opacity=80); /* IE 4-7 */ + zoom: 1; /* set "zoom", "width" or "height" to trigger "hasLayout" in IE 7 and lower */ +} + +/** and again */ +code { + -ms-filter: "PROGID:DXImageTransform.Microsoft.Alpha(Opacity=80)"; /* IE 8 */ + filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); /* IE 4-7 */ +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/opacity-filter.css.min b/min_unit_tests/_test_files/yuic/opacity-filter.css.min new file mode 100644 index 0000000..99b4fa8 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/opacity-filter.css.min @@ -0,0 +1 @@ +pre{border:solid red;opacity:.8;-ms-filter:"alpha(opacity=80)";filter:alpha(opacity=80);zoom:1}code{-ms-filter:"alpha(opacity=80)";filter:alpha(opacity=80)} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/preserve-new-line.css b/min_unit_tests/_test_files/yuic/preserve-new-line.css new file mode 100644 index 0000000..e1f0c92 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/preserve-new-line.css @@ -0,0 +1,6 @@ +#sel-o { + content: "on\"ce upon \ +a time"; + content: 'once upon \ +a ti\'me'; +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/preserve-new-line.css.min b/min_unit_tests/_test_files/yuic/preserve-new-line.css.min new file mode 100644 index 0000000..6ac20b6 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/preserve-new-line.css.min @@ -0,0 +1,3 @@ +#sel-o{content:"on\"ce upon \ +a time";content:'once upon \ +a ti\'me'} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/preserve-strings.css b/min_unit_tests/_test_files/yuic/preserve-strings.css new file mode 100644 index 0000000..9151373 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/preserve-strings.css @@ -0,0 +1,7 @@ +/* preserving strings */ +.sele { + content: "\"keep \" me"; + something: '\\\' . . '; + else: 'empty{}'; + content: "/* test */"; /* <---- this is not a comment, should be be kept */ +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/preserve-strings.css.min b/min_unit_tests/_test_files/yuic/preserve-strings.css.min new file mode 100644 index 0000000..3f1d010 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/preserve-strings.css.min @@ -0,0 +1 @@ +.sele{content:"\"keep \" me";something:'\\\' . . ';else:'empty{}';content:"/* test */"} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/pseudo-first.css b/min_unit_tests/_test_files/yuic/pseudo-first.css new file mode 100644 index 0000000..dbadef4 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/pseudo-first.css @@ -0,0 +1,16 @@ +/* +because of IE6 first-letter and first-line +must be followed by a space +http://reference.sitepoint.com/css/pseudoelement-firstletter +Thanks: P.Sorokin comment at http://www.phpied.com/cssmin-js/ +*/ +p:first-letter{ + buh: hum; +} +p:first-line{ + baa: 1; +} + +p:first-line,a,p:first-letter,b{ + color: red; +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/pseudo-first.css.min b/min_unit_tests/_test_files/yuic/pseudo-first.css.min new file mode 100644 index 0000000..687117c --- /dev/null +++ b/min_unit_tests/_test_files/yuic/pseudo-first.css.min @@ -0,0 +1 @@ +p:first-letter {buh:hum}p:first-line {baa:1}p:first-line ,a,p:first-letter ,b{color:red} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/pseudo.css b/min_unit_tests/_test_files/yuic/pseudo.css new file mode 100644 index 0000000..126a5b1 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/pseudo.css @@ -0,0 +1,4 @@ +p :link { + ba:zinga;;; + foo: bar;;; +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/pseudo.css.min b/min_unit_tests/_test_files/yuic/pseudo.css.min new file mode 100644 index 0000000..bb7f8e7 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/pseudo.css.min @@ -0,0 +1 @@ +p :link{ba:zinga;foo:bar} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/special-comments.css b/min_unit_tests/_test_files/yuic/special-comments.css new file mode 100644 index 0000000..4e184ba --- /dev/null +++ b/min_unit_tests/_test_files/yuic/special-comments.css @@ -0,0 +1,13 @@ +/*!************88**** + Preserving comments + as they are + ******************** + Keep the initial ! + *******************/ +#yo { + ma: "ma"; +} +/*! +I said +pre- +serve! */ \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/special-comments.css.min b/min_unit_tests/_test_files/yuic/special-comments.css.min new file mode 100644 index 0000000..92ecbac --- /dev/null +++ b/min_unit_tests/_test_files/yuic/special-comments.css.min @@ -0,0 +1,9 @@ +/*!************88**** + Preserving comments + as they are + ******************** + Keep the initial ! + *******************/#yo{ma:"ma"}/*! +I said +pre- +serve! */ \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/star-underscore-hacks.css b/min_unit_tests/_test_files/yuic/star-underscore-hacks.css new file mode 100644 index 0000000..8b6e517 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/star-underscore-hacks.css @@ -0,0 +1,5 @@ +#elementarr { + width: 1px; + *width: 3pt; + _width: 2em; +} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/star-underscore-hacks.css.min b/min_unit_tests/_test_files/yuic/star-underscore-hacks.css.min new file mode 100644 index 0000000..0a014c3 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/star-underscore-hacks.css.min @@ -0,0 +1 @@ +#elementarr{width:1px;*width:3pt;_width:2em} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/string-in-comment.css b/min_unit_tests/_test_files/yuic/string-in-comment.css new file mode 100644 index 0000000..d94d192 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/string-in-comment.css @@ -0,0 +1,8 @@ +/* te " st */ +a{a:1} +/*!"preserve" me*/ +b{content: "/**/"} +/* quite " quote ' \' \" */ +/* ie mac \*/ +c {c : 3} +/* end hiding */ \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/string-in-comment.css.min b/min_unit_tests/_test_files/yuic/string-in-comment.css.min new file mode 100644 index 0000000..7cdec2d --- /dev/null +++ b/min_unit_tests/_test_files/yuic/string-in-comment.css.min @@ -0,0 +1 @@ +a{a:1}/*!"preserve" me*/b{content:"/**/"}/*\*/c{c:3}/**/ \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/webkit-transform.css b/min_unit_tests/_test_files/yuic/webkit-transform.css new file mode 100644 index 0000000..83a50f2 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/webkit-transform.css @@ -0,0 +1,2 @@ +c {-webkit-transform-origin: 0 0;} +d {-MOZ-TRANSFORM-ORIGIN: 0 0 } \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/webkit-transform.css.min b/min_unit_tests/_test_files/yuic/webkit-transform.css.min new file mode 100644 index 0000000..b640ddf --- /dev/null +++ b/min_unit_tests/_test_files/yuic/webkit-transform.css.min @@ -0,0 +1 @@ +c{-webkit-transform-origin:0 0}d{-moz-transform-origin:0 0} \ No newline at end of file diff --git a/min_unit_tests/_test_files/yuic/zeros.css b/min_unit_tests/_test_files/yuic/zeros.css new file mode 100644 index 0000000..a5a4da2 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/zeros.css @@ -0,0 +1,6 @@ +a { + margin: 0px 0pt 0em 0%; + _padding-top: 0ex; + background-position: 0 0; + padding: 0in 0cm 0mm 0pc +} diff --git a/min_unit_tests/_test_files/yuic/zeros.css.min b/min_unit_tests/_test_files/yuic/zeros.css.min new file mode 100644 index 0000000..14ac7a9 --- /dev/null +++ b/min_unit_tests/_test_files/yuic/zeros.css.min @@ -0,0 +1 @@ +a{margin:0;_padding-top:0;background-position:0 0;padding:0} \ No newline at end of file diff --git a/min_unit_tests/test_HTTP_Encoder.php b/min_unit_tests/test_HTTP_Encoder.php index 9573e80..4c57103 100644 --- a/min_unit_tests/test_HTTP_Encoder.php +++ b/min_unit_tests/test_HTTP_Encoder.php @@ -94,15 +94,16 @@ function test_HTTP_Encoder() } // test compression of varied content (HTML,JS, & CSS) + $variedContent = file_get_contents($thisDir . '/_test_files/html/before.html') . file_get_contents($thisDir . '/_test_files/css/subsilver.css') . file_get_contents($thisDir . '/_test_files/js/jquery-1.2.3.js'); - $variedLength = strlen($variedContent); + $variedLength = countBytes($variedContent); $encodingTests = array( - array('method' => 'deflate', 'inv' => 'gzinflate', 'exp' => 32157) - ,array('method' => 'gzip', 'inv' => '_gzdecode', 'exp' => 32175) - ,array('method' => 'compress', 'inv' => 'gzuncompress', 'exp' => 32211) + array('method' => 'deflate', 'inv' => 'gzinflate', 'exp' => 32268) + ,array('method' => 'gzip', 'inv' => '_gzdecode', 'exp' => 32286) + ,array('method' => 'compress', 'inv' => 'gzuncompress', 'exp' => 32325) ); foreach ($encodingTests as $test) { @@ -111,7 +112,7 @@ function test_HTTP_Encoder() ,'method' => $test['method'] )); $e->encode(9); - $ret = strlen($e->getContent()); + $ret = countBytes($e->getContent()); // test uncompression $roundTrip = @call_user_func($test['inv'], $e->getContent()); @@ -128,14 +129,19 @@ function test_HTTP_Encoder() , "(off by ". abs($ret - $test['exp']) . " bytes)\n\n"; } } - + + HTTP_Encoder::$encodeToIe6 = true; $_SERVER['HTTP_ACCEPT_ENCODING'] = 'identity'; - $he = new HTTP_Encoder(array( - 'content' => 'Hello' - )); + $he = new HTTP_Encoder(array('content' => 'Hello')); $he->encode(); $headers = $he->getHeaders(); - assertTrue(isset($headers['Vary']), 'HTTP_Encoder : Vary always sent'); + assertTrue(isset($headers['Vary']), 'HTTP_Encoder : Vary always sent to good browsers'); + + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)'; + $he = new HTTP_Encoder(array('content' => 'Hello')); + $he->encode(); + $headers = $he->getHeaders(); + assertTrue(! isset($headers['Vary']), 'HTTP_Encoder : Vary not sent to bad IE (Issue 126)'); } test_HTTP_Encoder(); @@ -149,15 +155,31 @@ function _gzdecode($data) // http://www.php.net/manual/en/function.gzdecode.php#82930 function _phpman_gzdecode($data, &$filename='', &$error='', $maxlength=null) { + $mbIntEnc = null; + $hasMbOverload = (function_exists('mb_strlen') + && (ini_get('mbstring.func_overload') !== '') + && ((int)ini_get('mbstring.func_overload') & 2)); + if ($hasMbOverload) { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + + $len = strlen($data); if ($len < 18 || strcmp(substr($data,0,2),"\x1f\x8b")) { $error = "Not in GZIP format."; + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return null; // Not GZIP format (See RFC 1952) } $method = ord(substr($data,2,1)); // Compression method $flags = ord(substr($data,3,1)); // Flags if ($flags & 31 != $flags) { $error = "Reserved bits not allowed."; + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return null; } // NOTE: $mtime may be negative (PHP integer limitations) @@ -171,11 +193,17 @@ function _phpman_gzdecode($data, &$filename='', &$error='', $maxlength=null) if ($flags & 4) { // 2-byte length prefixed EXTRA data in header if ($len - $headerlen - 2 < 8) { + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return false; // invalid } $extralen = unpack("v",substr($data,8,2)); $extralen = $extralen[1]; if ($len - $headerlen - 2 - $extralen < 8) { + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return false; // invalid } $extra = substr($data,10,$extralen); @@ -186,10 +214,16 @@ function _phpman_gzdecode($data, &$filename='', &$error='', $maxlength=null) if ($flags & 8) { // C-style string if ($len - $headerlen - 1 < 8) { + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return false; // invalid } $filenamelen = strpos(substr($data,$headerlen),chr(0)); if ($filenamelen === false || $len - $headerlen - $filenamelen - 1 < 8) { + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return false; // invalid } $filename = substr($data,$headerlen,$filenamelen); @@ -200,10 +234,16 @@ function _phpman_gzdecode($data, &$filename='', &$error='', $maxlength=null) if ($flags & 16) { // C-style string COMMENT data in header if ($len - $headerlen - 1 < 8) { + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return false; // invalid } $commentlen = strpos(substr($data,$headerlen),chr(0)); if ($commentlen === false || $len - $headerlen - $commentlen - 1 < 8) { + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return false; // Invalid header format } $comment = substr($data,$headerlen,$commentlen); @@ -213,6 +253,9 @@ function _phpman_gzdecode($data, &$filename='', &$error='', $maxlength=null) if ($flags & 2) { // 2-bytes (lowest order) of CRC32 on header present if ($len - $headerlen - 2 < 8) { + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return false; // invalid } $calccrc = crc32(substr($data,0,$headerlen)) & 0xffff; @@ -220,6 +263,9 @@ function _phpman_gzdecode($data, &$filename='', &$error='', $maxlength=null) $headercrc = $headercrc[1]; if ($headercrc != $calccrc) { $error = "Header checksum failed."; + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return false; // Bad header CRC } $headerlen += 2; @@ -233,6 +279,9 @@ function _phpman_gzdecode($data, &$filename='', &$error='', $maxlength=null) $bodylen = $len-$headerlen-8; if ($bodylen < 1) { // IMPLEMENTATION BUG! + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return null; } $body = substr($data,$headerlen,$bodylen); @@ -245,6 +294,9 @@ function _phpman_gzdecode($data, &$filename='', &$error='', $maxlength=null) break; default: $error = "Unknown compression method."; + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return false; } } // zero-byte body content is allowed @@ -254,7 +306,11 @@ function _phpman_gzdecode($data, &$filename='', &$error='', $maxlength=null) $lenOK = $isize == strlen($data); if (!$lenOK || !$crcOK) { $error = ( $lenOK ? '' : 'Length check FAILED. ') . ( $crcOK ? '' : 'Checksum FAILED.'); - return false; + $ret = false; } - return $data; + $ret = $data; + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } + return $ret; } \ No newline at end of file diff --git a/min_unit_tests/test_JSMin.php b/min_unit_tests/test_JSMin.php index 56d07a2..ef3a978 100644 --- a/min_unit_tests/test_JSMin.php +++ b/min_unit_tests/test_JSMin.php @@ -10,38 +10,52 @@ function test_JSMin() $src = file_get_contents($thisDir . '/_test_files/js/before.js'); $minExpected = file_get_contents($thisDir . '/_test_files/js/before.min.js'); $minOutput = JSMin::minify($src); - $passed = assertTrue($minExpected == $minOutput, 'JSMin : Overall'); - if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - echo "\n---Output: " .strlen($minOutput). " bytes\n\n{$minOutput}\n\n"; - echo "---Expected: " .strlen($minExpected). " bytes\n\n{$minExpected}\n\n"; - echo "---Source: " .strlen($src). " bytes\n\n{$src}\n\n\n"; + echo "\n---Output: " .countBytes($minOutput). " bytes\n\n{$minOutput}\n\n"; + echo "---Expected: " .countBytes($minExpected). " bytes\n\n{$minExpected}\n\n"; + echo "---Source: " .countBytes($src). " bytes\n\n{$src}\n\n\n"; } + $src = file_get_contents($thisDir . '/_test_files/js/issue144.js'); + $minExpected = file_get_contents($thisDir . '/_test_files/js/issue144.min.js'); + $minOutput = JSMin::minify($src); + $passed = assertTrue($minExpected == $minOutput, 'JSMin : Don\'t minify files with + ++ (Issue 144)'); + + if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { + $src = file_get_contents($thisDir . '/_test_files/js/issue132.js'); + $minExpected = file_get_contents($thisDir . '/_test_files/js/issue132.min.js'); + $minOutput = JSMin::minify($src); + $passed = assertTrue($minExpected == $minOutput, 'JSMin : mbstring.func_overload shouldn\'t cause failure (Issue 132)'); + if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { + echo "\n---Output: " .countBytes($minOutput). " bytes\n\n{$minOutput}\n\n"; + echo "---Expected: " .countBytes($minExpected). " bytes\n\n{$minExpected}\n\n"; + echo "---Source: " .countBytes($src). " bytes\n\n{$src}\n\n\n"; + } + } + $src = file_get_contents($thisDir . '/_test_files/js/issue74.js'); $minExpected = file_get_contents($thisDir . '/_test_files/js/issue74.min.js'); $minOutput = JSMin::minify($src); - $passed = assertTrue($minExpected == $minOutput, 'JSMin : Quotes in RegExp literals (Issue 74)'); - if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - echo "\n---Output: " .strlen($minOutput). " bytes\n\n{$minOutput}\n\n"; - echo "---Expected: " .strlen($minExpected). " bytes\n\n{$minExpected}\n\n"; - echo "---Source: " .strlen($src). " bytes\n\n{$src}\n\n\n"; - + echo "\n---Output: " .countBytes($minOutput). " bytes\n\n{$minOutput}\n\n"; + echo "---Expected: " .countBytes($minExpected). " bytes\n\n{$minExpected}\n\n"; + echo "---Source: " .countBytes($src). " bytes\n\n{$src}\n\n\n"; + + // only test exceptions on this page test_JSMin_exception('"Hello' ,'Unterminated String' ,'JSMin_UnterminatedStringException' - ,"Unterminated String: '\"Hello'"); + ,"JSMin: Unterminated String at byte 6: \"Hello"); test_JSMin_exception("return /regexp\n}" ,'Unterminated RegExp' ,'JSMin_UnterminatedRegExpException' - ,"Unterminated RegExp: '/regexp\n'"); + ,"JSMin: Unterminated RegExp at byte 15: /regexp\n"); test_JSMin_exception("/* Comment " ,'Unterminated Comment' ,'JSMin_UnterminatedCommentException' - ,"Unterminated Comment: '/* Comment '"); + ,"JSMin: Unterminated comment at byte 11: /* Comment "); } } diff --git a/min_unit_tests/test_JSMinPlus.php b/min_unit_tests/test_JSMinPlus.php index eaae8bd..880f9eb 100644 --- a/min_unit_tests/test_JSMinPlus.php +++ b/min_unit_tests/test_JSMinPlus.php @@ -15,9 +15,9 @@ function test_JSMinPlus() $passed = assertTrue($minExpected == $minOutput, 'JSMinPlus : Conditional Comments'); if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - echo "\n---Output: " .strlen($minOutput). " bytes\n\n{$minOutput}\n\n"; - echo "---Expected: " .strlen($minExpected). " bytes\n\n{$minExpected}\n\n"; - echo "---Source: " .strlen($src). " bytes\n\n{$src}\n\n\n"; + echo "\n---Output: " .countBytes($minOutput). " bytes\n\n{$minOutput}\n\n"; + echo "---Expected: " .countBytes($minExpected). " bytes\n\n{$minExpected}\n\n"; + echo "---Source: " .countBytes($src). " bytes\n\n{$src}\n\n\n"; } return; @@ -30,9 +30,9 @@ function test_JSMinPlus() $passed = assertTrue($minExpected == $minOutput, 'JSMinPlus : Overall'); if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - echo "\n---Output: " .strlen($minOutput). " bytes\n\n{$minOutput}\n\n"; - echo "---Expected: " .strlen($minExpected). " bytes\n\n{$minExpected}\n\n"; - echo "---Source: " .strlen($src). " bytes\n\n{$src}\n\n\n"; + echo "\n---Output: " .countBytes($minOutput). " bytes\n\n{$minOutput}\n\n"; + echo "---Expected: " .countBytes($minExpected). " bytes\n\n{$minExpected}\n\n"; + echo "---Source: " .countBytes($src). " bytes\n\n{$src}\n\n\n"; } $src = file_get_contents($thisDir . '/_test_files/js/issue74.js'); @@ -42,39 +42,10 @@ function test_JSMinPlus() $passed = assertTrue($minExpected == $minOutput, 'JSMinPlus : Quotes in RegExp literals (Issue 74)'); if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - echo "\n---Output: " .strlen($minOutput). " bytes\n\n{$minOutput}\n\n"; - echo "---Expected: " .strlen($minExpected). " bytes\n\n{$minExpected}\n\n"; - echo "---Source: " .strlen($src). " bytes\n\n{$src}\n\n\n"; - /* - test_JSMin_exception('"Hello' - ,'Unterminated String' - ,'JSMin_UnterminatedStringException' - ,"Unterminated String: '\"Hello'"); - test_JSMin_exception("return /regexp\n}" - ,'Unterminated RegExp' - ,'JSMin_UnterminatedRegExpException' - ,"Unterminated RegExp: '/regexp\n'"); - test_JSMin_exception("/* Comment " - ,'Unterminated Comment' - ,'JSMin_UnterminatedCommentException' - ,"Unterminated Comment: '/* Comment '"); - //*/ + echo "\n---Output: " .countBytes($minOutput). " bytes\n\n{$minOutput}\n\n"; + echo "---Expected: " .countBytes($minExpected). " bytes\n\n{$minExpected}\n\n"; + echo "---Source: " .countBytes($src). " bytes\n\n{$src}\n\n\n"; } } -/*function test_JSMin_exception($js, $label, $expClass, $expMessage) { - $eClass = $eMsg = ''; - try { - JSMin::minify($js); - } catch (Exception $e) { - $eClass = get_class($e); - $eMsg = $e->getMessage(); - } - $passed = assertTrue($eClass === $expClass && $eMsg === $expMessage, - 'JSMin : throw on ' . $label); - if (! $passed && __FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - echo "\n ---" , $e, "\n\n"; - } -}//*/ - test_JSMinPlus(); diff --git a/min_unit_tests/test_Minify.php b/min_unit_tests/test_Minify.php index 7075570..f45be60 100644 --- a/min_unit_tests/test_Minify.php +++ b/min_unit_tests/test_Minify.php @@ -29,7 +29,7 @@ function test_Minify() 'Vary' => 'Accept-Encoding', 'Last-Modified' => gmdate('D, d M Y H:i:s \G\M\T', $lastModified), 'ETag' => "\"pub{$lastModified}\"", - 'Cache-Control' => 'max-age=1800, public', + 'Cache-Control' => 'max-age=1800', '_responseCode' => 'HTTP/1.0 304 Not Modified', ) ); @@ -49,11 +49,11 @@ function test_Minify() assertTrue( ! class_exists('Minify_CSS', false) - && ! class_exists('Minify_Cache', false) + && ! class_exists('Minify_Cache_File', false) ,'Minify : cache, and minifier classes aren\'t loaded for 304s' ); - // Test minifying JS and serving with Expires header + // Test JS and Expires $content = preg_replace('/\\r\\n?/', "\n", file_get_contents($minifyTestPath . '/minified.js')); $lastModified = max( @@ -70,11 +70,13 @@ function test_Minify() 'Vary' => 'Accept-Encoding', 'Last-Modified' => gmdate('D, d M Y H:i:s \G\M\T', $lastModified), 'ETag' => "\"pub{$lastModified}\"", - 'Cache-Control' => 'max-age=86400, public', - 'Content-Length' => strlen($content), + 'Cache-Control' => 'max-age=86400', + 'Content-Length' => countBytes($content), 'Content-Type' => 'application/x-javascript; charset=utf-8', ) ); + unset($_SERVER['HTTP_IF_NONE_MATCH']); + unset($_SERVER['HTTP_IF_MODIFIED_SINCE']); $output = Minify::serve('Files', array( 'files' => array( $minifyTestPath . '/email.js' @@ -96,7 +98,7 @@ function test_Minify() // test for Issue 73 Minify::setCache(null); - $expected = ";function h(){}"; + $expected = "\n;function h(){}"; $output = Minify::serve('Files', array( 'files' => array( $minifyTestPath . '/issue73_1.js' @@ -168,9 +170,17 @@ function test_Minify() } } - // Test minifying CSS and responding with Etag/Last-Modified + // Test Issue 132 + if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { + $output = Minify::serve('Files', array( + 'files' => array(dirname(__FILE__) . '/_test_files/js/issue132.js') + ,'quiet' => true + ,'encodeOutput' => false + )); + $passed = assertTrue($output['headers']['Content-Length'] == 77, 'Minify : Issue 132 : mbstring.func_overload shouldn\'t cause incorrect Content-Length'); + } - Minify::setCache(null); + // Test minifying CSS and responding with Etag/Last-Modified // don't allow conditional headers unset($_SERVER['HTTP_IF_NONE_MATCH'], $_SERVER['HTTP_IF_MODIFIED_SINCE']); @@ -185,8 +195,8 @@ function test_Minify() 'Vary' => 'Accept-Encoding', 'Last-Modified' => gmdate('D, d M Y H:i:s \G\M\T', $lastModified), 'ETag' => "\"pub{$lastModified}\"", - 'Cache-Control' => 'max-age=0, public', - 'Content-Length' => strlen($expectedContent), + 'Cache-Control' => 'max-age=0', + 'Content-Length' => countBytes($expectedContent), 'Content-Type' => 'text/css; charset=utf-8', ) ); diff --git a/min_unit_tests/test_Minify_CSS.php b/min_unit_tests/test_Minify_CSS.php index 96e1875..0e3f630 100644 --- a/min_unit_tests/test_Minify_CSS.php +++ b/min_unit_tests/test_Minify_CSS.php @@ -19,7 +19,7 @@ function test_CSS() $d->close(); foreach ($list as $item) { - + $options = array(); if ($item === 'paths_prepend') { $options = array('prependRelativePath' => '../'); @@ -41,10 +41,10 @@ function test_CSS() $passed = assertTrue($minExpected === $minOutput, 'Minify_CSS : ' . $item); if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - echo "\n---Output: " .strlen($minOutput). " bytes\n\n{$minOutput}\n\n"; + echo "\n---Output: " .countBytes($minOutput). " bytes\n\n{$minOutput}\n\n"; if (!$passed) { - echo "---Expected: " .strlen($minExpected). " bytes\n\n{$minExpected}\n\n"; - echo "---Source: " .strlen($src). " bytes\n\n{$src}\n\n\n"; + echo "---Expected: " .countBytes($minExpected). " bytes\n\n{$minExpected}\n\n"; + echo "---Source: " .countBytes($src). " bytes\n\n{$src}\n\n\n"; } } } diff --git a/min_unit_tests/test_Minify_CSS_UriRewriter.php b/min_unit_tests/test_Minify_CSS_UriRewriter.php index 55f09b0..40eba91 100644 --- a/min_unit_tests/test_Minify_CSS_UriRewriter.php +++ b/min_unit_tests/test_Minify_CSS_UriRewriter.php @@ -17,18 +17,57 @@ function test_Minify_CSS_UriRewriter() ,$thisDir // use DOCUMENT_ROOT = '/full/path/to/min_unit_tests' ); - $passed = assertTrue($expected === $actual, 'Minify_CSS_UriRewriter'); + $passed = assertTrue($expected === $actual, 'Minify_CSS_UriRewriter : rewrite'); if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { echo "\n---Input:\n\n{$in}\n"; - echo "\n---Output: " .strlen($actual). " bytes\n\n{$actual}\n\n"; + echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n"; if (!$passed) { - echo "---Expected: " .strlen($expected). " bytes\n\n{$expected}\n\n\n"; + echo "---Expected: " .countBytes($expected). " bytes\n\n{$expected}\n\n\n"; } // show debugging only when test run directly echo "--- Minify_CSS_UriRewriter::\$debugText\n\n" , Minify_CSS_UriRewriter::$debugText; } + + + Minify_CSS_UriRewriter::$debugText = ''; + $in = file_get_contents($thisDir . '/_test_files/css_uriRewriter/in.css'); + $expected = file_get_contents($thisDir . '/_test_files/css_uriRewriter/exp_prepend.css'); + $actual = Minify_CSS_UriRewriter::prepend($in, 'http://cnd.com/A/B/'); + + $passed = assertTrue($expected === $actual, 'Minify_CSS_UriRewriter : prepend1'); + if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { + echo "\n---Input:\n\n{$in}\n"; + echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n"; + if (!$passed) { + echo "---Expected: " .countBytes($expected). " bytes\n\n{$expected}\n\n\n"; + } + + // show debugging only when test run directly + echo "--- Minify_CSS_UriRewriter::\$debugText\n\n" + , Minify_CSS_UriRewriter::$debugText; + } + + + Minify_CSS_UriRewriter::$debugText = ''; + $in = file_get_contents($thisDir . '/_test_files/css_uriRewriter/in.css'); + $expected = file_get_contents($thisDir . '/_test_files/css_uriRewriter/exp_prepend2.css'); + $actual = Minify_CSS_UriRewriter::prepend($in, '//cnd.com/A/B/'); + + $passed = assertTrue($expected === $actual, 'Minify_CSS_UriRewriter : prepend2'); + if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { + echo "\n---Input:\n\n{$in}\n"; + echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n"; + if (!$passed) { + echo "---Expected: " .countBytes($expected). " bytes\n\n{$expected}\n\n\n"; + } + + // show debugging only when test run directly + echo "--- Minify_CSS_UriRewriter::\$debugText\n\n" + , Minify_CSS_UriRewriter::$debugText; + } + Minify_CSS_UriRewriter::$debugText = ''; $in = '../../../../assets/skins/sam/sprite.png'; @@ -42,9 +81,9 @@ function test_Minify_CSS_UriRewriter() $passed = assertTrue($exp === $actual, 'Minify_CSS_UriRewriter : Issue 99'); if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { echo "\n---Input:\n\n{$in}\n"; - echo "\n---Output: " .strlen($actual). " bytes\n\n{$actual}\n\n"; + echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n"; if (!$passed) { - echo "---Expected: " .strlen($exp). " bytes\n\n{$exp}\n\n\n"; + echo "---Expected: " .countBytes($exp). " bytes\n\n{$exp}\n\n\n"; } // show debugging only when test run directly diff --git a/min_unit_tests/test_Minify_Cache_APC.php b/min_unit_tests/test_Minify_Cache_APC.php index 74a1b2c..1761add 100644 --- a/min_unit_tests/test_Minify_Cache_APC.php +++ b/min_unit_tests/test_Minify_Cache_APC.php @@ -9,14 +9,14 @@ function test_Minify_Cache_APC() if (! function_exists('apc_store')) { return; } - $data = str_repeat(md5('testing'), 160); + $data = str_repeat(md5(time()) . 'í', 100); // 3400 bytes in UTF-8 $id = 'Minify_test_cache'; $cache = new Minify_Cache_APC(); assertTrue(true === $cache->store($id, $data), $prefix . 'store'); - assertTrue(strlen($data) === $cache->getSize($id), $prefix . 'getSize'); + assertTrue(countBytes($data) === $cache->getSize($id), $prefix . 'getSize'); assertTrue(true === $cache->isValid($id, $_SERVER['REQUEST_TIME'] - 10), $prefix . 'isValid'); diff --git a/min_unit_tests/test_Minify_Cache_File.php b/min_unit_tests/test_Minify_Cache_File.php index 2463e5b..16582fd 100644 --- a/min_unit_tests/test_Minify_Cache_File.php +++ b/min_unit_tests/test_Minify_Cache_File.php @@ -5,19 +5,17 @@ require_once 'Minify/Cache/File.php'; function test_Minify_Cache_File() { - global $minifyCachePath; - - $data = str_repeat(md5(time()), 160); + $data = str_repeat(md5(time()) . 'í', 100); // 3400 bytes in UTF-8 $id = 'Minify_test_cache_noLock'; $prefix = 'Minify_Cache_File : '; - $cache = new Minify_Cache_File($minifyCachePath); + $cache = new Minify_Cache_File(); echo "NOTE: Minify_Cache_File : path is set to: '" . $cache->getPath() . "'.\n"; assertTrue(true === $cache->store($id, $data), $prefix . 'store'); - - assertTrue(strlen($data) === $cache->getSize($id), $prefix . 'getSize'); + + assertTrue(countBytes($data) === $cache->getSize($id), $prefix . 'getSize'); assertTrue(true === $cache->isValid($id, $_SERVER['REQUEST_TIME'] - 10), $prefix . 'isValid'); @@ -28,26 +26,26 @@ function test_Minify_Cache_File() assertTrue($data === $displayed, $prefix . 'display'); - assertTrue($data === $cache->fetch($id), $prefix . 'fetch'); - - // test with locks - - $id = 'Minify_test_cache_withLock'; - $cache = new Minify_Cache_File($minifyCachePath, true); - - assertTrue(true === $cache->store($id, $data), $prefix . 'store w/ lock'); - - assertTrue(strlen($data) === $cache->getSize($id), $prefix . 'getSize'); - - assertTrue(true === $cache->isValid($id, $_SERVER['REQUEST_TIME'] - 10), $prefix . 'isValid'); - - ob_start(); - $cache->display($id); - $displayed = ob_get_contents(); - ob_end_clean(); - - assertTrue($data === $displayed, $prefix . 'display w/ lock'); - + assertTrue($data === $cache->fetch($id), $prefix . 'fetch'); + + // test with locks + + $id = 'Minify_test_cache_withLock'; + $cache = new Minify_Cache_File('', true); + + assertTrue(true === $cache->store($id, $data), $prefix . 'store w/ lock'); + + assertTrue(countBytes($data) === $cache->getSize($id), $prefix . 'getSize'); + + assertTrue(true === $cache->isValid($id, $_SERVER['REQUEST_TIME'] - 10), $prefix . 'isValid'); + + ob_start(); + $cache->display($id); + $displayed = ob_get_contents(); + ob_end_clean(); + + assertTrue($data === $displayed, $prefix . 'display w/ lock'); + assertTrue($data === $cache->fetch($id), $prefix . 'fetch w/ lock'); } diff --git a/min_unit_tests/test_Minify_Cache_Memcache.php b/min_unit_tests/test_Minify_Cache_Memcache.php index b0223aa..2321045 100644 --- a/min_unit_tests/test_Minify_Cache_Memcache.php +++ b/min_unit_tests/test_Minify_Cache_Memcache.php @@ -6,33 +6,47 @@ require_once 'Minify/Cache/Memcache.php'; function test_Minify_Cache_Memcache() { $prefix = 'Minify_Cache_Memcache : '; + $thisFileActive = (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])); + if (! function_exists('memcache_set')) { + if ($thisFileActive) { + echo "NOTE: {$prefix}PHP lacks memcache support\n"; + } return; } $mc = new Memcache; if (! @$mc->connect('localhost', 11211)) { + if ($thisFileActive) { + echo "NOTE: {$prefix}Could not connect to localhost:11211\n"; + } return; } - $data = str_repeat(md5('testing'), 160); - $id = 'Minify_test_cache'; - + $data = str_repeat(md5(time()) . 'í', 100); // 3400 bytes in UTF-8 + $id = 'Minify_test_memcache'; $cache = new Minify_Cache_Memcache($mc); - + assertTrue(true === $cache->store($id, $data), $prefix . 'store'); - - assertTrue(strlen($data) === $cache->getSize($id), $prefix . 'getSize'); - + + assertTrue(countBytes($data) === $cache->getSize($id), $prefix . 'getSize'); + assertTrue(true === $cache->isValid($id, $_SERVER['REQUEST_TIME'] - 10), $prefix . 'isValid'); - + ob_start(); $cache->display($id); $displayed = ob_get_contents(); ob_end_clean(); - + assertTrue($data === $displayed, $prefix . 'display'); - + assertTrue($data === $cache->fetch($id), $prefix . 'fetch'); + + if (function_exists('gzencode')) { + $data = gzencode($data); + $id .= ".gz"; + $cache->store($id, $data); + assertTrue($data === $cache->fetch($id), $prefix . 'store/fetch gzencoded string'); + } } test_Minify_Cache_Memcache(); \ No newline at end of file diff --git a/min_unit_tests/test_Minify_Cache_ZendPlatform.php b/min_unit_tests/test_Minify_Cache_ZendPlatform.php new file mode 100644 index 0000000..9b0ef4d --- /dev/null +++ b/min_unit_tests/test_Minify_Cache_ZendPlatform.php @@ -0,0 +1,33 @@ +<?php +require_once '_inc.php'; + +require_once 'Minify/Cache/ZendPlatform.php'; + +function test_Minify_Cache_ZendPlatform() +{ + $prefix = 'Minify_Cache_ZendPlatform : '; + if (! function_exists('output_cache_put')) { + return; + } + $data = str_repeat(md5(time()) . 'í', 100); // 3400 bytes in UTF-8 + $id = 'Minify_test_cache'; + + $cache = new Minify_Cache_ZendPlatform(); + + assertTrue(true === $cache->store($id, $data), $prefix . 'store'); + + assertTrue(countBytes($data) === $cache->getSize($id), $prefix . 'getSize'); + + assertTrue(true === $cache->isValid($id, $_SERVER['REQUEST_TIME'] - 10), $prefix . 'isValid'); + + ob_start(); + $cache->display($id); + $displayed = ob_get_contents(); + ob_end_clean(); + + assertTrue($data === $displayed, $prefix . 'display'); + + assertTrue($data === $cache->fetch($id), $prefix . 'fetch'); +} + +test_Minify_Cache_ZendPlatform(); diff --git a/min_unit_tests/test_Minify_CommentPreserver.php b/min_unit_tests/test_Minify_CommentPreserver.php index e4e0e9f..fe1439a 100644 --- a/min_unit_tests/test_Minify_CommentPreserver.php +++ b/min_unit_tests/test_Minify_CommentPreserver.php @@ -9,19 +9,19 @@ function test_Minify_CommentPreserver() global $thisDir; $inOut = array( - '/*!*/' => "\n/**/\n" - ,'/*!*/a' => "\n/**/\n1A" - ,'a/*!*//*!*/b' => "2A\n/**/\n\n/**/\n3B" - ,'a/*!*/b/*!*/' => "4A\n/**/\n5B\n/**/\n" + '/*!*/' => "\n/*!*/\n" + ,'/*!*/a' => "\n/*!*/\n1A" + ,'a/*!*//*!*/b' => "2A\n/*!*/\n\n/*!*/\n3B" + ,'a/*!*/b/*!*/' => "4A\n/*!*/\n5B\n/*!*/\n" ); foreach ($inOut as $in => $expected) { $actual = Minify_CommentPreserver::process($in, '_test_MCP_processor'); $passed = assertTrue($expected === $actual, 'Minify_CommentPreserver'); if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - echo "\n---Output: " .strlen($actual). " bytes\n\n{$actual}\n\n"; + echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n"; if (!$passed) { - echo "---Expected: " .strlen($expected). " bytes\n\n{$expected}\n\n\n"; + echo "---Expected: " .countBytes($expected). " bytes\n\n{$expected}\n\n\n"; } } } diff --git a/min_unit_tests/test_Minify_HTML.php b/min_unit_tests/test_Minify_HTML.php index 69aeff5..fbc2a02 100644 --- a/min_unit_tests/test_Minify_HTML.php +++ b/min_unit_tests/test_Minify_HTML.php @@ -23,12 +23,12 @@ function test_HTML() if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { if ($passed) { - echo "\n---Source: ", strlen($src), " bytes\n" - , "---Output: ", strlen($minOutput), " bytes (", round($time * 1000), " ms)\n\n{$minOutput}\n\n\n"; + echo "\n---Source: ", countBytes($src), " bytes\n" + , "---Output: ", countBytes($minOutput), " bytes (", round($time * 1000), " ms)\n\n{$minOutput}\n\n\n"; } else { - echo "\n---Output: ", strlen($minOutput), " bytes (", round($time * 1000), " ms)\n\n{$minOutput}\n\n" - , "---Expected: ", strlen($minExpected), " bytes\n\n{$minExpected}\n\n" - , "---Source: ", strlen($src), " bytes\n\n{$src}\n\n\n"; + echo "\n---Output: ", countBytes($minOutput), " bytes (", round($time * 1000), " ms)\n\n{$minOutput}\n\n" + , "---Expected: ", countBytes($minExpected), " bytes\n\n{$minExpected}\n\n" + , "---Source: ", countBytes($src), " bytes\n\n{$src}\n\n\n"; } } @@ -46,12 +46,12 @@ function test_HTML() if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { if ($passed) { - echo "\n---Source: ", strlen($src), " bytes\n" - , "---Output: ", strlen($minOutput), " bytes (", round($time * 1000), " ms)\n\n{$minOutput}\n\n\n"; + echo "\n---Source: ", countBytes($src), " bytes\n" + , "---Output: ", countBytes($minOutput), " bytes (", round($time * 1000), " ms)\n\n{$minOutput}\n\n\n"; } else { - echo "\n---Output: ", strlen($minOutput), " bytes (", round($time * 1000), " ms)\n\n{$minOutput}\n\n" - , "---Expected: ", strlen($minExpected), " bytes\n\n{$minExpected}\n\n" - , "---Source: ", strlen($src), " bytes\n\n{$src}\n\n\n"; + echo "\n---Output: ", countBytes($minOutput), " bytes (", round($time * 1000), " ms)\n\n{$minOutput}\n\n" + , "---Expected: ", countBytes($minExpected), " bytes\n\n{$minExpected}\n\n" + , "---Source: ", countBytes($src), " bytes\n\n{$src}\n\n\n"; } } } diff --git a/min_unit_tests/test_Minify_HTML_Helper.php b/min_unit_tests/test_Minify_HTML_Helper.php new file mode 100644 index 0000000..19abb8b --- /dev/null +++ b/min_unit_tests/test_Minify_HTML_Helper.php @@ -0,0 +1,68 @@ +<?php +require_once '_inc.php'; + +require_once 'Minify/HTML/Helper.php'; + +function test_Minify_HTML_Helper() +{ + global $thisDir; + + $realDocRoot = $_SERVER['DOCUMENT_ROOT']; + $_SERVER['DOCUMENT_ROOT'] = $thisDir; + + $file1 = $thisDir . '/_test_files/css/paths_prepend.css'; + $file2 = $thisDir . '/_test_files/css/styles.css'; + $maxTime = max(filemtime($file1), filemtime($file2)); + + $uri1 = '//_test_files/css/paths_prepend.css'; + $uri2 = '//_test_files/css/styles.css'; + + $expected = "/min/b=_test_files/css&f=paths_prepend.css,styles.css&{$maxTime}"; + $actual = Minify_HTML_Helper::getUri(array($uri1, $uri2)); + $passed = assertTrue($actual === $expected, 'Minify_HTML_Helper : given URIs'); + + $expected = "/min/b=_test_files/css&f=paths_prepend.css,styles.css&{$maxTime}"; + $actual = Minify_HTML_Helper::getUri(array($file1, $file2)); + $passed = assertTrue($actual === $expected, 'Minify_HTML_Helper : given filepaths'); + + $expected = "/min/g=notRealGroup&debug"; + $actual = Minify_HTML_Helper::getUri('notRealGroup', array('debug' => true)); + $passed = assertTrue($actual === $expected, 'Minify_HTML_Helper : non-existent group & debug'); + + $expected = "/myApp/min/?g=css&{$maxTime}"; + $actual = Minify_HTML_Helper::getUri('css', array( + 'rewriteWorks' => false + ,'minAppUri' => '/myApp/min/' + ,'groupsConfigFile' => $thisDir . '/_test_files/htmlHelper_groupsConfig.php' + )); + $passed = assertTrue($actual === $expected, 'Minify_HTML_Helper : existing group'); + + $utilsFile = dirname(dirname(__FILE__)) . '/min/utils.php'; + if (is_file($utilsFile)) { + require_once $utilsFile; + + $fiveSecondsAgo = $_SERVER['REQUEST_TIME'] - 5; + $obj = new stdClass(); + $obj->lastModified = $fiveSecondsAgo; + + $output = Minify_mtime(array( + $uri1 + ,$uri2 + ,$obj + )); + $passed = assertTrue($output === $fiveSecondsAgo, 'utils.php : Minify_mtime w/ files & obj'); + + $obj = new stdClass(); + $obj->lastModified = strtotime('2000-01-01'); + $output = Minify_mtime(array( + $obj + ,'css' + ), $thisDir . '/_test_files/htmlHelper_groupsConfig.php'); + $passed = assertTrue($output === $maxTime, 'utils.php : Minify_mtime w/ obj & group'); + + } + + $_SERVER['DOCUMENT_ROOT'] = $realDocRoot; +} + +test_Minify_HTML_Helper(); \ No newline at end of file diff --git a/min_unit_tests/test_Minify_ImportProcessor.php b/min_unit_tests/test_Minify_ImportProcessor.php index 52b19ff..59abfca 100644 --- a/min_unit_tests/test_Minify_ImportProcessor.php +++ b/min_unit_tests/test_Minify_ImportProcessor.php @@ -27,18 +27,18 @@ function test_Minify_ImportProcessor() $passed = assertTrue($expected === $actual, 'ImportProcessor'); if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - echo "\n---Output: " .strlen($actual). " bytes\n\n{$actual}\n\n"; + echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n"; if (!$passed) { - echo "---Expected: " .strlen($expected). " bytes\n\n{$expected}\n\n\n"; + echo "---Expected: " .countBytes($expected). " bytes\n\n{$expected}\n\n\n"; } } - $expectedIncludes = array ( - realpath($linDir . '/input.css') - ,realpath($linDir . '/adjacent.css') - ,realpath($linDir . '/../css/styles.css') - ,realpath($linDir . '/1/tv.css') - ,realpath($linDir . '/1/adjacent.css') + $expectedIncludes = array ( + realpath($linDir . '/input.css') + ,realpath($linDir . '/adjacent.css') + ,realpath($linDir . '/../css/styles.css') + ,realpath($linDir . '/1/tv.css') + ,realpath($linDir . '/1/adjacent.css') ); $passed = assertTrue($expectedIncludes === Minify_ImportProcessor::$filesIncluded diff --git a/min_unit_tests/test_Minify_JS_ClosureCompiler.php b/min_unit_tests/test_Minify_JS_ClosureCompiler.php new file mode 100644 index 0000000..bc71324 --- /dev/null +++ b/min_unit_tests/test_Minify_JS_ClosureCompiler.php @@ -0,0 +1,49 @@ +<?php +require_once '_inc.php'; + +require_once 'Minify/JS/ClosureCompiler.php'; + +function test_Minify_JS_ClosureCompiler() +{ + global $thisDir; + + $src = " +(function (window, undefined){ + function addOne(input) { + return 1 + input; + } + window.addOne = addOne; + window.undefined = undefined; +})(window); + "; + $minExpected = "(function(a,b){a.addOne=function(a){return 1+a};a.undefined=b})(window);"; + $minOutput = Minify_JS_ClosureCompiler::minify($src); + if (false !== strpos($minOutput, 'Error(22): Too many compiles')) { + echo "!NOTE: Too many recent calls to Closure Compiler API to test.\n"; + return; + } + + + $passed = assertTrue($minExpected == $minOutput, 'Minify_JS_ClosureCompiler : Overall'); + if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { + echo "\n---Output: " .countBytes($minOutput). " bytes\n\n{$minOutput}\n\n"; + echo "---Expected: " .countBytes($minExpected). " bytes\n\n{$minExpected}\n\n"; + echo "---Source: " .countBytes($src). " bytes\n\n{$src}\n\n\n"; + } + + $src = "function blah({ return 'blah';} "; + $exc = null; + try { + $minOutput = Minify_JS_ClosureCompiler::minify($src); + } catch (Exception $e) { + $exc = $e; + } + $passed = assertTrue( + $exc instanceof Minify_JS_ClosureCompiler_Exception + , 'Minify_JS_ClosureCompiler : Throws Minify_JS_ClosureCompiler_Exception'); + if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { + echo "\n---Message: " . var_export($exc->getMessage(), 1) . "\n\n\n"; + } +} + +test_Minify_JS_ClosureCompiler(); diff --git a/min_unit_tests/test_Minify_Lines.php b/min_unit_tests/test_Minify_Lines.php index c518fce..40d531f 100644 --- a/min_unit_tests/test_Minify_Lines.php +++ b/min_unit_tests/test_Minify_Lines.php @@ -26,9 +26,9 @@ function test_Lines() $passed = assertTrue($exp === $ret['content'], 'Minify_Lines'); if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - echo "\n---Output: " .strlen($ret['content']). " bytes\n\n{$ret['content']}\n\n"; + echo "\n---Output: " .countBytes($ret['content']). " bytes\n\n{$ret['content']}\n\n"; if (!$passed) { - echo "---Expected: " .strlen($exp). " bytes\n\n{$exp}\n\n\n"; + echo "---Expected: " .countBytes($exp). " bytes\n\n{$exp}\n\n\n"; } } } diff --git a/min_unit_tests/test_all.php b/min_unit_tests/test_all.php index 3b7921d..ff26b51 100644 --- a/min_unit_tests/test_all.php +++ b/min_unit_tests/test_all.php @@ -2,11 +2,14 @@ require 'test_Minify.php'; require 'test_Minify_Build.php'; +require 'test_Minify_HTML_Helper.php'; require 'test_Minify_Cache_APC.php'; require 'test_Minify_Cache_File.php'; require 'test_Minify_Cache_Memcache.php'; +require 'test_Minify_Cache_ZendPlatform.php'; require 'test_Minify_CSS.php'; require 'test_Minify_CSS_UriRewriter.php'; +require 'test_Minify_JS_ClosureCompiler.php'; require 'test_Minify_CommentPreserver.php'; require 'test_Minify_HTML.php'; require 'test_Minify_ImportProcessor.php'; diff --git a/min_unit_tests/test_environment.php b/min_unit_tests/test_environment.php index f801569..3d4162c 100644 --- a/min_unit_tests/test_environment.php +++ b/min_unit_tests/test_environment.php @@ -1,5 +1,7 @@ <?php +//phpinfo(); exit(); + if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { // called directly if (isset($_GET['getOutputCompression'])) { @@ -9,6 +11,10 @@ if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { if (isset($_GET['hello'])) { // try to disable (may not work) ini_set('zlib.output_compression', '0'); + $type = ($_GET['hello'] == 'js') + ? 'application/x-javascript' + : "text/{$_GET['hello']}"; + header("Content-Type: {$type}"); echo 'World!'; exit(); } @@ -60,41 +66,73 @@ function test_environment() echo "!WARN: environment : zlib.output_compression is enabled in php.ini" . " or .htaccess.\n"; } - - $fp = fopen($thisUrl . '?hello=1', 'r', false, stream_context_create(array( - 'http' => array( - 'method' => "GET", - 'header' => "Accept-Encoding: deflate, gzip\r\n" - ) - ))); - - $meta = stream_get_meta_data($fp); - - $passed = true; - foreach ($meta['wrapper_data'] as $i => $header) { - if ((preg_match('@^Content-Length: (\\d+)$@i', $header, $m) && $m[1] !== '6') - || preg_match('@^Content-Encoding:@i', $header, $m) - ) { - $passed = false; - break; - } - } - if ($passed && stream_get_contents($fp) !== 'World!') { - $passed = false; - } - assertTrue( - $passed - ,'environment : PHP/server does not auto-HTTP-encode content' + + $testJs = _test_environment_getHello($thisUrl . '?hello=js'); + $passed = assertTrue( + $testJs['length'] == 6 + ,'environment : PHP/server should not auto-encode application/x-javascript output' ); - fclose($fp); - - if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { - if (! $passed) { - echo "\nReturned content should be 6 bytes and not HTTP encoded.\n" - . "Headers returned by: {$thisUrl}?hello=1\n\n"; - var_export($meta['wrapper_data']); + + $testCss = _test_environment_getHello($thisUrl . '?hello=css'); + $passed = $passed && assertTrue( + $testCss['length'] == 6 + ,'environment : PHP/server should not auto-encode text/css output' + ); + + $testHtml = _test_environment_getHello($thisUrl . '?hello=html'); + $passed = $passed && assertTrue( + $testHtml['length'] == 6 + ,'environment : PHP/server should not auto-encode text/html output' + ); + + if (! $passed) { + $testFake = _test_environment_getHello($thisUrl . '?hello=faketype'); + if ($testFake['length'] == 6) { + echo "!NOTE: environment : Server does not auto-encode arbitrary types. This\n" + . " may indicate that the auto-encoding is caused by Apache's \n" + . " AddOutputFilterByType."; } } } +function _test_environment_getHello($url) +{ + $fp = fopen($url, 'r', false, stream_context_create(array( + 'http' => array( + 'method' => "GET", + 'timeout' => '10', + 'header' => "Accept-Encoding: deflate, gzip\r\n", + ) + ))); + $meta = stream_get_meta_data($fp); + $encoding = ''; + $length = 0; + foreach ($meta['wrapper_data'] as $i => $header) { + if (preg_match('@^Content-Length:\\s*(\\d+)$@i', $header, $m)) { + $length = $m[1]; + } elseif (preg_match('@^Content-Encoding:\\s*(\\S+)$@i', $header, $m)) { + if ($m[1] !== 'identity') { + $encoding = $m[1]; + } + } + } + $streamContents = stream_get_contents($fp); + fclose($fp); + + if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { + if ($length != 6) { + echo "\nReturned content should be 6 bytes and not HTTP encoded.\n" + . "Headers returned by: {$url}\n\n"; + var_export($meta['wrapper_data']); + echo "\n\n"; + } + } + + return array( + 'length' => $length + ,'encoding' => $encoding + ,'bytes' => $streamContents + ); +} + test_environment();