Вот моя первая попытка этого. Это выглядело достаточно легко, пока я не столкнулся с проблемой рекурсивной группировки. Я думал об использовании набора узлов, но я думаю, что это расширение XSL 1.0, верно? Итак, для этого нет набора узлов? Без этого все выглядит примерно так:
Найти все корневые узлы, обращаться с ними по-разному (у них нет родителей, и поэтому применяются разные правила).
Для каждого такого узла просмотрите каждый узел // ячейки и проверьте, не является ли он прямым потомком текущего узла. Если это может быть ребенок, мы все равно должны сгруппировать его, так что ...
Просматривать каждый // узел ячейки вдоль предыдущей оси. Для каждого такого предыдущего // узла ячейки посмотрите, может ли он быть также прямым потомком родительского узла, найденного на шаге 2.
Найдя предыдущий // узел ячейки, который является дочерним узлом, сравните метку предыдущего дочернего элемента с дочерним, найденным на шаге 2. Если они равны, ничего не выводите (поскольку эта метка была выход раньше). В противном случае ...
Начать вывод дочернего узла. Выполните обход каждого узла // ячейки еще раз, чтобы найти все позиции этой метки, в которой он является дочерним узлом родительского узла, найденного на шаге 2.
Recurse: вернитесь к шагу 2, используя этот дочерний узел в качестве нового родительского узла.
Протестировано на MSXML2, похоже, оно генерирует то, что вы просили. Кажется, он работает, поскольку я понимаю, что он должен работать, даже если я добавлю еще элементов. Это было, конечно, весело писать, и это заставило меня задуматься о направлениях, о которых я обычно не думаю, работая с XSL. Я думаю, это хорошо ...? Опять же, может быть, это пример проблемы, которая МОЖЕТ быть решена с помощью XSL, но может быть решена легче другим способом.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="roots" match="/tables/table/row[1]/cell" use="text()" />
<xsl:key name="cell-by-label" match="/tables/table/row/cell" use="text()"/>
<xsl:variable name="cells" select="/tables/table/row/cell" />
<xsl:template match="/tables">
<xsl:variable name="rootCells" select="/tables/table/row[1]/cell"/>
<tree>
<!-- Work on each root-level nodes first. -->
<xsl:apply-templates
mode="root"
select="$rootCells[count(.|key('roots', text())[1]) = 1]" />
</tree>
</xsl:template>
<!--
Root level cells are handled differently. They have no available $parent,
for one thing. Because of that, it seems simpler to handle them as an
exception here, rather than peppering the mode="crawl" with exceptions
for parentless nodes.
-->
<xsl:template match="cell" mode="root">
<node label="{.}">
<!--
Get a list of everywhere that this cell is found.
We are looking for only other root-level cells here.
-->
<positions>
<xsl:for-each select="key('roots', text())">
<pos table="{../../@pos}" row="{../@pos}" cell="{@pos}"/>
</xsl:for-each>
</positions>
<!--
Locate all child nodes, across all tables in which this node is found.
A node is a child node if:
1. It is in a row directly following the row in which this cell is found.
2. If the @pos is >= the @pos of the parent
3. If the @pos is < the @pos of the parent to the right (if there is a parent to the right)
Note: Meeting the above conditions is not difficult; it's grouping at this
point that gives trouble. If the problem permitted extension functions
to XSL 1.0, then perhaps we could generate a node-set and group that.
I've not tried that way, since I'm under the impression that a node-set
would be an extension to 1.0, and therefore, "cheating."
However, if we could generate a node-set and group based on that,
then the following block selects the correct nodes (I think):
<xsl:for-each select="$matches">
<xsl:variable name="childRow" select="../following-sibling::row[1]"/>
<xsl:variable name="Lparent" select="@pos"/>
<xsl:variable name="Rparent" select="following-sibling::cell[1]/@pos"/>
<xsl:choose>
<xsl:when test="$Rparent">
<xsl:apply-templates
select="$childRow/cell[
@pos >= $Lparent
and @pos < $Rparent]"
mode="child" />
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates
select="$childRow/cell
[@pos >= $Lparent]"
mode="child"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
Without using a node-set, I'll try to solve this by crawling over every
table/row/cell and test each one, every time. Not pretty by a long shot.
But hey, memory and processors are getting cheaper every day.
-->
<xsl:apply-templates select="$cells" mode="crawl">
<xsl:with-param name="parent" select="."/>
<xsl:with-param name="task">Group children by their labels</xsl:with-param>
</xsl:apply-templates>
</node>
</xsl:template>
<xsl:template match="cell" mode="child">
<xsl:param name="parent"/>
<node label="{.}">
<positions>
<xsl:apply-templates mode="crawl"
select="key('cell-by-label', text())">
<xsl:with-param name="parent" select="$parent"/>
<xsl:with-param name="child" select="."/>
<xsl:with-param name="task">Find all positions of a child</xsl:with-param>
</xsl:apply-templates>
</positions>
<!-- And.... Recursion Start Now! -->
<xsl:apply-templates select="$cells" mode="crawl">
<xsl:with-param name="parent" select="."/>
<xsl:with-param name="task">Group children by their labels</xsl:with-param>
</xsl:apply-templates>
</node>
</xsl:template>
<xsl:template match="cell" mode="crawl">
<xsl:param name="parent"/>
<xsl:param name="child"/>
<xsl:param name="task"/>
<xsl:variable name="parentRow"
select="generate-id(../preceding-sibling::row[1])"/>
<xsl:variable name="parentCell"
select="key('cell-by-label', $parent/text())
[$parentRow = generate-id(..)]" />
<xsl:variable name="RparentPos"
select="$parentCell/following-sibling::cell[1]/@pos"/>
<!--
This cell is a child if it is in a row directly following a row
in which the parent cell's text value made an appearance.
<xsl:if test="$parentCell">
This cell is a child if it's @pos is >= the parent cell's pos
<xsl:if test="@pos >= $parentCell/@pos">
If there is a parent cell to the right of this cell's parent cell,
this this cell is a child only if its @pos is < the right-parent
cell's @pos.
<xsl:if test="not($RparentPos) or @pos < $RparentPos">
-->
<xsl:if test="
$parentCell
and (@pos >= $parentCell/@pos)
and (not($RparentPos) or @pos < $RparentPos)">
<xsl:choose>
<!--
If our task is to determine whether there are any nodes prior to
the given child node in the document order which are also
children of the parent and which have the same label value, we do
that now. All we really want is to make a mark. We will later use
string-length to see if we made any marks here or not.
-->
<xsl:when test="$task = 'Are there prior children with equal labels?'">
Yes
</xsl:when>
<!--
Here, our task is to generate the <pos> nodes of the children.
-->
<xsl:when test="$task = 'Find all positions of a child'">
<pos table="{../../@pos}" row="{../@pos}" cell="{@pos}"/>
</xsl:when>
<!--
If our task is to group children by their labels, we need to know
if this is the first child node with this particular label. To do
that, we crawl over all cells along the preceding axis, and if
they are otherwise potential children (see above block), then a
mark is made (perhaps several such marks, doesn't matter how many,
really). If we have any marks when we are done, we know we have
output this label before, so we don't do it again.
-->
<xsl:when test="$task = 'Group children by their labels'">
<xsl:variable name="priorMatches">
<xsl:apply-templates mode="crawl"
select="preceding::cell[text() = current()/text()]">
<xsl:with-param name="parent" select="$parent"/>
<xsl:with-param name="task">Are there prior children with equal labels?</xsl:with-param>
</xsl:apply-templates>
</xsl:variable>
<xsl:if test="string-length($priorMatches) = 0">
<xsl:apply-templates select="." mode="child">
<xsl:with-param name="parent" select="$parent"/>
</xsl:apply-templates>
</xsl:if>
</xsl:when>
</xsl:choose>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Редактировать 1: немного переформатирован. Добавлено использование нескольких элементов xsl: key, чтобы помочь найти дочерние узлы по их метке. Сделано несколько других оптимизаций - надеюсь, без снижения читабельности.
Мысли / комментарии?