1
0
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:
Steve Clay
2008-02-28 18:42:56 +00:00
parent 5527771acf
commit 0a939d4f91
65 changed files with 8081 additions and 608 deletions

View File

@@ -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
View File

@@ -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
View 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&amp;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&amp;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

File diff suppressed because it is too large Load Diff

50
examples/1/m.php Normal file
View 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
View File

@@ -0,0 +1,5 @@
$(function () {
$('#jsFail1').html('PASS');
});

19
examples/1/test.css Normal file
View 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
View 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
View 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;
}
}

View 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
View 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);
}
}

View 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);
?>

View 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);
?>

View 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());
?>

View 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
));
?>

View 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
&quot;public&quot; or &quot;private&quot;. 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;
}
?>

View 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
View 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

View 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
View 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
View 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})";
}
}
}

View 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';
}
}
}

View 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);
}
}

View 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);
}
}

View 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
View 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));
}
}

View File

@@ -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
View 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
View 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;
}

View File

@@ -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;
}
}
?>

View File

@@ -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
View 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
View File

@@ -0,0 +1,3 @@
/*/*/ a{}
.foo {color:red}
/* blah */

1
test/css/caio.min.css vendored Normal file
View File

@@ -0,0 +1 @@
/*/*/a{}.foo{color:red}/**/

6
test/css/comments.css Normal file
View 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
View File

@@ -0,0 +1 @@
/* comments containing the word "copyright" are left in, though */

1
test/css/comments.min.css vendored Normal file
View File

@@ -0,0 +1 @@
/* comments containing the word "copyright" are left in, though */

31
test/css/hacks.css Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
Test suite from http://search.cpan.org/~gtermars/CSS-Minifier-XS/

21
test/css/styles.css Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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,{}))

View File

@@ -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
View 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
View 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
View 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
View 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
View 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}";