File Coverage

blib/lib/SVGPDF/Parser.pm
Criterion Covered Total %
statement 111 121 91.7
branch 40 62 64.5
condition 19 38 50.0
subroutine 9 9 100.0
pod 0 2 0.0
total 179 232 77.1


line stmt bran cond sub pod time code
1             #! perl
2              
3 5     5   372004 use v5.26;
  5         18  
4 5     5   2103 use Object::Pad;
  5         42495  
  5         32  
5 5     5   1772 use utf8;
  5         836  
  5         44  
6              
7             # SVG Parser, based on a modified version of XML::Tiny.
8              
9             class SVGPDF::Parser;
10              
11 5     5   4519 use File::LoadLines;
  5         129176  
  5         718  
12 5     5   53 use Carp;
  5         10  
  5         29601  
13              
14             field $debug;
15              
16 32     32 0 73 method parse_file ( $fname, %args ) {
  32         82  
  32         109  
  32         114  
  32         57  
17 32 50       135 $debug = $args{debug} if defined $args{debug};
18 32         264 my $data = loadlines( $fname, { split => 0, chomp => 0 } );
19 32         10520 $self->parse( $data, %args );
20             }
21              
22 35     35 0 590 method parse ( $data, %args ) {
  35         157  
  35         90  
  35         96  
  35         123  
23 35 50       148 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 35         212 $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 43     43   1168 method _parse ( $data, %params) {
  43         136  
  43         94  
  43         92  
  43         73  
43 43         168 my $elem = { content => [] };
44              
45             # TODO: Accept whitespace tokens by default within elements.
46 43         116 my $whitespace_tokens = $params{whitespace_tokens};
47              
48 43   100     259 $re_name //= '[:_a-z][\\w:\\.-]*';
49 43         362 %emap = qw( lt < gt > amp & quot " apos ' );
50              
51 852     852   1242 my $fixent = sub ( $e ) {
  852         1297  
  852         1133  
52 852 100       1900 $e =~ s/&#(\d+);/chr($1)/ge && return $e;
  2         24  
53 850 100       1671 $e =~ s/&#(x[0-9a-f]+);/chr(hex($1))/gie && return $e;
  2         20  
54 848 100       1836 $e =~ s/&(lt|gt|quot|apos|amp);/$emap{$1}/ge && return $e;
  5         29  
55 847 100       2324 croak( "SVG Parser: Illegal ampersand or entity \"$1\"" )
56             if $e =~ /(&[^;]{0,10})/;
57 845         2239 $e;
58 43         324 };
59              
60 43 50 33     395 croak( "SVG Parser: No elements" ) if !defined($data) || $data !~ /\S/;
61              
62             # Illegal low-ASCII chars.
63 43 50       1279 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 43         190 $data =~ s{}{
68 1         36 $_ = $1.chr(0); # this makes sure that empty CDATAs become
69 1         8 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         7 $_;
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 43 100 33     13985 || grep { $_ && /--/ }
  42 50 33     298  
83             ( $data =~ /^\s+||\s+$/gs); # -- in comm
84              
85             # Strip leading/trailing whitespace and comments (which don't nest - phew!).
86 43         14023 $data =~ s/^\s+||\s+$//gs;
87            
88             # Turn quoted > in attribs into >.
89             # Double- and single-quoted attrib values get done seperately.
90 43         91093 while ( $data =~ s/($re_name\s*=\s*"[^"]*)>([^"]*")/$1>$2/gsi ) {}
91 43         72618 while ( $data =~ s/($re_name\s*=\s*'[^']*)>([^']*')/$1>$2/gsi ) {}
92              
93 43 50 33     227 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 43 50       212 if ( $data =~ /\
99 0         0 $data =~ s;\s*;;;
100 0         0 $whitespace_tokens++;
101             }
102              
103             # Ignore empty tokens/whitespace tokens.
104 43         1356 foreach my $token ( grep { length }
  576         1332  
105             split( /(<[^>]+>)/, $data ) ) {
106 528 100 100     2456 next if $token =~ /^\s+$/s && !$whitespace_tokens;
107 519 100 66     3844 next if $token =~ /<\?$re_name.*?\?>/is
108             || $token =~ /^
109              
110 513 100       5314 if ( $token =~ m!^!i ) { # close tag
    100          
    50          
111             croak( "SVG Parser: Not well-formed (at \"$token\")" )
112 90 50       400 if $elem->{name} ne $1;
113 90         228 $elem = delete $elem->{parent};
114             }
115             elsif ( $token =~ /^<$re_name(\s[^>]*)*(\s*\/)?>/is ) { # open tag
116 190         20709 my ( $tagname, $attribs_raw ) =
117             ( $token =~ m!<(\S*)(.*?)(\s*/)?>!s );
118             # First make attribs into a list so we can spot duplicate keys.
119 190         6752 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 190 50       460 if ( @{$attrib} == 2 * keys %{{@{$attrib}}} ) {
  190         388  
  190         321  
  190         1150  
125 190         292 $attrib = { @{$attrib} }
  190         641  
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 190         3650 $attribs_raw =~ s/\s($re_name)\s*=\s*"([^"]*?)"//gi;
134 190         1184 $attribs_raw =~ s/\s($re_name)\s*=\s*'([^']*?)'//gi;
135             croak( "SVG Parser: Not well-formed ($attribs_raw)" )
136 190 50 33     585 if $attribs_raw =~ /\S/ || grep { /
  620         1543  
  190         600  
137              
138 190 100       535 unless ( $params{no_entity_parsing} ) {
139 189         270 foreach my $key ( keys %{$attrib} ) {
  189         686  
140 620         1471 ($attrib->{$key} = $fixent->($attrib->{$key})) =~
141             s/\x00//g; # get rid of CDATA marker
142             }
143             }
144             # We have an element. Push it.
145 190         1003 $elem = { content => [],
146             name => $tagname,
147             type => 'e',
148             attrib => $attrib,
149             parent => $elem
150             };
151 190         322 push( @{ $elem->{parent}->{content} }, $elem );
  190         586  
152              
153             # Handle self-closing tags.
154 190 100       8353 if ( $token =~ /\s*\/>$/ ) {
155 98         263 $elem->{name} =~ s/\/$//;
156 98         278 $elem = delete( $elem->{parent} );
157             }
158             }
159             elsif ( $token =~ /^
160 0         0 croak( "SVG Parser: Unexpected \"$token\"" );
161             }
162             else { # ordinary content
163 233         535 $token =~ s/\x00//g; # get rid of our CDATA marker
164 233 100       583 unless ( $params{no_entity_parsing} ) {
165 232         481 $token = $fixent->($token);
166             }
167 231         367 push( @{$elem->{content}},
  231         1037  
168             { content => $token, type => 't' } );
169             }
170             }
171             croak( "SVG Parser: Not well-formed (", $elem->{name}, " duplicated parent)" )
172 41 50       311 if exists($elem->{parent});
173              
174 41 100       124 if ( $whitespace_tokens ) {
175 33   66     64 while ( @{$elem->{content}} > 1
  39   66     239  
176             && $elem->{content}->[0]->{type} eq 't'
177             && $elem->{content}->[0]->{content} !~ /\S/
178             )
179             {
180 6         14 shift( @{$elem->{content}} );
  6         27  
181             }
182 33   33     72 while ( @{$elem->{content}} > 1
  33   33     198  
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: Junk after end of document" )
191 41 50       81 if @{$elem->{content}} > 1;
  41         147  
192             croak( "SVG Parser: No elements?" )
193 41 50 33     64 if @{$elem->{content}} == 0 || $elem->{content}->[0]->{type} ne 'e';
  41         273  
194              
195 41         639 return $elem->{content};
196             }
197              
198             1;