Хотя этот вопрос был задан (и получен ответ) несколько лет назад, ни этот ответ, ни (многие!) Другие варианты, которые я нашел при поиске в сети за последние пару дней, не смогли сделать то, что мне было нужно: заменить несколько строки в узлах, которые могут содержать несколько kb текста.
Версия Dimitre работает хорошо, когда узлы содержат очень мало текста, но когда я попытался использовать его, я почти сразу же испортился из-за переполнения страшного стека (рекурсивные вызовы, помните!) поиск шаблонов до начала текста. Это означает, что выполняется много (рекурсивных) вызовов, каждый из которых использует самые правые n-1 символов исходного текста. Для 1k текста это означает более 1000 рекурсивных вызовов!
После поиска альтернативных вариантов я наткнулся на пример Ибрагима Наджи (http://thinknook.com/xslt-replace-multiple-strings-2010-09-07/), который использует более обычную комбинацию substring-before / substring-after для выполнения замены. Однако этот код ограничен одним строка замены для любого количества строк поиска.
Поэтому я решил, что пришло время на самом деле пачкать руки (и одновременно изучать XSLT!). Результатом является следующий код, который выполняет множественные замены строк (заданные с помощью внутреннего шаблона, но это легко заменить на внешний файл, например) и который (пока в моих тестах) не страдает от чрезмерных рекурсивных вызовов.
Следует отметить, что замены являются очень простыми (как и большинство других существующих реализаций), что означает, что не делается никаких попыток, например, сопоставить только целые слова. Я надеюсь, что комментариев будет достаточно, чтобы объяснить, как это работает, особенно для других начинающих XSLT (таких как я).
А теперь код ...
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
xmlns:dps="dps:dps">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<!--
The original version of this code was published by Ibrahim Naji (http://thinknook.com/xslt-replace-multiple-strings-2010-09-07/).
It works but suffered the limitation of only being able to supply a single replacement text. An alternative implementation, which
did allow find/replace pairs to be specified, was published by Dimitre Novatchev
(/4691314/xslt-zamena-neskolkih-strok-s-rekursiei).
However, that implementation suffers from stack overflow problems if the node contains more than a few hundred bytes of text (and
in my case I needed to process nodes which could include several kb of data). Hence this version which combines the best features
of both implementations.
John Cullen, 14 July 2017.
-->
<!-- IdentityTransform, copy the input to the output -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<!-- Process all text nodes. -->
<xsl:template match="text()">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text" select="."/>
</xsl:call-template>
</xsl:template>
<!-- Table of replacement patterns -->
<xsl:variable name="vPatterns">
<dps:patterns>
<pattern>
<old><i></old>
<new><em></new>
</pattern>
<pattern>
<old></i></old>
<new></em></new>
</pattern>
<pattern>
<old><b></old>
<new><strong></new>
</pattern>
<pattern>
<old></b></old>
<new></strong></new>
</pattern>
</dps:patterns>
</xsl:variable>
<!--
Convert the internal table into a node-set. This could also be done via a call to document()
for example select="document('')/*/myns:params/*" with a suitable namespace declaration, but
in my case that was not possible because the code is being used in with a StreamSource.
-->
<xsl:variable name="vPats" select="exsl:node-set($vPatterns)/dps:patterns/*"/>
<!-- This template matches all text() nodes, and calls itself recursively to performs the actual replacements. -->
<xsl:template name="string-replace-all">
<xsl:param name="text"/>
<xsl:param name="pos" select="1"/>
<xsl:variable name="replace" select="$vPats[$pos]/old"/>
<xsl:variable name="by" select="$vPats[$pos]/new"/>
<xsl:choose>
<!-- Ignore empty strings -->
<xsl:when test="string-length(translate(normalize-space($text), ' ', '')) = 0">
<xsl:value-of select="$text"/>
</xsl:when>
<!-- Return the unchanged text if the replacement is larger than the input (so no match possible) -->
<xsl:when test="string-length($replace) > string-length($text)">
<xsl:value-of select="$text"/>
</xsl:when>
<!-- If the current text contains the next pattern -->
<xsl:when test="contains($text, $replace)">
<!-- Perform a recursive call, each time replacing the next occurrence of the current pattern -->
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text" select="concat(substring-before($text,$replace),$by,substring-after($text,$replace))"/>
<xsl:with-param name="pos" select="$pos"/>
</xsl:call-template>
</xsl:when>
<!-- No (more) matches found -->
<xsl:otherwise>
<!-- Bump the counter to pick up the next pattern we want to search for -->
<xsl:variable name="next" select="$pos+1"/>
<xsl:choose>
<!-- If we haven't finished yet, perform a recursive call to process the next pattern in the list. -->
<xsl:when test="boolean($vPats[$next])">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text" select="$text"/>
<xsl:with-param name="pos" select="$next"/>
</xsl:call-template>
</xsl:when>
<!-- No more patterns, we're done. Return the fully processed text. -->
<xsl:otherwise>
<xsl:value-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>