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 characters: [Character] = []
|
||||
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()
|
||||
})
|
||||
var events = 0
|
||||
|
||||
@ -18,7 +18,9 @@ extension PureXML.Canonical.Canonicalizer {
|
||||
let declared = Set(apex.attributes.map(\.name.description))
|
||||
for (prefix, uri) in ancestorNamespaces {
|
||||
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)
|
||||
@ -43,7 +45,9 @@ extension PureXML.Canonical.Canonicalizer {
|
||||
/// 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] {
|
||||
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
|
||||
// apex does not already set.
|
||||
var seen: Set<String> = []
|
||||
@ -63,8 +67,12 @@ extension PureXML.Canonical.Canonicalizer {
|
||||
/// The prefix a namespace declaration binds (empty for the default
|
||||
/// namespace), or nil when the attribute is not a namespace declaration.
|
||||
static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
|
||||
if name.prefix == nil, name.localName == "xmlns" { return "" }
|
||||
if name.prefix == "xmlns" { return name.localName }
|
||||
if name.prefix == nil, name.localName == "xmlns" {
|
||||
return ""
|
||||
}
|
||||
if name.prefix == "xmlns" {
|
||||
return name.localName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,8 +208,12 @@ extension PureXML.Canonical {
|
||||
}
|
||||
|
||||
private static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
|
||||
if name.prefix == nil, name.localName == "xmlns" { return "" }
|
||||
if name.prefix == "xmlns" { return name.localName }
|
||||
if name.prefix == nil, name.localName == "xmlns" {
|
||||
return ""
|
||||
}
|
||||
if name.prefix == "xmlns" {
|
||||
return name.localName
|
||||
}
|
||||
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] {
|
||||
var result: [PureXML.Model.Attribute] = []
|
||||
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) {
|
||||
result.append(PureXML.Model.Attribute("xml:base", base))
|
||||
@ -94,27 +96,35 @@ extension PureXML.Canonical.Canonicalizer {
|
||||
var input = Substring(path)
|
||||
var output = ""
|
||||
while !input.isEmpty {
|
||||
if input.hasPrefix("../") { input = input.dropFirst(3)
|
||||
if input.hasPrefix("../") {
|
||||
input = input.dropFirst(3)
|
||||
continue
|
||||
}
|
||||
if input.hasPrefix("./") { input = input.dropFirst(2)
|
||||
if input.hasPrefix("./") {
|
||||
input = input.dropFirst(2)
|
||||
continue
|
||||
}
|
||||
if input.hasPrefix("/./") { input = "/" + input.dropFirst(3)
|
||||
if input.hasPrefix("/./") {
|
||||
input = "/" + input.dropFirst(3)
|
||||
continue
|
||||
}
|
||||
if input == "/." { input = "/"
|
||||
if input == "/." {
|
||||
input = "/"
|
||||
continue
|
||||
}
|
||||
if input.hasPrefix("/../") { input = "/" + input.dropFirst(4)
|
||||
if input.hasPrefix("/../") {
|
||||
input = "/" + input.dropFirst(4)
|
||||
removeLastSegment(&output)
|
||||
continue
|
||||
}
|
||||
if input == "/.." { input = "/"
|
||||
if input == "/.." {
|
||||
input = "/"
|
||||
removeLastSegment(&output)
|
||||
continue
|
||||
}
|
||||
if input == "." || input == ".." { break }
|
||||
if input == "." || input == ".." {
|
||||
break
|
||||
}
|
||||
let start = input.hasPrefix("/") ? input.index(after: input.startIndex) : input.startIndex
|
||||
let next = input[start...].firstIndex(of: "/") ?? input.endIndex
|
||||
output += input[..<next]
|
||||
@ -192,11 +202,19 @@ private struct URIParts {
|
||||
/// Recomposes the components into a URI string (RFC 3986 section 5.3).
|
||||
var recomposed: String {
|
||||
var result = ""
|
||||
if let scheme { result += scheme + ":" }
|
||||
if let authority { result += "//" + authority }
|
||||
if let scheme {
|
||||
result += scheme + ":"
|
||||
}
|
||||
if let authority {
|
||||
result += "//" + authority
|
||||
}
|
||||
result += path
|
||||
if let query { result += "?" + query }
|
||||
if let fragment { result += "#" + fragment }
|
||||
if let query {
|
||||
result += "?" + query
|
||||
}
|
||||
if let fragment {
|
||||
result += "#" + fragment
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +68,9 @@ public extension PureXML.Canonical {
|
||||
for child in children {
|
||||
var 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")
|
||||
case let .element(element):
|
||||
@ -77,7 +79,9 @@ public extension PureXML.Canonical {
|
||||
let text = options.trimTextNodes ? value.trimmingXMLWhitespace() : value
|
||||
output += Self.escapeText(text)
|
||||
case let .comment(value):
|
||||
if options.includeComments { output += "<!--\(value)-->" }
|
||||
if options.includeComments {
|
||||
output += "<!--\(value)-->"
|
||||
}
|
||||
case let .processingInstruction(target, data):
|
||||
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(data)?>"
|
||||
}
|
||||
@ -149,7 +153,9 @@ public extension PureXML.Canonical {
|
||||
let text = options.trimTextNodes ? value.trimmingXMLWhitespace() : value
|
||||
output += Self.escapeText(text)
|
||||
case let .comment(value):
|
||||
if options.includeComments { output += "<!--\(value)-->" }
|
||||
if options.includeComments {
|
||||
output += "<!--\(value)-->"
|
||||
}
|
||||
case let .processingInstruction(target, data):
|
||||
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(data)?>"
|
||||
case .element, .document:
|
||||
@ -198,7 +204,9 @@ public extension PureXML.Canonical {
|
||||
for prefix in utilized {
|
||||
let uri = inScope[prefix] ?? ""
|
||||
if prefix.isEmpty {
|
||||
if uri != (rendered[""] ?? "") { result.append(("", uri)) }
|
||||
if uri != (rendered[""] ?? "") {
|
||||
result.append(("", uri))
|
||||
}
|
||||
} else if !uri.isEmpty, rendered[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 {
|
||||
let leftURI = lhs.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
|
||||
}
|
||||
|
||||
private static func namespaceDeclarations(_ element: PureXML.Model.Element) -> [(String, String)] {
|
||||
element.attributes.compactMap { attribute in
|
||||
let name = attribute.name
|
||||
if name.prefix == nil, name.localName == "xmlns" { return ("", attribute.value) }
|
||||
if name.prefix == "xmlns" { return (name.localName, attribute.value) }
|
||||
if name.prefix == nil, name.localName == "xmlns" {
|
||||
return ("", attribute.value)
|
||||
}
|
||||
if name.prefix == "xmlns" {
|
||||
return (name.localName, attribute.value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -327,7 +341,9 @@ public extension PureXML.Canonical.Canonicalizer {
|
||||
case .text, .cdata:
|
||||
output += Self.escapeText(options.trimTextNodes ? node.value.trimmingXMLWhitespace() : node.value)
|
||||
case .comment:
|
||||
if options.includeComments { output += "<!--\(node.value)-->" }
|
||||
if options.includeComments {
|
||||
output += "<!--\(node.value)-->"
|
||||
}
|
||||
case .processingInstruction:
|
||||
output += node.value.isEmpty ? "<?\(node.name?.description ?? "")?>" : "<?\(node.name?.description ?? "") \(node.value)?>"
|
||||
case .element, .document, .doctype, .entityReference, .namespace:
|
||||
|
||||
@ -27,7 +27,9 @@ private struct CatalogBuilder {
|
||||
insert(node, key: "systemId", base: base, into: &systemMap)
|
||||
case "public":
|
||||
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":
|
||||
insert(node, key: "name", base: base, into: &uriMap)
|
||||
case "rewriteSystem":
|
||||
@ -62,7 +64,9 @@ private struct CatalogBuilder {
|
||||
case "delegateURI":
|
||||
appendDelegate(node, start: "uriStartString", base: base, into: &delegateURI)
|
||||
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:
|
||||
break
|
||||
}
|
||||
|
||||
@ -163,16 +163,22 @@ public extension PureXML.Catalog {
|
||||
_ loadingCatalog: (String) -> String?,
|
||||
_ visited: inout Set<String>,
|
||||
) -> String? {
|
||||
if let local = pick(self) { return local }
|
||||
if let local = pick(self) {
|
||||
return local
|
||||
}
|
||||
let matching = delegates
|
||||
.filter { identifier.hasPrefix($0.startString) }
|
||||
.sorted { $0.startString.count > $1.startString.count }
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@ -27,16 +27,22 @@ private struct SGMLBuilder {
|
||||
|
||||
mutating func apply(_ keyword: String, _ scanner: inout SGMLScanner) {
|
||||
let upper = keyword.uppercased()
|
||||
if applyPair(upper, &scanner) { return }
|
||||
if applyPair(upper, &scanner) {
|
||||
return
|
||||
}
|
||||
applySingle(upper, &scanner)
|
||||
}
|
||||
|
||||
private mutating func applyPair(_ keyword: String, _ scanner: inout SGMLScanner) -> Bool {
|
||||
switch keyword {
|
||||
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":
|
||||
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":
|
||||
if let prefix = scanner.next(), let catalog = scanner.next() {
|
||||
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) {
|
||||
switch keyword {
|
||||
case "CATALOG":
|
||||
if let catalog = scanner.next() { nextCatalogs.append(resolve(catalog)) }
|
||||
if let catalog = scanner.next() {
|
||||
nextCatalogs.append(resolve(catalog))
|
||||
}
|
||||
case "BASE":
|
||||
if let declared = scanner.next() { base = resolve(declared) }
|
||||
if let declared = scanner.next() {
|
||||
base = resolve(declared)
|
||||
}
|
||||
case "OVERRIDE":
|
||||
if let value = scanner.next() { preferPublic = value.uppercased() == "YES" }
|
||||
if let value = scanner.next() {
|
||||
preferPublic = value.uppercased() == "YES"
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@ -20,12 +20,16 @@ extension PureXML.Emitting {
|
||||
while let marker = value.utf8[runStart...].firstIndex(where: {
|
||||
textEscapableByte($0, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn)
|
||||
}) {
|
||||
if runStart < marker { result += value[runStart ..< marker] }
|
||||
if runStart < marker {
|
||||
result += value[runStart ..< marker]
|
||||
}
|
||||
let character = value[marker]
|
||||
result += textEscape(character, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn) ?? String(character)
|
||||
runStart = value.index(after: marker)
|
||||
}
|
||||
if runStart < value.endIndex { result += value[runStart...] }
|
||||
if runStart < value.endIndex {
|
||||
result += value[runStart...]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -98,12 +102,16 @@ extension PureXML.Emitting {
|
||||
while let marker = value.utf8[runStart...].firstIndex(where: {
|
||||
attributeEscapableByte($0, quoteByte: quoteByte, asciiOnly: asciiOnly)
|
||||
}) {
|
||||
if runStart < marker { result += value[runStart ..< marker] }
|
||||
if runStart < marker {
|
||||
result += value[runStart ..< marker]
|
||||
}
|
||||
let character = value[marker]
|
||||
result += attributeEscape(character, quote: quote, asciiOnly: asciiOnly) ?? String(character)
|
||||
runStart = value.index(after: marker)
|
||||
}
|
||||
if runStart < value.endIndex { result += value[runStart...] }
|
||||
if runStart < value.endIndex {
|
||||
result += value[runStart...]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -155,15 +163,21 @@ extension PureXML.Emitting {
|
||||
static func comment(_ value: String) -> String {
|
||||
// Fast path: a comment with no hyphen has no `--` and no trailing `-`,
|
||||
// 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 previousHyphen = false
|
||||
for character in value {
|
||||
if character == "-", previousHyphen { result.append(" ") }
|
||||
if character == "-", previousHyphen {
|
||||
result.append(" ")
|
||||
}
|
||||
result.append(character)
|
||||
previousHyphen = character == "-"
|
||||
}
|
||||
if previousHyphen { result.append(" ") }
|
||||
if previousHyphen {
|
||||
result.append(" ")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -173,11 +187,15 @@ extension PureXML.Emitting {
|
||||
static func processingInstruction(_ data: String) -> String {
|
||||
// Fast path: data with no '?' cannot contain `?>`, so it is returned
|
||||
// unchanged rather than rebuilt character by character.
|
||||
if !data.utf8.contains(0x3F) { return data }
|
||||
if !data.utf8.contains(0x3F) {
|
||||
return data
|
||||
}
|
||||
var result = ""
|
||||
var previousQuestion = false
|
||||
for character in data {
|
||||
if character == ">", previousQuestion { result.append(" ") }
|
||||
if character == ">", previousQuestion {
|
||||
result.append(" ")
|
||||
}
|
||||
result.append(character)
|
||||
previousQuestion = character == "?"
|
||||
}
|
||||
|
||||
@ -37,8 +37,12 @@ extension HTMLDocument {
|
||||
/// scope boundary.
|
||||
private func hasInScope(_ target: PureXML.Model.TreeNode) -> Bool {
|
||||
for node in openBody.reversed() {
|
||||
if node === target { return true }
|
||||
if Self.scopeBoundaries.contains(tagName(node)) { return false }
|
||||
if node === target {
|
||||
return true
|
||||
}
|
||||
if Self.scopeBoundaries.contains(tagName(node)) {
|
||||
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).
|
||||
func reconstructActiveFormatting() {
|
||||
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 {
|
||||
index -= 1
|
||||
if activeFormatting[index] == nil || isOpen(activeFormatting[index]) {
|
||||
@ -91,7 +97,9 @@ extension HTMLDocument {
|
||||
activeFormatting.remove(at: formattingIndexInList)
|
||||
return
|
||||
}
|
||||
if !hasInScope(formattingElement) { return }
|
||||
if !hasInScope(formattingElement) {
|
||||
return
|
||||
}
|
||||
guard let furthestBlockIndex = openBody[(formattingOpenIndex + 1)...].firstIndex(where: { Self.special.contains(tagName($0)) }) else {
|
||||
openBody.removeLast(openBody.count - formattingOpenIndex)
|
||||
activeFormatting.remove(at: formattingIndexInList)
|
||||
@ -104,7 +112,9 @@ extension HTMLDocument {
|
||||
private func lastFormatting(tag: String) -> Int? {
|
||||
for index in activeFormatting.indices.reversed() {
|
||||
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
|
||||
}
|
||||
@ -124,7 +134,9 @@ extension HTMLDocument {
|
||||
inner += 1
|
||||
nodeIndex -= 1
|
||||
let node = openBody[nodeIndex]
|
||||
if node === formattingElement { break }
|
||||
if node === formattingElement {
|
||||
break
|
||||
}
|
||||
guard let activeIndex = formattingIndex(node) else {
|
||||
openBody.remove(at: nodeIndex)
|
||||
continue
|
||||
@ -132,7 +144,9 @@ extension HTMLDocument {
|
||||
let clone = node.shallowCopy()
|
||||
activeFormatting[activeIndex] = clone
|
||||
openBody[nodeIndex] = clone
|
||||
if lastNode === furthestBlock { bookmark = activeIndex + 1 }
|
||||
if lastNode === furthestBlock {
|
||||
bookmark = activeIndex + 1
|
||||
}
|
||||
clone.append(lastNode)
|
||||
lastNode = clone
|
||||
}
|
||||
@ -144,13 +158,19 @@ extension HTMLDocument {
|
||||
furthestBlock.append(formattingClone)
|
||||
if let removeIndex = formattingIndex(formattingElement) {
|
||||
activeFormatting.remove(at: removeIndex)
|
||||
if removeIndex < bookmark { bookmark -= 1 }
|
||||
if removeIndex < bookmark {
|
||||
bookmark -= 1
|
||||
}
|
||||
}
|
||||
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
|
||||
// above it on the open-elements stack; inserting below would re-find the
|
||||
// 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) {
|
||||
if case .doctype = token { return }
|
||||
if isIgnorableWhitespace(token) { return }
|
||||
if case .doctype = token {
|
||||
return
|
||||
}
|
||||
if isIgnorableWhitespace(token) {
|
||||
return
|
||||
}
|
||||
mode = .beforeHtml
|
||||
process(token)
|
||||
}
|
||||
@ -96,13 +100,17 @@ final class HTMLDocument {
|
||||
mode = .beforeHead
|
||||
return
|
||||
}
|
||||
if isIgnorableWhitespace(token) { return }
|
||||
if isIgnorableWhitespace(token) {
|
||||
return
|
||||
}
|
||||
mode = .beforeHead
|
||||
process(token)
|
||||
}
|
||||
|
||||
private func processBeforeHead(_ token: Token) {
|
||||
if isIgnorableWhitespace(token) { return }
|
||||
if isIgnorableWhitespace(token) {
|
||||
return
|
||||
}
|
||||
if case let .startTag(name, attributes, _) = token, name == "head" {
|
||||
headAttributes = modelAttributes(attributes)
|
||||
mode = .inHead
|
||||
@ -140,7 +148,9 @@ final class HTMLDocument {
|
||||
}
|
||||
let element = PureXML.Model.TreeNode.element(name, attributes: modelAttributes(attributes))
|
||||
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) {
|
||||
@ -168,7 +178,9 @@ final class HTMLDocument {
|
||||
process(token)
|
||||
return
|
||||
}
|
||||
if isIgnorableWhitespace(token) { return }
|
||||
if isIgnorableWhitespace(token) {
|
||||
return
|
||||
}
|
||||
mode = .inBody
|
||||
process(token)
|
||||
}
|
||||
@ -210,7 +222,11 @@ final class HTMLDocument {
|
||||
case let .startTag(name, attributes, _) where name == "noframes":
|
||||
framesetStack.last?.append(PureXML.Model.TreeNode.element("noframes", attributes: modelAttributes(attributes)))
|
||||
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):
|
||||
framesetStack.last?.append(.comment(value))
|
||||
default:
|
||||
@ -243,7 +259,9 @@ final class HTMLDocument {
|
||||
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 {
|
||||
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) {
|
||||
openBody.removeLast(openBody.count - selectIndex)
|
||||
@ -278,8 +296,12 @@ final class HTMLDocument {
|
||||
case "tr" where context == "table":
|
||||
bodyPushImplied("tbody")
|
||||
case "td", "th":
|
||||
if context == "table" { bodyPushImplied("tbody") }
|
||||
if context == "table" || ["tbody", "thead", "tfoot"].contains(context) { bodyPushImplied("tr") }
|
||||
if context == "table" {
|
||||
bodyPushImplied("tbody")
|
||||
}
|
||||
if context == "table" || ["tbody", "thead", "tfoot"].contains(context) {
|
||||
bodyPushImplied("tr")
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -303,11 +325,17 @@ final class HTMLDocument {
|
||||
/// 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.
|
||||
private func bodyForeignNamespace(for name: String) -> String? {
|
||||
if name == "svg" { return ForeignNamespace.svg }
|
||||
if name == "math" { return ForeignNamespace.mathml }
|
||||
if name == "svg" {
|
||||
return ForeignNamespace.svg
|
||||
}
|
||||
if name == "math" {
|
||||
return ForeignNamespace.mathml
|
||||
}
|
||||
for node in openBody.reversed() {
|
||||
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 nil
|
||||
@ -330,7 +358,9 @@ extension HTMLDocument {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,13 +59,17 @@ extension PureXML.HTML.Tokenizer {
|
||||
private static func decodeNumeric(_ characters: [Character], at start: Int) -> (String, Int)? {
|
||||
var cursor = start + 2 // past "&#"
|
||||
let isHex = cursor < characters.count && (characters[cursor] == "x" || characters[cursor] == "X")
|
||||
if isHex { cursor += 1 }
|
||||
if isHex {
|
||||
cursor += 1
|
||||
}
|
||||
let digitsStart = cursor
|
||||
while cursor < characters.count, isDigit(characters[cursor], hex: isHex) {
|
||||
cursor += 1
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,9 @@ extension PureXML.HTML {
|
||||
output += ">"
|
||||
// A void element has no end tag and no content (the html model);
|
||||
// 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))
|
||||
// A raw-text element (script, style) writes its own text children
|
||||
// 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 {
|
||||
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
|
||||
// (CHECKED="CHECKED" serializes as CHECKED), the html form.
|
||||
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))
|
||||
}
|
||||
}
|
||||
if previousWasAmpersand { result += "&" }
|
||||
if previousWasAmpersand {
|
||||
result += "&"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@ -27,10 +27,18 @@ extension PureXML.HTML {
|
||||
return rawText(until: raw)
|
||||
}
|
||||
guard peek() == "<" else { return text() }
|
||||
if matches("<!--") { return comment() }
|
||||
if matches("<!") { return doctype() }
|
||||
if matches("</") { return endTag() }
|
||||
if peek(1)?.isLetter == true { return startTag() }
|
||||
if matches("<!--") {
|
||||
return comment()
|
||||
}
|
||||
if matches("<!") {
|
||||
return doctype()
|
||||
}
|
||||
if matches("</") {
|
||||
return endTag()
|
||||
}
|
||||
if peek(1)?.isLetter == true {
|
||||
return startTag()
|
||||
}
|
||||
return text()
|
||||
}
|
||||
|
||||
@ -38,7 +46,9 @@ extension PureXML.HTML {
|
||||
|
||||
private mutating func text() -> Token {
|
||||
var value = ""
|
||||
if let first = advance() { value.append(first) }
|
||||
if let first = advance() {
|
||||
value.append(first)
|
||||
}
|
||||
while let character = peek(), character != "<" {
|
||||
value.append(character)
|
||||
advance()
|
||||
@ -53,16 +63,21 @@ extension PureXML.HTML {
|
||||
var selfClosing = false
|
||||
while !isAtEnd {
|
||||
skipSpace()
|
||||
if consume("/>") { selfClosing = true
|
||||
if consume("/>") {
|
||||
selfClosing = true
|
||||
break
|
||||
}
|
||||
if consume(">") {
|
||||
break
|
||||
}
|
||||
if consume(">") { break }
|
||||
guard let attribute = readAttribute() else { advance()
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@ -133,9 +148,13 @@ extension PureXML.HTML {
|
||||
}
|
||||
value = Self.replacingNull(value)
|
||||
// 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).
|
||||
if name == "textarea", value.first == "\n" { value.removeFirst() }
|
||||
if name == "textarea", value.first == "\n" {
|
||||
value.removeFirst()
|
||||
}
|
||||
return .text(value)
|
||||
}
|
||||
|
||||
|
||||
@ -55,7 +55,9 @@ extension PureXML.Model.Node: Equatable, Hashable {
|
||||
stack.append(pair)
|
||||
}
|
||||
default:
|
||||
if !leavesEqual(left, right) { return false }
|
||||
if !leavesEqual(left, right) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
@ -194,8 +194,12 @@ public extension PureXML.Model {
|
||||
internalSubset: String = "",
|
||||
) -> TreeNode {
|
||||
var attributes: [PureXML.Model.Attribute] = []
|
||||
if let publicID { attributes.append(PureXML.Model.Attribute("public", publicID)) }
|
||||
if let systemID { attributes.append(PureXML.Model.Attribute("system", systemID)) }
|
||||
if let publicID {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -189,7 +189,9 @@ public extension PureXML.Model.TreeNode {
|
||||
/// rebuilding each subtree, so a result set of nested nodes is linear rather
|
||||
/// than quadratic. The projection is identical to ``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 {
|
||||
case .document:
|
||||
.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
|
||||
/// ancestor precedes its descendants.
|
||||
func precedes(_ other: PureXML.Model.TreeNode) -> Bool {
|
||||
if self === other { return false }
|
||||
if self === other {
|
||||
return false
|
||||
}
|
||||
let mine = Self.pathFromRoot(self)
|
||||
let theirs = Self.pathFromRoot(other)
|
||||
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] {
|
||||
depth += 1
|
||||
}
|
||||
if depth == mine.count { return true }
|
||||
if depth == theirs.count { return false }
|
||||
if depth == mine.count {
|
||||
return true
|
||||
}
|
||||
if depth == theirs.count {
|
||||
return false
|
||||
}
|
||||
let parent = mine[depth - 1]
|
||||
let mineIndex = parent.children.firstIndex { $0 === mine[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 {
|
||||
var current: PureXML.Model.TreeNode? = self
|
||||
while let node = current {
|
||||
if node === ancestor { return true }
|
||||
if node === ancestor {
|
||||
return true
|
||||
}
|
||||
current = node.parent
|
||||
}
|
||||
return false
|
||||
|
||||
@ -9,7 +9,9 @@ public extension PureXML.Model.TreeNode {
|
||||
var current: PureXML.Model.TreeNode? = self
|
||||
for key in path {
|
||||
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 index = (key.intValue ?? 1) - 1
|
||||
current = matches.indices.contains(index) ? matches[index] : nil
|
||||
|
||||
@ -16,7 +16,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
scalars.append(contentsOf: sequence)
|
||||
} else {
|
||||
scalars.append("\u{FFFD}")
|
||||
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
|
||||
if byte <= 0x7F {
|
||||
scalars.append(Unicode.Scalar(byte))
|
||||
}
|
||||
}
|
||||
} else if byte <= 0x7F {
|
||||
scalars.append(Unicode.Scalar(byte))
|
||||
@ -26,7 +28,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
scalars.append("\u{FFFD}")
|
||||
}
|
||||
}
|
||||
if lead != 0 { scalars.append("\u{FFFD}") }
|
||||
if lead != 0 {
|
||||
scalars.append("\u{FFFD}")
|
||||
}
|
||||
return String(scalars)
|
||||
}
|
||||
|
||||
|
||||
@ -166,7 +166,9 @@ extension PureXML.Parsing {
|
||||
/// BOM is still a contradiction, only utf-8 itself agrees; the generic
|
||||
/// utf-16/utf-32 names agree with either byte order.
|
||||
private static func sameFamily(_ declared: InputEncoding, as sniffed: InputEncoding) -> Bool {
|
||||
if declared == sniffed { return true }
|
||||
if declared == sniffed {
|
||||
return true
|
||||
}
|
||||
switch (sniffed, declared) {
|
||||
case (.utf16BigEndian, .utf16LittleEndian), (.utf16LittleEndian, .utf16BigEndian),
|
||||
(.utf32BigEndian, .utf32LittleEndian), (.utf32LittleEndian, .utf32BigEndian):
|
||||
@ -191,7 +193,9 @@ extension PureXML.Parsing {
|
||||
while index < body.endIndex, body.index(after: index) < body.endIndex {
|
||||
let high = sniffed == .utf16BigEndian ? body[index] : body[body.index(after: 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)
|
||||
}
|
||||
return declaredEncoding(in: ascii)
|
||||
|
||||
@ -90,8 +90,12 @@ public extension PureXML.Parsing {
|
||||
var map: [UInt32: UInt8] = [:]
|
||||
for byte in UInt8.min ... UInt8.max {
|
||||
let value = forward(byte).value
|
||||
if value == 0xFFFD { continue }
|
||||
if map[value] == nil { map[value] = byte }
|
||||
if value == 0xFFFD {
|
||||
continue
|
||||
}
|
||||
if map[value] == nil {
|
||||
map[value] = byte
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
@ -104,7 +104,9 @@ extension PureXML.Parsing.ByteEncoder {
|
||||
func record(_ bytes: [UInt8]) {
|
||||
let scalars = Array(decode(bytes[...]).unicodeScalars)
|
||||
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 {
|
||||
record([UInt8(byte)])
|
||||
|
||||
@ -25,13 +25,17 @@ extension PureXML.Parsing {
|
||||
// Each definition starts with required whitespace (the body begins
|
||||
// right after the element name).
|
||||
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.requiredSpace() else { return nil }
|
||||
guard attType(&scanner) else { return nil }
|
||||
guard scanner.requiredSpace() 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
|
||||
}
|
||||
@ -41,7 +45,9 @@ extension PureXML.Parsing {
|
||||
return scanner.nameGroup(nmtokens: true)
|
||||
}
|
||||
guard let keyword = scanner.keyword() else { return false }
|
||||
if tokenizedTypes.contains(keyword) { return true }
|
||||
if tokenizedTypes.contains(keyword) {
|
||||
return true
|
||||
}
|
||||
if keyword == "NOTATION" {
|
||||
guard scanner.requiredSpace() else { return false }
|
||||
return scanner.nameGroup(nmtokens: false)
|
||||
|
||||
@ -8,7 +8,9 @@ extension PureXML.Parsing {
|
||||
enum DTDContentModelGrammar {
|
||||
static func isValid(_ model: String) -> Bool {
|
||||
let text = model.trimmingXMLWhitespace()
|
||||
if text == "EMPTY" || text == "ANY" { return true }
|
||||
if text == "EMPTY" || text == "ANY" {
|
||||
return true
|
||||
}
|
||||
var scanner = ModelScanner(text)
|
||||
guard scanner.peek() == "(" else { return false }
|
||||
// Mixed and children both start with '('; Mixed is identified by
|
||||
@ -74,7 +76,9 @@ private struct ModelScanner {
|
||||
var sawAlternative = false
|
||||
while true {
|
||||
skipSpace()
|
||||
if consume(")") { break }
|
||||
if consume(")") {
|
||||
break
|
||||
}
|
||||
guard consume("|") else { return false }
|
||||
sawAlternative = true
|
||||
skipSpace()
|
||||
@ -104,7 +108,9 @@ private struct ModelScanner {
|
||||
var connector: Character?
|
||||
while !consume(")") {
|
||||
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
|
||||
skipSpace()
|
||||
guard particle() else { return false }
|
||||
|
||||
@ -83,7 +83,9 @@ struct DTDScanner {
|
||||
/// a VC (deferred finding), since the unread external declarations might
|
||||
/// have declared the entity (production 68).
|
||||
var entityDeclaredIsWellFormedness: Bool {
|
||||
if standalone { return true }
|
||||
if standalone {
|
||||
return true
|
||||
}
|
||||
return doctype.externalSubset == nil && doctype.parameterEntities.isEmpty && unresolvedParameterEntities.isEmpty
|
||||
}
|
||||
|
||||
|
||||
@ -50,7 +50,9 @@ extension DTDScanner {
|
||||
} else if reader.matches("]]>") {
|
||||
reader.consume("]]>")
|
||||
nesting -= 1
|
||||
if nesting == 0 { return body }
|
||||
if nesting == 0 {
|
||||
return body
|
||||
}
|
||||
body += "]]>"
|
||||
} else if let character = reader.peek() {
|
||||
body.append(character)
|
||||
@ -168,7 +170,9 @@ extension DTDScanner {
|
||||
while index < text.endIndex {
|
||||
let character = text[index]
|
||||
if let open = quote {
|
||||
if character == open { quote = nil }
|
||||
if character == open {
|
||||
quote = nil
|
||||
}
|
||||
index = text.index(after: index)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -95,7 +95,9 @@ extension DTDScanner {
|
||||
if character == "&" || character == "%" {
|
||||
var probe = index + 1
|
||||
let isCharacterReference = character == "&" && probe < characters.count && characters[probe] == "#"
|
||||
if isCharacterReference { probe += 1 }
|
||||
if isCharacterReference {
|
||||
probe += 1
|
||||
}
|
||||
let bodyStart = probe
|
||||
let stops: Set<Character> = [";", "&", "%"]
|
||||
while probe < characters.count, !stops.contains(characters[probe]), !characters[probe].isWhitespace {
|
||||
@ -237,7 +239,9 @@ extension DTDScanner {
|
||||
var systemID = ""
|
||||
if reader.peek()?.isWhitespace == true {
|
||||
reader.skipSpace()
|
||||
if let literal = scanLiteral(&reader) { systemID = literal }
|
||||
if let literal = scanLiteral(&reader) {
|
||||
systemID = literal
|
||||
}
|
||||
}
|
||||
return ExternalID(publicID: publicID, systemID: systemID, base: currentBase)
|
||||
}
|
||||
|
||||
@ -26,7 +26,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
scalars.append(scalar)
|
||||
} else {
|
||||
scalars.append("\u{FFFD}")
|
||||
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
|
||||
if byte <= 0x7F {
|
||||
scalars.append(Unicode.Scalar(byte))
|
||||
}
|
||||
}
|
||||
} else if byte <= 0x7F {
|
||||
scalars.append(Unicode.Scalar(byte))
|
||||
@ -36,7 +38,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
scalars.append("\u{FFFD}")
|
||||
}
|
||||
}
|
||||
if lead != 0 { scalars.append("\u{FFFD}") }
|
||||
if lead != 0 {
|
||||
scalars.append("\u{FFFD}")
|
||||
}
|
||||
return String(scalars)
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
scalars.append(scalar)
|
||||
} else {
|
||||
scalars.append("\u{FFFD}")
|
||||
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
|
||||
if byte <= 0x7F {
|
||||
scalars.append(Unicode.Scalar(byte))
|
||||
}
|
||||
}
|
||||
} else if byte <= 0x7F {
|
||||
scalars.append(Unicode.Scalar(byte))
|
||||
@ -27,7 +29,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
scalars.append("\u{FFFD}")
|
||||
}
|
||||
}
|
||||
if lead != 0 { scalars.append("\u{FFFD}") }
|
||||
if lead != 0 {
|
||||
scalars.append("\u{FFFD}")
|
||||
}
|
||||
return String(scalars)
|
||||
}
|
||||
|
||||
|
||||
@ -126,7 +126,9 @@ extension PureXML.Parsing.EntityDecoder {
|
||||
/// A tentative `false` recorded before the recursion breaks any cycle (a
|
||||
/// 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 {
|
||||
if let cached = cache[name] { return cached }
|
||||
if let cached = cache[name] {
|
||||
return cached
|
||||
}
|
||||
guard let replacement = entities[name] else {
|
||||
cache[name] = false
|
||||
return false
|
||||
|
||||
@ -57,9 +57,13 @@ extension PureXML.Parsing {
|
||||
while let character = scanner.advance() {
|
||||
switch character {
|
||||
case "<":
|
||||
if let reason = scanner.markup(depth: &depth) { return reason }
|
||||
if let reason = scanner.markup(depth: &depth) {
|
||||
return reason
|
||||
}
|
||||
case "&":
|
||||
if !scanner.completeReference() { return "incomplete reference" }
|
||||
if !scanner.completeReference() {
|
||||
return "incomplete reference"
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -100,7 +104,9 @@ private struct ReplacementScanner {
|
||||
|
||||
private mutating func skip(past terminator: String) -> Bool {
|
||||
while index < characters.count {
|
||||
if consume(terminator) { return true }
|
||||
if consume(terminator) {
|
||||
return true
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
return false
|
||||
@ -126,8 +132,12 @@ private struct ReplacementScanner {
|
||||
depth -= 1
|
||||
return depth < 0 ? "an end tag closes an element opened outside the entity" : nil
|
||||
}
|
||||
if let reason = attributes() { return reason }
|
||||
if consume("/>") { return nil }
|
||||
if let reason = attributes() {
|
||||
return reason
|
||||
}
|
||||
if consume("/>") {
|
||||
return nil
|
||||
}
|
||||
guard consume(">") else { return "malformed start tag" }
|
||||
depth += 1
|
||||
return nil
|
||||
@ -146,12 +156,16 @@ private struct ReplacementScanner {
|
||||
while true {
|
||||
skipSpace()
|
||||
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" }
|
||||
skipSpace()
|
||||
guard consume("=") else { return "attribute without a value" }
|
||||
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" }
|
||||
index += 1
|
||||
while let character = peek(), character != quote {
|
||||
if character == "<" { return "'<' in an attribute value" }
|
||||
if character == "<" {
|
||||
return "'<' in an attribute value"
|
||||
}
|
||||
if character == "&" {
|
||||
index += 1
|
||||
if !completeReference() { return "incomplete reference in an attribute value" }
|
||||
if !completeReference() {
|
||||
return "incomplete reference in an attribute value"
|
||||
}
|
||||
continue
|
||||
}
|
||||
index += 1
|
||||
@ -174,7 +192,9 @@ private struct ReplacementScanner {
|
||||
/// After a consumed `&`: a complete `name;` or `#digits;`.
|
||||
mutating func completeReference() -> Bool {
|
||||
var sawBody = false
|
||||
if peek() == "#" { index += 1 }
|
||||
if peek() == "#" {
|
||||
index += 1
|
||||
}
|
||||
while let character = peek(), character != ";" {
|
||||
guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isNameChar) else { return false }
|
||||
sawBody = true
|
||||
|
||||
@ -31,9 +31,15 @@ extension PureXML.Parsing.EventReader {
|
||||
/// streaming source or spliced entity text). Equivalent to the byte switch
|
||||
/// in `scanContentMarkup`.
|
||||
private mutating func scanContentMarkupViaCharacters() throws -> Event {
|
||||
if reader.matches("</") { return try scanEndTag() }
|
||||
if reader.matches("<?") { return try scanContentProcessingInstruction() }
|
||||
if reader.matches("<!") { return try scanContentDeclaration() }
|
||||
if reader.matches("</") {
|
||||
return try scanEndTag()
|
||||
}
|
||||
if reader.matches("<?") {
|
||||
return try scanContentProcessingInstruction()
|
||||
}
|
||||
if reader.matches("<!") {
|
||||
return try scanContentDeclaration()
|
||||
}
|
||||
return try scanStartTag()
|
||||
}
|
||||
|
||||
@ -46,7 +52,9 @@ extension PureXML.Parsing.EventReader {
|
||||
recordEmptyElementContent("a comment")
|
||||
return try scanComment()
|
||||
}
|
||||
if reader.matches("<![CDATA[") { return try scanCDATA() }
|
||||
if reader.matches("<![CDATA[") {
|
||||
return try scanCDATA()
|
||||
}
|
||||
if reader.matches("<!DOCTYPE") {
|
||||
throw PureXML.Parsing.ParseError.unsupportedDoctype(reader.mark)
|
||||
}
|
||||
|
||||
@ -7,7 +7,9 @@ extension PureXML.Parsing.EventReader {
|
||||
throw PureXML.Parsing.ParseError.malformedDeclaration(mark)
|
||||
}
|
||||
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
|
||||
@ -31,7 +33,9 @@ extension PureXML.Parsing.EventReader {
|
||||
mutating func recover(from offset: Int) -> Bool {
|
||||
pending.removeAll()
|
||||
guard reader.peek() != nil else { return false }
|
||||
if reader.mark.offset > offset { return true }
|
||||
if reader.mark.offset > offset {
|
||||
return true
|
||||
}
|
||||
reader.advance()
|
||||
while let character = reader.peek(), character != "<" {
|
||||
reader.advance()
|
||||
@ -102,7 +106,9 @@ extension PureXML.Parsing.EventReader {
|
||||
case let .mismatchedEndTag(_, found, _), let .unexpectedEndTag(found, _):
|
||||
return closeTo(found)
|
||||
default:
|
||||
if !recover(from: start) { reachedEnd = true }
|
||||
if !recover(from: start) {
|
||||
reachedEnd = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,9 @@ extension PureXML.Parsing.EventReader {
|
||||
/// it is a WFC when the document is standalone or could not have
|
||||
/// external declarations, and a validity finding otherwise.
|
||||
var undeclaredEntityIsFatal: Bool {
|
||||
if xmlDeclaration?.standalone == true { return true }
|
||||
if xmlDeclaration?.standalone == true {
|
||||
return true
|
||||
}
|
||||
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
|
||||
/// double-escaped reference surfaces when the replacement is reparsed).
|
||||
mutating func referencesCharacter(_ raw: String) -> Bool {
|
||||
if containsCharacterReference(raw) { return true }
|
||||
if containsCharacterReference(raw) {
|
||||
return true
|
||||
}
|
||||
let entities = referencableEntities
|
||||
var index = raw.startIndex
|
||||
while index < raw.endIndex, let amp = raw[index...].firstIndex(of: "&") {
|
||||
@ -66,7 +70,9 @@ extension PureXML.Parsing.EventReader {
|
||||
func containsCharacterReference(_ raw: String) -> Bool {
|
||||
var sawAmp = false
|
||||
for character in raw {
|
||||
if sawAmp, character == "#" { return true }
|
||||
if sawAmp, character == "#" {
|
||||
return true
|
||||
}
|
||||
sawAmp = character == "&"
|
||||
}
|
||||
return false
|
||||
@ -136,7 +142,9 @@ extension PureXML.Parsing.EventReader {
|
||||
guard isBinding,
|
||||
let declaration = declarations.first(where: { $0.name == attribute.name.description })
|
||||
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: " ")
|
||||
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 unmapped gap and the out-of-range tail.
|
||||
static func rangeScalar(_ pointer: Int) -> Unicode.Scalar? {
|
||||
if pointer > 39419, pointer < 189_000 { return nil }
|
||||
if pointer > 1_237_575 { return nil }
|
||||
if pointer > 39419, pointer < 189_000 {
|
||||
return nil
|
||||
}
|
||||
if pointer > 1_237_575 {
|
||||
return nil
|
||||
}
|
||||
let target = UInt32(pointer)
|
||||
var low = 0
|
||||
var high = gb18030RangePointers.count - 1
|
||||
|
||||
@ -18,7 +18,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
scalars.append(scalar)
|
||||
} else {
|
||||
scalars.append("\u{FFFD}")
|
||||
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
|
||||
if byte <= 0x7F {
|
||||
scalars.append(Unicode.Scalar(byte))
|
||||
}
|
||||
}
|
||||
} else if byte <= 0x7F {
|
||||
scalars.append(Unicode.Scalar(byte))
|
||||
@ -30,7 +32,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
scalars.append("\u{FFFD}")
|
||||
}
|
||||
}
|
||||
if lead != 0 { scalars.append("\u{FFFD}") }
|
||||
if lead != 0 {
|
||||
scalars.append("\u{FFFD}")
|
||||
}
|
||||
return String(scalars)
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,9 @@ extension PureXML.Parsing.XMLDeclaration {
|
||||
// Pseudo-attributes must be separated by whitespace.
|
||||
let before = index
|
||||
skipSpace()
|
||||
if index == before, index < chars.count { return nil }
|
||||
if index == before, index < chars.count {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
@ -52,13 +52,17 @@ public extension PureXML.Parsing {
|
||||
case let .close(name):
|
||||
close(name)
|
||||
case let .text(value):
|
||||
if !value.isEmpty { handler.characters?(value) }
|
||||
if !value.isEmpty {
|
||||
handler.characters?(value)
|
||||
}
|
||||
case let .comment(value):
|
||||
handler.comment?(value)
|
||||
case let .cdata(value):
|
||||
handler.cdata?(value)
|
||||
case let .processingInstruction(target, data):
|
||||
if target.lowercased() != "xml" { handler.processingInstruction?(target, data) }
|
||||
if target.lowercased() != "xml" {
|
||||
handler.processingInstruction?(target, data)
|
||||
}
|
||||
case .ignorable:
|
||||
break
|
||||
}
|
||||
|
||||
@ -64,7 +64,9 @@ private struct TagCursor {
|
||||
}
|
||||
|
||||
private mutating func advance() {
|
||||
if index < chars.count { index += 1 }
|
||||
if index < chars.count {
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
private mutating func skipSpace() {
|
||||
@ -131,11 +133,21 @@ extension PureXML.Parsing {
|
||||
if isPrefix(buffer, ofAny: ["<!--", "<![CDATA[", "<!", "</", "<?"], decidedAt: 2), !final {
|
||||
return .needMore
|
||||
}
|
||||
if startsWith(buffer, "<!--") { return delimited(buffer, open: 4, close: "-->") { .comment(String($0)) } }
|
||||
if startsWith(buffer, "<![CDATA[") { 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) }
|
||||
if startsWith(buffer, "<!--") {
|
||||
return delimited(buffer, open: 4, close: "-->") { .comment(String($0)) }
|
||||
}
|
||||
if startsWith(buffer, "<![CDATA[") {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -162,7 +174,9 @@ extension PureXML.Parsing {
|
||||
guard let end = tagEnd(buffer) else { return final ? .token(.text("<"), consumed: 1) : .needMore }
|
||||
var inner = Array(buffer[1 ..< end])
|
||||
let selfClosing = inner.last == "/"
|
||||
if selfClosing { inner.removeLast() }
|
||||
if selfClosing {
|
||||
inner.removeLast()
|
||||
}
|
||||
let (name, attributes) = parseTag(inner)
|
||||
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 {
|
||||
let character = buffer[index]
|
||||
if let active = quote {
|
||||
if character == active { quote = nil }
|
||||
if character == active {
|
||||
quote = nil
|
||||
}
|
||||
} else if character == "\"" || character == "'" {
|
||||
quote = character
|
||||
} else if character == ">" {
|
||||
|
||||
@ -67,7 +67,9 @@ extension PureXML.Parsing {
|
||||
// Split multi-scalar graphemes so the buffer is scalar-level.
|
||||
var queued: [Character] = []
|
||||
self.pull = {
|
||||
if !queued.isEmpty { return queued.removeFirst() }
|
||||
if !queued.isEmpty {
|
||||
return queued.removeFirst()
|
||||
}
|
||||
guard let next = pull() else { return nil }
|
||||
let scalars = next.unicodeScalars
|
||||
guard scalars.count > 1 else { return next }
|
||||
@ -133,10 +135,14 @@ extension PureXML.Parsing {
|
||||
private mutating func normalizedPull() -> Character? {
|
||||
guard let character = rawNext() else { return nil }
|
||||
// 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" {
|
||||
let next = rawNext()
|
||||
if next != "\n", !(xml11 && next == "\u{85}") { pendingRaw = next }
|
||||
if next != "\n", !(xml11 && next == "\u{85}") {
|
||||
pendingRaw = next
|
||||
}
|
||||
return "\n"
|
||||
}
|
||||
if xml11, character == "\u{85}" || character == "\u{2028}" {
|
||||
@ -201,7 +207,9 @@ extension PureXML.Parsing {
|
||||
/// True if the upcoming characters equal `literal`, without consuming.
|
||||
mutating func matches(_ literal: String) -> Bool {
|
||||
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
|
||||
// and this runs on every scan decision (the hottest comparison in
|
||||
@ -211,7 +219,9 @@ extension PureXML.Parsing {
|
||||
guard available >= count else { return false }
|
||||
var index = 0
|
||||
for character in literal {
|
||||
if buffer[head + index] != character { return false }
|
||||
if buffer[head + index] != character {
|
||||
return false
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
return true
|
||||
@ -254,10 +264,18 @@ extension PureXML.Parsing {
|
||||
let limit = byteIndex + maxLength
|
||||
while index < storage.count {
|
||||
let byte = pointer[index]
|
||||
if byte >= 0x80 { return nil }
|
||||
if !PureXML.Parsing.XMLCharacter.isASCIINameChar(byte) { break }
|
||||
if index >= limit { return nil }
|
||||
if colon == nil, byte == 0x3A { colon = index - byteIndex }
|
||||
if byte >= 0x80 {
|
||||
return nil
|
||||
}
|
||||
if !PureXML.Parsing.XMLCharacter.isASCIINameChar(byte) {
|
||||
break
|
||||
}
|
||||
if index >= limit {
|
||||
return nil
|
||||
}
|
||||
if colon == nil, byte == 0x3A {
|
||||
colon = index - byteIndex
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
let length = index - byteIndex
|
||||
@ -289,7 +307,9 @@ extension PureXML.Parsing {
|
||||
}
|
||||
|
||||
mutating func skipSpace() {
|
||||
if skipSpaceBytes() { return }
|
||||
if skipSpaceBytes() {
|
||||
return
|
||||
}
|
||||
while let character = peek(), character.isXMLWhitespace {
|
||||
advance()
|
||||
}
|
||||
|
||||
@ -22,19 +22,25 @@ extension PureXML.Parsing.Reader {
|
||||
// the slow path consumed "]]" and the run would begin with '>': leave a
|
||||
// leading '>' to the slow path and its cdataCloseInContent check (W3C
|
||||
// ibm14n01).
|
||||
if byteIndex < count, pointer[byteIndex] == 0x3E { return nil }
|
||||
if byteIndex < count, pointer[byteIndex] == 0x3E {
|
||||
return nil
|
||||
}
|
||||
var index = byteIndex
|
||||
var newlines = 0
|
||||
var lastLineStart = -1
|
||||
while index < count {
|
||||
let byte = pointer[index]
|
||||
if byte == 0x3C { break } // '<'
|
||||
if byte == 0x3C {
|
||||
break
|
||||
} // '<'
|
||||
// Valid plain ASCII content only; CR, ']' and non-ASCII go to the
|
||||
// character path.
|
||||
guard byte < 0x80, byte != 0x0D, byte != 0x5D,
|
||||
byte >= 0x20 || byte == 0x09 || byte == 0x0A
|
||||
else {
|
||||
if index == byteIndex { return nil }
|
||||
if index == byteIndex {
|
||||
return nil
|
||||
}
|
||||
break
|
||||
}
|
||||
if byte == 0x0A {
|
||||
|
||||
@ -19,7 +19,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
scalars.append(scalar)
|
||||
} else {
|
||||
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) {
|
||||
scalars.append(single)
|
||||
@ -29,15 +31,21 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
scalars.append("\u{FFFD}")
|
||||
}
|
||||
}
|
||||
if lead != 0 { scalars.append("\u{FFFD}") }
|
||||
if lead != 0 {
|
||||
scalars.append("\u{FFFD}")
|
||||
}
|
||||
return String(scalars)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
static func singleByteScalar(_ byte: UInt8) -> Unicode.Scalar? {
|
||||
if byte <= 0x80 { return Unicode.Scalar(byte) }
|
||||
if (0xA1 ... 0xDF).contains(byte) { return Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1) }
|
||||
if byte <= 0x80 {
|
||||
return Unicode.Scalar(byte)
|
||||
}
|
||||
if (0xA1 ... 0xDF).contains(byte) {
|
||||
return Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -53,7 +61,9 @@ extension PureXML.Parsing.ByteDecoder {
|
||||
let offset = lead < 0xA0 ? 0x81 : 0xC1
|
||||
let trailOffset = trail < 0x7F ? 0x40 : 0x41
|
||||
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 }
|
||||
return Unicode.Scalar(UInt32(jis0208[pointer]))
|
||||
}
|
||||
|
||||
@ -68,8 +68,12 @@ extension PureXML.Parsing {
|
||||
finished = true
|
||||
return nil
|
||||
}
|
||||
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
|
||||
if lead == 0x80 { return "\u{20AC}" }
|
||||
if lead <= 0x7F {
|
||||
return Character(Unicode.Scalar(lead))
|
||||
}
|
||||
if lead == 0x80 {
|
||||
return "\u{20AC}"
|
||||
}
|
||||
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
|
||||
guard let trail = nextByte() else { finished = true
|
||||
return Self.replacement
|
||||
@ -84,8 +88,12 @@ extension PureXML.Parsing {
|
||||
finished = true
|
||||
return nil
|
||||
}
|
||||
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
|
||||
if lead == 0x80 { return "\u{20AC}" }
|
||||
if lead <= 0x7F {
|
||||
return Character(Unicode.Scalar(lead))
|
||||
}
|
||||
if lead == 0x80 {
|
||||
return "\u{20AC}"
|
||||
}
|
||||
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
|
||||
guard let second = nextByte() else { finished = true
|
||||
return Self.replacement
|
||||
@ -108,7 +116,9 @@ extension PureXML.Parsing {
|
||||
finished = true
|
||||
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 let trail = nextByte() else { finished = true
|
||||
return Self.replacement
|
||||
@ -125,7 +135,9 @@ extension PureXML.Parsing {
|
||||
finished = true
|
||||
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 let trail = nextByte() else { finished = true
|
||||
return Self.replacement
|
||||
@ -140,7 +152,9 @@ extension PureXML.Parsing {
|
||||
finished = true
|
||||
return nil
|
||||
}
|
||||
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
|
||||
if lead <= 0x7F {
|
||||
return Character(Unicode.Scalar(lead))
|
||||
}
|
||||
if lead == 0x8E {
|
||||
guard let byte = nextByte() else { finished = true
|
||||
return Self.replacement
|
||||
@ -167,7 +181,9 @@ extension PureXML.Parsing {
|
||||
finished = true
|
||||
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 let trail = nextByte() else {
|
||||
finished = true
|
||||
@ -186,7 +202,9 @@ extension PureXML.Parsing {
|
||||
}
|
||||
ordered.append(UInt32(byte))
|
||||
}
|
||||
if !bigEndian { ordered.reverse() }
|
||||
if !bigEndian {
|
||||
ordered.reverse()
|
||||
}
|
||||
let value = ordered.reduce(UInt32(0)) { ($0 << 8) | $1 }
|
||||
return Character(Unicode.Scalar(value) ?? "\u{FFFD}")
|
||||
}
|
||||
|
||||
@ -6,7 +6,9 @@ extension PureXML.Parsing.StreamingDecoder {
|
||||
finished = true
|
||||
return nil
|
||||
}
|
||||
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
|
||||
if lead <= 0x7F {
|
||||
return Character(Unicode.Scalar(lead))
|
||||
}
|
||||
if lead == 0x8E {
|
||||
guard let plane = nextByte(), let byte1 = nextByte(), let byte2 = nextByte() else {
|
||||
finished = true
|
||||
|
||||
@ -186,7 +186,9 @@ public extension PureXML.Parsing {
|
||||
applyStartElement(name: name, attributes: attributes)
|
||||
case let .endElement(name):
|
||||
currentDepth -= 1
|
||||
if !langStack.isEmpty { langStack.removeLast() }
|
||||
if !langStack.isEmpty {
|
||||
langStack.removeLast()
|
||||
}
|
||||
currentName = name
|
||||
xmlLang = langStack.last ?? nil
|
||||
set(.endElement, name: name.description, depth: currentDepth)
|
||||
|
||||
@ -67,7 +67,9 @@ public extension PureXML.Parsing {
|
||||
|
||||
/// `NameChar`: a name character after the first.
|
||||
public static func isNameChar(_ scalar: Unicode.Scalar) -> Bool {
|
||||
if isNameStart(scalar) { return true }
|
||||
if isNameStart(scalar) {
|
||||
return true
|
||||
}
|
||||
switch scalar.value {
|
||||
case 0x2D, // "-"
|
||||
0x2E, // "."
|
||||
@ -86,7 +88,9 @@ public extension PureXML.Parsing {
|
||||
var scalars = name.unicodeScalars.makeIterator()
|
||||
guard let first = scalars.next(), isNameStart(first) else { return false }
|
||||
while let scalar = scalars.next() {
|
||||
if !isNameChar(scalar) { return false }
|
||||
if !isNameChar(scalar) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -22,7 +22,9 @@ extension PureXML.Pattern {
|
||||
case let .prefixWildcard(prefix):
|
||||
return name.prefix == prefix
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -105,7 +107,9 @@ public extension PureXML.Pattern {
|
||||
private func ancestorMatches(_ stepIndex: Int, below pathIndex: Int, _ path: [PureXML.Model.QualifiedName]) -> Bool {
|
||||
var candidate = pathIndex - 1
|
||||
while candidate >= 0 {
|
||||
if matchUpTo(stepIndex, candidate, path) { return true }
|
||||
if matchUpTo(stepIndex, candidate, path) {
|
||||
return true
|
||||
}
|
||||
candidate -= 1
|
||||
}
|
||||
return false
|
||||
|
||||
@ -42,8 +42,12 @@ extension PureXML.Pattern {
|
||||
}
|
||||
|
||||
private mutating func leadingGap() -> Gap {
|
||||
if consume("//") { return .descendant }
|
||||
if consume("/") { return .root }
|
||||
if consume("//") {
|
||||
return .descendant
|
||||
}
|
||||
if consume("/") {
|
||||
return .root
|
||||
}
|
||||
return .descendant
|
||||
}
|
||||
|
||||
@ -60,11 +64,17 @@ extension PureXML.Pattern {
|
||||
|
||||
private mutating func parseStep(gap: Gap) throws -> RawStep {
|
||||
guard let character = peek() else { throw PatternError.expectedStep }
|
||||
if character == "[" { throw PatternError.unsupported("predicate") }
|
||||
if character == "." { throw PatternError.unsupported(".") }
|
||||
if character == "[" {
|
||||
throw PatternError.unsupported("predicate")
|
||||
}
|
||||
if character == "." {
|
||||
throw PatternError.unsupported(".")
|
||||
}
|
||||
let isAttribute = consume("@")
|
||||
let test = try parseTest()
|
||||
if peek() == "[" { throw PatternError.unsupported("predicate") }
|
||||
if peek() == "[" {
|
||||
throw PatternError.unsupported("predicate")
|
||||
}
|
||||
return RawStep(gap: gap, isAttribute: isAttribute, test: test)
|
||||
}
|
||||
|
||||
@ -116,7 +126,9 @@ extension PureXML.Pattern {
|
||||
}
|
||||
|
||||
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)
|
||||
collect(stack: stack, attributes: attributes, into: &results)
|
||||
case .endElement:
|
||||
if !stack.isEmpty { stack.removeLast() }
|
||||
if !stack.isEmpty {
|
||||
stack.removeLast()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@ -30,7 +30,9 @@ extension PureXML.Regex {
|
||||
next.insert(transition.target)
|
||||
}
|
||||
}
|
||||
if next.isEmpty { return false }
|
||||
if next.isEmpty {
|
||||
return false
|
||||
}
|
||||
current = closure(next)
|
||||
}
|
||||
return current.contains(accept)
|
||||
@ -104,7 +106,11 @@ extension PureXML.Regex {
|
||||
var last: Int?
|
||||
for node in nodes {
|
||||
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
|
||||
}
|
||||
return (first ?? addState(), last ?? addState())
|
||||
@ -127,7 +133,9 @@ extension PureXML.Regex {
|
||||
let boundedMin = Swift.min(minimum, Self.repeatUnrollCap)
|
||||
var unrolled = 0
|
||||
for _ in 0 ..< boundedMin {
|
||||
if stateBudgetExhausted { break }
|
||||
if stateBudgetExhausted {
|
||||
break
|
||||
}
|
||||
let part = fragment(for: node)
|
||||
states[current].epsilon.append(part.start)
|
||||
current = part.accept
|
||||
|
||||
@ -23,7 +23,9 @@ extension PureXML.Regex {
|
||||
/// a two-letter category (`Lu`, `Nd`, ...), or an `Is<Block>` block name.
|
||||
enum CategoryMatcher {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,9 @@ extension PureXML.Regex {
|
||||
}
|
||||
|
||||
static func parse(_ pattern: String) throws -> Node {
|
||||
if pattern.isEmpty { return .empty }
|
||||
if pattern.isEmpty {
|
||||
return .empty
|
||||
}
|
||||
var parser = RegexParser(pattern)
|
||||
let node = try parser.parseAlternation()
|
||||
guard parser.isAtEnd else { throw RegexError.unbalanced }
|
||||
@ -41,7 +43,9 @@ extension PureXML.Regex {
|
||||
while let character = peek(), character != "|", character != ")" {
|
||||
try pieces.append(parsePiece())
|
||||
}
|
||||
if pieces.isEmpty { return .empty }
|
||||
if pieces.isEmpty {
|
||||
return .empty
|
||||
}
|
||||
return pieces.count == 1 ? pieces[0] : .concat(pieces)
|
||||
}
|
||||
|
||||
@ -75,7 +79,9 @@ extension PureXML.Regex {
|
||||
maximum = peek() == "}" ? nil : parseInteger()
|
||||
}
|
||||
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()
|
||||
return .repeated(atom, min: minimum, max: maximum)
|
||||
}
|
||||
@ -186,7 +192,9 @@ extension PureXML.Regex {
|
||||
cls.negated = true
|
||||
}
|
||||
// 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 != "]" {
|
||||
if character == "-", peek(1) == "[" {
|
||||
advance()
|
||||
@ -236,13 +244,19 @@ extension PureXML.Regex {
|
||||
// A nested `[` is legal only as a `-[...]` subtraction, routed by
|
||||
// parseClass/parseClassMember before reaching here; any other `[`
|
||||
// is an unescaped class bracket, invalid per Appendix F.
|
||||
if character == "[" { throw RegexError.unescapedClassBracket }
|
||||
if character == "[" {
|
||||
throw RegexError.unescapedClassBracket
|
||||
}
|
||||
return .scalar(scalar(of: character))
|
||||
}
|
||||
guard let escaped = peek() else { throw RegexError.incompleteEscape }
|
||||
advance()
|
||||
if let single = Self.singleEscape(escaped) { return .scalar(single) }
|
||||
if let member = Self.classEscape(escaped) { return member }
|
||||
if let single = Self.singleEscape(escaped) {
|
||||
return .scalar(single)
|
||||
}
|
||||
if let member = Self.classEscape(escaped) {
|
||||
return member
|
||||
}
|
||||
if escaped == "p" || escaped == "P" {
|
||||
return try .category(parseCategory(negated: escaped == "P"))
|
||||
}
|
||||
@ -274,7 +288,9 @@ extension PureXML.Regex {
|
||||
}
|
||||
|
||||
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,
|
||||
// i.e. excludes {a,c,d,e,g}, not just {c,d,e} (XSTS RegexTest_430).
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -47,7 +47,9 @@ extension PureXML.Regex {
|
||||
]
|
||||
|
||||
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" }
|
||||
return categoryCodes[scalar.properties.generalCategory] ?? "Cn"
|
||||
}
|
||||
|
||||
@ -92,7 +92,9 @@ public extension PureXML.Schema {
|
||||
func derives(from other: BuiltinType) -> Bool {
|
||||
var current: BuiltinType? = self
|
||||
while let type = current {
|
||||
if type == other { return true }
|
||||
if type == other {
|
||||
return true
|
||||
}
|
||||
current = type.derivationBase
|
||||
}
|
||||
return false
|
||||
@ -108,8 +110,12 @@ public extension PureXML.Schema {
|
||||
}
|
||||
|
||||
var primitive: Primitive {
|
||||
if let primitive = Self.stringLike[self] { return primitive }
|
||||
if Self.integerLike.contains(self) { return .integer }
|
||||
if let primitive = Self.stringLike[self] {
|
||||
return primitive
|
||||
}
|
||||
if Self.integerLike.contains(self) {
|
||||
return .integer
|
||||
}
|
||||
return computedPrimitive
|
||||
}
|
||||
|
||||
|
||||
@ -121,9 +121,15 @@ public extension PureXML.Schema {
|
||||
return .notAbsent
|
||||
case let (.notNamespace(name), .enumerated(set)), let (.enumerated(set), .notNamespace(name)):
|
||||
let hasName = set.contains(name), hasAbsent = set.contains("")
|
||||
if hasName, hasAbsent { return .any } // 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
|
||||
if hasName, hasAbsent {
|
||||
return .any
|
||||
} // 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
|
||||
case let (.notAbsent, .enumerated(set)), let (.enumerated(set), .notAbsent):
|
||||
return set.contains("") ? .any : .notAbsent
|
||||
@ -131,8 +137,12 @@ public extension PureXML.Schema {
|
||||
}
|
||||
|
||||
private static func unionProcessContents(_ left: ProcessContents, _ right: ProcessContents) -> ProcessContents {
|
||||
if left == .strict || right == .strict { return .strict }
|
||||
if left == .lax || right == .lax { return .lax }
|
||||
if left == .strict || right == .strict {
|
||||
return .strict
|
||||
}
|
||||
if left == .lax || right == .lax {
|
||||
return .lax
|
||||
}
|
||||
return .skip
|
||||
}
|
||||
|
||||
@ -191,8 +201,12 @@ public extension PureXML.Schema {
|
||||
// When the two differ the intersection is strictly not expressible; keep
|
||||
// the less strict so a matched attribute is never validated more harshly
|
||||
// than either source asked for (under-validation, not over-rejection).
|
||||
if left == right { return left }
|
||||
if left == .skip || right == .skip { return .skip }
|
||||
if left == right {
|
||||
return left
|
||||
}
|
||||
if left == .skip || right == .skip {
|
||||
return .skip
|
||||
}
|
||||
return .lax
|
||||
}
|
||||
}
|
||||
@ -292,7 +306,9 @@ public extension PureXML.Schema {
|
||||
|
||||
/// The fixed value, when this is a `fixed` constraint.
|
||||
public var fixedValue: String? {
|
||||
if case let .fixed(value) = self { return value }
|
||||
if case let .fixed(value) = self {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -23,9 +23,9 @@ public extension PureXML.Schema {
|
||||
/// The named type table that `ElementType.typeReference` resolves against.
|
||||
let types: [String: ElementType]
|
||||
/// 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.
|
||||
private let elementConstraints: [String: ValueConstraint]
|
||||
let elementConstraints: [String: ValueConstraint]
|
||||
/// Complex types declared `abstract="true"`, keyed by namespaced identity
|
||||
/// (`{ns}local`) so same-local-name types in different namespaces stay distinct.
|
||||
let abstractTypes: Set<String>
|
||||
@ -79,8 +79,21 @@ public extension PureXML.Schema {
|
||||
at path: [PureXML.Validation.PathKey] = [],
|
||||
namespaceBindings inheritedBindings: [String: String] = [:],
|
||||
) -> [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)
|
||||
var errors: [XSDFailure] = []
|
||||
var errors: [XSDFinding] = []
|
||||
validateAttributes(element, type, at: path, into: &errors)
|
||||
// An xsi:nil element keeps its attribute obligations but must have no
|
||||
// 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)
|
||||
// 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".
|
||||
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)
|
||||
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
|
||||
/// `typeReference` through the type table.
|
||||
public func validate(
|
||||
@ -159,7 +119,16 @@ public extension PureXML.Schema {
|
||||
as type: ElementType,
|
||||
at path: [PureXML.Validation.PathKey] = [],
|
||||
) -> [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)
|
||||
validateChild(element, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
return errors
|
||||
@ -172,17 +141,21 @@ public extension PureXML.Schema {
|
||||
_ content: ContentType,
|
||||
at path: XSDPath,
|
||||
namespaceBindings: [String: String],
|
||||
into errors: inout [XSDFailure],
|
||||
into errors: inout [XSDFinding],
|
||||
) {
|
||||
let children = element.children.compactMap(\.element)
|
||||
switch content {
|
||||
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)
|
||||
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)) {
|
||||
errors.append(XSDFailure(reason: "content: \(error)", at: path))
|
||||
errors.append(XSDFinding(.simpleContentTypeValid, reason: "content: \(error)", at: path))
|
||||
}
|
||||
case let .elementOnly(particle):
|
||||
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 }
|
||||
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
|
||||
@ -204,7 +177,7 @@ public extension PureXML.Schema {
|
||||
children: [PureXML.Model.Element],
|
||||
at path: XSDPath,
|
||||
namespaceBindings: [String: String],
|
||||
into errors: inout [XSDFailure],
|
||||
into errors: inout [XSDFinding],
|
||||
) {
|
||||
// Locate each content-model violation at the offending child (or the
|
||||
// missing one), with a recovery hint, rather than one opaque failure,
|
||||
@ -232,7 +205,7 @@ public extension PureXML.Schema {
|
||||
_ children: [PureXML.Model.Element],
|
||||
matched: [PureXML.Schema.MatchedParticle?],
|
||||
frame: XSDChildValidationFrame,
|
||||
into errors: inout [XSDFailure],
|
||||
into errors: inout [XSDFinding],
|
||||
) {
|
||||
let steps = Self.childSteps(children)
|
||||
for (index, child) in children.enumerated() {
|
||||
@ -254,60 +227,13 @@ public extension PureXML.Schema {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates a wildcard-matched child by its `processContents`: skip does
|
||||
/// 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(
|
||||
func validateChild(
|
||||
_ child: PureXML.Model.Element,
|
||||
against declared: ElementType,
|
||||
at path: XSDPath,
|
||||
namespaceBindings: [String: String],
|
||||
particleConstraint: ValueConstraint? = nil,
|
||||
into errors: inout [XSDFailure],
|
||||
into errors: inout [XSDFinding],
|
||||
) {
|
||||
// An instance `xsi:type` overrides the declared type, provided the named
|
||||
// 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
|
||||
// type requires one.
|
||||
if let xsiError = xsiTypeOverrideError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
|
||||
errors.append(xsiError)
|
||||
errors.append(XSDFinding(.xsiTypeSubstitutionValid, xsiError))
|
||||
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
|
||||
// xsi:nil element is equally bound by it.
|
||||
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) {
|
||||
errors += nilErrors
|
||||
return
|
||||
}
|
||||
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
|
||||
// (not the empty string) is what must be valid against the type,
|
||||
// including any xsi:type override already resolved into `simple`.
|
||||
let text = Self.rawTextContent(child)
|
||||
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)
|
||||
errors += elementFixedErrors(child, valueType: simple, particleFixed: particleConstraint?.fixedValue, at: path)
|
||||
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:
|
||||
validateResolvedReference(declared, child, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
}
|
||||
@ -360,13 +290,13 @@ public extension PureXML.Schema {
|
||||
_ child: PureXML.Model.Element,
|
||||
at path: XSDPath,
|
||||
namespaceBindings: [String: String],
|
||||
into errors: inout [XSDFailure],
|
||||
into errors: inout [XSDFinding],
|
||||
) {
|
||||
switch resolveReference(declared) {
|
||||
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):
|
||||
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):
|
||||
validateChild(child, against: resolved, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
}
|
||||
|
||||
@ -3,21 +3,21 @@ extension PureXML.Schema.ComplexValidator {
|
||||
_ element: PureXML.Model.Element,
|
||||
_ type: PureXML.Schema.ComplexType,
|
||||
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) }
|
||||
for use in type.attributes where !use.prohibited {
|
||||
let match = present.first { Self.attributeMatches($0.name, declared: use.name, use: use, on: element.name) }
|
||||
if let match {
|
||||
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) {
|
||||
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)
|
||||
} 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 {
|
||||
// An absent attribute takes its `default`/`fixed` value, so a
|
||||
// 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)
|
||||
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",
|
||||
at: path,
|
||||
))
|
||||
@ -45,7 +46,9 @@ extension PureXML.Schema.ComplexValidator {
|
||||
on element: PureXML.Model.Element,
|
||||
) -> Int {
|
||||
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,
|
||||
on elementName: PureXML.Model.QualifiedName,
|
||||
) -> Bool {
|
||||
if sameName(instance, declared) { return true }
|
||||
if sameName(instance, declared) {
|
||||
return true
|
||||
}
|
||||
guard use.chameleonUnprefixed,
|
||||
instance.namespaceURI == nil || instance.namespaceURI?.isEmpty == true,
|
||||
let declaredNamespace = declared.namespaceURI, !declaredNamespace.isEmpty
|
||||
@ -86,7 +91,7 @@ extension PureXML.Schema.ComplexValidator {
|
||||
element: PureXML.Model.Element,
|
||||
type: PureXML.Schema.ComplexType,
|
||||
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},
|
||||
// so a prohibited attribute is treated as undeclared and left to the
|
||||
@ -96,7 +101,7 @@ extension PureXML.Schema.ComplexValidator {
|
||||
let wildcard = type.attributeWildcard
|
||||
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 {
|
||||
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
|
||||
}
|
||||
switch wildcard.processContents {
|
||||
@ -108,7 +113,8 @@ extension PureXML.Schema.ComplexValidator {
|
||||
}
|
||||
case .strict:
|
||||
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)'",
|
||||
at: path,
|
||||
))
|
||||
@ -127,13 +133,13 @@ extension PureXML.Schema.ComplexValidator {
|
||||
_ attribute: PureXML.Model.Attribute,
|
||||
use: PureXML.Schema.AttributeUse,
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
into errors: inout [PureXML.Validation.ValidationError],
|
||||
into errors: inout [XSDFinding],
|
||||
) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
extension PureXML.Schema.ComplexValidator {
|
||||
/// Walks the children through the content automaton, flagging the first child
|
||||
/// 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 steps = Self.childSteps(children)
|
||||
// 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() {
|
||||
guard let next = matcher.step(current, over: child.name) else {
|
||||
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
|
||||
}
|
||||
current = next
|
||||
}
|
||||
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,
|
||||
/// 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)
|
||||
let steps = Self.childSteps(children)
|
||||
for (index, child) in children.enumerated() {
|
||||
@ -35,7 +39,7 @@ extension PureXML.Schema.ComplexValidator {
|
||||
let room = member.maxOccurs.map { counts[slot] < $0 } ?? true
|
||||
return room && Self.memberMatches(member.term, child.name)
|
||||
}) 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
|
||||
}
|
||||
counts[position] += 1
|
||||
@ -44,10 +48,12 @@ extension PureXML.Schema.ComplexValidator {
|
||||
// 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
|
||||
// `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 {
|
||||
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.
|
||||
static func expectation(_ labels: [PureXML.Schema.TermLabel]) -> String {
|
||||
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
|
||||
}
|
||||
let unique = Set(names).sorted()
|
||||
|
||||
@ -14,12 +14,24 @@ extension PureXML.Schema.ComplexValidator {
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
namespaceBindings: [String: String],
|
||||
) -> PureXML.Validation.ValidationError? {
|
||||
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return blocked }
|
||||
if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return notDerived }
|
||||
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) }
|
||||
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
|
||||
return blocked
|
||||
}
|
||||
if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
|
||||
return notDerived
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -99,7 +111,9 @@ extension PureXML.Schema.ComplexValidator {
|
||||
case .simple, .complex: declared
|
||||
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
|
||||
}
|
||||
|
||||
@ -157,7 +171,9 @@ extension PureXML.Schema.ComplexValidator {
|
||||
|
||||
/// Whether an element type resolves to a complex type.
|
||||
static func isComplexType(_ type: PureXML.Schema.ElementType) -> Bool {
|
||||
if case .complex = type { return true }
|
||||
if case .complex = type {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -237,13 +253,17 @@ extension PureXML.Schema.ComplexValidator {
|
||||
for member in members {
|
||||
// The substitute IS a member: derived with no method of its own, so
|
||||
// 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 {
|
||||
continue
|
||||
}
|
||||
anyConfirmedPath = true
|
||||
// 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.
|
||||
return anyConfirmedPath
|
||||
@ -325,8 +345,12 @@ extension PureXML.Schema.ComplexValidator {
|
||||
/// namespaced backbone and the check stays silent rather than risk a false
|
||||
/// positive.
|
||||
static func derivationKey(fromReference reference: String) -> String {
|
||||
if reference.hasPrefix("type:") { return String(reference.dropFirst("type:".count)) }
|
||||
if reference.hasPrefix("{") || reference.contains(":") { return reference }
|
||||
if reference.hasPrefix("type:") {
|
||||
return String(reference.dropFirst("type:".count))
|
||||
}
|
||||
if reference.hasPrefix("{") || reference.contains(":") {
|
||||
return reference
|
||||
}
|
||||
return key(PureXML.Model.QualifiedName(localName: reference, namespaceURI: nil))
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,9 @@ extension PureXML.Schema.ComplexValidator {
|
||||
return wildcard.processContents
|
||||
case let .group(group):
|
||||
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
|
||||
case .element:
|
||||
@ -39,7 +41,9 @@ extension PureXML.Schema.ComplexValidator {
|
||||
fileprivate static func collectTypes(_ term: XSDTerm, into result: inout [String: XSDElementType]) {
|
||||
switch term {
|
||||
case let .element(name, type, _, _, _, _):
|
||||
if let type { result[key(name)] = type }
|
||||
if let type {
|
||||
result[key(name)] = type
|
||||
}
|
||||
case let .group(group):
|
||||
for member in group.particles {
|
||||
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]) {
|
||||
switch term {
|
||||
case let .element(name, _, _, valueConstraint, _, _):
|
||||
if let valueConstraint { result[key(name)] = valueConstraint }
|
||||
if let valueConstraint {
|
||||
result[key(name)] = valueConstraint
|
||||
}
|
||||
case let .group(group):
|
||||
for member in group.particles {
|
||||
collectValueConstraints(member.term, into: &result)
|
||||
@ -200,7 +206,13 @@ extension PureXML.Schema.ComplexValidator {
|
||||
|
||||
/// Whether the element has any child element or non-whitespace text.
|
||||
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.
|
||||
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 uri == PureXML.Schema.XSDParser.xsdNamespace, let builtin = urOrBuiltinType(named: local) {
|
||||
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 {
|
||||
// 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
|
||||
/// `parent`'s content model, keyed by namespaced identity (so same-local-name
|
||||
/// 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 }
|
||||
switch complex.content {
|
||||
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,
|
||||
types[PureXML.Schema.XSDParser.elementDeclarationKey(child)] == nil
|
||||
else { return nil }
|
||||
@ -131,7 +148,9 @@ public extension PureXML.Schema.ComplexValidator {
|
||||
return nil
|
||||
case let .group(group):
|
||||
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
|
||||
}
|
||||
@ -160,28 +179,45 @@ public extension PureXML.Schema.ComplexValidator {
|
||||
valueConstraint: PureXML.Schema.ValueConstraint? = nil,
|
||||
at path: [PureXML.Validation.PathKey] = [],
|
||||
) -> [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 {
|
||||
case let .simple(simple):
|
||||
validateSimpleElement(element, simple, valueConstraint: valueConstraint, at: path, into: &errors)
|
||||
case let .complex(complex):
|
||||
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)
|
||||
// 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:).
|
||||
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)
|
||||
case .typeReference:
|
||||
// The shared resolver also guards a circular chain, which would
|
||||
// otherwise recurse here without terminating.
|
||||
switch resolveReference(type) {
|
||||
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):
|
||||
return [XSDFailure(reason: "circular type reference '\(name)'", at: path)]
|
||||
return [XSDFinding(.typeReferenceResolves, reason: "circular type reference '\(name)'", at: path)]
|
||||
case let .resolved(resolved):
|
||||
return validateShallow(element, as: resolved, valueConstraint: valueConstraint, at: path)
|
||||
return validateShallowTagged(element, as: resolved, valueConstraint: valueConstraint, at: path)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
@ -192,21 +228,21 @@ public extension PureXML.Schema.ComplexValidator {
|
||||
_ simple: PureXML.Schema.SimpleType,
|
||||
valueConstraint: PureXML.Schema.ValueConstraint? = nil,
|
||||
at path: XSDPath,
|
||||
into errors: inout [XSDFailure],
|
||||
into errors: inout [XSDFinding],
|
||||
) {
|
||||
if let nilErrors = nilErrors(element, fixedValue: valueConstraint?.fixedValue, at: path) {
|
||||
errors += nilErrors
|
||||
return
|
||||
}
|
||||
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
|
||||
// empty string) is what must be valid against the type, matching the tree
|
||||
// path's validateChild.
|
||||
let text = Self.rawTextContent(element)
|
||||
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)
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
switch content {
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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 {
|
||||
allStructureErrors(group, children: children, groupOptional: particle.occurrenceRange.minimum.isZero, at: path, into: &errors)
|
||||
} 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,
|
||||
namespaceBindings: [String: String] = [:],
|
||||
at path: XSDPath,
|
||||
into errors: inout [XSDFailure],
|
||||
into errors: inout [XSDFinding],
|
||||
) -> PureXML.Schema.ElementType? {
|
||||
guard Self.xsiTypeAttributeValue(child) != nil else { return declared }
|
||||
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
|
||||
if let builtin = PureXML.Schema.BuiltinType(rawValue: overriding) {
|
||||
if xsiBuiltinSubstitutes(builtin, for: declared) {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -32,7 +38,9 @@ extension PureXML.Schema.ComplexValidator {
|
||||
private func xsiBuiltinSubstitutes(_ xsi: PureXML.Schema.BuiltinType, for declared: PureXML.Schema.ElementType) -> Bool {
|
||||
switch declared {
|
||||
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) {
|
||||
case let .resolved(resolved): return xsiBuiltinSubstitutes(xsi, for: resolved)
|
||||
case .unknown, .circular: return false
|
||||
|
||||
@ -140,7 +140,9 @@ extension PureXML.Schema {
|
||||
func admissible(from current: Set<ContentConfiguration>) -> [TermLabel] {
|
||||
var labels: [TermLabel] = []
|
||||
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
|
||||
}
|
||||
@ -203,7 +205,9 @@ extension PureXML.Schema {
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
@ -63,8 +63,12 @@ public extension PureXML.Schema {
|
||||
/// foreign annotation content.
|
||||
private static func forEachComplexType(_ node: XSDTree, _ visit: (XSDTree) -> Void) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
if local == "appinfo" || local == "documentation" { return }
|
||||
if local == "complexType" { visit(node) }
|
||||
if local == "appinfo" || local == "documentation" {
|
||||
return
|
||||
}
|
||||
if local == "complexType" {
|
||||
visit(node)
|
||||
}
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||
forEachComplexType(child, visit)
|
||||
}
|
||||
|
||||
@ -98,7 +98,9 @@ private struct CompWalk {
|
||||
var identityConflict: String?
|
||||
|
||||
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
|
||||
@ -156,13 +158,17 @@ private struct CompWalk {
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
mutating func summarize(_ node: PureXML.Model.TreeNode, visiting: Set<String>) -> CompSummary {
|
||||
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)
|
||||
// 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
|
||||
@ -227,7 +233,9 @@ private struct CompWalk {
|
||||
var suffix: [CompItem] = []
|
||||
for next in parts[(index + 1)...] {
|
||||
suffix.append(contentsOf: next.first)
|
||||
if !next.nullable { break }
|
||||
if !next.nullable {
|
||||
break
|
||||
}
|
||||
}
|
||||
suffixFirst[index] = suffix
|
||||
}
|
||||
@ -239,7 +247,9 @@ private struct CompWalk {
|
||||
var prefixFirsts: [[CompItem]] = []
|
||||
for part in parts {
|
||||
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
|
||||
// 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 {
|
||||
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))
|
||||
}
|
||||
@ -293,8 +305,12 @@ private struct CompWalk {
|
||||
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: []) }
|
||||
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
|
||||
if visiting.contains(name) { return redefineSelfReference(name, visiting: visiting) }
|
||||
if let cached = groupCache[name] { return cached }
|
||||
if visiting.contains(name) {
|
||||
return redefineSelfReference(name, visiting: visiting)
|
||||
}
|
||||
if let cached = groupCache[name] {
|
||||
return cached
|
||||
}
|
||||
guard let definition = context.groups[name], let model = modelChild(definition)
|
||||
else { return CompSummary(nullable: true, first: [], lastFollow: []) }
|
||||
// Summarize the group once: its internal decision sets are context-independent
|
||||
|
||||
@ -11,8 +11,12 @@ enum CompConflict {
|
||||
for item in set {
|
||||
switch item.label {
|
||||
case let .name(qname):
|
||||
if let owner = nameOwner[qname], owner != item.particle { return qname.localName }
|
||||
if nameOwner[qname] == nil { nameOwner[qname] = item.particle }
|
||||
if let owner = nameOwner[qname], owner != item.particle {
|
||||
return qname.localName
|
||||
}
|
||||
if nameOwner[qname] == nil {
|
||||
nameOwner[qname] = item.particle
|
||||
}
|
||||
case let .wildcard(card):
|
||||
wildcards.append((card, item.particle))
|
||||
}
|
||||
@ -20,7 +24,9 @@ enum CompConflict {
|
||||
for (index, wildcard) in wildcards.enumerated() {
|
||||
// A wildcard conflicts with any name it admits, 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) {
|
||||
return "any"
|
||||
}
|
||||
|
||||
@ -3,10 +3,14 @@ extension PureXML.Schema.ContentModelDeterminism {
|
||||
/// complex type's content model, reached through `complexContent` and its
|
||||
/// `restriction`/`extension`, or nil for simple or empty content.
|
||||
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 }
|
||||
for derivation in PureXML.Schema.XSDNode.elementChildren(complexContent) {
|
||||
if let model = modelGroupChild(derivation) { return model }
|
||||
if let model = modelGroupChild(derivation) {
|
||||
return model
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -28,7 +28,9 @@ enum GroupDependencyOrder {
|
||||
order.append(name)
|
||||
continue
|
||||
}
|
||||
if visited.contains(name) { continue }
|
||||
if visited.contains(name) {
|
||||
continue
|
||||
}
|
||||
visited.insert(name)
|
||||
onPath.insert(name)
|
||||
work.append((name, true))
|
||||
|
||||
@ -108,7 +108,11 @@ extension PureXML.Schema {
|
||||
var nullable = true
|
||||
for member in particles {
|
||||
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
|
||||
fragmentCounters.append(contentsOf: part.counters)
|
||||
nullable = nullable && part.nullable
|
||||
|
||||
@ -139,7 +139,9 @@ extension PureXML.Schema {
|
||||
/// The shared fragment for a group definition, compiled once and memoized.
|
||||
/// Its accept is marked `isReturn`.
|
||||
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
|
||||
// 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
|
||||
@ -176,7 +178,11 @@ extension PureXML.Schema {
|
||||
var nullable = true
|
||||
for member in particles {
|
||||
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
|
||||
fragmentCounters.append(contentsOf: part.counters)
|
||||
nullable = nullable && part.nullable
|
||||
|
||||
@ -109,7 +109,9 @@ extension PureXML.Schema {
|
||||
next.insert(ContentRTNConfiguration(state: target, counters: configuration.counters, gss: configuration.gss))
|
||||
}
|
||||
}
|
||||
if next.isEmpty { return nil }
|
||||
if next.isEmpty {
|
||||
return nil
|
||||
}
|
||||
position += 1
|
||||
return closure(next)
|
||||
}
|
||||
@ -121,7 +123,9 @@ extension PureXML.Schema {
|
||||
func admissible(from current: Set<ContentRTNConfiguration>) -> [TermLabel] {
|
||||
var labels: [TermLabel] = []
|
||||
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
|
||||
}
|
||||
@ -154,8 +158,12 @@ extension PureXML.Schema {
|
||||
let state = configuration.state
|
||||
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))
|
||||
if anyState.map({ state < $0 }) ?? true { anyState = state }
|
||||
if case .name = label, namedState.map({ state < $0 }) ?? true { namedState = state }
|
||||
if anyState.map({ state < $0 }) ?? true {
|
||||
anyState = state
|
||||
}
|
||||
if case .name = label, namedState.map({ state < $0 }) ?? true {
|
||||
namedState = state
|
||||
}
|
||||
}
|
||||
guard let chosenState = namedState ?? anyState, !next.isEmpty else {
|
||||
result.append(contentsOf: Array(repeating: nil, count: names.count - result.count))
|
||||
@ -190,7 +198,9 @@ extension PureXML.Schema {
|
||||
while true {
|
||||
if !pending.isEmpty {
|
||||
let configuration = pending.removeLast()
|
||||
if seen.insert(configuration).inserted { work.append(configuration) }
|
||||
if seen.insert(configuration).inserted {
|
||||
work.append(configuration)
|
||||
}
|
||||
continue
|
||||
}
|
||||
guard let configuration = work.popLast() else { break }
|
||||
@ -203,7 +213,9 @@ extension PureXML.Schema {
|
||||
for call in state.calls {
|
||||
let node = internNode(returnState: call.returnTo, savedCounters: configuration.counters, parent: configuration.gss)
|
||||
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)
|
||||
}
|
||||
if state.isReturn, configuration.gss != 0 {
|
||||
@ -218,7 +230,9 @@ extension PureXML.Schema {
|
||||
|
||||
private func drainPending(into seen: inout Set<ContentRTNConfiguration>, work: inout [ContentRTNConfiguration]) {
|
||||
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(":"),
|
||||
let second = readFixed(2) 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.minute = minute
|
||||
value.second = second
|
||||
@ -107,12 +109,15 @@ extension PureXML.Schema {
|
||||
}
|
||||
|
||||
private mutating func parseTimezone(into value: inout DateTimeValue) -> Bool {
|
||||
if consume("Z") { value.offsetMinutes = 0
|
||||
if consume("Z") {
|
||||
value.offsetMinutes = 0
|
||||
return true
|
||||
}
|
||||
guard peek() == "+" || peek() == "-" else { return true }
|
||||
let negative = consume("-")
|
||||
if !negative { advance() }
|
||||
if !negative {
|
||||
advance()
|
||||
}
|
||||
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 {
|
||||
return false
|
||||
@ -136,7 +141,9 @@ extension PureXML.Schema {
|
||||
}
|
||||
|
||||
private mutating func advance() {
|
||||
if index < chars.count { index += 1 }
|
||||
if index < chars.count {
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
private mutating func consume(_ literal: String) -> Bool {
|
||||
|
||||
@ -65,7 +65,9 @@ extension PureXML.Schema {
|
||||
}
|
||||
|
||||
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)
|
||||
return lhs.negative ? magnitude > 0 : magnitude < 0
|
||||
}
|
||||
@ -75,7 +77,9 @@ extension PureXML.Schema {
|
||||
if lhs.integerDigits.count != rhs.integerDigits.count {
|
||||
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
|
||||
}
|
||||
|
||||
@ -85,7 +89,9 @@ extension PureXML.Schema {
|
||||
for (left, right) in zip(lhs, rhs) where left != right {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,9 +36,15 @@ extension PureXML.Schema {
|
||||
}
|
||||
// P < Q only when s+P < s+Q at every reference; > likewise; equal
|
||||
// when all four coincide (which holds iff the components are equal).
|
||||
if sawLess, !sawGreater, !sawEqual { return .lessThan }
|
||||
if sawGreater, !sawLess, !sawEqual { return .greaterThan }
|
||||
if sawEqual, !sawLess, !sawGreater { return .equal }
|
||||
if sawLess, !sawGreater, !sawEqual {
|
||||
return .lessThan
|
||||
}
|
||||
if sawGreater, !sawLess, !sawEqual {
|
||||
return .greaterThan
|
||||
}
|
||||
if sawEqual, !sawLess, !sawGreater {
|
||||
return .equal
|
||||
}
|
||||
return .incomparable
|
||||
}
|
||||
|
||||
|
||||
@ -35,19 +35,25 @@ extension PureXML.Schema {
|
||||
extension PureXML.Schema.SimpleType {
|
||||
/// Whether this is `xs:ID` (or an atomic restriction of it).
|
||||
var isID: Bool {
|
||||
if case .atomic = variety { return base == .id }
|
||||
if case .atomic = variety {
|
||||
return base == .id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Whether this is `xs:IDREF` (or an atomic restriction of it).
|
||||
var isIDReference: Bool {
|
||||
if case .atomic = variety { return base == .idref }
|
||||
if case .atomic = variety {
|
||||
return base == .idref
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Whether this is `xs:IDREFS` (a list whose item type is `xs:IDREF`).
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,9 @@ extension PureXML.Schema.XSDParser {
|
||||
var current: XSDTree? = start
|
||||
for rawStep in path.split(separator: "/", omittingEmptySubsequences: false) {
|
||||
let step = String(rawStep).trimmingXMLWhitespace()
|
||||
if step == "." { continue }
|
||||
if step == "." {
|
||||
continue
|
||||
}
|
||||
guard isSimpleNameTest(step), let node = current,
|
||||
let child = directChildElement(named: PureXML.Schema.XSDNode.stripPrefix(step), of: node)
|
||||
else { return nil }
|
||||
@ -92,7 +94,9 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
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") {
|
||||
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),
|
||||
case let .complex(fieldComplex) = fieldType
|
||||
else { return false }
|
||||
if case .simpleContent = fieldComplex.content { return false }
|
||||
if case .simpleContent = fieldComplex.content {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -49,7 +49,9 @@ struct TupleSet {
|
||||
key.reserveCapacity(tuple.count)
|
||||
for field in tuple {
|
||||
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)
|
||||
}
|
||||
return key
|
||||
|
||||
@ -49,11 +49,22 @@ extension PureXML.Schema {
|
||||
_ root: PureXML.Model.TreeNode,
|
||||
at path: [PureXML.Validation.PathKey] = [],
|
||||
) -> [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 [] }
|
||||
// A selector or field XPath that does not compile is a schema-author
|
||||
// error, reported once up front rather than silently disabling the
|
||||
// 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)
|
||||
return issues
|
||||
}
|
||||
@ -61,15 +72,19 @@ extension PureXML.Schema {
|
||||
/// One located error per constraint XPath that fails to compile, in a
|
||||
/// deterministic order. The failed compilations are cached, so the walk
|
||||
/// afterwards treats those constraints as matching nothing.
|
||||
private func compileErrors(at path: [PureXML.Validation.PathKey]) -> [PureXML.Validation.ValidationError] {
|
||||
var errors: [PureXML.Validation.ValidationError] = []
|
||||
private func compileErrors(at path: [PureXML.Validation.PathKey]) -> [XSDFinding] {
|
||||
var errors: [XSDFinding] = []
|
||||
for key in constraints.keys.sorted() {
|
||||
for constraint in constraints[key] ?? [] {
|
||||
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 {
|
||||
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,
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
scopes: [[String: [[FieldValue?]]]],
|
||||
into issues: inout [PureXML.Validation.ValidationError],
|
||||
into issues: inout [XSDFinding],
|
||||
) {
|
||||
var frame: [String: [[FieldValue?]]] = [:]
|
||||
let declared = node.name.flatMap { constraints[$0.localName] } ?? []
|
||||
@ -132,7 +147,7 @@ extension PureXML.Schema {
|
||||
at node: PureXML.Model.TreeNode,
|
||||
path: [PureXML.Validation.PathKey],
|
||||
frame: inout [String: [[FieldValue?]]],
|
||||
into issues: inout [PureXML.Validation.ValidationError],
|
||||
into issues: inout [XSDFinding],
|
||||
) {
|
||||
guard isNonRef(constraint) else { return }
|
||||
var tuples: [[FieldValue?]] = []
|
||||
@ -145,15 +160,19 @@ extension PureXML.Schema {
|
||||
}
|
||||
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
|
||||
if constraint.kind == .key, tuple.contains(where: { $0 == nil }) {
|
||||
issues.append(.init(
|
||||
issues.append(XSDFinding(
|
||||
.identityKeyFieldPresent,
|
||||
reason: "key '\(constraint.name)': a field is missing",
|
||||
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
|
||||
))
|
||||
continue
|
||||
}
|
||||
if tuple.contains(where: { $0 == nil }) { continue }
|
||||
if tuple.contains(where: { $0 == nil }) {
|
||||
continue
|
||||
}
|
||||
if seen.contains(tuple) {
|
||||
issues.append(.init(
|
||||
issues.append(XSDFinding(
|
||||
.identityValueUnique,
|
||||
reason: "\(label(constraint)) '\(constraint.name)': duplicate value",
|
||||
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
|
||||
))
|
||||
@ -170,7 +189,7 @@ extension PureXML.Schema {
|
||||
at node: PureXML.Model.TreeNode,
|
||||
path: [PureXML.Validation.PathKey],
|
||||
scopes: [[String: [[FieldValue?]]]],
|
||||
into issues: inout [PureXML.Validation.ValidationError],
|
||||
into issues: inout [XSDFinding],
|
||||
) {
|
||||
guard case let .keyref(refer) = constraint.kind else { return }
|
||||
let keyTuples = scopes.reversed().compactMap { $0[refer] }.first ?? []
|
||||
@ -185,9 +204,12 @@ extension PureXML.Schema {
|
||||
continue
|
||||
}
|
||||
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) {
|
||||
issues.append(.init(
|
||||
issues.append(XSDFinding(
|
||||
.identityKeyrefResolves,
|
||||
reason: "keyref '\(constraint.name)': no matching key '\(refer)'",
|
||||
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
|
||||
))
|
||||
@ -203,13 +225,14 @@ extension PureXML.Schema {
|
||||
_ constraint: IdentityConstraint,
|
||||
at target: PureXML.Model.TreeNode,
|
||||
path targetPath: [PureXML.Validation.PathKey],
|
||||
) -> PureXML.Validation.ValidationError? {
|
||||
) -> XSDFinding? {
|
||||
let step = fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings)
|
||||
for field in constraint.fields {
|
||||
let selected = select(field, at: target, namespaces: constraint.namespaceBindings)
|
||||
// cvc-identity-constraint.3: each field must evaluate to at most one node.
|
||||
if selected.count > 1 {
|
||||
return .init(
|
||||
return XSDFinding(
|
||||
.identityFieldCardinality,
|
||||
reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must select at most one node",
|
||||
at: targetPath + step,
|
||||
)
|
||||
@ -235,7 +258,8 @@ extension PureXML.Schema {
|
||||
&& Self.xsiTypeLocalName(of: node) == nil
|
||||
} ?? false
|
||||
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",
|
||||
at: targetPath + step,
|
||||
)
|
||||
@ -270,7 +294,9 @@ extension PureXML.Schema {
|
||||
if let defaulted = defaultedValue(field, value: value, type: type, constraint: constraint, at: node) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
func isFieldEmpty(_ field: String, value: PureXML.XPath.Value) -> Bool {
|
||||
guard let nodes = value.nodes else { return false }
|
||||
if field.hasPrefix("@") { return nodes.isEmpty }
|
||||
if field.hasPrefix("@") {
|
||||
return nodes.isEmpty
|
||||
}
|
||||
if field == "." {
|
||||
guard case let .tree(element)? = nodes.first, element.kind == .element else { return nodes.isEmpty }
|
||||
return !element.children.contains { $0.kind == .text || $0.kind == .cdata }
|
||||
@ -41,7 +43,9 @@ extension PureXML.Schema.IdentityValidator {
|
||||
) -> PureXML.Schema.ValueConstraint? {
|
||||
if field == ".", let local = target.name?.localName {
|
||||
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)]
|
||||
}
|
||||
|
||||
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,
|
||||
/// or the special values `INF`, `-INF`, `NaN`.
|
||||
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)
|
||||
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 altSplit = characters.split(separator: "E", omittingEmptySubsequences: false)
|
||||
let parts = exponentSplit.count == 2 ? exponentSplit : altSplit
|
||||
@ -31,7 +35,9 @@ extension PureXML.Schema {
|
||||
|
||||
private static func isSignedInteger(_ value: Substring) -> Bool {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -39,7 +45,9 @@ extension PureXML.Schema {
|
||||
/// least one component and no empty `T`.
|
||||
static func isDuration(_ value: String) -> Bool {
|
||||
var rest = Substring(value)
|
||||
if rest.first == "-" { rest = rest.dropFirst() }
|
||||
if rest.first == "-" {
|
||||
rest = rest.dropFirst()
|
||||
}
|
||||
guard rest.first == "P" else { return false }
|
||||
rest = rest.dropFirst()
|
||||
let timeSplit = rest.split(separator: "T", omittingEmptySubsequences: false)
|
||||
|
||||
@ -14,7 +14,9 @@ public extension PureXML.Schema {
|
||||
|
||||
public init?(lexical value: String) {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
@ -32,14 +34,18 @@ public extension PureXML.Schema {
|
||||
|
||||
func clamped(to limit: Int) -> Int {
|
||||
precondition(limit >= 0, "occurrence clamp limit must be non-negative")
|
||||
if isGreaterThan(limit) { return limit }
|
||||
if isGreaterThan(limit) {
|
||||
return limit
|
||||
}
|
||||
return Int(digits) ?? limit
|
||||
}
|
||||
|
||||
func isGreaterThan(_ value: Int) -> Bool {
|
||||
precondition(value >= 0, "occurrence comparison value must be non-negative")
|
||||
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
|
||||
}
|
||||
|
||||
@ -69,7 +75,9 @@ public extension PureXML.Schema {
|
||||
}
|
||||
|
||||
public var isZero: Bool {
|
||||
if case let .finite(value) = self { return value.isZero }
|
||||
if case let .finite(value) = self {
|
||||
return value.isZero
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,9 @@ extension PureXML.Schema.Particle {
|
||||
var members: [Self] = []
|
||||
for child in group.particles {
|
||||
let normalized = child.normalized()
|
||||
if normalized.maxOccurs == 0 { continue }
|
||||
if normalized.maxOccurs == 0 {
|
||||
continue
|
||||
}
|
||||
let splice = normalized.minOccurs == 1 && normalized.maxOccurs == 1
|
||||
if case let .group(inner) = normalized.term, inner.compositor == group.compositor, splice {
|
||||
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
|
||||
/// (an empty group included). Its only legal content is the empty sequence.
|
||||
static func contentFree(_ particle: Particle) -> Bool {
|
||||
if particle.maxOccurs == 0 { return true }
|
||||
if particle.maxOccurs == 0 {
|
||||
return true
|
||||
}
|
||||
switch particle.term {
|
||||
case .element, .wildcard:
|
||||
return false
|
||||
@ -216,13 +218,19 @@ extension PureXML.Schema {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
if skippedMustBeEmptiable, !emptiable(candidate) { return false }
|
||||
if skippedMustBeEmptiable, !emptiable(candidate) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
if !matched { return false }
|
||||
}
|
||||
if skippedMustBeEmptiable {
|
||||
while baseIndex < base.count {
|
||||
if !emptiable(base[baseIndex]) { return false }
|
||||
if !emptiable(base[baseIndex]) {
|
||||
return false
|
||||
}
|
||||
baseIndex += 1
|
||||
}
|
||||
}
|
||||
@ -253,7 +261,9 @@ extension PureXML.Schema {
|
||||
|
||||
/// Whether a particle can match no children at all.
|
||||
static func emptiable(_ particle: Particle) -> Bool {
|
||||
if particle.minOccurs == 0 { return true }
|
||||
if particle.minOccurs == 0 {
|
||||
return true
|
||||
}
|
||||
switch particle.term {
|
||||
case .element, .wildcard:
|
||||
return false
|
||||
|
||||
@ -50,7 +50,9 @@ extension PureXML.Schema.ParticleRestriction {
|
||||
// declared union whose members merely subset the base union's is
|
||||
// NOT a derivation (particlesIk011); structural member subsetting
|
||||
// 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) }
|
||||
}
|
||||
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
|
||||
// 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
|
||||
@ -79,7 +83,9 @@ extension PureXML.Schema.ParticleRestriction {
|
||||
while let resolved = current, steps <= types.count {
|
||||
switch resolved {
|
||||
case let .simple(simple):
|
||||
if case .union = simple.variety { return true }
|
||||
if case .union = simple.variety {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case .complex:
|
||||
return false
|
||||
@ -112,11 +118,15 @@ extension PureXML.Schema.ParticleRestriction {
|
||||
_ derivation: [String: PureXML.Schema.TypeDerivation],
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> Bool {
|
||||
if isUrTypeName(base, types) { return true }
|
||||
if isUrTypeName(base, types) {
|
||||
return true
|
||||
}
|
||||
var current = derived
|
||||
var visited: Set<String> = []
|
||||
while visited.insert(current).inserted {
|
||||
if current == base { return true }
|
||||
if current == base {
|
||||
return true
|
||||
}
|
||||
guard let step = derivation[current] else { break }
|
||||
current = step.base
|
||||
}
|
||||
@ -139,11 +149,15 @@ extension PureXML.Schema.ParticleRestriction {
|
||||
_ derivation: [String: PureXML.Schema.TypeDerivation],
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> Bool {
|
||||
if isUrTypeName(base, types) { return true }
|
||||
if isUrTypeName(base, types) {
|
||||
return true
|
||||
}
|
||||
var current = derived
|
||||
var visited: Set<String> = []
|
||||
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 }
|
||||
current = step.base
|
||||
}
|
||||
|
||||
@ -58,7 +58,9 @@ extension RNCParser {
|
||||
private func qualifiedName(_ word: String, attribute: Bool) -> NameClass {
|
||||
let parts = word.split(separator: ":", maxSplits: 1).map(String.init)
|
||||
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: attribute ? "" : defaultNamespace, localName: word)
|
||||
@ -78,7 +80,8 @@ extension RNCParser {
|
||||
}
|
||||
|
||||
func wordValue() -> String {
|
||||
if case let .word(word) = peek() { advance()
|
||||
if case let .word(word) = peek() {
|
||||
advance()
|
||||
return word
|
||||
}
|
||||
return ""
|
||||
@ -93,11 +96,14 @@ extension RNCParser {
|
||||
}
|
||||
|
||||
func expectSymbol(_ symbol: String) {
|
||||
if peek() == .symbol(symbol) { advance() }
|
||||
if peek() == .symbol(symbol) {
|
||||
advance()
|
||||
}
|
||||
}
|
||||
|
||||
func expectString() -> String {
|
||||
if case let .string(value) = peek() { advance()
|
||||
if case let .string(value) = peek() {
|
||||
advance()
|
||||
return concatenated(value)
|
||||
}
|
||||
return ""
|
||||
|
||||
@ -50,24 +50,33 @@ extension RNCParser {
|
||||
|
||||
private func parseDefault() {
|
||||
advance() // default
|
||||
if case .word("namespace") = peek() { parseNamespace(isDefault: true) }
|
||||
if case .word("namespace") = peek() {
|
||||
parseNamespace(isDefault: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func parseNamespace(isDefault: Bool) {
|
||||
advance() // namespace
|
||||
var prefix = ""
|
||||
if case let .word(name) = peek(), name != "=" { prefix = name
|
||||
if case let .word(name) = peek(), name != "=" {
|
||||
prefix = name
|
||||
advance()
|
||||
}
|
||||
expectSymbol("=")
|
||||
let uri = expectString()
|
||||
if isDefault { defaultNamespace = uri }
|
||||
if !prefix.isEmpty { namespaces[prefix] = uri }
|
||||
if isDefault {
|
||||
defaultNamespace = uri
|
||||
}
|
||||
if !prefix.isEmpty {
|
||||
namespaces[prefix] = uri
|
||||
}
|
||||
}
|
||||
|
||||
private func parseDatatypes() {
|
||||
advance() // datatypes
|
||||
if case .word = peek() { advance() }
|
||||
if case .word = peek() {
|
||||
advance()
|
||||
}
|
||||
expectSymbol("=")
|
||||
_ = expectString()
|
||||
}
|
||||
@ -76,9 +85,13 @@ extension RNCParser {
|
||||
|
||||
private func looksLikeGrammar() -> Bool {
|
||||
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 =, |=, &=.
|
||||
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
|
||||
}
|
||||
|
||||
@ -118,7 +131,11 @@ extension RNCParser {
|
||||
var depth = 1
|
||||
while position < tokens.count, depth > 0 {
|
||||
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 {
|
||||
defines[name] = pattern
|
||||
}
|
||||
if startPattern == nil { startPattern = sub.startPattern }
|
||||
if startPattern == nil {
|
||||
startPattern = sub.startPattern
|
||||
}
|
||||
visited = sub.visited
|
||||
}
|
||||
// An override block re-defines names after the include, overriding them.
|
||||
@ -212,7 +231,9 @@ extension RNCParser {
|
||||
while peek() == .symbol(">"), peek(1) == .symbol(">") {
|
||||
advance()
|
||||
advance()
|
||||
if case .word = peek() { advance() }
|
||||
if case .word = peek() {
|
||||
advance()
|
||||
}
|
||||
skipAnnotation()
|
||||
}
|
||||
}
|
||||
@ -281,8 +302,11 @@ extension RNCParser {
|
||||
guard word.contains(":") else { return .ref(word) }
|
||||
let type = PureXML.Schema.BuiltinType(rawValue: strip(word)) ?? .string
|
||||
var facets = PureXML.Schema.Facets()
|
||||
if peek() == .symbol("{") { parseParams(into: &facets) }
|
||||
if case let .string(value) = peek() { advance()
|
||||
if peek() == .symbol("{") {
|
||||
parseParams(into: &facets)
|
||||
}
|
||||
if case let .string(value) = peek() {
|
||||
advance()
|
||||
return .value(PureXML.Schema.SimpleType(base: type), concatenated(value))
|
||||
}
|
||||
return .data(PureXML.Schema.SimpleType(base: type, facets: facets))
|
||||
@ -297,12 +321,16 @@ extension RNCParser {
|
||||
continue
|
||||
}
|
||||
advance()
|
||||
if peek() == .symbol("=") { advance() }
|
||||
if peek() == .symbol("=") {
|
||||
advance()
|
||||
}
|
||||
guard case let .string(value) = peek() else { continue }
|
||||
advance()
|
||||
PureXML.Schema.RelaxNGFacets.apply(name, value, into: &facets)
|
||||
}
|
||||
if peek() == .symbol("}") { advance() }
|
||||
if peek() == .symbol("}") {
|
||||
advance()
|
||||
}
|
||||
}
|
||||
|
||||
private func parseElement() -> Pattern {
|
||||
|
||||
@ -47,30 +47,54 @@ extension PureXML.Schema {
|
||||
// MARK: Simplifying constructors
|
||||
|
||||
func choice(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
if case .notAllowed = lhs { return rhs }
|
||||
if case .notAllowed = rhs { return lhs }
|
||||
if case .notAllowed = lhs {
|
||||
return rhs
|
||||
}
|
||||
if case .notAllowed = rhs {
|
||||
return lhs
|
||||
}
|
||||
return .choice(lhs, rhs)
|
||||
}
|
||||
|
||||
private func group(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
if case .notAllowed = lhs { return .notAllowed }
|
||||
if case .notAllowed = rhs { return .notAllowed }
|
||||
if case .empty = lhs { return rhs }
|
||||
if case .empty = rhs { return lhs }
|
||||
if case .notAllowed = lhs {
|
||||
return .notAllowed
|
||||
}
|
||||
if case .notAllowed = rhs {
|
||||
return .notAllowed
|
||||
}
|
||||
if case .empty = lhs {
|
||||
return rhs
|
||||
}
|
||||
if case .empty = rhs {
|
||||
return lhs
|
||||
}
|
||||
return .group(lhs, rhs)
|
||||
}
|
||||
|
||||
private func interleave(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
if case .notAllowed = lhs { return .notAllowed }
|
||||
if case .notAllowed = rhs { return .notAllowed }
|
||||
if case .empty = lhs { return rhs }
|
||||
if case .empty = rhs { return lhs }
|
||||
if case .notAllowed = lhs {
|
||||
return .notAllowed
|
||||
}
|
||||
if case .notAllowed = rhs {
|
||||
return .notAllowed
|
||||
}
|
||||
if case .empty = lhs {
|
||||
return rhs
|
||||
}
|
||||
if case .empty = rhs {
|
||||
return lhs
|
||||
}
|
||||
return .interleave(lhs, rhs)
|
||||
}
|
||||
|
||||
private func after(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
if case .notAllowed = lhs { return .notAllowed }
|
||||
if case .notAllowed = rhs { return .notAllowed }
|
||||
if case .notAllowed = lhs {
|
||||
return .notAllowed
|
||||
}
|
||||
if case .notAllowed = rhs {
|
||||
return .notAllowed
|
||||
}
|
||||
return .after(lhs, rhs)
|
||||
}
|
||||
|
||||
@ -155,7 +179,9 @@ extension PureXML.Schema {
|
||||
}
|
||||
|
||||
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
|
||||
// matches the empty sequence, the same leniency element content
|
||||
// gets for ignorable whitespace.
|
||||
@ -163,7 +189,9 @@ extension PureXML.Schema {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -244,7 +272,9 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
let coalesced = Self.coalesceText(children)
|
||||
// Clark's algorithm treats an empty element as optional empty text, so
|
||||
// a data/value pattern whose lexical space admits "" matches <e/>.
|
||||
if coalesced.isEmpty { return choice(pattern, textDeriv(pattern, "", env: env)) }
|
||||
if coalesced.isEmpty {
|
||||
return choice(pattern, textDeriv(pattern, "", env: env))
|
||||
}
|
||||
if coalesced.count == 1, case let .text(string) = coalesced[0] {
|
||||
let derived = textDeriv(pattern, string, env: env)
|
||||
return Self.isWhitespace(string) ? choice(pattern, derived) : derived
|
||||
@ -276,13 +306,16 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
case .comment, .processingInstruction:
|
||||
continue
|
||||
default:
|
||||
if !run.isEmpty { result.append(.text(run))
|
||||
if !run.isEmpty {
|
||||
result.append(.text(run))
|
||||
run = ""
|
||||
}
|
||||
result.append(child)
|
||||
}
|
||||
}
|
||||
if !run.isEmpty { result.append(.text(run)) }
|
||||
if !run.isEmpty {
|
||||
result.append(.text(run))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -291,7 +324,9 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -341,7 +376,9 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
switch event {
|
||||
case let .startElement(name, attributes):
|
||||
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
|
||||
for attribute in attributes where Self.isNamespaceDeclaration(attribute) {
|
||||
env[attribute.name.prefix == "xmlns" ? attribute.name.localName : ""] = attribute.value
|
||||
@ -353,11 +390,15 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
state.current = startTagCloseDeriv(attributed)
|
||||
state.frames.append(RelaxNGStreamFrame(env: env))
|
||||
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:
|
||||
flushText(&state, closing: true)
|
||||
state.current = endTagDeriv(state.current)
|
||||
if !state.frames.isEmpty { state.frames.removeLast() }
|
||||
if !state.frames.isEmpty {
|
||||
state.frames.removeLast()
|
||||
}
|
||||
case .comment, .processingInstruction:
|
||||
break
|
||||
}
|
||||
|
||||
@ -18,12 +18,16 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
private func isNotAllowed(_ pattern: Pattern) -> Bool {
|
||||
if case .notAllowed = pattern { return true }
|
||||
if case .notAllowed = pattern {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -46,7 +50,9 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
if isNotAllowed(closed) {
|
||||
// A genuine missing/extra attribute, unless an attribute error above
|
||||
// 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
|
||||
}
|
||||
// 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? {
|
||||
guard let name = RNGNode.localName(node) else { return nil }
|
||||
if let problem = attributeProblem(node, name: name) { return problem }
|
||||
if let problem = textProblem(node, name: name) { return problem }
|
||||
if let problem = attributeProblem(node, name: name) {
|
||||
return problem
|
||||
}
|
||||
if let problem = textProblem(node, name: name) {
|
||||
return problem
|
||||
}
|
||||
switch context {
|
||||
case .pattern: return checkPattern(node, name: name)
|
||||
case .nameClass: return checkNameClass(node, name: name)
|
||||
@ -102,7 +106,9 @@ enum RelaxNGGrammarCheck {
|
||||
}
|
||||
} else {
|
||||
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()
|
||||
}
|
||||
if name == "element", content.isEmpty {
|
||||
@ -160,14 +166,20 @@ enum RelaxNGGrammarCheck {
|
||||
for child in children {
|
||||
switch RNGNode.localName(child) {
|
||||
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" }
|
||||
case "except":
|
||||
if sawExcept { return "<data> takes at most one <except>" }
|
||||
if sawExcept {
|
||||
return "<data> takes at most one <except>"
|
||||
}
|
||||
sawExcept = true
|
||||
let members = RNGNode.elementChildren(child)
|
||||
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:
|
||||
return "<\(RNGNode.localName(child) ?? "?")> is not allowed in <data>"
|
||||
}
|
||||
|
||||
@ -34,7 +34,9 @@ extension RelaxNGGrammarCheck {
|
||||
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 }
|
||||
let members = RNGNode.elementChildren(except)
|
||||
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
|
||||
/// one pattern; both may carry a combine method.
|
||||
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" {
|
||||
guard children.count == 1 else { return "<start> takes exactly one pattern" }
|
||||
return check(children[0], in: .pattern)
|
||||
@ -82,7 +86,9 @@ extension RelaxNGGrammarCheck {
|
||||
|
||||
static func firstViolation(_ nodes: [RNGTree], in context: Context) -> String? {
|
||||
for node in nodes {
|
||||
if let problem = check(node, in: context) { return problem }
|
||||
if let problem = check(node, in: context) {
|
||||
return problem
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -120,7 +126,9 @@ extension RelaxNGGrammarCheck {
|
||||
/// of ALPHA *(ALPHA / DIGIT / "+" / "-" / "."), or the empty string for
|
||||
/// the built-in library.
|
||||
static func isDatatypeLibraryURI(_ value: String) -> Bool {
|
||||
if value.isEmpty { return true }
|
||||
if value.isEmpty {
|
||||
return true
|
||||
}
|
||||
guard !value.contains("#") else { return false }
|
||||
// A percent must introduce a two-digit hex escape (RFC 2396).
|
||||
var cursor = value.startIndex
|
||||
|
||||
@ -27,7 +27,9 @@ enum RNGNode {
|
||||
// only when the whole document is unqualified (leniency for
|
||||
// namespace-less schemas); under a qualified parent it is foreign.
|
||||
let uri = child.name?.namespaceURI
|
||||
if uri == relaxNGNamespace { return true }
|
||||
if uri == relaxNGNamespace {
|
||||
return true
|
||||
}
|
||||
return uri == nil && node.name?.namespaceURI == nil
|
||||
}
|
||||
}
|
||||
@ -84,7 +86,9 @@ enum RNGNode {
|
||||
guard let colon = raw.firstIndex(of: ":") else { return nil }
|
||||
let prefix = String(raw[..<colon])
|
||||
let local = String(raw[raw.index(after: colon)...])
|
||||
if prefix == "xml" { return ("http://www.w3.org/XML/1998/namespace", local) }
|
||||
if prefix == "xml" {
|
||||
return ("http://www.w3.org/XML/1998/namespace", local)
|
||||
}
|
||||
var current: RNGTree? = node
|
||||
while let candidate = current {
|
||||
if let uri = candidate.attributes.first(where: { $0.name.prefix == "xmlns" && $0.name.localName == prefix })?.value {
|
||||
@ -120,7 +124,9 @@ enum RNGNode {
|
||||
}
|
||||
cursor = current.parent
|
||||
}
|
||||
if let documentBase { bases.append(documentBase) }
|
||||
if let documentBase {
|
||||
bases.append(documentBase)
|
||||
}
|
||||
var resolved = href
|
||||
for base in bases {
|
||||
resolved = resolveRelative(resolved, against: base)
|
||||
|
||||
@ -106,7 +106,9 @@ final class RNGCompiler {
|
||||
// and the single start that omits `combine` still merges by it.
|
||||
var method: String?
|
||||
for start in starts {
|
||||
if let named = RNGNode.attribute(start, "combine") { method = named }
|
||||
if let named = RNGNode.attribute(start, "combine") {
|
||||
method = named
|
||||
}
|
||||
}
|
||||
var result = combined(RNGNode.elementChildren(first), .sequence)
|
||||
for start in starts.dropFirst() {
|
||||
@ -262,7 +264,9 @@ final class RNGCompiler {
|
||||
private func datatypeLibrary(of node: Tree) -> String {
|
||||
var current: Tree? = node
|
||||
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
|
||||
}
|
||||
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