use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
const SYMBOLS: &[(&str, &str)] = &[
("alpha", "α"),
("beta", "β"),
("gamma", "γ"),
("delta", "δ"),
("epsilon", "ϵ"),
("varepsilon", "ε"),
("zeta", "ζ"),
("eta", "η"),
("theta", "θ"),
("vartheta", "ϑ"),
("iota", "ι"),
("kappa", "κ"),
("lambda", "λ"),
("mu", "μ"),
("nu", "ν"),
("xi", "ξ"),
("omicron", "ο"),
("pi", "π"),
("rho", "ρ"),
("sigma", "σ"),
("varsigma", "ς"),
("tau", "τ"),
("upsilon", "υ"),
("phi", "ϕ"),
("varphi", "φ"),
("chi", "χ"),
("psi", "ψ"),
("omega", "ω"),
("Alpha", "Α"),
("Beta", "Β"),
("Gamma", "Γ"),
("Delta", "Δ"),
("Epsilon", "Ε"),
("Zeta", "Ζ"),
("Eta", "Η"),
("Theta", "Θ"),
("Iota", "Ι"),
("Kappa", "Κ"),
("Lambda", "Λ"),
("Mu", "Μ"),
("Nu", "Ν"),
("Xi", "Ξ"),
("Omicron", "Ο"),
("Pi", "Π"),
("Rho", "Ρ"),
("Sigma", "Σ"),
("Tau", "Τ"),
("Upsilon", "Υ"),
("Phi", "Φ"),
("Chi", "Χ"),
("Psi", "Ψ"),
("Omega", "Ω"),
("sum", "∑"),
("prod", "∏"),
("coprod", "∐"),
("int", "∫"),
("iint", "∬"),
("iiint", "∭"),
("oint", "∮"),
("bigcap", "⋂"),
("bigcup", "⋃"),
("pm", "±"),
("mp", "∓"),
("times", "×"),
("div", "÷"),
("ast", "∗"),
("star", "⋆"),
("circ", "∘"),
("bullet", "•"),
("cdot", "⋅"),
("centerdot", "·"),
("cap", "∩"),
("cup", "∪"),
("oplus", "⊕"),
("ominus", "⊖"),
("otimes", "⊗"),
("oslash", "⊘"),
("odot", "⊙"),
("dagger", "†"),
("ddagger", "‡"),
("wr", "≀"),
("diamond", "⋄"),
("leq", "≤"),
("le", "≤"),
("geq", "≥"),
("ge", "≥"),
("ll", "≪"),
("gg", "≫"),
("neq", "≠"),
("ne", "≠"),
("equiv", "≡"),
("sim", "∼"),
("simeq", "≃"),
("approx", "≈"),
("cong", "≅"),
("propto", "∝"),
("asymp", "≍"),
("prec", "≺"),
("succ", "≻"),
("subset", "⊂"),
("supset", "⊃"),
("subseteq", "⊆"),
("supseteq", "⊇"),
("in", "∈"),
("notin", "∉"),
("ni", "∋"),
("mid", "∣"),
("parallel", "∥"),
("perp", "⊥"),
("leftarrow", "←"),
("to", "→"),
("rightarrow", "→"),
("leftrightarrow", "↔"),
("Leftarrow", "⇐"),
("Rightarrow", "⇒"),
("Leftrightarrow", "⇔"),
("uparrow", "↑"),
("downarrow", "↓"),
("updownarrow", "↕"),
("Uparrow", "⇑"),
("Downarrow", "⇓"),
("Updownarrow", "⇕"),
("mapsto", "↦"),
("hookleftarrow", "↩"),
("hookrightarrow", "↪"),
("implies", "⟹"),
("iff", "⟺"),
("infty", "∞"),
("partial", "∂"),
("nabla", "∇"),
("forall", "∀"),
("exists", "∃"),
("nexists", "∄"),
("emptyset", "∅"),
("varnothing", "∅"),
("angle", "∠"),
("triangle", "△"),
("square", "□"),
("ldots", "…"),
("dots", "…"),
("cdots", "⋯"),
("degree", "°"),
("checkmark", "✓"),
("hbar", "ℏ"),
("ell", "ℓ"),
("Re", "ℜ"),
("Im", "ℑ"),
("wp", "℘"),
("aleph", "ℵ"),
("mho", "℧"),
];
const SUBSCRIPT: &[(char, &str)] = &[
('0', "₀"),
('1', "₁"),
('2', "₂"),
('3', "₃"),
('4', "₄"),
('5', "₅"),
('6', "₆"),
('7', "₇"),
('8', "₈"),
('9', "₉"),
('+', "₊"),
('-', "₋"),
('=', "₌"),
('(', "₍"),
(')', "₎"),
('a', "ₐ"),
('e', "ₑ"),
('h', "ₕ"),
('i', "ᵢ"),
('j', "ⱼ"),
('k', "ₖ"),
('l', "ₗ"),
('m', "ₘ"),
('n', "ₙ"),
('o', "ₒ"),
('p', "ₚ"),
('r', "ᵣ"),
('s', "ₛ"),
('t', "ₜ"),
('u', "ᵤ"),
('v', "ᵥ"),
('x', "ₓ"),
];
const SUPERSCRIPT: &[(char, &str)] = &[
('0', "⁰"),
('1', "¹"),
('2', "²"),
('3', "³"),
('4', "⁴"),
('5', "⁵"),
('6', "⁶"),
('7', "⁷"),
('8', "⁸"),
('9', "⁹"),
('+', "⁺"),
('-', "⁻"),
('=', "⁼"),
('(', "⁽"),
(')', "⁾"),
('a', "ᵃ"),
('b', "ᵇ"),
('c', "ᶜ"),
('d', "ᵈ"),
('e', "ᵉ"),
('f', "ᶠ"),
('g', "ᵍ"),
('h', "ʰ"),
('i', "ⁱ"),
('j', "ʲ"),
('k', "ᵏ"),
('l', "ˡ"),
('m', "ᵐ"),
('n', "ⁿ"),
('o', "ᵒ"),
('p', "ᵖ"),
('r', "ʳ"),
('s', "ˢ"),
('t', "ᵗ"),
('u', "ᵘ"),
('v', "ᵛ"),
('w', "ʷ"),
('x', "ˣ"),
('y', "ʸ"),
('z', "ᶻ"),
];
const FUNCTIONS: &[&str] = &[
"sin", "cos", "tan", "cot", "sec", "csc", "sinh", "cosh", "tanh", "coth", "arcsin", "arccos",
"arctan", "ln", "log", "exp", "lim", "limsup", "liminf", "max", "min", "sup", "inf", "det",
"dim", "ker", "hom", "arg", "deg", "gcd", "lcm", "Pr", "mod",
];
const PRECOMPOSED_ACCENTS: &[(u8, char, &str)] = &[
(b'\'', 'a', "á"),
(b'\'', 'e', "é"),
(b'\'', 'i', "í"),
(b'\'', 'o', "ó"),
(b'\'', 'u', "ú"),
(b'\'', 'y', "ý"),
(b'\'', 'A', "Á"),
(b'\'', 'E', "É"),
(b'\'', 'I', "Í"),
(b'\'', 'O', "Ó"),
(b'\'', 'U', "Ú"),
(b'\'', 'Y', "Ý"),
(b'\'', 'c', "ć"),
(b'\'', 's', "ś"),
(b'\'', 'z', "ź"),
(b'\'', 'n', "ń"),
(b'`', 'a', "à"),
(b'`', 'e', "è"),
(b'`', 'i', "ì"),
(b'`', 'o', "ò"),
(b'`', 'u', "ù"),
(b'`', 'A', "À"),
(b'`', 'E', "È"),
(b'`', 'I', "Ì"),
(b'`', 'O', "Ò"),
(b'`', 'U', "Ù"),
(b'^', 'a', "â"),
(b'^', 'e', "ê"),
(b'^', 'i', "î"),
(b'^', 'o', "ô"),
(b'^', 'u', "û"),
(b'^', 'A', "Â"),
(b'^', 'E', "Ê"),
(b'^', 'I', "Î"),
(b'^', 'O', "Ô"),
(b'^', 'U', "Û"),
(b'"', 'a', "ä"),
(b'"', 'e', "ë"),
(b'"', 'i', "ï"),
(b'"', 'o', "ö"),
(b'"', 'u', "ü"),
(b'"', 'y', "ÿ"),
(b'"', 'A', "Ä"),
(b'"', 'E', "Ë"),
(b'"', 'I', "Ï"),
(b'"', 'O', "Ö"),
(b'"', 'U', "Ü"),
(b'~', 'n', "ñ"),
(b'~', 'N', "Ñ"),
(b'~', 'a', "ã"),
(b'~', 'o', "õ"),
(b'~', 'A', "Ã"),
(b'~', 'O', "Õ"),
(b'c', 'c', "ç"),
(b'c', 'C', "Ç"),
];
const ACCENT_COMBINING: &[(u8, &str)] = &[
(b'\'', "\u{0301}"), (b'`', "\u{0300}"), (b'^', "\u{0302}"), (b'"', "\u{0308}"), (b'~', "\u{0303}"), (b'=', "\u{0304}"), (b'u', "\u{0306}"), (b'.', "\u{0307}"), (b'v', "\u{030C}"), (b'H', "\u{030B}"), (b'k', "\u{0328}"), (b'r', "\u{030A}"), ];
fn accent_combining(name: &str) -> Option<&'static str> {
let mark: Option<&str> = match name {
"hat" | "widehat" => Some("\u{0302}"),
"check" | "widecheck" => Some("\u{030C}"),
"tilde" | "widetilde" => Some("\u{0303}"),
"acute" => Some("\u{0301}"),
"grave" => Some("\u{0300}"),
"dot" => Some("\u{0307}"),
"ddot" => Some("\u{0308}"),
"dddot" => Some("\u{20DB}"),
"ddddot" => Some("\u{20DC}"),
"breve" => Some("\u{0306}"),
"bar" | "overline" => Some("\u{0304}"),
"vec" | "overrightarrow" => Some("\u{20D7}"),
"overleftarrow" => Some("\u{20D6}"),
"mathring" => Some("\u{030A}"),
"underline" | "underbar" => Some("\u{0332}"),
_ => None,
};
mark
}
static SYMBOL_TABLE: LazyLock<HashMap<&'static str, &'static str>> =
LazyLock::new(|| SYMBOLS.iter().copied().collect());
static SUBSCRIPT_TABLE: LazyLock<HashMap<char, &'static str>> =
LazyLock::new(|| SUBSCRIPT.iter().copied().collect());
static SUPERSCRIPT_TABLE: LazyLock<HashMap<char, &'static str>> =
LazyLock::new(|| SUPERSCRIPT.iter().copied().collect());
static FUNCTION_SET: LazyLock<HashSet<&'static str>> =
LazyLock::new(|| FUNCTIONS.iter().copied().collect());
static PRECOMPOSED_ACCENT_TABLE: LazyLock<HashMap<(u8, char), &'static str>> =
LazyLock::new(|| {
PRECOMPOSED_ACCENTS
.iter()
.map(|&(a, b, g)| ((a, b), g))
.collect()
});
static ACCENT_COMBINING_TABLE: LazyLock<HashMap<u8, &'static str>> =
LazyLock::new(|| ACCENT_COMBINING.iter().copied().collect());
#[must_use]
pub fn has_latex(input: &str) -> bool {
if input.is_empty() {
return false;
}
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
if i + 1 < bytes.len() && bytes[i + 1].is_ascii_alphabetic() {
return true;
}
if i + 1 < bytes.len() && !bytes[i + 1].is_ascii_alphabetic() {
return true;
}
i += 1;
}
b'^' | b'_' => {
if i + 1 < bytes.len() {
let next = bytes[i + 1];
if next.is_ascii_alphanumeric() || next == b'{' || next == b'\\' {
return true;
}
}
i += 1;
}
_ => i += 1,
}
}
false
}
fn apply_accent_char(accent: u8, base: char) -> Option<String> {
if let Some(&g) = PRECOMPOSED_ACCENT_TABLE.get(&(accent, base)) {
return Some(g.to_string());
}
let &combining = ACCENT_COMBINING_TABLE.get(&accent)?;
let mut s = String::with_capacity(base.len_utf8() + combining.len());
s.push(base);
s.push_str(combining);
Some(s)
}
#[must_use]
pub fn latex_to_unicode(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
i += 1;
if i >= bytes.len() {
out.push('\\');
break;
}
let c = bytes[i];
if c.is_ascii_alphabetic() {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
i += 1;
}
let name = &input[start..i];
if let Some(&glyph) = SYMBOL_TABLE.get(name) {
out.push_str(glyph);
} else if FUNCTION_SET.contains(name) {
out.push_str(name);
} else if let Some(combining) = accent_combining(name) {
i = parse_named_accent(input, i, &mut out, combining);
} else if name == "frac" {
i = parse_frac(input, i, &mut out);
} else if name == "sqrt" {
i = parse_sqrt(input, i, &mut out);
} else {
out.push_str(name);
}
} else if let Some(&combining) = ACCENT_COMBINING_TABLE.get(&c) {
i += 1;
if i >= bytes.len() {
break;
}
if bytes[i] == b'{' {
let end = find_matching_brace(input, i + 1);
let inner = &input[i + 1..end.min(input.len())];
for ch in inner.chars() {
if let Some(acc) = apply_accent_char(c, ch) {
out.push_str(&acc);
} else {
out.push(ch);
out.push_str(combining);
}
}
i = if end < input.len() {
end + 1
} else {
input.len()
};
} else {
let base = bytes[i] as char;
if let Some(acc) = apply_accent_char(c, base) {
out.push_str(&acc);
} else {
out.push(base);
out.push_str(combining);
}
i += 1;
}
} else {
match c {
b'\\' => out.push('\n'), b'n' => out.push('\n'), b',' | b':' | b';' | b'>' => out.push(' '), b'!' => {} b'|' => out.push('‖'),
b'%' => out.push('%'),
b'#' => out.push('#'),
b'$' => out.push('$'),
b'&' => out.push('&'),
b'_' => out.push('_'),
b'{' => out.push('{'),
b'}' => out.push('}'),
b' ' => out.push(' '),
b'.' => out.push('.'),
_ => out.push(c as char),
}
i += 1;
}
}
b'^' => {
i += 1;
parse_script(input, i, &mut out, true);
i = skip_script_payload(input, i);
}
b'_' => {
i += 1;
parse_script(input, i, &mut out, false);
i = skip_script_payload(input, i);
}
b'{' => {
i += 1;
let group_end = find_matching_brace(input, i);
let inner = &input[i..group_end];
out.push_str(&latex_to_unicode(inner));
i = if group_end < input.len() {
group_end + 1
} else {
group_end
};
}
b'}' => {
i += 1;
}
b'$' | b'&' | b'#' | b'%' => {
i += 1;
}
_ => {
let start = i;
while i < bytes.len()
&& !matches!(
bytes[i],
b'\\' | b'^' | b'_' | b'{' | b'}' | b'$' | b'&' | b'#' | b'%'
)
{
i += 1;
}
out.push_str(&input[start..i]);
}
}
}
out
}
fn find_matching_brace(input: &str, open_after: usize) -> usize {
let bytes = input.as_bytes();
let mut depth = 1;
let mut i = open_after;
while i < bytes.len() {
match bytes[i] {
b'\\' if i + 1 < bytes.len() => i += 2,
b'{' => {
depth += 1;
i += 1;
}
b'}' => {
depth -= 1;
i += 1;
if depth == 0 {
return i - 1;
}
}
_ => i += 1,
}
}
input.len()
}
fn skip_script_payload(input: &str, i: usize) -> usize {
let bytes = input.as_bytes();
if i >= bytes.len() {
return i;
}
if bytes[i] == b'{' {
let end = find_matching_brace(input, i + 1);
if end < input.len() {
end + 1
} else {
input.len()
}
} else {
i + 1
}
}
fn parse_script(input: &str, i: usize, out: &mut String, sup: bool) {
let bytes = input.as_bytes();
if i >= bytes.len() {
return;
}
let payload = if bytes[i] == b'{' {
let end = find_matching_brace(input, i + 1);
&input[i + 1..end.min(input.len())]
} else {
let start = i;
let end = (i + 1).min(bytes.len());
&input[start..end]
};
let table = if sup {
&SUPERSCRIPT_TABLE
} else {
&SUBSCRIPT_TABLE
};
let mut mapped = String::new();
let mut all_ok = true;
for ch in payload.chars() {
match table.get(&ch) {
Some(&g) => mapped.push_str(g),
None => {
all_ok = false;
break;
}
}
}
if all_ok && !mapped.is_empty() {
out.push_str(&mapped);
} else {
out.push(if sup { '^' } else { '_' });
out.push('(');
out.push_str(payload);
out.push(')');
}
}
fn parse_named_accent(input: &str, mut i: usize, out: &mut String, combining: &str) -> usize {
let bytes = input.as_bytes();
while i < bytes.len() && bytes[i] == b' ' {
i += 1;
}
if i >= bytes.len() {
return i;
}
if bytes[i] == b'{' {
let end = find_matching_brace(input, i + 1);
let inner = &input[i + 1..end.min(input.len())];
for ch in inner.chars() {
out.push(ch);
out.push_str(combining);
}
if end < input.len() {
end + 1
} else {
input.len()
}
} else {
let ch = bytes[i] as char;
out.push(ch);
out.push_str(combining);
i + 1
}
}
fn parse_frac(input: &str, mut i: usize, out: &mut String) -> usize {
let (num, ni) = read_group_or_char(input, i);
out.push('(');
out.push_str(&latex_to_unicode(&num));
out.push(')');
out.push('/');
i = ni;
let (den, ni) = read_group_or_char(input, i);
out.push('(');
out.push_str(&latex_to_unicode(&den));
out.push(')');
ni
}
fn parse_sqrt(input: &str, mut i: usize, out: &mut String) -> usize {
let bytes = input.as_bytes();
while i < bytes.len() && bytes[i] == b' ' {
i += 1;
}
if i < bytes.len() && bytes[i] == b'[' {
let close = find_byte(bytes, i + 1, b']');
i = if close < bytes.len() {
close + 1
} else {
bytes.len()
};
}
let (payload, ni) = read_group_or_char(input, i);
if payload.chars().count() > 1 {
out.push('√');
out.push('(');
out.push_str(&latex_to_unicode(&payload));
out.push(')');
} else {
out.push('√');
out.push_str(&latex_to_unicode(&payload));
}
ni
}
fn find_byte(bytes: &[u8], from: usize, target: u8) -> usize {
let mut i = from;
while i < bytes.len() {
if bytes[i] == target {
return i;
}
i += 1;
}
bytes.len()
}
fn read_group_or_char(input: &str, i: usize) -> (String, usize) {
let bytes = input.as_bytes();
if i >= bytes.len() {
return (String::new(), i);
}
if bytes[i] == b'{' {
let end = find_matching_brace(input, i + 1);
let inner = &input[i + 1..end.min(input.len())];
let next = if end < input.len() {
end + 1
} else {
input.len()
};
(inner.to_string(), next)
} else {
let ch = (bytes[i] as char).to_string();
(ch, i + 1)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn greek_lowercase() {
assert_eq!(latex_to_unicode("\\alpha + \\beta"), "α + β");
assert_eq!(
latex_to_unicode("\\pi \\theta \\lambda \\mu \\sigma"),
"π θ λ μ σ"
);
assert_eq!(latex_to_unicode("\\omega \\psi \\phi \\chi"), "ω ψ ϕ χ");
}
#[test]
fn greek_uppercase() {
assert_eq!(latex_to_unicode("\\Delta \\Sigma \\Omega"), "Δ Σ Ω");
assert_eq!(latex_to_unicode("\\Gamma \\Lambda \\Pi"), "Γ Λ Π");
}
#[test]
fn math_operators() {
assert_eq!(latex_to_unicode("\\times"), "×");
assert_eq!(latex_to_unicode("\\div"), "÷");
assert_eq!(latex_to_unicode("\\pm \\mp"), "± ∓");
assert_eq!(latex_to_unicode("\\cdot"), "⋅");
assert_eq!(latex_to_unicode("\\sum \\prod \\int"), "∑ ∏ ∫");
}
#[test]
fn relations_and_comparisons() {
assert_eq!(latex_to_unicode("\\leq \\geq"), "≤ ≥");
assert_eq!(latex_to_unicode("\\neq \\approx \\equiv"), "≠ ≈ ≡");
assert_eq!(latex_to_unicode("\\infty \\partial \\nabla"), "∞ ∂ ∇");
}
#[test]
fn arrows() {
assert_eq!(latex_to_unicode("\\rightarrow \\leftarrow"), "→ ←");
assert_eq!(latex_to_unicode("\\Rightarrow \\Leftarrow"), "⇒ ⇐");
assert_eq!(
latex_to_unicode("\\leftrightarrow \\uparrow \\downarrow"),
"↔ ↑ ↓"
);
}
#[test]
fn sets_and_membership() {
assert_eq!(latex_to_unicode("\\in \\notin"), "∈ ∉");
assert_eq!(latex_to_unicode("\\subset \\supset"), "⊂ ⊃");
assert_eq!(latex_to_unicode("\\cup \\cap"), "∪ ∩");
assert_eq!(latex_to_unicode("\\emptyset"), "∅");
}
#[test]
fn subscripts_single_digit() {
assert_eq!(latex_to_unicode("x_1"), "x₁");
assert_eq!(latex_to_unicode("x_2"), "x₂");
assert_eq!(latex_to_unicode("x_0"), "x₀");
assert_eq!(latex_to_unicode("x_9"), "x₉");
}
#[test]
fn subscripts_group() {
assert_eq!(latex_to_unicode("x_{10}"), "x₁₀");
assert_eq!(latex_to_unicode("a_{ij}"), "aᵢⱼ");
assert_eq!(latex_to_unicode("y_{n+1}"), "yₙ₊₁");
}
#[test]
fn superscripts_single() {
assert_eq!(latex_to_unicode("x^2"), "x²");
assert_eq!(latex_to_unicode("x^3"), "x³");
assert_eq!(latex_to_unicode("x^0"), "x⁰");
assert_eq!(latex_to_unicode("x^+"), "x⁺");
assert_eq!(latex_to_unicode("x^-"), "x⁻");
}
#[test]
fn superscripts_group() {
assert_eq!(latex_to_unicode("x^{n+1}"), "xⁿ⁺¹");
assert_eq!(latex_to_unicode("e^{-1}"), "e⁻¹");
}
#[test]
fn accents() {
assert_eq!(latex_to_unicode("\\'e"), "é");
assert_eq!(latex_to_unicode("\\`e"), "è");
assert_eq!(latex_to_unicode("\\^e"), "ê");
assert_eq!(latex_to_unicode("\\\"e"), "ë");
assert_eq!(latex_to_unicode("\\~n"), "ñ");
assert_eq!(latex_to_unicode("\\'{e}"), "é");
assert_eq!(latex_to_unicode("\\^{ou}"), "ôû");
assert_eq!(latex_to_unicode("\\hat e"), "e\u{0302}");
assert_eq!(latex_to_unicode("\\bar y"), "y\u{0304}");
}
#[test]
fn common_symbols() {
assert_eq!(latex_to_unicode("\\degree"), "°");
assert_eq!(latex_to_unicode("\\checkmark"), "✓");
assert_eq!(latex_to_unicode("\\bullet"), "•");
assert_eq!(latex_to_unicode("\\dots"), "…");
}
#[test]
fn multi_replacement() {
assert_eq!(
latex_to_unicode("\\alpha^2 + \\beta_1 \\leq \\gamma"),
"α² + β₁ ≤ γ"
);
}
#[test]
fn nested_braces() {
assert_eq!(latex_to_unicode("\\frac{a}{b}"), "(a)/(b)");
assert_eq!(latex_to_unicode("\\sqrt{x+1}"), "√(x+1)");
assert_eq!(latex_to_unicode("{\\alpha\\beta}"), "αβ");
}
#[test]
fn no_match_passthrough() {
assert_eq!(latex_to_unicode("hello world"), "hello world");
assert_eq!(latex_to_unicode("\\unknown"), "unknown");
assert_eq!(latex_to_unicode("$x$"), "x");
}
#[test]
fn function_names_verbatim() {
assert_eq!(latex_to_unicode("\\sin(x)"), "sin(x)");
assert_eq!(latex_to_unicode("\\cos\\theta"), "cosθ");
}
#[test]
fn has_latex_detects_commands() {
assert!(has_latex("\\alpha"));
assert!(has_latex("x^2"));
assert!(has_latex("x_1"));
assert!(has_latex("\\frac{a}{b}"));
assert!(!has_latex("plain text"));
assert!(!has_latex(""));
assert!(!has_latex("no latex here"));
}
#[test]
fn empty_input() {
assert_eq!(latex_to_unicode(""), "");
assert!(!has_latex(""));
}
}