xslt tree navigation, siblings

Navigation

1. How to express go up and get my nearest older sibling
2. How to go above the context node in the XML tree
3. Graph traversal

1.

How to express go up and get my nearest older sibling

Mike Kay


In this example:

 <list type="gloss"><label><gi>front</gi></label>
 <item>contains any
 prefatory matter (headers, title page, prefaces, dedications, etc.)
 found before the start of a text proper.</item>

when processing <item>, I want to get the associated <label>.



You could try:

<xsl:variable name="here"><xsl:number count="*"/></xsl:variable>
<xsl:apply-templates select="../*[$here - 1]"/>

(Nearly forgot the space around the minus sign - memories of COBOL!)

            

Or, From Ken Holman

(Updated DaveP) The preceding axis (ordered in proximity order (1 is closest), not document order (1 is first in parse order)) will find the older sibling:

   preceding-sibling::*[1]

or explicitly if you know you only have one label:

   preceding-sibling::label

... but this isn't yet implemented in XT.[7 99]

Until the preceding access is implemented, I would use the element's index as in my example below.

xml file:

<list type="gloss">
  <label>
    <gi>front</gi>
  </label>
  <item>contains any prefatory matter 
   </item>
 </list>


stylesheet:
<xsl:template match="list"><!--document element-->
    <xsl:apply-templates/> <!--set node list to children-->
</xsl:template>                          


<xsl:template match="item"> <!--node list = siblings-->
                       <!--sibling #-->
  <xsl:variable name="this-pos" expr="position()"/> 
                       <!--prev sibling-->
Label = <xsl:value-of select="../*[$this-pos - 1]"/> 
</xsl:template>

output:
    Label = front

2.

How to go above the context node in the XML tree

Ken Holman.

Updated, DaveP

You have your choice of traversing the source tree from where you are, or from the root of the tree, or from an arbitrary node identified by the node's unique identifier (assigned to element nodes by an attribute of type ID), or from an arbitrary node based on key values you identify in the content.

Is there a way to directly assign the value of the context node?

I think that question is mis-worded, as one doesn't "assign" the value of the context node. The stylesheet asks the XSLT engine to process a given context node list and the engine then processes each node of the context node list in a particular order (typically document order, though some requests are reverse document order).

Where you say:

   <xsl:apply-templates/>

You are actually saying:

   <xsl:apply-templates select="child::/*"/>

Which can also be abbreviated by saying:

   <xsl:apply-templates select="*"/>

Each of which is giving the XSL engine a context node list of all children and then asking the engine to apply the template for each member of the context node list. This is indicated by the "child" axis of the select pattern.

According to 2.2 there are 13 axes you can choose from when building a pattern:

 [6] AxisName ::=  'ancestors'
                       | 'ancestors-or-self'
                       | 'attributes'
                       | 'child'
                       | 'descendants'
                       | 'descendant-or-self'
                       | 'following'
                       | 'following-sibling'
                       | 'namespace'
                       | 'parent'
                       | 'preceding'
                       | 'preceding-sibling'
                       | 'self'

And according to 6.1.4 there are abbreviations available to be used to address 5 of the axes:

    .       refers to   self (selects the context node)
    @name   refers to   attributes   (name of "*" means "all")
    name    refers to   child     (name of "*" means "all")
    ..      refers to   parent
    //      refers to   descendants

When the pattern starts with "/", the orientation is from the root of the tree.

The pattern can be comprised of a number of location steps, each step delimited by "/".

3.

Graph traversal

Nikolai Grigoriev

Q expansion

How can I traverse a graph with XSL? Specifically, hyperlinks encode a graph, as in the following four files:

 root.html
<html version = "6">
  <a href = "left_branch.html"/>
  <a href = "right_branch.html"/>
</html>

left_branch.html
<html version = "2">
  <a href = "leaf.html"/>
</html>

right_branch.html
<html version = "3">
  <a href = "leaf.html"/>
</html>

 leaf.html
<html version = "1"/>

There was a lot of discussion about the XSLT power, so I felt kinda challenged to write a graph traversal stylesheet. Given below are my humble results. The stylesheet operates on files of the kind described in the original letter by Jeff Lansing. It lists files reachable from the top node, ordering them by the number of hyperlink transitions required to get there from the root. (It is not exactly the same as was requested; I have chosen the simplest output, to avoid stylesheet overgrowth). The output looks like this (in Jeff's case):

Level 1:  left_branch.html  right_branch.html
Level 2:  leaf.html

The whole thing uses no extensions to XSLT REC, and has been tested under SAXON and XT.

Sure enough, it isn't a complete graph traversal engine: it may choke on files distributed in multiple directories, it produces a spurious reference to root.html (since there is no info about the initial path), etc. Nevertheless, I think the possibility to traverse the graph has been proven, and the rest of functionality is easy to incorporate.

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="ISO-8859-1"/>

<xsl:param name="delimiter" select="'|'"/>
<xsl:variable name="separator" 
       select="concat ($delimiter, $delimiter)"/>

<!-- ===================================================== -->
<!-- Top template: get a list of links and start recursion -->
<!-- ===================================================== -->

<xsl:template match="/">

  <!-- collect links for the first level -->
  <xsl:variable name="links">
    <xsl:apply-templates select="html"/>
  </xsl:variable>

  <!-- start recursion -->
  <xsl:call-template name="visit-node-list">
    <xsl:with-param name="level" select="1"/>
    <xsl:with-param name="node-list" select="$links"/>
    <xsl:with-param name="visited-nodes" select="$links"/>
  </xsl:call-template>

</xsl:template>


<!-- ===================================================== -->
<!-- Extract unique unvisited links from a document.       -->
<!-- Names of visited nodes are stored in a string;        -->
<!-- $delimiter is prepended and appended to every entry   -->
<!-- ===================================================== -->

<xsl:template match="html">
  <xsl:param name="visited-nodes"/>

  <xsl:message>Processing node <xsl:value-of
select="@version"/></xsl:message>

  <xsl:for-each select=".//a">
    <xsl:variable name="link" select="@href"/>
    <xsl:variable name="link-record" 
                    select="concat($delimiter, $link,
$delimiter)"/>

    <!-- check if this is the first occurence of the $link -->
    <xsl:if test="not (preceding::a[@href=$link]) and
                  not (contains($visited-nodes, $link-record))">
      <xsl:value-of select="$link-record"/>
    </xsl:if>

  </xsl:for-each>

</xsl:template>


<!-- ===================================================== -->
<!-- Process all documents from a list. Invokes a document -->
<!-- enumeration template for a list, then recurses.       -->
<!-- ===================================================== -->

<xsl:template name="visit-node-list">
  <xsl:param name="level" select="1"/>
  <xsl:param name="node-list"/>
  <xsl:param name="visited-nodes"/>

  <xsl:if test="contains ($node-list, $delimiter)">

    <!-- print out level info -->
    <xsl:text>Level </xsl:text>
    <xsl:value-of select="$level"/>
    <xsl:text>: </xsl:text>
    <xsl:value-of 
                 select="translate($node-list, $delimiter, ' ')"/>
    <xsl:text>
</xsl:text>

    <!-- collect links for the next level -->
    <xsl:variable name="links">
      <xsl:call-template 
                     name="enumerate-links-from-list">
        <xsl:with-param 
                     name="node-list" select="$node-list"/>
        <xsl:with-param 
                     name="visited-nodes" select="$visited-nodes"/>
      </xsl:call-template>
    </xsl:variable>

    <!-- recurse to next level -->
    <xsl:call-template 
             name="visit-node-list">
      <xsl:with-param 
            name="level" select="$level+1"/>
      <xsl:with-param 
            name="node-list" select="$links"/>
      <xsl:with-param 
             name="visited-nodes" select="concat($visited-nodes,
$links)"/>
    </xsl:call-template>

  </xsl:if>
</xsl:template>


<!-- ===================================================== -->
<!-- Enumerates all documents pointed out from a node list -->
<!-- ===================================================== -->

<xsl:template name="enumerate-links-from-list">
  <xsl:param name="node-list"/>
  <xsl:param name="visited-nodes"/>

  <xsl:if test="contains($node-list, $delimiter)">

    <!-- split the first record from the $node-list -->
    <xsl:variable name="head-record">
      <xsl:choose>
        <xsl:when test="contains($node-list, $separator)">
          <xsl:value-of 
          select="substring-before($node-list, $separator)"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="$node-list"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:variable>

    <xsl:variable name="head"
     select="translate($head-record, $delimiter, '')"/>

    <!-- collect links from 
               the first document in the $node-list -->
    <xsl:variable name="links">
      <xsl:apply-templates select="document($head)/html">
        <xsl:with-param 
           name="visited-nodes" select="$visited-nodes"/>
      </xsl:apply-templates>
    </xsl:variable>
    <xsl:value-of select="$links"/>

    <!-- recursion: 
        repeat the same for the rest of the $node-list -->
    <xsl:call-template name="enumerate-links-from-list">
      <xsl:with-param name="node-list"
        select="substring-after($node-list, $separator)"/>
      <xsl:with-param 
             name="visited-nodes" select="concat($visited-nodes,
$links)"/>
    </xsl:call-template>
  </xsl:if>
</xsl:template>

</xsl:stylesheet>

David Carlisle adds

<?xml version="1.0"?>
<xsl:stylesheet 
         xmlns:xsl="http://www.w3.org/1999/XSL/Transform">


<xsl:template match="html">
  <xsl:call-template name="bfirst">
    <xsl:with-param name="todo" select="."/>
  </xsl:call-template>
</xsl:template>


<xsl:template name="bfirst">
  <xsl:param name="seen" select="/.."/>
  <xsl:param name="todo"/>
  <xsl:if test="not(string($todo[1]/@version) = $seen)">
  <xsl:value-of select="$todo[1]/@version"/>
  <xsl:text> </xsl:text>
 </xsl:if>
    <xsl:if test="count($todo) >0">
 <xsl:call-template name="bfirst">
    <xsl:with-param name="seen" 
    select="$seen|$todo[1]/@version"/>
    <xsl:with-param name="todo" 
           select="$todo[position() > 1] | 
            document($todo[1]/a/@href)/html"/>
  </xsl:call-template>
  </xsl:if>
</xsl:template>


</xsl:stylesheet>