Re: [xsl] XML to HTML: How to make decisions based on what has been display ed

Subject: Re: [xsl] XML to HTML: How to make decisions based on what has been display ed
From: Wendell Piez <wapiez@xxxxxxxxxxxxxxxx>
Date: Wed, 29 Oct 2003 18:49:08 -0500
Hi Constantin,

At 05:58 PM 10/29/2003, you wrote:
I am creating the stylesheet for displaying info from fairly large XML files
(1 to 2 MB). The output is HTML and I'm using the namespace. I have to alternate
background colors of every row in a table. The FAQ list has a
straightforward answer that I've used:

        <xsl:when test="position() mod 2 = 0">
                <xsl:attribute name="BGCOLOR">white</xsl:attribute>
                <xsl:attribute name="BGCOLOR">gainsboro</xsl:attribute>

This should work, right? Wrong! The problem I have is that I'm not selecting
every element for the output tree. For instance, if I have five elements,
and for some reason (immaterial for the purpose of this question) I want to
select only elements 1, 3 and 5, they'll all end up being the same color.

Ah, but alas, it isn't immaterial -- or I shouldn't say that: *why* you're only selecting some elements is immaterial, but *that* you are selecting only some, and which ones you are selecting, is very material.

You've found the position() test doesn't work since having selected a superset of the elements you want, the positions of the ones you want within that set (ordered by the selection) are all wrong.

The solution is either not to use position() for your test (there are often other ways to do it), or to make the selection in such a way that you're only selecting the nodes you want, not selecting a bunch and then throwing some away.

Let me show you what I mean:

source XML:

  <element status="okay">Whee</element>
  <element status="okay">Hoo</element>
  <element status="not okay">Boo!</element>
  <element status="okay">Yip</element>
  <element status="okay">Zing</element>


<xsl:template match="set">

<xsl:template match="element">
  <xsl:if test="@status = 'okay'">
    <xsl:value-of select="."/>
    <xsl:text> </xsl:text>
    <xsl:value-of select="position()"/>

... run that and you'll get positions 1, 2, 4, 5. (Unless you fail to strip whitespace from the <set> element so the whitespace-only text nodes aren't selected, in which case you'll probably get 2, 4, 8, 10 -- which is also wrong.)

-- but --

<xsl:template match="set">
  <xsl:apply-templates select="element[@status='okay']"/>

<xsl:template match="element">
  <xsl:value-of select="."/>
  <xsl:text> </xsl:text>
  <xsl:value-of select="position()"/>

The positions come out right, since only the nodes you actually wanted were selected.

I know your case will be more complex than this, but it illustrates the general idea.

idea is that if I can store somehow the value of the previous background, I
can look at it and select the next one. I looked at xsl:variable, but you
can't change (or reassign) that value. I'd like to know if this can be done
at all and if so, how? If it can't be done, the source XML files will have
to be changed.

It can't be done this way -- XSLT does not depend on, and does not use, the state of its own internal processing to control what it does. Rather, it rewards a "synchronous" view of all-the-document-at-once. Assume any processing of any nodes can occur in any order, and you'll be closer to the truth. (And bizarre as it may seem, this is actually a good thing.)

On the other hand, you may be too quick to conclude that if it can't be done by switching a flag back and forth in your processor, it can't be done at all -- witness the example above. So it may not be necessary to change your XML source.

Even in the worst cases, it's always possible to work in two passes: one pass filters the data, the second one adds the colors.

Also, I have to display in the HTML document the name - only once - of an
element who has at least one descendant satisfying certain conditions. The
way I solved that was to use xsl:for-each on the descendants, and select the
name of the element only on the first descendant satisfying the condition:

<!-- Display the test name only if the test has numeric results -->
<xsl:for-each select="preceding-sibling::ResultList/Element/Numeric">
        <!-- Display the test name only for the first encountered numeric
result (guarantees exactly one display of the test name) -->
        <xsl:if test="../preceding-sibling::Element/Numeric=false()">
                        <FONT FACE="ARIAL">
                                <xsl:if test="../Status = ' Passed'">
                                <xsl:if test="../Status = ' Failed'">

That worked just fine until somebody threw at me an XML file that had some
additional elements defeating the purpose my checks to determine the "first"
descendant. Again, if I would have a way of knowing if the ancestor name has
already been displayed, I would be done. Wouldn't have to determine the
first descendant any more, just get a match on every descendant but display
only if the ancestor name hasn't been displayed. Can this be done?

Again, it can't be done this way -- but it's usually possible to develop much more robust ways of checking and de-duplicating than the above.

One of the most elegant ways of de-duplicating, usually found in grouping algorithms (which present the same set of problems), is to use keys. You might want to look into resources on groupring in XSLT to learn more about this. You will find Google to be a big help, since this is a well-documented problem area. And see, as always, on XSLT grouping.


====================================================================== Wendell Piez mailto:wapiez@xxxxxxxxxxxxxxxx Mulberry Technologies, Inc. 17 West Jefferson Street Direct Phone: 301/315-9635 Suite 207 Phone: 301/315-9631 Rockville, MD 20850 Fax: 301/315-8285 ---------------------------------------------------------------------- Mulberry Technologies: A Consultancy Specializing in SGML and XML ======================================================================

XSL-List info and archive:

Current Thread