views:

170

answers:

3

hi!

i have to build an XSLT stylesheet that transforms an xml like this:

<message>
 <line/> 
 <silence/> 
 <dot/><line/><line/> 
 <silence/> 
 <dot/> 
 <silence/> 
 <line/><dot/><dot/><dot/> 
</message>

into something like this:

<complexMessage> 
 <word code="-"/> 
 <word code=".--"/> 
 <word code="."/>
 <word code="-..."/> 
</complexMessage>

(notice how each word element gets closed after a silence element)

how would you do that?

+1  A: 

How about:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/message">
      <complexMessage>
        <xsl:apply-templates select="*[1]"/>
      </complexMessage>
    </xsl:template>
  <xsl:template match="line" mode="value">-</xsl:template>
  <xsl:template match="dot" mode="value">.</xsl:template>
  <xsl:template match="silence" mode="value"/>

  <xsl:template match="silence" name="write">
    <xsl:param name="prev" select="''"/>
    <word code="{$prev}"/>
    <xsl:apply-templates select="following-sibling::*[1]"/>
  </xsl:template>
  <xsl:template match="line | dot">
    <xsl:param name="prev" select="''"/>
    <xsl:variable name="value"><xsl:apply-templates select="." mode="value"/></xsl:variable>
    <xsl:choose>
      <xsl:when test="following-sibling::*">
        <xsl:apply-templates select="following-sibling::*[1]">
          <xsl:with-param name="prev" select="concat($prev,$value)"/>
        </xsl:apply-templates>
      </xsl:when>
      <xsl:otherwise>
        <xsl:call-template name="write">
          <xsl:with-param name="prev" select="concat($prev,$value)"/>
        </xsl:call-template>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

For large documents it might trigger infinite recursion detection - in which case I'd try to look at something involving <xsl:apply-templates select="*[1] | silence"/>, and in each case only go forward as far as the next silence or EOF (if you see what I mean). Let me know if you want a refactored version to show this...

Marc Gravell
+2  A: 

I believe that this achieves what you are looking for:

    <?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:variable name="firstSilenceID" select="generate-id(//silence[1])" />
    <xsl:template match="/">
        <complexMessage>
            <xsl:apply-templates select="/message//silence"/>
        </complexMessage>
    </xsl:template>

    <xsl:template name="message">

    </xsl:template>

    <xsl:template match="silence" >
        <!--If this is the first <silince> element, generate words for everything before it -->
        <xsl:if test="$firstSilenceID = generate-id(current())">
            <xsl:call-template name="complexMessage">
                <xsl:with-param name="word" select="preceding-sibling::*"/>
            </xsl:call-template>
        </xsl:if>
        <!--Generate words for everything after THIS <silence> element -->
        <xsl:call-template name="complexMessage">
            <xsl:with-param name="word" select="following-sibling::*[generate-id(preceding-sibling::silence[1]) = generate-id(current())]"/>
        </xsl:call-template>
    </xsl:template>

    <xsl:template name="complexMessage">
        <xsl:param name="word"/>
            <word>
                <xsl:attribute name="code">
                    <xsl:apply-templates select="$word" />
                </xsl:attribute>
            </word>
    </xsl:template>

    <xsl:template match="dot">
        <xsl:text>.</xsl:text>
    </xsl:template>

    <xsl:template match="line">
        <xsl:text>-</xsl:text>
    </xsl:template>

</xsl:stylesheet>
Mads Hansen
Some trivial glitches in the output element names, but a very slick approach; nice.
Marc Gravell
Whoops! Thanks, Marc. I have updated the stylesheet to produce the correct element/attribute names. Also, moved the evaluation of the generate-id for the first source element into a variable and out of the silence template for better efficiency.
Mads Hansen
+3  A: 

This solution is both: slightly shorter and, more importantly, more efficient due to the use of keys:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <xsl:key name="kPhraseByMorse" 
           match="*[not(self::silence) and not(self::message)]" 
           use="generate-id(preceding-sibling::silence[1])"/>

    <xsl:template match="/">
      <complexMessage>
        <word>
          <xsl:call-template name="makeCode"/>
        </word>

        <xsl:apply-templates select="*/silence"/>
      </complexMessage>
    </xsl:template>

    <xsl:template match="silence">
      <word>
     <xsl:call-template name="makeCode">
       <xsl:with-param name="pId" select="generate-id()"/>
     </xsl:call-template>
      </word>
    </xsl:template>

    <xsl:template name="makeCode">
      <xsl:param name="pId"/>
      <xsl:attribute name="code">
        <xsl:apply-templates select="key('kPhraseByMorse', $pId)"/>
      </xsl:attribute>
    </xsl:template>

    <xsl:template match="dot">.</xsl:template>
    <xsl:template match="line">-</xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided source XML, the correct result is produced:

<complexMessage>
   <word code="-"/>
   <word code=".--"/>
   <word code="."/>
   <word code="-..."/>
</complexMessage>
Dimitre Novatchev
Traditionally, it's "dot" and "dash" (not "line") in Morse code, unless I'm mistaken. But that's minor. :-) +1 from me, superior solution.
Tomalak
Oh, but I see the OP is using "line" himself. So never mind. ;)
Tomalak