XSLT: A sequence of more than one item is not allowed as the first argument of concat()

Viewed 22615

I am trying to generate a pagelist which consists of a concatenation of the id of various divs (@chapter)|(@part). My problem stems from nested structures: @part can contain any number of @chapter. I only want to select the id of the last ancestor of a page number in document order.

Below is a snippet of the XML:

<text>
    <div class="part" id="s9781483352947.i870">
        <a class="page" id="pbr-31"/>
        <h1 class="title">Part 1</h1>
        <p>This is a paragraph.</p>
        <p>This is a paragraph.</p>
        <div class="chapter" id="s9781483352947.n3.i884">
            <a class="page" id="pbr-34"/>
            <h1 class="title">Chapter 1</h1>
            <p>This is a paragraph</p>
        </div>
    </div>
</text>

And below is my XSLT:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="xs"
    version="2.0">

    <xsl:template match="/">
    <xsl:element name="nav">
        <xsl:element name="h1">
            <xsl:value-of select="'Pages'"/>
        </xsl:element>
        <xsl:element name="ol">
            <xsl:apply-templates mode="pagelist" select="//a[@class='page']"/>
        </xsl:element>
    </xsl:element>
    </xsl:template>

    <xsl:template match="a[@class='page']" mode="pagelist">
        <xsl:variable name="html" select="concat(ancestor::div[@class='chapter'][1]/@id | ancestor::div[@class='part'][1]/@id,'.xhtml#page')"/>
        <xsl:variable name="pagenumber" select="substring-after(@id,'-')"/>
        <xsl:element name="li">
            <xsl:element name="a">
                    <xsl:attribute name="href" select="concat($html,$pagenumber)"/>
                    <xsl:value-of select="substring-after(@id,'-')"/>
                </xsl:element>
        </xsl:element>
    </xsl:template>

</xsl:stylesheet>

However, I keep getting the error message: A sequence of more than one item is not allowed as the first argument of concat(). Obviously my XPath is selecting multiple strings in the first argument due to the @part and @chapters. How do I limit the selection to the first ancestor of a class='page'?

This is the desired ouput for the above sample:

<nav>
    <h1>Pages</h1>
    <ol>
        <li>
            <a href="s9781483352947.i870.xhtml#page31">31</a>
        </li>
        <li>
            <a href="s9781483352947.n3.i884.xhtml#page34">34</a>
        </li>
    </ol>
</nav>
2 Answers

First of all, the pipe symbol in Xpath isn't the "or" you are thinking of. What it does it combines two separate querys and returns them as one node-set. So what is happening, each of your queries for chapters and parts is returning one element (the @id attribute of the first node), and then its combined returning a node-set with two nodes. Hence why concat is having issues.

You could do:

(ancestor::div[@class='chapter'] | ancestor::div[@class='part'])[1]/@id

But it would be wrong!! As this will get the first in the combined series, but it won't merge them into the correct document order. So it would always get a chapter if available before getting a part.

What you need instead is this:

ancestor::div[@class='chapter' or @class='part'][1]/@id

I'm not sure why you are using concat on something you've already concated. As an aside if you are concating with a string and making an attribute, you can just surround it in curly braces to make the XSLT shorter, as in the below:

<xsl:template match="a[@class='page']" mode="pagelist">
    <xsl:variable name="html" select="ancestor::div[@class='chapter' or @class='part'][1]/@id"/>
    <xsl:variable name="pagenumber" select="substring-after(@id,'-')"/>
    <li>
        <a href="{$html}.xhtml#page{$pagenumber}">
            <xsl:value-of select="$pagenumber"/>
        </a>
    </li>
</xsl:template>

edit: If you wanted to continue using xsl:attribute elements you could use concat like in the following template:

<xsl:template match="a[@class='page']" mode="pagelist">
    <xsl:variable name="html" select="ancestor::div[@class='chapter' or @class='part'][1]/@id"/>
    <xsl:variable name="pagenumber" select="substring-after(@id,'-')"/>
    <li>
        <a>
            <xsl:attribute name="href">
                <xsl:value-of select="concat($html,'.xhtml#page',$pagenumber)"/>
            </xsl:attribute>
            <xsl:value-of select="$pagenumber"/>
        </a>
    </li>
</xsl:template>

Its a little bit more verbose, which is why I prefer inline attribute values.

I would suggest that you change the computation of the variable html to

<xsl:variable name="html" select="concat(ancestor::div[@class='chapter' or @class='part'][1]/@id, '.xhtml#page')"/>

By moving the check for the valid class into the brackets you will never get more that one node from the ancestor XPath expression.

Related