1

I am trying to flatten a tree of nested XML elements by combining the values of an attribute using XSLT. For instance, if I have the following input:

<node value="a">
    <node value="b">
        <node value="c">
            <node value="d">
            </node>
        </node>
        <node value="e">
            <node value="f">
            </node>
            <node value="g">
                <node value="h">
                </node>
            </node>
        </node>
    </node>
</node>

Then these would be the "flattened" results I would like to be able to get:

a/b/c/d
a/b/e/f
a/b/e/g/h

All I currently have been able to achieve is outputting a record only on the most deeply nested occurrences of node "node" with the "value" attribute:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv">
    <xsl:output method="text" encoding="utf-8" />
    <xsl:template match="text()|@*"/>
    <xsl:template match="node[@value]">
        <xsl:if test="not(descendant::node[@value])">
            <xsl:value-of select="@value"/>
            <xsl:text>&#xa;</xsl:text>
        </xsl:if>
        <xsl:apply-templates/>
    </xsl:template>
</xsl:stylesheet>

As you may have gathered from my description and the xsl:if test, a potential complication is that some instances of the "node" element may not have the "value" attribute and so this must be explicitly checked. How can I update this stylesheet to achieve the desired result?

1 Answer 1

2

With XSLT 2 or 3 it becomes

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

  <xsl:output method="text"/>

  <xsl:template match="/">
      <xsl:value-of select="descendant::node[@value and not(descendant::node[@value])]!string-join(ancestor-or-self::node/@value, '/')" separator="&#10;"/>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/b4GWVh/0

With XSLT 1 I would use

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

  <xsl:output method="text"/>

  <xsl:template match="/">
      <xsl:apply-templates select="descendant::node[@value and not(descendant::node[@value])]"/>
  </xsl:template>

  <xsl:template match="node">
      <xsl:apply-templates select="ancestor-or-self::node/@value"/>
      <xsl:text>&#10;</xsl:text>
  </xsl:template>

  <xsl:template match="@value">
      <xsl:if test="position() > 1">/</xsl:if>
      <xsl:value-of select="."/>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/b4GWVh/1

Sign up to request clarification or add additional context in comments.

7 Comments

This helped me reach a solution that seems to be working (using your second approach since xsltproc is limited to XSLT 1.0), but I had to modify it a bit first. As is, it works for the immediate example, but afaik fails to allow me to retain access to any other attributes of the respective "node" elements I may like to process. I had to push the attribute template's value-of back into the node template (now needing last() in apply-templates) and put the delimiter/newline within a choose block based on another "am I the last descendant" test (position() didn't seem to work anymore).
I'm not sure what the proper etiquette here is - should I just leave the modification as a comment here for future readers that share that need, accept the answer, and walk away, or should I update the question and let you similarly update the answer...?
I am afraid your problem description does not show any other attributes at all and does not explain that and where you want to output them. In a template matching any attribute of an element, like the @value, you can of course navigate to ../@foo to select the foo attribute of the same element.
If you want to process the node elements twice instead of processing the value attributes then you can use a mode e.g. <xsl:apply-templates select="ancestor-or-self::node/." mode="atts"/> and then output e.g. <xsl:template match="node" mode="atts"><xsl:if test="position() > 1">/</xsl:if><xsl:value-of select="concat(@value, ',', @foo)"/></xsl:template>.
A simple, borderline hello world style input for me is 60k lines long, so I was merely attempting to create a minimal example that got straight to the specific problem I was dealing with. For posterity, the modification I originally made to your answer looked like this: <xsl:template match="node[@value]"><xsl:apply-templates select="(ancestor::node[@value])[last()]"/><do-other-stuff-here><xsl:value-of select="@value"/><xsl:choose><xsl:when test="not(descendant::node[@value])"><xsl:text>&#xa;</xsl:text></xsl:when><xsl:otherwise><xsl:text>/</xsl:text></xsl:otherwise></xsl:choose></xsl:template>
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.