如何用递归实现XSLT中字符串字符与<app>节点的双重遍历替换
问题描述
需求:将<p>元素中的字符串里的指定字符,替换为对应<lem>包含该字符的<app>节点。要求每个<app>仅对应文本中的一个字符,按顺序替换,每个<app>顶部只有一个<lem>,下方可包含任意其他节点。
本人刚接触XSLT,受Java/MATLAB的循环遍历思维(比如i=1:n、j=1:m)限制,写不出合适的递归实现。现有代码因为对每个<app>遍历整个文本导致输出错误,求正确的递归实现方法。
示例输入
<div> <p>SOMEWONDERFULOLDTEXT</p> <app> <lem>O</lem> <rdg>Ø</rdg> </app> <app> <lem>W</lem> <rdg>V</rdg> </app> <app> <lem>O</lem> <rdg>Ö</rdg> </app> <app> <lem>E</lem> <rdg>Ë</rdg> <rdg>ę</rdg> </app> </div>
现有错误XSLT代码
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:tei="http://www.tei-c.org/ns/1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs tei" version="3.0"> <xsl:template match="node() | @*"> <xsl:copy> <xsl:apply-templates select="node() | @*"/> </xsl:copy> </xsl:template> <!-- now build the apparatus --> <xsl:template match="tei:div"> <xsl:param name="thisBlock" select="./tei:p/node()"/> <xsl:for-each select="tei:app"> <xsl:variable name="thisApp" select="."/> <xsl:for-each select="tokenize(replace(replace($thisBlock, '(.)', '$1\n'), '\n$', ''), '\n')"> <xsl:choose> <xsl:when test="$thisApp/tei:lem/text() = ."> <xsl:copy-of select="$thisApp"></xsl:copy-of> </xsl:when> <xsl:otherwise> <xsl:apply-templates></xsl:apply-templates> </xsl:otherwise> </xsl:choose> </xsl:for-each> </xsl:for-each> </xsl:template> </xsl:stylesheet>
期望输出
<div> <p>S<app> <lem>O</lem> <rdg>Ø</rdg> </app>ME<app> <lem>W</lem> <rdg>V</rdg> </app><app> <lem>O</lem> <rdg>Ö</rdg> </app>ND<app> <lem>E</lem> <rdg>Ë</rdg> <rdg>ę</rdg> </app>RFULOLDTEXT</p> </div>
解决方案
核心思路是递归遍历文本字符和<app>序列,按顺序匹配替换,避免原代码中重复遍历文本的问题。以下是XSLT 3.0的实现:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:tei="http://www.tei-c.org/ns/1.0" exclude-result-prefixes="tei" version="3.0"> <!-- 身份模板:复制所有未匹配的节点和属性 --> <xsl:template match="node() | @*"> <xsl:copy> <xsl:apply-templates select="node() | @*"/> </xsl:copy> </xsl:template> <!-- 处理div节点:准备文本和app序列,调用递归模板 --> <xsl:template match="div"> <xsl:copy> <xsl:variable name="text" select="p/text()"/> <xsl:variable name="apps" select="app"/> <p> <!-- 调用递归模板,传入剩余文本、剩余app序列 --> <xsl:call-template name="replace-chars"> <xsl:with-param name="remaining-text" select="$text"/> <xsl:with-param name="remaining-apps" select="$apps"/> </xsl:call-template> </p> </xsl:copy> </xsl:template> <!-- 递归替换模板 --> <xsl:template name="replace-chars"> <xsl:param name="remaining-text"/> <xsl:param name="remaining-apps"/> <!-- 终止条件:文本为空或无剩余app时退出 --> <xsl:if test="$remaining-text ne ''"> <xsl:variable name="first-char" select="substring($remaining-text, 1, 1)"/> <xsl:choose> <!-- 当前字符匹配第一个app的lem时,替换为该app,递归处理剩余文本和剩余app --> <xsl:when test="$remaining-apps and $remaining-apps[1]/lem = $first-char"> <xsl:copy-of select="$remaining-apps[1]"/> <xsl:call-template name="replace-chars"> <xsl:with-param name="remaining-text" select="substring($remaining-text, 2)"/> <xsl:with-param name="remaining-apps" select="$remaining-apps[position() > 1]"/> </xsl:call-template> </xsl:when> <!-- 不匹配时,输出当前字符,递归处理剩余文本和原app序列 --> <xsl:otherwise> <xsl:value-of select="$first-char"/> <xsl:call-template name="replace-chars"> <xsl:with-param name="remaining-text" select="substring($remaining-text, 2)"/> <xsl:with-param name="remaining-apps" select="$remaining-apps"/> </xsl:call-template> </xsl:otherwise> </xsl:choose> </xsl:if> </xsl:template> </xsl:stylesheet>
代码说明
- 身份模板:保留XML结构中不需要修改的部分,是XSLT的基础模板。
- div处理模板:提取
<p>的文本内容和所有<app>节点,调用递归模板开始替换流程。 - 递归替换模板:
- 每次取文本的第一个字符和第一个
<app>的<lem>内容对比。 - 匹配时输出该
<app>,并递归处理剩余文本和剩余的<app>序列(去掉已使用的第一个)。 - 不匹配时输出当前字符,递归处理剩余文本和原
<app>序列。 - 当文本为空或没有剩余
<app>时,递归终止。
- 每次取文本的第一个字符和第一个
这种方式严格按顺序逐个匹配替换,符合XSLT的声明式思维,而非命令式循环逻辑。
内容的提问来源于stack exchange,提问作者Dotno
相关产品推荐
相关产品推荐

