///|
fn linkify_left_boundary(text : StringView, pos : Int) -> Bool {
if pos <= 0 {
return true
}
match text.get_char(pos - 1) {
Some(ch) => !(linkify_ascii_alnum(ch) || ch == '_')
None => true
}
}
///|
fn linkify_candidate_can_start(text : StringView, pos : Int) -> Bool {
linkify_broad_candidate_start(text, pos) ||
linkify_fuzzy_start_char(text.get_char(pos).unwrap())
}
///|
fn linkify_broad_candidate_start(text : StringView, pos : Int) -> Bool {
@syn.starts_with_case_insensitive(text, pos, "http://") ||
@syn.starts_with_case_insensitive(text, pos, "https://") ||
@syn.starts_with_case_insensitive(text, pos, "ftp://") ||
@syn.starts_with_case_insensitive(text, pos, "mailto:") ||
@syn.starts_with_case_insensitive(text, pos, "www.") ||
(text.get_char(pos) is Some('/') && text.get_char(pos + 1) is Some('/'))
}
///|
fn linkify_candidate_end(text : StringView, start : Int) -> Int {
let mut pos = start
for ch in text[start:text.length()] {
if linkify_candidate_delimiter(ch) {
return pos
}
pos += ch.utf16_len()
}
text.length()
}
///|
fn linkify_fuzzy_candidate_end(text : StringView, start : Int) -> Int {
let mut pos = start
for ch in text[start:text.length()] {
if linkify_candidate_delimiter(ch) ||
ch == '(' ||
ch == ')' ||
ch == '[' ||
ch == ']' {
return pos
}
pos += ch.utf16_len()
}
text.length()
}
///|
fn linkify_candidate_delimiter(ch : Char) -> Bool {
ch.is_ascii_whitespace() || ch == '<' || ch == '>' || ch == '\u{FF5C}'
}
///|
fn linkify_match_candidate(
text : StringView,
start : Int,
raw_end : Int,
config : LinkifyConfig,
) -> LinkMatch? {
let mut end = linkify_trim_trailing_end(text, start, raw_end)
if end <= start {
return None
}
guard text.get_view(start~, end~) is Some(candidate) else { return None }
if linkify_is_scheme_url(candidate) {
match linkify_find_substring(candidate, ")[") {
Some(offset) =>
end = linkify_trim_trailing_end(text, start, start + offset)
None => ()
}
}
guard text.get_view(start~, end~) is Some(candidate) else { return None }
if !linkify_validate_candidate(candidate, text, start, config) {
return None
}
let match_text = candidate.to_owned()
let (href, kind) = linkify_href_for(candidate)
Some({ start, end, text: match_text, href, kind })
}
///|
fn linkify_trim_trailing_end(text : StringView, start : Int, end : Int) -> Int {
let mut end = end
while end > start {
match text.get_char(end - 1) {
Some(ch) if ch == '.' ||
ch == ',' ||
ch == ';' ||
ch == ':' ||
ch == '!' ||
ch == '?' => end -= 1
_ => break
}
}
while end > start {
match text.get_char(end - 1) {
Some('"') if linkify_count_char(text, start, end, '"') % 2 == 1 =>
end -= 1
Some('\'') if linkify_count_char(text, start, end, '\'') % 2 == 1 =>
end -= 1
_ => break
}
}
while end > start {
match text.get_char(end - 1) {
Some(')') if linkify_count_char(text, start, end, ')') >
linkify_count_char(text, start, end, '(') => end -= 1
Some(']') if linkify_count_char(text, start, end, ']') >
linkify_count_char(text, start, end, '[') => end -= 1
Some('}') if linkify_count_char(text, start, end, '}') >
linkify_count_char(text, start, end, '{') => end -= 1
Some('>') if linkify_count_char(text, start, end, '>') >
linkify_count_char(text, start, end, '<') => end -= 1
_ => break
}
}
end
}
///|
fn linkify_embedded_scheme_start(
text : StringView,
start : Int,
end : Int,
) -> Int? {
let mut pos = start
for ch in text[start:end] {
let next = pos + ch.utf16_len()
if next < end &&
(
@syn.starts_with_case_insensitive(text, next, "http://") ||
@syn.starts_with_case_insensitive(text, next, "https://") ||
@syn.starts_with_case_insensitive(text, next, "ftp://") ||
@syn.starts_with_case_insensitive(text, next, "mailto:")
) {
return Some(next)
}
pos = next
}
None
}