File Coverage

blib/lib/CSS/Packer.pm
Criterion Covered Total %
statement 135 152 88.8
branch 49 68 72.0
condition 15 24 62.5
subroutine 17 19 89.4
pod 1 3 33.3
total 217 266 81.5


line stmt bran cond sub pod time code
1             package CSS::Packer;
2              
3 6     6   195521 use 5.008009;
  6         38  
4 6     6   32 use warnings;
  6         9  
  6         148  
5 6     6   29 use strict;
  6         16  
  6         123  
6 6     6   30 use Carp;
  6         10  
  6         378  
7 6     6   2758 use Regexp::RegGrp;
  6         24042  
  6         1877  
8              
9             our $VERSION = '2.07';
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 6     6   46 no strict 'refs';
  6         20  
  6         14178  
55              
56             foreach my $reggrp ( @REGGRPS ) {
57             next if defined *{ __PACKAGE__ . '::reggrp_' . $reggrp }{CODE};
58              
59             *{ __PACKAGE__ . '::reggrp_' . $reggrp } = sub {
60 108     108   150 my ( $self ) = shift;
61              
62 108         516 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 59     59   118 my ( $self, $value ) = @_;
71              
72 59 100       146 $self->{'_' . $field} = $value ? 1 : undef if ( defined( $value ) );
    100          
73              
74 59         179 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 66     66   121 my ( $self, $value ) = @_;
84              
85 66 100 66     177 if ( defined( $value ) and not ref( $value ) ) {
86 26         154 $value =~ s/^\s*|\s*$//gs;
87 26         83 $self->{'_' . $field} = $value;
88             }
89              
90 66         96 my $ret = '';
91              
92 66 100       149 if ( $self->{'_' . $field} ) {
93 10         24 $ret = '/* ' . $self->{'_' . $field} . ' */' . "\n";
94             }
95              
96 66         213 return $ret;
97             };
98             }
99             }
100              
101             sub compress {
102 48     48 1 1158 my ( $self, $value ) = @_;
103              
104 48 100       103 if ( defined( $value ) ) {
105 20 100       78 if ( grep( $value eq $_, @COMPRESS ) ) {
    50          
106 19         46 $self->{_compress} = $value;
107             }
108             elsif ( ! $value ) {
109 0         0 $self->{_compress} = undef;
110             }
111             }
112              
113 48   66     109 $self->{_compress} ||= $DEFAULT_COMPRESS;
114              
115 48         94 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 15     15 0 12023 my $class = shift;
131 15         33 my $self = {};
132              
133 15         31 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 0     0   0 return $_[0]->{submatches}->[0] . '(' . $_[0]->{submatches}->[0] . ')';
146             }
147             },
148             {
149 15         184 regexp => $WHITESPACES,
150             replacement => ''
151             }
152             ];
153              
154             $self->{whitespaces}->{reggrp_data} = [
155             {
156 15         55 regexp => $WHITESPACES,
157             replacement => ''
158             }
159             ];
160              
161             $self->{url}->{reggrp_data} = [
162             {
163             regexp => $URL,
164             replacement => sub {
165 5     5   929 my $url = $_[0]->{submatches}->[0];
166              
167 5         16 return 'url(' . $url . ')';
168             }
169             }
170 15         66 ];
171              
172             $self->{import}->{reggrp_data} = [
173             {
174             regexp => $IMPORT,
175             replacement => sub {
176 5     5   2108 my $submatches = $_[0]->{submatches};
177 5         10 my $url = $submatches->[0];
178 5         8 my $mediatype = $submatches->[2];
179              
180 5         8 my $compress = $global_compress;
181 5         19 $reggrp_url->exec( \$url );
182              
183 5         96 $mediatype =~ s/^\s*|\s*$//gs;
184 5         9 $mediatype =~ s/\s*,\s*/,/gsm;
185              
186 5 50       28 return '@import ' . $url . ( $mediatype ? ( ' ' . $mediatype ) : '' ) . ';' . ( $compress eq 'pretty' ? "\n" : '' );
    100          
187             }
188             }
189 15         68 ];
190              
191             $self->{declaration}->{reggrp_data} = [
192             {
193             regexp => $DECLARATION,
194             replacement => sub {
195 77     77   9757 my $submatches = $_[0]->{submatches};
196 77         134 my $key = $submatches->[0];
197 77         118 my $value = $submatches->[1];
198              
199 77         111 my $compress = $global_compress;
200              
201 77         399 $key =~ s/^\s*|\s*$//gs;
202 77         320 $value =~ s/^\s*|\s*$//gs;
203              
204 77 100       203 if ( $key eq 'content' ) {
205 10         32 $reggrp_content_value->exec( \$value );
206             }
207             else {
208 67         107 $value =~ s/\s*,\s*/,/gsm;
209 67         118 $value =~ s/\s+/ /gsm;
210             }
211              
212 77 50 33     2400 return '' if ( not $key or $value eq '' );
213              
214 77 100       311 return $indent . $key . ':' . $value . ';' . ( $compress eq 'pretty' ? "\n" : '' );
215             }
216             }
217 15         96 ];
218              
219             $self->{rule}->{reggrp_data} = [
220             {
221             regexp => $RULE,
222             replacement => sub {
223 41     41   4680 my $submatches = $_[0]->{submatches};
224 41         74 my $selector = $submatches->[0];
225 41         65 my $declaration = $submatches->[1];
226              
227 41         67 my $compress = $global_compress;
228              
229 41         261 $selector =~ s/^\s*|\s*$//gs;
230 41         85 $selector =~ s/\s*,\s*/,/gsm;
231 41         87 $selector =~ s/\s+/ /gsm;
232              
233 41         315 $declaration =~ s/^\s*|\s*$//gs;
234              
235 41         136 $reggrp_declaration->exec( \$declaration );
236              
237 41 100       842 my $store = $selector . '{' . ( $compress eq 'pretty' ? "\n" : '' ) . $declaration . '}' .
    100          
238             ( $compress eq 'pretty' ? "\n" : '' );
239              
240 41 50 33     95 $store = '' unless ( $selector or $declaration );
241              
242 41         97 return $store;
243             }
244             }
245 15         85 ];
246              
247             $self->{mediarules}->{reggrp_data} = [
248 15         31 @{$self->{import}->{reggrp_data}},
249 15         41 @{$self->{rule}->{reggrp_data}},
250 15         28 @{$self->{whitespaces}->{reggrp_data}}
  15         42  
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   708 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         6 my $mediarules = $submatches->[1];
273              
274 3         4 my $compress = $global_compress;
275              
276 3         18 $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       253 return "\@$selector " . $mediatype . '{' . ( $compress eq 'pretty' ? "\n" : '' ) .
    50          
282             $mediarules . '}' . ( $compress eq 'pretty' ? "\n" : '' );
283             }
284             },
285 15         89 @{$self->{mediarules}->{reggrp_data}}
  15         61  
286             ];
287              
288              
289             map {
290 15         40 $self->{ '_reggrp_' . $_ } = Regexp::RegGrp->new(
291             {
292             reggrp => $self->{$_}->{reggrp_data}
293             }
294 120         22609 );
295             } @REGGRPS;
296              
297 15         9024 return $self;
298             }
299              
300             sub minify {
301 22     22 0 2722 my ( $self, $input, $opts );
302              
303 22 50 33     132 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 22         51 ( $self, $input, $opts ) = @_;
315             }
316              
317 22 50       58 if ( ref( $input ) ne 'SCALAR' ) {
318 0         0 carp( 'First argument must be a scalarref!' );
319 0         0 return undef;
320             }
321              
322 22         33 my $css = \'';
323 22         30 my $cont = 'void';
324              
325 22 50       44 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 22 50       48 $css = ref( $input ) ? $input : \$input;
333             }
334              
335 22 100       55 if ( ref( $opts ) eq 'HASH' ) {
336 19         37 foreach my $field ( @BOOLEAN_ACCESSORS ) {
337 38 100       97 $self->$field( $opts->{$field} ) if ( defined( $opts->{$field} ) );
338             }
339              
340 19         33 foreach my $field ( 'compress', 'copyright' ) {
341 38 100       123 $self->$field( $opts->{$field} ) if ( defined( $opts->{$field} ) );
342             }
343              
344 19 100 66     68 $indent = $opts->{indent} && ( !$opts->{compress} || $opts->{compress} eq 'pretty' ) ? ' ' x $opts->{indent} : '';
345             }
346              
347             # (re)initialize variables used in the closures
348 22         60 $reggrp_url = $self->reggrp_url;
349 22         51 $reggrp_declaration = $self->reggrp_declaration;
350 22         42 $reggrp_mediarules = $self->reggrp_mediarules;
351 22         47 $reggrp_content_value = $self->reggrp_content_value;
352 22         42 $global_compress = $self->compress;
353              
354 22         37 my $copyright_comment = '';
355              
356 22 100       30 if ( ${$css} =~ /$COPYRIGHT_COMMENT/ism ) {
  22         513  
357 2         8 $copyright_comment = $1;
358             }
359             # Resets copyright_comment() if there is no copyright comment
360 22         71 $self->_copyright_comment( $copyright_comment );
361              
362 22 100 100     51 if ( not $self->no_compress_comment() and ${$css} =~ /$PACKER_COMMENT/ ) {
  19         142  
363 2         7 my $compress = $1;
364 2 50       5 if ( $compress eq '_no_compress_' ) {
365 2 50       7 return ( $cont eq 'scalar' ) ? ${$css} : undef;
  0         0  
366             }
367              
368 0         0 $self->compress( $compress );
369             }
370              
371 20         36 ${$css} =~ s/$COMMENT/ /gsm;
  20         200  
372              
373 20         67 $self->reggrp_global()->exec( $css );
374              
375 20 100       1320 if ( not $self->remove_copyright() ) {
376 19   100     41 ${$css} = ( $self->copyright() || $self->_copyright_comment() ) . ${$css};
  19         38  
  19         45  
377             }
378              
379 20 50       77 return ${$css} if ( $cont eq 'scalar' );
  0            
380             }
381              
382             1;
383              
384             __END__