2

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.

Abel
  • 56,041
  • 24
  • 146
  • 247
cforster
  • 577
  • 2
  • 7
  • 19

2 Answers2

3

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

Abel
  • 56,041
  • 24
  • 146
  • 247
  • Thanks for this *excellent* answer. I want to go try a couple of things and then I'll make it approved. – cforster Aug 18 '14 at 16:40
  • Thanks very much. The key for me is: "be aware that attributes are special nodes. You need to specifically apply templates to attributes to be able to match them." If I understand you, this means that code that would work for: `<italics>...</italics>` cannot be made to work for `` with _just_ a syntax change. Intellectually, `xsl:next-match` seems ugly to me, but if I understand you, it is my only real option. – cforster Aug 18 '14 at 17:07
1

Abel has already given a good answer, the reason I am writing another answer is:

...but if I understand you, it is my only real option.

Far from it - as is often the case with programming, there is more than one way to do it. I would not go as far as saying that xsl:next-match is ugly, but it undermines a principle that is present in many stylesheets, namely that each node is processed only once and that a single suitable template is found for it.

Another option would be to match the attributes that should be turned into elements enclosing the text content in a separate template:

<xsl:template match="title/@*">
  <xsl:element name="{.}">
     <xsl:apply-templates select="../text()"/>
  </xsl:element>
</xsl:template>

If I understand correctly, this is what you tried to do in the first place. The template above matches any attribute of title elements. Then, a new element is constructed, its name corresponding to the attribute value (in this case "italic"). Finally, the built-in template for text nodes is applied to process the text content of the parent node of the attribute.

Stylesheet

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

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

  <xsl:strip-space elements="*"/>

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

  <xsl:template match="title/@*">
    <xsl:element name="{.}">
        <xsl:apply-templates select="../text()"/>
    </xsl:element>
  </xsl:template>

  <xsl:template match="title">
    <title>
        <xsl:choose>
            <xsl:when test="@*">
                <xsl:apply-templates select="@*"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:apply-templates/>
            </xsl:otherwise>
        </xsl:choose>
    </title>
  </xsl:template>

</xsl:stylesheet>

As an aside note, you do not need to exclude the XSLT namespace from the result tree using exclude-result-prefixes="xsl". Anything prefixed with xsl: is recognized as an instruction to be carried out and the namespace is excluded by default from the resulting XML.

XML Output

<document>
   <title>"One Title"</title>
   <title>
      <italics>Another Title</italics>
   </title>
</document>
Mathias Müller
  • 22,203
  • 13
  • 58
  • 75