# $Id: /xmltwig/trunk/Twig/XPath.pm 32 2008-01-18T13:11:52.128782Z mrodrigu $
package XML::Twig::XPath;
use strict;
use warnings;
use XML::Twig;
my $XPATH; # XPath engine (XML::XPath or XML::XPathEngine);
my $XPATH_NUMBER; # <$XPATH>::Number, the XPath number class
BEGIN
{ foreach my $xpath_engine ( qw( XML::XPathEngine XML::XPath) )
{ if( XML::Twig::_use( $xpath_engine) ) { $XPATH= $xpath_engine; last; } }
unless( $XPATH) { die "cannot use XML::Twig::XPath: neither XML::XPathEngine 0.09+ nor XML::XPath are available"; }
$XPATH_NUMBER= "${XPATH}::Number";
}
use vars qw($VERSION);
$VERSION="0.02";
BEGIN
{ package # hide from PAUSE
XML::XPath::NodeSet;
no warnings; # to avoid the "Subroutine sort redefined" message
# replace the native sort routine by a Twig'd one
sub sort
{ my $self = CORE::shift;
@$self = CORE::sort { $a->node_cmp( $b) } @$self;
return $self;
}
package # hide from PAUSE
XML::XPathEngine::NodeSet;
no warnings; # to avoid the "Subroutine sort redefined" message
# replace the native sort routine by a Twig'd one
sub sort
{ my $self = CORE::shift;
@$self = CORE::sort { $a->node_cmp( $b) } @$self;
return $self;
}
}
package XML::Twig::XPath;
use base 'XML::Twig';
my $XP; # the global xp object;
sub to_number { return $XPATH_NUMBER->new( $_[0]->root->text); }
sub new
{ my $class= shift;
my $t= XML::Twig->new( elt_class => 'XML::Twig::XPath::Elt', @_);
$t->{twig_xp}= $XPATH->new();
bless $t, $class;
return $t;
}
sub set_namespace { my $t= shift; $t->{twig_xp}->set_namespace( @_); }
sub set_strict_namespaces { my $t= shift; $t->{twig_xp}->set_strict_namespaces( @_); }
sub node_cmp($$) { return $_[1] == $_[0] ? 0 : -1; } # document is before anything but itself
sub isElementNode { 0 }
sub isAttributeNode { 0 }
sub isTextNode { 0 }
sub isProcessingInstructionNode { 0 }
sub isPINode { 0 }
sub isCommentNode { 0 }
sub isNamespaceNode { 0 }
sub getAttributes { [] }
sub getValue { return $_[0]->root->text; }
sub findnodes { my( $t, $path)= @_; return $t->{twig_xp}->findnodes( $path, $t); }
sub findnodes_as_string { my( $t, $path)= @_; return $t->{twig_xp}->findnodes_as_string( $path, $t); }
sub findvalue { my( $t, $path)= @_; return $t->{twig_xp}->findvalue( $path, $t); }
sub exists { my( $t, $path)= @_; return $t->{twig_xp}->exists( $path, $t); }
sub find { my( $t, $path)= @_; return $t->{twig_xp}->find( $path, $t); }
sub matches { my( $t, $path, $node)= @_; $node ||= $t; return $t->{twig_xp}->matches( $node, $path, $t) || 0; }
sub getNamespaces { $_[0]->root->getNamespaces(); }
#TODO: it would be nice to be able to pass in any object in this
#distribution and cast it to the proper $XPATH class to use as a
#variable (via 'nodes' argument or something)
sub set_var {
my ($t, $name, $value) = @_;
if( ! ref $value) { $value= $t->findnodes( qq{"$value"}); }
$t->{twig_xp}->set_var($name, $value);
}
1;
# adds the appropriate methods to XML::Twig::Elt so XML::XPath can be used as the XPath engine
package XML::Twig::XPath::Elt;
use base 'XML::Twig::Elt';
*getLocalName= *XML::Twig::Elt::local_name;
*getValue = *XML::Twig::Elt::text;
sub isAttributeNode { 0 }
sub isNamespaceNode { 0 }
sub to_number { return $XPATH_NUMBER->new( $_[0]->text); }
sub getAttributes
{ my $elt= shift;
my $atts= $elt->atts;
# alternate, faster but less clean, way
my @atts= map { bless( { name => $_, value => $atts->{$_}, elt => $elt },
'XML::Twig::XPath::Attribute')
}
sort keys %$atts;
# my @atts= map { XML::Twig::XPath::Attribute->new( $elt, $_) } sort keys %$atts;
return wantarray ? @atts : \@atts;
}
sub getNamespace
{ my $elt= shift;
my $prefix= shift() || $elt->ns_prefix;
if( my $expanded= $elt->namespace( $prefix))
{ return XML::Twig::XPath::Namespace->new( $prefix, $expanded); }
else
{ return XML::Twig::XPath::Namespace->new( $prefix, ''); }
}
# returns namespaces declared in the element
sub getNamespaces #_get_namespaces
{ my( $elt)= @_;
my @namespaces;
foreach my $att ($elt->att_names)
{ if( $att=~ m{^xmlns(?::(\w+))?$})
{ my $prefix= $1 || '';
my $expanded= $elt->att( $att);
push @namespaces, XML::Twig::XPath::Namespace->new( $prefix, $expanded);
}
}
return wantarray() ? @namespaces : \@namespaces;
}
sub node_cmp($$)
{ my( $a, $b)= @_;
if( UNIVERSAL::isa( $b, 'XML::Twig::XPath::Elt'))
{ # 2 elts, compare them
return $a->cmp( $b);
}
elsif( UNIVERSAL::isa( $b, 'XML::Twig::XPath::Attribute'))
{ # elt <=> att, compare the elt to the att->{elt}
# if the elt is the att->{elt} (cmp return 0) then -1, elt is before att
return ($a->cmp( $b->{elt}) ) || -1 ;
}
elsif( UNIVERSAL::isa( $b, 'XML::Twig::XPath'))
{ # elt <=> document, elt is after document
return 1;
}
else
{ die "unknown node type ", ref( $b); }
}
sub getParentNode
{ return $_[0]->_parent
|| $_[0]->twig;
}
sub findnodes { my( $elt, $path)= @_; return $elt->twig->{twig_xp}->findnodes( $path, $elt); }
sub findnodes_as_string { my( $elt, $path)= @_; return $elt->twig->{twig_xp}->findnodes_as_string( $path, $elt); }
sub findvalue { my( $elt, $path)= @_; return $elt->twig->{twig_xp}->findvalue( $path, $elt); }
sub exists { my( $elt, $path)= @_; return $elt->twig->{twig_xp}->exists( $path, $elt); }
sub find { my( $elt, $path)= @_; return $elt->twig->{twig_xp}->find( $path, $elt); }
sub matches { my( $elt, $path)= @_; return $elt->twig->{twig_xp}->matches( $elt, $path, $elt->getParentNode) || 0; }
1;
# this package is only used to allow XML::XPath as the XPath engine, otherwise
# attributes are just attached to their parent element and are not considered objects
package XML::Twig::XPath::Attribute;
sub new
{ my( $class, $elt, $att)= @_;
return bless { name => $att, value => $elt->att( $att), elt => $elt }, $class;
}
sub getValue { return $_[0]->{value}; }
sub getName { return $_[0]->{name} ; }
sub getLocalName { (my $name= $_[0]->{name}) =~ s{^.*:}{}; $name; }
sub string_value { return $_[0]->{value}; }
sub to_number { return $XPATH_NUMBER->new( $_[0]->{value}); }
sub isElementNode { 0 }
sub isAttributeNode { 1 }
sub isNamespaceNode { 0 }
sub isTextNode { 0 }
sub isProcessingInstructionNode { 0 }
sub isPINode { 0 }
sub isCommentNode { 0 }
sub toString { return qq{$_[0]->{name}="$_[0]->{value}"}; }
sub getNamespace
{ my $att= shift;
my $prefix= shift();
if( ! defined( $prefix))
{ if($att->{name}=~ m{^(.*):}) { $prefix= $1; }
else { $prefix=''; }
}
if( my $expanded= $att->{elt}->namespace( $prefix))
{ return XML::Twig::XPath::Namespace->new( $prefix, $expanded); }
}
sub node_cmp($$)
{ my( $a, $b)= @_;
if( UNIVERSAL::isa( $b, 'XML::Twig::XPath::Attribute'))
{ # 2 attributes, compare their elements, then their name
return ($a->{elt}->cmp( $b->{elt}) ) || ($a->{name} cmp $b->{name});
}
elsif( UNIVERSAL::isa( $b, 'XML::Twig::XPath::Elt'))
{ # att <=> elt : compare the att->elt and the elt
# if att->elt is the elt (cmp returns 0) then 1 (elt is before att)
return ($a->{elt}->cmp( $b) ) || 1 ;
}
elsif( UNIVERSAL::isa( $b, 'XML::Twig::XPath'))
{ # att <=> document, att is after document
return 1;
}
else
{ die "unknown node type ", ref( $b); }
}
*cmp=*node_cmp;
1;
package XML::Twig::XPath::Namespace;
sub new
{ my( $class, $prefix, $expanded)= @_;
bless { prefix => $prefix, expanded => $expanded }, $class;
}
sub isNamespaceNode { 1; }
sub getPrefix { $_[0]->{prefix}; }
sub getExpanded { $_[0]->{expanded}; }
sub getValue { $_[0]->{expanded}; }
sub getData { $_[0]->{expanded}; }
sub node_cmp($$)
{ my( $a, $b)= @_;
if( UNIVERSAL::isa( $b, 'XML::Twig::XPath::Namespace'))
{ # 2 attributes, compare their elements, then their name
return $a->{prefix} cmp $b->{prefix};
}
else
{ die "unknown node type ", ref( $b); }
}
*cmp=*node_cmp;
1