File Coverage

lib/CSS/Simple.pm
Criterion Covered Total %
statement 165 194 85.0
branch 37 54 68.5
condition 16 27 59.2
subroutine 25 28 89.2
pod 15 15 100.0
total 258 318 81.1


line stmt bran cond sub pod time code
1             package CSS::Simple;
2 7     7   8752 use strict;
  7         14  
  7         185  
3 7     7   55 use warnings;
  7         11  
  7         267  
4              
5             our $VERSION = '3224';
6              
7 7     7   29 use Carp;
  7         10  
  7         360  
8              
9 7     7   1999 use Tie::IxHash;
  7         20182  
  7         186  
10 7     7   2669 use Storable qw(dclone);
  7         16328  
  7         418  
11 7     7   2379 use Ref::Util qw(is_plain_arrayref is_plain_hashref);
  7         7661  
  7         630  
12              
13             =pod
14              
15             =head1 NAME
16              
17             CSS::Simple - Interface through which to read/write/manipulate CSS files while respecting the cascade order
18              
19             =head1 SYNOPSIS
20              
21             use CSS::Simple;
22              
23             my $css = new CSS::Simple();
24              
25             $css->read_file({ filename => 'input.css' });
26              
27             #perform manipulations...
28              
29             $css->write_file({ filename => 'output.css' });
30              
31             =head1 DESCRIPTION
32              
33             Class for reading, manipulating and writing CSS. Unlike other CSS classes on CPAN this particular module
34             focuses on respecting the order of selectors while providing a common sense API through which to manipulate the
35             rules.
36              
37             Please note that while ordering is respected, the exact order of selectors may change. I.e. the rules
38             implied by the styles and their ordering will not change, but the actual ordering of the styles may shift around.
39             See the read method for more information.
40              
41             =cut
42              
43             BEGIN {
44 7     7   28 my $members = ['ordered','stylesheet','warns_as_errors','content_warnings','browser_specific_properties', 'allow_duplicate_properties'];
45              
46             #generate all the getter/setter we need
47 7         12 foreach my $member (@{$members}) {
  7         16  
48 7     7   47 no strict 'refs';
  7         10  
  7         576  
49              
50 42         12081 *{'_' . $member} = sub {
51 318     318   352 my ($self,$value) = @_;
52              
53 318         405 $self->_check_object();
54              
55 318 100       382 $self->{$member} = $value if defined($value);
56              
57 318         1332 return $self->{$member};
58             }
59 42         125 }
60             }
61              
62              
63             =pod
64              
65             =head1 CONSTRUCTOR
66              
67             =over 4
68              
69             =item new ([ OPTIONS ])
70              
71             Instantiates the CSS::Simple object. Sets up class variables that are used during file parsing/processing.
72              
73             B (optional). Boolean value to indicate whether fatal errors should occur during parse failures.
74              
75             B (optional). Boolean value to indicate whether browser specific properties should be processed.
76              
77             =back
78              
79             =cut
80              
81             sub new {
82 12     12 1 5285 my ($proto, $params) = @_;
83              
84 12   33     73 my $class = ref($proto) || $proto;
85              
86 12         22 my $css = {};
87              
88             my $self = {
89             stylesheet => undef,
90 12         70 ordered => tie(%{$css}, 'Tie::IxHash'),
91             content_warnings => undef,
92             warns_as_errors => (defined($$params{warns_as_errors}) && $$params{warns_as_errors}) ? 1 : 0,
93             browser_specific_properties => (defined($$params{browser_specific_properties}) && $$params{browser_specific_properties}) ? 1 : 0,
94 12 100 66     21 allow_duplicate_properties => (defined($$params{allow_duplicate_properties}) && $$params{allow_duplicate_properties}) ? 1 : 0,
    100 100        
    100 66        
95             };
96              
97 12         296 bless $self, $class;
98 12         35 return $self;
99             }
100              
101             =head1 METHODS
102              
103             =cut
104              
105             =pod
106              
107             =over 4
108              
109             =item read_file( params )
110              
111             Opens and reads a CSS file, then subsequently performs the parsing of the CSS file
112             necessary for later manipulation.
113              
114             This method requires you to pass in a params hash that contains a
115             filename argument. For example:
116              
117             $self->read_file({filename => 'myfile.css'});
118              
119             =cut
120              
121             sub read_file {
122 0     0 1 0 my ($self,$params) = @_;
123              
124 0         0 $self->_check_object();
125              
126 0 0 0     0 unless ($params && $$params{filename}) {
127 0         0 croak "You must pass in hash params that contain a filename argument";
128             }
129              
130 0 0       0 open FILE, "<", $$params{filename} or croak $!;
131 0         0 my $css = do { local( $/ ) ; } ;
  0         0  
  0         0  
132 0         0 close FILE;
133              
134 0         0 $self->read({css => $css});
135              
136 0         0 return();
137             }
138              
139             =pod
140              
141             =item read( params )
142              
143             Reads css data and parses it. The intermediate data is stored in class variables.
144              
145             Compound selectors (i.e. "a, span") are split apart during parsing and stored
146             separately, so the output of any given stylesheet may not match the output 100%, but the
147             rules themselves should apply as expected.
148              
149             Ordering of selectors may shift if the same selector is seen twice within the stylesheet.
150             The precendence for any given selector is the last time it was seen by the parser.
151              
152             This method requires you to pass in a params hash that contains scalar
153             css data. For example:
154              
155             $self->read({css => $css});
156              
157             =cut
158              
159             sub read {
160 10     10 1 55 my ($self,$params) = @_;
161              
162 10         33 $self->_check_object();
163              
164 10         23 $self->_content_warnings({}); # overwrite any existing warnings
165              
166 10 50       51 unless (exists $$params{css}) {
167 0         0 croak 'You must pass in hash params that contains the css data';
168             }
169              
170 10 50 33     52 if ($params && $$params{css}) {
171             # Flatten whitespace and remove /* comment */ style comments
172 10         16 my $string = $$params{css};
173 10         27 $string =~ tr/\n\t/ /;
174 10         24 $string =~ s!/\*.*?\*\/!!g;
175              
176             # Split into styles
177 10         151 foreach (grep { /\S/ } split /(?<=\})/, $string) {
  42         91  
178 32 50       165 unless ( /^\s*([^{]+?)\s*\{(.*)\}\s*$/ ) {
179 0         0 $self->_report_warning({ info => "Invalid or unexpected style data '$_'" });
180 0         0 next;
181             }
182              
183             # Split in such a way as to support grouped styles
184 32         58 my $rule = $1;
185 32         47 my $props = $2;
186              
187 32         51 $rule =~ s/\s{2,}/ /g;
188              
189             # Split into properties
190 32         38 my $properties = {};
191 32         74 foreach ( grep { /\S/ } split /\;/, $props ) {
  99         183  
192              
193             # skip over browser specific properties unless specified in constructor
194 67 100 100     98 if (!$self->_browser_specific_properties() && (( /^\s*[*-_]/ ) || ( /\\/ ))) {
      100        
195 12         19 next;
196             }
197              
198             # check if properties are valid, reporting error as configured
199 55 100       189 unless ( /^\s*([\w._-]+)\s*:\s*(.*?)\s*$/ ) {
200 3         15 $self->_report_warning({ info => "Invalid or unexpected property '$_' in style '$rule'" });
201 2         4 next;
202             }
203              
204             #store the property for later
205 52         82 $self->_store_property($properties, $1, $2);
206             }
207              
208 31         62 my @selectors = split /,/, $rule; # break the rule into the component selector(s)
209              
210             #apply the found rules to each selector
211 31         43 foreach my $selector (@selectors) {
212 32         81 $selector =~ s/^\s+|\s+$//g;
213 32 100       72 if ($self->check_selector({selector => $selector})) { #check if we already exist
214 3         12 my $old_properties = $self->get_properties({selector => $selector});
215 3         19 $self->delete_selector({selector => $selector});
216              
217 3         12 my %merged = (%$old_properties, %$properties);
218              
219 3         9 $self->add_selector({selector => $selector, properties => \%merged});
220             }
221             else {
222             #store the properties within this selector
223 29         155 $self->add_selector({selector => $selector, properties => $properties});
224             }
225             }
226             }
227             }
228             else {
229 0         0 $self->_report_warning({ info => 'No stylesheet data was found in the document'});
230             }
231              
232 9         23 return();
233             }
234              
235             # store the value as a string or as arrayref of strings
236             sub _store_property {
237 52     52   125 my ($self, $properties, $key, $value) = @_;
238 52         83 $key = lc $key;
239              
240 52 100       67 if (!$self->_allow_duplicate_properties()) {
241             # store as scalar
242 48         90 $properties->{$key} = $value;
243 48         76 return;
244             }
245              
246 4 100       9 if (exists $properties->{$key}) {
247 1         2 my $existing_value = $properties->{$key};
248              
249             # store in arrayref
250 1 50       3 if (is_plain_arrayref $existing_value) {
251 0         0 push @$existing_value, $value;
252             }
253             else {
254 1         3 $properties->{$key} = [ $existing_value, $value ];
255             }
256             }
257             else {
258             # store as scalar
259 3         6 $properties->{$key} = $value;
260             }
261             }
262              
263             =pod
264              
265             =item write_file()
266              
267             Write the parsed and manipulated CSS out to a file parameter
268              
269             This method requires you to pass in a params hash that contains a
270             filename argument. For example:
271              
272             $self->write_file({filename => 'myfile.css'});
273              
274             =cut
275              
276             sub write_file {
277 0     0 1 0 my ($self,$params) = @_;
278              
279 0         0 $self->_check_object();
280              
281 0 0       0 unless (exists $$params{filename}) {
282 0         0 croak "No filename specified for write operation";
283             }
284              
285             # Write the file
286 0 0       0 open( CSS, '>'. $$params{filename} ) or croak "Failed to open file '$$params{filename}' for writing: $!";
287 0         0 print CSS $self->write();
288 0         0 close( CSS );
289              
290 0         0 return();
291             }
292              
293             =pod
294              
295             =item write()
296              
297             Write the parsed and manipulated CSS out to a scalar and return it
298              
299             =cut
300              
301             sub write {
302 6     6 1 2552 my ($self,$params) = @_;
303              
304 6         16 $self->_check_object();
305              
306 6         9 my $contents = '';
307              
308 6         11 foreach my $selector ( $self->_ordered()->Keys ) {
309              
310             #grab the properties that make up this particular selector
311 30         98 my $properties = $self->get_properties({selector => $selector});
312              
313 30 100       126 if (keys(%{$properties})) { # only output if the selector has properties
  30         61  
314 25         35 $contents .= "$selector {\n";
315 25         24 foreach my $property ( sort keys %{ $properties } ) {
  25         57  
316 48         65 my $values = $self->_retrieve_property_values($properties->{$property});
317 48         54 for my $a_val (@$values) {
318 49         105 $contents .= "\t" . lc($property) . ": $a_val;\n" ;
319             }
320             }
321 25         31 $contents .= "}\n";
322             }
323             }
324              
325 6         20 return $contents;
326             }
327              
328             =item output_selector
329              
330             Output the parsed and manipulated CSS for a specific selector.
331             The string that is output does not contain tabs or carriage returns.
332             The output is mainly to be inserted in the 'style' html attribute.
333              
334             =cut
335              
336             sub output_selector {
337 2     2 1 6 my ($self, $params) = @_;
338            
339 2 50       10 croak "Parameters must be passed as a hashref" unless is_plain_hashref($params);
340 2 50       6 croak "No selector specified" unless $params->{selector};
341              
342 2         7 $self->_check_object();
343 2         5 my $contents = '';
344              
345             #grab the properties that make up this particular selector
346 2         6 my $properties = $self->get_properties({selector => $params->{selector}});
347              
348 2 50       11 if (keys(%{$properties})) { # only output if the selector has properties
  2         6  
349 2         3 foreach my $property ( sort keys %{ $properties } ) {
  2         6  
350 6         9 my $values = $self->_retrieve_property_values($properties->{$property});
351 6         9 for my $a_val (@$values) {
352 7         16 $contents .= lc($property) . ":$a_val;" ;
353             }
354             }
355             }
356 2         7 return $contents;
357             }
358              
359             # the value may be a scalar or an arrayref of scalars.
360             # If the value is a scalar return an array ref containing the scalar.
361             # This simplifies processing when it can be assumed everything
362             # is in a list.
363             sub _retrieve_property_values {
364 54     54   63 my ($self, $value) = @_;
365 54 100       97 return is_plain_arrayref $value ?
366             $value :
367             [ $value ];
368             }
369              
370             =pod
371            
372             =item content_warnings()
373            
374             Return back any warnings thrown while parsing a given block of css
375              
376             Note: content warnings are initialized at read time. In order to
377             receive back content feedback you must perform read() first.
378              
379             =cut
380              
381             sub content_warnings {
382 1     1 1 7 my ($self,$params) = @_;
383              
384 1         4 $self->_check_object();
385              
386 1         1 my @content_warnings = keys %{$self->_content_warnings()};
  1         2  
387              
388 1         5 return \@content_warnings;
389             }
390              
391             ####################################################################
392             # #
393             # The following are all get/set methods for manipulating the #
394             # stored stylesheet #
395             # #
396             # Provides a nicer interface than dealing with TIE #
397             # #
398             ####################################################################
399              
400             =pod
401              
402             =item get_selectors( params )
403              
404             Get an array of selectors that represents an inclusive list of all selectors
405             stored.
406              
407             =cut
408              
409             sub get_selectors {
410 0     0 1 0 my ($self,$params) = @_;
411              
412 0         0 $self->_check_object();
413              
414 0         0 return($self->_ordered()->Keys());
415             }
416              
417             =pod
418              
419             =item get_properties( params )
420              
421             Get a hash that represents the various properties for this particular selector
422              
423             This method requires you to pass in a params hash that contains scalar
424             css data. For example:
425              
426             $self->get_properties({selector => '.foo'});
427              
428             =cut
429              
430             sub get_properties {
431 47     47 1 1161 my ($self,$params) = @_;
432              
433 47         68 $self->_check_object();
434              
435 47         53 return($self->_ordered()->FETCH($$params{selector}));
436             }
437              
438             =pod
439              
440             =item check_selector( params )
441              
442             Determine if a selector exists within the stored rulesets
443              
444             This method requires you to pass in a params hash that contains scalar
445             css data. For example:
446              
447             $self->check_selector({selector => '.foo'});
448              
449             =cut
450              
451             sub check_selector {
452 81     81 1 96 my ($self,$params) = @_;
453              
454 81         106 $self->_check_object();
455              
456 81         109 return($self->_ordered()->EXISTS($$params{selector}));
457             }
458              
459             =pod
460              
461             =item modify_selector( params )
462              
463             Modify an existing selector
464            
465             Modifying a selector maintains the existing selectivity of the rule with relation to the
466             original stylesheet. If you want to ignore that selectivity, delete the element and re-add
467             it to CSS::Simple
468              
469             This method requires you to pass in a params hash that contains scalar
470             css data. For example:
471              
472             $self->modify_selector({selector => '.foo', new_selector => '.bar' });
473              
474             =cut
475              
476             sub modify_selector {
477 2     2 1 10 my ($self,$params) = @_;
478              
479 2         3 $self->_check_object();
480              
481             #if the selector is found, replace the selector
482 2 50       4 if ($self->check_selector({selector => $$params{selector}})) {
483             #we probably want to be doing this explicitely
484 2         7 my ($index) = $self->_ordered()->Indices( $$params{selector} );
485 2         12 my $properties = $self->get_properties({selector => $$params{selector}});
486              
487 2         12 $self->_ordered()->Replace($index,$properties,$$params{new_selector});
488             }
489             #otherwise new element, stick it onto the end of the rulesets
490             else {
491             #add a selector, there was nothing to replace
492 0         0 $self->add_selector({selector => $$params{new_selector}, properties => {}});
493             }
494              
495 2         39 return();
496             }
497              
498             =pod
499              
500             =item add_selector( params )
501              
502             Add a selector and associated properties to the stored rulesets
503              
504             In the event that this particular ruleset already exists, invoking this method will
505             simply replace the item. This is important - if you are modifying an existing rule
506             using this method than the previously existing selectivity will continue to persist.
507             Delete the selector first if you want to ignore the previous selectivity.
508              
509             This method requires you to pass in a params hash that contains scalar
510             css data. For example:
511              
512             $self->add_selector({selector => '.foo', properties => {color => 'red' }});
513              
514             =cut
515              
516             sub add_selector {
517 43     43 1 81 my ($self,$params) = @_;
518              
519 43         67 $self->_check_object();
520              
521             #if we existed already, invoke REPLACE to preserve selectivity
522 43 100       82 if ($self->check_selector({selector => $$params{selector}})) {
523             #we probably want to be doing this explicitely
524 2         6 my ($index) = $self->_ordered()->Indices( $$params{selector} );
525              
526 2         9 $self->_ordered()->Replace($index,dclone($$params{properties}));
527             }
528             #new element, stick it onto the end of the rulesets
529             else {
530             #store the properties
531 41         144 $self->_ordered()->STORE($$params{selector},dclone($$params{properties}));
532             }
533              
534 43         614 return();
535             }
536              
537             =pod
538              
539             =item add_properties( params )
540              
541             Add properties to an existing selector, preserving the selectivity of the original declaration.
542              
543             In the event that this method is invoked with a selector that doesn't exist then the call
544             is just translated to an add_selector call, thus creating the rule at the end of the ruleset.
545              
546             This method requires you to pass in a params hash that contains scalar
547             css data. For example:
548              
549             $self->add_properties({selector => '.foo', properties => {color => 'red' }});
550              
551             =cut
552              
553             sub add_properties {
554 4     4 1 24 my ($self,$params) = @_;
555              
556 4         7 $self->_check_object();
557              
558             #If selector exists already, merge properties into this selector
559 4 100       6 if ($self->check_selector({selector => $$params{selector}})) {
560             #merge property sets together
561 1         4 my %properties = (%{$self->get_properties({selector => $$params{selector}})}, %{$$params{properties}});
  1         2  
  1         8  
562              
563             #overwrite the existing properties for this selector with the new hybrid style
564 1         4 $self->add_selector({selector => $$params{selector}, properties => \%properties});
565             }
566             #otherwise add it wholesale
567             else {
568 3         12 $self->add_selector({selector => $$params{selector}, properties => $$params{properties}});
569             }
570              
571 4         8 return();
572             }
573              
574             =pod
575              
576             =item delete_selector( params )
577              
578             Delete a selector from the ruleset
579              
580             This method requires you to pass in a params hash that contains scalar
581             css data. For example:
582              
583             $self->delete_selector({selector => '.foo' });
584              
585             =cut
586              
587             sub delete_selector {
588 3     3 1 4 my ($self,$params) = @_;
589              
590 3         6 $self->_check_object();
591              
592             #store the properties, potentially overwriting properties that were there
593 3         4 $self->_ordered()->DELETE($$params{selector});
594              
595 3         49 return();
596             }
597              
598             =pod
599              
600             =item delete_property( params )
601              
602             Delete a property from a specific selectors rules
603              
604             This method requires you to pass in a params hash that contains scalar
605             css data. For example:
606              
607             $self->delete_property({selector => '.foo', property => 'color' });
608              
609             =back
610              
611             =cut
612              
613             sub delete_property {
614 1     1 1 5 my ($self,$params) = @_;
615              
616 1         2 $self->_check_object();
617              
618             #get the properties so we can remove the requested property from the hash
619 1         2 my $properties = $self->get_properties({selector => $$params{selector}});
620              
621 1         6 delete $$properties{$$params{property}};
622              
623 1         3 $self->add_selector({selector => $$params{selector}, properties => $properties});
624              
625 1         2 return();
626             }
627            
628             ####################################################################
629             # #
630             # The following are all private methods and are not for normal use #
631             # I am working to finalize the get/set methods to make them public #
632             # #
633             ####################################################################
634              
635             sub _check_object {
636 521     521   593 my ($self,$params) = @_;
637              
638 521 50 33     1124 unless ($self && ref $self) {
639 0         0 croak "You must instantiate this class in order to properly use it";
640             }
641              
642 521         476 return();
643             }
644              
645             sub _report_warning {
646 3     3   5 my ($self,$params) = @_;
647              
648 3         5 $self->_check_object();
649              
650 3 100       5 if ($self->{warns_as_errors}) {
651 1         152 croak $$params{info};
652             }
653             else {
654 2         3 my $warnings = $self->_content_warnings();
655 2         7 $$warnings{$$params{info}} = 1;
656             }
657              
658 2         2 return();
659             }
660              
661             1;
662              
663             =pod
664              
665             =head1 Sponsor
666              
667             This code has been developed under sponsorship of MailerMailer LLC, http://www.mailermailer.com/
668              
669             =head1 AUTHOR
670              
671             Kevin Kamel >
672              
673             =head1 ATTRIBUTION
674              
675             This module is directly based off of Adam Kennedy's CSS::Tiny module.
676              
677             This particular version differs in terms of interface and the ultimate ordering of the CSS.
678              
679             =head1 LICENSE
680              
681             This module is a derived version of Adam Kennedy's CSS::Tiny Module.
682              
683             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
684              
685             The full text of the license can be found in the LICENSE file included with this module.
686              
687             =cut