Very interesting Python bounty question which I know can be solved with XSLT 1.0. Please note this is not a duplicate question as previous post centered on Python methods while this is attempting an XSLT solution to the same problem. Below is my attempt but is constrained to a pre-set number of parent/child combinations here being four levels deep and walks through each level conditionally.
Is there a way to generalize my solution for any combination level? I understand this may require tokenizing values with the -->
separator. Expected output is current output but need a dynamic solution. I include the Python script to show final end result. To be clear in conflict of interest, I will not use any answer here in above post but do kindly invite you to do so!
XML Input
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<nodes>
<node name="Car" child="Engine"/>
<node name="Car" child="Wheel"/>
<node name="Engine" child="Piston"/>
<node name="Engine" child="Carb"/>
<node name="Carb" child="Bolt"/>
<node name="Spare Wheel"/>
<node name="Bolt" child="Thread"/>
<node name="Carb" child="Foat"/>
<node name="Truck" child="Engine"/>
<node name="Engine" child="Bolt"/>
<node name="Wheel" child="Hubcap"/>
</nodes>
XSLT
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output version="1.0" encoding="UTF-8" indent="yes" />
<xsl:strip-space elements="*"/>
<xsl:template match="nodes">
<data>
<xsl:apply-templates select="node[not(@name=ancestor::nodes/node/@child)]"/>
</data>
</xsl:template>
<xsl:template match="node">
<xsl:variable select="@name" name="currname"/>
<xsl:variable select="@child" name="currchild"/>
<xsl:variable select="/nodes/node" name="nodeset1"/>
<xsl:variable select="/nodes/node[@name=$currchild]" name="nodeset2"/>
<xsl:variable select="/nodes/node[@name=$nodeset2/@child]" name="nodeset3"/>
<xsl:variable select="/nodes/node[@name=$nodeset3/@child]" name="nodeset4"/>
<xsl:for-each select="$nodeset2">
<xsl:variable select="@child" name="nodeset2child"/>
<xsl:for-each select="$nodeset3">
<xsl:variable select="@child" name="nodeset3child"/>
<xsl:if test="@name=$nodeset2child">
<xsl:for-each select="$nodeset4">
<xsl:if test="@name=$nodeset3child">
<xsl:value-of select="$currname"/> --> <xsl:value-of select="$currchild"/> --> <xsl:value-of select="$nodeset2child"/> --> <xsl:value-of select="$nodeset3child"/> --> <xsl:value-of select="@child"/><xsl:text>
</xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:if test="$nodeset2child!=$nodeset3/@child and $nodeset3child != $nodeset4/@name">
<xsl:value-of select="$currname"/> --> <xsl:value-of select="$currchild"/> --> <xsl:value-of select="$nodeset2child"/> --> <xsl:value-of select="$nodeset3child"/><xsl:text>
</xsl:text>
</xsl:if>
</xsl:if>
</xsl:for-each>
<xsl:if test="not($nodeset2child=$nodeset3/@child or ancestor::nodes/node[@name=$nodeset2child]/@child)">
<xsl:value-of select="$currname"/> --> <xsl:value-of select="$currchild"/> --> <xsl:value-of select="$nodeset2child"/><xsl:text>
</xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:value-of select="@name[not(ancestor::node/@child=$nodeset2/@name)]"/><xsl:text>
</xsl:text>
</xsl:template>
</xsl:transform>
XML Transformed Output
<?xml version='1.0' encoding='UTF-8'?>
<data>Car --> Engine --> Piston
Car --> Engine --> Carb --> Bolt --> Thread
Car --> Engine --> Carb --> Foat
Car --> Engine --> Bolt --> Thread
Car --> Wheel --> Hubcap
Spare Wheel
Truck --> Engine --> Piston
Truck --> Engine --> Carb --> Bolt --> Thread
Truck --> Engine --> Carb --> Foat
Truck --> Engine --> Bolt --> Thread
</data>
Python script (running an xpath on the transformed output root node)
import lxml.etree as ET
# LOAD XML AND XSL DOCS
dom = ET.parse('Input.xml')
xslt = ET.parse('XSLTScript.xsl')
# TRANSFORM XML
transform = ET.XSLT(xslt)
newdom = transform(dom)
# XPATH NEW DOM ROOT NODE (<data>)
print(newdom.xpath('/data')[0].text.replace("\n\n", "\n"))
# Car --> Engine --> Piston
# Car --> Engine --> Carb --> Bolt --> Thread
# Car --> Engine --> Carb --> Foat
# Car --> Engine --> Bolt --> Thread
# Car --> Wheel --> Hubcap
# Spare Wheel
# Truck --> Engine --> Piston
# Truck --> Engine --> Carb --> Bolt --> Thread
# Truck --> Engine --> Carb --> Foat
# Truck --> Engine --> Bolt --> Thread
And if you want only the leaf nodes
This should do any recursion level but will also output all intermediate steps:
Ups, had to adjust the output in third case, @name no more necessary as already in the $already...
Here is a much shorter (23-line) and efficient solution
This is also computationally the simplest one -- compare nesting level 1 to nesting level of 3 - 4 ...
This solution is tail-recursive meaning that any good XSLT processor optimizes it with iteration, thus avoiding the possibility of stack-overflow, as the maximum call-stack depth remains constant (1):
When this transformation is applied on the provided XML document:
The wanted, correct result is produced: