Search code examples
xsltxpathxslt-1.0docbookdocbook-5

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?


Solution

  • 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.