1
votes

I want to produce an HTML file like this example:

English

Alt

En Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Nunc rutrum, eros sit amet ornare faucibus.

Français

Fr Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Nunc rutrum, eros sit amet ornare faucibus.

From the following example XML source, on which I have no control:

<?xml version="1.0" encoding="ISO-8859-1"?>
<content id="">
    <header language="en">
        <enabled>true</enabled>
        <img src="http://i.stack.imgur.com/xGCNw.gif" />
        <!-- more header-related elements -->
    </header>
    <header language="fr">
        <enabled>false</enabled>
        <img src="" />
        <!-- more header-related elements -->
    </header>
    <html language="en" type="source">
    <![CDATA[
        <p>En Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
        <p>Nunc rutrum, eros sit amet ornare faucibus.</p>
        ]]>
    </html>
    <html language="fr" type="source">
    <![CDATA[
        <p>Fr Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
        <p>Nunc rutrum, eros sit amet ornare faucibus.</p>
        ]]>
    </html>
</content>

So I wrote this XSLT to do it:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:template match="/">
        <h1>English</h1>
        <hr/>
        <xsl:for-each select="content/header">
            <xsl:call-template name="header">
                <xsl:with-param name="lang">en</xsl:with-param>
            </xsl:call-template>
        </xsl:for-each>
        <xsl:for-each select="content/html">
            <xsl:call-template name="html">
                <xsl:with-param name="lang">en</xsl:with-param>
            </xsl:call-template>
        </xsl:for-each>
        <hr/>
        <h1>Français</h1>
        <hr/>
        <xsl:for-each select="content/header">
            <xsl:call-template name="header">
                <xsl:with-param name="lang">fr</xsl:with-param>
            </xsl:call-template>
        </xsl:for-each>
        <xsl:for-each select="content/html">
            <xsl:call-template name="html">
                <xsl:with-param name="lang">fr</xsl:with-param>
            </xsl:call-template>
        </xsl:for-each>
    </xsl:template>

    <xsl:template name="header" match="*">
        <xsl:param name='lang'/>
        <xsl:if test="current()[@language=$lang]">
            <xsl:if test="enabled[normalize-space(text())='true']">
                <xsl:call-template name="image"/>
                <!-- more header-related elements -->
            </xsl:if>
        </xsl:if>
    </xsl:template>

    <xsl:template name="image" match="*">
        <xsl:if test="img[not(normalize-space(@src)='')]">
            <xsl:copy-of select="img"/>
        </xsl:if>
    </xsl:template>

    <xsl:template name="html" match="*">
        <xsl:param name='lang'/>
        <xsl:if test="current()[@language=$lang]">
            <xsl:value-of select="node()" disable-output-escaping="yes" />
        </xsl:if>
    </xsl:template>

</xsl:stylesheet>

That said, my solution feels very verbose, but seems to enable useful composition and reutilization properties, which are two things very important to me, since I will need to transform lots of XML documents whose structure is similar to this example; and thus, if most of my XSLTs can reuse parts of other XSLTs, this will be very useful.

A few notes of interest:

  • Transformation speed is of no importance, although it might be interesting to learn more on this aspect.
  • The transformations will never need to handle XML documents which encode more than the 2 example languages (en and fr).

So I would like to learn if my solution looks either anti-idiomatic-or even plain wrong-to you, XSLT wranglers.

2
You wrote [it] seems to enable useful composition and reutilization properties. I don't think so with those CDATA sections...user357812
Yes, but I have no control on the XML source. :\Daniel Jomphe
Good question, +1. See my answer for a solution that doesn't use any xslt conditional directives and any named templates.Dimitre Novatchev

2 Answers

1
votes

This transformation doesn't contain any conditional XSLT instructions or any named templates:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:my="my:my" exclude-result-prefixes="my">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <my:langs>
  <lang short="en">English</lang>
  <lang short="fr">Français</lang>
 </my:langs>

 <xsl:variable name="vLangs" select="document('')/*/my:langs/*"/>

 <xsl:template match="header">
  <xsl:apply-templates select="node()|@*"/>
  <hr/>
  <xsl:apply-templates select=
        "../html[@language=current()/@language]/text()"/>
 </xsl:template>

 <xsl:template match="header/@language">
  <h1><xsl:value-of select="$vLangs[@short=current()]"/></h1>
 </xsl:template>

 <xsl:template match="header[enabled='true']/img[@src and not(@src='')]">
  <xsl:copy-of select="."/>
 </xsl:template>

 <xsl:template match="html/text()">
  <xsl:value-of select="." disable-output-escaping="yes"/>
 </xsl:template>

 <xsl:template match="html|text()"/>
</xsl:stylesheet>

when applied on the provided XML document:

<content id="">
    <header language="en">
        <enabled>true</enabled>
        <img src="http://i.stack.imgur.com/xGCNw.gif" />
        <!-- more header-related elements -->
    </header>
    <header language="fr">
        <enabled>false</enabled>
        <img src="" />
        <!-- more header-related elements -->
    </header>
    <html language="en" type="source">
    <![CDATA[
             <p>En Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
             <p>Nunc rutrum, eros sit amet ornare faucibus.</p>
             ]]>
   </html>
    <html language="fr" type="source">
    <![CDATA[
            <p>Fr Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
            <p>Nunc rutrum, eros sit amet ornare faucibus.</p>
            ]]>
 </html>
</content>

the wanted, correct result is produced:

<h1>English</h1>
<img src="http://i.stack.imgur.com/xGCNw.gif" />
<hr />

             <p>En Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
             <p>Nunc rutrum, eros sit amet ornare faucibus.</p>

   <h1>Français</h1>
<hr />

            <p>Fr Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
            <p>Nunc rutrum, eros sit amet ornare faucibus.</p>

Do note:

  1. The use of templates and pattern matching.

  2. The use of <xsl:apply-templates>

  3. The use of predicates both in the match pattern of the templates and for selecting the necessary node-list in <xsl:apply-templates>

1
votes

That's how I would've wrote it:

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my"
exclude-result-prefixes="my">
<xsl:output method="html" indent="yes"/>

<my:language-labels>
    <lang code="en" label="English"/>
    <lang code="fr" label="Francais"/>
</my:language-labels>

<xsl:template match="/*">
    <xsl:apply-templates select="header"/>
</xsl:template>

<xsl:template match="header">
    <xsl:variable name="curLang" select="@language"/>
    <h1>
        <xsl:value-of select="document('')/*/my:language-labels/lang[@code = $curLang]/@label"/>
    </h1>
    <hr/>
    <xsl:copy-of select="img[not(normalize-space(@src) = '')][../enabled[normalize-space() = 'true']]"/>
    <xsl:value-of select="../html[@language = $curLang]" disable-output-escaping="yes"/>
 </xsl:template>

 </xsl:stylesheet>

Your HTML sample looks broken, so I may be a little wrong. My result is:

<h1>English</h1>
<hr>
<img src="http://i.stack.imgur.com/xGCNw.gif"> 
<p>En Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<p>Nunc rutrum, eros sit amet ornare faucibus.</p>
<h1>Francais</h1> 
<hr>
<p>Fr Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<p>Nunc rutrum, eros sit amet ornare faucibus.</p>

Notes:

  1. Use predicates. It is a very powerful instrument.
  2. Keep hardcoded data in one place if possible.
  3. apply-templates it's not a mysterious tool but a very essential XSLT mechanism.
  4. You can call normalize-space and other functions without actually passing an argument, if the argument you need is the context element.