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 sorting;
18pub mod substitute;
19pub mod title_class;
20
21pub use crate::presets::{MultilingualConfigEntry, MultilingualPreset};
22pub use bibliography::{
23 AnonymousEntriesMode, ArticleJournalBibliographyConfig, ArticleJournalNoPageFallback,
24 BibliographyConfig, BibliographyPartitionHeading, BibliographyPartitionKind,
25 BibliographyPartitionMode, BibliographySortPartitioning, SubsequentAuthorSubstituteRule,
26};
27pub use contributors::{
28 AndOptions, AndOtherOptions, ContributorConfig, ContributorConfigEntry, DelimiterPrecedesLast,
29 DemoteNonDroppingParticle, DisplayAsSort, NameForm, RoleLabelDefaults, RoleLabelPreset,
30 RoleOptions, RoleOptionsEntry, RoleRendering, ShortenListOptions,
31};
32pub use dates::{DateConfig, DateConfigEntry, NoDateForm};
33pub use integral_name_memory::{
34 IntegralNameContexts, IntegralNameMemoryConfig, IntegralNameScope, OrgAbbreviationMemoryConfig,
35 ResolvedIntegralNameMemoryConfig, ResolvedOrgAbbreviationMemoryConfig, ShortNameDisplay,
36 SubsequentNameForm,
37};
38pub use localization::{Localize, MonthFormat, Scope};
39pub use locators::{
40 LabelForm, LabelRepeat, LocatorConfig, LocatorConfigEntry, LocatorKindConfig, LocatorPattern,
41 LocatorPreset, TypeClass,
42};
43pub use multilingual::{
44 MultilingualConfig, MultilingualMode, MultilingualSegment, MultilingualView, ScriptConfig,
45 SegmentWrap,
46};
47pub use processing::{
48 CitationSortPolicy, Disambiguation, GivennameRule, Group, LabelConfig, LabelParams,
49 LabelPreset, Processing, ProcessingBase, ProcessingCustom, RegimeFamily, Sort, SortEntry,
50 SortKey, SortSpec,
51};
52pub use scoped::{
53 BibliographyLabelMode, BibliographyLabelWrap, CitationGroupDelimiter, DatePosition, LabelWrap,
54 RepeatedAuthorRendering, TitleTerminator,
55};
56pub use sorting::{SortingConfig, SortingLocale, SortingMultilingualMode};
57pub use substitute::{Substitute, SubstituteConfig, SubstituteKey, SubstituteTitleQuoteMode};
58
59use crate::template::DelimiterPunctuation;
60#[cfg(feature = "schema")]
61use schemars::JsonSchema;
62use serde::{Deserialize, Serialize};
63use std::collections::HashMap;
64
65#[derive(Debug, Default, PartialEq, Clone, Serialize)]
67#[cfg_attr(feature = "schema", derive(JsonSchema))]
68#[serde(rename_all = "kebab-case")]
69pub struct Config {
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub substitute: Option<SubstituteConfig>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub processing: Option<Processing>,
76 #[serde(skip_serializing_if = "Option::is_none")]
82 pub locale_override: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub localize: Option<Localize>,
86 #[serde(
89 skip_serializing_if = "Option::is_none",
90 deserialize_with = "deserialize_multilingual_config",
91 default
92 )]
93 #[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
94 pub multilingual: Option<MultilingualConfig>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub sorting: Option<SortingConfig>,
98 #[serde(
101 skip_serializing_if = "Option::is_none",
102 deserialize_with = "deserialize_contributor_config",
103 default
104 )]
105 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
106 pub contributors: Option<ContributorConfig>,
107 #[serde(
110 skip_serializing_if = "Option::is_none",
111 deserialize_with = "deserialize_date_config",
112 default
113 )]
114 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
115 pub dates: Option<DateConfig>,
116 #[serde(
119 skip_serializing_if = "Option::is_none",
120 deserialize_with = "deserialize_titles_config",
121 default
122 )]
123 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
124 pub titles: Option<crate::options::titles::TitlesConfig>,
125 #[serde(
128 skip_serializing_if = "Option::is_none",
129 deserialize_with = "deserialize_locator_config",
130 default
131 )]
132 #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
133 pub locators: Option<LocatorConfig>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub page_range_format: Option<PageRangeFormat>,
137 #[serde(skip_serializing_if = "Option::is_none")]
140 pub page_range_delimiter: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub links: Option<LinksConfig>,
144 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
148 pub punctuation_in_quote: bool,
149 #[serde(skip_serializing_if = "Option::is_none")]
153 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
154 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
156 pub strip_periods: Option<bool>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub notes: Option<NoteConfig>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub integral_name_memory: Option<IntegralNameMemoryConfig>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub custom: Option<HashMap<String, serde_json::Value>>,
169 #[serde(
173 flatten,
174 default,
175 skip_serializing_if = "std::collections::BTreeMap::is_empty"
176 )]
177 #[cfg_attr(feature = "schema", schemars(skip))]
178 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
179}
180
181#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
183#[cfg_attr(feature = "schema", derive(JsonSchema))]
184#[serde(rename_all = "kebab-case")]
185pub struct CitationOptions {
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub substitute: Option<SubstituteConfig>,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub processing: Option<Processing>,
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub localize: Option<Localize>,
195 #[serde(
198 skip_serializing_if = "Option::is_none",
199 deserialize_with = "deserialize_multilingual_config",
200 default
201 )]
202 #[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
203 pub multilingual: Option<MultilingualConfig>,
204 #[serde(
206 skip_serializing_if = "Option::is_none",
207 deserialize_with = "deserialize_contributor_config",
208 default
209 )]
210 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
211 pub contributors: Option<ContributorConfig>,
212 #[serde(
214 skip_serializing_if = "Option::is_none",
215 deserialize_with = "deserialize_date_config",
216 default
217 )]
218 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
219 pub dates: Option<DateConfig>,
220 #[serde(
222 skip_serializing_if = "Option::is_none",
223 deserialize_with = "deserialize_titles_config",
224 default
225 )]
226 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
227 pub titles: Option<crate::options::titles::TitlesConfig>,
228 #[serde(
230 skip_serializing_if = "Option::is_none",
231 deserialize_with = "deserialize_locator_config",
232 default
233 )]
234 #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
235 pub locators: Option<LocatorConfig>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub page_range_format: Option<PageRangeFormat>,
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub links: Option<LinksConfig>,
242 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
244 pub punctuation_in_quote: bool,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
248 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
250 pub strip_periods: Option<bool>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub notes: Option<NoteConfig>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub integral_name_memory: Option<IntegralNameMemoryConfig>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub label_wrap: Option<LabelWrap>,
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub group_delimiter: Option<CitationGroupDelimiter>,
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub custom: Option<HashMap<String, serde_json::Value>>,
269 #[serde(
273 flatten,
274 default,
275 skip_serializing_if = "std::collections::BTreeMap::is_empty"
276 )]
277 #[cfg_attr(feature = "schema", schemars(skip))]
278 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
279}
280
281#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
283#[cfg_attr(feature = "schema", derive(JsonSchema))]
284#[serde(rename_all = "kebab-case")]
285pub struct BibliographyOptions {
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub substitute: Option<SubstituteConfig>,
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub processing: Option<Processing>,
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub localize: Option<Localize>,
295 #[serde(
298 skip_serializing_if = "Option::is_none",
299 deserialize_with = "deserialize_multilingual_config",
300 default
301 )]
302 #[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
303 pub multilingual: Option<MultilingualConfig>,
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub sorting: Option<SortingConfig>,
307 #[serde(
309 skip_serializing_if = "Option::is_none",
310 deserialize_with = "deserialize_contributor_config",
311 default
312 )]
313 #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
314 pub contributors: Option<ContributorConfig>,
315 #[serde(
317 skip_serializing_if = "Option::is_none",
318 deserialize_with = "deserialize_date_config",
319 default
320 )]
321 #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
322 pub dates: Option<DateConfig>,
323 #[serde(
325 skip_serializing_if = "Option::is_none",
326 deserialize_with = "deserialize_titles_config",
327 default
328 )]
329 #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
330 pub titles: Option<crate::options::titles::TitlesConfig>,
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub page_range_format: Option<PageRangeFormat>,
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub article_journal: Option<ArticleJournalBibliographyConfig>,
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub subsequent_author_substitute: Option<String>,
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub subsequent_author_substitute_rule: Option<SubsequentAuthorSubstituteRule>,
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub hanging_indent: Option<bool>,
346 #[serde(skip_serializing_if = "Option::is_none")]
348 pub entry_suffix: Option<String>,
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub separator: Option<String>,
352 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
354 pub suppress_period_after_url: bool,
355 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
357 pub entry_suffix_after_url: bool,
358 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
360 pub entry_suffix_after_doi: bool,
361 #[serde(skip_serializing_if = "Option::is_none")]
363 pub compound_numeric: Option<bibliography::CompoundNumericConfig>,
364 #[serde(skip_serializing_if = "Option::is_none")]
366 pub sort_partitioning: Option<bibliography::BibliographySortPartitioning>,
367 #[serde(skip_serializing_if = "Option::is_none")]
371 pub anonymous_entries: Option<AnonymousEntriesMode>,
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub links: Option<LinksConfig>,
375 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
377 pub punctuation_in_quote: bool,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub volume_pages_delimiter: Option<DelimiterPunctuation>,
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub label_mode: Option<BibliographyLabelMode>,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub label_wrap: Option<BibliographyLabelWrap>,
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub date_position: Option<DatePosition>,
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub title_terminator: Option<TitleTerminator>,
393 #[serde(skip_serializing_if = "Option::is_none")]
395 pub repeated_author_rendering: Option<RepeatedAuthorRendering>,
396 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
398 pub strip_periods: Option<bool>,
399 #[serde(skip_serializing_if = "Option::is_none")]
401 pub custom: Option<HashMap<String, serde_json::Value>>,
402 #[serde(
406 flatten,
407 default,
408 skip_serializing_if = "std::collections::BTreeMap::is_empty"
409 )]
410 #[cfg_attr(feature = "schema", schemars(skip))]
411 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
412}
413
414#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
416#[cfg_attr(feature = "schema", derive(JsonSchema))]
417#[serde(rename_all = "kebab-case")]
418pub struct NoteConfig {
419 #[serde(skip_serializing_if = "Option::is_none")]
422 pub punctuation: Option<NoteQuotePlacement>,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub number: Option<NoteNumberPlacement>,
426 #[serde(skip_serializing_if = "Option::is_none")]
429 pub order: Option<NoteMarkerOrder>,
430 #[serde(
434 flatten,
435 default,
436 skip_serializing_if = "std::collections::BTreeMap::is_empty"
437 )]
438 #[cfg_attr(feature = "schema", schemars(skip))]
439 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
440}
441
442#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
444#[cfg_attr(feature = "schema", derive(JsonSchema))]
445#[serde(rename_all = "kebab-case")]
446pub enum NoteQuotePlacement {
447 Inside,
449 Outside,
451 #[default]
455 Adaptive,
456}
457
458#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
460#[cfg_attr(feature = "schema", derive(JsonSchema))]
461#[serde(rename_all = "kebab-case")]
462pub enum NoteNumberPlacement {
463 Inside,
465 #[default]
467 Outside,
468 Same,
471}
472
473#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
475#[cfg_attr(feature = "schema", derive(JsonSchema))]
476#[serde(rename_all = "kebab-case")]
477pub enum NoteMarkerOrder {
478 Before,
480 #[default]
482 After,
483}
484
485#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
487#[cfg_attr(feature = "schema", derive(JsonSchema))]
488#[serde(rename_all = "kebab-case")]
489#[non_exhaustive]
490pub enum PageRangeFormat {
491 #[default]
493 Expanded,
494 Minimal,
496 MinimalTwo,
498 Chicago,
500 Chicago16,
502}
503
504pub mod titles;
505
506pub use title_class::{
507 TitleCategory, classified_ref_types, container_title_category, parent_serial_title_category,
508 title_category,
509};
510pub use titles::{TextCase, TitleRendering, TitlesConfig, TitlesConfigEntry};
511
512#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
514#[cfg_attr(feature = "schema", derive(JsonSchema))]
515#[serde(rename_all = "kebab-case")]
516pub struct LinksConfig {
517 #[serde(skip_serializing_if = "Option::is_none")]
519 pub doi: Option<bool>,
520 #[serde(skip_serializing_if = "Option::is_none")]
522 pub url: Option<bool>,
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub target: Option<LinkTarget>,
526 #[serde(skip_serializing_if = "Option::is_none")]
528 pub anchor: Option<LinkAnchor>,
529}
530
531#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
533#[cfg_attr(feature = "schema", derive(JsonSchema))]
534#[serde(rename_all = "kebab-case")]
535pub enum LinkTarget {
536 Url,
537 Doi,
538 UrlOrDoi,
539 Pubmed,
540 Pmcid,
541}
542
543#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
545#[cfg_attr(feature = "schema", derive(JsonSchema))]
546#[serde(rename_all = "kebab-case")]
547pub enum LinkAnchor {
548 Title,
550 Url,
552 Doi,
554 Component,
556 Entry,
558}
559
560impl Config {
561 pub fn effective_processing(&self) -> Processing {
566 self.processing.clone().unwrap_or_default()
567 }
568
569 pub fn merge(&mut self, other: &Config) {
574 crate::merge_options!(
575 self,
576 other,
577 processing,
578 locale_override,
579 localize,
580 multilingual,
581 dates,
582 titles,
583 locators,
584 page_range_format,
585 page_range_delimiter,
586 links,
587 volume_pages_delimiter,
588 locale_override,
589 strip_periods,
590 notes,
591 integral_name_memory,
592 org_abbreviation_memory,
593 custom,
594 );
595
596 if let Some(other_sorting) = &other.sorting {
597 if let Some(this_sorting) = &mut self.sorting {
598 this_sorting.merge(other_sorting);
599 } else {
600 self.sorting = Some(other_sorting.clone());
601 }
602 }
603
604 if let Some(other_substitute) = &other.substitute {
605 if let Some(this_substitute) = &self.substitute {
606 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
607 } else {
608 self.substitute = Some(other_substitute.clone());
609 }
610 }
611
612 if let Some(other_contributors) = &other.contributors {
613 if let Some(this_contributors) = &mut self.contributors {
614 this_contributors.merge(other_contributors);
615 } else {
616 self.contributors = Some(other_contributors.clone());
617 }
618 }
619
620 if other.punctuation_in_quote {
621 self.punctuation_in_quote = true;
622 }
623 }
624
625 pub fn merged(base: &Config, override_config: &Config) -> Config {
629 let mut result = base.clone();
630 result.merge(override_config);
631 result
632 }
633}
634
635impl CitationOptions {
636 #[must_use]
638 pub fn to_config(&self) -> Config {
639 Config {
640 substitute: self.substitute.clone(),
641 processing: self.processing.clone(),
642 locale_override: None,
643 localize: self.localize.clone(),
644 multilingual: self.multilingual.clone(),
645 sorting: None,
646 contributors: self.contributors.clone(),
647 dates: self.dates.clone(),
648 titles: self.titles.clone(),
649 locators: self.locators.clone(),
650 page_range_format: self.page_range_format.clone(),
651 page_range_delimiter: None,
652 links: self.links.clone(),
653 punctuation_in_quote: self.punctuation_in_quote,
654 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
655 strip_periods: self.strip_periods,
656 notes: self.notes.clone(),
657 integral_name_memory: self.integral_name_memory.clone(),
658 org_abbreviation_memory: self.org_abbreviation_memory.clone(),
659 custom: self.custom.clone(),
660 unknown_fields: std::collections::BTreeMap::new(),
661 }
662 }
663
664 #[must_use]
666 pub fn merged_with(&self, base: &Config) -> Config {
667 Config::merged(base, &self.to_config())
668 }
669
670 pub fn merge(&mut self, other: &CitationOptions) {
672 crate::merge_options!(
673 self,
674 other,
675 processing,
676 localize,
677 multilingual,
678 dates,
679 titles,
680 locators,
681 page_range_format,
682 links,
683 volume_pages_delimiter,
684 strip_periods,
685 notes,
686 integral_name_memory,
687 org_abbreviation_memory,
688 label_wrap,
689 group_delimiter,
690 custom,
691 );
692
693 if let Some(other_substitute) = &other.substitute {
694 if let Some(this_substitute) = &self.substitute {
695 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
696 } else {
697 self.substitute = Some(other_substitute.clone());
698 }
699 }
700
701 if let Some(other_contributors) = &other.contributors {
702 if let Some(this_contributors) = &mut self.contributors {
703 this_contributors.merge(other_contributors);
704 } else {
705 self.contributors = Some(other_contributors.clone());
706 }
707 }
708
709 if other.punctuation_in_quote {
710 self.punctuation_in_quote = true;
711 }
712 }
713}
714
715impl BibliographyOptions {
716 #[must_use]
718 pub fn to_bibliography_config(&self) -> BibliographyConfig {
719 BibliographyConfig {
720 article_journal: self.article_journal.clone(),
721 subsequent_author_substitute: self.subsequent_author_substitute.clone(),
722 subsequent_author_substitute_rule: self.subsequent_author_substitute_rule.clone(),
723 hanging_indent: self.hanging_indent,
724 entry_suffix: self.entry_suffix.clone(),
725 separator: self.separator.clone(),
726 suppress_period_after_url: self.suppress_period_after_url,
727 entry_suffix_after_url: self.entry_suffix_after_url,
728 entry_suffix_after_doi: self.entry_suffix_after_doi,
729 custom: None,
730 compound_numeric: self.compound_numeric.clone(),
731 sort_partitioning: self.sort_partitioning.clone(),
732 anonymous_entries: self.anonymous_entries,
733 unknown_fields: std::collections::BTreeMap::new(),
734 }
735 }
736
737 #[must_use]
739 pub fn to_config(&self) -> Config {
740 Config {
741 substitute: self.substitute.clone(),
742 processing: self.processing.clone(),
743 locale_override: None,
744 localize: self.localize.clone(),
745 multilingual: self.multilingual.clone(),
746 sorting: self.sorting.clone(),
747 contributors: self.contributors.clone(),
748 dates: self.dates.clone(),
749 titles: self.titles.clone(),
750 locators: None,
751 page_range_format: self.page_range_format.clone(),
752 page_range_delimiter: None,
753 links: self.links.clone(),
754 punctuation_in_quote: self.punctuation_in_quote,
755 volume_pages_delimiter: self.volume_pages_delimiter.clone(),
756 strip_periods: self.strip_periods,
757 notes: None,
758 integral_name_memory: None,
759 org_abbreviation_memory: None,
760 custom: self.custom.clone(),
761 unknown_fields: std::collections::BTreeMap::new(),
762 }
763 }
764
765 #[must_use]
767 pub fn merged_with(&self, base: &Config) -> Config {
768 Config::merged(base, &self.to_config())
769 }
770
771 pub fn merge(&mut self, other: &BibliographyOptions) {
773 crate::merge_options!(
774 self,
775 other,
776 processing,
777 localize,
778 multilingual,
779 dates,
780 titles,
781 page_range_format,
782 links,
783 volume_pages_delimiter,
784 strip_periods,
785 article_journal,
786 subsequent_author_substitute,
787 subsequent_author_substitute_rule,
788 hanging_indent,
789 entry_suffix,
790 separator,
791 compound_numeric,
792 sort_partitioning,
793 anonymous_entries,
794 label_mode,
795 label_wrap,
796 date_position,
797 title_terminator,
798 repeated_author_rendering,
799 custom,
800 );
801
802 self.merge_shared_fields(other);
803 }
804
805 fn merge_shared_fields(&mut self, other: &BibliographyOptions) {
806 if let Some(other_sorting) = &other.sorting {
807 if let Some(this_sorting) = &mut self.sorting {
808 this_sorting.merge(other_sorting);
809 } else {
810 self.sorting = Some(other_sorting.clone());
811 }
812 }
813
814 if let Some(other_substitute) = &other.substitute {
815 if let Some(this_substitute) = &self.substitute {
816 self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
817 } else {
818 self.substitute = Some(other_substitute.clone());
819 }
820 }
821
822 if let Some(other_contributors) = &other.contributors {
823 if let Some(this_contributors) = &mut self.contributors {
824 this_contributors.merge(other_contributors);
825 } else {
826 self.contributors = Some(other_contributors.clone());
827 }
828 }
829
830 if other.punctuation_in_quote {
831 self.punctuation_in_quote = true;
832 }
833 if other.suppress_period_after_url {
834 self.suppress_period_after_url = true;
835 }
836 if other.entry_suffix_after_url {
837 self.entry_suffix_after_url = true;
838 }
839 if other.entry_suffix_after_doi {
840 self.entry_suffix_after_doi = true;
841 }
842
843 for (key, value) in &other.unknown_fields {
844 self.unknown_fields.insert(key.clone(), value.clone());
845 }
846 }
847}
848
849fn deserialize_contributor_config<'de, D>(
851 deserializer: D,
852) -> Result<Option<ContributorConfig>, D::Error>
853where
854 D: serde::Deserializer<'de>,
855{
856 let value: Option<ContributorConfigEntry> = Option::deserialize(deserializer)?;
857 Ok(value.map(|entry| entry.resolve()))
858}
859
860fn deserialize_date_config<'de, D>(deserializer: D) -> Result<Option<DateConfig>, D::Error>
862where
863 D: serde::Deserializer<'de>,
864{
865 let value: Option<DateConfigEntry> = Option::deserialize(deserializer)?;
866 Ok(value.map(|entry| entry.resolve()))
867}
868
869fn deserialize_titles_config<'de, D>(
871 deserializer: D,
872) -> Result<Option<crate::options::titles::TitlesConfig>, D::Error>
873where
874 D: serde::Deserializer<'de>,
875{
876 let value: Option<crate::options::titles::TitlesConfigEntry> =
877 Option::deserialize(deserializer)?;
878 Ok(value.map(|entry| entry.resolve()))
879}
880
881fn deserialize_locator_config<'de, D>(deserializer: D) -> Result<Option<LocatorConfig>, D::Error>
883where
884 D: serde::Deserializer<'de>,
885{
886 let value: Option<LocatorConfigEntry> = Option::deserialize(deserializer)?;
887 Ok(value.map(|entry| entry.resolve()))
888}
889
890fn deserialize_multilingual_config<'de, D>(
892 deserializer: D,
893) -> Result<Option<MultilingualConfig>, D::Error>
894where
895 D: serde::Deserializer<'de>,
896{
897 let value: Option<crate::presets::MultilingualConfigEntry> = Option::deserialize(deserializer)?;
898 Ok(value.map(|entry| entry.resolve()))
899}
900
901impl<'de> Deserialize<'de> for Config {
902 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
903 where
904 D: serde::Deserializer<'de>,
905 {
906 #[derive(Deserialize)]
907 #[serde(rename_all = "kebab-case")]
908 struct ConfigWire {
909 #[serde(skip_serializing_if = "Option::is_none")]
910 substitute: Option<SubstituteConfig>,
911 #[serde(skip_serializing_if = "Option::is_none")]
912 processing: Option<Processing>,
913 #[serde(skip_serializing_if = "Option::is_none")]
914 locale_override: Option<String>,
915 #[serde(skip_serializing_if = "Option::is_none")]
916 localize: Option<Localize>,
917 #[serde(
918 skip_serializing_if = "Option::is_none",
919 deserialize_with = "deserialize_multilingual_config",
920 default
921 )]
922 multilingual: Option<MultilingualConfig>,
923 #[serde(skip_serializing_if = "Option::is_none")]
924 sorting: Option<SortingConfig>,
925 #[serde(
926 skip_serializing_if = "Option::is_none",
927 deserialize_with = "deserialize_contributor_config",
928 default
929 )]
930 contributors: Option<ContributorConfig>,
931 #[serde(
932 skip_serializing_if = "Option::is_none",
933 deserialize_with = "deserialize_date_config",
934 default
935 )]
936 dates: Option<DateConfig>,
937 #[serde(
938 skip_serializing_if = "Option::is_none",
939 deserialize_with = "deserialize_titles_config",
940 default
941 )]
942 titles: Option<crate::options::titles::TitlesConfig>,
943 #[serde(
944 skip_serializing_if = "Option::is_none",
945 deserialize_with = "deserialize_locator_config",
946 default
947 )]
948 locators: Option<LocatorConfig>,
949 #[serde(skip_serializing_if = "Option::is_none")]
950 page_range_format: Option<PageRangeFormat>,
951 #[serde(skip_serializing_if = "Option::is_none")]
952 page_range_delimiter: Option<String>,
953 #[serde(skip_serializing_if = "Option::is_none")]
954 links: Option<LinksConfig>,
955 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
956 punctuation_in_quote: bool,
957 #[serde(skip_serializing_if = "Option::is_none")]
958 volume_pages_delimiter: Option<DelimiterPunctuation>,
959 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
960 strip_periods: Option<bool>,
961 #[serde(skip_serializing_if = "Option::is_none")]
962 notes: Option<NoteConfig>,
963 #[serde(skip_serializing_if = "Option::is_none")]
964 integral_name_memory: Option<IntegralNameMemoryConfig>,
965 #[serde(skip_serializing_if = "Option::is_none")]
966 org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
967 #[serde(default)]
968 profile: Option<serde_yaml::Value>,
969 #[serde(skip_serializing_if = "Option::is_none")]
970 custom: Option<HashMap<String, serde_json::Value>>,
971 #[serde(flatten)]
972 unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
973 }
974
975 let wire = ConfigWire::deserialize(deserializer)?;
976 if wire.profile.is_some() {
977 return Err(serde::de::Error::custom(
978 "`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`",
979 ));
980 }
981
982 Ok(Self {
983 substitute: wire.substitute,
984 processing: wire.processing,
985 locale_override: wire.locale_override,
986 localize: wire.localize,
987 multilingual: wire.multilingual,
988 sorting: wire.sorting,
989 contributors: wire.contributors,
990 dates: wire.dates,
991 titles: wire.titles,
992 locators: wire.locators,
993 page_range_format: wire.page_range_format,
994 page_range_delimiter: wire.page_range_delimiter,
995 links: wire.links,
996 punctuation_in_quote: wire.punctuation_in_quote,
997 volume_pages_delimiter: wire.volume_pages_delimiter,
998 strip_periods: wire.strip_periods,
999 notes: wire.notes,
1000 integral_name_memory: wire.integral_name_memory,
1001 org_abbreviation_memory: wire.org_abbreviation_memory,
1002 custom: wire.custom,
1003 unknown_fields: wire.unknown_fields,
1004 })
1005 }
1006}
1007
1008#[cfg(test)]
1009#[allow(
1010 clippy::unwrap_used,
1011 clippy::expect_used,
1012 clippy::panic,
1013 clippy::indexing_slicing,
1014 clippy::todo,
1015 clippy::unimplemented,
1016 clippy::unreachable,
1017 clippy::get_unwrap,
1018 reason = "Panicking is acceptable and often desired in tests."
1019)]
1020mod tests {
1021 use super::*;
1022
1023 #[test]
1024 fn test_config_default() {
1025 let config = Config::default();
1026 assert!(config.substitute.is_none());
1027 assert!(config.processing.is_none());
1028 }
1029
1030 #[test]
1031 fn test_author_date_processing() {
1032 let processing = Processing::AuthorDate;
1033 let config = processing.config();
1034 let disambiguate = config.disambiguate.unwrap();
1035 assert!(disambiguate.year_suffix);
1036 assert!(!disambiguate.names);
1037 assert!(!disambiguate.add_givenname);
1038 assert_eq!(
1039 processing.default_bibliography_sort(),
1040 Some(crate::presets::SortPreset::AuthorDateTitle)
1041 );
1042 assert_eq!(
1043 config.sort,
1044 Some(SortEntry::Preset(
1045 crate::presets::SortPreset::AuthorDateTitle
1046 ))
1047 );
1048 }
1049
1050 #[test]
1051 fn test_processing_default_bibliography_sorts() {
1052 assert_eq!(Processing::Numeric.default_bibliography_sort(), None);
1053 assert_eq!(
1054 Processing::Note.default_bibliography_sort(),
1055 Some(crate::presets::SortPreset::AuthorTitleDate)
1056 );
1057 assert_eq!(
1058 Processing::Label(LabelConfig::default()).default_bibliography_sort(),
1059 Some(crate::presets::SortPreset::AuthorDateTitle)
1060 );
1061 }
1062
1063 #[test]
1064 fn test_processing_default_citation_sort_policy_is_explicit_only() {
1065 assert_eq!(
1066 Processing::AuthorDate.default_citation_sort_policy(),
1067 CitationSortPolicy::ExplicitOnly
1068 );
1069 assert_eq!(
1070 Processing::Note.default_citation_sort_policy(),
1071 CitationSortPolicy::ExplicitOnly
1072 );
1073 }
1074
1075 #[test]
1076 fn test_substitute_default() {
1077 let sub = Substitute::default();
1078 assert_eq!(sub.template.len(), 3);
1079 }
1080
1081 #[test]
1082 fn test_config_yaml_roundtrip() {
1083 let yaml = r#"
1084substitute:
1085 contributor-role-form: short
1086 template:
1087 - editor
1088 - title
1089processing: author-date
1090contributors:
1091 display-as-sort: first
1092 and: symbol
1093"#;
1094 let config: Config = serde_yaml::from_str(yaml).unwrap();
1095 assert!(config.substitute.is_some());
1096 assert_eq!(config.processing, Some(Processing::AuthorDate));
1097 assert_eq!(
1098 config.contributors.as_ref().unwrap().and,
1099 Some(AndOptions::Symbol)
1100 );
1101 }
1102
1103 #[test]
1104 fn test_sorting_config_deserializes_and_roundtrips() {
1105 let yaml = r#"
1106sorting:
1107 locale: sv-SE
1108 multilingual: romanized
1109"#;
1110 let config: Config = serde_yaml::from_str(yaml).unwrap();
1111 let sorting = config.sorting.as_ref().expect("sorting should parse");
1112
1113 assert_eq!(
1114 sorting.locale,
1115 Some(SortingLocale::Bcp47("sv-SE".to_string()))
1116 );
1117 assert_eq!(
1118 sorting.multilingual,
1119 Some(SortingMultilingualMode::Romanized)
1120 );
1121
1122 let serialized = serde_yaml::to_string(&config).unwrap();
1123 let reparsed: Config = serde_yaml::from_str(&serialized).unwrap();
1124 assert_eq!(reparsed.sorting, config.sorting);
1125 }
1126
1127 #[test]
1128 fn test_sorting_config_defaults_and_unknown_fields() {
1129 let yaml = r#"
1130sorting:
1131 future-key: true
1132"#;
1133 let config: Config = serde_yaml::from_str(yaml).unwrap();
1134 let sorting = config.sorting.as_ref().expect("sorting should parse");
1135
1136 assert_eq!(sorting.effective_locale(), SortingLocale::Auto);
1137 assert_eq!(
1138 sorting.effective_multilingual(),
1139 SortingMultilingualMode::Uniform
1140 );
1141 assert!(sorting.unknown_fields.contains_key("future-key"));
1142 }
1143
1144 #[test]
1145 fn test_bibliography_sorting_override_merges_partially() {
1146 let base: Config = serde_yaml::from_str(
1147 r#"
1148sorting:
1149 locale: de-DE
1150 multilingual: uniform
1151"#,
1152 )
1153 .unwrap();
1154 let bib: BibliographyOptions = serde_yaml::from_str(
1155 r#"
1156sorting:
1157 multilingual: romanized
1158"#,
1159 )
1160 .unwrap();
1161
1162 let merged = bib.merged_with(&base);
1163 let sorting = merged.sorting.expect("merged sorting should exist");
1164 assert_eq!(
1165 sorting.locale,
1166 Some(SortingLocale::Bcp47("de-DE".to_string()))
1167 );
1168 assert_eq!(
1169 sorting.multilingual,
1170 Some(SortingMultilingualMode::Romanized)
1171 );
1172 }
1173
1174 #[test]
1175 fn test_contributor_config_preset() {
1176 let yaml = r#"contributors: apa"#;
1178 let config: Config = serde_yaml::from_str(yaml).unwrap();
1179 let contributors = config.contributors.unwrap();
1180 assert_eq!(contributors.and, Some(AndOptions::Symbol));
1181 assert_eq!(contributors.display_as_sort, Some(DisplayAsSort::First));
1182 }
1183
1184 #[test]
1185 fn test_role_label_presets_parse_and_resolve_precedence() {
1186 let yaml = r#"
1187contributors:
1188 role:
1189 preset: short-suffix
1190 roles:
1191 editor:
1192 preset: long-suffix
1193"#;
1194 let config: Config = serde_yaml::from_str(yaml).unwrap();
1195 let contributors = config.contributors.unwrap();
1196
1197 assert_eq!(
1198 contributors.effective_role_label_preset(&crate::template::ContributorRole::Editor),
1199 Some(RoleLabelPreset::LongSuffix)
1200 );
1201 assert_eq!(
1202 contributors.effective_role_label_preset(&crate::template::ContributorRole::Translator),
1203 Some(RoleLabelPreset::ShortSuffix)
1204 );
1205
1206 let yaml_scalar = r#"
1208contributors:
1209 role: short-suffix
1210"#;
1211 let config2: Config = serde_yaml::from_str(yaml_scalar).unwrap();
1212 let contributors2 = config2.contributors.unwrap();
1213
1214 assert_eq!(
1215 contributors2
1216 .effective_role_label_preset(&crate::template::ContributorRole::Translator),
1217 Some(RoleLabelPreset::ShortSuffix)
1218 );
1219 }
1220
1221 #[test]
1222 fn test_role_specific_name_order_override_is_available() {
1223 let yaml = r#"
1224contributors:
1225 role:
1226 roles:
1227 translator:
1228 name-order: given-first
1229"#;
1230 let config: Config = serde_yaml::from_str(yaml).unwrap();
1231 let contributors = config.contributors.unwrap();
1232
1233 assert_eq!(
1234 contributors.effective_role_name_order(&crate::template::ContributorRole::Translator),
1235 Some(&crate::template::NameOrder::GivenFirst)
1236 );
1237 }
1238
1239 #[test]
1240 fn test_date_config_preset() {
1241 let yaml = r#"dates: long"#;
1243 let config: Config = serde_yaml::from_str(yaml).unwrap();
1244 let dates = config.dates.unwrap();
1245 assert_eq!(dates.month, MonthFormat::Long);
1246 }
1247
1248 #[test]
1249 fn test_titles_config_preset() {
1250 let yaml = r#"titles: chicago"#;
1252 let config: Config = serde_yaml::from_str(yaml).unwrap();
1253 let titles = config.titles.unwrap();
1254 assert_eq!(titles.component.unwrap().quote, Some(true));
1255 assert_eq!(titles.monograph.unwrap().emph, Some(true));
1256 }
1257
1258 #[test]
1259 fn test_substitute_config_preset() {
1260 let yaml = r#"substitute: standard"#;
1262 let config: Config = serde_yaml::from_str(yaml).unwrap();
1263 assert!(config.substitute.is_some());
1264 let resolved = config.substitute.unwrap().resolve();
1265 assert_eq!(resolved.template.len(), 3);
1266 assert_eq!(resolved.template[0], SubstituteKey::Editor);
1267 }
1268
1269 #[test]
1270 fn test_substitute_config_explicit() {
1271 let yaml = r#"
1273substitute:
1274 template:
1275 - title
1276 - editor
1277"#;
1278 let config: Config = serde_yaml::from_str(yaml).unwrap();
1279 let resolved = config.substitute.unwrap().resolve();
1280 assert_eq!(resolved.template[0], SubstituteKey::Title);
1281 assert_eq!(resolved.template[1], SubstituteKey::Editor);
1282 }
1283
1284 #[test]
1285 fn test_config_merge_precedence() {
1286 let base_yaml = r#"
1288processing: author-date
1289locale-override: en-US-base
1290contributors:
1291 display-as-sort: first
1292 and: symbol
1293"#;
1294 let mut base: Config = serde_yaml::from_str(base_yaml).unwrap();
1295
1296 let override_yaml = r#"
1298contributors:
1299 and: text
1300locale-override: en-US-chicago
1301"#;
1302 let override_config: Config = serde_yaml::from_str(override_yaml).unwrap();
1303
1304 base.merge(&override_config);
1306
1307 assert_eq!(base.processing, Some(Processing::AuthorDate));
1309 assert_eq!(base.locale_override.as_deref(), Some("en-US-chicago"));
1310
1311 assert_eq!(
1313 base.contributors.as_ref().unwrap().and,
1314 Some(AndOptions::Text)
1315 );
1316 }
1317
1318 #[test]
1319 fn test_config_deserializes_locale_override() {
1320 let config: Config = serde_yaml::from_str("locale-override: en-US-chicago").unwrap();
1321 assert_eq!(config.locale_override.as_deref(), Some("en-US-chicago"));
1322 }
1323
1324 #[test]
1325 fn test_config_merged_convenience() {
1326 let base = Config {
1327 processing: Some(Processing::AuthorDate),
1328 ..Default::default()
1329 };
1330 let override_config = Config {
1331 punctuation_in_quote: true,
1332 ..Default::default()
1333 };
1334
1335 let merged = Config::merged(&base, &override_config);
1336
1337 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1339 assert!(merged.punctuation_in_quote);
1340 }
1341
1342 #[test]
1343 fn test_citation_options_merge_overrides_citation_fields_only() {
1344 let base = Config {
1345 processing: Some(Processing::AuthorDate),
1346 ..Default::default()
1347 };
1348
1349 let overrides = CitationOptions {
1350 strip_periods: Some(true),
1351 locators: Some(LocatorConfig::default()),
1352 ..Default::default()
1353 };
1354
1355 let merged = overrides.merged_with(&base);
1356 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1357 assert!(merged.strip_periods.unwrap_or(false));
1358 assert!(merged.locators.is_some());
1359 }
1360
1361 #[test]
1362 fn test_bibliography_options_merge_projects_shared_fields_only() {
1363 let base = Config {
1364 processing: Some(Processing::AuthorDate),
1365 ..Default::default()
1366 };
1367
1368 let overrides = BibliographyOptions {
1369 entry_suffix: Some(".".to_string()),
1370 separator: Some(", ".to_string()),
1371 suppress_period_after_url: true,
1372 ..Default::default()
1373 };
1374
1375 let merged = overrides.merged_with(&base);
1376 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1377 assert!(merged.locators.is_none());
1378 assert!(merged.notes.is_none());
1379 let bibliography = overrides.to_bibliography_config();
1380 assert_eq!(bibliography.entry_suffix.as_deref(), Some("."));
1381 assert_eq!(bibliography.separator.as_deref(), Some(", "));
1382 assert!(bibliography.suppress_period_after_url);
1383 }
1384
1385 #[test]
1386 fn test_bibliography_options_merge_leaves_shared_base_when_only_shared_overrides_exist() {
1387 let base = Config {
1388 processing: Some(Processing::AuthorDate),
1389 ..Default::default()
1390 };
1391
1392 let overrides = BibliographyOptions {
1393 contributors: Some(ContributorConfig::default()),
1394 ..Default::default()
1395 };
1396
1397 let merged = overrides.merged_with(&base);
1398 assert_eq!(merged.processing, Some(Processing::AuthorDate));
1399 assert!(merged.contributors.is_some());
1400 }
1401
1402 #[test]
1403 fn citation_options_captures_unknown_fields_for_forward_compat() {
1404 let yaml = "future-key: true\n";
1405 let opts: CitationOptions = serde_yaml::from_str(yaml).unwrap();
1406 assert!(opts.unknown_fields.contains_key("future-key"));
1407 }
1408
1409 #[test]
1410 fn bibliography_options_captures_unknown_fields_for_forward_compat() {
1411 let yaml = "future-key: true\n";
1412 let opts: BibliographyOptions = serde_yaml::from_str(yaml).unwrap();
1413 assert!(opts.unknown_fields.contains_key("future-key"));
1414 }
1415
1416 #[test]
1417 fn note_config_captures_unknown_fields_for_forward_compat() {
1418 let yaml = "punctuation: inside\nfuture-key: true\n";
1419 let cfg: NoteConfig = serde_yaml::from_str(yaml).unwrap();
1420 assert!(cfg.unknown_fields.contains_key("future-key"));
1421 assert_eq!(cfg.punctuation, Some(NoteQuotePlacement::Inside));
1422 }
1423
1424 #[test]
1425 fn test_multilingual_preset_romanized_translated_parses_and_resolves() {
1426 let yaml = r#"multilingual: romanized-translated"#;
1428 let config: Config = serde_yaml::from_str(yaml).unwrap();
1429 let ml = config.multilingual.unwrap();
1430 assert_eq!(ml.title_mode, Some(MultilingualMode::Combined));
1431 assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
1432 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1433 }
1434
1435 #[test]
1436 fn test_multilingual_preset_romanized_only_parses_and_resolves() {
1437 let yaml = r#"multilingual: romanized-only"#;
1439 let config: Config = serde_yaml::from_str(yaml).unwrap();
1440 let ml = config.multilingual.unwrap();
1441 assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
1442 assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
1443 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1444 }
1445
1446 #[test]
1447 fn test_multilingual_preset_romanized_script_translated_parses_and_resolves() {
1448 use crate::options::multilingual::{MultilingualSegment, MultilingualView, SegmentWrap};
1451 let yaml = r#"multilingual: romanized-script-translated"#;
1452 let config: Config = serde_yaml::from_str(yaml).unwrap();
1453 let ml = config.multilingual.unwrap();
1454 assert_eq!(
1455 ml.title_mode,
1456 Some(MultilingualMode::Pattern(vec![
1457 MultilingualSegment {
1458 view: MultilingualView::Transliterated,
1459 wrap: SegmentWrap::None,
1460 },
1461 MultilingualSegment {
1462 view: MultilingualView::OriginalScript,
1463 wrap: SegmentWrap::None,
1464 },
1465 MultilingualSegment {
1466 view: MultilingualView::Translated,
1467 wrap: SegmentWrap::Brackets,
1468 },
1469 ]))
1470 );
1471 assert_eq!(
1472 ml.name_mode,
1473 Some(MultilingualMode::Pattern(vec![
1474 MultilingualSegment {
1475 view: MultilingualView::Transliterated,
1476 wrap: SegmentWrap::None,
1477 },
1478 MultilingualSegment {
1479 view: MultilingualView::OriginalScript,
1480 wrap: SegmentWrap::None,
1481 },
1482 ]))
1483 );
1484 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1485 assert!(ml.scripts.get("Han").is_some_and(|s| s.use_native_ordering));
1486 assert!(
1487 ml.scripts
1488 .get("Hangul")
1489 .is_some_and(|s| s.use_native_ordering)
1490 );
1491 }
1492
1493 #[test]
1494 fn test_multilingual_explicit_block_transliterated_roundtrips() {
1495 let yaml = r#"
1497multilingual:
1498 title-mode: transliterated
1499 preferred-script: Latn
1500"#;
1501 let config: Config = serde_yaml::from_str(yaml).unwrap();
1502 let ml = config.multilingual.clone().unwrap();
1503 assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
1504 assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
1505
1506 let yaml2 = serde_yaml::to_string(&config).unwrap();
1507 let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
1508 assert_eq!(config2.multilingual, config.multilingual);
1509 }
1510
1511 #[test]
1512 fn test_multilingual_pattern_block_roundtrips() {
1513 let yaml = r#"
1516multilingual:
1517 title-mode:
1518 pattern:
1519 - view: original-script
1520 - view: translated
1521 wrap: brackets
1522"#;
1523 let config: Config = serde_yaml::from_str(yaml).unwrap();
1524 let ml = config.multilingual.clone().unwrap();
1525 assert!(
1526 matches!(ml.title_mode, Some(MultilingualMode::Pattern(_))),
1527 "expected Pattern mode, got {:?}",
1528 ml.title_mode
1529 );
1530
1531 let yaml2 = serde_yaml::to_string(&config).unwrap();
1532 let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
1533 assert_eq!(config2.multilingual, config.multilingual);
1534 }
1535}