File Coverage

blib/lib/XML/LibXML/jQuery.pm
Criterion Covered Total %
statement 15 17 88.2
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 21 23 91.3


line stmt bran cond sub pod time code
1             package XML::LibXML::jQuery;
2              
3 32     32   454979 use 5.008001;
  32         89  
4 32     32   125 use strict;
  32         96  
  32         622  
5 32     32   98 use warnings;
  32         38  
  32         796  
6 32     32   97 use Exporter qw(import);
  32         37  
  32         969  
7 32     32   100 use Scalar::Util qw/ blessed /;
  32         33  
  32         2657  
8 32     32   25844 use XML::LibXML;
  0            
  0            
9             use HTML::Selector::XPath qw/selector_to_xpath/;
10             use Carp qw/ confess /;
11             use JSON qw/ decode_json /;
12              
13             our $VERSION = "0.08";
14              
15             our @EXPORT_OK = qw/ j fn /;
16             our @EXPORT = qw/ j /;
17              
18             use constant {
19             XML_ELEMENT_NODE => 1,
20             XML_TEXT_NODE => 3,
21             XML_COMMENT_NODE => 8,
22             XML_DOCUMENT_NODE => 9,
23             XML_DOCUMENT_FRAG_NODE => 11,
24             XML_HTML_DOCUMENT_NODE => 13
25             };
26              
27             our ($PARSER);
28              
29             # plugin functions
30             my %fn;
31              
32             # for data()
33             my $data = {};
34              
35             sub fn($$) {
36             my ($name, $sub) = @_;
37              
38             die sprintf("fn '$name' already defined by %s (at %s line %s)", @{$fn{$name}->{caller}})
39             if exists $fn{$name};
40              
41             $fn{$name} = {
42             sub => $sub,
43             caller => [caller]
44             };
45             }
46              
47              
48              
49             #*j = \&jQuery;
50             sub j {
51             __PACKAGE__->new(@_);
52             }
53              
54              
55             sub new {
56             my ($class, $stuff, $before) = @_;
57             my ($self, $document, $nodes);
58              
59             # instance method, reuse document
60             if (ref $class) {
61             $self = $class;
62             $class = ref $self;
63             $document = $self->{document};
64             }
65              
66             if (defined $stuff) {
67              
68             $nodes = _stuff_to_nodes($stuff);
69              
70             # catch bugs :)
71             # confess "undefined node" if grep { !defined } @$nodes;
72              
73             # adopt nodes to existing document
74             # - if its not in the same dorcument already
75             # - if its not a document node
76             # - testing only first node for better performance
77             if (defined $document
78             && defined $nodes->[0]
79             && $nodes->[0] ->nodeType != XML_DOCUMENT_NODE
80             && !$nodes->[0]->ownerDocument->isSameNode($document)) {
81              
82             # my $doc_id = $existing_document->unique_key;
83             foreach my $n (@$nodes) {
84             $document->adoptNode($n);
85             }
86             }
87             }
88              
89             # resolve document
90             unless (defined $document) {
91              
92             $document = defined $nodes->[0] ? $nodes->[0]->ownerDocument
93             : XML::LibXML->createDocument;
94             }
95              
96             # increment document data refcount
97             my $doc_id = $document->unique_key;
98             $data->{$doc_id}{refcount}++;
99             # printf STDERR "[%s] incremented document %d data ref count: %d\n", __PACKAGE__, $doc_id, $data->{$doc_id}{refcount};
100              
101             bless {
102             document => $document,
103             document_id => $doc_id,
104             nodes => $nodes,
105             before => $before
106             }, $class;
107             }
108              
109             # faster instantiation for new nodes of the same document
110             sub _new_nodes {
111             my ($self, $nodes, $before, $new_document) = @_;
112              
113             my $doc_id = $new_document ? $new_document->unique_key
114             : $self->{document_id};
115              
116             $data->{$doc_id}{refcount}++;
117              
118             bless {
119             document => $new_document || $self->{document},
120             document_id => $doc_id,
121             nodes => $nodes,
122             before => $before
123             }, ref $self;
124             }
125              
126             sub _stuff_to_nodes {
127              
128             my $reftype = ref $_[0];
129             my $nodes;
130              
131             # string
132             if (not $reftype) {
133              
134             # parse as xml
135             if ($_[0] =~ /^\s*<\?xml/) {
136              
137             $nodes = [ $PARSER->parse_string($_[0]) ];
138             # parse as html
139             } else {
140             $nodes = _parse_html($_[0]);
141             }
142             }
143              
144             # arrayref
145             elsif ($reftype eq 'ARRAY') {
146              
147             $nodes = $_[0];
148             }
149              
150             # object
151             elsif (blessed $_[0]) {
152              
153             if ($_[0]->isa(__PACKAGE__)) {
154             $nodes = $_[0]->{nodes};
155             }
156             # TODO this is too restrictive.. what about text, comment, other nodes?
157             elsif ($_[0]->isa('XML::LibXML::Element')) {
158             $nodes = [$_[0]];
159             }
160             else {
161             confess "Can't handle this type of object: '$reftype'";
162             }
163             }
164             else {
165              
166             confess "Can't handle this type of data: '$reftype'";
167             }
168              
169             $nodes;
170             }
171              
172             sub _parse_html {
173             my $source = $_[0];
174              
175             if (!$PARSER){
176             $PARSER = XML::LibXML->new();
177             $PARSER->recover(1);
178             $PARSER->recover_silently(1);
179             $PARSER->keep_blanks(0);
180             $PARSER->expand_entities(1);
181             $PARSER->no_network(1);
182             # local $XML::LibXML::skipXMLDeclaration = 0;
183             # local $XML::LibXML::skipDTD = 0;
184             }
185              
186             my $dom = $PARSER->parse_html_string($source);
187             my @nodes;
188              
189              
190             # full html
191             if ($source =~ /
192             @nodes = $dom->getDocumentElement;
193             }
194             # html fragment
195             elsif ($source =~ /<(?!!).*?>/) { # < not followed by ! then stuff until > (match a html tag)
196             @nodes = map { $_->childNodes } $dom->findnodes('/html/head | /html/body');
197             }
198             # plain text
199             else {
200             $dom->removeInternalSubset;
201             @nodes = $dom->exists('//p') ? $dom->findnodes('/html/body/p')->pop->childNodes : $dom->childNodes;
202             }
203              
204             confess "empy nodes :[" unless @nodes;
205             confess "undefined node :[" if grep { ! defined } @nodes;
206             # new doc (setDocumentElement accepts only element nodes)
207             if ($nodes[0]->nodeType == XML_ELEMENT_NODE) {
208             my $doc = XML::LibXML->createDocument;
209             if ($source =~ /^\s*ownerDocument->internalSubset)) {
210             $doc->createInternalSubset( $dtd->getName, $dtd->publicId, $dtd->systemId );
211             }
212             $doc->setDocumentElement($nodes[0]);
213             $nodes[0]->addSibling($_) foreach @nodes[1..$#nodes];
214             }
215              
216             \@nodes;
217             }
218              
219              
220             sub get {
221             my ($self, $i) = @_;
222             $self->{nodes}->[$i];
223             }
224              
225             sub eq {
226             my ($self, $i) = @_;
227             $self->_new_nodes([ $self->{nodes}[$i] || () ], $self);
228             }
229              
230              
231             sub end {
232             shift->{before};
233             }
234              
235             sub document {
236             my $self = shift;
237             $self->_new_nodes([ $self->{document} ], $self);
238             }
239              
240             sub tagname {
241             my $self = shift;
242             defined $self->{nodes}[0]
243             ? $self->{nodes}[0]->localname
244             : undef;
245             }
246              
247             sub first {
248             my $self = shift;
249             $self->_new_nodes([ $self->{nodes}[0] || () ], $self);
250             }
251              
252             sub last {
253             my $self = shift;
254             $self->_new_nodes([ $self->{nodes}[-1] || () ], $self);
255             }
256              
257             sub serialize {
258             my ($self) = @_;
259             my $output = '';
260              
261             $output .= $_->serialize
262             for (@{$self->{nodes}});
263              
264             $output;
265             }
266              
267              
268             sub as_html {
269             my ($self) = @_;
270              
271             my $output = '';
272              
273             foreach (@{$self->{nodes}}) {
274              
275             # TODO benchmark as_html() using can() vs nodeType to detect document nodes
276             # best method, but only document nodes can toStringHTML()
277             if ($_->can('toStringHTML')) {
278             # printf STDERR "%s: toStringHTML\n", ref $_;
279             $output .= $_->toStringHTML;
280             next;
281             }
282              
283              
284             # second best is to call toStringC14N(1), which generates valid HTML (eg. no auto closed
),
285             # but dies on some cases with "Failed to convert doc to string in doc->toStringC14N" error.
286             # so we fallback to toString()
287             # the most common case where toStringC14N fails is unbound nodes (getOwner returns a DocumentFragment)
288             {
289             local $@; # protect existing $@
290             my $html = eval { $_->toStringC14N(1) };
291             # printf STDERR "%s: %s\n", ref $_->getOwner, ($@ ? "toString: $@" : 'toStringC14N');
292             $output .= $@ ? $_->toString : $html;
293             }
294             }
295              
296             $output;
297             }
298              
299             sub html {
300             my ($self, $stuff) = @_;
301              
302             # output
303             unless ($stuff) {
304             my $out = '';
305             foreach my $node (map { $_->childNodes } @{$self->{nodes}}) {
306             {
307             local $@;
308             my $html = eval { $node->toStringC14N(1) };
309             $out .= $@ ? $node->toString : $html;
310             }
311             }
312             return $out;
313             }
314              
315             # replace content
316             my $nodes = $self->new($stuff)->{nodes};
317              
318             foreach my $node (@{$self->{nodes}}) {
319             $node->removeChildNodes;
320             $node->appendChild($_->cloneNode(1)) for @$nodes;
321             }
322              
323             $self;
324             }
325              
326             sub text {
327             my ($self, $stuff) = @_;
328              
329             # output
330             unless (defined $stuff) {
331             my $out = '';
332             $out .= $_->textContent for @{$self->{nodes}};
333             return $out;
334             }
335              
336             # replace content
337             return $self unless @{$self->{nodes}};
338              
339             my $textnode = $self->{nodes}[0]->ownerDocument->createTextNode($stuff);
340              
341             foreach my $node (@{$self->{nodes}}) {
342             $node->removeChildNodes;
343             $node->appendChild($textnode->cloneNode(1));
344             }
345              
346             $self;
347             }
348              
349              
350             sub size {
351             my ($self) = @_;
352             scalar @{$self->{nodes}};
353             }
354              
355             sub children {
356             my ($self, $selector) = @_;
357              
358             my $xpath = selector_to_xpath($selector, root => '.')
359             if $selector;
360              
361             my @new = map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () }
362             map { $xpath ? $_->findnodes($xpath) : $_->childNodes }
363             @{$self->{nodes}};
364              
365             $self->_new_nodes(\@new, $self);
366             }
367              
368             sub find {
369             my ($self, $selector) = @_;
370              
371             my $xpath = selector_to_xpath($selector, root => './');
372             my @new = map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () }
373             map { $_->findnodes($xpath) }
374             @{$self->{nodes}};
375              
376             $self->_new_nodes(\@new, $self);
377             }
378              
379             sub xfind {
380             my ($self, $xpath) = @_;
381             my @new = map { $_->findnodes($xpath) } @{$self->{nodes}};
382             $self->_new_nodes(\@new, $self);
383             }
384              
385             sub filter {
386             my ($self, $selector) = @_;
387              
388             my $xpath = selector_to_xpath($selector, root => '.');
389             my @new = map { _node_matches($_, $xpath) ? $_ : () }
390             map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () } @{$self->{nodes}};
391              
392             $self->_new_nodes(\@new, $self);
393             }
394              
395             sub xfilter {
396             my ($self, $xpath) = @_;
397              
398             my @new = map { _node_matches($_, $xpath) ? $_ : () }
399             map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () } @{$self->{nodes}};
400              
401             $self->_new_nodes(\@new, $self);
402             }
403              
404             sub parent {
405             my ($self, $selector) = @_;
406              
407             my $xpath = selector_to_xpath($selector, root => '.')
408             if $selector;
409              
410             my @new = map {
411              
412             !$xpath ? $_
413             : _node_matches($_, $xpath) ? $_ : ()
414             }
415             grep { defined }
416             map { $_->parentNode } @{$self->{nodes}};
417              
418             $self->_new_nodes(\@new, $self);
419             }
420              
421             sub clone {
422             my ($self) = @_;
423             return $self unless @{$self->{nodes}};
424              
425             my @clones = map { $_->cloneNode(1) } @{$self->{nodes}};
426             # when cloning a document node, pass it as the new jQuery document (3rd arg)
427             $self->_new_nodes(\@clones, $self, $clones[0]->nodeType == XML_DOCUMENT_NODE ? $clones[0] : () );
428             }
429              
430             sub _node_matches {
431             my ($node, $xpath) = @_;
432             # warn sprintf "# matching node: %s (%s)\n", ref $node, $node;
433             foreach ($node->parentNode->findnodes($xpath)) {
434             # warn sprintf "# - against node: %s (%s)\n", ref $_, $_;
435             return 1 if $_->isSameNode($node);
436             }
437             0;
438             }
439              
440             # TODO add() can ruin our refcount for data()
441             sub add {
442             my ($self, $stuff, $context) = @_;
443             $context ||= $self->document;
444              
445             # find(): add(selector[, context])
446             # new: add(html), add(elements), add(jQuery)
447             my $new_selection = !ref $stuff && $stuff !~ /<(?!!).*?>/
448             ? $context->find($stuff)
449             : $self->new($stuff);
450              
451             # prepend our nodes
452             unshift @{$new_selection->{nodes}}, @{ $self->{nodes} };
453              
454             $new_selection;
455             }
456              
457             sub each {
458             my ($self, $cb) = @_;
459              
460             for (my $i = 0; $i < @{$self->{nodes}}; $i++) {
461              
462             local $_ = $self->{nodes}[$i];
463             my @rv = $cb->($i, $_);
464             last if @rv == 1 && !defined $rv[0];
465             }
466              
467             $self;
468             }
469              
470              
471             sub append {
472             my $self = shift;
473             my $nodes = _stuff_to_nodes($_[0]);
474             _append_to($nodes, $self->{nodes});
475             $self;
476             }
477              
478             sub append_to {
479             my $self = shift;
480             my $nodes = _stuff_to_nodes($_[0]);
481             _append_to($self->{nodes}, $nodes);
482             $self;
483             }
484              
485             sub _append_to {
486             my ($content, $target) = @_;
487              
488             for (my $i = 0; $i < @$target; $i++) {
489              
490             my $is_last = $i == $#$target;
491             my $node = $target->[$i];
492              
493              
494             # thats because appendChild() is not supported on a Document node (as of XML::LibXML 2.0017)
495             if ($node->isa('XML::LibXML::Document')) {
496              
497             foreach (@$content) {
498             confess "# Document->setDocumentElement: doc\n"
499             if ref $_ eq 'XML::LibXML::Document';
500              
501             $node->hasChildNodes ? $node->lastChild->addSibling($is_last ? $_ : $_->cloneNode(1))
502             : $node->setDocumentElement($is_last ? $_ : $_->cloneNode(1));
503             }
504             }
505             else {
506             $node->appendChild($is_last ? $_ : $_->cloneNode(1))
507             for @$content;
508             }
509             }
510             }
511              
512              
513             sub prepend {
514             my $self = shift;
515             _prepend_to($self->new(@_)->{nodes}, $self->{nodes});
516             $self;
517             }
518              
519             sub prepend_to {
520             my $self = shift;
521             _prepend_to($self->{nodes}, (ref $self)->new(@_)->{nodes});
522             $self;
523             }
524              
525             sub _prepend_to {
526             my ($content, $target) = @_;
527              
528             for (my $i = 0; $i < @$target; $i++) {
529              
530             my $is_last = $i == $#$target;
531             my $node = $target->[$i];
532              
533             # thats because insertBefore() is not supported on a Document node (as of XML::LibXML 2.0017)
534             if ($node->isa('XML::LibXML::Document')) {
535              
536             foreach (@$content) {
537             $node->hasChildNodes ? $node->lastChild->addSibling($is_last ? $_ : $_->cloneNode(1))
538             : $node->setDocumentElement($is_last ? $_ : $_->cloneNode(1));
539             }
540              
541             # rotate
542             while (not $node->firstChild->isSameNode($content->[0])) {
543             my $first_node = $node->firstChild;
544             $first_node->unbindNode;
545             $node->lastChild->addSibling($first_node);
546              
547             }
548             }
549              
550             # insert before first child
551             my $first_child = $node->firstChild;
552             $node->insertBefore($is_last ? $_ : $_->cloneNode(1), $first_child || undef) for @$content;
553             }
554             }
555              
556              
557             sub before {
558             my $self = shift;
559             my $content = ref $_[0] eq 'CODE'
560             ? $_[0]
561             : [map { @{ $self->new($_)->{nodes} } } @_];
562              
563             $self->_insert_before($content, $self->{nodes});
564             $self;
565             }
566              
567             sub insert_before {
568             my ($self, $target) = @_;
569             $target = _is_selector($target) ? $self->document->find($target)
570             : (ref $self)->new($target);
571              
572             $self->_insert_before($self->{nodes}, $target->{nodes});
573             $self;
574             }
575              
576             sub _insert_before {
577             my ($self, $content, $target) = @_;
578             return if ref $content eq 'ARRAY' && @$content == 0;
579              
580             for (my $i = 0; $i < @$target; $i++) {
581              
582             my $is_last = $i == $#$target;
583             my $node = $target->[ $i ];
584             my $parent = $node->parentNode;
585             my $items;
586              
587             if (ref $content eq 'CODE') {
588             local $_ = $node;
589             $items = (ref $self)->new($content->($i, $_))->{nodes};
590             }
591             else {
592             # content is cloned except for last target
593             $items = $i == $#$target ? $content : [map { $_->cloneNode(1) } @$content];
594             }
595              
596             # thats because insertAfter() is not supported on a Document node (as of XML::LibXML 2.0017)
597             unless ($parent->isa('XML::LibXML::Document')) {
598              
599             $parent->insertBefore($_, $node) for @$items;
600             next;
601             }
602              
603             # workaround for when parent is document:
604             # append nodes then rotate until content is before node
605             $parent->lastChild->addSibling($_) for @$items;
606              
607             my $next = $node;
608             while (not $next->isSameNode($items->[0])) {
609             my $node_to_move = $next;
610             $next = $node_to_move->nextSibling;
611             $parent->lastChild->addSibling($node_to_move);
612             }
613             }
614             }
615              
616              
617             sub after {
618             my $self = shift;
619             my $content = ref $_[0] eq 'CODE'
620             ? $_[0]
621             : [map { @{ $self->new($_)->{nodes} } } @_];
622              
623             $self->_insert_after($content, $self->{nodes});
624             $self;
625             }
626              
627             sub insert_after {
628             my ($self, $target) = @_;
629             $target = _is_selector($target) ? $self->document->find($target)
630             : (ref $self)->new($target);
631              
632             $self->_insert_after($self->{nodes}, $target->{nodes});
633             $self;
634             }
635              
636             sub _insert_after {
637             my ($self, $content, $target) = @_;
638             return if ref $content eq 'ARRAY' && @$content == 0;
639              
640             for (my $i = 0; $i < @$target; $i++) {
641              
642             my $node = $target->[ $i ];
643             my $parent = $node->parentNode;
644             my $items;
645              
646             if (ref $content eq 'CODE') {
647             local $_ = $node;
648             $items = (ref $self)->new($content->($i, $_))->{nodes};
649             }
650             else {
651              
652             # content is cloned except for last target
653             $items = $i == $#$target ? $content : [map { $_->cloneNode(1) } @$content];
654             }
655              
656             # thats because insertAfter() is not supported on a Document node (as of XML::LibXML 2.0017)
657             unless ($parent->isa('XML::LibXML::Document')) {
658              
659             $parent->insertAfter($_, $node) for reverse @$items;
660             next;
661             }
662              
663             # workaround for when parent is document:
664             # append nodes then rotate next siblings until content is after node
665             $parent->lastChild->addSibling($_) for @$items;
666             # warn "# rotating until $items[0] is after to $node\n";
667             while (not $node->nextSibling->isSameNode($items->[0])) {
668             my $next = $node->nextSibling;
669             # warn "# - next: $next\n";
670             # $next->unbindNode;
671             $parent->lastChild->addSibling($next);
672             }
673             }
674             }
675              
676              
677             sub contents {
678             my $self = shift;
679             my @new = map { $_->childNodes } @{$self->{nodes}};
680             $self->_new_nodes(\@new, $self);
681             }
682              
683             {
684             no warnings;
685             *detach = \&remove;
686             }
687              
688             sub remove {
689             my ($self, $selector) = @_;
690              
691             if ($selector) {
692             $self->find($selector)->remove;
693             return $self;
694             }
695              
696             foreach (@{$self->{nodes}}) {
697             # TODO test when there is no parent node
698             $_->parentNode->removeChild($_);
699             }
700              
701             $self;
702             }
703              
704              
705              
706             sub replace_with {
707             my ($self, $content) = @_;
708             $content = $self->new($content)->{nodes}
709             unless ref $content eq 'CODE';
710              
711             my $target = $self->{nodes};
712             for (my $i = 0; $i < @$target; $i++) {
713              
714             my $node = $target->[ $i ];
715             my $parent = $node->parentNode;
716             my $final_content = $content;
717              
718             if (ref $content eq 'CODE') {
719             local $_ = $self->new($node);
720             $final_content = $content->($i, $_); # TODO check this callback signature
721             $final_content = $self->new($final_content)->{nodes};
722             }
723              
724             # no content, just remove node
725             unless (@$final_content) {
726             $parent->removeChild($node);
727             delete $data->{$node->ownerDocument->unique_key}->{$node->unique_key};
728             return $self;
729             }
730              
731             # content is cloned except for last target
732             my @items = $i == $#$target ? @$final_content : map { $_->cloneNode(1) } @$final_content;
733              
734             # on doc: append then rotate
735             if ($parent->nodeType == XML_DOCUMENT_NODE) {
736              
737             $parent->lastChild->addSibling($_) for @items;
738             while (not $node->nextSibling->isSameNode($items[0])) {
739             $parent->lastChild->addSibling($node->nextSibling);
740             }
741              
742             $parent->removeChild($node);
743             }
744             else {
745             # my $new_node = shift @items;
746             # $parent->replaceChild($new_node, $node);
747             foreach (reverse @items) {
748             $parent->insertAfter($_, $node);
749             # $new_node = $_;
750             }
751             $parent->removeChild($node);
752             }
753              
754             }
755              
756             $self;
757             }
758              
759             sub attr {
760             my $self = shift;
761             my $attr_name = shift;
762              
763             return unless defined $attr_name;
764              
765             # only element nodes
766             my @nodes = @{$self->{nodes}};
767              
768             # get
769             return $nodes[0] ? $nodes[0]->getAttribute(lc $attr_name) : undef
770             unless @_ || ref $attr_name;
771              
772             # set
773             return $self unless @nodes;
774              
775             # set multiple
776             if (ref $attr_name eq 'HASH') {
777              
778             foreach (@nodes) {
779             while (my ($k, $v) = CORE::each %$attr_name) {
780             $_->setAttribute($k, $v);
781             }
782             }
783              
784             return $self;
785             }
786              
787             $attr_name = lc $attr_name;
788              
789             # from callback
790             if (ref $_[0] eq 'CODE') {
791              
792             for (my $i = 0; $i < @nodes; $i++) {
793              
794             local $_ = $nodes[$i];
795             my $value = $_[0]->($i, $_->getAttribute($attr_name));
796             $_->setAttribute($attr_name, $value)
797             if defined $value;
798             }
799             }
800             else {
801             $_->setAttribute($attr_name, $_[0])
802             for @nodes;
803             }
804              
805             $self;
806             }
807              
808             sub remove_attr {
809             my ($self, $attr_name) = @_;
810             return $self unless defined $attr_name;
811              
812             $attr_name =~ s/(?:^\s*|\s$)//g;
813              
814             foreach my $node (map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () } @{$self->{nodes}}) {
815             foreach my $attr (split /\s+/, $attr_name) {
816             $node->removeAttribute($attr);
817             }
818             }
819              
820             $self;
821             }
822              
823              
824             sub add_class {
825             my ($self, $class) = @_;
826              
827             # only element nodes
828             my @nodes = map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () } @{$self->{nodes}};
829              
830             for (my $i = 0; $i < @nodes; $i++) {
831              
832             my $node = $nodes[$i];
833             my $new_classes = $class;
834             my $current_class = $node->getAttribute('class') || '';
835              
836             # from callback
837             if (ref $class eq 'CODE') {
838             local $_ = $self->new($node);
839             $new_classes = $class->($i, $current_class);
840             }
841              
842             my %distinct;
843             $node->setAttribute('class', join ' ', grep { !$distinct{$_}++ } split(/\s+/, "$current_class $new_classes"));
844             }
845              
846             $self
847             }
848              
849             sub remove_class {
850             my ($self, $class) = @_;
851              
852             # only element nodes
853             my @nodes = map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () } @{$self->{nodes}};
854              
855             for (my $i = 0; $i < @nodes; $i++) {
856              
857             my $node = $nodes[$i];
858              
859             # remove all classes
860             unless (defined $class) {
861             $node->removeAttribute('class');
862             next;
863             }
864              
865             my $to_remove = $class;
866             my $current_class = $node->getAttribute('class') || '';
867              
868             # from callback
869             if (ref $class eq 'CODE') {
870             local $_ = $self->new($node);
871             $to_remove = $class->($i, $current_class);
872             }
873              
874             my %to_remove = map { $_ => 1 } split /\s+/, $to_remove;
875             my @new_classes = grep { !$to_remove{$_} } split /\s+/, $current_class;
876              
877             @new_classes > 0 ? $node->setAttribute('class', join ' ', @new_classes)
878             : $node->removeAttribute('class');
879             }
880              
881             $self
882             }
883              
884             sub data {
885              
886             my $self = shift;
887              
888             # class method: return whole $data (mainly for test/debug)
889             return $data unless ref $self;
890              
891             # data(key, val)
892             if (@_ == 2 && defined $_[1]) {
893              
894             $data->{$_->ownerDocument->unique_key}->{$_->unique_key}->{$_[0]} = $_[1]
895             foreach @{$self->{nodes}};
896              
897             return $self;
898             }
899              
900              
901             if (@_ == 1) {
902              
903             # no nodes
904             return unless defined $self->{nodes}->[0];
905              
906             # data(undefined)
907             return $self unless defined $_[0];
908              
909             # data(obj)
910             if (ref $_[0]) {
911              
912             die 'data(obj) only accepts a hashref' unless ref $_[0] eq 'HASH';
913              
914             $data->{$_->ownerDocument->unique_key}->{$_->unique_key} = $_[0]
915             foreach @{$self->{nodes}};
916              
917             return $self;
918             }
919              
920             # data(key)
921             my $key = $_[0];
922             my $node = $self->{nodes}->[0];
923              
924             $data->{$node->ownerDocument->unique_key}->{$node->unique_key} = {}
925             unless $data->{$node->ownerDocument->unique_key}->{$node->unique_key};
926              
927             my $node_data = $data->{$node->ownerDocument->unique_key}->{$node->unique_key};
928              
929             # try to pull from data-* attribute
930             my $data_attr = 'data-'._decamelize($key);
931             $data_attr =~ tr/_/-/;
932              
933             $node_data->{$key} = _convert_data_attr_value($node->getAttribute($data_attr))
934             if !exists $node_data->{$key}
935             && $node->nodeType == XML_ELEMENT_NODE
936             && $node->hasAttribute($data_attr);
937              
938             return $node_data->{$key};
939             }
940              
941             # data()
942             if (@_ == 0) {
943              
944             # return all data for first node
945             my $node = $self->{nodes}[0];
946             return unless $node;
947              
948             # poor man's //= {} (for perls < 5.10)
949             exists $data->{$node->ownerDocument->unique_key}->{$node->unique_key}->{autovivify_hash};
950             my $node_data = $data->{$node->ownerDocument->unique_key}->{$node->unique_key};
951              
952             # pull data-* attributes
953             foreach my $attr (grep { $_->name =~ /^data-/ } $node->attributes) {
954              
955             my $key = substr($attr->name, 5);
956             $key =~ tr/-/_/;
957             $key = lcfirst _camelize($key);
958              
959             next if exists $node_data->{$key};
960             $node_data->{$key} = _convert_data_attr_value($attr->value);
961             }
962              
963             return $node_data;
964             }
965              
966             $self;
967             }
968              
969             sub _convert_data_attr_value {
970              
971             # number
972             return $_[0] += 0
973             if $_[0] =~ /^\d+$/;
974              
975             # json array or object
976             return decode_json($_[0])
977             if $_[0] =~ /^(?:\{|\[)/;
978              
979             # boolean
980             return JSON::true if $_[0] eq 'true';
981             return JSON::false if $_[0] eq 'false';
982              
983             # undef
984             return undef if $_[0] eq 'null' || $_[0] eq 'undefined';
985              
986             # other stuff, return unmodified
987             $_[0];
988             }
989              
990              
991              
992             sub _decamelize {
993             my $s = shift;
994             $s =~ s{([^a-zA-Z]?)([A-Z]*)([A-Z])([a-z]?)}{
995             my $fc = pos($s)==0;
996             my ($p0,$p1,$p2,$p3) = ($1,lc$2,lc$3,$4);
997             my $t = $p0 || $fc ? $p0 : '_';
998             $t .= $p3 ? $p1 ? "${p1}_$p2$p3" : "$p2$p3" : "$p1$p2";
999             $t;
1000             }ge;
1001             $s;
1002             }
1003              
1004             sub _camelize {
1005             my $s = shift;
1006             join('', map{ ucfirst $_ } split(/(?<=[A-Za-z])_(?=[A-Za-z])|\b/, $s));
1007             }
1008              
1009             sub _is_selector {
1010             defined $_[0]
1011             && !ref $_[0]
1012             && length $_[0]
1013             && $_[0] !~ /<(?!!).*?>/
1014             }
1015              
1016             # TODO rethink this autoload thing... issues:
1017             # - global var is bad... spooky action at a distance;
1018             # - and if thats ok, we could just add the subref to the symbol table directly
1019              
1020             sub AUTOLOAD {
1021             my $self = shift;
1022             our $AUTOLOAD;
1023             (my $method = $AUTOLOAD) =~ s/.*:://s;
1024              
1025             die sprintf "unknown method '$method'"
1026             unless ref $self && exists $fn{$method};
1027              
1028             local $_ = $self;
1029             $fn{$method}{sub}->(@_);
1030             }
1031              
1032              
1033              
1034             # decrement data ref counter, delete data when counter == 0
1035             sub DESTROY {
1036             my $self = shift;
1037              
1038             # Don't know why, but document is undefined in some situations..
1039             # wiped out by XS code probably.
1040             return unless defined $self->{document_id};
1041              
1042             # decrement $data refcount
1043             my $doc_id = $self->{document_id};
1044             $data->{$doc_id}{refcount}--;
1045             # printf STDERR "[%s] decremented document %d data ref count: %d\n", __PACKAGE__, $doc_id, $data->{$doc_id}{refcount};
1046              
1047             # delete document data if refcount is 0
1048             delete $data->{$doc_id}
1049             if $data->{$doc_id}{refcount} == 0;
1050             }
1051              
1052              
1053             # TODO create camelized methods alias
1054              
1055              
1056              
1057             1;
1058             __END__