PureXML/Sources/Schema/RelaxNGNameClasses.swift
Mihaela Mihaljevic b8fa90ccf5 feat(relaxng): ns inheritance, QNames, include-start, combine, data-except
The compiler implements simplification 4.9-4.10 (ns inheritance across
ancestors and document boundaries; QName resolution against in-scope
xmlns declarations; the attribute name= shorthand stays namespace-less),
include brings in the included grammar's start with overrides (4.7),
multiple starts and defines combine with a single omitted combine
merging by the named method (4.17), data/except lands as a dataExcept
pattern in the derivative engine (4.12), an empty element matches data
whose lexical space admits the empty string, and whitespace-only
attribute values match empty-matching patterns. Spec suite: valid
274/289, invalid 279/291.
2026-06-10 10:24:46 +02:00

93 lines
3.8 KiB
Swift

/// The name-class side of the RELAX NG compiler (4.9-4.10: ns inheritance and
/// QName resolution), split from the compiler body to keep it under the
/// length caps.
extension RNGCompiler {
typealias Tree = RNGTree
func nameClassAndContent(_ node: Tree) -> (NameClass, [Tree]) {
let children = RNGNode.elementChildren(node)
if let name = RNGNode.attribute(node, "name") {
if let qualified = RNGNode.resolveQName(name, at: node) {
return (.name(namespace: qualified.namespace, localName: qualified.localName), children)
}
// The `name=` shorthand on an attribute does not inherit ns: an
// unprefixed attribute name has no namespace unless the attribute
// element itself carries ns (4.9). Elements inherit normally.
let isAttribute = RNGNode.localName(node) == "attribute"
let namespace = isAttribute
? (node.attributes.first { $0.name.prefix == nil && $0.name.localName == "ns" }?.value ?? "")
: effectiveNS(node)
return (.name(namespace: namespace, localName: name), children)
}
guard let first = children.first else { return (.anyName, []) }
return (nameClass(first), Array(children.dropFirst()))
}
// MARK: Combinators and name classes
func combined(_ nodes: [Tree], _ compositor: Compositor) -> Pattern {
let patterns = nodes.map(pattern)
let identity: Pattern = compositor == .choice ? .notAllowed : .empty
guard let first = patterns.first else { return identity }
return patterns.dropFirst().reduce(first) { combine($0, $1, compositor) }
}
func combine(_ lhs: Pattern, _ rhs: Pattern, _ compositor: Compositor) -> Pattern {
switch compositor {
case .sequence: .group(lhs, rhs)
case .choice: .choice(lhs, rhs)
case .all: .interleave(lhs, rhs)
}
}
func nameClass(_ node: Tree) -> NameClass {
switch RNGNode.localName(node) {
case "name":
nameElementClass(node)
case "anyName":
anyName(node)
case "nsName":
nsName(node)
case "choice":
nameClassChoice(RNGNode.elementChildren(node))
default:
.anyName
}
}
func anyName(_ node: Tree) -> NameClass {
guard let except = exceptClass(node) else { return .anyName }
return .anyNameExcept(except)
}
/// A `<name>` element: its text may be a QName resolved against in-scope
/// xmlns declarations (4.10); otherwise the inherited ns applies (4.9).
func nameElementClass(_ node: Tree) -> NameClass {
let raw = RNGNode.text(node)
if let qualified = RNGNode.resolveQName(raw, at: node) {
return .name(namespace: qualified.namespace, localName: qualified.localName)
}
return .name(namespace: effectiveNS(node), localName: raw)
}
func nsName(_ node: Tree) -> NameClass {
let namespace = effectiveNS(node)
guard let except = exceptClass(node) else { return .nsName(namespace) }
return .nsNameExcept(namespace: namespace, except: except)
}
/// The name class of a `<name-class>`'s `<except>` child, treating multiple
/// children as an implicit choice (`anyName`/`nsName` minus the union).
func exceptClass(_ node: Tree) -> NameClass? {
guard let except = RNGNode.children(node, named: "except").first else { return nil }
let children = RNGNode.elementChildren(except)
return children.isEmpty ? nil : nameClassChoice(children)
}
func nameClassChoice(_ nodes: [Tree]) -> NameClass {
let classes = nodes.map(nameClass)
guard let first = classes.first else { return .anyName }
return classes.dropFirst().reduce(first) { .choice($0, $1) }
}
}