refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit validation idiom at the wrong granularity: no per-constraint removability, no isolable failing/near-miss tests. Split them into 32 named rules, each finding tagged at its origin and cached per XSDContext so the walk still runs once per document (the #101 decision against duplicating XSD's recursive type resolution still holds). Wired the streaming path's ID/IDREF and identity checks through the same framework instead of calling the validators imperatively, and added the unwrapAndLookup combinator the rule already named as required. Also includes the mandated repo-wide swiftformat/swiftlint pass.
This commit is contained in:
parent
36f1485f2e
commit
380160cc2b
@ -39,7 +39,9 @@ struct PublicSurfaceTests {
|
|||||||
var chunks = ["<r><a>", "hello", "</a></r>"].makeIterator()
|
var chunks = ["<r><a>", "hello", "</a></r>"].makeIterator()
|
||||||
var characters: [Character] = []
|
var characters: [Character] = []
|
||||||
var reader = PureXML.Parsing.EventReader(pulling: {
|
var reader = PureXML.Parsing.EventReader(pulling: {
|
||||||
if characters.isEmpty, let chunk = chunks.next() { characters = Array(chunk) }
|
if characters.isEmpty, let chunk = chunks.next() {
|
||||||
|
characters = Array(chunk)
|
||||||
|
}
|
||||||
return characters.isEmpty ? nil : characters.removeFirst()
|
return characters.isEmpty ? nil : characters.removeFirst()
|
||||||
})
|
})
|
||||||
var events = 0
|
var events = 0
|
||||||
|
|||||||
@ -18,7 +18,9 @@ extension PureXML.Canonical.Canonicalizer {
|
|||||||
let declared = Set(apex.attributes.map(\.name.description))
|
let declared = Set(apex.attributes.map(\.name.description))
|
||||||
for (prefix, uri) in ancestorNamespaces {
|
for (prefix, uri) in ancestorNamespaces {
|
||||||
let name = prefix.isEmpty ? "xmlns" : "xmlns:\(prefix)"
|
let name = prefix.isEmpty ? "xmlns" : "xmlns:\(prefix)"
|
||||||
if !declared.contains(name) { attributes.append(PureXML.Model.Attribute(name, uri)) }
|
if !declared.contains(name) {
|
||||||
|
attributes.append(PureXML.Model.Attribute(name, uri))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return PureXML.Model.Element(name: apex.name, attributes: attributes, children: apex.children)
|
return PureXML.Model.Element(name: apex.name, attributes: attributes, children: apex.children)
|
||||||
@ -43,7 +45,9 @@ extension PureXML.Canonical.Canonicalizer {
|
|||||||
/// apex does not already set, nearest ancestor winning.
|
/// apex does not already set, nearest ancestor winning.
|
||||||
static func inheritedXMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, mergeBase: Bool) -> [PureXML.Model.Attribute] {
|
static func inheritedXMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, mergeBase: Bool) -> [PureXML.Model.Attribute] {
|
||||||
let present = Set(apex.attributes.map(\.name.description))
|
let present = Set(apex.attributes.map(\.name.description))
|
||||||
if mergeBase { return inherited11XMLAttributes(above: node, apex: apex, present: present) }
|
if mergeBase {
|
||||||
|
return inherited11XMLAttributes(above: node, apex: apex, present: present)
|
||||||
|
}
|
||||||
// Canonical XML 1.0: the nearest of every in-scope xml:* attribute the
|
// Canonical XML 1.0: the nearest of every in-scope xml:* attribute the
|
||||||
// apex does not already set.
|
// apex does not already set.
|
||||||
var seen: Set<String> = []
|
var seen: Set<String> = []
|
||||||
@ -63,8 +67,12 @@ extension PureXML.Canonical.Canonicalizer {
|
|||||||
/// The prefix a namespace declaration binds (empty for the default
|
/// The prefix a namespace declaration binds (empty for the default
|
||||||
/// namespace), or nil when the attribute is not a namespace declaration.
|
/// namespace), or nil when the attribute is not a namespace declaration.
|
||||||
static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
|
static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
|
||||||
if name.prefix == nil, name.localName == "xmlns" { return "" }
|
if name.prefix == nil, name.localName == "xmlns" {
|
||||||
if name.prefix == "xmlns" { return name.localName }
|
return ""
|
||||||
|
}
|
||||||
|
if name.prefix == "xmlns" {
|
||||||
|
return name.localName
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -208,8 +208,12 @@ extension PureXML.Canonical {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
|
private static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
|
||||||
if name.prefix == nil, name.localName == "xmlns" { return "" }
|
if name.prefix == nil, name.localName == "xmlns" {
|
||||||
if name.prefix == "xmlns" { return name.localName }
|
return ""
|
||||||
|
}
|
||||||
|
if name.prefix == "xmlns" {
|
||||||
|
return name.localName
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,9 @@ extension PureXML.Canonical.Canonicalizer {
|
|||||||
static func inherited11XMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, present: Set<String>) -> [PureXML.Model.Attribute] {
|
static func inherited11XMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, present: Set<String>) -> [PureXML.Model.Attribute] {
|
||||||
var result: [PureXML.Model.Attribute] = []
|
var result: [PureXML.Model.Attribute] = []
|
||||||
for local in ["lang", "space"] where !present.contains("xml:\(local)") {
|
for local in ["lang", "space"] where !present.contains("xml:\(local)") {
|
||||||
if let attribute = nearestXMLAttribute(local, above: node) { result.append(attribute) }
|
if let attribute = nearestXMLAttribute(local, above: node) {
|
||||||
|
result.append(attribute)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let base = mergedBase(above: node, apex: apex) {
|
if let base = mergedBase(above: node, apex: apex) {
|
||||||
result.append(PureXML.Model.Attribute("xml:base", base))
|
result.append(PureXML.Model.Attribute("xml:base", base))
|
||||||
@ -94,27 +96,35 @@ extension PureXML.Canonical.Canonicalizer {
|
|||||||
var input = Substring(path)
|
var input = Substring(path)
|
||||||
var output = ""
|
var output = ""
|
||||||
while !input.isEmpty {
|
while !input.isEmpty {
|
||||||
if input.hasPrefix("../") { input = input.dropFirst(3)
|
if input.hasPrefix("../") {
|
||||||
|
input = input.dropFirst(3)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if input.hasPrefix("./") { input = input.dropFirst(2)
|
if input.hasPrefix("./") {
|
||||||
|
input = input.dropFirst(2)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if input.hasPrefix("/./") { input = "/" + input.dropFirst(3)
|
if input.hasPrefix("/./") {
|
||||||
|
input = "/" + input.dropFirst(3)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if input == "/." { input = "/"
|
if input == "/." {
|
||||||
|
input = "/"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if input.hasPrefix("/../") { input = "/" + input.dropFirst(4)
|
if input.hasPrefix("/../") {
|
||||||
|
input = "/" + input.dropFirst(4)
|
||||||
removeLastSegment(&output)
|
removeLastSegment(&output)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if input == "/.." { input = "/"
|
if input == "/.." {
|
||||||
|
input = "/"
|
||||||
removeLastSegment(&output)
|
removeLastSegment(&output)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if input == "." || input == ".." { break }
|
if input == "." || input == ".." {
|
||||||
|
break
|
||||||
|
}
|
||||||
let start = input.hasPrefix("/") ? input.index(after: input.startIndex) : input.startIndex
|
let start = input.hasPrefix("/") ? input.index(after: input.startIndex) : input.startIndex
|
||||||
let next = input[start...].firstIndex(of: "/") ?? input.endIndex
|
let next = input[start...].firstIndex(of: "/") ?? input.endIndex
|
||||||
output += input[..<next]
|
output += input[..<next]
|
||||||
@ -192,11 +202,19 @@ private struct URIParts {
|
|||||||
/// Recomposes the components into a URI string (RFC 3986 section 5.3).
|
/// Recomposes the components into a URI string (RFC 3986 section 5.3).
|
||||||
var recomposed: String {
|
var recomposed: String {
|
||||||
var result = ""
|
var result = ""
|
||||||
if let scheme { result += scheme + ":" }
|
if let scheme {
|
||||||
if let authority { result += "//" + authority }
|
result += scheme + ":"
|
||||||
|
}
|
||||||
|
if let authority {
|
||||||
|
result += "//" + authority
|
||||||
|
}
|
||||||
result += path
|
result += path
|
||||||
if let query { result += "?" + query }
|
if let query {
|
||||||
if let fragment { result += "#" + fragment }
|
result += "?" + query
|
||||||
|
}
|
||||||
|
if let fragment {
|
||||||
|
result += "#" + fragment
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,7 +68,9 @@ public extension PureXML.Canonical {
|
|||||||
for child in children {
|
for child in children {
|
||||||
var rendering = ""
|
var rendering = ""
|
||||||
emit(child, inScope: inScope, rendered: rendered, output: &rendering)
|
emit(child, inScope: inScope, rendered: rendered, output: &rendering)
|
||||||
if !rendering.isEmpty { renderings.append(rendering) }
|
if !rendering.isEmpty {
|
||||||
|
renderings.append(rendering)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
output += renderings.joined(separator: "\n")
|
output += renderings.joined(separator: "\n")
|
||||||
case let .element(element):
|
case let .element(element):
|
||||||
@ -77,7 +79,9 @@ public extension PureXML.Canonical {
|
|||||||
let text = options.trimTextNodes ? value.trimmingXMLWhitespace() : value
|
let text = options.trimTextNodes ? value.trimmingXMLWhitespace() : value
|
||||||
output += Self.escapeText(text)
|
output += Self.escapeText(text)
|
||||||
case let .comment(value):
|
case let .comment(value):
|
||||||
if options.includeComments { output += "<!--\(value)-->" }
|
if options.includeComments {
|
||||||
|
output += "<!--\(value)-->"
|
||||||
|
}
|
||||||
case let .processingInstruction(target, data):
|
case let .processingInstruction(target, data):
|
||||||
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(data)?>"
|
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(data)?>"
|
||||||
}
|
}
|
||||||
@ -149,7 +153,9 @@ public extension PureXML.Canonical {
|
|||||||
let text = options.trimTextNodes ? value.trimmingXMLWhitespace() : value
|
let text = options.trimTextNodes ? value.trimmingXMLWhitespace() : value
|
||||||
output += Self.escapeText(text)
|
output += Self.escapeText(text)
|
||||||
case let .comment(value):
|
case let .comment(value):
|
||||||
if options.includeComments { output += "<!--\(value)-->" }
|
if options.includeComments {
|
||||||
|
output += "<!--\(value)-->"
|
||||||
|
}
|
||||||
case let .processingInstruction(target, data):
|
case let .processingInstruction(target, data):
|
||||||
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(data)?>"
|
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(data)?>"
|
||||||
case .element, .document:
|
case .element, .document:
|
||||||
@ -198,7 +204,9 @@ public extension PureXML.Canonical {
|
|||||||
for prefix in utilized {
|
for prefix in utilized {
|
||||||
let uri = inScope[prefix] ?? ""
|
let uri = inScope[prefix] ?? ""
|
||||||
if prefix.isEmpty {
|
if prefix.isEmpty {
|
||||||
if uri != (rendered[""] ?? "") { result.append(("", uri)) }
|
if uri != (rendered[""] ?? "") {
|
||||||
|
result.append(("", uri))
|
||||||
|
}
|
||||||
} else if !uri.isEmpty, rendered[prefix] != uri {
|
} else if !uri.isEmpty, rendered[prefix] != uri {
|
||||||
result.append((prefix, uri))
|
result.append((prefix, uri))
|
||||||
}
|
}
|
||||||
@ -215,15 +223,21 @@ public extension PureXML.Canonical {
|
|||||||
private static func attributeOrder(_ lhs: PureXML.Model.Attribute, _ rhs: PureXML.Model.Attribute) -> Bool {
|
private static func attributeOrder(_ lhs: PureXML.Model.Attribute, _ rhs: PureXML.Model.Attribute) -> Bool {
|
||||||
let leftURI = lhs.name.namespaceURI ?? ""
|
let leftURI = lhs.name.namespaceURI ?? ""
|
||||||
let rightURI = rhs.name.namespaceURI ?? ""
|
let rightURI = rhs.name.namespaceURI ?? ""
|
||||||
if leftURI != rightURI { return leftURI < rightURI }
|
if leftURI != rightURI {
|
||||||
|
return leftURI < rightURI
|
||||||
|
}
|
||||||
return lhs.name.localName < rhs.name.localName
|
return lhs.name.localName < rhs.name.localName
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func namespaceDeclarations(_ element: PureXML.Model.Element) -> [(String, String)] {
|
private static func namespaceDeclarations(_ element: PureXML.Model.Element) -> [(String, String)] {
|
||||||
element.attributes.compactMap { attribute in
|
element.attributes.compactMap { attribute in
|
||||||
let name = attribute.name
|
let name = attribute.name
|
||||||
if name.prefix == nil, name.localName == "xmlns" { return ("", attribute.value) }
|
if name.prefix == nil, name.localName == "xmlns" {
|
||||||
if name.prefix == "xmlns" { return (name.localName, attribute.value) }
|
return ("", attribute.value)
|
||||||
|
}
|
||||||
|
if name.prefix == "xmlns" {
|
||||||
|
return (name.localName, attribute.value)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -327,7 +341,9 @@ public extension PureXML.Canonical.Canonicalizer {
|
|||||||
case .text, .cdata:
|
case .text, .cdata:
|
||||||
output += Self.escapeText(options.trimTextNodes ? node.value.trimmingXMLWhitespace() : node.value)
|
output += Self.escapeText(options.trimTextNodes ? node.value.trimmingXMLWhitespace() : node.value)
|
||||||
case .comment:
|
case .comment:
|
||||||
if options.includeComments { output += "<!--\(node.value)-->" }
|
if options.includeComments {
|
||||||
|
output += "<!--\(node.value)-->"
|
||||||
|
}
|
||||||
case .processingInstruction:
|
case .processingInstruction:
|
||||||
output += node.value.isEmpty ? "<?\(node.name?.description ?? "")?>" : "<?\(node.name?.description ?? "") \(node.value)?>"
|
output += node.value.isEmpty ? "<?\(node.name?.description ?? "")?>" : "<?\(node.name?.description ?? "") \(node.value)?>"
|
||||||
case .element, .document, .doctype, .entityReference, .namespace:
|
case .element, .document, .doctype, .entityReference, .namespace:
|
||||||
|
|||||||
@ -27,7 +27,9 @@ private struct CatalogBuilder {
|
|||||||
insert(node, key: "systemId", base: base, into: &systemMap)
|
insert(node, key: "systemId", base: base, into: &systemMap)
|
||||||
case "public":
|
case "public":
|
||||||
insert(node, key: "publicId", base: base, into: &publicMap)
|
insert(node, key: "publicId", base: base, into: &publicMap)
|
||||||
if let id = attribute(node, "publicId") { publicPrefer[id] = prefer }
|
if let id = attribute(node, "publicId") {
|
||||||
|
publicPrefer[id] = prefer
|
||||||
|
}
|
||||||
case "uri":
|
case "uri":
|
||||||
insert(node, key: "name", base: base, into: &uriMap)
|
insert(node, key: "name", base: base, into: &uriMap)
|
||||||
case "rewriteSystem":
|
case "rewriteSystem":
|
||||||
@ -62,7 +64,9 @@ private struct CatalogBuilder {
|
|||||||
case "delegateURI":
|
case "delegateURI":
|
||||||
appendDelegate(node, start: "uriStartString", base: base, into: &delegateURI)
|
appendDelegate(node, start: "uriStartString", base: base, into: &delegateURI)
|
||||||
case "nextCatalog":
|
case "nextCatalog":
|
||||||
if let catalog = attribute(node, "catalog") { nextCatalogs.append(resolve(catalog, base)) }
|
if let catalog = attribute(node, "catalog") {
|
||||||
|
nextCatalogs.append(resolve(catalog, base))
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,16 +163,22 @@ public extension PureXML.Catalog {
|
|||||||
_ loadingCatalog: (String) -> String?,
|
_ loadingCatalog: (String) -> String?,
|
||||||
_ visited: inout Set<String>,
|
_ visited: inout Set<String>,
|
||||||
) -> String? {
|
) -> String? {
|
||||||
if let local = pick(self) { return local }
|
if let local = pick(self) {
|
||||||
|
return local
|
||||||
|
}
|
||||||
let matching = delegates
|
let matching = delegates
|
||||||
.filter { identifier.hasPrefix($0.startString) }
|
.filter { identifier.hasPrefix($0.startString) }
|
||||||
.sorted { $0.startString.count > $1.startString.count }
|
.sorted { $0.startString.count > $1.startString.count }
|
||||||
for rule in matching {
|
for rule in matching {
|
||||||
if let sub = load(rule.catalog, loadingCatalog, &visited), let resolved = pick(sub) { return resolved }
|
if let sub = load(rule.catalog, loadingCatalog, &visited), let resolved = pick(sub) {
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for next in nextCatalogs {
|
for next in nextCatalogs {
|
||||||
guard let sub = load(next, loadingCatalog, &visited) else { continue }
|
guard let sub = load(next, loadingCatalog, &visited) else { continue }
|
||||||
if let resolved = sub.chained(identifier, delegates: [], pick: pick, loadingCatalog, &visited) { return resolved }
|
if let resolved = sub.chained(identifier, delegates: [], pick: pick, loadingCatalog, &visited) {
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,16 +27,22 @@ private struct SGMLBuilder {
|
|||||||
|
|
||||||
mutating func apply(_ keyword: String, _ scanner: inout SGMLScanner) {
|
mutating func apply(_ keyword: String, _ scanner: inout SGMLScanner) {
|
||||||
let upper = keyword.uppercased()
|
let upper = keyword.uppercased()
|
||||||
if applyPair(upper, &scanner) { return }
|
if applyPair(upper, &scanner) {
|
||||||
|
return
|
||||||
|
}
|
||||||
applySingle(upper, &scanner)
|
applySingle(upper, &scanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func applyPair(_ keyword: String, _ scanner: inout SGMLScanner) -> Bool {
|
private mutating func applyPair(_ keyword: String, _ scanner: inout SGMLScanner) -> Bool {
|
||||||
switch keyword {
|
switch keyword {
|
||||||
case "PUBLIC":
|
case "PUBLIC":
|
||||||
if let id = scanner.next(), let uri = scanner.next() { publicMap[id] = resolve(uri) }
|
if let id = scanner.next(), let uri = scanner.next() {
|
||||||
|
publicMap[id] = resolve(uri)
|
||||||
|
}
|
||||||
case "SYSTEM":
|
case "SYSTEM":
|
||||||
if let id = scanner.next(), let uri = scanner.next() { systemMap[id] = resolve(uri) }
|
if let id = scanner.next(), let uri = scanner.next() {
|
||||||
|
systemMap[id] = resolve(uri)
|
||||||
|
}
|
||||||
case "DELEGATE":
|
case "DELEGATE":
|
||||||
if let prefix = scanner.next(), let catalog = scanner.next() {
|
if let prefix = scanner.next(), let catalog = scanner.next() {
|
||||||
delegatePublic.append(PureXML.Catalog.DelegateRule(startString: prefix, catalog: resolve(catalog)))
|
delegatePublic.append(PureXML.Catalog.DelegateRule(startString: prefix, catalog: resolve(catalog)))
|
||||||
@ -50,11 +56,17 @@ private struct SGMLBuilder {
|
|||||||
private mutating func applySingle(_ keyword: String, _ scanner: inout SGMLScanner) {
|
private mutating func applySingle(_ keyword: String, _ scanner: inout SGMLScanner) {
|
||||||
switch keyword {
|
switch keyword {
|
||||||
case "CATALOG":
|
case "CATALOG":
|
||||||
if let catalog = scanner.next() { nextCatalogs.append(resolve(catalog)) }
|
if let catalog = scanner.next() {
|
||||||
|
nextCatalogs.append(resolve(catalog))
|
||||||
|
}
|
||||||
case "BASE":
|
case "BASE":
|
||||||
if let declared = scanner.next() { base = resolve(declared) }
|
if let declared = scanner.next() {
|
||||||
|
base = resolve(declared)
|
||||||
|
}
|
||||||
case "OVERRIDE":
|
case "OVERRIDE":
|
||||||
if let value = scanner.next() { preferPublic = value.uppercased() == "YES" }
|
if let value = scanner.next() {
|
||||||
|
preferPublic = value.uppercased() == "YES"
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,12 +20,16 @@ extension PureXML.Emitting {
|
|||||||
while let marker = value.utf8[runStart...].firstIndex(where: {
|
while let marker = value.utf8[runStart...].firstIndex(where: {
|
||||||
textEscapableByte($0, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn)
|
textEscapableByte($0, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn)
|
||||||
}) {
|
}) {
|
||||||
if runStart < marker { result += value[runStart ..< marker] }
|
if runStart < marker {
|
||||||
|
result += value[runStart ..< marker]
|
||||||
|
}
|
||||||
let character = value[marker]
|
let character = value[marker]
|
||||||
result += textEscape(character, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn) ?? String(character)
|
result += textEscape(character, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn) ?? String(character)
|
||||||
runStart = value.index(after: marker)
|
runStart = value.index(after: marker)
|
||||||
}
|
}
|
||||||
if runStart < value.endIndex { result += value[runStart...] }
|
if runStart < value.endIndex {
|
||||||
|
result += value[runStart...]
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,12 +102,16 @@ extension PureXML.Emitting {
|
|||||||
while let marker = value.utf8[runStart...].firstIndex(where: {
|
while let marker = value.utf8[runStart...].firstIndex(where: {
|
||||||
attributeEscapableByte($0, quoteByte: quoteByte, asciiOnly: asciiOnly)
|
attributeEscapableByte($0, quoteByte: quoteByte, asciiOnly: asciiOnly)
|
||||||
}) {
|
}) {
|
||||||
if runStart < marker { result += value[runStart ..< marker] }
|
if runStart < marker {
|
||||||
|
result += value[runStart ..< marker]
|
||||||
|
}
|
||||||
let character = value[marker]
|
let character = value[marker]
|
||||||
result += attributeEscape(character, quote: quote, asciiOnly: asciiOnly) ?? String(character)
|
result += attributeEscape(character, quote: quote, asciiOnly: asciiOnly) ?? String(character)
|
||||||
runStart = value.index(after: marker)
|
runStart = value.index(after: marker)
|
||||||
}
|
}
|
||||||
if runStart < value.endIndex { result += value[runStart...] }
|
if runStart < value.endIndex {
|
||||||
|
result += value[runStart...]
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,15 +163,21 @@ extension PureXML.Emitting {
|
|||||||
static func comment(_ value: String) -> String {
|
static func comment(_ value: String) -> String {
|
||||||
// Fast path: a comment with no hyphen has no `--` and no trailing `-`,
|
// Fast path: a comment with no hyphen has no `--` and no trailing `-`,
|
||||||
// so it is returned unchanged rather than rebuilt character by character.
|
// so it is returned unchanged rather than rebuilt character by character.
|
||||||
if !value.utf8.contains(0x2D) { return value }
|
if !value.utf8.contains(0x2D) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
var result = ""
|
var result = ""
|
||||||
var previousHyphen = false
|
var previousHyphen = false
|
||||||
for character in value {
|
for character in value {
|
||||||
if character == "-", previousHyphen { result.append(" ") }
|
if character == "-", previousHyphen {
|
||||||
|
result.append(" ")
|
||||||
|
}
|
||||||
result.append(character)
|
result.append(character)
|
||||||
previousHyphen = character == "-"
|
previousHyphen = character == "-"
|
||||||
}
|
}
|
||||||
if previousHyphen { result.append(" ") }
|
if previousHyphen {
|
||||||
|
result.append(" ")
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,11 +187,15 @@ extension PureXML.Emitting {
|
|||||||
static func processingInstruction(_ data: String) -> String {
|
static func processingInstruction(_ data: String) -> String {
|
||||||
// Fast path: data with no '?' cannot contain `?>`, so it is returned
|
// Fast path: data with no '?' cannot contain `?>`, so it is returned
|
||||||
// unchanged rather than rebuilt character by character.
|
// unchanged rather than rebuilt character by character.
|
||||||
if !data.utf8.contains(0x3F) { return data }
|
if !data.utf8.contains(0x3F) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
var result = ""
|
var result = ""
|
||||||
var previousQuestion = false
|
var previousQuestion = false
|
||||||
for character in data {
|
for character in data {
|
||||||
if character == ">", previousQuestion { result.append(" ") }
|
if character == ">", previousQuestion {
|
||||||
|
result.append(" ")
|
||||||
|
}
|
||||||
result.append(character)
|
result.append(character)
|
||||||
previousQuestion = character == "?"
|
previousQuestion = character == "?"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,8 +37,12 @@ extension HTMLDocument {
|
|||||||
/// scope boundary.
|
/// scope boundary.
|
||||||
private func hasInScope(_ target: PureXML.Model.TreeNode) -> Bool {
|
private func hasInScope(_ target: PureXML.Model.TreeNode) -> Bool {
|
||||||
for node in openBody.reversed() {
|
for node in openBody.reversed() {
|
||||||
if node === target { return true }
|
if node === target {
|
||||||
if Self.scopeBoundaries.contains(tagName(node)) { return false }
|
return true
|
||||||
|
}
|
||||||
|
if Self.scopeBoundaries.contains(tagName(node)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -48,7 +52,9 @@ extension HTMLDocument {
|
|||||||
/// re-wrapped (the `<b><i></b>X</i>` -> `<b><i></i></b><i>X</i>` behavior).
|
/// re-wrapped (the `<b><i></b>X</i>` -> `<b><i></i></b><i>X</i>` behavior).
|
||||||
func reconstructActiveFormatting() {
|
func reconstructActiveFormatting() {
|
||||||
guard var index = activeFormatting.indices.last else { return }
|
guard var index = activeFormatting.indices.last else { return }
|
||||||
if activeFormatting[index] == nil || isOpen(activeFormatting[index]) { return }
|
if activeFormatting[index] == nil || isOpen(activeFormatting[index]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
while index > 0 {
|
while index > 0 {
|
||||||
index -= 1
|
index -= 1
|
||||||
if activeFormatting[index] == nil || isOpen(activeFormatting[index]) {
|
if activeFormatting[index] == nil || isOpen(activeFormatting[index]) {
|
||||||
@ -91,7 +97,9 @@ extension HTMLDocument {
|
|||||||
activeFormatting.remove(at: formattingIndexInList)
|
activeFormatting.remove(at: formattingIndexInList)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !hasInScope(formattingElement) { return }
|
if !hasInScope(formattingElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
guard let furthestBlockIndex = openBody[(formattingOpenIndex + 1)...].firstIndex(where: { Self.special.contains(tagName($0)) }) else {
|
guard let furthestBlockIndex = openBody[(formattingOpenIndex + 1)...].firstIndex(where: { Self.special.contains(tagName($0)) }) else {
|
||||||
openBody.removeLast(openBody.count - formattingOpenIndex)
|
openBody.removeLast(openBody.count - formattingOpenIndex)
|
||||||
activeFormatting.remove(at: formattingIndexInList)
|
activeFormatting.remove(at: formattingIndexInList)
|
||||||
@ -104,7 +112,9 @@ extension HTMLDocument {
|
|||||||
private func lastFormatting(tag: String) -> Int? {
|
private func lastFormatting(tag: String) -> Int? {
|
||||||
for index in activeFormatting.indices.reversed() {
|
for index in activeFormatting.indices.reversed() {
|
||||||
guard let entry = activeFormatting[index] else { return nil } // a marker bounds the search
|
guard let entry = activeFormatting[index] else { return nil } // a marker bounds the search
|
||||||
if tagName(entry) == tag { return index }
|
if tagName(entry) == tag {
|
||||||
|
return index
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -124,7 +134,9 @@ extension HTMLDocument {
|
|||||||
inner += 1
|
inner += 1
|
||||||
nodeIndex -= 1
|
nodeIndex -= 1
|
||||||
let node = openBody[nodeIndex]
|
let node = openBody[nodeIndex]
|
||||||
if node === formattingElement { break }
|
if node === formattingElement {
|
||||||
|
break
|
||||||
|
}
|
||||||
guard let activeIndex = formattingIndex(node) else {
|
guard let activeIndex = formattingIndex(node) else {
|
||||||
openBody.remove(at: nodeIndex)
|
openBody.remove(at: nodeIndex)
|
||||||
continue
|
continue
|
||||||
@ -132,7 +144,9 @@ extension HTMLDocument {
|
|||||||
let clone = node.shallowCopy()
|
let clone = node.shallowCopy()
|
||||||
activeFormatting[activeIndex] = clone
|
activeFormatting[activeIndex] = clone
|
||||||
openBody[nodeIndex] = clone
|
openBody[nodeIndex] = clone
|
||||||
if lastNode === furthestBlock { bookmark = activeIndex + 1 }
|
if lastNode === furthestBlock {
|
||||||
|
bookmark = activeIndex + 1
|
||||||
|
}
|
||||||
clone.append(lastNode)
|
clone.append(lastNode)
|
||||||
lastNode = clone
|
lastNode = clone
|
||||||
}
|
}
|
||||||
@ -144,13 +158,19 @@ extension HTMLDocument {
|
|||||||
furthestBlock.append(formattingClone)
|
furthestBlock.append(formattingClone)
|
||||||
if let removeIndex = formattingIndex(formattingElement) {
|
if let removeIndex = formattingIndex(formattingElement) {
|
||||||
activeFormatting.remove(at: removeIndex)
|
activeFormatting.remove(at: removeIndex)
|
||||||
if removeIndex < bookmark { bookmark -= 1 }
|
if removeIndex < bookmark {
|
||||||
|
bookmark -= 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
activeFormatting.insert(formattingClone, at: min(bookmark, activeFormatting.count))
|
activeFormatting.insert(formattingClone, at: min(bookmark, activeFormatting.count))
|
||||||
if let removeOpen = openIndex(formattingElement) { openBody.remove(at: removeOpen) }
|
if let removeOpen = openIndex(formattingElement) {
|
||||||
|
openBody.remove(at: removeOpen)
|
||||||
|
}
|
||||||
// The clone becomes a child of the furthest block, so it sits immediately
|
// The clone becomes a child of the furthest block, so it sits immediately
|
||||||
// above it on the open-elements stack; inserting below would re-find the
|
// above it on the open-elements stack; inserting below would re-find the
|
||||||
// same furthest block and loop.
|
// same furthest block and loop.
|
||||||
if let blockIndex = openIndex(furthestBlock) { openBody.insert(formattingClone, at: blockIndex + 1) }
|
if let blockIndex = openIndex(furthestBlock) {
|
||||||
|
openBody.insert(formattingClone, at: blockIndex + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,8 +84,12 @@ final class HTMLDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func processInitial(_ token: Token) {
|
private func processInitial(_ token: Token) {
|
||||||
if case .doctype = token { return }
|
if case .doctype = token {
|
||||||
if isIgnorableWhitespace(token) { return }
|
return
|
||||||
|
}
|
||||||
|
if isIgnorableWhitespace(token) {
|
||||||
|
return
|
||||||
|
}
|
||||||
mode = .beforeHtml
|
mode = .beforeHtml
|
||||||
process(token)
|
process(token)
|
||||||
}
|
}
|
||||||
@ -96,13 +100,17 @@ final class HTMLDocument {
|
|||||||
mode = .beforeHead
|
mode = .beforeHead
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isIgnorableWhitespace(token) { return }
|
if isIgnorableWhitespace(token) {
|
||||||
|
return
|
||||||
|
}
|
||||||
mode = .beforeHead
|
mode = .beforeHead
|
||||||
process(token)
|
process(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processBeforeHead(_ token: Token) {
|
private func processBeforeHead(_ token: Token) {
|
||||||
if isIgnorableWhitespace(token) { return }
|
if isIgnorableWhitespace(token) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if case let .startTag(name, attributes, _) = token, name == "head" {
|
if case let .startTag(name, attributes, _) = token, name == "head" {
|
||||||
headAttributes = modelAttributes(attributes)
|
headAttributes = modelAttributes(attributes)
|
||||||
mode = .inHead
|
mode = .inHead
|
||||||
@ -140,7 +148,9 @@ final class HTMLDocument {
|
|||||||
}
|
}
|
||||||
let element = PureXML.Model.TreeNode.element(name, attributes: modelAttributes(attributes))
|
let element = PureXML.Model.TreeNode.element(name, attributes: modelAttributes(attributes))
|
||||||
openHead.last?.append(element)
|
openHead.last?.append(element)
|
||||||
if !(PureXML.HTML.Elements.void.contains(name) || selfClosing) { openHead.append(element) }
|
if !(PureXML.HTML.Elements.void.contains(name) || selfClosing) {
|
||||||
|
openHead.append(element)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func headClose(_ name: String) {
|
private func headClose(_ name: String) {
|
||||||
@ -168,7 +178,9 @@ final class HTMLDocument {
|
|||||||
process(token)
|
process(token)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isIgnorableWhitespace(token) { return }
|
if isIgnorableWhitespace(token) {
|
||||||
|
return
|
||||||
|
}
|
||||||
mode = .inBody
|
mode = .inBody
|
||||||
process(token)
|
process(token)
|
||||||
}
|
}
|
||||||
@ -210,7 +222,11 @@ final class HTMLDocument {
|
|||||||
case let .startTag(name, attributes, _) where name == "noframes":
|
case let .startTag(name, attributes, _) where name == "noframes":
|
||||||
framesetStack.last?.append(PureXML.Model.TreeNode.element("noframes", attributes: modelAttributes(attributes)))
|
framesetStack.last?.append(PureXML.Model.TreeNode.element("noframes", attributes: modelAttributes(attributes)))
|
||||||
case let .endTag(name) where name == "frameset":
|
case let .endTag(name) where name == "frameset":
|
||||||
if framesetStack.count > 1 { framesetStack.removeLast() } else { mode = .afterFrameset }
|
if framesetStack.count > 1 {
|
||||||
|
framesetStack.removeLast()
|
||||||
|
} else {
|
||||||
|
mode = .afterFrameset
|
||||||
|
}
|
||||||
case let .comment(value):
|
case let .comment(value):
|
||||||
framesetStack.last?.append(.comment(value))
|
framesetStack.last?.append(.comment(value))
|
||||||
default:
|
default:
|
||||||
@ -243,7 +259,9 @@ final class HTMLDocument {
|
|||||||
private func bodyOpen(_ name: String, _ attributes: [(String, String)], selfClosing: Bool) {
|
private func bodyOpen(_ name: String, _ attributes: [(String, String)], selfClosing: Bool) {
|
||||||
if Self.closesSelect.contains(name), let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1 {
|
if Self.closesSelect.contains(name), let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1 {
|
||||||
openBody.removeLast(openBody.count - selectIndex)
|
openBody.removeLast(openBody.count - selectIndex)
|
||||||
if name == "select" { return } // a nested select just closes the open one
|
if name == "select" {
|
||||||
|
return
|
||||||
|
} // a nested select just closes the open one
|
||||||
}
|
}
|
||||||
if let selectIndex = selectInTableToClose(for: name) {
|
if let selectIndex = selectInTableToClose(for: name) {
|
||||||
openBody.removeLast(openBody.count - selectIndex)
|
openBody.removeLast(openBody.count - selectIndex)
|
||||||
@ -278,8 +296,12 @@ final class HTMLDocument {
|
|||||||
case "tr" where context == "table":
|
case "tr" where context == "table":
|
||||||
bodyPushImplied("tbody")
|
bodyPushImplied("tbody")
|
||||||
case "td", "th":
|
case "td", "th":
|
||||||
if context == "table" { bodyPushImplied("tbody") }
|
if context == "table" {
|
||||||
if context == "table" || ["tbody", "thead", "tfoot"].contains(context) { bodyPushImplied("tr") }
|
bodyPushImplied("tbody")
|
||||||
|
}
|
||||||
|
if context == "table" || ["tbody", "thead", "tfoot"].contains(context) {
|
||||||
|
bodyPushImplied("tr")
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -303,11 +325,17 @@ final class HTMLDocument {
|
|||||||
/// The foreign-content namespace for a body element: SVG/MathML on entry, the
|
/// The foreign-content namespace for a body element: SVG/MathML on entry, the
|
||||||
/// nearest open foreign ancestor's, or HTML (nil) inside an integration point.
|
/// nearest open foreign ancestor's, or HTML (nil) inside an integration point.
|
||||||
private func bodyForeignNamespace(for name: String) -> String? {
|
private func bodyForeignNamespace(for name: String) -> String? {
|
||||||
if name == "svg" { return ForeignNamespace.svg }
|
if name == "svg" {
|
||||||
if name == "math" { return ForeignNamespace.mathml }
|
return ForeignNamespace.svg
|
||||||
|
}
|
||||||
|
if name == "math" {
|
||||||
|
return ForeignNamespace.mathml
|
||||||
|
}
|
||||||
for node in openBody.reversed() {
|
for node in openBody.reversed() {
|
||||||
guard let namespace = node.name?.namespaceURI else { continue }
|
guard let namespace = node.name?.namespaceURI else { continue }
|
||||||
if Self.svgIntegrationPoints.contains(tagName(node)) { return nil }
|
if Self.svgIntegrationPoints.contains(tagName(node)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return namespace
|
return namespace
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -330,7 +358,9 @@ extension HTMLDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func isIgnorableWhitespace(_ token: Token) -> Bool {
|
private func isIgnorableWhitespace(_ token: Token) -> Bool {
|
||||||
if case let .text(value) = token { return value.trimmingXMLWhitespace().isEmpty }
|
if case let .text(value) = token {
|
||||||
|
return value.trimmingXMLWhitespace().isEmpty
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,13 +59,17 @@ extension PureXML.HTML.Tokenizer {
|
|||||||
private static func decodeNumeric(_ characters: [Character], at start: Int) -> (String, Int)? {
|
private static func decodeNumeric(_ characters: [Character], at start: Int) -> (String, Int)? {
|
||||||
var cursor = start + 2 // past "&#"
|
var cursor = start + 2 // past "&#"
|
||||||
let isHex = cursor < characters.count && (characters[cursor] == "x" || characters[cursor] == "X")
|
let isHex = cursor < characters.count && (characters[cursor] == "x" || characters[cursor] == "X")
|
||||||
if isHex { cursor += 1 }
|
if isHex {
|
||||||
|
cursor += 1
|
||||||
|
}
|
||||||
let digitsStart = cursor
|
let digitsStart = cursor
|
||||||
while cursor < characters.count, isDigit(characters[cursor], hex: isHex) {
|
while cursor < characters.count, isDigit(characters[cursor], hex: isHex) {
|
||||||
cursor += 1
|
cursor += 1
|
||||||
}
|
}
|
||||||
guard cursor > digitsStart, let number = UInt32(String(characters[digitsStart ..< cursor]), radix: isHex ? 16 : 10) else { return nil }
|
guard cursor > digitsStart, let number = UInt32(String(characters[digitsStart ..< cursor]), radix: isHex ? 16 : 10) else { return nil }
|
||||||
if cursor < characters.count, characters[cursor] == ";" { cursor += 1 }
|
if cursor < characters.count, characters[cursor] == ";" {
|
||||||
|
cursor += 1
|
||||||
|
}
|
||||||
return (String(numericCharacter(number)), cursor - start)
|
return (String(numericCharacter(number)), cursor - start)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,9 @@ extension PureXML.HTML {
|
|||||||
output += ">"
|
output += ">"
|
||||||
// A void element has no end tag and no content (the html model);
|
// A void element has no end tag and no content (the html model);
|
||||||
// anything nested is dropped, as the recursive form did.
|
// anything nested is dropped, as the recursive form did.
|
||||||
if Elements.void.contains(name.lowercased()) { return }
|
if Elements.void.contains(name.lowercased()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
stack.append(HTMLSerializeStep.close(name))
|
stack.append(HTMLSerializeStep.close(name))
|
||||||
// A raw-text element (script, style) writes its own text children
|
// A raw-text element (script, style) writes its own text children
|
||||||
// verbatim; nested elements still serialize normally by their own
|
// verbatim; nested elements still serialize normally by their own
|
||||||
@ -57,7 +59,9 @@ extension PureXML.HTML {
|
|||||||
|
|
||||||
private static func attributeText(_ attribute: PureXML.Model.Attribute) -> String {
|
private static func attributeText(_ attribute: PureXML.Model.Attribute) -> String {
|
||||||
let name = attribute.name.description
|
let name = attribute.name.description
|
||||||
if attribute.value.isEmpty { return " \(name)" }
|
if attribute.value.isEmpty {
|
||||||
|
return " \(name)"
|
||||||
|
}
|
||||||
// Boolean attributes minimize when the value repeats the name
|
// Boolean attributes minimize when the value repeats the name
|
||||||
// (CHECKED="CHECKED" serializes as CHECKED), the html form.
|
// (CHECKED="CHECKED" serializes as CHECKED), the html form.
|
||||||
if Elements.booleanAttributes.contains(name.lowercased()), attribute.value.lowercased() == name.lowercased() {
|
if Elements.booleanAttributes.contains(name.lowercased()), attribute.value.lowercased() == name.lowercased() {
|
||||||
@ -132,7 +136,9 @@ extension PureXML.HTML {
|
|||||||
default: result += latin1Entity(scalar) ?? String(Character(scalar))
|
default: result += latin1Entity(scalar) ?? String(Character(scalar))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if previousWasAmpersand { result += "&" }
|
if previousWasAmpersand {
|
||||||
|
result += "&"
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,10 +27,18 @@ extension PureXML.HTML {
|
|||||||
return rawText(until: raw)
|
return rawText(until: raw)
|
||||||
}
|
}
|
||||||
guard peek() == "<" else { return text() }
|
guard peek() == "<" else { return text() }
|
||||||
if matches("<!--") { return comment() }
|
if matches("<!--") {
|
||||||
if matches("<!") { return doctype() }
|
return comment()
|
||||||
if matches("</") { return endTag() }
|
}
|
||||||
if peek(1)?.isLetter == true { return startTag() }
|
if matches("<!") {
|
||||||
|
return doctype()
|
||||||
|
}
|
||||||
|
if matches("</") {
|
||||||
|
return endTag()
|
||||||
|
}
|
||||||
|
if peek(1)?.isLetter == true {
|
||||||
|
return startTag()
|
||||||
|
}
|
||||||
return text()
|
return text()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +46,9 @@ extension PureXML.HTML {
|
|||||||
|
|
||||||
private mutating func text() -> Token {
|
private mutating func text() -> Token {
|
||||||
var value = ""
|
var value = ""
|
||||||
if let first = advance() { value.append(first) }
|
if let first = advance() {
|
||||||
|
value.append(first)
|
||||||
|
}
|
||||||
while let character = peek(), character != "<" {
|
while let character = peek(), character != "<" {
|
||||||
value.append(character)
|
value.append(character)
|
||||||
advance()
|
advance()
|
||||||
@ -53,16 +63,21 @@ extension PureXML.HTML {
|
|||||||
var selfClosing = false
|
var selfClosing = false
|
||||||
while !isAtEnd {
|
while !isAtEnd {
|
||||||
skipSpace()
|
skipSpace()
|
||||||
if consume("/>") { selfClosing = true
|
if consume("/>") {
|
||||||
|
selfClosing = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if consume(">") {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if consume(">") { break }
|
|
||||||
guard let attribute = readAttribute() else { advance()
|
guard let attribute = readAttribute() else { advance()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
attributes.append(attribute)
|
attributes.append(attribute)
|
||||||
}
|
}
|
||||||
if Elements.rawText.contains(name) { rawTextName = name }
|
if Elements.rawText.contains(name) {
|
||||||
|
rawTextName = name
|
||||||
|
}
|
||||||
return .startTag(name: name, attributes: attributes, selfClosing: selfClosing)
|
return .startTag(name: name, attributes: attributes, selfClosing: selfClosing)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,9 +148,13 @@ extension PureXML.HTML {
|
|||||||
}
|
}
|
||||||
value = Self.replacingNull(value)
|
value = Self.replacingNull(value)
|
||||||
// RCDATA decodes character references; raw text (script/style) does not.
|
// RCDATA decodes character references; raw text (script/style) does not.
|
||||||
if Elements.rcdata.contains(name) { value = Self.decodeEntities(value) }
|
if Elements.rcdata.contains(name) {
|
||||||
|
value = Self.decodeEntities(value)
|
||||||
|
}
|
||||||
// A single leading newline after <textarea> is stripped (the HTML rule).
|
// A single leading newline after <textarea> is stripped (the HTML rule).
|
||||||
if name == "textarea", value.first == "\n" { value.removeFirst() }
|
if name == "textarea", value.first == "\n" {
|
||||||
|
value.removeFirst()
|
||||||
|
}
|
||||||
return .text(value)
|
return .text(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,9 @@ extension PureXML.Model.Node: Equatable, Hashable {
|
|||||||
stack.append(pair)
|
stack.append(pair)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if !leavesEqual(left, right) { return false }
|
if !leavesEqual(left, right) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -194,8 +194,12 @@ public extension PureXML.Model {
|
|||||||
internalSubset: String = "",
|
internalSubset: String = "",
|
||||||
) -> TreeNode {
|
) -> TreeNode {
|
||||||
var attributes: [PureXML.Model.Attribute] = []
|
var attributes: [PureXML.Model.Attribute] = []
|
||||||
if let publicID { attributes.append(PureXML.Model.Attribute("public", publicID)) }
|
if let publicID {
|
||||||
if let systemID { attributes.append(PureXML.Model.Attribute("system", systemID)) }
|
attributes.append(PureXML.Model.Attribute("public", publicID))
|
||||||
|
}
|
||||||
|
if let systemID {
|
||||||
|
attributes.append(PureXML.Model.Attribute("system", systemID))
|
||||||
|
}
|
||||||
return TreeNode(kind: .doctype, name: PureXML.Model.QualifiedName(name), attributes: attributes, value: internalSubset)
|
return TreeNode(kind: .doctype, name: PureXML.Model.QualifiedName(name), attributes: attributes, value: internalSubset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -189,7 +189,9 @@ public extension PureXML.Model.TreeNode {
|
|||||||
/// rebuilding each subtree, so a result set of nested nodes is linear rather
|
/// rebuilding each subtree, so a result set of nested nodes is linear rather
|
||||||
/// than quadratic. The projection is identical to ``node``.
|
/// than quadratic. The projection is identical to ``node``.
|
||||||
func node(memo: inout [ObjectIdentifier: PureXML.Model.Node]) -> PureXML.Model.Node {
|
func node(memo: inout [ObjectIdentifier: PureXML.Model.Node]) -> PureXML.Model.Node {
|
||||||
if let cached = memo[ObjectIdentifier(self)] { return cached }
|
if let cached = memo[ObjectIdentifier(self)] {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
let result: PureXML.Model.Node = switch kind {
|
let result: PureXML.Model.Node = switch kind {
|
||||||
case .document:
|
case .document:
|
||||||
.document(Self.projectedChildren(of: self, memo: &memo))
|
.document(Self.projectedChildren(of: self, memo: &memo))
|
||||||
|
|||||||
@ -45,7 +45,9 @@ public extension PureXML.Model.TreeNode {
|
|||||||
/// comparable only when they share a root; an unrelated node returns false. An
|
/// comparable only when they share a root; an unrelated node returns false. An
|
||||||
/// ancestor precedes its descendants.
|
/// ancestor precedes its descendants.
|
||||||
func precedes(_ other: PureXML.Model.TreeNode) -> Bool {
|
func precedes(_ other: PureXML.Model.TreeNode) -> Bool {
|
||||||
if self === other { return false }
|
if self === other {
|
||||||
|
return false
|
||||||
|
}
|
||||||
let mine = Self.pathFromRoot(self)
|
let mine = Self.pathFromRoot(self)
|
||||||
let theirs = Self.pathFromRoot(other)
|
let theirs = Self.pathFromRoot(other)
|
||||||
guard mine.first === theirs.first else { return false }
|
guard mine.first === theirs.first else { return false }
|
||||||
@ -53,8 +55,12 @@ public extension PureXML.Model.TreeNode {
|
|||||||
while depth < mine.count, depth < theirs.count, mine[depth] === theirs[depth] {
|
while depth < mine.count, depth < theirs.count, mine[depth] === theirs[depth] {
|
||||||
depth += 1
|
depth += 1
|
||||||
}
|
}
|
||||||
if depth == mine.count { return true }
|
if depth == mine.count {
|
||||||
if depth == theirs.count { return false }
|
return true
|
||||||
|
}
|
||||||
|
if depth == theirs.count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
let parent = mine[depth - 1]
|
let parent = mine[depth - 1]
|
||||||
let mineIndex = parent.children.firstIndex { $0 === mine[depth] } ?? 0
|
let mineIndex = parent.children.firstIndex { $0 === mine[depth] } ?? 0
|
||||||
let theirsIndex = parent.children.firstIndex { $0 === theirs[depth] } ?? 0
|
let theirsIndex = parent.children.firstIndex { $0 === theirs[depth] } ?? 0
|
||||||
|
|||||||
@ -75,7 +75,9 @@ public extension PureXML.Model.TreeNode {
|
|||||||
private func isDescendant(of ancestor: PureXML.Model.TreeNode) -> Bool {
|
private func isDescendant(of ancestor: PureXML.Model.TreeNode) -> Bool {
|
||||||
var current: PureXML.Model.TreeNode? = self
|
var current: PureXML.Model.TreeNode? = self
|
||||||
while let node = current {
|
while let node = current {
|
||||||
if node === ancestor { return true }
|
if node === ancestor {
|
||||||
|
return true
|
||||||
|
}
|
||||||
current = node.parent
|
current = node.parent
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -9,7 +9,9 @@ public extension PureXML.Model.TreeNode {
|
|||||||
var current: PureXML.Model.TreeNode? = self
|
var current: PureXML.Model.TreeNode? = self
|
||||||
for key in path {
|
for key in path {
|
||||||
guard let node = current else { return nil }
|
guard let node = current else { return nil }
|
||||||
if key.stringValue.hasPrefix("@") { return node }
|
if key.stringValue.hasPrefix("@") {
|
||||||
|
return node
|
||||||
|
}
|
||||||
let matches = node.children.filter { $0.kind == .element && $0.name?.description == key.stringValue }
|
let matches = node.children.filter { $0.kind == .element && $0.name?.description == key.stringValue }
|
||||||
let index = (key.intValue ?? 1) - 1
|
let index = (key.intValue ?? 1) - 1
|
||||||
current = matches.indices.contains(index) ? matches[index] : nil
|
current = matches.indices.contains(index) ? matches[index] : nil
|
||||||
|
|||||||
@ -16,7 +16,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
scalars.append(contentsOf: sequence)
|
scalars.append(contentsOf: sequence)
|
||||||
} else {
|
} else {
|
||||||
scalars.append("\u{FFFD}")
|
scalars.append("\u{FFFD}")
|
||||||
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
|
if byte <= 0x7F {
|
||||||
|
scalars.append(Unicode.Scalar(byte))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if byte <= 0x7F {
|
} else if byte <= 0x7F {
|
||||||
scalars.append(Unicode.Scalar(byte))
|
scalars.append(Unicode.Scalar(byte))
|
||||||
@ -26,7 +28,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
scalars.append("\u{FFFD}")
|
scalars.append("\u{FFFD}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lead != 0 { scalars.append("\u{FFFD}") }
|
if lead != 0 {
|
||||||
|
scalars.append("\u{FFFD}")
|
||||||
|
}
|
||||||
return String(scalars)
|
return String(scalars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -166,7 +166,9 @@ extension PureXML.Parsing {
|
|||||||
/// BOM is still a contradiction, only utf-8 itself agrees; the generic
|
/// BOM is still a contradiction, only utf-8 itself agrees; the generic
|
||||||
/// utf-16/utf-32 names agree with either byte order.
|
/// utf-16/utf-32 names agree with either byte order.
|
||||||
private static func sameFamily(_ declared: InputEncoding, as sniffed: InputEncoding) -> Bool {
|
private static func sameFamily(_ declared: InputEncoding, as sniffed: InputEncoding) -> Bool {
|
||||||
if declared == sniffed { return true }
|
if declared == sniffed {
|
||||||
|
return true
|
||||||
|
}
|
||||||
switch (sniffed, declared) {
|
switch (sniffed, declared) {
|
||||||
case (.utf16BigEndian, .utf16LittleEndian), (.utf16LittleEndian, .utf16BigEndian),
|
case (.utf16BigEndian, .utf16LittleEndian), (.utf16LittleEndian, .utf16BigEndian),
|
||||||
(.utf32BigEndian, .utf32LittleEndian), (.utf32LittleEndian, .utf32BigEndian):
|
(.utf32BigEndian, .utf32LittleEndian), (.utf32LittleEndian, .utf32BigEndian):
|
||||||
@ -191,7 +193,9 @@ extension PureXML.Parsing {
|
|||||||
while index < body.endIndex, body.index(after: index) < body.endIndex {
|
while index < body.endIndex, body.index(after: index) < body.endIndex {
|
||||||
let high = sniffed == .utf16BigEndian ? body[index] : body[body.index(after: index)]
|
let high = sniffed == .utf16BigEndian ? body[index] : body[body.index(after: index)]
|
||||||
let low = sniffed == .utf16BigEndian ? body[body.index(after: index)] : body[index]
|
let low = sniffed == .utf16BigEndian ? body[body.index(after: index)] : body[index]
|
||||||
if high == 0 { ascii.append(low) }
|
if high == 0 {
|
||||||
|
ascii.append(low)
|
||||||
|
}
|
||||||
index = body.index(index, offsetBy: 2)
|
index = body.index(index, offsetBy: 2)
|
||||||
}
|
}
|
||||||
return declaredEncoding(in: ascii)
|
return declaredEncoding(in: ascii)
|
||||||
|
|||||||
@ -90,8 +90,12 @@ public extension PureXML.Parsing {
|
|||||||
var map: [UInt32: UInt8] = [:]
|
var map: [UInt32: UInt8] = [:]
|
||||||
for byte in UInt8.min ... UInt8.max {
|
for byte in UInt8.min ... UInt8.max {
|
||||||
let value = forward(byte).value
|
let value = forward(byte).value
|
||||||
if value == 0xFFFD { continue }
|
if value == 0xFFFD {
|
||||||
if map[value] == nil { map[value] = byte }
|
continue
|
||||||
|
}
|
||||||
|
if map[value] == nil {
|
||||||
|
map[value] = byte
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,7 +104,9 @@ extension PureXML.Parsing.ByteEncoder {
|
|||||||
func record(_ bytes: [UInt8]) {
|
func record(_ bytes: [UInt8]) {
|
||||||
let scalars = Array(decode(bytes[...]).unicodeScalars)
|
let scalars = Array(decode(bytes[...]).unicodeScalars)
|
||||||
guard scalars.count == 1, scalars[0].value != 0xFFFD, scalars[0].value >= 0x80 else { return }
|
guard scalars.count == 1, scalars[0].value != 0xFFFD, scalars[0].value >= 0x80 else { return }
|
||||||
if map[scalars[0].value] == nil { map[scalars[0].value] = bytes }
|
if map[scalars[0].value] == nil {
|
||||||
|
map[scalars[0].value] = bytes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for byte in 0x80 ... 0xFF {
|
for byte in 0x80 ... 0xFF {
|
||||||
record([UInt8(byte)])
|
record([UInt8(byte)])
|
||||||
|
|||||||
@ -25,13 +25,17 @@ extension PureXML.Parsing {
|
|||||||
// Each definition starts with required whitespace (the body begins
|
// Each definition starts with required whitespace (the body begins
|
||||||
// right after the element name).
|
// right after the element name).
|
||||||
guard scanner.requiredSpace() else { return nil }
|
guard scanner.requiredSpace() else { return nil }
|
||||||
if scanner.isAtEnd { return defaults } // trailing whitespace
|
if scanner.isAtEnd {
|
||||||
|
return defaults
|
||||||
|
} // trailing whitespace
|
||||||
guard scanner.name() else { return nil }
|
guard scanner.name() else { return nil }
|
||||||
guard scanner.requiredSpace() else { return nil }
|
guard scanner.requiredSpace() else { return nil }
|
||||||
guard attType(&scanner) else { return nil }
|
guard attType(&scanner) else { return nil }
|
||||||
guard scanner.requiredSpace() else { return nil }
|
guard scanner.requiredSpace() else { return nil }
|
||||||
guard let value = defaultDecl(&scanner) else { return nil }
|
guard let value = defaultDecl(&scanner) else { return nil }
|
||||||
if let value { defaults.append(value) }
|
if let value {
|
||||||
|
defaults.append(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return defaults
|
return defaults
|
||||||
}
|
}
|
||||||
@ -41,7 +45,9 @@ extension PureXML.Parsing {
|
|||||||
return scanner.nameGroup(nmtokens: true)
|
return scanner.nameGroup(nmtokens: true)
|
||||||
}
|
}
|
||||||
guard let keyword = scanner.keyword() else { return false }
|
guard let keyword = scanner.keyword() else { return false }
|
||||||
if tokenizedTypes.contains(keyword) { return true }
|
if tokenizedTypes.contains(keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if keyword == "NOTATION" {
|
if keyword == "NOTATION" {
|
||||||
guard scanner.requiredSpace() else { return false }
|
guard scanner.requiredSpace() else { return false }
|
||||||
return scanner.nameGroup(nmtokens: false)
|
return scanner.nameGroup(nmtokens: false)
|
||||||
|
|||||||
@ -8,7 +8,9 @@ extension PureXML.Parsing {
|
|||||||
enum DTDContentModelGrammar {
|
enum DTDContentModelGrammar {
|
||||||
static func isValid(_ model: String) -> Bool {
|
static func isValid(_ model: String) -> Bool {
|
||||||
let text = model.trimmingXMLWhitespace()
|
let text = model.trimmingXMLWhitespace()
|
||||||
if text == "EMPTY" || text == "ANY" { return true }
|
if text == "EMPTY" || text == "ANY" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
var scanner = ModelScanner(text)
|
var scanner = ModelScanner(text)
|
||||||
guard scanner.peek() == "(" else { return false }
|
guard scanner.peek() == "(" else { return false }
|
||||||
// Mixed and children both start with '('; Mixed is identified by
|
// Mixed and children both start with '('; Mixed is identified by
|
||||||
@ -74,7 +76,9 @@ private struct ModelScanner {
|
|||||||
var sawAlternative = false
|
var sawAlternative = false
|
||||||
while true {
|
while true {
|
||||||
skipSpace()
|
skipSpace()
|
||||||
if consume(")") { break }
|
if consume(")") {
|
||||||
|
break
|
||||||
|
}
|
||||||
guard consume("|") else { return false }
|
guard consume("|") else { return false }
|
||||||
sawAlternative = true
|
sawAlternative = true
|
||||||
skipSpace()
|
skipSpace()
|
||||||
@ -104,7 +108,9 @@ private struct ModelScanner {
|
|||||||
var connector: Character?
|
var connector: Character?
|
||||||
while !consume(")") {
|
while !consume(")") {
|
||||||
guard let next = advance(), next == "|" || next == "," else { return false }
|
guard let next = advance(), next == "|" || next == "," else { return false }
|
||||||
if let connector, connector != next { return false }
|
if let connector, connector != next {
|
||||||
|
return false
|
||||||
|
}
|
||||||
connector = next
|
connector = next
|
||||||
skipSpace()
|
skipSpace()
|
||||||
guard particle() else { return false }
|
guard particle() else { return false }
|
||||||
|
|||||||
@ -83,7 +83,9 @@ struct DTDScanner {
|
|||||||
/// a VC (deferred finding), since the unread external declarations might
|
/// a VC (deferred finding), since the unread external declarations might
|
||||||
/// have declared the entity (production 68).
|
/// have declared the entity (production 68).
|
||||||
var entityDeclaredIsWellFormedness: Bool {
|
var entityDeclaredIsWellFormedness: Bool {
|
||||||
if standalone { return true }
|
if standalone {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return doctype.externalSubset == nil && doctype.parameterEntities.isEmpty && unresolvedParameterEntities.isEmpty
|
return doctype.externalSubset == nil && doctype.parameterEntities.isEmpty && unresolvedParameterEntities.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,9 @@ extension DTDScanner {
|
|||||||
} else if reader.matches("]]>") {
|
} else if reader.matches("]]>") {
|
||||||
reader.consume("]]>")
|
reader.consume("]]>")
|
||||||
nesting -= 1
|
nesting -= 1
|
||||||
if nesting == 0 { return body }
|
if nesting == 0 {
|
||||||
|
return body
|
||||||
|
}
|
||||||
body += "]]>"
|
body += "]]>"
|
||||||
} else if let character = reader.peek() {
|
} else if let character = reader.peek() {
|
||||||
body.append(character)
|
body.append(character)
|
||||||
@ -168,7 +170,9 @@ extension DTDScanner {
|
|||||||
while index < text.endIndex {
|
while index < text.endIndex {
|
||||||
let character = text[index]
|
let character = text[index]
|
||||||
if let open = quote {
|
if let open = quote {
|
||||||
if character == open { quote = nil }
|
if character == open {
|
||||||
|
quote = nil
|
||||||
|
}
|
||||||
index = text.index(after: index)
|
index = text.index(after: index)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,7 +95,9 @@ extension DTDScanner {
|
|||||||
if character == "&" || character == "%" {
|
if character == "&" || character == "%" {
|
||||||
var probe = index + 1
|
var probe = index + 1
|
||||||
let isCharacterReference = character == "&" && probe < characters.count && characters[probe] == "#"
|
let isCharacterReference = character == "&" && probe < characters.count && characters[probe] == "#"
|
||||||
if isCharacterReference { probe += 1 }
|
if isCharacterReference {
|
||||||
|
probe += 1
|
||||||
|
}
|
||||||
let bodyStart = probe
|
let bodyStart = probe
|
||||||
let stops: Set<Character> = [";", "&", "%"]
|
let stops: Set<Character> = [";", "&", "%"]
|
||||||
while probe < characters.count, !stops.contains(characters[probe]), !characters[probe].isWhitespace {
|
while probe < characters.count, !stops.contains(characters[probe]), !characters[probe].isWhitespace {
|
||||||
@ -237,7 +239,9 @@ extension DTDScanner {
|
|||||||
var systemID = ""
|
var systemID = ""
|
||||||
if reader.peek()?.isWhitespace == true {
|
if reader.peek()?.isWhitespace == true {
|
||||||
reader.skipSpace()
|
reader.skipSpace()
|
||||||
if let literal = scanLiteral(&reader) { systemID = literal }
|
if let literal = scanLiteral(&reader) {
|
||||||
|
systemID = literal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ExternalID(publicID: publicID, systemID: systemID, base: currentBase)
|
return ExternalID(publicID: publicID, systemID: systemID, base: currentBase)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
scalars.append(scalar)
|
scalars.append(scalar)
|
||||||
} else {
|
} else {
|
||||||
scalars.append("\u{FFFD}")
|
scalars.append("\u{FFFD}")
|
||||||
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
|
if byte <= 0x7F {
|
||||||
|
scalars.append(Unicode.Scalar(byte))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if byte <= 0x7F {
|
} else if byte <= 0x7F {
|
||||||
scalars.append(Unicode.Scalar(byte))
|
scalars.append(Unicode.Scalar(byte))
|
||||||
@ -36,7 +38,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
scalars.append("\u{FFFD}")
|
scalars.append("\u{FFFD}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lead != 0 { scalars.append("\u{FFFD}") }
|
if lead != 0 {
|
||||||
|
scalars.append("\u{FFFD}")
|
||||||
|
}
|
||||||
return String(scalars)
|
return String(scalars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
scalars.append(scalar)
|
scalars.append(scalar)
|
||||||
} else {
|
} else {
|
||||||
scalars.append("\u{FFFD}")
|
scalars.append("\u{FFFD}")
|
||||||
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
|
if byte <= 0x7F {
|
||||||
|
scalars.append(Unicode.Scalar(byte))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if byte <= 0x7F {
|
} else if byte <= 0x7F {
|
||||||
scalars.append(Unicode.Scalar(byte))
|
scalars.append(Unicode.Scalar(byte))
|
||||||
@ -27,7 +29,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
scalars.append("\u{FFFD}")
|
scalars.append("\u{FFFD}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lead != 0 { scalars.append("\u{FFFD}") }
|
if lead != 0 {
|
||||||
|
scalars.append("\u{FFFD}")
|
||||||
|
}
|
||||||
return String(scalars)
|
return String(scalars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -126,7 +126,9 @@ extension PureXML.Parsing.EntityDecoder {
|
|||||||
/// A tentative `false` recorded before the recursion breaks any cycle (a
|
/// A tentative `false` recorded before the recursion breaks any cycle (a
|
||||||
/// cyclic entity is an error caught when the reference is expanded).
|
/// cyclic entity is an error caught when the reference is expanded).
|
||||||
private static func producesMarkup(_ name: String, entities: [String: String], cache: inout [String: Bool]) -> Bool {
|
private static func producesMarkup(_ name: String, entities: [String: String], cache: inout [String: Bool]) -> Bool {
|
||||||
if let cached = cache[name] { return cached }
|
if let cached = cache[name] {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
guard let replacement = entities[name] else {
|
guard let replacement = entities[name] else {
|
||||||
cache[name] = false
|
cache[name] = false
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -57,9 +57,13 @@ extension PureXML.Parsing {
|
|||||||
while let character = scanner.advance() {
|
while let character = scanner.advance() {
|
||||||
switch character {
|
switch character {
|
||||||
case "<":
|
case "<":
|
||||||
if let reason = scanner.markup(depth: &depth) { return reason }
|
if let reason = scanner.markup(depth: &depth) {
|
||||||
|
return reason
|
||||||
|
}
|
||||||
case "&":
|
case "&":
|
||||||
if !scanner.completeReference() { return "incomplete reference" }
|
if !scanner.completeReference() {
|
||||||
|
return "incomplete reference"
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -100,7 +104,9 @@ private struct ReplacementScanner {
|
|||||||
|
|
||||||
private mutating func skip(past terminator: String) -> Bool {
|
private mutating func skip(past terminator: String) -> Bool {
|
||||||
while index < characters.count {
|
while index < characters.count {
|
||||||
if consume(terminator) { return true }
|
if consume(terminator) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -126,8 +132,12 @@ private struct ReplacementScanner {
|
|||||||
depth -= 1
|
depth -= 1
|
||||||
return depth < 0 ? "an end tag closes an element opened outside the entity" : nil
|
return depth < 0 ? "an end tag closes an element opened outside the entity" : nil
|
||||||
}
|
}
|
||||||
if let reason = attributes() { return reason }
|
if let reason = attributes() {
|
||||||
if consume("/>") { return nil }
|
return reason
|
||||||
|
}
|
||||||
|
if consume("/>") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
guard consume(">") else { return "malformed start tag" }
|
guard consume(">") else { return "malformed start tag" }
|
||||||
depth += 1
|
depth += 1
|
||||||
return nil
|
return nil
|
||||||
@ -146,12 +156,16 @@ private struct ReplacementScanner {
|
|||||||
while true {
|
while true {
|
||||||
skipSpace()
|
skipSpace()
|
||||||
guard let next = peek() else { return "unterminated start tag" }
|
guard let next = peek() else { return "unterminated start tag" }
|
||||||
if next == ">" || next == "/" { return nil }
|
if next == ">" || next == "/" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
guard name() else { return "malformed attribute name" }
|
guard name() else { return "malformed attribute name" }
|
||||||
skipSpace()
|
skipSpace()
|
||||||
guard consume("=") else { return "attribute without a value" }
|
guard consume("=") else { return "attribute without a value" }
|
||||||
skipSpace()
|
skipSpace()
|
||||||
if let reason = attributeValue() { return reason }
|
if let reason = attributeValue() {
|
||||||
|
return reason
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,10 +174,14 @@ private struct ReplacementScanner {
|
|||||||
guard let quote = peek(), quote == "\"" || quote == "'" else { return "unquoted attribute value" }
|
guard let quote = peek(), quote == "\"" || quote == "'" else { return "unquoted attribute value" }
|
||||||
index += 1
|
index += 1
|
||||||
while let character = peek(), character != quote {
|
while let character = peek(), character != quote {
|
||||||
if character == "<" { return "'<' in an attribute value" }
|
if character == "<" {
|
||||||
|
return "'<' in an attribute value"
|
||||||
|
}
|
||||||
if character == "&" {
|
if character == "&" {
|
||||||
index += 1
|
index += 1
|
||||||
if !completeReference() { return "incomplete reference in an attribute value" }
|
if !completeReference() {
|
||||||
|
return "incomplete reference in an attribute value"
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
index += 1
|
index += 1
|
||||||
@ -174,7 +192,9 @@ private struct ReplacementScanner {
|
|||||||
/// After a consumed `&`: a complete `name;` or `#digits;`.
|
/// After a consumed `&`: a complete `name;` or `#digits;`.
|
||||||
mutating func completeReference() -> Bool {
|
mutating func completeReference() -> Bool {
|
||||||
var sawBody = false
|
var sawBody = false
|
||||||
if peek() == "#" { index += 1 }
|
if peek() == "#" {
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
while let character = peek(), character != ";" {
|
while let character = peek(), character != ";" {
|
||||||
guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isNameChar) else { return false }
|
guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isNameChar) else { return false }
|
||||||
sawBody = true
|
sawBody = true
|
||||||
|
|||||||
@ -31,9 +31,15 @@ extension PureXML.Parsing.EventReader {
|
|||||||
/// streaming source or spliced entity text). Equivalent to the byte switch
|
/// streaming source or spliced entity text). Equivalent to the byte switch
|
||||||
/// in `scanContentMarkup`.
|
/// in `scanContentMarkup`.
|
||||||
private mutating func scanContentMarkupViaCharacters() throws -> Event {
|
private mutating func scanContentMarkupViaCharacters() throws -> Event {
|
||||||
if reader.matches("</") { return try scanEndTag() }
|
if reader.matches("</") {
|
||||||
if reader.matches("<?") { return try scanContentProcessingInstruction() }
|
return try scanEndTag()
|
||||||
if reader.matches("<!") { return try scanContentDeclaration() }
|
}
|
||||||
|
if reader.matches("<?") {
|
||||||
|
return try scanContentProcessingInstruction()
|
||||||
|
}
|
||||||
|
if reader.matches("<!") {
|
||||||
|
return try scanContentDeclaration()
|
||||||
|
}
|
||||||
return try scanStartTag()
|
return try scanStartTag()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +52,9 @@ extension PureXML.Parsing.EventReader {
|
|||||||
recordEmptyElementContent("a comment")
|
recordEmptyElementContent("a comment")
|
||||||
return try scanComment()
|
return try scanComment()
|
||||||
}
|
}
|
||||||
if reader.matches("<![CDATA[") { return try scanCDATA() }
|
if reader.matches("<![CDATA[") {
|
||||||
|
return try scanCDATA()
|
||||||
|
}
|
||||||
if reader.matches("<!DOCTYPE") {
|
if reader.matches("<!DOCTYPE") {
|
||||||
throw PureXML.Parsing.ParseError.unsupportedDoctype(reader.mark)
|
throw PureXML.Parsing.ParseError.unsupportedDoctype(reader.mark)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,9 @@ extension PureXML.Parsing.EventReader {
|
|||||||
throw PureXML.Parsing.ParseError.malformedDeclaration(mark)
|
throw PureXML.Parsing.ParseError.malformedDeclaration(mark)
|
||||||
}
|
}
|
||||||
xmlDeclaration = declaration
|
xmlDeclaration = declaration
|
||||||
if declaration.version == "1.1" { reader.xml11 = true }
|
if declaration.version == "1.1" {
|
||||||
|
reader.xml11 = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The current source offset, used to tell whether a failing ``next()`` made
|
/// The current source offset, used to tell whether a failing ``next()`` made
|
||||||
@ -31,7 +33,9 @@ extension PureXML.Parsing.EventReader {
|
|||||||
mutating func recover(from offset: Int) -> Bool {
|
mutating func recover(from offset: Int) -> Bool {
|
||||||
pending.removeAll()
|
pending.removeAll()
|
||||||
guard reader.peek() != nil else { return false }
|
guard reader.peek() != nil else { return false }
|
||||||
if reader.mark.offset > offset { return true }
|
if reader.mark.offset > offset {
|
||||||
|
return true
|
||||||
|
}
|
||||||
reader.advance()
|
reader.advance()
|
||||||
while let character = reader.peek(), character != "<" {
|
while let character = reader.peek(), character != "<" {
|
||||||
reader.advance()
|
reader.advance()
|
||||||
@ -102,7 +106,9 @@ extension PureXML.Parsing.EventReader {
|
|||||||
case let .mismatchedEndTag(_, found, _), let .unexpectedEndTag(found, _):
|
case let .mismatchedEndTag(_, found, _), let .unexpectedEndTag(found, _):
|
||||||
return closeTo(found)
|
return closeTo(found)
|
||||||
default:
|
default:
|
||||||
if !recover(from: start) { reachedEnd = true }
|
if !recover(from: start) {
|
||||||
|
reachedEnd = true
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,9 @@ extension PureXML.Parsing.EventReader {
|
|||||||
/// it is a WFC when the document is standalone or could not have
|
/// it is a WFC when the document is standalone or could not have
|
||||||
/// external declarations, and a validity finding otherwise.
|
/// external declarations, and a validity finding otherwise.
|
||||||
var undeclaredEntityIsFatal: Bool {
|
var undeclaredEntityIsFatal: Bool {
|
||||||
if xmlDeclaration?.standalone == true { return true }
|
if xmlDeclaration?.standalone == true {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return documentType.externalSubset == nil && documentType.parameterEntities.isEmpty
|
return documentType.externalSubset == nil && documentType.parameterEntities.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +49,9 @@ extension PureXML.Parsing.EventReader {
|
|||||||
/// an entity whose stored replacement does (the errata E15h shape: the
|
/// an entity whose stored replacement does (the errata E15h shape: the
|
||||||
/// double-escaped reference surfaces when the replacement is reparsed).
|
/// double-escaped reference surfaces when the replacement is reparsed).
|
||||||
mutating func referencesCharacter(_ raw: String) -> Bool {
|
mutating func referencesCharacter(_ raw: String) -> Bool {
|
||||||
if containsCharacterReference(raw) { return true }
|
if containsCharacterReference(raw) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
let entities = referencableEntities
|
let entities = referencableEntities
|
||||||
var index = raw.startIndex
|
var index = raw.startIndex
|
||||||
while index < raw.endIndex, let amp = raw[index...].firstIndex(of: "&") {
|
while index < raw.endIndex, let amp = raw[index...].firstIndex(of: "&") {
|
||||||
@ -66,7 +70,9 @@ extension PureXML.Parsing.EventReader {
|
|||||||
func containsCharacterReference(_ raw: String) -> Bool {
|
func containsCharacterReference(_ raw: String) -> Bool {
|
||||||
var sawAmp = false
|
var sawAmp = false
|
||||||
for character in raw {
|
for character in raw {
|
||||||
if sawAmp, character == "#" { return true }
|
if sawAmp, character == "#" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
sawAmp = character == "&"
|
sawAmp = character == "&"
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -136,7 +142,9 @@ extension PureXML.Parsing.EventReader {
|
|||||||
guard isBinding,
|
guard isBinding,
|
||||||
let declaration = declarations.first(where: { $0.name == attribute.name.description })
|
let declaration = declarations.first(where: { $0.name == attribute.name.description })
|
||||||
else { return attribute }
|
else { return attribute }
|
||||||
if case .cdata = declaration.type { return attribute }
|
if case .cdata = declaration.type {
|
||||||
|
return attribute
|
||||||
|
}
|
||||||
let collapsed = attribute.value.split(separator: " ").joined(separator: " ")
|
let collapsed = attribute.value.split(separator: " ").joined(separator: " ")
|
||||||
return PureXML.Model.Attribute(name: attribute.name, value: collapsed)
|
return PureXML.Model.Attribute(name: attribute.name, value: collapsed)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,8 +71,12 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
/// The scalar for a four-byte pointer through the linear ranges, or nil for
|
/// The scalar for a four-byte pointer through the linear ranges, or nil for
|
||||||
/// the unmapped gap and the out-of-range tail.
|
/// the unmapped gap and the out-of-range tail.
|
||||||
static func rangeScalar(_ pointer: Int) -> Unicode.Scalar? {
|
static func rangeScalar(_ pointer: Int) -> Unicode.Scalar? {
|
||||||
if pointer > 39419, pointer < 189_000 { return nil }
|
if pointer > 39419, pointer < 189_000 {
|
||||||
if pointer > 1_237_575 { return nil }
|
return nil
|
||||||
|
}
|
||||||
|
if pointer > 1_237_575 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
let target = UInt32(pointer)
|
let target = UInt32(pointer)
|
||||||
var low = 0
|
var low = 0
|
||||||
var high = gb18030RangePointers.count - 1
|
var high = gb18030RangePointers.count - 1
|
||||||
|
|||||||
@ -18,7 +18,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
scalars.append(scalar)
|
scalars.append(scalar)
|
||||||
} else {
|
} else {
|
||||||
scalars.append("\u{FFFD}")
|
scalars.append("\u{FFFD}")
|
||||||
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
|
if byte <= 0x7F {
|
||||||
|
scalars.append(Unicode.Scalar(byte))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if byte <= 0x7F {
|
} else if byte <= 0x7F {
|
||||||
scalars.append(Unicode.Scalar(byte))
|
scalars.append(Unicode.Scalar(byte))
|
||||||
@ -30,7 +32,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
scalars.append("\u{FFFD}")
|
scalars.append("\u{FFFD}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lead != 0 { scalars.append("\u{FFFD}") }
|
if lead != 0 {
|
||||||
|
scalars.append("\u{FFFD}")
|
||||||
|
}
|
||||||
return String(scalars)
|
return String(scalars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
}
|
}
|
||||||
decodeByte(byte, mode: mode, lead: &lead, into: &scalars)
|
decodeByte(byte, mode: mode, lead: &lead, into: &scalars)
|
||||||
}
|
}
|
||||||
if lead != nil { scalars.append("\u{FFFD}") }
|
if lead != nil {
|
||||||
|
scalars.append("\u{FFFD}")
|
||||||
|
}
|
||||||
return String(scalars)
|
return String(scalars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,9 @@ extension PureXML.Parsing.XMLDeclaration {
|
|||||||
// Pseudo-attributes must be separated by whitespace.
|
// Pseudo-attributes must be separated by whitespace.
|
||||||
let before = index
|
let before = index
|
||||||
skipSpace()
|
skipSpace()
|
||||||
if index == before, index < chars.count { return nil }
|
if index == before, index < chars.count {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return pairs
|
return pairs
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,13 +52,17 @@ public extension PureXML.Parsing {
|
|||||||
case let .close(name):
|
case let .close(name):
|
||||||
close(name)
|
close(name)
|
||||||
case let .text(value):
|
case let .text(value):
|
||||||
if !value.isEmpty { handler.characters?(value) }
|
if !value.isEmpty {
|
||||||
|
handler.characters?(value)
|
||||||
|
}
|
||||||
case let .comment(value):
|
case let .comment(value):
|
||||||
handler.comment?(value)
|
handler.comment?(value)
|
||||||
case let .cdata(value):
|
case let .cdata(value):
|
||||||
handler.cdata?(value)
|
handler.cdata?(value)
|
||||||
case let .processingInstruction(target, data):
|
case let .processingInstruction(target, data):
|
||||||
if target.lowercased() != "xml" { handler.processingInstruction?(target, data) }
|
if target.lowercased() != "xml" {
|
||||||
|
handler.processingInstruction?(target, data)
|
||||||
|
}
|
||||||
case .ignorable:
|
case .ignorable:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,9 @@ private struct TagCursor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private mutating func advance() {
|
private mutating func advance() {
|
||||||
if index < chars.count { index += 1 }
|
if index < chars.count {
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func skipSpace() {
|
private mutating func skipSpace() {
|
||||||
@ -131,11 +133,21 @@ extension PureXML.Parsing {
|
|||||||
if isPrefix(buffer, ofAny: ["<!--", "<![CDATA[", "<!", "</", "<?"], decidedAt: 2), !final {
|
if isPrefix(buffer, ofAny: ["<!--", "<![CDATA[", "<!", "</", "<?"], decidedAt: 2), !final {
|
||||||
return .needMore
|
return .needMore
|
||||||
}
|
}
|
||||||
if startsWith(buffer, "<!--") { return delimited(buffer, open: 4, close: "-->") { .comment(String($0)) } }
|
if startsWith(buffer, "<!--") {
|
||||||
if startsWith(buffer, "<![CDATA[") { return delimited(buffer, open: 9, close: "]]>") { .cdata(String($0)) } }
|
return delimited(buffer, open: 4, close: "-->") { .comment(String($0)) }
|
||||||
if startsWith(buffer, "<!") { return until(buffer, terminator: ">") { _ in .ignorable } }
|
}
|
||||||
if startsWith(buffer, "</") { return scanEndTag(buffer) }
|
if startsWith(buffer, "<![CDATA[") {
|
||||||
if startsWith(buffer, "<?") { return scanProcessingInstruction(buffer) }
|
return delimited(buffer, open: 9, close: "]]>") { .cdata(String($0)) }
|
||||||
|
}
|
||||||
|
if startsWith(buffer, "<!") {
|
||||||
|
return until(buffer, terminator: ">") { _ in .ignorable }
|
||||||
|
}
|
||||||
|
if startsWith(buffer, "</") {
|
||||||
|
return scanEndTag(buffer)
|
||||||
|
}
|
||||||
|
if startsWith(buffer, "<?") {
|
||||||
|
return scanProcessingInstruction(buffer)
|
||||||
|
}
|
||||||
return scanStartTag(buffer, final: final)
|
return scanStartTag(buffer, final: final)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +174,9 @@ extension PureXML.Parsing {
|
|||||||
guard let end = tagEnd(buffer) else { return final ? .token(.text("<"), consumed: 1) : .needMore }
|
guard let end = tagEnd(buffer) else { return final ? .token(.text("<"), consumed: 1) : .needMore }
|
||||||
var inner = Array(buffer[1 ..< end])
|
var inner = Array(buffer[1 ..< end])
|
||||||
let selfClosing = inner.last == "/"
|
let selfClosing = inner.last == "/"
|
||||||
if selfClosing { inner.removeLast() }
|
if selfClosing {
|
||||||
|
inner.removeLast()
|
||||||
|
}
|
||||||
let (name, attributes) = parseTag(inner)
|
let (name, attributes) = parseTag(inner)
|
||||||
return .token(.open(name: name, attributes: attributes, selfClosing: selfClosing), consumed: end + 1)
|
return .token(.open(name: name, attributes: attributes, selfClosing: selfClosing), consumed: end + 1)
|
||||||
}
|
}
|
||||||
@ -201,7 +215,9 @@ extension PureXML.Parsing {
|
|||||||
for index in 1 ..< buffer.count {
|
for index in 1 ..< buffer.count {
|
||||||
let character = buffer[index]
|
let character = buffer[index]
|
||||||
if let active = quote {
|
if let active = quote {
|
||||||
if character == active { quote = nil }
|
if character == active {
|
||||||
|
quote = nil
|
||||||
|
}
|
||||||
} else if character == "\"" || character == "'" {
|
} else if character == "\"" || character == "'" {
|
||||||
quote = character
|
quote = character
|
||||||
} else if character == ">" {
|
} else if character == ">" {
|
||||||
|
|||||||
@ -67,7 +67,9 @@ extension PureXML.Parsing {
|
|||||||
// Split multi-scalar graphemes so the buffer is scalar-level.
|
// Split multi-scalar graphemes so the buffer is scalar-level.
|
||||||
var queued: [Character] = []
|
var queued: [Character] = []
|
||||||
self.pull = {
|
self.pull = {
|
||||||
if !queued.isEmpty { return queued.removeFirst() }
|
if !queued.isEmpty {
|
||||||
|
return queued.removeFirst()
|
||||||
|
}
|
||||||
guard let next = pull() else { return nil }
|
guard let next = pull() else { return nil }
|
||||||
let scalars = next.unicodeScalars
|
let scalars = next.unicodeScalars
|
||||||
guard scalars.count > 1 else { return next }
|
guard scalars.count > 1 else { return next }
|
||||||
@ -133,10 +135,14 @@ extension PureXML.Parsing {
|
|||||||
private mutating func normalizedPull() -> Character? {
|
private mutating func normalizedPull() -> Character? {
|
||||||
guard let character = rawNext() else { return nil }
|
guard let character = rawNext() else { return nil }
|
||||||
// Swift coalesces a CR+LF pair into one grapheme-cluster character.
|
// Swift coalesces a CR+LF pair into one grapheme-cluster character.
|
||||||
if character == "\r\n" { return "\n" }
|
if character == "\r\n" {
|
||||||
|
return "\n"
|
||||||
|
}
|
||||||
if character == "\r" {
|
if character == "\r" {
|
||||||
let next = rawNext()
|
let next = rawNext()
|
||||||
if next != "\n", !(xml11 && next == "\u{85}") { pendingRaw = next }
|
if next != "\n", !(xml11 && next == "\u{85}") {
|
||||||
|
pendingRaw = next
|
||||||
|
}
|
||||||
return "\n"
|
return "\n"
|
||||||
}
|
}
|
||||||
if xml11, character == "\u{85}" || character == "\u{2028}" {
|
if xml11, character == "\u{85}" || character == "\u{2028}" {
|
||||||
@ -201,7 +207,9 @@ extension PureXML.Parsing {
|
|||||||
/// True if the upcoming characters equal `literal`, without consuming.
|
/// True if the upcoming characters equal `literal`, without consuming.
|
||||||
mutating func matches(_ literal: String) -> Bool {
|
mutating func matches(_ literal: String) -> Bool {
|
||||||
if buffer.isEmpty, pendingRaw == nil, let storage {
|
if buffer.isEmpty, pendingRaw == nil, let storage {
|
||||||
if let byteResult = matchesBytes(literal, storage) { return byteResult }
|
if let byteResult = matchesBytes(literal, storage) {
|
||||||
|
return byteResult
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// No per-call Array materialization: literals are short constants
|
// No per-call Array materialization: literals are short constants
|
||||||
// and this runs on every scan decision (the hottest comparison in
|
// and this runs on every scan decision (the hottest comparison in
|
||||||
@ -211,7 +219,9 @@ extension PureXML.Parsing {
|
|||||||
guard available >= count else { return false }
|
guard available >= count else { return false }
|
||||||
var index = 0
|
var index = 0
|
||||||
for character in literal {
|
for character in literal {
|
||||||
if buffer[head + index] != character { return false }
|
if buffer[head + index] != character {
|
||||||
|
return false
|
||||||
|
}
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -254,10 +264,18 @@ extension PureXML.Parsing {
|
|||||||
let limit = byteIndex + maxLength
|
let limit = byteIndex + maxLength
|
||||||
while index < storage.count {
|
while index < storage.count {
|
||||||
let byte = pointer[index]
|
let byte = pointer[index]
|
||||||
if byte >= 0x80 { return nil }
|
if byte >= 0x80 {
|
||||||
if !PureXML.Parsing.XMLCharacter.isASCIINameChar(byte) { break }
|
return nil
|
||||||
if index >= limit { return nil }
|
}
|
||||||
if colon == nil, byte == 0x3A { colon = index - byteIndex }
|
if !PureXML.Parsing.XMLCharacter.isASCIINameChar(byte) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if index >= limit {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if colon == nil, byte == 0x3A {
|
||||||
|
colon = index - byteIndex
|
||||||
|
}
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
let length = index - byteIndex
|
let length = index - byteIndex
|
||||||
@ -289,7 +307,9 @@ extension PureXML.Parsing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mutating func skipSpace() {
|
mutating func skipSpace() {
|
||||||
if skipSpaceBytes() { return }
|
if skipSpaceBytes() {
|
||||||
|
return
|
||||||
|
}
|
||||||
while let character = peek(), character.isXMLWhitespace {
|
while let character = peek(), character.isXMLWhitespace {
|
||||||
advance()
|
advance()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,19 +22,25 @@ extension PureXML.Parsing.Reader {
|
|||||||
// the slow path consumed "]]" and the run would begin with '>': leave a
|
// the slow path consumed "]]" and the run would begin with '>': leave a
|
||||||
// leading '>' to the slow path and its cdataCloseInContent check (W3C
|
// leading '>' to the slow path and its cdataCloseInContent check (W3C
|
||||||
// ibm14n01).
|
// ibm14n01).
|
||||||
if byteIndex < count, pointer[byteIndex] == 0x3E { return nil }
|
if byteIndex < count, pointer[byteIndex] == 0x3E {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var index = byteIndex
|
var index = byteIndex
|
||||||
var newlines = 0
|
var newlines = 0
|
||||||
var lastLineStart = -1
|
var lastLineStart = -1
|
||||||
while index < count {
|
while index < count {
|
||||||
let byte = pointer[index]
|
let byte = pointer[index]
|
||||||
if byte == 0x3C { break } // '<'
|
if byte == 0x3C {
|
||||||
|
break
|
||||||
|
} // '<'
|
||||||
// Valid plain ASCII content only; CR, ']' and non-ASCII go to the
|
// Valid plain ASCII content only; CR, ']' and non-ASCII go to the
|
||||||
// character path.
|
// character path.
|
||||||
guard byte < 0x80, byte != 0x0D, byte != 0x5D,
|
guard byte < 0x80, byte != 0x0D, byte != 0x5D,
|
||||||
byte >= 0x20 || byte == 0x09 || byte == 0x0A
|
byte >= 0x20 || byte == 0x09 || byte == 0x0A
|
||||||
else {
|
else {
|
||||||
if index == byteIndex { return nil }
|
if index == byteIndex {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if byte == 0x0A {
|
if byte == 0x0A {
|
||||||
|
|||||||
@ -19,7 +19,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
scalars.append(scalar)
|
scalars.append(scalar)
|
||||||
} else {
|
} else {
|
||||||
scalars.append("\u{FFFD}")
|
scalars.append("\u{FFFD}")
|
||||||
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) } // reprocess ASCII trail
|
if byte <= 0x7F {
|
||||||
|
scalars.append(Unicode.Scalar(byte))
|
||||||
|
} // reprocess ASCII trail
|
||||||
}
|
}
|
||||||
} else if let single = singleByteScalar(byte) {
|
} else if let single = singleByteScalar(byte) {
|
||||||
scalars.append(single)
|
scalars.append(single)
|
||||||
@ -29,15 +31,21 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
scalars.append("\u{FFFD}")
|
scalars.append("\u{FFFD}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lead != 0 { scalars.append("\u{FFFD}") }
|
if lead != 0 {
|
||||||
|
scalars.append("\u{FFFD}")
|
||||||
|
}
|
||||||
return String(scalars)
|
return String(scalars)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The scalar for a byte that is not part of a two-byte sequence: ASCII
|
/// The scalar for a byte that is not part of a two-byte sequence: ASCII
|
||||||
/// (`0x00`-`0x80`) or half-width katakana (`0xA1`-`0xDF`), else nil.
|
/// (`0x00`-`0x80`) or half-width katakana (`0xA1`-`0xDF`), else nil.
|
||||||
static func singleByteScalar(_ byte: UInt8) -> Unicode.Scalar? {
|
static func singleByteScalar(_ byte: UInt8) -> Unicode.Scalar? {
|
||||||
if byte <= 0x80 { return Unicode.Scalar(byte) }
|
if byte <= 0x80 {
|
||||||
if (0xA1 ... 0xDF).contains(byte) { return Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1) }
|
return Unicode.Scalar(byte)
|
||||||
|
}
|
||||||
|
if (0xA1 ... 0xDF).contains(byte) {
|
||||||
|
return Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +61,9 @@ extension PureXML.Parsing.ByteDecoder {
|
|||||||
let offset = lead < 0xA0 ? 0x81 : 0xC1
|
let offset = lead < 0xA0 ? 0x81 : 0xC1
|
||||||
let trailOffset = trail < 0x7F ? 0x40 : 0x41
|
let trailOffset = trail < 0x7F ? 0x40 : 0x41
|
||||||
let pointer = (Int(lead) - offset) * 188 + Int(trail) - trailOffset
|
let pointer = (Int(lead) - offset) * 188 + Int(trail) - trailOffset
|
||||||
if (8836 ... 10715).contains(pointer) { return Unicode.Scalar(0xE000 + pointer - 8836) } // EUDC private use
|
if (8836 ... 10715).contains(pointer) {
|
||||||
|
return Unicode.Scalar(0xE000 + pointer - 8836)
|
||||||
|
} // EUDC private use
|
||||||
guard pointer >= 0, pointer < jis0208.count, jis0208[pointer] != 0xFFFF else { return nil }
|
guard pointer >= 0, pointer < jis0208.count, jis0208[pointer] != 0xFFFF else { return nil }
|
||||||
return Unicode.Scalar(UInt32(jis0208[pointer]))
|
return Unicode.Scalar(UInt32(jis0208[pointer]))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,8 +68,12 @@ extension PureXML.Parsing {
|
|||||||
finished = true
|
finished = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
|
if lead <= 0x7F {
|
||||||
if lead == 0x80 { return "\u{20AC}" }
|
return Character(Unicode.Scalar(lead))
|
||||||
|
}
|
||||||
|
if lead == 0x80 {
|
||||||
|
return "\u{20AC}"
|
||||||
|
}
|
||||||
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
|
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
|
||||||
guard let trail = nextByte() else { finished = true
|
guard let trail = nextByte() else { finished = true
|
||||||
return Self.replacement
|
return Self.replacement
|
||||||
@ -84,8 +88,12 @@ extension PureXML.Parsing {
|
|||||||
finished = true
|
finished = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
|
if lead <= 0x7F {
|
||||||
if lead == 0x80 { return "\u{20AC}" }
|
return Character(Unicode.Scalar(lead))
|
||||||
|
}
|
||||||
|
if lead == 0x80 {
|
||||||
|
return "\u{20AC}"
|
||||||
|
}
|
||||||
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
|
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
|
||||||
guard let second = nextByte() else { finished = true
|
guard let second = nextByte() else { finished = true
|
||||||
return Self.replacement
|
return Self.replacement
|
||||||
@ -108,7 +116,9 @@ extension PureXML.Parsing {
|
|||||||
finished = true
|
finished = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
|
if lead <= 0x7F {
|
||||||
|
return Character(Unicode.Scalar(lead))
|
||||||
|
}
|
||||||
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
|
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
|
||||||
guard let trail = nextByte() else { finished = true
|
guard let trail = nextByte() else { finished = true
|
||||||
return Self.replacement
|
return Self.replacement
|
||||||
@ -125,7 +135,9 @@ extension PureXML.Parsing {
|
|||||||
finished = true
|
finished = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
|
if lead <= 0x7F {
|
||||||
|
return Character(Unicode.Scalar(lead))
|
||||||
|
}
|
||||||
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
|
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
|
||||||
guard let trail = nextByte() else { finished = true
|
guard let trail = nextByte() else { finished = true
|
||||||
return Self.replacement
|
return Self.replacement
|
||||||
@ -140,7 +152,9 @@ extension PureXML.Parsing {
|
|||||||
finished = true
|
finished = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
|
if lead <= 0x7F {
|
||||||
|
return Character(Unicode.Scalar(lead))
|
||||||
|
}
|
||||||
if lead == 0x8E {
|
if lead == 0x8E {
|
||||||
guard let byte = nextByte() else { finished = true
|
guard let byte = nextByte() else { finished = true
|
||||||
return Self.replacement
|
return Self.replacement
|
||||||
@ -167,7 +181,9 @@ extension PureXML.Parsing {
|
|||||||
finished = true
|
finished = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if let single = PureXML.Parsing.ByteDecoder.ShiftJIS.singleByteScalar(lead) { return Character(single) }
|
if let single = PureXML.Parsing.ByteDecoder.ShiftJIS.singleByteScalar(lead) {
|
||||||
|
return Character(single)
|
||||||
|
}
|
||||||
guard PureXML.Parsing.ByteDecoder.ShiftJIS.isLead(lead) else { return Self.replacement }
|
guard PureXML.Parsing.ByteDecoder.ShiftJIS.isLead(lead) else { return Self.replacement }
|
||||||
guard let trail = nextByte() else {
|
guard let trail = nextByte() else {
|
||||||
finished = true
|
finished = true
|
||||||
@ -186,7 +202,9 @@ extension PureXML.Parsing {
|
|||||||
}
|
}
|
||||||
ordered.append(UInt32(byte))
|
ordered.append(UInt32(byte))
|
||||||
}
|
}
|
||||||
if !bigEndian { ordered.reverse() }
|
if !bigEndian {
|
||||||
|
ordered.reverse()
|
||||||
|
}
|
||||||
let value = ordered.reduce(UInt32(0)) { ($0 << 8) | $1 }
|
let value = ordered.reduce(UInt32(0)) { ($0 << 8) | $1 }
|
||||||
return Character(Unicode.Scalar(value) ?? "\u{FFFD}")
|
return Character(Unicode.Scalar(value) ?? "\u{FFFD}")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,9 @@ extension PureXML.Parsing.StreamingDecoder {
|
|||||||
finished = true
|
finished = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
|
if lead <= 0x7F {
|
||||||
|
return Character(Unicode.Scalar(lead))
|
||||||
|
}
|
||||||
if lead == 0x8E {
|
if lead == 0x8E {
|
||||||
guard let plane = nextByte(), let byte1 = nextByte(), let byte2 = nextByte() else {
|
guard let plane = nextByte(), let byte1 = nextByte(), let byte2 = nextByte() else {
|
||||||
finished = true
|
finished = true
|
||||||
|
|||||||
@ -186,7 +186,9 @@ public extension PureXML.Parsing {
|
|||||||
applyStartElement(name: name, attributes: attributes)
|
applyStartElement(name: name, attributes: attributes)
|
||||||
case let .endElement(name):
|
case let .endElement(name):
|
||||||
currentDepth -= 1
|
currentDepth -= 1
|
||||||
if !langStack.isEmpty { langStack.removeLast() }
|
if !langStack.isEmpty {
|
||||||
|
langStack.removeLast()
|
||||||
|
}
|
||||||
currentName = name
|
currentName = name
|
||||||
xmlLang = langStack.last ?? nil
|
xmlLang = langStack.last ?? nil
|
||||||
set(.endElement, name: name.description, depth: currentDepth)
|
set(.endElement, name: name.description, depth: currentDepth)
|
||||||
|
|||||||
@ -67,7 +67,9 @@ public extension PureXML.Parsing {
|
|||||||
|
|
||||||
/// `NameChar`: a name character after the first.
|
/// `NameChar`: a name character after the first.
|
||||||
public static func isNameChar(_ scalar: Unicode.Scalar) -> Bool {
|
public static func isNameChar(_ scalar: Unicode.Scalar) -> Bool {
|
||||||
if isNameStart(scalar) { return true }
|
if isNameStart(scalar) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
switch scalar.value {
|
switch scalar.value {
|
||||||
case 0x2D, // "-"
|
case 0x2D, // "-"
|
||||||
0x2E, // "."
|
0x2E, // "."
|
||||||
@ -86,7 +88,9 @@ public extension PureXML.Parsing {
|
|||||||
var scalars = name.unicodeScalars.makeIterator()
|
var scalars = name.unicodeScalars.makeIterator()
|
||||||
guard let first = scalars.next(), isNameStart(first) else { return false }
|
guard let first = scalars.next(), isNameStart(first) else { return false }
|
||||||
while let scalar = scalars.next() {
|
while let scalar = scalars.next() {
|
||||||
if !isNameChar(scalar) { return false }
|
if !isNameChar(scalar) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,9 @@ extension PureXML.Pattern {
|
|||||||
case let .prefixWildcard(prefix):
|
case let .prefixWildcard(prefix):
|
||||||
return name.prefix == prefix
|
return name.prefix == prefix
|
||||||
case let .name(text):
|
case let .name(text):
|
||||||
if text.contains(":") { return name.description == text }
|
if text.contains(":") {
|
||||||
|
return name.description == text
|
||||||
|
}
|
||||||
return name.description == text || name.localName == text
|
return name.description == text || name.localName == text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,7 +107,9 @@ public extension PureXML.Pattern {
|
|||||||
private func ancestorMatches(_ stepIndex: Int, below pathIndex: Int, _ path: [PureXML.Model.QualifiedName]) -> Bool {
|
private func ancestorMatches(_ stepIndex: Int, below pathIndex: Int, _ path: [PureXML.Model.QualifiedName]) -> Bool {
|
||||||
var candidate = pathIndex - 1
|
var candidate = pathIndex - 1
|
||||||
while candidate >= 0 {
|
while candidate >= 0 {
|
||||||
if matchUpTo(stepIndex, candidate, path) { return true }
|
if matchUpTo(stepIndex, candidate, path) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
candidate -= 1
|
candidate -= 1
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -42,8 +42,12 @@ extension PureXML.Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private mutating func leadingGap() -> Gap {
|
private mutating func leadingGap() -> Gap {
|
||||||
if consume("//") { return .descendant }
|
if consume("//") {
|
||||||
if consume("/") { return .root }
|
return .descendant
|
||||||
|
}
|
||||||
|
if consume("/") {
|
||||||
|
return .root
|
||||||
|
}
|
||||||
return .descendant
|
return .descendant
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,11 +64,17 @@ extension PureXML.Pattern {
|
|||||||
|
|
||||||
private mutating func parseStep(gap: Gap) throws -> RawStep {
|
private mutating func parseStep(gap: Gap) throws -> RawStep {
|
||||||
guard let character = peek() else { throw PatternError.expectedStep }
|
guard let character = peek() else { throw PatternError.expectedStep }
|
||||||
if character == "[" { throw PatternError.unsupported("predicate") }
|
if character == "[" {
|
||||||
if character == "." { throw PatternError.unsupported(".") }
|
throw PatternError.unsupported("predicate")
|
||||||
|
}
|
||||||
|
if character == "." {
|
||||||
|
throw PatternError.unsupported(".")
|
||||||
|
}
|
||||||
let isAttribute = consume("@")
|
let isAttribute = consume("@")
|
||||||
let test = try parseTest()
|
let test = try parseTest()
|
||||||
if peek() == "[" { throw PatternError.unsupported("predicate") }
|
if peek() == "[" {
|
||||||
|
throw PatternError.unsupported("predicate")
|
||||||
|
}
|
||||||
return RawStep(gap: gap, isAttribute: isAttribute, test: test)
|
return RawStep(gap: gap, isAttribute: isAttribute, test: test)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +126,9 @@ extension PureXML.Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private mutating func advance() {
|
private mutating func advance() {
|
||||||
if index < chars.count { index += 1 }
|
if index < chars.count {
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,9 @@ public extension PureXML.Pattern.Matcher {
|
|||||||
stack.append(name)
|
stack.append(name)
|
||||||
collect(stack: stack, attributes: attributes, into: &results)
|
collect(stack: stack, attributes: attributes, into: &results)
|
||||||
case .endElement:
|
case .endElement:
|
||||||
if !stack.isEmpty { stack.removeLast() }
|
if !stack.isEmpty {
|
||||||
|
stack.removeLast()
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,9 @@ extension PureXML.Regex {
|
|||||||
next.insert(transition.target)
|
next.insert(transition.target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if next.isEmpty { return false }
|
if next.isEmpty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
current = closure(next)
|
current = closure(next)
|
||||||
}
|
}
|
||||||
return current.contains(accept)
|
return current.contains(accept)
|
||||||
@ -104,7 +106,11 @@ extension PureXML.Regex {
|
|||||||
var last: Int?
|
var last: Int?
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
let part = fragment(for: node)
|
let part = fragment(for: node)
|
||||||
if let previous = last { states[previous].epsilon.append(part.start) } else { first = part.start }
|
if let previous = last {
|
||||||
|
states[previous].epsilon.append(part.start)
|
||||||
|
} else {
|
||||||
|
first = part.start
|
||||||
|
}
|
||||||
last = part.accept
|
last = part.accept
|
||||||
}
|
}
|
||||||
return (first ?? addState(), last ?? addState())
|
return (first ?? addState(), last ?? addState())
|
||||||
@ -127,7 +133,9 @@ extension PureXML.Regex {
|
|||||||
let boundedMin = Swift.min(minimum, Self.repeatUnrollCap)
|
let boundedMin = Swift.min(minimum, Self.repeatUnrollCap)
|
||||||
var unrolled = 0
|
var unrolled = 0
|
||||||
for _ in 0 ..< boundedMin {
|
for _ in 0 ..< boundedMin {
|
||||||
if stateBudgetExhausted { break }
|
if stateBudgetExhausted {
|
||||||
|
break
|
||||||
|
}
|
||||||
let part = fragment(for: node)
|
let part = fragment(for: node)
|
||||||
states[current].epsilon.append(part.start)
|
states[current].epsilon.append(part.start)
|
||||||
current = part.accept
|
current = part.accept
|
||||||
|
|||||||
@ -23,7 +23,9 @@ extension PureXML.Regex {
|
|||||||
/// a two-letter category (`Lu`, `Nd`, ...), or an `Is<Block>` block name.
|
/// a two-letter category (`Lu`, `Nd`, ...), or an `Is<Block>` block name.
|
||||||
enum CategoryMatcher {
|
enum CategoryMatcher {
|
||||||
static func isKnown(_ name: String) -> Bool {
|
static func isKnown(_ name: String) -> Bool {
|
||||||
if name.hasPrefix("Is") { return blocks[String(name.dropFirst(2))] != nil }
|
if name.hasPrefix("Is") {
|
||||||
|
return blocks[String(name.dropFirst(2))] != nil
|
||||||
|
}
|
||||||
return groups.contains(name) || codeValues.contains(name)
|
return groups.contains(name) || codeValues.contains(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,9 @@ extension PureXML.Regex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func parse(_ pattern: String) throws -> Node {
|
static func parse(_ pattern: String) throws -> Node {
|
||||||
if pattern.isEmpty { return .empty }
|
if pattern.isEmpty {
|
||||||
|
return .empty
|
||||||
|
}
|
||||||
var parser = RegexParser(pattern)
|
var parser = RegexParser(pattern)
|
||||||
let node = try parser.parseAlternation()
|
let node = try parser.parseAlternation()
|
||||||
guard parser.isAtEnd else { throw RegexError.unbalanced }
|
guard parser.isAtEnd else { throw RegexError.unbalanced }
|
||||||
@ -41,7 +43,9 @@ extension PureXML.Regex {
|
|||||||
while let character = peek(), character != "|", character != ")" {
|
while let character = peek(), character != "|", character != ")" {
|
||||||
try pieces.append(parsePiece())
|
try pieces.append(parsePiece())
|
||||||
}
|
}
|
||||||
if pieces.isEmpty { return .empty }
|
if pieces.isEmpty {
|
||||||
|
return .empty
|
||||||
|
}
|
||||||
return pieces.count == 1 ? pieces[0] : .concat(pieces)
|
return pieces.count == 1 ? pieces[0] : .concat(pieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +79,9 @@ extension PureXML.Regex {
|
|||||||
maximum = peek() == "}" ? nil : parseInteger()
|
maximum = peek() == "}" ? nil : parseInteger()
|
||||||
}
|
}
|
||||||
guard peek() == "}", let minimum else { throw RegexError.badQuantifier }
|
guard peek() == "}", let minimum else { throw RegexError.badQuantifier }
|
||||||
if let maximum, minimum > maximum { throw RegexError.reversedQuantifier }
|
if let maximum, minimum > maximum {
|
||||||
|
throw RegexError.reversedQuantifier
|
||||||
|
}
|
||||||
advance()
|
advance()
|
||||||
return .repeated(atom, min: minimum, max: maximum)
|
return .repeated(atom, min: minimum, max: maximum)
|
||||||
}
|
}
|
||||||
@ -186,7 +192,9 @@ extension PureXML.Regex {
|
|||||||
cls.negated = true
|
cls.negated = true
|
||||||
}
|
}
|
||||||
// A class must have at least one member; `[]` (or `[^]`) is invalid.
|
// A class must have at least one member; `[]` (or `[^]`) is invalid.
|
||||||
if peek() == "]" { throw RegexError.emptyClass }
|
if peek() == "]" {
|
||||||
|
throw RegexError.emptyClass
|
||||||
|
}
|
||||||
while let character = peek(), character != "]" {
|
while let character = peek(), character != "]" {
|
||||||
if character == "-", peek(1) == "[" {
|
if character == "-", peek(1) == "[" {
|
||||||
advance()
|
advance()
|
||||||
@ -236,13 +244,19 @@ extension PureXML.Regex {
|
|||||||
// A nested `[` is legal only as a `-[...]` subtraction, routed by
|
// A nested `[` is legal only as a `-[...]` subtraction, routed by
|
||||||
// parseClass/parseClassMember before reaching here; any other `[`
|
// parseClass/parseClassMember before reaching here; any other `[`
|
||||||
// is an unescaped class bracket, invalid per Appendix F.
|
// is an unescaped class bracket, invalid per Appendix F.
|
||||||
if character == "[" { throw RegexError.unescapedClassBracket }
|
if character == "[" {
|
||||||
|
throw RegexError.unescapedClassBracket
|
||||||
|
}
|
||||||
return .scalar(scalar(of: character))
|
return .scalar(scalar(of: character))
|
||||||
}
|
}
|
||||||
guard let escaped = peek() else { throw RegexError.incompleteEscape }
|
guard let escaped = peek() else { throw RegexError.incompleteEscape }
|
||||||
advance()
|
advance()
|
||||||
if let single = Self.singleEscape(escaped) { return .scalar(single) }
|
if let single = Self.singleEscape(escaped) {
|
||||||
if let member = Self.classEscape(escaped) { return member }
|
return .scalar(single)
|
||||||
|
}
|
||||||
|
if let member = Self.classEscape(escaped) {
|
||||||
|
return member
|
||||||
|
}
|
||||||
if escaped == "p" || escaped == "P" {
|
if escaped == "p" || escaped == "P" {
|
||||||
return try .category(parseCategory(negated: escaped == "P"))
|
return try .category(parseCategory(negated: escaped == "P"))
|
||||||
}
|
}
|
||||||
@ -274,7 +288,9 @@ extension PureXML.Regex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private mutating func advance() {
|
private mutating func advance() {
|
||||||
if index < chars.count { index += 1 }
|
if index < chars.count {
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,7 +121,9 @@ extension PureXML.Regex {
|
|||||||
// removes from that result: `[^cde-[ag]]` is (not c/d/e) minus a/g,
|
// removes from that result: `[^cde-[ag]]` is (not c/d/e) minus a/g,
|
||||||
// i.e. excludes {a,c,d,e,g}, not just {c,d,e} (XSTS RegexTest_430).
|
// i.e. excludes {a,c,d,e,g}, not just {c,d,e} (XSTS RegexTest_430).
|
||||||
var hit = negated ? !inBase : inBase
|
var hit = negated ? !inBase : inBase
|
||||||
if hit, subtraction.contains(where: { $0.matches(scalar) }) { hit = false }
|
if hit, subtraction.contains(where: { $0.matches(scalar) }) {
|
||||||
|
hit = false
|
||||||
|
}
|
||||||
return hit
|
return hit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,9 @@ extension PureXML.Regex {
|
|||||||
]
|
]
|
||||||
|
|
||||||
static func generalCategoryCode(_ scalar: Unicode.Scalar) -> String {
|
static func generalCategoryCode(_ scalar: Unicode.Scalar) -> String {
|
||||||
if let override = categoryOverrides[scalar.value] { return override }
|
if let override = categoryOverrides[scalar.value] {
|
||||||
|
return override
|
||||||
|
}
|
||||||
guard assignedInUnicode32(scalar.value) else { return "Cn" }
|
guard assignedInUnicode32(scalar.value) else { return "Cn" }
|
||||||
return categoryCodes[scalar.properties.generalCategory] ?? "Cn"
|
return categoryCodes[scalar.properties.generalCategory] ?? "Cn"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,9 @@ public extension PureXML.Schema {
|
|||||||
func derives(from other: BuiltinType) -> Bool {
|
func derives(from other: BuiltinType) -> Bool {
|
||||||
var current: BuiltinType? = self
|
var current: BuiltinType? = self
|
||||||
while let type = current {
|
while let type = current {
|
||||||
if type == other { return true }
|
if type == other {
|
||||||
|
return true
|
||||||
|
}
|
||||||
current = type.derivationBase
|
current = type.derivationBase
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -108,8 +110,12 @@ public extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var primitive: Primitive {
|
var primitive: Primitive {
|
||||||
if let primitive = Self.stringLike[self] { return primitive }
|
if let primitive = Self.stringLike[self] {
|
||||||
if Self.integerLike.contains(self) { return .integer }
|
return primitive
|
||||||
|
}
|
||||||
|
if Self.integerLike.contains(self) {
|
||||||
|
return .integer
|
||||||
|
}
|
||||||
return computedPrimitive
|
return computedPrimitive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -121,9 +121,15 @@ public extension PureXML.Schema {
|
|||||||
return .notAbsent
|
return .notAbsent
|
||||||
case let (.notNamespace(name), .enumerated(set)), let (.enumerated(set), .notNamespace(name)):
|
case let (.notNamespace(name), .enumerated(set)), let (.enumerated(set), .notNamespace(name)):
|
||||||
let hasName = set.contains(name), hasAbsent = set.contains("")
|
let hasName = set.contains(name), hasAbsent = set.contains("")
|
||||||
if hasName, hasAbsent { return .any } // E1-10 5.1.1
|
if hasName, hasAbsent {
|
||||||
if hasName { return .notAbsent } // E1-10 5.1.2
|
return .any
|
||||||
if hasAbsent { return nil } // NOT EXPRESSIBLE, E1-10 5.1.3
|
} // E1-10 5.1.1
|
||||||
|
if hasName {
|
||||||
|
return .notAbsent
|
||||||
|
} // E1-10 5.1.2
|
||||||
|
if hasAbsent {
|
||||||
|
return nil
|
||||||
|
} // NOT EXPRESSIBLE, E1-10 5.1.3
|
||||||
return .notNamespace(name) // E1-10 5.1.4
|
return .notNamespace(name) // E1-10 5.1.4
|
||||||
case let (.notAbsent, .enumerated(set)), let (.enumerated(set), .notAbsent):
|
case let (.notAbsent, .enumerated(set)), let (.enumerated(set), .notAbsent):
|
||||||
return set.contains("") ? .any : .notAbsent
|
return set.contains("") ? .any : .notAbsent
|
||||||
@ -131,8 +137,12 @@ public extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func unionProcessContents(_ left: ProcessContents, _ right: ProcessContents) -> ProcessContents {
|
private static func unionProcessContents(_ left: ProcessContents, _ right: ProcessContents) -> ProcessContents {
|
||||||
if left == .strict || right == .strict { return .strict }
|
if left == .strict || right == .strict {
|
||||||
if left == .lax || right == .lax { return .lax }
|
return .strict
|
||||||
|
}
|
||||||
|
if left == .lax || right == .lax {
|
||||||
|
return .lax
|
||||||
|
}
|
||||||
return .skip
|
return .skip
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,8 +201,12 @@ public extension PureXML.Schema {
|
|||||||
// When the two differ the intersection is strictly not expressible; keep
|
// When the two differ the intersection is strictly not expressible; keep
|
||||||
// the less strict so a matched attribute is never validated more harshly
|
// the less strict so a matched attribute is never validated more harshly
|
||||||
// than either source asked for (under-validation, not over-rejection).
|
// than either source asked for (under-validation, not over-rejection).
|
||||||
if left == right { return left }
|
if left == right {
|
||||||
if left == .skip || right == .skip { return .skip }
|
return left
|
||||||
|
}
|
||||||
|
if left == .skip || right == .skip {
|
||||||
|
return .skip
|
||||||
|
}
|
||||||
return .lax
|
return .lax
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -292,7 +306,9 @@ public extension PureXML.Schema {
|
|||||||
|
|
||||||
/// The fixed value, when this is a `fixed` constraint.
|
/// The fixed value, when this is a `fixed` constraint.
|
||||||
public var fixedValue: String? {
|
public var fixedValue: String? {
|
||||||
if case let .fixed(value) = self { return value }
|
if case let .fixed(value) = self {
|
||||||
|
return value
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,9 +23,9 @@ public extension PureXML.Schema {
|
|||||||
/// The named type table that `ElementType.typeReference` resolves against.
|
/// The named type table that `ElementType.typeReference` resolves against.
|
||||||
let types: [String: ElementType]
|
let types: [String: ElementType]
|
||||||
/// Local names of elements declared `nillable="true"`.
|
/// Local names of elements declared `nillable="true"`.
|
||||||
private let nillableElements: Set<String>
|
let nillableElements: Set<String>
|
||||||
/// The `default`/`fixed` value constraint declared on each element name.
|
/// The `default`/`fixed` value constraint declared on each element name.
|
||||||
private let elementConstraints: [String: ValueConstraint]
|
let elementConstraints: [String: ValueConstraint]
|
||||||
/// Complex types declared `abstract="true"`, keyed by namespaced identity
|
/// Complex types declared `abstract="true"`, keyed by namespaced identity
|
||||||
/// (`{ns}local`) so same-local-name types in different namespaces stay distinct.
|
/// (`{ns}local`) so same-local-name types in different namespaces stay distinct.
|
||||||
let abstractTypes: Set<String>
|
let abstractTypes: Set<String>
|
||||||
@ -79,8 +79,21 @@ public extension PureXML.Schema {
|
|||||||
at path: [PureXML.Validation.PathKey] = [],
|
at path: [PureXML.Validation.PathKey] = [],
|
||||||
namespaceBindings inheritedBindings: [String: String] = [:],
|
namespaceBindings inheritedBindings: [String: String] = [:],
|
||||||
) -> [PureXML.Validation.ValidationError] {
|
) -> [PureXML.Validation.ValidationError] {
|
||||||
|
validateTagged(element, against: type, at: path, namespaceBindings: inheritedBindings).map(\.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tagged form of ``validate(_:against:at:namespaceBindings:)``: the same
|
||||||
|
/// computation, with each finding labeled by the decomposed rule it belongs
|
||||||
|
/// to. The public, untagged form is a thin `.map(\.error)` over this so
|
||||||
|
/// behavior and output stay identical for every existing caller.
|
||||||
|
func validateTagged(
|
||||||
|
_ element: PureXML.Model.Element,
|
||||||
|
against type: ComplexType,
|
||||||
|
at path: [PureXML.Validation.PathKey] = [],
|
||||||
|
namespaceBindings inheritedBindings: [String: String] = [:],
|
||||||
|
) -> [XSDFinding] {
|
||||||
let namespaceBindings = Self.namespaceBindings(for: element, inherited: inheritedBindings)
|
let namespaceBindings = Self.namespaceBindings(for: element, inherited: inheritedBindings)
|
||||||
var errors: [XSDFailure] = []
|
var errors: [XSDFinding] = []
|
||||||
validateAttributes(element, type, at: path, into: &errors)
|
validateAttributes(element, type, at: path, into: &errors)
|
||||||
// An xsi:nil element keeps its attribute obligations but must have no
|
// An xsi:nil element keeps its attribute obligations but must have no
|
||||||
// content; its content model is not otherwise checked.
|
// content; its content model is not otherwise checked.
|
||||||
@ -90,68 +103,15 @@ public extension PureXML.Schema {
|
|||||||
validateContent(element, type.content, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
validateContent(element, type.content, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||||
// A simpleContent fixed value is compared in its simple type's value space
|
// A simpleContent fixed value is compared in its simple type's value space
|
||||||
// (cvc-elt.5.2.2.1), e.g. "05" equals an xs:int fixed "5".
|
// (cvc-elt.5.2.2.1), e.g. "05" equals an xs:int fixed "5".
|
||||||
let simpleContentType: SimpleType? = if case let .simpleContent(simple) = type.content { simple } else { nil }
|
let simpleContentType: SimpleType? = if case let .simpleContent(simple) = type.content {
|
||||||
|
simple
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
errors += elementFixedErrors(element, valueType: simpleContentType, at: path)
|
errors += elementFixedErrors(element, valueType: simpleContentType, at: path)
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The errors from an `xsi:nil="true"` element: rejecting it when the
|
|
||||||
/// element is not nillable, or when it carries content. Returns nil when
|
|
||||||
/// the element is not nilled.
|
|
||||||
func nilErrors(_ element: PureXML.Model.Element, fixedValue: String? = nil, at path: XSDPath) -> [XSDFailure]? {
|
|
||||||
let name = element.name.localName
|
|
||||||
// cvc-elt.3.1: a non-nillable element must carry no `xsi:nil` attribute at
|
|
||||||
// all, whatever its value (so `xsi:nil="false"` is equally forbidden, not
|
|
||||||
// only `xsi:nil="true"`).
|
|
||||||
if Self.nilAttributeValue(element) != nil, !nillableElements.contains(name) {
|
|
||||||
return [XSDFailure(reason: "element '\(name)' is not nillable but has xsi:nil", at: path)]
|
|
||||||
}
|
|
||||||
guard Self.isNil(element) else { return nil }
|
|
||||||
if Self.hasContent(element) {
|
|
||||||
return [XSDFailure(reason: "nil element '\(name)' must be empty", at: path)]
|
|
||||||
}
|
|
||||||
// cvc-elt.3.2.2: a nilled element may not have a fixed {value constraint}.
|
|
||||||
// The matched particle's constraint is authoritative; the by-local-name map
|
|
||||||
// is a fallback for paths without a particle (e.g. the resolved-type entry).
|
|
||||||
if (fixedValue ?? elementConstraints[name]?.fixedValue) != nil {
|
|
||||||
return [XSDFailure(reason: "nil element '\(name)' must not have a fixed value constraint", at: path)]
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The text an element's simple content is validated against: its character
|
|
||||||
/// content, or, when the element is empty, its `default`/`fixed` value (an
|
|
||||||
/// empty element takes that value, and the value, not the empty string, must
|
|
||||||
/// satisfy the type). The matched particle's constraint takes precedence over
|
|
||||||
/// the by-local-name `elementConstraints` map, exactly as the tree path's
|
|
||||||
/// `validateChild` resolves it.
|
|
||||||
func effectiveSimpleText(for element: PureXML.Model.Element, particleConstraint: ValueConstraint? = nil) -> String {
|
|
||||||
let text = Self.rawTextContent(element)
|
|
||||||
let constraint = particleConstraint ?? elementConstraints[element.name.localName]
|
|
||||||
return text.isEmpty ? (constraint?.value ?? text) : text
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The error from a `fixed` element value constraint: the element's text
|
|
||||||
/// must equal the fixed value in the element type's value space when known.
|
|
||||||
func elementFixedErrors(
|
|
||||||
_ element: PureXML.Model.Element,
|
|
||||||
valueType: SimpleType? = nil,
|
|
||||||
particleFixed: String? = nil,
|
|
||||||
at path: XSDPath,
|
|
||||||
) -> [XSDFailure] {
|
|
||||||
guard let fixed = particleFixed ?? elementConstraints[element.name.localName]?.fixedValue else { return [] }
|
|
||||||
// A fixed element's content is its fixed character value: it may not carry
|
|
||||||
// element children, even under a mixed content type (cvc-elt.5.2.2.2.1).
|
|
||||||
if !element.children.compactMap(\.element).isEmpty {
|
|
||||||
return [XSDFailure(reason: "fixed element '\(element.name.localName)' must not have element children", at: path)]
|
|
||||||
}
|
|
||||||
let text = Self.rawTextContent(element)
|
|
||||||
// An empty element takes the fixed value; only present content must equal it.
|
|
||||||
if text.isEmpty { return [] }
|
|
||||||
if let valueType, valueType.valueMatches(text, literal: fixed) { return [] }
|
|
||||||
return text == fixed ? [] : [XSDFailure(reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates `element` against any element type at `path`, resolving a
|
/// Validates `element` against any element type at `path`, resolving a
|
||||||
/// `typeReference` through the type table.
|
/// `typeReference` through the type table.
|
||||||
public func validate(
|
public func validate(
|
||||||
@ -159,7 +119,16 @@ public extension PureXML.Schema {
|
|||||||
as type: ElementType,
|
as type: ElementType,
|
||||||
at path: [PureXML.Validation.PathKey] = [],
|
at path: [PureXML.Validation.PathKey] = [],
|
||||||
) -> [PureXML.Validation.ValidationError] {
|
) -> [PureXML.Validation.ValidationError] {
|
||||||
var errors: [XSDFailure] = []
|
validateTagged(element, as: type, at: path).map(\.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tagged form of ``validate(_:as:at:)``. See ``validateTagged(_:against:at:namespaceBindings:)``.
|
||||||
|
func validateTagged(
|
||||||
|
_ element: PureXML.Model.Element,
|
||||||
|
as type: ElementType,
|
||||||
|
at path: [PureXML.Validation.PathKey] = [],
|
||||||
|
) -> [XSDFinding] {
|
||||||
|
var errors: [XSDFinding] = []
|
||||||
let namespaceBindings = Self.namespaceBindings(for: element)
|
let namespaceBindings = Self.namespaceBindings(for: element)
|
||||||
validateChild(element, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
validateChild(element, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||||
return errors
|
return errors
|
||||||
@ -172,17 +141,21 @@ public extension PureXML.Schema {
|
|||||||
_ content: ContentType,
|
_ content: ContentType,
|
||||||
at path: XSDPath,
|
at path: XSDPath,
|
||||||
namespaceBindings: [String: String],
|
namespaceBindings: [String: String],
|
||||||
into errors: inout [XSDFailure],
|
into errors: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
let children = element.children.compactMap(\.element)
|
let children = element.children.compactMap(\.element)
|
||||||
switch content {
|
switch content {
|
||||||
case .empty:
|
case .empty:
|
||||||
if !children.isEmpty { errors.append(XSDFailure(reason: "element must be empty", at: path)) }
|
if !children.isEmpty {
|
||||||
|
errors.append(XSDFinding(.emptyContentForbidsChildren, reason: "element must be empty", at: path))
|
||||||
|
}
|
||||||
rejectText(element, at: path, into: &errors)
|
rejectText(element, at: path, into: &errors)
|
||||||
case let .simpleContent(type):
|
case let .simpleContent(type):
|
||||||
if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) }
|
if !children.isEmpty {
|
||||||
|
errors.append(XSDFinding(.simpleContentForbidsChildren, reason: "element must not have children", at: path))
|
||||||
|
}
|
||||||
if let error = type.validate(Self.rawTextContent(element)) {
|
if let error = type.validate(Self.rawTextContent(element)) {
|
||||||
errors.append(XSDFailure(reason: "content: \(error)", at: path))
|
errors.append(XSDFinding(.simpleContentTypeValid, reason: "content: \(error)", at: path))
|
||||||
}
|
}
|
||||||
case let .elementOnly(particle):
|
case let .elementOnly(particle):
|
||||||
rejectText(element, at: path, into: &errors)
|
rejectText(element, at: path, into: &errors)
|
||||||
@ -192,9 +165,9 @@ public extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFailure]) {
|
func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFinding]) {
|
||||||
guard !Self.textContent(element).isEmpty else { return }
|
guard !Self.textContent(element).isEmpty else { return }
|
||||||
errors.append(XSDFailure(reason: "element must not contain text", at: path))
|
errors.append(XSDFinding(.textForbidden, reason: "element must not contain text", at: path))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Particles
|
// MARK: Particles
|
||||||
@ -204,7 +177,7 @@ public extension PureXML.Schema {
|
|||||||
children: [PureXML.Model.Element],
|
children: [PureXML.Model.Element],
|
||||||
at path: XSDPath,
|
at path: XSDPath,
|
||||||
namespaceBindings: [String: String],
|
namespaceBindings: [String: String],
|
||||||
into errors: inout [XSDFailure],
|
into errors: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
// Locate each content-model violation at the offending child (or the
|
// Locate each content-model violation at the offending child (or the
|
||||||
// missing one), with a recovery hint, rather than one opaque failure,
|
// missing one), with a recovery hint, rather than one opaque failure,
|
||||||
@ -232,7 +205,7 @@ public extension PureXML.Schema {
|
|||||||
_ children: [PureXML.Model.Element],
|
_ children: [PureXML.Model.Element],
|
||||||
matched: [PureXML.Schema.MatchedParticle?],
|
matched: [PureXML.Schema.MatchedParticle?],
|
||||||
frame: XSDChildValidationFrame,
|
frame: XSDChildValidationFrame,
|
||||||
into errors: inout [XSDFailure],
|
into errors: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
let steps = Self.childSteps(children)
|
let steps = Self.childSteps(children)
|
||||||
for (index, child) in children.enumerated() {
|
for (index, child) in children.enumerated() {
|
||||||
@ -254,60 +227,13 @@ public extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates a wildcard-matched child by its `processContents`: skip does
|
func validateChild(
|
||||||
/// nothing, lax validates against a global declaration when one exists, and
|
|
||||||
/// strict requires one.
|
|
||||||
private func globalElementDeclaration(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
|
|
||||||
// A global element declaration is always in the schema's target
|
|
||||||
// namespace, so an instance element matches one only by full qualified
|
|
||||||
// name. The namespaced key already covers a no-namespace global (its key
|
|
||||||
// is `element:{}local`); matching an unqualified instance against a
|
|
||||||
// namespaced global by bare local name would conflate namespaces, e.g.
|
|
||||||
// accepting unqualified text where a namespaced `xs:short` is declared.
|
|
||||||
types[PureXML.Schema.XSDParser.elementDeclarationKey(name)]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func validateWildcardChild(
|
|
||||||
_ child: PureXML.Model.Element,
|
|
||||||
processContents: ProcessContents,
|
|
||||||
at path: XSDPath,
|
|
||||||
namespaceBindings: [String: String],
|
|
||||||
into errors: inout [XSDFailure],
|
|
||||||
) {
|
|
||||||
switch processContents {
|
|
||||||
case .skip:
|
|
||||||
return
|
|
||||||
case .lax:
|
|
||||||
if let declaration = globalElementDeclaration(for: child.name) {
|
|
||||||
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
||||||
}
|
|
||||||
case .strict:
|
|
||||||
if let declaration = globalElementDeclaration(for: child.name) {
|
|
||||||
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
||||||
} else if let type = xsiDeclaredType(for: child, namespaceBindings: namespaceBindings) {
|
|
||||||
validateChild(child, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
||||||
} else {
|
|
||||||
errors.append(XSDFailure(reason: "no declaration for wildcard-matched element '\(child.name.localName)'", at: path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The type named by a wildcard-matched element's `xsi:type`, when present.
|
|
||||||
private func xsiDeclaredType(for child: PureXML.Model.Element, namespaceBindings: [String: String]) -> ElementType? {
|
|
||||||
guard Self.xsiTypeAttributeValue(child) != nil else { return nil }
|
|
||||||
guard let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
|
|
||||||
let type = Self.resolveNamedType(reference, in: types)
|
|
||||||
else { return nil }
|
|
||||||
return type
|
|
||||||
}
|
|
||||||
|
|
||||||
private func validateChild(
|
|
||||||
_ child: PureXML.Model.Element,
|
_ child: PureXML.Model.Element,
|
||||||
against declared: ElementType,
|
against declared: ElementType,
|
||||||
at path: XSDPath,
|
at path: XSDPath,
|
||||||
namespaceBindings: [String: String],
|
namespaceBindings: [String: String],
|
||||||
particleConstraint: ValueConstraint? = nil,
|
particleConstraint: ValueConstraint? = nil,
|
||||||
into errors: inout [XSDFailure],
|
into errors: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
// An instance `xsi:type` overrides the declared type, provided the named
|
// An instance `xsi:type` overrides the declared type, provided the named
|
||||||
// type exists in the schema. Resolve to the type itself rather than a
|
// type exists in the schema. Resolve to the type itself rather than a
|
||||||
@ -315,7 +241,7 @@ public extension PureXML.Schema {
|
|||||||
// the declared type can forbid the substitution; an abstract declared
|
// the declared type can forbid the substitution; an abstract declared
|
||||||
// type requires one.
|
// type requires one.
|
||||||
if let xsiError = xsiTypeOverrideError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
|
if let xsiError = xsiTypeOverrideError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
|
||||||
errors.append(xsiError)
|
errors.append(XSDFinding(.xsiTypeSubstitutionValid, xsiError))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let declared = overriddenType(declared, for: child, namespaceBindings: namespaceBindings, at: path, into: &errors) else { return }
|
guard let declared = overriddenType(declared, for: child, namespaceBindings: namespaceBindings, at: path, into: &errors) else { return }
|
||||||
@ -328,26 +254,30 @@ public extension PureXML.Schema {
|
|||||||
// violation. Checked before the nil short-circuit, since an
|
// violation. Checked before the nil short-circuit, since an
|
||||||
// xsi:nil element is equally bound by it.
|
// xsi:nil element is equally bound by it.
|
||||||
for attribute in child.attributes where !Self.isNamespaceDeclaration(attribute) && !Self.isSchemaInstanceAttribute(attribute) {
|
for attribute in child.attributes where !Self.isNamespaceDeclaration(attribute) && !Self.isSchemaInstanceAttribute(attribute) {
|
||||||
errors.append(XSDFailure(reason: "'\(child.name.localName)' has a simple type and must not carry attribute '\(attribute.name.localName)'", at: path))
|
errors.append(XSDFinding(
|
||||||
|
.simpleTypeDisallowedAttribute,
|
||||||
|
reason: "'\(child.name.localName)' has a simple type and must not carry attribute '\(attribute.name.localName)'",
|
||||||
|
at: path,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
if let nilErrors = nilErrors(child, fixedValue: particleConstraint?.fixedValue, at: path) {
|
if let nilErrors = nilErrors(child, fixedValue: particleConstraint?.fixedValue, at: path) {
|
||||||
errors += nilErrors
|
errors += nilErrors
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !child.children.compactMap(\.element).isEmpty {
|
if !child.children.compactMap(\.element).isEmpty {
|
||||||
errors.append(XSDFailure(reason: "'\(child.name.localName)' must not have children", at: path))
|
errors.append(XSDFinding(.simpleTypeForbidsChildren, reason: "'\(child.name.localName)' must not have children", at: path))
|
||||||
}
|
}
|
||||||
// An empty element takes its `default`/`fixed` value; that value
|
// An empty element takes its `default`/`fixed` value; that value
|
||||||
// (not the empty string) is what must be valid against the type,
|
// (not the empty string) is what must be valid against the type,
|
||||||
// including any xsi:type override already resolved into `simple`.
|
// including any xsi:type override already resolved into `simple`.
|
||||||
let text = Self.rawTextContent(child)
|
let text = Self.rawTextContent(child)
|
||||||
if let error = simple.validate(effectiveSimpleText(for: child, particleConstraint: particleConstraint)) {
|
if let error = simple.validate(effectiveSimpleText(for: child, particleConstraint: particleConstraint)) {
|
||||||
errors.append(XSDFailure(reason: "'\(child.name.localName)': \(error)", at: path))
|
errors.append(XSDFinding(.simpleTypeValueValid, reason: "'\(child.name.localName)': \(error)", at: path))
|
||||||
}
|
}
|
||||||
recordIDs(simple, value: text, at: path)
|
recordIDs(simple, value: text, at: path)
|
||||||
errors += elementFixedErrors(child, valueType: simple, particleFixed: particleConstraint?.fixedValue, at: path)
|
errors += elementFixedErrors(child, valueType: simple, particleFixed: particleConstraint?.fixedValue, at: path)
|
||||||
case let .complex(complex):
|
case let .complex(complex):
|
||||||
errors.append(contentsOf: validate(child, against: complex, at: path, namespaceBindings: namespaceBindings))
|
errors.append(contentsOf: validateTagged(child, against: complex, at: path, namespaceBindings: namespaceBindings))
|
||||||
case .typeReference:
|
case .typeReference:
|
||||||
validateResolvedReference(declared, child, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
validateResolvedReference(declared, child, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||||
}
|
}
|
||||||
@ -360,13 +290,13 @@ public extension PureXML.Schema {
|
|||||||
_ child: PureXML.Model.Element,
|
_ child: PureXML.Model.Element,
|
||||||
at path: XSDPath,
|
at path: XSDPath,
|
||||||
namespaceBindings: [String: String],
|
namespaceBindings: [String: String],
|
||||||
into errors: inout [XSDFailure],
|
into errors: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
switch resolveReference(declared) {
|
switch resolveReference(declared) {
|
||||||
case let .unknown(name):
|
case let .unknown(name):
|
||||||
errors.append(XSDFailure(reason: "unknown type '\(name)'", at: path))
|
errors.append(XSDFinding(.typeReferenceResolves, reason: "unknown type '\(name)'", at: path))
|
||||||
case let .circular(name):
|
case let .circular(name):
|
||||||
errors.append(XSDFailure(reason: "circular type reference '\(name)'", at: path))
|
errors.append(XSDFinding(.typeReferenceResolves, reason: "circular type reference '\(name)'", at: path))
|
||||||
case let .resolved(resolved):
|
case let .resolved(resolved):
|
||||||
validateChild(child, against: resolved, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
validateChild(child, against: resolved, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,21 +3,21 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
_ element: PureXML.Model.Element,
|
_ element: PureXML.Model.Element,
|
||||||
_ type: PureXML.Schema.ComplexType,
|
_ type: PureXML.Schema.ComplexType,
|
||||||
at path: [PureXML.Validation.PathKey],
|
at path: [PureXML.Validation.PathKey],
|
||||||
into errors: inout [PureXML.Validation.ValidationError],
|
into errors: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
let present = element.attributes.filter { !Self.isNamespaceDeclaration($0) && !Self.isSchemaInstanceAttribute($0) }
|
let present = element.attributes.filter { !Self.isNamespaceDeclaration($0) && !Self.isSchemaInstanceAttribute($0) }
|
||||||
for use in type.attributes where !use.prohibited {
|
for use in type.attributes where !use.prohibited {
|
||||||
let match = present.first { Self.attributeMatches($0.name, declared: use.name, use: use, on: element.name) }
|
let match = present.first { Self.attributeMatches($0.name, declared: use.name, use: use, on: element.name) }
|
||||||
if let match {
|
if let match {
|
||||||
if let error = use.type.validate(match.value) {
|
if let error = use.type.validate(match.value) {
|
||||||
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(use.name.localName)': \(error)", at: path))
|
errors.append(XSDFinding(.attributeTypeValid, reason: "attribute '\(use.name.localName)': \(error)", at: path))
|
||||||
}
|
}
|
||||||
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(match.value, literal: fixed) {
|
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(match.value, literal: fixed) {
|
||||||
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
errors.append(XSDFinding(.attributeFixedMatches, reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
||||||
}
|
}
|
||||||
recordIDs(use.type, value: match.value, at: path)
|
recordIDs(use.type, value: match.value, at: path)
|
||||||
} else if use.required {
|
} else if use.required {
|
||||||
errors.append(PureXML.Validation.ValidationError(reason: "missing required attribute '\(use.name.localName)'", at: path))
|
errors.append(XSDFinding(.attributeRequiredPresent, reason: "missing required attribute '\(use.name.localName)'", at: path))
|
||||||
} else if let constraint = use.valueConstraint {
|
} else if let constraint = use.valueConstraint {
|
||||||
// An absent attribute takes its `default`/`fixed` value, so a
|
// An absent attribute takes its `default`/`fixed` value, so a
|
||||||
// defaulted or fixed IDREF/IDREFS must still resolve to a matching
|
// defaulted or fixed IDREF/IDREFS must still resolve to a matching
|
||||||
@ -28,7 +28,8 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
}
|
}
|
||||||
validateWildcardAttributes(present, element: element, type: type, at: path, into: &errors)
|
validateWildcardAttributes(present, element: element, type: type, at: path, into: &errors)
|
||||||
if idTypedAttributeCount(present, type, on: element) > 1 {
|
if idTypedAttributeCount(present, type, on: element) > 1 {
|
||||||
errors.append(PureXML.Validation.ValidationError(
|
errors.append(XSDFinding(
|
||||||
|
.attributeSingleID,
|
||||||
reason: "an element may carry at most one attribute of type ID",
|
reason: "an element may carry at most one attribute of type ID",
|
||||||
at: path,
|
at: path,
|
||||||
))
|
))
|
||||||
@ -45,7 +46,9 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
on element: PureXML.Model.Element,
|
on element: PureXML.Model.Element,
|
||||||
) -> Int {
|
) -> Int {
|
||||||
present.reduce(into: 0) { count, attribute in
|
present.reduce(into: 0) { count, attribute in
|
||||||
if isIDTyped(attribute, type, on: element) { count += 1 }
|
if isIDTyped(attribute, type, on: element) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +76,9 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
use: PureXML.Schema.AttributeUse,
|
use: PureXML.Schema.AttributeUse,
|
||||||
on elementName: PureXML.Model.QualifiedName,
|
on elementName: PureXML.Model.QualifiedName,
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
if sameName(instance, declared) { return true }
|
if sameName(instance, declared) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
guard use.chameleonUnprefixed,
|
guard use.chameleonUnprefixed,
|
||||||
instance.namespaceURI == nil || instance.namespaceURI?.isEmpty == true,
|
instance.namespaceURI == nil || instance.namespaceURI?.isEmpty == true,
|
||||||
let declaredNamespace = declared.namespaceURI, !declaredNamespace.isEmpty
|
let declaredNamespace = declared.namespaceURI, !declaredNamespace.isEmpty
|
||||||
@ -86,7 +91,7 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
element: PureXML.Model.Element,
|
element: PureXML.Model.Element,
|
||||||
type: PureXML.Schema.ComplexType,
|
type: PureXML.Schema.ComplexType,
|
||||||
at path: [PureXML.Validation.PathKey],
|
at path: [PureXML.Validation.PathKey],
|
||||||
into errors: inout [PureXML.Validation.ValidationError],
|
into errors: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
// A `use="prohibited"` use is excluded from the effective {attribute uses},
|
// A `use="prohibited"` use is excluded from the effective {attribute uses},
|
||||||
// so a prohibited attribute is treated as undeclared and left to the
|
// so a prohibited attribute is treated as undeclared and left to the
|
||||||
@ -96,7 +101,7 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
let wildcard = type.attributeWildcard
|
let wildcard = type.attributeWildcard
|
||||||
for attribute in present where !declared.contains(where: { Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: elementName) }) {
|
for attribute in present where !declared.contains(where: { Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: elementName) }) {
|
||||||
guard let wildcard, wildcard.admits(attribute.name) else {
|
guard let wildcard, wildcard.admits(attribute.name) else {
|
||||||
errors.append(PureXML.Validation.ValidationError(reason: "undeclared attribute '\(attribute.name.localName)'", at: path))
|
errors.append(XSDFinding(.attributeDeclaredOrPermitted, reason: "undeclared attribute '\(attribute.name.localName)'", at: path))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch wildcard.processContents {
|
switch wildcard.processContents {
|
||||||
@ -108,7 +113,8 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
}
|
}
|
||||||
case .strict:
|
case .strict:
|
||||||
guard let use = globalAttributeUse(for: attribute.name) else {
|
guard let use = globalAttributeUse(for: attribute.name) else {
|
||||||
errors.append(PureXML.Validation.ValidationError(
|
errors.append(XSDFinding(
|
||||||
|
.wildcardAttributeUndeclared,
|
||||||
reason: "no declaration for wildcard-matched attribute '\(attribute.name.localName)'",
|
reason: "no declaration for wildcard-matched attribute '\(attribute.name.localName)'",
|
||||||
at: path,
|
at: path,
|
||||||
))
|
))
|
||||||
@ -127,13 +133,13 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
_ attribute: PureXML.Model.Attribute,
|
_ attribute: PureXML.Model.Attribute,
|
||||||
use: PureXML.Schema.AttributeUse,
|
use: PureXML.Schema.AttributeUse,
|
||||||
at path: [PureXML.Validation.PathKey],
|
at path: [PureXML.Validation.PathKey],
|
||||||
into errors: inout [PureXML.Validation.ValidationError],
|
into errors: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
if let error = use.type.validate(attribute.value) {
|
if let error = use.type.validate(attribute.value) {
|
||||||
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(attribute.name.localName)': \(error)", at: path))
|
errors.append(XSDFinding(.wildcardAttributeTypeValid, reason: "attribute '\(attribute.name.localName)': \(error)", at: path))
|
||||||
}
|
}
|
||||||
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(attribute.value, literal: fixed) {
|
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(attribute.value, literal: fixed) {
|
||||||
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(attribute.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
errors.append(XSDFinding(.wildcardAttributeFixedMatches, reason: "attribute '\(attribute.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
||||||
}
|
}
|
||||||
recordIDs(use.type, value: attribute.value, at: path)
|
recordIDs(use.type, value: attribute.value, at: path)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
extension PureXML.Schema.ComplexValidator {
|
extension PureXML.Schema.ComplexValidator {
|
||||||
/// Walks the children through the content automaton, flagging the first child
|
/// Walks the children through the content automaton, flagging the first child
|
||||||
/// the follow-set rejects, or the missing content when the sequence ends early.
|
/// the follow-set rejects, or the missing content when the sequence ends early.
|
||||||
func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFinding]) {
|
||||||
let matcher = PureXML.Schema.ContentRTNMatcher(PureXML.Schema.ContentRTNBuilder.build(particle))
|
let matcher = PureXML.Schema.ContentRTNMatcher(PureXML.Schema.ContentRTNBuilder.build(particle))
|
||||||
let steps = Self.childSteps(children)
|
let steps = Self.childSteps(children)
|
||||||
// Advance one active state-set across the children rather than re-walking
|
// Advance one active state-set across the children rather than re-walking
|
||||||
@ -14,19 +14,23 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
for (index, child) in children.enumerated() {
|
for (index, child) in children.enumerated() {
|
||||||
guard let next = matcher.step(current, over: child.name) else {
|
guard let next = matcher.step(current, over: child.name) else {
|
||||||
let allowed = matcher.admissible(from: current)
|
let allowed = matcher.admissible(from: current)
|
||||||
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here\(Self.expectation(allowed))", at: path + [steps[index]]))
|
errors.append(XSDFinding(
|
||||||
|
.contentModelStructure,
|
||||||
|
reason: "element '\(child.name.localName)' is not allowed here\(Self.expectation(allowed))",
|
||||||
|
at: path + [steps[index]],
|
||||||
|
))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
current = next
|
current = next
|
||||||
}
|
}
|
||||||
if !matcher.isComplete(current) {
|
if !matcher.isComplete(current) {
|
||||||
errors.append(XSDFailure(reason: "content is incomplete\(Self.expectation(matcher.admissible(from: current)))", at: path))
|
errors.append(XSDFinding(.contentModelStructure, reason: "content is incomplete\(Self.expectation(matcher.admissible(from: current)))", at: path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Locates `all`-group violations: each child that is not an in-bounds member,
|
/// Locates `all`-group violations: each child that is not an in-bounds member,
|
||||||
/// recovering past it, then each required member that never appeared.
|
/// recovering past it, then each required member that never appeared.
|
||||||
func allStructureErrors(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element], groupOptional: Bool = false, at path: XSDPath, into errors: inout [XSDFailure]) {
|
func allStructureErrors(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element], groupOptional: Bool = false, at path: XSDPath, into errors: inout [XSDFinding]) {
|
||||||
var counts = [Int](repeating: 0, count: group.particles.count)
|
var counts = [Int](repeating: 0, count: group.particles.count)
|
||||||
let steps = Self.childSteps(children)
|
let steps = Self.childSteps(children)
|
||||||
for (index, child) in children.enumerated() {
|
for (index, child) in children.enumerated() {
|
||||||
@ -35,7 +39,7 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
let room = member.maxOccurs.map { counts[slot] < $0 } ?? true
|
let room = member.maxOccurs.map { counts[slot] < $0 } ?? true
|
||||||
return room && Self.memberMatches(member.term, child.name)
|
return room && Self.memberMatches(member.term, child.name)
|
||||||
}) else {
|
}) else {
|
||||||
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here", at: path + [steps[index]]))
|
errors.append(XSDFinding(.contentModelStructure, reason: "element '\(child.name.localName)' is not allowed here", at: path + [steps[index]]))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
counts[position] += 1
|
counts[position] += 1
|
||||||
@ -44,10 +48,12 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
// absent (zero occurrences), so its required members are not expected; but
|
// absent (zero occurrences), so its required members are not expected; but
|
||||||
// once any child appears the group is present once and every member's own
|
// once any child appears the group is present once and every member's own
|
||||||
// `minOccurs` applies. A required group (`minOccurs="1"`) always enforces.
|
// `minOccurs` applies. A required group (`minOccurs="1"`) always enforces.
|
||||||
if groupOptional, children.isEmpty { return }
|
if groupOptional, children.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
for (index, member) in group.particles.enumerated() where counts[index] < member.minOccurs {
|
for (index, member) in group.particles.enumerated() where counts[index] < member.minOccurs {
|
||||||
if case let .element(name, _, _, _, _, _) = member.term {
|
if case let .element(name, _, _, _, _, _) = member.term {
|
||||||
errors.append(XSDFailure(reason: "element '\(name.localName)' is required but missing", at: path))
|
errors.append(XSDFinding(.contentModelStructure, reason: "element '\(name.localName)' is required but missing", at: path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,7 +98,9 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
/// A "; expected a, b" hint naming the elements the automaton accepts next.
|
/// A "; expected a, b" hint naming the elements the automaton accepts next.
|
||||||
static func expectation(_ labels: [PureXML.Schema.TermLabel]) -> String {
|
static func expectation(_ labels: [PureXML.Schema.TermLabel]) -> String {
|
||||||
let names = labels.compactMap { label -> String? in
|
let names = labels.compactMap { label -> String? in
|
||||||
if case let .name(qualified) = label { return "<\(qualified.localName)>" }
|
if case let .name(qualified) = label {
|
||||||
|
return "<\(qualified.localName)>"
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let unique = Set(names).sorted()
|
let unique = Set(names).sorted()
|
||||||
|
|||||||
@ -14,12 +14,24 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
at path: [PureXML.Validation.PathKey],
|
at path: [PureXML.Validation.PathKey],
|
||||||
namespaceBindings: [String: String],
|
namespaceBindings: [String: String],
|
||||||
) -> PureXML.Validation.ValidationError? {
|
) -> PureXML.Validation.ValidationError? {
|
||||||
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return blocked }
|
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
|
||||||
if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return notDerived }
|
return blocked
|
||||||
if let anyTypeSub = anyTypeForAnySimpleTypeError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return anyTypeSub }
|
}
|
||||||
if let listUnion = listUnionSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return listUnion }
|
if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
|
||||||
if let abstractType = abstractXsiTypeError(child: child, at: path, namespaceBindings: namespaceBindings) { return abstractType }
|
return notDerived
|
||||||
if case let .typeReference(name) = declared { return abstractTypeError(named: name, child: child, at: path) }
|
}
|
||||||
|
if let anyTypeSub = anyTypeForAnySimpleTypeError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
|
||||||
|
return anyTypeSub
|
||||||
|
}
|
||||||
|
if let listUnion = listUnionSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
|
||||||
|
return listUnion
|
||||||
|
}
|
||||||
|
if let abstractType = abstractXsiTypeError(child: child, at: path, namespaceBindings: namespaceBindings) {
|
||||||
|
return abstractType
|
||||||
|
}
|
||||||
|
if case let .typeReference(name) = declared {
|
||||||
|
return abstractTypeError(named: name, child: child, at: path)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +111,9 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
case .simple, .complex: declared
|
case .simple, .complex: declared
|
||||||
case let .typeReference(reference): resolvedDeclaredType(reference)
|
case let .typeReference(reference): resolvedDeclaredType(reference)
|
||||||
}
|
}
|
||||||
if case let .simple(type)? = resolved { return type.isAnySimpleType }
|
if case let .simple(type)? = resolved {
|
||||||
|
return type.isAnySimpleType
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +171,9 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
|
|
||||||
/// Whether an element type resolves to a complex type.
|
/// Whether an element type resolves to a complex type.
|
||||||
static func isComplexType(_ type: PureXML.Schema.ElementType) -> Bool {
|
static func isComplexType(_ type: PureXML.Schema.ElementType) -> Bool {
|
||||||
if case .complex = type { return true }
|
if case .complex = type {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,13 +253,17 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
for member in members {
|
for member in members {
|
||||||
// The substitute IS a member: derived with no method of its own, so
|
// The substitute IS a member: derived with no method of its own, so
|
||||||
// this path is unblocked and the substitution is permitted outright.
|
// this path is unblocked and the substitution is permitted outright.
|
||||||
if member == substitute { return false }
|
if member == substitute {
|
||||||
|
return false
|
||||||
|
}
|
||||||
guard let toMember = PureXML.Schema.XSDParser.derivationMethods(from: substitute, to: member, typeDerivation) else {
|
guard let toMember = PureXML.Schema.XSDParser.derivationMethods(from: substitute, to: member, typeDerivation) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
anyConfirmedPath = true
|
anyConfirmedPath = true
|
||||||
// An unblocked path to any member makes the whole substitution valid.
|
// An unblocked path to any member makes the whole substitution valid.
|
||||||
if toMember.isDisjoint(with: blocked) { return false }
|
if toMember.isDisjoint(with: blocked) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Every confirmed member path is blocked (and at least one exists): bar it.
|
// Every confirmed member path is blocked (and at least one exists): bar it.
|
||||||
return anyConfirmedPath
|
return anyConfirmedPath
|
||||||
@ -325,8 +345,12 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
/// namespaced backbone and the check stays silent rather than risk a false
|
/// namespaced backbone and the check stays silent rather than risk a false
|
||||||
/// positive.
|
/// positive.
|
||||||
static func derivationKey(fromReference reference: String) -> String {
|
static func derivationKey(fromReference reference: String) -> String {
|
||||||
if reference.hasPrefix("type:") { return String(reference.dropFirst("type:".count)) }
|
if reference.hasPrefix("type:") {
|
||||||
if reference.hasPrefix("{") || reference.contains(":") { return reference }
|
return String(reference.dropFirst("type:".count))
|
||||||
|
}
|
||||||
|
if reference.hasPrefix("{") || reference.contains(":") {
|
||||||
|
return reference
|
||||||
|
}
|
||||||
return key(PureXML.Model.QualifiedName(localName: reference, namespaceURI: nil))
|
return key(PureXML.Model.QualifiedName(localName: reference, namespaceURI: nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,9 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
return wildcard.processContents
|
return wildcard.processContents
|
||||||
case let .group(group):
|
case let .group(group):
|
||||||
for member in group.particles {
|
for member in group.particles {
|
||||||
if let found = wildcard(in: member.term) { return found }
|
if let found = wildcard(in: member.term) {
|
||||||
|
return found
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case .element:
|
case .element:
|
||||||
@ -39,7 +41,9 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
fileprivate static func collectTypes(_ term: XSDTerm, into result: inout [String: XSDElementType]) {
|
fileprivate static func collectTypes(_ term: XSDTerm, into result: inout [String: XSDElementType]) {
|
||||||
switch term {
|
switch term {
|
||||||
case let .element(name, type, _, _, _, _):
|
case let .element(name, type, _, _, _, _):
|
||||||
if let type { result[key(name)] = type }
|
if let type {
|
||||||
|
result[key(name)] = type
|
||||||
|
}
|
||||||
case let .group(group):
|
case let .group(group):
|
||||||
for member in group.particles {
|
for member in group.particles {
|
||||||
collectTypes(member.term, into: &result)
|
collectTypes(member.term, into: &result)
|
||||||
@ -62,7 +66,9 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
fileprivate static func collectValueConstraints(_ term: XSDTerm, into result: inout [String: PureXML.Schema.ValueConstraint]) {
|
fileprivate static func collectValueConstraints(_ term: XSDTerm, into result: inout [String: PureXML.Schema.ValueConstraint]) {
|
||||||
switch term {
|
switch term {
|
||||||
case let .element(name, _, _, valueConstraint, _, _):
|
case let .element(name, _, _, valueConstraint, _, _):
|
||||||
if let valueConstraint { result[key(name)] = valueConstraint }
|
if let valueConstraint {
|
||||||
|
result[key(name)] = valueConstraint
|
||||||
|
}
|
||||||
case let .group(group):
|
case let .group(group):
|
||||||
for member in group.particles {
|
for member in group.particles {
|
||||||
collectValueConstraints(member.term, into: &result)
|
collectValueConstraints(member.term, into: &result)
|
||||||
@ -200,7 +206,13 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
|
|
||||||
/// Whether the element has any child element or non-whitespace text.
|
/// Whether the element has any child element or non-whitespace text.
|
||||||
static func hasContent(_ element: PureXML.Model.Element) -> Bool {
|
static func hasContent(_ element: PureXML.Model.Element) -> Bool {
|
||||||
!textContent(element).isEmpty || element.children.contains { if case .element = $0 { true } else { false } }
|
!textContent(element).isEmpty || element.children.contains {
|
||||||
|
if case .element = $0 {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +248,9 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
|
|
||||||
/// Resolves a named type reference, including built-in datatypes and ur-types.
|
/// Resolves a named type reference, including built-in datatypes and ur-types.
|
||||||
static func resolveNamedType(_ name: String, in types: [String: PureXML.Schema.ElementType]) -> PureXML.Schema.ElementType? {
|
static func resolveNamedType(_ name: String, in types: [String: PureXML.Schema.ElementType]) -> PureXML.Schema.ElementType? {
|
||||||
if let next = types[name] { return next }
|
if let next = types[name] {
|
||||||
|
return next
|
||||||
|
}
|
||||||
if name.hasPrefix("type:"), let (uri, local) = parseQualifiedTypeKey(name) {
|
if name.hasPrefix("type:"), let (uri, local) = parseQualifiedTypeKey(name) {
|
||||||
if uri == PureXML.Schema.XSDParser.xsdNamespace, let builtin = urOrBuiltinType(named: local) {
|
if uri == PureXML.Schema.XSDParser.xsdNamespace, let builtin = urOrBuiltinType(named: local) {
|
||||||
return builtin
|
return builtin
|
||||||
|
|||||||
62
Sources/Schema/ComplexValidatorNilFixed.swift
Normal file
62
Sources/Schema/ComplexValidatorNilFixed.swift
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
extension PureXML.Schema.ComplexValidator {
|
||||||
|
/// The errors from an `xsi:nil="true"` element: rejecting it when the
|
||||||
|
/// element is not nillable, or when it carries content. Returns nil when
|
||||||
|
/// the element is not nilled.
|
||||||
|
func nilErrors(_ element: PureXML.Model.Element, fixedValue: String? = nil, at path: XSDPath) -> [XSDFinding]? {
|
||||||
|
let name = element.name.localName
|
||||||
|
// cvc-elt.3.1: a non-nillable element must carry no `xsi:nil` attribute at
|
||||||
|
// all, whatever its value (so `xsi:nil="false"` is equally forbidden, not
|
||||||
|
// only `xsi:nil="true"`).
|
||||||
|
if Self.nilAttributeValue(element) != nil, !nillableElements.contains(name) {
|
||||||
|
return [XSDFinding(.nilAttributeRequiresNillable, reason: "element '\(name)' is not nillable but has xsi:nil", at: path)]
|
||||||
|
}
|
||||||
|
guard Self.isNil(element) else { return nil }
|
||||||
|
if Self.hasContent(element) {
|
||||||
|
return [XSDFinding(.nilElementMustBeEmpty, reason: "nil element '\(name)' must be empty", at: path)]
|
||||||
|
}
|
||||||
|
// cvc-elt.3.2.2: a nilled element may not have a fixed {value constraint}.
|
||||||
|
// The matched particle's constraint is authoritative; the by-local-name map
|
||||||
|
// is a fallback for paths without a particle (e.g. the resolved-type entry).
|
||||||
|
if (fixedValue ?? elementConstraints[name]?.fixedValue) != nil {
|
||||||
|
return [XSDFinding(.nilElementNoFixedConstraint, reason: "nil element '\(name)' must not have a fixed value constraint", at: path)]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The text an element's simple content is validated against: its character
|
||||||
|
/// content, or, when the element is empty, its `default`/`fixed` value (an
|
||||||
|
/// empty element takes that value, and the value, not the empty string, must
|
||||||
|
/// satisfy the type). The matched particle's constraint takes precedence over
|
||||||
|
/// the by-local-name `elementConstraints` map, exactly as the tree path's
|
||||||
|
/// `validateChild` resolves it.
|
||||||
|
func effectiveSimpleText(for element: PureXML.Model.Element, particleConstraint: PureXML.Schema.ValueConstraint? = nil) -> String {
|
||||||
|
let text = Self.rawTextContent(element)
|
||||||
|
let constraint = particleConstraint ?? elementConstraints[element.name.localName]
|
||||||
|
return text.isEmpty ? (constraint?.value ?? text) : text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The error from a `fixed` element value constraint: the element's text
|
||||||
|
/// must equal the fixed value in the element type's value space when known.
|
||||||
|
func elementFixedErrors(
|
||||||
|
_ element: PureXML.Model.Element,
|
||||||
|
valueType: PureXML.Schema.SimpleType? = nil,
|
||||||
|
particleFixed: String? = nil,
|
||||||
|
at path: XSDPath,
|
||||||
|
) -> [XSDFinding] {
|
||||||
|
guard let fixed = particleFixed ?? elementConstraints[element.name.localName]?.fixedValue else { return [] }
|
||||||
|
// A fixed element's content is its fixed character value: it may not carry
|
||||||
|
// element children, even under a mixed content type (cvc-elt.5.2.2.2.1).
|
||||||
|
if !element.children.compactMap(\.element).isEmpty {
|
||||||
|
return [XSDFinding(.fixedElementNoChildren, reason: "fixed element '\(element.name.localName)' must not have element children", at: path)]
|
||||||
|
}
|
||||||
|
let text = Self.rawTextContent(element)
|
||||||
|
// An empty element takes the fixed value; only present content must equal it.
|
||||||
|
if text.isEmpty {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if let valueType, valueType.valueMatches(text, literal: fixed) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return text == fixed ? [] : [XSDFinding(.fixedElementValueMatches, reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,8 @@ public extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PureXML.Schema.ComplexValidator: PureXML.Validation.Validatable {}
|
||||||
|
|
||||||
public extension PureXML.Schema.ComplexValidator {
|
public extension PureXML.Schema.ComplexValidator {
|
||||||
// MARK: Streaming support
|
// MARK: Streaming support
|
||||||
|
|
||||||
@ -36,6 +38,19 @@ public extension PureXML.Schema.ComplexValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The streaming path's document-scoped ID/IDREF check as a composable
|
||||||
|
/// ``PureXML/Validation`` value, reusing ``idErrors()`` (the same accumulator
|
||||||
|
/// the tree path's `idIdrefConsistent` rule reads) rather than the streaming
|
||||||
|
/// driver calling it imperatively outside the framework. The validator is its
|
||||||
|
/// own subject: by the time this runs (after the whole document has streamed
|
||||||
|
/// through ``shallowValidity``), its `idTracker` already holds every recorded
|
||||||
|
/// ID/IDREF.
|
||||||
|
static var idConsistency: PureXML.Validation.Validation<PureXML.Schema.ComplexValidator, Void> {
|
||||||
|
.init(description: "Every streamed xs:ID is unique and every streamed xs:IDREF resolves to one") { context in
|
||||||
|
context.subject.idErrors()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The matched particle's `default`/`fixed` value constraint for a child of
|
/// The matched particle's `default`/`fixed` value constraint for a child of
|
||||||
/// `parent`'s content model, keyed by namespaced identity (so same-local-name
|
/// `parent`'s content model, keyed by namespaced identity (so same-local-name
|
||||||
/// elements in different types do not collide as in the flat `elementConstraints`).
|
/// elements in different types do not collide as in the flat `elementConstraints`).
|
||||||
@ -109,7 +124,9 @@ public extension PureXML.Schema.ComplexValidator {
|
|||||||
guard case let .complex(complex) = parent else { return nil }
|
guard case let .complex(complex) = parent else { return nil }
|
||||||
switch complex.content {
|
switch complex.content {
|
||||||
case let .elementOnly(particle), let .mixed(particle):
|
case let .elementOnly(particle), let .mixed(particle):
|
||||||
if Self.elementTypes(in: particle.term)[Self.key(child)] != nil { return nil }
|
if Self.elementTypes(in: particle.term)[Self.key(child)] != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
guard Self.wildcardMatch(for: child, in: particle.term) == .strict,
|
guard Self.wildcardMatch(for: child, in: particle.term) == .strict,
|
||||||
types[PureXML.Schema.XSDParser.elementDeclarationKey(child)] == nil
|
types[PureXML.Schema.XSDParser.elementDeclarationKey(child)] == nil
|
||||||
else { return nil }
|
else { return nil }
|
||||||
@ -131,7 +148,9 @@ public extension PureXML.Schema.ComplexValidator {
|
|||||||
return nil
|
return nil
|
||||||
case let .group(group):
|
case let .group(group):
|
||||||
for member in group.particles {
|
for member in group.particles {
|
||||||
if let match = wildcardMatch(for: name, in: member.term) { return match }
|
if let match = wildcardMatch(for: name, in: member.term) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -160,28 +179,45 @@ public extension PureXML.Schema.ComplexValidator {
|
|||||||
valueConstraint: PureXML.Schema.ValueConstraint? = nil,
|
valueConstraint: PureXML.Schema.ValueConstraint? = nil,
|
||||||
at path: [PureXML.Validation.PathKey] = [],
|
at path: [PureXML.Validation.PathKey] = [],
|
||||||
) -> [PureXML.Validation.ValidationError] {
|
) -> [PureXML.Validation.ValidationError] {
|
||||||
var errors: [XSDFailure] = []
|
validateShallowTagged(element, as: type, valueConstraint: valueConstraint, at: path).map(\.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tagged form of ``validateShallow(_:as:valueConstraint:at:)``. See
|
||||||
|
/// ``PureXML/Schema/ComplexValidator/validateTagged(_:against:at:namespaceBindings:)``.
|
||||||
|
private func validateShallowTagged(
|
||||||
|
_ element: PureXML.Model.Element,
|
||||||
|
as type: PureXML.Schema.ElementType,
|
||||||
|
valueConstraint: PureXML.Schema.ValueConstraint? = nil,
|
||||||
|
at path: [PureXML.Validation.PathKey] = [],
|
||||||
|
) -> [XSDFinding] {
|
||||||
|
var errors: [XSDFinding] = []
|
||||||
switch type {
|
switch type {
|
||||||
case let .simple(simple):
|
case let .simple(simple):
|
||||||
validateSimpleElement(element, simple, valueConstraint: valueConstraint, at: path, into: &errors)
|
validateSimpleElement(element, simple, valueConstraint: valueConstraint, at: path, into: &errors)
|
||||||
case let .complex(complex):
|
case let .complex(complex):
|
||||||
validateAttributes(element, complex, at: path, into: &errors)
|
validateAttributes(element, complex, at: path, into: &errors)
|
||||||
if let nilErrors = nilErrors(element, at: path) { return errors + nilErrors }
|
if let nilErrors = nilErrors(element, at: path) {
|
||||||
|
return errors + nilErrors
|
||||||
|
}
|
||||||
shallowContent(element, complex.content, at: path, into: &errors)
|
shallowContent(element, complex.content, at: path, into: &errors)
|
||||||
// A simpleContent fixed value is compared in its simple type's value space
|
// A simpleContent fixed value is compared in its simple type's value space
|
||||||
// (cvc-elt.5.2.2.1), matching the tree path's validate(_:against:).
|
// (cvc-elt.5.2.2.1), matching the tree path's validate(_:against:).
|
||||||
let simpleContentType: PureXML.Schema.SimpleType? = if case let .simpleContent(simple) = complex.content { simple } else { nil }
|
let simpleContentType: PureXML.Schema.SimpleType? = if case let .simpleContent(simple) = complex.content {
|
||||||
|
simple
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
errors += elementFixedErrors(element, valueType: simpleContentType, at: path)
|
errors += elementFixedErrors(element, valueType: simpleContentType, at: path)
|
||||||
case .typeReference:
|
case .typeReference:
|
||||||
// The shared resolver also guards a circular chain, which would
|
// The shared resolver also guards a circular chain, which would
|
||||||
// otherwise recurse here without terminating.
|
// otherwise recurse here without terminating.
|
||||||
switch resolveReference(type) {
|
switch resolveReference(type) {
|
||||||
case let .unknown(name):
|
case let .unknown(name):
|
||||||
return [XSDFailure(reason: "unknown type '\(name)'", at: path)]
|
return [XSDFinding(.typeReferenceResolves, reason: "unknown type '\(name)'", at: path)]
|
||||||
case let .circular(name):
|
case let .circular(name):
|
||||||
return [XSDFailure(reason: "circular type reference '\(name)'", at: path)]
|
return [XSDFinding(.typeReferenceResolves, reason: "circular type reference '\(name)'", at: path)]
|
||||||
case let .resolved(resolved):
|
case let .resolved(resolved):
|
||||||
return validateShallow(element, as: resolved, valueConstraint: valueConstraint, at: path)
|
return validateShallowTagged(element, as: resolved, valueConstraint: valueConstraint, at: path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errors
|
return errors
|
||||||
@ -192,21 +228,21 @@ public extension PureXML.Schema.ComplexValidator {
|
|||||||
_ simple: PureXML.Schema.SimpleType,
|
_ simple: PureXML.Schema.SimpleType,
|
||||||
valueConstraint: PureXML.Schema.ValueConstraint? = nil,
|
valueConstraint: PureXML.Schema.ValueConstraint? = nil,
|
||||||
at path: XSDPath,
|
at path: XSDPath,
|
||||||
into errors: inout [XSDFailure],
|
into errors: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
if let nilErrors = nilErrors(element, fixedValue: valueConstraint?.fixedValue, at: path) {
|
if let nilErrors = nilErrors(element, fixedValue: valueConstraint?.fixedValue, at: path) {
|
||||||
errors += nilErrors
|
errors += nilErrors
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !element.children.compactMap(\.element).isEmpty {
|
if !element.children.compactMap(\.element).isEmpty {
|
||||||
errors.append(XSDFailure(reason: "'\(element.name.localName)' must not have children", at: path))
|
errors.append(XSDFinding(.simpleTypeForbidsChildren, reason: "'\(element.name.localName)' must not have children", at: path))
|
||||||
}
|
}
|
||||||
// An empty element takes its `default`/`fixed` value; that value (not the
|
// An empty element takes its `default`/`fixed` value; that value (not the
|
||||||
// empty string) is what must be valid against the type, matching the tree
|
// empty string) is what must be valid against the type, matching the tree
|
||||||
// path's validateChild.
|
// path's validateChild.
|
||||||
let text = Self.rawTextContent(element)
|
let text = Self.rawTextContent(element)
|
||||||
if let error = simple.validate(effectiveSimpleText(for: element, particleConstraint: valueConstraint)) {
|
if let error = simple.validate(effectiveSimpleText(for: element, particleConstraint: valueConstraint)) {
|
||||||
errors.append(XSDFailure(reason: "'\(element.name.localName)': \(error)", at: path))
|
errors.append(XSDFinding(.simpleTypeValueValid, reason: "'\(element.name.localName)': \(error)", at: path))
|
||||||
}
|
}
|
||||||
recordIDs(simple, value: text, at: path)
|
recordIDs(simple, value: text, at: path)
|
||||||
// Compare a present fixed value in the type's value space (e.g. "05" == "5"
|
// Compare a present fixed value in the type's value space (e.g. "05" == "5"
|
||||||
@ -215,17 +251,21 @@ public extension PureXML.Schema.ComplexValidator {
|
|||||||
errors += elementFixedErrors(element, valueType: simple, particleFixed: valueConstraint?.fixedValue, at: path)
|
errors += elementFixedErrors(element, valueType: simple, particleFixed: valueConstraint?.fixedValue, at: path)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func shallowContent(_ element: PureXML.Model.Element, _ content: PureXML.Schema.ContentType, at path: XSDPath, into errors: inout [XSDFailure]) {
|
private func shallowContent(_ element: PureXML.Model.Element, _ content: PureXML.Schema.ContentType, at path: XSDPath, into errors: inout [XSDFinding]) {
|
||||||
let children = element.children.compactMap(\.element)
|
let children = element.children.compactMap(\.element)
|
||||||
switch content {
|
switch content {
|
||||||
case .empty:
|
case .empty:
|
||||||
if !children.isEmpty { errors.append(XSDFailure(reason: "element must be empty", at: path)) }
|
if !children.isEmpty {
|
||||||
|
errors.append(XSDFinding(.emptyContentForbidsChildren, reason: "element must be empty", at: path))
|
||||||
|
}
|
||||||
rejectText(element, at: path, into: &errors)
|
rejectText(element, at: path, into: &errors)
|
||||||
case let .simpleContent(type):
|
case let .simpleContent(type):
|
||||||
if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) }
|
if !children.isEmpty {
|
||||||
|
errors.append(XSDFinding(.simpleContentForbidsChildren, reason: "element must not have children", at: path))
|
||||||
|
}
|
||||||
let text = Self.rawTextContent(element)
|
let text = Self.rawTextContent(element)
|
||||||
if let error = type.validate(text) {
|
if let error = type.validate(text) {
|
||||||
errors.append(XSDFailure(reason: "content: \(error)", at: path))
|
errors.append(XSDFinding(.simpleContentTypeValid, reason: "content: \(error)", at: path))
|
||||||
}
|
}
|
||||||
recordIDs(type, value: text, at: path)
|
recordIDs(type, value: text, at: path)
|
||||||
case let .elementOnly(particle):
|
case let .elementOnly(particle):
|
||||||
@ -236,7 +276,7 @@ public extension PureXML.Schema.ComplexValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func shallowStructure(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
private func shallowStructure(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFinding]) {
|
||||||
if case let .group(group) = particle.term, group.compositor == .all {
|
if case let .group(group) = particle.term, group.compositor == .all {
|
||||||
allStructureErrors(group, children: children, groupOptional: particle.occurrenceRange.minimum.isZero, at: path, into: &errors)
|
allStructureErrors(group, children: children, groupOptional: particle.occurrenceRange.minimum.isZero, at: path, into: &errors)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
48
Sources/Schema/ComplexValidatorWildcards.swift
Normal file
48
Sources/Schema/ComplexValidatorWildcards.swift
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
extension PureXML.Schema.ComplexValidator {
|
||||||
|
/// A global element declaration is always in the schema's target
|
||||||
|
/// namespace, so an instance element matches one only by full qualified
|
||||||
|
/// name. The namespaced key already covers a no-namespace global (its key
|
||||||
|
/// is `element:{}local`); matching an unqualified instance against a
|
||||||
|
/// namespaced global by bare local name would conflate namespaces, e.g.
|
||||||
|
/// accepting unqualified text where a namespaced `xs:short` is declared.
|
||||||
|
func globalElementDeclaration(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
|
||||||
|
types[PureXML.Schema.XSDParser.elementDeclarationKey(name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a wildcard-matched child by its `processContents`: skip does
|
||||||
|
/// nothing, lax validates against a global declaration when one exists, and
|
||||||
|
/// strict requires one.
|
||||||
|
func validateWildcardChild(
|
||||||
|
_ child: PureXML.Model.Element,
|
||||||
|
processContents: PureXML.Schema.ProcessContents,
|
||||||
|
at path: XSDPath,
|
||||||
|
namespaceBindings: [String: String],
|
||||||
|
into errors: inout [XSDFinding],
|
||||||
|
) {
|
||||||
|
switch processContents {
|
||||||
|
case .skip:
|
||||||
|
return
|
||||||
|
case .lax:
|
||||||
|
if let declaration = globalElementDeclaration(for: child.name) {
|
||||||
|
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||||
|
}
|
||||||
|
case .strict:
|
||||||
|
if let declaration = globalElementDeclaration(for: child.name) {
|
||||||
|
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||||
|
} else if let type = xsiDeclaredType(for: child, namespaceBindings: namespaceBindings) {
|
||||||
|
validateChild(child, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||||
|
} else {
|
||||||
|
errors.append(XSDFinding(.wildcardElementUndeclared, reason: "no declaration for wildcard-matched element '\(child.name.localName)'", at: path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type named by a wildcard-matched element's `xsi:type`, when present.
|
||||||
|
func xsiDeclaredType(for child: PureXML.Model.Element, namespaceBindings: [String: String]) -> PureXML.Schema.ElementType? {
|
||||||
|
guard Self.xsiTypeAttributeValue(child) != nil else { return nil }
|
||||||
|
guard let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
|
||||||
|
let type = Self.resolveNamedType(reference, in: types)
|
||||||
|
else { return nil }
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,20 +7,26 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
for child: PureXML.Model.Element,
|
for child: PureXML.Model.Element,
|
||||||
namespaceBindings: [String: String] = [:],
|
namespaceBindings: [String: String] = [:],
|
||||||
at path: XSDPath,
|
at path: XSDPath,
|
||||||
into errors: inout [XSDFailure],
|
into errors: inout [XSDFinding],
|
||||||
) -> PureXML.Schema.ElementType? {
|
) -> PureXML.Schema.ElementType? {
|
||||||
guard Self.xsiTypeAttributeValue(child) != nil else { return declared }
|
guard Self.xsiTypeAttributeValue(child) != nil else { return declared }
|
||||||
let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings) ?? Self.xsiTypeName(child) ?? ""
|
let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings) ?? Self.xsiTypeName(child) ?? ""
|
||||||
if let resolved = Self.resolveNamedType(reference, in: types) { return resolved }
|
if let resolved = Self.resolveNamedType(reference, in: types) {
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
let overriding = Self.xsiTypeName(child) ?? reference
|
let overriding = Self.xsiTypeName(child) ?? reference
|
||||||
if let builtin = PureXML.Schema.BuiltinType(rawValue: overriding) {
|
if let builtin = PureXML.Schema.BuiltinType(rawValue: overriding) {
|
||||||
if xsiBuiltinSubstitutes(builtin, for: declared) {
|
if xsiBuiltinSubstitutes(builtin, for: declared) {
|
||||||
return .simple(PureXML.Schema.SimpleType(base: builtin))
|
return .simple(PureXML.Schema.SimpleType(base: builtin))
|
||||||
}
|
}
|
||||||
errors.append(XSDFailure(reason: "xsi:type '\(overriding)' is not validly derived from the declared type of '\(child.name.localName)'", at: path))
|
errors.append(XSDFinding(
|
||||||
|
.xsiTypeOverrideResolves,
|
||||||
|
reason: "xsi:type '\(overriding)' is not validly derived from the declared type of '\(child.name.localName)'",
|
||||||
|
at: path,
|
||||||
|
))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
errors.append(XSDFailure(reason: "unknown xsi:type '\(overriding)' on '\(child.name.localName)'", at: path))
|
errors.append(XSDFinding(.xsiTypeOverrideResolves, reason: "unknown xsi:type '\(overriding)' on '\(child.name.localName)'", at: path))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +38,9 @@ extension PureXML.Schema.ComplexValidator {
|
|||||||
private func xsiBuiltinSubstitutes(_ xsi: PureXML.Schema.BuiltinType, for declared: PureXML.Schema.ElementType) -> Bool {
|
private func xsiBuiltinSubstitutes(_ xsi: PureXML.Schema.BuiltinType, for declared: PureXML.Schema.ElementType) -> Bool {
|
||||||
switch declared {
|
switch declared {
|
||||||
case let .typeReference(name):
|
case let .typeReference(name):
|
||||||
if name == "anyType" || name == "anySimpleType" || name.hasSuffix("}anyType") || name.hasSuffix("}anySimpleType") { return true }
|
if name == "anyType" || name == "anySimpleType" || name.hasSuffix("}anyType") || name.hasSuffix("}anySimpleType") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
switch resolveReference(declared) {
|
switch resolveReference(declared) {
|
||||||
case let .resolved(resolved): return xsiBuiltinSubstitutes(xsi, for: resolved)
|
case let .resolved(resolved): return xsiBuiltinSubstitutes(xsi, for: resolved)
|
||||||
case .unknown, .circular: return false
|
case .unknown, .circular: return false
|
||||||
|
|||||||
@ -140,7 +140,9 @@ extension PureXML.Schema {
|
|||||||
func admissible(from current: Set<ContentConfiguration>) -> [TermLabel] {
|
func admissible(from current: Set<ContentConfiguration>) -> [TermLabel] {
|
||||||
var labels: [TermLabel] = []
|
var labels: [TermLabel] = []
|
||||||
for configuration in current where states[configuration.state].target != nil {
|
for configuration in current where states[configuration.state].target != nil {
|
||||||
if let label = states[configuration.state].label { labels.append(label) }
|
if let label = states[configuration.state].label {
|
||||||
|
labels.append(label)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
@ -203,7 +205,9 @@ extension PureXML.Schema {
|
|||||||
next.insert(ContentConfiguration(state: target, counters: configuration.counters))
|
next.insert(ContentConfiguration(state: target, counters: configuration.counters))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if next.isEmpty { return nil }
|
if next.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return closure(next, caps: counterCaps(for: inputLength))
|
return closure(next, caps: counterCaps(for: inputLength))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -63,8 +63,12 @@ public extension PureXML.Schema {
|
|||||||
/// foreign annotation content.
|
/// foreign annotation content.
|
||||||
private static func forEachComplexType(_ node: XSDTree, _ visit: (XSDTree) -> Void) {
|
private static func forEachComplexType(_ node: XSDTree, _ visit: (XSDTree) -> Void) {
|
||||||
let local = PureXML.Schema.XSDNode.localName(node)
|
let local = PureXML.Schema.XSDNode.localName(node)
|
||||||
if local == "appinfo" || local == "documentation" { return }
|
if local == "appinfo" || local == "documentation" {
|
||||||
if local == "complexType" { visit(node) }
|
return
|
||||||
|
}
|
||||||
|
if local == "complexType" {
|
||||||
|
visit(node)
|
||||||
|
}
|
||||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||||
forEachComplexType(child, visit)
|
forEachComplexType(child, visit)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,7 +98,9 @@ private struct CompWalk {
|
|||||||
var identityConflict: String?
|
var identityConflict: String?
|
||||||
|
|
||||||
mutating func emit(_ set: [CompItem]) {
|
mutating func emit(_ set: [CompItem]) {
|
||||||
if set.count > 1 { decisionSets.append(set) }
|
if set.count > 1 {
|
||||||
|
decisionSets.append(set)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dedupe a decision set (first set or follow set) by particle identity. A repeated
|
/// Dedupe a decision set (first set or follow set) by particle identity. A repeated
|
||||||
@ -156,13 +158,17 @@ private struct CompWalk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func labelName(_ label: PureXML.Schema.TermLabel) -> String {
|
private func labelName(_ label: PureXML.Schema.TermLabel) -> String {
|
||||||
if case let .name(qname) = label { return qname.localName }
|
if case let .name(qname) = label {
|
||||||
|
return qname.localName
|
||||||
|
}
|
||||||
return "any"
|
return "any"
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func summarize(_ node: PureXML.Model.TreeNode, visiting: Set<String>) -> CompSummary {
|
mutating func summarize(_ node: PureXML.Model.TreeNode, visiting: Set<String>) -> CompSummary {
|
||||||
let (minimum, maximum) = PureXML.Schema.XSDNode.occurrence(node)
|
let (minimum, maximum) = PureXML.Schema.XSDNode.occurrence(node)
|
||||||
if let maximum, maximum <= 0 { return CompSummary(nullable: true, first: [], lastFollow: []) }
|
if let maximum, maximum <= 0 {
|
||||||
|
return CompSummary(nullable: true, first: [], lastFollow: [])
|
||||||
|
}
|
||||||
var body = bodySummary(node, visiting: visiting)
|
var body = bodySummary(node, visiting: visiting)
|
||||||
// A repeat edge folds the body's first into every end-item's follow. Kept in a
|
// A repeat edge folds the body's first into every end-item's follow. Kept in a
|
||||||
// separate method, not inline: `summarize` recurses on nesting depth, so any
|
// separate method, not inline: `summarize` recurses on nesting depth, so any
|
||||||
@ -227,7 +233,9 @@ private struct CompWalk {
|
|||||||
var suffix: [CompItem] = []
|
var suffix: [CompItem] = []
|
||||||
for next in parts[(index + 1)...] {
|
for next in parts[(index + 1)...] {
|
||||||
suffix.append(contentsOf: next.first)
|
suffix.append(contentsOf: next.first)
|
||||||
if !next.nullable { break }
|
if !next.nullable {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
suffixFirst[index] = suffix
|
suffixFirst[index] = suffix
|
||||||
}
|
}
|
||||||
@ -239,7 +247,9 @@ private struct CompWalk {
|
|||||||
var prefixFirsts: [[CompItem]] = []
|
var prefixFirsts: [[CompItem]] = []
|
||||||
for part in parts {
|
for part in parts {
|
||||||
prefixFirsts.append(part.first)
|
prefixFirsts.append(part.first)
|
||||||
if !part.nullable { break }
|
if !part.nullable {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Each trailing end-item carries its internal follow PLUS the suffix that follows
|
// Each trailing end-item carries its internal follow PLUS the suffix that follows
|
||||||
// it inside the sequence. Otherwise an end-item that is also followed internally
|
// it inside the sequence. Otherwise an end-item that is also followed internally
|
||||||
@ -253,7 +263,9 @@ private struct CompWalk {
|
|||||||
for entry in parts[index].lastFollow {
|
for entry in parts[index].lastFollow {
|
||||||
trailing.append((entry.item, dedupOnly([entry.follow, suffixFirst[index]])))
|
trailing.append((entry.item, dedupOnly([entry.follow, suffixFirst[index]])))
|
||||||
}
|
}
|
||||||
if !parts[index].nullable { break }
|
if !parts[index].nullable {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return CompSummary(nullable: parts.allSatisfy(\.nullable), first: mergeList(prefixFirsts), lastFollow: mergeLastFollow(trailing))
|
return CompSummary(nullable: parts.allSatisfy(\.nullable), first: mergeList(prefixFirsts), lastFollow: mergeLastFollow(trailing))
|
||||||
}
|
}
|
||||||
@ -293,8 +305,12 @@ private struct CompWalk {
|
|||||||
private mutating func group(_ node: PureXML.Model.TreeNode, visiting: Set<String>) -> CompSummary {
|
private mutating func group(_ node: PureXML.Model.TreeNode, visiting: Set<String>) -> CompSummary {
|
||||||
guard let ref = PureXML.Schema.XSDNode.attribute(node, "ref") else { return CompSummary(nullable: true, first: [], lastFollow: []) }
|
guard let ref = PureXML.Schema.XSDNode.attribute(node, "ref") else { return CompSummary(nullable: true, first: [], lastFollow: []) }
|
||||||
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
|
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
|
||||||
if visiting.contains(name) { return redefineSelfReference(name, visiting: visiting) }
|
if visiting.contains(name) {
|
||||||
if let cached = groupCache[name] { return cached }
|
return redefineSelfReference(name, visiting: visiting)
|
||||||
|
}
|
||||||
|
if let cached = groupCache[name] {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
guard let definition = context.groups[name], let model = modelChild(definition)
|
guard let definition = context.groups[name], let model = modelChild(definition)
|
||||||
else { return CompSummary(nullable: true, first: [], lastFollow: []) }
|
else { return CompSummary(nullable: true, first: [], lastFollow: []) }
|
||||||
// Summarize the group once: its internal decision sets are context-independent
|
// Summarize the group once: its internal decision sets are context-independent
|
||||||
|
|||||||
@ -11,8 +11,12 @@ enum CompConflict {
|
|||||||
for item in set {
|
for item in set {
|
||||||
switch item.label {
|
switch item.label {
|
||||||
case let .name(qname):
|
case let .name(qname):
|
||||||
if let owner = nameOwner[qname], owner != item.particle { return qname.localName }
|
if let owner = nameOwner[qname], owner != item.particle {
|
||||||
if nameOwner[qname] == nil { nameOwner[qname] = item.particle }
|
return qname.localName
|
||||||
|
}
|
||||||
|
if nameOwner[qname] == nil {
|
||||||
|
nameOwner[qname] = item.particle
|
||||||
|
}
|
||||||
case let .wildcard(card):
|
case let .wildcard(card):
|
||||||
wildcards.append((card, item.particle))
|
wildcards.append((card, item.particle))
|
||||||
}
|
}
|
||||||
@ -20,7 +24,9 @@ enum CompConflict {
|
|||||||
for (index, wildcard) in wildcards.enumerated() {
|
for (index, wildcard) in wildcards.enumerated() {
|
||||||
// A wildcard conflicts with any name it admits, from a distinct particle,
|
// A wildcard conflicts with any name it admits, from a distinct particle,
|
||||||
// and with any other wildcard it intersects, from a distinct particle.
|
// and with any other wildcard it intersects, from a distinct particle.
|
||||||
if nameOwner.contains(where: { wildcard.card.admits($0.key) && $0.value != wildcard.particle }) { return "any" }
|
if nameOwner.contains(where: { wildcard.card.admits($0.key) && $0.value != wildcard.particle }) {
|
||||||
|
return "any"
|
||||||
|
}
|
||||||
for other in wildcards[(index + 1)...] where other.particle != wildcard.particle && wildcardsOverlap(wildcard.card, other.card) {
|
for other in wildcards[(index + 1)...] where other.particle != wildcard.particle && wildcardsOverlap(wildcard.card, other.card) {
|
||||||
return "any"
|
return "any"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,14 @@ extension PureXML.Schema.ContentModelDeterminism {
|
|||||||
/// complex type's content model, reached through `complexContent` and its
|
/// complex type's content model, reached through `complexContent` and its
|
||||||
/// `restriction`/`extension`, or nil for simple or empty content.
|
/// `restriction`/`extension`, or nil for simple or empty content.
|
||||||
static func contentModelNode(_ complexType: XSDTree) -> XSDTree? {
|
static func contentModelNode(_ complexType: XSDTree) -> XSDTree? {
|
||||||
if let direct = modelGroupChild(complexType) { return direct }
|
if let direct = modelGroupChild(complexType) {
|
||||||
|
return direct
|
||||||
|
}
|
||||||
guard let complexContent = PureXML.Schema.XSDNode.firstChild(complexType, named: "complexContent") else { return nil }
|
guard let complexContent = PureXML.Schema.XSDNode.firstChild(complexType, named: "complexContent") else { return nil }
|
||||||
for derivation in PureXML.Schema.XSDNode.elementChildren(complexContent) {
|
for derivation in PureXML.Schema.XSDNode.elementChildren(complexContent) {
|
||||||
if let model = modelGroupChild(derivation) { return model }
|
if let model = modelGroupChild(derivation) {
|
||||||
|
return model
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,9 @@ enum GroupDependencyOrder {
|
|||||||
order.append(name)
|
order.append(name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if visited.contains(name) { continue }
|
if visited.contains(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
visited.insert(name)
|
visited.insert(name)
|
||||||
onPath.insert(name)
|
onPath.insert(name)
|
||||||
work.append((name, true))
|
work.append((name, true))
|
||||||
|
|||||||
@ -108,7 +108,11 @@ extension PureXML.Schema {
|
|||||||
var nullable = true
|
var nullable = true
|
||||||
for member in particles {
|
for member in particles {
|
||||||
let part = particle(member)
|
let part = particle(member)
|
||||||
if let previous = last { addEpsilon(from: previous, to: part.start) } else { first = part.start }
|
if let previous = last {
|
||||||
|
addEpsilon(from: previous, to: part.start)
|
||||||
|
} else {
|
||||||
|
first = part.start
|
||||||
|
}
|
||||||
last = part.accept
|
last = part.accept
|
||||||
fragmentCounters.append(contentsOf: part.counters)
|
fragmentCounters.append(contentsOf: part.counters)
|
||||||
nullable = nullable && part.nullable
|
nullable = nullable && part.nullable
|
||||||
|
|||||||
@ -139,7 +139,9 @@ extension PureXML.Schema {
|
|||||||
/// The shared fragment for a group definition, compiled once and memoized.
|
/// The shared fragment for a group definition, compiled once and memoized.
|
||||||
/// Its accept is marked `isReturn`.
|
/// Its accept is marked `isReturn`.
|
||||||
private mutating func groupFragment(_ group: PureXML.Schema.Group, id: Int) -> ContentNFAFragment {
|
private mutating func groupFragment(_ group: PureXML.Schema.Group, id: Int) -> ContentNFAFragment {
|
||||||
if let cached = fragments[id] { return cached }
|
if let cached = fragments[id] {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
// `definitionID` groups are acyclic: XSD forbids a group referencing itself
|
// `definitionID` groups are acyclic: XSD forbids a group referencing itself
|
||||||
// except through an element, and the parser's expansion breaks every cycle at
|
// except through an element, and the parser's expansion breaks every cycle at
|
||||||
// an element boundary, so a fragment is never referenced during its own
|
// an element boundary, so a fragment is never referenced during its own
|
||||||
@ -176,7 +178,11 @@ extension PureXML.Schema {
|
|||||||
var nullable = true
|
var nullable = true
|
||||||
for member in particles {
|
for member in particles {
|
||||||
let part = particle(member)
|
let part = particle(member)
|
||||||
if let previous = last { addEpsilon(from: previous, to: part.start) } else { first = part.start }
|
if let previous = last {
|
||||||
|
addEpsilon(from: previous, to: part.start)
|
||||||
|
} else {
|
||||||
|
first = part.start
|
||||||
|
}
|
||||||
last = part.accept
|
last = part.accept
|
||||||
fragmentCounters.append(contentsOf: part.counters)
|
fragmentCounters.append(contentsOf: part.counters)
|
||||||
nullable = nullable && part.nullable
|
nullable = nullable && part.nullable
|
||||||
|
|||||||
@ -109,7 +109,9 @@ extension PureXML.Schema {
|
|||||||
next.insert(ContentRTNConfiguration(state: target, counters: configuration.counters, gss: configuration.gss))
|
next.insert(ContentRTNConfiguration(state: target, counters: configuration.counters, gss: configuration.gss))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if next.isEmpty { return nil }
|
if next.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
position += 1
|
position += 1
|
||||||
return closure(next)
|
return closure(next)
|
||||||
}
|
}
|
||||||
@ -121,7 +123,9 @@ extension PureXML.Schema {
|
|||||||
func admissible(from current: Set<ContentRTNConfiguration>) -> [TermLabel] {
|
func admissible(from current: Set<ContentRTNConfiguration>) -> [TermLabel] {
|
||||||
var labels: [TermLabel] = []
|
var labels: [TermLabel] = []
|
||||||
for configuration in current where rtn.states[configuration.state].target != nil {
|
for configuration in current where rtn.states[configuration.state].target != nil {
|
||||||
if let label = rtn.states[configuration.state].label { labels.append(label) }
|
if let label = rtn.states[configuration.state].label {
|
||||||
|
labels.append(label)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
@ -154,8 +158,12 @@ extension PureXML.Schema {
|
|||||||
let state = configuration.state
|
let state = configuration.state
|
||||||
guard let label = rtn.states[state].label, let target = rtn.states[state].target, label.matches(name) else { continue }
|
guard let label = rtn.states[state].label, let target = rtn.states[state].target, label.matches(name) else { continue }
|
||||||
next.insert(ContentRTNConfiguration(state: target, counters: configuration.counters, gss: configuration.gss))
|
next.insert(ContentRTNConfiguration(state: target, counters: configuration.counters, gss: configuration.gss))
|
||||||
if anyState.map({ state < $0 }) ?? true { anyState = state }
|
if anyState.map({ state < $0 }) ?? true {
|
||||||
if case .name = label, namedState.map({ state < $0 }) ?? true { namedState = state }
|
anyState = state
|
||||||
|
}
|
||||||
|
if case .name = label, namedState.map({ state < $0 }) ?? true {
|
||||||
|
namedState = state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
guard let chosenState = namedState ?? anyState, !next.isEmpty else {
|
guard let chosenState = namedState ?? anyState, !next.isEmpty else {
|
||||||
result.append(contentsOf: Array(repeating: nil, count: names.count - result.count))
|
result.append(contentsOf: Array(repeating: nil, count: names.count - result.count))
|
||||||
@ -190,7 +198,9 @@ extension PureXML.Schema {
|
|||||||
while true {
|
while true {
|
||||||
if !pending.isEmpty {
|
if !pending.isEmpty {
|
||||||
let configuration = pending.removeLast()
|
let configuration = pending.removeLast()
|
||||||
if seen.insert(configuration).inserted { work.append(configuration) }
|
if seen.insert(configuration).inserted {
|
||||||
|
work.append(configuration)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
guard let configuration = work.popLast() else { break }
|
guard let configuration = work.popLast() else { break }
|
||||||
@ -203,7 +213,9 @@ extension PureXML.Schema {
|
|||||||
for call in state.calls {
|
for call in state.calls {
|
||||||
let node = internNode(returnState: call.returnTo, savedCounters: configuration.counters, parent: configuration.gss)
|
let node = internNode(returnState: call.returnTo, savedCounters: configuration.counters, parent: configuration.gss)
|
||||||
let nextConfiguration = ContentRTNConfiguration(state: call.calleeStart, counters: configuration.counters, gss: node)
|
let nextConfiguration = ContentRTNConfiguration(state: call.calleeStart, counters: configuration.counters, gss: node)
|
||||||
if seen.insert(nextConfiguration).inserted { work.append(nextConfiguration) }
|
if seen.insert(nextConfiguration).inserted {
|
||||||
|
work.append(nextConfiguration)
|
||||||
|
}
|
||||||
drainPending(into: &seen, work: &work)
|
drainPending(into: &seen, work: &work)
|
||||||
}
|
}
|
||||||
if state.isReturn, configuration.gss != 0 {
|
if state.isReturn, configuration.gss != 0 {
|
||||||
@ -218,7 +230,9 @@ extension PureXML.Schema {
|
|||||||
|
|
||||||
private func drainPending(into seen: inout Set<ContentRTNConfiguration>, work: inout [ContentRTNConfiguration]) {
|
private func drainPending(into seen: inout Set<ContentRTNConfiguration>, work: inout [ContentRTNConfiguration]) {
|
||||||
while let configuration = pending.popLast() {
|
while let configuration = pending.popLast() {
|
||||||
if seen.insert(configuration).inserted { work.append(configuration) }
|
if seen.insert(configuration).inserted {
|
||||||
|
work.append(configuration)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -95,7 +95,9 @@ extension PureXML.Schema {
|
|||||||
guard let hour = readFixed(2), consume(":"), let minute = readFixed(2), consume(":"),
|
guard let hour = readFixed(2), consume(":"), let minute = readFixed(2), consume(":"),
|
||||||
let second = readFixed(2) else { return false }
|
let second = readFixed(2) else { return false }
|
||||||
guard (0 ... 24).contains(hour), (0 ... 59).contains(minute), (0 ... 59).contains(second) else { return false }
|
guard (0 ... 24).contains(hour), (0 ... 59).contains(minute), (0 ... 59).contains(second) else { return false }
|
||||||
if hour == 24, minute != 0 || second != 0 { return false }
|
if hour == 24, minute != 0 || second != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
value.hour = hour
|
value.hour = hour
|
||||||
value.minute = minute
|
value.minute = minute
|
||||||
value.second = second
|
value.second = second
|
||||||
@ -107,12 +109,15 @@ extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private mutating func parseTimezone(into value: inout DateTimeValue) -> Bool {
|
private mutating func parseTimezone(into value: inout DateTimeValue) -> Bool {
|
||||||
if consume("Z") { value.offsetMinutes = 0
|
if consume("Z") {
|
||||||
|
value.offsetMinutes = 0
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
guard peek() == "+" || peek() == "-" else { return true }
|
guard peek() == "+" || peek() == "-" else { return true }
|
||||||
let negative = consume("-")
|
let negative = consume("-")
|
||||||
if !negative { advance() }
|
if !negative {
|
||||||
|
advance()
|
||||||
|
}
|
||||||
guard let hour = readFixed(2), consume(":"), let minute = readFixed(2) else { return false }
|
guard let hour = readFixed(2), consume(":"), let minute = readFixed(2) else { return false }
|
||||||
guard (0 ... 14).contains(hour), (0 ... 59).contains(minute), hour * 60 + minute <= 14 * 60 else {
|
guard (0 ... 14).contains(hour), (0 ... 59).contains(minute), hour * 60 + minute <= 14 * 60 else {
|
||||||
return false
|
return false
|
||||||
@ -136,7 +141,9 @@ extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private mutating func advance() {
|
private mutating func advance() {
|
||||||
if index < chars.count { index += 1 }
|
if index < chars.count {
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func consume(_ literal: String) -> Bool {
|
private mutating func consume(_ literal: String) -> Bool {
|
||||||
|
|||||||
@ -65,7 +65,9 @@ extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func < (lhs: DecimalValue, rhs: DecimalValue) -> Bool {
|
static func < (lhs: DecimalValue, rhs: DecimalValue) -> Bool {
|
||||||
if lhs.negative != rhs.negative { return lhs.negative }
|
if lhs.negative != rhs.negative {
|
||||||
|
return lhs.negative
|
||||||
|
}
|
||||||
let magnitude = compareMagnitude(lhs, rhs)
|
let magnitude = compareMagnitude(lhs, rhs)
|
||||||
return lhs.negative ? magnitude > 0 : magnitude < 0
|
return lhs.negative ? magnitude > 0 : magnitude < 0
|
||||||
}
|
}
|
||||||
@ -75,7 +77,9 @@ extension PureXML.Schema {
|
|||||||
if lhs.integerDigits.count != rhs.integerDigits.count {
|
if lhs.integerDigits.count != rhs.integerDigits.count {
|
||||||
return lhs.integerDigits.count < rhs.integerDigits.count ? -1 : 1
|
return lhs.integerDigits.count < rhs.integerDigits.count ? -1 : 1
|
||||||
}
|
}
|
||||||
if let order = lexicographic(lhs.integerDigits, rhs.integerDigits) { return order }
|
if let order = lexicographic(lhs.integerDigits, rhs.integerDigits) {
|
||||||
|
return order
|
||||||
|
}
|
||||||
return lexicographic(lhs.fractionDigits, rhs.fractionDigits) ?? 0
|
return lexicographic(lhs.fractionDigits, rhs.fractionDigits) ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +89,9 @@ extension PureXML.Schema {
|
|||||||
for (left, right) in zip(lhs, rhs) where left != right {
|
for (left, right) in zip(lhs, rhs) where left != right {
|
||||||
return left < right ? -1 : 1
|
return left < right ? -1 : 1
|
||||||
}
|
}
|
||||||
if lhs.count == rhs.count { return nil }
|
if lhs.count == rhs.count {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return lhs.count < rhs.count ? -1 : 1
|
return lhs.count < rhs.count ? -1 : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,9 +36,15 @@ extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
// P < Q only when s+P < s+Q at every reference; > likewise; equal
|
// P < Q only when s+P < s+Q at every reference; > likewise; equal
|
||||||
// when all four coincide (which holds iff the components are equal).
|
// when all four coincide (which holds iff the components are equal).
|
||||||
if sawLess, !sawGreater, !sawEqual { return .lessThan }
|
if sawLess, !sawGreater, !sawEqual {
|
||||||
if sawGreater, !sawLess, !sawEqual { return .greaterThan }
|
return .lessThan
|
||||||
if sawEqual, !sawLess, !sawGreater { return .equal }
|
}
|
||||||
|
if sawGreater, !sawLess, !sawEqual {
|
||||||
|
return .greaterThan
|
||||||
|
}
|
||||||
|
if sawEqual, !sawLess, !sawGreater {
|
||||||
|
return .equal
|
||||||
|
}
|
||||||
return .incomparable
|
return .incomparable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -35,19 +35,25 @@ extension PureXML.Schema {
|
|||||||
extension PureXML.Schema.SimpleType {
|
extension PureXML.Schema.SimpleType {
|
||||||
/// Whether this is `xs:ID` (or an atomic restriction of it).
|
/// Whether this is `xs:ID` (or an atomic restriction of it).
|
||||||
var isID: Bool {
|
var isID: Bool {
|
||||||
if case .atomic = variety { return base == .id }
|
if case .atomic = variety {
|
||||||
|
return base == .id
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this is `xs:IDREF` (or an atomic restriction of it).
|
/// Whether this is `xs:IDREF` (or an atomic restriction of it).
|
||||||
var isIDReference: Bool {
|
var isIDReference: Bool {
|
||||||
if case .atomic = variety { return base == .idref }
|
if case .atomic = variety {
|
||||||
|
return base == .idref
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this is `xs:IDREFS` (a list whose item type is `xs:IDREF`).
|
/// Whether this is `xs:IDREFS` (a list whose item type is `xs:IDREF`).
|
||||||
var isIDReferenceList: Bool {
|
var isIDReferenceList: Bool {
|
||||||
if case let .list(item) = variety { return item.base == .idref }
|
if case let .list(item) = variety {
|
||||||
|
return item.base == .idref
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,9 @@ extension PureXML.Schema.XSDParser {
|
|||||||
var current: XSDTree? = start
|
var current: XSDTree? = start
|
||||||
for rawStep in path.split(separator: "/", omittingEmptySubsequences: false) {
|
for rawStep in path.split(separator: "/", omittingEmptySubsequences: false) {
|
||||||
let step = String(rawStep).trimmingXMLWhitespace()
|
let step = String(rawStep).trimmingXMLWhitespace()
|
||||||
if step == "." { continue }
|
if step == "." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
guard isSimpleNameTest(step), let node = current,
|
guard isSimpleNameTest(step), let node = current,
|
||||||
let child = directChildElement(named: PureXML.Schema.XSDNode.stripPrefix(step), of: node)
|
let child = directChildElement(named: PureXML.Schema.XSDNode.stripPrefix(step), of: node)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
@ -92,7 +94,9 @@ extension PureXML.Schema.XSDParser {
|
|||||||
}
|
}
|
||||||
return local == "anyType"
|
return local == "anyType"
|
||||||
}
|
}
|
||||||
if PureXML.Schema.XSDNode.firstChild(element, named: "simpleType") != nil { return false }
|
if PureXML.Schema.XSDNode.firstChild(element, named: "simpleType") != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if let complex = PureXML.Schema.XSDNode.firstChild(element, named: "complexType") {
|
if let complex = PureXML.Schema.XSDNode.firstChild(element, named: "complexType") {
|
||||||
return PureXML.Schema.XSDNode.firstChild(complex, named: "simpleContent") == nil
|
return PureXML.Schema.XSDNode.firstChild(complex, named: "simpleContent") == nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,9 @@ extension PureXML.Schema.IdentityValidator {
|
|||||||
let fieldType = childElementType(named: field, in: targetComplex.content),
|
let fieldType = childElementType(named: field, in: targetComplex.content),
|
||||||
case let .complex(fieldComplex) = fieldType
|
case let .complex(fieldComplex) = fieldType
|
||||||
else { return false }
|
else { return false }
|
||||||
if case .simpleContent = fieldComplex.content { return false }
|
if case .simpleContent = fieldComplex.content {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,9 @@ struct TupleSet {
|
|||||||
key.reserveCapacity(tuple.count)
|
key.reserveCapacity(tuple.count)
|
||||||
for field in tuple {
|
for field in tuple {
|
||||||
guard let field, !field.string.contains(":") else { return nil }
|
guard let field, !field.string.contains(":") else { return nil }
|
||||||
if let type = field.type, !type.comparesRawLexically { return nil }
|
if let type = field.type, !type.comparesRawLexically {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
key.append(field.string)
|
key.append(field.string)
|
||||||
}
|
}
|
||||||
return key
|
return key
|
||||||
|
|||||||
@ -49,11 +49,22 @@ extension PureXML.Schema {
|
|||||||
_ root: PureXML.Model.TreeNode,
|
_ root: PureXML.Model.TreeNode,
|
||||||
at path: [PureXML.Validation.PathKey] = [],
|
at path: [PureXML.Validation.PathKey] = [],
|
||||||
) -> [PureXML.Validation.ValidationError] {
|
) -> [PureXML.Validation.ValidationError] {
|
||||||
|
validateTagged(root, at: path).map(\.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tagged form of ``validate(_:at:)``: the same computation, with each
|
||||||
|
/// finding labeled by the decomposed identity rule it belongs to. The
|
||||||
|
/// public, untagged form is a thin `.map(\.error)` over this so behavior and
|
||||||
|
/// output stay identical for every existing caller.
|
||||||
|
func validateTagged(
|
||||||
|
_ root: PureXML.Model.TreeNode,
|
||||||
|
at path: [PureXML.Validation.PathKey] = [],
|
||||||
|
) -> [XSDFinding] {
|
||||||
guard !constraints.isEmpty else { return [] }
|
guard !constraints.isEmpty else { return [] }
|
||||||
// A selector or field XPath that does not compile is a schema-author
|
// A selector or field XPath that does not compile is a schema-author
|
||||||
// error, reported once up front rather than silently disabling the
|
// error, reported once up front rather than silently disabling the
|
||||||
// constraint (the broken query still evaluates as no-match below).
|
// constraint (the broken query still evaluates as no-match below).
|
||||||
var issues: [PureXML.Validation.ValidationError] = compileErrors(at: path)
|
var issues: [XSDFinding] = compileErrors(at: path)
|
||||||
walk(root, at: path, scopes: [], into: &issues)
|
walk(root, at: path, scopes: [], into: &issues)
|
||||||
return issues
|
return issues
|
||||||
}
|
}
|
||||||
@ -61,15 +72,19 @@ extension PureXML.Schema {
|
|||||||
/// One located error per constraint XPath that fails to compile, in a
|
/// One located error per constraint XPath that fails to compile, in a
|
||||||
/// deterministic order. The failed compilations are cached, so the walk
|
/// deterministic order. The failed compilations are cached, so the walk
|
||||||
/// afterwards treats those constraints as matching nothing.
|
/// afterwards treats those constraints as matching nothing.
|
||||||
private func compileErrors(at path: [PureXML.Validation.PathKey]) -> [PureXML.Validation.ValidationError] {
|
private func compileErrors(at path: [PureXML.Validation.PathKey]) -> [XSDFinding] {
|
||||||
var errors: [PureXML.Validation.ValidationError] = []
|
var errors: [XSDFinding] = []
|
||||||
for key in constraints.keys.sorted() {
|
for key in constraints.keys.sorted() {
|
||||||
for constraint in constraints[key] ?? [] {
|
for constraint in constraints[key] ?? [] {
|
||||||
if cache.query(constraint.selector) == nil {
|
if cache.query(constraint.selector) == nil {
|
||||||
errors.append(.init(reason: "identity constraint '\(constraint.name)': invalid selector XPath '\(constraint.selector)'", at: path))
|
errors.append(XSDFinding(
|
||||||
|
.identityConstraintXPathsValid,
|
||||||
|
reason: "identity constraint '\(constraint.name)': invalid selector XPath '\(constraint.selector)'",
|
||||||
|
at: path,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
for field in constraint.fields where cache.query(field) == nil {
|
for field in constraint.fields where cache.query(field) == nil {
|
||||||
errors.append(.init(reason: "identity constraint '\(constraint.name)': invalid field XPath '\(field)'", at: path))
|
errors.append(XSDFinding(.identityConstraintXPathsValid, reason: "identity constraint '\(constraint.name)': invalid field XPath '\(field)'", at: path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,7 +95,7 @@ extension PureXML.Schema {
|
|||||||
_ node: PureXML.Model.TreeNode,
|
_ node: PureXML.Model.TreeNode,
|
||||||
at path: [PureXML.Validation.PathKey],
|
at path: [PureXML.Validation.PathKey],
|
||||||
scopes: [[String: [[FieldValue?]]]],
|
scopes: [[String: [[FieldValue?]]]],
|
||||||
into issues: inout [PureXML.Validation.ValidationError],
|
into issues: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
var frame: [String: [[FieldValue?]]] = [:]
|
var frame: [String: [[FieldValue?]]] = [:]
|
||||||
let declared = node.name.flatMap { constraints[$0.localName] } ?? []
|
let declared = node.name.flatMap { constraints[$0.localName] } ?? []
|
||||||
@ -132,7 +147,7 @@ extension PureXML.Schema {
|
|||||||
at node: PureXML.Model.TreeNode,
|
at node: PureXML.Model.TreeNode,
|
||||||
path: [PureXML.Validation.PathKey],
|
path: [PureXML.Validation.PathKey],
|
||||||
frame: inout [String: [[FieldValue?]]],
|
frame: inout [String: [[FieldValue?]]],
|
||||||
into issues: inout [PureXML.Validation.ValidationError],
|
into issues: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
guard isNonRef(constraint) else { return }
|
guard isNonRef(constraint) else { return }
|
||||||
var tuples: [[FieldValue?]] = []
|
var tuples: [[FieldValue?]] = []
|
||||||
@ -145,15 +160,19 @@ extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
|
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
|
||||||
if constraint.kind == .key, tuple.contains(where: { $0 == nil }) {
|
if constraint.kind == .key, tuple.contains(where: { $0 == nil }) {
|
||||||
issues.append(.init(
|
issues.append(XSDFinding(
|
||||||
|
.identityKeyFieldPresent,
|
||||||
reason: "key '\(constraint.name)': a field is missing",
|
reason: "key '\(constraint.name)': a field is missing",
|
||||||
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
|
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
|
||||||
))
|
))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if tuple.contains(where: { $0 == nil }) { continue }
|
if tuple.contains(where: { $0 == nil }) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if seen.contains(tuple) {
|
if seen.contains(tuple) {
|
||||||
issues.append(.init(
|
issues.append(XSDFinding(
|
||||||
|
.identityValueUnique,
|
||||||
reason: "\(label(constraint)) '\(constraint.name)': duplicate value",
|
reason: "\(label(constraint)) '\(constraint.name)': duplicate value",
|
||||||
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
|
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
|
||||||
))
|
))
|
||||||
@ -170,7 +189,7 @@ extension PureXML.Schema {
|
|||||||
at node: PureXML.Model.TreeNode,
|
at node: PureXML.Model.TreeNode,
|
||||||
path: [PureXML.Validation.PathKey],
|
path: [PureXML.Validation.PathKey],
|
||||||
scopes: [[String: [[FieldValue?]]]],
|
scopes: [[String: [[FieldValue?]]]],
|
||||||
into issues: inout [PureXML.Validation.ValidationError],
|
into issues: inout [XSDFinding],
|
||||||
) {
|
) {
|
||||||
guard case let .keyref(refer) = constraint.kind else { return }
|
guard case let .keyref(refer) = constraint.kind else { return }
|
||||||
let keyTuples = scopes.reversed().compactMap { $0[refer] }.first ?? []
|
let keyTuples = scopes.reversed().compactMap { $0[refer] }.first ?? []
|
||||||
@ -185,9 +204,12 @@ extension PureXML.Schema {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
|
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
|
||||||
if tuple.contains(where: { $0 == nil }) { continue }
|
if tuple.contains(where: { $0 == nil }) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !keys.contains(tuple) {
|
if !keys.contains(tuple) {
|
||||||
issues.append(.init(
|
issues.append(XSDFinding(
|
||||||
|
.identityKeyrefResolves,
|
||||||
reason: "keyref '\(constraint.name)': no matching key '\(refer)'",
|
reason: "keyref '\(constraint.name)': no matching key '\(refer)'",
|
||||||
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
|
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
|
||||||
))
|
))
|
||||||
@ -203,13 +225,14 @@ extension PureXML.Schema {
|
|||||||
_ constraint: IdentityConstraint,
|
_ constraint: IdentityConstraint,
|
||||||
at target: PureXML.Model.TreeNode,
|
at target: PureXML.Model.TreeNode,
|
||||||
path targetPath: [PureXML.Validation.PathKey],
|
path targetPath: [PureXML.Validation.PathKey],
|
||||||
) -> PureXML.Validation.ValidationError? {
|
) -> XSDFinding? {
|
||||||
let step = fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings)
|
let step = fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings)
|
||||||
for field in constraint.fields {
|
for field in constraint.fields {
|
||||||
let selected = select(field, at: target, namespaces: constraint.namespaceBindings)
|
let selected = select(field, at: target, namespaces: constraint.namespaceBindings)
|
||||||
// cvc-identity-constraint.3: each field must evaluate to at most one node.
|
// cvc-identity-constraint.3: each field must evaluate to at most one node.
|
||||||
if selected.count > 1 {
|
if selected.count > 1 {
|
||||||
return .init(
|
return XSDFinding(
|
||||||
|
.identityFieldCardinality,
|
||||||
reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must select at most one node",
|
reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must select at most one node",
|
||||||
at: targetPath + step,
|
at: targetPath + step,
|
||||||
)
|
)
|
||||||
@ -235,7 +258,8 @@ extension PureXML.Schema {
|
|||||||
&& Self.xsiTypeLocalName(of: node) == nil
|
&& Self.xsiTypeLocalName(of: node) == nil
|
||||||
} ?? false
|
} ?? false
|
||||||
if element != nil, hasElementChildren || declaredNonSimple || fieldTargetIsComplexNonSimple(field, at: target) {
|
if element != nil, hasElementChildren || declaredNonSimple || fieldTargetIsComplexNonSimple(field, at: target) {
|
||||||
return .init(
|
return XSDFinding(
|
||||||
|
.identityFieldMustBeSimple,
|
||||||
reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must identify a node with simple content",
|
reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must identify a node with simple content",
|
||||||
at: targetPath + step,
|
at: targetPath + step,
|
||||||
)
|
)
|
||||||
@ -270,7 +294,9 @@ extension PureXML.Schema {
|
|||||||
if let defaulted = defaultedValue(field, value: value, type: type, constraint: constraint, at: node) {
|
if let defaulted = defaultedValue(field, value: value, type: type, constraint: constraint, at: node) {
|
||||||
return defaulted
|
return defaulted
|
||||||
}
|
}
|
||||||
if let nodes = value.nodes, nodes.isEmpty { return nil }
|
if let nodes = value.nodes, nodes.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return FieldValue(string: value.string, type: type ?? Self.effectiveType(of: value), namespaceBindings: instanceNamespaces)
|
return FieldValue(string: value.string, type: type ?? Self.effectiveType(of: value), namespaceBindings: instanceNamespaces)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,69 +345,3 @@ extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PureXML.Schema.IdentityValidator {
|
|
||||||
func simpleType(of declaration: PureXML.Schema.ElementType) -> PureXML.Schema.SimpleType? {
|
|
||||||
switch declaration {
|
|
||||||
case let .simple(simple): simple
|
|
||||||
case let .complex(complex):
|
|
||||||
if case let .simpleContent(simple) = complex.content { simple } else { nil }
|
|
||||||
case .typeReference: nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func effectiveType(of value: PureXML.XPath.Value) -> PureXML.Schema.SimpleType? {
|
|
||||||
guard let nodes = value.nodes, case let .tree(treeNode)? = nodes.first,
|
|
||||||
treeNode.kind == .element, let local = xsiTypeLocalName(of: treeNode),
|
|
||||||
let builtin = PureXML.Schema.BuiltinType(rawValue: local)
|
|
||||||
else { return nil }
|
|
||||||
return PureXML.Schema.SimpleType(base: builtin)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The local name of a tree node's `xsi:type` attribute value, recognized by
|
|
||||||
/// the schema-instance namespace or the conventional `xsi` prefix, or nil.
|
|
||||||
static func xsiTypeLocalName(of node: PureXML.Model.TreeNode) -> String? {
|
|
||||||
let schemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
guard let attribute = node.attributes.first(where: {
|
|
||||||
$0.name.localName == "type" && ($0.name.namespaceURI == schemaInstance || $0.name.prefix == "xsi")
|
|
||||||
}) else { return nil }
|
|
||||||
return attribute.value.split(separator: ":").last.map(String.init) ?? attribute.value
|
|
||||||
}
|
|
||||||
|
|
||||||
func label(_ constraint: PureXML.Schema.IdentityConstraint) -> String {
|
|
||||||
constraint.kind == .key ? "key" : "unique"
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a constraint collects tuples (`key`/`unique`) rather than
|
|
||||||
/// referencing them (`keyref`).
|
|
||||||
func isNonRef(_ constraint: PureXML.Schema.IdentityConstraint) -> Bool {
|
|
||||||
if case .keyref = constraint.kind { return false }
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The coding-path steps from the constraint-bearing `container` down to a
|
|
||||||
/// selected `target`, so an identity error locates the actual offending
|
|
||||||
/// element rather than the element that merely declares the constraint.
|
|
||||||
/// Returns an empty path if `target` is not a descendant of `container`.
|
|
||||||
func steps(from container: PureXML.Model.TreeNode, to target: PureXML.Model.TreeNode) -> [PureXML.Validation.PathKey] {
|
|
||||||
var chain: [PureXML.Model.TreeNode] = []
|
|
||||||
var current = target
|
|
||||||
while current !== container {
|
|
||||||
guard let parent = current.parent else { return [] }
|
|
||||||
chain.append(current)
|
|
||||||
current = parent
|
|
||||||
}
|
|
||||||
var steps: [PureXML.Validation.PathKey] = []
|
|
||||||
var parent = container
|
|
||||||
for node in chain.reversed() {
|
|
||||||
let name = node.name?.description ?? ""
|
|
||||||
if let index = stepIndex.position(of: node, under: parent, name: name) {
|
|
||||||
steps.append(.element(name, index: index))
|
|
||||||
} else {
|
|
||||||
steps.append(.element(name))
|
|
||||||
}
|
|
||||||
parent = node
|
|
||||||
}
|
|
||||||
return steps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -23,7 +23,9 @@ extension PureXML.Schema.IdentityValidator {
|
|||||||
/// character content; any other field is empty only on an empty node-set.
|
/// character content; any other field is empty only on an empty node-set.
|
||||||
func isFieldEmpty(_ field: String, value: PureXML.XPath.Value) -> Bool {
|
func isFieldEmpty(_ field: String, value: PureXML.XPath.Value) -> Bool {
|
||||||
guard let nodes = value.nodes else { return false }
|
guard let nodes = value.nodes else { return false }
|
||||||
if field.hasPrefix("@") { return nodes.isEmpty }
|
if field.hasPrefix("@") {
|
||||||
|
return nodes.isEmpty
|
||||||
|
}
|
||||||
if field == "." {
|
if field == "." {
|
||||||
guard case let .tree(element)? = nodes.first, element.kind == .element else { return nodes.isEmpty }
|
guard case let .tree(element)? = nodes.first, element.kind == .element else { return nodes.isEmpty }
|
||||||
return !element.children.contains { $0.kind == .text || $0.kind == .cdata }
|
return !element.children.contains { $0.kind == .text || $0.kind == .cdata }
|
||||||
@ -41,7 +43,9 @@ extension PureXML.Schema.IdentityValidator {
|
|||||||
) -> PureXML.Schema.ValueConstraint? {
|
) -> PureXML.Schema.ValueConstraint? {
|
||||||
if field == ".", let local = target.name?.localName {
|
if field == ".", let local = target.name?.localName {
|
||||||
let key = PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field, targetLocal: local)
|
let key = PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field, targetLocal: local)
|
||||||
if let value = fieldConstraints[key] { return value }
|
if let value = fieldConstraints[key] {
|
||||||
|
return value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return fieldConstraints[PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field)]
|
return fieldConstraints[PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field)]
|
||||||
}
|
}
|
||||||
|
|||||||
71
Sources/Schema/IdentityValidatorHelpers.swift
Normal file
71
Sources/Schema/IdentityValidatorHelpers.swift
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
extension PureXML.Schema.IdentityValidator {
|
||||||
|
func simpleType(of declaration: PureXML.Schema.ElementType) -> PureXML.Schema.SimpleType? {
|
||||||
|
switch declaration {
|
||||||
|
case let .simple(simple): simple
|
||||||
|
case let .complex(complex):
|
||||||
|
if case let .simpleContent(simple) = complex.content {
|
||||||
|
simple
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
case .typeReference: nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func effectiveType(of value: PureXML.XPath.Value) -> PureXML.Schema.SimpleType? {
|
||||||
|
guard let nodes = value.nodes, case let .tree(treeNode)? = nodes.first,
|
||||||
|
treeNode.kind == .element, let local = xsiTypeLocalName(of: treeNode),
|
||||||
|
let builtin = PureXML.Schema.BuiltinType(rawValue: local)
|
||||||
|
else { return nil }
|
||||||
|
return PureXML.Schema.SimpleType(base: builtin)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The local name of a tree node's `xsi:type` attribute value, recognized by
|
||||||
|
/// the schema-instance namespace or the conventional `xsi` prefix, or nil.
|
||||||
|
static func xsiTypeLocalName(of node: PureXML.Model.TreeNode) -> String? {
|
||||||
|
let schemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
guard let attribute = node.attributes.first(where: {
|
||||||
|
$0.name.localName == "type" && ($0.name.namespaceURI == schemaInstance || $0.name.prefix == "xsi")
|
||||||
|
}) else { return nil }
|
||||||
|
return attribute.value.split(separator: ":").last.map(String.init) ?? attribute.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func label(_ constraint: PureXML.Schema.IdentityConstraint) -> String {
|
||||||
|
constraint.kind == .key ? "key" : "unique"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a constraint collects tuples (`key`/`unique`) rather than
|
||||||
|
/// referencing them (`keyref`).
|
||||||
|
func isNonRef(_ constraint: PureXML.Schema.IdentityConstraint) -> Bool {
|
||||||
|
if case .keyref = constraint.kind {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The coding-path steps from the constraint-bearing `container` down to a
|
||||||
|
/// selected `target`, so an identity error locates the actual offending
|
||||||
|
/// element rather than the element that merely declares the constraint.
|
||||||
|
/// Returns an empty path if `target` is not a descendant of `container`.
|
||||||
|
func steps(from container: PureXML.Model.TreeNode, to target: PureXML.Model.TreeNode) -> [PureXML.Validation.PathKey] {
|
||||||
|
var chain: [PureXML.Model.TreeNode] = []
|
||||||
|
var current = target
|
||||||
|
while current !== container {
|
||||||
|
guard let parent = current.parent else { return [] }
|
||||||
|
chain.append(current)
|
||||||
|
current = parent
|
||||||
|
}
|
||||||
|
var steps: [PureXML.Validation.PathKey] = []
|
||||||
|
var parent = container
|
||||||
|
for node in chain.reversed() {
|
||||||
|
let name = node.name?.description ?? ""
|
||||||
|
if let index = stepIndex.position(of: node, under: parent, name: name) {
|
||||||
|
steps.append(.element(name, index: index))
|
||||||
|
} else {
|
||||||
|
steps.append(.element(name))
|
||||||
|
}
|
||||||
|
parent = node
|
||||||
|
}
|
||||||
|
return steps
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,9 +9,13 @@ extension PureXML.Schema {
|
|||||||
/// `float`/`double`: an optional-sign mantissa with an optional exponent,
|
/// `float`/`double`: an optional-sign mantissa with an optional exponent,
|
||||||
/// or the special values `INF`, `-INF`, `NaN`.
|
/// or the special values `INF`, `-INF`, `NaN`.
|
||||||
static func isFloating(_ value: String) -> Bool {
|
static func isFloating(_ value: String) -> Bool {
|
||||||
if value == "INF" || value == "-INF" || value == "NaN" { return true }
|
if value == "INF" || value == "-INF" || value == "NaN" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
var characters = Substring(value)
|
var characters = Substring(value)
|
||||||
if characters.first == "+" || characters.first == "-" { characters = characters.dropFirst() }
|
if characters.first == "+" || characters.first == "-" {
|
||||||
|
characters = characters.dropFirst()
|
||||||
|
}
|
||||||
let exponentSplit = characters.split(separator: "e", omittingEmptySubsequences: false)
|
let exponentSplit = characters.split(separator: "e", omittingEmptySubsequences: false)
|
||||||
let altSplit = characters.split(separator: "E", omittingEmptySubsequences: false)
|
let altSplit = characters.split(separator: "E", omittingEmptySubsequences: false)
|
||||||
let parts = exponentSplit.count == 2 ? exponentSplit : altSplit
|
let parts = exponentSplit.count == 2 ? exponentSplit : altSplit
|
||||||
@ -31,7 +35,9 @@ extension PureXML.Schema {
|
|||||||
|
|
||||||
private static func isSignedInteger(_ value: Substring) -> Bool {
|
private static func isSignedInteger(_ value: Substring) -> Bool {
|
||||||
var digits = value
|
var digits = value
|
||||||
if digits.first == "+" || digits.first == "-" { digits = digits.dropFirst() }
|
if digits.first == "+" || digits.first == "-" {
|
||||||
|
digits = digits.dropFirst()
|
||||||
|
}
|
||||||
return !digits.isEmpty && digits.allSatisfy(\.isNumber)
|
return !digits.isEmpty && digits.allSatisfy(\.isNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +45,9 @@ extension PureXML.Schema {
|
|||||||
/// least one component and no empty `T`.
|
/// least one component and no empty `T`.
|
||||||
static func isDuration(_ value: String) -> Bool {
|
static func isDuration(_ value: String) -> Bool {
|
||||||
var rest = Substring(value)
|
var rest = Substring(value)
|
||||||
if rest.first == "-" { rest = rest.dropFirst() }
|
if rest.first == "-" {
|
||||||
|
rest = rest.dropFirst()
|
||||||
|
}
|
||||||
guard rest.first == "P" else { return false }
|
guard rest.first == "P" else { return false }
|
||||||
rest = rest.dropFirst()
|
rest = rest.dropFirst()
|
||||||
let timeSplit = rest.split(separator: "T", omittingEmptySubsequences: false)
|
let timeSplit = rest.split(separator: "T", omittingEmptySubsequences: false)
|
||||||
|
|||||||
@ -14,7 +14,9 @@ public extension PureXML.Schema {
|
|||||||
|
|
||||||
public init?(lexical value: String) {
|
public init?(lexical value: String) {
|
||||||
var raw = Substring(value.trimmingXMLWhitespace())
|
var raw = Substring(value.trimmingXMLWhitespace())
|
||||||
if raw.first == "+" { raw = raw.dropFirst() }
|
if raw.first == "+" {
|
||||||
|
raw = raw.dropFirst()
|
||||||
|
}
|
||||||
guard !raw.isEmpty, raw.allSatisfy({ $0.isASCII && $0.isNumber }) else {
|
guard !raw.isEmpty, raw.allSatisfy({ $0.isASCII && $0.isNumber }) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -32,14 +34,18 @@ public extension PureXML.Schema {
|
|||||||
|
|
||||||
func clamped(to limit: Int) -> Int {
|
func clamped(to limit: Int) -> Int {
|
||||||
precondition(limit >= 0, "occurrence clamp limit must be non-negative")
|
precondition(limit >= 0, "occurrence clamp limit must be non-negative")
|
||||||
if isGreaterThan(limit) { return limit }
|
if isGreaterThan(limit) {
|
||||||
|
return limit
|
||||||
|
}
|
||||||
return Int(digits) ?? limit
|
return Int(digits) ?? limit
|
||||||
}
|
}
|
||||||
|
|
||||||
func isGreaterThan(_ value: Int) -> Bool {
|
func isGreaterThan(_ value: Int) -> Bool {
|
||||||
precondition(value >= 0, "occurrence comparison value must be non-negative")
|
precondition(value >= 0, "occurrence comparison value must be non-negative")
|
||||||
let other = String(value)
|
let other = String(value)
|
||||||
if digits.count != other.count { return digits.count > other.count }
|
if digits.count != other.count {
|
||||||
|
return digits.count > other.count
|
||||||
|
}
|
||||||
return digits > other
|
return digits > other
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +75,9 @@ public extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var isZero: Bool {
|
public var isZero: Bool {
|
||||||
if case let .finite(value) = self { return value.isZero }
|
if case let .finite(value) = self {
|
||||||
|
return value.isZero
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,9 @@ extension PureXML.Schema.Particle {
|
|||||||
var members: [Self] = []
|
var members: [Self] = []
|
||||||
for child in group.particles {
|
for child in group.particles {
|
||||||
let normalized = child.normalized()
|
let normalized = child.normalized()
|
||||||
if normalized.maxOccurs == 0 { continue }
|
if normalized.maxOccurs == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
let splice = normalized.minOccurs == 1 && normalized.maxOccurs == 1
|
let splice = normalized.minOccurs == 1 && normalized.maxOccurs == 1
|
||||||
if case let .group(inner) = normalized.term, inner.compositor == group.compositor, splice {
|
if case let .group(inner) = normalized.term, inner.compositor == group.compositor, splice {
|
||||||
members.append(contentsOf: inner.particles)
|
members.append(contentsOf: inner.particles)
|
||||||
|
|||||||
@ -187,7 +187,9 @@ extension PureXML.Schema {
|
|||||||
/// has `maxOccurs=0`, or is a group all of whose members are content-free
|
/// has `maxOccurs=0`, or is a group all of whose members are content-free
|
||||||
/// (an empty group included). Its only legal content is the empty sequence.
|
/// (an empty group included). Its only legal content is the empty sequence.
|
||||||
static func contentFree(_ particle: Particle) -> Bool {
|
static func contentFree(_ particle: Particle) -> Bool {
|
||||||
if particle.maxOccurs == 0 { return true }
|
if particle.maxOccurs == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
switch particle.term {
|
switch particle.term {
|
||||||
case .element, .wildcard:
|
case .element, .wildcard:
|
||||||
return false
|
return false
|
||||||
@ -216,13 +218,19 @@ extension PureXML.Schema {
|
|||||||
matched = true
|
matched = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if skippedMustBeEmptiable, !emptiable(candidate) { return false }
|
if skippedMustBeEmptiable, !emptiable(candidate) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
if !matched { return false }
|
|
||||||
}
|
}
|
||||||
if skippedMustBeEmptiable {
|
if skippedMustBeEmptiable {
|
||||||
while baseIndex < base.count {
|
while baseIndex < base.count {
|
||||||
if !emptiable(base[baseIndex]) { return false }
|
if !emptiable(base[baseIndex]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
baseIndex += 1
|
baseIndex += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -253,7 +261,9 @@ extension PureXML.Schema {
|
|||||||
|
|
||||||
/// Whether a particle can match no children at all.
|
/// Whether a particle can match no children at all.
|
||||||
static func emptiable(_ particle: Particle) -> Bool {
|
static func emptiable(_ particle: Particle) -> Bool {
|
||||||
if particle.minOccurs == 0 { return true }
|
if particle.minOccurs == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
switch particle.term {
|
switch particle.term {
|
||||||
case .element, .wildcard:
|
case .element, .wildcard:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -50,7 +50,9 @@ extension PureXML.Schema.ParticleRestriction {
|
|||||||
// declared union whose members merely subset the base union's is
|
// declared union whose members merely subset the base union's is
|
||||||
// NOT a derivation (particlesIk011); structural member subsetting
|
// NOT a derivation (particlesIk011); structural member subsetting
|
||||||
// is not the spec's derivation relation.
|
// is not the spec's derivation relation.
|
||||||
if typeDerivesByRestriction(restrictedTypeName, baseTypeName, derivation, types) { return true }
|
if typeDerivesByRestriction(restrictedTypeName, baseTypeName, derivation, types) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return baseMembers.contains { PureXML.Schema.XSDParser.isSimpleTypeRestrictionOK(derived: derivedSimple, base: $0) }
|
return baseMembers.contains { PureXML.Schema.XSDParser.isSimpleTypeRestrictionOK(derived: derivedSimple, base: $0) }
|
||||||
}
|
}
|
||||||
case (.complex, .simple):
|
case (.complex, .simple):
|
||||||
@ -62,7 +64,9 @@ extension PureXML.Schema.ParticleRestriction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if baseIsUnion(baseTypeName, types) { return true }
|
if baseIsUnion(baseTypeName, types) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
// NameAndTypeOK.2.2: a restricting element's type must be validly derived from
|
// NameAndTypeOK.2.2: a restricting element's type must be validly derived from
|
||||||
// the base element's type EXCLUDING extension (and list/union), i.e. by
|
// the base element's type EXCLUDING extension (and list/union), i.e. by
|
||||||
// restriction only. A type that reaches the base through an extension step is
|
// restriction only. A type that reaches the base through an extension step is
|
||||||
@ -79,7 +83,9 @@ extension PureXML.Schema.ParticleRestriction {
|
|||||||
while let resolved = current, steps <= types.count {
|
while let resolved = current, steps <= types.count {
|
||||||
switch resolved {
|
switch resolved {
|
||||||
case let .simple(simple):
|
case let .simple(simple):
|
||||||
if case .union = simple.variety { return true }
|
if case .union = simple.variety {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
case .complex:
|
case .complex:
|
||||||
return false
|
return false
|
||||||
@ -112,11 +118,15 @@ extension PureXML.Schema.ParticleRestriction {
|
|||||||
_ derivation: [String: PureXML.Schema.TypeDerivation],
|
_ derivation: [String: PureXML.Schema.TypeDerivation],
|
||||||
_ types: [String: PureXML.Schema.ElementType],
|
_ types: [String: PureXML.Schema.ElementType],
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
if isUrTypeName(base, types) { return true }
|
if isUrTypeName(base, types) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
var current = derived
|
var current = derived
|
||||||
var visited: Set<String> = []
|
var visited: Set<String> = []
|
||||||
while visited.insert(current).inserted {
|
while visited.insert(current).inserted {
|
||||||
if current == base { return true }
|
if current == base {
|
||||||
|
return true
|
||||||
|
}
|
||||||
guard let step = derivation[current] else { break }
|
guard let step = derivation[current] else { break }
|
||||||
current = step.base
|
current = step.base
|
||||||
}
|
}
|
||||||
@ -139,11 +149,15 @@ extension PureXML.Schema.ParticleRestriction {
|
|||||||
_ derivation: [String: PureXML.Schema.TypeDerivation],
|
_ derivation: [String: PureXML.Schema.TypeDerivation],
|
||||||
_ types: [String: PureXML.Schema.ElementType],
|
_ types: [String: PureXML.Schema.ElementType],
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
if isUrTypeName(base, types) { return true }
|
if isUrTypeName(base, types) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
var current = derived
|
var current = derived
|
||||||
var visited: Set<String> = []
|
var visited: Set<String> = []
|
||||||
while visited.insert(current).inserted {
|
while visited.insert(current).inserted {
|
||||||
if current == base { return true }
|
if current == base {
|
||||||
|
return true
|
||||||
|
}
|
||||||
guard let step = derivation[current], step.method == .restriction else { break }
|
guard let step = derivation[current], step.method == .restriction else { break }
|
||||||
current = step.base
|
current = step.base
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,7 +58,9 @@ extension RNCParser {
|
|||||||
private func qualifiedName(_ word: String, attribute: Bool) -> NameClass {
|
private func qualifiedName(_ word: String, attribute: Bool) -> NameClass {
|
||||||
let parts = word.split(separator: ":", maxSplits: 1).map(String.init)
|
let parts = word.split(separator: ":", maxSplits: 1).map(String.init)
|
||||||
if parts.count == 2 {
|
if parts.count == 2 {
|
||||||
if parts[1] == "*" { return .nsName(namespaces[parts[0]] ?? "") }
|
if parts[1] == "*" {
|
||||||
|
return .nsName(namespaces[parts[0]] ?? "")
|
||||||
|
}
|
||||||
return .name(namespace: namespaces[parts[0]] ?? "", localName: parts[1])
|
return .name(namespace: namespaces[parts[0]] ?? "", localName: parts[1])
|
||||||
}
|
}
|
||||||
return .name(namespace: attribute ? "" : defaultNamespace, localName: word)
|
return .name(namespace: attribute ? "" : defaultNamespace, localName: word)
|
||||||
@ -78,7 +80,8 @@ extension RNCParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func wordValue() -> String {
|
func wordValue() -> String {
|
||||||
if case let .word(word) = peek() { advance()
|
if case let .word(word) = peek() {
|
||||||
|
advance()
|
||||||
return word
|
return word
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
@ -93,11 +96,14 @@ extension RNCParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func expectSymbol(_ symbol: String) {
|
func expectSymbol(_ symbol: String) {
|
||||||
if peek() == .symbol(symbol) { advance() }
|
if peek() == .symbol(symbol) {
|
||||||
|
advance()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectString() -> String {
|
func expectString() -> String {
|
||||||
if case let .string(value) = peek() { advance()
|
if case let .string(value) = peek() {
|
||||||
|
advance()
|
||||||
return concatenated(value)
|
return concatenated(value)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@ -50,24 +50,33 @@ extension RNCParser {
|
|||||||
|
|
||||||
private func parseDefault() {
|
private func parseDefault() {
|
||||||
advance() // default
|
advance() // default
|
||||||
if case .word("namespace") = peek() { parseNamespace(isDefault: true) }
|
if case .word("namespace") = peek() {
|
||||||
|
parseNamespace(isDefault: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func parseNamespace(isDefault: Bool) {
|
private func parseNamespace(isDefault: Bool) {
|
||||||
advance() // namespace
|
advance() // namespace
|
||||||
var prefix = ""
|
var prefix = ""
|
||||||
if case let .word(name) = peek(), name != "=" { prefix = name
|
if case let .word(name) = peek(), name != "=" {
|
||||||
|
prefix = name
|
||||||
advance()
|
advance()
|
||||||
}
|
}
|
||||||
expectSymbol("=")
|
expectSymbol("=")
|
||||||
let uri = expectString()
|
let uri = expectString()
|
||||||
if isDefault { defaultNamespace = uri }
|
if isDefault {
|
||||||
if !prefix.isEmpty { namespaces[prefix] = uri }
|
defaultNamespace = uri
|
||||||
|
}
|
||||||
|
if !prefix.isEmpty {
|
||||||
|
namespaces[prefix] = uri
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func parseDatatypes() {
|
private func parseDatatypes() {
|
||||||
advance() // datatypes
|
advance() // datatypes
|
||||||
if case .word = peek() { advance() }
|
if case .word = peek() {
|
||||||
|
advance()
|
||||||
|
}
|
||||||
expectSymbol("=")
|
expectSymbol("=")
|
||||||
_ = expectString()
|
_ = expectString()
|
||||||
}
|
}
|
||||||
@ -76,9 +85,13 @@ extension RNCParser {
|
|||||||
|
|
||||||
private func looksLikeGrammar() -> Bool {
|
private func looksLikeGrammar() -> Bool {
|
||||||
guard case let .word(word) = peek() else { return false }
|
guard case let .word(word) = peek() else { return false }
|
||||||
if word == "include" || word == "start" { return true }
|
if word == "include" || word == "start" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
// A define is a word followed by one of =, |=, &=.
|
// A define is a word followed by one of =, |=, &=.
|
||||||
if case let .symbol(symbol) = peek(1), symbol == "=" || symbol == "|=" || symbol == "&=" { return true }
|
if case let .symbol(symbol) = peek(1), symbol == "=" || symbol == "|=" || symbol == "&=" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +131,11 @@ extension RNCParser {
|
|||||||
var depth = 1
|
var depth = 1
|
||||||
while position < tokens.count, depth > 0 {
|
while position < tokens.count, depth > 0 {
|
||||||
let token = advance()
|
let token = advance()
|
||||||
if token == .symbol("[") { depth += 1 } else if token == .symbol("]") { depth -= 1 }
|
if token == .symbol("[") {
|
||||||
|
depth += 1
|
||||||
|
} else if token == .symbol("]") {
|
||||||
|
depth -= 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,7 +165,9 @@ extension RNCParser {
|
|||||||
for (name, pattern) in sub.defines {
|
for (name, pattern) in sub.defines {
|
||||||
defines[name] = pattern
|
defines[name] = pattern
|
||||||
}
|
}
|
||||||
if startPattern == nil { startPattern = sub.startPattern }
|
if startPattern == nil {
|
||||||
|
startPattern = sub.startPattern
|
||||||
|
}
|
||||||
visited = sub.visited
|
visited = sub.visited
|
||||||
}
|
}
|
||||||
// An override block re-defines names after the include, overriding them.
|
// An override block re-defines names after the include, overriding them.
|
||||||
@ -212,7 +231,9 @@ extension RNCParser {
|
|||||||
while peek() == .symbol(">"), peek(1) == .symbol(">") {
|
while peek() == .symbol(">"), peek(1) == .symbol(">") {
|
||||||
advance()
|
advance()
|
||||||
advance()
|
advance()
|
||||||
if case .word = peek() { advance() }
|
if case .word = peek() {
|
||||||
|
advance()
|
||||||
|
}
|
||||||
skipAnnotation()
|
skipAnnotation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,8 +302,11 @@ extension RNCParser {
|
|||||||
guard word.contains(":") else { return .ref(word) }
|
guard word.contains(":") else { return .ref(word) }
|
||||||
let type = PureXML.Schema.BuiltinType(rawValue: strip(word)) ?? .string
|
let type = PureXML.Schema.BuiltinType(rawValue: strip(word)) ?? .string
|
||||||
var facets = PureXML.Schema.Facets()
|
var facets = PureXML.Schema.Facets()
|
||||||
if peek() == .symbol("{") { parseParams(into: &facets) }
|
if peek() == .symbol("{") {
|
||||||
if case let .string(value) = peek() { advance()
|
parseParams(into: &facets)
|
||||||
|
}
|
||||||
|
if case let .string(value) = peek() {
|
||||||
|
advance()
|
||||||
return .value(PureXML.Schema.SimpleType(base: type), concatenated(value))
|
return .value(PureXML.Schema.SimpleType(base: type), concatenated(value))
|
||||||
}
|
}
|
||||||
return .data(PureXML.Schema.SimpleType(base: type, facets: facets))
|
return .data(PureXML.Schema.SimpleType(base: type, facets: facets))
|
||||||
@ -297,12 +321,16 @@ extension RNCParser {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
advance()
|
advance()
|
||||||
if peek() == .symbol("=") { advance() }
|
if peek() == .symbol("=") {
|
||||||
|
advance()
|
||||||
|
}
|
||||||
guard case let .string(value) = peek() else { continue }
|
guard case let .string(value) = peek() else { continue }
|
||||||
advance()
|
advance()
|
||||||
PureXML.Schema.RelaxNGFacets.apply(name, value, into: &facets)
|
PureXML.Schema.RelaxNGFacets.apply(name, value, into: &facets)
|
||||||
}
|
}
|
||||||
if peek() == .symbol("}") { advance() }
|
if peek() == .symbol("}") {
|
||||||
|
advance()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func parseElement() -> Pattern {
|
private func parseElement() -> Pattern {
|
||||||
|
|||||||
@ -47,30 +47,54 @@ extension PureXML.Schema {
|
|||||||
// MARK: Simplifying constructors
|
// MARK: Simplifying constructors
|
||||||
|
|
||||||
func choice(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
func choice(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||||
if case .notAllowed = lhs { return rhs }
|
if case .notAllowed = lhs {
|
||||||
if case .notAllowed = rhs { return lhs }
|
return rhs
|
||||||
|
}
|
||||||
|
if case .notAllowed = rhs {
|
||||||
|
return lhs
|
||||||
|
}
|
||||||
return .choice(lhs, rhs)
|
return .choice(lhs, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func group(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
private func group(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||||
if case .notAllowed = lhs { return .notAllowed }
|
if case .notAllowed = lhs {
|
||||||
if case .notAllowed = rhs { return .notAllowed }
|
return .notAllowed
|
||||||
if case .empty = lhs { return rhs }
|
}
|
||||||
if case .empty = rhs { return lhs }
|
if case .notAllowed = rhs {
|
||||||
|
return .notAllowed
|
||||||
|
}
|
||||||
|
if case .empty = lhs {
|
||||||
|
return rhs
|
||||||
|
}
|
||||||
|
if case .empty = rhs {
|
||||||
|
return lhs
|
||||||
|
}
|
||||||
return .group(lhs, rhs)
|
return .group(lhs, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func interleave(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
private func interleave(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||||
if case .notAllowed = lhs { return .notAllowed }
|
if case .notAllowed = lhs {
|
||||||
if case .notAllowed = rhs { return .notAllowed }
|
return .notAllowed
|
||||||
if case .empty = lhs { return rhs }
|
}
|
||||||
if case .empty = rhs { return lhs }
|
if case .notAllowed = rhs {
|
||||||
|
return .notAllowed
|
||||||
|
}
|
||||||
|
if case .empty = lhs {
|
||||||
|
return rhs
|
||||||
|
}
|
||||||
|
if case .empty = rhs {
|
||||||
|
return lhs
|
||||||
|
}
|
||||||
return .interleave(lhs, rhs)
|
return .interleave(lhs, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func after(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
private func after(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||||
if case .notAllowed = lhs { return .notAllowed }
|
if case .notAllowed = lhs {
|
||||||
if case .notAllowed = rhs { return .notAllowed }
|
return .notAllowed
|
||||||
|
}
|
||||||
|
if case .notAllowed = rhs {
|
||||||
|
return .notAllowed
|
||||||
|
}
|
||||||
return .after(lhs, rhs)
|
return .after(lhs, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +179,9 @@ extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func valueMatches(_ content: PureXML.Schema.Pattern, _ value: String, env: [String: String]) -> Bool {
|
private func valueMatches(_ content: PureXML.Schema.Pattern, _ value: String, env: [String: String]) -> Bool {
|
||||||
if nullable(textDeriv(content, value, env: env)) { return true }
|
if nullable(textDeriv(content, value, env: env)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
// A whitespace-only attribute value also matches a pattern that
|
// A whitespace-only attribute value also matches a pattern that
|
||||||
// matches the empty sequence, the same leniency element content
|
// matches the empty sequence, the same leniency element content
|
||||||
// gets for ignorable whitespace.
|
// gets for ignorable whitespace.
|
||||||
@ -163,7 +189,9 @@ extension PureXML.Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func oneOrMoreP(_ inner: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
private func oneOrMoreP(_ inner: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||||
if case .notAllowed = inner { return .notAllowed }
|
if case .notAllowed = inner {
|
||||||
|
return .notAllowed
|
||||||
|
}
|
||||||
return .oneOrMore(inner)
|
return .oneOrMore(inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,7 +272,9 @@ extension PureXML.Schema.RelaxNGEngine {
|
|||||||
let coalesced = Self.coalesceText(children)
|
let coalesced = Self.coalesceText(children)
|
||||||
// Clark's algorithm treats an empty element as optional empty text, so
|
// Clark's algorithm treats an empty element as optional empty text, so
|
||||||
// a data/value pattern whose lexical space admits "" matches <e/>.
|
// a data/value pattern whose lexical space admits "" matches <e/>.
|
||||||
if coalesced.isEmpty { return choice(pattern, textDeriv(pattern, "", env: env)) }
|
if coalesced.isEmpty {
|
||||||
|
return choice(pattern, textDeriv(pattern, "", env: env))
|
||||||
|
}
|
||||||
if coalesced.count == 1, case let .text(string) = coalesced[0] {
|
if coalesced.count == 1, case let .text(string) = coalesced[0] {
|
||||||
let derived = textDeriv(pattern, string, env: env)
|
let derived = textDeriv(pattern, string, env: env)
|
||||||
return Self.isWhitespace(string) ? choice(pattern, derived) : derived
|
return Self.isWhitespace(string) ? choice(pattern, derived) : derived
|
||||||
@ -276,13 +306,16 @@ extension PureXML.Schema.RelaxNGEngine {
|
|||||||
case .comment, .processingInstruction:
|
case .comment, .processingInstruction:
|
||||||
continue
|
continue
|
||||||
default:
|
default:
|
||||||
if !run.isEmpty { result.append(.text(run))
|
if !run.isEmpty {
|
||||||
|
result.append(.text(run))
|
||||||
run = ""
|
run = ""
|
||||||
}
|
}
|
||||||
result.append(child)
|
result.append(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !run.isEmpty { result.append(.text(run)) }
|
if !run.isEmpty {
|
||||||
|
result.append(.text(run))
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +324,9 @@ extension PureXML.Schema.RelaxNGEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func isWhitespaceText(_ node: PureXML.Model.Node) -> Bool {
|
static func isWhitespaceText(_ node: PureXML.Model.Node) -> Bool {
|
||||||
if case let .text(value) = node { return isWhitespace(value) }
|
if case let .text(value) = node {
|
||||||
|
return isWhitespace(value)
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,7 +376,9 @@ extension PureXML.Schema.RelaxNGEngine {
|
|||||||
switch event {
|
switch event {
|
||||||
case let .startElement(name, attributes):
|
case let .startElement(name, attributes):
|
||||||
flushText(&state, closing: false)
|
flushText(&state, closing: false)
|
||||||
if !state.frames.isEmpty { state.frames[state.frames.count - 1].hadElementChild = true }
|
if !state.frames.isEmpty {
|
||||||
|
state.frames[state.frames.count - 1].hadElementChild = true
|
||||||
|
}
|
||||||
var env = state.frames.last?.env ?? Self.rootEnvironment
|
var env = state.frames.last?.env ?? Self.rootEnvironment
|
||||||
for attribute in attributes where Self.isNamespaceDeclaration(attribute) {
|
for attribute in attributes where Self.isNamespaceDeclaration(attribute) {
|
||||||
env[attribute.name.prefix == "xmlns" ? attribute.name.localName : ""] = attribute.value
|
env[attribute.name.prefix == "xmlns" ? attribute.name.localName : ""] = attribute.value
|
||||||
@ -353,11 +390,15 @@ extension PureXML.Schema.RelaxNGEngine {
|
|||||||
state.current = startTagCloseDeriv(attributed)
|
state.current = startTagCloseDeriv(attributed)
|
||||||
state.frames.append(RelaxNGStreamFrame(env: env))
|
state.frames.append(RelaxNGStreamFrame(env: env))
|
||||||
case let .characters(text), let .cdata(text):
|
case let .characters(text), let .cdata(text):
|
||||||
if !state.frames.isEmpty { state.frames[state.frames.count - 1].pendingText += text }
|
if !state.frames.isEmpty {
|
||||||
|
state.frames[state.frames.count - 1].pendingText += text
|
||||||
|
}
|
||||||
case .endElement:
|
case .endElement:
|
||||||
flushText(&state, closing: true)
|
flushText(&state, closing: true)
|
||||||
state.current = endTagDeriv(state.current)
|
state.current = endTagDeriv(state.current)
|
||||||
if !state.frames.isEmpty { state.frames.removeLast() }
|
if !state.frames.isEmpty {
|
||||||
|
state.frames.removeLast()
|
||||||
|
}
|
||||||
case .comment, .processingInstruction:
|
case .comment, .processingInstruction:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,12 +18,16 @@ extension PureXML.Schema.RelaxNGEngine {
|
|||||||
} else {
|
} else {
|
||||||
validateElement(opened, root, path, &errors)
|
validateElement(opened, root, path, &errors)
|
||||||
}
|
}
|
||||||
if errors.isEmpty { errors.append(.init(reason: "does not satisfy the RELAX NG schema", at: path)) }
|
if errors.isEmpty {
|
||||||
|
errors.append(.init(reason: "does not satisfy the RELAX NG schema", at: path))
|
||||||
|
}
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isNotAllowed(_ pattern: Pattern) -> Bool {
|
private func isNotAllowed(_ pattern: Pattern) -> Bool {
|
||||||
if case .notAllowed = pattern { return true }
|
if case .notAllowed = pattern {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +50,9 @@ extension PureXML.Schema.RelaxNGEngine {
|
|||||||
if isNotAllowed(closed) {
|
if isNotAllowed(closed) {
|
||||||
// A genuine missing/extra attribute, unless an attribute error above
|
// A genuine missing/extra attribute, unless an attribute error above
|
||||||
// already explains why the requirement is unsatisfied (no cascade).
|
// already explains why the requirement is unsatisfied (no cascade).
|
||||||
if errors.count == before { errors.append(.init(reason: "element <\(name)> is missing a required attribute", at: path)) }
|
if errors.count == before {
|
||||||
|
errors.append(.init(reason: "element <\(name)> is missing a required attribute", at: path))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// An attribute problem leaves the content model unreliable; stop here so a
|
// An attribute problem leaves the content model unreliable; stop here so a
|
||||||
|
|||||||
@ -23,8 +23,12 @@ enum RelaxNGGrammarCheck {
|
|||||||
|
|
||||||
static func check(_ node: RNGTree, in context: Context) -> String? {
|
static func check(_ node: RNGTree, in context: Context) -> String? {
|
||||||
guard let name = RNGNode.localName(node) else { return nil }
|
guard let name = RNGNode.localName(node) else { return nil }
|
||||||
if let problem = attributeProblem(node, name: name) { return problem }
|
if let problem = attributeProblem(node, name: name) {
|
||||||
if let problem = textProblem(node, name: name) { return problem }
|
return problem
|
||||||
|
}
|
||||||
|
if let problem = textProblem(node, name: name) {
|
||||||
|
return problem
|
||||||
|
}
|
||||||
switch context {
|
switch context {
|
||||||
case .pattern: return checkPattern(node, name: name)
|
case .pattern: return checkPattern(node, name: name)
|
||||||
case .nameClass: return checkNameClass(node, name: name)
|
case .nameClass: return checkNameClass(node, name: name)
|
||||||
@ -102,7 +106,9 @@ enum RelaxNGGrammarCheck {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
guard let nameClass = content.first else { return "<\(name)> needs a name class" }
|
guard let nameClass = content.first else { return "<\(name)> needs a name class" }
|
||||||
if let problem = check(nameClass, in: .nameClass) { return problem }
|
if let problem = check(nameClass, in: .nameClass) {
|
||||||
|
return problem
|
||||||
|
}
|
||||||
content = content.dropFirst()
|
content = content.dropFirst()
|
||||||
}
|
}
|
||||||
if name == "element", content.isEmpty {
|
if name == "element", content.isEmpty {
|
||||||
@ -160,14 +166,20 @@ enum RelaxNGGrammarCheck {
|
|||||||
for child in children {
|
for child in children {
|
||||||
switch RNGNode.localName(child) {
|
switch RNGNode.localName(child) {
|
||||||
case "param":
|
case "param":
|
||||||
if sawExcept { return "<param> may not follow <except> in <data>" }
|
if sawExcept {
|
||||||
|
return "<param> may not follow <except> in <data>"
|
||||||
|
}
|
||||||
guard hasNCName(child, "name") else { return "<param> needs a name attribute" }
|
guard hasNCName(child, "name") else { return "<param> needs a name attribute" }
|
||||||
case "except":
|
case "except":
|
||||||
if sawExcept { return "<data> takes at most one <except>" }
|
if sawExcept {
|
||||||
|
return "<data> takes at most one <except>"
|
||||||
|
}
|
||||||
sawExcept = true
|
sawExcept = true
|
||||||
let members = RNGNode.elementChildren(child)
|
let members = RNGNode.elementChildren(child)
|
||||||
guard !members.isEmpty else { return "<except> in <data> needs content" }
|
guard !members.isEmpty else { return "<except> in <data> needs content" }
|
||||||
if let problem = firstViolation(members, in: .dataExcept) { return problem }
|
if let problem = firstViolation(members, in: .dataExcept) {
|
||||||
|
return problem
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return "<\(RNGNode.localName(child) ?? "?")> is not allowed in <data>"
|
return "<\(RNGNode.localName(child) ?? "?")> is not allowed in <data>"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,9 @@ extension RelaxNGGrammarCheck {
|
|||||||
return "<\(name)> admits only an <except> child"
|
return "<\(name)> admits only an <except> child"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if children.count > 1 { return "<\(name)> takes at most one <except>" }
|
if children.count > 1 {
|
||||||
|
return "<\(name)> takes at most one <except>"
|
||||||
|
}
|
||||||
guard let except = children.first else { return nil }
|
guard let except = children.first else { return nil }
|
||||||
let members = RNGNode.elementChildren(except)
|
let members = RNGNode.elementChildren(except)
|
||||||
guard !members.isEmpty else { return "<except> needs content" }
|
guard !members.isEmpty else { return "<except> needs content" }
|
||||||
@ -61,7 +63,9 @@ extension RelaxNGGrammarCheck {
|
|||||||
/// `start` takes exactly one pattern; `define` needs a name and at least
|
/// `start` takes exactly one pattern; `define` needs a name and at least
|
||||||
/// one pattern; both may carry a combine method.
|
/// one pattern; both may carry a combine method.
|
||||||
private static func checkCombiningComponent(_ node: RNGTree, name: String, children: [RNGTree]) -> String? {
|
private static func checkCombiningComponent(_ node: RNGTree, name: String, children: [RNGTree]) -> String? {
|
||||||
if let problem = combineProblem(node) { return problem }
|
if let problem = combineProblem(node) {
|
||||||
|
return problem
|
||||||
|
}
|
||||||
if name == "start" {
|
if name == "start" {
|
||||||
guard children.count == 1 else { return "<start> takes exactly one pattern" }
|
guard children.count == 1 else { return "<start> takes exactly one pattern" }
|
||||||
return check(children[0], in: .pattern)
|
return check(children[0], in: .pattern)
|
||||||
@ -82,7 +86,9 @@ extension RelaxNGGrammarCheck {
|
|||||||
|
|
||||||
static func firstViolation(_ nodes: [RNGTree], in context: Context) -> String? {
|
static func firstViolation(_ nodes: [RNGTree], in context: Context) -> String? {
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
if let problem = check(node, in: context) { return problem }
|
if let problem = check(node, in: context) {
|
||||||
|
return problem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -120,7 +126,9 @@ extension RelaxNGGrammarCheck {
|
|||||||
/// of ALPHA *(ALPHA / DIGIT / "+" / "-" / "."), or the empty string for
|
/// of ALPHA *(ALPHA / DIGIT / "+" / "-" / "."), or the empty string for
|
||||||
/// the built-in library.
|
/// the built-in library.
|
||||||
static func isDatatypeLibraryURI(_ value: String) -> Bool {
|
static func isDatatypeLibraryURI(_ value: String) -> Bool {
|
||||||
if value.isEmpty { return true }
|
if value.isEmpty {
|
||||||
|
return true
|
||||||
|
}
|
||||||
guard !value.contains("#") else { return false }
|
guard !value.contains("#") else { return false }
|
||||||
// A percent must introduce a two-digit hex escape (RFC 2396).
|
// A percent must introduce a two-digit hex escape (RFC 2396).
|
||||||
var cursor = value.startIndex
|
var cursor = value.startIndex
|
||||||
|
|||||||
@ -27,7 +27,9 @@ enum RNGNode {
|
|||||||
// only when the whole document is unqualified (leniency for
|
// only when the whole document is unqualified (leniency for
|
||||||
// namespace-less schemas); under a qualified parent it is foreign.
|
// namespace-less schemas); under a qualified parent it is foreign.
|
||||||
let uri = child.name?.namespaceURI
|
let uri = child.name?.namespaceURI
|
||||||
if uri == relaxNGNamespace { return true }
|
if uri == relaxNGNamespace {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return uri == nil && node.name?.namespaceURI == nil
|
return uri == nil && node.name?.namespaceURI == nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,7 +86,9 @@ enum RNGNode {
|
|||||||
guard let colon = raw.firstIndex(of: ":") else { return nil }
|
guard let colon = raw.firstIndex(of: ":") else { return nil }
|
||||||
let prefix = String(raw[..<colon])
|
let prefix = String(raw[..<colon])
|
||||||
let local = String(raw[raw.index(after: colon)...])
|
let local = String(raw[raw.index(after: colon)...])
|
||||||
if prefix == "xml" { return ("http://www.w3.org/XML/1998/namespace", local) }
|
if prefix == "xml" {
|
||||||
|
return ("http://www.w3.org/XML/1998/namespace", local)
|
||||||
|
}
|
||||||
var current: RNGTree? = node
|
var current: RNGTree? = node
|
||||||
while let candidate = current {
|
while let candidate = current {
|
||||||
if let uri = candidate.attributes.first(where: { $0.name.prefix == "xmlns" && $0.name.localName == prefix })?.value {
|
if let uri = candidate.attributes.first(where: { $0.name.prefix == "xmlns" && $0.name.localName == prefix })?.value {
|
||||||
@ -120,7 +124,9 @@ enum RNGNode {
|
|||||||
}
|
}
|
||||||
cursor = current.parent
|
cursor = current.parent
|
||||||
}
|
}
|
||||||
if let documentBase { bases.append(documentBase) }
|
if let documentBase {
|
||||||
|
bases.append(documentBase)
|
||||||
|
}
|
||||||
var resolved = href
|
var resolved = href
|
||||||
for base in bases {
|
for base in bases {
|
||||||
resolved = resolveRelative(resolved, against: base)
|
resolved = resolveRelative(resolved, against: base)
|
||||||
|
|||||||
@ -106,7 +106,9 @@ final class RNGCompiler {
|
|||||||
// and the single start that omits `combine` still merges by it.
|
// and the single start that omits `combine` still merges by it.
|
||||||
var method: String?
|
var method: String?
|
||||||
for start in starts {
|
for start in starts {
|
||||||
if let named = RNGNode.attribute(start, "combine") { method = named }
|
if let named = RNGNode.attribute(start, "combine") {
|
||||||
|
method = named
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var result = combined(RNGNode.elementChildren(first), .sequence)
|
var result = combined(RNGNode.elementChildren(first), .sequence)
|
||||||
for start in starts.dropFirst() {
|
for start in starts.dropFirst() {
|
||||||
@ -262,7 +264,9 @@ final class RNGCompiler {
|
|||||||
private func datatypeLibrary(of node: Tree) -> String {
|
private func datatypeLibrary(of node: Tree) -> String {
|
||||||
var current: Tree? = node
|
var current: Tree? = node
|
||||||
while let candidate = current {
|
while let candidate = current {
|
||||||
if let library = RNGNode.attribute(candidate, "datatypeLibrary") { return library }
|
if let library = RNGNode.attribute(candidate, "datatypeLibrary") {
|
||||||
|
return library
|
||||||
|
}
|
||||||
current = candidate.parent
|
current = candidate.parent
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user