+++ /dev/null
-The files in this directory represent the default Minify setup designed to ease
-integration with your site. This app will combine and minify your Javascript or
-CSS files and serve them with HTTP compression and cache headers.
-
-
-RECOMMENDED
-
-It's recommended to edit config.php to set $min_cachePath to a writeable
-(by PHP) directory on your system. This will improve performance.
-
-
-GETTING STARTED
-
-The quickest way to get started is to use the Minify URI Builder application
-on your website: http://example.com/min/builder/
-
-
-MINIFYING A SINGLE FILE
-
-Let's say you want to serve this file:
- http://example.com/wp-content/themes/default/default.css
-
-Here's the "Minify URL" for this file:
- http://example.com/min/?f=wp-content/themes/default/default.css
-
-In other words, the "f" argument is set to the file path from root without the
-initial "/". As CSS files may contain relative URIs, Minify will automatically
-"fix" these by rewriting them as root relative.
-
-
-COMBINING MULTIPLE FILES IN ONE DOWNLOAD
-
-Separate the paths given to "f" with commas.
-
-Let's say you have CSS files at these URLs:
- http://example.com/scripts/jquery-1.2.6.js
- http://example.com/scripts/site.js
-
-You can combine these files through Minify by requesting this URL:
- http://example.com/min/?f=scripts/jquery-1.2.6.js,scripts/site.js
-
-
-SIMPLIFYING URLS WITH A BASE PATH
-
-If you're combining files that share the same ancestor directory, you can use
-the "b" argument to set the base directory for the "f" argument. Do not include
-the leading or trailing "/" characters.
-
-E.g., the following URLs will serve the exact same content:
- http://example.com/min/?f=scripts/jquery-1.2.6.js,scripts/site.js,scripts/home.js
- http://example.com/min/?b=scripts&f=jquery-1.2.6.js,site.js,home.js
-
-
-MINIFY URLS IN HTML
-
-In (X)HTML files, don't forget to replace any "&" characters with "&".
-
-
-SPECIFYING ALLOWED DIRECTORIES
-
-By default, Minify will serve any *.css/*.js files within the DOCUMENT_ROOT. If
-you'd prefer to limit Minify's access to certain directories, set the
-$min_serveOptions['minApp']['allowDirs'] array in config.php. E.g. to limit
-to the /js and /themes/default directories, use:
-
-$min_serveOptions['minApp']['allowDirs'] = array('//js', '//themes/default');
-
-
-GROUPS: FASTER PERFORMANCE AND BETTER URLS
-
-For the best performance, 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')
-);
-
-This pre-selects the following files to be combined under the key "js":
- http://example.com/js/Class.js
- http://example.com/js/email.js
-
-You can now serve these files with this simple URL:
- http://example.com/min/?g=js
-
-
-GROUPS: SPECIFYING FILES OUTSIDE THE DOC_ROOT
-
-In the groupsConfig.php array, the "//" in the file paths is a shortcut for
-the DOCUMENT_ROOT, but you can also specify paths from the root of the filesystem
-or relative to the DOC_ROOT:
-
-return array(
- 'js' => array(
- '//js/file.js' // file within DOC_ROOT
- ,'//../file.js' // file in parent directory of DOC_ROOT
- ,'C:/Users/Steve/file.js' // file anywhere on filesystem
- )
-);
-
-
-FAR-FUTURE EXPIRES HEADERS
-
-Minify can send far-future (one year) Expires headers. To enable this you must
-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.:
-
-<?php
-// add /min/lib to your include_path first!
-require $_SERVER['DOCUMENT_ROOT'] . '/min/utils.php';
-
-$jsUri = Minify_groupUri('js');
-echo "<script type='text/javascript' src='{$jsUri}'></script>";
-
-
-DEBUG MODE
-
-In debug mode, instead of compressing files, Minify sends combined files with
-comments prepended to each line to show the line number in the original source
-file. To enable this, set $min_allowDebugFlag to true in config.php and append
-"&debug=1" to your URIs. E.g. /min/?f=script1.js,script2.js&debug=1
-
-Known issue: files with comment-like strings/regexps can cause problems in this mode.
-
-
-QUESTIONS?
-
-http://groups.google.com/group/minify
+++ /dev/null
-var MUB = {
- _uid : 0
- ,_minRoot : '/min/?'
- ,checkRewrite : function () {
- var testUri = location.pathname.replace(/\/[^\/]*$/, '/rewriteTest.js').substr(1);
- function fail() {
- $('#minRewriteFailed')[0].className = 'topNote';
- };
- $.ajax({
- url : '../f=' + testUri + '&' + (new Date()).getTime()
- ,success : function (data) {
- if (data === '1') {
- MUB._minRoot = '/min/';
- $('span.minRoot').html('/min/');
- } else
- fail();
- }
- ,error : fail
- });
- }
- /**
- * Get markup for new source LI element
- */
- ,newLi : function () {
- return '<li id="li' + MUB._uid + '">http://' + location.host + '/<input type=text size=20>'
- + ' <button title="Remove">x</button> <button title="Include Earlier">↑</button>'
- + ' <button title="Include Later">↓</button> <span></span></li>';
- }
- /**
- * Add new empty source LI and attach handlers to buttons
- */
- ,addLi : function () {
- $('#sources').append(MUB.newLi());
- var li = $('#li' + MUB._uid)[0];
- $('button[title=Remove]', li).click(function () {
- $('#results').hide();
- var hadValue = !!$('input', li)[0].value;
- $(li).remove();
- });
- $('button[title$=Earlier]', li).click(function () {
- $(li).prev('li').find('input').each(function () {
- $('#results').hide();
- // this = previous li input
- var tmp = this.value;
- this.value = $('input', li).val();
- $('input', li).val(tmp);
- MUB.updateAllTestLinks();
- });
- });
- $('button[title$=Later]', li).click(function () {
- $(li).next('li').find('input').each(function () {
- $('#results').hide();
- // this = next li input
- var tmp = this.value;
- this.value = $('input', li).val();
- $('input', li).val(tmp);
- MUB.updateAllTestLinks();
- });
- });
- ++MUB._uid;
- }
- /**
- * In the context of a source LI element, this will analyze the URI in
- * the INPUT and check the URL on the site.
- */
- ,liUpdateTestLink : function () { // call in context of li element
- if (! $('input', this)[0].value)
- return;
- var li = this;
- $('span', this).html('');
- var url = 'http://' + location.host + '/'
- + $('input', this)[0].value.replace(/^\//, '');
- $.ajax({
- url : url
- ,complete : function (xhr, stat) {
- if ('success' == stat)
- $('span', li).html('✓');
- else {
- $('span', li).html('<button><b>404! </b> recheck</button>')
- .find('button').click(function () {
- MUB.liUpdateTestLink.call(li);
- });
- }
- }
- ,dataType : 'text'
- });
- }
- /**
- * Check all source URLs
- */
- ,updateAllTestLinks : function () {
- $('#sources li').each(MUB.liUpdateTestLink);
- }
- /**
- * In a given array of strings, find the character they all have at
- * a particular index
- * @param Array arr array of strings
- * @param Number pos index to check
- * @return mixed a common char or '' if any do not match
- */
- ,getCommonCharAtPos : function (arr, pos) {
- var i
- ,l = arr.length
- ,c = arr[0].charAt(pos);
- if (c === '' || l === 1)
- return c;
- for (i = 1; i < l; ++i)
- if (arr[i].charAt(pos) !== c)
- return '';
- return c;
- }
- /**
- * Get the shortest URI to minify the set of source files
- * @param Array sources URIs
- */
- ,getBestUri : function (sources) {
- var pos = 0
- ,base = ''
- ,c;
- while (true) {
- c = MUB.getCommonCharAtPos(sources, pos);
- if (c === '')
- break;
- else
- base += c;
- ++pos;
- }
- base = base.replace(/[^\/]+$/, '');
- var uri = MUB._minRoot + 'f=' + sources.join(',');
- if (base.charAt(base.length - 1) === '/') {
- // we have a base dir!
- var basedSources = sources
- ,i
- ,l = sources.length;
- for (i = 0; i < l; ++i) {
- basedSources[i] = sources[i].substr(base.length);
- }
- base = base.substr(0, base.length - 1);
- var bUri = MUB._minRoot + 'b=' + base + '&f=' + basedSources.join(',');
- //window.console && console.log([uri, bUri]);
- uri = uri.length < bUri.length
- ? uri
- : bUri;
- }
- return uri;
- }
- /**
- * Create the Minify URI for the sources
- */
- ,update : function () {
- MUB.updateAllTestLinks();
- var sources = []
- ,ext = false
- ,fail = false;
- $('#sources input').each(function () {
- var m, val;
- if (! fail && this.value && (m = this.value.match(/\.(css|js)$/))) {
- var thisExt = m[1];
- if (ext === false)
- ext = thisExt;
- else if (thisExt !== ext) {
- fail = true;
- return alert('extensions must match!');
- }
- this.value = this.value.replace(/^\//, '');
- if (-1 != $.inArray(this.value, sources)) {
- fail = true;
- return alert('duplicate file!');
- }
- sources.push(this.value);
- }
- });
- if (fail || ! sources.length)
- return;
- $('#groupConfig').val(" 'keyName' => array('//" + sources.join("', '//") + "'),");
- var uri = MUB.getBestUri(sources)
- ,uriH = uri.replace(/</, '<').replace(/>/, '>').replace(/&/, '&');
- $('#uriA').html(uriH)[0].href = uri;
- $('#uriHtml').val(
- ext === 'js'
- ? '<script type="text/javascript" src="' + uriH + '"></script>'
- : '<link type="text/css" rel="stylesheet" href="' + uriH + '" />'
- );
- $('#results').show();
- }
- /**
- * Handler for the "Add file +" button
- */
- ,addButtonClick : function () {
- $('#results').hide();
- MUB.addLi();
- MUB.updateAllTestLinks();
- $('#update').show().click(MUB.update);
- $('#sources li:last input')[0].focus();
- }
- /**
- * Runs on DOMready
- */
- ,init : function () {
- $('#app').show();
- $('#sources').html('');
- $('#add button').click(MUB.addButtonClick);
- // make easier to copy text out of
- $('#uriHtml, #groupConfig').click(function () {
- this.select();
- }).focus(function () {
- this.select();
- });
- $('a.ext').attr({target:'_blank'});
- if (location.hash) {
- // make links out of URIs from bookmarklet
- $('#getBm').hide();
- $('#bmUris').html('<p><strong>Found by bookmarklet:</strong> /<a href=#>'
- + location.hash.substr(1).split(',').join('</a> | /<a href=#>')
- + '</a></p>'
- );
- $('#bmUris a').click(function () {
- MUB.addButtonClick();
- $('#sources li:last input').val(this.innerHTML)
- MUB.liUpdateTestLink.call($('#sources li:last')[0]);
- $('#results').hide();
- return false;
- }).attr({title:'Add file +'});
- } else {
- // copy bookmarklet code into href
- var bmUri = location.pathname.replace(/\/[^\/]*$/, '/bm.js').substr(1);
- $.ajax({
- url : '../?f=' + bmUri
- ,success : function (code) {
- $('#bm')[0].href = code
- .replace('%BUILDER_URL%', location.href)
- .replace(/\n/g, ' ');
- }
- ,dataType : 'text'
- });
- $.browser.msie && $('#getBm p:last').append(' Sorry, not supported in MSIE!');
- MUB.addButtonClick();
- }
- MUB.checkRewrite();
- }
-};
-window.onload = MUB.init;
+++ /dev/null
-javascript:(function() {
- var d = document
- ,uris = []
- ,i = 0
- ,o
- ,home = (location + '').split('/').splice(0, 3).join('/') + '/';
- function add(uri) {
- return (0 === uri.indexOf(home))
- && (!/[\?&]/.test(uri))
- && uris.push(escape(uri.substr(home.length)));
- };
- function sheet(ss) {
- // we must check the domain with add() before accessing ss.cssRules
- // otherwise a security exception will be thrown
- if (ss.href && add(ss.href) && ss.cssRules) {
- var i = 0, r;
- while (r = ss.cssRules[i++])
- r.styleSheet && sheet(r.styleSheet);
- }
- };
- while (o = d.getElementsByTagName('script')[i++])
- o.src && !(o.type && /vbs/i.test(o.type)) && add(o.src);
- i = 0;
- while (o = d.styleSheets[i++])
- /* http://www.w3.org/TR/DOM-Level-2-Style/stylesheets.html#StyleSheets-DocumentStyle-styleSheets
- document.styleSheet is a list property where [0] accesses the 1st element and
- [outOfRange] returns null. In IE, styleSheets is a function, and also throws an
- exception when you check the out of bounds index. (sigh) */
- sheet(o);
- if (uris.length)
- window.open('%BUILDER_URL%#' + uris.join(','));
- else
- alert('No js/css files found with URLs within "'
- + home.split('/')[2]
- + '".\n(This tool is limited to URLs with the same domain.)');
-})();
+++ /dev/null
-<?php
-
-if (phpversion() < 5) {
- exit('Minify requires PHP5 or greater.');
-}
-
-// check for auto-encoding
-$encodeOutput = (function_exists('gzdeflate')
- && !ini_get('zlib.output_compression'));
-
-require dirname(__FILE__) . '/../config.php';
-
-if (! $min_enableBuilder) {
- header('Location: /');
- exit();
-}
-
-ob_start();
-?>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<head>
- <meta name="ROBOTS" content="NOINDEX, NOFOLLOW">
- <title>Minify URI Builder</title>
- <style type="text/css">
-body {margin:1em 60px;}
-h1, h2, h3 {margin-left:-25px; position:relative;}
-h1 {margin-top:0;}
-#sources {margin:0; padding:0;}
-#sources li {margin:0 0 0 40px}
-#sources li input {margin-left:2px}
-#add {margin:5px 0 1em 40px}
-.hide {display:none}
-#uriTable {border-collapse:collapse;}
-#uriTable td, #uriTable th {padding-top:10px;}
-#uriTable th {padding-right:10px;}
-#groupConfig {font-family:monospace;}
-b {color:#c00}
-.topNote {background: #ff9; display:inline-block; padding:.5em .6em; margin:0 0 1em;}
-.topWarning {background:#c00; color:#fff; padding:.5em .6em; margin:0 0 1em;}
- </style>
-</head>
-
-<?php if (! isset($min_cachePath)): ?>
-<p class=topNote><strong>Note:</strong> Please set <code>$min_cachePath</code>
-in /min/config.php to improve performance.</p>
-<?php endIf; ?>
-
-<p id=minRewriteFailed class="hide"><strong>Note:</strong> Your webserver does not seem to
- support mod_rewrite (used in /min/.htaccess). Your Minify URIs will contain "?", which
-<a href="http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/"
->may reduce the benefit of proxy cache servers</a>.</p>
-
-<h1>Minify URI Builder</h1>
-
-<noscript><p class="topNote">Javascript and a browser supported by jQuery 1.2.6 is required
-for this application.</p></noscript>
-
-<div id=app class=hide>
-
-<p>Create a list of Javascript or CSS files (or 1 is fine) you'd like to combine
-and click [Update].</p>
-
-<ol id=sources><li></li></ol>
-<div id=add><button>Add file +</button></div>
-
-<div id=bmUris></div>
-
-<p><button id=update class=hide>Update</button></p>
-
-<div id=results class=hide>
-
-<h2>Minify URI</h2>
-<p>Place this URI in your HTML to serve the files above combined, minified, compressed and
-with cache headers.</p>
-<table id=uriTable>
- <tr><th>URI</th><td><a id=uriA class=ext>/min</a> <small>(opens in new window)</small></td></tr>
- <tr><th>HTML</th><td><input id=uriHtml type=text size=100 readonly></td></tr>
-</table>
-
-<h2>How to serve these files as a group</h2>
-<p>For the best performance you can serve these files as a pre-defined group with a URI
-like: <code><span class=minRoot>/min/?</span>g=keyName</code></p>
-<p>To do this, add a line like this to /min/groupsConfig.php:</p>
-
-<pre><code>return array(
- <span style="color:#666">... your existing groups here ...</span>
-<input id=groupConfig size=100 type=text readonly>
-);</code></pre>
-
-<p><em>Make sure to replace <code>keyName</code> with a unique key for this group.</em></p>
-</div>
-
-<div id=getBm>
-<h3>Find URIs on a Page</h3>
-<p>You can use the bookmarklet below to fetch all CSS & Javascript URIs from a page
-on your site. When you active it, this page will open in a new window with a list of
-available URIs to add.</p>
-
-<p><a id=bm>Create Minify URIs</a> <small>(right-click, add to bookmarks)</small></p>
-</div>
-
-<h3>Combining CSS files that contain <code>@import</code></h3>
-<p>If your CSS files contain <code>@import</code> declarations, Minify will not
-remove them. Therefore, you will want to remove those that point to files already
-in your list, and move any others to the top of the first file in your list
-(imports below any styles will be ignored by browsers as invalid).</p>
-<p>If you desire, you can use Minify URIs in imports and they will not be touched
-by Minify. E.g. <code>@import "<span class=minRoot>/min/?</span>g=css2";</code></p>
-
-</div><!-- #app -->
-
-<hr>
-<p>Need help? Search or post to the <a class=ext
-href="http://groups.google.com/group/minify">Minify discussion list</a>.</p>
-<p><small>This app is minified :) <a class=ext
-href="http://code.google.com/p/minify/source/browse/trunk/min/builder/index.php">view
-source</a></small></p>
-
-<script type="text/javascript"
-src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js"></script>
-
-<script type="text/javascript">
-$(function () {
- // detection of double output encoding
- var msg = '<\p class=topWarning><\strong>Warning:<\/strong> ';
- var url = 'ocCheck.php?' + (new Date()).getTime();
- $.get(url, function (ocStatus) {
- $.get(url + '&hello=1', function (ocHello) {
- if (ocHello != 'World!') {
- msg += 'It appears output is being automatically compressed, interfering '
- + ' with Minify\'s own compression. ';
- if (ocStatus == '1')
- msg += 'The option "zlib.output_compression" is enabled in your PHP configuration. '
- + 'Minify set this to "0", but it had no effect. This option must be disabled '
- + 'in php.ini or .htaccess.';
- else
- msg += 'The option "zlib.output_compression" is disabled in your PHP configuration '
- + 'so this behavior is likely due to a server option.';
- $(document.body).prepend(msg + '<\/p>');
- } else
- if (ocStatus == '1')
- $(document.body).prepend('<\p class=topNote><\strong>Note:</\strong> The option '
- + '"zlib.output_compression" is enabled in your PHP configuration, but has been '
- + 'successfully disabled via ini_set(). If you experience mangled output you '
- + 'may want to consider disabling this option in your PHP configuration.<\/p>'
- );
- });
- });
-});
-</script>
-<script type="text/javascript">
- // workaround required to test when /min isn't child of web root
- var src = location.pathname.replace(/\/[^\/]*$/, '/_index.js').substr(1);
- document.write('<\script type="text/javascript" src="../?f=' + src + '"><\/script>');
-</script>
-
-<?php
-
-$serveOpts = array(
- 'content' => 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';
-
-if (0 === stripos(PHP_OS, 'win')) {
- Minify::setDocRoot(); // we may be on IIS
-}
-Minify::setCache(isset($min_cachePath) ? $min_cachePath : null);
-Minify::$uploaderHoursBehind = $min_uploaderHoursBehind;
-
-Minify::serve('Page', $serveOpts);
+++ /dev/null
-<?php
-/**
- * AJAX checks for zlib.output_compression
- *
- * @package Minify
- */
-
-$_oc = ini_get('zlib.output_compression');
-
-// allow access only if builder is enabled
-require dirname(__FILE__) . '/../config.php';
-if (! $min_enableBuilder) {
- header('Location: /');
- exit();
-}
-
-if (isset($_GET['hello'])) {
- // echo 'World!'
-
- // try to prevent double encoding (may not have an effect)
- ini_set('zlib.output_compression', '0');
-
- require $min_libPath . '/HTTP/Encoder.php';
- HTTP_Encoder::$encodeToIe6 = true; // just in case
- $he = new HTTP_Encoder(array(
- 'content' => 'World!'
- ,'method' => 'deflate'
- ));
- $he->encode();
- $he->sendAll();
-
-} else {
- // echo status "0" or "1"
- header('Content-Type: text/plain');
- echo (int)$_oc;
-}
<?php
/**
- * Configuration for default Minify application
+ * Configuration for "min", the default application built with the Minify
+ * library
+ *
* @package Minify
*/
-defined('MOODLE_INTERNAL') || die();
+
+defined('MOODLE_INTERNAL') || die(); // start of moodle modification
+
+// NOTE: Copy all necessary settings here, do not modify the rest.
+// Minifier can not be accessed directly, only use PHP api.
+
+$min_enableBuilder = false;
+$min_errorLogger = false;
+$min_allowDebugFlag = debugging('', DEBUG_DEVELOPER);
+$min_cachePath = $CFG->tempdir;
+$min_documentRoot = $CFG->dirroot.'/lib/minify';
+$min_cacheFileLocking = true;
+$min_serveOptions['bubbleCssImports'] = false;
+$min_serveOptions['maxAge'] = 1800;
+$min_serveOptions['minApp']['groupsOnly'] = true;
+$min_symlinks = array();
+$min_uploaderHoursBehind = 0;
+$min_libPath = dirname(__FILE__) . '/lib';
+// do not change zlib compression or buffering here
+
+// TODO: locking setting, caching setting
+
+return; // end of moodle modification
+
/**
- * In 'debug' mode, Minify can combine files with no minification and
- * add comments to indicate line #s of the original files.
- *
- * To allow debugging, set this option to true and add "&debug=1" to
- * a URI. E.g. /min/?f=script1.js,script2.js&debug=1
- */
-$min_allowDebugFlag = ($CFG->debug);
+ * Allow use of the Minify URI Builder app. Only set this to true while you need it.
+ **/
+$min_enableBuilder = true;
/**
*
* If you want to use a custom error logger, set this to your logger
* instance. Your object should have a method log(string $message).
- *
- * @todo cache system does not have error logging yet.
*/
$min_errorLogger = false;
/**
- * Allow use of the Minify URI Builder app. If you no longer need
- * this, set to false.
- **/
-$min_enableBuilder = false;
+ * To allow debug mode output, you must set this option to true.
+ *
+ * Once true, you can send the cookie minDebug to request debug mode output. The
+ * cookie value should match the URIs you'd like to debug. E.g. to debug
+ * /min/f=file1.js send the cookie minDebug=file1.js
+ * You can manually enable debugging by appending "&debug" to a URI.
+ * E.g. /min/?f=script1.js,script2.js&debug
+ *
+ * In 'debug' mode, Minify combines files with no minification and adds comments
+ * to indicate line #s of the original files.
+ */
+$min_allowDebugFlag = false;
/**
* For best performance, specify your temp directory here. Otherwise Minify
* will have to load extra code to guess. Some examples below:
*/
-$min_cachePath = $CFG->tempdir.'';
+//$min_cachePath = 'c:\\WINDOWS\\Temp';
+//$min_cachePath = '/tmp';
+//$min_cachePath = preg_replace('/^\\d+;/', '', session_save_path());
+/**
+ * To use APC/Memcache/ZendPlatform for cache storage, require the class and
+ * set $min_cachePath to an instance. Example below:
+ */
+//require dirname(__FILE__) . '/lib/Minify/Cache/APC.php';
+//$min_cachePath = new Minify_Cache_APC();
/**
* If /min/ is directly inside your document root, just uncomment the
* second line. The third line might work on some Apache servers.
*/
-$min_documentRoot = $CFG->dirroot.'/lib/minify';
-//$min_documentRoot = substr(__FILE__, 0, strlen(__FILE__) - 15);
+$min_documentRoot = '';
+//$min_documentRoot = substr(__FILE__, 0, -15);
//$min_documentRoot = $_SERVER['SUBDOMAIN_DOCUMENT_ROOT'];
/**
- * Maximum age of browser cache in seconds. After this period, the browser
- * will send another conditional GET. Use a longer period for lower traffic
- * but you may want to shorten this before making changes if it's crucial
+ * Cache-Control: max-age value sent to browser (in seconds). After this period,
+ * the browser will send another conditional GET. Use a longer period for lower
+ * traffic but you may want to shorten this before making changes if it's crucial
* those changes are seen immediately.
*
* Note: Despite this setting, if you include a number at the end of the
$min_serveOptions['maxAge'] = 1800;
+/**
+ * To use Google's Closure Compiler API (falling back to JSMin on failure),
+ * uncomment the following lines:
+ */
+/*function closureCompiler($js) {
+ require_once 'Minify/JS/ClosureCompiler.php';
+ return Minify_JS_ClosureCompiler::minify($js);
+}
+$min_serveOptions['minifiers']['application/x-javascript'] = 'closureCompiler';
+//*/
+
+
/**
* If you'd like to restrict the "f" option to files within/below
* particular directories below DOCUMENT_ROOT, set this here.
* Set to true to disable the "f" GET parameter for specifying files.
* Only the "g" parameter will be considered.
*/
-$min_serveOptions['minApp']['groupsOnly'] = true;
+$min_serveOptions['minApp']['groupsOnly'] = false;
+
/**
- * Maximum # of files that can be specified in the "f" GET parameter
+ * By default, Minify will not minify files with names containing .min or -min
+ * before the extension. E.g. myFile.min.js will not be processed by JSMin
+ *
+ * To minify all files, set this option to null. You could also specify your
+ * own pattern that is matched against the filename.
*/
-$min_serveOptions['minApp']['maxFiles'] = 10;
+//$min_serveOptions['minApp']['noMinPattern'] = '@[-\\.]min\\.(?:js|css)$@i';
/**
* Path to Minify's lib folder. If you happen to move it, change
* this accordingly.
*/
-$min_libPath = $CFG->libdir . '/minify/lib';
+$min_libPath = dirname(__FILE__) . '/lib';
// try to disable output_compression (may not have an effect)
/**
* You may wish to use the Minify URI Builder app to suggest
* changes. http://yourdomain/min/builder/
+ *
+ * See http://code.google.com/p/minify/wiki/CustomSource for other ideas
**/
return array(
// 'js' => 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
* @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
* @author Christoph Dorn <christoph@christophdorn.com>
* @copyright 2005 Michal Migurski
- * @version CVS: $Id$
+ * @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
* @license http://www.opensource.org/licenses/bsd-license.php
* @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198
*/
/**
* @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.
* seconds, and also set the Expires header to the equivalent GMT date.
* After the max-age period has passed, the browser will again send a
* conditional GET to revalidate its cache.
- *
- * @return null
*/
public function __construct($spec)
{
} 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
{
$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) {
* "private" will be sent, allowing only browser caching.
*
* @param array $options (default empty) additional options for constructor
- *
- * @return null
*/
public static function check($lastModifiedTime = null, $isPublic = false, $options = array())
{
protected $_lmTime = null;
protected $_etag = null;
protected $_stripEtag = false;
-
+
+ /**
+ * @param string $hash
+ *
+ * @param string $scope
+ */
protected function _setEtag($hash, $scope)
{
$this->_etag = '"' . substr($scope, 0, 3) . $hash . '"';
$this->_headers['ETag'] = $this->_etag;
}
+ /**
+ * @param int $time
+ */
protected function _setLastModified($time)
{
$this->_lmTime = (int)$time;
/**
* Determine validity of client cache and queue 304 header if valid
+ *
+ * @return bool
*/
protected function _isCacheValid()
{
return $isValid;
}
+ /**
+ * @return bool
+ */
protected function resourceMatchedEtag()
{
if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
}
return false;
}
-
+
+ /**
+ * @param string $etag
+ *
+ * @return string
+ */
protected function normalizeEtag($etag) {
$etag = trim($etag);
return $this->_stripEtag
: $etag;
}
+ /**
+ * @return bool
+ */
protected function resourceNotModified()
{
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);
*
* @var bool
*/
- public static $encodeToIe6 = false;
+ public static $encodeToIe6 = true;
/**
* method. If not set, the best method will be chosen by getAcceptedEncoding()
* The available methods are 'gzip', 'deflate', 'compress', and '' (no
* encoding)
- *
- * @return null
*/
public function __construct($spec)
{
+ $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'];
}
*
* Call after encode() for encoded content.
*
- * return string
+ * @return string
*/
public function getContent()
{
* not handled purposefully.
*
* @see getHeaders()
- *
- * @return null
*/
public function sendHeaders()
{
* You must call this before headers are sent and it probably cannot be
* used in conjunction with zlib output buffering / mod_gzip. Errors are
* not handled purposefully.
- *
- * @return null
*/
public function sendAll()
{
// @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])
- || self::_isBuggyIe())
+ || self::isBuggyIe())
{
return array('', '');
}
*/
public function encode($compressionLevel = null)
{
- $this->_headers['Vary'] = 'Accept-Encoding';
+ if (! self::isBuggyIe()) {
+ $this->_headers['Vary'] = 'Accept-Encoding';
+ }
if (null === $compressionLevel) {
$compressionLevel = self::$compressionLevel;
}
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;
$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()
{
+ if (empty($_SERVER['HTTP_USER_AGENT'])) {
+ return false;
+ }
$ua = $_SERVER['HTTP_USER_AGENT'];
// quick escape for non-IEs
if (0 !== strpos($ua, 'Mozilla/4.0 (compatible; MSIE ')
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;
}
<?php
/**
- * jsmin.php - PHP implementation of Douglas Crockford's JSMin.
+ * JSMin.php - modified PHP implementation of Douglas Crockford's JSMin.
*
- * 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.
+ * <code>
+ * $minifiedJs = JSMin::minify($js);
+ * </code>
+ *
+ * This is a modified port of jsmin.c. Improvements:
+ *
+ * Does not choke on some regexp literals containing quote characters. E.g. /'/
*
- * 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.
+ * Spaces are preserved after some add/sub operators, so they are not mistakenly
+ * converted to post-inc/dec. E.g. a + ++b -> a+ ++b
*
+ * Preserves multi-line comments that begin with /*!
+ *
* PHP 5 or higher is required.
*
* Permission is hereby granted to use this version of the library under the
const ACTION_KEEP_A = 1;
const ACTION_DELETE_A = 2;
const ACTION_DELETE_A_B = 3;
-
+
protected $a = "\n";
protected $b = '';
protected $input = '';
protected $inputLength = 0;
protected $lookAhead = null;
protected $output = '';
-
+ protected $lastByteOut = '';
+
/**
- * Minify Javascript
+ * Minify Javascript.
*
* @param string $js Javascript to be minified
+ *
* @return string
*/
public static function minify($js)
$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;
}
-
+
/**
* Perform minification, return result
+ *
+ * @return string
*/
public function min()
{
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
if ($this->a === ' ') {
- if (! $this->isAlphaNum($this->b)) {
+ if (($this->lastByteOut === '+' || $this->lastByteOut === '-')
+ && ($this->b === $this->lastByteOut)) {
+ // Don't delete this space. If we do, the addition/subtraction
+ // could be parsed as a post-increment
+ } elseif (! $this->isAlphaNum($this->b)) {
$command = self::ACTION_DELETE_A;
}
} 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)) {
$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.
* ACTION_DELETE_A_B = Get the next B.
+ *
+ * @param int $command
+ * @throws JSMin_UnterminatedRegExpException|JSMin_UnterminatedStringException
*/
protected function action($command)
{
+ if ($command === self::ACTION_DELETE_A_B
+ && $this->b === ' '
+ && ($this->a === '+' || $this->a === '-')) {
+ // Note: we're at an addition/substraction operator; the inputIndex
+ // will certainly be a valid index
+ if ($this->input[$this->inputIndex] === $this->a) {
+ // This is "+ +" or "- -". Don't delete the space.
+ $command = self::ACTION_KEEP_A;
+ }
+ }
switch ($command) {
case self::ACTION_KEEP_A:
$this->output .= $this->a;
+ $this->lastByteOut = $this->a;
+
// fallthrough
case self::ACTION_DELETE_A:
$this->a = $this->b;
$str = $this->a; // in case needed for exception
while (true) {
$this->output .= $this->a;
+ $this->lastByteOut = $this->a;
+
$this->a = $this->get();
if ($this->a === $this->b) { // end quote
break;
}
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 === '\\') {
$this->output .= $this->a;
+ $this->lastByteOut = $this->a;
+
$this->a = $this->get();
$str .= $this->a;
}
$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;
+ $this->lastByteOut = $this->a;
}
$this->b = $this->next();
}
// end case ACTION_DELETE_A_B
}
}
-
+
+ /**
+ * @return bool
+ */
protected function isRegexpLiteral()
{
if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing
}
return false;
}
-
+
/**
* Get next char. Convert ctrl char to space.
+ *
+ * @return string
*/
protected function get()
{
}
return $c;
}
-
+
/**
* Get next char. If is ctrl character, translate to a space or newline.
+ *
+ * @return string
*/
protected function peek()
{
$this->lookAhead = $this->get();
return $this->lookAhead;
}
-
+
/**
* Is $c a letter, digit, underscore, dollar sign, escape, or non-ASCII?
+ *
+ * @param string $c
+ *
+ * @return bool
*/
protected function isAlphaNum($c)
{
return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126);
}
-
+
+ /**
+ * @return string
+ */
protected function singleLineComment()
{
$comment = '';
}
}
}
-
+
+ /**
+ * @return string
+ * @throws JSMin_UnterminatedCommentException
+ */
protected function multipleLineComment()
{
$this->get();
$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)) {
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.
+ *
+ * @return string
*/
protected function next()
{
<?php
/**
- * JSMinPlus version 1.1
+ * JSMinPlus version 1.4
*
* Minifies a javascript file using a javascript parser
*
* Usage: $minified = JSMinPlus::minify($script [, $filename])
*
* Versionlog (see also changelog.txt):
+ * 23-07-2011 - remove dynamic creation of OP_* and KEYWORD_* defines and declare them on top
+ * reduce memory footprint by minifying by block-scope
+ * some small byte-saving and performance improvements
+ * 12-05-2009 - fixed hook:colon precedence, fixed empty body in loop and if-constructs
+ * 18-04-2009 - fixed crashbug in PHP 5.2.9 and several other bugfixes
* 12-04-2009 - some small bugfixes and performance improvements
* 09-04-2009 - initial open sourced version 1.0
*
* the Initial Developer. All Rights Reserved.
*
* Contributor(s): Tino Zijdel <crisp@tweakers.net>
- * PHP port, modifications and minifier routine are (C) 2009
+ * PHP port, modifications and minifier routine are (C) 2009-2011
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
define('TOKEN_STRING', 4);
define('TOKEN_REGEXP', 5);
define('TOKEN_NEWLINE', 6);
-define('TOKEN_CONDCOMMENT_MULTILINE', 7);
+define('TOKEN_CONDCOMMENT_START', 7);
+define('TOKEN_CONDCOMMENT_END', 8);
define('JS_SCRIPT', 100);
define('JS_BLOCK', 101);
define('JS_GROUP', 112);
define('JS_LIST', 113);
+define('JS_MINIFIED', 999);
+
define('DECLARED_FORM', 0);
define('EXPRESSED_FORM', 1);
define('STATEMENT_FORM', 2);
+/* Operators */
+define('OP_SEMICOLON', ';');
+define('OP_COMMA', ',');
+define('OP_HOOK', '?');
+define('OP_COLON', ':');
+define('OP_OR', '||');
+define('OP_AND', '&&');
+define('OP_BITWISE_OR', '|');
+define('OP_BITWISE_XOR', '^');
+define('OP_BITWISE_AND', '&');
+define('OP_STRICT_EQ', '===');
+define('OP_EQ', '==');
+define('OP_ASSIGN', '=');
+define('OP_STRICT_NE', '!==');
+define('OP_NE', '!=');
+define('OP_LSH', '<<');
+define('OP_LE', '<=');
+define('OP_LT', '<');
+define('OP_URSH', '>>>');
+define('OP_RSH', '>>');
+define('OP_GE', '>=');
+define('OP_GT', '>');
+define('OP_INCREMENT', '++');
+define('OP_DECREMENT', '--');
+define('OP_PLUS', '+');
+define('OP_MINUS', '-');
+define('OP_MUL', '*');
+define('OP_DIV', '/');
+define('OP_MOD', '%');
+define('OP_NOT', '!');
+define('OP_BITWISE_NOT', '~');
+define('OP_DOT', '.');
+define('OP_LEFT_BRACKET', '[');
+define('OP_RIGHT_BRACKET', ']');
+define('OP_LEFT_CURLY', '{');
+define('OP_RIGHT_CURLY', '}');
+define('OP_LEFT_PAREN', '(');
+define('OP_RIGHT_PAREN', ')');
+define('OP_CONDCOMMENT_END', '@*/');
+
+define('OP_UNARY_PLUS', 'U+');
+define('OP_UNARY_MINUS', 'U-');
+
+/* Keywords */
+define('KEYWORD_BREAK', 'break');
+define('KEYWORD_CASE', 'case');
+define('KEYWORD_CATCH', 'catch');
+define('KEYWORD_CONST', 'const');
+define('KEYWORD_CONTINUE', 'continue');
+define('KEYWORD_DEBUGGER', 'debugger');
+define('KEYWORD_DEFAULT', 'default');
+define('KEYWORD_DELETE', 'delete');
+define('KEYWORD_DO', 'do');
+define('KEYWORD_ELSE', 'else');
+define('KEYWORD_ENUM', 'enum');
+define('KEYWORD_FALSE', 'false');
+define('KEYWORD_FINALLY', 'finally');
+define('KEYWORD_FOR', 'for');
+define('KEYWORD_FUNCTION', 'function');
+define('KEYWORD_IF', 'if');
+define('KEYWORD_IN', 'in');
+define('KEYWORD_INSTANCEOF', 'instanceof');
+define('KEYWORD_NEW', 'new');
+define('KEYWORD_NULL', 'null');
+define('KEYWORD_RETURN', 'return');
+define('KEYWORD_SWITCH', 'switch');
+define('KEYWORD_THIS', 'this');
+define('KEYWORD_THROW', 'throw');
+define('KEYWORD_TRUE', 'true');
+define('KEYWORD_TRY', 'try');
+define('KEYWORD_TYPEOF', 'typeof');
+define('KEYWORD_VAR', 'var');
+define('KEYWORD_VOID', 'void');
+define('KEYWORD_WHILE', 'while');
+define('KEYWORD_WITH', 'with');
+
+
class JSMinPlus
{
private $parser;
private function __construct()
{
- $this->parser = new JSParser();
+ $this->parser = new JSParser($this);
}
public static function minify($js, $filename='')
return false;
}
- private function parseTree($n, $noBlockGrouping = false)
+ public function parseTree($n, $noBlockGrouping = false)
{
$s = '';
switch ($n->type)
{
- case KEYWORD_FUNCTION:
- $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '(';
- $params = $n->params;
- for ($i = 0, $j = count($params); $i < $j; $i++)
- $s .= ($i ? ',' : '') . $params[$i];
- $s .= '){' . $this->parseTree($n->body, true) . '}';
+ case JS_MINIFIED:
+ $s = $n->value;
break;
case JS_SCRIPT:
- // we do nothing with funDecls or varDecls
+ // we do nothing yet with funDecls or varDecls
$noBlockGrouping = true;
- // fall through
+ // FALL THROUGH
+
case JS_BLOCK:
$childs = $n->treeNodes;
+ $lastType = 0;
for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++)
{
+ $type = $childs[$i]->type;
$t = $this->parseTree($childs[$i]);
if (strlen($t))
{
if ($c)
{
- if ($childs[$i]->type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM)
- $s .= "\n"; // put declared functions on a new line
+ $s = rtrim($s, ';');
+
+ if ($type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM)
+ {
+ // put declared functions on a new line
+ $s .= "\n";
+ }
+ elseif ($type == KEYWORD_VAR && $type == $lastType)
+ {
+ // mutiple var-statements can go into one
+ $t = ',' . substr($t, 4);
+ }
else
+ {
+ // add terminator
$s .= ';';
+ }
}
$s .= $t;
$c++;
+ $lastType = $type;
}
}
}
break;
+ case KEYWORD_FUNCTION:
+ $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '(';
+ $params = $n->params;
+ for ($i = 0, $j = count($params); $i < $j; $i++)
+ $s .= ($i ? ',' : '') . $params[$i];
+ $s .= '){' . $this->parseTree($n->body, true) . '}';
+ break;
+
case KEYWORD_IF:
$s = 'if(' . $this->parseTree($n->condition) . ')';
$thenPart = $this->parseTree($n->thenPart);
$elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null;
- // quite a rancid hack to see if we should enclose the thenpart in brackets
- if ($thenPart[0] != '{')
- {
- if (strpos($thenPart, 'if(') !== false)
- $thenPart = '{' . $thenPart . '}';
- elseif ($elsePart)
- $thenPart .= ';';
- }
-
- $s .= $thenPart;
+ // empty if-statement
+ if ($thenPart == '')
+ $thenPart = ';';
if ($elsePart)
{
- $s .= 'else';
+ // be carefull and always make a block out of the thenPart; could be more optimized but is a lot of trouble
+ if ($thenPart != ';' && $thenPart[0] != '{')
+ $thenPart = '{' . $thenPart . '}';
+
+ $s .= $thenPart . 'else';
+ // we could check for more, but that hardly ever applies so go for performance
if ($elsePart[0] != '{')
$s .= ' ';
$s .= $elsePart;
}
+ else
+ {
+ $s .= $thenPart;
+ }
break;
case KEYWORD_SWITCH:
else
$s .= 'default:';
- $statement = $this->parseTree($case->statements);
+ $statement = $this->parseTree($case->statements, true);
if ($statement)
- $s .= $statement . ';';
+ {
+ $s .= $statement;
+ // no terminator for last statement
+ if ($i + 1 < $j)
+ $s .= ';';
+ }
}
- $s = rtrim($s, ';') . '}';
+ $s .= '}';
break;
case KEYWORD_FOR:
$s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '')
. ';' . ($n->condition ? $this->parseTree($n->condition) : '')
- . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')'
- . $this->parseTree($n->body);
+ . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')';
+
+ $body = $this->parseTree($n->body);
+ if ($body == '')
+ $body = ';';
+
+ $s .= $body;
break;
case KEYWORD_WHILE:
- $s = 'while(' . $this->parseTree($n->condition) . ')' . $this->parseTree($n->body);
+ $s = 'while(' . $this->parseTree($n->condition) . ')';
+
+ $body = $this->parseTree($n->body);
+ if ($body == '')
+ $body = ';';
+
+ $s .= $body;
break;
case JS_FOR_IN:
- $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
+ $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')';
+
+ $body = $this->parseTree($n->body);
+ if ($body == '')
+ $body = ';';
+
+ $s .= $body;
break;
case KEYWORD_DO:
break;
case KEYWORD_THROW:
- $s = 'throw ' . $this->parseTree($n->exception);
- break;
-
case KEYWORD_RETURN:
- $s = 'return' . ($n->value ? ' ' . $this->parseTree($n->value) : '');
+ $s = $n->type;
+ if ($n->value)
+ {
+ $t = $this->parseTree($n->value);
+ if (strlen($t))
+ {
+ if ($this->isWordChar($t[0]) || $t[0] == '\\')
+ $s .= ' ';
+
+ $s .= $t;
+ }
+ }
break;
case KEYWORD_WITH:
}
break;
+ case KEYWORD_IN:
+ case KEYWORD_INSTANCEOF:
+ $left = $this->parseTree($n->treeNodes[0]);
+ $right = $this->parseTree($n->treeNodes[1]);
+
+ $s = $left;
+
+ if ($this->isWordChar(substr($left, -1)))
+ $s .= ' ';
+
+ $s .= $n->type;
+
+ if ($this->isWordChar($right[0]) || $right[0] == '\\')
+ $s .= ' ';
+
+ $s .= $right;
+ break;
+
+ case KEYWORD_DELETE:
+ case KEYWORD_TYPEOF:
+ $right = $this->parseTree($n->treeNodes[0]);
+
+ $s = $n->type;
+
+ if ($this->isWordChar($right[0]) || $right[0] == '\\')
+ $s .= ' ';
+
+ $s .= $right;
+ break;
+
+ case KEYWORD_VOID:
+ $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')';
+ break;
+
case KEYWORD_DEBUGGER:
throw new Exception('NOT IMPLEMENTED: DEBUGGER');
break;
- case TOKEN_CONDCOMMENT_MULTILINE:
- $s = $n->value . ' ';
+ case TOKEN_CONDCOMMENT_START:
+ case TOKEN_CONDCOMMENT_END:
+ $s = $n->value . ($n->type == TOKEN_CONDCOMMENT_START ? ' ' : '');
$childs = $n->treeNodes;
for ($i = 0, $j = count($childs); $i < $j; $i++)
$s .= $this->parseTree($childs[$i]);
case OP_PLUS:
case OP_MINUS:
- $s = $this->parseTree($n->treeNodes[0]) . $n->type;
- $nextTokenType = $n->treeNodes[1]->type;
- if ( $nextTokenType == OP_PLUS || $nextTokenType == OP_MINUS ||
- $nextTokenType == OP_INCREMENT || $nextTokenType == OP_DECREMENT ||
- $nextTokenType == OP_UNARY_PLUS || $nextTokenType == OP_UNARY_MINUS
- )
- $s .= ' ';
- $s .= $this->parseTree($n->treeNodes[1]);
- break;
-
- case KEYWORD_IN:
- $s = $this->parseTree($n->treeNodes[0]) . ' in ' . $this->parseTree($n->treeNodes[1]);
- break;
+ $left = $this->parseTree($n->treeNodes[0]);
+ $right = $this->parseTree($n->treeNodes[1]);
- case KEYWORD_INSTANCEOF:
- $s = $this->parseTree($n->treeNodes[0]) . ' instanceof ' . $this->parseTree($n->treeNodes[1]);
- break;
-
- case KEYWORD_DELETE:
- $s = 'delete ' . $this->parseTree($n->treeNodes[0]);
- break;
+ switch ($n->treeNodes[1]->type)
+ {
+ case OP_PLUS:
+ case OP_MINUS:
+ case OP_INCREMENT:
+ case OP_DECREMENT:
+ case OP_UNARY_PLUS:
+ case OP_UNARY_MINUS:
+ $s = $left . $n->type . ' ' . $right;
+ break;
- case KEYWORD_VOID:
- $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')';
- break;
+ case TOKEN_STRING:
+ //combine concatted strings with same quotestyle
+ if ($n->type == OP_PLUS && substr($left, -1) == $right[0])
+ {
+ $s = substr($left, 0, -1) . substr($right, 1);
+ break;
+ }
+ // FALL THROUGH
- case KEYWORD_TYPEOF:
- $s = 'typeof ' . $this->parseTree($n->treeNodes[0]);
+ default:
+ $s = $left . $n->type . $right;
+ }
break;
case OP_NOT:
$s .= '}';
break;
+ case TOKEN_NUMBER:
+ $s = $n->value;
+ if (preg_match('/^([1-9]+)(0{3,})$/', $s, $m))
+ $s = $m[1] . 'e' . strlen($m[2]);
+ break;
+
case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
- case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
+ case TOKEN_IDENTIFIER: case TOKEN_STRING: case TOKEN_REGEXP:
$s = $n->value;
break;
case JS_GROUP:
- $s = '(' . $this->parseTree($n->treeNodes[0]) . ')';
+ if (in_array(
+ $n->treeNodes[0]->type,
+ array(
+ JS_ARRAY_INIT, JS_OBJECT_INIT, JS_GROUP,
+ TOKEN_NUMBER, TOKEN_STRING, TOKEN_REGEXP, TOKEN_IDENTIFIER,
+ KEYWORD_NULL, KEYWORD_THIS, KEYWORD_TRUE, KEYWORD_FALSE
+ )
+ ))
+ {
+ $s = $this->parseTree($n->treeNodes[0]);
+ }
+ else
+ {
+ $s = '(' . $this->parseTree($n->treeNodes[0]) . ')';
+ }
break;
default:
{
return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved);
}
+
+ private function isWordChar($char)
+ {
+ return $char == '_' || $char == '$' || ctype_alnum($char);
+ }
}
class JSParser
{
private $t;
+ private $minifier;
private $opPrecedence = array(
';' => 0,
',' => 1,
'=' => 2, '?' => 2, ':' => 2,
- // The above all have to have the same precedence, see bug 330975.
+ // The above all have to have the same precedence, see bug 330975
'||' => 4,
'&&' => 5,
'|' => 6,
'.' => 2,
JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2,
JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1,
- TOKEN_CONDCOMMENT_MULTILINE => 1
+ TOKEN_CONDCOMMENT_START => 1, TOKEN_CONDCOMMENT_END => 1
);
- public function __construct()
+ public function __construct($minifier=null)
{
+ $this->minifier = $minifier;
$this->t = new JSTokenizer();
}
$n->funDecls = $x->funDecls;
$n->varDecls = $x->varDecls;
+ // minify by scope
+ if ($this->minifier)
+ {
+ $n->value = $this->minifier->parseTree($n);
+
+ // clear tree from node to save memory
+ $n->treeNodes = null;
+ $n->funDecls = null;
+ $n->varDecls = null;
+
+ $n->type = JS_MINIFIED;
+ }
+
return $n;
}
case KEYWORD_THROW:
$n = new JSNode($this->t);
- $n->exception = $this->Expression($x);
+ $n->value = $this->Expression($x);
break;
case KEYWORD_RETURN:
$n = $this->Variables($x);
break;
- case TOKEN_CONDCOMMENT_MULTILINE:
+ case TOKEN_CONDCOMMENT_START:
+ case TOKEN_CONDCOMMENT_END:
$n = new JSNode($this->t);
return $n;
// NB: cannot be empty, Statement handled that.
break 2;
- case OP_ASSIGN:
case OP_HOOK:
- case OP_COLON:
if ($this->t->scanOperand)
break 2;
- // Use >, not >=, for right-associative ASSIGN and HOOK/COLON.
while ( !empty($operators) &&
- ( $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] ||
- ($tt == OP_COLON && end($operators)->type == OP_ASSIGN)
- )
+ $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]
)
$this->reduce($operators, $operands);
- if ($tt == OP_COLON)
- {
- $n = end($operators);
- if ($n->type != OP_HOOK)
- throw $this->t->newSyntaxError('Invalid label');
+ array_push($operators, new JSNode($this->t));
- --$x->hookLevel;
- }
- else
- {
- array_push($operators, new JSNode($this->t));
- if ($tt == OP_ASSIGN)
- end($operands)->assignOp = $this->t->currentToken()->assignOp;
- else
- ++$x->hookLevel;
- }
+ ++$x->hookLevel;
+ $this->t->scanOperand = true;
+ $n = $this->Expression($x);
+
+ if (!$this->t->match(OP_COLON))
+ break 2;
+
+ --$x->hookLevel;
+ array_push($operands, $n);
+ break;
+
+ case OP_COLON:
+ if ($x->hookLevel)
+ break 2;
+
+ throw $this->t->newSyntaxError('Invalid label');
+ break;
+ case OP_ASSIGN:
+ if ($this->t->scanOperand)
+ break 2;
+
+ // Use >, not >=, for right-associative ASSIGN
+ while ( !empty($operators) &&
+ $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]
+ )
+ $this->reduce($operators, $operands);
+
+ array_push($operators, new JSNode($this->t));
+ end($operands)->assignOp = $this->t->currentToken()->assignOp;
$this->t->scanOperand = true;
break;
!$x->bracketLevel && !$x->curlyLevel &&
!$x->parenLevel
)
- {
break 2;
- }
// FALL THROUGH
case OP_COMMA:
- // Treat comma as left-associative so reduce can fold left-heavy
- // COMMA trees into a single array.
- // FALL THROUGH
+ // A comma operator should not be parsed if we're parsing the then part
+ // of a conditional expression unless it's parenthesized somehow.
+ if ($tt == OP_COMMA && $x->hookLevel &&
+ !$x->bracketLevel && !$x->curlyLevel &&
+ !$x->parenLevel
+ )
+ break 2;
+ // Treat comma as left-associative so reduce can fold left-heavy
+ // COMMA trees into a single array.
+ // FALL THROUGH
case OP_OR:
case OP_AND:
case OP_BITWISE_OR:
$this->t->scanOperand = false;
break;
- case TOKEN_CONDCOMMENT_MULTILINE:
+ case TOKEN_CONDCOMMENT_START:
+ case TOKEN_CONDCOMMENT_END:
if ($this->t->scanOperand)
array_push($operators, new JSNode($this->t));
else
}
if ($x->hookLevel != $hl)
- throw $this->t->newSyntaxError('Missing : after ?');
+ throw $this->t->newSyntaxError('Missing : in conditional expression');
if ($x->parenLevel != $pl)
throw $this->t->newSyntaxError('Missing ) in parenthetical');
if (($numargs = func_num_args()) > 2)
{
- $args = func_get_args();;
+ $args = func_get_args();
for ($i = 2; $i < $numargs; $i++)
$this->addNode($args[$i]);
}
public function addNode($node)
{
+ if ($node !== null)
+ {
+ if ($node->start < $this->start)
+ $this->start = $node->start;
+ if ($this->end < $node->end)
+ $this->end = $node->end;
+ }
+
$this->treeNodes[] = $node;
}
}
);
private $opTypeNames = array(
- ';' => 'SEMICOLON',
- ',' => 'COMMA',
- '?' => 'HOOK',
- ':' => 'COLON',
- '||' => 'OR',
- '&&' => 'AND',
- '|' => 'BITWISE_OR',
- '^' => 'BITWISE_XOR',
- '&' => 'BITWISE_AND',
- '===' => 'STRICT_EQ',
- '==' => 'EQ',
- '=' => 'ASSIGN',
- '!==' => 'STRICT_NE',
- '!=' => 'NE',
- '<<' => 'LSH',
- '<=' => 'LE',
- '<' => 'LT',
- '>>>' => 'URSH',
- '>>' => 'RSH',
- '>=' => 'GE',
- '>' => 'GT',
- '++' => 'INCREMENT',
- '--' => 'DECREMENT',
- '+' => 'PLUS',
- '-' => 'MINUS',
- '*' => 'MUL',
- '/' => 'DIV',
- '%' => 'MOD',
- '!' => 'NOT',
- '~' => 'BITWISE_NOT',
- '.' => 'DOT',
- '[' => 'LEFT_BRACKET',
- ']' => 'RIGHT_BRACKET',
- '{' => 'LEFT_CURLY',
- '}' => 'RIGHT_CURLY',
- '(' => 'LEFT_PAREN',
- ')' => 'RIGHT_PAREN',
- '@*/' => 'CONDCOMMENT_END'
+ ';', ',', '?', ':', '||', '&&', '|', '^',
+ '&', '===', '==', '=', '!==', '!=', '<<', '<=',
+ '<', '>>>', '>>', '>=', '>', '++', '--', '+',
+ '-', '*', '/', '%', '!', '~', '.', '[',
+ ']', '{', '}', '(', ')', '@*/'
);
private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
public function __construct()
{
- $this->opRegExp = '#^(' . implode('|', array_map('preg_quote', array_keys($this->opTypeNames))) . ')#';
-
- // this is quite a hidden yet convenient place to create the defines for operators and keywords
- foreach ($this->opTypeNames as $operand => $name)
- define('OP_' . $name, $operand);
-
- define('OP_UNARY_PLUS', 'U+');
- define('OP_UNARY_MINUS', 'U-');
-
- foreach ($this->keywords as $keyword)
- define('KEYWORD_' . strtoupper($keyword), $keyword);
+ $this->opRegExp = '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames)) . ')#';
}
public function init($source, $filename = '', $lineno = 1)
}
// Comments
- if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?(?:.|\n)*?\*\/|\/.*)/', $input, $match))
+ if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match))
{
if (!$chunksize)
break;
// check if this is a conditional (JScript) comment
if (!empty($match[1]))
{
- //$match[0] = '/*' . $match[1];
+ $match[0] = '/*' . $match[1];
$conditional_comment = true;
break;
}
}
elseif ($conditional_comment)
{
- $tt = TOKEN_CONDCOMMENT_MULTILINE;
+ $tt = TOKEN_CONDCOMMENT_START;
}
else
{
switch ($input[0])
{
- case '0': case '1': case '2': case '3': case '4':
- case '5': case '6': case '7': case '8': case '9':
- if (preg_match('/^\d+\.\d*(?:[eE][-+]?\d+)?|^\d+(?:\.\d*)?[eE][-+]?\d+/', $input, $match))
+ case '0':
+ // hexadecimal
+ if (($input[1] == 'x' || $input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match))
{
$tt = TOKEN_NUMBER;
+ break;
}
- elseif (preg_match('/^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/', $input, $match))
+ // FALL THROUGH
+
+ case '1': case '2': case '3': case '4': case '5':
+ case '6': case '7': case '8': case '9':
+ // should always match
+ preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match);
+ $tt = TOKEN_NUMBER;
+ break;
+
+ case "'":
+ if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match))
{
- // this should always match because of \d+
- $tt = TOKEN_NUMBER;
+ $tt = TOKEN_STRING;
+ }
+ else
+ {
+ if ($chunksize)
+ return $this->get(null); // retry with a full chunk fetch
+
+ throw $this->newSyntaxError('Unterminated string literal');
}
break;
case '"':
- case "'":
- if (preg_match('/^"(?:\\\\(?:.|\r?\n)|[^\\\\"\r\n])*"|^\'(?:\\\\(?:.|\r?\n)|[^\\\\\'\r\n])*\'/', $input, $match))
+ if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match))
{
$tt = TOKEN_STRING;
}
$tt = TOKEN_REGEXP;
break;
}
- // fall through
+ // FALL THROUGH
case '|':
case '^':
$tt = TOKEN_NUMBER;
break;
}
- // fall through
+ // FALL THROUGH
case ';':
case ',':
break;
case '@':
- throw $this->newSyntaxError('Illegal token');
+ // check end of conditional comment
+ if (substr($input, 0, 3) == '@*/')
+ {
+ $match = array('@*/');
+ $tt = TOKEN_CONDCOMMENT_END;
+ }
+ else
+ throw $this->newSyntaxError('Illegal token');
break;
case "\n":
public $lineno;
public $assignOp;
}
-
-?>
*/
class Minify {
- const VERSION = '2.1.3';
+ const VERSION = '2.1.5';
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?
* @var string $importWarning
*/
public static $importWarning = "/* See http://code.google.com/p/minify/wiki/CommonProblems#@imports_can_appear_in_invalid_locations_in_combined_CSS_files */\n";
-
+
+ /**
+ * Has the DOCUMENT_ROOT been set in user code?
+ *
+ * @var bool
+ */
+ public static $isDocRootSet = false;
+
/**
* Specify a cache object (with identical interface as Minify_Cache_File) or
* a path to use with Minify_Cache_File.
*
* Any controller options are documented in that controller's setupSources() method.
*
- * @param mixed instance of subclass of Minify_Controller_Base or string name of
- * controller. E.g. 'Files'
+ * @param mixed $controller instance of subclass of Minify_Controller_Base or string
+ * name of controller. E.g. 'Files'
*
* @param array $options controller/serve options
*
*/
public static function serve($controller, $options = array())
{
+ if (! self::$isDocRootSet && 0 === stripos(PHP_OS, 'win')) {
+ self::setDocRoot();
+ }
+
if (is_string($controller)) {
// make $controller into object
$class = 'Minify_Controller_' . $controller;
. str_replace('_', '/', $controller) . ".php";
}
$controller = new $class();
+ /* @var Minify_Controller_Base $controller */
}
// set up controller sources and mix remaining options with
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(
// determine encoding
if (self::$_options['encodeOutput']) {
+ $sendVary = true;
if (self::$_options['encodeMethod'] !== null) {
// controller specifically requested this
$contentEncoding = self::$_options['encodeMethod'];
// '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)
);
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) {
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'])
}
// 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;
$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']));
} 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
// 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';
}
}
/**
- * On IIS, create $_SERVER['DOCUMENT_ROOT']
- *
- * @param bool $unsetPathInfo (default false) if true, $_SERVER['PATH_INFO']
- * will be unset (it is inconsistent with Apache's setting)
+ * Set $_SERVER['DOCUMENT_ROOT']. On IIS, the value is created from SCRIPT_FILENAME and SCRIPT_NAME.
*
- * @return null
+ * @param string $docRoot value to use for DOCUMENT_ROOT
*/
- public static function setDocRoot($unsetPathInfo = false)
+ public static function setDocRoot($docRoot = '')
{
- if (isset($_SERVER['SERVER_SOFTWARE'])
- && 0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/')
- ) {
- $_SERVER['DOCUMENT_ROOT'] = rtrim(substr(
- $_SERVER['PATH_TRANSLATED']
+ self::$isDocRootSet = true;
+ if ($docRoot) {
+ $_SERVER['DOCUMENT_ROOT'] = $docRoot;
+ } elseif (isset($_SERVER['SERVER_SOFTWARE'])
+ && 0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/')) {
+ $_SERVER['DOCUMENT_ROOT'] = substr(
+ $_SERVER['SCRIPT_FILENAME']
,0
- ,strlen($_SERVER['PATH_TRANSLATED']) - strlen($_SERVER['SCRIPT_NAME'])
- ), '\\');
- if ($unsetPathInfo) {
- unset($_SERVER['PATH_INFO']);
- }
- require_once 'Minify/Logger.php';
- Minify_Logger::log("setDocRoot() set DOCUMENT_ROOT to \"{$_SERVER['DOCUMENT_ROOT']}\"");
+ ,strlen($_SERVER['SCRIPT_FILENAME']) - strlen($_SERVER['SCRIPT_NAME']));
+ $_SERVER['DOCUMENT_ROOT'] = rtrim($_SERVER['DOCUMENT_ROOT'], '\\');
}
}
/**
- * @var mixed Minify_Cache_* object or null (i.e. no server cache is used)
+ * Any Minify_Cache_* object or null (i.e. no server cache is used)
+ *
+ * @var Minify_Cache_File
*/
private static $_cache = null;
/**
- * @var Minify_Controller active controller for current request
+ * Active controller for current request
+ *
+ * @var Minify_Controller_Base
*/
protected static $_controller = null;
/**
- * @var array options for current request
+ * Options for current request
+ *
+ * @var array
*/
protected static $_options = null;
-
+
+ /**
+ * @param string $header
+ *
+ * @param string $url
+ */
+ 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>$h1</h1>";
+ echo "<p>Please see <a href='$url'>$url</a>.</p>";
+ exit();
+ }
+
/**
* Set up sources to use Minify_Lines
*
* @param array $sources Minify_Source instances
- *
- * @return null
*/
protected static function _setupDebug($sources)
{
$defaultMinifier = isset(self::$_options['minifiers'][$type])
? self::$_options['minifiers'][$type]
: false;
-
- if (Minify_Source::haveNoMinifyPrefs(self::$_controller->sources)) {
- // all source have same options/minifier, better performance
- // to combine, then minify once
- foreach (self::$_controller->sources as $source) {
- $pieces[] = $source->getContent();
- }
- $content = implode($implodeSeparator, $pieces);
- if ($defaultMinifier) {
- self::$_controller->loadMinifier($defaultMinifier);
- $content = call_user_func($defaultMinifier, $content, $defaultOptions);
- }
- } else {
- // minify each source with its own options and minifier, then combine
- foreach (self::$_controller->sources as $source) {
+
+ // process groups of sources with identical minifiers/options
+ $content = array();
+ $i = 0;
+ $l = count(self::$_controller->sources);
+ $groupToProcessTogether = array();
+ $lastMinifier = null;
+ $lastOptions = null;
+ do {
+ // get next source
+ $source = null;
+ if ($i < $l) {
+ $source = self::$_controller->sources[$i];
+ /* @var Minify_Source $source */
+ $sourceContent = $source->getContent();
+
// allow the source to override our minifier and options
$minifier = (null !== $source->minifier)
? $source->minifier
$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);
+ }
+ // do we need to process our group right now?
+ if ($i > 0 // yes, we have at least the first group populated
+ && (
+ ! $source // yes, we ran out of sources
+ || $type === self::TYPE_CSS // yes, to process CSS individually (avoiding PCRE bugs/limits)
+ || $minifier !== $lastMinifier // yes, minifier changed
+ || $options !== $lastOptions) // yes, options changed
+ )
+ {
+ // minify previous sources with last settings
+ $imploded = implode($implodeSeparator, $groupToProcessTogether);
+ $groupToProcessTogether = array();
+ if ($lastMinifier) {
+ self::$_controller->loadMinifier($lastMinifier);
+ try {
+ $content[] = call_user_func($lastMinifier, $imploded, $lastOptions);
+ } catch (Exception $e) {
+ throw new Exception("Exception in minifier: " . $e->getMessage());
+ }
} else {
- $pieces[] = $source->getContent();
+ $content[] = $imploded;
}
}
- $content = implode($implodeSeparator, $pieces);
- }
+ // add content to the group
+ if ($source) {
+ $groupToProcessTogether[] = $sourceContent;
+ $lastMinifier = $minifier;
+ $lastOptions = $options;
+ }
+ $i++;
+ } while ($source);
+
+ $content = implode($implodeSeparator, $content);
if ($type === self::TYPE_CSS && false !== strpos($content, '@import')) {
$content = self::_handleCssImports($content);
*
* 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']
+ ,self::VERSION
)));
+ return "{$prefix}_{$name}_{$md5}";
}
/**
- * Bubble CSS @imports to the top or prepend a warning if an
- * @import is detected not at the top.
+ * Bubble CSS @imports to the top or prepend a warning if an import is detected not at the top.
+ *
+ * @param string $css
+ *
+ * @return string
*/
protected static function _handleCssImports($css)
{
-<?php
-/**
- * Class Minify_CSS
- * @package Minify
- */
-
-/**
- * Minify CSS
- *
- * This class uses Minify_CSS_Compressor and Minify_CSS_UriRewriter to
- * minify CSS and rewrite relative URIs.
- *
- * @package Minify
- * @author Stephen Clay <steve@mrclay.org>
- * @author http://code.google.com/u/1stvamp/ (Issue 64 patch)
- */
-class Minify_CSS {
-
- /**
- * Minify a CSS string
- *
- * @param string $css
- *
- * @param array $options available options:
- *
- * 'preserveComments': (default true) multi-line comments that begin
- * with "/*!" will be preserved with newlines before and after to
- * enhance readability.
- *
- * 'prependRelativePath': (default null) if given, this string will be
- * prepended to all relative URIs in import/url declarations
- *
- * 'currentDir': (default null) if given, this is assumed to be the
- * directory of the current CSS file. Using this, minify will rewrite
- * all relative URIs in import/url declarations to correctly point to
- * the desired files. For this to work, the files *must* exist and be
- * visible by the PHP process.
- *
- * 'symlinks': (default = array()) If the CSS file is stored in
- * a symlink-ed directory, provide an array of link paths to
- * target paths, where the link paths are within the document root. Because
- * paths need to be normalized for this to work, use "//" to substitute
- * the doc root in the link paths (the array keys). E.g.:
- * <code>
- * array('//symlink' => '/real/target/path') // unix
- * array('//static' => 'D:\\staticStorage') // Windows
- * </code>
- *
- * @return string
- */
- public static function minify($css, $options = array())
- {
- require_once 'Minify/CSS/Compressor.php';
- if (isset($options['preserveComments'])
- && !$options['preserveComments']) {
- $css = Minify_CSS_Compressor::process($css, $options);
- } else {
- require_once 'Minify/CommentPreserver.php';
- $css = Minify_CommentPreserver::process(
- $css
- ,array('Minify_CSS_Compressor', 'process')
- ,array($options)
- );
- }
- if (! isset($options['currentDir']) && ! isset($options['prependRelativePath'])) {
- return $css;
- }
- require_once 'Minify/CSS/UriRewriter.php';
- if (isset($options['currentDir'])) {
- return Minify_CSS_UriRewriter::rewrite(
- $css
- ,$options['currentDir']
- ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT']
- ,isset($options['symlinks']) ? $options['symlinks'] : array()
- );
- } else {
- return Minify_CSS_UriRewriter::prepend(
- $css
- ,$options['prependRelativePath']
- );
- }
- }
-}
+<?php\r
+/**\r
+ * Class Minify_CSS \r
+ * @package Minify\r
+ */\r
+\r
+/**\r
+ * Minify CSS\r
+ *\r
+ * This class uses Minify_CSS_Compressor and Minify_CSS_UriRewriter to \r
+ * minify CSS and rewrite relative URIs.\r
+ * \r
+ * @package Minify\r
+ * @author Stephen Clay <steve@mrclay.org>\r
+ * @author http://code.google.com/u/1stvamp/ (Issue 64 patch)\r
+ */\r
+class Minify_CSS {\r
+ \r
+ /**\r
+ * Minify a CSS string\r
+ * \r
+ * @param string $css\r
+ * \r
+ * @param array $options available options:\r
+ * \r
+ * 'preserveComments': (default true) multi-line comments that begin\r
+ * with "/*!" will be preserved with newlines before and after to\r
+ * enhance readability.\r
+ *\r
+ * 'removeCharsets': (default true) remove all @charset at-rules\r
+ * \r
+ * 'prependRelativePath': (default null) if given, this string will be\r
+ * prepended to all relative URIs in import/url declarations\r
+ * \r
+ * 'currentDir': (default null) if given, this is assumed to be the\r
+ * directory of the current CSS file. Using this, minify will rewrite\r
+ * all relative URIs in import/url declarations to correctly point to\r
+ * the desired files. For this to work, the files *must* exist and be\r
+ * visible by the PHP process.\r
+ *\r
+ * 'symlinks': (default = array()) If the CSS file is stored in \r
+ * a symlink-ed directory, provide an array of link paths to\r
+ * target paths, where the link paths are within the document root. Because \r
+ * paths need to be normalized for this to work, use "//" to substitute \r
+ * the doc root in the link paths (the array keys). E.g.:\r
+ * <code>\r
+ * array('//symlink' => '/real/target/path') // unix\r
+ * array('//static' => 'D:\\staticStorage') // Windows\r
+ * </code>\r
+ *\r
+ * 'docRoot': (default = $_SERVER['DOCUMENT_ROOT'])\r
+ * see Minify_CSS_UriRewriter::rewrite\r
+ * \r
+ * @return string\r
+ */\r
+ public static function minify($css, $options = array()) \r
+ {\r
+ $options = array_merge(array(\r
+ 'removeCharsets' => true,\r
+ 'preserveComments' => true,\r
+ 'currentDir' => null,\r
+ 'docRoot' => $_SERVER['DOCUMENT_ROOT'],\r
+ 'prependRelativePath' => null,\r
+ 'symlinks' => array(),\r
+ ), $options);\r
+ \r
+ if ($options['removeCharsets']) {\r
+ $css = preg_replace('/@charset[^;]+;\\s*/', '', $css);\r
+ }\r
+ require_once 'Minify/CSS/Compressor.php';\r
+ if (! $options['preserveComments']) {\r
+ $css = Minify_CSS_Compressor::process($css, $options);\r
+ } else {\r
+ require_once 'Minify/CommentPreserver.php';\r
+ $css = Minify_CommentPreserver::process(\r
+ $css\r
+ ,array('Minify_CSS_Compressor', 'process')\r
+ ,array($options)\r
+ );\r
+ }\r
+ if (! $options['currentDir'] && ! $options['prependRelativePath']) {\r
+ return $css;\r
+ }\r
+ require_once 'Minify/CSS/UriRewriter.php';\r
+ if ($options['currentDir']) {\r
+ return Minify_CSS_UriRewriter::rewrite(\r
+ $css\r
+ ,$options['currentDir']\r
+ ,$options['docRoot']\r
+ ,$options['symlinks']\r
+ ); \r
+ } else {\r
+ return Minify_CSS_UriRewriter::prepend(\r
+ $css\r
+ ,$options['prependRelativePath']\r
+ );\r
+ }\r
+ }\r
+}\r
-<?php
-/**
- * Class Minify_CSS_Compressor
- * @package Minify
- */
-
-/**
- * Compress CSS
- *
- * This is a heavy regex-based removal of whitespace, unnecessary
- * comments and tokens, and some CSS value minimization, where practical.
- * Many steps have been taken to avoid breaking comment-based hacks,
- * including the ie5/mac filter (and its inversion), but expect tricky
- * hacks involving comment tokens in 'content' value strings to break
- * minimization badly. A test suite is available.
- *
- * @package Minify
- * @author Stephen Clay <steve@mrclay.org>
- * @author http://code.google.com/u/1stvamp/ (Issue 64 patch)
- */
-class Minify_CSS_Compressor {
-
- /**
- * Minify a CSS string
- *
- * @param string $css
- *
- * @param array $options (currently ignored)
- *
- * @return string
- */
- public static function process($css, $options = array())
- {
- $obj = new Minify_CSS_Compressor($options);
- return $obj->_process($css);
- }
-
- /**
- * @var array options
- */
- protected $_options = null;
-
- /**
- * @var bool Are we "in" a hack?
- *
- * I.e. are some browsers targetted until the next comment?
- */
- protected $_inHack = false;
-
-
- /**
- * Constructor
- *
- * @param array $options (currently ignored)
- *
- * @return null
- */
- private function __construct($options) {
- $this->_options = $options;
- }
-
- /**
- * Minify a CSS string
- *
- * @param string $css
- *
- * @return string
- */
- protected function _process($css)
- {
- $css = str_replace("\r\n", "\n", $css);
-
- // preserve empty comment after '>'
- // http://www.webdevout.net/css-hacks#in_css-selectors
- $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
-
- // preserve empty comment between property and value
- // http://css-discuss.incutio.com/?page=BoxModelHack
- $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
- $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
-
- // apply callback to all valid comments (and strip out surrounding ws
- $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@'
- ,array($this, '_commentCB'), $css);
-
- // remove ws around { } and last semicolon in declaration block
- $css = preg_replace('/\\s*{\\s*/', '{', $css);
- $css = preg_replace('/;?\\s*}\\s*/', '}', $css);
-
- // remove ws surrounding semicolons
- $css = preg_replace('/\\s*;\\s*/', ';', $css);
-
- // remove ws around urls
- $css = preg_replace('/
- url\\( # url(
- \\s*
- ([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis)
- \\s*
- \\) # )
- /x', 'url($1)', $css);
-
- // remove ws between rules and colons
- $css = preg_replace('/
- \\s*
- ([{;]) # 1 = beginning of block or rule separator
- \\s*
- ([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter)
- \\s*
- :
- \\s*
- (\\b|[#\'"]) # 3 = first character of a value
- /x', '$1$2:$3', $css);
-
- // remove ws in selectors
- $css = preg_replace_callback('/
- (?: # non-capture
- \\s*
- [^~>+,\\s]+ # selector part
- \\s*
- [,>+~] # combinators
- )+
- \\s*
- [^~>+,\\s]+ # selector part
- { # open declaration block
- /x'
- ,array($this, '_selectorsCB'), $css);
-
- // minimize hex colors
- $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i'
- , '$1#$2$3$4$5', $css);
-
- // remove spaces between font families
- $css = preg_replace_callback('/font-family:([^;}]+)([;}])/'
- ,array($this, '_fontFamilyCB'), $css);
-
- $css = preg_replace('/@import\\s+url/', '@import url', $css);
-
- // replace any ws involving newlines with a single newline
- $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
-
- // separate common descendent selectors w/ newlines (to limit line lengths)
- $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
-
- // Use newline after 1st numeric value (to limit line lengths).
- $css = preg_replace('/
- ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
- \\s+
- /x'
- ,"$1\n", $css);
-
- // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
- $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);
-
- return trim($css);
- }
-
- /**
- * Replace what looks like a set of selectors
- *
- * @param array $m regex matches
- *
- * @return string
- */
- protected function _selectorsCB($m)
- {
- // remove ws around the combinators
- return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
- }
-
- /**
- * Process a comment and return a replacement
- *
- * @param array $m regex matches
- *
- * @return string
- */
- protected function _commentCB($m)
- {
- $hasSurroundingWs = (trim($m[0]) !== $m[1]);
- $m = $m[1];
- // $m is the comment content w/o the surrounding tokens,
- // but the return value will replace the entire comment.
- if ($m === 'keep') {
- return '/**/';
- }
- if ($m === '" "') {
- // component of http://tantek.com/CSS/Examples/midpass.html
- return '/*" "*/';
- }
- if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
- // component of http://tantek.com/CSS/Examples/midpass.html
- return '/*";}}/* */';
- }
- if ($this->_inHack) {
- // inversion: feeding only to one browser
- if (preg_match('@
- ^/ # comment started like /*/
- \\s*
- (\\S[\\s\\S]+?) # has at least some non-ws content
- \\s*
- /\\* # ends like /*/ or /**/
- @x', $m, $n)) {
- // end hack mode after this comment, but preserve the hack and comment content
- $this->_inHack = false;
- return "/*/{$n[1]}/**/";
- }
- }
- if (substr($m, -1) === '\\') { // comment ends like \*/
- // begin hack mode and preserve hack
- $this->_inHack = true;
- return '/*\\*/';
- }
- if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
- // begin hack mode and preserve hack
- $this->_inHack = true;
- return '/*/*/';
- }
- if ($this->_inHack) {
- // a regular comment ends hack mode but should be preserved
- $this->_inHack = false;
- return '/**/';
- }
- // Issue 107: if there's any surrounding whitespace, it may be important, so
- // replace the comment with a single space
- return $hasSurroundingWs // remove all other comments
- ? ' '
- : '';
- }
-
- /**
- * Process a font-family listing and return a replacement
- *
- * @param array $m regex matches
- *
- * @return string
- */
- protected function _fontFamilyCB($m)
- {
- // 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];
- }
-}
+<?php\r
+/**\r
+ * Class Minify_CSS_Compressor \r
+ * @package Minify\r
+ */\r
+\r
+/**\r
+ * Compress CSS\r
+ *\r
+ * This is a heavy regex-based removal of whitespace, unnecessary\r
+ * comments and tokens, and some CSS value minimization, where practical.\r
+ * Many steps have been taken to avoid breaking comment-based hacks, \r
+ * including the ie5/mac filter (and its inversion), but expect tricky\r
+ * hacks involving comment tokens in 'content' value strings to break\r
+ * minimization badly. A test suite is available.\r
+ * \r
+ * @package Minify\r
+ * @author Stephen Clay <steve@mrclay.org>\r
+ * @author http://code.google.com/u/1stvamp/ (Issue 64 patch)\r
+ */\r
+class Minify_CSS_Compressor {\r
+\r
+ /**\r
+ * Minify a CSS string\r
+ * \r
+ * @param string $css\r
+ * \r
+ * @param array $options (currently ignored)\r
+ * \r
+ * @return string\r
+ */\r
+ public static function process($css, $options = array())\r
+ {\r
+ $obj = new Minify_CSS_Compressor($options);\r
+ return $obj->_process($css);\r
+ }\r
+ \r
+ /**\r
+ * @var array\r
+ */\r
+ protected $_options = null;\r
+ \r
+ /**\r
+ * Are we "in" a hack? I.e. are some browsers targetted until the next comment?\r
+ *\r
+ * @var bool\r
+ */\r
+ protected $_inHack = false;\r
+ \r
+ \r
+ /**\r
+ * Constructor\r
+ * \r
+ * @param array $options (currently ignored)\r
+ */\r
+ private function __construct($options) {\r
+ $this->_options = $options;\r
+ }\r
+ \r
+ /**\r
+ * Minify a CSS string\r
+ * \r
+ * @param string $css\r
+ * \r
+ * @return string\r
+ */\r
+ protected function _process($css)\r
+ {\r
+ $css = str_replace("\r\n", "\n", $css);\r
+ \r
+ // preserve empty comment after '>'\r
+ // http://www.webdevout.net/css-hacks#in_css-selectors\r
+ $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);\r
+ \r
+ // preserve empty comment between property and value\r
+ // http://css-discuss.incutio.com/?page=BoxModelHack\r
+ $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);\r
+ $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);\r
+ \r
+ // apply callback to all valid comments (and strip out surrounding ws\r
+ $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@'\r
+ ,array($this, '_commentCB'), $css);\r
+\r
+ // remove ws around { } and last semicolon in declaration block\r
+ $css = preg_replace('/\\s*{\\s*/', '{', $css);\r
+ $css = preg_replace('/;?\\s*}\\s*/', '}', $css);\r
+ \r
+ // remove ws surrounding semicolons\r
+ $css = preg_replace('/\\s*;\\s*/', ';', $css);\r
+ \r
+ // remove ws around urls\r
+ $css = preg_replace('/\r
+ url\\( # url(\r
+ \\s*\r
+ ([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis)\r
+ \\s*\r
+ \\) # )\r
+ /x', 'url($1)', $css);\r
+ \r
+ // remove ws between rules and colons\r
+ $css = preg_replace('/\r
+ \\s*\r
+ ([{;]) # 1 = beginning of block or rule separator \r
+ \\s*\r
+ ([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter)\r
+ \\s*\r
+ :\r
+ \\s*\r
+ (\\b|[#\'"-]) # 3 = first character of a value\r
+ /x', '$1$2:$3', $css);\r
+ \r
+ // remove ws in selectors\r
+ $css = preg_replace_callback('/\r
+ (?: # non-capture\r
+ \\s*\r
+ [^~>+,\\s]+ # selector part\r
+ \\s*\r
+ [,>+~] # combinators\r
+ )+\r
+ \\s*\r
+ [^~>+,\\s]+ # selector part\r
+ { # open declaration block\r
+ /x'\r
+ ,array($this, '_selectorsCB'), $css);\r
+ \r
+ // minimize hex colors\r
+ $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i'\r
+ , '$1#$2$3$4$5', $css);\r
+ \r
+ // remove spaces between font families\r
+ $css = preg_replace_callback('/font-family:([^;}]+)([;}])/'\r
+ ,array($this, '_fontFamilyCB'), $css);\r
+ \r
+ $css = preg_replace('/@import\\s+url/', '@import url', $css);\r
+ \r
+ // replace any ws involving newlines with a single newline\r
+ $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);\r
+ \r
+ // separate common descendent selectors w/ newlines (to limit line lengths)\r
+ $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);\r
+ \r
+ // Use newline after 1st numeric value (to limit line lengths).\r
+ $css = preg_replace('/\r
+ ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value\r
+ \\s+\r
+ /x'\r
+ ,"$1\n", $css);\r
+ \r
+ // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/\r
+ $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);\r
+ \r
+ return trim($css);\r
+ }\r
+ \r
+ /**\r
+ * Replace what looks like a set of selectors \r
+ *\r
+ * @param array $m regex matches\r
+ * \r
+ * @return string\r
+ */\r
+ protected function _selectorsCB($m)\r
+ {\r
+ // remove ws around the combinators\r
+ return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);\r
+ }\r
+ \r
+ /**\r
+ * Process a comment and return a replacement\r
+ * \r
+ * @param array $m regex matches\r
+ * \r
+ * @return string\r
+ */\r
+ protected function _commentCB($m)\r
+ {\r
+ $hasSurroundingWs = (trim($m[0]) !== $m[1]);\r
+ $m = $m[1]; \r
+ // $m is the comment content w/o the surrounding tokens, \r
+ // but the return value will replace the entire comment.\r
+ if ($m === 'keep') {\r
+ return '/**/';\r
+ }\r
+ if ($m === '" "') {\r
+ // component of http://tantek.com/CSS/Examples/midpass.html\r
+ return '/*" "*/';\r
+ }\r
+ if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {\r
+ // component of http://tantek.com/CSS/Examples/midpass.html\r
+ return '/*";}}/* */';\r
+ }\r
+ if ($this->_inHack) {\r
+ // inversion: feeding only to one browser\r
+ if (preg_match('@\r
+ ^/ # comment started like /*/\r
+ \\s*\r
+ (\\S[\\s\\S]+?) # has at least some non-ws content\r
+ \\s*\r
+ /\\* # ends like /*/ or /**/\r
+ @x', $m, $n)) {\r
+ // end hack mode after this comment, but preserve the hack and comment content\r
+ $this->_inHack = false;\r
+ return "/*/{$n[1]}/**/";\r
+ }\r
+ }\r
+ if (substr($m, -1) === '\\') { // comment ends like \*/\r
+ // begin hack mode and preserve hack\r
+ $this->_inHack = true;\r
+ return '/*\\*/';\r
+ }\r
+ if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */\r
+ // begin hack mode and preserve hack\r
+ $this->_inHack = true;\r
+ return '/*/*/';\r
+ }\r
+ if ($this->_inHack) {\r
+ // a regular comment ends hack mode but should be preserved\r
+ $this->_inHack = false;\r
+ return '/**/';\r
+ }\r
+ // Issue 107: if there's any surrounding whitespace, it may be important, so \r
+ // replace the comment with a single space\r
+ return $hasSurroundingWs // remove all other comments\r
+ ? ' '\r
+ : '';\r
+ }\r
+ \r
+ /**\r
+ * Process a font-family listing and return a replacement\r
+ * \r
+ * @param array $m regex matches\r
+ * \r
+ * @return string \r
+ */\r
+ protected function _fontFamilyCB($m)\r
+ {\r
+ // Issue 210: must not eliminate WS between words in unquoted families\r
+ $pieces = preg_split('/(\'[^\']+\'|"[^"]+")/', $m[1], null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);\r
+ $out = 'font-family:';\r
+ while (null !== ($piece = array_shift($pieces))) {\r
+ if ($piece[0] !== '"' && $piece[0] !== "'") {\r
+ $piece = preg_replace('/\\s+/', ' ', $piece);\r
+ $piece = preg_replace('/\\s?,\\s?/', ',', $piece);\r
+ }\r
+ $out .= $piece;\r
+ }\r
+ return $out . $m[2];\r
+ }\r
+}\r
*/
class Minify_CSS_UriRewriter {
- /**
- * Defines which class to call as part of callbacks, change this
- * if you extend Minify_CSS_UriRewriter
- * @var string
- */
- protected static $className = 'Minify_CSS_UriRewriter';
-
/**
* rewrite() and rewriteRelative() append debugging information here
+ *
* @var string
*/
public static $debugText = '';
/**
- * Rewrite file relative URIs as root relative in CSS files
+ * In CSS content, rewrite file relative URIs as root relative
*
* @param string $css
*
}
/**
- * Prepend a path to relative URIs in CSS files
+ * In CSS content, prepend a path to relative URIs
*
* @param string $css
*
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
*
* <code>
* Minify_CSS_UriRewriter::rewriteRelative(
self::$debugText .= "docroot stripped : {$path}\n";
// fix to root-relative URI
-
$uri = strtr($path, '/\\', '//');
+ $uri = self::removeDots($uri);
+
+ self::$debugText .= "traversals removed : {$uri}\n\n";
+
+ return $uri;
+ }
- // remove /./ and /../ where possible
+ /**
+ * 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);
-
- self::$debugText .= "traversals removed : {$uri}\n\n";
-
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.
}
return rtrim($path, '/\\');
}
+
+ /**
+ * Directory of this stylesheet
+ *
+ * @var string
+ */
+ private static $_currentDir = '';
+
+ /**
+ * DOC_ROOT
+ *
+ * @var string
+ */
+ private static $_docRoot = '';
+
+ /**
+ * directory replacements to map symlink targets back to their
+ * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath'
+ *
+ * @var array
+ */
+ private static $_symlinks = array();
+
+ /**
+ * Path to prepend
+ *
+ * @var string
+ */
+ private static $_prependPath = null;
+
+ /**
+ * @param string $css
+ *
+ * @return string
+ */
+ private static function _trimUrls($css)
+ {
+ return preg_replace('/
+ url\\( # url(
+ \\s*
+ ([^\\)]+?) # 1 = URI (assuming does not contain ")")
+ \\s*
+ \\) # )
+ /x', 'url($1)', $css);
+ }
+
+ /**
+ * @param array $m
+ *
+ * @return string
+ */
+ 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})";
+ }
}
*/
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);
}
/**
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.
*
$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;
+ $file = $this->_path . '/' . $id;
+ if (! @file_put_contents($file, $data, $flag)) {
+ $this->_log("Minify_Cache_File: Write failed to '$file'");
}
// write control
if ($data !== $this->fetch($id)) {
@unlink($file);
+ $this->_log("Minify_Cache_File: Post-write read failed for '$file'");
return false;
}
return true;
{
return $this->_path;
}
+
+ /**
+ * Get a usable temp directory
+ *
+ * Adapted from Solar/Dir.php
+ * @author Paul M. Jones <pmjones@solarphp.com>
+ * @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 <pmjones@solarphp.com>
+ * @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;
*/
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);
}
/**
--- /dev/null
+<?php
+/**
+ * Class Minify_Cache_ZendPlatform
+ * @package Minify
+ */
+
+
+/**
+ * ZendPlatform-based cache class for Minify
+ *
+ * Based on Minify_Cache_APC, uses output_cache_get/put (currently deprecated)
+ *
+ * <code>
+ * Minify::setCache(new Minify_Cache_ZendPlatform());
+ * </code>
+ *
+ * @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;
+ }
+}
* 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
* @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.
*/
}
$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)
*
* @param array $options controller and Minify options
*
- * return array $options Minify::serve options
+ * @return array $options Minify::serve options
*/
abstract public function setupSources($options);
,'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
* be in subdirectories of these directories.
*
* @return bool file is safe
+ *
+ * @deprecated use checkAllowDirs, checkNotHidden instead
*/
public static function _fileIsSafe($file, $safeDirs)
{
list($revExt) = explode('.', strrev($base));
return in_array(strrev($revExt), array('js', 'css', 'html', 'txt'));
}
-
+
+ /**
+ * @param string $file
+ * @param array $allowDirs
+ * @param string $uri
+ * @return bool
+ * @throws Exception
+ */
+ 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) . "';");
+ }
+
+ /**
+ * @param string $file
+ * @throws Exception
+ */
+ 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.
+ * instances of Minify_Source, which provide content and any individual minification needs.
+ *
+ * @var array
*
* @see Minify_Source
*/
public $sources = array();
+ /**
+ * Short name to place inside cache id
+ *
+ * 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
+ */
+ public $selectionId = '';
+
/**
* Mix in default controller options with user-given options
*
/**
* Send message to the Minify logger
+ *
* @param string $msg
+ *
* @return null
*/
- protected function log($msg) {
+ public function log($msg) {
require_once 'Minify/Logger.php';
Minify_Logger::log($msg);
}
* Set up groups of files as sources
*
* @param array $options controller and Minify options
- * @return array Minify options
- *
- * Controller options:
- *
+ *
* 'groups': (required) array mapping PATH_INFO strings to arrays
- * of complete file paths. @see Minify_Controller_Groups
+ * of complete file paths. @see Minify_Controller_Groups
+ *
+ * @return array Minify options
*/
public function setupSources($options) {
// strip controller options
* Set up groups of files as sources
*
* @param array $options controller and Minify options
+ *
* @return array Minify options
- *
*/
public function setupSources($options) {
// filter controller options
'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 = '';
+ $firstMissingResource = null;
+
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;
+ $keys = explode(',', $_GET['g']);
+ foreach ($keys as $key) {
+ if (! isset($cOptions['groups'][$key])) {
+ $this->log("A group configuration for \"{$key}\" was not found");
+ return $options;
}
- if (0 === strpos($file, '//')) {
- $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1);
+ $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;
}
- $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)");
- return $options;
+ 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)");
+ if (null === $firstMissingResource) {
+ $firstMissingResource = basename($file);
+ continue;
+ } else {
+ $secondMissingResource = basename($file);
+ $this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource'");
+ 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'])) {
// 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 {
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()");
+ $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 (null === $firstMissingResource) {
+ $firstMissingResource = $uri;
+ continue;
+ } else {
+ $secondMissingResource = $uri;
+ $this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource`'");
+ return $options;
+ }
+ }
+ try {
+ parent::checkNotHidden($realpath);
+ parent::checkAllowDirs($realpath, $allowDirs, $uri);
+ } catch (Exception $e) {
+ $this->log($e->getMessage());
return $options;
- } else {
- $sources[] = new Minify_Source(array(
- 'filepath' => $file
- ));
}
+ $sources[] = $this->_getFileSource($realpath, $cOptions);
+ $basenames[] = basename($realpath, $ext);
+ }
+ if ($this->selectionId) {
+ $this->selectionId .= '_f=';
}
+ $this->selectionId .= implode(',', $basenames) . $ext;
}
if ($sources) {
+ if (null !== $firstMissingResource) {
+ array_unshift($sources, new Minify_Source(array(
+ 'id' => 'missingFile'
+ // should not cause cache invalidation
+ ,'lastModified' => 0
+ // due to caching, filename is unreliable.
+ ,'content' => "/* Minify: at least one missing file. See " . Minify::URL_DEBUG . " */\n"
+ ,'minifier' => ''
+ )));
+ }
$this->sources = $sources;
} else {
$this->log("No sources to serve");
}
return $options;
}
+
+ /**
+ * @param string $file
+ *
+ * @param array $cOptions
+ *
+ * @return Minify_Source
+ */
+ 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
+ *
+ * @param string $sourceOrExt
+ *
+ * @throws Exception
+ */
+ 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');
+ }
+ }
}
$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(
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)
*
* <code>
* Minify::serve('Version1');
--- /dev/null
+<?php\r
+\r
+/**\r
+ * Detect whether request should be debugged\r
+ *\r
+ * @package Minify\r
+ * @author Stephen Clay <steve@mrclay.org>\r
+ */\r
+class Minify_DebugDetector {\r
+ public static function shouldDebugRequest($cookie, $get, $requestUri)\r
+ {\r
+ if (isset($get['debug'])) {\r
+ return true;\r
+ }\r
+ if (! empty($cookie['minifyDebug'])) {\r
+ foreach (preg_split('/\\s+/', $cookie['minifyDebug']) as $debugUri) {\r
+ $pattern = '@' . preg_quote($debugUri, '@') . '@i';\r
+ $pattern = str_replace(array('\\*', '\\?'), array('.*', '.'), $pattern);\r
+ if (preg_match($pattern, $requestUri)) {\r
+ return true;\r
+ }\r
+ }\r
+ }\r
+ return false;\r
+ }\r
+}\r
-<?php
-/**
- * Class Minify_HTML
- * @package Minify
- */
-
-/**
- * Compress HTML
- *
- * This is a heavy regex-based removal of whitespace, unnecessary comments and
- * tokens. IE conditional comments are preserved. There are also options to have
- * STYLE and SCRIPT blocks compressed by callback functions.
- *
- * A test suite is available.
- *
- * @package Minify
- * @author Stephen Clay <steve@mrclay.org>
- */
-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, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML'));
- }
-
- $this->_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']);
- $this->_placeholders = array();
-
- // replace SCRIPTs (and minify) with placeholders
- $this->_html = preg_replace_callback(
- '/(\\s*)(<script\\b[^>]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i'
- ,array($this, '_removeScriptCB')
- ,$this->_html);
-
- // replace STYLEs (and minify) with placeholders
- $this->_html = preg_replace_callback(
- '/\\s*(<style\\b[^>]*?>)([\\s\\S]*?)<\\/style>\\s*/i'
- ,array($this, '_removeStyleCB')
- ,$this->_html);
-
- // remove HTML comments (not containing IE conditional comments).
- $this->_html = preg_replace_callback(
- '/<!--([\\s\\S]*?)-->/'
- ,array($this, '_commentCB')
- ,$this->_html);
-
- // replace PREs with placeholders
- $this->_html = preg_replace_callback('/\\s*(<pre\\b[^>]*?>[\\s\\S]*?<\\/pre>)\\s*/i'
- ,array($this, '_removePreCB')
- ,$this->_html);
-
- // replace TEXTAREAs with placeholders
- $this->_html = preg_replace_callback(
- '/\\s*(<textarea\\b[^>]*?>[\\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(
- '/>([^<]+)</'
- ,array($this, '_outsideTagCB')
- ,$this->_html);
-
- // use newlines before 1st attribute in open tags (to limit line lengths)
- $this->_html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $this->_html);
-
- // fill placeholders
- $this->_html = str_replace(
- array_keys($this->_placeholders)
- ,array_values($this->_placeholders)
- ,$this->_html
- );
- return $this->_html;
- }
-
- protected function _commentCB($m)
- {
- return (0 === strpos($m[1], '[') || false !== strpos($m[1], '<!['))
- ? $m[0]
- : '';
- }
-
- protected function _reservePlace($content)
- {
- $placeholder = '%' . $this->_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}/*<![CDATA[*/{$css}/*]]>*/</style>"
- : "{$openStyle}{$css}</style>"
- );
- }
-
- 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*|\\s*(?:\\/\\/)?\\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}/*<![CDATA[*/{$js}/*]]>*/</script>{$ws2}"
- : "{$ws1}{$openScript}{$js}</script>{$ws2}"
- );
- }
-
- protected function _removeCdata($str)
- {
- return (false !== strpos($str, '<![CDATA['))
- ? str_replace(array('<![CDATA[', ']]>'), '', $str)
- : $str;
- }
-
- protected function _needsCdata($str)
- {
- return ($this->_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str));
- }
-}
+<?php\r
+/**\r
+ * Class Minify_HTML \r
+ * @package Minify\r
+ */\r
+\r
+/**\r
+ * Compress HTML\r
+ *\r
+ * This is a heavy regex-based removal of whitespace, unnecessary comments and \r
+ * tokens. IE conditional comments are preserved. There are also options to have\r
+ * STYLE and SCRIPT blocks compressed by callback functions. \r
+ * \r
+ * A test suite is available.\r
+ * \r
+ * @package Minify\r
+ * @author Stephen Clay <steve@mrclay.org>\r
+ */\r
+class Minify_HTML {\r
+\r
+ /**\r
+ * "Minify" an HTML page\r
+ *\r
+ * @param string $html\r
+ *\r
+ * @param array $options\r
+ *\r
+ * 'cssMinifier' : (optional) callback function to process content of STYLE\r
+ * elements.\r
+ * \r
+ * 'jsMinifier' : (optional) callback function to process content of SCRIPT\r
+ * elements. Note: the type attribute is ignored.\r
+ * \r
+ * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If\r
+ * unset, minify will sniff for an XHTML doctype.\r
+ * \r
+ * @return string\r
+ */\r
+ public static function minify($html, $options = array()) {\r
+ $min = new Minify_HTML($html, $options);\r
+ return $min->process();\r
+ }\r
+ \r
+ \r
+ /**\r
+ * Create a minifier object\r
+ *\r
+ * @param string $html\r
+ *\r
+ * @param array $options\r
+ *\r
+ * 'cssMinifier' : (optional) callback function to process content of STYLE\r
+ * elements.\r
+ * \r
+ * 'jsMinifier' : (optional) callback function to process content of SCRIPT\r
+ * elements. Note: the type attribute is ignored.\r
+ * \r
+ * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If\r
+ * unset, minify will sniff for an XHTML doctype.\r
+ * \r
+ * @return null\r
+ */\r
+ public function __construct($html, $options = array())\r
+ {\r
+ $this->_html = str_replace("\r\n", "\n", trim($html));\r
+ if (isset($options['xhtml'])) {\r
+ $this->_isXhtml = (bool)$options['xhtml'];\r
+ }\r
+ if (isset($options['cssMinifier'])) {\r
+ $this->_cssMinifier = $options['cssMinifier'];\r
+ }\r
+ if (isset($options['jsMinifier'])) {\r
+ $this->_jsMinifier = $options['jsMinifier'];\r
+ }\r
+ }\r
+ \r
+ \r
+ /**\r
+ * Minify the markeup given in the constructor\r
+ * \r
+ * @return string\r
+ */\r
+ public function process()\r
+ {\r
+ if ($this->_isXhtml === null) {\r
+ $this->_isXhtml = (false !== strpos($this->_html, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML'));\r
+ }\r
+ \r
+ $this->_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']);\r
+ $this->_placeholders = array();\r
+ \r
+ // replace SCRIPTs (and minify) with placeholders\r
+ $this->_html = preg_replace_callback(\r
+ '/(\\s*)<script(\\b[^>]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i'\r
+ ,array($this, '_removeScriptCB')\r
+ ,$this->_html);\r
+ \r
+ // replace STYLEs (and minify) with placeholders\r
+ $this->_html = preg_replace_callback(\r
+ '/\\s*<style(\\b[^>]*>)([\\s\\S]*?)<\\/style>\\s*/i'\r
+ ,array($this, '_removeStyleCB')\r
+ ,$this->_html);\r
+ \r
+ // remove HTML comments (not containing IE conditional comments).\r
+ $this->_html = preg_replace_callback(\r
+ '/<!--([\\s\\S]*?)-->/'\r
+ ,array($this, '_commentCB')\r
+ ,$this->_html);\r
+ \r
+ // replace PREs with placeholders\r
+ $this->_html = preg_replace_callback('/\\s*<pre(\\b[^>]*?>[\\s\\S]*?<\\/pre>)\\s*/i'\r
+ ,array($this, '_removePreCB')\r
+ ,$this->_html);\r
+ \r
+ // replace TEXTAREAs with placeholders\r
+ $this->_html = preg_replace_callback(\r
+ '/\\s*<textarea(\\b[^>]*?>[\\s\\S]*?<\\/textarea>)\\s*/i'\r
+ ,array($this, '_removeTextareaCB')\r
+ ,$this->_html);\r
+ \r
+ // trim each line.\r
+ // @todo take into account attribute values that span multiple lines.\r
+ $this->_html = preg_replace('/^\\s+|\\s+$/m', '', $this->_html);\r
+ \r
+ // remove ws around block/undisplayed elements\r
+ $this->_html = preg_replace('/\\s+(<\\/?(?:area|base(?:font)?|blockquote|body'\r
+ .'|caption|center|cite|col(?:group)?|dd|dir|div|dl|dt|fieldset|form'\r
+ .'|frame(?:set)?|h[1-6]|head|hr|html|legend|li|link|map|menu|meta'\r
+ .'|ol|opt(?:group|ion)|p|param|t(?:able|body|head|d|h||r|foot|itle)'\r
+ .'|ul)\\b[^>]*>)/i', '$1', $this->_html);\r
+ \r
+ // remove ws outside of all elements\r
+ $this->_html = preg_replace(\r
+ '/>(\\s(?:\\s*))?([^<]+)(\\s(?:\s*))?</'\r
+ ,'>$1$2$3<'\r
+ ,$this->_html);\r
+ \r
+ // use newlines before 1st attribute in open tags (to limit line lengths)\r
+ $this->_html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $this->_html);\r
+ \r
+ // fill placeholders\r
+ $this->_html = str_replace(\r
+ array_keys($this->_placeholders)\r
+ ,array_values($this->_placeholders)\r
+ ,$this->_html\r
+ );\r
+ // issue 229: multi-pass to catch scripts that didn't get replaced in textareas\r
+ $this->_html = str_replace(\r
+ array_keys($this->_placeholders)\r
+ ,array_values($this->_placeholders)\r
+ ,$this->_html\r
+ );\r
+ return $this->_html;\r
+ }\r
+ \r
+ protected function _commentCB($m)\r
+ {\r
+ return (0 === strpos($m[1], '[') || false !== strpos($m[1], '<!['))\r
+ ? $m[0]\r
+ : '';\r
+ }\r
+ \r
+ protected function _reservePlace($content)\r
+ {\r
+ $placeholder = '%' . $this->_replacementHash . count($this->_placeholders) . '%';\r
+ $this->_placeholders[$placeholder] = $content;\r
+ return $placeholder;\r
+ }\r
+\r
+ protected $_isXhtml = null;\r
+ protected $_replacementHash = null;\r
+ protected $_placeholders = array();\r
+ protected $_cssMinifier = null;\r
+ protected $_jsMinifier = null;\r
+\r
+ protected function _removePreCB($m)\r
+ {\r
+ return $this->_reservePlace("<pre{$m[1]}");\r
+ }\r
+ \r
+ protected function _removeTextareaCB($m)\r
+ {\r
+ return $this->_reservePlace("<textarea{$m[1]}");\r
+ }\r
+\r
+ protected function _removeStyleCB($m)\r
+ {\r
+ $openStyle = "<style{$m[1]}";\r
+ $css = $m[2];\r
+ // remove HTML comments\r
+ $css = preg_replace('/(?:^\\s*<!--|-->\\s*$)/', '', $css);\r
+ \r
+ // remove CDATA section markers\r
+ $css = $this->_removeCdata($css);\r
+ \r
+ // minify\r
+ $minifier = $this->_cssMinifier\r
+ ? $this->_cssMinifier\r
+ : 'trim';\r
+ $css = call_user_func($minifier, $css);\r
+ \r
+ return $this->_reservePlace($this->_needsCdata($css)\r
+ ? "{$openStyle}/*<![CDATA[*/{$css}/*]]>*/</style>"\r
+ : "{$openStyle}{$css}</style>"\r
+ );\r
+ }\r
+\r
+ protected function _removeScriptCB($m)\r
+ {\r
+ $openScript = "<script{$m[2]}";\r
+ $js = $m[3];\r
+ \r
+ // whitespace surrounding? preserve at least one space\r
+ $ws1 = ($m[1] === '') ? '' : ' ';\r
+ $ws2 = ($m[4] === '') ? '' : ' ';\r
+ \r
+ // remove HTML comments (and ending "//" if present)\r
+ $js = preg_replace('/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/', '', $js);\r
+ \r
+ // remove CDATA section markers\r
+ $js = $this->_removeCdata($js);\r
+ \r
+ // minify\r
+ $minifier = $this->_jsMinifier\r
+ ? $this->_jsMinifier\r
+ : 'trim'; \r
+ $js = call_user_func($minifier, $js);\r
+ \r
+ return $this->_reservePlace($this->_needsCdata($js)\r
+ ? "{$ws1}{$openScript}/*<![CDATA[*/{$js}/*]]>*/</script>{$ws2}"\r
+ : "{$ws1}{$openScript}{$js}</script>{$ws2}"\r
+ );\r
+ }\r
+\r
+ protected function _removeCdata($str)\r
+ {\r
+ return (false !== strpos($str, '<![CDATA['))\r
+ ? str_replace(array('<![CDATA[', ']]>'), '', $str)\r
+ : $str;\r
+ }\r
+ \r
+ protected function _needsCdata($str)\r
+ {\r
+ return ($this->_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str));\r
+ }\r
+}\r
--- /dev/null
+<?php
+/**
+ * Class Minify_HTML_Helper
+ * @package Minify
+ */
+
+/**
+ * Helpers for writing Minfy URIs into HTML
+ *
+ * @package Minify
+ * @author Stephen Clay <steve@mrclay.org>
+ */
+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=<paths>
+ 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;
+ }
+}
-<?php
-/**
- * Class Minify_ImportProcessor
- * @package Minify
- */
-
-/**
- * Linearize a CSS/JS file by including content specified by CSS import
- * declarations. In CSS files, relative URIs are fixed.
- *
- * @imports will be processed regardless of where they appear in the source
- * files; i.e. @imports commented out or in string content will still be
- * processed!
- *
- * This has a unit test but should be considered "experimental".
- *
- * @package Minify
- * @author Stephen Clay <steve@mrclay.org>
- */
-class Minify_ImportProcessor {
-
- public static $filesIncluded = array();
-
- public static function process($file)
- {
- self::$filesIncluded = array();
- self::$_isCss = (strtolower(substr($file, -4)) === '.css');
- $obj = new Minify_ImportProcessor(dirname($file));
- return $obj->_getContent($file);
- }
-
- // allows callback funcs to know the current directory
- private $_currentDir = null;
-
- // allows _importCB to write the fetched content back to the obj
- private $_importedContent = '';
-
- private static $_isCss = null;
-
- private function __construct($currentDir)
- {
- $this->_currentDir = $currentDir;
- }
-
- private function _getContent($file)
- {
- $file = realpath($file);
- if (! $file
- || in_array($file, self::$filesIncluded)
- || false === ($content = @file_get_contents($file))
- ) {
- // file missing, already included, or failed read
- return '';
- }
- self::$filesIncluded[] = realpath($file);
- $this->_currentDir = dirname($file);
-
- // remove UTF-8 BOM if present
- if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) {
- $content = substr($content, 3);
- }
- // ensure uniform EOLs
- $content = str_replace("\r\n", "\n", $content);
-
- // process @imports
- $content = preg_replace_callback(
- '/
- @import\\s+
- (?:url\\(\\s*)? # maybe url(
- [\'"]? # maybe quote
- (.*?) # 1 = URI
- [\'"]? # maybe end quote
- (?:\\s*\\))? # maybe )
- ([a-zA-Z,\\s]*)? # 2 = media list
- ; # end token
- /x'
- ,array($this, '_importCB')
- ,$content
- );
-
- if (self::$_isCss) {
- // rewrite remaining relative URIs
- $content = preg_replace_callback(
- '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
- ,array($this, '_urlCB')
- ,$content
- );
- }
-
- return $this->_importedContent . $content;
- }
-
- private function _importCB($m)
- {
- $url = $m[1];
- $mediaList = preg_replace('/\\s+/', '', $m[2]);
-
- if (strpos($url, '://') > 0) {
- // protocol, leave in place for CSS, comment for JS
- return self::$_isCss
- ? $m[0]
- : "/* Minify_ImportProcessor will not include remote content */";
- }
- if ('/' === $url[0]) {
- // protocol-relative or root path
- $url = ltrim($url, '/');
- $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR
- . strtr($url, '/', DIRECTORY_SEPARATOR);
- } else {
- // relative to current path
- $file = $this->_currentDir . DIRECTORY_SEPARATOR
- . strtr($url, '/', DIRECTORY_SEPARATOR);
- }
- $obj = new Minify_ImportProcessor(dirname($file));
- $content = $obj->_getContent($file);
- if ('' === $content) {
- // failed. leave in place for CSS, comment for JS
- return self::$_isCss
- ? $m[0]
- : "/* Minify_ImportProcessor could not fetch '{$file}' */";;
- }
- return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList))
- ? $content
- : "@media {$mediaList} {\n{$content}\n}\n";
- }
-
- private function _urlCB($m)
- {
- // $m[1] is either quoted or not
- $quote = ($m[1][0] === "'" || $m[1][0] === '"')
- ? $m[1][0]
- : '';
- $url = ($quote === '')
- ? $m[1]
- : substr($m[1], 1, strlen($m[1]) - 2);
- if ('/' !== $url[0]) {
- if (strpos($url, '//') > 0) {
- // probably starts with protocol, do not alter
- } else {
- // prepend path with current dir separator (OS-independent)
- $path = $this->_currentDir
- . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR);
- // strip doc root
- $path = substr($path, strlen(realpath($_SERVER['DOCUMENT_ROOT'])));
- // fix to absolute URL
- $url = strtr($path, '/\\', '//');
- // remove /./ and /../ where possible
- $url = str_replace('/./', '/', $url);
- // inspired by patch from Oleg Cherniy
- do {
- $url = preg_replace('@/[^/]+/\\.\\./@', '/', $url, 1, $changed);
- } while ($changed);
- }
- }
- return "url({$quote}{$url}{$quote})";
- }
-}
+<?php\r
+/**\r
+ * Class Minify_ImportProcessor\r
+ * @package Minify\r
+ */\r
+\r
+/**\r
+ * Linearize a CSS/JS file by including content specified by CSS import\r
+ * declarations. In CSS files, relative URIs are fixed.\r
+ *\r
+ * @imports will be processed regardless of where they appear in the source\r
+ * files; i.e. @imports commented out or in string content will still be\r
+ * processed!\r
+ *\r
+ * This has a unit test but should be considered "experimental".\r
+ *\r
+ * @package Minify\r
+ * @author Stephen Clay <steve@mrclay.org>\r
+ * @author Simon Schick <simonsimcity@gmail.com>\r
+ */\r
+class Minify_ImportProcessor {\r
+\r
+ public static $filesIncluded = array();\r
+\r
+ public static function process($file)\r
+ {\r
+ self::$filesIncluded = array();\r
+ self::$_isCss = (strtolower(substr($file, -4)) === '.css');\r
+ $obj = new Minify_ImportProcessor(dirname($file));\r
+ return $obj->_getContent($file);\r
+ }\r
+\r
+ // allows callback funcs to know the current directory\r
+ private $_currentDir = null;\r
+\r
+ // allows callback funcs to know the directory of the file that inherits this one\r
+ private $_previewsDir = null;\r
+\r
+ // allows _importCB to write the fetched content back to the obj\r
+ private $_importedContent = '';\r
+\r
+ private static $_isCss = null;\r
+\r
+ /**\r
+ * @param String $currentDir\r
+ * @param String $previewsDir Is only used internally\r
+ */\r
+ private function __construct($currentDir, $previewsDir = "")\r
+ {\r
+ $this->_currentDir = $currentDir;\r
+ $this->_previewsDir = $previewsDir;\r
+ }\r
+\r
+ private function _getContent($file, $is_imported = false)\r
+ {\r
+ $file = realpath($file);\r
+ if (! $file\r
+ || in_array($file, self::$filesIncluded)\r
+ || false === ($content = @file_get_contents($file))\r
+ ) {\r
+ // file missing, already included, or failed read\r
+ return '';\r
+ }\r
+ self::$filesIncluded[] = realpath($file);\r
+ $this->_currentDir = dirname($file);\r
+\r
+ // remove UTF-8 BOM if present\r
+ if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) {\r
+ $content = substr($content, 3);\r
+ }\r
+ // ensure uniform EOLs\r
+ $content = str_replace("\r\n", "\n", $content);\r
+\r
+ // process @imports\r
+ $content = preg_replace_callback(\r
+ '/\r
+ @import\\s+\r
+ (?:url\\(\\s*)? # maybe url(\r
+ [\'"]? # maybe quote\r
+ (.*?) # 1 = URI\r
+ [\'"]? # maybe end quote\r
+ (?:\\s*\\))? # maybe )\r
+ ([a-zA-Z,\\s]*)? # 2 = media list\r
+ ; # end token\r
+ /x'\r
+ ,array($this, '_importCB')\r
+ ,$content\r
+ );\r
+\r
+ // You only need to rework the import-path if the script is imported\r
+ if (self::$_isCss && $is_imported) {\r
+ // rewrite remaining relative URIs\r
+ $content = preg_replace_callback(\r
+ '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'\r
+ ,array($this, '_urlCB')\r
+ ,$content\r
+ );\r
+ }\r
+\r
+ return $this->_importedContent . $content;\r
+ }\r
+\r
+ private function _importCB($m)\r
+ {\r
+ $url = $m[1];\r
+ $mediaList = preg_replace('/\\s+/', '', $m[2]);\r
+\r
+ if (strpos($url, '://') > 0) {\r
+ // protocol, leave in place for CSS, comment for JS\r
+ return self::$_isCss\r
+ ? $m[0]\r
+ : "/* Minify_ImportProcessor will not include remote content */";\r
+ }\r
+ if ('/' === $url[0]) {\r
+ // protocol-relative or root path\r
+ $url = ltrim($url, '/');\r
+ $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR\r
+ . strtr($url, '/', DIRECTORY_SEPARATOR);\r
+ } else {\r
+ // relative to current path\r
+ $file = $this->_currentDir . DIRECTORY_SEPARATOR\r
+ . strtr($url, '/', DIRECTORY_SEPARATOR);\r
+ }\r
+ $obj = new Minify_ImportProcessor(dirname($file), $this->_currentDir);\r
+ $content = $obj->_getContent($file, true);\r
+ if ('' === $content) {\r
+ // failed. leave in place for CSS, comment for JS\r
+ return self::$_isCss\r
+ ? $m[0]\r
+ : "/* Minify_ImportProcessor could not fetch '{$file}' */";\r
+ }\r
+ return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList))\r
+ ? $content\r
+ : "@media {$mediaList} {\n{$content}\n}\n";\r
+ }\r
+\r
+ private function _urlCB($m)\r
+ {\r
+ // $m[1] is either quoted or not\r
+ $quote = ($m[1][0] === "'" || $m[1][0] === '"')\r
+ ? $m[1][0]\r
+ : '';\r
+ $url = ($quote === '')\r
+ ? $m[1]\r
+ : substr($m[1], 1, strlen($m[1]) - 2);\r
+ if ('/' !== $url[0]) {\r
+ if (strpos($url, '//') > 0) {\r
+ // probably starts with protocol, do not alter\r
+ } else {\r
+ // prepend path with current dir separator (OS-independent)\r
+ $path = $this->_currentDir\r
+ . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR);\r
+ // update the relative path by the directory of the file that imported this one\r
+ $url = self::getPathDiff(realpath($this->_previewsDir), $path);\r
+ }\r
+ }\r
+ return "url({$quote}{$url}{$quote})";\r
+ }\r
+\r
+ /**\r
+ * @param string $from\r
+ * @param string $to\r
+ * @param string $ps\r
+ * @return string\r
+ */\r
+ private function getPathDiff($from, $to, $ps = DIRECTORY_SEPARATOR)\r
+ {\r
+ $realFrom = $this->truepath($from);\r
+ $realTo = $this->truepath($to);\r
+\r
+ $arFrom = explode($ps, rtrim($realFrom, $ps));\r
+ $arTo = explode($ps, rtrim($realTo, $ps));\r
+ while (count($arFrom) && count($arTo) && ($arFrom[0] == $arTo[0]))\r
+ {\r
+ array_shift($arFrom);\r
+ array_shift($arTo);\r
+ }\r
+ return str_pad("", count($arFrom) * 3, '..' . $ps) . implode($ps, $arTo);\r
+ }\r
+\r
+ /**\r
+ * This function is to replace PHP's extremely buggy realpath().\r
+ * @param string $path The original path, can be relative etc.\r
+ * @return string The resolved path, it might not exist.\r
+ * @see http://stackoverflow.com/questions/4049856/replace-phps-realpath\r
+ */\r
+ function truepath($path)\r
+ {\r
+ // whether $path is unix or not\r
+ $unipath = strlen($path) == 0 || $path{0} != '/';\r
+ // attempts to detect if path is relative in which case, add cwd\r
+ if (strpos($path, ':') === false && $unipath)\r
+ $path = $this->_currentDir . DIRECTORY_SEPARATOR . $path;\r
+\r
+ // resolve path parts (single dot, double dot and double delimiters)\r
+ $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);\r
+ $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');\r
+ $absolutes = array();\r
+ foreach ($parts as $part) {\r
+ if ('.' == $part)\r
+ continue;\r
+ if ('..' == $part) {\r
+ array_pop($absolutes);\r
+ } else {\r
+ $absolutes[] = $part;\r
+ }\r
+ }\r
+ $path = implode(DIRECTORY_SEPARATOR, $absolutes);\r
+ // resolve any symlinks\r
+ if (file_exists($path) && linkinfo($path) > 0)\r
+ $path = readlink($path);\r
+ // put initial separator that could have been lost\r
+ $path = !$unipath ? '/' . $path : $path;\r
+ return $path;\r
+ }\r
+}\r
--- /dev/null
+<?php
+/**
+ * Class Minify_JS_ClosureCompiler
+ * @package Minify
+ */
+
+/**
+ * Minify Javascript using Google's Closure Compiler API
+ *
+ * @link http://code.google.com/closure/compiler/
+ * @package Minify
+ * @author Stephen Clay <steve@mrclay.org>
+ *
+ * @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)
+ {
+ $allowUrlFopen = preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen'));
+ if ($allowUrlFopen) {
+ $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,
+ )
+ )));
+ } elseif (defined('CURLOPT_POST')) {
+ $ch = curl_init(self::URL);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded'));
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $postBody);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
+ $contents = curl_exec($ch);
+ curl_close($ch);
+ } else {
+ throw new Minify_JS_ClosureCompiler_Exception(
+ "Could not make HTTP request: allow_url_open is false and cURL not available"
+ );
+ }
+ 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 {}
? $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();
*
* @param bool $inComment was the parser in a comment at the
* beginning of the line?
- *
+ *
* @return bool
*/
private static function _eolInComment($line, $inComment)
*
* @package Minify
* @author Stephen Clay <steve@mrclay.org>
+ *
+ * @todo lose this singleton! pass log object in Minify::serve and distribute to others
*/
class Minify_Logger {
--- /dev/null
+/*
+ * YUI Compressor
+ * http://developer.yahoo.com/yui/compressor/
+ * Author: Julien Lecomte - http://www.julienlecomte.net/
+ * Author: Isaac Schlueter - http://foohack.com/
+ * Author: Stoyan Stefanov - http://phpied.com/
+ * Copyright (c) 2011 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);
+ }
+ }
+
+ // Leave data urls alone to increase parse performance.
+ protected String extractDataUrls(String css, ArrayList preservedTokens) {
+
+ int maxIndex = css.length() - 1;
+ int appendIndex = 0;
+
+ StringBuffer sb = new StringBuffer();
+
+ Pattern p = Pattern.compile("url\\(\\s*([\"']?)data\\:");
+ Matcher m = p.matcher(css);
+
+ /*
+ * Since we need to account for non-base64 data urls, we need to handle
+ * ' and ) being part of the data string. Hence switching to indexOf,
+ * to determine whether or not we have matching string terminators and
+ * handling sb appends directly, instead of using matcher.append* methods.
+ */
+
+ while (m.find()) {
+
+ int startIndex = m.start() + 4; // "url(".length()
+ String terminator = m.group(1); // ', " or empty (not quoted)
+
+ if (terminator.length() == 0) {
+ terminator = ")";
+ }
+
+ boolean foundTerminator = false;
+
+ int endIndex = m.end() - 1;
+ while(foundTerminator == false && endIndex+1 <= maxIndex) {
+ endIndex = css.indexOf(terminator, endIndex+1);
+
+ if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) {
+ foundTerminator = true;
+ if (!")".equals(terminator)) {
+ endIndex = css.indexOf(")", endIndex);
+ }
+ }
+ }
+
+ // Enough searching, start moving stuff over to the buffer
+ sb.append(css.substring(appendIndex, m.start()));
+
+ if (foundTerminator) {
+ String token = css.substring(startIndex, endIndex);
+ token = token.replaceAll("\\s+", "");
+ preservedTokens.add(token);
+
+ String preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)";
+ sb.append(preserver);
+
+ appendIndex = endIndex + 1;
+ } else {
+ // No end terminator found, re-add the whole match. Should we throw/warn here?
+ sb.append(css.substring(m.start(), m.end()));
+ appendIndex = m.end();
+ }
+ }
+
+ sb.append(css.substring(appendIndex));
+
+ return sb.toString();
+ }
+
+ public void compress(Writer out, int linebreakpos)
+ throws IOException {
+
+ Pattern p;
+ Matcher m;
+ String css = srcsb.toString();
+
+ 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;
+
+ css = this.extractDataUrls(css, preservedTokens);
+
+ StringBuffer sb = new StringBuffer(css);
+
+ // 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.
+ // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} )
+ // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD)
+ p = Pattern.compile("(\\=\\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])" + "(:?\\}|[^0-9a-fA-F{][^{]*?\\})");
+
+ m = p.matcher(css);
+ sb = new StringBuffer();
+ int index = 0;
+
+ while (m.find(index)) {
+
+ sb.append(css.substring(index, m.start()));
+
+ boolean isFilter = (m.group(1) != null && !"".equals(m.group(1)));
+
+ if (isFilter) {
+ // Restore, as is. Compression will break filters
+ sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7));
+ } else {
+ if( m.group(2).equalsIgnoreCase(m.group(3)) &&
+ m.group(4).equalsIgnoreCase(m.group(5)) &&
+ m.group(6).equalsIgnoreCase(m.group(7))) {
+
+ // #AABBCC pattern
+ sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase());
+
+ } else {
+
+ // Non-compressible color, restore, but lower case.
+ sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase());
+ }
+ }
+
+ index = m.end(7);
+ }
+
+ sb.append(css.substring(index));
+ 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("[^\\}\\{/;]+\\{\\}", "");
+
+ // TODO: Should this be after we re-insert tokens. These could alter the break points. However then
+ // we'd need to make sure we don't break in the middle of a string etc.
+ 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);
+ }
+}
--- /dev/null
+<?php
+/**
+ * Class Minify_YUI_CssCompressor
+ * @package Minify
+ *
+ * YUI Compressor
+ * Author: Julien Lecomte - http://www.julienlecomte.net/
+ * Author: Isaac Schlueter - http://foohack.com/
+ * Author: Stoyan Stefanov - http://phpied.com/
+ * Author: Steve Clay - http://www.mrclay.org/ (PHP port)
+ * 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.
+ */
+
+/**
+ * Compress CSS (incomplete DO NOT USE)
+ *
+ * @see https://github.com/yui/yuicompressor/blob/master/src/com/yahoo/platform/yui/compressor/CssCompressor.java
+ *
+ * @package Minify
+ */
+class Minify_YUI_CssCompressor {
+
+ /**
+ * Minify a CSS string
+ *
+ * @param string $css
+ *
+ * @return string
+ */
+ public function compress($css, $linebreakpos = 0)
+ {
+ $css = str_replace("\r\n", "\n", $css);
+
+ /**
+ * @todo comment removal
+ * @todo re-port from newer Java version
+ */
+
+ // Normalize all whitespace strings to single spaces. Easier to work with that way.
+ $css = preg_replace('@\s+@', ' ', $css);
+
+ // Make a pseudo class for the Box Model Hack
+ $css = preg_replace("@\"\\\\\"}\\\\\"\"@", "___PSEUDOCLASSBMH___", $css);
+
+ // 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.
+ $css = preg_replace_callback("@(^|\\})(([^\\{:])+:)+([^\\{]*\\{)@", array($this, '_removeSpacesCB'), $css);
+
+ $css = preg_replace("@\\s+([!{};:>+\\(\\)\\],])@", "$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
throw new Exception('Minify_YUICompressor : could not create temp file.');
}
file_put_contents($tmpFile, $content);
- exec(self::_getCmd($options, $type, $tmpFile), $output);
+ exec(self::_getCmd($options, $type, $tmpFile), $output, $result_code);
unlink($tmpFile);
+ if ($result_code != 0) {
+ throw new Exception('Minify_YUICompressor : YUI compressor execution failed.');
+ }
return implode("\n", $output);
}
);
$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
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_executable(self::$jarFile)) {
+ throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not executable.');
+ }
+ 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.');
}
}
}
--- /dev/null
+<?php
+
+namespace MrClay;
+
+/**
+ * Forms a front controller for a console app, handling and validating arguments (options)
+ *
+ * Instantiate, add arguments, then call validate(). Afterwards, the user's valid arguments
+ * and their values will be available in $cli->values.
+ *
+ * You may also specify that some arguments be used to provide input/output. By communicating
+ * solely through the file pointers provided by openInput()/openOutput(), you can make your
+ * app more flexible to end users.
+ *
+ * @author Steve Clay <steve@mrclay.org>
+ * @license http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+class Cli {
+
+ /**
+ * @var array validation errors
+ */
+ public $errors = array();
+
+ /**
+ * @var array option values available after validation.
+ *
+ * E.g. array(
+ * 'a' => false // option was missing
+ * ,'b' => true // option was present
+ * ,'c' => "Hello" // option had value
+ * ,'f' => "/home/user/file" // file path from root
+ * ,'f.raw' => "~/file" // file path as given to option
+ * )
+ */
+ public $values = array();
+
+ /**
+ * @var array
+ */
+ public $moreArgs = array();
+
+ /**
+ * @var array
+ */
+ public $debug = array();
+
+ /**
+ * @var bool The user wants help info
+ */
+ public $isHelpRequest = false;
+
+ /**
+ * @var array of Cli\Arg
+ */
+ protected $_args = array();
+
+ /**
+ * @var resource
+ */
+ protected $_stdin = null;
+
+ /**
+ * @var resource
+ */
+ protected $_stdout = null;
+
+ /**
+ * @param bool $exitIfNoStdin (default true) Exit() if STDIN is not defined
+ */
+ public function __construct($exitIfNoStdin = true)
+ {
+ if ($exitIfNoStdin && ! defined('STDIN')) {
+ exit('This script is for command-line use only.');
+ }
+ if (isset($GLOBALS['argv'][1])
+ && ($GLOBALS['argv'][1] === '-?' || $GLOBALS['argv'][1] === '--help')) {
+ $this->isHelpRequest = true;
+ }
+ }
+
+ /**
+ * @param Cli\Arg|string $letter
+ * @return Cli\Arg
+ */
+ public function addOptionalArg($letter)
+ {
+ return $this->addArgument($letter, false);
+ }
+
+ /**
+ * @param Cli\Arg|string $letter
+ * @return Cli\Arg
+ */
+ public function addRequiredArg($letter)
+ {
+ return $this->addArgument($letter, true);
+ }
+
+ /**
+ * @param string $letter
+ * @param bool $required
+ * @param Cli\Arg|null $arg
+ * @return Cli\Arg
+ * @throws \InvalidArgumentException
+ */
+ public function addArgument($letter, $required, Cli\Arg $arg = null)
+ {
+ if (! preg_match('/^[a-zA-Z]$/', $letter)) {
+ throw new \InvalidArgumentException('$letter must be in [a-zA-z]');
+ }
+ if (! $arg) {
+ $arg = new Cli\Arg($required);
+ }
+ $this->_args[$letter] = $arg;
+ return $arg;
+ }
+
+ /**
+ * @param string $letter
+ * @return Cli\Arg|null
+ */
+ public function getArgument($letter)
+ {
+ return isset($this->_args[$letter]) ? $this->_args[$letter] : null;
+ }
+
+ /*
+ * Read and validate options
+ *
+ * @return bool true if all options are valid
+ */
+ public function validate()
+ {
+ $options = '';
+ $this->errors = array();
+ $this->values = array();
+ $this->_stdin = null;
+
+ if ($this->isHelpRequest) {
+ return false;
+ }
+
+ $lettersUsed = '';
+ foreach ($this->_args as $letter => $arg) {
+ /* @var Cli\Arg $arg */
+ $options .= $letter;
+ $lettersUsed .= $letter;
+
+ if ($arg->mayHaveValue || $arg->mustHaveValue) {
+ $options .= ($arg->mustHaveValue ? ':' : '::');
+ }
+ }
+
+ $this->debug['argv'] = $GLOBALS['argv'];
+ $argvCopy = array_slice($GLOBALS['argv'], 1);
+ $o = getopt($options);
+ $this->debug['getopt_options'] = $options;
+ $this->debug['getopt_return'] = $o;
+
+ foreach ($this->_args as $letter => $arg) {
+ /* @var Cli\Arg $arg */
+ $this->values[$letter] = false;
+ if (isset($o[$letter])) {
+ if (is_bool($o[$letter])) {
+
+ // remove from argv copy
+ $k = array_search("-$letter", $argvCopy);
+ if ($k !== false) {
+ array_splice($argvCopy, $k, 1);
+ }
+
+ if ($arg->mustHaveValue) {
+ $this->addError($letter, "Missing value");
+ } else {
+ $this->values[$letter] = true;
+ }
+ } else {
+ // string
+ $this->values[$letter] = $o[$letter];
+ $v =& $this->values[$letter];
+
+ // remove from argv copy
+ // first look for -ovalue or -o=value
+ $pattern = "/^-{$letter}=?" . preg_quote($v, '/') . "$/";
+ $foundInArgv = false;
+ foreach ($argvCopy as $k => $argV) {
+ if (preg_match($pattern, $argV)) {
+ array_splice($argvCopy, $k, 1);
+ $foundInArgv = true;
+ break;
+ }
+ }
+ if (! $foundInArgv) {
+ // space separated
+ $k = array_search("-$letter", $argvCopy);
+ if ($k !== false) {
+ array_splice($argvCopy, $k, 2);
+ }
+ }
+
+ // check that value isn't really another option
+ if (strlen($lettersUsed) > 1) {
+ $pattern = "/^-[" . str_replace($letter, '', $lettersUsed) . "]/i";
+ if (preg_match($pattern, $v)) {
+ $this->addError($letter, "Value was read as another option: %s", $v);
+ return false;
+ }
+ }
+ if ($arg->assertFile || $arg->assertDir) {
+ if ($v[0] !== '/' && $v[0] !== '~') {
+ $this->values["$letter.raw"] = $v;
+ $v = getcwd() . "/$v";
+ }
+ }
+ if ($arg->assertFile) {
+ if ($arg->useAsInfile) {
+ $this->_stdin = $v;
+ } elseif ($arg->useAsOutfile) {
+ $this->_stdout = $v;
+ }
+ if ($arg->assertReadable && ! is_readable($v)) {
+ $this->addError($letter, "File not readable: %s", $v);
+ continue;
+ }
+ if ($arg->assertWritable) {
+ if (is_file($v)) {
+ if (! is_writable($v)) {
+ $this->addError($letter, "File not writable: %s", $v);
+ }
+ } else {
+ if (! is_writable(dirname($v))) {
+ $this->addError($letter, "Directory not writable: %s", dirname($v));
+ }
+ }
+ }
+ } elseif ($arg->assertDir && $arg->assertWritable && ! is_writable($v)) {
+ $this->addError($letter, "Directory not readable: %s", $v);
+ }
+ }
+ } else {
+ if ($arg->isRequired()) {
+ $this->addError($letter, "Missing");
+ }
+ }
+ }
+ $this->moreArgs = $argvCopy;
+ reset($this->moreArgs);
+ return empty($this->errors);
+ }
+
+ /**
+ * Get the full paths of file(s) passed in as unspecified arguments
+ *
+ * @return array
+ */
+ public function getPathArgs()
+ {
+ $r = $this->moreArgs;
+ foreach ($r as $k => $v) {
+ if ($v[0] !== '/' && $v[0] !== '~') {
+ $v = getcwd() . "/$v";
+ $v = str_replace('/./', '/', $v);
+ do {
+ $v = preg_replace('@/[^/]+/\\.\\./@', '/', $v, 1, $changed);
+ } while ($changed);
+ $r[$k] = $v;
+ }
+ }
+ return $r;
+ }
+
+ /**
+ * Get a short list of errors with options
+ *
+ * @return string
+ */
+ public function getErrorReport()
+ {
+ if (empty($this->errors)) {
+ return '';
+ }
+ $r = "Some arguments did not pass validation:\n";
+ foreach ($this->errors as $letter => $arr) {
+ $r .= " $letter : " . implode(', ', $arr) . "\n";
+ }
+ $r .= "\n";
+ return $r;
+ }
+
+ /**
+ * @return string
+ */
+ public function getArgumentsListing()
+ {
+ $r = "\n";
+ foreach ($this->_args as $letter => $arg) {
+ /* @var Cli\Arg $arg */
+ $desc = $arg->getDescription();
+ $flag = " -$letter ";
+ if ($arg->mayHaveValue) {
+ $flag .= "[VAL]";
+ } elseif ($arg->mustHaveValue) {
+ $flag .= "VAL";
+ }
+ if ($arg->assertFile) {
+ $flag = str_replace('VAL', 'FILE', $flag);
+ } elseif ($arg->assertDir) {
+ $flag = str_replace('VAL', 'DIR', $flag);
+ }
+ if ($arg->isRequired()) {
+ $desc = "(required) $desc";
+ }
+ $flag = str_pad($flag, 12, " ", STR_PAD_RIGHT);
+ $desc = wordwrap($desc, 70);
+ $r .= $flag . str_replace("\n", "\n ", $desc) . "\n\n";
+ }
+ return $r;
+ }
+
+ /**
+ * Get resource of open input stream. May be STDIN or a file pointer
+ * to the file specified by an option with 'STDIN'.
+ *
+ * @return resource
+ */
+ public function openInput()
+ {
+ if (null === $this->_stdin) {
+ return STDIN;
+ } else {
+ $this->_stdin = fopen($this->_stdin, 'rb');
+ return $this->_stdin;
+ }
+ }
+
+ public function closeInput()
+ {
+ if (null !== $this->_stdin) {
+ fclose($this->_stdin);
+ }
+ }
+
+ /**
+ * Get resource of open output stream. May be STDOUT or a file pointer
+ * to the file specified by an option with 'STDOUT'. The file will be
+ * truncated to 0 bytes on opening.
+ *
+ * @return resource
+ */
+ public function openOutput()
+ {
+ if (null === $this->_stdout) {
+ return STDOUT;
+ } else {
+ $this->_stdout = fopen($this->_stdout, 'wb');
+ return $this->_stdout;
+ }
+ }
+
+ public function closeOutput()
+ {
+ if (null !== $this->_stdout) {
+ fclose($this->_stdout);
+ }
+ }
+
+ /**
+ * @param string $letter
+ * @param string $msg
+ * @param string $value
+ */
+ protected function addError($letter, $msg, $value = null)
+ {
+ if ($value !== null) {
+ $value = var_export($value, 1);
+ }
+ $this->errors[$letter][] = sprintf($msg, $value);
+ }
+}
+
--- /dev/null
+<?php
+
+namespace MrClay\Cli;
+
+/**
+ * An argument for a CLI app. This specifies the argument, what values it expects and
+ * how it's treated during validation.
+ *
+ * By default, the argument will be assumed to be an optional letter flag with no value following.
+ *
+ * If the argument may receive a value, call mayHaveValue(). If there's whitespace after the
+ * flag, the value will be returned as true instead of the string.
+ *
+ * If the argument MUST be accompanied by a value, call mustHaveValue(). In this case, whitespace
+ * is permitted between the flag and its value.
+ *
+ * Use assertFile() or assertDir() to indicate that the argument must return a string value
+ * specifying a file or directory. During validation, the value will be resolved to a
+ * full file/dir path (not necessarily existing!) and the original value will be accessible
+ * via a "*.raw" key. E.g. $cli->values['f.raw']
+ *
+ * Use assertReadable()/assertWritable() to cause the validator to test the file/dir for
+ * read/write permissions respectively.
+ *
+ * @method \MrClay\Cli\Arg mayHaveValue() Assert that the argument, if present, may receive a string value
+ * @method \MrClay\Cli\Arg mustHaveValue() Assert that the argument, if present, must receive a string value
+ * @method \MrClay\Cli\Arg assertFile() Assert that the argument's value must specify a file
+ * @method \MrClay\Cli\Arg assertDir() Assert that the argument's value must specify a directory
+ * @method \MrClay\Cli\Arg assertReadable() Assert that the specified file/dir must be readable
+ * @method \MrClay\Cli\Arg assertWritable() Assert that the specified file/dir must be writable
+ *
+ * @property-read bool mayHaveValue
+ * @property-read bool mustHaveValue
+ * @property-read bool assertFile
+ * @property-read bool assertDir
+ * @property-read bool assertReadable
+ * @property-read bool assertWritable
+ * @property-read bool useAsInfile
+ * @property-read bool useAsOutfile
+ *
+ * @author Steve Clay <steve@mrclay.org>
+ * @license http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+class Arg {
+ /**
+ * @return array
+ */
+ public function getDefaultSpec()
+ {
+ return array(
+ 'mayHaveValue' => false,
+ 'mustHaveValue' => false,
+ 'assertFile' => false,
+ 'assertDir' => false,
+ 'assertReadable' => false,
+ 'assertWritable' => false,
+ 'useAsInfile' => false,
+ 'useAsOutfile' => false,
+ );
+ }
+
+ /**
+ * @var array
+ */
+ protected $spec = array();
+
+ /**
+ * @var bool
+ */
+ protected $required = false;
+
+ /**
+ * @var string
+ */
+ protected $description = '';
+
+ /**
+ * @param bool $isRequired
+ */
+ public function __construct($isRequired = false)
+ {
+ $this->spec = $this->getDefaultSpec();
+ $this->required = (bool) $isRequired;
+ if ($isRequired) {
+ $this->spec['mustHaveValue'] = true;
+ }
+ }
+
+ /**
+ * Assert that the argument's value points to a writable file. When
+ * Cli::openOutput() is called, a write pointer to this file will
+ * be provided.
+ * @return Arg
+ */
+ public function useAsOutfile()
+ {
+ $this->spec['useAsOutfile'] = true;
+ return $this->assertFile()->assertWritable();
+ }
+
+ /**
+ * Assert that the argument's value points to a readable file. When
+ * Cli::openInput() is called, a read pointer to this file will
+ * be provided.
+ * @return Arg
+ */
+ public function useAsInfile()
+ {
+ $this->spec['useAsInfile'] = true;
+ return $this->assertFile()->assertReadable();
+ }
+
+ /**
+ * @return array
+ */
+ public function getSpec()
+ {
+ return $this->spec;
+ }
+
+ /**
+ * @param string $desc
+ * @return Arg
+ */
+ public function setDescription($desc)
+ {
+ $this->description = $desc;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isRequired()
+ {
+ return $this->required;
+ }
+
+ /**
+ * Note: magic methods declared in class PHPDOC
+ *
+ * @param string $name
+ * @param array $args
+ * @return Arg
+ * @throws \BadMethodCallException
+ */
+ public function __call($name, array $args = array())
+ {
+ if (array_key_exists($name, $this->spec)) {
+ $this->spec[$name] = true;
+ if ($name === 'assertFile' || $name === 'assertDir') {
+ $this->spec['mustHaveValue'] = true;
+ }
+ } else {
+ throw new \BadMethodCallException('Method does not exist');
+ }
+ return $this;
+ }
+
+ /**
+ * Note: magic properties declared in class PHPDOC
+ *
+ * @param string $name
+ * @return bool|null
+ */
+ public function __get($name)
+ {
+ if (array_key_exists($name, $this->spec)) {
+ return $this->spec[$name];
+ }
+ return null;
+ }
+}
+++ /dev/null
-<?php
-/**
- *
- * Utility class for static directory methods.
- *
- * @category Solar
- *
- * @package Solar
- *
- * @author Paul M. Jones <pmjones@solarphp.com>
- *
- * @license http://opensource.org/licenses/bsd-license.php BSD
- *
- * @version $Id$
- *
- */
-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;
- }