pQuery/lib/pQuery.pm
use strict; use warnings;
package pQuery;
our $VERSION = '0.24';
use pQuery::DOM;
use Carp;
use HTML::TreeBuilder();
use base 'Exporter';
our $document;
*pQuery = \$document;
our @EXPORT = qw(pQuery $pQuery PQUERY);
my $my = {};
my $lwp_user_agent;
my $quickExpr = qr/^([^<]*<(.|\s)+>[^>]*)$|^#(\w+)$/;
my $isSimple = qr/^.[^:#\[\.]*$/;
my $dom_element_class = 'pQuery::DOM';
sub pQuery {
return 'pQuery'->new(@_);
}
sub PQUERY {
return 'PQUERY'->new(@_);
}
#------------------------------------------------------------------------------#
# New ideas / Playing around stuffs
#------------------------------------------------------------------------------#
sub url {
my $this = shift;
return $my->{$this}{url}
if $my->{$this}{url};
while ($this = $my->{$this}{prevObject}) {
return $my->{$this}{url}
if $my->{$this}{url};
}
return;
}
#------------------------------------------------------------------------------#
# Truly ported from jQuery stuff
#------------------------------------------------------------------------------#
sub new {
my $class = shift;
my $this = bless [], $class;
$my->{$this} = {};
return $this->_init(@_);
}
sub _init {
my ($this, $selector, $context) = @_;
$selector ||= $document or return $this;
if (ref($selector) eq $dom_element_class) {
@$this = $selector;
return $this;
}
elsif (not ref($selector)) {
my $match = ($selector =~ m/$quickExpr/o);
if ($match and ($1 or not $context)) {
if ($1) {
my $html = $this->_clean($1);
$selector = [pQuery::DOM->fromHTML($html)];
# $selector = $this->_clean([$1], $context);
}
else {
my $elem = $document->getElementById($3);
if ($elem) {
@$this = $elem;
return $this;
}
else {
$selector = [];
}
}
}
else {
if ($selector =~ /^\s*(https?|file):/) {
$my->{$this}{url} = $selector;
return $document = $this->_new_from_url($selector);
}
elsif ($selector =~ /^\S+\.html?$/) {
$my->{$this}{file} = $selector;
open FILE, $selector
or croak "Can't open file '$selector' for input:\n$!";
my $html = do {local $/; <FILE>};
close FILE;
$html = $this->_clean($html);
$selector = [$document = pQuery::DOM->fromHTML($html)];
}
else {
$context ||= $document;
return pQuery($context)->find($selector);
}
}
}
@$this = (ref($selector) eq 'ARRAY' or ref($selector) eq 'pQuery')
? @$selector
: $selector;
return $this;
}
sub _clean {
my ($this, $html) = @_;
$html =~ s/^\s*<\?xml\s.*?>\s*//s;
$html =~ s/^\s*<!DOCTYPE\s.*?>\s*//s;
return $html;
}
sub pquery { return $VERSION }
sub size { return $#{$_[0]} + 1 }
sub get {
my $this = shift;
# Get could be for Ajax URL or Object Member
return $this->_web_get(@_)
if @_ and $_[0] !~ /^\d+$/;
return @_
? $this->[$_[0]]
: wantarray ? (@$this) : $this->[0];
}
sub pushStack {
my ($this, $elems) = @_;
my $ret = pQuery($elems);
$ret->_prevObject($this);
return $ret;
}
sub _prevObject {
my $this = shift;
return @_
? ($my->{$this}{prevObject} = $_[0])
: $my->{$this}{prevObject};
}
# Not needed in Perl
# sub _setArray {}
sub each {
my ($this, $sub) = @_;
my $i = 0;
&$sub($i++) for @$this;
return $this;
}
sub attr { # (elem, name, value)
my ($elem, $name, $value) = @_;
my $node = $elem->[0];
# don't set attributes on text and comment nodes
return undef if (!$node || $node->nodeType eq 3 || $node->nodeType eq 8);
if ( defined $value ) {
# convert the value to a string
$node->setAttribute( $name, $value );
}
return $node->getAttribute( $name );
}
sub css { # (key, value)
# TODO - Get/set a css attribute
}
# TODO/XXX Made up. Not ported yet.
sub text {
# TODO - Get/set text value
my $this = shift;
my @text;
$this->each(sub {
my $text = '';
_to_text($_, \$text);
$text =~ s/\s+/ /g;
$text =~ s/^\s+|\s+$//g;
push @text, $text;
});
return wantarray ? @text : join(' ', grep /\S/, @text);
}
sub wrapAll { # (html)
# TODO - Wrap element with HTML
}
sub wrapInner { # (html)
# TODO - Wrap sub elements with HTML
}
sub wrap { # (html)
# TODO - Wrap current objects with HTML
}
sub append { # (@_)
# TODO - Append arguments to current objects
}
sub prepend { # (@_)
# TODO - Prepend arguments to current objects
}
sub before { # (@_)
# TODO - Insert arguments before current objects
}
sub after { # (@_)
# TODO - Insert arguments after current objects
}
sub end {
my $this = shift;
return $this->_prevObject || pQuery([]);
}
sub find {
my ($this, $selector) = @_;
my $elems = [];
for (my $i = 0; $i < @$this; $i++) {
push @$elems, @{$this->_find($selector, $this->[$i])};
}
return $this->pushStack(
$selector =~ /[^+>] [^+>]/
? $this->_unique($elems)
: $elems
)
}
sub clone { # (events)
# TODO - Not sure if we need this one.
}
sub filter { # (selector)
# TODO - A kind of grep
}
sub add { # (selector)
# TODO - Some kind of merge
}
sub is { # (selector)
# TODO - One element matches the selector
}
sub hasClass {
my ($this, $selector) = @_;
$this.is(".$selector");
}
sub val { # (value)
# TODO Get/set
}
# XXX - Not really ported yet.
sub html {
my $this = shift;
return unless @$this;
if (@_) {
for (@$this) {
next unless ref($_);
$_->innerHTML(@_);
}
return $this;
}
return $this->[0]->innerHTML(@_);
}
# Not a jQuery function.
sub toHtml {
my $this = shift;
return unless @$this;
return $this->[0]->toHTML;
}
# TODO - Not tested
sub replaceWith { # (value)
my ($this, $value) = @_;
return $this->after($value)->remove;
}
# TODO - Not tested
sub eq {
my ($this, $i) = @_;
return $this->pushStack($this->[$i]);
}
sub slice { #(i, j)
# TODO - Behave like JS slice()
}
sub map {
my ($this, $callback) = @_;
return $this->pushStack(__map($this, sub {
my ($elem, $i) = @_;
return $callback->($elem, $i, $elem);
}));
}
# TODO - Not tested
sub andSelf {
my $this = shift;
return $this.add($this->prevObject);
}
sub data { # (key, value)
# TODO - Not sure
}
sub removeData { # (key)
# TODO - Not Sure
}
sub domManip {
my ($this, $args, $table, $reverse, $callback) = @_;
my $elems;
return $this->each(sub {
if (not defined $elems) {
$elems = $args;
@$elems = reverse @$elems
if $reverse;
}
pQuery::each($elems, sub {
$callback->($this, $_);
});
});
}
#------------------------------------------------------------------------------#
# "Class" methods
#------------------------------------------------------------------------------#
# sub noConflict {}
# sub isFunction {}
# sub isXMLdoc {}
# sub globalEval {}
sub _nodeName {
my ($this, $elem, $name) = @_;
return $elem->nodeName &&
uc($elem->nodeName) eq uc($name);
}
# sub cache {}
# sub data {}
# sub removeData {}
# sub each {}
# sub prop {}
# sub className {}
# sub swap {}
# sub css {}
# sub curCSS {}
# sub clean {}
# sub attr {}
sub _trim {
(my $string = $_[1]) =~ s/^\s+|\s+$//g;
return $string;
}
# sub makeArray {}
# sub inArray {}
sub _merge {
push @{$_[1]}, @{$_[2]};
return $_[1];
}
sub _unique {
my $seen = {};
return [ grep {not $seen->{$_}++} @{$_[1]} ];
}
sub _grep {
my ($this, $elems, $callback, $inv) = @_;
my $ret = [];
for (my ($i, $length) = (0, scalar(@$elems)); $i < $length; $i++) {
push @$ret, $elems->[$i]
if (not $inv and &$callback($elems->[$i], $i)) or
($inv and not &$callback($elems->[$i], $i));
}
return $ret;
}
# sub map {}
#------------------------------------------------------------------------------#
# Selector functions
#------------------------------------------------------------------------------#
my $chars = qr/(?:[\w\x{128}-\x{FFFF}*_-]|\\.)/;
my $quickChild = qr/^>\s*($chars+)/;
my $quickId = qr/^($chars+)(#)($chars+)/;
my $quickClass = qr/^(([#.]?)($chars*))/;
my $expr = {
# XXX Can't figure out how to create tests for these yet :(
"" => sub {
die 'pQuery selector error #1001. Please notify ingy@cpan.org';
},
"#" => sub {
die 'pQuery selector error #1002. Please notify ingy@cpan.org';
},
":" => {
# Position Checks
lt => sub { return $_[1] < $_[2][3] },
gt => sub { return $_[1] > $_[2][3] },
nth => sub { return $_[2][3] == $_[1] },
eq => sub { return $_[2][3] == $_[1] },
first => sub { return $_[1] == 0 },
last => sub { return $_[1] == $#{$_[3]} },
even => sub { return $_[1] % 2 == 0 },
odd => sub { return $_[1] % 2 },
# Child Checks
"first-child" => sub {
return $_[0]->parentNode->getElementsByTagName("*")->[0] == $_[0];
},
"last-child" => sub {
return pQuery->_nth(
$_[0]->parentNode->lastChildRef,
1,
"previousSiblingRef"
) == $_[0];
},
"only-child" => sub {
return ! pQuery->_nth(
$_[0]->parentNode->lastChildRef,
2,
"previousSiblingRef"
);
},
# Parent Checks
parent => sub { return $_[0]->firstChild ? 1 : 0 },
empty => sub { return $_[0]->firstChild ? 0 : 1 },
# Text Check
contains => sub { return index(pQuery($_[0])->text, $_[2][3]) >= 0 },
# XXX Finish porting these if it makes sense...
# // Visibility
# visible: function(a){return "hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},
# hidden: function(a){return "hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},
#
# // Form attributes
# enabled: function(a){return !a.disabled;},
# disabled: function(a){return a.disabled;},
# checked: function(a){return a.checked;},
# selected: function(a){return a.selected||jQuery.attr(a,"selected");},
#
# // Form elements
# text: function(a){return "text"==a.type;},
# radio: function(a){return "radio"==a.type;},
# checkbox: function(a){return "checkbox"==a.type;},
# file: function(a){return "file"==a.type;},
# password: function(a){return "password"==a.type;},
# submit: function(a){return "submit"==a.type;},
# image: function(a){return "image"==a.type;},
# reset: function(a){return "reset"==a.type;},
# button: function(a){return "button"==a.type||jQuery.nodeName(a,"button");},
# input: function(a){return /input|select|textarea|button/i.test(a.nodeName);},
# :has()
# XXX - The first form should work. Indicates that context is messed up.
# has => sub { return pQuery->find($_[2][3], $_[0])->length ? 1 : 0 },
has => sub { return pQuery($_[0])->find($_[2][3])->length ? 1 : 0 },
# :header
header => sub { return $_[0]->nodeName =~ /^h[1-6]$/i },
},
};
# The regular expressions that power the parsing engine
my $parse = [
# Match: [@value='test'], [@foo]
qr/^(\[)\s*\@?([\w-]+)\s*((?:[\!\*\$\^\~\|\=]?\=)?)\s*([\'\"]?)(.*?)(?:\4)\s*\]/,
# Match: :contains('foo')
qr/^(:)([\w-]+)\(\"?\'?(.*?(\(.*?\))?[^(]*?)\"?\'?\)/,
# Match: :even, :last-chlid, #id, .class
qr/^([:.#]*)($chars+)/,
];
sub _multiFilter {
# XXX - Port me.
}
sub _find {
my ($this, $t, $context) = @_;
return [ $t ]
if ref($t);
return []
unless ref($context) and
$context->can('nodeType') and
$context->nodeType == 1;
$context ||= $document or return [];
my ($ret, $done, $last, $nodeName) = ([$context], [], '', '');
while ($t and $last ne $t) {
my $r = [];
$last = $t;
$t = $this->_trim($t);
my $foundToken = 0;
if ($t =~ s/$quickChild//o) {
$nodeName = uc($1);
for (my $i = 0; $ret->[$i]; $i++) {
for (my $c = $ret->[$i]->firstChildRef; $c; $c = $c->nextSiblingRef) {
if ($c->nodeType == 1 and
(
$nodeName eq "*" or
uc($c->nodeName) eq $nodeName
)
) { push @$r, $c }
}
}
$ret = $r;
$t = $this->_trim($t);
$foundToken = 1;
}
else {
if ($t =~ s/^([>+~])\s*(\w*)//) {
$r = [];
my $merge = {};
$nodeName = uc($2);
my $m = $1;
for (my ($j, $rl) = (0, scalar(@$ret)); $j < $rl; $j++) {
my $n = ($m eq "~" or $m eq "+")
? $ret->[$j]->nextSiblingRef
: $ret->[$j]->firstChildRef;
for (; $n; $n = $n->nextSiblingRef) {
if ($n->nodeType == 1) {
my $id = $n;
last if ($m eq "~" and $merge->{$id});
if (not $nodeName or
uc($n->nodeName) eq $nodeName
) {
$merge->{$id} = 1 if $m eq "~";
push @$r, $n;
}
last if $m eq "+";
}
}
}
$ret = $r;
$t = $this->_trim($t);
$foundToken = 1;
}
}
my $m;
if ($t and not $foundToken) {
if ($t =~ s/^,//) {
shift @$ret if $context == $ret->[0];
$done = $this._merge($done, $ret);
$r = $ret = [$context];
$t = " $t";
}
else {
if ($t =~ s/$quickId//o) {
$m = [0, $2, $3, $1];
}
else {
if ($t =~ s/$quickClass//o) {
$m = [$1, $2, $3];
}
}
$m->[2] =~s/\\//g;
my $elem = $ret->[-1];
my $oid;
if ($m->[1] eq "#" and
$elem and
$elem->can('getElementById')
) {
$oid = $elem->getElementById($m->[2]);
$ret = $r = (
$oid &&
(not $m->[3] or $this->_nodeName($oid, $m->[3]))
) ? [$oid] : [];
}
else {
for (my $i = 0; $ret->[$i]; $i++) {
my $tag = ($m->[1] eq "#" and $m->[3])
? $m->[3]
: ($m->[1] ne "" or $m->[0] eq "")
? "*"
: $m->[2];
$r = $this->_merge(
$r,
$ret->[$i]->getElementsByTagName($tag)
);
}
$r = $this->_classFilter($r, $m->[2])
if ($m->[1] eq ".");
if ($m->[1] eq "#") {
my $tmp = [];
for (my $i = 0; $r->[$i]; $i++) {
if ($r->[$i]->getAttribute("id") eq $m->[2]) {
$tmp = [ $r->[$i] ];
last;
}
}
$r = $tmp;
}
$ret = $r;
}
}
}
if ($t) {
my $val = $this->_filter($t, $r);
$ret = $r = $val->{r};
$t = $this->_trim($val->{t});
}
}
# $ret = [] if $t;
die "selector error: $t" if $t;
shift(@$ret) if $ret and @$ret and $context == $ret->[0];
$done = $this->_merge($done, $ret);
return $done;
}
sub _classFilter {
my ($this, $r, $m, $not) = @_;
$m = " $m ";
my $tmp = [];
for (my $i = 0; $r->[$i]; $i++) {
my $pass = CORE::index((" " . $r->[$i]->className . " "), $m) >= 0;
push @$tmp, $r->[$i]
if not $not and $pass or $not and not $pass;
}
return $tmp;
}
sub _filter {
my ($this, $t, $r, $not) = @_;
my $last = '';
while ($t and $t ne $last) {
$last = $t;
my ($p, $m) = ($parse);
for (my $i = 0; $p->[$i]; $i++) {
my $re = $p->[$i];
if ($t =~ s/$re//) {
$m = [0, $1, $2, $3, $4, $5];
$m->[2] =~ s/\\//g;
last;
}
}
last
if not $m;
if ( $m->[1] eq ":" && $m->[2] eq "not") {
$r = ($m->[3] =~ m/$isSimple/o)
? $this->_filter($m->[3], $r, 1)->{r}
: pQuery($r)->not($m->[3]);
}
elsif ($m->[1] eq ".") {
$r = $this->_classFilter($r, $m->[2], $not);
}
elsif ($m->[1] eq "[") {
my ($tmp, $type) = ([], $m->[3]);
for (my ($i, $rl) = (0, scalar(@$r)); $i < $rl; $i++) {
my $a = $r->[$i];
my $z = $a->{($this->_props->{$m->[2]} || $m->[2])};
if (not defined $z or $m->[2] =~ m/href|src|selected/) {
$z = $a->attr($m->[2]);
}
if (
((
# Selects elements that have the specified attribute.
($type eq "" and defined $z) or
# Selects elements that have the specified attribute with
# a value exactly equal to a certain value.
($type eq "=" and defined $z and $z eq $m->[5]) or
# Select elements that either don�t have the specified attribute,
# or do have the specified attribute but not with a certain value.
($type eq "!=" and (not defined $z or $z ne $m->[5])) or
# Selects elements that have the specified attribute with a
# value beginning exactly with a given string.
($type eq "^=" and defined $z and $z =~ /\A\Q$m->[5]\E/) or
# Selects elements that have the specified attribute with
# a value ending exactly with a given string (case sensitive)
($type eq '$=' and defined $z and $z =~ /\Q$m->[5]\E\z/) or
# Selects elements that have the specified attribute with
# a value containing the given substring.
($type eq "*=" and defined $z and $z =~ /\Q$m->[5]\E/) or
# Selects elements that have the specified attribute with
# a value containing a given word, delimited by spaces.
($type eq "~=" and defined $z and $z =~ /(?:\W|\A)\Q$m->[5]\E(?:\W|\z)/) or
# Selects elements that have the specified attribute with
# a value either equal to a given string or starting with
# that string followed by a hyphen (-).
($type eq "|=" and defined $z and $z =~ /\A\Q$m->[5]\E(?:-|\z)/)
) ? 1 : 0) ^ ($not ? 1 : 0)
) { push @$tmp, $a }
}
$r = $tmp;
}
elsif ($m->[1] eq ":" && $m->[2] eq "nth-child") {
# XXX - Finish porting this. Not sure how useful it is though...
}
else {
my $fn = $expr->{$m->[1]};
if (ref($fn) eq "HASH") {
$fn = $fn->{ $m->[2] };
}
# if ( typeof fn == "string" )
# fn = eval("false||function(a,i){return " + fn + ";}");
$fn = sub { 0 }
if ref($fn) ne 'CODE';
$r = $this->_grep(
$r,
sub {
return &$fn($_[0], $_[1], $m, $r);
},
$not
);
}
}
return { r => $r, t => $t };
}
sub _dir {
# XXX - Port me.
}
sub _nth {
my ($this, $cur, $result, $dir, $elem) = @_;
$result ||= 1;
my $num = 0;
for (; $cur; $cur = $cur->$dir) {
last if (ref($cur) and $cur->nodeType == 1 and ++$num == $result);
}
return $cur;
}
sub _sibling {
# XXX - Port me.
}
sub _props {
return {
for => "htmlFor",
class => "className",
# float => styleFloat,
# cssFloat => styleFloat,
# styleFloat => styleFloat,
innerHTML => "innerHTML",
className => "className",
value => "value",
disabled => "disabled",
checked => "checked",
readonly => "readOnly",
selected => "selected",
maxlength => "maxLength",
selectedIndex => "selectedIndex",
defaultValue => "defaultValue",
tagName => "tagName",
nodeName => "nodeName"
};
}
#------------------------------------------------------------------------------#
# These methods need to go down here because they are Perl builtins.
#------------------------------------------------------------------------------#
sub length { return $#{$_[0]} + 1 }
sub index {
my ($this, $elem) = @_;
my $ret = -1;
$this->each(sub {
$ret = shift
if (ref($_) && ref($elem)) ? ($_ == $elem) : ($_ eq $elem);
});
return $ret;
}
sub not { # (selector)
# TODO - An anti-grep??
}
#------------------------------------------------------------------------------#
# Helper functions (not methods)
#------------------------------------------------------------------------------#
sub _new_from_url {
require Encode;
my $this = shift;
my $url = shift;
my $response = $this->_web_get($url);
return $this
unless $response->is_success;
my $html = Encode::decode_utf8($response->content);
@$this = pQuery::DOM->fromHTML($html);
return $this;
}
sub _web_get {
my $this = shift;
my $url = shift;
require LWP::UserAgent;
$lwp_user_agent ||= LWP::UserAgent->new;
my $request = HTTP::Request->new(GET => $url);
my $response = $lwp_user_agent->request($request);
return $response;
}
sub _to_text {
my ($elem, $text) = @_;
if (ref $elem) {
for my $child (@{$elem->{_content}}) {
_to_text($child, $text);
}
}
else {
$$text .= $elem;
}
}
sub _find_elems {
my ($elem, $selector, $elems) = @_;
return unless ref $elem;
if ($selector =~ /^\w+$/) {
if ($elem->{_tag} eq $selector) {
push @$elems, $elem;
}
}
for my $child (@{$elem->{_content}}) {
_find_elems($child, $selector, $elems);
}
}
sub DESTROY { delete $my->{$_[0]}; }
#------------------------------------------------------------------------------#
# THE AMAZING PQUERY
#------------------------------------------------------------------------------#
package PQUERY;
sub new {
my $class = shift;
my $this = bless [], $class;
@$this = map 'pQuery'->new($_), @_;
return $this;
}
sub AUTOLOAD {
(my $method = $PQUERY::AUTOLOAD) =~ s/.*:://;
my $this = shift;
my @args = @_;
$this->EACH(sub {
my $i = shift;
$this->[$i] = $_->$method(@args);
});
return $this;
}
sub EACH {
my ($this, $sub) = @_;
my $index = 0;
&$sub($index++) for @$this;
return $this;
}
sub DESTROY {}
1;