pub mod core;
pub mod data;
pub mod features;
pub mod utils;
#[cfg(feature = "wasm")]
pub mod wasm;
pub use core::typst2latex;
pub use core::typst2latex::T2LOptions;
pub use core::typst2latex::{
typst_document_to_latex, typst_to_latex, typst_to_latex_with_diagnostics,
typst_to_latex_with_eval, typst_to_latex_with_options, ConversionResult as T2LConversionResult,
};
pub use core::latex2typst::{
convert_document_with_ast, convert_document_with_ast_options, convert_math_with_ast,
convert_math_with_ast_options, convert_with_ast, convert_with_ast_options,
latex_math_to_typst_with_diagnostics, latex_math_to_typst_with_eval,
latex_to_typst_with_diagnostics, latex_to_typst_with_eval, ConversionMode,
ConversionResult as L2TConversionResult, ConversionState, EnvironmentContext, L2TOptions,
LatexConverter, WarningKind,
};
pub use data::constants;
pub use data::maps;
pub use features::bibtex;
pub use features::images;
pub use features::refs;
pub use features::tables;
pub use features::templates;
pub use features::tikz;
pub use data::colors;
pub use data::extended_symbols;
pub use data::physics;
pub use data::siunitx;
pub use data::symbols;
pub use utils::diagnostics;
pub use utils::error::{
CliDiagnostic, ConversionError, ConversionOutput, ConversionResult, ConversionWarning,
DiagnosticSeverity,
};
pub use utils::files;
pub use core::typst2latex::engine::{
self, expand_macros, ContentNode, EvalError, EvalResult, MiniEval, Value,
};
pub fn latex_to_typst(input: &str) -> String {
convert_math_with_ast(input)
}
pub fn latex_to_typst_with_options(input: &str, options: &L2TOptions) -> String {
convert_math_with_ast_options(input, options.clone())
}
pub fn latex_document_to_typst(input: &str) -> String {
convert_document_with_ast(input)
}
pub fn latex_document_to_typst_with_options(input: &str, options: &L2TOptions) -> String {
convert_document_with_ast_options(input, options.clone())
}
pub fn convert_auto(input: &str) -> (String, &'static str) {
let is_latex = input.contains('\\')
&& (input.contains("\\frac")
|| input.contains("\\alpha")
|| input.contains("\\sum")
|| input.contains("\\int")
|| input.contains("\\begin")
|| input.contains("\\section")
|| input.contains("\\documentclass"));
if is_latex {
(latex_to_typst(input), "typst")
} else {
(typst_to_latex(input), "latex")
}
}
pub fn convert_auto_document(input: &str) -> (String, &'static str) {
let is_latex = input.contains("\\documentclass")
|| input.contains("\\begin{document}")
|| (input.contains('\\') && (input.contains("\\section") || input.contains("\\chapter")));
let is_typst = input.contains("#set")
|| input.contains("#show")
|| input.starts_with('=')
|| input.contains("\n=");
if is_latex && !is_typst {
(latex_document_to_typst(input), "typst")
} else if is_typst && !is_latex {
(typst_document_to_latex(input), "latex")
} else if is_latex {
(latex_document_to_typst(input), "typst")
} else {
(typst_document_to_latex(input), "latex")
}
}
pub fn detect_format(input: &str) -> &'static str {
let latex_score: i32 = if input.contains("\\documentclass") {
10
} else {
0
} + if input.contains("\\begin{document}") {
10
} else {
0
} + if input.contains("\\section") { 5 } else { 0 }
+ if input.contains("\\frac") { 3 } else { 0 }
+ if input.contains("\\alpha") { 2 } else { 0 }
+ if input.contains("\\\\") { 2 } else { 0 }
+ (input.matches('\\').count() as i32);
let typst_score: i32 = if input.contains("#set") { 10 } else { 0 }
+ if input.contains("#show") { 10 } else { 0 }
+ if input.contains("#import") { 8 } else { 0 }
+ if input.starts_with('=') { 5 } else { 0 }
+ if input.contains("\n= ") { 5 } else { 0 }
+ if input.contains("frac(") { 3 } else { 0 }
+ if input.contains("sqrt(") { 3 } else { 0 };
if latex_score > typst_score + 3 {
"latex"
} else if typst_score > latex_score + 3 {
"typst"
} else if latex_score > 0 {
"latex"
} else if typst_score > 0 {
"typst"
} else {
"unknown"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_latex_to_typst_basic() {
let result = latex_to_typst(r"\alpha + \beta");
assert!(result.contains("alpha") || result.contains("α"));
assert!(result.contains("beta") || result.contains("β"));
}
#[test]
fn test_latex_to_typst_frac() {
let result = latex_to_typst(r"\frac{1}{2}");
assert!(result.contains("/") || result.contains("frac"));
}
#[test]
fn test_typst_to_latex_basic() {
let result = typst_to_latex("alpha + beta");
assert!(result.contains("alpha"));
assert!(result.contains("beta"));
}
#[test]
fn test_typst_to_latex_frac() {
let result = typst_to_latex("frac(1, 2)");
assert!(result.contains("frac"));
}
#[test]
fn test_convert_auto_latex() {
let (result, format) = convert_auto(r"\frac{1}{2}");
assert_eq!(format, "typst");
assert!(result.contains("/") || result.contains("frac"));
}
#[test]
fn test_convert_auto_typst() {
let (result, format) = convert_auto("alpha + beta");
assert_eq!(format, "latex");
assert!(result.contains("alpha"));
}
#[test]
fn test_detect_format_latex() {
assert_eq!(detect_format(r"\documentclass{article}"), "latex");
assert_eq!(detect_format(r"\frac{1}{2}"), "latex");
assert_eq!(detect_format(r"\begin{document}"), "latex");
}
#[test]
fn test_detect_format_typst() {
assert_eq!(detect_format("#set page(paper: \"a4\")"), "typst");
assert_eq!(detect_format("= Heading"), "typst");
assert_eq!(detect_format("#import \"test.typ\""), "typst");
}
#[test]
fn test_document_conversion_typst() {
let input = "= Hello\n\nWorld!";
let result = typst_document_to_latex(input);
assert!(result.contains("section"));
}
#[test]
fn test_l2t_options_prefer_shorthands() {
let opts_short = L2TOptions {
prefer_shorthands: true,
..Default::default()
};
let result_short = latex_to_typst_with_options(r"\rightarrow", &opts_short);
assert!(result_short.contains("->") || result_short.contains("arrow.r"));
let opts_long = L2TOptions {
prefer_shorthands: false,
..Default::default()
};
let result_long = latex_to_typst_with_options(r"\rightarrow", &opts_long);
assert!(result_long.contains("arrow.r"));
}
#[test]
fn test_l2t_options_infty_to_oo() {
let opts_default = L2TOptions {
infty_to_oo: false,
..Default::default()
};
let result_default = latex_to_typst_with_options(r"\infty", &opts_default);
assert!(result_default.contains("infinity"));
let opts_oo = L2TOptions {
infty_to_oo: true,
..Default::default()
};
let result_oo = latex_to_typst_with_options(r"\infty", &opts_oo);
assert!(result_oo.contains("oo"));
}
#[test]
fn test_l2t_options_frac_to_slash() {
let opts_slash = L2TOptions {
frac_to_slash: true,
..Default::default()
};
let result_slash = latex_to_typst_with_options(r"\frac{a}{b}", &opts_slash);
assert!(result_slash.contains("/") || result_slash.contains("frac"));
let opts_frac = L2TOptions {
frac_to_slash: false,
..Default::default()
};
let result_frac = latex_to_typst_with_options(r"\frac{a}{b}", &opts_frac);
assert!(result_frac.contains("frac("));
}
#[test]
fn test_l2t_options_preset_readable() {
let opts = L2TOptions::readable();
assert!(opts.prefer_shorthands);
assert!(opts.frac_to_slash);
assert!(opts.infty_to_oo);
}
#[test]
fn test_l2t_options_preset_verbose() {
let opts = L2TOptions::verbose();
assert!(!opts.prefer_shorthands);
assert!(!opts.frac_to_slash);
assert!(!opts.infty_to_oo);
}
#[test]
fn test_t2l_options_block_math_mode() {
let opts_block = T2LOptions {
block_math_mode: true,
math_only: true,
..Default::default()
};
let result_block = typst_to_latex_with_options("display(sum)", &opts_block);
assert!(result_block.contains("displaystyle"));
let opts_inline = T2LOptions {
block_math_mode: false,
math_only: true,
..Default::default()
};
let result_inline = typst_to_latex_with_options("display(sum)", &opts_inline);
assert!(result_inline.contains("displaystyle"));
}
#[test]
fn test_ifmmode_nested_full_conversion() {
let input = r#"\documentclass{article}
\usepackage{amsmath}
\newcommand{\RR}{\mathbb{R}}
\newcommand{\norm}[1]{\left\lVert #1 \right\rVert}
\newcommand{\inner}[2]{\langle #1, #2 \rangle}
\newcommand{\strong}[1]{\ifmmode \mathbf{#1} \else \textbf{#1} \fi}
\newcommand{\xvec}{\strong{x}}
\begin{document}
\section{Test}
Text: \xvec.
Math: $\norm{\xvec} = \sqrt{\inner{\xvec}{\xvec}}$
\end{document}
"#;
let result = latex_document_to_typst(input);
eprintln!("Full conversion result:\n{}", result);
let math_section = result.split("Math:").nth(1).unwrap_or("");
eprintln!("Math section: {}", math_section);
assert!(
!math_section.contains("*x*"),
"Math section should not have *x* (which is multiplication in Typst math), got: {}",
math_section
);
assert!(
math_section.contains("bold(x)") || math_section.contains("bold(x"),
"Math section should have bold(x), got: {}",
math_section
);
}
#[test]
fn test_ifmmode_bracket_display_math_full() {
let input = r#"\documentclass{article}
\usepackage{amsmath}
\newcommand{\norm}[1]{\left\lVert #1 \right\rVert}
\newcommand{\inner}[2]{\langle #1, #2 \rangle}
\newcommand{\strong}[1]{\ifmmode \mathbf{#1} \else \textbf{#1} \fi}
\newcommand{\xvec}{\strong{x}}
\begin{document}
\section{Test}
\[
\norm{\xvec} = \sqrt{\inner{\xvec}{\xvec}}
\]
\end{document}
"#;
let result = latex_document_to_typst(input);
eprintln!("Bracket display math result:\n{}", result);
assert!(
!result.contains("*x*"),
"Should not have *x* in result (would be multiplication in Typst math), got: {}",
result
);
assert!(
result.contains("bold(x)"),
"Should have bold(x) in display math, got: {}",
result
);
}
#[test]
fn test_langle_rangle_in_sqrt() {
let input = r#"\sqrt{\langle x, y \rangle}"#;
let result = latex_to_typst(input);
eprintln!("langle in sqrt result: {}", result);
assert!(
result.contains("sqrt({"),
"Should have sqrt({{...}}) wrapper to protect comma, got: {}",
result
);
assert!(
result.contains("chevron.l"),
"Should have chevron.l, got: {}",
result
);
assert!(
result.contains("chevron.r"),
"Should have chevron.r, got: {}",
result
);
}
#[test]
fn test_sqrt_without_comma_no_braces() {
let input = r#"\sqrt{x + y}"#;
let result = latex_to_typst(input);
eprintln!("sqrt without comma result: {}", result);
assert!(
!result.contains("sqrt({"),
"Should not have extra braces when no comma, got: {}",
result
);
assert!(
result.contains("sqrt("),
"Should have sqrt(...), got: {}",
result
);
}
#[test]
fn test_sqrt_with_nested_fraction_commas_does_not_add_braces() {
let input = r#"\sqrt{\frac{\partial f^2}{\partial x}+\frac{\partial f^2}{\partial y}}"#;
let result = latex_to_typst(input);
assert!(
!result.contains("sqrt({"),
"nested frac commas should not force extra sqrt braces, got: {}",
result
);
assert!(
result.contains("frac("),
"sqrt over fraction sum should still contain frac calls, got: {}",
result
);
}
#[test]
fn test_sqrt_with_nested_fraction_sum_no_extra_braces() {
let input = r#"\sqrt{\frac{a}{b}+\frac{c}{d}}"#;
let result = latex_to_typst(input);
assert!(
!result.contains("sqrt({"),
"nested fractions should not force extra sqrt braces, got: {}",
result
);
assert!(
result.contains("sqrt(")
&& (result.matches("frac(").count() >= 2 || result.contains("/")),
"expected nested fractions to remain as valid root content, got: {}",
result
);
}
}