I have a stylesheet I’m using with a perl module that only works with XSLT 1.0. I want to create a JSON array inside a JSON dictionary so I need proper comma seperation for the elements. I’m parsing an XHTML table where there are 1 or more spans in the second cell. So for-each select=”./tr” and then for-each select=”./td[1]/span” or something like that.
After changing it a little it behaves as expected as Ian said it would.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" version="1.0" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" doctype-public="-//W3C//DTD HTML 4.01//EN" encoding="UTF-8" />
<xsl:template match="text()">
</xsl:template>
<xsl:template match="table/tbody">
<xsl:text>[</xsl:text>
<xsl:for-each select="./tr[not(@class='no-results')]">
<xsl:text>{"</xsl:text>
<xsl:value-of select="normalize-space(.//strong)" />
<xsl:text>":{"ingredients":{</xsl:text>
<xsl:for-each select=".//div[@class='reagent-list']//a[@class='item-link reagent']">
<xsl:value-of select="substring(./@href, 14)" />:<xsl:value-of select="normalize-space(./span[1])" />
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:text>}</xsl:text>
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
<xsl:text> </xsl:text>
</xsl:for-each>
<xsl:text>]</xsl:text>
</xsl:template>
</xsl:stylesheet>
I realize that the stylesheet does not match the xml below. The actual document is huge. I hope you understand what I mean, though. I just made this up:
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
<th>c</th>
</tr>
</thead>
<tbody>
<tr>
<td>foo</td>
<td><span>an element</span></td>
<td>bar</td>
</tr>
<tr>
<td>foo</td>
<td><span>an element</span><span>an element</span></td>
<td>bar</td>
</tr>
<tr>
<td>foo</td>
<td><span>an element</span><span>an element</span><span>an element</span><span>an element</span></td>
<td>bar</td>
</tr>
</tbody>
</table>
=>
{
"Row one":["an element"],
"Row two":["an element", "an element"],
"Row three":["an element", "an element", "an element", "an element"]
}
Instead I get this:
{
"Row one":["an element",],
"Row two":["an element", "an element",],
"Row three":["an element" "an element" "an element" "an element"]
}
I’ve been using position() and last() in a test tag to print a comma and it seems to work correctly for the outer loop, but how do I tell my test tag to use the inner for-each scope when printing the commas that seperate the array?
As mentioned by @IanRoberts, it is difficult to give targeted assistance without seeing what your existing XSLT looks like.
That said, here is a solution that is push-oriented (i.e., no
<xsl:for-each>) and does not requirelast().When this XSLT:
…is run against the provided XML:
…the wanted result is produced:
Explanation:
The first template matches the root element. It’s purpose is to apply templates to that element’s
<tr>grandchildren and sandwich those results between{and}(adding newlines as appropriate).The second template matches
<tr>elements. It outputs row information and is instructed to apply templates to all<span>children of the second<td>element (again, sandwiching the results between braces and other text as necessary).NOTE: instead of using
last(), you’ll see that the first element outputs a comma, followed by a newline, if the position of this<tr>in the current context is greater than 1. This has the same effect of applying commas correctly; it’s merely a different way of looking at the same problem (and is what I use because it seems more efficient to me 😉 ).NOTE: to make this solution more extensible, you’ll see that I’m not statically outputting the words
"one","two", etc. in each row. Instead, at the top of the XSLT, I’ve defined a<my:ones>element to hold onto the text values of each “ones” number. When processing each<tr>, I use the position of that<tr>in the current context to retrieve the correct<num>element’s value. I’ve left it as an exercise to the reader, but it would indeed be possible to define<my:tens>,<my:hundreds>, etc. to scale this solution up to potentially large numbers of rows.The final template matches
<span>elements. Again, it uses an<xsl:if>element to test whether the position of this<span>in the current context is greater than 1; if so, a comma (followed by a space) is output. After that, we merely concatenate two"symbols with the value of the span sandwiched in-between.