Merge successive descendant nodes into one

2019-07-19 07:52发布

问题:

XML:

<t>
  <ScreenSize>
    <Width>1440</Width>
    <Height>900</Height>
  </ScreenSize>
  <ConfigurationHotSpots>
    <Rectangle>
      <Location>
        <X>0</X>
        <Y>0</Y>
      </Location>
      <Size>
        <Width>50</Width>
        <Height>50</Height>
      </Size>
      <X>0</X>
      <Y>0</Y>
      <Width>50</Width>
      <Height>50</Height>
    </Rectangle>
  </ConfigurationHotSpots>
</t>

Desired Output XML:

<t>
  <ScreenSizeWidth>1440</ScreenSizeWidth>
  <ScreenSizeWidth>900</ScreenSizeWidth>
  <ConfigurationHotSpotsRectangleLocationX>0</ConfigurationHotSpotsRectangleLocationX>
  <ConfigurationHotSpotsRectangleLocationY>0</ConfigurationHotSpotsRectangleLocationY>
  <ConfigurationHotSpotsRectangleSizeWidth>50</ConfigurationHotSpotsRectangleSizeWidth>
  <ConfigurationHotSpotsRectangleSizeHeight>50</ConfigurationHotSpotsRectangleSizeHeight>
  <ConfigurationHotSpotsRectangleX>0</ConfigurationHotSpotsRectangleX>
  <ConfigurationHotSpotsRectangleY>0</ConfigurationHotSpotsRectangleY>
  <ConfigurationHotSpotsRectangleWidth>50</ConfigurationHotSpotsRectangleWidth>
  <ConfigurationHotSpotsRectangleHeight>50</ConfigurationHotSpotsRectangleHeight>
</t>

Rules:

  • For every element in a defined nodeset (in this case <ScreenSize> | <ConfigurationHotSpots>), do the following: process all leaf descendants (i.e., those without children) such that a new element is created; the name of this new element should be the concatenation of all elements between the current node and the childless descendant.
  • There is a variable number of these "blocks" throughout the document, so no manual templates (i.e., one that processes only descendants of <ScreenSize>, one that processes only descendants of <ConfigurationHotSpots>, etc.)

What I Currently Have:

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

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

  <xsl:template match="ScreenSize|ConfigurationHotSpots">
    <xsl:apply-templates select="descendant::*[not(*)]" mode="descendants" />
  </xsl:template>

  <xsl:template match="*" mode="descendants">
    <xsl:element name="{concat(name(ancestor::*[not(self::t)]), name())}">
      <xsl:apply-templates />
    </xsl:element>
  </xsl:template>

</xsl:stylesheet>

The problem seems to be the name(ancestor::*[not(self::t)]) portion; it's not doing what I would like it to be doing (magically outputting the names of those elements, one after another). Instead, this is what I get:

<?xml version="1.0" encoding="UTF-8"?>
<t>
  <ScreenSizeWidth>1440</ScreenSizeWidth>
  <ScreenSizeHeight>900</ScreenSizeHeight>
  <ConfigurationHotSpotsX>0</ConfigurationHotSpotsX>
  <ConfigurationHotSpotsY>0</ConfigurationHotSpotsY>
  <ConfigurationHotSpotsWidth>50</ConfigurationHotSpotsWidth>
  <ConfigurationHotSpotsHeight>50</ConfigurationHotSpotsHeight>
  <ConfigurationHotSpotsX>0</ConfigurationHotSpotsX>
  <ConfigurationHotSpotsY>0</ConfigurationHotSpotsY>
  <ConfigurationHotSpotsWidth>50</ConfigurationHotSpotsWidth>
  <ConfigurationHotSpotsHeight>50</ConfigurationHotSpotsHeight>
</t>

Thanks in advance!

回答1:

Doing name(ancestor::*[not(self::t)]) will not return a list of names, but just the name of the last one it matches (or is it the first?).

A slightly different approach you can take, not that far off from what you are currently doing, is rather than jumping straight to the 'leaf' element, match each level in turn, but keep a running concatenation of the element names, which are passed from one level to anoter by parameters.

Try this XSLT

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="ScreenSize|ConfigurationHotSpots">
        <xsl:apply-templates mode="descendants">
            <xsl:with-param name="name" select="local-name()" />
        </xsl:apply-templates>
    </xsl:template>

    <xsl:template match="*" mode="descendants">
        <xsl:param name="name" />
        <xsl:apply-templates mode="descendants">
            <xsl:with-param name="name" select="concat($name, local-name())" />
        </xsl:apply-templates>
    </xsl:template>

    <xsl:template match="*[not(*)]" mode="descendants">
        <xsl:param name="name" />
        <xsl:element name="{concat($name, local-name())}">
            <xsl:value-of select="." />
        </xsl:element>
    </xsl:template>

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

When applied to your sample XML, the following is output

<t>
   <ScreenSizeWidth>1440</ScreenSizeWidth>
   <ScreenSizeHeight>900</ScreenSizeHeight>
   <ConfigurationHotSpotsRectangleLocationX>0</ConfigurationHotSpotsRectangleLocationX>
   <ConfigurationHotSpotsRectangleLocationY>0</ConfigurationHotSpotsRectangleLocationY>
   <ConfigurationHotSpotsRectangleSizeWidth>50</ConfigurationHotSpotsRectangleSizeWidth>
   <ConfigurationHotSpotsRectangleSizeHeight>50</ConfigurationHotSpotsRectangleSizeHeight>
   <ConfigurationHotSpotsRectangleX>0</ConfigurationHotSpotsRectangleX>
   <ConfigurationHotSpotsRectangleY>0</ConfigurationHotSpotsRectangleY>
   <ConfigurationHotSpotsRectangleWidth>50</ConfigurationHotSpotsRectangleWidth>
   <ConfigurationHotSpotsRectangleHeight>50</ConfigurationHotSpotsRectangleHeight>
</t>


回答2:

This is one of the shortest possible solutions. It isn't as efficient as passing the accumulated path as parameter, but for not too-deeply nested structures this will not be significant:

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

 <xsl:template match=
 "*[not(*) and (ancestor::ScreenSize or ancestor::ConfigurationHotSpots)]">

    <xsl:variable name="vName">
        <xsl:for-each select=
        "ancestor-or-self::*
           [not(descendant::*
                   [self::ScreenSize or self::ConfigurationHotSpots])
           ]">
           <xsl:value-of select="name()"/>
      </xsl:for-each>
  </xsl:variable>
  <xsl:element name="{$vName}"><xsl:value-of select="."/></xsl:element>
 </xsl:template>
 <xsl:template match="text()"/>
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<t>
  <ScreenSize>
    <Width>1440</Width>
    <Height>900</Height>
  </ScreenSize>
  <ConfigurationHotSpots>
    <Rectangle>
      <Location>
        <X>0</X>
        <Y>0</Y>
      </Location>
      <Size>
        <Width>50</Width>
        <Height>50</Height>
      </Size>
      <X>0</X>
      <Y>0</Y>
      <Width>50</Width>
      <Height>50</Height>
    </Rectangle>
  </ConfigurationHotSpots>
</t>

the wanted, correct result is produced:

<ScreenSizeWidth>1440</ScreenSizeWidth>
<ScreenSizeHeight>900</ScreenSizeHeight>
<ConfigurationHotSpotsRectangleLocationX>0</ConfigurationHotSpotsRectangleLocationX>
<ConfigurationHotSpotsRectangleLocationY>0</ConfigurationHotSpotsRectangleLocationY>
<ConfigurationHotSpotsRectangleSizeWidth>50</ConfigurationHotSpotsRectangleSizeWidth>
<ConfigurationHotSpotsRectangleSizeHeight>50</ConfigurationHotSpotsRectangleSizeHeight>
<ConfigurationHotSpotsRectangleX>0</ConfigurationHotSpotsRectangleX>
<ConfigurationHotSpotsRectangleY>0</ConfigurationHotSpotsRectangleY>
<ConfigurationHotSpotsRectangleWidth>50</ConfigurationHotSpotsRectangleWidth>
<ConfigurationHotSpotsRectangleHeight>50</ConfigurationHotSpotsRectangleHeight>


回答3:

Here is a tweak on Dimitre's solution. It is simpler and leverages off the known root element and known positions of the ScreenSize etc., as per OP's comments...

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

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

 <xsl:template match="*[not(*)]
                       [ancestor::ScreenSize|ancestor::ConfigurationHotSpots]">   
    <xsl:variable name="vName">
        <xsl:for-each select="ancestor-or-self::*[not(self::t)]">
           <xsl:value-of select="local-name()"/>
        </xsl:for-each>
    </xsl:variable>
  <xsl:element name="{$vName}"><xsl:value-of select="."/></xsl:element>
 </xsl:template>
 <xsl:template match="text()"/>
</xsl:stylesheet>

Here is an alternate form of the match condition...

 <xsl:template match="(ScreenSize//* | ConfigurationHotSpots//*)[not(*)]">  

I'm not actually sure which is better. If performance isn't a big deal, go with which ever reads easier. Just beware that this means two whole document scans for each template match attempt.