diff --git a/CHANGELOG.md b/CHANGELOG.md
index b920117..3f52bdf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- XSLT result-tree fragments usable as node-sets (#82). An `xsl:variable` with a body now binds a queryable document fragment instead of a plain string, so `count($rtf/*)`, `$rtf/child`, and `xsl:for-each select="$rtf/..."` work; it still has its concatenated text as a string value.
- XSLT `xsl:output cdata-section-elements` (#82). The text content of the named result elements is emitted in `` sections instead of escaped, leaving nested elements and other elements unaffected.
- XSLT `xsl:output` `doctype-public`/`doctype-system` (#82). When either is set, a `` for the result's root element (PUBLIC when both are given, SYSTEM otherwise) is emitted before the serialized output, for XML and HTML methods.
- XSLT `key()` over a node-set second argument (#82). `key(name, node-set)` (including `key('k', .)`) now unions the matches for every node's string value and de-duplicates, rather than using only the first node's string value.
diff --git a/Sources/XSLT/XSLTTransformer.swift b/Sources/XSLT/XSLTTransformer.swift
index b544f3e..ba5dd3c 100644
--- a/Sources/XSLT/XSLTTransformer.swift
+++ b/Sources/XSLT/XSLTTransformer.swift
@@ -110,8 +110,6 @@ extension PureXML.XSLT {
}
}
- // MARK: Instantiation
-
fileprivate func instantiate(_ body: [Instruction], _ context: XSLTContext) -> [ResultItem] {
var items: [ResultItem] = []
var context = context
@@ -131,7 +129,9 @@ extension PureXML.XSLT {
private func variableValue(_ select: String?, _ body: [Instruction], _ context: XSLTContext) -> PureXML.XPath.Value {
if let select { return value(select, context) ?? .string("") }
- return .string(Self.text(of: instantiate(body, context)))
+ // A body variable is a result-tree fragment: a queryable document node.
+ let children = instantiate(body, context).compactMap(Self.nodeOf).map(PureXML.Model.TreeNode.init)
+ return .nodeSet([.tree(PureXML.Model.TreeNode.document(children: children))])
}
fileprivate func value(_ expression: String, _ context: XSLTContext) -> PureXML.XPath.Value? {
diff --git a/Tests/XSLTResultFragmentTests.swift b/Tests/XSLTResultFragmentTests.swift
new file mode 100644
index 0000000..ae45b5f
--- /dev/null
+++ b/Tests/XSLTResultFragmentTests.swift
@@ -0,0 +1,57 @@
+@testable import PureXML
+import Testing
+
+@Suite("XSLT result-tree fragment as node-set")
+struct XSLTResultFragmentTests {
+ private let xsl = "xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\""
+
+ private func transform(_ stylesheet: String, _ source: String) throws -> String {
+ try PureXML.XSLT.transform(stylesheet: stylesheet, source: source)
+ }
+
+ private func body(_ select: String) -> String {
+ """
+
+
+
+ 123
+
+
+
+ """
+ }
+
+ @Test("count($rtf/*) counts the fragment's top-level elements")
+ func test_countAll() throws {
+ #expect(try transform(body("count($rtf/*)"), "") == "3")
+ }
+
+ @Test("A name test selects matching fragment children")
+ func test_nameTest() throws {
+ #expect(try transform(body("count($rtf/a)"), "") == "2")
+ }
+
+ @Test("A path into the fragment reads a child's value")
+ func test_childValue() throws {
+ #expect(try transform(body("$rtf/b"), "") == "2")
+ }
+
+ @Test("The fragment still has a string value (its concatenated text)")
+ func test_stringValue() throws {
+ #expect(try transform(body("string($rtf)"), "") == "123")
+ }
+
+ @Test("for-each iterates the fragment's nodes")
+ func test_forEach() throws {
+ let style = """
+
+
+
+ 123
+ ,
+
+
+ """
+ #expect(try transform(style, "") == "1,3,")
+ }
+}