* fix(xslt): resolve string-form document() against the stylesheet element's base URI
A string-form document() reference resolves against the base URI of the
stylesheet element holding it (XSLT 1.0 12.1). For a declaration in an
included or imported file that is the included file's URI, not the top
stylesheet's, so document('data.xml') (or document('')) in an included file
must resolve against the included file.
The parser already tracked each top-level declaration's source-file base but
discarded it after collection. That base is now carried onto the compiled
templates and global variables, into the runtime context, and through to the
function table, so the document() string form resolves against it. Templates
and globals both carry it, so the rule holds wherever the call appears.
Closes Apache Xalan conformance cases mdocs12 and mdocs13.
* fix(xslt): carry base URI into for-each and sort contexts; keep document('') self-referential
Follow-up to the string-form document() base URI work. Three coverage gaps:
- xsl:for-each and xsl:sort evaluate their body and key expressions in a
context derived from, not equal to, the enclosing template's, so a
document() call there resolved against the empty base when the template came
from an included file. Both now inherit the enclosing context's base URI.
- A non-empty base URI made document('') in the top stylesheet resolve to that
base and route through the loader, rather than returning the stylesheet's own
parsed document as before. The parsed stylesheet is now registered under its
base URI, so document('') reaches it without the loader; document('') in an
included file (a different base) still loads that file.
136 lines
6.0 KiB
Swift
136 lines
6.0 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// `document()` resolves a relative URI reference against a base URI (XSLT 1.0
|
|
/// 12.1): the second argument's first node supplies the base for the two-argument
|
|
/// form, and a node's own document supplies it for the node-set form. A document
|
|
/// loaded from `dir/sub/inner.xml` lends its `dir/sub/` directory to references
|
|
/// taken relative to it.
|
|
@Suite("XSLT document() base URI")
|
|
struct XSLTDocumentBaseURITests {
|
|
private let xsl = "xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\""
|
|
|
|
/// A loader over a fixed in-memory directory tree, exact path to content.
|
|
private static let files = [
|
|
"dir/sub/inner.xml": "<inner><ref>sibling.xml</ref></inner>",
|
|
"dir/sub/target.xml": "<hit>TWO_ARG</hit>",
|
|
"dir/sub/sibling.xml": "<sib>NODE_SET</sib>",
|
|
]
|
|
|
|
private func run(_ select: String) throws -> String {
|
|
try PureXML.XSLT.transform(
|
|
stylesheet: """
|
|
<xsl:stylesheet version="1.0" \(xsl)>
|
|
<xsl:output omit-xml-declaration="yes"/>
|
|
<xsl:template match="/"><out><xsl:value-of select="\(select)"/></out></xsl:template>
|
|
</xsl:stylesheet>
|
|
""",
|
|
source: "<doc/>",
|
|
documentLoader: { Self.files[$0] },
|
|
)
|
|
}
|
|
|
|
@Test("two-argument form resolves against the second argument's base URI")
|
|
func test_twoArgument() throws {
|
|
// `target.xml` resolves against `dir/sub/inner.xml`, reaching dir/sub/target.xml.
|
|
let out = try run("document('target.xml', document('dir/sub/inner.xml'))/hit")
|
|
#expect(out == "<out>TWO_ARG</out>")
|
|
}
|
|
|
|
@Test("node-set form resolves against the referencing node's own base URI")
|
|
func test_nodeSet() throws {
|
|
// The <ref> node lives in dir/sub/inner.xml, so its `sibling.xml` value
|
|
// resolves to dir/sub/sibling.xml.
|
|
let out = try run("document(document('dir/sub/inner.xml')/inner/ref)/sib")
|
|
#expect(out == "<out>NODE_SET</out>")
|
|
}
|
|
|
|
@Test("a fragment-selected node still carries its document's base URI")
|
|
func test_fragmentBase() throws {
|
|
// The fragment selection is a detached subtree, yet it must still resolve
|
|
// `target.xml` against dir/sub/inner.xml's directory.
|
|
let out = try run("document('target.xml', document('dir/sub/inner.xml#xpointer(/inner)'))/hit")
|
|
#expect(out == "<out>TWO_ARG</out>")
|
|
}
|
|
|
|
/// A string-form `document()` reference resolves against the base URI of the
|
|
/// stylesheet element holding it, which for a declaration in an included or
|
|
/// imported file is that file's URI, not the top stylesheet's (XSLT 1.0 12.1;
|
|
/// the semantics behind Apache Xalan impincl08).
|
|
@Test("a global document() in an included file resolves against the included file's base")
|
|
func test_includedGlobalBase() throws {
|
|
let files = [
|
|
"lib/inc.xsl": """
|
|
<xsl:stylesheet version="1.0" \(xsl)>
|
|
<xsl:variable name="d" select="document('data.xml')"/>
|
|
<xsl:template name="emit"><xsl:value-of select="$d/d/v"/></xsl:template>
|
|
</xsl:stylesheet>
|
|
""",
|
|
"lib/data.xml": "<d><v>INCLUDED</v></d>",
|
|
]
|
|
let out = try PureXML.XSLT.transform(
|
|
stylesheet: """
|
|
<xsl:stylesheet version="1.0" \(xsl)>
|
|
<xsl:output omit-xml-declaration="yes"/>
|
|
<xsl:include href="lib/inc.xsl"/>
|
|
<xsl:template match="/"><out><xsl:call-template name="emit"/></out></xsl:template>
|
|
</xsl:stylesheet>
|
|
""",
|
|
source: "<doc/>",
|
|
documentLoader: { files[$0] },
|
|
)
|
|
// `data.xml` resolves against lib/inc.xsl, reaching lib/data.xml; without
|
|
// base provenance it would resolve against the top stylesheet and miss.
|
|
#expect(out == "<out>INCLUDED</out>")
|
|
}
|
|
|
|
/// The base URI must reach a `document()` call inside an `xsl:for-each` body,
|
|
/// which runs in a context derived from (not equal to) the enclosing
|
|
/// template's, when that template comes from an included file.
|
|
@Test("document() inside a for-each body in an included file keeps the file's base")
|
|
func test_forEachBodyBase() throws {
|
|
let files = [
|
|
"lib/inc.xsl": """
|
|
<xsl:stylesheet version="1.0" \(xsl)>
|
|
<xsl:template name="emit">
|
|
<xsl:for-each select="/doc/item"><xsl:value-of select="document('data.xml')/d/v"/></xsl:for-each>
|
|
</xsl:template>
|
|
</xsl:stylesheet>
|
|
""",
|
|
"lib/data.xml": "<d><v>INCLUDED</v></d>",
|
|
]
|
|
let out = try PureXML.XSLT.transform(
|
|
stylesheet: """
|
|
<xsl:stylesheet version="1.0" \(xsl)>
|
|
<xsl:output omit-xml-declaration="yes"/>
|
|
<xsl:include href="lib/inc.xsl"/>
|
|
<xsl:template match="/"><out><xsl:call-template name="emit"/></out></xsl:template>
|
|
</xsl:stylesheet>
|
|
""",
|
|
source: "<doc><item/></doc>",
|
|
documentLoader: { files[$0] },
|
|
)
|
|
#expect(out == "<out>INCLUDED</out>")
|
|
}
|
|
|
|
/// `document('')` in the top stylesheet is the stylesheet's own document and
|
|
/// must not start depending on the loader to serve the stylesheet's URL once
|
|
/// a non-empty base URI is supplied (a regression the base threading could
|
|
/// introduce, since the empty reference resolves to that base).
|
|
@Test("document('') uses the self document even with a non-empty base URI")
|
|
func test_emptyReferenceWithBaseURI() throws {
|
|
let out = try PureXML.XSLT.transform(
|
|
stylesheet: """
|
|
<xsl:stylesheet version="1.0" \(xsl)>
|
|
<xsl:output omit-xml-declaration="yes"/>
|
|
<xsl:template match="/"><out><xsl:value-of select="count(document('')/xsl:stylesheet)"/></out></xsl:template>
|
|
</xsl:stylesheet>
|
|
""",
|
|
source: "<doc/>",
|
|
documentLoader: { _ in nil },
|
|
baseURI: "http://example.org/style.xsl",
|
|
)
|
|
#expect(out == "<out>1</out>")
|
|
}
|
|
}
|