File Coverage

blib/lib/CSS/Compressor.pm
Criterion Covered Total %
statement 73 73 100.0
branch 4 4 100.0
condition 19 26 73.0
subroutine 5 5 100.0
pod 1 1 100.0
total 102 109 93.5


line stmt bran cond sub pod time code
1             package CSS::Compressor;
2              
3 7     7   499016 use strict;
  7         67  
  7         238  
4 7     7   42 use warnings;
  7         14  
  7         209  
5              
6 7     7   43 use Exporter qw( import );
  7         12  
  7         689  
7              
8             our @EXPORT_OK = qw( css_compress );
9              
10             our $VERSION = '0.05';
11              
12             our $MARKER;
13              
14             # take package name, replace double colons with underscore and use that as
15             # marker for search and replace operations
16             BEGIN {
17 7     7   26 $MARKER = uc __PACKAGE__;
18 7         13732 $MARKER =~ tr!:!_!s;
19             }
20              
21             # build optimized regular expression variables ( foo -> [Ff][Oo][Oo] )
22             my (
23             $RE_BACKGROUND_POSITION,
24             $RE_TRANSFORM_ORIGIN_MOZ,
25             $RE_TRANSFORM_ORIGIN_MS,
26             $RE_TRANSFORM_ORIGIN_O,
27             $RE_TRANSFORM_ORIGIN_WEBKIT,
28             $RE_TRANSFORM_ORIGIN,
29             $RE_BORDER,
30             $RE_BORDER_TOP,
31             $RE_BORDER_RIGHT,
32             $RE_BORDER_BOTTOM,
33             $RE_BORDER_LEFT,
34             $RE_OUTLINE,
35             $RE_BACKGROUND,
36             $RE_ALPHA_FILTER,
37             ) = map +(
38             join '' => map m![a-zA-Z]!
39             ? '['.ucfirst($_).lc($_).']'
40             : '\\'.$_,
41             split m//
42             ) => qw[
43             background-position
44             moz-transform-origin
45             ms-transform-origin
46             o-transform-origin
47             webkit-transform-origin
48             transform-origin
49             border
50             border-top
51             border-right
52             border-bottom
53             border-right
54             outline
55             background
56             progid:DXImageTransform.Microsoft.Alpha(Opacity=
57             ];
58              
59             # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
60             # compress
61             #
62             # IN: 1 uncompressed CSS
63             # OUT: 1 compressed CSS
64              
65             sub css_compress {
66 36     36 1 26372 my ( $css ) = @_;
67 36         76 my @comments,
68             my @tokens;
69              
70             # collect all comment blocks...
71 36         206 $css =~ s! /\* (.*?) \*/
72 40         263 ! '/*___'.$MARKER.'_PRESERVE_CANDIDATE_COMMENT_'.
73             ( -1 + push @comments => $1 ).'___*/'
74             !sogex;
75              
76             # preserve urls to prevent breaking inline SVG for example
77 36         119 $css =~ s! ( url \( ( (?: [^()]++ | \( (?2) \) )*+ ) \) ) !
78 4         34 '___'.$MARKER.'_PRESERVED_TOKEN_'.(-1+push @tokens => $1).'___'
79             !gxe;
80              
81             # preserve strings so their content doesn't get accidentally minified
82 36         114 $css =~ s! " ( [^"\\]*(?:\\.[^"\\]*)* ) " !
83 15         187 $_ = $1,
84              
85             # maybe the string contains a comment-like substring?
86             # one, maybe more? put'em back then
87             s/___${MARKER}_PRESERVE_CANDIDATE_COMMENT_([0-9]+)___/$comments[$1]/go,
88              
89             # minify alpha opacity in filter strings
90             s/$RE_ALPHA_FILTER/alpha(opacity=/go,
91              
92             '"___'.$MARKER.'_PRESERVED_TOKEN_'.(-1+push @tokens => $_).'___"'
93             !sgxe;
94 36         85 $css =~ s! ' ( [^'\\]*(?:\\.[^'\\]*)* ) ' !
95 8         121 $_ = $1,
96              
97             s/___${MARKER}_PRESERVE_CANDIDATE_COMMENT_([0-9]+)___/$comments[$1]/go,
98              
99             s/$RE_ALPHA_FILTER/alpha(opacity=/go,
100              
101             '\'___'.$MARKER.'_PRESERVED_TOKEN_'.(-1+push @tokens => $_).'___\''
102             !sgxe;
103              
104             # strings are safe, now wrestle the comments
105              
106             # ! in the first position of the comment means preserve
107             # so push to the preserved tokens while stripping the !
108             0 == index $_->[1] => '!'
109             and -1 == index $_->[1] => '! @noflip'
110             and
111             $css =~ s!___${MARKER}_PRESERVE_CANDIDATE_COMMENT_$_->[0]___!
112 10         176 '___'.$MARKER.'_PRESERVED_TOKEN_'.(-1+push @tokens => $_->[1]).'___'!e
113              
114             # keep empty comments after child selectors (IE7 hack)
115             # e.g. html >/**/ body
116             or 0 == length $_->[1]
117             and
118             $css =~ s!>/\*___${MARKER}_PRESERVE_CANDIDATE_COMMENT_$_->[0]___!
119 1         12 '>/*___'.$MARKER.'_PRESERVED_TOKEN_'.(-1+push @tokens => '').'___'!e
120              
121             # \ in the last position looks like hack for Mac/IE5
122             # shorten that to /*\*/ and the next one to /**/
123             or '\\' eq substr $_->[1] => -1
124             and
125             $css =~ s!___${MARKER}_PRESERVE_CANDIDATE_COMMENT_$_->[0]___!
126 2         46 '___'.$MARKER.'_PRESERVED_TOKEN_'.(-1+push @tokens => '\\').'___'!e &&
127             # attention: inline modification
128             ++$_->[0] &&
129             $css =~ s!___${MARKER}_PRESERVE_CANDIDATE_COMMENT_$_->[0]___!
130 2         20 '___'.$MARKER.'_PRESERVED_TOKEN_'.(-1+push @tokens => '').'___'!e
131              
132 36   100     475 for map +[ $_, $comments[$_] ], 0..$#comments;
      66        
      100        
      100        
      33        
      33        
      66        
      66        
133              
134             # in all other cases kill the comment
135 36         327 $css =~ s!/\*___${MARKER}_PRESERVE_CANDIDATE_COMMENT_([0-9]+)___\*/!!g;
136              
137             # Normalize all whitespace strings to single spaces. Easier to work with that way.
138 36         421 $css =~ s!\s+! !g;
139              
140              
141             # From here on all white space is just space - no more multi line matches!
142              
143              
144             # Remove the spaces before the things that should not have spaces before them.
145             # But, be careful not to turn "p :link {...}" into "p:link{...}"
146             # Swap out any pseudo-class colons with the token, and then swap back.
147 36         137 $css =~ s! ( \} [^{:]+ (?:: [^{:]+)+ \{ ) !
148 6         49 $_ = $1,
149             s/:/___${MARKER}_PSEUDOCLASSCOLON___/go,
150             s/\\([\\\$])/\\$1/g,
151             $_
152             !gxe;
153 36         125 $css =~ s! ( ^ [^{:]+ (?:: [^{:]+)+ \{ ) !
154 7         61 $_ = $1,
155             s/:/___${MARKER}_PSEUDOCLASSCOLON___/go,
156             s/\\([\\\$])/\\$1/g,
157             $_
158             !xe;
159              
160             # Similarly, don't strip spaces around addition within calc(10px + 10px) -
161             # swap out any calc()-related plus symbol to a token, and then swap back.
162             # Recursive regular expression is needed to match nested brackets, for
163             # example: `calc(10px + var(--foo))`.
164 36         81 $css =~ s!calc(\((?:[^()]|(?1))*\))!
165 4         47 $_ = $1,
166             s/\+/___${MARKER}_CALCADDITIONSYMBOL___/go,
167             "calc$_"
168             !gxe;
169              
170             # Remove spaces before the things that should not have spaces before them.
171 36         307 $css =~ s/ +([!{};:>+()\],])/$1/g;
172              
173             # bring back the colon
174 36         151 $css =~ s!___${MARKER}_PSEUDOCLASSCOLON___!:!go;
175              
176             # retain space for special IE6 cases
177 36         85 $css =~ s!:first\-(line|letter)([{,])!:first-$1 $2!g;
178              
179             # no space after the end of a preserved comment
180 36         88 $css =~ s!\*/ !*/!g;
181              
182             # If there is a @charset, then only allow one, and push to the top of the file.
183 36         73 $css =~ s!^(.*)(\@charset "[^"]*";)!$2$1!g;
184 36         71 $css =~ s!^( *\@charset [^;]+; *)+!$1!g;
185              
186             # Put the space back in some cases, to support stuff like
187             # @media screen and (-webkit-min-device-pixel-ratio:0){
188             # @supports((display: flex) or (display: -webkit-flex))
189 36         67 $css =~ s! \b and \( !and (!gx;
190 36         62 $css =~ s! \b or \( !or (!gx;
191              
192             # Remove the spaces after the things that should not have spaces after them.
193 36         373 $css =~ s/([!{},;:>+(\[]) +/$1/g;
194              
195             # Swap back the plus sign from calc() addition.
196 36         144 $css =~ s!___${MARKER}_CALCADDITIONSYMBOL___!+!go;
197              
198             # Replace 0.6 to .6, but only when preceded by :
199 36         75 $css =~ s!:0+\.([0-9]+)!:.$1!g;
200              
201             # remove unnecessary semicolons
202 36         145 $css =~ s!;+\}!}!g;
203              
204             # Replace 0(px,em,%) with 0
205 36         90 $css =~ s!([ :]0)(?:px|em|%|in|cm|mm|pc|pt|ex)!$1!g;
206              
207             # Replace 0 0 0 0; with 0.
208 36         123 $css =~ s!:0(?: 0){0,3}(;|})!:0$1!g;
209              
210             # Replace background-position:0; with background-position:0 0;
211             # same for transform-origin
212 36         331 $css =~ s! $RE_BACKGROUND_POSITION :0 ( [;}] ) !background-position:0 0$1!gox;
213 36         300 $css =~ s! $RE_TRANSFORM_ORIGIN_MOZ :0 ( [;}] ) !moz-transform-origin:0 0$1!gox;
214 36         342 $css =~ s! $RE_TRANSFORM_ORIGIN_MS :0 ( [;}] ) !ms-transform-origin:0 0$1!gox;
215 36         256 $css =~ s! $RE_TRANSFORM_ORIGIN_O :0 ( [;}] ) !o-transform-origin:0 0$1!gox;
216 36         311 $css =~ s! $RE_TRANSFORM_ORIGIN_WEBKIT :0 ( [;}] ) !webkit-transform-origin:0 0$1!gox;
217 36         292 $css =~ s! $RE_TRANSFORM_ORIGIN :0 ( [;}] ) !transform-origin:0 0$1!gox;
218              
219             # Replace 0.6 to .6, but only when preceded by : or a white-space
220 36         73 $css =~ s! 0+\.([0-9]+)! .$1!g;
221              
222             # Shorten colors from rgb(51,102,153) to #336699
223             # This makes it more likely that it'll get further compressed in the next step.
224 36         72 $css =~ s!rgb *\( *([0-9, ]+) *\)!
225 2         25 sprintf('#%02x%02x%02x',
226             split(m/ *, */, $1, 3) )
227             !ge;
228              
229             # Shorten colors from #AABBCC to #ABC. Note that we want to make sure
230             # the color is not preceded by either ", " or =. Indeed, the property
231             # filter: chroma(color="#FFFFFF");
232             # would become
233             # filter: chroma(color="#FFF");
234             # which makes the filter break in IE.
235             # We also want to make sure we're only compressing #AABBCC patterns inside
236             # { }, not id selectors ( #FAABAC {} ).
237             # Further we want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD).
238 36         75 $css =~ s!
239             (=[ ]*?["']?)?
240             \#
241             ([0-9a-fA-F]) # a
242             ([0-9a-fA-F]) # a
243             ([0-9a-fA-F]) # b
244             ([0-9a-fA-F]) # b
245             ([0-9a-fA-F]) # c
246             ([0-9a-fA-F]) # c
247             \b
248             ([^{.])
249             !
250 32 100 100     281 ( $1 || '' ) ne ''
    100          
251             # keep as compression will break filters
252             ? $1.'#'.$2.$3.$4.$5.$6.$7.$8
253             # not a filter, safe to compress
254             : '#'.lc(
255             lc $2.$4.$6 eq lc $3.$5.$7
256             ? $2.$4.$6
257             : $2.$3.$4.$5.$6.$7
258             ).$8
259             !gex;
260              
261             # border: none -> border:0
262 36         247 $css =~ s! $RE_BORDER :none ( [;}] ) !border:0$1!gox;
263 36         237 $css =~ s! $RE_BORDER_TOP :none ( [;}] ) !border-top:0$1!gox;
264 36         207 $css =~ s! $RE_BORDER_RIGHT :none ( [;}] ) !border-right:0$1!gox;
265 36         259 $css =~ s! $RE_BORDER_BOTTOM :none ( [;}] ) !border-bottom:0$1!gox;
266 36         213 $css =~ s! $RE_BORDER_LEFT :none ( [;}] ) !border-left:0$1!gox;
267 36         200 $css =~ s! $RE_OUTLINE :none ( [;}] ) !outline:0$1!gox;
268 36         212 $css =~ s! $RE_BACKGROUND :none ( [;}] ) !background:0$1!gox;
269              
270             # shorter opacity IE filter
271 36         467 $css =~ s!$RE_ALPHA_FILTER!alpha(opacity=!go;
272              
273             # Remove empty rules.
274 36         104 $css =~ s![^{}/;]+\{\}!!g;
275              
276             # Replace multiple semi-colons in a row by a single one
277             # See SF bug #1980989
278 36         64 $css =~ s!;;+!;!g;
279              
280             # restore preserved comments and strings
281 36         253 $css =~ s!___${MARKER}_PRESERVED_TOKEN_([0-9]+)___!$tokens[$1]!go;
282              
283             # Trim the final string (for any leading or trailing white spaces)
284 36         100 $css =~ s!\A +!!;
285 36         115 $css =~ s! +\z!!;
286              
287 36         163 $css;
288             }
289              
290             1;
291              
292             __END__