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   249901 use v5.26;
  5         16  
4 5     5   1677 use Object::Pad;
  5         31090  
  5         26  
5 5     5   1353 use utf8;
  5         501  
  5         29  
6              
7             # SVG Parser, based on a modified version of XML::Tiny.
8              
9             class SVGPDF::Parser;
10              
11 5     5   3469 use File::LoadLines;
  5         88241  
  5         565  
12 5     5   34 use Carp;
  5         15  
  5         18185  
13              
14             field $debug;
15              
16 33     33 0 40 method parse_file ( $fname, %args ) {
  33         75  
  33         48  
  33         64  
  33         29  
17 33 50       73 $debug = $args{debug} if defined $args{debug};
18 33         169 my $data = loadlines( $fname, { split => 0, chomp => 0 } );
19 33         7782 $self->parse( $data, %args );
20             }
21              
22 36     36 0 525 method parse ( $data, %args ) {
  36         102  
  36         51  
  36         61  
  36         42  
23 36 50       78 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         124 $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   713 method _parse ( $data, %params) {
  44         86  
  44         72  
  44         57  
  44         52  
43 44         108 my $elem = { content => [] };
44              
45             # TODO: Accept whitespace tokens by default within elements.
46 44         91 my $whitespace_tokens = $params{whitespace_tokens};
47              
48 44   100     187 $re_name //= '[:_a-z][\\w:\\.-]*';
49 44         243 %emap = qw( lt < gt > amp & quot " apos ' );
50              
51 983     983   891 my $fixent = sub ( $e ) {
  983         1018  
  983         858  
52 983 100       1298 $e =~ s/&#(\d+);/chr($1)/ge && return $e;
  2         15  
53 981 100       1271 $e =~ s/&#(x[0-9a-f]+);/chr(hex($1))/gie && return $e;
  2         13  
54 979 100       1257 $e =~ s/&(lt|gt|quot|apos|amp);/$emap{$1}/ge && return $e;
  5         15  
55 978 100       1663 croak( "SVG Parser: Illegal ampersand or entity \"$1\"" )
56             if $e =~ /(&[^;]{0,10})/;
57 976         1788 $e;
58 44         209 };
59              
60 44 50 33     298 croak( "SVG Parser: No elements" ) if !defined($data) || $data !~ /\S/;
61              
62             # Illegal low-ASCII chars.
63 44 50       922 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         156 $data =~ s{}{
68 1         5 $_ = $1.chr(0); # this makes sure that empty CDATAs become
69 1         4 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     12109 || grep { $_ && /--/ }
  42 50 33     172  
83             ( $data =~ /^\s+||\s+$/gs); # -- in comm
84              
85             # Strip leading/trailing whitespace and comments (which don't nest - phew!).
86 44         10838 $data =~ s/^\s+||\s+$//gs;
87            
88             # Turn quoted > in attribs into >.
89             # Double- and single-quoted attrib values get done seperately.
90 44         75337 while ( $data =~ s/($re_name\s*=\s*"[^"]*)>([^"]*")/$1>$2/gsi ) {}
91 44         77748 while ( $data =~ s/($re_name\s*=\s*'[^']*)>([^']*')/$1>$2/gsi ) {}
92              
93 44 50 33     174 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       152 if ( $data =~ /\
99 0         0 $data =~ s;\s*;;;
100 0         0 $whitespace_tokens++;
101             }
102              
103             # Ignore empty tokens/whitespace tokens.
104 44         1062 foreach my $token ( grep { length }
  682         969  
105             split( /(<[^>]+>)/, $data ) ) {
106 617 100 100     1548 next if $token =~ /^\s+$/s && !$whitespace_tokens;
107 608 100 66     2562 next if $token =~ /<\?$re_name.*?\?>/is
108             || $token =~ /^
109              
110 602 100       3774 if ( $token =~ m!^!i ) { # close tag
    100          
    50          
111             croak( "SVG Parser: Not well-formed (at \"$token\")" )
112 116 50       298 if $elem->{name} ne $1;
113 116         186 $elem = delete $elem->{parent};
114             }
115             elsif ( $token =~ /^<$re_name(\s[^>]*)*(\s*\/)?>/is ) { # open tag
116 217         20364 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         4726 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       378 if ( @{$attrib} == 2 * keys %{{@{$attrib}}} ) {
  217         277  
  217         197  
  217         830  
125 217         212 $attrib = { @{$attrib} }
  217         465  
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         2476 $attribs_raw =~ s/\s($re_name)\s*=\s*"([^"]*?)"//gi;
134 217         903 $attribs_raw =~ s/\s($re_name)\s*=\s*'([^']*?)'//gi;
135             croak( "SVG Parser: Not well-formed ($attribs_raw)" )
136 217 50 33     476 if $attribs_raw =~ /\S/ || grep { /
  715         1259  
  217         374  
137              
138 217 100       382 unless ( $params{no_entity_parsing} ) {
139 216         191 foreach my $key ( keys %{$attrib} ) {
  216         470  
140 715         1000 ($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         724 $elem = { content => [],
146             name => $tagname,
147             type => 'e',
148             attrib => $attrib,
149             parent => $elem
150             };
151 217         254 push( @{ $elem->{parent}->{content} }, $elem );
  217         361  
152              
153             # Handle self-closing tags.
154 217 100       9741 if ( $token =~ /\s*\/>$/ ) {
155 99         185 $elem->{name} =~ s/\/$//;
156 99         210 $elem = delete( $elem->{parent} );
157             }
158             }
159             elsif ( $token =~ /^
160 0         0 croak( "SVG Parser: Unexpected \"$token\"" );
161             }
162             else { # ordinary content
163 269         391 $token =~ s/\x00//g; # get rid of our CDATA marker
164 269 100       400 unless ( $params{no_entity_parsing} ) {
165 268         318 $token = $fixent->($token);
166             }
167 267         286 push( @{$elem->{content}},
  267         734  
168             { content => $token, type => 't' } );
169             }
170             }
171             croak( "SVG Parser: Not well-formed (", $elem->{name}, " duplicated parent)" )
172 42 50       244 if exists($elem->{parent});
173              
174 42 100       85 if ( $whitespace_tokens ) {
175 34   66     77 while ( @{$elem->{content}} > 1
  40   66     163  
176             && $elem->{content}->[0]->{type} eq 't'
177             && $elem->{content}->[0]->{content} !~ /\S/
178             )
179             {
180 6         11 shift( @{$elem->{content}} );
  6         16  
181             }
182 34   33     42 while ( @{$elem->{content}} > 1
  34   33     104  
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     59 if @{$elem->{content}} == 0 || $elem->{content}->[0]->{type} ne 'e';
  42         195  
192              
193             # If multiple, they must be the same elements.
194 42         92 my $e = $elem->{content}->[0]->{name};
195 42         56 for ( @{$elem->{content}} ) {
  42         92  
196             croak( "SVG Parser: Junk in or after end of document" )
197 42 50       101 unless $_->{name} eq $e;
198             }
199              
200 42         454 return $elem->{content};
201             }
202              
203             1;