idiotprogrammer
idiotprogrammer

Reputation: 371

making compound xpath statement for xslt: match

Here's a generic XSLT 1.0 question which I need to know to write an XSLT statement for processing docbook xml files. In my docbook XML, I'm trying to write a compound xpath statement in XSLT 1.0 that says, hardcode a new attribute "class = "play" for p tags in html output.

I want this action to be done for every <para> tag which does NOT have these attributes

  1. role="normal-play-paragraph" AND
  2. role ="no-indent" AND
  3. "role="line-verse"

Here is my XML source:

<chapter xmlns="http://docbook.org/ns/docbook" 
 xmlns:xlink="http://www.w3.org/1999/xlink"
        version="5.0" xml:id="play">
  <title> Hamlet </title>

    <para role="no-indent"> SPHINX. Do you think about it very much?</para>
    <para role="normal-play-para"> INTERVIEWER. I do so say. </para>
    <para>SPHINX. Hello </para>
    <para> INTERVIEWER. dddddWhy I do so say. </para>
    <para> SPHINX. Yes. </para>
    <para role="line-verse"> Cosmologists have theorized or guessed</para>
</chapter>

I want the HTML output to look like this after Docbook XSLT processes it:

<html>
<body>
<p class="no-indent">SPHINX. Do you think about it very much? much. </p>
 <p class="normal-play-para"> INTERVIEWER. I do so say. </p>
   <p class="play">SPHINX. Hello </p>
   <p class="play">INTERVIEWER. dddddWhy I do so say. </p>
   <p class="play">SPHINX. Yes.  </p>
 <p class="line-verse"> Cosmologists have theorized or guessed</p>
</body>
<html> 

The docbook xslt has 2 mechanisms at work which you don't really need to know about.

First, in <para role=""> elements, the value of role is changed into class of p. This is the default behavior. Second, I'm using a special mode to hardcode a "class='play'" into p tags.

<xsl:template match="d:chapter[@xml:id = 'play']/d:para" mode="class.attribute" >
  <xsl:param name="class" select="local-name(.)"/>
  <xsl:attribute name="class">play</xsl:attribute>
</xsl:template>

However, I want class="play" to be hardcoded only when there are other attributes & values NOT present. I can modify the above statement to exclude all para tags with the attribute role="line-verse" :

  <xsl:template match="d:chapter[@xml:id = 'play']/d:para[@role != 'line-verse']" mode="class.attribute" >
    <xsl:param name="class" select="local-name(.)"/>
    <xsl:attribute name="class">play</xsl:attribute>
  </xsl:template>    

But I need more than that. I want to exclude not only role= "line-verse," but also role="no-indent" and role="normal-play-para".

So I have to change the value of the xpath statement in the match attribute so that it excludes three attribute values. I haven't the foggiest idea how to do that. Does anybody know? Thanks.

Update about Answer:

First, I want to thank all of you for taking the time to understand my question and formulate an answer. I should mention that I am still a novice on this stuff, and also, my question was a little unfair because I am using some sophisticated/complicated Docbook XSL. Therefore I need an answer that doesn't cause collisions with the Docbook XSL stylesheets. Also, I realize that you wrote transformations that may be perfectly valid answers in generating html output if I were not also importing the docbook xsl.

The answer which I chose as "best" here may not be the most elegant, but simply the one that worked for me in the case when I am importing the epub3 docbook-ns stylesheets. So Mr. Rishe's one line answer actually does exactly I need it to do even if it isn't as elegant.

I really don't know what's going on in this customization which I started out with:

<xsl:template match="d:chapter[@xml:id = 'play']/d:para" mode="class.attribute" >
  <xsl:param name="class" select="local-name(.)"/>
  <xsl:attribute name="class">play</xsl:attribute>
</xsl:template>

What I do know is that it's invoking a <xsl:template name="generate.class.attribute"> which is found here. http://50.56.245.89/xsl-ns/xhtml-1_1/html.xsl

Another thing. Dimitre Novatchev's 2 answers looks as though they would work. By the way, you forgot to include the <xsl:param name="class" select="local-name(.)"/> statement -- which is easily fixed -- and that solution works.

However, Dimitre, I have another question. The second answer you gave used variables, which looks simple and functional. If I try it, my Saxon 6.5 parser gives a validation error. (E [Saxon6.5.5] The match pattern in xsl:template may not contain references to variables). Maybe it's something simple like a typo. But is it possible that variables are not allowed in XSLT 1.0 template matches?

Upvotes: 2

Views: 466

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243479

One possible solution is:

  <xsl:template mode="class.attribute" match=
  "d:chapter[@xml:id = 'play']
       /d:para[not(@role = 'line-verse' 
                  or @role = 'no-indent' 
                  or @role = 'normal-play-para'
                   )]"  >
    <xsl:attribute name="class">play</xsl:attribute>
  </xsl:template>

However, I would use a more flexible and extensible solution, that allows easy modification of the "non-play" values:

 <xsl:param name="pNonPlayVals">
  <val>line-verse</val>
  <val>no-indent</val>
  <val>normal-play-para</val>
 </xsl:param>

  <xsl:template mode="class.attribute" match=
  "d:chapter[@xml:id = 'play']/d:para
         [not(@role = document('')/*/xsl:param[@name='pNonPlayVals']/val)]"  >
    <xsl:attribute name="class">play</xsl:attribute>
  </xsl:template>

Upvotes: 1

JLRishe
JLRishe

Reputation: 101700

Could you give this a try:

<!-- Special handling for paras with one of the three roles -->
<xsl:template 
  match="d:chapter[@xml:id = 'play']/d:para[@role = 'line-verse' or @role = 'normal-play-para' or @role - 'line-indent']" 
  mode="class.attribute" >
  <xsl:attribute name="class">
      <xsl:value-of select="@role" />
  </xsl:attribute>
</xsl:template>

<!-- Other paras get the default class "play" -->
<xsl:template match="d:chapter[@xml:id = 'play']/d:para" mode="class.attribute">
  <xsl:attribute name="class">play</xsl:attribute>
</xsl:template>

One step further would be to have the <xsl:attribute> in the template that's calling these templates, and just have the needed value in the class.attribute templates themselves. Something like this:

<xsl:template match="d:chapter[@xml:id = 'play']/d:para">
    <p>
      <xsl:attribute name="class">
         <xsl:apply-templates select="." mode="class.attribute" />
      </xsl:attribute>
    ...
    </p>
</xsl:template>

<!-- Special handling for paras with one of the three roles -->
<xsl:template 
  match="d:chapter[@xml:id = 'play']/d:para[@role = 'line-verse' or @role = 'normal-play-para' or @role - 'line-indent']" 
  mode="class.attribute" >
      <xsl:value-of select="@role" />
</xsl:template>

<!-- Other paras get the default class "play" -->
<xsl:template match="d:chapter[@xml:id = 'play']/d:para" mode="class.attribute">
  <xsl:text>play</xsl:text>
</xsl:template>

To specifically answer your original question, if you really needed a template that specifically matches paras that don't have one of those @role values, you could match on this XPath:

d:chapter[@xml:id = 'play']/d:para[not(@role = 'line-verse' or @role = 'normal-play-para' or @role - 'line-indent')]

But I think the approach I've presented above (treat paras those roles as the special case, and treat everything else as the default) is the better way to go.

Upvotes: 2

Related Questions