Oliver Smith
Oliver Smith

Reputation: 63

XSLT: element that has no attributes and no children transform to parent attribute

The structure, names, values of the given .xml files is unknown.

For every not-root element that has simple structure(has no child-nodes, has no attributes, BUT has text and is not empty) transform it into parent's attribute.

I have .xml file:

<list>
   <worker>
      <name atr="ss">val1</name>
   </worker>
   <worker>
      <make1>val2</make1>
   </worker>
   <worker>
      <name>
        <make2>val3</make2>
      </name>
   </worker>
   <worker>
      <name>
        <doo atr="ss1">val4</doo>
        <make3></make3>
      </name>
   </worker>
</list>

And I want to get this:

<list>
   <worker>
      <name atr="ss">val1</name>
   </worker>
   <worker make1="val2"/>
   <worker>
      <name make2="val3"/>
   </worker>
   <worker>
      <name>
        <doo atr="ss1">val4</doo>
        <make3/>
      </name>
   </worker>
</list>

Here is my .xsl for now (doesn't work correctly):

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output indent="yes" method="xml"/>
    <xsl:template match="@*|node()">
      <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
      </xsl:copy>
    </xsl:template>
    <xsl:template match="//*[not(*|@*)]">
        <xsl:copy>
            <xsl:attribute name="{name()}">
                <xsl:value-of select="text()"/>
            </xsl:attribute>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

Upvotes: 2

Views: 1733

Answers (2)

michael.hor257k
michael.hor257k

Reputation: 117073

How about:

XSL 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="*"/>

<xsl:template match="*">
    <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:apply-templates select="*[not(*|@*)]" mode="attribute"/>
        <xsl:apply-templates select="*[*|@*] | text()" />
    </xsl:copy>
</xsl:template>

<xsl:template match="*" mode="attribute">
    <xsl:attribute name="{name()}">
        <xsl:value-of select="."/>
    </xsl:attribute>
</xsl:template>

</xsl:stylesheet>

Keep in mind that attributes must be created before child elements.


Added in response to change in requirements:

At some point, the amount of conditions becomes sufficient to justify writing them only once, and avoid duplicating them in the negative by defining the "other" node-set in terms of set-difference (i.e. non-intersection):

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="*"/>

<xsl:template match="*">
    <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:variable name="my-set" select="*[text() and not(*|@*)]" />
        <xsl:apply-templates select="$my-set" mode="attribute"/>
        <xsl:apply-templates select="node()[not(count(.|$my-set) = count($my-set)]" />
    </xsl:copy>
</xsl:template>

<xsl:template match="*" mode="attribute">
    <xsl:attribute name="{name()}">
        <xsl:value-of select="."/>
    </xsl:attribute>
</xsl:template>

</xsl:stylesheet>

Upvotes: 2

Eir&#237;kr &#218;tlendi
Eir&#237;kr &#218;tlendi

Reputation: 1180

Your code

You've got two templates:

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

<xsl:template match="*[not(*) and not(@*)]">
    <xsl:copy>
        <xsl:attribute name="{name()}">
            <xsl:value-of select="text()"/>
        </xsl:attribute>
    </xsl:copy>
</xsl:template>

Your output produces <worker><make1 make1="val2"/></worker> instead of <worker make1="val2"/>. This is because the outer <worker> element is processed by the top template, which just copies it and then passes the child along, which gets processed by the bottom template.

A working approach

The following works for me, and uses only one template.

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

    <xsl:output indent="yes"/>

    <xsl:strip-space elements="*"/>

    <xsl:template match="*">
        <xsl:copy>
            <xsl:copy-of select="@*"/>
            <!-- Capture any child elements with no attributes and no children. -->
            <xsl:for-each select="*[not(@*) and not(*)]">
                <xsl:attribute name="{name()}">
                    <xsl:value-of select="."/>
                </xsl:attribute>
            </xsl:for-each>
            <!-- Apply templates to **only** those children that have either
                attributes or children of their own, and to text. -->
            <xsl:apply-templates select="*[@* or *]|text()"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

The key difference: any element that meets your criteria -- has no child elements, has no attributes, only has text -- does not get processed by applying a template, and instead it gets processed within that for-each loop. So we never wind up with a copy of that element.

Update

We now have a clarified provision that empty elements that lack even text are to be kept as independent elements. So for a snippet like the following, with the empty EXTRA element:

<worker>
    <name>
        <doo atr="ss1">val4</doo>
        <make3>val4</make3>
        <EXTRA></EXTRA>
    </name>
</worker>

... we would want output like:

<worker>
    <name make3="val4">
        <doo atr="ss1">val4</doo>
        <EXTRA/>
    </name>
</worker>

... which maintains EXTRA as an independent element, and only attribute-ifies the make3 element.

This XSL should do the trick. This reworks the select statements from the code above.

<xsl:template match="*">
    <xsl:copy>
        <xsl:copy-of select="@*"/>
        <!-- Capture any child elements with no attributes and no children,
            and that also have text. -->
        <xsl:for-each select="*[not(@*) and not(*) and text()]">
            <xsl:attribute name="{name()}">
                <xsl:value-of select="."/>
            </xsl:attribute>
        </xsl:for-each>
        <!-- Apply templates to **only** those children that have no text, or
            that have attributes or children of their own, and also apply to text. -->
        <xsl:apply-templates select="*[@* or * or not(text())] | text()"/>
    </xsl:copy>
</xsl:template>

Upvotes: 1

Related Questions