0

What XSLT will copy my XML document, modifying elements at a certain path by changing one attribute value and deleting another attribute, and get past the error message I am encountering?

My XSLT:

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

    <xsl:output method="xml" indent="no" encoding="UTF-8" />

    <xsl:template match="node()|@*">
      <xsl:copy>
        <xsl:apply-templates select="node()|@*"/>
      </xsl:copy>
    </xsl:template>
    
    <xsl:template match="/document/body/table/cell[@a2='delete me'][@a3='change me']" >
        <!-- Not trying to change @a3 yet, just delete @a2. -->
        <xsl:copy-of select="@*[name(.)!='a2']|node()" />
    </xsl:template>

</xsl:stylesheet>

My input XML document (simplified from a real LibreOffice document example):

<?xml version="1.0" encoding="UTF-8"?>
<document>
  <body>
    <otherstuff/>
    <table>
      <cell id="1" a1="keep me" a2="delete me" a3="change me">
        cell contents #1
      </cell>
      <cell id="2" a1="keep me too">
        cell contents #2
      </cell>
      <not_cell id="3" a1="keep me" a2="delete me" a3="change me">
        not_cell contents #3
      </not_cell>
    </table>
    <otherstuff/>
  </body>
</document>

What I want to change is that cell elements with attributes a2="delete me" and a3="change me" should change. I want to delete the attribute a2, and change the value of a3 so that a3="changed". All the rest of the cell element is unchanged. All other parts of the XML document are unchanged.

Expected output document: same as input, except for <cell id='1' …/>:

<cell id="1" a1="keep me" a3="changed">

Note that <not_cell id="3" a2="delete me" …/> is not touched, because it is not a cell element.

What I get when I apply this XSLT to this document:

  % xsltproc Error-Cannot-add-an-attribute-node.xslt Doc.xml 
  runtime error: file Error-Cannot-add-an-attribute-node.xslt line 49 element copy-of
  Cannot add an attribute node to a non-element node.

This seems to say that the copy-of expression is attempting to apply an attribute to an attribute or to element text. I can't follow the execution of XSLT well enough to understand where exactly it goes wrong. Can you explain that?

What is the right XSLT pattern (preferably XSLT-1.0) for making this transformation?

(It may be that the answer, XSLT: How to change an attribute value during xsl:copy?, will lead me to the right XSLT pattern, but I can't quite see it. And, it doesn't address why the XSLT I am trying leads to that error message.)

Jim DeLaHunt
  • 10,960
  • 3
  • 45
  • 74

2 Answers2

2

If you want to add attributes to an element, you must create the element first - for example:

<xsl:template match="cell">
    <xsl:copy>
        <xsl:copy-of select="@*[name(.)!='a2'] | node()" />
    </xsl:copy>
</xsl:template>

However, if you only want to remove (or modify) an attribute, you could simply match the attribute itself instead:

<xsl:template match="@a2[parent::cell]"/>
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • To clarify, the second example deletes an attribute by having nothing — especially, no `xsl:copy` or `xsl:apply-templates` — in its body? That is, it deletes by matching the attribute then doing nothing with it? – Jim DeLaHunt Dec 16 '20 at 20:49
  • 1
    Yes, that is correct. If you want to *modify* the attribute, you need to match it, (re)create it and fill it with new content. – michael.hor257k Dec 16 '20 at 21:19
1

Use mode to change multiple things within an element.

  <xsl:template match="cell">
    <xsl:copy>
      <!-- Use a mode to change multiple things within an element.  -->
      <xsl:apply-templates select="node()|@*" mode="cell"/>
    </xsl:copy>
  </xsl:template>

  <!--  *** Begin cell mode *** -->

  <!--  You need a mode identity.  -->
  <xsl:template match="node()|@*" mode="cell">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*" mode="cell"/>
    </xsl:copy>
  </xsl:template>

  <!-- use mode to delete me -->
  <xsl:template match="@a2" mode="cell"/>

  <!-- use mode to change me -->
  <xsl:template match="@a3" mode="cell">
    <xsl:attribute name="a3">
      <xsl:value-of select="'something new'"/>
    </xsl:attribute>    
  </xsl:template>

  <!-- Don't do anything for keep me.  It's handled my the mode identity. -->

  <!-- *** End cell mode *** -->

 <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>
John Ernst
  • 1,206
  • 1
  • 7
  • 11
  • Note: you can change text nodes using modes like this, but only if the text node is there. To be sure, after the apply-templates statement in the cell template, test for the text node. If it's not there, then add the text there. – John Ernst Dec 17 '20 at 13:24
  • 1
    Thank you! Clear explanation, and an example XSLT sheet which is easy to modify and re-use. – Jim DeLaHunt Dec 17 '20 at 23:01