File Coverage

blib/lib/HTML/Xit.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package HTML::Xit;
2              
3 13     13   301376 use strict;
  13         26  
  13         427  
4 13     13   51 use warnings;
  13         19  
  13         286  
5              
6 13     13   638 use Data::Dumper;
  13         7244  
  13         719  
7 13         803 use HTML::Selector::XPath qw(
8             selector_to_xpath
9 13     13   6097 );
  13         26277  
10 13         998 use Scalar::Util qw(
11             blessed
12             reftype
13             weaken
14 13     13   67 );
  13         17  
15 13     13   2565 use XML::LibXML;
  0            
  0            
16              
17             our $VERSION = '0.05';
18              
19             # default arguments for XML::LibXML
20             my $default_libxml_args = {
21             recover => 1,
22             suppress_errors => 1,
23             suppress_warnings => 1,
24             };
25              
26             ###################
27             # #
28             # PRIVATE METHODS #
29             # #
30             ###################
31              
32             # empty_x
33             #
34             # because methods are chained we need an HTML::Xit object to return
35             # even if a previous selection in the chain failed.
36             my $empty_x = sub { };
37             bless($empty_x, __PACKAGE__);
38              
39             # new_explicit_args
40             #
41             # create new XML::LibXML instance with args that are passed
42             my $new_explicit_args = sub {
43             my %args = @_;
44              
45             for my $arg (keys %$default_libxml_args) {
46             # set default args values unless the arg is
47             # explicitly set
48             $args{$arg} = $default_libxml_args->{$arg}
49             unless exists $args{$arg};
50             }
51              
52             return {
53             _xml => XML::LibXML->load_html( %args )
54             };
55             };
56              
57             # new_guess_args
58             #
59             # guess source as one of IO, location, or string based
60             # on the type of scalar that is passed.
61             #
62             # call new_explicit_args once type is guessed.
63             my $new_guess_args = sub {
64             my $arg = shift or die;
65              
66             if (ref $arg) {
67             # file handle
68             if (reftype $arg eq 'GLOB') {
69             return $new_explicit_args->(IO => $arg);
70             }
71             # scalar ref
72             elsif (reftype $arg eq 'SCALAR') {
73             return $new_explicit_args->(string => $arg);
74             }
75             }
76             # url or valid file path
77             elsif ( $arg =~ m{^http} || ($arg !~ m{\n} && -e $arg) ) {
78             return $new_explicit_args->(location => $arg);
79             }
80             # text
81             else {
82             return $new_explicit_args->(string => $arg);
83             }
84             };
85              
86             # declare this first since we need to call in from closure
87             my $new_X;
88             # new_X
89             #
90             # create a new X instance which is a function reference that
91             # can return a hidden hash ref instance where data is stored
92             $new_X = sub {
93             my($self) = @_;
94             # self must be a hash ref that includes a XML::LibXML object
95             return $empty_x unless eval { $self->{_xml} };
96              
97             # need to jump through some hoops here to avoid creating
98             # a circular ref. We can't have $X getting captured in its
99             # own closure, but we need to know the scalar value of $X, so
100             # create $Y, then assign the scalar value of $X to $Y after creating
101             # $X
102             my $Y;
103             # HTML::Xit instance
104             my $X = sub {
105             my($select) = @_;
106             # if we are passed a self-reference then
107             # return our hidden instance variable
108             return $self
109             if ref $select
110             and $select eq $Y;
111              
112             my $xml = $self->{_xml};
113             # else create a new instance
114             my $self = {};
115             # create new node
116             if ($select =~ m{^<([^>]+)>$}) {
117             my $node_name = $1;
118             # try to get XML::LibXML::Document
119             my $doc = $xml
120             # if the current node supports createElement then use it
121             ? $xml->can('createElement')
122             ? $xml
123             # if the current belongs to a Document then use that
124             : $xml->can('ownerDocument')
125             ? $xml->ownerDocument
126             : XML::LibXML::Document->new
127             # create a new Document to create node from
128             : XML::LibXML::Document->new;
129             # create element under document
130             $xml = $self->{_xml} = $doc->createElement($node_name);
131             }
132             # value is selector
133             else {
134             # generate xpath from CSS selector using HTML::Selector::XPath
135             my $xpath = selector_to_xpath($select)
136             or return $empty_x;
137             # set the current xml context as the result of the xpath selection
138             $self->{_xml} = $xml->find('.'.$xpath);
139             }
140             # return a new HTML::Xit instace with either the selected or created elements
141             return $new_X->($self);
142             };
143              
144             bless($X, __PACKAGE__);
145             # store scalar value of $X without creating another ref
146             $Y = "$X";
147             # return blessed sub ref
148             return $X;
149             };
150              
151             # each
152             #
153             # call callback function for each argument
154             #
155             # unlike the public version of this method the private version does
156             # not create HTML::Xit instances around the returned object.
157             my $each = sub {
158             my($elm, $func) = @_;
159              
160             if (ref $elm && reftype $elm eq 'ARRAY') {
161             $func->($_) for @$elm;
162             }
163             else {
164             $func->($elm);
165             }
166             };
167              
168             # first
169             #
170             # return the first element from array or return arg if not an array.
171             #
172             # unlike the public version of this method the private version does
173             # not create HTML::Xit instances around the returned object.
174             my $first = sub {
175             my($elm) = @_;
176              
177             return ref $elm && reftype $elm eq 'ARRAY'
178             ? $elm->[0]
179             : $elm;
180             };
181              
182             # arg_to_nodes
183             #
184             # take one or more arguments that may be strings of xml/html
185             # Xit objects referencing XML, or XML objects and return an
186             # XML fragment
187             my $arg_to_nodes = sub
188             {
189             my $parser = XML::LibXML->new(%$default_libxml_args)
190             or return;
191              
192             my $nodes = [];
193              
194             for my $arg (@_) {
195             if (ref $arg) {
196             if ($arg->isa('HTML::Xit')) {
197             push(@$nodes, @{$arg->get});
198             }
199             }
200             else {
201             my $xml = $parser->parse_balanced_chunk($arg);
202             push(@$nodes, $xml);
203             }
204             }
205              
206             return $nodes;
207             };
208              
209             # mod_class
210             #
211             # perform addClass, removeClass, and toggleClass methods
212             my $mod_class = sub {
213             my($action, $X, $value) = @_;
214              
215             return $X unless $value;
216              
217             my $self = $X->($X);
218             my $xml = $self->{_xml} or return $X;
219             # class list may be one or more space seperated class names
220             my @mod_classes = grep {$_ =~ /\w+/} split(/\s+/, $value);
221              
222             return $X unless @mod_classes;
223              
224             $each->($xml, sub {
225             my($sel) = @_;
226              
227             my $class = $sel->getAttribute('class');
228              
229             my $classes = {
230             map {$_ => 1} grep {$_ =~ /\w+/} split(/\s+/, $class)
231             };
232              
233             if ($action eq 'add') {
234             $classes->{$_} = 1 for @mod_classes;
235             }
236             elsif ($action eq 'remove') {
237             delete $classes->{$_} for @mod_classes;
238             }
239             elsif ($action eq 'toggle') {
240             for my $mod_class (@mod_classes) {
241             if ($classes->{$mod_class}) {
242             delete $classes->{$mod_class};
243             }
244             else {
245             $classes->{$mod_class} = 1;
246             }
247             }
248             }
249              
250             $class = join(' ', sort keys %$classes);
251              
252             $sel->setAttribute('class', $class);
253             });
254              
255             return $X;
256             };
257              
258             ##################
259             # #
260             # PUBLIC METHODS #
261             # #
262             ##################
263              
264             # new
265             #
266             # create new HTML::Xit instance which is a function ref
267             sub new
268             {
269             my $class = shift;
270             # process args which may be in the form of:
271             # new("...")
272             # new("http://www...")
273             # new(FH)
274             # new("/my/file.html")
275             # new(a => 1, b => 2, ...)
276             # new({a => 1, b => 2})
277             my $self = @_ == 1
278             ? ref $_[0] && ref $_[0] eq 'HASH'
279             # first arg is hash ref, use as args
280             ? $new_explicit_args->( %{$_[0]} )
281             # first arg is not hash ref, guess what it is
282             : $new_guess_args->(@_)
283             # treat multiple args as explicit, which are passed
284             # directly to XML::LibXML
285             : $new_explicit_args->(@_);
286             # need XML::LibXML instance to continue
287             return unless $self and $self->{_xml};
288              
289             return $new_X->($self);
290             }
291              
292             # addClass
293             #
294             # add class using $mod_class
295             sub addClass { $mod_class->('add', @_) }
296              
297             # append
298             #
299             # add nodes after last child
300             sub append
301             {
302             my($X) = shift;
303             my $self = $X->($X);
304              
305             my $xml = $self->{_xml} or return $X;
306              
307             my $child_nodes = $arg_to_nodes->(@_)
308             or return $X;
309              
310             $each->($xml, sub {
311             my $sel = shift or return;
312             # must be able to have children
313             return unless $sel->can('appendChild');
314             # append one or more child nodes
315             $each->($child_nodes, sub {
316             my $node = shift or return;
317             # deep clone child
318             $sel->appendChild( $node->cloneNode(1) );
319             });
320             });
321              
322             return $X;
323             }
324              
325             # attr
326             #
327             # get or set an attribute on selected XML nodes
328             sub attr
329             {
330             my($X, $name, $value) = @_;
331             my $self = $X->($X);
332             my $xml = $self->{_xml};
333              
334             if (defined $value)
335             {
336             return $X unless $xml;
337              
338             $each->($xml, sub {
339             my $sel = shift or return;
340              
341             $sel->setAttribute($name, $value)
342             if $sel->can('setAttribute');
343             });
344              
345             return $X;
346             }
347             else
348             {
349             return undef unless $xml;
350              
351             my $sel = $first->($xml);
352              
353             return $sel->can('getAttribute')
354             ? $sel->getAttribute($name)
355             : undef;
356             }
357             }
358              
359             # children
360             #
361             # return child nodes. *does not* process optional selector
362             # at this time.
363             sub children
364             {
365             my($X) = @_;
366             my $self = $X->($X);
367              
368             my $xml = $self->{_xml} or return $X;
369              
370             # if we are working on a list of elements then return
371             # the children of all of the elements
372             if (reftype $xml eq 'ARRAY') {
373             my @child_nodes;
374             # add child nodes of each element to list
375             push(@child_nodes, $_->childNodes) for grep {
376             $_->can('childNodes')
377             } @$xml;
378             # return empty HTML::Xit unless child nodes were found
379             return $empty_x unless @child_nodes;
380             # return new HTML::Xit with child nodes
381             return $new_X->( {_xml => \@child_nodes} );
382             }
383             else {
384             # get child nodes or return empty HTML::Xit object
385             return $empty_x unless $xml->can('childNodes')
386             and (my @child_nodes = $xml->childNodes);
387             # create new HTML::Xit object from child nodes
388             return $new_X->( {_xml => \@child_nodes} );
389             }
390             }
391              
392             # classes
393             #
394             # return a list of the classes assigned to elements
395             sub classes
396             {
397             my($X) = @_;
398             my $self = $X->($X);
399              
400             my $xml = $self->{_xml} or return;
401              
402             my $classes;
403              
404             # for multiple elements add each element's class attribute
405             # to the list of classes
406             if (reftype $xml eq 'ARRAY') {
407             $classes .= " " . $_->getAttribute('class') for @$xml;
408             }
409             # get class attribute for single element
410             else {
411             $classes = $xml->getAttribute('class');
412             }
413             # build table of unique class names
414             $classes = {
415             map {$_ => 1} grep {defined $_ && $_ =~ /\w+/} split(/\s+/, $classes)
416             };
417              
418             return wantarray
419             # return sorted list of classes
420             ? sort keys %$classes
421             # return hashref of classes
422             : $classes;
423             }
424              
425             # each
426             #
427             # call callback function for each argument
428             #
429             # unlike the private version, this method creates a new
430             # HTML::Xit instance for each XML element being iterated on
431             sub each
432             {
433             my($X, $func) = @_;
434             my $self = $X->($X);
435              
436             my $xml = $self->{_xml} or return $X;
437              
438             if (ref $xml && reftype $xml eq 'ARRAY') {
439             # call callback for each element in array, creating a
440             # new HTML::Xit instance for each XML element
441             $func->( $new_X->({_xml => $_}) ) for @$xml;
442             }
443             else {
444             # call callback, creating a new HTML::Xit instance
445             $func->( $new_X->({_xml => $xml}) );
446             }
447              
448             return $X;
449             }
450              
451             # find
452             #
453             # get all matching child elements
454             sub find
455             {
456             my($X, $query) = @_;
457              
458             my $nodes = [];
459              
460             $X->each(sub {
461             my($X) = @_;
462             my $node = $X->($query)->get
463             or return;
464             $node = [$node]
465             unless reftype $node eq 'ARRAY';
466             push(@$nodes, @$node);
467             });
468              
469             return $new_X->({_xml => $nodes})
470             }
471              
472             # first
473             #
474             # return the first element from array or return arg if not an array.
475             #
476             # unlike the private version, this method creates a new
477             # HTML::Xit instance for node being returned
478             sub first {
479             my($X) = @_;
480             my $self = $X->($X);
481              
482             my $xml = $self->{_xml} or return $X;
483              
484             return ref $xml && reftype $xml eq 'ARRAY'
485             ? $new_X->( {_xml => $xml->[0]} )
486             : $new_X->( {_xml => $xml} );
487             };
488              
489             # get
490             #
491             # return the XML::LibXML nodes or node identified by index
492             sub get
493             {
494             my($X, $index) = @_;
495             my $self = $X->($X);
496              
497             my $xml = $self->{_xml} or return $X;
498             # make sure we have nodes in array
499             my $nodes = ref $xml && reftype $xml eq 'ARRAY'
500             ? $xml : [ $xml ];
501             # return either all nodes or specified index
502             return defined $index && int $index
503             ? $nodes->[ $index ]
504             : $nodes;
505             }
506              
507             # hasClass
508             #
509             # return true (1) / false (0) if any of the elements have
510             # the class. return undef on errors.
511             sub hasClass
512             {
513             my($X, $class) = @_;
514             # require class name to test
515             return unless defined $class;
516             # get hashref of class names
517             my $classes = $X->classes()
518             or return;
519              
520             return $classes->{$class} ? 1 : 0;
521             }
522              
523             # html
524             #
525             # return html content or if that is not possible return text
526             # content
527             sub html
528             {
529             my($X) = shift;
530             my $self = $X->($X);
531              
532             my $xml = $self->{_xml};
533             # if there are args we modify the current elements
534             # and return the current HTML::Xit instance
535             if (@_) {
536             # require current element
537             return $X unless $xml;
538             # build nodes to be appended from args which can
539             # be either HTML strings or HTML::Xit objects
540             my $child_nodes = $arg_to_nodes->(@_)
541             or return $X;
542             # go through each of the current elements and set
543             # the child nodes from args to be their HTML content
544             $each->($xml, sub {
545             my $sel = shift or return;
546             # must be able to have children
547             return unless $sel->can('appendChild');
548             # html replaces any existing child nodes
549             $sel->removeChildNodes()
550             if $sel->can('removeChildNodes');
551             # append one or more child nodes
552             $each->($child_nodes, sub {
553             my $node = shift or return;
554             # deep clone child
555             $sel->appendChild( $node->cloneNode(1) );
556             });
557             });
558             # return HTML::Xit instance
559             return $X;
560             }
561             # if there are no args then we return the HTML text of the
562             # current elements or an empty string
563             else {
564             # require current element
565             return undef unless $xml;
566             # get the first element
567             my $sel = $first->($xml);
568             # if the current node has children then create html
569             # by concatenating html values of child nodes
570             if ( $sel->can('childNodes') && (my @child_nodes = $sel->childNodes) ) {
571             return join('', map {
572             $_->can('toStringHTML')
573             ? $_->toStringHTML
574             : $_->can('toString')
575             ? $_->toString
576             : ''
577             } @child_nodes);
578             }
579             # if the node has no children then it can only have
580             # text content (?) maybe not possible
581             else {
582             return $sel->can('toString')
583             ? $sel->toString
584             : '';
585             }
586             }
587             }
588              
589             # last
590             #
591             # return the last matching element
592             sub last {
593             my($X) = @_;
594             my $self = $X->($X);
595              
596             my $xml = $self->{_xml} or return $X;
597              
598             return ref $xml && reftype $xml eq 'ARRAY'
599             ? $new_X->( {_xml => $xml->[-1]} )
600             : $new_X->( {_xml => $xml} );
601             };
602              
603             # prepend
604             #
605             # add nodes before first child
606             sub prepend
607             {
608             my($X) = shift;
609             my $self = $X->($X);
610              
611             my $xml = $self->{_xml} or return $X;
612              
613             my $child_nodes = $arg_to_nodes->(@_)
614             or return $X;
615              
616             my $first_child = shift @$child_nodes;
617              
618             $each->($xml, sub {
619             my $sel = shift or return;
620              
621             if ($sel->can('firstChild')) {
622             # insert first node before first child
623             my $insert_after = $sel->insertBefore(
624             $first_child->cloneNode(1),
625             $sel->firstChild,
626             ) or return;
627              
628             $each->($child_nodes, sub {
629             my $node = shift or return;
630              
631             return unless $insert_after
632             and $insert_after->can('insertAfter');
633              
634             $insert_after = $sel->insertAfter(
635             $node->cloneNode(1),
636             $insert_after
637             );
638             });
639             }
640             elsif ($self->can('addChild')) {
641             $sel->addChild( $first_child->cloneNode(1) );
642             $each->($child_nodes, sub {
643             my $node = shift or return;
644             });
645             }
646             else {
647             return;
648             }
649             });
650              
651             return $X;
652             }
653              
654             # removeClass
655             #
656             # remove class using $mod_class
657             sub removeClass { $mod_class->('remove', @_) }
658              
659             # text
660             #
661             # return text content
662             sub text
663             {
664             my($X, $value) = @_;
665             my $self = $X->($X);
666              
667             my $xml = $self->{_xml};
668              
669             if (defined $value) {
670             return $X unless $xml;
671              
672             $each->($xml, sub {
673             my $sel = shift or return;
674             # text replaces everything else so remove child nodes
675             # if they exist
676             $sel->removeChildNodes() if $sel->can('removeChildNodes');
677             # attempt different methods of adding text
678             # XML::LibXML::Element
679             if ($sel->can('appendText')) {
680             $sel->appendText($value);
681             }
682             # XML::LibXML::Text
683             elsif ($sel->can('setData')) {
684             $sel->setData($value);
685             }
686             # XML::LibXML::Node
687             elsif ($sel->can('appendChild')) {
688             $sel->appendChild( $sel->createTextNode($value) );
689             }
690             });
691             }
692             else {
693             return undef unless $xml;
694              
695             my $sel = $first->($xml);
696             return $sel && $sel->can('textContent')
697             ? $sel->textContent
698             : undef;
699             }
700              
701             return $X;
702             }
703              
704             # trimText
705             sub trimText
706             {
707             my($X) = @_;
708              
709             my $text = $X->text;
710             return unless defined $text;
711              
712             $text =~ s{^\s*|\s*$}{}g;
713              
714             return $text;
715             }
716              
717             # toggleClass
718             #
719             # toggle class using $mod_class
720             sub toggleClass { $mod_class->('toggle', @_) }
721              
722             1;
723              
724             __END__