fix(xslt): honor xml:space=preserve when stripping stylesheet whitespace (#271)
XSLT 1.0 3.4: a whitespace-only text node in the stylesheet is preserved when the nearest ancestor-or-self with an xml:space attribute has value preserve. The body parser dropped every whitespace-only run unconditionally, so a literal result element with xml:space="preserve" lost its whitespace content. Consult the in-scope xml:space before stripping. Closes Apache Xalan conformance case whitespace20.
This commit is contained in:
parent
42a56abcbf
commit
12af2fbc24
@ -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`.
|
||||
|
||||
@ -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: " <![CDATA[x]]> " 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 {
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -34,4 +34,3 @@ sort/sort27
|
||||
sort/sort32
|
||||
sort/sort33
|
||||
sort/sort34
|
||||
whitespace/whitespace20
|
||||
|
||||
@ -81,4 +81,18 @@ struct XSLTWhitespaceTests {
|
||||
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>")
|
||||
}
|
||||
|
||||
@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 = """
|
||||
<xsl:stylesheet version="1.0" \(xsl)>
|
||||
<xsl:output omit-xml-declaration="yes"/>
|
||||
<xsl:template match="/"><r><a xml:space="preserve"> </a><b xml:space="default"> </b><c> </c></r></xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
#expect(try transform(style, "<x/>") == "<r><a xml:space=\"preserve\"> </a><b xml:space=\"default\"/><c/></r>")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user