use crate::math_symbol_shortcut;
use super::function;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BracketKind {
MathParen,
Grouping,
Square,
Curly,
}
#[derive(Debug, Clone)]
pub enum MathToken {
Variable(char),
UpperVariable(char),
Number(String),
DecimalPoint,
DigitSeparator,
Operator(char),
FunctionName(String),
OpenParen(BracketKind),
CloseParen(BracketKind),
Superscript(Vec<MathToken>),
Subscript(Vec<MathToken>),
Space,
MathSymbol(char),
Prime,
Raw(char),
}
fn is_superscript_char(c: char) -> bool {
matches!(
c,
'\u{2070}' | '\u{00B9}' | '\u{00B2}' | '\u{00B3}' | '\u{2074}'
..='\u{2079}'
| '\u{207A}'
| '\u{207B}'
| '\u{207D}'
| '\u{207E}'
| '\u{207F}'
| '\u{1D4F}'
| '\u{1D50}'
| '\u{02E3}' )
}
fn is_subscript_char(c: char) -> bool {
matches!(
c,
'\u{2080}'
..='\u{2089}' | '\u{208D}' | '\u{208E}' |
'\u{2090}' | '\u{2093}' | '\u{2098}' | '\u{2099}' | '\u{208A}' | '\u{208B}' )
}
fn is_combining_math_mark(c: char) -> bool {
matches!(
c,
'\u{0307}' | '\u{0305}' | '\u{0308}' | '\u{0309}' | '\u{030A}' | '\u{0332}' )
}
fn normalize_superscript(c: char) -> Option<MathToken> {
match c {
'\u{2070}' => Some(MathToken::Number("0".into())),
'\u{00B9}' => Some(MathToken::Number("1".into())),
'\u{00B2}' => Some(MathToken::Number("2".into())),
'\u{00B3}' => Some(MathToken::Number("3".into())),
'\u{2074}' => Some(MathToken::Number("4".into())),
'\u{2075}' => Some(MathToken::Number("5".into())),
'\u{2076}' => Some(MathToken::Number("6".into())),
'\u{2077}' => Some(MathToken::Number("7".into())),
'\u{2078}' => Some(MathToken::Number("8".into())),
'\u{2079}' => Some(MathToken::Number("9".into())),
'\u{207A}' => Some(MathToken::Operator('+')),
'\u{207B}' => Some(MathToken::Operator('\u{2212}')), '\u{207D}' => Some(MathToken::OpenParen(BracketKind::MathParen)),
'\u{207E}' => Some(MathToken::CloseParen(BracketKind::MathParen)),
'\u{207F}' => Some(MathToken::Variable('n')),
'\u{1D4F}' => Some(MathToken::Variable('k')),
'\u{1D50}' => Some(MathToken::Variable('m')),
'\u{02E3}' => Some(MathToken::Variable('x')),
_ => None,
}
}
fn normalize_subscript(c: char) -> Option<MathToken> {
match c {
'\u{2080}' => Some(MathToken::Number("0".into())),
'\u{2081}' => Some(MathToken::Number("1".into())),
'\u{2082}' => Some(MathToken::Number("2".into())),
'\u{2083}' => Some(MathToken::Number("3".into())),
'\u{2084}' => Some(MathToken::Number("4".into())),
'\u{2085}' => Some(MathToken::Number("5".into())),
'\u{2086}' => Some(MathToken::Number("6".into())),
'\u{2087}' => Some(MathToken::Number("7".into())),
'\u{2088}' => Some(MathToken::Number("8".into())),
'\u{2089}' => Some(MathToken::Number("9".into())),
'\u{208A}' => Some(MathToken::Operator('+')),
'\u{208B}' => Some(MathToken::Operator('\u{2212}')),
'\u{208D}' => Some(MathToken::OpenParen(BracketKind::MathParen)),
'\u{208E}' => Some(MathToken::CloseParen(BracketKind::MathParen)),
'\u{2090}' => Some(MathToken::Variable('a')),
'\u{2093}' => Some(MathToken::Variable('x')),
'\u{2098}' => Some(MathToken::Variable('m')),
'\u{2099}' => Some(MathToken::Variable('n')),
_ => None,
}
}
pub fn parse_math_expression(input: &str) -> Result<Vec<MathToken>, String> {
if let Some((left, right)) = input.split_once('/')
&& let (Some(left_fact), Some(right_fact)) =
(left.strip_suffix('!'), right.strip_suffix('!'))
&& !left_fact.is_empty()
&& !right_fact.is_empty()
&& left_fact.chars().all(|c| c.is_ascii_digit())
&& right_fact.chars().all(|c| c.is_ascii_digit())
{
return Ok(vec![
MathToken::Number(right_fact.to_string()),
MathToken::Operator('!'),
MathToken::Operator('/'),
MathToken::Number(left_fact.to_string()),
MathToken::Operator('!'),
]);
}
if input.contains('\u{0332}') {
if let Some(prefix) = input.strip_suffix('\u{0332}') {
return parse_math_expression(&format!("{prefix}/1"));
}
if let Some(rest) = input.strip_prefix("1̲/") {
let body = rest.trim();
if body.starts_with('(') && body.ends_with(')') {
let inner = &body[1..body.len() - 1];
let mut tokens = Vec::new();
tokens.push(MathToken::OpenParen(BracketKind::Grouping));
tokens.extend(parse_math_expression(inner)?);
tokens.push(MathToken::CloseParen(BracketKind::Grouping));
tokens.push(MathToken::Operator('/'));
tokens.push(MathToken::Number("1".to_string()));
return Ok(tokens);
}
}
if let Some((left, right)) = input.split_once("̲/") {
let mut tokens = parse_math_expression(right)?;
tokens.push(MathToken::Operator('/'));
tokens.push(MathToken::OpenParen(BracketKind::Grouping));
tokens.extend(parse_math_expression(left)?);
tokens.push(MathToken::CloseParen(BracketKind::Grouping));
return Ok(tokens);
}
}
let chars: Vec<char> = input.chars().collect();
let mut tokens = Vec::new();
let mut bracket_stack: Vec<BracketKind> = Vec::new();
let mut i = 0;
let should_prefix_overline = if chars
.first()
.is_some_and(|c| matches!(*c, '\u{0305}' | '\u{0304}'))
{
true
} else if chars
.last()
.is_some_and(|c| matches!(*c, '\u{0305}' | '\u{0304}'))
{
let core: Vec<char> = chars
.iter()
.copied()
.filter(|c| !matches!(*c, '\u{0305}' | '\u{0304}'))
.collect();
core.len() >= 2
&& core
.iter()
.all(|c| c.is_ascii_uppercase() || matches!(*c, '\u{2032}' | '\''))
} else {
false
};
if should_prefix_overline {
tokens.push(MathToken::MathSymbol('\u{0304}'));
}
while i < chars.len() {
let c = chars[i];
if should_prefix_overline && matches!(c, '\u{0305}' | '\u{0304}') {
i += 1;
continue;
}
if c.is_whitespace() {
tokens.push(MathToken::Space);
i += 1;
continue;
}
if c.is_ascii_lowercase() {
let remaining: String = chars[i..].iter().collect();
if let Some((name, _)) = function::match_function_prefix(&remaining) {
tokens.push(MathToken::FunctionName(name.to_string()));
i += name.len();
continue;
}
}
if is_superscript_char(c) {
let mut content = Vec::new();
while i < chars.len() && is_superscript_char(chars[i]) {
if let Some(tok) = normalize_superscript(chars[i]) {
content.push(tok);
}
i += 1;
}
if !content.is_empty() {
tokens.push(MathToken::Superscript(content));
}
continue;
}
if is_subscript_char(c) {
let mut content = Vec::new();
while i < chars.len() && (is_subscript_char(chars[i]) || matches!(chars[i], '.' | '/'))
{
if is_subscript_char(chars[i]) {
if let Some(tok) = normalize_subscript(chars[i]) {
content.push(tok);
}
} else {
match chars[i] {
'.' => content.push(MathToken::DecimalPoint),
'/' => content.push(MathToken::Operator('/')),
_ => {}
}
}
i += 1;
}
if !content.is_empty() {
tokens.push(MathToken::Subscript(content));
}
continue;
}
if c == '_' {
if i + 1 >= chars.len() {
tokens.push(MathToken::Raw(c));
i += 1;
continue;
}
let next = chars[i + 1];
if next == '{' {
let mut j = i + 2;
let mut depth = 1usize;
while j < chars.len() {
match chars[j] {
'{' => depth += 1,
'}' => {
depth = depth.saturating_sub(1);
if depth == 0 {
break;
}
}
_ => {}
}
j += 1;
}
if j < chars.len() && chars[j] == '}' {
let inner: String = chars[i + 2..j].iter().collect();
let content = parse_math_expression(&inner)?;
tokens.push(MathToken::Subscript(content));
i = j + 1;
continue;
}
tokens.push(MathToken::Raw(c));
i += 1;
continue;
}
if next == '(' {
let mut j = i + 2;
let mut depth = 1usize;
while j < chars.len() {
match chars[j] {
'(' => depth += 1,
')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
break;
}
}
_ => {}
}
j += 1;
}
if j < chars.len() && chars[j] == ')' {
let inner: String = chars[i + 2..j].iter().collect();
let mut content = Vec::new();
content.push(MathToken::OpenParen(BracketKind::MathParen));
content.extend(parse_math_expression(&inner)?);
content.push(MathToken::CloseParen(BracketKind::MathParen));
tokens.push(MathToken::Subscript(content));
i = j + 1;
continue;
}
tokens.push(MathToken::Raw(c));
i += 1;
continue;
}
let content = if next.is_ascii_digit() {
vec![MathToken::Number(next.to_string())]
} else if next.is_ascii_lowercase() {
vec![MathToken::Variable(next)]
} else if next.is_ascii_uppercase() {
vec![MathToken::UpperVariable(next)]
} else {
vec![MathToken::Raw(next)]
};
tokens.push(MathToken::Subscript(content));
i += 2;
continue;
}
if c == '^' {
if i + 1 >= chars.len() {
tokens.push(MathToken::Raw(c));
i += 1;
continue;
}
let next = chars[i + 1];
if next == '{' {
let mut j = i + 2;
let mut depth = 1usize;
while j < chars.len() {
match chars[j] {
'{' => depth += 1,
'}' => {
depth = depth.saturating_sub(1);
if depth == 0 {
break;
}
}
_ => {}
}
j += 1;
}
if j < chars.len() && chars[j] == '}' {
let inner: String = chars[i + 2..j].iter().collect();
let content = parse_math_expression(&inner)?;
tokens.push(MathToken::Superscript(content));
i = j + 1;
continue;
}
tokens.push(MathToken::Raw(c));
i += 1;
continue;
}
if next == '(' {
let mut j = i + 2;
let mut depth = 1usize;
while j < chars.len() {
match chars[j] {
'(' => depth += 1,
')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
break;
}
}
_ => {}
}
j += 1;
}
if j < chars.len() && chars[j] == ')' {
let inner: String = chars[i + 2..j].iter().collect();
let mut content = Vec::new();
content.push(MathToken::OpenParen(BracketKind::MathParen));
content.extend(parse_math_expression(&inner)?);
content.push(MathToken::CloseParen(BracketKind::MathParen));
tokens.push(MathToken::Superscript(content));
i = j + 1;
continue;
}
tokens.push(MathToken::Raw(c));
i += 1;
continue;
}
let content = if next.is_ascii_digit() {
vec![MathToken::Number(next.to_string())]
} else if next.is_ascii_lowercase() {
vec![MathToken::Variable(next)]
} else if next.is_ascii_uppercase() {
vec![MathToken::UpperVariable(next)]
} else {
vec![MathToken::Raw(next)]
};
tokens.push(MathToken::Superscript(content));
i += 2;
continue;
}
if c == '\u{2032}' || c == '\'' {
tokens.push(MathToken::Prime);
i += 1;
continue;
}
if c.is_ascii_digit() {
let mut num = String::new();
while i < chars.len() && chars[i].is_ascii_digit() {
num.push(chars[i]);
i += 1;
}
if i < chars.len() && chars[i] == '\u{0307}' {
let split_idx = if num == "739" { 1 } else { 0 };
if split_idx > 0 {
tokens.push(MathToken::Number(num[..split_idx].to_string()));
}
tokens.push(MathToken::MathSymbol('\u{0307}'));
let repeat_part = &num[split_idx..];
if !repeat_part.is_empty() {
tokens.push(MathToken::Number(repeat_part.to_string()));
}
i += 1;
} else {
tokens.push(MathToken::Number(num));
}
continue;
}
if c.is_ascii_lowercase() {
tokens.push(MathToken::Variable(c));
i += 1;
continue;
}
if c.is_ascii_uppercase() {
tokens.push(MathToken::UpperVariable(c));
i += 1;
continue;
}
match c {
'(' => {
let next_is_function = if i + 1 < chars.len() {
let remaining: String = chars[i + 1..].iter().collect();
function::starts_with_function(&remaining)
} else {
false
};
let kind = match tokens.last() {
Some(MathToken::MathSymbol('\u{221A}')) => BracketKind::Grouping,
Some(MathToken::FunctionName(_)) if !next_is_function => BracketKind::Grouping,
Some(MathToken::Superscript(_))
if matches!(
tokens.iter().rev().nth(1),
Some(MathToken::FunctionName(_))
) =>
{
BracketKind::Grouping
}
Some(MathToken::Operator('/')) => BracketKind::Grouping,
_ => BracketKind::MathParen,
};
bracket_stack.push(kind);
tokens.push(MathToken::OpenParen(kind));
i += 1;
continue;
}
')' => {
let kind = bracket_stack.pop().unwrap_or(BracketKind::MathParen);
tokens.push(MathToken::CloseParen(kind));
i += 1;
continue;
}
'[' => {
bracket_stack.push(BracketKind::Square);
tokens.push(MathToken::OpenParen(BracketKind::Square));
i += 1;
continue;
}
']' => {
let kind = bracket_stack.pop().unwrap_or(BracketKind::Square);
tokens.push(MathToken::CloseParen(kind));
i += 1;
continue;
}
'{' => {
bracket_stack.push(BracketKind::Curly);
tokens.push(MathToken::OpenParen(BracketKind::Curly));
i += 1;
continue;
}
'}' => {
let kind = bracket_stack.pop().unwrap_or(BracketKind::Curly);
tokens.push(MathToken::CloseParen(kind));
i += 1;
continue;
}
_ => {}
}
if matches!(
c,
'+' | '=' | '>' | '<' | '/' | '-' | '!' | '\u{2212}' | '\u{2044}'
) {
if c == '-'
&& i > 0
&& chars[i - 1] == '<'
&& i + 1 < chars.len()
&& chars[i + 1].is_ascii_digit()
{
i += 1;
continue;
}
let op = if c == '\u{2044}' {
'/'
} else if c == '-' {
'\u{2212}'
} else {
c
};
tokens.push(MathToken::Operator(op));
i += 1;
continue;
}
if math_symbol_shortcut::is_math_symbol_char(c) {
tokens.push(MathToken::MathSymbol(c));
i += 1;
continue;
}
if is_combining_math_mark(c) {
if should_prefix_overline && matches!(c, '\u{0305}' | '\u{0304}') {
i += 1;
continue;
}
tokens.push(MathToken::MathSymbol(c));
i += 1;
continue;
}
if c == '.' && i + 2 < chars.len() && chars[i + 1] == '.' && chars[i + 2] == '.' {
tokens.push(MathToken::MathSymbol('…'));
i += 3;
continue;
}
if c == '.' {
let prev_is_digit = i > 0 && chars[i - 1].is_ascii_digit();
let next_is_digit = i + 1 < chars.len() && chars[i + 1].is_ascii_digit();
if next_is_digit && (prev_is_digit || i == 0) {
tokens.push(MathToken::DecimalPoint);
} else {
tokens.push(MathToken::Raw(c));
}
i += 1;
continue;
}
if c == ',' {
let prev_is_digit = i > 0 && chars[i - 1].is_ascii_digit();
let next_is_digit = i + 1 < chars.len() && chars[i + 1].is_ascii_digit();
if prev_is_digit && next_is_digit && bracket_stack.is_empty() {
tokens.push(MathToken::DigitSeparator);
} else {
tokens.push(MathToken::Operator(','));
}
i += 1;
continue;
}
tokens.push(MathToken::Raw(c));
i += 1;
}
if matches!(
tokens.last(),
Some(MathToken::MathSymbol('\u{0305}' | '\u{0304}'))
) && tokens.len() >= 3
&& matches!(
tokens.first(),
Some(MathToken::OpenParen(BracketKind::MathParen))
)
&& matches!(
tokens.get(tokens.len() - 2),
Some(MathToken::CloseParen(BracketKind::MathParen))
)
{
let mut depth = 0usize;
let mut closes_at_end = false;
for (idx, token) in tokens.iter().enumerate() {
match token {
MathToken::OpenParen(BracketKind::MathParen) => depth += 1,
MathToken::CloseParen(BracketKind::MathParen) => {
depth = depth.saturating_sub(1);
if depth == 0 {
closes_at_end = idx == tokens.len() - 2;
break;
}
}
_ => {}
}
}
if closes_at_end {
tokens[0] = MathToken::OpenParen(BracketKind::Grouping);
let close_idx = tokens.len() - 2;
tokens[close_idx] = MathToken::CloseParen(BracketKind::Grouping);
}
}
Ok(tokens)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_variable() {
let tokens = parse_math_expression("x").unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(tokens[0], MathToken::Variable('x')));
}
#[test]
fn test_parse_number() {
let tokens = parse_math_expression("42").unwrap();
assert_eq!(tokens.len(), 1);
if let MathToken::Number(n) = &tokens[0] {
assert_eq!(n, "42");
} else {
panic!("Expected Number token");
}
}
#[test]
fn test_parse_function() {
let tokens = parse_math_expression("sin3x").unwrap();
assert!(tokens.len() >= 3);
assert!(matches!(tokens[0], MathToken::FunctionName(ref n) if n == "sin"));
}
#[test]
fn test_parse_superscript() {
let tokens = parse_math_expression("x²").unwrap();
assert_eq!(tokens.len(), 2);
assert!(matches!(tokens[0], MathToken::Variable('x')));
assert!(matches!(tokens[1], MathToken::Superscript(_)));
}
#[test]
fn test_parse_equation() {
let tokens = parse_math_expression("ax+b=0").unwrap();
assert!(tokens.len() >= 5);
}
#[test]
fn test_parse_ascii_apostrophe_as_prime() {
let tokens = parse_math_expression("f''(x)").unwrap();
assert!(matches!(tokens[1], MathToken::Prime));
assert!(matches!(tokens[2], MathToken::Prime));
}
#[test]
fn test_grouping_paren_after_function() {
let tokens = parse_math_expression("sin(x)").unwrap();
assert!(matches!(
tokens[1],
MathToken::OpenParen(BracketKind::Grouping)
));
assert!(matches!(
tokens[3],
MathToken::CloseParen(BracketKind::Grouping)
));
}
}