PureXML/Sources/XSLT/XSLTWhitespace.swift
Mihaela Mihaljevic 10d9525e3e feat(xslt): implement xsl:strip-space and xsl:preserve-space
Whitespace-only text nodes are removed from the source tree for elements
named by strip-space before transformation, with preserve-space and a
more specific name test winning over *, and xml:space=preserve on a
source element or ancestor overriding the stripping. The name sets fold
across xsl:include/xsl:import. Refactors the stylesheet compile loop
through a Parts accumulator to absorb the new top-level elements within
the complexity budget.
2026-06-09 01:22:59 +02:00

50 lines
2.4 KiB
Swift

extension PureXML.XSLT {
/// Applies a stylesheet's `xsl:strip-space` / `xsl:preserve-space` to the
/// source tree, removing whitespace-only text nodes from stripped elements
/// before transformation. `xml:space="preserve"` on an element or an ancestor
/// (until an `xml:space="default"` resets it) forces preservation regardless
/// of `strip-space`, per the XSLT 1.0 whitespace-stripping rules. CDATA is
/// significant and never stripped.
enum Whitespace {
static func strip(_ root: PureXML.Model.TreeNode, stylesheet: Stylesheet) {
guard !stylesheet.stripSpace.isEmpty else { return }
strip(root, stylesheet.stripSpace, stylesheet.preserveSpace, inScopePreserve: false)
}
private static func strip(
_ node: PureXML.Model.TreeNode,
_ stripSet: Set<String>,
_ preserveSet: Set<String>,
inScopePreserve: Bool,
) {
let preserveHere = xmlSpace(node).map { $0 == "preserve" } ?? inScopePreserve
if node.kind == .element, !preserveHere, shouldStrip(node, stripSet, preserveSet) {
for child in node.children where child.kind == .text && isWhitespace(child.value) {
node.removeChild(child)
}
}
for child in node.children {
strip(child, stripSet, preserveSet, inScopePreserve: preserveHere)
}
}
/// Whether an element with this name has its whitespace stripped: a name
/// test wins over `*`, and `preserve-space` beats `strip-space` on a tie.
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("*")
}
private static func xmlSpace(_ node: PureXML.Model.TreeNode) -> String? {
node.attributes.first { $0.name.localName == "space" && $0.name.prefix == "xml" }?.value
}
private static func isWhitespace(_ value: String) -> Bool {
value.allSatisfy(\.isWhitespace)
}
}
}