I'm looking for a workaround on passing parameters to a template-match. I'm aware this isn't allowed within XPath, and therefore I'm looking for a 'plan B' solution.
This is what I wished would work :
Part 1 of xslt (2.0) :
<xsl:template match="/">
<xsl:for-each select="//Main/PageList/Page">
<xsl:result-document href="{#ID}.xml">
<Page ID="{#ID}">
<xsl:apply-templates select="node()">
<xsl:with-param name="theID" select="#ID"/>
</xsl:apply-templates>
</Page>
</xsl:result-document>
</xsl:for-each>
</xsl:template>
So fairly straightforward, don't bother about the tags used, what it does in essence is go through a node, and for each node it creates a single XML file. Each node starts with an ID, and it's this ID I'd like to make available for other templates. Unfortunately this works fine for named templates, but it doesn't work for matched ones (if I understood the theory correctly at least)
So below is what I'd like to see working :
<!-- identity template -->
<xsl:template match="node()|#*">
<xsl:param name="theID"/>
<xsl:copy>
<xsl:apply-templates select="node()|#*">
<xsl:with-param name="theID" select="$theID"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<!-- lots of templates here doing what I want them to do -->
<!-- one template creating an issue -->
<xsl:template match="#src">
<!-- would be nice to know the current ID, but unfortunately this one stays empty... -->
<xsl:param name="theID"/>
<!-- clean current attribute a bit -->
<xsl:variable name="S1" select="replace(.,'\.\.\/','')"/>
<xsl:attribute name="src">
<xsl:choose>
<xsl:when test="contains($S1,'common')">
<!-- just use current value, don't bother about current ID -->
<xsl:value-of select="$S1"/>
</xsl:when>
<xsl:otherwise>
<!-- use ID parameter -->
<xsl:value-of select="concat($theID,'_',$S1)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
</xsl:template>
Don't look at the code as such, it's just a smaller part of the whole file, but the essence is that I want to use the ID parameter from my first part (match="/") inside the other template (match="#src"), but this seems to be rather complex.
Am I missing something ? If I'm just having bad luck and it's not possible indeed, would anyone have an advice how I could proceed ?
Thanks in advance !
Your problem is here:
<xsl:apply-templates select="node()">
<xsl:with-param name="theID" select="#ID"/>
</xsl:apply-templates>
the select attribute specifies that the processing should continue on any children nodes of the current node.
However node() only selects children nodes (elements, text nodes, processing instructions and comments) -- not attributes.
Solution:
In order to directly cause processing of (all) attributes of the current node, use:
--
<xsl:apply-templates select="#*">
<xsl:with-param name="theID" select="#ID"/>
</xsl:apply-templates>
.2. In order to cause the processing only of the src attribute of the current node use:
--
<xsl:apply-templates select="#src">
<xsl:with-param name="theID" select="#ID"/>
</xsl:apply-templates>
.3. In order to process indirectly attributes tof the descendants of the current node, do:
--
<xsl:apply-templates select="node()|#*">
<xsl:with-param name="theID" select="#ID"/>
</xsl:apply-templates>
You also must ensure that any template that processes node() must have an xsl:param named theID and that it must pass this param in any xsl:apply-templates instruction.
In practice this means that you mus override all XSLT built-in templates, because they aren't aware of your xsl:param.
I think instead of
<xsl:apply-templates select="node()">
<xsl:with-param name="theID" select="#ID"/>
</xsl:apply-templates>
you rather want
<xsl:apply-templates select="node()">
<xsl:with-param name="theID" select="parent::Page/#ID"/>
</xsl:apply-templates>
assuming you want to pass on the ID attribute of the parent Page element.
On the other hand the select="node()" select any child nodes like child elements, child comment nodes, child text nodes, child processing instruction nodes, so I don't see why you later show a template matching an attribute node.
Related
I have two templates like this ...one to create normal package name and another to append "-Non" with the name if it passes certain conditions as shown
<xsl:template match="text()" name="checkForAgency">
<xsl:param name="Name"/>
<xsl:param name="Agency"/>
<xsl:if test="contains($Agency,'AG') ">
<xsl:value-of select="concat($Name,' - Non')"/>
</xsl:if>
</xsl:template>
<xsl:template match="text()" name="createPackageName">
<xsl:param name="Type"/>
<xsl:if test="contains($Type,'NEW')">
<xsl:value-of select="concat('New',' Goal')"/>
</xsl:if>
<xsl:if test="contains($Type,'AMD')">
<xsl:value-of select="concat('Amended',' Goal')"/>
</xsl:if>
<xsl:if test="contains($Type,'REN') or contains($Type,'ARN')">
<xsl:value-of select="concat('Renewal',' Goal')"/>
</xsl:if>
</xsl:template>
Calling these templates from
<xsl:variable name="NonAgencyPackageName">
<xsl:call-template name="createPackageName">
<xsl:with-param name="Type" select="ID"/>
</xsl:call-template>
</xsl:variable>
<xsl:element name="PackageName">
<xsl:call-template name="checkForAgency">
<xsl:with-param name="Name" select="$NonAgencyPackageName"/>
<xsl:with-param name="Agency" select="Output"/>
</xsl:call-template>
</xsl:element>
If ID has 'New', the package name should be New Goal and if the Output has 'AG', then name should be New Goal- Non.
Else name should be new goal. I want it two separate templates only. I am getting empty value now. Please help me in achieving this and how to correct it?
At a guess, the XPaths ID and Output are not selecting what you expect, so all conditionals are failing. Since you don't provide any XML input, it's impossible to be sure.
Ask yourself several questions:
When this code is evaluated, what is the current element?
Does this element have a child element named ID?
What is the string value of that element?
Does it have a child element named Output?
What is its string value?
The PackageName element you generate will be non-empty only if there is an Output child and its string value contains the sequence 'AG'. It will contain the string 'New Goal - Non' only if the Output child contains 'AG' and the ID child contains NEW. No power in creation could make it have the value 'New Goal', since the template CheckForAgency either produces a value ending with ' - Non' or no output at all. (You may wish to read about the choose/when instruction in XSLT.)
From my root template for unique account value, call goes to trans template were in the input xml I will have multiple nodes, my requirement is that once the call goes from root template to trans template, if a match of accountId found in between multiple elements, account details template is called only once, irrespective of other match found. I need a solution for above requirement.
input sample :
<elements><accountId>1</accountId></elements>
<elements><accountId>1</accountId></elements>
<elements><accountId>2</accountId></elements>
<elements><accountId>2</accountId></elements>
<elements><accountId>3</accountId></elements>
The below line should be wrapped under some code so its called only once
<xsl:call-template name="Account_details" />
Below is my complete code for xsl
<xsl:template match="/">
<xsl:variable name="unique-accounts" select="//*/*/*/accountId/text()[generate-id()=generate-id(key('account-by-id', .)[1])]"/>
<xsl:for-each select="$unique-accounts">
<xsl:variable name="currentValue" select="current()"/>
<xsl:apply-templates select="//trans">
<xsl:with-param name="passCurrentValue" select="$currentValue"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="trans">
<xsl:param name="passCurrentValue" />
<xsl:variable name="booleanValue" select="true()"/>
<xsl:for-each select="elements">
<xsl:if test="$passCurrentValue=/*/*/accountId">
<xsl:if test="$booleanValue">
<xsl:call-template name="Account_details" />
<xsl:variable name="booleanValue" select="false()"></xsl:variable>
</xsl:if>
</xsl:for-each>
</xsl:template>
<xsl:template name="Account_details">
.............
</xsl:template>
With the code
<xsl:template match="/">
<xsl:variable name="booleanCheck" select="true"></xsl:variable>
the variable booleanCheck is a node-set (XSLT 1.0) or sequence of elements named true below the document node /. So unless your XML input has a root element named true the value is an empty node-set respectively empty sequence. If you want to select a boolean value then use <xsl:variable name="booleanValue" select="true()"/>. See http://www.w3.org/TR/xpath/#function-true. Then you can test <xsl:if test="$booleanValue">. That should explain how to use boolean values, whether that fits into your context I am not sure.
I've been having a hell of a week trying to write XSLT code that can process XML documents that conform to the (very permissive) EAD standards.
The useful information in an EAD document is hard to locate precisely. Different EAD documents can place the same bit of information in entirely different parts of the data tree. In addition, within a single EAD document, the same tag can be used numerous times in different locations for different information. For an example of this, please see this SO post. This makes it hard to design a single XSLT file that properly handles these different files.
In general terms, the problem can be described as:
How do I select a specific EAD node which is in an unknown location,
Without accidentally selecting unwanted nodes that have the same name()?
I've finally put together the XSLT I needed and thought it would be best to drop a generic version of the code here so others can benifit from it or improve upon it.
I'd love to tag this question with an "EAD" tag, but I don't have enough rep. If anyone with the appropriate amount of rep thinks it would be useful, please do so.
First a quick description of the solution, followed by the code.
Check if this EAD document contains component (child) records (designated with a <cXX>). If not, we don't have to worry about duplicate EAD tags. The tags can still be burried under arbitrary wrappers. To find them, see step 3.
If child records exist, be careful to not process the <dsc> tag until other tags are processed. To find the other tags, see step 3, then step 4 to process child records.
Recurse through the various wrappers with a template that matches them and calls apply-template on any element node farther down the tree.
We are now processing a child record. Do this by repeating step 2 (carefully process all other tags before tackling the children of this child record), then step 4.
Here's the (generic version of the) XSLT code I came up with:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="ISO-8859-1" indent="yes"/>
<xsl:template match="/ead">
<records>
<xsl:if test="//dsc">
<!-- if there are <cXX> nodes, we'll handle the main record differently.
<cXX> nodes are always found in the 'dsc' node, which contains nothing else -->
<xsl:call-template name="carefully_process"/>
</xsl:if>
<xsl:if test="not(//dsc)">
<record>
<!-- Just process the existing nodes -->
<xsl:apply-templates select="*"/>
</record>
</xsl:if>
</records>
</xsl:template>
<xsl:template name="carefully_process">
<!-- first we'll process all the nodes for the main
record. Then we'll call the child records -->
<record>
<!-- have to be careful not to process //archdesc/dsc yet -->
<xsl:apply-templates select="*[not(self::archdesc)]"/>
<xsl:apply-templates select="archdesc/*[not(self::dsc)]"/>
<!-- Now we can close off the master record, -->
</record>
<!-- and process the child records -->
<xsl:apply-templates select="/ead/archdesc/dsc"/>
</xsl:template>
<xsl:template match="dsc">
<!-- Start processing the child records (we use for-each to get a good position() -->
<xsl:for-each select="*[starts-with(name(),'c0') or starts-with(name(),'c1') or name() = 'c']">
<xsl:apply-templates select=".">
<!-- we pass the unittitle and unitid of the master record, so that child
records can be linked to it. We pass the position of the child so that
a unitid can be created if it doesn't exist -->
<xsl:with-param name="partitle" select="normalize-space(/ead/archdesc/did/unittitle)"/>
<xsl:with-param name="parid" select="normalize-space(/ead/archdesc/did/unitid)"/>
<xsl:with-param name="pos" select="position()"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<!-- process child nodes -->
<xsl:template match="*[starts-with(name(),'c0') or starts-with(name(),'c1') or name() = 'c']" >
<xsl:param name="partitle"/>
<xsl:param name="parid"/>
<xsl:param name="pos"/>
<!-- start this child record -->
<record>
<!-- EAD does not require a unitid, but my code does.
If it doesn't exist, create it -->
<xsl:if test="not(./did/unitid)">
<atom name="unitid">
<xsl:value-of select="$parid"/><xsl:text>-</xsl:text><xsl:value-of select="$pos"/>
</atom>
</xsl:if>
<!-- get the level of this component -->
<atom name="eadlevel">
<xsl:value-of select="concat(translate(substring(#level,1,1),'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ'),substring(#level,2))"/>
</atom>
<!-- Do *something* to attach this record to it's parent.
Probably involves $partitle and $parid. For example: -->
<ref>
<atom name="unittitle"><xsl:value-of select="$partitle"/></atom>
<atom name="unitid"><xsl:value-of select="$parid"/></atom>
</ref>
<!-- now process all the other nodes -->
<xsl:apply-templates select="*[not(starts-with(name(),'c0') or starts-with(name(),'c1') or name() = 'c')]"/>
<!-- finish this child record -->
</record>
<!-- prep the variables we'll need for attaching any child records (<cXX+1>) to this record -->
<xsl:variable name="this_title">
<xsl:value-of select="normalize-space(./did/unittitle)"/>
</xsl:variable>
<xsl:variable name="this_id">
<xsl:if test="./did/unitid">
<xsl:value-of select="./did/unitid"/>
</xsl:if>
<xsl:if test="not(./did/unitid)">
<xsl:value-of select="$parid"/><xsl:text>-</xsl:text><xsl:value-of select="$pos"/>
</xsl:if>
</xsl:variable>
<!-- now process the children of this node -->
<xsl:for-each select="*[starts-with(name(),'c0') or starts-with(name(),'c1') or name() = 'c']">
<xsl:apply-templates select=".">
<xsl:with-param name="partitle" select="$this_title"/>
<xsl:with-param name="parid" select="$this_id"/>
<xsl:with-param name="pos" select="position()"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<!-- these are usually just wrappers. Go one level deeper -->
<xsl:template match="descgrp|eadheader|revisiondesc|filedesc|titlestmt|profiledesc|archdesc|archdescgrp|daogrp|langusage|did|frontmatter">
<xsl:apply-templates select="*"/>
</xsl:template>
<!-- below this point, add templates for processing specific EAD units
of information. For example, the template might look like
<xsl:template match="titleproper">
<atom name="titleproper">
<xsl:value-of select="normalize-space(.)"/>
</atom>
</xsl:template>
-->
<!-- instead of having a template for each EAD information unit, consider
a generic template that handles them all the same way. For example:
-->
<xsl:template match="*">
<atom>
<xsl:attribute name="name"><xsl:value-of select="name()"/></xsl:attribute>
<xsl:value-of select="normalize-space(.)"/>
</atom>
</xsl:template>
</xsl:stylesheet>
Apart from rewriting a lot of XSLT code (which I'm not going to do), is there a way to find the position of an element within its parent, when the context is arbitrarily set to something else? Here's an example:
<!-- Here are my records-->
<xsl:for-each select="/path/to/record">
<xsl:variable name="record" select="."/>
<!-- At this point, I could use position() -->
<!-- Set the context to the current record -->
<xsl:for-each select="$record">
<!-- At this point, position() is meaningless because it's always 1 -->
<xsl:call-template name="SomeTemplate"/>
</xsl:for-each>
</xsl:for-each>
<!-- This template expects the current context being set to a record -->
<xsl:template name="SomeTemplate">
<!-- it does stuff with the record's fields -->
<xsl:value-of select="SomeRecordField"/>
<!-- How to access the record's position in /path/to or in any other path? -->
</xsl:template>
NOTE: This is a simplified example. I have several constraints keeping me from implementing obvious solutions, such as passing new parameters to SomeTemplate, etc. I can really only modify the internals of SomeTemplate.
NOTE: I'm using Xalan 2.7.1 with EXSLT. So those tricks are available
Any ideas?
You could use
<xsl:value-of select="count(preceding-sibling::record)" />
or even, generically,
<xsl:value-of select="count(preceding-sibling::*[name() = name(current())])" />
Of course this approach will not work if you process a list of nodes that is not uniform, i.e.:
<xsl:apply-templates select="here/foo|/somewhere/else/bar" />
Position information is lost in such a case, unless you store it in a variable and pass that to the called template:
<xsl:variable name="pos" select="position()" />
<xsl:for-each select="$record">
<xsl:call-template name="SomeTemplate">
<xsl:with-param name="pos" select="$pos" />
</xsl:call-template>
</xsl:for-each>
but obviously that would mean some code rewriting, which I realize you want to avoid.
Final hint: position() does not tell you the position of the node within its parent. It tells you the position of the current node relative to the list of nodes you are processing right now.
If you only process (i.e. "apply templates to" or "loop over") nodes within one parent, this happens to be the same thing. If you don't, it's not.
Final hint #2: This
<xsl:for-each select="/path/to/record">
<xsl:variable name="record" select="."/>
<xsl:for-each select="$record">
<xsl:call-template name="SomeTemplate"/>
</xsl:for-each>
</xsl:for-each>
is is equivalent to this:
<xsl:for-each select="/path/to/record">
<xsl:call-template name="SomeTemplate"/>
</xsl:for-each>
but the latter works without destroying the meaning of position(). Calling a template does not change context, so . will refer to the correct node withing the called template.
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>