mirror of
https://github.com/mrclay/minify.git
synced 2025-08-12 00:54:35 +02:00
V1.9.0 overhaul
This commit is contained in:
5
HISTORY
5
HISTORY
@@ -1,5 +1,10 @@
|
||||
Minify Release History
|
||||
|
||||
Version 1.9.0 (2008-02-28)
|
||||
* Complete overhaul! Minify is now a PEAR-style class and toolkit for building
|
||||
customized minifying file servers.
|
||||
* Utility classes HTTP_Encoder and HTTP_ConditionalGet
|
||||
|
||||
Version 1.0.1 (2007-05-05)
|
||||
* Fixed various problems resolving pathnames when hosted on an NFS mount.
|
||||
* Fixed 'undefined constant' notice.
|
||||
|
6
README
6
README
@@ -1 +1,5 @@
|
||||
Please see http://code.google.com/p/minify/ for documentation.
|
||||
Note: Current trunk is progress on V2 and should be considered "alpha".
|
||||
|
||||
Documentation at http://code.google.com/p/minify/ needs updating.
|
||||
|
||||
For example usage, see files in /examples/1/
|
66
examples/1/index.php
Normal file
66
examples/1/index.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
require '../config.php';
|
||||
ob_start();
|
||||
?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>Minify Example 1</title>
|
||||
<link rel="stylesheet" type="text/css" href="m.php?f=test.css&v=3" />
|
||||
<style type="text/css">
|
||||
#cssFail {
|
||||
width:2.8em;
|
||||
overflow:hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<?php if (! $minifyCachePath): ?>
|
||||
<p><strong>Note:</strong> You should <em>always</em> enable caching using
|
||||
<code>Minify::useServerCache()</code>. For the examples this can be set in
|
||||
<code>config.php</code>. Notice that minifying jquery.js takes several seconds!.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<h1>Minify Example 1</h1>
|
||||
|
||||
<p>This is an example of Minify serving a directory of single css/js files.
|
||||
Each file is minified and sent with HTTP encoding (browser-permitting). </p>
|
||||
|
||||
<ul>
|
||||
<li id="cssFail"><span>FAIL</span>PASS</li>
|
||||
<li id="jsFail1">FAIL</li>
|
||||
<li id="jsFail2">FAIL</li>
|
||||
</ul>
|
||||
|
||||
<p><a href="">Link to this page (F5 can trigger no-cache headers)</a></p>
|
||||
|
||||
<script type="text/javascript" src="m.php?f=jquery-1.2.3.js&v=1"></script>
|
||||
<script type="text/javascript" src="m.php?f=test+space.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
if ( 1 < 2 ) {
|
||||
$('#jsFail2').html('PASS');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
|
||||
require 'Minify.php';
|
||||
|
||||
if ($minifyCachePath) {
|
||||
Minify::useServerCache($minifyCachePath);
|
||||
}
|
||||
|
||||
Minify::serve('Page', array(
|
||||
'content' => $content
|
||||
,'id' => __FILE__
|
||||
,'lastModifiedTime' => filemtime(__FILE__)
|
||||
|
||||
// also minify the CSS/JS inside the HTML
|
||||
,'minifyAll' => true
|
||||
));
|
3408
examples/1/jquery-1.2.3.js
vendored
Normal file
3408
examples/1/jquery-1.2.3.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
50
examples/1/m.php
Normal file
50
examples/1/m.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This script will serve a single js/css file in this directory. Here we place
|
||||
* the front-end-controller logic in user code, then use the "Files" controller
|
||||
* to minify the file. Alternately, we could have created a custom controller
|
||||
* with the same logic and passed it to Minify::handleRequest().
|
||||
*/
|
||||
|
||||
require '../config.php';
|
||||
|
||||
/**
|
||||
* The Files controller only "knows" HTML, CSS, and JS files. Other files
|
||||
* would only be trim()ed and sent as plain/text.
|
||||
*/
|
||||
$serveExtensions = array('css', 'js');
|
||||
|
||||
// set HTTP Expires header if GET 'v' is sent
|
||||
$cacheUntil = isset($_GET['v'])
|
||||
? (time() + 86400 * 30)
|
||||
: null;
|
||||
|
||||
// serve
|
||||
if (isset($_GET['f'])) {
|
||||
$filename = basename($_GET['f']); // remove any naughty bits
|
||||
$filenamePattern = '/[^\'"\\/\\\\]+\\.(?:'
|
||||
.implode('|', $serveExtensions). ')$/';
|
||||
|
||||
if (preg_match($filenamePattern, $filename)
|
||||
&& file_exists(dirname(__FILE__) . '/' . $filename)) {
|
||||
|
||||
require 'Minify.php';
|
||||
|
||||
if ($minifyCachePath) {
|
||||
Minify::useServerCache($minifyCachePath);
|
||||
}
|
||||
|
||||
// The Files controller serves an array of files, but here we just
|
||||
// need one.
|
||||
Minify::serve('Files', array(
|
||||
dirname(__FILE__) . '/' . $filename
|
||||
), array(
|
||||
'cacheUntil' => $cacheUntil
|
||||
));
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
header("HTTP/1.0 404 Not Found");
|
||||
echo "HTTP/1.0 404 Not Found";
|
5
examples/1/test space.js
Normal file
5
examples/1/test space.js
Normal file
@@ -0,0 +1,5 @@
|
||||
$(function () {
|
||||
|
||||
$('#jsFail1').html('PASS');
|
||||
|
||||
});
|
19
examples/1/test.css
Normal file
19
examples/1/test.css
Normal file
@@ -0,0 +1,19 @@
|
||||
/* Test file to minify */
|
||||
|
||||
/* Minify copyright notice here... */
|
||||
|
||||
h1 {
|
||||
color: #00cc00;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
ul, li {
|
||||
padding:0;
|
||||
margin:0;
|
||||
display:block;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#cssFail span {
|
||||
display: none;
|
||||
}
|
15
examples/config.php
Normal file
15
examples/config.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Set $minifyCachePath to a PHP-writeable path to enable server-side caching
|
||||
* in all examples.
|
||||
*/
|
||||
$minifyCachePath = '';
|
||||
|
||||
// get lib in include path
|
||||
ini_set('include_path',
|
||||
dirname(__FILE__) . '/../lib'
|
||||
. PATH_SEPARATOR . ini_get('include_path')
|
||||
);
|
||||
|
||||
?>
|
925
lib/Cache/Lite/File.php
Normal file
925
lib/Cache/Lite/File.php
Normal file
@@ -0,0 +1,925 @@
|
||||
<?php
|
||||
|
||||
// since Minify relies on a (slightly) patched version of Cache_Lite_File,
|
||||
// I've included it here in a single include.
|
||||
|
||||
/**
|
||||
* Fast, light and safe Cache Class
|
||||
*
|
||||
* Cache_Lite is a fast, light and safe cache system. It's optimized
|
||||
* for file containers. It is fast and safe (because it uses file
|
||||
* locking and/or anti-corruption tests).
|
||||
*
|
||||
* There are some examples in the 'docs/examples' file
|
||||
* Technical choices are described in the 'docs/technical' file
|
||||
*
|
||||
* Memory Caching is from an original idea of
|
||||
* Mike BENOIT <ipso@snappymail.ca>
|
||||
*
|
||||
* Nota : A chinese documentation (thanks to RainX <china_1982@163.com>) is
|
||||
* available at :
|
||||
* http://rainx.phpmore.com/manual/cache_lite.html
|
||||
*
|
||||
* @package Cache_Lite
|
||||
* @category Caching
|
||||
* @version $Id: Lite.php,v 1.45 2006/06/03 08:10:33 fab Exp $
|
||||
* @author Fabien MARTY <fab@php.net>
|
||||
*/
|
||||
|
||||
define('CACHE_LITE_ERROR_RETURN', 1);
|
||||
define('CACHE_LITE_ERROR_DIE', 8);
|
||||
|
||||
class Cache_Lite
|
||||
{
|
||||
|
||||
// --- Private properties ---
|
||||
|
||||
/**
|
||||
* Directory where to put the cache files
|
||||
* (make sure to add a trailing slash)
|
||||
*
|
||||
* @var string $_cacheDir
|
||||
*/
|
||||
var $_cacheDir = '/tmp/';
|
||||
|
||||
/**
|
||||
* Enable / disable caching
|
||||
*
|
||||
* (can be very usefull for the debug of cached scripts)
|
||||
*
|
||||
* @var boolean $_caching
|
||||
*/
|
||||
var $_caching = true;
|
||||
|
||||
/**
|
||||
* Cache lifetime (in seconds)
|
||||
*
|
||||
* If null, the cache is valid forever.
|
||||
*
|
||||
* @var int $_lifeTime
|
||||
*/
|
||||
var $_lifeTime = 3600;
|
||||
|
||||
/**
|
||||
* Enable / disable fileLocking
|
||||
*
|
||||
* (can avoid cache corruption under bad circumstances)
|
||||
*
|
||||
* @var boolean $_fileLocking
|
||||
*/
|
||||
var $_fileLocking = true;
|
||||
|
||||
/**
|
||||
* Timestamp of the last valid cache
|
||||
*
|
||||
* @var int $_refreshTime
|
||||
*/
|
||||
var $_refreshTime;
|
||||
|
||||
/**
|
||||
* File name (with path)
|
||||
*
|
||||
* @var string $_file
|
||||
*/
|
||||
var $_file;
|
||||
|
||||
/**
|
||||
* File name (without path)
|
||||
*
|
||||
* @var string $_fileName
|
||||
*/
|
||||
var $_fileName;
|
||||
|
||||
/**
|
||||
* Enable / disable write control (the cache is read just after writing to detect corrupt entries)
|
||||
*
|
||||
* Enable write control will lightly slow the cache writing but not the cache reading
|
||||
* Write control can detect some corrupt cache files but maybe it's not a perfect control
|
||||
*
|
||||
* @var boolean $_writeControl
|
||||
*/
|
||||
var $_writeControl = true;
|
||||
|
||||
/**
|
||||
* Enable / disable read control
|
||||
*
|
||||
* If enabled, a control key is embeded in cache file and this key is compared with the one
|
||||
* calculated after the reading.
|
||||
*
|
||||
* @var boolean $_writeControl
|
||||
*/
|
||||
var $_readControl = true;
|
||||
|
||||
/**
|
||||
* Type of read control (only if read control is enabled)
|
||||
*
|
||||
* Available values are :
|
||||
* 'md5' for a md5 hash control (best but slowest)
|
||||
* 'crc32' for a crc32 hash control (lightly less safe but faster, better choice)
|
||||
* 'strlen' for a length only test (fastest)
|
||||
*
|
||||
* @var boolean $_readControlType
|
||||
*/
|
||||
var $_readControlType = 'crc32';
|
||||
|
||||
/**
|
||||
* Pear error mode (when raiseError is called)
|
||||
*
|
||||
* (see PEAR doc)
|
||||
*
|
||||
* @see setToDebug()
|
||||
* @var int $_pearErrorMode
|
||||
*/
|
||||
var $_pearErrorMode = CACHE_LITE_ERROR_RETURN;
|
||||
|
||||
/**
|
||||
* Current cache id
|
||||
*
|
||||
* @var string $_id
|
||||
*/
|
||||
var $_id;
|
||||
|
||||
/**
|
||||
* Current cache group
|
||||
*
|
||||
* @var string $_group
|
||||
*/
|
||||
var $_group;
|
||||
|
||||
/**
|
||||
* Enable / Disable "Memory Caching"
|
||||
*
|
||||
* NB : There is no lifetime for memory caching !
|
||||
*
|
||||
* @var boolean $_memoryCaching
|
||||
*/
|
||||
var $_memoryCaching = false;
|
||||
|
||||
/**
|
||||
* Enable / Disable "Only Memory Caching"
|
||||
* (be carefull, memory caching is "beta quality")
|
||||
*
|
||||
* @var boolean $_onlyMemoryCaching
|
||||
*/
|
||||
var $_onlyMemoryCaching = false;
|
||||
|
||||
/**
|
||||
* Memory caching array
|
||||
*
|
||||
* @var array $_memoryCachingArray
|
||||
*/
|
||||
var $_memoryCachingArray = array();
|
||||
|
||||
/**
|
||||
* Memory caching counter
|
||||
*
|
||||
* @var int $memoryCachingCounter
|
||||
*/
|
||||
var $_memoryCachingCounter = 0;
|
||||
|
||||
/**
|
||||
* Memory caching limit
|
||||
*
|
||||
* @var int $memoryCachingLimit
|
||||
*/
|
||||
var $_memoryCachingLimit = 1000;
|
||||
|
||||
/**
|
||||
* File Name protection
|
||||
*
|
||||
* if set to true, you can use any cache id or group name
|
||||
* if set to false, it can be faster but cache ids and group names
|
||||
* will be used directly in cache file names so be carefull with
|
||||
* special characters...
|
||||
*
|
||||
* @var boolean $fileNameProtection
|
||||
*/
|
||||
var $_fileNameProtection = true;
|
||||
|
||||
/**
|
||||
* Enable / disable automatic serialization
|
||||
*
|
||||
* it can be used to save directly datas which aren't strings
|
||||
* (but it's slower)
|
||||
*
|
||||
* @var boolean $_serialize
|
||||
*/
|
||||
var $_automaticSerialization = false;
|
||||
|
||||
/**
|
||||
* Disable / Tune the automatic cleaning process
|
||||
*
|
||||
* The automatic cleaning process destroy too old (for the given life time)
|
||||
* cache files when a new cache file is written.
|
||||
* 0 => no automatic cache cleaning
|
||||
* 1 => systematic cache cleaning
|
||||
* x (integer) > 1 => automatic cleaning randomly 1 times on x cache write
|
||||
*
|
||||
* @var int $_automaticCleaning
|
||||
*/
|
||||
var $_automaticCleaningFactor = 0;
|
||||
|
||||
/**
|
||||
* Nested directory level
|
||||
*
|
||||
* Set the hashed directory structure level. 0 means "no hashed directory
|
||||
* structure", 1 means "one level of directory", 2 means "two levels"...
|
||||
* This option can speed up Cache_Lite only when you have many thousands of
|
||||
* cache file. Only specific benchs can help you to choose the perfect value
|
||||
* for you. Maybe, 1 or 2 is a good start.
|
||||
*
|
||||
* @var int $_hashedDirectoryLevel
|
||||
*/
|
||||
var $_hashedDirectoryLevel = 0;
|
||||
|
||||
/**
|
||||
* Umask for hashed directory structure
|
||||
*
|
||||
* @var int $_hashedDirectoryUmask
|
||||
*/
|
||||
var $_hashedDirectoryUmask = 0700;
|
||||
|
||||
/**
|
||||
* API break for error handling in CACHE_LITE_ERROR_RETURN mode
|
||||
*
|
||||
* In CACHE_LITE_ERROR_RETURN mode, error handling was not good because
|
||||
* for example save() method always returned a boolean (a PEAR_Error object
|
||||
* would be better in CACHE_LITE_ERROR_RETURN mode). To correct this without
|
||||
* breaking the API, this option (false by default) can change this handling.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
var $_errorHandlingAPIBreak = false;
|
||||
|
||||
// --- Public methods ---
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* $options is an assoc. Available options are :
|
||||
* $options = array(
|
||||
* 'cacheDir' => directory where to put the cache files (string),
|
||||
* 'caching' => enable / disable caching (boolean),
|
||||
* 'lifeTime' => cache lifetime in seconds (int),
|
||||
* 'fileLocking' => enable / disable fileLocking (boolean),
|
||||
* 'writeControl' => enable / disable write control (boolean),
|
||||
* 'readControl' => enable / disable read control (boolean),
|
||||
* 'readControlType' => type of read control 'crc32', 'md5', 'strlen' (string),
|
||||
* 'pearErrorMode' => pear error mode (when raiseError is called) (cf PEAR doc) (int),
|
||||
* 'memoryCaching' => enable / disable memory caching (boolean),
|
||||
* 'onlyMemoryCaching' => enable / disable only memory caching (boolean),
|
||||
* 'memoryCachingLimit' => max nbr of records to store into memory caching (int),
|
||||
* 'fileNameProtection' => enable / disable automatic file name protection (boolean),
|
||||
* 'automaticSerialization' => enable / disable automatic serialization (boolean),
|
||||
* 'automaticCleaningFactor' => distable / tune automatic cleaning process (int),
|
||||
* 'hashedDirectoryLevel' => level of the hashed directory system (int),
|
||||
* 'hashedDirectoryUmask' => umask for hashed directory structure (int),
|
||||
* 'errorHandlingAPIBreak' => API break for better error handling ? (boolean)
|
||||
* );
|
||||
*
|
||||
* @param array $options options
|
||||
* @access public
|
||||
*/
|
||||
function Cache_Lite($options = array(NULL))
|
||||
{
|
||||
foreach($options as $key => $value) {
|
||||
$this->setOption($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic way to set a Cache_Lite option
|
||||
*
|
||||
* see Cache_Lite constructor for available options
|
||||
*
|
||||
* @var string $name name of the option
|
||||
* @var mixed $value value of the option
|
||||
* @access public
|
||||
*/
|
||||
function setOption($name, $value)
|
||||
{
|
||||
$availableOptions = array('errorHandlingAPIBreak', 'hashedDirectoryUmask', 'hashedDirectoryLevel', 'automaticCleaningFactor', 'automaticSerialization', 'fileNameProtection', 'memoryCaching', 'onlyMemoryCaching', 'memoryCachingLimit', 'cacheDir', 'caching', 'lifeTime', 'fileLocking', 'writeControl', 'readControl', 'readControlType', 'pearErrorMode');
|
||||
if (in_array($name, $availableOptions)) {
|
||||
$property = '_'.$name;
|
||||
$this->$property = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a cache is available and (if yes) return it
|
||||
*
|
||||
* @param string $id cache id
|
||||
* @param string $group name of the cache group
|
||||
* @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested
|
||||
* @return string data of the cache (else : false)
|
||||
* @access public
|
||||
*/
|
||||
function get($id, $group = 'default', $doNotTestCacheValidity = false)
|
||||
{
|
||||
$this->_id = $id;
|
||||
$this->_group = $group;
|
||||
$data = false;
|
||||
if ($this->_caching) {
|
||||
$this->_setRefreshTime();
|
||||
$this->_setFileName($id, $group);
|
||||
clearstatcache();
|
||||
if ($this->_memoryCaching) {
|
||||
if (isset($this->_memoryCachingArray[$this->_file])) {
|
||||
if ($this->_automaticSerialization) {
|
||||
return unserialize($this->_memoryCachingArray[$this->_file]);
|
||||
}
|
||||
return $this->_memoryCachingArray[$this->_file];
|
||||
}
|
||||
if ($this->_onlyMemoryCaching) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (($doNotTestCacheValidity) || (is_null($this->_refreshTime))) {
|
||||
if (file_exists($this->_file)) {
|
||||
$data = $this->_read();
|
||||
}
|
||||
} else {
|
||||
if ((file_exists($this->_file)) && (@filemtime($this->_file) > $this->_refreshTime)) {
|
||||
$data = $this->_read();
|
||||
}
|
||||
}
|
||||
if (($data) and ($this->_memoryCaching)) {
|
||||
$this->_memoryCacheAdd($data);
|
||||
}
|
||||
if (($this->_automaticSerialization) and (is_string($data))) {
|
||||
$data = unserialize($data);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save some data in a cache file
|
||||
*
|
||||
* @param string $data data to put in cache (can be another type than strings if automaticSerialization is on)
|
||||
* @param string $id cache id
|
||||
* @param string $group name of the cache group
|
||||
* @return boolean true if no problem (else : false or a PEAR_Error object)
|
||||
* @access public
|
||||
*/
|
||||
function save($data, $id = NULL, $group = 'default')
|
||||
{
|
||||
if ($this->_caching) {
|
||||
if ($this->_automaticSerialization) {
|
||||
$data = serialize($data);
|
||||
}
|
||||
if (isset($id)) {
|
||||
$this->_setFileName($id, $group);
|
||||
}
|
||||
if ($this->_memoryCaching) {
|
||||
$this->_memoryCacheAdd($data);
|
||||
if ($this->_onlyMemoryCaching) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ($this->_automaticCleaningFactor>0) {
|
||||
$rand = rand(1, $this->_automaticCleaningFactor);
|
||||
if ($rand==1) {
|
||||
$this->clean(false, 'old');
|
||||
}
|
||||
}
|
||||
if ($this->_writeControl) {
|
||||
$res = $this->_writeAndControl($data);
|
||||
if (is_bool($res)) {
|
||||
if ($res) {
|
||||
return true;
|
||||
}
|
||||
// if $res if false, we need to invalidate the cache
|
||||
@touch($this->_file, time() - 2*abs($this->_lifeTime));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$res = $this->_write($data);
|
||||
}
|
||||
if (is_object($res)) {
|
||||
// $res is a PEAR_Error object
|
||||
if (!($this->_errorHandlingAPIBreak)) {
|
||||
return false; // we return false (old API)
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a cache file
|
||||
*
|
||||
* @param string $id cache id
|
||||
* @param string $group name of the cache group
|
||||
* @return boolean true if no problem
|
||||
* @access public
|
||||
*/
|
||||
function remove($id, $group = 'default')
|
||||
{
|
||||
$this->_setFileName($id, $group);
|
||||
if ($this->_memoryCaching) {
|
||||
if (isset($this->_memoryCachingArray[$this->_file])) {
|
||||
unset($this->_memoryCachingArray[$this->_file]);
|
||||
$this->_memoryCachingCounter = $this->_memoryCachingCounter - 1;
|
||||
}
|
||||
if ($this->_onlyMemoryCaching) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return $this->_unlink($this->_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the cache
|
||||
*
|
||||
* if no group is specified all cache files will be destroyed
|
||||
* else only cache files of the specified group will be destroyed
|
||||
*
|
||||
* @param string $group name of the cache group
|
||||
* @param string $mode flush cache mode : 'old', 'ingroup', 'notingroup',
|
||||
* 'callback_myFunction'
|
||||
* @return boolean true if no problem
|
||||
* @access public
|
||||
*/
|
||||
function clean($group = false, $mode = 'ingroup')
|
||||
{
|
||||
return $this->_cleanDir($this->_cacheDir, $group, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to debug mode
|
||||
*
|
||||
* When an error is found, the script will stop and the message will be displayed
|
||||
* (in debug mode only).
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function setToDebug()
|
||||
{
|
||||
$this->setOption('pearErrorMode', CACHE_LITE_ERROR_DIE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new life time
|
||||
*
|
||||
* @param int $newLifeTime new life time (in seconds)
|
||||
* @access public
|
||||
*/
|
||||
function setLifeTime($newLifeTime)
|
||||
{
|
||||
$this->_lifeTime = $newLifeTime;
|
||||
$this->_setRefreshTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the state of the caching memory array into a cache file cache
|
||||
*
|
||||
* @param string $id cache id
|
||||
* @param string $group name of the cache group
|
||||
* @access public
|
||||
*/
|
||||
function saveMemoryCachingState($id, $group = 'default')
|
||||
{
|
||||
if ($this->_caching) {
|
||||
$array = array(
|
||||
'counter' => $this->_memoryCachingCounter,
|
||||
'array' => $this->_memoryCachingState
|
||||
);
|
||||
$data = serialize($array);
|
||||
$this->save($data, $id, $group);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the state of the caching memory array from a given cache file cache
|
||||
*
|
||||
* @param string $id cache id
|
||||
* @param string $group name of the cache group
|
||||
* @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested
|
||||
* @access public
|
||||
*/
|
||||
function getMemoryCachingState($id, $group = 'default', $doNotTestCacheValidity = false)
|
||||
{
|
||||
if ($this->_caching) {
|
||||
if ($data = $this->get($id, $group, $doNotTestCacheValidity)) {
|
||||
$array = unserialize($data);
|
||||
$this->_memoryCachingCounter = $array['counter'];
|
||||
$this->_memoryCachingArray = $array['array'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cache last modification time
|
||||
*
|
||||
* BE CAREFUL : THIS METHOD IS FOR HACKING ONLY !
|
||||
*
|
||||
* @return int last modification time
|
||||
*/
|
||||
function lastModified()
|
||||
{
|
||||
return @filemtime($this->_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a PEAR error
|
||||
*
|
||||
* To improve performances, the PEAR.php file is included dynamically.
|
||||
* The file is so included only when an error is triggered. So, in most
|
||||
* cases, the file isn't included and perfs are much better.
|
||||
*
|
||||
* @param string $msg error message
|
||||
* @param int $code error code
|
||||
* @access public
|
||||
*/
|
||||
function raiseError($msg, $code)
|
||||
{
|
||||
include_once('PEAR.php');
|
||||
return PEAR::raiseError($msg, $code, $this->_pearErrorMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the life of a valid cache file
|
||||
*
|
||||
* see http://pear.php.net/bugs/bug.php?id=6681
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function extendLife()
|
||||
{
|
||||
@touch($this->_file);
|
||||
}
|
||||
|
||||
// --- Private methods ---
|
||||
|
||||
/**
|
||||
* Compute & set the refresh time
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
function _setRefreshTime()
|
||||
{
|
||||
if (is_null($this->_lifeTime)) {
|
||||
$this->_refreshTime = null;
|
||||
} else {
|
||||
$this->_refreshTime = time() - $this->_lifeTime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a file
|
||||
*
|
||||
* @param string $file complete file path and name
|
||||
* @return boolean true if no problem
|
||||
* @access private
|
||||
*/
|
||||
function _unlink($file)
|
||||
{
|
||||
if (!@unlink($file)) {
|
||||
return $this->raiseError('Cache_Lite : Unable to remove cache !', -3);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive function for cleaning cache file in the given directory
|
||||
*
|
||||
* @param string $dir directory complete path (with a trailing slash)
|
||||
* @param string $group name of the cache group
|
||||
* @param string $mode flush cache mode : 'old', 'ingroup', 'notingroup',
|
||||
'callback_myFunction'
|
||||
* @return boolean true if no problem
|
||||
* @access private
|
||||
*/
|
||||
function _cleanDir($dir, $group = false, $mode = 'ingroup')
|
||||
{
|
||||
if ($this->_fileNameProtection) {
|
||||
$motif = ($group) ? 'cache_'.md5($group).'_' : 'cache_';
|
||||
} else {
|
||||
$motif = ($group) ? 'cache_'.$group.'_' : 'cache_';
|
||||
}
|
||||
if ($this->_memoryCaching) {
|
||||
while (list($key, ) = each($this->_memoryCachingArray)) {
|
||||
if (strpos($key, $motif, 0)) {
|
||||
unset($this->_memoryCachingArray[$key]);
|
||||
$this->_memoryCachingCounter = $this->_memoryCachingCounter - 1;
|
||||
}
|
||||
}
|
||||
if ($this->_onlyMemoryCaching) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!($dh = opendir($dir))) {
|
||||
return $this->raiseError('Cache_Lite : Unable to open cache directory !', -4);
|
||||
}
|
||||
$result = true;
|
||||
while ($file = readdir($dh)) {
|
||||
if (($file != '.') && ($file != '..')) {
|
||||
if (substr($file, 0, 6)=='cache_') {
|
||||
$file2 = $dir . $file;
|
||||
if (is_file($file2)) {
|
||||
switch (substr($mode, 0, 9)) {
|
||||
case 'old':
|
||||
// files older than lifeTime get deleted from cache
|
||||
if (!is_null($this->_lifeTime)) {
|
||||
if ((mktime() - @filemtime($file2)) > $this->_lifeTime) {
|
||||
$result = ($result and ($this->_unlink($file2)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'notingrou':
|
||||
if (!strpos($file2, $motif, 0)) {
|
||||
$result = ($result and ($this->_unlink($file2)));
|
||||
}
|
||||
break;
|
||||
case 'callback_':
|
||||
$func = substr($mode, 9, strlen($mode) - 9);
|
||||
if ($func($file2, $group)) {
|
||||
$result = ($result and ($this->_unlink($file2)));
|
||||
}
|
||||
break;
|
||||
case 'ingroup':
|
||||
default:
|
||||
if (strpos($file2, $motif, 0)) {
|
||||
$result = ($result and ($this->_unlink($file2)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((is_dir($file2)) and ($this->_hashedDirectoryLevel>0)) {
|
||||
$result = ($result and ($this->_cleanDir($file2 . '/', $group, $mode)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add some date in the memory caching array
|
||||
*
|
||||
* @param string $data data to cache
|
||||
* @access private
|
||||
*/
|
||||
function _memoryCacheAdd($data)
|
||||
{
|
||||
$this->_memoryCachingArray[$this->_file] = $data;
|
||||
if ($this->_memoryCachingCounter >= $this->_memoryCachingLimit) {
|
||||
list($key, ) = each($this->_memoryCachingArray);
|
||||
unset($this->_memoryCachingArray[$key]);
|
||||
} else {
|
||||
$this->_memoryCachingCounter = $this->_memoryCachingCounter + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a file name (with path)
|
||||
*
|
||||
* @param string $id cache id
|
||||
* @param string $group name of the group
|
||||
* @access private
|
||||
*/
|
||||
function _setFileName($id, $group)
|
||||
{
|
||||
|
||||
if ($this->_fileNameProtection) {
|
||||
$suffix = 'cache_'.md5($group).'_'.md5($id);
|
||||
} else {
|
||||
$suffix = 'cache_'.$group.'_'.$id;
|
||||
}
|
||||
$root = $this->_cacheDir;
|
||||
if ($this->_hashedDirectoryLevel>0) {
|
||||
$hash = md5($suffix);
|
||||
for ($i=0 ; $i<$this->_hashedDirectoryLevel ; $i++) {
|
||||
$root = $root . 'cache_' . substr($hash, 0, $i + 1) . '/';
|
||||
}
|
||||
}
|
||||
$this->_fileName = $suffix;
|
||||
$this->_file = $root.$suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the cache file and return the content
|
||||
*
|
||||
* @return string content of the cache file (else : false or a PEAR_Error object)
|
||||
* @access private
|
||||
*/
|
||||
function _read()
|
||||
{
|
||||
$fp = @fopen($this->_file, "rb");
|
||||
if ($this->_fileLocking) @flock($fp, LOCK_SH);
|
||||
if ($fp) {
|
||||
clearstatcache();
|
||||
$length = @filesize($this->_file);
|
||||
$mqr = get_magic_quotes_runtime();
|
||||
set_magic_quotes_runtime(0);
|
||||
if ($this->_readControl) {
|
||||
$hashControl = @fread($fp, 32);
|
||||
$length = $length - 32;
|
||||
}
|
||||
if ($length) {
|
||||
$data = @fread($fp, $length);
|
||||
} else {
|
||||
$data = '';
|
||||
}
|
||||
set_magic_quotes_runtime($mqr);
|
||||
if ($this->_fileLocking) @flock($fp, LOCK_UN);
|
||||
@fclose($fp);
|
||||
if ($this->_readControl) {
|
||||
$hashData = $this->_hash($data, $this->_readControlType);
|
||||
if ($hashData != $hashControl) {
|
||||
if (!(is_null($this->_lifeTime))) {
|
||||
@touch($this->_file, time() - 2*abs($this->_lifeTime));
|
||||
} else {
|
||||
@unlink($this->_file);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
return $this->raiseError('Cache_Lite : Unable to read cache !', -2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the given data in the cache file
|
||||
*
|
||||
* @param string $data data to put in cache
|
||||
* @return boolean true if ok (a PEAR_Error object else)
|
||||
* @access private
|
||||
*/
|
||||
function _write($data)
|
||||
{
|
||||
if ($this->_hashedDirectoryLevel > 0) {
|
||||
$hash = md5($this->_fileName);
|
||||
$root = $this->_cacheDir;
|
||||
for ($i=0 ; $i<$this->_hashedDirectoryLevel ; $i++) {
|
||||
$root = $root . 'cache_' . substr($hash, 0, $i + 1) . '/';
|
||||
if (!(@is_dir($root))) {
|
||||
@mkdir($root, $this->_hashedDirectoryUmask);
|
||||
}
|
||||
}
|
||||
}
|
||||
$fp = @fopen($this->_file, "wb");
|
||||
if ($fp) {
|
||||
if ($this->_fileLocking) @flock($fp, LOCK_EX);
|
||||
if ($this->_readControl) {
|
||||
@fwrite($fp, $this->_hash($data, $this->_readControlType), 32);
|
||||
}
|
||||
$len = strlen($data);
|
||||
@fwrite($fp, $data, $len);
|
||||
if ($this->_fileLocking) @flock($fp, LOCK_UN);
|
||||
@fclose($fp);
|
||||
return true;
|
||||
}
|
||||
return $this->raiseError('Cache_Lite : Unable to write cache file : '.$this->_file, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the given data in the cache file and control it just after to avoir corrupted cache entries
|
||||
*
|
||||
* @param string $data data to put in cache
|
||||
* @return boolean true if the test is ok (else : false or a PEAR_Error object)
|
||||
* @access private
|
||||
*/
|
||||
function _writeAndControl($data)
|
||||
{
|
||||
$result = $this->_write($data);
|
||||
if (is_object($result)) {
|
||||
return $result; # We return the PEAR_Error object
|
||||
}
|
||||
$dataRead = $this->_read();
|
||||
if (is_object($dataRead)) {
|
||||
return $result; # We return the PEAR_Error object
|
||||
}
|
||||
if ((is_bool($dataRead)) && (!$dataRead)) {
|
||||
return false;
|
||||
}
|
||||
return ($dataRead==$data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a control key with the string containing datas
|
||||
*
|
||||
* @param string $data data
|
||||
* @param string $controlType type of control 'md5', 'crc32' or 'strlen'
|
||||
* @return string control key
|
||||
* @access private
|
||||
*/
|
||||
function _hash($data, $controlType)
|
||||
{
|
||||
switch ($controlType) {
|
||||
case 'md5':
|
||||
return md5($data);
|
||||
case 'crc32':
|
||||
return sprintf('% 32d', crc32($data));
|
||||
case 'strlen':
|
||||
return sprintf('% 32d', strlen($data));
|
||||
default:
|
||||
return $this->raiseError('Unknown controlType ! (available values are only \'md5\', \'crc32\', \'strlen\')', -5);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This class extends Cache_Lite and offers a cache system driven by a master
|
||||
* file or timestamp
|
||||
*
|
||||
* With this class, cache validity is only dependent of a given file or timestamp.
|
||||
* Cache files are valid only if they are older than the master file or the given
|
||||
* timestamp. It's a perfect way for caching templates results (if the template
|
||||
* file is newer than the cache, cache must be rebuild...) or for config classes...
|
||||
*
|
||||
* If the cache is dependent on multiple files, supply the constructor's
|
||||
* 'masterTime' option with the greatest of the files' mtimes.
|
||||
*
|
||||
* There are some examples in the 'docs/examples' file
|
||||
* Technical choices are described in the 'docs/technical' file
|
||||
*
|
||||
* @package Cache_Lite
|
||||
* @version $Id: File.php,v 1.3 2005/12/04 16:03:55 fab Exp $
|
||||
* @author Fabien MARTY <fab@php.net>
|
||||
*/
|
||||
|
||||
// require_once('Cache/Lite.php');
|
||||
|
||||
class Cache_Lite_File extends Cache_Lite
|
||||
{
|
||||
|
||||
// --- Private properties ---
|
||||
|
||||
/**
|
||||
* Complete path of the file used for controlling the cache lifetime
|
||||
*
|
||||
* @var string $_masterFile
|
||||
*/
|
||||
var $_masterFile = '';
|
||||
|
||||
/**
|
||||
* Masterfile mtime
|
||||
*
|
||||
* @var int $_masterFile_mtime
|
||||
*/
|
||||
var $_masterFile_mtime = 0;
|
||||
|
||||
// --- Public methods ----
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* $options is an assoc. To have a look at availables options,
|
||||
* see the constructor of the Cache_Lite class in 'Cache_Lite.php'
|
||||
*
|
||||
* Comparing to Cache_Lite constructor, there are two more options:
|
||||
* $options = array(
|
||||
* (...) see Cache_Lite constructor
|
||||
* 'masterFile' => complete path of the file used for controlling the cache lifetime(string)
|
||||
* 'masterTime' => timestamp of last application change that would invalidate the cache(int).
|
||||
* );
|
||||
* Supply only one of these. If 'masterFile' is supplied, 'masterTime' is
|
||||
* ignored, otherwise 'masterTime' is required.
|
||||
*
|
||||
* @param array $options options
|
||||
* @access public
|
||||
*/
|
||||
function Cache_Lite_File($options = array(NULL))
|
||||
{
|
||||
$options['lifetime'] = 0;
|
||||
$this->Cache_Lite($options);
|
||||
if (isset($options['masterFile'])) {
|
||||
$this->_masterFile = $options['masterFile'];
|
||||
if (!($this->_masterFile_mtime = @filemtime($this->_masterFile))) {
|
||||
return $this->raiseError('Cache_Lite_File : Unable to read masterFile : '.$this->_masterFile, -3);
|
||||
}
|
||||
} elseif (isset($options['masterTime'])) {
|
||||
$this->_masterFile_mtime = $options['masterTime'];
|
||||
} else {
|
||||
return $this->raiseError('Cache_Lite_File : either masterFile or masterTime option must be set !');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a cache is available and (if yes) return it
|
||||
*
|
||||
* @param string $id cache id
|
||||
* @param string $group name of the cache group
|
||||
* @return string data of the cache (or false if no cache available)
|
||||
* @access public
|
||||
*/
|
||||
function get($id, $group = 'default')
|
||||
{
|
||||
if ($data = parent::get($id, $group, true)) {
|
||||
if ($filemtime = $this->lastModified()) {
|
||||
if ($filemtime > $this->_masterFile_mtime) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
6
lib/Cache/Lite/readme.txt
Normal file
6
lib/Cache/Lite/readme.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
File.php contains PEAR's Cache_Lite and a patched version of Cache_Lite_File.
|
||||
|
||||
See: http://pear.php.net/bugs/bug.php?id=12179
|
||||
|
||||
Until the patch is accepted (hopefully), we'll include it here.
|
||||
|
164
lib/HTTP/ConditionalGet.php
Normal file
164
lib/HTTP/ConditionalGet.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Implement conditional GET via a timestamp or hash of content
|
||||
*
|
||||
* <code>
|
||||
* // easiest usage
|
||||
* $cg = new HTTP_ConditionalGet(array(
|
||||
* 'lastModifiedTime' => filemtime(__FILE__)
|
||||
* ));
|
||||
* $cg->sendHeaders();
|
||||
* if ($cg->cacheIsValid) {
|
||||
* exit(); // done
|
||||
* }
|
||||
* // echo content
|
||||
* </code>
|
||||
*
|
||||
*
|
||||
* <code>
|
||||
* // better to add content length once it's known
|
||||
* $cg = new HTTP_ConditionalGet(array(
|
||||
* 'lastModifiedTime' => filemtime(__FILE__)
|
||||
* ));
|
||||
* if ($cg->cacheIsValid) {
|
||||
* $cg->sendHeaders();
|
||||
* exit();
|
||||
* }
|
||||
* $content = get_content();
|
||||
* $cg->setContentLength(strlen($content));
|
||||
* $cg->sendHeaders();
|
||||
* </code>
|
||||
*/
|
||||
class HTTP_ConditionalGet {
|
||||
|
||||
private $headers = array();
|
||||
private $lmTime = null;
|
||||
private $etag = null;
|
||||
public $cacheIsValid = null;
|
||||
|
||||
public function getHeaders() {
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Depending on the PHP config, PHP will buffer all output and set
|
||||
* Content-Length for you. If it doesn't, or you flush() while sending data,
|
||||
* you'll want to call this to let the client know up front.
|
||||
*/
|
||||
public function setContentLength($bytes) {
|
||||
return $this->headers['Content-Length'] = $bytes;
|
||||
}
|
||||
|
||||
public function sendHeaders() {
|
||||
$headers = $this->headers;
|
||||
if (array_key_exists('_responseCode', $headers)) {
|
||||
header($headers['_responseCode']);
|
||||
unset($headers['_responseCode']);
|
||||
}
|
||||
foreach ($headers as $name => $val) {
|
||||
header($name . ': ' . $val);
|
||||
}
|
||||
}
|
||||
|
||||
private function setEtag($hash, $scope) {
|
||||
$this->etag = '"' . $hash
|
||||
. substr($scope, 0, 3)
|
||||
. '"';
|
||||
$this->headers['ETag'] = $this->etag;
|
||||
}
|
||||
|
||||
private function setLastModified($time) {
|
||||
$this->lmTime = (int)$time;
|
||||
$this->headers['Last-Modified'] = self::gmtdate($time);
|
||||
}
|
||||
|
||||
// TODO: allow custom Cache-Control directives, but offer pre-configured
|
||||
// "modes" for common cache models
|
||||
public function __construct($spec) {
|
||||
$scope = (isset($spec['isPublic']) && $spec['isPublic'])
|
||||
? 'public'
|
||||
: 'private';
|
||||
// allow far-expires header
|
||||
if (isset($spec['cacheUntil'])) {
|
||||
if (is_numeric($spec['cacheUntil'])) {
|
||||
$spec['cacheUntil'] = self::gmtdate($spec['cacheUntil']);
|
||||
}
|
||||
$this->headers = array(
|
||||
'Cache-Control' => $scope
|
||||
,'Expires' => $spec['cacheUntil']
|
||||
);
|
||||
$this->cacheIsValid = false;
|
||||
return;
|
||||
}
|
||||
if (isset($spec['lastModifiedTime'])) {
|
||||
// base both headers on time
|
||||
$this->setLastModified($spec['lastModifiedTime']);
|
||||
$this->setEtag($spec['lastModifiedTime'], $scope);
|
||||
} else {
|
||||
// hope to use ETag
|
||||
if (isset($spec['contentHash'])) {
|
||||
$this->setEtag($spec['contentHash'], $scope);
|
||||
}
|
||||
}
|
||||
$this->headers['Cache-Control'] = "max-age=0, {$scope}, must-revalidate";
|
||||
// invalidate cache if disabled, otherwise check
|
||||
$this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate'])
|
||||
? false
|
||||
: $this->isCacheValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine validity of client cache and queue 304 header if valid
|
||||
*/
|
||||
private function isCacheValid()
|
||||
{
|
||||
if (null === $this->etag) {
|
||||
// ETag was our backup, so we know we don't have lmTime either
|
||||
return false;
|
||||
}
|
||||
$isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified());
|
||||
if ($isValid) {
|
||||
// overwrite headers, only need 304
|
||||
$this->headers = array(
|
||||
'_responseCode' => 'HTTP/1.0 304 Not Modified'
|
||||
);
|
||||
}
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
private function resourceMatchedEtag() {
|
||||
if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
|
||||
return false;
|
||||
}
|
||||
$cachedEtagList = get_magic_quotes_gpc()
|
||||
? stripslashes($_SERVER['HTTP_IF_NONE_MATCH'])
|
||||
: $_SERVER['HTTP_IF_NONE_MATCH'];
|
||||
$cachedEtags = split(',', $cachedEtagList);
|
||||
foreach ($cachedEtags as $cachedEtag) {
|
||||
if (trim($cachedEtag) == $this->etag) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function resourceNotModified() {
|
||||
if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
|
||||
return false;
|
||||
}
|
||||
$ifModifiedSince = get_magic_quotes_gpc()
|
||||
? stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE'])
|
||||
: $_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);
|
||||
}
|
||||
return ($ifModifiedSince == self::gmtdate($this->lmTime));
|
||||
}
|
||||
|
||||
private static function gmtdate($ts) {
|
||||
return gmdate('D, d M Y H:i:s \G\M\T', $ts);
|
||||
}
|
||||
}
|
||||
|
44
lib/HTTP/ConditionalGet/test/2.php
Normal file
44
lib/HTTP/ConditionalGet/test/2.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
require '../../ConditionalGet.php';
|
||||
|
||||
// emulate regularly updating document
|
||||
$every = 20;
|
||||
$lastModified = round(time()/$every)*$every - $every;
|
||||
|
||||
$cg = new HTTP_ConditionalGet(array(
|
||||
'lastModifiedTime' => $lastModified
|
||||
));
|
||||
if ($cg->cacheIsValid) {
|
||||
$cg->sendHeaders();
|
||||
// we're done
|
||||
exit();
|
||||
}
|
||||
|
||||
// generate content
|
||||
$title = 'Last-Modified is known : add Content-Length';
|
||||
$explain = '
|
||||
<p>Here, like <a href="./">the first example</a>, we know the Last-Modified time,
|
||||
but we also want to set the Content-Length to increase cacheability and allow
|
||||
HTTP persistent connections. Instead of sending headers immediately, we first
|
||||
generate our content, then use <code>setContentLength(strlen($content))</code>
|
||||
to add the header. Then finally call <code>sendHeaders()</code> and send the
|
||||
content.</p>
|
||||
<p><strong>Note:</strong> This is not required if your PHP config buffers all
|
||||
output and your script doesn\'t do any incremental flushing of the output
|
||||
buffer. PHP will generally set Content-Length for you if it can.</p>
|
||||
<p>This script emulates a document that changes every ' .$every. ' seconds.
|
||||
<br>This is version: ' . date('r', $lastModified) . '</p>
|
||||
';
|
||||
|
||||
require '_include.php';
|
||||
$content = get_content(array(
|
||||
'title' => $title
|
||||
,'explain' => $explain
|
||||
));
|
||||
|
||||
$cg->setContentLength(strlen($content));
|
||||
$cg->sendHeaders();
|
||||
send_slowly($content);
|
||||
|
||||
?>
|
39
lib/HTTP/ConditionalGet/test/3.php
Normal file
39
lib/HTTP/ConditionalGet/test/3.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
require '../../ConditionalGet.php';
|
||||
|
||||
// generate content first (not ideal)
|
||||
// emulate regularly updating document
|
||||
$every = 20;
|
||||
$lastModified = round(time()/$every)*$every - $every;
|
||||
$title = 'Last-Modified is unknown : use hash of content for ETag';
|
||||
$explain = '
|
||||
<p>When Last-Modified is unknown, you can still use ETags, but you need a short
|
||||
string that is unique for that content. In the worst case, you have to generate
|
||||
all the content first, <em>then</em> instantiate HTTP_ConditionalGet, setting
|
||||
the array key <code>contentHash</code> to the output of a hash function of the
|
||||
content. Since we have the full content, we might as well also use
|
||||
<code>setContentLength(strlen($content))</code> in the case where we need to
|
||||
send it.</p>
|
||||
<p>This script emulates a document that changes every ' .$every. ' seconds.
|
||||
<br>This is version: ' . date('r', $lastModified) . '</p>
|
||||
';
|
||||
require '_include.php';
|
||||
$content = get_content(array(
|
||||
'title' => $title
|
||||
,'explain' => $explain
|
||||
));
|
||||
|
||||
$cg = new HTTP_ConditionalGet(array(
|
||||
'contentHash' => substr(md5($content), 7)
|
||||
));
|
||||
if ($cg->cacheIsValid) {
|
||||
$cg->sendHeaders();
|
||||
// we're done
|
||||
exit();
|
||||
}
|
||||
$cg->setContentLength(strlen($content));
|
||||
$cg->sendHeaders();
|
||||
|
||||
send_slowly($content);
|
||||
|
||||
?>
|
46
lib/HTTP/ConditionalGet/test/4.php
Normal file
46
lib/HTTP/ConditionalGet/test/4.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
require '../../ConditionalGet.php';
|
||||
|
||||
// emulate regularly updating document
|
||||
$every = 20;
|
||||
$lastModified = round(time()/$every)*$every - $every;
|
||||
|
||||
$cg = new HTTP_ConditionalGet(array(
|
||||
'lastModifiedTime' => $lastModified
|
||||
));
|
||||
$cg->sendHeaders();
|
||||
if ($cg->cacheIsValid) {
|
||||
// we're done
|
||||
exit();
|
||||
}
|
||||
|
||||
// output encoded content
|
||||
|
||||
$title = 'ConditionalGet + Encoder';
|
||||
$explain = '
|
||||
<p>Using ConditionalGet and Encoder is straightforward. First impliment the
|
||||
ConditionalGet, then if the cache is not valid, encode and send the content</p>
|
||||
<p>This script emulates a document that changes every ' .$every. ' seconds.
|
||||
<br>This is version: ' . date('r', $lastModified) . '</p>
|
||||
';
|
||||
require '_include.php';
|
||||
$content = get_content(array(
|
||||
'title' => $title
|
||||
,'explain' => $explain
|
||||
));
|
||||
|
||||
require '../../Encoder.php';
|
||||
$he = new HTTP_Encoder(array(
|
||||
'content' => get_content(array(
|
||||
'title' => $title
|
||||
,'explain' => $explain
|
||||
))
|
||||
));
|
||||
$he->encode();
|
||||
|
||||
// usually you would just $he->sendAll(), but here we want to emulate slow
|
||||
// connection
|
||||
$he->sendHeaders();
|
||||
send_slowly($he->getContent());
|
||||
|
||||
?>
|
27
lib/HTTP/ConditionalGet/test/5.php
Normal file
27
lib/HTTP/ConditionalGet/test/5.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
require '../../ConditionalGet.php';
|
||||
|
||||
// far expires
|
||||
$cg = new HTTP_ConditionalGet(array(
|
||||
'cacheUntil' => (time() + 86400 * 365) // 1 yr
|
||||
));
|
||||
$cg->sendHeaders();
|
||||
|
||||
// generate, send content
|
||||
$title = 'Expires date is known';
|
||||
$explain = '
|
||||
<p>Here we set "cacheUntil" to a timestamp or GMT date string. This results in
|
||||
<code>$cacheIsValid</code> always being false, so content is always served, but
|
||||
with an Expires header.
|
||||
<p><strong>Note:</strong> This isn\'t a conditional GET, but is useful if you\'re
|
||||
used to the HTTP_ConditionalGet workflow already.</p>
|
||||
';
|
||||
|
||||
require '_include.php';
|
||||
echo get_content(array(
|
||||
'title' => $title
|
||||
,'explain' => $explain
|
||||
));
|
||||
|
||||
?>
|
67
lib/HTTP/ConditionalGet/test/_include.php
Normal file
67
lib/HTTP/ConditionalGet/test/_include.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
function send_slowly($content)
|
||||
{
|
||||
$half = ceil(strlen($content) / 2);
|
||||
$content = str_split($content, $half);
|
||||
while ($chunk = array_shift($content)) {
|
||||
sleep(1);
|
||||
echo $chunk;
|
||||
ob_flush();
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
function get_content($data)
|
||||
{
|
||||
ob_start();
|
||||
?>
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>HTTP_ConditionalGet : <?php echo $data['title']; ?></title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>HTTP_ConditionalGet</h1>
|
||||
<h2><?php echo $data['title']; ?></h2>
|
||||
<?php echo $data['explain']; ?>
|
||||
<ul>
|
||||
<li><a href="./">Last-Modified is known : simple usage</a></li>
|
||||
<li><a href="2.php">Last-Modified is known : add Content-Length</a></li>
|
||||
<li><a href="3.php">Last-Modified is unknown : use hash of content for ETag</a></li>
|
||||
<li><a href="4.php">ConditionalGet + Encoder</a></li>
|
||||
<li><a href="5.php">Expires date is known</a></li>
|
||||
</ul>
|
||||
<h2>Notes</h2>
|
||||
<h3>How to distinguish 200 and 304 responses</h3>
|
||||
<p>For these pages all 200 responses are sent in chunks a second apart, so you
|
||||
should notice that 304 responses are quicker. You can also use HTTP sniffers
|
||||
like <a href="http://www.fiddlertool.com/">Fiddler (win)</a> and
|
||||
<a href="http://livehttpheaders.mozdev.org/">LiveHTTPHeaders (Firefox add-on)</a>
|
||||
to verify headers and content being sent.</p>
|
||||
<h3>Browser notes</h3>
|
||||
<dl>
|
||||
<dt>Opera</dt>
|
||||
<dd>Opera has a couple behaviors against the HTTP spec: Manual refreshes (F5)
|
||||
prevents the ETag/If-Modified-Since headers from being sent; it only sends
|
||||
them when following a link or bookmark. Also, Opera will not honor the
|
||||
<code>must-revalidate</code> Cache-Control value unless <code>max-age</code>
|
||||
is set. To get Opera to follow the spec, ConditionalGet will send Opera max-age=0
|
||||
(if one is not already set).</dd>
|
||||
<dt>Safari</dt>
|
||||
<dd>ETag validation is unsupported, but Safari supports HTTP/1.0 validation via
|
||||
If-Modified-Since headers as long as the cache is explicitly marked
|
||||
"public" or "private". ConditionalGet can send one of these
|
||||
values determined by cookies/session data, but it's best to explicitly
|
||||
set the option 'isPublic' to true or false.</dd>
|
||||
</dl>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
return $content;
|
||||
}
|
||||
|
||||
?>
|
36
lib/HTTP/ConditionalGet/test/index.php
Normal file
36
lib/HTTP/ConditionalGet/test/index.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
require '../../ConditionalGet.php';
|
||||
|
||||
// emulate regularly updating document
|
||||
$every = 20;
|
||||
$lastModified = round(time()/$every)*$every - $every;
|
||||
|
||||
$cg = new HTTP_ConditionalGet(array(
|
||||
'lastModifiedTime' => $lastModified
|
||||
));
|
||||
$cg->sendHeaders();
|
||||
if ($cg->cacheIsValid) {
|
||||
// we're done
|
||||
exit();
|
||||
}
|
||||
|
||||
$title = 'Last-Modified is known : simple usage';
|
||||
$explain = '
|
||||
<p>If your content has not changed since a certain timestamp, set this via the
|
||||
the <code>lastModifiedTime</code> array key when instantiating HTTP_ConditionalGet.
|
||||
You can immediately call the method <code>sendHeaders()</code> to set the
|
||||
Last-Modified, ETag, and Cache-Control headers. The, if <code>cacheIsValid</code>
|
||||
property is false, you echo the content.</p>
|
||||
<p>This script emulates a document that changes every ' .$every. ' seconds.
|
||||
<br>This is version: ' . date('r', $lastModified) . '</p>
|
||||
';
|
||||
|
||||
require '_include.php';
|
||||
|
||||
echo send_slowly(get_content(array(
|
||||
'title' => $title
|
||||
,'explain' => $explain
|
||||
)));
|
||||
|
||||
?>
|
151
lib/HTTP/Encoder.php
Normal file
151
lib/HTTP/Encoder.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Encode and send gzipped/deflated content
|
||||
*
|
||||
* <code>
|
||||
* // Send a CSS file, compressed if possible
|
||||
* $he = new HTTP_Encoder(array(
|
||||
* 'content' => file_get_contents($cssFile)
|
||||
* ,'type' => 'text/css'
|
||||
* ));
|
||||
* $he->encode();
|
||||
* $he->sendAll();
|
||||
* </code>
|
||||
*
|
||||
* <code>
|
||||
* // Just sniff for the accepted encoding
|
||||
* $encoding = HTTP_Encoder::getAcceptedEncoding();
|
||||
* </code>
|
||||
*
|
||||
* For more control over headers, use getHeaders() and getData() and send your
|
||||
* own output.
|
||||
*/
|
||||
class HTTP_Encoder {
|
||||
|
||||
public static $compressionLevel = 6;
|
||||
private static $clientEncodeMethod = null;
|
||||
|
||||
private $content = '';
|
||||
private $headers = array();
|
||||
|
||||
private $encodeMethod = array('', '');
|
||||
|
||||
public function __construct($spec) {
|
||||
if (isset($spec['content'])) {
|
||||
$this->content = $spec['content'];
|
||||
}
|
||||
$this->headers['Content-Length'] = strlen($this->content);
|
||||
if (isset($spec['type'])) {
|
||||
$this->headers['Content-Type'] = $spec['type'];
|
||||
}
|
||||
if (self::$clientEncodeMethod === null) {
|
||||
self::$clientEncodeMethod = self::getAcceptedEncoding();
|
||||
}
|
||||
if (isset($spec['method'])
|
||||
&& in_array($spec['method'], array('gzip', 'deflate', 'compress', '')))
|
||||
{
|
||||
$this->encodeMethod = array($spec['method'], $spec['method']);
|
||||
} else {
|
||||
$this->encodeMethod = self::$clientEncodeMethod;
|
||||
}
|
||||
}
|
||||
|
||||
public function getContent() {
|
||||
return $this->content;
|
||||
}
|
||||
public function getHeaders() {
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the file and headers (encoded or not)
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
public function sendAll() {
|
||||
$this->sendHeaders();
|
||||
echo $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send just the headers
|
||||
*/
|
||||
public function sendHeaders() {
|
||||
foreach ($this->headers as $name => $val) {
|
||||
header($name . ': ' . $val);
|
||||
}
|
||||
}
|
||||
|
||||
// returns array(encoding, encoding to use in Content-Encoding header)
|
||||
// eg. array('gzip', 'x-gzip')
|
||||
public static function getAcceptedEncoding() {
|
||||
if (self::$clientEncodeMethod !== null) {
|
||||
return self::$clientEncodeMethod;
|
||||
}
|
||||
if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])
|
||||
|| self::isBuggyIe())
|
||||
{
|
||||
return array('', '');
|
||||
}
|
||||
// test for (x-)gzip, if q is specified, can't be "0"
|
||||
if (preg_match('@(?:^|,)\s*((?:x-)?gzip)\s*(?:$|,|;\s*q=(?:0\.|1))@', $_SERVER['HTTP_ACCEPT_ENCODING'], $m)) {
|
||||
return array('gzip', $m[1]);
|
||||
}
|
||||
if (preg_match('@(?:^|,)\s*deflate\s*(?:$|,|;\s*q=(?:0\.|1))@', $_SERVER['HTTP_ACCEPT_ENCODING'])) {
|
||||
return array('deflate', 'deflate');
|
||||
}
|
||||
if (preg_match('@(?:^|,)\s*((?:x-)?compress)\s*(?:$|,|;\s*q=(?:0\.|1))@', $_SERVER['HTTP_ACCEPT_ENCODING'], $m)) {
|
||||
return array('compress', $m[1]);
|
||||
}
|
||||
return array('', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* If conditionsEncode the content
|
||||
* @return bool success
|
||||
*/
|
||||
public function encode($compressionLevel = null) {
|
||||
if (null === $compressionLevel) {
|
||||
$compressionLevel = self::$compressionLevel;
|
||||
}
|
||||
if ('' === $this->encodeMethod[0]
|
||||
|| ($compressionLevel == 0)
|
||||
|| !extension_loaded('zlib'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if ($this->encodeMethod[0] === 'gzip') {
|
||||
$encoded = gzencode($this->content, $compressionLevel);
|
||||
} elseif ($this->encodeMethod[0] === 'deflate') {
|
||||
$encoded = gzdeflate($this->content, $compressionLevel);
|
||||
} else {
|
||||
$encoded = gzcompress($this->content, $compressionLevel);
|
||||
}
|
||||
if (false === $encoded) {
|
||||
return false;
|
||||
}
|
||||
$this->headers['Content-Length'] = strlen($encoded);
|
||||
$this->headers['Content-Encoding'] = $this->encodeMethod[1];
|
||||
$this->headers['Vary'] = 'Accept-Encoding';
|
||||
$this->content = $encoded;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function isBuggyIe()
|
||||
{
|
||||
if (strstr($_SERVER['HTTP_USER_AGENT'], 'Opera')
|
||||
|| !preg_match('/^Mozilla\/4\.0 \(compatible; MSIE ([0-9]\.[0-9])/i', $_SERVER['HTTP_USER_AGENT'], $m))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
$version = floatval($m[1]);
|
||||
if ($version < 6) return true;
|
||||
if ($version == 6 && !strstr($_SERVER['HTTP_USER_AGENT'], 'SV1')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
BIN
lib/HTTP/Encoder/test/green.png
Normal file
BIN
lib/HTTP/Encoder/test/green.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 202 B |
60
lib/HTTP/Encoder/test/index.php
Normal file
60
lib/HTTP/Encoder/test/index.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
ini_set('display_errors', 'on');
|
||||
|
||||
require '../../Encoder.php';
|
||||
|
||||
if (!isset($_GET['test'])) {
|
||||
$type = 'text/html';
|
||||
ob_start();
|
||||
?>
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>HTTP_Encoder Test</title>
|
||||
<style type="text/css">
|
||||
@import "?test=2";
|
||||
#img {background:url("?test=1");}
|
||||
.green {background:#0f0;}
|
||||
p span {padding:0 .5em;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>HTTP_Encoder test</h1>
|
||||
<p><span class="green"> HTML </span></p>
|
||||
<p><span id="css"> CSS </span></p>
|
||||
<p><span id="js"> Javascript </span></p>
|
||||
<p><span id="img"> image </span></p>
|
||||
<script src="?test=3" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
} elseif ($_GET['test'] == '1') {
|
||||
$content = file_get_contents(dirname(__FILE__) . '/green.png');
|
||||
$type = 'image/png';
|
||||
|
||||
} elseif ($_GET['test'] == '2') {
|
||||
$content = '#css {background:#0f0;}';
|
||||
$type = 'text/css';
|
||||
|
||||
} else {
|
||||
$content = '
|
||||
window.onload = function(){
|
||||
document.getElementById("js").className = "green";
|
||||
};
|
||||
';
|
||||
$type = 'text/javascript';
|
||||
|
||||
}
|
||||
|
||||
$he = new HTTP_Encoder(array(
|
||||
'content' => $content
|
||||
,'type' => $type
|
||||
));
|
||||
$he->encode();
|
||||
$he->sendAll();
|
||||
|
||||
?>
|
364
lib/Minify.php
Normal file
364
lib/Minify.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Minify - Combines, minifies, and caches JavaScript and CSS files on demand.
|
||||
*
|
||||
* See http://code.google.com/p/minify/ for usage instructions.
|
||||
*
|
||||
* This library was inspired by jscsscomp by Maxim Martynyuk <flashkot@mail.ru>
|
||||
* and by the article "Supercharged JavaScript" by Patrick Hunlock
|
||||
* <wb@hunlock.com>.
|
||||
*
|
||||
* JSMin was originally written by Douglas Crockford <douglas@crockford.com>.
|
||||
*
|
||||
* Requires PHP 5.2.1+.
|
||||
*
|
||||
* @package Minify
|
||||
* @author Ryan Grove <ryan@wonko.com>
|
||||
* @author Stephen Clay <steve@mrclay.org>
|
||||
* @copyright 2007 Ryan Grove. All rights reserved.
|
||||
* @license http://opensource.org/licenses/bsd-license.php New BSD License
|
||||
* @version 1.9.0
|
||||
* @link http://code.google.com/p/minify/
|
||||
*/
|
||||
|
||||
require_once 'Minify/Source.php';
|
||||
|
||||
class Minify {
|
||||
|
||||
/**
|
||||
* @var bool Should the un-encoded version be cached?
|
||||
*
|
||||
* True results in more cache files, but lower PHP load if different
|
||||
* encodings are commonly requested.
|
||||
*/
|
||||
public static $cacheUnencodedVersion = true;
|
||||
|
||||
/**
|
||||
* Specify a writeable directory for cache files. If not called, Minify
|
||||
* will not use a disk cache and, for each 200 response, will need to
|
||||
* recombine files, minify and encode the output.
|
||||
*
|
||||
* @param string $path Full directory path for cache files (should not end
|
||||
* in directory separator character). If not provided, Minify will attempt to
|
||||
* write to the path returned by sys_get_temp_dir().
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public static function useServerCache($path = null) {
|
||||
self::$_cachePath = (null === $path)
|
||||
? sys_get_temp_dir()
|
||||
: $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a controller instance and handle the request
|
||||
*
|
||||
* @param string type This should be the filename of the controller without
|
||||
* extension. e.g. 'Group'
|
||||
*
|
||||
* @param array $spec options for the controller's constructor
|
||||
*
|
||||
* @return mixed a Minify controller object
|
||||
*/
|
||||
public static function serve($type, $spec = array(), $options = array()) {
|
||||
$class = 'Minify_Controller_' . $type;
|
||||
if (! class_exists($class, false)) {
|
||||
require_once "Minify/Controller/{$type}.php";
|
||||
}
|
||||
$ctrl = new $class($spec, $options);
|
||||
if (! self::handleRequest($ctrl)) {
|
||||
header("HTTP/1.0 400 Bad Request");
|
||||
exit('400 Bad Request');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request for a minified file.
|
||||
*
|
||||
* You must supply a controller object which has the same public API
|
||||
* as Minify_Controller.
|
||||
*
|
||||
* @param Minify_Controller $controller
|
||||
*
|
||||
* @return bool successfully sent a 304 or 200 with content
|
||||
*/
|
||||
public static function handleRequest($controller) {
|
||||
if (! $controller->requestIsValid) {
|
||||
return false;
|
||||
}
|
||||
self::$_controller = $controller;
|
||||
self::_setOptions();
|
||||
|
||||
$cgOptions = array(
|
||||
'lastModifiedTime' => self::$_options['lastModifiedTime']
|
||||
,'isPublic' => self::$_options['isPublic']
|
||||
);
|
||||
if (null !== self::$_options['cacheUntil']) {
|
||||
$cgOptions['cacheUntil'] = self::$_options['cacheUntil'];
|
||||
}
|
||||
|
||||
// check client cache
|
||||
require_once 'HTTP/ConditionalGet.php';
|
||||
$cg = new HTTP_ConditionalGet($cgOptions);
|
||||
if ($cg->cacheIsValid) {
|
||||
// client's cache is valid
|
||||
$cg->sendHeaders();
|
||||
return true;
|
||||
}
|
||||
// client will need output
|
||||
$headers = $cg->getHeaders();
|
||||
unset($cg);
|
||||
|
||||
// determine encoding
|
||||
if (self::$_options['encodeOutput']) {
|
||||
if (self::$_options['encodeMethod'] !== null) {
|
||||
// controller specifically requested this
|
||||
$contentEncoding = self::$_options['encodeMethod'];
|
||||
} else {
|
||||
// sniff request header
|
||||
require_once 'HTTP/Encoder.php';
|
||||
// depending on what the client accepts, $contentEncoding may be
|
||||
// 'x-gzip' while our internal encodeMethod is 'gzip'
|
||||
list(self::$_options['encodeMethod'], $contentEncoding) = HTTP_Encoder::getAcceptedEncoding();
|
||||
}
|
||||
} else {
|
||||
self::$_options['encodeMethod'] = ''; // identity (no encoding)
|
||||
}
|
||||
|
||||
if (null !== self::$_cachePath) {
|
||||
self::_setupCache();
|
||||
// fetch content from cache file(s).
|
||||
$content = self::_fetchContent(self::$_options['encodeMethod']);
|
||||
self::$_cache = null;
|
||||
} else {
|
||||
// no cache, just combine, minify, encode
|
||||
$content = self::_combineMinify();
|
||||
$content = self::_encode($content);
|
||||
}
|
||||
|
||||
// add headers to those from ConditionalGet
|
||||
//$headers['Content-Length'] = strlen($content);
|
||||
$headers['Content-Type'] = (null !== self::$_options['contentTypeCharset'])
|
||||
? self::$_options['contentType'] . ';charset=' . self::$_options['contentTypeCharset']
|
||||
: self::$_options['contentType'];
|
||||
if (self::$_options['encodeMethod'] !== '') {
|
||||
$headers['Content-Encoding'] = $contentEncoding;
|
||||
$headers['Vary'] = 'Accept-Encoding';
|
||||
}
|
||||
|
||||
// output headers & content
|
||||
foreach ($headers as $name => $val) {
|
||||
header($name . ': ' . $val);
|
||||
}
|
||||
echo $content;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var mixed null if disk cache is not to be used
|
||||
*/
|
||||
private static $_cachePath = null;
|
||||
|
||||
/**
|
||||
* @var Minify_Controller active controller for current request
|
||||
*/
|
||||
private static $_controller = null;
|
||||
|
||||
/**
|
||||
* @var array options for current request
|
||||
*/
|
||||
private static $_options = null;
|
||||
|
||||
/**
|
||||
* @var Cache_Lite_File cache obj for current request
|
||||
*/
|
||||
private static $_cache = null;
|
||||
|
||||
/**
|
||||
* Set class options based on controller's options and defaults
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
private static function _setOptions()
|
||||
{
|
||||
$given = self::$_controller->options;
|
||||
self::$_options = array_merge(array(
|
||||
// default options
|
||||
'isPublic' => true
|
||||
,'encodeOutput' => true
|
||||
,'encodeMethod' => null // determine later
|
||||
,'encodeLevel' => 9
|
||||
,'perType' => array() // per-type minifier options
|
||||
,'contentTypeCharset' => null // leave out of Content-Type header
|
||||
,'cacheUntil' => null
|
||||
), $given);
|
||||
$defaultMinifiers = array(
|
||||
'text/css' => array('Minify_CSS', 'minify')
|
||||
,'application/x-javascript' => array('Minify_Javascript', 'minify')
|
||||
,'text/html' => array('Minify_HTML', 'minify')
|
||||
);
|
||||
if (! isset($given['minifiers'])) {
|
||||
$given['minifiers'] = array();
|
||||
}
|
||||
self::$_options['minifiers'] = array_merge($defaultMinifiers, $given['minifiers']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch encoded content from cache (or generate and store it).
|
||||
*
|
||||
* If self::$cacheUnencodedVersion is true and encoded content must be
|
||||
* generated, this function will call itself recursively to fetch (or
|
||||
* generate) the minified content. Otherwise, it will always recombine
|
||||
* and reminify files to generate different encodings.
|
||||
*
|
||||
* @param string $encodeMethod
|
||||
*
|
||||
* @return string minified, encoded content
|
||||
*/
|
||||
private static function _fetchContent($encodeMethod)
|
||||
{
|
||||
$cacheId = self::_getCacheId(self::$_controller->sources, self::$_options)
|
||||
. $encodeMethod;
|
||||
$content = self::$_cache->get($cacheId, 'Minify');
|
||||
if (false === $content) {
|
||||
// must generate
|
||||
if ($encodeMethod === '') {
|
||||
// generate identity cache to store
|
||||
$content = self::_combineMinify();
|
||||
} else {
|
||||
// fetch identity cache & encode it to store
|
||||
if (self::$cacheUnencodedVersion) {
|
||||
// double layer cache
|
||||
$content = self::_fetchContent('');
|
||||
} else {
|
||||
// recombine
|
||||
$content = self::_combineMinify();
|
||||
}
|
||||
$content = self::_encode($content);
|
||||
}
|
||||
self::$_cache->save($content, $cacheId, 'Minify');
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set self::$_cache to a new instance of Cache_Lite_File (patched 2007-10-03)
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
private static function _setupCache() {
|
||||
// until the patch is rolled into PEAR, we'll provide the
|
||||
// class in our package
|
||||
require_once dirname(__FILE__) . '/Cache/Lite/File.php';
|
||||
|
||||
self::$_cache = new Cache_Lite_File(array(
|
||||
'cacheDir' => self::$_cachePath . '/'
|
||||
,'fileNameProtection' => false
|
||||
|
||||
// currently only available in patched Cache_Lite_File
|
||||
,'masterTime' => self::$_options['lastModifiedTime']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines sources and minifies the result.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function _combineMinify() {
|
||||
$type = self::$_options['contentType']; // ease readability
|
||||
|
||||
// when combining scripts, make sure all statements separated
|
||||
$implodeSeparator = ($type === 'application/x-javascript')
|
||||
? ';'
|
||||
: '';
|
||||
|
||||
// default options and minifier function for all sources
|
||||
$defaultOptions = isset(self::$_options['perType'][$type])
|
||||
? self::$_options['perType'][$type]
|
||||
: array();
|
||||
$defaultMinifier = isset(self::$_options['minifiers'][$type])
|
||||
? self::$_options['minifiers'][$type]
|
||||
: array('Minify', '_trim');
|
||||
|
||||
if (Minify_Source::haveNoMinifyPrefs(self::$_controller->sources)) {
|
||||
// all source have same options/minifier, better performance
|
||||
foreach (self::$_controller->sources as $source) {
|
||||
$pieces[] = $source->getContent();
|
||||
}
|
||||
$content = implode($implodeSeparator, $pieces);
|
||||
self::$_controller->loadMinifier($defaultMinifier);
|
||||
$content = call_user_func($defaultMinifier, $content, $defaultOptions);
|
||||
} else {
|
||||
// minify each source with its own options and minifier
|
||||
foreach (self::$_controller->sources as $source) {
|
||||
// allow the source to override our minifier and options
|
||||
$minifier = (null !== $source->minifier)
|
||||
? $source->minifier
|
||||
: $defaultMinifier;
|
||||
$options = (null !== $source->minifyOptions)
|
||||
? array_merge($defaultOptions, $source->minifyOptions)
|
||||
: $defaultOptions;
|
||||
self::$_controller->loadMinifier($minifier);
|
||||
// get source content and minify it
|
||||
$pieces[] = call_user_func($minifier, $source->getContent(), $options);
|
||||
}
|
||||
$content = implode($implodeSeparator, $pieces);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies HTTP encoding
|
||||
*
|
||||
* @param string $content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function _encode($content)
|
||||
{
|
||||
if (self::$_options['encodeMethod'] === ''
|
||||
|| ! self::$_options['encodeOutput']) {
|
||||
// "identity" encoding
|
||||
return $content;
|
||||
}
|
||||
require_once 'HTTP/Encoder.php';
|
||||
$encoder = new HTTP_Encoder(array(
|
||||
'content' => $content
|
||||
,'method' => self::$_options['encodeMethod']
|
||||
));
|
||||
$encoder->encode(self::$_options['encodeLevel']);
|
||||
return $encoder->getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a unique cache id for for this request.
|
||||
*
|
||||
* Any settings that could affect output are taken into consideration
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function _getCacheId() {
|
||||
return md5(serialize(array(
|
||||
Minify_Source::getDigest(self::$_controller->sources)
|
||||
,self::$_options['minifiers']
|
||||
,self::$_options['perType']
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* The default minifier if content-type has no minifier
|
||||
*
|
||||
* This is necessary because trim() throws notices when you send in options
|
||||
* as a 2nd arg.
|
||||
*
|
||||
* @param string $content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function _trim($content, $options)
|
||||
{
|
||||
return trim($content);
|
||||
}
|
||||
}
|
180
lib/Minify/CSS.php
Normal file
180
lib/Minify/CSS.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* "Minify" 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 hacks
|
||||
* involving comment tokens in 'content' value strings to break minimization
|
||||
* badly. A test suite is available
|
||||
*/
|
||||
class Minify_CSS {
|
||||
|
||||
/**
|
||||
* Minify a CSS string
|
||||
*
|
||||
* @param string $css
|
||||
*
|
||||
* @param array $options optional. To enable URL rewriting, set the value
|
||||
* for key 'prependRelativePath'.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function minify($css, $options = array()) {
|
||||
// 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
|
||||
self::$_inHack = false;
|
||||
$css = preg_replace_callback('/\\s*\\/\\*([\\s\\S]*?)\\*\\/\\s*/'
|
||||
,array('Minify_CSS', '_commentCB'), $css);
|
||||
|
||||
// compress whitespace. Yes, this will affect "copyright" comments.
|
||||
$css = preg_replace('/\s+/', ' ', $css);
|
||||
|
||||
// leave needed comments
|
||||
$css = str_replace('/*keep*/', '/**/', $css);
|
||||
|
||||
// remove ws around { }
|
||||
$css = preg_replace('/\\s*{\\s*/', '{', $css);
|
||||
$css = preg_replace('/;?\\s*}\\s*/', '}', $css);
|
||||
|
||||
// remove ws between rules
|
||||
$css = preg_replace('/\\s*;\\s*/', ';', $css);
|
||||
|
||||
// remove ws around urls
|
||||
$css = preg_replace('/url\\([\\s]*([^\\)]+?)[\\s]*\\)/', 'url($1)', $css);
|
||||
|
||||
// remove ws between rules and colons
|
||||
$css = preg_replace('/\\s*([{;])\\s*([\\w\\-]+)\\s*:\\s*\\b/', '$1$2:', $css);
|
||||
|
||||
// remove ws in selectors
|
||||
$css = preg_replace_callback('/(?:\\s*[^~>+,\\s]+\\s*[,>+~])+\\s*[^~>+,\\s]+{/'
|
||||
,array('Minify_CSS', '_selectorsCB'), $css);
|
||||
|
||||
// minimize hex colors
|
||||
$css = preg_replace('/#([a-f\\d])\\1([a-f\\d])\\2([a-f\\d])\\3([\\s;\\}])/i'
|
||||
, '#$1$2$3$4', $css);
|
||||
|
||||
if (isset($options['prependRelativePath'])) {
|
||||
self::$_tempPrepend = $options['prependRelativePath'];
|
||||
$css = preg_replace_callback('/@import ([\'"])(.*?)[\'"]\\s*;/'
|
||||
,array('Minify_CSS', '_urlCB'), $css);
|
||||
|
||||
$css = preg_replace_callback('/url\\(([^\\)]+)\\)/'
|
||||
,array('Minify_CSS', '_urlCB'), $css);
|
||||
}
|
||||
|
||||
return trim($css);
|
||||
}
|
||||
|
||||
/**
|
||||
* @var bool Are we "in" a hack?
|
||||
*
|
||||
* I.e. are some browsers targetted until the next comment?
|
||||
*/
|
||||
private static $_inHack = false;
|
||||
|
||||
/**
|
||||
* @var string string to be prepended to relative URIs
|
||||
*/
|
||||
private static $_tempPrepend = '';
|
||||
|
||||
/**
|
||||
* Process what looks like a comment and return a replacement
|
||||
*
|
||||
* @param array $m regex matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function _commentCB($m)
|
||||
{
|
||||
$m = $m[1];
|
||||
// $m is everything after the opening tokens and before the closing tokens
|
||||
// but return will replace the entire comment.
|
||||
if ($m === 'keep') {
|
||||
return '/*keep*/';
|
||||
}
|
||||
if (false !== strpos($m, 'copyright')) {
|
||||
// contains copyright, preserve
|
||||
self::$_inHack = false;
|
||||
return "/*{$m}*/";
|
||||
}
|
||||
if (self::$_inHack) {
|
||||
// inversion: feeding only to one browser
|
||||
if (preg_match('/^\\/\\s*(\\S[\\s\\S]+?)\\s*\\/\\*/', $m, $n)) {
|
||||
self::$_inHack = false;
|
||||
return "/*/{$n[1]}/*keep*/";
|
||||
}
|
||||
}
|
||||
if (substr($m, -1) === '\\') {
|
||||
self::$_inHack = true;
|
||||
return '/*\\*/';
|
||||
}
|
||||
if (substr($m, 0, 1) === '/') {
|
||||
self::$_inHack = true;
|
||||
return '/*/*/';
|
||||
}
|
||||
if (self::$_inHack) {
|
||||
self::$_inHack = false;
|
||||
return '/*keep*/';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace what looks like a set of selectors
|
||||
*
|
||||
* @param array $m regex matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function _selectorsCB($m)
|
||||
{
|
||||
return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
|
||||
}
|
||||
|
||||
private static function _urlCB($m)
|
||||
{
|
||||
$isImport = (0 === strpos($m[0], '@import'));
|
||||
if ($isImport) {
|
||||
$quote = $m[1];
|
||||
$url = $m[2];
|
||||
} else {
|
||||
// $m[1] is surrounded by quotes 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 ('/' === $url[1]) {
|
||||
// protocol relative URI!
|
||||
$url = '//' . self::$_tempPrepend . substr($url, 2);
|
||||
}
|
||||
} else {
|
||||
if (strpos($url, '//') > 0) {
|
||||
// probably starts with protocol, do not alter
|
||||
} else {
|
||||
// relative URI
|
||||
$url = self::$_tempPrepend . $url;
|
||||
}
|
||||
}
|
||||
if ($isImport) {
|
||||
return "@import {$quote}{$url}{$quote};";
|
||||
} else {
|
||||
return "url({$quote}{$url}{$quote})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
129
lib/Minify/Controller/Base.php
Normal file
129
lib/Minify/Controller/Base.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Base class for Minify controller
|
||||
*
|
||||
* The controller class validates a request and uses it to create sources
|
||||
* for minification and set options like contentType. It's also responsible
|
||||
* for loading minifier code upon request.
|
||||
*/
|
||||
class Minify_Controller_Base {
|
||||
|
||||
/**
|
||||
* @var array instances of Minify_Source, which provide content and
|
||||
* any individual minification needs.
|
||||
*
|
||||
* @see Minify_Source
|
||||
*/
|
||||
public $sources = array();
|
||||
|
||||
/**
|
||||
* @var array options to be read by read by Minify
|
||||
*
|
||||
* Any unspecified options will use the default values.
|
||||
*
|
||||
* 'minifiers': this is an array with content-types as keys and callbacks as
|
||||
* values. Specify a custom minifier by setting this option. E.g.:
|
||||
* $this->options['minifiers']['application/x-javascript'] = 'myJsPacker';
|
||||
* Note that, when providing your own minifier, the controller must be able
|
||||
* to load its code on demand. @see loadMinifier()
|
||||
*
|
||||
* 'perType' : this is an array of options to send to a particular content
|
||||
* type minifier by using the content-type as key. E.g. To send the CSS
|
||||
* minifier an option: $options['perType']['text/css']['foo'] = 'bar';
|
||||
* When the CSS minifier is called, the 2nd argument will be
|
||||
* array('foo' => 'bar').
|
||||
*
|
||||
* 'isPublic' : send "public" instead of "private" in Cache-Control headers,
|
||||
* allowing shared caches to cache the output. (default true)
|
||||
*
|
||||
* 'encodeOutput' : to disable content encoding, set this to false
|
||||
*
|
||||
* 'encodeMethod' : generally you should let this be determined by
|
||||
* HTTP_Encoder (the default null), but you can force a particular encoding
|
||||
* to be returned, by setting this to 'gzip', 'deflate', 'compress', or ''
|
||||
* (no encoding)
|
||||
*
|
||||
* 'encodeLevel' : level of encoding compression (0 to 9, default 9)
|
||||
*
|
||||
* 'contentTypeCharset' : if given, this will be appended to the Content-Type
|
||||
* header sent, useful mainly for HTML docs.
|
||||
*
|
||||
* 'cacheUntil' : set this to a timestamp or GMT date to have Minify send
|
||||
* an HTTP Expires header instead of checking for conditional GET.
|
||||
* E.g. (time() + 86400 * 365) for 1yr (default null)
|
||||
* This has nothing to do with server-side caching.
|
||||
*
|
||||
*/
|
||||
public $options = array();
|
||||
|
||||
/**
|
||||
* @var bool was the user request valid
|
||||
*
|
||||
* This must be explicity be set to true to process the request. This should
|
||||
* be done by the child class constructor.
|
||||
*/
|
||||
public $requestIsValid = false;
|
||||
|
||||
/**
|
||||
* Parent constructor for a controller class
|
||||
*
|
||||
* Generally you'll call this at the end of your child class constructor:
|
||||
* <code>
|
||||
* parent::__construct($sources, $options);
|
||||
* </code>
|
||||
*
|
||||
* This function sets the sources and determines the 'contentType' and
|
||||
* 'lastModifiedTime', if not given.
|
||||
*
|
||||
* If no sources are provided, $this->requestIsValid will be set to false.
|
||||
*
|
||||
* @param array $sources array of instances of Minify_Source
|
||||
*
|
||||
* @param array $options
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function __construct($sources, $options = array()) {
|
||||
if (empty($sources)) {
|
||||
$this->requestIsValid = false;
|
||||
}
|
||||
$this->sources = $sources;
|
||||
if (! isset($options['contentType'])) {
|
||||
$options['contentType'] = Minify_Source::getContentType($this->sources);
|
||||
}
|
||||
// last modified is needed for caching, even if cacheUntil is set
|
||||
if (! isset($options['lastModifiedTime'])) {
|
||||
$max = 0;
|
||||
foreach ($sources as $source) {
|
||||
$max = max($source->lastModified, $max);
|
||||
}
|
||||
$options['lastModifiedTime'] = $max;
|
||||
}
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load any code necessary to execute the given minifier callback.
|
||||
*
|
||||
* The controller is responsible for loading minification code on demand
|
||||
* via this method. This built-in function will only load classes for
|
||||
* static method callbacks where the class isn't already defined. It uses
|
||||
* the PEAR convention, so, given array('Jimmy_Minifier', 'minCss'), this
|
||||
* function will include 'Jimmy/Minifier.php'
|
||||
*
|
||||
* If you need code loaded on demand and this doesn't suit you, you'll need
|
||||
* to override this function by extending the class.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function loadMinifier($minifierCallback)
|
||||
{
|
||||
if (is_array($minifierCallback)
|
||||
&& is_string($minifierCallback[0])
|
||||
&& !class_exists($minifierCallback[0], false)) {
|
||||
|
||||
require str_replace('_', '/', $minifierCallback[0]) . '.php';
|
||||
}
|
||||
}
|
||||
}
|
46
lib/Minify/Controller/Files.php
Normal file
46
lib/Minify/Controller/Files.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
require_once 'Minify/Controller/Base.php';
|
||||
|
||||
/**
|
||||
* Controller class for minifying a set of files
|
||||
*
|
||||
* E.g. the following would serve minified Javascript for a site
|
||||
* <code>
|
||||
* $dr = $_SERVER['DOCUMENT_ROOT'];
|
||||
* Minify::minify('Files', array(
|
||||
* $dr . '/js/jquery.js'
|
||||
* ,$dr . '/js/plugins.js'
|
||||
* ,$dr . '/js/site.js'
|
||||
* ));
|
||||
* </code>
|
||||
*
|
||||
*/
|
||||
class Minify_Controller_Files extends Minify_Controller_Base {
|
||||
|
||||
/**
|
||||
* @param array $spec array or full paths of files to be minified
|
||||
*
|
||||
* @param array $options optional options to pass to Minify
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function __construct($spec, $options = array()) {
|
||||
$sources = array();
|
||||
foreach ($spec as $file) {
|
||||
$file = realpath($file);
|
||||
if (file_exists($file)) {
|
||||
$sources[] = new Minify_Source(array(
|
||||
'filepath' => $file
|
||||
));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($sources) {
|
||||
$this->requestIsValid = true;
|
||||
}
|
||||
parent::__construct($sources, $options);
|
||||
}
|
||||
}
|
||||
|
59
lib/Minify/Controller/Groups.php
Normal file
59
lib/Minify/Controller/Groups.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
require_once 'Minify/Controller/Base.php';
|
||||
|
||||
/**
|
||||
* Controller class for serving predetermined groups of minimized sets, selected
|
||||
* by PATH_INFO
|
||||
*
|
||||
* <code>
|
||||
* $dr = $_SERVER['DOCUMENT_ROOT'];
|
||||
* Minify::minify('Groups', array(
|
||||
* 'css' => array(
|
||||
* $dr . '/css/type.css'
|
||||
* ,$dr . '/css/layout.css'
|
||||
* )
|
||||
* ,'js' => array(
|
||||
* $dr . '/js/jquery.js'
|
||||
* ,$dr . '/js/plugins.js'
|
||||
* ,$dr . '/js/site.js'
|
||||
* )
|
||||
* ));
|
||||
* </code>
|
||||
*
|
||||
* If the above code were placed in /serve.php, it would enable the URLs
|
||||
* /serve.php/js and /serve.php/css
|
||||
*/
|
||||
class Minify_Controller_Groups extends Minify_Controller_Base {
|
||||
|
||||
/**
|
||||
* @param array $spec associative array of keys to arrays of file paths.
|
||||
*
|
||||
* @param array $options optional options to pass to Minify
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function __construct($spec, $options = array()) {
|
||||
$pi = substr($_SERVER['PATH_INFO'], 1);
|
||||
if (! isset($spec[$pi])) {
|
||||
// not a valid group
|
||||
return;
|
||||
}
|
||||
$sources = array();
|
||||
foreach ($spec[$pi] as $file) {
|
||||
$file = realpath($file);
|
||||
if (file_exists($file)) {
|
||||
$sources[] = new Minify_Source(array(
|
||||
'filepath' => $file
|
||||
));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($sources) {
|
||||
$this->requestIsValid = true;
|
||||
}
|
||||
parent::__construct($sources, $options);
|
||||
}
|
||||
}
|
||||
|
61
lib/Minify/Controller/Page.php
Normal file
61
lib/Minify/Controller/Page.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
require_once 'Minify/Controller/Base.php';
|
||||
|
||||
/**
|
||||
* Controller class for minifying a set of files
|
||||
*
|
||||
* E.g. the following would serve minified Javascript for a site
|
||||
* <code>
|
||||
* $dr = $_SERVER['DOCUMENT_ROOT'];
|
||||
* Minify::minify('Files', array(
|
||||
* $dr . '/js/jquery.js'
|
||||
* ,$dr . '/js/plugins.js'
|
||||
* ,$dr . '/js/site.js'
|
||||
* ));
|
||||
* </code>
|
||||
*
|
||||
*/
|
||||
class Minify_Controller_Page extends Minify_Controller_Base {
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param array $options optional options to pass to Minify
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function __construct($spec, $options = array()) {
|
||||
$sourceSpec = array(
|
||||
'content' => $spec['content']
|
||||
,'id' => $spec['id']
|
||||
,'minifier' => array('Minify_HTML', 'minify')
|
||||
);
|
||||
if (isset($spec['minifyAll'])) {
|
||||
$sourceSpec['minifyOptions'] = array(
|
||||
'cssMinifier' => array('Minify_CSS', 'minify')
|
||||
,'jsMinifier' => array('Minify_Javascript', 'minify')
|
||||
);
|
||||
$this->_loadCssJsMinifiers = true;
|
||||
}
|
||||
$sources[] = new Minify_Source($sourceSpec);
|
||||
if (isset($spec['lastModifiedTime'])) {
|
||||
$options['lastModifiedTime'] = $spec['lastModifiedTime'];
|
||||
}
|
||||
$options['contentType'] = 'text/html';
|
||||
$this->requestIsValid = true;
|
||||
parent::__construct($sources, $options);
|
||||
}
|
||||
|
||||
private $_loadCssJsMinifiers = false;
|
||||
|
||||
public function loadMinifier($minifierCallback)
|
||||
{
|
||||
if ($this->_loadCssJsMinifiers) {
|
||||
require 'Minify/CSS.php';
|
||||
require 'Minify/Javascript.php';
|
||||
}
|
||||
parent::loadMinifier($minifierCallback);
|
||||
}
|
||||
}
|
||||
|
162
lib/Minify/HTML.php
Normal file
162
lib/Minify/HTML.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
class Minify_HTML {
|
||||
|
||||
/**
|
||||
* "Minify" an HTML page
|
||||
*
|
||||
* @todo: To also minify embedded Javascript/CSS, you must...
|
||||
*
|
||||
*/
|
||||
public static function minify($string, $options = array()) {
|
||||
|
||||
if (isset($options['cssMinifier'])) {
|
||||
self::$_cssMinifier = $options['cssMinifier'];
|
||||
}
|
||||
if (isset($options['jsMinifier'])) {
|
||||
self::$_jsMinifier = $options['jsMinifier'];
|
||||
}
|
||||
|
||||
$html = trim($string);
|
||||
|
||||
self::$_isXhtml = (false !== strpos($html, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML'));
|
||||
|
||||
self::$_replacementHash = 'HTTPMINIFY' . md5(time());
|
||||
|
||||
// remove SCRIPTs (and minify)
|
||||
$html = preg_replace_callback('/\\s*(<script\\b[^>]*?>)([\\s\\S]*?)<\\/script>\\s*/i',
|
||||
array('Minify_HTML', '_removeScriptCB'), $html);
|
||||
|
||||
// remove STYLEs (and minify)
|
||||
$html = preg_replace_callback('/\\s*(<style\\b[^>]*?>)([\\s\\S]*?)<\\/style>\\s*/i',
|
||||
array('Minify_HTML', '_removeStyleCB'), $html);
|
||||
|
||||
// remove HTML comments (but not IE conditional comments).
|
||||
$html = preg_replace('/<!--[^\\[][\\s\\S]*?-->/', '', $html);
|
||||
|
||||
// replace PREs with token text
|
||||
self::$_pres = array();
|
||||
$html = preg_replace_callback('/\\s*(<pre\\b[^>]*?>[\\s\\S]*?<\\/pre>)\\s*/i'
|
||||
,array('Minify_HTML', '_removePreCB')
|
||||
, $html);
|
||||
|
||||
// remove leading and trailing ws from each line.
|
||||
// @todo take into account attribute values that span multiple lines.
|
||||
$html = preg_replace('/^\\s*(.*?)\\s*$/m', "$1", $html);
|
||||
|
||||
// remove ws around block/undisplayed elements
|
||||
$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)|title'
|
||||
.'|ul)\\b[^>]*>)/i', '$1', $html);
|
||||
|
||||
// remove ws between and inside elements.
|
||||
$html = preg_replace('/>\\s+(\\S[\\s\\S]*?)?</', "> $1<", $html);
|
||||
$html = preg_replace('/>(\\S[\\s\\S]*?)?\\s+</', ">$1 <", $html);
|
||||
$html = preg_replace('/>\\s+</', "> <", $html);
|
||||
|
||||
// replace PREs
|
||||
$i = count(self::$_pres);
|
||||
while ($i > 0) {
|
||||
$rep = array_pop(self::$_pres);
|
||||
$html = str_replace(self::$_replacementHash . 'PRE' . $i, $rep, $html);
|
||||
$i--;
|
||||
}
|
||||
|
||||
// replace SCRIPTs
|
||||
$i = count(self::$_scripts);
|
||||
while ($i > 0) {
|
||||
$rep = array_pop(self::$_scripts);
|
||||
$html = str_replace(self::$_replacementHash . 'SCRIPT' . $i, $rep, $html);
|
||||
$i--;
|
||||
}
|
||||
|
||||
// replace STYLEs
|
||||
$i = count(self::$_styles);
|
||||
while ($i > 0) {
|
||||
$rep = array_pop(self::$_styles);
|
||||
$html = str_replace(self::$_replacementHash . 'STYLE' . $i, $rep, $html);
|
||||
$i--;
|
||||
}
|
||||
|
||||
self::$_cssMinifier = self::$_jsMinifier = null;
|
||||
return $html;
|
||||
}
|
||||
|
||||
private static $_isXhtml = false;
|
||||
private static $_replacementHash = null;
|
||||
private static $_pres = array();
|
||||
private static $_scripts = array();
|
||||
private static $_styles = array();
|
||||
private static $_cssMinifier = null;
|
||||
private static $_jsMinifier = null;
|
||||
|
||||
private static function _removePreCB($m)
|
||||
{
|
||||
self::$_pres[] = $m[1];
|
||||
return self::$_replacementHash . 'PRE' . count(self::$_pres);
|
||||
}
|
||||
|
||||
private static function _removeStyleCB($m)
|
||||
{
|
||||
$openStyle = $m[1];
|
||||
$css = $m[2];
|
||||
// remove HTML comments
|
||||
$css = preg_replace('/(?:^\\s*<!--|-->\\s*$)/', '', $css);
|
||||
|
||||
// remove CDATA section markers
|
||||
$css = self::_removeCdata($css);
|
||||
|
||||
// minify
|
||||
$minifier = self::$_cssMinifier
|
||||
? self::$_cssMinifier
|
||||
: 'trim';
|
||||
$css = call_user_func($minifier, $css);
|
||||
|
||||
// store
|
||||
self::$_styles[] = self::_needsCdata($css)
|
||||
? "{$openStyle}/*<![CDATA[*/{$css}/*]]>*/</style>"
|
||||
: "{$openStyle}{$css}</style>";
|
||||
|
||||
|
||||
return self::$_replacementHash . 'STYLE' . count(self::$_styles);
|
||||
}
|
||||
|
||||
private static function _removeScriptCB($m)
|
||||
{
|
||||
$openScript = $m[1];
|
||||
$js = $m[2];
|
||||
|
||||
// remove HTML comments (and ending "//" if present)
|
||||
$js = preg_replace('/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/', '', $js);
|
||||
|
||||
// remove CDATA section markers
|
||||
$js = self::_removeCdata($js);
|
||||
|
||||
// minify
|
||||
$minifier = self::$_jsMinifier
|
||||
? self::$_jsMinifier
|
||||
: 'trim';
|
||||
$js = call_user_func($minifier, $js);
|
||||
|
||||
// store
|
||||
self::$_scripts[] = self::_needsCdata($js)
|
||||
? "{$openScript}/*<![CDATA[*/{$js}/*]]>*/</script>"
|
||||
: "{$openScript}{$js}</script>";
|
||||
return self::$_replacementHash . 'SCRIPT' . count(self::$_scripts);
|
||||
}
|
||||
|
||||
private static function _removeCdata($str)
|
||||
{
|
||||
return (false !== strpos($str, '<![CDATA['))
|
||||
? str_replace(array('<![CDATA[', ']]>'), '', $str)
|
||||
: $str;
|
||||
}
|
||||
|
||||
private static function _needsCdata($str)
|
||||
{
|
||||
return (self::$_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str));
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* jsmin.php - PHP implementation of Douglas Crockford's JSMin.
|
||||
* Minify_Javascript - PHP implementation of Douglas Crockford's JSMin.
|
||||
*
|
||||
* This is pretty much a direct port of jsmin.c to PHP with just a few
|
||||
* PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
|
||||
* This is pretty much a direct port of JSMin.c to PHP with just a few
|
||||
* PHP-specific performance tweaks. Also, whereas JSMin.c reads from stdin and
|
||||
* outputs to stdout, this library accepts a string as input and returns another
|
||||
* string as output.
|
||||
*
|
||||
* PHP 5 or higher is required.
|
||||
*
|
||||
* Permission is hereby granted to use this version of the library under the
|
||||
* same terms as jsmin.c, which has the following license:
|
||||
* same terms as JSMin.c, which has the following license:
|
||||
*
|
||||
* --
|
||||
* Copyright (c) 2002 Douglas Crockford (www.crockford.com)
|
||||
@@ -36,44 +36,42 @@
|
||||
* SOFTWARE.
|
||||
* --
|
||||
*
|
||||
* @package JSMin
|
||||
* @package Minify_Javascript
|
||||
* @author Ryan Grove <ryan@wonko.com>
|
||||
* @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
|
||||
* @copyright 2002 Douglas Crockford <douglas@crockford.com> (JSMin.c)
|
||||
* @copyright 2007 Ryan Grove <ryan@wonko.com> (PHP port)
|
||||
* @license http://opensource.org/licenses/mit-license.php MIT License
|
||||
* @version 1.1.0 (2007-06-01)
|
||||
* @link http://code.google.com/p/jsmin-php/
|
||||
*/
|
||||
|
||||
class JSMin {
|
||||
class Minify_Javascript {
|
||||
const ORD_LF = 10;
|
||||
const ORD_SPACE = 32;
|
||||
|
||||
protected $a = '';
|
||||
protected $b = '';
|
||||
protected $input = '';
|
||||
protected $inputIndex = 0;
|
||||
protected $inputLength = 0;
|
||||
protected $lookAhead = null;
|
||||
protected $output = array();
|
||||
private $a = '';
|
||||
private $b = '';
|
||||
private $input = '';
|
||||
private $inputIndex = 0;
|
||||
private $inputLength = 0;
|
||||
private $lookAhead = null;
|
||||
private $output = array();
|
||||
|
||||
// -- Public Static Methods --------------------------------------------------
|
||||
|
||||
public static function minify($js) {
|
||||
$jsmin = new JSMin($js);
|
||||
return $jsmin->min();
|
||||
public static function minify($js, $options = array()) {
|
||||
$js = new Minify_Javascript($js);
|
||||
return trim($js->min());
|
||||
}
|
||||
|
||||
// -- Public Instance Methods ------------------------------------------------
|
||||
// -- Private Instance Methods ---------------------------------------------
|
||||
|
||||
public function __construct($input) {
|
||||
private function __construct($input) {
|
||||
$this->input = str_replace("\r\n", "\n", $input);
|
||||
$this->inputLength = strlen($this->input);
|
||||
}
|
||||
|
||||
// -- Protected Instance Methods ---------------------------------------------
|
||||
|
||||
protected function action($d) {
|
||||
private function action($d) {
|
||||
switch($d) {
|
||||
case 1:
|
||||
$this->output[] = $this->a;
|
||||
@@ -91,7 +89,7 @@ class JSMin {
|
||||
}
|
||||
|
||||
if (ord($this->a) <= self::ORD_LF) {
|
||||
throw new JSMinException('Unterminated string literal.');
|
||||
throw new Minify_JavascriptException('Unterminated string literal.');
|
||||
}
|
||||
|
||||
if ($this->a === '\\') {
|
||||
@@ -123,7 +121,7 @@ class JSMin {
|
||||
$this->a = $this->get();
|
||||
}
|
||||
elseif (ord($this->a) <= self::ORD_LF) {
|
||||
throw new JSMinException('Unterminated regular expression '.
|
||||
throw new Minify_JavascriptException('Unterminated regular expression '.
|
||||
'literal.');
|
||||
}
|
||||
|
||||
@@ -135,7 +133,7 @@ class JSMin {
|
||||
}
|
||||
}
|
||||
|
||||
protected function get() {
|
||||
private function get() {
|
||||
$c = $this->lookAhead;
|
||||
$this->lookAhead = null;
|
||||
|
||||
@@ -160,18 +158,14 @@ class JSMin {
|
||||
return ' ';
|
||||
}
|
||||
|
||||
protected function isAlphaNum($c) {
|
||||
return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1;
|
||||
}
|
||||
|
||||
protected function min() {
|
||||
private function min() {
|
||||
$this->a = "\n";
|
||||
$this->action(3);
|
||||
|
||||
while ($this->a !== null) {
|
||||
switch ($this->a) {
|
||||
case ' ':
|
||||
if ($this->isAlphaNum($this->b)) {
|
||||
if (self::isAlphaNum($this->b)) {
|
||||
$this->action(1);
|
||||
}
|
||||
else {
|
||||
@@ -194,7 +188,7 @@ class JSMin {
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($this->isAlphaNum($this->b)) {
|
||||
if (self::isAlphaNum($this->b)) {
|
||||
$this->action(1);
|
||||
}
|
||||
else {
|
||||
@@ -206,7 +200,7 @@ class JSMin {
|
||||
default:
|
||||
switch ($this->b) {
|
||||
case ' ':
|
||||
if ($this->isAlphaNum($this->a)) {
|
||||
if (self::isAlphaNum($this->a)) {
|
||||
$this->action(1);
|
||||
break;
|
||||
}
|
||||
@@ -227,7 +221,7 @@ class JSMin {
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($this->isAlphaNum($this->a)) {
|
||||
if (self::isAlphaNum($this->a)) {
|
||||
$this->action(1);
|
||||
}
|
||||
else {
|
||||
@@ -246,7 +240,7 @@ class JSMin {
|
||||
return implode('', $this->output);
|
||||
}
|
||||
|
||||
protected function next() {
|
||||
private function next() {
|
||||
$c = $this->get();
|
||||
|
||||
if ($c === '/') {
|
||||
@@ -273,7 +267,7 @@ class JSMin {
|
||||
break;
|
||||
|
||||
case null:
|
||||
throw new JSMinException('Unterminated comment.');
|
||||
throw new Minify_JavascriptException('Unterminated comment.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,12 +279,18 @@ class JSMin {
|
||||
return $c;
|
||||
}
|
||||
|
||||
protected function peek() {
|
||||
private function peek() {
|
||||
$this->lookAhead = $this->get();
|
||||
return $this->lookAhead;
|
||||
}
|
||||
|
||||
// Private static functions --------------------------------------------------
|
||||
|
||||
private static function isAlphaNum($c) {
|
||||
return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1;
|
||||
}
|
||||
}
|
||||
|
||||
// -- Exceptions ---------------------------------------------------------------
|
||||
class JSMinException extends Exception {}
|
||||
?>
|
||||
class Minify_JavascriptException extends Exception {}
|
||||
|
745
lib/Minify/Packer.php
Normal file
745
lib/Minify/Packer.php
Normal file
@@ -0,0 +1,745 @@
|
||||
<?php
|
||||
/* 7 December 2006. version 1.0
|
||||
*
|
||||
* This is the php version of the Dean Edwards JavaScript 's Packer,
|
||||
* Based on :
|
||||
*
|
||||
* ParseMaster, version 1.0.2 (2005-08-19) Copyright 2005, Dean Edwards
|
||||
* a multi-pattern parser.
|
||||
* KNOWN BUG: erroneous behavior when using escapeChar with a replacement
|
||||
* value that is a function
|
||||
*
|
||||
* packer, version 2.0.2 (2005-08-19) Copyright 2004-2005, Dean Edwards
|
||||
*
|
||||
* License: http://creativecommons.org/licenses/LGPL/2.1/
|
||||
*
|
||||
* Ported to PHP by Nicolas Martin.
|
||||
*
|
||||
* ----------------------------------------------------------------------
|
||||
*
|
||||
* examples of usage :
|
||||
* $myPacker = new JavaScriptPacker($script, 62, true, false);
|
||||
* $packed = $myPacker->pack();
|
||||
*
|
||||
* or
|
||||
*
|
||||
* $myPacker = new JavaScriptPacker($script, 'Normal', true, false);
|
||||
* $packed = $myPacker->pack();
|
||||
*
|
||||
* or (default values)
|
||||
*
|
||||
* $myPacker = new JavaScriptPacker($script);
|
||||
* $packed = $myPacker->pack();
|
||||
*
|
||||
*
|
||||
* params of the constructor :
|
||||
* $script: the JavaScript to pack, string.
|
||||
* $encoding: level of encoding, int or string :
|
||||
* 0,10,62,95 or 'None', 'Numeric', 'Normal', 'High ASCII'.
|
||||
* default: 62.
|
||||
* $fastDecode: include the fast decoder in the packed result, boolean.
|
||||
* default : true.
|
||||
* $specialChars: if you are flagged your private and local variables
|
||||
* in the script, boolean.
|
||||
* default: false.
|
||||
*
|
||||
* The pack() method return the compressed JavasScript, as a string.
|
||||
*
|
||||
* see http://dean.edwards.name/packer/usage/ for more information.
|
||||
*
|
||||
* Notes :
|
||||
* # need PHP 5 . Tested with PHP 5.1.2
|
||||
*
|
||||
* # The packed result may be different than with the Dean Edwards
|
||||
* version, but with the same length. The reason is that the PHP
|
||||
* function usort to sort array don't necessarily preserve the
|
||||
* original order of two equal member. The Javascript sort function
|
||||
* in fact preserve this order (but that's not require by the
|
||||
* ECMAScript standard). So the encoded keywords order can be
|
||||
* different in the two results.
|
||||
*
|
||||
* # Be careful with the 'High ASCII' Level encoding if you use
|
||||
* UTF-8 in your files...
|
||||
*/
|
||||
|
||||
|
||||
class JavaScriptPacker {
|
||||
// constants
|
||||
const IGNORE = '$1';
|
||||
|
||||
// validate parameters
|
||||
private $_script = '';
|
||||
private $_encoding = 62;
|
||||
private $_fastDecode = true;
|
||||
private $_specialChars = false;
|
||||
|
||||
private $LITERAL_ENCODING = array(
|
||||
'None' => 0,
|
||||
'Numeric' => 10,
|
||||
'Normal' => 62,
|
||||
'High ASCII' => 95
|
||||
);
|
||||
|
||||
public function __construct($_script, $_encoding = 62, $_fastDecode = true, $_specialChars = false)
|
||||
{
|
||||
$this->_script = $_script . "\n";
|
||||
if (array_key_exists($_encoding, $this->LITERAL_ENCODING))
|
||||
$_encoding = $this->LITERAL_ENCODING[$_encoding];
|
||||
$this->_encoding = min((int)$_encoding, 95);
|
||||
$this->_fastDecode = $_fastDecode;
|
||||
$this->_specialChars = $_specialChars;
|
||||
}
|
||||
|
||||
public function pack() {
|
||||
$this->_addParser('_basicCompression');
|
||||
if ($this->_specialChars)
|
||||
$this->_addParser('_encodeSpecialChars');
|
||||
if ($this->_encoding)
|
||||
$this->_addParser('_encodeKeywords');
|
||||
|
||||
// go!
|
||||
return $this->_pack($this->_script);
|
||||
}
|
||||
|
||||
// apply all parsing routines
|
||||
private function _pack($script) {
|
||||
for ($i = 0; isset($this->_parsers[$i]); $i++) {
|
||||
$script = call_user_func(array(&$this,$this->_parsers[$i]), $script);
|
||||
}
|
||||
return $script;
|
||||
}
|
||||
|
||||
// keep a list of parsing functions, they'll be executed all at once
|
||||
private $_parsers = array();
|
||||
private function _addParser($parser) {
|
||||
$this->_parsers[] = $parser;
|
||||
}
|
||||
|
||||
// zero encoding - just removal of white space and comments
|
||||
private function _basicCompression($script) {
|
||||
$parser = new ParseMaster();
|
||||
// make safe
|
||||
$parser->escapeChar = '\\';
|
||||
// protect strings
|
||||
$parser->add('/\'[^\'\\n\\r]*\'/', self::IGNORE);
|
||||
$parser->add('/"[^"\\n\\r]*"/', self::IGNORE);
|
||||
// remove comments
|
||||
$parser->add('/\\/\\/[^\\n\\r]*[\\n\\r]/', ' ');
|
||||
$parser->add('/\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\//', ' ');
|
||||
// protect regular expressions
|
||||
$parser->add('/\\s+(\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?)/', '$2'); // IGNORE
|
||||
$parser->add('/[^\\w\\x24\\/\'"*)\\?:]\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?/', self::IGNORE);
|
||||
// remove: ;;; doSomething();
|
||||
if ($this->_specialChars) $parser->add('/;;;[^\\n\\r]+[\\n\\r]/');
|
||||
// remove redundant semi-colons
|
||||
$parser->add('/\\(;;\\)/', self::IGNORE); // protect for (;;) loops
|
||||
$parser->add('/;+\\s*([};])/', '$2');
|
||||
// apply the above
|
||||
$script = $parser->exec($script);
|
||||
|
||||
// remove white-space
|
||||
$parser->add('/(\\b|\\x24)\\s+(\\b|\\x24)/', '$2 $3');
|
||||
$parser->add('/([+\\-])\\s+([+\\-])/', '$2 $3');
|
||||
$parser->add('/\\s+/', '');
|
||||
// done
|
||||
return $parser->exec($script);
|
||||
}
|
||||
|
||||
private function _encodeSpecialChars($script) {
|
||||
$parser = new ParseMaster();
|
||||
// replace: $name -> n, $$name -> na
|
||||
$parser->add('/((\\x24+)([a-zA-Z$_]+))(\\d*)/',
|
||||
array('fn' => '_replace_name')
|
||||
);
|
||||
// replace: _name -> _0, double-underscore (__name) is ignored
|
||||
$regexp = '/\\b_[A-Za-z\\d]\\w*/';
|
||||
// build the word list
|
||||
$keywords = $this->_analyze($script, $regexp, '_encodePrivate');
|
||||
// quick ref
|
||||
$encoded = $keywords['encoded'];
|
||||
|
||||
$parser->add($regexp,
|
||||
array(
|
||||
'fn' => '_replace_encoded',
|
||||
'data' => $encoded
|
||||
)
|
||||
);
|
||||
return $parser->exec($script);
|
||||
}
|
||||
|
||||
private function _encodeKeywords($script) {
|
||||
// escape high-ascii values already in the script (i.e. in strings)
|
||||
if ($this->_encoding > 62)
|
||||
$script = $this->_escape95($script);
|
||||
// create the parser
|
||||
$parser = new ParseMaster();
|
||||
$encode = $this->_getEncoder($this->_encoding);
|
||||
// for high-ascii, don't encode single character low-ascii
|
||||
$regexp = ($this->_encoding > 62) ? '/\\w\\w+/' : '/\\w+/';
|
||||
// build the word list
|
||||
$keywords = $this->_analyze($script, $regexp, $encode);
|
||||
$encoded = $keywords['encoded'];
|
||||
|
||||
// encode
|
||||
$parser->add($regexp,
|
||||
array(
|
||||
'fn' => '_replace_encoded',
|
||||
'data' => $encoded
|
||||
)
|
||||
);
|
||||
if (empty($script)) return $script;
|
||||
else {
|
||||
//$res = $parser->exec($script);
|
||||
//$res = $this->_bootStrap($res, $keywords);
|
||||
//return $res;
|
||||
return $this->_bootStrap($parser->exec($script), $keywords);
|
||||
}
|
||||
}
|
||||
|
||||
private function _analyze($script, $regexp, $encode) {
|
||||
// analyse
|
||||
// retreive all words in the script
|
||||
$all = array();
|
||||
preg_match_all($regexp, $script, $all);
|
||||
$_sorted = array(); // list of words sorted by frequency
|
||||
$_encoded = array(); // dictionary of word->encoding
|
||||
$_protected = array(); // instances of "protected" words
|
||||
$all = $all[0]; // simulate the javascript comportement of global match
|
||||
if (!empty($all)) {
|
||||
$unsorted = array(); // same list, not sorted
|
||||
$protected = array(); // "protected" words (dictionary of word->"word")
|
||||
$value = array(); // dictionary of charCode->encoding (eg. 256->ff)
|
||||
$this->_count = array(); // word->count
|
||||
$i = count($all); $j = 0; //$word = null;
|
||||
// count the occurrences - used for sorting later
|
||||
do {
|
||||
--$i;
|
||||
$word = '$' . $all[$i];
|
||||
if (!isset($this->_count[$word])) {
|
||||
$this->_count[$word] = 0;
|
||||
$unsorted[$j] = $word;
|
||||
// make a dictionary of all of the protected words in this script
|
||||
// these are words that might be mistaken for encoding
|
||||
//if (is_string($encode) && method_exists($this, $encode))
|
||||
$values[$j] = call_user_func(array(&$this, $encode), $j);
|
||||
$protected['$' . $values[$j]] = $j++;
|
||||
}
|
||||
// increment the word counter
|
||||
$this->_count[$word]++;
|
||||
} while ($i > 0);
|
||||
// prepare to sort the word list, first we must protect
|
||||
// words that are also used as codes. we assign them a code
|
||||
// equivalent to the word itself.
|
||||
// e.g. if "do" falls within our encoding range
|
||||
// then we store keywords["do"] = "do";
|
||||
// this avoids problems when decoding
|
||||
$i = count($unsorted);
|
||||
do {
|
||||
$word = $unsorted[--$i];
|
||||
if (isset($protected[$word]) /*!= null*/) {
|
||||
$_sorted[$protected[$word]] = substr($word, 1);
|
||||
$_protected[$protected[$word]] = true;
|
||||
$this->_count[$word] = 0;
|
||||
}
|
||||
} while ($i);
|
||||
|
||||
// sort the words by frequency
|
||||
// Note: the javascript and php version of sort can be different :
|
||||
// in php manual, usort :
|
||||
// " If two members compare as equal,
|
||||
// their order in the sorted array is undefined."
|
||||
// so the final packed script is different of the Dean's javascript version
|
||||
// but equivalent.
|
||||
// the ECMAscript standard does not guarantee this behaviour,
|
||||
// and thus not all browsers (e.g. Mozilla versions dating back to at
|
||||
// least 2003) respect this.
|
||||
usort($unsorted, array(&$this, '_sortWords'));
|
||||
$j = 0;
|
||||
// because there are "protected" words in the list
|
||||
// we must add the sorted words around them
|
||||
do {
|
||||
if (!isset($_sorted[$i]))
|
||||
$_sorted[$i] = substr($unsorted[$j++], 1);
|
||||
$_encoded[$_sorted[$i]] = $values[$i];
|
||||
} while (++$i < count($unsorted));
|
||||
}
|
||||
return array(
|
||||
'sorted' => $_sorted,
|
||||
'encoded' => $_encoded,
|
||||
'protected' => $_protected);
|
||||
}
|
||||
|
||||
private $_count = array();
|
||||
private function _sortWords($match1, $match2) {
|
||||
return $this->_count[$match2] - $this->_count[$match1];
|
||||
}
|
||||
|
||||
// build the boot function used for loading and decoding
|
||||
private function _bootStrap($packed, $keywords) {
|
||||
$ENCODE = $this->_safeRegExp('$encode\\($count\\)');
|
||||
|
||||
// $packed: the packed script
|
||||
$packed = "'" . $this->_escape($packed) . "'";
|
||||
|
||||
// $ascii: base for encoding
|
||||
$ascii = min(count($keywords['sorted']), $this->_encoding);
|
||||
if ($ascii == 0) $ascii = 1;
|
||||
|
||||
// $count: number of words contained in the script
|
||||
$count = count($keywords['sorted']);
|
||||
|
||||
// $keywords: list of words contained in the script
|
||||
foreach ($keywords['protected'] as $i=>$value) {
|
||||
$keywords['sorted'][$i] = '';
|
||||
}
|
||||
// convert from a string to an array
|
||||
ksort($keywords['sorted']);
|
||||
$keywords = "'" . implode('|',$keywords['sorted']) . "'.split('|')";
|
||||
|
||||
$encode = ($this->_encoding > 62) ? '_encode95' : $this->_getEncoder($ascii);
|
||||
$encode = $this->_getJSFunction($encode);
|
||||
$encode = preg_replace('/_encoding/','$ascii', $encode);
|
||||
$encode = preg_replace('/arguments\\.callee/','$encode', $encode);
|
||||
$inline = '\\$count' . ($ascii > 10 ? '.toString(\\$ascii)' : '');
|
||||
|
||||
// $decode: code snippet to speed up decoding
|
||||
if ($this->_fastDecode) {
|
||||
// create the decoder
|
||||
$decode = $this->_getJSFunction('_decodeBody');
|
||||
if ($this->_encoding > 62)
|
||||
$decode = preg_replace('/\\\\w/', '[\\xa1-\\xff]', $decode);
|
||||
// perform the encoding inline for lower ascii values
|
||||
elseif ($ascii < 36)
|
||||
$decode = preg_replace($ENCODE, $inline, $decode);
|
||||
// special case: when $count==0 there are no keywords. I want to keep
|
||||
// the basic shape of the unpacking funcion so i'll frig the code...
|
||||
if ($count == 0)
|
||||
$decode = preg_replace($this->_safeRegExp('($count)\\s*=\\s*1'), '$1=0', $decode, 1);
|
||||
}
|
||||
|
||||
// boot function
|
||||
$unpack = $this->_getJSFunction('_unpack');
|
||||
if ($this->_fastDecode) {
|
||||
// insert the decoder
|
||||
$this->buffer = $decode;
|
||||
$unpack = preg_replace_callback('/\\{/', array(&$this, '_insertFastDecode'), $unpack, 1);
|
||||
}
|
||||
$unpack = preg_replace('/"/', "'", $unpack);
|
||||
if ($this->_encoding > 62) { // high-ascii
|
||||
// get rid of the word-boundaries for regexp matches
|
||||
$unpack = preg_replace('/\'\\\\\\\\b\'\s*\\+|\\+\s*\'\\\\\\\\b\'/', '', $unpack);
|
||||
}
|
||||
if ($ascii > 36 || $this->_encoding > 62 || $this->_fastDecode) {
|
||||
// insert the encode function
|
||||
$this->buffer = $encode;
|
||||
$unpack = preg_replace_callback('/\\{/', array(&$this, '_insertFastEncode'), $unpack, 1);
|
||||
} else {
|
||||
// perform the encoding inline
|
||||
$unpack = preg_replace($ENCODE, $inline, $unpack);
|
||||
}
|
||||
// pack the boot function too
|
||||
$unpackPacker = new JavaScriptPacker($unpack, 0, false, true);
|
||||
$unpack = $unpackPacker->pack();
|
||||
|
||||
// arguments
|
||||
$params = array($packed, $ascii, $count, $keywords);
|
||||
if ($this->_fastDecode) {
|
||||
$params[] = 0;
|
||||
$params[] = '{}';
|
||||
}
|
||||
$params = implode(',', $params);
|
||||
|
||||
// the whole thing
|
||||
return 'eval(' . $unpack . '(' . $params . "))\n";
|
||||
}
|
||||
|
||||
private $buffer;
|
||||
private function _insertFastDecode($match) {
|
||||
return '{' . $this->buffer . ';';
|
||||
}
|
||||
private function _insertFastEncode($match) {
|
||||
return '{$encode=' . $this->buffer . ';';
|
||||
}
|
||||
|
||||
// mmm.. ..which one do i need ??
|
||||
private function _getEncoder($ascii) {
|
||||
return $ascii > 10 ? $ascii > 36 ? $ascii > 62 ?
|
||||
'_encode95' : '_encode62' : '_encode36' : '_encode10';
|
||||
}
|
||||
|
||||
// zero encoding
|
||||
// characters: 0123456789
|
||||
private function _encode10($charCode) {
|
||||
return $charCode;
|
||||
}
|
||||
|
||||
// inherent base36 support
|
||||
// characters: 0123456789abcdefghijklmnopqrstuvwxyz
|
||||
private function _encode36($charCode) {
|
||||
return base_convert($charCode, 10, 36);
|
||||
}
|
||||
|
||||
// hitch a ride on base36 and add the upper case alpha characters
|
||||
// characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
private function _encode62($charCode) {
|
||||
$res = '';
|
||||
if ($charCode >= $this->_encoding) {
|
||||
$res = $this->_encode62((int)($charCode / $this->_encoding));
|
||||
}
|
||||
$charCode = $charCode % $this->_encoding;
|
||||
|
||||
if ($charCode > 35)
|
||||
return $res . chr($charCode + 29);
|
||||
else
|
||||
return $res . base_convert($charCode, 10, 36);
|
||||
}
|
||||
|
||||
// use high-ascii values
|
||||
// characters: ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ
|
||||
private function _encode95($charCode) {
|
||||
$res = '';
|
||||
if ($charCode >= $this->_encoding)
|
||||
$res = $this->_encode95($charCode / $this->_encoding);
|
||||
|
||||
return $res . chr(($charCode % $this->_encoding) + 161);
|
||||
}
|
||||
|
||||
private function _safeRegExp($string) {
|
||||
return '/'.preg_replace('/\$/', '\\\$', $string).'/';
|
||||
}
|
||||
|
||||
private function _encodePrivate($charCode) {
|
||||
return "_" . $charCode;
|
||||
}
|
||||
|
||||
// protect characters used by the parser
|
||||
private function _escape($script) {
|
||||
return preg_replace('/([\\\\\'])/', '\\\$1', $script);
|
||||
}
|
||||
|
||||
// protect high-ascii characters already in the script
|
||||
private function _escape95($script) {
|
||||
return preg_replace_callback(
|
||||
'/[\\xa1-\\xff]/',
|
||||
array(&$this, '_escape95Bis'),
|
||||
$script
|
||||
);
|
||||
}
|
||||
private function _escape95Bis($match) {
|
||||
return '\x'.((string)dechex(ord($match)));
|
||||
}
|
||||
|
||||
|
||||
private function _getJSFunction($aName) {
|
||||
if (defined('self::JSFUNCTION'.$aName))
|
||||
return constant('self::JSFUNCTION'.$aName);
|
||||
else
|
||||
return '';
|
||||
}
|
||||
|
||||
// JavaScript Functions used.
|
||||
// Note : In Dean's version, these functions are converted
|
||||
// with 'String(aFunctionName);'.
|
||||
// This internal conversion complete the original code, ex :
|
||||
// 'while (aBool) anAction();' is converted to
|
||||
// 'while (aBool) { anAction(); }'.
|
||||
// The JavaScript functions below are corrected.
|
||||
|
||||
// unpacking function - this is the boot strap function
|
||||
// data extracted from this packing routine is passed to
|
||||
// this function when decoded in the target
|
||||
// NOTE ! : without the ';' final.
|
||||
const JSFUNCTION_unpack =
|
||||
|
||||
'function($packed, $ascii, $count, $keywords, $encode, $decode) {
|
||||
while ($count--) {
|
||||
if ($keywords[$count]) {
|
||||
$packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]);
|
||||
}
|
||||
}
|
||||
return $packed;
|
||||
}';
|
||||
/*
|
||||
'function($packed, $ascii, $count, $keywords, $encode, $decode) {
|
||||
while ($count--)
|
||||
if ($keywords[$count])
|
||||
$packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]);
|
||||
return $packed;
|
||||
}';
|
||||
*/
|
||||
|
||||
// code-snippet inserted into the unpacker to speed up decoding
|
||||
const JSFUNCTION_decodeBody =
|
||||
//_decode = function() {
|
||||
// does the browser support String.replace where the
|
||||
// replacement value is a function?
|
||||
|
||||
' if (!\'\'.replace(/^/, String)) {
|
||||
// decode all the values we need
|
||||
while ($count--) {
|
||||
$decode[$encode($count)] = $keywords[$count] || $encode($count);
|
||||
}
|
||||
// global replacement function
|
||||
$keywords = [function ($encoded) {return $decode[$encoded]}];
|
||||
// generic match
|
||||
$encode = function () {return \'\\\\w+\'};
|
||||
// reset the loop counter - we are now doing a global replace
|
||||
$count = 1;
|
||||
}
|
||||
';
|
||||
//};
|
||||
/*
|
||||
' if (!\'\'.replace(/^/, String)) {
|
||||
// decode all the values we need
|
||||
while ($count--) $decode[$encode($count)] = $keywords[$count] || $encode($count);
|
||||
// global replacement function
|
||||
$keywords = [function ($encoded) {return $decode[$encoded]}];
|
||||
// generic match
|
||||
$encode = function () {return\'\\\\w+\'};
|
||||
// reset the loop counter - we are now doing a global replace
|
||||
$count = 1;
|
||||
}';
|
||||
*/
|
||||
|
||||
// zero encoding
|
||||
// characters: 0123456789
|
||||
const JSFUNCTION_encode10 =
|
||||
'function($charCode) {
|
||||
return $charCode;
|
||||
}';//;';
|
||||
|
||||
// inherent base36 support
|
||||
// characters: 0123456789abcdefghijklmnopqrstuvwxyz
|
||||
const JSFUNCTION_encode36 =
|
||||
'function($charCode) {
|
||||
return $charCode.toString(36);
|
||||
}';//;';
|
||||
|
||||
// hitch a ride on base36 and add the upper case alpha characters
|
||||
// characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
const JSFUNCTION_encode62 =
|
||||
'function($charCode) {
|
||||
return ($charCode < _encoding ? \'\' : arguments.callee(parseInt($charCode / _encoding))) +
|
||||
(($charCode = $charCode % _encoding) > 35 ? String.fromCharCode($charCode + 29) : $charCode.toString(36));
|
||||
}';
|
||||
|
||||
// use high-ascii values
|
||||
// characters: ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ
|
||||
const JSFUNCTION_encode95 =
|
||||
'function($charCode) {
|
||||
return ($charCode < _encoding ? \'\' : arguments.callee($charCode / _encoding)) +
|
||||
String.fromCharCode($charCode % _encoding + 161);
|
||||
}';
|
||||
|
||||
}
|
||||
|
||||
|
||||
class ParseMaster {
|
||||
public $ignoreCase = false;
|
||||
public $escapeChar = '';
|
||||
|
||||
// constants
|
||||
const EXPRESSION = 0;
|
||||
const REPLACEMENT = 1;
|
||||
const LENGTH = 2;
|
||||
|
||||
// used to determine nesting levels
|
||||
private $GROUPS = '/\\(/';//g
|
||||
private $SUB_REPLACE = '/\\$\\d/';
|
||||
private $INDEXED = '/^\\$\\d+$/';
|
||||
private $TRIM = '/([\'"])\\1\\.(.*)\\.\\1\\1$/';
|
||||
private $ESCAPE = '/\\\./';//g
|
||||
private $QUOTE = '/\'/';
|
||||
private $DELETED = '/\\x01[^\\x01]*\\x01/';//g
|
||||
|
||||
public function add($expression, $replacement = '') {
|
||||
// count the number of sub-expressions
|
||||
// - add one because each pattern is itself a sub-expression
|
||||
$length = 1 + preg_match_all($this->GROUPS, $this->_internalEscape((string)$expression), $out);
|
||||
|
||||
// treat only strings $replacement
|
||||
if (is_string($replacement)) {
|
||||
// does the pattern deal with sub-expressions?
|
||||
if (preg_match($this->SUB_REPLACE, $replacement)) {
|
||||
// a simple lookup? (e.g. "$2")
|
||||
if (preg_match($this->INDEXED, $replacement)) {
|
||||
// store the index (used for fast retrieval of matched strings)
|
||||
$replacement = (int)(substr($replacement, 1)) - 1;
|
||||
} else { // a complicated lookup (e.g. "Hello $2 $1")
|
||||
// build a function to do the lookup
|
||||
$quote = preg_match($this->QUOTE, $this->_internalEscape($replacement))
|
||||
? '"' : "'";
|
||||
$replacement = array(
|
||||
'fn' => '_backReferences',
|
||||
'data' => array(
|
||||
'replacement' => $replacement,
|
||||
'length' => $length,
|
||||
'quote' => $quote
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// pass the modified arguments
|
||||
if (!empty($expression)) $this->_add($expression, $replacement, $length);
|
||||
else $this->_add('/^$/', $replacement, $length);
|
||||
}
|
||||
|
||||
public function exec($string) {
|
||||
// execute the global replacement
|
||||
$this->_escaped = array();
|
||||
|
||||
// simulate the _patterns.toSTring of Dean
|
||||
$regexp = '/';
|
||||
foreach ($this->_patterns as $reg) {
|
||||
$regexp .= '(' . substr($reg[self::EXPRESSION], 1, -1) . ')|';
|
||||
}
|
||||
$regexp = substr($regexp, 0, -1) . '/';
|
||||
$regexp .= ($this->ignoreCase) ? 'i' : '';
|
||||
|
||||
$string = $this->_escape($string, $this->escapeChar);
|
||||
$string = preg_replace_callback(
|
||||
$regexp,
|
||||
array(
|
||||
&$this,
|
||||
'_replacement'
|
||||
),
|
||||
$string
|
||||
);
|
||||
$string = $this->_unescape($string, $this->escapeChar);
|
||||
|
||||
return preg_replace($this->DELETED, '', $string);
|
||||
}
|
||||
|
||||
public function reset() {
|
||||
// clear the patterns collection so that this object may be re-used
|
||||
$this->_patterns = array();
|
||||
}
|
||||
|
||||
// private
|
||||
private $_escaped = array(); // escaped characters
|
||||
private $_patterns = array(); // patterns stored by index
|
||||
|
||||
// create and add a new pattern to the patterns collection
|
||||
private function _add() {
|
||||
$arguments = func_get_args();
|
||||
$this->_patterns[] = $arguments;
|
||||
}
|
||||
|
||||
// this is the global replace function (it's quite complicated)
|
||||
private function _replacement($arguments) {
|
||||
if (empty($arguments)) return '';
|
||||
|
||||
$i = 1; $j = 0;
|
||||
// loop through the patterns
|
||||
while (isset($this->_patterns[$j])) {
|
||||
$pattern = $this->_patterns[$j++];
|
||||
// do we have a result?
|
||||
if (isset($arguments[$i]) && ($arguments[$i] != '')) {
|
||||
$replacement = $pattern[self::REPLACEMENT];
|
||||
|
||||
if (is_array($replacement) && isset($replacement['fn'])) {
|
||||
|
||||
if (isset($replacement['data'])) $this->buffer = $replacement['data'];
|
||||
return call_user_func(array(&$this, $replacement['fn']), $arguments, $i);
|
||||
|
||||
} elseif (is_int($replacement)) {
|
||||
return $arguments[$replacement + $i];
|
||||
|
||||
}
|
||||
$delete = ($this->escapeChar == '' ||
|
||||
strpos($arguments[$i], $this->escapeChar) === false)
|
||||
? '' : "\x01" . $arguments[$i] . "\x01";
|
||||
return $delete . $replacement;
|
||||
|
||||
// skip over references to sub-expressions
|
||||
} else {
|
||||
$i += $pattern[self::LENGTH];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function _backReferences($match, $offset) {
|
||||
$replacement = $this->buffer['replacement'];
|
||||
$quote = $this->buffer['quote'];
|
||||
$i = $this->buffer['length'];
|
||||
while ($i) {
|
||||
$replacement = str_replace('$'.$i--, $match[$offset + $i], $replacement);
|
||||
}
|
||||
return $replacement;
|
||||
}
|
||||
|
||||
private function _replace_name($match, $offset){
|
||||
$length = strlen($match[$offset + 2]);
|
||||
$start = $length - max($length - strlen($match[$offset + 3]), 0);
|
||||
return substr($match[$offset + 1], $start, $length) . $match[$offset + 4];
|
||||
}
|
||||
|
||||
private function _replace_encoded($match, $offset) {
|
||||
return $this->buffer[$match[$offset]];
|
||||
}
|
||||
|
||||
|
||||
// php : we cannot pass additional data to preg_replace_callback,
|
||||
// and we cannot use &$this in create_function, so let's go to lower level
|
||||
private $buffer;
|
||||
|
||||
// encode escaped characters
|
||||
private function _escape($string, $escapeChar) {
|
||||
if ($escapeChar) {
|
||||
$this->buffer = $escapeChar;
|
||||
return preg_replace_callback(
|
||||
'/\\' . $escapeChar . '(.)' .'/',
|
||||
array(&$this, '_escapeBis'),
|
||||
$string
|
||||
);
|
||||
|
||||
} else {
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
private function _escapeBis($match) {
|
||||
$this->_escaped[] = $match[1];
|
||||
return $this->buffer;
|
||||
}
|
||||
|
||||
// decode escaped characters
|
||||
private function _unescape($string, $escapeChar) {
|
||||
if ($escapeChar) {
|
||||
$regexp = '/'.'\\'.$escapeChar.'/';
|
||||
$this->buffer = array('escapeChar'=> $escapeChar, 'i' => 0);
|
||||
return preg_replace_callback
|
||||
(
|
||||
$regexp,
|
||||
array(&$this, '_unescapeBis'),
|
||||
$string
|
||||
);
|
||||
|
||||
} else {
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
private function _unescapeBis() {
|
||||
if (!empty($this->_escaped[$this->buffer['i']])) {
|
||||
$temp = $this->_escaped[$this->buffer['i']];
|
||||
} else {
|
||||
$temp = '';
|
||||
}
|
||||
$this->buffer['i']++;
|
||||
return $this->buffer['escapeChar'] . $temp;
|
||||
}
|
||||
|
||||
private function _internalEscape($string) {
|
||||
return preg_replace($this->ESCAPE, '', $string);
|
||||
}
|
||||
}
|
||||
|
||||
// trivial wrapper for Minify
|
||||
class Minify_Packer {
|
||||
public static function minify($code, $options = array())
|
||||
{
|
||||
// @todo: set encoding options based on $options :)
|
||||
$packer = new JavascriptPacker($code, 'Normal', true, false);
|
||||
return trim($packer->pack());
|
||||
}
|
||||
}
|
136
lib/Minify/Source.php
Normal file
136
lib/Minify/Source.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A content source to be minified by Minify.
|
||||
*
|
||||
* This allows per-source minification options and the mixing of files with
|
||||
* content from other sources.
|
||||
*/
|
||||
class Minify_Source {
|
||||
|
||||
/**
|
||||
* @var int time of last modification
|
||||
*/
|
||||
public $lastModified = null;
|
||||
|
||||
/**
|
||||
* @var callback minifier function specifically for this source.
|
||||
*/
|
||||
public $minifier = null;
|
||||
|
||||
/**
|
||||
* @var array minification options specific to this source.
|
||||
*/
|
||||
public $minifyOptions = null;
|
||||
|
||||
/**
|
||||
* Create a Minify_Source
|
||||
*
|
||||
* In the $spec array(), you can either provide a 'filepath' to an existing
|
||||
* file (existence will not be checked!) or give 'id' (unique string for
|
||||
* the content), 'content' (the string content) and 'lastModified'
|
||||
* (unixtime of last update).
|
||||
*
|
||||
* @param array $spec options
|
||||
*/
|
||||
public function __construct($spec)
|
||||
{
|
||||
if (isset($spec['filepath'])) {
|
||||
$this->_filepath = $spec['filepath'];
|
||||
$this->_id = $spec['filepath'];
|
||||
$this->lastModified = filemtime($spec['filepath']);
|
||||
} elseif (isset($spec['id'])) {
|
||||
$this->_id = 'id::' . $spec['id'];
|
||||
$this->_content = $spec['content'];
|
||||
$this->lastModified = isset($spec['lastModified'])
|
||||
? $spec['lastModified']
|
||||
: time();
|
||||
}
|
||||
if (isset($spec['minifier'])) {
|
||||
$this->minifier = $spec['minifier'];
|
||||
}
|
||||
if (isset($spec['minifyOptions'])) {
|
||||
$this->minifyOptions = $spec['minifyOptions'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return (null !== $this->_content)
|
||||
? $this->_content
|
||||
: file_get_contents($this->_filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a single minification call can handle all sources
|
||||
*
|
||||
* @param array $sources Minify_Source instances
|
||||
*
|
||||
* @return bool true iff there no sources with specific minifier preferences.
|
||||
*/
|
||||
public static function haveNoMinifyPrefs($sources)
|
||||
{
|
||||
foreach ($sources as $source) {
|
||||
if (null !== $source->minifier
|
||||
|| null !== $source->minifyOptions) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique string for a set of sources
|
||||
*
|
||||
* @param array $sources Minify_Source instances
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getDigest($sources)
|
||||
{
|
||||
foreach ($sources as $source) {
|
||||
$info[] = array(
|
||||
$source->_id, $source->minifier, $source->minifyOptions
|
||||
);
|
||||
}
|
||||
return md5(serialize($info));
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess content type from the first filename extension available
|
||||
*
|
||||
* This is called if the user doesn't pass in a 'contentType' options
|
||||
*
|
||||
* @param array $sources Minify_Source instances
|
||||
*
|
||||
* @return string content type. e.g. 'text/css'
|
||||
*/
|
||||
public static function getContentType($sources)
|
||||
{
|
||||
$exts = array(
|
||||
'css' => 'text/css'
|
||||
,'js' => 'application/x-javascript'
|
||||
,'html' => 'text/html'
|
||||
);
|
||||
foreach ($sources as $source) {
|
||||
if (null !== $source->_filepath) {
|
||||
$segments = explode('.', $source->_filepath);
|
||||
$ext = array_pop($segments);
|
||||
if (isset($exts[$ext])) {
|
||||
return $exts[$ext];
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'text/plain';
|
||||
}
|
||||
|
||||
private $_content = null;
|
||||
private $_filepath = null;
|
||||
private $_id = null;
|
||||
}
|
||||
|
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
class HTMLMin {
|
||||
// -- Public Static Methods --------------------------------------------------
|
||||
public static function minify($string) {
|
||||
$htmlmin = new HTMLMin($string);
|
||||
return $htmlmin->getMinifiedHtml();
|
||||
}
|
||||
|
||||
// -- Private Instance Variables ---------------------------------------------
|
||||
private $input;
|
||||
|
||||
// -- Private Instance Methods -----------------------------------------------
|
||||
private function replaceCSS($matches) {
|
||||
// Remove HTML comment markers from the CSS (they shouldn't be there
|
||||
// anyway).
|
||||
$css = preg_replace('/<!--([\s\S]*?)-->/', "$1", $matches[2]);
|
||||
|
||||
return '<style'.$matches[1].'>'.trim(Minify::min($css, Minify::TYPE_CSS)).
|
||||
'</style>';
|
||||
}
|
||||
|
||||
private function replaceJavaScript($matches) {
|
||||
// Remove HTML comment markers from the JS (they shouldn't be there anyway).
|
||||
$js = preg_replace('/<!--([\s\S]*?)-->/', "$1", $matches[2]);
|
||||
|
||||
return '<script'.$matches[1].'>'.trim(Minify::min($js, Minify::TYPE_JS)).
|
||||
'</script>';
|
||||
}
|
||||
|
||||
// -- Public Instance Methods ------------------------------------------------
|
||||
public function __construct($input = '') {
|
||||
$this->setInput($input);
|
||||
}
|
||||
|
||||
public function getInput() {
|
||||
return $this->input;
|
||||
}
|
||||
|
||||
public function getMinifiedHtml() {
|
||||
$html = trim($this->input);
|
||||
|
||||
// Run JavaScript blocks through JSMin.
|
||||
$html = preg_replace_callback('/<script(\s+[\s\S]*?)?>([\s\S]*?)<\/script>/i',
|
||||
array($this, 'replaceJavaScript'), $html);
|
||||
|
||||
// Run CSS blocks through Minify's CSS minifier.
|
||||
$html = preg_replace_callback('/<style(\s+[\s\S]*?)?>([\s\S]*?)<\/style>/i',
|
||||
array($this, 'replaceCSS'), $html);
|
||||
|
||||
// Remove HTML comments (but not IE conditional comments).
|
||||
$html = preg_replace('/<!--[^[][\s\S]*?-->/', '', $html);
|
||||
|
||||
// Remove leading and trailing whitespace from each line.
|
||||
// FIXME: This needs to take into account attribute values that span multiple lines.
|
||||
$html = preg_replace('/^\s*(.*?)\s*$/m', "$1", $html);
|
||||
|
||||
// Remove unnecessary whitespace between and inside elements.
|
||||
$html = preg_replace('/>\s+(\S[\s\S]*?)?</', "> $1<", $html);
|
||||
$html = preg_replace('/>(\S[\s\S]*?)?\s+</', ">$1 <", $html);
|
||||
$html = preg_replace('/>\s+</', "> <", $html);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function setInput($input) {
|
||||
$this->input = $input;
|
||||
}
|
||||
}
|
||||
?>
|
490
minify.php
490
minify.php
@@ -1,490 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Minify - Combines, minifies, and caches JavaScript and CSS files on demand.
|
||||
*
|
||||
* See http://code.google.com/p/minify/ for usage instructions.
|
||||
*
|
||||
* This library was inspired by jscsscomp by Maxim Martynyuk <flashkot@mail.ru>
|
||||
* and by the article "Supercharged JavaScript" by Patrick Hunlock
|
||||
* <wb@hunlock.com>.
|
||||
*
|
||||
* JSMin was originally written by Douglas Crockford <douglas@crockford.com>.
|
||||
*
|
||||
* Requires PHP 5.2.1+.
|
||||
*
|
||||
* @package Minify
|
||||
* @author Ryan Grove <ryan@wonko.com>
|
||||
* @copyright 2007 Ryan Grove. All rights reserved.
|
||||
* @license http://opensource.org/licenses/bsd-license.php New BSD License
|
||||
* @version 1.1.0 (?)
|
||||
* @link http://code.google.com/p/minify/
|
||||
*/
|
||||
|
||||
if (!defined('MINIFY_BASE_DIR')) {
|
||||
/**
|
||||
* Base path from which all relative file paths should be resolved. By default
|
||||
* this is set to the document root.
|
||||
*/
|
||||
define('MINIFY_BASE_DIR', realpath($_SERVER['DOCUMENT_ROOT']));
|
||||
}
|
||||
|
||||
if (!defined('MINIFY_CACHE_DIR')) {
|
||||
/** Directory where compressed files will be cached. */
|
||||
define('MINIFY_CACHE_DIR', sys_get_temp_dir());
|
||||
}
|
||||
|
||||
if (!defined('MINIFY_ENCODING')) {
|
||||
/** Character set to use when outputting the minified files. */
|
||||
define('MINIFY_ENCODING', 'utf-8');
|
||||
}
|
||||
|
||||
if (!defined('MINIFY_MAX_FILES')) {
|
||||
/** Maximum number of files to combine in one request. */
|
||||
define('MINIFY_MAX_FILES', 16);
|
||||
}
|
||||
|
||||
if (!defined('MINIFY_REWRITE_CSS_URLS')) {
|
||||
/**
|
||||
* Whether or not Minify should attempt to rewrite relative URLs used in CSS
|
||||
* files so that they continue to point to the correct location after the file
|
||||
* is combined and minified.
|
||||
*
|
||||
* Minify is pretty good at getting this right, but occasionally it can make
|
||||
* mistakes. If you find that URL rewriting results in problems, you should
|
||||
* disable it.
|
||||
*/
|
||||
define('MINIFY_REWRITE_CSS_URLS', true);
|
||||
}
|
||||
|
||||
if (!defined('MINIFY_USE_CACHE')) {
|
||||
/**
|
||||
* Whether or not Minify should use a disk-based cache to increase
|
||||
* performance.
|
||||
*/
|
||||
define('MINIFY_USE_CACHE', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minify is a library for combining, minifying, and caching JavaScript and CSS
|
||||
* files on demand before sending them to a web browser.
|
||||
*
|
||||
* @package Minify
|
||||
* @author Ryan Grove <ryan@wonko.com>
|
||||
* @copyright 2007 Ryan Grove. All rights reserved.
|
||||
* @license http://opensource.org/licenses/bsd-license.php New BSD License
|
||||
* @version 1.1.0 (?)
|
||||
* @link http://code.google.com/p/minify/
|
||||
*/
|
||||
class Minify {
|
||||
const TYPE_CSS = 'text/css';
|
||||
const TYPE_HTML = 'text/html';
|
||||
const TYPE_JS = 'text/javascript';
|
||||
|
||||
protected $files = array();
|
||||
protected $type = self::TYPE_JS;
|
||||
|
||||
// -- Public Static Methods --------------------------------------------------
|
||||
|
||||
/**
|
||||
* Combines, minifies, and outputs the requested files.
|
||||
*
|
||||
* Inspects the $_GET array for a 'files' entry containing a comma-separated
|
||||
* list and uses this as the set of files to be combined and minified.
|
||||
*/
|
||||
public static function handleRequest() {
|
||||
// 404 if no files were requested.
|
||||
if (!isset($_GET['files'])) {
|
||||
header('HTTP/1.0 404 Not Found');
|
||||
exit;
|
||||
}
|
||||
|
||||
$files = array_map('trim', explode(',', $_GET['files'], MINIFY_MAX_FILES));
|
||||
|
||||
// 404 if the $files array is empty for some weird reason.
|
||||
if (!count($files)) {
|
||||
header('HTTP/1.0 404 Not Found');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Determine the content type based on the extension of the first file
|
||||
// requested.
|
||||
if (preg_match('/\.js$/iD', $files[0])) {
|
||||
$type = self::TYPE_JS;
|
||||
} else if (preg_match('/\.css$/iD', $files[0])) {
|
||||
$type = self::TYPE_CSS;
|
||||
} else {
|
||||
$type = self::TYPE_HTML;
|
||||
}
|
||||
|
||||
// Minify and spit out the result.
|
||||
try {
|
||||
$minify = new Minify($type, $files);
|
||||
|
||||
header("Content-Type: $type;charset=".MINIFY_ENCODING);
|
||||
|
||||
$minify->browserCache();
|
||||
echo $minify->combine();
|
||||
exit;
|
||||
}
|
||||
catch (MinifyException $e) {
|
||||
header('HTTP/1.0 404 Not Found');
|
||||
echo htmlentities($e->getMessage());
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minifies the specified string and returns it.
|
||||
*
|
||||
* @param string $string JavaScript, CSS, or HTML string to minify
|
||||
* @param string $type content type of the string (Minify::TYPE_CSS,
|
||||
* Minify::TYPE_HTML, or Minify::TYPE_JS)
|
||||
* @return string minified string
|
||||
*/
|
||||
public static function min($string, $type = self::TYPE_JS) {
|
||||
switch ($type) {
|
||||
case self::TYPE_CSS:
|
||||
return self::minifyCSS($string);
|
||||
break;
|
||||
|
||||
case self::TYPE_HTML:
|
||||
return self::minifyHTML($string);
|
||||
break;
|
||||
|
||||
case self::TYPE_JS:
|
||||
return self::minifyJS($string);
|
||||
break;
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
// -- Protected Static Methods -----------------------------------------------
|
||||
|
||||
/**
|
||||
* Minifies the specified CSS string and returns it.
|
||||
*
|
||||
* @param string $css CSS string
|
||||
* @return string minified string
|
||||
* @see minify()
|
||||
* @see minifyJS()
|
||||
*/
|
||||
protected static function minifyCSS($css) {
|
||||
// Compress whitespace.
|
||||
$css = preg_replace('/\s+/', ' ', $css);
|
||||
|
||||
// Remove comments.
|
||||
$css = preg_replace('/\/\*.*?\*\//', '', $css);
|
||||
|
||||
return trim($css);
|
||||
}
|
||||
|
||||
protected static function minifyHTML($html) {
|
||||
require_once dirname(__FILE__).'/lib/htmlmin.php';
|
||||
return HTMLMin::minify($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minifies the specified JavaScript string and returns it.
|
||||
*
|
||||
* @param string $js JavaScript string
|
||||
* @return string minified string
|
||||
* @see minify()
|
||||
* @see minifyCSS()
|
||||
*/
|
||||
protected static function minifyJS($js) {
|
||||
require_once dirname(__FILE__).'/lib/jsmin.php';
|
||||
return JSMin::minify($js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites relative URLs in the specified CSS string to point to the correct
|
||||
* location. URLs are assumed to be relative to the absolute path specified in
|
||||
* the $path parameter.
|
||||
*
|
||||
* @param string $css CSS string
|
||||
* @param string $path absolute path to which URLs are relative (should be a
|
||||
* directory, not a file)
|
||||
* @return string CSS string with rewritten URLs
|
||||
*/
|
||||
protected static function rewriteCSSUrls($css, $path) {
|
||||
/*
|
||||
Parentheses, commas, whitespace chars, single quotes, and double quotes are
|
||||
escaped with a backslash as described in the CSS spec:
|
||||
http://www.w3.org/TR/REC-CSS1#url
|
||||
*/
|
||||
$relativePath = preg_replace('/([\(\),\s\'"])/', '\\\$1',
|
||||
str_replace(MINIFY_BASE_DIR, '', $path));
|
||||
|
||||
return preg_replace('/url\(\s*[\'"]?\/?(.+?)[\'"]?\s*\)/i', 'url('.
|
||||
$relativePath.'/$1)', $css);
|
||||
}
|
||||
|
||||
// -- Public Instance Methods ------------------------------------------------
|
||||
|
||||
/**
|
||||
* Instantiates a new Minify object. A filename can be in the form of a
|
||||
* relative path or a URL that resolves to the same site that hosts Minify.
|
||||
*
|
||||
* @param string $type content type of the specified files (either
|
||||
* Minify::TYPE_CSS or Minify::TYPE_JS)
|
||||
* @param array|string $files filename or array of filenames to be minified
|
||||
*/
|
||||
public function __construct($type = self::TYPE_JS, $files = array()) {
|
||||
if ($type !== self::TYPE_JS && $type !== self::TYPE_CSS) {
|
||||
throw new MinifyInvalidArgumentException('Invalid argument ($type): '.
|
||||
$type);
|
||||
}
|
||||
|
||||
$this->type = $type;
|
||||
|
||||
if (count((array) $files)) {
|
||||
$this->addFile($files);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the specified filename or array of filenames to the list of files to
|
||||
* be minified. A filename can be in the form of a relative path or a URL
|
||||
* that resolves to the same site that hosts Minify.
|
||||
*
|
||||
* @param array|string $files filename or array of filenames
|
||||
* @see getFiles()
|
||||
* @see removeFile()
|
||||
*/
|
||||
public function addFile($files) {
|
||||
$files = @array_map(array($this, 'resolveFilePath'), (array) $files);
|
||||
$this->files = array_unique(array_merge($this->files, $files));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to serve the combined, minified files from the cache if possible.
|
||||
*
|
||||
* This method first checks the ETag value and If-Modified-Since timestamp
|
||||
* sent by the browser and exits with an HTTP "304 Not Modified" response if
|
||||
* the requested files haven't changed since they were last sent to the
|
||||
* client.
|
||||
*
|
||||
* If the browser hasn't cached the content, we check to see if it's been
|
||||
* cached on the server and, if so, we send the cached content and exit.
|
||||
*
|
||||
* If neither the client nor the server has the content in its cache, we don't
|
||||
* do anything.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function browserCache() {
|
||||
$hash = $this->getHash();
|
||||
$lastModified = $this->getLastModified();
|
||||
|
||||
$lastModifiedGMT = gmdate('D, d M Y H:i:s', $lastModified).' GMT';
|
||||
|
||||
// Check/set the ETag.
|
||||
$etag = $hash.'_'.$lastModified;
|
||||
|
||||
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
|
||||
if (strpos($_SERVER['HTTP_IF_NONE_MATCH'], $etag) !== false) {
|
||||
header("Last-Modified: $lastModifiedGMT", true, 304);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
header('ETag: "'.$etag.'"');
|
||||
|
||||
// Check If-Modified-Since.
|
||||
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
|
||||
if ($lastModified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
|
||||
header("Last-Modified: $lastModifiedGMT", true, 304);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
header("Last-Modified: $lastModifiedGMT");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines and returns the contents of all files that have been added with
|
||||
* addFile() or via this class's constructor.
|
||||
*
|
||||
* If MINIFY_USE_CACHE is true, the content will be returned from the server's
|
||||
* cache if the cache is up to date; otherwise the new content will be saved
|
||||
* to the cache for future use.
|
||||
*
|
||||
* @param bool $minify minify the combined contents before returning them
|
||||
* @return string combined file contents
|
||||
*/
|
||||
public function combine($minify = true) {
|
||||
// Return contents from server cache if possible.
|
||||
if (MINIFY_USE_CACHE) {
|
||||
if ($cacheResult = $this->serverCache(true)) {
|
||||
return $cacheResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Combine contents.
|
||||
$combined = array();
|
||||
|
||||
foreach($this->files as $file) {
|
||||
if ($this->type === self::TYPE_CSS && MINIFY_REWRITE_CSS_URLS) {
|
||||
// Rewrite relative CSS URLs.
|
||||
$combined[] = self::rewriteCSSUrls(file_get_contents($file),
|
||||
dirname($file));
|
||||
}
|
||||
else {
|
||||
$combined[] = file_get_contents($file);
|
||||
}
|
||||
}
|
||||
|
||||
$combined = $minify ? self::minify(implode("\n", $combined), $this->type) :
|
||||
implode("\n", $combined);
|
||||
|
||||
// Save combined contents to the cache.
|
||||
if (MINIFY_USE_CACHE) {
|
||||
$cacheFile = MINIFY_CACHE_DIR.'/minify_'.$this->getHash();
|
||||
@file_put_contents($cacheFile, $combined, LOCK_EX);
|
||||
}
|
||||
|
||||
return $combined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of absolute pathnames of all files that have been added with
|
||||
* addFile() or via this class's constructor.
|
||||
*
|
||||
* @return array array of absolute pathnames
|
||||
* @see addFile()
|
||||
* @see removeFile()
|
||||
*/
|
||||
public function getFiles() {
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the MD5 hash of the concatenated filenames from the list of files to
|
||||
* be minified.
|
||||
*/
|
||||
public function getHash() {
|
||||
return hash('md5', implode('', $this->files));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp of the most recently modified file.
|
||||
*
|
||||
* @return int timestamp
|
||||
*/
|
||||
public function getLastModified() {
|
||||
$lastModified = 0;
|
||||
|
||||
// Get the timestamp of the most recently modified file.
|
||||
foreach($this->files as $file) {
|
||||
$modified = filemtime($file);
|
||||
|
||||
if ($modified !== false && $modified > $lastModified) {
|
||||
$lastModified = $modified;
|
||||
}
|
||||
}
|
||||
|
||||
return $lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified filename or array of filenames from the list of files
|
||||
* to be minified.
|
||||
*
|
||||
* @param array|string $files filename or array of filenames
|
||||
* @see addFile()
|
||||
* @see getFiles()
|
||||
*/
|
||||
public function removeFile($files) {
|
||||
$files = @array_map(array($this, 'resolveFilePath'), (array) $files);
|
||||
$this->files = array_diff($this->files, $files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to serve the combined, minified files from the server's disk-based
|
||||
* cache if possible.
|
||||
*
|
||||
* @param bool $return return cached content as a string instead of outputting
|
||||
* it to the client
|
||||
* @return bool|string
|
||||
*/
|
||||
public function serverCache($return = false) {
|
||||
$cacheFile = MINIFY_CACHE_DIR.'/minify_'.$this->getHash();
|
||||
$lastModified = $this->getLastModified();
|
||||
|
||||
if (is_file($cacheFile) && $lastModified <= filemtime($cacheFile)) {
|
||||
if ($return) {
|
||||
return file_get_contents($cacheFile);
|
||||
}
|
||||
else {
|
||||
echo file_get_contents($cacheFile);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// -- Protected Instance Methods ---------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the canonicalized absolute pathname to the specified file or local
|
||||
* URL.
|
||||
*
|
||||
* @param string $file relative file path
|
||||
* @return string canonicalized absolute pathname
|
||||
*/
|
||||
protected function resolveFilePath($file) {
|
||||
// Is this a URL?
|
||||
if (preg_match('/^https?:\/\//i', $file)) {
|
||||
if (!$parsedUrl = parse_url($file)) {
|
||||
throw new MinifyInvalidUrlException("Invalid URL: $file");
|
||||
}
|
||||
|
||||
// Does the server name match the local server name?
|
||||
if (!isset($parsedUrl['host']) ||
|
||||
$parsedUrl['host'] != $_SERVER['SERVER_NAME']) {
|
||||
throw new MinifyInvalidUrlException('Non-local URL not supported: '.
|
||||
$file);
|
||||
}
|
||||
|
||||
// Get the file's absolute path.
|
||||
$filepath = realpath(MINIFY_BASE_DIR.$parsedUrl['path']);
|
||||
}
|
||||
else {
|
||||
// Get the file's absolute path.
|
||||
$filepath = realpath(MINIFY_BASE_DIR.'/'.$file);
|
||||
}
|
||||
|
||||
// Ensure that the file exists, that the path is under the base directory,
|
||||
// that the file's extension is either '.css' or '.js', and that the file is
|
||||
// actually readable.
|
||||
if (!$filepath ||
|
||||
!is_file($filepath) ||
|
||||
!is_readable($filepath) ||
|
||||
!preg_match('/^'.preg_quote(MINIFY_BASE_DIR, '/').'/', $filepath) ||
|
||||
!preg_match('/\.(?:css|js)$/iD', $filepath)) {
|
||||
|
||||
// Even when the file exists, we still throw a
|
||||
// MinifyFileNotFoundException in order to try to prevent an information
|
||||
// disclosure vulnerability.
|
||||
throw new MinifyFileNotFoundException("File not found: $file");
|
||||
}
|
||||
|
||||
return $filepath;
|
||||
}
|
||||
}
|
||||
|
||||
// -- Exception Classes --------------------------------------------------------
|
||||
class MinifyException extends Exception {}
|
||||
class MinifyFileNotFoundException extends MinifyException {}
|
||||
class MinifyInvalidArgumentException extends MinifyException {}
|
||||
class MinifyInvalidUrlException extends MinifyException {}
|
||||
|
||||
// -- Global Scope -------------------------------------------------------------
|
||||
if (realpath(__FILE__) == realpath($_SERVER['SCRIPT_FILENAME'])) {
|
||||
Minify::handleRequest();
|
||||
}
|
||||
?>
|
27
test/_inc.php
Normal file
27
test/_inc.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
ini_set('display_errors', 1);
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$thisDir = dirname(__FILE__);
|
||||
|
||||
/**
|
||||
* pTest - PHP Unit Tester
|
||||
* @param mixed $test Condition to test, evaluated as boolean
|
||||
* @param string $message Descriptive message to output upon test
|
||||
* @url http://www.sitepoint.com/blogs/2007/08/13/ptest-php-unit-tester-in-9-lines-of-code/
|
||||
*/
|
||||
function assertTrue($test, $message)
|
||||
{
|
||||
static $count;
|
||||
if (!isset($count)) $count = array('pass'=>0, 'fail'=>0, 'total'=>0);
|
||||
|
||||
$mode = $test ? 'pass' : 'fail';
|
||||
printf("%s: %s (%d of %d tests run so far have %sed)\n",
|
||||
strtoupper($mode), $message, ++$count[$mode], ++$count['total'], $mode);
|
||||
|
||||
return (bool)$test;
|
||||
}
|
||||
|
||||
?>
|
3
test/css/caio.css
Normal file
3
test/css/caio.css
Normal file
@@ -0,0 +1,3 @@
|
||||
/*/*/ a{}
|
||||
.foo {color:red}
|
||||
/* blah */
|
1
test/css/caio.min.css
vendored
Normal file
1
test/css/caio.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/*/*/a{}.foo{color:red}/**/
|
6
test/css/comments.css
Normal file
6
test/css/comments.css
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
/* block comments get removed */
|
||||
|
||||
/* comments containing the word "copyright" are left in, though */
|
||||
|
||||
/* but all other comments are removed */
|
1
test/css/comments.min
Normal file
1
test/css/comments.min
Normal file
@@ -0,0 +1 @@
|
||||
/* comments containing the word "copyright" are left in, though */
|
1
test/css/comments.min.css
vendored
Normal file
1
test/css/comments.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/* comments containing the word "copyright" are left in, though */
|
31
test/css/hacks.css
Normal file
31
test/css/hacks.css
Normal file
@@ -0,0 +1,31 @@
|
||||
/* hide from ie5/mac \*/ a{}
|
||||
.foo {color:red}
|
||||
/* necessary comment */
|
||||
|
||||
/* comment */
|
||||
|
||||
/* feed to ie5/mac \*//*/
|
||||
@import "ie5mac.css";
|
||||
/* necessary comment */
|
||||
|
||||
/* comment */
|
||||
|
||||
/*/ hide from nav4 */
|
||||
.foo {display:block;}
|
||||
/* necessary comment */
|
||||
|
||||
/* comment */
|
||||
|
||||
/*/ feed to nav *//*/
|
||||
.foo {display:crazy;}
|
||||
/* necessary comment */
|
||||
|
||||
/* comment */
|
||||
|
||||
div {
|
||||
width: 140px;
|
||||
width/* */:/**/100px;
|
||||
width: /**/100px;
|
||||
}
|
||||
|
||||
html>/**/body {}
|
1
test/css/hacks.min
Normal file
1
test/css/hacks.min
Normal file
@@ -0,0 +1 @@
|
||||
/*\*/a{}.foo{color:red}/**//*\*//*/@import "ie5mac.css";/**//*/*/.foo{display:block}/**//*/*//*/.foo{display:crazy}/**/div{width:140px;width/**/:/**/100px;width:/**/100px}html>/**/body{}
|
1
test/css/hacks.min.css
vendored
Normal file
1
test/css/hacks.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/*\*/a{}.foo{color:red}/**//*\*//*/@import "ie5mac.css";/**//*/*/.foo{display:block}/**//*/*//*/.foo{display:crazy}/**/div{width:140px;width/**/:/**/100px;width:/**/100px}html>/**/body{}
|
9
test/css/paths.css
Normal file
9
test/css/paths.css
Normal file
@@ -0,0 +1,9 @@
|
||||
@import "foo.css";
|
||||
@import 'bar/foo.css';
|
||||
@import '/css/foo.css';
|
||||
@import 'http://foo.com/css/foo.css';
|
||||
@import url(./foo.css);
|
||||
@import url("/css/foo.css");
|
||||
@import url(/css2/foo.css);
|
||||
foo {background:url('bar/foo.png')}
|
||||
foo {background:url('http://foo.com/css/foo.css');}
|
1
test/css/paths.min
Normal file
1
test/css/paths.min
Normal file
@@ -0,0 +1 @@
|
||||
@import "../foo.css";@import '../bar/foo.css';@import '/css/foo.css';@import 'http://foo.com/css/foo.css';@import url(.././foo.css);@import url("/css/foo.css");@import url(/css2/foo.css);foo{background:url('../bar/foo.png')}foo{background:url('http://foo.com/css/foo.css')}
|
1
test/css/paths.min.css
vendored
Normal file
1
test/css/paths.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import "../foo.css";@import '../bar/foo.css';@import '/css/foo.css';@import 'http://foo.com/css/foo.css';@import url(.././foo.css);@import url("/css/foo.css");@import url(/css2/foo.css);foo{background:url('../bar/foo.png')}foo{background:url('http://foo.com/css/foo.css')}
|
1
test/css/readme.txt
Normal file
1
test/css/readme.txt
Normal file
@@ -0,0 +1 @@
|
||||
Test suite from http://search.cpan.org/~gtermars/CSS-Minifier-XS/
|
21
test/css/styles.css
Normal file
21
test/css/styles.css
Normal file
@@ -0,0 +1,21 @@
|
||||
/* some CSS to try to exercise things in general */
|
||||
|
||||
@import url( more.css );
|
||||
|
||||
body, td, th {
|
||||
font-family: Verdana, "Bitstream Vera Sans", sans-serif;
|
||||
|
||||
font-size : 12px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
margin-left: 20%;
|
||||
}
|
||||
#main-nav {
|
||||
background-color: red;
|
||||
border: 1px solid #00ff77;
|
||||
}
|
||||
div#content h1 + p {
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
1
test/css/styles.min
Normal file
1
test/css/styles.min
Normal file
@@ -0,0 +1 @@
|
||||
@import url(more.css);body,td,th{font-family:Verdana, "Bitstream Vera Sans",sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px solid #0f7}div#content h1+p{padding-top:0;margin-top:0}
|
1
test/css/styles.min.css
vendored
Normal file
1
test/css/styles.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import url(more.css);body,td,th{font-family:Verdana, "Bitstream Vera Sans",sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px solid #0f7}div#content h1+p{padding-top:0;margin-top:0}
|
434
test/css/subsilver.css
Normal file
434
test/css/subsilver.css
Normal file
@@ -0,0 +1,434 @@
|
||||
/* Based on the original Style Sheet for the fisubsilver v2 Theme for phpBB version 2+
|
||||
Edited by Daz - http://www.forumimages.com - last updated 26-06-03 */
|
||||
/* The content of the posts (body of text) */
|
||||
/* General page style */
|
||||
|
||||
|
||||
|
||||
/* begin suggest post */
|
||||
.float-l{
|
||||
float: left;
|
||||
}
|
||||
|
||||
.form-suggest{
|
||||
height:200px;
|
||||
background:#DEE2D0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.form-input input{
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.hide{
|
||||
display:none;
|
||||
}
|
||||
|
||||
.form-input textarea{
|
||||
font-size: 11px;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.form-label{
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
line-height: 25px;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
width: 100px;
|
||||
color: #39738F;
|
||||
}
|
||||
|
||||
.font-9{
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.form-topic{
|
||||
font-weight:bold;
|
||||
|
||||
}
|
||||
|
||||
.form-error{
|
||||
color:red;
|
||||
}
|
||||
|
||||
.inline{
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.space-10{
|
||||
clear: both;
|
||||
font-size: 10px;
|
||||
height: 10px;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.suggest-success{
|
||||
color:green;
|
||||
padding-left:10px;
|
||||
font-size:11px;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
.top{
|
||||
vertical-align: top;
|
||||
}
|
||||
/* end suggest post */
|
||||
|
||||
table td{
|
||||
padding:3px;
|
||||
}
|
||||
|
||||
a:link,a:active,a:visited,a.postlink{
|
||||
color: #006699;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover{
|
||||
color: #DD6900;
|
||||
}
|
||||
|
||||
a.admin:hover,a.mod:hover{
|
||||
color: #DD6900;
|
||||
}
|
||||
|
||||
a.but,a.but:hover,a.but:visited{
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.topictitle:visited{
|
||||
color: #5493B4;
|
||||
}
|
||||
|
||||
a.topictitle:hover{
|
||||
color: #DD6900;
|
||||
}
|
||||
|
||||
|
||||
|
||||
body{
|
||||
color: #000000;
|
||||
font: 11px Verdana,Arial,Helvetica,sans-serif;
|
||||
margin: 0 10px 10px 10px;
|
||||
padding: 0;
|
||||
overflow:auto;
|
||||
}
|
||||
|
||||
/* General font families for common tags */
|
||||
font,th,td,p{
|
||||
font: 12px Verdana,Arial,Helvetica,sans-serif;
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
form{
|
||||
display: inline;
|
||||
}
|
||||
|
||||
hr{
|
||||
border: 0px solid #FFFFFF;
|
||||
border-top-width: 1px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
/* Gets rid of the need for border="0" on hyperlinked images */
|
||||
img{
|
||||
border: 0 solid;
|
||||
}
|
||||
|
||||
input{
|
||||
font: 11px Verdana,Arial,Helvetica,sans-serif;
|
||||
}
|
||||
|
||||
input.button,input.liteoption,.fakebut{
|
||||
background: #FAFAFA;
|
||||
border: 1px solid #000000;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
input.catbutton{
|
||||
background: #FAFAFA;
|
||||
border: 1px solid #000000;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
input.mainoption{
|
||||
background: #FAFAFA;
|
||||
border: 1px solid #000000;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input.post,textarea.post{
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #000000;
|
||||
font: 11px Verdana,Arial,Helvetica,sans-serif;
|
||||
padding-bottom: 2px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
select{
|
||||
background: #FFFFFF;
|
||||
font: 11px Verdana,Arial,Helvetica,sans-serif;
|
||||
}
|
||||
|
||||
table{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td{
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Category gradients*/
|
||||
td.cat{
|
||||
background-color: #C2C6BA;
|
||||
font-weight: bold;
|
||||
height: 20px;
|
||||
letter-spacing: 1px;
|
||||
text-indent: 4px;
|
||||
}
|
||||
|
||||
td.genmed,.genmed{
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* This is for the table cell above the Topics,Post & Last posts on the index.php */
|
||||
td.rowpic{
|
||||
background: #C2C6BA;
|
||||
}
|
||||
|
||||
td.spacerow{
|
||||
background: #E5E6E2;
|
||||
}
|
||||
|
||||
/* Table Header cells */
|
||||
th{
|
||||
background-color: #FADD31;
|
||||
background-image: url(images/cellpic3.gif);
|
||||
background-repeat: repeat-x;
|
||||
color: #68685E;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
line-height:16px;
|
||||
height: 16px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Admin & Moderator Colours MODification */
|
||||
.admin,.mod{
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.admin,a.admin,a.admin:visited{
|
||||
color: #FFA34F;
|
||||
}
|
||||
|
||||
/* This is the border line & background colour round the entire page */
|
||||
.bodyline{
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #98AAB1;
|
||||
}
|
||||
|
||||
.center{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
.code{
|
||||
background: #FAFAFA;
|
||||
border: 1px solid #D1D7DC;
|
||||
color: #006600;
|
||||
font: 12px Courier,"Courier New",sans-serif;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* This is for the error messages that pop up */
|
||||
.errorline{
|
||||
background: #E5E6E2;
|
||||
border: 1px solid #8F8B8B;
|
||||
color:#D92A2A;
|
||||
}
|
||||
|
||||
.explaintitle{
|
||||
color: #5C81B1;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* This is the outline round the main forum tables */
|
||||
.forumline{
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
/* General text */
|
||||
.gensmall{
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.h1-font{
|
||||
color: #006699;
|
||||
display: inline;
|
||||
font: bold 13px Verdana, Arial, Helvetica, sans-serif;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.h2-font{
|
||||
display: inline;
|
||||
font-family: Verdana, Arial, Helvetica, sans-serif;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.height1{
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.height22{
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.height25{
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.height28{
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.height30{
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.height40{
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* This is the line in the posting page which shows the rollover
|
||||
help line. Colour value in row2 */
|
||||
.helpline{
|
||||
border: 0 solid;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.imgfolder{
|
||||
margin: 1px 4px 1px 4px;
|
||||
}
|
||||
|
||||
.imgspace{
|
||||
margin-left: 1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
/* Specify the space around images */
|
||||
.imgtopic,.imgicon{
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.left{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* The largest text used in the index page title and toptic title etc. */
|
||||
.maintitle,h1,h2{
|
||||
color: #5C81B1;
|
||||
font: bold 20px/120% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.maxwidth{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mod,a.mod,a.mod:visited{
|
||||
color: #006600;
|
||||
}
|
||||
|
||||
/* Name of poster in viewmsg.php and viewtopic.php and other places */
|
||||
.name{
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Used for the navigation text,(Page 1,2,3 etc) and the navigation bar when in a forum */
|
||||
.nav{
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nowrap{
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.postbody{
|
||||
font-size: 12px;
|
||||
line-height: 125%;
|
||||
}
|
||||
|
||||
.postbody a{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Location,number of posts,post date etc */
|
||||
.postdetails{
|
||||
color: #00396A;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Quote blocks */
|
||||
.quote{
|
||||
background: #F3F3EF;
|
||||
border: 1px solid #C2C6BA;
|
||||
color: #006699;
|
||||
font-size: 11px;
|
||||
line-height: 125%;
|
||||
}
|
||||
|
||||
.right{
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Main table cell colours and backgrounds */
|
||||
.row1{
|
||||
background: #F0F0EB;
|
||||
}
|
||||
|
||||
.row2,.helpline{
|
||||
background: #E5E6E2;
|
||||
}
|
||||
|
||||
.row3{
|
||||
background: #DBDBD4;
|
||||
}
|
||||
|
||||
.subtitle,h2{
|
||||
font: bold 18px/180% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* titles for the topics:could specify viewed link colour too */
|
||||
.topictitle {
|
||||
color: #000000;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.underline{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top{
|
||||
vertical-align:top;
|
||||
}
|
||||
|
||||
.image-hspace{
|
||||
margin-right:3px;
|
||||
}
|
||||
|
||||
.clear{
|
||||
clear:both;
|
||||
}
|
1
test/css/subsilver.min
Normal file
1
test/css/subsilver.min
Normal file
@@ -0,0 +1 @@
|
||||
.float-l{float:left}.form-suggest{height:200px;background:#DEE2D0;vertical-align:top}.form-input input{font-size:10px}.hide{display:none}.form-input textarea{font-size:11px;width:350px}.form-label{font-size:10px;font-weight:bold;line-height:25px;padding-right:10px;text-align:right;width:100px;color: #39738F}.font-9{font-size:9px}.form-topic{font-weight:bold}.form-error{color:red}.inline{display:inline}.space-10{clear:both;font-size:10px;height:10px;line-height:10px}.suggest-success{color:green;padding-left:10px;font-size:11px;font-weight:bold}.top{vertical-align:top}table td{padding:3px}a:link,a:active,a:visited,a.postlink{color: #069;text-decoration:none}a:hover{color: #DD6900}a.admin:hover,a.mod:hover{color: #DD6900}a.but,a.but:hover,a.but:visited{color: #000;text-decoration:none}a.topictitle:visited{color: #5493B4}a.topictitle:hover{color: #DD6900}body{color: #000;font:11px Verdana,Arial,Helvetica,sans-serif;margin:0 10px 10px 10px;padding:0;overflow:auto}font,th,td,p{font:12px Verdana,Arial,Helvetica,sans-serif}form{display:inline}hr{border:0px solid #FFF;border-top-width:1px;height:0px}img{border:0 solid}input{font:11px Verdana,Arial,Helvetica,sans-serif}input.button,input.liteoption,.fakebut{background: #FAFAFA;border:1px solid #000;font-size:11px}input.catbutton{background: #FAFAFA;border:1px solid #000;font-size:10px}input.mainoption{background: #FAFAFA;border:1px solid #000;font-size:11px;font-weight:bold}input.post,textarea.post{background: #FFF;border:1px solid #000;font:11px Verdana,Arial,Helvetica,sans-serif;padding-bottom:2px;padding-left:2px}select{background: #FFF;font:11px Verdana,Arial,Helvetica,sans-serif}table{text-align:left}td{vertical-align:middle}td.cat{background-color: #C2C6BA;font-weight:bold;height:20px;letter-spacing:1px;text-indent:4px}td.genmed,.genmed{font-size:11px}td.rowpic{background: #C2C6BA}td.spacerow{background: #E5E6E2}th{background-color: #FADD31;background-image:url(images/cellpic3.gif);background-repeat:repeat-x;color: #68685E;font-size:11px;font-weight:bold;line-height:16px;height:16px;padding-left:8px;padding-right:8px;text-align:center;white-space:nowrap}.admin,.mod{font-size:11px;font-weight:bold}.admin,a.admin,a.admin:visited{color: #FFA34F}.bodyline{background: #FFF;border:1px solid #98AAB1}.center{text-align:center}.code{background: #FAFAFA;border:1px solid #D1D7DC;color: #060;font:12px Courier,"Courier New",sans-serif;padding:5px}.errorline{background: #E5E6E2;border:1px solid #8F8B8B;color:#D92A2A}.explaintitle{color: #5C81B1;font-size:11px;font-weight:bold}.forumline{background: #FFF}.gensmall{font-size:10px}.h1-font{color: #069;display:inline;font:bold 13px Verdana,Arial,Helvetica,sans-serif;margin:0;text-decoration:none}.h2-font{display:inline;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:11px}.height1{height:1px}.height22{height:22px}.height25{height:25px}.height28{height:28px}.height30{height:30px}.height40{height:40px}.helpline{border:0 solid;font-size:10px}.imgfolder{margin:1px 4px 1px 4px}.imgspace{margin-left:1px;margin-right:2px}.imgtopic,.imgicon{margin-left:3px}.left{text-align:left}.maintitle,h1,h2{color: #5C81B1;font:bold 20px/120% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;text-decoration:none}.maxwidth{width:100%}.mod,a.mod,a.mod:visited{color: #060}.name{font-size:11px;font-weight:bold}.nav{font-size:11px;font-weight:bold}.nowrap{white-space:nowrap}.postbody{font-size:12px;line-height:125%}.postbody a{text-decoration:underline}.postdetails{color: #00396A;font-size:10px}.quote{background: #F3F3EF;border:1px solid #C2C6BA;color: #069;font-size:11px;line-height:125%}.right{text-align:right}.row1{background: #F0F0EB}.row2,.helpline{background: #E5E6E2}.row3{background: #DBDBD4}.subtitle,h2{font:bold 18px/180% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;text-decoration:none}.topictitle{color: #000;font-size:11px;font-weight:bold}.underline{text-decoration:underline}.top{vertical-align:top}.image-hspace{margin-right:3px}.clear{clear:both}
|
1
test/css/subsilver.min.css
vendored
Normal file
1
test/css/subsilver.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.float-l{float:left}.form-suggest{height:200px;background:#DEE2D0;vertical-align:top}.form-input input{font-size:10px}.hide{display:none}.form-input textarea{font-size:11px;width:350px}.form-label{font-size:10px;font-weight:bold;line-height:25px;padding-right:10px;text-align:right;width:100px;color: #39738F}.font-9{font-size:9px}.form-topic{font-weight:bold}.form-error{color:red}.inline{display:inline}.space-10{clear:both;font-size:10px;height:10px;line-height:10px}.suggest-success{color:green;padding-left:10px;font-size:11px;font-weight:bold}.top{vertical-align:top}table td{padding:3px}a:link,a:active,a:visited,a.postlink{color: #069;text-decoration:none}a:hover{color: #DD6900}a.admin:hover,a.mod:hover{color: #DD6900}a.but,a.but:hover,a.but:visited{color: #000;text-decoration:none}a.topictitle:visited{color: #5493B4}a.topictitle:hover{color: #DD6900}body{color: #000;font:11px Verdana,Arial,Helvetica,sans-serif;margin:0 10px 10px 10px;padding:0;overflow:auto}font,th,td,p{font:12px Verdana,Arial,Helvetica,sans-serif}form{display:inline}hr{border:0px solid #FFF;border-top-width:1px;height:0px}img{border:0 solid}input{font:11px Verdana,Arial,Helvetica,sans-serif}input.button,input.liteoption,.fakebut{background: #FAFAFA;border:1px solid #000;font-size:11px}input.catbutton{background: #FAFAFA;border:1px solid #000;font-size:10px}input.mainoption{background: #FAFAFA;border:1px solid #000;font-size:11px;font-weight:bold}input.post,textarea.post{background: #FFF;border:1px solid #000;font:11px Verdana,Arial,Helvetica,sans-serif;padding-bottom:2px;padding-left:2px}select{background: #FFF;font:11px Verdana,Arial,Helvetica,sans-serif}table{text-align:left}td{vertical-align:middle}td.cat{background-color: #C2C6BA;font-weight:bold;height:20px;letter-spacing:1px;text-indent:4px}td.genmed,.genmed{font-size:11px}td.rowpic{background: #C2C6BA}td.spacerow{background: #E5E6E2}th{background-color: #FADD31;background-image:url(images/cellpic3.gif);background-repeat:repeat-x;color: #68685E;font-size:11px;font-weight:bold;line-height:16px;height:16px;padding-left:8px;padding-right:8px;text-align:center;white-space:nowrap}.admin,.mod{font-size:11px;font-weight:bold}.admin,a.admin,a.admin:visited{color: #FFA34F}.bodyline{background: #FFF;border:1px solid #98AAB1}.center{text-align:center}.code{background: #FAFAFA;border:1px solid #D1D7DC;color: #060;font:12px Courier,"Courier New",sans-serif;padding:5px}.errorline{background: #E5E6E2;border:1px solid #8F8B8B;color:#D92A2A}.explaintitle{color: #5C81B1;font-size:11px;font-weight:bold}.forumline{background: #FFF}.gensmall{font-size:10px}.h1-font{color: #069;display:inline;font:bold 13px Verdana,Arial,Helvetica,sans-serif;margin:0;text-decoration:none}.h2-font{display:inline;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:11px}.height1{height:1px}.height22{height:22px}.height25{height:25px}.height28{height:28px}.height30{height:30px}.height40{height:40px}.helpline{border:0 solid;font-size:10px}.imgfolder{margin:1px 4px 1px 4px}.imgspace{margin-left:1px;margin-right:2px}.imgtopic,.imgicon{margin-left:3px}.left{text-align:left}.maintitle,h1,h2{color: #5C81B1;font:bold 20px/120% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;text-decoration:none}.maxwidth{width:100%}.mod,a.mod,a.mod:visited{color: #060}.name{font-size:11px;font-weight:bold}.nav{font-size:11px;font-weight:bold}.nowrap{white-space:nowrap}.postbody{font-size:12px;line-height:125%}.postbody a{text-decoration:underline}.postdetails{color: #00396A;font-size:10px}.quote{background: #F3F3EF;border:1px solid #C2C6BA;color: #069;font-size:11px;line-height:125%}.right{text-align:right}.row1{background: #F0F0EB}.row2,.helpline{background: #E5E6E2}.row3{background: #DBDBD4}.subtitle,h2{font:bold 18px/180% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;text-decoration:none}.topictitle{color: #000;font-size:11px;font-weight:bold}.underline{text-decoration:underline}.top{vertical-align:top}.image-hspace{margin-right:3px}.clear{clear:both}
|
91
test/html/before.html
Normal file
91
test/html/before.html
Normal file
@@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
|
||||
<head>
|
||||
<!-- comments get removed -->
|
||||
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
|
||||
<meta name="author" content="Dave Shea" />
|
||||
<!-- also whitespace around block or undisplayed elements -->
|
||||
<meta name="keywords" content="design, css, cascading, style, sheets, xhtml, graphic design, w3c, web standards, visual, display" />
|
||||
<meta name="description" content="A demonstration of what can be accomplished visually through CSS-based design." />
|
||||
<meta name="robots" content="all" />
|
||||
<title>css Zen Garden: The Beauty in CSS Design</title>
|
||||
|
||||
<!-- to correct the unsightly Flash of Unstyled Content. http://www.bluerobot.com/web/css/fouc.asp -->
|
||||
<script type="text/javascript"><!--
|
||||
// js comment inside SCRIPT element
|
||||
var is = {
|
||||
ie: navigator.appName == 'Microsoft Internet Explorer',
|
||||
java: navigator.javaEnabled(),
|
||||
ns: navigator.appName == 'Netscape',
|
||||
ua: navigator.userAgent.toLowerCase(),
|
||||
version: parseFloat(navigator.appVersion.substr(21)) ||
|
||||
parseFloat(navigator.appVersion),
|
||||
win: navigator.platform == 'Win32'
|
||||
}
|
||||
is.mac = is.ua.indexOf('mac') >= 0;
|
||||
if (is.ua.indexOf('opera') >= 0) {
|
||||
is.ie = is.ns = false;
|
||||
is.opera = true;
|
||||
}
|
||||
if (is.ua.indexOf('gecko') >= 0) {
|
||||
is.ie = is.ns = false;
|
||||
is.gecko = true;
|
||||
}
|
||||
// --></script>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var i = 0;
|
||||
while (++i < 10)
|
||||
{
|
||||
// ...
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
/* <![CDATA[ */ i = 1; /* ]]> */
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
(i < 1); /* CDATA needed */
|
||||
</script>
|
||||
<!--[if IE 6]>
|
||||
<style type="text/css">
|
||||
/* copyright: you'll need CDATA for this -- < & */
|
||||
body {background:white;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css" title="currentStyle" media="screen">
|
||||
@import "/001/001.css";
|
||||
/*\*/ css hack {} /* */
|
||||
/* normal CSS comment */
|
||||
/*/*/ css hack {} /* */
|
||||
css hack {
|
||||
display/**/:/**/none;
|
||||
display:none;
|
||||
}
|
||||
</style>
|
||||
<link
|
||||
rel="Shortcut Icon"
|
||||
type="image/x-icon"
|
||||
href="http://www.csszengarden.com/favicon.ico" />
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title="RSS"
|
||||
href="http://www.csszengarden.com/zengarden.xml" />
|
||||
</head>
|
||||
<body id="css-zen-garden">
|
||||
<div id="container">
|
||||
<div id="pageHeader">
|
||||
<h1><span>css Zen Garden</span></h1>
|
||||
<h2><span>The Beauty of <acronym title="Cascading Style Sheets">CSS</acronym> Design</span></h2>
|
||||
</div>
|
||||
<pre>
|
||||
White space is important here!
|
||||
</pre>
|
||||
<div id="quickSummary">
|
||||
<p class="p1"><span>A demonstration of what can be accomplished visually through <acronym title="Cascading Style Sheets">CSS</acronym>-based design. Select any style sheet from the list to load it into this page.</span></p>
|
||||
<p class="p2"><span>Download the sample <a href="/zengarden-sample.html" title="This page's source HTML code, not to be modified.">html file</a> and <a href="/zengarden-sample.css" title="This page's sample CSS, the file you may modify.">css file</a></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
13
test/html/before.min.html
Normal file
13
test/html/before.min.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" ><head><meta http-equiv="content-type" content="text/html; charset=iso-8859-1" /><meta name="author" content="Dave Shea" /><meta name="keywords" content="design, css, cascading, style, sheets, xhtml, graphic design, w3c, web standards, visual, display" /><meta name="description" content="A demonstration of what can be accomplished visually through CSS-based design." /><meta name="robots" content="all" /><title>css Zen Garden: The Beauty in CSS Design</title><script type="text/javascript">var is={ie:navigator.appName=='Microsoft Internet Explorer',java:navigator.javaEnabled(),ns:navigator.appName=='Netscape',ua:navigator.userAgent.toLowerCase(),version:parseFloat(navigator.appVersion.substr(21))||parseFloat(navigator.appVersion),win:navigator.platform=='Win32'}
|
||||
is.mac=is.ua.indexOf('mac')>=0;if(is.ua.indexOf('opera')>=0){is.ie=is.ns=false;is.opera=true;}
|
||||
if(is.ua.indexOf('gecko')>=0){is.ie=is.ns=false;is.gecko=true;}</script><script type="text/javascript">/*<![CDATA[*/var i=0;while(++i<10)
|
||||
{}/*]]>*/</script><script type="text/javascript">i=1;</script><script type="text/javascript">/*<![CDATA[*/(i<1);/*]]>*/</script><!--[if IE 6]><style type="text/css">/*<![CDATA[*//* copyright: you'll need CDATA for this -- < & */body{background:white}/*]]>*/</style><![endif]--><style type="text/css" title="currentStyle" media="screen">@import "/001/001.css";/*\*/css hack{}/**//*/*/css hack{}/**/css hack{display/**/:/**/none;display:none}</style><link
|
||||
rel="Shortcut Icon"
|
||||
type="image/x-icon"
|
||||
href="http://www.csszengarden.com/favicon.ico" /><link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title="RSS"
|
||||
href="http://www.csszengarden.com/zengarden.xml" /></head><body id="css-zen-garden"><div id="container"><div id="pageHeader"><h1><span>css Zen Garden</span></h1><h2><span>The Beauty of <acronym title="Cascading Style Sheets">CSS</acronym> Design</span></h2></div><pre>
|
||||
White space is important here!
|
||||
</pre><div id="quickSummary"><p class="p1"><span>A demonstration of what can be accomplished visually through <acronym title="Cascading Style Sheets">CSS</acronym>-based design. Select any style sheet from the list to load it into this page.</span></p><p class="p2"><span>Download the sample <a href="/zengarden-sample.html" title="This page's source HTML code, not to be modified.">html file</a> and <a href="/zengarden-sample.css" title="This page's sample CSS, the file you may modify.">css file</a></span></p></div></div></body></html>
|
32
test/js/before.js
Normal file
32
test/js/before.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// is.js
|
||||
|
||||
// (c) 2001 Douglas Crockford
|
||||
// 2001 June 3
|
||||
|
||||
|
||||
// is
|
||||
|
||||
// The -is- object is used to identify the browser. Every browser edition
|
||||
// identifies itself, but there is no standard way of doing it, and some of
|
||||
// the identification is deceptive. This is because the authors of web
|
||||
// browsers are liars. For example, Microsoft's IE browsers claim to be
|
||||
// Mozilla 4. Netscape 6 claims to be version 5.
|
||||
|
||||
var is = {
|
||||
ie: navigator.appName == 'Microsoft Internet Explorer',
|
||||
java: navigator.javaEnabled(),
|
||||
ns: navigator.appName == 'Netscape',
|
||||
ua: navigator.userAgent.toLowerCase(),
|
||||
version: parseFloat(navigator.appVersion.substr(21)) ||
|
||||
parseFloat(navigator.appVersion),
|
||||
win: navigator.platform == 'Win32'
|
||||
}
|
||||
is.mac = is.ua.indexOf('mac') >= 0;
|
||||
if (is.ua.indexOf('opera') >= 0) {
|
||||
is.ie = is.ns = false;
|
||||
is.opera = true;
|
||||
}
|
||||
if (is.ua.indexOf('gecko') >= 0) {
|
||||
is.ie = is.ns = false;
|
||||
is.gecko = true;
|
||||
}
|
3
test/js/before.min.js
vendored
Normal file
3
test/js/before.min.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
var is={ie:navigator.appName=='Microsoft Internet Explorer',java:navigator.javaEnabled(),ns:navigator.appName=='Netscape',ua:navigator.userAgent.toLowerCase(),version:parseFloat(navigator.appVersion.substr(21))||parseFloat(navigator.appVersion),win:navigator.platform=='Win32'}
|
||||
is.mac=is.ua.indexOf('mac')>=0;if(is.ua.indexOf('opera')>=0){is.ie=is.ns=false;is.opera=true;}
|
||||
if(is.ua.indexOf('gecko')>=0){is.ie=is.ns=false;is.gecko=true;}
|
168
test/minify/QueryString.js
Normal file
168
test/minify/QueryString.js
Normal file
@@ -0,0 +1,168 @@
|
||||
var MrClay = window.MrClay || {};
|
||||
|
||||
/**
|
||||
* Simplified access to/manipulation of the query string
|
||||
*
|
||||
* Based on: http://adamv.com/dev/javascript/files/querystring.js
|
||||
* Design pattern: http://www.litotes.demon.co.uk/js_info/private_static.html#wConst
|
||||
*/
|
||||
MrClay.QueryString = function(){
|
||||
/**
|
||||
* @static
|
||||
* @private
|
||||
*/
|
||||
var parse = function(str) {
|
||||
var assignments = str.split('&')
|
||||
,obj = {}
|
||||
,propValue;
|
||||
for (var i = 0, l = assignments.length; i < l; ++i) {
|
||||
propValue = assignments[i].split('=');
|
||||
if (propValue.length > 2
|
||||
|| -1 != propValue[0].indexOf('+')
|
||||
|| propValue[0] == ''
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (propValue.length == 1) {
|
||||
propValue[1] = propValue[0];
|
||||
}
|
||||
obj[unescape(propValue[0])] = unescape(propValue[1].replace(/\+/g, ' '));
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor (MrClay.QueryString becomes this)
|
||||
*
|
||||
* @param mixed A window object, a query string, or empty (default current window)
|
||||
*/
|
||||
function construct_(spec) {
|
||||
spec = spec || window;
|
||||
if (typeof spec == 'object') {
|
||||
// get querystring from window
|
||||
this.window = spec;
|
||||
spec = spec.location.search.substr(1);
|
||||
} else {
|
||||
this.window = window;
|
||||
}
|
||||
this.vars = parse(spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the window
|
||||
*
|
||||
* @static
|
||||
* @public
|
||||
* @param object vars Specify querystring vars only if you wish to replace them
|
||||
* @param object window_ window to be reloaded (current window by default)
|
||||
*/
|
||||
construct_.reload = function(vars, window_) {
|
||||
window_ = window_ || window;
|
||||
vars = vars || (new MrClay.QueryString(window_)).vars;
|
||||
var l = window_.location
|
||||
,currUrl = l.href
|
||||
,s = MrClay.QueryString.toString(vars)
|
||||
,newUrl = l.protocol + '//' + l.hostname + l.pathname
|
||||
+ (s ? '?' + s : '') + l.hash;
|
||||
if (currUrl == newUrl) {
|
||||
l.reload();
|
||||
} else {
|
||||
l.assign(newUrl);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the value of a querystring var
|
||||
*
|
||||
* @static
|
||||
* @public
|
||||
* @param string key
|
||||
* @param mixed default_ value to return if key not found
|
||||
* @param object window_ window to check (current window by default)
|
||||
* @return mixed
|
||||
*/
|
||||
construct_.get = function(key, default_, window_) {
|
||||
window_ = window_ || window;
|
||||
return (new MrClay.QueryString(window_)).get(key, default_);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reload the page setting one or multiple querystring vars
|
||||
*
|
||||
* @static
|
||||
* @public
|
||||
* @param mixed key object of query vars/values, or a string key for a single
|
||||
* assignment
|
||||
* @param mixed null for multiple settings, the value to assign for single
|
||||
* @param object window_ window to reload (current window by default)
|
||||
*/
|
||||
construct_.set = function(key, value, window_) {
|
||||
window_ = window_ || window;
|
||||
(new MrClay.QueryString(window_)).set(key, value).reload();
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert an object of query vars/values to a querystring
|
||||
*
|
||||
* @static
|
||||
* @public
|
||||
* @param object query vars/values
|
||||
* @return string
|
||||
*/
|
||||
construct_.toString = function(vars) {
|
||||
var pieces = [];
|
||||
for (var prop in vars) {
|
||||
pieces.push(escape(prop) + '=' + escape(vars[prop]));
|
||||
}
|
||||
return pieces.join('&');
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
construct_.prototype.reload = function() {
|
||||
MrClay.QueryString.reload(this.vars, this.window);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
construct_.prototype.get = function(key, default_) {
|
||||
if (typeof default_ == 'undefined') {
|
||||
default_ = null;
|
||||
}
|
||||
return (this.vars[key] == null)
|
||||
? default_
|
||||
: this.vars[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
construct_.prototype.set = function(key, value) {
|
||||
var obj = {};
|
||||
if (typeof key == 'string') {
|
||||
obj[key] = value;
|
||||
} else {
|
||||
obj = key;
|
||||
}
|
||||
for (var prop in obj) {
|
||||
if (obj[prop] == null) {
|
||||
delete this.vars[prop];
|
||||
} else {
|
||||
this.vars[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
construct_.prototype.toString = function() {
|
||||
return QueryString.toString(this.vars);
|
||||
};
|
||||
|
||||
return construct_;
|
||||
}(); // define and execute
|
24
test/minify/email.js
Normal file
24
test/minify/email.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// http://mrclay.org/
|
||||
(function(){
|
||||
var
|
||||
reMailto = /^mailto:my_name_is_(\S+)_and_the_domain_is_(\S+)$/,
|
||||
reRemoveTitleIf = /^my name is/,
|
||||
oo = window.onload,
|
||||
fixHrefs = function() {
|
||||
var i = 0, l, m;
|
||||
while (l = document.links[i++]) {
|
||||
// require phrase in href property
|
||||
if (m = l.href.match(reMailto)) {
|
||||
l.href = 'mailto:' + m[1] + '@' + m[2];
|
||||
if (reRemoveTitleIf.test(l.title)) {
|
||||
l.title = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// end var
|
||||
window.onload = function() {
|
||||
oo && oo();
|
||||
fixHrefs();
|
||||
};
|
||||
})();
|
32
test/packer/before.js
Normal file
32
test/packer/before.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// is.js
|
||||
|
||||
// (c) 2001 Douglas Crockford
|
||||
// 2001 June 3
|
||||
|
||||
|
||||
// is
|
||||
|
||||
// The -is- object is used to identify the browser. Every browser edition
|
||||
// identifies itself, but there is no standard way of doing it, and some of
|
||||
// the identification is deceptive. This is because the authors of web
|
||||
// browsers are liars. For example, Microsoft's IE browsers claim to be
|
||||
// Mozilla 4. Netscape 6 claims to be version 5.
|
||||
|
||||
var is = {
|
||||
ie: navigator.appName == 'Microsoft Internet Explorer',
|
||||
java: navigator.javaEnabled(),
|
||||
ns: navigator.appName == 'Netscape',
|
||||
ua: navigator.userAgent.toLowerCase(),
|
||||
version: parseFloat(navigator.appVersion.substr(21)) ||
|
||||
parseFloat(navigator.appVersion),
|
||||
win: navigator.platform == 'Win32'
|
||||
}
|
||||
is.mac = is.ua.indexOf('mac') >= 0;
|
||||
if (is.ua.indexOf('opera') >= 0) {
|
||||
is.ie = is.ns = false;
|
||||
is.opera = true;
|
||||
}
|
||||
if (is.ua.indexOf('gecko') >= 0) {
|
||||
is.ie = is.ns = false;
|
||||
is.gecko = true;
|
||||
}
|
1
test/packer/before.min.js
vendored
Normal file
1
test/packer/before.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
eval(function(p,a,c,k,e,d){e=function(c){return c.toString(36)};if(!''.replace(/^/,String)){while(c--){d[c.toString(a)]=k[c]||c.toString(a)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('j 1={5:2.7==\'m k i\',g:2.h(),6:2.7==\'l\',3:2.u.s(),t:9(2.b.r(q))||9(2.b),n:2.o==\'p\'}1.a=1.3.4(\'a\')>=0;d(1.3.4(\'c\')>=0){1.5=1.6=e;1.c=8}d(1.3.4(\'f\')>=0){1.5=1.6=e;1.f=8}',31,31,'|is|navigator|ua|indexOf|ie|ns|appName|true|parseFloat|mac|appVersion|opera|if|false|gecko|java|javaEnabled|Explorer|var|Internet|Netscape|Microsoft|win|platform|Win32|21|substr|toLowerCase|version|userAgent'.split('|'),0,{}))
|
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
ini_set('display_errors', 'on');
|
||||
|
||||
define('MINIFY_REWRITE_CSS_URLS', false);
|
||||
|
||||
require '../minify.php';
|
||||
echo Minify::min(file_get_contents('test.html'), Minify::TYPE_HTML);
|
||||
?>
|
32
test/test_CSS.php
Normal file
32
test/test_CSS.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
require '_inc.php';
|
||||
|
||||
require_once $thisDir . '/../lib/Minify/CSS.php';
|
||||
|
||||
// build test file list
|
||||
$d = dir(dirname(__FILE__) . '/css');
|
||||
while (false !== ($entry = $d->read())) {
|
||||
if (preg_match('/^([\w\\-]+)\.css$/', $entry, $m)) {
|
||||
$list[] = $m[1];
|
||||
}
|
||||
}
|
||||
$d->close();
|
||||
|
||||
foreach ($list as $item) {
|
||||
|
||||
$options = ($item === 'paths')
|
||||
? array('prependRelativePath' => '../')
|
||||
: array();
|
||||
|
||||
$src = file_get_contents($thisDir . '/css/' . $item . '.css');
|
||||
$minExpected = file_get_contents($thisDir . '/css/' . $item . '.min.css');
|
||||
$minOutput = Minify_CSS::minify($src, $options);
|
||||
assertTrue($minExpected === $minOutput, 'Minify_CSS : ' . $item);
|
||||
|
||||
if ($minExpected !== $minOutput) {
|
||||
echo "\n---Source\n\n{$src}";
|
||||
echo "\n---Expected\n\n{$minExpected}";
|
||||
echo "\n---Output\n\n{$minOutput}\n\n\n\n";
|
||||
}
|
||||
}
|
||||
|
22
test/test_HTML.php
Normal file
22
test/test_HTML.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
require '_inc.php';
|
||||
|
||||
require_once $thisDir . '/../lib/Minify/HTML.php';
|
||||
require_once $thisDir . '/../lib/Minify/CSS.php';
|
||||
require_once $thisDir . '/../lib/Minify/Javascript.php';
|
||||
|
||||
$src = file_get_contents($thisDir . '/html/before.html');
|
||||
$minExpected = file_get_contents($thisDir . '/html/before.min.html');
|
||||
|
||||
$minOutput = Minify_HTML::minify($src, array(
|
||||
'cssMinifier' => array('Minify_CSS', 'minify')
|
||||
,'jsMinifier' => array('Minify_Javascript', 'minify')
|
||||
));
|
||||
|
||||
$passed = assertTrue($minExpected === $minOutput, 'Minify_HTML');
|
||||
|
||||
echo "\n---Output: " .strlen($minOutput). " bytes\n\n{$minOutput}";
|
||||
if (! $passed) {
|
||||
echo "\n\n\n\n---Expected: " .strlen($minExpected). " bytes\n\n{$minExpected}";
|
||||
}
|
||||
echo "\n\n---Source: " .strlen($src). " bytes\n\n{$src}";
|
16
test/test_Javascript.php
Normal file
16
test/test_Javascript.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require '_inc.php';
|
||||
|
||||
require_once $thisDir . '/../lib/Minify/Javascript.php';
|
||||
|
||||
$src = file_get_contents($thisDir . '/js/before.js');
|
||||
$minExpected = file_get_contents($thisDir . '/js/before.min.js');;
|
||||
$minOutput = Minify_Javascript::minify($src);
|
||||
|
||||
$passed = assertTrue($minExpected == $minOutput, 'Minify_Javascript converts before.js to before.min.js');
|
||||
|
||||
echo "\n---Output: " .strlen($minOutput). " bytes\n\n{$minOutput}";
|
||||
if (! $passed) {
|
||||
echo "\n\n\n\n---Expected: " .strlen($minExpected). " bytes\n\n{$minExpected}";
|
||||
}
|
||||
echo "\n\n---Source: " .strlen($src). " bytes\n\n{$src}";
|
29
test/test_Minify.php
Normal file
29
test/test_Minify.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Note: All Minify class are E_STRICT except for Cache_Lite_File.
|
||||
*/
|
||||
error_reporting(E_ALL);
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
// setup
|
||||
$cachePath = $_SERVER['DOCUMENT_ROOT'] . '/_cache/private';
|
||||
ini_set('include_path',
|
||||
'.'
|
||||
. PATH_SEPARATOR . '../lib'
|
||||
. PATH_SEPARATOR . ini_get('include_path')
|
||||
);
|
||||
|
||||
require 'Minify.php';
|
||||
|
||||
// cache output files on filesystem
|
||||
Minify::useServerCache($cachePath);
|
||||
|
||||
//Minify::$cacheUnencodedVersion = false;
|
||||
|
||||
// serve an array of files as one
|
||||
Minify::serve('Files', array(
|
||||
dirname(__FILE__) . '/minify/email.js'
|
||||
,dirname(__FILE__) . '/minify/QueryString.js'
|
||||
));
|
16
test/test_Packer.php
Normal file
16
test/test_Packer.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require '_inc.php';
|
||||
|
||||
require_once $thisDir . '/../lib/Minify/Packer.php';
|
||||
|
||||
$src = file_get_contents($thisDir . '/packer/before.js');
|
||||
$minExpected = file_get_contents($thisDir . '/packer/before.min.js');
|
||||
$minOutput = Minify_Packer::minify($src);
|
||||
|
||||
$passed = assertTrue($minExpected === $minOutput, 'Minify_Packer');
|
||||
|
||||
echo "\n---Output: " .strlen($minOutput). " bytes\n\n{$minOutput}";
|
||||
if (! $passed) {
|
||||
echo "\n\n\n\n---Expected: " .strlen($minExpected). " bytes\n\n{$minExpected}";
|
||||
}
|
||||
echo "\n\n---Source: " .strlen($src). " bytes\n\n{$src}";
|
Reference in New Issue
Block a user