feat(relaxng): ns inheritance, QNames, include-start, combine, data-except
The compiler implements simplification 4.9-4.10 (ns inheritance across ancestors and document boundaries; QName resolution against in-scope xmlns declarations; the attribute name= shorthand stays namespace-less), include brings in the included grammar's start with overrides (4.7), multiple starts and defines combine with a single omitted combine merging by the named method (4.17), data/except lands as a dataExcept pattern in the derivative engine (4.12), an empty element matches data whose lexical space admits the empty string, and whitespace-only attribute values match empty-matching patterns. Spec suite: valid 274/289, invalid 279/291.
This commit is contained in:
parent
2eab91bf2b
commit
b8fa90ccf5
@ -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 `<name>eg:foo</name>` 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 `<start>` elements combine like defines and a single define or start may omit `combine` while still merging by the named method (4.17), `<data>` supports `<except>` 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).
|
||||
|
||||
@ -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 <e/>.
|
||||
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
|
||||
|
||||
92
Sources/Schema/RelaxNGNameClasses.swift
Normal file
92
Sources/Schema/RelaxNGNameClasses.swift
Normal file
@ -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 `<name>` 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 `<name-class>`'s `<except>` 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) }
|
||||
}
|
||||
}
|
||||
109
Sources/Schema/RelaxNGNode.swift
Normal file
109
Sources/Schema/RelaxNGNode.swift
Normal file
@ -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[..<colon])
|
||||
let local = String(raw[raw.index(after: colon)...])
|
||||
if prefix == "xml" { return ("http://www.w3.org/XML/1998/namespace", local) }
|
||||
var current: RNGTree? = node
|
||||
while let candidate = current {
|
||||
if let uri = candidate.attributes.first(where: { $0.name.prefix == "xmlns" && $0.name.localName == prefix })?.value {
|
||||
return (uri, local)
|
||||
}
|
||||
current = candidate.parent
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 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: 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
|
||||
}
|
||||
}
|
||||
@ -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<String> = []
|
||||
/// 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 `<value>` 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 `<name-class>`'s `<except>` 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 {
|
||||
|
||||
@ -36,6 +36,9 @@ public extension PureXML.Schema {
|
||||
case element(NameClass, Pattern)
|
||||
case attribute(NameClass, Pattern)
|
||||
case data(SimpleType)
|
||||
/// `<data>` with an `<except>` 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)
|
||||
|
||||
@ -33,12 +33,10 @@ struct RelaxNGSpecSuiteTests {
|
||||
private let knownIncorrectCompiledCount = 213
|
||||
private let knownCorrectRejected: Set<Int> = []
|
||||
private let knownValidRejected: Set<Int> = [
|
||||
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<Int> = [
|
||||
104, 124, 125, 126, 127, 128, 142, 264, 271,
|
||||
120, 142, 264, 271, 378, 379,
|
||||
]
|
||||
|
||||
private struct SpecCase {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user