Skip to main content

citum_engine/render/
latex.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus
4*/
5
6//! LaTeX output format.
7
8use super::format::OutputFormat;
9use citum_schema::template::WrapPunctuation;
10
11/// LaTeX renderer.
12#[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        // Escape any bare & not already preceded by backslash.
44        // Locale terms (e.g. the & from AndOptions::Symbol) bypass text() and
45        // arrive here unescaped; this final pass makes the output valid LaTeX.
46        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"\textit{{{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(&self, content: Self::Output) -> Self::Output {
76        format!("``{content}''")
77    }
78
79    fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
80        format!("{}{}{}", self.text(prefix), content, self.text(suffix))
81    }
82
83    fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
84        format!("{}{}{}", self.text(prefix), content, self.text(suffix))
85    }
86
87    fn wrap_punctuation(&self, wrap: &WrapPunctuation, content: Self::Output) -> Self::Output {
88        match wrap {
89            WrapPunctuation::Parentheses => format!("({content})"),
90            WrapPunctuation::Brackets => format!("[{content}]"),
91            WrapPunctuation::Quotes => self.quote(content),
92        }
93    }
94
95    fn semantic(&self, _class: &str, content: Self::Output) -> Self::Output {
96        // In LaTeX, we could use custom commands if we wanted semantic tagging
97        // For now, just return content
98        content
99    }
100
101    fn annotation(&self, content: Self::Output) -> Self::Output {
102        if content.is_empty() {
103            return content;
104        }
105        format!(
106            "\n\\begin{{citumannotation}}\n{}\n\\end{{citumannotation}}",
107            content
108        )
109    }
110
111    fn link(&self, url: &str, content: Self::Output) -> Self::Output {
112        format!(r"\href{{{url}}}{{{content}}}")
113    }
114
115    fn bibliography(&self, entries: Vec<Self::Output>) -> Self::Output {
116        entries.join("\\par\\vspace{0.5em}")
117    }
118
119    fn entry(
120        &self,
121        _id: &str,
122        content: Self::Output,
123        _url: Option<&str>,
124        _metadata: &super::format::ProcEntryMetadata,
125    ) -> Self::Output {
126        format!("\\noindent\\hangindent=2em\\hangafter=1 {content}")
127    }
128}