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, Serialize)]
131#[cfg_attr(feature = "schema", derive(JsonSchema))]
132#[serde(rename_all = "kebab-case")]
133#[non_exhaustive]
134pub enum Processing {
135 #[default]
138 AuthorDate,
139 Numeric,
142 Note,
145 Label(LabelConfig),
148 Custom(ProcessingCustom),
151}
152
153#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
158#[cfg_attr(feature = "schema", derive(JsonSchema))]
159#[serde(rename_all = "kebab-case")]
160pub enum CitationSortPolicy {
161 ExplicitOnly,
163}
164
165#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
170#[cfg_attr(feature = "schema", derive(JsonSchema))]
171#[serde(rename_all = "kebab-case")]
172pub struct ProcessingCustom {
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub sort: Option<SortEntry>,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub group: Option<Group>,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub disambiguate: Option<Disambiguation>,
182}
183
184impl Processing {
185 pub fn default_bibliography_sort(&self) -> Option<SortPreset> {
192 match self {
193 Processing::AuthorDate => Some(SortPreset::AuthorDateTitle),
194 Processing::Numeric => None,
195 Processing::Note => Some(SortPreset::AuthorTitleDate),
196 Processing::Label(_) => Some(SortPreset::AuthorDateTitle),
197 Processing::Custom(_) => None,
198 }
199 }
200
201 pub fn default_citation_sort_policy(&self) -> CitationSortPolicy {
206 CitationSortPolicy::ExplicitOnly
207 }
208
209 pub fn config(&self) -> ProcessingCustom {
214 match self {
215 Processing::AuthorDate => ProcessingCustom {
216 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
217 group: Some(Group {
218 template: vec![SortKey::Author, SortKey::Year],
219 }),
220 disambiguate: Some(Disambiguation {
221 names: true,
222 add_givenname: true,
223 year_suffix: true,
224 }),
225 },
226 Processing::Numeric => ProcessingCustom {
227 sort: None,
228 group: None,
229 disambiguate: None,
230 },
231 Processing::Note => ProcessingCustom {
232 sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
233 group: None,
234 disambiguate: Some(Disambiguation {
235 names: true,
236 add_givenname: false,
237 year_suffix: false,
238 }),
239 },
240 Processing::Label(_) => ProcessingCustom {
241 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
242 group: None,
243 disambiguate: Some(Disambiguation {
244 names: false,
245 add_givenname: false,
246 year_suffix: true,
247 }),
248 },
249 Processing::Custom(custom) => custom.clone(),
250 }
251 }
252}
253
254impl<'de> Deserialize<'de> for Processing {
255 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
256 where
257 D: serde::Deserializer<'de>,
258 {
259 use serde::de::{self, MapAccess, Visitor};
260
261 struct ProcessingVisitor;
262
263 impl<'de> Visitor<'de> for ProcessingVisitor {
264 type Value = Processing;
265
266 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
267 f.write_str("a processing mode string or map")
268 }
269
270 fn visit_str<E: de::Error>(self, v: &str) -> Result<Processing, E> {
271 match v {
272 "author-date" => Ok(Processing::AuthorDate),
273 "numeric" => Ok(Processing::Numeric),
274 "note" => Ok(Processing::Note),
275 "label" => Ok(Processing::Label(LabelConfig::default())),
276 other => Err(E::unknown_variant(
277 other,
278 &["author-date", "numeric", "note", "label"],
279 )),
280 }
281 }
282
283 fn visit_enum<A: de::EnumAccess<'de>>(self, data: A) -> Result<Processing, A::Error> {
284 use serde::de::VariantAccess;
285 let (variant, access) = data.variant::<String>()?;
286 match variant.as_str() {
287 "custom" => {
288 let custom: ProcessingCustom = access.newtype_variant()?;
289 Ok(Processing::Custom(custom))
290 }
291 other => Err(de::Error::unknown_variant(
292 other,
293 &["author-date", "numeric", "note", "label", "custom"],
294 )),
295 }
296 }
297
298 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Processing, A::Error> {
299 let key: String = map
300 .next_key()?
301 .ok_or_else(|| de::Error::invalid_length(0, &"1"))?;
302 match key.as_str() {
303 "label" => {
304 let config: LabelConfig = map.next_value()?;
305 Ok(Processing::Label(config))
306 }
307 "sort" | "group" | "disambiguate" => {
308 let mut sort = None;
313 let mut group = None;
314 let mut disambiguate = None;
315
316 match key.as_str() {
318 "sort" => sort = Some(map.next_value()?),
319 "group" => group = Some(map.next_value()?),
320 "disambiguate" => disambiguate = Some(map.next_value()?),
321 _ => {
322 return Err(de::Error::unknown_field(
323 &key,
324 &["sort", "group", "disambiguate"],
325 ));
326 }
327 }
328
329 while let Some(k) = map.next_key::<String>()? {
331 match k.as_str() {
332 "sort" => sort = Some(map.next_value()?),
333 "group" => group = Some(map.next_value()?),
334 "disambiguate" => disambiguate = Some(map.next_value()?),
335 other => {
336 return Err(de::Error::unknown_field(
337 other,
338 &["sort", "group", "disambiguate"],
339 ));
340 }
341 }
342 }
343
344 Ok(Processing::Custom(ProcessingCustom {
345 sort,
346 group,
347 disambiguate,
348 }))
349 }
350 other => Err(de::Error::unknown_field(
351 other,
352 &["label", "sort", "group", "disambiguate"],
353 )),
354 }
355 }
356 }
357
358 deserializer.deserialize_any(ProcessingVisitor)
359 }
360}
361
362#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
366#[cfg_attr(feature = "schema", derive(JsonSchema))]
367#[serde(rename_all = "kebab-case")]
368pub struct Disambiguation {
369 pub names: bool,
371 #[serde(default)]
373 pub add_givenname: bool,
374 pub year_suffix: bool,
376}
377
378impl Default for Disambiguation {
379 fn default() -> Self {
380 Self {
381 names: true,
382 add_givenname: false,
383 year_suffix: false,
384 }
385 }
386}
387
388#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
392#[cfg_attr(feature = "schema", derive(JsonSchema))]
393#[serde(rename_all = "kebab-case")]
394pub struct Sort {
395 #[serde(default)]
397 pub shorten_names: bool,
398 #[serde(default)]
400 pub render_substitutions: bool,
401 pub template: Vec<SortSpec>,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
409#[cfg_attr(feature = "schema", derive(JsonSchema))]
410#[serde(untagged)]
411pub enum SortEntry {
412 Preset(crate::presets::SortPreset),
414 Explicit(Sort),
416}
417
418impl SortEntry {
419 pub fn resolve(&self) -> Sort {
423 match self {
424 SortEntry::Preset(preset) => preset.sort(),
425 SortEntry::Explicit(sort) => sort.clone(),
426 }
427 }
428}
429
430#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
434#[cfg_attr(feature = "schema", derive(JsonSchema))]
435#[serde(rename_all = "kebab-case")]
436pub struct SortSpec {
437 pub key: SortKey,
439 #[serde(default = "default_ascending")]
441 pub ascending: bool,
442}
443
444fn default_ascending() -> bool {
445 true
446}
447
448#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
452#[cfg_attr(feature = "schema", derive(JsonSchema))]
453#[serde(rename_all = "kebab-case")]
454#[non_exhaustive]
455pub enum SortKey {
456 #[default]
458 Author,
459 Year,
461 Title,
463 CitationNumber,
465}
466
467#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
471#[cfg_attr(feature = "schema", derive(JsonSchema))]
472#[serde(rename_all = "kebab-case")]
473pub struct Group {
474 pub template: Vec<SortKey>,
476}
477
478#[cfg(test)]
479#[allow(
480 clippy::unwrap_used,
481 clippy::expect_used,
482 clippy::panic,
483 clippy::indexing_slicing,
484 clippy::todo,
485 clippy::unimplemented,
486 clippy::unreachable,
487 clippy::get_unwrap,
488 reason = "Panicking is acceptable and often desired in tests."
489)]
490mod tests {
491 use super::*;
492
493 #[test]
495 fn test_label_config_alpha_preset_defaults() {
496 let config = LabelConfig {
497 preset: LabelPreset::Alpha,
498 single_author_chars: None,
499 multi_author_chars: None,
500 et_al_min: None,
501 et_al_marker: None,
502 et_al_names: None,
503 year_digits: None,
504 };
505
506 let params = config.effective_params();
507 assert_eq!(params.single_author_chars, 3);
508 assert_eq!(params.multi_author_chars, 1);
509 assert_eq!(params.et_al_min, 4);
510 assert_eq!(params.et_al_marker, "+");
511 assert_eq!(params.et_al_names, 3);
512 assert_eq!(params.year_digits, 2);
513 }
514
515 #[test]
517 fn test_label_config_alpha_with_overrides() {
518 let config = LabelConfig {
519 preset: LabelPreset::Alpha,
520 single_author_chars: Some(5),
521 multi_author_chars: Some(2),
522 et_al_min: Some(5),
523 et_al_marker: Some("*".to_string()),
524 et_al_names: Some(4),
525 year_digits: Some(4),
526 };
527
528 let params = config.effective_params();
529 assert_eq!(params.single_author_chars, 5);
530 assert_eq!(params.multi_author_chars, 2);
531 assert_eq!(params.et_al_min, 5);
532 assert_eq!(params.et_al_marker, "*");
533 assert_eq!(params.et_al_names, 4);
534 assert_eq!(params.year_digits, 4);
535 }
536
537 #[test]
539 fn test_label_config_din_preset_defaults() {
540 let config = LabelConfig {
541 preset: LabelPreset::Din,
542 single_author_chars: None,
543 multi_author_chars: None,
544 et_al_min: None,
545 et_al_marker: None,
546 et_al_names: None,
547 year_digits: None,
548 };
549
550 let params = config.effective_params();
551 assert_eq!(params.single_author_chars, 4);
552 assert_eq!(params.multi_author_chars, 1);
553 assert_eq!(params.et_al_min, 3);
554 assert_eq!(params.et_al_marker, "");
555 assert_eq!(params.et_al_names, 3);
556 assert_eq!(params.year_digits, 2);
557 }
558
559 #[test]
561 fn test_processing_author_date_default_bibliography_sort() {
562 let processing = Processing::AuthorDate;
563 let sort = processing.default_bibliography_sort();
564 assert_eq!(sort, Some(SortPreset::AuthorDateTitle));
565 }
566
567 #[test]
569 fn test_processing_numeric_default_bibliography_sort() {
570 let processing = Processing::Numeric;
571 let sort = processing.default_bibliography_sort();
572 assert_eq!(sort, None);
573 }
574
575 #[test]
577 fn test_processing_note_default_bibliography_sort() {
578 let processing = Processing::Note;
579 let sort = processing.default_bibliography_sort();
580 assert_eq!(sort, Some(SortPreset::AuthorTitleDate));
581 }
582
583 #[test]
585 fn test_processing_citation_sort_policy() {
586 let modes = vec![
587 Processing::AuthorDate,
588 Processing::Numeric,
589 Processing::Note,
590 Processing::Label(LabelConfig::default()),
591 Processing::Custom(ProcessingCustom::default()),
592 ];
593
594 for mode in modes {
595 assert_eq!(
596 mode.default_citation_sort_policy(),
597 CitationSortPolicy::ExplicitOnly
598 );
599 }
600 }
601
602 #[test]
604 fn test_processing_author_date_config() {
605 let processing = Processing::AuthorDate;
606 let config = processing.config();
607
608 assert!(config.sort.is_some());
609 assert!(config.group.is_some());
610 assert!(config.disambiguate.is_some());
611
612 let disambig = config.disambiguate.unwrap();
613 assert!(disambig.names);
614 assert!(disambig.add_givenname);
615 assert!(disambig.year_suffix);
616 }
617
618 #[test]
620 fn test_disambiguation_defaults() {
621 let disambig = Disambiguation::default();
622 assert!(disambig.names);
623 assert!(!disambig.add_givenname);
624 assert!(!disambig.year_suffix);
625 }
626
627 #[test]
629 fn test_sort_entry_resolve_preset() {
630 let entry = SortEntry::Preset(SortPreset::AuthorDateTitle);
631 let sort = entry.resolve();
632
633 assert!(!sort.template.is_empty());
635 }
636
637 #[test]
639 fn test_sort_entry_resolve_explicit() {
640 let explicit = Sort {
641 shorten_names: true,
642 render_substitutions: false,
643 template: vec![SortSpec {
644 key: SortKey::Title,
645 ascending: false,
646 }],
647 };
648 let entry = SortEntry::Explicit(explicit.clone());
649 let resolved = entry.resolve();
650
651 assert!(resolved.shorten_names);
652 assert!(!resolved.render_substitutions);
653 assert_eq!(resolved.template.len(), 1);
654 assert_eq!(resolved.template[0].key, SortKey::Title);
655 assert!(!resolved.template[0].ascending);
656 }
657}