pub fn count_of(text: &str, needle: &str) -> usize {
let needle = needle.as_bytes();
let mut count = 0;
let mut rest = text.as_bytes();
while let Some(pos) = memchr::memmem::find(rest, needle) {
count += 1;
rest = &rest[pos + needle.len()..];
}
count
}
pub fn is_ws(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\r' | '\u{000c}' | '\u{000b}')
}
pub fn js_trim(s: &str) -> &str {
let start = s
.char_indices()
.find(|(_, c)| !c.is_whitespace())
.map(|(i, _)| i)
.unwrap_or(s.len());
let end = s
.char_indices()
.rev()
.find(|(_, c)| !c.is_whitespace())
.map(|(i, c)| i + c.len_utf8())
.unwrap_or(start);
&s[start..end]
}
pub fn trailing_ws_offset(s: &str) -> usize {
let mut end = s.len();
for (i, c) in s.char_indices().rev() {
if is_ws(c) {
end = i;
} else {
break;
}
}
end
}
pub fn find_closed_code_block_ranges(content: &str) -> Vec<(usize, usize)> {
let bytes = content.as_bytes();
let mut ranges = Vec::new();
let mut search = 0;
while let Some(rel) = memchr::memmem::find(&bytes[search..], b"```") {
let start = search + rel;
let after = start + 3;
let Some(rel2) = memchr::memmem::find(&bytes[after..], b"```") else {
break;
};
let end = after + rel2 + 3;
ranges.push((start, end));
search = end;
}
ranges
}
pub fn is_position_in_ranges(position: usize, ranges: &[(usize, usize)]) -> bool {
ranges
.iter()
.any(|&(start, end)| position >= start && position < end)
}
pub fn is_range_overlapping_ranges(start: usize, end: usize, ranges: &[(usize, usize)]) -> bool {
ranges.iter().any(|&(rs, re)| {
(start >= rs && start < re) || (end > rs && end <= re) || (start < rs && end > re)
})
}
pub(crate) fn is_triple_backtick_at(bytes: &[u8], i: usize) -> bool {
bytes[i] == b'`' && bytes.get(i + 1) == Some(&b'`') && bytes.get(i + 2) == Some(&b'`')
}
pub fn is_within_code_block(text: &str, position: usize) -> bool {
let bytes = text.as_bytes();
let mut in_code = false;
let mut i = 0;
while i < position && i < bytes.len() {
if is_triple_backtick_at(bytes, i) {
in_code = !in_code;
i += 3;
} else {
i += 1;
}
}
in_code
}
pub fn is_inside_unclosed_code_block(content: &str) -> bool {
is_within_code_block(content, content.len())
}
pub fn last_paragraph_range(content: &str, skip_trailing_empty: bool) -> (usize, usize) {
let lines: Vec<&str> = content.split('\n').collect();
let mut start_line = 0;
for i in (0..lines.len()).rev() {
if skip_trailing_empty && i == lines.len() - 1 && lines[i].trim().is_empty() {
continue;
}
if lines[i].trim().is_empty() {
start_line = i + 1;
break;
}
}
let offset = if start_line == 0 {
0
} else {
(lines[..start_line].join("\n").len() + 1).min(content.len())
};
(start_line, offset)
}
pub fn last_paragraph(content: &str, skip_trailing_empty: bool) -> &str {
let (_start_line, offset) = last_paragraph_range(content, skip_trailing_empty);
&content[offset..]
}
pub fn last_non_empty_line_index(lines: &[&str]) -> isize {
for (i, line) in lines.iter().enumerate().rev() {
if !line.trim().is_empty() {
return i as isize;
}
}
-1
}
pub(crate) fn is_backtick_part_of_triple(text: &str, index: usize) -> bool {
let mut back = text[..index].chars().rev();
let before = back.next();
let before2 = back.next();
let after = text.get(index + 1..).and_then(|s| s.chars().next());
let after2 = text.get(index + 2..).and_then(|s| s.chars().next());
let c = |o: Option<char>| o == Some('`');
(c(before) && c(before2)) || (c(before) && c(after)) || (c(after) && c(after2))
}
pub fn find_inline_code_ranges(
content: &str,
code_block_ranges: &[(usize, usize)],
) -> Vec<(usize, usize)> {
let mut positions: Vec<usize> = Vec::new();
let bytes = content.as_bytes();
for i in 0..bytes.len() {
if is_position_in_ranges(i, code_block_ranges) {
continue;
}
if bytes[i] != b'`' {
continue;
}
if is_backtick_part_of_triple(content, i) {
continue;
}
positions.push(i);
}
let mut ranges = Vec::new();
for pair in positions.chunks(2) {
if pair.len() == 2 {
ranges.push((pair[0], pair[1] + 1));
}
}
ranges
}
pub fn mask_inline_code_markdown_markers(content: &str, ranges: &[(usize, usize)]) -> String {
if ranges.is_empty() {
return content.to_string();
}
let mut bytes = content.as_bytes().to_vec();
for &(start, end) in ranges {
let range = start..end.min(bytes.len());
for b in &mut bytes[range] {
if *b == b'*' || *b == b'_' || *b == b'~' {
*b = b' ';
}
}
}
String::from_utf8(bytes).expect("masking preserves utf8")
}
pub fn is_escaped_character(content: &str, index: usize) -> bool {
let mut backslashes = 0;
let bytes = content.as_bytes();
let mut i = index;
while i > 0 && bytes[i - 1] == b'\\' {
backslashes += 1;
i -= 1;
}
backslashes % 2 == 1
}
fn is_word_char(c: char) -> bool {
c.is_alphanumeric()
}
pub fn is_underscore_inside_word(content: &str, start: usize, len: usize) -> bool {
let prev = content.get(..start).and_then(|s| s.chars().next_back());
let next = content.get(start + len..).and_then(|s| s.chars().next());
prev.is_some_and(is_word_char) && next.is_some_and(is_word_char)
}
pub fn should_ignore_underscore_marker(content: &str, start: usize, len: usize) -> bool {
is_escaped_character(content, start) || is_underscore_inside_word(content, start, len)
}
pub fn mask_invalid_underscore_markers(content: &str) -> String {
let mut bytes = content.as_bytes().to_vec();
let mut index = 0;
while index < bytes.len() {
if bytes[index] != b'_' {
index += 1;
continue;
}
let run_start = index;
while index < bytes.len() && bytes[index] == b'_' {
index += 1;
}
let run_len = index - run_start;
if should_ignore_underscore_marker(content, run_start, run_len) {
for b in &mut bytes[run_start..index] {
*b = b' ';
}
}
}
String::from_utf8(bytes).expect("masking preserves utf8")
}
fn replace_complete_links(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut i = 0;
while i < text.len() {
let ch = text[i..].chars().next().expect("char at boundary");
let rest = &text[i..];
let is_image = rest.starts_with("![");
if ch == '[' || (is_image && ch == '!') {
let label_start = i + usize::from(is_image);
if let Some(close_rel) = memchr::memchr(b']', &text.as_bytes()[label_start..]) {
let after_label = label_start + close_rel + 1;
if text[after_label..].starts_with('(') {
let paren_rest = &text[after_label + 1..];
if let Some(cp) = memchr::memchr(b')', paren_rest.as_bytes()) {
out.push_str(&text[i..after_label]);
out.push_str("()");
i = after_label + 1 + cp + 1;
continue;
}
}
}
}
out.push(ch);
i += ch.len_utf8();
}
out
}
fn replace_incomplete_link_suffix(text: &str) -> String {
let bytes = text.as_bytes();
let mut best: Option<usize> = None;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'[' || (bytes[i] == b'!' && bytes.get(i + 1) == Some(&b'[')) {
let label_start = if bytes[i] == b'!' { i + 1 } else { i };
if let Some(rel) = memchr::memchr(b']', &text.as_bytes()[label_start..]) {
let after = label_start + rel + 1;
if text[after..].starts_with('(') {
let rest = &text[after + 1..];
if !rest.contains(')') && !rest.is_empty() {
best = Some(after - 1);
i = text.len();
continue;
}
}
}
}
i += 1;
}
if let Some(pos) = best {
let mut out = text[..pos].to_string();
out.push_str("](");
out
} else {
text.to_string()
}
}
pub fn remove_urls_from_text(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let ranges = find_closed_code_block_ranges(text);
let mut cursor = 0;
for &(start, end) in &ranges {
result.push_str(&text[cursor..start]);
cursor = end;
}
result.push_str(&text[cursor..]);
let mut no_html = String::with_capacity(result.len());
let mut i = 0;
while i < result.len() {
let ch = result[i..].chars().next().expect("char at boundary");
if ch == '<' {
if let Some(rel) = memchr::memchr(b'>', &result.as_bytes()[i..]) {
no_html.push(' ');
i += rel + 1;
continue;
}
}
no_html.push(ch);
i += ch.len_utf8();
}
let after_complete = replace_complete_links(&no_html);
replace_incomplete_link_suffix(&after_complete)
}
pub fn remove_math_blocks_from_text(text: &str, single_dollar_enabled: bool) -> String {
let mut bytes = text.as_bytes().to_vec();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && bytes.get(i + 1) == Some(&b'$') {
i += 2;
continue;
}
if bytes[i] == b'$' && bytes.get(i + 1) == Some(&b'$') {
let mut j = i + 2;
let mut closing = None;
while j + 1 < bytes.len() {
if bytes[j] == b'$' && bytes[j + 1] == b'$' {
closing = Some(j);
break;
}
j += 1;
}
if let Some(pos) = closing {
bytes.drain(i..pos + 2);
continue; } else {
bytes.truncate(i);
break;
}
}
if single_dollar_enabled && bytes[i] == b'$' {
let mut j = i + 1;
let mut closing = None;
while j < bytes.len() {
if bytes[j] == b'$' && bytes.get(j - 1) != Some(&b'\\') {
closing = Some(j);
break;
}
j += 1;
}
if let Some(pos) = closing {
bytes.drain(i..pos + 1);
continue;
} else {
bytes.truncate(i);
break;
}
}
i += 1;
}
String::from_utf8(bytes).expect("math removal preserves utf8")
}
pub fn is_within_math_block(text: &str, position: usize, single_dollar_enabled: bool) -> bool {
let mut in_block = false;
let mut in_inline = false;
let bytes = text.as_bytes();
let mut i = 0;
while i < position && i < text.len() {
if bytes[i] == b'\\' && bytes.get(i + 1) == Some(&b'$') {
i += 2;
continue;
}
if bytes[i] == b'$' && bytes.get(i + 1) == Some(&b'$') {
in_block = !in_block;
i += 2;
continue;
}
if single_dollar_enabled && !in_block && bytes[i] == b'$' {
in_inline = !in_inline;
}
i += 1;
}
in_block || in_inline
}
pub fn join_multiline_math(content: &str) -> String {
if !content.contains("$$") {
return content.to_string();
}
let code = find_closed_code_block_ranges(content);
let mut out = String::with_capacity(content.len());
let mut copied = 0;
let mut from = 0;
while let Some(open) = next_double_dollar(content, from, &code) {
let Some(close) = next_double_dollar(content, open + 2, &code) else {
break;
};
let span = &content[open..close + 2];
if span.contains('\n') {
out.push_str(&content[copied..open]);
let mut lines = span.lines().map(str::trim).filter(|line| !line.is_empty());
if let Some(first) = lines.next() {
out.push_str(first);
}
for line in lines {
out.push(' ');
out.push_str(line);
}
copied = close + 2;
}
from = close + 2;
}
out.push_str(&content[copied..]);
out
}
fn next_double_dollar(content: &str, mut from: usize, code: &[(usize, usize)]) -> Option<usize> {
let bytes = content.as_bytes();
loop {
let at = from + memchr::memmem::find(&bytes[from..], b"$$")?;
if at > 0 && bytes[at - 1] == b'\\' {
from = at + 1;
continue;
}
if let Some(&(_, end)) = code.iter().find(|&&(start, end)| start <= at && at < end) {
from = end;
continue;
}
return Some(at);
}
}
pub fn is_within_link_or_image_url(text: &str, position: usize) -> bool {
let bytes = text.as_bytes();
let mut i = position;
while i > 0 {
i -= 1;
match bytes[i] {
b')' => return false,
b'(' if i > 0 && bytes[i - 1] == b']' => return true,
b'(' => return false,
b'\n' => return false,
_ => {}
}
}
false
}
pub fn is_within_html_tag(text: &str, position: usize) -> bool {
let mut in_tag = false;
let bytes = text.as_bytes();
let mut i = 0;
while i < position && i < text.len() {
if bytes[i] == b'<' && (i == 0 || bytes[i - 1] != b'\\') {
in_tag = true;
} else if bytes[i] == b'>' && in_tag && (i == 0 || bytes[i - 1] != b'\\') {
in_tag = false;
}
i += 1;
}
in_tag
}
pub fn append_before_trailing_whitespace(content: &str, suffix: &str) -> String {
let ws = trailing_ws_offset(content);
format!("{}{}{}", &content[..ws], suffix, &content[ws..])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn closed_code_ranges() {
let s = "```js\ncode\n``` and `inline";
let ranges = find_closed_code_block_ranges(s);
assert_eq!(ranges, vec![(0, 14)]);
assert!(is_position_in_ranges(5, &ranges));
assert!(!is_position_in_ranges(20, &ranges));
}
#[test]
fn underscore_after_multibyte_char_is_safe() {
let content = "`x`,_y_";
let code = find_closed_code_block_ranges(content);
let inline = find_inline_code_ranges(content, &code);
let masked = mask_inline_code_markdown_markers(content, &inline);
let star_pos = masked.find('_').unwrap();
assert!(!should_ignore_underscore_marker(content, star_pos, 1));
}
#[test]
fn underscore_inside_cjk_word_boundaries() {
let content = "中文_x_结尾";
let star_pos = content.find('_').unwrap();
let _ = should_ignore_underscore_marker(content, star_pos, 1);
assert!(content.is_char_boundary(star_pos + 1));
}
#[test]
fn inline_code_ranges_skip_triples() {
let s = "```a``` text `b` and `c";
let ranges = find_inline_code_ranges(s, &find_closed_code_block_ranges(s));
assert_eq!(ranges.len(), 1);
assert_eq!(&s[ranges[0].0..ranges[0].1], "`b`");
}
#[test]
fn last_paragraph_split() {
let (line, off) = last_paragraph_range("Para1\n\nPara2 text", false);
assert_eq!(line, 2);
assert_eq!(&"Para1\n\nPara2 text"[off..], "Para2 text");
let (line2, off2) = last_paragraph_range("Para1 **bold**\n\n", true);
assert_eq!(line2, 2);
assert_eq!(off2, 16);
let (line3, _) = last_paragraph_range("Para1\n\nPara2\n", true);
assert_eq!(line3, 2);
let (line4, _) = last_paragraph_range("**Contribution\n", true);
assert_eq!(line4, 0);
}
#[test]
fn url_removal_keeps_label() {
let s = "[text](https://example.com/page_with_underscore) more";
let r = remove_urls_from_text(s);
assert_eq!(r, "[text]() more");
}
#[test]
fn incomplete_url_suffix() {
let s = "Visit [Google](https://www.goo";
let r = remove_urls_from_text(s);
assert_eq!(r, "Visit [Google](");
}
#[test]
fn math_removal() {
let s = "a $$x = 1$$ b $$y = 2$$ c";
assert_eq!(remove_math_blocks_from_text(s, false), "a b c");
let s2 = "unclosed $$x";
assert_eq!(remove_math_blocks_from_text(s2, false), "unclosed ");
}
#[test]
fn underscore_inside_word_ignored() {
let s = "snake_case text";
assert!(should_ignore_underscore_marker(s, 5, 1));
let s2 = "snake case _";
assert!(!should_ignore_underscore_marker(s2, 10, 1));
}
#[test]
fn append_before_trailing() {
assert_eq!(
append_before_trailing_whitespace("text ", "**"),
"text** "
);
assert_eq!(append_before_trailing_whitespace("text", "**"), "text**");
}
#[test]
fn backtick_after_cjk_does_not_panic() {
let s = "中文:`code` 后";
let tick = s.find('`').unwrap();
let _ = is_backtick_part_of_triple(s, tick);
let s2 = "注解``code``";
let tick = s2.find('`').unwrap();
let _ = is_backtick_part_of_triple(s2, tick);
}
#[test]
fn within_link_url() {
assert!(is_within_link_or_image_url("[t](https://a", 9));
assert!(is_within_link_or_image_url("[t](https://a)", 9));
assert!(is_within_link_or_image_url("[t](https://a) mid", 9));
assert!(!is_within_link_or_image_url("[t](https://a)", 14));
assert!(!is_within_link_or_image_url("plain text", 4));
}
#[test]
fn join_multiline_math_folds_a_span_onto_one_line() {
assert_eq!(
join_multiline_math("before\n\n$$ \\boxed{ Z[J]\n=\n\\int x\\,dx $$\n\nafter"),
"before\n\n$$ \\boxed{ Z[J] = \\int x\\,dx $$\n\nafter"
);
assert_eq!(join_multiline_math("$$\nE = mc^2\n$$"), "$$ E = mc^2 $$");
assert_eq!(
join_multiline_math("$$ a\n\n b \n$$ and $$c\nd$$"),
"$$ a b $$ and $$c d$$"
);
}
#[test]
fn join_multiline_math_leaves_the_rest_alone() {
for untouched in [
"no math here",
"single line $$ a = b $$ stays",
"unclosed $$ a\n= b",
"escaped \\$$ a\n= b \\$$",
"```\n$$\nx\n$$\n```",
] {
assert_eq!(join_multiline_math(untouched), untouched);
}
}
}