Re: [xsl] generate-id for identical elements

Subject: Re: [xsl] generate-id for identical elements
From: Jeni Tennison <jeni@xxxxxxxxxxxxxxxx>
Date: Thu, 23 Sep 2004 20:19:49 +0100
To summarize the problem, I have identical elements (but they do have different parent elements), and I am trying to get cross-references to these identical elements with generate-id().

To get the same generated ID, you need to make sure that you call generate-id() with the same argument. You generate the anchor from the <chapter> element, in:


  <xsl:template match="chapter">
    <h1><a name="{generate-id()}"/><xsl:value-of select="@title"/></h1>
    <xsl:apply-templates/>
  </xsl:template>

You generate the link to that chapter with the code:


  <xsl:template match="ref">
    <xsl:variable name="reftext">
      <xsl:call-template name="makeRef">
        <xsl:with-param name="inString" select="@target"/>
      </xsl:call-template>
    </xsl:variable>
    <a href="#{generate-id(//$reftext)}"><xsl:value-of select="."/></a>
  </xsl:template>

Here, the argument to the generate-id() function is the new document node that's generated when you use the content of <xsl:variable> to set the variable.


Using XSLT 2.0 (I assume that you're happy to do so, since you're using Saxon 8), what you need to do is write a function that takes a path like those used in your references and returns the element that the path references. You can then use the result of calling the function with that path as the argument to the generate-id() function, and get the same ID as the one that you've used in the anchor.

A function to do this might look like:

<xsl:variable name="book" select="/book" />

<xsl:function name="my:parsePath" as="element()">
  <xsl:param name="path" as="xs:string" />
  <xsl:sequence select="my:parsePath($path, $book)" />
</xsl:function>

<xsl:function name="my:parsePath" as="element()">
  <xsl:param name="path" as="xs:string" />
  <xsl:param name="element" as="element()" />
  <xsl:variable name="step" as="xs:string"
    select="if (contains($path, '/')) then substring-before($path, '/')
                                      else $path" />

  <xsl:variable name="elementName" as="xs:string"
    select="substring-before($step, '|')" />
  <xsl:variable name="title" as="xs:string"
    select="substring-after($step, '|')" />

  <xsl:variable name="newElement" as="element()"
    select="$element/*[name() = $elementName][@title = $title]" />

  <xsl:sequence select="if (contains($path, '/'))
                        then my:parsePath(substring-after($path, '/'),
                                          $newElement)
                        else $newElement" />
</xsl:function>

Note that the two function definitions both have the same name, but have different arguments; essentially, this means you have a function whose second argument is optional and defaults to the <book> document element. Also note that the <book> document element has to be stored in a (global) variable to be used in the function, since function bodies are evaluated without the context item being set, and therefore the processor doesn't know what "/" means (which document it refers to) when it sees it at the beginning of a path.

The code breaks down the path step by step, and locates, from the element passed as the second argument, the child element whose name is the same as the substring before the | within the step, and whose title is the same as the substring after the | within the step. If there are more steps, the function calls itself recursively from the element that it's just identified.

You can use the function as in:

<xsl:template match="ref">
  <xsl:variable name="reftext" select="my:parsePath(@target)" />
  <a href="#{generate-id($reftext)}"><xsl:value-of select="."/></a>
</xsl:template>

If you're don't want to use XSLT 2.0, then you need to use a recursive *template* instead, and since templates can't return existing nodes in XSLT 1.0, it will need to do the generate-id() thing itself. Something like:

<xsl:template name="my:parsePath">
  <xsl:param name="path" />
  <xsl:param name="element" select="/book" />
  <xsl:variable name="step">
    <xsl:choose>
      <xsl:when test="contains($path, '/')">
        <xsl:value-of select="substring-before($path, '/')" />
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$path" />
      </xsl:otherwise>
    </xsl:choose>
  </xsl:variable>

  <xsl:variable name="elementName"
    select="substring-before($step, '|')" />
  <xsl:variable name="title"
    select="substring-after($step, '|')" />

  <xsl:variable name="newElement"
    select="$element/*[name() = $elementName][@title = $title]" />

  <xsl:choose>
    <xsl:when test="contains($path, '/')">
      <xsl:call-template name="my:parsePath">
        <xsl:with-param name="path"
          select="substring-after($path, '/')" />
        <xsl:with-param name="element" select="$newElement" />
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="generate-id($newElement)" />
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

and call it like:

<xsl:template match="ref">
  <xsl:variable name="reftext">
    <xsl:call-template name="my:parsePath">
      <xsl:with-param name="path" select="@target" />
    </xsl:call-template>
  </xsl:variable>
  <a href="#{$reftext}"><xsl:value-of select="."/></a>
</xsl:template>

Cheers,

Jeni
--
Jeni Tennison
http://www.jenitennison.com

Current Thread