///| Entity and numeric character references.
///| See .
///|
/// Replacement character used for code points the spec rejects (NUL, values
/// outside Unicode, and unpaired surrogates).
const ReplacementChar : String = "\u{fffd}"
///|
/// Named references, expanded from `entity_chunks` the first time one is seen.
/// Keys omit the surrounding `&` and `;`.
let entity_map : Map[String, String] = Map([], capacity=2048)
///|
fn ensure_entity_map() -> Unit {
if !entity_map.is_empty() {
return
}
for chunk in entity_chunks {
let len = chunk.length()
let mut i = 0
while i < len {
let mut eq = i
while eq < len && chunk.unsafe_get(eq) != '=' {
eq = eq + 1
}
let mut end = eq + 1
while end < len && chunk.unsafe_get(end) != '\u{1}' {
end = end + 1
}
entity_map[chunk.unsafe_substring(start=i, end=eq)] = chunk.unsafe_substring(
start=eq + 1,
end~,
)
i = end + 1
}
}
}
///|
/// Look up a named character reference, e.g. `"amp"` -> `"&"`.
fn lookup_entity(name : String) -> String? {
ensure_entity_map()
entity_map.get(name)
}
///|
fn code_point_to_string(code : Int) -> String {
if code == 0 || code > 0x10FFFF || (code >= 0xD800 && code <= 0xDFFF) {
return ReplacementChar
}
match code.to_char() {
Some(c) => String::make(1, c)
None => ReplacementChar
}
}
///|
fn is_ascii_digit_unit(c : UInt16) -> Bool {
c >= '0' && c <= '9'
}
///|
fn is_ascii_alnum_unit(c : UInt16) -> Bool {
(c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
///|
fn hex_digit_value(c : UInt16) -> Int? {
if c >= '0' && c <= '9' {
Some(c.to_int() - '0'.to_int())
} else if c >= 'a' && c <= 'f' {
Some(c.to_int() - 'a'.to_int() + 10)
} else if c >= 'A' && c <= 'F' {
Some(c.to_int() - 'A'.to_int() + 10)
} else {
None
}
}
///|
/// Decode the entity reference that starts with the `&` at `start`.
///
/// Returns the replacement text together with the index just past the closing
/// `;`, or `None` when this is not a valid reference and the `&` should be kept
/// as literal text.
fn decode_entity(text : String, start : Int) -> (String, Int)? {
let len = text.length()
guard start < len && text.unsafe_get(start) == '&' else { return None }
let mut i = start + 1
guard i < len else { return None }
if text.unsafe_get(i) == '#' {
i = i + 1
guard i < len else { return None }
let c = text.unsafe_get(i)
if c == 'x' || c == 'X' {
i = i + 1
let digits_start = i
let mut value = 0
while i < len && i - digits_start < 6 {
match hex_digit_value(text.unsafe_get(i)) {
Some(d) => {
value = value * 16 + d
i = i + 1
}
None => break
}
}
guard i > digits_start && i < len && text.unsafe_get(i) == ';' else {
return None
}
return Some((code_point_to_string(value), i + 1))
}
let digits_start = i
let mut value = 0
while i < len &&
i - digits_start < 7 &&
is_ascii_digit_unit(text.unsafe_get(i)) {
value = value * 10 + (text.unsafe_get(i).to_int() - '0'.to_int())
i = i + 1
}
guard i > digits_start && i < len && text.unsafe_get(i) == ';' else {
return None
}
return Some((code_point_to_string(value), i + 1))
}
let name_start = i
while i < len && is_ascii_alnum_unit(text.unsafe_get(i)) {
i = i + 1
}
guard i > name_start && i < len && text.unsafe_get(i) == ';' else {
return None
}
match lookup_entity(text.unsafe_substring(start=name_start, end=i)) {
Some(value) => Some((value, i + 1))
None => None
}
}