1

Is there a clever way to simplify the following stylesheet in order to avoid repeating a whole when block when only one variable is changing between each of those?

Ideally I would like something like this, looping 6 times on $i:

<xsl:when test="$depth &gt; $i">
    [...]
    <xsl:value-of select="substring($npath,($nlength - $i*2),1) - 1"/>
    [...]
</xsl:when>

I'm using XSLT 1.0.

XML Input

<?xml version='1.0'?>
<?xml-stylesheet type="text/xsl" href="stylesheet.xsl" version="1.0"?>
<root>
  <item>Main_A
    <item>Item_A</item>
    <item>Item_B
      <item>Subitem_A</item>
      <item>Subitem_B</item>
    </item>
    <item>Item_C</item>
  </item>
  <item>Main_B
    <item>Item_A
      <item>Subitem_A</item>
      </item>
    <item>Item_B</item>
  </item>
</root>

XSLT 1.0 Stylesheet

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

<xsl:template match="root">
  <html>
    <body>
      <xsl:apply-templates select="item">
        <xsl:with-param name="depth" select="1"/>
      </xsl:apply-templates>
    </body>
  </html>
</xsl:template>

<xsl:template match="item">
  <xsl:param name="depth" select="1"/>
  <xsl:if test="$depth &lt; 10">
    <ul>
      <li>
        <xsl:text>path</xsl:text>
        <xsl:call-template name="loopnumformat">
          <xsl:with-param name="depth" select="$depth"/>
        </xsl:call-template>
        <xsl:text> = </xsl:text>
        <xsl:apply-templates match="item">
          <xsl:with-param name="depth" select="$depth + 1"/>
        </xsl:apply-templates>
      </li>
    </ul>
  </xsl:if>
</xsl:template>

<xsl:template name="loopnumformat">
  <xsl:param name="depth" select="1"/>
  <xsl:variable name="npath">
    <xsl:number level="multiple" from="*[10]"/>
  </xsl:variable>
  <xsl:variable name="nlength">
    <xsl:value-of select="string-length($npath)"/>
  </xsl:variable>

  <xsl:choose>
    <xsl:when test="$depth &gt; 2">
      <xsl:text>:</xsl:text>
      <xsl:value-of select="substring($npath,($nlength - 2*2),1) - 1"/>
      <xsl:call-template name="loopnumformat">
        <xsl:with-param name="depth" select="$depth - 1"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:when test="$depth &gt; 1">
      <xsl:text>:</xsl:text>
      <xsl:value-of select="substring($npath,($nlength - 1*2),1) - 1"/>
      <xsl:call-template name="loopnumformat">
        <xsl:with-param name="depth" select="$depth - 1"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:when test="$depth &gt; 0">
      <xsl:text>:</xsl:text>
      <xsl:value-of select="substring($npath,($nlength - 0*2),1) - 1"/>
      <xsl:call-template name="loopnumformat">
        <xsl:with-param name="depth" select="$depth - 1"/>
      </xsl:call-template>
    </xsl:when>
  </xsl:choose>

</xsl:template>

</xsl:stylesheet>

HTML Output

<html>
<body>
  <ul>
    <li>path:0 = Main_A
      <ul>
        <li>path:0:0 = Item_A</li>
      </ul>
      <ul>
        <li>path:0:1 = Item_B
          <ul>
            <li>path:0:1:0 = Subitem_A</li>
          </ul>
          <ul>
            <li>path:0:1:1 = Subitem_B</li>
          </ul>
        </li>
      </ul>
      <ul>
        <li>path:0:2 = Item_C</li>
      </ul>
    </li>
  </ul>
  <ul>
    <li>path:1 = Main_B
      <ul>
        <li>path:1:0 = Item_A
          <ul>
            <li>path:1:0:0 = Subitem_A</li>
          </ul>
        </li>
      </ul>
      <ul>
        <li>path:1:1 = Item_B</li>
      </ul>
    </li>
  </ul>
</body>
</html>
rmercier
  • 155
  • 1
  • 10

2 Answers2

3

Is there a clever way to simplify the following stylesheet in order to avoid repeating a whole when block when only one variable is changing between each of those?

It depends on the circumstances, but in your particular case, one way you could do it would be to move the xsl:choose inside, so that the parts that are the same in every case are expressed only once each:

  <xsl:text>:</xsl:text>
  <xsl:choose>
    <xsl:when test="$depth &gt; 2">
      <xsl:value-of select="substring($npath,($nlength - 2*2),1) - 1"/>
    </xsl:when>
    <xsl:when test="$depth &gt; 1">
      <xsl:value-of select="substring($npath,($nlength - 1*2),1) - 1"/>
    </xsl:when>
    <xsl:when test="$depth &gt; 0">
      <xsl:value-of select="substring($npath,($nlength - 0*2),1) - 1"/>
    </xsl:when>
  </xsl:choose>
  <xsl:call-template name="loopnumformat">
    <xsl:with-param name="depth" select="$depth - 1"/>
  </xsl:call-template>

But you seem to be going to extreme lengths to use xsl:number when it doesn't quite meet your needs. It appears that you can do the whole thing much more simply:

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

  <xsl:template match="root">
    <html>
      <body>
        <xsl:apply-templates select="item" />
      </body>
    </html>
  </xsl:template>

  <xsl:template match="item">
    <xsl:param name="path-prefix" select="'path'"/>
    <!-- if there are fewer than nine ':' characters in the path prefix for
         this item ... -->
    <xsl:if test="string-length($path-prefix) - string-length(translate($path-prefix, ':', '')) &lt; 9">
      <xsl:variable name="my-path"
          select="concat($path-prefix, ':', position() - 1)" />
      <ul>
        <li>
          <xsl:value-of select="$my-path"/>
          <xsl:text> = </xsl:text>
          <xsl:apply-templates select="text()[1]"/>
          <xsl:apply-templates select="item">
            <xsl:with-param name="path-prefix" select="$my-path"/>
          </xsl:apply-templates>
        </li>
      </ul>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

Perhaps you could even get rid of the xsl:if, which seems as if it may have been present to serve the limitations of the loopnumformat template in your original stylesheet -- this version has no inherent depth limitation. Note that it does assume that the text nodes you want to copy into the output document will be limited to the first in each <item>, but it would be easy enough to modify the stylesheet to instead copy all of them.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • Thanks a lot for the detailed explanation! This is a very elegant solution I would have never thought of. Seems like I still have a lot to learn regarding XSLT and functional programming. (and yes, you assumed correctly, the `xsl:if` was only there to avoid causing an infinite loop) – rmercier May 05 '17 at 15:22
2

It looks like you are over-engineering here. You are simply looking for a way to count preceding <item> nodes, recursively up the document tree.

This simple transformation:

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

  <xsl:template match="root">
    <html>
      <body>
        <ul>
          <xsl:apply-templates select="item" />
        </ul>
      </body>
    </html>
  </xsl:template>

  <xsl:template match="item">
      <li>
        <xsl:text>path</xsl:text>
        <xsl:apply-templates select="." mode="path" />
        <xsl:text> = </xsl:text>
        <xsl:value-of select="normalize-space(text()[1])" />
        <xsl:if test="item">
          <ul>
            <xsl:apply-templates select="item" />
          </ul>
        </xsl:if>
      </li>
  </xsl:template>

  <xsl:template match="item" mode="path">
      <xsl:apply-templates select="parent::item" mode="path" />
      <xsl:text>:</xsl:text>
      <xsl:value-of select="count(preceding-sibling::item)" />
  </xsl:template>

</xsl:stylesheet>

results in:

<html>
   <body>
      <ul>
         <li>path:0 = Main_A
            <ul>
               <li>path:0:0 = Item_A</li>
               <li>path:0:1 = Item_B
                  <ul>
                     <li>path:0:1:0 = Subitem_A</li>
                     <li>path:0:1:1 = Subitem_B</li>
                  </ul>
               </li>
               <li>path:0:2 = Item_C</li>
            </ul>
         </li>
         <li>path:1 = Main_B
            <ul>
               <li>path:1:0 = Item_A
                  <ul>
                     <li>path:1:0:0 = Subitem_A</li>
                  </ul>
               </li>
               <li>path:1:1 = Item_B</li>
            </ul>
         </li>
      </ul>
   </body>
</html>
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Thanks! Including for bringing `mode` and `preceding-sibling` to my attention. I didn't know about those beforehand. – rmercier May 05 '17 at 16:02
  • You are welcome. Here's a nice visual explanation of the other XPath axes. https://our.umbraco.org/documentation/reference/templating/macros/xslt/xpath-axes-and-their-shortcuts – Tomalak May 05 '17 at 16:11
  • @rmercier Updated answer: I had the recursion reversed, resulting in a wrong-order path. Fixed now. – Tomalak May 05 '17 at 16:15
  • This diagram is perfect for me, thanks a lot. I have been playing with all the solutions provided here in order to get a better grasp of XSLT and one thing I can't seem to do with yours even after trying all the XPath axes is skip a first additional root node. In my actual code I have one more item element at the root that I don't want to include in the tree list, so one ":0" too many at the start of the path (same XML input as in this question but with replaced with ). Not sure about the stackoverflow etiquette here, should I create a new question? @Tomalak – rmercier May 07 '17 at 10:24
  • The closest I got was using `` in the `` template but it removes the last number in the path not the first one. – rmercier May 07 '17 at 10:26
  • The recursive step `` goes to the parent `` until (and including) the top-most one. If you don't want to include the top-most item, select only those items that *themselves* have a parent item. Very simple: ``. – Tomalak May 07 '17 at 11:15
  • Maybe I'm misunderstanding something but after several tests it seems to me that `parent::item` already excludes the top-most item and that `parent::item[parent::item]` excludes the top-most item *and* its children. The problem in both case remains: on top of excluding these items (i.e. not outputing the path for these items, which is what I want), both expressions also remove the **last** number in the path of all their descendants, which is not what I want. I would like instead to remove the **first** number in the path for all these descendants. Sorry if this is not clear. – rmercier May 07 '17 at 13:36
  • Interactive debugging via Stack Overflow comments about code that does not really exist in the question and is only described verbally instead of shown is, frankly, a pain in the ass. It creates walls of text that nobody else reads and that therefore benefit nobody. My solution provides (I believe) a solution to the question as it was asked. Next time, please make an example that reflects your actual input (or include your actual input). – Tomalak May 07 '17 at 14:43
  • Sure. Sorry about that. – rmercier May 07 '17 at 18:38