///|
/// GFM extended-autolink helpers used by the HTML renderer.
priv enum BareAutolinkKind {
Url
Www
Email
}
///|
#valtype
priv struct BareAutolink {
start : Int
end : Int
kind : BareAutolinkKind
}
///|
/// Find the next URL, `www.` link, or email in a plain-text inline node.
fn find_next_autolink(content : String, from : Int) -> BareAutolink? {
let len = content.length()
let mut pos = from
while pos < len {
pos = find_autolink_candidate(content, pos, len)
if pos >= len {
return None
}
let c = content.unsafe_get(pos)
// GFM's URL forms are lowercase and can only start with h/f/w. Avoid
// constructing and comparing four StringViews at every text position.
if c == 'h' || c == 'f' || c == 'w' {
match url_autolink_at(content, pos) {
Some(found) => return Some(found)
None => ()
}
}
// An email can be identified from its single mandatory '@'. Scanning the
// local part backwards here avoids rescanning every ordinary word as a
// possible email address.
if c == '@' {
match email_autolink_at_sign(content, pos) {
Some(found) => return Some(found)
None => ()
}
}
pos = pos + 1
}
None
}
///|
/// Scalar fallback for short strings and non-linear-memory targets.
#inline
fn find_autolink_candidate_scalar(
content : String,
start : Int,
end : Int,
) -> Int {
for pos in start.. Int {
find_autolink_candidate_scalar(content, start, end)
}
///|
/// Find the four possible bare-autolink anchors eight code units at a time.
#cfg(any(target="native", target="wasm"))
fn find_autolink_candidate(content : String, start : Int, end : Int) -> Int {
guard end - start >= 16 else {
return find_autolink_candidate_scalar(content, start, end)
}
let h = @v128.i16x8_splat('h')
let f = @v128.i16x8_splat('f')
let w = @v128.i16x8_splat('w')
let at = @v128.i16x8_splat('@')
let tail_start = for pos = start; pos + 8 <= end; {
let block = @v128.v128_load_i16x8(content, pos)
let mask = @v128.i16x8_bitmask(
@v128.v128_or_(
@v128.v128_or_(@v128.i16x8_eq(block, h), @v128.i16x8_eq(block, f)),
@v128.v128_or_(@v128.i16x8_eq(block, w), @v128.i16x8_eq(block, at)),
),
)
if mask != 0 {
return pos + mask.ctz()
}
continue pos + 8
} nobreak {
pos
}
find_autolink_candidate_scalar(content, tail_start, end)
}
///|
/// Match an explicit `http(s)://` / `ftp://` URL or a `www.` URL.
fn url_autolink_at(content : String, start : Int) -> BareAutolink? {
let is_www = string_has_prefix_at(content, start, "www.")
let scheme_length = if string_has_prefix_at(content, start, "https://") {
8
} else if string_has_prefix_at(content, start, "http://") ||
string_has_prefix_at(content, start, "ftp://") {
7
} else {
0
}
if !is_www && scheme_length == 0 {
return None
}
if start > 0 {
let previous = content.unsafe_get(start - 1)
if is_www {
if !(is_html_space(previous) ||
previous == '*' ||
previous == '_' ||
previous == '~' ||
previous == '(') {
return None
}
} else if is_ascii_alnum_unit(previous) {
return None
}
}
let host_start = if is_www { start + 4 } else { start + scheme_length }
let host_end = scan_url_host(content, host_start)
if host_end == host_start {
return None
}
if is_www && !url_host_has_dot(content, host_start, host_end) {
return None
}
if url_host_has_invalid_underscore(content, host_start, host_end) {
return None
}
let raw_end = find_url_raw_end(content, host_end)
let end = trim_url_end(content, start, raw_end)
guard end > host_start else { return None }
Some({ start, end, kind: if is_www { Www } else { Url } })
}
///|
/// The host portion ends before URL path/query punctuation.
fn scan_url_host(content : String, start : Int) -> Int {
let len = content.length()
let mut end = start
while end < len {
let c = content.unsafe_get(end)
if is_ascii_alnum_unit(c) || c == '.' || c == '-' || c == '_' {
end = end + 1
} else {
break
}
}
end
}
///|
fn url_host_has_dot(content : String, start : Int, end : Int) -> Bool {
for i = start; i < end; i = i + 1 {
if content.unsafe_get(i) == '.' {
return true
}
}
false
}
///|
/// Hostnames reject underscores in either of their final two labels.
fn url_host_has_invalid_underscore(
content : String,
start : Int,
end : Int,
) -> Bool {
let mut last_dot = end
let mut previous_dot = end
for i = end - 1; i >= start; i = i - 1 {
if content.unsafe_get(i) == '.' {
if last_dot == end {
last_dot = i
} else {
previous_dot = i
break
}
}
}
let check_from = if previous_dot < end { previous_dot + 1 } else { start }
for i = check_from; i < end; i = i + 1 {
if content.unsafe_get(i) == '_' {
return true
}
}
false
}
///|
/// Match the GFM extended-email form around its mandatory `@`.
fn email_autolink_at_sign(content : String, at : Int) -> BareAutolink? {
let len = content.length()
guard at > 0 && at + 1 < len && content.unsafe_get(at) == '@' else {
return None
}
let mut start = at
while start > 0 && is_email_local_char(content.unsafe_get(start - 1)) {
start = start - 1
}
guard start < at else { return None }
if start > 0 && is_email_local_char(content.unsafe_get(start - 1)) {
return None
}
let domain_start = at + 1
let mut end = domain_start
let mut dots = 0
while end < len {
let c = content.unsafe_get(end)
if is_ascii_alnum_unit(c) || c == '-' || c == '_' {
end = end + 1
} else if c == '.' &&
end + 1 < len &&
is_ascii_alnum_unit(content.unsafe_get(end + 1)) {
dots = dots + 1
end = end + 1
} else {
break
}
}
guard end > domain_start && dots > 0 else { return None }
let last = content.unsafe_get(end - 1)
guard is_ascii_letter_unit(last) else { return None }
guard !url_host_has_invalid_underscore(content, domain_start, end) else {
return None
}
Some({ start, end, kind: Email })
}
///|
fn is_email_local_char(c : UInt16) -> Bool {
is_ascii_alnum_unit(c) || c == '.' || c == '+' || c == '-' || c == '_'
}
///|
fn string_has_prefix_at(content : String, pos : Int, prefix : String) -> Bool {
let end = pos + prefix.length()
guard end <= content.length() else { return false }
for i = 0; i < prefix.length(); i = i + 1 {
if content.unsafe_get(pos + i) != prefix.unsafe_get(i) {
return false
}
}
true
}
///|
fn find_url_raw_end(content : String, start : Int) -> Int {
let len = content.length()
let mut end = start
while end < len {
let c = content.unsafe_get(end)
if is_html_space(c) || c == '<' {
break
}
end = end + 1
}
end
}
///|
/// Apply cmark-gfm's balanced-parenthesis and trailing-delimiter rules.
fn trim_url_end(content : String, start : Int, raw_end : Int) -> Int {
let mut opening = 0
let mut closing = 0
for i = start; i < raw_end; i = i + 1 {
let c = content.unsafe_get(i)
if c == '(' {
opening = opening + 1
} else if c == ')' {
closing = closing + 1
}
}
let mut end = raw_end
while end > start {
let c = content.unsafe_get(end - 1)
if c == ')' {
if closing <= opening {
return end
}
closing = closing - 1
end = end - 1
} else if c == '?' ||
c == '!' ||
c == '.' ||
c == ',' ||
c == ':' ||
c == '*' ||
c == '_' ||
c == '~' ||
c == '\'' ||
c == '"' {
end = end - 1
} else if c == ';' {
let mut entity_start = end - 2
while entity_start > start &&
is_ascii_letter_unit(content.unsafe_get(entity_start)) {
entity_start = entity_start - 1
}
if entity_start < end - 2 && content.unsafe_get(entity_start) == '&' {
end = entity_start
} else {
end = end - 1
}
} else {
return end
}
}
end
}