File Coverage

blib/lib/HTML/TreeBuilder/LibXML/Node.pm
Criterion Covered Total %
statement 18 251 7.1
branch 0 118 0.0
condition 0 15 0.0
subroutine 6 51 11.7
pod 0 42 0.0
total 24 477 5.0


line stmt bran cond sub pod time code
1             package HTML::TreeBuilder::LibXML::Node;
2 22     22   101 use strict;
  22         26  
  22         485  
3 22     22   69 use warnings;
  22         23  
  22         344  
4 22     22   65 use Carp();
  22         23  
  22         19237  
5              
6             sub new {
7 0     0 0   my $class = shift;
8 0 0         Carp::croak 'missing arguments' unless @_>=1;
9 0 0 0       if (@_==1 && ref($_[0])) {
10 0           bless {node => $_[0]}, $class;
11             } else {
12 0           my ($tag, @attrs) = @_;
13 0           my $doc = XML::LibXML->createDocument;
14 0           my $node = $doc->createElement($tag);
15 0           while (my ($k, $v) = splice @attrs, 0, 2) {
16 0           $node->setAttribute($k, $v);
17             }
18 0           bless {node => $node}, $class;
19             }
20             }
21              
22             sub attr {
23 0     0 0   my ($self, $key, $value) = @_;
24 0 0 0       if (@_ == 3) {
    0          
25 0 0         if (defined $value) {
26 0           $self->{node}->setAttribute (lc $key, $value);
27             } else {
28 0           $self->{node}->removeAttribute(lc $key);
29             }
30             } elsif (@_ == 2 and lc $key eq 'text') {
31 0           return $self->{node}->textContent;
32             }
33 0           $self->{node}->getAttribute(lc $key);
34             }
35              
36             sub isTextNode {
37 0     0 0   my $self = shift;
38 0           $self->{node}->isa('XML::LibXML::Text');
39             }
40              
41             # The analog of HTML::TreeBuilder::XPath::getValue for comment nodes
42             *getValue = \&as_text;
43              
44             sub string_value {
45 0     0 0   $_[0]->{node}->textContent;
46             }
47              
48             sub as_text {
49 0     0 0   $_[0]->{node}->textContent;
50             }
51              
52             sub as_trimmed_text {
53 0     0 0   my $text = shift->as_text(@_);
54 0           $text =~ s/[\n\r\f\t ]+$//s;
55 0           $text =~ s/^[\n\r\f\t ]+//s;
56 0           $text =~ s/[\n\r\f\t ]+/ /g;
57 0           return $text;
58             }
59 0     0 0   sub as_text_trimmed { shift->as_trimmed_text(@_) } # alias
60              
61       0 0   sub objectify_text { }
62       0 0   sub deobjectify_text { }
63              
64             sub as_XML {
65 0     0 0   $_[0]->{node}->toString;
66             }
67              
68             sub as_HTML {
69 0 0   0 0   return $_[0]->{node}->toStringHTML if $_[0]->{node}->can('toStringHTML'); # best method, but only document nodes can toStringHTML()
70            
71             # second best is to call toStringC14N(1), which generates valid HTML (eg. no auto closed
),
72             # but dies on some cases with "Failed to convert doc to string in doc->toStringC14N" error.
73             # so we fallback to toString()
74             {
75 0           local $@; # protect existing $@
  0            
76 0           my $output = eval { $_[0]->{node}->toStringC14N(1) };
  0            
77 0 0         return $@ ? $_[0]->{node}->toString : $output;
78             }
79             }
80              
81             sub tag {
82             $_[0]->{node}->localname
83 0     0 0   }
84              
85             sub id {
86 0 0   0 0   if (@_==2) {
87             # setter
88 0 0         if (defined $_[1]) {
89 0           $_[0]->{node}->setAttribute('id', $_[1]);
90             } else {
91 0           $_[0]->{node}->removeAttribute('id');
92             }
93             } else {
94 0           $_[0]->{node}->getAttribute('id');
95             }
96             }
97              
98             # hack for Web::Scraper
99             sub isa {
100 0     0 0   my ($self, $klass) = @_;
101 0 0         $klass eq 'HTML::Element' ? 1 : UNIVERSAL::isa($self, $klass);
102             }
103              
104             sub exists {
105 0     0 0   my( $self , $xpath ) = @_;
106              
107 0 0         $self->_eof_or_die unless $self->{node};
108 0           my @nodes = $self->{node}->findnodes( $xpath );
109 0 0         return scalar( @nodes ) ? 1 : 0;
110             }
111              
112             sub find {
113 0     0 0   my( $self , $elem ) = @_;
114              
115 0 0         $self->_eof_or_die unless $self->{node};
116              
117 0           my @nodes = $self->{node}->getElementsByTagName( $elem );
118 0           @nodes = map { HTML::TreeBuilder::LibXML::Node->new( $_ ) } @nodes;
  0            
119              
120 0 0         wantarray ? @nodes : \@nodes;
121             }
122              
123             sub findnodes {
124 0     0 0   my ($self, $xpath) = @_;
125              
126 0 0         $self->_eof_or_die unless $self->{node};
127 0           my @nodes = $self->{node}->findnodes( $xpath );
128 0           @nodes = map { HTML::TreeBuilder::LibXML::Node->new($_) } @nodes;
  0            
129 0 0         wantarray ? @nodes : \@nodes;
130             }
131              
132             *findnodes_as_string = \&findvalue;
133             *findnodes_as_strings = \&findvalues;
134              
135             sub findnodes_filter {
136 0     0 0   my( $self , $xpath , $callback ) = @_;
137              
138 0 0 0       Carp::croak "Second argument must be coderef"
139             unless $callback and ref $callback eq 'CODE';
140              
141 0           my @nodes = $self->findnodes( $xpath );
142 0           @nodes = grep { $callback->($_) } @nodes;
  0            
143              
144 0 0         wantarray ? @nodes : \@nodes;
145             }
146              
147             sub findvalue {
148 0     0 0   my ($self, $xpath) = @_;
149              
150 0 0         $self->_eof_or_die unless $self->{node};
151 0           $self->{node}->findvalue( $xpath );
152             }
153              
154             sub findvalues {
155 0     0 0   my( $self , $xpath ) = @_;
156              
157 0 0         $self->_eof_or_die unless $self->{node};
158 0           my $nodes = $self->{node}->find( $xpath );
159 0           my @nodes = map { $_->textContent } $nodes->get_nodelist;
  0            
160 0 0         wantarray ? @nodes : \@nodes;
161             }
162              
163             sub clone {
164 0     0 0   my ($self, ) = @_;
165              
166 0           my $orignode = $self->{node};
167 0           my $origdoc = $orignode->ownerDocument;
168              
169 0           my $node = $orignode->cloneNode(1);
170              
171             # arguments can be undefined
172 0           my $doc = do {
173 22     22   101 no warnings;
  22         29  
  22         17855  
174 0           XML::LibXML::Document->new($origdoc->version, $origdoc->encoding);
175             };
176            
177 0 0         if ($node->isa('XML::LibXML::Dtd')) {
    0          
178 0           $doc->createInternalSubset( $node->getName, $node->publicId, $node->systemId );
179 0           $node = $doc->internalSubset;
180             } elsif ($node->isa('XML::LibXML::Element')) {
181 0           $doc->setDocumentElement($node);
182             } else {
183 0           $doc->adoptNode($node);
184             }
185            
186 0           my $cloned = __PACKAGE__->new($node);
187 0           return $cloned;
188             }
189              
190             sub clone_list {
191 0     0 0   my $class = shift;
192 0           my @clones = map { $_->clone } @_;
  0            
193 0           @clones;
194             }
195              
196             sub detach {
197 0     0 0   my $self = shift;
198 0           my $parent = $self->parent;
199             #$self->{node}->unbindNode();
200 0           my $doc = XML::LibXML->createDocument;
201 0           $doc->adoptNode($self->{node});
202 0           $doc->setDocumentElement($self->{node});
203 0           $parent;
204             }
205              
206             sub delete {
207 0     0 0   my $self = shift;
208 0           $self->{node}->unbindNode();
209             }
210              
211             sub delete_content {
212 0     0 0   my ($self) = @_;
213 0           $self->{node}->removeChildNodes;
214             }
215              
216             sub getFirstChild {
217 0     0 0   my $self = shift;
218 0           __PACKAGE__->new($self->{node}->getFirstChild);
219             }
220              
221             sub childNodes {
222 0     0 0   my $self = shift;
223              
224 0 0         $self->_eof_or_die unless $self->{node};
225 0           my @nodes = $self->{node}->childNodes;
226 0           @nodes = map { __PACKAGE__->new($_) } @nodes;
  0            
227 0 0         wantarray ? @nodes : \@nodes;
228             }
229              
230             sub content_list {
231 0     0 0   my ($self) = @_;
232 0           my @nodes = $self->childNodes;
233 0           @nodes;
234             }
235              
236             sub replace_with {
237 0     0 0   my $self = shift;
238            
239             # TODO handle @_ == 0
240            
241 0           my $node = $self->{node};
242 0           my $doc = $node->ownerDocument;
243 0           my $parent = $node->parentNode;
244 0 0         die "can't replace_with(), node has no parent!" unless $parent;
245            
246 0 0         my @nodes = map { ref $_ ? $_->{node} : $doc->createTextNode($_) } @_;
  0            
247            
248 0 0         if ($parent->isa('XML::LibXML::Document')) {
249             # can't call insertBefore() in a document node,
250             # so this is the best hack so far :[
251             # works only if $node is the last child
252 0 0         die "[not supported] calling replace_with() in a node that is child of a document node, and its not the last child."
253             unless $node->isSameNode($parent->lastChild);
254            
255 0           foreach (@nodes) {
256            
257 0 0         if ($_->isa('XML::LibXML::Dtd')) {
258 0           $parent->createInternalSubset($_->getName, $_->publicId, $_->systemId);
259 0           next;
260             }
261 0           $parent->adoptNode($_);
262 0           $node->addSibling($_);
263             }
264            
265             }
266             else {
267             $parent->insertBefore($_, $node)
268 0           for @nodes;
269             }
270            
271 0           $self->detach;
272 0           $self;
273             }
274              
275             sub push_content {
276 0     0 0   my $self = shift;
277            
278 0           my $node = $self->{node};
279 0 0         my $doc = $node->isa('XML::LibXML::Document') ? $node : $node->ownerDocument;
280 0 0         my @nodes = map { ref $_ ? $_->{node} : $doc->createTextNode($_) } @_;
  0            
281            
282             # thats because appendChild() is not supported on a Document node (as of XML::LibXML 2.0017)
283 0 0         if ($node->isa('XML::LibXML::Document')) {
284            
285 0           foreach (@nodes) {
286             #$node->adoptNode($_);
287 0 0         $node->hasChildNodes ? $node->lastChild->addSibling($_)
288             : $node->setDocumentElement($_);
289             }
290             }
291             else {
292 0           $node->appendChild($_) for @nodes;
293             }
294            
295 0           $self;
296             }
297              
298             sub unshift_content {
299 0     0 0   my $self = shift;
300            
301             return $self->push_content(@_)
302 0 0         unless $self->{node}->hasChildNodes;
303              
304 0           my $node = $self->{node};
305 0 0         my $doc = $node->isa('XML::LibXML::Document') ? $node : $node->ownerDocument;
306 0 0         my @nodes = map { ref $_ ? $_->{node} : $doc->createTextNode($_) } @_;
  0            
307            
308             # thats because insertBefore() is not supported on a Document node (as of XML::LibXML 2.0017)
309 0 0         if ($node->isa('XML::LibXML::Document')) {
310            
311 0           foreach (@nodes) {
312 0 0         $node->hasChildNodes ? $node->lastChild->addSibling($_)
313             : $node->setDocumentElement($_);
314             }
315            
316             # rotate
317 0           while (not $node->firstChild->isSameNode($nodes[0])) {
318 0           my $first_node = $node->firstChild;
319 0           $first_node->unbindNode;
320 0           $node->lastChild->addSibling($first_node);
321            
322             }
323             }
324             else {
325 0           my $first_child = $node->firstChild;
326 0           $node->insertBefore($_, $first_child) for @nodes;
327             }
328            
329 0           $self;
330             }
331              
332             sub left {
333 0     0 0   my $self = shift;
334              
335 0 0         $self->_eof_or_die unless $self->{node};
336 0           my $prev = $self->{node}->previousNonBlankSibling;
337 0 0         return $prev ? __PACKAGE__->new( $prev ) : undef;
338             }
339              
340             sub right {
341 0     0 0   my $self = shift;
342              
343 0 0         $self->_eof_or_die unless $self->{node};
344 0           my $next = $self->{node}->nextNonBlankSibling;
345 0 0         return $next ? __PACKAGE__->new( $next ) : undef;
346             }
347              
348             sub look_down {
349 0     0 0   my $self = shift;
350 0           my @args = @_;
351              
352 0 0         $self->_eof_or_die unless $self->{node};
353              
354 0           my @filter;
355 0           my $xpath = "//*"; # default
356 0           while (@args) {
357 0 0 0       if (ref $args[0] eq 'CODE') {
    0          
    0          
358 0           my $code = shift @args;
359 0           push @filter, $code;
360             } elsif (@args >= 2 && $args[0] eq '_tag') {
361 0           my($tag, $want_tag) = splice(@args, 0, 2);
362 0           $xpath = "//$want_tag";
363             } elsif (@args >= 2) {
364 0           my($attr, $stuff) = splice(@args, 0, 2);
365 0 0         if (ref $stuff eq 'Regexp') {
366 22     22   101 push @filter, sub { no warnings 'uninitialized'; $_[0]->attr($attr) =~ $stuff };
  22     0   28  
  22         1281  
  0            
  0            
367             } else {
368 22     22   246 push @filter, sub { no warnings 'uninitialized'; $_[0]->attr($attr) eq $stuff };
  22     0   23  
  22         12543  
  0            
  0            
369             }
370             } else {
371 0           Carp::carp("Don't know what to do with @args");
372 0           shift @args;
373             }
374             }
375              
376 0           $xpath =~ s/~text\b/text()/g;
377              
378 0           my @nodes = $self->findnodes($xpath);
379             my @wants = grep {
380 0           my $node = $_;
  0            
381 0           my $ok = 1;
382 0           for my $filter (@filter) {
383 0 0         $filter->($_) or $ok = 0;
384             }
385 0 0         $ok ? $node : ();
386             } @nodes;
387              
388 0 0         wantarray ? @wants : $wants[0];
389             }
390              
391             sub all_attr {
392 0     0 0   my $self = shift;
393 0           return map { $_->name => $_->value } $self->{node}->attributes;
  0            
394             }
395              
396             sub all_attr_names {
397 0     0 0   my $self = shift;
398 0           return map $_->name, $self->{node}->attributes;
399             }
400              
401 0     0 0   sub all_external_attr { shift->all_attr(@_) }
402 0     0 0   sub all_external_attr_names { shift->all_attr_names(@_) }
403              
404             sub _eof_or_die {
405 0     0     my $self = shift;
406 0 0         if (defined($self->{_content})) {
407 0           $self->eof;
408             } else {
409 0           Carp::croak "\$self is not loaded: $self"
410             }
411             }
412              
413              
414             sub matches {
415 0     0 0   my ($self, $xpath) = @_;
416            
417 0           foreach ($self->{node}->ownerDocument->findnodes($xpath)) {
418 0 0         return 1 if $_->isEqual($self->{node});
419             }
420            
421 0           return;
422             }
423              
424             sub parent {
425 0     0 0   my $self = shift;
426            
427 0 0         if (@_) {
428            
429             # unset
430 0 0         unless (defined $_[0]) {
431 0           $self->{node}->unbindNode;
432 0           return;
433             }
434            
435             # set
436             Carp::croak "an element can't be made its own parent"
437 0 0         if ref $_[0]->{node}->isSameNode($self->{node}); # sanity
438            
439 0           my $parent = $_[0]->{node};
440            
441 0 0         if ($_[0]->{node}->isa('XML::LibXML::Document')) {
442              
443 0 0         if ($parent->hasChildNodes) {
444 0           $parent->lastChild->addSibling($self->{node});
445             }
446             else {
447 0           $parent->adoptNode($self->{node});
448 0           $parent->setDocumentElement($self->{node});
449             }
450            
451             }
452             else {
453 0           $parent->appendChild($self->{node});
454             }
455            
456             }
457             else {
458             # get
459 0           my $parent = $self->{node}->parentNode;
460 0 0 0       return defined $parent && !$parent->isa('XML::LibXML::DocumentFragment')? ref($self)->new($parent) : undef;
461             }
462              
463             }
464              
465             sub postinsert {
466 0     0 0   my $self = shift;
467 0           my @nodes = map { $_->{node} } @_;
  0            
468 0           my $parent = $self->{node}->parentNode;
469            
470             $parent->insertAfter($_, $self->{node})
471 0           foreach reverse @nodes;
472            
473 0           $self;
474             }
475              
476             sub preinsert {
477 0     0 0   my $self = shift;
478 0           my @nodes = map { $_->{node} } @_;
  0            
479 0           my $parent = $self->{node}->parentNode;
480            
481             $parent->insertBefore($_, $self->{node})
482 0           foreach @nodes;
483            
484 0           $self;
485             }
486              
487              
488              
489             1;
490              
491             __END__