首页 文章

XSLT作为替换应用程序:实例化“XML宏”

提问于
浏览
0

我想使用XSLT根据不同XML文件中指定的键值映射从一个XML文件创建XML模式的多个实例,以供参考,参见https://en.wikipedia.org/wiki/Substitution_%28logic%29 . 应将XSL应用于键值映射(替换),其中包含对模式的引用 . 我猜这可以用一般的(也可能是优雅的)解决方案来完成,但我不知道如何继续 .

一个例子如下:

模式:(pattern.xml)

<?xml version="1.0" encoding="UTF-8"?>
<pattern>
  <key1>a</key1>
  <element2>key2</element2>
  <element3 attribute="key2">key1</element3>
</pattern>

键值映射:(substitution.xml)(更新:从input.xml重命名)

Update: 替换应该包含对模式的引用,并且在常见的浏览器中工作,我理解这需要使用XSLT 1.0 .

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="apply-substitution.xsl"?>
<root pattern="http://example.com/link-to-pattern.xml">
  <copy>
    <key1>VALUE1</key1>
    <key2>VALUE2</key2>
    <key3>VALUE3</key3>
  </copy>
    <copy>
    <key1>VALUE3</key1>
    <key2>VALUE4</key2>
    <key3>VALUE5</key3>
  </copy>
</root>

将 Map “应用”到模式的结果应该是:

<?xml version="1.0" encoding="UTF-8"?>
<some_root>
<!-- instance 1 -->
<pattern>
  <VALUE1>a</VALUE1>
  <element2>VALUE2</element2>
  <element3 attribute="VALUE2">VALUE1</element3>
</pattern>

<!-- instance 2 -->
<pattern>
  <VALUE3>a</VALUE3>
  <element2>VALUE4</element2>
  <element3 attribute="VALUE4">VALUE3</element3>
</pattern>
</some_root>

...创建模式的实例,其中所有元素名称,属性名称和值(匹配整个值,例如,不是子字符串)按键值映射的指定放置 .

我的用例是模式是一个非常复杂的XML结构(不是我设计的),我想创建(可能非常大的一组)实例 - 保持模式的结构和大多数内容原样,但更改一些节点,这些节点可能出现在模式XML的任何位置,即元素名称,属性名称和值(匹配完整值) . 模式实例的总和在模式XML语言中形成完整且合理的数据集 . 将模式视为一种由input.xml中的每个 <copy> 调用或实例化的宏 .

到目前为止,我的解决方案只复制模式而不替换值:

[removed since irrelevant due to updates. New version below]

Update:
以下是一个开始(但是,包含@Eiríkr的回答所指出的错误,并使用对pattern.xml的硬编码引用) . 它替换元素,属性名称和值以及文本节点---并递归应用 .

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

  <xsl:output omit-xml-declaration="yes" indent="yes" />

  <xsl:variable name="pattern" select="document('pattern.xml')"/>

  <xsl:template match="/">
    <root>
      <xsl:for-each select="*/copy">
    <xsl:call-template name="substitute-element">
      <xsl:with-param name="element" select="$pattern"/>
      <xsl:with-param name="map" select="."/>
    </xsl:call-template>    
      </xsl:for-each>
    </root>
  </xsl:template>

  <xsl:template name="substitute-element">
    <xsl:param name="element"/>
    <xsl:param name="map"/>

    <!-- substitute element name -->
    <xsl:for-each select="$element/*">

      <xsl:variable name="elementName">
    <xsl:call-template name="substitute-value">
      <xsl:with-param name="value" select="name(.)"/>
      <xsl:with-param name="map" select="$map"/>
    </xsl:call-template>
      </xsl:variable>

      <xsl:element name="{$elementName}">

    <!-- replace attributes -->
    <xsl:for-each select="@*">
      <xsl:variable name="attrName">
        <xsl:call-template name="substitute-value">
          <xsl:with-param name="value" select="name(.)"/>
          <xsl:with-param name="map" select="$map"/>
        </xsl:call-template>
      </xsl:variable>


      <xsl:attribute name="{$attrName}">
        <!-- replace attribute value -->
        <xsl:call-template name="substitute-value">
          <xsl:with-param name="value" select="."/>
          <xsl:with-param name="map" select="$map"/>
        </xsl:call-template>
      </xsl:attribute>
    </xsl:for-each>


    <!-- replace element text node -->
    <xsl:call-template name="substitute-value">
      <xsl:with-param name="value" select="text()"/>
      <xsl:with-param name="map" select="$map"/>
    </xsl:call-template>


    <!-- recurse -->
    <xsl:for-each select="$element/*">
      <xsl:call-template name="substitute-element">
        <xsl:with-param name="element" select="."/>
        <xsl:with-param name="map" select="$map"/>
      </xsl:call-template>
    </xsl:for-each>

      </xsl:element>

    </xsl:for-each>
  </xsl:template>

  <xsl:template name="substitute-value">
    <xsl:param name="value"/>
    <xsl:param name="map"/>
    <xsl:choose>
      <!-- search for matching key -->
      <xsl:when test="$map/*[name(.)=$value]">
    <xsl:value-of select="$map/*[name(.)=$value]/text()"/>
      </xsl:when>
      <!-- default to incoming value if matching key not found -->
      <xsl:otherwise>
    <xsl:value-of select="$value"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>


</xsl:stylesheet>

1 回答

  • 1

    答案帖子中的XSL代码

    你问,"is it possible to avoid passing $map around everywhere?"

    如果您要以这种方式交叉应用不同的XML结构,那么您可能需要传入参数 .

    除此之外,我注意到以下几点:

    • 在答案中的XSL代码中,您有两个文件: input.xmlpattern.xml . 这些名称具有误导性 - 您需要将 input.xml 中的模式应用于 pattern.xml 中的XML结构 . 为了更流畅和更自然的XSL-y处理方式,我们想要引用 input.xml ,实际上输入 pattern.xml . 这样做也可以使您的XSL样式表更加灵活 - 您可以将其传递给任何文件而不是 pattern.xml ,而无需编辑XSL本身来更改硬编码路径 .

    • 简单地包含您在XSL本身的外部 input.xml 文件中定义的所有键值替换可能更有意义 .

    • 作为进一步的考虑,如果您使用带有 <xsl:apply-templates/>match 模板而不是使用 name d模板,则XSL本质上是递归的 .

    • 扩展 <pattern> XML结构时,代码的输出不符合您声明的规范 . 例如,扩展XML如下:

    <pattern>
        <key1>a</key1>
        <element2>key2</element2>
        <element3 attribute="key2">key1</element3>
        <element4>
            <key3>something <key1>and something else with <another key1="key1">key2</another> content.</key1></key3>
        </element4>
    </pattern>
    

    ...并且应用您的代码会产生太多输出:

    <root>
       <pattern>
          <VALUE1>a<VALUE3>something <VALUE1>and something else with <another VALUE1="VALUE1">VALUE2</another>
                </VALUE1>
             </VALUE3>
          </VALUE1>
          <element2>VALUE2<VALUE3>something <VALUE1>and something else with <another VALUE1="VALUE1">VALUE2</another>
                </VALUE1>
             </VALUE3>
          </element2>
          <element3 attribute="VALUE2">VALUE1<VALUE3>something <VALUE1>and something else with <another VALUE1="VALUE1">VALUE2</another>
                </VALUE1>
             </VALUE3>
          </element3>
          <element4>
            <VALUE3>something <VALUE1>and something else with <another VALUE1="VALUE1">VALUE2</another>
                </VALUE1>
             </VALUE3>
          </element4>
       </pattern>
       <pattern>
          <VALUE3>a<VALUE5>something <VALUE3>and something else with <another VALUE3="VALUE3">VALUE4</another>
                </VALUE3>
             </VALUE5>
          </VALUE3>
          <element2>VALUE4<VALUE5>something <VALUE3>and something else with <another VALUE3="VALUE3">VALUE4</another>
                </VALUE3>
             </VALUE5>
          </element2>
          <element3 attribute="VALUE4">VALUE3<VALUE5>something <VALUE3>and something else with <another VALUE3="VALUE3">VALUE4</another>
                </VALUE3>
             </VALUE5>
          </element3>
          <element4>
            <VALUE5>something <VALUE3>and something else with <another VALUE3="VALUE3">VALUE4</another>
                </VALUE3>
             </VALUE5>
          </element4>
       </pattern>
    </root>
    

    如果我已正确理解您的要求,那么您需要的是:

    <root>
       <pattern>
          <VALUE1>a</VALUE1>
          <element2>VALUE2</element2>
          <element3 attribute="VALUE2">VALUE1</element3>
          <element4>
            <VALUE3>something <VALUE1>and something else with <another VALUE1="VALUE1">VALUE2</another> content.</VALUE1>
             </VALUE3>
          </element4>
       </pattern>
       <pattern>
          <VALUE3>a</VALUE3>
          <element2>VALUE4</element2>
          <element3 attribute="VALUE4">VALUE3</element3>
          <element4>
            <VALUE5>something <VALUE3>and something else with <another VALUE3="VALUE3">VALUE4</another> content.</VALUE3>
             </VALUE5>
          </element4>
       </pattern>
    </root>
    

    另一种方法

    在咀嚼了一段时间之后,为了确保我理解你要做的事情,我意识到我最初理解你的代码的部分困难在于你开始处理 input.xml 文件并处理这个上下文,然后你改为处理 pattern.xml 文件并处理该上下文 .

    当您调用模板并希望该模板处理某些内容(而不仅仅是返回静态值或XML结构)时,您必须明确传入该内容,这会使上下文混乱 . 这是解决同一问题的另一种方法,使用 <xsl:apply-templates> 而不是 <xsl:call-template name="..."> .

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
    
        <xsl:output omit-xml-declaration="yes" indent="yes"/>
    
        <!-- We need to create output based on processing the 
            `pattern.xml` file, so we use that as our input. -->
    
        <!-- Despite its name, the `input.xml` file actually contains
            the pattern we wish to apply to the incoming XML file. 
            We'll store this pattern in a variable here. -->
        <xsl:variable name="replacements" select="document('input.xml')">
            <!-- Rather than relying on external files, we could leave out the
                `select` attribute and just put all the replacement information 
                right here in the XSL file. --><!--
            <root>
                <copy>
                    <key1>VALUE1</key1>
                    <key2>VALUE2</key2>
                    <key3>VALUE3</key3>
                </copy>
                <copy>
                    <key1>VALUE3</key1>
                    <key2>VALUE4</key2>
                    <key3>VALUE5</key3>
                </copy>
            </root>-->
        </xsl:variable>
    
        <!-- We assume that we don't know the names of any of the elements
            or attributes in the file we're processing. 
            If you _do_ know any of those names, specify them: literal
            XPaths tend to be faster. -->
        <xsl:template match="/">
            <!-- We put the top-level XML structure into a variable so 
                we can refer to it from within the for-each. If we 
                don't do this, we can't get at it: within the for-each,
                both the `.` operator and the `current()` function
                refer to the context item for the `for-each` call,
                and _not_ to the context item for the `template match`
                statement.  The `child::` axis in the `select` statement
                is optional; we could just use `select="*"` instead.
                I added the `child::` to make it clear for humans. -->
            <xsl:variable name="top" select="child::*"/>
            <!-- At the root level, we want to create a copy of the 
                topmost element from the $replacements variable. -->
            <xsl:element name="{name($replacements/*[1])}">
                <!-- We want to apply the key-value pairs to the topmost
                    XML structure from the input file, once for each 
                    instance of <copy> in the $replacements variable. -->
                <xsl:for-each select="$replacements/*/copy">
                    <!-- Now we apply our templates to the input
                        file structure, which we stored in $top. -->
                    <xsl:apply-templates select="$top">
                        <!-- We do need to have some way of knowing which
                            `copy` we want to reference, so we pass this
                            along as a parameter. -->
                        <xsl:with-param name="this" select="."/>
                    </xsl:apply-templates>
                </xsl:for-each>
            </xsl:element>
        </xsl:template>
    
        <!-- This matches all elements. -->
        <xsl:template match="*">
            <xsl:param name="this"/>
            <!-- We compare the current element name against all the 
                element names in $this, and use that value if found,
                or the current element name if not found. -->
            <xsl:element name="{if (name(.) = $this//*/name())
                then ($this//*[name() = name(current())])
                else (name(.))}">
                <xsl:apply-templates select="*|@*|text()">
                    <xsl:with-param name="this" select="$this"/>
                </xsl:apply-templates>
            </xsl:element>
        </xsl:template>
    
        <!-- This matches all atributes.  We evaluate both attribute
            names and attribute values, allowing us to replace either
            or both, if they match the key-value pairs in $this.-->
        <xsl:template match="@*">
            <xsl:param name="this"/>
            <xsl:variable name="attname" select="if (name(.) = $this//*/name())
                then ($this//*[name() = name(current())])
                else (name(.))"/>
            <xsl:variable name="attval" select="if (. = $this//*/name())
                then ($this//*[name() = current()])
                else (.)"/>
            <xsl:attribute name="{$attname}" select="$attval"/>
        </xsl:template>
    
        <!-- This matches all text. -->
        <xsl:template match="text()">
            <xsl:param name="this"/>
            <xsl:value-of select="if (. = $this//*/name())
                then ($this//*[name() = current()])
                else (.)"/>
        </xsl:template>
    
    </xsl:stylesheet>
    

    从您的帖子来看,您是否仅限于XSL 1.0并不完全清楚 . 我使用上面的XSL 2.0,XPath表达式中的 if () then () else () 逻辑仅适用于XSL 2.0 . 在XSL 1.0中也不支持在XPath表达式的末尾使用_2682179 . 这两个问题都可以根据需要进行编码 .

    这个XSL通过添加 <element4> 扩展输入XML产生(我相信预期的?)更短的XML输出 .

    更新 - XSL1.0版本

    以下内容大致相当于上面的XSL,但重新设计为与XSL 1.0兼容 . 主要区别:

    • $replacements 变量中引用的文件名更改为 substitution.xml .

    • 创建了一个命名模板,用于替换XSL 2.0 XPath表达式中的 if () then () else () 逻辑内联 .

    • 在模板中创建变量以调用该模板并存储该值 .

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    
        <xsl:output omit-xml-declaration="yes" indent="yes"/>
    
        <!-- Filename changed to match the new requirements. -->
        <xsl:variable name="replacements" select="document('substitution.xml')"/>
    
        <xsl:template match="/">
            <xsl:variable name="top" select="*"/>
            <xsl:element name="{name($replacements/*[1])}">
                <xsl:for-each select="$replacements/*/copy">
                    <xsl:apply-templates select="$top">
                        <xsl:with-param name="this" select="."/>
                    </xsl:apply-templates>
                </xsl:for-each>
            </xsl:element>
        </xsl:template>
    
        <!-- This matches all elements. -->
        <xsl:template match="*">
            <xsl:param name="this"/>
            <xsl:variable name="elem_name">
                <!-- XSL 1.0 does not support `if / then / else` logic in
                    XPath statements, so we use this variable instead to
                    call a named template containing the logic, and then
                    to store the resulting value. -->
                <xsl:call-template name="get_sub_value">
                    <xsl:with-param name="this" select="$this"/>
                    <xsl:with-param name="val" select="name()"/>
                </xsl:call-template>
            </xsl:variable>
            <xsl:element name="{$elem_name}">
                <xsl:apply-templates select="*|@*|text()">
                    <xsl:with-param name="this" select="$this"/>
                </xsl:apply-templates>
            </xsl:element>
        </xsl:template>
    
        <!-- This matches all atributes. -->
        <xsl:template match="@*">
            <xsl:param name="this"/>
            <xsl:variable name="attname">
                <xsl:call-template name="get_sub_value">
                    <xsl:with-param name="this" select="$this"/>
                    <xsl:with-param name="val" select="name()"/>
                </xsl:call-template>
            </xsl:variable>
    
            <xsl:attribute name="{$attname}">
                <!-- For attribute alues, we just call `get_sub_value` directly. -->
                <xsl:call-template name="get_sub_value">
                    <xsl:with-param name="this" select="$this"/>
                    <xsl:with-param name="val" select="."/>
                </xsl:call-template>
            </xsl:attribute>
        </xsl:template>
    
        <!-- This matches all text. -->
        <xsl:template match="text()">
            <xsl:param name="this"/>
            <!-- For straight text, we just call `get_sub_value` directly. -->
            <xsl:call-template name="get_sub_value">
                <xsl:with-param name="this" select="$this"/>
                <xsl:with-param name="val" select="."/>
            </xsl:call-template>
        </xsl:template>
    
        <!-- Named template to handle the logic of looking for key-value pairs
            in the relevant `<copy>` element from the `substitution.xml` file. -->
        <xsl:template name="get_sub_value">
            <xsl:param name="this"/>
            <xsl:param name="val"/>
            <xsl:choose>
                <xsl:when test="$this//*[name() = $val]">
                    <xsl:value-of select="$this//*[name() = $val]"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="$val"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:template>
    
    </xsl:stylesheet>
    

    我对您在浏览器中执行此操作的用例感到有点困惑,但您知道自己需要什么 . :)

    使用Xalan,Saxon 6.5和Saxon 9.6.0.7处理器在Oxygen XML v17.1中进行了测试 . 唯一的区别是缩进 .

相关问题