1#[cfg(feature = "schema")]
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20
21use crate::presets::SortPreset;
22
23const PROCESSING_STRING_VARIANTS: &[&str] = &[
24 "author-date",
25 "author-date-givenname",
26 "author-date-names",
27 "author-date-full",
28 "numeric",
29 "note",
30 "label",
31];
32
33#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
35#[cfg_attr(feature = "schema", derive(JsonSchema))]
36#[serde(rename_all = "kebab-case")]
37#[non_exhaustive]
38pub enum LabelPreset {
39 #[default]
41 Alpha,
42 Din,
44 Ams,
46}
47
48#[derive(Debug, Clone)]
53pub struct LabelParams {
54 pub single_author_chars: u8,
56 pub multi_author_chars: u8,
58 pub et_al_min: u8,
60 pub et_al_marker: String,
62 pub et_al_names: u8,
64 pub year_digits: u8,
66}
67
68#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
70#[cfg_attr(feature = "schema", derive(JsonSchema))]
71#[serde(rename_all = "kebab-case")]
72pub struct LabelConfig {
73 #[serde(default)]
75 pub preset: LabelPreset,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub single_author_chars: Option<u8>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub multi_author_chars: Option<u8>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub et_al_min: Option<u8>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub et_al_marker: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub et_al_names: Option<u8>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub year_digits: Option<u8>,
94}
95
96impl LabelConfig {
97 pub fn effective_params(&self) -> LabelParams {
107 let (
108 default_single_author_chars,
109 default_multi_author_chars,
110 default_et_al_min,
111 default_marker,
112 default_et_al_names,
113 ) = match self.preset {
114 LabelPreset::Alpha => (3u8, 1u8, 4u8, "+".to_string(), 3u8),
115 LabelPreset::Ams => (4u8, 1u8, 5u8, String::new(), 4u8),
116 LabelPreset::Din => (4u8, 1u8, 3u8, String::new(), 3u8),
117 };
118 LabelParams {
119 single_author_chars: self
120 .single_author_chars
121 .unwrap_or(default_single_author_chars),
122 multi_author_chars: self
123 .multi_author_chars
124 .unwrap_or(default_multi_author_chars),
125 et_al_min: self.et_al_min.unwrap_or(default_et_al_min),
126 et_al_marker: self.et_al_marker.clone().unwrap_or(default_marker),
127 et_al_names: self.et_al_names.unwrap_or(default_et_al_names),
128 year_digits: self.year_digits.unwrap_or(2),
129 }
130 }
131}
132
133#[derive(Debug, Default, PartialEq, Clone)]
143#[cfg_attr(feature = "schema", derive(JsonSchema))]
144#[cfg_attr(feature = "schema", schemars(rename_all = "kebab-case"))]
145#[non_exhaustive]
146pub enum Processing {
147 #[default]
150 AuthorDate,
151 AuthorDateGivenname,
153 AuthorDateNames,
155 AuthorDateFull,
157 Numeric,
160 Note,
163 Label(LabelConfig),
166 Custom(ProcessingCustom),
169}
170
171#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
176#[cfg_attr(feature = "schema", derive(JsonSchema))]
177#[serde(rename_all = "kebab-case")]
178pub enum CitationSortPolicy {
179 ExplicitOnly,
181}
182
183#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
188#[cfg_attr(feature = "schema", derive(JsonSchema))]
189#[serde(rename_all = "kebab-case")]
190pub struct ProcessingCustom {
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub sort: Option<SortEntry>,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub group: Option<Group>,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub disambiguate: Option<Disambiguation>,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum RegimeFamily {
213 AuthorDate,
215 Numeric,
217 Note,
219 Label,
221 Custom,
223}
224
225fn author_date_config(names: bool, add_givenname: bool) -> ProcessingCustom {
226 ProcessingCustom {
227 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
228 group: Some(Group {
229 template: vec![SortKey::Author, SortKey::Year],
230 }),
231 disambiguate: Some(Disambiguation {
232 names,
233 add_givenname,
234 givenname_rule: GivennameRule::default(),
235 year_suffix: true,
236 }),
237 }
238}
239
240impl Processing {
241 pub fn default_bibliography_sort(&self) -> Option<SortPreset> {
248 match self {
249 Processing::AuthorDate
250 | Processing::AuthorDateGivenname
251 | Processing::AuthorDateNames
252 | Processing::AuthorDateFull => Some(SortPreset::AuthorDateTitle),
253 Processing::Numeric => None,
254 Processing::Note => Some(SortPreset::AuthorTitleDate),
255 Processing::Label(_) => Some(SortPreset::AuthorDateTitle),
256 Processing::Custom(_) => None,
257 }
258 }
259
260 pub fn is_author_date_family(&self) -> bool {
265 matches!(
266 self,
267 Self::AuthorDate
268 | Self::AuthorDateGivenname
269 | Self::AuthorDateNames
270 | Self::AuthorDateFull
271 )
272 }
273
274 pub fn regime_family(&self) -> RegimeFamily {
285 match self {
286 Self::AuthorDate
287 | Self::AuthorDateGivenname
288 | Self::AuthorDateNames
289 | Self::AuthorDateFull => RegimeFamily::AuthorDate,
290 Self::Numeric => RegimeFamily::Numeric,
291 Self::Note => RegimeFamily::Note,
292 Self::Label(_) => RegimeFamily::Label,
293 Self::Custom(_) => RegimeFamily::Custom,
294 }
295 }
296
297 pub fn default_citation_sort_policy(&self) -> CitationSortPolicy {
302 CitationSortPolicy::ExplicitOnly
303 }
304
305 pub fn config(&self) -> ProcessingCustom {
310 match self {
311 Processing::AuthorDate => author_date_config(false, false),
312 Processing::AuthorDateGivenname => author_date_config(false, true),
313 Processing::AuthorDateNames => author_date_config(true, false),
314 Processing::AuthorDateFull => author_date_config(true, true),
315 Processing::Numeric => ProcessingCustom {
316 sort: None,
317 group: None,
318 disambiguate: None,
319 },
320 Processing::Note => ProcessingCustom {
321 sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
322 group: None,
323 disambiguate: Some(Disambiguation {
324 names: true,
325 add_givenname: false,
326 givenname_rule: GivennameRule::default(),
327 year_suffix: false,
328 }),
329 },
330 Processing::Label(_) => ProcessingCustom {
331 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
332 group: None,
333 disambiguate: Some(Disambiguation {
334 names: false,
335 add_givenname: false,
336 givenname_rule: GivennameRule::default(),
337 year_suffix: true,
338 }),
339 },
340 Processing::Custom(custom) => custom.clone(),
341 }
342 }
343}
344
345impl Serialize for Processing {
346 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
347 where
348 S: serde::Serializer,
349 {
350 match self {
351 Processing::AuthorDate => serializer.serialize_str("author-date"),
352 Processing::AuthorDateGivenname => serializer.serialize_str("author-date-givenname"),
353 Processing::AuthorDateNames => serializer.serialize_str("author-date-names"),
354 Processing::AuthorDateFull => serializer.serialize_str("author-date-full"),
355 Processing::Numeric => serializer.serialize_str("numeric"),
356 Processing::Note => serializer.serialize_str("note"),
357 Processing::Label(config) => {
358 use serde::ser::SerializeMap;
359 let mut map = serializer.serialize_map(Some(1))?;
360 map.serialize_entry("label", config)?;
361 map.end()
362 }
363 Processing::Custom(custom) => custom.serialize(serializer),
367 }
368 }
369}
370
371impl<'de> Deserialize<'de> for Processing {
372 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
373 where
374 D: serde::Deserializer<'de>,
375 {
376 use serde::de::{self, MapAccess, Visitor};
377
378 struct ProcessingVisitor;
379
380 impl<'de> Visitor<'de> for ProcessingVisitor {
381 type Value = Processing;
382
383 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
384 f.write_str("a processing mode string or map")
385 }
386
387 fn visit_str<E: de::Error>(self, v: &str) -> Result<Processing, E> {
388 match v {
389 "author-date" => Ok(Processing::AuthorDate),
390 "author-date-givenname" => Ok(Processing::AuthorDateGivenname),
391 "author-date-names" => Ok(Processing::AuthorDateNames),
392 "author-date-full" => Ok(Processing::AuthorDateFull),
393 "numeric" => Ok(Processing::Numeric),
394 "note" => Ok(Processing::Note),
395 "label" => Ok(Processing::Label(LabelConfig::default())),
396 other => Err(E::unknown_variant(other, PROCESSING_STRING_VARIANTS)),
397 }
398 }
399
400 fn visit_enum<A: de::EnumAccess<'de>>(self, data: A) -> Result<Processing, A::Error> {
401 use serde::de::VariantAccess;
402 let (variant, access) = data.variant::<String>()?;
403 match variant.as_str() {
404 "custom" => {
405 let custom: ProcessingCustom = access.newtype_variant()?;
406 Ok(Processing::Custom(custom))
407 }
408 other => Err(de::Error::unknown_variant(other, &["custom"])),
411 }
412 }
413
414 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Processing, A::Error> {
415 let key: String = map
416 .next_key()?
417 .ok_or_else(|| de::Error::invalid_length(0, &"1"))?;
418 match key.as_str() {
419 "label" => {
420 let config: LabelConfig = map.next_value()?;
421 Ok(Processing::Label(config))
422 }
423 "sort" | "group" | "disambiguate" => {
424 let mut sort = None;
429 let mut group = None;
430 let mut disambiguate = None;
431
432 match key.as_str() {
434 "sort" => sort = Some(map.next_value()?),
435 "group" => group = Some(map.next_value()?),
436 "disambiguate" => disambiguate = Some(map.next_value()?),
437 _ => {
438 return Err(de::Error::unknown_field(
439 &key,
440 &["sort", "group", "disambiguate"],
441 ));
442 }
443 }
444
445 while let Some(k) = map.next_key::<String>()? {
447 match k.as_str() {
448 "sort" => sort = Some(map.next_value()?),
449 "group" => group = Some(map.next_value()?),
450 "disambiguate" => disambiguate = Some(map.next_value()?),
451 other => {
452 return Err(de::Error::unknown_field(
453 other,
454 &["sort", "group", "disambiguate"],
455 ));
456 }
457 }
458 }
459
460 Ok(Processing::Custom(ProcessingCustom {
461 sort,
462 group,
463 disambiguate,
464 }))
465 }
466 other => Err(de::Error::unknown_field(
467 other,
468 &["label", "sort", "group", "disambiguate"],
469 )),
470 }
471 }
472 }
473
474 deserializer.deserialize_any(ProcessingVisitor)
475 }
476}
477
478#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
486#[cfg_attr(feature = "schema", derive(JsonSchema))]
487#[serde(rename_all = "kebab-case")]
488#[non_exhaustive]
489pub enum GivennameRule {
490 #[default]
493 ByCite,
494 AllNames,
496 AllNamesWithInitials,
498 PrimaryName,
500 PrimaryNameWithInitials,
502}
503
504#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
508#[cfg_attr(feature = "schema", derive(JsonSchema))]
509#[serde(rename_all = "kebab-case")]
510pub struct Disambiguation {
511 pub names: bool,
513 #[serde(default)]
515 pub add_givenname: bool,
516 #[serde(default)]
518 pub givenname_rule: GivennameRule,
519 pub year_suffix: bool,
521}
522
523impl Default for Disambiguation {
524 fn default() -> Self {
525 Self {
526 names: true,
527 add_givenname: false,
528 givenname_rule: GivennameRule::default(),
529 year_suffix: false,
530 }
531 }
532}
533
534#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
538#[cfg_attr(feature = "schema", derive(JsonSchema))]
539#[serde(rename_all = "kebab-case")]
540pub struct Sort {
541 #[serde(default)]
543 pub shorten_names: bool,
544 #[serde(default)]
546 pub render_substitutions: bool,
547 pub template: Vec<SortSpec>,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
555#[cfg_attr(feature = "schema", derive(JsonSchema))]
556#[serde(untagged)]
557pub enum SortEntry {
558 Preset(crate::presets::SortPreset),
560 Explicit(Sort),
562}
563
564impl SortEntry {
565 pub fn resolve(&self) -> Sort {
569 match self {
570 SortEntry::Preset(preset) => preset.sort(),
571 SortEntry::Explicit(sort) => sort.clone(),
572 }
573 }
574}
575
576#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
580#[cfg_attr(feature = "schema", derive(JsonSchema))]
581#[serde(rename_all = "kebab-case")]
582pub struct SortSpec {
583 pub key: SortKey,
585 #[serde(default = "default_ascending")]
587 pub ascending: bool,
588}
589
590fn default_ascending() -> bool {
591 true
592}
593
594#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
598#[cfg_attr(feature = "schema", derive(JsonSchema))]
599#[serde(rename_all = "kebab-case")]
600#[non_exhaustive]
601pub enum SortKey {
602 #[default]
604 Author,
605 Year,
607 Title,
609 CitationNumber,
611}
612
613#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
617#[cfg_attr(feature = "schema", derive(JsonSchema))]
618#[serde(rename_all = "kebab-case")]
619pub struct Group {
620 pub template: Vec<SortKey>,
622}
623
624#[cfg(test)]
625#[allow(
626 clippy::unwrap_used,
627 clippy::expect_used,
628 clippy::panic,
629 clippy::indexing_slicing,
630 clippy::todo,
631 clippy::unimplemented,
632 clippy::unreachable,
633 clippy::get_unwrap,
634 reason = "Panicking is acceptable and often desired in tests."
635)]
636mod tests {
637 use super::*;
638
639 #[test]
641 fn test_label_config_alpha_preset_defaults() {
642 let config = LabelConfig {
643 preset: LabelPreset::Alpha,
644 single_author_chars: None,
645 multi_author_chars: None,
646 et_al_min: None,
647 et_al_marker: None,
648 et_al_names: None,
649 year_digits: None,
650 };
651
652 let params = config.effective_params();
653 assert_eq!(params.single_author_chars, 3);
654 assert_eq!(params.multi_author_chars, 1);
655 assert_eq!(params.et_al_min, 4);
656 assert_eq!(params.et_al_marker, "+");
657 assert_eq!(params.et_al_names, 3);
658 assert_eq!(params.year_digits, 2);
659 }
660
661 #[test]
663 fn test_label_config_alpha_with_overrides() {
664 let config = LabelConfig {
665 preset: LabelPreset::Alpha,
666 single_author_chars: Some(5),
667 multi_author_chars: Some(2),
668 et_al_min: Some(5),
669 et_al_marker: Some("*".to_string()),
670 et_al_names: Some(4),
671 year_digits: Some(4),
672 };
673
674 let params = config.effective_params();
675 assert_eq!(params.single_author_chars, 5);
676 assert_eq!(params.multi_author_chars, 2);
677 assert_eq!(params.et_al_min, 5);
678 assert_eq!(params.et_al_marker, "*");
679 assert_eq!(params.et_al_names, 4);
680 assert_eq!(params.year_digits, 4);
681 }
682
683 #[test]
685 fn test_label_config_din_preset_defaults() {
686 let config = LabelConfig {
687 preset: LabelPreset::Din,
688 single_author_chars: None,
689 multi_author_chars: None,
690 et_al_min: None,
691 et_al_marker: None,
692 et_al_names: None,
693 year_digits: None,
694 };
695
696 let params = config.effective_params();
697 assert_eq!(params.single_author_chars, 4);
698 assert_eq!(params.multi_author_chars, 1);
699 assert_eq!(params.et_al_min, 3);
700 assert_eq!(params.et_al_marker, "");
701 assert_eq!(params.et_al_names, 3);
702 assert_eq!(params.year_digits, 2);
703 }
704
705 #[test]
707 fn test_label_config_ams_preset_defaults() {
708 let config = LabelConfig {
709 preset: LabelPreset::Ams,
710 single_author_chars: None,
711 multi_author_chars: None,
712 et_al_min: None,
713 et_al_marker: None,
714 et_al_names: None,
715 year_digits: None,
716 };
717
718 let params = config.effective_params();
719 assert_eq!(params.single_author_chars, 4);
720 assert_eq!(params.multi_author_chars, 1);
721 assert_eq!(params.et_al_min, 5);
722 assert_eq!(params.et_al_marker, "");
723 assert_eq!(params.et_al_names, 4);
724 assert_eq!(params.year_digits, 2);
725 }
726
727 #[test]
729 fn test_processing_author_date_default_bibliography_sort() {
730 let processing = Processing::AuthorDate;
731 let sort = processing.default_bibliography_sort();
732 assert_eq!(sort, Some(SortPreset::AuthorDateTitle));
733 }
734
735 #[test]
737 fn test_processing_numeric_default_bibliography_sort() {
738 let processing = Processing::Numeric;
739 let sort = processing.default_bibliography_sort();
740 assert_eq!(sort, None);
741 }
742
743 #[test]
745 fn test_processing_note_default_bibliography_sort() {
746 let processing = Processing::Note;
747 let sort = processing.default_bibliography_sort();
748 assert_eq!(sort, Some(SortPreset::AuthorTitleDate));
749 }
750
751 #[test]
753 fn test_processing_citation_sort_policy() {
754 let modes = vec![
755 Processing::AuthorDate,
756 Processing::AuthorDateGivenname,
757 Processing::AuthorDateNames,
758 Processing::AuthorDateFull,
759 Processing::Numeric,
760 Processing::Note,
761 Processing::Label(LabelConfig::default()),
762 Processing::Custom(ProcessingCustom::default()),
763 ];
764
765 for mode in modes {
766 assert_eq!(
767 mode.default_citation_sort_policy(),
768 CitationSortPolicy::ExplicitOnly
769 );
770 }
771 }
772
773 #[test]
775 fn test_processing_author_date_variant_configs() {
776 let cases = [
777 (Processing::AuthorDate, false, false),
778 (Processing::AuthorDateGivenname, false, true),
779 (Processing::AuthorDateNames, true, false),
780 (Processing::AuthorDateFull, true, true),
781 ];
782
783 for (processing, names, add_givenname) in cases {
784 let config = processing.config();
785
786 assert_eq!(
787 config.sort,
788 Some(SortEntry::Preset(SortPreset::AuthorDateTitle))
789 );
790 assert_eq!(
791 config.group,
792 Some(Group {
793 template: vec![SortKey::Author, SortKey::Year],
794 })
795 );
796
797 let disambig = config.disambiguate.unwrap();
798 assert_eq!(disambig.names, names);
799 assert_eq!(disambig.add_givenname, add_givenname);
800 assert_eq!(disambig.givenname_rule, GivennameRule::ByCite);
801 assert!(disambig.year_suffix);
802 }
803 }
804
805 #[test]
807 fn test_processing_author_date_variant_names() {
808 let cases = [
809 (Processing::AuthorDate, "author-date"),
810 (Processing::AuthorDateGivenname, "author-date-givenname"),
811 (Processing::AuthorDateNames, "author-date-names"),
812 (Processing::AuthorDateFull, "author-date-full"),
813 ];
814
815 for (processing, name) in cases {
816 let serialized = serde_yaml::to_string(&processing).unwrap();
817 assert_eq!(serialized.trim(), name);
818
819 let deserialized: Processing = serde_yaml::from_str(name).unwrap();
820 assert_eq!(deserialized, processing);
821 }
822 }
823
824 #[test]
826 fn test_disambiguation_defaults() {
827 let disambig = Disambiguation::default();
828 assert!(disambig.names);
829 assert!(!disambig.add_givenname);
830 assert_eq!(disambig.givenname_rule, GivennameRule::ByCite);
831 assert!(!disambig.year_suffix);
832 }
833
834 #[test]
836 fn test_sort_entry_resolve_preset() {
837 let entry = SortEntry::Preset(SortPreset::AuthorDateTitle);
838 let sort = entry.resolve();
839
840 assert!(!sort.template.is_empty());
842 }
843
844 #[test]
846 fn test_sort_entry_resolve_explicit() {
847 let explicit = Sort {
848 shorten_names: true,
849 render_substitutions: false,
850 template: vec![SortSpec {
851 key: SortKey::Title,
852 ascending: false,
853 }],
854 };
855 let entry = SortEntry::Explicit(explicit.clone());
856 let resolved = entry.resolve();
857
858 assert!(resolved.shorten_names);
859 assert!(!resolved.render_substitutions);
860 assert_eq!(resolved.template.len(), 1);
861 assert_eq!(resolved.template[0].key, SortKey::Title);
862 assert!(!resolved.template[0].ascending);
863 }
864}