In PHP can you compress/minify CSS with regex (PCRE)?
(As a theoretical in regex. I'm sure there are libraries out there that do this well.)
Background note: After spending hours writing an answer to a deleted (half crap) question, I thought I'd post a part of the underlying question and answer it my self. Hope it's ok.
(Ok, it may not be overly simple, but pretty straight forward.)
This answer assumes that the requirements are:
{
, }
, ;
, ,
, >
, ~
, +
, -
!important
:
, except in selectors (where you have to keep a space before it)$=
(
/[
and left of )
/]
;
in a blockNote that the requirements here do not include converting CSS properties to shorter versions (like using shorthand properties instead of several full length properties, removing quotes where not required). This is something that regex would not be able to solve in general.
It's easier to solve this in two passes: first remove the comments, then everything else.
It should be possible to do in a single pass, but then you have to replace all \s
with an expression that matches both spaces and comments (among some other modifications).
The first pass expression to remove comments:
(?xs)
# quotes
(
"(?:[^"\\]++|\\.)*+"
| '(?:[^'\\]++|\\.)*+'
)
|
# comments
/\* (?> .*? \*/ )
Replace with $1
.
And to remove everything else you can use:
(?six)
# quotes
(
"(?:[^"\\]++|\\.)*+"
| '(?:[^'\\]++|\\.)*+'
)
|
# ; before } (and the spaces after it while we're here)
\s*+ ; \s*+ ( } ) \s*+
|
# all spaces around meta chars/operators
\s*+ ( [*$~^|]?+= | [{};,>~+-] | !important\b ) \s*+
|
# spaces right of ( [ :
( [[(:] ) \s++
|
# spaces left of ) ]
\s++ ( [])] )
|
# spaces left (and right) of :
\s++ ( : ) \s*+
# but not in selectors: not followed by a {
(?!
(?>
[^{}"']++
| "(?:[^"\\]++|\\.)*+"
| '(?:[^'\\]++|\\.)*+'
)*+
{
)
|
# spaces at beginning/end of string
^ \s++ | \s++ \z
|
# double spaces to single
(\s)\s+
Replaced with $1$2$3$4$5$6$7
.
The selector check for removing spaces before :
(the negative lookahead) can slow this down compared to proper parsers.
Parsers already know if they are in a selector or not, and don't have to do extra searches to check that.
function minify_css($str){
# remove comments first (simplifies the other regex)
$re1 = <<<'EOS'
(?sx)
# quotes
(
"(?:[^"\\]++|\\.)*+"
| '(?:[^'\\]++|\\.)*+'
)
|
# comments
/\* (?> .*? \*/ )
EOS;
$re2 = <<<'EOS'
(?six)
# quotes
(
"(?:[^"\\]++|\\.)*+"
| '(?:[^'\\]++|\\.)*+'
)
|
# ; before } (and the spaces after it while we're here)
\s*+ ; \s*+ ( } ) \s*+
|
# all spaces around meta chars/operators
\s*+ ( [*$~^|]?+= | [{};,>~+-] | !important\b ) \s*+
|
# spaces right of ( [ :
( [[(:] ) \s++
|
# spaces left of ) ]
\s++ ( [])] )
|
# spaces left (and right) of :
\s++ ( : ) \s*+
# but not in selectors: not followed by a {
(?!
(?>
[^{}"']++
| "(?:[^"\\]++|\\.)*+"
| '(?:[^'\\]++|\\.)*+'
)*+
{
)
|
# spaces at beginning/end of string
^ \s++ | \s++ \z
|
# double spaces to single
(\s)\s+
EOS;
$str = preg_replace("%$re1%", '$1', $str);
return preg_replace("%$re2%", '$1$2$3$4$5$6$7', $str);
}
Can be found at ideone.com:
$in = <<<'EOS'
p * i , html
/* remove spaces */
/* " comments have no escapes \*/
body/* keep */ /* space */p,
p [ remove ~= " spaces " ] :nth-child( 3 + 2n ) > b span i , div::after
{
/* comment */
background : url( " /* string */ " ) blue !important ;
content : " escapes \" allowed \\" ;
width: calc( 100% - 3em + 5px ) ;
margin-top : 0;
margin-bottom : 0;
margin-left : 10px;
margin-right : 10px;
}
EOS;
$out = minify_css($in);
echo "input:\n";
var_dump($in);
echo "\n\n";
echo "output:\n";
var_dump($out);
Output:
input:
string(435) "
p * i , html
/* remove spaces */
/* " comments have no escapes \*/
body/* keep */ /* space */p,
p [ remove ~= " spaces " ] :nth-child( 3 + 2n ) > b span i , div::after
{
/* comment */
background : url( " /* string */ " ) blue !important ;
content : " escapes \" allowed \\" ;
width: calc( 100% - 3em + 5px ) ;
margin-top : 0;
margin-bottom : 0;
margin-left : 10px;
margin-right : 10px;
}
"
output:
string(251) "p * i,html body p,p [remove~=" spaces "] :nth-child(3+2n)>b span i,div::after{background:url(" /* string */ ") blue!important;content:" escapes \" allowed \\";width:calc(100%-3em+5px);margin-top:0;margin-bottom:0;margin-left:10px;margin-right:10px}"
Results of cssminifier.com for the same input as the test above:
p * i,html /*\*/body/**/p,p [remove ~= " spaces "] :nth-child(3+2n)>b span i,div::after{background:url(" /* string */ ") blue;content:" escapes \" allowed \\";width:calc(100% - 3em+5px);margin-top:0;margin-bottom:0;margin-left:10px;margin-right:10px}
Length 263 byte. 12 byte longer than the output of the regex minifier above.
cssminifier.com has some disadvantages compared to this regex minifier:
Output of CSSTidy 1.3 (via codebeautifier.com) at highest compression level preset:
p * i,html /* remove spaces */
/* " comments have no escapes \*/
body/* keep */ /* space */p,p [ remove ~= " spaces " ] :nth-child( 3 + 2n ) > b span i,div::after{background:url(" /* string */ ") blue!important;content:" escapes \" allowed \\";width:calc(100%-3em+5px);margin:0 10px;}
Length 286 byte. 35 byte longer than the output of the regex minifier.
CSSTidy doesn't remove comments or spaces in some selectors. But it does minify to shorthand properties. The latter should probably help compress normal CSS a lot more.
Minified output from the different minifiers for the same input as in the above example. (Leftover line breaks replaced with spaces.)
this answern (251): p * i,html body p,p [remove~=" spaces "] :nth-child(3+2n)>b span i,div::after{background:url(" /* string */ ") blue!important;content:" escapes \" allowed \\";width:calc(100%-3em+5px);margin-top:0;margin-bottom:0;margin-left:10px;margin-right:10px}
cssminifier.com (263): p * i,html /*\*/body/**/p,p [remove ~= " spaces "] :nth-child(3+2n)>b span i,div::after{background:url(" /* string */ ") blue!important;content:" escapes \" allowed \\";width:calc(100% - 3em+5px);margin-top:0;margin-bottom:0;margin-left:10px;margin-right:10px}
CSSTidy 1.3 (286): p * i,html /* remove spaces */ /* " comments have no escapes \*/ body/* keep */ /* space */p,p [ remove ~= " spaces " ] :nth-child( 3 + 2n ) > b span i,div::after{background:url(" /* string */ ") blue!important;content:" escapes \" allowed \\";width:calc(100%-3em+5px);margin:0 10px;}
For normal CSS CSSTidy is probably best as it converts to shorthand properties.
I assume there are other minifiers (like the YUI compressor) that should be better at this, and give shorter result than this regex minifier.
Here's a slightly modified version of @Qtax's answer which resolves the issues with calc()
thanks to an alternative regex from @matthiasmullie's Minify library.
function minify_css( $string = '' ) {
$comments = <<<'EOS'
(?sx)
# don't change anything inside of quotes
( "(?:[^"\\]++|\\.)*+" | '(?:[^'\\]++|\\.)*+' )
|
# comments
/\* (?> .*? \*/ )
EOS;
$everything_else = <<<'EOS'
(?six)
# don't change anything inside of quotes
( "(?:[^"\\]++|\\.)*+" | '(?:[^'\\]++|\\.)*+' )
|
# spaces before and after ; and }
\s*+ ; \s*+ ( } ) \s*+
|
# all spaces around meta chars/operators (excluding + and -)
\s*+ ( [*$~^|]?+= | [{};,>~] | !important\b ) \s*+
|
# all spaces around + and - (in selectors only!)
\s*([+-])\s*(?=[^}]*{)
|
# spaces right of ( [ :
( [[(:] ) \s++
|
# spaces left of ) ]
\s++ ( [])] )
|
# spaces left (and right) of : (but not in selectors)!
\s+(:)(?![^\}]*\{)
|
# spaces at beginning/end of string
^ \s++ | \s++ \z
|
# double spaces to single
(\s)\s+
EOS;
$search_patterns = array( "%{$comments}%", "%{$everything_else}%" );
$replace_patterns = array( '$1', '$1$2$3$4$5$6$7$8' );
return preg_replace( $search_patterns, $replace_patterns, $string );
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With