I have an xml document following this overall pattern:
<A b="c" d="e" f="g" h="i">
<!-- plenty of children -->
</A>
I would like to copy the A
node with only some of it's attributes:
<A b="c" f="g">
<!-- some of the children -->
</A>
Other answers here have come close to solving my challenge but have not quite been enough:
- This answer gave me a solution that would work but would be very long: https://stackoverflow.com/a/672962/145978
- so I could go with
<xsl:copy-of select="@*[(name()!='d') or (name()!='h']"/>
but my actual attribute list is very long.
- I did try finding a 'is-a-member-of-this-list'-type function but got lost rather quickly.
- This answer seemed to discuss a Whitelist but I am apparently not smart enough to be able to apply it to attribute selection: https://stackoverflow.com/a/5790798/145978
Please help
The whitelist solution that you linked uses an embedded document containing the list of elements that should be preserved. You can have a similar one for your attributes:
<myns:whitelist>
<keep>b</keep>
<keep>f</keep>
</myns:whitelist>
It can be loaded and parsed using the document('')
function, and you can store it in a variable to make it easier to refer to it:
<xsl:variable name="keep" select="document('')/*/myns:whitelist/keep"/>
Now the $keep
variable contains the names of all the attributes in the list. The asterisk represents the <xsl:stylesheet>
element, since the argument passed to document()
is an empty string, which causes it to load from the current document.
Then you can test if the names of arbitrary atrributes match any one that is in the $keep
node-set:
@*[name()=$keep]
The others you copy using an identity transformation.
Here is a full stylesheet for the example you provided:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns:myns="myns">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<myns:whitelist>
<keep>b</keep>
<keep>f</keep>
</myns:whitelist>
<xsl:variable name="keep" select="document('')/*/myns:whitelist/keep"/>
<xsl:template match="A">
<xsl:copy>
<xsl:apply-templates select="node()|@*[name()=$keep]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Generally, if you want to drop something from the input, write an empty template for what you want to remove:
<!-- drop every attribute of <A> ... -->
<xsl:template match="A/@*" />
and another, non-empty template for what you want to keep:
<!-- ... except @b and @f -->
<xsl:template match="A/@b | A/@f">
<xsl:copy-of select="." />
</xsl:template>
and then simply apply templates normally:
<xsl:template match="A">
<xsl:copy>
<xsl:apply-templates select="@*" />
<!-- other output -->
</xsl:copy>
</xsl:template>
That's all, this does the right thing already.
Hint: If you have an identity template in place in your stylesheet and no other changes to <A>
are necessary then you don't even need that third template.
If you're using XSLT 2.0, you can use a sequence. If you put it in an xsl:param
instead of an xsl:variable
, you can define the whitelist at runtime (if you want).
Example:
XSLT 2.0
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="whitelist" select="('b','f')"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="A">
<xsl:copy>
<xsl:apply-templates select="@*[name()=$whitelist]|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>