pub(crate) const PRELUDE: &str = include_str!("math.typ");
pub(crate) fn typst(source: &str) -> Option<String> {
let source = source.trim();
if source.is_empty() {
return None;
}
let converted = mitex::convert_math(source, None).ok()?;
(!converted.trim().is_empty()).then(|| retarget(&converted))
}
const RENAMED: &[(&str, &str)] = &[
("plus.circle", "plus.o"),
("minus.circle", "minus.o"),
("times.circle", "times.o"),
("dot.circle", "dot.o"),
("ast.circle", "ast.o"),
("dash.circle", "dash.o"),
("angle.l", "chevron.l"),
("angle.r", "chevron.r"),
];
fn retarget(converted: &str) -> String {
let mut out = converted.to_string();
for (old, new) in RENAMED {
out = replace_symbol(&out, old, new);
}
out
}
fn replace_symbol(text: &str, old: &str, new: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(at) = rest.find(old) {
let before = rest[..at].chars().next_back();
let after = rest[at + old.len()..].chars().next();
let starts = !before.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '.');
let ends = !after.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '-');
out.push_str(&rest[..at]);
out.push_str(if starts && ends { new } else { old });
rest = &rest[at + old.len()..];
}
out.push_str(rest);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn translates_what_typst_writes_differently() {
let out = typst(r"\sum_{i=1}^{n} i = \frac{n(n+1)}{2}").expect("converts");
assert!(out.contains("sum _(i = 1 )^(n )"), "{out}");
assert!(out.contains("frac("), "{out}");
}
#[test]
fn calls_a_handler_the_prelude_defines() {
let out = typst(r"\sqrt{4}").expect("converts");
assert!(out.contains("mitexsqrt"), "{out}");
assert!(PRELUDE.contains("#let mitexsqrt"), "the prelude defines it");
}
#[test]
fn puts_a_renamed_symbol_under_its_current_name() {
let out = typst(r"a \oplus b \quad \bigotimes \quad \langle x \rangle").expect("converts");
assert!(out.contains("plus.o"), "{out}");
assert!(
out.contains("times.o.big"),
"the modifier comes along: {out}"
);
assert!(
out.contains("chevron.l") && out.contains("chevron.r"),
"{out}"
);
assert!(!out.contains("circle"), "{out}");
}
#[test]
fn leaves_a_name_that_merely_begins_the_same_way() {
assert_eq!(
replace_symbol("angle.l", "angle.l", "chevron.l"),
"chevron.l"
);
assert_eq!(
replace_symbol("angle.left", "angle.l", "chevron.l"),
"angle.left"
);
assert_eq!(
replace_symbol("triangle.l", "angle.l", "chevron.l"),
"triangle.l"
);
assert_eq!(
replace_symbol("angle.l.big", "angle.l", "chevron.l"),
"chevron.l.big"
);
}
#[test]
fn declines_what_it_cannot_read() {
assert!(typst(r"\thiscommanddoesnotexist{x}").is_none());
assert!(typst(" ").is_none());
}
}