使用XslCompiledTransform进行空白字符剥离

10

我正在尝试将一个大型应用程序从XslTransform迁移到编译的XSL文件和XslCompiledTransform

该应用程序使用Xsl创建HTML文件,转换数据(Xml)通过从数据库返回的XmlDataDocument传递给Xsl

现在我已经对所有这些进行了更改(至少是暂时性的更改):

C#

 public string ProcessCompiledXsl(XmlDataDocument xml)
 {
       StringBuilder stringControl = new StringBuilder();
       XslCompiledTransform xslTran = new XslCompiledTransform();

       xslTran.Load(
           System.Reflection.Assembly.Load("CompiledXsl").GetType(dllName)
       );

       xslTran.Transform(xml, this.Arguments, XmlWriter.Create(stringControl, othersettings), null);

       return stringControl.ToString();
 }

XSL(只是一个例子)

...
  <xsl:output method="html" indent="yes"/>
  <xsl:template match="/">
       <xsl:for-each select="//Object/Table">
              <a href="#">
                     some text
              </a>
       </xsl:for-each>
  </xsl:template>

问题

这个方案是可行的,但是 XSL 会移除标签之间的空格并输出:

<a href="#">
   some text
</a><a href="#">
   some text
</a><a href="#">
   some text
</a><a...etc

我已经尝试过:

  • 使用xml:space="preserve",但是我无法使其工作
  • 覆盖OutputSettings,但没有得到好的结果(也许我错过了什么)
  • 使用xsl:output method="xml",这可以工作,但会创建自闭合标签和许多其他问题

所以我不知道该怎么办。也许我做错了什么。非常感谢任何帮助。

谢谢!

编辑

供以后参考,如果你想保留所有XSL并解决这个问题,可以尝试这个C#类,名为CustomHtmlWriter

基本上我是从XmlTextWriter继承并修改了每个标签写入的开始和结束方法。

在这种特殊情况下,你可以像这样使用它:

    StringBuilder sb = new StringBuilder();
    CustomHtmlWriter writer = new CustomHtmlWriter(sb);

    xslTran.Transform(nodeReader, this.Arguments, writer);

    return sb.ToString();

希望对某些人有所帮助。

4个回答

5

I. 解决方案 1:

首先让我分析一下这里的问题:

假设有以下源XML文档(由于您没有提供任何文档,因此是虚构的):

<Object>
 <Table>

 </Table>

 <Table>

 </Table>

 <Table>

 </Table>

 <Table>

 </Table>
</Object>

这个转换:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="html" indent="yes"/>

  <xsl:template match="/">
       <xsl:for-each select="//Object/Table">
              <a href="#">
                     some text
              </a>
       </xsl:for-each>
  </xsl:template>
<!--
 <xsl:template match="Table">
   <a href="#">
    Table here
   </a>
 </xsl:template>
 -->
</xsl:stylesheet>

完全复现问题 -- 结果如下:

<a href="#">
                     some text
              </a><a href="#">
                     some text
              </a><a href="#">
                     some text
              </a><a href="#">
                     some text
              </a>

现在,只需取消注释第二个模板,并注释掉第一个模板即可:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="html" indent="yes"/>
<!--
  <xsl:template match="/">
       <xsl:for-each select="//Object/Table">
              <a href="#">
                     some text
              </a>
       </xsl:for-each>
  </xsl:template>
 -->
 <xsl:template match="Table">
   <a href="#">
    Table here
   </a>
 </xsl:template>
</xsl:stylesheet>

结果具有想要的缩进:

 <a href="#">
    Table here
   </a>

 <a href="#">
    Table here
   </a>

 <a href="#">
    Table here
   </a>

 <a href="#">
    Table here
   </a>

这是解决方案1


II. 解决方案2:

这个解决方案可以将对现有XSLT代码所需的修改降至最低:

这是一个两阶段转换:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="ext">
 <xsl:output method="html"/>

  <xsl:template match="/">
    <xsl:variable name="vrtfPass1">
       <xsl:for-each select="//Object/Table">
              <a href="#">
                     some text
              </a>
       </xsl:for-each>
    </xsl:variable>

    <xsl:apply-templates select=
        "ext:node-set($vrtfPass1)" mode="pass2"/>
  </xsl:template>

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

  <xsl:template mode="pass2" match="*[preceding-sibling::node()[1][self::*]]">
   <xsl:text>&#xA;</xsl:text>
   <xsl:copy-of select="."/>
  </xsl:template>
</xsl:stylesheet>

我们的想法是不需要修改现有代码,只需捕获其输出,并使用几行额外的代码格式化输出以达到期望的最终外观。

当同一个XML文档应用此转换时,将产生相同的期望结果:

<a href="#">
                     some text
              </a>
<a href="#">
                     some text
              </a>
<a href="#">
                     some text
              </a>
<a href="#">
                     some text
              </a>

最后,这里演示了如何引入这个小改变,而不需要修改任何现有的XSLT代码::

让我们看一下现有的代码 c:\ temp \ delete \ existing.xsl

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="html"/>

  <xsl:template match="/">
    <xsl:for-each select="//Object/Table">
      <a href="#">
        some text
      </a>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

如果我们运行这个程序,会得到问题输出.

现在,不要运行existing.xsl,而是运行这个转换:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="ext">
 <xsl:import href="file:///c:/temp/delete/existing.xsl"/>
 <xsl:output method="html"/>


  <xsl:template match="/">
    <xsl:variable name="vrtfPass1">
       <xsl:apply-imports/>
    </xsl:variable>

    <xsl:apply-templates select=
        "ext:node-set($vrtfPass1)" mode="pass2"/>
  </xsl:template>

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

  <xsl:template mode="pass2" match="*[preceding-sibling::node()[1][self::*]]">
   <xsl:text>&#xA;</xsl:text>
   <xsl:copy-of select="."/>
  </xsl:template>
</xsl:stylesheet>

结果是所需的,并且现有代码完全未被触及:

<a href="#">
        some text
      </a>
<a href="#">
        some text
      </a>
<a href="#">
        some text
      </a>
<a href="#">
        some text
      </a>

解释:

  1. 我们使用 xsl:import 导入任何现有代码,该代码位于导入优先级层次结构的顶层(未被其他样式表导入)。

  2. 我们在变量中捕获现有转换的输出。它具有臭名昭著的 RTF (结果树片段),需要转换为常规树以进一步处理。

  3. 关键时刻是在捕获转换输出时执行xsl:apply-imports。这确保了任何来自现有代码的模板(即使是我们覆盖的模板--例如匹配/的模板)都将被选择执行,就像执行现有转换本身一样。

  4. 我们使用 msxsl:node-set() 扩展函数(XslCompiledTransform 也支持 EXSLT node-set() 扩展函数)将 RTF 转换为常规树。

  5. 我们对所产生的常规树进行美化调整。

请注意:

这代表了一种不触及现有代码的后处理现有转换的通用算法


好的,完美。所以这个“问题”是预期的行为...我可以问一下你知道他们为什么要这样实现吗?XslTransform不会改变XSL。如果你知道的话,你知道一个学习XSL的好来源吗?我可以谷歌,但如果有人校对过就更好了 :)。谢谢! - nicosantangelo
@Nicosunshine,我不知道XSLT处理器为什么不能产生良好的缩进。我猜这反映了HTML序列化的要求(或者开发人员想要保留一些内存--特别是在通过网络发送转换结果时)。至于好的XSLT学习资源,Michael Kays的书是最好的。它们看起来很大,但是仔细阅读后,你会对这个主题有很好的掌握。请参见此答案以获取更多资源:http://stackoverflow.com/questions/339930/any-good-xslt-tutorial-book-blog-site-online/341589#341589 - Dimitre Novatchev
非常感谢,我会尝试实现您的解决方案(并在这里推广更好的编码)。如果我做不到,我已经扩展了XmlTextWriter以满足我的一些需求,我会编辑答案并附上一个Git链接供以后参考。 - nicosantangelo

1

我不记得XML/XSLT空格保留的详细信息,但其中一个更可能丢弃空格的实例是在元素之间,没有非空格文本(即仅包含空格的文本节点,例如在</a></xsl:for-each>之间的文本节点)。您可以通过使用<xsl:text>元素来防止这种情况。

例如,在

          <a href="#">
                 some text
          </a>

放置

          <xsl:text>&#10;</xsl:text>

即一个字面上的行尾字符。

这符合您的要求吗?


“那个方法”虽然可行,但问题是我有很多XSL文件,如果这是唯一的解决方案,我就必须逐个文件添加显式换行符。如果可能的话,我想避免这种情况^^ - nicosantangelo
哼,我不确定那是否正是我需要的,但无论如何将其添加到XSL中,都会导致此异常:无法从已加载的输入文档中剥离空格。请改用XmlReader提供输入文档。 我会尝试谷歌一下看发生了什么(顺便感谢您)。 - nicosantangelo
xsl:preserve-space 影响源文档中的空格处理,而不是样式表中的空格。这个建议完全是错误的。 - Michael Kay
@MichaelKay:好的,我误读了规范,认为对于样式表,保留空格元素名称集仅包括xsl:text,由xsl:preserve-space中的内容修改。正如你所指出的那样,它并没有提到后面那部分。 - LarsH
2
@Nicosunshine:我删除了更新;看起来你回到了我的原始答案,使用<xsl:text> - LarsH

1

我认为问题是:

  <xsl:output method="html" indent="yes"/> 

如果我没记错,HTML 尝试只关心空格,这对 HTML 的显示方式非常重要。

如果你尝试:

  <xsl:output method="xml" indent="yes"/> 

那么它应该创建你所期望的缩进空格。


这个可以工作,但它会在我的HTML中创建其他问题,比如自闭合标签(这在问题中提到)。 - nicosantangelo
使用indent="yes"的问题在于它不允许你控制空格输出的位置。如果你想在两个超链接之间添加一个可见的空格,你需要这种控制。 - Michael Kay

1

样式表中的空白文本节点总是被忽略,除非它们包含在 xsl:text 中。如果您想要将空格输出到结果树中,请使用 xsl:text。

(在样式表中也可以使用 xml:space="preserve",但通常不建议这样做,因为它会产生不必要的副作用。)


好的,只有一件事我不明白。为什么当没有XSL时它可以正确地呈现换行符,但是当我使用“xsl:each”时它会剥离它们?示例:https://gist.github.com/3557306 。谢谢。 - nicosantangelo
@Nicosunshine:不太清楚你的意思。你说“当没有XSL存在”是指当你没有运行XSLT转换时吗?如果是这种情况,那么如果有任何处理正在进行,它是什么类型的处理呢?如果没有任何处理正在进行,那么当然文本就不会改变。 - LarsH
抱歉表述不够清晰。我的意思是,当我使用XSL时,我使用非XSL结构,比如一个div。我在第一条评论中的代码片段中提供了一个例子,其中包含一个类为“parent”的div结构。 - nicosantangelo
抱歉,我不知道您所指的示例是什么。请记住,在样式表中被忽略的是“空格文本节点”,而不是“空格”。如果样式表中的换行符不全为空格,则其是有意义的。 - Michael Kay

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