Richard
Richard

Reputation: 11

XSLT how to loop through each node and wrap it in a tag

Here is the XML:

<svg xmlns:xlink="http://www.w3.org/1999/xlink">
    <g>
        <text id="b376">
            <tspan x="59" y="156" font-size="13px" font-family="Arial">80</tspan>
        </text>
        <use xlink:href="#b376" fill="#000000"/>
        <text id="b374">
            <tspan x="59" y="204" font-size="13px" font-family="Arial">60</tspan>
        </text>
        <use xlink:href="#b374" fill="#000000"/>
        <defs>testDef</defs>
     </g>
</svg>

Here is my XSL input:

<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="g">
    <g>
      <xsl:apply-templates select="use|defs"/>
      <defs>
        <xsl:apply-templates select="*[name() != 'use' and name() != 'defs']"/>
      </defs>
    </g>
  </xsl:template>
</xsl:stylesheet>

I want to wrap all nodes in a defs tag EXCEPT use tags and defs tags. So the 2 text nodes would be wrapped in defs tags but defs and use would not.

Here is what I am getting

<?xml version="1.0"?>
<svg xmlns:xlink="http://www.w3.org/1999/xlink">
  <g>
    <use xlink:href="#b376" fill="#000000"/>
    <use xlink:href="#b374" fill="#000000"/>
    <defs>testDef</defs>
    <defs>
      <text id="b376">
        <tspan x="59" y="156" font-size="13px" font-family="Arial">80</tspan>
      </text>
      <text id="b374">
        <tspan x="59" y="204" font-size="13px" font-family="Arial">60</tspan>
      </text>
    </defs>
  </g>
</svg>

This is what I want:

<?xml version="1.0"?>
    <svg xmlns:xlink="http://www.w3.org/1999/xlink">
      <g>
        <use xlink:href="#b376" fill="#000000"/>
        <use xlink:href="#b374" fill="#000000"/>
        <defs>testDef</defs>
        <defs>
          <text id="b376">
            <tspan x="59" y="156" font-size="13px" font-family="Arial">80</tspan>
          </text>
        </defs>
         <defs>
          <text id="b374">
            <tspan x="59" y="204" font-size="13px" font-family="Arial">60</tspan>
          </text>
        </defs>
      </g>
    </svg>

I am using this online tool to test. Thanks!

Upvotes: 1

Views: 610

Answers (2)

Valdi_Bo
Valdi_Bo

Reputation: 30971

Your expected output shows that the order of element is important to you.

You want first use and defs tags and only after them all remaining elements, each wrapped in its own defs element.

In order to achieve this use the following script:

<?xml version="1.0" encoding="UTF-8" ?>
<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="g">
    <g>
      <xsl:apply-templates select="use|defs"/>
      <xsl:for-each select="*[name() != 'use' and name() != 'defs']">
        <defs>
          <xsl:apply-templates select="."/>
        </defs>
      </xsl:for-each>
    </g>
  </xsl:template>

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

As you see, I added a for-each loop with the select attribute copied from your example.

The content of this loop:

  • creates wrapper defs element,
  • and inside it "replays" the source element (the current element of the loop).

Upvotes: 1

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

Reputation: 1180

Your current output, where all of the <text> elements are wrapped in a single <defs> tag, is exactly what I would expect from reading your XSL code -- for every <g>, you have a single <defs> within which you handle all of the elements that are not <use> or <defs>:

<xsl:template match="g">
    <g>
        <xsl:apply-templates select="use|defs"/>

        <!-- This part here: -->
        <defs>
            <xsl:apply-templates select="*[name() != 'use' and name() != 'defs']"/>
        </defs>

    </g>
</xsl:template>

Since <xsl:apply-templates select="*[name() != 'use' and name() != 'defs']"/> is inside the literal <defs>, all of the non-use and non-refs elements are processed as a batch, within that single literal <defs> element.

You apparently want each non-use and non-defs element wrapped in its own <defs>. In this case, you need to move the literal <defs> to within a separate template that matches on non-use and non-defs elements.

A quick-and-dirty refactoring might look like this:

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

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

    <!-- We don't really need a specific template for <g>, so 
        we let the identity template handle that case. -->

    <!-- The only case where we need to define a different flow is
        for children of <g> elements, that aren't <use> or <defs>. -->
    <xsl:template match="*[name(..) = 'g'][name() != 'use' and name() != 'defs']">
        <!-- The template matches _each_ such element, so if we
            put the literal `<defs>` here, we get that `<defs>` as
            a wrapper for _each_ such element.  -->
        <defs>
            <xsl:copy-of select="."/>
        </defs>
    </xsl:template>

</xsl:stylesheet>

Note that this approach also leaves the original <use> and <defs> elements in the same location within the <g> parent.

Upvotes: 1

Related Questions