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