Avoid duplicated attributes names with XSLT/ XPath

故事扮演 提交于 2019-12-12 03:48:26

问题


Say I have an XML like that:

<parole>
   <parola id="a">1</parola>
   <parola id="b">2</parola>
   <parola id="c">3</parola>
   <parola id="a">4</parola>
   <parola id="a">5</parola>
   <parola id="b">6</parola>
</parole>

Now, I know that the generate-id() function exists. But, for a learning purpose, I would like to know how to change with XSLT the values of the attributes called "id".

I've thought about an "algorithm" like:

consider the following and the preceding sibling of an attribute.
If you meet a duplicate of the present attribute, add "f" to the end of this name and (recursively) add another "f" until we have no more attributes with the same value.

So my final XML would be like:

<parole>
  <parola id="a">1</parola>
  <parola id="b">2</parola>
  <parola id="c">3</parola>
  <parola id="af">4</parola>
  <parola id="aff">5</parola>
  <parola id="bf">6</parola>
</parole>

Now, I've tried to get this result with a recursive function like:

<xsl:variable name="following-siblings-ids" select="/parole/parola/following-sibling::parola/@id"/>
<xsl:variable name="preceding-siblings-ids" select="/parole/parola/preceding-sibling::parola/@id"/>

<xsl:function name="du:check" as="xs:string">
    <xsl:param name="id" /> 
    <xsl:choose>
        <xsl:when test="$id  = $following-siblings-ids and $preceding-siblings-ids">
            <xsl:value-of select="du:check(concat($id, 'f'))"/>
        </xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="$id"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:function>

<xsl:template match="parola">
  <xsl:value-of select="du:check(@id)"/>
</xsl:template match="parola">

But I get a result like

"af" "bf" "cf" "af" "af" "bf"

instead of the the desired one.

Any hints? This would be a simple task in a non-declarative language with dynamic variables and for-eachs, but I don't know how to realize it in XSLT...


回答1:


Here is an XSLT 1 solution. The following transformation

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

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

  <xsl:template name="make-f">
    <xsl:param name="n"/>
    <xsl:if test="$n > 0">f<xsl:call-template name="make-f">
    <xsl:with-param name="n" select="$n - 1"/></xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template match="parola/@id">
    <xsl:variable name="id" select="."/>
    <xsl:variable name="f">
      <xsl:call-template name="make-f">
    <xsl:with-param name="n" select="count(../preceding-sibling::parola/@id[.=$id])"/>
      </xsl:call-template>
    </xsl:variable>
    <xsl:attribute name="id">
      <xsl:value-of select="concat(., $f)"/>
    </xsl:attribute>
  </xsl:template>

</xsl:stylesheet>

when applied to your input sample, produces the following output:

$ xsltproc test.xsl test.xml
<?xml version="1.0"?>
<parole>
  <parola id="a">1</parola>
  <parola id="b">2</parola>
  <parola id="c">3</parola>
  <parola id="af">4</parola>
  <parola id="aff">5</parola>
  <parola id="bf">6</parola>
</parole>



回答2:


I would use a key to "group" the elements by the id attribute and then identify the position in that group to append an index or if you want the number of f letters:

<?xml version="1.0" encoding="UTF-8"?>
<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"
    exclude-result-prefixes="xs mf"
    version="2.0">

    <xsl:param name="n" select="'f'"/>

    <xsl:function name="mf:node-index-of" as="xs:integer">
        <xsl:param name="node" as="node()"/>
        <xsl:param name="sequence" as="node()*"/>
        <xsl:sequence select="for $pos in 1 to count($sequence) return $pos[$node is $sequence[$pos]]"/>
    </xsl:function>

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

    <xsl:key name="p-by-id" match="parola" use="@id"/>

    <xsl:template match="parola[not(. is key('p-by-id', @id)[1])]/@id">
        <xsl:attribute name="id" select="concat(., string-join(for $i in 1 to mf:node-index-of(.., key('p-by-id', .)) - 1 return $n, ''))"/>
    </xsl:template>

</xsl:stylesheet>

The template with match="@* | node()" is the so called identity transformation template that copies everything unchanged level by level, allowing us to add further templates for the nodes we want to transform.

The only node you want to transform is the id attribute of parola elements where more than one exists with a certain id value, so I have written a template match="parola[not(. is key('p-by-id', @id)[1])]/@id" for that, using a key <xsl:key name="p-by-id" match="parola" use="@id"/> to identify all elements with the same value.

The new attribute value concat(., string-join(for $i in 1 to mf:node-index-of(.., key('p-by-id', .)) - 1 return $n, '')) is then computed by concatenating the existing value with a sequence containing $n one less that the number of times of the positional index of the parent parola in its group.

If you are new to XSLT and XPath 2.0 take some time to read up in a tutorial or book on the identity transformation, keys, and user defined functions.



来源:https://stackoverflow.com/questions/40794064/avoid-duplicated-attributes-names-with-xslt-xpath

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!