1

I would like to write some generic templates to transform collections of nodes into HTML lists. Each element of the collection should correspond to one list item. Ideally I would write

<xsl:apply-templates select="..." mode="ul"/>

along with a template which matches the individual elements in the selection, and the resulting HTML should look like

<ul>
  <li>Transformation of first element in selection</li>
  <li>Transformation of second element</li>
  ...
</ul>

That is, the content of each <li> is generated by a non-generic template; but the list structure itself is generated by a generic one. The problem is to write a generic template that produces this list structure for any non-empty collection, and no output for an empty collection.

I tried the following:

<xsl:template match="*" mode="ul">
  <xsl:if test="count(*) > 0">
    <ul>
      <xsl:apply-templates select="*" mode="li"/>
    </ul>
  </xsl:if>
</xsl:template>

<xsl:template match="*" mode="li">
  <li>
    <xsl:apply-templates select="." />
  </li>
</xsl:template>

But this doesn't work: each element of the collection will individually become a <ul>. Conceptually, what I want is a way to transform the collection itself into a <ul>, and then turn the elements of the collection into individual <li>s.

Important here:

  1. The test for the non-empty collection should be in the generic template, because I don't want to wrap every call to this template with a conditional, and I don't want to output empty <ul> elements when the collection is empty.

  2. In the XML documents I'm transforming, there is in general no common parent of the elements in the collection. That means I cannot transform the parent into the <ul> and its children into <li>s; there is no element in the source document which corresponds to the <ul>.

Is this possible? The searching I've done increasingly suggests to me that it isn't, but that seems insane to me, since this must an incredibly common use case.

wyleyr
  • 13
  • 2
  • A template matches on a single item or node, it is not clear what you refer to with a "collection". – Martin Honnen Jul 17 '21 at 13:26
  • By a "collection" I mean the set of nodes that match an XPath expression in, for example, the select="..." attribute of `apply-templates`. Is "selection" a better word for this? or "node set"? – wyleyr Jul 17 '21 at 15:05
  • In XSLT 1 it would be a node set, in later editions a sequence of nodes or items in general. – Martin Honnen Jul 17 '21 at 15:09

2 Answers2

1

At least in theory, you should be able to do something like this:

    <xsl:call-template name="ul">
        <xsl:with-param name="nodes" select="..."/>
    </xsl:call-template>

and then:

<xsl:template name="ul">
    <xsl:param name="nodes"/>
    <xsl:if test="$nodes">
        <ul>
            <xsl:apply-templates select="$nodes" mode="li"/>
        </ul>
    </xsl:if>
</xsl:template>

<xsl:template match="*" mode="li">
    <li>
        <xsl:apply-templates select="." />
    </li>
</xsl:template>

This would create a ul wrapper around the selected nodes before applying the li template to each element in the selection (which is, I think, what you call a collection).


Is this a worthwhile effort? Probably not. XML inputs come in a very wide variety of schemas, and rarely can a generic stylesheet fit all. Much easier to write a specific stylesheet that handles a known schema.

michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • Thanks. I agree that it's probably not worth the effort if one has to use a parameter like this -- unless the call can be a one-liner, it doesn't really save any lines of code, and just adds a level of indirection. But it bothers me to be writing this pattern all over the place without some way to abstract it out! – wyleyr Jul 18 '21 at 04:38
  • Well, it does accomplish the purpose of creating a "function" that you can call from anywhere and modify once to be applied to all. XSLT is naturally verbose (esp. XSLT 1.0) so "saving lines of code" is often not feasible. But in XSLT 2.0 or higher you could make it a true function and call it by a one-liner: https://xsltfiddle.liberty-development.net/3MP42Nh – michael.hor257k Jul 18 '21 at 05:06
  • Thanks for the fiddle; that seems like a nice way of doing it in XSLT 2.0. – wyleyr Jul 19 '21 at 13:35
1

In XSLT 3.0 (with XPath 3.1) you could do

<xsl:apply-templates select="array{...}" mode="ul"/>

and

<xsl:template match="." mode="ul">
    <ul>
      <xsl:apply-templates select="?*" mode="li"/>
    </ul>
</xsl:template>

<xsl:template match="*" mode="li">
  <li>
    <xsl:apply-templates select="." />
  </li>
</xsl:template>

By wrapping your "collection" into an array, you make it a single item, matched by a single invocation of a template rule, which can output the required <ul> element.

Michael Kay
  • 156,231
  • 11
  • 92
  • 164
  • It would make sense, then, to wrap the `ul` into an `xsl:where-populated`, to implement the initial attempt of `xsl:if test="count(*) > 0"`. – Martin Honnen Jul 17 '21 at 16:32
  • Thanks. This seems like what I'm looking for. There's nothing comparable before XSLT 3.0, though? – wyleyr Jul 18 '21 at 05:09
  • No, with earlier releases the call-template approach would be the way to go. – Michael Kay Jul 18 '21 at 07:22
  • Got it, thank you! I'll have to consider whether it's worth moving to XSLT 3.0 for this... Wish I could accept both answers! – wyleyr Jul 19 '21 at 13:36