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 ArticleJournalBibliographyConfig, ArticleJournalNoPageFallback, BibliographyConfig,
22 BibliographyPartitionHeading, BibliographyPartitionKind, BibliographyPartitionMode,
23 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};
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")]
359 pub links: Option<LinksConfig>,
360 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
362 pub punctuation_in_quote: bool,
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
366 #[serde(skip_serializing_if = "Option::is_none")]
368 pub label_mode: Option<BibliographyLabelMode>,
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub label_wrap: Option<BibliographyLabelWrap>,
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub date_position: Option<DatePosition>,
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub title_terminator: Option<TitleTerminator>,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub repeated_author_rendering: Option<RepeatedAuthorRendering>,
381 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
383 pub strip_periods: Option<bool>,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub custom: Option<HashMap<String, serde_json::Value>>,
387 #[serde(
391 flatten,
392 default,
393 skip_serializing_if = "std::collections::BTreeMap::is_empty"
394 )]
395 #[cfg_attr(feature = "schema", schemars(skip))]
396 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
397}
398
399#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
401#[cfg_attr(feature = "schema", derive(JsonSchema))]
402#[serde(rename_all = "kebab-case")]
403pub struct NoteConfig {
404 #[serde(skip_serializing_if = "Option::is_none")]
407 pub punctuation: Option<NoteQuotePlacement>,
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub number: Option<NoteNumberPlacement>,
411 #[serde(skip_serializing_if = "Option::is_none")]
414 pub order: Option<NoteMarkerOrder>,
415 #[serde(
419 flatten,
420 default,
421 skip_serializing_if = "std::collections::BTreeMap::is_empty"
422 )]
423 #[cfg_attr(feature = "schema", schemars(skip))]
424 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
425}
426
427#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
429#[cfg_attr(feature = "schema", derive(JsonSchema))]
430#[serde(rename_all = "kebab-case")]
431pub enum NoteQuotePlacement {
432 Inside,
434 Outside,
436 Adaptive,
440}
441
442#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
444#[cfg_attr(feature = "schema", derive(JsonSchema))]
445#[serde(rename_all = "kebab-case")]
446pub enum NoteNumberPlacement {
447 Inside,
449 Outside,
451 Same,
454}
455
456#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
458#[cfg_attr(feature = "schema", derive(JsonSchema))]
459#[serde(rename_all = "kebab-case")]
460pub enum NoteMarkerOrder {
461 Before,
463 After,
465}
466
467#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
469#[cfg_attr(feature = "schema", derive(JsonSchema))]
470#[serde(rename_all = "kebab-case")]
471#[non_exhaustive]
472pub enum PageRangeFormat {
473 #[default]
475 Expanded,
476 Minimal,
478 MinimalTwo,
480 Chicago,
482 Chicago16,
484}
485
486pub mod titles;
487
488pub use titles::{TextCase, TitleRendering, TitlesConfig, TitlesConfigEntry};
489
490#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
492#[cfg_attr(feature = "schema", derive(JsonSchema))]
493#[serde(rename_all = "kebab-case")]
494pub struct LinksConfig {
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub doi: Option<bool>,
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub url: Option<bool>,
501 #[serde(skip_serializing_if = "Option::is_none")]
503 pub target: Option<LinkTarget>,
504 #[serde(skip_serializing_if = "Option::is_none")]
506 pub anchor: Option<LinkAnchor>,
507}
508
509#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
511#[cfg_attr(feature = "schema", derive(JsonSchema))]
512#[serde(rename_all = "kebab-case")]
513pub enum LinkTarget {
514 Url,
515 Doi,
516 UrlOrDoi,
517 Pubmed,
518 Pmcid,
519}
520
521#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
523#[cfg_attr(feature = "schema", derive(JsonSchema))]
524#[serde(rename_all = "kebab-case")]
525pub enum LinkAnchor {
526 Title,
528 Url,
530 Doi,
532 Component,
534 Entry,
536}
537
538impl Config {
539 pub fn effective_processing(&self) -> Processing {
544 self.processing.clone().unwrap_or_default()
545 }
546
547 pub fn merge(&mut self, other: &Config) {
552 crate::merge_options!(
553 self,
554 other,
555 processing,
556 locale_override,
557 localize,
558 multilingual,
559 dates,
560 titles,
561 locators,
562 page_range_format,
563 page_range_delimiter,
564 links,
565 volume_pages_delimiter,
566 locale_override,
567 strip_periods,
568 notes,
569 integral_name_memory,
570 org_abbreviation_memory,
571 custom,
572 );
573
574 if let Some(other_substitute) = &other.substitute {
575 if let Some(this_substitute) = &self.substitute {
576 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
577 } else {
578 self.substitute = Some(other_substitute.clone());
579 }
580 }
581
582 if let Some(other_contributors) = &other.contributors {
583 if let Some(this_contributors) = &mut self.contributors {
584 this_contributors.merge(other_contributors);
585 } else {
586 self.contributors = Some(other_contributors.clone());
587 }
588 }
589
590 if other.punctuation_in_quote {
591 self.punctuation_in_quote = true;
592 }
593 }
594
595 pub fn merged(base: &Config, override_config: &Config) -> Config {
599 let mut result = base.clone();
600 result.merge(override_config);
601 result
602 }
603}
604
605impl CitationOptions {
606 #[must_use]
608 pub fn to_config(&self) -> Config {
609 Config {
610 substitute: self.substitute.clone(),
611 processing: self.processing.clone(),
612 locale_override: None,
613 localize: self.localize.clone(),
614 multilingual: self.multilingual.clone(),
615 contributors: self.contributors.clone(),
616 dates: self.dates.clone(),
617 titles: self.titles.clone(),
618 locators: self.locators.clone(),
619 page_range_format: self.page_range_format.clone(),
620 page_range_delimiter: None,
621 links: self.links.clone(),
622 punctuation_in_quote: self.punctuation_in_quote,
623 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
624 strip_periods: self.strip_periods,
625 notes: self.notes.clone(),
626 integral_name_memory: self.integral_name_memory.clone(),
627 org_abbreviation_memory: self.org_abbreviation_memory.clone(),
628 custom: self.custom.clone(),
629 unknown_fields: std::collections::BTreeMap::new(),
630 }
631 }
632
633 #[must_use]
635 pub fn merged_with(&self, base: &Config) -> Config {
636 Config::merged(base, &self.to_config())
637 }
638
639 pub fn merge(&mut self, other: &CitationOptions) {
641 crate::merge_options!(
642 self,
643 other,
644 processing,
645 localize,
646 multilingual,
647 dates,
648 titles,
649 locators,
650 page_range_format,
651 links,
652 volume_pages_delimiter,
653 strip_periods,
654 notes,
655 integral_name_memory,
656 org_abbreviation_memory,
657 label_wrap,
658 group_delimiter,
659 custom,
660 );
661
662 if let Some(other_substitute) = &other.substitute {
663 if let Some(this_substitute) = &self.substitute {
664 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
665 } else {
666 self.substitute = Some(other_substitute.clone());
667 }
668 }
669
670 if let Some(other_contributors) = &other.contributors {
671 if let Some(this_contributors) = &mut self.contributors {
672 this_contributors.merge(other_contributors);
673 } else {
674 self.contributors = Some(other_contributors.clone());
675 }
676 }
677
678 if other.punctuation_in_quote {
679 self.punctuation_in_quote = true;
680 }
681 }
682}
683
684impl BibliographyOptions {
685 #[must_use]
687 pub fn to_bibliography_config(&self) -> BibliographyConfig {
688 BibliographyConfig {
689 article_journal: self.article_journal.clone(),
690 subsequent_author_substitute: self.subsequent_author_substitute.clone(),
691 subsequent_author_substitute_rule: self.subsequent_author_substitute_rule.clone(),
692 hanging_indent: self.hanging_indent,
693 entry_suffix: self.entry_suffix.clone(),
694 separator: self.separator.clone(),
695 suppress_period_after_url: self.suppress_period_after_url,
696 entry_suffix_after_url: self.entry_suffix_after_url,
697 entry_suffix_after_doi: self.entry_suffix_after_doi,
698 custom: None,
699 compound_numeric: self.compound_numeric.clone(),
700 sort_partitioning: self.sort_partitioning.clone(),
701 unknown_fields: std::collections::BTreeMap::new(),
702 }
703 }
704
705 #[must_use]
707 pub fn to_config(&self) -> Config {
708 Config {
709 substitute: self.substitute.clone(),
710 processing: self.processing.clone(),
711 locale_override: None,
712 localize: self.localize.clone(),
713 multilingual: self.multilingual.clone(),
714 contributors: self.contributors.clone(),
715 dates: self.dates.clone(),
716 titles: self.titles.clone(),
717 locators: None,
718 page_range_format: self.page_range_format.clone(),
719 page_range_delimiter: None,
720 links: self.links.clone(),
721 punctuation_in_quote: self.punctuation_in_quote,
722 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
723 strip_periods: self.strip_periods,
724 notes: None,
725 integral_name_memory: None,
726 org_abbreviation_memory: None,
727 custom: self.custom.clone(),
728 unknown_fields: std::collections::BTreeMap::new(),
729 }
730 }
731
732 #[must_use]
734 pub fn merged_with(&self, base: &Config) -> Config {
735 Config::merged(base, &self.to_config())
736 }
737
738 pub fn merge(&mut self, other: &BibliographyOptions) {
740 crate::merge_options!(
741 self,
742 other,
743 processing,
744 localize,
745 multilingual,
746 dates,
747 titles,
748 page_range_format,
749 links,
750 volume_pages_delimiter,
751 strip_periods,
752 article_journal,
753 subsequent_author_substitute,
754 subsequent_author_substitute_rule,
755 hanging_indent,
756 entry_suffix,
757 separator,
758 compound_numeric,
759 sort_partitioning,
760 label_mode,
761 label_wrap,
762 date_position,
763 title_terminator,
764 repeated_author_rendering,
765 custom,
766 );
767
768 self.merge_shared_fields(other);
769 }
770
771 fn merge_shared_fields(&mut self, other: &BibliographyOptions) {
772 if let Some(other_substitute) = &other.substitute {
773 if let Some(this_substitute) = &self.substitute {
774 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
775 } else {
776 self.substitute = Some(other_substitute.clone());
777 }
778 }
779
780 if let Some(other_contributors) = &other.contributors {
781 if let Some(this_contributors) = &mut self.contributors {
782 this_contributors.merge(other_contributors);
783 } else {
784 self.contributors = Some(other_contributors.clone());
785 }
786 }
787
788 if other.punctuation_in_quote {
789 self.punctuation_in_quote = true;
790 }
791 if other.suppress_period_after_url {
792 self.suppress_period_after_url = true;
793 }
794 if other.entry_suffix_after_url {
795 self.entry_suffix_after_url = true;
796 }
797 if other.entry_suffix_after_doi {
798 self.entry_suffix_after_doi = true;
799 }
800
801 for (key, value) in &other.unknown_fields {
802 self.unknown_fields.insert(key.clone(), value.clone());
803 }
804 }
805}
806
807fn deserialize_contributor_config<'de, D>(
809 deserializer: D,
810) -> Result<Option<ContributorConfig>, D::Error>
811where
812 D: serde::Deserializer<'de>,
813{
814 let value: Option<ContributorConfigEntry> = Option::deserialize(deserializer)?;
815 Ok(value.map(|entry| entry.resolve()))
816}
817
818fn deserialize_date_config<'de, D>(deserializer: D) -> Result<Option<DateConfig>, D::Error>
820where
821 D: serde::Deserializer<'de>,
822{
823 let value: Option<DateConfigEntry> = Option::deserialize(deserializer)?;
824 Ok(value.map(|entry| entry.resolve()))
825}
826
827fn deserialize_titles_config<'de, D>(
829 deserializer: D,
830) -> Result<Option<crate::options::titles::TitlesConfig>, D::Error>
831where
832 D: serde::Deserializer<'de>,
833{
834 let value: Option<crate::options::titles::TitlesConfigEntry> =
835 Option::deserialize(deserializer)?;
836 Ok(value.map(|entry| entry.resolve()))
837}
838
839fn deserialize_locator_config<'de, D>(deserializer: D) -> Result<Option<LocatorConfig>, D::Error>
841where
842 D: serde::Deserializer<'de>,
843{
844 let value: Option<LocatorConfigEntry> = Option::deserialize(deserializer)?;
845 Ok(value.map(|entry| entry.resolve()))
846}
847
848fn deserialize_multilingual_config<'de, D>(
850 deserializer: D,
851) -> Result<Option<MultilingualConfig>, D::Error>
852where
853 D: serde::Deserializer<'de>,
854{
855 let value: Option<crate::presets::MultilingualConfigEntry> = Option::deserialize(deserializer)?;
856 Ok(value.map(|entry| entry.resolve()))
857}
858
859impl<'de> Deserialize<'de> for Config {
860 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
861 where
862 D: serde::Deserializer<'de>,
863 {
864 #[derive(Deserialize)]
865 #[serde(rename_all = "kebab-case")]
866 struct ConfigWire {
867 #[serde(skip_serializing_if = "Option::is_none")]
868 substitute: Option<SubstituteConfig>,
869 #[serde(skip_serializing_if = "Option::is_none")]
870 processing: Option<Processing>,
871 #[serde(skip_serializing_if = "Option::is_none")]
872 locale_override: Option<String>,
873 #[serde(skip_serializing_if = "Option::is_none")]
874 localize: Option<Localize>,
875 #[serde(
876 skip_serializing_if = "Option::is_none",
877 deserialize_with = "deserialize_multilingual_config",
878 default
879 )]
880 multilingual: Option<MultilingualConfig>,
881 #[serde(
882 skip_serializing_if = "Option::is_none",
883 deserialize_with = "deserialize_contributor_config",
884 default
885 )]
886 contributors: Option<ContributorConfig>,
887 #[serde(
888 skip_serializing_if = "Option::is_none",
889 deserialize_with = "deserialize_date_config",
890 default
891 )]
892 dates: Option<DateConfig>,
893 #[serde(
894 skip_serializing_if = "Option::is_none",
895 deserialize_with = "deserialize_titles_config",
896 default
897 )]
898 titles: Option<crate::options::titles::TitlesConfig>,
899 #[serde(
900 skip_serializing_if = "Option::is_none",
901 deserialize_with = "deserialize_locator_config",
902 default
903 )]
904 locators: Option<LocatorConfig>,
905 #[serde(skip_serializing_if = "Option::is_none")]
906 page_range_format: Option<PageRangeFormat>,
907 #[serde(skip_serializing_if = "Option::is_none")]
908 page_range_delimiter: Option<String>,
909 #[serde(skip_serializing_if = "Option::is_none")]
910 links: Option<LinksConfig>,
911 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
912 punctuation_in_quote: bool,
913 #[serde(skip_serializing_if = "Option::is_none")]
914 volume_pages_delimiter: Option<DelimiterPunctuation>,
915 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
916 strip_periods: Option<bool>,
917 #[serde(skip_serializing_if = "Option::is_none")]
918 notes: Option<NoteConfig>,
919 #[serde(skip_serializing_if = "Option::is_none")]
920 integral_name_memory: Option<IntegralNameMemoryConfig>,
921 #[serde(skip_serializing_if = "Option::is_none")]
922 org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
923 #[serde(default)]
924 profile: Option<serde_yaml::Value>,
925 #[serde(skip_serializing_if = "Option::is_none")]
926 custom: Option<HashMap<String, serde_json::Value>>,
927 #[serde(flatten)]
928 unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
929 }
930
931 let wire = ConfigWire::deserialize(deserializer)?;
932 if wire.profile.is_some() {
933 return Err(serde::de::Error::custom(
934 "`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`",
935 ));
936 }
937
938 Ok(Self {
939 substitute: wire.substitute,
940 processing: wire.processing,
941 locale_override: wire.locale_override,
942 localize: wire.localize,
943 multilingual: wire.multilingual,
944 contributors: wire.contributors,
945 dates: wire.dates,
946 titles: wire.titles,
947 locators: wire.locators,
948 page_range_format: wire.page_range_format,
949 page_range_delimiter: wire.page_range_delimiter,
950 links: wire.links,
951 punctuation_in_quote: wire.punctuation_in_quote,
952 volume_pages_delimiter: wire.volume_pages_delimiter,
953 strip_periods: wire.strip_periods,
954 notes: wire.notes,
955 integral_name_memory: wire.integral_name_memory,
956 org_abbreviation_memory: wire.org_abbreviation_memory,
957 custom: wire.custom,
958 unknown_fields: wire.unknown_fields,
959 })
960 }
961}
962
963#[cfg(test)]
964#[allow(
965 clippy::unwrap_used,
966 clippy::expect_used,
967 clippy::panic,
968 clippy::indexing_slicing,
969 clippy::todo,
970 clippy::unimplemented,
971 clippy::unreachable,
972 clippy::get_unwrap,
973 reason = "Panicking is acceptable and often desired in tests."
974)]
975mod tests {
976 use super::*;
977
978 #[test]
979 fn test_config_default() {
980 let config = Config::default();
981 assert!(config.substitute.is_none());
982 assert!(config.processing.is_none());
983 }
984
985 #[test]
986 fn test_author_date_processing() {
987 let processing = Processing::AuthorDate;
988 let config = processing.config();
989 let disambiguate = config.disambiguate.unwrap();
990 assert!(disambiguate.year_suffix);
991 assert!(!disambiguate.names);
992 assert!(!disambiguate.add_givenname);
993 assert_eq!(
994 processing.default_bibliography_sort(),
995 Some(crate::presets::SortPreset::AuthorDateTitle)
996 );
997 assert_eq!(
998 config.sort,
999 Some(SortEntry::Preset(
1000 crate::presets::SortPreset::AuthorDateTitle
1001 ))
1002 );
1003 }
1004
1005 #[test]
1006 fn test_processing_default_bibliography_sorts() {
1007 assert_eq!(Processing::Numeric.default_bibliography_sort(), None);
1008 assert_eq!(
1009 Processing::Note.default_bibliography_sort(),
1010 Some(crate::presets::SortPreset::AuthorTitleDate)
1011 );
1012 assert_eq!(
1013 Processing::Label(LabelConfig::default()).default_bibliography_sort(),
1014 Some(crate::presets::SortPreset::AuthorDateTitle)
1015 );
1016 }
1017
1018 #[test]
1019 fn test_processing_default_citation_sort_policy_is_explicit_only() {
1020 assert_eq!(
1021 Processing::AuthorDate.default_citation_sort_policy(),
1022 CitationSortPolicy::ExplicitOnly
1023 );
1024 assert_eq!(
1025 Processing::Note.default_citation_sort_policy(),
1026 CitationSortPolicy::ExplicitOnly
1027 );
1028 }
1029
1030 #[test]
1031 fn test_substitute_default() {
1032 let sub = Substitute::default();
1033 assert_eq!(sub.template.len(), 3);
1034 }
1035
1036 #[test]
1037 fn test_config_yaml_roundtrip() {
1038 let yaml = r#"
1039substitute:
1040 contributor-role-form: short
1041 template:
1042 - editor
1043 - title
1044processing: author-date
1045contributors:
1046 display-as-sort: first
1047 and: symbol
1048"#;
1049 let config: Config = serde_yaml::from_str(yaml).unwrap();
1050 assert!(config.substitute.is_some());
1051 assert_eq!(config.processing, Some(Processing::AuthorDate));
1052 assert_eq!(
1053 config.contributors.as_ref().unwrap().and,
1054 Some(AndOptions::Symbol)
1055 );
1056 }
1057
1058 #[test]
1059 fn test_contributor_config_preset() {
1060 let yaml = r#"contributors: apa"#;
1062 let config: Config = serde_yaml::from_str(yaml).unwrap();
1063 let contributors = config.contributors.unwrap();
1064 assert_eq!(contributors.and, Some(AndOptions::Symbol));
1065 assert_eq!(contributors.display_as_sort, Some(DisplayAsSort::First));
1066 }
1067
1068 #[test]
1069 fn test_role_label_presets_parse_and_resolve_precedence() {
1070 let yaml = r#"
1071contributors:
1072 role:
1073 preset: short-suffix
1074 roles:
1075 editor:
1076 preset: long-suffix
1077"#;
1078 let config: Config = serde_yaml::from_str(yaml).unwrap();
1079 let contributors = config.contributors.unwrap();
1080
1081 assert_eq!(
1082 contributors.effective_role_label_preset(&crate::template::ContributorRole::Editor),
1083 Some(RoleLabelPreset::LongSuffix)
1084 );
1085 assert_eq!(
1086 contributors.effective_role_label_preset(&crate::template::ContributorRole::Translator),
1087 Some(RoleLabelPreset::ShortSuffix)
1088 );
1089
1090 let yaml_scalar = r#"
1092contributors:
1093 role: short-suffix
1094"#;
1095 let config2: Config = serde_yaml::from_str(yaml_scalar).unwrap();
1096 let contributors2 = config2.contributors.unwrap();
1097
1098 assert_eq!(
1099 contributors2
1100 .effective_role_label_preset(&crate::template::ContributorRole::Translator),
1101 Some(RoleLabelPreset::ShortSuffix)
1102 );
1103 }
1104
1105 #[test]
1106 fn test_role_specific_name_order_override_is_available() {
1107 let yaml = r#"
1108contributors:
1109 role:
1110 roles:
1111 translator:
1112 name-order: given-first
1113"#;
1114 let config: Config = serde_yaml::from_str(yaml).unwrap();
1115 let contributors = config.contributors.unwrap();
1116
1117 assert_eq!(
1118 contributors.effective_role_name_order(&crate::template::ContributorRole::Translator),
1119 Some(&crate::template::NameOrder::GivenFirst)
1120 );
1121 }
1122
1123 #[test]
1124 fn test_date_config_preset() {
1125 let yaml = r#"dates: long"#;
1127 let config: Config = serde_yaml::from_str(yaml).unwrap();
1128 let dates = config.dates.unwrap();
1129 assert_eq!(dates.month, MonthFormat::Long);
1130 }
1131
1132 #[test]
1133 fn test_titles_config_preset() {
1134 let yaml = r#"titles: chicago"#;
1136 let config: Config = serde_yaml::from_str(yaml).unwrap();
1137 let titles = config.titles.unwrap();
1138 assert_eq!(titles.component.unwrap().quote, Some(true));
1139 assert_eq!(titles.monograph.unwrap().emph, Some(true));
1140 }
1141
1142 #[test]
1143 fn test_substitute_config_preset() {
1144 let yaml = r#"substitute: standard"#;
1146 let config: Config = serde_yaml::from_str(yaml).unwrap();
1147 assert!(config.substitute.is_some());
1148 let resolved = config.substitute.unwrap().resolve();
1149 assert_eq!(resolved.template.len(), 3);
1150 assert_eq!(resolved.template[0], SubstituteKey::Editor);
1151 }
1152
1153 #[test]
1154 fn test_substitute_config_explicit() {
1155 let yaml = r#"
1157substitute:
1158 template:
1159 - title
1160 - editor
1161"#;
1162 let config: Config = serde_yaml::from_str(yaml).unwrap();
1163 let resolved = config.substitute.unwrap().resolve();
1164 assert_eq!(resolved.template[0], SubstituteKey::Title);
1165 assert_eq!(resolved.template[1], SubstituteKey::Editor);
1166 }
1167
1168 #[test]
1169 fn test_config_merge_precedence() {
1170 let base_yaml = r#"
1172processing: author-date
1173locale-override: en-US-base
1174contributors:
1175 display-as-sort: first
1176 and: symbol
1177"#;
1178 let mut base: Config = serde_yaml::from_str(base_yaml).unwrap();
1179
1180 let override_yaml = r#"
1182contributors:
1183 and: text
1184locale-override: en-US-chicago
1185"#;
1186 let override_config: Config = serde_yaml::from_str(override_yaml).unwrap();
1187
1188 base.merge(&override_config);
1190
1191 assert_eq!(base.processing, Some(Processing::AuthorDate));
1193 assert_eq!(base.locale_override.as_deref(), Some("en-US-chicago"));
1194
1195 assert_eq!(
1197 base.contributors.as_ref().unwrap().and,
1198 Some(AndOptions::Text)
1199 );
1200 }
1201
1202 #[test]
1203 fn test_config_deserializes_locale_override() {
1204 let config: Config = serde_yaml::from_str("locale-override: en-US-chicago").unwrap();
1205 assert_eq!(config.locale_override.as_deref(), Some("en-US-chicago"));
1206 }
1207
1208 #[test]
1209 fn test_config_merged_convenience() {
1210 let base = Config {
1211 processing: Some(Processing::AuthorDate),
1212 ..Default::default()
1213 };
1214 let override_config = Config {
1215 punctuation_in_quote: true,
1216 ..Default::default()
1217 };
1218
1219 let merged = Config::merged(&base, &override_config);
1220
1221 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1223 assert!(merged.punctuation_in_quote);
1224 }
1225
1226 #[test]
1227 fn test_citation_options_merge_overrides_citation_fields_only() {
1228 let base = Config {
1229 processing: Some(Processing::AuthorDate),
1230 ..Default::default()
1231 };
1232
1233 let overrides = CitationOptions {
1234 strip_periods: Some(true),
1235 locators: Some(LocatorConfig::default()),
1236 ..Default::default()
1237 };
1238
1239 let merged = overrides.merged_with(&base);
1240 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1241 assert!(merged.strip_periods.unwrap_or(false));
1242 assert!(merged.locators.is_some());
1243 }
1244
1245 #[test]
1246 fn test_bibliography_options_merge_projects_shared_fields_only() {
1247 let base = Config {
1248 processing: Some(Processing::AuthorDate),
1249 ..Default::default()
1250 };
1251
1252 let overrides = BibliographyOptions {
1253 entry_suffix: Some(".".to_string()),
1254 separator: Some(", ".to_string()),
1255 suppress_period_after_url: true,
1256 ..Default::default()
1257 };
1258
1259 let merged = overrides.merged_with(&base);
1260 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1261 assert!(merged.locators.is_none());
1262 assert!(merged.notes.is_none());
1263 let bibliography = overrides.to_bibliography_config();
1264 assert_eq!(bibliography.entry_suffix.as_deref(), Some("."));
1265 assert_eq!(bibliography.separator.as_deref(), Some(", "));
1266 assert!(bibliography.suppress_period_after_url);
1267 }
1268
1269 #[test]
1270 fn test_bibliography_options_merge_leaves_shared_base_when_only_shared_overrides_exist() {
1271 let base = Config {
1272 processing: Some(Processing::AuthorDate),
1273 ..Default::default()
1274 };
1275
1276 let overrides = BibliographyOptions {
1277 contributors: Some(ContributorConfig::default()),
1278 ..Default::default()
1279 };
1280
1281 let merged = overrides.merged_with(&base);
1282 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1283 assert!(merged.contributors.is_some());
1284 }
1285
1286 #[test]
1287 fn citation_options_captures_unknown_fields_for_forward_compat() {
1288 let yaml = "future-key: true\n";
1289 let opts: CitationOptions = serde_yaml::from_str(yaml).unwrap();
1290 assert!(opts.unknown_fields.contains_key("future-key"));
1291 }
1292
1293 #[test]
1294 fn bibliography_options_captures_unknown_fields_for_forward_compat() {
1295 let yaml = "future-key: true\n";
1296 let opts: BibliographyOptions = serde_yaml::from_str(yaml).unwrap();
1297 assert!(opts.unknown_fields.contains_key("future-key"));
1298 }
1299
1300 #[test]
1301 fn note_config_captures_unknown_fields_for_forward_compat() {
1302 let yaml = "punctuation: inside\nfuture-key: true\n";
1303 let cfg: NoteConfig = serde_yaml::from_str(yaml).unwrap();
1304 assert!(cfg.unknown_fields.contains_key("future-key"));
1305 assert_eq!(cfg.punctuation, Some(NoteQuotePlacement::Inside));
1306 }
1307
1308 #[test]
1309 fn test_multilingual_preset_romanized_translated_parses_and_resolves() {
1310 let yaml = r#"multilingual: romanized-translated"#;
1312 let config: Config = serde_yaml::from_str(yaml).unwrap();
1313 let ml = config.multilingual.unwrap();
1314 assert_eq!(ml.title_mode, Some(MultilingualMode::Combined));
1315 assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
1316 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1317 }
1318
1319 #[test]
1320 fn test_multilingual_preset_romanized_only_parses_and_resolves() {
1321 let yaml = r#"multilingual: romanized-only"#;
1323 let config: Config = serde_yaml::from_str(yaml).unwrap();
1324 let ml = config.multilingual.unwrap();
1325 assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
1326 assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
1327 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1328 }
1329
1330 #[test]
1331 fn test_multilingual_preset_romanized_script_translated_parses_and_resolves() {
1332 use crate::options::multilingual::{MultilingualSegment, MultilingualView, SegmentWrap};
1335 let yaml = r#"multilingual: romanized-script-translated"#;
1336 let config: Config = serde_yaml::from_str(yaml).unwrap();
1337 let ml = config.multilingual.unwrap();
1338 assert_eq!(
1339 ml.title_mode,
1340 Some(MultilingualMode::Pattern(vec![
1341 MultilingualSegment {
1342 view: MultilingualView::Transliterated,
1343 wrap: SegmentWrap::None,
1344 },
1345 MultilingualSegment {
1346 view: MultilingualView::OriginalScript,
1347 wrap: SegmentWrap::None,
1348 },
1349 MultilingualSegment {
1350 view: MultilingualView::Translated,
1351 wrap: SegmentWrap::Brackets,
1352 },
1353 ]))
1354 );
1355 assert_eq!(
1356 ml.name_mode,
1357 Some(MultilingualMode::Pattern(vec![
1358 MultilingualSegment {
1359 view: MultilingualView::Transliterated,
1360 wrap: SegmentWrap::None,
1361 },
1362 MultilingualSegment {
1363 view: MultilingualView::OriginalScript,
1364 wrap: SegmentWrap::None,
1365 },
1366 ]))
1367 );
1368 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1369 assert!(ml.scripts.get("Han").is_some_and(|s| s.use_native_ordering));
1370 assert!(
1371 ml.scripts
1372 .get("Hangul")
1373 .is_some_and(|s| s.use_native_ordering)
1374 );
1375 }
1376
1377 #[test]
1378 fn test_multilingual_explicit_block_transliterated_roundtrips() {
1379 let yaml = r#"
1381multilingual:
1382 title-mode: transliterated
1383 preferred-script: Latn
1384"#;
1385 let config: Config = serde_yaml::from_str(yaml).unwrap();
1386 let ml = config.multilingual.clone().unwrap();
1387 assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
1388 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1389
1390 let yaml2 = serde_yaml::to_string(&config).unwrap();
1391 let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
1392 assert_eq!(config2.multilingual, config.multilingual);
1393 }
1394
1395 #[test]
1396 fn test_multilingual_pattern_block_roundtrips() {
1397 let yaml = r#"
1400multilingual:
1401 title-mode:
1402 pattern:
1403 - view: original-script
1404 - view: translated
1405 wrap: brackets
1406"#;
1407 let config: Config = serde_yaml::from_str(yaml).unwrap();
1408 let ml = config.multilingual.clone().unwrap();
1409 assert!(
1410 matches!(ml.title_mode, Some(MultilingualMode::Pattern(_))),
1411 "expected Pattern mode, got {:?}",
1412 ml.title_mode
1413 );
1414
1415 let yaml2 = serde_yaml::to_string(&config).unwrap();
1416 let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
1417 assert_eq!(config2.multilingual, config.multilingual);
1418 }
1419}