diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43b5542..d4a261a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- 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),
+ falling back to the built-in rule when none match.
- XSLT `xsl:attribute-set` (#81). Named attribute sets are applied to a literal
result element or `xsl:element` via `use-attribute-sets`; sets may include other
sets (recursively, with a cycle guard), set attributes are lowest precedence so
diff --git a/Sources/XSLT/XSLTModel.swift b/Sources/XSLT/XSLTModel.swift
index 0314514..00cfffb 100644
--- a/Sources/XSLT/XSLTModel.swift
+++ b/Sources/XSLT/XSLTModel.swift
@@ -64,6 +64,9 @@ public extension PureXML.XSLT {
/// `xsl:message`: instantiates its body as a diagnostic. `terminate` ends
/// the transformation with that text; it produces no result-tree output.
case message(terminate: Bool, body: [Instruction])
+ /// `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:output` controls that shape serialization: the output method, and
diff --git a/Sources/XSLT/XSLTParser.swift b/Sources/XSLT/XSLTParser.swift
index ff99cd5..010076e 100644
--- a/Sources/XSLT/XSLTParser.swift
+++ b/Sources/XSLT/XSLTParser.swift
@@ -246,6 +246,7 @@ extension PureXML.XSLT {
case "variable", "param": variable(node)
case "copy": .copy(body: body(node))
case "message": .message(terminate: XSLTNode.attribute(node, "terminate") == "yes", body: body(node))
+ case "apply-imports": .applyImports
default: nil
}
}
diff --git a/Sources/XSLT/XSLTRuntime.swift b/Sources/XSLT/XSLTRuntime.swift
new file mode 100644
index 0000000..558c7b7
--- /dev/null
+++ b/Sources/XSLT/XSLTRuntime.swift
@@ -0,0 +1,44 @@
+extension PureXML.XSLT {
+ /// One produced item: a node for the result tree, or an attribute to attach to
+ /// the enclosing element.
+ enum ResultItem {
+ case node(PureXML.Model.Node)
+ case attribute(PureXML.Model.Attribute)
+ }
+
+ /// The evaluation context during instantiation.
+ struct XSLTContext {
+ var node: PureXML.Model.TreeNode
+ var position: Int
+ var size: Int
+ var variables: [String: PureXML.XPath.Value]
+ /// The mode of the template currently being instantiated, so `apply-imports`
+ /// re-applies in the same mode.
+ var mode: String?
+ /// The import precedence of the template currently being instantiated, so
+ /// `apply-imports` considers only templates below it. `.max` outside a template.
+ var importPrecedence: Int
+
+ init(
+ node: PureXML.Model.TreeNode,
+ position: Int,
+ size: Int,
+ variables: [String: PureXML.XPath.Value],
+ mode: String? = nil,
+ importPrecedence: Int = .max,
+ ) {
+ self.node = node
+ self.position = position
+ self.size = size
+ self.variables = variables
+ self.mode = mode
+ self.importPrecedence = importPrecedence
+ }
+ }
+
+ /// Shared, mutable signal that an `xsl:message terminate="yes"` fired, carrying
+ /// its text. A reference type so the value-semantics transformer can set it.
+ final class Termination {
+ var message: String?
+ }
+}
diff --git a/Sources/XSLT/XSLTTransformer.swift b/Sources/XSLT/XSLTTransformer.swift
index cb18924..c51c8b8 100644
--- a/Sources/XSLT/XSLTTransformer.swift
+++ b/Sources/XSLT/XSLTTransformer.swift
@@ -1,24 +1,8 @@
-/// One produced item: a node for the result tree, or an attribute to attach to
-/// the enclosing element. File-scope and private.
-private enum ResultItem {
- case node(PureXML.Model.Node)
- case attribute(PureXML.Model.Attribute)
-}
-
-/// The evaluation context during instantiation. File-scope and private.
-private struct XSLTContext {
- var node: PureXML.Model.TreeNode
- var position: Int
- var size: Int
- var variables: [String: PureXML.XPath.Value]
-}
-
-/// Shared, mutable signal that an `xsl:message terminate="yes"` fired, carrying
-/// its text. A reference type so the value-semantics transformer can set it.
-/// File-scope and private.
-private final class Termination {
- var message: String?
-}
+/// The transformer's runtime types are defined in XSLTRuntime.swift (nested in
+/// the namespace); these file-private aliases keep them unqualified here.
+private typealias ResultItem = PureXML.XSLT.ResultItem
+private typealias XSLTContext = PureXML.XSLT.XSLTContext
+private typealias Termination = PureXML.XSLT.Termination
extension PureXML.XSLT {
/// Runs a compiled stylesheet against a source tree, producing a result tree
@@ -58,9 +42,9 @@ extension PureXML.XSLT {
// MARK: Template matching
- fileprivate func bestTemplate(for node: PureXML.Model.TreeNode, mode: String?) -> Template? {
+ fileprivate func bestTemplate(for node: PureXML.Model.TreeNode, mode: String?, below ceiling: Int = .max) -> Template? {
stylesheet.templates.enumerated()
- .filter { $0.element.mode == mode && ($0.element.match.map { matches(node, $0) } ?? false) }
+ .filter { $0.element.mode == mode && $0.element.importPrecedence < ceiling && ($0.element.match.map { matches(node, $0) } ?? false) }
.max { lhs, rhs in
(lhs.element.importPrecedence, lhs.element.priority, lhs.offset)
< (rhs.element.importPrecedence, rhs.element.priority, rhs.offset)
@@ -89,7 +73,7 @@ extension PureXML.XSLT {
) -> [ResultItem] {
var items: [ResultItem] = []
for (offset, node) in nodes.enumerated() {
- let nodeContext = XSLTContext(node: node, position: offset + 1, size: nodes.count, variables: context.variables)
+ let nodeContext = XSLTContext(node: node, position: offset + 1, size: nodes.count, variables: context.variables, mode: mode)
if let template = bestTemplate(for: node, mode: mode) {
items += instantiateTemplate(template, nodeContext, passing: parameters, from: context)
} else {
@@ -108,6 +92,7 @@ extension PureXML.XSLT {
from caller: XSLTContext,
) -> [ResultItem] {
var context = context
+ context.importPrecedence = template.importPrecedence
for parameter in template.parameters {
if let passed = parameters.first(where: { $0.name == parameter.name }) {
context.variables[parameter.name] = variableValue(passed.select, passed.body, caller)
@@ -254,11 +239,23 @@ extension PureXML.XSLT.Transformer {
[.node(.processingInstruction(target: avt(name, context), data: Self.text(of: instantiate(body, context))))]
case let .message(terminate, body):
message(terminate, body, context)
+ case .applyImports:
+ applyImports(context)
default:
[]
}
}
+ /// Re-applies templates to the current node in the current mode, considering
+ /// only those below the current template's import precedence, falling back to
+ /// the built-in rule when none match.
+ private func applyImports(_ context: XSLTContext) -> [ResultItem] {
+ if let template = bestTemplate(for: context.node, mode: context.mode, below: context.importPrecedence) {
+ return instantiateTemplate(template, context, passing: [], from: context)
+ }
+ return builtInRule(context.node, mode: context.mode, context)
+ }
+
private func forEach(
_ select: String,
_ sorts: [PureXML.XSLT.Sort],
diff --git a/Tests/XSLTApplyImportsTests.swift b/Tests/XSLTApplyImportsTests.swift
new file mode 100644
index 0000000..3b98021
--- /dev/null
+++ b/Tests/XSLTApplyImportsTests.swift
@@ -0,0 +1,51 @@
+@testable import PureXML
+import Testing
+
+@Suite("XSLT xsl:apply-imports")
+struct XSLTApplyImportsTests {
+ private let xsl = "xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\""
+
+ private func transform(_ stylesheet: String, _ source: String, loader: @escaping (String) -> String?) throws -> String {
+ try PureXML.XSLT.transform(stylesheet: stylesheet, source: source, documentLoader: loader)
+ }
+
+ @Test("apply-imports invokes the lower-precedence imported template")
+ func test_applyImports() throws {
+ let base = """
+
+ [base:]
+
+ """
+ let main = """
+
+
+
+
+ main()
+
+ """
+ let output = try transform(main, "- x
") { $0 == "base.xsl" ? base : nil }
+ #expect(output == "main([base:x])")
+ }
+
+ @Test("apply-imports with no lower-precedence match falls back to the built-in rule")
+ func test_applyImportsBuiltIn() throws {
+ let base = """
+
+ never
+
+ """
+ let main = """
+
+
+
+
+ main[]
+
+ """
+ // No imported template matches "item", so apply-imports uses the built-in
+ // rule, which outputs the element's text value.
+ let output = try transform(main, "- y
") { $0 == "base.xsl" ? base : nil }
+ #expect(output == "main[y]")
+ }
+}