0

I'd like to duplicate some nodes in my XML file. This file is meant to be sent towards a printing engine. It considers a purchase order with some lines, and for each line, a number of labels needs to be printed. The number is dependent on the number of items that will be received for that purchase order. Therefore I'd like to duplicate the XML node for that specific line n times, n equal to the number of copies specified in the specific line.

My source XML:

<?xml version="1.0" encoding="utf-8"?>
<report>
    <header>
        <purchaseorder>KER123456</purchaseorder>
    </header>
    <lines>
        <line>
            <copies>2</copies>
            <item>item1</item>
        </line>
        <line>
            <copies>3</copies>
            <item>item2</item>
        </line>
    </lines>
</report>

The requested result:

<report>
    <header>
        <purchaseorder>KER123456</purchaseorder>
    </header>
    <lines>
        <line>
            <item>item1</item>
        </line>
        <line>
            <item>item1</item>
        </line>
        <line>
            <item>item2</item>
        </line>
        <line>
            <item>item2</item>
        </line>
        <line>
            <item>item2</item>
        </line>
    </lines>
</report>

I already fiddled with an XSLT example I found on Stack Overflow: Duplicate element x number of times with XSLT

But unfortunately I couldn't get it to work.

My XSLT experiment:

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

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

    <xsl:template match="copies">
        <xsl:variable name="copies" select="../copies"/>
        <xsl:copy-of select="."/>
        <xsl:for-each select="1 to .">
            <xsl:apply-templates select="$copies" mode="replicate"/>
        </xsl:for-each>
    </xsl:template>

    <xsl:template match="line" mode="replicate">
        <line>
            <xsl:apply-templates select="@* except @name|node()"/>
        </line>
    </xsl:template>
    <xsl:template match="line"/>

</xsl:stylesheet>

2 Answers2

0

First you need a well-formed XML input, with a single root element.

Then you can do simply:

XSLT 2.0

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

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

<xsl:template match="line">
    <xsl:variable name="item" select="item"/>
    <xsl:for-each select="1 to copies">
        <line>
            <xsl:copy-of select="$item"/>
        </line>
    </xsl:for-each>
</xsl:template>

</xsl:stylesheet>

Demo: https://xsltfiddle.liberty-development.net/eiorv1b


Added:

To do the same in XSLT 1.0:

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

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

<xsl:template match="line" name="generate-lines">
    <xsl:param name="n" select="copies"/>
    <xsl:if test="$n > 0">
        <xsl:copy>
            <xsl:copy-of select="item"/>
        </xsl:copy>
        <!-- recursive call -->
        <xsl:call-template name="generate-lines">
            <xsl:with-param name="n" select="$n - 1"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>

</xsl:stylesheet>
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • Woops, I indeed created a faulty XML, sorry. I stripped down the original XML file for easier reading purposes on this forum, but I stripped a bit too much. I've edited my post, thank you for your effort! – Richard de Hond Nov 26 '21 at 15:31
  • XSLT template works like a charm! Unfortunately the solution where I need to implement this XSLT in, doesn't support XSLT 2.0. So I need to convert the xsl:for-each loop to something else... I'll manage! – Richard de Hond Nov 26 '21 at 17:52
  • I have added an XSLT 1.0 solution to my answer. BTW, `xsl:for-each` is not a "loop". A recursive named template, like the one above, is. – michael.hor257k Nov 26 '21 at 18:13
0

XSLT 1.0

Here's my solution for XSLT 1.0. In XSLT 1.0, you cannot make use of a xsl:for-each loop with a range in the select parameter. Instead, I made a recursive call to a template.

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

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

<xsl:template match="line">
    <xsl:call-template name="block-generator">
        <xsl:with-param name="N" select="copies"/>
    </xsl:call-template>
</xsl:template>

<xsl:template name="block-generator">
    <xsl:param name="N"/>
    <xsl:param name="i" select="0"/>
    <xsl:if test="$N > $i">
        <line>
            <xsl:copy-of select="item"/>
        </line>
        <xsl:call-template name="block-generator">
            <xsl:with-param name="N" select="$N"/>
            <xsl:with-param name="i" select="$i + 1"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>

</xsl:stylesheet>