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   7388 use strict;
  31         75  
  31         919  
12 31     31   152 use warnings;
  31         59  
  31         790  
13              
14 31     31   148 use Carp;
  31         56  
  31         1931  
15              
16 31     31   3381 use Storable qw(dclone);
  31         15833  
  31         3057  
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   143 my $members = ['ordered','stylesheet','warns_as_errors','content_warnings'];
50              
51             #generate all the getter/setter we need
52 31         63 foreach my $member (@{$members}) {
  31         109  
53 31     31   210 no strict 'refs';
  31         76  
  31         3195  
54              
55 124         73967 *{'_' . $member} = sub {
56 300     300   437 my ($self,$value) = @_;
57              
58 300         480 $self->_check_object();
59              
60 300 100       489 $self->{$member} = $value if defined($value);
61              
62 300         675 return $self->{$member};
63             }
64 124         418 }
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 3324 my ($proto, $params) = @_;
86              
87 35   33     182 my $class = ref($proto) || $proto;
88              
89 35         72 my $rules = [];
90 35         66 my $selectors = {};
91              
92             my $self = {
93             stylesheet => undef,
94             ordered => $rules,
95             selectors => $selectors,
96             content_warnings => undef,
97 35 100 66     265 warns_as_errors => (defined($$params{warns_as_errors}) && $$params{warns_as_errors}) ? 1 : 0
98             };
99              
100 35         83 bless $self, $class;
101 35         435 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 108 my ($self,$params) = @_;
160              
161 33         108 $self->_check_object();
162              
163 33         129 $self->_content_warnings({}); # overwrite any existing warnings
164              
165 33 50       98 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     177 if ($params && $$params{css}) {
170             # Flatten whitespace and remove /* comment */ style comments
171 32         69 my $string = $$params{css};
172 32         145 $string =~ tr/\n\t/ /;
173 32         116 $string =~ s!/\*.*?\*\/!!g;
174              
175             # Split into styles
176 32         1071 my @tokens = grep { /\S/ } (split /(?<=\})/, $string);
  246         545  
177 32         141 while (my $token = shift @tokens) {
178 194 100       1486 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         8 my $atrule = $token;
184            
185 6         21 $atrule =~ /^\s*(@[\w-]+)\s*((?:url\()?"[^;]*;)(.*)/;
186            
187 6         29 $self->add_at_rule({ type => $1, prelude => $2, block => undef });
188            
189 6         21 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         18 my $atrule = $token;
200              
201 11         29 for (my $attoken = shift(@tokens); defined($attoken); $attoken = shift(@tokens)) {
202 28 100       74 if ($attoken !~ /^\s*\}\s*$/) {
203 17         51 $atrule .= "\n$attoken\n";
204             }
205             else {
206 11         16 last;
207             }
208             }
209              
210 11         44 $atrule =~ /^\s*(@[\w-]+)\s*([^{]*)\{\s*(.*?})$/s;
211              
212 11         56 $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         365 my $rule = $1;
218 175         287 my $props = $2;
219              
220 175         303 $rule =~ s/\s{2,}/ /g;
221              
222             # Split into properties
223 175         235 my $properties = {};
224 175         429 foreach (grep { /\S/ } split /\;/, $props) {
  434         948  
225             # skip over browser specific properties
226 290 100 100     1067 if ((/^\s*[\*\-\_]/) || (/\\/)) {
227 4         8 next;
228             }
229              
230             # check if properties are valid, reporting error as configured
231 286 100       1023 unless (/^\s*([\w._-]+)\s*:\s*(.*?)\s*$/) {
232 3         14 $self->_report_warning({ info => "Invalid or unexpected property '$_' in style '$rule'" });
233 2         4 next;
234             }
235              
236             #store the property for later
237 283         862 $$properties{lc $1} = $2;
238             }
239              
240 174         411 my @selectors = split /,/, $rule; # break the rule into the component selector(s)
241              
242             #apply the found rules to each selector
243 174         254 foreach my $selector (@selectors) {
244 184         585 $selector =~ s/^\s+|\s+$//g;
245              
246 184         527 $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         100 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 17 my ($self,$params) = @_;
304              
305 4         9 $self->_check_object();
306              
307 4         6 my $contents = '';
308              
309 4         6 foreach my $rule ( @{$self->_ordered()} ) {
  4         7  
310 20 100 66     98 if ($$rule{selector} && $$rule{declarations}) {
    100 66        
    100 66        
    50 33        
      33        
311             #grab the properties that make up this particular selector
312 12         14 my $selector = $$rule{selector};
313 12         14 my $declarations = $$rule{declarations};
314              
315 12         18 $contents .= "$selector {\n";
316 12         14 foreach my $property ( sort keys %{ $declarations } ) {
  12         33  
317 13         34 $contents .= " " . lc($property) . ": ".$$declarations{$property}. ";\n";
318             }
319 12         21 $contents .= "}\n";
320             }
321             elsif ($$rule{type} && $$rule{prelude} && $$rule{block}) {
322 4         42 $$rule{block} =~ s/([;{])\s*([^;{])/$1\n$2/mg; # attempt to restrict whitespace
323 4         65 $$rule{block} =~ s/^\s+|\s+$//mg;
324 4         29 $$rule{block} =~ s/[^\S\r\n]+/ /mg;
325 4         37 $$rule{block} =~ s/^([\w-]+:)/ $1/mg;
326 4         27 $$rule{block} =~ s/^/ /mg;
327              
328 4         13 $contents .= $$rule{type} . " " . $$rule{prelude} . "{\n" . $$rule{block} . "\n}\n";
329             }
330             elsif ($$rule{type} && $$rule{prelude}) {
331 3         8 $contents .= $$rule{type} . " " . $$rule{prelude} . "\n";
332             }
333             elsif ($$rule{type} && $$rule{block}) {
334 1         12 $$rule{block} =~ s/;\s*([\w-]+)/;\n$1/mg; # attempt to restrict whitespace
335 1         12 $$rule{block} =~ s/^\s+|\s+$//mg;
336 1         5 $$rule{block} =~ s/[^\S\r\n]+/ /mg;
337 1         9 $$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         13 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 96 my ($self,$params) = @_;
362              
363 28         122 $self->_check_object();
364              
365 28         61 my @content_warnings = keys %{$self->_content_warnings()};
  28         90  
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 81 my ($self,$params) = @_;
394              
395 27         80 $self->_check_object();
396              
397 27         82 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 331 my ($self,$params) = @_;
417              
418 186         327 $self->_check_object();
419              
420 186         183 my $rule;
421 186 50 33     552 if (exists $$params{selector} && exists $$params{declarations}) {
422 186         448 $rule = { selector => $$params{selector}, declarations => $$params{declarations} };
423              
424 186         223 push @{$self->_ordered()}, $rule;
  186         318  
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         615 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 31 my ($self,$params) = @_;
450              
451 19         31 $self->_check_object();
452              
453 19         12 my $rule;
454 19 50 33     77 if (exists $$params{type} && exists $$params{prelude} && exists $$params{block}) {
      33        
455 19         50 $rule = { type => $$params{type}, prelude => $$params{prelude}, block => $$params{block} };
456            
457 19         30 push @{$self->_ordered()}, $rule;
  19         24  
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         45 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   724 my ($self,$params) = @_;
475              
476 601 50 33     1759 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   6 my ($self,$params) = @_;
485              
486 4         19 $self->_check_object();
487              
488 4 100       9 if ($self->{warns_as_errors}) {
489 1         161 croak $$params{info};
490             }
491             else {
492 3         8 my $warnings = $self->_content_warnings();
493 3         10 $$warnings{$$params{info}} = 1;
494             }
495              
496 3         5 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