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 givenname_rule: GivennameRule::default(),
226 year_suffix: true,
227 }),
228 },
229 Processing::Numeric => ProcessingCustom {
230 sort: None,
231 group: None,
232 disambiguate: None,
233 },
234 Processing::Note => ProcessingCustom {
235 sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
236 group: None,
237 disambiguate: Some(Disambiguation {
238 names: true,
239 add_givenname: false,
240 givenname_rule: GivennameRule::default(),
241 year_suffix: false,
242 }),
243 },
244 Processing::Label(_) => ProcessingCustom {
245 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
246 group: None,
247 disambiguate: Some(Disambiguation {
248 names: false,
249 add_givenname: false,
250 givenname_rule: GivennameRule::default(),
251 year_suffix: true,
252 }),
253 },
254 Processing::Custom(custom) => custom.clone(),
255 }
256 }
257}
258
259impl Serialize for Processing {
260 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
261 where
262 S: serde::Serializer,
263 {
264 match self {
265 Processing::AuthorDate => serializer.serialize_str("author-date"),
266 Processing::Numeric => serializer.serialize_str("numeric"),
267 Processing::Note => serializer.serialize_str("note"),
268 Processing::Label(config) => {
269 use serde::ser::SerializeMap;
270 let mut map = serializer.serialize_map(Some(1))?;
271 map.serialize_entry("label", config)?;
272 map.end()
273 }
274 Processing::Custom(custom) => custom.serialize(serializer),
278 }
279 }
280}
281
282impl<'de> Deserialize<'de> for Processing {
283 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
284 where
285 D: serde::Deserializer<'de>,
286 {
287 use serde::de::{self, MapAccess, Visitor};
288
289 struct ProcessingVisitor;
290
291 impl<'de> Visitor<'de> for ProcessingVisitor {
292 type Value = Processing;
293
294 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
295 f.write_str("a processing mode string or map")
296 }
297
298 fn visit_str<E: de::Error>(self, v: &str) -> Result<Processing, E> {
299 match v {
300 "author-date" => Ok(Processing::AuthorDate),
301 "numeric" => Ok(Processing::Numeric),
302 "note" => Ok(Processing::Note),
303 "label" => Ok(Processing::Label(LabelConfig::default())),
304 other => Err(E::unknown_variant(
305 other,
306 &["author-date", "numeric", "note", "label"],
307 )),
308 }
309 }
310
311 fn visit_enum<A: de::EnumAccess<'de>>(self, data: A) -> Result<Processing, A::Error> {
312 use serde::de::VariantAccess;
313 let (variant, access) = data.variant::<String>()?;
314 match variant.as_str() {
315 "custom" => {
316 let custom: ProcessingCustom = access.newtype_variant()?;
317 Ok(Processing::Custom(custom))
318 }
319 other => Err(de::Error::unknown_variant(
320 other,
321 &["author-date", "numeric", "note", "label", "custom"],
322 )),
323 }
324 }
325
326 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Processing, A::Error> {
327 let key: String = map
328 .next_key()?
329 .ok_or_else(|| de::Error::invalid_length(0, &"1"))?;
330 match key.as_str() {
331 "label" => {
332 let config: LabelConfig = map.next_value()?;
333 Ok(Processing::Label(config))
334 }
335 "sort" | "group" | "disambiguate" => {
336 let mut sort = None;
341 let mut group = None;
342 let mut disambiguate = None;
343
344 match key.as_str() {
346 "sort" => sort = Some(map.next_value()?),
347 "group" => group = Some(map.next_value()?),
348 "disambiguate" => disambiguate = Some(map.next_value()?),
349 _ => {
350 return Err(de::Error::unknown_field(
351 &key,
352 &["sort", "group", "disambiguate"],
353 ));
354 }
355 }
356
357 while let Some(k) = map.next_key::<String>()? {
359 match k.as_str() {
360 "sort" => sort = Some(map.next_value()?),
361 "group" => group = Some(map.next_value()?),
362 "disambiguate" => disambiguate = Some(map.next_value()?),
363 other => {
364 return Err(de::Error::unknown_field(
365 other,
366 &["sort", "group", "disambiguate"],
367 ));
368 }
369 }
370 }
371
372 Ok(Processing::Custom(ProcessingCustom {
373 sort,
374 group,
375 disambiguate,
376 }))
377 }
378 other => Err(de::Error::unknown_field(
379 other,
380 &["label", "sort", "group", "disambiguate"],
381 )),
382 }
383 }
384 }
385
386 deserializer.deserialize_any(ProcessingVisitor)
387 }
388}
389
390#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
398#[cfg_attr(feature = "schema", derive(JsonSchema))]
399#[serde(rename_all = "kebab-case")]
400#[non_exhaustive]
401pub enum GivennameRule {
402 #[default]
405 ByCite,
406 AllNames,
408 AllNamesWithInitials,
410 PrimaryName,
412 PrimaryNameWithInitials,
414}
415
416#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
420#[cfg_attr(feature = "schema", derive(JsonSchema))]
421#[serde(rename_all = "kebab-case")]
422pub struct Disambiguation {
423 pub names: bool,
425 #[serde(default)]
427 pub add_givenname: bool,
428 #[serde(default)]
430 pub givenname_rule: GivennameRule,
431 pub year_suffix: bool,
433}
434
435impl Default for Disambiguation {
436 fn default() -> Self {
437 Self {
438 names: true,
439 add_givenname: false,
440 givenname_rule: GivennameRule::default(),
441 year_suffix: false,
442 }
443 }
444}
445
446#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
450#[cfg_attr(feature = "schema", derive(JsonSchema))]
451#[serde(rename_all = "kebab-case")]
452pub struct Sort {
453 #[serde(default)]
455 pub shorten_names: bool,
456 #[serde(default)]
458 pub render_substitutions: bool,
459 pub template: Vec<SortSpec>,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
467#[cfg_attr(feature = "schema", derive(JsonSchema))]
468#[serde(untagged)]
469pub enum SortEntry {
470 Preset(crate::presets::SortPreset),
472 Explicit(Sort),
474}
475
476impl SortEntry {
477 pub fn resolve(&self) -> Sort {
481 match self {
482 SortEntry::Preset(preset) => preset.sort(),
483 SortEntry::Explicit(sort) => sort.clone(),
484 }
485 }
486}
487
488#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
492#[cfg_attr(feature = "schema", derive(JsonSchema))]
493#[serde(rename_all = "kebab-case")]
494pub struct SortSpec {
495 pub key: SortKey,
497 #[serde(default = "default_ascending")]
499 pub ascending: bool,
500}
501
502fn default_ascending() -> bool {
503 true
504}
505
506#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
510#[cfg_attr(feature = "schema", derive(JsonSchema))]
511#[serde(rename_all = "kebab-case")]
512#[non_exhaustive]
513pub enum SortKey {
514 #[default]
516 Author,
517 Year,
519 Title,
521 CitationNumber,
523}
524
525#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
529#[cfg_attr(feature = "schema", derive(JsonSchema))]
530#[serde(rename_all = "kebab-case")]
531pub struct Group {
532 pub template: Vec<SortKey>,
534}
535
536#[cfg(test)]
537#[allow(
538 clippy::unwrap_used,
539 clippy::expect_used,
540 clippy::panic,
541 clippy::indexing_slicing,
542 clippy::todo,
543 clippy::unimplemented,
544 clippy::unreachable,
545 clippy::get_unwrap,
546 reason = "Panicking is acceptable and often desired in tests."
547)]
548mod tests {
549 use super::*;
550
551 #[test]
553 fn test_label_config_alpha_preset_defaults() {
554 let config = LabelConfig {
555 preset: LabelPreset::Alpha,
556 single_author_chars: None,
557 multi_author_chars: None,
558 et_al_min: None,
559 et_al_marker: None,
560 et_al_names: None,
561 year_digits: None,
562 };
563
564 let params = config.effective_params();
565 assert_eq!(params.single_author_chars, 3);
566 assert_eq!(params.multi_author_chars, 1);
567 assert_eq!(params.et_al_min, 4);
568 assert_eq!(params.et_al_marker, "+");
569 assert_eq!(params.et_al_names, 3);
570 assert_eq!(params.year_digits, 2);
571 }
572
573 #[test]
575 fn test_label_config_alpha_with_overrides() {
576 let config = LabelConfig {
577 preset: LabelPreset::Alpha,
578 single_author_chars: Some(5),
579 multi_author_chars: Some(2),
580 et_al_min: Some(5),
581 et_al_marker: Some("*".to_string()),
582 et_al_names: Some(4),
583 year_digits: Some(4),
584 };
585
586 let params = config.effective_params();
587 assert_eq!(params.single_author_chars, 5);
588 assert_eq!(params.multi_author_chars, 2);
589 assert_eq!(params.et_al_min, 5);
590 assert_eq!(params.et_al_marker, "*");
591 assert_eq!(params.et_al_names, 4);
592 assert_eq!(params.year_digits, 4);
593 }
594
595 #[test]
597 fn test_label_config_din_preset_defaults() {
598 let config = LabelConfig {
599 preset: LabelPreset::Din,
600 single_author_chars: None,
601 multi_author_chars: None,
602 et_al_min: None,
603 et_al_marker: None,
604 et_al_names: None,
605 year_digits: None,
606 };
607
608 let params = config.effective_params();
609 assert_eq!(params.single_author_chars, 4);
610 assert_eq!(params.multi_author_chars, 1);
611 assert_eq!(params.et_al_min, 3);
612 assert_eq!(params.et_al_marker, "");
613 assert_eq!(params.et_al_names, 3);
614 assert_eq!(params.year_digits, 2);
615 }
616
617 #[test]
619 fn test_processing_author_date_default_bibliography_sort() {
620 let processing = Processing::AuthorDate;
621 let sort = processing.default_bibliography_sort();
622 assert_eq!(sort, Some(SortPreset::AuthorDateTitle));
623 }
624
625 #[test]
627 fn test_processing_numeric_default_bibliography_sort() {
628 let processing = Processing::Numeric;
629 let sort = processing.default_bibliography_sort();
630 assert_eq!(sort, None);
631 }
632
633 #[test]
635 fn test_processing_note_default_bibliography_sort() {
636 let processing = Processing::Note;
637 let sort = processing.default_bibliography_sort();
638 assert_eq!(sort, Some(SortPreset::AuthorTitleDate));
639 }
640
641 #[test]
643 fn test_processing_citation_sort_policy() {
644 let modes = vec![
645 Processing::AuthorDate,
646 Processing::Numeric,
647 Processing::Note,
648 Processing::Label(LabelConfig::default()),
649 Processing::Custom(ProcessingCustom::default()),
650 ];
651
652 for mode in modes {
653 assert_eq!(
654 mode.default_citation_sort_policy(),
655 CitationSortPolicy::ExplicitOnly
656 );
657 }
658 }
659
660 #[test]
662 fn test_processing_author_date_config() {
663 let processing = Processing::AuthorDate;
664 let config = processing.config();
665
666 assert!(config.sort.is_some());
667 assert!(config.group.is_some());
668 assert!(config.disambiguate.is_some());
669
670 let disambig = config.disambiguate.unwrap();
671 assert!(disambig.names);
672 assert!(disambig.add_givenname);
673 assert!(disambig.year_suffix);
674 }
675
676 #[test]
678 fn test_disambiguation_defaults() {
679 let disambig = Disambiguation::default();
680 assert!(disambig.names);
681 assert!(!disambig.add_givenname);
682 assert_eq!(disambig.givenname_rule, GivennameRule::ByCite);
683 assert!(!disambig.year_suffix);
684 }
685
686 #[test]
688 fn test_sort_entry_resolve_preset() {
689 let entry = SortEntry::Preset(SortPreset::AuthorDateTitle);
690 let sort = entry.resolve();
691
692 assert!(!sort.template.is_empty());
694 }
695
696 #[test]
698 fn test_sort_entry_resolve_explicit() {
699 let explicit = Sort {
700 shorten_names: true,
701 render_substitutions: false,
702 template: vec![SortSpec {
703 key: SortKey::Title,
704 ascending: false,
705 }],
706 };
707 let entry = SortEntry::Explicit(explicit.clone());
708 let resolved = entry.resolve();
709
710 assert!(resolved.shorten_names);
711 assert!(!resolved.render_substitutions);
712 assert_eq!(resolved.template.len(), 1);
713 assert_eq!(resolved.template[0].key, SortKey::Title);
714 assert!(!resolved.template[0].ascending);
715 }
716}