В общем, спустя два года, на помощь пришел китайский ИИ. Совместными усилиями был рожден следующий способ:
<xsl:template match="entry">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:for-each-group select="node()"
group-adjacent="boolean(
self::para |
self::itemizedlist |
self::orderedlist |
self::variablelist |
self::table |
self::informaltable
)">
<xsl:choose>
<!-- Группа блочных элементов -->
<xsl:when test="current-grouping-key()">
<xsl:apply-templates select="current-group()"/>
</xsl:when>
<!-- Группа текста/инлайн-элементов - оборачиваем только если есть значимый контент -->
<xsl:otherwise>
<xsl:variable name="group-content">
<xsl:apply-templates select="current-group()"/>
</xsl:variable>
<xsl:if test="normalize-space($group-content) != ''">
<para>
<xsl:copy-of select="$group-content"/>
</para>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
где мы используем инструкции и функции xsl 2.0 for-each-group и group-content() для группировки. Т.к. у нас есть четкое деление на блочные элементы, и все остальное, то отделяем блочные от неблочных, и все непустые неблочные (к которым и относится голый текст либо инлайн-элементы) оборачиваем в нужный нам блочный элемент. Вроде работает.