XSLT to flatten XML
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>
(revised)
<xsl:for-each "//author | /book[not(author)]" />
<xsl:variable name="style" select="('even','odd')[(position() mod 2) + 1]" />
<xsl:apply-templates select="."><xsl:with-param name="style" select="$style" /></xsl:apply-templates>
</xsl:for-each>
Then in your author template you can construct the book elements using ..
精彩评论