PureXML/Sources/Parsing/PseudoAttributeScanner.swift
Mihaela Mihaljevic 9168f990d6 feat(parsing): surface the XML declaration (version, encoding, standalone)
Parse the XML declaration's pseudo-attributes instead of skipping the
<?xml?> processing instruction:

- version, encoding, and standalone are surfaced on a new XMLDeclaration,
  returned from parseWithDocumentType and via PureXML.xmlDeclaration(_:).
- The pseudo-attributes are validated: fixed order (version, encoding,
  standalone), known names only, and standalone must be yes or no. A
  malformed declaration is rejected.
- The declared version is now readable, a prerequisite for XML 1.1.

parseWithDocumentType returns a ParsedDocument struct (node,
documentType, declaration) rather than a 2-tuple; the member names are
unchanged so existing call sites are unaffected.

Closes #95
2026-06-08 14:53:04 +02:00

75 lines
2.6 KiB
Swift

extension PureXML.Parsing.XMLDeclaration {
/// A name/value pair from an XML declaration.
struct PseudoAttribute: Equatable {
let name: String
let value: String
}
/// Scans the `name="value"` pseudo-attributes of an XML declaration's text.
/// Each value must be quoted; anything malformed (a missing `=`, an unquoted
/// value, trailing junk) makes the whole scan fail so the declaration is
/// rejected rather than partly read.
struct PseudoAttributeScanner {
private let chars: [Character]
private var index = 0
init(_ text: String) {
chars = Array(text)
}
mutating func scan() -> [PseudoAttribute]? {
var pairs: [PseudoAttribute] = []
skipSpace()
while index < chars.count {
guard let name = scanName(), !name.isEmpty else { return nil }
skipSpace()
guard consume("=") else { return nil }
skipSpace()
guard let value = scanQuoted() else { return nil }
pairs.append(PseudoAttribute(name: name, value: value))
skipSpace()
}
return pairs
}
private mutating func scanName() -> String? {
var name = ""
while index < chars.count, isNameCharacter(chars[index]) {
name.append(chars[index])
index += 1
}
return name.isEmpty ? nil : name
}
private mutating func scanQuoted() -> String? {
guard index < chars.count, chars[index] == "\"" || chars[index] == "'" else { return nil }
let quote = chars[index]
index += 1
var value = ""
while index < chars.count, chars[index] != quote {
value.append(chars[index])
index += 1
}
guard index < chars.count else { return nil }
index += 1
return value
}
private mutating func consume(_ character: Character) -> Bool {
guard index < chars.count, chars[index] == character else { return false }
index += 1
return true
}
private mutating func skipSpace() {
while index < chars.count, chars[index] == " " || chars[index] == "\t" || chars[index] == "\n" || chars[index] == "\r" {
index += 1
}
}
private func isNameCharacter(_ character: Character) -> Bool {
character.isLetter || character.isNumber || character == "-" || character == "_" || character == "."
}
}
}