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   238815 use v5.26;
  5         14  
4 5     5   1563 use Object::Pad;
  5         28485  
  5         21  
5 5     5   1269 use utf8;
  5         538  
  5         30  
6              
7             # SVG Parser, based on a modified version of XML::Tiny.
8              
9             class SVGPDF::Parser;
10              
11 5     5   3354 use File::LoadLines;
  5         83100  
  5         502  
12 5     5   31 use Carp;
  5         5  
  5         17757  
13              
14             field $debug;
15              
16 33     33 0 35 method parse_file ( $fname, %args ) {
  33         67  
  33         35  
  33         52  
  33         29  
17 33 50       64 $debug = $args{debug} if defined $args{debug};
18 33         141 my $data = loadlines( $fname, { split => 0, chomp => 0 } );
19 33         8248 $self->parse( $data, %args );
20             }
21              
22 36     36 0 487 method parse ( $data, %args ) {
  36         138  
  36         60  
  36         53  
  36         67  
23 36 50       92 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         93 $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   711 method _parse ( $data, %params) {
  44         81  
  44         61  
  44         55  
  44         40  
43 44         125 my $elem = { content => [] };
44              
45             # TODO: Accept whitespace tokens by default within elements.
46 44         70 my $whitespace_tokens = $params{whitespace_tokens};
47              
48 44   100     170 $re_name //= '[:_a-z][\\w:\\.-]*';
49 44         218 %emap = qw( lt < gt > amp & quot " apos ' );
50              
51 983     983   920 my $fixent = sub ( $e ) {
  983         928  
  983         867  
52 983 100       1266 $e =~ s/&#(\d+);/chr($1)/ge && return $e;
  2         16  
53 981 100       1119 $e =~ s/&#(x[0-9a-f]+);/chr(hex($1))/gie && return $e;
  2         13  
54 979 100       1204 $e =~ s/&(lt|gt|quot|apos|amp);/$emap{$1}/ge && return $e;
  5         16  
55 978 100       1562 croak( "SVG Parser: Illegal ampersand or entity \"$1\"" )
56             if $e =~ /(&[^;]{0,10})/;
57 976         1716 $e;
58 44         202 };
59              
60 44 50 33     271 croak( "SVG Parser: No elements" ) if !defined($data) || $data !~ /\S/;
61              
62             # Illegal low-ASCII chars.
63 44 50       1000 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         117 $data =~ s{}{
68 1         4 $_ = $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         3 $_;
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     188  
83             ( $data =~ /^\s+||\s+$/gs); # -- in comm
84              
85             # Strip leading/trailing whitespace and comments (which don't nest - phew!).
86 44         10950 $data =~ s/^\s+||\s+$//gs;
87            
88             # Turn quoted > in attribs into >.
89             # Double- and single-quoted attrib values get done seperately.
90 44         73581 while ( $data =~ s/($re_name\s*=\s*"[^"]*)>([^"]*")/$1>$2/gsi ) {}
91 44         66329 while ( $data =~ s/($re_name\s*=\s*'[^']*)>([^']*')/$1>$2/gsi ) {}
92              
93 44 50 33     128 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       125 if ( $data =~ /\
99 0         0 $data =~ s;\s*;;;
100 0         0 $whitespace_tokens++;
101             }
102              
103             # Ignore empty tokens/whitespace tokens.
104 44         1141 foreach my $token ( grep { length }
  682         892  
105             split( /(<[^>]+>)/, $data ) ) {
106 617 100 100     1520 next if $token =~ /^\s+$/s && !$whitespace_tokens;
107 608 100 66     2500 next if $token =~ /<\?$re_name.*?\?>/is
108             || $token =~ /^
109              
110 602 100       3661 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         166 $elem = delete $elem->{parent};
114             }
115             elsif ( $token =~ /^<$re_name(\s[^>]*)*(\s*\/)?>/is ) { # open tag
116 217         17583 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         4534 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       322 if ( @{$attrib} == 2 * keys %{{@{$attrib}}} ) {
  217         251  
  217         207  
  217         755  
125 217         199 $attrib = { @{$attrib} }
  217         428  
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         2418 $attribs_raw =~ s/\s($re_name)\s*=\s*"([^"]*?)"//gi;
134 217         777 $attribs_raw =~ s/\s($re_name)\s*=\s*'([^']*?)'//gi;
135             croak( "SVG Parser: Not well-formed ($attribs_raw)" )
136 217 50 33     403 if $attribs_raw =~ /\S/ || grep { /
  715         1138  
  217         389  
137              
138 217 100       383 unless ( $params{no_entity_parsing} ) {
139 216         197 foreach my $key ( keys %{$attrib} ) {
  216         456  
140 715         958 ($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         653 $elem = { content => [],
146             name => $tagname,
147             type => 'e',
148             attrib => $attrib,
149             parent => $elem
150             };
151 217         239 push( @{ $elem->{parent}->{content} }, $elem );
  217         373  
152              
153             # Handle self-closing tags.
154 217 100       6795 if ( $token =~ /\s*\/>$/ ) {
155 99         150 $elem->{name} =~ s/\/$//;
156 99         179 $elem = delete( $elem->{parent} );
157             }
158             }
159             elsif ( $token =~ /^
160 0         0 croak( "SVG Parser: Unexpected \"$token\"" );
161             }
162             else { # ordinary content
163 269         326 $token =~ s/\x00//g; # get rid of our CDATA marker
164 269 100       370 unless ( $params{no_entity_parsing} ) {
165 268         346 $token = $fixent->($token);
166             }
167 267         291 push( @{$elem->{content}},
  267         653  
168             { content => $token, type => 't' } );
169             }
170             }
171             croak( "SVG Parser: Not well-formed (", $elem->{name}, " duplicated parent)" )
172 42 50       197 if exists($elem->{parent});
173              
174 42 100       80 if ( $whitespace_tokens ) {
175 34   66     39 while ( @{$elem->{content}} > 1
  40   66     139  
176             && $elem->{content}->[0]->{type} eq 't'
177             && $elem->{content}->[0]->{content} !~ /\S/
178             )
179             {
180 6         8 shift( @{$elem->{content}} );
  6         16  
181             }
182 34   33     39 while ( @{$elem->{content}} > 1
  34   33     118  
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     45 if @{$elem->{content}} == 0 || $elem->{content}->[0]->{type} ne 'e';
  42         186  
192              
193             # If multiple, they must be the same elements.
194 42         79 my $e = $elem->{content}->[0]->{name};
195 42         56 for ( @{$elem->{content}} ) {
  42         82  
196             croak( "SVG Parser: Junk in or after end of document" )
197 42 50       90 unless $_->{name} eq $e;
198             }
199              
200 42         424 return $elem->{content};
201             }
202              
203             1;