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   1637987 use strict;
  31         65  
  31         1324  
12 31     31   197 use warnings;
  31         54  
  31         2256  
13              
14 31     31   183 use Carp;
  31         54  
  31         2706  
15              
16 31     31   222 use Storable qw(dclone);
  31         54  
  31         4197  
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   161 my $members = ['ordered','stylesheet','warns_as_errors','content_warnings'];
50              
51             #generate all the getter/setter we need
52 31         69 foreach my $member (@{$members}) {
  31         106  
53 31     31   308 no strict 'refs';
  31         94  
  31         4717  
54              
55 124         135049 *{'_' . $member} = sub {
56 300     300   492 my ($self,$value) = @_;
57              
58 300         684 $self->_check_object();
59              
60 300 100       598 $self->{$member} = $value if defined($value);
61              
62 300         853 return $self->{$member};
63             }
64 124         748 }
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 3704 my ($proto, $params) = @_;
86              
87 35   33     194 my $class = ref($proto) || $proto;
88              
89 35         82 my $rules = [];
90 35         73 my $selectors = {};
91              
92             my $self = {
93             stylesheet => undef,
94             ordered => $rules,
95             selectors => $selectors,
96             content_warnings => undef,
97 35 100 66     414 warns_as_errors => (defined($$params{warns_as_errors}) && $$params{warns_as_errors}) ? 1 : 0
98             };
99              
100 35         273 bless $self, $class;
101 35         1779 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 315 my ($self,$params) = @_;
160              
161 33         277 $self->_check_object();
162              
163 33         136 $self->_content_warnings({}); # overwrite any existing warnings
164              
165 33 50       121 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     234 if ($params && $$params{css}) {
170             # Flatten whitespace and remove /* comment */ style comments
171 32         96 my $string = $$params{css};
172 32         165 $string =~ tr/\n\t/ /;
173 32         172 $string =~ s!/\*.*?\*\/!!g;
174              
175             # Split into styles
176 32         1812 my @tokens = grep { /\S/ } (split /(?<=\})/, $string);
  246         681  
177 32         178 while (my $token = shift @tokens) {
178 194 100       2150 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         34 $atrule =~ /^\s*(@[\w-]+)\s*((?:url\()?"[^;]*;)(.*)/;
186            
187 6         55 $self->add_at_rule({ type => $1, prelude => $2, block => undef });
188            
189 6         34 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         15 $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         22 my $atrule = $token;
200              
201 11         53 for (my $attoken = shift(@tokens); defined($attoken); $attoken = shift(@tokens)) {
202 28 100       95 if ($attoken !~ /^\s*\}\s*$/) {
203 17         79 $atrule .= "\n$attoken\n";
204             }
205             else {
206 11         28 last;
207             }
208             }
209              
210 11         54 $atrule =~ /^\s*(@[\w-]+)\s*([^{]*)\{\s*(.*?})$/s;
211              
212 11         79 $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         404 my $rule = $1;
218 175         345 my $props = $2;
219              
220 175         405 $rule =~ s/\s{2,}/ /g;
221              
222             # Split into properties
223 175         241 my $properties = [];
224 175         530 foreach (grep { /\S/ } split /\;/, $props) {
  434         1038  
225             # skip over browser specific properties
226 290 100 100     1316 if ((/^\s*[\*\-\_]/) || (/\\/)) {
227 4         9 next;
228             }
229              
230             # check if properties are valid, reporting error as configured
231 286 100       1329 unless (/^\s*([\w._-]+)\s*:\s*(.*?)\s*$/) {
232 3         18 $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         984 push @$properties, lc $1, $2;
238             }
239              
240 174         3274 my @selectors = split /,/, $rule; # break the rule into the component selector(s)
241              
242             #apply the found rules to each selector
243 174         319 foreach my $selector (@selectors) {
244 184         889 $selector =~ s/^\s+|\s+$//g;
245              
246 184         739 $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         8 $self->_report_warning({ info => 'No stylesheet data was found in the document'});
256             }
257              
258 32         124 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 19 my ($self,$params) = @_;
304              
305 4         10 $self->_check_object();
306              
307 4         7 my $contents = '';
308              
309 4         5 foreach my $rule ( @{$self->_ordered()} ) {
  4         11  
310 20 100 66     140 if ($$rule{selector} && $$rule{declarations}) {
    100 66        
    100 66        
    50 33        
      33        
311             #grab the properties that make up this particular selector
312 12         30 my $selector = $$rule{selector};
313 12         47 my $declarations = $$rule{declarations};
314              
315 12         19 $contents .= "$selector {\n";
316 12         26 for ( my $i = 0; $i < @$declarations; $i+=2 ) {
317 13         35 $contents .= " " . lc($declarations->[$i]) . ": ".$declarations->[$i+1]. ";\n";
318             }
319 12         19 $contents .= "}\n";
320             }
321             elsif ($$rule{type} && $$rule{prelude} && $$rule{block}) {
322 4         56 $$rule{block} =~ s/([;{])\s*([^;{])/$1\n$2/mg; # attempt to restrict whitespace
323 4         110 $$rule{block} =~ s/^\s+|\s+$//mg;
324 4         39 $$rule{block} =~ s/[^\S\r\n]+/ /mg;
325 4         50 $$rule{block} =~ s/^([\w-]+:)/ $1/mg;
326 4         33 $$rule{block} =~ s/^/ /mg;
327              
328 4         17 $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         15 $$rule{block} =~ s/;\s*([\w-]+)/;\n$1/mg; # attempt to restrict whitespace
335 1         15 $$rule{block} =~ s/^\s+|\s+$//mg;
336 1         9 $$rule{block} =~ s/[^\S\r\n]+/ /mg;
337 1         11 $$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 28     28 1 111 my ($self,$params) = @_;
362              
363 28         185 $self->_check_object();
364              
365 28         48 my @content_warnings = keys %{$self->_content_warnings()};
  28         136  
366              
367 28         112 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 75 my ($self,$params) = @_;
394              
395 27         170 $self->_check_object();
396              
397 27         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 186     186 1 359 my ($self,$params) = @_;
417              
418 186         453 $self->_check_object();
419              
420 186         234 my $rule;
421 186 50 33     715 if (exists $$params{selector} && exists $$params{declarations}) {
422 186         556 $rule = { selector => $$params{selector}, declarations => $$params{declarations} };
423              
424 186         268 push @{$self->_ordered()}, $rule;
  186         422  
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         799 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 38 my ($self,$params) = @_;
450              
451 19         50 $self->_check_object();
452              
453 19         25 my $rule;
454 19 50 33     132 if (exists $$params{type} && exists $$params{prelude} && exists $$params{block}) {
      33        
455 19         70 $rule = { type => $$params{type}, prelude => $$params{prelude}, block => $$params{block} };
456            
457 19         30 push @{$self->_ordered()}, $rule;
  19         45  
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         102 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   969 my ($self,$params) = @_;
475              
476 601 50 33     2038 unless ($self && ref $self) {
477 0         0 croak "You must instantiate this class in order to properly use it";
478             }
479              
480 601         887 return();
481             }
482              
483             sub _report_warning {
484 4     4   8 my ($self,$params) = @_;
485              
486 4         11 $self->_check_object();
487              
488 4 100       10 if ($self->{warns_as_errors}) {
489 1         240 croak $$params{info};
490             }
491             else {
492 3         8 my $warnings = $self->_content_warnings();
493 3         11 $$warnings{$$params{info}} = 1;
494             }
495              
496 3         7 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