PureXML/Sources/Model/QualifiedName.swift
Mihaela Mihaljevic 43f82312e6 feat(parser): resolve XML namespaces at the start-element boundary
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.
2026-06-08 01:03:29 +02:00

52 lines
2.0 KiB
Swift

public extension PureXML.Model {
/// An optionally namespace-prefixed XML name such as `xs:element`.
struct QualifiedName: Equatable, Hashable, Sendable, CustomStringConvertible {
/// Namespace prefix preceding the colon, when present (`xs` in `xs:element`).
public var prefix: String?
/// The local part of the name (`element` in `xs:element`).
public var localName: String
/// The resolved namespace URI, or nil when the name is in no namespace or
/// has not been namespace-resolved. Populated by the parser from in-scope
/// `xmlns` declarations; nil for names built directly.
public var namespaceURI: String?
public init(prefix: String? = nil, localName: String, namespaceURI: String? = nil) {
self.prefix = prefix
self.localName = localName
self.namespaceURI = namespaceURI
}
/// Returns a copy of this name carrying the given resolved namespace URI.
public func resolved(namespaceURI: String?) -> QualifiedName {
QualifiedName(prefix: prefix, localName: localName, namespaceURI: namespaceURI)
}
/// Parses a raw `prefix:local` or bare `local` name.
public init(_ raw: String) {
namespaceURI = nil
if let colon = raw.firstIndex(of: ":") {
let head = String(raw[raw.startIndex ..< colon])
let tail = String(raw[raw.index(after: colon)...])
if head.isEmpty || tail.isEmpty {
prefix = nil
localName = raw
} else {
prefix = head
localName = tail
}
} else {
prefix = nil
localName = raw
}
}
/// The full `prefix:local` rendering used in serialized output.
public var description: String {
guard let prefix else { return localName }
return "\(prefix):\(localName)"
}
}
}