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 paragraph(&self, content: Self::Output) -> Self::Output {
118 content
119 }
120
121 fn block_quote(&self, content: Self::Output) -> Self::Output {
123 content
124 }
125
126 fn bullet_list(&self, items: Vec<Self::Output>) -> Self::Output {
128 self.join(items, "\n")
129 }
130
131 fn ordered_list(&self, items: Vec<Self::Output>) -> Self::Output {
133 self.join(items, "\n")
134 }
135
136 fn list_item(&self, content: Self::Output) -> Self::Output {
138 content
139 }
140
141 fn heading(&self, _level: u8, content: Self::Output) -> Self::Output {
143 content
144 }
145
146 fn code_block(&self, _lang: Option<&str>, content: Self::Output) -> Self::Output {
150 content
151 }
152
153 fn inline_code(&self, content: Self::Output) -> Self::Output {
155 content
156 }
157
158 fn strikeout(&self, content: Self::Output) -> Self::Output {
160 content
161 }
162
163 fn hard_break(&self) -> Self::Output {
165 self.text(" ")
166 }
167
168 fn semantic_with_attributes(
173 &self,
174 class: &str,
175 content: Self::Output,
176 _attributes: &[SemanticAttribute],
177 ) -> Self::Output {
178 self.semantic(class, content)
179 }
180
181 fn citation(&self, _ids: Vec<String>, content: Self::Output) -> Self::Output {
183 content
184 }
185
186 fn link(&self, url: &str, content: Self::Output) -> Self::Output;
188
189 fn format_id(&self, id: &str) -> String {
191 id.to_string()
192 }
193
194 fn bibliography(&self, entries: Vec<Self::Output>) -> Self::Output {
198 self.join(entries, "\n\n")
199 }
200
201 fn entry(
205 &self,
206 _id: &str,
207 content: Self::Output,
208 _url: Option<&str>,
209 _metadata: &ProcEntryMetadata,
210 ) -> Self::Output {
211 content
212 }
213}
214
215#[derive(Debug, Clone, Default, PartialEq)]
217pub struct ProcEntryMetadata {
218 pub author: Option<String>,
220 pub year: Option<String>,
222 pub title: Option<String>,
224}
225
226#[cfg(test)]
227#[allow(
228 clippy::unwrap_used,
229 clippy::expect_used,
230 clippy::panic,
231 clippy::indexing_slicing,
232 clippy::todo,
233 clippy::unimplemented,
234 clippy::unreachable,
235 clippy::get_unwrap,
236 reason = "Panicking is acceptable and often desired in tests."
237)]
238mod tests {
239 use super::*;
240
241 #[derive(Default, Clone)]
242 struct DummyFormat;
243
244 impl OutputFormat for DummyFormat {
245 type Output = String;
246 fn text(&self, s: &str) -> Self::Output {
247 s.to_string()
248 }
249 fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output {
250 items.join(delimiter)
251 }
252 fn finish(&self, output: Self::Output) -> String {
253 output
254 }
255 fn emph(&self, content: Self::Output) -> Self::Output {
256 format!("emph({content})")
257 }
258 fn strong(&self, content: Self::Output) -> Self::Output {
259 format!("strong({content})")
260 }
261 fn small_caps(&self, content: Self::Output) -> Self::Output {
262 format!("sc({content})")
263 }
264 fn superscript(&self, content: Self::Output) -> Self::Output {
265 format!("sup({content})")
266 }
267 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
268 format!("{prefix}{content}{suffix}")
269 }
270 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
271 format!("{prefix}{content}{suffix}")
272 }
273 fn wrap_punctuation(&self, _wrap: &WrapPunctuation, content: Self::Output) -> Self::Output {
274 content
275 }
276 fn semantic(&self, class: &str, content: Self::Output) -> Self::Output {
277 format!("sem[{class}]({content})")
278 }
279 fn annotation(&self, content: Self::Output) -> Self::Output {
280 format!("annot({content})")
281 }
282 fn link(&self, url: &str, content: Self::Output) -> Self::Output {
283 format!("link[{url}]({content})")
284 }
285 }
286
287 #[test]
288 fn test_default_methods() {
289 let fmt = DummyFormat;
290 assert_eq!(
291 fmt.semantic_with_attributes("test", "content".to_string(), &[]),
292 "sem[test](content)"
293 );
294 assert_eq!(
295 fmt.citation(vec!["id1".to_string()], "content".to_string()),
296 "content"
297 );
298 assert_eq!(fmt.format_id("id1"), "id1");
299 assert_eq!(
300 fmt.bibliography(vec!["entry1".to_string(), "entry2".to_string()]),
301 "entry1\n\nentry2"
302 );
303 assert_eq!(
304 fmt.entry(
305 "id1",
306 "content".to_string(),
307 None,
308 &ProcEntryMetadata::default()
309 ),
310 "content"
311 );
312 }
313}