File Coverage

blib/lib/jQuery.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package jQuery;
2              
3 25     25   58716 use warnings;
  25         42  
  25         821  
4 25     25   129 use strict;
  25         36  
  25         919  
5 25     25   49278 use XML::LibXML 1.70;
  0            
  0            
6             use HTML::Entities;
7             use Encode;
8             use Carp;
9              
10             use base qw/jQuery::Functions Exporter/;
11             our @EXPORT = qw(jquery jQuery this);
12              
13             use vars qw/$DOC $PARSER $VERSION/;
14             $VERSION = "0.004";
15              
16             my $obj_class = 'jQuery::Obj';
17             sub this { 'jQuery::Functions'->this(@_) }
18              
19             sub new {
20             my $class = shift;
21             my $string = shift;
22             my $parser;
23             my $doc;
24             if (!$PARSER){
25             $parser = XML::LibXML->new();
26             $parser->recover(1);
27             $parser->recover_silently(1);
28             $parser->keep_blanks(1);
29             $parser->expand_entities(1);
30             $parser->no_network(1);
31             local $XML::LibXML::skipXMLDeclaration = 1;
32             local $XML::LibXML::skipDTD = 1;
33             local $XML::LibXML::setTagCompression = 1;
34             $PARSER = $parser;
35             } else {
36             $parser = $PARSER;
37             }
38            
39             if ($string){
40             if ($string =~ /^http/){
41             $string = $class->get($string);
42             } elsif ($string =~ /^<\?xml/) {
43             $doc = $parser->parse_string($string);
44             } else {
45             $doc = $parser->parse_html_string($string);
46             }
47             $DOC = $doc;
48             }
49            
50             return bless({
51             document => $doc
52             }, __PACKAGE__);
53             }
54              
55             *jQuery = \&jquery;
56             sub jquery {
57             my ($selector, $context) = (shift,shift);
58             my $this;
59             my $c;
60            
61             if ( ref ($selector) eq __PACKAGE__ ){
62             $this->{document} = $selector->{document};
63             $selector = $context;
64             $context = shift;
65             } else {
66             $this->{document} = $DOC;
67             }
68            
69             if ($context){
70             if (@_){
71             $context = bless([$context,@_], ref $context);
72             }
73             return jQuery($context)->find($selector);
74             }
75            
76             if ( ref($selector) eq $obj_class ){
77             return $selector;
78             }
79            
80             bless($this, $obj_class);
81             $this->{nodes} = [];
82             return $this if !$selector;
83            
84             my $nodes;
85             if ( ref($selector) =~ /XML::/i ){
86             if ($selector->isa('ARRAY')){
87             $nodes = $selector;
88             } else {
89             $nodes = [$selector];
90             }
91             } elsif ( ref ($selector) eq 'ARRAY' ){
92             $nodes = $selector;
93             } elsif ($this->is_HTML($selector)) {
94             $nodes = $this->createNode($selector);
95             } else {
96             $nodes = $this->_find($selector);
97             ##another try to create html fragment
98             if (!$nodes->[0]){
99             $nodes = $this->createNode($selector);
100             }
101             }
102            
103             return $this->pushStack(@$nodes);
104             }
105              
106             ##something like pushStack in jquery
107             sub pushStack {
108             my $self = shift;
109             my @elements;
110            
111             return jQuery($self,@_) unless $self->isa('HASH') and
112             ref $self eq $obj_class;
113             @elements = ref $_[0] eq 'ARRAY'
114             ? @{$_[0]}
115             : @_;
116            
117             #save old object
118             $self->{prevObject} = $self->{nodes};
119            
120             #$self = bless ([@elements], $obj_class);
121             $self->{nodes} = \@elements;
122             return $self;
123             }
124              
125             sub toArray {
126             my $self = shift;
127             my @nodes;
128             if ($self->isa('ARRAY')) {
129             @nodes = @{$self};
130             } elsif ($self->isa('HASH')) {
131             @nodes = @{$self->{nodes}};
132             } else {
133             @nodes = ($self);
134             }
135            
136             return wantarray ?
137             @nodes
138             : \@nodes;
139             }
140              
141             sub getNodes {
142             my $self = shift;
143             my @nodes;
144             if ($self->isa('ARRAY')) {
145             @nodes = @{$self};
146             } elsif ($self->isa('HASH')) {
147             @nodes = @{$self->{nodes}};
148             } else {
149             @nodes = ($self);
150             }
151             return wantarray ? @nodes : bless(\@nodes, $obj_class);
152             }
153              
154              
155             sub _find {
156             my ($this, $selector, $context ) = @_;
157             $context ||= $this->document;
158             my @nodes;
159             eval {
160             if ($selector !~ m/\//){
161             $selector = $this->_translate_css_to_xpath($selector,$context->nodePath);
162             }
163             @nodes = $context->findnodes($selector) if $selector;
164             };
165            
166             if ($@){
167             croak $@;
168             }
169             return wantarray
170             ? @nodes
171             : \@nodes;
172             }
173              
174             #Should I use HTML::Selector::XPath and port some jquery selectors by hand ??
175             #it's well maintained
176             #this is faster but not easy to maintain and not quiet mature
177             sub _translate_css_to_xpath {
178             my $self = shift;
179             my $start_query = shift;
180             my $old_query = shift;
181             my $custompath = shift || '';
182             my @args;
183             $start_query =~ s/\((.*?),(.*?)\)/\($1$2\)/g;
184             my @queries = split(/\,/,$start_query);
185             my $this_query = 0;
186            
187             foreach my $query (@queries){
188             $query =~ s//,/g;
189             #remove all leading and ending whitespaces
190             $query =~ s/^\s+|\s+$//g;
191             $query =~ s/\s+/
192             ##add one whitespace at the beginning
193             $query = " ".$query;
194            
195             my $selector;
196             $selector = $old_query if $old_query;
197            
198             my $pos = 0;
199             my $directpath = 0;
200             my $empty_path =0;
201             my $single = 0;
202            
203             ##setting starting search path custom path is // which search down all elements
204             my $path = '//';
205             $path = $custompath if $custompath;
206            
207             ##I wrote this some while ago, I feel dizzy when I try to read it again
208             while ($query =~ /([\s|\.|\:|\#]?)((\[.*?\])|(~)|(\+)|(\>)|(\<)|(\*)|([\w\-]+(\(.*?\))?))/g){
209            
210             my $type = $1;
211             my $value = $2;
212             my $pos2 = 0;
213            
214             ##set empty starting path
215             if ($custompath eq 'empty' && $pos2 eq '0'){
216             $path = '';
217             ###if we want to search direct childrens
218             } elsif ($directpath || $empty_path){
219             $path = '/';
220             $path = '//' if $directpath eq '2';
221             $path = '' if $empty_path;
222             if (($type =~ /(\:|\.|\#)/ || $value =~ /\[.*?\]/)){
223             $selector .= $path.'*';
224             }
225            
226             #reset direct path
227             $directpath = 0;
228             ##reset empty path
229             $empty_path = 0;
230             } else {
231             $path = '//';
232             }
233            
234             $type =~ s/\s+//;
235             $value =~ s/\s+//;
236             if ($pos == 0){
237             $path = $custompath if $custompath && $custompath ne 'empty';
238             if (($type =~ /(\:|\.|\#)/ || $value =~ /\[.*?\]/)){
239             $selector .= $path.'*';
240             }
241             }
242            
243             if ($value eq '*'){
244             $selector .= $path.'*';
245             } elsif ($value eq '>'){
246             $directpath = 1;
247             } elsif ($value eq '<'){
248             $directpath = 2;
249             } elsif ($value eq '+'){
250             $selector .= '/following-sibling::';
251             $empty_path = 1;
252             $single = 1;
253             } elsif ($value eq '~'){
254             $selector .= '/following-sibling::';
255             $empty_path = 1;
256             } elsif ($value =~ /\[(.*?)\]/){
257            
258             my ($name, $value) = split(/\s*=\s*/, $1, 2);
259             if (defined $value) {
260             for ($value) {
261             s/^['"]//;
262             s/['"]$//;
263             }
264            
265             if ($name =~ s/\^$//){
266             $selector .= "[starts-with (\@$name,'$value')]";
267             } elsif ($name =~ s/\$$//){
268             #$selector .= "[ends-with (\@$name,'$value')]";
269             $selector .= "[substring(\@$name, string-length(\@$name) - string-length('$value')+ 1, string-length(\@$name))= '$value']";
270             } elsif ($name =~ s/\*$//){
271             $selector .= "[contains (\@$name,'$value')]";
272             } elsif ($name =~ s/\|$//){
273             $selector .= "[\@$name ='$value'"." or "."starts-with(\@$name,'$value".'-'."')]";
274             } else {
275             $selector .= "[\@$name='$value']";
276             }
277            
278             } else {
279             $selector .= "[\@$name]";
280             }
281            
282             ##this is tag
283             } elsif (!$type){
284             $selector .= $path.$value;
285             ##this is class
286             } elsif ($type eq "."){
287             ##finally found a good solution from
288             ##http://plasmasturm.org/log/444/
289             $selector .= '[ contains(concat( " ", @class, " " ),concat( " ", "'.$value.'", " " )) ]';
290            
291             ##id selector
292             } elsif ($type eq '#'){
293             $selector .= '[@id="'.$value.'"]';
294             }
295            
296             ###pseduo-class
297             elsif ($type eq ':'){
298            
299             if ($value eq "first-child"){
300             $selector .= '[1]';
301             } elsif ($value eq "first"){
302             #$selector = "getIndex($selector,'eq','0')";
303             $selector .= '[position() = 1]'; ##this doesn't really do the job exactly as jQuery
304             } elsif ($value eq "odd"){
305             #$selector = 'getOdd('.$selector.')';
306             $selector .= '[position() mod 2 != 1]'; ##this doesn't do the job exactly as jQuery
307             } elsif ($value eq "even"){
308             #$selector = 'getEven('.$selector.')';
309             $selector .= '[position() mod 2 != 0]'; ##this doesn't do the job exactly as jQuery
310             } elsif ($value =~ /(gt|lt|eq)\((.*?)\)/){
311             $selector = "getIndex($selector,'$1','$2')";
312             } elsif ($value =~ /nth-child\((.*?)\)/){
313             $selector .= "[position() = $1]";
314             } elsif ($value =~ /has\((.*?)\)/){
315             $selector = "getHas($selector,'$1')";
316             } elsif ($value =~ /not\((.*?)\)/){
317             $selector = "getNot($selector,'$1')";
318             } elsif ($value eq "button"){
319             #$selector .= '[@type="button"]';
320             $selector = "getButton($selector)";
321             } elsif ($value =~ /(checkbox|file|hidden|image|text|submit|radio|password|reset)/){
322             $selector .= "[\@type='$value']";
323             } elsif ($value eq "checked"){
324             $selector .= '[@checked="checked"]';
325             } elsif ($value eq "selected"){
326             $selector .= '[@selected="selected"]';
327             } elsif ($value eq "disabled"){
328             $selector .= '[@disabled]';
329             } elsif ($value eq "enabled"){
330             $selector .= '[not(@disabled)]';
331             } elsif ($value =~ /contains\((.*?)\)/){
332             my $str = $1;
333             for ($str) {
334             s/^['"]//;
335             s/['"]$//;
336             }
337             $selector .= "[contains(.,'$str')]";
338             } elsif ($value eq "empty"){
339             $selector .= '[not(node())]';
340             } elsif ($value eq "only-child"){
341            
342             my ($str1, $str2) = $selector =~ /(.*)\/(.*)/;
343             if ($str1 =~ s/\/$//){
344             $str2 = '/'.$str2;
345             $selector = $str1.'//child::*/parent::*[count(*)=1]'.'/'.$str2;
346             } else{
347             $selector = $str1.'/child::*/parent::*[count(*)=1]'.'/'.$str2;
348             }
349            
350             } elsif ($value eq "header"){
351             $selector = "getHeaders($selector)";
352             } elsif ($value eq "parent"){
353             $selector .= '[(node())]';
354             } elsif ($value eq "last"){
355             #$selector = "getLast($selector)";
356             $selector .= '[position()=last()]';
357             } elsif ($value eq "last-child"){
358             $selector .= "[last()]";
359             }
360             }
361             $pos++; $pos2++;
362             }
363             if ($single){
364             $selector .= '[1]';
365             $single = 0;
366             }
367             $this_query++;
368             push (@args,$selector);
369             }
370             return join(' | ',@args);
371             }
372              
373             sub as_HTML {
374             my $self = shift;
375             my $doc = $self->document;
376             if (ref($doc) eq 'XML::LibXML::Document' ){
377             my $html = $doc->serialize_html();
378             if ($html =~ m/
/g){
379             $html =~ s/(?:.*?)
(.*)<\/div>(?:.*)/$1/s;
380             }
381             return $html;
382             }
383             return $doc->getDocumentElement->html();
384             }
385              
386             sub as_XML {
387             my $doc = $_[0]->document;
388             if ($$DOC ne $$doc){
389             if (ref($doc) eq 'XML::LibXML::Document' ){
390             my $xml = $doc->serialize();
391             if ($xml =~ m/
/g){
392             $xml =~ s/(?:.*?)
(.*)<\/div>(?:.*)/$1/s;
393             }
394             return $xml;
395             }
396             return $doc->getDocumentElement->html();
397             }
398            
399             return $doc->serialize();
400             }
401              
402              
403             sub is_HTML {
404             my ($self,$html) = @_;
405             ### very permative solution but it seems
406             ### to work with all tests so far
407             if ($html =~ /<.*>/g){
408             return 1;
409             }
410             return 0;
411             }
412              
413             sub createNode {
414             my ($self,$html) = @_;
415             my $node;
416             if (!$PARSER){ $self->new(); }
417             $html = "
".$html."
";
418             if ($html =~ /^
<\?xml/) {
419             $node = $PARSER->parse_string($html);
420             } else{
421             $node = $PARSER->parse_html_string($html);
422             }
423            
424             $DOC = $node if !$DOC;
425             $node->removeInternalSubset;
426             my $nodes = $node->getDocumentElement->findnodes("//*[\@class='REMOVE_THIS_ELEMENT']")->[0]->childNodes;
427             return $nodes;
428             }
429              
430             sub createNode2 {
431             my ($self,$html) = @_;
432             $html = "
".$html."
";
433             my $new = $self->new($html);
434             return $new->jQuery('.REMOVE_THIS_ELEMENT *');
435             }
436              
437             ##detect node parent document
438             sub document {
439             my $self = shift;
440             my $doc;
441             if ($self->isa('ARRAY') && $self->[0]){
442             $doc = $self->[0]->ownerDocument;
443             } elsif ($self->isa('HASH') && $self->{document}){
444             $doc = $self->{document};
445             }
446             return $doc ? $doc : $DOC;
447             }
448              
449             sub cloneDOC {
450             my $self = shift;
451             my $clone = $self->document->cloneNode(1);
452             return bless([$clone], __PACKAGE__);
453             }
454              
455             sub decode_html { return decode_entities($_[1]); }
456             sub parser { return shift->{parser}; }
457              
458             ###custom internal functions
459             ###copied from jQuery.js
460             sub body { return shift->getElementsByTagName('body'); }
461              
462             sub makeArray {
463             my ( $array, $results ) = @_;
464             my $ret = $results || [];
465             if ( ref $array eq 'ARRAY' ) {
466             push @{$ret}, @{$array};
467             }
468             else { $ret = \@_; }
469             return wantarray
470             ? @$ret
471             : $ret;
472             }
473              
474             sub merge {
475             my ( $first, $second ) = @_;
476             my $i = _length($first);
477             my $j = 0;
478             if ( _length($second) ) {
479             for ( my $l = _length($second); $j < $l; $j++ ) {
480             $first->[ $i++ ] = $second->[ $j ];
481             }
482             } else {
483             while ( $second->[$j] ) {
484             $first->[ $i++ ] = $second->[ $j++ ];
485             }
486             }
487             $i = _length($first);
488             return wantarray
489             ? @$first
490             : $first ;
491             }
492              
493             sub _length {
494             if (ref $_[0] eq 'ARRAY'){
495             @_ = @{$_[0]};
496             }
497             return $#_ + 1;
498             }
499              
500             sub isDisconnected {
501             my $node = shift;
502             return !$node || !$node->parentNode || $node->parentNode->nodeType == 11;
503             }
504              
505             sub unique {
506             my %hash;
507             my @ele = grep{!$hash{$$_}++} @_;
508             return @ele;
509             }
510              
511             sub nodeName {
512             my ( $elem, $name ) = @_;
513             return $elem->nodeName && uc $elem->nodeName eq uc $name;
514             }
515              
516             package jQuery::Obj;
517             use base 'jQuery';
518              
519             ##hack Libxml modules to use jQuery as base module
520             ##is this a bad practice?
521             package XML::LibXML::NodeList;
522             use base 'jQuery::Obj';
523              
524             package XML::LibXML::Node;
525             use base 'jQuery::Obj';
526              
527             package XML::LibXML::Element;
528             use base 'jQuery::Obj';
529              
530             package XML::LibXML::Text;
531             use base 'jQuery::Obj';
532             use base 'XML::LibXML::Element';
533              
534             1;
535              
536             __END__