0

I'm trying to produce a tree of data that looks like this:

  • root
    • 1stGenChild1
      • 2ndGenChild1
      • 2ndGenChild2
    • 1stGenChild2

and so produce the code as follows:

<ul>
  <li>root</li>
  <ul>
    <li>1stGenChild1</li>
  <ul>
    <li>2ndGenChild1</li>
    <li>2ndGenChild2</li>
  </ul>
  <li>1stGenChild2</li>
</ul>

where my data is in the form:

<XML_FILTER>
  <XPATH @xpath="root/1stGenChild1" />
  <XPATH @xpath="root/1stGenChild1/2ndGenChild1" />
  <XPATH @xpath="root/1stGenChild1/2ndGenChild2" />
  <XPATH @xpath="root/1stGenChild2" />
</XML_FILTER>

It would be relatively simple to produce this in XSLT2 with tokenise, but I cannot use XSLT2 for this as I am restricted to using only MSXML 6.0 by the system in use.

The biggest problem I've found is that the normal methods of doing this can't deal with the root never being explicitly stated in its own attribute, but I still need this node in the output.

How can I produce a tree for data which may have more child node levels? - ie. many more lists within lists than the example shown above.

Also does anyone know if there is a limit to the number of lists within lists before indentation is not rendered by browsers, as this will make the view useless.

Many thanks.

Luke Hargraves
  • 93
  • 1
  • 1
  • 6
  • "*the normal methods of doing this can't deal with the data being in an attribute, rather than inside of the element.*" What difference does it make? What are the "normal methods" you refer to? – michael.hor257k Apr 21 '17 at 16:27
  • On reflection, I think the difference in my case is that the first xpath in my data is not the root, it's the first child. I still want the same output, with root in its own level. I've adjusted the question accordingly. – Luke Hargraves Apr 21 '17 at 20:03
  • Is the name of the root element known in advance? – michael.hor257k Apr 21 '17 at 20:35

1 Answers1

2

Here is a simple method for nesting the nodes according to their hierarchy:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/XML_FILTER">
    <ul>
        <xsl:apply-templates select="XPATH[not(contains(@xpath, '/'))]"/>
    </ul>
</xsl:template>

<xsl:template match="XPATH">
    <xsl:variable name="dir" select="concat(@xpath, '/')" />
    <li>
        <xsl:value-of select="@xpath"/>
    </li>   
    <xsl:variable name="child" select="../XPATH[starts-with(@xpath, $dir) and not(contains(substring-after(@xpath, $dir), '/'))]" />
    <xsl:if test="$child">
        <ul>
            <xsl:apply-templates select="$child"/>
        </ul>           
    </xsl:if>
</xsl:template>

</xsl:stylesheet> 

Applied to your example input (after removing the illegal @ characters from the attribute's name!), the result will be:

<?xml version="1.0" encoding="UTF-8"?>
<ul>
  <li>root</li>
  <ul>
    <li>root/1stGenChild1</li>
    <ul>
      <li>root/1stGenChild1/2ndGenChild1</li>
      <li>root/1stGenChild1/2ndGenChild2</li>
    </ul>
    <li>root/1stGenChild2</li>
  </ul>
</ul>

Now you only need to replace the:

<xsl:value-of select="@xpath"/>

instruction with a call to a named template that returns the last token - see: https://stackoverflow.com/a/41625340/3016153


Or do this instead:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/XML_FILTER">
    <ul>
        <xsl:apply-templates select="XPATH[not(contains(@xpath, '/'))]"/>
    </ul>
</xsl:template>

<xsl:template match="XPATH">
    <xsl:param name="parent"/>
    <xsl:variable name="dir" select="concat(@xpath, '/')" />
    <li>
        <xsl:value-of select="substring-after(concat('/', @xpath), concat($parent, '/'))"/>
    </li>   
    <xsl:variable name="child" select="../XPATH[starts-with(@xpath, $dir) and not(contains(substring-after(@xpath, $dir), '/'))]" />
    <xsl:if test="$child">
        <ul>
            <xsl:apply-templates select="$child">
                <xsl:with-param name="parent" select="concat('/', @xpath)"/>
            </xsl:apply-templates>
        </ul>           
    </xsl:if>
</xsl:template>

</xsl:stylesheet> 
Community
  • 1
  • 1
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • Very good solutions, but not quite the output I need. How would I modify this to only display the last portion of the path in the child ul elements, so that the output matches my description? (ie. looks like a directory structure with native paths, rather than showing the absolute path at each level) – Luke Hargraves Apr 21 '17 at 19:56