consider this simple problem:
we wish to map this input to the same output except the first occurence of a 'foo' element with "@bar = '1'", we add a new attribute @wibble, so this:
<root>
<foo/>
<foo/>
<foo/>
<foo bar="1"/>
<foo bar="1"/>
<foo/>
<foo/>
<foo/>
<foo/>
<foo/>
</root>
goes to this:
<root>
<foo />
<foo />
<foo />
<foo wibble="2" bar="1" />
<foo bar="1" />
<foo />
<foo />
<foo />
<foo />
<foo />
</root>
I could implement this mapping using the identity pattern (not sure what this pattern is called), but it would go like this:
<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="/">
<xsl:apply-templates select="root" mode="findFirst"/>
</xsl:template>
<xsl:template match="@* | node()" mode="findFirst">
<xsl:copy>
<xsl:apply-templates select="@* | node()" mode="findFirst"/>
</xsl:copy>
</xsl:template>
<xsl:template match="foo[@bar='1'][1]" mode="findFirst">
<xsl:copy>
<xsl:attribute name="wibble">2</xsl:attribute>
<xsl:apply-templates select="@* | node()" mode="findFirst"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
i.e. we override the identity template with some match statement which matches the specific scenario we want to match, implement our overriding mapping, and then continue.
I use this style a lot.
Sometimes though the match statement is complex (we saw this in another question recently about mapping lines of code). I find these sort of matches problematic, in the above scenario the use case is simple, but sometimes the logic isnt easily (or at all) expressibly inside the match statement, in which case I'm tempted to fall back on recursive functional patterns, and in this case I'd write a recursive template like this.
<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>
<xsl:apply-templates select="root/foo[1]" mode="findFirst">
<xsl:with-param name="isFound" select="false()"/>
</xsl:apply-templates>
</root>
</xsl:template>
<xsl:template match="foo" mode="findFirst">
<xsl:param name="isFound"/>
<xsl:copy>
<xsl:if test="$isFound = false() and @bar = '1'">
<xsl:attribute name="wibble">2</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="@* | node()" mode="identity"/>
</xsl:copy>
<xsl:choose>
<xsl:when test="$isFound = false() and @bar = '1'">
<xsl:apply-templates select="following-sibling::foo[1]" mode="findFirst">
<xsl:with-param name="isFound" select="true()"/>
</xsl:apply-templates>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="following-sibling::foo[1]" mode="findFirst">
<xsl:with-param name="isFound" select="$isFound"/>
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="@* | node()" mode="identity">
<xsl:copy>
<xsl:apply-templates select="@* | node()" mode="identity"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
this basically treats the nodeset as a functional 'list', taking the head (and passing the tail implicitly). Now we can implement much more complex logic and use parameters to pass the current state of the (effectively fold) through the recursion, but at the cost of extra complexity.
BUT....
Is this style of programming sustainable in XSLT? - I always worry about stack overflow (ironically!), due to probable non tail recursion in the XSLT engine of the recursive template.
My knowledge of XSLT 3.0 is extremely limited (any references to good learning resources always appreciated), but in a FP language the alternative to direct recursion would be to use fold, where fold is written as a tail recursive function, and fold IS available in XSLT 3.0, but is this a sensible alternative?
are there other patterns of usage that I can use?
XSLT has xsl:iterate
(https://www.w3.org/TR/xslt-30/#iterate) which allows you to implement your sibling recursion in a declarative way that looks a bit like a loop and due to its structure and implementation avoids any stack overflow recursion; iterate example:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
expand-text="yes">
<xsl:template match="/*">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:iterate select="node()">
<xsl:param name="found" select="false()"/>
<xsl:variable name="is-first-foo" select="if (. instance of element(foo)) then not($found) and boolean(self::foo[@bar = 1]) else $found"/>
<xsl:choose>
<xsl:when test="$is-first-foo">
<xsl:copy>
<xsl:attribute name="wibble" select="2"/>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="."/>
</xsl:otherwise>
</xsl:choose>
<xsl:next-iteration>
<xsl:with-param name="found" select="$is-first-foo"/>
</xsl:next-iteration>
</xsl:iterate>
</xsl:copy>
</xsl:template>
<xsl:mode on-no-match="shallow-copy"/>
</xsl:stylesheet>
fold-left
is certainly also available at the XPath 3.1 level, integrating it with the XML syntax of XSLT (3.0) is a bit more convoluted than in XQuery 3.1 where basically all is an expression. But is is certainly an option; example online:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
xmlns:mf="http://example.com/mf"
expand-text="yes">
<xsl:function name="mf:add-attribute" as="element()">
<xsl:param name="element" as="element()"/>
<xsl:copy select="$element">
<xsl:attribute name="wibble" select="2"/>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:function>
<xsl:template match="/*">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:sequence
select="fold-left(
node(),
map { 'found-foos' : 0, 'nodes' : () },
function($a, $n) {
let $is-foo := $n instance of element(foo) and boolean($n/self::foo[@bar = 1]),
$is-first-foo := $a?found-foos = 0 and $is-foo
return
map {
'found-foos' : if ($is-foo) then $a?found-foos + 1 else $a?found-foos,
'nodes': ($a?nodes, if ($is-first-foo) then mf:add-attribute($n) else $n)
}
}
)?nodes"/>
</xsl:copy>
</xsl:template>
<xsl:mode on-no-match="shallow-copy"/>
</xsl:stylesheet>
And for your sample an accumulator might allow you to check your conditions in a declarative way and then use its value in your match pattern to check whether you need to add your attribute. Online sample of accumulator use:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
expand-text="yes">
<xsl:param name="pattern" static="yes" as="xs:string" select="'foo[@bar = 1][1]'"/>
<xsl:accumulator name="have-first-foo-bar" as="xs:boolean" initial-value="false()">
<xsl:accumulator-rule _match="{$pattern}" select="true()"/>
<xsl:accumulator-rule phase="end" _match="{$pattern}" select="false()"/>
</xsl:accumulator>
<xsl:template match="foo[accumulator-before('have-first-foo-bar')]">
<xsl:copy>
<xsl:attribute name="wibble" select="2"/>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:mode on-no-match="shallow-copy" use-accumulators="#all"/>
</xsl:stylesheet>