Skip to main content

citum_engine/render/
plain.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Plain text output format.
7
8use super::format::OutputFormat;
9use citum_schema::template::WrapPunctuation;
10
11#[derive(Default, Clone)]
12/// Renders processed citations and bibliography entries as plain text.
13pub struct PlainText;
14
15impl OutputFormat for PlainText {
16    type Output = String;
17
18    fn text(&self, s: &str) -> Self::Output {
19        s.to_string()
20    }
21
22    fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output {
23        items.join(delimiter)
24    }
25
26    fn finish(&self, output: Self::Output) -> String {
27        output
28    }
29
30    fn emph(&self, content: Self::Output) -> Self::Output {
31        if content.is_empty() {
32            return content;
33        }
34        format!("_{content}_")
35    }
36
37    fn strong(&self, content: Self::Output) -> Self::Output {
38        if content.is_empty() {
39            return content;
40        }
41        format!("**{content}**")
42    }
43
44    fn small_caps(&self, content: Self::Output) -> Self::Output {
45        content.to_uppercase()
46    }
47
48    fn superscript(&self, content: Self::Output) -> Self::Output {
49        if content.is_empty() {
50            return content;
51        }
52        format!("^{content}^")
53    }
54
55    fn quote(&self, content: Self::Output) -> Self::Output {
56        if content.is_empty() {
57            return content;
58        }
59        format!("\u{201C}{content}\u{201D}")
60    }
61
62    fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
63        format!("{prefix}{content}{suffix}")
64    }
65
66    fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
67        format!("{prefix}{content}{suffix}")
68    }
69
70    fn wrap_punctuation(&self, wrap: &WrapPunctuation, content: Self::Output) -> Self::Output {
71        match wrap {
72            WrapPunctuation::Parentheses => format!("({content})"),
73            WrapPunctuation::Brackets => format!("[{content}]"),
74            WrapPunctuation::Quotes => format!("\u{201C}{content}\u{201D}"),
75        }
76    }
77
78    fn semantic(&self, _class: &str, content: Self::Output) -> Self::Output {
79        // Plain text ignores semantic classes
80        content
81    }
82
83    fn annotation(&self, content: Self::Output) -> Self::Output {
84        if content.is_empty() {
85            return content;
86        }
87
88        format!("\n\n{content}")
89    }
90
91    fn link(&self, _url: &str, content: Self::Output) -> Self::Output {
92        // Plain text just renders the text content of the link
93        content
94    }
95
96    fn entry(
97        &self,
98        _id: &str,
99        content: Self::Output,
100        _url: Option<&str>,
101        _metadata: &super::format::ProcEntryMetadata,
102    ) -> Self::Output {
103        content
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn small_caps_preserves_empty_text() {
113        let fmt = PlainText;
114
115        assert_eq!(fmt.small_caps(String::new()), "");
116    }
117
118    #[test]
119    fn small_caps_uppercases_plain_text() {
120        let fmt = PlainText;
121
122        assert_eq!(
123            fmt.small_caps("Smith and Lumière".to_string()),
124            "SMITH AND LUMIÈRE"
125        );
126    }
127}