File Coverage

blib/lib/CSS/Object/Parser/Enhanced.pm
Criterion Covered Total %
statement 22 105 20.9
branch 0 54 0.0
condition 0 48 0.0
subroutine 8 13 61.5
pod 3 3 100.0
total 33 223 14.8


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## CSS Object Oriented - ~/lib/CSS/Object/Parser/Enhanced.pm
3             ## Version v0.2.0
4             ## Copyright(c) 2020 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2020/08/09
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::Parser::Enhanced;
14             BEGIN
15             {
16 1     1   1312 use strict;
  1         3  
  1         44  
17 1     1   5 use warnings;
  1         2  
  1         50  
18 1     1   5 use Module::Generic;
  1         2  
  1         7  
19 1     1   277 use parent qw( CSS::Object::Parser );
  1         2  
  1         6  
20 1     1   68 use CSS::Object::Rule;
  1         2  
  1         6  
21 1     1   210 use CSS::Object::Selector;
  1         3  
  1         6  
22 1     1   219 use CSS::Object::Property;
  1         2  
  1         6  
23 1     1   1810 our $VERSION = 'v0.2.0';
24             };
25              
26             sub parse_string
27             {
28 0     0 1   my $self = shift( @_ );
29 0   0       my $string = shift( @_ ) || return;
30 0   0       my $css = $self->css || return( $self->error( "Our css object is gone!" ) );
31 0           my $this = {};
32 0           for( my $pos = 0; $pos < length( $string ); $pos++ )
33             {
34 0           my $c = substr( $string, $pos, 1 );
35 0           my $next = substr( $string, $pos + 1, 1 );
36 0 0         my $prev = $pos > 0 ? substr( $string, $pos - 1, 1 ) : '';
37 0 0 0       if( $c eq '*' && $next eq '/' )
    0 0        
    0 0        
    0          
    0          
38             {
39 0           $css->new_comment( [ split( /\r?\n/, $this->{line} ) ] )->add_to( $css );
40 0           $this->{line} = '';
41 0           $this->{inside_comment} = 0;
42 0           next;
43             }
44             ## We found a comment in between rules. Comments within rules are processed separately in parse_element
45             elsif( $c eq '/' && $next eq '*' )
46             {
47 0           $this->{inside_comment}++;
48 0           next;
49             }
50             elsif( $this->{inside_comment} )
51             {
52 0           $this->{line} .= $c;
53             }
54             elsif( $this->{inside_statement} )
55             {
56             ## If we found a space and the next character is an opening brace, we are inside the element definition
57 0 0 0       if( $c =~ /^[[:space:]\h]$/ &&
    0 0        
    0 0        
    0 0        
58             !$this->{inside_quote} &&
59             $next eq '{' )
60             {
61 0           $this->{name} = $this->{buffer};
62 0           $this->{name} = $self->_trim( $this->{name} );
63             ## $pos + 1 because we skip the opening brace
64             $pos = $self->parse_element({
65             data => \$string,
66             pos => $pos + 1,
67             name => $this->{name},
68 0           });
69             }
70             elsif( $this->{inside_quote} )
71             {
72 0 0 0       if( $c eq $this->{inside_quote} && $prev ne '\\' )
73             {
74 0           $this->{inside_quote} = '';
75             }
76 0           $this->{buffer} .= $c;
77             }
78             elsif( ( $c eq '"' || $c eq "'" ) && $prev ne '\\' )
79             {
80 0 0         if( $this->{inside_quote} )
81             {
82 0           $this->{inside_quote} = '';
83             }
84             else
85             {
86 0           $this->{inside_quote} = $c;
87             }
88 0           $this->{buffer} .= $c;
89             }
90             elsif( $this->{inside_quote} )
91             {
92 0           $this->{buffer} .= $c;
93             }
94             else
95             {
96 0           $this->{buffer} .= $c;
97             }
98             }
99             ## We may have found an element, check the first character
100             ## NOTE: Confirm with rfc for the lawful characters
101             elsif( !$this->{inside_statement} && $c =~ /^[\@\:\[\#\.a-zA-Z0-9]$/ )
102             {
103 0           $this->{inside_statement}++;
104 0           $this->{buffer} = $c;
105             }
106             }
107             }
108              
109             sub parse_element
110             {
111 0     0 1   my $self = shift( @_ );
112 0           my $opts = {};
113 0 0         $opts = shift( @_ ) if( $self->_is_hash( $_[0] ) );
114             ## String reference
115 0           my $sref = $opts->{data};
116 0           my $pos = $opts->{pos};
117 0 0         die( "Value provided is not a scalar reference\n" ) if( ref( $sref ) ne 'SCALAR' );
118 0 0         die( "Position provided is not an integer\n" ) if( $pos !~ /^\-?\d+$/ );
119 0   0       my $css = $self->css || return( $self->error( "Our css object is gone!" ) );
120 0   0       my $rule = $self->rule_from_token( $opts->{name} ) || return( $self->pass_error );
121 0           my $p;
122 0           my $this = {};
123 0           for( $p = $pos; $p < length( $$sref ); $p++ )
124             {
125 0           my $c = substr( $string, $p, 1 );
126 0           my $next = substr( $string, $p + 1, 1 );
127 0 0         my $prev = $p > 0 ? substr( $string, $p - 1, 1 ) : '';
128 0 0 0       if( ( $c eq "'" || $c eq '"' ) && $prev ne '\\' )
    0 0        
    0 0        
    0          
129             {
130 0 0         if( $this->{inside_quote} )
131             {
132 0           $this->{inside_quote} = '';
133             }
134             else
135             {
136 0           $this->{inside_quote} = $c;
137             }
138 0           $this->{buffer} .= $c;
139             }
140             elsif( $this->{inside_quote} )
141             {
142 0           $this->{buffer} .= $c;
143             }
144             elsif( !length( $this->{prop} ) )
145             {
146 0           $this->{prop} = $c;
147             }
148             ## We are done with this property, and either this is the start of a sub element, such as with keyframes containing braces for the definitions of each frames
149             elsif( $c eq '{' && $prev ne '\\' )
150             {
151 0           $this->{prop} .= $this->{buffer};
152 0           $this->{buffer} = '';
153 0           $this->{prop} = $self->_trim( $this->{prop} );
154             my $res = $self->parse_element({
155             name => $this->{prop},
156 0   0       data => $sref,
157             ## After the opening brace we just found
158             pos => $p + 1,
159             }) || return( $self->pass_error );
160 0           my $props = $res->{properties};
161 0           $p = $res->{pos};
162             }
163             ## or we found semicolon which signals the start of the property value
164             else
165             {
166 0           $this->{buffer} .= $c;
167             }
168             }
169 0           $pos = $p;
170 0           return({ pos => $pos });
171             }
172              
173             ## "There are two kinds of statements"
174             ## https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax#CSS_statements
175             sub rule_from_token
176             {
177 0     0 1   my $self = shift( @_ );
178 0   0       my $token = shift( @_ ) || return( $self->error( "No token was provided to create associated rule" ) );
179             ## If it's an at-rule
180 0 0         if( substr( $token, 0, 1 ) eq '@' )
181             {
182             }
183             else
184             {
185 0           my $selectors = $self->_split( $token );
186             }
187             }
188              
189             sub _split
190             {
191 0     0     my $self = shift( @_ );
192 0           my $token = shift( @_ );
193 0 0         return( [] ) if( !length( $token ) );
194 0           my $this = {};
195 0           for( my $i = 0; $i < length( $token ); $i++ )
196             {
197 0           my $c = substr( $token, $i, 1 );
198 0           my $next = substr( $token, $i + 1, 1 );
199 0 0         my $prev = $i > 0 ? substr( $token, $i - 1, 1 ) : '';
200 0 0         if( $this->{inside_quote} )
201             {
202 0 0 0       if( $c eq $this->{inside_quote} && $prev ne '\\' )
203             {
204 0           $this->{inside_quote} = '';
205             }
206 0           $this->{buffer} .= $c;
207             }
208             }
209             }
210              
211             sub _trim
212             {
213 0     0     my $self = shift( @_ );
214 0           my $text = shift( @_ );
215 0 0         return if( !length( $text ) );
216 0           $text =~ s/^[[:blank:]\h\r\n\v]+|[[:blank:]\h\r\n\v]+$//gs;
217 0           return( $text );
218             }
219              
220             1;
221             # NOTE: POD
222             __END__
223              
224             =encoding utf-8
225              
226             =head1 NAME
227              
228             CSS::Object::Parser::Enhanced - CSS Object Oriented Enhanced Parser
229              
230             =head1 SYNOPSIS
231              
232             use CSS::Object;
233             my $css = CSS::Object->new(
234             parser => 'CSS::Object::Parser::Enhanced',
235             format => $format_object,
236             debug => 3,
237             ) || die( CSS::Object->error );
238             $css->read( '/my/file.css' ) || die( $css->error );
239              
240             =head1 VERSION
241              
242             v0.2.0
243              
244             =head1 DESCRIPTION
245              
246             L<CSS::Object::Parser::Enhanced> is a lightweight, but thorough css parser. It aims at being very reliable and fast. The elements parsed are stored in a way so they can be stringified to produce a css stylesheet very close to the one that was parsed.
247              
248             =head1 CONSTRUCTOR
249              
250             =head2 new
251              
252             To instantiate a new L<CSS::Object::Parser::Enhanced> object, pass an hash reference of following parameters:
253              
254             =over 4
255              
256             =item I<debug>
257              
258             This is an integer. The bigger it is and the more verbose is the output.
259              
260             =back
261              
262             =head1 METHODS
263              
264             =head2 add_rule
265              
266             It takes 2 parameters: string of selectors and the rule content, i.e. inside the curly braces.
267              
268             It creates a new L<CSS::Object::Rule> object, adds to it a new L<CSS::Object::Selector> object for each selector found and also add a new L<CSS::Object::Property> object for each property found.
269              
270             It returns the rule object created.
271              
272             =head2 parse_element
273              
274             Provided with a set of parameters as an hash reference and this parse the element and returns a hash reference with 2 properties: I<rule> which is a L<CSS::Object::Rule> object and I<pos> which is an integer representing the position of the pointer in the parsed string.
275              
276             =head2 parse_string
277              
278             Provided with some css text data and this will parse it and return an array object of L<CSS::Object::Rule> objects. The array returned is an L<Module::Generic::Array> object.
279              
280             It does this by calling L</add_rule> on each rule found in the css data provided.
281              
282             Each L<CSS::Object::Rule> object containing one more more L<CSS::Object::Selector> objects and one or more L<CSS::Object::Property> objects.
283              
284             =head2 rule_from_token
285              
286             Provided with a css token and this returns an adequate rule object. CSS token can be a css selector, or an at rule
287              
288             =head1 AUTHOR
289              
290             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
291              
292             =head1 SEE ALSO
293              
294             L<CSS::Object>
295              
296             =head1 COPYRIGHT & LICENSE
297              
298             Copyright (c) 2020 DEGUEST Pte. Ltd.
299              
300             You can use, copy, modify and redistribute this package and associated
301             files under the same terms as Perl itself.
302              
303             =cut