С помощью рекурсии можно эмулировать поиск последнего вхождения подстроки substr. Эта техника позволяет написать шаблоны substring-before-last (выделение строки, предшествующей последнему вхождению) и substring-after-last (выделение строки, следующей за последним вхождению):
<xsl:templatename="str:substring-before-last"><xsl:paramname="input"/><xsl:paramname="substr"/><xsl:iftest="$substr and contains($input, $substr)"><xsl:variablename="temp"select="substring-after($input, $substr)"/><xsl:value-ofselect="substring-before($input, $substr)"/><xsl:iftest="contains($temp, $substr)"><xsl:value-ofselect="$substr"/><xsl:call-templatename="str:substring-before-last"><xsl:with-paramname="input"select="$temp"/><xsl:with-paramname="substr"select="$substr"/></xsl:call-template></xsl:if></xsl:if></xsl:template><xsl:templatename="str:substring-after-last"><xsl:paramname="input"/><xsl:paramname="substr"/><!-- Выделить строку, следующую за первым вхождением --><xsl:variablename="temp"select="substring-after($input,$substr)"/><xsl:choose><xsl:whentest="$substr and contains($temp,$substr)"><xsl:call-templatename="str:substring-after-last"><xsl:with-paramname="input"select="$temp"/><xsl:with-paramname="substr"select="$substr"/></xsl:call-template></xsl:when><xsl:otherwise><xsl:value-ofselect="$temp"/></xsl:otherwise></xsl:choose></xsl:template>
XSLT 2.0
В XSLT 2.0 нет вариантов функций substring-before / substring-after, которые позволяли бы искать от конца строки, но добиться желаемого результата позволяет функция tokenize(), основанная на применении регулярных выражений:
1 2 3 4 5 6 7 8 91011
<xsl:functionname="ckbk:substring-before-last"><xsl:paramname="input as xs:string"/><xsl:paramname="substr as xs:string"/><xsl:sequenceselect="if ($substr) then if (contains($input, $substr)) then string-join(tokenize($input, $substr) [position() ne last()],$substr) else '' else $input"/></xsl:function><xsl:functionname="ckbk:substring-after-last"><xsl:paramname="input as xs:string"/><xsl:paramname="substr as xs:string"/><xsl:sequenceselect="if ($substr) then if (contains($input, $substr)) then tokenize($input, $substr)[last()] else '' else $input"/></xsl:function>
В обеих функциях нужно проверять, не является ли строка substr пустой, поскольку функция tokenize не примет пустой образец для поиска. К сожалению, эти реализации работают не совсем так, как встроенные аналоги. Связано это с тем, что tokenize трактует свой второй аргумент как регулярное выражение, а не как литеральную строку. И это может стать источником неожиданностей.
Можно исправить этот недостаток путем экранирования всех специальных символов, встречающихся в регулярном выражении. И включать или отключать такое поведение с помощью третьего булевского аргумента. Первоначальная версия с двумя аргументами и новая с тремя могут сосуществовать, так как XSLT допускает перегрузку функций (то есть функция полностью определяется своим именем и арностью (количеством аргументов).
1 2 3 4 5 6 7 8 910111213141516
<xsl:functionname="ckbk:substring-before-last"><xsl:paramname="input as xs:string"/><xsl:paramname="substr as xs:string"/><xsl:paramname="mask-regex as boolean"/><xsl:variablename="matchstr"select="if ($mask-regex) then replace($substr,'([.+?*^$])','\$1') else $substr"/><xsl:sequenceselect="ckbk:substring-before-last($input,$matchstr)"/></xsl:function><xsl:functionname="ckbk:substring-after-last"><xsl:paramname="input"/><xsl:paramname="substr"/><xsl:paramname="mask-regex"/><xsl:variablename="matchstr"select="if ($mask-regex) then replace($substr,'([.+?*^$])','\$1') else $substr"/><xsl:sequenceselect="ckbk:substring-after-last($input,$matchstr)"/></xsl:function>
Обе функции поиска подстроки в XSLT (substring-before и substring-after) начинают поиск с начала строки. Но иногда нужно искать подстроку с конца строки. Проще всего решить эту задачу, рекурсивно применяя встроенные функции поиска, пока не будет найдено последнее вхождение подстроки.
В первой попытке написать эти шаблоны я столкнулся с неприятным эффектом, о котором вы должны помнить, когда работаете с рекурсивными шаблонами. Напомню, что выражение contains($anything,'') всегда возвращает true! Поэтому при рекурсивном вызове substring-before-last и substring-after-last я проверяю, что значение $substr не пусто. Без такой проверки мы попали бы в бесконечный цикл поиска пустой подстроки, а если реализация не поддерживает хвостовую рекурсию, то произошло бы переполнение стека.
Есть и другой алгоритм, который называется разделяй и властвуй или деление пополам. Его основная идея заключается в том, чтобы разбить строку на две половинки. Если искомая подстрока находится во второй половине, то первую можно не рассматривать и тем самым свести исходную задачу к другой, вдвое меньшей сложности. Этот процесс повторяется рекурсивно. Но нужно учесть еще случай, когда искомая строка частично находится в первой половине, а частично во второй. Ниже приведено решение для функции substring-before-last:
<xsl:templatename="str:substring-before-last"><xsl:paramname="input"/><xsl:paramname="substr"/><xsl:variablename="mid"select="ceiling(string-length($input) div 2)"/><xsl:variablename="temp1"select="substring($input,1, $mid)"/><xsl:variablename="temp2"select="substring($input,$mid +1)"/><xsl:choose><xsl:whentest="$temp2 and contains($temp2,$substr)"><!--искомая строка во второй половине, поэтому просто добавим первую половину и --><!-- выполним рекурсивный вызов для второй --><xsl:value-ofselect="$temp1"/><xsl:call-templatename="str:substring-before-last"><xsl:with-paramname="input"select="$temp2"/><xsl:with-paramname="substr"select="$substr"/></xsl:call-template></xsl:when><!-- искомая строка на границе, задача решается простым вызовом substring-before --><xsl:whentest="contains(substring($input, $mid - string-length($substr) +1), $substr)"><xsl:value-ofselect="substring-before($input,$substr)"/></xsl:when><!--искомая строка в первой половине, поэтому вторую отбрасываем--><xsl:whentest="contains($temp1,$substr)"><xsl:call-templatename="str:substring-before-last"><xsl:with-paramname="input"select="$temp1"/><xsl:with-paramname="substr"select="$substr"/></xsl:call-template></xsl:when><!-- Искомая строка не найдена, завершаемся --><xsl:otherwise/></xsl:choose></xsl:template>
Выясняется, что такой алгоритм деления пополам дает ощутимый выигрыш, только если просматриваемый текст достаточно велик (порядка 4000 символов и более). Можно написать шаблон-обертку, который будет выбирать подходящий алгоритм в зависимости от длины текста или переключаться с алгоритма деления пополам на более простой, если очередная часть оказывается достаточно короткой.