diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1b78a38..d5ec8b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- XSLT template parameters and modes (toward full parity, #61). `xsl:param`
+ declarations (with defaults), `xsl:with-param` on `apply-templates` and
+ `call-template`, and `mode` on templates and `apply-templates` are now honored:
+ a passed parameter overrides the declared default, and a mode routes nodes only
+ to templates declared in that mode (the built-in rules recurse in the same
+ mode).
- Push / feed streaming API (#1), the last epic. A resumable scanner reports a
token only once its terminator is fully buffered and otherwise signals
need-more (the Expat `XML_TOK_PARTIAL` model), so `PureXML.Parsing.PushParser`
diff --git a/README.md b/README.md
index c7570bd..dea5bf4 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,17 @@ remaining libxml2 surface is tracked as epics. Deliberate non-goals: network
fetching (`nanohttp`/`nanoftp`) and the threading/memory infrastructure stay out,
and external resolution is opt-in through an injected resolver so XXE stays closed.
-Every tracked epic is now shipped; the diagram above summarizes the surface.
+The original epics are all shipped (summarized above). The remaining work is
+closing the documented within-subsystem gaps to reach exhaustive libxml2 parity:
+
+```mermaid
+flowchart TB
+ classDef done fill:#ddf9e4,stroke:#34c759,color:#111827
+ classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
+ classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
+ classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
+ Parity["#61 Full parity completion: XSLT params/keys/output, XSD union/import/identity, RELAX NG compact, HTML5 tree construction, C14N 2.0"]:::epic
+```
## Status
diff --git a/Sources/XSLT/XSLTModel.swift b/Sources/XSLT/XSLTModel.swift
index c01ff29..a3876b8 100644
--- a/Sources/XSLT/XSLTModel.swift
+++ b/Sources/XSLT/XSLTModel.swift
@@ -29,8 +29,9 @@ public extension PureXML.XSLT {
case literalText(String)
/// `xsl:value-of`: the string value of an XPath expression.
case valueOf(select: String)
- /// `xsl:apply-templates` with an optional node-set selection and sorts.
- case applyTemplates(select: String?, sorts: [Sort])
+ /// `xsl:apply-templates` with an optional node-set selection, mode, sorts,
+ /// and passed parameters.
+ case applyTemplates(select: String?, mode: String?, sorts: [Sort], parameters: [Binding])
/// `xsl:for-each` over a node-set.
case forEach(select: String, sorts: [Sort], body: [Instruction])
/// `xsl:if`.
@@ -47,8 +48,8 @@ public extension PureXML.XSLT {
case copy(body: [Instruction])
/// `xsl:copy-of`: a deep copy of a selected node-set.
case copyOf(select: String)
- /// `xsl:call-template` by name.
- case callTemplate(name: String)
+ /// `xsl:call-template` by name with passed parameters.
+ case callTemplate(name: String, parameters: [Binding])
/// `xsl:variable` bound for the rest of the sequence constructor.
case variable(name: String, select: String?, body: [Instruction])
}
@@ -59,11 +60,22 @@ public extension PureXML.XSLT {
public var body: [Instruction]
}
- /// A template rule: a match pattern and/or a name, a priority, and a body.
+ /// A name binding: an `xsl:param` declaration (with a default) or an
+ /// `xsl:with-param` value, supplied either by `select` or by a body.
+ struct Binding: Sendable {
+ public var name: String
+ public var select: String?
+ public var body: [Instruction]
+ }
+
+ /// A template rule: a match pattern and/or a name, a mode, a priority, its
+ /// declared parameters, and a body.
struct Template: Sendable {
public var match: String?
public var name: String?
+ public var mode: String?
public var priority: Double
+ public var parameters: [Binding]
public var body: [Instruction]
}
diff --git a/Sources/XSLT/XSLTParser.swift b/Sources/XSLT/XSLTParser.swift
index 7c587ee..47b485a 100644
--- a/Sources/XSLT/XSLTParser.swift
+++ b/Sources/XSLT/XSLTParser.swift
@@ -69,14 +69,32 @@ extension PureXML.XSLT {
let match = XSLTNode.attribute(node, "match")
let priority = XSLTNode.attribute(node, "priority").flatMap(Double.init)
?? match.map(defaultPriority) ?? 0
+ let parameters = XSLTNode.children(node, named: "param").map(binding)
+ let body = node.children
+ .filter { !(XSLTNode.isXSL($0) && XSLTNode.localName($0) == "param") }
+ .compactMap(instruction)
return Template(
match: match,
name: XSLTNode.attribute(node, "name"),
+ mode: XSLTNode.attribute(node, "mode"),
priority: priority,
+ parameters: parameters,
+ body: body,
+ )
+ }
+
+ private static func binding(_ node: Tree) -> Binding {
+ Binding(
+ name: XSLTNode.attribute(node, "name") ?? "",
+ select: XSLTNode.attribute(node, "select"),
body: body(node),
)
}
+ private static func withParameters(_ node: Tree) -> [Binding] {
+ XSLTNode.children(node, named: "with-param").map(binding)
+ }
+
/// The XSLT default-priority rules for a match pattern.
static func defaultPriority(_ pattern: String) -> Double {
if pattern.contains("/") || pattern.contains("[") { return 0.5 }
@@ -110,9 +128,17 @@ extension PureXML.XSLT {
private static func simpleInstruction(_ node: Tree) -> Instruction? {
switch XSLTNode.localName(node) {
case "value-of": .valueOf(select: XSLTNode.attribute(node, "select") ?? "")
- case "apply-templates": .applyTemplates(select: XSLTNode.attribute(node, "select"), sorts: sorts(node))
+ case "apply-templates": .applyTemplates(
+ select: XSLTNode.attribute(node, "select"),
+ mode: XSLTNode.attribute(node, "mode"),
+ sorts: sorts(node),
+ parameters: withParameters(node),
+ )
case "copy-of": .copyOf(select: XSLTNode.attribute(node, "select") ?? "")
- case "call-template": .callTemplate(name: XSLTNode.attribute(node, "name") ?? "")
+ case "call-template": .callTemplate(
+ name: XSLTNode.attribute(node, "name") ?? "",
+ parameters: withParameters(node),
+ )
case "text": .literalText(node.stringValue)
case "variable", "param": variable(node)
case "copy": .copy(body: body(node))
diff --git a/Sources/XSLT/XSLTTransformer.swift b/Sources/XSLT/XSLTTransformer.swift
index 29c32dc..165a1cd 100644
--- a/Sources/XSLT/XSLTTransformer.swift
+++ b/Sources/XSLT/XSLTTransformer.swift
@@ -31,14 +31,14 @@ extension PureXML.XSLT {
}
}
let context = XSLTContext(node: root, position: 1, size: 1, variables: variables)
- return .document(applyTemplates(to: [root], context).compactMap(Self.nodeOf))
+ return .document(applyTemplates(to: [root], mode: nil, parameters: [], context).compactMap(Self.nodeOf))
}
// MARK: Template matching
- fileprivate func bestTemplate(for node: PureXML.Model.TreeNode) -> Template? {
+ fileprivate func bestTemplate(for node: PureXML.Model.TreeNode, mode: String?) -> Template? {
stylesheet.templates.enumerated()
- .filter { $0.element.match.map { matches(node, $0) } ?? false }
+ .filter { $0.element.mode == mode && ($0.element.match.map { matches(node, $0) } ?? false) }
.max { lhs, rhs in (lhs.element.priority, lhs.offset) < (rhs.element.priority, rhs.offset) }?
.element
}
@@ -56,23 +56,47 @@ extension PureXML.XSLT {
// MARK: apply-templates and built-in rules
- fileprivate func applyTemplates(to nodes: [PureXML.Model.TreeNode], _ context: XSLTContext) -> [ResultItem] {
+ fileprivate func applyTemplates(
+ to nodes: [PureXML.Model.TreeNode],
+ mode: String?,
+ parameters: [Binding],
+ _ context: XSLTContext,
+ ) -> [ResultItem] {
var items: [ResultItem] = []
for (offset, node) in nodes.enumerated() {
let nodeContext = XSLTContext(node: node, position: offset + 1, size: nodes.count, variables: context.variables)
- if let template = bestTemplate(for: node) {
- items += instantiate(template.body, nodeContext)
+ if let template = bestTemplate(for: node, mode: mode) {
+ items += instantiateTemplate(template, nodeContext, passing: parameters, from: context)
} else {
- items += builtInRule(node, nodeContext)
+ items += builtInRule(node, mode: mode, nodeContext)
}
}
return items
}
- private func builtInRule(_ node: PureXML.Model.TreeNode, _ context: XSLTContext) -> [ResultItem] {
+ /// Binds the template's parameters (a passed `with-param` wins over the
+ /// declared default) and instantiates its body.
+ fileprivate func instantiateTemplate(
+ _ template: Template,
+ _ context: XSLTContext,
+ passing parameters: [Binding],
+ from caller: XSLTContext,
+ ) -> [ResultItem] {
+ var context = context
+ 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)
+ } else {
+ context.variables[parameter.name] = variableValue(parameter.select, parameter.body, context)
+ }
+ }
+ return instantiate(template.body, context)
+ }
+
+ private func builtInRule(_ node: PureXML.Model.TreeNode, mode: String?, _ context: XSLTContext) -> [ResultItem] {
switch node.kind {
case .element, .document:
- applyTemplates(to: node.children, context)
+ applyTemplates(to: node.children, mode: mode, parameters: [], context)
case .text, .cdata:
[.node(.text(node.value))]
default:
@@ -159,13 +183,18 @@ extension PureXML.XSLT.Transformer {
switch instruction {
case let .literalText(text): [.node(.text(text))]
case let .valueOf(select): [.node(.text(string(select, context)))]
- case let .applyTemplates(select, sorts):
- applyTemplates(to: sorted(selectNodes(select ?? "node()", context), sorts), context)
+ case let .applyTemplates(select, mode, sorts, parameters):
+ applyTemplates(
+ to: sorted(selectNodes(select ?? "node()", context), sorts),
+ mode: mode,
+ parameters: parameters,
+ context,
+ )
case let .forEach(select, sorts, body): forEach(select, sorts, body, context)
case let .ifInstruction(test, body): boolean(test, context) ? instantiate(body, context) : []
case let .choose(whens, otherwise): chooseInstruction(whens, otherwise, context)
case let .copyOf(select): copyOf(select, context)
- case let .callTemplate(name): callTemplate(name, context)
+ case let .callTemplate(name, parameters): callTemplate(name, parameters, context)
case .variable: []
default: nil
}
@@ -211,9 +240,9 @@ extension PureXML.XSLT.Transformer {
return instantiate(otherwise, context)
}
- private func callTemplate(_ name: String, _ context: XSLTContext) -> [ResultItem] {
+ private func callTemplate(_ name: String, _ parameters: [PureXML.XSLT.Binding], _ context: XSLTContext) -> [ResultItem] {
guard let template = stylesheet.templates.first(where: { $0.name == name }) else { return [] }
- return instantiate(template.body, context)
+ return instantiateTemplate(template, context, passing: parameters, from: context)
}
private func copyOf(_ select: String, _ context: XSLTContext) -> [ResultItem] {
diff --git a/Tests/XSLTTests.swift b/Tests/XSLTTests.swift
index ed67070..87078c8 100644
--- a/Tests/XSLTTests.swift
+++ b/Tests/XSLTTests.swift
@@ -9,6 +9,57 @@ struct XSLTTests {
try PureXML.XSLT.transform(stylesheet: stylesheet, source: source)
}
+ @Test("call-template passes with-param values, falling back to param defaults")
+ func test_callTemplateParams() throws {
+ let stylesheet = """
+
+