feat(xslt): make result-tree fragments usable as node-sets

An xsl:variable with a body now binds a queryable document fragment (its
instantiated result converted to a TreeNode document) instead of a plain
string, so count($rtf/*), $rtf/child, and for-each over $rtf/... work.
It still has its concatenated text as a string value, so existing string
uses are unchanged. Part of #82.
This commit is contained in:
Mihaela Mihaljevic 2026-06-09 03:04:56 +02:00
parent aa65842bcc
commit 4ea84b3d58
3 changed files with 61 additions and 3 deletions

View File

@ -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 `<![CDATA[…]]>` sections instead of escaped, leaving nested elements and other elements unaffected.
- XSLT `xsl:output` `doctype-public`/`doctype-system` (#82). When either is set, a `<!DOCTYPE>` 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.

View File

@ -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? {

View File

@ -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 {
"""
<xsl:stylesheet version="1.0" \(xsl)>
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:variable name="rtf"><a>1</a><b>2</b><a>3</a></xsl:variable>
<xsl:value-of select="\(select)"/>
</xsl:template>
</xsl:stylesheet>
"""
}
@Test("count($rtf/*) counts the fragment's top-level elements")
func test_countAll() throws {
#expect(try transform(body("count($rtf/*)"), "<x/>") == "3")
}
@Test("A name test selects matching fragment children")
func test_nameTest() throws {
#expect(try transform(body("count($rtf/a)"), "<x/>") == "2")
}
@Test("A path into the fragment reads a child's value")
func test_childValue() throws {
#expect(try transform(body("$rtf/b"), "<x/>") == "2")
}
@Test("The fragment still has a string value (its concatenated text)")
func test_stringValue() throws {
#expect(try transform(body("string($rtf)"), "<x/>") == "123")
}
@Test("for-each iterates the fragment's nodes")
func test_forEach() throws {
let style = """
<xsl:stylesheet version="1.0" \(xsl)>
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:variable name="rtf"><a>1</a><b>2</b><a>3</a></xsl:variable>
<xsl:for-each select="$rtf/a"><xsl:value-of select="."/><xsl:text>,</xsl:text></xsl:for-each>
</xsl:template>
</xsl:stylesheet>
"""
#expect(try transform(style, "<x/>") == "1,3,")
}
}