File Coverage

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