I'm using the following to match all <section>s with a revision attribute set. <section>can appear at many different levels of the document tree, always contained within <chapter>s.
<xsl:for-each select="//section[#revision]">
<!-- Do one thing if this is the first section
matched in this chapter -->
<!-- Do something else if this section is in the same
chapter as the last section matched -->
</xsl:for-each>
As the comments say, I need to make each for-each iteration aware of the chapter to which the previous matched section belonged. I know that <xsl:variable>s are actually static once set, and that <xsl:param> only applies to calling templates.
This being Docbook, I can retreive a section's chapter number with:
<xsl:apply-templates select="ancestor::chapter[1]" mode="label.markup" />
but I think it can be done with purely XPath.
Any ideas? Thanks!
Not sure if I unterstood your requirements 100%, but…
<xsl:variable name="sections" select="//section[#revision]" />
<xsl:for-each select="$sections">
<xsl:variable name="ThisPos" select="position()" />
<xsl:variable name="PrevMatchedSection" select="$sections[$ThisPos - 1]" />
<xsl:choose>
<xsl:when test="
not($PrevMatchedSection)
or
generate-id($PrevMatchedSection/ancestor::chapter[1])
!=
generate-id(ancestor::chapter[1])
">
<!-- Do one thing if this is the first section
matched in this chapter -->
</xsl:when>
<xsl:otherwise>
<!-- Do something else if this section is in the same
chapter as the last section matched -->
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
However, I suspect this whole thing can be solved more elegantly with a <xsl:template> / <xsl:apply-templates> approach. But without seeing your input and expected output this is hard to say.
position() will return your position within the current for-each iteration. The first iteration, IIRC, will return 0, so testing for "position()=0" will tell you if you are in the first iteration.
Related
I have a piece of an XSLT stylesheet that works as expected using xsltproc but produces a different output in my actual application, where the transform is applied via org.jdom.transform.XSLTransformer (jdom 1.0), I believe using Xalan.
Stylesheet snippet (this is part of a larger template that starts like this: <xsl:template match="/dspace:dim[#dspaceType='ITEM']">):
<xsl:if test="//dspace:field[#mdschema='dc' and #element='rights']">
<rightsList>
<xsl:if test="//dspace:field[#mdschema='dc' and #element='rights' and not(#qualifier) and #language='*']">
<rights>
<xsl:if test="//dspace:field[#mdschema='dc' and #element='rights' and #qualifier='uri' and #language='*']">
<xsl:attribute name="rightsUri">
<xsl:value-of select="//dspace:field[#mdschema='dc' and #element='rights' and #qualifier='uri' and #language='*']"/>
</xsl:attribute>
</xsl:if>
<xsl:value-of select="//dspace:field[#mdschema='dc' and #element='rights' and not(#qualifier) and #language='*']" />
</rights>
</xsl:if>
<xsl:apply-templates select="//dspace:field[#mdschema='dc' and #element='rights' and not(#language='*')]" />
</rightsList>
</xsl:if>
and
<xsl:template match="//dspace:field[#mdschema='dc' and #element='rights' and not(#language='*')]">
<rights><xsl:value-of select="." /></rights>
</xsl:template>
XML snippet:
<dim:dim dspaceType="ITEM" xmlns:dim="http://www.dspace.org/xmlns/dspace/dim">
<dim:field element="rights" language="en_NZ" mdschema="dc">Actual text redacted</dim:field>
<dim:field element="rights" language="*" mdschema="dc">Attribution 3.0 New Zealand</dim:field>
<dim:field element="rights" qualifier="uri" language="*" mdschema="dc">http://creativecommons.org/licenses/by/3.0/nz/</dim:field>
</dim:dim>
With xsltproc, this produces
<rightsList>
<rights rightsUri="http://creativecommons.org/licenses/by/3.0/nz/">Attribution 3.0 New Zealand</rights>
<rights>Actual text redacted</rights>
</rightsList>
In my application, this produces
<rightsList>
<rights>Actual text redacted</rights>
<rights>Attribution 3.0 New Zealand</rights>
<rights>http://creativecommons.org/licenses/by/3.0/nz/</rights>
</rightsList>
So to me it looks like the not(#qualifier) bit doesn't work using jdom.
I'd appreciate any insight into what's going on here and how I might change the stylesheet to get the same result in my application that I currently get via xsltproc.
Edited to add: just in case it makes any difference, the stylesheet starts out as
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dspace="http://www.dspace.org/xmlns/dspace/dim"
xmlns:exslt="http://exslt.org/common"
xmlns="http://datacite.org/schema/kernel-3"
extension-element-prefixes="exslt"
exclude-result-prefixes="exslt"
version="1.0">
and also includes this template:
<!-- Don't copy everything by default! -->
<xsl:template match="#* | text()" />
See my answer below the XML structure is actually different from what I thought it was, so the problem wasn't in the XSL after all.
Apart from solving your original problem, let's have a quick look at how to reorganize your code.
You use a lot of //foo expressions. Starting an expression with //foo means "search the whole document, at any level, for the element with the name foo". Apart from this being a potentially expensive operation, this often has unwanted side effects and makes your code hard to read, because it requires you to specify each element uniquely, leading to a lot of duplicated code.
You also use a lot of xsl:if, but in XSLT, it is hardly ever necessary to use if-statements (an exception in XSLT 1.0 and 2.0 being when you deal with something other than nodes). In almost all cases, you can replace an xsl:if with a simple xsl:apply-templates.
That said, let's have a look how we can rewrite your code to get the same effect and have less chance for error:
<xsl:if test="//dspace:field[#mdschema='dc' and #element='rights']">
<rightsList>
.....
Is similar to having a matching template as follows (assuming you have a throw-away template for uninteresting nodes):
<xsl:template match="dspace:dim[dspace:field[#mdschema='dc' and #element='rights']]">
<rightsList>
This says: if you encounter a dim element with any field element that has those properties set, then output <rightsList>.
Then you have:
<xsl:if test="//dspace:field[#mdschema='dc' and #element='rights' and not(#qualifier) and #language='*']">
<rights>
Which is precisely equivalent to the following apply-template expression (assuming a matching template with it):
<xsl:apply-templates select="dspace:field[#mdschema='dc' and #element='rights' and not(#qualifier) and #language='*']" />
Here we find that a little bit below that, we have an almost equivalent expression, this time with not(#language='*'). So let's see if we can get rid of those duplicate expressions altogether.
First, let's go back a bit and have a look at what you were doing:
If anywhere any "dc" and "rights", then create a <rightsList>
If anywhere any of these have do not have a qualifier but have a language "*", create <rights>
Inside this, create an attribute rightsUri if anywhere any qualifier has value "uri" and language "*", set its value to the first such you find
After this <rights> element (there can be at most one of them in your current structure), create a list of <rights> for each field element with language "*"
If this is correct, then this can be rewritten as follows:
<xsl:template match="dspace:dim[dspace:field[#mdschema='dc' and #element='rights']]">
<xsl:variable name="adjusted">
<xsl:copy-of select="dspace:field[#mdschema='dc' and #element='rights']"/>
</xsl:variable>
<rightsList>
<xsl:apply-templates select="exsl:node-set($adjusted)/*[not(#qualifier) and #language='*'][1]" mode="noquali"/>
<xsl:apply-templates select="exsl:node-set($adjusted)/*[not(#language='*')]" />
</rightsList>
</xsl:template>
<xsl:template match="dspace:field" mode="noquali">
<rights>
<xsl:apply-templates select="/dspace:field[#qualifier='uri' and #language='*'][1]" mode="uri"/>
<xsl:value-of select="."/>
</rights>
</xsl:template>
<xsl:template match="dspace:field" mode="uri">
<xsl:attribute name="rightsUri" select="." />
</xsl:template>
<!-- matching anything else -->
<xsl:template match="dspace:field">
<rights><xsl:value-of select="." /></rights>
</xsl:template>
The exsl:node-set function is supported by just about every XSLT 1.0 processor, just add the namespace xmlns:exsl="http://exslt.org/common" to your xsl:stylesheet declaration.
Note that I added a few times [1] to the select-expressions. While you don't do that in your code, your current code has the same effect, but if you use apply-templates, if you encounter multiple matches, you have to specify that you are only interested in the first match.
I think your code can be further simplified, but I wanted to make sure that the logic remains exactly the same. As you can see, the end result is without any //. However, you do see one /, which is now pointing to the root of the node-set, which conveniently only has the nodes you are interested in: the ones with schema "dc" and "rights" element attributes, so we do not have to repeat that expression over and over again.
You may try this rewrite and see if it helps with your current bug, otherwise I'll gladly to help you further.
Edit
After your edit, your original context item will have been dspace:dim already. If you don't mind always outputting <rightsList> (even if it ends up empty), you can simply replace my first template match pattern above with your existing dspace:dim pattern.
Duh. Forest/trees indeed. Even though the language attribute is called "language" pretty much everywhere else in the application (see also, the XML snippet I gave), it is actually called "lang" in the XML that my stylesheet operates on - I finally gave in and used this answer to be sure what the XML structure is. Surprise!
Anyway, I followed the advice Abel gave in his answer in part and simplified the templates for this particular case quite a bit. I now just have
<xsl:if test="dspace:field[#mdschema='dc' and #element='rights']">
<rightsList>
<xsl:apply-templates select="dspace:field[#mdschema='dc' and #element='rights']"/>
</rightsList>
</xsl:if>
in the big template, plus a couple of custom ones:
<xsl:template match="dspace:field[#mdschema='dc' and #element='rights']">
<xsl:choose>
<xsl:when test="#qualifier='uri'"/>
<xsl:otherwise>
<rights>
<xsl:if test="#lang='*'">
<xsl:apply-templates select="//dspace:field[#mdschema='dc' and #element='rights' and #qualifier='uri' and #lang='*'][1]" mode="rightsURI"/>
</xsl:if>
<xsl:value-of select="."/>
</rights>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="dspace:field[#mdschema='dc' and #element='rights' and #qualifier='uri' and #lang='*']" mode="rightsURI">
<xsl:attribute name="rightsURI"><xsl:value-of select="."/></xsl:attribute>
</xsl:template>
While searching for some profiling tools for XSLT, I came across this post. Since a lot of people there suggested to just post the code and offered to give feedback on that, I was wondering if anyone could give me some feedback on mine. I tried this (http://www.saxonica.com/documentation/#!using-xsl/performanceanalysis), but the output html is not very detailed.
I'm new to XSLT and usually work with python/perl, where regex support is much better (however, I won't rule out the possibility that it's just my very basic understanding of XSLT). For the purpose of this project however, I had to work with XSLT. It could be that I'm forcing it to do things in a very unnatural way. Any comments -on performance in particular, but anything else is also welcome, as I'd like to learn- are welcome!
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:template name="my_terms">
<xsl:variable name="excludes" select="not (codeblock or draft-comment or filepath or shortdesc or uicontrol or varname)"/>
<!-- leftover example of how to work with excludes var -->
<!--<xsl:if test=".//*[$excludes]/text()[contains(.,'access management console')]"><li class="prodterm"><b>PB QA:access management console should be "AppCenter"</b></li></xsl:if>-->
<!-- Loop through all sentences and check for deprecated stuff -->
<xsl:for-each select=".//*[$excludes]/text()">
<xsl:variable name="sentenceList" select="tokenize(., '[\.!\?:;]\s+')"/>
<xsl:variable name="segment" select="."/>
<!-- main sentence loop -->
<xsl:for-each select="$sentenceList">
<xsl:variable name="sentence" select="."/>
<!-- very rudimentary sentence length check -->
<xsl:if test="count(tokenize(., '\W+')) > 30"> <li class="prodterm"><b>Sentence too long:</b> <xsl:value-of select="."/></li></xsl:if>
<!-- efforts to flag the shady case of the gerund -->
<xsl:if test="matches(., '\w+ \w+ing (the|a)')">
<!-- some extra checks to weed out the false positives -->
<xsl:if test="not(matches(., '\b(on|about|for|before|while|when|after|by|a|the|an|some|all|every) \w+ing (the|a)', '!i')) and not(matches(., 'during'))">
<li class="prodterm"><b>Possible unclear usage of gerund. If so, consider rewriting:</b> <xsl:value-of select="."/></li>
</xsl:if>
</xsl:if>
<!-- comma's after certain starting phrases -->
<xsl:if test="matches(., '^\s*Therefore[^,]')"><li class="prodterm"><b>Use a comma after starting a sentence with 'Therefore':</b> <xsl:value-of select="."/></li></xsl:if>
<xsl:if test="matches(., '^\s*(If you|Before|When)[^,]+$')"><li class="prodterm"><b>Use a comma after starting a sentence with 'Before', 'If you' or 'When':</b> <xsl:value-of select="."/></li></xsl:if>
<!-- experimenting with phrasal verbs (if there are a lot of verbs in phrasalVerbs.xml, it will be better to have this as the main loop (and do it outside the sentence loop)) -->
<xsl:for-each select="document('phrasalVerbs.xml')/verbs/verb[matches($sentence, concat('.* ', ./#text, ' .*'))]">
<xsl:variable name="verbPart" select="."/>
<xsl:for-each select="$verbPart/particles/particle/#text[matches($sentence, .) and not(matches($sentence, concat($verbPart/#text, ' ', .)))]">
<xsl:variable name="particle" select="."/>
<li class="prodterm"><b>Separated phrasal verb found in:</b> <xsl:value-of select="$sentence"/></li>
</xsl:for-each>
</xsl:for-each>
<!-- checking if conditionals (should be followed by then) -->
<xsl:if test="matches($sentence, '^\s*If\b', '!i') and not(matches($sentence, '\bthen\b', '!i'))"><li class="prodterm"><b>Conditional If found, but no then:</b> <xsl:value-of select="."/></li></xsl:if>
<!-- very dodgy way of detecting passive voice -->
<!--<xsl:if test="matches($sentence, '\b(are|can be|must be) \w+ed\b', '!i')"><li class="prodterm"><b>PB QA:Possible passive voice. If so, consider using active voice for:</b> <xsl:value-of select="."/></li></xsl:if>-->
<xsl:for-each select='document("generalDeprecatedTermsAndPhrases.xml")/terms/dt'>
<xsl:variable name="pattern" select="./#pattern"/>
<xsl:variable name="message" select="./#message"/>
<xsl:variable name="regexFlag" select="./#regexFlag"/>
<!-- <xsl:if test="matches($sentence, $pattern, $regexFlag)"> -->
<xsl:if test="matches($sentence, concat('(^|\W)', $pattern, '($|\W)'), $regexFlag)"> <!-- This is the work around for not being able to use \b when variable is passed on inside matches() -->
<li class="prodterm"><b><xsl:value-of select="$message"/> in: </b> <xsl:value-of select="$sentence"/> </li>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
To get an idea, the stripped down version of my "generalDeprecatedTermsAndPhrases.xml" looks like this:
<dt pattern='to be able to' message="Use 'to' instead of 'to be able to'" regexFlag="i"></dt>
</terms>
The reason that Saxon's profile is not very detailed is that your code is so monolithic: it's all in one great big template rule.
However, being monolithic isn't by itself the cause of any performance problems.
First observation is a functionality problem: your variable
<xsl:variable name="excludes" select="not (codeblock or draft-comment or filepath or shortdesc or uicontrol or varname)"/>
doesn't do what you think. It's evaluated with the root document node as the context item, and its value is a boolean which is true if the outermost element has a name which is not one of those listed. So I think your xsl:for-each that uses [$excludes] as a predicate is applying to all elements, whereas I suspect you intended it to apply to selected elements. I don't know how much that affects the performance.
The main influence on performance will be the cost of evaluating the regular expressions. The best way to find out which ones are causing the problem is to measure the impact of removing them one-by-one. When you've isolated the problem, there may be a way of rewriting the regular expression to make it perform better (e.g. by making it avoid backtracking).
I have searched, but might have missed something obvious just because I don't know what to search for. And I found it hard to explain my question as a simple question, so let me explain: I am using the following code (XSLT 1.0 & XPath) to check if the parent to this node belongs to the last grand-parent node or not:
<xsl:when test="count(parent::*/preceding-sibling::*)+1 = count(parent::*/parent::*/*)">
It works exactly like I want. What I would like though is to make it more general in one way or the other, to make it work with even more parent nodes. I can add another template match and add another test:
<xsl:when test ="count(parent::*/parent::*/preceding-sibling::*)+1 = count(parent::*/parent::*/*)">
Is there a way to add "parent::*/" in a recursive template loop instead of creating a lot of specific template matches? Or should I work out a better XPath code altogether?
Please note: I want to do a check for every level of parent nodes. Is the parent the last of the parents, is the grand-parent the last of the grand-parents, etc.
For clarity's sake, I use it like this:
<xsl:choose>
<xsl:when test="count(parent::*/preceding-sibling::*)+1 = count(parent::*/parent::*/*)">
<!-- show image A -->
</xsl:when>
<xsl:otherwise>
<!-- show image B -->
</xsl:otherwise>
</xsl:choose>
<xsl:when test="count(parent::*/preceding-sibling::*)+1 = count(parent::*/parent::*/*)">
can be simplified to:
<xsl:when test="not(../following-sibling::*)">
In plain English "my parent does not have a following element sibling".
That would be easily modifiable to:
<xsl:when test="not(../../following-sibling::*)">
In plain English "my parent's parent does not have a following element sibling". Etc.
To check all ancestors at the same time:
<xsl:when test="not(ancestor::*/following-sibling::*)">
In plain English "none of my ancestors has a following element sibling".
To check parent and grandparent at the same time:
<xsl:when test="not(ancestor::*[position() <= 2]/following-sibling::*)">
In plain English "none of my two closest ancestors has a following element sibling".
EDIT To check all ancestors individually, either use a recursive template (advantage: the position of the inner <xsl:apply-templates> determines if you effectively go up or down the list of ancestors):
<xsl:template match="*" mode="line-img">
<xsl:if test="following-sibling::*">
<!-- show image A -->
</xsl:if>
<xsl:if test="not(following-sibling::*)">
<!-- show image B -->
</xsl:if>
<xsl:apply-templates select=".." mode="line-img" />
</xsl:template>
<!-- and later... -->
<xsl:apply-templates select=".." mode="line-img" />
...or a simple for-each loop (always works in document order):
<xsl:for-each select="ancestor::*">
<xsl:if test="following-sibling::*">
<!-- show image A -->
</xsl:if>
<xsl:if test="not(following-sibling::*)">
<!-- show image B -->
</xsl:if>
</xsl:for-each>
...or, to be completely idiomatic (always works in document order):
<xsl:template match="*" mode="line-img">
<xsl:if test="following-sibling::*">
<!-- show image A -->
</xsl:if>
<xsl:if test="not(following-sibling::*)">
<!-- show image B -->
</xsl:if>
</xsl:template>
<!-- and later... -->
<xsl:apply-templates select="ancestor::*" mode="line-img" />
I've the below sample xml tree.
-root
-para
-page
-footnum
-para
-footnum
-para
-footnum
-para
-page
-footnum
I want to apply templates on the first footnum whose preceding is page. From inside the footnum template. here below para there will be even more nodesbut here i want to apply-template only to the first footnum followed by page,, whether it is in same para or different.
The code i tried is as below.
<xsl:template match="footnote" mode="footnote">
<xsl:variable name="rt">
<xsl:value-of select="//page/#num/following::footnote[1]/#num"/>
</xsl:variable>
<xsl:value-of select="$rt"/>
<xsl:if test="contains($rt,#num)">
<xsl:variable name="op"><</xsl:variable>
<xsl:variable name="apos">'</xsl:variable>
<xsl:variable name="cl">></xsl:variable>
<xsl:value-of select="concat($op,'?pb label=',$apos,./#num,$apos,'?',$cl)"/>
</xsl:if>
<div class="tr_footnote">
<div class="footnote">
<sup>
<a>
<xsl:attribute name="name"><xsl:text>ftn.</xsl:text><xsl:value-of select="#num"/></xsl:attribute>
<xsl:attribute name="href"><xsl:text>#f</xsl:text><xsl:value-of select="#num"/></xsl:attribute>
<xsl:attribute name="class"><xsl:text>tr_ftn</xsl:text></xsl:attribute>
<xsl:value-of select="#num"/>
</a>
</sup>
<xsl:apply-templates/>
</div>
</div>
</xsl:template>
Update:
here when i'm running this template, it is giving me before which footnotes the number is to be placed, but here i'm getting the footnote number, but not the pagenumber. please let me know how i can retrieve the page number.
here it is applying templates for all the footnums. please let me know where am i going wrong.
Thanks
You are already in the footnote context. To get the first preceding page node, you must use
<xsl:variable name="rt" select="preceding-sibling::*[1][local-name()='page']/#num"/>
If (as I think you are saying) you want to call apply-templates to the first footnum with a preceding page element, then your first problem is that the apply-templates instruction in your sample code applies to the page element, not to the footnum element.
Your second problem (whether the error is in your code or in your problem description) is that you say you wish to do this work only in some cases (it's not clear to me what conditions you have in mind -- one footnum per document, or once per page element on the first footnum following that page element, or something else), but you don't seem to have any test in your code that tests for the condition you have in mind. The test in your conditional is preceding::page[1], which is synonymous (as a Boolean) with preceding::page, and which excludes all those footnum elements which appear before any page element in the document, and includes all the others.
Assuming that your page elements mark page breaks, and your footnum elements mark footnote numbers, and what you want is to do something special for the first footnote (if any) on each page [if that's not right, you'll have to make the appropriate adjustments to the solution], then you need to find an XPath expression that returns true for the first footnote on each page, and not for the others.
What is the crucial characteristic here of the first footnum on each page? That there is no other footnum element between it and the immediately preceding page. So we could write (not tested):
<xsl:variable name="mypage" as="element(page)?"
select="preceding::page[1]"/>
<xsl:variable name="preceding-footnum-on-same-page"
as="element(footnum)?"
select="preceding::footnum[preceding::page[. is $mypage]]"/>
<xsl:if test="$mypage and not($preceding-footnum-on-same-page)">
... do your stuff here ...
</xsl:if>
Another way to formulate it is: the current node is the first footnum on the current page if, when we find the current page and then find its first footnum, it's the current node.
<xsl:if test=". is ./preceding::page[1]/following::footnum[1]">
...
</xsl:if>
I hope this helps.
This question is very similar to XSL store node-set in variable. The major difference is that if XPath does not find a node which matches the filter, I would like to return the first unfiltered result.
The code I have here works, but I feel like it is a hack and not good XSL style. In this case, each chapter node is identified by a string id. The variable showChapter is the string identifying a chapter. If no chapter is found with this id attribute, I want to return the first chapter.
Relevant code:
<xsl:param name="showChapter" />
<!-- if $showChapter does not match any chapter id attribute,
set validShowChapter to id of first chapter.
-->
<xsl:variable name="validShowChapter">
<xsl:choose>
<xsl:when test="/book/chapter[#id=string($showChapter)][position()=1]">
<xsl:value-of select="$showChapter" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="/book/chapter[position()=1]/#id" />
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- I want $chapter to be a valid node-set so I can use it in
XPath select statements in my templates
-->
<xsl:variable
name="chapter"
select="/book/chapter[#id=string($validShowChapter)][position()=1]"
>
Is this approach as poor of a hack as I think it is, and if so could you point me to a better solution? I am using XSLT 1.0 processed by PHP5's XSLTProcessor, but XSLT 2.0 solutions are welcome.
The following should work. A lot of the usage of position() and string() in your example were unneeded, btw:
<xsl:param name="showChapter" />
<xsl:variable name="foundChapter" select="/book/chapter[#id = $showChapter]" />
<!-- Will select either the first chapter in $foundChapter, or
the first chapter available if $foundChapter is empty -->
<xsl:variable name="chapter"
select="($foundChapter | /book/chapter[not($foundChapter)])[1]" />