0

Hi I have one xml that need to be nest based on there level attribute but some how i miss the step and it create duplicates. Logic for wrap is wrap element based on @level

My input xml

<root>
   <wrap level="101" name="pgrp">
       <title>0</title>
   </wrap>
   <wrap level="201" name="p">
       <text>2</text>
   </wrap>
   <wrap level="301" name="quote">
       <qp>q2</qp>
   </wrap>
   <wrap level="301" name="quote">
       <qp>q3</qp>
   </wrap>
   <wrap level="102" name="pgrp">
       <title>0</title>
   </wrap>
   <wrap level="201" name="p">
       <text>3</text>
   </wrap>
   <wrap level="201" name="p">
       <text>4</text>
   </wrap>
   <wrap level="301" name="list">
       <qp>q4</qp>
   </wrap>
   <wrap level="301" name="list">
       <qp>q9</qp>
   </wrap>
   <wrap level="201" name="p">
       <text>5</text>
   </wrap>
   <wrap level="301" name="quote">
       <qp>q5</qp>
   </wrap>
   <wrap level="302" name="list">
       <ql>ql51</ql>
   </wrap>
   <wrap level="202" name="p">
       <text>1a</text>
   </wrap>
   <wrap level="202" name="p">
       <text>1aa</text>
   </wrap>
   
</root>

XSLT

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    version="2.0" exclude-result-prefixes="#all">
    <xsl:output method="xml" indent="yes"/>
    <xsl:strip-space elements="*"/>
    
    <xsl:template match="root">
        <xsl:variable name="initial">
            <xsl:apply-templates select="node()"/>
        </xsl:variable>
        
        <xsl:variable name="wrapper">
            <firstwrap>
                <xsl:for-each-group select="$initial/*" group-adjacent="name(.)">
                    <wrapper>
                        <xsl:for-each-group select="current-group()" group-adjacent="concat(@name, '|', @level)">
                            <xsl:choose>
                                <xsl:when test="current-group()/@name = ('list', 'quote') and count(current-group()) gt 1">
                                    <wrap level="{current-group()[1]/@level}" name="{current-group()[1]/@name}">
                                        <xsl:copy-of select="current-group()/node()"/>
                                    </wrap>
                                </xsl:when>
                                <xsl:otherwise>
                                    <xsl:copy-of select="current-group()"/>
                                </xsl:otherwise>
                            </xsl:choose>
                        </xsl:for-each-group>
                    </wrapper>
                </xsl:for-each-group>
            </firstwrap>
        </xsl:variable>
        
        <xsl:result-document href="tempoutput/000wrapper.xml">
            <xsl:copy-of select="$wrapper"></xsl:copy-of>
        </xsl:result-document>
        
        <secondwrap>
            <xsl:apply-templates select="$wrapper"/>
        </secondwrap>
    
    </xsl:template>
    
    <xsl:template match="node() | @*">
        <xsl:copy>
            <xsl:apply-templates select="node() | @*"/>
        </xsl:copy>
    </xsl:template>
    
    <xsl:template match="root//wrap">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:attribute name="posid" select="generate-id(.)"/>
            <xsl:apply-templates/>
        </xsl:copy>
    </xsl:template>
    

    <xsl:template match="firstwrap | secondwrap">
        <xsl:apply-templates/>
    </xsl:template>
    
    <xsl:template match="secondwrap//*[@tempid = preceding::*/@tempid]" priority="10"/>
    
    <xsl:template match="secondwrap//@tempid"/>
        
    <xsl:template match="wrapper" priority="10">
        <xsl:choose>
            <xsl:when test="wrap">
                <xsl:variable name="wrap" select="wrap[1]"/>
                <xsl:variable name="level" select="$wrap/@level"/>
                <xsl:call-template name="wrapper">
                    <xsl:with-param name="wrap" select="$wrap"/>
                </xsl:call-template>                
            </xsl:when>
            <xsl:otherwise>
                <xsl:apply-templates/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>        
    
    <xsl:template name="wrapper">
        <xsl:param name="wrap"/>
        <xsl:param name="wrappercallsame" select="false()"/>
        <xsl:variable name="level" select="$wrap/@level"/>
        <xsl:variable name="wrapidentifier" select="$wrap/@posid"/>
        
        <xsl:element name="{$wrap/@name}">
            <xsl:attribute name="tempid" select="$wrapidentifier"/>
            <xsl:copy-of select="$wrap/node()"/>
            <xsl:if test="$wrap/following-sibling::wrap[1][@level gt $wrap/@level]">
                <xsl:call-template name="wraplevelnest">
                    <xsl:with-param name="wrap" select="$wrap"/>
                </xsl:call-template>
            </xsl:if>
        </xsl:element>
        <!--<xsl:if test="not($wrappercallsame)">-->
            <xsl:if test="$wrap/following-sibling::*[@level le $wrap/@level][preceding-sibling::wrap[@level = $wrap/@level][1][@posid = $wrap//@posid]]">
                <xsl:call-template name="wraplevelsame">
                    <xsl:with-param name="wrap" select="$wrap"/>
                </xsl:call-template>
            </xsl:if>
        <!--</xsl:if>-->
        
    </xsl:template>
    
    <xsl:template name="wraplevelnest">
        <xsl:param name="wrap"/>
        <xsl:variable name="wrapidentifier" select="$wrap/@posid"/>
        <xsl:for-each select="$wrap/following-sibling::*[@level gt $wrap/@level][not(@level = $wrap/@level)][preceding-sibling::wrap[@level = $wrap/@level][1][@posid = $wrapidentifier]]">
            <xsl:variable name="currlevel" select="@level"/>
            <xsl:choose>
                <xsl:when test="preceding-sibling::wrap[preceding-sibling::wrap[@level = $wrap/@level][1][@posid = $wrapidentifier]]">
                    <xsl:variable name="level">
                        <xsl:for-each select="preceding-sibling::wrap[preceding-sibling::wrap[@level = $wrap/@level][1][@posid = $wrapidentifier]]">
                            <lev><xsl:value-of select="@level"/></lev>
                        </xsl:for-each>
                    </xsl:variable>
                    <xsl:if test="every $l in $level//lev satisfies number(@level) lt number($l)">
                        <xsl:call-template name="wrapper">
                            <xsl:with-param name="wrap" select="."/>
                            <xsl:with-param name="wrappercallsame" select="false()"/>
                        </xsl:call-template>
                    </xsl:if>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:call-template name="wrapper">
                        <xsl:with-param name="wrap" select="."/>
                        <xsl:with-param name="wrappercallsame" select="false()"/>
                    </xsl:call-template>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:for-each>
    </xsl:template>
    
    <xsl:template name="wraplevelsame">
        <xsl:param name="wrap"/>
        <xsl:variable name="wrapidentifier" select="$wrap/@posid"/>
        <xsl:for-each select="$wrap/following-sibling::*[@level le $wrap/@level][preceding-sibling::wrap[@level = $wrap/@level][1][@posid = $wrap//@posid]]">
            <xsl:variable name="currlevel" select="@level"/>
            <xsl:choose>
                <xsl:when test="preceding-sibling::wrap[preceding-sibling::wrap[@level = $wrap/@level][1][@posid = $wrapidentifier]]">
                    <xsl:variable name="level">
                        <xsl:for-each select="preceding-sibling::wrap[preceding-sibling::wrap[@level = $wrap/@level][1][@posid = $wrapidentifier]]">
                            <lev><xsl:value-of select="@level"/></lev>
                        </xsl:for-each>
                    </xsl:variable>
                    <xsl:if test="every $l in $level//lev satisfies number(@level) lt number($l)">
                        <xsl:call-template name="wrapper">
                            <xsl:with-param name="wrap" select="."/>
                            <xsl:with-param name="wrappercallsame" select="true()"/>
                        </xsl:call-template>
                    </xsl:if>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:call-template name="wrapper">
                        <xsl:with-param name="wrap" select="."/>
                        <xsl:with-param name="wrappercallsame" select="true()"/>
                    </xsl:call-template>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:for-each>
    </xsl:template>
    
    
    <xsl:template match="wrapper/wrap[position() ne 1]"/>
        
    
    
    
</xsl:stylesheet>

current output

<?xml version="1.0" encoding="UTF-8"?>
<secondwrap>
   <pgrp tempid="d1e2">
      <title>0</title>
      <p tempid="d1e4">
         <text>2</text>
         <quote tempid="">
            <qp>q2</qp>
            <qp>q3</qp>
         </quote>
      </p>
      <pgrp tempid="d1e10">
         <title>0</title>
         <p tempid="d1e12">
            <text>3</text>
         </p>
         <p tempid="d1e14">
            <text>4</text>
            <list tempid="">
               <qp>q4</qp>
               <qp>q9</qp>
            </list>
         </p>
         <p tempid="d1e20">
            <text>5</text>
            <quote tempid="d1e22">
               <qp>q5</qp>
               <list tempid="d1e25">
                  <ql>ql51</ql>
               </list>
               <p tempid="d1e27">
                  <text>1a</text>
               </p>
               <p tempid="d1e29">
                  <text>1aa</text>
               </p>
            </quote>
            <p tempid="d1e27">
               <text>1a</text>
            </p>
            <p tempid="d1e29">
               <text>1aa</text>
            </p>
            <p tempid="d1e27">
               <text>1a</text>
            </p>
            <p tempid="d1e29">
               <text>1aa</text>
            </p>
         </p>
      </pgrp>
      <pgrp tempid="d1e10">
         <title>0</title>
         <p tempid="d1e12">
            <text>3</text>
         </p>
         <p tempid="d1e14">
            <text>4</text>
            <list tempid="">
               <qp>q4</qp>
               <qp>q9</qp>
            </list>
         </p>
         <p tempid="d1e20">
            <text>5</text>
            <quote tempid="d1e22">
               <qp>q5</qp>
               <list tempid="d1e25">
                  <ql>ql51</ql>
               </list>
               <p tempid="d1e27">
                  <text>1a</text>
               </p>
               <p tempid="d1e29">
                  <text>1aa</text>
               </p>
            </quote>
            <p tempid="d1e27">
               <text>1a</text>
            </p>
            <p tempid="d1e29">
               <text>1aa</text>
            </p>
            <p tempid="d1e27">
               <text>1a</text>
            </p>
            <p tempid="d1e29">
               <text>1aa</text>
            </p>
         </p>
      </pgrp>
   </pgrp>
</secondwrap>


Expected output

<?xml version="1.0" encoding="UTF-8"?>
<secondwrap>
   <pgrp tempid="d1e2">
      <title>0</title>
      <p tempid="d1e4">
         <text>2</text>
         <quote tempid="">
            <qp>q2</qp>
            <qp>q3</qp>
         </quote>
      </p>
      <pgrp tempid="d1e10">
         <title>0</title>
         <p tempid="d1e12">
            <text>3</text>
         </p>
         <p tempid="d1e14">
            <text>4</text>
            <list tempid="">
               <qp>q4</qp>
               <qp>q9</qp>
            </list>
         </p>
         <p tempid="d1e20">
            <text>5</text>
            <quote tempid="d1e22">
               <qp>q5</qp>
               <list tempid="d1e25">
                  <ql>ql51</ql>
               </list>
            </quote>
            <p tempid="d1e27">
               <text>1a</text>
            </p>
            <p tempid="d1e29">
               <text>1aa</text>
            </p>
         </p>
      </pgrp>
   </pgrp>
</secondwrap>

Thanks in Advance for your help

1
  • "Logic for wrap is wrap element based on @level": in what way do those level attributes determine the nesting, any level higher than another means another level of nesting? Can these be arbitrary numbers at any range or is there any reason the example has lots of e.g. 101, 201, 301? Commented May 13 at 8:14

1 Answer 1

1

I would try to solve this through recursive and nested grouping, the following code sample uses XSLT 3, not 2, as I think in 2025 the supported versions of e.g. Saxon or Altova support XSLT 3 anyway:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="#all"
  xmlns:mf="http://example.com/mf"
  expand-text="yes">
  
  <xsl:function name="mf:nest" as="node()*">
    <xsl:param name="nodes" as="node()*"/>
    <xsl:variable name="min-level" select="min($nodes/@level/xs:integer(.))"/>

    <xsl:for-each-group select="$nodes" group-starting-with="*[@level = $min-level]">
      <xsl:choose>
        <xsl:when test="self::*[@level = $min-level]">
          <xsl:element name="{@name}">
            <xsl:copy-of select="@level, node()"/>
            <xsl:sequence select="mf:nest(tail(current-group()))"/>
          </xsl:element>          
        </xsl:when>
        <xsl:otherwise>
          <xsl:sequence select="mf:nest(current-group())"/>
        </xsl:otherwise>
      </xsl:choose>

    </xsl:for-each-group>
  </xsl:function>
  
  
  <xsl:function name="mf:merge" as="node()*">
    <xsl:param name="nodes" as="node()*"/>
    <xsl:for-each-group select="$nodes" composite="yes" group-adjacent="node-name(), @level, node-name() = (QName('', 'list'), QName('', 'quote'))">
      <xsl:choose>
        <xsl:when test="current-grouping-key()[3]">
          <xsl:copy>
            <xsl:sequence select="@* except @level, mf:merge(current-group()/node())"/>
          </xsl:copy>
        </xsl:when>
        <xsl:otherwise>
          <xsl:apply-templates select="current-group()" mode="merge"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each-group>    
  </xsl:function>
  
  <xsl:mode name="merge" on-no-match="shallow-copy"/>
  
  <xsl:template mode="merge" match="*">
    <xsl:copy>
      <xsl:sequence select="mf:merge(node())"/>
    </xsl:copy>
  </xsl:template>
  
  <xsl:template match="root">
    <secondwrap>
      <xsl:sequence select="mf:nest(*) => mf:merge()"/>
    </secondwrap>
  </xsl:template>

  <xsl:output method="xml" indent="yes"/>

  <xsl:mode on-no-match="shallow-copy"/>

</xsl:stylesheet>

I get e.g.

<secondwrap>
   <pgrp>
      <title>0</title>
      <p>
         <text>2</text>
         <quote>
            <qp>q2</qp>
            <qp>q3</qp>
         </quote>
      </p>
      <pgrp>
         <title>0</title>
         <p>
            <text>3</text>
         </p>
         <p>
            <text>4</text>
            <list>
               <qp>q4</qp>
               <qp>q9</qp>
            </list>
         </p>
         <p>
            <text>5</text>
            <quote>
               <qp>q5</qp>
               <list>
                  <ql>ql51</ql>
               </list>
            </quote>
            <p>
               <text>1a</text>
            </p>
            <p>
               <text>1aa</text>
            </p>
         </p>
      </pgrp>
   </pgrp>
</secondwrap>

which I think has the result structure you want, with the exception of the tempid attributes I hope you can add in where you need them.

As for an XSLT 2.0 version, I would hope that

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="2.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="#all"
  xmlns:mf="http://example.com/mf">

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

  
  <xsl:function name="mf:nest" as="node()*">
    <xsl:param name="nodes" as="node()*"/>
    <xsl:variable name="min-level" select="min($nodes/@level/xs:integer(.))"/>

    <xsl:for-each-group select="$nodes" group-starting-with="*[@level = $min-level]">
      <xsl:choose>
        <xsl:when test="self::*[@level = $min-level]">
          <xsl:element name="{@name}">
            <xsl:copy-of select="@level, node()"/>
            <xsl:sequence select="mf:nest(subsequence(current-group(), 2))"/>
          </xsl:element>          
        </xsl:when>
        <xsl:otherwise>
          <xsl:sequence select="mf:nest(current-group())"/>
        </xsl:otherwise>
      </xsl:choose>

    </xsl:for-each-group>
  </xsl:function>
  
  
  <xsl:function name="mf:merge" as="node()*">
    <xsl:param name="nodes" as="node()*"/>
    <xsl:for-each-group select="$nodes" group-adjacent="concat(node-name(), '|', @level, '|',  node-name() = (QName('', 'list'), QName('', 'quote')))">
      <xsl:choose>
        <xsl:when test="tokenize(trace(current-grouping-key()), '\|')[last()] = 'true'">
          <xsl:copy>
            <xsl:sequence select="@* except @level, mf:merge(current-group()/node())"/>
          </xsl:copy>
        </xsl:when>
        <xsl:otherwise>
          <xsl:apply-templates select="current-group()" mode="merge"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each-group>    
  </xsl:function>
  

  <xsl:template mode="merge" match="*">
    <xsl:copy>
      <xsl:sequence select="mf:merge(node())"/>
    </xsl:copy>
  </xsl:template>
  
  <xsl:template match="root">
    <secondwrap>
      <xsl:sequence select="mf:merge(mf:nest(*))"/>
    </secondwrap>
  </xsl:template>

  <xsl:output method="xml" indent="yes"/>

</xsl:stylesheet>

runs with an XSLT 2.0 processor and gives the same result as the XSLT 3 version run with an XSLT 3 processor.

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

9 Comments

Thanks Martin, this is the same output, i am looking for, but at this time we can't migrate to xslt 3.0
Most XPath 3(.1) expressions I have used can be easily rewritten as XPath 2.0 e.g. mf:nest(tail(current-group())) -> mf:nest(subsequence(current-group(), 2)), mf:nest(*) => mf:merge() -> mf:merge(mf:nest(*)). There are no composite grouping keys in XSLT 2.0 so you would need to string concatenate the three values of that composite grouping key I used e.g. group-adjacent="node-name(), @level, node-name() = (QName('', 'list'), QName('', 'quote'))" -> group-adjacent="concat(node-name(), '|', @level, '|', node-name() = (QName('', 'list'), QName('', 'quote')))".
For current-grouping-key()[3] use then tokenize(current-grouping-key(), '\|')[last()]. The last issue would then be to replace the declarative xsl:mode declarations by an explicit template <xsl:template match="@* | node()" mode="#all"><xsl:copy><xsl:apply-templates select="@*, node()" mode="#current"/></xsl:copy></xsl:template>.
@Rupesh_Kr, I think it all depends on your time and mindset and resources, in general I would say if you want to be more familiar with any programming language then you have to study it and practise it. As for resources specific to XSLT 2 and later, Michael Kay has written the "bible" for XSLT and XPath 2 ("XSLT 2.0 and XPath 2.0 Programmer's Reference 4th Edition" from Wrox), Ken Holman by now has made his book "Practical Transformation Using XSLT and XPath" freely available cranesoftwrights.com/training/index.htm#ptux.
Then are video courses: udemy.com/course/practical-transformation-using-xslt-and-xpath and pluralsight.com/courses/xslt-foundations-part1, to name two. In my view, it initially takes a bit of effort, but once you have worked your way into the official specifications like w3.org/TR/xpath-functions-31 or w3.org/TR/xquery-operators (for XPath 2.0), you can learn from them. And then it is up to you to practise.
|

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.