summaryrefslogtreecommitdiff
path: root/src/format_latex.rs
diff options
context:
space:
mode:
authorCollin Williams <96917990+bluedragon1221@users.noreply.github.com>2026-06-14 16:41:52 -0500
committerCollin Williams <96917990+bluedragon1221@users.noreply.github.com>2026-06-14 16:41:52 -0500
commit3a03bdd7e7a2dbe1d83c33f5fbe8e94d6bac9f71 (patch)
treee814112f8ed166eddfca24426649ad48906d9339 /src/format_latex.rs
parente9469f6e7c4ff04cdc939030c5af4c1516a0271b (diff)
messy codeHEADmain
Diffstat (limited to 'src/format_latex.rs')
-rw-r--r--src/format_latex.rs194
1 files changed, 194 insertions, 0 deletions
diff --git a/src/format_latex.rs b/src/format_latex.rs
new file mode 100644
index 0000000..d754cb0
--- /dev/null
+++ b/src/format_latex.rs
@@ -0,0 +1,194 @@
+use crate::sexpr::{Atom, Expr};
+
+const PREC_EQUAL: u8 = 10;
+const PREC_ADD: u8 = 20;
+const PREC_MUL: u8 = 30;
+const PREC_POW: u8 = 40;
+const PREC_ATOM: u8 = 50;
+
+pub fn to_latex(expr: &Expr) -> String {
+ render_expr(expr, 0)
+}
+
+fn render_expr(expr: &Expr, parent_prec: u8) -> String {
+ let (rendered, prec) = render_expr_inner(expr);
+ if prec < parent_prec {
+ format!("\\left({}\\right)", rendered)
+ } else {
+ rendered
+ }
+}
+
+fn render_expr_inner(expr: &Expr) -> (String, u8) {
+ match expr {
+ Expr::Atom(atom) => (render_atom(atom), PREC_ATOM),
+ Expr::OrderedList(items) => {
+ let pieces: Vec<String> = items.iter().map(|item| render_expr(item, 0)).collect();
+ (format!("\\left[{}\\right]", pieces.join(", ")), PREC_ATOM)
+ }
+ Expr::Application(items) => render_application(items),
+ }
+}
+
+fn render_application(items: &[Expr]) -> (String, u8) {
+ let Some(Expr::Atom(Atom::Builtin(head))) = items.first() else {
+ let pieces: Vec<String> = items.iter().map(|item| render_expr(item, 0)).collect();
+ return (format!("\\left({}\\right)", pieces.join(" ")), PREC_ATOM);
+ };
+
+ let args = &items[1..];
+
+ match head.as_str() {
+ "+" => {
+ let pieces: Vec<String> = args.iter().map(|arg| render_expr(arg, PREC_ADD)).collect();
+ (pieces.join(" + "), PREC_ADD)
+ }
+ "*" => {
+ let pieces: Vec<String> = args.iter().map(|arg| render_expr(arg, PREC_MUL)).collect();
+ (pieces.join(" \\cdot "), PREC_MUL)
+ }
+ "=" => {
+ let pieces: Vec<String> = args.iter().map(|arg| render_expr(arg, PREC_EQUAL)).collect();
+ (pieces.join(" = "), PREC_EQUAL)
+ }
+ "sin" => unary_func("\\sin", args),
+ "cos" => unary_func("\\cos", args),
+ "d/dx" => {
+ if args.len() == 1 {
+ (
+ format!(
+ "\\frac{{d}}{{dx}}\\left({}\\right)",
+ render_expr(&args[0], 0)
+ ),
+ PREC_ATOM,
+ )
+ } else {
+ fallback_call(head, args)
+ }
+ }
+ _ => {
+ if let Some(power) = power_from_head(head) {
+ if args.len() == 1 {
+ let base = render_expr(&args[0], PREC_POW);
+ (format!("{}^{{{}}}", base, power), PREC_POW)
+ } else {
+ fallback_call(head, args)
+ }
+ } else {
+ fallback_call(head, args)
+ }
+ }
+ }
+}
+
+fn unary_func(name: &str, args: &[Expr]) -> (String, u8) {
+ if args.len() == 1 {
+ (format!("{}\\left({}\\right)", name, render_expr(&args[0], 0)), PREC_ATOM)
+ } else {
+ fallback_call(name, args)
+ }
+}
+
+fn fallback_call(head: &str, args: &[Expr]) -> (String, u8) {
+ let pieces: Vec<String> = args.iter().map(|arg| render_expr(arg, 0)).collect();
+ (
+ format!(
+ "{}\\left({}\\right)",
+ latex_symbol(head),
+ pieces.join(", ")
+ ),
+ PREC_ATOM,
+ )
+}
+
+fn power_from_head(head: &str) -> Option<&str> {
+ head.strip_prefix('^')
+ .filter(|suffix| !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()))
+}
+
+fn render_atom(atom: &Atom) -> String {
+ match atom {
+ Atom::Number(n) => n.to_string(),
+ Atom::Builtin(name) => latex_symbol(name),
+ Atom::Variable(var) => latex_text(&format!("{}{}", var.r#type.r#type_prefix(), var.index)),
+ }
+}
+
+fn latex_symbol(input: &str) -> String {
+ input.replace('_', "\\_")
+}
+
+fn latex_text(input: &str) -> String {
+ let escaped = input
+ .replace('\\', "\\textbackslash{}")
+ .replace('{', "\\{")
+ .replace('}', "\\}")
+ .replace('$', "\\$")
+ .replace('_', "\\_")
+ .replace('&', "\\&")
+ .replace('#', "\\#")
+ .replace('%', "\\%")
+ .replace('^', "\\^")
+ .replace('~', "\\~{}");
+ format!("\\text{{{}}}", escaped)
+}
+
+trait VariablePrefix {
+ fn r#type_prefix(&self) -> &'static str;
+}
+
+impl VariablePrefix for crate::sexpr::VariableType {
+ fn r#type_prefix(&self) -> &'static str {
+ match self {
+ crate::sexpr::VariableType::Number => "$",
+ crate::sexpr::VariableType::Expr => "@",
+ crate::sexpr::VariableType::NonNumberExpr => "!",
+ crate::sexpr::VariableType::Ellipsis => "..",
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::sexpr::{Number, Variable, VariableType};
+
+ fn num(n: f64) -> Expr {
+ Expr::Atom(Atom::Number(Number(n)))
+ }
+
+ fn sym(s: &str) -> Expr {
+ Expr::Atom(Atom::Builtin(s.to_string()))
+ }
+
+ fn app(items: Vec<Expr>) -> Expr {
+ Expr::Application(items)
+ }
+
+ #[test]
+ fn renders_basic_arithmetic() {
+ let expr = app(vec![sym("+"), num(2.0), app(vec![sym("*"), num(3.0), sym("x")])]);
+ assert_eq!(to_latex(&expr), "2 + 3 \\cdot x");
+ }
+
+ #[test]
+ fn renders_derivative_and_power() {
+ let expr = app(vec![sym("d/dx"), app(vec![sym("^2"), sym("x")])]);
+ assert_eq!(to_latex(&expr), "\\frac{d}{dx}\\left(x^{2}\\right)");
+ }
+
+ #[test]
+ fn renders_ordered_list() {
+ let expr = Expr::OrderedList(vec![sym("x"), num(1.0)]);
+ assert_eq!(to_latex(&expr), "\\left[x, 1\\right]");
+ }
+
+ #[test]
+ fn renders_variable_atom() {
+ let var = Expr::Atom(Atom::Variable(Variable {
+ r#type: VariableType::NonNumberExpr,
+ index: "a".to_string(),
+ }));
+ assert_eq!(to_latex(&var), "\\text{!a}");
+ }
+}