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, RegimeFamily, 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 effective_processing(&self) -> Processing {
534 self.processing.clone().unwrap_or_default()
535 }
536
537 pub fn merge(&mut self, other: &Config) {
542 crate::merge_options!(
543 self,
544 other,
545 processing,
546 locale_override,
547 localize,
548 multilingual,
549 dates,
550 titles,
551 locators,
552 page_range_format,
553 links,
554 volume_pages_delimiter,
555 locale_override,
556 strip_periods,
557 notes,
558 integral_name_memory,
559 org_abbreviation_memory,
560 custom,
561 );
562
563 if let Some(other_substitute) = &other.substitute {
564 if let Some(this_substitute) = &self.substitute {
565 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
566 } else {
567 self.substitute = Some(other_substitute.clone());
568 }
569 }
570
571 if let Some(other_contributors) = &other.contributors {
572 if let Some(this_contributors) = &mut self.contributors {
573 this_contributors.merge(other_contributors);
574 } else {
575 self.contributors = Some(other_contributors.clone());
576 }
577 }
578
579 if other.punctuation_in_quote {
580 self.punctuation_in_quote = true;
581 }
582 }
583
584 pub fn merged(base: &Config, override_config: &Config) -> Config {
588 let mut result = base.clone();
589 result.merge(override_config);
590 result
591 }
592}
593
594impl CitationOptions {
595 #[must_use]
597 pub fn to_config(&self) -> Config {
598 Config {
599 substitute: self.substitute.clone(),
600 processing: self.processing.clone(),
601 locale_override: None,
602 localize: self.localize.clone(),
603 multilingual: self.multilingual.clone(),
604 contributors: self.contributors.clone(),
605 dates: self.dates.clone(),
606 titles: self.titles.clone(),
607 locators: self.locators.clone(),
608 page_range_format: self.page_range_format.clone(),
609 links: self.links.clone(),
610 punctuation_in_quote: self.punctuation_in_quote,
611 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
612 strip_periods: self.strip_periods,
613 notes: self.notes.clone(),
614 integral_name_memory: self.integral_name_memory.clone(),
615 org_abbreviation_memory: self.org_abbreviation_memory.clone(),
616 custom: self.custom.clone(),
617 unknown_fields: std::collections::BTreeMap::new(),
618 }
619 }
620
621 #[must_use]
623 pub fn merged_with(&self, base: &Config) -> Config {
624 Config::merged(base, &self.to_config())
625 }
626
627 pub fn merge(&mut self, other: &CitationOptions) {
629 crate::merge_options!(
630 self,
631 other,
632 processing,
633 localize,
634 multilingual,
635 dates,
636 titles,
637 locators,
638 page_range_format,
639 links,
640 volume_pages_delimiter,
641 strip_periods,
642 notes,
643 integral_name_memory,
644 org_abbreviation_memory,
645 label_wrap,
646 group_delimiter,
647 custom,
648 );
649
650 if let Some(other_substitute) = &other.substitute {
651 if let Some(this_substitute) = &self.substitute {
652 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
653 } else {
654 self.substitute = Some(other_substitute.clone());
655 }
656 }
657
658 if let Some(other_contributors) = &other.contributors {
659 if let Some(this_contributors) = &mut self.contributors {
660 this_contributors.merge(other_contributors);
661 } else {
662 self.contributors = Some(other_contributors.clone());
663 }
664 }
665
666 if other.punctuation_in_quote {
667 self.punctuation_in_quote = true;
668 }
669 }
670}
671
672impl BibliographyOptions {
673 #[must_use]
675 pub fn to_bibliography_config(&self) -> BibliographyConfig {
676 BibliographyConfig {
677 article_journal: self.article_journal.clone(),
678 subsequent_author_substitute: self.subsequent_author_substitute.clone(),
679 subsequent_author_substitute_rule: self.subsequent_author_substitute_rule.clone(),
680 hanging_indent: self.hanging_indent,
681 entry_suffix: self.entry_suffix.clone(),
682 separator: self.separator.clone(),
683 suppress_period_after_url: self.suppress_period_after_url,
684 custom: None,
685 compound_numeric: self.compound_numeric.clone(),
686 sort_partitioning: self.sort_partitioning.clone(),
687 unknown_fields: std::collections::BTreeMap::new(),
688 }
689 }
690
691 #[must_use]
693 pub fn to_config(&self) -> Config {
694 Config {
695 substitute: self.substitute.clone(),
696 processing: self.processing.clone(),
697 locale_override: None,
698 localize: self.localize.clone(),
699 multilingual: self.multilingual.clone(),
700 contributors: self.contributors.clone(),
701 dates: self.dates.clone(),
702 titles: self.titles.clone(),
703 locators: None,
704 page_range_format: self.page_range_format.clone(),
705 links: self.links.clone(),
706 punctuation_in_quote: self.punctuation_in_quote,
707 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
708 strip_periods: self.strip_periods,
709 notes: None,
710 integral_name_memory: None,
711 org_abbreviation_memory: None,
712 custom: self.custom.clone(),
713 unknown_fields: std::collections::BTreeMap::new(),
714 }
715 }
716
717 #[must_use]
719 pub fn merged_with(&self, base: &Config) -> Config {
720 Config::merged(base, &self.to_config())
721 }
722
723 pub fn merge(&mut self, other: &BibliographyOptions) {
725 crate::merge_options!(
726 self,
727 other,
728 processing,
729 localize,
730 multilingual,
731 dates,
732 titles,
733 page_range_format,
734 links,
735 volume_pages_delimiter,
736 strip_periods,
737 article_journal,
738 subsequent_author_substitute,
739 subsequent_author_substitute_rule,
740 hanging_indent,
741 entry_suffix,
742 separator,
743 compound_numeric,
744 sort_partitioning,
745 label_mode,
746 label_wrap,
747 date_position,
748 title_terminator,
749 repeated_author_rendering,
750 custom,
751 );
752
753 self.merge_shared_fields(other);
754 }
755
756 fn merge_shared_fields(&mut self, other: &BibliographyOptions) {
757 if let Some(other_substitute) = &other.substitute {
758 if let Some(this_substitute) = &self.substitute {
759 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
760 } else {
761 self.substitute = Some(other_substitute.clone());
762 }
763 }
764
765 if let Some(other_contributors) = &other.contributors {
766 if let Some(this_contributors) = &mut self.contributors {
767 this_contributors.merge(other_contributors);
768 } else {
769 self.contributors = Some(other_contributors.clone());
770 }
771 }
772
773 if other.punctuation_in_quote {
774 self.punctuation_in_quote = true;
775 }
776 if other.suppress_period_after_url {
777 self.suppress_period_after_url = true;
778 }
779
780 for (key, value) in &other.unknown_fields {
781 self.unknown_fields.insert(key.clone(), value.clone());
782 }
783 }
784}
785
786fn deserialize_contributor_config<'de, D>(
788 deserializer: D,
789) -> Result<Option<ContributorConfig>, D::Error>
790where
791 D: serde::Deserializer<'de>,
792{
793 let value: Option<ContributorConfigEntry> = Option::deserialize(deserializer)?;
794 Ok(value.map(|entry| entry.resolve()))
795}
796
797fn deserialize_date_config<'de, D>(deserializer: D) -> Result<Option<DateConfig>, D::Error>
799where
800 D: serde::Deserializer<'de>,
801{
802 let value: Option<DateConfigEntry> = Option::deserialize(deserializer)?;
803 Ok(value.map(|entry| entry.resolve()))
804}
805
806fn deserialize_titles_config<'de, D>(
808 deserializer: D,
809) -> Result<Option<crate::options::titles::TitlesConfig>, D::Error>
810where
811 D: serde::Deserializer<'de>,
812{
813 let value: Option<crate::options::titles::TitlesConfigEntry> =
814 Option::deserialize(deserializer)?;
815 Ok(value.map(|entry| entry.resolve()))
816}
817
818fn deserialize_locator_config<'de, D>(deserializer: D) -> Result<Option<LocatorConfig>, D::Error>
820where
821 D: serde::Deserializer<'de>,
822{
823 let value: Option<LocatorConfigEntry> = Option::deserialize(deserializer)?;
824 Ok(value.map(|entry| entry.resolve()))
825}
826
827fn deserialize_multilingual_config<'de, D>(
829 deserializer: D,
830) -> Result<Option<MultilingualConfig>, D::Error>
831where
832 D: serde::Deserializer<'de>,
833{
834 let value: Option<crate::presets::MultilingualConfigEntry> = Option::deserialize(deserializer)?;
835 Ok(value.map(|entry| entry.resolve()))
836}
837
838impl<'de> Deserialize<'de> for Config {
839 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
840 where
841 D: serde::Deserializer<'de>,
842 {
843 #[derive(Deserialize)]
844 #[serde(rename_all = "kebab-case")]
845 struct ConfigWire {
846 #[serde(skip_serializing_if = "Option::is_none")]
847 substitute: Option<SubstituteConfig>,
848 #[serde(skip_serializing_if = "Option::is_none")]
849 processing: Option<Processing>,
850 #[serde(skip_serializing_if = "Option::is_none")]
851 locale_override: Option<String>,
852 #[serde(skip_serializing_if = "Option::is_none")]
853 localize: Option<Localize>,
854 #[serde(
855 skip_serializing_if = "Option::is_none",
856 deserialize_with = "deserialize_multilingual_config",
857 default
858 )]
859 multilingual: Option<MultilingualConfig>,
860 #[serde(
861 skip_serializing_if = "Option::is_none",
862 deserialize_with = "deserialize_contributor_config",
863 default
864 )]
865 contributors: Option<ContributorConfig>,
866 #[serde(
867 skip_serializing_if = "Option::is_none",
868 deserialize_with = "deserialize_date_config",
869 default
870 )]
871 dates: Option<DateConfig>,
872 #[serde(
873 skip_serializing_if = "Option::is_none",
874 deserialize_with = "deserialize_titles_config",
875 default
876 )]
877 titles: Option<crate::options::titles::TitlesConfig>,
878 #[serde(
879 skip_serializing_if = "Option::is_none",
880 deserialize_with = "deserialize_locator_config",
881 default
882 )]
883 locators: Option<LocatorConfig>,
884 #[serde(skip_serializing_if = "Option::is_none")]
885 page_range_format: Option<PageRangeFormat>,
886 #[serde(skip_serializing_if = "Option::is_none")]
887 links: Option<LinksConfig>,
888 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
889 punctuation_in_quote: bool,
890 #[serde(skip_serializing_if = "Option::is_none")]
891 volume_pages_delimiter: Option<DelimiterPunctuation>,
892 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
893 strip_periods: Option<bool>,
894 #[serde(skip_serializing_if = "Option::is_none")]
895 notes: Option<NoteConfig>,
896 #[serde(skip_serializing_if = "Option::is_none")]
897 integral_name_memory: Option<IntegralNameMemoryConfig>,
898 #[serde(skip_serializing_if = "Option::is_none")]
899 org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
900 #[serde(default)]
901 profile: Option<serde_yaml::Value>,
902 #[serde(skip_serializing_if = "Option::is_none")]
903 custom: Option<HashMap<String, serde_json::Value>>,
904 #[serde(flatten)]
905 unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
906 }
907
908 let wire = ConfigWire::deserialize(deserializer)?;
909 if wire.profile.is_some() {
910 return Err(serde::de::Error::custom(
911 "`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`",
912 ));
913 }
914
915 Ok(Self {
916 substitute: wire.substitute,
917 processing: wire.processing,
918 locale_override: wire.locale_override,
919 localize: wire.localize,
920 multilingual: wire.multilingual,
921 contributors: wire.contributors,
922 dates: wire.dates,
923 titles: wire.titles,
924 locators: wire.locators,
925 page_range_format: wire.page_range_format,
926 links: wire.links,
927 punctuation_in_quote: wire.punctuation_in_quote,
928 volume_pages_delimiter: wire.volume_pages_delimiter,
929 strip_periods: wire.strip_periods,
930 notes: wire.notes,
931 integral_name_memory: wire.integral_name_memory,
932 org_abbreviation_memory: wire.org_abbreviation_memory,
933 custom: wire.custom,
934 unknown_fields: wire.unknown_fields,
935 })
936 }
937}
938
939#[cfg(test)]
940#[allow(
941 clippy::unwrap_used,
942 clippy::expect_used,
943 clippy::panic,
944 clippy::indexing_slicing,
945 clippy::todo,
946 clippy::unimplemented,
947 clippy::unreachable,
948 clippy::get_unwrap,
949 reason = "Panicking is acceptable and often desired in tests."
950)]
951mod tests {
952 use super::*;
953
954 #[test]
955 fn test_config_default() {
956 let config = Config::default();
957 assert!(config.substitute.is_none());
958 assert!(config.processing.is_none());
959 }
960
961 #[test]
962 fn test_author_date_processing() {
963 let processing = Processing::AuthorDate;
964 let config = processing.config();
965 let disambiguate = config.disambiguate.unwrap();
966 assert!(disambiguate.year_suffix);
967 assert!(!disambiguate.names);
968 assert!(!disambiguate.add_givenname);
969 assert_eq!(
970 processing.default_bibliography_sort(),
971 Some(crate::presets::SortPreset::AuthorDateTitle)
972 );
973 assert_eq!(
974 config.sort,
975 Some(SortEntry::Preset(
976 crate::presets::SortPreset::AuthorDateTitle
977 ))
978 );
979 }
980
981 #[test]
982 fn test_processing_default_bibliography_sorts() {
983 assert_eq!(Processing::Numeric.default_bibliography_sort(), None);
984 assert_eq!(
985 Processing::Note.default_bibliography_sort(),
986 Some(crate::presets::SortPreset::AuthorTitleDate)
987 );
988 assert_eq!(
989 Processing::Label(LabelConfig::default()).default_bibliography_sort(),
990 Some(crate::presets::SortPreset::AuthorDateTitle)
991 );
992 }
993
994 #[test]
995 fn test_processing_default_citation_sort_policy_is_explicit_only() {
996 assert_eq!(
997 Processing::AuthorDate.default_citation_sort_policy(),
998 CitationSortPolicy::ExplicitOnly
999 );
1000 assert_eq!(
1001 Processing::Note.default_citation_sort_policy(),
1002 CitationSortPolicy::ExplicitOnly
1003 );
1004 }
1005
1006 #[test]
1007 fn test_substitute_default() {
1008 let sub = Substitute::default();
1009 assert_eq!(sub.template.len(), 3);
1010 }
1011
1012 #[test]
1013 fn test_config_yaml_roundtrip() {
1014 let yaml = r#"
1015substitute:
1016 contributor-role-form: short
1017 template:
1018 - editor
1019 - title
1020processing: author-date
1021contributors:
1022 display-as-sort: first
1023 and: symbol
1024"#;
1025 let config: Config = serde_yaml::from_str(yaml).unwrap();
1026 assert!(config.substitute.is_some());
1027 assert_eq!(config.processing, Some(Processing::AuthorDate));
1028 assert_eq!(
1029 config.contributors.as_ref().unwrap().and,
1030 Some(AndOptions::Symbol)
1031 );
1032 }
1033
1034 #[test]
1035 fn test_contributor_config_preset() {
1036 let yaml = r#"contributors: apa"#;
1038 let config: Config = serde_yaml::from_str(yaml).unwrap();
1039 let contributors = config.contributors.unwrap();
1040 assert_eq!(contributors.and, Some(AndOptions::Symbol));
1041 assert_eq!(contributors.display_as_sort, Some(DisplayAsSort::First));
1042 }
1043
1044 #[test]
1045 fn test_role_label_presets_parse_and_resolve_precedence() {
1046 let yaml = r#"
1047contributors:
1048 role:
1049 preset: short-suffix
1050 roles:
1051 editor:
1052 preset: long-suffix
1053"#;
1054 let config: Config = serde_yaml::from_str(yaml).unwrap();
1055 let contributors = config.contributors.unwrap();
1056
1057 assert_eq!(
1058 contributors.effective_role_label_preset(&crate::template::ContributorRole::Editor),
1059 Some(RoleLabelPreset::LongSuffix)
1060 );
1061 assert_eq!(
1062 contributors.effective_role_label_preset(&crate::template::ContributorRole::Translator),
1063 Some(RoleLabelPreset::ShortSuffix)
1064 );
1065
1066 let yaml_scalar = r#"
1068contributors:
1069 role: short-suffix
1070"#;
1071 let config2: Config = serde_yaml::from_str(yaml_scalar).unwrap();
1072 let contributors2 = config2.contributors.unwrap();
1073
1074 assert_eq!(
1075 contributors2
1076 .effective_role_label_preset(&crate::template::ContributorRole::Translator),
1077 Some(RoleLabelPreset::ShortSuffix)
1078 );
1079 }
1080
1081 #[test]
1082 fn test_role_specific_name_order_override_is_available() {
1083 let yaml = r#"
1084contributors:
1085 role:
1086 roles:
1087 translator:
1088 name-order: given-first
1089"#;
1090 let config: Config = serde_yaml::from_str(yaml).unwrap();
1091 let contributors = config.contributors.unwrap();
1092
1093 assert_eq!(
1094 contributors.effective_role_name_order(&crate::template::ContributorRole::Translator),
1095 Some(&crate::template::NameOrder::GivenFirst)
1096 );
1097 }
1098
1099 #[test]
1100 fn test_date_config_preset() {
1101 let yaml = r#"dates: long"#;
1103 let config: Config = serde_yaml::from_str(yaml).unwrap();
1104 let dates = config.dates.unwrap();
1105 assert_eq!(dates.month, MonthFormat::Long);
1106 }
1107
1108 #[test]
1109 fn test_titles_config_preset() {
1110 let yaml = r#"titles: chicago"#;
1112 let config: Config = serde_yaml::from_str(yaml).unwrap();
1113 let titles = config.titles.unwrap();
1114 assert_eq!(titles.component.unwrap().quote, Some(true));
1115 assert_eq!(titles.monograph.unwrap().emph, Some(true));
1116 }
1117
1118 #[test]
1119 fn test_substitute_config_preset() {
1120 let yaml = r#"substitute: standard"#;
1122 let config: Config = serde_yaml::from_str(yaml).unwrap();
1123 assert!(config.substitute.is_some());
1124 let resolved = config.substitute.unwrap().resolve();
1125 assert_eq!(resolved.template.len(), 3);
1126 assert_eq!(resolved.template[0], SubstituteKey::Editor);
1127 }
1128
1129 #[test]
1130 fn test_substitute_config_explicit() {
1131 let yaml = r#"
1133substitute:
1134 template:
1135 - title
1136 - editor
1137"#;
1138 let config: Config = serde_yaml::from_str(yaml).unwrap();
1139 let resolved = config.substitute.unwrap().resolve();
1140 assert_eq!(resolved.template[0], SubstituteKey::Title);
1141 assert_eq!(resolved.template[1], SubstituteKey::Editor);
1142 }
1143
1144 #[test]
1145 fn test_config_merge_precedence() {
1146 let base_yaml = r#"
1148processing: author-date
1149locale-override: en-US-base
1150contributors:
1151 display-as-sort: first
1152 and: symbol
1153"#;
1154 let mut base: Config = serde_yaml::from_str(base_yaml).unwrap();
1155
1156 let override_yaml = r#"
1158contributors:
1159 and: text
1160locale-override: en-US-chicago
1161"#;
1162 let override_config: Config = serde_yaml::from_str(override_yaml).unwrap();
1163
1164 base.merge(&override_config);
1166
1167 assert_eq!(base.processing, Some(Processing::AuthorDate));
1169 assert_eq!(base.locale_override.as_deref(), Some("en-US-chicago"));
1170
1171 assert_eq!(
1173 base.contributors.as_ref().unwrap().and,
1174 Some(AndOptions::Text)
1175 );
1176 }
1177
1178 #[test]
1179 fn test_config_deserializes_locale_override() {
1180 let config: Config = serde_yaml::from_str("locale-override: en-US-chicago").unwrap();
1181 assert_eq!(config.locale_override.as_deref(), Some("en-US-chicago"));
1182 }
1183
1184 #[test]
1185 fn test_config_merged_convenience() {
1186 let base = Config {
1187 processing: Some(Processing::AuthorDate),
1188 ..Default::default()
1189 };
1190 let override_config = Config {
1191 punctuation_in_quote: true,
1192 ..Default::default()
1193 };
1194
1195 let merged = Config::merged(&base, &override_config);
1196
1197 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1199 assert!(merged.punctuation_in_quote);
1200 }
1201
1202 #[test]
1203 fn test_citation_options_merge_overrides_citation_fields_only() {
1204 let base = Config {
1205 processing: Some(Processing::AuthorDate),
1206 ..Default::default()
1207 };
1208
1209 let overrides = CitationOptions {
1210 strip_periods: Some(true),
1211 locators: Some(LocatorConfig::default()),
1212 ..Default::default()
1213 };
1214
1215 let merged = overrides.merged_with(&base);
1216 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1217 assert!(merged.strip_periods.unwrap_or(false));
1218 assert!(merged.locators.is_some());
1219 }
1220
1221 #[test]
1222 fn test_bibliography_options_merge_projects_shared_fields_only() {
1223 let base = Config {
1224 processing: Some(Processing::AuthorDate),
1225 ..Default::default()
1226 };
1227
1228 let overrides = BibliographyOptions {
1229 entry_suffix: Some(".".to_string()),
1230 separator: Some(", ".to_string()),
1231 suppress_period_after_url: true,
1232 ..Default::default()
1233 };
1234
1235 let merged = overrides.merged_with(&base);
1236 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1237 assert!(merged.locators.is_none());
1238 assert!(merged.notes.is_none());
1239 let bibliography = overrides.to_bibliography_config();
1240 assert_eq!(bibliography.entry_suffix.as_deref(), Some("."));
1241 assert_eq!(bibliography.separator.as_deref(), Some(", "));
1242 assert!(bibliography.suppress_period_after_url);
1243 }
1244
1245 #[test]
1246 fn test_bibliography_options_merge_leaves_shared_base_when_only_shared_overrides_exist() {
1247 let base = Config {
1248 processing: Some(Processing::AuthorDate),
1249 ..Default::default()
1250 };
1251
1252 let overrides = BibliographyOptions {
1253 contributors: Some(ContributorConfig::default()),
1254 ..Default::default()
1255 };
1256
1257 let merged = overrides.merged_with(&base);
1258 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1259 assert!(merged.contributors.is_some());
1260 }
1261
1262 #[test]
1263 fn citation_options_captures_unknown_fields_for_forward_compat() {
1264 let yaml = "future-key: true\n";
1265 let opts: CitationOptions = serde_yaml::from_str(yaml).unwrap();
1266 assert!(opts.unknown_fields.contains_key("future-key"));
1267 }
1268
1269 #[test]
1270 fn bibliography_options_captures_unknown_fields_for_forward_compat() {
1271 let yaml = "future-key: true\n";
1272 let opts: BibliographyOptions = serde_yaml::from_str(yaml).unwrap();
1273 assert!(opts.unknown_fields.contains_key("future-key"));
1274 }
1275
1276 #[test]
1277 fn note_config_captures_unknown_fields_for_forward_compat() {
1278 let yaml = "punctuation: inside\nfuture-key: true\n";
1279 let cfg: NoteConfig = serde_yaml::from_str(yaml).unwrap();
1280 assert!(cfg.unknown_fields.contains_key("future-key"));
1281 assert_eq!(cfg.punctuation, Some(NoteQuotePlacement::Inside));
1282 }
1283
1284 #[test]
1285 fn test_multilingual_preset_romanized_translated_parses_and_resolves() {
1286 let yaml = r#"multilingual: romanized-translated"#;
1288 let config: Config = serde_yaml::from_str(yaml).unwrap();
1289 let ml = config.multilingual.unwrap();
1290 assert_eq!(ml.title_mode, Some(MultilingualMode::Combined));
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_preset_romanized_only_parses_and_resolves() {
1297 let yaml = r#"multilingual: romanized-only"#;
1299 let config: Config = serde_yaml::from_str(yaml).unwrap();
1300 let ml = config.multilingual.unwrap();
1301 assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
1302 assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
1303 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1304 }
1305
1306 #[test]
1307 fn test_multilingual_preset_romanized_script_translated_parses_and_resolves() {
1308 use crate::options::multilingual::{MultilingualSegment, MultilingualView, SegmentWrap};
1311 let yaml = r#"multilingual: romanized-script-translated"#;
1312 let config: Config = serde_yaml::from_str(yaml).unwrap();
1313 let ml = config.multilingual.unwrap();
1314 assert_eq!(
1315 ml.title_mode,
1316 Some(MultilingualMode::Pattern(vec![
1317 MultilingualSegment {
1318 view: MultilingualView::Transliterated,
1319 wrap: SegmentWrap::None,
1320 },
1321 MultilingualSegment {
1322 view: MultilingualView::OriginalScript,
1323 wrap: SegmentWrap::None,
1324 },
1325 MultilingualSegment {
1326 view: MultilingualView::Translated,
1327 wrap: SegmentWrap::Brackets,
1328 },
1329 ]))
1330 );
1331 assert_eq!(
1332 ml.name_mode,
1333 Some(MultilingualMode::Pattern(vec![
1334 MultilingualSegment {
1335 view: MultilingualView::Transliterated,
1336 wrap: SegmentWrap::None,
1337 },
1338 MultilingualSegment {
1339 view: MultilingualView::OriginalScript,
1340 wrap: SegmentWrap::None,
1341 },
1342 ]))
1343 );
1344 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1345 assert!(ml.scripts.get("Han").is_some_and(|s| s.use_native_ordering));
1346 assert!(
1347 ml.scripts
1348 .get("Hangul")
1349 .is_some_and(|s| s.use_native_ordering)
1350 );
1351 }
1352
1353 #[test]
1354 fn test_multilingual_explicit_block_transliterated_roundtrips() {
1355 let yaml = r#"
1357multilingual:
1358 title-mode: transliterated
1359 preferred-script: Latn
1360"#;
1361 let config: Config = serde_yaml::from_str(yaml).unwrap();
1362 let ml = config.multilingual.clone().unwrap();
1363 assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
1364 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1365
1366 let yaml2 = serde_yaml::to_string(&config).unwrap();
1367 let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
1368 assert_eq!(config2.multilingual, config.multilingual);
1369 }
1370
1371 #[test]
1372 fn test_multilingual_pattern_block_roundtrips() {
1373 let yaml = r#"
1376multilingual:
1377 title-mode:
1378 pattern:
1379 - view: original-script
1380 - view: translated
1381 wrap: brackets
1382"#;
1383 let config: Config = serde_yaml::from_str(yaml).unwrap();
1384 let ml = config.multilingual.clone().unwrap();
1385 assert!(
1386 matches!(ml.title_mode, Some(MultilingualMode::Pattern(_))),
1387 "expected Pattern mode, got {:?}",
1388 ml.title_mode
1389 );
1390
1391 let yaml2 = serde_yaml::to_string(&config).unwrap();
1392 let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
1393 assert_eq!(config2.multilingual, config.multilingual);
1394 }
1395}