1use std::path::Path;
4
5use cmark_writer::ast::Node;
6use ecow::EcoString;
7use tinymist_std::path::unix_slash;
8
9use crate::Result;
10use crate::common::{
11 CenterNode, ExternalFrameNode, FigureNode, FormatWriter, HighlightNode, InlineNode, ListState,
12 VerbatimNode,
13};
14
15pub struct LaTeXWriter {
17 list_state: Option<ListState>,
18}
19
20impl Default for LaTeXWriter {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl LaTeXWriter {
27 pub fn new() -> Self {
28 Self { list_state: None }
29 }
30
31 fn write_inline_nodes(&mut self, nodes: &[Node], output: &mut EcoString) -> Result<()> {
32 for node in nodes {
33 self.write_node(node, output)?;
34 }
35 Ok(())
36 }
37
38 fn write_node(&mut self, node: &Node, output: &mut EcoString) -> Result<()> {
40 match node {
41 Node::Document(blocks) => {
42 for block in blocks {
43 self.write_node(block, output)?;
44 }
45 }
46 Node::Paragraph(inlines) => {
47 self.write_inline_nodes(inlines, output)?;
48 output.push_str("\n\n");
49 }
50 Node::Heading {
51 level,
52 content,
53 heading_type: _,
54 } => {
55 if *level > 4 {
56 return Err(format!("heading level {level} is not supported in LaTeX").into());
57 }
58
59 output.push('\\');
60 match level {
61 1 => output.push_str("chapter{"),
62 2 => output.push_str("section{"),
63 3 => output.push_str("subsection{"),
64 4 => output.push_str("subsubsection{"),
65 _ => return Err(format!("Heading level {level} is not supported").into()),
66 }
67
68 self.write_inline_nodes(content, output)?;
69 output.push_str("}\n\n");
70 }
71 Node::BlockQuote(content) => {
72 output.push_str("\\begin{quote}\n");
73 for block in content {
74 self.write_node(block, output)?;
75 }
76 output.push_str("\\end{quote}\n");
77 }
78 Node::CodeBlock {
79 language,
80 content,
81 block_type: _,
82 } => {
83 if let Some(lang) = language {
84 if !lang.is_empty() {
85 output.push_str("\\begin{lstlisting}[language=");
86 output.push_str(lang);
87 output.push_str("]\n");
88 } else {
89 output.push_str("\\begin{verbatim}\n");
90 }
91 } else {
92 output.push_str("\\begin{verbatim}\n");
93 }
94
95 output.push_str(content);
96
97 if language.as_ref().is_none_or(|lang| lang.is_empty()) {
98 output.push_str("\n\\end{verbatim}");
99 } else {
100 output.push_str("\n\\end{lstlisting}");
101 }
102 output.push_str("\n\n");
103 }
104 Node::OrderedList { start: _, items } => {
105 let previous_state = self.list_state;
106 self.list_state = Some(ListState::Ordered);
107
108 output.push_str("\\begin{enumerate}\n");
109 for item in items {
110 match item {
111 cmark_writer::ast::ListItem::Ordered { content, .. }
112 | cmark_writer::ast::ListItem::Unordered { content } => {
113 output.push_str("\\item ");
114 for block in content {
115 match block {
116 Node::Paragraph(inlines) => {
119 self.write_inline_nodes(inlines, output)?;
120 }
121 _ => self.write_node(block, output)?,
122 }
123 }
124 output.push('\n');
125 }
126 _ => {}
127 }
128 }
129 output.push_str("\\end{enumerate}\n\n");
130
131 self.list_state = previous_state;
132 }
133 Node::UnorderedList(items) => {
134 let previous_state = self.list_state;
135 self.list_state = Some(ListState::Unordered);
136
137 output.push_str("\\begin{itemize}\n");
138 for item in items {
139 match item {
140 cmark_writer::ast::ListItem::Ordered { content, .. }
141 | cmark_writer::ast::ListItem::Unordered { content } => {
142 output.push_str("\\item ");
143 for block in content {
144 match block {
145 Node::Paragraph(inlines) => {
148 self.write_inline_nodes(inlines, output)?;
149 }
150 _ => self.write_node(block, output)?,
151 }
152 }
153 output.push('\n');
154 }
155 _ => {}
156 }
157 }
158 output.push_str("\\end{itemize}\n\n");
159
160 self.list_state = previous_state;
161 }
162 Node::Table {
163 headers,
164 rows,
165 alignments: _,
166 } => {
167 let col_count = headers
169 .len()
170 .max(rows.iter().map(|row| row.len()).max().unwrap_or(0));
171
172 output.push_str("\\begin{table}[htbp]\n");
173 output.push_str("\\centering\n");
174 output.push_str("\\begin{tabular}{");
175
176 for _ in 0..col_count {
178 output.push('c');
179 }
180 output.push_str("}\n\\hline\n");
181
182 if !headers.is_empty() {
184 for (i, cell) in headers.iter().enumerate() {
185 if i > 0 {
186 output.push_str(" & ");
187 }
188 self.write_node(cell, output)?;
189 }
190 output.push_str(" \\\\\n\\hline\n");
191 }
192
193 for row in rows {
195 for (i, cell) in row.iter().enumerate() {
196 if i > 0 {
197 output.push_str(" & ");
198 }
199 self.write_node(cell, output)?;
200 }
201 output.push_str(" \\\\\n");
202 }
203
204 output.push_str("\\hline\n");
206 output.push_str("\\end{tabular}\n");
207 output.push_str("\\end{table}\n\n");
208 }
209 node if node.is_custom_type::<FigureNode>() => {
210 let figure_node = node.as_custom_type::<FigureNode>().unwrap();
211 output.push_str("\\begin{figure}[htbp]\n\\centering\n");
213
214 match &*figure_node.body {
216 Node::Paragraph(content) => {
217 for node in content {
218 if let Node::Image {
220 url,
221 title: _,
222 alt: _,
223 } = node
224 {
225 let path = unix_slash(Path::new(url.as_str()));
227
228 output.push_str("\\includegraphics[width=0.8\\textwidth]{");
230 output.push_str(&path);
231 output.push_str("}\n");
232 } else {
233 self.write_node(node, output)?;
235 }
236 }
237 }
238 node => self.write_node(node, output)?,
240 }
241
242 if !figure_node.caption.is_empty() {
244 output.push_str("\\caption{");
245 output.push_str(&escape_latex(&figure_node.caption));
246 output.push_str("}\n");
247 }
248
249 output.push_str("\\end{figure}\n\n");
251 }
252 node if node.is_custom_type::<ExternalFrameNode>() => {
253 let external_frame = node.as_custom_type::<ExternalFrameNode>().unwrap();
254 let path = unix_slash(&external_frame.file_path);
256
257 output.push_str("\\begin{figure}[htbp]\n");
258 output.push_str("\\centering\n");
259 output.push_str("\\includegraphics[width=0.8\\textwidth]{");
260 output.push_str(&path);
261 output.push_str("}\n");
262
263 if !external_frame.alt_text.is_empty() {
264 output.push_str("\\caption{");
265 output.push_str(&escape_latex(&external_frame.alt_text));
266 output.push_str("}\n");
267 }
268
269 output.push_str("\\end{figure}\n\n");
270 }
271 node if node.is_custom_type::<CenterNode>() => {
272 let center_node = node.as_custom_type::<CenterNode>().unwrap();
273 output.push_str("\\begin{center}\n");
274 self.write_node(¢er_node.node, output)?;
275 output.push_str("\\end{center}\n\n");
276 }
277 node if node.is_custom_type::<HighlightNode>() => {
278 let highlight_node = node.as_custom_type::<HighlightNode>().unwrap();
279 output.push_str("\\colorbox{yellow}{");
280 for child in &highlight_node.content {
281 self.write_node(child, output)?;
282 }
283 output.push_str("}");
284 }
285 node if node.is_custom_type::<InlineNode>() => {
286 let inline_node = node.as_custom_type::<InlineNode>().unwrap();
287 for child in &inline_node.content {
289 self.write_node(child, output)?;
290 }
291 }
292 node if node.is_custom_type::<VerbatimNode>() => {
293 let inline_node = node.as_custom_type::<VerbatimNode>().unwrap();
294 output.push_str(&inline_node.content);
295 }
296 Node::Text(text) => {
297 output.push_str(&escape_latex(text));
298 }
299 Node::Emphasis(content) => {
300 output.push_str("\\textit{");
301 self.write_inline_nodes(content, output)?;
302 output.push_str("}");
303 }
304 Node::Strong(content) => {
305 output.push_str("\\textbf{");
306 self.write_inline_nodes(content, output)?;
307 output.push_str("}");
308 }
309 Node::Strikethrough(content) => {
310 output.push_str("\\sout{");
311 self.write_inline_nodes(content, output)?;
312 output.push_str("}");
313 }
314 Node::Link {
315 url,
316 title: _,
317 content,
318 } => {
319 output.push_str("\\href{");
320 output.push_str(url);
321 output.push_str("}{");
322 self.write_inline_nodes(content, output)?;
323 output.push_str("}");
324 }
325 Node::Image { url, title: _, alt } => {
326 let alt_text = if !alt.is_empty() {
327 let mut alt_str = EcoString::new();
328 self.write_inline_nodes(alt, &mut alt_str)?;
329 alt_str
330 } else {
331 "".into()
332 };
333
334 let path = unix_slash(Path::new(&url.as_str()));
335
336 output.push_str("\\begin{figure}\n");
337 output.push_str("\\centering\n");
338 output.push_str("\\includegraphics[width=0.8\\textwidth]{");
339 output.push_str(&path);
340 output.push_str("}\n");
341
342 if !alt_text.is_empty() {
343 output.push_str("\\caption{");
344 output.push_str(&alt_text);
345 output.push_str("}\n");
346 }
347
348 output.push_str("\\end{figure}\n\n");
349 }
350 Node::InlineCode(code) => {
351 output.push_str("\\texttt{");
352 output.push_str(&escape_latex(code));
353 output.push_str("}");
354 }
355 Node::HardBreak => {
356 output.push_str("\\\\\n");
357 }
358 Node::SoftBreak => {
359 output.push(' ');
360 }
361 Node::ThematicBreak => {
362 output.push_str("\\hrule\n\n");
363 }
364 Node::HtmlElement(element) => {
365 for child in &element.children {
366 self.write_node(child, output)?;
367 }
368 }
369 _ => {}
370 }
371
372 Ok(())
373 }
374}
375
376fn escape_latex(text: &str) -> String {
378 text.replace('&', "\\&")
379 .replace('%', "\\%")
380 .replace('$', "\\$")
381 .replace('#', "\\#")
382 .replace('_', "\\_")
383 .replace('{', "\\{")
384 .replace('}', "\\}")
385 .replace('~', "\\textasciitilde{}")
386 .replace('^', "\\textasciicircum{}")
387 .replace('\\', "\\textbackslash{}")
388}
389
390impl FormatWriter for LaTeXWriter {
391 fn write_eco(&mut self, document: &Node, output: &mut EcoString) -> Result<()> {
392 self.write_node(document, output)?;
394 Ok(())
395 }
396
397 fn write_vec(&mut self, document: &Node) -> Result<Vec<u8>> {
398 let mut output = EcoString::new();
399 self.write_eco(document, &mut output)?;
400 Ok(output.as_str().as_bytes().to_vec())
401 }
402}