File Coverage

blib/lib/CSS/Compressor.pm
Criterion Covered Total %
statement 67 67 100.0
branch 4 4 100.0
condition 15 23 65.2
subroutine 5 5 100.0
pod 1 1 100.0
total 92 100 92.0


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