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