xsl-list
[Top] [All Lists]

Re: generate-id for identical elements

2005-02-03 08:37:07
I used Jeni's functions for all these months, but then I found the time to 
try my hand at making my own solution again. This problem was the only XSL 
(or coding in any language) problem I had not solved with my own code, and 
that bugged me. So, just to close the loop (and because someone might need 
an XSL 1 solution someday), here's my solution to my own problem (which I 
bet many others have run into):

  <xsl:template name="getRef">
    <xsl:param name="target"/>
    <xsl:param name="soFar"/>
    <!-- soFar has to be seeded with the document's root element -->
    <xsl:choose>
      <xsl:when test="contains($target, '/')">
        <xsl:variable name="partBefore" select="substring-before($target, 
'/')"/>
        <xsl:variable name="elementName" 
select="substring-before($partBefore, '|')"/>
        <xsl:variable name="title" select="substring-after($partBefore, 
'|')"/>
        <xsl:call-template name="getRefID">
          <xsl:with-param name="target" select="substring-after($target, 
'/')"/>
          <xsl:with-param name="soFar" 
select="$soFar/*[name()=$elementName][(_at_)title=$title]"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:variable name="elementName" select="substring-before($target, 
'|')"/>
        <xsl:variable name="title" select="substring-after($target, 
'|')"/>
        <xsl:for-each 
select="$soFar/*[name()=$elementName][(_at_)title=$title]">
          <xsl:value-of select="generate-id(./@title)"/>
        </xsl:for-each>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

Also, replacing

  <xsl:value-of select="generate-id(./@title)"/>

with

  <xsl:number level="multiple" 
count="chapter|topic|heading|subheading|detail" from="book"/>

produces the right number if you are using xsl:number to make IDs (as 
suggested by someone).

BIG thanks to Jeni for the solution that I used all these months and for 
the education that I got from studying it.

Jay Bryant
Bryant Communication Services
(on contract at Synergistic Solution Technologies)




Jeni Tennison <jeni(_at_)jenitennison(_dot_)com> 
09/23/2004 02:19 PM
Please respond to
xsl-list(_at_)lists(_dot_)mulberrytech(_dot_)com


To
xsl-list(_at_)lists(_dot_)mulberrytech(_dot_)com
cc
JBryant(_at_)s-s-t(_dot_)com
Subject
Re: [xsl] generate-id for identical elements






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][(_at_)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][(_at_)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


<Prev in Thread] Current Thread [Next in Thread>
  • Re: generate-id for identical elements, JBryant <=