Selecting the last (closest) element with specific child only with XSLT - xslt

I need to check all elements to see if there is a preceding element (not always a sibling) that has any PI element. If so, I need to assign an attribute to all following elements UNTIL the next PI element is found.
Example XML Input:
<chapter id = "1">
<section>
<heading>some text here</heading>
<p>some text here<?Test1?></p>
</section>
<section>
<heading>some text here</heading>
<p>some text here<?Test2?></p>
</section>
</chapter>
<chapter id="2">
<section>
<p></p>
</section>
</chapter>
In this example, the chapter element with the id = "2" should get an attribute named "test2" and then stop (so it should not also get "test1".
What I have now:
<xsl:key name="test1PIs" match="*[not(self::main|self::title)]/processing-instruction('test1')" use="following::*/#id"/>
<xsl:key name="test2PIs" match="*[not(self::main|self::title)]/processing-instruction('test2')"
use="following::*/#id"/>
<xsl:template match="*">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:if test="key('test1PIs',#id)">
<xsl:attribute name="test1"/>
</xsl:if>
<xsl:if test="key('test2PIs',#id)">
<xsl:attribute name="test2"/>
</xsl:if>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
What I end up with:
<chapter id="2" test1="" test2="">
**child elements here**
</chapter>
What I really want:
<chapter id="2" test2="">
**child elements here**
</chapter>
#Michael Kay's response got me where I needed to be. In final:
<xsl:template match="*">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:variable name="piParent"
select="preceding::processing-instruction()[1]"/>
<xsl:if test="$piParent">
<xsl:attribute name="processPI">
<xsl:value-of select="name($piParent)"/>
</xsl:attribute>
</xsl:if>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
It works by assigning an attribute to all elements following a specific PI until it reaches another PI.

I think you want something like:
<xsl:template match="*">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:variable name="piParent"
select="preceding::*[processing-instruction()][1]"/>
<xsl:if test="$piParent">
<xsl:attribute name="name($piParent/processing-instruction())"/>
</xsl:if>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
However, I'm guessing a little because you haven't specified the task very clearly.

Related

XSLT: in a <p> element, how to replace a line break (<br/>) with successive <alinea> element?

I am trying to transform a <p> element with <br/> elements within into several <alinea>subtext</alinea>. For instance :
<p>subtext<br/>some more text<br/> some more subtext</p>
From the previous <p> I was hoping to simply replace all <br/> into </alinea><alinea> since the template for <p> opens a <alinea> element already.
<xsl:template match="p">
<para><alinea><xsl:apply-templates/></alinea></para>
</xsl:template>
<xsl:template match="br">
</alinea><xsl:apply-templates/><alinea>
</xsl:template>
But it is doesn't validate.
Expected result :
<para>
<alinea>
subtext
</alinea>
<alinea>
some more text
</alinea>
<alinea>
some more subtext
</alinea>
</para>
This is quite easy to do in XSLT 2.0:
<xsl:template match="p">
<para>
<xsl:for-each-group select="node()" group-starting-with="br">
<alinea>
<xsl:copy-of select="current-group()[not(self::br)]" />
</alinea>
</xsl:for-each-group>
</para>
</xsl:template>
Demo: https://xsltfiddle.liberty-development.net/6r5Gh2Q
Prob you can use something like this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="p">
<para>
<xsl:apply-templates/>
</para>
</xsl:template>
<xsl:template match="text()">
<aline>
<xsl:value-of select="."/>
</aline>
</xsl:template>
<xsl:template match="br"/>
<xsl:template match="div">
<blockquote>
<xsl:value-of select="."/>
</blockquote>
</xsl:template>
</xsl:stylesheet>
I found a (dirty?) way to replace <br> with </alinea><alinea> :
<xsl:template match="br">
<xsl:value-of disable-output-escaping="yes">
</alinea><alinea>
</xsl:value-of>
</xsl:template>
Anything prettier ?

XSLT Gouping, for-each and implicated loop. How to eliminate duplicates?

Hope find a guru's help to figure out the next problem.
I have two xml files. Firts one here (text.xml):
<text>
<ref>Author1, Title1, Date1</ref>
<ref>Author75, Title75, Date2</ref>
<ref>Author2, Title2, Date2</ref>
<ref>Author3, Title3, Date3</ref>
<text>
And the second one like this (list.xml):
<list>
<bibl xml:id="1"><author>Author1</author><date>Date1</date></bibl>
<bibl xml:id="2"><author>Author2</author><date>Date2</date></bibl>
<bibl xml:id="3"><author>Author3</author><date>Date3</date></bibl>
</list>
I want to query text.xml and check against list.xml to add #xml:id (from list.xml) to <ref> (from text.xml) wich contain same Author and Date. If not, then just copy original <ref>.
So I want to obtain:
<ref xml:id="1">Author1, Title1, Date1</ref>
<ref>Author75, Title75, Date2</ref>
<ref xml:id="2>Author2, Title2, Date2</ref>
etc.
My XSLT identify well all correpondence:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="ref">
<xsl:variable name="ref" select="."/>
<xsl:for-each select="document('list.xml')//bibl">
<xsl:variable name="bibl" select="."/>
<xsl:variable name="author" select="author"/>
<xsl:variable name="date" select="date"/>
<xsl:choose>
<xsl:when test="contains($ref, $author) and contains($ref, $date)">
<ref>
<xsl:attribute name="xml:id">
<xsl:value-of select="$bibl/#xml:id"/>
</xsl:attribute>
<xsl:value-of select="$ref"/>
</ref>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$ref"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
But, then there aren't correpondence it's not just copy right <ref>, but copy all <ref> the number of time I have <bibl> nodes in the second file.
So problem is in <xsl:otherwise><xsl:copy-of select="$ref"/></xsl:otherwise>.
Any ideas how I can obtain only this distinct value I need? I know it's must be very simple actually and I try key, generate-id, for-each-group, distinct-values, but can't figure it out.
The problem is that you are creating a ref element for each iteration of the for-each loop whether there is a match or not.
What you need to do in this case is create the ref element outside of the for-each and then only create the id attribute for the matching element inside the loop
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="ref">
<xsl:variable name="ref" select="."/>
<ref>
<xsl:apply-templates select="#* "/>
<xsl:for-each select="document('list.xml')//bibl">
<xsl:variable name="bibl" select="."/>
<xsl:variable name="author" select="author"/>
<xsl:variable name="date" select="date"/>
<xsl:choose>
<xsl:when test="contains($ref, $author) and contains($ref, $date)">
<xsl:attribute name="xml:id">
<xsl:value-of select="$bibl/#xml:id"/>
</xsl:attribute>
</xsl:when>
</xsl:choose>
</xsl:for-each>
<xsl:apply-templates select="node()"/>
</ref>
</xsl:template>
</xsl:stylesheet>
When applied to your sample XML, the following is output
<text>
<ref xml:id="1">Author1, Title1, Date1</ref>
<ref>Author75, Title75, Date2</ref>
<ref xml:id="2">Author2, Title2, Date2</ref>
<ref xml:id="3">Author3, Title3, Date3</ref>
</text>
However, your current method is not very efficient, as for each ref element you are iterating over all bibl elements. Another approach would be to extract the author and date from the ref elements, and then look up the bibl element directly
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="ref">
<xsl:variable name="author" select="normalize-space(substring-before(., ','))"/>
<xsl:variable name="date" select="normalize-space(substring-after(substring-after(., ','), ','))"/>
<ref>
<xsl:apply-templates select="#* "/>
<xsl:apply-templates select="document('list.xml')//bibl[author=$author][date=$date]"/>
<xsl:apply-templates select="node()"/>
</ref>
</xsl:template>
<xsl:template match="bibl">
<xsl:attribute name="xml:id">
<xsl:value-of select="#xml:id"/>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
This should also give the same results.
EDIT:
humm...too much focus on xsl syntax, i should have seen that earlier...
you have an implicated outer loop over each ref and an inner loop over each bibl. You generate one element for every bibl for every ref regardless of match or no match.
So, instead of the xsl:otherwise you need a check after the for-each loop to see if there was no match and do the copy-of if neccessary.
Not sure how to do the check, though..maybe using position() and count() of the generated <ref>s, sorry...don't have any more time to think about this right now.
not really an explanation, but a workaround:
<xsl:otherwise>
<ref>
<xsl:value-of select="$ref"/>
</ref>
</xsl:otherwise>
My guess is that the problem lies in $ref, which is not a xpath expression at that moment (if i remember xsl-t correctly)

Looping and recursion based on parameter passed

I have an XML organized like below-
<section name="Parent 1 Text here" ID="1" >
<section name="Child 1 Text here" ID="11">
</section>
<section name="Child 2 Text here" ID="12">
<section name="GrandChild Text here" ID="121" >
</section>
</section>
</section>
<section name="Parent 2 Text here" ID="2" >
<section name="Child 1 Text here" ID="22">
</section>
<section name="Child 2 Text here" ID="23">
<section name="GrandChild Text here" ID="232" >
</section>
</section>
</section>
I have to produce the below output XML -
<section name="Parent 1 Text here" ID="1" >
<section name="Child 2 Text here" ID="12">
<section name="GrandChild Text here" ID="121" >
</section>
</section>
</section>
<section name="Parent 2 Text here" ID="2" >
<section name="Child 2 Text here" ID="23">
</section>
</section>
I have to achive above using XSLT 1.0 transformation. I was planning to pass a comma separated string as a parameter with value= "1,12,121,2,23"
My question- How to loop the comma separated parameter in XSLT 1.0 ?
Is there a simpler way to achieve the above. Please remember I have to do this in XSLT 1.0
Your help is appreciated.
Recursion is not necessarily the solution. Basically your comma-seperated string can be applied as a filter:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:param name="filter">1,12,121,2,23</xsl:param>
<xsl:template match="section">
<xsl:if test="contains(concat(',', $filter, ','), concat(',', #ID, ','))">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:if>
</xsl:template>
<!-- copy the rest -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Here is an alternative to Jan Willem B's approach. I don't claim that it is either better or worse. I present it for two reasons:
You may want to see what a recursive approach would look like
It behaves differently for some input data (although it behaves the same for your example data). Specifically, if your filter includes two nodes that are both descendants of the same node, this stylesheet will output two nested data structures, each with one leaf node, while Jan Willem B's answer outputs one nested structure with two leaf nodes. Don't know which you want.
For my stylesheet, you would pass a filter listing only the leaf nodes that you want. It will find the ancestors. I have assumed for this that your root node is called <doc>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml"/>
<xsl:param name="descendant-ids" select="'121,23'"/>
<xsl:template match="/">
<doc>
<xsl:call-template name="recurse-ids">
<xsl:with-param name="ids" select="concat($descendant-ids,',')"/>
</xsl:call-template>
</doc>
</xsl:template>
<xsl:template name="recurse-ids">
<xsl:param name="ids"/>
<xsl:variable name="id" select="substring-before($ids,',')"/>
<xsl:variable name="remaining-ids" select="substring-after($ids,',')"/>
<xsl:apply-templates select="/doc/section">
<xsl:with-param name="id" select="$id"/>
</xsl:apply-templates>
<xsl:if test="$remaining-ids">
<xsl:call-template name="recurse-ids">
<xsl:with-param name="ids" select="$remaining-ids"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template match="section">
<xsl:param name="id"/>
<xsl:if test="starts-with($id,#ID)">
<xsl:copy>
<xsl:apply-templates select="#*|node()">
<xsl:with-param name="id" select="$id"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:if>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

Grouping several groups in XSLT 2

I'm trying to add hierarchy to some grotty extruded typesetting XML. I can't seem to manage grouping several kinds of groups in the same parent element at once.
What I have (simplified, obviously):
<article>
<h1>A section title here</h1>
<p>A paragraph.</p>
<p>Another paragraph.</p>
<bl>Bulleted list item.</bl>
<bl>Another bulleted list item.</bl>
<h1>Another section title</h1>
<p>Yet another paragraph.</p>
</article>
What I want:
<article>
<sec>
<h1>A section title here</h1>
<p>A paragraph.</p>
<p>Another paragraph.</p>
<list>
<list-item>Bulleted list item.</list-item>
<list-item>Another bulleted list item.</list-item>
</list>
</sec>
<sec>
<h1>Another section title</h1>
<p>Yet another paragraph.</p>
</sec>
</article>
This almost works for the list items:
<xsl:for-each-group select="*" group-adjacent="boolean(self::BL)">
<xsl:choose>
<xsl:when test="current-grouping-key()">
<list><xsl:apply-templates select="current-group()"/></list>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
but it only handles the very first list in an article; and as soon as I try to add another xsl:for-each-group to cover the sections, the list-item one stops working.
Ideas? Many thanks in advance!
Here is a sample stylesheet that produces the output you posted for the input sample you posted:
<xsl:stylesheet
version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:output indent="yes"/>
<xsl:template match="article">
<xsl:copy>
<xsl:for-each-group select="*" group-starting-with="h1">
<sec>
<xsl:copy-of select="."/>
<xsl:for-each-group select="current-group() except ." group-adjacent="boolean(self::bl)">
<xsl:choose>
<xsl:when test="current-grouping-key()">
<list>
<xsl:apply-templates select="current-group()"/>
</list>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</sec>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
<xsl:template match="bl">
<list-item>
<xsl:apply-templates/>
</list-item>
</xsl:template>
</xsl:stylesheet>

cdata in xslt for html

I have an XSLT file generating plain HTML. I need to wrap some elements in CDATA blocks, so intend to use cdata-section-elements. But, if the element I want to have contain CDATA is only one <p> on the page, how do I get it to not put CDATA in all the other <p> elements?
The input data is this:
<item>
...
<g:category>Gifts under &pound;10</g:category>
</item>
My XSL is:
<xsl:element name="a">
<xsl:attribute name="href">productlist.aspx</xsl:attribute>
<xsl:copy-of select="text()" />
</xsl:element>
I want this to render something like:
Gifts under £10
But all I get is:
Gifts under £10
Well assuming you have some way of targeting the <p> tag that you want to enclose in CDATA section, you could do something like:
<xsl:output method="xml" version="1.0" encoding="UTF-8"
indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="p[#test = 'test']">
<xsl:copy>
<xsl:text disable-output-escaping="yes"><![CDATA[</xsl:text>
<xsl:apply-templates/>
<xsl:text disable-output-escaping="yes">]]></xsl:text>
</xsl:copy>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
In this case all <p> tags with an attribute test = 'test' will have CDATA applied to them, all other tags will just be output as normal.
The code I have is:
<xsl:element name="a">
<xsl:attribute name="href">
product.aspx?prod=<xsl:copy-of select="title/text()" />
</xsl:attribute>
<xsl:text disable-output-escaping="yes"><![CDATA[</xsl:text>
<xsl:copy-of select="g:price" /> - <xsl:copy-of select="title/text()" />
<xsl:text disable-output-escaping="yes">]]></xsl:text>
</xsl:element>