1use serde_json::Value;
2use std::fs::{self, File};
3use std::io::Write;
4use std::path::Path;
5use std::process::Command;
6
7use crate::commands::timeline::TimelineArgs;
8use crate::renderer::utils::load_message;
9
10fn latex_preamble(conversation_title: &str) -> String {
11 format!(
12 r#"\documentclass[12pt]{{article}}
13\usepackage[margin=1in]{{geometry}}
14\usepackage{{parskip}}
15\usepackage{{xcolor}}
16\usepackage{{titlesec}}
17\usepackage{{amsmath}} % better math
18\usepackage{{hyperref}}
19\usepackage[T1]{{fontenc}}
20\usepackage[utf8]{{inputenc}}
21\usepackage{{lmodern}}
22\usepackage{{graphicx}}
23
24% Pandoc fix for lists
25\providecommand{{\tightlist}}{{%
26 \setlength{{\itemsep}}{{0pt}}\setlength{{\parskip}}{{0pt}}}}
27
28% Message macro
29\newcommand{{\MessageBlock}}[3]{{
30 \vspace{{1em}}
31 \noindent\textbf{{#1}} \hfill \textit{{#2}} \\
32 #3
33 \par
34}}
35
36\begin{{document}}
37\begin{{center}}
38 \LARGE\bfseries {title} \\
39 \rule{{\linewidth}}{{0.4pt}}
40\end{{center}}
41"#,
42 title = conversation_title
43 )
44}
45
46fn latex_ending() -> &'static str {
47 r#"\end{document}"#
48}
49
50fn strip_emojis_n_nonascii(input: &str) -> String {
51 input
52 .chars()
53 .filter(|c| c.is_ascii() || c.is_alphanumeric() || c.is_whitespace())
54 .collect()
55}
56
57pub fn render_single_message_to_tex(
58 fur_dir: &Path,
59 msg_id: &str,
60 label: String, args: &TimelineArgs,
62 avatars: &Value,
63 tex_out: &mut File,
64 depth: usize, ) {
66 let Some(msg) = load_message(fur_dir, msg_id, avatars) else {
67 return;
68 };
69
70 let escape = |s: &str| {
72 s.replace("&", "\\&")
73 .replace("%", "\\%")
74 .replace("$", "\\$")
75 .replace("#", "\\#")
76 .replace("_", "\\_")
77 .replace("{", "\\{")
78 .replace("}", "\\}")
79 .replace("~", "\\textasciitilde{}")
80 .replace("^", "\\textasciicircum{}")
81 .replace("\n", " \\\\\n")
82 };
83
84 let base_content = if args.verbose || args.contents {
86 if let Some(path_str) = msg.markdown.clone() {
87 let mut out = String::new();
88
89 if !msg.text.trim().is_empty() {
91 out += &format!("{}\n\n", escape(&strip_emojis_n_nonascii(&msg.text)));
92 }
93
94 match Command::new("pandoc")
96 .args(&["-f", "markdown", "-t", "latex", &path_str])
97 .output()
98 {
99 Ok(output) if output.status.success() => {
100 let latex_body = String::from_utf8_lossy(&output.stdout);
101 out += &format!(
102 "Attached document:\n\n\\begin{{quote}}\n{}\n\\end{{quote}}\n\\clearpage",
103 strip_emojis_n_nonascii(&latex_body)
104 );
105 }
106 _ => {
107 let fallback = fs::read_to_string(path_str)
109 .map(|s| escape(&strip_emojis_n_nonascii(&s)))
110 .unwrap_or_else(|_| String::from("[Markdown file missing]"));
111 out += &format!("{}\n\\clearpage", fallback);
112 }
113 }
114
115 out
116 } else {
117 escape(&strip_emojis_n_nonascii(&msg.text))
118 }
119 } else {
120 escape(&strip_emojis_n_nonascii(&msg.text))
121 };
122
123 let mut full_content = base_content.clone();
124
125 if let Some(att) = msg.attachment.clone() {
126 if att.ends_with(".png")
127 || att.ends_with(".jpg")
128 || att.ends_with(".jpeg")
129 || att.ends_with(".pdf")
130 {
132 full_content += &format!(
133 "\n\\begin{{center}}\\includegraphics[width=0.9\\linewidth]{{{}}}\\end{{center}}\n",
134 att
135 );
136 } else {
137 full_content += &format!("\n[Attachment: {}]\n", att);
138 }
139 }
140
141 writeln!(
142 tex_out,
143 "\\MessageBlock{{{}}}{{{} {} - {}}}{{{}}}",
144 escape(&msg.name),
145 msg.date_str,
146 msg.time_str,
147 label,
148 full_content
149 )
150 .unwrap();
151
152 for (bi, block) in msg.branches.iter().enumerate() {
154 let branch_label = format!("{} - Branch {}", label, bi + 1);
155
156 for cid in block {
157 render_single_message_to_tex(
158 fur_dir,
159 cid,
160 branch_label.clone(),
161 args,
162 avatars,
163 tex_out,
164 depth + 1,
165 );
166 }
167 }
168}
169
170pub fn export_convo_to_pdf(
171 fur_dir: &Path,
172 conversation_title: &str,
173 root_msgs: &[Value],
174 args: &TimelineArgs,
175 avatars: &Value,
176 out_path: &str,
177) {
178 let tex_file = out_path.replace(".pdf", ".tex");
179 let mut file = File::create(&tex_file).expect("❌ Failed to create .tex file");
180
181 file.write_all(latex_preamble(conversation_title).as_bytes())
183 .unwrap();
184
185 for mid in root_msgs {
187 if let Some(mid_str) = mid.as_str() {
188 render_single_message_to_tex(
189 fur_dir,
190 mid_str,
191 "Root".to_string(),
192 args,
193 avatars,
194 &mut file,
195 0,
196 );
197 }
198 }
199
200 file.write_all(latex_ending().as_bytes()).unwrap();
202
203 Command::new("pdflatex")
205 .arg("-interaction=nonstopmode")
206 .arg(&tex_file)
207 .status()
208 .expect("❌ Failed to run pdflatex");
209
210 println!("✔️ Exported LaTeX to {}", out_path);
211}