Search code examples
xmlxsltxpathxslt-2.0xpath-2.0

With XPath, select parent instead, if all children are selected


I have a stylesheet (XSLT 2.0) that removes elements. Now I have an issue where the DTD of the XML I am "pruning" does not allow me to remove all elements under a certain node, without removing the empty node as well. Hence I want to remove the parent element as well if all the children are removed. I want to select the elements to remove with an XPath expression.

As an example, consider this XML (the DTD is not provided, but basically states that a box must contain at least one crayon):

<?xml version="1.0" encoding="UTF-8"?>
<test>
    <box>
        <crayon color="red"/>
        <crayon color="red"/>
        <crayon color="red"/>
        <crayon/>
    </box>
    <box>
        <crayon/>
        <crayon/>
    </box>
    <box>
        <crayon color="red"/>
        <crayon color="red"/>
    </box>
    <box>
        <crayon color="red"/>
        <crayon color="red"/>
        <crayon color="red"/>
        <crayon/>
    </box>
</test>

The output I want is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<test>
    <box>
        <crayon/>
    </box>
    <box>
        <crayon/>
        <crayon/>
    </box>
    <box>
        <crayon/>
    </box>
</test>

This is a stylesheet that unfortunately does not do what I want, but shows the form I want to achieve:

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

    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>

    <!-- Next row should apply to sets of crayons or complete boxes. -->
    <xsl:template match="//box[if (count(crayon[@color = 'red']) = count(crayon)) then (.) else (crayon[@color = 'red'])]"/>

</xsl:stylesheet>

The reason that I want to manage this using one XPath expression is that I have a function that generates the stylesheet, taking the XPath as the input argument.


Solution

  • Is XSLT 3 (as supported by Saxon 9.8 or Altova 2017 or 2018 or Exselt) an option? There you could exploit the new xsl:where-populated (https://www.w3.org/TR/xslt/#element-where-populated):

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        version="3.0">
    
      <xsl:strip-space elements="*"/>
      <xsl:output indent="yes"/>
    
      <xsl:mode on-no-match="shallow-copy"/>
    
      <xsl:template match="box">
          <xsl:where-populated>
              <xsl:next-match/>
          </xsl:where-populated>
      </xsl:template>
    
      <xsl:template match="box/crayon[@color = 'red']"/>
    
    </xsl:stylesheet>
    

    http://xsltfiddle.liberty-development.net/6qM2e2g

    I am not quite sure which part you need to set us a parameter or variable but XSLT 3 with shallow attributes eases that task as well.

    Using XSLT 2 I think you can use

    <xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
    
    
      <xsl:strip-space elements="*"/>
      <xsl:output indent="yes"/>
    
        <xsl:template match="@*|node()">
            <xsl:copy>
                <xsl:apply-templates select="@*|node()"/>
            </xsl:copy>
        </xsl:template>
    
        <xsl:template match="box[not(crayon[not(@color = 'red')])] | box/crayon[@color = 'red']"/>
    
    </xsl:transform>
    

    http://xsltransform.hikmatu.com/nbUY4kp