use serde_json::Value;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use std::process::Command;
use crate::commands::timeline::TimelineArgs;
use crate::renderer::utils::load_message;
fn latex_preamble(conversation_title: &str) -> String {
format!(
r#"\documentclass[12pt]{{article}}
\usepackage[margin=1in]{{geometry}}
\usepackage{{parskip}}
\usepackage{{xcolor}}
\usepackage{{titlesec}}
\usepackage{{amsmath}} % better math
\usepackage{{hyperref}}
\usepackage[T1]{{fontenc}}
\usepackage[utf8]{{inputenc}}
\usepackage{{lmodern}}
\usepackage{{graphicx}}
% Pandoc fix for lists
\providecommand{{\tightlist}}{{%
\setlength{{\itemsep}}{{0pt}}\setlength{{\parskip}}{{0pt}}}}
% Message macro
\newcommand{{\MessageBlock}}[3]{{
\vspace{{1em}}
\noindent\textbf{{#1}} \hfill \textit{{#2}} \\
#3
\par
}}
\begin{{document}}
\begin{{center}}
\LARGE\bfseries {title} \\
\rule{{\linewidth}}{{0.4pt}}
\end{{center}}
"#,
title = conversation_title
)
}
fn latex_ending() -> &'static str {
r#"\end{document}"#
}
fn strip_emojis_n_nonascii(input: &str) -> String {
input
.chars()
.filter(|c| c.is_ascii() || c.is_alphanumeric() || c.is_whitespace())
.collect()
}
pub fn render_single_message_to_tex(
fur_dir: &Path,
msg_id: &str,
label: String, args: &TimelineArgs,
avatars: &Value,
tex_out: &mut File,
depth: usize, ) {
let Some(msg) = load_message(fur_dir, msg_id, avatars) else {
return;
};
let escape = |s: &str| {
s.replace("&", "\\&")
.replace("%", "\\%")
.replace("$", "\\$")
.replace("#", "\\#")
.replace("_", "\\_")
.replace("{", "\\{")
.replace("}", "\\}")
.replace("~", "\\textasciitilde{}")
.replace("^", "\\textasciicircum{}")
.replace("\n", " \\\\\n")
};
let base_content = if args.verbose || args.contents {
if let Some(path_str) = msg.markdown.clone() {
let mut out = String::new();
if !msg.text.trim().is_empty() {
out += &format!("{}\n\n", escape(&strip_emojis_n_nonascii(&msg.text)));
}
match Command::new("pandoc")
.args(&["-f", "markdown", "-t", "latex", &path_str])
.output()
{
Ok(output) if output.status.success() => {
let latex_body = String::from_utf8_lossy(&output.stdout);
out += &format!(
"Attached document:\n\n\\begin{{quote}}\n{}\n\\end{{quote}}\n\\clearpage",
strip_emojis_n_nonascii(&latex_body)
);
}
_ => {
let fallback = fs::read_to_string(path_str)
.map(|s| escape(&strip_emojis_n_nonascii(&s)))
.unwrap_or_else(|_| String::from("[Markdown file missing]"));
out += &format!("{}\n\\clearpage", fallback);
}
}
out
} else {
escape(&strip_emojis_n_nonascii(&msg.text))
}
} else {
escape(&strip_emojis_n_nonascii(&msg.text))
};
let mut full_content = base_content.clone();
if let Some(att) = msg.attachment.clone() {
if att.ends_with(".png")
|| att.ends_with(".jpg")
|| att.ends_with(".jpeg")
|| att.ends_with(".pdf")
{
full_content += &format!(
"\n\\begin{{center}}\\includegraphics[width=0.9\\linewidth]{{{}}}\\end{{center}}\n",
att
);
} else {
full_content += &format!("\n[Attachment: {}]\n", att);
}
}
writeln!(
tex_out,
"\\MessageBlock{{{}}}{{{} {} - {}}}{{{}}}",
escape(&msg.name),
msg.date_str,
msg.time_str,
label,
full_content
)
.unwrap();
for (bi, block) in msg.branches.iter().enumerate() {
let branch_label = format!("{} - Branch {}", label, bi + 1);
for cid in block {
render_single_message_to_tex(
fur_dir,
cid,
branch_label.clone(),
args,
avatars,
tex_out,
depth + 1,
);
}
}
}
pub fn export_convo_to_pdf(
fur_dir: &Path,
conversation_title: &str,
root_msgs: &[Value],
args: &TimelineArgs,
avatars: &Value,
out_path: &str,
) {
let tex_file = out_path.replace(".pdf", ".tex");
let mut file = File::create(&tex_file).expect("❌ Failed to create .tex file");
file.write_all(latex_preamble(conversation_title).as_bytes())
.unwrap();
for mid in root_msgs {
if let Some(mid_str) = mid.as_str() {
render_single_message_to_tex(
fur_dir,
mid_str,
"Root".to_string(),
args,
avatars,
&mut file,
0,
);
}
}
file.write_all(latex_ending().as_bytes()).unwrap();
Command::new("pdflatex")
.arg("-interaction=nonstopmode")
.arg(&tex_file)
.status()
.expect("❌ Failed to run pdflatex");
println!("✔️ Exported LaTeX to {}", out_path);
}