How do you output the current element path in XSLT?

前端 未结 5 827
名媛妹妹
名媛妹妹 2020-11-30 02:18

In XSLT, is there a way to determine where you are in an XML document when processing an element?

Example: Given the following XML Doc Fragment...

&l         


        
相关标签:
5条回答
  • 2020-11-30 02:22

    You can use the ancestor XPath Axes to walk all parent, and grandparents.

    <xsl:for-each select="ancestor::*">...
    
    0 讨论(0)
  • 2020-11-30 02:26

    Since XPath 3.0 as supported by Saxon 9.8 (all editions) or Saxon 9.7 with version="3.0" in the XSLT and XmlPrime 4 (using --xt30) as well as 2017 releases of Altova (using version="3.0" stylesheets) there is the built-in path function (https://www.w3.org/TR/xpath-functions-30/#func-path, https://www.w3.org/TR/xpath-functions-31/#func-path) which for an input like

    <?xml version="1.0" encoding="UTF-8"?>
    <Doc>
        <Ele1>
            <Ele11>
                <Ele111>
                    <foo/>
                    <foo/>
                    <bar/>
                    <foo/>
                    <foo/>
                    <bar/>
                    <bar/>
                </Ele111>
            </Ele11>
        </Ele1>
        <Ele2/>  
    </Doc>
    

    and a stylesheet like

    <?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:math="http://www.w3.org/2005/xpath-functions/math"
        exclude-result-prefixes="xs math"
        version="3.0">
    
        <xsl:output method="text"/>
    
        <xsl:template match="/">
            <xsl:value-of select="//*/path()" separator="&#10;"/>
        </xsl:template>
    
    </xsl:stylesheet>
    

    gives the output

    /Q{}Doc[1]
    /Q{}Doc[1]/Q{}Ele1[1]
    /Q{}Doc[1]/Q{}Ele1[1]/Q{}Ele11[1]
    /Q{}Doc[1]/Q{}Ele1[1]/Q{}Ele11[1]/Q{}Ele111[1]
    /Q{}Doc[1]/Q{}Ele1[1]/Q{}Ele11[1]/Q{}Ele111[1]/Q{}foo[1]
    /Q{}Doc[1]/Q{}Ele1[1]/Q{}Ele11[1]/Q{}Ele111[1]/Q{}foo[2]
    /Q{}Doc[1]/Q{}Ele1[1]/Q{}Ele11[1]/Q{}Ele111[1]/Q{}bar[1]
    /Q{}Doc[1]/Q{}Ele1[1]/Q{}Ele11[1]/Q{}Ele111[1]/Q{}foo[3]
    /Q{}Doc[1]/Q{}Ele1[1]/Q{}Ele11[1]/Q{}Ele111[1]/Q{}foo[4]
    /Q{}Doc[1]/Q{}Ele1[1]/Q{}Ele11[1]/Q{}Ele111[1]/Q{}bar[2]
    /Q{}Doc[1]/Q{}Ele1[1]/Q{}Ele11[1]/Q{}Ele111[1]/Q{}bar[3]
    /Q{}Doc[1]/Q{}Ele2[1]
    

    That output is not as compact in case of lack of namespaces as most hand-made attempts but the format has the advantage (at least given XPath 3.0 or 3.1 support) to allow for namespaces being used and get a format for the returned path that does not require the user of the path expression to set up any namespace bindings to evaluate it.

    0 讨论(0)
  • 2020-11-30 02:42

    The currently accepted answer will return incorrect paths. For example, the element Ele2 in the OP sample XML would return the path /Doc[1]/Ele2[2]. It should be /Doc[1]/Ele2[1].

    Here's a similar XSLT 1.0 template that returns the correct paths:

      <xsl:template name="genPath">
        <xsl:param name="prevPath"/>
        <xsl:variable name="currPath" select="concat('/',name(),'[',
          count(preceding-sibling::*[name() = name(current())])+1,']',$prevPath)"/>
        <xsl:for-each select="parent::*">
          <xsl:call-template name="genPath">
            <xsl:with-param name="prevPath" select="$currPath"/>
          </xsl:call-template>
        </xsl:for-each>
        <xsl:if test="not(parent::*)">
          <xsl:value-of select="$currPath"/>      
        </xsl:if>
      </xsl:template>
    

    Here's an example that will add a path attribute to all elements.

    XML Input

    <Doc>
      <Ele1>
        <Ele11>
          <Ele111>
            <foo/>
            <foo/>
            <bar/>
            <foo/>
            <foo/>
            <bar/>
            <bar/>
          </Ele111>
        </Ele11>
      </Ele1>
      <Ele2/>  
    </Doc>
    

    XSLT 1.0

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output indent="yes"/>
      <xsl:strip-space elements="*"/>
    
      <xsl:template match="text()|@*">
        <xsl:copy>
          <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="*">
        <xsl:copy>
          <xsl:attribute name="path">
            <xsl:call-template name="genPath"/>
          </xsl:attribute>
          <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>    
      </xsl:template>
    
      <xsl:template name="genPath">
        <xsl:param name="prevPath"/>
        <xsl:variable name="currPath" select="concat('/',name(),'[',
          count(preceding-sibling::*[name() = name(current())])+1,']',$prevPath)"/>
        <xsl:for-each select="parent::*">
          <xsl:call-template name="genPath">
            <xsl:with-param name="prevPath" select="$currPath"/>
          </xsl:call-template>
        </xsl:for-each>
        <xsl:if test="not(parent::*)">
          <xsl:value-of select="$currPath"/>      
        </xsl:if>
      </xsl:template>
    
    </xsl:stylesheet>
    

    XML Output

    <Doc path="/Doc[1]">
       <Ele1 path="/Doc[1]/Ele1[1]">
          <Ele11 path="/Doc[1]/Ele1[1]/Ele11[1]">
             <Ele111 path="/Doc[1]/Ele1[1]/Ele11[1]/Ele111[1]">
                <foo path="/Doc[1]/Ele1[1]/Ele11[1]/Ele111[1]/foo[1]"/>
                <foo path="/Doc[1]/Ele1[1]/Ele11[1]/Ele111[1]/foo[2]"/>
                <bar path="/Doc[1]/Ele1[1]/Ele11[1]/Ele111[1]/bar[1]"/>
                <foo path="/Doc[1]/Ele1[1]/Ele11[1]/Ele111[1]/foo[3]"/>
                <foo path="/Doc[1]/Ele1[1]/Ele11[1]/Ele111[1]/foo[4]"/>
                <bar path="/Doc[1]/Ele1[1]/Ele11[1]/Ele111[1]/bar[2]"/>
                <bar path="/Doc[1]/Ele1[1]/Ele11[1]/Ele111[1]/bar[3]"/>
             </Ele111>
          </Ele11>
       </Ele1>
       <Ele2 path="/Doc[1]/Ele2[1]"/>
    </Doc>
    

    Here's another version that only outputs the positional predicate if it's needed. This example is also different in that it's just outputting the path instead of adding an attribute.

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output method="text"/>
        <xsl:strip-space elements="*"/>
    
        <xsl:template match="text()"/>
    
        <xsl:template match="*">
            <xsl:for-each select="ancestor-or-self::*">
                <xsl:value-of select="concat('/',local-name())"/>
                <!--Predicate is only output when needed.-->
                <xsl:if test="(preceding-sibling::*|following-sibling::*)[local-name()=local-name(current())]">
                    <xsl:value-of select="concat('[',count(preceding-sibling::*[local-name()=local-name(current())])+1,']')"/>
                </xsl:if>
            </xsl:for-each>
            <xsl:text>&#xA;</xsl:text>
            <xsl:apply-templates select="node()"/>
        </xsl:template>
    
    </xsl:stylesheet>
    

    using the input above, this stylesheet outputs:

    /Doc
    /Doc/Ele1
    /Doc/Ele1/Ele11
    /Doc/Ele1/Ele11/Ele111
    /Doc/Ele1/Ele11/Ele111/foo[1]
    /Doc/Ele1/Ele11/Ele111/foo[2]
    /Doc/Ele1/Ele11/Ele111/bar[1]
    /Doc/Ele1/Ele11/Ele111/foo[3]
    /Doc/Ele1/Ele11/Ele111/foo[4]
    /Doc/Ele1/Ele11/Ele111/bar[2]
    /Doc/Ele1/Ele11/Ele111/bar[3]
    /Doc/Ele2
    
    0 讨论(0)
  • 2020-11-30 02:44

    I'm not sure which XSLT processor you're using, but if it is Saxon, you can use extension function path(). Other processors may have same functionality.

    0 讨论(0)
  • 2020-11-30 02:45

    Don't think this is built into XPath, you probably need a recursive template, like the one here, which I've based this example on. It will walk every element in an XML document and output the path to that element in a style similar to the one you've described.

    <?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"
          exclude-result-prefixes="xs"
          version="2.0">
    
        <xsl:template match="/">
            <paths>
                <xsl:apply-templates/>
            </paths>
        </xsl:template>
    
        <xsl:template match="//*">
            <path>
            <xsl:for-each select="ancestor-or-self::*">
                <xsl:call-template name="print-step"/>
            </xsl:for-each>
            </path>
            <xsl:apply-templates select="*"/>
        </xsl:template>
    
        <xsl:template name="print-step">
            <xsl:text>/</xsl:text>
            <xsl:value-of select="name()"/>
            <xsl:text>[</xsl:text>
            <xsl:value-of select="1+count(preceding-sibling::*)"/>
            <xsl:text>]</xsl:text>
        </xsl:template>
    
    </xsl:stylesheet>
    

    There are a few complications; consider this tree:

    <root>
      <child/>
      <child/>
    </root>
    

    How do you tell the difference between the two child nodes? So you need some index into your item-sequence, child1 and child[2], for example.

    0 讨论(0)
提交回复
热议问题