XSLT 2.0 - Nesting Elements In Output From Non-Nested Input
I'm working on a style sheet that outputs in a hierarchical from an input file with none. Every element of the input file is a sibling of one another, 开发者_开发技巧and each "header" element denotes a different section.
Sample input file (the actual sections under each header are about 10 times longer than this):
<Header>
Header1
</Header>
<Sub1>
Sub1 first
</Sub1>
<Sub1>
Sub1 second
</Sub1>
<Sub2>
Sub2 first, Sub1 second
</Sub2>
<Sub1>
Sub1 third
</Sub1>
<Sub2>
Sub2 first, Sub1 third
</Sub2>
<Header>
Header2
</Header>
Etc...
And the output of the above input should look like:
<Header>
Header1
<Step>
Sub1 first
</Step>
<Step>
Sub1 second
<Step>
Sub2 first, Sub1 second
</Step>
</Step>
<Step>
Sub1 third
<Step>
Sub2 first, Sub1 third
</Step>
</Step>
</Header>
<Header>
Header2
Etc.....
</Header>
Right now, I'm able to get the output up to the "Sub2 first, sub1 second". When I know that the next element is not another Sub2 or deeper subelement (Sub3), then I break from my Sub2 template, returning back into Sub1 template. And from here, my position variable still has the position of "Sub1 second". I have positioning information processed each time a template is called - so I have all the current position in document order for "Sub2 first, sub1 second" but once I break from the template back into Sub1's template, I lose that information. I can't seem to get the current position in document order to determine what the real next element is. A modifiable global parameter or variable would be ideal, but I know that's not possible in XSLT.
I'm not sure how to go about accomplishing this. Any suggestions would be great!
This XSLT 2.0 transformation:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/*">
<t>
<xsl:apply-templates select="*[1]">
<xsl:with-param name="pScope" select="*"/>
<xsl:with-param name="pElemName" select="name(*[1])"/>
</xsl:apply-templates>
</t>
</xsl:template>
<xsl:template match="*">
<xsl:param name="pScope"/>
<xsl:param name="pElemName" select="'Step'"/>
<xsl:for-each-group select="$pScope"
group-starting-with="*[name()= name($pScope[1])]">
<xsl:element name="{$pElemName}">
<xsl:value-of select="."/>
<xsl:apply-templates select="current-group()[2]">
<xsl:with-param name="pScope" select=
"current-group()[position() > 1]"/>
</xsl:apply-templates>
</xsl:element>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document (based on the provided, but wrapped into a top element and added one more header with different names):
<t>
<Header>Header1</Header>
<Sub1>Sub1 first</Sub1>
<Sub1>Sub1 second</Sub1>
<Sub2>Sub2 first, Sub1 second</Sub2>
<Sub1>Sub1 third</Sub1>
<Sub2>Sub2 first, Sub1 third</Sub2>
<Header>Header2</Header>
<x>Sub1 first</x>
<x>Sub1 second</x>
<y>Sub2 first, Sub1 second</y>
<x>Sub1 third</x>
<z>Sub2 first, Sub1 third</z>
</t>
produces the wanted, correct output:
<t>
<Header>Header1<Step>Sub1 first</Step>
<Step>Sub1 second<Step>Sub2 first, Sub1 second</Step>
</Step>
<Step>Sub1 third<Step>Sub2 first, Sub1 third</Step>
</Step>
</Header>
<Header>Header2<Step>Sub1 first</Step>
<Step>Sub1 second<Step>Sub2 first, Sub1 second</Step>
</Step>
<Step>Sub1 third<Step>Sub2 first, Sub1 third</Step>
</Step>
</Header>
</t>
Explanation:
Using
<xsl:for-each-group>
with thegroup-starting-with
attribute.Fine-grained processing -- templates are applied on the second element of the
current-group()
and the scope for the next grouping is passed as parameter.
Just for fun an XSLT 1.0 stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="*">
<xsl:param name="pNames" select="'|'"/>
<xsl:if test="not(contains($pNames,concat('|',name(),'|')))">
<xsl:variable name="vNext" select="following-sibling::*[1]"/>
<xsl:copy>
<xsl:apply-templates select="node()[1]"/>
<xsl:apply-templates select="$vNext">
<xsl:with-param name="pNames"
select="concat($pNames,name(),'|')"/>
</xsl:apply-templates>
</xsl:copy>
<xsl:apply-templates select="$vNext" mode="search">
<xsl:with-param name="pNames" select="$pNames"/>
<xsl:with-param name="pSearch" select="name()"/>
</xsl:apply-templates>
</xsl:if>
</xsl:template>
<xsl:template match="*" mode="search">
<xsl:param name="pNames"/>
<xsl:param name="pSearch"/>
<xsl:if test="not(contains($pNames,concat('|',name(),'|')))">
<xsl:choose>
<xsl:when test="name()=$pSearch">
<xsl:apply-templates select=".">
<xsl:with-param name="pNames" select="$pNames"/>
</xsl:apply-templates>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="following-sibling::*[1]"
mode="search">
<xsl:with-param name="pNames" select="$pNames"/>
<xsl:with-param name="pSearch" select="$pSearch"/>
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
With welformed input:
<root>
<Header>Header1</Header>
<Sub1>Sub1 first</Sub1>
<Sub1>Sub1 second</Sub1>
<Sub2>Sub2 first, Sub1 second</Sub2>
<Sub1>Sub1 third</Sub1>
<Sub2>Sub2 first, Sub1 third</Sub2>
<Header>Header2</Header>
</root>
Output:
<root>
<Header>Header1
<Sub1>Sub1 first</Sub1>
<Sub1>Sub1 second
<Sub2>Sub2 first, Sub1 second</Sub2>
</Sub1>
<Sub1>Sub1 third
<Sub2>Sub2 first, Sub1 third</Sub2>
</Sub1>
</Header>
<Header>Header2</Header>
</root>
With the name replacement and the new requeriment of one level with several names, this stylesheet:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:r="rules"
exclude-result-prefixes="r">
<r:r m="|Header|">Header</r:r>
<r:r m="|Sub1|">Step</r:r>
<r:r m="|Sub2|">Step</r:r>
<xsl:variable name="vRules" select="document('')/*/r:r"/>
<xsl:template match="*">
<xsl:param name="pNames" select="'|'"/>
<xsl:variable name="vName" select="concat('|',name(),'|')"/>
<xsl:if test="not(contains($pNames,$vName))">
<xsl:variable name="vNext" select="following-sibling::*[1]"/>
<xsl:variable name="vMatch"
select="$vRules[contains(@m,$vName)]"/>
<xsl:variable name="vReplace"
select="concat($vMatch,name((.)[not($vMatch)]))"/>
<xsl:variable name="vSearch"
select="concat($vMatch/@m,$vName)"/>
<xsl:element name="{$vReplace}">
<xsl:apply-templates select="node()[1]"/>
<xsl:apply-templates select="$vNext">
<xsl:with-param name="pNames"
select="concat($pNames,$vSearch)"/>
</xsl:apply-templates>
</xsl:element>
<xsl:apply-templates select="$vNext" mode="search">
<xsl:with-param name="pNames" select="$pNames"/>
<xsl:with-param name="pSearch" select="$vSearch"/>
</xsl:apply-templates>
</xsl:if>
</xsl:template>
<xsl:template match="*" mode="search">
<xsl:param name="pNames"/>
<xsl:param name="pSearch"/>
<xsl:variable name="vName" select="concat('|',name(),'|')"/>
<xsl:choose>
<xsl:when test="contains($pNames,$vName)"/>
<xsl:when test="contains($pSearch,$vName)">
<xsl:apply-templates select=".">
<xsl:with-param name="pNames" select="$pNames"/>
</xsl:apply-templates>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="following-sibling::*[1]"
mode="search">
<xsl:with-param name="pNames" select="$pNames"/>
<xsl:with-param name="pSearch" select="$pSearch"/>
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Output:
<root>
<Header>Header1
<Step>Sub1 first</Step>
<Step>Sub1 second
<Step>Sub2 first, Sub1 second</Step>
</Step>
<Step>Sub1 third
<Step>Sub2 first, Sub1 third</Step>
</Step>
</Header>
<Header>Header2</Header>
</root>
Note: Synonyms for same level should be in r:r/@m
Your input xml isn't really an xml so I added a root node. I'd advise against using xslt to do this kind of processing. But if you must - here's something to get you started.
Btw. Do your subsections have any kind of Id?
<?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="xml" indent="yes"/>
<xsl:template match="/Root">
<Root>
<xsl:apply-templates select="Header"/>
</Root>
</xsl:template>
<xsl:template match="Header">
<Header>
<xsl:copy-of select="text()"/>
<xsl:variable name="current" select="."/>
<xsl:apply-templates select="/Root/Sub1[preceding-sibling::Header[1]/. = $current]"/>
</Header>
</xsl:template>
<xsl:template match="Sub1">
<Step>
<xsl:copy-of select="text()"/>
<xsl:variable name="current" select="."/>
<xsl:apply-templates select="/Root/Sub2[preceding-sibling::Sub1[1]/. = $current]"/>
</Step>
</xsl:template>
<xsl:template match="Sub2">
<Step>
<xsl:copy-of select="text()"/>
<!-- todo -->
</Step>
</xsl:template>
</xsl:stylesheet>
精彩评论