File Coverage

lib/ChordPro/lib/SVGPDF/Parser.pm
Criterion Covered Total %
statement 14 121 11.5
branch 0 62 0.0
condition 0 38 0.0
subroutine 5 9 55.5
pod 0 2 0.0
total 19 232 8.1


line stmt bran cond sub pod time code
1             #! perl
2              
3 1     1   13 use v5.26;
  1         3  
4 1     1   26 use Object::Pad;
  1         2  
  1         7  
5 1     1   101 use utf8;
  1         12  
  1         7  
6              
7             # SVG Parser, based on a modified version of XML::Tiny.
8              
9             class SVGPDF::Parser;
10              
11 1     1   315 use File::LoadLines;
  1         6  
  1         69  
12 1     1   7 use Carp;
  1         3  
  1         3681  
13              
14             field $debug;
15              
16 0     0 0   method parse_file ( $fname, %args ) {
  0            
  0            
  0            
  0            
17 0 0         $debug = $args{debug} if defined $args{debug};
18 0           my $data = loadlines( $fname, { split => 0, chomp => 0 } );
19 0           $self->parse( $data, %args );
20             }
21              
22 0     0 0   method parse ( $data, %args ) {
  0            
  0            
  0            
  0            
23 0 0         if ( $debug ) {
24             # Make it easier to read/write long lines and disable parts.
25 0           $data =~ s/^#.*//mg;
26 0           $data =~ s/\\[\n\r]+\s*//g;
27             }
28 0           $self->_parse( $data, %args );
29             }
30              
31             # The _parse method is a modified version of XML::Tiny. All comments
32             # and restrictions of L are applicable.
33             # Main modification is to allow whitespace elements in elements.
34             # These are significant in SVG.
35             # Since we're aiming at SVG parsing, and SVG is strict XML but often
36             # wrapped in an (X)HTML document, the parser functionality is set
37             # to no fatal_declarations and strict_entity_parsing.
38              
39             field $re_name;
40             field %emap;
41              
42 0     0     method _parse ( $data, %params) {
  0            
  0            
  0            
  0            
43 0           my $elem = { content => [] };
44              
45             # TODO: Accept whitespace tokens by default within elements.
46 0           my $whitespace_tokens = $params{whitespace_tokens};
47              
48 0   0       $re_name //= '[:_a-z][\\w:\\.-]*';
49 0           %emap = qw( lt < gt > amp & quot " apos ' );
50              
51 0     0     my $fixent = sub ( $e ) {
  0            
  0            
52 0 0         $e =~ s/&#(\d+);/chr($1)/ge && return $e;
  0            
53 0 0         $e =~ s/&#(x[0-9a-f]+);/chr(hex($1))/gie && return $e;
  0            
54 0 0         $e =~ s/&(lt|gt|quot|apos|amp);/$emap{$1}/ge && return $e;
  0            
55 0 0         croak( "SVG Parser: Illegal ampersand or entity \"$1\"" )
56             if $e =~ /(&[^;]{0,10})/;
57 0           $e;
58 0           };
59              
60 0 0 0       croak( "SVG Parser: No elements" ) if !defined($data) || $data !~ /\S/;
61              
62             # Illegal low-ASCII chars.
63 0 0         croak( "SVG Parser: Not well-formed (illegal low-ASCII chars)" )
64             if $data =~ /[\x00-\x08\x0b\x0c\x0e-\x1f]/;
65              
66             # Turn CDATA into PCDATA.
67 0           $data =~ s{}{
68 0           $_ = $1.chr(0); # this makes sure that empty CDATAs become
69 0           s/([&<>'"])/ # the empty string and aren't just thrown away.
70 0 0         $1 eq '&' ? '&' :
    0          
    0          
    0          
71             $1 eq '<' ? '<' :
72             $1 eq '"' ? '"' :
73             $1 eq "'" ? ''' :
74             '>'
75             /eg;
76 0           $_;
77             }egs;
78              
79             croak( "SVG Parser: Not well-formed (CDATA not delimited or bad comment)" )
80             if $data =~ /]]>/ # ]]> not delimiting CDATA
81             || $data =~ //s # ---> can't end a comment
82 0 0 0       || grep { $_ && /--/ }
  0 0 0        
83             ( $data =~ /^\s+||\s+$/gs); # -- in comm
84              
85             # Strip leading/trailing whitespace and comments (which don't nest - phew!).
86 0           $data =~ s/^\s+||\s+$//gs;
87            
88             # Turn quoted > in attribs into >.
89             # Double- and single-quoted attrib values get done seperately.
90 0           while ( $data =~ s/($re_name\s*=\s*"[^"]*)>([^"]*")/$1>$2/gsi ) {}
91 0           while ( $data =~ s/($re_name\s*=\s*'[^']*)>([^']*')/$1>$2/gsi ) {}
92              
93 0 0 0       if ( $params{fatal_declarations} && $data =~ /
94 0           croak( "SVG Parser: Unexpected \"$1\"" );
95             }
96              
97             # The abc2svg generator forgets the close the body. Fix it.
98 0 0         if ( $data =~ /\
99 0           $data =~ s;\s*;;;
100 0           $whitespace_tokens++;
101             }
102              
103             # Ignore empty tokens/whitespace tokens.
104 0           foreach my $token ( grep { length }
  0            
105             split( /(<[^>]+>)/, $data ) ) {
106 0 0 0       next if $token =~ /^\s+$/s && !$whitespace_tokens;
107 0 0 0       next if $token =~ /<\?$re_name.*?\?>/is
108             || $token =~ /^
109              
110 0 0         if ( $token =~ m!^!i ) { # close tag
    0          
    0          
111             croak( "SVG Parser: Not well-formed (at \"$token\")" )
112 0 0         if $elem->{name} ne $1;
113 0           $elem = delete $elem->{parent};
114             }
115             elsif ( $token =~ /^<$re_name(\s[^>]*)*(\s*\/)?>/is ) { # open tag
116 0           my ( $tagname, $attribs_raw ) =
117             ( $token =~ m!<(\S*)(.*?)(\s*/)?>!s );
118             # First make attribs into a list so we can spot duplicate keys.
119 0           my $attrib = [
120             # Do double- and single- quoted attribs seperately.
121             $attribs_raw =~ /\s($re_name)\s*=\s*"([^"]*?)"/gi,
122             $attribs_raw =~ /\s($re_name)\s*=\s*'([^']*?)'/gi
123             ];
124 0 0         if ( @{$attrib} == 2 * keys %{{@{$attrib}}} ) {
  0            
  0            
  0            
125 0           $attrib = { @{$attrib} }
  0            
126             }
127             else {
128 0           croak( "SVG Parser: Not well-formed (duplicate attribute)" );
129             }
130              
131             # Now trash any attribs that we *did* manage to parse and see
132             # if there's anything left.
133 0           $attribs_raw =~ s/\s($re_name)\s*=\s*"([^"]*?)"//gi;
134 0           $attribs_raw =~ s/\s($re_name)\s*=\s*'([^']*?)'//gi;
135             croak( "SVG Parser: Not well-formed ($attribs_raw)" )
136 0 0 0       if $attribs_raw =~ /\S/ || grep { /
  0            
  0            
137              
138 0 0         unless ( $params{no_entity_parsing} ) {
139 0           foreach my $key ( keys %{$attrib} ) {
  0            
140 0           ($attrib->{$key} = $fixent->($attrib->{$key})) =~
141             s/\x00//g; # get rid of CDATA marker
142             }
143             }
144             # We have an element. Push it.
145 0           $elem = { content => [],
146             name => $tagname,
147             type => 'e',
148             attrib => $attrib,
149             parent => $elem
150             };
151 0           push( @{ $elem->{parent}->{content} }, $elem );
  0            
152              
153             # Handle self-closing tags.
154 0 0         if ( $token =~ /\s*\/>$/ ) {
155 0           $elem->{name} =~ s/\/$//;
156 0           $elem = delete( $elem->{parent} );
157             }
158             }
159             elsif ( $token =~ /^
160 0           croak( "SVG Parser: Unexpected \"$token\"" );
161             }
162             else { # ordinary content
163 0           $token =~ s/\x00//g; # get rid of our CDATA marker
164 0 0         unless ( $params{no_entity_parsing} ) {
165 0           $token = $fixent->($token);
166             }
167 0           push( @{$elem->{content}},
  0            
168             { content => $token, type => 't' } );
169             }
170             }
171             croak( "SVG Parser: Not well-formed (", $elem->{name}, " duplicated parent)" )
172 0 0         if exists($elem->{parent});
173              
174 0 0         if ( $whitespace_tokens ) {
175 0   0       while ( @{$elem->{content}} > 1
  0   0        
176             && $elem->{content}->[0]->{type} eq 't'
177             && $elem->{content}->[0]->{content} !~ /\S/
178             )
179             {
180 0           shift( @{$elem->{content}} );
  0            
181             }
182 0   0       while ( @{$elem->{content}} > 1
  0   0        
183             && $elem->{content}->[-1]->{type} eq 't'
184             && $elem->{content}->[-1]->{content} !~ /\S/
185             )
186             {
187 0           pop( @{$elem->{content}} );
  0            
188             }
189             }
190             croak( "SVG Parser: Junk after end of document" )
191 0 0         if @{$elem->{content}} > 1;
  0            
192             croak( "SVG Parser: No elements?" )
193 0 0 0       if @{$elem->{content}} == 0 || $elem->{content}->[0]->{type} ne 'e';
  0            
194              
195 0           return $elem->{content};
196             }
197              
198             1;