File Coverage

blib/lib/XML/Writer/Lazy.pm
Criterion Covered Total %
statement 30 161 18.6
branch 0 16 0.0
condition 0 2 0.0
subroutine 9 43 20.9
pod 3 3 100.0
total 42 225 18.6


line stmt bran cond sub pod time code
1             package XML::Writer::Lazy;
2             $XML::Writer::Lazy::VERSION = '0.04';
3             =head1 NAME
4              
5             XML::Writer::Lazy - Pass stringified XML to XML::Writer
6              
7             =head1 VERSION
8              
9             version 0.04
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             B L.
47              
48             =cut
49              
50 1     1   12505 use strict;
  1         2  
  1         20  
51 1     1   3 use warnings;
  1         1  
  1         19  
52 1     1   3 use base 'XML::Writer';
  1         1  
  1         558  
53 1     1   11276 use XML::SAX;
  1         2841  
  1         321  
54              
55             #
56             # The SAX ChunkParser's input, and XML::Writer's output need to be kept in
57             # sync, because the ChunkParser still expects well-formed XML eventually.
58             #
59             # So that means there are two main modes here:
60             # - When `lazily` is being used, SAX ChunkParser is being used explicitly to
61             # drive XML::Writer, and this keeps it in sync
62             # - When XML::Writer's methods are being used directly, we intercept calls
63             # to `print`, and add that to a buffer that's fed to the ChunkParser each
64             # time before it's used again
65             #
66              
67             my $KEY = '_XML_Writer_Lazy_Parser';
68              
69             =head2 new
70              
71             Call's L's C and then instantiates the lazy
72             parser pieces. Accepts all the same arguments as the parent method.
73              
74             =cut
75              
76             sub new {
77 1     1 1 8 my $classname = shift;
78 1         9 my $self = $classname->SUPER::new(@_);
79              
80             # Create the parser
81 1         152 my $parser;
82             {
83 1         2 local $XML::SAX::ParserPackage = 'XML::LibXML::SAX::ChunkParser';
  1         1  
84 1         10 $parser = XML::SAX::ParserFactory->parser(
85             Handler => XML::Writer::Lazy::Handler->new );
86             }
87              
88             # And the buffer...
89 0           my $buffer = '';
90              
91             # Save them both in the parent object
92 0           $self->{$KEY} = {
93             parser => $parser,
94             buffer => $buffer,
95             };
96              
97             # Capture anything print()'ed via XML::Writer
98 0           $self->wrap_output();
99 0           return $self;
100             }
101              
102             my $null_handler = bless {}, 'XML::Writer::Lazy::NullHandler';
103              
104             =head2 lazily
105              
106             Take a string of XML. It should be parseable, although doesn't need to be
107             balanced. C<< asdf >> is fine, where C<< > is not. Exercises
108             the XML::Writer methods appropriately to re-create whatever you'd passed in.
109              
110             =cut
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
117             = ( defined $writer ) ? $writer : $self;
118              
119             # Whether or not we might be trying to print an XML dec
120 0           local $XML::Writer::Lazy::Handler::xml_dec
121             = ( $string =~ m/^(\xEF\xBB\xBF)?<\?xml/i );
122              
123             # First thing we do is look at anything that was output directly by
124             # XML::Writer, and pass that to the Chunk Parser
125 0 0         if ( length $self->{$KEY}->{'buffer'} ) {
126              
127             # Save a copy of the buffer, and then nuke the buffer
128 0           my $directly = $self->{$KEY}->{'buffer'};
129 0           $self->{$KEY}->{'buffer'} = '';
130              
131             # Re-enter this sub with the buffer as the argument
132 0           $self->lazily( $directly, $null_handler );
133             }
134              
135             {
136             # Turn off buffer collection
137 0           local $XML::Writer::Lazy::InterceptPrint::intercept = 0;
  0            
138              
139             # Push in the user's string
140 0           $self->{$KEY}->{'parser'}->parse_chunk($string);
141              
142             # Flush using a comment
143 0           local $XML::Writer::Lazy::Handler::writer = $null_handler;
144 0           $self->{$KEY}->{'parser'}->parse_chunk("");
145             }
146              
147 0           return $self;
148             }
149              
150             =head2 wrap_output
151              
152             Only important if you're doing strange things with the C after
153             instantiation. In order to keep track of what's been written already, this
154             class wraps the C object inside a delegate that intercepts and stores
155             the contents of C. If you -- post instantiation -- replace the output
156             object, you can call this method to rewrap it. It will change the class that
157             that object belongs to.
158              
159             =cut
160              
161             sub wrap_output {
162 0     0 1   my $self = shift;
163 0           $self->setOutput(
164             XML::Writer::Lazy::InterceptPrint->___wrap(
165             $self->getOutput(), $self
166             )
167             );
168              
169 0           return $self;
170             }
171              
172             =head1 AUTHOR
173              
174             Peter Sergeant - C
175              
176             L
177              
178             =head1 LICENSE
179              
180             MIT - see the C file included in the tar.gz of this distribution.
181              
182             =cut
183              
184             # I capture output that's produced by calling XML::Writer's methods directly
185             package XML::Writer::Lazy::InterceptPrint;
186             $XML::Writer::Lazy::InterceptPrint::VERSION = '0.04';
187             our $intercept = 1;
188              
189 1     1   4 use vars '$AUTOLOAD';
  1         1  
  1         30  
190 1     1   4 use Scalar::Util qw/weaken/;
  1         1  
  1         826  
191              
192             sub ___wrap {
193 0     0     my ( $classname, $delegate, $me ) = @_;
194 0           weaken $delegate;
195 0           weaken $me;
196 0           return bless [ $delegate, $me ], $classname;
197             }
198              
199             sub AUTOLOAD {
200 0     0     my $self = shift;
201              
202 0           my ($sub) = $AUTOLOAD =~ /.*::(.*?)$/;
203 0 0         return if $sub eq "DESTROY";
204              
205             # The object we'll be executing against
206 0           my $wraps = $self->[0];
207              
208             # Get a reference to the original
209 0           my $ref = $wraps->can($sub);
210              
211             # Do something clever with print
212 0 0         if ( $sub eq 'print' ) {
213 0 0         if ($intercept) {
214 0           $self->[1]->{$KEY}->{'buffer'} .= join '', @_;
215             }
216             }
217              
218             # Add the wrapped object to the front of @_
219 0           unshift( @_, $wraps );
220              
221             # Redispatch; goto replaces the current stack frame, so it's like
222             # we were never here...
223 0           goto &$ref;
224             }
225              
226             # I'm the handling class the SAX parser calls when events are generated
227             package XML::Writer::Lazy::Handler;
228             $XML::Writer::Lazy::Handler::VERSION = '0.04';
229             our $writer;
230             our $xml_dec = 0;
231              
232 1     1   5 use base qw(XML::SAX::Base);
  1         0  
  1         749  
233 1     1   11532 use Carp qw/croak/;
  1         2  
  1         917  
234              
235             # This gets run for the first chunk
236             sub xml_decl {
237 0     0     my ( $self, $element ) = @_;
238 0 0         return unless $xml_dec;
239             $writer->xmlDecl(
240             ( defined $element->{'Encoding'} )
241 0 0         ? ( $element->{'Encoding'} )
242             : ()
243             );
244 0           return;
245             }
246              
247             sub start_element {
248 0     0     my ( $self, $element ) = @_;
249 0           my %attributes = %{ $element->{'Attributes'} };
  0            
250 0           my @attributes;
251 0           for my $attr ( keys %attributes ) {
252 0 0         if ( ref( $attributes{$attr} ) eq 'HASH' ) {
253 0           my $data = $attributes{$attr};
254 0           push( @attributes, [ $data->{'Name'}, $data->{'Value'} ] );
255             }
256             else {
257 0           push( @attributes, [ $attr, $attributes{$attr} ] );
258             }
259             }
260              
261 0           @attributes = map {@$_} sort { $a->[0] cmp $b->[0] } @attributes;
  0            
  0            
262 0           $writer->startTag( $element->{'Name'}, @attributes );
263 0           return;
264             }
265              
266             sub end_element {
267 0     0     my ( $self, $element ) = @_;
268 0           $writer->endTag( $element->{'Name'} );
269 0           return;
270             }
271              
272             sub characters {
273 0     0     my ( $self, $characters ) = @_;
274 0           $writer->characters( $characters->{'Data'} );
275 0           return;
276             }
277              
278             sub processing_instruction {
279 0     0     my ( $self, $pi ) = @_;
280 0           $writer->pi( $pi->{'Target'}, $pi->{'Data'} );
281 0           return;
282             }
283              
284             sub comment {
285 0     0     my ( $self, $comment ) = @_;
286 0   0       $comment->{'Data'} ||= '';
287 0           $comment->{'Data'} =~ s/^ //;
288 0           $comment->{'Data'} =~ s/ $//;
289 0           $writer->comment( $comment->{'Data'} );
290 0           return;
291             }
292              
293             sub start_dtd {
294 0     0     my ( $self, $dtd ) = @_;
295             $writer->doctype( $dtd->{'Name'}, $dtd->{'PublicId'},
296 0           $dtd->{'SystemId'} );
297 0           return;
298             }
299              
300             sub start_prefix_mapping {
301 0     0     my ( $self, $prefix_mapping ) = @_;
302             $writer->addPrefix( $prefix_mapping->{'NamespaceURI'},
303 0           $prefix_mapping->{'Prefix'} );
304 0           return;
305             }
306              
307             # No work needed, as the insides will already be magically quoted
308 0     0     sub start_cdata { return; }
309 0     0     sub end_cdata { return; }
310              
311             sub not_implemented {
312 0     0     my ($event) = @_;
313 0           return <<"MSG";
314             The XML passed to 'lazily()' triggered a `$event` SAX event,
315             but XML::Writer::Lazy doesn't yet handle those. You can either simplify your
316             input XML or alternatively you could monkey-patch `XML::Writer::Lazy::Handler`
317             to add a `$event()` handler method to it.
318             MSG
319             }
320              
321             sub set_document_locator {
322 0     0     my $self = shift;
323 0           my $data = shift;
324 0           return croak( not_implemented('set_document_locator') );
325             }
326              
327             sub skipped_entity {
328 0     0     my $self = shift;
329 0           my $data = shift;
330 0           return croak( not_implemented('skipped_entity') );
331             }
332              
333             sub entity_reference {
334 0     0     my $self = shift;
335 0           my $data = shift;
336 0           return croak( not_implemented('entity_reference') );
337             }
338              
339             sub notation_decl {
340 0     0     my $self = shift;
341 0           my $data = shift;
342 0           return croak( not_implemented('notation_decl') );
343             }
344              
345             sub unparsed_entity_decl {
346 0     0     my $self = shift;
347 0           my $data = shift;
348 0           return croak( not_implemented('unparsed_entity_decl') );
349             }
350              
351             sub element_decl {
352 0     0     my $self = shift;
353 0           my $data = shift;
354 0           return croak( not_implemented('element_decl') );
355             }
356              
357             sub attlist_decl {
358 0     0     my $self = shift;
359 0           my $data = shift;
360 0           return croak( not_implemented('attlist_decl') );
361             }
362              
363             sub doctype_decl {
364 0     0     my $self = shift;
365 0           my $data = shift;
366 0           return croak( not_implemented('doctype_decl') );
367             }
368              
369             sub entity_decl {
370 0     0     my $self = shift;
371 0           my $data = shift;
372 0           return croak( not_implemented('entity_decl') );
373             }
374              
375             sub attribute_decl {
376 0     0     my $self = shift;
377 0           my $data = shift;
378 0           return croak( not_implemented('attribute_decl') );
379             }
380              
381             sub internal_entity_decl {
382 0     0     my $self = shift;
383 0           my $data = shift;
384 0           return croak( not_implemented('internal_entity_decl') );
385             }
386              
387             sub external_entity_decl {
388 0     0     my $self = shift;
389 0           my $data = shift;
390 0           return croak( not_implemented('external_entity_decl') );
391             }
392              
393             sub resolve_entity {
394 0     0     my $self = shift;
395 0           my $data = shift;
396 0           return croak( not_implemented('resolve_entity') );
397             }
398              
399             sub start_entity {
400 0     0     my $self = shift;
401 0           my $data = shift;
402 0           return croak( not_implemented('start_entity') );
403             }
404              
405             sub end_entity {
406 0     0     my $self = shift;
407 0           my $data = shift;
408 0           return croak( not_implemented('end_entity') );
409             }
410              
411             sub warning {
412 0     0     my $self = shift;
413 0           my $data = shift;
414 0           return croak( not_implemented('warning') );
415             }
416              
417             sub error {
418 0     0     my $self = shift;
419 0           my $data = shift;
420 0           return croak( not_implemented('error') );
421             }
422              
423             sub fatal_error {
424 0     0     my $self = shift;
425 0           my $data = shift;
426 0           return croak( not_implemented('fatal_error') );
427             }
428              
429             # I'm used when we feed captured output from XML::Writer to the SAX parser,
430             # and don't actually want to redispatch that to XML::Writer. I accept and
431             # silently drop all method calls.
432             package XML::Writer::Lazy::NullHandler;
433             $XML::Writer::Lazy::NullHandler::VERSION = '0.04';
434       0     sub AUTOLOAD { }
435              
436             1;