XSLT 将打开的 XML 转换为干净的 HTML 列表

XSLT conversion of open XML into clean HTML lists

我创建了一个 XSLT 文件,可以将 Word XML 中的所有内容转换为干净的 HTML 但是我无法正确隐藏嵌套列表。

我将 word v16.12 文件保存到 XML。 Word 文件包含两个列表

这是导出的 Open XML(仅与项目符号相关)。

<w:body>
<w:p w:rsidR="00875AF6" w:rsidRDefault="007A38EC" w:rsidP="007A38EC">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="0"/>
            <w:numId w:val="1"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 1 Bullet 1 level 1</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="007A38EC">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="0"/>
            <w:numId w:val="1"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 1 Bullet 2 level 1</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="007A38EC">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="1"/>
            <w:numId w:val="1"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 1 Bullet 3 level 2</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="007A38EC">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="2"/>
            <w:numId w:val="1"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 1 Bullet 4 level 3</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="007A38EC">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="2"/>
            <w:numId w:val="1"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 1 Bullet 5 level 3</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="007A38EC">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="1"/>
            <w:numId w:val="1"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 1 Bullet 6 level 2</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="007A38EC">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="2"/>
            <w:numId w:val="1"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 1 Bullet 7 level 3</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="007A38EC">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="0"/>
            <w:numId w:val="1"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 1 Bullet 8 level 1</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="00575241"/>
<w:p w:rsidR="00575241" w:rsidRDefault="00575241" w:rsidP="00575241">
    <w:r>
        <w:t>This is a break</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="00575241" w:rsidRDefault="00575241" w:rsidP="00575241"/>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="00575241">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="0"/>
            <w:numId w:val="2"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 2 Bullet 1 level 1</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="00575241">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="1"/>
            <w:numId w:val="2"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 2 Bullet 2 level 2</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="00575241">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="2"/>
            <w:numId w:val="2"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 2 Bullet 3 level 3</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="00575241">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="0"/>
            <w:numId w:val="2"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 2 Bullet 4 level 1</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="00575241">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="1"/>
            <w:numId w:val="2"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 2 Bullet 5 level 2</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="00575241">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="1"/>
            <w:numId w:val="2"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 2 Bullet 6 level 2</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="00575241">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="2"/>
            <w:numId w:val="2"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 2 Bullet 7 level 3</w:t>
    </w:r>
    <w:bookmarkStart w:id="0" w:name="_GoBack"/>
    <w:bookmarkEnd w:id="0"/>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="00575241">
    <w:pPr>
        <w:pStyle w:val="ListParagraph"/>
        <w:numPr>
            <w:ilvl w:val="1"/>
            <w:numId w:val="2"/>
        </w:numPr>
    </w:pPr>
    <w:r>
        <w:t>List 2 Bullet 8 level 2</w:t>
    </w:r>
</w:p>
<w:p w:rsidR="007A38EC" w:rsidRDefault="007A38EC" w:rsidP="00575241"/>
<w:sectPr w:rsidR="007A38EC" w:rsidSect="00D678D3">
    <w:pgSz w:w="11900" w:h="16840"/>
    <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"
        w:header="708" w:footer="708" w:gutter="0"/>
        <w:cols w:space="708"/>
        <w:docGrid w:linePitch="360"/>
    </w:sectPr>
</w:body>

使用 XSLT 我需要将 XML 转换成这个 HTML

<ul>
  <li>List 1 Bullet 1 level 1</li>
  <li>List 1 Bullet 2 level 1
    <ul>
      <li>List 1 Bullet 3 level 2
        <ul>
          <li>List 1 Bullet 4 level 3</li>
          <li>List 1 Bullet 5 level 3</li>
        </ul>
      </li>
      <li>List 1 Bullet 6 level 2
        <ul>
          <li>List 1 Bullet 7 level 3</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>List 1 Bullet 8 level 1</li>
</ul>
<p>This is a gap</p>
<ul>
  <li>List 2 Bullet 1 level 1
    <ul>
      <li>List 2 Bullet 2 level 2
        <ul>
          <li>List 2 Bullet 3 level 3</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>List 2 Bullet 4 level 1
    <ul>
      <li>List 2 Bullet 5 level 2</li>
      <li>List 2 Bullet 6 level 2
        <ul>
          <li>List 2 Bullet 7 level 3</li>
        </ul>
      </li>
      <li>List 2 Bullet 8 level 2</li>
    </ul>
  </li>
</ul>

我已经研究过,我发现最接近的是使用函数和 for-each-group,如下所示。

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:mf="http://example.com/mf" version="2.0"
    exclude-result-prefixes="xs mf">

    <xsl:strip-space elements="*"/>
    <xsl:output indent="yes"/>

    <xsl:function name="mf:group" as="node()*">
        <xsl:param name="nodes" as="node()*"/>
        <xsl:param name="level" as="xs:integer"/>
        <xsl:if test="$nodes">
            <list type="ul">
                <xsl:for-each-group select="$nodes"
                    group-adjacent="boolean(self::*[@level = $level])">
                    <xsl:choose>
                        <xsl:when test="current-grouping-key()">
                            <xsl:apply-templates select="current-group()"/>
                        </xsl:when>
                        <xsl:otherwise>
                            <xsl:sequence select="mf:group(current-group(), $level + 1)"/>
                        </xsl:otherwise>
                    </xsl:choose>
                </xsl:for-each-group>
            </list>
        </xsl:if>
    </xsl:function>

    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@*, node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="item[@level]">
        <item>
            <xsl:apply-templates/>
        </item>
    </xsl:template>

    <xsl:template match="test">
        <xsl:copy>
            <xsl:for-each-group select="*" group-adjacent="boolean(self::item)">
                <xsl:choose>
                    <xsl:when test="current-grouping-key()">
                        <xsl:sequence select="mf:group(current-group(), 0)"/>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:apply-templates select="current-group()"/>
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:for-each-group>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

不幸的是,使用函数和 for-each-group 超出了我的能力范围。我的问题是如何修改上述 XSLT 以使用我从 Word 获得的 XML?

首先,我们将从身份模板开始:

<xsl:template match="@* | node()">
    <xsl:copy>
        <xsl:apply-templates select="@*, node()"/>
    </xsl:copy>
</xsl:template>

其次,我们必须匹配根节点 w:body 并使用 xsl:for-each-group 对元素进行分组。之后,我们将节点存储在一个变量(firstpass)中,以便稍后进一步操作节点,例如:

<!-- If you want to specify the target node (1 in 22 as you say),
     you can adjust the xpath below to match your target node.
-->
<xsl:template match="w:body">
    <xsl:variable name="firstPass">
        <xsl:for-each-group select="*" group-adjacent="boolean(self::w:p[descendant::w:ilvl])">
            <xsl:choose>
                <xsl:when test="current-grouping-key()">
                    <!-- the zero (0) was obtained from the value of
                         w:val attribute of w:ilvl node -->
                    <xsl:sequence select="mf:group(current-group(), 0)"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:apply-templates select="current-group()"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:for-each-group>
    </xsl:variable>
    <xsl:apply-templates select="$firstPass/node()"/>
</xsl:template>

我们可以适配您提到的功能。我们可以修改group-adjacent目标节点为

<xsl:function name="mf:group" as="node()*">
    <xsl:param name="nodes" as="node()*"/>
    <xsl:param name="level" as="xs:integer"/>
    <xsl:if test="$nodes">
        <ul>
            <xsl:for-each-group select="$nodes"
                group-adjacent="boolean(self::*[descendant::w:ilvl/@w:val = $level])">
                <xsl:choose>
                    <xsl:when test="current-grouping-key()">
                        <xsl:apply-templates select="current-group()"/>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:sequence select="mf:group(current-group(), $level + 1)"/>
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:for-each-group>
        </ul>
    </xsl:if>
</xsl:function>

以下是清理所需的模板

<xsl:template match="w:p">
    <xsl:apply-templates select="descendant::w:t"/>
</xsl:template>

<xsl:template match="w:p[.='']|w:sectPr"/>

<xsl:template match="w:t">
    <xsl:choose>
        <xsl:when test="ancestor::w:p[descendant::w:pStyle[@w:val='ListParagraph']]">
            <li>
                <xsl:apply-templates/>
            </li>
        </xsl:when>
        <xsl:otherwise>
            <p>
                <xsl:apply-templates/>
            </p>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

之后,我们还需要将 <ul> 个子关卡插入父级 <li>。为此,我们必须进行第二次转换。

我们现在将匹配 firstpass 变量中存在的节点

<xsl:template match="li[following-sibling::*[1][name()='ul']]">
    <xsl:copy>
        <xsl:apply-templates/>
        <!-- this will copy the target ul nodes, albeit in a different mode -->
        <xsl:apply-templates select="following-sibling::*[1][name()='ul']" mode="transfer"/>
    </xsl:copy>
</xsl:template>

<!-- this will delete the target node -->
<xsl:template match="ul[preceding-sibling::*[1][name()='li']]"/>

和另一个模式的身份模板

<xsl:template match="@* | node()" mode="transfer">
    <xsl:copy>
        <xsl:apply-templates select="@*, node()"/>
    </xsl:copy>
</xsl:template>

整个样式表如下:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:mf="http://example.com/mf"
    xmlns:w="www.wnamespace.com"
    version="2.0"
    exclude-result-prefixes="xs mf w">

    <xsl:strip-space elements="*"/>
    <xsl:output indent="yes" omit-xml-declaration="yes"/>

    <xsl:function name="mf:group" as="node()*">
        <xsl:param name="nodes" as="node()*"/>
        <xsl:param name="level" as="xs:integer"/>
        <xsl:if test="$nodes">
            <ul>
                <xsl:for-each-group select="$nodes"
                    group-adjacent="boolean(self::*[descendant::w:ilvl/@w:val = $level])">
                    <xsl:choose>
                        <xsl:when test="current-grouping-key()">
                            <xsl:apply-templates select="current-group()"/>
                        </xsl:when>
                        <xsl:otherwise>
                            <xsl:sequence select="mf:group(current-group(), $level + 1)"/>
                        </xsl:otherwise>
                    </xsl:choose>
                </xsl:for-each-group>
            </ul>
        </xsl:if>
    </xsl:function>

    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@*, node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="@* | node()" mode="transfer">
        <xsl:copy>
            <xsl:apply-templates select="@*, node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="w:p">
        <xsl:apply-templates select="descendant::w:t"/>
    </xsl:template>

    <xsl:template match="w:p[.='']|w:sectPr"/>

    <xsl:template match="w:t">
        <xsl:choose>
            <xsl:when test="ancestor::w:p[descendant::w:pStyle[@w:val='ListParagraph']]">
                <li>
                    <xsl:apply-templates/>
                </li>
            </xsl:when>
            <xsl:otherwise>
                <p>
                    <xsl:apply-templates/>
                </p>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

<xsl:template match="w:body">
    <xsl:variable name="firstPass">
        <xsl:for-each-group select="*" group-adjacent="boolean(self::w:p[descendant::w:ilvl])">
            <xsl:choose>
                <xsl:when test="current-grouping-key()">
                    <xsl:sequence select="mf:group(current-group(), 0)"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:apply-templates select="current-group()"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:for-each-group>
    </xsl:variable>
    <xsl:apply-templates select="$firstPass/node()"/>
</xsl:template>

    <xsl:template match="ul[preceding-sibling::*[1][name()='li']]"/>

</xsl:stylesheet>

查看实际效果 here