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