XSL Sort on Autogenerated and User entered Titles - xslt

I have a document that contains paragraphs with a mixture of user authored titles and autogenerated titles that I need to have sorted alphabetically for a table of contents.
The data looks like
<theory><text>Stuff goes here</text></theory>
<general><text>More stuff</text></general>
<paragraph><title>First user title</title><text>Lots of stuff</text>
<subparagraph><title>Subordinate Paragraph</title><text>The last stuff.</text></subparagraph>
</paragraph>
<paragraph><title>Last user title</title><text>Just a little stuff</text>
</paragraph>
The expected output is
3. First User Title............
2. General Information.........
4. Last User Title.............
3.1 Subordinate Paragraph......
1. Theory of Operation.........
I can't figure out how to get it to sort on the literal values for some paragraphs and the titles when they exist. Right now I have:
<xsl:for-each select="paragraph|theory|general|paragraph/subparagraph">
<xsl:sort select="name()"/>
<xsl:sort select="title"/>
<xsl:apply-templates select="." mode="TOC"/>
</xsl:for-each>
but it outputs as:
2. General................
3. First User Title.......
4. Last User Title........
1. Theory of Operation....
3.1 Subordinate Paragraph.

You could handle this in a more general way by using template rules to compute the sort key:
<xsl:for-each select="paragraph,theory,general">
<xsl:sort>
<xsl:apply-templates select="." mode="sort-key"/>
</xsl:sort>
<xsl:apply-templates select="." mode="TOC"/>
</xsl:for-each>
<xsl:template match="paragraph" mode="sort-key" as="xs:string">
<xsl:sequence select="string(title)"/>
</xsl:template>
<xsl:template match="theory|general" mode="sort-key" as="xs:string">
<xsl:sequence select="string(text)"/>
</xsl:template>

I think you have two issues.
You only want to use the name() if it doesn't have a title
You want to sort case-insensitive
You could change your sort to something like this (XSLT 2.0 or greater):
<xsl:sort select="lower-case((title,name())[1])"/>

Related

In XSLT, how do I use a for-each loop?

If I had a bunch of fields on a screen, say ten for first name and ten for last name, and they're named firstName1, firstName2, etc., and lastName1, lastName2, etc., how would I create a loop that goes through each last name field?
Right now, I have it set up to perform a task ten times, one for each last name field. How can I set up a for-each loop that goes through lastName1, lastName2, lastName3...lastName10 and does a specific task for each of them?
Input XML:
<Arguments>
<EnteredBy>SDSADFSADF</EnteredBy>
<IDNumber>WERWEW</IDNumber>
<Book1>Y</Book1>
<LastName1>ASDFASFASDFASFA</LastName1>
</Arguments>
XSLT:
<xsl:apply-templates select="/myQuery/Arguments/lastName1"/>
and there are lastName2 through lastName10, as well.
I want to loop through each of the ten and truncate the last names to five characters.
The simplest and most idiomatic form of iteration in XSLT is to use xsl:apply-templates on the set of elements you want to handle. If you don't see how to use that idiom to solve your problem, it's worth spending time working to learn it.
[Addendum:]
For example:
<xsl:template match="/myQuery/Arguments">
...
<xsl:element name="Arguments">
<xsl:apply-templates/>
</
...
</
<xsl:template match="lastName1 | lastName2 | lastName3
| ... | lastName9">
<xsl:element name="{name()">
<xsl:value-of select="substring(.,1,5)"/>
</
</
This assumes you have elements names lastName1, lastName2, etc., instead of the simpler idiom where all last names are called (wait for it!) lastName, and that what you want in this particular part of the document is a near-identity transform.
XSLT does also have an xsl:for-each, which can be regarded as syntactic sugar for xsl:apply-templates and is sometimes preferred by programmers with a procedural bent. If you think of it as analogous to a loop in an imperative language, however, your instincts will eventually fail you and you will be surprised because your attempts at variable mutation don't work the way you expected them to.
Systematic work through a good book or tutorial will pay off.
Here's what I got to work:
<xsl:template match="/">
<xsl:element name="Arguments">
<xsl:apply-templates />
</xsl:element>
</xsl:template>
<xsl:template match="Arguments">
<xsl:for-each select="./*[contains(name(), 'LastName')]">
<xsl:call-template name="test">
</xsl:call-template>
</xsl:for-each>
</xsl:template>
<xsl:template name="test">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
This way you don't need to make 9 calls, just one per loop.
Or if you want to avoid the for-each, you can do this instead:
<xsl:template match="Arguments/*[contains(name(), 'LastName')]">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>

XSLT; Group 2 or more different elements BUT only when adjacent

Initially this seemed a trivial problem, but it seems to be harder than I thought.
In the following xml, I want to group adjacent 'note' and p elements only. A note should always start a notegroup and any following p should be included. No other elements are allowed in the group.
From this:
<doc>
<note />
<p/>
<p/>
<other/>
<p/>
<p/>
</doc>
To this:
<doc>
<notegroup>
<note />
<p/>
<p/>
</notegroup>
<other/>
<p/>
<p/>
</doc>
Seems ridiculously easy, but the rule is: 'note' and any following 'p'. Any p on their own are to be ignored (as in the last 2 p above)
With xslt 2.0, if I try something like:
<xsl:for-each-group select="*" group-adjacent="boolean(self::note) or boolean(self::p)">
fails because it also groups the two later p elements.
Or the 'starts-with' approach on the 'note' which seems to indiscriminately group any element after (instead of just the p elements).
The other approach I'm considering is to simply add an attribute to each note and the p that immediately follow the note, and using that to group later, but how can I do that?
Thanks for any answers
I think you can do this by being a bit creative with group-starting-with, essentially you want to start a new group whenever you see an element that is not one that belongs in a notegroup. In your example this would generate two groups - note+p+p and other+p+p, the trick is to only wrap a notegroup around groups where the initial item is a note, and to simply output groups that are not headed by a note unchanged
<xsl:for-each-group select="*" group-starting-with="*[not(self::p)]">
<xsl:choose>
<xsl:when test="self::note">
<notegroup>
<xsl:copy-of select="current-group()"/>
</notegroup>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
This will wrap all note elements in a notegroup even if they don't actually have any following p elements. If you don't want to wrap a "bare" note then make it <xsl:when test="self::note and current-group()[2]"> to trigger the wrapping only when the current group has more than one member.
If you have more than one element name that can be part of a notegroup then you could either list them all in the predicate
<xsl:for-each-group select="*" group-starting-with="*[not(self::p|self::ul)]">
or declare a variable holding the node names that can be part of a notegroup:
<xsl:variable name="notegroupMembers" select="(xs:QName('p'), xs:QName('ul'))" />
and then say
<xsl:for-each-group select="*"
group-starting-with="*[not(node-name(.) = $notegroupMembers)]">
taking advantage of the fact that an = comparison where one side is a sequence succeeds if any of the items in the sequence match.
Issue
You're group on either <note> or <p>. Hence the failing.
Hint
You can try by using group-starting-with="note",as describe in Grouping With XSLT 2.0 # XML.com:
<xsl:template match="doc">
<doc>
<xsl:for-each-group select="*" group-starting-with="note">
<notegroup>
<xsl:for-each select="current-group()">
<xsl:copy>
<xsl:apply-templates select="self::note or self::p" />
</xsl:copy>
</xsl:for-each>
</notegroup>
</xsl:for-each-group>
</doc>
</xsl:template>
You may need another <xsl:apply-templates /> around the end.

Multi layer conditional wrap HTML with XSLT

In my Umbraco CMS site, I'm making a list of node "widgets" for content editors to use, with a list of many options they can toggle to change the display. This often involves wrapping an element with an anchor, div, or something else.
Using XSLT to display these from the XML output, I've put together what a kludge approach as I'm a very new XSLT beginner.
What I've come to as a solution is multiple nested apply-templates. This creates a large list of conditions, often asking repeat checks, which trees out pretty huge. It's a bit of a nightmare to manage.
It looks as such (but with more than two options in each choose):
<xsl:template match="/">
<xsl:choose>
<xsl:when test="type='1'">
<xsl:apply-templates select="widgetContent" mode="type_1" />
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="widgetContent" mode="type_default" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="wigetContent" mode="type_1">
<xsl:choose>
<xsl:when test="./wrap_with_hyperlink != 0">
<xsl:element name="a">
<xsl:apply-templates select="." mode="hyperlink_wrapped" />
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="widgetContent" mode="not_hyperlink_wrapped" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
What can I do to reduce this tangled mess? I've structured the conditions to be as top down as much as possible, but there are definitely repeated checks where type_2 has to ask the same questions as type_1 all over again.
(edit: clarity) Because the design is basically an onion, type_1 is wrapped by certain tags, type_2 is wrapped by different tags. Next layer in, both could be wrapped by the same tags, and so forth. What would be perfect is:
<xsl:if test="this_wrap_style = 1"><xsl:element name="abc"></xsl:if>
<xsl:if test="this_wrap_style = 2"><xsl:element name="xyz"></xsl:if>
(everything else)
</abc> //if it exist.
</xyz> //etc
Which definitely doesn't work.
Some of this has been reduced by using Umbraco Doc Types for different widget controls, but part of the nature is that to the ideal structure for content editors is selecting a box widget will give them 5 different types of widget boxes (or more) to choose from, and a coherent back end isn't as important.
Thank you all for your time.
<!--Store data in processing instruction nodes in a separate XML file-->
<?xml version='1.0' encoding="utf-8"?>
<root>
<?_1 div?>
<?_2 p?>
</root>
type_1 is wrapped by certain tags, type_2 is wrapped by different tags.
<xsl:variable name="divider" select="document('condition.xml')//processing-instruction(concat('_', $type) )" />
<xsl:variable name="equalizer" select="'span'"/>
<xsl:element name="{$divider}">
...
</xsl:element>
Next layer in, both could be wrapped by the same tags
<xsl:if test="contains('1,2',$type)">
<xsl:element name="{$equalizer}">
...
</xsl:element>
</xsl:if>

I need to build a string from a list of attributes (not elements) in XSLT

want to make a comma-delimited string from a list of 3 possible attributes of an element.
I have found a thread here:
XSLT concat string, remove last comma
that describes how to build a comma-delimited string from elements. I want to do the same thing with a list of attributes.
From the following element:
<myElement attr1="Don't report this one" attr2="value1" attr3="value2" attr4="value3" />
I would like to produce a string that reads: "value1,value2,value3"
One other caveat: attr2 thru attr4 may or may not have values but, if they do have values, they will go in order. So, attr4 will not have a value if attr3 does not. attr3 will not have a value if attr2 does not. So, for an attribute to have a value, the one before it in the attribute list must have a value.
How can I modify the code in the solution to the thread linked to above so that it is attribute-centric instead of element-centric?
Thanks in advance for whatever help you can provide.
This is easy in principe, but only if it is really clear which attribute you want to exclude. Since attributes are not by definition ordered in XML (in contrast the elements), you need to say how the attribute(s) to skip can be identified.
Edit: Regarding the attribute order, XML section 3.1 says:
Note that the order of attribute specifications in a start-tag or empty-element tag is not significant.
That said, something like this should do the trick (adjust the [] condition as you see fit):
<xsl:template match="myElement">
<xsl:for-each select="#*[position()!=1]">
<xsl:value-of select="." />
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
In XSLT 2.0 it is as easy as
<xsl:template match="myElement">
<xsl:value-of select="#* except #att1" separator=","/>
</xsl:template>
I would appreciate Lucero's answer .. He definitely has nailed it ..
Well, Here is one more code which truncates attribute attr1, which appears at any positions other than 1 in attribute list.
scenario like this::
<myElement attr2="value1" attr3="value2" attr4="value3" attr1="Don't report this one" />
Here is the XSLT code .. ::
<xsl:template match="myElement">
<xsl:for-each select="#*[name()!='attr1']">
<xsl:value-of select="." />
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
The output will be:
value1,value2,value3
If you wish to omit more than one attribute say .. attr1 and attr2 ..
<xsl:template match="myElement">
<xsl:for-each select="#*[name()!='attr1' and name()!='attr2']">
<xsl:value-of select="." />
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
The corresponding output will be:
value2,value3

XSLT Apply sort to second value if first is empty

I have a for each which loops round news item nodes. Among other properties these news items have two attributes for created date. System added date and a user entered created date (to override the system date). I would like the list sorted by created date with the preference on the user entered date.
Below is my humble invalid attempt!
<xsl:for-each select="$currentPage/ancestor-or-self::node /node [#nodeTypeAlias = $documentTypeAlias and string(data [#alias='umbracoNaviHide']) != '1']">
<xsl:choose>
<xsl:when test="data [#alias = 'createdDate'] != ''">
<xsl:variable name="sort" select="string(data [#alias = 'createdDate'])"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="sort" select="string(#createDate)"/>
</xsl:otherwise>
</xsl:choose>
<xsl:sort select="$sort" order="descending"/>
Many thanks
<xsl:sort select="(data[#alias='createdDate' and normalize-space() != '']|#createDate)[last()]" order="descending" />
This statement creates a nodeset with the two nodes containing date, and get the last one according the document order to do the sorting. If data node exists and is not empty, it will be used for the sorting because child elements of an element occur after its attribute nodes.
concat() can only work, and in a few cases, if you use text sorting; it will fail with numeric sorting.
Right, seems like a hack but I have been able to achieve this by using a concat with the sort.
Example below
<xsl:for-each select="$currentPage/ancestor-or-self::node /node [#nodeTypeAlias = $documentTypeAlias and string(data [#alias='umbracoNaviHide']) != '1']">
<xsl:sort select="concat(data [#alias = 'createdDate'],#createDate)" order="descending"/>
To test if a node is empty (or omitted) in XSLT:
<xsl:when test="not(string(name))">...</xsl:when>
<!-- or -->
<xsl:when test="not(name)">...</xsl:when>
Many thanks to Erlock for his solution. I did struggle for a while to get this working in my version of Umbraco (4.7.1) due to the changes made to the Umbraco XSLT syntax.
For anyone interested, my working sample would change Erlock's code to become;
<xsl:sort select="(current()/createdDate[normalize-space() != '']|#createDate)[last()]" order="descending" />