XSLT 仅使用 XSLT 1.0

问题描述

我需要将 XML 数据从一种结构转换为另一种结构。我需要根据源 XML 中的元数据来构建目标 XML。源 XML 具有固定结构,但需要根据源 XML 中的元数据动态构建目标 XML 的结构(包括标签名称和数据分组)。

以下源 XML 提供了结构示例。源 XML 将始终使用 FMPXMLRESULT 结构。

XML

<?xml version="1.0" encoding="UTF-8" ?>
<FMPXMLRESULT xmlns="http://www.filemaker.com/fmpxmlresult">
  <MetaDATA>
    <FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="ID" TYPE="NUMBER"/>
    <FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="Description" TYPE="TEXT"/>
    <FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="Customer" TYPE="TEXT"/>
    <FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderItem::ProductName" TYPE="TEXT"/>
    <FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderItem::UnitPrice" TYPE="NUMBER"/>
    <FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderItem::Quantity" TYPE="NUMBER"/>
    <FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderItem::TaxCode" TYPE="TEXT"/>
    <FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderItem::Total" TYPE="NUMBER"/>
    <FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderTaxCode::TaxCode" TYPE="TEXT"/>
    <FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderTaxCode::TaxRate" TYPE="NUMBER"/>
  </MetaDATA>
  <RESULTSET FOUND="2">
    <ROW MODID="1" RECORDID="1">
      <COL><DATA>1</DATA></COL>
      <COL><DATA>Order for first project</DATA></COL>
      <COL><DATA>Customer No 1</DATA></COL>
      <COL>
        <DATA>Product A</DATA>
        <DATA>Product B</DATA>
      </COL>
      <COL>
        <DATA>10.50</DATA>
        <DATA>12.10</DATA>
      </COL>
      <COL>
        <DATA>2</DATA>
        <DATA>1</DATA>
      </COL>
      <COL>
        <DATA>VAT</DATA>
        <DATA>VAT0</DATA>
      </COL>
      <COL>
        <DATA>21</DATA>
        <DATA>12.1</DATA>
      </COL>
      <COL>
        <DATA>VAT</DATA>
        <DATA>VAT0</DATA>
      </COL>
      <COL>
        <DATA>0.2</DATA>
        <DATA>0</DATA>
      </COL>
    </ROW>
    <ROW MODID="1" RECORDID="2">
      <COL><DATA>2</DATA></COL>
      <COL><DATA>Order for second project</DATA></COL>
      <COL><DATA>Customer No 2</DATA></COL>
      <COL>
        <DATA>Product 2A</DATA>
        <DATA>Product 2B</DATA>
      </COL>
      <COL>
        <DATA>1.50</DATA>
        <DATA>345</DATA>
      </COL>
      <COL>
        <DATA>17</DATA>
        <DATA>2</DATA>
      </COL>
      <COL>
        <DATA>VAT0</DATA>
        <DATA>VAT</DATA>
      </COL>
      <COL>
        <DATA>25.5</DATA>
        <DATA>690</DATA>
      </COL>
      <COL>
        <DATA>VAT</DATA>
        <DATA>VAT0</DATA>
      </COL>
      <COL>
        <DATA>0.2</DATA>
        <DATA>0</DATA>
      </COL>
    </ROW>
  </RESULTSET>
</FMPXMLRESULT>

鉴于上述 XML(包括元数据),目标 XML 格式如下。

XML

<?xml version="1.0" encoding="UTF-8"?>
<OrderBatch>
  
  <Order>
    <ID>1</ID>
    <Description>Order for first project</Description>
    <Customer>Customer No 1</Customer>
    <OrderItem>
      <ProductName>Product A</ProductName>
      <UnitPrice>10.50</UnitPrice>
      <Quantity>2</Quantity>
      <TaxCode>VAT</TaxCode>
      <Total>21</Total>
    </OrderItem>
    <OrderItem>
      <ProductName>Product B</ProductName>
      <UnitPrice>12.10</UnitPrice>
      <Quantity>1</Quantity>
      <TaxCode>VAT0</TaxCode>
      <Total>12.1</Total>
    </OrderItem>
    <OrderTaxCode>
      <TaxCode>VAT</TaxCode>
      <TaxRate>0.2</TaxRate>
    </OrderTaxCode>
    <OrderTaxCode>
      <TaxCode>VAT0</TaxCode>
      <TaxRate>0</TaxRate>
    </OrderTaxCode>
  </Order>

  <Order>
    <ID>2</ID>
    <Description>Order for second project</Description>
    <Customer>Customer No 2</Customer>
    <OrderItem>
      <ProductName>Product 2A</ProductName>
      <UnitPrice>1.50</UnitPrice>
      <Quantity>17</Quantity>
      <TaxCode>VAT0</TaxCode>
      <Total>25.5</Total>
    </OrderItem>
    <OrderItem>
      <ProductName>Product 2B</ProductName>
      <UnitPrice>345</UnitPrice>
      <Quantity>2</Quantity>
      <TaxCode>VAT</TaxCode>
      <Total>690</Total>
    </OrderItem>
    <OrderTaxCode>
      <TaxCode>VAT</TaxCode>
      <TaxRate>0.2</TaxRate>
    </OrderTaxCode>
    <OrderTaxCode>
      <TaxCode>VAT0</TaxCode>
      <TaxRate>0</TaxRate>
    </OrderTaxCode>
  </Order>

</OrderBatch>

源 XML 中的不同元数据会产生不同的目标 XML。一般规则如下

  1. 字段名称包含在 MetaDATA 中,数据包含在 RESULTSET 中
  2. RESULTSET 每一行的 COL 节点与 MetaDATA 中的 FIELD 节点按位置对应
  3. 名称包含文本“::”的任何结果集/字段都应被视为“分组”数据
  4. 分组数据的组名应该等于“::”之前的文本(此符号在 FIELD NAME 中只会出现一次)
  5. 分组数据 COL 节点可能包含 0、1 或多个 DATA 子节点
  6. 未分组的 FIELD 节点(即 NAME 不包含“::”)在 COL 节点中将始终只有 1 个 DATA 子节点
  7. 分组的数据字段将始终相邻(例如,ORDERITEM 组中的所有字段:: 在字段顺序中它们之间不会有来自其他组的字段)
  8. 组名、字段名和字段顺序事先不知道并且可能会改变,XSLT 需要动态处理这些。上面的 XML 是需要处理的事情的一个很好的例子
  9. 我只能使用 XSLT 1.0
  10. FMPDSORESULT 是一项已弃用的技术,我无法使用

两个主要症结是

  1. 按位置从 COL 节点中拉出 DATA 节点并分配给它们自己的组
  2. 实现所需的元数据分离

我尝试了多种嵌套 for-each 循环的方法,以及组织模板的不同方法。我想知道创建内部数据结构是否可能是要走的路,但我也可能看错了问题?

这是迄今为止我想出的最好的,这是我最近的,但仍然不够接近

XSLT 1.0

<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:fmp="http://www.filemaker.com/fmpxmlresult"
  exclude-result-prefixes="fmp"
>
  <xsl:output indent="yes"/>


  <!-- the key indexes the MetaDATA fields by their position -->
  <xsl:key
    name="fieldList"
    match="fmp:MetaDATA/fmp:FIELD"
    use="count(preceding-sibling::fmp:FIELD) + 1"
  />

  <!-- template for the data section of the FileMaker XML -->
  <xsl:template match="/fmp:FMPXMLRESULT">
    <OrderBatch>
      <xsl:apply-templates select="fmp:RESULTSET/fmp:ROW" />
    </OrderBatch>
  </xsl:template>

    <!-- template for each row -->
  <xsl:template match="fmp:ROW">
        <!-- for each row,create Order element and apply the relevant template for each column -->
    <Order>
      <xsl:apply-templates select="fmp:COL" />
    </Order>
  </xsl:template>

  <!-- template for each column within each row -->
  <xsl:template match="fmp:COL">
        <!-- set $qualified with the name of the field - this will be qualified with the table occurrence if related -->
    <xsl:variable name="qualified" select="string(key('fieldList',position())/@NAME)"/>
    <!-- set $group with the name of the field's group -->
    <xsl:variable name="group" select="substring-before($qualified,'::')"/>
        <!-- set $name to a value for use as an XML element -->
        <xsl:variable name="name">
            <xsl:choose>

                <!-- if the qualified field is related (contains "::") then remove the table occurrence name -->
            <xsl:when test="contains($qualified,'::')">
                    <xsl:value-of select="substring-after($qualified,'::')"/>
            </xsl:when>

                <!-- if the qualified field is not related then just return the field name -->
            <xsl:otherwise>
                    <xsl:value-of select="$qualified"/>
            </xsl:otherwise>
            </xsl:choose>
        </xsl:variable>



    <!-- create the element with the field's name and use the data as the element's value -->
    <xsl:choose>
      <!-- related element - need to group -->
      <xsl:when test="contains($qualified,'::')">
        <!-- group each DATA element in turn -->
        <!-- actually only need to run this on the first COL in a group - but I'll figure that out later -->
        <xsl:for-each select="fmp:DATA">
          <xsl:apply-templates select=".">
            <xsl:with-param name="pGroup" select="$group" />
            <xsl:with-param name="pName" select="$name" />
          </xsl:apply-templates>
        </xsl:for-each>
      </xsl:when>

      <!-- element is at top level so just create the field/value -->
      <xsl:otherwise>
        <xsl:element name="{$name}">
                <xsl:value-of select="." />
            </xsl:element>
        </xsl:otherwise>
        </xsl:choose>

    </xsl:template>

  <!-- template for grouping DATA nodes across multiple COL nodes -->
  <xsl:template match="fmp:DATA">
    <xsl:param name = "pGroup" />
    <xsl:param name = "pName" />
    <xsl:element name="{$pGroup}">
      <xsl:variable name="pos" select="position()" />
        <xsl:apply-templates select="../../fmp:COL" mode="group">
          <xsl:with-param name="pGroup" select="$pGroup" />
          <xsl:with-param name="pName" select="$pName" />
          <xsl:with-param name="pos" select="$pos" />
        </xsl:apply-templates>

    </xsl:element>
  </xsl:template>

  <!-- template for cycling through COL nodes and getting the DATA node if it belongs to the specified group -->
  <xsl:template match="fmp:COL" mode="group">
    <xsl:param name = "pGroup" />
    <xsl:param name = "pName" />
    <xsl:param name = "pos" /> <!-- this will help select the correct DATA node - not sure how to use it yet though -->
    <xsl:variable name="qualified" select="string(key('fieldList',position())/@NAME)"/>
    <xsl:variable name="colGroup" select="substring-before($qualified,'::')"/>
    <xsl:if test="contains($qualified,'::') and $pGroup = $colGroup">
      <xsl:element name="substring-after($qualified,'::')">
                <xsl:value-of select="." />
            </xsl:element>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

我知道这并不容易,也不是使用 XSLT 的正常方式(通常会编写以适应目标结构),但是我相信这是一个可以解决的问题,而且 XSLT 似乎能够做到更复杂的任务。

非常欢迎有关如何解决此问题的任何帮助。非常感谢。

解决方法

这可能对您有所帮助。您需要计算出前缀才能使用如上所述的节点集函数。您可以验证假设并进行调整。如果您有任何问题,请告诉我。

<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:fmp="http://www.filemaker.com/fmpxmlresult"
  xmlns:msxml="urn:schemas-microsoft-com:xslt"
  exclude-result-prefixes="fmp">

  <xsl:output indent="yes"/>

  <xsl:variable name="metadata">
    <xsl:for-each select="//fmp:FIELD">
      <xsl:element name="element">
        <xsl:variable name="name" select="@NAME"/>
        <xsl:choose>
          <xsl:when test="contains($name,'::')">
            <xsl:element name="parent">
              <xsl:value-of select="substring-before($name,'::')"/>
            </xsl:element>
            <xsl:element name="child">
              <xsl:value-of select="substring-after($name,'::')"/>
            </xsl:element>
          </xsl:when>
          <xsl:otherwise>
            <xsl:element name="parent">
              <xsl:value-of select="$name"/>
            </xsl:element>
            <xsl:element name="child"/>
          </xsl:otherwise>
        </xsl:choose>  
        <xsl:element name="position">
          <xsl:value-of select="position()"/>
        </xsl:element>
    </xsl:element>
    </xsl:for-each>
  </xsl:variable>

  <!-- NOTE:  Replace msxml with your prefix that converts RTF's to node sets.  -->
  <xsl:variable name="metadataList" select="msxml:node-set($metadata)"/>

  <!-- Get a distinct list of parent elements.  -->
  <xsl:variable name="parents">
    <xsl:copy-of select="$metadataList/element[not(parent = preceding-sibling::element/parent)]"/>  
  </xsl:variable>

  <xsl:variable name="parentList" select="msxml:node-set($parents)"/>

  <xsl:template match="fmp:FMPXMLRESULT">
    <xsl:element name="OrderBatch">
      <xsl:apply-templates select="node()"/>
    </xsl:element>
  </xsl:template>

  <xsl:template match="fmp:ROW">
    <xsl:variable name="rowNode" select="."/> 
    <xsl:element name="Order">
      <!-- Loop thru the distinct list of parents.  -->
      <xsl:for-each select="$parentList/element">
        <xsl:variable name="parent" select="parent"/>
        <xsl:variable name="firstSetPosition" select="position"/>
        <!-- Loop thru on the first set of data nodes for this parent.  -->
        <xsl:for-each select="$rowNode/fmp:COL[number($firstSetPosition)]/fmp:DATA">
          <xsl:variable name="dataposition" select="position()"/>
          <xsl:element name="{$parent}">
            <!-- Loop thru all the child nodes for this parent.  -->
            <xsl:for-each select="$metadataList/element[parent = $parent]">
              <xsl:variable name="position" select="position"/>
              <xsl:variable name="child" select="string(child)"/>
              <xsl:choose>
                <!-- When the parent has no child nodes.  -->
                <xsl:when test="$child = ''">
                  <xsl:value-of select="string($rowNode/fmp:COL[number($position)]/fmp:DATA[$dataposition])"/>
                </xsl:when>
                <xsl:otherwise>
                  <xsl:element name="{$child}">
                    <xsl:value-of select="string($rowNode/fmp:COL[number($position)]/fmp:DATA[$dataposition])"/>
                  </xsl:element>
                </xsl:otherwise>
              </xsl:choose>
            </xsl:for-each>
          </xsl:element>
        </xsl:for-each>
        </xsl:for-each>  
      </xsl:element>
  </xsl:template>

  <xsl:template match="fmp:DATA">
    <xsl:value-of select="."/>
  </xsl:template>

  <xsl:template match="node()">
      <xsl:apply-templates select="node()"/>
  </xsl:template>
</xsl:stylesheet>
,

FMPXMLRESULT 语法的优势在于它允许您在不破坏 XSLT 样式表的情况下更改解决方案的字段名称。输出元素和属性名称应该符合目标应用程序的 XML 架构,并被硬编码到样式表中。

因此,我建议您忽略包含在 METADATA 部分中的字段名称,并仅根据字段在字段导出顺序中的位置进行转换。否则,您应该使用 FMPDSORESULT 语法,您可以在其中更改字段导出顺序,但不能更改字段名称。

考虑到这一点,您的样式表可能如下所示:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fmp="http://www.filemaker.com/fmpxmlresult" 
exclude-result-prefixes="fmp">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:template match="/fmp:FMPXMLRESULT">
    <OrderBatch>
        <xsl:for-each select="fmp:RESULTSET/fmp:ROW">
            <Order>
                <!-- order -->
                <ID>
                    <xsl:value-of select="fmp:COL[1]/fmp:DATA"/>
                </ID>
                <Description>
                    <xsl:value-of select="fmp:COL[2]/fmp:DATA"/>
                </Description>
                <Customer>
                    <xsl:value-of select="fmp:COL[3]/fmp:DATA"/>
                </Customer>
                <!-- items -->
                <xsl:for-each select="fmp:COL[4]/fmp:DATA">
                    <xsl:variable name="i" select="position()"/>
                    <OrderItem>
                        <ProductName>
                            <xsl:value-of select="."/>
                        </ProductName>
                        <UnitPrice>
                            <xsl:value-of select="../../fmp:COL[5]/fmp:DATA[$i]"/>
                        </UnitPrice>
                        <Quantity>
                            <xsl:value-of select="../../fmp:COL[6]/fmp:DATA[$i]"/>
                        </Quantity>
                        <TaxCode>
                            <xsl:value-of select="../../fmp:COL[7]/fmp:DATA[$i]"/>
                        </TaxCode>
                        <Total>
                            <xsl:value-of select="../../fmp:COL[8]/fmp:DATA[$i]"/>
                        </Total>
                    </OrderItem>
                </xsl:for-each>
                <!-- tax codes -->
                <xsl:for-each select="fmp:COL[9]/fmp:DATA">
                    <xsl:variable name="i" select="position()"/>
                    <OrderTaxCode>
                        <TaxCode>
                            <xsl:value-of select="."/>
                        </TaxCode>
                        <TaxRate>
                            <xsl:value-of select="../../fmp:COL[10]/fmp:DATA[$i]"/>
                        </TaxRate>
                    </OrderTaxCode>
                </xsl:for-each>
            </Order>
        </xsl:for-each>
    </OrderBatch>
</xsl:template>

</xsl:stylesheet>
,

为了解决这个问题,我在 XSLT 中采用了 2 pass 方法并使用了 xmlns:exslt 扩展。

正如已经指出的那样,确实没有完全通用的解决方案,也没有完全面向未来的解决方案,但是我这里的解决方案足以满足我的需求。我已经评论/记录了下面的代码。希望其他人会发现它很有用,甚至将来会对其进行改进。

XSLT 1.0

<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:fmp="http://www.filemaker.com/fmpxmlresult"
  xmlns:exslt="http://exslt.org/common"
  exclude-result-prefixes="fmp"
>
  <xsl:output indent="yes"/>

  <!--
  NOTES ON NON-GENERALISED ELEMENTS

  REQUIRED NODE - <pk>
  each record in the source XML must contain a node named <pk> for the following to work
  this is required when defining the matching criteria for the xsl:key "KeyGroups",and anywhere that needs to match this key
  the <pk> node is used in the second pass,the name of the <pk> node would need to be amended in all 3 places in the second pass if it needed changing

  SPECIFIC NODE NAMES - this information can't be derived from the source document
  OrderBatch - is the name of the root node that will contain the target XML
  Order - is the name of each of the record nodes inside the container
  these node names can be changed as long as every instance of them is changed throughout the document

  OVERRIDE FIELD NAMES - there is a section in the first pass that allows the renaming of field names (by substituting the fully qualified field name for a custom string)
  the code should still work without any renaming as the renaming serves no functional purpose it is merely a way to tidy field names up if needed
  -->


  <!-- index the METADATA fields by their position -->
  <xsl:key
    name="KeyMetaData"
    match="fmp:METADATA/fmp:FIELD"
    use="count(preceding-sibling::fmp:FIELD) + 1"
  />



  <!-- ### FIRST PASS ### -->

  <!-- set the results of the first pass into $firstPassResult,this will then be used in the second pass -->
  <xsl:variable name="firstPassResult">
    <xsl:apply-templates select="/" mode="firstPass"/>
  </xsl:variable>

  <!-- template to start reading the data from FMPXMLRESULT -->
  <xsl:template match="/fmp:FMPXMLRESULT" mode="firstPass">
    <OrderBatch>
      <xsl:apply-templates select="fmp:RESULTSET/fmp:ROW" mode="firstPass" />
    </OrderBatch>
  </xsl:template>

    <!-- template for each row in FMPXMLRESULT -->
  <xsl:template match="fmp:ROW" mode="firstPass">
        <!-- for each row,create Order element and apply the relevant template for each column -->
    <Order>
      <xsl:apply-templates select="fmp:COL" mode="firstPass" />
    </Order>
  </xsl:template>

  <!-- template for each column in FMPXMLRESULT -->
  <xsl:template match="fmp:COL" mode="firstPass">
        <!-- set $qualified with the name of the field - this will be qualified with the table occurrence if related -->
    <xsl:variable name="qualified" select="string(key('KeyMetaData',position())/@NAME)"/>
    <xsl:variable name="group" select="translate(substring-before($qualified,'::'),' :.','')"/>

        <!-- set $name to a value for use as an XML element -->
        <xsl:variable name="name">
            <xsl:choose>

                <!-- ################################################## -->
                <!-- SPECIFIC FIELD NAME OVERRIDES HERE -->
                <xsl:when test="$qualified = 'order.order_revision.company_CUSTOMER::company_name'"><xsl:value-of select="'CustomerName'"/></xsl:when>
                <xsl:when test="$qualified = 'order.order_revision.company_SUPPLIER::company_name'"><xsl:value-of select="'SupplierName'"/></xsl:when>
                <!-- ################################################## -->

                <!-- if the qualified field is related (contains "::") then $name shouldn't include the table occurrence name -->
            <xsl:when test="contains($qualified,'::')">
                    <xsl:value-of select="translate(substring-after($qualified,'')"/>
            </xsl:when>

                <!-- if the qualified field is not related then qualified will be the field name -->
            <xsl:otherwise>
                    <xsl:value-of select="translate($qualified,'')"/>
            </xsl:otherwise>
            </xsl:choose>
        </xsl:variable>

        <!-- create the element with the field's name and use the data as the element's value -->
        <xsl:for-each select="fmp:DATA">
            <xsl:element name="{$name}">
        <!-- include some attributes for elements that need to be grouped in the next pass -->
                <xsl:if test="count(preceding-sibling::fmp:DATA | following-sibling::fmp:DATA) > 0">
          <!-- add an attribute 'g' to indicate the name of the group it belongs to -->
          <xsl:attribute name="g">
                        <xsl:value-of select="$group" />
                    </xsl:attribute>
          <!-- add an attribute 'n' to indicate the which iteration of the data this is -->
          <xsl:attribute name="n">
                        <xsl:value-of select="count(preceding-sibling::fmp:DATA) + 1" />
                    </xsl:attribute>
                </xsl:if>
        <!-- populate with data from the original node -->
                <xsl:value-of select="." />
            </xsl:element>
        </xsl:for-each>
    </xsl:template>



  <!-- ### SECOND PASS ### -->

  <!-- the second pass uses $firstPassResult (converted to a node set using namepsace exslt:node-set as declared in header) -->
  <xsl:template match="/">
    <xsl:apply-templates select="exslt:node-set($firstPassResult)" mode="secondPass"/>
  </xsl:template>

  <!-- template for the outer container -->
  <xsl:template match="OrderBatch" mode="secondPass">
    <xsl:copy>
      <xsl:apply-templates select="Order" mode="secondPass" />
    </xsl:copy>
  </xsl:template>

  <!-- template for each record -->
  <xsl:template match="Order" mode="secondPass">
    <xsl:call-template name="grouping" />
  </xsl:template>

  <!-- index unique groups,uniqueness is based on
  attribute @g - the group as defined in first pass
  attribute @n - the data iteration as defined in first pass
  the parent record's <pk> node - the requirement of this node is an unfortunate but manageable constraint on the source data -->
  <xsl:key name="KeyGroups" match="*[@n]" use="concat(@g,'+',@n,../pk)" />

  <!-- the grouping template -->
  <xsl:template match="@*|node()" name="grouping" mode="secondPass">
    <xsl:copy><xsl:apply-templates select="@*|node()" mode="secondPass"/></xsl:copy>
  </xsl:template>

  <!-- template to match the first element with each @gr value -->
  <xsl:template match="*[@n][generate-id() =
         generate-id(key('KeyGroups',concat(@g,../pk))[1])]" priority="2" mode="secondPass">
   <xsl:element name="{@g}">
      <xsl:for-each select="key('KeyGroups',../pk))">
        <xsl:call-template name="grouping" />
      </xsl:for-each>
    </xsl:element>
  </xsl:template>

  <!-- ignore subsequent nodes,they're handled within the first element template -->
  <xsl:template match="*[@n]" priority="1" mode="secondPass" />

</xsl:stylesheet>