Removing attributes from elements when they have a certain value - xslt

I'm writing a stylesheet to "canonicalize" XML schemas, in order to make comparing them easier. It will sort top-level elements by name, order attribute in a fixed order etc. This is what I have for attributes so far:
<xsl:template match="xsd:attribute">
<xsl:copy>
<xsl:copy-of select="#name"/>
<xsl:copy-of select="#type"/>
<xsl:copy-of select="#ref"/>
<xsl:copy-of select="#use"/>
<xsl:copy-of select="#default"/>
<xsl:apply-templates select="#*">
<xsl:sort select="name()"/>
</xsl:apply-templates>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:template>
Now I would like to also remove redundant attributes. For example, use = "optional" should be dropped, since optional is the default anyway.
So my question is: what would be the easiest way to enhance the code above in order to drop the use attribute when its value is optional?

#JohnBollinger and #MartinHonnen correctly pointed out that the #* condition will insert all the attributes, even the ones selected above. So the condition has to be refined. It was also pointed out that according to the specification the order of attributes cannot be ensured. The fact that they are being ordered is simply an artifact of how my processor (Saxon9-HE) happens to work. I'm fine with this though. Here is the solution I arrived at:
<xsl:template match="xsd:attribute">
<xsl:copy>
<xsl:copy-of select="#name" />
<xsl:copy-of select="#type" />
<xsl:copy-of select="#ref" />
<xsl:copy-of select="#use[string() != 'optional']" />
<xsl:copy-of select="#default" />
<xsl:apply-templates select="#*[name(.) != 'use']">
<xsl:sort select="name()" />
</xsl:apply-templates>
<xsl:apply-templates select="node()" />
</xsl:copy>
</xsl:template>
I didn't add the other attribute names to the negative filter on #* since Saxon won't duplicate attributes and leaves them in the required order even if it's re-inserted by the #* clause. So this does fulfill my present needs with the least effort, even though it's not a generic solution (which I don't actually need).

Related

apply-templates ambiguous match warning

I get a warning
ambiguous rule match
from the processor for the templates copyReference and the identity transform.
<xsl:template name="processChildNodes">
<xsl:param name="El"/>
<xsl:for-each select="$El/node()">
<xsl:choose>
<xsl:when test="#sameas">
<xsl:apply-templates mode="copyReference" select="id(substring-after(#sameas, '#'))"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="." />
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
<xsl:template match="*" mode="copyReference" name="copyReference">
<xsl:copy>
<xsl:apply-templates select="#* except (#stem.dir, #stem.sameas)"/>
</xsl:copy>
</xsl:template>
<xsl:template match="node() | #*" mode="#all">
<xsl:copy>
<xsl:apply-templates select="node() | #*"/>
</xsl:copy>
</xsl:template>
Here is an xml snippet:
<layer>
<note oct="3" pname="b" stem.dir="up" stem.sameas="#note_17544b" xml:id="note_17544"/>
</layer>
<layer>
<note oct="4" pname="d" xml:id="note_17592"/>
<note sameas="#note_17544" xml:id="note_17544b"/>
</layer>
What I want to do is just to copy the node which is referenced from the #sameas-attribute without #stem.dir and #stem.sameas. There could be different nodes local-names() on which the will be applied on. So I'd rather not specify the node names in the #match-attribute of the copyReference template. I thought if I pass the nodes I need with #select-attribute and also add #mode it will match only what I need. And actually it works, but as I'm getting the warning something should be wrong.
node() is short-hand for *|text()|comment()|processing-instruction() and so because the identity template has mode="#all" on it, it will match any element with the same priority as the "copyReference" template when the "copyReference" mode is used.
The solution depends on what else your stylesheet does, but there are a number of possibilities
Remove mode="#all" from the identity template (this would only work if there were not other modes in your XSLT)
Add priority="2" to your "copyReference" template, so that when the mode "copyReference" was used, your specific template would get priority.
Change <xsl:apply-templates mode="copyReference"... to be an xsl:for-each instead and do away with the template match.
Change the "copyReference" template to explicitly match "note" rather than "*" as this would then give it a higher priority (but this obviously assumes you would only ever need to match note elements)

XSLT: create node if does not exists

I know similar questions are already there but none of them seem to work for me.
So shortly, I have XML file with tag "Lokal" that in most cases does not appear but it should. Not making things easier: I also need to change a name of "Lokal" to let's say "Lokal_test". My goal is modify node name(if exists) or create it and rename (if does not exists).
Data from XML will be imported to MS Access data so they need to match perfectly with table...
Sample XML:
<Dane>
<InformacjeOWpisie>
<DaneAdresowe>
<AdresGlownegoMiejscaWykonywaniaDzialalnosci>
<Budynek>3a</Budynek>
<Wojewodztwo>podlaskie</Wojewodztwo>
</AdresGlownegoMiejscaWykonywaniaDzialalnosci>
</DaneAdresowe>
</InformacjeOWpisie>
<InformacjeOWpisie>
<DaneAdresowe>
<AdresGlownegoMiejscaWykonywaniaDzialalnosci>
<Budynek>8r</Budynek>
<Lokal>2</Lokal>
<Wojewodztwo>mazowieckie</Wojewodztwo>
</AdresGlownegoMiejscaWykonywaniaDzialalnosci>
</DaneAdresowe>
</InformacjeOWpisie>
</Dane>
Desired output:
<Dane>
<InformacjeOWpisie>
<DaneAdresowe>
<AdresGlownegoMiejscaWykonywaniaDzialalnosci>
<Budynek>3a</Budynek>
<Lokal_test/>
<Wojewodztwo>podlaskie</Wojewodztwo>
</AdresGlownegoMiejscaWykonywaniaDzialalnosci>
</DaneAdresowe>
</InformacjeOWpisie>
<InformacjeOWpisie>
<DaneAdresowe>
<AdresGlownegoMiejscaWykonywaniaDzialalnosci>
<Budynek>8r</Budynek>
<Lokal_test>2</Lokal_test>
<Wojewodztwo>mazowieckie</Wojewodztwo>
</AdresGlownegoMiejscaWykonywaniaDzialalnosci>
</DaneAdresowe>
</InformacjeOWpisie>
</Dane>
This question(XSLT: create node if not exists seemed to be the awnser to my problems but when trying to use it does not work.
Not sure why?
<xsl:template match="InformacjeOWpisie/DaneAdresowe/AdresGlownegoMiejscaWykonywaniaDzialalnosci/Lokal">
<Lokal_test>
<xsl:apply-templates select="#*|node()" />
</Lokal_test>
</xsl:template>
EDIT:
When I get rid of parent Lokal_test dissapears. I use below code to say "bye bye" to parent:
<xsl:template match="InformacjeOWpisie/DaneAdresowe/AdresGlownegoMiejscaWykonywaniaDzialalnosci">
<xsl:apply-templates select="#*|node()" />
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="AdresGlownegoMiejscaWykonywaniaDzialalnosci/Budynek">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
<xsl:choose>
<xsl:when test="exists(following-sibling::Lokal)">
<Lokal_test>
<xsl:value-of select="following-sibling::Lokal"/>
</Lokal_test>
</xsl:when>
<xsl:when test="not(following-sibling::Lokal)">
<xsl:element name="Lokal_test"/>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template match="Lokal"/>
You approach was right, but incomplete. You only created the new Local_test element.
So try these two templates in combination with the indentity template:
<!-- Handles the replacement of the 'Lokal' element -->
<xsl:template match="AdresGlownegoMiejscaWykonywaniaDzialalnosci/Lokal">
<Lokal_test>
<xsl:apply-templates select="node()|#*" />
</Lokal_test>
</xsl:template>
<!-- Creates a new 'Lokal_test' element if no 'Lokal' element exists -->
<xsl:template match="AdresGlownegoMiejscaWykonywaniaDzialalnosci[not(Lokal)]">
<xsl:copy>
<xsl:apply-templates select="node()/following-sibling::Wojewodztwo/preceding-sibling::*|#*" /> <!-- Copy nodes before 'Wojewodztwo' -->
<Lokal_test />
<xsl:apply-templates select="Wojewodztwo|Wojewodztwo/following-sibling::*|#*" /> <!-- Copy nodes after 'Wojewodztwo' (including) -->
</xsl:copy>
</xsl:template>
The second template puts the Lokal_test element before the Wojewodztwo element and copies the surrounding nodes.

How can I insert a Diazo theme parameter into some theme's class attribute?

I want to replace a class tag attribute in my static theme on-the-fly, based on a theme parameter.
I tried this:
<replace attributes="class" css:theme=".conteudo">conteudo-$section</replace>
And this:
<replace css:theme=".conteudo">
<xsl:attribute name="class">conteudo-$section</xsl:attribute>
<xsl:value-of select="."/>
</replace>
And even this:
<xsl:template match="//div[contains(concat(' ', normalize-space(#class), ' '), ' conteudo ')]">
<xsl:attribute name="class">
<xsl:value-of select="substring((body/#class), 'section-', 0)" />
</xsl:attribute>
</xsl:template>
Since I also have other rules referencing .conteudo element, it'd be also nice to get to know best practices on how to deal with those (after the desired transformation occurs), ie:
<replace
css:content-children="#portal-column-content"
css:theme-children=".conteudo" />
You can't reference a variable just anywhere, but need to do so from an XPath expression.
You can avoid interfering with your replacement of the children nodes by inserting the attribute "before" the children.
Here's what I would try:
<before css:theme-children=".conteudo">
<xsl:attribute name="class">conteudo-<xsl:value-of select="$section" /></xsl:attribute>
</before>
As David said, is the right way to do it.
And to set the attributes before the child elements, you have to do that:
<xsl:template match="//*[contains(#class, 'conteudo')]">
<xsl:copy>
<xsl:attribute name="class"><xsl:value-of select="$section" /></xsl:attribute>
<xsl:apply-templates select="#*[not(name()='class')]|node()" />
</xsl:copy>
</xsl:template>
<!--Identity template copies content forward -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>

How to make uppercase while doing <xsl:copy-of select="./*"/>

I have to make uppercase of all node values inside a parent node while copying the whole section.
For Example:
ATPM/37 zATP - Miscellaneous Exceptions Blank text box without number 1 Said
Change to:
ATPM/37 ZATP - MISCELLANEOUS EXCEPTIONS BLANK TEXT BOX WITHOUT NUMBER 1 SAID
In XSLT 2.0, try adding
<xsl:template match="text()">
<xsl:value-of select="upper-case(.)"/>
</xsl:template>
In XSLT 1.0, use
<xsl:template match="text()">
<xsl:value-of select="translate(., $smallcase, $uppercase)" />
</xsl:template>
<xsl:variable name="smallcase" select="'abcdefghijklmnopqrstuvwxyz'" />
<xsl:variable name="uppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" />
instead
The xsl:copy-of instruction always does an exact copy; you can't use it to make a copy-with-changes. For that, use the identity template to copy things that you want to copy exactly, and a different template for things that you want to change. Thus:
<xsl:template match="*">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()">
<xsl:value-of select="upper-case(.)"/>
</xsl:template>

Sort attributes in specific order for output

How do you write element attributes in a specific order without writing it explicitly?
Consider:
<xsl:template match="Element/#1|#2|#3|#4">
<xsl:if test="string(.)">
<span>
<xsl:value-of select="."/><br/>
</span>
</xsl:if>
</xsl:template>
The attributes should appear in the order 1, 2, 3, 4. Unfortunately, you can't guarantee the order of attributes in XML, it could be <Element 2="2" 4="4" 3="3" 1="1">
So the template above will produce the following:
<span>2</span>
<span>4</span>
<span>3</span>
<span>1</span>
Ideally I don't want to test each attribute if it has got a value. I was wondering if I can somehow set an order of my display? Or will I need to do it explicitly and repeating the if test as in:
<xsl:template match="Element">
<xsl:if test="string(./#1)>
<span>
<xsl:value-of select="./#1"/><br/>
</span>
</xsl:if>
...
<xsl:if test="string(./#4)>
<span>
<xsl:value-of select="./#4"/><br/>
</span>
</xsl:if>
</xsl:template>
What can be done in this case?
In an earlier question you seemed to use XSLT 2.0 so I hope this time too an XSLT 2.0 solution is possible.
The order is not determined in the match pattern of a template, rather it is determined when you do xsl:apply-templates. So (with XSLT 2.0) you can simply write a sequence of the attributes in the order you want e.g. <xsl:apply-templates select="#att2, #att1, #att3"/> will process the attributes in that order.
XSLT 1.0 doesn't have sequences, only node-sets. To produce the same result, use xsl:apply-templates in the required order, such as:
<xsl:apply-templates select="#att2"/>
<xsl:apply-templates select="#att1"/>
<xsl:apply-templates select="#att3"/>
Do not produce XML that relies on the order of the attributes. This is very brittle and I would consider it bad style, to say the least. XML was not designed in that way; <elem a="1" b="2" /> and <elem a="1" b="2" /> are explicitly equivalent.
If you want ordered output, order your output (instead of relying on ordered input).
Furthermore, match="Element/#1|#2|#3|#4" is not equivalent to match="Element/#1|Element/#2|Element/#3|Element/#4", but I'm sure you mean the latter.
That being said, you can do:
<xsl:template match="Element/#1|Element/#2|Element/#3|Element/#4">
<xsl:if test="string(.)">
<span>
<xsl:value-of select="."/><br/>
</span>
</xsl:if>
</xsl:template>
<xsl:template match="Element">
<xsl:apply-templates select="#1|#2|#3|#4">
<!-- order your output... -->
<xsl:sort select="name()" />
</xsl:apply-templates>
</xsl:template>
EDIT: I'll take it as read that #1 etc are just examples, because names cannot actually start with a number in XML.
I'd use xsl:sort on the local-name of the attribute to get the result you want. I'd also use a different mode so the results don't get called by accident somewhere else.
<xsl:stylesheet version="1.1" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="Element">
<xsl:apply-templates select="#*" mode="sorted">
<xsl:sort select="local-name()" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Element/#a|#b|#c|#d" mode="sorted">
<xsl:if test="string(.)">
<span>
<xsl:value-of select="."/><br/>
</span>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
The clue was is the answer by Martin Honnen
To copy attributes and conditionally add a new attribute to the end of the list of attributes.
Add rel="noopener noreferrer" to all external links.
<xsl:template match="a">
<xsl:copy>
<xsl:if test="starts-with(./#href,'http')">
<xsl:apply-templates select="./#*"/>
<!-- Insert rel as last node -->
<xsl:attribute name="rel">noopener noreferrer</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="a/#href|a/#target|a/#rel">
<!--
Allowed attribute on anchor
-->
<xsl:attribute name="{name()}">
<xsl:value-of select="."></xsl:value-of>
</xsl:attribute>
</xsl:template>
You can also specify the attribute sequence by calling apply templates with each select in the order you want.
<xsl:template match="a">
<xsl:copy>
<xsl:if test="starts-with(./#href,'http')">
<xsl:apply-templates select="./#id"/>
<xsl:apply-templates select="./#href"/>
<xsl:apply-templates select="./#target"/>
<!-- Insert rel as last node -->
<xsl:attribute name="rel">noopener noreferrer</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>