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, QuoteMarks, realize_wrap};
9use crate::values::ScriptClass;
10use citum_schema::template::WrapPunctuation;
11
12#[derive(Default, Clone)]
13/// Renders processed citations and bibliography entries as plain text.
14pub struct PlainText;
15
16impl OutputFormat for PlainText {
17    type Output = String;
18
19    fn text(&self, s: &str) -> Self::Output {
20        s.to_string()
21    }
22
23    fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output {
24        items.join(delimiter)
25    }
26
27    fn finish(&self, output: Self::Output) -> String {
28        output
29    }
30
31    fn heading(&self, level: u8, content: Self::Output) -> Self::Output {
32        let marks = "#".repeat(level.max(1) as usize);
33        format!("{marks} {content}\n\n")
34    }
35
36    fn emph(&self, content: Self::Output) -> Self::Output {
37        if content.is_empty() {
38            return content;
39        }
40        format!("_{content}_")
41    }
42
43    fn strong(&self, content: Self::Output) -> Self::Output {
44        if content.is_empty() {
45            return content;
46        }
47        format!("**{content}**")
48    }
49
50    fn small_caps(&self, content: Self::Output) -> Self::Output {
51        content.to_uppercase()
52    }
53
54    fn superscript(&self, content: Self::Output) -> Self::Output {
55        if content.is_empty() {
56            return content;
57        }
58        format!("^{content}^")
59    }
60
61    fn quote(&self, content: Self::Output, marks: &QuoteMarks) -> Self::Output {
62        if content.is_empty() {
63            return content;
64        }
65        let (open, close) = marks.for_depth(0);
66        format!("{open}{content}{close}")
67    }
68
69    fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
70        format!("{prefix}{content}{suffix}")
71    }
72
73    fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
74        format!("{prefix}{content}{suffix}")
75    }
76
77    fn wrap_punctuation(
78        &self,
79        wrap: &WrapPunctuation,
80        content: Self::Output,
81        marks: &QuoteMarks,
82        script: ScriptClass,
83        realization: Option<&citum_schema::options::PunctuationRealization>,
84    ) -> Self::Output {
85        match realize_wrap(wrap, script, realization) {
86            Some((open, close)) => {
87                format!("{}{}{}", self.text(&open), content, self.text(&close))
88            }
89            None => self.quote(content, marks),
90        }
91    }
92
93    fn semantic(&self, _class: &str, content: Self::Output) -> Self::Output {
94        // Plain text ignores semantic classes
95        content
96    }
97
98    fn annotation(&self, content: Self::Output) -> Self::Output {
99        if content.is_empty() {
100            return content;
101        }
102
103        format!("\n\n{content}")
104    }
105
106    fn link(&self, _url: &str, content: Self::Output) -> Self::Output {
107        // Plain text just renders the text content of the link
108        content
109    }
110
111    fn entry(
112        &self,
113        _id: &str,
114        content: Self::Output,
115        _url: Option<&str>,
116        _metadata: &super::format::ProcEntryMetadata,
117    ) -> Self::Output {
118        content
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn small_caps_preserves_empty_text() {
128        let fmt = PlainText;
129
130        assert_eq!(fmt.small_caps(String::new()), "");
131    }
132
133    #[test]
134    fn small_caps_uppercases_plain_text() {
135        let fmt = PlainText;
136
137        assert_eq!(
138            fmt.small_caps("Smith and Lumière".to_string()),
139            "SMITH AND LUMIÈRE"
140        );
141    }
142}