tags:

views:

208

answers:

3

Is it possible for me to have a conditional in XSLT such that I find and replace only the FIRST tag of a particular tag name?

For example, I have an XML file with many <title> tags. I would like to replace the first of these tags with <PageTitle>. The rest should be left alone. How would I do this in my transform? What I currently have is this:

<xsl:template match="title">
     <PageTitle>
       <xsl:apply-templates />
     </PageTitle>
</xsl:template>

which finds all <title> tags and replaces them with <PageTitle>. Any help would be greatly appreciated!

+1  A: 

This one should work:

<xsl:template match="title[1]">
     <PageTitle>
       <xsl:apply-templates />
     </PageTitle>
</xsl:template>

But it matches first title in every context. So in the following example, both /a/x/title[1] and /a/title[1] will get matched. So you might want to specify something like match="/a/title[1]".

<a>
    <x>
        <title/> <!-- first title in the context -->
    </x>
    <title/> <!-- first title in the context -->
    <title/>
    <c/>
    <title/>
</a>
Krab
This template will be applied to every `title` element that is the first child of its parent -- not to the first `title` element in the document.
Dimitre Novatchev
Well, if you would have actually read the answer, this comment wouldn't be necessary... Hint: try reading the second sentence, starting with "But".
Krab
@Krab: The second part of your solution still doesn't select the first `title` element in some specific XML documents. What if it is not known in advance that the name of the top element will be `a`? Or, if it is not known if the first `title` element will be a child of the top element? Do you need a specific example, or can you find it yourself?
Dimitre Novatchev
+2  A: 

If all the title tags are siblings, you can use:

<xsl:template match="title[1]">
    <PageTitle>
        <xsl:apply-templates />
    </PageTitle>
</xsl:template> 

However, this will match all title elements that are the first child of any node. If titles may have different parent nodes, and you only want the first title in the entire document to be replaced with PageTitle, you can use

<xsl:template match="title[not(preceding::title or ancestor::title)]">
    <PageTitle>
        <xsl:apply-templates />
    </PageTitle>
</xsl:template>
markusk
title[not(preceding::title)] will fail if the first title element itself contains a title element. title[not(preceding::title or ancestor::title)] would be better.
Alohci
Fair point, I've modified the pattern accordingly.
markusk
I see that you corrected this solution: now it is OK and I reversed my downvote :)
Dimitre Novatchev
+3  A: 

The first title element in the document is selected by:

(//title)[1]

Many people mistakenly think that //title[1] selects the first title in the document and this is a frequently committed error. //title[1] selects every title element that is the first title child of its parent -- not what is wanted here.

Using this, the following transformation produces the required output:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match=
  "title[count(.|((//title)[1])) = 1]">

     <PageTitle>
       <xsl:apply-templates />
     </PageTitle>
 </xsl:template>
</xsl:stylesheet>

When applied on this XML document:

<t>
 <a>
  <b>
    <title>Page Title</title>
  </b>
 </a>
 <b>
  <title/>
 </b>
 <c>
  <title/>
 </c>
</t>

the wanted result is produced:

<t>
 <a>
  <b>
    <PageTitle>Page Title</PageTitle>
  </b>
 </a>
 <b>
  <title />
 </b>
 <c>
  <title />
 </c>
</t>

Do note how we use the well-known Kaysian method of set intersection in XPath 1.0:

If there are two nodesets $ns1 and $ns2, the following expression selects every node which belongs to both $ns1 and $ns2:

$ns1[count(.|$ns2) = count($ns2)]

In the specific case when both node-sets contain only one node, and one of them is the current node, the following expression evaluates to true() exactly when the two nodes are identical:

count(.|$ns2) = 1

A variation of this is used in the match pattern of the template that overrides the identity rule:

title[count(.|((//title)[1])) = 1]

matches only the first title element in the document.

Dimitre Novatchev
It would be good if you also commented on the difference between `(//title)[1]` and `//title[1]` as that's not the most obvious thing.
Donal Fellows
@Donal-Fellows: Done.
Dimitre Novatchev
@krab, `title[not(preceding::title)]` may not match the first `title` element in some XML documents. Do you need an example, or are you able to find it yourself? :)
Dimitre Novatchev
@Dimitre, are you referring to the possibility of nested title elements like Alohci pointed out, and the need for checking for ancestor::title as well, or are there other issues with the approach?
markusk
@markusk: yes, the ancestor axis needs to be checked, too.
Dimitre Novatchev