PureXML/Sources/Model/QualifiedName.swift
Mihaela Mihaljevic 7a5761b64d
perf(parse): split the name prefix from a scanned colon offset (#292)
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.
2026-06-25 12:23:53 +02:00

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)"
}
}
}