views:

570

answers:

2

hi all,

i would like to convert xhtml to dokuwiki syntax using xslt.

now, one thing i can not seem to work my head around is how to handle nested lists. the dokuwiki syntax uses an asterisk (*) for a list item which is prepended by two white spaces per nesting level (c.f. wiki syntax).

my question: in the following example, how can the <xsl:template mach="li"> that matches the list item 2.1.1 be aware of it's nesting level, in order to prepend the right amount of white spaces?

* list item 1
* list item 2
  * list item 2.1
    * list item 2.1.1
  * list item 2.2
  * list item 2.3
* list item 3

corresponds to

  • list item 1
  • list item 2
    • list item 2.1
      • list item 2.1.1
    • list item 2.2
    • list item 2.3
  • list item 3

which is how the following html is displayed:

<ul>
    <li>
        list item 1
    </li>
    <li>
        list item 2
        <ul>
            <li>
                list item 2.1
                <ul>
                    <li>list item 2.1.1</li>
                </ul>
            </li>
            <li>list item 2.2</li>
            <li>list item 2.3</li>
        </ul>
    </li>
    <li>
        list item 3
    </li>
</ul>
+2  A: 

Here is how I got it to work:

<?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="text"/>
    <xsl:strip-space elements="*"/>
    <xsl:template match="//li">
     <xsl:call-template name="loop">
      <xsl:with-param name="maxcount" select="count(ancestor::li)"/>
      <xsl:with-param name="initial-value" select="0"/>
     </xsl:call-template>
     <xsl:text>* </xsl:text>
     <xsl:value-of select="normalize-space(text())"/>
     <xsl:text>&#xd;</xsl:text>
     <xsl:apply-templates select="ul/li" />
    </xsl:template>
    <xsl:template name="loop">
     <xsl:param name="maxcount"/>
     <xsl:param name="initial-value"/>
     <xsl:if test="$initial-value &lt; $maxcount">
      <xsl:text>&#x9;</xsl:text>
      <xsl:call-template name="loop">
       <xsl:with-param name="maxcount" select="$maxcount"/>
       <xsl:with-param name="initial-value" select="$initial-value+1"/>
      </xsl:call-template>
     </xsl:if>
    </xsl:template>
</xsl:stylesheet>

Here is how it breaks down:

<xsl:output method="text"/>
<xsl:strip-space elements="*"/>

You need to make sure that the output of the XSLT is text and you also want to strip any existing whitespace.

<xsl:template match="//li">
    ...
</xsl:template>

This is your main template and will match every single <li> in the document. The first step in this template is to output the appropriate number of tab characters (feel free to adjust this to be spaces or whatever you need). The way this is done is by calling a custom loop template that will recursively call itself, looping from initial-value to maxcount, outputting a tab character (&#x9;) on each iteration.

<xsl:text>* </xsl:text>
<xsl:value-of select="normalize-space(text())"/>
<xsl:text>&#xd;</xsl:text>

This chunk simply outputs the text with the * in front and a newline (&#xd;) after. Note that I used the text() function instead of . to retrieve the value of the node. If you don't the output of the parent node will (as it should according to the W3C recommendation) concatenate all child text nodes with the parent.

<xsl:apply-templates select="ul/li" />

Finally we recursively call the current template but explicitly reference the next <li> that is a direct child of a <ul> - this keeps us from accidentally calling the template twice on the same parent element.

Andrew Hare
+5  A: 

The following transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output method="text"/>

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

 <xsl:variable name="vBlanks"
  select="'                                        '"/>
 <xsl:variable name="vnNestSpaces" select="2"/>


    <xsl:template match="li">
      <xsl:variable name="vNestLevel"
           select="count(ancestor::li)"/>
      <xsl:value-of select=
       "concat('&#xA;',
               substring($vBlanks,1,$vnNestSpaces*$vNestLevel),
               '*  ', normalize-space(text()[1])
               )"/>
      <xsl:apply-templates select="*"/>
    </xsl:template>
</xsl:stylesheet>

when applied on the original XML document:

<ul>
    <li> list item 1
    </li>
    <li> list item 2        
     <ul>
      <li> list item 2.1                
       <ul>
        <li>list item 2.1.1</li>
       </ul>
      </li>
      <li>list item 2.2</li>
      <li>list item 2.3</li>
     </ul>
    </li>
    <li> list item 3    </li>
</ul>

produces the desired result:

*  list item 1
*  list item 2
  *  list item 2.1
    *  list item 2.1.1
  *  list item 2.2
  *  list item 2.3
*  list item 3

Do note the following:

  1. The required indentation is determined by the value of count(ancesstor::li).

  2. The space for indenting is taken directly from a sufficiently large blank line (contains enough blanks for 20 levels of nesting). There is no need to recursively output the spaces one by one.

  3. The transformation is more efficient, due to 2. above.

  4. Note the use of the XPath substring() function.

Dimitre Novatchev
+1 Very nice!! I like the blank line idea - I knew there had to be a better way to get the indenting.
Andrew Hare
I have nothing to add. +1 :)
Tomalak
great answer ... thank you!
Pierre Spring