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(thread_title: &str) -> String {
12 format!(
13r#"\documentclass[12pt]{{article}}
14\usepackage[margin=1in]{{geometry}}
15\usepackage{{parskip}}
16\usepackage{{xcolor}}
17\usepackage{{titlesec}}
18\usepackage{{amsmath}} % better math
19\usepackage{{hyperref}}
20\usepackage[T1]{{fontenc}}
21\usepackage[utf8]{{inputenc}}
22\usepackage{{lmodern}}
23\usepackage{{graphicx}}
24
25% Pandoc fix for lists
26\providecommand{{\tightlist}}{{%
27 \setlength{{\itemsep}}{{0pt}}\setlength{{\parskip}}{{0pt}}}}
28
29% Message macro
30\newcommand{{\MessageBlock}}[3]{{
31 \vspace{{1em}}
32 \noindent\textbf{{#1}} \hfill \textit{{#2}} \\
33 #3
34 \par
35}}
36
37\begin{{document}}
38\begin{{center}}
39 \LARGE\bfseries {title} \\
40 \rule{{\linewidth}}{{0.4pt}}
41\end{{center}}
42"#,
43 title = thread_title
44 )
45}
46
47fn latex_end() -> &'static str {
49 r#"\end{document}"#
50}
51
52fn strip_emojis(input: &str) -> String {
54 input.chars().filter(|c| c.is_ascii() || c.is_alphanumeric() || c.is_whitespace()).collect()
55}
56
57pub fn render_message_tex(
59 fur_dir: &Path,
60 msg_id: &str,
61 label: String, args: &TimelineArgs,
63 avatars: &Value,
64 tex_out: &mut File,
65 depth: usize, ) {
67 let Some(msg) = load_message(fur_dir, msg_id, avatars) else { return };
68
69 let escape = |s: &str| {
71 s.replace("&", "\\&")
72 .replace("%", "\\%")
73 .replace("$", "\\$")
74 .replace("#", "\\#")
75 .replace("_", "\\_")
76 .replace("{", "\\{")
77 .replace("}", "\\}")
78 .replace("~", "\\textasciitilde{}")
79 .replace("^", "\\textasciicircum{}")
80 .replace("\n", " \\\\\n")
81 };
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(&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(&latex_body)
104 );
105 }
106 _ => {
107 let fallback = fs::read_to_string(path_str)
109 .map(|s| escape(&strip_emojis(&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(&msg.text))
118 }
119 } else {
120 escape(&strip_emojis(&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") {
131 full_content += &format!(
132 "\n\\begin{{center}}\\includegraphics[width=0.9\\linewidth]{{{}}}\\end{{center}}\n",
133 att
134 );
135 } else {
136 full_content += &format!("\n[Attachment: {}]\n", att);
137 }
138 }
139
140 writeln!(
141 tex_out,
142 "\\MessageBlock{{{}}}{{{} {} - {}}}{{{}}}",
143 escape(&msg.name),
144 msg.date_str,
145 msg.time_str,
146 label,
147 full_content
148 )
149 .unwrap();
150
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_message_tex(fur_dir, cid, branch_label.clone(), args, avatars, tex_out, depth + 1);
158 }
159 }
160}
161
162
163pub fn export_to_pdf(
165 fur_dir: &Path,
166 thread_title: &str,
167 root_msgs: &[Value],
168 args: &TimelineArgs,
169 avatars: &Value,
170 out_path: &str,
171) {
172 let tex_file = out_path.replace(".pdf", ".tex");
173 let mut file = File::create(&tex_file).expect("❌ Failed to create .tex file");
174
175 file.write_all(latex_preamble(thread_title).as_bytes()).unwrap();
177
178 for mid in root_msgs {
180 if let Some(mid_str) = mid.as_str() {
181 render_message_tex(fur_dir, mid_str, "Root".to_string(), args, avatars, &mut file, 0);
182 }
183 }
184
185 file.write_all(latex_end().as_bytes()).unwrap();
187
188 Command::new("pdflatex")
190 .arg("-interaction=nonstopmode")
191 .arg(&tex_file)
192 .status()
193 .expect("❌ Failed to run pdflatex");
194
195 println!("✔️ Exported LaTeX to {}", out_path);
196}