Milestone-chunk.xquery

Summary
This is an XQuery 1.0 function that will return all of the content between two milestone elements such as pb while preserving the hierarchical structure of the containing elements. For example, given content like this in a TEI document:    An example of a very short page here. 

the function would produce the following XML fragment as output when asked to return content between pb/@n=3 and pb/@n=4 with the text element as the ancestor:

  of a very short page

In other words, the full hierarchical structure of the ancestor elements, including their attributes, is preserved, but only the nodal content between the milestons is included.

Required Input
The function signature is local:milestone-chunk( $ms1 as element,  $ms2 as element,  $node as node* )

$node is an element known to be a common ancestor of the two milestones. For example, it could be TEI/text, or TEI/text/body, or even TEI/text/body/div1[3] if the milestone parameters are both descendants of that div1.

$ms1 is the first milestone element; $ms2 is the second milestone element.

For example, the output in "Summary" above might have been produced by the call

let $input := doc("mydoc.xml")/tei:TEI/tei:text return local:milestone-chunk($input//pb[@n="3"], $input//pb[@n="4"], $input)

If $input had been doc("mydoc.xml")/tei:TEI/tei:text/tei:body then the output would have started at the body element, etc.

$ms1 and $ms2 do not need to be adjacent milestones. You can, for example, return content between pb/@n=4 and pb/@n=7. Nor do the milestones need to be of the same type; you can return content between the pb for page 4 and an arbitrary anchor or pointer element later in the document.

Expected Output
As indicated in the example in "Summary" above, the output should be a single XML element reflecting the structure of the input ancestor element and its descendants, but otherwise containing only the nodal content between the two milestone elements in the original input.

Known Restrictions or Problems
The output will contain a copy of the first milestone element, but not of the second (closing) one. This is a consequence of the need to use pseudo-milestones for the second milestone in some cases; see next paragraph.

When using this function to generate content between TEI pb elements, an obvious problem is that the final page will not have a final pb milestone. In this case, use a pseudo-milestone such as the last node in the input element. For example, using the sample document in "Summary" above, the content of page 4 ("here.") would be output via the call let $input := doc("mydoc.xml")/tei:TEI/tei:text local:milestone-chunk($input//pb[@n="4"], ($input//node)[last], $input)

If you use this function to recurse over all the pb elements in a document, you will need to use a strategy like this when $ms1 has no following pb element.

The function will not gracefully handle invalid input, but will probably throw run-time errors. You should be sure that the parameters passed to it reflect actual milestone elements with a common ancestor.

Code
declare function local:milestone-chunk( $ms1 as element,  $ms2 as element,  $node as node* ) as node* { typeswitch ($node) case element return if ($node is $ms1) then $node else if ( some $n in $node/descendant::* satisfies ($n is $ms1 or $n is $ms2) ) then element { name($node) } { for $i in ( $node/node | $node/@* ) return local:milestone-chunk($ms1, $ms2, $i) } else if ( $node >> $ms1 and $node << $ms2 ) then $node else case attribute return $node (: will never match attributes outside non-returned elements :) default return if ( $node >> $ms1 and $node << $ms2 ) then $node else };

Implementation in eXist
This function is implemented in eXist (as a function called util:get-fragment-between) by Josef Willenborg, Max Planck Institute for the History of Science [mailto:jwillenborg@mpiwg-berlin.mpg.de jwillenborg@mpiwg-berlin.mpg.de]:

util:get-fragment-between($beginning-node as node?, $ending-node as node?, $make-fragment as xs:boolean?) xs:string

Returns an xml fragment or a sequence of nodes between two elements (normally milestone elements). The $beginning-node represents the first node/milestone element, $ending-node, the second one. The third argument, $make-fragment, is a boolean value for the path completion. If it is set to true the result sequence is wrapped into a parent element node.

Example call of the function for getting the fragment between two TEI page break element nodes: let $fragment := util:get-fragment-between(//pb[1], //pb[2], true)

XSL-Implementation
This function is also implemented as a XSL transformation (by Josef Willenborg, Max Planck Institute for the History of Science [mailto:jwillenborg@mpiwg-berlin.mpg.de jwillenborg@mpiwg-berlin.mpg.de]). Input of the transformation script is the XML document of which a fragment should be extracted. Parameters of the transformation script are: It returns an xml fragment between two milestone elements. The result sequence is wrapped into a parent element node.
 * ms1Name (first milestone name: e.g. "pb")
 * ms1Position (first milestone position: e.g. 5)
 * ms2Name (second milestone name: e.g. "pb")
 * ms2Position (second milestone position: e.g. 6)

Restrictions:
 * uses the Saxon extension function "saxon:evaluate" (so the Saxon library is needed for this)
 * takes no care about namespaces
 * wrapping into parent node cannot be configured





    



 <xsl:param name="ms1Position"></xsl:param> <xsl:param name="ms2Name"></xsl:param> <xsl:param name="ms2Position"></xsl:param>

<xsl:variable name="ms1XPath" select="concat('subsequence(//*:', $ms1Name, ', ', $ms1Position, ', 1)')"/> <xsl:variable name="ms2XPath" select="concat('subsequence(//*:', $ms2Name, ', ', $ms2Position, ', 1)')"/> <xsl:variable name="ms1" select="saxon:evaluate($ms1XPath)" xmlns:saxon="http://saxon.sf.net/"/> <xsl:variable name="ms2" select="saxon:evaluate($ms2XPath)" xmlns:saxon="http://saxon.sf.net/"/> <xsl:variable name="ms1Ancestors" select="$ms1/ancestor::*"/> <xsl:variable name="ms2Ancestors" select="$ms2/ancestor::*"/>

<xsl:template match="element[local-name != $ms1Name and local-name != $ms2Name]"> <xsl:choose> <xsl:when test="(. >> $ms1 or fn:contains($ms1Ancestors, .)) and ($ms2 >> . or fn:contains($ms2Ancestors, .))"> <xsl:element name="{local-name(.)}"><xsl:apply-templates/></xsl:element> </xsl:when> <xsl:otherwise><xsl:apply-templates/></xsl:otherwise> </xsl:choose> </xsl:template>

<xsl:template match="attribute|text|comment|processing-instruction"> <xsl:choose> <xsl:when test=". >> $ms1 and $ms2 >> ."> <xsl:copy><xsl:apply-templates/></xsl:copy> </xsl:when> <xsl:otherwise><xsl:apply-templates/></xsl:otherwise> </xsl:choose> </xsl:template>

<xsl:template match="element[local-name = $ms1Name or local-name = $ms2Name]"> <xsl:choose> <xsl:when test=". is $ms1"> <xsl:element name="{local-name(.)}"><xsl:copy-of select="@*"></xsl:copy-of></xsl:element> </xsl:when> <xsl:when test=". is $ms2"> <xsl:element name="{local-name(.)}"><xsl:copy-of select="@*"></xsl:copy-of></xsl:element> </xsl:when> <xsl:otherwise></xsl:otherwise> </xsl:choose> </xsl:template>

</xsl:stylesheet>