///| Links, images, wikilinks and footnote references.
///| Bracket handling follows the "look for link or image" procedure from the
///| CommonMark spec appendix.
///|
fn InlineCtx::open_bracket(self : InlineCtx, pos : Int, image : Bool) -> Unit {
let width = if image { 2 } else { 1 }
let literal = if image { "![" } else { "[" }
let slot = self.append({
inline: Some(
Inline::Text(content=literal, span=Span::new(pos, pos + width)),
),
mergeable: false,
ch: ' ',
count: 0,
orig: 0,
can_open: false,
can_close: false,
start: pos,
prev: -1,
next: -1,
})
self.brackets.push({
slot,
content_start: pos + width,
image,
active: true,
delim_bottom: self.delims.length(),
})
}
///|
/// Handle a `]`, turning the matching bracket into a link or image when one of
/// the inline or reference forms applies. Returns the index to resume from.
fn InlineCtx::close_bracket(self : InlineCtx, pos : Int) -> Int {
let text = self.text
let len = self.len
if self.brackets.is_empty() {
self.push_text("]", pos, pos + 1)
return pos + 1
}
let bracket = self.brackets[self.brackets.length() - 1]
let _ = self.brackets.pop()
if !bracket.active {
self.push_text("]", pos, pos + 1)
return pos + 1
}
let label_src = text.unsafe_substring(start=bracket.content_start, end=pos)
let mut dest = ""
let mut title = ""
let mut ref_label : String? = None
let mut ref_style = ReferenceStyle::Full
let mut end = -1
// Inline form: `](dest "title")`.
if pos + 1 < len && text.unsafe_get(pos + 1) == '(' {
match parse_inline_link_tail(text, pos + 1) {
Some((d, t, e)) => {
dest = d
title = t
end = e
}
None => ()
}
}
if end < 0 {
// Reference forms: full `][label]`, collapsed `][]`, or shortcut.
let (label, after, style) = match read_reference_label(text, pos + 1) {
Some((label, after)) =>
if label.is_empty() {
(label_src, after, ReferenceStyle::Collapsed)
} else {
(label, after, ReferenceStyle::Full)
}
None => (label_src, pos + 1, ReferenceStyle::Shortcut)
}
match self.defs.get(normalize_label(label)) {
Some(def) => {
dest = def.url
title = def.title
ref_label = Some(label)
ref_style = style
end = after
}
None => ()
}
}
if end < 0 {
self.push_text("]", pos, pos + 1)
return pos + 1
}
// Resolve emphasis inside the bracket before wrapping it up.
self.process_emphasis(bracket.delim_bottom)
let children = self.materialize(self.slots[bracket.slot].next, -1)
let mut cur = self.slots[bracket.slot].next
while cur >= 0 {
let next = self.slots[cur].next
self.unlink(cur)
cur = next
}
self.unlink(bracket.slot)
let span = Span::new(
bracket.content_start - (if bracket.image { 2 } else { 1 }),
end,
)
let node = if bracket.image {
match ref_label {
Some(label) =>
Inline::RefImage(alt=label_src, label~, style=ref_style, span~)
None => Inline::Image(alt=label_src, url=dest, title~, span~)
}
} else {
match ref_label {
Some(label) => Inline::RefLink(children~, label~, style=ref_style, span~)
None => Inline::Link(children~, url=dest, title~, span~)
}
}
let _ = self.push_inline(node, span.from)
if !bracket.image {
// No links inside links: earlier `[` openers can no longer match.
for entry in self.brackets {
if !entry.image {
entry.active = false
}
}
}
end
}
///|
/// Parse `(dest "title")` starting at the `(`.
fn parse_inline_link_tail(text : String, start : Int) -> (String, String, Int)? {
let len = text.length()
let mut i = start + 1
i = skip_link_space(text, i)
let (dest, after_dest) = match parse_link_destination(text, i) {
Some(v) => v
None => ("", i)
}
i = after_dest
let before_space = i
i = skip_link_space(text, i)
let mut title = ""
if i > before_space || before_space == after_dest {
match parse_link_title(text, i) {
Some((t, after_title)) => {
title = t
i = skip_link_space(text, after_title)
}
None => ()
}
}
guard i < len && text.unsafe_get(i) == ')' else { return None }
Some((normalize_destination(dest), unescape_string(title), i + 1))
}
///|
fn skip_link_space(text : String, start : Int) -> Int {
let len = text.length()
let mut i = start
while i < len && is_html_space(text.unsafe_get(i)) {
i = i + 1
}
i
}
///|
/// A link destination: either `<...>` or a bare run with balanced parentheses.
fn parse_link_destination(text : String, start : Int) -> (String, Int)? {
let len = text.length()
guard start < len else { return None }
if text.unsafe_get(start) == '<' {
let mut i = start + 1
while i < len {
let c = text.unsafe_get(i)
if c == '>' {
return Some((text.unsafe_substring(start=start + 1, end=i), i + 1))
}
if c == '\n' || c == '<' {
return None
}
if c == '\\' && i + 1 < len {
i = i + 2
} else {
i = i + 1
}
}
return None
}
let mut i = start
let mut depth = 0
while i < len {
let c = text.unsafe_get(i)
if c == '\\' &&
i + 1 < len &&
is_punctuation(text.unsafe_get(i + 1).unsafe_to_char()) {
i = i + 2
continue
}
if is_html_space(c) || c < 0x20 {
break
}
if c == '(' {
depth = depth + 1
} else if c == ')' {
if depth == 0 {
break
}
depth = depth - 1
}
i = i + 1
}
if depth != 0 {
return None
}
Some((text.unsafe_substring(start~, end=i), i))
}
///|
/// A link title in `"…"`, `'…'` or `(…)`.
fn parse_link_title(text : String, start : Int) -> (String, Int)? {
let len = text.length()
guard start < len else { return None }
let open = text.unsafe_get(start)
let close : UInt16 = if open == '"' {
'"'
} else if open == '\'' {
'\''
} else if open == '(' {
')'
} else {
return None
}
let mut i = start + 1
while i < len {
let c = text.unsafe_get(i)
if c == '\\' && i + 1 < len {
i = i + 2
continue
}
if c == close {
return Some((text.unsafe_substring(start=start + 1, end=i), i + 1))
}
if open == '(' && c == '(' {
return None
}
i = i + 1
}
None
}
///|
/// Read the `[label]` that may follow a `]`, distinguishing the collapsed form
/// `[]` (empty label) from a full reference. Returns `None` for the shortcut
/// form, where nothing follows.
fn read_reference_label(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
while i < len {
let c = text.unsafe_get(i)
if c == '\\' && i + 1 < len {
i = i + 2
continue
}
if c == '[' {
return None
}
if c == ']' {
let label = text.unsafe_substring(start=start + 1, end=i)
if label.length() > 999 {
return None
}
return Some((label, i + 1))
}
i = i + 1
}
None
}
///|
/// Fold a link label for matching: trim, collapse internal whitespace, and
/// case-fold.
fn normalize_label(label : String) -> String {
let len = label.length()
let buf = StringBuilder()
let mut i = 0
let mut pending_space = false
let mut written = false
while i < len {
let c = label.unsafe_get(i)
if is_html_space(c) {
pending_space = written
i = i + 1
continue
}
if pending_space {
buf.write_char(' ')
pending_space = false
}
let ch = char_at_index(label, i)
let width = if ch.to_int() > 0xFFFF { 2 } else { 1 }
// A few characters fold to more than one: ß and ẞ both become "ss".
if ch == '\u{df}' || ch == '\u{1e9e}' {
buf.write_string("ss")
} else {
buf.write_char(fold_case(ch))
}
written = true
i = i + width
}
buf.to_string()
}
///|
/// Case folding for label matching. Covers the alphabets CommonMark's own test
/// suite exercises — ASCII, Latin-1, Latin Extended-A, Greek and Cyrillic —
/// and leaves anything else untouched.
fn fold_case(c : Char) -> Char {
let code = c.to_int()
if code >= 'A'.to_int() && code <= 'Z'.to_int() {
return (code + 32).unsafe_to_char()
}
if code >= 0xC0 && code <= 0xDE && code != 0xD7 {
return (code + 32).unsafe_to_char()
}
if code >= 0x100 && code <= 0x17F {
return fold_latin_extended_a(code)
}
if code >= 0x391 && code <= 0x3A9 && code != 0x3A2 {
return (code + 32).unsafe_to_char()
}
if code >= 0x400 && code <= 0x40F {
return (code + 80).unsafe_to_char()
}
if code >= 0x410 && code <= 0x42F {
return (code + 32).unsafe_to_char()
}
c
}
///|
/// Latin Extended-A is laid out as upper/lower pairs, but the parity flips
/// twice inside the block, so the sub-ranges have to be spelled out.
fn fold_latin_extended_a(code : Int) -> Char {
if code <= 0x137 || (code >= 0x14A && code <= 0x177) {
// Even is uppercase.
return (if code % 2 == 0 { code + 1 } else { code }).unsafe_to_char()
}
if (code >= 0x139 && code <= 0x148) || (code >= 0x179 && code <= 0x17E) {
// Odd is uppercase.
return (if code % 2 == 1 { code + 1 } else { code }).unsafe_to_char()
}
if code == 0x178 {
// Ÿ folds to ÿ, back in Latin-1.
return '\u{ff}'
}
// U+0138 ĸ, U+0149 ʼn and U+017F ſ have no simple pair here.
code.unsafe_to_char()
}
///|
/// Resolve backslash escapes and entity references in a destination, then
/// leave the result for the renderer to percent-encode.
fn normalize_destination(dest : String) -> String {
unescape_string(dest)
}
///|
/// Resolve backslash escapes and entity references.
fn unescape_string(s : String) -> String {
let len = s.length()
let mut needs = false
for i = 0; i < len; i = i + 1 {
let c = s.unsafe_get(i)
if c == '\\' || c == '&' {
needs = true
break
}
}
if !needs {
return s
}
let buf = StringBuilder()
let mut i = 0
while i < len {
let c = s.unsafe_get(i)
if c == '\\' &&
i + 1 < len &&
is_punctuation(s.unsafe_get(i + 1).unsafe_to_char()) {
buf.write_string(s.unsafe_substring(start=i + 1, end=i + 2))
i = i + 2
} else if c == '&' {
match decode_entity(s, i) {
Some((value, end)) => {
buf.write_string(value)
i = end
}
None => {
buf.write_char('&')
i = i + 1
}
}
} else {
buf.write_string(s.unsafe_substring(start=i, end=i + 1))
i = i + 1
}
}
buf.to_string()
}
///|
/// Split a wiki destination into page target and optional fragment.
fn split_wikilink_destination(destination : String) -> (String, String) {
let parts = destination.split("#").collect()
if parts.length() <= 1 {
(destination, "")
} else {
(parts[0].to_owned(), parts[1].to_owned())
}
}
///|
/// `[[target]]` or `[[target|label]]` (opt-in extension).
fn parse_wikilink(text : String, start : Int) -> (Inline, Int)? {
let len = text.length()
guard start + 1 < len && text.unsafe_get(start + 1) == '[' else {
return None
}
let target_buf = StringBuilder()
let label_buf = StringBuilder()
let mut in_label = false
let mut i = start + 2
while i < len {
let c = text.unsafe_get(i)
if c == ']' && i + 1 < len && text.unsafe_get(i + 1) == ']' {
let raw_target = target_buf.to_string().trim(chars=" \t").to_owned()
if raw_target.is_empty() {
return None
}
let raw_label = if in_label {
label_buf.to_string().trim(chars=" \t").to_owned()
} else {
""
}
let (target, fragment) = split_wikilink_destination(raw_target)
return Some(
(
Inline::WikiLink(
target~,
label=raw_label,
fragment~,
span=Span::new(start, i + 2),
),
i + 2,
),
)
}
if c == '\n' {
return None
}
if c == '[' && i + 1 < len && text.unsafe_get(i + 1) == '[' {
return None
}
if c == '|' && !in_label {
in_label = true
i = i + 1
continue
}
let piece = if c == '\\' && i + 1 < len {
let s = text.unsafe_substring(start=i + 1, end=i + 2)
i = i + 2
s
} else {
let s = text.unsafe_substring(start=i, end=i + 1)
i = i + 1
s
}
if in_label {
label_buf.write_string(piece)
} else {
target_buf.write_string(piece)
}
}
None
}
///|
/// `[^label]` footnote reference (GFM).
fn parse_footnote_reference(text : String, start : Int) -> (Inline, Int)? {
let len = text.length()
guard start + 1 < len && text.unsafe_get(start + 1) == '^' else {
return None
}
let mut i = start + 2
while i < len {
let c = text.unsafe_get(i)
if c == ']' {
break
}
if c == '[' || c == '\n' || c == '\r' || c == 0 {
return None
}
i = i + 1
}
guard i < len && i > start + 2 else { return None }
let label = text
.unsafe_substring(start=start + 2, end=i)
.trim(chars=" \t")
.to_owned()
guard !label.is_empty() else { return None }
Some((Inline::FootnoteReference(label~, span=Span::new(start, i + 1)), i + 1))
}