xpathxslt

XSLT - Find most recently _closed_ prior element


xslt 1.0 I am afraid. I have access to exslt. I am otherwise looking to try to avoid using extension functions.

I need to find the most recently CLOSED xslt element of a particular type, rather than the most recently opened.

EDIT for clarity: ie if i moved a cursor backwards from current() through the text representation of the document until I hit </x> then I wish to find the x that that is the closing tag of.

an illustrative example:

<e3>
  <e2>
    <x id="1">
      <y/><!--if run with this 'y' as current() then it should pick up nothing-->
      ...
      <x id="2">
        <y/><!--if run with this 'y' as current() then it should pick up nothing-->
        ...
        <x id="3"/>
        <y/><!--if run with this 'y' as current() then it should pick up id="3"-->
      </x>
      ...
      <y/><!--if run with this 'y' as current() then it should pick up id="2"-->
    </x>
  </e2>
  <e1>
    ...
    <y/><!--if run with this 'y' as current() then it should pick up id="1"-->
  </e1>
</e3>

[please note this is illustrative, the x's and y's can be arbitrarily nested withing containing elements]

All of my attempts to do this are nasty horror shows. The 'best' of them is simply to preprocess and put a self closing marker after each relevant close, and then use preceding::marker[1]/preceding-sibling:x[1] but I hate doing a full copy to achieve this.

Otherwise my best bet is

<xsl:variable name="currentId" select="generate-id(.)"/>
preceding:x[1]/ancestor-or-self:x[following::node()[generate-id(.) = $currentId]][last()]

(ie find the most recently opened one, check its ancestors to see if the current node is after it)

The issue is efficiency, this potentially runs generate-id(.) though the whole document many, many times.

I guess I am asking if I am missing something 'obvious'.


TRYING AGAIN: Suppose I had the following xslt:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
    <xsl:variable name="intermediate">
      <xsl:apply-template select="." mode="intermediate" />
    </xsl:variable>
    <xsl:copy>
      <xsl:apply-templates mode="final" select="exslt:node-set($intermediate)"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template mode="intermediate" match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*" mode="intermediate" />
    </xsl:copy>
  </xsl:template>

  <xsl:template mode="intermediate" match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*" mode="intermediate" />
    </xsl:copy>
  </xsl:template>
  <xsl:template mode="intermediate" match="x">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*" mode="intermediate" />
    </xsl:copy>
    <heyAnXJustEndedHere/><!-- heyAnXJustEndedHere is guaranteed to not be an element name in the input-->
  </xsl:template>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates mode="final" select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="y">
    <xsl:copy>
      <xsl:attribute name="lastClosingXhadID"><xsl:value-of select="preceeding::heyAnXJustEndedHere[1]/preceding-sibling::x[1]/@id"/></xsl:attribute>
      <xsl:apply-templates mode="final" select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

I am looking for a way to achieve the same output, for all input documents, without using node-set / 2 passes.


Solution

  • I have gone for this in the end, which is not too bad:

    <!-- If something preceeds me it closed before me, its ancestors either preceed me OR contain me
     the last x to close before me MUST be either the last to open before me, or an ancestor of it. 
     They closed before me iff they are not my ancestors, with the lowest depth that does so being the last closing-->
    
    <xsl:variable name="lastClosingX" select="preceding::x[1]/ancestor-or-self::x[not(exslt:has-same-node(., current()/ancestor::x))][last()]"/>