use crate::parser::inline::emphasis::can_close;
use std::cell::RefCell;
const UNKNOWN: usize = usize::MAX;
const NEEDS_ATTENTION: [bool; 256] = {
let mut t = [false; 256];
let mut b = 0usize;
while b < 256 {
t[b] = matches!(
b as u8,
b'\\' | b'*' | b'_' | b'[' | b']' | b'(' | b')' | b'"' | b'\''
) || !is_destination_byte(b as u8);
b += 1;
}
t
};
const fn is_destination_byte(b: u8) -> bool {
!(b.is_ascii_control() || b == b' ' || b == b'<')
}
pub(crate) const EMPHASIS_TAGS: [&str; 6] = ["***", "___", "**", "__", "*", "_"];
const MAX_BRACKET_DEPTH: usize = 32;
pub(crate) struct InlineIndex {
base: usize,
len: usize,
closers: [Vec<usize>; 6],
brackets: Vec<(usize, usize)>,
close_brackets: Vec<usize>,
parens: Vec<(usize, usize)>,
title_ends: [Vec<usize>; 3],
destination_ends: RefCell<Vec<usize>>,
}
impl InlineIndex {
pub(crate) fn build(input: &str) -> Self {
let bytes = input.as_bytes();
let mut closers: [Vec<usize>; 6] = Default::default();
let mut brackets = Vec::new();
let mut close_brackets = Vec::new();
let mut parens = Vec::new();
let mut title_ends: [Vec<usize>; 3] = Default::default();
let mut bracket_stack: Vec<usize> = Vec::new();
let mut paren_stack: Vec<usize> = Vec::new();
let mut escaped = false;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if !escaped && !NEEDS_ATTENTION[b as usize] {
i += 1;
while i < bytes.len() && !NEEDS_ATTENTION[bytes[i] as usize] {
i += 1;
}
continue;
}
if escaped {
escaped = false;
if b == b'*' || b == b'_' {
push_closers(&mut closers, input, i, true);
}
i += 1;
continue;
}
match b {
b'\\' => escaped = true,
b'*' | b'_' => push_closers(&mut closers, input, i, false),
b'[' => {
if bracket_stack.len() <= MAX_BRACKET_DEPTH {
bracket_stack.push(i);
}
}
b']' => {
close_brackets.push(i);
if let Some(open) = bracket_stack.pop() {
brackets.push((open, i));
}
}
b'(' => paren_stack.push(i),
b')' => {
title_ends[2].push(i);
if let Some(open) = paren_stack.pop() {
parens.push((open, i));
}
}
b'"' => title_ends[0].push(i),
b'\'' => title_ends[1].push(i),
_ => {
if !is_destination_char(b as char) {
paren_stack.clear();
}
}
}
i += 1;
}
brackets.sort_unstable_by_key(|&(open, _)| open);
parens.sort_unstable_by_key(|&(open, _)| open);
Self {
base: input.as_ptr() as usize,
len: input.len(),
closers,
brackets,
close_brackets,
parens,
title_ends,
destination_ends: RefCell::new(Vec::new()),
}
}
pub(crate) fn title_end(&self, at: &str, delim: u8) -> Option<Option<usize>> {
let from = self.offset(at)?;
let list = match delim {
b'"' => &self.title_ends[0],
b'\'' => &self.title_ends[1],
b')' => &self.title_ends[2],
_ => return None,
};
let idx = list.partition_point(|&c| c <= from);
let Some(&close) = list.get(idx) else {
return Some(None);
};
Some((close < from + at.len()).then_some(close - from))
}
fn offset(&self, s: &str) -> Option<usize> {
let ptr = s.as_ptr() as usize;
if ptr < self.base || ptr + s.len() > self.base + self.len {
return None;
}
Some(ptr - self.base)
}
pub(crate) fn emphasis_content_len(&self, content: &str, tag: &str) -> Option<Option<usize>> {
let from = self.offset(content)?;
let k = EMPHASIS_TAGS.iter().position(|t| *t == tag)?;
let list = &self.closers[k];
let idx = list.partition_point(|&c| c < from);
let Some(&closer) = list.get(idx) else {
return Some(None);
};
if closer >= from + content.len() || closer == from {
return Some(None);
}
Some(Some(closer - from))
}
pub(crate) fn covers(&self, at: &str) -> bool {
self.offset(at).is_some()
}
pub(crate) fn bracket_match(&self, at: &str) -> Option<usize> {
let from = self.offset(at)?;
let close = lookup(&self.brackets, from)?;
(close < from + at.len()).then_some(close - from)
}
pub(crate) fn next_close_bracket(&self, at: &str) -> Option<usize> {
let from = self.offset(at)?;
let idx = self.close_brackets.partition_point(|&c| c < from);
let close = *self.close_brackets.get(idx)?;
(close < from + at.len()).then_some(close - from)
}
pub(crate) fn destination_len(&self, at: &str) -> usize {
let Some(from) = self.offset(at) else {
return destination_len_slow(at);
};
let limit = from + at.len();
let mut memo = self.destination_ends.borrow_mut();
if memo.is_empty() {
memo.resize(self.len, UNKNOWN);
}
let mut pos = from;
let end = loop {
if pos >= limit {
break limit;
}
let known = memo[pos];
if known != UNKNOWN {
break known.min(limit);
}
let rest = &at[pos - from..];
let c = rest.chars().next().unwrap();
match c {
'\\' => pos += escape_len(rest),
'(' => match lookup(&self.parens, pos) {
Some(close) if close < limit => pos = close + 1,
_ => break pos,
},
')' => break pos,
c if is_destination_char(c) => pos += c.len_utf8(),
_ => break pos,
}
};
for slot in &mut memo[from..pos.min(self.len)] {
if *slot == UNKNOWN {
*slot = end;
}
}
end - from
}
}
fn lookup(pairs: &[(usize, usize)], key: usize) -> Option<usize> {
pairs
.binary_search_by_key(&key, |&(open, _)| open)
.ok()
.map(|i| pairs[i].1)
}
fn push_closers(closers: &mut [Vec<usize>; 6], input: &str, i: usize, escaped: bool) {
let rest = &input[i..];
let marker = rest.chars().next().unwrap();
if escaped && marker == '*' {
return;
}
for (k, tag) in EMPHASIS_TAGS.iter().enumerate() {
if tag.starts_with(marker) && rest.starts_with(tag) {
let next = rest[tag.len()..].chars().next();
if can_close(marker, next) {
closers[k].push(i);
}
}
}
}
fn escape_len(rest: &str) -> usize {
1 + rest[1..].chars().next().map_or(0, char::len_utf8)
}
pub(crate) fn is_destination_char(c: char) -> bool {
!c.is_ascii_control() && c != ' ' && c != '<'
}
pub(crate) fn destination_len_slow(at: &str) -> usize {
let bytes = at.as_bytes();
let mut pos = 0;
while pos < bytes.len() {
match bytes[pos] {
b'\\' => pos += escape_len(&at[pos..]),
b'(' => match paren_group_len(&at[pos..]) {
Some(len) => pos += len,
None => break,
},
b')' => break,
b if is_destination_byte(b) => pos += 1,
_ => break,
}
}
pos
}
fn paren_group_len(at: &str) -> Option<usize> {
let bytes = at.as_bytes();
let mut depth = 0usize;
let mut pos = 0;
while pos < bytes.len() {
match bytes[pos] {
b'\\' => pos += escape_len(&at[pos..]),
b'(' => {
depth += 1;
pos += 1;
}
b')' => {
depth -= 1;
pos += 1;
if depth == 0 {
return Some(pos);
}
}
b if is_destination_byte(b) => pos += 1,
_ => return None,
}
}
None
}