use crate::parser::blocks::raw_blocks::{extract_environment_name, is_inline_math_environment};
use crate::syntax::SyntaxKind;
use rowan::GreenNodeBuilder;
pub fn try_parse_inline_math(text: &str) -> Option<(usize, &str)> {
if !text.starts_with('$') || text.starts_with("$$") {
return None;
}
let rest = &text[1..];
if rest.is_empty() || rest.starts_with(char::is_whitespace) {
return None;
}
let mut pos = 0;
while pos < rest.len() {
let ch = rest[pos..].chars().next()?;
if ch == '$' {
if pos > 0 && rest.as_bytes()[pos - 1] == b'\\' {
pos += 1;
continue;
}
if pos == 0 || rest[..pos].ends_with(char::is_whitespace) {
pos += 1;
continue;
}
if let Some(next_ch) = rest[pos + 1..].chars().next()
&& next_ch.is_ascii_digit()
{
pos += 1;
continue;
}
let math_content = &rest[..pos];
let total_len = 1 + pos + 1; return Some((total_len, math_content));
}
if ch == '\n' {
return None;
}
pos += ch.len_utf8();
}
None
}
pub fn try_parse_gfm_inline_math(text: &str) -> Option<(usize, &str)> {
if !text.starts_with("$`") {
return None;
}
let rest = &text[2..];
if rest.is_empty() {
return None;
}
let mut pos = 0;
while pos < rest.len() {
let ch = rest[pos..].chars().next()?;
if ch == '\n' {
return None;
}
if rest[pos..].starts_with("`$") {
if pos == 0 {
return None;
}
let math_content = &rest[..pos];
let total_len = 2 + pos + 2; return Some((total_len, math_content));
}
pos += ch.len_utf8();
}
None
}
pub fn try_parse_single_backslash_inline_math(text: &str) -> Option<(usize, &str)> {
if !text.starts_with(r"\(") {
return None;
}
let rest = &text[2..];
let mut pos = 0;
while pos < rest.len() {
let ch = rest[pos..].chars().next()?;
if ch == '\\' && rest[pos..].starts_with(r"\)") {
let math_content = &rest[..pos];
let total_len = 2 + pos + 2; return Some((total_len, math_content));
}
if ch == '\n' {
return None;
}
pos += ch.len_utf8();
}
None
}
pub fn try_parse_double_backslash_inline_math(text: &str) -> Option<(usize, &str)> {
if !text.starts_with(r"\\(") {
return None;
}
let rest = &text[3..];
let mut pos = 0;
while pos < rest.len() {
let ch = rest[pos..].chars().next()?;
if ch == '\\' && rest[pos..].starts_with(r"\\)") {
let math_content = &rest[..pos];
let total_len = 3 + pos + 3; return Some((total_len, math_content));
}
if ch == '\n' {
return None;
}
pos += ch.len_utf8();
}
None
}
pub fn try_parse_display_math(text: &str) -> Option<(usize, &str)> {
if !text.starts_with("$$") {
return None;
}
let opening_count = text.chars().take_while(|&c| c == '$').count();
if opening_count < 2 {
return None;
}
let rest = &text[opening_count..];
let mut pos = 0;
while pos < rest.len() {
let ch = rest[pos..].chars().next()?;
if ch == '$' {
if pos > 0 && rest.as_bytes()[pos - 1] == b'\\' {
pos += ch.len_utf8();
continue;
}
let closing_count = rest[pos..].chars().take_while(|&c| c == '$').count();
if closing_count >= opening_count {
let math_content = &rest[..pos];
let total_len = opening_count + pos + closing_count;
return Some((total_len, math_content));
}
pos += closing_count;
continue;
}
pos += ch.len_utf8();
}
None
}
pub fn try_parse_single_backslash_display_math(text: &str) -> Option<(usize, &str)> {
if !text.starts_with(r"\[") {
return None;
}
let rest = &text[2..];
let mut pos = 0;
while pos < rest.len() {
let ch = rest[pos..].chars().next()?;
if ch == '\\' && rest[pos..].starts_with(r"\]") {
let math_content = &rest[..pos];
let total_len = 2 + pos + 2; return Some((total_len, math_content));
}
pos += ch.len_utf8();
}
None
}
pub fn try_parse_double_backslash_display_math(text: &str) -> Option<(usize, &str)> {
if !text.starts_with(r"\\[") {
return None;
}
let rest = &text[3..];
let mut pos = 0;
while pos < rest.len() {
let ch = rest[pos..].chars().next()?;
if ch == '\\' && rest[pos..].starts_with(r"\\]") {
let math_content = &rest[..pos];
let total_len = 3 + pos + 3; return Some((total_len, math_content));
}
pos += ch.len_utf8();
}
None
}
pub fn try_parse_math_environment(text: &str) -> Option<(usize, &str, &str, &str)> {
let env_name = extract_environment_name(text)?;
if !is_inline_math_environment(&env_name) {
return None;
}
let begin_marker_len = text.find('}')? + 1;
let begin_marker = &text[..begin_marker_len];
let end_marker = format!("\\end{{{}}}", env_name);
let after_begin = &text[begin_marker_len..];
let end_rel = after_begin.find(&end_marker)?;
let end_start = begin_marker_len + end_rel;
let end_marker_end = end_start + end_marker.len();
let mut end_line_end = end_marker_end;
while end_line_end < text.len() {
let ch = text[end_line_end..].chars().next()?;
if ch == '\n' || ch == '\r' {
break;
}
end_line_end += ch.len_utf8();
}
if end_line_end < text.len() {
if text[end_line_end..].starts_with("\r\n") {
end_line_end += 2;
} else {
end_line_end += 1;
}
}
let content = &text[begin_marker_len..end_start];
let end_marker_text = &text[end_start..end_line_end];
Some((end_line_end, begin_marker, content, end_marker_text))
}
pub fn emit_inline_math(builder: &mut GreenNodeBuilder, content: &str) {
builder.start_node(SyntaxKind::INLINE_MATH.into());
builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "$");
builder.token(SyntaxKind::TEXT.into(), content);
builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "$");
builder.finish_node();
}
pub fn emit_gfm_inline_math(builder: &mut GreenNodeBuilder, content: &str) {
builder.start_node(SyntaxKind::INLINE_MATH.into());
builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "$`");
builder.token(SyntaxKind::TEXT.into(), content);
builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "`$");
builder.finish_node();
}
pub fn emit_single_backslash_inline_math(builder: &mut GreenNodeBuilder, content: &str) {
builder.start_node(SyntaxKind::INLINE_MATH.into());
builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\(");
builder.token(SyntaxKind::TEXT.into(), content);
builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\)");
builder.finish_node();
}
pub fn emit_double_backslash_inline_math(builder: &mut GreenNodeBuilder, content: &str) {
builder.start_node(SyntaxKind::INLINE_MATH.into());
builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\\(");
builder.token(SyntaxKind::TEXT.into(), content);
builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\\)");
builder.finish_node();
}
pub fn emit_display_math(builder: &mut GreenNodeBuilder, content: &str, dollar_count: usize) {
builder.start_node(SyntaxKind::DISPLAY_MATH.into());
let marker = "$".repeat(dollar_count);
builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), &marker);
builder.token(SyntaxKind::TEXT.into(), content);
builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), &marker);
builder.finish_node();
}
pub fn emit_display_math_environment(
builder: &mut GreenNodeBuilder,
begin_marker: &str,
content: &str,
end_marker: &str,
) {
builder.start_node(SyntaxKind::DISPLAY_MATH.into());
builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), begin_marker);
builder.token(SyntaxKind::TEXT.into(), content);
builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), end_marker);
builder.finish_node();
}
pub fn emit_single_backslash_display_math(builder: &mut GreenNodeBuilder, content: &str) {
builder.start_node(SyntaxKind::DISPLAY_MATH.into());
builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\[");
builder.token(SyntaxKind::TEXT.into(), content);
builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\]");
builder.finish_node();
}
pub fn emit_double_backslash_display_math(builder: &mut GreenNodeBuilder, content: &str) {
builder.start_node(SyntaxKind::DISPLAY_MATH.into());
builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\\[");
builder.token(SyntaxKind::TEXT.into(), content);
builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\\]");
builder.finish_node();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_inline_math() {
let result = try_parse_inline_math("$x = y$");
assert_eq!(result, Some((7, "x = y")));
}
#[test]
fn test_parse_inline_math_with_spaces_inside() {
let result = try_parse_inline_math("$a + b$");
assert_eq!(result, Some((7, "a + b")));
}
#[test]
fn test_parse_inline_math_complex() {
let result = try_parse_inline_math(r"$\frac{1}{2}$");
assert_eq!(result, Some((13, r"\frac{1}{2}")));
}
#[test]
fn test_not_inline_math_display() {
let result = try_parse_inline_math("$$x = y$$");
assert_eq!(result, None);
}
#[test]
fn test_inline_math_no_close() {
let result = try_parse_inline_math("$no close");
assert_eq!(result, None);
}
#[test]
fn test_inline_math_no_multiline() {
let result = try_parse_inline_math("$x =\ny$");
assert_eq!(result, None);
}
#[test]
fn test_not_inline_math() {
let result = try_parse_inline_math("no dollar");
assert_eq!(result, None);
}
#[test]
fn test_inline_math_with_trailing_text() {
let result = try_parse_inline_math("$x$ and more");
assert_eq!(result, Some((3, "x")));
}
#[test]
fn test_spec_opening_must_have_non_space_right() {
let result = try_parse_inline_math("$ x$");
assert_eq!(result, None, "Opening $ with space should not parse");
}
#[test]
fn test_spec_closing_must_have_non_space_left() {
let result = try_parse_inline_math("$x $");
assert_eq!(result, None, "Closing $ with space should not parse");
}
#[test]
fn test_spec_closing_not_followed_by_digit() {
let result = try_parse_inline_math("$x$5");
assert_eq!(result, None, "Closing $ followed by digit should not parse");
}
#[test]
fn test_spec_dollar_amounts() {
let result = try_parse_inline_math("$20,000");
assert_eq!(result, None, "Dollar amounts should not parse as math");
}
#[test]
fn test_valid_math_after_spec_checks() {
let result = try_parse_inline_math("$x$");
assert_eq!(result, Some((3, "x")), "Valid math should parse");
}
#[test]
fn test_math_followed_by_non_digit() {
let result = try_parse_inline_math("$x$a");
assert_eq!(
result,
Some((3, "x")),
"Math followed by non-digit should parse"
);
}
#[test]
fn test_parse_display_math_simple() {
let result = try_parse_display_math("$$x = y$$");
assert_eq!(result, Some((9, "x = y")));
}
#[test]
fn test_parse_display_math_multiline() {
let result = try_parse_display_math("$$\nx = y\n$$");
assert_eq!(result, Some((11, "\nx = y\n")));
}
#[test]
fn test_parse_display_math_triple_dollars() {
let result = try_parse_display_math("$$$x = y$$$");
assert_eq!(result, Some((11, "x = y")));
}
#[test]
fn test_parse_display_math_no_close() {
let result = try_parse_display_math("$$no close");
assert_eq!(result, None);
}
#[test]
fn test_not_display_math() {
let result = try_parse_display_math("$single dollar");
assert_eq!(result, None);
}
#[test]
fn test_display_math_with_trailing_text() {
let result = try_parse_display_math("$$x = y$$ and more");
assert_eq!(result, Some((9, "x = y")));
}
#[test]
fn test_single_backslash_inline_math() {
let result = try_parse_single_backslash_inline_math(r"\(x^2\)");
assert_eq!(result, Some((7, "x^2")));
}
#[test]
fn test_single_backslash_inline_math_complex() {
let result = try_parse_single_backslash_inline_math(r"\(\frac{a}{b}\)");
assert_eq!(result, Some((15, r"\frac{a}{b}")));
}
#[test]
fn test_single_backslash_inline_math_no_close() {
let result = try_parse_single_backslash_inline_math(r"\(no close");
assert_eq!(result, None);
}
#[test]
fn test_single_backslash_inline_math_no_multiline() {
let result = try_parse_single_backslash_inline_math("\\(x =\ny\\)");
assert_eq!(result, None);
}
#[test]
fn test_single_backslash_display_math() {
let result = try_parse_single_backslash_display_math(r"\[E = mc^2\]");
assert_eq!(result, Some((12, "E = mc^2")));
}
#[test]
fn test_single_backslash_display_math_multiline() {
let result = try_parse_single_backslash_display_math("\\[\nx = y\n\\]");
assert_eq!(result, Some((11, "\nx = y\n")));
}
#[test]
fn test_single_backslash_display_math_no_close() {
let result = try_parse_single_backslash_display_math(r"\[no close");
assert_eq!(result, None);
}
#[test]
fn test_double_backslash_inline_math() {
let result = try_parse_double_backslash_inline_math(r"\\(x^2\\)");
assert_eq!(result, Some((9, "x^2")));
}
#[test]
fn test_double_backslash_inline_math_complex() {
let result = try_parse_double_backslash_inline_math(r"\\(\alpha + \beta\\)");
assert_eq!(result, Some((20, r"\alpha + \beta")));
}
#[test]
fn test_double_backslash_inline_math_no_close() {
let result = try_parse_double_backslash_inline_math(r"\\(no close");
assert_eq!(result, None);
}
#[test]
fn test_double_backslash_inline_math_no_multiline() {
let result = try_parse_double_backslash_inline_math("\\\\(x =\ny\\\\)");
assert_eq!(result, None);
}
#[test]
fn test_double_backslash_display_math() {
let result = try_parse_double_backslash_display_math(r"\\[E = mc^2\\]");
assert_eq!(result, Some((14, "E = mc^2")));
}
#[test]
fn test_double_backslash_display_math_multiline() {
let result = try_parse_double_backslash_display_math("\\\\[\nx = y\n\\\\]");
assert_eq!(result, Some((13, "\nx = y\n")));
}
#[test]
fn test_double_backslash_display_math_no_close() {
let result = try_parse_double_backslash_display_math(r"\\[no close");
assert_eq!(result, None);
}
#[test]
fn test_display_math_escaped_dollar() {
let result = try_parse_display_math(r"$$a = \$100$$");
assert_eq!(result, Some((13, r"a = \$100")));
}
#[test]
fn test_display_math_with_content_on_fence_line() {
let result = try_parse_display_math("$$x = y\n$$");
assert_eq!(result, Some((10, "x = y\n")));
}
}