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(
226 names: bool,
227 add_givenname: bool,
228 givenname_rule: GivennameRule,
229) -> ProcessingCustom {
230 ProcessingCustom {
231 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
232 group: Some(Group {
233 template: vec![SortKey::Author, SortKey::Year],
234 }),
235 disambiguate: Some(Disambiguation {
236 names,
237 add_givenname,
238 givenname_rule,
239 year_suffix: true,
240 }),
241 }
242}
243
244impl Processing {
245 pub fn default_bibliography_sort(&self) -> Option<SortPreset> {
252 match self {
253 Processing::AuthorDate
254 | Processing::AuthorDateGivenname
255 | Processing::AuthorDateNames
256 | Processing::AuthorDateFull => Some(SortPreset::AuthorDateTitle),
257 Processing::Numeric => None,
258 Processing::Note => Some(SortPreset::AuthorTitleDate),
259 Processing::Label(_) => Some(SortPreset::AuthorDateTitle),
260 Processing::Custom(_) => None,
261 }
262 }
263
264 pub fn is_author_date_family(&self) -> bool {
269 matches!(
270 self,
271 Self::AuthorDate
272 | Self::AuthorDateGivenname
273 | Self::AuthorDateNames
274 | Self::AuthorDateFull
275 )
276 }
277
278 pub fn regime_family(&self) -> RegimeFamily {
289 match self {
290 Self::AuthorDate
291 | Self::AuthorDateGivenname
292 | Self::AuthorDateNames
293 | Self::AuthorDateFull => RegimeFamily::AuthorDate,
294 Self::Numeric => RegimeFamily::Numeric,
295 Self::Note => RegimeFamily::Note,
296 Self::Label(_) => RegimeFamily::Label,
297 Self::Custom(_) => RegimeFamily::Custom,
298 }
299 }
300
301 pub fn default_citation_sort_policy(&self) -> CitationSortPolicy {
306 CitationSortPolicy::ExplicitOnly
307 }
308
309 pub fn config(&self) -> ProcessingCustom {
314 match self {
315 Processing::AuthorDate => author_date_config(false, false, GivennameRule::ByCite),
316 Processing::AuthorDateGivenname => {
317 author_date_config(false, true, GivennameRule::ByCite)
318 }
319 Processing::AuthorDateNames => author_date_config(true, false, GivennameRule::ByCite),
320 Processing::AuthorDateFull => {
327 author_date_config(true, true, GivennameRule::PrimaryName)
328 }
329 Processing::Numeric => ProcessingCustom {
330 sort: None,
331 group: None,
332 disambiguate: None,
333 },
334 Processing::Note => ProcessingCustom {
335 sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
336 group: None,
337 disambiguate: Some(Disambiguation {
338 names: true,
339 add_givenname: false,
340 givenname_rule: GivennameRule::default(),
341 year_suffix: false,
342 }),
343 },
344 Processing::Label(_) => ProcessingCustom {
345 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
346 group: None,
347 disambiguate: Some(Disambiguation {
348 names: false,
349 add_givenname: false,
350 givenname_rule: GivennameRule::default(),
351 year_suffix: true,
352 }),
353 },
354 Processing::Custom(custom) => custom.clone(),
355 }
356 }
357}
358
359impl Serialize for Processing {
360 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
361 where
362 S: serde::Serializer,
363 {
364 match self {
365 Processing::AuthorDate => serializer.serialize_str("author-date"),
366 Processing::AuthorDateGivenname => serializer.serialize_str("author-date-givenname"),
367 Processing::AuthorDateNames => serializer.serialize_str("author-date-names"),
368 Processing::AuthorDateFull => serializer.serialize_str("author-date-full"),
369 Processing::Numeric => serializer.serialize_str("numeric"),
370 Processing::Note => serializer.serialize_str("note"),
371 Processing::Label(config) => {
372 use serde::ser::SerializeMap;
373 let mut map = serializer.serialize_map(Some(1))?;
374 map.serialize_entry("label", config)?;
375 map.end()
376 }
377 Processing::Custom(custom) => custom.serialize(serializer),
381 }
382 }
383}
384
385impl<'de> Deserialize<'de> for Processing {
386 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
387 where
388 D: serde::Deserializer<'de>,
389 {
390 use serde::de::{self, MapAccess, Visitor};
391
392 struct ProcessingVisitor;
393
394 impl<'de> Visitor<'de> for ProcessingVisitor {
395 type Value = Processing;
396
397 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
398 f.write_str("a processing mode string or map")
399 }
400
401 fn visit_str<E: de::Error>(self, v: &str) -> Result<Processing, E> {
402 match v {
403 "author-date" => Ok(Processing::AuthorDate),
404 "author-date-givenname" => Ok(Processing::AuthorDateGivenname),
405 "author-date-names" => Ok(Processing::AuthorDateNames),
406 "author-date-full" => Ok(Processing::AuthorDateFull),
407 "numeric" => Ok(Processing::Numeric),
408 "note" => Ok(Processing::Note),
409 "label" => Ok(Processing::Label(LabelConfig::default())),
410 other => Err(E::unknown_variant(other, PROCESSING_STRING_VARIANTS)),
411 }
412 }
413
414 fn visit_enum<A: de::EnumAccess<'de>>(self, data: A) -> Result<Processing, A::Error> {
415 use serde::de::VariantAccess;
416 let (variant, access) = data.variant::<String>()?;
417 match variant.as_str() {
418 "custom" => {
419 let custom: ProcessingCustom = access.newtype_variant()?;
420 Ok(Processing::Custom(custom))
421 }
422 other => Err(de::Error::unknown_variant(other, &["custom"])),
425 }
426 }
427
428 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Processing, A::Error> {
429 let key: String = map
430 .next_key()?
431 .ok_or_else(|| de::Error::invalid_length(0, &"1"))?;
432 match key.as_str() {
433 "label" => {
434 let config: LabelConfig = map.next_value()?;
435 Ok(Processing::Label(config))
436 }
437 "sort" | "group" | "disambiguate" => {
438 let mut sort = None;
443 let mut group = None;
444 let mut disambiguate = None;
445
446 match key.as_str() {
448 "sort" => sort = Some(map.next_value()?),
449 "group" => group = Some(map.next_value()?),
450 "disambiguate" => disambiguate = Some(map.next_value()?),
451 _ => {
452 return Err(de::Error::unknown_field(
453 &key,
454 &["sort", "group", "disambiguate"],
455 ));
456 }
457 }
458
459 while let Some(k) = map.next_key::<String>()? {
461 match k.as_str() {
462 "sort" => sort = Some(map.next_value()?),
463 "group" => group = Some(map.next_value()?),
464 "disambiguate" => disambiguate = Some(map.next_value()?),
465 other => {
466 return Err(de::Error::unknown_field(
467 other,
468 &["sort", "group", "disambiguate"],
469 ));
470 }
471 }
472 }
473
474 Ok(Processing::Custom(ProcessingCustom {
475 sort,
476 group,
477 disambiguate,
478 }))
479 }
480 other => Err(de::Error::unknown_field(
481 other,
482 &["label", "sort", "group", "disambiguate"],
483 )),
484 }
485 }
486 }
487
488 deserializer.deserialize_any(ProcessingVisitor)
489 }
490}
491
492#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
500#[cfg_attr(feature = "schema", derive(JsonSchema))]
501#[serde(rename_all = "kebab-case")]
502#[non_exhaustive]
503pub enum GivennameRule {
504 #[default]
507 ByCite,
508 AllNames,
510 AllNamesWithInitials,
512 PrimaryName,
514 PrimaryNameWithInitials,
516}
517
518#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
522#[cfg_attr(feature = "schema", derive(JsonSchema))]
523#[serde(rename_all = "kebab-case")]
524pub struct Disambiguation {
525 pub names: bool,
527 #[serde(default)]
529 pub add_givenname: bool,
530 #[serde(default)]
532 pub givenname_rule: GivennameRule,
533 pub year_suffix: bool,
535}
536
537impl Default for Disambiguation {
538 fn default() -> Self {
539 Self {
540 names: true,
541 add_givenname: false,
542 givenname_rule: GivennameRule::default(),
543 year_suffix: false,
544 }
545 }
546}
547
548#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
552#[cfg_attr(feature = "schema", derive(JsonSchema))]
553#[serde(rename_all = "kebab-case")]
554pub struct Sort {
555 #[serde(default)]
557 pub shorten_names: bool,
558 #[serde(default)]
560 pub render_substitutions: bool,
561 pub template: Vec<SortSpec>,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
569#[cfg_attr(feature = "schema", derive(JsonSchema))]
570#[serde(untagged)]
571pub enum SortEntry {
572 Preset(crate::presets::SortPreset),
574 Explicit(Sort),
576}
577
578impl SortEntry {
579 pub fn resolve(&self) -> Sort {
583 match self {
584 SortEntry::Preset(preset) => preset.sort(),
585 SortEntry::Explicit(sort) => sort.clone(),
586 }
587 }
588}
589
590#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
594#[cfg_attr(feature = "schema", derive(JsonSchema))]
595#[serde(rename_all = "kebab-case")]
596pub struct SortSpec {
597 pub key: SortKey,
599 #[serde(default = "default_ascending")]
601 pub ascending: bool,
602}
603
604fn default_ascending() -> bool {
605 true
606}
607
608#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
612#[cfg_attr(feature = "schema", derive(JsonSchema))]
613#[serde(rename_all = "kebab-case")]
614#[non_exhaustive]
615pub enum SortKey {
616 #[default]
618 Author,
619 Year,
621 Title,
623 CitationNumber,
625}
626
627#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
631#[cfg_attr(feature = "schema", derive(JsonSchema))]
632#[serde(rename_all = "kebab-case")]
633pub struct Group {
634 pub template: Vec<SortKey>,
636}
637
638#[cfg(test)]
639#[allow(
640 clippy::unwrap_used,
641 clippy::expect_used,
642 clippy::panic,
643 clippy::indexing_slicing,
644 clippy::todo,
645 clippy::unimplemented,
646 clippy::unreachable,
647 clippy::get_unwrap,
648 reason = "Panicking is acceptable and often desired in tests."
649)]
650mod tests {
651 use super::*;
652
653 #[test]
655 fn test_label_config_alpha_preset_defaults() {
656 let config = LabelConfig {
657 preset: LabelPreset::Alpha,
658 single_author_chars: None,
659 multi_author_chars: None,
660 et_al_min: None,
661 et_al_marker: None,
662 et_al_names: None,
663 year_digits: None,
664 };
665
666 let params = config.effective_params();
667 assert_eq!(params.single_author_chars, 3);
668 assert_eq!(params.multi_author_chars, 1);
669 assert_eq!(params.et_al_min, 4);
670 assert_eq!(params.et_al_marker, "+");
671 assert_eq!(params.et_al_names, 3);
672 assert_eq!(params.year_digits, 2);
673 }
674
675 #[test]
677 fn test_label_config_alpha_with_overrides() {
678 let config = LabelConfig {
679 preset: LabelPreset::Alpha,
680 single_author_chars: Some(5),
681 multi_author_chars: Some(2),
682 et_al_min: Some(5),
683 et_al_marker: Some("*".to_string()),
684 et_al_names: Some(4),
685 year_digits: Some(4),
686 };
687
688 let params = config.effective_params();
689 assert_eq!(params.single_author_chars, 5);
690 assert_eq!(params.multi_author_chars, 2);
691 assert_eq!(params.et_al_min, 5);
692 assert_eq!(params.et_al_marker, "*");
693 assert_eq!(params.et_al_names, 4);
694 assert_eq!(params.year_digits, 4);
695 }
696
697 #[test]
699 fn test_label_config_din_preset_defaults() {
700 let config = LabelConfig {
701 preset: LabelPreset::Din,
702 single_author_chars: None,
703 multi_author_chars: None,
704 et_al_min: None,
705 et_al_marker: None,
706 et_al_names: None,
707 year_digits: None,
708 };
709
710 let params = config.effective_params();
711 assert_eq!(params.single_author_chars, 4);
712 assert_eq!(params.multi_author_chars, 1);
713 assert_eq!(params.et_al_min, 3);
714 assert_eq!(params.et_al_marker, "");
715 assert_eq!(params.et_al_names, 3);
716 assert_eq!(params.year_digits, 2);
717 }
718
719 #[test]
721 fn test_label_config_ams_preset_defaults() {
722 let config = LabelConfig {
723 preset: LabelPreset::Ams,
724 single_author_chars: None,
725 multi_author_chars: None,
726 et_al_min: None,
727 et_al_marker: None,
728 et_al_names: None,
729 year_digits: None,
730 };
731
732 let params = config.effective_params();
733 assert_eq!(params.single_author_chars, 4);
734 assert_eq!(params.multi_author_chars, 1);
735 assert_eq!(params.et_al_min, 5);
736 assert_eq!(params.et_al_marker, "");
737 assert_eq!(params.et_al_names, 4);
738 assert_eq!(params.year_digits, 2);
739 }
740
741 #[test]
743 fn test_processing_author_date_default_bibliography_sort() {
744 let processing = Processing::AuthorDate;
745 let sort = processing.default_bibliography_sort();
746 assert_eq!(sort, Some(SortPreset::AuthorDateTitle));
747 }
748
749 #[test]
751 fn test_processing_numeric_default_bibliography_sort() {
752 let processing = Processing::Numeric;
753 let sort = processing.default_bibliography_sort();
754 assert_eq!(sort, None);
755 }
756
757 #[test]
759 fn test_processing_note_default_bibliography_sort() {
760 let processing = Processing::Note;
761 let sort = processing.default_bibliography_sort();
762 assert_eq!(sort, Some(SortPreset::AuthorTitleDate));
763 }
764
765 #[test]
767 fn test_processing_citation_sort_policy() {
768 let modes = vec![
769 Processing::AuthorDate,
770 Processing::AuthorDateGivenname,
771 Processing::AuthorDateNames,
772 Processing::AuthorDateFull,
773 Processing::Numeric,
774 Processing::Note,
775 Processing::Label(LabelConfig::default()),
776 Processing::Custom(ProcessingCustom::default()),
777 ];
778
779 for mode in modes {
780 assert_eq!(
781 mode.default_citation_sort_policy(),
782 CitationSortPolicy::ExplicitOnly
783 );
784 }
785 }
786
787 #[test]
789 fn test_processing_author_date_variant_configs() {
790 let cases = [
791 (Processing::AuthorDate, false, false, GivennameRule::ByCite),
792 (
793 Processing::AuthorDateGivenname,
794 false,
795 true,
796 GivennameRule::ByCite,
797 ),
798 (
799 Processing::AuthorDateNames,
800 true,
801 false,
802 GivennameRule::ByCite,
803 ),
804 (
806 Processing::AuthorDateFull,
807 true,
808 true,
809 GivennameRule::PrimaryName,
810 ),
811 ];
812
813 for (processing, names, add_givenname, expected_rule) in cases {
814 let config = processing.config();
815
816 assert_eq!(
817 config.sort,
818 Some(SortEntry::Preset(SortPreset::AuthorDateTitle))
819 );
820 assert_eq!(
821 config.group,
822 Some(Group {
823 template: vec![SortKey::Author, SortKey::Year],
824 })
825 );
826
827 let disambig = config.disambiguate.unwrap();
828 assert_eq!(disambig.names, names);
829 assert_eq!(disambig.add_givenname, add_givenname);
830 assert_eq!(disambig.givenname_rule, expected_rule);
831 assert!(disambig.year_suffix);
832 }
833 }
834
835 #[test]
837 fn test_processing_author_date_variant_names() {
838 let cases = [
839 (Processing::AuthorDate, "author-date"),
840 (Processing::AuthorDateGivenname, "author-date-givenname"),
841 (Processing::AuthorDateNames, "author-date-names"),
842 (Processing::AuthorDateFull, "author-date-full"),
843 ];
844
845 for (processing, name) in cases {
846 let serialized = serde_yaml::to_string(&processing).unwrap();
847 assert_eq!(serialized.trim(), name);
848
849 let deserialized: Processing = serde_yaml::from_str(name).unwrap();
850 assert_eq!(deserialized, processing);
851 }
852 }
853
854 #[test]
856 fn test_disambiguation_defaults() {
857 let disambig = Disambiguation::default();
858 assert!(disambig.names);
859 assert!(!disambig.add_givenname);
860 assert_eq!(disambig.givenname_rule, GivennameRule::ByCite);
861 assert!(!disambig.year_suffix);
862 }
863
864 #[test]
866 fn test_sort_entry_resolve_preset() {
867 let entry = SortEntry::Preset(SortPreset::AuthorDateTitle);
868 let sort = entry.resolve();
869
870 assert!(!sort.template.is_empty());
872 }
873
874 #[test]
876 fn test_sort_entry_resolve_explicit() {
877 let explicit = Sort {
878 shorten_names: true,
879 render_substitutions: false,
880 template: vec![SortSpec {
881 key: SortKey::Title,
882 ascending: false,
883 }],
884 };
885 let entry = SortEntry::Explicit(explicit.clone());
886 let resolved = entry.resolve();
887
888 assert!(resolved.shorten_names);
889 assert!(!resolved.render_substitutions);
890 assert_eq!(resolved.template.len(), 1);
891 assert_eq!(resolved.template[0].key, SortKey::Title);
892 assert!(!resolved.template[0].ascending);
893 }
894}