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