Reputation: 364
I am having some trouble with transforming XML with an XSL stylesheet where the output needs to have a structure that does not follow the structure in the XML-file.
Simple example:
Input:
<p>This is <b>bold text</b>, <i>italic text</i> and normal text</p>
My problem is that the character formats in the input are nested, and they must not be nested in the output (text without any apparent formating is considered having a "normal" character style, and must be defined so in the output.
Valid output:
<p>
<c style="normal">This is </c>
<c style="bold">bold text</c>
<c style="normal">, </c>
<c style="italic">italic text</c>
<c style="normal"> and normal text</c>
</p>
How does one go about doing that?
I can figure out how to get the bold and italic formatting in place, but not how to start a new "normal" format after having returned from one of the others, as I have no way of saving the current state. Note that it is not necessarilly "normal" that must follow "bold" or "italic", it is whatever format was active before the bold or italic, so what I am really after is some way of remembering the format I am currently using, so that I can define it once again after having used some other character formatting.
Please note that the following (obvious) styling is not valid as it contains nested formats. I have no problems with creating this:
<p>
<c style="normal">This is
<c style="bold">bold text</c>
,
<c style="italic">italic text</c>
and normal text
</c>
</p>
Upvotes: 2
Views: 120
Reputation: 122364
I would approach this by defining templates not for the elements but for the text nodes inside them, using the ancestors of each text node to work out what style
it should be:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes" />
<xsl:template match="p">
<p><xsl:apply-templates /></p>
</xsl:template>
<!-- direct text nodes under a p become "normal" -->
<xsl:template match="p/text()">
<c style="normal"><xsl:value-of select="." /></c>
</xsl:template>
<!-- text nodes under both an i and a b become bold-italic -->
<xsl:template match="b//i/text() | i//b/text()" priority="2">
<c style="bold-italic"><xsl:value-of select="." /></c>
</xsl:template>
<!-- text nodes under a b but not an i become bold -->
<xsl:template match="b/text()">
<c style="bold"><xsl:value-of select="." /></c>
</xsl:template>
<!-- text nodes under an i but not a b become italic -->
<xsl:template match="i/text()">
<c style="italic"><xsl:value-of select="." /></c>
</xsl:template>
</xsl:stylesheet>
The b
and i
elements themselves will use the default built-in template rule which simply does <xsl:apply-templates/>
(i.e. output nothing specifically for this node but continue processing children).
Alternatively, you could define templates for the elements and use parameters to pass the "current state" from one template to the next as you work your way down the tree. If you have XSLT 2.0 then this is a nice use for "tunnel" parameters:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:output indent="yes" />
<xsl:template match="p">
<p><xsl:apply-templates /></p>
</xsl:template>
<!-- output text nodes as <c style="current-style">. The $style parameter
is a sequence of strings giving the current nested styles, in order.
Therefore <b><i>some text</i></b> would be bold-italic whereas
<i><b>some text</b></i> would be italic-bold. -->
<xsl:template match="text()">
<xsl:param tunnel="yes" name="style" as="xs:string*" />
<!-- use "normal" if no current style -->
<c style="{if (count($style)) then string-join($style, '-') else 'normal'}">
<xsl:value-of select="."/>
</c>
</xsl:template>
<xsl:template match="b">
<xsl:param tunnel="yes" name="style" as="xs:string*" />
<xsl:apply-templates>
<xsl:with-param tunnel="yes" name="style" select="($style, 'bold')" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="i">
<xsl:param tunnel="yes" name="style" as="xs:string*" />
<xsl:apply-templates>
<xsl:with-param tunnel="yes" name="style" select="($style, 'italic')" />
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
The logic to decide what to pass as the $style
parameter when you apply-templates
can be as complex as necessary.
If you are limited to XSLT 1.0 then you don't have tunnel parameters so you'll have to make the param
and with-param
elements explicit at all levels, but this should give you some idea of how to proceed.
Upvotes: 2
Reputation: 2578
This should solve your problem:
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" encoding="UTF-8"/>
<!-- identity-copy template (see http://en.wikipedia.org/wiki/Identity_transform#Using_XSLT ) -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<!-- transformation b - bold -->
<xsl:template match="b">
<c style="bold">
<xsl:apply-templates/>
</c>
</xsl:template>
<!-- transformation i - italic -->
<xsl:template match="i">
<c style="italic">
<xsl:apply-templates/>
</c>
</xsl:template>
<!-- if text does not have b or i as an ancestor, it is normal -->
<xsl:template match="text()[not(ancestor::b) and not(ancestor::i)]">
<c style="normal">
<xsl:copy/>
</c>
</xsl:template>
</xsl:stylesheet>
Here is groovy script showing how it works: https://gist.github.com/akhikhl/8146185
Upvotes: 0