///|
/// `link_destination(s, last, start)` matches a link destination
/// starting at `start` in the range `start..=last` (assumed on
/// the same line). This is `Some((delimited, first, last))` with the
/// data in [first..=last] the destination data. `delimited` is
/// `true` if `first-1` is '<' and `last + 1` is '>'.
///
/// See: https://spec.commonmark.org/current/#link-destination
pub fn link_destination(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> (Bool, First, Last)? {
if start > last {
return None
}
if s[start] == '<' {
// delimited, i.e. start has '<'
// https://spec.commonmark.org/current/#link-destination 1st
for prev = '\u{0}', k = start + 1 {
if k > last {
break None
}
let c = s[k].unsafe_to_char()
match (c, prev) {
('\n' | '\r', _) => break None
('\\', '\\') => continue '\u{0}', k + 1
('<', '\\') => ()
('<', _) => break None
('>', '\\') => ()
('>', _) => break Some((true, start + 1, k - 1))
(_, _) => ()
}
continue c, k + 1
}
} else {
// not_delimited
// https://spec.commonmark.org/current/#link-destination 2nd
for prev = '\u{0}', bal = 0, k = start {
if k > last {
break if bal == 0 { Some((false, start, k - 1)) } else { None }
}
let c = s[k].unsafe_to_char()
match (c, prev) {
('\\', '\\') => continue '\u{0}', bal, k + 1
('(', '\\') => ()
('(', _) => continue c, bal + 1, k + 1
(')', '\\') => ()
(')', _) => {
let bal = bal - 1
if bal < 0 {
break Some((false, start, k - 1)) // hit inline link closing ')'
}
continue c, bal, k + 1
}
(c, _) =>
if c == ' ' || @char.is_ascii_control(c) {
break if k != start && bal == 0 {
Some((false, start, k - 1))
} else {
None
}
}
}
continue c, bal, k + 1
}
}
}
///|
/// `link_title(next_line, s, lines, line, last)` is a link title on line `line` starting at `start`.
/// Returns `Some((lines, last_line, spans, last))` with `lines` the lines after consuming the title,
/// `last_line` the line where it stops, `spans` the byte ranges of `s` that make up the title in reverse
/// order, `last` is on the closing delimiter and guaranteed to be on `last_line`.
///
/// See: https://spec.commonmark.org/current/#link-title
pub fn[A] link_title(
next_line~ : NextLineFn[A],
s : String,
lines : A,
line~ : LineSpan,
start~ : CharCodePos,
) -> (LineSpan, Array[Span], Last)? {
if start > line.last {
return None
}
match s[start].unsafe_to_char() {
'"' | '\'' as char => {
let spans = []
accept_upto(char~, next_line~, s, lines, line~, spans, after=start).map(span_last => {
(span_last.0, spans, span_last.1)
})
}
'(' => {
let start = start + 1
let acc = []
for line = line, prev_bslash = false, start = start, k = start {
if k > line.last {
guard next_line(lines) is Some(newline) else { break None }
guard newline.first <= newline.last else { break None }
push_span(line~, start, line.last, acc)
let start = first_non_blank_in_span(s, newline)
let prev_bslash = false
continue newline, prev_bslash, start, start
}
if !prev_bslash {
if s[k] == '(' {
break None
}
if s[k] == ')' {
push_span(line~, start, k - 1, acc)
break Some((line, acc, k))
}
}
let prev_bslash = s[k] == '\\' && !prev_bslash
continue line, prev_bslash, start, k + 1
}
}
_ => None
}
}
///|
/// Matches a link label on `line` starting at `start`.
/// The byte ranges have the label's content, the string is the normalized
/// label (blanks collapsed, Unicode case folded).
/// `buf` is used as scratch space.
///
/// See: https://spec.commonmark.org/current/#link-label
pub fn[A] link_label(
buf : StringBuilder,
next_line~ : NextLineFn[A],
s : String,
lines : A,
line~ : LineSpan,
start~ : CharCodePos,
) -> (LineSpan, Array[Span], Last, String)? {
if start > line.last || s[start] != '[' {
return None
}
let start = start + 1
buf.reset()
let acc = []
let b = buf
for line = line, prev = '\u{0}', start = start, count = 0, k = start {
if k > line.last {
guard next_line(lines) is Some(newline) else { break None }
if newline.first == newline.last {
break None
}
push_span(line~, start, line.last, acc)
let start = first_non_blank_in_span(s, newline)
if !b.is_empty() {
b.write_char(' ')
}
let prev = '\u{0}'
continue newline, prev, start, count, start
}
if count > 999 {
break None
}
match (s[k].unsafe_to_char(), prev) {
('\\', '\\') => {
b.write_char('\\')
let prev = '\u{0}'
continue line, prev, start, count + 1, k + 1
}
(']', '\\') => ()
(']', _) => {
// Labels match case-insensitively with Unicode case folding.
let key = @casefold.casefold(b.to_string())
if key.iter().all(@char.is_ascii_whitespace) {
break None
}
push_span(line~, start, k - 1, acc)
break Some((line, acc, k, key))
}
('[', '\\') => ()
('[', _) => break None
(' ' | '\t' as prev, _) => continue line, prev, start, count + 1, k + 1
_ => ()
}
// Collapse non initial whitespace
if @char.is_ascii_blank(prev) && !b.is_empty() {
b.write_char(' ')
}
// Indices are UTF-16 code units, so advance by the char's UTF-16
// length; unpaired surrogates normalize to U+FFFD.
let mut u = match @char.at_checked(s, k) {
Ok(c) => c
Err(_) => @char.rep
}
if u == '\u{0}' {
u = @char.rep
}
let k1 = k + @char.length_utf16(u.to_int())
b.write_char(u)
continue line, u, start, count + 1, k1
}
}