1

I am using XSLT 2.0. Inside a xsl:template (template-1), I use xsl:analyze-string to create new span elements with an xml:lang attribute. I have a second template (template-2) that adds a class attribute to elements that contain a xml:lang attribute. In my stylesheet, the newly-created span elements created by the first template are not being processed by the second. How can I fix this, and have the second template operate on results from the first?

Example:

Input: <p>The base form of a noun is technically called a <span xml:lang="sa-Latn">prātipadika</span> (प्रातिपदिक).</p>

Desired Output: <p>The base form of a noun is technically called a <span xml:lang="sa-Latn" class="lang-sa-latn">prātipadika</span> (<span xml:lang="sa-Deva" class="lang-sa-deva">प्रातिपदिक</span>).</p> This correct output has a final span with both xml:lang and class attributes.

Stylesheet Output: <p>The base form of a noun is technically called a <span xml:lang="sa-Latn" class="lang-sa-latn">prātipadika</span> (<span xml:lang="sa-Deva">प्रातिपदिक</span>).</p> This incorrect output is missing class="sa-lang-deva" on the final span.

(The additional classes produced by the stylesheet help to work around the deficient CSS support of a certain eBook reader.)


Here is my stylesheet:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns="http://www.w3.org/1999/xhtml"
                xpath-default-namespace="http://www.w3.org/1999/xhtml"
                xmlns:xml="http://www.w3.org/XML/1998/namespace"
                xmlns:epub="http://www.idpf.org/2007/ops">

    <xsl:output method="xhtml" encoding="utf-8" indent="no"/>

    <xsl:template match="/">
        <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
            <xsl:apply-templates/>
        </html>
    </xsl:template>

    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

    <!-- Template-1: Add xml:lang attribute to Devanagari text. -->
    <xsl:template match="element()/text()">
        <xsl:variable name="textValue" select="."/>
        <xsl:analyze-string select="$textValue" regex="([&#x0900;-&#x097f;]+)((\s+[&#x0900;-&#x097f;]+)*)">
            <xsl:matching-substring>
                <span xml:lang="sa-Deva"><xsl:value-of select="."/></span>
            </xsl:matching-substring>
            <xsl:non-matching-substring>
                <xsl:value-of select="."/>
            </xsl:non-matching-substring>
        </xsl:analyze-string>
    </xsl:template>

    <!-- Template-2: Add lang-* class attribute when xml:lang attribute present. -->
    <xsl:template match="*[@xml:lang]">
        <xsl:call-template name="addClass">
            <xsl:with-param name="newClass">lang-<xsl:value-of select="@xml:lang"/></xsl:with-param>
        </xsl:call-template>
    </xsl:template>

    <!-- Add a class attribute to an element. -->
    <xsl:template name="addClass">
        <xsl:param name="newClass"/>
        <xsl:copy>
            <xsl:copy-of select="@*"/>
            <xsl:attribute name="class"><xsl:value-of select="normalize-space(concat(@class, ' ', lower-case($newClass)))"/></xsl:attribute>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>
keithm
  • 2,813
  • 3
  • 31
  • 38

1 Answers1

1

You would need to capture the nodes created by the analyze-string into a variable and then apply templates to them. You can use template modes to avoid infinite recursion.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns="http://www.w3.org/1999/xhtml"
                xpath-default-namespace="http://www.w3.org/1999/xhtml"
                xmlns:xml="http://www.w3.org/XML/1998/namespace"
                xmlns:epub="http://www.idpf.org/2007/ops">

    <xsl:output method="xhtml" encoding="utf-8" indent="no"/>

    <xsl:template match="/">
        <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
            <xsl:apply-templates/>
        </html>
    </xsl:template>

    <xsl:template match="@*|node()" mode="#all">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()" mode="#current"/>
        </xsl:copy>
    </xsl:template>

    <!-- Template-1: Add xml:lang attribute to Devanagari text. Note no
         mode attribute, so only applies in default mode. -->
    <xsl:template match="text()">
        <xsl:variable name="textValue" select="."/>
        <xsl:variable name="nodes" as="node()*">
            <xsl:analyze-string select="$textValue" regex="([&#x0900;-&#x097f;]+)((\s+[&#x0900;-&#x097f;]+)*)">
                <xsl:matching-substring>
                    <span xml:lang="sa-Deva"><xsl:value-of select="."/></span>
                </xsl:matching-substring>
                <xsl:non-matching-substring>
                    <xsl:value-of select="."/>
                </xsl:non-matching-substring>
            </xsl:analyze-string>
        </xsl:variable>
        <!-- apply templates to generated nodes, but with a mode that stops
             this template from firing again -->
        <xsl:apply-templates select="$nodes" mode="no-deva" />
    </xsl:template>

    <!-- Template-2: Add lang-* class attribute when xml:lang attribute present. -->
    <xsl:template match="*[@xml:lang]" mode="#all">
        <xsl:call-template name="addClass">
            <xsl:with-param name="newClass">lang-<xsl:value-of select="@xml:lang"/></xsl:with-param>
        </xsl:call-template>
    </xsl:template>

    <!-- Add a class attribute to an element. -->
    <xsl:template name="addClass">
        <xsl:param name="newClass"/>
        <xsl:copy>
            <xsl:copy-of select="@*"/>
            <xsl:attribute name="class"><xsl:value-of select="normalize-space(concat(@class, ' ', lower-case($newClass)))"/></xsl:attribute>
            <xsl:apply-templates select="@*|node()" mode="#current"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

Working example

The changes I've made to your original XSLT are:

  • added mode="#all" to the identity template and "template 2", so they apply in all modes, and added mode="#current" to the relevant apply-templates instructions, so they recurse using whatever is the current mode.
  • surrounded the analyze-string with an <xsl:variable> to capture the node(s) that it generates.
  • apply-templates to these nodes using a different mode

Since I (deliberately) did not add mode="#all" to template 1 it only matches during the initial pass, and not during the apply-templates mode="no-deva".

Ian Roberts
  • 120,891
  • 16
  • 170
  • 183
  • This works great! Thanks. I didn't think of capturing nodes that way. The way you set up the modes is instructive too. – keithm Feb 08 '15 at 23:44