tags:

views:

57

answers:

4

I am traversing an XML file (that contains multiple tables) using XSLT. Part of the job of the page is to get the title of each table, and present that title with along with the number of items that table contains (i.e. "Problems (5)").

I am able to get the number of items, but I now need to separate the sections with 0 (zero) items in them, and put them at the bottom of the list of table titles. I'm having trouble with this because the other items with positive numbers need to be left in their original order/not sorted.

Here is the code for the list of titles:

<ul>
  <xsl:for-each select="n1:component/n1:structuredBody/n1:component/n1:section/n1:title">
     <li style="list-style-type:none;">
     <div style = "padding:3px"><a href="#{generate-id(.)}">
     <xsl:variable name ="count" select ="count(../n1:entry)"/>
     <xsl:choose>
        <xsl:when test = "$count != 0">
           <xsl:value-of select="."/> (<xsl:value-of select="$count"/>)
        </xsl:when>
        <xsl:otherwise>
           <div id = "zero"><xsl:value-of select="."/> (<xsl:value-of select="$count"/>)</div>
        </xsl:otherwise>
     </xsl:choose>
     </a>
     </div>
     </li>
   </xsl:for-each>
</ul>

Right now, the "zero" div just marks each link as gray.

Any help regarding how to place the "zero" divs at the bottom of the list would be greatly appreciated. Thank you!

Edited code using zero/nonzero templates:

<xsl:for-each select="n1:component/n1:structuredBody/n1:component/n1:section/n1:title">
   <li style="list-style-type:none;">
      <div style = "padding:3px"><a href="#{generate-id(.)}">
         <xsl:apply-templates select="n1:title[count(../n1:entry) != 0]" mode="nonzero" />
         <xsl:apply-templates select="n1:title[count(../n1:entry) == 0]" mode="zero" />
      </a>
      </div>
   </li>
</xsl:for-each>
+1  A: 

You can create two templates to process the nodes with a mode attribute on each. A mode="zero" and a mode="nonzero", if you like.

<xsl:template match="n1:title" mode="zero">
  ...
</xsl:template>
<xsl:template match="n1:title" mode="nonzero">
  ...
</xsl:template>

Call the nonzero template, selecting items with a count &gt; 0 and the zero template with count = 0.

<xsl:apply-templates select="n1:title[count(../n1:entry) &gt; 0]" mode="nonzero" />
<xsl:apply-templates select="n1:title[count(../n1:entry) = 0]" mode="zero" />

Place the call to the nonzero template above the call to the zero template.

Oded
Thanks for the quick response! I tried your suggestion but I must be missing something because I received an error ("Error loading stylesheet: XPath parse failure: Name or Nodetype test expected"). I made an edit with the new code; please let me know if I'm making a mistake. Thanks again (:
danielle
@danielle - You need to change your `foreach`, so it doesn't have the `n1:title` part, as the `apply-templates` at this point will not be able to select them.
Oded
Okay, so, after some rearranging I can tell that the templates are differentiating which items have zero or nonzero counts, but when the list is printed, it is still in the original order instead of from nonzero to zero; despite calling the templates in order. This pertains to the answer below as well--is there a way to preserve the integrity of the template functionality while getting rid of the <xsl:for-each> statement(I assume this is the problem)?
danielle
@danielle - It is very difficult to tell without seeing the complete XSL, sample of source XML, current output and expected output.
Oded
After combining with parts of the answer below, it works! Thank you so much.
danielle
+1  A: 

You should traverse them seperately. You can accomplish this by using xsl:apply-templates instead of xsl:for-each

<xsl:template match=".... something ....">
  <ul>
    <xsl:apply-templates select="n1:component/n1:structuredBody/n1:component/n1:section/n1:title[count(../n1:entry) != 0]"/>
    <xsl:apply-templates select="n1:component/n1:structuredBody/n1:component/n1:section/n1:title[count(../n1:entry) = 0]"/>
  </ul>

</xsl:template>

<xsl:template match="n1:title" >
  <li style="list-style-type:none;">
    <div style = "padding:3px"><a href="#{generate-id(.)}">
      <xsl:variable name ="count" select ="count(../n1:entry)"/>
      <xsl:choose>
        <xsl:when test = "$count != 0">
          <xsl:value-of select="."/> (<xsl:value-of select="$count"/>)
        </xsl:when>
        <xsl:otherwise>
          <div id = "zero"><xsl:value-of select="."/> (<xsl:value-of select="$count"/>)</div>
        </xsl:otherwise>
      </xsl:choose>
    </a>
    </div>
  </li>

</xsl:template>
Jan Willem B
Hi, thanks for the input! I entered your code but I received an error "Error during XSLT transformation: An XPath expression was expected to return a NodeSet." I'm not sure where it would be expecting a nodeset from--is the error indicative of anything I might have missed?
danielle
sorry, there was an error in my answer (misplaced brackets). I edited the apply-templates statements to correct it
Jan Willem B
I took your traversing method and used the "mode" from above, it works perfectly. Thanks very much
danielle
+1  A: 

No mode is necessary and no conditional logic (<xsl:choose>).

Also, the transformation can use just one <xsl:apply-templates>:

<xsl:apply-templates select="n1:title>
  <xsl:sort select="count(../n1:entry) = 0" data-type="number"/>
</xsl:apply-templates>

Here are the two templates to be used:

<!-- There is one or more ../n1:entry -->
<xsl:template match="n1:title"[../n1:entry] >  
  ...  
</xsl:template> 

<!-- No ../n1:entry exists --> 
<xsl:template match="n1:title"[not(../n1:entry)]">  
  ...  
</xsl:template> 
Dimitre Novatchev
+1 for not using a <xsl:for-each/> I tried your code and could not get the <xsl:sort/> statement to work until I applied the template to the <section/> element. Also, I was not able to make sorting work with @data-type="number". I do not know how XSL handles booleans, but it appears to use 'true' and 'false'. Anyway, it was fun dissecting your answer. I'm including my version. Thank you, Dimitre.
Zachary Young
+1  A: 

Hi Danielle,

You probably are happy with what you've got, but I liked Dimitre's answer and wanted to make it work just for my own learning.

I've simplified (what I believe to be is) the input data:

<component>
  <section>
    <title>table 1</title>
    <entry/>
  </section>
  <section>
    <title>table 2</title>
  </section>
  <section>
    <title>table 3</title>
    <entry/>
    <entry/>
  </section>
</component>

and the resulting stylesheet:

<xsl:stylesheet
    version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

  <xsl:output method="html" indent="yes"/>

  <xsl:template match="/component">
    <ul>
      <xsl:apply-templates select="section">
        <xsl:sort select="count(entry) = 0"/>
      </xsl:apply-templates>
    </ul>
  </xsl:template>

  <xsl:template match="section[entry]">
    <li>
      <xsl:value-of select="title"/>
      <xsl:text> (</xsl:text>
      <xsl:value-of select="count(entry)"/>
      <xsl:text>)</xsl:text>
    </li>
  </xsl:template>

  <xsl:template match="section[not(entry)]">
    <li>
      <div id="zero"><xsl:value-of select="title"/></div>
    </li>
  </xsl:template>
</xsl:stylesheet>

Here is the output I get:

<ul>
   <li>table 1 (3)</li>
   <li>table 3 (2)</li>
   <li>
      <div id="zero">table 2</div>
   </li>
</ul>

Thanks again to Dimitre for suggesting the solution he did. I like <xsl:apply-templates/> over <xsl:for-each/> because they feel more in the vein of XSL being declarative and are just a little bit more uncomfortable for me coming from an imperative scripting and programming background.

Thank you,
Zachary

Zachary Young
I am glad my answer was useful. +1.
Dimitre Novatchev