QualifiedName(_:) ran firstIndex(of: ":") over the whole name, grapheme by grapheme, on every element and attribute. Most names have no prefix, so the common case scanned the entire name to find no colon at all. The byte scanner already visits every byte of an all-ASCII name, so have takeASCIIName record the offset of the first colon (one comparison per byte), and split at that known offset in QualifiedName(ascii:colonOffset:). The unprefixed name (no colon) is then built with no scan; a prefixed name splits at the recorded offset. The names are identical to the firstIndex path for every colon placement: none, leading (:local), trailing (prefix:), middle, doubled (::), and multiple (first colon wins). The hint is confined to the all-ASCII fast path; non-ASCII names and every other construction site keep QualifiedName(_:). Measured parse ~12% faster on the 20k-item generated corpus via a same-state A/B (8.5x to ~7.7x slower than libxml2). Verified by QualifiedNameColonHintTests (colon-placement differential), the W3C XML conformance corpora, the Xalan XSLT gold-output corpus, the full suite, the WASM build, and an adversarial equivalence review.
80 lines
3.3 KiB
Swift
80 lines
3.3 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. A malformed colon
|
|
/// placement (`:local`, `prefix:`, `::`) does not split: the whole string
|
|
/// is kept as an unprefixed local name rather than inventing an empty
|
|
/// prefix or local part.
|
|
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
|
|
}
|
|
}
|
|
|
|
/// Builds a name whose first colon is already known by byte offset (the
|
|
/// byte scanner found it while reading the name), so the prefix split
|
|
/// needs no second scan. `colonOffset` is nil for the common unprefixed
|
|
/// name. The split follows ``init(_:)`` exactly: a colon at the start or
|
|
/// end (`:local`, `prefix:`) does not split. The name is all-ASCII, so a
|
|
/// byte offset is a character offset.
|
|
init(ascii raw: String, colonOffset: Int?) {
|
|
namespaceURI = nil
|
|
guard let colonOffset else {
|
|
prefix = nil
|
|
localName = raw
|
|
return
|
|
}
|
|
let colon = raw.index(raw.startIndex, offsetBy: colonOffset)
|
|
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
|
|
}
|
|
}
|
|
|
|
/// The full `prefix:local` rendering used in serialized output.
|
|
public var description: String {
|
|
guard let prefix else { return localName }
|
|
return "\(prefix):\(localName)"
|
|
}
|
|
}
|
|
}
|