Ты почти у цели!
Ваша проблема заключается в том, как вы обрабатываете b [старые] узлы. Используя // b, вы эффективно выбираете не ту ось, чтобы вы выбрали все 4 узла b (отсюда и повторение).
Следующее преобразование выводит следующий результат на моей машине:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
<xsl:strip-space elements="*" />
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*" />
</xsl:copy>
</xsl:template>
<xsl:template match="*/p[b]" >
<xsl:if test="count(preceding-sibling::p[b]) = 0">
<table>
<xsl:apply-templates select="parent::node()//b" mode="test"/>
</table>
</xsl:if>
</xsl:template>
<xsl:template match="b" mode="test">
<tr>
<td>
<xsl:value-of select="substring-before(., ' ')" />
</td>
<td>
<xsl:value-of select="substring-after(., ' ')" />
</td>
</tr>
</xsl:template>
</xsl:stylesheet>
Результат:
<Recipe>
<p>Some text goes here</p>
<p>Some text goes here</p>
<p>Some text goes here</p>
<p>Some text goes here</p>
<p>Ingredients</p>
<table>
<tr>
<td>1</td>
<td>cup of flour</td>
</tr>
<tr>
<td>2</td>
<td>eggs</td>
</tr>
<tr>
<td>1/4</td>
<td>stick of butter</td>
</tr>
<tr>
<td>1/4</td>
<td>cup of sugar</td>
</tr>
</table>
<p>Some text goes here</p>
<p>Some text goes here</p>
<p>Some text goes here</p>
<p>Some text goes here</p>
</Recipe>
(Обратите внимание, что ваш оригинальный фрагмент XML заключен в узел для создания правильно сформированного документа)
Как это работает? Дело в том, сколько раз мы сопоставляем шаблон p / b - хотя он существует четыре раза, нам нужна только одна таблица со всеми последующими узлами b как дочерними.
Итак, мы проверяем первый p с дочерним элементом b p[b]
, подсчитывая, сколько раз этот шаблон встречался раньше - count(preceding-sibling::p[b]) = 0
дает мне первый. Затем мы поднимаемся обратно на узел и выбираем отсюда всех детей b.