File Coverage

blib/lib/XML/LibXML/PrettyPrint.pm
Criterion Covered Total %
statement 27 29 93.1
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 37 39 94.8


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