// Text handling that is not specific to blocks or inlines: entity references,
// the backslash unescaping of link destinations, the normalisation of labels,
// and the two escapers the HTML renderer writes through.
///|
/// Whether the text holds nothing but spaces, tabs and newlines.
fn is_blank(text : StringView) -> Bool {
for c in text {
if c != ' ' && c != '\t' && c != '\n' && c != '\r' {
return false
}
}
true
}
///|
/// Drop the spaces, tabs and line endings at both ends.
fn trim_view(text : StringView) -> StringView {
text.trim(chars=" \t\r\n\u{0B}\u{0C}"[:])
}
///|
/// The Unicode character at `i`, taking a surrogate pair as one.
fn char_at(text : StringView, i : Int32) -> Char {
match text.get_char(i) {
Some(c) => c
None => '\u{0}'
}
}
///|
/// The Unicode character just before `i`, taking a surrogate pair as one.
fn char_before(text : StringView, i : Int32) -> Char {
if i <= 0 {
return '\n'
}
let last = char_at(text, i - 1)
if i >= 2 && last.to_int() >= 0xDC00 && last.to_int() <= 0xDFFF {
let high = char_at(text, i - 2)
if high.to_int() >= 0xD800 && high.to_int() <= 0xDBFF {
return combine_surrogates(char_at(text, i - 2), last)
}
}
last
}
///|
fn combine_surrogates(high : Char, low : Char) -> Char {
let cp = 0x10000 + ((high.to_int() - 0xD800) << 10) + (low.to_int() - 0xDC00)
Int32::unsafe_to_char(cp)
}
///|
/// The Unicode White_Space property.
fn is_unicode_space(c : Char) -> Bool {
let cp = c.to_int()
let mut low = 0
let mut high = whitespace_code_points.length() - 1
while low <= high {
let mid = (low + high) / 2
let value = whitespace_code_points[mid]
if value == cp {
return true
} else if value < cp {
low = mid + 1
} else {
high = mid - 1
}
}
false
}
///|
/// The P general category alone, which is what a host name stops at: a symbol
/// may be part of one, an emoji included.
fn is_unicode_punctuation_only(c : Char) -> Bool {
let cp = c.to_int()
let mut low = 0
let mut high = punctuation_only_ranges.length() / 2 - 1
while low <= high {
let mid = (low + high) / 2
let start = punctuation_only_ranges[mid * 2]
let end = punctuation_only_ranges[mid * 2 + 1]
if cp < start {
high = mid - 1
} else if cp > end {
low = mid + 1
} else {
return true
}
}
false
}
///|
/// The P or S general category.
fn is_unicode_punct(c : Char) -> Bool {
let cp = c.to_int()
let mut low = 0
let mut high = punctuation_ranges.length() / 2 - 1
while low <= high {
let mid = (low + high) / 2
let start = punctuation_ranges[mid * 2]
let end = punctuation_ranges[mid * 2 + 1]
if cp < start {
high = mid - 1
} else if cp > end {
low = mid + 1
} else {
return true
}
}
false
}
///|
/// Look up a named character reference by its name, without the `&` and `;`.
fn entity_by_name(name : StringView) -> String? {
let mut low = 0
let mut high = entity_names.length() - 1
while low <= high {
let mid = (low + high) / 2
let order = entity_names[mid][:].lexical_compare(name)
if order == 0 {
return Some(entity_values[mid])
} else if order < 0 {
low = mid + 1
} else {
high = mid - 1
}
}
None
}
///|
/// The upper case hexadecimal digit for a value below sixteen.
fn hex_digit(value : Int32) -> Char {
Int32::unsafe_to_char(
if value < 10 {
0x30 + value
} else {
0x41 + value - 10
},
)
}
///|
/// Encode a code point as the UTF-8 percent escapes an href may hold.
fn write_percent_encoded(buf : StringBuilder, cp : Int32) -> Unit {
let bytes : Array[Int32] = []
if cp < 0x80 {
bytes.push(cp)
} else if cp < 0x800 {
bytes.push(0xC0 | (cp >> 6))
bytes.push(0x80 | (cp & 0x3F))
} else if cp < 0x10000 {
bytes.push(0xE0 | (cp >> 12))
bytes.push(0x80 | ((cp >> 6) & 0x3F))
bytes.push(0x80 | (cp & 0x3F))
} else {
bytes.push(0xF0 | (cp >> 18))
bytes.push(0x80 | ((cp >> 12) & 0x3F))
bytes.push(0x80 | ((cp >> 6) & 0x3F))
bytes.push(0x80 | (cp & 0x3F))
}
for byte in bytes {
buf.write_char('%')
buf.write_char(hex_digit(byte >> 4))
buf.write_char(hex_digit(byte & 0xF))
}
}
///|
/// The replacement text of the entity reference at `pos` and how many code
/// units it spans, or `("", 0)` when there is none there. `pos` points at `&`.
fn entity_at(text : StringView, pos : Int32) -> (String, Int32) {
let n = text.length()
let mut i = pos + 1
if cu(text, i) != 0x23 {
// A named reference.
let mut name_len = 0
while name_len < 32 && is_alnum(cu(text, i + name_len)) {
name_len = name_len + 1
}
if name_len < 2 || cu(text, i + name_len) != 0x3B {
return ("", 0)
}
match entity_by_name(text.clamped_view(start=i, end=i + name_len)) {
Some(value) => (value, name_len + 2)
None => ("", 0)
}
} else {
// `{` or `{`, at most seven decimal or six hexadecimal digits.
i = i + 1
let mut value = 0
let mut digits = 0
if cu(text, i) == 0x78 || cu(text, i) == 0x58 {
i = i + 1
while digits < 6 {
let c = cu(text, i)
let digit = if c >= 0x30 && c <= 0x39 {
c - 0x30
} else if c >= 0x61 && c <= 0x66 {
c - 0x57
} else if c >= 0x41 && c <= 0x46 {
c - 0x37
} else {
break
}
value = value * 16 + digit
digits = digits + 1
i = i + 1
}
} else {
while digits < 7 && is_digit(cu(text, i)) {
value = value * 10 + (cu(text, i) - 0x30)
digits = digits + 1
i = i + 1
}
}
if digits == 0 || cu(text, i) != 0x3B || i >= n + 1 {
return ("", 0)
}
if value == 0 || value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF) {
value = 0xFFFD
}
let buf = StringBuilder()
buf.write_char(Int32::unsafe_to_char(value))
(buf.to_string(), i + 1 - pos)
}
}
///|
/// Replace every entity reference in `text` by what it stands for.
fn html_unescape(text : StringView) -> String {
if !text.contains_char('&') {
return text.to_owned()
}
let buf = StringBuilder()
let n = text.length()
let mut i = 0
while i < n {
if cu(text, i) == 0x26 {
let (value, len) = entity_at(text, i)
if len > 0 {
buf.write_string(value)
i = i + len
continue
}
}
let c = char_at(text, i)
buf.write_char(c)
i = i + c.utf16_len()
}
buf.to_string()
}
///|
/// Remove the backslashes in front of ASCII punctuation. Entities in the
/// entities region are left alone: callers run this after `html_unescape`.
fn unescape_backslashes(text : StringView) -> String {
if !text.contains_char('\\') {
return text.to_owned()
}
let buf = StringBuilder()
let n = text.length()
let mut i = 0
while i < n {
if cu(text, i) == 0x5C && is_ascii_punct(cu(text, i + 1)) {
i = i + 1
}
buf.write_char(char_at(text, i))
i = i + char_at(text, i).utf16_len()
}
buf.to_string()
}
///|
/// Turn the code span's newlines into spaces and drop one space at each end
/// when the span is not made of spaces only.
fn normalize_code(text : StringView) -> String {
let buf = StringBuilder()
let mut has_content = false
for c in text {
if c == '\r' {
continue
}
if c == '\n' {
buf.write_char(' ')
} else {
buf.write_char(c)
if c != ' ' {
has_content = true
}
}
}
let result = buf.to_string()
if has_content &&
result.length() >= 2 &&
result.at(0).to_int() == 0x20 &&
result.at(result.length() - 1).to_int() == 0x20 {
return result.clamped_view(start=1, end=result.length() - 1).to_owned()
}
result
}
///|
/// How a link label compares: case folded, trimmed, inner whitespace collapsed.
fn normalize_label(text : StringView) -> String {
let folded = StringBuilder()
for c in text {
match fold_lookup(c.to_int()) {
Some(value) => folded.write_string(value)
None => folded.write_char(c)
}
}
let buf = StringBuilder()
let mut pending_space = false
let mut written = 0
for c in folded.to_string() {
if c == ' ' ||
c == '\t' ||
c == '\n' ||
c == '\r' ||
c == '\u{0B}' ||
c == '\u{0C}' {
pending_space = true
} else {
if pending_space && written > 0 {
buf.write_char(' ')
written = written + 1
}
pending_space = false
buf.write_char(c)
written = written + 1
}
}
buf.to_string()
}
///|
/// The full case folding of a code point, if it differs from the character.
fn fold_lookup(cp : Int32) -> String? {
let mut low = 0
let mut high = fold_from.length() - 1
while low <= high {
let mid = (low + high) / 2
let value = fold_from[mid]
if value == cp {
return Some(fold_to[mid])
} else if value < cp {
low = mid + 1
} else {
high = mid - 1
}
}
None
}
///|
/// A link destination as the tree keeps it: trimmed, entities and backslash
/// escapes resolved.
fn clean_url(text : StringView) -> String {
let trimmed = trim_view(text)
if trimmed.is_empty() {
return ""
}
unescape_backslashes(html_unescape(trimmed))
}
///|
/// A link title as the tree keeps it: surrounding quotes removed, entities and
/// backslash escapes resolved.
fn clean_title(text : StringView) -> String {
if text.is_empty() {
return ""
}
let first = text.at(0).to_int()
let last = text.at(text.length() - 1).to_int()
let inner = if (first == 0x27 && last == 0x27) ||
(first == 0x28 && last == 0x29) ||
(first == 0x22 && last == 0x22) {
text.clamped_view(start=1, end=text.length() - 1)
} else {
text
}
unescape_backslashes(html_unescape(inner))
}
///|
/// Write `text` with the four characters that matter inside HTML escaped.
fn write_html_escaped(buf : StringBuilder, text : StringView) -> Unit {
for c in text {
match c {
'&' => buf.write_string("&")
'<' => buf.write_string("<")
'>' => buf.write_string(">")
'"' => buf.write_string(""")
_ => buf.write_char(c)
}
}
}
///|
/// Whether a code point may appear in an href as it is. Everything else is
/// percent-encoded, which is what keeps international URLs working.
fn is_href_safe(c : Int32) -> Bool {
if (c >= 0x30 && c <= 0x39) ||
(c >= 0x41 && c <= 0x5A) ||
(c >= 0x61 && c <= 0x7A) {
return true
}
match c {
0x21
| 0x23
| 0x24
| 0x25
| 0x28
| 0x29
| 0x2A
| 0x2B
| 0x2C
| 0x2D
| 0x2E
| 0x2F
| 0x3A
| 0x3B
| 0x3D
| 0x3F
| 0x40
| 0x5F
| 0x7E => true
_ => false
}
}
///|
/// Write a link destination the way an `href` attribute needs it.
fn write_href_escaped(buf : StringBuilder, text : StringView) -> Unit {
for c in text {
let cp = c.to_int()
if c == '&' {
buf.write_string("&")
} else if c == '\'' {
buf.write_string("'")
} else if cp < 0x80 && is_href_safe(cp) {
buf.write_char(c)
} else {
write_percent_encoded(buf, cp)
}
}
}
///|
/// Whether a destination would run script or reach the local file system, and
/// so must not be linked to when the document is rendered safely.
fn is_dangerous_url(url : StringView) -> Bool {
let lower = url.to_lower().to_owned()
if lower.has_prefix("javascript:"[:]) ||
lower.has_prefix("vbscript:"[:]) ||
lower.has_prefix("file:"[:]) {
return true
}
if lower.has_prefix("data:"[:]) {
return !(lower.has_prefix("data:image/png"[:]) ||
lower.has_prefix("data:image/gif"[:]) ||
lower.has_prefix("data:image/jpeg"[:]) ||
lower.has_prefix("data:image/webp"[:]))
}
false
}