626 lines
21 KiB
PHP
626 lines
21 KiB
PHP
<?php
|
|
|
|
/* Taken from minify by Ryan Grove and Steve Clay and distributed
|
|
under the following license:
|
|
|
|
Copyright (c) 2008 Ryan Grove <ryan@wonko.com>
|
|
Copyright (c) 2008 Steve Clay <steve@mrclay.org>
|
|
All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
modification, are permitted provided that the following conditions are met:
|
|
|
|
* Redistributions of source code must retain the above copyright notice,
|
|
this list of conditions and the following disclaimer.
|
|
* Redistributions in binary form must reproduce the above copyright notice,
|
|
this list of conditions and the following disclaimer in the documentation
|
|
and/or other materials provided with the distribution.
|
|
* Neither the name of this project nor the names of its contributors may be
|
|
used to endorse or promote products derived from this software without
|
|
specific prior written permission.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
*/
|
|
|
|
class Minify_CSS
|
|
{
|
|
public static function minify($css, $options = [])
|
|
{
|
|
if (isset($options['preserveComments'])
|
|
&& !$options['preserveComments']
|
|
) {
|
|
$css = Minify_CSS_Compressor::process($css, $options);
|
|
} else {
|
|
$css = Minify_CommentPreserver::process(
|
|
$css, ['Minify_CSS_Compressor', 'process'], [$options]
|
|
);
|
|
}
|
|
if (!isset($options['currentDir']) && !isset($options['prependRelativePath'])) {
|
|
return $css;
|
|
}
|
|
if (isset($options['currentDir'])) {
|
|
return Minify_CSS_UriRewriter::rewrite(
|
|
$css, $options['currentDir'], isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT'], isset($options['symlinks']) ? $options['symlinks'] : []
|
|
);
|
|
} else {
|
|
return Minify_CSS_UriRewriter::prepend(
|
|
$css, $options['prependRelativePath']
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class Minify_CSS_UriRewriter
|
|
{
|
|
/**
|
|
* Defines which class to call as part of callbacks, change this
|
|
* if you extend Minify_CSS_UriRewriter.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $className = 'Minify_CSS_UriRewriter';
|
|
|
|
/**
|
|
* rewrite() and rewriteRelative() append debugging information here.
|
|
*
|
|
* @var string
|
|
*/
|
|
public static $debugText = '';
|
|
|
|
/**
|
|
* Rewrite file relative URIs as root relative in CSS files.
|
|
*
|
|
* @param string $css
|
|
* @param string $currentDir the directory of the current CSS file
|
|
* @param string $docRoot the document root of the web site in which
|
|
* the CSS file resides (default = $_SERVER['DOCUMENT_ROOT'])
|
|
* @param array $symlinks (default = array()) If the CSS file is stored in
|
|
* a symlink-ed directory, provide an array of link paths to
|
|
* target paths, where the link paths are within the document root. Because
|
|
* paths need to be normalized for this to work, use "//" to substitute
|
|
* the doc root in the link paths (the array keys). E.g.:
|
|
* <code>
|
|
* array('//symlink' => '/real/target/path') // unix
|
|
* array('//static' => 'D:\\staticStorage') // Windows
|
|
* </code>
|
|
* @param bool $leave_imports don't rewrite imports. Only touch URLs
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = [], $leave_imports = false)
|
|
{
|
|
self::$_docRoot = self::_realpath(
|
|
$docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT']
|
|
);
|
|
self::$_currentDir = self::_realpath($currentDir);
|
|
self::$_symlinks = [];
|
|
|
|
// normalize symlinks
|
|
foreach ($symlinks as $link => $target) {
|
|
$link = ($link === '//')
|
|
? self::$_docRoot
|
|
: str_replace('//', self::$_docRoot.'/', $link);
|
|
$link = strtr($link, '/', DIRECTORY_SEPARATOR);
|
|
self::$_symlinks[$link] = self::_realpath($target);
|
|
}
|
|
|
|
self::$debugText .= 'docRoot : '.self::$_docRoot."\n"
|
|
.'currentDir : '.self::$_currentDir."\n";
|
|
if (self::$_symlinks) {
|
|
self::$debugText .= 'symlinks : '.var_export(self::$_symlinks, 1)."\n";
|
|
}
|
|
self::$debugText .= "\n";
|
|
|
|
$css = self::_trimUrls($css);
|
|
|
|
// rewrite
|
|
if (!$leave_imports) {
|
|
$css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/', [self::$className, '_processUriCB'], $css);
|
|
}
|
|
$css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/', [self::$className, '_processUriCB'], $css);
|
|
|
|
return $css;
|
|
}
|
|
|
|
/**
|
|
* Prepend a path to relative URIs in CSS files.
|
|
*
|
|
* @param string $css
|
|
* @param string $path the path to prepend
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function prepend($css, $path)
|
|
{
|
|
self::$_prependPath = $path;
|
|
|
|
$css = self::_trimUrls($css);
|
|
|
|
// append
|
|
$css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/', [self::$className, '_processUriCB'], $css);
|
|
$css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/', [self::$className, '_processUriCB'], $css);
|
|
|
|
self::$_prependPath = null;
|
|
|
|
return $css;
|
|
}
|
|
|
|
/**
|
|
* @var string directory of this stylesheet
|
|
*/
|
|
private static $_currentDir = '';
|
|
|
|
/**
|
|
* @var string DOC_ROOT
|
|
*/
|
|
private static $_docRoot = '';
|
|
|
|
/**
|
|
* @var array directory replacements to map symlink targets back to their
|
|
* source (within the document root) E.g. '/var/www/symlink' => '/var/realpath'
|
|
*/
|
|
private static $_symlinks = [];
|
|
|
|
/**
|
|
* @var string path to prepend
|
|
*/
|
|
private static $_prependPath;
|
|
|
|
private static function _trimUrls($css)
|
|
{
|
|
return preg_replace('/
|
|
url\\( # url(
|
|
\\s*
|
|
([^\\)]+?) # 1 = URI (assuming does not contain ")")
|
|
\\s*
|
|
\\) # )
|
|
/x', 'url($1)', $css);
|
|
}
|
|
|
|
private static function _processUriCB($m)
|
|
{
|
|
// $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
|
|
$isImport = ($m[0][0] === '@');
|
|
// determine URI and the quote character (if any)
|
|
if ($isImport) {
|
|
$quoteChar = $m[1];
|
|
$uri = $m[2];
|
|
} else {
|
|
// $m[1] is either quoted or not
|
|
$quoteChar = ($m[1][0] === "'" || $m[1][0] === '"')
|
|
? $m[1][0]
|
|
: '';
|
|
$uri = ($quoteChar === '')
|
|
? $m[1]
|
|
: substr($m[1], 1, strlen($m[1]) - 2);
|
|
}
|
|
// analyze URI
|
|
if ('/' !== $uri[0] // root-relative
|
|
&& false === strpos($uri, '//') // protocol (non-data)
|
|
&& 0 !== strpos($uri, 'data:') // data protocol
|
|
) {
|
|
// URI is file-relative: rewrite depending on options
|
|
$uri = (self::$_prependPath !== null)
|
|
? (self::$_prependPath.$uri)
|
|
: self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks);
|
|
}
|
|
|
|
return $isImport
|
|
? "@import {$quoteChar}{$uri}{$quoteChar}"
|
|
: "url({$quoteChar}{$uri}{$quoteChar})";
|
|
}
|
|
|
|
/**
|
|
* Rewrite a file relative URI as root relative.
|
|
*
|
|
* <code>
|
|
* Minify_CSS_UriRewriter::rewriteRelative(
|
|
* '../img/hello.gif'
|
|
* , '/home/user/www/css' // path of CSS file
|
|
* , '/home/user/www' // doc root
|
|
* );
|
|
* // returns '/img/hello.gif'
|
|
*
|
|
* // example where static files are stored in a symlinked directory
|
|
* Minify_CSS_UriRewriter::rewriteRelative(
|
|
* 'hello.gif'
|
|
* , '/var/staticFiles/theme'
|
|
* , '/home/user/www'
|
|
* , array('/home/user/www/static' => '/var/staticFiles')
|
|
* );
|
|
* // returns '/static/theme/hello.gif'
|
|
* </code>
|
|
*
|
|
* @param string $uri file relative URI
|
|
* @param string $realCurrentDir realpath of the current file's directory
|
|
* @param string $realDocRoot realpath of the site document root
|
|
* @param array $symlinks (default = array()) If the file is stored in
|
|
* a symlink-ed directory, provide an array of link paths to
|
|
* real target paths, where the link paths "appear" to be within the document
|
|
* root. E.g.:
|
|
* <code>
|
|
* array('/home/foo/www/not/real/path' => '/real/target/path') // unix
|
|
* array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path') // Windows
|
|
* </code>
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = [])
|
|
{
|
|
// prepend path with current dir separator (OS-independent)
|
|
$path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR)
|
|
.DIRECTORY_SEPARATOR.strtr($uri, '/', DIRECTORY_SEPARATOR);
|
|
|
|
self::$debugText .= "file-relative URI : {$uri}\n"
|
|
."path prepended : {$path}\n";
|
|
|
|
// "unresolve" a symlink back to doc root
|
|
foreach ($symlinks as $link => $target) {
|
|
if (0 === strpos($path, $target)) {
|
|
// replace $target with $link
|
|
$path = $link.substr($path, strlen($target));
|
|
|
|
self::$debugText .= "symlink unresolved : {$path}\n";
|
|
|
|
break;
|
|
}
|
|
}
|
|
// strip doc root
|
|
$path = substr($path, strlen($realDocRoot));
|
|
|
|
self::$debugText .= "docroot stripped : {$path}\n";
|
|
|
|
// fix to root-relative URI
|
|
|
|
$uri = strtr($path, '/\\', '//');
|
|
|
|
// remove /./ and /../ where possible
|
|
$uri = str_replace('/./', '/', $uri);
|
|
// inspired by patch from Oleg Cherniy
|
|
do {
|
|
$uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed);
|
|
} while ($changed);
|
|
|
|
self::$debugText .= "traversals removed : {$uri}\n\n";
|
|
|
|
return $uri;
|
|
}
|
|
|
|
/**
|
|
* Get realpath with any trailing slash removed. If realpath() fails,
|
|
* just remove the trailing slash.
|
|
*
|
|
* @param string $path
|
|
*
|
|
* @return mixed path with no trailing slash
|
|
*/
|
|
protected static function _realpath($path)
|
|
{
|
|
$realPath = realpath($path);
|
|
if ($realPath !== false) {
|
|
$path = $realPath;
|
|
}
|
|
|
|
return rtrim($path, '/\\');
|
|
}
|
|
}
|
|
|
|
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 callable $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 = [])
|
|
{
|
|
$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 [$in, false, false];
|
|
}
|
|
$ret = [
|
|
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;
|
|
}
|
|
}
|
|
|
|
class Minify_CSS_Compressor
|
|
{
|
|
/**
|
|
* Minify a CSS string.
|
|
*
|
|
* @param string $css
|
|
* @param array $options (currently ignored)
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function process($css, $options = [])
|
|
{
|
|
$obj = new self($options);
|
|
|
|
return $obj->_process($css);
|
|
}
|
|
|
|
/**
|
|
* @var array options
|
|
*/
|
|
protected $_options;
|
|
|
|
/**
|
|
* @var bool Are we "in" a hack?
|
|
*
|
|
* I.e. are some browsers targetted until the next comment?
|
|
*/
|
|
protected $_inHack = false;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param array $options (currently ignored)
|
|
*/
|
|
private function __construct($options)
|
|
{
|
|
$this->_options = $options;
|
|
}
|
|
|
|
/**
|
|
* Minify a CSS string.
|
|
*
|
|
* @param string $css
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _process($css)
|
|
{
|
|
$css = str_replace("\r\n", "\n", $css);
|
|
|
|
// preserve empty comment after '>'
|
|
// http://www.webdevout.net/css-hacks#in_css-selectors
|
|
$css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
|
|
|
|
// preserve empty comment between property and value
|
|
// http://css-discuss.incutio.com/?page=BoxModelHack
|
|
$css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
|
|
$css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
|
|
|
|
// apply callback to all valid comments (and strip out surrounding ws
|
|
$css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@', [$this, '_commentCB'], $css);
|
|
|
|
// remove ws around { } and last semicolon in declaration block
|
|
$css = preg_replace('/\\s*{\\s*/', '{', $css);
|
|
$css = preg_replace('/;?\\s*}\\s*/', '}', $css);
|
|
|
|
// remove ws surrounding semicolons
|
|
$css = preg_replace('/\\s*;\\s*/', ';', $css);
|
|
|
|
// remove ws around urls
|
|
$css = preg_replace('/
|
|
url\\( # url(
|
|
\\s*
|
|
([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis)
|
|
\\s*
|
|
\\) # )
|
|
/x', 'url($1)', $css);
|
|
|
|
// remove ws between rules and colons
|
|
$css = preg_replace('/
|
|
\\s*
|
|
([{;]) # 1 = beginning of block or rule separator
|
|
\\s*
|
|
([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter)
|
|
\\s*
|
|
:
|
|
\\s*
|
|
(\\b|[#\'"]) # 3 = first character of a value
|
|
/x', '$1$2:$3', $css);
|
|
|
|
// remove ws in selectors
|
|
$css = preg_replace_callback('/
|
|
(?: # non-capture
|
|
\\s*
|
|
[^~>+,\\s]+ # selector part
|
|
\\s*
|
|
[,>+~] # combinators
|
|
)+
|
|
\\s*
|
|
[^~>+,\\s]+ # selector part
|
|
{ # open declaration block
|
|
/x', [$this, '_selectorsCB'], $css);
|
|
|
|
// minimize hex colors
|
|
$css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i', '$1#$2$3$4$5', $css);
|
|
|
|
// remove spaces between font families
|
|
$css = preg_replace_callback('/font-family:([^;}]+)([;}])/', [$this, '_fontFamilyCB'], $css);
|
|
|
|
$css = preg_replace('/@import\\s+url/', '@import url', $css);
|
|
|
|
// replace any ws involving newlines with a single newline
|
|
$css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
|
|
|
|
// separate common descendent selectors w/ newlines (to limit line lengths)
|
|
// $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
|
|
|
|
// Use newline after 1st numeric value (to limit line lengths).
|
|
$css = preg_replace('/
|
|
((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
|
|
\\s+
|
|
/x', "$1\n", $css);
|
|
|
|
// prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
|
|
$css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);
|
|
|
|
return trim($css);
|
|
}
|
|
|
|
/**
|
|
* Replace what looks like a set of selectors.
|
|
*
|
|
* @param array $m regex matches
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _selectorsCB($m)
|
|
{
|
|
// remove ws around the combinators
|
|
return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
|
|
}
|
|
|
|
/**
|
|
* Process a comment and return a replacement.
|
|
*
|
|
* @param array $m regex matches
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _commentCB($m)
|
|
{
|
|
$hasSurroundingWs = (trim($m[0]) !== $m[1]);
|
|
$m = $m[1];
|
|
// $m is the comment content w/o the surrounding tokens,
|
|
// but the return value will replace the entire comment.
|
|
if ($m === 'keep') {
|
|
return '/**/';
|
|
}
|
|
if ($m === '" "') {
|
|
// component of http://tantek.com/CSS/Examples/midpass.html
|
|
return '/*" "*/';
|
|
}
|
|
if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
|
|
// component of http://tantek.com/CSS/Examples/midpass.html
|
|
return '/*";}}/* */';
|
|
}
|
|
if ($this->_inHack) {
|
|
// inversion: feeding only to one browser
|
|
if (preg_match('@
|
|
^/ # comment started like /*/
|
|
\\s*
|
|
(\\S[\\s\\S]+?) # has at least some non-ws content
|
|
\\s*
|
|
/\\* # ends like /*/ or /**/
|
|
@x', $m, $n)) {
|
|
// end hack mode after this comment, but preserve the hack and comment content
|
|
$this->_inHack = false;
|
|
|
|
return "/*/{$n[1]}/**/";
|
|
}
|
|
}
|
|
if (substr($m, -1) === '\\') { // comment ends like \*/
|
|
// begin hack mode and preserve hack
|
|
$this->_inHack = true;
|
|
|
|
return '/*\\*/';
|
|
}
|
|
if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
|
|
// begin hack mode and preserve hack
|
|
$this->_inHack = true;
|
|
|
|
return '/*/*/';
|
|
}
|
|
if ($this->_inHack) {
|
|
// a regular comment ends hack mode but should be preserved
|
|
$this->_inHack = false;
|
|
|
|
return '/**/';
|
|
}
|
|
|
|
// Issue 107: if there's any surrounding whitespace, it may be important, so
|
|
// replace the comment with a single space
|
|
return $hasSurroundingWs // remove all other comments
|
|
? ' '
|
|
: '';
|
|
}
|
|
|
|
/**
|
|
* Process a font-family listing and return a replacement.
|
|
*
|
|
* @param array $m regex matches
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _fontFamilyCB($m)
|
|
{
|
|
$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];
|
|
}
|
|
}
|