Merge: XSLT xsl:apply-imports (#81)
This commit is contained in:
commit
b9e2cd9fa5
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
44
Sources/XSLT/XSLTRuntime.swift
Normal file
44
Sources/XSLT/XSLTRuntime.swift
Normal file
@ -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?
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
|
||||
51
Tests/XSLTApplyImportsTests.swift
Normal file
51
Tests/XSLTApplyImportsTests.swift
Normal file
@ -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 = """
|
||||
<xsl:stylesheet version="1.0" \(xsl)>
|
||||
<xsl:template match="item">[base:<xsl:value-of select="."/>]</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
let main = """
|
||||
<xsl:stylesheet version="1.0" \(xsl)>
|
||||
<xsl:import href="base.xsl"/>
|
||||
<xsl:output method="xml" omit-xml-declaration="yes"/>
|
||||
<xsl:template match="/"><out><xsl:apply-templates select="r/item"/></out></xsl:template>
|
||||
<xsl:template match="item">main(<xsl:apply-imports/>)</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
let output = try transform(main, "<r><item>x</item></r>") { $0 == "base.xsl" ? base : nil }
|
||||
#expect(output == "<out>main([base:x])</out>")
|
||||
}
|
||||
|
||||
@Test("apply-imports with no lower-precedence match falls back to the built-in rule")
|
||||
func test_applyImportsBuiltIn() throws {
|
||||
let base = """
|
||||
<xsl:stylesheet version="1.0" \(xsl)>
|
||||
<xsl:template match="other">never</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
let main = """
|
||||
<xsl:stylesheet version="1.0" \(xsl)>
|
||||
<xsl:import href="base.xsl"/>
|
||||
<xsl:output method="xml" omit-xml-declaration="yes"/>
|
||||
<xsl:template match="/"><out><xsl:apply-templates select="r/item"/></out></xsl:template>
|
||||
<xsl:template match="item">main[<xsl:apply-imports/>]</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
// 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, "<r><item>y</item></r>") { $0 == "base.xsl" ? base : nil }
|
||||
#expect(output == "<out>main[y]</out>")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user