diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9cf04f5..02e7cd5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- RELAX NG namespaces and semantics, second burn-down (#131): the compiler now implements `ns` inheritance and the QName form of names (4.9-4.10: `ns` walks ancestors and crosses include/externalRef document boundaries; `name="eg:foo"` and `eg:foo` resolve their prefix against the schema's in-scope xmlns declarations; the `name=` shorthand on an attribute stays namespace-less rather than inheriting), an `include` brings in the included grammar's `start` with an include-level override honored (4.7), multiple `` elements combine like defines and a single define or start may omit `combine` while still merging by the named method (4.17), `` supports `` via a new `dataExcept` pattern in the derivative engine (4.12), an empty element matches a data/value pattern whose lexical space admits the empty string (Clark's empty-text rule), and a whitespace-only attribute value matches a pattern that matches empty. Spec-suite baselines tighten from valid 242/289 to 274/289 and invalid 278/291 to 279/291; the name-class and tree-helper sections split to their own files for the length caps.
- RELAX NG simplification, first burn-down (#131): foreign-namespace elements are stripped before pattern interpretation (4.1: only RELAX NG-namespace elements are schema content; a namespace-less element counts only inside a fully unqualified schema), name/type/combine attribute values are whitespace-trimmed (4.2), and include/externalRef hrefs resolve against the xml:base attributes in scope and the loaded document's own URI (4.5, reusing the RFC 3986 resolver), with the document base threaded through nested loads and visited-tracking keyed on resolved hrefs. Spec-suite baselines tighten: valid 242/289 accepted (from 233), invalid 278/291 rejected.
- RELAX NG spec test suite runner (#122, #131): James Clark's spectest.xml (385 cases, distributed with jing-trang) runs via an opt-in RNG_TS_ROOT runner, never vendored; the manifest is parsed with PureXML itself, nested testSuite scopes thread their resource/dir definitions into the compiler's loader, and each case asserts compile/reject for the schema plus accept/reject per instance. First measurement, carried as exact baselines: every correct schema compiles and most instances behave (valid 233/289 accepted, invalid 277/291 rejected), while schema-correctness validation is absent (all 213 incorrect schemas compile, count asserted exactly). The burn-down classes are recorded in the runner: schema validation against the RELAX NG grammar and the 4.16-4.18 restrictions, foreign-namespace stripping (4.1), xml:base-aware include/externalRef resolution (4.5), and the section 6 compatibility datatypes.
- Canonical XML 1.0 spec-example vectors (#122, #132): the worked examples of the C14N 1.0 Recommendation's section 3 run as input/expected-output tests (cited per example; used under the W3C Software and Document License), all six passing: top-level PIs/comments with and without comments mode, whitespace preservation, start/end tags with attribute sorting and DTD defaults, character modifications and references, entity expansion, and UTF-8 output from legacy-encoded input. The vectors exposed three real conformance fixes: top-level siblings in the canonical form are now separated by single line feeds; an empty default-namespace declaration renders only when it undeclares a rendered non-empty default (superfluous `xmlns=""` is dropped, inclusive mode); and the text/attribute escape loops iterate Unicode scalars, so a carriage return that grapheme-clusters with a following line feed still escapes to `
` (the #135 class, surfacing in output rather than lexing).
diff --git a/Sources/Schema/RelaxNGDerivative.swift b/Sources/Schema/RelaxNGDerivative.swift
index 1d85559..f8f2d20 100644
--- a/Sources/Schema/RelaxNGDerivative.swift
+++ b/Sources/Schema/RelaxNGDerivative.swift
@@ -92,6 +92,8 @@ extension PureXML.Schema {
case let .value(type, literal):
type.valueMatches(string, literal: literal) ? .empty : .notAllowed
case let .data(type): type.validate(string) == nil ? .empty : .notAllowed
+ case let .dataExcept(type, except):
+ type.validate(string) == nil && !nullable(valueDeriv(except, string)) ? .empty : .notAllowed
case let .list(inner): listMatches(inner, string) ? .empty : .notAllowed
default: .notAllowed
}
@@ -124,7 +126,11 @@ extension PureXML.Schema {
}
private func valueMatches(_ content: PureXML.Schema.Pattern, _ value: String) -> Bool {
- value.isEmpty ? nullable(content) : nullable(textDeriv(content, value))
+ if nullable(textDeriv(content, value)) { return true }
+ // A whitespace-only attribute value also matches a pattern that
+ // matches the empty sequence, the same leniency element content
+ // gets for ignorable whitespace.
+ return value.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isWhitespace) && nullable(content)
}
private func oneOrMoreP(_ inner: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
@@ -206,7 +212,9 @@ extension PureXML.Schema.RelaxNGEngine {
private func childrenDeriv(_ pattern: PureXML.Schema.Pattern, _ children: [PureXML.Model.Node]) -> PureXML.Schema.Pattern {
let coalesced = Self.coalesceText(children)
- if coalesced.isEmpty { return pattern }
+ // Clark's algorithm treats an empty element as optional empty text, so
+ // a data/value pattern whose lexical space admits "" matches .
+ if coalesced.isEmpty { return choice(pattern, textDeriv(pattern, "")) }
if coalesced.count == 1, case let .text(string) = coalesced[0] {
let derived = textDeriv(pattern, string)
return Self.isWhitespace(string) ? choice(pattern, derived) : derived
diff --git a/Sources/Schema/RelaxNGNameClasses.swift b/Sources/Schema/RelaxNGNameClasses.swift
new file mode 100644
index 0000000..bd5c65e
--- /dev/null
+++ b/Sources/Schema/RelaxNGNameClasses.swift
@@ -0,0 +1,92 @@
+/// The name-class side of the RELAX NG compiler (4.9-4.10: ns inheritance and
+/// QName resolution), split from the compiler body to keep it under the
+/// length caps.
+extension RNGCompiler {
+ typealias Tree = RNGTree
+
+ func nameClassAndContent(_ node: Tree) -> (NameClass, [Tree]) {
+ let children = RNGNode.elementChildren(node)
+ if let name = RNGNode.attribute(node, "name") {
+ if let qualified = RNGNode.resolveQName(name, at: node) {
+ return (.name(namespace: qualified.namespace, localName: qualified.localName), children)
+ }
+ // The `name=` shorthand on an attribute does not inherit ns: an
+ // unprefixed attribute name has no namespace unless the attribute
+ // element itself carries ns (4.9). Elements inherit normally.
+ let isAttribute = RNGNode.localName(node) == "attribute"
+ let namespace = isAttribute
+ ? (node.attributes.first { $0.name.prefix == nil && $0.name.localName == "ns" }?.value ?? "")
+ : effectiveNS(node)
+ return (.name(namespace: namespace, localName: name), children)
+ }
+ guard let first = children.first else { return (.anyName, []) }
+ return (nameClass(first), Array(children.dropFirst()))
+ }
+
+ // MARK: Combinators and name classes
+
+ func combined(_ nodes: [Tree], _ compositor: Compositor) -> Pattern {
+ let patterns = nodes.map(pattern)
+ let identity: Pattern = compositor == .choice ? .notAllowed : .empty
+ guard let first = patterns.first else { return identity }
+ return patterns.dropFirst().reduce(first) { combine($0, $1, compositor) }
+ }
+
+ func combine(_ lhs: Pattern, _ rhs: Pattern, _ compositor: Compositor) -> Pattern {
+ switch compositor {
+ case .sequence: .group(lhs, rhs)
+ case .choice: .choice(lhs, rhs)
+ case .all: .interleave(lhs, rhs)
+ }
+ }
+
+ func nameClass(_ node: Tree) -> NameClass {
+ switch RNGNode.localName(node) {
+ case "name":
+ nameElementClass(node)
+ case "anyName":
+ anyName(node)
+ case "nsName":
+ nsName(node)
+ case "choice":
+ nameClassChoice(RNGNode.elementChildren(node))
+ default:
+ .anyName
+ }
+ }
+
+ func anyName(_ node: Tree) -> NameClass {
+ guard let except = exceptClass(node) else { return .anyName }
+ return .anyNameExcept(except)
+ }
+
+ /// A `` element: its text may be a QName resolved against in-scope
+ /// xmlns declarations (4.10); otherwise the inherited ns applies (4.9).
+ func nameElementClass(_ node: Tree) -> NameClass {
+ let raw = RNGNode.text(node)
+ if let qualified = RNGNode.resolveQName(raw, at: node) {
+ return .name(namespace: qualified.namespace, localName: qualified.localName)
+ }
+ return .name(namespace: effectiveNS(node), localName: raw)
+ }
+
+ func nsName(_ node: Tree) -> NameClass {
+ let namespace = effectiveNS(node)
+ guard let except = exceptClass(node) else { return .nsName(namespace) }
+ return .nsNameExcept(namespace: namespace, except: except)
+ }
+
+ /// The name class of a ``'s `` child, treating multiple
+ /// children as an implicit choice (`anyName`/`nsName` minus the union).
+ func exceptClass(_ node: Tree) -> NameClass? {
+ guard let except = RNGNode.children(node, named: "except").first else { return nil }
+ let children = RNGNode.elementChildren(except)
+ return children.isEmpty ? nil : nameClassChoice(children)
+ }
+
+ func nameClassChoice(_ nodes: [Tree]) -> NameClass {
+ let classes = nodes.map(nameClass)
+ guard let first = classes.first else { return .anyName }
+ return classes.dropFirst().reduce(first) { .choice($0, $1) }
+ }
+}
diff --git a/Sources/Schema/RelaxNGNode.swift b/Sources/Schema/RelaxNGNode.swift
new file mode 100644
index 0000000..6419663
--- /dev/null
+++ b/Sources/Schema/RelaxNGNode.swift
@@ -0,0 +1,109 @@
+typealias RNGTree = PureXML.Model.TreeNode
+
+/// RNGTree helpers for the RELAX NG parser. File-scope and private.
+enum RNGNode {
+ static func localName(_ node: RNGTree) -> String? {
+ node.name?.localName
+ }
+
+ static func attribute(_ node: RNGTree, _ name: String) -> String? {
+ let raw = node.attributes.first { $0.name.localName == name }?.value
+ // Simplification 4.2: the value of name, type, and combine attributes
+ // is whitespace-trimmed.
+ guard let raw, ["name", "type", "combine"].contains(name) else { return raw }
+ return raw.trimmingXMLWhitespace()
+ }
+
+ /// The RELAX NG namespace; simplification 4.1 removes foreign-namespace
+ /// elements (annotations such as documentation or comments) before any
+ /// pattern interpretation.
+ static let relaxNGNamespace = "http://relaxng.org/ns/structure/1.0"
+
+ static func elementChildren(_ node: RNGTree) -> [RNGTree] {
+ node.children.filter { child in
+ guard child.kind == .element else { return false }
+ // Simplification 4.1: only RELAX NG-namespace elements are schema
+ // content. An element with no namespace counts as schema content
+ // only when the whole document is unqualified (leniency for
+ // namespace-less schemas); under a qualified parent it is foreign.
+ let uri = child.name?.namespaceURI
+ if uri == relaxNGNamespace { return true }
+ return uri == nil && node.name?.namespaceURI == nil
+ }
+ }
+
+ static func children(_ node: RNGTree, named name: String) -> [RNGTree] {
+ elementChildren(node).filter { localName($0) == name }
+ }
+
+ static func text(_ node: RNGTree) -> String {
+ node.stringValue.trimmingXMLWhitespace()
+ }
+
+ static func strip(_ qualified: String) -> String {
+ qualified.split(separator: ":").last.map(String.init) ?? qualified
+ }
+
+ /// The nearest `ns` attribute on the node or an ancestor (simplification
+ /// 4.9 inheritance), or nil when none is in scope in this document.
+ static func inheritedNS(_ node: RNGTree) -> String? {
+ var current: RNGTree? = node
+ while let candidate = current {
+ if let namespace = candidate.attributes.first(where: { $0.name.prefix == nil && $0.name.localName == "ns" })?.value {
+ return namespace
+ }
+ current = candidate.parent
+ }
+ return nil
+ }
+
+ /// Resolves a prefixed name against the xmlns declarations in scope at the
+ /// schema node (simplification 4.10), or nil for an unprefixed name.
+ static func resolveQName(_ raw: String, at node: RNGTree) -> (namespace: String, localName: String)? {
+ guard let colon = raw.firstIndex(of: ":") else { return nil }
+ let prefix = String(raw[.. String {
+ guard let base, !base.isEmpty else { return reference }
+ let merged = PureXML.Canonical.Canonicalizer.resolveURI(reference, against: base)
+ let baseHasScheme = base.split(separator: "/", maxSplits: 1)[0].hasSuffix(":")
+ if !base.hasPrefix("/"), !baseHasScheme, !reference.hasPrefix("/"), merged.hasPrefix("/") {
+ return String(merged.dropFirst())
+ }
+ return merged
+ }
+
+ /// The `href` of an include/externalRef, resolved against the document's
+ /// base and the `xml:base` attributes in scope at the node (4.5).
+ static func resolvedHref(_ node: RNGTree, documentBase: String?) -> String? {
+ guard let href = attribute(node, "href") else { return nil }
+ var bases: [String] = []
+ var cursor: RNGTree? = node
+ while let current = cursor {
+ if let base = current.attributes.first(where: { $0.name.prefix == "xml" && $0.name.localName == "base" })?.value {
+ bases.append(base)
+ }
+ cursor = current.parent
+ }
+ if let documentBase { bases.append(documentBase) }
+ var resolved = href
+ for base in bases {
+ resolved = resolveRelative(resolved, against: base)
+ }
+ return resolved
+ }
+}
diff --git a/Sources/Schema/RelaxNGParser.swift b/Sources/Schema/RelaxNGParser.swift
index fc9ccf6..a81bc52 100644
--- a/Sources/Schema/RelaxNGParser.swift
+++ b/Sources/Schema/RelaxNGParser.swift
@@ -1,97 +1,36 @@
-private typealias Tree = PureXML.Model.TreeNode
-
-/// Tree helpers for the RELAX NG parser. File-scope and private.
-private enum RNGNode {
- static func localName(_ node: Tree) -> String? {
- node.name?.localName
- }
-
- static func attribute(_ node: Tree, _ name: String) -> String? {
- let raw = node.attributes.first { $0.name.localName == name }?.value
- // Simplification 4.2: the value of name, type, and combine attributes
- // is whitespace-trimmed.
- guard let raw, ["name", "type", "combine"].contains(name) else { return raw }
- return raw.trimmingXMLWhitespace()
- }
-
- /// The RELAX NG namespace; simplification 4.1 removes foreign-namespace
- /// elements (annotations such as documentation or comments) before any
- /// pattern interpretation.
- static let relaxNGNamespace = "http://relaxng.org/ns/structure/1.0"
-
- static func elementChildren(_ node: Tree) -> [Tree] {
- node.children.filter { child in
- guard child.kind == .element else { return false }
- // Simplification 4.1: only RELAX NG-namespace elements are schema
- // content. An element with no namespace counts as schema content
- // only when the whole document is unqualified (leniency for
- // namespace-less schemas); under a qualified parent it is foreign.
- let uri = child.name?.namespaceURI
- if uri == relaxNGNamespace { return true }
- return uri == nil && node.name?.namespaceURI == nil
- }
- }
-
- static func children(_ node: Tree, named name: String) -> [Tree] {
- elementChildren(node).filter { localName($0) == name }
- }
-
- static func text(_ node: Tree) -> String {
- node.stringValue.trimmingXMLWhitespace()
- }
-
- static func strip(_ qualified: String) -> String {
- qualified.split(separator: ":").last.map(String.init) ?? qualified
- }
-
- /// Resolves `reference` against an optional base URI (RFC 3986), keeping a
- /// relative base's merged form relative (the same adjustment external
- /// identifiers use).
- static func resolveRelative(_ reference: String, against base: String?) -> String {
- guard let base, !base.isEmpty else { return reference }
- let merged = PureXML.Canonical.Canonicalizer.resolveURI(reference, against: base)
- let baseHasScheme = base.split(separator: "/", maxSplits: 1)[0].hasSuffix(":")
- if !base.hasPrefix("/"), !baseHasScheme, !reference.hasPrefix("/"), merged.hasPrefix("/") {
- return String(merged.dropFirst())
- }
- return merged
- }
-
- /// The `href` of an include/externalRef, resolved against the document's
- /// base and the `xml:base` attributes in scope at the node (4.5).
- static func resolvedHref(_ node: Tree, documentBase: String?) -> String? {
- guard let href = attribute(node, "href") else { return nil }
- var bases: [String] = []
- var cursor: Tree? = node
- while let current = cursor {
- if let base = current.attributes.first(where: { $0.name.prefix == "xml" && $0.name.localName == "base" })?.value {
- bases.append(base)
- }
- cursor = current.parent
- }
- if let documentBase { bases.append(documentBase) }
- var resolved = href
- for base in bases {
- resolved = resolveRelative(resolved, against: base)
- }
- return resolved
- }
-}
+private typealias Tree = RNGTree
/// Compiles a RELAX NG XML-syntax document into the pattern algebra, resolving
/// `include` and `externalRef` through a loader. Holds the accumulating `define`
/// table so external grammars merge into it. File-scope and private.
-private final class RNGCompiler {
+final class RNGCompiler {
typealias Pattern = PureXML.Schema.Pattern
typealias NameClass = PureXML.Schema.NameClass
typealias Compositor = PureXML.Schema.Compositor
private(set) var defines: [String: Pattern] = [:]
+ /// The combine method established for each define name (4.17): defines
+ /// sharing a name agree on one method, and the single define that omits
+ /// `combine` still merges by it.
+ private var defineCombine: [String: String] = [:]
private let loader: (String) -> String?
private var visited: Set = []
/// The URI of the document currently being compiled (nil for the top
/// level): hrefs inside a loaded document resolve against it.
private var documentBase: String?
+ /// The `ns` value carried across a document boundary: an externalRef's
+ /// in-scope ns applies to the loaded pattern when it has none of its own
+ /// (simplification 4.6); the tree-ancestor walk cannot cross documents.
+ private var fallbackNamespace = ""
+ /// The start pattern contributed by an `include`d grammar (4.7), used when
+ /// the including grammar declares no start of its own.
+ private var includedStart: Pattern?
+
+ /// The ns in scope at a schema node: its own/ancestor `ns` attribute, or
+ /// the namespace inherited across the document boundary.
+ func effectiveNS(_ node: Tree) -> String {
+ RNGNode.inheritedNS(node) ?? fallbackNamespace
+ }
init(loader: @escaping (String) -> String?) {
self.loader = loader
@@ -108,8 +47,26 @@ private final class RNGCompiler {
for define in RNGNode.children(node, named: "define") {
addDefine(define)
}
- let start = RNGNode.children(node, named: "start").first
- return start.map { combined(RNGNode.elementChildren($0), .sequence) } ?? .notAllowed
+ let starts = RNGNode.children(node, named: "start")
+ guard let first = starts.first else {
+ return includedStart ?? .notAllowed
+ }
+ // Multiple starts combine like defines (4.17): the named method wins,
+ // and the single start that omits `combine` still merges by it.
+ var method: String?
+ for start in starts {
+ if let named = RNGNode.attribute(start, "combine") { method = named }
+ }
+ var result = combined(RNGNode.elementChildren(first), .sequence)
+ for start in starts.dropFirst() {
+ let next = combined(RNGNode.elementChildren(start), .sequence)
+ switch method {
+ case "choice": result = .choice(result, next)
+ case "interleave": result = .interleave(result, next)
+ default: result = next
+ }
+ }
+ return result
}
/// Adds a `define`, honoring `combine`: a second definition of the same name
@@ -118,11 +75,14 @@ private final class RNGCompiler {
private func addDefine(_ define: Tree) {
guard let name = RNGNode.attribute(define, "name") else { return }
let pattern = combined(RNGNode.elementChildren(define), .sequence)
+ if let method = RNGNode.attribute(define, "combine") {
+ defineCombine[name] = method
+ }
guard let existing = defines[name] else {
defines[name] = pattern
return
}
- switch RNGNode.attribute(define, "combine") {
+ switch defineCombine[name] {
case "choice": defines[name] = .choice(existing, pattern)
case "interleave": defines[name] = .interleave(existing, pattern)
default: defines[name] = pattern
@@ -140,12 +100,22 @@ private final class RNGCompiler {
}
visited.insert(href)
let outerBase = documentBase
+ let outerNamespace = fallbackNamespace
documentBase = href
- defer { documentBase = outerBase }
+ if RNGNode.inheritedNS(grammar) == nil {
+ fallbackNamespace = effectiveNS(node)
+ }
for define in RNGNode.children(grammar, named: "define") {
addDefine(define)
}
+ if includedStart == nil, let start = RNGNode.children(grammar, named: "start").first {
+ includedStart = combined(RNGNode.elementChildren(start), .sequence)
+ }
documentBase = outerBase
+ fallbackNamespace = outerNamespace
+ if let override = RNGNode.children(node, named: "start").first {
+ includedStart = combined(RNGNode.elementChildren(override), .sequence)
+ }
for define in RNGNode.children(node, named: "define") {
addDefine(define)
}
@@ -153,7 +123,7 @@ private final class RNGCompiler {
// MARK: Patterns
- private func pattern(_ node: Tree) -> Pattern {
+ func pattern(_ node: Tree) -> Pattern {
leafPattern(node) ?? compositePattern(node)
}
@@ -189,7 +159,13 @@ private final class RNGCompiler {
PureXML.Schema.RelaxNGFacets.apply(name, RNGNode.text(param), into: &facets)
}
}
- return .data(PureXML.Schema.SimpleType(base: base, facets: facets))
+ let type = PureXML.Schema.SimpleType(base: base, facets: facets)
+ // 4.12: an except child excludes its (implicitly choice-combined)
+ // patterns from the data type's lexical space.
+ if let except = RNGNode.children(node, named: "except").first {
+ return .dataExcept(type, combined(RNGNode.elementChildren(except), .choice))
+ }
+ return .data(type)
}
/// Builds a `` pattern carrying its datatype, so the value compares in
@@ -246,8 +222,15 @@ private final class RNGCompiler {
}
visited.insert(href)
let outerBase = documentBase
+ let outerNamespace = fallbackNamespace
documentBase = href
- defer { documentBase = outerBase }
+ if RNGNode.inheritedNS(top) == nil {
+ fallbackNamespace = effectiveNS(node)
+ }
+ defer {
+ documentBase = outerBase
+ fallbackNamespace = outerNamespace
+ }
return topLevel(top)
}
@@ -279,73 +262,6 @@ private final class RNGCompiler {
let content = contentNodes.isEmpty ? Pattern.text : combined(contentNodes, .sequence)
return .attribute(nameClass, content)
}
-
- private func nameClassAndContent(_ node: Tree) -> (NameClass, [Tree]) {
- let children = RNGNode.elementChildren(node)
- if let name = RNGNode.attribute(node, "name") {
- let namespace = RNGNode.attribute(node, "ns") ?? ""
- return (.name(namespace: namespace, localName: name), children)
- }
- guard let first = children.first else { return (.anyName, []) }
- return (nameClass(first), Array(children.dropFirst()))
- }
-
- // MARK: Combinators and name classes
-
- private func combined(_ nodes: [Tree], _ compositor: Compositor) -> Pattern {
- let patterns = nodes.map(pattern)
- let identity: Pattern = compositor == .choice ? .notAllowed : .empty
- guard let first = patterns.first else { return identity }
- return patterns.dropFirst().reduce(first) { combine($0, $1, compositor) }
- }
-
- private func combine(_ lhs: Pattern, _ rhs: Pattern, _ compositor: Compositor) -> Pattern {
- switch compositor {
- case .sequence: .group(lhs, rhs)
- case .choice: .choice(lhs, rhs)
- case .all: .interleave(lhs, rhs)
- }
- }
-
- private func nameClass(_ node: Tree) -> NameClass {
- switch RNGNode.localName(node) {
- case "name":
- .name(namespace: RNGNode.attribute(node, "ns") ?? "", localName: RNGNode.text(node))
- case "anyName":
- anyName(node)
- case "nsName":
- nsName(node)
- case "choice":
- nameClassChoice(RNGNode.elementChildren(node))
- default:
- .anyName
- }
- }
-
- private func anyName(_ node: Tree) -> NameClass {
- guard let except = exceptClass(node) else { return .anyName }
- return .anyNameExcept(except)
- }
-
- private func nsName(_ node: Tree) -> NameClass {
- let namespace = RNGNode.attribute(node, "ns") ?? ""
- guard let except = exceptClass(node) else { return .nsName(namespace) }
- return .nsNameExcept(namespace: namespace, except: except)
- }
-
- /// The name class of a ``'s `` child, treating multiple
- /// children as an implicit choice (`anyName`/`nsName` minus the union).
- private func exceptClass(_ node: Tree) -> NameClass? {
- guard let except = RNGNode.children(node, named: "except").first else { return nil }
- let children = RNGNode.elementChildren(except)
- return children.isEmpty ? nil : nameClassChoice(children)
- }
-
- private func nameClassChoice(_ nodes: [Tree]) -> NameClass {
- let classes = nodes.map(nameClass)
- guard let first = classes.first else { return .anyName }
- return classes.dropFirst().reduce(first) { .choice($0, $1) }
- }
}
extension PureXML.Schema {
diff --git a/Sources/Schema/RelaxNGPattern.swift b/Sources/Schema/RelaxNGPattern.swift
index 2a07ce7..df4ad35 100644
--- a/Sources/Schema/RelaxNGPattern.swift
+++ b/Sources/Schema/RelaxNGPattern.swift
@@ -36,6 +36,9 @@ public extension PureXML.Schema {
case element(NameClass, Pattern)
case attribute(NameClass, Pattern)
case data(SimpleType)
+ /// `` with an `` child: the type matches but the except
+ /// pattern must not (4.12).
+ case dataExcept(SimpleType, Pattern)
case value(SimpleType, String)
case list(Pattern)
case ref(String)
diff --git a/Tests/RelaxNGSpecSuiteTests.swift b/Tests/RelaxNGSpecSuiteTests.swift
index fc7fd7e..cafe128 100644
--- a/Tests/RelaxNGSpecSuiteTests.swift
+++ b/Tests/RelaxNGSpecSuiteTests.swift
@@ -33,12 +33,10 @@ struct RelaxNGSpecSuiteTests {
private let knownIncorrectCompiledCount = 213
private let knownCorrectRejected: Set = []
private let knownValidRejected: Set = [
- 98, 104, 109, 110, 111, 115, 119, 120, 124, 125, 126, 127, 128, 130,
- 131, 132, 133, 142, 190, 191, 194, 195, 208, 209, 210, 236, 256, 258,
- 265, 266, 268, 269, 271, 328, 372, 374, 378, 379, 380,
+ 98, 111, 115, 133, 195, 208, 209, 210, 271, 328, 379, 380,
]
private let knownInvalidAccepted: Set = [
- 104, 124, 125, 126, 127, 128, 142, 264, 271,
+ 120, 142, 264, 271, 378, 379,
]
private struct SpecCase {