Input xml is like that,
<section level="2" counter="yes">
<title id="c1_3"><!--1.3--> Reeve’s Prosthesis (1972)</title>
<figure counter="yes" id="f1_1">
<legend><para><!--<emph type="bold">Fig. 1.1</emph>--> Reeve’s prosthesis. (Reproduced with permission from Reeves B, Jobbins B, Dowson D, Wright V. A Total Shoulder Endo-Prosthesis. Eng Med 1972;1(3):64–67.)</para></legend>
<para><!--<inline-figure xlink:href="images/copy.jpg"/>--></para>
</figure>
</section>
Output should be,
<section level="2" counter="yes">
<title id="c1_3">1.3 Reeve’s Prosthesis (1972)</title>
<figure counter="yes" id="f1_1">
<legend><para><emph type="bold">Fig. 1.1</emph> Reeve’s prosthesis. (Reproduced with permission from Reeves B, Jobbins B, Dowson D, Wright V. A Total Shoulder Endo-Prosthesis. Eng Med 1972;1(3):64–67.)</para></legend>
<para><!--<inline-figure xlink:href="images/copy.jpg"/>--></para>
</figure>
</section>
My xslt wrote like this,
<xsl:template match="document//comment()">
<xsl:choose>
<xsl:when test="ancestor::para | ancestor::caption | ancestor::section | ancestor::document">
<xsl:text disable-output-escaping="yes"><comment></xsl:text>
<xsl:variable name="commentText0"><xsl:copy-of select="replace(normalize-space(.),'
',' ')"/></xsl:variable>
<xsl:variable name="commentText2"><xsl:value-of select="replace($commentText0, 'Fig([.]) ', 'Fig. ')" disable-output-escaping="yes"/></xsl:variable>
<xsl:variable name="commentText3"><xsl:value-of select="replace($commentText2, 'Table ', 'Table ')" disable-output-escaping="yes"/></xsl:variable>
<xsl:value-of select="replace($commentText3, 'Formula ', 'Formula ')" disable-output-escaping="yes"/>
<xsl:text disable-output-escaping="yes"></comment></xsl:text>
</xsl:when>
<xsl:when test="comment[contains(.,'inline-figure')]">
<xsl:text disable-output-escaping="yes"><!--</xsl:text>
<xsl:apply-templates select="node() | #*"/>
<xsl:text disable-output-escaping="yes">--></xsl:text>
</xsl:when>
</xsl:choose>
</xsl:template>
I want to extract comment content except <inline-figure> element. Could you please guide me that how to write code for it.
There's a lot of stuff in your XSLT that seems to bear no relationship to your stated requirement. Assuming you have an XSLT environment where disable-output-escaping is working, you should be able to simply do:
<xsl:template match="comment()">
<xsl:value-of select="." disable-output-escaping="yes"/>
</xsl:template>
<xsl:template match="comment()[contains(., 'inline-figure')]">
<xsl:copy/>
</xsl:template>
I can't see what the rest of your logic is trying to achieve.
Note also that disable-output-escaping generally works only when writing final serialized output from the transformation, not when writing to a variable.
Asuming XSLT 3.0 (as supported by Saxon 9.8 or Altova 2017/2018) you could use
<xsl:mode on-no-match="shallow-copy"/>
<xsl:template match="document//comment()[not(contains(., 'inline-figure'))]">
<xsl:copy-of select="parse-xml-fragment(.)"/>
</xsl:template>
Related
Need to write XSpec test case to test the XSLT, in which multiple modes are used for transformation.
But with below test-case, the xspec only tests the output with default mode applied.
I wonder if there is a way to test the final output of the transformation.
<!-- input.xml -->
<body>
<div>
<p class="Title"><span>My first title</span></p>
<p class="BodyText"><span style="font-weight:bold">AAAAAAA</span><span>2 Jan 2020</span></p>
</div>
</body>
<!-- conv.xsl -->
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<!-- default mode : adding text-align attribute where #class=Title -->
<xsl:template match="*[ancestor::body]">
<xsl:choose>
<xsl:when test="#class = 'Title'">
<xsl:element name="{local-name()}">
<xsl:copy-of select="#* except #style"/>
<xsl:attribute name="text-align" select="'center'"/>
<xsl:apply-templates/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:element name="{local-name()}">
<xsl:copy-of select="#*"/>
<xsl:apply-templates/>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- bodytext mode : changing element name to <title> where p[#class=Title] -->
<xsl:template match="p[#class]" mode="bodytext">
<xsl:choose>
<xsl:when test="#class = 'Title'">
<title>
<xsl:copy-of select="#* except #class"/>
<xsl:apply-templates mode="bodytext"/>
</title>
</xsl:when>
<xsl:otherwise>
<para>
<xsl:apply-templates mode="bodytext"/>
</para>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="body">
<xsl:variable name="data">
<body>
<xsl:copy-of select="#*"/>
<xsl:apply-templates/>
</body>
</xsl:variable>
<xsl:apply-templates select="$data" mode="bodytext"/>
</xsl:template>
<xsl:template match="node() | #*" mode="#all">
<xsl:copy>
<xsl:apply-templates select="node() | #*" mode="#current"/>
</xsl:copy>
</xsl:template>
O\P for first <p>:
-- after default mode applied: <p class="Title" text-align="center">. [below xspec tests this o\p]
-- final: <title text-align="center">. [Want to test this o\p]
<!-- test.xspec -->
<x:description xmlns:x="http://www.jenitennison.com/xslt/xspec" stylesheet="conv.xsl">
<x:scenario label="XSS00001: Testing 'p[#class=Title]' converts to 'title'">
<x:context href="input.xml" select="/body/div[1]/p[1]"/>
<x:expect label="Testing 'p' converts to 'title'">
<title text-align="center">
<span>My first title</span>
</title>
</x:expect>
</x:scenario>
</x:description>
Any suggestion in this regard would be a great help. Thanks...
I don't think it is solely the use of the modes that doesn't give you the result you want. However, the way you have set up the modes in your XSLT, if you match on that /body/div[1]/p[1] in the XSpec test scenario, you will get the stylesheet applied to only that p element. And obviously for that p there is the match on *[ancestor::body] in the unnamed mode and processing stops in that mode as the other mode is never used from that template.
So you might need to make the body element the context and use a scenario like the following:
<x:scenario label="XSS00002: Testing 'p[#class=Title]' converts to 'title'">
<x:context>
<body>
<div>
<p class="Title">...</p>
<p class="BodyText">...</p>
</div>
</body>
</x:context>
<x:expect label="Testing 'p' converts to 'title'">
<body>
<div>
<title text-align="center">...</title>
<para>...</para>
</div>
</body>
</x:expect>
</x:scenario>
Martin is quite right.
Another way of writing would be:
<x:scenario label="When a document contains 'body//p[#class=Title]'">
<x:context href="input.xml" />
<x:expect label="'p' is converted to 'title[#text-align]'"
test="body/div/title">
<title text-align="center">
<span>My first title</span>
</title>
</x:expect>
</x:scenario>
that is,
Remove #select from x:context, because you and/or conv.xsl seem to assume the transformation to start always from the document node (/).
Add #test to x:expect, because you seem to be interested only in the title element in the transformation result.
In my data it's possible that there are one or more processing-instructions which are used to give that specific block of content new attributes or overwrite the values of existing ones.
The way I do this requires it that the name of the PIs are valid xsl attribute names.
The Question: Is it possible to check within the xsl-stylesheet if the name of the PI is an actual valid (=allowed as <xsl:attribute name="*thisname*"> in XSL-FO) attribute name?
<xsl:if test="./processing-instruction()"> <!-- add condition to test for valid name? -->
<xsl:for-each select="./processing-instruction()">
<xsl:variable name="pi_name"><xsl:value-of select="local-name()" /></xsl:variable>
<xsl:attribute name="{$pi_name}"><xsl:value-of select="." /></xsl:attribute>
</xsl:for-each>
</xsl:if>
EDIT:
Regarding this problem: Check if xsl:attribute name is valid for XSL-FO
That's the code I use derived from Tony's solution:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template name="handle_block_attribute">
<xsl:variable name="attributes" select="document('PATH\attributelist.xml')//attributelist/attribute"/>
<xsl:if test=".//processing-instruction()">
<xsl:for-each select=".//processing-instruction()">
<xsl:variable name="pi_name"><xsl:value-of select="local-name()" /></xsl:variable>
<xsl:choose>
<xsl:when test="$pi_name = $attributes">
<xsl:attribute name="{$pi_name}">
<xsl:value-of select="." />
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<fo:inline color="red">Invalid attribute-name in PI: <xsl:value-of select="$pi_name" /></fo:inline><fo:block />
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
The template is called in this way:
<xsl:template match="para">
<fo:block xsl:use-attribute-sets="para.standard">
<xsl:call-template name="handle_block_attribute" />
<xsl:apply-templates/>
</fo:block>
</xsl:template>
And that's the data:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<book>
<sect class="hierarchic" type="chapter">
<para class="heading" style="Chapter">
<inline style="HeadingText">HD</inline>
</para>
<para style="Standard">Lorem ipsum dolor sit amet consectetuer eleifend consequat pede Aenean est. <?font-size 13pt?><?fotn-family Arial?><?color green?><?fline-height 3pt?></para>
<para style="Standard">Consequat semper tortor id convallis leo Phasellus eget non sagittis neque.</para>
</sect>
</book>
EDIT2:
Well, maybe there is a more elegant way, but it works: I'm going with two for-each-loops, one for the correct PIs and after that another one for the flawed ones including error-output.
<xsl:if test=".//processing-instruction()">
<xsl:for-each select=".//processing-instruction()">
<xsl:variable name="pi_name"><xsl:value-of select="local-name()" /></xsl:variable>
<xsl:if test="$pi_name = $attributes">
<xsl:attribute name="{$pi_name}">
<xsl:value-of select="." />
</xsl:attribute>
</xsl:if>
</xsl:for-each>
<xsl:for-each select=".//processing-instruction()">
<xsl:variable name="pi_name"><xsl:value-of select="local-name()" /></xsl:variable>
<xsl:if test="not($pi_name = $attributes)">
<fo:inline color="red">Invalid attribute name in PI: <xsl:value-of select="$pi_name" /></fo:inline><fo:block />
</xsl:if>
</xsl:for-each>
</xsl:if>
You've made it harder than necessary for yourself by using XSLT 1.0 instead of either XSLT 2.0 or XSLT 3.0 simply because it's harder to construct a list of property names to compare against. With the later versions, you could just make a sequence of strings. With XSLT 1.0, the simplest way (that I can remember) is to put each property name as the value of a separate element and then compare the prospective property name against the selected set of elements. The magic of XPath's = is that it is true if any value of one side matches any value on the other side.
You can extract the definitive list of XSL-FO property names by processing the XML source for the XSL specification: http://www.w3.org/TR/2006/REC-xsl11-20061205/xslspec.xml
With this stylesheet:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0"
xmlns:fo="http://www.w3.org/1999/XSL/Format"
xmlns:local="uuid:503f6e96-fda1-4464-98b6-a60dbecc5946"
exclude-result-prefixes="local">
<properties xmlns="uuid:503f6e96-fda1-4464-98b6-a60dbecc5946">
<property>font-size</property>
<property>font-weight</property>
</properties>
<xsl:variable name="properties" select="document('')/*/local:properties/local:property"/>
<xsl:template match="fo:*">
<xsl:copy>
<xsl:copy-of select="#*" />
<xsl:for-each select="processing-instruction()">
<xsl:variable name="pi_name" select="local-name()" />
<xsl:choose>
<xsl:when test="$pi_name = $properties">
<xsl:attribute name="{$pi_name}">
<xsl:value-of select="." />
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:message>Unrecognised property: <xsl:value-of select="$pi_name"
/></xsl:message>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
<xsl:apply-templates />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
this document:
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
<fo:block><?font-size 16pt?><?font-weight bold?></fo:block>
<fo:block><?fotn-size 16pt?><?bogus bold?></fo:block>
</fo:root>
generates this:
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
<fo:block font-size="16pt" font-weight="bold"/>
<fo:block/>
</fo:root>
plus messages about 'fotn-size' and 'bogus'.
I put the property names in the stylesheet to make it easier to test. You could put the XML in an external document and not have to fuss with the local namespace. The namespace was necessary because the only non-XSLT elements allowed at the top level of an XSLT stylesheet have to be in a non-XSLT namespace.
As Martin Honnen has commented, PI target and attributes names must conform the Name production of XML specification. Thus, the only case when this might conflict is when a PI target could be interpreted as a QName and the prefix does not match an in scope namespace binding.
Because of that, : is not allowed in PI target.
Here is the errors given by different XSLT processor when parsing the input source:
<?a:b 7pt?>
<root/>
Saxon:
org.xml.sax.SAXParseException; systemId: urn:from-string; lineNumber:
1; columnNumber: 6; A colon is not allowed in the name 'a:b' when
namespaces are enabled.
MSXML:
Input parsing error: Entity names, PI targets, notation names and
attribute values declared to be of types ID, IDREF(S), ENTITY(IES) or
NOTATION cannot contain any colons. , 1:7, <?a:b 7pt?>
Edit: about <?fotn-size 7pt?>, this stylesheet following your example
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<root>
<xsl:if test="./processing-instruction()">
<xsl:for-each select="./processing-instruction()">
<xsl:variable name="pi_name">
<xsl:value-of select="local-name()" />
</xsl:variable>
<xsl:attribute name="{$pi_name}">
<xsl:value-of select="." />
</xsl:attribute>
</xsl:for-each>
</xsl:if>
</root>
</xsl:template>
</xsl:stylesheet>
With this input:
<?fotn-size 7pt?>
<root/>
Output:
<root fotn-size="7pt"/>
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
Input is like as,
<section counter="yes" level="5">
<title><target id="page92"/></title>
<section counter="yes" level="6">
<title>Standard 12-lead ECG at Rest</title>
<para>The standard ECG is recorded at rest using 12 leads in order to collect as much information as possible:</para>
<listing type="dash">
<litem><para>Standard limb leads according to Einthoven (I, II, III)</para></litem>
Output should be,
<section counter="yes" level="5">
<title><target /></title>
<section counter="yes" level="6">
<title>Standard 12-lead ECG at Rest</title>
<para id="page92">The standard ECG is recorded at rest using 12 leads in order to collect as much information as possible:</para>
<listing type="dash">
<litem><para>Standard limb leads according to Einthoven (I, II, III)</para></litem>
We wrote xslt as shown below,
<xsl:template match="para[1][parent::section[parent::section[not(normalize-space(title))]]]">
<xsl:choose>
<xsl:when test="position() = 1">
<para>
<xsl:attribute name="id" select="ancestor::section[not(normalize-space(title))]/title/target/#id"/>
<xsl:apply-templates select="#*"/>
<xsl:apply-templates/>
</para>
</xsl:when>
<xsl:otherwise>
<para>
<xsl:apply-templates select="#*|node()"/>
</para>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
While using above xslt, we are unable to meet expected output.
<section counter="yes" level="5">
<title><target /></title>
<section counter="yes" level="6">
<title>Standard 12-lead ECG at Rest</title>
<para id="page92">The standard ECG is recorded at rest using 12 leads in order to collect as much information as possible:</para>
<listing type="dash">
<litem><para id="page92">Standard limb leads according to Einthoven (I, II, III)</para></litem>
The "page ID" value is repeating on following paragraphs which we didn't required. We need to maintain the page ID only on 1st paragraph.
Could you please guide us.
As for getting the result you want, it should be as simple as
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="section[not(normalize-space(title))]/section/para[1]">
<xsl:copy>
<xsl:attribute name="id" select="ancestor::section[not(normalize-space(title))]/title/target/#id"/>
<xsl:apply-templates select="#*"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="section/title[not(normalize-space())]/target/#id"/>
</xsl:transform>
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.