Reputation: 195
I have multiple instances of xml similar to the following:
<refbody>
<ul>
<li><uicontrol>FOO</uicontrol>BAR</li>
</ul>
<p>Values: 000 - 999</p>
<ul>
<li><uicontrol>FOO</uicontrol>BAR</li>
<li><uicontrol>FOO</uicontrol>BAR</li>
<li><uicontrol>FOO</uicontrol>BAR</li>
<li><uicontrol>FOO</uicontrol>BAR</li>
</ul>
<p>Values:</p>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
</refbody>
What I would like to have happen is to consolidate all <ul>
elements into a single <dl>
.
Each <li>
should create a child <dlentry>
.
Each <uicontrol>
should become a <dt>
that is a child of its <dlentry>
.
The remaining text in the <li>
should be put in a <dd>
element, which is also a child of the <dlentry>
.
My style sheet (provided below) accomplishes much this already. Where I am running into trouble is the last requirement:
Any <p>
needs to be put inside of the <dlentry>
associated with its preceding-sibling::li[1]
.
So the desired result looks like this:
<refbody>
<dl>
<dlentry>
<dt>FOO</dt>
<dd>BAR
<p>Values: 000 - 999</p>
</dd>
</dlentry>
<dlentry>
<dt>FOO</dt>
<dd>BAR</dd>
</dlentry>
<dlentry>
<dt>FOO</dt>
<dd>BAR</dd>
</dlentry>
<dlentry>
<dt>FOO</dt>
<dd>BAR</dd>
</dlentry>
<dlentry>
<dt>FOO</dt>
<dd>BAR
<p>Values:</p>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
</dd>
</dlentry>
</dl>
</refbody>
I think I need to use keys to do this, but I just can't get it to work properly. I would really appreciate it if someone could tell me what I'm doing wrong.
Here is my stylesheet:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:key name="kValues" match="p"
use="generate-id[preceding::li[1]]"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="ul[ancestor::refbody][1]">
<dl>
<xsl:for-each select="child::li|following::li">
<xsl:variable name="vValues">
<xsl:value-of select="key('kValues',generate-id())"/>
</xsl:variable>
<dlentry>
<dt>
<xsl:value-of select="child::uicontrol"/>
</dt>
<dd>
<xsl:value-of select="text()"/>
<xsl:if test="$vValues">
<xsl:copy-of select="$vValues" />
</xsl:if>
</dd>
</dlentry>
</xsl:for-each>
</dl>
</xsl:template>
</xsl:stylesheet>
Upvotes: 2
Views: 173
Reputation: 243529
I. This transformation (my solution using keys):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kFollowing"
match="node()[self::p or self::text() and preceding-sibling::ul]"
use="generate-id(preceding::li[1])"/>
<xsl:template match="/*[ul]">
<refbody>
<dl>
<xsl:apply-templates/>
</dl>
</refbody>
</xsl:template>
<xsl:template match="li">
<dlentry>
<xsl:apply-templates/>
</dlentry>
</xsl:template>
<xsl:template match="li/text()">
<dd>
<xsl:value-of select="normalize-space()"/>
</dd>
</xsl:template>
<xsl:template match="li[last()]/text()">
<dd>
<xsl:value-of select="."/>
<xsl:copy-of select="key('kFollowing', generate-id(..))"/>
</dd>
</xsl:template>
<xsl:template match="uicontrol">
<dt>
<xsl:apply-templates/>
</dt>
</xsl:template>
<xsl:template match="p/text()"/>
</xsl:stylesheet>
when applied on the provided XML document:
<refbody>
<ul>
<li>
<uicontrol>FOO</uicontrol>BAR
</li>
</ul>
<p>Values: 000 - 999</p>
<ul>
<li>
<uicontrol>FOO</uicontrol>BAR
</li>
<li>
<uicontrol>FOO</uicontrol>BAR
</li>
<li>
<uicontrol>FOO</uicontrol>BAR
</li>
<li>
<uicontrol>FOO</uicontrol>BAR
</li>
</ul>
<p>Values:</p>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
</refbody>
produces the wanted, correct result:
<refbody>
<dl>
<dlentry>
<dt>FOO</dt>
<dd>BAR
<p>Values: 000 - 999</p>
</dd>
</dlentry>
<dlentry>
<dt>FOO</dt>
<dd>BAR</dd>
</dlentry>
<dlentry>
<dt>FOO</dt>
<dd>BAR</dd>
</dlentry>
<dlentry>
<dt>FOO</dt>
<dd>BAR</dd>
</dlentry>
<dlentry>
<dt>FOO</dt>
<dd>BAR
<p>Values:</p>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
</dd>
</dlentry>
</dl>
</refbody>
II. Your solution after correcting all found mistakes:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kValues" match="p"
use="generate-id(preceding::li[1])"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="ul[ancestor::refbody][1]">
<dl>
<xsl:for-each select="child::li|following::li">
<xsl:variable name="vValues" select="key('kValues',generate-id())"/>
<dlentry>
<dt>
<xsl:value-of select="child::uicontrol"/>
</dt>
<dd>
<xsl:value-of select="text()[normalize-space()]"/>
<xsl:copy-of select="$vValues" />
</dd>
</dlentry>
</xsl:for-each>
</dl>
</xsl:template>
<xsl:template match="p|ul|text()"/>
</xsl:stylesheet>
When this corrected transformation is applied on the same XML document (above) it produces exactly the wanted, correct result.
III. Analysis of the mistakes that I found:
...
<xsl:key name="kValues" match="p"
use="generate-id[preceding::li[1]]"/>
...
here any p
element is indexed by the string value of its generate-id
children that have a preceding li
.
However, there are no elements named generate-id
in the XML document.
What was really intended was to use the generate-id()
function. Function calls are formed by the name of the function, followed by a sequence of its actual argument values, enclosed in regular, round brackets (
and )
. Replacing the round brackets by square brackets radically changes the semantics of the expression, turning the function call into a (relative) location step.
.2. This:
<xsl:variable name="vValues">
<xsl:value-of select="key('kValues',generate-id())"/>
</xsl:variable>
Is not only a (very) bad practice, but is often cause of errors, like in the current case. You are creating a tree (document node), not a nodeset of elements. The tree always has at least one node (the document node) and its boolean value, therefore is always true.
Never ever use the above syntax.
The correct variable declaration is:
<xsl:variable name="vValues" select="key('kValues',generate-id())"/>
.3. There are no template rules to prevent the copying by the identity template of any unwanted ul
, p
, and text()
nodes and these appear in the result of the original transformation.
Upvotes: 1
Reputation: 66783
The following stylesheet produces the desired output without the use of keys.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="refbody">
<xsl:copy>
<dl>
<xsl:apply-templates select="ul" />
</dl>
</xsl:copy>
</xsl:template>
<xsl:template match="ul">
<xsl:apply-templates select="*"/>
</xsl:template>
<xsl:template match="li">
<dlentry><xsl:apply-templates select="@*|node()"/></dlentry>
</xsl:template>
<xsl:template match="uicontrol">
<dt><xsl:apply-templates select="@*|node()"/></dt>
</xsl:template>
<xsl:template match="li/text()">
<dd>
<xsl:copy/>
<xsl:apply-templates
select="parent::li
[not(following-sibling::li)]
/parent::ul
/following-sibling::p
[
count(preceding-sibling::ul)
= count(current()/../../preceding-sibling::ul)
+1
]"/>
</dd>
</xsl:template>
</xsl:stylesheet>
Upvotes: 1