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   168063 use 5.008009;
  5         63  
4 5     5   22 use warnings;
  5         8  
  5         112  
5 5     5   19 use strict;
  5         8  
  5         99  
6 5     5   19 use Carp;
  5         8  
  5         254  
7 5     5   2069 use Regexp::RegGrp;
  5         17363  
  5         1536  
8              
9             our $VERSION = '2.09';
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 = '\/\*((?>[^*]|\*[^/])*(?:license|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   36 no strict 'refs';
  5         10  
  5         10831  
55              
56             foreach my $reggrp ( @REGGRPS ) {
57             next if defined *{ __PACKAGE__ . '::reggrp_' . $reggrp }{CODE};
58              
59             *{ __PACKAGE__ . '::reggrp_' . $reggrp } = sub {
60 123     123   169 my ( $self ) = shift;
61              
62 123         472 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 67     67   135 my ( $self, $value ) = @_;
71              
72 67 100       128 $self->{'_' . $field} = $value ? 1 : undef if ( defined( $value ) );
    100          
73              
74 67         152 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 73     73   107 my ( $self, $value ) = @_;
84              
85 73 100 66     159 if ( defined( $value ) and not ref( $value ) ) {
86 29         142 $value =~ s/^\s*|\s*$//gs;
87 29         74 $self->{'_' . $field} = $value;
88             }
89              
90 73         88 my $ret = '';
91              
92 73 100       141 if ( $self->{'_' . $field} ) {
93 13         29 $ret = '/* ' . $self->{'_' . $field} . ' */' . "\n";
94             }
95              
96 73         148 return $ret;
97             };
98             }
99             }
100              
101             sub compress {
102 52     52 1 1010 my ( $self, $value ) = @_;
103              
104 52 100       128 if ( defined( $value ) ) {
105 21 100       71 if ( grep( $value eq $_, @COMPRESS ) ) {
    50          
106 20         29 $self->{_compress} = $value;
107             }
108             elsif ( ! $value ) {
109 0         0 $self->{_compress} = undef;
110             }
111             }
112              
113 52   66     103 $self->{_compress} ||= $DEFAULT_COMPRESS;
114              
115 52         118 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 11089 my $class = shift;
131 16         31 my $self = {};
132              
133 16         28 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   164 return $_[0]->{submatches}->[0] . '(' . $_[0]->{submatches}->[1] . ')';
146             }
147             },
148             {
149 16         174 regexp => $WHITESPACES,
150             replacement => ''
151             }
152             ];
153              
154             $self->{whitespaces}->{reggrp_data} = [
155             {
156 16         46 regexp => $WHITESPACES,
157             replacement => ''
158             }
159             ];
160              
161             $self->{url}->{reggrp_data} = [
162             {
163             regexp => $URL,
164             replacement => sub {
165 5     5   774 my $url = $_[0]->{submatches}->[0];
166              
167 5         14 return 'url(' . $url . ')';
168             }
169             }
170 16         57 ];
171              
172             $self->{import}->{reggrp_data} = [
173             {
174             regexp => $IMPORT,
175             replacement => sub {
176 5     5   1656 my $submatches = $_[0]->{submatches};
177 5         9 my $url = $submatches->[0];
178 5         7 my $mediatype = $submatches->[2];
179              
180 5         7 my $compress = $global_compress;
181 5         14 $reggrp_url->exec( \$url );
182              
183 5         78 $mediatype =~ s/^\s*|\s*$//gs;
184 5         8 $mediatype =~ s/\s*,\s*/,/gsm;
185              
186 5 50       31 return '@import ' . $url . ( $mediatype ? ( ' ' . $mediatype ) : '' ) . ';' . ( $compress eq 'pretty' ? "\n" : '' );
    100          
187             }
188             }
189 16         73 ];
190              
191             $self->{declaration}->{reggrp_data} = [
192             {
193             regexp => $DECLARATION,
194             replacement => sub {
195 84     84   8635 my $submatches = $_[0]->{submatches};
196 84         119 my $key = $submatches->[0];
197 84         104 my $value = $submatches->[1];
198              
199 84         91 my $compress = $global_compress;
200              
201 84         520 $key =~ s/^\s*|\s*$//gs;
202 84         267 $value =~ s/^\s*|\s*$//gs;
203              
204 84 100       171 if ( $key eq 'content' ) {
205 11         28 $reggrp_content_value->exec( \$value );
206             }
207             else {
208 73         87 $value =~ s/\s*,\s*/,/gsm;
209 73         91 $value =~ s/\s+/ /gsm;
210             }
211              
212 84 50 33     1955 return '' if ( not $key or $value eq '' );
213              
214 84 100       249 return $indent . $key . ':' . $value . ';' . ( $compress eq 'pretty' ? "\n" : '' );
215             }
216             }
217 16         59 ];
218              
219             $self->{rule}->{reggrp_data} = [
220             {
221             regexp => $RULE,
222             replacement => sub {
223 44     44   3728 my $submatches = $_[0]->{submatches};
224 44         64 my $selector = $submatches->[0];
225 44         53 my $declaration = $submatches->[1];
226              
227 44         49 my $compress = $global_compress;
228              
229 44         215 $selector =~ s/^\s*|\s*$//gs;
230 44         69 $selector =~ s/\s*,\s*/,/gsm;
231 44         64 $selector =~ s/\s+/ /gsm;
232              
233 44         271 $declaration =~ s/^\s*|\s*$//gs;
234              
235 44         109 $reggrp_declaration->exec( \$declaration );
236              
237 44 100       1027 my $store = $selector . '{' . ( $compress eq 'pretty' ? "\n" : '' ) . $declaration . '}' .
    100          
238             ( $compress eq 'pretty' ? "\n" : '' );
239              
240 44 50 33     80 $store = '' unless ( $selector or $declaration );
241              
242 44         83 return $store;
243             }
244             }
245 16         64 ];
246              
247             $self->{mediarules}->{reggrp_data} = [
248 16         25 @{$self->{import}->{reggrp_data}},
249 16         26 @{$self->{rule}->{reggrp_data}},
250 16         26 @{$self->{whitespaces}->{reggrp_data}}
  16         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   581 my $original = $_[0]->{match};
268 3         11 my ($selector) = ( $original =~ /\@(\S+)/ );
269              
270 3         5 my $submatches = $_[0]->{submatches};
271 3         24 my $mediatype = $submatches->[0];
272 3         4 my $mediarules = $submatches->[1];
273              
274 3         4 my $compress = $global_compress;
275              
276 3         15 $mediatype =~ s/^\s*|\s*$//gs;
277 3         5 $mediatype =~ s/\s*,\s*/,/gsm;
278              
279 3         9 $reggrp_mediarules->exec( \$mediarules );
280              
281 3 50       202 return "\@$selector " . $mediatype . '{' . ( $compress eq 'pretty' ? "\n" : '' ) .
    50          
282             $mediarules . '}' . ( $compress eq 'pretty' ? "\n" : '' );
283             }
284             },
285 16         98 @{$self->{mediarules}->{reggrp_data}}
  16         49  
286             ];
287              
288              
289             map {
290 16         40 $self->{ '_reggrp_' . $_ } = Regexp::RegGrp->new(
291             {
292             reggrp => $self->{$_}->{reggrp_data}
293             }
294 128         19727 );
295             } @REGGRPS;
296              
297 16         7895 return $self;
298             }
299              
300             sub minify {
301 25     25 0 2950 my ( $self, $input, $opts );
302              
303 25 50 33     115 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 25         44 ( $self, $input, $opts ) = @_;
315             }
316              
317 25 50       56 if ( ref( $input ) ne 'SCALAR' ) {
318 0         0 carp( 'First argument must be a scalarref!' );
319 0         0 return undef;
320             }
321              
322 25         31 my $css = \'';
323 25         34 my $cont = 'void';
324              
325 25 50       38 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 25 50       57 $css = ref( $input ) ? $input : \$input;
333             }
334              
335 25 100       55 if ( ref( $opts ) eq 'HASH' ) {
336 22         38 foreach my $field ( @BOOLEAN_ACCESSORS ) {
337 44 100       94 $self->$field( $opts->{$field} ) if ( defined( $opts->{$field} ) );
338             }
339              
340 22         29 foreach my $field ( 'compress', 'copyright' ) {
341 44 100       108 $self->$field( $opts->{$field} ) if ( defined( $opts->{$field} ) );
342             }
343              
344 22 100 66     62 $indent = $opts->{indent} && ( !$opts->{compress} || $opts->{compress} eq 'pretty' ) ? ' ' x $opts->{indent} : '';
345             }
346              
347             # (re)initialize variables used in the closures
348 25         43 $reggrp_url = $self->reggrp_url;
349 25         44 $reggrp_declaration = $self->reggrp_declaration;
350 25         41 $reggrp_mediarules = $self->reggrp_mediarules;
351 25         130 $reggrp_content_value = $self->reggrp_content_value;
352 25         38 $global_compress = $self->compress;
353              
354 25         30 my $copyright_comment = '';
355              
356 25 100       27 if ( ${$css} =~ /$COPYRIGHT_COMMENT/ism ) {
  25         501  
357 4         9 $copyright_comment = $1;
358             }
359             # Resets copyright_comment() if there is no copyright comment
360 25         60 $self->_copyright_comment( $copyright_comment );
361              
362 25 100 100     39 if ( not $self->no_compress_comment() and ${$css} =~ /$PACKER_COMMENT/ ) {
  22         100  
363 2         6 my $compress = $1;
364 2 50       5 if ( $compress eq '_no_compress_' ) {
365 2 50       8 return ( $cont eq 'scalar' ) ? ${$css} : undef;
  0         0  
366             }
367              
368 0         0 $self->compress( $compress );
369             }
370              
371 23         30 ${$css} =~ s/$COMMENT/ /gsm;
  23         163  
372              
373 23         72 $self->reggrp_global()->exec( $css );
374              
375 23 100       1135 if ( not $self->remove_copyright() ) {
376 21   100     37 ${$css} = ( $self->copyright() || $self->_copyright_comment() ) . ${$css};
  21         27  
  21         38  
377             }
378              
379 23 50       73 return ${$css} if ( $cont eq 'scalar' );
  0            
380             }
381              
382             1;
383              
384             __END__