510 lines
17 KiB
PHP
510 lines
17 KiB
PHP
|
<?php
|
||
|
|
||
|
/**
|
||
|
* CodeIgniter Port of 'Minify_CSS' CSS Compression Library from Minify ( http://code.google.com/p/minify/ )
|
||
|
*
|
||
|
* Minifies CSS, preserving comments as directed. Note: This port moves the Minify_CommentPreserver
|
||
|
* class into this file, and adds a simple meta class to access the normal minify_css class.
|
||
|
*
|
||
|
* @author Tony Dewan <tony@tonydewan.com>
|
||
|
* @version 1.1 (2009-01-28)
|
||
|
* @license http://www.opensource.org/licenses/bsd-license.php BSD license, as per the original Minify_CSS class
|
||
|
*
|
||
|
**/
|
||
|
|
||
|
/*
|
||
|
===============================================================================================
|
||
|
USAGE
|
||
|
===============================================================================================
|
||
|
|
||
|
Load the library as normal:
|
||
|
-----------------------------------------------------------------------------------------------
|
||
|
$this->load->library('cssmin');
|
||
|
-----------------------------------------------------------------------------------------------
|
||
|
|
||
|
Minify a string like so:
|
||
|
-----------------------------------------------------------------------------------------------
|
||
|
$this->cssmin->minify( file_get_contents('styles.css') );
|
||
|
-----------------------------------------------------------------------------------------------
|
||
|
|
||
|
|
||
|
There are two options:
|
||
|
|
||
|
'preserveComments'
|
||
|
Boolean flag for preserving comments. Only comments starting with /*! are preserved.
|
||
|
Defaults to true.
|
||
|
|
||
|
'relativePath'
|
||
|
String that will be prepended to all relative URIs in import/url declarations.
|
||
|
Defaults to null.
|
||
|
|
||
|
|
||
|
The options can either be set globally using the config function:
|
||
|
-----------------------------------------------------------------------------------------------
|
||
|
$cssmin_options = array(
|
||
|
'preserveComments'=> TRUE,
|
||
|
'relativePath'=> 'http://www.example.com/styles/images/'
|
||
|
);
|
||
|
|
||
|
$this->cssmin->config($cssmin_options);
|
||
|
-----------------------------------------------------------------------------------------------
|
||
|
|
||
|
|
||
|
Or on individual calls to the minify function:
|
||
|
-----------------------------------------------------------------------------------------------
|
||
|
$this->cssmin->minify( $string, FALSE, $path );
|
||
|
-----------------------------------------------------------------------------------------------
|
||
|
|
||
|
NOTE: Global settings override settings in individual calls.
|
||
|
===============================================================================================
|
||
|
*/
|
||
|
|
||
|
class cssmin {
|
||
|
|
||
|
public function __construct()
|
||
|
{
|
||
|
log_message('debug', 'CSSMin library initialized.');
|
||
|
}
|
||
|
|
||
|
|
||
|
public function config($config)
|
||
|
{
|
||
|
foreach ($config as $key => $value)
|
||
|
{
|
||
|
$this->$key = $value;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
public function minify($css, $preserveComments = TRUE, $relativePath = null)
|
||
|
{
|
||
|
$c = ( isset($this->preserveComments) ) ? $this->preserveComments : $preserveComments;
|
||
|
$p = ( isset($this->relativePath) ) ? $this->relativePath : $relativePath;
|
||
|
|
||
|
$min = new Minify_CSS();
|
||
|
return $min->minify($css, array('preserveComments'=> $c, 'prependRelativePath' => $p));
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Class Minify_CSS
|
||
|
* @package Minify
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Compress CSS
|
||
|
*
|
||
|
* This is a heavy regex-based removal of whitespace, unnecessary
|
||
|
* comments and tokens, and some CSS value minimization, where practical.
|
||
|
* Many steps have been taken to avoid breaking comment-based hacks,
|
||
|
* including the ie5/mac filter (and its inversion), but expect tricky
|
||
|
* hacks involving comment tokens in 'content' value strings to break
|
||
|
* minimization badly. A test suite is available.
|
||
|
*
|
||
|
* @package Minify
|
||
|
* @author Stephen Clay <steve@mrclay.org>
|
||
|
* @author http://code.google.com/u/1stvamp/ (Issue 64 patch)
|
||
|
*/
|
||
|
class Minify_CSS {
|
||
|
|
||
|
/**
|
||
|
* Defines which class to call as part of callbacks, change this
|
||
|
* if you extend Minify_CSS
|
||
|
* @var string
|
||
|
*/
|
||
|
protected static $className = 'Minify_CSS';
|
||
|
|
||
|
/**
|
||
|
* Minify a CSS string
|
||
|
*
|
||
|
* @param string $css
|
||
|
*
|
||
|
* @param array $options available options:
|
||
|
*
|
||
|
* 'preserveComments': (default true) multi-line comments that begin
|
||
|
* with "/*!" will be preserved with newlines before and after to
|
||
|
* enhance readability.
|
||
|
*
|
||
|
* 'prependRelativePath': (default null) if given, this string will be
|
||
|
* prepended to all relative URIs in import/url declarations
|
||
|
*
|
||
|
* 'currentDir': (default null) if given, this is assumed to be the
|
||
|
* directory of the current CSS file. Using this, minify will rewrite
|
||
|
* all relative URIs in import/url declarations to correctly point to
|
||
|
* the desired files. For this to work, the files *must* exist and be
|
||
|
* visible by the PHP process.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public static function minify($css, $options = array())
|
||
|
{
|
||
|
if (isset($options['preserveComments'])
|
||
|
&& !$options['preserveComments']) {
|
||
|
return self::_minify($css, $options);
|
||
|
}
|
||
|
|
||
|
// recursive calls don't preserve comments
|
||
|
$options['preserveComments'] = false;
|
||
|
return Minify_CommentPreserver::process(
|
||
|
$css
|
||
|
,array(self::$className, 'minify')
|
||
|
,array($options)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Minify a CSS string
|
||
|
*
|
||
|
* @param string $css
|
||
|
*
|
||
|
* @param array $options To enable URL rewriting, set the value
|
||
|
* for key 'prependRelativePath'.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
protected static function _minify($css, $options)
|
||
|
{
|
||
|
$css = str_replace("\r\n", "\n", $css);
|
||
|
|
||
|
// preserve empty comment after '>'
|
||
|
// http://www.webdevout.net/css-hacks#in_css-selectors
|
||
|
$css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
|
||
|
|
||
|
// preserve empty comment between property and value
|
||
|
// http://css-discuss.incutio.com/?page=BoxModelHack
|
||
|
$css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
|
||
|
$css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
|
||
|
|
||
|
// apply callback to all valid comments (and strip out surrounding ws
|
||
|
self::$_inHack = false;
|
||
|
$css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@'
|
||
|
,array(self::$className, '_commentCB'), $css);
|
||
|
|
||
|
// remove ws around { } and last semicolon in declaration block
|
||
|
$css = preg_replace('/\\s*{\\s*/', '{', $css);
|
||
|
$css = preg_replace('/;?\\s*}\\s*/', '}', $css);
|
||
|
|
||
|
// remove ws surrounding semicolons
|
||
|
$css = preg_replace('/\\s*;\\s*/', ';', $css);
|
||
|
|
||
|
// remove ws around urls
|
||
|
$css = preg_replace('/
|
||
|
url\\( # url(
|
||
|
\\s*
|
||
|
([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis)
|
||
|
\\s*
|
||
|
\\) # )
|
||
|
/x', 'url($1)', $css);
|
||
|
|
||
|
// remove ws between rules and colons
|
||
|
$css = preg_replace('/
|
||
|
\\s*
|
||
|
([{;]) # 1 = beginning of block or rule separator
|
||
|
\\s*
|
||
|
([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter)
|
||
|
\\s*
|
||
|
:
|
||
|
\\s*
|
||
|
(\\b|[#\'"]) # 3 = first character of a value
|
||
|
/x', '$1$2:$3', $css);
|
||
|
|
||
|
// remove ws in selectors
|
||
|
$css = preg_replace_callback('/
|
||
|
(?: # non-capture
|
||
|
\\s*
|
||
|
[^~>+,\\s]+ # selector part
|
||
|
\\s*
|
||
|
[,>+~] # combinators
|
||
|
)+
|
||
|
\\s*
|
||
|
[^~>+,\\s]+ # selector part
|
||
|
{ # open declaration block
|
||
|
/x'
|
||
|
,array(self::$className, '_selectorsCB'), $css);
|
||
|
|
||
|
// minimize hex colors
|
||
|
$css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i'
|
||
|
, '$1#$2$3$4$5', $css);
|
||
|
|
||
|
// remove spaces between font families
|
||
|
$css = preg_replace_callback('/font-family:([^;}]+)([;}])/'
|
||
|
,array(self::$className, '_fontFamilyCB'), $css);
|
||
|
|
||
|
$css = preg_replace('/@import\\s+url/', '@import url', $css);
|
||
|
|
||
|
// replace any ws involving newlines with a single newline
|
||
|
$css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
|
||
|
|
||
|
// separate common descendent selectors w/ newlines (to limit line lengths)
|
||
|
$css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
|
||
|
|
||
|
// Use newline after 1st numeric value (to limit line lengths).
|
||
|
$css = preg_replace('/
|
||
|
((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
|
||
|
\\s+
|
||
|
/x'
|
||
|
,"$1\n", $css);
|
||
|
|
||
|
$rewrite = false;
|
||
|
if (isset($options['prependRelativePath'])) {
|
||
|
self::$_tempPrepend = $options['prependRelativePath'];
|
||
|
$rewrite = true;
|
||
|
} elseif (isset($options['currentDir'])) {
|
||
|
self::$_tempCurrentDir = $options['currentDir'];
|
||
|
$rewrite = true;
|
||
|
}
|
||
|
if ($rewrite) {
|
||
|
$css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/'
|
||
|
,array(self::$className, '_urlCB'), $css);
|
||
|
$css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
|
||
|
,array(self::$className, '_urlCB'), $css);
|
||
|
}
|
||
|
self::$_tempPrepend = self::$_tempCurrentDir = '';
|
||
|
return trim($css);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Replace what looks like a set of selectors
|
||
|
*
|
||
|
* @param array $m regex matches
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
protected static function _selectorsCB($m)
|
||
|
{
|
||
|
// remove ws around the combinators
|
||
|
return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @var bool Are we "in" a hack?
|
||
|
*
|
||
|
* I.e. are some browsers targetted until the next comment?
|
||
|
*/
|
||
|
protected static $_inHack = false;
|
||
|
|
||
|
/**
|
||
|
* @var string string to be prepended to relative URIs
|
||
|
*/
|
||
|
protected static $_tempPrepend = '';
|
||
|
|
||
|
/**
|
||
|
* @var string directory of this stylesheet for rewriting purposes
|
||
|
*/
|
||
|
protected static $_tempCurrentDir = '';
|
||
|
|
||
|
/**
|
||
|
* Process a comment and return a replacement
|
||
|
*
|
||
|
* @param array $m regex matches
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
protected static function _commentCB($m)
|
||
|
{
|
||
|
$m = $m[1];
|
||
|
// $m is the comment content w/o the surrounding tokens,
|
||
|
// but the return value will replace the entire comment.
|
||
|
if ($m === 'keep') {
|
||
|
return '/**/';
|
||
|
}
|
||
|
if ($m === '" "') {
|
||
|
// component of http://tantek.com/CSS/Examples/midpass.html
|
||
|
return '/*" "*/';
|
||
|
}
|
||
|
if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
|
||
|
// component of http://tantek.com/CSS/Examples/midpass.html
|
||
|
return '/*";}}/* */';
|
||
|
}
|
||
|
if (self::$_inHack) {
|
||
|
// inversion: feeding only to one browser
|
||
|
if (preg_match('@
|
||
|
^/ # comment started like /*/
|
||
|
\\s*
|
||
|
(\\S[\\s\\S]+?) # has at least some non-ws content
|
||
|
\\s*
|
||
|
/\\* # ends like /*/ or /**/
|
||
|
@x', $m, $n)) {
|
||
|
// end hack mode after this comment, but preserve the hack and comment content
|
||
|
self::$_inHack = false;
|
||
|
return "/*/{$n[1]}/**/";
|
||
|
}
|
||
|
}
|
||
|
if (substr($m, -1) === '\\') { // comment ends like \*/
|
||
|
// begin hack mode and preserve hack
|
||
|
self::$_inHack = true;
|
||
|
return '/*\\*/';
|
||
|
}
|
||
|
if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
|
||
|
// begin hack mode and preserve hack
|
||
|
self::$_inHack = true;
|
||
|
return '/*/*/';
|
||
|
}
|
||
|
if (self::$_inHack) {
|
||
|
// a regular comment ends hack mode but should be preserved
|
||
|
self::$_inHack = false;
|
||
|
return '/**/';
|
||
|
}
|
||
|
return ''; // remove all other comments
|
||
|
}
|
||
|
|
||
|
protected static function _urlCB($m)
|
||
|
{
|
||
|
$isImport = (0 === strpos($m[0], '@import'));
|
||
|
if ($isImport) {
|
||
|
$quote = $m[1];
|
||
|
$url = $m[2];
|
||
|
} else {
|
||
|
// is url()
|
||
|
// $m[1] is either quoted or not
|
||
|
$quote = ($m[1][0] === "'" || $m[1][0] === '"')
|
||
|
? $m[1][0]
|
||
|
: '';
|
||
|
$url = ($quote === '')
|
||
|
? $m[1]
|
||
|
: substr($m[1], 1, strlen($m[1]) - 2);
|
||
|
}
|
||
|
if ('/' !== $url[0]) {
|
||
|
if (strpos($url, '//') > 0) {
|
||
|
// probably starts with protocol, do not alter
|
||
|
} else {
|
||
|
// relative URI, rewrite!
|
||
|
if (self::$_tempPrepend) {
|
||
|
$url = self::$_tempPrepend . $url;
|
||
|
} else {
|
||
|
// rewrite absolute url from scratch!
|
||
|
// prepend path with current dir separator (OS-independent)
|
||
|
$path = self::$_tempCurrentDir
|
||
|
. DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR);
|
||
|
// strip doc root
|
||
|
$path = substr($path, strlen(realpath($_SERVER['DOCUMENT_ROOT'])));
|
||
|
// fix to absolute URL
|
||
|
$url = strtr($path, DIRECTORY_SEPARATOR, '/');
|
||
|
// remove /./ and /../ where possible
|
||
|
$url = str_replace('/./', '/', $url);
|
||
|
// inspired by patch from Oleg Cherniy
|
||
|
do {
|
||
|
$url = preg_replace('@/[^/]+/\\.\\./@', '/', $url, -1, $changed);
|
||
|
} while ($changed);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return $isImport
|
||
|
? "@import {$quote}{$url}{$quote}"
|
||
|
: "url({$quote}{$url}{$quote})";
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Process a font-family listing and return a replacement
|
||
|
*
|
||
|
* @param array $m regex matches
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
protected static function _fontFamilyCB($m)
|
||
|
{
|
||
|
$m[1] = preg_replace('/
|
||
|
\\s*
|
||
|
(
|
||
|
"[^"]+" # 1 = family in double qutoes
|
||
|
|\'[^\']+\' # or 1 = family in single quotes
|
||
|
|[\\w\\-]+ # or 1 = unquoted family
|
||
|
)
|
||
|
\\s*
|
||
|
/x', '$1', $m[1]);
|
||
|
return 'font-family:' . $m[1] . $m[2];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Class Minify_CommentPreserver
|
||
|
* @package Minify
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Process a string in pieces preserving C-style comments that begin with "/*!"
|
||
|
*
|
||
|
* @package Minify
|
||
|
* @author Stephen Clay <steve@mrclay.org>
|
||
|
*/
|
||
|
class Minify_CommentPreserver {
|
||
|
|
||
|
/**
|
||
|
* String to be prepended to each preserved comment
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
public static $prepend = "\n";
|
||
|
|
||
|
/**
|
||
|
* String to be appended to each preserved comment
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
public static $append = "\n";
|
||
|
|
||
|
/**
|
||
|
* Process a string outside of C-style comments that begin with "/*!"
|
||
|
*
|
||
|
* On each non-empty string outside these comments, the given processor
|
||
|
* function will be called. The first "!" will be removed from the
|
||
|
* preserved comments, and the comments will be surrounded by
|
||
|
* Minify_CommentPreserver::$preprend and Minify_CommentPreserver::$append.
|
||
|
*
|
||
|
* @param string $content
|
||
|
* @param callback $processor function
|
||
|
* @param array $args array of extra arguments to pass to the processor
|
||
|
* function (default = array())
|
||
|
* @return string
|
||
|
*/
|
||
|
public static function process($content, $processor, $args = array())
|
||
|
{
|
||
|
$ret = '';
|
||
|
while (true) {
|
||
|
list($beforeComment, $comment, $afterComment) = self::_nextComment($content);
|
||
|
if ('' !== $beforeComment) {
|
||
|
$callArgs = $args;
|
||
|
array_unshift($callArgs, $beforeComment);
|
||
|
$ret .= call_user_func_array($processor, $callArgs);
|
||
|
}
|
||
|
if (false === $comment) {
|
||
|
break;
|
||
|
}
|
||
|
$ret .= $comment;
|
||
|
$content = $afterComment;
|
||
|
}
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Extract comments that YUI Compressor preserves.
|
||
|
*
|
||
|
* @param string $in input
|
||
|
*
|
||
|
* @return array 3 elements are returned. If a YUI comment is found, the
|
||
|
* 2nd element is the comment and the 1st and 2nd are the surrounding
|
||
|
* strings. If no comment is found, the entire string is returned as the
|
||
|
* 1st element and the other two are false.
|
||
|
*/
|
||
|
private static function _nextComment($in)
|
||
|
{
|
||
|
if (
|
||
|
false === ($start = strpos($in, '/*!'))
|
||
|
|| false === ($end = strpos($in, '*/', $start + 3))
|
||
|
) {
|
||
|
return array($in, false, false);
|
||
|
}
|
||
|
$ret = array(
|
||
|
substr($in, 0, $start)
|
||
|
,self::$prepend . '/*' . substr($in, $start + 3, $end - $start - 1) . self::$append
|
||
|
);
|
||
|
$endChars = (strlen($in) - $end - 2);
|
||
|
$ret[] = (0 === $endChars)
|
||
|
? ''
|
||
|
: substr($in, -$endChars);
|
||
|
return $ret;
|
||
|
}
|
||
|
}
|