diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4a261a..0ccbfdb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- XSLT `xsl:fallback` (#81). An unrecognized XSLT element instantiates its
+ `xsl:fallback` children in its place (forwards-compatible processing), or is
+ dropped when it has none; a fallback under a supported instruction is ignored.
- XSLT `xsl:apply-imports` (#81). Re-applies templates to the current node in the
current mode, considering only templates of lower import precedence than the
one being instantiated (so an overriding template can extend the imported one),
diff --git a/Sources/XSLT/XSLTModel.swift b/Sources/XSLT/XSLTModel.swift
index 00cfffb..7e5b2b5 100644
--- a/Sources/XSLT/XSLTModel.swift
+++ b/Sources/XSLT/XSLTModel.swift
@@ -67,6 +67,9 @@ public extension PureXML.XSLT {
/// `xsl:apply-imports`: re-applies templates to the current node, in the
/// current mode, considering only templates of lower import precedence.
case applyImports
+ /// The `xsl:fallback` content of an unrecognized XSLT element, instantiated
+ /// in its place under forwards-compatible processing.
+ case fallback(body: [Instruction])
}
/// The `xsl:output` controls that shape serialization: the output method, and
diff --git a/Sources/XSLT/XSLTParser.swift b/Sources/XSLT/XSLTParser.swift
index 010076e..e3096d4 100644
--- a/Sources/XSLT/XSLTParser.swift
+++ b/Sources/XSLT/XSLTParser.swift
@@ -225,7 +225,11 @@ extension PureXML.XSLT {
}
private static func xslInstruction(_ node: Tree) -> Instruction? {
- simpleInstruction(node) ?? structuralInstruction(node)
+ if let known = simpleInstruction(node) ?? structuralInstruction(node) { return known }
+ // An unrecognized XSLT element instantiates its xsl:fallback children
+ // (forwards-compatible processing); with none, it is dropped.
+ let fallback = XSLTNode.children(node, named: "fallback").flatMap(body)
+ return fallback.isEmpty ? nil : .fallback(body: fallback)
}
private static func simpleInstruction(_ node: Tree) -> Instruction? {
diff --git a/Sources/XSLT/XSLTTransformer.swift b/Sources/XSLT/XSLTTransformer.swift
index c51c8b8..ba49ddc 100644
--- a/Sources/XSLT/XSLTTransformer.swift
+++ b/Sources/XSLT/XSLTTransformer.swift
@@ -121,9 +121,12 @@ extension PureXML.XSLT {
var context = context
for instruction in body {
if termination.message != nil { break }
- if case let .variable(name, select, varBody) = instruction {
+ switch instruction {
+ case let .variable(name, select, varBody):
context.variables[name] = variableValue(select, varBody, context)
- } else {
+ case let .fallback(fallbackBody):
+ items += instantiate(fallbackBody, context)
+ default:
items += evaluate(instruction, context)
}
}
diff --git a/Tests/XSLTFallbackTests.swift b/Tests/XSLTFallbackTests.swift
new file mode 100644
index 0000000..3f22091
--- /dev/null
+++ b/Tests/XSLTFallbackTests.swift
@@ -0,0 +1,44 @@
+@testable import PureXML
+import Testing
+
+@Suite("XSLT xsl:fallback")
+struct XSLTFallbackTests {
+ 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 wrap(_ templateBody: String) -> String {
+ """
+
+
+ \(templateBody)
+
+ """
+ }
+
+ @Test("An unknown XSLT element instantiates its xsl:fallback")
+ func test_unknownWithFallback() throws {
+ let style = wrap("fb")
+ #expect(try transform(style, "") == "fb")
+ }
+
+ @Test("An unknown XSLT element with no fallback is dropped")
+ func test_unknownNoFallback() throws {
+ let style = wrap("kept")
+ #expect(try transform(style, "") == "kept")
+ }
+
+ @Test("xsl:fallback under a supported instruction is ignored")
+ func test_fallbackUnderSupported() throws {
+ let style = wrap("ignoredkept")
+ #expect(try transform(style, "") == "kept")
+ }
+
+ @Test("Fallback content may itself be an instruction sequence")
+ func test_fallbackSequence() throws {
+ let style = wrap("")
+ #expect(try transform(style, "") == "7")
+ }
+}