PureXML/Sources/HTML/HTMLEntities.swift
Mihaela Mihaljevic cfba3ed7d2 feat(html): lenient HTML parser and serializer
Add the PureXML.HTML namespace: a lenient tokenizer for tag-soup input
(case-insensitive tags; quoted, unquoted, and boolean attributes; comments;
doctype; character references), a tree builder handling void elements,
optional end tags (li, p, table rows and cells), raw-text elements (script,
style), and unmatched end tags, and a serializer that writes void elements
unclosed and raw-text content unescaped.

Closes #36
Closes #37
Closes #38
Closes #39
Closes #20
2026-06-08 07:12:42 +02:00

55 lines
2.1 KiB
Swift

extension PureXML.HTML.Tokenizer {
private static let named: [String: Character] = [
"amp": "&", "lt": "<", "gt": ">", "quot": "\"", "apos": "'",
"nbsp": "\u{A0}", "copy": "\u{A9}", "reg": "\u{AE}", "trade": "\u{2122}",
"mdash": "\u{2014}", "ndash": "\u{2013}", "hellip": "\u{2026}",
"laquo": "\u{AB}", "raquo": "\u{BB}", "deg": "\u{B0}", "euro": "\u{20AC}",
]
/// Decodes the common HTML character references in `value`, leaving any
/// unrecognized `&...;` sequence as written (the lenient HTML behavior).
static func decodeEntities(_ value: String) -> String {
guard value.contains("&") else { return value }
var result = ""
var characters = Array(value)
var index = 0
while index < characters.count {
guard characters[index] == "&", let semicolon = nextSemicolon(characters, after: index) else {
result.append(characters[index])
index += 1
continue
}
let body = String(characters[(index + 1) ..< semicolon])
if let decoded = decode(body) {
result.append(decoded)
index = semicolon + 1
} else {
result.append("&")
index += 1
}
}
return result
}
private static func nextSemicolon(_ characters: [Character], after start: Int) -> Int? {
var index = start + 1
while index < characters.count, index - start <= 10 {
if characters[index] == ";" { return index }
index += 1
}
return nil
}
private static func decode(_ body: String) -> Character? {
if let named = named[body] { return named }
guard body.hasPrefix("#") else { return nil }
let digits = body.dropFirst()
let scalar: UInt32? = if digits.hasPrefix("x") || digits.hasPrefix("X") {
UInt32(digits.dropFirst(), radix: 16)
} else {
UInt32(digits, radix: 10)
}
return scalar.flatMap(Unicode.Scalar.init).map(Character.init)
}
}