How to avoid creating duplicated footnotes? - xslt

I am parsing an XML that has two footnotes pointing to the same source, as in reference [1] in this example:
This is my parsing code:
<xsl:template match="//sup[#class='reference']/a">
<xsl:variable name="cite_number">
<xsl:value-of select="substring-after(substring-before(./text(),']'),'[')"/> <!-- to remove the [ ] characters -->
</xsl:variable>
<fo:footnote>
<fo:inline font-weight="bold"><fo:inline font-size="6pt" vertical-align="super"><xsl:value-of select="$cite_number"/></fo:inline></fo:inline>
<fo:footnote-body>
<xsl:variable name="cite_id"> <!-- variable to find the content of the cite -->
<xsl:value-of select="substring-after(#href,'#')"/> <!-- to remove the # character -->
</xsl:variable>
<fo:block color="#999999">
<xsl:value-of select="$cite_number"/>
<xsl:text>. </xsl:text>
<xsl:apply-templates select="ancestor::subchapter[#lang]//li[#id=$cite_id]/span[#class='reference-text']"/>
</fo:block>
</fo:footnote-body>
</fo:footnote>
</xsl:template>
The variable cite_number is the number of the cite (1, 2, 3, etc.). If there are two cites pointing at the same source, as shown in the image, the footnote is created twice.
What would be the way of having only one footnote for multiple repeated cites?

I solved it by adding a conditional that prints either the full footnote or just the superindex. In my case, which is MediaWiki pages, I check the id attribute of the parent to determine if there is only one instance of the cite not(contains(../#id,':')) or if there are more than one, and then look for the first one (contains(../#id,'-0')):
<xsl:template match="//sup[#class='reference']/a">
<xsl:variable name="cite_number">
<xsl:value-of select="substring-after(substring-before(./text(),']'),'[')"/> <!-- to remove the [ ] characters -->
</xsl:variable>
<xsl:choose>
<xsl:when test="not(contains(../#id,':')) or (contains(../#id,'-0'))]">
<fo:footnote>
<fo:inline font-weight="bold"><fo:inline font-size="6pt" vertical-align="super"><xsl:value-of select="$cite_number"/></fo:inline></fo:inline>
<fo:footnote-body>
<!-- footnote-body here -->
</fo:footnote-body>
</fo:footnote>
</xsl:when>
<xsl:otherwise>
<fo:inline font-weight="bold"><fo:inline font-size="6pt" vertical-align="super"><xsl:value-of select="$cite_number"/></fo:inline></fo:inline>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

Related

confusing error thown while trying to match templates

I've the below XML line of code.
<entry colname="col3" align="left" valign="top"><para>grandchild, cousin, <content-style font-style="italic">etc</content-style>., shall be described as “lawful” and “one of the next-of-kin” or “only next-of-kin”.</para></entry>
and below XSL
<xsl:template match="entry" name="entry">
<xsl:choose>
<xsl:when test="./#namest">
<xsl:variable name="namest" select="#namest" />
<xsl:variable name="nameend" select="#nameend" />
<xsl:variable name="namestPos" select="count(ancestor::tgroup/colspec[#colname=$namest]/preceding-sibling::colspec)" />
<xsl:variable name="nameendPos" select="count(ancestor::tgroup/colspec[#colname=$nameend]/preceding-sibling::colspec)" />
<td colspan="{$nameendPos - $namestPos + 1}" align="{#align}">
<xsl:apply-templates select="child::node()[not(self::page)]" />
</td>
</xsl:when>
<xsl:otherwise>
<td>
<xsl:if test="./#morerows">
<xsl:attribute name="rowspan">
<xsl:value-of select="number(./#morerows)+1" />
</xsl:attribute>
</xsl:if>
<xsl:if test="./#align">
<xsl:attribute name="align">
<xsl:value-of select="#align" />
</xsl:attribute>
</xsl:if>
<xsl:if test="./#valign">
<xsl:attribute name="valign">
<xsl:value-of select="#valign" />
</xsl:attribute>
</xsl:if>
<xsl:for-each select="para">
<div class="para">
<xsl:choose>
<xsl:when test="../#colname='col3' and contains(./text(),'.')">
<xsl:variable name="strl">
<xsl:value-of select="fn:string-length(fn:substring-before(.,'.'))" />
</xsl:variable>
<xsl:choose>
<xsl:when test="$strl < '6'">
<a href="{concat('er:#SCP_ORD_',//chapter/#num,'/','P',translate(./text(),'.','-'))}">
<xsl:value-of select="./text()" />
</a>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates />
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates />
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:for-each>
</td>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
and when i run this on my XML in the above mentioned line(the XML given in the sample), it is throwing me an error. and the error is as below.
Wrong occurrence to match required sequence type - Details: - XPTY0004: The supplied sequence ('2' item(s)) has the wrong occurrence to match the sequence type xs:string ('zero or one')
here what i actually was trying to achieve is, i have another table(which is basically a TOC), where in there is some linking needed, and below is such sample entries.
<entry colname="col3" align="right" valign="top"><para>A.1</para></entry>
<entry colname="col3" align="right" valign="top"><para>A.2</para></entry>
here i'm searching if the colname is col3 and if this has a . in it, and the above two cases mentioned are passing and are getting linked successfully, where in the case mentioned in the top is throwing the error, can anyone please suggest some better method to differentiate these two cases, and i use XSLT 2.0.
Thanks
The problem is
contains(./text(),'.')
./text() is not "the text of the current element" but rather the sequence of all text node children of the current element. In the case of
<para>grandchild, cousin, <content-style font-style="italic">etc</content-style>., shall be described as “lawful” and “one of the next-of-kin” or “only next-of-kin”.</para>
there are two such nodes, one containing everything between the <para> and <content-style> tags ("grandchild, cousin, " including the trailing space) and the other containing everything between the </content-style> and the </para>. But the contains function expects its first argument to be a single string, not a sequence of two nodes.
Instead of testing ./text() you probably just need to test .:
contains(., '.')
which is interpreted as the string value of the whole para element, i.e. a single string consisting of the concatenation of all the descendant text nodes (so the whole text "grandchild, cousin, etc., shall be described...").

Get conditional value from previous node

I have this XSLT 2.0 template:
<xsl:template match="footnote">
<xsl:variable name = "string" select="./text()"/>
<xsl:variable name = "bool">
<xsl:choose>
<xsl:when test="$string = preceding::footnote/text()">
<xsl:text>false</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>true</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:if test="$bool = 'true'">
<xsl:variable name="footnoteCount">
<xsl:call-template name="getItemNumber">
<xsl:with-param name="node" select="."/>
</xsl:call-template>
</xsl:variable>
<!-- DO XSL-FO TRANSFORMATION STUFF-->
</xsl:if>
<xsl:if test="$bool = 'false'">
<xsl:variable name = "footnoteCount">
<xsl:if test="$string = preceding::footnote/text()">
<xsl:value-of select="preceding::footnote/$footnoteCount"/>
</xsl:if>
</xsl:variable>
<!--DO XSL-FO TRANSFORMATION STUFF-->
</xsl:if>
</xsl:template>
Edited sample XML. I'd like to transform this:
<footnote>Foo bar</footnote>
<footnote>Bar foo</footnote>
<footnote>Foo bar</footnote>
<footnote>Foo bar</footnote>
<footnote>Bar</footnote>
<footnote>Foo</footnote>
Into this:
<footnote>Foo bar</footnote>
<footnote>Bar foo</footnote>
<footnote>Bar</footnote>
<footnote>Foo</footnote
And then stylise it using XSL-FO. The aim of this styling is that text in the main body will have a numbered reference, which is represented by $footnotecount, to the footnote which is then rendered at the bottom of the page. I need to transform the document so that duplicate footnates are only rendered once and that the number reference ($footnoteCount) is the same for each duplicate.
So what I'm trying to do with this template is:
Determine whether a footnote element with the current node's text already exists.
If it doesn't exist (i.e, '$bool' is 'true') , find the previous footnote's number, increment it (this is done in the 'getItemNumber' template) and create a 'new' footnote. If it does exist ($bool is 'false'), get the $footnoteCount variable of the node that the text matches and use it for the current node.
It's the scenario in which the footnote already exists that I'm having trouble with. I have no idea how to get the $footnoteCount variable from a previous, specific node dependent on whether than node meets a certain criteria (whether its text is the same as the $string variable in the current node). It's being made more difficult by the fact that the $footnoteCount variable only exists conditionally (even if in practice it will always exist since $bool has to be either true or false).
Does anyone have advice on what to do here?
This looks like a grouping problem, and in XSLT2.0 you can make use of the xsl:for-each-group element to get the distinct elements
<xsl:for-each-group select="footnote" group-by=".">
I think you need a different approach to do your numbering. Firstly you could create a variable to hold a 'look-up' of footnote descriptions and their index
<xsl:variable name="footnotes">
<xsl:for-each-group select="//footnote" group-by=".">
<footnote id="{position()}">
<xsl:value-of select="."/>
</footnote>
</xsl:for-each-group>
</xsl:variable>
This means the footnotes variable contains the following elements
<footnote id="1">Foo bar</footnote>
<footnote id="2">Bar foo</footnote>
<footnote id="3">Bar</footnote>
<footnote id="4">Foo</footnote>
Then, to replace your existing footnote elements with numeric references, you would have a template like this
<xsl:template match="footnote">
<footnote>
<xsl:value-of select="$footnotes/footnote[. = current()]/#id"/>
</footnote>
</xsl:template>
Try the following XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:variable name="footnotes">
<xsl:for-each-group select="//footnote" group-by=".">
<footnote id="{position()}">
<xsl:value-of select="."/>
</footnote>
</xsl:for-each-group>
</xsl:variable>
<xsl:template match="/*">
<xsl:apply-templates select="footnote"/>
Footnotes
<xsl:copy-of select="$footnotes"/>
</xsl:template>
<xsl:template match="footnote">
<footnote>
<xsl:value-of select="$footnotes/footnote[. = current()]/#id"/>
</footnote>
</xsl:template>
</xsl:stylesheet>
When applied to your sample XML (assuming a root element), the following is output
<footnote>1</footnote>
<footnote>2</footnote>
<footnote>1</footnote>
<footnote>1</footnote>
<footnote>3</footnote>
<footnote>4</footnote>
Footnotes
<footnote id="1">Foo bar</footnote>
<footnote id="2">Bar foo</footnote>
<footnote id="3">Bar</footnote>
<footnote id="4">Foo</footnote>
As well as failing to use xsl:for-each-group, there are many other things wrong with your code. For example, take this:
<xsl:variable name = "bool">
<xsl:choose>
<xsl:when test="$string = preceding::footnote/text()">
<xsl:text>false</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>true</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
You can write that as
<xsl:variable name="bool" as="xs:boolean()"
select="$string = preceding::footnote"/>
Then you do this:
<xsl:if test="$bool = 'true'">
<xsl:variable name="footnoteCount">
<xsl:call-template name="getItemNumber">
<xsl:with-param name="node" select="."/>
</xsl:call-template>
</xsl:variable>
</xsl:if>
which is completely useless: the variable goes out of scope as soon as it is declared, so it can never be referenced.
You need to do some background reading about XSLT, there are a lot of ideas you haven't yet grasped.

XSLT tokenize nodeset

I'm trying to create a variable that stores the value of an input string (TypeInput) in init cap form. This new variable will be used in different places in my stylesheet. I created a template that I call to convert the input string to init cap form. However, when I run the stylesheet, the resulting variable TypeInputInitCap shows up as NodeSet(1) in the debugger and doesn't output text in my output. Any ideas why? See sample below.
<xsl:variable name="TypeInputInitCap">
<xsl:call-template name="ConvertToInitCapString">
<xsl:with-param name="str" select="$TypeInput"></xsl:with-param>
</xsl:call-template>
</xsl:variable>
<xsl:template name="ConvertToInitCapString">
<xsl:param name="str"></xsl:param>
<!-- Extract each component of the name delimited by . -->
<xsl:variable name="TokenNodeSet">
<xsl:for-each select="tokenize($str, '.')">
<!-- Init cap each component -->
<xsl:value-of select="concat(upper-case(substring(.,1,1)), lower-case(substring(.,2)))"></xsl:value-of>
</xsl:for-each>
</xsl:variable>
<xsl:for-each select="$TokenNodeSet">
<xsl:value-of select="."></xsl:value-of>
<xsl:if test="not(last())">
<xsl:text>.</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
I think that the problem is that the $TokenNodeSet variable contains just a single string, and so the second for-each just loops once.
What about doing this instead:
<xsl:template name="ConvertToInitCapString">
<xsl:param name="str"></xsl:param>
<xsl:for-each select="tokenize($str, '\.')">
<xsl:value-of select="concat(upper-case(substring(.,1,1)), lower-case(substring(.,2)))"/>
<xsl:if test="not(last())">
<xsl:text>.</xsl:text>
</xsl:if>
</xsl:for-each>
EDIT
Fixed the tokenize() call above as suggested by LarsH in the comments
I would replace
<xsl:variable name="TokenNodeSet">
<xsl:for-each select="tokenize($str, '.')">
<!-- Init cap each component -->
<xsl:value-of select="concat(upper-case(substring(.,1,1)), lower-case(substring(.,2)))"></xsl:value-of>
</xsl:for-each>
</xsl:variable>
with
<xsl:variable name="TokenNodeSet" select="for $token in tokenize($str, '\.') return concat(upper-case(substring($token,1,1)), lower-case(substring($token,2)))" />
or better yet with
<xsl:variable name="TokenNodeSet" as="xs:string*" select="for $token in tokenize($str, '\.') return concat(upper-case(substring($token,1,1)), lower-case(substring($token,2)))" />
or finally as it is XSLT 2.0 where there are no nodesets I would rename the variable as e.g.
<xsl:variable name="TokenSequence" as="xs:string*" select="for $token in tokenize($str, '\.') return concat(upper-case(substring($token,1,1)), lower-case(substring($token,2)))" />
Thanks all for your help. The second part in my template was not necessary, so I'm now using this version, which works. It re-adds a '.' character between the tokens. (I didn't use the short version suggested in this thread because I will end up with an extra dot at the end if concatenated.):
<xsl:template name="ConvertToInitCapString">
<xsl:param name="str" select="."></xsl:param>
<!-- Extract each component of the name delimited by . -->
<xsl:for-each select="tokenize($str, '\.')">
<!-- Init cap each component -->
<xsl:value-of select="concat(upper-case(substring(.,1,1)), lower-case(substring(.,2)))"></xsl:value-of>
<xsl:if test="position() != last()">
<xsl:value-of select="'.'"></xsl:value-of>
</xsl:if>
</xsl:for-each>
</xsl:template>

Different way of sorting a list, depending on a variable in XSLT

I am making a newslist, and have until now sorted the news after their date. Newest first.
But I would like to give the administrator of the site a more flexible solution. This means that in the backend, the admin can choose from a dropdown-list, in wich way he/she want's the list to be sorted. By date(newest first and oldest first), or by title (A-Z and Z-A). This means 4 possible ways right.
Right now I have the following XSLT:
<xsl:variable name="alleNyheder" select="$currentPage//node" />
<xsl:variable name="sort">
<news>
<xsl:for-each select="$alleNyheder[#template='1092']">
<news>
<id>
<xsl:value-of select="./#id"></xsl:value-of>
</id>
<date>
<xsl:choose>
<xsl:when test="./data[#alias='date'] != ''">
<xsl:value-of select="./data[#alias='date']"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="./#createDate"/>
</xsl:otherwise>
</xsl:choose>
</date>
</news>
</xsl:for-each>
</news>
</xsl:variable>
<xsl:for-each select="msxml:node-set($sort)/news/news">
<xsl:sort data-type="text" select="date" order="descending" />
---- My newsitems ----
</xsl:for-each>
So in the moment I sort my list after the "date"-value in my variable $sort.
If I change the "date"-field in $sort, to titles instead of date's I can actually sort my list after the titles of the news. But unfortunately it should be sorted in an ascending order, instead of a descending order. And I can't figure out how to change the order-value dynamically like I do in the select-value.
If it helps anyone, I am working on Umbraco CMS.
Thanks
-Kim
<xsl:choose>
<xsl:when test="$sortfield = 'date' and $sortorder = 'D'>
<xsl:for-each select="msxml:node-set($sort)/news/news">
<xsl:sort data-type="text" select="date" order="descending" />
<!-- ... -->
</xsl:for-each>
</xsl:when>
<xsl:when test="$sortfield = 'date' and $sortorder = 'A'>
<xsl:for-each select="msxml:node-set($sort)/news/news">
<xsl:sort data-type="text" select="date" order="ascending" />
<!-- ... -->
</xsl:for-each>
</xsl:when>
<!-- ... -->
</xsl:choose>
On a different note, you really should look into <xsl:apply-templates> and avoid <xsl:for-each>. Your code gets cleaner and a lot more idiomatic this way. I am also sure that the entire temporary node-set() business is completely avoidable.

XSLT Paging - default to current Date

I am using an xslt transform to show a long list of events.
It has paging, but what I would like is for it to default to the first events that are closest to the current date.
I'm assuming you have a useful date format (YYYY-MM-DD).
<xsl:param name="currentDate" select="''" /><!-- fill this from outside! -->
<xsl:template name="isPageSelected"><!-- returns true or false -->
<xsl:param name="eventsOnPage" /><!-- expects a node-set of events -->
<xsl:choose>
<xsl:when test="$eventsOnPage">
<!-- create a string "yyyy-mm-dd,YYYY-MM-DD-" (note the trailing dash) -->
<xsl:variable name="dateRange">
<xsl:for-each select="$eventsOnPage">
<xsl:sort select="date" />
<xsl:if test="position() = 1">
<xsl:value-of select="concat(date, ',')" />
</xsl:if>
<xsl:if test="position() = last()">
<xsl:value-of select="concat(date, '-')" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<!-- trailing dash ensures that the "less than" comparison succeeds -->
<xsl:value-of select="
$currentDate >= substring-before($dateRange, ',')
and
$currentDate < substring-after($dateRange, ',')
" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="false()" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
So in your paging routine, to find out if the current page is the selected one, invoke
<xsl:variable name="isPageSelected">
<xsl:call-template name="isPageSelected">
<xsl:with-param name="eventsOnPage" select="event[whatever]" />
</xsl:call-template>
</xsl:variable>
<!-- $isPageSelected now is true or false, proceed accordingly -->
Since sorting in XSLT 1.0 is horribly, horribly broken, your best bet is to find an extension or include a Unix-style time somewhere in your source XML so you can sort on that (although an ISO-formatted string might also work).