PureXML/Sources/Schema/XSDSimpleParser.swift
Mihaela Mihaljevic b01499743c feat(schema): XSD wildcards and list built-in datatypes
Add xs:any and xs:anyAttribute wildcards with namespace and
processContents constraints (skip/lax/strict), and resolve the
list built-in datatypes (IDREFS, ENTITIES, NMTOKENS) to their
item types so each list item validates.

WildcardNamespace models ##any/##other/enumerated URI sets;
Wildcard.admits gates undeclared attributes and wildcard-matched
children. ContentNFA.follow exposes the follow set for completions.

Closes #77
2026-06-08 13:53:22 +02:00

112 lines
5.0 KiB
Swift

extension PureXML.Schema {
/// Parses XSD simple types: atomic restrictions with the full facet set, and
/// the `list` and `union` varieties. Kept beside ``XSDParser``, sharing its
/// module-scope tree helpers and parsing context.
enum XSDSimpleParser {
static func simpleType(_ node: XSDTree, _ context: XSDContext) -> SimpleType {
if let list = XSDNode.firstChild(node, named: "list") {
return listType(list, context)
}
if let union = XSDNode.firstChild(node, named: "union") {
return unionType(union, context)
}
guard let restriction = XSDNode.firstChild(node, named: "restriction") else {
return SimpleType(base: .string)
}
let baseName = XSDNode.stripPrefix(XSDNode.attribute(restriction, "base") ?? "string")
var base = BuiltinType.string
var facets = Facets()
var variety = Variety.atomic
if let builtin = BuiltinType(rawValue: baseName) {
base = builtin
} else if let parent = context.simpleTypes[baseName] {
base = parent.base
facets = parent.facets
variety = parent.variety
}
applyFacets(restriction, into: &facets)
return SimpleType(base: base, facets: facets, variety: variety)
}
static func simpleTypeReference(_ typeName: String, _ context: XSDContext) -> SimpleType {
let local = XSDNode.stripPrefix(typeName)
if let builtin = BuiltinType(rawValue: local) { return SimpleType(base: builtin) }
if let item = listBuiltinItem(local) { return .list(item: SimpleType(base: item)) }
return context.simpleTypes[local] ?? SimpleType(base: .string)
}
/// The item type of a built-in list datatype (`IDREFS`, `ENTITIES`,
/// `NMTOKENS`), each a whitespace-separated list of its singular form.
static func listBuiltinItem(_ name: String) -> BuiltinType? {
switch name {
case "IDREFS": .idref
case "ENTITIES": .entity
case "NMTOKENS": .nmtoken
default: nil
}
}
static func applyFacets(_ restriction: XSDTree, into facets: inout Facets) {
for facet in XSDNode.elementChildren(restriction) {
let value = XSDNode.attribute(facet, "value")
applyStringFacet(XSDNode.localName(facet), value, into: &facets)
applyNumericFacet(XSDNode.localName(facet), value, into: &facets)
}
}
private static func listType(_ node: XSDTree, _ context: XSDContext) -> SimpleType {
let item: SimpleType = if let itemType = XSDNode.attribute(node, "itemType") {
simpleTypeReference(itemType, context)
} else if let inline = XSDNode.firstChild(node, named: "simpleType") {
simpleType(inline, context)
} else {
SimpleType(base: .string)
}
return .list(item: item)
}
private static func unionType(_ node: XSDTree, _ context: XSDContext) -> SimpleType {
var members: [SimpleType] = []
if let names = XSDNode.attribute(node, "memberTypes") {
members += names.split(whereSeparator: \.isWhitespace).map { simpleTypeReference(String($0), context) }
}
members += XSDNode.children(node, named: "simpleType").map { simpleType($0, context) }
return .union(members)
}
private static func applyStringFacet(_ name: String?, _ value: String?, into facets: inout Facets) {
switch name {
case "pattern": if let value { facets.patterns.append(value) }
case "enumeration": if let value { facets.enumeration = (facets.enumeration ?? []) + [value] }
case "minInclusive": facets.minInclusive = value
case "maxInclusive": facets.maxInclusive = value
case "minExclusive": facets.minExclusive = value
case "maxExclusive": facets.maxExclusive = value
case "whiteSpace": facets.whiteSpace = whiteSpace(value)
default: break
}
}
private static func applyNumericFacet(_ name: String?, _ value: String?, into facets: inout Facets) {
let number = value.flatMap(Int.init)
switch name {
case "length": facets.length = number
case "minLength": facets.minLength = number
case "maxLength": facets.maxLength = number
case "totalDigits": facets.totalDigits = number
case "fractionDigits": facets.fractionDigits = number
default: break
}
}
private static func whiteSpace(_ value: String?) -> WhiteSpace? {
switch value {
case "preserve": .preserve
case "replace": .replace
case "collapse": .collapse
default: nil
}
}
}
}