[XSL-LIST Mailing List Archive Home]
[By Thread]
[By Date]
Eliot Kimber wrote:
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.
As always I welcome any feedback on the code.
Cheers,
Eliot
relpath_util.xsl:
<?xml version="1.0" encoding="UTF-8"?>
<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"
>
<xsl:function name="local:getAbsolutePath" as="xs:string">
<!-- Given a path resolves any ".." or "." terms to produce an absolute path -->
<xsl:param name="sourcePath" as="xs:string"/>
<xsl:variable name="pathTokens" select="tokenize($sourcePath, '/')" as="xs:string*"/>
<xsl:if test="false()">
<xsl:message> + DEBUG local:getAbsolutePath(): Starting</xsl:message>
<xsl:message> + sourcePath="<xsl:value-of select="$sourcePath"/>"</xsl:message>
</xsl:if>
<xsl:variable name="baseResult"
select="string-join(local:makePathAbsolute($pathTokens, ()), '/')" as="xs:string"/>
<xsl:variable name="result" as="xs:string"
select="if (starts-with($sourcePath, '/') and not(starts-with($baseResult, '/')))
then concat('/', $baseResult)
else $baseResult
"
/>
<xsl:if test="false()">
<xsl:message> + DEBUG: result="<xsl:value-of select="$result"/>"</xsl:message>
</xsl:if>
<xsl:value-of select="$result"/>
</xsl:function>
<xsl:function name="local:makePathAbsolute" as="xs:string*">
<xsl:param name="pathTokens" as="xs:string*"/>
<xsl:param name="resultTokens" as="xs:string*"/>
<xsl:if test="false()">
<xsl:message> + DEBUG: local:makePathAbsolute(): Starting...</xsl:message>
<xsl:message> + DEBUG: pathTokens="<xsl:value-of select="string-join($pathTokens, ',')"/>"</xsl:message>
<xsl:message> + DEBUG: resultTokens="<xsl:value-of select="string-join($resultTokens, ',')"/>"</xsl:message>
</xsl:if>
<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]))
"/>
</xsl:function>
Given:
Return: "X"
Return: "/E/F/G/X"
Return: "../../D/E/X"
Return: "../../X"
-->
<xsl:param name="source" as="xs:string"/><!-- Path to get relative path *from* -->
<xsl:param name="target" as="xs:string"/><!-- Path to get relataive path *to* -->
<xsl:if test="false()">
<xsl:message> + DEBUG: local:getRelativePath(): Starting...</xsl:message>
<xsl:message> + DEBUG: source="<xsl:value-of select="$source"/>"</xsl:message>
<xsl:message> + DEBUG: target="<xsl:value-of select="$target"/>"</xsl:message>
</xsl:if>
<xsl:variable name="sourceTokens" select="tokenize((if (starts-with($source, '/')) then substring-after($source, '/') else $source), '/')" as="xs:string*"/>
<xsl:variable name="targetTokens" select="tokenize((if (starts-with($target, '/')) then substring-after($target, '/') else $target), '/')" as="xs:string*"/>
<xsl:choose>
<xsl:when test="(count($sourceTokens) > 0 and count($targetTokens) > 0) and
(($sourceTokens[1] != $targetTokens[1]) and
(contains($sourceTokens[1], ':') or contains($targetTokens[1], ':')))">
<!-- Must be absolute URLs with different schemes, cannot be relative, return
target as is. -->
<xsl:value-of select="$target"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="resultTokens"
select="local:analyzePathTokens($sourceTokens, $targetTokens, ())" as="xs:string*"/>
<xsl:variable name="result" select="string-join($resultTokens, '/')" as="xs:string"/>
<xsl:value-of select="$result"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>
<xsl:function name="local:analyzePathTokens" as="xs:string*">
<xsl:param name="sourceTokens" as="xs:string*"/>
<xsl:param name="targetTokens" as="xs:string*"/>
<xsl:param name="resultTokens" as="xs:string*"/>
<xsl:if test="false()">
<xsl:message> + DEBUG: local:analyzePathTokens(): Starting...</xsl:message>
<xsl:message> + DEBUG: sourceTokens=<xsl:value-of select="string-join($sourceTokens, ',')"/></xsl:message>
<xsl:message> + DEBUG: targetTokens=<xsl:value-of select="string-join($targetTokens, ',')"/></xsl:message>
<xsl:message> + DEBUG: resultTokens=<xsl:value-of select="string-join($resultTokens, ',')"/></xsl:message>
</xsl:if>
<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)"/>
</xsl:function>
</xsl:stylesheet>
Unit tests for the utility functions:
<?xml version="1.0" encoding="UTF-8"?>
<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"
>
<xsl:include href="relpath_util.xsl"/>
<xsl:template name="testGetRelativePath">
<xsl:variable name="testData" as="element()">
<test_data>
<title>getRelativePath() Tests</title>
<test>
<source>/</source>
<target>/A</target>
<result>A</result>
</test>
<test>
<source>/A</source>
<target>/</target>
<result>..</result>
</test>
<test>
<source>/A</source>
<target>/B</target>
<result>../B</result>
</test>
<test>
<source>/A</source>
<target>/A/B</target>
<result>B</result>
</test>
<test>
<source>/A/B/C/D</source>
<target>/A</target>
<result>../../..</result>
</test>
<test>
<source>/A/B/C/D</source>
<target>/A/E</target>
<result>../../../E</result>
</test>
<test>
<source>/A/B/C/D.xml</source>
<target>/A/E</target>
<result>../../E</result>
<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.</comment>
</test>
<test>
<source>/A/B</source>
<target>/A/C/D</target>
<result>../C/D</result>
</test>
<test>
<source>/A/B/C</source>
<target>/A/B/C/D/E</target>
<result>D/E</result>
</test>
<test>
<source>file:///A/B/C</source>
<target>http://A/B/C/D/E</target>
<result>http://A/B/C/D/E</result>
</test>
<test>
<source>file://A/B/C</source>
<target>file://A/B/C/D/E.xml</target>
<result>D/E.xml</result>
</test>
</test_data>
</xsl:variable>
<xsl:apply-templates select="$testData" mode="testGetRelativePath"/>
</xsl:template>
<xsl:template match="test" mode="testGetAbsolutePath">
<xsl:text>Test Case: </xsl:text><xsl:number count="test" format="[1]"/><xsl:text>
</xsl:text>
<xsl:text> source: "</xsl:text><xsl:value-of select="source"/><xsl:text>"
</xsl:text>
<xsl:variable name="cand" select="local:getAbsolutePath(string(source))" as="xs:string"/>
<xsl:variable name="pass" select="$cand = string(result)" as="xs:boolean"/>
<xsl:text> result: "</xsl:text><xsl:value-of select="$cand"/><xsl:text>", pass: </xsl:text><xsl:value-of select="$pass"/><xsl:text>
</xsl:text>
<xsl:if test="not($pass)">
<xsl:text> expected result: "</xsl:text><xsl:value-of select="result"/><xsl:text>"
</xsl:text>
</xsl:if>
<xsl:copy-of select="comment"/>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="test" mode="testGetRelativePath">
<xsl:text>Test Case: </xsl:text><xsl:number count="test" format="[1]"/><xsl:text>
</xsl:text>
<xsl:text> source: "</xsl:text><xsl:value-of select="source"/><xsl:text>"
</xsl:text>
<xsl:text> target: "</xsl:text><xsl:value-of select="target"/><xsl:text>"
</xsl:text>
<xsl:variable name="cand" select="local:getRelativePath(string(source), string(target))" as="xs:string"/>
<xsl:variable name="pass" select="$cand = string(result)" as="xs:boolean"/>
<xsl:text> result: "</xsl:text><xsl:value-of select="$cand"/><xsl:text>", pass: </xsl:text><xsl:value-of select="$pass"/><xsl:text>
</xsl:text>
<xsl:if test="not($pass)">
<xsl:text> expected result: "</xsl:text><xsl:value-of select="result"/><xsl:text>"
</xsl:text>
</xsl:if>
<xsl:copy-of select="comment"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
Re: [xsl] XSLT 2 Function To Calculate Relative Paths?
Subject: Re: [xsl] XSLT 2 Function To Calculate Relative Paths? From: Eliot Kimber <ekimber@xxxxxxxxxxxx> Date: Tue, 11 Mar 2008 14:39:31 -0500 |
Eliot Kimber wrote:
Eliot Kimber wrote:Does anyone have code lying about or can anyone point me to a description of the algorithm for calculating the relative path from one fully-qualified file to another? I know I've figured this out in the past but I remember it being hard for my addled brain to work out the details. A search of this list via MarkMail didn't reveal any past discussion in an XSLT 2 context (where the task should be much easier given functions and string tokenization).
Thanks for everyone's replies--I'll be trying to implement an XSLT 2 function for this here directly and will post my results here.
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.
As always I welcome any feedback on the code.
Cheers,
Eliot
relpath_util.xsl:
<?xml version="1.0" encoding="UTF-8"?>
<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"
>
<xsl:function name="local:getAbsolutePath" as="xs:string">
<!-- Given a path resolves any ".." or "." terms to produce an absolute path -->
<xsl:param name="sourcePath" as="xs:string"/>
<xsl:variable name="pathTokens" select="tokenize($sourcePath, '/')" as="xs:string*"/>
<xsl:if test="false()">
<xsl:message> + DEBUG local:getAbsolutePath(): Starting</xsl:message>
<xsl:message> + sourcePath="<xsl:value-of select="$sourcePath"/>"</xsl:message>
</xsl:if>
<xsl:variable name="baseResult"
select="string-join(local:makePathAbsolute($pathTokens, ()), '/')" as="xs:string"/>
<xsl:variable name="result" as="xs:string"
select="if (starts-with($sourcePath, '/') and not(starts-with($baseResult, '/')))
then concat('/', $baseResult)
else $baseResult
"
/>
<xsl:if test="false()">
<xsl:message> + DEBUG: result="<xsl:value-of select="$result"/>"</xsl:message>
</xsl:if>
<xsl:value-of select="$result"/>
</xsl:function>
<xsl:function name="local:makePathAbsolute" as="xs:string*">
<xsl:param name="pathTokens" as="xs:string*"/>
<xsl:param name="resultTokens" as="xs:string*"/>
<xsl:if test="false()">
<xsl:message> + DEBUG: local:makePathAbsolute(): Starting...</xsl:message>
<xsl:message> + DEBUG: pathTokens="<xsl:value-of select="string-join($pathTokens, ',')"/>"</xsl:message>
<xsl:message> + DEBUG: resultTokens="<xsl:value-of select="string-join($resultTokens, ',')"/>"</xsl:message>
</xsl:if>
<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]))
"/>
</xsl:function>
<xsl:function name="local:getRelativePath" as="xs:string"> <!-- 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"
-->
<xsl:param name="source" as="xs:string"/><!-- Path to get relative path *from* -->
<xsl:param name="target" as="xs:string"/><!-- Path to get relataive path *to* -->
<xsl:if test="false()">
<xsl:message> + DEBUG: local:getRelativePath(): Starting...</xsl:message>
<xsl:message> + DEBUG: source="<xsl:value-of select="$source"/>"</xsl:message>
<xsl:message> + DEBUG: target="<xsl:value-of select="$target"/>"</xsl:message>
</xsl:if>
<xsl:variable name="sourceTokens" select="tokenize((if (starts-with($source, '/')) then substring-after($source, '/') else $source), '/')" as="xs:string*"/>
<xsl:variable name="targetTokens" select="tokenize((if (starts-with($target, '/')) then substring-after($target, '/') else $target), '/')" as="xs:string*"/>
<xsl:choose>
<xsl:when test="(count($sourceTokens) > 0 and count($targetTokens) > 0) and
(($sourceTokens[1] != $targetTokens[1]) and
(contains($sourceTokens[1], ':') or contains($targetTokens[1], ':')))">
<!-- Must be absolute URLs with different schemes, cannot be relative, return
target as is. -->
<xsl:value-of select="$target"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="resultTokens"
select="local:analyzePathTokens($sourceTokens, $targetTokens, ())" as="xs:string*"/>
<xsl:variable name="result" select="string-join($resultTokens, '/')" as="xs:string"/>
<xsl:value-of select="$result"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>
<xsl:function name="local:analyzePathTokens" as="xs:string*">
<xsl:param name="sourceTokens" as="xs:string*"/>
<xsl:param name="targetTokens" as="xs:string*"/>
<xsl:param name="resultTokens" as="xs:string*"/>
<xsl:if test="false()">
<xsl:message> + DEBUG: local:analyzePathTokens(): Starting...</xsl:message>
<xsl:message> + DEBUG: sourceTokens=<xsl:value-of select="string-join($sourceTokens, ',')"/></xsl:message>
<xsl:message> + DEBUG: targetTokens=<xsl:value-of select="string-join($targetTokens, ',')"/></xsl:message>
<xsl:message> + DEBUG: resultTokens=<xsl:value-of select="string-join($resultTokens, ',')"/></xsl:message>
</xsl:if>
<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)"/>
</xsl:function>
</xsl:stylesheet>
Unit tests for the utility functions:
<?xml version="1.0" encoding="UTF-8"?>
<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"
>
<xsl:include href="relpath_util.xsl"/>
<!-- Tests for the relpath_util functions -->
<xsl:template match="/"> <xsl:call-template name="testGetAbsolutePath"/> <xsl:call-template name="testGetRelativePath"/> </xsl:template>
<xsl:template name="testGetAbsolutePath"> <xsl:variable name="testData" as="element()"> <test_data> <title>getAbsolutePath() Tests</title> <test> <source>/</source> <result>/</result> </test> <test> <source>/A</source> <result>/A</result> </test> <test> <source>/A/..</source> <result>/</result> </test> <test> <source>/A/./B</source> <result>/A/B</result> </test> <test> <source>/A/B/C/D/../../E</source> <result>/A/B/E</result> </test> <test> <source>/A/B/C/D/../../E/F</source> <result>/A/B/E/F</result> </test> <test> <source>file:///A/B/C</source> <result>file:///A/B/C</result> </test> <test> <source>./A/B/C/D/E.xml</source> <result>A/B/C/D/E.xml</result> </test> </test_data> </xsl:variable> <xsl:apply-templates select="$testData" mode="testGetAbsolutePath"/> </xsl:template>
<xsl:template name="testGetRelativePath">
<xsl:variable name="testData" as="element()">
<test_data>
<title>getRelativePath() Tests</title>
<test>
<source>/</source>
<target>/A</target>
<result>A</result>
</test>
<test>
<source>/A</source>
<target>/</target>
<result>..</result>
</test>
<test>
<source>/A</source>
<target>/B</target>
<result>../B</result>
</test>
<test>
<source>/A</source>
<target>/A/B</target>
<result>B</result>
</test>
<test>
<source>/A/B/C/D</source>
<target>/A</target>
<result>../../..</result>
</test>
<test>
<source>/A/B/C/D</source>
<target>/A/E</target>
<result>../../../E</result>
</test>
<test>
<source>/A/B/C/D.xml</source>
<target>/A/E</target>
<result>../../E</result>
<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.</comment>
</test>
<test>
<source>/A/B</source>
<target>/A/C/D</target>
<result>../C/D</result>
</test>
<test>
<source>/A/B/C</source>
<target>/A/B/C/D/E</target>
<result>D/E</result>
</test>
<test>
<source>file:///A/B/C</source>
<target>http://A/B/C/D/E</target>
<result>http://A/B/C/D/E</result>
</test>
<test>
<source>file://A/B/C</source>
<target>file://A/B/C/D/E.xml</target>
<result>D/E.xml</result>
</test>
</test_data>
</xsl:variable>
<xsl:apply-templates select="$testData" mode="testGetRelativePath"/>
</xsl:template>
<xsl:template match="test_data" mode="#all"> <test_results> <xsl:apply-templates mode="#current"/> </test_results> </xsl:template>
<xsl:template match="title" mode="#all"> <xsl:text>
</xsl:text> <xsl:value-of select="."/> <xsl:text>

</xsl:text> </xsl:template>
<xsl:template match="test" mode="testGetAbsolutePath">
<xsl:text>Test Case: </xsl:text><xsl:number count="test" format="[1]"/><xsl:text>
</xsl:text>
<xsl:text> source: "</xsl:text><xsl:value-of select="source"/><xsl:text>"
</xsl:text>
<xsl:variable name="cand" select="local:getAbsolutePath(string(source))" as="xs:string"/>
<xsl:variable name="pass" select="$cand = string(result)" as="xs:boolean"/>
<xsl:text> result: "</xsl:text><xsl:value-of select="$cand"/><xsl:text>", pass: </xsl:text><xsl:value-of select="$pass"/><xsl:text>
</xsl:text>
<xsl:if test="not($pass)">
<xsl:text> expected result: "</xsl:text><xsl:value-of select="result"/><xsl:text>"
</xsl:text>
</xsl:if>
<xsl:copy-of select="comment"/>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="test" mode="testGetRelativePath">
<xsl:text>Test Case: </xsl:text><xsl:number count="test" format="[1]"/><xsl:text>
</xsl:text>
<xsl:text> source: "</xsl:text><xsl:value-of select="source"/><xsl:text>"
</xsl:text>
<xsl:text> target: "</xsl:text><xsl:value-of select="target"/><xsl:text>"
</xsl:text>
<xsl:variable name="cand" select="local:getRelativePath(string(source), string(target))" as="xs:string"/>
<xsl:variable name="pass" select="$cand = string(result)" as="xs:boolean"/>
<xsl:text> result: "</xsl:text><xsl:value-of select="$cand"/><xsl:text>", pass: </xsl:text><xsl:value-of select="$pass"/><xsl:text>
</xsl:text>
<xsl:if test="not($pass)">
<xsl:text> expected result: "</xsl:text><xsl:value-of select="result"/><xsl:text>"
</xsl:text>
</xsl:if>
<xsl:copy-of select="comment"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
-- Eliot Kimber Senior Solutions Architect "Bringing Strategy, Content, and Technology Together" Main: 610.631.6770 www.reallysi.com www.rsuitecms.com
Current Thread |
---|
|
<- Previous | Index | Next -> |
---|---|---|
Re: [xsl] XSLT 2 Function To Calcul, Eliot Kimber | Thread | [xsl] Having a stylesheet process x, Glen Mazza |
Re: [xsl] Manipulating elements dep, Adil Ladhani | Date | [xsl] Processing Memory-Hungry Data, Eliot Kimber |
Month |
Keywords