feat(relaxng): simplification 4.1, 4.2, and xml:base href resolution
Foreign-namespace elements are stripped before pattern interpretation (namespace-less elements count only inside fully unqualified schemas), name/type/combine values are whitespace-trimmed, and include and externalRef hrefs resolve against in-scope xml:base attributes and the loaded document's URI, threaded through nested loads with visited tracking on resolved hrefs. Spec-suite baselines tighten to valid 242/289 and invalid 278/291.
This commit is contained in:
parent
b6981baf19
commit
2eab91bf2b
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- 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).
|
||||
- Scalar-level lexing (#135): the reader now buffers exactly one Unicode scalar per element (multi-scalar graphemes from any source are split, string input iterates scalars directly), so a combining mark directly after an ASCII delimiter can no longer merge with it into one Swift grapheme cluster and XML's scalar-defined name productions classify correctly: U+06D6 lexes as a Fifth Edition PI-target NameStartChar, ZWNJ/ZWJ attribute names parse, and U+309A markup from entity expansion forms a real element. The replacement-text, content-model, and ATTLIST sub-scanners and the entity-literal validator go scalar-level the same way, while grapheme clusters reassemble naturally wherever scanned text is appended to a String (asserted by test). This clears the last eduni baseline (now empty), grows the IBM pre-5e class from 279 to 300 exactly as the Fifth Edition prescribes, and, combined with the EDITION="1 2 3 4" exclusions the xmltest manifest itself carries for cases 140/141, empties the xmltest deviation baseline entirely: every xmlconf case applicable to a Fifth Edition, namespace-aware processor now passes.
|
||||
|
||||
@ -7,11 +7,29 @@ private enum RNGNode {
|
||||
}
|
||||
|
||||
static func attribute(_ node: Tree, _ name: String) -> String? {
|
||||
node.attributes.first { $0.name.localName == name }?.value
|
||||
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 { $0.kind == .element }
|
||||
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] {
|
||||
@ -25,6 +43,39 @@ private enum RNGNode {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// Compiles a RELAX NG XML-syntax document into the pattern algebra, resolving
|
||||
@ -38,6 +89,9 @@ private final class RNGCompiler {
|
||||
private(set) var defines: [String: Pattern] = [:]
|
||||
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?
|
||||
|
||||
init(loader: @escaping (String) -> String?) {
|
||||
self.loader = loader
|
||||
@ -78,16 +132,20 @@ private final class RNGCompiler {
|
||||
/// Merges an `include`d grammar: its `define`s first, then the `include`'s own
|
||||
/// 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),
|
||||
guard let href = RNGNode.resolvedHref(node, documentBase: documentBase), !visited.contains(href),
|
||||
let text = loader(href), let root = try? PureXML.parseTree(text),
|
||||
let grammar = RNGNode.elementChildren(root).first
|
||||
else {
|
||||
return
|
||||
}
|
||||
visited.insert(href)
|
||||
let outerBase = documentBase
|
||||
documentBase = href
|
||||
defer { documentBase = outerBase }
|
||||
for define in RNGNode.children(grammar, named: "define") {
|
||||
addDefine(define)
|
||||
}
|
||||
documentBase = outerBase
|
||||
for define in RNGNode.children(node, named: "define") {
|
||||
addDefine(define)
|
||||
}
|
||||
@ -180,13 +238,16 @@ private final class RNGCompiler {
|
||||
/// Resolves an `externalRef` to the referenced schema's pattern, merging any
|
||||
/// grammar `define`s it carries into this compiler's table.
|
||||
private func externalRef(_ node: Tree) -> Pattern {
|
||||
guard let href = RNGNode.attribute(node, "href"), !visited.contains(href),
|
||||
guard let href = RNGNode.resolvedHref(node, documentBase: documentBase), !visited.contains(href),
|
||||
let text = loader(href), let root = try? PureXML.parseTree(text),
|
||||
let top = RNGNode.elementChildren(root).first
|
||||
else {
|
||||
return .notAllowed
|
||||
}
|
||||
visited.insert(href)
|
||||
let outerBase = documentBase
|
||||
documentBase = href
|
||||
defer { documentBase = outerBase }
|
||||
return topLevel(top)
|
||||
}
|
||||
|
||||
|
||||
@ -33,13 +33,12 @@ struct RelaxNGSpecSuiteTests {
|
||||
private let knownIncorrectCompiledCount = 213
|
||||
private let knownCorrectRejected: Set<Int> = []
|
||||
private let knownValidRejected: Set<Int> = [
|
||||
90, 92, 93, 94, 95, 96, 98, 100, 101, 104, 108, 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, 264, 265, 266, 268, 269, 271,
|
||||
328, 372, 374, 378, 379, 380,
|
||||
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,
|
||||
]
|
||||
private let knownInvalidAccepted: Set<Int> = [
|
||||
101, 104, 124, 125, 126, 127, 128, 142, 264, 271,
|
||||
104, 124, 125, 126, 127, 128, 142, 264, 271,
|
||||
]
|
||||
|
||||
private struct SpecCase {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user