我该如何使用XSLT从平面XML列表构建树形结构?

6

我使用了一个极简的MVC框架,在这个框架中PHP控制器DOM模型传递给XSLT视图(参见okapi)。

为了构建导航树,我在MYSQL中使用了嵌套集。这样,我最终得到的模型XML如下所示:

<tree>
    <node>
        <name>root</name>
        <depth>0</depth>
    </node>
    <node>
        <name>TELEVISIONS</name>
        <depth>1</depth>
    </node>
    <node>
        <name>TUBE</name>
        <depth>2</depth>
    </node>
    <node>
        <name>LCD</name>
        <depth>2</depth>
    </node>
    <node>
        <name>PLASMA</name>
        <depth>2</depth>
    </node>
    <node>
        <name>PORTABLE ELECTRONICS</name>
        <depth>1</depth>
    </node>
    <node>
        <name>MP3 PLAYERS</name>
        <depth>2</depth>
    </node>
    <node>
        <name>FLASH</name>
        <depth>3</depth>
    </node>
    <node>
        <name>CD PLAYERS</name>
        <depth>2</depth>
    </node>
    <node>
        <name>2 WAY RADIOS</name>
        <depth>2</depth>
    </node>
</tree>

以下是代表以下结构的内容:

    • 电视
      • 管式电视
      • 液晶电视
      • 等离子电视
    • 便携式电子产品
      • MP3播放器
        • 闪存型
      • CD播放器
      • 双向无线电

如何使用XSLT将此扁平的XML列表转换为嵌套的HTML列表?

PS:这是来自在MySQL中管理分层数据的示例树。

4个回答

5

那种扁平的列表在xslt中很难处理,因为你需要找到下一个分组的位置等。你能使用不同的xml吗?例如,使用扁平的xml:

<?xml version="1.0" encoding="utf-8" ?>
<tree>
  <node key="0">root</node>
  <node key="1" parent="0">TELEVISIONS</node>
  <node key="2" parent="1">TUBE</node>
  <node key="3" parent="1">LCD</node>
  <node key="4" parent="1">PLASMA</node>
  <node key="5" parent="0">PORTABLE ELECTRONICS</node>
  <node key="6" parent="5">MP3 PLAYERS</node>
  <node key="7" parent="6">FLASH</node>
  <node key="8" parent="5">CD PLAYERS</node>
  <node key="9" parent="5">2 WAY RADIOS</node>
</tree>

现在可以轻松高效地完成以下操作:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:key name="nodeChildren" match="/tree/node" use="@parent"/>
  <xsl:template match="tree">
    <ul>
      <xsl:apply-templates select="node[not(@parent)]"/>
    </ul>
  </xsl:template>
  <xsl:template match="node">
    <li>
      <xsl:value-of select="."/>
      <ul>
        <xsl:apply-templates select="key('nodeChildren',@key)"/>
      </ul>
    </li>
  </xsl:template>
</xsl:stylesheet>

这是一个选项吗?

当然,如果你将xml构建为层次结构,那么它会更容易 ;-p


1
@Edward - 再看一遍...抱歉,但你错了。实际上,除非你使用原始输出来打破它,否则在xslt中做你所说的是不可能的,因为xslt本身就是格式良好的xml。输出结果是:<ul>[<li><ul>[etc]</ul></li>]</ul>,其中[]表示“可选,取决于数据”。 - Marc Gravell
@Marc - 我复制了上面的数据,并在氧气中使用Saxon 6.5.5和HE 9.3.0.4运行。输出格式不正确。 <?xml version="1.0" encoding="UTF-8"?><ul> <li>根<ul> <li>电视<ul> <li>管式<ul/></li><li>LCD<ul/></li><li>等离子<ul/></li></ul></li><li>便携式 电子产品<ul><li>MP3播放器<ul><li>闪存<ul/></li></ul></li><li>CD 播放器<ul/></li><li>双向无线电<ul/></li></ul></li></ul></li> </ul> - Edward
@Marc - 为了澄清,我认为 "<ul><li>TUBE<ul /></li>" 不是格式良好的。我认为 "<ul><li>TUBE</li></ul>" 是格式良好的。 - Edward
@Edward,然而,它发出 ul li /ul /li 的说法完全不正确。 - Marc Gravell
@Marc - 好的。看起来有很多相互重叠的元素,这在技术上是有效的XML。但我仍然认为它是混乱的输出。我支持你的观点。(试图投票但被锁定了 - 我给了一条评论点赞) - Edward
显示剩余2条评论

1
非常有帮助!
一个建议是将 <ul> 移到模板内部,这样可以去掉空的 ul。
<xsl:template match="tree">
      <xsl:apply-templates select="node[not(@parent)]"/>
  </xsl:template>
<xsl:template match="node">
   <ul>
    <li>
      <xsl:value-of select="."/>
      <xsl:apply-templates select="key('nodeChildren',@key)"/>  
    </li>
   </ul>
  </xsl:template>
</xsl:stylesheet>

是的,但您将获得每个项目一个ul而不是一个包含多个li的ul;要删除空的ul,您需要测试节点。 - Marc Gravell

1
在XSLT 2.0中,使用新的分组函数会相当容易。
在XSLT 1.0中,稍微有些复杂,但是这个方法可行:
<xsl:template match="/tree">
    <xhtml>
        <head/>
        <body>
            <ul>
                <xsl:apply-templates select="node[depth='0']"/>
                </ul>
            </body>
        </xhtml>
    </xsl:template>

<xsl:template match="node">
    <xsl:variable name="thisNodeId" select="generate-id(.)"/>
    <xsl:variable name="depth" select="depth"/>
    <xsl:variable name="descendants">
        <xsl:apply-templates select="following-sibling::node[depth = $depth + 1][preceding-sibling::node[depth = $depth][1]/generate-id() = $thisNodeId]"/>
        </xsl:variable>
    <li>
        <xsl:value-of select="name"/>
        </li>
    <xsl:if test="$descendants/*">
        <ul>
            <xsl:copy-of select="$descendants"/>
            </ul>
        </xsl:if>
    </xsl:template>

问题的关键在于那个又长又难看的"descendants"变量,它会查找当前节点之后具有“depth”子节点深度大于当前深度的节点,但不会查找那些与当前深度相同的节点之后的节点(因为如果这样的话,它们将是那个节点的子节点而不是当前节点的子节点)。
顺便提一下,在你的示例结果中有一个错误:“FLASH”应该是“MP3 PLAYERS”的子节点而不是兄弟节点。
编辑
实际上(如评论所述),在“纯”XSLT 1.0中,这种情况有两个原因:路径表达式不正确使用generate-id(),并且不能在路径表达式中使用“结果树片段”。
这里是一个XSLT 1.0版本的正确的“节点”模板(已经在Saxon 6.5上成功测试),不使用EXSLT或XSLT 1.1:
<xsl:template match="node">
    <xsl:variable name="thisNodeId" select="generate-id(.)"/>
    <xsl:variable name="depth" select="depth"/>
    <xsl:variable name="descendants">
        <xsl:apply-templates select="following-sibling::node[depth = $depth + 1][generate-id(preceding-sibling::node[depth = $depth][1]) = $thisNodeId]"/>
        </xsl:variable>
    <xsl:variable name="descendantsNb">
        <xsl:value-of select="count(following-sibling::node[depth = $depth + 1][generate-id(preceding-sibling::node[depth = $depth][1]) = $thisNodeId])"/>
        </xsl:variable>
    <li>
        <xsl:value-of select="name"/>
        </li>
    <xsl:if test="$descendantsNb &gt; 0">
        <ul>
            <xsl:copy-of select="$descendants"/>
            </ul>
        </xsl:if>
    </xsl:template>

当然,应该将重复的路径表达式进行因式分解,但是如果不能将“结果树片段”转换为实际可处理的XML,我不知道是否可能?(编写自定义函数当然可以解决问题,但使用EXSLT要简单得多)

底线:如果可以,请使用XSLT 1.1或EXSLT!

第二次编辑

为了避免重复路径表达式,您还可以完全忘记测试,这只会导致一些空的<div>,您可以将其保留在结果中或进行后处理以消除。


看起来你不能像这样做 <xsl:if test="$descendants/*"> … 但是使用EXSLT,你可以这样做 <xsl:if test="exslt:node-set($descendants)/*"> … - Pierre Spring

0

您并没有明确说明您希望HTML输出的样式,但我可以告诉您,从XSLT的角度来看,如果您还基于树中项目的位置和它们与兄弟关系的关系,从平面结构转换为树形结构将会非常复杂和昂贵。

最好提供一个<parent>属性/节点而不是<depth>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接