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   299154 use strict;
  13         23  
  13         421  
4 13     13   53 use warnings;
  13         14  
  13         361  
5              
6 13     13   575 use Data::Dumper;
  13         6999  
  13         650  
7 0           use HTML::Selector::XPath qw(
8             selector_to_xpath
9 13     13   2415 );
  0            
10             use Scalar::Util qw(
11             blessed
12             reftype
13             );
14             use XML::LibXML;
15              
16             our $VERSION = '0.03';
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} or return $X;
323              
324             if (defined $value)
325             {
326             $each->($xml, sub {
327             my $sel = shift or return;
328              
329             $sel->setAttribute($name, $value)
330             if $sel->can('setAttribute');
331             });
332             }
333             else
334             {
335             my $sel = $first->($xml);
336              
337             return $sel->getAttribute($name)
338             if $sel->can('getAttribute');
339             return;
340             }
341              
342             return $X;
343             }
344              
345             # children
346             #
347             # return child nodes. *does not* process optional selector
348             # at this time.
349             sub children
350             {
351             my($X) = @_;
352             my $self = $X->($X);
353              
354             my $xml = $self->{_xml} or return $X;
355              
356             # if we are working on a list of elements then return
357             # the children of all of the elements
358             if (reftype $xml eq 'ARRAY') {
359             my @child_nodes;
360             # add child nodes of each element to list
361             push(@child_nodes, $_->childNodes) for grep {
362             $_->can('childNodes')
363             } @$xml;
364             # return empty HTML::Xit unless child nodes were found
365             return $empty_x unless @child_nodes;
366             # return new HTML::Xit with child nodes
367             return $new_X->( {_xml => \@child_nodes} );
368             }
369             else {
370             # get child nodes or return empty HTML::Xit object
371             return $empty_x unless $xml->can('childNodes')
372             and (my @child_nodes = $xml->childNodes);
373             # create new HTML::Xit object from child nodes
374             return $new_X->( {_xml => \@child_nodes} );
375             }
376             }
377              
378             # classes
379             #
380             # return a list of the classes assigned to elements
381             sub classes
382             {
383             my($X) = @_;
384             my $self = $X->($X);
385              
386             my $xml = $self->{_xml} or return;
387              
388             my $classes;
389              
390             # for multiple elements add each element's class attribute
391             # to the list of classes
392             if (reftype $xml eq 'ARRAY') {
393             $classes .= " " . $_->getAttribute('class') for @$xml;
394             }
395             # get class attribute for single element
396             else {
397             $classes = $xml->getAttribute('class');
398             }
399             # build table of unique class names
400             $classes = {
401             map {$_ => 1} grep {defined $_ && $_ =~ /\w+/} split(/\s+/, $classes)
402             };
403              
404             return wantarray
405             # return sorted list of classes
406             ? sort keys %$classes
407             # return hashref of classes
408             : $classes;
409             }
410              
411             # each
412             #
413             # call callback function for each argument
414             #
415             # unlike the private version, this method creates a new
416             # HTML::Xit instance for each XML element being iterated on
417             sub each
418             {
419             my($X, $func) = @_;
420             my $self = $X->($X);
421              
422             my $xml = $self->{_xml} or return $X;
423              
424             if (ref $xml && reftype $xml eq 'ARRAY') {
425             # call callback for each element in array, creating a
426             # new HTML::Xit instance for each XML element
427             $func->( $new_X->({_xml => $_}) ) for @$xml;
428             }
429             else {
430             # call callback, creating a new HTML::Xit instance
431             $func->( $new_X->({_xml => $xml}) );
432             }
433              
434             return $X;
435             }
436              
437             # find
438             #
439             # get all matching child elements
440             sub find
441             {
442             my($X, $query) = @_;
443              
444             my $nodes = [];
445              
446             $X->each(sub {
447             my($X) = @_;
448             my $node = $X->($query)->get
449             or return;
450             $node = [$node]
451             unless reftype $node eq 'ARRAY';
452             push(@$nodes, @$node);
453             });
454              
455             return $new_X->({_xml => $nodes})
456             }
457              
458             # first
459             #
460             # return the first element from array or return arg if not an array.
461             #
462             # unlike the private version, this method creates a new
463             # HTML::Xit instance for node being returned
464             sub first {
465             my($X) = @_;
466             my $self = $X->($X);
467              
468             my $xml = $self->{_xml} or return $X;
469              
470             return ref $xml && reftype $xml eq 'ARRAY'
471             ? $new_X->( {_xml => $xml->[0]} )
472             : $new_X->( {_xml => $xml} );
473             };
474              
475             # get
476             #
477             # return the XML::LibXML nodes or node identified by index
478             sub get
479             {
480             my($X, $index) = @_;
481             my $self = $X->($X);
482              
483             my $xml = $self->{_xml} or return $X;
484             # make sure we have nodes in array
485             my $nodes = ref $xml && reftype $xml eq 'ARRAY'
486             ? $xml : [ $xml ];
487             # return either all nodes or specified index
488             return defined $index && int $index
489             ? $nodes->[ $index ]
490             : $nodes;
491             }
492              
493             # hasClass
494             #
495             # return true (1) / false (0) if any of the elements have
496             # the class. return undef on errors.
497             sub hasClass
498             {
499             my($X, $class) = @_;
500             # require class name to test
501             return unless defined $class;
502             # get hashref of class names
503             my $classes = $X->classes()
504             or return;
505              
506             return $classes->{$class} ? 1 : 0;
507             }
508              
509             # html
510             #
511             # return html content or if that is not possible return text
512             # content
513             sub html
514             {
515             my($X) = shift;
516             my $self = $X->($X);
517              
518             my $xml = $self->{_xml};
519             # if there are args we modify the current elements
520             # and return the current HTML::Xit instance
521             if (@_) {
522             # require current element
523             return $X unless $xml;
524             # build nodes to be appended from args which can
525             # be either HTML strings or HTML::Xit objects
526             my $child_nodes = $arg_to_nodes->(@_)
527             or return $X;
528             # go through each of the current elements and set
529             # the child nodes from args to be their HTML content
530             $each->($xml, sub {
531             my $sel = shift or return;
532             # must be able to have children
533             return unless $sel->can('appendChild');
534             # html replaces any existing child nodes
535             $sel->removeChildNodes()
536             if $sel->can('removeChildNodes');
537             # append one or more child nodes
538             $each->($child_nodes, sub {
539             my $node = shift or return;
540             # deep clone child
541             $sel->appendChild( $node->cloneNode(1) );
542             });
543             });
544             # return HTML::Xit instance
545             return $X;
546             }
547             # if there are no args then we return the HTML text of the
548             # current elements or an empty string
549             else {
550             # require current element
551             return '' unless $xml;
552             # get the first element
553             my $sel = $first->($xml);
554             # if the current node has children then create html
555             # by concatenating html values of child nodes
556             if ( $sel->can('childNodes') && (my @child_nodes = $sel->childNodes) ) {
557             return join('', map {
558             $_->can('toStringHTML')
559             ? $_->toStringHTML
560             : $_->can('toString')
561             ? $_->toString
562             : ''
563             } @child_nodes);
564             }
565             # if the node has no children then it can only have
566             # text content (?) maybe not possible
567             else {
568             return $sel->can('toString')
569             ? $sel->toString
570             : '';
571             }
572             }
573             }
574              
575             # last
576             #
577             # return the last matching element
578             sub last {
579             my($X) = @_;
580             my $self = $X->($X);
581              
582             my $xml = $self->{_xml} or return $X;
583              
584             return ref $xml && reftype $xml eq 'ARRAY'
585             ? $new_X->( {_xml => $xml->[-1]} )
586             : $new_X->( {_xml => $xml} );
587             };
588              
589             # prepend
590             #
591             # add nodes before first child
592             sub prepend
593             {
594             my($X) = shift;
595             my $self = $X->($X);
596              
597             my $xml = $self->{_xml} or return $X;
598              
599             my $child_nodes = $arg_to_nodes->(@_)
600             or return $X;
601              
602             my $first_child = shift @$child_nodes;
603              
604             $each->($xml, sub {
605             my $sel = shift or return;
606              
607             if ($sel->can('firstChild')) {
608             # insert first node before first child
609             my $insert_after = $sel->insertBefore(
610             $first_child->cloneNode(1),
611             $sel->firstChild,
612             ) or return;
613              
614             $each->($child_nodes, sub {
615             my $node = shift or return;
616              
617             return unless $insert_after
618             and $insert_after->can('insertAfter');
619              
620             $insert_after = $sel->insertAfter(
621             $node->cloneNode(1),
622             $insert_after
623             );
624             });
625             }
626             elsif ($self->can('addChild')) {
627             $sel->addChild( $first_child->cloneNode(1) );
628             $each->($child_nodes, sub {
629             my $node = shift or return;
630             });
631             }
632             else {
633             return;
634             }
635             });
636              
637             return $X;
638             }
639              
640             # removeClass
641             #
642             # remove class using $mod_class
643             sub removeClass { $mod_class->('remove', @_) }
644              
645             # text
646             #
647             # return text content
648             sub text
649             {
650             my($X, $value) = @_;
651             my $self = $X->($X);
652              
653             my $xml = $self->{_xml} or return $X;
654              
655             if (defined $value) {
656             $each->($xml, sub {
657             my $sel = shift or return;
658             # text replaces everything else so remove child nodes
659             # if they exist
660             $sel->removeChildNodes() if $sel->can('removeChildNodes');
661             # attempt different methods of adding text
662             # XML::LibXML::Element
663             if ($sel->can('appendText')) {
664             $sel->appendText($value);
665             }
666             # XML::LibXML::Text
667             elsif ($sel->can('setData')) {
668             $sel->setData($value);
669             }
670             # XML::LibXML::Node
671             elsif ($sel->can('appendChild')) {
672             $sel->appendChild( $sel->createTextNode($value) );
673             }
674             });
675             }
676             else {
677             my $sel = $first->($xml);
678             return $sel && $sel->can('textContent')
679             ? $sel->textContent
680             : undef;
681             }
682              
683             return $X;
684             }
685              
686             # trimText
687             sub trimText
688             {
689             my($X) = @_;
690              
691             my $text = $X->text;
692             return unless defined $text;
693              
694             $text =~ s{^\s*|\s*$}{}g;
695              
696             return $text;
697             }
698              
699             # toggleClass
700             #
701             # toggle class using $mod_class
702             sub toggleClass { $mod_class->('toggle', @_) }
703              
704             1;
705              
706             __END__