citum_engine/render/
latex.rs1use super::format::{OutputFormat, QuoteMarks};
9use citum_schema::template::WrapPunctuation;
10
11#[derive(Debug, Clone, Default)]
13pub struct Latex;
14
15impl Latex {
16 fn escape_href_target(url: &str) -> String {
24 url.replace('\\', r"\textbackslash{}")
25 .replace('%', r"\%")
26 .replace('#', r"\#")
27 }
28}
29
30impl OutputFormat for Latex {
31 type Output = String;
32
33 fn text(&self, s: &str) -> Self::Output {
34 let mut res = String::with_capacity(s.len() + 10);
35 for c in s.chars() {
36 match c {
37 '\\' => res.push_str(r"\textbackslash{}"),
38 '{' => res.push_str(r"\{"),
39 '}' => res.push_str(r"\}"),
40 '$' => res.push_str(r"\$"),
41 '&' => res.push_str(r"\&"),
42 '#' => res.push_str(r"\#"),
43 '_' => res.push_str(r"\_"),
44 '%' => res.push_str(r"\%"),
45 '~' => res.push_str(r"\textasciitilde{}"),
46 '^' => res.push_str(r"\textasciicircum{}"),
47 _ => res.push(c),
48 }
49 }
50 res
51 }
52
53 fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output {
54 items.join(&self.text(delimiter))
55 }
56
57 fn finish(&self, output: Self::Output) -> String {
58 let mut result = String::with_capacity(output.len() + 4);
62 let mut prev = '\0';
63 for c in output.chars() {
64 if c == '&' && prev != '\\' {
65 result.push_str(r"\&");
66 } else {
67 result.push(c);
68 }
69 prev = c;
70 }
71 result
72 }
73
74 fn emph(&self, content: Self::Output) -> Self::Output {
75 format!(r"\emph{{{content}}}")
76 }
77
78 fn strong(&self, content: Self::Output) -> Self::Output {
79 format!(r"\textbf{{{content}}}")
80 }
81
82 fn small_caps(&self, content: Self::Output) -> Self::Output {
83 format!(r"\textsc{{{content}}}")
84 }
85
86 fn superscript(&self, content: Self::Output) -> Self::Output {
87 format!(r"\textsuperscript{{{content}}}")
88 }
89
90 fn quote(&self, content: Self::Output, marks: &QuoteMarks) -> Self::Output {
91 let (open, close) = marks.for_depth(0);
92 format!("{open}{content}{close}")
93 }
94
95 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
96 format!("{}{}{}", self.text(prefix), content, self.text(suffix))
97 }
98
99 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
100 format!("{}{}{}", self.text(prefix), content, self.text(suffix))
101 }
102
103 fn wrap_punctuation(
104 &self,
105 wrap: &WrapPunctuation,
106 content: Self::Output,
107 marks: &QuoteMarks,
108 ) -> Self::Output {
109 match wrap {
110 WrapPunctuation::Parentheses => format!("({content})"),
111 WrapPunctuation::Brackets => format!("[{content}]"),
112 WrapPunctuation::Quotes => self.quote(content, marks),
113 }
114 }
115
116 fn semantic(&self, _class: &str, content: Self::Output) -> Self::Output {
117 content
120 }
121
122 fn annotation(&self, content: Self::Output) -> Self::Output {
123 if content.is_empty() {
124 return content;
125 }
126 format!(
127 "\n\\begin{{citumannotation}}\n{}\n\\end{{citumannotation}}",
128 content
129 )
130 }
131
132 fn link(&self, url: &str, content: Self::Output) -> Self::Output {
133 let target = Self::escape_href_target(url);
134 format!(r"\href{{{target}}}{{{content}}}")
135 }
136
137 fn paragraph(&self, content: Self::Output) -> Self::Output {
140 if content.is_empty() {
141 return content;
142 }
143 format!("{content}\n\n")
144 }
145
146 fn block_quote(&self, content: Self::Output) -> Self::Output {
147 if content.is_empty() {
148 return content;
149 }
150 let trimmed = content.trim_end();
151 format!("\\begin{{quote}}\n{trimmed}\n\\end{{quote}}\n\n")
152 }
153
154 fn bullet_list(&self, items: Vec<Self::Output>) -> Self::Output {
155 if items.is_empty() {
156 return String::new();
157 }
158 let body = items
159 .iter()
160 .map(|item| format!(" \\item {}", item.trim()))
161 .collect::<Vec<_>>()
162 .join("\n");
163 format!("\\begin{{itemize}}\n{body}\n\\end{{itemize}}\n\n")
164 }
165
166 fn ordered_list(&self, items: Vec<Self::Output>) -> Self::Output {
167 if items.is_empty() {
168 return String::new();
169 }
170 let body = items
171 .iter()
172 .map(|item| format!(" \\item {}", item.trim()))
173 .collect::<Vec<_>>()
174 .join("\n");
175 format!("\\begin{{enumerate}}\n{body}\n\\end{{enumerate}}\n\n")
176 }
177
178 fn heading(&self, level: u8, content: Self::Output) -> Self::Output {
179 let cmd = match level {
180 1 => "\\section",
181 2 => "\\subsection",
182 3 => "\\subsubsection",
183 _ => "\\paragraph",
184 };
185 format!("{cmd}{{{content}}}\n\n")
186 }
187
188 fn unnumbered_heading(&self, level: u8, content: Self::Output) -> Self::Output {
189 let cmd = match level {
190 1 => "\\section*",
191 2 => "\\subsection*",
192 3 => "\\subsubsection*",
193 _ => "\\paragraph*",
194 };
195 format!("{cmd}{{{content}}}\n\n")
196 }
197
198 fn code_block(&self, _lang: Option<&str>, content: Self::Output) -> Self::Output {
199 format!("\\begin{{verbatim}}\n{content}\\end{{verbatim}}\n\n")
200 }
201
202 fn inline_code(&self, content: Self::Output) -> Self::Output {
203 format!("\\texttt{{{}}}", self.text(&content))
205 }
206
207 fn strikeout(&self, content: Self::Output) -> Self::Output {
208 if content.is_empty() {
209 return content;
210 }
211 format!("\\sout{{{content}}}")
212 }
213
214 fn hard_break(&self) -> Self::Output {
215 "\\\\\n".to_string()
216 }
217
218 fn bibliography(&self, entries: Vec<Self::Output>) -> Self::Output {
219 entries.join("\\par\\vspace{0.5em}")
220 }
221
222 fn entry(
223 &self,
224 _id: &str,
225 content: Self::Output,
226 _url: Option<&str>,
227 _metadata: &super::format::ProcEntryMetadata,
228 ) -> Self::Output {
229 format!("\\noindent\\hangindent=2em\\hangafter=1 {content}")
230 }
231}