Reputation: 95
I have xml where I'm trying to convert two empty elements into a start/end tag pair. My xml looks like...
<para>This is a paragraph with text <Emph type="bold" mode="start"/>this text needs to be bolded<Emph type="bold" mode="end"/>, and we might also have some text that needs to be <Emph type="italic" mode="start"/>italicized<Emph type="italic" mode="end"/>.
And I'm trying to match the pairs of empty elements to turn them into
<para>This is a paragraph with text <b>this text needs to be bolded</b>, and we might also have some text that needs to be <i>italicized</i>.
I'm using XSLT 2.0, and I can match the empty elements, but I can't just convert one to a start tag and the other to an end tag. I'm wrestling with treating the two as a single set (keeping in mind that they could have other elements inside that need to be processed).
Any advice would be appreciated.
Upvotes: 1
Views: 636
Reputation: 163312
Well, you don't create tags in XSLT, you create a tree of nodes. So you're not trying to convert one empty-element tag into a start tag and the other into an end tag, you are trying to convert the text between these two empty-element nodes into a new element node. This affects the whole way you should be thinking about the problem: think nodes, not tags.
Positional grouping as shown by Martin Honnen is one way of tackling this. Another approach is "sibling recursion" which looks something like this:
<xsl:template match="para">
<xsl:apply-templates select="node()[1]" mode="traverse"/>
</xsl:template>
<xsl:template match="node()" mode="traverse">
<xsl:copy-of select="."/>
<xsl:apply-templates select="following-sibling::node()[1]"
mode="traverse"/>
</xsl:template>
<xsl:template match="Emph[@mode = 'start'][@type='bold']"
mode="traverse">
<b>
<xsl:apply-templates match="following-sibling::node()[1]"
mode="traverse"/>
</b>
<xsl:apply-templates match="following-sibling::Emph[@mode = 'end']
[@type='bold'][1]/following-sibling::node()[1]"
mode="traverse"/>
</xsl:template>
<xsl:template match="Emph[@mode = 'end'][@type='bold']"
mode="traverse"/>
What's happening here is that for each sibling node, you copy the node and then move on to process its next sibling; but when you hit a "start" marker, the result of that recursion gets added to a new b element, and when you hit the end marker the recursion stops, with the scan being resumed again by the template for the start marker.
The sibling recursion also works in XSLT 1.0.
Upvotes: 1
Reputation: 1458
I have a suggestion here that covers markup overlapping parapraphs:
<xsl:output indent="yes" method="xml"/>
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="para/text()">
<xsl:choose>
<xsl:when test="preceding::Emph[@type = 'bold'][1]/@mode = 'start'
and following::Emph[@type = 'bold'][1]/@mode = 'end'">
<b>
<xsl:value-of select="."/>
</b>
</xsl:when>
<xsl:when test="preceding::Emph[@type = 'italic'][1]/@mode = 'start'
and following::Emph[@type = 'italic'][1]/@mode = 'end'">
<i>
<xsl:value-of select="."/>
</i>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="Emph"/>
This does not cover overlapping or nesting bold and italic. Another disadvantage here is that it operates only on text nodes which makes further processing a bit more complicated (Martin Honnen solved this with nested groups). However, I still post my approach here, as it could be of interest.
Upvotes: 0
Reputation: 167516
Here is a suggestion that should do for regular input with matching pairs of Emph mode="start"/Emph mode="end"
as siblings:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="2.0">
<xsl:param name="type-map">
<map input="bold">b</map>
<map input="italic">i</map>
</xsl:param>
<xsl:key name="type-map" match="map" use="@input"/>
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* |node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[Emph]">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:for-each-group select="node()" group-starting-with="Emph[@mode = 'start']">
<xsl:choose>
<xsl:when test="self::Emph[@mode = 'start']">
<xsl:variable name="start" select="."/>
<xsl:for-each-group select="current-group() except ." group-ending-with="Emph[@mode = 'end' and @type = $start/@type]">
<xsl:choose>
<xsl:when test="current-group()[last()][self::Emph[@mode = 'end' and @type = $start/@type]]">
<xsl:element name="{key('type-map', $start/@type, $type-map)}">
<xsl:apply-templates select="current-group()[position() lt last()]"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Implementing the overlapping case mentioned in a comment is much more complicated.
Upvotes: 1