fix(xslt): match strip-space/preserve-space element tests by namespace (#265)
XSLT 1.0 3.4: an xsl:strip-space/xsl:preserve-space elements name test is a
namespace-qualified NameTest, matched by namespace, not by prefix. A specifier
prefix:* or prefix:local now resolves its prefix to a URI at parse time, and a
source element is matched on its expanded name by specificity (exact {uri}local
over namespace wildcard {uri}* over *). The tests compared raw prefixed strings,
so ns1:* did not strip a source element bound to the same namespace under a
different prefix.
Closes Apache Xalan conformance cases whitespace06 and whitespace07.
This commit is contained in:
parent
3ab8637c14
commit
2fe8632bb2
@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- `xsl:strip-space` and `xsl:preserve-space` now match their `elements` name tests by namespace rather than by prefix (XSLT 1.0 3.4). A specifier `prefix:*` or `prefix:local` resolves the prefix to its namespace URI at parse time, and a source element is matched on its expanded name in order of specificity (an exact `{uri}local` over a namespace wildcard `{uri}*` over `*`), so a stylesheet that strips `ns1:*` strips a source element in that namespace whatever prefix the source bound it to, while leaving another namespace untouched. The tests previously compared the raw prefixed name strings, so a differing prefix did not match. Closes Apache Xalan conformance cases `whitespace06` and `whitespace07`.
|
||||
|
||||
- `disable-output-escaping` is now ignored when an `xsl:text` or `xsl:value-of` produces the string value of an attribute, comment, or processing-instruction node (XSLT 1.0 16.4: disabling escaping for a value used as something other than a text node is an error, with the recovery being to ignore it). The attribute value built from such content kept its raw-output markers, so the serializer left `<` and `>` unescaped in the attribute; the markers are now stripped for these three node kinds (only when the stylesheet uses the feature, so a private-use character in ordinary data is untouched). Closes Apache Xalan conformance case `output75`.
|
||||
|
||||
- The `html` output method now percent-escapes non-ASCII and control characters in URI-valued attributes (XSLT 1.0 16.2, HTML 4.01 appendix B.2.1). For the HTML 4 attributes of type %URI (`href`, `src`, `cite`, `action`, and the rest), each non-ASCII or control/space character in the value is written as the uppercase hex of its UTF-8 bytes (so `cite="bë.xml"` becomes `cite="b%C3%AB.xml"`), while `"` and `&` keep their entity form; other attributes are unaffected. Closes Apache Xalan conformance case `output32`.
|
||||
|
||||
@ -139,9 +139,12 @@ extension PureXML.XSLT {
|
||||
}
|
||||
|
||||
/// The whitespace-separated element name tests of an `xsl:strip-space` or
|
||||
/// `xsl:preserve-space` element's `elements` attribute.
|
||||
/// `xsl:preserve-space` element's `elements` attribute, each resolved to
|
||||
/// namespace form so matching is by namespace, not prefix.
|
||||
private static func elementNames(_ node: XSLTTree) -> Set<String> {
|
||||
Set((XSLTNode.attribute(node, "elements") ?? "").split(whereSeparator: \.isWhitespace).map(String.init))
|
||||
Set((XSLTNode.attribute(node, "elements") ?? "").split(whereSeparator: \.isWhitespace).map {
|
||||
expandedSpecifier(String($0), at: node)
|
||||
})
|
||||
}
|
||||
|
||||
/// Loads an include/import target's stylesheet element: the href
|
||||
|
||||
@ -48,6 +48,17 @@ extension PureXML.XSLT.XSLTParser {
|
||||
XSLTNode.attribute(node, "mode").map { expandedQName($0, at: node) }
|
||||
}
|
||||
|
||||
/// A strip-space/preserve-space NameTest resolved to namespace form: `*` and
|
||||
/// an unprefixed name (the null namespace) stay as is, `prefix:*` becomes
|
||||
/// `{uri}*`, and `prefix:local` becomes `{uri}local` (XSLT 1.0 3.4).
|
||||
static func expandedSpecifier(_ token: String, at node: XSLTTree) -> String {
|
||||
guard let colon = token.firstIndex(of: ":") else { return token }
|
||||
let prefix = String(token[..<colon])
|
||||
let local = String(token[token.index(after: colon)...])
|
||||
guard let uri = resolvePrefix(prefix, at: node) else { return token }
|
||||
return "{\(uri)}\(local)"
|
||||
}
|
||||
|
||||
/// A variable or parameter `name` as an expanded name `{uri}local` when it
|
||||
/// carries a prefix, or the bare name when unprefixed. A variable is named by
|
||||
/// expanded QName (XSLT 1.0 11.1), so two prefixes bound to one namespace name
|
||||
|
||||
@ -28,14 +28,20 @@ extension PureXML.XSLT {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an element with this name has its whitespace stripped: a name
|
||||
/// test wins over `*`, and `preserve-space` beats `strip-space` on a tie.
|
||||
/// Whether an element has its whitespace stripped. Specifiers are matched
|
||||
/// by namespace in order of specificity (an expanded `{uri}local` name
|
||||
/// over a namespace wildcard `{uri}*` over `*`), and `preserve-space`
|
||||
/// beats `strip-space` on a tie (XSLT 1.0 3.4).
|
||||
private static func shouldStrip(_ node: PureXML.Model.TreeNode, _ stripSet: Set<String>, _ preserveSet: Set<String>) -> Bool {
|
||||
let names = [node.name?.localName, node.name?.description].compactMap(\.self)
|
||||
if names.contains(where: preserveSet.contains) { return false }
|
||||
if names.contains(where: stripSet.contains) { return true }
|
||||
if preserveSet.contains("*") { return false }
|
||||
return stripSet.contains("*")
|
||||
let uri = node.name?.namespaceURI ?? ""
|
||||
let local = node.name?.localName ?? ""
|
||||
let expanded = uri.isEmpty ? local : "{\(uri)}\(local)"
|
||||
let namespaceWildcard = uri.isEmpty ? nil : "{\(uri)}*"
|
||||
for test in [expanded, namespaceWildcard, "*"].compactMap(\.self) {
|
||||
if preserveSet.contains(test) { return false }
|
||||
if stripSet.contains(test) { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private static func xmlSpace(_ node: PureXML.Model.TreeNode) -> String? {
|
||||
|
||||
@ -41,6 +41,4 @@ sort/sort32
|
||||
sort/sort33
|
||||
sort/sort34
|
||||
string/string132
|
||||
whitespace/whitespace06
|
||||
whitespace/whitespace07
|
||||
whitespace/whitespace20
|
||||
|
||||
@ -64,4 +64,21 @@ struct XSLTWhitespaceTests {
|
||||
let source = "<a xml:space=\"preserve\">\n <b/>\n</a>"
|
||||
#expect(try transform(style, source).contains("\n "))
|
||||
}
|
||||
|
||||
@Test("strip-space matches a namespace wildcard by namespace, not prefix")
|
||||
func test_namespaceWildcard() throws {
|
||||
// strip-space `p:*` strips elements in p's namespace. The source binds the
|
||||
// same namespace to a different prefix (q), which must still match, while
|
||||
// an element in another namespace is preserved (Apache Xalan whitespace06,
|
||||
// whitespace07).
|
||||
let style = """
|
||||
<xsl:stylesheet version="1.0" \(xsl) xmlns:p="urn:n1" xmlns:s="urn:n2" exclude-result-prefixes="p s">
|
||||
<xsl:output omit-xml-declaration="yes"/>
|
||||
<xsl:strip-space elements="p:*"/>
|
||||
<xsl:template match="/"><out><xsl:value-of select="string-length(doc/p:a)"/>,<xsl:value-of select="string-length(doc/s:b)"/></out></xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
let source = "<doc xmlns:q=\"urn:n1\" xmlns:r=\"urn:n2\"><q:a> </q:a><r:b> </r:b></doc>"
|
||||
#expect(try transform(style, source) == "<out>0,2</out>")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user