To start, let’s consider possible nodes. We do not want to make any assumptions of what may or may not be expandable, so we will handle them all consistently. The first question is whether the node is even expandable.
Our perspective is abstraction—we do not know (nor should we) what the caller may consider to be expandable, or how it may be expanded.
To solve this problem, we introduce an abstract predicate that must be overridden by an implementation to provide anything of value. Otherwise, all we can do is assume that the node we have encountered cannot be expanded (what would we even do to expand it?).
An implementation must override this function.
This predicate determines exclusively whether a node should be expanded or hoisted. Therefore, it should account for both nodes that cannot be expanded (for example, text nodes may not be expanded), and nodes that have already been expanded that can undergo no further expansion.
The default implementation therefore will always yield false, meaning that no processing will take place.
<function name="eseq:is-expandable" as="xs:boolean"> <param name="node" as="node()" /> <sequence select="false()" /> </function>
With that question answered, we are now able to proceed:
The former allows us to save re-passes (and therefore improve performance), as well as the headache of handling every possible case.5 The latter has an important consideration.
Let’s start with the first case.
When we encounter a head that is not expandable, it will be immediately hoisted, as there is no work to be done.
The result of this operation will be a sequence of nodes, one of them being the hoisted node, and the last being the remaining expansion sequence. See Node Hoisting.
<template mode="_eseq:expand" as="node()+" match="*[ node()[ not( eseq:is-expandable( . ) ) ] ]"> <sequence select="_eseq:hoist( . )" /> </template>
As we only have an abstract view of the concept of “expansion”, we
need a way for an implementation to notify us whether a node needs
further expansion, or is ready to be hoisted. Fortunately, we
already have that information because of how we defined
eseq:is-expandable#1. In other words, our previously declared
processing will already take care of hoisting for us when necessary,
so we need only continue to expand nodes as necessary.
We must continue to expand expandable nodes; otherwise, nested expansions would never take place.
Once expansion is complete, by the definition of
eseq:is-expandable#1, expansion will halt.
<template mode="_eseq:expand" as="element()" match="*[ node()[ eseq:is-expandable( . ) ] ]"> <sequence select="_eseq:expand-head( . )" /> </template>
Up until this point, we have been assuming that there is an actual head node to process; this may not be the case. In fact, we are guaranteed to encounter and empty expansion sequence at some point, because all nodes will have been hoisted (see Node Hoisting)!
In this instance, we will consider our job to have been completed, and self-destruct.
Once there is no head node, expansion is complete and the sequence parent is no longer necessary.
<template mode="_eseq:expand" match="*[ not( node() ) ]" />
The above matches satisfy all possible conditions.
PROOF: Let the expansion sequence element be the context node. As
it is an element, it is matched by
*, which is the context of
each match. The expansion sequence either has a head node, or it
does not have a head node. If it does have a head node, then
it can be defined as
yields an empty sequence, and the final template is matched. When
the head node is available, it is either expandable or
non-expandable, determined by the predicate
eseq:is-expandable#1. Since the predicate returns a boolean,
it must be either
true(), and so it
must satisfy either the first or second template respectively. â
We have therefore determined hoisting/expansion actions through use of a single predicate.
An astute reader may have come to an uncomfortable realization:
after all expansions are complete and the expansion sequence node
itself is eliminated (per the final match above), then the node that
was last expanded and hoisted will be considered to be the expansion
eseq:expand-step#1. This is true, but should not
be a problem in practice: hoisting is intended to place nodes into
context for the caller; it is expected that the caller will
recognize when to invoke sequence expansion (likely on a pre-defined
node type, which would no longer match after it is eliminated). The
discomfort comes from the fact that we cannot use this
implementation recursively; this is a consequence of the current
preprocessor implementation, and is subject to change in the future.
Well, we deferred that complexity to the caller via