use crate::config::TexExportConfig;
pub fn typst_to_tex(input: &str, cfg: &TexExportConfig) -> String {
let body = tylax::typst_document_to_latex(input);
if body.contains("\\documentclass") {
apply_config(body, cfg)
} else {
wrap_with_preamble(&body, cfg)
}
}
fn overrides_configured(cfg: &TexExportConfig) -> bool {
!cfg.document_class.trim().is_empty()
|| cfg.extra_packages.iter().any(|p| !p.trim().is_empty())
|| cfg.preamble.iter().any(|p| !p.trim().is_empty())
}
fn documentclass_line(class: &str, opts: &str) -> String {
if opts.is_empty() {
format!("\\documentclass{{{class}}}")
} else {
format!("\\documentclass[{opts}]{{{class}}}")
}
}
fn extra_lines(cfg: &TexExportConfig) -> Vec<String> {
let mut lines = Vec::new();
for pkg in &cfg.extra_packages {
let p = pkg.trim();
if p.is_empty() {
continue;
}
if p.starts_with('\\') {
lines.push(p.to_string());
} else {
lines.push(format!("\\usepackage{{{p}}}"));
}
}
for line in &cfg.preamble {
lines.push(line.clone());
}
lines
}
fn apply_config(body: String, cfg: &TexExportConfig) -> String {
if !overrides_configured(cfg) {
return body;
}
let class = cfg.document_class.trim();
let opts = cfg.class_options.trim();
let trailing_newline = body.ends_with('\n');
let mut lines: Vec<String> = body.lines().map(str::to_string).collect();
if !class.is_empty() {
if let Some(i) = lines
.iter()
.position(|l| l.trim_start().starts_with("\\documentclass"))
{
lines[i] = documentclass_line(class, opts);
}
}
let existing: std::collections::HashSet<&str> =
lines.iter().map(|l| l.trim()).collect();
let extras: Vec<String> = extra_lines(cfg)
.into_iter()
.filter(|l| !existing.contains(l.trim()))
.collect();
if !extras.is_empty() {
let at = lines
.iter()
.position(|l| l.trim_start().starts_with("\\begin{document}"))
.unwrap_or(lines.len());
for (k, line) in extras.into_iter().enumerate() {
lines.insert(at + k, line);
}
}
let mut out = lines.join("\n");
if trailing_newline {
out.push('\n');
}
out
}
fn wrap_with_preamble(body: &str, cfg: &TexExportConfig) -> String {
let class = {
let c = cfg.document_class.trim();
if c.is_empty() { "article" } else { c }
};
let opts = cfg.class_options.trim();
let mut out = String::new();
out.push_str(&documentclass_line(class, opts));
out.push('\n');
out.push_str("\\usepackage[utf8]{inputenc}\n");
out.push_str("\\usepackage[T1]{fontenc}\n");
out.push_str("\\usepackage{graphicx}\n");
out.push_str("\\usepackage{hyperref}\n");
for line in extra_lines(cfg) {
out.push_str(&line);
out.push('\n');
}
out.push_str("\\begin{document}\n");
out.push_str(body);
out.push_str("\n\\end{document}\n");
out
}
#[cfg(test)]
mod tests {
use crate::config::TexExportConfig;
#[test]
fn default_config_leaves_tylax_article_untouched() {
let raw = tylax::typst_document_to_latex("= Head\n\ntext\n");
let out = super::typst_to_tex("= Head\n\ntext\n", &TexExportConfig::default());
assert_eq!(out, raw);
assert!(out.contains("\\documentclass{article}"));
}
#[test]
fn journal_class_rewrites_and_injects_packages() {
let cfg = TexExportConfig {
document_class: "IEEEtran".into(),
class_options: "conference".into(),
extra_packages: vec!["amsmath".into(), "\\usepackage[numbers]{natbib}".into()],
preamble: vec!["\\pagestyle{empty}".into()],
};
let out = super::typst_to_tex("= H\n\nbody\n", &cfg);
assert!(out.contains("\\documentclass[conference]{IEEEtran}"), "{out}");
assert!(!out.contains("\\documentclass{article}"), "{out}");
assert!(out.contains("\\usepackage{amsmath}"));
assert!(out.contains("\\usepackage[numbers]{natbib}"));
assert!(out.contains("\\pagestyle{empty}"));
let doc = out.find("\\begin{document}").unwrap();
assert!(out.find("\\pagestyle{empty}").unwrap() < doc);
}
#[test]
fn class_only_override_keeps_tylax_packages() {
let cfg = TexExportConfig {
document_class: "elsarticle".into(),
class_options: String::new(),
..Default::default()
};
let out = super::typst_to_tex("body\n", &cfg);
assert!(out.contains("\\documentclass{elsarticle}"), "{out}");
assert!(!out.contains("\\documentclass[]"));
assert!(out.contains("\\usepackage{amsmath}"));
}
}