use carta_ast::MathType;
#[cfg(feature = "html")]
pub(crate) fn scan_inline_math(chars: &[char], pos: usize) -> Option<(String, usize)> {
if chars
.get(pos + 1)
.copied()
.is_none_or(is_unicode_whitespace)
{
return None;
}
let content_start = pos + 1;
let mut i = content_start;
while let Some(&ch) = chars.get(i) {
if ch == '\\' && chars.get(i + 1).is_some() {
i += 2;
continue;
}
if ch == '$' {
let prev_space = chars.get(i - 1).copied().is_none_or(is_unicode_whitespace);
let next_digit = chars.get(i + 1).is_some_and(char::is_ascii_digit);
if prev_space || next_digit {
return None;
}
let content: String = chars.get(content_start..i)?.iter().collect();
return Some((content, i + 1));
}
i += 1;
}
None
}
#[cfg(feature = "html")]
pub(crate) fn scan_display_math(chars: &[char], pos: usize) -> Option<(String, usize)> {
let content_start = pos + 2;
let mut i = content_start;
while chars.get(i).is_some() {
if chars.get(i) == Some(&'$') && chars.get(i + 1) == Some(&'$') {
let content: String = chars.get(content_start..i)?.iter().collect();
return Some((content, i + 2));
}
i += 1;
}
None
}
#[cfg(feature = "html")]
pub(crate) fn scan_backslash_math(
chars: &[char],
pos: usize,
slashes: usize,
) -> Option<(MathType, String, usize)> {
let open = pos + slashes;
let (close_bracket, math_type) = match chars.get(open).copied() {
Some('(') => (')', MathType::InlineMath),
Some('[') => (']', MathType::DisplayMath),
_ => return None,
};
let content_start = open + 1;
let mut i = content_start;
while chars.get(i).is_some() {
if is_backslash_math_closer(chars, i, slashes, close_bracket) {
if i == content_start {
return None; }
let raw: String = chars.get(content_start..i)?.iter().collect();
let content = match math_type {
MathType::InlineMath => raw.trim().to_owned(),
MathType::DisplayMath => raw,
};
return Some((math_type, content, i + slashes + 1));
}
if slashes == 1 && chars.get(i) == Some(&'\\') && chars.get(i + 1).is_some() {
i += 2;
continue;
}
i += 1;
}
None
}
#[cfg(feature = "html")]
fn is_backslash_math_closer(chars: &[char], i: usize, slashes: usize, close: char) -> bool {
(0..slashes).all(|k| chars.get(i + k) == Some(&'\\')) && chars.get(i + slashes) == Some(&close)
}
#[cfg(feature = "commonmark")]
fn char_at(text: &str, at: usize) -> Option<char> {
text.get(at..).and_then(|rest| rest.chars().next())
}
#[cfg(feature = "commonmark")]
fn char_before(text: &str, at: usize) -> Option<char> {
text.get(..at).and_then(|head| head.chars().next_back())
}
#[cfg(feature = "commonmark")]
pub(crate) fn scan_inline_math_bytes(text: &str, pos: usize) -> Option<(String, usize)> {
if char_at(text, pos + 1).is_none_or(is_unicode_whitespace) {
return None;
}
let content_start = pos + 1;
let mut i = content_start;
while let Some(ch) = char_at(text, i) {
if ch == '\\'
&& let Some(next) = char_at(text, i + 1)
{
i += 1 + next.len_utf8();
continue;
}
if ch == '$' {
let prev_space = char_before(text, i).is_none_or(is_unicode_whitespace);
let next_digit = char_at(text, i + 1).is_some_and(|c| c.is_ascii_digit());
if prev_space || next_digit {
return None;
}
let content = text.get(content_start..i)?.to_owned();
return Some((content, i + 1));
}
i += ch.len_utf8();
}
None
}
#[cfg(feature = "commonmark")]
pub(crate) fn scan_display_math_bytes(text: &str, pos: usize) -> Option<(String, usize)> {
let content_start = pos + 2;
let mut i = content_start;
while let Some(ch) = char_at(text, i) {
if text.get(i..).is_some_and(|rest| rest.starts_with("$$")) {
let content = text.get(content_start..i)?.to_owned();
return Some((content, i + 2));
}
i += ch.len_utf8();
}
None
}
#[cfg(feature = "commonmark")]
pub(crate) fn scan_backslash_math_bytes(
text: &str,
pos: usize,
slashes: usize,
) -> Option<(MathType, String, usize)> {
let open = pos + slashes;
let (close_bracket, math_type) = match char_at(text, open) {
Some('(') => (')', MathType::InlineMath),
Some('[') => (']', MathType::DisplayMath),
_ => return None,
};
let content_start = open + 1;
let mut i = content_start;
while let Some(ch) = char_at(text, i) {
if is_backslash_math_closer_bytes(text, i, slashes, close_bracket) {
if i == content_start {
return None; }
let raw = text.get(content_start..i)?.to_owned();
let content = match math_type {
MathType::InlineMath => raw.trim().to_owned(),
MathType::DisplayMath => raw,
};
return Some((math_type, content, i + slashes + 1));
}
if slashes == 1
&& ch == '\\'
&& let Some(next) = char_at(text, i + 1)
{
i += 1 + next.len_utf8();
continue;
}
i += ch.len_utf8();
}
None
}
#[cfg(feature = "commonmark")]
fn is_backslash_math_closer_bytes(text: &str, i: usize, slashes: usize, close: char) -> bool {
(0..slashes).all(|k| text.as_bytes().get(i + k) == Some(&b'\\'))
&& char_at(text, i + slashes) == Some(close)
}
#[cfg(feature = "commonmark")]
pub(crate) fn fold_dash_run(len: usize) -> String {
let (em, en) = if len.is_multiple_of(3) {
(len / 3, 0)
} else if len.is_multiple_of(2) {
(0, len / 2)
} else {
let en = if len % 3 == 1 { 2 } else { 1 };
((len - 2 * en) / 3, en)
};
let mut out = String::with_capacity((em + en) * 3);
out.extend(std::iter::repeat_n('\u{2014}', em));
out.extend(std::iter::repeat_n('\u{2013}', en));
out
}
pub(crate) fn fold_ellipsis_run(len: usize) -> String {
let mut out = String::with_capacity(len / 3 * 3 + len % 3);
out.extend(std::iter::repeat_n('\u{2026}', len / 3));
out.extend(std::iter::repeat_n('.', len % 3));
out
}
pub(crate) fn is_unicode_whitespace(ch: char) -> bool {
ch == ' '
|| ch == '\t'
|| ch == '\n'
|| ch == '\u{0c}'
|| ch == '\u{0b}'
|| ch == '\r'
|| ch.is_whitespace()
}
#[cfg(all(test, feature = "commonmark"))]
mod tests {
use super::{scan_display_math_bytes, scan_inline_math_bytes};
#[test]
fn inline_math_spans_multi_byte_content() {
assert_eq!(
scan_inline_math_bytes("$αβ$", 0),
Some(("αβ".to_owned(), 6))
);
}
#[test]
fn inline_math_opens_after_a_multi_byte_neighbor() {
assert_eq!(scan_inline_math_bytes("é$β$", 2), Some(("β".to_owned(), 6)));
}
#[test]
fn inline_math_escape_hops_over_a_multi_byte_character() {
assert_eq!(
scan_inline_math_bytes("$\\éx$", 0),
Some(("\\éx".to_owned(), 6))
);
}
#[test]
fn inline_math_rejects_ill_flanked_closers() {
assert_eq!(scan_inline_math_bytes("$x $", 0), None);
assert_eq!(scan_inline_math_bytes("$x$5", 0), None);
assert_eq!(scan_inline_math_bytes("$ x$", 0), None);
}
#[test]
fn display_math_spans_multi_byte_content() {
assert_eq!(
scan_display_math_bytes("$$α+β$$", 0),
Some(("α+β".to_owned(), 9))
);
}
#[test]
fn display_math_without_a_closer_is_not_math() {
assert_eq!(scan_display_math_bytes("$$αβ$", 0), None);
}
}