I know there is the current() function to retrieve the current node in XSL, but is there a way to be able to reference the "previous" and "next" nodes in reference to the current position?
No. The current context cannot know which nodes are "next" or "previous".
This is because when, for example, templates are applied, the mechanics go like this:
You do: <xsl:apply-templates select="*" /><!-- select 3 nodes (a,b,c) -->
The XSLT processor makes list of nodes to be processed (a,b,c)
For each of those nodes, the XSLT processor selects and executes a matching template
When the template is called, the current() node is defined, and position() is defined, but other than that the template has no knowledge of the execution flow.
Execution order is subject to the processors preferences, as long as the outcome is guaranteed to be the same. Your (theoretical) predictions might be true for one processor, and wrong for another. For a side-effects free programming language like XSLT, knowledge like this would be a dangerous thing, I think (because people would start to rely on execution order).
You can do use the following::sibling or preceding::sibling XPath axes, but that's something different from knowing what node will be processed next
EDIT
The above explanation tries to answer the question as it has been asked, but the OP meant something different. It's about grouping/outputting unique nodes only.
As per the OP's request, here a quick demonstration of how to achieve a grouping using the XPath axes.
XML (the items are pre-sorted):
<items>
<item type="a"></item>
<item type="a"></item>
<item type="a"></item>
<item type="a"></item>
<item type="b"></item>
<item type="e"></item>
</items>
XSLT
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:template match="/items">
<!-- copy the root element -->
<xsl:copy>
<!-- select those items that differ from any of their predecessors -->
<xsl:apply-templates select="
item[
not(#type = preceding-sibling::item/#type)
]
" />
</xsl:copy>
</xsl:template>
<xsl:template match="item">
<!-- copy the item to the output -->
<xsl:copy-of select="." />
</xsl:template>
</xsl:stylesheet>
Output:
<items>
<item type="a"></item>
<item type="b"></item>
<item type="e"></item>
</items>
Assuming you are talking about next and previous nodes in the document structure, as opposed to the current execution flow (such as a for-each loop), see the preceding-sibling and following-sibling axes: XPath Axes on W3.
To get the next node of the same name:
following-sibling::*[name() = name(current())]
Depending on the context, you may need to use name(.) in the first part.
You could track previous and current in variables for later handling.
I.e. you can keep tag(i-2), tag(i-1) and work with them in tag(i).
Just another idea.
Regards.
Related
Is there a way to copy 2 or 3 nodes of XML to a variable using XSLT? I'm looking for nodes and not the node values.
My sample XML is:
<node1>
<node2>
<node3>abc</node3>
<node4>def</node4>
</node2>
</node1>
<node1>
<node2>
<node3>123</node3>
<node4>456</node4>
</node2>
</node1>
And my XSLT sample is:
<xsl:for-each select="/node1/node2">
<xsl:if test="current()/node4 ! = '456'">
<xsl:copy-of select="./node3" />
<xsl:copy-of select="./node4" />
</xsl:if>
</xsl:foreach>
The problem with this is that I'm getting node4 everytime as the first node of the XML instead of current one. On node3 I'm getting the current one and there's no problem.
Your problem may be caused by the RTF(Resulting Tree Fragment) problem of XSLT-1.0:
A variable cannot contain a nodeset (but only a RTF)
I explained this problem in this SO answer.
RTFs cannot be queried by XPath-1.0 expressions, so they are only useful in a very limited subset of situations.
One solution would be using the newer XSLT-2.0.
It may help you to just select the nodes that you want all at once.
<xsl:variable name="sample">
<xsl:copy-of select="/node1/node2[node4!='456']/*[name()='node3' or name()='node4']"/>
</xsl:variable>
I have this schema:
<Root>
<Customers>
<Customer>
<ID>123</ID>
<Name>John</Name>
</Customer>
</Customers>
<Order>
<ID>ABC</ID>
<Title>Boat</Title>
</Order>
</Root>
I need to map the two different records into one repeating record like this:
<Root>
<Data>
<ID>123</ID>
<Text>John</Text>
</Data>
<Data>
<ID>ABC</ID>
<Text>Boat</Text>
</Data>
</Root>
I tried to create two table loopings (one for Customer, one for Order) and got:
<Root>
<Data>
<ID>ABC</ID>
<Text>Boat</Text>
</Data>
</Root>
Tried one table looping with two rows, and got the same. (Tried also with the Gated option to check for existance which made no difference)
In reality the schemas are huge, the map is super complex (not built by me), has a lot of functoids, and many wires. So I would like to avoid creating a custom XSL, which will be easier for this task, but harder to maintain. This is the only part I need to change.
Anybody ?
Thanks.
For complex mapping, using a custom XSLT almost always ends up being simpler and more maintainable than the spider-web we often find in BizTalk maps. However, as you stated, you need to avoid re-coding the complete map, as you are only changing a small section.
You should be able to use the 'Inline XSLT Call Template' script type in the Scripting Functoid to combine the best of BizTalk maps and custom XSLT.
Extending from Sean B. Durkin's answer, you will need to set up 2 Call Template functoids, the first one wired to your output 'Data' node
<xsl:template name="DataTemplate">
<xsl:apply-templates select="//*[local-name()='Customer']|//*[local-name()='Order']" />
</xsl:template>
Your second Call Template will output the relevant data into the current output 'Data' node. Note, this second Functoid does not need to be wired to any node in your output document.
<xsl:template match="*[local-name()='Customer']|*[local-name()='Order']">
<xsl:element name="Data">
<xsl:element name="ID">
<xsl:value-of select="*[local-name()='ID']"/>
</xsl:element>
<xsl:element name="Text">
<xsl:value-of select="*[local-name()='Name']|*[local-name()='Title']" />
</xsl:element>
</xsl:element>
</xsl:template>
No need to use XSLT here. Simply drag a Looping functoid on the map. Connect both the Customer and the Order record as inputs to the functoid (yes, you can have multiple inputs). Connect the output of the functoid to the Data record. Then connect your fields directly (ID --> ID, Name --> Text). This will work.
The individual input records to a Looping functoid don't have to be repeating records in themselves. By connecting multiple inputs to the functoid, you are looping over the collection of instances.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<Root>
<xsl:apply-templates select="*/Customers/Customer|*/Order"/>
</Root>
</xsl:template>
<xsl:template match="Customer|Order">
<Data>
<ID><xsl:value-of select="ID" /></ID>
<Text><xsl:value-of select="Name|Title" /></Text>
</Data>
</xsl:template>
</xsl:stylesheet>
XSLT available is 1.0.
I'm working on a dual-language site in an XML-based CMS (Symphony CMS), and need to replace the English version of a category name with the French version.
This is my source XML.
<data>
<our-news-categories-for-list-fr>
<entry id="118">
<title-fr handle="technology">Technologie</title-fr>
</entry>
<entry id="117">
<title-fr handle="healthcare">Santé</title-fr>
</entry>
</our-news-categories-for-list-fr>
<our-news-article-fr>
<entry id="100">
<categories>
<item id="117" handle="healthcare" section-handle="our-news-categories" section-name="Our News categories">Healthcare</item>
<item id="118" handle="technology" section-handle="our-news-categories" section-name="Our News categories">Technology</item>
</categories>
<main-text-fr mode="formatted"><p>Blah blah</p></main-text-fr>
</entry>
</our-news-article-fr>
</data>
This is part of the XSLT that I currently have for the French version.
<xsl:template match="data">
<xsl:apply-templates select="our-news-article-fr/entry"/>
</xsl:template>
<xsl:template match="our-news-article-fr/entry">
<xsl:if test="categories/item">
<p class="category">In:</p>
<ul class="category">
<xsl:for-each select="categories/item">
<li><xsl:value-of select="."/></li>
</xsl:for-each>
</ul>
</xsl:if>
</xsl:template match>
The problem: the visible text of the anchor (<xsl:value-of select="."/>) gives the English version of the category title.
The handles of the following nodes match (all handles are in English), and so I'm thinking I should be able to match one from the other.
/data/our-news-categories-for-list-fr/entry/title-fr/#handle (value of title-fr node is French translation of category title)
/data/our-news-article-fr/entry/categories/item/#handle
I'm new to XSLT and am struggling to find how to do this.
Many thanks.
Add <xsl:key name="k1" match="our-news-categories-for-list-fr/entry" use="#id"/> as a child of your XSLT stylesheet element. Then use e.g. <li><xsl:value-of select="key('k1', #id)/title-fr"/></li>.
../our-news-categories-for-list-fr/entry/title-fr/text() instead of #handle should do it
Your problem is that you are in
<our-news-article-fr>
and need to reference
<our-news-categories-for-list-fr>
so I do a parent .. to walk up the tree and then down the entry nodes
Within the xsl:for-each repetition instruction, the context is our-news-article-fr/entry/categories/item. If you use . you select the current context, that's why you are receiving the english version there.
Another approach (not saying the simplest and the best one) is simply specify an XPath expression which locates the correct node. You can use the ancestor:: axis to go out from the current context to data and then use your test node. The needed predicate must match against the current context using current() function:
<xsl:value-of select="
ancestor::data[1]/
our-news-categories-for-list-fr/
entry/
title-fr
[#handle=current()/#handle]
"/>
If data is the root of your document you can obviously use an absolute location path:
/
data/
our-news-categories-for-list-fr/
entry/
title-fr
[#handle=current()/#handle]
I'm adapting an XSLT from a third party which transforms an arbitrary number of XMLs into a single HTML document. It's a pretty complex script and it will be revised in the future, so I'm trying to do a minimal adaptation in order to get it to work for our needs.
The following is a stripped down version of the XSLT (containing the essentials):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml">
<xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/>
<xsl:param name="files" select="document('files.xml')//File"/>
<xsl:param name="root" select="document($files)"/>
<xsl:template match="/">
<xsl:for-each select="$root/RootNode">
<xsl:apply-templates select="."/>
</xsl:for-each>
</xsl:template>
<xsl:template match="RootNode">
<xsl:for-each select="//Node">
<xsl:text>Node: </xsl:text><xsl:value-of select="."/><xsl:text>, </xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Now files.xml contains a list of all the URLs of the files to be included (in this case the local files file1.xml and file2.xml). Because we want to read XMLs from memory rather than from disk, and because the invocation of the XSLT only allows for a single XML source, I have combined the two files in a single XML document. The following is a combination of two files (there may be more in a real situation)
<?xml version="1.0" encoding="UTF-8"?>
<TempNode>
<RootNode>
<Node>1</Node>
<Node>2</Node>
</RootNode>
<RootNode>
<Node>3</Node>
<Node>4</Node>
</RootNode>
</TempNode>
where the first RootNode originally resided in file1.xml and the second in file2.xml.
Due to the complexity of the actual XSLT, I've figured that my best shot is to try to alter the $root-param. I've tried the following:
<xsl:param name="root" select="/TempNode"/>
The problem is this. In the case of <xsl:param name="root" select="document($files)"/>, the XPath expression "//Node" in <xsl:for-each select="//Node"> selects the Node's from file1.xml and file2.xml independently, i.e. producing the following (desired) list:
Node: 1, Node: 2, Node: 3, Node: 4,
However, when I combine the content of the two files into a single XML and parse this (and use the suggested $root-definition), the expression "//Node" will select all Node's that are children of the TempNode. (In other words, the desired list, as represented above, is produced twice due to the combination with the outer <xsl:for-each select="$root/RootNode"> loop.)
(A side note: as observed in comment a) in this page, document() apparently changes the root node, perhaps explaining this behavior.)
My question becomes:
How can I re-define $root, using the combined XML as source instead of a multi-source through document(), so that the list is only produced once, without touching the remainder of the XSLT? It's like if $root defined using the document()-function, there is no common root node in the param. Is it possible to define a param with two "separate" node trees?
Btw: I've tried defining a document like this
<xsl:param name="root">
<xsl:for-each select="/TempNode/*">
<xsl:document>
<xsl:copy-of select="."/>
</xsl:document>
</xsl:for-each>
</xsl:param>
thinking it might solve the problem, but the "//Node" expression still fetches all the Nodes. Is the context node in the <xsl:template match="RootNode">-template actually somewhere in the input document and not the param? (Honestly, I'm pretty confused when it comes to context nodes.)
Thanks in advance!
(Updated more)
OK, some of the problem is becoming clear. First, just to make sure I understand, you aren't actually passing parameters for $files and $root to the XSLT processor invocation, right? (They might as well be variables rather than params?)
Now to the main issues... In XPath, when you evaluate an expression that begins with "/" (including "//"), the context node is ignored [mostly]. Therefore, when you have
<xsl:template match="RootNode">
<xsl:for-each select="//Node">
the matched RootNode is ignored. Maybe you wanted
<xsl:template match="RootNode">
<xsl:for-each select=".//Node">
in which the for-each would select Node elements that are descendants of the matched RootNode? This would fix your problem of generating the desired node list twice.
I inserted [mostly] above because I recalled that an "absolute location path" starts from "the root node of the document containing the context node". So the context node does affect what document is used for "//Node". Maybe that's what you intended all along? I guess I was slow to catch on to that.
(A side note: as observed in comment
a) in this page, document() apparently
changes the root node, perhaps
explaining this behavior.)
Or more precisely,
An absolute location path ["/..."]
followed by a relative location
path... selects the set of nodes that
would be selected by the relative
location path relative to the root
node of the document containing the
context node.
document() doesn't actually change anything, in the sense of side effects; rather, it returns a set of nodes contained (usually) by different documents than the primary source document. XSLT instructions like xsl:apply-templates and xsl:for-each establish new values for the context node inside the scope of their template bodies. So if you use xsl:apply-templates and xsl:for-each with select="document(...)/...", the context node inside the scope of those instructions will belong to an external document, so any use of "/..." as an XPath will start from that external document.
Updated again
How can I re-define $root, using the
combined XML as source instead of a
multi-source through document(), so
that the list is only produced once,
without touching the remainder of the
XSLT?
As #Alej hinted, it's really not possible given the above constraint. If you're selecting "//Node" in each iteration of the loop over "$root/RootNode", then in order for each iteration not to select the same nodes as the other iterations, each value of "$root/RootNode" must be in a different document. Since you're using the combined XML source, instead of a multi-source, this is not possible.
But if you don't insist that your <xsl:for-each select="//..."> XPath expression cannot change, it becomes very easy. :-) Just put a "." before the "//".
It's like if $root defined using the document()-function, there is no common root node
in the param.
The value of the param is a node-set. All nodes in the set may be contained in the same document, or they may not, depending on whether the first argument to document() is a nodeset or just a single node.
Is it possible to define a param with two "separate" node trees?
I believe by "separate", you mean "belonging to different documents"? Yes it is, but I don't think you can do it in XSLT 1.0 unless you're selecting nodes that belong to different documents in the first place.
You mentioned trying
<xsl:param name="root">
<xsl:for-each select="/TempNode/*">
<xsl:document>
<xsl:copy-of select="."/>
</xsl:document>
</xsl:for-each>
</xsl:param>
but <xsl:document> is not defined in XSLT 1.0, and your stylesheet says version="1.0". Do you have XSLT 2.0 available? If so, let us know and we can pursue this option. To be honest, <xsl:document> is not familiar territory for me. But I'm happy to learn along with you.
You can apply only nodes you need:
Input:
<?xml version="1.0" encoding="UTF-8"?>
<TempNode>
<RootNode>
<Node>1</Node>
<Node>2</Node>
</RootNode>
<RootNode>
<Node>3</Node>
<Node>4</Node>
</RootNode>
</TempNode>
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="html" indent="yes"/>
<xsl:template match="/">
<xsl:copy>
<xsl:apply-templates select="TempNode/RootNode"/>
</xsl:copy>
</xsl:template>
<xsl:template match="RootNode">
<xsl:value-of select="concat('RootNode-', generate-id(.), '
')"/>
<xsl:apply-templates select="Node"/>
</xsl:template>
<xsl:template match="Node">
<xsl:value-of select="concat('Node', ., '
')"/>
</xsl:template>
</xsl:stylesheet>
Output:
RootNode-N65540
Node1
Node2
RootNode-N65549
Node3
Node4
I have a repeating xml tree like this -
<xml>
<head>this is a sample xml file</head>
<item><color>yellow</color><color>red</color></item>
<item><color>blue</color></item>
<item><color>grey</color><color>red</color><color>blue</color></item>
</xml>
As you can see, each item can have a varying number of color tags.
I wish to get all the color tags for the first two items only.
<xsl:template match="xml">
<xsl:apply-templates select="item[position() < 3]/color" />
</xsl:template>
<xsl:template match="color">
<xsl:copy-of select="." />
</xsl:template>
Applied to your XML this yields:
<color>yellow</color>
<color>red</color>
<color>blue</color>
One potential possible way to get the items which is technically perfectly correct and in no way makes assumptions about the structure of your document with respect to namespacing, future requirements or template construction is just a simple:
/xml/item[position() < 3]/color
Try this...
/xml/item[ position() < 3 ]/color
Add an ordinal field to each item and select the first two.