Merge branch 'w12_MDL-32050_m23_minify' of git://github.com/skodak/moodle
authorSam Hemelryk <sam@moodle.com>
Mon, 19 Mar 2012 23:50:21 +0000 (12:50 +1300)
committerSam Hemelryk <sam@moodle.com>
Mon, 19 Mar 2012 23:50:21 +0000 (12:50 +1300)
44 files changed:
lib/minify/README.txt [deleted file]
lib/minify/builder/_index.js [deleted file]
lib/minify/builder/bm.js [deleted file]
lib/minify/builder/index.php [deleted file]
lib/minify/builder/ocCheck.php [deleted file]
lib/minify/builder/rewriteTest.js [deleted file]
lib/minify/config.php
lib/minify/groupsConfig.php
lib/minify/lib/FirePHP.php
lib/minify/lib/HTTP/ConditionalGet.php
lib/minify/lib/HTTP/Encoder.php
lib/minify/lib/JSMin.php
lib/minify/lib/JSMinPlus.php
lib/minify/lib/Minify.php
lib/minify/lib/Minify/CSS.php
lib/minify/lib/Minify/CSS/Compressor.php
lib/minify/lib/Minify/CSS/UriRewriter.php
lib/minify/lib/Minify/Cache/APC.php
lib/minify/lib/Minify/Cache/File.php
lib/minify/lib/Minify/Cache/Memcache.php
lib/minify/lib/Minify/Cache/ZendPlatform.php [new file with mode: 0644]
lib/minify/lib/Minify/CommentPreserver.php
lib/minify/lib/Minify/Controller/Base.php
lib/minify/lib/Minify/Controller/Groups.php
lib/minify/lib/Minify/Controller/MinApp.php
lib/minify/lib/Minify/Controller/Page.php
lib/minify/lib/Minify/Controller/Version1.php
lib/minify/lib/Minify/DebugDetector.php [new file with mode: 0644]
lib/minify/lib/Minify/HTML.php
lib/minify/lib/Minify/HTML/Helper.php [new file with mode: 0644]
lib/minify/lib/Minify/ImportProcessor.php
lib/minify/lib/Minify/JS/ClosureCompiler.php [new file with mode: 0644]
lib/minify/lib/Minify/Lines.php
lib/minify/lib/Minify/Logger.php
lib/minify/lib/Minify/YUI/CssCompressor.java [new file with mode: 0644]
lib/minify/lib/Minify/YUI/CssCompressor.php [new file with mode: 0644]
lib/minify/lib/Minify/YUICompressor.php
lib/minify/lib/MrClay/Cli.php [new file with mode: 0644]
lib/minify/lib/MrClay/Cli/Arg.php [new file with mode: 0644]
lib/minify/lib/Solar/Dir.php [deleted file]
lib/minify/readme_moodle.txt
lib/minify/utils.php
lib/thirdpartylibs.xml
lib/upgradelib.php

diff --git a/lib/minify/README.txt b/lib/minify/README.txt
deleted file mode 100644 (file)
index 6f527a5..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-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 "&amp;".
-
-
-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
diff --git a/lib/minify/builder/_index.js b/lib/minify/builder/_index.js
deleted file mode 100644 (file)
index c4b6655..0000000
+++ /dev/null
@@ -1,242 +0,0 @@
-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">&uarr;</button>'
-        + ' <button title="Include Later">&darr;</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('&#x2713;');
-                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(/</, '&lt;').replace(/>/, '&gt;').replace(/&/, '&amp;');
-        $('#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;
diff --git a/lib/minify/builder/bm.js b/lib/minify/builder/bm.js
deleted file mode 100644 (file)
index 703039b..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-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.)');
-})();
diff --git a/lib/minify/builder/index.php b/lib/minify/builder/index.php
deleted file mode 100644 (file)
index 1b20982..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-<?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 &amp; 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);
diff --git a/lib/minify/builder/ocCheck.php b/lib/minify/builder/ocCheck.php
deleted file mode 100644 (file)
index 8cc19af..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-<?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;
-}
diff --git a/lib/minify/builder/rewriteTest.js b/lib/minify/builder/rewriteTest.js
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
index 7e29c0b..e2f709c 100644 (file)
@@ -1,19 +1,40 @@
 <?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;
 
 
 /**
@@ -23,24 +44,38 @@ $min_allowDebugFlag = ($CFG->debug);
  *
  * 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();
 
 
 /**
@@ -53,8 +88,8 @@ $min_cachePath = $CFG->tempdir.'';
  * 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'];
 
 
@@ -77,9 +112,9 @@ $min_serveOptions['bubbleCssImports'] = false;
 
 
 /**
- * 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
@@ -88,6 +123,18 @@ $min_serveOptions['bubbleCssImports'] = false;
 $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.
@@ -102,12 +149,17 @@ $min_serveOptions['maxAge'] = 1800;
  * 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';
 
 
 /**
@@ -148,7 +200,7 @@ $min_uploaderHoursBehind = 0;
  * 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)
index 5639880..c900776 100644 (file)
@@ -7,28 +7,11 @@
 /** 
  * 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
index d9d1210..d301a64 100644 (file)
@@ -1065,7 +1065,7 @@ class FirePHP {
    * @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
    */
index 823db05..93b7e75 100644 (file)
@@ -75,9 +75,8 @@ class HTTP_ConditionalGet {
     /**
      * @param array $spec options
      * 
-     * 'isPublic': (bool) if true, the Cache-Control header will contain 
-     * "public", allowing proxies to cache the content. Otherwise "private" will 
-     * be sent, allowing only browser caching. (default false)
+     * 'isPublic': (bool) if false, the Cache-Control header will contain
+     * "private", allowing only browser caching. (default false)
      * 
      * 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers
      * will be sent with content. This is recommended.
@@ -106,8 +105,6 @@ class HTTP_ConditionalGet {
      * 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)
     {
@@ -150,7 +147,10 @@ class HTTP_ConditionalGet {
         } elseif (isset($spec['contentHash'])) { // Use the hash as the ETag
             $this->_setEtag($spec['contentHash'] . $etagAppend, $scope);
         }
-        $this->_headers['Cache-Control'] = "max-age={$maxAge}, {$scope}";
+        $privacy = ($scope === 'private')
+            ? ', private'
+            : '';
+        $this->_headers['Cache-Control'] = "max-age={$maxAge}{$privacy}";
         // invalidate cache if disabled, otherwise check
         $this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate'])
             ? false
@@ -209,7 +209,9 @@ class HTTP_ConditionalGet {
     {
         $headers = $this->_headers;
         if (array_key_exists('_responseCode', $headers)) {
-            header($headers['_responseCode']);
+            // FastCGI environments require 3rd arg to header() to be set
+            list(, $code) = explode(' ', $headers['_responseCode'], 3);
+            header($headers['_responseCode'], true, $code);
             unset($headers['_responseCode']);
         }
         foreach ($headers as $name => $val) {
@@ -230,8 +232,6 @@ class HTTP_ConditionalGet {
      * "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())
     {
@@ -267,13 +267,21 @@ class HTTP_ConditionalGet {
     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;
@@ -282,6 +290,8 @@ class HTTP_ConditionalGet {
 
     /**
      * Determine validity of client cache and queue 304 header if valid
+     *
+     * @return bool
      */
     protected function _isCacheValid()
     {
@@ -298,6 +308,9 @@ class HTTP_ConditionalGet {
         return $isValid;
     }
 
+    /**
+     * @return bool
+     */
     protected function resourceMatchedEtag()
     {
         if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
@@ -319,7 +332,12 @@ class HTTP_ConditionalGet {
         }
         return false;
     }
-    
+
+    /**
+     * @param string $etag
+     *
+     * @return string
+     */
     protected function normalizeEtag($etag) {
         $etag = trim($etag);
         return $this->_stripEtag
@@ -327,17 +345,17 @@ class HTTP_ConditionalGet {
             : $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);
index 05ca552..8f34779 100644 (file)
@@ -59,7 +59,7 @@ class HTTP_Encoder {
      * 
      * @var bool
      */
-    public static $encodeToIe6 = false;
+    public static $encodeToIe6 = true;
     
     
     /**
@@ -85,13 +85,16 @@ class HTTP_Encoder {
      * 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'];
         }
@@ -109,7 +112,7 @@ class HTTP_Encoder {
      * 
      * Call after encode() for encoded content.
      * 
-     * return string
+     * @return string
      */
     public function getContent() 
     {
@@ -143,8 +146,6 @@ class HTTP_Encoder {
      * not handled purposefully.
      * 
      * @see getHeaders()
-     * 
-     * @return null
      */
     public function sendHeaders()
     {
@@ -161,8 +162,6 @@ class HTTP_Encoder {
      * 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()
     {
@@ -195,7 +194,7 @@ class HTTP_Encoder {
         // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
         
         if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])
-            || self::_isBuggyIe())
+            || self::isBuggyIe())
         {
             return array('', '');
         }
@@ -253,7 +252,9 @@ class HTTP_Encoder {
      */
     public function encode($compressionLevel = null)
     {
-        $this->_headers['Vary'] = 'Accept-Encoding';
+        if (! self::isBuggyIe()) {
+            $this->_headers['Vary'] = 'Accept-Encoding';
+        }
         if (null === $compressionLevel) {
             $compressionLevel = self::$compressionLevel;
         }
@@ -273,7 +274,9 @@ class HTTP_Encoder {
         if (false === $encoded) {
             return false;
         }
-        $this->_headers['Content-Length'] = strlen($encoded);
+        $this->_headers['Content-Length'] = $this->_useMbStrlen
+            ? (string)mb_strlen($encoded, '8bit')
+            : (string)strlen($encoded);
         $this->_headers['Content-Encoding'] = $this->_encodeMethod[1];
         $this->_content = $encoded;
         return true;
@@ -301,16 +304,17 @@ class HTTP_Encoder {
         $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 ')
@@ -318,9 +322,14 @@ class HTTP_Encoder {
             return false;
         }
         // no regex = faaast
-        $version = (float)substr($ua, 30); 
+        $version = (float)substr($ua, 30);
         return self::$encodeToIe6
             ? ($version < 6 || ($version == 6 && false === strpos($ua, 'SV1')))
             : ($version < 7);
     }
+    
+    protected $_content = '';
+    protected $_headers = array();
+    protected $_encodeMethod = array('', '');
+    protected $_useMbStrlen = false;
 }
index 770e1c6..b6879f3 100644 (file)
@@ -1,16 +1,20 @@
 <?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
@@ -56,7 +60,7 @@ class JSMin {
     const ACTION_KEEP_A     = 1;
     const ACTION_DELETE_A   = 2;
     const ACTION_DELETE_A_B = 3;
-    
+
     protected $a           = "\n";
     protected $b           = '';
     protected $input       = '';
@@ -64,11 +68,13 @@ class JSMin {
     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)
@@ -76,38 +82,55 @@ class JSMin {
         $jsmin = new JSMin($js);
         return $jsmin->min();
     }
-    
+
     /**
-     * Setup process
+     * @param string $input
      */
     public function __construct($input)
     {
-        $this->input       = str_replace("\r\n", "\n", $input);
-        $this->inputLength = strlen($this->input);
+        $this->input = $input;
     }
-    
+
     /**
      * 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)) {
@@ -120,19 +143,38 @@ class JSMin {
             $this->action($command);
         }
         $this->output = trim($this->output);
+
+        if ($mbIntEnc !== null) {
+            mb_internal_encoding($mbIntEnc);
+        }
         return $this->output;
     }
-    
+
     /**
      * ACTION_KEEP_A = Output A. Copy B to A. Get the next B.
      * ACTION_DELETE_A = Copy B to A. Get the next B.
      * 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;
@@ -140,17 +182,22 @@ class JSMin {
                     $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;
                         }
@@ -173,16 +220,21 @@ class JSMin {
                             $pattern      .= $this->a;
                         } elseif (ord($this->a) <= self::ORD_LF) {
                             throw new JSMin_UnterminatedRegExpException(
-                                'Unterminated RegExp: '. var_export($pattern, true));
+                                "JSMin: Unterminated RegExp at byte "
+                                . $this->inputIndex .": {$pattern}");
                         }
                         $this->output .= $this->a;
+                        $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
@@ -207,9 +259,11 @@ class JSMin {
         }
         return false;
     }
-    
+
     /**
      * Get next char. Convert ctrl char to space.
+     *
+     * @return string
      */
     protected function get()
     {
@@ -231,24 +285,33 @@ class JSMin {
         }
         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 = '';
@@ -264,7 +327,11 @@ class JSMin {
             }
         }
     }
-    
+
+    /**
+     * @return string
+     * @throws JSMin_UnterminatedCommentException
+     */
     protected function multipleLineComment()
     {
         $this->get();
@@ -276,7 +343,7 @@ class JSMin {
                     $this->get();
                     // if comment preserved by YUI Compressor
                     if (0 === strpos($comment, '!')) {
-                        return "\n/*" . substr($comment, 1) . "*/\n";
+                        return "\n/*!" . substr($comment, 1) . "*/\n";
                     }
                     // if IE conditional comment
                     if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
@@ -285,15 +352,19 @@ class JSMin {
                     return ' ';
                 }
             } elseif ($get === null) {
-                throw new JSMin_UnterminatedCommentException('Unterminated Comment: ' . var_export('/*' . $comment, true));
+                throw new JSMin_UnterminatedCommentException(
+                    "JSMin: Unterminated comment at byte "
+                    . $this->inputIndex . ": /*{$comment}");
             }
             $comment .= $get;
         }
     }
-    
+
     /**
      * Get the next character, skipping over comments.
      * Some comments may be preserved.
+     *
+     * @return string
      */
     protected function next()
     {
index 08de880..5a3c5bd 100644 (file)
@@ -1,7 +1,7 @@
 <?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
  *
@@ -43,7 +48,7 @@
  * 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
@@ -65,7 +70,8 @@ define('TOKEN_IDENTIFIER', 3);
 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);
@@ -82,10 +88,89 @@ define('JS_SETTER', 111);
 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;
@@ -107,7 +192,7 @@ class JSMinPlus
 
        private function __construct()
        {
-               $this->parser = new JSParser();
+               $this->parser = new JSParser($this);
        }
 
        public static function minify($js, $filename='')
@@ -136,42 +221,55 @@ class JSMinPlus
                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;
                                        }
                                }
 
@@ -181,31 +279,41 @@ class JSMinPlus
                                }
                        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:
@@ -219,26 +327,48 @@ class JSMinPlus
                                        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:
@@ -263,11 +393,19 @@ class JSMinPlus
                        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:
@@ -288,12 +426,47 @@ class JSMinPlus
                                }
                        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]);
@@ -333,34 +506,32 @@ class JSMinPlus
 
                        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:
@@ -452,13 +623,33 @@ class JSMinPlus
                                $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:
@@ -472,17 +663,23 @@ class JSMinPlus
        {
                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,
@@ -523,11 +720,12 @@ class JSParser
                '.' => 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();
        }
 
@@ -551,6 +749,19 @@ class JSParser
                $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;
        }
 
@@ -809,7 +1020,7 @@ class JSParser
 
                        case KEYWORD_THROW:
                                $n = new JSNode($this->t);
-                               $n->exception = $this->Expression($x);
+                               $n->value = $this->Expression($x);
                        break;
 
                        case KEYWORD_RETURN:
@@ -835,7 +1046,8 @@ class JSParser
                                $n = $this->Variables($x);
                        break;
 
-                       case TOKEN_CONDCOMMENT_MULTILINE:
+                       case TOKEN_CONDCOMMENT_START:
+                       case TOKEN_CONDCOMMENT_END:
                                $n = new JSNode($this->t);
                        return $n;
 
@@ -994,37 +1206,47 @@ class JSParser
                                        // 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;
 
@@ -1036,14 +1258,19 @@ class JSParser
                                                !$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:
@@ -1127,7 +1354,8 @@ class JSParser
                                        $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
@@ -1312,7 +1540,7 @@ class JSParser
                }
 
                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');
@@ -1443,7 +1671,7 @@ class JSNode
 
                if (($numargs = func_num_args()) > 2)
                {
-                       $args = func_get_args();;
+                       $args = func_get_args();
                        for ($i = 2; $i < $numargs; $i++)
                                $this->addNode($args[$i]);
                }
@@ -1465,6 +1693,14 @@ class JSNode
 
        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;
        }
 }
@@ -1499,44 +1735,11 @@ class JSTokenizer
        );
 
        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('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
@@ -1544,17 +1747,7 @@ class JSTokenizer
 
        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)
@@ -1668,7 +1861,7 @@ class JSTokenizer
                        }
 
                        // 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;
@@ -1681,7 +1874,7 @@ class JSTokenizer
                        // check if this is a conditional (JScript) comment
                        if (!empty($match[1]))
                        {
-                               //$match[0] = '/*' . $match[1];
+                               $match[0] = '/*' . $match[1];
                                $conditional_comment = true;
                                break;
                        }
@@ -1699,28 +1892,44 @@ class JSTokenizer
                }
                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;
                                        }
@@ -1739,7 +1948,7 @@ class JSTokenizer
                                                $tt = TOKEN_REGEXP;
                                                break;
                                        }
-                               // fall through
+                               // FALL THROUGH
 
                                case '|':
                                case '^':
@@ -1780,7 +1989,7 @@ class JSTokenizer
                                                $tt = TOKEN_NUMBER;
                                                break;
                                        }
-                               // fall through
+                               // FALL THROUGH
 
                                case ';':
                                case ',':
@@ -1799,7 +2008,14 @@ class JSTokenizer
                                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":
@@ -1868,5 +2084,3 @@ class JSToken
        public $lineno;
        public $assignOp;
 }
-
-?>
index 25f0827..9634f22 100644 (file)
@@ -29,12 +29,13 @@ require_once 'Minify/Source.php';
  */
 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?
@@ -58,7 +59,14 @@ class Minify {
      * @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.
@@ -148,8 +156,8 @@ class Minify {
      * 
      * 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
      * 
@@ -159,6 +167,10 @@ class Minify {
      */
     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;
@@ -167,6 +179,7 @@ class Minify {
                     . str_replace('_', '/', $controller) . ".php";    
             }
             $controller = new $class();
+            /* @var Minify_Controller_Base $controller */
         }
         
         // set up controller sources and mix remaining options with
@@ -179,9 +192,7 @@ class Minify {
         if (! $controller->sources) {
             // invalid request!
             if (! self::$_options['quiet']) {
-                header(self::$_options['badRequestHeader']);
-                echo self::$_options['badRequestHeader'];
-                return;
+                self::_errorExit(self::$_options['badRequestHeader'], self::URL_DEBUG);
             } else {
                 list(,$statusCode) = explode(' ', self::$_options['badRequestHeader']);
                 return array(
@@ -202,6 +213,7 @@ class Minify {
         
         // determine encoding
         if (self::$_options['encodeOutput']) {
+            $sendVary = true;
             if (self::$_options['encodeMethod'] !== null) {
                 // controller specifically requested this
                 $contentEncoding = self::$_options['encodeMethod'];
@@ -212,6 +224,7 @@ class Minify {
                 // 'x-gzip' while our internal encodeMethod is 'gzip'. Calling
                 // getAcceptedEncoding(false, false) leaves out compress and deflate as options.
                 list(self::$_options['encodeMethod'], $contentEncoding) = HTTP_Encoder::getAcceptedEncoding(false, false);
+                $sendVary = ! HTTP_Encoder::isBuggyIe();
             }
         } else {
             self::$_options['encodeMethod'] = ''; // identity (no encoding)
@@ -226,6 +239,8 @@ class Minify {
         );
         if (self::$_options['maxAge'] > 0) {
             $cgOptions['maxAge'] = self::$_options['maxAge'];
+        } elseif (self::$_options['debug']) {
+            $cgOptions['invalidate'] = true;
         }
         $cg = new HTTP_ConditionalGet($cgOptions);
         if ($cg->cacheIsValid) {
@@ -249,8 +264,7 @@ class Minify {
         
         if (self::$_options['contentType'] === self::TYPE_CSS
             && self::$_options['rewriteCssUris']) {
-            reset($controller->sources);
-            while (list($key, $source) = each($controller->sources)) {
+            foreach($controller->sources as $key => $source) {
                 if ($source->filepath 
                     && !isset($source->minifyOptions['currentDir'])
                     && !isset($source->minifyOptions['prependRelativePath'])
@@ -261,12 +275,12 @@ class Minify {
         }
         
         // check server cache
-        if (null !== self::$_cache) {
+        if (null !== self::$_cache && ! self::$_options['debug']) {
             // using cache
             // the goal is to use only the cache methods to sniff the length and 
             // output the content, as they do not require ever loading the file into
             // memory.
-            $cacheId = 'minify_' . self::_getCacheId();
+            $cacheId = self::_getCacheId();
             $fullCacheId = (self::$_options['encodeMethod'])
                 ? $cacheId . '.gz'
                 : $cacheId;
@@ -276,7 +290,15 @@ class Minify {
                 $cacheContentLength = self::$_cache->getSize($fullCacheId);    
             } else {
                 // generate & cache content
-                $content = self::_combineMinify();
+                try {
+                    $content = self::_combineMinify();
+                } catch (Exception $e) {
+                    self::$_controller->log($e->getMessage());
+                    if (! self::$_options['quiet']) {
+                        self::_errorExit(self::$_options['errorHeader'], self::URL_DEBUG);
+                    }
+                    throw $e;
+                }
                 self::$_cache->store($cacheId, $content);
                 if (function_exists('gzencode')) {
                     self::$_cache->store($cacheId . '.gz', gzencode($content, self::$_options['encodeLevel']));
@@ -285,7 +307,15 @@ class Minify {
         } else {
             // no cache
             $cacheIsReady = false;
-            $content = self::_combineMinify();
+            try {
+                $content = self::_combineMinify();
+            } catch (Exception $e) {
+                self::$_controller->log($e->getMessage());
+                if (! self::$_options['quiet']) {
+                    self::_errorExit(self::$_options['errorHeader'], self::URL_DEBUG);
+                }
+                throw $e;
+            }
         }
         if (! $cacheIsReady && self::$_options['encodeMethod']) {
             // still need to encode
@@ -295,14 +325,17 @@ class Minify {
         // add headers
         $headers['Content-Length'] = $cacheIsReady
             ? $cacheContentLength
-            : strlen($content);
+            : ((function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2))
+                ? mb_strlen($content, '8bit')
+                : strlen($content)
+            );
         $headers['Content-Type'] = self::$_options['contentTypeCharset']
             ? self::$_options['contentType'] . '; charset=' . self::$_options['contentTypeCharset']
             : self::$_options['contentType'];
         if (self::$_options['encodeMethod'] !== '') {
             $headers['Content-Encoding'] = $contentEncoding;
         }
-        if (self::$_options['encodeOutput']) {
+        if (self::$_options['encodeOutput'] && $sendVary) {
             $headers['Vary'] = 'Accept-Encoding';
         }
 
@@ -356,52 +389,69 @@ class Minify {
     }
     
     /**
-     * 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)
     {
@@ -439,21 +489,22 @@ class Minify {
         $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
@@ -461,16 +512,40 @@ class Minify {
                 $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);
@@ -491,22 +566,32 @@ class Minify {
      * 
      * Any settings that could affect output are taken into consideration  
      *
+     * @param string $prefix
+     *
      * @return string
      */
-    protected static function _getCacheId()
+    protected static function _getCacheId($prefix = 'minify')
     {
-        return md5(serialize(array(
+        $name = preg_replace('/[^a-zA-Z0-9\\.=_,]/', '', self::$_controller->selectionId);
+        $name = preg_replace('/\\.+/', '.', $name);
+        $name = substr($name, 0, 200 - 34 - strlen($prefix));
+        $md5 = md5(serialize(array(
             Minify_Source::getDigest(self::$_controller->sources)
             ,self::$_options['minifiers'] 
             ,self::$_options['minifierOptions']
             ,self::$_options['postprocessor']
             ,self::$_options['bubbleCssImports']
+            ,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)
     {
index b2d8e0b..49882e9 100644 (file)
@@ -1,83 +1,99 @@
-<?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
index dad700a..c6cdd8b 100644 (file)
-<?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
index 2b47cfc..8845f15 100644 (file)
  */
 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
      * 
@@ -83,7 +77,7 @@ class Minify_CSS_UriRewriter {
     }
     
     /**
-     * Prepend a path to relative URIs in CSS files
+     * In CSS content, prepend a path to relative URIs
      * 
      * @param string $css
      * 
@@ -107,73 +101,8 @@ class Minify_CSS_UriRewriter {
         return $css;
     }
     
-    
-    /**
-     * @var string directory of this stylesheet
-     */
-    private static $_currentDir = '';
-    
-    /**
-     * @var string DOC_ROOT
-     */
-    private static $_docRoot = '';
-    
-    /**
-     * @var array directory replacements to map symlink targets back to their
-     * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath'
-     */
-    private static $_symlinks = array();
-    
-    /**
-     * @var string path to prepend
-     */
-    private static $_prependPath = null;
-    
-    private static function _trimUrls($css)
-    {
-        return preg_replace('/
-            url\\(      # url(
-            \\s*
-            ([^\\)]+?)  # 1 = URI (assuming does not contain ")")
-            \\s*
-            \\)         # )
-        /x', 'url($1)', $css);
-    }
-    
-    private static function _processUriCB($m)
-    {
-        // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
-        $isImport = ($m[0][0] === '@');
-        // determine URI and the quote character (if any)
-        if ($isImport) {
-            $quoteChar = $m[1];
-            $uri = $m[2];
-        } else {
-            // $m[1] is either quoted or not
-            $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"')
-                ? $m[1][0]
-                : '';
-            $uri = ($quoteChar === '')
-                ? $m[1]
-                : substr($m[1], 1, strlen($m[1]) - 2);
-        }
-        // analyze URI
-        if ('/' !== $uri[0]                  // root-relative
-            && false === strpos($uri, '//')  // protocol (non-data)
-            && 0 !== strpos($uri, 'data:')   // data protocol
-        ) {
-            // URI is file-relative: rewrite depending on options
-            $uri = (self::$_prependPath !== null)
-                ? (self::$_prependPath . $uri)
-                : self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks);
-        }
-        return $isImport
-            ? "@import {$quoteChar}{$uri}{$quoteChar}"
-            : "url({$quoteChar}{$uri}{$quoteChar})";
-    }
-    
     /**
-     * Rewrite a file relative URI as root relative
+     * Get a root relative URI from a file relative URI
      *
      * <code>
      * Minify_CSS_UriRewriter::rewriteRelative(
@@ -236,21 +165,39 @@ class Minify_CSS_UriRewriter {
         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.
@@ -267,4 +214,97 @@ class Minify_CSS_UriRewriter {
         }
         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})";
+    }
 }
index ca84d29..24ab046 100644 (file)
@@ -54,9 +54,12 @@ class Minify_Cache_APC {
      */
     public function getSize($id)
     {
-        return $this->_fetch($id)
-            ? strlen($this->_data)
-            : false;
+        if (! $this->_fetch($id)) {
+            return false;
+        }
+        return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2))
+            ? mb_strlen($this->_data, '8bit')
+            : strlen($this->_data);
     }
 
     /**
index 2753e6b..3fa6b18 100644 (file)
@@ -9,13 +9,12 @@ class Minify_Cache_File {
     public function __construct($path = '', $fileLocking = false)
     {
         if (! $path) {
-            require_once 'Solar/Dir.php';
-            $path = rtrim(Solar_Dir::tmp(), DIRECTORY_SEPARATOR);
+            $path = self::tmp();
         }
         $this->_locking = $fileLocking;
         $this->_path = $path;
     }
-    
+
     /**
      * Write data to cache.
      *
@@ -30,15 +29,14 @@ class Minify_Cache_File {
         $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;
@@ -119,6 +117,78 @@ class Minify_Cache_File {
     {
         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;
index 2b81e7a..72bf454 100644 (file)
@@ -60,9 +60,12 @@ class Minify_Cache_Memcache {
      */
     public function getSize($id)
     {
-        return $this->_fetch($id)
-            ? strlen($this->_data)
-            : false;
+        if (! $this->_fetch($id)) {
+            return false;
+        }
+        return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2))
+            ? mb_strlen($this->_data, '8bit')
+            : strlen($this->_data);
     }
     
     /**
diff --git a/lib/minify/lib/Minify/Cache/ZendPlatform.php b/lib/minify/lib/Minify/Cache/ZendPlatform.php
new file mode 100644 (file)
index 0000000..3130d69
--- /dev/null
@@ -0,0 +1,142 @@
+<?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;
+    }
+}
index f56eb34..7a359bf 100644 (file)
@@ -30,8 +30,7 @@ class Minify_CommentPreserver {
      * Process a string outside of C-style comments that begin with "/*!"
      *
      * On each non-empty string outside these comments, the given processor 
-     * function will be called. The first "!" will be removed from the 
-     * preserved comments, and the comments will be surrounded by 
+     * function will be called. The comments will be surrounded by 
      * Minify_CommentPreserver::$preprend and Minify_CommentPreserver::$append.
      * 
      * @param string $content
@@ -65,7 +64,7 @@ class Minify_CommentPreserver {
      * @param string $in input
      * 
      * @return array 3 elements are returned. If a YUI comment is found, the
-     * 2nd element is the comment and the 1st and 2nd are the surrounding
+     * 2nd element is the comment and the 1st and 3rd are the surrounding
      * strings. If no comment is found, the entire string is returned as the 
      * 1st element and the other two are false.
      */
@@ -79,7 +78,7 @@ class Minify_CommentPreserver {
         }
         $ret = array(
             substr($in, 0, $start)
-            ,self::$prepend . '/*' . substr($in, $start + 3, $end - $start - 1) . self::$append
+            ,self::$prepend . '/*!' . substr($in, $start + 3, $end - $start - 1) . self::$append
         );
         $endChars = (strlen($in) - $end - 2);
         $ret[] = (0 === $endChars)
index 84889b3..240b544 100644 (file)
@@ -27,7 +27,7 @@ abstract class Minify_Controller_Base {
      * 
      * @param array $options controller and Minify options
      * 
-     * return array $options Minify::serve options
+     * @return array $options Minify::serve options
      */
     abstract public function setupSources($options);
     
@@ -52,9 +52,10 @@ abstract class Minify_Controller_Base {
             ,'quiet' => false // serve() will send headers and output
             ,'debug' => false
             
-            // if you override this, the response code MUST be directly after 
+            // if you override these, the response codes MUST be directly after
             // the first space.
             ,'badRequestHeader' => 'HTTP/1.0 400 Bad Request'
+            ,'errorHeader'      => 'HTTP/1.0 500 Internal Server Error'
             
             // callback function to see/modify content of all sources
             ,'postprocessor' => null
@@ -117,6 +118,8 @@ abstract class Minify_Controller_Base {
      * be in subdirectories of these directories.
      * 
      * @return bool file is safe
+     *
+     * @deprecated use checkAllowDirs, checkNotHidden instead
      */
     public static function _fileIsSafe($file, $safeDirs)
     {
@@ -134,15 +137,58 @@ abstract class Minify_Controller_Base {
         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
      * 
@@ -192,10 +238,12 @@ abstract class Minify_Controller_Base {
 
     /**
      * 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);
     }
index 1ac5770..2d4e43b 100644 (file)
@@ -34,12 +34,11 @@ class Minify_Controller_Groups extends Minify_Controller_Base {
      * 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
index 9582d29..d47c60d 100644 (file)
@@ -18,8 +18,8 @@ class Minify_Controller_MinApp extends Minify_Controller_Base {
      * 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
@@ -28,64 +28,94 @@ class Minify_Controller_MinApp extends Minify_Controller_Base {
                 'allowDirs' => '//'
                 ,'groupsOnly' => false
                 ,'groups' => array()
-                ,'maxFiles' => 10                
+                ,'noMinPattern' => '@[-\\.]min\\.(?:js|css)$@i' // matched against basename
             )
             ,(isset($options['minApp']) ? $options['minApp'] : array())
         );
         unset($options['minApp']);
         $sources = array();
+        $this->selectionId = '';
+        $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'])) {
@@ -96,7 +126,7 @@ class Minify_Controller_MinApp extends Minify_Controller_Base {
                     // valid base
                     $base = "/{$_GET['b']}/";       
                 } else {
-                    $this->log("GET param 'b' invalid (see MinApp.php line 84)");
+                    $this->log("GET param 'b' was invalid");
                     return $options;
                 }
             } else {
@@ -106,27 +136,96 @@ class Minify_Controller_MinApp extends Minify_Controller_Base {
             foreach ((array)$cOptions['allowDirs'] as $allowDir) {
                 $allowDirs[] = realpath(str_replace('//', $_SERVER['DOCUMENT_ROOT'] . '/', $allowDir));
             }
+            $basenames = array(); // just for cache id
             foreach ($files as $file) {
-                $path = $_SERVER['DOCUMENT_ROOT'] . $base . $file;
-                $file = realpath($path);
-                if (false === $file) {
-                    $this->log("Path \"{$path}\" failed realpath()");
-                    return $options;
-                } elseif (! parent::_fileIsSafe($file, $allowDirs)) {
-                    $this->log("Path \"{$path}\" failed Minify_Controller_Base::_fileIsSafe()");
+                $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');
+        }
+    }
 }
index fa4599a..de471e1 100644 (file)
@@ -40,14 +40,19 @@ class Minify_Controller_Page extends Minify_Controller_Base {
             $sourceSpec = array(
                 'filepath' => $options['file']
             );
+            $f = $options['file'];
         } else {
             // strip controller options
             $sourceSpec = array(
                 'content' => $options['content']
                 ,'id' => $options['id']
             );
+            $f = $options['id'];
             unset($options['content'], $options['id']);
         }
+        // something like "builder,index.php" or "directory,file.html"
+        $this->selectionId = strtr(substr($f, 1 + strlen(dirname(dirname($f)))), '/\\', ',,');
+
         if (isset($options['minifyAll'])) {
             // this will be the 2nd argument passed to Minify_HTML::minify()
             $sourceSpec['minifyOptions'] = array(
index 1861aab..5279d36 100644 (file)
@@ -7,7 +7,7 @@
 require_once 'Minify/Controller/Base.php';
 
 /**
- * Controller class for emulating version 1 of minify.php
+ * Controller class for emulating version 1 of minify.php (mostly a proof-of-concept)
  * 
  * <code>
  * Minify::serve('Version1');
diff --git a/lib/minify/lib/Minify/DebugDetector.php b/lib/minify/lib/Minify/DebugDetector.php
new file mode 100644 (file)
index 0000000..1def974
--- /dev/null
@@ -0,0 +1,26 @@
+<?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
index fb5c1e9..e9453ff 100644 (file)
-<?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
diff --git a/lib/minify/lib/Minify/HTML/Helper.php b/lib/minify/lib/Minify/HTML/Helper.php
new file mode 100644 (file)
index 0000000..807fdc9
--- /dev/null
@@ -0,0 +1,193 @@
+<?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;
+    }
+}
index 0d6d90a..bdfae54 100644 (file)
-<?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
diff --git a/lib/minify/lib/Minify/JS/ClosureCompiler.php b/lib/minify/lib/Minify/JS/ClosureCompiler.php
new file mode 100644 (file)
index 0000000..f4ed5bd
--- /dev/null
@@ -0,0 +1,133 @@
+<?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 {}
index 186e701..ca8afa1 100644 (file)
@@ -40,10 +40,16 @@ class Minify_Lines {
             ? $options['id']
             : '';
         $content = str_replace("\r\n", "\n", $content);
+
+        // Hackily rewrite strings with XPath expressions that are
+        // likely to throw off our dumb parser (for Prototype 1.6.1).
+        $content = str_replace('"/*"', '"/"+"*"', $content);
+        $content = preg_replace('@([\'"])(\\.?//?)\\*@', '$1$2$1+$1*', $content);
+
         $lines = explode("\n", $content);
         $numLines = count($lines);
         // determine left padding
-        $padTo = strlen($numLines);
+        $padTo = strlen((string) $numLines); // e.g. 103 lines = 3 digits
         $inComment = false;
         $i = 0;
         $newLines = array();
@@ -82,7 +88,7 @@ class Minify_Lines {
      * 
      * @param bool $inComment was the parser in a comment at the
      * beginning of the line?
-     * 
+     *
      * @return bool
      */
     private static function _eolInComment($line, $inComment)
index 7844eea..8eb72f4 100644 (file)
@@ -9,6 +9,8 @@
  * 
  * @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 {
 
diff --git a/lib/minify/lib/Minify/YUI/CssCompressor.java b/lib/minify/lib/Minify/YUI/CssCompressor.java
new file mode 100644 (file)
index 0000000..3fb497a
--- /dev/null
@@ -0,0 +1,382 @@
+/*
+ * 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);
+    }
+}
diff --git a/lib/minify/lib/Minify/YUI/CssCompressor.php b/lib/minify/lib/Minify/YUI/CssCompressor.php
new file mode 100644 (file)
index 0000000..ae3443d
--- /dev/null
@@ -0,0 +1,171 @@
+<?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
index 7cb61ad..c5bd8a1 100644 (file)
@@ -90,8 +90,11 @@ class Minify_YUICompressor {
             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);
     }
     
@@ -110,7 +113,7 @@ class Minify_YUICompressor {
         );
         $cmd = self::$javaExecutable . ' -jar ' . escapeshellarg(self::$jarFile)
              . " --type {$type}"
-             . (preg_match('/^[a-zA-Z\\-]+$/', $o['charset']) 
+             . (preg_match('/^[\\da-zA-Z0-9\\-]+$/', $o['charset'])
                 ? " --charset {$o['charset']}" 
                 : '')
              . (is_numeric($o['line-break']) && $o['line-break'] >= 0
@@ -128,11 +131,17 @@ class Minify_YUICompressor {
     
     private static function _prepare()
     {
-        if (! is_file(self::$jarFile) 
-            || ! is_dir(self::$tempDir)
-            || ! is_writable(self::$tempDir)
-        ) {
-            throw new Exception('Minify_YUICompressor : $jarFile and $tempDir must be set.');
+        if (! is_file(self::$jarFile)) {
+            throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not a valid file.');
+        }
+        if (! is_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.');
         }
     }
 }
diff --git a/lib/minify/lib/MrClay/Cli.php b/lib/minify/lib/MrClay/Cli.php
new file mode 100644 (file)
index 0000000..ac20158
--- /dev/null
@@ -0,0 +1,381 @@
+<?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);
+    }
+}
+
diff --git a/lib/minify/lib/MrClay/Cli/Arg.php b/lib/minify/lib/MrClay/Cli/Arg.php
new file mode 100644 (file)
index 0000000..81146a7
--- /dev/null
@@ -0,0 +1,181 @@
+<?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;
+    }
+}
diff --git a/lib/minify/lib/Solar/Dir.php b/lib/minify/lib/Solar/Dir.php
deleted file mode 100644 (file)
index b4928ef..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-<?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;
-        }