1pub mod bibliography;
9pub mod contributors;
10pub mod dates;
11pub mod integral_name_memory;
12pub mod localization;
13pub mod locators;
14pub mod multilingual;
15pub mod processing;
16pub mod scoped;
17pub mod substitute;
18
19pub use crate::presets::{MultilingualConfigEntry, MultilingualPreset};
20pub use bibliography::{
21 AnonymousEntriesMode, ArticleJournalBibliographyConfig, ArticleJournalNoPageFallback,
22 BibliographyConfig, BibliographyPartitionHeading, BibliographyPartitionKind,
23 BibliographyPartitionMode, BibliographySortPartitioning, SubsequentAuthorSubstituteRule,
24};
25pub use contributors::{
26 AndOptions, AndOtherOptions, ContributorConfig, ContributorConfigEntry, DelimiterPrecedesLast,
27 DemoteNonDroppingParticle, DisplayAsSort, NameForm, RoleLabelPreset, RoleOptions,
28 RoleOptionsEntry, RoleRendering, ShortenListOptions,
29};
30pub use dates::{DateConfig, DateConfigEntry, NoDateForm};
31pub use integral_name_memory::{
32 IntegralNameContexts, IntegralNameMemoryConfig, IntegralNameScope, OrgAbbreviationMemoryConfig,
33 ResolvedIntegralNameMemoryConfig, ResolvedOrgAbbreviationMemoryConfig, ShortNameDisplay,
34 SubsequentNameForm,
35};
36pub use localization::{Localize, MonthFormat, Scope};
37pub use locators::{
38 LabelForm, LabelRepeat, LocatorConfig, LocatorConfigEntry, LocatorKindConfig, LocatorPattern,
39 LocatorPreset, TypeClass,
40};
41pub use multilingual::{
42 MultilingualConfig, MultilingualMode, MultilingualSegment, MultilingualView, ScriptConfig,
43 SegmentWrap,
44};
45pub use processing::{
46 CitationSortPolicy, Disambiguation, GivennameRule, Group, LabelConfig, LabelParams,
47 LabelPreset, Processing, ProcessingCustom, RegimeFamily, Sort, SortEntry, SortKey, SortSpec,
48};
49pub use scoped::{
50 BibliographyLabelMode, BibliographyLabelWrap, CitationGroupDelimiter, DatePosition, LabelWrap,
51 RepeatedAuthorRendering, TitleTerminator,
52};
53pub use substitute::{Substitute, SubstituteConfig, SubstituteKey};
54
55use crate::template::DelimiterPunctuation;
56#[cfg(feature = "schema")]
57use schemars::JsonSchema;
58use serde::{Deserialize, Serialize};
59use std::collections::HashMap;
60
61#[derive(Debug, Default, PartialEq, Clone, Serialize)]
63#[cfg_attr(feature = "schema", derive(JsonSchema))]
64#[serde(rename_all = "kebab-case")]
65pub struct Config {
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub substitute: Option<SubstituteConfig>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub processing: Option<Processing>,
72 #[serde(skip_serializing_if = "Option::is_none")]
78 pub locale_override: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub localize: Option<Localize>,
82 #[serde(
85 skip_serializing_if = "Option::is_none",
86 deserialize_with = "deserialize_multilingual_config",
87 default
88 )]
89 #[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
90 pub multilingual: Option<MultilingualConfig>,
91 #[serde(
94 skip_serializing_if = "Option::is_none",
95 deserialize_with = "deserialize_contributor_config",
96 default
97 )]
98 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
99 pub contributors: Option<ContributorConfig>,
100 #[serde(
103 skip_serializing_if = "Option::is_none",
104 deserialize_with = "deserialize_date_config",
105 default
106 )]
107 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
108 pub dates: Option<DateConfig>,
109 #[serde(
112 skip_serializing_if = "Option::is_none",
113 deserialize_with = "deserialize_titles_config",
114 default
115 )]
116 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
117 pub titles: Option<crate::options::titles::TitlesConfig>,
118 #[serde(
121 skip_serializing_if = "Option::is_none",
122 deserialize_with = "deserialize_locator_config",
123 default
124 )]
125 #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
126 pub locators: Option<LocatorConfig>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub page_range_format: Option<PageRangeFormat>,
130 #[serde(skip_serializing_if = "Option::is_none")]
133 pub page_range_delimiter: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub links: Option<LinksConfig>,
137 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
141 pub punctuation_in_quote: bool,
142 #[serde(skip_serializing_if = "Option::is_none")]
146 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
147 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
149 pub strip_periods: Option<bool>,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub notes: Option<NoteConfig>,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub integral_name_memory: Option<IntegralNameMemoryConfig>,
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub custom: Option<HashMap<String, serde_json::Value>>,
162 #[serde(
166 flatten,
167 default,
168 skip_serializing_if = "std::collections::BTreeMap::is_empty"
169 )]
170 #[cfg_attr(feature = "schema", schemars(skip))]
171 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
172}
173
174#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
176#[cfg_attr(feature = "schema", derive(JsonSchema))]
177#[serde(rename_all = "kebab-case")]
178pub struct CitationOptions {
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub substitute: Option<SubstituteConfig>,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub processing: Option<Processing>,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub localize: Option<Localize>,
188 #[serde(
191 skip_serializing_if = "Option::is_none",
192 deserialize_with = "deserialize_multilingual_config",
193 default
194 )]
195 #[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
196 pub multilingual: Option<MultilingualConfig>,
197 #[serde(
199 skip_serializing_if = "Option::is_none",
200 deserialize_with = "deserialize_contributor_config",
201 default
202 )]
203 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
204 pub contributors: Option<ContributorConfig>,
205 #[serde(
207 skip_serializing_if = "Option::is_none",
208 deserialize_with = "deserialize_date_config",
209 default
210 )]
211 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
212 pub dates: Option<DateConfig>,
213 #[serde(
215 skip_serializing_if = "Option::is_none",
216 deserialize_with = "deserialize_titles_config",
217 default
218 )]
219 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
220 pub titles: Option<crate::options::titles::TitlesConfig>,
221 #[serde(
223 skip_serializing_if = "Option::is_none",
224 deserialize_with = "deserialize_locator_config",
225 default
226 )]
227 #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
228 pub locators: Option<LocatorConfig>,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub page_range_format: Option<PageRangeFormat>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub links: Option<LinksConfig>,
235 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
237 pub punctuation_in_quote: bool,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
241 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
243 pub strip_periods: Option<bool>,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub notes: Option<NoteConfig>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub integral_name_memory: Option<IntegralNameMemoryConfig>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub label_wrap: Option<LabelWrap>,
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub group_delimiter: Option<CitationGroupDelimiter>,
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub custom: Option<HashMap<String, serde_json::Value>>,
262 #[serde(
266 flatten,
267 default,
268 skip_serializing_if = "std::collections::BTreeMap::is_empty"
269 )]
270 #[cfg_attr(feature = "schema", schemars(skip))]
271 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
272}
273
274#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
276#[cfg_attr(feature = "schema", derive(JsonSchema))]
277#[serde(rename_all = "kebab-case")]
278pub struct BibliographyOptions {
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub substitute: Option<SubstituteConfig>,
282 #[serde(skip_serializing_if = "Option::is_none")]
284 pub processing: Option<Processing>,
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub localize: Option<Localize>,
288 #[serde(
291 skip_serializing_if = "Option::is_none",
292 deserialize_with = "deserialize_multilingual_config",
293 default
294 )]
295 #[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
296 pub multilingual: Option<MultilingualConfig>,
297 #[serde(
299 skip_serializing_if = "Option::is_none",
300 deserialize_with = "deserialize_contributor_config",
301 default
302 )]
303 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
304 pub contributors: Option<ContributorConfig>,
305 #[serde(
307 skip_serializing_if = "Option::is_none",
308 deserialize_with = "deserialize_date_config",
309 default
310 )]
311 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
312 pub dates: Option<DateConfig>,
313 #[serde(
315 skip_serializing_if = "Option::is_none",
316 deserialize_with = "deserialize_titles_config",
317 default
318 )]
319 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
320 pub titles: Option<crate::options::titles::TitlesConfig>,
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub page_range_format: Option<PageRangeFormat>,
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub article_journal: Option<ArticleJournalBibliographyConfig>,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub subsequent_author_substitute: Option<String>,
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub subsequent_author_substitute_rule: Option<SubsequentAuthorSubstituteRule>,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub hanging_indent: Option<bool>,
336 #[serde(skip_serializing_if = "Option::is_none")]
338 pub entry_suffix: Option<String>,
339 #[serde(skip_serializing_if = "Option::is_none")]
341 pub separator: Option<String>,
342 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
344 pub suppress_period_after_url: bool,
345 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
347 pub entry_suffix_after_url: bool,
348 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
350 pub entry_suffix_after_doi: bool,
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub compound_numeric: Option<bibliography::CompoundNumericConfig>,
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub sort_partitioning: Option<bibliography::BibliographySortPartitioning>,
357 #[serde(skip_serializing_if = "Option::is_none")]
361 pub anonymous_entries: Option<AnonymousEntriesMode>,
362 #[serde(skip_serializing_if = "Option::is_none")]
364 pub links: Option<LinksConfig>,
365 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
367 pub punctuation_in_quote: bool,
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub label_mode: Option<BibliographyLabelMode>,
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub label_wrap: Option<BibliographyLabelWrap>,
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub date_position: Option<DatePosition>,
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub title_terminator: Option<TitleTerminator>,
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub repeated_author_rendering: Option<RepeatedAuthorRendering>,
386 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
388 pub strip_periods: Option<bool>,
389 #[serde(skip_serializing_if = "Option::is_none")]
391 pub custom: Option<HashMap<String, serde_json::Value>>,
392 #[serde(
396 flatten,
397 default,
398 skip_serializing_if = "std::collections::BTreeMap::is_empty"
399 )]
400 #[cfg_attr(feature = "schema", schemars(skip))]
401 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
402}
403
404#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
406#[cfg_attr(feature = "schema", derive(JsonSchema))]
407#[serde(rename_all = "kebab-case")]
408pub struct NoteConfig {
409 #[serde(skip_serializing_if = "Option::is_none")]
412 pub punctuation: Option<NoteQuotePlacement>,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub number: Option<NoteNumberPlacement>,
416 #[serde(skip_serializing_if = "Option::is_none")]
419 pub order: Option<NoteMarkerOrder>,
420 #[serde(
424 flatten,
425 default,
426 skip_serializing_if = "std::collections::BTreeMap::is_empty"
427 )]
428 #[cfg_attr(feature = "schema", schemars(skip))]
429 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
430}
431
432#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
434#[cfg_attr(feature = "schema", derive(JsonSchema))]
435#[serde(rename_all = "kebab-case")]
436pub enum NoteQuotePlacement {
437 Inside,
439 Outside,
441 Adaptive,
445}
446
447#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
449#[cfg_attr(feature = "schema", derive(JsonSchema))]
450#[serde(rename_all = "kebab-case")]
451pub enum NoteNumberPlacement {
452 Inside,
454 Outside,
456 Same,
459}
460
461#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
463#[cfg_attr(feature = "schema", derive(JsonSchema))]
464#[serde(rename_all = "kebab-case")]
465pub enum NoteMarkerOrder {
466 Before,
468 After,
470}
471
472#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
474#[cfg_attr(feature = "schema", derive(JsonSchema))]
475#[serde(rename_all = "kebab-case")]
476#[non_exhaustive]
477pub enum PageRangeFormat {
478 #[default]
480 Expanded,
481 Minimal,
483 MinimalTwo,
485 Chicago,
487 Chicago16,
489}
490
491pub mod titles;
492
493pub use titles::{TextCase, TitleRendering, TitlesConfig, TitlesConfigEntry};
494
495#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
497#[cfg_attr(feature = "schema", derive(JsonSchema))]
498#[serde(rename_all = "kebab-case")]
499pub struct LinksConfig {
500 #[serde(skip_serializing_if = "Option::is_none")]
502 pub doi: Option<bool>,
503 #[serde(skip_serializing_if = "Option::is_none")]
505 pub url: Option<bool>,
506 #[serde(skip_serializing_if = "Option::is_none")]
508 pub target: Option<LinkTarget>,
509 #[serde(skip_serializing_if = "Option::is_none")]
511 pub anchor: Option<LinkAnchor>,
512}
513
514#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
516#[cfg_attr(feature = "schema", derive(JsonSchema))]
517#[serde(rename_all = "kebab-case")]
518pub enum LinkTarget {
519 Url,
520 Doi,
521 UrlOrDoi,
522 Pubmed,
523 Pmcid,
524}
525
526#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
528#[cfg_attr(feature = "schema", derive(JsonSchema))]
529#[serde(rename_all = "kebab-case")]
530pub enum LinkAnchor {
531 Title,
533 Url,
535 Doi,
537 Component,
539 Entry,
541}
542
543impl Config {
544 pub fn effective_processing(&self) -> Processing {
549 self.processing.clone().unwrap_or_default()
550 }
551
552 pub fn merge(&mut self, other: &Config) {
557 crate::merge_options!(
558 self,
559 other,
560 processing,
561 locale_override,
562 localize,
563 multilingual,
564 dates,
565 titles,
566 locators,
567 page_range_format,
568 page_range_delimiter,
569 links,
570 volume_pages_delimiter,
571 locale_override,
572 strip_periods,
573 notes,
574 integral_name_memory,
575 org_abbreviation_memory,
576 custom,
577 );
578
579 if let Some(other_substitute) = &other.substitute {
580 if let Some(this_substitute) = &self.substitute {
581 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
582 } else {
583 self.substitute = Some(other_substitute.clone());
584 }
585 }
586
587 if let Some(other_contributors) = &other.contributors {
588 if let Some(this_contributors) = &mut self.contributors {
589 this_contributors.merge(other_contributors);
590 } else {
591 self.contributors = Some(other_contributors.clone());
592 }
593 }
594
595 if other.punctuation_in_quote {
596 self.punctuation_in_quote = true;
597 }
598 }
599
600 pub fn merged(base: &Config, override_config: &Config) -> Config {
604 let mut result = base.clone();
605 result.merge(override_config);
606 result
607 }
608}
609
610impl CitationOptions {
611 #[must_use]
613 pub fn to_config(&self) -> Config {
614 Config {
615 substitute: self.substitute.clone(),
616 processing: self.processing.clone(),
617 locale_override: None,
618 localize: self.localize.clone(),
619 multilingual: self.multilingual.clone(),
620 contributors: self.contributors.clone(),
621 dates: self.dates.clone(),
622 titles: self.titles.clone(),
623 locators: self.locators.clone(),
624 page_range_format: self.page_range_format.clone(),
625 page_range_delimiter: None,
626 links: self.links.clone(),
627 punctuation_in_quote: self.punctuation_in_quote,
628 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
629 strip_periods: self.strip_periods,
630 notes: self.notes.clone(),
631 integral_name_memory: self.integral_name_memory.clone(),
632 org_abbreviation_memory: self.org_abbreviation_memory.clone(),
633 custom: self.custom.clone(),
634 unknown_fields: std::collections::BTreeMap::new(),
635 }
636 }
637
638 #[must_use]
640 pub fn merged_with(&self, base: &Config) -> Config {
641 Config::merged(base, &self.to_config())
642 }
643
644 pub fn merge(&mut self, other: &CitationOptions) {
646 crate::merge_options!(
647 self,
648 other,
649 processing,
650 localize,
651 multilingual,
652 dates,
653 titles,
654 locators,
655 page_range_format,
656 links,
657 volume_pages_delimiter,
658 strip_periods,
659 notes,
660 integral_name_memory,
661 org_abbreviation_memory,
662 label_wrap,
663 group_delimiter,
664 custom,
665 );
666
667 if let Some(other_substitute) = &other.substitute {
668 if let Some(this_substitute) = &self.substitute {
669 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
670 } else {
671 self.substitute = Some(other_substitute.clone());
672 }
673 }
674
675 if let Some(other_contributors) = &other.contributors {
676 if let Some(this_contributors) = &mut self.contributors {
677 this_contributors.merge(other_contributors);
678 } else {
679 self.contributors = Some(other_contributors.clone());
680 }
681 }
682
683 if other.punctuation_in_quote {
684 self.punctuation_in_quote = true;
685 }
686 }
687}
688
689impl BibliographyOptions {
690 #[must_use]
692 pub fn to_bibliography_config(&self) -> BibliographyConfig {
693 BibliographyConfig {
694 article_journal: self.article_journal.clone(),
695 subsequent_author_substitute: self.subsequent_author_substitute.clone(),
696 subsequent_author_substitute_rule: self.subsequent_author_substitute_rule.clone(),
697 hanging_indent: self.hanging_indent,
698 entry_suffix: self.entry_suffix.clone(),
699 separator: self.separator.clone(),
700 suppress_period_after_url: self.suppress_period_after_url,
701 entry_suffix_after_url: self.entry_suffix_after_url,
702 entry_suffix_after_doi: self.entry_suffix_after_doi,
703 custom: None,
704 compound_numeric: self.compound_numeric.clone(),
705 sort_partitioning: self.sort_partitioning.clone(),
706 anonymous_entries: self.anonymous_entries,
707 unknown_fields: std::collections::BTreeMap::new(),
708 }
709 }
710
711 #[must_use]
713 pub fn to_config(&self) -> Config {
714 Config {
715 substitute: self.substitute.clone(),
716 processing: self.processing.clone(),
717 locale_override: None,
718 localize: self.localize.clone(),
719 multilingual: self.multilingual.clone(),
720 contributors: self.contributors.clone(),
721 dates: self.dates.clone(),
722 titles: self.titles.clone(),
723 locators: None,
724 page_range_format: self.page_range_format.clone(),
725 page_range_delimiter: None,
726 links: self.links.clone(),
727 punctuation_in_quote: self.punctuation_in_quote,
728 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
729 strip_periods: self.strip_periods,
730 notes: None,
731 integral_name_memory: None,
732 org_abbreviation_memory: None,
733 custom: self.custom.clone(),
734 unknown_fields: std::collections::BTreeMap::new(),
735 }
736 }
737
738 #[must_use]
740 pub fn merged_with(&self, base: &Config) -> Config {
741 Config::merged(base, &self.to_config())
742 }
743
744 pub fn merge(&mut self, other: &BibliographyOptions) {
746 crate::merge_options!(
747 self,
748 other,
749 processing,
750 localize,
751 multilingual,
752 dates,
753 titles,
754 page_range_format,
755 links,
756 volume_pages_delimiter,
757 strip_periods,
758 article_journal,
759 subsequent_author_substitute,
760 subsequent_author_substitute_rule,
761 hanging_indent,
762 entry_suffix,
763 separator,
764 compound_numeric,
765 sort_partitioning,
766 anonymous_entries,
767 label_mode,
768 label_wrap,
769 date_position,
770 title_terminator,
771 repeated_author_rendering,
772 custom,
773 );
774
775 self.merge_shared_fields(other);
776 }
777
778 fn merge_shared_fields(&mut self, other: &BibliographyOptions) {
779 if let Some(other_substitute) = &other.substitute {
780 if let Some(this_substitute) = &self.substitute {
781 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
782 } else {
783 self.substitute = Some(other_substitute.clone());
784 }
785 }
786
787 if let Some(other_contributors) = &other.contributors {
788 if let Some(this_contributors) = &mut self.contributors {
789 this_contributors.merge(other_contributors);
790 } else {
791 self.contributors = Some(other_contributors.clone());
792 }
793 }
794
795 if other.punctuation_in_quote {
796 self.punctuation_in_quote = true;
797 }
798 if other.suppress_period_after_url {
799 self.suppress_period_after_url = true;
800 }
801 if other.entry_suffix_after_url {
802 self.entry_suffix_after_url = true;
803 }
804 if other.entry_suffix_after_doi {
805 self.entry_suffix_after_doi = true;
806 }
807
808 for (key, value) in &other.unknown_fields {
809 self.unknown_fields.insert(key.clone(), value.clone());
810 }
811 }
812}
813
814fn deserialize_contributor_config<'de, D>(
816 deserializer: D,
817) -> Result<Option<ContributorConfig>, D::Error>
818where
819 D: serde::Deserializer<'de>,
820{
821 let value: Option<ContributorConfigEntry> = Option::deserialize(deserializer)?;
822 Ok(value.map(|entry| entry.resolve()))
823}
824
825fn deserialize_date_config<'de, D>(deserializer: D) -> Result<Option<DateConfig>, D::Error>
827where
828 D: serde::Deserializer<'de>,
829{
830 let value: Option<DateConfigEntry> = Option::deserialize(deserializer)?;
831 Ok(value.map(|entry| entry.resolve()))
832}
833
834fn deserialize_titles_config<'de, D>(
836 deserializer: D,
837) -> Result<Option<crate::options::titles::TitlesConfig>, D::Error>
838where
839 D: serde::Deserializer<'de>,
840{
841 let value: Option<crate::options::titles::TitlesConfigEntry> =
842 Option::deserialize(deserializer)?;
843 Ok(value.map(|entry| entry.resolve()))
844}
845
846fn deserialize_locator_config<'de, D>(deserializer: D) -> Result<Option<LocatorConfig>, D::Error>
848where
849 D: serde::Deserializer<'de>,
850{
851 let value: Option<LocatorConfigEntry> = Option::deserialize(deserializer)?;
852 Ok(value.map(|entry| entry.resolve()))
853}
854
855fn deserialize_multilingual_config<'de, D>(
857 deserializer: D,
858) -> Result<Option<MultilingualConfig>, D::Error>
859where
860 D: serde::Deserializer<'de>,
861{
862 let value: Option<crate::presets::MultilingualConfigEntry> = Option::deserialize(deserializer)?;
863 Ok(value.map(|entry| entry.resolve()))
864}
865
866impl<'de> Deserialize<'de> for Config {
867 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
868 where
869 D: serde::Deserializer<'de>,
870 {
871 #[derive(Deserialize)]
872 #[serde(rename_all = "kebab-case")]
873 struct ConfigWire {
874 #[serde(skip_serializing_if = "Option::is_none")]
875 substitute: Option<SubstituteConfig>,
876 #[serde(skip_serializing_if = "Option::is_none")]
877 processing: Option<Processing>,
878 #[serde(skip_serializing_if = "Option::is_none")]
879 locale_override: Option<String>,
880 #[serde(skip_serializing_if = "Option::is_none")]
881 localize: Option<Localize>,
882 #[serde(
883 skip_serializing_if = "Option::is_none",
884 deserialize_with = "deserialize_multilingual_config",
885 default
886 )]
887 multilingual: Option<MultilingualConfig>,
888 #[serde(
889 skip_serializing_if = "Option::is_none",
890 deserialize_with = "deserialize_contributor_config",
891 default
892 )]
893 contributors: Option<ContributorConfig>,
894 #[serde(
895 skip_serializing_if = "Option::is_none",
896 deserialize_with = "deserialize_date_config",
897 default
898 )]
899 dates: Option<DateConfig>,
900 #[serde(
901 skip_serializing_if = "Option::is_none",
902 deserialize_with = "deserialize_titles_config",
903 default
904 )]
905 titles: Option<crate::options::titles::TitlesConfig>,
906 #[serde(
907 skip_serializing_if = "Option::is_none",
908 deserialize_with = "deserialize_locator_config",
909 default
910 )]
911 locators: Option<LocatorConfig>,
912 #[serde(skip_serializing_if = "Option::is_none")]
913 page_range_format: Option<PageRangeFormat>,
914 #[serde(skip_serializing_if = "Option::is_none")]
915 page_range_delimiter: Option<String>,
916 #[serde(skip_serializing_if = "Option::is_none")]
917 links: Option<LinksConfig>,
918 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
919 punctuation_in_quote: bool,
920 #[serde(skip_serializing_if = "Option::is_none")]
921 volume_pages_delimiter: Option<DelimiterPunctuation>,
922 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
923 strip_periods: Option<bool>,
924 #[serde(skip_serializing_if = "Option::is_none")]
925 notes: Option<NoteConfig>,
926 #[serde(skip_serializing_if = "Option::is_none")]
927 integral_name_memory: Option<IntegralNameMemoryConfig>,
928 #[serde(skip_serializing_if = "Option::is_none")]
929 org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
930 #[serde(default)]
931 profile: Option<serde_yaml::Value>,
932 #[serde(skip_serializing_if = "Option::is_none")]
933 custom: Option<HashMap<String, serde_json::Value>>,
934 #[serde(flatten)]
935 unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
936 }
937
938 let wire = ConfigWire::deserialize(deserializer)?;
939 if wire.profile.is_some() {
940 return Err(serde::de::Error::custom(
941 "`options.profile` was removed; use `options.contributors`, `citation.options.label-wrap`, `citation.options.group-delimiter`, `bibliography.options.label-mode`, `bibliography.options.label-wrap`, `bibliography.options.date-position`, `bibliography.options.title-terminator`, `bibliography.options.repeated-author-rendering`, or `bibliography.options.volume-pages-delimiter`",
942 ));
943 }
944
945 Ok(Self {
946 substitute: wire.substitute,
947 processing: wire.processing,
948 locale_override: wire.locale_override,
949 localize: wire.localize,
950 multilingual: wire.multilingual,
951 contributors: wire.contributors,
952 dates: wire.dates,
953 titles: wire.titles,
954 locators: wire.locators,
955 page_range_format: wire.page_range_format,
956 page_range_delimiter: wire.page_range_delimiter,
957 links: wire.links,
958 punctuation_in_quote: wire.punctuation_in_quote,
959 volume_pages_delimiter: wire.volume_pages_delimiter,
960 strip_periods: wire.strip_periods,
961 notes: wire.notes,
962 integral_name_memory: wire.integral_name_memory,
963 org_abbreviation_memory: wire.org_abbreviation_memory,
964 custom: wire.custom,
965 unknown_fields: wire.unknown_fields,
966 })
967 }
968}
969
970#[cfg(test)]
971#[allow(
972 clippy::unwrap_used,
973 clippy::expect_used,
974 clippy::panic,
975 clippy::indexing_slicing,
976 clippy::todo,
977 clippy::unimplemented,
978 clippy::unreachable,
979 clippy::get_unwrap,
980 reason = "Panicking is acceptable and often desired in tests."
981)]
982mod tests {
983 use super::*;
984
985 #[test]
986 fn test_config_default() {
987 let config = Config::default();
988 assert!(config.substitute.is_none());
989 assert!(config.processing.is_none());
990 }
991
992 #[test]
993 fn test_author_date_processing() {
994 let processing = Processing::AuthorDate;
995 let config = processing.config();
996 let disambiguate = config.disambiguate.unwrap();
997 assert!(disambiguate.year_suffix);
998 assert!(!disambiguate.names);
999 assert!(!disambiguate.add_givenname);
1000 assert_eq!(
1001 processing.default_bibliography_sort(),
1002 Some(crate::presets::SortPreset::AuthorDateTitle)
1003 );
1004 assert_eq!(
1005 config.sort,
1006 Some(SortEntry::Preset(
1007 crate::presets::SortPreset::AuthorDateTitle
1008 ))
1009 );
1010 }
1011
1012 #[test]
1013 fn test_processing_default_bibliography_sorts() {
1014 assert_eq!(Processing::Numeric.default_bibliography_sort(), None);
1015 assert_eq!(
1016 Processing::Note.default_bibliography_sort(),
1017 Some(crate::presets::SortPreset::AuthorTitleDate)
1018 );
1019 assert_eq!(
1020 Processing::Label(LabelConfig::default()).default_bibliography_sort(),
1021 Some(crate::presets::SortPreset::AuthorDateTitle)
1022 );
1023 }
1024
1025 #[test]
1026 fn test_processing_default_citation_sort_policy_is_explicit_only() {
1027 assert_eq!(
1028 Processing::AuthorDate.default_citation_sort_policy(),
1029 CitationSortPolicy::ExplicitOnly
1030 );
1031 assert_eq!(
1032 Processing::Note.default_citation_sort_policy(),
1033 CitationSortPolicy::ExplicitOnly
1034 );
1035 }
1036
1037 #[test]
1038 fn test_substitute_default() {
1039 let sub = Substitute::default();
1040 assert_eq!(sub.template.len(), 3);
1041 }
1042
1043 #[test]
1044 fn test_config_yaml_roundtrip() {
1045 let yaml = r#"
1046substitute:
1047 contributor-role-form: short
1048 template:
1049 - editor
1050 - title
1051processing: author-date
1052contributors:
1053 display-as-sort: first
1054 and: symbol
1055"#;
1056 let config: Config = serde_yaml::from_str(yaml).unwrap();
1057 assert!(config.substitute.is_some());
1058 assert_eq!(config.processing, Some(Processing::AuthorDate));
1059 assert_eq!(
1060 config.contributors.as_ref().unwrap().and,
1061 Some(AndOptions::Symbol)
1062 );
1063 }
1064
1065 #[test]
1066 fn test_contributor_config_preset() {
1067 let yaml = r#"contributors: apa"#;
1069 let config: Config = serde_yaml::from_str(yaml).unwrap();
1070 let contributors = config.contributors.unwrap();
1071 assert_eq!(contributors.and, Some(AndOptions::Symbol));
1072 assert_eq!(contributors.display_as_sort, Some(DisplayAsSort::First));
1073 }
1074
1075 #[test]
1076 fn test_role_label_presets_parse_and_resolve_precedence() {
1077 let yaml = r#"
1078contributors:
1079 role:
1080 preset: short-suffix
1081 roles:
1082 editor:
1083 preset: long-suffix
1084"#;
1085 let config: Config = serde_yaml::from_str(yaml).unwrap();
1086 let contributors = config.contributors.unwrap();
1087
1088 assert_eq!(
1089 contributors.effective_role_label_preset(&crate::template::ContributorRole::Editor),
1090 Some(RoleLabelPreset::LongSuffix)
1091 );
1092 assert_eq!(
1093 contributors.effective_role_label_preset(&crate::template::ContributorRole::Translator),
1094 Some(RoleLabelPreset::ShortSuffix)
1095 );
1096
1097 let yaml_scalar = r#"
1099contributors:
1100 role: short-suffix
1101"#;
1102 let config2: Config = serde_yaml::from_str(yaml_scalar).unwrap();
1103 let contributors2 = config2.contributors.unwrap();
1104
1105 assert_eq!(
1106 contributors2
1107 .effective_role_label_preset(&crate::template::ContributorRole::Translator),
1108 Some(RoleLabelPreset::ShortSuffix)
1109 );
1110 }
1111
1112 #[test]
1113 fn test_role_specific_name_order_override_is_available() {
1114 let yaml = r#"
1115contributors:
1116 role:
1117 roles:
1118 translator:
1119 name-order: given-first
1120"#;
1121 let config: Config = serde_yaml::from_str(yaml).unwrap();
1122 let contributors = config.contributors.unwrap();
1123
1124 assert_eq!(
1125 contributors.effective_role_name_order(&crate::template::ContributorRole::Translator),
1126 Some(&crate::template::NameOrder::GivenFirst)
1127 );
1128 }
1129
1130 #[test]
1131 fn test_date_config_preset() {
1132 let yaml = r#"dates: long"#;
1134 let config: Config = serde_yaml::from_str(yaml).unwrap();
1135 let dates = config.dates.unwrap();
1136 assert_eq!(dates.month, MonthFormat::Long);
1137 }
1138
1139 #[test]
1140 fn test_titles_config_preset() {
1141 let yaml = r#"titles: chicago"#;
1143 let config: Config = serde_yaml::from_str(yaml).unwrap();
1144 let titles = config.titles.unwrap();
1145 assert_eq!(titles.component.unwrap().quote, Some(true));
1146 assert_eq!(titles.monograph.unwrap().emph, Some(true));
1147 }
1148
1149 #[test]
1150 fn test_substitute_config_preset() {
1151 let yaml = r#"substitute: standard"#;
1153 let config: Config = serde_yaml::from_str(yaml).unwrap();
1154 assert!(config.substitute.is_some());
1155 let resolved = config.substitute.unwrap().resolve();
1156 assert_eq!(resolved.template.len(), 3);
1157 assert_eq!(resolved.template[0], SubstituteKey::Editor);
1158 }
1159
1160 #[test]
1161 fn test_substitute_config_explicit() {
1162 let yaml = r#"
1164substitute:
1165 template:
1166 - title
1167 - editor
1168"#;
1169 let config: Config = serde_yaml::from_str(yaml).unwrap();
1170 let resolved = config.substitute.unwrap().resolve();
1171 assert_eq!(resolved.template[0], SubstituteKey::Title);
1172 assert_eq!(resolved.template[1], SubstituteKey::Editor);
1173 }
1174
1175 #[test]
1176 fn test_config_merge_precedence() {
1177 let base_yaml = r#"
1179processing: author-date
1180locale-override: en-US-base
1181contributors:
1182 display-as-sort: first
1183 and: symbol
1184"#;
1185 let mut base: Config = serde_yaml::from_str(base_yaml).unwrap();
1186
1187 let override_yaml = r#"
1189contributors:
1190 and: text
1191locale-override: en-US-chicago
1192"#;
1193 let override_config: Config = serde_yaml::from_str(override_yaml).unwrap();
1194
1195 base.merge(&override_config);
1197
1198 assert_eq!(base.processing, Some(Processing::AuthorDate));
1200 assert_eq!(base.locale_override.as_deref(), Some("en-US-chicago"));
1201
1202 assert_eq!(
1204 base.contributors.as_ref().unwrap().and,
1205 Some(AndOptions::Text)
1206 );
1207 }
1208
1209 #[test]
1210 fn test_config_deserializes_locale_override() {
1211 let config: Config = serde_yaml::from_str("locale-override: en-US-chicago").unwrap();
1212 assert_eq!(config.locale_override.as_deref(), Some("en-US-chicago"));
1213 }
1214
1215 #[test]
1216 fn test_config_merged_convenience() {
1217 let base = Config {
1218 processing: Some(Processing::AuthorDate),
1219 ..Default::default()
1220 };
1221 let override_config = Config {
1222 punctuation_in_quote: true,
1223 ..Default::default()
1224 };
1225
1226 let merged = Config::merged(&base, &override_config);
1227
1228 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1230 assert!(merged.punctuation_in_quote);
1231 }
1232
1233 #[test]
1234 fn test_citation_options_merge_overrides_citation_fields_only() {
1235 let base = Config {
1236 processing: Some(Processing::AuthorDate),
1237 ..Default::default()
1238 };
1239
1240 let overrides = CitationOptions {
1241 strip_periods: Some(true),
1242 locators: Some(LocatorConfig::default()),
1243 ..Default::default()
1244 };
1245
1246 let merged = overrides.merged_with(&base);
1247 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1248 assert!(merged.strip_periods.unwrap_or(false));
1249 assert!(merged.locators.is_some());
1250 }
1251
1252 #[test]
1253 fn test_bibliography_options_merge_projects_shared_fields_only() {
1254 let base = Config {
1255 processing: Some(Processing::AuthorDate),
1256 ..Default::default()
1257 };
1258
1259 let overrides = BibliographyOptions {
1260 entry_suffix: Some(".".to_string()),
1261 separator: Some(", ".to_string()),
1262 suppress_period_after_url: true,
1263 ..Default::default()
1264 };
1265
1266 let merged = overrides.merged_with(&base);
1267 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1268 assert!(merged.locators.is_none());
1269 assert!(merged.notes.is_none());
1270 let bibliography = overrides.to_bibliography_config();
1271 assert_eq!(bibliography.entry_suffix.as_deref(), Some("."));
1272 assert_eq!(bibliography.separator.as_deref(), Some(", "));
1273 assert!(bibliography.suppress_period_after_url);
1274 }
1275
1276 #[test]
1277 fn test_bibliography_options_merge_leaves_shared_base_when_only_shared_overrides_exist() {
1278 let base = Config {
1279 processing: Some(Processing::AuthorDate),
1280 ..Default::default()
1281 };
1282
1283 let overrides = BibliographyOptions {
1284 contributors: Some(ContributorConfig::default()),
1285 ..Default::default()
1286 };
1287
1288 let merged = overrides.merged_with(&base);
1289 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1290 assert!(merged.contributors.is_some());
1291 }
1292
1293 #[test]
1294 fn citation_options_captures_unknown_fields_for_forward_compat() {
1295 let yaml = "future-key: true\n";
1296 let opts: CitationOptions = serde_yaml::from_str(yaml).unwrap();
1297 assert!(opts.unknown_fields.contains_key("future-key"));
1298 }
1299
1300 #[test]
1301 fn bibliography_options_captures_unknown_fields_for_forward_compat() {
1302 let yaml = "future-key: true\n";
1303 let opts: BibliographyOptions = serde_yaml::from_str(yaml).unwrap();
1304 assert!(opts.unknown_fields.contains_key("future-key"));
1305 }
1306
1307 #[test]
1308 fn note_config_captures_unknown_fields_for_forward_compat() {
1309 let yaml = "punctuation: inside\nfuture-key: true\n";
1310 let cfg: NoteConfig = serde_yaml::from_str(yaml).unwrap();
1311 assert!(cfg.unknown_fields.contains_key("future-key"));
1312 assert_eq!(cfg.punctuation, Some(NoteQuotePlacement::Inside));
1313 }
1314
1315 #[test]
1316 fn test_multilingual_preset_romanized_translated_parses_and_resolves() {
1317 let yaml = r#"multilingual: romanized-translated"#;
1319 let config: Config = serde_yaml::from_str(yaml).unwrap();
1320 let ml = config.multilingual.unwrap();
1321 assert_eq!(ml.title_mode, Some(MultilingualMode::Combined));
1322 assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
1323 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1324 }
1325
1326 #[test]
1327 fn test_multilingual_preset_romanized_only_parses_and_resolves() {
1328 let yaml = r#"multilingual: romanized-only"#;
1330 let config: Config = serde_yaml::from_str(yaml).unwrap();
1331 let ml = config.multilingual.unwrap();
1332 assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
1333 assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
1334 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1335 }
1336
1337 #[test]
1338 fn test_multilingual_preset_romanized_script_translated_parses_and_resolves() {
1339 use crate::options::multilingual::{MultilingualSegment, MultilingualView, SegmentWrap};
1342 let yaml = r#"multilingual: romanized-script-translated"#;
1343 let config: Config = serde_yaml::from_str(yaml).unwrap();
1344 let ml = config.multilingual.unwrap();
1345 assert_eq!(
1346 ml.title_mode,
1347 Some(MultilingualMode::Pattern(vec![
1348 MultilingualSegment {
1349 view: MultilingualView::Transliterated,
1350 wrap: SegmentWrap::None,
1351 },
1352 MultilingualSegment {
1353 view: MultilingualView::OriginalScript,
1354 wrap: SegmentWrap::None,
1355 },
1356 MultilingualSegment {
1357 view: MultilingualView::Translated,
1358 wrap: SegmentWrap::Brackets,
1359 },
1360 ]))
1361 );
1362 assert_eq!(
1363 ml.name_mode,
1364 Some(MultilingualMode::Pattern(vec![
1365 MultilingualSegment {
1366 view: MultilingualView::Transliterated,
1367 wrap: SegmentWrap::None,
1368 },
1369 MultilingualSegment {
1370 view: MultilingualView::OriginalScript,
1371 wrap: SegmentWrap::None,
1372 },
1373 ]))
1374 );
1375 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1376 assert!(ml.scripts.get("Han").is_some_and(|s| s.use_native_ordering));
1377 assert!(
1378 ml.scripts
1379 .get("Hangul")
1380 .is_some_and(|s| s.use_native_ordering)
1381 );
1382 }
1383
1384 #[test]
1385 fn test_multilingual_explicit_block_transliterated_roundtrips() {
1386 let yaml = r#"
1388multilingual:
1389 title-mode: transliterated
1390 preferred-script: Latn
1391"#;
1392 let config: Config = serde_yaml::from_str(yaml).unwrap();
1393 let ml = config.multilingual.clone().unwrap();
1394 assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
1395 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1396
1397 let yaml2 = serde_yaml::to_string(&config).unwrap();
1398 let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
1399 assert_eq!(config2.multilingual, config.multilingual);
1400 }
1401
1402 #[test]
1403 fn test_multilingual_pattern_block_roundtrips() {
1404 let yaml = r#"
1407multilingual:
1408 title-mode:
1409 pattern:
1410 - view: original-script
1411 - view: translated
1412 wrap: brackets
1413"#;
1414 let config: Config = serde_yaml::from_str(yaml).unwrap();
1415 let ml = config.multilingual.clone().unwrap();
1416 assert!(
1417 matches!(ml.title_mode, Some(MultilingualMode::Pattern(_))),
1418 "expected Pattern mode, got {:?}",
1419 ml.title_mode
1420 );
1421
1422 let yaml2 = serde_yaml::to_string(&config).unwrap();
1423 let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
1424 assert_eq!(config2.multilingual, config.multilingual);
1425 }
1426}