Building list with xslt - xslt

I am trying to build a list that parses my entire xml document. I need to list the numeric names then the alpha names. The list should look something like this.
6
6600 Training
6500 Training
A
Accelerated Training
T
Training
This is a snippet of the xml.
<courses>
<course>
<name>Accelerated Training</name>
</course>
<course>
<name>6600 Training</name>
</course>
<course>
<name>Training</name>
</course>
<course>
<name>6500 Training</name>
</course>
</courses>
This is the code I am currently using. I found this in another question on the site and have customized it somewhat. Currently it doesn't take into account my need for parsing by number and it also returns out of alphabetical order.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vLower" select= "'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="vUpper" select= "'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:key name="kTitleBy1stLetter" match="courses/course" use="substring(name,1,1)"/>
<xsl:template match="/*">
<xsl:for-each select="course [generate-id() = generate-id(key('kTitleBy1stLetter', substring(name,1,1)) [1] ) ]">
<xsl:variable name="v1st" select="substring(name,1,1)"/>
<h2><xsl:value-of select="$v1st"/></h2>
<div class="{translate($v1st, $vUpper, $vLower)}-content">
<ul>
<xsl:for-each select="key('kTitleBy1stLetter',$v1st)">
<li><xsl:value-of select="name"/></li>
</xsl:for-each>
</ul>
</div>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

Basically you need to group by first letter and sort by <name>. You are on a good way with your Muenchian grouping approach already.
I would suggest an alternative that's a bit easier on the eye:
<xsl:key name="kInitial" match="course" use="substring(name, 1, 1)" />
<xsl:template match="courses">
<xsl:apply-templates select="course" mode="initial">
<xsl:sort select="name" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="course" mode="initial">
<xsl:variable name="initial" select="substring(name, 1, 1)" />
<xsl:variable name="courses" select="key('kInitial', $initial)" />
<xsl:if test="generate-id() = generate-id($courses[1])">
<h2><xsl:value-of select="$initial"/></h2>
<ul>
<xsl:apply-templates select="$courses">
<xsl:sort select="name" />
</xsl:apply-templates>
</ul>
</xsl:if>
</xsl:template>
<xsl:template match="course">
<li>
<xsl:value-of select="name"/>
</li>
</xsl:template>
outputs:
<h2>6</h2>
<ul>
<li>6500 Training</li>
<li>6600 Training</li>
</ul>
<h2>A</h2>
<ul>
<li>Accelerated Training</li>
</ul>
<h2>T</h2>
<ul>
<li>Training</li>
</ul>
EDIT: For the sake of legibility I left out the upper-casing of the first letter. The correct key would be this (you can't use a variable in a key, hence the literal alphabet strings):
<xsl:key name="kInitial" match="course" use="
translate(
substring(name, 1, 1),
'abcdefghijklmnopqrstuvwxyz',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
)
" />
The same goes of course for the $initial variable in the second template, but here you can in fact use variables again.
EDIT #2: Since sorting is case-sensitive as well, you can use the same expression:
<xsl:sort select="translate(substring(name, 1, 1), $vLower, $vUpper)" />

An XSLT 2.0 solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/*">
<xsl:for-each-group select="course"
group-by="upper-case(substring(name,1,1))">
<xsl:sort select="current-grouping-key()"/>
<xsl:sequence select=
"concat('
', current-grouping-key())"/>
<xsl:for-each select="current-group()">
<xsl:sort select="upper-case(name)"/>
<xsl:sequence select="concat('
', name)"/>
</xsl:for-each>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
When the above transformation is applied on the originally-provided XML document:
<courses>
<course>
<name>Accelerated Training</name>
</course>
<course>
<name>6600 Training</name>
</course>
<course>
<name>Training</name>
</course>
<course>
<name>6500 Training</name>
</course>
</courses>
the wanted result is produced (in text format for simplicity -- producing the Html is left as an exercise for the reader :)
6
6500 Training
6600 Training
A
Accelerated Training
T
Training
Do note:
The use of the <xsl:for-each-group> XSLT 2.0 instruction
The use of the current-grouping-key() and current-group() XSLT 2.0 functions.
The use of the upper-case() XPath 2.0 function

Well the numbers part is tricky if you want anything complex, but based on your ideal output all you're missing is a simple sort on your for-each:
<xsl:sort select="key('kTitleBy1stLetter', substring(name,1,1))" />
caveat: I make no claims about this being the best or only or otherwise method, merely that this works full stop, and uses what you already have.

Related

Better way to cycle xsl:for-each letter of the alphabet?

I have a long XML file from which I ned to pull out book titles and other information, then sort it alphabetically, with a separator for each letter. I also need a section for items that don't begin with a letter, say a number or symbol. Something like:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
…
I also need to create a separate list of authors, created from the same data but showing different kinds of information.
How I'm currently doing it
This is simplified, but I basically have this same code twice, once for titles and once for authors. The author version of the template works with different elements and does different things with the data, so I can't use the same template.
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'#'" />
</xsl:call-template>
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'A'" />
</xsl:call-template>
…
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'Z'" />
</xsl:call-template>
<xsl:template name="BIP-letter">
<xsl:param name="letter" />
<xsl:choose>
<xsl:when test="$letter = '#'">
<xsl:text>#</xsl:text>
<xsl:for-each select="//Book[
not(substring(Title,1,1) = 'A') and
not(substring(Title,1,1) = 'B') and
…
not(substring(Title/,1,1) = 'Z')
]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$letter" />
<xsl:for-each select="//Book[substring(Title,1,1) = $letter]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
My questions
The code above works just fine, but:
Manually cycling through each letter gets very long, especially having to do it twice. Is there a way to simplify that? Something like a <xsl:for-each select="[A-Z]"> that I could use to set the parameter when calling the template?
Is there a simpler way to select all titles that don't begin with a letter? Something like //Book[not(substring(Title,1,1) = [A-Z])?
There may be cases where the title or author name starts with a lowercase letter. In the code above, they would get grouped with under the # heading, rather than with the actual letter. The only way I can think to accommodate that—doing it manually—would significantly bloat up the code.
This solution answers all questions asked:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vLowercase" select="'abcdefghijklmnopqrstuvuxyz'"/>
<xsl:variable name="vUppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="vDigits" select="'0123456789'"/>
<xsl:key name="kBookBy1stChar" match="Book"
use="translate(substring(Title, 1, 1),
'abcdefghijklmnopqrstuvuxyz0123456789',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ##########'
)"/>
<xsl:template match="/*">
<xsl:apply-templates mode="firstInGroup" select=
"Book[generate-id()
= generate-id(key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)[1]
)
]">
<xsl:sort select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book" mode="firstInGroup">
<xsl:value-of select="'
'"/>
<xsl:value-of select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
<xsl:apply-templates select=
"key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)">
<xsl:sort select="Title"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book">
<xsl:value-of select="'
'"/>
<xsl:value-of select="concat(Title, ' - ', Binding, ', $', price)"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following xml document (none provided in the question!):
<Books>
<Book>
<Title>After the Sands</Title>
<Binding>paperback</Binding>
<price>24.95</price>
</Book>
<Book>
<Title>Cats Galore: A Compendium of Cultured Cats</Title>
<Binding>hardcover</Binding>
<price>5.00</price>
</Book>
<Book>
<Title>Arctic Spirit</Title>
<Binding>hardcover</Binding>
<price>65.00</price>
</Book>
<Book>
<Title>1494</Title>
<Binding>hardcover</Binding>
<price>9.99</price>
</Book>
<Book>
<Title>Back to the Front</Title>
<Binding>paperback</Binding>
<price>18.95</price>
</Book>
</Books>
the wanted, correct result is produced:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
C
Cats Galore: A Compendium of Cultured Cats - hardcover, $5.00
Explanation:
Use of the Muenchian method for grouping
Use of the standard XPath translate() function
Using mode to process the first book in a group of books starting with the same (case-insensitive) character
Using <xsl:sort> to sort the books in alphabetical orser
The most problematic part is this:
I also need a section for items that don't begin with a letter, say a number or symbol.
If you have a list of all possible symbols that an item can begin with, then you can simply use translate() to convert them all to the # character. Otherwise it gets more complicated. I would try something like:
XSLT 1.0 (+ EXSLT node-set())
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:key name="book" match="Book" use="index" />
<xsl:template match="/Books">
<!-- first-pass: add index char -->
<xsl:variable name="books-rtf">
<xsl:for-each select="Book">
<xsl:copy>
<xsl:copy-of select="*"/>
<index>
<xsl:variable name="index" select="translate(substring(Title, 1, 1), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
<xsl:choose>
<xsl:when test="contains('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $index)">
<xsl:value-of select="$index"/>
</xsl:when>
<xsl:otherwise>#</xsl:otherwise>
</xsl:choose>
</index>
</xsl:copy>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="books" select="exsl:node-set($books-rtf)/Book" />
<!-- group by index char -->
<xsl:for-each select="$books[count(. | key('book', index)[1]) = 1]">
<xsl:sort select="index"/>
<xsl:value-of select="index"/>
<xsl:text>
</xsl:text>
<!-- list books -->
<xsl:for-each select="key('book', index)">
<xsl:sort select="Title"/>
<xsl:value-of select="Title"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="Binding"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="Price"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However, this still leaves the problem of items that begin with a diacritic, e.g. "Österreich" or say a Greek letter. Under this method they too will be clumped under #.
Unfortunately, the only good solution for this is to move to XSLT 2.0.
Demo: https://xsltfiddle.liberty-development.net/jyRYYjj/2

Sorting grouped items using XSLT 1.0

I'be got a rather large XML file that contains a list of vehicle models, their price and a monthly payment price. There is actually loads of other information in there, but those are the salient bits of data I'm interested in.
There are loads of duplicate models in there at different prices/monthly payments. My task, which I have done so far with the help of this forum, is to construct a distinct list of the models (IE. no duplicates), showing the lowest priced vehicle in that model.
The bit I'm stuck on, is I then need to sort this list displaying the lowest monthly payment to the highest monthly payment. The complication being the lowest priced vehicle don't always equal the lowest monthly payment.
My XML looks a bit like this:
<?xml version="1.0" encoding="utf-8"?>
<Dealer>
<Vehicle>
<Model>KA</Model>
<DealerPriceNoFormat>8700.00</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>300.50</OptionsFinanceMonthlyPayment>
</Vehicle>
<Vehicle>
<Model>KA</Model>
<DealerPriceNoFormat>10000.50</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>270.50</OptionsFinanceMonthlyPayment>
</Vehicle>
<Vehicle>
<Model>Focus</Model>
<DealerPriceNoFormat>12000.00</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>340.00</OptionsFinanceMonthlyPayment>
</Vehicle>
<Vehicle>
<Model>KA</Model>
<DealerPriceNoFormat>9910.00</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>430.75</OptionsFinanceMonthlyPayment>
</Vehicle>
<Vehicle>
<Model>KUGA</Model>
<DealerPriceNoFormat>23010.00</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>550.20</OptionsFinanceMonthlyPayment>
</Vehicle>
<Vehicle>
<Model>Focus</Model>
<DealerPriceNoFormat>15900.00</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>430.00</OptionsFinanceMonthlyPayment>
</Vehicle>
</Dealer>
As I said, there is loads of other data in there, but that's the basic structure.
And my XSLT looks like this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="html" omit-xml-declaration="yes" indent="yes" version="4.0" encoding="iso-8859-1" />
<xsl:key name="by-id" match="Dealer/Vehicle" use="Model"/>
<xsl:template match="Dealer">
<xsl:copy>
<xsl:for-each select="Vehicle[generate-id() = generate-id(key('by-id', Model)[1])]">
<xsl:for-each select="key('by-id', Model)">
<xsl:sort select="DealerPriceNoFormat" data-type="number" order="ascending" />
<xsl:if test="position()=1">
<p>
<xsl:value-of select="Model" /><br />
<xsl:value-of select="DealerPriceNoFormat" /><br />
<xsl:value-of select="OptionsFinanceMonthlyPayment" />
</p>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Like I say, I'm almost there, just can't figure out how to then sort the output list by OptionsFinanceMonthlyPayment.
So the in the case above output would look something like this, showing the cheapest car in each model, but sorted by the monthly payment on the output list:
KA
8700.00
300.50
Focus
12000.00
340.00
KUGA
23010.00
550.20
Thanks in advance.
I would do this in two passes - something like:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:key name="vehicle-by-model" match="Vehicle" use="Model"/>
<xsl:template match="/Dealer">
<!-- first-pass -->
<xsl:variable name="groups">
<xsl:for-each select="Vehicle[generate-id() = generate-id(key('vehicle-by-model', Model)[1])]">
<group>
<xsl:for-each select="key('vehicle-by-model', Model)">
<xsl:sort select="DealerPriceNoFormat" data-type="number" order="ascending" />
<xsl:if test="position()=1">
<model><xsl:value-of select="Model" /></model>
<price><xsl:value-of select="DealerPriceNoFormat" /></price>
<pmt><xsl:value-of select="OptionsFinanceMonthlyPayment" /></pmt>
</xsl:if>
</xsl:for-each>
</group>
</xsl:for-each>
</xsl:variable>
<!-- output -->
<html>
<xsl:for-each select="exsl:node-set($groups)/group">
<xsl:sort select="pmt" data-type="number" order="ascending" />
<p>
<xsl:value-of select="model" /><br />
<xsl:value-of select="price" /><br />
<xsl:value-of select="pmt" />
</p>
</xsl:for-each>
</html>
</xsl:template>
</xsl:stylesheet>
Note that this requires a processor that supports a node-set() extension function.

Sort data in the xml alphabetical order

Input XML :
<?xml version="1.0" encoding="utf-8" ?>
<infoset>
<info>
<title>Bill</title>
<group>
<code>state</code>
</group>
</info>
<info>
<title>Auto</title>
<group>
<code>state</code>
</group>
</info>
<info>
<title>Auto2</title>
</info>
<info>
<title>Auto3</title>
</info>
<info>
<title>Auto5</title>
</info>
<info>
<title>Certificate</title>
<group>
<code>Auto4</code>
</group>
</info>
</infoset>
Expected output :
A
Auto2
Auto3
Auto4
Certificate
Auto5
S
state
Auto
Bill
I need to arrange the title and code in alphabetical order.If the info has group the tile should come under the group. I am using visual studio2010 , xslt1.0 Processor and xml editor.
Sorting in XSLT is straight-forward. What you are actually really needing to know is how to 'group' items. As Michael Kay commented, this is much easier in XSLT 2.0 than in XSLT 1.0. In XSLT 1.0 you tend to use the Muenchian Grouping method, which appears confusing when you first see it, but is generally the most efficient way of doing it.
From the looks of your output, you are doing two lots of grouping. Firstly, by the first letter, then by either group/code (if it exists), or title.
Muenchian Grouping works by defining a key, to enable quick look up of all items in a 'group'. For the first letter of eithe group/code or title, you would define it like so
<xsl:key name="letter" match="info" use="substring(concat(group/code, title), 1, 1)"/>
(Note: This is case sensitive, so you may need to use the 'translate' function if you can have a mix of lower and upper case start letters).
If group/code exists, it will use the first letter of that, otherwise it will pick up the first letter of the title.
For the group/code or title itself, the key would be as follows
<xsl:key name="info" match="info" use="title[not(../group)]|group/code"/>
So, it only uses "title" elements where there is no "group" element present.
To get the distinct first letters for your first grouping, you select all the info elements and check whether they are the first element in the key for their given letter. This is done like so
<xsl:apply-templates
select="info
[generate-id()
= generate-id(key('letter', substring(concat(group/code, title), 1, 1))[1])]"
mode="letter">
<xsl:sort select="substring(concat(group/code, title), 1, 1)" />
</xsl:apply-templates>
The 'mode' is used here because the final XSLT will have to templates matching info.
Within the matching template, to group by either code/group or title you can then do this
<xsl:apply-templates
select="key('letter', $letter)
[generate-id() = generate-id(key('info', title[not(../group)]|group/code)[1])]">
<xsl:sort select="title[not(../group)]|group/code" />
</xsl:apply-templates>
And finally, to output all the elements within the final group, you would just use the key again
<xsl:apply-templates select="key('info', $value)[group/code=$value]/title">
Here is the full XSLT.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:key name="letter" match="info" use="substring(concat(group/code, title), 1, 1)"/>
<xsl:key name="info" match="info" use="title[not(../group)]|group/code"/>
<xsl:template match="/*">
<xsl:apply-templates select="info[generate-id() = generate-id(key('letter', substring(concat(group/code, title), 1, 1))[1])]" mode="letter">
<xsl:sort select="substring(concat(group/code, title), 1, 1)" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="info" mode="letter">
<xsl:variable name="letter" select="substring(concat(group/code, title), 1, 1)" />
<xsl:value-of select="concat($letter, '
')" />
<xsl:apply-templates select="key('letter', $letter)[generate-id() = generate-id(key('info', title[not(../group)]|group/code)[1])]">
<xsl:sort select="title[not(../group)]|group/code" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="info">
<xsl:variable name="value" select="title[not(../group)]|group/code" />
<xsl:value-of select="concat($value, '
')" />
<xsl:apply-templates select="key('info', $value)[group/code=$value]/title">
<xsl:sort select="." />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="title">
<xsl:value-of select="concat(' ', ., '
')" />
</xsl:template>
</xsl:stylesheet>
When applied to your XML, the following is output
A
Auto2
Auto3
Auto4
Certificate
Auto5
s
state
Auto
Bill

Get the maximum starting substring of the text node, such that the last word in it isn't truncated, and its length doesn't exceed a given limit

How do I select only n number of comments element using xsl. I am able to select n number of characters but that snaps in the middle of a word and makes it look ugly.
I have been able to get a count of total number of words in 'comments' node but not sure how to only show like 15 words if there are like a total of 20 there.
<div class="NewsDescription">
<xsl:value-of select="string-length(translate(normalize-space(#Comments),'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',''))+1" />
<xsl:value-of select="substring(#Comments,0,120)"/>
<xsl:if test="string-length(#Comments) > 120">…</xsl:if>
Read More
</div>
Actual xml is like this. I am trying to rollup story pieces in "Related Stories' manner on the right side of a dynamic page.
<news>
<title>Story title</title>
<comments> story gist here..a couple of sentences.</comments>
<content> actualy story </content>
Please help. I found this resources by Marc andersen but the xsl is too complex for me filter through and make use of myself..
http://sympmarc.com/2010/07/13/displaying-the-first-n-words-of-announcement-bodies-with-xsl-in-a-dvwp/
Some help from the XSL gurus will be appreciated..
Here is a non-recursive, pure XSLT 1.0 solution:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vUpper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="vLower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="vDigits" select="'0123456789'"/>
<xsl:variable name="vAlhanum" select="concat($vLower, $vUpper, $vDigits)"/>
<xsl:template match="comments">
<xsl:param name="pText" select="."/>
<xsl:choose>
<xsl:when test="not(string-length($pText) > 120)">
<xsl:value-of select="$pText"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vTruncated" select="substring($pText, 1, 121)"/>
<xsl:variable name="vPunct"
select="translate($vTruncated, $vAlhanum, '')"/>
<xsl:for-each select=
"(document('')//node() | document('')//#* | document('')//namespace::*)
[not(position() > 121)]">
<xsl:variable name="vPos" select="122 - position()"/>
<xsl:variable name="vRemaining" select="substring($vTruncated, $vPos+1)"/>
<xsl:if test=
"contains($vPunct, substring($vTruncated, $vPos, 1))
and
contains($vAlhanum, substring($vTruncated, $vPos -1, 1))
and
string-length(translate($vRemaining, $vPunct, ''))
= string-length($vRemaining)
">
<xsl:value-of select="substring($vTruncated, 1, $vPos -1)"/>
</xsl:if>
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
When this transformation is applied on the following XML document:
<news>
<title>Story title</title>
<comments> story gist here..a couple of sentences. Many more sentences ...
even some more text with lots of meaning and sense aaand a lot of humor. </comments>
<content> actualy story </content>
</news>
the wanted, correct result is produced:
story gist here..a couple of sentences. Many more sentences ...
even some more text with lots of meaning and sense

Extract Xpaths of all nodes and then their attributes

I am struggling with xslt from the past 2 days, owing to my starter status.My requirement is that given any input XML file ,I want the output to be a list of all the XPaths of all the tags in order in which they appear in the original XML document(parent, then parent,parents Attributes list/child, parent/child/childOFchild and so forth). THe XSLT should not be specific to any single XMl schema. It should work for any XML file, which is a valid one.
Ex:
If the Input XML Is :
<v1:Root>
<v1:UserID>test</v1:UserID>
<v1:Destination>test</v1:Destination>
<v1:entity name="entiTyName">
<v11:attribute name="entiTyName"/>
<v11:attribute name="entiTyName"/>
<v11:attribute name="entiTyName"/>
<v11:filter type="entiTyName">
<v11:condition attribute="entiTyName" operator="eq" value="{FB8D669E-D090-E011-8F43-0050568E222C}"/>
<v11:condition attribute="entiTyName" operator="eq" value="1"/>
</v11:filter>
<v11:filter type="or">
<v11:filter type="or">
<v11:filter type="and">
<v11:filter type="and">
<v11:condition attribute="cir_customerissuecode" operator="not-like" value="03%"/>
</v11:filter>
</v11:filter>
</v11:filter>
</v11:filter>
</v1:entity>
</v1:Root>
I want my output to be :
/v1:Root/v1:UserID
/v1:Root/v1:Destination
/v1:Root/v1:entity/#name
/v1:Root/v1:entity/v11:attribute
/v1:Root/v1:entity/v11:attribute/#name
/v1:Root/v1:entity/v11:attribute[2]
/v1:Root/v1:entity/v11:attribute[2]/#name
/v1:Root/v1:entity/v11:attribute[3]
/v1:Root/v1:entity/v11:attribute[3]/#name
/v1:Root/v1:entity/v11:filter/#type
/v1:Root/v1:entity/v11:filter/v11:condition
/v1:Root/v1:entity/v11:filter/v11:condition/#attribute
/v1:Root/v1:entity/v11:filter/v11:condition/#operator
/v1:Root/v1:entity/v11:filter/v11:condition/#value
/v1:Root/v1:entity/v11:filter/v11:condition[2]
/v1:Root/v1:entity/v11:filter/v11:condition[2]/#attribute
/v1:Root/v1:entity/v11:filter/v11:condition[2]/#operator
/v1:Root/v1:entity/v11:filter/v11:condition[2]/#value
/v1:Root/v1:entity/v11:filter[2]/v11:filter/#type
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/#type
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/#type
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/v11:condition/#attribute
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/v11:condition/#operator
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/v11:condition/#value
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/#type
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition/#attribute
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition/#operator
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition/#value
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition[2]
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition[2]/#attribute
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition[2]/#operator
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition[2]/#value
So, it is basically all the XPath of each element ,then the Xpath of the elements Attributes.
I have an XSLT with me, which is like this:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="no" />
<xsl:template match="*[not(child::*)]">
<xsl:for-each select="ancestor-or-self::*">
<xsl:value-of select="concat('/', name())" />
<xsl:if test="count(preceding-sibling::*[name() = name(current())]) != 0">
<xsl:value-of
select="concat('[', count(preceding-sibling::*[name() = name(current())]) + 1, ']')" />
</xsl:if>
</xsl:for-each>
<xsl:apply-templates select="*" />
</xsl:template>
<xsl:template match="/">
<xsl:apply-templates select="*" />
</xsl:template>
</xsl:stylesheet>
THe output which gets Produced does not cater to complex tags and also the tag's attributes in the resulting Xpath list :(.
Kindly help me in fixing this xslt to produce the output as mentioned above.
THe present output from the above XSLT is like this :
/v1:Root/v1:UserID
/v1:Root/v1:Destination
/v1:Root/v1:entity/v11:attribute
/v1:Root/v1:entity/v11:attribute[2]
/v1:Root/v1:entity/v11:attribute[3]
/v1:Root/v1:entity/v11:filter/v11:condition
/v1:Root/v1:entity/v11:filter/v11:condition[2]
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition[2]
/v1:Root/v1:entity/v11:filter[2]/v11:filter[2]/v11:filter/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter[2]/v11:filter[2]/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter[2]/v11:filter[2]/v11:condition[2]
/v1:Root/v1:entity/v11:filter[2]/v11:filter[2]/v11:filter[2]/v11:condition[3]
I think there's a discrepancy between your sample input and output, in that the output describes a filter element with two conditions that's not in the source XML. At any rate, I believe this works:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="no" />
<!-- Handle attributes -->
<xsl:template match="#*">
<xsl:apply-templates select="ancestor-or-self::*" mode="buildPath" />
<xsl:value-of select="concat('/#', name())"/>
<xsl:text>
</xsl:text>
</xsl:template>
<!-- Handle non-leaf elements (just pass processing downwards) -->
<xsl:template match="*[#* and *]">
<xsl:apply-templates select="#* | *" />
</xsl:template>
<!-- Handle leaf elements -->
<xsl:template match="*[not(*)]">
<xsl:apply-templates select="ancestor-or-self::*" mode="buildPath" />
<xsl:text>
</xsl:text>
<xsl:apply-templates select="#*" />
</xsl:template>
<!-- Outputs a path segment for the matched element: '/' + name() + [ordinalPredicate > 1] -->
<xsl:template match="*" mode="buildPath">
<xsl:value-of select="concat('/', name())" />
<xsl:variable name="sameNameSiblings" select="preceding-sibling::*[name() = name(current())]" />
<xsl:if test="$sameNameSiblings">
<xsl:value-of select="concat('[', count($sameNameSiblings) + 1, ']')" />
</xsl:if>
</xsl:template>
<!-- Ignore text -->
<xsl:template match="text()" />
</xsl:stylesheet>