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;
18pub mod title_class;
19
20pub use crate::presets::{MultilingualConfigEntry, MultilingualPreset};
21pub use bibliography::{
22 AnonymousEntriesMode, ArticleJournalBibliographyConfig, ArticleJournalNoPageFallback,
23 BibliographyConfig, BibliographyPartitionHeading, BibliographyPartitionKind,
24 BibliographyPartitionMode, BibliographySortPartitioning, SubsequentAuthorSubstituteRule,
25};
26pub use contributors::{
27 AndOptions, AndOtherOptions, ContributorConfig, ContributorConfigEntry, DelimiterPrecedesLast,
28 DemoteNonDroppingParticle, DisplayAsSort, NameForm, RoleLabelDefaults, RoleLabelPreset,
29 RoleOptions, RoleOptionsEntry, RoleRendering, ShortenListOptions,
30};
31pub use dates::{DateConfig, DateConfigEntry, NoDateForm};
32pub use integral_name_memory::{
33 IntegralNameContexts, IntegralNameMemoryConfig, IntegralNameScope, OrgAbbreviationMemoryConfig,
34 ResolvedIntegralNameMemoryConfig, ResolvedOrgAbbreviationMemoryConfig, ShortNameDisplay,
35 SubsequentNameForm,
36};
37pub use localization::{Localize, MonthFormat, Scope};
38pub use locators::{
39 LabelForm, LabelRepeat, LocatorConfig, LocatorConfigEntry, LocatorKindConfig, LocatorPattern,
40 LocatorPreset, TypeClass,
41};
42pub use multilingual::{
43 MultilingualConfig, MultilingualMode, MultilingualSegment, MultilingualView, ScriptConfig,
44 SegmentWrap,
45};
46pub use processing::{
47 CitationSortPolicy, Disambiguation, GivennameRule, Group, LabelConfig, LabelParams,
48 LabelPreset, Processing, ProcessingBase, ProcessingCustom, RegimeFamily, Sort, SortEntry,
49 SortKey, SortSpec,
50};
51pub use scoped::{
52 BibliographyLabelMode, BibliographyLabelWrap, CitationGroupDelimiter, DatePosition, LabelWrap,
53 RepeatedAuthorRendering, TitleTerminator,
54};
55pub use substitute::{Substitute, SubstituteConfig, SubstituteKey, SubstituteTitleQuoteMode};
56
57use crate::template::DelimiterPunctuation;
58#[cfg(feature = "schema")]
59use schemars::JsonSchema;
60use serde::{Deserialize, Serialize};
61use std::collections::HashMap;
62
63#[derive(Debug, Default, PartialEq, Clone, Serialize)]
65#[cfg_attr(feature = "schema", derive(JsonSchema))]
66#[serde(rename_all = "kebab-case")]
67pub struct Config {
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub substitute: Option<SubstituteConfig>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub processing: Option<Processing>,
74 #[serde(skip_serializing_if = "Option::is_none")]
80 pub locale_override: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub localize: Option<Localize>,
84 #[serde(
87 skip_serializing_if = "Option::is_none",
88 deserialize_with = "deserialize_multilingual_config",
89 default
90 )]
91 #[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
92 pub multilingual: Option<MultilingualConfig>,
93 #[serde(
96 skip_serializing_if = "Option::is_none",
97 deserialize_with = "deserialize_contributor_config",
98 default
99 )]
100 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
101 pub contributors: Option<ContributorConfig>,
102 #[serde(
105 skip_serializing_if = "Option::is_none",
106 deserialize_with = "deserialize_date_config",
107 default
108 )]
109 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
110 pub dates: Option<DateConfig>,
111 #[serde(
114 skip_serializing_if = "Option::is_none",
115 deserialize_with = "deserialize_titles_config",
116 default
117 )]
118 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
119 pub titles: Option<crate::options::titles::TitlesConfig>,
120 #[serde(
123 skip_serializing_if = "Option::is_none",
124 deserialize_with = "deserialize_locator_config",
125 default
126 )]
127 #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
128 pub locators: Option<LocatorConfig>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub page_range_format: Option<PageRangeFormat>,
132 #[serde(skip_serializing_if = "Option::is_none")]
135 pub page_range_delimiter: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub links: Option<LinksConfig>,
139 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
143 pub punctuation_in_quote: bool,
144 #[serde(skip_serializing_if = "Option::is_none")]
148 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
149 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
151 pub strip_periods: Option<bool>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub notes: Option<NoteConfig>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub integral_name_memory: Option<IntegralNameMemoryConfig>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub custom: Option<HashMap<String, serde_json::Value>>,
164 #[serde(
168 flatten,
169 default,
170 skip_serializing_if = "std::collections::BTreeMap::is_empty"
171 )]
172 #[cfg_attr(feature = "schema", schemars(skip))]
173 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
174}
175
176#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
178#[cfg_attr(feature = "schema", derive(JsonSchema))]
179#[serde(rename_all = "kebab-case")]
180pub struct CitationOptions {
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub substitute: Option<SubstituteConfig>,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub processing: Option<Processing>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub localize: Option<Localize>,
190 #[serde(
193 skip_serializing_if = "Option::is_none",
194 deserialize_with = "deserialize_multilingual_config",
195 default
196 )]
197 #[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
198 pub multilingual: Option<MultilingualConfig>,
199 #[serde(
201 skip_serializing_if = "Option::is_none",
202 deserialize_with = "deserialize_contributor_config",
203 default
204 )]
205 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
206 pub contributors: Option<ContributorConfig>,
207 #[serde(
209 skip_serializing_if = "Option::is_none",
210 deserialize_with = "deserialize_date_config",
211 default
212 )]
213 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
214 pub dates: Option<DateConfig>,
215 #[serde(
217 skip_serializing_if = "Option::is_none",
218 deserialize_with = "deserialize_titles_config",
219 default
220 )]
221 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
222 pub titles: Option<crate::options::titles::TitlesConfig>,
223 #[serde(
225 skip_serializing_if = "Option::is_none",
226 deserialize_with = "deserialize_locator_config",
227 default
228 )]
229 #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
230 pub locators: Option<LocatorConfig>,
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub page_range_format: Option<PageRangeFormat>,
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub links: Option<LinksConfig>,
237 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
239 pub punctuation_in_quote: bool,
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
243 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
245 pub strip_periods: Option<bool>,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub notes: Option<NoteConfig>,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub integral_name_memory: Option<IntegralNameMemoryConfig>,
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub label_wrap: Option<LabelWrap>,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub group_delimiter: Option<CitationGroupDelimiter>,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub custom: Option<HashMap<String, serde_json::Value>>,
264 #[serde(
268 flatten,
269 default,
270 skip_serializing_if = "std::collections::BTreeMap::is_empty"
271 )]
272 #[cfg_attr(feature = "schema", schemars(skip))]
273 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
274}
275
276#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
278#[cfg_attr(feature = "schema", derive(JsonSchema))]
279#[serde(rename_all = "kebab-case")]
280pub struct BibliographyOptions {
281 #[serde(skip_serializing_if = "Option::is_none")]
283 pub substitute: Option<SubstituteConfig>,
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub processing: Option<Processing>,
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub localize: Option<Localize>,
290 #[serde(
293 skip_serializing_if = "Option::is_none",
294 deserialize_with = "deserialize_multilingual_config",
295 default
296 )]
297 #[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
298 pub multilingual: Option<MultilingualConfig>,
299 #[serde(
301 skip_serializing_if = "Option::is_none",
302 deserialize_with = "deserialize_contributor_config",
303 default
304 )]
305 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
306 pub contributors: Option<ContributorConfig>,
307 #[serde(
309 skip_serializing_if = "Option::is_none",
310 deserialize_with = "deserialize_date_config",
311 default
312 )]
313 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
314 pub dates: Option<DateConfig>,
315 #[serde(
317 skip_serializing_if = "Option::is_none",
318 deserialize_with = "deserialize_titles_config",
319 default
320 )]
321 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
322 pub titles: Option<crate::options::titles::TitlesConfig>,
323 #[serde(skip_serializing_if = "Option::is_none")]
325 pub page_range_format: Option<PageRangeFormat>,
326 #[serde(skip_serializing_if = "Option::is_none")]
328 pub article_journal: Option<ArticleJournalBibliographyConfig>,
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub subsequent_author_substitute: Option<String>,
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub subsequent_author_substitute_rule: Option<SubsequentAuthorSubstituteRule>,
335 #[serde(skip_serializing_if = "Option::is_none")]
337 pub hanging_indent: Option<bool>,
338 #[serde(skip_serializing_if = "Option::is_none")]
340 pub entry_suffix: Option<String>,
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub separator: Option<String>,
344 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
346 pub suppress_period_after_url: bool,
347 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
349 pub entry_suffix_after_url: bool,
350 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
352 pub entry_suffix_after_doi: bool,
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub compound_numeric: Option<bibliography::CompoundNumericConfig>,
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub sort_partitioning: Option<bibliography::BibliographySortPartitioning>,
359 #[serde(skip_serializing_if = "Option::is_none")]
363 pub anonymous_entries: Option<AnonymousEntriesMode>,
364 #[serde(skip_serializing_if = "Option::is_none")]
366 pub links: Option<LinksConfig>,
367 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
369 pub punctuation_in_quote: bool,
370 #[serde(skip_serializing_if = "Option::is_none")]
372 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
373 #[serde(skip_serializing_if = "Option::is_none")]
375 pub label_mode: Option<BibliographyLabelMode>,
376 #[serde(skip_serializing_if = "Option::is_none")]
378 pub label_wrap: Option<BibliographyLabelWrap>,
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub date_position: Option<DatePosition>,
382 #[serde(skip_serializing_if = "Option::is_none")]
384 pub title_terminator: Option<TitleTerminator>,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub repeated_author_rendering: Option<RepeatedAuthorRendering>,
388 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
390 pub strip_periods: Option<bool>,
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub custom: Option<HashMap<String, serde_json::Value>>,
394 #[serde(
398 flatten,
399 default,
400 skip_serializing_if = "std::collections::BTreeMap::is_empty"
401 )]
402 #[cfg_attr(feature = "schema", schemars(skip))]
403 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
404}
405
406#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
408#[cfg_attr(feature = "schema", derive(JsonSchema))]
409#[serde(rename_all = "kebab-case")]
410pub struct NoteConfig {
411 #[serde(skip_serializing_if = "Option::is_none")]
414 pub punctuation: Option<NoteQuotePlacement>,
415 #[serde(skip_serializing_if = "Option::is_none")]
417 pub number: Option<NoteNumberPlacement>,
418 #[serde(skip_serializing_if = "Option::is_none")]
421 pub order: Option<NoteMarkerOrder>,
422 #[serde(
426 flatten,
427 default,
428 skip_serializing_if = "std::collections::BTreeMap::is_empty"
429 )]
430 #[cfg_attr(feature = "schema", schemars(skip))]
431 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
432}
433
434#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
436#[cfg_attr(feature = "schema", derive(JsonSchema))]
437#[serde(rename_all = "kebab-case")]
438pub enum NoteQuotePlacement {
439 Inside,
441 Outside,
443 #[default]
447 Adaptive,
448}
449
450#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
452#[cfg_attr(feature = "schema", derive(JsonSchema))]
453#[serde(rename_all = "kebab-case")]
454pub enum NoteNumberPlacement {
455 Inside,
457 #[default]
459 Outside,
460 Same,
463}
464
465#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
467#[cfg_attr(feature = "schema", derive(JsonSchema))]
468#[serde(rename_all = "kebab-case")]
469pub enum NoteMarkerOrder {
470 Before,
472 #[default]
474 After,
475}
476
477#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
479#[cfg_attr(feature = "schema", derive(JsonSchema))]
480#[serde(rename_all = "kebab-case")]
481#[non_exhaustive]
482pub enum PageRangeFormat {
483 #[default]
485 Expanded,
486 Minimal,
488 MinimalTwo,
490 Chicago,
492 Chicago16,
494}
495
496pub mod titles;
497
498pub use title_class::{
499 TitleCategory, classified_ref_types, container_title_category, parent_serial_title_category,
500 title_category,
501};
502pub use titles::{TextCase, TitleRendering, TitlesConfig, TitlesConfigEntry};
503
504#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
506#[cfg_attr(feature = "schema", derive(JsonSchema))]
507#[serde(rename_all = "kebab-case")]
508pub struct LinksConfig {
509 #[serde(skip_serializing_if = "Option::is_none")]
511 pub doi: Option<bool>,
512 #[serde(skip_serializing_if = "Option::is_none")]
514 pub url: Option<bool>,
515 #[serde(skip_serializing_if = "Option::is_none")]
517 pub target: Option<LinkTarget>,
518 #[serde(skip_serializing_if = "Option::is_none")]
520 pub anchor: Option<LinkAnchor>,
521}
522
523#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
525#[cfg_attr(feature = "schema", derive(JsonSchema))]
526#[serde(rename_all = "kebab-case")]
527pub enum LinkTarget {
528 Url,
529 Doi,
530 UrlOrDoi,
531 Pubmed,
532 Pmcid,
533}
534
535#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
537#[cfg_attr(feature = "schema", derive(JsonSchema))]
538#[serde(rename_all = "kebab-case")]
539pub enum LinkAnchor {
540 Title,
542 Url,
544 Doi,
546 Component,
548 Entry,
550}
551
552impl Config {
553 pub fn effective_processing(&self) -> Processing {
558 self.processing.clone().unwrap_or_default()
559 }
560
561 pub fn merge(&mut self, other: &Config) {
566 crate::merge_options!(
567 self,
568 other,
569 processing,
570 locale_override,
571 localize,
572 multilingual,
573 dates,
574 titles,
575 locators,
576 page_range_format,
577 page_range_delimiter,
578 links,
579 volume_pages_delimiter,
580 locale_override,
581 strip_periods,
582 notes,
583 integral_name_memory,
584 org_abbreviation_memory,
585 custom,
586 );
587
588 if let Some(other_substitute) = &other.substitute {
589 if let Some(this_substitute) = &self.substitute {
590 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
591 } else {
592 self.substitute = Some(other_substitute.clone());
593 }
594 }
595
596 if let Some(other_contributors) = &other.contributors {
597 if let Some(this_contributors) = &mut self.contributors {
598 this_contributors.merge(other_contributors);
599 } else {
600 self.contributors = Some(other_contributors.clone());
601 }
602 }
603
604 if other.punctuation_in_quote {
605 self.punctuation_in_quote = true;
606 }
607 }
608
609 pub fn merged(base: &Config, override_config: &Config) -> Config {
613 let mut result = base.clone();
614 result.merge(override_config);
615 result
616 }
617}
618
619impl CitationOptions {
620 #[must_use]
622 pub fn to_config(&self) -> Config {
623 Config {
624 substitute: self.substitute.clone(),
625 processing: self.processing.clone(),
626 locale_override: None,
627 localize: self.localize.clone(),
628 multilingual: self.multilingual.clone(),
629 contributors: self.contributors.clone(),
630 dates: self.dates.clone(),
631 titles: self.titles.clone(),
632 locators: self.locators.clone(),
633 page_range_format: self.page_range_format.clone(),
634 page_range_delimiter: None,
635 links: self.links.clone(),
636 punctuation_in_quote: self.punctuation_in_quote,
637 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
638 strip_periods: self.strip_periods,
639 notes: self.notes.clone(),
640 integral_name_memory: self.integral_name_memory.clone(),
641 org_abbreviation_memory: self.org_abbreviation_memory.clone(),
642 custom: self.custom.clone(),
643 unknown_fields: std::collections::BTreeMap::new(),
644 }
645 }
646
647 #[must_use]
649 pub fn merged_with(&self, base: &Config) -> Config {
650 Config::merged(base, &self.to_config())
651 }
652
653 pub fn merge(&mut self, other: &CitationOptions) {
655 crate::merge_options!(
656 self,
657 other,
658 processing,
659 localize,
660 multilingual,
661 dates,
662 titles,
663 locators,
664 page_range_format,
665 links,
666 volume_pages_delimiter,
667 strip_periods,
668 notes,
669 integral_name_memory,
670 org_abbreviation_memory,
671 label_wrap,
672 group_delimiter,
673 custom,
674 );
675
676 if let Some(other_substitute) = &other.substitute {
677 if let Some(this_substitute) = &self.substitute {
678 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
679 } else {
680 self.substitute = Some(other_substitute.clone());
681 }
682 }
683
684 if let Some(other_contributors) = &other.contributors {
685 if let Some(this_contributors) = &mut self.contributors {
686 this_contributors.merge(other_contributors);
687 } else {
688 self.contributors = Some(other_contributors.clone());
689 }
690 }
691
692 if other.punctuation_in_quote {
693 self.punctuation_in_quote = true;
694 }
695 }
696}
697
698impl BibliographyOptions {
699 #[must_use]
701 pub fn to_bibliography_config(&self) -> BibliographyConfig {
702 BibliographyConfig {
703 article_journal: self.article_journal.clone(),
704 subsequent_author_substitute: self.subsequent_author_substitute.clone(),
705 subsequent_author_substitute_rule: self.subsequent_author_substitute_rule.clone(),
706 hanging_indent: self.hanging_indent,
707 entry_suffix: self.entry_suffix.clone(),
708 separator: self.separator.clone(),
709 suppress_period_after_url: self.suppress_period_after_url,
710 entry_suffix_after_url: self.entry_suffix_after_url,
711 entry_suffix_after_doi: self.entry_suffix_after_doi,
712 custom: None,
713 compound_numeric: self.compound_numeric.clone(),
714 sort_partitioning: self.sort_partitioning.clone(),
715 anonymous_entries: self.anonymous_entries,
716 unknown_fields: std::collections::BTreeMap::new(),
717 }
718 }
719
720 #[must_use]
722 pub fn to_config(&self) -> Config {
723 Config {
724 substitute: self.substitute.clone(),
725 processing: self.processing.clone(),
726 locale_override: None,
727 localize: self.localize.clone(),
728 multilingual: self.multilingual.clone(),
729 contributors: self.contributors.clone(),
730 dates: self.dates.clone(),
731 titles: self.titles.clone(),
732 locators: None,
733 page_range_format: self.page_range_format.clone(),
734 page_range_delimiter: None,
735 links: self.links.clone(),
736 punctuation_in_quote: self.punctuation_in_quote,
737 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
738 strip_periods: self.strip_periods,
739 notes: None,
740 integral_name_memory: None,
741 org_abbreviation_memory: None,
742 custom: self.custom.clone(),
743 unknown_fields: std::collections::BTreeMap::new(),
744 }
745 }
746
747 #[must_use]
749 pub fn merged_with(&self, base: &Config) -> Config {
750 Config::merged(base, &self.to_config())
751 }
752
753 pub fn merge(&mut self, other: &BibliographyOptions) {
755 crate::merge_options!(
756 self,
757 other,
758 processing,
759 localize,
760 multilingual,
761 dates,
762 titles,
763 page_range_format,
764 links,
765 volume_pages_delimiter,
766 strip_periods,
767 article_journal,
768 subsequent_author_substitute,
769 subsequent_author_substitute_rule,
770 hanging_indent,
771 entry_suffix,
772 separator,
773 compound_numeric,
774 sort_partitioning,
775 anonymous_entries,
776 label_mode,
777 label_wrap,
778 date_position,
779 title_terminator,
780 repeated_author_rendering,
781 custom,
782 );
783
784 self.merge_shared_fields(other);
785 }
786
787 fn merge_shared_fields(&mut self, other: &BibliographyOptions) {
788 if let Some(other_substitute) = &other.substitute {
789 if let Some(this_substitute) = &self.substitute {
790 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
791 } else {
792 self.substitute = Some(other_substitute.clone());
793 }
794 }
795
796 if let Some(other_contributors) = &other.contributors {
797 if let Some(this_contributors) = &mut self.contributors {
798 this_contributors.merge(other_contributors);
799 } else {
800 self.contributors = Some(other_contributors.clone());
801 }
802 }
803
804 if other.punctuation_in_quote {
805 self.punctuation_in_quote = true;
806 }
807 if other.suppress_period_after_url {
808 self.suppress_period_after_url = true;
809 }
810 if other.entry_suffix_after_url {
811 self.entry_suffix_after_url = true;
812 }
813 if other.entry_suffix_after_doi {
814 self.entry_suffix_after_doi = true;
815 }
816
817 for (key, value) in &other.unknown_fields {
818 self.unknown_fields.insert(key.clone(), value.clone());
819 }
820 }
821}
822
823fn deserialize_contributor_config<'de, D>(
825 deserializer: D,
826) -> Result<Option<ContributorConfig>, D::Error>
827where
828 D: serde::Deserializer<'de>,
829{
830 let value: Option<ContributorConfigEntry> = Option::deserialize(deserializer)?;
831 Ok(value.map(|entry| entry.resolve()))
832}
833
834fn deserialize_date_config<'de, D>(deserializer: D) -> Result<Option<DateConfig>, D::Error>
836where
837 D: serde::Deserializer<'de>,
838{
839 let value: Option<DateConfigEntry> = Option::deserialize(deserializer)?;
840 Ok(value.map(|entry| entry.resolve()))
841}
842
843fn deserialize_titles_config<'de, D>(
845 deserializer: D,
846) -> Result<Option<crate::options::titles::TitlesConfig>, D::Error>
847where
848 D: serde::Deserializer<'de>,
849{
850 let value: Option<crate::options::titles::TitlesConfigEntry> =
851 Option::deserialize(deserializer)?;
852 Ok(value.map(|entry| entry.resolve()))
853}
854
855fn deserialize_locator_config<'de, D>(deserializer: D) -> Result<Option<LocatorConfig>, D::Error>
857where
858 D: serde::Deserializer<'de>,
859{
860 let value: Option<LocatorConfigEntry> = Option::deserialize(deserializer)?;
861 Ok(value.map(|entry| entry.resolve()))
862}
863
864fn deserialize_multilingual_config<'de, D>(
866 deserializer: D,
867) -> Result<Option<MultilingualConfig>, D::Error>
868where
869 D: serde::Deserializer<'de>,
870{
871 let value: Option<crate::presets::MultilingualConfigEntry> = Option::deserialize(deserializer)?;
872 Ok(value.map(|entry| entry.resolve()))
873}
874
875impl<'de> Deserialize<'de> for Config {
876 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
877 where
878 D: serde::Deserializer<'de>,
879 {
880 #[derive(Deserialize)]
881 #[serde(rename_all = "kebab-case")]
882 struct ConfigWire {
883 #[serde(skip_serializing_if = "Option::is_none")]
884 substitute: Option<SubstituteConfig>,
885 #[serde(skip_serializing_if = "Option::is_none")]
886 processing: Option<Processing>,
887 #[serde(skip_serializing_if = "Option::is_none")]
888 locale_override: Option<String>,
889 #[serde(skip_serializing_if = "Option::is_none")]
890 localize: Option<Localize>,
891 #[serde(
892 skip_serializing_if = "Option::is_none",
893 deserialize_with = "deserialize_multilingual_config",
894 default
895 )]
896 multilingual: Option<MultilingualConfig>,
897 #[serde(
898 skip_serializing_if = "Option::is_none",
899 deserialize_with = "deserialize_contributor_config",
900 default
901 )]
902 contributors: Option<ContributorConfig>,
903 #[serde(
904 skip_serializing_if = "Option::is_none",
905 deserialize_with = "deserialize_date_config",
906 default
907 )]
908 dates: Option<DateConfig>,
909 #[serde(
910 skip_serializing_if = "Option::is_none",
911 deserialize_with = "deserialize_titles_config",
912 default
913 )]
914 titles: Option<crate::options::titles::TitlesConfig>,
915 #[serde(
916 skip_serializing_if = "Option::is_none",
917 deserialize_with = "deserialize_locator_config",
918 default
919 )]
920 locators: Option<LocatorConfig>,
921 #[serde(skip_serializing_if = "Option::is_none")]
922 page_range_format: Option<PageRangeFormat>,
923 #[serde(skip_serializing_if = "Option::is_none")]
924 page_range_delimiter: Option<String>,
925 #[serde(skip_serializing_if = "Option::is_none")]
926 links: Option<LinksConfig>,
927 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
928 punctuation_in_quote: bool,
929 #[serde(skip_serializing_if = "Option::is_none")]
930 volume_pages_delimiter: Option<DelimiterPunctuation>,
931 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
932 strip_periods: Option<bool>,
933 #[serde(skip_serializing_if = "Option::is_none")]
934 notes: Option<NoteConfig>,
935 #[serde(skip_serializing_if = "Option::is_none")]
936 integral_name_memory: Option<IntegralNameMemoryConfig>,
937 #[serde(skip_serializing_if = "Option::is_none")]
938 org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
939 #[serde(default)]
940 profile: Option<serde_yaml::Value>,
941 #[serde(skip_serializing_if = "Option::is_none")]
942 custom: Option<HashMap<String, serde_json::Value>>,
943 #[serde(flatten)]
944 unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
945 }
946
947 let wire = ConfigWire::deserialize(deserializer)?;
948 if wire.profile.is_some() {
949 return Err(serde::de::Error::custom(
950 "`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`",
951 ));
952 }
953
954 Ok(Self {
955 substitute: wire.substitute,
956 processing: wire.processing,
957 locale_override: wire.locale_override,
958 localize: wire.localize,
959 multilingual: wire.multilingual,
960 contributors: wire.contributors,
961 dates: wire.dates,
962 titles: wire.titles,
963 locators: wire.locators,
964 page_range_format: wire.page_range_format,
965 page_range_delimiter: wire.page_range_delimiter,
966 links: wire.links,
967 punctuation_in_quote: wire.punctuation_in_quote,
968 volume_pages_delimiter: wire.volume_pages_delimiter,
969 strip_periods: wire.strip_periods,
970 notes: wire.notes,
971 integral_name_memory: wire.integral_name_memory,
972 org_abbreviation_memory: wire.org_abbreviation_memory,
973 custom: wire.custom,
974 unknown_fields: wire.unknown_fields,
975 })
976 }
977}
978
979#[cfg(test)]
980#[allow(
981 clippy::unwrap_used,
982 clippy::expect_used,
983 clippy::panic,
984 clippy::indexing_slicing,
985 clippy::todo,
986 clippy::unimplemented,
987 clippy::unreachable,
988 clippy::get_unwrap,
989 reason = "Panicking is acceptable and often desired in tests."
990)]
991mod tests {
992 use super::*;
993
994 #[test]
995 fn test_config_default() {
996 let config = Config::default();
997 assert!(config.substitute.is_none());
998 assert!(config.processing.is_none());
999 }
1000
1001 #[test]
1002 fn test_author_date_processing() {
1003 let processing = Processing::AuthorDate;
1004 let config = processing.config();
1005 let disambiguate = config.disambiguate.unwrap();
1006 assert!(disambiguate.year_suffix);
1007 assert!(!disambiguate.names);
1008 assert!(!disambiguate.add_givenname);
1009 assert_eq!(
1010 processing.default_bibliography_sort(),
1011 Some(crate::presets::SortPreset::AuthorDateTitle)
1012 );
1013 assert_eq!(
1014 config.sort,
1015 Some(SortEntry::Preset(
1016 crate::presets::SortPreset::AuthorDateTitle
1017 ))
1018 );
1019 }
1020
1021 #[test]
1022 fn test_processing_default_bibliography_sorts() {
1023 assert_eq!(Processing::Numeric.default_bibliography_sort(), None);
1024 assert_eq!(
1025 Processing::Note.default_bibliography_sort(),
1026 Some(crate::presets::SortPreset::AuthorTitleDate)
1027 );
1028 assert_eq!(
1029 Processing::Label(LabelConfig::default()).default_bibliography_sort(),
1030 Some(crate::presets::SortPreset::AuthorDateTitle)
1031 );
1032 }
1033
1034 #[test]
1035 fn test_processing_default_citation_sort_policy_is_explicit_only() {
1036 assert_eq!(
1037 Processing::AuthorDate.default_citation_sort_policy(),
1038 CitationSortPolicy::ExplicitOnly
1039 );
1040 assert_eq!(
1041 Processing::Note.default_citation_sort_policy(),
1042 CitationSortPolicy::ExplicitOnly
1043 );
1044 }
1045
1046 #[test]
1047 fn test_substitute_default() {
1048 let sub = Substitute::default();
1049 assert_eq!(sub.template.len(), 3);
1050 }
1051
1052 #[test]
1053 fn test_config_yaml_roundtrip() {
1054 let yaml = r#"
1055substitute:
1056 contributor-role-form: short
1057 template:
1058 - editor
1059 - title
1060processing: author-date
1061contributors:
1062 display-as-sort: first
1063 and: symbol
1064"#;
1065 let config: Config = serde_yaml::from_str(yaml).unwrap();
1066 assert!(config.substitute.is_some());
1067 assert_eq!(config.processing, Some(Processing::AuthorDate));
1068 assert_eq!(
1069 config.contributors.as_ref().unwrap().and,
1070 Some(AndOptions::Symbol)
1071 );
1072 }
1073
1074 #[test]
1075 fn test_contributor_config_preset() {
1076 let yaml = r#"contributors: apa"#;
1078 let config: Config = serde_yaml::from_str(yaml).unwrap();
1079 let contributors = config.contributors.unwrap();
1080 assert_eq!(contributors.and, Some(AndOptions::Symbol));
1081 assert_eq!(contributors.display_as_sort, Some(DisplayAsSort::First));
1082 }
1083
1084 #[test]
1085 fn test_role_label_presets_parse_and_resolve_precedence() {
1086 let yaml = r#"
1087contributors:
1088 role:
1089 preset: short-suffix
1090 roles:
1091 editor:
1092 preset: long-suffix
1093"#;
1094 let config: Config = serde_yaml::from_str(yaml).unwrap();
1095 let contributors = config.contributors.unwrap();
1096
1097 assert_eq!(
1098 contributors.effective_role_label_preset(&crate::template::ContributorRole::Editor),
1099 Some(RoleLabelPreset::LongSuffix)
1100 );
1101 assert_eq!(
1102 contributors.effective_role_label_preset(&crate::template::ContributorRole::Translator),
1103 Some(RoleLabelPreset::ShortSuffix)
1104 );
1105
1106 let yaml_scalar = r#"
1108contributors:
1109 role: short-suffix
1110"#;
1111 let config2: Config = serde_yaml::from_str(yaml_scalar).unwrap();
1112 let contributors2 = config2.contributors.unwrap();
1113
1114 assert_eq!(
1115 contributors2
1116 .effective_role_label_preset(&crate::template::ContributorRole::Translator),
1117 Some(RoleLabelPreset::ShortSuffix)
1118 );
1119 }
1120
1121 #[test]
1122 fn test_role_specific_name_order_override_is_available() {
1123 let yaml = r#"
1124contributors:
1125 role:
1126 roles:
1127 translator:
1128 name-order: given-first
1129"#;
1130 let config: Config = serde_yaml::from_str(yaml).unwrap();
1131 let contributors = config.contributors.unwrap();
1132
1133 assert_eq!(
1134 contributors.effective_role_name_order(&crate::template::ContributorRole::Translator),
1135 Some(&crate::template::NameOrder::GivenFirst)
1136 );
1137 }
1138
1139 #[test]
1140 fn test_date_config_preset() {
1141 let yaml = r#"dates: long"#;
1143 let config: Config = serde_yaml::from_str(yaml).unwrap();
1144 let dates = config.dates.unwrap();
1145 assert_eq!(dates.month, MonthFormat::Long);
1146 }
1147
1148 #[test]
1149 fn test_titles_config_preset() {
1150 let yaml = r#"titles: chicago"#;
1152 let config: Config = serde_yaml::from_str(yaml).unwrap();
1153 let titles = config.titles.unwrap();
1154 assert_eq!(titles.component.unwrap().quote, Some(true));
1155 assert_eq!(titles.monograph.unwrap().emph, Some(true));
1156 }
1157
1158 #[test]
1159 fn test_substitute_config_preset() {
1160 let yaml = r#"substitute: standard"#;
1162 let config: Config = serde_yaml::from_str(yaml).unwrap();
1163 assert!(config.substitute.is_some());
1164 let resolved = config.substitute.unwrap().resolve();
1165 assert_eq!(resolved.template.len(), 3);
1166 assert_eq!(resolved.template[0], SubstituteKey::Editor);
1167 }
1168
1169 #[test]
1170 fn test_substitute_config_explicit() {
1171 let yaml = r#"
1173substitute:
1174 template:
1175 - title
1176 - editor
1177"#;
1178 let config: Config = serde_yaml::from_str(yaml).unwrap();
1179 let resolved = config.substitute.unwrap().resolve();
1180 assert_eq!(resolved.template[0], SubstituteKey::Title);
1181 assert_eq!(resolved.template[1], SubstituteKey::Editor);
1182 }
1183
1184 #[test]
1185 fn test_config_merge_precedence() {
1186 let base_yaml = r#"
1188processing: author-date
1189locale-override: en-US-base
1190contributors:
1191 display-as-sort: first
1192 and: symbol
1193"#;
1194 let mut base: Config = serde_yaml::from_str(base_yaml).unwrap();
1195
1196 let override_yaml = r#"
1198contributors:
1199 and: text
1200locale-override: en-US-chicago
1201"#;
1202 let override_config: Config = serde_yaml::from_str(override_yaml).unwrap();
1203
1204 base.merge(&override_config);
1206
1207 assert_eq!(base.processing, Some(Processing::AuthorDate));
1209 assert_eq!(base.locale_override.as_deref(), Some("en-US-chicago"));
1210
1211 assert_eq!(
1213 base.contributors.as_ref().unwrap().and,
1214 Some(AndOptions::Text)
1215 );
1216 }
1217
1218 #[test]
1219 fn test_config_deserializes_locale_override() {
1220 let config: Config = serde_yaml::from_str("locale-override: en-US-chicago").unwrap();
1221 assert_eq!(config.locale_override.as_deref(), Some("en-US-chicago"));
1222 }
1223
1224 #[test]
1225 fn test_config_merged_convenience() {
1226 let base = Config {
1227 processing: Some(Processing::AuthorDate),
1228 ..Default::default()
1229 };
1230 let override_config = Config {
1231 punctuation_in_quote: true,
1232 ..Default::default()
1233 };
1234
1235 let merged = Config::merged(&base, &override_config);
1236
1237 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1239 assert!(merged.punctuation_in_quote);
1240 }
1241
1242 #[test]
1243 fn test_citation_options_merge_overrides_citation_fields_only() {
1244 let base = Config {
1245 processing: Some(Processing::AuthorDate),
1246 ..Default::default()
1247 };
1248
1249 let overrides = CitationOptions {
1250 strip_periods: Some(true),
1251 locators: Some(LocatorConfig::default()),
1252 ..Default::default()
1253 };
1254
1255 let merged = overrides.merged_with(&base);
1256 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1257 assert!(merged.strip_periods.unwrap_or(false));
1258 assert!(merged.locators.is_some());
1259 }
1260
1261 #[test]
1262 fn test_bibliography_options_merge_projects_shared_fields_only() {
1263 let base = Config {
1264 processing: Some(Processing::AuthorDate),
1265 ..Default::default()
1266 };
1267
1268 let overrides = BibliographyOptions {
1269 entry_suffix: Some(".".to_string()),
1270 separator: Some(", ".to_string()),
1271 suppress_period_after_url: true,
1272 ..Default::default()
1273 };
1274
1275 let merged = overrides.merged_with(&base);
1276 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1277 assert!(merged.locators.is_none());
1278 assert!(merged.notes.is_none());
1279 let bibliography = overrides.to_bibliography_config();
1280 assert_eq!(bibliography.entry_suffix.as_deref(), Some("."));
1281 assert_eq!(bibliography.separator.as_deref(), Some(", "));
1282 assert!(bibliography.suppress_period_after_url);
1283 }
1284
1285 #[test]
1286 fn test_bibliography_options_merge_leaves_shared_base_when_only_shared_overrides_exist() {
1287 let base = Config {
1288 processing: Some(Processing::AuthorDate),
1289 ..Default::default()
1290 };
1291
1292 let overrides = BibliographyOptions {
1293 contributors: Some(ContributorConfig::default()),
1294 ..Default::default()
1295 };
1296
1297 let merged = overrides.merged_with(&base);
1298 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1299 assert!(merged.contributors.is_some());
1300 }
1301
1302 #[test]
1303 fn citation_options_captures_unknown_fields_for_forward_compat() {
1304 let yaml = "future-key: true\n";
1305 let opts: CitationOptions = serde_yaml::from_str(yaml).unwrap();
1306 assert!(opts.unknown_fields.contains_key("future-key"));
1307 }
1308
1309 #[test]
1310 fn bibliography_options_captures_unknown_fields_for_forward_compat() {
1311 let yaml = "future-key: true\n";
1312 let opts: BibliographyOptions = serde_yaml::from_str(yaml).unwrap();
1313 assert!(opts.unknown_fields.contains_key("future-key"));
1314 }
1315
1316 #[test]
1317 fn note_config_captures_unknown_fields_for_forward_compat() {
1318 let yaml = "punctuation: inside\nfuture-key: true\n";
1319 let cfg: NoteConfig = serde_yaml::from_str(yaml).unwrap();
1320 assert!(cfg.unknown_fields.contains_key("future-key"));
1321 assert_eq!(cfg.punctuation, Some(NoteQuotePlacement::Inside));
1322 }
1323
1324 #[test]
1325 fn test_multilingual_preset_romanized_translated_parses_and_resolves() {
1326 let yaml = r#"multilingual: romanized-translated"#;
1328 let config: Config = serde_yaml::from_str(yaml).unwrap();
1329 let ml = config.multilingual.unwrap();
1330 assert_eq!(ml.title_mode, Some(MultilingualMode::Combined));
1331 assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
1332 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1333 }
1334
1335 #[test]
1336 fn test_multilingual_preset_romanized_only_parses_and_resolves() {
1337 let yaml = r#"multilingual: romanized-only"#;
1339 let config: Config = serde_yaml::from_str(yaml).unwrap();
1340 let ml = config.multilingual.unwrap();
1341 assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
1342 assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
1343 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1344 }
1345
1346 #[test]
1347 fn test_multilingual_preset_romanized_script_translated_parses_and_resolves() {
1348 use crate::options::multilingual::{MultilingualSegment, MultilingualView, SegmentWrap};
1351 let yaml = r#"multilingual: romanized-script-translated"#;
1352 let config: Config = serde_yaml::from_str(yaml).unwrap();
1353 let ml = config.multilingual.unwrap();
1354 assert_eq!(
1355 ml.title_mode,
1356 Some(MultilingualMode::Pattern(vec![
1357 MultilingualSegment {
1358 view: MultilingualView::Transliterated,
1359 wrap: SegmentWrap::None,
1360 },
1361 MultilingualSegment {
1362 view: MultilingualView::OriginalScript,
1363 wrap: SegmentWrap::None,
1364 },
1365 MultilingualSegment {
1366 view: MultilingualView::Translated,
1367 wrap: SegmentWrap::Brackets,
1368 },
1369 ]))
1370 );
1371 assert_eq!(
1372 ml.name_mode,
1373 Some(MultilingualMode::Pattern(vec![
1374 MultilingualSegment {
1375 view: MultilingualView::Transliterated,
1376 wrap: SegmentWrap::None,
1377 },
1378 MultilingualSegment {
1379 view: MultilingualView::OriginalScript,
1380 wrap: SegmentWrap::None,
1381 },
1382 ]))
1383 );
1384 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1385 assert!(ml.scripts.get("Han").is_some_and(|s| s.use_native_ordering));
1386 assert!(
1387 ml.scripts
1388 .get("Hangul")
1389 .is_some_and(|s| s.use_native_ordering)
1390 );
1391 }
1392
1393 #[test]
1394 fn test_multilingual_explicit_block_transliterated_roundtrips() {
1395 let yaml = r#"
1397multilingual:
1398 title-mode: transliterated
1399 preferred-script: Latn
1400"#;
1401 let config: Config = serde_yaml::from_str(yaml).unwrap();
1402 let ml = config.multilingual.clone().unwrap();
1403 assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
1404 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1405
1406 let yaml2 = serde_yaml::to_string(&config).unwrap();
1407 let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
1408 assert_eq!(config2.multilingual, config.multilingual);
1409 }
1410
1411 #[test]
1412 fn test_multilingual_pattern_block_roundtrips() {
1413 let yaml = r#"
1416multilingual:
1417 title-mode:
1418 pattern:
1419 - view: original-script
1420 - view: translated
1421 wrap: brackets
1422"#;
1423 let config: Config = serde_yaml::from_str(yaml).unwrap();
1424 let ml = config.multilingual.clone().unwrap();
1425 assert!(
1426 matches!(ml.title_mode, Some(MultilingualMode::Pattern(_))),
1427 "expected Pattern mode, got {:?}",
1428 ml.title_mode
1429 );
1430
1431 let yaml2 = serde_yaml::to_string(&config).unwrap();
1432 let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
1433 assert_eq!(config2.multilingual, config.multilingual);
1434 }
1435}