pub fn render_latex_in_text(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if i + 1 < len && chars[i] == '$' && chars[i + 1] == '$' {
if let Some(end) = find_closing_dollars(&chars, i + 2, true) {
let math: String = chars[i + 2..end].iter().collect();
let rendered = render_math_expression(&math, true);
result.push_str(&rendered);
i = end + 2; continue;
}
}
if chars[i] == '$' {
if let Some(end) = find_closing_dollars(&chars, i + 1, false) {
let math: String = chars[i + 1..end].iter().collect();
if math.len() > 1 && contains_math_chars(&math) {
let rendered = render_math_expression(&math, false);
result.push_str(&rendered);
i = end + 1; continue;
}
}
}
result.push(chars[i]);
i += 1;
}
result
}
fn find_closing_dollars(chars: &[char], start: usize, double: bool) -> Option<usize> {
let len = chars.len();
let mut i = start;
while i < len {
if double {
if i + 1 < len && chars[i] == '$' && chars[i + 1] == '$' {
return Some(i);
}
} else if chars[i] == '$' {
return Some(i);
}
if chars[i] == '\\' {
i += 2;
continue;
}
i += 1;
}
None
}
fn contains_math_chars(s: &str) -> bool {
s.contains(|c: char| {
matches!(c, '+' | '-' | '*' | '/' | '=' | '<' | '>' | '^' | '_' | '{' | '}'
| '\\' | '(' | ')' | '[' | ']' | '|' | '∑' | '∫' | '∏' | '√'
| 'α' | 'β' | 'γ' | 'δ' | 'ε' | 'θ' | 'λ' | 'μ' | 'π' | 'σ' | 'φ' | 'ω')
})
}
fn render_math_expression(expr: &str, display: bool) -> String {
let mut html = expr.to_string();
let replacements = [
("\\alpha", "α"), ("\\beta", "β"), ("\\gamma", "γ"), ("\\delta", "δ"),
("\\epsilon", "ε"), ("\\zeta", "ζ"), ("\\eta", "η"), ("\\theta", "θ"),
("\\iota", "ι"), ("\\kappa", "κ"), ("\\lambda", "λ"), ("\\mu", "μ"),
("\\nu", "ν"), ("\\xi", "ξ"), ("\\pi", "π"), ("\\rho", "ρ"),
("\\sigma", "σ"), ("\\tau", "τ"), ("\\upsilon", "υ"), ("\\phi", "φ"),
("\\chi", "χ"), ("\\psi", "ψ"), ("\\omega", "ω"),
("\\Alpha", "Α"), ("\\Beta", "Β"), ("\\Gamma", "Γ"), ("\\Delta", "Δ"),
("\\Theta", "Θ"), ("\\Lambda", "Λ"), ("\\Pi", "Π"), ("\\Sigma", "Σ"),
("\\Phi", "Φ"), ("\\Psi", "Ψ"), ("\\Omega", "Ω"),
("\\infty", "∞"), ("\\infinity", "∞"),
("\\sum", "∑"), ("\\prod", "∏"), ("\\int", "∫"),
("\\partial", "∂"), ("\\nabla", "∇"),
("\\pm", "±"), ("\\mp", "∓"), ("\\times", "×"), ("\\div", "÷"),
("\\cdot", "·"), ("\\ldots", "…"), ("\\cdots", "⋯"),
("\\leq", "≤"), ("\\geq", "≥"), ("\\neq", "≠"),
("\\approx", "≈"), ("\\equiv", "≡"), ("\\sim", "∼"),
("\\subset", "⊂"), ("\\supset", "⊃"), ("\\subseteq", "⊆"), ("\\supseteq", "⊇"),
("\\in", "∈"), ("\\notin", "∉"),
("\\cup", "∪"), ("\\cap", "∩"),
("\\forall", "∀"), ("\\exists", "∃"),
("\\neg", "¬"), ("\\land", "∧"), ("\\lor", "∨"),
("\\to", "→"), ("\\gets", "←"), ("\\leftrightarrow", "↔"),
("\\Rightarrow", "⇒"), ("\\Leftarrow", "⇐"), ("\\Leftrightarrow", "⇔"),
("\\sqrt", "√"), ("\\langle", "⟨"), ("\\rangle", "⟩"),
("\\lceil", "⌈"), ("\\rceil", "⌉"), ("\\lfloor", "⌊"), ("\\rfloor", "⌋"),
("\\emptyset", "∅"), ("\\varnothing", "∅"),
("\\mathbb{R}", "ℝ"), ("\\mathbb{Z}", "ℤ"), ("\\mathbb{N}", "ℕ"),
("\\mathbb{Q}", "ℚ"), ("\\mathbb{C}", "ℂ"),
];
for (cmd, unicode) in replacements {
html = html.replace(cmd, unicode);
}
html = replace_braced_command(&html, "^", "sup");
html = replace_braced_command(&html, "_", "sub");
html = render_fractions(&html);
html = strip_command(&html, "\\text");
html = strip_command(&html, "\\mathrm");
html = strip_command(&html, "\\mathbf");
let class = if display { "math-display" } else { "math-inline" };
let tag = if display { "div" } else { "span" };
format!("<{tag} class=\"{class}\">{html}</{tag}>")
}
fn replace_braced_command(text: &str, prefix: &str, tag: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = text.chars().collect();
let prefix_char = prefix.chars().next().unwrap();
let mut i = 0;
while i < chars.len() {
if chars[i] == prefix_char && i + 1 < chars.len() && chars[i + 1] == '{' {
if let Some(end) = find_closing_brace(&chars, i + 2) {
let content: String = chars[i + 2..end].iter().collect();
result.push_str(&format!("<{tag}>{content}</{tag}>"));
i = end + 1;
continue;
}
}
if chars[i] == prefix_char && i + 1 < chars.len() && chars[i + 1] != '{' && chars[i + 1].is_alphanumeric() {
result.push_str(&format!("<{tag}>{}</{tag}>", chars[i + 1]));
i += 2;
continue;
}
result.push(chars[i]);
i += 1;
}
result
}
fn find_closing_brace(chars: &[char], start: usize) -> Option<usize> {
let mut depth = 1;
let mut i = start;
while i < chars.len() {
match chars[i] {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
i += 1;
}
None
}
fn render_fractions(text: &str) -> String {
let mut result = text.to_string();
while let Some(pos) = result.find("\\frac{") {
let chars: Vec<char> = result.chars().collect();
let start = pos + 6; if let Some(mid) = find_closing_brace(&chars, start) {
let num: String = chars[start..mid].iter().collect();
if mid + 1 < chars.len() && chars[mid + 1] == '{' {
if let Some(end) = find_closing_brace(&chars, mid + 2) {
let den: String = chars[mid + 2..end].iter().collect();
let before: String = chars[..pos].iter().collect();
let after: String = chars[end + 1..].iter().collect();
result = format!("{before}<span class=\"math-frac\"><span class=\"math-num\">{num}</span><span class=\"math-den\">{den}</span></span>{after}");
continue;
}
}
}
break; }
result
}
fn strip_command(text: &str, cmd: &str) -> String {
let mut result = text.to_string();
let pattern = format!("{cmd}{{");
while let Some(pos) = result.find(&pattern) {
let chars: Vec<char> = result.chars().collect();
let start = pos + pattern.len();
if let Some(end) = find_closing_brace(&chars, start) {
let content: String = chars[start..end].iter().collect();
let before: String = chars[..pos].iter().collect();
let after: String = chars[end + 1..].iter().collect();
result = format!("{before}{content}{after}");
continue;
}
break;
}
result
}