diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b97ba..d8b31fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/Sources/XSLT/XSLTParser.swift b/Sources/XSLT/XSLTParser.swift index 805c26d..d0bdcd5 100644 --- a/Sources/XSLT/XSLTParser.swift +++ b/Sources/XSLT/XSLTParser.swift @@ -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 { - 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 diff --git a/Sources/XSLT/XSLTParserDeclarations.swift b/Sources/XSLT/XSLTParserDeclarations.swift index 25b581f..ea717fe 100644 --- a/Sources/XSLT/XSLTParserDeclarations.swift +++ b/Sources/XSLT/XSLTParserDeclarations.swift @@ -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[.., _ preserveSet: Set) -> 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? { diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt index 3abc6ed..bbbe92c 100644 --- a/Tests/Fixtures/xalan-baseline.txt +++ b/Tests/Fixtures/xalan-baseline.txt @@ -41,6 +41,4 @@ sort/sort32 sort/sort33 sort/sort34 string/string132 -whitespace/whitespace06 -whitespace/whitespace07 whitespace/whitespace20 diff --git a/Tests/XSLTWhitespaceTests.swift b/Tests/XSLTWhitespaceTests.swift index 073cbc5..56c71d7 100644 --- a/Tests/XSLTWhitespaceTests.swift +++ b/Tests/XSLTWhitespaceTests.swift @@ -64,4 +64,21 @@ struct XSLTWhitespaceTests { let source = "\n \n" #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 = """ + + + + , + + """ + let source = " " + #expect(try transform(style, source) == "0,2") + } }