问题
I often need to aggregate a sequence of nodes using XSLT 1.0 but I always struggle to find a clean solution.
This is a typical example;
Input
<x>Foo/Red</x>
<x>Foo/Green</x>
<x>Foo/Blue</x>
<x>Bar/Hello</x>
<x>Bar/World</x>
Desired output
<y s="Foo">Red, Green, Blue</y>
<y s="Bar">Hello, World</y>
I always end up in a mess with this type of problem. Is there an elegant XSLT 1.0 solution to the above?
I'm using PHP
's libxslt
so I do have the exslt:node-set()
function available to use if necessary.
回答1:
Here an adaption of muenchian grouping to your example. For more info look e.g. here. If you ones have understand how it work and tried to adapt it to changing grouping issues it becomes quite handy.
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="kXprev" match="x" use="substring-before(text(),'/')"/>
<xsl:template name="y">
<xsl:param name="s" />
<y s="{$s}">
<xsl:for-each select="key('kXprev', $s)">
<xsl:if test="position()>1" >
<xsl:text> </xsl:text>
</xsl:if>
<xsl:value-of select="substring-after(text(),'/')"/>
</xsl:for-each>
</y>
</xsl:template>
<xsl:template match="/*">
<out>
<xsl:for-each select="x[count( . | key('kXprev',substring-before(text(),'/') )[1] ) =1]" >
<xsl:call-template name="y">
<xsl:with-param name="s" select="substring-before(text(),'/') "/>
</xsl:call-template>
</xsl:for-each>
</out>
</xsl:template>
</xsl:stylesheet>
回答2:
In general: Read up on Muenchian grouping.
For this specific problem (but also useful advice in general): break it up into smaller pieces.
One way to break it up: You want to produce one y
element for each distinct value of the expression substring-before(.,'/') for the x
elements in the input. Or possibly for each occurrence of x
where that substring differs from the corresponding substring for its immediately preceding sibling. First write a stylesheet which just produces the correct number of y
elements, with the appropriate value for the s
attribute. So the output is
<y s="Foo"/>
<y s="Bar"/>
How can you do that?
Once you've done that, you need to provide content for the y
element: for each x
in the input which shares the same prefix, print out the string value. In the final version you'll want commas and spaces, but getting them right involves some fussy details, so write the next version of your stylesheet to produce a sequence of z
elements as children of y
:
<y s="Foo"><z>Red</z><z>Green</z><z>Blue</z></y>
<y s="Bar"><z>Hello</z><z>World</z></y>
The z
elements need to appear inside the y
element. So the template that's producing the y
element changes to read:
<xsl:template match="...">
<!--* old code here ... *-->
<xsl:element name="y">
<xsl:attribute name="s">
<xsl:value-of select="..."/>
</xsl:attribute>
<!--* code to produce 'z' elements goes here *-->
</xsl:element>
</xsl:template>
What should the code for producing z
elements look like? Within a y
element for a given prefix value $prefix
, we want one z
element for each x
in the input that shares that prefix. So one simple way to do the job is to call apply-templates on just that set of y
elements. To avoid interference with the templates which match x
elements and produce y
elements, give it a mode. The call to apply-templates might look like this:
<xsl:apply-templates mode="z-production"
select="//x[substring-before(.,'/') = $prefix]"/>
Now write the template for x elements in mode z-production.
Finally, change the stylesheet to generate the commas and blanks instead of the z
elements.
There are other ways to break the problem down into smaller pieces; sometimes it's helpful to walk the tree from sibling to sibling instead of from parent to siblings.
来源:https://stackoverflow.com/questions/16817119/how-to-aggregate-nodes-in-xslt-1-0