如何用XSLT 1.0对HTML中同class的同级元素分组?
XSLT 1.0 同级连续元素分组问题解决建议
问题需求
使用XSLT 1.0处理HTML,将连续同级的div.case1/div.case2元素分组到<box>容器中,同时将每个div的内容转换为<p>标签,其他元素保持原样输出。
输入HTML
<html> <head> <meta charset="utf-8" /> </head> <body> <div> <p>A</p> <div class="case1">0</div> <div class="case1">1</div> <div class="case1">2</div> <p>B</p> <p>C</p> <div class="case2">3</div> <div class="case2">4</div> <p>D</p> <p>E</p> <div class="case1">5</div> <div class="case1">6</div> </div> </body> </html>
期望输出
<?xml version="1.0"?> <html> <head> <meta charset="utf-8"/> </head> <body> <div> <p>A</p> <box class="case1"> <p>0</p> <p>1</p> <p>2</p> </box> <p>B</p> <p>C</p> <box class="case2"> <p>3</p> <p>4</p> </box> <p>D</p> <p>E</p> <box class="case1"> <p>5</p> <p>6</p> </box> </div> </body> </html>
用户尝试的XSLT代码及问题
用户编写的XSLT模板中,mode="inbox"未生效,分组后的<box>内容为空,仅最后一个空模板<xsl:template match="div[@class='case1']" />生效:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*|node()"/> </xsl:copy> </xsl:template> <!-- For the case of "case1". --> <xsl:template match="div[@class='case1'][not(preceding-sibling::*[1][@class='case1'])]"> <box> <xsl:apply-templates select="." mode="inbox" /> <xsl:apply-templates select="following-sibling::div[@class='case1'][preceding-sibling::*[not(self::div[@class='case1'])][1]/following-sibling::div[@class='case1'][1] = current()]" mode="inbox" /> </box> </xsl:template> <xsl:template match="div[@class='case1']" mode="inbox"> <p><xsl:apply-templates select="node()" /></p> </xsl:template> <xsl:template match="div[@class='case1']" /> </xsl:stylesheet> <!-- For case2, similarly -->
问题根源:
- 后续兄弟节点的选择逻辑过于复杂且存在错误,无法正确选中连续的同class
div元素; - 分开处理
case1和case2导致代码冗余,且空模板会阻止非分组触发节点的正确处理。
解决方案:Muenchian分组法
XSLT 1.0中最可靠的分组方式是Muenchian分组,通过定义key将连续同类型元素归为一组,统一处理所有case类div:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <!-- 定义分组key:以最近的非case元素ID + class作为分组标识 --> <xsl:key name="case-divs" match="div[starts-with(@class, 'case')]" use="concat( generate-id(preceding-sibling::*[not(starts-with(@class, 'case'))][1]), '|', @class )" /> <!-- 身份模板:复制所有未被特殊模板匹配的节点 --> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*|node()"/> </xsl:copy> </xsl:template> <!-- 匹配每组的第一个case div,生成box容器 --> <xsl:template match="div[starts-with(@class, 'case')][count(. | key('case-divs', concat(generate-id(preceding-sibling::*[not(starts-with(@class, 'case'))][1]), '|', @class))[1]) = 1]"> <box class="{@class}"> <!-- 处理当前组内所有case div,使用wrap模式转换为p标签 --> <xsl:apply-templates select="key('case-divs', concat(generate-id(preceding-sibling::*[not(starts-with(@class, 'case'))][1]), '|', @class))" mode="wrap" /> </box> </xsl:template> <!-- wrap模式:将case div转换为p标签 --> <xsl:template match="div[starts-with(@class, 'case')]" mode="wrap"> <p> <xsl:apply-templates/> </p> </xsl:template> <!-- 默认模式下,忽略非分组首节点的case div,避免重复输出 --> <xsl:template match="div[starts-with(@class, 'case')]"/> </xsl:stylesheet>
方案说明
- 分组Key:通过
preceding-sibling::*[not(starts-with(@class, 'case'))][1]定位每个case div之前最近的非case元素,结合class生成唯一分组标识,确保连续同class的div被归为同一组; - 分组触发模板:仅匹配每组的第一个
case div,生成<box>容器并遍历组内所有元素; - 模式转换:使用
mode="wrap"将组内每个div转换为<p>标签,避免与默认模板冲突; - 空模板:默认模式下忽略非分组首节点的
case div,防止重复输出。
内容的提问来源于stack exchange,提问作者msnrhnd
相关产品推荐
相关产品推荐

