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