1use crate::config::InstanceConfig;
2use crate::types::{BuiltinDirective, CssId, CustomDirective, CustomDirectiveMap, Overrides};
3use std::fmt;
4use std::str::FromStr;
5
6#[derive(Debug, PartialEq)]
10pub(crate) struct AdmonitionMeta {
11 pub directive: String,
12 pub title: String,
13 pub css_id: CssId,
14 pub additional_classnames: Vec<String>,
15 pub collapsible: bool,
16}
17
18enum Directive {
20 Builtin(BuiltinDirective),
21 Custom(CustomDirective),
22}
23
24impl fmt::Display for Directive {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Self::Builtin(builtin) => builtin.fmt(f),
28 Self::Custom(custom) => f.write_str(&custom.directive),
29 }
30 }
31}
32
33impl Directive {
34 fn from_str(custom_directive_map: &CustomDirectiveMap, string: &str) -> Result<Self, ()> {
35 if let Ok(builtin) = BuiltinDirective::from_str(string) {
36 return Ok(Self::Builtin(builtin));
37 }
38
39 if let Some(config) = custom_directive_map.get(string) {
40 return Ok(Self::Custom(config.clone()));
41 }
42
43 Err(())
44 }
45
46 fn title(self, raw_directive: &str) -> String {
47 match self {
48 Directive::Builtin(_) => format_builtin_directive_title(raw_directive),
49 Directive::Custom(custom) => custom
50 .title
51 .clone()
52 .unwrap_or_else(|| uppercase_first(raw_directive)),
53 }
54 }
55}
56
57impl AdmonitionMeta {
58 pub fn from_info_string(
59 info_string: &str,
60 overrides: &Overrides,
61 ) -> Option<Result<Self, String>> {
62 InstanceConfig::from_info_string(info_string)
63 .map(|raw| raw.map(|raw| Self::resolve(raw, overrides)))
64 }
65
66 fn resolve(raw: InstanceConfig, overrides: &Overrides) -> Self {
69 let InstanceConfig {
70 directive: raw_directive,
71 title,
72 id,
73 additional_classnames,
74 collapsible,
75 } = raw;
76
77 let title = title.or_else(|| overrides.book.title.clone());
79
80 let directive = Directive::from_str(&overrides.custom, &raw_directive);
81
82 let collapsible = match directive {
83 Ok(Directive::Builtin(directive)) => collapsible.unwrap_or(
86 overrides
87 .builtin
88 .get(&directive)
89 .and_then(|config| config.collapsible)
90 .unwrap_or(overrides.book.collapsible),
91 ),
92 Ok(Directive::Custom(ref custom_dir)) => {
95 collapsible.unwrap_or(custom_dir.collapsible.unwrap_or(overrides.book.collapsible))
96 }
97 Err(_) => collapsible.unwrap_or(overrides.book.collapsible),
98 };
99
100 let (directive, title) = match (directive, title) {
102 (Ok(directive), None) => (directive.to_string(), directive.title(&raw_directive)),
103 (Err(_), None) => (BuiltinDirective::Note.to_string(), "Note".to_owned()),
104 (Ok(directive), Some(title)) => (directive.to_string(), title),
105 (Err(_), Some(title)) => (BuiltinDirective::Note.to_string(), title),
106 };
107
108 let css_id = if let Some(verbatim) = id {
109 CssId::Verbatim(verbatim)
110 } else {
111 const DEFAULT_CSS_ID_PREFIX: &str = "admonition-";
112 CssId::Prefix(
113 overrides
114 .book
115 .css_id_prefix
116 .clone()
117 .unwrap_or_else(|| DEFAULT_CSS_ID_PREFIX.to_owned()),
118 )
119 };
120
121 Self {
122 directive,
123 title,
124 css_id,
125 additional_classnames,
126 collapsible,
127 }
128 }
129}
130
131fn format_builtin_directive_title(input: &str) -> String {
135 match input {
136 "tldr" => "TL;DR".to_owned(),
137 "faq" => "FAQ".to_owned(),
138 _ => uppercase_first(input),
139 }
140}
141
142fn uppercase_first(input: &str) -> String {
146 let mut chars = input.chars();
147 match chars.next() {
148 None => String::new(),
149 Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
150 }
151}
152
153#[cfg(test)]
154mod test {
155 use std::collections::HashMap;
156
157 use crate::types::{AdmonitionDefaults, BuiltinDirectiveConfig};
158
159 use super::*;
160 use pretty_assertions::assert_eq;
161
162 #[test]
163 fn test_format_builtin_directive_title() {
164 assert_eq!(format_builtin_directive_title(""), "");
165 assert_eq!(format_builtin_directive_title("a"), "A");
166 assert_eq!(format_builtin_directive_title("tldr"), "TL;DR");
167 assert_eq!(format_builtin_directive_title("faq"), "FAQ");
168 assert_eq!(format_builtin_directive_title("note"), "Note");
169 assert_eq!(format_builtin_directive_title("abstract"), "Abstract");
170 assert_eq!(format_builtin_directive_title("🦀"), "🦀");
172 }
173
174 #[test]
175 fn test_admonition_info_from_raw() {
176 assert_eq!(
177 AdmonitionMeta::resolve(
178 InstanceConfig {
179 directive: " ".to_owned(),
180 title: None,
181 id: None,
182 additional_classnames: Vec::new(),
183 collapsible: None,
184 },
185 &Overrides::default(),
186 ),
187 AdmonitionMeta {
188 directive: "note".to_owned(),
189 title: "Note".to_owned(),
190 css_id: CssId::Prefix("admonition-".to_owned()),
191 additional_classnames: Vec::new(),
192 collapsible: false,
193 }
194 );
195 }
196
197 #[test]
198 fn test_admonition_info_from_raw_with_defaults() {
199 assert_eq!(
200 AdmonitionMeta::resolve(
201 InstanceConfig {
202 directive: " ".to_owned(),
203 title: None,
204 id: None,
205 additional_classnames: Vec::new(),
206 collapsible: None,
207 },
208 &Overrides {
209 book: AdmonitionDefaults {
210 title: Some("Important!!!".to_owned()),
211 css_id_prefix: Some("custom-prefix-".to_owned()),
212 collapsible: true,
213 },
214 ..Default::default()
215 }
216 ),
217 AdmonitionMeta {
218 directive: "note".to_owned(),
219 title: "Important!!!".to_owned(),
220 css_id: CssId::Prefix("custom-prefix-".to_owned()),
221 additional_classnames: Vec::new(),
222 collapsible: true,
223 }
224 );
225 }
226
227 #[test]
228 fn test_admonition_info_from_raw_with_defaults_and_custom_id() {
229 assert_eq!(
230 AdmonitionMeta::resolve(
231 InstanceConfig {
232 directive: " ".to_owned(),
233 title: None,
234 id: Some("my-custom-id".to_owned()),
235 additional_classnames: Vec::new(),
236 collapsible: None,
237 },
238 &Overrides {
239 book: AdmonitionDefaults {
240 title: Some("Important!!!".to_owned()),
241 css_id_prefix: Some("ignored-custom-prefix-".to_owned()),
242 collapsible: true,
243 },
244 ..Default::default()
245 }
246 ),
247 AdmonitionMeta {
248 directive: "note".to_owned(),
249 title: "Important!!!".to_owned(),
250 css_id: CssId::Verbatim("my-custom-id".to_owned()),
251 additional_classnames: Vec::new(),
252 collapsible: true,
253 }
254 );
255 }
256
257 #[test]
258 fn test_admonition_info_from_raw_with_custom_directive() {
259 assert_eq!(
260 AdmonitionMeta::resolve(
261 InstanceConfig {
262 directive: "frog".to_owned(),
263 title: None,
264 id: None,
265 additional_classnames: Vec::new(),
266 collapsible: None,
267 },
268 &Overrides {
269 custom: [CustomDirective {
270 directive: "frog".to_owned(),
271 aliases: Vec::new(),
272 title: None,
273 collapsible: None,
274 }]
275 .into_iter()
276 .collect(),
277 ..Default::default()
278 }
279 ),
280 AdmonitionMeta {
281 directive: "frog".to_owned(),
282 title: "Frog".to_owned(),
283 css_id: CssId::Prefix("admonition-".to_owned()),
284 additional_classnames: Vec::new(),
285 collapsible: false,
286 }
287 );
288 }
289
290 #[test]
291 fn test_admonition_info_from_raw_with_custom_directive_and_title() {
292 assert_eq!(
293 AdmonitionMeta::resolve(
294 InstanceConfig {
295 directive: "frog".to_owned(),
296 title: None,
297 id: None,
298 additional_classnames: Vec::new(),
299 collapsible: None,
300 },
301 &Overrides {
302 custom: [CustomDirective {
303 directive: "frog".to_owned(),
304 aliases: Vec::new(),
305 title: Some("🏳️🌈".to_owned()),
306 collapsible: None,
307 }]
308 .into_iter()
309 .collect(),
310 ..Default::default()
311 }
312 ),
313 AdmonitionMeta {
314 directive: "frog".to_owned(),
315 title: "🏳️🌈".to_owned(),
316 css_id: CssId::Prefix("admonition-".to_owned()),
317 additional_classnames: Vec::new(),
318 collapsible: false,
319 }
320 );
321 }
322
323 #[test]
324 fn test_admonition_info_from_raw_with_custom_directive_alias() {
325 assert_eq!(
326 AdmonitionMeta::resolve(
327 InstanceConfig {
328 directive: "toad".to_owned(),
329 title: Some("Still a frog".to_owned()),
330 id: None,
331 additional_classnames: Vec::new(),
332 collapsible: None,
333 },
334 &Overrides {
335 custom: [CustomDirective {
336 directive: "frog".to_owned(),
337 aliases: vec!["newt".to_owned(), "toad".to_owned()],
338 title: Some("🏳️🌈".to_owned()),
339 collapsible: None,
340 }]
341 .into_iter()
342 .collect(),
343 ..Default::default()
344 }
345 ),
346 AdmonitionMeta {
347 directive: "frog".to_owned(),
348 title: "Still a frog".to_owned(),
349 css_id: CssId::Prefix("admonition-".to_owned()),
350 additional_classnames: Vec::new(),
351 collapsible: false,
352 }
353 );
354 }
355
356 #[test]
357 fn test_admonition_info_from_raw_with_collapsible_custom_directive() {
358 assert_eq!(
359 AdmonitionMeta::resolve(
360 InstanceConfig {
361 directive: "frog".to_owned(),
362 title: None,
363 id: None,
364 additional_classnames: Vec::new(),
365 collapsible: None,
366 },
367 &Overrides {
368 custom: [CustomDirective {
369 directive: "frog".to_owned(),
370 aliases: Vec::new(),
371 title: None,
372 collapsible: Some(true),
373 }]
374 .into_iter()
375 .collect(),
376 ..Default::default()
377 }
378 ),
379 AdmonitionMeta {
380 directive: "frog".to_owned(),
381 title: "Frog".to_owned(),
382 css_id: CssId::Prefix("admonition-".to_owned()),
383 additional_classnames: Vec::new(),
384 collapsible: true,
385 }
386 );
387 }
388
389 #[test]
390 fn test_admonition_info_from_raw_with_collapsible_builtin_directive() {
391 assert_eq!(
392 AdmonitionMeta::resolve(
393 InstanceConfig {
394 directive: "abstract".to_owned(),
395 title: None,
396 id: None,
397 additional_classnames: Vec::new(),
398 collapsible: None,
399 },
400 &Overrides {
401 book: AdmonitionDefaults {
402 title: None,
403 css_id_prefix: None,
404 collapsible: false,
405 },
406 builtin: HashMap::from([(
407 BuiltinDirective::Abstract,
408 BuiltinDirectiveConfig {
409 collapsible: Some(true),
410 }
411 )]),
412 ..Default::default()
413 }
414 ),
415 AdmonitionMeta {
416 directive: "abstract".to_owned(),
417 title: "Abstract".to_owned(),
418 css_id: CssId::Prefix("admonition-".to_owned()),
419 additional_classnames: Vec::new(),
420 collapsible: true,
421 }
422 );
423 }
424
425 #[test]
426 fn test_admonition_info_from_raw_with_non_collapsible_builtin_directive() {
427 assert_eq!(
428 AdmonitionMeta::resolve(
429 InstanceConfig {
430 directive: "abstract".to_owned(),
431 title: None,
432 id: None,
433 additional_classnames: Vec::new(),
434 collapsible: None,
435 },
436 &Overrides {
437 book: AdmonitionDefaults {
438 title: None,
439 css_id_prefix: None,
440 collapsible: true,
441 },
442 builtin: HashMap::from([(
443 BuiltinDirective::Abstract,
444 BuiltinDirectiveConfig {
445 collapsible: Some(false),
446 }
447 )]),
448 ..Default::default()
449 }
450 ),
451 AdmonitionMeta {
452 directive: "abstract".to_owned(),
453 title: "Abstract".to_owned(),
454 css_id: CssId::Prefix("admonition-".to_owned()),
455 additional_classnames: Vec::new(),
456 collapsible: false,
457 }
458 );
459 }
460}