Re: [xsl] Revision Marking in HTML

Subject: Re: [xsl] Revision Marking in HTML
From: Wendell Piez <wapiez@xxxxxxxxxxxxxxxx>
Date: Mon, 02 May 2005 14:11:00 -0400
Nadia,

Ah, I see now. This is hard (even harder than before) -- I might've said impossible except (a) nothing is impossible, and (b) even this isn't impossible if we use a couple of idioms developed by smart XSLers in the past.

The problem as it stands is that the nodes you want to group are not actually defined by the key. Expressed in English, the key allows you to select all the nodes that follow an insertion-mark-start up to (but not including) the next insertion-mark-start. But you only want those up to (but not including) the next insertion-mark-end. This is a complication to the problem for which David provided a known and tested solution.

One way to approach this would be to define a second key and use node-identity tests to sort out the right nodes. To keep the two keys straight, I'm renaming both:

<xsl:key name="by-last-insertion-start" match="node()"
use="generate-id(preceding-sibling::processing-instruction('xm-insertion_mark_start')[1])"/>

<xsl:key name="by-next-insertion-end" match="node()"
use="generate-id(following-sibling::processing-instruction('xm-insertion_mark_end')[1])"/>

Notice the second key is the "complement" of the first. The first enables you to grab all the following-sibling nodes to any insertion_mark_start that are not also following a later insertion_mark_start. The second works the same way, but looking backwards at preceding siblings up to the most recent insertion_mark_end.

Then when you use the key for your selection

<xsl:for-each select="processing-instruction('xm-insertion_mark_start')">
  <div class="revcontrol">
    <xsl:apply-templates select="key('p',generate-id())"/>
  </div>
</xsl:for-each>

rewrite this as

<xsl:for-each select="processing-instruction('xm-insertion_mark_start')">
  <xsl:variable select="this-insertion-end-id"
    select="generate-id(following-sibling::processing-instruction('xm-insertion_mark_end')[1])"/>
  <div class="revcontrol">
    <xsl:apply-templates select="key('by-last-insertion-start',generate-id())
      [count(.|(key('by-next-insertion-end',$this-insertion-end-id))) =
       count(key('by-next-insertion-end',$this-insertion-end-id))]"/>
  </div>
  <xsl:apply-templates select="key('by-last-insertion-start',generate-id())
    [count(.|(key('by-next-insertion-end',$this-insertion-end-id))) !=
     count(key('by-next-insertion-end',$this-insertion-end-id))]"/>
 </xsl:for-each>

This works by splitting the nodes returned by the first key into two groups. Inside the div, it selects those nodes that are also returned by the second key (using the correct end-marker), and so are in your group, and belong inside the div. After the div, it selects again, but this time grabs those nodes returned by the first key that are *not* also returned by the second (using the end-marker).

In order to do this we have to use quite a cumbersome test, in a predicate. For this test we have to know which end-marker "belongs" to our start-marker. We assume that this will be the next following-sibling insertion-mark-end, and bind that to a variable, $this-insertion-end.

The test works as follows: we want all those nodes whose immediately preceding insertion start is this one, but whose immediately following insertion end is the one that belongs to this one. Say we have

<?insert-start?>
<a/>
<?insert-end?>
<b/>
<?insert-start?>
<c/>
<?insert-end?>

When we do the first start-marker (when we want 'a'), 'c' will not get selected, because it "belongs" to the second one. But 'b' will also not get selected, because its immediately next insert-end is not the end-marker that "belongs" to the one we are doing (the one before 'a').

(Sorry I know this is very convoluted.)

So in this case we extract this magic list by picking up 'a' and 'b' and then tossing 'b' because it isn't before the correct insert-end. Our test knows this because when it counts the set of nodes that end with its end, it gets 1 (for 'a'), and it also gets one when it counts this set plus 'a': the counts are the same. But when it tests 'b', the count of nodes that belong with its end is still 1 (for 'a'), but the count for this set plus 'b' (that is, both 'a' and 'b') is 2. So 'b' is outside the set.

(Note this kind of thing is much easier in XPath 2.0, where we have an 'intersect' operation.)

Having tossed 'b', we then need to pick it up again after our div is over. So we have another apply-templates that picks up 'b' and 'a', but this time tosses 'a'.

Sorry this is so brain-numbing. Blame not only XSLT 1.0 for this, but XML itself, or perhaps your application, which has to deal with one hierarchy transparently (but invisibly) inside another one ... and has chosen to use processing instructions to do this. Fortunately, even if these methods are obscure, they do work. (And they're not so hard to understand if you can break the problem down into its pieces.)

Warning: untested. Also XSL-Listers should be on notice that more elegant approaches get big points (at least from me :-).

Cheers,
Wendell

At 01:13 PM 5/2/2005, you wrote:
I have been pondering it all morning.  I have an idea what the problem is,
but no idea how to fix it!

Here is the key declaration:

<xsl:key name="p" match="node()"
use="generate-id(preceding-sibling::processing-instruction('xm-insertion_mark_start')[1])"/>

Here is the "apply-templates" section:

<xsl:choose>
      <xsl:when test="text()[normalize-space()] and
processing-instruction('xm-insertion_mark_start')">
            <xsl:apply-templates
select="processing-instruction('xm-insertion_mark_start')[1]/preceding-sibling::node()"/>
            <xsl:for-each
select="processing-instruction('xm-insertion_mark_start')">
                  <span class="revcontrol">
                        <xsl:apply-templates
select="key('p',generate-id())"/>
                  </span>
            </xsl:for-each>
      </xsl:when>
      <xsl:when test="processing-instruction('xm-insertion_mark_start')">
            <xsl:apply-templates
select="processing-instruction('xm-insertion_mark_start')[1]/preceding-sibling::node()"/>
            <xsl:for-each
select="processing-instruction('xm-insertion_mark_start')">
                  <div class="revcontrol">
                        <xsl:apply-templates
select="key('p',generate-id())"/>
                  </div>
            </xsl:for-each>
      </xsl:when>
      <xsl:otherwise>
            <xsl:apply-templates/>
      </xsl:otherwise>
</xsl:choose>

The problem is that if there is an xm-insertion_mark_start processing
instruction found, all the following-sibling nodes get placed inside the
<div class = "revcontrol"></div> section.  I need to figure out a way to
test to see if the node has a
preceding-sibling::processing-instruction('xm-insertion_mark_start') and a
following-sibling::processing-instruction('xm-insertion_mark_end').  So
far, I have been having no luck, so I was hoping someone else had an idea.


======================================================================
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
======================================================================

Current Thread