1use super::format::{OutputFormat, QuoteMarks};
9use citum_schema::template::WrapPunctuation;
10
11#[derive(Default, Clone)]
12pub struct Djot;
14
15impl OutputFormat for Djot {
16 type Output = String;
17
18 fn text(&self, s: &str) -> Self::Output {
19 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 if content.is_empty() {
52 return content;
53 }
54 format!("[{content}]{{.small-caps}}")
55 }
56
57 fn superscript(&self, content: Self::Output) -> Self::Output {
58 if content.is_empty() {
59 return content;
60 }
61 format!("[{content}]{{.superscript}}")
62 }
63
64 fn quote(&self, content: Self::Output, marks: &QuoteMarks) -> Self::Output {
65 if content.is_empty() {
66 return content;
67 }
68 let (open, close) = marks.for_depth(0);
69 format!("{open}{content}{close}")
70 }
71
72 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
73 format!("{prefix}{content}{suffix}")
74 }
75
76 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
77 format!("{prefix}{content}{suffix}")
78 }
79
80 fn wrap_punctuation(
81 &self,
82 wrap: &WrapPunctuation,
83 content: Self::Output,
84 marks: &QuoteMarks,
85 ) -> Self::Output {
86 match wrap {
87 WrapPunctuation::Parentheses => format!("({content})"),
88 WrapPunctuation::Brackets => format!("[{content}]"),
89 WrapPunctuation::Quotes => self.quote(content, marks),
90 }
91 }
92
93 fn semantic(&self, class: &str, content: Self::Output) -> Self::Output {
94 if content.is_empty() {
95 return content;
96 }
97 format!("[{content}]{{.{class}}}")
98 }
99
100 fn annotation(&self, content: Self::Output) -> Self::Output {
101 if content.is_empty() {
102 return content;
103 }
104 format!("\n\n::: citum-annotation\n{content}\n:::")
105 }
106
107 fn link(&self, url: &str, content: Self::Output) -> Self::Output {
108 if content.is_empty() {
109 return content;
110 }
111 format!("[{content}]({url})")
112 }
113
114 fn entry(
115 &self,
116 _id: &str,
117 content: Self::Output,
118 url: Option<&str>,
119 _metadata: &super::format::ProcEntryMetadata,
120 ) -> Self::Output {
121 if let Some(u) = url {
122 self.link(u, content)
123 } else {
124 content
125 }
126 }
127}
128
129#[cfg(test)]
130#[allow(
131 clippy::unwrap_used,
132 clippy::expect_used,
133 clippy::panic,
134 clippy::indexing_slicing,
135 clippy::todo,
136 clippy::unimplemented,
137 clippy::unreachable,
138 clippy::get_unwrap,
139 reason = "Panicking is acceptable and often desired in tests."
140)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_djot_emph() {
146 let fmt = Djot;
147
148 for (input, expected) in [("", ""), ("text", "_text_")] {
149 assert_eq!(fmt.emph(input.to_string()), expected);
150 }
151 }
152
153 #[test]
154 fn test_djot_strong() {
155 let fmt = Djot;
156
157 for (input, expected) in [("", ""), ("text", "*text*")] {
158 assert_eq!(fmt.strong(input.to_string()), expected);
159 }
160 }
161
162 #[test]
163 fn test_djot_small_caps() {
164 let fmt = Djot;
165
166 for (input, expected) in [("", ""), ("text", "[text]{.small-caps}")] {
167 assert_eq!(fmt.small_caps(input.to_string()), expected);
168 }
169 }
170
171 #[test]
172 fn test_djot_quote() {
173 let fmt = Djot;
174 let marks = QuoteMarks::default();
175
176 for (input, expected) in [("", ""), ("text", "\u{201C}text\u{201D}")] {
177 assert_eq!(fmt.quote(input.to_string(), &marks), expected);
178 }
179 }
180
181 #[test]
182 fn test_djot_quote_uses_locale_marks() {
183 let fmt = Djot;
184 let marks = QuoteMarks {
185 open: "\u{ab}".to_string(),
186 close: "\u{bb}".to_string(),
187 open_inner: "\u{2039}".to_string(),
188 close_inner: "\u{203a}".to_string(),
189 };
190
191 assert_eq!(fmt.quote("text".to_string(), &marks), "\u{ab}text\u{bb}");
192 }
193
194 #[test]
195 fn test_djot_semantic() {
196 let fmt = Djot;
197
198 for (input, class, expected) in [("", "author", ""), ("text", "author", "[text]{.author}")]
199 {
200 assert_eq!(fmt.semantic(class, input.to_string()), expected);
201 }
202 }
203
204 #[test]
205 fn test_djot_link() {
206 let fmt = Djot;
207
208 for (input, url, expected) in [
209 ("", "https://example.com", ""),
210 ("text", "https://example.com", "[text](https://example.com)"),
211 ] {
212 assert_eq!(fmt.link(url, input.to_string()), expected);
213 }
214 }
215
216 #[test]
217 fn test_djot_wrap_punctuation() {
218 let fmt = Djot;
219 let marks = QuoteMarks::default();
220
221 for (wrap, input, expected) in [
222 (WrapPunctuation::Parentheses, "text", "(text)"),
223 (WrapPunctuation::Brackets, "text", "[text]"),
224 (WrapPunctuation::Quotes, "text", "\u{201C}text\u{201D}"),
225 ] {
226 assert_eq!(
227 fmt.wrap_punctuation(&wrap, input.to_string(), &marks),
228 expected
229 );
230 }
231 }
232}