File Coverage

lib/CSS/Inliner/Parser.pm
Criterion Covered Total %
statement 132 156 84.6
branch 32 46 69.5
condition 20 42 47.6
subroutine 16 18 88.8
pod 9 9 100.0
total 209 271 77.1


line stmt bran cond sub pod time code
1             # Based in large part on the CSS::Tiny CPAN Module
2             # http://search.cpan.org/~adamk/CSS-Tiny/
3             #
4             # This is version 2 of this module, which concerns itself with very strictly preserving ordering of rules,
5             # something that has been the focus of this module series from the beginning. We focus more on preservation
6             # of rule ordering than we do on ease of modifying enclosed rules. If you are attempting to modify
7             # rules through an API please see CSS::Simple
8              
9             package CSS::Inliner::Parser;
10              
11 31     31   8733 use strict;
  31         76  
  31         943  
12 31     31   184 use warnings;
  31         71  
  31         813  
13              
14 31     31   156 use Carp;
  31         65  
  31         2071  
15              
16 31     31   3355 use Storable qw(dclone);
  31         16738  
  31         2786  
17              
18             =pod
19              
20             =head1 NAME
21              
22             CSS::Inliner::Parser - Interface through which to read/write CSS files while respecting the cascade order
23              
24             NOTE: This sub-module very seriously focuses on respecting cascade order. As such this module is not for you
25             if you want to modified a stylesheet once it's read. If you are looking for that functionality you may
26             want to look at the sister module, CSS::Simple
27              
28             =head1 SYNOPSIS
29              
30             use CSS::Inliner::Parser;
31              
32             my $css = new CSS::Inliner::Parser();
33              
34             $css->read({ filename => 'input.css' });
35              
36             #perform manipulations...
37              
38             $css->write({ filename => 'output.css' });
39              
40             =head1 DESCRIPTION
41              
42             Class for reading and writing CSS. Unlike other CSS classes on CPAN this particular module
43             focuses on respecting the order of selectors. This is very useful for things like... inlining
44             CSS, or for similar "strict" CSS work.
45              
46             =cut
47              
48             BEGIN {
49 31     31   201 my $members = ['ordered','stylesheet','warns_as_errors','content_warnings'];
50              
51             #generate all the getter/setter we need
52 31         75 foreach my $member (@{$members}) {
  31         111  
53 31     31   225 no strict 'refs';
  31         66  
  31         3254  
54              
55 124         79193 *{'_' . $member} = sub {
56 300     300   487 my ($self,$value) = @_;
57              
58 300         572 $self->_check_object();
59              
60 300 100       553 $self->{$member} = $value if defined($value);
61              
62 300         844 return $self->{$member};
63             }
64 124         626 }
65             }
66              
67              
68             =pod
69              
70             =head1 CONSTRUCTOR
71              
72             =over 4
73              
74             =item new ([ OPTIONS ])
75              
76             Instantiates the CSS::Inliner::Parser object. Sets up class variables that are used during file parsing/processing.
77              
78             B (optional). Boolean value to indicate whether fatal errors should occur during parse failures.
79              
80             =back
81              
82             =cut
83              
84             sub new {
85 35     35 1 3778 my ($proto, $params) = @_;
86              
87 35   33     183 my $class = ref($proto) || $proto;
88              
89 35         75 my $rules = [];
90 35         70 my $selectors = {};
91              
92             my $self = {
93             stylesheet => undef,
94             ordered => $rules,
95             selectors => $selectors,
96             content_warnings => undef,
97 35 100 66     298 warns_as_errors => (defined($$params{warns_as_errors}) && $$params{warns_as_errors}) ? 1 : 0
98             };
99              
100 35         82 bless $self, $class;
101 35         528 return $self;
102             }
103              
104             =head1 METHODS
105              
106             =cut
107              
108             =pod
109              
110             =over 4
111              
112             =item read_file( params )
113              
114             Opens and reads a CSS file, then subsequently performs the parsing of the CSS file
115             necessary for later manipulation.
116              
117             This method requires you to pass in a params hash that contains a
118             filename argument. For example:
119              
120             $self->read_file({ filename => 'myfile.css' });
121              
122             =cut
123              
124             sub read_file {
125 0     0 1 0 my ($self,$params) = @_;
126              
127 0         0 $self->_check_object();
128              
129 0 0 0     0 unless ($params && $$params{filename}) {
130 0         0 croak "You must pass in hash params that contain a filename argument";
131             }
132              
133 0 0       0 open FILE, "<", $$params{filename} or croak $!;
134 0         0 my $css = do { local( $/ ) ; } ;
  0         0  
  0         0  
135              
136 0         0 $self->read({ css => $css });
137              
138 0         0 return();
139             }
140              
141             =pod
142              
143             =item read( params )
144              
145             Reads css data and parses it. The intermediate data is stored in class variables.
146              
147             Compound selectors (i.e. "a, span") are split apart during parsing and stored
148             separately, so the output of any given stylesheet may not match the output 100%, but the
149             rules themselves should apply as expected.
150              
151             This method requires you to pass in a params hash that contains scalar
152             css data. For example:
153              
154             $self->read({ css => $css });
155              
156             =cut
157              
158             sub read {
159 33     33 1 193 my ($self,$params) = @_;
160              
161 33         144 $self->_check_object();
162              
163 33         174 $self->_content_warnings({}); # overwrite any existing warnings
164              
165 33 50       114 unless (exists $$params{css}) {
166 0         0 croak 'You must pass in hash params that contains the css data';
167             }
168              
169 33 100 66     218 if ($params && $$params{css}) {
170             # Flatten whitespace and remove /* comment */ style comments
171 32         78 my $string = $$params{css};
172 32         153 $string =~ tr/\n\t/ /;
173 32         123 $string =~ s!/\*.*?\*\/!!g;
174              
175             # Split into styles
176 32         1096 my @tokens = grep { /\S/ } (split /(?<=\})/, $string);
  246         578  
177 32         151 while (my $token = shift @tokens) {
178 194 100       1555 if ($token =~ /^\s*@[\w-]+\s+(?:url\()?"/) {
    100          
    100          
    50          
179             # simple at-rules consisting of a rule name and prelude, but no block - we have to jump through some
180             # hoops as we can accidentally capture multi-line rules here. If such a thing happens we capture
181             # any inadvertently trapped content and push it back for parsing later
182            
183 6         10 my $atrule = $token;
184            
185 6         25 $atrule =~ /^\s*(@[\w-]+)\s*((?:url\()?"[^;]*;)(.*)/;
186            
187 6         32 $self->add_at_rule({ type => $1, prelude => $2, block => undef });
188            
189 6         25 unshift(@tokens, $3);
190             }
191             elsif ($token =~ /^\s*(\@[\w-]+)\s*{\s*([^{]*)}$/) {
192             # multiline at-rules without a prelude, nothing to protect here
193              
194 2         11 $self->add_at_rule({ type => $1, prelude => undef, block => $2 });
195             }
196             elsif ($token =~ /^\s*\@/) {
197             # multiline at-rules with a prelude, nothing to protect here
198              
199 11         23 my $atrule = $token;
200              
201 11         33 for (my $attoken = shift(@tokens); defined($attoken); $attoken = shift(@tokens)) {
202 28 100       77 if ($attoken !~ /^\s*\}\s*$/) {
203 17         55 $atrule .= "\n$attoken\n";
204             }
205             else {
206 11         24 last;
207             }
208             }
209              
210 11         50 $atrule =~ /^\s*(@[\w-]+)\s*([^{]*)\{\s*(.*?})$/s;
211              
212 11         71 $self->add_at_rule({ type => $1, prelude => $2, block => $3 });
213             }
214             elsif ($token =~ /^\s*([^{]+?)\s*{\s*(.*)}\s*$/) {
215             # Split in such a way as to support grouped styles
216              
217 175         362 my $rule = $1;
218 175         296 my $props = $2;
219              
220 175         315 $rule =~ s/\s{2,}/ /g;
221              
222             # Split into properties
223 175         239 my $properties = [];
224 175         456 foreach (grep { /\S/ } split /\;/, $props) {
  434         1000  
225             # skip over browser specific properties
226 290 100 100     1106 if ((/^\s*[\*\-\_]/) || (/\\/)) {
227 4         7 next;
228             }
229              
230             # check if properties are valid, reporting error as configured
231 286 100       1093 unless (/^\s*([\w._-]+)\s*:\s*(.*?)\s*$/) {
232 3         15 $self->_report_warning({ info => "Invalid or unexpected property '$_' in style '$rule'" });
233 2         6 next;
234             }
235              
236             #store the property for later
237 283         918 push @$properties, lc $1, $2;
238             }
239              
240 174         446 my @selectors = split /,/, $rule; # break the rule into the component selector(s)
241              
242             #apply the found rules to each selector
243 174         283 foreach my $selector (@selectors) {
244 184         666 $selector =~ s/^\s+|\s+$//g;
245              
246 184         575 $self->add_qualified_rule({ selector => $selector, declarations => $properties });
247             }
248             }
249             else {
250 0         0 $self->_report_warning({ info => "Invalid or unexpected style data '$_'" });
251             }
252             }
253             }
254             else {
255 1         5 $self->_report_warning({ info => 'No stylesheet data was found in the document'});
256             }
257              
258 32         114 return();
259             }
260              
261             =pod
262              
263             =item write_file()
264              
265             Write the parsed and manipulated CSS out to a file parameter
266              
267             This method requires you to pass in a params hash that contains a
268             filename argument. For example:
269              
270             $self->write_file({ filename => 'myfile.css' });
271              
272             =cut
273              
274             sub write_file {
275 0     0 1 0 my ($self,$params) = @_;
276              
277 0         0 $self->_check_object();
278              
279 0 0       0 unless (exists $$params{filename}) {
280 0         0 croak "No filename specified for write operation";
281             }
282              
283             # Write the file
284 0 0       0 open( CSS, '>'. $$params{filename} ) or croak "Failed to open file '$$params{filename}' for writing: $!";
285 0         0 print CSS $self->write();
286 0         0 close( CSS );
287              
288 0         0 return();
289             }
290              
291             =pod
292              
293             =item write()
294              
295             Write the parsed and manipulated CSS out to a scalar and return it
296              
297             This code makes some assumptions about the nature of the prelude and data portions of the stored css rules
298             and possibly is insufficient.
299              
300             =cut
301              
302             sub write {
303 4     4 1 24 my ($self,$params) = @_;
304              
305 4         8 $self->_check_object();
306              
307 4         5 my $contents = '';
308              
309 4         4 foreach my $rule ( @{$self->_ordered()} ) {
  4         8  
310 20 100 66     101 if ($$rule{selector} && $$rule{declarations}) {
    100 66        
    100 66        
    50 33        
      33        
311             #grab the properties that make up this particular selector
312 12         16 my $selector = $$rule{selector};
313 12         11 my $declarations = $$rule{declarations};
314              
315 12         20 $contents .= "$selector {\n";
316 12         24 for ( my $i = 0; $i < @$declarations; $i+=2 ) {
317 13         37 $contents .= " " . lc($declarations->[$i]) . ": ".$declarations->[$i+1]. ";\n";
318             }
319 12         20 $contents .= "}\n";
320             }
321             elsif ($$rule{type} && $$rule{prelude} && $$rule{block}) {
322 4         39 $$rule{block} =~ s/([;{])\s*([^;{])/$1\n$2/mg; # attempt to restrict whitespace
323 4         62 $$rule{block} =~ s/^\s+|\s+$//mg;
324 4         26 $$rule{block} =~ s/[^\S\r\n]+/ /mg;
325 4         32 $$rule{block} =~ s/^([\w-]+:)/ $1/mg;
326 4         24 $$rule{block} =~ s/^/ /mg;
327              
328 4         12 $contents .= $$rule{type} . " " . $$rule{prelude} . "{\n" . $$rule{block} . "\n}\n";
329             }
330             elsif ($$rule{type} && $$rule{prelude}) {
331 3         9 $contents .= $$rule{type} . " " . $$rule{prelude} . "\n";
332             }
333             elsif ($$rule{type} && $$rule{block}) {
334 1         11 $$rule{block} =~ s/;\s*([\w-]+)/;\n$1/mg; # attempt to restrict whitespace
335 1         11 $$rule{block} =~ s/^\s+|\s+$//mg;
336 1         7 $$rule{block} =~ s/[^\S\r\n]+/ /mg;
337 1         8 $$rule{block} =~ s/([\w-]+:)/ $1/mg;
338              
339 1         3 $contents .= $$rule{type} . " {\n" . $$rule{block} . "\n}\n";
340             }
341             else {
342 0         0 $self->_report_warning({ info => "Invalid or unexpected rule encountered while writing out stylesheet" });
343             }
344             }
345              
346 4         18 return $contents;
347             }
348              
349             =pod
350            
351             =item content_warnings()
352            
353             Return back any warnings thrown while parsing a given block of css
354              
355             Note: content warnings are initialized at read time. In order to
356             receive back content feedback you must perform read() first.
357              
358             =cut
359              
360             sub content_warnings {
361 28     28 1 100 my ($self,$params) = @_;
362              
363 28         83 $self->_check_object();
364              
365 28         56 my @content_warnings = keys %{$self->_content_warnings()};
  28         169  
366              
367 28         128 return \@content_warnings;
368             }
369              
370             ####################################################################
371             # #
372             # The following are all get/set methods for manipulating the #
373             # stored stylesheet #
374             # #
375             ####################################################################
376              
377             =pod
378              
379             =item get_rules( params )
380              
381             Get an array of rules representing the composition of the stylesheet. These rules
382             are returned in the exact order that they were discovered. Both qualified and at
383             rules are returned by this method. It is left to the caller to pull out the kinds of
384             rules your application needs to accomplish your goals.
385              
386             The structures returned match up with the fields set while adding the rules via the add_x_rule collection methods.
387              
388             Specifically at-rules will contain a type, prelude and block while qualified rules will contain a selector and declarations.
389              
390             =cut
391              
392             sub get_rules {
393 27     27 1 445 my ($self,$params) = @_;
394              
395 27         121 $self->_check_object();
396              
397 27         87 return $self->_ordered();
398             }
399              
400             =pod
401              
402             =item add_qualified_rule( params )
403              
404             Add a qualified CSS rule to the ruleset store.
405              
406             The most common type of CSS rule is a qualified rule. This term became more prominent with the rise of CSS3, but is still
407             relevant when handling earlier versions of the standard. These rules have a prelude consisting of a CSS selector, along
408             with a data block consisting of various rule declarations.
409              
410             Adding a qualified rule is trivial, for example:
411             $self->add_qualified_rule({ selector => 'p > a', block => 'color: blue;' });
412              
413             =cut
414              
415             sub add_qualified_rule {
416 186     186 1 326 my ($self,$params) = @_;
417              
418 186         345 $self->_check_object();
419              
420 186         179 my $rule;
421 186 50 33     652 if (exists $$params{selector} && exists $$params{declarations}) {
422 186         491 $rule = { selector => $$params{selector}, declarations => $$params{declarations} };
423              
424 186         251 push @{$self->_ordered()}, $rule;
  186         356  
425             }
426             else {
427 0         0 $self->_report_warning({ info => "Invalid or unexpected data '$_' encountered while trying to add stylesheet rule" });
428             }
429              
430 186         653 return $rule;
431             }
432              
433             =pod
434              
435             =item add_at_rule( params )
436              
437             Add an at-rule to the ruleset store.
438              
439             The less common variants of CSS rules are know as at-rules. These rules implement various behaviours through various expressions
440             containing a rule type, prelude and associated data block. The standard is evolving here, so it is not easy to enumerate such
441             examples, but these rules always start with @.
442              
443             At rules are a little more complex, an example:
444             $self->add_at_rule({ type => '@media', prelude => 'print', block => 'body { font-size: 10pt; }' });
445              
446             =cut
447              
448             sub add_at_rule {
449 19     19 1 33 my ($self,$params) = @_;
450              
451 19         37 $self->_check_object();
452              
453 19         19 my $rule;
454 19 50 33     98 if (exists $$params{type} && exists $$params{prelude} && exists $$params{block}) {
      33        
455 19         56 $rule = { type => $$params{type}, prelude => $$params{prelude}, block => $$params{block} };
456            
457 19         35 push @{$self->_ordered()}, $rule;
  19         31  
458             }
459             else {
460 0         0 $self->_report_warning({ info => "Invalid or unexpected data '$_' encountered while trying to add stylesheet rule" });
461             }
462              
463 19         54 return $rule;
464             }
465              
466             ####################################################################
467             # #
468             # The following are all private methods and are not for normal use #
469             # I am working to finalize the get/set methods to make them public #
470             # #
471             ####################################################################
472              
473             sub _check_object {
474 601     601   775 my ($self,$params) = @_;
475              
476 601 50 33     1843 unless ($self && ref $self) {
477 0         0 croak "You must instantiate this class in order to properly use it";
478             }
479              
480 601         704 return();
481             }
482              
483             sub _report_warning {
484 4     4   7 my ($self,$params) = @_;
485              
486 4         8 $self->_check_object();
487              
488 4 100       11 if ($self->{warns_as_errors}) {
489 1         169 croak $$params{info};
490             }
491             else {
492 3         5 my $warnings = $self->_content_warnings();
493 3         10 $$warnings{$$params{info}} = 1;
494             }
495              
496 3         11 return();
497             }
498              
499             1;
500              
501             =pod
502              
503             =back
504              
505             =head1 AUTHOR
506              
507             Kevin Kamel >
508              
509             =head1 ATTRIBUTION
510              
511             This module is directly based off of Adam Kennedy's CSS::Tiny module.
512              
513             This particular version differs in terms of interface and the ultimate ordering of the CSS.
514              
515             =head1 LICENSE
516              
517             This module is a derived version of Adam Kennedy's CSS::Tiny Module.
518              
519             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
520              
521             The full text of the license can be found in the LICENSE file included with this module.
522              
523             =cut