File Coverage

blib/lib/XML/Handler/Composer.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package XML::Handler::Composer;
2 1     1   6566 use strict;
  1         2  
  1         37  
3 1     1   1754 use XML::UM;
  0            
  0            
4             use Carp;
5              
6             use vars qw{ $VERSION %DEFAULT_QUOTES %XML_MAPPING_CRITERIA };
7              
8             $VERSION = '0.01';
9              
10             %DEFAULT_QUOTES = (
11             XMLDecl => '"',
12             Attr => '"',
13             Entity => '"',
14             SystemLiteral => '"',
15             );
16              
17             %XML_MAPPING_CRITERIA =
18             (
19             Text =>
20             {
21             '<' => '<',
22             '&' => '&',
23              
24             ']]>' => ']]>',
25             },
26              
27             CDataSection =>
28             {
29             ']]>' => ']]>', # NOTE: this won't be translated back correctly
30             },
31              
32             Attr => # attribute value (assuming double quotes "" are used)
33             {
34             # '"' => '"', # Use ("'" => ''') when using single quotes
35             '<' => '<',
36             '&' => '&',
37             },
38              
39             Entity => # entity value (assuming double quotes "" are used)
40             {
41             # '"' => '"', # Use ("'" => ''') when using single quotes
42             '%' => '%',
43             '&' => '&',
44             },
45              
46             Comment =>
47             {
48             '--' => '--', # NOTE: this won't be translated back correctly
49             },
50              
51             ProcessingInstruction =>
52             {
53             '?>' => '?>', # not sure if this will be translated back correctly
54             },
55              
56             # The SYSTEM and PUBLIC identifiers in DOCTYPE declaration (quoted strings)
57             SystemLiteral =>
58             {
59             # '"' => '"', # Use ("'" => ''') when using single quotes
60             },
61              
62             );
63              
64             sub new
65             {
66             my ($class, %options) = @_;
67             my $self = bless \%options, $class;
68              
69             $self->{EndWithNewline} = 1 unless defined $self->{EndWithNewline};
70              
71             if (defined $self->{Newline})
72             {
73             $self->{ConvertNewlines} = 1;
74             }
75             else
76             {
77             # Use this when printing newlines in case the user didn't specify one
78             $self->{Newline} = "\x0A";
79             }
80              
81             $self->{DocTypeIndent} = $self->{Newline} . " "
82             unless defined $self->{DocTypeIndent};
83              
84             $self->{IndentAttlist} = " " unless defined $self->{IndentAttlist};
85              
86             $self->{Print} = sub { print @_ } unless defined $self->{Print};
87              
88             $self->{Quote} ||= {};
89             for my $q (keys %DEFAULT_QUOTES)
90             {
91             $self->{Quote}->{$q} ||= $DEFAULT_QUOTES{$q};
92             }
93              
94             # Convert to UTF-8 by default, i.e. when is missing
95             # and no {Encoding} is specified.
96             # Note that the internal representation *is* UTF-8, so we
97             # simply return the (string) parameter.
98             $self->{Encode} = sub { shift } unless defined $self->{Encode};
99              
100             # Convert unmapped characters to hexadecimal constants a la '号'
101             $self->{EncodeUnmapped} = \&XML::UM::encode_unmapped_hex
102             unless defined $self->{EncodeUnmapped};
103              
104             my $encoding = $self->{Encoding};
105             $self->setEncoding ($encoding) if defined $encoding;
106              
107             $self->initMappers;
108              
109             $self;
110             }
111              
112             #
113             # Setup the mapping routines that convert '<' to '<' etc.
114             # for the specific XML constructs.
115             #
116             sub initMappers
117             {
118             my $self = shift;
119             my %escape;
120             my $convert_newlines = $self->{ConvertNewlines};
121              
122             for my $n (qw{ Text Comment CDataSection Attr SystemLiteral
123             ProcessingInstruction Entity })
124             {
125             $escape{$n} = $self->create_utf8_mapper ($n, $convert_newlines);
126             }
127              
128             # Text with xml:space="preserve", should not have newlines converted.
129             $escape{TextPreserveNL} = $self->create_utf8_mapper ('Text', 0);
130             # (If newline conversion is inactive, $escape{TextPreserveNL} does the
131             # same as $escape{Text} defined above ...)
132              
133             $self->{Escape} = \%escape;
134             }
135              
136             sub setEncoding
137             {
138             my ($self, $encoding) = @_;
139              
140             $self->{Encode} = XML::UM::get_encode (
141             Encoding => $encoding, EncodeUnmapped => $self->{EncodeUnmapped});
142             }
143              
144             sub create_utf8_mapper
145             {
146             my ($self, $construct, $convert_newlines) = @_;
147              
148             my $c = $XML_MAPPING_CRITERIA{$construct};
149             croak "no XML mapping criteria defined for $construct"
150             unless defined $c;
151              
152             my %hash = %$c;
153              
154             # If this construct appears between quotes in the XML document
155             # (and it has a quoting character defined),
156             # ensure that the quoting character is appropriately converted
157             # to " or '
158             my $quote = $self->{Quote}->{$construct};
159             if (defined $quote)
160             {
161             $hash{$quote} = $quote eq '"' ? '"' : ''';
162             }
163              
164             if ($convert_newlines)
165             {
166             $hash{"\x0A"} = $self->{Newline};
167             }
168              
169             gen_utf8_subst (%hash);
170             }
171              
172             #
173             # Converts a string literal e.g. "ABC" into '\x41\x42\x43'
174             # so it can be stuffed into a regular expression
175             #
176             sub str_to_hex # static
177             {
178             my $s = shift;
179              
180             $s =~ s/(.)/ sprintf ("\\x%02x", ord ($1)) /egos;
181              
182             $s;
183             }
184              
185             #
186             # In later perl versions (5.005_55 and up) we can simply say:
187             #
188             # use utf8;
189             # $literals = join ("|", map { str_to_hex ($_) } keys %hash);
190             # $s =~ s/($literals)/$hash{$1}/ego;
191             #
192              
193             sub gen_utf8_subst # static
194             {
195             my (%hash) = @_;
196              
197             my $code = 'sub { my $s = shift; $s =~ s/(';
198             $code .= join ("|", map { str_to_hex ($_) } keys %hash);
199             $code .= ')|(';
200             $code .= '[\\x00-\\xBF]|[\\xC0-\\xDF].|[\\xE0-\\xEF]..|[\\xF0-\\xFF]...';
201             $code .= ')/ defined ($1) ? $hash{$1} : $2 /ego; $s }';
202              
203             my $f = eval $code;
204             croak "XML::Handler::Composer - can't eval code: $code\nReason: $@" if $@;
205              
206             $f;
207             }
208              
209             # This should be optimized!
210             sub print
211             {
212             my ($self, $str) = @_;
213             $self->{Print}->($self->{Encode}->($str));
214             }
215              
216             # Used by start_element. It determines the style in which empty elements
217             # are printed. The default implementation returns "/>" so they are printed
218             # like this:
219             # Override this method to support e.g. XHTML style tags.
220             sub get_compressed_element_suffix
221             {
222             my ($self, $event) = @_;
223              
224             "/>";
225              
226             # return " />" for XHTML style, or
227             # "><$tagName/>" for uncompressed tags (where $tagName is $event->{Name})
228             }
229              
230             #----- PerlSAX handlers -------------------------------------------------------
231              
232             sub start_document
233             {
234             my ($self) = @_;
235              
236             $self->{InCDATA} = 0;
237             $self->{DTD} = undef;
238             $self->{PreserveWS} = 0; # root element has xml:space="default"
239             $self->{PreserveStack} = [];
240             $self->{PrintedXmlDecl} = 0; # whether was printed
241             }
242              
243             sub end_document
244             {
245             my ($self) = @_;
246              
247             # Print final Newline at the end of the XML document (if desired)
248             $self->print ($self->{Newline}) if $self->{EndWithNewline};
249             }
250              
251             # This event is received *AFTER* the Notation, Element, Attlist etc. events
252             # that are contained within the DTD.
253             sub doctype_decl
254             {
255             my ($self, $event) = @_;
256             $self->flush_xml_decl;
257              
258             my $q = $self->{Quote}->{SystemLiteral};
259             my $escape_literal = $self->{Escape}->{SystemLiteral};
260              
261             my $name = $event->{Name};
262             my $sysId = $event->{SystemId};
263             $sysId = &$escape_literal ($sysId) if defined $sysId;
264             my $pubId = $event->{PublicId};
265             $pubId = &$escape_literal ($pubId) if defined $pubId;
266              
267             my $str = "
268             if (defined $pubId)
269             {
270             $str .= " PUBLIC $q$pubId$q $q$sysId$q";
271             }
272             elsif (defined $sysId)
273             {
274             $str .= " SYSTEM $q$sysId$q";
275             }
276              
277             my $dtd_contents = $self->{DTD};
278             my $nl = $self->{Newline};
279            
280             if (defined $dtd_contents)
281             {
282             delete $self->{DTD};
283            
284             $str .= " [$dtd_contents$nl]>$nl";
285             }
286             else
287             {
288             $str .= ">$nl";
289             }
290             $self->print ($str);
291             }
292              
293             sub start_element
294             {
295             my ($self, $event) = @_;
296              
297             my $preserve_stack = $self->{PreserveStack};
298             if (@$preserve_stack == 0)
299             {
300             # This is the root element. Print the declaration now if
301             # it wasn't printed and it should be.
302             $self->flush_xml_decl;
303             }
304              
305             my $str = "<" . $event->{Name};
306              
307             my $suffix = ">";
308             if ($event->{Compress})
309             {
310             $suffix = $self->get_compressed_element_suffix ($event);
311             }
312              
313             # Push PreserveWS state of parent element on the stack
314             push @{ $preserve_stack }, $self->{PreserveWS};
315             $self->{PreserveWS} = $event->{PreserveWS};
316              
317             my $ha = $event->{Attributes};
318             my @attr;
319             if (exists $event->{AttributeOrder})
320             {
321             my $defaulted = $event->{Defaulted};
322             if (defined $defaulted && !$self->{PrintDefaultAttr})
323             {
324             if ($defaulted > 0)
325             {
326             @attr = @{ $event->{AttributeOrder} }[0 .. $defaulted - 1];
327             }
328             # else: all attributes are defaulted i.e. @attr = ();
329             }
330             else # no attr are defaulted
331             {
332             @attr = @{ $event->{AttributeOrder} };
333             }
334             }
335             else # no attr order defined
336             {
337             @attr = keys %$ha;
338             }
339              
340             my $escape = $self->{Escape}->{Attr};
341             my $q = $self->{Quote}->{Attr};
342              
343             for (my $i = 0; $i < @attr; $i++)
344             {
345             #?? could print a newline every so often...
346             my $name = $attr[$i];
347             my $val = &$escape ($ha->{$name});
348             $str .= " $name=$q$val$q";
349             }
350             $str .= $suffix;
351              
352             $self->print ($str);
353             }
354              
355             sub end_element
356             {
357             my ($self, $event) = @_;
358              
359             $self->{PreserveWS} = pop @{ $self->{PreserveStack} };
360              
361             return if $event->{Compress};
362              
363             $self->print ("{Name} . ">");
364             }
365              
366             sub characters
367             {
368             my ($self, $event) = @_;
369              
370             if ($self->{InCDATA})
371             {
372             #?? should this use $self->{PreserveWS} ?
373              
374             my $esc = $self->{Escape}->{CDataSection};
375             $self->print (&$esc ($event->{Data}));
376             }
377             else # regular text
378             {
379             my $esc = $self->{PreserveWS} ?
380             $self->{Escape}->{TextPreserveNL} :
381             $self->{Escape}->{Text};
382              
383             $self->print (&$esc ($event->{Data}));
384             }
385             }
386              
387             sub start_cdata
388             {
389             my $self = shift;
390             $self->{InCDATA} = 1;
391              
392             $self->print ("
393             }
394              
395             sub end_cdata
396             {
397             my $self = shift;
398             $self->{InCDATA} = 0;
399              
400             $self->print ("]]>");
401             }
402              
403             sub comment
404             {
405             my ($self, $event) = @_;
406             $self->flush_xml_decl;
407              
408             my $esc = $self->{Escape}->{Comment};
409             #?? still need to support comments in the DTD
410              
411             $self->print ("");
412             }
413              
414             sub entity_reference
415             {
416             my ($self, $event) = @_;
417             $self->flush_xml_decl;
418              
419             my $par = $event->{Parameter} ? '%' : '&';
420             #?? parameter entities (like %par;) are NOT supported!
421             # PerlSAX::handle_default should be fixed!
422              
423             $self->print ($par . $event->{Name} . ";");
424             }
425              
426             sub unparsed_entity_decl
427             {
428             my ($self, $event) = @_;
429             $self->flush_xml_decl;
430              
431             $self->entity_decl ($event);
432             }
433              
434             sub notation_decl
435             {
436             my ($self, $event) = @_;
437             $self->flush_xml_decl;
438              
439             my $name = $event->{Name};
440             my $sysId = $event->{SystemId};
441             my $pubId = $event->{PublicId};
442              
443             my $q = $self->{Quote}->{SystemLiteral};
444             my $escape = $self->{Escape}->{SystemLiteral};
445              
446             $sysId = &$escape ($sysId) if defined $sysId;
447             $pubId = &$escape ($pubId) if defined $pubId;
448              
449             my $str = $self->{DocTypeIndent} . "
450              
451             if (defined $pubId)
452             {
453             $str .= " PUBLIC $q$pubId$q";
454             }
455             if (defined $sysId)
456             {
457             $str .= " SYSTEM $q$sysId$q";
458             }
459             $str .= ">";
460              
461             $self->{DTD} .= $str;
462             }
463              
464             sub element_decl
465             {
466             my ($self, $event) = @_;
467             $self->flush_xml_decl;
468              
469             my $name = $event->{Name};
470             my $model = $event->{Model};
471              
472             $self->{DTD} .= $self->{DocTypeIndent} . "";
473             }
474              
475             sub entity_decl
476             {
477             my ($self, $event) = @_;
478             $self->flush_xml_decl;
479              
480             my $name = $event->{Name};
481              
482             my $par = "";
483             if ($name =~ /^%/)
484             {
485             # It's a parameter entity (i.e. %ent; instead of &ent;)
486             $name = substr ($name, 1);
487             $par = "% ";
488             }
489              
490             my $str = $self->{DocTypeIndent} . "
491              
492             my $value = $event->{Value};
493             my $sysId = $event->{SysId};
494             my $pubId = $event->{PubId};
495             my $ndata = $event->{Ndata};
496              
497             my $q = $self->{Quote}->{SystemLiteral};
498             my $escape = $self->{Escape}->{SystemLiteral};
499              
500             if (defined $value)
501             {
502             #?? use {Entity} quote etc...
503             my $esc = $self->{Escape}->{Entity};
504             my $p = $self->{Quote}->{Entity};
505             $str .= " $p" . &$esc ($value) . $p;
506             }
507             if (defined $pubId)
508             {
509             $str .= " PUBLIC $q" . &$escape ($pubId) . $q;
510             }
511             elsif (defined $sysId)
512             {
513             $str .= " SYSTEM";
514             }
515              
516             if (defined $sysId)
517             {
518             $str .= " $q" . &$escape ($sysId) . $q;
519             }
520             $str .= " NDATA $ndata" if defined $ndata;
521             $str .= ">";
522              
523             $self->{DTD} .= $str;
524             }
525              
526             sub attlist_decl
527             {
528             my ($self, $event) = @_;
529             $self->flush_xml_decl;
530              
531             my $elem = $event->{ElementName};
532              
533             my $str = $event->{AttributeName} . " " . $event->{Type};
534             $str .= " #FIXED" if defined $event->{Fixed};
535              
536             $str = $str;
537              
538             my $def = $event->{Default};
539             if (defined $def)
540             {
541             $str .= " $def";
542            
543             # Note sometimes Default is a value with quotes.
544             # We'll use the existing quotes in that case...
545             }
546              
547             my $indent;
548             if (!exists($event->{First}) || $event->{First})
549             {
550             $self->{DTD} .= $self->{DocTypeIndent} . "
551              
552             if ($event->{MoreFollow})
553             {
554             $indent = $self->{Newline} . $self->{IndentAttlist};
555             }
556             else
557             {
558             $indent = " ";
559             }
560             }
561             else
562             {
563             $indent = $self->{Newline} . $self->{IndentAttlist};
564             }
565              
566             $self->{DTD} .= $indent . $str;
567              
568             unless ($event->{MoreFollow})
569             {
570             $self->{DTD} .= '>';
571             }
572             }
573              
574             sub xml_decl
575             {
576             my ($self, $event) = @_;
577             return if $self->{PrintedXmlDecl}; # already printed it
578              
579             my $version = $event->{Version};
580             my $encoding = $event->{Encoding};
581             if (defined $self->{Encoding})
582             {
583             $encoding = $self->{Encoding};
584             }
585             else
586             {
587             $self->setEncoding ($encoding) if defined $encoding;
588             }
589              
590             my $standalone = $event->{Standalone};
591             $standalone = ($standalone ? "yes" : "no") if defined $standalone;
592              
593             my $q = $self->{Quote}->{XMLDecl};
594             my $nl = $self->{Newline};
595              
596             my $str = "
597             $str .= " version=$q$version$q" if defined $version;
598             $str .= " encoding=$q$encoding$q" if defined $encoding;
599             $str .= " standalone=$q$standalone$q" if defined $standalone;
600             $str .= "?>$nl$nl";
601              
602             $self->print ($str);
603             $self->{PrintedXmlDecl} = 1;
604             }
605              
606             #
607             # Prints the declaration if it wasn't already printed
608             # *and* the user wanted it to be printed (because s/he set $self->{Encoding})
609             #
610             sub flush_xml_decl
611             {
612             my ($self) = @_;
613             return if $self->{PrintedXmlDecl};
614              
615             if (defined $self->{Encoding})
616             {
617             $self->xml_decl ({ Version => '1.0', Encoding => $self->{Encoding} });
618             }
619              
620             # If it wasn't printed just now, it doesn't need to be printed at all,
621             # so pretend we did print it.
622             $self->{PrintedXmlDecl} = 1;
623             }
624              
625             sub processing_instruction
626             {
627             my ($self, $event) = @_;
628             $self->flush_xml_decl;
629              
630             my $escape = $self->{Escape}->{ProcessingInstruction};
631              
632             my $str = "{Target} . " " .
633             &$escape ($event->{Data}). "?>";
634              
635             $self->print ($str);
636             }
637              
638             1; # package return code
639              
640             __END__