Kalina
Kalina

Reputation: 5594

Using xslt, how to add an xml node that starts before the first occurrence, and ends after the last occurrence of an item?

I have an xml that looks like this:

<ShoppingList>
    <MoneyIHaveToSpend>20.00</MoneyIHaveToSpend>
    <Item>
        <Name>Apples</Name>
        <Price>1.00</Price>
    </Item>
    <Item>
        <Name>Oranges</Name>
        <Price>1.00</Price>
    </Item>
    <AdditionalInfo>...</AdditionalInfo>
</ShoppingList>

I would like to wrap the "items" into a GroceryList, like so:

<ShoppingList>
    <MoneyIHaveToSpend>20.00</MoneyIHaveToSpend>
    <GroceryList>
        <Item>
            <Name>Apples</Name>
            <Price>1.00</Price>
        </Item>
        <Item>
            <Name>Oranges</Name>
            <Price>1.00</Price>
        </Item>
    </GroceryList>
    <AdditionalInfo>...</AdditionalInfo>
</ShoppingList>

There is only one sub-list inside of "ShoppingList". How would I go about doing this using xslt?

I tried to do something like the following, but splitting up <GroceryList> and </GroceryList> gives me compile errors. I realize that the rest of this is wrong anyway, but I can't even test it or mess around with it because of the compile error:

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

<xsl:template match="ShoppingList">
    <xsl:apply-templates select="/ShoppingList/Item"/>
    <xsl:if test="position() = 1">
        <GroceryList> <!-- Error: "XML element is not closed" -->
        <xsl:apply-templates/>
    </xsl:if>
    <xsl:if test="???">
        </GroceryList> <!-- Error: "No open tag found" -->
    </xsl:if>
</xsl:template>

Upvotes: 2

Views: 3067

Answers (3)

Michael Kay
Michael Kay

Reputation: 163322

Nodes don't "start" and "end", they are points in a tree with parents and children. You're trying to write XSLT as if it's outputting start and end tags as separable operations, rather than constructing a tree of nodes. Nodes on a tree are indivisible.

If your grouping problem is as simple as the example suggests, then the simplest solution will use a template like:

<xsl:template match="ShoppingList">
  <xsl:apply-templates select="MoneyIHaveToSpend"/>
  <GroceryList>
    <xsl:apply-templates select="Item"/>
  </GroceryList>
  <xsl:apply-templates select="AdditionalInfo"/>
</xsl:template>

When you read this, don't think of <GroceryList> and </GroceryList> as separate instructions. The template body contains a sequence of three instructions: apply-templates, GroceryList, and apply-templates, and the GroceryList instruction has content (a "sequence constructor") containing instructions to generate the content of the GroceryList node.

Upvotes: 1

fred02138
fred02138

Reputation: 3361

One other solution:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output encoding="utf-8" method="xml"/>

    <xsl:template match="ShoppingList">

        <ShoppingList>
            <xsl:variable name="firstItemName"><xsl:value-of select="./Item/Name/text()"/></xsl:variable>
            <xsl:apply-templates>
                <xsl:with-param name="firstItem"><xsl:value-of select="$firstItemName"/></xsl:with-param>
            </xsl:apply-templates>
        </ShoppingList>
    </xsl:template>

    <xsl:template match="Item">
        <xsl:param name="firstItem"/>

        <xsl:choose>
            <xsl:when test="./Name/text()=$firstItem">
                <xsl:element name="GroceryList">
                    <xsl:apply-templates select="../Item"/>
                </xsl:element>
            </xsl:when>
            <xsl:otherwise>
                <xsl:copy-of select="."/>
            </xsl:otherwise>
        </xsl:choose>

        </xsl:template>

    <xsl:template match="MoneyIHaveToSpend|AdditionalInfo|text()">
        <xsl:copy-of select="."/>
    </xsl:template>

</xsl:stylesheet>

Upvotes: 1

Daniel Haley
Daniel Haley

Reputation: 52858

Your XSLT has to be well-formed XML, so you can't start an element in one xsl:if and close it in another.

Like C. M. Sperberg-McQueen suggested, start with an identity transform and override from there.

Example (if the order of the ShoppingList children doesn't matter):

<xsl:stylesheet version="1.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="ShoppingList">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()[not(self::Item)]"/>
            <GroceryList>
                <xsl:apply-templates select="Item"/>
            </GroceryList>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

If the order does matter, you could do something like this:

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

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

    <xsl:template match="Item[1]">
        <GroceryList>
            <xsl:apply-templates select=".|following-sibling::Item" mode="keep"/>
        </GroceryList>
    </xsl:template>

    <xsl:template match="Item" mode="keep">
        <!--You could overwrite identity transform here.-->
        <xsl:call-template name="ident"/>
    </xsl:template>

    <xsl:template match="Item"/>

</xsl:stylesheet>

The above is pretty generic but could be simplified if you always know what children ShoppingList will have or if you don't need to modify any of the elements. (You could use xsl:copy-of instead of xsl:apply-templates.)

Upvotes: 3

Related Questions