File Coverage

blib/lib/CSS/Packer.pm
Criterion Covered Total %
statement 136 152 89.4
branch 49 68 72.0
condition 15 24 62.5
subroutine 18 19 94.7
pod 1 3 33.3
total 219 266 82.3


line stmt bran cond sub pod time code
1             package CSS::Packer;
2              
3 5     5   214271 use 5.008009;
  5         36  
4 5     5   28 use warnings;
  5         10  
  5         148  
5 5     5   26 use strict;
  5         9  
  5         130  
6 5     5   27 use Carp;
  5         6  
  5         346  
7 5     5   2622 use Regexp::RegGrp;
  5         21712  
  5         1775  
8              
9             our $VERSION = '2.08';
10              
11             our @COMPRESS = ( 'minify', 'pretty' );
12             our $DEFAULT_COMPRESS = 'pretty';
13              
14             our @BOOLEAN_ACCESSORS = (
15             'no_compress_comment',
16             'remove_copyright'
17             );
18              
19             our @COPYRIGHT_ACCESSORS = (
20             'copyright',
21             'copyright_comment'
22             );
23              
24             our $COPYRIGHT_COMMENT = '\/\*((?>[^*]|\*[^/])*copyright(?>[^*]|\*[^/])*)\*\/';
25              
26             our $DICTIONARY = {
27             'STRING1' => qr~"(?>(?:(?>[^"\\]+)|\\.|\\"|\\\s)*)"~,
28             'STRING2' => qr~'(?>(?:(?>[^'\\]+)|\\.|\\'|\\\s)*)'~
29             };
30              
31             our $WHITESPACES = '\s+';
32              
33             our $RULE = '([^{};]+)\{([^{}]*)\}';
34              
35             our $URL = 'url\(\s*(' . $DICTIONARY->{STRING1} . '|' . $DICTIONARY->{STRING2} . '|[^\'"\s]+?)\s*\)';
36              
37             our $IMPORT = '\@import\s+(' . $DICTIONARY->{STRING1} . '|' . $DICTIONARY->{STRING2} . '|' . $URL . ')([^;]*);';
38              
39             our $AT_RULE = '\@\S+([^{}]+)\{((?:' . $IMPORT . '|' . $RULE . '|' . $WHITESPACES . ')+)\}';
40              
41             our $DECLARATION = '((?>[^;:]+)):(?<=:)((?>[^;]*))(?:;(?!(charset|base64))|\s*$)';
42              
43             our $COMMENT = '(\/\*[^*]*\*+([^/][^*]*\*+)*\/)';
44              
45             our $PACKER_COMMENT = '\/\*\s*CSS::Packer\s*(\w+)\s*\*\/';
46              
47             our $CHARSET = '^(\@charset)\s+(' . $DICTIONARY->{STRING1} . '|' . $DICTIONARY->{STRING2} . ');';
48              
49             our @REGGRPS = ( 'whitespaces', 'url', 'import', 'declaration', 'rule', 'content_value', 'mediarules', 'global' );
50              
51             # --------------------------------------------------------------------------- #
52              
53             {
54 5     5   49 no strict 'refs';
  5         10  
  5         12567  
55              
56             foreach my $reggrp ( @REGGRPS ) {
57             next if defined *{ __PACKAGE__ . '::reggrp_' . $reggrp }{CODE};
58              
59             *{ __PACKAGE__ . '::reggrp_' . $reggrp } = sub {
60 113     113   169 my ( $self ) = shift;
61              
62 113         576 return $self->{ '_reggrp_' . $reggrp };
63             };
64             }
65              
66             foreach my $field ( @BOOLEAN_ACCESSORS ) {
67             next if defined *{ __PACKAGE__ . '::' . $field }{CODE};
68              
69             *{ __PACKAGE__ . '::' . $field} = sub {
70 61     61   121 my ( $self, $value ) = @_;
71              
72 61 100       142 $self->{'_' . $field} = $value ? 1 : undef if ( defined( $value ) );
    100          
73              
74 61         186 return $self->{'_' . $field};
75             };
76             }
77              
78             foreach my $field ( @COPYRIGHT_ACCESSORS ) {
79             $field = '_' . $field if ( $field eq 'copyright_comment' );
80             next if defined *{ __PACKAGE__ . '::' . $field }{CODE};
81              
82             *{ __PACKAGE__ . '::' . $field} = sub {
83 69     69   128 my ( $self, $value ) = @_;
84              
85 69 100 66     201 if ( defined( $value ) and not ref( $value ) ) {
86 27         154 $value =~ s/^\s*|\s*$//gs;
87 27         87 $self->{'_' . $field} = $value;
88             }
89              
90 69         104 my $ret = '';
91              
92 69 100       156 if ( $self->{'_' . $field} ) {
93 10         24 $ret = '/* ' . $self->{'_' . $field} . ' */' . "\n";
94             }
95              
96 69         175 return $ret;
97             };
98             }
99             }
100              
101             sub compress {
102 50     50 1 1173 my ( $self, $value ) = @_;
103              
104 50 100       111 if ( defined( $value ) ) {
105 21 100       81 if ( grep( $value eq $_, @COMPRESS ) ) {
    50          
106 20         44 $self->{_compress} = $value;
107             }
108             elsif ( ! $value ) {
109 0         0 $self->{_compress} = undef;
110             }
111             }
112              
113 50   66     110 $self->{_compress} ||= $DEFAULT_COMPRESS;
114              
115 50         106 return $self->{_compress};
116             }
117              
118             # these variables are used in the closures defined in the init function
119             # below - we have to use globals as using $self within the closures leads
120             # to a reference cycle and thus memory leak, and we can't scope them to
121             # the init method as they may change. they are set by the minify sub
122             our $reggrp_url;
123             our $reggrp_declaration;
124             our $reggrp_mediarules;
125             our $reggrp_content_value;
126             our $global_compress;
127             our $indent = '';
128              
129             sub init {
130 16     16 0 13998 my $class = shift;
131 16         35 my $self = {};
132              
133 16         35 bless( $self, $class );
134              
135             $self->{content_value}->{reggrp_data} = [
136             {
137             regexp => $DICTIONARY->{STRING1}
138             },
139             {
140             regexp => $DICTIONARY->{STRING2}
141             },
142             {
143             regexp => qr~([\w-]+)\(\s*([\w-]+)\s*\)~,
144             replacement => sub {
145 1     1   170 return $_[0]->{submatches}->[0] . '(' . $_[0]->{submatches}->[1] . ')';
146             }
147             },
148             {
149 16         205 regexp => $WHITESPACES,
150             replacement => ''
151             }
152             ];
153              
154             $self->{whitespaces}->{reggrp_data} = [
155             {
156 16         58 regexp => $WHITESPACES,
157             replacement => ''
158             }
159             ];
160              
161             $self->{url}->{reggrp_data} = [
162             {
163             regexp => $URL,
164             replacement => sub {
165 5     5   937 my $url = $_[0]->{submatches}->[0];
166              
167 5         16 return 'url(' . $url . ')';
168             }
169             }
170 16         71 ];
171              
172             $self->{import}->{reggrp_data} = [
173             {
174             regexp => $IMPORT,
175             replacement => sub {
176 5     5   2168 my $submatches = $_[0]->{submatches};
177 5         12 my $url = $submatches->[0];
178 5         9 my $mediatype = $submatches->[2];
179              
180 5         9 my $compress = $global_compress;
181 5         19 $reggrp_url->exec( \$url );
182              
183 5         97 $mediatype =~ s/^\s*|\s*$//gs;
184 5         11 $mediatype =~ s/\s*,\s*/,/gsm;
185              
186 5 50       29 return '@import ' . $url . ( $mediatype ? ( ' ' . $mediatype ) : '' ) . ';' . ( $compress eq 'pretty' ? "\n" : '' );
    100          
187             }
188             }
189 16         76 ];
190              
191             $self->{declaration}->{reggrp_data} = [
192             {
193             regexp => $DECLARATION,
194             replacement => sub {
195 78     78   9743 my $submatches = $_[0]->{submatches};
196 78         124 my $key = $submatches->[0];
197 78         174 my $value = $submatches->[1];
198              
199 78         102 my $compress = $global_compress;
200              
201 78         421 $key =~ s/^\s*|\s*$//gs;
202 78         475 $value =~ s/^\s*|\s*$//gs;
203              
204 78 100       213 if ( $key eq 'content' ) {
205 11         33 $reggrp_content_value->exec( \$value );
206             }
207             else {
208 67         116 $value =~ s/\s*,\s*/,/gsm;
209 67         117 $value =~ s/\s+/ /gsm;
210             }
211              
212 78 50 33     2472 return '' if ( not $key or $value eq '' );
213              
214 78 100       302 return $indent . $key . ':' . $value . ';' . ( $compress eq 'pretty' ? "\n" : '' );
215             }
216             }
217 16         72 ];
218              
219             $self->{rule}->{reggrp_data} = [
220             {
221             regexp => $RULE,
222             replacement => sub {
223 42     42   4525 my $submatches = $_[0]->{submatches};
224 42         80 my $selector = $submatches->[0];
225 42         65 my $declaration = $submatches->[1];
226              
227 42         63 my $compress = $global_compress;
228              
229 42         260 $selector =~ s/^\s*|\s*$//gs;
230 42         86 $selector =~ s/\s*,\s*/,/gsm;
231 42         77 $selector =~ s/\s+/ /gsm;
232              
233 42         320 $declaration =~ s/^\s*|\s*$//gs;
234              
235 42         133 $reggrp_declaration->exec( \$declaration );
236              
237 42 100       896 my $store = $selector . '{' . ( $compress eq 'pretty' ? "\n" : '' ) . $declaration . '}' .
    100          
238             ( $compress eq 'pretty' ? "\n" : '' );
239              
240 42 50 33     104 $store = '' unless ( $selector or $declaration );
241              
242 42         92 return $store;
243             }
244             }
245 16         99 ];
246              
247             $self->{mediarules}->{reggrp_data} = [
248 16         36 @{$self->{import}->{reggrp_data}},
249 16         44 @{$self->{rule}->{reggrp_data}},
250 16         30 @{$self->{whitespaces}->{reggrp_data}}
  16         40  
251             ];
252              
253             $self->{global}->{reggrp_data} = [
254             {
255             regexp => $CHARSET,
256             replacement => sub {
257 0     0   0 my $submatches = $_[0]->{submatches};
258              
259 0         0 my $compress = $global_compress;
260              
261 0 0       0 return $submatches->[0] . " " . $submatches->[1] . ( $compress eq 'pretty' ? "\n" : '' );
262             }
263             },
264             {
265             regexp => $AT_RULE,
266             replacement => sub {
267 3     3   780 my $original = $_[0]->{match};
268 3         13 my ($selector) = ( $original =~ /\@(\S+)/ );
269              
270 3         6 my $submatches = $_[0]->{submatches};
271 3         28 my $mediatype = $submatches->[0];
272 3         5 my $mediarules = $submatches->[1];
273              
274 3         4 my $compress = $global_compress;
275              
276 3         19 $mediatype =~ s/^\s*|\s*$//gs;
277 3         7 $mediatype =~ s/\s*,\s*/,/gsm;
278              
279 3         9 $reggrp_mediarules->exec( \$mediarules );
280              
281 3 50       267 return "\@$selector " . $mediatype . '{' . ( $compress eq 'pretty' ? "\n" : '' ) .
    50          
282             $mediarules . '}' . ( $compress eq 'pretty' ? "\n" : '' );
283             }
284             },
285 16         105 @{$self->{mediarules}->{reggrp_data}}
  16         54  
286             ];
287              
288              
289             map {
290 16         41 $self->{ '_reggrp_' . $_ } = Regexp::RegGrp->new(
291             {
292             reggrp => $self->{$_}->{reggrp_data}
293             }
294 128         23765 );
295             } @REGGRPS;
296              
297 16         9506 return $self;
298             }
299              
300             sub minify {
301 23     23 0 2441 my ( $self, $input, $opts );
302              
303 23 50 33     138 unless (
304             ref( $_[0] ) and
305             ref( $_[0] ) eq __PACKAGE__
306             ) {
307 0         0 $self = __PACKAGE__->init();
308              
309 0 0       0 shift( @_ ) unless ( ref( $_[0] ) );
310              
311 0         0 ( $input, $opts ) = @_;
312             }
313             else {
314 23         56 ( $self, $input, $opts ) = @_;
315             }
316              
317 23 50       60 if ( ref( $input ) ne 'SCALAR' ) {
318 0         0 carp( 'First argument must be a scalarref!' );
319 0         0 return undef;
320             }
321              
322 23         40 my $css = \'';
323 23         30 my $cont = 'void';
324              
325 23 50       51 if ( defined( wantarray ) ) {
326 0 0       0 my $tmp_input = ref( $input ) ? ${$input} : $input;
  0         0  
327              
328 0         0 $css = \$tmp_input;
329 0         0 $cont = 'scalar';
330             }
331             else {
332 23 50       46 $css = ref( $input ) ? $input : \$input;
333             }
334              
335 23 100       57 if ( ref( $opts ) eq 'HASH' ) {
336 20         41 foreach my $field ( @BOOLEAN_ACCESSORS ) {
337 40 100       103 $self->$field( $opts->{$field} ) if ( defined( $opts->{$field} ) );
338             }
339              
340 20         37 foreach my $field ( 'compress', 'copyright' ) {
341 40 100       139 $self->$field( $opts->{$field} ) if ( defined( $opts->{$field} ) );
342             }
343              
344 20 100 66     78 $indent = $opts->{indent} && ( !$opts->{compress} || $opts->{compress} eq 'pretty' ) ? ' ' x $opts->{indent} : '';
345             }
346              
347             # (re)initialize variables used in the closures
348 23         57 $reggrp_url = $self->reggrp_url;
349 23         50 $reggrp_declaration = $self->reggrp_declaration;
350 23         49 $reggrp_mediarules = $self->reggrp_mediarules;
351 23         49 $reggrp_content_value = $self->reggrp_content_value;
352 23         45 $global_compress = $self->compress;
353              
354 23         42 my $copyright_comment = '';
355              
356 23 100       28 if ( ${$css} =~ /$COPYRIGHT_COMMENT/ism ) {
  23         498  
357 2         7 $copyright_comment = $1;
358             }
359             # Resets copyright_comment() if there is no copyright comment
360 23         73 $self->_copyright_comment( $copyright_comment );
361              
362 23 100 100     59 if ( not $self->no_compress_comment() and ${$css} =~ /$PACKER_COMMENT/ ) {
  20         162  
363 2         7 my $compress = $1;
364 2 50       8 if ( $compress eq '_no_compress_' ) {
365 2 50       9 return ( $cont eq 'scalar' ) ? ${$css} : undef;
  0         0  
366             }
367              
368 0         0 $self->compress( $compress );
369             }
370              
371 21         45 ${$css} =~ s/$COMMENT/ /gsm;
  21         203  
372              
373 21         59 $self->reggrp_global()->exec( $css );
374              
375 21 100       1390 if ( not $self->remove_copyright() ) {
376 20   100     45 ${$css} = ( $self->copyright() || $self->_copyright_comment() ) . ${$css};
  20         36  
  20         43  
377             }
378              
379 21 50       85 return ${$css} if ( $cont eq 'scalar' );
  0            
380             }
381              
382             1;
383              
384             __END__