1use std::fs::{self, File};
2use std::io::Write;
3use std::path::Path;
4use std::process::Command;
5use serde_json::Value;
6
7use crate::commands::timeline::TimelineArgs;
8use crate::renderer::utils::load_message;
9
10fn latex_preamble(conversation_title: &str) -> String {
11 format!(
12r#"\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.chars().filter(|c| c.is_ascii() || c.is_alphanumeric() || c.is_whitespace()).collect()
52}
53
54pub fn render_single_message_to_tex(
55 fur_dir: &Path,
56 msg_id: &str,
57 label: String, args: &TimelineArgs,
59 avatars: &Value,
60 tex_out: &mut File,
61 depth: usize, ) {
63 let Some(msg) = load_message(fur_dir, msg_id, avatars) else { return };
64
65 let escape = |s: &str| {
67 s.replace("&", "\\&")
68 .replace("%", "\\%")
69 .replace("$", "\\$")
70 .replace("#", "\\#")
71 .replace("_", "\\_")
72 .replace("{", "\\{")
73 .replace("}", "\\}")
74 .replace("~", "\\textasciitilde{}")
75 .replace("^", "\\textasciicircum{}")
76 .replace("\n", " \\\\\n")
77 };
78
79
80 let base_content = if args.verbose || args.contents {
82 if let Some(path_str) = msg.markdown.clone() {
83 let mut out = String::new();
84
85 if !msg.text.trim().is_empty() {
87 out += &format!("{}\n\n", escape(&strip_emojis_n_nonascii(&msg.text)));
88 }
89
90 match Command::new("pandoc")
92 .args(&["-f", "markdown", "-t", "latex", &path_str])
93 .output()
94 {
95 Ok(output) if output.status.success() => {
96 let latex_body = String::from_utf8_lossy(&output.stdout);
97 out += &format!(
98 "Attached document:\n\n\\begin{{quote}}\n{}\n\\end{{quote}}\n\\clearpage",
99 strip_emojis_n_nonascii(&latex_body)
100 );
101 }
102 _ => {
103 let fallback = fs::read_to_string(path_str)
105 .map(|s| escape(&strip_emojis_n_nonascii(&s)))
106 .unwrap_or_else(|_| String::from("[Markdown file missing]"));
107 out += &format!("{}\n\\clearpage", fallback);
108 }
109 }
110
111 out
112 } else {
113 escape(&strip_emojis_n_nonascii(&msg.text))
114 }
115 } else {
116 escape(&strip_emojis_n_nonascii(&msg.text))
117 };
118
119 let mut full_content = base_content.clone();
120
121 if let Some(att) = msg.attachment.clone() {
122 if att.ends_with(".png")
123 || att.ends_with(".jpg")
124 || att.ends_with(".jpeg")
125 || att.ends_with(".pdf") {
127 full_content += &format!(
128 "\n\\begin{{center}}\\includegraphics[width=0.9\\linewidth]{{{}}}\\end{{center}}\n",
129 att
130 );
131 } else {
132 full_content += &format!("\n[Attachment: {}]\n", att);
133 }
134 }
135
136 writeln!(
137 tex_out,
138 "\\MessageBlock{{{}}}{{{} {} - {}}}{{{}}}",
139 escape(&msg.name),
140 msg.date_str,
141 msg.time_str,
142 label,
143 full_content
144 )
145 .unwrap();
146
147
148 for (bi, block) in msg.branches.iter().enumerate() {
150 let branch_label = format!("{} - Branch {}", label, bi + 1);
151
152 for cid in block {
153 render_single_message_to_tex(fur_dir, cid, branch_label.clone(), args, avatars, tex_out, depth + 1);
154 }
155 }
156}
157
158
159pub fn export_convo_to_pdf(
160 fur_dir: &Path,
161 conversation_title: &str,
162 root_msgs: &[Value],
163 args: &TimelineArgs,
164 avatars: &Value,
165 out_path: &str,
166) {
167 let tex_file = out_path.replace(".pdf", ".tex");
168 let mut file = File::create(&tex_file).expect("❌ Failed to create .tex file");
169
170 file.write_all(latex_preamble(conversation_title).as_bytes()).unwrap();
172
173 for mid in root_msgs {
175 if let Some(mid_str) = mid.as_str() {
176 render_single_message_to_tex(fur_dir, mid_str, "Root".to_string(), args, avatars, &mut file, 0);
177 }
178 }
179
180 file.write_all(latex_ending().as_bytes()).unwrap();
182
183 Command::new("pdflatex")
185 .arg("-interaction=nonstopmode")
186 .arg(&tex_file)
187 .status()
188 .expect("❌ Failed to run pdflatex");
189
190 println!("✔️ Exported LaTeX to {}", out_path);
191}