File Coverage

blib/lib/XML/TreeBuilder.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             require 5;
2              
3             package XML::TreeBuilder;
4              
5 2     2   36750 use warnings;
  2         5  
  2         76  
6 2     2   14 use strict;
  2         3  
  2         87  
7 2     2   1407 use XML::Element ();
  2         5  
  2         51  
8 2     2   1366 use XML::Parser ();
  0            
  0            
9             use Carp;
10             use IO::File;
11             use XML::Catalog 1.02;
12             use File::Basename;
13             use File::Spec;
14             use vars qw(@ISA $VERSION);
15              
16             $VERSION = '5.4';
17             @ISA = ('XML::Element');
18              
19             #==========================================================================
20             sub new {
21             my ( $this, $arg ) = @_;
22             my $class = ref($this) || $this;
23              
24             if ( $arg && ( ref($arg) ne 'HASH' ) ) {
25             croak(
26             q|new expects an anonymous hash, $t->new( { NoExpand => 1, ErrorContext => 2 } ), for it's parameters, not a |
27             . ref($arg) );
28             }
29              
30             my $NoExpand = ( delete $arg->{NoExpand} || undef );
31             my $ErrorContext = ( delete $arg->{ErrorContext} || undef );
32             my $catalog
33             = ( delete $arg->{catalog}
34             || $ENV{XML_CATALOG_FILES}
35             || '/etc/xml/catalog' );
36             my $debug = ( delete $arg->{debug} || undef );
37              
38             if ( %{$arg} ) {
39             croak "unknown args: " . join( ", ", keys %{$arg} );
40             }
41              
42             my $self = XML::Element->new('NIL');
43             bless $self, $class; # and rebless
44             $self->{_element_class} = 'XML::Element';
45             $self->{_store_comments} = 0;
46             $self->{_store_pis} = 0;
47             $self->{_store_declarations} = 0;
48             $self->{_store_cdata} = 0;
49              
50             # have to let HTML::Element know there are encoded entities
51             $XML::Element::encoded_content = $NoExpand if ($NoExpand);
52              
53             my @stack;
54              
55             # Compare the simplicity of this to the sheer nastiness of HTML::TreeBuilder!
56              
57             $self->{_xml_parser} = XML::Parser->new(
58             Handlers => {
59             Default => sub {
60              
61             # Stuff unexpanded entities back on to the stack as is.
62             if ( ($NoExpand) && ( $_[1] =~ /&[^\;]+\;/ ) ) {
63             $stack[-1]->push_content( $_[1] );
64             }
65             return;
66             },
67              
68             Start => sub {
69             my $xp = shift;
70             my $str = $xp->original_string();
71             if (@stack) {
72             my @args;
73             my $tag = shift(@_);
74             while (@_) {
75             my ( $attr, $val ) = splice( @_, 0, 2 );
76             ## BUGBUG This dirty hack is because the $val from XML::Parser isn't correct when $NoExpand is set ... can we fix it?
77             ## any entity in an attribute is lost
78             ## given $val is "this--attr" not "this-&FOO;-attr"
79             if ( $NoExpand && $str =~ /\s$attr="([^"]*\&[^"]*)"/ )
80             {
81             $val = $1;
82             }
83             push( @args, $attr, $val );
84             }
85              
86             unshift( @args, $tag );
87             push @stack, $self->{_element_class}->new(@args);
88             $stack[-2]->push_content( $stack[-1] );
89             }
90             else {
91             $self->tag(shift);
92             while (@_) {
93             my ( $attr, $val ) = splice( @_, 0, 2 );
94             ## BUGBUG This dirty hack is because the $val from XML::Parser isn't correct when $NoExpand is set ... can we fix it?
95             ## any entity in an attribute is lost
96             ## given $val is "this--attr" not "this-&FOO;-attr"
97             if ( $NoExpand && $str =~ /\s$attr="([^"]*\&[^"]*)"/ )
98             {
99             $val = $1;
100             }
101             $self->attr( $attr, $val );
102             }
103             push @stack, $self;
104             }
105             },
106              
107             End => sub { pop @stack; return },
108              
109             Char => sub {
110              
111             # have to escape '&' if we have entities to catch things like &foo;
112             if ( $_[1] eq '&' and $NoExpand ) {
113             $stack[-1]->push_content('&');
114             }
115             else {
116             $stack[-1]->push_content( $_[1] );
117             }
118             },
119              
120             Comment => sub {
121             return unless $self->{_store_comments};
122             ( @stack ? $stack[-1] : $self )
123             ->push_content( $self->{_element_class}
124             ->new( '~comment', 'text' => $_[1] ) );
125             return;
126             },
127              
128             Proc => sub {
129             return unless $self->{'_store_pis'};
130             ( @stack ? $stack[-1] : $self )
131             ->push_content( $self->{_element_class}
132             ->new( '~pi', 'text' => "$_[1] $_[2]" ) );
133             return;
134             },
135              
136             # And now, declarations:
137              
138             Attlist => sub {
139             return unless $self->{_store_declarations};
140             shift;
141             ( @stack ? $stack[-1] : $self )->push_content(
142             $self->{_element_class}->new(
143             '~declaration',
144             'text' => join ' ',
145             'ATTLIST', @_
146             )
147             );
148             return;
149             },
150              
151             Element => sub {
152             return unless $self->{_store_declarations};
153             shift;
154             ( @stack ? $stack[-1] : $self )->push_content(
155             $self->{_element_class}->new(
156             '~declaration',
157             'text' => join ' ',
158             'ELEMENT', @_
159             )
160             );
161             return;
162             },
163              
164             Doctype => sub {
165             return unless $self->{_store_declarations};
166             shift;
167             ## Need this because different types set different array entries.
168             no warnings 'uninitialized';
169             ( @stack ? $stack[-1] : $self )->push_content(
170             $self->{_element_class}->new(
171             '~declaration',
172             'text' => join( ' ', ( 'DOCTYPE', @_ ) ),
173             type => 'DOCTYPE',
174             mytag => $_[0],
175             uri => $_[1],
176             pid => $_[2],
177             )
178             );
179             return;
180             },
181              
182             Entity => sub {
183             return unless $self->{_store_declarations};
184             shift;
185             ## Need this because different entity types set different array entries.
186             no warnings 'uninitialized';
187             ( @stack ? $stack[-1] : $self )->push_content(
188             $self->{_element_class}->new(
189             '~declaration',
190             'text' => join( ' ', ( 'ENTITY', @_ ) ),
191             type => 'ENTITY',
192             name => $_[0],
193             value => $_[1],
194             )
195             );
196             return;
197             },
198              
199             CdataStart => sub {
200             return unless $self->{_store_cdata};
201             shift;
202             push @stack,
203             $self->{_element_class}->new( '~cdata', 'text' => $_[1] );
204             $stack[-2]->push_content( $stack[-1] );
205             return;
206             },
207              
208             CdataEnd => sub {
209             return unless $self->{_store_cdata};
210             pop @stack;
211             return;
212             },
213              
214             ExternEnt => sub {
215             return if ($NoExpand);
216             my $xp = shift;
217             my ( $base, $sysid, $pubid ) = @_;
218             my $file = "$sysid";
219              
220             if ( $sysid =~ /^http:/ ) {
221             ## BUGBUG need to catch when there is no local file
222             my $cat = XML::Catalog->new($catalog);
223             $file = $cat->resolve_public($pubid);
224             croak("Can't resolve '$pubid'")
225             if ( !defined($file) || $file eq '' );
226             $file =~ s/^file:\/\///;
227             my ( $filename, $directories, $suffix )
228             = fileparse($file);
229             $base = $directories;
230             }
231             else {
232             $sysid =~ s/^file:\/\/// if ( $sysid =~ /^file:/ );
233              
234             if ( File::Spec->file_name_is_absolute($sysid) ) {
235             my ( $filename, $directories, $suffix )
236             = fileparse($sysid);
237             $base = $directories;
238             }
239             else {
240             my ( $filename, $directories, $suffix )
241             = fileparse($base);
242             $file = File::Spec->rel2abs( $sysid, $directories );
243             }
244             }
245             my $fh = new IO::File( $file, "r" );
246             croak "$!" unless $fh;
247             $xp->{_BaseStack} ||= [];
248             $xp->{_FhStack} ||= [];
249              
250             push( @{ $xp->{_BaseStack} }, $base );
251             push( @{ $xp->{_FhStack} }, $fh );
252              
253             $xp->base($base);
254             return ($fh);
255             },
256              
257             ExternEntFin => sub {
258             return if ($NoExpand);
259             my ($xp) = shift;
260              
261             my $fh = pop( @{ $xp->{_FhStack} } );
262             $fh->close if ($fh);
263              
264             my $base = pop( @{ $xp->{_BaseStack} } );
265             $xp->base($base) if ($base);
266             return;
267             },
268              
269             },
270             NoExpand => $NoExpand,
271             ErrorContext => $ErrorContext,
272             ParseParamEnt => !$NoExpand,
273             NoLWP => 0,
274             );
275              
276             return $self;
277             }
278              
279             #==========================================================================
280             sub _elem # universal accessor...
281             {
282             my ( $self, $elem, $val ) = @_;
283             my $old = $self->{$elem};
284             $self->{$elem} = $val if defined $val;
285             return $old;
286             }
287              
288             sub store_comments { shift->_elem( '_store_comments', @_ ); }
289             sub store_declarations { shift->_elem( '_store_declarations', @_ ); }
290             sub store_pis { shift->_elem( '_store_pis', @_ ); }
291             sub store_cdata { shift->_elem( '_store_cdata', @_ ); }
292              
293             #==========================================================================
294              
295             sub parse {
296             shift->{_xml_parser}->parse(@_);
297             }
298              
299             sub parse_file { shift->parsefile(@_) } # alias
300              
301             sub parsefile {
302             shift->{_xml_parser}->parsefile(@_);
303             }
304              
305             sub eof {
306             delete shift->{_xml_parser}; # sure, why not?
307             }
308              
309             #==========================================================================
310             1;
311              
312             __END__