File Coverage

lib/CSS/Inliner/Parser.pm
Criterion Covered Total %
statement 133 157 84.7
branch 32 46 69.5
condition 20 42 47.6
subroutine 16 18 88.8
pod 9 9 100.0
total 210 272 77.2


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   4222 use strict;
  31         71  
  31         935  
12 31     31   163 use warnings;
  31         197  
  31         1012  
13              
14 31     31   148 use Carp;
  31         55  
  31         2329  
15              
16 31     31   3214 use Storable qw(dclone);
  31         14381  
  31         2926  
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   133 my $members = ['ordered','stylesheet','warns_as_errors','content_warnings'];
50              
51             #generate all the getter/setter we need
52 31         67 foreach my $member (@{$members}) {
  31         80  
53 31     31   194 no strict 'refs';
  31         57  
  31         2718  
54              
55 124         59967 *{'_' . $member} = sub {
56 310     310   494 my ($self,$value) = @_;
57              
58 310         611 $self->_check_object();
59              
60 310 100       653 $self->{$member} = $value if defined($value);
61              
62 310         802 return $self->{$member};
63             }
64 124         482 }
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 4205 my ($proto, $params) = @_;
86              
87 35   33     206 my $class = ref($proto) || $proto;
88              
89 35         80 my $rules = [];
90 35         84 my $selectors = {};
91              
92             my $self = {
93             stylesheet => undef,
94             ordered => $rules,
95             selectors => $selectors,
96             content_warnings => undef,
97 35 100 66     372 warns_as_errors => (defined($$params{warns_as_errors}) && $$params{warns_as_errors}) ? 1 : 0
98             };
99              
100 35         95 bless $self, $class;
101 35         550 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 34     34 1 132 my ($self,$params) = @_;
160              
161 34         142 $self->_check_object();
162              
163 34         135 $self->_content_warnings({}); # overwrite any existing warnings
164              
165 34 50       137 unless (exists $$params{css}) {
166 0         0 croak 'You must pass in hash params that contains the css data';
167             }
168              
169 34 100 66     225 if ($params && $$params{css}) {
170             # Flatten whitespace and remove /* comment */ style comments
171 33         82 my $string = $$params{css};
172 33         192 $string =~ tr/\n\t/ /;
173 33         168 $string =~ s!/\*.*?\*\/!!g;
174              
175             # Split into styles
176 33         1172 my @tokens = grep { /\S/ } (split /(?<=\})/, $string);
  252         665  
177 33         199 while (my $token = shift @tokens) {
178 199 100       1742 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         9 my $atrule = $token;
184            
185 6         18 $atrule =~ /^\s*(@[\w-]+)\s*((?:url\()?"[^;]*;)(.*)/;
186            
187 6         34 $self->add_at_rule({ type => $1, prelude => $2, block => undef });
188            
189 6         23 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         10 $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         17 my $atrule = $token;
200              
201 11         31 for (my $attoken = shift(@tokens); defined($attoken); $attoken = shift(@tokens)) {
202 28 100       74 if ($attoken !~ /^\s*\}\s*$/) {
203 17         57 $atrule .= "\n$attoken\n";
204             }
205             else {
206 11         19 last;
207             }
208             }
209              
210 11         37 $atrule =~ /^\s*(@[\w-]+)\s*([^{]*)\{\s*(.*?})$/s;
211              
212 11         58 $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 180         496 my $rule = $1;
218 180         404 my $props = $2;
219              
220 180         616 $rule =~ s/\s{2,}/ /g;
221              
222             # Split into properties
223 180         520 my $properties = {};
224 180         543 foreach (grep { /\S/ } split /\;/, $props) {
  449         1123  
225             # skip over browser specific properties
226 300 100 100     1403 if ((/^\s*[\*\-\_]/) || (/\\/)) {
227 4         9 next;
228             }
229              
230             # check if properties are valid, reporting error as configured
231 296 100       1191 unless (/^\s*([\w._-]+)\s*:\s*(.*?)\s*$/) {
232 3         16 $self->_report_warning({ info => "Invalid or unexpected property '$_' in style '$rule'" });
233 2         7 next;
234             }
235              
236             #store the property for later
237 293         951 $$properties{lc $1} = $2;
238             }
239              
240 179         478 my @selectors = split /,/, $rule; # break the rule into the component selector(s)
241              
242             #apply the found rules to each selector
243 179         337 foreach my $selector (@selectors) {
244 191         671 $selector =~ s/^\s+|\s+$//g;
245              
246 191         690 $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         9 $self->_report_warning({ info => 'No stylesheet data was found in the document'});
256             }
257              
258 33         111 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 18 my ($self,$params) = @_;
304              
305 4         13 $self->_check_object();
306              
307 4         8 my $contents = '';
308              
309 4         6 foreach my $rule ( @{$self->_ordered()} ) {
  4         11  
310 20 100 66     102 if ($$rule{selector} && $$rule{declarations}) {
    100 66        
    100 66        
    50 33        
      33        
311             #grab the properties that make up this particular selector
312 12         19 my $selector = $$rule{selector};
313 12         18 my $declarations = $$rule{declarations};
314              
315 12         21 $contents .= "$selector {\n";
316 12         15 foreach my $property ( sort keys %{ $declarations } ) {
  12         43  
317 13         33 $contents .= " " . lc($property) . ": ".$$declarations{$property}. ";\n";
318             }
319 12         24 $contents .= "}\n";
320             }
321             elsif ($$rule{type} && $$rule{prelude} && $$rule{block}) {
322 4         33 $$rule{block} =~ s/([;{])\s*([^;{])/$1\n$2/mg; # attempt to restrict whitespace
323 4         48 $$rule{block} =~ s/^\s+|\s+$//mg;
324 4         24 $$rule{block} =~ s/[^\S\r\n]+/ /mg;
325 4         27 $$rule{block} =~ s/^([\w-]+:)/ $1/mg;
326 4         22 $$rule{block} =~ s/^/ /mg;
327              
328 4         11 $contents .= $$rule{type} . " " . $$rule{prelude} . "{\n" . $$rule{block} . "\n}\n";
329             }
330             elsif ($$rule{type} && $$rule{prelude}) {
331 3         7 $contents .= $$rule{type} . " " . $$rule{prelude} . "\n";
332             }
333             elsif ($$rule{type} && $$rule{block}) {
334 1         8 $$rule{block} =~ s/;\s*([\w-]+)/;\n$1/mg; # attempt to restrict whitespace
335 1         8 $$rule{block} =~ s/^\s+|\s+$//mg;
336 1         5 $$rule{block} =~ s/[^\S\r\n]+/ /mg;
337 1         6 $$rule{block} =~ s/([\w-]+:)/ $1/mg;
338              
339 1         4 $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         15 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 29     29 1 93 my ($self,$params) = @_;
362              
363 29         104 $self->_check_object();
364              
365 29         50 my @content_warnings = keys %{$self->_content_warnings()};
  29         83  
366              
367 29         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 28     28 1 78 my ($self,$params) = @_;
394              
395 28         89 $self->_check_object();
396              
397 28         78 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 193     193 1 376 my ($self,$params) = @_;
417              
418 193         395 $self->_check_object();
419              
420 193         255 my $rule;
421 193 50 33     753 if (exists $$params{selector} && exists $$params{declarations}) {
422 193         528 $rule = { selector => $$params{selector}, declarations => $$params{declarations} };
423              
424 193         267 push @{$self->_ordered()}, $rule;
  193         380  
425             }
426             else {
427 0         0 $self->_report_warning({ info => "Invalid or unexpected data '$_' encountered while trying to add stylesheet rule" });
428             }
429              
430 193         732 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 44 my ($self,$params) = @_;
450              
451 19         46 $self->_check_object();
452              
453 19         24 my $rule;
454 19 50 33     129 if (exists $$params{type} && exists $$params{prelude} && exists $$params{block}) {
      33        
455 19         54 $rule = { type => $$params{type}, prelude => $$params{prelude}, block => $$params{block} };
456            
457 19         24 push @{$self->_ordered()}, $rule;
  19         33  
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         50 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 621     621   855 my ($self,$params) = @_;
475              
476 621 50 33     2450 unless ($self && ref $self) {
477 0         0 croak "You must instantiate this class in order to properly use it";
478             }
479              
480 621         861 return();
481             }
482              
483             sub _report_warning {
484 4     4   9 my ($self,$params) = @_;
485              
486 4         15 $self->_check_object();
487              
488 4 100       11 if ($self->{warns_as_errors}) {
489 1         132 croak $$params{info};
490             }
491             else {
492 3         8 my $warnings = $self->_content_warnings();
493 3         14 $$warnings{$$params{info}} = 1;
494             }
495              
496 3         15 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