First matching ancestor depeding on name - xslt

Hello XPath/Xslt Friends,
i have the following Xml. I want to determine the id of the first matching section or chapter of my cout element. If the the closest node of the cout is a chapter, then i will get the chapter id, otherwise the section id.
<book>
<chapter id="chapter1">
<aa>
<cout></cout> --> i will get "chapter1"
</aa>
<section id="section1">
<a>
<b>
<cout></cout> --> i will get section1
</b>
</a>
</section>
<section id="section2">
<a>
<b>
<cout></cout> --> i will get section2
</b>
</a>
</section>
</chapter>
</book>
i tried the following statement:
<xsl:value-of select="ancestor::*[local-name() = 'section' or local-name() = 'chapter']/#id" />
but in case of the cout contained in section1, it will give me chapter1, instead of section1. Any solutions?

Your current statement is selecting all ancestors with the name section or chapter, and after being selected, xsl:value-of will only return the value of the first one, in document order (in XSLT 1.0 that is).
Try this instead
<xsl:value-of select="ancestor::*[local-name() = 'section' or local-name() = 'chapter'][1]/#id" />

if cout has no ancestor section , print chapter id
else print section id.
<xsl:for-each select="//cout">
<xsl:if test="count(ancestor::section)= 0">
<xsl:value-of select="ancestor::chapter/#id"/>
</xsl:if>
<xsl:if test="count(ancestor::section)>0">
<xsl:value-of select="ancestor::section/#id"/>
</xsl:if>
</xsl:for-each>

Related

Change attribute value to position of another element with corresponding attribute value

I have a single XHTML document that contains span and div elements that refer to page breaks of a print version using id and epub:type attributes. For example: <div epub:type="pagebreak" id="page-3"/>. The document also has links to those elements, for example: 3.
This single XHTML document will be split into multiple XHTML documents to form an EPUB package. For this reason, the href attributes need to be updated to match the new location of the corresponding id. For example: 3. The name of the new XHTML file is equal to the position of the body/section elements. So in the last example, the page break with id="page-3" is apparently in the second body/section element.
I'm using the following XSLT 2.0 stylesheet:
<!--identity transform-->
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<!--variable to match id of elements with pagebreak values-->
<xsl:variable name="page-id" select="//*[#epub:type = 'pagebreak']/#id"/>
<!--update href attributes to match new filenames-->
<xsl:template match="a/#href">
<xsl:choose>
<xsl:when test="tokenize(., '#')[last()] = $page-id">
<xsl:attribute name="href">
<xsl:number count="//body/section[$page-id = tokenize(., '#')[last()]]" format="01"/>
<xsl:value-of select="concat('.xhtml', .)"/>
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
It checks for href attributes that have a corresponding id using the $page-id variable. If there is a match, the href attribute should be updated using the count() function. Otherwise, the href should remain unchanged. The test seems to work, however, I'm not getting the result I want. This is the input:
<body>
<section>
<p>Link to page 3: 3</p>
</section>
<section>
<div epub:type="pagebreak" id="page-3"/>
</section>
</body>
This is the output I get:
<body>
<section>
<p>Link to page 3: 3</p>
</section>
<section>
<div epub:type="pagebreak" id="page-3"/>
</section>
</body>
This is the output I want:
<body>
<section>
<p>Link to page 3: 3</p>
</section>
<section>
<div epub:type="pagebreak" id="page-3"/>
</section>
</body>
It seems as if the XPath expression within xsl:number doesn't return a result, but I can't figure out why. Can anyone help me with this please?
I think you want e.g.
<xsl:template match="body/section" mode="number">
<xsl:number format="01"/>
<xsl:template>
and then instead of
<xsl:number count="//body/section[$page-id = tokenize(., '#')[last()]]" format="01"/>
use
<xsl:apply-templates select="key('page-id', substring-after(., '#'))" mode="number"/>
plus a key declaration
<xsl:key name="page-id" match="body/section" use=".//*[#epub:type = 'pagebreak']/#id"/>

XSLT: how to match exact text value within an element and replace

I need to find a specific text value within a document,'method'and for each instance replace that text value 'method' with the following:
element to replace method
This 'method' value can appear several times throughout the document. The issue is that I also need to retain the remaining text within the element, apart from 'method' which will be replaced.
<section id="1">
<title>Methods</title>
<p>The test method blah has 6 types of methods available</p>
<p>With the exception of a specific method<p
</section>
<section id="2">
<title>Organisations</title>
<p>The organisation has a method</p>
</section>
I'm not sure if using fn:replace would the best approach, and if i also need to use regular expressions (something i'm not currently familiar with). Any advice on an approach here would be greatly appreciated.
Expected output only replaces the exact text 'method' with the content element, but retains 'methods':
<section id="1">
<title>Methods</title>
<p>The test <content type="description" xlink:href="linktodescription">method</named-content> blah has 6 types of methods available</p>
</section>
<section id="2">
<title>Organisations</title>
<p>The organisation has a <content type="description" xlink:href="linktodescription">method</named-content></p>
</section>
Assuming Saxon 9 as the XSLT processor you can use (based on http://saxonica.com/html/documentation/xsl-elements/analyze-string.html)
<xsl:template match="section/p">
<xsl:copy>
<xsl:analyze-string select="." regex="\bmethod\b" flags=";j">
<xsl:matching-substring>
<content type="description" xlink:href="linktodescription">
<xsl:value-of select="."/>
</content>
</xsl:matching-substring>
<xsl:non-matching-substring>
<xsl:value-of select="."/>
</xsl:non-matching-substring>
</xsl:analyze-string>
</xsl:copy>
</xsl:template>

different cases differentiate and link them

I've the below XML.
<orderedlist type="manual">
<item num="1."><para>identify the issues in dispute;</para></item>
<item num="1.1"><para>explore and generate options;</para></item>
<item num="1.1.1"><para>communicate with one another; and/or</para></item>
<item num="(i)"><para>copy of the cancellation of employment pass; and</para></item>
<orderedlist>
here i want to translate all the . to - in item-num and surround them with a different tag, where there is some number or period after . previously I've received a solution and I've used not(ends-with(./#num,'.')). That worked fine till some point, recently I've came across a situation(4th item num in the above XML) where in the item num has values like (i), i, a, a), and these also satisfy the condition and are getting the new tag. below is my XSL template.
<xsl:template name="orderedlist" match="orderedlist">
<ol class="eng-orderedlist orderedlist">
<xsl:apply-templates/>
</ol>
</xsl:template>
<xsl:template match="item">
<xsl:call-template name="paran"></xsl:call-template>
</xsl:template>
<xsl:template name="paran">
<xsl:apply-templates select="./para/node()[1][self::page]" mode="first"/>
<li class="item">
<div class="para">
<xsl:choose>
<xsl:when test="not(ends-with(./#num,'.'))">
<a name="{translate(./#num,'.','-')}"/>
<span class="phrase">
<xsl:value-of select="./#num"></xsl:value-of>
</span>
</xsl:when>
<xsl:when test="./#num">
<span class="item-num">
<xsl:value-of select="./#num"></xsl:value-of>
</span>
</xsl:when>
</xsl:choose>
<xsl:apply-templates/>
</div>
</li>
</xsl:template>
<xsl:template match="page[not(preceding-sibling::node()[not(self::text()) or normalize-space()])]"/>
here i don't want phrase around other numbers than i format X.X X.X.X.
you can find a demo of this issue here.
please let me know how can i fix it.
Thanks
Perhaps you need to add a check on whether the num attribute contains a full-stop in the first place
<xsl:when test="contains(#num, '.') and not(ends-with(./#num,'.'))">
i.e. #num contains a full-stop, but not one at the end.
I'd suggest you do it this way:
<xsl:template match="item">
<!-- do one thing here -->
</xsl:template>
<xsl:template match="item [not(ends-with(#num, '.'))] [not (translate(#num, '.123456789', ''))]">
<!-- do another thing here -->
</xsl:template>
--
Doing the predicate by regex as suggested by Ian Roberts would probably be more elegant.

XSLT contains two periods

I have an HTML document that used an unordered list to display the navigation. When it is output as XML it no longer uses the list, but instead assigns each link a <Description>. The way that the description works is that the main links are assigned numbers like 100, 200, 300, 400, and so on. Subnavigation links from those are assigned as follows: 200.100, 200.200, 200.300, and so on.
The XML looks like the following:
<Items>
<PgIndexElementItem>
<Description>100</Description>
<Title>This is the Title</Title>
</PgIndexElementItem>
<PgIndexElementItem>
<Description>200</Description>
<Title>This is the Title</Title>
</PgIndexElementItem>
<PgIndexElementItem>
<Description>200.100</Description>
<Title>This is the Title</Title>
</PgIndexElementItem>
<PgIndexElementItem>
<Description>200.100.100</Description>
<Title>This is the Title</Title>
</PgIndexElementItem>
</Items>
If you look at the last PgIndexElementItem, there are three sets of numbers.
I'm trying to recreate the unordered list using XSLT. This is what I have been doing so far:
<ul>
<xsl:for-each select="//PgIndexElementItem">
<xsl:if test="not(contains(Description, '.'))">
<li><xsl:value-of select="Title"/>
<ul>
<xsl:for-each select="//PgIndexElementItem">
<xsl:if test="contains(Description, '.')">
<li><xsl:value-of select="Title"/></li>
</xsl:if>
</xsl:for-each>
</ul>
</li>
</xsl:if>
</xsl:for-each>
</ul>
My question would be, is there a way to specify a contains(Description that targets descriptions with two periods, like the last PgIndexElementItem above?
I hope I explained this well enough. I don't know how to make it less confusing. Thanks in advance!
EDIT
o Graduate Program
* How to Apply
* Masters Program
o M.A. Handbook
o FAQs
o Alumni
o Masters Theses
o Sample Thesis Proposals
* M.A. Handbook
* FAQs
* Alumni
* Masters Theses
* Sample Thesis Proposals
You can use
<xsl:if test="contains(substring-after(Description, '.'), '.')">
...
to test whether an element has a Description child with two or more periods.
However, I would instead change your XSLT to:
<!-- this part replaces your <ul> code above -->
<ul>
<xsl:apply-templates
select="//PgIndexElementItem[not(contains(Description, '.'))]" />
</ul>
Updated this template:
<!-- add this new template -->
<xsl:template match="PgIndexElementItem">
<li>
<xsl:value-of select="Title"/>
<xsl:variable name="prefix" select="concat(Description, '.')"/>
<xsl:variable name="childOptions"
select="../PgIndexElementItem[starts-with(Description, $prefix)
and not(contains(substring-after(Description, $prefix), '.'))]"/>
<xsl:if test="$childOptions">
<ul>
<xsl:apply-templates select="$childOptions" />
</ul>
</xsl:if>
</li>
</xsl:template>
This will avoid unnecessary code duplication, and unwanted empty <ul> elements.
I've made some assumptions about how the Description children are to be interpreted... e.g. "200.100" should appear in a sublist under "200" but not under "100".
Use:
string-length(Description) - string-length(translate(Description, '.','')) = 2
This works the same for any number $n (while the "substring-after" technique is simply unusable in the general case):
string-length(Description) - string-length(translate(Description, '.','')) = $n
I looks like you're dealing with nested PgIndexElementItem elements - if they nest in multiple layers you should consider refactoring your XSL to use a recursive template (assuming the HTML output for each level is predictable).
As it is written, your for-each loop selects all the PgIndexElementItem descendants of the current element, then does that again when it hits the inner for-each loop. Possibly not what you intended.

How do I get an XML node with xpath in a loop, based on an attribute with the same name as the attribute in the tree I'm searching?

I cant really formulate that better, so I'll go with an example instead:
XML:
<root>
<foo>
<bar id="1">sdf</bar>
<bar id="2">sdooo</bar
</foo>
<feng>
<heppa id="4">hihi</heppa>
<heppa id="2">sseeapeea</heppa>
<heppa id="1">....</heppa>
</feng>
</root>
XSLT:
<xsl:for-each select="/root/foo/bar">
<p>
<xsl:value-of select="." />: <xsl:value-of select="/root/feng/heppa[#id = #id]" />
</p>
</xsl:for-each>
Desired output:
<p>sdf: ....</p>
<p>sdooo: sseeapeea</p>
Actual output:
<p>sdf: hihi</p>
<p>sdooo: hihi</p>
For selecting nodes with XPath 1.0 only, you need to do a node set comparison:
/root/feng/heppa[#id=/root/foo/bar/#id]
Of course, this has NxM complexity (as the others XSLT solutions)
With XSLT 1.0 you should use keys because there are cross references:
<xsl:key name="kBarById" select="bar" use="#id"/>
<xsl:template match="/root/feng/heppa[key('kBarById',#id)]">
<p>
<xsl:value-of select="concat(key('kBarById',#id),': ',.)"/>
</p>
</xsl:template>
I assume you mean /root/foo/bar since /root/foo elements don't have id.
You're comparing the #id with itself, so of course it's true for the first node examined. You can use current() to reference the current node in an expression:
<xsl:for-each select="/root/foo/bar">
<p>
<xsl:value-of select="." />: <xsl:value-of select="/root/feng/heppa[#id = current()/#id]" />
</p>
</xsl:for-each>
Another solution is to read the id attribute into a variable.
<xsl:for-each select="/root/foo/bar">
<xsl:variable name="id" select="#id"/>
<p>
<xsl:value-of select="." />: <xsl:value-of select="/root/feng/heppa[#id = $id]" />
</p>
</xsl:for-each>
This might be handier, if your real use case is more complicated and you need to use the value of the id multiple times in this for-each section.