| Ken Holman
>I recently saw an XPATH expression (in Dave Obasanjo's "Things to
>Know and Avoid When Querying XML Documents") that included parentheses
>used for grouping:
I wouldn't use the word "grouping" but rather "for containing a location
path expression". Sometimes, as in Dan's example, this is important in
order to distinguish what would be construed as a location step expression
to be a location path expression:
>(//*)[position()=1]/@on-loan
In the above "//*" is a location path expression because of the parens.
Consider taking out the parens:
//*[position()=1]/@on-loan
This time the predicate is considered part of the second step "*" so as to
address the on-load attribute of the first element child at every child
level of the document tree.
Note that you cannot be fast and loose with parens, as addressing in a
location step is proximity ordered while the addressing in a location path
is document ordered, such that
preceding-sibling::*[1] (proximity order; addresses closest)
and
(preceding-sibling::*)[1] (document order; addresses furthest)
do not address the same node if there is more than one preceding
sibling. This is because the parens make the location step into a location
path.
I have found parens very useful when starting off a location path
expression with a union: Remember that in a multiple step location path, parens can only be used in
the first step.
>I had to scratch my head wondering if I have ever seen parentheses
>used in this fashion. Is anyone familiar enough with this usage
Since day one of my XSLT/XPath training course I've underscored this with
students because it helps to understand the nuances between location steps
and paths and how proximity order differs from document order.
>to explain when parenthesis would be needed and where in the
>specs they are even allowed?
Production 15 in XPath. |
| Eliot Kimber
Here is my first stab at a set of XSLT 2 functions for path
manipulation, one to make a path with relative components absolute
(relative to itself, as opposed to some base, although I suppose I'll
need that too) as well as a function to calculate the relative path
between two absolute paths. A unit test script follows. All my tests
pass and I think the code is about as efficient as it can be but I
fear there are some edge cases I've overlooked.
I did realize that one limitation is that there's no obvious way to
determine if the last token in a path is a file or directory, which
means the caller is responsible for knowing and passing in appropriate
values. relpath_util.xsl: %lt;?xml version="1.0" encoding="UTF-8"?>
%lt;xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:local="http://www.example.com/functions/local"
exclude-result-prefixes="local xs"
>
%lt;xsl:function name="local:getAbsolutePath" as="xs:string">
%lt;!-- Given a path resolves any ".." or "." terms
to produce an absolute path -->
%lt;xsl:param name="sourcePath" as="xs:string"/>
%lt;xsl:variable name="pathTokens"
select="tokenize($sourcePath, '/')" as="xs:string*"/>
%lt;xsl:if test="false()">
%lt;xsl:message> +
DEBUG local:getAbsolutePath(): Starting%lt;/xsl:message>
%lt;xsl:message> +
sourcePath="%lt;xsl:value-of select="$sourcePath"/>"%lt;/xsl:message>
%lt;/xsl:if>
%lt;xsl:variable name="baseResult"
select="string-join(local:makePathAbsolute($pathTokens, ()),
'/')" as="xs:string"/>
%lt;xsl:variable name="result" as="xs:string"
select="if (starts-with($sourcePath, '/') and
not(starts-with($baseResult, '/')))
then concat('/', $baseResult)
else $baseResult
"
/>
%lt;xsl:if test="false()">
%lt;xsl:message> +
DEBUG: result="%lt;xsl:value-of select="$result"/>"%lt;/xsl:message>
%lt;/xsl:if>
%lt;xsl:value-of select="$result"/>
%lt;/xsl:function>
%lt;xsl:function name="local:makePathAbsolute" as="xs:string*">
%lt;xsl:param name="pathTokens" as="xs:string*"/>
%lt;xsl:param name="resultTokens" as="xs:string*"/>
%lt;xsl:if test="false()">
%lt;xsl:message> +
DEBUG: local:makePathAbsolute(): Starting...%lt;/xsl:message>
%lt;xsl:message> +
DEBUG: pathTokens="%lt;xsl:value-of
select="string-join($pathTokens,
',')"/>"%lt;/xsl:message>
%lt;xsl:message> +
DEBUG: resultTokens="%lt;xsl:value-of
select="string-join($resultTokens,
',')"/>"%lt;/xsl:message>
%lt;/xsl:if>
%lt;xsl:sequence select="if (count($pathTokens) = 0)
then $resultTokens
else
if ($pathTokens[1] = '.')
then local:makePathAbsolute($pathTokens[position() > 1],
$resultTokens)
else
if ($pathTokens[1] = '..')
then local:makePathAbsolute($pathTokens[position() > 1],
$resultTokens[position() < last()])
else local:makePathAbsolute($pathTokens[position() > 1],
($resultTokens, $pathTokens[1]))
"/>
%lt;/xsl:function>
%lt;xsl:function name="local:getRelativePath" as="xs:string">
%lt;!-- Calculate relative path that gets
from from source path to target path.
Given:
[1] Target: /A/B/C
Source: /A/B/C/X
Return: "X"
[2] Target: /A/B/C
Source: /E/F/G/X
Return: "/E/F/G/X"
[3] Target: /A/B/C
Source: /A/D/E/X
Return: "../../D/E/X"
[4] Target: /A/B/C
Source: /A/X
Return: "../../X"
-->
%lt;xsl:param name="source" as="xs:string"/>
%lt;!-- Path to get relative path *from* -->
%lt;xsl:param name="target" as="xs:string"/>
%lt;!-- Path to get relataive path *to* -->
%lt;xsl:if test="false()">
%lt;xsl:message> + DEBUG: local:getRelativePath(): Starting...%lt;/xsl:message>
%lt;xsl:message> +
DEBUG: source="%lt;xsl:value-of select="$source"/>"%lt;/xsl:message>
%lt;xsl:message> +
DEBUG: target="%lt;xsl:value-of select="$target"/>"%lt;/xsl:message>
%lt;/xsl:if>
%lt;xsl:variable name="sourceTokens"
select="tokenize((
if (starts-with($source, '/'))
then substring-after($source, '/')
else $source), '/')" as="xs:string*"/>
%lt;xsl:variable name="targetTokens"
select="tokenize((
if (starts-with($target, '/'))
then substring-after($target, '/')
else $target), '/')" as="xs:string*"/>
%lt;xsl:choose>
%lt;xsl:when test="(count($sourceTokens) > 0
and count($targetTokens) > 0) and
(($sourceTokens[1] != $targetTokens[1]) and
(contains($sourceTokens[1], ':') or
contains($targetTokens[1], ':')))">
%lt;!-- Must be absolute URLs with different
schemes, cannot be relative, return
target as is. -->
%lt;xsl:value-of select="$target"/>
%lt;/xsl:when>
%lt;xsl:otherwise>
%lt;xsl:variable name="resultTokens"
select="local:analyzePathTokens($sourceTokens, $targetTokens, ())"
as="xs:string*"/>
%lt;xsl:variable name="result" select="string-join($resultTokens, '/')"
as="xs:string"/>
%lt;xsl:value-of select="$result"/>
%lt;/xsl:otherwise>
%lt;/xsl:choose>
%lt;/xsl:function>
%lt;xsl:function name="local:analyzePathTokens" as="xs:string*">
%lt;xsl:param name="sourceTokens" as="xs:string*"/>
%lt;xsl:param name="targetTokens" as="xs:string*"/>
%lt;xsl:param name="resultTokens" as="xs:string*"/>
%lt;xsl:if test="false()">
%lt;xsl:message> + DEBUG: local:analyzePathTokens(): Starting...%lt;/xsl:message>
%lt;xsl:message> + DEBUG: sourceTokens=%lt;xsl:value-of
select="string-join($sourceTokens, ',')"/>%lt;/xsl:message>
%lt;xsl:message> + DEBUG: targetTokens=%lt;xsl:value-of
select="string-join($targetTokens, ',')"/>%lt;/xsl:message>
%lt;xsl:message> + DEBUG: resultTokens=%lt;xsl:value-of
select="string-join($resultTokens, ',')"/>%lt;/xsl:message>
%lt;/xsl:if>
%lt;xsl:sequence
select="if (count($sourceTokens) = 0 and count($targetTokens) = 0)
then $resultTokens
else if (count($sourceTokens) = 0)
then trace(($resultTokens, $targetTokens), ' +
DEBUG: count(sourceTokens) = 0')
else
if (string($sourceTokens[1]) != string($targetTokens[1]))
then local:analyzePathTokens($sourceTokens[position() > 1],
$targetTokens, ($resultTokens, '..'))
else local:analyzePathTokens($sourceTokens[position() > 1],
$targetTokens[position() > 1], $resultTokens)"/>
%lt;/xsl:function>
%lt;/xsl:stylesheet>
Unit tests for the utility functions: %lt;?xml version="1.0" encoding="UTF-8"?>
%lt;xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:local="http://www.example.com/functions/local"
exclude-result-prefixes="local xs"
>
%lt;xsl:include href="relpath_util.xsl"/>
%lt;!-- Tests for the relpath_util functions
-->
%lt;xsl:template match="/">
%lt;xsl:call-template name="testGetAbsolutePath"/>
%lt;xsl:call-template name="testGetRelativePath"/>
%lt;/xsl:template>
%lt;xsl:template name="testGetAbsolutePath">
%lt;xsl:variable name="testData" as="element()">
%lt;test_data>
%lt;title>getAbsolutePath() Tests%lt;/title>
%lt;test>
%lt;source>/%lt;/source>
%lt;result>/%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A%lt;/source>
%lt;result>/A%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A/..%lt;/source>
%lt;result>/%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A/./B%lt;/source>
%lt;result>/A/B%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A/B/C/D/../../E%lt;/source>
%lt;result>/A/B/E%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A/B/C/D/../../E/F%lt;/source>
%lt;result>/A/B/E/F%lt;/result>
%lt;/test>
%lt;test>
%lt;source>file:///A/B/C%lt;/source>
%lt;result>file:///A/B/C%lt;/result>
%lt;/test>
%lt;test>
%lt;source>./A/B/C/D/E.xml%lt;/source>
%lt;result>A/B/C/D/E.xml%lt;/result>
%lt;/test>
%lt;/test_data>
%lt;/xsl:variable>
%lt;xsl:apply-templates select="$testData" mode="testGetAbsolutePath"/>
%lt;/xsl:template>
%lt;xsl:template name="testGetRelativePath">
%lt;xsl:variable name="testData" as="element()">
%lt;test_data>
%lt;title>getRelativePath() Tests%lt;/title>
%lt;test>
%lt;source>/%lt;/source>
%lt;target>/A%lt;/target>
%lt;result>A%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A%lt;/source>
%lt;target>/%lt;/target>
%lt;result>..%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A%lt;/source>
%lt;target>/B%lt;/target>
%lt;result>../B%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A%lt;/source>
%lt;target>/A/B%lt;/target>
%lt;result>B%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A/B/C/D%lt;/source>
%lt;target>/A%lt;/target>
%lt;result>../../..%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A/B/C/D%lt;/source>
%lt;target>/A/E%lt;/target>
%lt;result>../../../E%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A/B/C/D.xml%lt;/source>
%lt;target>/A/E%lt;/target>
%lt;result>../../E%lt;/result>
%lt;comment>This test should fail because there's no way for the XSLT
to know that D.xml is a file and not a directory.
The source parameter to relpath must be a directory path,
not a filename.%lt;/comment>
%lt;/test>
%lt;test>
%lt;source>/A/B%lt;/source>
%lt;target>/A/C/D%lt;/target>
%lt;result>../C/D%lt;/result>
%lt;/test>
%lt;test>
%lt;source>/A/B/C%lt;/source>
%lt;target>/A/B/C/D/E%lt;/target>
%lt;result>D/E%lt;/result>
%lt;/test>
%lt;test>
%lt;source>file:///A/B/C%lt;/source>
%lt;target>http://A/B/C/D/E%lt;/target>
%lt;result>http://A/B/C/D/E%lt;/result>
%lt;/test>
%lt;test>
%lt;source>file://A/B/C%lt;/source>
%lt;target>file://A/B/C/D/E.xml%lt;/target>
%lt;result>D/E.xml%lt;/result>
%lt;/test>
%lt;/test_data>
%lt;/xsl:variable>
%lt;xsl:apply-templates select="$testData" mode="testGetRelativePath"/>
%lt;/xsl:template>
%lt;xsl:template match="test_data" mode="#all">
%lt;test_results>
%lt;xsl:apply-templates mode="#current"/>
%lt;/test_results>
%lt;/xsl:template>
%lt;xsl:template match="title" mode="#all">
%lt;xsl:text>
%lt;/xsl:text>
%lt;xsl:value-of select="."/>
%lt;xsl:text>

%lt;/xsl:text>
%lt;/xsl:template>
%lt;xsl:template match="test" mode="testGetAbsolutePath">
%lt;xsl:text>Test Case: %lt;/xsl:text>
%lt;xsl:number count="test" format="[1]"/>%lt;xsl:text>
%lt;/xsl:text>
%lt;xsl:text> source: "%lt;/xsl:text>
%lt;xsl:value-of select="source"/>%lt;xsl:text>"
%lt;/xsl:text>
%lt;xsl:variable name="cand"
select="local:getAbsolutePath(string(source))" as="xs:string"/>
%lt;xsl:variable name="pass" select="$cand = string(result)" as="xs:boolean"/>
%lt;xsl:text> result: "%lt;/xsl:text>
%lt;xsl:value-of select="$cand"/>%lt;xsl:text>", pass: %lt;/xsl:text>
%lt;xsl:value-of select="$pass"/>%lt;xsl:text>
%lt;/xsl:text>
%lt;xsl:if test="not($pass)">
%lt;xsl:text> expected result: "%lt;/xsl:text>
%lt;xsl:value-of select="result"/>%lt;xsl:text>"
%lt;/xsl:text>
%lt;/xsl:if>
%lt;xsl:copy-of select="comment"/>
%lt;xsl:text>
%lt;/xsl:text>
%lt;/xsl:template>
%lt;xsl:template match="test" mode="testGetRelativePath">
%lt;xsl:text>Test Case: %lt;/xsl:text>
%lt;xsl:number
count="test" format="[1]"/>%lt;xsl:text>
%lt;/xsl:text>
%lt;xsl:text> source: "%lt;/xsl:text>
%lt;xsl:value-of
select="source"/>%lt;xsl:text>"
%lt;/xsl:text>
%lt;xsl:text> target: "%lt;/xsl:text>
%lt;xsl:value-of
select="target"/>%lt;xsl:text>"
%lt;/xsl:text>
%lt;xsl:variable name="cand"
select="local:getRelativePath(string(source), string(target))" as="xs:string"/>
%lt;xsl:variable name="pass"
select="$cand = string(result)" as="xs:boolean"/>
%lt;xsl:text> result: "%lt;/xsl:text>
%lt;xsl:value-of select="$cand"/>%lt;xsl:text>",
pass: %lt;/xsl:text>
%lt;xsl:value-of select="$pass"/>%lt;xsl:text>
%lt;/xsl:text>
%lt;xsl:if test="not($pass)">
%lt;xsl:text> expected result: "%lt;/xsl:text>
%lt;xsl:value-of select="result"/>%lt;xsl:text>"
%lt;/xsl:text>
%lt;/xsl:if>
%lt;xsl:copy-of select="comment"/>
%lt;xsl:text>
%lt;/xsl:text>
%lt;/xsl:template>
%lt;/xsl:stylesheet>
|