mpounsett
mpounsett

Reputation: 1194

Recursively counting nested tags

I have a document format with <section> tags that can be infinitely nested. Each section contains things like paragraphs and tables, etc. but it's possible for the first element of a new section to be anything, including another section.

<doc>
  <section name="one">
    <p> Some text about <i>this</i> section.</p>
    <section name="one.one">
      <section name="one.one.one">
        <p>Other text</p>
      </section>
    </section>
  </section>
</doc>

I'm converting the document format to markdown. It seems the natural way to apply most tags would be with a structure like this:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="doc|p">
    <xsl:apply-templates select="* | text()"/>
  </xsl:template>
  <xsl:template match="i">
    <xsl:text>_</xsl:text>
    <xsl:apply-templates select="* | text()"/>
    <xsl:text>_</xsl:text>
  </xsl:template>
</xsl:stylesheet>

However, with <section> tags I need to keep track of the nesting depth to construct the section name. It seems like the natural way to do that is with <xsl:call-template> and <xsl:param>.

<xsl:template match="section">
  <xsl:param name="depth" select="1"/>
  <!-- do stuff to write the section name -->
  <xsl:apply-templates select="* | text()"/>
</xsl>

The problem I have is that this needs to be triggered with <xsl:call-template> rather than <xsl:apply-templates> in order to increment the param.

<xsl:call-template name="section">
  <xsl:with-param name="depth" select="$depth+1"/>
</xsl:call-template>

I can't give a parameter to apply-templates, but neither can I just start the section (or doc) template with a recursive call-template either, because a new section may not be the first tag.

Is there some way to approach this that lets me use call-template, but maintains the tag order that xslt would normally follow with just the simple apply-templates tag?

Upvotes: 0

Views: 257

Answers (2)

Daniel Haley
Daniel Haley

Reputation: 52858

Here's an example of using xsl:number (with level="multiple") to determine the depth. This will give you something like 1.1.1. (third level) but then you can translate the . to # (and the numbers to nothing).

XML Input

<doc>
    <section name="one">
        <p> Some text about <i>this</i> section.</p>
        <section name="one.one">
            <section name="one.one.one">
                <p>Other text</p>
            </section>
        </section>
    </section>
</doc>

XSLT 1.0

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:strip-space elements="*"/>

  <xsl:variable name="chars" select="'.0123456789'"/>

  <xsl:template match="i">
    <xsl:value-of select="concat('_',.,'_')"/>
  </xsl:template>

  <xsl:template match="p">
    <xsl:apply-templates/>
    <xsl:text>&#xA;</xsl:text>
  </xsl:template>

  <xsl:template match="section">
    <xsl:variable name="depth">
      <xsl:number level="multiple" format="1."/>
    </xsl:variable>
    <xsl:value-of select="translate($depth,$chars,'#')"/>
    <xsl:value-of select="concat(@name,'&#xA;')"/>
    <xsl:apply-templates/>
  </xsl:template>

</xsl:stylesheet>

Output

#one
 Some text about _this_ section.
##one.one
###one.one.one
Other text

EDIT

Here's another way counting ancestor-or-self like Michael Kay suggested (duh!)...

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="i">
    <xsl:value-of select="concat('_',.,'_')"/>
  </xsl:template>

  <xsl:template match="p">
    <xsl:apply-templates/>
    <xsl:text>&#xA;</xsl:text>
  </xsl:template>

  <xsl:template match="section">
    <xsl:for-each select="ancestor-or-self::section">#</xsl:for-each>
    <xsl:value-of select="concat(@name,'&#xA;')"/>
    <xsl:apply-templates/>
  </xsl:template>

</xsl:stylesheet>

Upvotes: 2

Michael Kay
Michael Kay

Reputation: 163342

Firstly, you can pass params with apply-templates just as easily as with call-template: apply-templates allows an xsl:with-param child element.

Secondly, you don't need parameters to determine the depth of nesting because you can use XPath expressions that navigate upwards, e.g. count(ancestor::section).

Upvotes: 1

Related Questions