File Coverage

blib/lib/XML/LibXML/PrettyPrint.pm
Criterion Covered Total %
statement 33 35 94.2
branch n/a
condition n/a
subroutine 12 12 100.0
pod n/a
total 45 47 95.7


line stmt bran cond sub pod time code
1 2     2   30610 use 5.008001;
  2         6  
  2         68  
2 2     2   10 use strict;
  2         3  
  2         55  
3 2     2   17 use warnings;
  2         4  
  2         58  
4 2     2   1809 use utf8;
  2         19  
  2         9  
5              
6 2     2   73 no warnings qw( once void uninitialized );
  2         4  
  2         81  
7              
8 2     2   1868 use IO::Handle 0 qw();
  2         14268  
  2         78  
9              
10             package XML::LibXML::PrettyPrint;
11              
12 2     2   13 use constant { FALSE => 0, TRUE => 1 };
  2         4  
  2         155  
13 2     2   10 use constant { EL_BLOCK => 1, EL_COMPACT => 2, EL_INLINE => 3};
  2         3  
  2         141  
14              
15             BEGIN
16             {
17 2     2   4 $XML::LibXML::PrettyPrint::AUTHORITY = 'cpan:TOBYINK';
18 2         53 $XML::LibXML::PrettyPrint::VERSION = '0.006';
19             }
20              
21 2     2   9 use Carp 0 qw(croak carp);
  2         38  
  2         98  
22 2     2   12 use Scalar::Util 0 qw(blessed refaddr);
  2         39  
  2         189  
23 2     2   2007 use XML::LibXML 1.62 qw(:ns);
  0            
  0            
24              
25             use Exporter::Tiny ();
26              
27             our @ISA = 'Exporter::Tiny';
28             our @EXPORT = qw();
29             our @EXPORT_OK = qw(print_xml EL_BLOCK EL_COMPACT EL_INLINE);
30             our %EXPORT_TAGS = (
31             constants => [qw(EL_BLOCK EL_COMPACT EL_INLINE)],
32             io => sub {
33             *IO::Handle::print_xml = sub ($$;$) {
34             my ($handle, $xml, $indent) = @_;
35             unless (blessed($xml)) {
36             local $@ = undef;
37             eval { $xml = XML::LibXML->new->parse_string($xml); 1; }
38             or croak("Could not parse XML: $@");
39             }
40             $indent = 0 unless defined $indent;
41             $handle->print(__PACKAGE__->pretty_print($xml, $indent)->toString);
42             };
43             return;
44             },
45             );
46              
47             our $Whitespace = qr/[\x20\t\r\n]/; # @@TODO need to check XML spec
48              
49             sub new
50             {
51             my ($class, %options) = @_;
52             $options{element} = delete $options{elements} unless defined $options{element};
53             if (defined $options{indent_string})
54             {
55             carp("Non-whitespace indent_string supplied")
56             unless $options{indent_string} =~ /^$Whitespace*$/
57             }
58             bless \%options, $class;
59             }
60              
61             {
62             my @compact = qw[area audio base basefont bgsound br button canvas
63             caption col command dd details dt embed figcaption
64             frame h1 h2 h3 h4 h5 h6 hr iframe img input isindex
65             keygen legend li link meta option p param summary td
66             th title video];
67             my @inline = qw[a abbr b bdi bdo big cite code dfn em font i kbd label
68             mark meter nobr progress q rp rt ruby s samp small span
69             strike strong sub sup time tt u var wbr];
70             my @block = qw[address applet article aside blockquote body center
71             colgroup datalist del dir div fieldset figure footer
72             form frameset head header hgroup html ins listing map
73             marquee menu nav noembed noframes noscript object ol
74             optgroup select section source table tbody tfoot thead
75             tr track ul dl];
76             my @pre = qw[plaintext output pre script style textarea xmp];
77            
78             my $rdfa_lit_content = sub
79             {
80             my ($el) = @_;
81             return TRUE
82             if ($el->hasAttribute('property') and not $el->hasAttribute('content'));
83             return undef;
84             };
85            
86             sub new_for_html
87             {
88             my ($class, %options) = @_;
89            
90             return $class->new(
91             %options,
92             element => {
93             block => [@block],
94             compact => [@compact],
95             inline => [@inline],
96             preserves_whitespace => [@pre, $rdfa_lit_content],
97             },
98             );
99             }
100             }
101              
102             sub _ensure_self
103             {
104             blessed($_[0]) ? $_[0] : $_[0]->new;
105             }
106              
107             sub strip_whitespace
108             {
109             my ($self, $node) = @_;
110             $self = $self->_ensure_self;
111            
112             croak("First parameter must be an XML::LibXML::Node")
113             unless blessed($node) && $node->isa('XML::LibXML::Node');
114            
115             if ($node->nodeName eq '#document')
116             {
117             return $self->strip_whitespace($node->documentElement);
118             }
119             elsif ($node->isa('XML::LibXML::Element'))
120             {
121             if ($self->element_preserves_whitespace($node))
122             {
123             return 0;
124             }
125            
126             my $node_category = $self->element_category($node);
127            
128             $node->normalize;
129             my @kids = $node->childNodes;
130             my $activity = 0;
131            
132             for (my $i = 0; exists $kids[$i]; $i++)
133             {
134             my $kid = $kids[$i];
135            
136             if ($kid->nodeName eq '#text')
137             {
138             my $prev = exists $kids[$i-1] ? $kids[$i-1] : undef;
139             my $next = exists $kids[$i+1] ? $kids[$i+1] : undef;
140             my $data = $kid->data;
141            
142             if ((defined $prev and $self->element_category($prev)==EL_INLINE)
143             or ($node_category==EL_INLINE and not defined $prev))
144             { $data =~ s/^$Whitespace+/ /; }
145             else
146             { $data =~ s/^$Whitespace+//; }
147              
148             if ((defined $next and $self->element_category($next)==EL_INLINE)
149             or ($node_category==EL_INLINE and not defined $next))
150             { $data =~ s/$Whitespace+$/ /; }
151             else
152             { $data =~ s/$Whitespace+$//; }
153            
154             $data =~ s/$Whitespace+/ /g;
155              
156             $activity++ if length $data ne length $kid->data;
157             $node->removeChild($kid) unless length $data;
158             $kid->setData($data);
159             }
160             else
161             {
162             $activity += $self->strip_whitespace($kid);
163             }
164             }
165            
166             return $activity;
167             }
168             else
169             {
170             carp(sprintf("Don't know how to handle %s object", ref $node))
171             unless $node->nodeName eq '#comment'
172             || $node->isa('XML::LibXML::CDATASection')
173             || $node->isa('XML::LibXML::PI');
174             return 0;
175             }
176             }
177              
178             sub indent
179             {
180             my ($self, $node, $indent_level) = @_;
181             $self = $self->_ensure_self;
182            
183             $indent_level = 0 unless defined $indent_level;
184              
185             $self->indent($node->documentElement, $indent_level)
186             if blessed($node) && $node->nodeName eq '#document';
187              
188             return unless blessed($node) && $node->isa('XML::LibXML::Element');
189              
190             return if $self->element_preserves_whitespace($node);
191              
192             my $node_category = $self->element_category($node);
193              
194             # EL_COMPACT nodes get treated as inline unless they contain a
195             # block descendent.
196             if ($node_category==EL_COMPACT)
197             {
198             $node_category = EL_INLINE;
199             my $descs = $node->getElementsByTagName('*');
200             DESC: while (my $desc = $descs->shift)
201             {
202             if ($self->element_category($desc) == EL_BLOCK)
203             {
204             $node_category = EL_BLOCK;
205             last DESC;
206             }
207             }
208             }
209            
210             if ($node_category==EL_BLOCK)
211             {
212             my $newline = $self->new_line;
213             my $indent_string = $self->indent_string($indent_level + 1);
214            
215             my @kids = $node->childNodes;
216             $node->removeChildNodes;
217             for (my $i = 0; exists $kids[$i]; $i++)
218             {
219             my $kid = $kids[$i];
220             my $did_indent = FALSE;
221            
222             if ($i==0)
223             {
224             $node->appendText($newline . $indent_string);
225             $did_indent = TRUE;
226             }
227             elsif ($self->element_category($kid)==EL_BLOCK)
228             {
229             $node->appendText($newline . $indent_string);
230             $did_indent = TRUE;
231             }
232             elsif ($self->element_category($kid)==EL_COMPACT)
233             {
234             $node->appendText($newline . $indent_string);
235             $did_indent = TRUE;
236             }
237             elsif (defined $kids[$i-1])
238             {
239             my $prev_category = $self->element_category($kids[$i-1]);
240             if (defined $prev_category
241             and ($prev_category==EL_BLOCK or $prev_category==EL_COMPACT))
242             {
243             $node->appendText($newline . $indent_string);
244             $did_indent = TRUE;
245             }
246             }
247            
248             if ($did_indent and $kid->nodeName eq '#text')
249             {
250             (my $data = $kid->data) =~ s/^ //;
251             $kid->setData($data);
252             }
253             $node->appendChild($kid);
254             $self->indent($kid, $indent_level + 1);
255             }
256             $node->appendText($newline . $self->indent_string($indent_level)) if @kids;
257             }
258             }
259              
260             sub pretty_print
261             {
262             my ($self, $node, $indent_level) = @_;
263             $self = $self->_ensure_self;
264            
265             $self->strip_whitespace($node);
266             $self->indent($node, $indent_level);
267             return $node;
268             }
269              
270             sub _run_checks
271             {
272             my ($self, $category, $node) = @_;
273              
274             return FALSE unless defined $self->{element}{$category};
275            
276             if (ref $self->{element}{$category} eq 'CODE'
277             or !ref $self->{element}{$category})
278             {
279             $self->{element}{$category} = [$self->{element}{$category}];
280             }
281            
282             if (ref $self->{element}{$category} eq 'ARRAY')
283             {
284             foreach my $check (@{$self->{element}{$category}})
285             {
286             if (!ref $check and $check =~ /^\{(.+)\}(.+)$/)
287             {
288             return TRUE if $node->namespaceURI eq $1 && $node->localname eq $2;
289             }
290             elsif (!ref $check)
291             {
292             return TRUE if $check eq $node->nodeName;
293             }
294             elsif (ref $check eq 'CODE')
295             {
296             my $return = $check->($node);
297             return $return if defined $return;
298             }
299             else
300             {
301             carp(sprintf("Check for category '%s' ignored; is of type %s", $category, ref $check));
302             }
303             }
304             }
305            
306             return FALSE;
307             }
308              
309             sub indent_string
310             {
311             my ($self, $level) = @_;
312             $self = $self->_ensure_self;
313            
314             $self->{indent_string} = "\t"
315             unless defined $self->{indent_string};
316            
317             $self->{indent_string} x $level;
318             }
319              
320             sub new_line
321             {
322             my ($self, $level) = @_;
323             $self = $self->_ensure_self;
324            
325             $self->{new_line} = "\n"
326             unless defined $self->{new_line};
327            
328             $self->{new_line};
329             }
330              
331             sub element_category
332             {
333             my ($self, $node) = @_;
334             $self = $self->_ensure_self;
335              
336             return undef unless blessed($node);
337            
338             return EL_BLOCK if $self->_run_checks(block => $node);
339             return EL_COMPACT if $self->_run_checks(compact => $node);
340             return EL_INLINE if $self->_run_checks(inline => $node);
341              
342             return EL_BLOCK if $node->isa('XML::LibXML::Element');
343             return EL_COMPACT if $node->nodeName eq '#comment';
344             return EL_COMPACT if $node->isa('XML::LibXML::PI');
345            
346             return undef;
347             }
348              
349             sub element_preserves_whitespace
350             {
351             my ($self, $node) = @_;
352             $self = $self->_ensure_self;
353              
354             return undef unless blessed($node);
355             return TRUE if $node->nodeName eq '#comment';
356             return TRUE if $node->isa('XML::LibXML::PI');
357            
358             return TRUE if $self->_run_checks(preserves_whitespace => $node);
359            
360             return TRUE
361             if $node->isa('XML::LibXML::Element')
362             && $node->hasAttributeNS(XML_XML_NS, 'space')
363             && lc $node->getAttributeNS(XML_XML_NS, 'space') eq 'preserve';
364            
365             return FALSE if $node->isa('XML::LibXML::Element');
366             return undef;
367             }
368              
369             sub print_xml ($;$)
370             {
371             my ($xml, $indent) = @_;
372             unless (blessed($xml))
373             {
374             local $@ = undef;
375             eval { $xml = XML::LibXML->new->parse_string($xml); 1; }
376             or croak("Could not parse XML: $@");
377             }
378             $indent = 0 unless defined $indent;
379             print __PACKAGE__->pretty_print($xml, $indent)->toString;
380             }
381              
382             TRUE;
383              
384             __END__