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