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 bibliography::{
20 ArticleJournalBibliographyConfig, ArticleJournalNoPageFallback, BibliographyConfig,
21 BibliographyPartitionHeading, BibliographyPartitionKind, BibliographyPartitionMode,
22 BibliographySortPartitioning, SubsequentAuthorSubstituteRule,
23};
24pub use contributors::{
25 AndOptions, AndOtherOptions, ContributorConfig, ContributorConfigEntry, DelimiterPrecedesLast,
26 DemoteNonDroppingParticle, DisplayAsSort, NameForm, RoleLabelPreset, RoleOptions,
27 RoleOptionsEntry, RoleRendering, ShortenListOptions,
28};
29pub use dates::{DateConfig, DateConfigEntry};
30pub use integral_name_memory::{
31 IntegralNameContexts, IntegralNameMemoryConfig, IntegralNameScope, OrgAbbreviationMemoryConfig,
32 ResolvedIntegralNameMemoryConfig, ResolvedOrgAbbreviationMemoryConfig, ShortNameDisplay,
33 SubsequentNameForm,
34};
35pub use localization::{Localize, MonthFormat, Scope};
36pub use locators::{
37 LabelForm, LabelRepeat, LocatorConfig, LocatorConfigEntry, LocatorKindConfig, LocatorPattern,
38 LocatorPreset, TypeClass,
39};
40pub use multilingual::{MultilingualConfig, MultilingualMode, ScriptConfig};
41pub use processing::{
42 CitationSortPolicy, Disambiguation, Group, LabelConfig, LabelParams, LabelPreset, Processing,
43 ProcessingCustom, Sort, SortEntry, SortKey, SortSpec,
44};
45pub use scoped::{
46 BibliographyLabelMode, BibliographyLabelWrap, CitationGroupDelimiter, DatePosition, LabelWrap,
47 RepeatedAuthorRendering, TitleTerminator,
48};
49pub use substitute::{Substitute, SubstituteConfig, SubstituteKey};
50
51use crate::template::DelimiterPunctuation;
52#[cfg(feature = "schema")]
53use schemars::JsonSchema;
54use serde::{Deserialize, Serialize};
55use std::collections::HashMap;
56
57#[derive(Debug, Default, PartialEq, Clone, Serialize)]
59#[cfg_attr(feature = "schema", derive(JsonSchema))]
60#[serde(rename_all = "kebab-case")]
61pub struct Config {
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub substitute: Option<SubstituteConfig>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub processing: Option<Processing>,
68 #[serde(skip_serializing_if = "Option::is_none")]
74 pub locale_override: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub localize: Option<Localize>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub multilingual: Option<MultilingualConfig>,
81 #[serde(
84 skip_serializing_if = "Option::is_none",
85 deserialize_with = "deserialize_contributor_config",
86 default
87 )]
88 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
89 pub contributors: Option<ContributorConfig>,
90 #[serde(
93 skip_serializing_if = "Option::is_none",
94 deserialize_with = "deserialize_date_config",
95 default
96 )]
97 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
98 pub dates: Option<DateConfig>,
99 #[serde(
102 skip_serializing_if = "Option::is_none",
103 deserialize_with = "deserialize_titles_config",
104 default
105 )]
106 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
107 pub titles: Option<crate::options::titles::TitlesConfig>,
108 #[serde(
111 skip_serializing_if = "Option::is_none",
112 deserialize_with = "deserialize_locator_config",
113 default
114 )]
115 #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
116 pub locators: Option<LocatorConfig>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub page_range_format: Option<PageRangeFormat>,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub links: Option<LinksConfig>,
123 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
127 pub punctuation_in_quote: bool,
128 #[serde(skip_serializing_if = "Option::is_none")]
132 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
133 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
135 pub strip_periods: Option<bool>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub notes: Option<NoteConfig>,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub integral_name_memory: Option<IntegralNameMemoryConfig>,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub custom: Option<HashMap<String, serde_json::Value>>,
148 #[serde(
152 flatten,
153 default,
154 skip_serializing_if = "std::collections::BTreeMap::is_empty"
155 )]
156 #[cfg_attr(feature = "schema", schemars(skip))]
157 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
158}
159
160#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
162#[cfg_attr(feature = "schema", derive(JsonSchema))]
163#[serde(rename_all = "kebab-case")]
164pub struct CitationOptions {
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub substitute: Option<SubstituteConfig>,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub processing: Option<Processing>,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub localize: Option<Localize>,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub multilingual: Option<MultilingualConfig>,
177 #[serde(
179 skip_serializing_if = "Option::is_none",
180 deserialize_with = "deserialize_contributor_config",
181 default
182 )]
183 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
184 pub contributors: Option<ContributorConfig>,
185 #[serde(
187 skip_serializing_if = "Option::is_none",
188 deserialize_with = "deserialize_date_config",
189 default
190 )]
191 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
192 pub dates: Option<DateConfig>,
193 #[serde(
195 skip_serializing_if = "Option::is_none",
196 deserialize_with = "deserialize_titles_config",
197 default
198 )]
199 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
200 pub titles: Option<crate::options::titles::TitlesConfig>,
201 #[serde(
203 skip_serializing_if = "Option::is_none",
204 deserialize_with = "deserialize_locator_config",
205 default
206 )]
207 #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
208 pub locators: Option<LocatorConfig>,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub page_range_format: Option<PageRangeFormat>,
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub links: Option<LinksConfig>,
215 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
217 pub punctuation_in_quote: bool,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
221 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
223 pub strip_periods: Option<bool>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub notes: Option<NoteConfig>,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub integral_name_memory: Option<IntegralNameMemoryConfig>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub label_wrap: Option<LabelWrap>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub group_delimiter: Option<CitationGroupDelimiter>,
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub custom: Option<HashMap<String, serde_json::Value>>,
242 #[serde(
246 flatten,
247 default,
248 skip_serializing_if = "std::collections::BTreeMap::is_empty"
249 )]
250 #[cfg_attr(feature = "schema", schemars(skip))]
251 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
252}
253
254#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
256#[cfg_attr(feature = "schema", derive(JsonSchema))]
257#[serde(rename_all = "kebab-case")]
258pub struct BibliographyOptions {
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub substitute: Option<SubstituteConfig>,
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub processing: Option<Processing>,
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub localize: Option<Localize>,
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub multilingual: Option<MultilingualConfig>,
271 #[serde(
273 skip_serializing_if = "Option::is_none",
274 deserialize_with = "deserialize_contributor_config",
275 default
276 )]
277 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
278 pub contributors: Option<ContributorConfig>,
279 #[serde(
281 skip_serializing_if = "Option::is_none",
282 deserialize_with = "deserialize_date_config",
283 default
284 )]
285 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
286 pub dates: Option<DateConfig>,
287 #[serde(
289 skip_serializing_if = "Option::is_none",
290 deserialize_with = "deserialize_titles_config",
291 default
292 )]
293 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
294 pub titles: Option<crate::options::titles::TitlesConfig>,
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub page_range_format: Option<PageRangeFormat>,
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub article_journal: Option<ArticleJournalBibliographyConfig>,
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub subsequent_author_substitute: Option<String>,
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub subsequent_author_substitute_rule: Option<SubsequentAuthorSubstituteRule>,
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub hanging_indent: Option<bool>,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub entry_suffix: Option<String>,
313 #[serde(skip_serializing_if = "Option::is_none")]
315 pub separator: Option<String>,
316 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
318 pub suppress_period_after_url: bool,
319 #[serde(skip_serializing_if = "Option::is_none")]
321 pub compound_numeric: Option<bibliography::CompoundNumericConfig>,
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub sort_partitioning: Option<bibliography::BibliographySortPartitioning>,
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub links: Option<LinksConfig>,
328 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
330 pub punctuation_in_quote: bool,
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub label_mode: Option<BibliographyLabelMode>,
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub label_wrap: Option<BibliographyLabelWrap>,
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub date_position: Option<DatePosition>,
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub title_terminator: Option<TitleTerminator>,
346 #[serde(skip_serializing_if = "Option::is_none")]
348 pub repeated_author_rendering: Option<RepeatedAuthorRendering>,
349 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
351 pub strip_periods: Option<bool>,
352 #[serde(skip_serializing_if = "Option::is_none")]
354 pub custom: Option<HashMap<String, serde_json::Value>>,
355 #[serde(
359 flatten,
360 default,
361 skip_serializing_if = "std::collections::BTreeMap::is_empty"
362 )]
363 #[cfg_attr(feature = "schema", schemars(skip))]
364 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
365}
366
367#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
369#[cfg_attr(feature = "schema", derive(JsonSchema))]
370#[serde(rename_all = "kebab-case")]
371pub struct NoteConfig {
372 #[serde(skip_serializing_if = "Option::is_none")]
375 pub punctuation: Option<NoteQuotePlacement>,
376 #[serde(skip_serializing_if = "Option::is_none")]
378 pub number: Option<NoteNumberPlacement>,
379 #[serde(skip_serializing_if = "Option::is_none")]
382 pub order: Option<NoteMarkerOrder>,
383 #[serde(
387 flatten,
388 default,
389 skip_serializing_if = "std::collections::BTreeMap::is_empty"
390 )]
391 #[cfg_attr(feature = "schema", schemars(skip))]
392 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
393}
394
395#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
397#[cfg_attr(feature = "schema", derive(JsonSchema))]
398#[serde(rename_all = "kebab-case")]
399pub enum NoteQuotePlacement {
400 Inside,
402 Outside,
404 Adaptive,
408}
409
410#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
412#[cfg_attr(feature = "schema", derive(JsonSchema))]
413#[serde(rename_all = "kebab-case")]
414pub enum NoteNumberPlacement {
415 Inside,
417 Outside,
419 Same,
422}
423
424#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
426#[cfg_attr(feature = "schema", derive(JsonSchema))]
427#[serde(rename_all = "kebab-case")]
428pub enum NoteMarkerOrder {
429 Before,
431 After,
433}
434
435#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
437#[cfg_attr(feature = "schema", derive(JsonSchema))]
438#[serde(rename_all = "kebab-case")]
439#[non_exhaustive]
440pub enum PageRangeFormat {
441 #[default]
443 Expanded,
444 Minimal,
446 MinimalTwo,
448 Chicago,
450 Chicago16,
452}
453
454pub mod titles;
455
456pub use titles::{TextCase, TitleRendering, TitlesConfig, TitlesConfigEntry};
457
458#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
460#[cfg_attr(feature = "schema", derive(JsonSchema))]
461#[serde(rename_all = "kebab-case")]
462pub struct LinksConfig {
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub doi: Option<bool>,
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub url: Option<bool>,
469 #[serde(skip_serializing_if = "Option::is_none")]
471 pub target: Option<LinkTarget>,
472 #[serde(skip_serializing_if = "Option::is_none")]
474 pub anchor: Option<LinkAnchor>,
475}
476
477#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
479#[cfg_attr(feature = "schema", derive(JsonSchema))]
480#[serde(rename_all = "kebab-case")]
481pub enum LinkTarget {
482 Url,
483 Doi,
484 UrlOrDoi,
485 Pubmed,
486 Pmcid,
487}
488
489#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
491#[cfg_attr(feature = "schema", derive(JsonSchema))]
492#[serde(rename_all = "kebab-case")]
493pub enum LinkAnchor {
494 Title,
496 Url,
498 Doi,
500 Component,
502 Entry,
504}
505
506impl Config {
507 pub fn merge(&mut self, other: &Config) {
512 crate::merge_options!(
513 self,
514 other,
515 processing,
516 locale_override,
517 localize,
518 multilingual,
519 dates,
520 titles,
521 locators,
522 page_range_format,
523 links,
524 volume_pages_delimiter,
525 locale_override,
526 strip_periods,
527 notes,
528 integral_name_memory,
529 org_abbreviation_memory,
530 custom,
531 );
532
533 if let Some(other_substitute) = &other.substitute {
534 if let Some(this_substitute) = &self.substitute {
535 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
536 } else {
537 self.substitute = Some(other_substitute.clone());
538 }
539 }
540
541 if let Some(other_contributors) = &other.contributors {
542 if let Some(this_contributors) = &mut self.contributors {
543 this_contributors.merge(other_contributors);
544 } else {
545 self.contributors = Some(other_contributors.clone());
546 }
547 }
548
549 if other.punctuation_in_quote {
550 self.punctuation_in_quote = true;
551 }
552 }
553
554 pub fn merged(base: &Config, override_config: &Config) -> Config {
558 let mut result = base.clone();
559 result.merge(override_config);
560 result
561 }
562}
563
564impl CitationOptions {
565 #[must_use]
567 pub fn to_config(&self) -> Config {
568 Config {
569 substitute: self.substitute.clone(),
570 processing: self.processing.clone(),
571 locale_override: None,
572 localize: self.localize.clone(),
573 multilingual: self.multilingual.clone(),
574 contributors: self.contributors.clone(),
575 dates: self.dates.clone(),
576 titles: self.titles.clone(),
577 locators: self.locators.clone(),
578 page_range_format: self.page_range_format.clone(),
579 links: self.links.clone(),
580 punctuation_in_quote: self.punctuation_in_quote,
581 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
582 strip_periods: self.strip_periods,
583 notes: self.notes.clone(),
584 integral_name_memory: self.integral_name_memory.clone(),
585 org_abbreviation_memory: self.org_abbreviation_memory.clone(),
586 custom: self.custom.clone(),
587 unknown_fields: std::collections::BTreeMap::new(),
588 }
589 }
590
591 #[must_use]
593 pub fn merged_with(&self, base: &Config) -> Config {
594 Config::merged(base, &self.to_config())
595 }
596
597 pub fn merge(&mut self, other: &CitationOptions) {
599 crate::merge_options!(
600 self,
601 other,
602 processing,
603 localize,
604 multilingual,
605 dates,
606 titles,
607 locators,
608 page_range_format,
609 links,
610 volume_pages_delimiter,
611 strip_periods,
612 notes,
613 integral_name_memory,
614 org_abbreviation_memory,
615 label_wrap,
616 group_delimiter,
617 custom,
618 );
619
620 if let Some(other_substitute) = &other.substitute {
621 if let Some(this_substitute) = &self.substitute {
622 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
623 } else {
624 self.substitute = Some(other_substitute.clone());
625 }
626 }
627
628 if let Some(other_contributors) = &other.contributors {
629 if let Some(this_contributors) = &mut self.contributors {
630 this_contributors.merge(other_contributors);
631 } else {
632 self.contributors = Some(other_contributors.clone());
633 }
634 }
635
636 if other.punctuation_in_quote {
637 self.punctuation_in_quote = true;
638 }
639 }
640}
641
642impl BibliographyOptions {
643 #[must_use]
645 pub fn to_bibliography_config(&self) -> BibliographyConfig {
646 BibliographyConfig {
647 article_journal: self.article_journal.clone(),
648 subsequent_author_substitute: self.subsequent_author_substitute.clone(),
649 subsequent_author_substitute_rule: self.subsequent_author_substitute_rule.clone(),
650 hanging_indent: self.hanging_indent,
651 entry_suffix: self.entry_suffix.clone(),
652 separator: self.separator.clone(),
653 suppress_period_after_url: self.suppress_period_after_url,
654 custom: None,
655 compound_numeric: self.compound_numeric.clone(),
656 sort_partitioning: self.sort_partitioning.clone(),
657 unknown_fields: std::collections::BTreeMap::new(),
658 }
659 }
660
661 #[must_use]
663 pub fn to_config(&self) -> Config {
664 Config {
665 substitute: self.substitute.clone(),
666 processing: self.processing.clone(),
667 locale_override: None,
668 localize: self.localize.clone(),
669 multilingual: self.multilingual.clone(),
670 contributors: self.contributors.clone(),
671 dates: self.dates.clone(),
672 titles: self.titles.clone(),
673 locators: None,
674 page_range_format: self.page_range_format.clone(),
675 links: self.links.clone(),
676 punctuation_in_quote: self.punctuation_in_quote,
677 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
678 strip_periods: self.strip_periods,
679 notes: None,
680 integral_name_memory: None,
681 org_abbreviation_memory: None,
682 custom: self.custom.clone(),
683 unknown_fields: std::collections::BTreeMap::new(),
684 }
685 }
686
687 #[must_use]
689 pub fn merged_with(&self, base: &Config) -> Config {
690 Config::merged(base, &self.to_config())
691 }
692
693 pub fn merge(&mut self, other: &BibliographyOptions) {
695 crate::merge_options!(
696 self,
697 other,
698 processing,
699 localize,
700 multilingual,
701 dates,
702 titles,
703 page_range_format,
704 links,
705 volume_pages_delimiter,
706 strip_periods,
707 article_journal,
708 subsequent_author_substitute,
709 subsequent_author_substitute_rule,
710 hanging_indent,
711 entry_suffix,
712 separator,
713 compound_numeric,
714 sort_partitioning,
715 label_mode,
716 label_wrap,
717 date_position,
718 title_terminator,
719 repeated_author_rendering,
720 custom,
721 );
722
723 self.merge_shared_fields(other);
724 }
725
726 fn merge_shared_fields(&mut self, other: &BibliographyOptions) {
727 if let Some(other_substitute) = &other.substitute {
728 if let Some(this_substitute) = &self.substitute {
729 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
730 } else {
731 self.substitute = Some(other_substitute.clone());
732 }
733 }
734
735 if let Some(other_contributors) = &other.contributors {
736 if let Some(this_contributors) = &mut self.contributors {
737 this_contributors.merge(other_contributors);
738 } else {
739 self.contributors = Some(other_contributors.clone());
740 }
741 }
742
743 if other.punctuation_in_quote {
744 self.punctuation_in_quote = true;
745 }
746 if other.suppress_period_after_url {
747 self.suppress_period_after_url = true;
748 }
749
750 for (key, value) in &other.unknown_fields {
751 self.unknown_fields.insert(key.clone(), value.clone());
752 }
753 }
754}
755
756fn deserialize_contributor_config<'de, D>(
758 deserializer: D,
759) -> Result<Option<ContributorConfig>, D::Error>
760where
761 D: serde::Deserializer<'de>,
762{
763 let value: Option<ContributorConfigEntry> = Option::deserialize(deserializer)?;
764 Ok(value.map(|entry| entry.resolve()))
765}
766
767fn deserialize_date_config<'de, D>(deserializer: D) -> Result<Option<DateConfig>, D::Error>
769where
770 D: serde::Deserializer<'de>,
771{
772 let value: Option<DateConfigEntry> = Option::deserialize(deserializer)?;
773 Ok(value.map(|entry| entry.resolve()))
774}
775
776fn deserialize_titles_config<'de, D>(
778 deserializer: D,
779) -> Result<Option<crate::options::titles::TitlesConfig>, D::Error>
780where
781 D: serde::Deserializer<'de>,
782{
783 let value: Option<crate::options::titles::TitlesConfigEntry> =
784 Option::deserialize(deserializer)?;
785 Ok(value.map(|entry| entry.resolve()))
786}
787
788fn deserialize_locator_config<'de, D>(deserializer: D) -> Result<Option<LocatorConfig>, D::Error>
790where
791 D: serde::Deserializer<'de>,
792{
793 let value: Option<LocatorConfigEntry> = Option::deserialize(deserializer)?;
794 Ok(value.map(|entry| entry.resolve()))
795}
796
797impl<'de> Deserialize<'de> for Config {
798 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
799 where
800 D: serde::Deserializer<'de>,
801 {
802 #[derive(Deserialize)]
803 #[serde(rename_all = "kebab-case")]
804 struct ConfigWire {
805 #[serde(skip_serializing_if = "Option::is_none")]
806 substitute: Option<SubstituteConfig>,
807 #[serde(skip_serializing_if = "Option::is_none")]
808 processing: Option<Processing>,
809 #[serde(skip_serializing_if = "Option::is_none")]
810 locale_override: Option<String>,
811 #[serde(skip_serializing_if = "Option::is_none")]
812 localize: Option<Localize>,
813 #[serde(skip_serializing_if = "Option::is_none")]
814 multilingual: Option<MultilingualConfig>,
815 #[serde(
816 skip_serializing_if = "Option::is_none",
817 deserialize_with = "deserialize_contributor_config",
818 default
819 )]
820 contributors: Option<ContributorConfig>,
821 #[serde(
822 skip_serializing_if = "Option::is_none",
823 deserialize_with = "deserialize_date_config",
824 default
825 )]
826 dates: Option<DateConfig>,
827 #[serde(
828 skip_serializing_if = "Option::is_none",
829 deserialize_with = "deserialize_titles_config",
830 default
831 )]
832 titles: Option<crate::options::titles::TitlesConfig>,
833 #[serde(
834 skip_serializing_if = "Option::is_none",
835 deserialize_with = "deserialize_locator_config",
836 default
837 )]
838 locators: Option<LocatorConfig>,
839 #[serde(skip_serializing_if = "Option::is_none")]
840 page_range_format: Option<PageRangeFormat>,
841 #[serde(skip_serializing_if = "Option::is_none")]
842 links: Option<LinksConfig>,
843 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
844 punctuation_in_quote: bool,
845 #[serde(skip_serializing_if = "Option::is_none")]
846 volume_pages_delimiter: Option<DelimiterPunctuation>,
847 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
848 strip_periods: Option<bool>,
849 #[serde(skip_serializing_if = "Option::is_none")]
850 notes: Option<NoteConfig>,
851 #[serde(skip_serializing_if = "Option::is_none")]
852 integral_name_memory: Option<IntegralNameMemoryConfig>,
853 #[serde(skip_serializing_if = "Option::is_none")]
854 org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
855 #[serde(default)]
856 profile: Option<serde_yaml::Value>,
857 #[serde(skip_serializing_if = "Option::is_none")]
858 custom: Option<HashMap<String, serde_json::Value>>,
859 #[serde(flatten)]
860 unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
861 }
862
863 let wire = ConfigWire::deserialize(deserializer)?;
864 if wire.profile.is_some() {
865 return Err(serde::de::Error::custom(
866 "`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`",
867 ));
868 }
869
870 Ok(Self {
871 substitute: wire.substitute,
872 processing: wire.processing,
873 locale_override: wire.locale_override,
874 localize: wire.localize,
875 multilingual: wire.multilingual,
876 contributors: wire.contributors,
877 dates: wire.dates,
878 titles: wire.titles,
879 locators: wire.locators,
880 page_range_format: wire.page_range_format,
881 links: wire.links,
882 punctuation_in_quote: wire.punctuation_in_quote,
883 volume_pages_delimiter: wire.volume_pages_delimiter,
884 strip_periods: wire.strip_periods,
885 notes: wire.notes,
886 integral_name_memory: wire.integral_name_memory,
887 org_abbreviation_memory: wire.org_abbreviation_memory,
888 custom: wire.custom,
889 unknown_fields: wire.unknown_fields,
890 })
891 }
892}
893
894#[cfg(test)]
895#[allow(
896 clippy::unwrap_used,
897 clippy::expect_used,
898 clippy::panic,
899 clippy::indexing_slicing,
900 clippy::todo,
901 clippy::unimplemented,
902 clippy::unreachable,
903 clippy::get_unwrap,
904 reason = "Panicking is acceptable and often desired in tests."
905)]
906mod tests {
907 use super::*;
908
909 #[test]
910 fn test_config_default() {
911 let config = Config::default();
912 assert!(config.substitute.is_none());
913 assert!(config.processing.is_none());
914 }
915
916 #[test]
917 fn test_author_date_processing() {
918 let processing = Processing::AuthorDate;
919 let config = processing.config();
920 assert!(config.disambiguate.unwrap().year_suffix);
921 assert_eq!(
922 processing.default_bibliography_sort(),
923 Some(crate::presets::SortPreset::AuthorDateTitle)
924 );
925 assert_eq!(
926 config.sort,
927 Some(SortEntry::Preset(
928 crate::presets::SortPreset::AuthorDateTitle
929 ))
930 );
931 }
932
933 #[test]
934 fn test_processing_default_bibliography_sorts() {
935 assert_eq!(Processing::Numeric.default_bibliography_sort(), None);
936 assert_eq!(
937 Processing::Note.default_bibliography_sort(),
938 Some(crate::presets::SortPreset::AuthorTitleDate)
939 );
940 assert_eq!(
941 Processing::Label(LabelConfig::default()).default_bibliography_sort(),
942 Some(crate::presets::SortPreset::AuthorDateTitle)
943 );
944 }
945
946 #[test]
947 fn test_processing_default_citation_sort_policy_is_explicit_only() {
948 assert_eq!(
949 Processing::AuthorDate.default_citation_sort_policy(),
950 CitationSortPolicy::ExplicitOnly
951 );
952 assert_eq!(
953 Processing::Note.default_citation_sort_policy(),
954 CitationSortPolicy::ExplicitOnly
955 );
956 }
957
958 #[test]
959 fn test_substitute_default() {
960 let sub = Substitute::default();
961 assert_eq!(sub.template.len(), 3);
962 }
963
964 #[test]
965 fn test_config_yaml_roundtrip() {
966 let yaml = r#"
967substitute:
968 contributor-role-form: short
969 template:
970 - editor
971 - title
972processing: author-date
973contributors:
974 display-as-sort: first
975 and: symbol
976"#;
977 let config: Config = serde_yaml::from_str(yaml).unwrap();
978 assert!(config.substitute.is_some());
979 assert_eq!(config.processing, Some(Processing::AuthorDate));
980 assert_eq!(
981 config.contributors.as_ref().unwrap().and,
982 Some(AndOptions::Symbol)
983 );
984 }
985
986 #[test]
987 fn test_contributor_config_preset() {
988 let yaml = r#"contributors: apa"#;
990 let config: Config = serde_yaml::from_str(yaml).unwrap();
991 let contributors = config.contributors.unwrap();
992 assert_eq!(contributors.and, Some(AndOptions::Symbol));
993 assert_eq!(contributors.display_as_sort, Some(DisplayAsSort::First));
994 }
995
996 #[test]
997 fn test_role_label_presets_parse_and_resolve_precedence() {
998 let yaml = r#"
999contributors:
1000 role:
1001 preset: short-suffix
1002 roles:
1003 editor:
1004 preset: long-suffix
1005"#;
1006 let config: Config = serde_yaml::from_str(yaml).unwrap();
1007 let contributors = config.contributors.unwrap();
1008
1009 assert_eq!(
1010 contributors.effective_role_label_preset(&crate::template::ContributorRole::Editor),
1011 Some(RoleLabelPreset::LongSuffix)
1012 );
1013 assert_eq!(
1014 contributors.effective_role_label_preset(&crate::template::ContributorRole::Translator),
1015 Some(RoleLabelPreset::ShortSuffix)
1016 );
1017
1018 let yaml_scalar = r#"
1020contributors:
1021 role: short-suffix
1022"#;
1023 let config2: Config = serde_yaml::from_str(yaml_scalar).unwrap();
1024 let contributors2 = config2.contributors.unwrap();
1025
1026 assert_eq!(
1027 contributors2
1028 .effective_role_label_preset(&crate::template::ContributorRole::Translator),
1029 Some(RoleLabelPreset::ShortSuffix)
1030 );
1031 }
1032
1033 #[test]
1034 fn test_role_specific_name_order_override_is_available() {
1035 let yaml = r#"
1036contributors:
1037 role:
1038 roles:
1039 translator:
1040 name-order: given-first
1041"#;
1042 let config: Config = serde_yaml::from_str(yaml).unwrap();
1043 let contributors = config.contributors.unwrap();
1044
1045 assert_eq!(
1046 contributors.effective_role_name_order(&crate::template::ContributorRole::Translator),
1047 Some(&crate::template::NameOrder::GivenFirst)
1048 );
1049 }
1050
1051 #[test]
1052 fn test_date_config_preset() {
1053 let yaml = r#"dates: long"#;
1055 let config: Config = serde_yaml::from_str(yaml).unwrap();
1056 let dates = config.dates.unwrap();
1057 assert_eq!(dates.month, MonthFormat::Long);
1058 }
1059
1060 #[test]
1061 fn test_titles_config_preset() {
1062 let yaml = r#"titles: chicago"#;
1064 let config: Config = serde_yaml::from_str(yaml).unwrap();
1065 let titles = config.titles.unwrap();
1066 assert_eq!(titles.component.unwrap().quote, Some(true));
1067 assert_eq!(titles.monograph.unwrap().emph, Some(true));
1068 }
1069
1070 #[test]
1071 fn test_substitute_config_preset() {
1072 let yaml = r#"substitute: standard"#;
1074 let config: Config = serde_yaml::from_str(yaml).unwrap();
1075 assert!(config.substitute.is_some());
1076 let resolved = config.substitute.unwrap().resolve();
1077 assert_eq!(resolved.template.len(), 3);
1078 assert_eq!(resolved.template[0], SubstituteKey::Editor);
1079 }
1080
1081 #[test]
1082 fn test_substitute_config_explicit() {
1083 let yaml = r#"
1085substitute:
1086 template:
1087 - title
1088 - editor
1089"#;
1090 let config: Config = serde_yaml::from_str(yaml).unwrap();
1091 let resolved = config.substitute.unwrap().resolve();
1092 assert_eq!(resolved.template[0], SubstituteKey::Title);
1093 assert_eq!(resolved.template[1], SubstituteKey::Editor);
1094 }
1095
1096 #[test]
1097 fn test_config_merge_precedence() {
1098 let base_yaml = r#"
1100processing: author-date
1101locale-override: en-US-base
1102contributors:
1103 display-as-sort: first
1104 and: symbol
1105"#;
1106 let mut base: Config = serde_yaml::from_str(base_yaml).unwrap();
1107
1108 let override_yaml = r#"
1110contributors:
1111 and: text
1112locale-override: en-US-chicago
1113"#;
1114 let override_config: Config = serde_yaml::from_str(override_yaml).unwrap();
1115
1116 base.merge(&override_config);
1118
1119 assert_eq!(base.processing, Some(Processing::AuthorDate));
1121 assert_eq!(base.locale_override.as_deref(), Some("en-US-chicago"));
1122
1123 assert_eq!(
1125 base.contributors.as_ref().unwrap().and,
1126 Some(AndOptions::Text)
1127 );
1128 }
1129
1130 #[test]
1131 fn test_config_deserializes_locale_override() {
1132 let config: Config = serde_yaml::from_str("locale-override: en-US-chicago").unwrap();
1133 assert_eq!(config.locale_override.as_deref(), Some("en-US-chicago"));
1134 }
1135
1136 #[test]
1137 fn test_config_merged_convenience() {
1138 let base = Config {
1139 processing: Some(Processing::AuthorDate),
1140 ..Default::default()
1141 };
1142 let override_config = Config {
1143 punctuation_in_quote: true,
1144 ..Default::default()
1145 };
1146
1147 let merged = Config::merged(&base, &override_config);
1148
1149 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1151 assert!(merged.punctuation_in_quote);
1152 }
1153
1154 #[test]
1155 fn test_citation_options_merge_overrides_citation_fields_only() {
1156 let base = Config {
1157 processing: Some(Processing::AuthorDate),
1158 ..Default::default()
1159 };
1160
1161 let overrides = CitationOptions {
1162 strip_periods: Some(true),
1163 locators: Some(LocatorConfig::default()),
1164 ..Default::default()
1165 };
1166
1167 let merged = overrides.merged_with(&base);
1168 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1169 assert!(merged.strip_periods.unwrap_or(false));
1170 assert!(merged.locators.is_some());
1171 }
1172
1173 #[test]
1174 fn test_bibliography_options_merge_projects_shared_fields_only() {
1175 let base = Config {
1176 processing: Some(Processing::AuthorDate),
1177 ..Default::default()
1178 };
1179
1180 let overrides = BibliographyOptions {
1181 entry_suffix: Some(".".to_string()),
1182 separator: Some(", ".to_string()),
1183 suppress_period_after_url: true,
1184 ..Default::default()
1185 };
1186
1187 let merged = overrides.merged_with(&base);
1188 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1189 assert!(merged.locators.is_none());
1190 assert!(merged.notes.is_none());
1191 let bibliography = overrides.to_bibliography_config();
1192 assert_eq!(bibliography.entry_suffix.as_deref(), Some("."));
1193 assert_eq!(bibliography.separator.as_deref(), Some(", "));
1194 assert!(bibliography.suppress_period_after_url);
1195 }
1196
1197 #[test]
1198 fn test_bibliography_options_merge_leaves_shared_base_when_only_shared_overrides_exist() {
1199 let base = Config {
1200 processing: Some(Processing::AuthorDate),
1201 ..Default::default()
1202 };
1203
1204 let overrides = BibliographyOptions {
1205 contributors: Some(ContributorConfig::default()),
1206 ..Default::default()
1207 };
1208
1209 let merged = overrides.merged_with(&base);
1210 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1211 assert!(merged.contributors.is_some());
1212 }
1213
1214 #[test]
1215 fn citation_options_captures_unknown_fields_for_forward_compat() {
1216 let yaml = "future-key: true\n";
1217 let opts: CitationOptions = serde_yaml::from_str(yaml).unwrap();
1218 assert!(opts.unknown_fields.contains_key("future-key"));
1219 }
1220
1221 #[test]
1222 fn bibliography_options_captures_unknown_fields_for_forward_compat() {
1223 let yaml = "future-key: true\n";
1224 let opts: BibliographyOptions = serde_yaml::from_str(yaml).unwrap();
1225 assert!(opts.unknown_fields.contains_key("future-key"));
1226 }
1227
1228 #[test]
1229 fn note_config_captures_unknown_fields_for_forward_compat() {
1230 let yaml = "punctuation: inside\nfuture-key: true\n";
1231 let cfg: NoteConfig = serde_yaml::from_str(yaml).unwrap();
1232 assert!(cfg.unknown_fields.contains_key("future-key"));
1233 assert_eq!(cfg.punctuation, Some(NoteQuotePlacement::Inside));
1234 }
1235}