Re: Newbie question on XSL and lists

Subject: Re: Newbie question on XSL and lists
From: Wendell Piez <wapiez@xxxxxxxxxxxxxxxx>
Date: Fri, 08 Sep 2000 12:13:05 +0100
At 10:50 AM 9/8/00 +0200, Michiel wrote:
...
>The algoritm should be something like...

This is an interesting exercise in rethinking procedural-style into
XSL-declarative style.

Basically your logic, as I see it, translates into:

>If <par-f> contains a <listing>
>    process the <listing>
>else
>   display the content of <par-f>
>endif

For all <par-f>, process the children

For all <listing> in <par-f>, process the children

For all children of <par-f> not <listing>, display contents


>if the <listing> contains <item>
>   process the <item>
>else
>  /* error */
>endif

For all <item> inside <listing>, process the children

For all children of <listing> not <item>, throw an error

>if the <item> contains a <par-f>
>   process the <par-f>
>else
>  /* error */
>endif

For all <par-f> inside <item>, process the children

For all children of <item> not <par-f>, throw an error

Collecting and reordering the rules, we have:

1. For all <par-f>, process the children
2. For all <par-f> inside <item>, process the children
3. For all <listing> in <par-f>, process the children
4. For all children of <par-f> not <listing>, display contents
5. For all children of <item> not <par-f>, throw an error
6. For all children of <listing> not <item>, throw an error
7. For all <item> inside <listing>, process the children

2. is subsumed by 1., so both can be expressed

<xsl:template match="par-f">
  <xsl:apply-templates/>
</xsl:template>

(As is happens, this is identical to the default template, so you shouldn't
need it -- but we'll come back to this.)

Since the default rule for processing in XSL ends up descending the tree
recursively until contents are displayed, 3. and 4. are also handled by it.

5. and 6. are the two "error-throwing" rules, which could look like:

<xsl:template match="item/*[not(self::par-f)] | listing/*[not(self::item]">
  <xsl:message>Out of place element not processed</xsl:message>
</xsl:template>

(It matches any element inside an item that is not a par-f, or any element
inside a listing that is not an item.)

But you know, this kind of validation is what DTDs are good at, so if your
input is already valid to the structure you want, you shouldn't need this
stuff.

That leaves 7., which ought to be pretty straightforward:

<xsl:template match="item">
  <xsl:apply-templates/>
</xsl:template>

(Since I don't see you'd have any items not inside lists, this just catches
all of them. Again, it's the default rule.)

So -- this is great, but it's still not going to give us the output we
want. We haven't looked at what we want to happen in presentation. There
are two rules we can state:

8. Items should each get a new line, be preceded with a "*", and indented
if they're deeper than one listing deep (to the number of listings)
9. The first par-f in an item should be on the same line as the "*"
generated by the item.
10. Subsequent par-f elements inside items should be on new lines and be
indented however many listings they are nested inside.

So we have to edit our rules:

For 8.:
<xsl:template match="item">
  <xsl:text>&#xA;</xsl:text>  <!-- a trick to get a new line -->
  <!-- but what do we do for the indent? -->
  <xsl:text>* </xsl:text>   <!-- here's our * -->
  <xsl:apply-templates/>  <!-- this will process our children -->
</xsl:template>

For 9. and 10.:
<xsl:template match="item/par-f">  <!-- only applies to par-f inside item -->
  <xsl:if test="position() &gt; 1">
    <!-- we only want new line and indent on non-first par-f -->
    <xsl:text>&#xA;</xsl:text>  <!-- the new line -->
    <!-- same problem with indenting -->
  </xsl:if>
  <xsl:apply-templates/>
</xsl:template>

Notice the special rule for par-f inside item. You were right: we've
disentangled rule 2. from 1. since it turns out they're different after all.

Okay, almost done ...

Now -- Chris is correct that beginners should stay away from xsl:for-each!
It seems to do something it doesn't do, if you think it has anything to do
with for-next loops. It's just a quick easy way to iterate over a node set
and do something for each node in it.

As it happens, however, it does provide us an elegant solution to our
indenting/nesting problem. We can do something like

<xsl:for-each select="ancestor::listing">...</xsl:for-each>

to do something (such as create white space) for each ancestor <listing>
... that is, accounting for how deep an element is in a listing structure.

Emending our two templates (first the par-f):

<xsl:template match="item/par-f">
  <xsl:if test="position() &gt; 1">
    <xsl:text>&#xA;</xsl:text>  <!-- the new line -->
    <xsl:for-each select="ancestor::listing">
      <xsl:text>  </xsl:text> <!-- gives us two spaces -->
    </xsl:for-each>
  </xsl:if>
  <xsl:apply-templates/>
</xsl:template>

Now for that to work, you'll need:

<xsl:strip-space elements="item"/>

...so loose whitespace nodes inside items in your source don't mess up your
counting.

For the item, it's a bit trickier, since we indent for ancestor listings
only after the first one:

<xsl:template match="item">
  <xsl:text>&#xA;</xsl:text>
  <xsl:for-each select="ancestor::listing[position() &gt; 1]">
    <xsl:text>  </xsl:text>
  </xsl:for-each>
  <xsl:text>* </xsl:text>
  <xsl:apply-templates/>
</xsl:template>

All this is assuming you are outputting to plain text. Use <xsl:output
method="text"/>. If you are creating a tag structure (after all, it really
seems you want HTML), you'll use <ul> elements or such like instead of all
this fancy indenting magic. In fact -- the method provided won't work when
you display in a browser, since it does whitespace munging and ruins all
your hard work!

So you can dump all the stuff to do the indenting, taking advantage of your
list-wrapper (<listing>) elements to do it for you in your browser:

<xsl:strip-space elements="item"/>

<xsl:template match="listing">
  <ul>
    <xsl:apply-templates/>
  </ul>
</xsl:template>

<xsl:template match="item">
  <li>
    <xsl:apply-templates/>
  </li>
</xsl:template>

<xsl:template match="item/par-f">
  <xsl:if test="position() &gt; 1">
    <br/>
  </xsl:if>
  <xsl:apply-templates/>
</xsl:template>

Simple, isn't it? Unless you want that error-catching, in which case you
saw what you could do.

Good luck,
Wendell


======================================================================
Wendell Piez                            mailto:wapiez@xxxxxxxxxxxxxxxx
Mulberry Technologies, Inc.                http://www.mulberrytech.com
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:  http://www.mulberrytech.com/xsl/xsl-list


Current Thread