Could anyone please help me with the following transformation?
Here is the input xml:
<?xml version="1.0" encoding="UTF-8"?>
<book>
<title>My book</title>
<pages>200</pages>
<size>big</size>
<author>
<name>Smith</name>
</author>
<author>
<name>Wallace</name>
</author>
<author>
<name>Brown</name>
</author>
</book>
<book>
<title>Other book</title>
<pages>100</pages>
<size>small</size>
<author>King</author>
</book>
<book>
<title>Pretty book</title>
<pages>150</pages>
<size>medium</size>
</book>
This is the desired output
<book style="even">
<title>My book</title>
<pages>200</pages>
<size>big</size>
<author-name>Smith</author-name>
</book>
<book style="odd">
<title>My book</title>
<pages>200</pages>
<size>big</size>
<author-name>Wallace</author-name>
</book>
<book style="even">
<title>My book</title>
<pages>200</pages>
<size>big</size>
<author-name>Brown</author-name>
</book>
<book style="odd" >
<title>Other book</title>
<pages>100</pages>
<size>small</size>
<author-name>King</author-name>
</book>
<book style="even">
<title>Pretty book</title>
<pages>150</pages>
<size>medium</size>
<author-name />
</book>
I tried using xsl:for-each
loops, but I suppose they brought me to a dead end. The tricky part here is the "style" attribute that somehow needs to be "global" no matter how many author tags are placed in any book.
This simple, pure XSLT 1.0 transformation (no conditionals, no xsl:for-each
, no parameter passing, no xsl:element
, no use of the infamously inefficient //
):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my" exclude-result-prefixes="my" >
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<my:names>
<n>odd</n>
<n>even</n>
</my:names>
<xsl:variable name="vStyles"
select="document('')/*/my:names/*"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="book/author">
<xsl:variable name="vPos">
<xsl:number level="any" count="book/author|book[not(author)]"/>
</xsl:variable>
<book style="{$vStyles[$vPos mod 2 +1]}">
<xsl:copy-of select="@*|../node()[not(self::author)]"/>
<author-name>
<xsl:value-of select="normalize-space()"/>
</author-name>
</book>
</xsl:template>
<xsl:template match="book[author]">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="book[not(author)]">
<xsl:variable name="vPos">
<xsl:number level="any" count="book/author|book[not(author)]"/>
</xsl:variable>
<book style="{$vStyles[$vPos mod 2 +1]}">
<xsl:copy-of select="@*|node()"/>
<author-name/>
</book>
</xsl:template>
<xsl:template match="book[author]/*[not(self::author)]"/>
</xsl:stylesheet>
when applied to this XML document (the provided one wrapped into a single top element):
<t>
<book>
<title>My book</title>
<pages>200</pages>
<size>big</size>
<author>
<name>Smith</name>
</author>
<author>
<name>Wallace</name>
</author>
<author>
<name>Brown</name>
</author>
</book>
<book>
<title>Other book</title>
<pages>100</pages>
<size>small</size>
<author>King</author>
</book>
<book>
<title>Pretty book</title>
<pages>150</pages>
<size>medium</size>
</book>
</t>
produces exactly the wanted, correct result:
<t>
<book style="even">
<title>My book</title>
<pages>200</pages>
<size>big</size>
<author-name>Smith</author-name>
</book>
<book style="odd">
<title>My book</title>
<pages>200</pages>
<size>big</size>
<author-name>Wallace</author-name>
</book>
<book style="even">
<title>My book</title>
<pages>200</pages>
<size>big</size>
<author-name>Brown</author-name>
</book>
<book style="odd">
<title>Other book</title>
<pages>100</pages>
<size>small</size>
<author-name>King</author-name>
</book>
<book style="even">
<title>Pretty book</title>
<pages>150</pages>
<size>medium</size>
<author-name/>
</book>
</t>
Explanation: Appropriate use of xsl:number
and templates/pattern matching.
This stylesheet first stores all author nodes as well as all book nodes without authors in a global variable. "/." sorts them in document order. Then the main template iterates over all nodes in this variable and generates output according to the position in the sequence.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:variable name="authors" select="(//author | //book[not(author)])/."/>
<xsl:template match="/">
<xsl:element name="result">
<xsl:for-each select="$authors">
<xsl:apply-templates select=".">
<xsl:with-param name="position" select="position()+1"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:element>
</xsl:template>
<xsl:template match="text()|title|pages|size">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="book">
<xsl:param name="position" select="1"/>
<xsl:element name="book">
<xsl:attribute name="style">
<xsl:if test="$position mod 2 = 0">even</xsl:if>
<xsl:if test="$position mod 2 = 1">odd</xsl:if>
</xsl:attribute>
<xsl:apply-templates select="title"/>
<xsl:apply-templates select="pages"/>
<xsl:apply-templates select="size"/>
<xsl:element name="author-name"/>
</xsl:element>
</xsl:template>
<xsl:template match="author">
<xsl:param name="position" select="1"/>
<xsl:element name="book">
<xsl:attribute name="style">
<xsl:if test="$position mod 2 = 0">even</xsl:if>
<xsl:if test="$position mod 2 = 1">odd</xsl:if>
</xsl:attribute>
<xsl:apply-templates select="../title"/>
<xsl:apply-templates select="../pages"/>
<xsl:apply-templates select="../size"/>
<xsl:element name="author-name">
<xsl:if test="name">
<xsl:value-of select="name"/>
</xsl:if>
<xsl:if test="not(name)">
<xsl:value-of select="."/>
</xsl:if>
</xsl:element>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With