diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3298964..a30d656 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
+- `xml:space="preserve"` on a literal result element (or an ancestor) in the stylesheet now keeps that element's whitespace-only text instead of stripping it (XSLT 1.0 3.4). The stylesheet whitespace stripping dropped every whitespace-only run unconditionally, ignoring an in-scope `xml:space="preserve"`; the nearest ancestor-or-self `xml:space` is now consulted, so `preserve` retains the run while `default` (or none) still strips it. Closes Apache Xalan conformance case `whitespace20`.
+
- `xsl:number level="single"` now counts the element that is the `from` boundary when that element also matches the `count` pattern (XSLT 1.0 7.7). The search for the count node stopped at the nearest `from`-matching ancestor before testing it against `count`, so a pattern like `count="a|b" from="a"` produced an empty number for an element inside an `a` (the count node is that `a`, which is also the boundary). The count pattern is now tested before the boundary stops the walk. Closes Apache Xalan conformance case `numbering63`.
- The `format` attribute of `xsl:number` is now evaluated as an attribute value template (XSLT 1.0 7.7), so `format="{$f}"` uses the value of `$f` rather than the literal string. It was stored and used verbatim, so a computed or passed-in format was never applied. Closes Apache Xalan conformance case `namedtemplate12`.
diff --git a/Sources/XSLT/XSLTParser.swift b/Sources/XSLT/XSLTParser.swift
index ef7f76c..152c11a 100644
--- a/Sources/XSLT/XSLTParser.swift
+++ b/Sources/XSLT/XSLTParser.swift
@@ -243,10 +243,12 @@ extension PureXML.XSLT {
// model, so a whitespace-only run is dropped (XSLT 1.0 3.4) only when
// the WHOLE coalesced run is whitespace: " " keeps its
// spaces because the run "x" surrounds them with non-whitespace.
+ // `xml:space="preserve"` in scope keeps a whitespace-only run too.
+ let preserve = preservesWhitespace(node)
var result: [Instruction] = []
var run = ""
func flush() {
- if !run.isEmpty, !run.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isWhitespace) { result.append(.literalText(run)) }
+ if !run.isEmpty, preserve || run.unicodeScalars.contains(where: { !PureXML.Parsing.XMLCharacter.isWhitespace($0) }) { result.append(.literalText(run)) }
run = ""
}
for child in node.children {
diff --git a/Sources/XSLT/XSLTParserDeclarations.swift b/Sources/XSLT/XSLTParserDeclarations.swift
index ea717fe..a3239af 100644
--- a/Sources/XSLT/XSLTParserDeclarations.swift
+++ b/Sources/XSLT/XSLTParserDeclarations.swift
@@ -48,6 +48,20 @@ extension PureXML.XSLT.XSLTParser {
XSLTNode.attribute(node, "mode").map { expandedQName($0, at: node) }
}
+ /// Whether `xml:space="preserve"` is in scope at `node`: the nearest
+ /// ancestor-or-self carrying an `xml:space` attribute has value `preserve`,
+ /// so a whitespace-only text run is kept rather than stripped (XSLT 1.0 3.4).
+ static func preservesWhitespace(_ node: XSLTTree) -> Bool {
+ var current: XSLTTree? = node
+ while let element = current {
+ if let space = element.attributes.first(where: { $0.name.prefix == "xml" && $0.name.localName == "space" })?.value {
+ return space == "preserve"
+ }
+ current = element.parent
+ }
+ return false
+ }
+
/// 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).
diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt
index cfcc397..7fb8525 100644
--- a/Tests/Fixtures/xalan-baseline.txt
+++ b/Tests/Fixtures/xalan-baseline.txt
@@ -34,4 +34,3 @@ sort/sort27
sort/sort32
sort/sort33
sort/sort34
-whitespace/whitespace20
diff --git a/Tests/XSLTWhitespaceTests.swift b/Tests/XSLTWhitespaceTests.swift
index 56c71d7..4678bb9 100644
--- a/Tests/XSLTWhitespaceTests.swift
+++ b/Tests/XSLTWhitespaceTests.swift
@@ -81,4 +81,18 @@ struct XSLTWhitespaceTests {
let source = " "
#expect(try transform(style, source) == "0,2")
}
+
+ @Test("xml:space=preserve in the stylesheet keeps a literal element's whitespace (3.4)")
+ func test_xmlSpacePreserveInStylesheet() throws {
+ // A literal result element with xml:space="preserve" keeps its
+ // whitespace-only content; xml:space="default" strips it, as does no
+ // xml:space at all (Apache Xalan whitespace20).
+ let style = """
+
+
+
+
+ """
+ #expect(try transform(style, "") == " ")
+ }
}