File Coverage

blib/lib/SVGPDF/Parser.pm
Criterion Covered Total %
statement 113 123 91.8
branch 40 62 64.5
condition 19 38 50.0
subroutine 9 9 100.0
pod 0 2 0.0
total 181 234 77.3


line stmt bran cond sub pod time code
1             #! perl
2              
3 5     5   273196 use v5.26;
  5         15  
4 5     5   1607 use Object::Pad;
  5         29704  
  5         26  
5 5     5   1256 use utf8;
  5         466  
  5         35  
6              
7             # SVG Parser, based on a modified version of XML::Tiny.
8              
9             class SVGPDF::Parser;
10              
11 5     5   3580 use File::LoadLines;
  5         97729  
  5         484  
12 5     5   48 use Carp;
  5         9  
  5         18295  
13              
14             field $debug;
15              
16 33     33 0 34 method parse_file ( $fname, %args ) {
  33         54  
  33         38  
  33         88  
  33         49  
17 33 50       65 $debug = $args{debug} if defined $args{debug};
18 33         152 my $data = loadlines( $fname, { split => 0, chomp => 0 } );
19 33         6812 $self->parse( $data, %args );
20             }
21              
22 36     36 0 585 method parse ( $data, %args ) {
  36         105  
  36         51  
  36         66  
  36         40  
23 36 50       82 if ( $debug ) {
24             # Make it easier to read/write long lines and disable parts.
25 0         0 $data =~ s/^#.*//mg;
26 0         0 $data =~ s/\\[\n\r]+\s*//g;
27             }
28 36         103 $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 44     44   703 method _parse ( $data, %params) {
  44         80  
  44         75  
  44         59  
  44         76  
43 44         118 my $elem = { content => [] };
44              
45             # TODO: Accept whitespace tokens by default within elements.
46 44         106 my $whitespace_tokens = $params{whitespace_tokens};
47              
48 44   100     179 $re_name //= '[:_a-z][\\w:\\.-]*';
49 44         213 %emap = qw( lt < gt > amp & quot " apos ' );
50              
51 983     983   869 my $fixent = sub ( $e ) {
  983         967  
  983         788  
52 983 100       1286 $e =~ s/&#(\d+);/chr($1)/ge && return $e;
  2         16  
53 981 100       1193 $e =~ s/&#(x[0-9a-f]+);/chr(hex($1))/gie && return $e;
  2         13  
54 979 100       1137 $e =~ s/&(lt|gt|quot|apos|amp);/$emap{$1}/ge && return $e;
  5         16  
55 978 100       1548 croak( "SVG Parser: Illegal ampersand or entity \"$1\"" )
56             if $e =~ /(&[^;]{0,10})/;
57 976         1584 $e;
58 44         236 };
59              
60 44 50 33     314 croak( "SVG Parser: No elements" ) if !defined($data) || $data !~ /\S/;
61              
62             # Illegal low-ASCII chars.
63 44 50       926 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 44         150 $data =~ s{}{
68 1         5 $_ = $1.chr(0); # this makes sure that empty CDATAs become
69 1         3 s/([&<>'"])/ # the empty string and aren't just thrown away.
70 0 0       0 $1 eq '&' ? '&' :
    0          
    0          
    0          
71             $1 eq '<' ? '<' :
72             $1 eq '"' ? '"' :
73             $1 eq "'" ? ''' :
74             '>'
75             /eg;
76 1         4 $_;
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 44 100 33     11208 || grep { $_ && /--/ }
  42 50 33     185  
83             ( $data =~ /^\s+||\s+$/gs); # -- in comm
84              
85             # Strip leading/trailing whitespace and comments (which don't nest - phew!).
86 44         10792 $data =~ s/^\s+||\s+$//gs;
87            
88             # Turn quoted > in attribs into >.
89             # Double- and single-quoted attrib values get done seperately.
90 44         72823 while ( $data =~ s/($re_name\s*=\s*"[^"]*)>([^"]*")/$1>$2/gsi ) {}
91 44         68429 while ( $data =~ s/($re_name\s*=\s*'[^']*)>([^']*')/$1>$2/gsi ) {}
92              
93 44 50 33     150 if ( $params{fatal_declarations} && $data =~ /
94 0         0 croak( "SVG Parser: Unexpected \"$1\"" );
95             }
96              
97             # The abc2svg generator forgets the close the body. Fix it.
98 44 50       157 if ( $data =~ /\
99 0         0 $data =~ s;\s*;;;
100 0         0 $whitespace_tokens++;
101             }
102              
103             # Ignore empty tokens/whitespace tokens.
104 44         1063 foreach my $token ( grep { length }
  682         901  
105             split( /(<[^>]+>)/, $data ) ) {
106 617 100 100     1564 next if $token =~ /^\s+$/s && !$whitespace_tokens;
107 608 100 66     2514 next if $token =~ /<\?$re_name.*?\?>/is
108             || $token =~ /^
109              
110 602 100       3568 if ( $token =~ m!^!i ) { # close tag
    100          
    50          
111             croak( "SVG Parser: Not well-formed (at \"$token\")" )
112 116 50       274 if $elem->{name} ne $1;
113 116         156 $elem = delete $elem->{parent};
114             }
115             elsif ( $token =~ /^<$re_name(\s[^>]*)*(\s*\/)?>/is ) { # open tag
116 217         17425 my ( $tagname, $attribs_raw ) =
117             ( $token =~ m!<(\S*)(.*?)(\s*/)?>!s );
118             # First make attribs into a list so we can spot duplicate keys.
119 217         4503 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 217 50       321 if ( @{$attrib} == 2 * keys %{{@{$attrib}}} ) {
  217         244  
  217         224  
  217         759  
125 217         221 $attrib = { @{$attrib} }
  217         422  
126             }
127             else {
128 0         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 217         2420 $attribs_raw =~ s/\s($re_name)\s*=\s*"([^"]*?)"//gi;
134 217         805 $attribs_raw =~ s/\s($re_name)\s*=\s*'([^']*?)'//gi;
135             croak( "SVG Parser: Not well-formed ($attribs_raw)" )
136 217 50 33     388 if $attribs_raw =~ /\S/ || grep { /
  715         1063  
  217         347  
137              
138 217 100       341 unless ( $params{no_entity_parsing} ) {
139 216         219 foreach my $key ( keys %{$attrib} ) {
  216         422  
140 715         881 ($attrib->{$key} = $fixent->($attrib->{$key})) =~
141             s/\x00//g; # get rid of CDATA marker
142             }
143             }
144             # We have an element. Push it.
145 217         680 $elem = { content => [],
146             name => $tagname,
147             type => 'e',
148             attrib => $attrib,
149             parent => $elem
150             };
151 217         268 push( @{ $elem->{parent}->{content} }, $elem );
  217         375  
152              
153             # Handle self-closing tags.
154 217 100       7079 if ( $token =~ /\s*\/>$/ ) {
155 99         169 $elem->{name} =~ s/\/$//;
156 99         196 $elem = delete( $elem->{parent} );
157             }
158             }
159             elsif ( $token =~ /^
160 0         0 croak( "SVG Parser: Unexpected \"$token\"" );
161             }
162             else { # ordinary content
163 269         327 $token =~ s/\x00//g; # get rid of our CDATA marker
164 269 100       436 unless ( $params{no_entity_parsing} ) {
165 268         293 $token = $fixent->($token);
166             }
167 267         257 push( @{$elem->{content}},
  267         634  
168             { content => $token, type => 't' } );
169             }
170             }
171             croak( "SVG Parser: Not well-formed (", $elem->{name}, " duplicated parent)" )
172 42 50       186 if exists($elem->{parent});
173              
174 42 100       75 if ( $whitespace_tokens ) {
175 34   66     40 while ( @{$elem->{content}} > 1
  40   66     140  
176             && $elem->{content}->[0]->{type} eq 't'
177             && $elem->{content}->[0]->{content} !~ /\S/
178             )
179             {
180 6         7 shift( @{$elem->{content}} );
  6         13  
181             }
182 34   33     45 while ( @{$elem->{content}} > 1
  34   33     90  
183             && $elem->{content}->[-1]->{type} eq 't'
184             && $elem->{content}->[-1]->{content} !~ /\S/
185             )
186             {
187 0         0 pop( @{$elem->{content}} );
  0         0  
188             }
189             }
190             croak( "SVG Parser: No elements?" )
191 42 50 33     62 if @{$elem->{content}} == 0 || $elem->{content}->[0]->{type} ne 'e';
  42         194  
192              
193             # If multiple, they must be the same elements.
194 42         75 my $e = $elem->{content}->[0]->{name};
195 42         47 for ( @{$elem->{content}} ) {
  42         79  
196             croak( "SVG Parser: Junk in or after end of document" )
197 42 50       112 unless $_->{name} eq $e;
198             }
199              
200 42         423 return $elem->{content};
201             }
202              
203             1;