///| Emphasis and strong emphasis via the CommonMark delimiter stack.
///|
/// Read the code point ending just before `i`.
fn char_before(text : String, i : Int) -> Char {
let lo = text.unsafe_get(i - 1).to_int()
if lo >= 0xDC00 && lo <= 0xDFFF && i >= 2 {
let hi = text.unsafe_get(i - 2).to_int()
if hi >= 0xD800 && hi <= 0xDBFF {
return (0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00)).unsafe_to_char()
}
}
lo.unsafe_to_char()
}
///|
/// Read the code point starting at `i`.
fn char_at_index(text : String, i : Int) -> Char {
let hi = text.unsafe_get(i).to_int()
if hi >= 0xD800 && hi <= 0xDBFF && i + 1 < text.length() {
let lo = text.unsafe_get(i + 1).to_int()
if lo >= 0xDC00 && lo <= 0xDFFF {
return (0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00)).unsafe_to_char()
}
}
hi.unsafe_to_char()
}
///|
/// Push the run of `ch` starting at `start` onto the delimiter stack and
/// return the index just past it.
fn InlineCtx::push_delimiter_run(
self : InlineCtx,
start : Int,
ch : Char,
) -> Int {
let text = self.text
let len = self.len
let mut end = start
while end < len && text.unsafe_get(end).to_int() == ch.to_int() {
end = end + 1
}
let count = end - start
let before_ws = start == 0 || is_unicode_whitespace(char_before(text, start))
let before_punct = start > 0 &&
is_unicode_punctuation(char_before(text, start))
let after_ws = end >= len || is_unicode_whitespace(char_at_index(text, end))
let after_punct = end < len &&
is_unicode_punctuation(char_at_index(text, end))
let left_flanking = !after_ws && (!after_punct || before_ws || before_punct)
let right_flanking = !before_ws && (!before_punct || after_ws || after_punct)
let (can_open, can_close) = if ch == '_' {
(
left_flanking && (!right_flanking || before_punct),
right_flanking && (!left_flanking || after_punct),
)
} else {
(left_flanking, right_flanking)
}
let idx = self.append({
inline: None,
mergeable: false,
ch,
count,
orig: count,
can_open,
can_close,
start,
prev: -1,
next: -1,
})
self.delims.push(idx)
end
}
///|
/// Key for the `openers_bottom` table: delimiters can only be reconsidered
/// per character, per closer length modulo three, and per "can also open".
fn openers_bottom_key(ch : Char, closer_len : Int, can_open : Bool) -> Int {
let base = match ch {
'*' => 0
'_' => 1
_ => 2
}
base * 8 + closer_len % 3 * 2 + (if can_open { 1 } else { 0 })
}
///|
/// Turn stacked `*`/`_`/`~` runs at or above `bottom` into emphasis nodes.
fn InlineCtx::process_emphasis(self : InlineCtx, bottom : Int) -> Unit {
let openers_bottom : Map[Int, Int] = Map([], capacity=8)
let mut closer_i = bottom
while closer_i < self.delims.length() {
let closer_slot = self.delims[closer_i]
if closer_slot < 0 {
closer_i = closer_i + 1
continue
}
let closer = self.slots[closer_slot]
if !closer.can_close || closer.count == 0 {
closer_i = closer_i + 1
continue
}
let key = openers_bottom_key(closer.ch, closer.orig, closer.can_open)
let limit = match openers_bottom.get(key) {
Some(v) => v
None => bottom
}
let mut opener_i = closer_i - 1
let mut found = -1
while opener_i >= limit {
let opener_slot = self.delims[opener_i]
if opener_slot < 0 {
opener_i = opener_i - 1
continue
}
let opener = self.slots[opener_slot]
if opener.count > 0 && opener.can_open && opener.ch == closer.ch {
// "Rule of three": runs that can both open and close only match when
// the combined lengths are not a multiple of three, unless both are.
let both_ways = closer.can_open || opener.can_close
let blocked = both_ways &&
(opener.orig + closer.orig) % 3 == 0 &&
!(opener.orig % 3 == 0 && closer.orig % 3 == 0)
if !blocked {
found = opener_i
break
}
}
opener_i = opener_i - 1
}
if found < 0 {
openers_bottom[key] = closer_i
if !closer.can_open {
self.delims[closer_i] = -1
}
closer_i = closer_i + 1
continue
}
let opener_slot = self.delims[found]
let opener = self.slots[opener_slot]
if closer.ch == '~' {
self.wrap_delimiters(found, closer_i, 2, true)
} else {
let use_count = if opener.count >= 2 && closer.count >= 2 { 2 } else { 1 }
self.wrap_delimiters(found, closer_i, use_count, false)
}
if self.slots[closer_slot].count == 0 {
self.delims[closer_i] = -1
closer_i = closer_i + 1
}
}
// Everything at or above `bottom` has now had its chance to match.
let mut i = bottom
while i < self.delims.length() {
self.delims[i] = -1
i = i + 1
}
while self.delims.length() > bottom {
let _ = self.delims.pop()
}
}
///|
/// Wrap the slots between two delimiter runs in an emphasis node.
fn InlineCtx::wrap_delimiters(
self : InlineCtx,
opener_i : Int,
closer_i : Int,
use_count : Int,
strike : Bool,
) -> Unit {
let opener_slot = self.delims[opener_i]
let closer_slot = self.delims[closer_i]
let opener = self.slots[opener_slot]
let closer = self.slots[closer_slot]
let children = self.materialize(opener.next, closer_slot)
let mut cur = opener.next
while cur >= 0 && cur != closer_slot {
let next = self.slots[cur].next
self.unlink(cur)
cur = next
}
let content_start = opener.start + opener.count - use_count
let content_end = closer.start + closer.orig - closer.count + use_count
let span = Span::new(content_start, content_end)
let marker = if opener.ch == '_' {
EmphasisMarker::Underscore
} else {
EmphasisMarker::Asterisk
}
let node = if strike {
Inline::Strikethrough(children~, span~)
} else if use_count == 2 {
Inline::Strong(marker~, children~, span~)
} else {
Inline::Emphasis(marker~, children~, span~)
}
let _ = self.insert_after(opener_slot, new_inline_slot(node, content_start))
self.slots[opener_slot].count = opener.count - use_count
self.slots[closer_slot].count = closer.count - use_count
if self.slots[opener_slot].count == 0 {
self.unlink(opener_slot)
self.delims[opener_i] = -1
}
if self.slots[closer_slot].count == 0 {
self.unlink(closer_slot)
}
// Delimiters between the pair can never match anything now.
let mut j = opener_i + 1
while j < closer_i {
self.delims[j] = -1
j = j + 1
}
}