Add namespace resolution following the XML Namespaces rules, reimplemented in Swift from libxml2's SAX2 model; no upstream code copied. - QualifiedName gains an optional resolved namespaceURI (nil for names built directly or in no namespace), with a resolved(namespaceURI:) helper. - NamespaceContext maintains a scope stack of xmlns bindings as elements open and close, resolving element and attribute names against it. - A default namespace applies to unprefixed element names but never to attributes; the xml prefix is built in; xmlns declarations are preserved as attributes so documents round-trip; an unbound prefix raises ParseError.undefinedNamespacePrefix. End-tag matching stays lexical. - Namespace-free documents are unchanged (URIs remain nil). - 7 namespace tests; 39 total.
88 lines
3.5 KiB
Swift
88 lines
3.5 KiB
Swift
extension PureXML.Parsing {
|
|
/// Tracks in-scope namespace bindings as elements open and close, resolving
|
|
/// qualified names to their namespace URI. The reserved `xml` and `xmlns`
|
|
/// prefixes are built in. Internal to the parser.
|
|
///
|
|
/// Follows the XML Namespaces rules: a default namespace (`xmlns="..."`)
|
|
/// applies to unprefixed element names but never to attribute names, an empty
|
|
/// declaration undeclares a binding, and an unbound prefix is an error.
|
|
struct NamespaceContext {
|
|
/// The reserved binding for the `xml` prefix.
|
|
static let xmlNamespaceURI = "http://www.w3.org/XML/1998/namespace"
|
|
|
|
/// A stack of scopes; each maps a prefix ("" = default namespace) to a URI.
|
|
private var scopes: [[String: String]] = []
|
|
|
|
/// Opens an element: pushes a scope built from its `xmlns` declarations,
|
|
/// then resolves the element name and its attribute names against the
|
|
/// now-in-scope bindings.
|
|
mutating func enterElement(
|
|
name: PureXML.Model.QualifiedName,
|
|
attributes: [PureXML.Model.Attribute],
|
|
at mark: Mark,
|
|
) throws -> (name: PureXML.Model.QualifiedName, attributes: [PureXML.Model.Attribute]) {
|
|
var scope: [String: String] = [:]
|
|
for attribute in attributes {
|
|
if attribute.name.prefix == nil, attribute.name.localName == "xmlns" {
|
|
scope[""] = attribute.value
|
|
} else if attribute.name.prefix == "xmlns" {
|
|
scope[attribute.name.localName] = attribute.value
|
|
}
|
|
}
|
|
scopes.append(scope)
|
|
|
|
let resolvedName = try resolveElement(name, at: mark)
|
|
let resolvedAttributes = try attributes.map { try resolveAttribute($0, at: mark) }
|
|
return (resolvedName, resolvedAttributes)
|
|
}
|
|
|
|
/// Closes an element, popping its scope.
|
|
mutating func leaveElement() {
|
|
if !scopes.isEmpty {
|
|
scopes.removeLast()
|
|
}
|
|
}
|
|
|
|
private func resolveElement(
|
|
_ name: PureXML.Model.QualifiedName,
|
|
at mark: Mark,
|
|
) throws -> PureXML.Model.QualifiedName {
|
|
if name.prefix == nil {
|
|
return name.resolved(namespaceURI: lookup(""))
|
|
}
|
|
return try name.resolved(namespaceURI: requireURI(forPrefix: name.prefix, at: mark))
|
|
}
|
|
|
|
private func resolveAttribute(
|
|
_ attribute: PureXML.Model.Attribute,
|
|
at mark: Mark,
|
|
) throws -> PureXML.Model.Attribute {
|
|
let name = attribute.name
|
|
if name.prefix == nil || name.prefix == "xmlns" {
|
|
return attribute
|
|
}
|
|
let uri = try requireURI(forPrefix: name.prefix, at: mark)
|
|
return PureXML.Model.Attribute(name: name.resolved(namespaceURI: uri), value: attribute.value)
|
|
}
|
|
|
|
private func requireURI(forPrefix prefix: String?, at mark: Mark) throws -> String {
|
|
if prefix == "xml" {
|
|
return Self.xmlNamespaceURI
|
|
}
|
|
if let uri = lookup(prefix ?? ""), !uri.isEmpty {
|
|
return uri
|
|
}
|
|
throw ParseError.undefinedNamespacePrefix(prefix: prefix ?? "", mark)
|
|
}
|
|
|
|
private func lookup(_ key: String) -> String? {
|
|
for scope in scopes.reversed() {
|
|
if let uri = scope[key] {
|
|
return uri.isEmpty ? nil : uri
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|