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   444787 use 5.008001;
  32         76  
4 32     32   116 use strict;
  32         44  
  32         596  
5 32     32   94 use warnings;
  32         38  
  32         775  
6 32     32   92 use Exporter qw(import);
  32         35  
  32         985  
7 32     32   103 use Scalar::Util qw/ blessed /;
  32         34  
  32         2549  
8 32     32   24812 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.06";
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             my @new = map { $_->cloneNode(1) } @{$self->{nodes}};
381             $self->new(\@new, $self);
382             }
383              
384             sub _node_matches {
385             my ($node, $xpath) = @_;
386             # warn sprintf "# matching node: %s (%s)\n", ref $node, $node;
387             foreach ($node->parentNode->findnodes($xpath)) {
388             # warn sprintf "# - against node: %s (%s)\n", ref $_, $_;
389             return 1 if $_->isSameNode($node);
390             }
391             0;
392             }
393              
394             # TODO add() can ruin our refcount for data()
395             sub add {
396             my ($self, $stuff, $context) = @_;
397             $context ||= $self->document;
398              
399             # find(): add(selector[, context])
400             # new: add(html), add(elements), add(jQuery)
401             my $new_selection = !ref $stuff && $stuff !~ /<(?!!).*?>/
402             ? $context->find($stuff)
403             : $self->new($stuff);
404              
405             # prepend our nodes
406             unshift @{$new_selection->{nodes}}, @{ $self->{nodes} };
407              
408             $new_selection;
409             }
410              
411             sub each {
412             my ($self, $cb) = @_;
413              
414             for (my $i = 0; $i < @{$self->{nodes}}; $i++) {
415              
416             local $_ = $self->{nodes}[$i];
417             my @rv = $cb->($i, $_);
418             last if @rv == 1 && !defined $rv[0];
419             }
420              
421             $self;
422             }
423              
424              
425             sub append {
426             my $self = shift;
427             _append_to($self->new(@_)->{nodes}, $self->{nodes});
428             $self;
429             }
430              
431             sub append_to {
432             my $self = shift;
433             _append_to($self->{nodes}, (ref $self)->new(@_)->{nodes});
434             $self;
435             }
436              
437             sub _append_to {
438             my ($content, $target) = @_;
439              
440             for (my $i = 0; $i < @$target; $i++) {
441              
442             my $is_last = $i == $#$target;
443             my $node = $target->[$i];
444              
445              
446             # thats because appendChild() is not supported on a Document node (as of XML::LibXML 2.0017)
447             if ($node->isa('XML::LibXML::Document')) {
448              
449             foreach (@$content) {
450             $node->hasChildNodes ? $node->lastChild->addSibling($is_last ? $_ : $_->cloneNode(1))
451             : $node->setDocumentElement($is_last ? $_ : $_->cloneNode(1));
452             }
453             }
454             else {
455             $node->appendChild($is_last ? $_ : $_->cloneNode(1))
456             for @$content;
457             }
458             }
459             }
460              
461              
462             sub prepend {
463             my $self = shift;
464             _prepend_to($self->new(@_)->{nodes}, $self->{nodes});
465             $self;
466             }
467              
468             sub prepend_to {
469             my $self = shift;
470             _prepend_to($self->{nodes}, (ref $self)->new(@_)->{nodes});
471             $self;
472             }
473              
474             sub _prepend_to {
475             my ($content, $target) = @_;
476              
477             for (my $i = 0; $i < @$target; $i++) {
478              
479             my $is_last = $i == $#$target;
480             my $node = $target->[$i];
481              
482             # thats because insertBefore() is not supported on a Document node (as of XML::LibXML 2.0017)
483             if ($node->isa('XML::LibXML::Document')) {
484              
485             foreach (@$content) {
486             $node->hasChildNodes ? $node->lastChild->addSibling($is_last ? $_ : $_->cloneNode(1))
487             : $node->setDocumentElement($is_last ? $_ : $_->cloneNode(1));
488             }
489              
490             # rotate
491             while (not $node->firstChild->isSameNode($content->[0])) {
492             my $first_node = $node->firstChild;
493             $first_node->unbindNode;
494             $node->lastChild->addSibling($first_node);
495              
496             }
497             }
498              
499             # insert before first child
500             my $first_child = $node->firstChild;
501             $node->insertBefore($is_last ? $_ : $_->cloneNode(1), $first_child || undef) for @$content;
502             }
503             }
504              
505              
506             sub before {
507             my $self = shift;
508             my $content = ref $_[0] eq 'CODE'
509             ? $_[0]
510             : [map { @{ $self->new($_)->{nodes} } } @_];
511              
512             $self->_insert_before($content, $self->{nodes});
513             $self;
514             }
515              
516             sub insert_before {
517             my ($self, $target) = @_;
518             $target = _is_selector($target) ? $self->document->find($target)
519             : (ref $self)->new($target);
520              
521             $self->_insert_before($self->{nodes}, $target->{nodes});
522             $self;
523             }
524              
525             sub _insert_before {
526             my ($self, $content, $target) = @_;
527             return if ref $content eq 'ARRAY' && @$content == 0;
528              
529             for (my $i = 0; $i < @$target; $i++) {
530              
531             my $is_last = $i == $#$target;
532             my $node = $target->[ $i ];
533             my $parent = $node->parentNode;
534             my $items;
535              
536             if (ref $content eq 'CODE') {
537             local $_ = $node;
538             $items = (ref $self)->new($content->($i, $_))->{nodes};
539             }
540             else {
541             # content is cloned except for last target
542             $items = $i == $#$target ? $content : [map { $_->cloneNode(1) } @$content];
543             }
544              
545             # thats because insertAfter() is not supported on a Document node (as of XML::LibXML 2.0017)
546             unless ($parent->isa('XML::LibXML::Document')) {
547              
548             $parent->insertBefore($_, $node) for @$items;
549             next;
550             }
551              
552             # workaround for when parent is document:
553             # append nodes then rotate until content is before node
554             $parent->lastChild->addSibling($_) for @$items;
555              
556             my $next = $node;
557             while (not $next->isSameNode($items->[0])) {
558             my $node_to_move = $next;
559             $next = $node_to_move->nextSibling;
560             $parent->lastChild->addSibling($node_to_move);
561             }
562             }
563             }
564              
565              
566             sub after {
567             my $self = shift;
568             my $content = ref $_[0] eq 'CODE'
569             ? $_[0]
570             : [map { @{ $self->new($_)->{nodes} } } @_];
571              
572             $self->_insert_after($content, $self->{nodes});
573             $self;
574             }
575              
576             sub insert_after {
577             my ($self, $target) = @_;
578             $target = _is_selector($target) ? $self->document->find($target)
579             : (ref $self)->new($target);
580              
581             $self->_insert_after($self->{nodes}, $target->{nodes});
582             $self;
583             }
584              
585             sub _insert_after {
586             my ($self, $content, $target) = @_;
587             return if ref $content eq 'ARRAY' && @$content == 0;
588              
589             for (my $i = 0; $i < @$target; $i++) {
590              
591             my $node = $target->[ $i ];
592             my $parent = $node->parentNode;
593             my $items;
594              
595             if (ref $content eq 'CODE') {
596             local $_ = $node;
597             $items = (ref $self)->new($content->($i, $_))->{nodes};
598             }
599             else {
600              
601             # content is cloned except for last target
602             $items = $i == $#$target ? $content : [map { $_->cloneNode(1) } @$content];
603             }
604              
605             # thats because insertAfter() is not supported on a Document node (as of XML::LibXML 2.0017)
606             unless ($parent->isa('XML::LibXML::Document')) {
607              
608             $parent->insertAfter($_, $node) for reverse @$items;
609             next;
610             }
611              
612             # workaround for when parent is document:
613             # append nodes then rotate next siblings until content is after node
614             $parent->lastChild->addSibling($_) for @$items;
615             # warn "# rotating until $items[0] is after to $node\n";
616             while (not $node->nextSibling->isSameNode($items->[0])) {
617             my $next = $node->nextSibling;
618             # warn "# - next: $next\n";
619             # $next->unbindNode;
620             $parent->lastChild->addSibling($next);
621             }
622             }
623             }
624              
625              
626             sub contents {
627             my $self = shift;
628             my @new = map { $_->childNodes } @{$self->{nodes}};
629             $self->new(\@new, $self);
630             }
631              
632             {
633             no warnings;
634             *detach = \&remove;
635             }
636              
637             sub remove {
638             my ($self, $selector) = @_;
639              
640             if ($selector) {
641             $self->find($selector)->remove;
642             return $self;
643             }
644              
645             foreach (@{$self->{nodes }}) {
646             # TODO test when there is no parent node
647             $_->parentNode->removeChild($_);
648             }
649              
650             $self;
651             }
652              
653              
654              
655             sub replace_with {
656             my ($self, $content) = @_;
657             $content = $self->new($content)->{nodes}
658             unless ref $content eq 'CODE';
659              
660             my $target = $self->{nodes};
661             for (my $i = 0; $i < @$target; $i++) {
662              
663             my $node = $target->[ $i ];
664             my $parent = $node->parentNode;
665             my $final_content = $content;
666              
667             if (ref $content eq 'CODE') {
668             local $_ = $self->new($node);
669             $final_content = $content->($i, $_); # TODO check this callback signature
670             $final_content = $self->new($final_content)->{nodes};
671             }
672              
673             # no content, just remove node
674             unless (@$final_content) {
675             $parent->removeChild($node);
676             delete $data->{$node->ownerDocument->unique_key}->{$node->unique_key};
677             return $self;
678             }
679              
680             # content is cloned except for last target
681             my @items = $i == $#$target ? @$final_content : map { $_->cloneNode(1) } @$final_content;
682              
683             # on doc: append then rotate
684             if ($parent->nodeType == XML_DOCUMENT_NODE) {
685              
686             $parent->lastChild->addSibling($_) for @items;
687             while (not $node->nextSibling->isSameNode($items[0])) {
688             $parent->lastChild->addSibling($node->nextSibling);
689             }
690              
691             $parent->removeChild($node);
692             }
693             else {
694             my $new_node = shift @items;
695             $parent->replaceChild($new_node, $node);
696             foreach (@items) {
697             $parent->insertAfter($_, $new_node);
698             $new_node = $_;
699             }
700             }
701              
702             }
703              
704             $self;
705             }
706              
707             sub attr {
708             my $self = shift;
709             my $attr_name = shift;
710              
711             return unless defined $attr_name;
712              
713             # only element nodes
714             my @nodes = map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () } @{$self->{nodes}};
715              
716             # get
717             return $nodes[0] ? $nodes[0]->getAttribute(lc $attr_name) : undef
718             unless @_ || ref $attr_name;
719              
720             # set
721             return $self unless @nodes;
722              
723             # set multiple
724             if (ref $attr_name eq 'HASH') {
725              
726             foreach (@nodes) {
727             while (my ($k, $v) = CORE::each %$attr_name) {
728             $_->setAttribute($k, $v);
729             }
730             }
731              
732             return $self;
733             }
734              
735             $attr_name = lc $attr_name;
736              
737             # from callback
738             if (ref $_[0] eq 'CODE') {
739              
740             for (my $i = 0; $i < @nodes; $i++) {
741              
742             local $_ = $nodes[$i];
743             my $value = $_[0]->($i, $_->getAttribute($attr_name));
744             $_->setAttribute($attr_name, $value)
745             if defined $value;
746             }
747             }
748             else {
749             $_->setAttribute($attr_name, $_[0])
750             for @nodes;
751             }
752              
753             $self;
754             }
755              
756             sub remove_attr {
757             my ($self, $attr_name) = @_;
758             return $self unless defined $attr_name;
759              
760             $attr_name =~ s/(?:^\s*|\s$)//g;
761              
762             foreach my $node (map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () } @{$self->{nodes}}) {
763             foreach my $attr (split /\s+/, $attr_name) {
764             $node->removeAttribute($attr);
765             }
766             }
767              
768             $self;
769             }
770              
771              
772             sub add_class {
773             my ($self, $class) = @_;
774              
775             # only element nodes
776             my @nodes = map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () } @{$self->{nodes}};
777              
778             for (my $i = 0; $i < @nodes; $i++) {
779              
780             my $node = $nodes[$i];
781             my $new_classes = $class;
782             my $current_class = $node->getAttribute('class') || '';
783              
784             # from callback
785             if (ref $class eq 'CODE') {
786             local $_ = $self->new($node);
787             $new_classes = $class->($i, $current_class);
788             }
789              
790             my %distinct;
791             $node->setAttribute('class', join ' ', grep { !$distinct{$_}++ } split(/\s+/, "$current_class $new_classes"));
792             }
793              
794             $self
795             }
796              
797             sub remove_class {
798             my ($self, $class) = @_;
799              
800             # only element nodes
801             my @nodes = map { $_->nodeType == XML_ELEMENT_NODE ? $_ : () } @{$self->{nodes}};
802              
803             for (my $i = 0; $i < @nodes; $i++) {
804              
805             my $node = $nodes[$i];
806              
807             # remove all classes
808             unless (defined $class) {
809             $node->removeAttribute('class');
810             next;
811             }
812              
813             my $to_remove = $class;
814             my $current_class = $node->getAttribute('class') || '';
815              
816             # from callback
817             if (ref $class eq 'CODE') {
818             local $_ = $self->new($node);
819             $to_remove = $class->($i, $current_class);
820             }
821              
822             my %to_remove = map { $_ => 1 } split /\s+/, $to_remove;
823             my @new_classes = grep { !$to_remove{$_} } split /\s+/, $current_class;
824              
825             @new_classes > 0 ? $node->setAttribute('class', join ' ', @new_classes)
826             : $node->removeAttribute('class');
827             }
828              
829             $self
830             }
831              
832             sub data {
833              
834             my $self = shift;
835              
836             # class method: return whole $data (mainly for test/debug)
837             return $data unless ref $self;
838              
839             # data(key, val)
840             if (@_ == 2 && defined $_[1]) {
841              
842             $data->{$_->ownerDocument->unique_key}->{$_->unique_key}->{$_[0]} = $_[1]
843             foreach @{$self->{nodes}};
844              
845             return $self;
846             }
847              
848              
849             if (@_ == 1) {
850              
851             # no nodes
852             return unless defined $self->{nodes}->[0];
853              
854             # data(undefined)
855             return $self unless defined $_[0];
856              
857             # data(obj)
858             if (ref $_[0]) {
859              
860             die 'data(obj) only accepts a hashref' unless ref $_[0] eq 'HASH';
861              
862             $data->{$_->ownerDocument->unique_key}->{$_->unique_key} = $_[0]
863             foreach @{$self->{nodes}};
864              
865             return $self;
866             }
867              
868             # data(key)
869             my $key = $_[0];
870             my $node = $self->{nodes}->[0];
871              
872             $data->{$node->ownerDocument->unique_key}->{$node->unique_key} = {}
873             unless $data->{$node->ownerDocument->unique_key}->{$node->unique_key};
874              
875             my $node_data = $data->{$node->ownerDocument->unique_key}->{$node->unique_key};
876              
877             # try to pull from data-* attribute
878             my $data_attr = 'data-'._decamelize($key);
879             $data_attr =~ tr/_/-/;
880              
881             $node_data->{$key} = _convert_data_attr_value($node->getAttribute($data_attr))
882             if !exists $node_data->{$key}
883             && $node->nodeType == XML_ELEMENT_NODE
884             && $node->hasAttribute($data_attr);
885              
886             return $node_data->{$key};
887             }
888              
889             # data()
890             if (@_ == 0) {
891              
892             # return all data for first node
893             my $node = $self->{nodes}[0];
894             return unless $node;
895              
896             # poor man's //= {} (for perls < 5.10)
897             exists $data->{$node->ownerDocument->unique_key}->{$node->unique_key}->{autovivify_hash};
898             my $node_data = $data->{$node->ownerDocument->unique_key}->{$node->unique_key};
899              
900             # pull data-* attributes
901             foreach my $attr (grep { $_->name =~ /^data-/ } $node->attributes) {
902              
903             my $key = substr($attr->name, 5);
904             $key =~ tr/-/_/;
905             $key = lcfirst _camelize($key);
906              
907             next if exists $node_data->{$key};
908             $node_data->{$key} = _convert_data_attr_value($attr->value);
909             }
910              
911             return $node_data;
912             }
913              
914             $self;
915             }
916              
917             sub _convert_data_attr_value {
918              
919             # number
920             return $_[0] += 0
921             if $_[0] =~ /^\d+$/;
922              
923             # json array or object
924             return decode_json($_[0])
925             if $_[0] =~ /^(?:\{|\[)/;
926              
927             # boolean
928             return JSON::true if $_[0] eq 'true';
929             return JSON::false if $_[0] eq 'false';
930              
931             # undef
932             return undef if $_[0] eq 'null' || $_[0] eq 'undefined';
933              
934             # other stuff, return unmodified
935             $_[0];
936             }
937              
938              
939              
940             sub _decamelize
941             {
942             my $s = shift;
943             $s =~ s{([^a-zA-Z]?)([A-Z]*)([A-Z])([a-z]?)}{
944             my $fc = pos($s)==0;
945             my ($p0,$p1,$p2,$p3) = ($1,lc$2,lc$3,$4);
946             my $t = $p0 || $fc ? $p0 : '_';
947             $t .= $p3 ? $p1 ? "${p1}_$p2$p3" : "$p2$p3" : "$p1$p2";
948             $t;
949             }ge;
950             $s;
951             }
952              
953             sub _camelize {
954             my $s = shift;
955             join('', map{ ucfirst $_ } split(/(?<=[A-Za-z])_(?=[A-Za-z])|\b/, $s));
956             }
957              
958             sub _is_selector {
959             defined $_[0]
960             && !ref $_[0]
961             && length $_[0]
962             && $_[0] !~ /<(?!!).*?>/
963             }
964              
965             # TODO rethink this autoload thing... issues:
966             # - global var is bad... spooky action at a distance;
967             # - and if thats ok, we could just add the subref to the symbol table directly
968              
969             sub AUTOLOAD {
970             my $self = shift;
971             our $AUTOLOAD;
972             (my $method = $AUTOLOAD) =~ s/.*:://s;
973              
974             die sprintf "unknown method '$method'"
975             unless ref $self && exists $fn{$method};
976              
977             local $_ = $self;
978             $fn{$method}{sub}->(@_);
979             }
980              
981              
982              
983             # decrement data ref counter, delete data when counter == 0
984             sub DESTROY {
985             my $self = shift;
986              
987             # decrement $data refcount
988             my $doc_id = $self->{document}->unique_key;
989             $data->{$doc_id}{refcount}--;
990             # printf STDERR "[%s] decremented document %d data ref count: %d\n", __PACKAGE__, $doc_id, $data->{$doc_id}{refcount};
991              
992             # delete document data if refcount is 0
993             delete $data->{$doc_id}
994             if $data->{$doc_id}{refcount} == 0;
995             }
996              
997              
998             # TODO create camelized methods alias
999              
1000              
1001              
1002             1;
1003             __END__