dougnorton
dougnorton

Reputation: 194

Multiple Attributes to Nested Elements

I'd like to transform the example snippet below into multiple elements.

<para font="Times" size="12" face="Bold Italic">This is some text.</para>
<para font="Times" size="12" face="Bold">This is some more text.</para>

I'm looking to convert it to something like this:

<para>
   <font name="Times" size="12">
       <b>
           <i>This is some text.</i>
       </b>
    </font>
</para>
<para>
   <font name="Times" size="12">
       <b>This is some text.</b>
    </font>
</para>

Is there a way to do this without using the mother of all <xsl:choose> block..? The above is a simple example of something that has many more attributes and attribute values.

I'm using XSLT 1.0 (xsltproc)

Upvotes: 1

Views: 391

Answers (3)

helderdarocha
helderdarocha

Reputation: 23637

I wrote a stylesheet containing a table where you can add styles and associate them with tags. You could, for example, replace <b> for <strong> or add new ones like <tag name="strike" style="Strike-through"/>. The table is accessible through a $tags variable and used to select the tag corresponding to the name of the face attribute style.

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

    <xsl:output indent="yes"/>
    <xsl:strip-space elements="*"/>

    <my:styles>
        <tag name="b" style="Bold"/>
        <tag name="i" style="Italic"/>
        <tag name="u" style="Underline"/>
    </my:styles>

    <xsl:variable name="tags" select="document('')//tag" />

    <xsl:template match="/">
        <xsl:apply-templates select="document/para" mode="initial" />
    </xsl:template>

    <xsl:template match="para" mode="initial">
        <xsl:copy>
            <font name="{@font}" size="{@size}">
                <xsl:choose>
                    <xsl:when test="@face">
                        <xsl:call-template name="add-style">
                            <xsl:with-param name="styles" select="concat(@face,' ')"/>
                        </xsl:call-template>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:value-of select="."/>
                    </xsl:otherwise>
                </xsl:choose>
            </font>
        </xsl:copy>
    </xsl:template>

    <xsl:template name="add-style">
        <xsl:param name="styles"/>
        <xsl:param name="current-style" select="substring-before($styles, ' ')"/>
        <xsl:message>value:<xsl:value-of select="$styles"/>:</xsl:message>
        <xsl:if test="$current-style">
            <xsl:element name="{$tags[@style = $current-style]/@name}">
                <xsl:call-template name="add-style">
                    <xsl:with-param name="styles" select="substring-after($styles, ' ')"/>
                </xsl:call-template>
                <xsl:if test="not(contains(substring-after($styles, ' '), ' '))">
                    <xsl:value-of select="." />
                </xsl:if>
            </xsl:element>
        </xsl:if>
    </xsl:template>

</xsl:stylesheet>

If you use this as input:

<document>
    <para font="Times" size="12" face="Bold Italic">This is some text.</para>
    <para font="Times" size="12" face="Bold">This is some more text.</para>
    <para font="Times" size="12" face="Italic Bold Underline">This is some text 3.</para>
    <para font="Times" size="12" face="Italic">This is some more text 4.</para>
    <para font="Times" size="12">This is some more text 4.</para>
</document>

You get this result:

<para>
   <font name="Times" size="12">
      <b>
         <i>This is some text.</i>
      </b>
   </font>
</para>
<para>
   <font name="Times" size="12">
      <b>This is some more text.</b>
   </font>
</para>
<para>
   <font name="Times" size="12">
      <i>
         <b>
            <u>This is some text 3.</u>
         </b>
      </i>
   </font>
</para>
<para>
   <font name="Times" size="12">
      <i>This is some more text 4.</i>
   </font>
</para>
<para>
   <font name="Times" size="12">This is some more text 4.</font>
</para>

I didn't use a big choose, but I had to use one to account for the situation where the face attribute might not be present.

It will fail if there are extra spaces in the face attribute, since I use the spaces (and even add one) to control the recursion. But this could be fixed with normalize-space().

Upvotes: 0

Mathias M&#252;ller
Mathias M&#252;ller

Reputation: 22617

Assuming that the values of @face actually correspond to element names in your output (several people have already commented on this), you could do something like this.

The recursive named template actually tokenizes the content of the @face attribute to determine the nesting sequence.

Stylesheet

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:exsl="http://exslt.org/common"
    extension-element-prefixes="exsl">

    <xsl:strip-space elements="*"/>
    <xsl:output omit-xml-declaration="yes" indent="yes" />

    <xsl:template match="/root">
        <xsl:copy>
            <xsl:apply-templates/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="para">
        <xsl:copy>
            <font name="{@font}" size="{@size}">
                        <xsl:call-template name="tokenize">
                            <xsl:with-param name="string" select="@face"/>
                            <xsl:with-param name="delim" select="' '"/>
                        </xsl:call-template>
            </font>
        </xsl:copy>
    </xsl:template>

    <xsl:template name="tokenize">
      <xsl:param name="string" />
      <xsl:param name="delim" />

      <xsl:choose>
        <xsl:when test="contains($string, $delim)">
          <xsl:element name="{substring-before($string,$delim)}">
            <xsl:call-template name="tokenize">
            <xsl:with-param name="string" select="substring-after($string, $delim)" />
            <xsl:with-param name="delim" select="$delim" />
          </xsl:call-template>
          </xsl:element>
        </xsl:when>
        <xsl:otherwise>
          <xsl:element name="{$string}">
            <xsl:apply-templates/>
          </xsl:element>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:template>

</xsl:stylesheet>

It processes an arbitrary number of tokens inside @face. For example, when applied to the following input:

<?xml version="1.0"?>
<root>
    <para font="Times" size="12" face="i b u s">This is some text.</para>
    <para font="Times" size="12" face="i b">This is some more text.</para>
</root>

the result is:

<root>
   <para>
      <font name="Times" size="12">
         <i>
            <b>
               <u>
                  <s>This is some text.</s>
               </u>
            </b>
         </i>
      </font>
   </para>
   <para>
      <font name="Times" size="12">
         <i>
            <b>This is some more text.</b>
         </i>
      </font>
   </para>
</root>

Upvotes: 1

Tim C
Tim C

Reputation: 70598

I think you may still end up with an xsl:choose here, to map words like "Bold" and "Italic" to element names of "b" and "I"

So, you would start off with a template that matches the @face attribute, but one that also has a name so it can be called recursively with what ever 'face' values remain to be processed

<xsl:template match="@face" name="face">
    <xsl:param name="face" select="." />

You would extract the first face name to be processed with some simple string handling

<xsl:variable name="facename" select="substring-before(concat($face, ' '), ' ')" />

(The concat here is to allow for a single word in the parameter)

Then you could map to an element name like so

    <xsl:variable name="element">
        <xsl:choose>
            <xsl:when test="$facename = 'Bold'">b</xsl:when>
            <xsl:when test="$facename = 'Italic'">i</xsl:when>
        </xsl:choose>
    </xsl:variable>

Having said that, you could in theory maintain the mappings in a separate XML file, and use the document function to looking them up.

Finally, you would have an xsl:choose to output the element, if one was found, or process the children of the parent element

    <xsl:choose>
        <xsl:when test="$element != ''">
            <xsl:element name="{$element}">
                <xsl:call-template name="face">
                    <xsl:with-param name="face" select="normalize-space(substring-after($face, $facename))" />
                </xsl:call-template>
            </xsl:element>
        </xsl:when>
        <xsl:otherwise>
            <xsl:apply-templates select="../node()" />
        </xsl:otherwise>
    </xsl:choose>

Try this XSLT

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
    <xsl:output indent="yes"/>

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

    <xsl:template match="*[@font]">
        <xsl:copy>
            <font>
                <xsl:apply-templates select="@*[name() != 'face']"/>
                <xsl:apply-templates select="@face" />
            </font>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="@face" name="face">
        <xsl:param name="face" select="." />

        <xsl:variable name="facename" select="substring-before(concat($face, ' '), ' ')" />

        <xsl:variable name="element">
            <xsl:choose>
                <xsl:when test="$facename = 'Bold'">b</xsl:when>
                <xsl:when test="$facename = 'Italic'">i</xsl:when>
            </xsl:choose>
        </xsl:variable>

        <xsl:choose>
            <xsl:when test="$element != ''">
                <xsl:element name="{$element}">
                    <xsl:call-template name="face">
                        <xsl:with-param name="face" select="normalize-space(substring-after($face, $facename))" />
                    </xsl:call-template>
                </xsl:element>
            </xsl:when>
            <xsl:otherwise>
                <xsl:apply-templates select="../node()" />
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

Upvotes: 1

Related Questions