package XML::DOM::Lite::XSLT;
use XML::DOM::Lite::XPath;
use XML::DOM::Lite::Constants qw(:all);
use Carp qw(confess);
use warnings;
use strict;
our $DEBUG = 0;
sub new { bless { }, $_[0] }
sub process {
my ($self, $xmlDoc, $stylesheet) = @_;
return xsltProcess($xmlDoc, $stylesheet);
}
sub xsltProcess {
my ($xmlDoc, $stylesheet) = @_;
$DEBUG && warn('XML STYLESHEET:');
$DEBUG && warn(xmlText($stylesheet));
$DEBUG && warn('XML INPUT:');
$DEBUG && warn(xmlText($xmlDoc));
my $output = $xmlDoc->createDocumentFragment();
xsltProcessContext(XML::DOM::Lite::XPath::ExprContext->new($xmlDoc), $stylesheet, $output);
my $ret = xmlText($output);
$DEBUG && warn('HTML OUTPUT:');
$DEBUG && warn($ret);
return $ret;
}
sub xsltProcessContext {
my ($input, $template, $output) = @_;
my @nodename = split(/:/, $template->nodeName);
if (@nodename == 1 or $nodename[0] ne 'xsl') {
xsltPassThrough($input, $template, $output);
} else {
if ($nodename[1] eq 'apply-imports') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'apply-templates') {
my $select = xmlGetAttribute($template, 'select');
my $nodes;
if ($select) {
$nodes = xpathEval($select, $input)->nodeSetValue();
} else {
$nodes = $input->{node}->childNodes;
}
my $sortContext = $input->clone($nodes->[0], 0, $nodes);
xsltWithParam($sortContext, $template);
xsltSort($sortContext, $template);
my $mode = xmlGetAttribute($template, 'mode');
my $top = $template->ownerDocument->documentElement;
for (my $i = 0; $i < $top->childNodes->length; ++$i) {
my $c = $top->childNodes->[$i];
if ($c->nodeType == ELEMENT_NODE and
$c->nodeName eq 'xsl:template' and
($c->getAttribute('mode') || '') eq ($mode || '')) {
for (my $j = 0; $j < @{$sortContext->{nodelist}}; ++$j) {
my $nj = $sortContext->{nodelist}->[$j];
xsltProcessContext($sortContext->clone($nj, $j), $c, $output);
}
}
}
} elsif ($nodename[1] eq 'attribute') {
my $nameexpr = xmlGetAttribute($template, 'name');
my $name = xsltAttributeValue($nameexpr, $input);
my $node = $output->ownerDocument->createDocumentFragment();
xsltChildNodes($input, $template, $node);
my $value = xmlValue($node);
$output->setAttribute($name, $value);
} elsif ($nodename[1] eq 'attribute-set') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'call-template') {
my $name = xmlGetAttribute($template, 'name');
my $top = $template->ownerDocument->documentElement;
my $paramContext = $input->clone();
xsltWithParam($paramContext, $template);
for (my $i = 0; $i < $top->childNodes->length; ++$i) {
my $c = $top->childNodes->[$i];
if ($c->nodeType == ELEMENT_NODE and
$c->nodeName eq 'xsl:template' and
$c->getAttribute('name') eq $name) {
xsltChildNodes($paramContext, $c, $output);
last;
}
}
} elsif ($nodename[1] eq 'choose') {
xsltChoose($input, $template, $output);
} elsif ($nodename[1] eq 'comment') {
my $node = $output->ownerDocument->createDocumentFragment();
xsltChildNodes($input, $template, $node);
my $commentData = xmlValue($node);
my $commentNode = $output->ownerDocument->createComment($commentData);
$output->appendChild($commentNode);
} elsif ($nodename[1] eq 'copy') {
if ($input->{node}->nodeType == ELEMENT_NODE) {
my $node = $output->ownerDocument->createElement($input->{node}->nodeName);
$output->appendChild($node);
xsltChildNodes($input, $template, $node);
} elsif ($input->{node}->nodeType == ATTRIBUTE_NODE) {
my $node = $output->ownerDocument->createAttribute($input->{node}->nodeName);
$node->nodeValue = $input->{node}->nodeValue;
$output->setAttribute($node);
}
} elsif ($nodename[1] eq 'copy-of') {
my $select = xmlGetAttribute($template, 'select');
my $value = xpathEval($select, $input);
if ($value->{type} eq 'node-set') {
my $nodes = $value->nodeSetValue();
for (my $i = 0; $i < @$nodes; ++$i) {
xsltCopyOf($output, $nodes->[$i]);
}
} else {
my $node = $output->ownerDocument->createTextNode($value->stringValue());
$output->appendChild($node);
}
} elsif ($nodename[1] eq 'decimal-format') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'element') {
my $nameexpr = xmlGetAttribute($template, 'name');
my $name = xsltAttributeValue($nameexpr, $input);
my $node = $output->ownerDocument->createElement($name);
$output->appendChild($node);
xsltChildNodes($input, $template, $node);
} elsif ($nodename[1] eq 'fallback') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'for-each') {
my $sortContext = $input->clone();
xsltSort($sortContext, $template);
xsltForEach($sortContext, $template, $output);
} elsif ($nodename[1] eq 'if') {
my $test = xmlGetAttribute($template, 'test');
if (xpathEval($test, $input)->booleanValue()) {
xsltChildNodes($input, $template, $output);
}
} elsif ($nodename[1] eq 'import') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'include') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'key') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'message') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'namespace-alias') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'number') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'otherwise') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'output') {
} elsif ($nodename[1] eq 'preserve-space') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'processing-instruction') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'sort') {
} elsif ($nodename[1] eq 'strip-space') {
warn('not implemented: ' . $nodename[1]);
} elsif ($nodename[1] eq 'stylesheet' or $nodename[1] eq 'transform') {
xsltChildNodes($input, $template, $output);
} elsif ($nodename[1] eq 'template') {
my $match = xmlGetAttribute($template, 'match');
if ($match and xpathMatch($match, $input)) {
xsltChildNodes($input, $template, $output);
}
} elsif ($nodename[1] eq 'text') {
my $text = xmlValue($template);
my $node = $output->ownerDocument->createTextNode($text);
$output->appendChild($node);
} elsif ($nodename[1] eq 'value-of') {
my $select = xmlGetAttribute($template, 'select');
my $value = xpathEval($select, $input)->stringValue();
unless ($output->ownerDocument) { die 'no ownerDocument for '.Dumper($output) }
my $node = $output->ownerDocument->createTextNode($value);
$output->appendChild($node);
} elsif ($nodename[1] eq 'param') {
xsltVariable($input, $template, 0);
} elsif ($nodename[1] eq 'variable') {
xsltVariable($input, $template, 1);
} elsif ($nodename[1] eq 'when') {
warn('error if here: ' . $nodename[1]);
} elsif ($nodename[1] eq 'with-param') {
warn('error if here: ' . $nodename[1]);
} else {
warn('error if here: ' . $nodename[1]);
}
}
}
sub xsltWithParam {
my ($input, $template) = @_;
for (my $i = 0; $i < $template->childNodes->length; ++$i) {
my $c = $template->childNodes->[$i];
if ($c->nodeType == ELEMENT_NODE and $c->nodeName eq 'xsl:with-param') {
xsltVariable($input, $c, 1);
}
}
}
sub xsltSort {
my ($input, $template) = @_;
my $sort = [];
for (my $i = 0; $i < $template->childNodes->length; ++$i) {
my $c = $template->childNodes->[$i];
if ($c->nodeType == ELEMENT_NODE and $c->nodeName eq 'xsl:sort') {
my $select = xmlGetAttribute($c, 'select');
my $expr = xpathParse($select);
my $type = xmlGetAttribute($c, 'data-type') || 'text';
my $order = xmlGetAttribute($c, 'order') || 'ascending';
push(@$sort, { expr=> $expr, type=> $type, order=> $order });
}
}
xpathSort($input, $sort);
}
sub xsltVariable {
my ($input, $template, $override) = @_;
my $name = xmlGetAttribute($template, 'name');
my $select = xmlGetAttribute($template, 'select');
my $value;
if ($template->childNodes->length > 0) {
my $root = $input->{node}->ownerDocument->createDocumentFragment();
xsltChildNodes($input, $template, $root);
$value = new NodeSetValue([$root]);
} elsif ($select) {
$value = xpathEval($select, $input);
} else {
$value = new StringValue('');
}
if ($override || !$input->getVariable($name)) {
$input->setVariable($name, $value);
}
}
sub xsltChoose {
my ($input, $template, $output) = @_;
for (my $i = 0; $i < $template->childNodes->length; ++$i) {
my $childNode = $template->childNodes->[$i];
if ($childNode->nodeType != ELEMENT_NODE) {
next;
} elsif ($childNode->nodeName eq 'xsl:when') {
my $test = xmlGetAttribute($childNode, 'test');
if (xpathEval($test, $input)->booleanValue()) {
xsltChildNodes($input, $childNode, $output);
last;
}
} elsif ($childNode->nodeName eq 'xsl:otherwise') {
xsltChildNodes($input, $childNode, $output);
last;
}
}
}
sub xsltForEach {
my ($input, $template, $output) = @_;
my $select = xmlGetAttribute($template, 'select');
my $nodes = xpathEval($select, $input)->nodeSetValue();
for (my $i = 0; $i < @$nodes; ++$i) {
my $context = $input->clone($nodes->[$i], $i, $nodes);
xsltChildNodes($context, $template, $output);
}
}
sub xsltChildNodes {
my ($input, $template, $output, $foo) = @_;
my $context = $input->clone();
foreach my $c (@{$template->childNodes}) {
xsltProcessContext($context, $c, $output);
}
}
sub xsltPassThrough {
my ($input, $template, $output) = @_;
if ($template->nodeType == TEXT_NODE) {
if (xsltPassText($template)) {
my $node = $output->ownerDocument->createTextNode($template->nodeValue);
$output->appendChild($node);
}
} elsif ($template->nodeType == ELEMENT_NODE) {
my $node = $output->ownerDocument->createElement($template->nodeName);
for (my $i = 0; $i < $template->attributes->length; ++$i) {
my $a = $template->attributes->[$i];
if ($a) {
my $name = $a->nodeName;
my $value = xsltAttributeValue($a->nodeValue, $input);
$node->setAttribute($name, $value);
}
}
$output->appendChild($node);
xsltChildNodes($input, $template, $node);
} else {
xsltChildNodes($input, $template, $output);
}
}
sub xsltPassText {
my ($template) = @_;
unless ($template->nodeValue =~ /^\s*$/) {
return 1;
}
my $element = $template->parentNode;
if ($element->nodeName eq 'xsl:text') {
return 1;
}
while ($element and $element->nodeType == ELEMENT_NODE) {
my $xmlspace = $element->getAttribute('xml:space');
if ($xmlspace) {
if ($xmlspace eq 'default') {
return 0;
} elsif ($xmlspace eq 'preserve') {
return 1;
}
}
$element = $element->parentNode;
}
return 0;
}
sub xsltAttributeValue {
my ($value, $context) = @_;
my $parts = [ split(/{/, $value) ];
if (@$parts == 1) {
return $value;
}
my $ret = '';
for (my $i = 0; $i < @$parts; ++$i) {
my $rp = [ split(/}/, $parts->[$i]) ];
if (@$rp != 2) {
$ret .= $parts->[$i];
next;
}
my $val = xpathEval($rp->[0], $context)->stringValue();
$ret .= ($val . $rp->[1]);
}
return $ret;
}
sub xmlGetAttribute {
my ($node, $name) = @_;
my $value = $node->getAttribute($name);
if ($value) {
return xmlResolveEntities($value);
} else {
return $value;
}
}
sub xsltCopyOf {
my ($dst, $src) = @_;
if ($src->nodeType == TEXT_NODE) {
my $node = $dst->ownerDocument->createTextNode($src->nodeValue);
$dst->appendChild($node);
} elsif ($src->nodeType == ATTRIBUTE_NODE) {
$dst->setAttribute($src->nodeName, $src->nodeValue);
} elsif ($src->nodeType == ELEMENT_NODE) {
my $node = $dst->ownerDocument->createElement($src->nodeName);
$dst->appendChild($node);
for (my $i = 0; $i < $src->attributes->length; ++$i) {
xsltCopyOf($node, $src->attributes->[$i]);
}
for (my $i = 0; $i < $src->childNodes->length; ++$i) {
xsltCopyOf($node, $src->childNodes->[$i]);
}
} elsif ($src->nodeType == DOCUMENT_FRAGMENT_NODE or
$src->nodeType == DOCUMENT_NODE) {
for (my $i = 0; $i < $src->childNodes->length; ++$i) {
xsltCopyOf($dst, $src->childNodes->[$i]);
}
}
}
sub xpathParse {
my ($match) = @_;
return XML::DOM::Lite::XPath->parse($match);
}
sub xpathMatch {
my ($match, $context) = @_;
my $expr = xpathParse($match);
my $ret;
if ($expr->{steps} and (not $expr->{absolute})
and (@{$expr->{steps}} == 1)
and ($expr->{steps}->[0]->{axis} eq 'child')
and (@{$expr->{steps}->[0]->{predicate}} == 0)) {
$ret = $expr->{steps}->[0]->{nodetest}->evaluate($context)->booleanValue();
} else {
$ret = 0;
my $node = $context->{node};
while ((not $ret) and $node) {
my $result = $expr->evaluate($context->clone($node,0,[$node]))->nodeSetValue();
for (my $i = 0; $i < @$result; ++$i) {
if ($result->[$i] == $context->{node}) {
$ret = 1;
last;
}
}
$node = $node->parentNode;
}
}
return $ret;
}
sub xpathSort {
return XML::DOM::Lite::XPath::xpathSort(@_);
}
sub xpathEval {
my ($select, $context) = @_;
my $expr = xpathParse($select);
my $ret = $expr->evaluate($context);
return $ret;
}
sub xmlText {
my ($node) = @_;
my $ret = '';
if ($node->nodeType == TEXT_NODE) {
$ret .= $node->nodeValue;
} elsif ($node->nodeType == ELEMENT_NODE) {
$ret .= '<' . $node->nodeName;
for (my $i = 0; $i < $node->attributes->length; ++$i) {
my $a = $node->attributes->[$i];
if ($a and $a->nodeName and $a->nodeValue) {
$ret .= ' ' . $a->nodeName;
$ret .= '="' . $a->nodeValue . '"';
}
}
if ($node->childNodes->length == 0) {
$ret .= '/>';
} else {
$ret .= '>';
for (my $i = 0; $i < $node->childNodes->length; ++$i) {
$ret .= xmlText($node->childNodes->[$i]);
}
$ret .= '</' . $node->nodeName . '>';
}
} elsif ($node->nodeType == DOCUMENT_NODE or
$node->nodeType == DOCUMENT_FRAGMENT_NODE) {
for (my $i = 0; $i < $node->childNodes->length; ++$i) {
$ret .= xmlText($node->childNodes->[$i]);
}
}
return $ret;
}
sub xmlResolveEntities {
my ($s) = @_;
my $parts = [ split(/&/, $s) ];
my $ret = $parts->[0];
for (my $i = 1; $i < @$parts; ++$i) {
my $rp = [ split(/;/, $parts->[$i]) ];
if (@$rp == 1) {
$ret .= $parts->[$i];
next;
}
my $ch;
if ($rp->[0] eq 'lt') {
$ch = '<';
} elsif ($rp->[0] eq 'gt') {
$ch = '>';
} elsif ($rp->[0] eq 'amp') {
$ch = '&';
} elsif ($rp->[0] eq 'quot') {
$ch = '"';
} elsif ($rp->[0] eq 'apos') {
$ch = "'";
} elsif ($rp->[0] eq 'nbsp') {
$ch = ' '; # "\x160"
} else {
warn 'unknown entity '.$rp->[0];
#my span = window.document.createElement('span');
#span.innerHTML = '&' + rp[0] + '; ';
#ch = span.childNodes[0].nodeValue.charAt(0);
}
$ret .= ($ch . $rp->[1]);
}
return $ret;
}
1;
__END__
=head1 NAME
XML::DOM::Lite::XSLT - XSLT engine for XML::DOM::Lite
=head1 SYNOPSIS
use XML::DOM::Lite qw(Parser XSLT);
$parser = Parser->new( whitespace => 'strip' );
$xsldoc = $parser->parse($xsl);
$xmldoc = $parser->parse($xml);
$output = XSLT->process($xmldoc, $xsldoc);
=head1 DESCRIPTION
=head1 AUTHOR
Copyright (C) 2005 Richard Hundt <richard NO SPAM AT protea-systems.com>
=head1 ACKNOWLEDGEMENTS
Google - for implementing the XPath and XSLT JavaScript libraries which I shamelessly stole
=head1 LICENCE
This library is free software and may be used under the same terms as
Perl itself.
=cut