Search code examples
xsltlibxml2libxslt

Generate a breadcrumb trail with xsl from a node structure


I have difficulties to write a template that generates a breadcrumb trial out of a node structure. It is not working correctly up to now, there is some flaw in my thinking how it should walk the item path.

Consider the following page structure:

<!-- ===== SITE PAGE STRUCTURE ===================================== -->
<index>
   <item section="home" id="index"></item>
   <item section="service" id="index">
      <item id="content-management-systems">
         <item id="p1-1"/>
         <item id="p1-2"/>
         <item id="p1-3"/>
      </item>
      <item id="online-stores"></item>
      <item id="search-engines-and-ir"></item>
      <item id="web-applications"></item>
   </item>

   <item section="solutions" id="index">
      <item id="document-clustering"></item>
   </item>
   <item section="company" id="index">
      <item section="company" id="about"></item>
      <item section="company" id="philosophy" ></item>
      ...
   </item>
...
</item>

This site index represents a site-structure of xml content pages in its hierarchy (consider it to be a menu). It contains of sections, that represent the site sections just as home, company, service, solutions, etc. These sections can contain sub-sections with pages, or just regular content pages. A content page (its xml contents such as title, text content, etc) is identified by the @id attribute in the item tree. The @id attribute mainly is used to fetch the content of the entire page that will be rendered to html. The breadcrumb template uses the item node @id attribute to get the title of the page (which will be shown in the breadcrumb trail).

I try to implement the following template that walks the tree by checking the target section attribute @section and the target page attribute @id in the tree. I expect it to walk the axis down until the target item_target is found by comparing the ancestors @section attribute and the @id with $item_target of each node in that axis.

For example: Attribute *$item_section=service* and the page id *target item_target=p1-1* should now recursively "walk" to the section branch "service" (depth 1), check if the target page @id is found on this level. In this case it is not found, so it makes the next recurive call (via apply-templates) to the next item node level (in this case it would be content-management-systems, there the target item page p1-1 is found, so the trail process is finished:

The result should like this:

home >> service >> content management systems >> p1-1

But unfortunately it is not working correct, at least not in every case. Also maybe it can be solved more easily. I try to implement it as an recursive template that walks from the top (level 0) to the target page (item node) as a leaf.

    <!-- walk item path to generate a breadcrumb trail -->
    <xsl:template name="breadcrumb">
        <a>
            <xsl:attribute name="href">
                <xsl:text>/</xsl:text>
                <xsl:value-of select="$req-lg"/>
                <xsl:text>/home/index</xsl:text>
            </xsl:attribute>
            <xsl:value-of select="'Home'"/>
        </a>

        <xsl:apply-templates select="$content/site/index" mode="Item-Path">
            <xsl:with-param name="item_section" select="'service'"/>
            <xsl:with-param name="item_target" select="'search-engines-and-ir'"/>
            <xsl:with-param name="depth" select="0"/>
        </xsl:apply-templates>
    </xsl:template>

    <xsl:template match="item" mode="Item-Path">
        <xsl:param name="item_section" />
        <xsl:param name="item_target" />
        <xsl:param name="depth" />
        <!--
        depth=<xsl:value-of select="$depth"/>
        count=<xsl:value-of select="count(./node())"/><br/>
-->
        <xsl:variable name="cur-id" select="@id"/>
        <xsl:variable name="cur-section" select="@section"/>
        <xsl:choose>    
            <xsl:when test="@id=$item_target">
                &gt;&gt;
                <a>
                    <xsl:attribute name="href">
                        <xsl:text>/</xsl:text>
                                            <!-- req-lg: global langauge variable -->
                        <xsl:value-of select="$req-lg"/>
                        <xsl:text>/</xsl:text>
                        <xsl:value-of select="$item_section"/>
                        <xsl:text>/</xsl:text>
                        <xsl:if test="$depth = 2">
                            <xsl:value-of select="../@id"/>
                            <xsl:text>/</xsl:text>
                        </xsl:if>
                        <xsl:value-of select="@id"/>
                    </xsl:attribute>
                    <xsl:value-of 
                        select="$content/page[@id=$cur-id]/title"/>
                </a>
            </xsl:when>
            <xsl:otherwise>
                <xsl:if test="ancestor-or-self::item/@section = $item_section and count(./node()) > 0">
                &gt;&gt;:
                <a>
                    <xsl:attribute name="href">
                        <xsl:text>/</xsl:text>
                                            <!-- req-lg: global langauge variable -->
                        <xsl:value-of select="$req-lg"/>
                        <xsl:text>/</xsl:text>
                        <xsl:value-of select="$item_section"/>
                        <xsl:text>/</xsl:text>
                        <xsl:if test="$depth = 2">
                            <xsl:value-of select="../@id"/>
                            <xsl:text>/</xsl:text>
                        </xsl:if>
                        <xsl:value-of select="@id"/>
                    </xsl:attribute>
                    <xsl:value-of 
                        select="$content/page[@id=$cur-id and @section=$item_section]/title"/>
                </a>
                </xsl:if>
            </xsl:otherwise>
        </xsl:choose>

        <xsl:apply-templates select="item" mode="Item-Path">
            <xsl:with-param name="item_section" select="$item_section"/>
            <xsl:with-param name="item_target" select="$item_target"/>
            <xsl:with-param name="depth" select="$depth + 1"/>
        </xsl:apply-templates>

    </xsl:template>

So as the hardcoded parameters in the template breadcrumb, target section = 'service' and target page = 'search-engines-and-ir', I expect an output like

home >> service >> search-engines-and-ir

But the output is

home >> service >> content-management-systems >> search-engines-and-ir

which is obviously not correct.

Can anybody give me a hint how to correct this issue? It would be even more elegant to avoid that depth checking, but up to now I cannot think of a other way, I am sure there is a more elegant solution.

I work with XSLT 1.0 (libxml via PHP5).

Hope my question is clear enough, if not, please ask :-) Thanks for the help in advance!


Solution

  • As simple as this:

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:output method="text"/>
    
     <xsl:key name="kNodeById" match="item" use="@id"/>
    
     <xsl:template match="/">
      <xsl:text>home</xsl:text>
      <xsl:call-template name="findPath">
       <xsl:with-param name="pStart" select="'service'"/>
       <xsl:with-param name="pEnd" select="'search-engines-and-ir'"/>
      </xsl:call-template>
     </xsl:template>
    
     <xsl:template name="findPath">
      <xsl:param name="pStart"/>
      <xsl:param name="pEnd"/>
    
      <xsl:for-each select=
      "key('kNodeById', $pEnd)
           [ancestor::item[@section=$pStart]]
            [1]
             /ancestor-or-self::item
                    [not(descendant::item[@section=$pStart])]
      ">
    
       <xsl:value-of select=
        "concat('>>', @id[not(../@section)], @section)"/>
      </xsl:for-each>
     </xsl:template>
    </xsl:stylesheet>
    

    the wanted, correct result is produced:

    home>>service>>search-engines-and-ir
    

    Do Note:

    1. This solution prints the breadcrumb from any node -- anywhere in the hierarchy to any of its descendent nodes -- anywhere in the hierarchy. More precisely, for the first item (in document order) with id attribute equal to $pEnd, the breadcrumb is generated from its inner-most ancestor whose section attribute is equal to $pStart -- to that item element.

    2. This solution should be much more efficient than any solution using //, because we are using a key to locate efficiently the "end" item element.


    II. XSLT 2.0 solution:

    Much shorter and easier -- an XPathe 2.0 single expression:

    <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:output method="text"/>
    
     <xsl:key name="kNodeById" match="item" use="@id"/>
    
     <xsl:template match="/">
      <xsl:value-of select=
      "string-join(
           (
            'home',
           key('kNodeById', $pEnd)
              [ancestor::item[@section=$pStart]]
                  [1]
                    /ancestor-or-self::item
                    [not(descendant::item[@section=$pStart])]
                           /(@id[not(../@section)], @section)[1]
    
            ),
          '>>'
            )
      "/>
     </xsl:template>
    </xsl:stylesheet>