File Coverage

blib/lib/HTML/TreeBuilder/LibXML/Node.pm
Criterion Covered Total %
statement 240 252 95.2
branch 88 118 74.5
condition 11 18 61.1
subroutine 48 51 94.1
pod 0 42 0.0
total 387 481 80.4


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