File Coverage

blib/lib/MarpaX/Languages/SVG/Parser/XMLHandler.pm
Criterion Covered Total %
statement 28 30 93.3
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 38 40 95.0


line stmt bran cond sub pod time code
1             package MarpaX::Languages::SVG::Parser::XMLHandler;
2              
3 1     1   17 use strict;
  1         3  
  1         35  
4 1     1   6 use warnings;
  1         2  
  1         35  
5 1     1   6 use warnings qw(FATAL utf8); # Fatalize encoding glitches.
  1         1  
  1         42  
6              
7 1     1   277 use Data::Section::Simple 'get_data_section';
  1         448  
  1         65  
8              
9 1     1   256 use Marpa::R2;
  1         109945  
  1         18  
10              
11 1     1   422 use MarpaX::Languages::SVG::Parser::Actions;
  1         3  
  1         9  
12              
13 1     1   338 use Moo;
  1         6743  
  1         5  
14              
15 1     1   1325 use Set::Array;
  1         7722  
  1         13  
16              
17 1     1   434 use Types::Standard qw/Any Int Object Str/;
  1         78054  
  1         14  
18              
19 1     1   105733 use XML::Parser;
  0            
  0            
20              
21             has grammar =>
22             (
23             default => sub{return ''},
24             is => 'rw',
25             isa => Any, # 'Marpa::R2::Scanless::G'.
26             required => 0,
27             );
28              
29             has item_count =>
30             (
31             default => sub{return 0},
32             is => 'rw',
33             isa => Int,
34             required => 0,
35             );
36              
37             has input_file_name =>
38             (
39             default => sub{return ''},
40             is => 'rw',
41             isa => Str,
42             required => 0,
43             );
44              
45             has items =>
46             (
47             default => sub{return Set::Array -> new},
48             is => 'rw',
49             isa => Object,
50             required => 0,
51             );
52              
53             has logger =>
54             (
55             default => sub{return undef},
56             is => 'rw',
57             isa => Any,
58             required => 0,
59             );
60              
61             has parser =>
62             (
63             default => sub{return ''},
64             is => 'rw',
65             isa => Any,
66             required => 0,
67             );
68              
69             has recce =>
70             (
71             default => sub{return ''},
72             is => 'rw',
73             isa => Any, # 'Marpa::R2::Scanless::R'.
74             required => 0,
75             );
76              
77             has text =>
78             (
79             default => sub{return ''},
80             is => 'rw',
81             isa => Any,
82             required => 0,
83             );
84              
85             has text_stack =>
86             (
87             default => sub{return Set::Array -> new},
88             is => 'rw',
89             isa => Object,
90             required => 0,
91             );
92              
93             my($myself);
94              
95             our $VERSION = '1.09';
96              
97             # -----------------------------------------------
98              
99             sub BUILD
100             {
101             my($self) = @_;
102             $myself = $self;
103              
104             # 1 of 2: Initialize the action class via global variables - Yuk!
105             # The point is that we don't create an action instance.
106             # Marpa creates one but we can't get our hands on it.
107              
108             $MarpaX::Languages::SVG::Parser::Actions::logger = $self -> logger;
109              
110             $self -> parser
111             (
112             XML::Parser -> new
113             (
114             NoExpand => 1,
115             ProtocolEncoding => 'ISO-8859-1',
116             Handlers =>
117             {
118             Char => \&handle_characters,
119             Comment => \&handle_comment,
120             Doctype => \&handle_doctype,
121             End => \&handle_end_tag,
122             Proc => \&handle_processing_instruction,
123             Start => \&handle_start_tag,
124             XMLDecl => \&handle_xml_declaration,
125             },
126             )
127             );
128              
129             # The 'if' is because the test code inputs strings, not files.
130              
131             $self -> parser -> parsefile($self -> input_file_name) if ($self -> input_file_name);
132              
133             } # End of BUILD.
134              
135             # ------------------------------------------------
136              
137             sub decode_result
138             {
139             my($self, $result) = @_;
140             my(@worklist) = $result;
141              
142             my($obj);
143             my($ref_type);
144             my(@stack);
145              
146             do
147             {
148             $obj = shift @worklist;
149             $ref_type = ref $obj;
150              
151             if ($ref_type eq 'ARRAY')
152             {
153             unshift @worklist, @$obj;
154             }
155             elsif ($ref_type eq 'HASH')
156             {
157             push @stack, {%$obj};
158             }
159             else
160             {
161             die "Unsupported object type $ref_type\n" if ($ref_type);
162             }
163             } while (@worklist);
164              
165             return [@stack];
166              
167             } # End of decode_result.
168              
169             # --------------------------------------------------
170              
171             sub encode_booleans
172             {
173             my($self, $str) = @_;
174             my(@str) = split(/([A-Za-z])/, $str);
175             my($inside_arc) = 0;
176              
177             my(@a);
178             my($j);
179             my($s, @s);
180              
181             while ($#str >= 0)
182             {
183             $s = shift @str;
184              
185             # If it's a elliptical arc, encode the 4th and 5th parameters from (0, 1) to (zero, one).
186              
187             if ($s =~ /\s*[Aa]\s*/)
188             {
189             push @s, $s;
190              
191             $s = shift @str;
192             $s =~ s/^\s+//;
193             $s =~ tr/ //c;
194             @a = split(/[\s,]/, $s);
195              
196             for $j (3 .. 4)
197             {
198             $a[$j] =~ s/^0$/ zero /g;
199             $a[$j] =~ s/^1$/ one /g;
200             }
201              
202             $s = join(' ', @a);
203             }
204              
205             push @s, $s;
206             }
207              
208             $str = join('', @s);
209              
210             return $str;
211              
212             } # End of encode_booleans.
213              
214             # ------------------------------------------------
215              
216             sub handle_characters
217             {
218             my($expat, $string) = @_;
219              
220             $myself -> text($myself -> text . $string);
221              
222             } # End of handle_characters.
223              
224             # ------------------------------------------------
225              
226             sub handle_comment
227             {
228             my($expat, $data) = @_;
229              
230             # Perl V 5.20.2 gets a segmentation fault if I try to access $data.
231              
232             #$myself -> new_item('Comment', 'comment', $data);
233              
234             } # End of handle_comment.
235              
236             # ------------------------------------------------
237              
238             sub handle_doctype
239             {
240             my($expat, @attributes) = @_;
241             my(%name) =
242             (
243             0 => 'name',
244             1 => 'sysid',
245             2 => 'pubid',
246             3 => 'internal',
247             );
248              
249             $myself -> new_item('Doctype', 'doctype', '-');
250              
251             for my $i (0 .. 3)
252             {
253             $myself -> new_item('attribute', $name{$i}, $attributes[$i]) if (defined $attributes[$i]);
254             }
255              
256             } # End of handle_doctype.
257              
258             # ------------------------------------------------
259              
260             sub handle_end_tag
261             {
262             my($expat, $tag) = @_;
263              
264             $myself -> new_item('content', $tag, $myself -> text);
265             $myself -> new_item('tag', $tag, 'close');
266             $myself -> text($myself -> text_stack -> pop -> print);
267              
268             } # End of handle_end_tag.
269              
270             # ------------------------------------------------
271              
272             sub handle_processing_instruction
273             {
274             my($expat, $target, $data) = @_;
275              
276             #say "PI. Target: <$target>. Data: <$data>";
277              
278             } # End of handle_processing_instruction.
279              
280             # ------------------------------------------------
281              
282             sub handle_start_tag
283             {
284             my($expat, $tag, %attributes) = @_;
285             my(%special) =
286             (
287             d => 1,
288             points => 1,
289             preserveAspectRatio => 1,
290             transform => 1,
291             viewBox => 1,
292             );
293              
294             # Stack the text parsed so far for the previous tag,
295             # and start accumulating the text for the current one.
296              
297             $myself -> text_stack -> push($myself -> text);
298             $myself -> text('');
299             $myself -> new_item('tag', $tag, 'open');
300              
301             my($value);
302              
303             for my $attribute (sort %attributes)
304             {
305             $value = $attributes{$attribute};
306              
307             next if (! defined $value);
308              
309             if ($special{$attribute})
310             {
311             $myself -> new_item('raw', $attribute, $value);
312             $myself -> run_marpa($attribute, $value);
313             }
314             else
315             {
316             $myself -> new_item('attribute', $attribute, $value);
317             }
318             }
319              
320             } # End of handle_start_tag.
321              
322             # ------------------------------------------------
323              
324             sub handle_xml_declaration
325             {
326             my($expat, @attributes) = @_;
327             my(%name) =
328             (
329             0 => 'version',
330             1 => 'encoding',
331             2 => 'standalone',
332             );
333              
334             $myself -> new_item('XMLDecl', 'xml', '-');
335              
336             for my $i (0 .. 2)
337             {
338             $myself -> new_item('attribute', $name{$i}, $attributes[$i]) if (defined $attributes[$i]);
339             }
340              
341             } # End of handle_xml_declaration.
342              
343             # --------------------------------------------------
344              
345             sub init_marpa
346             {
347             my($self, $attribute) = @_;
348              
349             # 2 of 2: Initialize the action class via global variables - Yuk!
350             # The point is that we don't create an action instance.
351             # Marpa creates one but we can't get our hands on it.
352              
353             MarpaX::Languages::SVG::Parser::Actions::init();
354              
355             $self -> grammar
356             (
357             Marpa::R2::Scanless::G -> new({source => \get_data_section("$attribute.bnf")})
358             );
359              
360             $self -> recce
361             (
362             Marpa::R2::Scanless::R -> new
363             ({
364             grammar => $self -> grammar,
365             semantics_package => 'MarpaX::Languages::SVG::Parser::Actions',
366             })
367             );
368              
369             } # End of init_marpa.
370              
371             # --------------------------------------------------
372              
373             sub log
374             {
375             my($self, $level, $s) = @_;
376             $level = 'notice' if (! defined $level);
377             $s = '' if (! defined $s);
378              
379             $self -> logger -> $level($s) if ($self -> logger);
380              
381             } # End of log.
382              
383             # --------------------------------------------------
384              
385             sub new_item
386             {
387             my($self, $type, $name, $value) = @_;
388              
389             $self -> item_count($self -> item_count + 1);
390             $self -> items -> push
391             ({
392             count => $self -> item_count,
393             name => $name,
394             type => $type,
395             value => $value,
396             });
397              
398             } # End of new_item.
399              
400             # -----------------------------------------------
401              
402             sub run_marpa
403             {
404             my($self, $attribute, $value) = @_;
405              
406             # The 2 flags in the (path) d's 'Aa' parameter list are Booleans.
407             # Here they are converted into 'zero' and 'one' to hide them from the
408             # code looking for numbers. Later, their values are restored.
409              
410             $value = ( ($attribute eq 'd') && ($value =~ /[Aa]/) )
411             ? $self -> encode_booleans($value)
412             : $value;
413              
414             $self -> log(debug => "Parsing: $value");
415             $self -> init_marpa($attribute);
416             $self -> recce -> read(\$value);
417              
418             my($result) = $self -> recce -> value;
419              
420             die "Marpa's parse failed\n" if (! defined $result);
421              
422             for my $item (@{$self -> decode_result($$result)})
423             {
424             if ($$item{type} eq 'command')
425             {
426             $self -> new_item($$item{type}, $$item{name}, '-');
427              
428             for my $param (@{$self -> decode_result($$item{value})})
429             {
430             $self -> new_item($$param{type}, $$param{name}, $$param{value});
431             }
432             }
433             else
434             {
435             $self -> new_item($$item{type}, $$item{name}, $$item{value});
436             }
437             }
438              
439             } # End of run_marpa.
440              
441             # -----------------------------------------------
442              
443             1;
444              
445             =pod
446              
447             =head1 NAME
448              
449             C - A nested SVG parser, using XML::SAX and Marpa::R2
450              
451             =head1 Synopsis
452              
453             See L.
454              
455             =head1 Description
456              
457             Basically just utility routines for L. Only used indirectly by
458             L.
459              
460             Specifically, parses an SVG file, and also runs L to parse the attribute value of some tag/attribute
461             combinations. Each such attribute value has its own Marpa-style BNF.
462              
463             Outputs to a stack managed by L. See L.
464              
465             =head1 Installation
466              
467             See L.
468              
469             =head1 Constructor and Initialization
470              
471             C is called as C<< my($handler) = MarpaX::Languages::SVG::Parser::SAXHandler -> new(k1 => v1, k2 => v2, ...) >>.
472              
473             It returns a new object of type C.
474              
475             Key-value pairs accepted in the parameter list (see also the corresponding methods
476             [e.g. L]):
477              
478             =over 4
479              
480             =item o logger => aLog::HandlerObject
481              
482             By default, an object of type L is created which prints to STDOUT,
483             but given the default, nothing is actually printed.
484              
485             Default: undef.
486              
487             =back
488              
489             =head1 Methods
490              
491             =head2 actions([$action_object])
492              
493             Here, the [] indicate an optional parameter.
494              
495             Get or set the action object.
496              
497             It is always an instance of L.
498              
499             =head2 characters($characters)
500              
501             A callback used to accumulate character text within XML tags.
502              
503             See L.
504              
505             =head2 encode_booleans($str)
506              
507             Replaces certain instances of 0 and 1 within $str with 'zero' and 'one' repectively.
508              
509             This is to stop the integer parser detecting them. Later, the original digits are restored.
510              
511             Returns $str.
512              
513             =head2 end_element($element)
514              
515             A callback used to log the closing of a tag.
516              
517             =head2 grammar([$grammar_object])
518              
519             Here, the [] indicate an optional parameter.
520              
521             Get or set the grammar object.
522              
523             It is always an instance of L.
524              
525             =head2 init_marpa(%args)
526              
527             Initialize Marpa using the BNF named with the 'attribute' key in %args, which must be one of the attributes' names
528             handled specially.
529              
530             Called by L.
531              
532             =head2 item_count([$new_value])
533              
534             Here, the [] indicate an optional parameter.
535              
536             Get or set the counter used to populate the C key in the hashref in the array of parsed tokens.
537              
538             =head2 items()
539              
540             Returns the instance of L which manages the array of hashrefs holding the parsed tokens.
541              
542             Note: C<< $object -> items -> print >> returns an array ref.
543              
544             See L for sample code.
545              
546             See also L.
547              
548             =head2 log($level, $s)
549              
550             Calls $self -> logger -> log($level => $s) if ($self -> logger).
551              
552             =head2 logger([$log_object])
553              
554             Here, the [] indicate an optional parameter.
555              
556             Get or set the log object.
557              
558             $log_object must be a L-compatible object.
559              
560             To disable logging, just set logger to the empty string.
561              
562             Note: C is a parameter to new().
563              
564             =head2 new()
565              
566             This method is auto-generated by L.
567              
568             =head2 new_item($type, $name, $value)
569              
570             Pushes another hashref onto the stack managed by $self -> items.
571              
572             =head2 recce([$recognizer_object])
573              
574             Here, the [] indicate an optional parameter.
575              
576             Get or set the recognizer object.
577              
578             It is always an instance of L.
579              
580             =head2 run_marpa(%args)
581              
582             Run's the instance of Marpa created in the call to L.
583              
584             %args is a hash of these (key => value) pairs:
585              
586             =over 4
587              
588             =item o attibute => $string
589              
590             See the docs for L above for valid values.
591              
592             =item o value => $string
593              
594             The string to be parsed, using the grammar named with (attribute => $string).
595              
596             =back
597              
598             =head2 start_document()
599              
600             Used as a way of initializing global variables in the action class.
601              
602             =head2 start_element($element)
603              
604             A callback used to log the opening of a tag.
605              
606             It also checks the names of all the tags attributes, and if there is one which is being treated specially (which is the
607             whole point of this distro), it loads the appropriate Marpa BNF and parses the value of the attribute.
608              
609             The result of this Marpa-based parse is a set of items pushed onto the stack managed by L.
610              
611             =head2 text($str)
612              
613             Accumulates the text belonging to the most recently opened tag.
614              
615             See L.
616              
617             =head2 test_stack([$text])
618              
619             Manages a stack of text-per-tag, since the text within a tag can be split when the tag contains nested tags.
620             With a stack, the inner tags can all have their own text.
621              
622             =head1 Credits
623              
624             The method L is a re-worked version of L's
625             process() method.
626              
627             =head1 Author
628              
629             L was written by Ron Savage Iron@savage.net.auE> in 2013.
630              
631             Home page: L.
632              
633             =head1 Copyright
634              
635             Australian copyright (c) 2013, Ron Savage.
636              
637             All Programs of mine are 'OSI Certified Open Source Software';
638             you can redistribute them and/or modify them under the terms of
639             The Artistic License 2.0, a copy of which is available at:
640             http://www.opensource.org/licenses/index.html
641              
642             =cut
643              
644             __DATA__