##############################################################################
#
# Perl module: XML::XSLT
#
# By Geert Josten, gjosten@sci.kun.nl
# and Egon Willighagen, egonw@sci.kun.nl
#
# $Log: XSLT.pm,v $
# Revision 1.34 2008/11/21 15:45:13 gellyfish
# Made current() work
#
# Revision 1.33 2008/01/30 13:49:48 gellyfish
# Interim release
#
# Revision 1.32 2007/10/04 18:37:11 gellyfish
# updated
#
# Revision 1.31 2007/05/25 15:16:18 gellyfish
# * Merged in some changes
#
# Revision 1.30 2006/11/17 21:16:45 gellyfish
# Check in interim fix for literal variable at top level
#
# Revision 1.29 2005/12/08 12:53:39 gellyfish
# Added patch from andy_bach@wiwb.usourts.gov to fix warning in __evaluate_test__
#
# Revision 1.28 2004/06/02 07:48:34 gellyfish
# Fixed the check if $args{Source} is an 'XML::DOM::Document' from John
# Bywater.
#
# Revision 1.27 2004/04/02 10:48:35 gellyfish
# Fixing disposal bug
#
# Revision 1.26 2004/02/20 09:24:26 gellyfish
# * Fixes to variables
#
# Revision 1.25 2004/02/19 08:38:40 gellyfish
# * Fixed overlapping attribute-sets
# * Allow multiple nodes for processing-instruction() etc
# * Added test for for-each
#
# Revision 1.24 2004/02/18 08:34:38 gellyfish
# * Fixed select on "comment()" "processing-instruction()" etc
# * Added test for select
#
# Revision 1.23 2004/02/17 10:06:12 gellyfish
# * Added test for xsl:copy
#
# Revision 1.22 2004/02/17 08:52:29 gellyfish
# * 'use-attribute-sets' works in xsl:copy and recursively
#
# Revision 1.21 2004/02/16 10:29:20 gellyfish
# * Fixed variable implementation to handle non literals
# * refactored test implementation
# * added tests
#
# Revision 1.20 2003/06/24 16:34:51 gellyfish
# * Allowed both name and match attributes in templates
# * Lost redefinition warning with perl 5.8
#
# Revision 1.19 2002/02/18 09:05:14 gellyfish
# Refactoring
#
# Revision 1.18 2002/01/16 21:05:27 gellyfish
# * Added the manpage as an example
# * Started to properly implement omit-xml-declaration
#
# Revision 1.17 2002/01/13 10:35:00 gellyfish
# Updated pod
#
# Revision 1.16 2002/01/09 09:17:40 gellyfish
# * added test for <xsl:text>
# * Stylesheet whitespace stripping as per spec and altered tests ...
#
# Revision 1.15 2002/01/08 10:11:47 gellyfish
# * First cut at cdata-section-element
# * test for above
#
# Revision 1.14 2001/12/24 16:00:19 gellyfish
# * Version released to CPAN
#
# Revision 1.13 2001/12/20 09:21:42 gellyfish
# More refactoring
#
# Revision 1.12 2001/12/19 21:06:31 gellyfish
# * Some refactoring and style changes
#
# Revision 1.11 2001/12/19 09:11:14 gellyfish
# * Added more accessors for object attributes
# * Fixed potentially broken usage of $variables in _evaluate_template
#
# Revision 1.10 2001/12/18 09:10:10 gellyfish
# Implemented attribute-sets
#
# Revision 1.9 2001/12/17 22:32:12 gellyfish
# * Added Test::More to Makefile.PL
# * Added _indent and _outdent methods
# * Placed __get_attribute_sets in transform()
#
# Revision 1.8 2001/12/17 11:32:08 gellyfish
# * Rolled in various patches
# * Added new tests
#
#
###############################################################################
=head1 NAME
XML::XSLT - A perl module for processing XSLT
=cut
######################################################################
package XML::XSLT;
######################################################################
use strict;
use XML::DOM 1.25;
use XML::DOM::XPath;
use LWP::Simple qw(get);
use URI;
use Cwd;
use File::Basename qw(dirname);
use Carp;
# Namespace constants
use constant NS_XSLT => 'http://www.w3.org/1999/XSL/Transform';
use constant NS_XHTML => 'http://www.w3.org/TR/xhtml1/strict';
use vars qw ( $VERSION @ISA @EXPORT_OK $AUTOLOAD );
$VERSION = '0.50_2';
@ISA = qw( Exporter );
@EXPORT_OK = qw( &transform &serve );
my %deprecation_used;
######################################################################
# PUBLIC DEFINITIONS
=head1 SYNOPSIS
use XML::XSLT;
my $xslt = XML::XSLT->new ($xsl, warnings => 1);
$xslt->transform ($xmlfile);
print $xslt->toString;
$xslt->dispose();
=head1 DESCRIPTION
This module implements the W3C's XSLT specification. The goal is full
implementation of this spec, but we have not yet achieved
that. However, it already works well. See L<XML::XSLT Commands> for
the current status of each command.
XML::XSLT makes use of XML::DOM and LWP::Simple, while XML::DOM
uses XML::Parser. Therefore XML::Parser, XML::DOM and LWP::Simple
have to be installed properly for XML::XSLT to run.
=head1 Specifying Sources
The stylesheets and the documents may be passed as filenames, file
handles regular strings, string references or DOM-trees. Functions
that require sources (e.g. new), will accept either a named parameter
or simply the argument.
Either of the following are allowed:
my $xslt = XML::XSLT->new($xsl);
my $xslt = XML::XSLT->new(Source => $xsl);
In documentation, the named parameter `Source' is always shown, but it
is never required.
=head2 METHODS
=over 4
=cut
=item new(Source => $xml [, %args])
Returns a new XSLT parser object. Valid flags are:
=over 2
=item DOMparser_args
Hashref of arguments to pass to the XML::DOM::Parser object's parse
method.
=item variables
Hashref of variables and their values for the stylesheet.
=item base
Base of URL for file inclusion.
=item debug
Turn on debugging messages.
=item warnings
Turn on warning messages.
=item indent
Starting amount of indention for debug messages. Defaults to 0.
=item indent_incr
Amount to indent each level of debug message. Defaults to 1.
=back
=cut
sub new
{
my $class = shift;
my $self = bless {}, $class;
my %args = $self->__parse_args(@_);
$self->{DEBUG} = defined $args{debug} ? $args{debug} : "";
no strict 'subs';
if ( $self->{DEBUG} )
{
*__PACKAGE__::debug = \&debug;
}
else
{
*__PACKAGE__::debug = sub {};
}
use strict 'subs';
$self->{INDENT} = defined $args{indent} ? $args{indent} : 0;
$self->{PARSER} = XML::DOM::Parser->new();
$self->{PARSER_ARGS} =
defined $args{DOMparser_args} ? $args{DOMparser_args} : {};
$self->{VARIABLES} = defined $args{variables} ? $args{variables} : {};
$self->debug(join ' ', keys %{$self->{VARIABLES}});
$self->{WARNINGS} = defined $args{warnings} ? $args{warnings} : 0;
$self->{INDENT_INCR} = defined $args{indent_incr} ? $args{indent_incr} : 1;
$self->{XSL_BASE} =
defined $args{base} ? $args{base} : 'file://' . cwd . '/';
$self->{XML_BASE} =
defined $args{base} ? $args{base} : 'file://' . cwd . '/';
$self->use_deprecated( $args{use_deprecated} )
if exists $args{use_deprecated};
$self->debug("creating parser object:");
$self->_indent();
$self->open_xsl(%args);
$self->_outdent();
return $self;
}
sub use_deprecated
{
my ( $self, $use_deprecated ) = @_;
if ( defined $use_deprecated )
{
$self->{USE_DEPRECATED} = $use_deprecated;
}
return $self->{USE_DEPRECATED} || 0;
}
sub DESTROY { } # Cuts out random dies on includes
=item default_xml_version
Gets and/or sets the default XML version used in the output documents,
this will almost certainly want to be 1.0
=cut
sub default_xml_version
{
my ( $self, $xml_version ) = @_;
if ( defined $xml_version )
{
$self->{DEFAULT_XML_VERSION} = $xml_version;
}
return $self->{DEFAULT_XML_VERSION} ||= '1.0';
}
=item serve(Source => $xml [, %args])
Processes the given XML through the stylesheet. Returns a string
containg the result. Example:
use XML::XSLT qw(serve);
$xslt = XML::XSLT->new($xsl);
print $xslt->serve $xml;
=over 4
=item http_headers
If true, then prepends the appropriate HTTP headers (e.g. Content-Type,
Content-Length);
Defaults to true.
=item xml_declaration
If true, then the result contains the appropriate <?xml?> header.
Defaults to true.
=item xml_version
The version of the XML.
Defaults to 1.0.
=item doctype
The type of DOCTYPE this document is. Defaults to SYSTEM.
=back
=cut
sub serve
{
my $self = shift;
my $class = ref $self || croak "Not a method call";
my %args = $self->__parse_args(@_);
my $ret;
$args{http_headers} = 1 unless defined $args{http_headers};
$args{xml_declaration} = 1 unless defined $args{xml_declaration};
$args{xml_version} = $self->default_xml_version()
unless defined $args{xml_version};
$args{doctype} = 'SYSTEM' unless defined $args{doctype};
$args{clean} = 0 unless defined $args{clean};
$ret = $self->transform( $args{Source} )->toString;
if ( $args{clean} )
{
eval { require HTML::Clean };
if ($@)
{
CORE::warn("Not passing through HTML::Clean -- install the module");
}
else
{
my $hold = HTML::Clean->new( \$ret );
$hold->strip;
$ret = ${ $hold->data };
}
}
if ( my $doctype = $self->doctype() )
{
$ret = $doctype . "\n" . $ret;
}
if ( $args{xml_declaration} )
{
$ret = $self->xml_declaration() . "\n" . $ret;
}
if ( $args{http_headers} )
{
$ret =
"Content-Type: "
. $self->media_type() . "\n"
. "Content-Length: "
. length($ret) . "\n\n"
. $ret;
}
return $ret;
}
=item xml_declaration
Will return an XML declaration element based on the output encoding and
XML version.
=cut
sub xml_declaration
{
my ( $self, $xml_version, $output_encoding ) = @_;
$xml_version ||= $self->default_xml_version();
$output_encoding ||= $self->output_encoding();
return qq{<?xml version="$xml_version" encoding="$output_encoding"?>};
}
=item output_encoding
Gets and/or sets the output encoding that is used in the xml declaration
and elsewhere (default: UTF-8)
=cut
# defaulting blindly to UTF-8 is a bug, this should also be used to
# appropriately set the encoding of the output.
#
sub output_encoding
{
my ( $self, $encoding ) = @_;
if ( defined $encoding )
{
$self->{OUTPUT_ENCODING} = $encoding;
}
return exists $self->{OUTPUT_ENCODING} ? $self->{OUTPUT_ENCODING} : 'UTF-8';
}
sub doctype_system
{
my ( $self, $doctype ) = @_;
if ( defined $doctype )
{
$self->{DOCTYPE_SYSTEM} = $doctype;
}
return $self->{DOCTYPE_SYSTEM};
}
sub doctype_public
{
my ( $self, $doctype ) = @_;
if ( defined $doctype )
{
$self->{DOCTYPE_PUBLIC} = $doctype;
}
return $self->{DOCTYPE_PUBLIC};
}
=item result_document
An accessor for the XML::DOM object that the transformed document is
assembled into.
=cut
sub result_document()
{
my ( $self, $document ) = @_;
if ( defined $document )
{
$self->{RESULT_DOCUMENT} = $document;
}
return $self->{RESULT_DOCUMENT};
}
sub debug
{
my $self = shift;
my $arg = shift || "";
if ($self->{DEBUG} and $self->{DEBUG} > 1 )
{
$arg = (caller(1))[3] . ": $arg";
}
print STDERR " " x $self->{INDENT}, "$arg\n"
if $self->{DEBUG};
}
sub warn
{
my $self = shift;
my $arg = shift || "";
print STDERR " " x $self->{INDENT}, "$arg\n"
if $self->{DEBUG};
print STDERR "$arg\n"
if $self->{WARNINGS} && !$self->{DEBUG};
}
=item open_xml(Source => $xml [, %args])
Gives the XSLT object new XML to process. Returns an XML::DOM object
corresponding to the XML.
=over 4
=item base
The base URL to use for opening documents.
=item parser_args
Arguments to pase to the parser.
=back
=cut
sub open_xml
{
my $self = shift;
my $class = ref $self || croak "Not a method call";
my %args = $self->__parse_args(@_);
if ( defined $self->xml_document() && not $self->{XML_PASSED_AS_DOM} )
{
$self->debug("flushing old XML::DOM::Document object...");
$self->xml_document()->dispose;
}
if (ref $args{Source} && UNIVERSAL::isa($args{Source}, 'XML::DOM::Document' ) )
{
$self->{XML_PASSED_AS_DOM} = 1;
}
if ( defined $self->result_document() )
{
$self->debug("flushing result...");
$self->result_document()->dispose();
}
$self->debug("opening xml...");
$args{parser_args} ||= {};
my $xml_document = $self->__open_document(
Source => $args{Source},
base => $self->{XML_BASE},
parser_args => { %{ $self->{PARSER_ARGS} }, %{ $args{parser_args} } },
);
$self->xml_document($xml_document);
$self->{XML_BASE} =
dirname( URI->new_abs( $args{Source}, $self->{XML_BASE} )->as_string )
. '/';
$self->result_document( $self->xml_document()->createDocumentFragment());
}
=item xml_document
Gets and/or sets the XML::DOM object corresponding to the XML document
being processed. The document might be altered during processing.
=cut
sub xml_document
{
my ( $self, $xml_document ) = @_;
if ( defined $xml_document )
{
$self->{XML_DOCUMENT} = $xml_document;
}
return $self->{XML_DOCUMENT};
}
=item open_xsl(Source => $xml, [, %args])
Gives the XSLT object a new stylesheet to use in processing XML.
Returns an XML::DOM object corresponding to the stylesheet. Any
arguments present are passed to the XML::DOM::Parser.
=over 4
=item base
The base URL to use for opening documents.
=item parser_args
Arguments to pase to the parser.
=back
=cut
sub open_xsl
{
my $self = shift;
my $class = ref $self || croak "Not a method call";
my %args = $self->__parse_args(@_);
$self->xsl_document()->dispose
if not $self->{XSL_PASSED_AS_DOM}
and defined $self->xsl_document();
if ( ref $args{Source} && UNIVERSAL::isa($args{Source}, 'XML::DOM::Document' ))
{
$self->{XSL_PASSED_AS_DOM} = 1
}
# open new document # open new document
$self->debug("opening xsl...");
$args{parser_args} ||= {};
my $xsl_document = $self->__open_document(
Source => $args{Source},
base => $self->{XSL_BASE},
parser_args => { %{ $self->{PARSER_ARGS} }, %{ $args{parser_args} } },
);
$self->{ORIG_XSL_DOC} = $xsl_document;
$self->xsl_document($xsl_document);
$self->{XSL_BASE} =
dirname( URI->new_abs( $args{Source}, $self->{XSL_BASE} )->as_string )
. '/';
$self->__preprocess_stylesheet;
}
=item xsl_document
Gets and/or sets the XML::DOM object corresponding to the XSLT document
that is being used for processing, this will be altered during processing
so should not be an object that needs to be reused elsewhere.
=cut
sub xsl_document
{
my ( $self, $xsl_document ) = @_;
if ( defined $xsl_document )
{
$self->{XSL_DOCUMENT} = $xsl_document;
}
return $self->{XSL_DOCUMENT};
}
# Argument parsing with backwards compatibility.
sub __parse_args
{
my $self = shift;
my %args;
if ( @_ % 2 )
{
$args{Source} = shift;
%args = ( %args, @_ );
}
else
{
%args = @_;
if ( not exists $args{Source} )
{
my $name = [ caller(1) ]->[3];
carp
"Argument syntax of call to $name deprecated. See the documentation for $name"
unless $self->use_deprecated($args{use_deprecated})
or exists $deprecation_used{$name};
$deprecation_used{$name} = 1;
%args = ();
$args{Source} = shift;
shift;
%args = ( %args, @_ );
}
}
return %args;
}
# private auxiliary function #
sub __my_tag_compression
{
my ( $tag, $elem ) = @_;
=begin internal_docs
__my_tag_compression__( $tag, $elem )
A function for DOM::XML::setTagCompression to determine the style for printing
of empty tags and empty container tags.
XML::XSLT implements an XHTML-friendly style.
Allow tag to be preceded by a namespace: ([\w\.]+\:){0,1}
<br> -> <br />
or
<myns:hr> -> <myns:hr />
Empty tag list obtained from:
http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd
According to "Appendix C. HTML Compatibility Guidelines",
C.3 Element Minimization and Empty Element Content
Given an empty instance of an element whose content model is not EMPTY
(for example, an empty title or paragraph) do not use the minimized form
(e.g. use <p> </p> and not <p />).
However, the <p> tag is processed like an empty tag here!
Tags allowed:
base meta link hr br param img area input col
Special Case: p (even though it violates C.3)
The tags are matched in order of expected common occurence.
=end internal_docs
=cut
$tag = [ split ':', $tag ]->[1] if index( $tag, ':' ) >= 0;
return 2 if $tag =~ m/^(p|br|img|hr|input|meta|base|link|param|area|col)$/i;
# Print other empty tags like this: <empty></empty>
return 1;
}
# private auxiliary function #
sub __preprocess_stylesheet
{
my $self = $_[0];
$self->debug("preprocessing stylesheet...");
$self->__get_first_element;
$self->__extract_namespaces;
$self->__get_stylesheet;
# Why is this here when __get_first_element does, apparently, the same thing?
# Because, in __get_stylesheet we warp the document.
$self->__expand_xsl_includes;
$self->_top_xsl_node( $self->xsl_document()->getFirstChild );
$self->__extract_top_level_variables;
$self->__add_default_templates;
$self->__cache_templates; # speed optim
$self->__set_xsl_output;
}
sub _top_xsl_node
{
my ( $self, $top_xsl_node ) = @_;
if ( defined $top_xsl_node )
{
$self->{TOP_XSL_NODE} = $top_xsl_node;
}
return $self->{TOP_XSL_NODE};
}
# private auxiliary function #
sub __get_stylesheet
{
my $self = shift;
my $stylesheet;
my $xsl_ns = $self->xsl_ns();
my $xsl = $self->xsl_document();
foreach my $child ( $xsl->getElementsByTagName( '*', 0 ) )
{
my ( $ns, $tag ) = split( ':', $child->getTagName() );
if ( not defined $tag )
{
$tag = $ns;
$ns = $self->default_ns();
}
if ( $tag eq 'stylesheet' || $tag eq 'transform' )
{
if ( my $attributes = $child->getAttributes() )
{
my $version = $attributes->getNamedItem('version');
$self->xslt_version( $version->getNodeValue() ) if $version;
}
$stylesheet = $child;
last;
}
}
if ( !$stylesheet )
{
# stylesheet is actually one complete template!
# put it in a template-element
$stylesheet = $xsl->createElement("${xsl_ns}stylesheet");
my $template = $xsl->createElement("${xsl_ns}template");
$template->setAttribute( 'match', "/" );
my $template_content = $xsl->getElementsByTagName( '*', 0 )->item(0);
$xsl->replaceChild( $stylesheet, $template_content );
$stylesheet->appendChild($template);
$template->appendChild($template_content);
}
$self->xsl_document($stylesheet);
}
sub xslt_version
{
my ( $self, $xslt_version ) = @_;
if ( defined $xslt_version )
{
$self->{XSLT_VERSION} = $xslt_version;
}
return $self->{XSLT_VERSION} ||= '1.0';
}
# private auxiliary function #
sub __get_first_element
{
my ($self) = @_;
my $node = $self->xsl_document()->getFirstChild();
$node = $node->getNextSibling until $node->isa( 'XML::DOM::Element' );
$self->_top_xsl_node($node);
}
# private auxiliary function #
sub __extract_namespaces
{
my ($self) = @_;
my $attr = $self->_top_xsl_node()->getAttributes;
if ( defined $attr )
{
foreach
my $attribute ( $self->_top_xsl_node()->getAttributes->getValues )
{
my ( $pre, $post ) = split( ":", $attribute->getName, 2 );
my $value = $attribute->getValue;
# Take care of namespaces
if ( $pre eq 'xmlns' and not defined $post )
{
$self->default_ns('');
$self->{NAMESPACE}->{ $self->default_ns() }->{namespace} =
$value;
$self->xsl_ns('')
if $value eq NS_XSLT;
$self->debug(
"Namespace `" . $self->default_ns() . "' = `$value'" );
}
elsif ( $pre eq 'xmlns' )
{
$self->{NAMESPACE}->{$post}->{namespace} = $value;
$self->xsl_ns("$post:")
if $value eq NS_XSLT;
$self->debug("Namespace `$post:' = `$value'");
}
else
{
$self->default_ns('');
}
# Take care of versions
if ( $pre eq "version" and not defined $post )
{
$self->{NAMESPACE}->{ $self->default_ns() }->{version} = $value;
$self->debug( "Version for namespace `"
. $self->default_ns()
. "' = `$value'" );
}
elsif ( $pre eq "version" )
{
$self->{NAMESPACE}->{$post}->{version} = $value;
$self->debug("Version for namespace `$post:' = `$value'");
}
}
}
if ( not defined $self->default_ns() )
{
my ($dns) = split( ':', $self->_top_xsl_node()->getTagName );
$self->default_ns($dns);
}
$self->debug( "Default Namespace: `" . $self->default_ns() . "'" );
$self->xsl_ns( $self->default_ns() ) unless $self->xsl_ns();
$self->debug( "XSL Namespace: `" . $self->xsl_ns() . "'" );
# ** FIXME: is this right?
$self->{NAMESPACE}->{ $self->default_ns() }->{namespace} ||= NS_XHTML;
}
=item default_ns
Gets and/or sets the default namespace to be used in the XSL
=cut
sub default_ns
{
my ( $self, $default_ns ) = @_;
if ( defined $default_ns )
{
$self->{DEFAULT_NS} = $default_ns;
}
return exists $self->{DEFAULT_NS} ? $self->{DEFAULT_NS} : undef;
}
sub xsl_ns
{
my ( $self, $prefix ) = @_;
if ( defined $prefix )
{
$prefix .= ':' unless $prefix =~ /:$/;
$self->{XSL_NS} = $prefix;
}
return $self->{XSL_NS};
}
# private auxiliary function #
sub __expand_xsl_includes
{
my $self = shift;
$self->debug("IN INCLUDE");
$self->debug($self->xsl_ns());
foreach my $include_node ( $self->xsl_document() # _top_xsl_node()
->getElementsByTagName( $self->xsl_ns() . "include" ) )
{
my $include_file = $include_node->getAttribute('href');
$self->debug("including - $include_file");
die "include tag carries no selection!"
unless defined $include_file;
my $include_doc;
my $tmp_doc;
eval {
$tmp_doc =
$self->__open_by_filename( $include_file, $self->{XSL_BASE} );
$include_doc = $tmp_doc->getFirstChild();
#$tmp_doc->removeChild($include_doc);
};
die "parsing of $include_file failed: $@"
if $@;
$self->debug("inserting `$include_file'");
#$self->xsl_document()->setOwnerDocument($include_doc);
$include_doc->setOwnerDocument( $self->{ORIG_XSL_DOC} );
$self->xsl_document()->replaceChild( $include_doc, $include_node );
#$include_doc->dispose;
}
}
# private auxiliary function #
sub __extract_top_level_variables
{
my $self = $_[0];
$self->debug("Extracting variables");
foreach my $child ( $self->xsl_document()->getChildNodes() )
{
next unless $child->getNodeType() == ELEMENT_NODE;
my $name = $child->getNodeName();
my ( $ns, $tag ) = split( ':', $name );
$self->debug("$ns $tag");
if ( 1 )
# ( $tag eq '' && $self->xsl_ns() eq '' )
# || $self->xsl_ns() eq $ns )
{
$tag = $ns if $tag eq '';
$self->debug($tag);
if ( $tag eq 'variable' || $tag eq 'param' )
{
my $name = $child->getAttribute("name");
if ( exists $self->{VARIABLES}->{$name} )
{
$self->debug("$tag $name already set to '$self->{VARIABLES}->{$name}'");
}
elsif ($name)
{
$self->debug("got $tag called $name");
my $value = $child->getAttributeNode("select");
if ( !defined $value )
{
if ( $child->getChildNodes()->getLength() )
{
my $result = XML::DOM::DocumentFragment->new();
#$self->xml_document()->createDocumentFragment;
$self->_evaluate_template( $child,
$self->xml_document(),
'',
$result );
$value = $self->__string__($result);
$result->dispose();
}
}
else
{
$value = $value->getValue();
if ( $value =~ /'(.*)'/ )
{
$value = $1;
}
}
unless ( !defined $value )
{
$self->debug("Setting $tag `$name' = `$value'");
$self->{VARIABLES}->{$name} = $value;
}
}
else
{
# Required, so we die (http://www.w3.org/TR/xslt#variables)
die "$tag tag carries no name!";
}
}
}
}
}
# private auxiliary function #
sub __add_default_templates
{
my $self = $_[0];
my $doc = $self->_top_xsl_node()->getOwnerDocument;
# create template for '*' and '/'
my $elem_template = $doc->createElement( $self->xsl_ns() . "template" );
$elem_template->setAttribute( 'match', '*|/' );
# <xsl:apply-templates />
$elem_template->appendChild(
$doc->createElement( $self->xsl_ns() . "apply-templates" ) );
# create template for 'text()' and '@*'
my $attr_template = $doc->createElement( $self->xsl_ns() . "template" );
$attr_template->setAttribute( 'match', 'text()|@*' );
# <xsl:value-of select="." />
$attr_template->appendChild(
$doc->createElement( $self->xsl_ns() . "value-of" ) );
$attr_template->getFirstChild->setAttribute( 'select', '.' );
# create template for 'processing-instruction()' and 'comment()'
my $pi_template = $doc->createElement( $self->xsl_ns() . "template" );
$pi_template->setAttribute( 'match', 'processing-instruction()|comment()' );
$self->debug("adding default templates to stylesheet");
# add them to the stylesheet
$self->xsl_document()->insertBefore( $pi_template, $self->_top_xsl_node );
$self->xsl_document()
->insertBefore( $attr_template, $self->_top_xsl_node() );
$self->xsl_document()
->insertBefore( $elem_template, $self->_top_xsl_node() );
}
=item
Returns the templates from the XSL document.
=cut
sub templates
{
my ( $self, $templates ) = @_;
if ( defined $templates )
{
$self->{TEMPLATE} = $templates;
}
$self->debug("templates() called from : " . (caller(1))[3]);
unless ( exists $self->{TEMPLATE} )
{
$self->{TEMPLATE} = [];
my $xsld = $self->xsl_document();
my $tag = $self->xsl_ns() . 'template';
$self->debug("getting $tag");
@{ $self->{TEMPLATE} } = $xsld->getElementsByTagName($tag);
}
return wantarray ? @{ $self->{TEMPLATE} } : $self->{TEMPLATE};
}
# private auxiliary function #
sub __cache_templates
{
my $self = $_[0];
# pre-cache template names and matches #
# reversing the template order is much more efficient #
foreach my $template ( reverse $self->templates() )
{
next unless $template->getParentNode();
if ( $template->getParentNode->getTagName =~
/^([\w\.\-]+\:){0,1}(stylesheet|transform|include)/ )
{
my $match = $template->getAttribute('match') || '';
my $name = $template->getAttribute('name') || '';
push( @{ $self->{TEMPLATE_MATCH} }, $match );
push( @{ $self->{TEMPLATE_NAME} }, $name );
}
}
}
=item xsl_output_method
Get or set the <xsl:output method= attribute. Valid arguments are 'html',
'text' and 'xml'
=cut
sub xsl_output_method
{
my ( $self, $method) = @_;
if (defined $method and $method =~ /(?:html|text|xml)/ )
{
$self->{METHOD} = $method;
}
return exists $self->{METHOD} ? $self->{METHOD} : 'xml';
}
# private auxiliary function #
sub __set_xsl_output
{
my $self = $_[0];
# default settings
$self->media_type('text/xml');
# extraction of top-level xsl:output tag
my ($output) =
$self->xsl_document()
->getElementsByTagName( $self->xsl_ns() . "output", 0 );
if ( defined $output )
{
# extraction and processing of the attributes
my $attribs = $output->getAttributes;
my $media = $attribs->getNamedItem('media-type');
my $method = $attribs->getNamedItem('method');
$self->media_type( $media->getNodeValue ) if defined $media;
$self->xsl_output_method($method->getNodeValue) if defined $method;
if ( my $omit = $attribs->getNamedItem('omit-xml-declaration') )
{
if ( $omit->getNodeValue() =~ /^(yes|no)$/ )
{
$self->omit_xml_declaration($1);
}
else
{
# I would say that this should be fatal
# Perhaps there should be a 'strict' option to the constructor
my $m =
qq{Wrong value for attribute "omit-xml-declaration" in\n\t}
. $self->xsl_ns()
. qq{output, should be "yes" or "no"};
$self->warn($m);
}
}
unless ( $self->omit_xml_declaration() )
{
my $output_ver = $attribs->getNamedItem('version');
my $output_enc = $attribs->getNamedItem('encoding');
$self->output_version( $output_ver->getNodeValue )
if defined $output_ver;
$self->output_encoding( $output_enc->getNodeValue )
if defined $output_enc;
if ( not $self->output_version() || not $self->output_encoding() )
{
$self->warn(
qq{Expected attributes "version" and "encoding" in\n\t}
. $self->xsl_ns()
. "output" );
}
}
my $doctype_public = $attribs->getNamedItem('doctype-public');
my $doctype_system = $attribs->getNamedItem('doctype-system');
my $dp = defined $doctype_public ? $doctype_public->getNodeValue : '';
$self->doctype_public($dp);
my $ds = defined $doctype_system ? $doctype_system->getNodeValue : '';
$self->doctype_system($ds);
# cdata-section-elements should only be used if the output type
# is XML but as we are not checking that right now ...
my $cdata_section = $attribs->getNamedItem('cdata-section-elements');
if ( defined $cdata_section )
{
my $cdata_sections = [];
@{$cdata_sections} = split /\s+/, $cdata_section->getNodeValue();
$self->cdata_sections($cdata_sections);
}
}
else
{
$self->debug("Default Output options being used");
}
}
sub omit_xml_declaration
{
my ( $self, $omit_xml_declaration ) = @_;
if ( defined $omit_xml_declaration )
{
if ( $omit_xml_declaration =~ /^(yes|no)$/ )
{
$self->{OMIT_XML_DECL} = ( $1 eq 'yes' );
}
else
{
$self->{OMIT_XML_DECL} = $omit_xml_declaration ? 1 : 0;
}
}
return exists $self->{OMIT_XML_DECL} ? $self->{OMIT_XML_DECL} : 0;
}
=item cdata_sections
Get or set the element names supplied via the cdata-section-elements
attribute (i.e. a space separated list of element names.)
=cut
sub cdata_sections
{
my ( $self, $cdata_sections ) = @_;
if ( defined $cdata_sections )
{
$self->{CDATA_SECTIONS} = $cdata_sections;
}
$self->{CDATA_SECTIONS} = [] unless exists $self->{CDATA_SECTIONS};
return wantarray() ? @{ $self->{CDATA_SECTIONS} } : $self->{CDATA_SECTIONS};
}
sub is_cdata_section
{
my ( $self, $element ) = @_;
my %cdata_sections;
my @cdata_temp = $self->cdata_sections();
@cdata_sections{@cdata_temp} = (1) x @cdata_temp;
my $tagname;
if ( defined $element and ref($element) and ref($element) eq 'XML::DOM' )
{
$tagname = $element->getTagName();
}
else
{
$tagname = $element;
}
# Will need to do namespace checking on this really
return exists $cdata_sections{$tagname} ? 1 : 0;
}
=item output_version
Gets and/or sets the XML version that will be used for the output
(defaults to default_xml_version())
=cut
sub output_version
{
my ( $self, $output_version ) = @_;
if ( defined $output_version )
{
$self->{OUTPUT_VERSION} = $output_version;
}
return exists $self->{OUTPUT_VERSION}
? $self->{OUTPUT_VERSION}
: $self->default_xml_version();
}
sub __get_attribute_sets
{
my ($self) = @_;
my $doc = $self->xsl_document();
my $nsp = $self->xsl_ns();
my $tagname = $nsp . 'attribute-set';
my %inc;
my @included;
foreach my $attribute_set ( $doc->getElementsByTagName( $tagname, 0 ) )
{
my $attribs = $attribute_set->getAttributes();
next unless defined $attribs;
my $name_attr = $attribs->getNamedItem('name');
next unless defined $name_attr;
my $name = $name_attr->getValue();
$self->debug("processing attribute-set $name");
if ( my $uas = $attribs->getNamedItem('use-attribute-sets') )
{
$self->_indent();
$inc{$name} = $uas->getValue();
$self->debug("Attribute set $name includes $inc{$name}");
push @included, $name;
$self->_outdent();
}
my $attr_set = {};
my $tagname = $nsp . 'attribute';
foreach
my $attribute ( $attribute_set->getElementsByTagName( $tagname, 0 ) )
{
my $attribs = $attribute->getAttributes();
next unless defined $attribs;
my $name_attr = $attribs->getNamedItem('name');
next unless defined $name_attr;
my $attr_name = $name_attr->getValue();
$self->debug("Processing attribute $attr_name");
if ($attr_name)
{
my $result = $self->xml_document()->createDocumentFragment();
$self->_evaluate_template( $attribute, $self->xml_document(),
'/', $result ); # might need variables
my $value =
$self->fix_attribute_value( $self->__string__($result) );
$attr_set->{$attr_name} = $value;
$result->dispose();
$self->debug("Adding attribute $attr_name with value $value");
}
}
$self->__attribute_set_( $name, $attr_set );
}
foreach my $as (@included )
{
$self->_indent();
$self->debug("adding attributes from $inc{$as} to $as");
my %fix = (%{$self->__attribute_set_($as)},%{$self->__attribute_set_($inc{$as})});
$self->__attribute_set_($as,\%fix);
$self->_outdent();
}
}
# Accessor for attribute sets
sub __attribute_set_
{
my ( $self, $name, $attr_hash ) = @_;
if ( defined $attr_hash && defined $name )
{
if ( exists $self->{ATTRIBUTE_SETS}->{$name} )
{
%{$self->{ATTRIBUTE_SETS}->{$name}} =
( %{$self->{ATTRIBUTE_SETS}->{$name}}, %{$attr_hash});
}
else
{
$self->{ATTRIBUTE_SETS}->{$name} = $attr_hash;
}
}
return defined $name
&& exists $self->{ATTRIBUTE_SETS}->{$name}
? $self->{ATTRIBUTE_SETS}->{$name}
: undef;
}
sub open_project
{
my $self = shift;
my $xml = shift;
my $xsl = shift;
my ( $xmlflag, $xslflag, %args ) = @_;
carp "open_project is deprecated."
unless $self->use_deprecated()
or exists $deprecation_used{open_project};
$deprecation_used{open_project} = 1;
$self->debug("opening project:");
$self->_indent();
$self->open_xml( $xml, %args );
$self->open_xsl( $xsl, %args );
$self->debug("done...");
$self->_outdent();
}
=item transform(Source => $xml [, %args])
Processes the given XML through the stylesheet. Returns an XML::DOM
object corresponding to the transformed XML. Any arguments present
are passed to the XML::DOM::Parser.
=cut
sub transform
{
my $self = shift;
if ( keys %{$self->{VARIABLES}} )
{
$self->debug("Adding variables");
push @_,'variables', $self->{VARIABLES};
}
my %topvariables = $self->__parse_args(@_);
$self->debug("transforming document:");
$self->_indent();
$self->open_xml(%topvariables);
$self->debug("done...");
$self->_outdent();
# The _get_attribute_set needs an open XML document
$self->_indent();
$self->__get_attribute_sets();
$self->_outdent();
$self->debug("processing project:");
$self->_indent();
$self->process(%topvariables);
$self->debug("done!");
$self->_outdent();
$self->result_document()->normalize();
return $self->result_document();
}
=item process(%variables)
Processes the previously loaded XML through the stylesheet using the
variables set in the argument.
=cut
sub process
{
my ( $self, %topvariables ) = @_;
$self->debug("processing project:");
$self->_indent();
my $root_template = $self->_match_template( "match", '/', 1, '' );
$self->debug(join ' ', keys %topvariables);
%topvariables = (
defined $topvariables{variables} ? %{$topvariables{variables}} : (),
defined $self->{VARIABLES}
&& ref $self->{VARIABLES}
&& ref $self->{VARIABLES} eq 'ARRAY' ? @{ $self->{VARIABLES} } : ()
);
$self->debug(join ' ', keys %topvariables);
$self->_evaluate_template(
$root_template, # starting template: the root template
$self->xml_document(),
'', # current XML selection path: the root
$self->result_document(), # current result tree node: the root
{ () }, # current known variables: none
\%topvariables # previously known variables: top level variables
);
$self->debug("done!");
$self->_outdent();
}
# Handles deprecations.
sub AUTOLOAD
{
my $self = shift;
my $type = ref($self) || croak "Not a method call";
my $name = $AUTOLOAD;
$name =~ s/.*://;
my %deprecation = (
'output_string' => 'toString',
'result_string' => 'toString',
'output' => 'toString',
'result' => 'toString',
'result_mime_type' => 'media_type',
'output_mime_type' => 'media_type',
'result_tree' => 'to_dom',
'output_tree' => 'to_dom',
'transform_document' => 'transform',
'process_project' => 'process'
);
if ( exists $deprecation{$name} )
{
carp "$name is deprecated. Use $deprecation{$name}"
unless $self->use_deprecated()
or exists $deprecation_used{$name};
$deprecation_used{$name} = 1;
eval qq{return \$self->$deprecation{$name}(\@_)};
}
else
{
croak "$name: No such method name";
}
}
sub _my_print_text
{
my ( $self, $FILE ) = @_;
if ( UNIVERSAL::isa( $self, "XML::DOM::CDATASection" ) )
{
$FILE->print( $self->getData() );
}
else
{
$FILE->print( XML::DOM::encodeText( $self->getData(), "<&" ) );
}
}
=item toString
Returns the result of transforming the XML with the stylesheet as a
string.
=cut
sub toString
{
my $self = $_[0];
local $^W;
local *XML::DOM::Text::print = \&_my_print_text;
my $string = '';
if (defined $self->result_document() )
{
$string = $self->result_document()->toString();
}
return $string;
}
=item to_dom
Returns the result of transforming the XML with the stylesheet as an
XML::DOM object.
=cut
sub to_dom
{
my ($self) = @_;
my $document = XML::DOM::Document->new();
my $dom = $self->result_document()->cloneNode(1);
$dom->setOwnerDocument($document);
$document->appendChild($dom);
return $document;
}
=item media_type
Returns the media type (aka mime type) of the object.
=cut
sub media_type
{
my ( $self, $media_type ) = @_;
if ( defined $media_type )
{
$self->{MEDIA_TYPE} = $media_type;
}
return $self->{MEDIA_TYPE};
}
sub print_output
{
my ( $self, $file, $mime ) = @_;
$file ||= ''; # print to STDOUT by default
$mime = 1 unless defined $mime;
# print mime-type header etc by default
# $self->{RESULT_DOCUMENT}->printToFileHandle (\*STDOUT);
# or $self->{RESULT_DOCUMENT}->print (\*STDOUT); ???
# exit;
carp "print_output is deprecated. Use serve."
unless $self->use_deprecated()
or exists $deprecation_used{print_output};
$deprecation_used{print_output} = 1;
if ($mime)
{
print "Content-type: " . $self->media_type() . "\n\n";
if ( $self->xsl_output_method =~ /(?:xml|html)/ )
{
unless ( $self->omit_xml_declaration() )
{
print $self->xml_declaration(), "\n";
}
}
if ( my $doctype = $self->doctype() )
{
print "$doctype\n";
}
}
if ($file)
{
if ( ref( \$file ) eq 'SCALAR' )
{
print $file $self->output_string, "\n";
}
else
{
if ( open( FILE, ">$file" ) )
{
print FILE $self->output_string, "\n";
if ( !close(FILE) )
{
die("Error writing $file: $!. Nothing written...\n");
}
}
else
{
die("Error opening $file: $!. Nothing done...\n");
}
}
}
else
{
print $self->output_string, "\n";
}
}
=item print_result
An alias for print_output
=cut
*print_result = *print_output;
sub doctype
{
my ($self) = @_;
my $doctype = "";
if ( $self->doctype_public() || $self->doctype_system() )
{
my $root_name =
$self->result_document()->getElementsByTagName( '*', 0 )->item(0)
->getTagName;
if ( $self->doctype_public() )
{
$doctype =
qq{<!DOCTYPE $root_name PUBLIC "}
. $self->doctype_public() . qq{" "}
. $self->doctype_system() . qq{">};
}
else
{
$doctype =
qq{<!DOCTYPE $root_name SYSTEM "}
. $self->doctype_system() . qq{">};
}
}
$self->debug("returning doctype of $doctype");
return $doctype;
}
=item dispose
Executes the C<dispose> method on each XML::DOM object.
=cut
sub dispose
{
$_[0]->result_document()->dispose if ( defined $_[0]->result_document() );
if ( (not defined $_[0]->{XML_PASSED_AS_DOM} )
and defined $_[0]->xml_document() )
{
$_[0]->xml_document()->dispose;
}
if ( (not defined $_[0]->{XSL_PASSED_AS_DOM} )
and defined $_[0]->xsl_document() )
{
$_[0]->xsl_document()->dispose;
}
$_[0]->_top_xsl_node()->dispose() if defined $_[0]->_top_xsl_node();
foreach my $topkey ( %{$_[0]} )
{
$_[0]->{$topkey} = undef if defined $topkey;
}
$_[0] = undef;
}
######################################################################
# PRIVATE DEFINITIONS
sub __open_document
{
my $self = shift;
my %args = @_;
%args = ( %{ $self->{PARSER_ARGS} }, %args );
my $doc;
$self->debug("opening document");
eval {
my $ref = ref( $args{Source} );
if (
!$ref
&& length $args{Source} < 255 && index("\n",$args{Source}) == -1
&& ( -f $args{Source}
|| lc( substr( $args{Source}, 0, 5 ) ) eq 'http:'
|| lc( substr( $args{Source}, 0, 6 ) ) eq 'https:'
|| lc( substr( $args{Source}, 0, 4 ) ) eq 'ftp:'
|| lc( substr( $args{Source}, 0, 5 ) ) eq 'file:' )
)
{
# Filename
$self->debug("Opening URL");
$doc = $self->__open_by_filename( $args{Source}, $args{base} );
}
elsif ( !$ref )
{
# String
$self->debug("Opening String");
$doc = $self->{PARSER}->parse( $args{Source} );
}
elsif ( $ref eq "SCALAR" )
{
# Stringref
$self->debug("Opening Stringref");
$doc = $self->{PARSER}->parse( ${ $args{Source} } );
}
elsif ( $args{Source}->isa( 'XML::DOM::Document' ) )
{
# DOM object
$self->debug("Opening XML::DOM");
$doc = $args{Source};
}
elsif ( $ref eq "GLOB" )
{ # This is a file glob
$self->debug("Opening GLOB");
my $ioref = *{ $args{Source} }{IO};
$doc = $self->{PARSER}->parse($ioref);
}
elsif ( UNIVERSAL::isa( $args{Source}, 'IO::Handle' ) )
{ # IO::Handle
$self->debug("Opening IO::Handle");
$doc = $self->{PARSER}->parse( $args{Source} );
}
else
{
$doc = undef;
}
};
die "Error while parsing: $@\n" . $args{Source} if $@;
return $doc;
}
# private auxiliary function #
sub __open_by_filename
{
my ( $self, $filename, $base ) = @_;
my $doc;
# ** FIXME: currently reads the whole document into memory
# might not be avoidable
# LWP should be able to deal with files as well as links
$ENV{DOMAIN} ||= "example.com"; # hide complaints from Net::Domain
my $file = get( URI->new_abs( $filename, $base ) );
return $self->{PARSER}->parse( $file, defined $self->{PARSER_ARGS} ? %{ $self->{PARSER_ARGS} } : undef );
}
sub _match_template
{
my ( $self, $attribute_name, $select_value, $xml_count, $xml_selection_path,
$mode )
= @_;
$mode ||= "";
my $template = "";
my @template_matches = ();
$self->debug(
qq{matching template for "$select_value" with count $xml_count\n\t}
. qq{and path "$xml_selection_path":} );
if ( $attribute_name eq "match" && ref $self->{TEMPLATE_MATCH} )
{
push @template_matches, @{ $self->{TEMPLATE_MATCH} };
}
elsif ( $attribute_name eq "name" && ref $self->{TEMPLATE_NAME} )
{
push @template_matches, @{ $self->{TEMPLATE_NAME} };
}
# note that the order of @template_matches is the reverse of $self->{TEMPLATE}
my $count = @template_matches;
$self->debug("matches: @template_matches");
foreach my $original_match (@template_matches)
{
# templates with no match or name or with both simultaniuously
# have no $template_match value
if ($original_match)
{
my $full_match = $original_match;
# multipe match? (for example: match="*|/")
while ( $full_match =~ s/^(.+?)\|// )
{
my $match = $1;
if (
&__template_matches__(
$match, $select_value,
$xml_count, $xml_selection_path
)
)
{
$self->debug(
qq{ found #$count with "$match" in "$original_match"});
$template = ( $self->templates() )[ $count - 1 ];
return $template;
# last;
}
}
# last match?
if ( !$template )
{
if (
&__template_matches__(
$full_match, $select_value,
$xml_count, $xml_selection_path
)
)
{
$self->debug(
qq{ found #$count with "$full_match" in "$original_match"}
);
$template = ( $self->templates() )[ $count - 1 ];
return $template;
# last;
}
else
{
$self->debug(qq{ #$count "$original_match" did not match});
}
}
}
$count--;
}
if ( !$template )
{
$self->warn(qq{No template matching `$xml_selection_path' found !!});
}
return $template;
}
# auxiliary function #
sub __template_matches__
{
my ( $template, $select, $count, $path ) = @_;
my $nocount_path = $path;
$nocount_path =~ s/\[.*?\]//g;
if ( ( $template eq $select )
|| ( $template eq $path )
|| ( $template eq "$select\[$count\]" )
|| ( $template eq "$path\[$count\]" ) )
{
# perfect match or path ends with templates match
#print "perfect match","\n";
return "True";
}
elsif (
( $template eq substr( $path, -length($template) ) )
|| ( $template eq substr( $nocount_path, -length($template) ) )
|| ( "$template\[$count\]" eq substr( $path, -length($template) ) )
|| (
"$template\[$count\]" eq substr( $nocount_path, -length($template) )
)
)
{
# template matches tail of path matches perfectly
#print "perfect tail match","\n";
return "True";
}
elsif ( $select =~ /\[\s*(\@.*?)\s*=\s*(.*?)\s*\]$/ )
{
# match attribute test
my $attribute = $1;
my $value = $2;
return ""; # False, no test evaluation yet #
}
elsif ( $select =~ /\[\s*(.*?)\s*=\s*(.*?)\s*\]$/ )
{
# match test
my $element = $1;
my $value = $2;
return ""; # False, no test evaluation yet #
}
elsif ( $select =~ /(\@\*|\@[\w\.\-\:]+)$/ )
{
# match attribute
my $attribute = $1;
#print "attribute match?\n";
return ( ( $template eq '@*' )
|| ( $template eq $attribute )
|| ( $template eq "\@*\[$count\]" )
|| ( $template eq "$attribute\[$count\]" ) );
}
elsif ( $select =~ /(\*|[\w\.\-\:]+)$/ )
{
# match element
my $element = $1;
#print "element match?\n";
return ( ( $template eq "*" )
|| ( $template eq $element )
|| ( $template eq "*\[$count\]" )
|| ( $template eq "$element\[$count\]" ) );
}
else
{
return ""; # False #
}
}
sub _evaluate_test
{
my ( $self, $test, $current_xml_node, $current_xml_selection_path,
$variables )
= @_;
$self->debug("Doing test $test");
if ( $test =~ /^(.+)\/\[(.+)\]$/ )
{
my $path = $1;
$test = $2;
$self->debug("evaluating test $test at path $path:");
$self->_indent();
my $node =
$self->_get_node_set( $path, $self->xml_document(),
$current_xml_selection_path, $current_xml_node, $variables );
if (@$node)
{
$current_xml_node = $$node[0];
}
else
{
return "";
}
$self->_outdent();
}
else
{
$self->debug("evaluating path or test $test:");
my $node =
$self->_get_node_set( $test, $self->xml_document(),
$current_xml_selection_path, $current_xml_node, $variables,
"silent" );
$self->_indent();
if (@$node)
{
$self->debug("path exists!");
return "true";
}
else
{
$self->debug("not a valid path, evaluating as test");
}
$self->_outdent();
}
$self->_indent();
my $result =
$self->__evaluate_test__( $test, $current_xml_selection_path,
$current_xml_node, $variables );
$self->debug("test evaluates @{[ $result ? 'true': 'false']}");
$self->_outdent();
return $result;
}
sub _evaluate_template
{
my ( $self, $template, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
$self->debug( qq{evaluating template content with current path }
. qq{"$current_xml_selection_path": } );
$self->_indent();
die "No Template"
unless defined $template && ref $template;
$template->normalize;
foreach my $child ( $template->getChildNodes )
{
my $ref = ref $child;
$self->debug("$ref");
$self->_indent();
my $node_type = $child->getNodeType;
if ( $node_type == ELEMENT_NODE )
{
$self->_evaluate_element( $child, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
elsif ( $node_type == TEXT_NODE )
{
my $value = $child->getNodeValue;
if ( length($value) and $value !~ /^[\x20\x09\x0D\x0A]+$/s )
{
$self->_add_node( $child, $current_result_node );
}
}
elsif ( $node_type == CDATA_SECTION_NODE )
{
my $text = $self->xml_document()->createTextNode( $child->getData );
$self->_add_node( $text, $current_result_node );
}
elsif ( $node_type == ENTITY_REFERENCE_NODE )
{
$self->_add_node( $child, $current_result_node );
}
elsif ( $node_type == DOCUMENT_TYPE_NODE )
{
# skip #
$self->debug("Skipping Document Type node...");
}
elsif ( $node_type == COMMENT_NODE )
{
# skip #
$self->debug("Skipping Comment node...");
}
else
{
$self->warn(
"evaluate-template: Dunno what to do with node of type $ref !!!\n\t"
. "($current_xml_selection_path)" );
}
$self->_outdent();
}
$self->debug("done!");
$self->_outdent();
}
sub _add_node
{
my ( $self, $node, $parent, $deep, $owner ) = @_;
$owner ||= $self->xml_document();
my $what = defined $deep ? 'deep' : 'non-deep';
$self->debug("adding node ($what)..");
$node = $node->cloneNode($deep);
$node->setOwnerDocument($owner);
if ( $node->getNodeType == ATTRIBUTE_NODE )
{
$parent->setAttributeNode($node);
}
else
{
$parent->appendChild($node);
}
}
sub _apply_templates
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
my $children;
my $params = {};
my $newvariables = defined $variables ? {%$variables} : {};
my $select = $xsl_node->getAttribute('select');
if ( $select =~ /\$/ and defined $variables )
{
# replacing occurences of variables:
foreach my $varname ( keys(%$variables) )
{
$self->debug("Applying variable $varname");
$select =~ s/[^\\]\$$varname/$$variables{$varname}/g;
}
}
if ($select)
{
$self->debug(
qq{applying templates on children select of "$current_xml_selection_path":}
);
$children =
$self->_get_node_set( $select, $self->xml_document(),
$current_xml_selection_path, $current_xml_node, $variables );
}
else
{
$self->debug(
qq{applying templates on all children of "$current_xml_selection_path":}
);
$children = [ $current_xml_node->getChildNodes ];
}
$self->_process_with_params( $xsl_node,
$current_xml_node,
$current_xml_selection_path,
$variables,
$params );
# process xsl:sort here
$self->_indent();
my $count = 1;
foreach my $child (@$children)
{
my $node_type = $child->getNodeType;
if ( $node_type == DOCUMENT_TYPE_NODE )
{
# skip #
$self->debug("Skipping Document Type node...");
}
elsif ( $node_type == DOCUMENT_FRAGMENT_NODE )
{
# skip #
$self->debug("Skipping Document Fragment node...");
}
elsif ( $node_type == NOTATION_NODE )
{
# skip #
$self->debug("Skipping Notation node...");
}
else
{
my $newselect = "";
my $newcount = $count;
if ( !$select || ( $select eq '.' ) )
{
if ( $node_type == ELEMENT_NODE )
{
$newselect = $child->getTagName;
}
elsif ( $node_type == ATTRIBUTE_NODE )
{
$newselect = "@$child->getName";
}
elsif (( $node_type == TEXT_NODE )
|| ( $node_type == ENTITY_REFERENCE_NODE ) )
{
$newselect = "text()";
}
elsif ( $node_type == PROCESSING_INSTRUCTION_NODE )
{
$newselect = "processing-instruction()";
}
elsif ( $node_type == COMMENT_NODE )
{
$newselect = "comment()";
}
else
{
my $ref = ref $child;
$self->debug("Unknown node encountered: `$ref'");
}
}
else
{
$newselect = $select;
if ( $newselect =~ s/\[(\d+)\]$// )
{
$newcount = $1;
}
}
$self->_select_template(
$child, $newselect,
$newcount, $current_xml_node,
$current_xml_selection_path, $current_result_node,
$newvariables, $params
);
}
$count++;
}
$self->_indent();
}
sub _for_each
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
my $ns = $self->xsl_ns();
my $select = $xsl_node->getAttribute('select')
|| die "No `select' attribute in for-each element";
if ( $select =~ /\$/ )
{
# replacing occurences of variables:
foreach my $varname ( keys(%$variables) )
{
$select =~ s/[^\\]\$$varname/$$variables{$varname}/g;
}
}
if ( defined $select )
{
$self->debug(
qq{applying template for each child $select of "$current_xml_selection_path":}
);
my $children = $self->_get_node_set( $select,
$self->xml_document(),
$current_xml_selection_path,
$current_xml_node, $variables );
my $sort = $xsl_node->getElementsByTagName("$ns:sort",0);
if ( my $nokeys = $sort->getLength() )
{
$self->debug("going to sort with $nokeys");
}
$self->_indent();
my $count = 1;
foreach my $child (@$children)
{
my $node_type = $child->getNodeType;
if ( $node_type == DOCUMENT_TYPE_NODE )
{
# skip #
$self->debug("Skipping Document Type node...");
}
elsif ( $node_type == DOCUMENT_FRAGMENT_NODE )
{
# skip #
$self->debug("Skipping Document Fragment node...");
}
elsif ( $node_type == NOTATION_NODE )
{
# skip #
$self->debug("Skipping Notation node...");
}
else
{
$self->_evaluate_template(
$xsl_node,
$child,
"$current_xml_selection_path/$select\[$count\]",
$current_result_node,
$variables,
$oldvariables
);
}
$count++;
}
$self->_outdent();
}
else
{
$self->warn(qq%expected attribute "select" in <${ns}for-each>%);
}
}
sub _select_template
{
my ( $self, $child, $select, $count, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables )
= @_;
my $ref = ref $child;
$self->debug(
qq{selecting template $select for child type $ref of "$current_xml_selection_path":}
);
$self->_indent();
foreach my $select_part ( split /\|/, $select )
{
my $child_xml_selection_path = "$current_xml_selection_path/$select_part";
my $template =
$self->_match_template( "match", $select_part, $count,
$child_xml_selection_path );
if ($template)
{
$self->_evaluate_template( $template, $child,
"$child_xml_selection_path\[$count\]",
$current_result_node, $variables, $oldvariables );
}
else
{
$self->debug("skipping template selection...");
}
}
$self->_outdent();
}
sub _evaluate_element
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
my ( $ns, $xsl_tag ) = split( ':', $xsl_node->getTagName );
if ( not defined $xsl_tag )
{
$xsl_tag = $ns;
$ns = $self->default_ns();
}
else
{
$ns .= ':';
}
$self->debug(
qq{evaluating element `$xsl_tag' from `$current_xml_selection_path': });
$self->_indent();
if ( $ns eq $self->xsl_ns() )
{
my @attributes = $xsl_node->getAttributes->getValues;
$self->debug(qq{This is an xsl tag});
if ( $xsl_tag eq 'apply-templates' )
{
$self->_apply_templates( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
elsif ( $xsl_tag eq 'attribute' )
{
$self->_attribute( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
elsif ( $xsl_tag eq 'call-template' )
{
$self->_call_template( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
elsif ( $xsl_tag eq 'choose' )
{
$self->_choose( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
elsif ( $xsl_tag eq 'comment' )
{
$self->_comment( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
elsif ( $xsl_tag eq 'copy' )
{
$self->_copy( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
elsif ( $xsl_tag eq 'copy-of' )
{
$self->_copy_of( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables );
}
elsif ( $xsl_tag eq 'element' )
{
$self->_element( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
elsif ( $xsl_tag eq 'for-each' )
{
$self->_for_each( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
elsif ( $xsl_tag eq 'if' )
{
$self->_if( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
# } elsif ($xsl_tag eq 'output') {
}
elsif ( $xsl_tag eq 'param' )
{
$self->_variable( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables, 1 );
}
elsif ( $xsl_tag eq 'processing-instruction' )
{
$self->_processing_instruction( $xsl_node, $current_result_node );
}
elsif ( $xsl_tag eq 'text' )
{
$self->_text( $xsl_node, $current_result_node );
}
elsif ( $xsl_tag eq 'value-of' )
{
$self->_value_of( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables );
}
elsif ( $xsl_tag eq 'variable' )
{
$self->_variable( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables, 0 );
}
elsif ( $xsl_tag eq 'sort' )
{
$self->_sort( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables, 0 );
}
elsif ( $xsl_tag eq 'fallback' )
{
$self->_fallback( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables, 0 );
}
elsif ( $xsl_tag eq 'attribute-set' )
{
$self->_attribute_set( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables, 0 );
}
else
{
$self->_add_and_recurse( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
}
else
{
$self->debug( $ns . " does not match " . $self->xsl_ns() );
# not entirely sure if this right but the spec is a bit vague
if ( $self->is_cdata_section($xsl_tag) )
{
$self->debug("This is a CDATA section element");
$self->_add_cdata_section( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
else
{
$self->debug("This is a literal element");
$self->_check_attributes_and_recurse( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
}
$self->_outdent();
}
sub _add_cdata_section
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
my $node = $self->xml_document()->createElement( $xsl_node->getTagName );
my $cdata = '';
foreach my $child_node ( $xsl_node->getChildNodes() )
{
if ( $child_node->can('asString') )
{
$cdata .= $child_node->asString();
}
else
{
$cdata .= $child_node->getNodeValue();
}
}
$node->addCDATA($cdata);
$current_result_node->appendChild($node);
}
sub _add_and_recurse
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
# the addition is commented out to prevent unknown xsl: commands to be printed in the result
$self->_add_node( $xsl_node, $current_result_node );
$self->_evaluate_template( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables ); #->getLastChild);
}
sub _check_attributes_and_recurse
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
$self->_add_node( $xsl_node, $current_result_node );
$self->_attribute_value_of(
$current_result_node->getLastChild, $current_xml_node,
$current_xml_selection_path, $variables
);
$self->_evaluate_template( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node->getLastChild,
$variables, $oldvariables );
}
sub _element
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
my $name = $xsl_node->getAttribute('name');
$self->debug(qq{inserting Element named "$name":});
$self->_indent();
if ( defined $name )
{
my $result = $self->xml_document()->createElement($name);
$self->_evaluate_template( $xsl_node, $current_xml_node,
$current_xml_selection_path, $result, $variables, $oldvariables );
$self->_apply_attribute_set($xsl_node,$result);
$current_result_node->appendChild($result);
}
else
{
$self->warn(
q{expected attribute "name" in <} . $self->xsl_ns() . q{element>} );
}
$self->_outdent();
}
sub _apply_attribute_set
{
my ( $self,$xsl_node, $output_node) = @_;
my $attr_set = $xsl_node->getAttribute('use-attribute-sets');
if ($attr_set)
{
$self->_indent();
my $set_name = $attr_set;
if ( my $set = $self->__attribute_set_($set_name) )
{
$self->debug("Adding attribute-set '$set_name'");
foreach my $attr_name ( keys %{$set} )
{
$self->debug(
"Adding attribute $attr_name ->" . $set->{$attr_name} );
$output_node->setAttribute( $attr_name, $set->{$attr_name} );
}
}
$self->_outdent();
}
}
{
######################################################################
# Auxiliary package for disable-output-escaping
######################################################################
package XML::XSLT::DOM::TextDOE;
use vars qw( @ISA );
@ISA = qw( XML::DOM::Text );
sub print
{
my ( $self, $FILE ) = @_;
$FILE->print( $self->getData );
}
}
sub _value_of
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables )
= @_;
my $select = $xsl_node->getAttribute('select');
# Need to determine here whether the value is an XPath expression
# and act accordingly
my $xml_node;
if ( defined $select )
{
$xml_node = $self->_get_node_set( $select,
$self->xml_document(),
$current_xml_selection_path,
$current_xml_node,
$variables );
$self->debug("stripping node to text:");
$self->_indent();
my $text = '';
$text = $self->__string__( $xml_node->[0] ) if @{$xml_node};
$self->_outdent();
if ( $text ne '' )
{
my $node = $self->xml_document()->createTextNode($text);
if ( $xsl_node->getAttribute('disable-output-escaping') eq 'yes' )
{
$self->debug("disabling output escaping");
bless $node, 'XML::XSLT::DOM::TextDOE';
}
$self->_move_node( $node, $current_result_node );
}
else
{
$self->debug("nothing left..");
}
}
else
{
$self->warn( qq{expected attribute "select" in <}
. $self->xsl_ns()
. q{value-of>} );
}
}
sub __strip_node_to_text__
{
my ( $self, $node ) = @_;
my $result = "";
my $node_type = $node->getNodeType;
if ( $node_type == TEXT_NODE )
{
$result = $node->getData;
}
elsif (( $node_type == ELEMENT_NODE )
|| ( $node_type == DOCUMENT_FRAGMENT_NODE ) )
{
$self->_indent();
foreach my $child ( $node->getChildNodes )
{
$result .= &__strip_node_to_text__( $self, $child );
}
$self->_outdent();
}
return $result;
}
sub __string__
{
my ( $self, $node, $depth ) = @_;
my $result = "";
if ( defined $node )
{
my $ref = ( ref($node) || "not a reference" );
$self->debug("stripping child nodes ($ref):");
$self->_indent();
if ( $ref eq "ARRAY" )
{
return $self->__string__( $$node[0], $depth );
}
else
{
my $node_type = $node->getNodeType;
if ( ( $node_type == ELEMENT_NODE )
|| ( $node_type == DOCUMENT_FRAGMENT_NODE )
|| ( $node_type == DOCUMENT_NODE ) )
{
foreach my $child ( $node->getChildNodes )
{
$result .= &__string__( $self, $child, 1 );
}
}
elsif ( $node_type == ATTRIBUTE_NODE )
{
$result .= $node->getValue;
}
elsif (( $node_type == TEXT_NODE )
|| ( $node_type == CDATA_SECTION_NODE )
|| ( $node_type == ENTITY_REFERENCE_NODE ) )
{
$result .= $node->getData;
}
elsif (
!$depth
&& ( ( $node_type == PROCESSING_INSTRUCTION_NODE )
|| ( $node_type == COMMENT_NODE ) )
)
{
$result .= $node->getData; # COM,PI - only in 'top-level' call
}
else
{
# just to be consistent
$self->warn("Can't get string-value for node of type $ref !");
}
}
$self->debug(qq{ "$result"});
$self->_outdent();
}
else
{
$self->debug(" no result");
}
return $result;
}
sub _move_node
{
my ( $self, $node, $parent ) = @_;
$self->debug("moving node..");
$parent->appendChild($node);
}
sub _get_node_set
{
my ( $self, $path, $root_node, $current_path, $current_node, $variables,
$silent )
= @_;
$current_path ||= "/";
$current_node ||= $root_node;
$silent ||= 0;
$self->{VARIABLES} ||= {};
$variables ||= {};
## JNS name() stuff here ?
#
%{$variables} = (%{$self->{VARIABLES}}, %{$variables});
$self->debug(qq{getting node-set "$path" from "$current_path"});
$self->_indent();
# expand abbriviated syntax
$path =~ s/current\(\s*\)/./g;
$path =~ s/\@/attribute\:\:/g;
$path =~ s/\.\./parent\:\:node\(\)/g;
$path =~ s/\./self\:\:node\(\)/g;
$path =~ s/\/\//\/descendant\-or\-self\:\:node\(\)\//g;
#$path =~ s/\/[^\:\/]*?\//attribute::/g;
if ( $path =~ /^\$([\w\.\-]+)$/ )
{
my $varname = $1;
$self->debug("looking for variable $varname");
$self->debug(join ' ', keys %{$variables});
my $var = $$variables{$varname};
if ( defined $var )
{
if ( ref( $$variables{$varname} ) eq 'ARRAY' )
{
# node-set array-ref
return $$variables{$varname};
}
elsif ( ref( $$variables{$varname} ) eq 'XML::DOM::NodeList' )
{
# node-set nodelist
return [ @{ $$variables{$varname} } ];
}
elsif (
ref( $$variables{$varname} ) eq 'XML::DOM::DocumentFragment' )
{
# node-set documentfragment
return [ $$variables{$varname}->getChildNodes ];
}
else
{
# string or number?
return [ $self->xml_document()
->createTextNode( $$variables{$varname} ) ];
}
}
else
{
# var does not exist
return [];
}
}
elsif ( $path eq $current_path || $path eq 'self::node()' )
{
$self->debug("direct hit!");
return [$current_node];
}
else
{
# open external documents first #
if ( $path =~
/^\s*document\s*\(["'](.*?)["']\s*(,\s*(.*)\s*){0,1}\)\s*(.*)$/ )
{
my $filename = $1;
my $sec_arg = $3;
$path = ( $4 || "" );
$self->debug(qq{external selection ("$filename")!});
if ($sec_arg)
{
$self->warn("Ignoring second argument of $path");
}
($root_node) =
$self->__open_by_filename( $filename, $self->{XSL_BASE} );
}
my $return_nodes = [];
foreach my $path_part ( split(/\|/, $path ) )
{
$self->debug("path_part: $path_part");
if ( $path_part =~ /^\// )
{
# start from the root #
$current_node = $root_node;
}
elsif ( $path_part =~ /^self\:\:node\(\)\// )
{ #'#"#'#"
# remove preceding dot from './etc', which is expanded to 'self::node()'
# at the top of this subroutine #
$path_part =~ s/^self\:\:node\(\)//;
}
else
{
# to facilitate parsing, precede path with a '/' #
$path_part = "/$path_part";
}
$self->debug(qq{using "$path_part":});
if ( $path_part eq '/' )
{
push @{$return_nodes}, @{$current_node};
}
else
{
push @{$return_nodes},@{$self->__get_node_set__( $path_part,
[$current_node],
$silent )};
}
$self->_outdent();
}
return $return_nodes;
}
}
# auxiliary function #
sub __get_node_set__
{
my ( $self, $path, $node, $silent ) = @_;
# a Qname (?) should actually be: [a-Z_][\w\.\-]*\:[a-Z_][\w\.\-]*
my $list = [];
if ( $path eq "" )
{
$self->debug("node found!");
push @{$list}, @{$node};
}
else
{
foreach my $item (@$node)
{
my $sublist = $self->__try_a_step__( $path, $item, $silent );
push @{$list}, @{$sublist} ;
}
}
return $list;
}
sub __try_a_step__
{
my ( $self, $path, $node, $silent ) = @_;
$self->_indent();
$self->debug("Trying $path >");
if ( $path =~ s/^\/parent\:\:node\(\)// )
{
# /.. #
$self->debug(qq{getting parent ("$path")});
return $self->__parent__( $path, $node, $silent );
}
elsif ( $path =~ s/^\/attribute\:\:(\*|[\w\.\:\-]+)// )
{
# /@attr #
$self->debug(qq{getting attribute `$1' ("$path")});
return $self->__attribute__( $1, $path, $node, $silent );
}
elsif ( $path =~
s/^\/descendant\-or\-self\:\:node\(\)\/(child\:\:|)(\*|[\w\.\:\-]+)\[(\S+?)\]//
)
{
# //elem[n] #
$self->debug(qq{getting deep indexed element `$1' `$2' ("$path")});
return &__indexed_element__( $self, $1, $2, $path, $node, $silent,
"deep" );
}
elsif ( $path =~ s/^\/descendant\-or\-self\:\:node\(\)\/(\*|[\w\.\:\-]+)// )
{
# //elem #
$self->debug(qq{getting deep element `$1' ("$path")});
return &__element__( $self, $1, $path, $node, $silent, "deep" );
}
elsif ( $path =~ s/^\/(child\:\:|)(\*|[\w\.\:\-]+)\[(\S+?)\]// )
{
# /elem[n] #
$self->debug(qq{getting indexed element `$2' `$3' ("$path")});
return &__indexed_element__( $self, $2, $3, $path, $node, $silent );
}
elsif ( $path =~ s/^\/(child\:\:|)text\(\)// )
{
# /text() #
$self->debug(qq{getting text ("$path")});
return &__get_nodes__( $self, TEXT_NODE, $path, $node, $silent );
}
elsif ( $path =~ s/^\/(child\:\:|)processing-instruction\(\)// )
{
# /processing-instruction() #
$self->debug(qq{getting processing instruction ("$path")});
return $self->__get_nodes__(PROCESSING_INSTRUCTION_NODE,
$path,
$node,
$silent );
}
elsif ( $path =~ s/^\/(child\:\:|)comment\(\)// )
{
# /comment() #
$self->debug(qq{getting comment ("$path")});
return &__get_nodes__( $self, COMMENT_NODE, $path, $node, $silent );
}
elsif ( $path =~ s/^\/(child\:\:|)(\*|[\w\.\:\-]+)// )
{
# /elem #
$self->debug(qq{getting element `$2' ("$path")});
return &__element__( $self, $2, $path, $node, $silent );
}
else
{
$self->warn(
"get-node-from-path: Don't know what to do with path $path !!!");
return [];
}
}
sub __parent__
{
my ( $self, $path, $node, $silent ) = @_;
$self->_indent();
if ( ( $node->getNodeType == DOCUMENT_NODE )
|| ( $node->getNodeType == DOCUMENT_FRAGMENT_NODE ) )
{
$self->debug("no parent!");
$node = [];
}
else
{
$node = $node->getParentNode;
$node = &__get_node_set__( $self, $path, [$node], $silent );
}
$self->_outdent();
return $node;
}
sub __indexed_element__
{
my ( $self, $element, $index, $path, $node, $silent, $deep ) = @_;
$index ||= 0;
$deep ||= ""; # False #
if ( $index =~ /^first\s*\(\)/ )
{
$index = 0;
}
elsif ( $index =~ /^last\s*\(\)/ )
{
$index = -1;
}
else
{
$index--;
}
my @list = $node->getElementsByTagName( $element, $deep );
if (@list)
{
$node = $list[$index];
}
else
{
$node = "";
}
$self->_indent();
if ($node)
{
$node = &__get_node_set__( $self, $path, [$node], $silent );
}
else
{
$self->debug("failed!");
$node = [];
}
$self->_outdent();
return $node;
}
sub __element__
{
my ( $self, $element, $path, $node, $silent, $deep ) = @_;
$deep ||= ""; # False #
$node = [ $node->getElementsByTagName( $element, $deep ) ];
$self->_indent();
if (@$node)
{
$node = $self->__get_node_set__( $path, $node, $silent );
}
else
{
$self->debug("failed!");
}
$self->_outdent();
return $node;
}
sub __attribute__
{
my ( $self, $attribute, $path, $node, $silent ) = @_;
$self->_indent();
if ( $attribute eq '*' )
{
$node = [ $node->getAttributes->getValues ];
if ($node)
{
$node = &__get_node_set__( $self, $path, $node, $silent );
}
else
{
$self->debug("failed!");
}
}
else
{
$node = $node->getAttributeNode($attribute);
if ($node)
{
$node = &__get_node_set__( $self, $path, [$node], $silent );
}
else
{
$self->debug("failed!");
$node = [];
}
}
$self->_outdent();
return $node;
}
sub __get_nodes__
{
my ( $self, $node_type, $path, $node, $silent ) = @_;
my $result = [];
$self->_indent();
foreach my $child ( $node->getChildNodes )
{
if ( $child->getNodeType == $node_type )
{
push @{$result}, @{$self->__get_node_set__($path,
[$child], $silent )};
}
}
$self->_outdent();
if ( !@$result )
{
$self->debug("failed!");
}
return $result;
}
sub _attribute_value_of
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$variables )
= @_;
foreach my $attribute ( $xsl_node->getAttributes->getValues )
{
my $value = $attribute->getValue;
study($value);
#$value =~ s/(\*|\$|\@|\&|\?|\+|\\)/\\$1/g;
$value =~ s/(\*|\?|\+)/\\$1/g;
study($value);
while ( $value =~ /\G[^\\]?\{(.*?[^\\]?)\}/ )
{
my $node =
$self->_get_node_set( $1, $self->xml_document(),
$current_xml_selection_path, $current_xml_node, $variables );
if (@$node)
{
$self->_indent();
my $text = $self->__string__( $$node[0] );
$self->_outdent();
$value =~ s/(\G[^\\]?)\{(.*?)[^\\]?\}/$1$text/;
}
else
{
$value =~ s/(\G[^\\]?)\{(.*?)[^\\]?\}/$1/;
}
}
#$value =~ s/\\(\*|\$|\@|\&|\?|\+|\\)/$1/g;
$value =~ s/\\(\*|\?|\+)/$1/g;
$value =~ s/\\(\{|\})/$1/g;
$attribute->setValue($value);
}
}
sub _processing_instruction
{
my ( $self, $xsl_node, $current_result_node, $variables, $oldvariables ) =
@_;
my $new_PI_name = $xsl_node->getAttribute('name');
if ( $new_PI_name eq "xml" )
{
$self->warn( "<"
. $self->xsl_ns()
. "processing-instruction> may not be used to create XML" );
$self->warn(
"declaration. Use <" . $self->xsl_ns() . "output> instead..." );
}
elsif ($new_PI_name)
{
my $text = $self->__string__($xsl_node);
my $new_PI =
$self->xml_document()
->createProcessingInstruction( $new_PI_name, $text );
if ($new_PI)
{
$self->_move_node( $new_PI, $current_result_node );
}
}
else
{
$self->warn( q{Expected attribute "name" in <}
. $self->xsl_ns()
. "processing-instruction> !" );
}
}
sub _process_with_params
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$variables, $params )
= @_;
my @params =
$xsl_node->getElementsByTagName( $self->xsl_ns() . "with-param" );
foreach my $param (@params)
{
my $varname = $param->getAttribute('name');
if ($varname)
{
my $value = $param->getAttribute('select');
if ( !$value )
{
# process content as template
$value = $self->xml_document()->createDocumentFragment;
$self->_evaluate_template( $param, $current_xml_node,
$current_xml_selection_path, $value, $variables, {} );
$$params{$varname} = $value;
}
else
{
# *** FIXME - should evaluate this as an expression!
$$params{$varname} = $value;
}
}
else
{
$self->warn( q{Expected attribute "name" in <}
. $self->xsl_ns()
. q{with-param> !} );
}
}
}
sub _call_template
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
my $params = {};
my $newvariables = defined $variables ? {%$variables} : {};
my $name = $xsl_node->getAttribute('name');
if ($name)
{
$self->debug(qq{calling template named "$name"});
$self->_process_with_params( $xsl_node, $current_xml_node,
$current_xml_selection_path, $variables, $params );
$self->_indent();
my $template = $self->_match_template( "name", $name, 0, '' );
if ($template)
{
$self->_evaluate_template( $template, $current_xml_node,
$current_xml_selection_path, $current_result_node,
$newvariables, $params );
}
else
{
$self->warn("no template named $name found!");
}
$self->_outdent();
}
else
{
$self->warn( q{Expected attribute "name" in <}
. $self->xsl_ns()
. q{call-template/>} );
}
}
sub _choose
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
$self->debug("evaluating choose:");
$self->_indent();
my $notdone = "true";
my $testwhen = "active";
foreach my $child ( $xsl_node->getElementsByTagName( '*', 0 ) )
{
if ( $notdone
&& $testwhen
&& ( $child->getTagName eq $self->xsl_ns() . "when" ) )
{
my $test = $child->getAttribute('test');
if ($test)
{
my $test_succeeds =
$self->_evaluate_test( $test, $current_xml_node,
$current_xml_selection_path, $variables );
if ($test_succeeds)
{
$self->_evaluate_template( $child, $current_xml_node,
$current_xml_selection_path, $current_result_node,
$variables, $oldvariables );
$testwhen = "";
$notdone = "";
}
}
else
{
$self->warn( q{expected attribute "test" in <}
. $self->xsl_ns()
. q{when>} );
}
}
elsif ( $notdone
&& ( $child->getTagName eq $self->xsl_ns() . "otherwise" ) )
{
$self->_evaluate_template( $child, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
$notdone = "";
}
}
if ($notdone)
{
$self->debug("nothing done!");
}
$self->_outdent();
}
sub _if
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
$self->debug("evaluating if:");
$self->_indent();
my $test = $xsl_node->getAttribute('test');
if ($test)
{
my $test_succeeds =
$self->_evaluate_test( $test, $current_xml_node,
$current_xml_selection_path, $variables );
if ($test_succeeds)
{
$self->_evaluate_template( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node, $variables,
$oldvariables );
}
}
else
{
$self->warn(
q{expected attribute "test" in <} . $self->xsl_ns() . q{if>} );
}
$self->_outdent();
}
sub __evaluate_test__
{
my ( $self, $test, $path, $node, $variables ) = @_;
my $tagname = eval { $node->getTagName() } || '';
my ( $content, $test_cond, $expval, $lhs );
$self->debug(qq{testing with "$test" and $tagname});
if ($test =~ /^\s*(\S+?)\s*(<=|>=|!=|<|>|=)\s*['"]?([^'"]*?)['"]?\s*$/)
{
$lhs = $1;
$test_cond = $2;
$expval = $3;
}
else
{
$self->debug("no match for test [$test]");
return '';
}
$self->debug("Test LHS: $lhs");
if ( $lhs =~ /^\@([\w\.\:\-]+)$/ )
{
$self ->debug("Attribute: $1");
$content = $node->getAttribute($1);
}
elsif ( $lhs =~ /^([\$\w\.\:\-]+)$/ )
{
$self ->debug("Path: $1");
my $test_path = $1;
my $nodeset = $self->_get_node_set( $test_path,
$self->xml_document(),
$path,
$node,
$variables );
return ( $expval ne '' ) unless @$nodeset;
$content = &__string__( $self, $$nodeset[0] );
}
else
{
$self->debug("no match for test");
return "";
}
my $numeric = ($content =~ /^\d+$/ && $expval =~ /^\d+$/ ? 1 : 0);
$self->debug("evaluating $content $test $expval");
$test_cond =~ s/\s+//g;
if ( $test_cond eq '!=' )
{
return $numeric ? $content != $expval : $content ne $expval;
}
elsif ( $test_cond eq '=' )
{
return $numeric ? $content == $expval : $content eq $expval;
}
elsif ( $test_cond eq '<' )
{
return $numeric ? $content < $expval : $content lt $expval;
}
elsif ( $test_cond eq '>' )
{
return $numeric ? $content > $expval : $content gt $expval;
}
elsif ( $test_cond eq '>=' )
{
return $numeric ? $content >= $expval : $content ge $expval;
}
elsif ( $test_cond eq '<=' )
{
return $numeric ? $content <= $expval : $content le $expval;
}
else
{
$self->debug("no test matches");
return 0;
}
}
sub _copy_of
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables )
= @_;
my $nodelist;
my $select = $xsl_node->getAttribute('select');
$self->debug(qq{evaluating copy-of with select "$select":});
$self->_indent();
if ($select)
{
$nodelist =
$self->_get_node_set( $select, $self->xml_document(),
$current_xml_selection_path, $current_xml_node, $variables );
}
else
{
$self->warn( q{expected attribute "select" in <}
. $self->xsl_ns()
. q{copy-of>} );
}
foreach my $node (@$nodelist)
{
$self->_add_node( $node, $current_result_node, "deep" );
}
$self->_outdent();
}
sub _copy
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
$self->debug("evaluating copy:");
$self->_indent();
if ( $current_xml_node->getNodeType == ATTRIBUTE_NODE )
{
my $attribute = $current_xml_node->cloneNode(0);
$current_result_node->setAttributeNode($attribute);
}
elsif (( $current_xml_node->getNodeType == COMMENT_NODE )
|| ( $current_xml_node->getNodeType == PROCESSING_INSTRUCTION_NODE ) )
{
$self->_add_node( $current_xml_node, $current_result_node );
}
else
{
$self->_add_node( $current_xml_node, $current_result_node );
$self->_apply_attribute_set($xsl_node,$current_result_node->getLastChild());
$self->_evaluate_template( $xsl_node, $current_xml_node,
$current_xml_selection_path, $current_result_node->getLastChild,
$variables, $oldvariables );
}
$self->_outdent();
}
sub _text
{
#=item addText (text)
#
#Appends the specified string to the last child if it is a Text node, or else
#appends a new Text node (with the specified text.)
#
#Return Value: the last child if it was a Text node or else the new Text node.
my ( $self, $xsl_node, $current_result_node ) = @_;
$self->debug("inserting text:");
$self->_indent();
$self->debug("stripping node to text:");
$self->_indent();
my $text = $self->__string__($xsl_node);
$self->_outdent();
if ( $text ne '' )
{
my $node = $self->xml_document()->createTextNode($text);
if ( $xsl_node->getAttribute('disable-output-escaping') eq 'yes' )
{
$self->debug("disabling output escaping");
bless $node, 'XML::XSLT::DOM::TextDOE';
}
$self->_move_node( $node, $current_result_node );
}
else
{
$self->debug("nothing left..");
}
$current_result_node->normalize();
$self->_outdent();
}
sub _attribute
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
my $name = $xsl_node->getAttribute('name');
$self->debug(qq{inserting attribute named "$name":});
$self->_indent();
if ($name)
{
if ( $name =~ /^xmlns:/ )
{
$self->debug("Won't create namespace declaration");
}
else
{
my $result = $self->xml_document()->createDocumentFragment;
$self->_evaluate_template( $xsl_node, $current_xml_node,
$current_xml_selection_path, $result, $variables,
$oldvariables );
$self->_indent();
my $text = $self->fix_attribute_value( $self->__string__($result) );
$self->_outdent();
$current_result_node->setAttribute( $name, $text );
$result->dispose();
}
}
else
{
$self->warn( q{expected attribute "name" in <}
. $self->xsl_ns()
. q{attribute>} );
}
$self->_outdent();
}
sub _comment
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $oldvariables )
= @_;
$self->debug("inserting comment:");
$self->_indent();
my $result = $self->xml_document()->createDocumentFragment;
$self->_evaluate_template( $xsl_node, $current_xml_node,
$current_xml_selection_path, $result, $variables, $oldvariables );
$self->_indent();
my $text = $self->__string__($result);
$self->_outdent();
$self->_move_node( $self->xml_document()->createComment($text),
$current_result_node );
$result->dispose();
$self->_outdent();
}
sub _variable
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $params, $is_param )
= @_;
my $varname = $xsl_node->getAttribute('name');
if ($varname)
{
$self->debug("definition of variable \$$varname:");
$self->_indent();
if ( $is_param and exists $$params{$varname} )
{
# copy from parent-template
$$variables{$varname} = $$params{$varname};
}
else
{
# new variable definition
my $value = $xsl_node->getAttribute('select');
if ( !$value )
{
#tough case, evaluate content as template
$value = $self->xml_document()->createDocumentFragment;
$self->_evaluate_template( $xsl_node, $current_xml_node,
$current_xml_selection_path, $value, $variables, $params );
}
else # either a literal or path
{
if ( $value =~ /'(.*)'/ )
{
$value = $1;
}
else
{
my $node =
$self->_get_node_set( $value, $self->xml_document(),
$current_xml_selection_path, $current_xml_node,
$variables );
$value = $self->__string__($node);
}
}
$variables->{$varname} = $value;
}
$self->_outdent();
}
else
{
$self->warn( q{expected attribute "name" in <}
. $self->xsl_ns()
. q{param> or <}
. $self->xsl_ns()
. q{variable>} );
}
}
# not implemented - but log it and make it go away
sub _sort
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $params, $is_param )
= @_;
$self->debug("dummy process for sort");
}
# Not quite sure how fallback should be implemented as the spec seems a
# little vague to me
sub _fallback
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $params, $is_param )
= @_;
$self->debug("dummy process for fallback");
}
# This is a no-op - attribute-sets should not appear within templates and
# we have already processed the stylesheet wide ones.
sub _attribute_set
{
my ( $self, $xsl_node, $current_xml_node, $current_xml_selection_path,
$current_result_node, $variables, $params, $is_param )
= @_;
$self->debug("in _attribute_set");
}
sub _indent
{
my ($self) = @_;
$self->{INDENT} += $self->{INDENT_INCR};
}
sub _outdent
{
my ($self) = @_;
$self->{INDENT} -= $self->{INDENT_INCR};
}
sub fix_attribute_value
{
my ( $self, $text ) = @_;
# The spec say's that there can't be a literal line break in the
# attributes value - white space at the beginning or the end is
# almost certainly an mistake.
$text =~ s/^\s+//g;
$text =~ s/\s+$//g;
if ($text)
{
$text =~ s/([\x0A\x0D])/sprintf("\&#%02X;",ord $1)/eg;
}
return $text;
}
1;
__DATA__
=back
=head1 XML::XSLT Commands
=over 4
=item xsl:apply-imports no
Not supported yet.
=item xsl:apply-templates limited
Attribute 'select' is supported to the same extent as xsl:value-of
supports path selections.
Not supported yet:
- attribute 'mode'
- xsl:sort and xsl:with-param in content
=item xsl:attribute partially
Adds an attribute named to the value of the attribute 'name' and as value
the stringified content-template.
Not supported yet:
- attribute 'namespace'
=item xsl:attribute-set yes
Partially
=item xsl:call-template yes
Takes attribute 'name' which selects xsl:template's by name.
Weak support:
- xsl:with-param (select attrib not supported)
Not supported yet:
- xsl:sort
=item xsl:choose yes
Tests sequentially all xsl:whens until one succeeds or
until an xsl:otherwise is found. Limited test support, see xsl:when
=item xsl:comment yes
Supported.
=item xsl:copy partially
=item xsl:copy-of limited
Attribute 'select' functions as well as with
xsl:value-of
=item xsl:decimal-format no
Not supported yet.
=item xsl:element yes
=item xsl:fallback no
Not supported yet.
=item xsl:for-each limited
Attribute 'select' functions as well as with
xsl:value-of
Not supported yet:
- xsl:sort in content
=item xsl:if limited
Identical to xsl:when, but outside xsl:choose context.
=item xsl:import no
Not supported yet.
=item xsl:include yes
Takes attribute href, which can be relative-local,
absolute-local as well as an URL (preceded by
identifier http:).
=item xsl:key no
Not supported yet.
=item xsl:message no
Not supported yet.
=item xsl:namespace-alias no
Not supported yet.
=item xsl:number no
Not supported yet.
=item xsl:otherwise yes
Supported.
=item xsl:output limited
Only the initial xsl:output element is used. The "text" output method
is not supported, but shouldn't be difficult to implement. Only the
"doctype-public", "doctype-system", "omit-xml-declaration", "method",
and "encoding" attributes have any support.
=item xsl:param experimental
Synonym for xsl:variable (currently). See xsl:variable for support.
=item xsl:preserve-space no
Not supported yet. Whitespace is always preserved.
=item xsl:processing-instruction yes
Supported.
=item xsl:sort no
Not supported yet.
=item xsl:strip-space no
Not supported yet. No whitespace is stripped.
=item xsl:stylesheet limited
Minor namespace support: other namespace than 'xsl:' for xsl-commands
is allowed if xmlns-attribute is present. xmlns URL is verified.
Other attributes are ignored.
=item xsl:template limited
Attribute 'name' and 'match' are supported to minor extend.
('name' must match exactly and 'match' must match with full
path or no path)
Not supported yet:
- attributes 'priority' and 'mode'
=item xsl:text yes
Supported.
=item xsl:transform limited
Synonym for xsl:stylesheet
=item xsl:value-of limited
Inserts attribute or element values. Limited support:
<xsl:value-of select="."/>
<xsl:value-of select="/root-elem"/>
<xsl:value-of select="elem"/>
<xsl:value-of select="//elem"/>
<xsl:value-of select="elem[n]"/>
<xsl:value-of select="//elem[n]"/>
<xsl:value-of select="@attr"/>
<xsl:value-of select="text()"/>
<xsl:value-of select="processing-instruction()"/>
<xsl:value-of select="comment()"/>
and combinations of these.
Not supported yet:
- attribute 'disable-output-escaping'
=item xsl:variable partial
or from literal text in the stylesheet.
=item xsl:when limited
Only inside xsl:choose. Limited test support:
<xsl:when test="@attr='value'">
<xsl:when test="elem='value'">
<xsl:when test="path/[@attr='value']">
<xsl:when test="path/[elem='value']">
<xsl:when test="path">
path is supported to the same extend as with xsl:value-of
=item xsl:with-param experimental
It is currently not functioning. (or is it?)
=back
=head1 SUPPORT
General information, bug reporting tools, the latest version, mailing
lists, etc. can be found at the XML::XSLT homepage:
http://xmlxslt.sourceforge.net/
=head1 DEPRECATIONS
Methods and interfaces from previous versions that are not documented in this
version are deprecated. Each of these deprecations can still be used
but will produce a warning when the deprecation is first used. You
can use the old interfaces without warnings by passing C<new()> the
flag C<use_deprecated>. Example:
$parser = XML::XSLT->new($xsl, "FILE",
use_deprecated => 1);
The deprecated methods will disappear by the time a 1.0 release is made.
The deprecated methods are :
=over 2
=item output_string
use toString instead
=item result_string
use toString instead
=item output
use toString instead
=item result
use toString instead
=item result_mime_type
use media_type instead
=item output_mime_type
use media_type instead
=item result_tree
use to_dom instead
=item output_tree
use to_dom instead
=item transform_document
use transform instead
=item process_project
use process instead
=item open_project
use C<Source> argument to B<new()> and B<transform> instead.
=item print_output
use B<serve()> instead.
=back
=head1 BUGS
Yes.
=head1 HISTORY
Geert Josten and Egon Willighagen developed and maintained XML::XSLT
up to version 0.22. At that point, Mark Hershberger started moving
the project to Sourceforge and began working on it with Bron Gondwana.
=head1 LICENCE
Copyright (c) 1999 Geert Josten & Egon Willighagen. All Rights
Reserverd. This module is free software, and may be distributed under
the same terms and conditions as Perl.
=head1 AUTHORS
Geert Josten <gjosten@sci.kun.nl>
Egon Willighagen <egonw@sci.kun.nl>
Mark A. Hershberger <mah@everybody.org>
Bron Gondwana <perlcode@brong.net>
Jonathan Stowe <jns@gellyfish.com>
=head1 SEE ALSO
L<XML::DOM>, L<LWP::Simple>, L<XML::Parser>
=cut
Filename: $RCSfile: XSLT.pm,v $
Revision: $Revision: 1.34 $
Label: $Name: $
Last Chg: $Author: gellyfish $
On: $Date: 2008/11/21 15:45:13 $
RCS ID: $Id: XSLT.pm,v 1.34 2008/11/21 15:45:13 gellyfish Exp $
Path: $Source: /cvsroot/xmlxslt/XML-XSLT/lib/XML/XSLT.pm,v $