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 and Citum contributors
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"\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        // In LaTeX, we could use custom commands if we wanted semantic tagging
105        // For now, just return content
106        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 bibliography(&self, entries: Vec<Self::Output>) -> Self::Output {
124        entries.join("\\par\\vspace{0.5em}")
125    }
126
127    fn entry(
128        &self,
129        _id: &str,
130        content: Self::Output,
131        _url: Option<&str>,
132        _metadata: &super::format::ProcEntryMetadata,
133    ) -> Self::Output {
134        format!("\\noindent\\hangindent=2em\\hangafter=1 {content}")
135    }
136}