extension PureXML.HTML {
/// A lenient HTML tokenizer for tag-soup input: case-insensitive tag names,
/// quoted/unquoted/boolean attributes, comments, doctype, and raw-text
/// elements (`script`, `style`, `textarea`, `title`) whose content is taken
/// verbatim. Malformed markup degrades to text rather than failing.
struct Tokenizer {
private let chars: [Character]
private var index = 0
private var rawTextName: String?
init(_ html: String) {
chars = Array(html)
}
mutating func tokenize() -> [Token] {
var tokens: [Token] = []
while let token = next() {
tokens.append(token)
}
return tokens
}
private mutating func next() -> Token? {
guard !isAtEnd else { return nil }
if let raw = rawTextName {
rawTextName = nil
return rawText(until: raw)
}
guard peek() == "<" else { return text() }
if matches("") {
value.append(chars[index])
index += 1
}
_ = consume("-->")
return .comment(value)
}
private mutating func doctype() -> Token {
index += 2
var value = ""
while let character = peek(), character != ">" {
value.append(character)
advance()
}
consume(">")
return .doctype(value)
}
private mutating func rawText(until name: String) -> Token {
let target = Array("" + name)
var value = ""
while !isAtEnd, !matchesCaseInsensitive(target) {
value.append(chars[index])
index += 1
}
value = Self.replacingNull(value)
// RCDATA decodes character references; raw text (script/style) does not.
if Elements.rcdata.contains(name) {
value = Self.decodeEntities(value)
}
// A single leading newline after