use std::ops::Range;
use crate::{
Constructs, syntax,
syntax::unicode::{is_punctuation, is_whitespace},
};
use super::{PN, Tokenizer};
#[derive(Clone, Copy)]
pub struct Delim {
pub node: usize,
pub marker: u8,
pub remaining: usize,
pub can_open: bool,
pub can_close: bool,
}
pub struct Formed {
pub r: Range<usize>,
pub marker: u8,
pub len: u8,
}
impl Tokenizer<'_, '_> {
pub(crate) fn delimiter_run(&mut self) {
let bytes = self.t.as_bytes();
let marker = bytes[self.pos];
let start = self.pos;
let len = syntax::run_len(&bytes[start..], marker);
let prev = self.t[..start].chars().next_back();
let next = self.t[start + len..].chars().next();
let run = Run::new(marker, len, prev, next, self.constructs);
self.flush_text(start);
self.nodes.push(PN::Text(start..start + len));
if run.valid {
self.first_delim_tilde.get_or_insert(marker == b'~');
}
if run.can_open || run.can_close {
self.delims.push(Delim {
node: self.nodes.len() - 1,
marker,
remaining: len,
can_open: run.can_open,
can_close: run.can_close,
});
}
self.pos = start + len;
self.text_start = self.pos;
}
pub(crate) fn process_emphasis(&mut self, bottom: usize) {
resolve(&mut self.nodes, &mut self.delims[bottom..], true, &mut self.formed);
}
pub(crate) fn process_emphasis_final(&mut self) {
let tilde_first = self.first_delim_tilde.unwrap_or(false);
resolve(&mut self.nodes, &mut self.delims, tilde_first, &mut self.formed);
}
}
fn resolve(nodes: &mut [PN], delims: &mut [Delim], tilde_first: bool, formed: &mut Vec<Formed>) {
let order = if tilde_first { [true, false] } else { [false, true] };
for tilde_pass in order {
if delims.iter().any(|d| d.remaining > 0 && d.can_close && (d.marker == b'~') == tilde_pass)
{
pass(nodes, delims, tilde_pass, formed);
}
}
}
#[expect(clippy::cast_possible_truncation)] fn pass(nodes: &mut [PN], delims: &mut [Delim], tilde_pass: bool, formed: &mut Vec<Formed>) {
let mut openers_bottom = [[[0; 2]; 3]; 2];
let mut tilde_bottom = [0; 3];
let mut closer = 0;
while closer < delims.len() {
let c = &delims[closer];
if (c.marker == b'~') != tilde_pass || !c.can_close || c.remaining == 0 {
closer += 1;
continue;
}
let class = (usize::from(c.marker == b'_'), c.remaining % 3, usize::from(c.can_open));
let scan_bottom = if tilde_pass {
tilde_bottom[c.remaining]
} else {
openers_bottom[class.0][class.1][class.2]
};
let mut opener = None;
let mut i = closer;
while i > scan_bottom {
i -= 1;
let o = &delims[i];
if o.marker != c.marker || !o.can_open || o.remaining == 0 {
continue;
}
if tilde_pass {
if o.remaining == c.remaining {
opener = Some(i);
break;
}
continue;
}
let forbidden = (o.can_close || c.can_open)
&& !c.remaining.is_multiple_of(3)
&& (o.remaining + c.remaining).is_multiple_of(3);
if !forbidden {
opener = Some(i);
break;
}
}
let Some(opener) = opener else {
if tilde_pass {
tilde_bottom[c.remaining] = closer;
} else {
openers_bottom[class.0][class.1][class.2] = closer;
}
if !delims[closer].can_open {
delims[closer].remaining = 0;
}
closer += 1;
continue;
};
let use_n = if tilde_pass {
delims[closer].remaining
} else if delims[opener].remaining >= 2 && delims[closer].remaining >= 2 {
2
} else {
1
};
let opener_node = delims[opener].node;
let closer_node = delims[closer].node;
let between = &mut delims[opener + 1..closer];
if between.iter().any(|d| d.remaining > 0 && (d.marker == b'~') != tilde_pass) {
for d in between.iter_mut() {
if (d.marker == b'~') == tilde_pass {
d.remaining = 0;
}
}
resolve(nodes, between, true, formed);
}
for d in between.iter_mut() {
d.remaining = 0;
}
let o_end = text_range(nodes, opener_node).end;
text_range(nodes, opener_node).end = o_end - use_n;
let c_start = text_range(nodes, closer_node).start;
text_range(nodes, closer_node).start = c_start + use_n;
formed.push(Formed {
r: o_end - use_n..c_start + use_n,
marker: delims[opener].marker,
len: use_n as u8,
});
delims[opener].remaining -= use_n;
delims[closer].remaining -= use_n;
if !tilde_pass && delims[opener].remaining > 0 {
let m = usize::from(delims[opener].marker == b'_');
for b in openers_bottom[m].iter_mut().flatten() {
*b = (*b).min(opener);
}
}
if delims[closer].remaining == 0 {
closer += 1;
}
}
}
fn text_range(nodes: &mut [PN], node: usize) -> &mut Range<usize> {
match &mut nodes[node] {
PN::Text(r) => r,
_ => unreachable!("delimiter nodes are text runs"),
}
}
pub fn build(nodes: impl IntoIterator<Item = PN>, formed: &mut Vec<Formed>) -> Vec<PN> {
formed.sort_unstable_by(|a, b| a.r.start.cmp(&b.r.start).then(b.r.end.cmp(&a.r.end)));
let mut pending = formed.drain(..).peekable();
let mut out = Vec::new();
let mut stack: Vec<(Formed, Vec<PN>)> = Vec::new();
let close = |stack: &mut Vec<(Formed, Vec<PN>)>, out: &mut Vec<PN>| {
let (pair, children) = stack.pop().expect("a pair is open");
let node = PN::Emph { marker: pair.marker, len: pair.len, children, r: pair.r };
stack.last_mut().map_or(out, |(_, children)| children).push(node);
};
for node in nodes {
let at = node.start();
while stack.last().is_some_and(|(pair, _)| pair.r.end <= at) {
close(&mut stack, &mut out);
}
while pending.peek().is_some_and(|pair| pair.r.start <= at) {
stack.push((pending.next().expect("peeked"), Vec::new()));
}
if matches!(&node, PN::Text(r) if r.is_empty()) {
continue;
}
stack.last_mut().map_or(&mut out, |(_, children)| children).push(node);
}
debug_assert_eq!(pending.len(), 0, "every pair starts before the last node");
while !stack.is_empty() {
close(&mut stack, &mut out);
}
out
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Run {
pub marker: u8,
pub len: usize,
pub can_open: bool,
pub can_close: bool,
pub valid: bool,
}
impl Run {
pub fn new(
marker: u8,
len: usize,
prev: Option<char>,
next: Option<char>,
constructs: &Constructs,
) -> Self {
let min = if constructs.gfm_strikethrough_single_tilde { 1 } else { 2 };
let valid = marker != b'~' || (min..=2).contains(&len);
let (can_open, can_close) = if valid {
classify(marker, prev, next, constructs.gfm_strikethrough)
} else {
(false, false)
};
Self { marker, len, can_open, can_close, valid }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Pair {
pub opener: usize,
pub closer: usize,
pub strong: bool,
}
pub fn pairs(runs: &[Run], in_link: bool) -> Vec<Pair> {
let tilde_first = in_link || runs.iter().find(|r| r.valid).is_some_and(|r| r.marker == b'~');
let mut nodes = Vec::with_capacity(runs.len() * 2);
let mut delims = Vec::new();
let mut starts = Vec::with_capacity(runs.len());
let mut pos = 0;
for run in runs {
if !nodes.is_empty() {
nodes.push(PN::Text(pos..pos + 1));
pos += 1;
}
starts.push(pos);
if run.can_open || run.can_close {
delims.push(Delim {
node: nodes.len(),
marker: run.marker,
remaining: run.len,
can_open: run.can_open,
can_close: run.can_close,
});
}
nodes.push(PN::Text(pos..pos + run.len));
pos += run.len;
}
let mut formed = Vec::new();
resolve(&mut nodes, &mut delims, tilde_first, &mut formed);
let run_at = |offset: usize| starts.partition_point(|&s| s <= offset) - 1;
let mut out: Vec<Pair> = formed
.iter()
.map(|pair| Pair {
opener: run_at(pair.r.start),
closer: run_at(pair.r.end - 1),
strong: pair.marker != b'~' && pair.len == 2,
})
.collect();
out.sort_unstable();
out
}
pub fn classify(marker: u8, prev: Option<char>, next: Option<char>, tilde: bool) -> (bool, bool) {
let is_marker = |c: Option<char>| {
marker != b'~' && (matches!(c, Some('*' | '_')) || (tilde && c == Some('~')))
};
let before = group(prev);
let after = group(next);
let open = after == 0 || (after == 2 && before != 0) || is_marker(next);
let close = before == 0 || (before == 2 && after != 0) || is_marker(prev);
if marker == b'_' {
(open && (before != 0 || !close), close && (after != 0 || !open))
} else {
(open, close)
}
}
fn group(c: Option<char>) -> u8 {
match c {
None => 1,
Some(c) if is_whitespace(c) => 1,
Some(c) if is_punctuation(c) => 2,
Some(_) => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run(marker: u8, len: usize, prev: Option<char>, next: Option<char>) -> Run {
Run::new(marker, len, prev, next, &Constructs::markdown())
}
#[test]
fn pairs_nested_and_rule_of_three() {
let runs = [run(b'*', 3, None, Some('b')), run(b'*', 3, Some('b'), None)];
assert_eq!(
pairs(&runs, false),
vec![
Pair { opener: 0, closer: 1, strong: false },
Pair { opener: 0, closer: 1, strong: true }
]
);
let runs = [
run(b'*', 1, None, Some('a')),
run(b'*', 2, Some('a'), Some('b')),
run(b'*', 1, Some('b'), None),
];
assert_eq!(pairs(&runs, false), vec![Pair { opener: 0, closer: 2, strong: false }]);
let runs = [
run(b'*', 2, None, Some('a')),
run(b'_', 1, Some(' '), Some('*')),
run(b'*', 2, Some('_'), Some('b')),
run(b'*', 2, Some('b'), Some('_')),
run(b'_', 1, Some('*'), Some(' ')),
run(b'*', 2, Some('c'), None),
];
assert_eq!(
pairs(&runs, false),
vec![
Pair { opener: 0, closer: 2, strong: true },
Pair { opener: 3, closer: 5, strong: true }
]
);
}
#[test]
fn pairs_family_order() {
let runs = [
run(b'~', 2, None, Some('*')),
run(b'*', 1, Some('~'), Some('a')),
run(b'~', 2, Some('a'), Some('*')),
run(b'*', 1, Some('~'), None),
];
assert_eq!(pairs(&runs, true), vec![Pair { opener: 0, closer: 2, strong: false }]);
}
}