File Coverage

blib/lib/CSS/Object.pm
Criterion Covered Total %
statement 165 203 81.2
branch 30 78 38.4
condition 10 26 38.4
subroutine 38 45 84.4
pod 21 24 87.5
total 264 376 70.2


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## CSS Object Oriented - ~/lib/CSS/Object.pm
3             ## Version v0.1.5
4             ## Copyright(c) 2021 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2020/06/24
7             ## Modified 2021/11/28
8             ## All rights reserved
9             ##
10             ## This program is free software; you can redistribute it and/or modify it
11             ## under the same terms as Perl itself.
12             ##----------------------------------------------------------------------------
13             package CSS::Object;
14             BEGIN
15             {
16 6     6   221028 use strict;
  6         25  
  6         194  
17 6     6   30 use warnings;
  6         12  
  6         177  
18 6     6   30 use warnings::register;
  6         13  
  6         757  
19 6     6   2774 use parent qw( Module::Generic );
  6         2019  
  6         33  
20 6     6   151540942 use CSS::Object::Builder;
  6         21  
  6         96  
21 6     6   4391 use CSS::Object::Comment;
  6         20  
  6         69  
22 6     6   1364 use CSS::Object::Format;
  6         13  
  6         37  
23 6     6   3885 use CSS::Object::Property;
  6         26  
  6         87  
24 6     6   1547 use CSS::Object::Rule;
  6         13  
  6         65  
25 6     6   4132 use CSS::Object::Rule::At;
  6         19  
  6         92  
26 6     6   3993 use CSS::Object::Rule::Keyframes;
  6         27  
  6         81  
27 6     6   4000 use CSS::Object::Selector;
  6         17  
  6         79  
28 6     6   1429 use CSS::Object::Value;
  6         13  
  6         53  
29 6     6   1338 use Want ();
  6         12  
  6         114  
30 6     6   31 use Devel::Confess;
  6         16  
  6         31  
31 6     6   12818 our $VERSION = 'v0.1.5';
32             };
33              
34             sub init
35             {
36 6     6 1 5823 my $self = shift( @_ );
37 6         368 $self->{parser} = 'CSS::Object::Parser::Default';
38 6         20 $self->{format} = '';
39 6         19 $self->{_init_strict_use_sub} = 1;
40 6 50       48 $self->SUPER::init( @_ ) || return( $self->pass_error );
41             # $self->message( 3, "Formatter class set: '", ref( $self->format ), "'." );
42 6 100       514 unless( $self->_is_a( $self->{format}, 'CSS::Object::Format' ) )
43             {
44 3         42 my $format = CSS::Object::Format->new(
45             debug => $self->debug
46             );
47 3         26 $self->format( $format );
48             }
49 6         86 $self->{rules} = Module::Generic::Array->new;
50 6         72 return( $self );
51             }
52              
53             # Add comment at the top level. To add comment inside a rule, see add_element in CSS::Object::Rule
54             sub add_element
55             {
56 1     1 1 3 my $self = shift( @_ );
57 1   50     4 my $elem = shift( @_ ) || return( $self->error( "No element object was provided to add to this rule." ) );
58 1 50       73 return( $self->error( "Element object provided ($elem) is not a CSS::Object::Element object." ) ) if( !$self->_is_a( $elem, 'CSS::Object::Element' ) );
59             # $self->message( 3, "Adding element object '$elem'." );
60             # $elem->format( $self->format );
61 1         20 $elem->debug( $self->debug );
62             # $self->properties->push( $prop );
63 1         38 $self->elements->push( $elem );
64 1         47 return( $self );
65             }
66              
67             sub add_rule
68             {
69 9     9 1 27 my $self = shift( @_ );
70 9         21 my $rule = shift( @_ );
71 9         59 $self->message( 4, "CSS rule provided to add to our stack of elements: '", overload::StrVal( $rule ), "'." );
72 9 50       283 return( $self->error( "No rule object was provided to add." ) ) if( !defined( $rule ) );
73 9 50       34 return( $self->error( "Object provided is not a CSS::Object::Rule object." ) ) if( !$self->_is_a( $rule, 'CSS::Object::Rule' ) );
74             # $self->rules->push( $rule );
75 9         176 $self->elements->push( $rule );
76 9         412 $self->message( 4, "Returning rule object added: '", overload::StrVal( $rule ), "'. Now we have ", $self->elements->length, " rules stored." );
77 9         464291 return( $rule );
78             }
79              
80             sub as_string
81             {
82 2     2 1 1042 my $self = shift( @_ );
83 2         50 $self->messagef( 3, "There are %d elements in our stack.", $self->elements->length );
84 2 50       103003 if( @_ )
85             {
86 0         0 my $format = shift( @_ );
87 0 0 0     0 return( $self->error( "Provided parameter to as_string was not an CSS::Object::Format object." ) ) if( $format !~ /^CSS\::Object\::Format/ && !$self->_is_a( $format, 'CSS::Object::Format' ) );
88             $self->elements->foreach(sub
89             {
90 0     0   0 shift->format( $format );
91 0         0 });
92             }
93              
94 2         18 my $output = Module::Generic::Array->new;
95             # $self->rules->foreach(sub
96             $self->elements->foreach(sub
97             {
98 4     4   333 $output->push( shift->as_string );
99 2         30 });
100 2         159 my $nl = $self->format->new_line;
101 2         89 return( $output->join( "$nl$nl" )->scalar );
102             }
103              
104             sub builder
105             {
106 1     1 1 407 my $self = shift( @_ );
107 1 50       6 return( $self->{_builder} ) if( $self->_is_object( $self->{_builder} ) );
108             # $self->message( 3, "Creating builder object with debug set to '", $self->debug, "' and formatter set to '", $self->format->class, "'." );
109 1   50     16 my $b = CSS::Object::Builder->new( $self, debug => $self->debug ) ||
110             return( $self->error( "Could not initialise the CSS builder: ", CSS::Object::Builder->error ) );
111 1         10 $self->{_builder} = $b;
112 1         3 return( $b );
113             }
114              
115 1     1 1 29 sub charset { return( shift->_set_get_scalar_as_object( 'charset', @_ ) ); }
116              
117             # Array of CSS::Object::Element objects or their sub classes
118 35     35 1 213 sub elements { return( shift->_set_get_object_array_object( 'elements', 'CSS::Object::Element', @_ ) ); }
119              
120             sub format
121             {
122 132     132 1 2195 my $self = shift( @_ );
123 132 100       434 if( @_ )
124             {
125 6         14 my $val = shift( @_ );
126 6         14 my $format;
127 6 100 33     45 if( ref( $val ) )
    50          
128             {
129 3   50     24 $format = $self->_set_get_object( 'format', 'CSS::Object::Format', $val ) || return( $self->pass_error );
130             }
131             # Formatter as a class name
132             elsif( !ref( $val ) && CORE::index( $val, '::' ) != -1 )
133             {
134 3 50       23 $self->_load_class( $val ) || return( $self->pass_error );
135 3   50     426 $format = $val->new( debug => $self->debug ) || return( $self->pass_error( $val->error ) );
136 3         41 $self->_set_get_object( 'format', 'CSS::Object::Format', $format );
137             }
138             else
139             {
140 0         0 return( $self->error( "Unknown format \"$val\". I do not know what to do with it." ) );
141             }
142 6     0   268 $self->messagef( 3, "Setting new formatter '$format' for %d elements -> %s", $self->elements->length, sub{ $self->elements->join( "', '" )} );
  0         0  
143             $self->elements->foreach(sub
144             {
145 0 0   0   0 return(1) if( !$self->_is_object( $_[0] ) );
146 0 0       0 shift->format( $format ) || return;
147 6         342598 });
148 6         647 return( $format );
149             }
150 126         409 return( $self->_set_get_object( 'format', 'CSS::Object::Format' ) );
151             }
152              
153             sub get_rule_by_selector
154             {
155 1     1 1 594 my( $self, $name ) = @_;
156 1 50       5 return( $self->error( "No selector was provided to find its equivalent rule object." ) ) if( !$name );
157             # $self->messagef( 3, "%d elements found.", $self->elements->length );
158 1         5 my $found = Module::Generic::Array->new;
159 1         10 foreach my $rule ( @{$self->elements} )
  1         4  
160             {
161 3 50       131 next if( !$rule->isa( 'CSS::Object::Rule' ) );
162             # $self->messagef( 3, "This rule has %d selectors", $rule->selectors->length );
163 3         5 foreach my $sel ( @{$rule->selectors} )
  3         12  
164             {
165             # $self->message( 3, "Does '", $sel->name, "' match our target selector '$name' ?" );
166 5 100       240 if( $sel->name eq $name )
167             {
168             # return( $rule );
169 1         73 $found->push( $rule );
170             }
171             }
172             }
173             ## The user is calling this in a chain context, we make sure this is possible using the Module::Generic::Null class if needed
174 1 50       51 if( Want::want( 'OBJECT' ) )
    50          
175             {
176 0 0       0 rreturn( $found->length > 0 ? $found->first : Module::Generic::Null->new );
177             }
178             elsif( Want::want( 'LIST' ) )
179             {
180 0         0 rreturn( @$found );
181             }
182             else
183             {
184 1         104 return( $found->first );
185             }
186             }
187              
188             sub load_parser
189             {
190 5     5 1 14 my $self = shift( @_ );
191 5         26 my $parser_class = $self->parser;
192 5 50       299 $self->_load_class( "$parser_class" ) || return( $self->error( "Unable to load parser class \"$parser_class\": ", $self->error ) );
193 5   50     926 my $parser = $parser_class->scalar->new( $self ) || return( $self->error( "Unable to instantiate parser \"$parser_class\" object: ", $parser_class->scalar->error ) );
194 5         57 $parser->debug( $self->debug );
195             # $self->message( 3, "Parser \"$parser_class\" initiated with object '$parser'." );
196 5         222 return( $parser );
197             }
198              
199             sub new_at_rule
200             {
201 0     0 0 0 my $self = shift( @_ );
202 0         0 my $o = CSS::Object::Rule::At->new( @_,
203             format => $self->format,
204             debug => $self->debug,
205             css => $self,
206             );
207 0 0       0 return( $self->error( "Cannot create a new at rule object: ", CSS::Object::Rule::At->error ) ) if( !defined( $o ) );
208 0         0 return( $o );
209             }
210              
211             sub new_keyframes_rule
212             {
213 1     1 0 22 my $self = shift( @_ );
214 1         4 my $o = CSS::Object::Rule::Keyframes->new( @_,
215             format => $self->format,
216             debug => $self->debug,
217             css => $self,
218             );
219 1 50       9 return( $self->error( "Cannot create a new keyframes rule object: ", CSS::Object::Rule::Keyframes->error ) ) if( !defined( $o ) );
220 1         5 return( $o );
221             }
222              
223             sub new_comment
224             {
225 5     5 1 54 my $self = shift( @_ );
226 5         30 my $o = CSS::Object::Comment->new( @_, format => $self->format, debug => $self->debug );
227 5 50       50 return( $self->error( "Cannot create a new comment object: ", CSS::Object::Comment->error ) ) if( !defined( $o ) );
228 5         29 return( $o );
229             }
230              
231             sub new_property
232             {
233 15     15 1 493 my $self = shift( @_ );
234 15         80 my $o = CSS::Object::Property->new( @_, format => $self->format, debug => $self->debug );
235 15 50       140 return( $self->error( "Cannot create a new property object: ", CSS::Object::Property->error ) ) if( !defined( $o ) );
236 15         79 return( $o );
237             }
238              
239             sub new_rule
240             {
241 26     26 1 923 my $self = shift( @_ );
242 26         152 $self->message( 3, "Creating new rule with formatter '", $self->format->class, "'." );
243 26         636 my $o = CSS::Object::Rule->new( @_, format => $self->format, debug => $self->debug );
244 26 50       222 return( $self->error( "Cannot create a new rule object: ", CSS::Object::Rule->error ) ) if( !defined( $o ) );
245             # $self->message( 3, "Returning \"", overload::StrVal( $o ), "\"." );
246 26         147 return( $o );
247             }
248              
249             sub new_selector
250             {
251 47     47 1 857 my $self = shift( @_ );
252 47         189 my $o = CSS::Object::Selector->new( @_, format => $self->format, debug => $self->debug );
253 47 50       362 return( $self->error( "Cannot create a new selector object: ", CSS::Object::Selector->error ) ) if( !defined( $o ) );
254 47         224 return( $o );
255             }
256              
257             sub new_value
258             {
259 0     0 1 0 my $self = shift( @_ );
260 0         0 my $o = CSS::Object::Value->new( @_, format => $self->format, debug => $self->debug );
261 0 0       0 return( $self->error( "Cannot create a new value object: ", CSS::Object::Value->error ) ) if( !defined( $o ) );
262 0         0 return( $o );
263             }
264              
265             sub parse_string
266             {
267 5     5 1 15 my $self = shift( @_ );
268 5         17 my $string = shift( @_ );
269 5         34 $self->message( 3, "Parsing string '$string'" );
270              
271             # remove comments
272             # $string =~ s!/\*.*?\*\/!!g;
273 5         107 $string =~ s|<!--||g;
274 5         21 $string =~ s|-->||g;
275            
276 5   50     26 my $parser = $self->load_parser || return( $self->pass_error );
277 5   50     26 my $elems = $parser->parse_string( $string ) || return( $self->pass_error( $parser->error ) );
278 5         30 $self->messagef( 3, "Parser returned %d elements.", $elems->length );
279 5         288059 $self->messagef( 3, "First element is of class \"", ref( $elems->first ), "\"." );
280             # $self->messagef( 3, "First rule has %d properties.", $rules->first->properties->length );
281 5         983 return( $elems );
282             }
283              
284 6     6 1 281 sub parser { return( shift->_set_get_scalar_as_object( 'parser', @_ ) ); }
285              
286 2     2 1 59163 sub purge { return( shift->elements->reset ); }
287              
288             sub read_file
289             {
290 5     5 1 61585 my $self = shift( @_ );
291 5         15 my $path = shift( @_ );
292              
293 5 50       41 if( ref( $path ) )
    50          
294             {
295 0 0       0 if( ref( $path ) eq 'ARRAY' )
296             {
297 0         0 $self->read_file( $_ ) for( @$path );
298 0         0 return( $self );
299             }
300             }
301             elsif( $path )
302             {
303 5         56 $self->message( 3, "Reading file \"$path\"." );
304 5   50     186 my $io = IO::File->new( "<$path" ) || return( $self->error( "Could not open file \"$path\": $!" ) );
305 5         859 $io->binmode( ':utf8' );
306 5         459 my $source = join( '', $io->getlines );
307 5         66 $io->close;
308 5         162 $self->messagef( 3, "%d bytes of data read.", CORE::length( $source ) );
309 5 50       152 if( $source )
310             {
311 5   50     30 my $elems = $self->parse_string( $source ) || return( $self->pass_error );
312 5         34 $self->messagef( 3, "%d elements found from parsing.", $elems->length );
313             # $self->rules->push( @$rules );
314 5         290588 $self->elements->push( @$elems );
315             }
316 5         438 return( $self );
317             }
318 0         0 return( $self->error( "Only scalars and arrays accepted: $!" ) );
319             }
320              
321             sub read_string
322             {
323 0     0 1 0 my $self = shift( @_ );
324 0         0 my $data = shift( @_ );
325              
326 0 0       0 if( ref( $data ) )
    0          
327             {
328 0 0       0 if( ref( $data ) eq 'ARRAY' )
329             {
330 0         0 for( @$data )
331             {
332 0 0       0 $self->read_string( $_ ) || return( $self->pass_error );
333             }
334 0         0 return( $self );
335             }
336             }
337             elsif( length( $data ) )
338             {
339 0   0     0 my $elems = $self->parse_string( $data ) || return( $self->pass_error );
340             ## $self->rules->push( @$rules );
341 0         0 $self->elements->push( @$elems );
342             }
343 0         0 return( $self );
344             }
345              
346             sub remove_rule
347             {
348 0     0 0 0 my $self = shift( @_ );
349 0         0 my $rule = shift( @_ );
350             # $self->message( 3, "CSS rule provided to add to our stack of elements: '", overload::StrVal( $rule ), "'." );
351 0 0       0 return( $self->error( "No rule object was provided to remove." ) ) if( !defined( $rule ) );
352 0 0       0 return( $self->error( "Object provided is not a CSS::Object::Rule object." ) ) if( !$self->_is_a( $rule, 'CSS::Object::Rule' ) );
353 0         0 $self->elements->remove( $rule );
354 0         0 return( $self );
355             }
356              
357             # sub rules { return( shift->_set_get_array_as_object( 'rules', @_ ) ); }
358 27 50   27 1 762 sub rules { return( $_[0]->elements->map(sub{ $_[0]->_is_a( $_, 'CSS::Object::Rule' ) ? $_ : () }) ); }
  6     6   121115  
359              
360             1;
361              
362             # XXX POD
363             __END__
364              
365             =encoding utf-8
366              
367             =head1 NAME
368              
369             CSS::Object - CSS Object Oriented
370              
371             =head1 SYNOPSIS
372              
373             use CSS::Object;
374              
375             =head1 VERSION
376              
377             v0.1.5
378              
379             =head1 DESCRIPTION
380              
381             L<CSS::Object> is a object oriented CSS parser and manipulation interface.
382              
383             =head1 CONSTRUCTOR
384              
385             =head2 new
386              
387             To instantiate a new L<CSS::Object> object, pass an hash reference of following parameters:
388              
389             =over 4
390              
391             =item I<debug>
392              
393             This is an integer. The bigger it is and the more verbose is the output.
394              
395             =item I<format>
396              
397             This is a L<CSS::Object::Format> object or one of its child modules.
398              
399             =item I<parser>
400              
401             This is a L<CSS::Object::Parser> object or one of its child modules.
402              
403             =back
404              
405             =head1 EXCEPTION HANDLING
406              
407             Whenever an error has occurred, L<CSS::Object> will set a L<Module::Generic::Exception> object containing the detail of the error and return undef.
408              
409             The error object can be retrieved with the inherited L<Module::Generic/error> method. For example:
410              
411             my $css = CSS::Object->new( debug => 3 ) || die( CSS::Object->error );
412              
413             =head1 METHODS
414              
415             =head2 add_element
416              
417             Provided with a L<CSS::Object::Element> object and this adds it to the list of css elements.
418              
419             It uses an array object L</elements> which is an L<Module::Generic::Array> object.
420              
421             =head2 add_rule
422              
423             Provided with a L<CSS::Object::Rule> object and this adds it to our list of rules. It returns the rule object that was added.
424              
425             =head2 as_string
426              
427             This will return the css data structure, currently registered, as a string.
428              
429             It takes an optional L<CSS::Object::Format> object as a parameter, to control the output. If none are provided, it will use the default one calling L</format>
430              
431             =head2 builder
432              
433             This returns a new L<CSS::Object::Builder> object.
434              
435             =head2 charset
436              
437             This sets or gets the css charset. It stores the value in a L<Module::Generic::Scalar> object.
438              
439             =head2 elements
440              
441             Sets or gets the array of CSS elements. This is a L<Module::Generic::Array> object that accepts only L<CSS::Object::Element> objects or its child classes, such as L<CSS::Object::Rule>, L<CSS::Object::Comment>, etc
442              
443             =head2 format
444              
445             Sets or gets a L<CSS::Object::Format> object. See L</as_string> below for more detail about their use.
446              
447             L<CSS::Object::Format> objects control the stringification of the css structure. By default, it will return the data in a string identical or at least very similar to the one parsed if it was parsed.
448              
449             =head2 get_rule_by_selector
450              
451             Provided with a selector and this returns a L<CSS::Object::Rule> object or an empty string.
452              
453             Hoever, if this method is called in an object context, such as chaining, then it returns a L<Module::Generic::Null> object instead of an empty string to prevent the perl error of C<xxx method called on an undefined value>. For example:
454              
455             $css->get_rule_by_selector( '.does-not-exists' )->add_element( $elem ) ||
456             die( "Unable to add css element to rule \".does-not-exists\": ", $css->error );
457              
458             But, in a non-object context, such as:
459              
460             my $rule = $css->get_rule_by_selector( '.does-not-exists' ) ||
461             die( "Unable to add css element to rule \".does-not-exists\": ", $css->error );
462              
463             L</get_rule_by_selector> will return an empty value.
464              
465             =head2 load_parser
466              
467             This will instantiate a new object based on the parser name specified with L</parser> or during css object instantiation.
468              
469             It returns a new L<CSS::Object::Parser> object, or one of its child module matching the L</parser> specified.
470              
471             =head2 new_comment
472              
473             This returns a new L<CSS::Object::Comment> object and pass its instantiation method the provided arguments.
474              
475             return( $css->new_comment( $array_ref_of_comment_ilnes ) );
476              
477             =head2 new_property
478              
479             This takes a property name, and an optional value o array of values and return a new L<CSS::Object::Property> object
480              
481             =head2 new_rule
482              
483             This returns a new L<CSS::Object::Rule> object.
484              
485             =head2 new_selector
486              
487             This takes a selector name and returns a new L<CSS::Object::Selector> object.
488              
489             =head2 new_value
490              
491             This takes a property value and returns a new L<CSS::Object::Value> object.
492              
493             =head2 parse_string
494              
495             Provided with some css data and this will instantiate the L</parser>, call L<CSS::Object::Parser/parse_string> and returns an array of L<CSS::Object::Rule> objects. The array is an array object from L<Module::Generic::Array> and can be used as a regular array or as an object.
496              
497             =head2 parser
498              
499             Sets or gets the L<CSS::Object::Parser> object to be used by L</parse_string> to parse css data.
500              
501             A valid parser object can be from L<CSS::Object::Parser> or any of its sub modules.
502              
503             It returns the current parser object.
504              
505             =head2 purge
506              
507             This empties the array containing all the L<CSS::Object::Rule> objects.
508              
509             =head2 read_file
510              
511             Provided with a css file, and this will load it into memory and parse it using the parser name registered with L</parser>.
512              
513             It can also take an array reference of css files who will be each fed to L</read_file>
514              
515             It returns the L<CSS::Object> used to call this method.
516              
517             =head2 read_string
518              
519             Provided with some css data, and this will call L</parse_string>. It also accepts an array reference of data.
520              
521             It returns the css object used to call this method.
522              
523             =head2 rules
524              
525             This sets or gets the L<Module::Generic::Array> object used to store all the L<CSS::Object::Rule> objects.
526              
527             =head1 AUTHOR
528              
529             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
530              
531             =head1 SEE ALSO
532              
533             L<CSS::Object>
534              
535             L<Mozilla documentation on Custom CSS Properties|https://developer.mozilla.org/en-US/docs/Web/CSS/--*>
536              
537             =head1 COPYRIGHT & LICENSE
538              
539             Copyright (c) 2020 DEGUEST Pte. Ltd.
540              
541             You can use, copy, modify and redistribute this package and associated
542             files under the same terms as Perl itself.
543              
544             =cut