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
598impl BibliographyOptions {
599 #[must_use]
601 pub fn to_bibliography_config(&self) -> BibliographyConfig {
602 BibliographyConfig {
603 article_journal: self.article_journal.clone(),
604 subsequent_author_substitute: self.subsequent_author_substitute.clone(),
605 subsequent_author_substitute_rule: self.subsequent_author_substitute_rule.clone(),
606 hanging_indent: self.hanging_indent,
607 entry_suffix: self.entry_suffix.clone(),
608 separator: self.separator.clone(),
609 suppress_period_after_url: self.suppress_period_after_url,
610 custom: None,
611 compound_numeric: self.compound_numeric.clone(),
612 sort_partitioning: self.sort_partitioning.clone(),
613 unknown_fields: std::collections::BTreeMap::new(),
614 }
615 }
616
617 #[must_use]
619 pub fn to_config(&self) -> Config {
620 Config {
621 substitute: self.substitute.clone(),
622 processing: self.processing.clone(),
623 locale_override: None,
624 localize: self.localize.clone(),
625 multilingual: self.multilingual.clone(),
626 contributors: self.contributors.clone(),
627 dates: self.dates.clone(),
628 titles: self.titles.clone(),
629 locators: None,
630 page_range_format: self.page_range_format.clone(),
631 links: self.links.clone(),
632 punctuation_in_quote: self.punctuation_in_quote,
633 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
634 strip_periods: self.strip_periods,
635 notes: None,
636 integral_name_memory: None,
637 org_abbreviation_memory: None,
638 custom: self.custom.clone(),
639 unknown_fields: std::collections::BTreeMap::new(),
640 }
641 }
642
643 #[must_use]
645 pub fn merged_with(&self, base: &Config) -> Config {
646 Config::merged(base, &self.to_config())
647 }
648}
649
650fn deserialize_contributor_config<'de, D>(
652 deserializer: D,
653) -> Result<Option<ContributorConfig>, D::Error>
654where
655 D: serde::Deserializer<'de>,
656{
657 let value: Option<ContributorConfigEntry> = Option::deserialize(deserializer)?;
658 Ok(value.map(|entry| entry.resolve()))
659}
660
661fn deserialize_date_config<'de, D>(deserializer: D) -> Result<Option<DateConfig>, D::Error>
663where
664 D: serde::Deserializer<'de>,
665{
666 let value: Option<DateConfigEntry> = Option::deserialize(deserializer)?;
667 Ok(value.map(|entry| entry.resolve()))
668}
669
670fn deserialize_titles_config<'de, D>(
672 deserializer: D,
673) -> Result<Option<crate::options::titles::TitlesConfig>, D::Error>
674where
675 D: serde::Deserializer<'de>,
676{
677 let value: Option<crate::options::titles::TitlesConfigEntry> =
678 Option::deserialize(deserializer)?;
679 Ok(value.map(|entry| entry.resolve()))
680}
681
682fn deserialize_locator_config<'de, D>(deserializer: D) -> Result<Option<LocatorConfig>, D::Error>
684where
685 D: serde::Deserializer<'de>,
686{
687 let value: Option<LocatorConfigEntry> = Option::deserialize(deserializer)?;
688 Ok(value.map(|entry| entry.resolve()))
689}
690
691impl<'de> Deserialize<'de> for Config {
692 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
693 where
694 D: serde::Deserializer<'de>,
695 {
696 #[derive(Deserialize)]
697 #[serde(rename_all = "kebab-case")]
698 struct ConfigWire {
699 #[serde(skip_serializing_if = "Option::is_none")]
700 substitute: Option<SubstituteConfig>,
701 #[serde(skip_serializing_if = "Option::is_none")]
702 processing: Option<Processing>,
703 #[serde(skip_serializing_if = "Option::is_none")]
704 locale_override: Option<String>,
705 #[serde(skip_serializing_if = "Option::is_none")]
706 localize: Option<Localize>,
707 #[serde(skip_serializing_if = "Option::is_none")]
708 multilingual: Option<MultilingualConfig>,
709 #[serde(
710 skip_serializing_if = "Option::is_none",
711 deserialize_with = "deserialize_contributor_config",
712 default
713 )]
714 contributors: Option<ContributorConfig>,
715 #[serde(
716 skip_serializing_if = "Option::is_none",
717 deserialize_with = "deserialize_date_config",
718 default
719 )]
720 dates: Option<DateConfig>,
721 #[serde(
722 skip_serializing_if = "Option::is_none",
723 deserialize_with = "deserialize_titles_config",
724 default
725 )]
726 titles: Option<crate::options::titles::TitlesConfig>,
727 #[serde(
728 skip_serializing_if = "Option::is_none",
729 deserialize_with = "deserialize_locator_config",
730 default
731 )]
732 locators: Option<LocatorConfig>,
733 #[serde(skip_serializing_if = "Option::is_none")]
734 page_range_format: Option<PageRangeFormat>,
735 #[serde(skip_serializing_if = "Option::is_none")]
736 links: Option<LinksConfig>,
737 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
738 punctuation_in_quote: bool,
739 #[serde(skip_serializing_if = "Option::is_none")]
740 volume_pages_delimiter: Option<DelimiterPunctuation>,
741 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
742 strip_periods: Option<bool>,
743 #[serde(skip_serializing_if = "Option::is_none")]
744 notes: Option<NoteConfig>,
745 #[serde(skip_serializing_if = "Option::is_none")]
746 integral_name_memory: Option<IntegralNameMemoryConfig>,
747 #[serde(skip_serializing_if = "Option::is_none")]
748 org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
749 #[serde(default)]
750 profile: Option<serde_yaml::Value>,
751 #[serde(skip_serializing_if = "Option::is_none")]
752 custom: Option<HashMap<String, serde_json::Value>>,
753 #[serde(flatten)]
754 unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
755 }
756
757 let wire = ConfigWire::deserialize(deserializer)?;
758 if wire.profile.is_some() {
759 return Err(serde::de::Error::custom(
760 "`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`",
761 ));
762 }
763
764 Ok(Self {
765 substitute: wire.substitute,
766 processing: wire.processing,
767 locale_override: wire.locale_override,
768 localize: wire.localize,
769 multilingual: wire.multilingual,
770 contributors: wire.contributors,
771 dates: wire.dates,
772 titles: wire.titles,
773 locators: wire.locators,
774 page_range_format: wire.page_range_format,
775 links: wire.links,
776 punctuation_in_quote: wire.punctuation_in_quote,
777 volume_pages_delimiter: wire.volume_pages_delimiter,
778 strip_periods: wire.strip_periods,
779 notes: wire.notes,
780 integral_name_memory: wire.integral_name_memory,
781 org_abbreviation_memory: wire.org_abbreviation_memory,
782 custom: wire.custom,
783 unknown_fields: wire.unknown_fields,
784 })
785 }
786}
787
788#[cfg(test)]
789#[allow(
790 clippy::unwrap_used,
791 clippy::expect_used,
792 clippy::panic,
793 clippy::indexing_slicing,
794 clippy::todo,
795 clippy::unimplemented,
796 clippy::unreachable,
797 clippy::get_unwrap,
798 reason = "Panicking is acceptable and often desired in tests."
799)]
800mod tests {
801 use super::*;
802
803 #[test]
804 fn test_config_default() {
805 let config = Config::default();
806 assert!(config.substitute.is_none());
807 assert!(config.processing.is_none());
808 }
809
810 #[test]
811 fn test_author_date_processing() {
812 let processing = Processing::AuthorDate;
813 let config = processing.config();
814 assert!(config.disambiguate.unwrap().year_suffix);
815 assert_eq!(
816 processing.default_bibliography_sort(),
817 Some(crate::presets::SortPreset::AuthorDateTitle)
818 );
819 assert_eq!(
820 config.sort,
821 Some(SortEntry::Preset(
822 crate::presets::SortPreset::AuthorDateTitle
823 ))
824 );
825 }
826
827 #[test]
828 fn test_processing_default_bibliography_sorts() {
829 assert_eq!(Processing::Numeric.default_bibliography_sort(), None);
830 assert_eq!(
831 Processing::Note.default_bibliography_sort(),
832 Some(crate::presets::SortPreset::AuthorTitleDate)
833 );
834 assert_eq!(
835 Processing::Label(LabelConfig::default()).default_bibliography_sort(),
836 Some(crate::presets::SortPreset::AuthorDateTitle)
837 );
838 }
839
840 #[test]
841 fn test_processing_default_citation_sort_policy_is_explicit_only() {
842 assert_eq!(
843 Processing::AuthorDate.default_citation_sort_policy(),
844 CitationSortPolicy::ExplicitOnly
845 );
846 assert_eq!(
847 Processing::Note.default_citation_sort_policy(),
848 CitationSortPolicy::ExplicitOnly
849 );
850 }
851
852 #[test]
853 fn test_substitute_default() {
854 let sub = Substitute::default();
855 assert_eq!(sub.template.len(), 3);
856 }
857
858 #[test]
859 fn test_config_yaml_roundtrip() {
860 let yaml = r#"
861substitute:
862 contributor-role-form: short
863 template:
864 - editor
865 - title
866processing: author-date
867contributors:
868 display-as-sort: first
869 and: symbol
870"#;
871 let config: Config = serde_yaml::from_str(yaml).unwrap();
872 assert!(config.substitute.is_some());
873 assert_eq!(config.processing, Some(Processing::AuthorDate));
874 assert_eq!(
875 config.contributors.as_ref().unwrap().and,
876 Some(AndOptions::Symbol)
877 );
878 }
879
880 #[test]
881 fn test_contributor_config_preset() {
882 let yaml = r#"contributors: apa"#;
884 let config: Config = serde_yaml::from_str(yaml).unwrap();
885 let contributors = config.contributors.unwrap();
886 assert_eq!(contributors.and, Some(AndOptions::Symbol));
887 assert_eq!(contributors.display_as_sort, Some(DisplayAsSort::First));
888 }
889
890 #[test]
891 fn test_role_label_presets_parse_and_resolve_precedence() {
892 let yaml = r#"
893contributors:
894 role:
895 preset: short-suffix
896 roles:
897 editor:
898 preset: long-suffix
899"#;
900 let config: Config = serde_yaml::from_str(yaml).unwrap();
901 let contributors = config.contributors.unwrap();
902
903 assert_eq!(
904 contributors.effective_role_label_preset(&crate::template::ContributorRole::Editor),
905 Some(RoleLabelPreset::LongSuffix)
906 );
907 assert_eq!(
908 contributors.effective_role_label_preset(&crate::template::ContributorRole::Translator),
909 Some(RoleLabelPreset::ShortSuffix)
910 );
911
912 let yaml_scalar = r#"
914contributors:
915 role: short-suffix
916"#;
917 let config2: Config = serde_yaml::from_str(yaml_scalar).unwrap();
918 let contributors2 = config2.contributors.unwrap();
919
920 assert_eq!(
921 contributors2
922 .effective_role_label_preset(&crate::template::ContributorRole::Translator),
923 Some(RoleLabelPreset::ShortSuffix)
924 );
925 }
926
927 #[test]
928 fn test_role_specific_name_order_override_is_available() {
929 let yaml = r#"
930contributors:
931 role:
932 roles:
933 translator:
934 name-order: given-first
935"#;
936 let config: Config = serde_yaml::from_str(yaml).unwrap();
937 let contributors = config.contributors.unwrap();
938
939 assert_eq!(
940 contributors.effective_role_name_order(&crate::template::ContributorRole::Translator),
941 Some(&crate::template::NameOrder::GivenFirst)
942 );
943 }
944
945 #[test]
946 fn test_date_config_preset() {
947 let yaml = r#"dates: long"#;
949 let config: Config = serde_yaml::from_str(yaml).unwrap();
950 let dates = config.dates.unwrap();
951 assert_eq!(dates.month, MonthFormat::Long);
952 }
953
954 #[test]
955 fn test_titles_config_preset() {
956 let yaml = r#"titles: chicago"#;
958 let config: Config = serde_yaml::from_str(yaml).unwrap();
959 let titles = config.titles.unwrap();
960 assert_eq!(titles.component.unwrap().quote, Some(true));
961 assert_eq!(titles.monograph.unwrap().emph, Some(true));
962 }
963
964 #[test]
965 fn test_substitute_config_preset() {
966 let yaml = r#"substitute: standard"#;
968 let config: Config = serde_yaml::from_str(yaml).unwrap();
969 assert!(config.substitute.is_some());
970 let resolved = config.substitute.unwrap().resolve();
971 assert_eq!(resolved.template.len(), 3);
972 assert_eq!(resolved.template[0], SubstituteKey::Editor);
973 }
974
975 #[test]
976 fn test_substitute_config_explicit() {
977 let yaml = r#"
979substitute:
980 template:
981 - title
982 - editor
983"#;
984 let config: Config = serde_yaml::from_str(yaml).unwrap();
985 let resolved = config.substitute.unwrap().resolve();
986 assert_eq!(resolved.template[0], SubstituteKey::Title);
987 assert_eq!(resolved.template[1], SubstituteKey::Editor);
988 }
989
990 #[test]
991 fn test_config_merge_precedence() {
992 let base_yaml = r#"
994processing: author-date
995locale-override: en-US-base
996contributors:
997 display-as-sort: first
998 and: symbol
999"#;
1000 let mut base: Config = serde_yaml::from_str(base_yaml).unwrap();
1001
1002 let override_yaml = r#"
1004contributors:
1005 and: text
1006locale-override: en-US-chicago
1007"#;
1008 let override_config: Config = serde_yaml::from_str(override_yaml).unwrap();
1009
1010 base.merge(&override_config);
1012
1013 assert_eq!(base.processing, Some(Processing::AuthorDate));
1015 assert_eq!(base.locale_override.as_deref(), Some("en-US-chicago"));
1016
1017 assert_eq!(
1019 base.contributors.as_ref().unwrap().and,
1020 Some(AndOptions::Text)
1021 );
1022 }
1023
1024 #[test]
1025 fn test_config_deserializes_locale_override() {
1026 let config: Config = serde_yaml::from_str("locale-override: en-US-chicago").unwrap();
1027 assert_eq!(config.locale_override.as_deref(), Some("en-US-chicago"));
1028 }
1029
1030 #[test]
1031 fn test_config_merged_convenience() {
1032 let base = Config {
1033 processing: Some(Processing::AuthorDate),
1034 ..Default::default()
1035 };
1036 let override_config = Config {
1037 punctuation_in_quote: true,
1038 ..Default::default()
1039 };
1040
1041 let merged = Config::merged(&base, &override_config);
1042
1043 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1045 assert!(merged.punctuation_in_quote);
1046 }
1047
1048 #[test]
1049 fn test_citation_options_merge_overrides_citation_fields_only() {
1050 let base = Config {
1051 processing: Some(Processing::AuthorDate),
1052 ..Default::default()
1053 };
1054
1055 let overrides = CitationOptions {
1056 strip_periods: Some(true),
1057 locators: Some(LocatorConfig::default()),
1058 ..Default::default()
1059 };
1060
1061 let merged = overrides.merged_with(&base);
1062 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1063 assert!(merged.strip_periods.unwrap_or(false));
1064 assert!(merged.locators.is_some());
1065 }
1066
1067 #[test]
1068 fn test_bibliography_options_merge_projects_shared_fields_only() {
1069 let base = Config {
1070 processing: Some(Processing::AuthorDate),
1071 ..Default::default()
1072 };
1073
1074 let overrides = BibliographyOptions {
1075 entry_suffix: Some(".".to_string()),
1076 separator: Some(", ".to_string()),
1077 suppress_period_after_url: true,
1078 ..Default::default()
1079 };
1080
1081 let merged = overrides.merged_with(&base);
1082 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1083 assert!(merged.locators.is_none());
1084 assert!(merged.notes.is_none());
1085 let bibliography = overrides.to_bibliography_config();
1086 assert_eq!(bibliography.entry_suffix.as_deref(), Some("."));
1087 assert_eq!(bibliography.separator.as_deref(), Some(", "));
1088 assert!(bibliography.suppress_period_after_url);
1089 }
1090
1091 #[test]
1092 fn test_bibliography_options_merge_leaves_shared_base_when_only_shared_overrides_exist() {
1093 let base = Config {
1094 processing: Some(Processing::AuthorDate),
1095 ..Default::default()
1096 };
1097
1098 let overrides = BibliographyOptions {
1099 contributors: Some(ContributorConfig::default()),
1100 ..Default::default()
1101 };
1102
1103 let merged = overrides.merged_with(&base);
1104 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1105 assert!(merged.contributors.is_some());
1106 }
1107
1108 #[test]
1109 fn citation_options_captures_unknown_fields_for_forward_compat() {
1110 let yaml = "future-key: true\n";
1111 let opts: CitationOptions = serde_yaml::from_str(yaml).unwrap();
1112 assert!(opts.unknown_fields.contains_key("future-key"));
1113 }
1114
1115 #[test]
1116 fn bibliography_options_captures_unknown_fields_for_forward_compat() {
1117 let yaml = "future-key: true\n";
1118 let opts: BibliographyOptions = serde_yaml::from_str(yaml).unwrap();
1119 assert!(opts.unknown_fields.contains_key("future-key"));
1120 }
1121
1122 #[test]
1123 fn note_config_captures_unknown_fields_for_forward_compat() {
1124 let yaml = "punctuation: inside\nfuture-key: true\n";
1125 let cfg: NoteConfig = serde_yaml::from_str(yaml).unwrap();
1126 assert!(cfg.unknown_fields.contains_key("future-key"));
1127 assert_eq!(cfg.punctuation, Some(NoteQuotePlacement::Inside));
1128 }
1129}