File Coverage

blib/lib/CSS/Object.pm
Criterion Covered Total %
statement 151 188 80.3
branch 30 78 38.4
condition 10 26 38.4
subroutine 37 43 86.0
pod 21 24 87.5
total 249 359 69.3


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## CSS Object Oriented - ~/lib/CSS/Object.pm
3             ## Version v0.2.0
4             ## Copyright(c) 2023 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2020/06/24
7             ## Modified 2024/09/05
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   1542739 use strict;
  6         17  
  6         270  
17 6     6   36 use warnings;
  6         11  
  6         424  
18 6     6   63 use warnings::register;
  6         22  
  6         400  
19 6     6   913 use parent qw( Module::Generic );
  6         643  
  6         46  
20 6     6   1427275 use CSS::Object::Builder;
  6         23  
  6         93  
21 6     6   5212 use CSS::Object::Comment;
  6         27  
  6         58  
22 6     6   1433 use CSS::Object::Format;
  6         11  
  6         42  
23 6     6   4554 use CSS::Object::Property;
  6         21  
  6         72  
24 6     6   1443 use CSS::Object::Rule;
  6         12  
  6         57  
25 6     6   4337 use CSS::Object::Rule::At;
  6         20  
  6         73  
26 6     6   4805 use CSS::Object::Rule::Keyframes;
  6         17  
  6         87  
27 6     6   5932 use CSS::Object::Selector;
  6         16  
  6         79  
28 6     6   1395 use CSS::Object::Value;
  6         11  
  6         40  
29 6     6   1151 use Want ();
  6         13  
  6         214  
30 6     6   16103 our $VERSION = 'v0.2.0';
31             };
32              
33             sub init
34             {
35 6     6 1 469531 my $self = shift( @_ );
36 6         474 $self->{parser} = 'CSS::Object::Parser::Default';
37 6         22 $self->{format} = '';
38 6         17 $self->{_init_strict_use_sub} = 1;
39 6 50       56 $self->SUPER::init( @_ ) || return( $self->pass_error );
40 6 100       54133 unless( $self->_is_a( $self->{format}, 'CSS::Object::Format' ) )
41             {
42 3         54 my $format = CSS::Object::Format->new(
43             debug => $self->debug
44             );
45 3         31 $self->format( $format );
46             }
47 6         192 $self->{rules} = Module::Generic::Array->new;
48 6         75 return( $self );
49             }
50              
51             # Add comment at the top level. To add comment inside a rule, see add_element in CSS::Object::Rule
52             sub add_element
53             {
54 1     1 1 3 my $self = shift( @_ );
55 1   50     7 my $elem = shift( @_ ) || return( $self->error( "No element object was provided to add to this rule." ) );
56 1 50       943 return( $self->error( "Element object provided ($elem) is not a CSS::Object::Element object." ) ) if( !$self->_is_a( $elem, 'CSS::Object::Element' ) );
57             # $elem->format( $self->format );
58 1         43 $elem->debug( $self->debug );
59             # $self->properties->push( $prop );
60 1         41 $self->elements->push( $elem );
61 1         665 return( $self );
62             }
63              
64             sub add_rule
65             {
66 9     9 1 21 my $self = shift( @_ );
67 9         22 my $rule = shift( @_ );
68 9 50       35 return( $self->error( "No rule object was provided to add." ) ) if( !defined( $rule ) );
69 9 50       28 return( $self->error( "Object provided is not a CSS::Object::Rule object." ) ) if( !$self->_is_a( $rule, 'CSS::Object::Rule' ) );
70             # $self->rules->push( $rule );
71 9         289 $self->elements->push( $rule );
72 9         7392 return( $rule );
73             }
74              
75             sub as_string
76             {
77 2     2 1 1550 my $self = shift( @_ );
78 2 50       46 if( @_ )
79             {
80 0         0 my $format = shift( @_ );
81 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' ) );
82             $self->elements->foreach(sub
83             {
84 0     0   0 shift->format( $format );
85 0         0 });
86             }
87              
88 2         14 my $output = Module::Generic::Array->new;
89             # $self->rules->foreach(sub
90             $self->elements->foreach(sub
91             {
92 4     4   5917 $output->push( shift->as_string );
93 2         24 });
94 2         2594 my $nl = $self->format->new_line;
95 2         2407 return( $output->join( "$nl$nl" )->scalar );
96             }
97              
98             sub builder
99             {
100 1     1 1 279 my $self = shift( @_ );
101 1 50       5 return( $self->{_builder} ) if( $self->_is_object( $self->{_builder} ) );
102 1   50     12 my $b = CSS::Object::Builder->new( $self, debug => $self->debug ) ||
103             return( $self->error( "Could not initialise the CSS builder: ", CSS::Object::Builder->error ) );
104 1         7 $self->{_builder} = $b;
105 1         2 return( $b );
106             }
107              
108 1     1 1 45 sub charset { return( shift->_set_get_scalar_as_object( 'charset', @_ ) ); }
109              
110             # Array of CSS::Object::Element objects or their sub classes
111 25     25 1 192 sub elements { return( shift->_set_get_object_array_object( 'elements', 'CSS::Object::Element', @_ ) ); }
112              
113             sub format
114             {
115 105     105 1 2376 my $self = shift( @_ );
116 105 100       406 if( @_ )
117             {
118 6         14 my $val = shift( @_ );
119 6         28 my $format;
120 6 100 33     39 if( ref( $val ) )
    50          
121             {
122 3   50     40 $format = $self->_set_get_object( 'format', 'CSS::Object::Format', $val ) || return( $self->pass_error );
123             }
124             # Formatter as a class name
125             elsif( !ref( $val ) && CORE::index( $val, '::' ) != -1 )
126             {
127 3 50       24 $self->_load_class( $val ) || return( $self->pass_error );
128 3   50     4573 $format = $val->new( debug => $self->debug ) || return( $self->pass_error( $val->error ) );
129 3         45 $self->_set_get_object( 'format', 'CSS::Object::Format', $format );
130             }
131             else
132             {
133 0         0 return( $self->error( "Unknown format \"$val\". I do not know what to do with it." ) );
134             }
135             $self->elements->foreach(sub
136             {
137 0 0   0   0 return(1) if( !$self->_is_object( $_[0] ) );
138 0 0       0 shift->format( $format ) || return;
139 6         2721 });
140 6         104958 return( $format );
141             }
142 99         442 return( $self->_set_get_object( 'format', 'CSS::Object::Format' ) );
143             }
144              
145             sub get_rule_by_selector
146             {
147 1     1 1 593 my( $self, $name ) = @_;
148 1 50       4 return( $self->error( "No selector was provided to find its equivalent rule object." ) ) if( !$name );
149 1         5 my $found = Module::Generic::Array->new;
150 1         10 foreach my $rule ( @{$self->elements} )
  1         4  
151             {
152 3 50       1254 next if( !$rule->isa( 'CSS::Object::Rule' ) );
153 3         5 foreach my $sel ( @{$rule->selectors} )
  3         10  
154             {
155 5 100       2824 if( $sel->name eq $name )
156             {
157             # return( $rule );
158 1         513 $found->push( $rule );
159             }
160             }
161             }
162             ## The user is calling this in a chain context, we make sure this is possible using the Module::Generic::Null class if needed
163 1 50       539 if( Want::want( 'OBJECT' ) )
    50          
164             {
165 0 0       0 rreturn( $found->length > 0 ? $found->first : Module::Generic::Null->new );
166             }
167             elsif( Want::want( 'LIST' ) )
168             {
169 0         0 rreturn( @$found );
170             }
171             else
172             {
173 1         95 return( $found->first );
174             }
175             }
176              
177             sub load_parser
178             {
179 5     5 1 13 my $self = shift( @_ );
180 5         22 my $parser_class = $self->parser;
181 5 50       118397 $self->_load_class( "$parser_class" ) || return( $self->error( "Unable to load parser class \"$parser_class\": ", $self->error ) );
182 5   50     7044 my $parser = $parser_class->scalar->new( $self ) || return( $self->error( "Unable to instantiate parser \"$parser_class\" object: ", $parser_class->scalar->error ) );
183 5         62 $parser->debug( $self->debug );
184 5         322 return( $parser );
185             }
186              
187             sub new_at_rule
188             {
189 0     0 0 0 my $self = shift( @_ );
190 0         0 my $o = CSS::Object::Rule::At->new( @_,
191             format => $self->format,
192             debug => $self->debug,
193             css => $self,
194             );
195 0 0       0 return( $self->error( "Cannot create a new at rule object: ", CSS::Object::Rule::At->error ) ) if( !defined( $o ) );
196 0         0 return( $o );
197             }
198              
199             sub new_keyframes_rule
200             {
201 1     1 0 39 my $self = shift( @_ );
202 1         7 my $o = CSS::Object::Rule::Keyframes->new( @_,
203             format => $self->format,
204             debug => $self->debug,
205             css => $self,
206             );
207 1 50       12 return( $self->error( "Cannot create a new keyframes rule object: ", CSS::Object::Rule::Keyframes->error ) ) if( !defined( $o ) );
208 1         10 return( $o );
209             }
210              
211             sub new_comment
212             {
213 5     5 1 52 my $self = shift( @_ );
214 5         33 my $o = CSS::Object::Comment->new( @_, format => $self->format, debug => $self->debug );
215 5 50       45 return( $self->error( "Cannot create a new comment object: ", CSS::Object::Comment->error ) ) if( !defined( $o ) );
216 5         25 return( $o );
217             }
218              
219             sub new_property
220             {
221 15     15 1 576 my $self = shift( @_ );
222 15         83 my $o = CSS::Object::Property->new( @_, format => $self->format, debug => $self->debug );
223 15 50       173 return( $self->error( "Cannot create a new property object: ", CSS::Object::Property->error ) ) if( !defined( $o ) );
224 15         98 return( $o );
225             }
226              
227             sub new_rule
228             {
229 26     26 1 1008 my $self = shift( @_ );
230 26         138 my $o = CSS::Object::Rule->new( @_, format => $self->format, debug => $self->debug );
231 26 50       229 return( $self->error( "Cannot create a new rule object: ", CSS::Object::Rule->error ) ) if( !defined( $o ) );
232 26         145 return( $o );
233             }
234              
235             sub new_selector
236             {
237 47     47 1 1050 my $self = shift( @_ );
238 47         237 my $o = CSS::Object::Selector->new( @_, format => $self->format, debug => $self->debug );
239 47 50       390 return( $self->error( "Cannot create a new selector object: ", CSS::Object::Selector->error ) ) if( !defined( $o ) );
240 47         292 return( $o );
241             }
242              
243             sub new_value
244             {
245 0     0 1 0 my $self = shift( @_ );
246 0         0 my $o = CSS::Object::Value->new( @_, format => $self->format, debug => $self->debug );
247 0 0       0 return( $self->error( "Cannot create a new value object: ", CSS::Object::Value->error ) ) if( !defined( $o ) );
248 0         0 return( $o );
249             }
250              
251             sub parse_string
252             {
253 5     5 1 14 my $self = shift( @_ );
254 5         12 my $string = shift( @_ );
255              
256             # remove comments
257             # $string =~ s!/\*.*?\*\/!!g;
258 5         27 $string =~ s|<!--||g;
259 5         18 $string =~ s|-->||g;
260            
261 5   50     27 my $parser = $self->load_parser || return( $self->pass_error );
262 5   50     27 my $elems = $parser->parse_string( $string ) || return( $self->pass_error( $parser->error ) );
263 5         51 return( $elems );
264             }
265              
266 6     6 1 432 sub parser { return( shift->_set_get_scalar_as_object( 'parser', @_ ) ); }
267              
268 2     2 1 52424 sub purge { return( shift->elements->reset ); }
269              
270             sub read_file
271             {
272 5     5 1 137714 my $self = shift( @_ );
273 5         19 my $path = shift( @_ );
274              
275 5 50       31 if( ref( $path ) )
    50          
276             {
277 0 0       0 if( ref( $path ) eq 'ARRAY' )
278             {
279 0         0 $self->read_file( $_ ) for( @$path );
280 0         0 return( $self );
281             }
282             }
283             elsif( $path )
284             {
285 5   50     72 my $io = IO::File->new( "<$path" ) || return( $self->error( "Could not open file \"$path\": $!" ) );
286 5         767 $io->binmode( ':utf8' );
287 5         467 my $source = join( '', $io->getlines );
288 5         54 $io->close;
289 5 50       115 if( $source )
290             {
291 5   50     27 my $elems = $self->parse_string( $source ) || return( $self->pass_error );
292             # $self->rules->push( @$rules );
293 5         3576 $self->elements->push( @$elems );
294             }
295 5         4818 return( $self );
296             }
297 0         0 return( $self->error( "Only scalars and arrays accepted: $!" ) );
298             }
299              
300             sub read_string
301             {
302 0     0 1 0 my $self = shift( @_ );
303 0         0 my $data = shift( @_ );
304              
305 0 0       0 if( ref( $data ) )
    0          
306             {
307 0 0       0 if( ref( $data ) eq 'ARRAY' )
308             {
309 0         0 for( @$data )
310             {
311 0 0       0 $self->read_string( $_ ) || return( $self->pass_error );
312             }
313 0         0 return( $self );
314             }
315             }
316             elsif( length( $data ) )
317             {
318 0   0     0 my $elems = $self->parse_string( $data ) || return( $self->pass_error );
319             ## $self->rules->push( @$rules );
320 0         0 $self->elements->push( @$elems );
321             }
322 0         0 return( $self );
323             }
324              
325             sub remove_rule
326             {
327 0     0 0 0 my $self = shift( @_ );
328 0         0 my $rule = shift( @_ );
329 0 0       0 return( $self->error( "No rule object was provided to remove." ) ) if( !defined( $rule ) );
330 0 0       0 return( $self->error( "Object provided is not a CSS::Object::Rule object." ) ) if( !$self->_is_a( $rule, 'CSS::Object::Rule' ) );
331 0         0 $self->elements->remove( $rule );
332 0         0 return( $self );
333             }
334              
335             # sub rules { return( shift->_set_get_array_as_object( 'rules', @_ ) ); }
336 27 50   27 1 4788 sub rules { return( $_[0]->elements->map(sub{ $_[0]->_is_a( $_, 'CSS::Object::Rule' ) ? $_ : () }) ); }
  6     6   113096  
337              
338             1;
339             # NOTE POD
340             __END__
341              
342             =encoding utf-8
343              
344             =head1 NAME
345              
346             CSS::Object - CSS Object Oriented
347              
348             =head1 SYNOPSIS
349              
350             use CSS::Object;
351             use LWP::UserAgent;
352             my $ua = LWP::UserAgent->new;
353             my $resp = $ua->get( $style_uri );
354             die( $resp->message ) if( $resp->is_error );
355             my $style = $resp->decoded_content;
356             my $css = CSS::Object->new;
357             $css->read_string( $style );
358             $css->rules->foreach(sub
359             {
360             my $rule = shift( @_ );
361             # more processing
362             });
363              
364             or, parsing inline stylesheets from a remote document:
365              
366             use HTML::Object;
367             use HTML::Object::XQuery;
368             use LWP::UserAgent;
369             my $ua = LWP::UserAgent->new;
370             my $resp = $ua->get( $document_uri );
371             die( $resp->message ) if( $resp->is_error );
372             my $html = $resp->decoded_content;
373             my $parser = HTML::Object->new;
374             my $doc = $parser->parse( $html ) || die( $parser->error );
375             my $styles = $doc->find( 'style' ) || die( $doc->error );
376             say "Nothing found", exit(0) unless( $styles->length > 0 );
377             my $data = $styles->children->first->text();
378             my $css = CSS::Object->new;
379             $css->read_string( $data );
380             $css->rules->foreach(sub
381             {
382             my $rule = shift( @_ );
383             # more processing
384             });
385              
386             Creating dynamically rules:
387              
388             use CSS::Object;
389             my $css = CSS::Object->new ||
390             die( CSS::Object->error );
391             my $b = $css->builder;
392             $b->select( ['#main_section > .article', 'section .article'] )
393             ->display( 'none' )
394             ->font_size( '+0.2rem' )
395             ->comment( ['Some multiline comment', 'that are made possible with array reference'] )
396             ->text_align( 'center' )
397             ->comment( 'Making it look pretty' )
398             ->padding( 5 );
399             $b->charset( 'UTF-8' );
400             $b->at( _webkit_keyframes => 'error' )
401             ->frame( 0, { _webkit_transform => 'translateX( 0px )' })
402             ->frame( 25, { _webkit_transform => 'translateX( 30px )' })
403             ->frame( 45, { _webkit_transform => 'translateX( -30px )' })
404             ->frame( 65, { _webkit_transform => 'translateX( 30px )' })
405             ->frame( 82, { _webkit_transform => 'translateX( -30px )' })
406             ->frame( 94, { _webkit_transform => 'translateX( 30px )' })
407             ->frame( [qw( 35 55 75 87 97 100 )], { _webkit_transform => 'translateX( 0px )' } );
408             say $css->as_string;
409              
410             =head1 VERSION
411              
412             v0.2.0
413              
414             =head1 DESCRIPTION
415              
416             L<CSS::Object> is a object oriented CSS parser and manipulation interface.
417              
418             =head1 CONSTRUCTOR
419              
420             =head2 new
421              
422             To instantiate a new L<CSS::Object> object, pass an hash reference of following parameters:
423              
424             =over 4
425              
426             =item I<debug>
427              
428             This is an integer. The bigger it is and the more verbose is the output.
429              
430             =item I<format>
431              
432             This is a L<CSS::Object::Format> object or one of its child modules.
433              
434             =item I<parser>
435              
436             This is a L<CSS::Object::Parser> object or one of its child modules.
437              
438             =back
439              
440             =head1 EXCEPTION HANDLING
441              
442             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.
443              
444             The error object can be retrieved with the inherited L<Module::Generic/error> method. For example:
445              
446             my $css = CSS::Object->new( debug => 3 ) || die( CSS::Object->error );
447              
448             =head1 METHODS
449              
450             =head2 add_element
451              
452             Provided with a L<CSS::Object::Element> object and this adds it to the list of css elements.
453              
454             It uses an array object L</elements> which is an L<Module::Generic::Array> object.
455              
456             =head2 add_rule
457              
458             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.
459              
460             =head2 as_string
461              
462             This will return the css data structure, currently registered, as a string.
463              
464             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>
465              
466             =head2 builder
467              
468             This returns a new L<CSS::Object::Builder> object.
469              
470             =head2 charset
471              
472             This sets or gets the css charset. It stores the value in a L<Module::Generic::Scalar> object.
473              
474             =head2 elements
475              
476             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
477              
478             =head2 format
479              
480             Sets or gets a L<CSS::Object::Format> object. See L</as_string> below for more detail about their use.
481              
482             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.
483              
484             =head2 get_rule_by_selector
485              
486             Provided with a selector and this returns a L<CSS::Object::Rule> object or an empty string.
487              
488             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:
489              
490             $css->get_rule_by_selector( '.does-not-exists' )->add_element( $elem ) ||
491             die( "Unable to add css element to rule \".does-not-exists\": ", $css->error );
492              
493             But, in a non-object context, such as:
494              
495             my $rule = $css->get_rule_by_selector( '.does-not-exists' ) ||
496             die( "Unable to add css element to rule \".does-not-exists\": ", $css->error );
497              
498             L</get_rule_by_selector> will return an empty value.
499              
500             =head2 load_parser
501              
502             This will instantiate a new object based on the parser name specified with L</parser> or during css object instantiation.
503              
504             It returns a new L<CSS::Object::Parser> object, or one of its child module matching the L</parser> specified.
505              
506             =head2 new_comment
507              
508             This returns a new L<CSS::Object::Comment> object and pass its instantiation method the provided arguments.
509              
510             return( $css->new_comment( $array_ref_of_comment_ilnes ) );
511              
512             =head2 new_property
513              
514             This takes a property name, and an optional value o array of values and return a new L<CSS::Object::Property> object
515              
516             =head2 new_rule
517              
518             This returns a new L<CSS::Object::Rule> object.
519              
520             =head2 new_selector
521              
522             This takes a selector name and returns a new L<CSS::Object::Selector> object.
523              
524             =head2 new_value
525              
526             This takes a property value and returns a new L<CSS::Object::Value> object.
527              
528             =head2 parse_string
529              
530             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.
531              
532             =head2 parser
533              
534             Sets or gets the L<CSS::Object::Parser> object to be used by L</parse_string> to parse css data.
535              
536             A valid parser object can be from L<CSS::Object::Parser> or any of its sub modules.
537              
538             It returns the current parser object.
539              
540             =head2 purge
541              
542             This empties the array containing all the L<CSS::Object::Rule> objects.
543              
544             =head2 read_file
545              
546             Provided with a css file, and this will load it into memory and parse it using the parser name registered with L</parser>.
547              
548             It can also take an array reference of css files who will be each fed to L</read_file>
549              
550             It returns the L<CSS::Object> used to call this method.
551              
552             =head2 read_string
553              
554             Provided with some css data, and this will call L</parse_string>. It also accepts an array reference of data.
555              
556             It returns the css object used to call this method.
557              
558             =head2 rules
559              
560             This sets or gets the L<Module::Generic::Array> object used to store all the L<CSS::Object::Rule> objects.
561              
562             =head1 AUTHOR
563              
564             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
565              
566             =head1 SEE ALSO
567              
568             L<CSS::Object>
569              
570             L<Mozilla documentation on Custom CSS Properties|https://developer.mozilla.org/en-US/docs/Web/CSS/--*>
571              
572             =head1 COPYRIGHT & LICENSE
573              
574             Copyright (c) 2020 DEGUEST Pte. Ltd.
575              
576             You can use, copy, modify and redistribute this package and associated
577             files under the same terms as Perl itself.
578              
579             =cut