<?php
/**
* Formatter
*
* This module uses the methods from Minify_Compressor.php from Minify.
* I've modified it to allow for optional compression in certain parts.
*
* @author Stephen Clay <steve@mrclay.org>
* @author Anthony Short
*/
class Formatter
{
public static function formatting_process()
{
if(Scaffold::$config['Formatter']['compress'] === true)
{
Scaffold::$css->string = self::minify(Scaffold::$css->string);
}
else
{
Scaffold::$css->string = self::prettify(Scaffold::$css->string);
}
}
/**
* Minify a CSS string
*
* @param string $css
* @return string
*/
public static function minify($css)
{
$css = str_replace("\r\n", "\n", $css);
if(Scaffold::$config['Formatter']['preserve_hacks'])
{
// preserve empty comment after '>'
// http://www.webdevout.net/css-hacks#in_css-selectors
$css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
// preserve empty comment between property and value
// http://css-discuss.incutio.com/?page=BoxModelHack
$css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
$css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
}
// apply callback to all valid comments (and strip out surrounding ws
$css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@',array('Formatter', '_commentCB'), $css);
// Convert rgb() values to hex
if(Scaffold::$config['Formatter']['rgb_to_hex'])
{
$css = self::rgb_to_hex($css);
}
// Strip out the units on 0 measurements eg 0px
if(Scaffold::$config['Formatter']['remove_empty_measurements'])
{
$css = preg_replace('/([^0-9])0(px|em|\%)/', "\${1}0", $css);
$css = preg_replace('/([^0-9])0\.([0-9]+)em/', '$1.$2em', $css);
$css = preg_replace('/\-0([^\.])/',"0\${1}",$css);
}
// Convert font-weights to numbers
if(Scaffold::$config['Formatter']['font_weights_to_numbers'])
{
$css = self::font_weights_to_numbers($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('Formatter', '_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('Formatter', '_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);
if(Scaffold::$config['Formatter']['limit_line_lengths'])
{
// 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);
}
return trim($css);
}
/**
* Converts font-weights into numbers
*
* @param $css
* @return return type
*/
static private function font_weights_to_numbers($css)
{
if( $found = Scaffold::$css->find_properties_with_value('font-weight','bold|normal') )
{
foreach($found[2] as $key => $value)
{
if($value == 'bold')
{
$css = str_replace($found[0][$key],'font-weight:700',$css);
}
elseif($value == 'normal')
{
$css = str_replace($found[0][$key],'font-weight:400',$css);
}
}
}
return $css;
}
/**
* Takes a CSS string, finds all rgb() values and converts them to hex format
*
* @param $css
* @return string
*/
private static function rgb_to_hex($css)
{
if( $rgbs = Scaffold::$css->find_functions('rgb') )
{
foreach( $rgbs[2] as $key => $found )
{
$color = null;
foreach(explode(',',$found) as $value)
{
if(is_int($value)){
$hex = dechex($value);
$color .= ( strlen($hex) == 1 ) ? 0 . $hex : $hex;
}
}
if($color)
$css = str_replace($rgbs[0][$key],"#" . $color,$css);
}
}
return $css;
}
/**
* Replace what looks like a set of selectors
*
* @param array $m regex matches
* @return string
*/
static 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
*/
static protected 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(Scaffold::$config['Formatter']['preserve_hacks'])
{
if ($m === 'keep')
{
return '/**/';
}
// component of http://tantek.com/CSS/Examples/midpass.html
if ($m === '" "')
{
return '/*" "*/';
}
// component of http://tantek.com/CSS/Examples/midpass.html
if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m))
{
return '/*";}}/* */';
}
// 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))
{
return "/*/{$n[1]}/**/";
}
// comment ends like \*/
elseif (substr($m, -1) === '\\')
{
return '/*\\*/';
}
// comment looks like /*/ foo */
elseif ($m !== '' && $m[0] === '/')
{
return '/*/*/';
}
else
{
return '/**/';
}
}
if(Scaffold::$config['Formatter']['preserve_comments'] === true)
{
return '/*' .$m. '*/';
}
else
{
return ''; // remove all other comments
}
}
/**
* Process a font-family listing and return a replacement
*
* @param array $m regex matches
*
* @return string
*/
static 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];
}
/**
* Makes a CSS string easier to read by adding line breaks where
* needed and stripping out unneeded whitespace
*
* @param $css
* @return string
*/
public static function prettify($css)
{
// escape data protocol to prevent processing
$css = preg_replace_callback('#(url\(data:[^\)]+\))#', function ($m)
{
return 'esc('.base64_encode($m[1]).')';
}, $css);
// line break after semi-colons (for @import)
$css = str_replace(';', ";\r\r", $css);
// normalize comments spacing and lines
$css = preg_replace('#\*/#sx',"*/\r",$css);
// normalize space around opening brackets
$css = preg_replace('#\s*\{\s*#', "\r{\r", $css);
// normalize property name/value space
$css = preg_replace('#([-a-z]+):\s*([^;}{]+);\s*#i', "\t$1: $2;\r", $css);
// normalize space around closing brackets
$css = preg_replace('#\s*\}\s*#', "\r}\r\r", $css);
// new line for each selector in a compound selector
$css = preg_replace('#,\s*#', ",\r", $css);
// remove returns after commas in property values
if (preg_match_all('#:[^;]+,[^;]+;#', $css, $m))
{
foreach($m[0] as $oops)
{
$css = str_replace($oops, preg_replace('#,\r#', ', ', $oops), $css);
}
}
$css = preg_replace_callback('#esc\(([^\)]+)\)#', function ($m)
{
return base64_decode($m[1]);
}, $css); // unescape escaped blocks
// indent nested @media rules
if (preg_match('#@media[^\{]*\{(.*\}\s*)\}#', $css, $m))
{
$css = str_replace($m[0], str_replace($m[1], "\r\t".preg_replace("#\r#", "\r\t", trim($m[1]))."\r", $m[0]), $css);
}
return $css;
}
/**
* Escape data protocol to prevent processing
*
* @param array $m regex matches
*
* @return string
*/
static protected function _encode($m)
{
return 'esc('.base64_encode($m[1]).')';
}
/**
* Unescape escaped blocks
*
* @param array $m regex matches
*
* @return string
*/
static protected function _decode($m)
{
return base64_decode($m[1]);
}
}