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,") + } +}