1#[cfg(feature = "schema")]
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20
21use crate::presets::SortPreset;
22
23#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
25#[cfg_attr(feature = "schema", derive(JsonSchema))]
26#[serde(rename_all = "kebab-case")]
27#[non_exhaustive]
28pub enum LabelPreset {
29 #[default]
31 Alpha,
32 Din,
34 Ams,
36}
37
38#[derive(Debug, Clone)]
43pub struct LabelParams {
44 pub single_author_chars: u8,
46 pub multi_author_chars: u8,
48 pub et_al_min: u8,
50 pub et_al_marker: String,
52 pub et_al_names: u8,
54 pub year_digits: u8,
56}
57
58#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
60#[cfg_attr(feature = "schema", derive(JsonSchema))]
61#[serde(rename_all = "kebab-case")]
62pub struct LabelConfig {
63 #[serde(default)]
65 pub preset: LabelPreset,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub single_author_chars: Option<u8>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub multi_author_chars: Option<u8>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub et_al_min: Option<u8>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub et_al_marker: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub et_al_names: Option<u8>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub year_digits: Option<u8>,
84}
85
86impl LabelConfig {
87 pub fn effective_params(&self) -> LabelParams {
97 let (
98 default_single_author_chars,
99 default_multi_author_chars,
100 default_et_al_min,
101 default_marker,
102 default_et_al_names,
103 ) = match self.preset {
104 LabelPreset::Alpha => (3u8, 1u8, 4u8, "+".to_string(), 3u8),
105 LabelPreset::Ams => (3u8, 1u8, 4u8, "+".to_string(), 4u8),
106 LabelPreset::Din => (4u8, 1u8, 3u8, String::new(), 3u8),
107 };
108 LabelParams {
109 single_author_chars: self
110 .single_author_chars
111 .unwrap_or(default_single_author_chars),
112 multi_author_chars: self
113 .multi_author_chars
114 .unwrap_or(default_multi_author_chars),
115 et_al_min: self.et_al_min.unwrap_or(default_et_al_min),
116 et_al_marker: self.et_al_marker.clone().unwrap_or(default_marker),
117 et_al_names: self.et_al_names.unwrap_or(default_et_al_names),
118 year_digits: self.year_digits.unwrap_or(2),
119 }
120 }
121}
122
123#[derive(Debug, Default, PartialEq, Clone)]
133#[cfg_attr(feature = "schema", derive(JsonSchema))]
134#[cfg_attr(feature = "schema", schemars(rename_all = "kebab-case"))]
135#[non_exhaustive]
136pub enum Processing {
137 #[default]
140 AuthorDate,
141 Numeric,
144 Note,
147 Label(LabelConfig),
150 Custom(ProcessingCustom),
153}
154
155#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
160#[cfg_attr(feature = "schema", derive(JsonSchema))]
161#[serde(rename_all = "kebab-case")]
162pub enum CitationSortPolicy {
163 ExplicitOnly,
165}
166
167#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
172#[cfg_attr(feature = "schema", derive(JsonSchema))]
173#[serde(rename_all = "kebab-case")]
174pub struct ProcessingCustom {
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub sort: Option<SortEntry>,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub group: Option<Group>,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub disambiguate: Option<Disambiguation>,
184}
185
186impl Processing {
187 pub fn default_bibliography_sort(&self) -> Option<SortPreset> {
194 match self {
195 Processing::AuthorDate => Some(SortPreset::AuthorDateTitle),
196 Processing::Numeric => None,
197 Processing::Note => Some(SortPreset::AuthorTitleDate),
198 Processing::Label(_) => Some(SortPreset::AuthorDateTitle),
199 Processing::Custom(_) => None,
200 }
201 }
202
203 pub fn default_citation_sort_policy(&self) -> CitationSortPolicy {
208 CitationSortPolicy::ExplicitOnly
209 }
210
211 pub fn config(&self) -> ProcessingCustom {
216 match self {
217 Processing::AuthorDate => ProcessingCustom {
218 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
219 group: Some(Group {
220 template: vec![SortKey::Author, SortKey::Year],
221 }),
222 disambiguate: Some(Disambiguation {
223 names: true,
224 add_givenname: true,
225 year_suffix: true,
226 }),
227 },
228 Processing::Numeric => ProcessingCustom {
229 sort: None,
230 group: None,
231 disambiguate: None,
232 },
233 Processing::Note => ProcessingCustom {
234 sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
235 group: None,
236 disambiguate: Some(Disambiguation {
237 names: true,
238 add_givenname: false,
239 year_suffix: false,
240 }),
241 },
242 Processing::Label(_) => ProcessingCustom {
243 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
244 group: None,
245 disambiguate: Some(Disambiguation {
246 names: false,
247 add_givenname: false,
248 year_suffix: true,
249 }),
250 },
251 Processing::Custom(custom) => custom.clone(),
252 }
253 }
254}
255
256impl Serialize for Processing {
257 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
258 where
259 S: serde::Serializer,
260 {
261 match self {
262 Processing::AuthorDate => serializer.serialize_str("author-date"),
263 Processing::Numeric => serializer.serialize_str("numeric"),
264 Processing::Note => serializer.serialize_str("note"),
265 Processing::Label(config) => {
266 use serde::ser::SerializeMap;
267 let mut map = serializer.serialize_map(Some(1))?;
268 map.serialize_entry("label", config)?;
269 map.end()
270 }
271 Processing::Custom(custom) => custom.serialize(serializer),
275 }
276 }
277}
278
279impl<'de> Deserialize<'de> for Processing {
280 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
281 where
282 D: serde::Deserializer<'de>,
283 {
284 use serde::de::{self, MapAccess, Visitor};
285
286 struct ProcessingVisitor;
287
288 impl<'de> Visitor<'de> for ProcessingVisitor {
289 type Value = Processing;
290
291 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
292 f.write_str("a processing mode string or map")
293 }
294
295 fn visit_str<E: de::Error>(self, v: &str) -> Result<Processing, E> {
296 match v {
297 "author-date" => Ok(Processing::AuthorDate),
298 "numeric" => Ok(Processing::Numeric),
299 "note" => Ok(Processing::Note),
300 "label" => Ok(Processing::Label(LabelConfig::default())),
301 other => Err(E::unknown_variant(
302 other,
303 &["author-date", "numeric", "note", "label"],
304 )),
305 }
306 }
307
308 fn visit_enum<A: de::EnumAccess<'de>>(self, data: A) -> Result<Processing, A::Error> {
309 use serde::de::VariantAccess;
310 let (variant, access) = data.variant::<String>()?;
311 match variant.as_str() {
312 "custom" => {
313 let custom: ProcessingCustom = access.newtype_variant()?;
314 Ok(Processing::Custom(custom))
315 }
316 other => Err(de::Error::unknown_variant(
317 other,
318 &["author-date", "numeric", "note", "label", "custom"],
319 )),
320 }
321 }
322
323 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Processing, A::Error> {
324 let key: String = map
325 .next_key()?
326 .ok_or_else(|| de::Error::invalid_length(0, &"1"))?;
327 match key.as_str() {
328 "label" => {
329 let config: LabelConfig = map.next_value()?;
330 Ok(Processing::Label(config))
331 }
332 "sort" | "group" | "disambiguate" => {
333 let mut sort = None;
338 let mut group = None;
339 let mut disambiguate = None;
340
341 match key.as_str() {
343 "sort" => sort = Some(map.next_value()?),
344 "group" => group = Some(map.next_value()?),
345 "disambiguate" => disambiguate = Some(map.next_value()?),
346 _ => {
347 return Err(de::Error::unknown_field(
348 &key,
349 &["sort", "group", "disambiguate"],
350 ));
351 }
352 }
353
354 while let Some(k) = map.next_key::<String>()? {
356 match k.as_str() {
357 "sort" => sort = Some(map.next_value()?),
358 "group" => group = Some(map.next_value()?),
359 "disambiguate" => disambiguate = Some(map.next_value()?),
360 other => {
361 return Err(de::Error::unknown_field(
362 other,
363 &["sort", "group", "disambiguate"],
364 ));
365 }
366 }
367 }
368
369 Ok(Processing::Custom(ProcessingCustom {
370 sort,
371 group,
372 disambiguate,
373 }))
374 }
375 other => Err(de::Error::unknown_field(
376 other,
377 &["label", "sort", "group", "disambiguate"],
378 )),
379 }
380 }
381 }
382
383 deserializer.deserialize_any(ProcessingVisitor)
384 }
385}
386
387#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
391#[cfg_attr(feature = "schema", derive(JsonSchema))]
392#[serde(rename_all = "kebab-case")]
393pub struct Disambiguation {
394 pub names: bool,
396 #[serde(default)]
398 pub add_givenname: bool,
399 pub year_suffix: bool,
401}
402
403impl Default for Disambiguation {
404 fn default() -> Self {
405 Self {
406 names: true,
407 add_givenname: false,
408 year_suffix: false,
409 }
410 }
411}
412
413#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
417#[cfg_attr(feature = "schema", derive(JsonSchema))]
418#[serde(rename_all = "kebab-case")]
419pub struct Sort {
420 #[serde(default)]
422 pub shorten_names: bool,
423 #[serde(default)]
425 pub render_substitutions: bool,
426 pub template: Vec<SortSpec>,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
434#[cfg_attr(feature = "schema", derive(JsonSchema))]
435#[serde(untagged)]
436pub enum SortEntry {
437 Preset(crate::presets::SortPreset),
439 Explicit(Sort),
441}
442
443impl SortEntry {
444 pub fn resolve(&self) -> Sort {
448 match self {
449 SortEntry::Preset(preset) => preset.sort(),
450 SortEntry::Explicit(sort) => sort.clone(),
451 }
452 }
453}
454
455#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
459#[cfg_attr(feature = "schema", derive(JsonSchema))]
460#[serde(rename_all = "kebab-case")]
461pub struct SortSpec {
462 pub key: SortKey,
464 #[serde(default = "default_ascending")]
466 pub ascending: bool,
467}
468
469fn default_ascending() -> bool {
470 true
471}
472
473#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
477#[cfg_attr(feature = "schema", derive(JsonSchema))]
478#[serde(rename_all = "kebab-case")]
479#[non_exhaustive]
480pub enum SortKey {
481 #[default]
483 Author,
484 Year,
486 Title,
488 CitationNumber,
490}
491
492#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
496#[cfg_attr(feature = "schema", derive(JsonSchema))]
497#[serde(rename_all = "kebab-case")]
498pub struct Group {
499 pub template: Vec<SortKey>,
501}
502
503#[cfg(test)]
504#[allow(
505 clippy::unwrap_used,
506 clippy::expect_used,
507 clippy::panic,
508 clippy::indexing_slicing,
509 clippy::todo,
510 clippy::unimplemented,
511 clippy::unreachable,
512 clippy::get_unwrap,
513 reason = "Panicking is acceptable and often desired in tests."
514)]
515mod tests {
516 use super::*;
517
518 #[test]
520 fn test_label_config_alpha_preset_defaults() {
521 let config = LabelConfig {
522 preset: LabelPreset::Alpha,
523 single_author_chars: None,
524 multi_author_chars: None,
525 et_al_min: None,
526 et_al_marker: None,
527 et_al_names: None,
528 year_digits: None,
529 };
530
531 let params = config.effective_params();
532 assert_eq!(params.single_author_chars, 3);
533 assert_eq!(params.multi_author_chars, 1);
534 assert_eq!(params.et_al_min, 4);
535 assert_eq!(params.et_al_marker, "+");
536 assert_eq!(params.et_al_names, 3);
537 assert_eq!(params.year_digits, 2);
538 }
539
540 #[test]
542 fn test_label_config_alpha_with_overrides() {
543 let config = LabelConfig {
544 preset: LabelPreset::Alpha,
545 single_author_chars: Some(5),
546 multi_author_chars: Some(2),
547 et_al_min: Some(5),
548 et_al_marker: Some("*".to_string()),
549 et_al_names: Some(4),
550 year_digits: Some(4),
551 };
552
553 let params = config.effective_params();
554 assert_eq!(params.single_author_chars, 5);
555 assert_eq!(params.multi_author_chars, 2);
556 assert_eq!(params.et_al_min, 5);
557 assert_eq!(params.et_al_marker, "*");
558 assert_eq!(params.et_al_names, 4);
559 assert_eq!(params.year_digits, 4);
560 }
561
562 #[test]
564 fn test_label_config_din_preset_defaults() {
565 let config = LabelConfig {
566 preset: LabelPreset::Din,
567 single_author_chars: None,
568 multi_author_chars: None,
569 et_al_min: None,
570 et_al_marker: None,
571 et_al_names: None,
572 year_digits: None,
573 };
574
575 let params = config.effective_params();
576 assert_eq!(params.single_author_chars, 4);
577 assert_eq!(params.multi_author_chars, 1);
578 assert_eq!(params.et_al_min, 3);
579 assert_eq!(params.et_al_marker, "");
580 assert_eq!(params.et_al_names, 3);
581 assert_eq!(params.year_digits, 2);
582 }
583
584 #[test]
586 fn test_processing_author_date_default_bibliography_sort() {
587 let processing = Processing::AuthorDate;
588 let sort = processing.default_bibliography_sort();
589 assert_eq!(sort, Some(SortPreset::AuthorDateTitle));
590 }
591
592 #[test]
594 fn test_processing_numeric_default_bibliography_sort() {
595 let processing = Processing::Numeric;
596 let sort = processing.default_bibliography_sort();
597 assert_eq!(sort, None);
598 }
599
600 #[test]
602 fn test_processing_note_default_bibliography_sort() {
603 let processing = Processing::Note;
604 let sort = processing.default_bibliography_sort();
605 assert_eq!(sort, Some(SortPreset::AuthorTitleDate));
606 }
607
608 #[test]
610 fn test_processing_citation_sort_policy() {
611 let modes = vec![
612 Processing::AuthorDate,
613 Processing::Numeric,
614 Processing::Note,
615 Processing::Label(LabelConfig::default()),
616 Processing::Custom(ProcessingCustom::default()),
617 ];
618
619 for mode in modes {
620 assert_eq!(
621 mode.default_citation_sort_policy(),
622 CitationSortPolicy::ExplicitOnly
623 );
624 }
625 }
626
627 #[test]
629 fn test_processing_author_date_config() {
630 let processing = Processing::AuthorDate;
631 let config = processing.config();
632
633 assert!(config.sort.is_some());
634 assert!(config.group.is_some());
635 assert!(config.disambiguate.is_some());
636
637 let disambig = config.disambiguate.unwrap();
638 assert!(disambig.names);
639 assert!(disambig.add_givenname);
640 assert!(disambig.year_suffix);
641 }
642
643 #[test]
645 fn test_disambiguation_defaults() {
646 let disambig = Disambiguation::default();
647 assert!(disambig.names);
648 assert!(!disambig.add_givenname);
649 assert!(!disambig.year_suffix);
650 }
651
652 #[test]
654 fn test_sort_entry_resolve_preset() {
655 let entry = SortEntry::Preset(SortPreset::AuthorDateTitle);
656 let sort = entry.resolve();
657
658 assert!(!sort.template.is_empty());
660 }
661
662 #[test]
664 fn test_sort_entry_resolve_explicit() {
665 let explicit = Sort {
666 shorten_names: true,
667 render_substitutions: false,
668 template: vec![SortSpec {
669 key: SortKey::Title,
670 ascending: false,
671 }],
672 };
673 let entry = SortEntry::Explicit(explicit.clone());
674 let resolved = entry.resolve();
675
676 assert!(resolved.shorten_names);
677 assert!(!resolved.render_substitutions);
678 assert_eq!(resolved.template.len(), 1);
679 assert_eq!(resolved.template[0].key, SortKey::Title);
680 assert!(!resolved.template[0].ascending);
681 }
682}