1use citum_schema::template::WrapPunctuation;
9
10#[must_use]
14pub fn unicode_quote_marks(depth: usize) -> (&'static str, &'static str) {
15 if depth.is_multiple_of(2) {
16 ("\u{201C}", "\u{201D}")
17 } else {
18 ("\u{2018}", "\u{2019}")
19 }
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SemanticAttribute {
25 pub name: &'static str,
27 pub value: String,
29}
30
31pub trait OutputFormat: Default + Clone {
36 type Output;
41
42 fn text(&self, s: &str) -> Self::Output;
47
48 fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output;
50
51 fn finish(&self, output: Self::Output) -> String;
56
57 fn emph(&self, content: Self::Output) -> Self::Output;
59
60 fn strong(&self, content: Self::Output) -> Self::Output;
62
63 fn small_caps(&self, content: Self::Output) -> Self::Output;
65
66 fn superscript(&self, content: Self::Output) -> Self::Output;
68
69 fn quote_marks(&self, depth: usize) -> (&'static str, &'static str) {
74 unicode_quote_marks(depth)
75 }
76
77 fn quote_with_depth(&self, content: Self::Output, depth: usize) -> Self::Output {
79 let (open, close) = self.quote_marks(depth);
80 self.affix(open, content, close)
81 }
82
83 fn quote(&self, content: Self::Output) -> Self::Output {
85 self.quote_with_depth(content, 0)
86 }
87
88 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output;
92
93 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output;
97
98 fn wrap_punctuation(&self, wrap: &WrapPunctuation, content: Self::Output) -> Self::Output;
100
101 fn semantic(&self, class: &str, content: Self::Output) -> Self::Output;
106
107 fn annotation(&self, content: Self::Output) -> Self::Output;
112
113 fn semantic_with_attributes(
118 &self,
119 class: &str,
120 content: Self::Output,
121 _attributes: &[SemanticAttribute],
122 ) -> Self::Output {
123 self.semantic(class, content)
124 }
125
126 fn citation(&self, _ids: Vec<String>, content: Self::Output) -> Self::Output {
128 content
129 }
130
131 fn link(&self, url: &str, content: Self::Output) -> Self::Output;
133
134 fn format_id(&self, id: &str) -> String {
136 id.to_string()
137 }
138
139 fn bibliography(&self, entries: Vec<Self::Output>) -> Self::Output {
143 self.join(entries, "\n\n")
144 }
145
146 fn entry(
150 &self,
151 _id: &str,
152 content: Self::Output,
153 _url: Option<&str>,
154 _metadata: &ProcEntryMetadata,
155 ) -> Self::Output {
156 content
157 }
158}
159
160#[derive(Debug, Clone, Default, PartialEq)]
162pub struct ProcEntryMetadata {
163 pub author: Option<String>,
165 pub year: Option<String>,
167 pub title: Option<String>,
169}
170
171#[cfg(test)]
172#[allow(
173 clippy::unwrap_used,
174 clippy::expect_used,
175 clippy::panic,
176 clippy::indexing_slicing,
177 clippy::todo,
178 clippy::unimplemented,
179 clippy::unreachable,
180 clippy::get_unwrap,
181 reason = "Panicking is acceptable and often desired in tests."
182)]
183mod tests {
184 use super::*;
185
186 #[derive(Default, Clone)]
187 struct DummyFormat;
188
189 impl OutputFormat for DummyFormat {
190 type Output = String;
191 fn text(&self, s: &str) -> Self::Output {
192 s.to_string()
193 }
194 fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output {
195 items.join(delimiter)
196 }
197 fn finish(&self, output: Self::Output) -> String {
198 output
199 }
200 fn emph(&self, content: Self::Output) -> Self::Output {
201 format!("emph({content})")
202 }
203 fn strong(&self, content: Self::Output) -> Self::Output {
204 format!("strong({content})")
205 }
206 fn small_caps(&self, content: Self::Output) -> Self::Output {
207 format!("sc({content})")
208 }
209 fn superscript(&self, content: Self::Output) -> Self::Output {
210 format!("sup({content})")
211 }
212 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
213 format!("{prefix}{content}{suffix}")
214 }
215 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
216 format!("{prefix}{content}{suffix}")
217 }
218 fn wrap_punctuation(&self, _wrap: &WrapPunctuation, content: Self::Output) -> Self::Output {
219 content
220 }
221 fn semantic(&self, class: &str, content: Self::Output) -> Self::Output {
222 format!("sem[{class}]({content})")
223 }
224 fn annotation(&self, content: Self::Output) -> Self::Output {
225 format!("annot({content})")
226 }
227 fn link(&self, url: &str, content: Self::Output) -> Self::Output {
228 format!("link[{url}]({content})")
229 }
230 }
231
232 #[test]
233 fn test_default_methods() {
234 let fmt = DummyFormat;
235 assert_eq!(
236 fmt.semantic_with_attributes("test", "content".to_string(), &[]),
237 "sem[test](content)"
238 );
239 assert_eq!(
240 fmt.citation(vec!["id1".to_string()], "content".to_string()),
241 "content"
242 );
243 assert_eq!(fmt.format_id("id1"), "id1");
244 assert_eq!(
245 fmt.bibliography(vec!["entry1".to_string(), "entry2".to_string()]),
246 "entry1\n\nentry2"
247 );
248 assert_eq!(
249 fmt.entry(
250 "id1",
251 "content".to_string(),
252 None,
253 &ProcEntryMetadata::default()
254 ),
255 "content"
256 );
257 }
258}