feat(relaxng): combine on define (choice/interleave merging)
A define repeated with combine="choice" or combine="interleave" now merges with the existing definition (as a choice or interleave pattern) instead of the last one silently replacing it. A plain redefinition with no combine still replaces. Applied consistently in a grammar and across include merges. <param> facets under <data>, datatypeLibrary resolution, typed <value> comparison, and the RNC-syntax gaps remain. Part of #79
This commit is contained in:
parent
8a52cdbcf2
commit
24a862e3a0
@ -52,16 +52,31 @@ private final class RNGCompiler {
|
||||
mergeInclude(include)
|
||||
}
|
||||
for define in RNGNode.children(node, named: "define") {
|
||||
if let name = RNGNode.attribute(define, "name") {
|
||||
defines[name] = combined(RNGNode.elementChildren(define), .sequence)
|
||||
}
|
||||
addDefine(define)
|
||||
}
|
||||
let start = RNGNode.children(node, named: "start").first
|
||||
return start.map { combined(RNGNode.elementChildren($0), .sequence) } ?? .notAllowed
|
||||
}
|
||||
|
||||
/// Adds a `define`, honoring `combine`: a second definition of the same name
|
||||
/// with `combine="choice"`/`"interleave"` merges with the existing one rather
|
||||
/// than replacing it (a plain redefinition still replaces).
|
||||
private func addDefine(_ define: Tree) {
|
||||
guard let name = RNGNode.attribute(define, "name") else { return }
|
||||
let pattern = combined(RNGNode.elementChildren(define), .sequence)
|
||||
guard let existing = defines[name] else {
|
||||
defines[name] = pattern
|
||||
return
|
||||
}
|
||||
switch RNGNode.attribute(define, "combine") {
|
||||
case "choice": defines[name] = .choice(existing, pattern)
|
||||
case "interleave": defines[name] = .interleave(existing, pattern)
|
||||
default: defines[name] = pattern
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges an `include`d grammar: its `define`s first, then the `include`'s own
|
||||
/// nested `define`s, which override the included ones.
|
||||
/// nested `define`s, which override or (with `combine`) merge with them.
|
||||
private func mergeInclude(_ node: Tree) {
|
||||
guard let href = RNGNode.attribute(node, "href"), !visited.contains(href),
|
||||
let text = loader(href), let root = try? PureXML.parseTree(text),
|
||||
@ -71,14 +86,10 @@ private final class RNGCompiler {
|
||||
}
|
||||
visited.insert(href)
|
||||
for define in RNGNode.children(grammar, named: "define") {
|
||||
if let name = RNGNode.attribute(define, "name") {
|
||||
defines[name] = combined(RNGNode.elementChildren(define), .sequence)
|
||||
}
|
||||
addDefine(define)
|
||||
}
|
||||
for define in RNGNode.children(node, named: "define") {
|
||||
if let name = RNGNode.attribute(define, "name") {
|
||||
defines[name] = combined(RNGNode.elementChildren(define), .sequence)
|
||||
}
|
||||
addDefine(define)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
55
Tests/RelaxNGCombineTests.swift
Normal file
55
Tests/RelaxNGCombineTests.swift
Normal file
@ -0,0 +1,55 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("RELAX NG combine on define")
|
||||
struct RelaxNGCombineTests {
|
||||
private let rngNamespace = "xmlns=\"http://relaxng.org/ns/structure/1.0\""
|
||||
|
||||
private func valid(_ rng: String, _ xml: String) throws -> Bool {
|
||||
try PureXML.Schema.RelaxNG(rng).validate(xml)
|
||||
}
|
||||
|
||||
@Test("combine=choice merges two definitions of the same name as a choice")
|
||||
func test_combineChoice() throws {
|
||||
let rng = """
|
||||
<grammar \(rngNamespace)>
|
||||
<start><ref name="content"/></start>
|
||||
<define name="content"><element name="a"><empty/></element></define>
|
||||
<define name="content" combine="choice"><element name="b"><empty/></element></define>
|
||||
</grammar>
|
||||
"""
|
||||
// Both alternatives validate against the merged choice.
|
||||
#expect(try valid(rng, "<a/>"))
|
||||
#expect(try valid(rng, "<b/>"))
|
||||
#expect(try !valid(rng, "<c/>"))
|
||||
}
|
||||
|
||||
@Test("combine=interleave merges two definitions as an interleave")
|
||||
func test_combineInterleave() throws {
|
||||
let rng = """
|
||||
<grammar \(rngNamespace)>
|
||||
<start><element name="root"><ref name="content"/></element></start>
|
||||
<define name="content"><element name="a"><empty/></element></define>
|
||||
<define name="content" combine="interleave"><element name="b"><empty/></element></define>
|
||||
</grammar>
|
||||
"""
|
||||
// Interleave admits the two children in either order.
|
||||
#expect(try valid(rng, "<root><a/><b/></root>"))
|
||||
#expect(try valid(rng, "<root><b/><a/></root>"))
|
||||
// Both are required by the interleave, so one alone fails.
|
||||
#expect(try !valid(rng, "<root><a/></root>"))
|
||||
}
|
||||
|
||||
@Test("A plain redefinition without combine still replaces")
|
||||
func test_plainRedefinitionReplaces() throws {
|
||||
let rng = """
|
||||
<grammar \(rngNamespace)>
|
||||
<start><ref name="content"/></start>
|
||||
<define name="content"><element name="a"><empty/></element></define>
|
||||
<define name="content"><element name="b"><empty/></element></define>
|
||||
</grammar>
|
||||
"""
|
||||
#expect(try !valid(rng, "<a/>"))
|
||||
#expect(try valid(rng, "<b/>"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user