1use citum_schema::locale::GrammarOptions;
9use citum_schema::template::WrapPunctuation;
10
11#[must_use]
15pub fn unicode_quote_marks(depth: usize) -> (&'static str, &'static str) {
16 if depth.is_multiple_of(2) {
17 ("\u{201C}", "\u{201D}")
18 } else {
19 ("\u{2018}", "\u{2019}")
20 }
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct QuoteMarks {
28 pub open: String,
30 pub close: String,
32 pub open_inner: String,
34 pub close_inner: String,
36}
37
38impl QuoteMarks {
39 #[must_use]
43 pub fn for_depth(&self, depth: usize) -> (&str, &str) {
44 if depth.is_multiple_of(2) {
45 (&self.open, &self.close)
46 } else {
47 (&self.open_inner, &self.close_inner)
48 }
49 }
50}
51
52impl Default for QuoteMarks {
53 fn default() -> Self {
55 let (open, close) = unicode_quote_marks(0);
56 let (open_inner, close_inner) = unicode_quote_marks(1);
57 Self {
58 open: open.to_string(),
59 close: close.to_string(),
60 open_inner: open_inner.to_string(),
61 close_inner: close_inner.to_string(),
62 }
63 }
64}
65
66impl From<&GrammarOptions> for QuoteMarks {
67 fn from(options: &GrammarOptions) -> Self {
68 Self {
69 open: options.open_quote.clone(),
70 close: options.close_quote.clone(),
71 open_inner: options.open_inner_quote.clone(),
72 close_inner: options.close_inner_quote.clone(),
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct SemanticAttribute {
80 pub name: &'static str,
82 pub value: String,
84}
85
86pub trait OutputFormat: Default + Clone {
91 type Output;
96
97 fn text(&self, s: &str) -> Self::Output;
102
103 fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output;
105
106 fn finish(&self, output: Self::Output) -> String;
111
112 fn emph(&self, content: Self::Output) -> Self::Output;
114
115 fn strong(&self, content: Self::Output) -> Self::Output;
117
118 fn small_caps(&self, content: Self::Output) -> Self::Output;
120
121 fn superscript(&self, content: Self::Output) -> Self::Output;
123
124 fn quote_marks<'a>(&self, depth: usize, marks: &'a QuoteMarks) -> (&'a str, &'a str) {
131 marks.for_depth(depth)
132 }
133
134 fn quote_with_depth(
136 &self,
137 content: Self::Output,
138 depth: usize,
139 marks: &QuoteMarks,
140 ) -> Self::Output {
141 let (open, close) = self.quote_marks(depth, marks);
142 self.affix(open, content, close)
143 }
144
145 fn quote(&self, content: Self::Output, marks: &QuoteMarks) -> Self::Output {
147 self.quote_with_depth(content, 0, marks)
148 }
149
150 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output;
154
155 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output;
159
160 fn wrap_punctuation(
164 &self,
165 wrap: &WrapPunctuation,
166 content: Self::Output,
167 marks: &QuoteMarks,
168 ) -> Self::Output;
169
170 fn semantic(&self, class: &str, content: Self::Output) -> Self::Output;
175
176 fn annotation(&self, content: Self::Output) -> Self::Output;
181
182 fn paragraph(&self, content: Self::Output) -> Self::Output {
187 content
188 }
189
190 fn block_quote(&self, content: Self::Output) -> Self::Output {
192 content
193 }
194
195 fn bullet_list(&self, items: Vec<Self::Output>) -> Self::Output {
197 self.join(items, "\n")
198 }
199
200 fn ordered_list(&self, items: Vec<Self::Output>) -> Self::Output {
202 self.join(items, "\n")
203 }
204
205 fn list_item(&self, content: Self::Output) -> Self::Output {
207 content
208 }
209
210 fn heading(&self, _level: u8, content: Self::Output) -> Self::Output {
212 content
213 }
214
215 fn unnumbered_heading(&self, level: u8, content: Self::Output) -> Self::Output {
222 self.heading(level, content)
223 }
224
225 fn code_block(&self, _lang: Option<&str>, content: Self::Output) -> Self::Output {
229 content
230 }
231
232 fn inline_code(&self, content: Self::Output) -> Self::Output {
234 content
235 }
236
237 fn strikeout(&self, content: Self::Output) -> Self::Output {
239 content
240 }
241
242 fn hard_break(&self) -> Self::Output {
244 self.text(" ")
245 }
246
247 fn semantic_with_attributes(
252 &self,
253 class: &str,
254 content: Self::Output,
255 _attributes: &[SemanticAttribute],
256 ) -> Self::Output {
257 self.semantic(class, content)
258 }
259
260 fn citation(&self, _ids: Vec<String>, content: Self::Output) -> Self::Output {
262 content
263 }
264
265 fn link(&self, url: &str, content: Self::Output) -> Self::Output;
267
268 fn format_id(&self, id: &str) -> String {
270 id.to_string()
271 }
272
273 fn bibliography(&self, entries: Vec<Self::Output>) -> Self::Output {
277 self.join(entries, "\n\n")
278 }
279
280 fn entry(
284 &self,
285 _id: &str,
286 content: Self::Output,
287 _url: Option<&str>,
288 _metadata: &ProcEntryMetadata,
289 ) -> Self::Output {
290 content
291 }
292}
293
294#[derive(Debug, Clone, Default, PartialEq)]
296pub struct ProcEntryMetadata {
297 pub author: Option<String>,
299 pub year: Option<String>,
301 pub title: Option<String>,
303}
304
305#[cfg(test)]
306#[allow(
307 clippy::unwrap_used,
308 clippy::expect_used,
309 clippy::panic,
310 clippy::indexing_slicing,
311 clippy::todo,
312 clippy::unimplemented,
313 clippy::unreachable,
314 clippy::get_unwrap,
315 reason = "Panicking is acceptable and often desired in tests."
316)]
317mod tests {
318 use super::*;
319
320 #[derive(Default, Clone)]
321 struct DummyFormat;
322
323 impl OutputFormat for DummyFormat {
324 type Output = String;
325 fn text(&self, s: &str) -> Self::Output {
326 s.to_string()
327 }
328 fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output {
329 items.join(delimiter)
330 }
331 fn finish(&self, output: Self::Output) -> String {
332 output
333 }
334 fn emph(&self, content: Self::Output) -> Self::Output {
335 format!("emph({content})")
336 }
337 fn strong(&self, content: Self::Output) -> Self::Output {
338 format!("strong({content})")
339 }
340 fn small_caps(&self, content: Self::Output) -> Self::Output {
341 format!("sc({content})")
342 }
343 fn superscript(&self, content: Self::Output) -> Self::Output {
344 format!("sup({content})")
345 }
346 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
347 format!("{prefix}{content}{suffix}")
348 }
349 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
350 format!("{prefix}{content}{suffix}")
351 }
352 fn wrap_punctuation(
353 &self,
354 _wrap: &WrapPunctuation,
355 content: Self::Output,
356 _marks: &QuoteMarks,
357 ) -> Self::Output {
358 content
359 }
360 fn semantic(&self, class: &str, content: Self::Output) -> Self::Output {
361 format!("sem[{class}]({content})")
362 }
363 fn annotation(&self, content: Self::Output) -> Self::Output {
364 format!("annot({content})")
365 }
366 fn link(&self, url: &str, content: Self::Output) -> Self::Output {
367 format!("link[{url}]({content})")
368 }
369 }
370
371 #[test]
372 fn test_default_methods() {
373 let fmt = DummyFormat;
374 assert_eq!(
375 fmt.semantic_with_attributes("test", "content".to_string(), &[]),
376 "sem[test](content)"
377 );
378 assert_eq!(
379 fmt.citation(vec!["id1".to_string()], "content".to_string()),
380 "content"
381 );
382 assert_eq!(fmt.format_id("id1"), "id1");
383 assert_eq!(
384 fmt.bibliography(vec!["entry1".to_string(), "entry2".to_string()]),
385 "entry1\n\nentry2"
386 );
387 assert_eq!(
388 fmt.entry(
389 "id1",
390 "content".to_string(),
391 None,
392 &ProcEntryMetadata::default()
393 ),
394 "content"
395 );
396 }
397}