File Coverage

blib/lib/XML/Writer/Lazy.pm
Criterion Covered Total %
statement 27 144 18.7
branch 0 12 0.0
condition 0 8 0.0
subroutine 8 41 19.5
pod 3 3 100.0
total 38 208 18.2


line stmt bran cond sub pod time code
1             package XML::Writer::Lazy;
2             $XML::Writer::Lazy::VERSION = '0.02';
3             =head1 NAME
4              
5             XML::Writer::Lazy - Pass stringified XML to XML::Writer
6              
7             =head1 VERSION
8              
9             version 0.02
10              
11             =head1 DESCRIPTION
12              
13             Pass stringified XML to XML::Writer
14              
15             =head1 SYNOPSIS
16              
17             my $writer = XML::Writer::Lazy->new( OUTPUT => 'self');
18             my $title = "My Title!";
19              
20             $writer->lazily(<<"XML");
21            
22            
23             $title
24            
25            
26            

Pipe in literal XML

27             XML
28              
29             $writer->startTag( "p", "class" => "simple" );
30             $writer->characters("Alongside the usual interface");
31             $writer->characters("123456789");
32             $writer->lazily("

");
33              
34             =head1 WHY
35              
36             This is 2016. The computer should do the hard work. Life's too short to write
37             a bunch of C and C when my computer's perfectly capable of
38             figuring out the right thing to do if I give it a chunk of XML.
39              
40             =head1 HOW
41              
42             Using a SAX parser whose events are then passed back to XML::Writer.
43              
44             =head1 METHODS
45              
46             This is a subclass of L. Two methods are added:
47              
48             =head2 lazily
49              
50             Take a string of XML. It should be parseable, although doesn't need to be
51             balanced. C<< asdf >> is fine, where C<< > is not. Exercises
52             the XML::Writer methods appropriately to re-create whatever you'd passed in.
53              
54             =head2 wrap_output
55              
56             Only important if you're doing strange things with the C after
57             instantiation. In order to keep track of what's been written already, this
58             class wraps the C object inside a delegate that intercepts and stores
59             the contents of C. If you -- post instantiation -- replace the output
60             object, you can call this method to rewrap it. It will change the class that
61             that object belongs to.
62              
63             =cut
64              
65 1     1   12936 use strict;
  1         2  
  1         22  
66 1     1   4 use warnings;
  1         1  
  1         23  
67 1     1   2 use base 'XML::Writer';
  1         2  
  1         630  
68 1     1   11573 use XML::SAX;
  1         2807  
  1         273  
69              
70             #
71             # The SAX ChunkParser's input, and XML::Writer's output need to be kept in
72             # sync, because the ChunkParser still expects well-formed XML eventually.
73             #
74             # So that means there are two main modes here:
75             # - When `lazily` is being used, SAX ChunkParser is being used explicitly to
76             # drive XML::Writer, and this keeps it in sync
77             # - When XML::Writer's methods are being used directly, we intercept calls
78             # to `print`, and add that to a buffer that's fed to the ChunkParser each
79             # time before it's used again
80             #
81              
82             my $KEY = '_XML_Writer_Lazy_Parser';
83              
84             sub new {
85 1     1 1 14 my $classname = shift;
86 1         11 my $self = $classname->SUPER::new(@_);
87              
88             # Create the parser
89 1         162 my $parser;
90             {
91 1         2 local $XML::SAX::ParserPackage = 'XML::LibXML::SAX::ChunkParser';
  1         2  
92 1         14 $parser = XML::SAX::ParserFactory->parser(
93             Handler => XML::Writer::Lazy::Handler->new );
94             }
95              
96             # And the buffer...
97 0           my $buffer = '';
98              
99             # Save them both in the parent object
100 0           $self->{$KEY} = {
101             parser => $parser,
102             buffer => $buffer,
103             };
104              
105             # Capture anything print()'ed via XML::Writer
106 0           $self->wrap_output();
107 0           return $self;
108             }
109              
110             my $null_handler = bless {}, 'XML::Writer::Lazy::NullHandler';
111              
112             sub lazily {
113 0     0 1   my ( $self, $string, $writer ) = @_;
114              
115             # Set the writer object that the Handler is using
116 0   0       local $XML::Writer::Lazy::Handler::writer = $writer // $self;
117              
118             # Whether or not we might be trying to print an XML dec
119 0           local $XML::Writer::Lazy::Handler::xml_dec
120             = ( $string =~ m/^(?:\xEF\xBB\xBF)?<\?xml/i );
121              
122             # First thing we do is look at anything that was output directly by
123             # XML::Writer, and pass that to the Chunk Parser
124 0 0         if ( length $self->{$KEY}->{'buffer'} ) {
125              
126             # Save a copy of the buffer, and then nuke the buffer
127 0           my $directly = $self->{$KEY}->{'buffer'};
128 0           $self->{$KEY}->{'buffer'} = '';
129              
130             # Re-enter this sub with the buffer as the argument
131 0           $self->lazily( $directly, $null_handler );
132             }
133              
134             {
135             # Turn off buffer collection
136 0           local $XML::Writer::Lazy::InterceptPrint::intercept = 0;
  0            
137              
138             # Push in the user's string
139 0           $self->{$KEY}->{'parser'}->parse_chunk($string);
140              
141             # Flush using a comment
142 0           local $XML::Writer::Lazy::Handler::writer = $null_handler;
143 0           $self->{$KEY}->{'parser'}->parse_chunk("");
144             }
145             }
146              
147             sub wrap_output {
148 0     0 1   my $self = shift;
149 0           $self->setOutput(
150             XML::Writer::Lazy::InterceptPrint->___wrap(
151             $self->getOutput(), $self
152             )
153             );
154             }
155              
156             package XML::Writer::Lazy::InterceptPrint {
157             our $intercept = 1;
158              
159 1     1   5 use vars '$AUTOLOAD';
  1         1  
  1         35  
160 1     1   4 use Scalar::Util qw/weaken/;
  1         1  
  1         935  
161              
162             sub ___wrap {
163 0     0     my ( $classname, $delegate, $me ) = @_;
164 0           weaken $delegate;
165 0           weaken $me;
166 0           return bless [ $delegate, $me ], $classname;
167             }
168              
169             sub AUTOLOAD {
170 0     0     my ($sub) = $AUTOLOAD =~ /.*::(.*?)$/;
171 0 0         return if $sub eq "DESTROY";
172 0           my $self = shift;
173              
174             # The object we'll be executing against
175 0           my $wraps = $self->[0];
176              
177             # Get a reference to the original
178 0           my $ref = $wraps->can($sub);
179              
180             # Do something clever with print
181 0 0         if ( $sub eq 'print' ) {
182 0 0         if ($intercept) {
183 0           $self->[1]->{$KEY}->{'buffer'} .= join '',
184             @_;
185             }
186             }
187              
188             # Add the wrapped object to the front of @_
189 0           unshift( @_, $wraps );
190              
191             # Redispatch; goto replaces the current stack frame, so it's like
192             # we were never here...
193 0           goto &$ref;
194             }
195             }
196              
197             package XML::Writer::Lazy::NullHandler {
198              
199             # I'm used when we don't want to actually write anything out
200             #use vars '$AUTOLOAD';
201       0     sub AUTOLOAD { }
202              
203             #my ($sub) = $AUTOLOAD =~ /.*::(.*?)$/;
204             }
205              
206             package XML::Writer::Lazy::Handler {
207             our $writer;
208             our $xml_dec = 0;
209              
210 1     1   4 use base qw(XML::SAX::Base);
  1         1  
  1         797  
211              
212             # This gets run for the first chunk
213             sub xml_decl {
214 0     0     my ( $self, $element ) = @_;
215 0 0         return unless $xml_dec;
216 0   0       $writer->xmlDecl( $element->{'Encoding'} // () );
217             }
218              
219             sub start_element {
220 0     0     my ( $self, $element ) = @_;
221 0           my %attributes = %{ $element->{'Attributes'} };
  0            
222 0           my @attributes;
223 0           for my $attr ( keys %attributes ) {
224 0 0         if ( ref( $attributes{$attr} ) eq 'HASH' ) {
225 0           my $data = $attributes{$attr};
226 0           push( @attributes, [ $data->{'Name'}, $data->{'Value'} ] );
227             }
228             else {
229 0           push( @attributes, [ $attr, $attributes{$attr} ] );
230             }
231             }
232              
233 0           @attributes = map {@$_} sort { $a->[0] cmp $b->[0] } @attributes;
  0            
  0            
234 0           $writer->startTag( $element->{'Name'}, @attributes );
235             }
236              
237             sub end_element {
238 0     0     my ( $self, $element ) = @_;
239 0           $writer->endTag( $element->{'Name'} );
240             }
241              
242             sub characters {
243 0     0     my ( $self, $characters ) = @_;
244 0           $writer->characters( $characters->{'Data'} );
245             }
246              
247             sub processing_instruction {
248 0     0     my ( $self, $pi ) = @_;
249 0           $writer->pi( $pi->{'Target'}, $pi->{'Data'} );
250             }
251              
252             sub comment {
253 0     0     my ( $self, $comment ) = @_;
254 0   0       $comment->{'Data'} ||= '';
255 0           $comment->{'Data'} =~ s/^ //;
256 0           $comment->{'Data'} =~ s/ $//;
257 0           $writer->comment( $comment->{'Data'} );
258             }
259              
260             sub start_dtd {
261 0     0     my ( $self, $dtd ) = @_;
262             $writer->doctype( $dtd->{'Name'}, $dtd->{'PublicId'},
263 0           $dtd->{'SystemId'} );
264             }
265              
266             sub start_prefix_mapping {
267 0     0     my ( $self, $prefix_mapping ) = @_;
268             $writer->addPrefix( $prefix_mapping->{'NamespaceURI'},
269 0           $prefix_mapping->{'Prefix'} );
270             }
271              
272             # No work needed, as the insides will already be magically quoted
273       0     sub start_cdata { }
274       0     sub end_cdata { }
275              
276             sub set_document_locator {
277 0     0     my $self = shift;
278 0           my $data = shift;
279 0           die "'set_document_locator' event not yet implemented";
280             }
281              
282             sub skipped_entity {
283 0     0     my $self = shift;
284 0           my $data = shift;
285 0           die "'skipped_entity' event not yet implemented";
286             }
287              
288             sub entity_reference {
289 0     0     my $self = shift;
290 0           my $data = shift;
291 0           die "'entity_reference' event not yet implemented";
292             }
293              
294             sub notation_decl {
295 0     0     my $self = shift;
296 0           my $data = shift;
297 0           die "'notation_decl' event not yet implemented";
298             }
299              
300             sub unparsed_entity_decl {
301 0     0     my $self = shift;
302 0           my $data = shift;
303 0           die "'unparsed_entity_decl' event not yet implemented";
304             }
305              
306             sub element_decl {
307 0     0     my $self = shift;
308 0           my $data = shift;
309 0           die "'element_decl' event not yet implemented";
310             }
311              
312             sub attlist_decl {
313 0     0     my $self = shift;
314 0           my $data = shift;
315 0           die "'attlist_decl' event not yet implemented";
316             }
317              
318             sub doctype_decl {
319 0     0     my $self = shift;
320 0           my $data = shift;
321 0           die "'doctype_decl' event not yet implemented";
322             }
323              
324             sub entity_decl {
325 0     0     my $self = shift;
326 0           my $data = shift;
327 0           die "'entity_decl' event not yet implemented";
328             }
329              
330             sub attribute_decl {
331 0     0     my $self = shift;
332 0           my $data = shift;
333 0           die "'attribute_decl' event not yet implemented";
334             }
335              
336             sub internal_entity_decl {
337 0     0     my $self = shift;
338 0           my $data = shift;
339 0           die "'internal_entity_decl' event not yet implemented";
340             }
341              
342             sub external_entity_decl {
343 0     0     my $self = shift;
344 0           my $data = shift;
345 0           die "'external_entity_decl' event not yet implemented";
346             }
347              
348             sub resolve_entity {
349 0     0     my $self = shift;
350 0           my $data = shift;
351 0           die "'resolve_entity' event not yet implemented";
352             }
353              
354             sub start_entity {
355 0     0     my $self = shift;
356 0           my $data = shift;
357 0           die "'start_entity' event not yet implemented";
358             }
359              
360             sub end_entity {
361 0     0     my $self = shift;
362 0           my $data = shift;
363 0           die "'end_entity' event not yet implemented";
364             }
365              
366             sub warning {
367 0     0     my $self = shift;
368 0           my $data = shift;
369 0           die "'warning' event not yet implemented";
370             }
371              
372             sub error {
373 0     0     my $self = shift;
374 0           my $data = shift;
375 0           die "'error' event not yet implemented";
376             }
377              
378             sub fatal_error {
379 0     0     my $self = shift;
380 0           my $data = shift;
381 0           die "'fatal_error' event not yet implemented";
382             }
383              
384             }
385              
386             1;
387             $XML::Writer::Lazy::InterceptPrint::VERSION = '0.02';