File Coverage

blib/lib/XML/Filter/DataIndenter.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package XML::Filter::DataIndenter;
2              
3             $VERSION = 0.1;
4              
5             =head1 NAME
6              
7             XML::Filter::DataIndenter - SAX2 Indenter for data oriented XML
8              
9             =head1 SYNOPSIS
10              
11             use XML::Filter::DataIndenter;
12              
13             use XML::SAX::Machines qw( Pipeline );
14              
15             Pipeline( XML::Filter::DataIndenter => \*STDOUT );
16              
17             =head1 DESCRIPTION
18              
19             B: This is the first release. Feedback and patches
20             welcome.
21              
22             In data oriented XML, leaf elements (those which contain no elements)
23             contain only character content, all other elements contain only child
24             elements and ignorable whitespace. This filter consumes all whitespace
25             not in leaf nodes and replaces it with whitespace that indents all
26             elements. Character data in leaf elements is left unmolested.
27              
28             This filter assumes you're emitting data oriented XML. It will die if
29             it sees non-whitespace character data outside of a leaf element. It
30             also dies if it sees start-tag / end-tag mismatch, just as a service to
31             the programmer.
32              
33             Processing instructions and comments are indented as though they were
34             leaf elements except when they occur in leaf elements.
35              
36             =head2 Example:
37              
38             This document:
39              
40            
41             B
42            
43            
44              
45             gets reindented as:
46              
47            
48            
49            
50             B
51            
52            
53              
54             (plus or minus a space in each PI, depending on your XML writer).
55              
56             =cut
57              
58 1     1   7123 use XML::SAX::Base;
  1         24869  
  1         47  
59             @ISA = qw( XML::SAX::Base );
60              
61 1     1   14 use strict;
  1         2  
  1         48  
62 1     1   1488 use XML::SAX::EventMethodMaker qw( compile_missing_methods sax_event_names);
  0            
  0            
63              
64             sub start_document {
65             my $self = shift;
66             $self->{Depth} = 0;
67             $self->{Queue} = [];
68             $self->{HasKids} = 0;
69             $self->{HasData} = 0; ## Data = Non-WS text, that is
70             $self->{Indent} = " " unless defined $self->{Indent};
71             $self->{Stack} = [];
72             $self->SUPER::start_document( @_ );
73             }
74              
75              
76             sub _flush_content {
77             ## Called only when a child element has been detected
78             my $self = shift;
79              
80             my $ctx = $self->{Stack}->[-1];
81              
82             my $content = delete $ctx->{Content};
83             return unless defined $content;
84              
85             while ( @$content ) {
86             my $event = shift @$content;
87              
88             my $method = shift @$event;
89              
90             next if $method eq "characters"
91             || $method eq "start_cdata"
92             || $method eq "end_cdata";
93              
94             if ( $method eq "comment" || $method eq "processing_instruction") {
95             my $indent = $self->{Indent} x @{$self->{Stack}};
96             $self->SUPER::characters( { Data => "\n$indent" } );
97             }
98              
99             $method = "SUPER::$method";
100             $self->$method( @$event );
101             }
102             }
103              
104              
105             sub _flush_leaf_content {
106             ## Called only when no child elements have been detected
107             my $self = shift;
108             my $ctx = $self->{Stack}->[-1];
109              
110             my $content = delete $ctx->{Content};
111             return unless defined $content;
112              
113             while ( @$content ) {
114             my $event = shift @$content;
115             my $method = "SUPER::" . shift @$event;
116             $self->$method( @$event );
117             }
118             }
119              
120              
121             sub start_element {
122             my $self = shift;
123              
124             if ( @{$self->{Stack}} ) {
125             my $ctx = $self->{Stack}->[-1];
126             die "$ctx->{Name} has both child elements and non-whitespace\n"
127             if $ctx->{HasData};
128              
129             $self->_flush_content;
130              
131             my $indent = $self->{Indent} x @{$self->{Stack}};
132             $self->SUPER::characters( { Data => "\n$indent" } );
133              
134             $ctx->{HasKids}++;
135             }
136              
137             push @{$self->{Stack}}, { Name => $_[0]->{Name} };
138             $self->SUPER::start_element( @_ );
139             }
140              
141              
142             sub characters {
143             my $self = shift;
144              
145            
146             if ( @{$self->{Stack}} ) {
147             my $ctx = $self->{Stack}->[-1];
148              
149             $ctx->{HasData} ||= $_[0]->{Data} =~ /[^ \t\n]/;
150              
151             die "$ctx->{Name} has both child elements and non-whitespace\n"
152             if $ctx->{HasData} && $ctx->{HasKids};
153              
154             unless ( $ctx->{HasData} ) {
155             push @{$ctx->{Content}}, [ characters => @_ ];
156             return;
157             }
158              
159             return if $ctx->{HasKids};
160             }
161             $self->_flush_leaf_content;
162             $self->SUPER::characters( @_ );
163             }
164              
165              
166             compile_missing_methods __PACKAGE__, <<'EVENT_END', sax_event_names;
167             #line 1 XML::Filter::DataIndenter::
168             sub {
169             my $self = shift;
170             if ( $self->{Stack} && @{$self->{Stack}} ) {
171             my $ctx = $self->{Stack}->[-1];
172             unless ( $ctx->{HasData} ) {
173             push @{$ctx->{Content}}, [ "", @_ ];
174             return;
175             }
176             }
177             ## We get here if the context has data or there's no stack.
178             $self->SUPER::( @_ );
179             }
180             EVENT_END
181              
182              
183             sub end_element {
184             my $self = shift;
185              
186             my $ctx = $self->{Stack}->[-1];
187              
188             die "Expected {Name}>, got [0]->{Name}>\n"
189             unless $ctx->{Name} eq $_[0]->{Name};
190              
191             if ( $ctx->{HasKids} ) {
192             $self->_flush_content;
193             my $indent = $self->{Indent} x ( @{$self->{Stack}} - 1 );
194             $self->SUPER::characters( { Data => "\n$indent" } );
195             }
196             else {
197             $self->_flush_leaf_content;
198             }
199              
200             pop @{$self->{Stack}};
201              
202             $self->SUPER::end_element( @_ );
203             }
204              
205             sub end_document {
206             my $self = shift;
207             my $ctx = $self->{Stack}->[-1];
208              
209             die "Missing end_element events for ",
210             map( "<$_->{Name}>", @{$self->{Stack}} ),
211             "\n"
212             if $self->{Stack} && @{$self->{Stack}};
213              
214             $self->SUPER::end_document( @_ );
215             }
216              
217              
218             =head1 LIMITATIONS
219              
220             Considers only [\r\n \t] to be whitespace; does not think about
221             the broader Unicode definition of whitespace. This will be addressed
222             when time and need permit.
223              
224             =head1 COPYRIGHT
225              
226             Copyright 2003, R. Barrie Slaymaker, Jr., All Rights Reserved
227              
228             =head1 LICENSE
229              
230             You may use this module under the terms of the BSD, Artistic, oir GPL licenses,
231             any version.
232              
233             =head1 AUTHOR
234              
235             Barrie Slaymaker
236              
237             =cut
238              
239             1;