Search code examples
xmlxsltxpathxslt-2.0tei

Use One Template for Attribute (by Value) and Another for the (Parent) Node


I am getting this ambiguous match warning on a largish (420 lines) XSL transformation of a large (TEI-flavored) XML file (~6000 lines) (using Saxon-HE 9.5.1.6J on OS X). I'd like to understand (and fix) the warning.

 Recoverable error 
  XTRE0540: Ambiguous rule match for /TEI/text[1]/group[1]/text[1]/body[1]/lg[33]/head[2]
 Matches both "tei:lg[@type='poem']/tei:head" on line 103 of
  file: hs2latex.xsl
 and "*[@rend='italics']" on line 110 of
  file: hs2latex.xsl

The XML looks something like:

<lg type='poem'>
<head rend='italics'>Sonnet 3<head>
...
</lg>

With the conflicting XSL rules looking something like this:

<xsl:template match="tei:lg[@type='poem']/tei:head">
...
<xsl:apply-templates />
</xsl:template>

and

<xsl:template match="*[@rend='italics']"><!-- blah blah --></xsl:template>

Since an attribute is just another node, I thought I could match against it separately. But if I have just an attribute in my match, I get an error, so I put the used to asterisk to match all nodes with rend='italics' attributes, which then produces the ambiguous error quoted above.

Is it possible to do what I am trying here, namely to use one template to match attributes based on value (regardless of the type of element)? I am interested in having a single template handle any element with, for instance, a "@rend='italics'" attribute.

I tried to reproduce this problem with a minimal working example, but came up with a slightly different example (which perhaps go to exactly what I'm misunderstanding).

Minimal Working XML

<?xml version="1.0" encoding="utf-8"?>

<document>
  <book>
    <title>"One Title"</title>
  </book>

  <book>
    <title rend="italics">Another Title</title>
  </book>
</document>

and Minimal XSLT

<?xml version="1.0" encoding="utf-8"?>

<xsl:stylesheet version="2.0" 
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
        exclude-result-prefixes="xsl">
  <xsl:output omit-xml-declaration="yes" />
  <xsl:template match="/">
    <xsl:apply-templates />
  </xsl:template>

  <xsl:template match="*[@rend='italics']">
    <italics><xsl:apply-templates /></italics>
  </xsl:template>

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

</xsl:stylesheet>

This minimal example (which I thought produces an identical situation to the one I describe above) does not produce an ambiguous match error, but instead results in this output:

<title>"One Title"</title>
<italics>Another Title</italics>

What I wanted (in this minimal example) would be:

<title>"One Title"</title>
<title><italics>Another Title</italics></title>

I suspect I am misunderstanding something basic about XSLT or about XPath, but I am at this point at a loss and would appreciate any guidance. Many thanks.


Solution

  • Is it possible to do what I am trying here, namely to use one template to match attributes based on value (regardless of the type of element)? I am interested in having a single template handle any element with, for instance, a "@rend='italics'" attribute.

    Yes, that is possible. The problem in your code is that you have two matching templates of the form NodeTest[predicate], that both take the same priority by default. If you want one to take precedence over the other, you should add a priority="X" , where X is any number. I.e.:

    <xsl:template match="*[@rend='italics']" priority="2">
      <italics><xsl:apply-templates /></italics>
    </xsl:template>
    

    This minimal example (which I thought produces an identical situation to the one I describe above) does not produce an ambiguous match error, but instead results in this output:

    Correct. That is because in XSLT, default values are assigned to the priorities based roughly on the complexity of the match pattern. Just a NodeTest has lower precedence than a NodeTest[predicate].

    Since an attribute is just another node, I thought I could match against it separately.

    Yes, you can. You didn't show what you tried with the attribute matching, but it should look something like this: match="@rend" or match="@rend[. = 'italics']". However, be aware that attributes are special nodes. You need to specifically apply templates to attributes to be able to match them. Also, the node that has the focus will be the attribute node itself, so you may have to walk the parent axis to get the same results you are currently having.

    What I wanted (in this minimal example) would be:

    What you seem to want is that when the more generic match matches, you want the specific match to be applied as well to the same node. Any node matches at most one matching template. To have one node match multiple templates, you can use the xsl:next-match instruction. However, this works from specific (which is matched first) to generic (which is matched last).

    In your case, you want the reverse. I would do something like this, which gives the output you expect (all title-elements match the title template first, because of the explicit priority, and only the italics elements also match the italics template, adding the <italics> to the output):

    <xsl:template match="/">
        <xsl:apply-templates />
    </xsl:template>
    
    <xsl:template match="*[@rend='italics']">
        <italics><xsl:apply-templates /></italics>
    </xsl:template>
    
    <xsl:template match="title" priority="2">
        <title><xsl:next-match /></title>
    </xsl:template>
    

    You may want to apply a similar coding pattern to your larger example, otherwise, the <italics> is the only one that is matched, and I think you want both to match there as well, and in the right order (generic first, then specific).