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,
32 ResolvedIntegralNameMemoryConfig, ShortNameDisplay, SubsequentNameForm,
33};
34pub use localization::{Localize, MonthFormat, Scope};
35pub use locators::{
36 LabelForm, LabelRepeat, LocatorConfig, LocatorConfigEntry, LocatorKindConfig, LocatorPattern,
37 LocatorPreset, TypeClass,
38};
39pub use multilingual::{MultilingualConfig, MultilingualMode, ScriptConfig};
40pub use processing::{
41 CitationSortPolicy, Disambiguation, Group, LabelConfig, LabelParams, LabelPreset, Processing,
42 ProcessingCustom, Sort, SortEntry, SortKey, SortSpec,
43};
44pub use scoped::{
45 BibliographyLabelMode, BibliographyLabelWrap, CitationGroupDelimiter, DatePosition, LabelWrap,
46 RepeatedAuthorRendering, TitleTerminator,
47};
48pub use substitute::{Substitute, SubstituteConfig, SubstituteKey};
49
50use crate::template::DelimiterPunctuation;
51#[cfg(feature = "schema")]
52use schemars::JsonSchema;
53use serde::{Deserialize, Serialize};
54use std::collections::HashMap;
55
56#[derive(Debug, Default, PartialEq, Clone, Serialize)]
58#[cfg_attr(feature = "schema", derive(JsonSchema))]
59#[serde(rename_all = "kebab-case")]
60pub struct Config {
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub substitute: Option<SubstituteConfig>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub processing: Option<Processing>,
67 #[serde(skip_serializing_if = "Option::is_none")]
73 pub locale_override: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub localize: Option<Localize>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub multilingual: Option<MultilingualConfig>,
80 #[serde(
83 skip_serializing_if = "Option::is_none",
84 deserialize_with = "deserialize_contributor_config",
85 default
86 )]
87 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
88 pub contributors: Option<ContributorConfig>,
89 #[serde(
92 skip_serializing_if = "Option::is_none",
93 deserialize_with = "deserialize_date_config",
94 default
95 )]
96 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
97 pub dates: Option<DateConfig>,
98 #[serde(
101 skip_serializing_if = "Option::is_none",
102 deserialize_with = "deserialize_titles_config",
103 default
104 )]
105 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
106 pub titles: Option<crate::options::titles::TitlesConfig>,
107 #[serde(
110 skip_serializing_if = "Option::is_none",
111 deserialize_with = "deserialize_locator_config",
112 default
113 )]
114 #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
115 pub locators: Option<LocatorConfig>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub page_range_format: Option<PageRangeFormat>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub links: Option<LinksConfig>,
122 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
126 pub punctuation_in_quote: bool,
127 #[serde(skip_serializing_if = "Option::is_none")]
131 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
132 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
134 pub strip_periods: Option<bool>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub notes: Option<NoteConfig>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub integral_name_memory: Option<IntegralNameMemoryConfig>,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub custom: Option<HashMap<String, serde_json::Value>>,
144 #[serde(
148 flatten,
149 default,
150 skip_serializing_if = "std::collections::BTreeMap::is_empty"
151 )]
152 #[cfg_attr(feature = "schema", schemars(skip))]
153 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
154}
155
156#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
158#[cfg_attr(feature = "schema", derive(JsonSchema))]
159#[serde(rename_all = "kebab-case")]
160pub struct CitationOptions {
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub substitute: Option<SubstituteConfig>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub processing: Option<Processing>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub localize: Option<Localize>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub multilingual: Option<MultilingualConfig>,
173 #[serde(
175 skip_serializing_if = "Option::is_none",
176 deserialize_with = "deserialize_contributor_config",
177 default
178 )]
179 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
180 pub contributors: Option<ContributorConfig>,
181 #[serde(
183 skip_serializing_if = "Option::is_none",
184 deserialize_with = "deserialize_date_config",
185 default
186 )]
187 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
188 pub dates: Option<DateConfig>,
189 #[serde(
191 skip_serializing_if = "Option::is_none",
192 deserialize_with = "deserialize_titles_config",
193 default
194 )]
195 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
196 pub titles: Option<crate::options::titles::TitlesConfig>,
197 #[serde(
199 skip_serializing_if = "Option::is_none",
200 deserialize_with = "deserialize_locator_config",
201 default
202 )]
203 #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
204 pub locators: Option<LocatorConfig>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub page_range_format: Option<PageRangeFormat>,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub links: Option<LinksConfig>,
211 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
213 pub punctuation_in_quote: bool,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
217 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
219 pub strip_periods: Option<bool>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub notes: Option<NoteConfig>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub integral_name_memory: Option<IntegralNameMemoryConfig>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub label_wrap: Option<LabelWrap>,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub group_delimiter: Option<CitationGroupDelimiter>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub custom: Option<HashMap<String, serde_json::Value>>,
235 #[serde(
239 flatten,
240 default,
241 skip_serializing_if = "std::collections::BTreeMap::is_empty"
242 )]
243 #[cfg_attr(feature = "schema", schemars(skip))]
244 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
245}
246
247#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
249#[cfg_attr(feature = "schema", derive(JsonSchema))]
250#[serde(rename_all = "kebab-case")]
251pub struct BibliographyOptions {
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub substitute: Option<SubstituteConfig>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub processing: Option<Processing>,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub localize: Option<Localize>,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub multilingual: Option<MultilingualConfig>,
264 #[serde(
266 skip_serializing_if = "Option::is_none",
267 deserialize_with = "deserialize_contributor_config",
268 default
269 )]
270 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
271 pub contributors: Option<ContributorConfig>,
272 #[serde(
274 skip_serializing_if = "Option::is_none",
275 deserialize_with = "deserialize_date_config",
276 default
277 )]
278 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
279 pub dates: Option<DateConfig>,
280 #[serde(
282 skip_serializing_if = "Option::is_none",
283 deserialize_with = "deserialize_titles_config",
284 default
285 )]
286 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
287 pub titles: Option<crate::options::titles::TitlesConfig>,
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub page_range_format: Option<PageRangeFormat>,
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub article_journal: Option<ArticleJournalBibliographyConfig>,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub subsequent_author_substitute: Option<String>,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub subsequent_author_substitute_rule: Option<SubsequentAuthorSubstituteRule>,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub hanging_indent: Option<bool>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub entry_suffix: Option<String>,
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub separator: Option<String>,
309 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
311 pub suppress_period_after_url: bool,
312 #[serde(skip_serializing_if = "Option::is_none")]
314 pub compound_numeric: Option<bibliography::CompoundNumericConfig>,
315 #[serde(skip_serializing_if = "Option::is_none")]
317 pub sort_partitioning: Option<bibliography::BibliographySortPartitioning>,
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub links: Option<LinksConfig>,
321 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
323 pub punctuation_in_quote: bool,
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub label_mode: Option<BibliographyLabelMode>,
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub label_wrap: Option<BibliographyLabelWrap>,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub date_position: Option<DatePosition>,
336 #[serde(skip_serializing_if = "Option::is_none")]
338 pub title_terminator: Option<TitleTerminator>,
339 #[serde(skip_serializing_if = "Option::is_none")]
341 pub repeated_author_rendering: Option<RepeatedAuthorRendering>,
342 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
344 pub strip_periods: Option<bool>,
345 #[serde(skip_serializing_if = "Option::is_none")]
347 pub custom: Option<HashMap<String, serde_json::Value>>,
348 #[serde(
352 flatten,
353 default,
354 skip_serializing_if = "std::collections::BTreeMap::is_empty"
355 )]
356 #[cfg_attr(feature = "schema", schemars(skip))]
357 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
358}
359
360#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
362#[cfg_attr(feature = "schema", derive(JsonSchema))]
363#[serde(rename_all = "kebab-case")]
364pub struct NoteConfig {
365 #[serde(skip_serializing_if = "Option::is_none")]
368 pub punctuation: Option<NoteQuotePlacement>,
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub number: Option<NoteNumberPlacement>,
372 #[serde(skip_serializing_if = "Option::is_none")]
375 pub order: Option<NoteMarkerOrder>,
376 #[serde(
380 flatten,
381 default,
382 skip_serializing_if = "std::collections::BTreeMap::is_empty"
383 )]
384 #[cfg_attr(feature = "schema", schemars(skip))]
385 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
386}
387
388#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
390#[cfg_attr(feature = "schema", derive(JsonSchema))]
391#[serde(rename_all = "kebab-case")]
392pub enum NoteQuotePlacement {
393 Inside,
395 Outside,
397 Adaptive,
401}
402
403#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
405#[cfg_attr(feature = "schema", derive(JsonSchema))]
406#[serde(rename_all = "kebab-case")]
407pub enum NoteNumberPlacement {
408 Inside,
410 Outside,
412 Same,
415}
416
417#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
419#[cfg_attr(feature = "schema", derive(JsonSchema))]
420#[serde(rename_all = "kebab-case")]
421pub enum NoteMarkerOrder {
422 Before,
424 After,
426}
427
428#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
430#[cfg_attr(feature = "schema", derive(JsonSchema))]
431#[serde(rename_all = "kebab-case")]
432#[non_exhaustive]
433pub enum PageRangeFormat {
434 #[default]
436 Expanded,
437 Minimal,
439 MinimalTwo,
441 Chicago,
443 Chicago16,
445}
446
447pub mod titles;
448
449pub use titles::{TextCase, TitleRendering, TitlesConfig, TitlesConfigEntry};
450
451#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
453#[cfg_attr(feature = "schema", derive(JsonSchema))]
454#[serde(rename_all = "kebab-case")]
455pub struct LinksConfig {
456 #[serde(skip_serializing_if = "Option::is_none")]
458 pub doi: Option<bool>,
459 #[serde(skip_serializing_if = "Option::is_none")]
461 pub url: Option<bool>,
462 #[serde(skip_serializing_if = "Option::is_none")]
464 pub target: Option<LinkTarget>,
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub anchor: Option<LinkAnchor>,
468}
469
470#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
472#[cfg_attr(feature = "schema", derive(JsonSchema))]
473#[serde(rename_all = "kebab-case")]
474pub enum LinkTarget {
475 Url,
476 Doi,
477 UrlOrDoi,
478 Pubmed,
479 Pmcid,
480}
481
482#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
484#[cfg_attr(feature = "schema", derive(JsonSchema))]
485#[serde(rename_all = "kebab-case")]
486pub enum LinkAnchor {
487 Title,
489 Url,
491 Doi,
493 Component,
495 Entry,
497}
498
499impl Config {
500 pub fn merge(&mut self, other: &Config) {
505 crate::merge_options!(
506 self,
507 other,
508 processing,
509 locale_override,
510 localize,
511 multilingual,
512 dates,
513 titles,
514 locators,
515 page_range_format,
516 links,
517 volume_pages_delimiter,
518 locale_override,
519 strip_periods,
520 notes,
521 integral_name_memory,
522 custom,
523 );
524
525 if let Some(other_substitute) = &other.substitute {
526 if let Some(this_substitute) = &self.substitute {
527 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
528 } else {
529 self.substitute = Some(other_substitute.clone());
530 }
531 }
532
533 if let Some(other_contributors) = &other.contributors {
534 if let Some(this_contributors) = &mut self.contributors {
535 this_contributors.merge(other_contributors);
536 } else {
537 self.contributors = Some(other_contributors.clone());
538 }
539 }
540
541 if other.punctuation_in_quote {
542 self.punctuation_in_quote = true;
543 }
544 }
545
546 pub fn merged(base: &Config, override_config: &Config) -> Config {
550 let mut result = base.clone();
551 result.merge(override_config);
552 result
553 }
554}
555
556impl CitationOptions {
557 #[must_use]
559 pub fn to_config(&self) -> Config {
560 Config {
561 substitute: self.substitute.clone(),
562 processing: self.processing.clone(),
563 locale_override: None,
564 localize: self.localize.clone(),
565 multilingual: self.multilingual.clone(),
566 contributors: self.contributors.clone(),
567 dates: self.dates.clone(),
568 titles: self.titles.clone(),
569 locators: self.locators.clone(),
570 page_range_format: self.page_range_format.clone(),
571 links: self.links.clone(),
572 punctuation_in_quote: self.punctuation_in_quote,
573 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
574 strip_periods: self.strip_periods,
575 notes: self.notes.clone(),
576 integral_name_memory: self.integral_name_memory.clone(),
577 custom: self.custom.clone(),
578 unknown_fields: std::collections::BTreeMap::new(),
579 }
580 }
581
582 #[must_use]
584 pub fn merged_with(&self, base: &Config) -> Config {
585 Config::merged(base, &self.to_config())
586 }
587}
588
589impl BibliographyOptions {
590 #[must_use]
592 pub fn to_bibliography_config(&self) -> BibliographyConfig {
593 BibliographyConfig {
594 article_journal: self.article_journal.clone(),
595 subsequent_author_substitute: self.subsequent_author_substitute.clone(),
596 subsequent_author_substitute_rule: self.subsequent_author_substitute_rule.clone(),
597 hanging_indent: self.hanging_indent,
598 entry_suffix: self.entry_suffix.clone(),
599 separator: self.separator.clone(),
600 suppress_period_after_url: self.suppress_period_after_url,
601 custom: None,
602 compound_numeric: self.compound_numeric.clone(),
603 sort_partitioning: self.sort_partitioning.clone(),
604 unknown_fields: std::collections::BTreeMap::new(),
605 }
606 }
607
608 #[must_use]
610 pub fn to_config(&self) -> Config {
611 Config {
612 substitute: self.substitute.clone(),
613 processing: self.processing.clone(),
614 locale_override: None,
615 localize: self.localize.clone(),
616 multilingual: self.multilingual.clone(),
617 contributors: self.contributors.clone(),
618 dates: self.dates.clone(),
619 titles: self.titles.clone(),
620 locators: None,
621 page_range_format: self.page_range_format.clone(),
622 links: self.links.clone(),
623 punctuation_in_quote: self.punctuation_in_quote,
624 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
625 strip_periods: self.strip_periods,
626 notes: None,
627 integral_name_memory: None,
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
640fn deserialize_contributor_config<'de, D>(
642 deserializer: D,
643) -> Result<Option<ContributorConfig>, D::Error>
644where
645 D: serde::Deserializer<'de>,
646{
647 let value: Option<ContributorConfigEntry> = Option::deserialize(deserializer)?;
648 Ok(value.map(|entry| entry.resolve()))
649}
650
651fn deserialize_date_config<'de, D>(deserializer: D) -> Result<Option<DateConfig>, D::Error>
653where
654 D: serde::Deserializer<'de>,
655{
656 let value: Option<DateConfigEntry> = Option::deserialize(deserializer)?;
657 Ok(value.map(|entry| entry.resolve()))
658}
659
660fn deserialize_titles_config<'de, D>(
662 deserializer: D,
663) -> Result<Option<crate::options::titles::TitlesConfig>, D::Error>
664where
665 D: serde::Deserializer<'de>,
666{
667 let value: Option<crate::options::titles::TitlesConfigEntry> =
668 Option::deserialize(deserializer)?;
669 Ok(value.map(|entry| entry.resolve()))
670}
671
672fn deserialize_locator_config<'de, D>(deserializer: D) -> Result<Option<LocatorConfig>, D::Error>
674where
675 D: serde::Deserializer<'de>,
676{
677 let value: Option<LocatorConfigEntry> = Option::deserialize(deserializer)?;
678 Ok(value.map(|entry| entry.resolve()))
679}
680
681impl<'de> Deserialize<'de> for Config {
682 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
683 where
684 D: serde::Deserializer<'de>,
685 {
686 #[derive(Deserialize)]
687 #[serde(rename_all = "kebab-case")]
688 struct ConfigWire {
689 #[serde(skip_serializing_if = "Option::is_none")]
690 substitute: Option<SubstituteConfig>,
691 #[serde(skip_serializing_if = "Option::is_none")]
692 processing: Option<Processing>,
693 #[serde(skip_serializing_if = "Option::is_none")]
694 locale_override: Option<String>,
695 #[serde(skip_serializing_if = "Option::is_none")]
696 localize: Option<Localize>,
697 #[serde(skip_serializing_if = "Option::is_none")]
698 multilingual: Option<MultilingualConfig>,
699 #[serde(
700 skip_serializing_if = "Option::is_none",
701 deserialize_with = "deserialize_contributor_config",
702 default
703 )]
704 contributors: Option<ContributorConfig>,
705 #[serde(
706 skip_serializing_if = "Option::is_none",
707 deserialize_with = "deserialize_date_config",
708 default
709 )]
710 dates: Option<DateConfig>,
711 #[serde(
712 skip_serializing_if = "Option::is_none",
713 deserialize_with = "deserialize_titles_config",
714 default
715 )]
716 titles: Option<crate::options::titles::TitlesConfig>,
717 #[serde(
718 skip_serializing_if = "Option::is_none",
719 deserialize_with = "deserialize_locator_config",
720 default
721 )]
722 locators: Option<LocatorConfig>,
723 #[serde(skip_serializing_if = "Option::is_none")]
724 page_range_format: Option<PageRangeFormat>,
725 #[serde(skip_serializing_if = "Option::is_none")]
726 links: Option<LinksConfig>,
727 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
728 punctuation_in_quote: bool,
729 #[serde(skip_serializing_if = "Option::is_none")]
730 volume_pages_delimiter: Option<DelimiterPunctuation>,
731 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
732 strip_periods: Option<bool>,
733 #[serde(skip_serializing_if = "Option::is_none")]
734 notes: Option<NoteConfig>,
735 #[serde(skip_serializing_if = "Option::is_none")]
736 integral_name_memory: Option<IntegralNameMemoryConfig>,
737 #[serde(default)]
738 profile: Option<serde_yaml::Value>,
739 #[serde(skip_serializing_if = "Option::is_none")]
740 custom: Option<HashMap<String, serde_json::Value>>,
741 #[serde(flatten)]
742 unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
743 }
744
745 let wire = ConfigWire::deserialize(deserializer)?;
746 if wire.profile.is_some() {
747 return Err(serde::de::Error::custom(
748 "`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`",
749 ));
750 }
751
752 Ok(Self {
753 substitute: wire.substitute,
754 processing: wire.processing,
755 locale_override: wire.locale_override,
756 localize: wire.localize,
757 multilingual: wire.multilingual,
758 contributors: wire.contributors,
759 dates: wire.dates,
760 titles: wire.titles,
761 locators: wire.locators,
762 page_range_format: wire.page_range_format,
763 links: wire.links,
764 punctuation_in_quote: wire.punctuation_in_quote,
765 volume_pages_delimiter: wire.volume_pages_delimiter,
766 strip_periods: wire.strip_periods,
767 notes: wire.notes,
768 integral_name_memory: wire.integral_name_memory,
769 custom: wire.custom,
770 unknown_fields: wire.unknown_fields,
771 })
772 }
773}
774
775#[cfg(test)]
776#[allow(
777 clippy::unwrap_used,
778 clippy::expect_used,
779 clippy::panic,
780 clippy::indexing_slicing,
781 clippy::todo,
782 clippy::unimplemented,
783 clippy::unreachable,
784 clippy::get_unwrap,
785 reason = "Panicking is acceptable and often desired in tests."
786)]
787mod tests {
788 use super::*;
789
790 #[test]
791 fn test_config_default() {
792 let config = Config::default();
793 assert!(config.substitute.is_none());
794 assert!(config.processing.is_none());
795 }
796
797 #[test]
798 fn test_author_date_processing() {
799 let processing = Processing::AuthorDate;
800 let config = processing.config();
801 assert!(config.disambiguate.unwrap().year_suffix);
802 assert_eq!(
803 processing.default_bibliography_sort(),
804 Some(crate::presets::SortPreset::AuthorDateTitle)
805 );
806 assert_eq!(
807 config.sort,
808 Some(SortEntry::Preset(
809 crate::presets::SortPreset::AuthorDateTitle
810 ))
811 );
812 }
813
814 #[test]
815 fn test_processing_default_bibliography_sorts() {
816 assert_eq!(Processing::Numeric.default_bibliography_sort(), None);
817 assert_eq!(
818 Processing::Note.default_bibliography_sort(),
819 Some(crate::presets::SortPreset::AuthorTitleDate)
820 );
821 assert_eq!(
822 Processing::Label(LabelConfig::default()).default_bibliography_sort(),
823 Some(crate::presets::SortPreset::AuthorDateTitle)
824 );
825 }
826
827 #[test]
828 fn test_processing_default_citation_sort_policy_is_explicit_only() {
829 assert_eq!(
830 Processing::AuthorDate.default_citation_sort_policy(),
831 CitationSortPolicy::ExplicitOnly
832 );
833 assert_eq!(
834 Processing::Note.default_citation_sort_policy(),
835 CitationSortPolicy::ExplicitOnly
836 );
837 }
838
839 #[test]
840 fn test_substitute_default() {
841 let sub = Substitute::default();
842 assert_eq!(sub.template.len(), 3);
843 }
844
845 #[test]
846 fn test_config_yaml_roundtrip() {
847 let yaml = r#"
848substitute:
849 contributor-role-form: short
850 template:
851 - editor
852 - title
853processing: author-date
854contributors:
855 display-as-sort: first
856 and: symbol
857"#;
858 let config: Config = serde_yaml::from_str(yaml).unwrap();
859 assert!(config.substitute.is_some());
860 assert_eq!(config.processing, Some(Processing::AuthorDate));
861 assert_eq!(
862 config.contributors.as_ref().unwrap().and,
863 Some(AndOptions::Symbol)
864 );
865 }
866
867 #[test]
868 fn test_contributor_config_preset() {
869 let yaml = r#"contributors: apa"#;
871 let config: Config = serde_yaml::from_str(yaml).unwrap();
872 let contributors = config.contributors.unwrap();
873 assert_eq!(contributors.and, Some(AndOptions::Symbol));
874 assert_eq!(contributors.display_as_sort, Some(DisplayAsSort::First));
875 }
876
877 #[test]
878 fn test_role_label_presets_parse_and_resolve_precedence() {
879 let yaml = r#"
880contributors:
881 role:
882 preset: short-suffix
883 roles:
884 editor:
885 preset: long-suffix
886"#;
887 let config: Config = serde_yaml::from_str(yaml).unwrap();
888 let contributors = config.contributors.unwrap();
889
890 assert_eq!(
891 contributors.effective_role_label_preset(&crate::template::ContributorRole::Editor),
892 Some(RoleLabelPreset::LongSuffix)
893 );
894 assert_eq!(
895 contributors.effective_role_label_preset(&crate::template::ContributorRole::Translator),
896 Some(RoleLabelPreset::ShortSuffix)
897 );
898
899 let yaml_scalar = r#"
901contributors:
902 role: short-suffix
903"#;
904 let config2: Config = serde_yaml::from_str(yaml_scalar).unwrap();
905 let contributors2 = config2.contributors.unwrap();
906
907 assert_eq!(
908 contributors2
909 .effective_role_label_preset(&crate::template::ContributorRole::Translator),
910 Some(RoleLabelPreset::ShortSuffix)
911 );
912 }
913
914 #[test]
915 fn test_role_specific_name_order_override_is_available() {
916 let yaml = r#"
917contributors:
918 role:
919 roles:
920 translator:
921 name-order: given-first
922"#;
923 let config: Config = serde_yaml::from_str(yaml).unwrap();
924 let contributors = config.contributors.unwrap();
925
926 assert_eq!(
927 contributors.effective_role_name_order(&crate::template::ContributorRole::Translator),
928 Some(&crate::template::NameOrder::GivenFirst)
929 );
930 }
931
932 #[test]
933 fn test_date_config_preset() {
934 let yaml = r#"dates: long"#;
936 let config: Config = serde_yaml::from_str(yaml).unwrap();
937 let dates = config.dates.unwrap();
938 assert_eq!(dates.month, MonthFormat::Long);
939 }
940
941 #[test]
942 fn test_titles_config_preset() {
943 let yaml = r#"titles: chicago"#;
945 let config: Config = serde_yaml::from_str(yaml).unwrap();
946 let titles = config.titles.unwrap();
947 assert_eq!(titles.component.unwrap().quote, Some(true));
948 assert_eq!(titles.monograph.unwrap().emph, Some(true));
949 }
950
951 #[test]
952 fn test_substitute_config_preset() {
953 let yaml = r#"substitute: standard"#;
955 let config: Config = serde_yaml::from_str(yaml).unwrap();
956 assert!(config.substitute.is_some());
957 let resolved = config.substitute.unwrap().resolve();
958 assert_eq!(resolved.template.len(), 3);
959 assert_eq!(resolved.template[0], SubstituteKey::Editor);
960 }
961
962 #[test]
963 fn test_substitute_config_explicit() {
964 let yaml = r#"
966substitute:
967 template:
968 - title
969 - editor
970"#;
971 let config: Config = serde_yaml::from_str(yaml).unwrap();
972 let resolved = config.substitute.unwrap().resolve();
973 assert_eq!(resolved.template[0], SubstituteKey::Title);
974 assert_eq!(resolved.template[1], SubstituteKey::Editor);
975 }
976
977 #[test]
978 fn test_config_merge_precedence() {
979 let base_yaml = r#"
981processing: author-date
982locale-override: en-US-base
983contributors:
984 display-as-sort: first
985 and: symbol
986"#;
987 let mut base: Config = serde_yaml::from_str(base_yaml).unwrap();
988
989 let override_yaml = r#"
991contributors:
992 and: text
993locale-override: en-US-chicago
994"#;
995 let override_config: Config = serde_yaml::from_str(override_yaml).unwrap();
996
997 base.merge(&override_config);
999
1000 assert_eq!(base.processing, Some(Processing::AuthorDate));
1002 assert_eq!(base.locale_override.as_deref(), Some("en-US-chicago"));
1003
1004 assert_eq!(
1006 base.contributors.as_ref().unwrap().and,
1007 Some(AndOptions::Text)
1008 );
1009 }
1010
1011 #[test]
1012 fn test_config_deserializes_locale_override() {
1013 let config: Config = serde_yaml::from_str("locale-override: en-US-chicago").unwrap();
1014 assert_eq!(config.locale_override.as_deref(), Some("en-US-chicago"));
1015 }
1016
1017 #[test]
1018 fn test_config_merged_convenience() {
1019 let base = Config {
1020 processing: Some(Processing::AuthorDate),
1021 ..Default::default()
1022 };
1023 let override_config = Config {
1024 punctuation_in_quote: true,
1025 ..Default::default()
1026 };
1027
1028 let merged = Config::merged(&base, &override_config);
1029
1030 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1032 assert!(merged.punctuation_in_quote);
1033 }
1034
1035 #[test]
1036 fn test_citation_options_merge_overrides_citation_fields_only() {
1037 let base = Config {
1038 processing: Some(Processing::AuthorDate),
1039 ..Default::default()
1040 };
1041
1042 let overrides = CitationOptions {
1043 strip_periods: Some(true),
1044 locators: Some(LocatorConfig::default()),
1045 ..Default::default()
1046 };
1047
1048 let merged = overrides.merged_with(&base);
1049 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1050 assert!(merged.strip_periods.unwrap_or(false));
1051 assert!(merged.locators.is_some());
1052 }
1053
1054 #[test]
1055 fn test_bibliography_options_merge_projects_shared_fields_only() {
1056 let base = Config {
1057 processing: Some(Processing::AuthorDate),
1058 ..Default::default()
1059 };
1060
1061 let overrides = BibliographyOptions {
1062 entry_suffix: Some(".".to_string()),
1063 separator: Some(", ".to_string()),
1064 suppress_period_after_url: true,
1065 ..Default::default()
1066 };
1067
1068 let merged = overrides.merged_with(&base);
1069 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1070 assert!(merged.locators.is_none());
1071 assert!(merged.notes.is_none());
1072 let bibliography = overrides.to_bibliography_config();
1073 assert_eq!(bibliography.entry_suffix.as_deref(), Some("."));
1074 assert_eq!(bibliography.separator.as_deref(), Some(", "));
1075 assert!(bibliography.suppress_period_after_url);
1076 }
1077
1078 #[test]
1079 fn test_bibliography_options_merge_leaves_shared_base_when_only_shared_overrides_exist() {
1080 let base = Config {
1081 processing: Some(Processing::AuthorDate),
1082 ..Default::default()
1083 };
1084
1085 let overrides = BibliographyOptions {
1086 contributors: Some(ContributorConfig::default()),
1087 ..Default::default()
1088 };
1089
1090 let merged = overrides.merged_with(&base);
1091 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1092 assert!(merged.contributors.is_some());
1093 }
1094
1095 #[test]
1096 fn citation_options_captures_unknown_fields_for_forward_compat() {
1097 let yaml = "future-key: true\n";
1098 let opts: CitationOptions = serde_yaml::from_str(yaml).unwrap();
1099 assert!(opts.unknown_fields.contains_key("future-key"));
1100 }
1101
1102 #[test]
1103 fn bibliography_options_captures_unknown_fields_for_forward_compat() {
1104 let yaml = "future-key: true\n";
1105 let opts: BibliographyOptions = serde_yaml::from_str(yaml).unwrap();
1106 assert!(opts.unknown_fields.contains_key("future-key"));
1107 }
1108
1109 #[test]
1110 fn note_config_captures_unknown_fields_for_forward_compat() {
1111 let yaml = "punctuation: inside\nfuture-key: true\n";
1112 let cfg: NoteConfig = serde_yaml::from_str(yaml).unwrap();
1113 assert!(cfg.unknown_fields.contains_key("future-key"));
1114 assert_eq!(cfg.punctuation, Some(NoteQuotePlacement::Inside));
1115 }
1116}