feat(xslt): implement xsl:output cdata-section-elements
The text content of the named result elements is emitted in CDATA sections rather than escaped: the result tree's text children of those elements are converted to CDATA nodes before serialization, leaving nested and other elements unaffected. Part of #82.
This commit is contained in:
parent
9b07a5901d
commit
3fec78da51
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- 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.
|
||||
- XSLT `method="html"` output (#82). An `xsl:output method="html"` result is now serialized by the HTML rules (void elements like `<br>` emitted without a self-closing slash, raw-text elements like `<script>`/`<style>` left unescaped) instead of as XML.
|
||||
|
||||
@ -102,6 +102,8 @@ public extension PureXML.XSLT {
|
||||
/// a `<!DOCTYPE>` for the result's root element is emitted before it.
|
||||
public var doctypePublic: String?
|
||||
public var doctypeSystem: String?
|
||||
/// Element names whose text content is emitted in `<![CDATA[…]]>` sections.
|
||||
public var cdataSectionElements: Set<String>
|
||||
|
||||
public init(
|
||||
method: String? = nil,
|
||||
@ -112,6 +114,7 @@ public extension PureXML.XSLT {
|
||||
standalone: Bool? = nil,
|
||||
doctypePublic: String? = nil,
|
||||
doctypeSystem: String? = nil,
|
||||
cdataSectionElements: Set<String> = [],
|
||||
) {
|
||||
self.method = method
|
||||
self.indent = indent
|
||||
@ -121,6 +124,7 @@ public extension PureXML.XSLT {
|
||||
self.standalone = standalone
|
||||
self.doctypePublic = doctypePublic
|
||||
self.doctypeSystem = doctypeSystem
|
||||
self.cdataSectionElements = cdataSectionElements
|
||||
}
|
||||
|
||||
/// This output's settings with `other`'s non-nil settings layered over them.
|
||||
@ -134,6 +138,7 @@ public extension PureXML.XSLT {
|
||||
standalone: other.standalone ?? standalone,
|
||||
doctypePublic: other.doctypePublic ?? doctypePublic,
|
||||
doctypeSystem: other.doctypeSystem ?? doctypeSystem,
|
||||
cdataSectionElements: cdataSectionElements.union(other.cdataSectionElements),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,6 +167,7 @@ extension PureXML.XSLT {
|
||||
standalone: XSLTNode.attribute(node, "standalone").map { $0 == "yes" },
|
||||
doctypePublic: XSLTNode.attribute(node, "doctype-public"),
|
||||
doctypeSystem: XSLTNode.attribute(node, "doctype-system"),
|
||||
cdataSectionElements: Set((XSLTNode.attribute(node, "cdata-section-elements") ?? "").split(whereSeparator: \.isWhitespace).map(String.init)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -17,12 +17,35 @@ public extension PureXML.XSLT {
|
||||
let result = transformer.run()
|
||||
if let message = transformer.terminationMessage { throw XSLTError.terminated(message) }
|
||||
if sheet.output.method == "text" { return textValue(of: result) }
|
||||
let body = sheet.output.method == "html"
|
||||
? PureXML.HTML.serialize(result)
|
||||
: PureXML.serialize(result, options: options.applying(sheet.output))
|
||||
let body: String
|
||||
if sheet.output.method == "html" {
|
||||
body = PureXML.HTML.serialize(result)
|
||||
} else {
|
||||
let prepared = withCDATASections(result, sheet.output.cdataSectionElements)
|
||||
body = PureXML.serialize(prepared, options: options.applying(sheet.output))
|
||||
}
|
||||
return doctype(for: result, sheet.output) + body
|
||||
}
|
||||
|
||||
/// Returns `node` with the text children of any element named in `names`
|
||||
/// replaced by CDATA nodes, so the serializer emits them as CDATA sections.
|
||||
private static func withCDATASections(_ node: PureXML.Model.Node, _ names: Set<String>) -> PureXML.Model.Node {
|
||||
guard !names.isEmpty else { return node }
|
||||
switch node {
|
||||
case let .document(children):
|
||||
return .document(children.map { withCDATASections($0, names) })
|
||||
case let .element(element):
|
||||
let wrap = names.contains(element.name.localName) || names.contains(element.name.description)
|
||||
let children = element.children.map { child -> PureXML.Model.Node in
|
||||
if wrap, case let .text(value) = child { return .cdata(value) }
|
||||
return withCDATASections(child, names)
|
||||
}
|
||||
return .element(.init(name: element.name, attributes: element.attributes, children: children))
|
||||
default:
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
/// The `<!DOCTYPE …>` prologue for the result's root element when the output
|
||||
/// declares `doctype-system` (optionally with `doctype-public`), else empty.
|
||||
private static func doctype(for result: PureXML.Model.Node, _ output: Output) -> String {
|
||||
|
||||
44
Tests/XSLTCDATASectionTests.swift
Normal file
44
Tests/XSLTCDATASectionTests.swift
Normal file
@ -0,0 +1,44 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("XSLT xsl:output cdata-section-elements")
|
||||
struct XSLTCDATASectionTests {
|
||||
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 style(_ cdataElements: String, _ body: String) -> String {
|
||||
"""
|
||||
<xsl:stylesheet version="1.0" \(xsl)>
|
||||
<xsl:output method="xml" omit-xml-declaration="yes" cdata-section-elements="\(cdataElements)"/>
|
||||
<xsl:template match="/">\(body)</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
}
|
||||
|
||||
@Test("A named element's text is emitted as a CDATA section")
|
||||
func test_wrapped() throws {
|
||||
let out = try transform(style("script", "<script>if (a < b) go()</script>"), "<x/>")
|
||||
#expect(out == "<script><![CDATA[if (a < b) go()]]></script>")
|
||||
}
|
||||
|
||||
@Test("An element not named keeps escaped text")
|
||||
func test_notNamed() throws {
|
||||
let out = try transform(style("script", "<data>a < b</data>"), "<x/>")
|
||||
#expect(out == "<data>a < b</data>")
|
||||
}
|
||||
|
||||
@Test("Only the named element's own text is wrapped, not nested elements")
|
||||
func test_nestedUnaffected() throws {
|
||||
let out = try transform(style("outer", "<outer>x<inner>y < z</inner></outer>"), "<x/>")
|
||||
#expect(out == "<outer><![CDATA[x]]><inner>y < z</inner></outer>")
|
||||
}
|
||||
|
||||
@Test("Multiple element names can be listed")
|
||||
func test_multiple() throws {
|
||||
let out = try transform(style("a b", "<root><a>1 & 2</a><b>3 & 4</b></root>"), "<x/>")
|
||||
#expect(out == "<root><a><![CDATA[1 & 2]]></a><b><![CDATA[3 & 4]]></b></root>")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user