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:
Mihaela Mihaljevic 2026-07-12 14:12:17 +02:00
parent 36f1485f2e
commit 380160cc2b
247 changed files with 4424 additions and 1197 deletions

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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:

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 == "?"
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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 += "&amp;" }
if previousWasAmpersand {
result += "&amp;"
}
return result
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)])

View File

@ -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)

View File

@ -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 }

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 == ">" {

View File

@ -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()
}

View File

@ -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 {

View File

@ -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]))
}

View File

@ -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}")
}

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}
}
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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))
}
}

View File

@ -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

View 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)]
}
}

View File

@ -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 {

View 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
}
}

View File

@ -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

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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

View File

@ -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"
}

View File

@ -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
}

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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)]
}

View 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
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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 ""

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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>"
}

View File

@ -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

View File

@ -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)

View File

@ -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