pub mod bibliography;
pub mod contributors;
pub mod dates;
pub mod integral_name_memory;
pub mod localization;
pub mod locators;
pub mod multilingual;
pub mod processing;
pub mod scoped;
pub mod substitute;
pub use crate::presets::{MultilingualConfigEntry, MultilingualPreset};
pub use bibliography::{
ArticleJournalBibliographyConfig, ArticleJournalNoPageFallback, BibliographyConfig,
BibliographyPartitionHeading, BibliographyPartitionKind, BibliographyPartitionMode,
BibliographySortPartitioning, SubsequentAuthorSubstituteRule,
};
pub use contributors::{
AndOptions, AndOtherOptions, ContributorConfig, ContributorConfigEntry, DelimiterPrecedesLast,
DemoteNonDroppingParticle, DisplayAsSort, NameForm, RoleLabelPreset, RoleOptions,
RoleOptionsEntry, RoleRendering, ShortenListOptions,
};
pub use dates::{DateConfig, DateConfigEntry};
pub use integral_name_memory::{
IntegralNameContexts, IntegralNameMemoryConfig, IntegralNameScope, OrgAbbreviationMemoryConfig,
ResolvedIntegralNameMemoryConfig, ResolvedOrgAbbreviationMemoryConfig, ShortNameDisplay,
SubsequentNameForm,
};
pub use localization::{Localize, MonthFormat, Scope};
pub use locators::{
LabelForm, LabelRepeat, LocatorConfig, LocatorConfigEntry, LocatorKindConfig, LocatorPattern,
LocatorPreset, TypeClass,
};
pub use multilingual::{
MultilingualConfig, MultilingualMode, MultilingualSegment, MultilingualView, ScriptConfig,
SegmentWrap,
};
pub use processing::{
CitationSortPolicy, Disambiguation, GivennameRule, Group, LabelConfig, LabelParams,
LabelPreset, Processing, ProcessingCustom, Sort, SortEntry, SortKey, SortSpec,
};
pub use scoped::{
BibliographyLabelMode, BibliographyLabelWrap, CitationGroupDelimiter, DatePosition, LabelWrap,
RepeatedAuthorRendering, TitleTerminator,
};
pub use substitute::{Substitute, SubstituteConfig, SubstituteKey};
use crate::template::DelimiterPunctuation;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Default, PartialEq, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct Config {
#[serde(skip_serializing_if = "Option::is_none")]
pub substitute: Option<SubstituteConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub processing: Option<Processing>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale_override: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub localize: Option<Localize>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_multilingual_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
pub multilingual: Option<MultilingualConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_contributor_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
pub contributors: Option<ContributorConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_date_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
pub dates: Option<DateConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_titles_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
pub titles: Option<crate::options::titles::TitlesConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_locator_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
pub locators: Option<LocatorConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_range_format: Option<PageRangeFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<LinksConfig>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub punctuation_in_quote: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume_pages_delimiter: Option<DelimiterPunctuation>,
#[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
pub strip_periods: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<NoteConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integral_name_memory: Option<IntegralNameMemoryConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom: Option<HashMap<String, serde_json::Value>>,
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct CitationOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub substitute: Option<SubstituteConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub processing: Option<Processing>,
#[serde(skip_serializing_if = "Option::is_none")]
pub localize: Option<Localize>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_multilingual_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
pub multilingual: Option<MultilingualConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_contributor_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
pub contributors: Option<ContributorConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_date_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
pub dates: Option<DateConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_titles_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
pub titles: Option<crate::options::titles::TitlesConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_locator_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
pub locators: Option<LocatorConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_range_format: Option<PageRangeFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<LinksConfig>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub punctuation_in_quote: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume_pages_delimiter: Option<DelimiterPunctuation>,
#[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
pub strip_periods: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<NoteConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integral_name_memory: Option<IntegralNameMemoryConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label_wrap: Option<LabelWrap>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_delimiter: Option<CitationGroupDelimiter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom: Option<HashMap<String, serde_json::Value>>,
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct BibliographyOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub substitute: Option<SubstituteConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub processing: Option<Processing>,
#[serde(skip_serializing_if = "Option::is_none")]
pub localize: Option<Localize>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_multilingual_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<MultilingualConfigEntry>"))]
pub multilingual: Option<MultilingualConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_contributor_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
pub contributors: Option<ContributorConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_date_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
pub dates: Option<DateConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_titles_config",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
pub titles: Option<crate::options::titles::TitlesConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_range_format: Option<PageRangeFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub article_journal: Option<ArticleJournalBibliographyConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subsequent_author_substitute: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subsequent_author_substitute_rule: Option<SubsequentAuthorSubstituteRule>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hanging_indent: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entry_suffix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub separator: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub suppress_period_after_url: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub compound_numeric: Option<bibliography::CompoundNumericConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_partitioning: Option<bibliography::BibliographySortPartitioning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<LinksConfig>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub punctuation_in_quote: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume_pages_delimiter: Option<DelimiterPunctuation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label_mode: Option<BibliographyLabelMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label_wrap: Option<BibliographyLabelWrap>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_position: Option<DatePosition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title_terminator: Option<TitleTerminator>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repeated_author_rendering: Option<RepeatedAuthorRendering>,
#[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
pub strip_periods: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom: Option<HashMap<String, serde_json::Value>>,
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct NoteConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub punctuation: Option<NoteQuotePlacement>,
#[serde(skip_serializing_if = "Option::is_none")]
pub number: Option<NoteNumberPlacement>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<NoteMarkerOrder>,
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum NoteQuotePlacement {
Inside,
Outside,
Adaptive,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum NoteNumberPlacement {
Inside,
Outside,
Same,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum NoteMarkerOrder {
Before,
After,
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum PageRangeFormat {
#[default]
Expanded,
Minimal,
MinimalTwo,
Chicago,
Chicago16,
}
pub mod titles;
pub use titles::{TextCase, TitleRendering, TitlesConfig, TitlesConfigEntry};
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct LinksConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub doi: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<LinkTarget>,
#[serde(skip_serializing_if = "Option::is_none")]
pub anchor: Option<LinkAnchor>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum LinkTarget {
Url,
Doi,
UrlOrDoi,
Pubmed,
Pmcid,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum LinkAnchor {
Title,
Url,
Doi,
Component,
Entry,
}
impl Config {
pub fn merge(&mut self, other: &Config) {
crate::merge_options!(
self,
other,
processing,
locale_override,
localize,
multilingual,
dates,
titles,
locators,
page_range_format,
links,
volume_pages_delimiter,
locale_override,
strip_periods,
notes,
integral_name_memory,
org_abbreviation_memory,
custom,
);
if let Some(other_substitute) = &other.substitute {
if let Some(this_substitute) = &self.substitute {
self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
} else {
self.substitute = Some(other_substitute.clone());
}
}
if let Some(other_contributors) = &other.contributors {
if let Some(this_contributors) = &mut self.contributors {
this_contributors.merge(other_contributors);
} else {
self.contributors = Some(other_contributors.clone());
}
}
if other.punctuation_in_quote {
self.punctuation_in_quote = true;
}
}
pub fn merged(base: &Config, override_config: &Config) -> Config {
let mut result = base.clone();
result.merge(override_config);
result
}
}
impl CitationOptions {
#[must_use]
pub fn to_config(&self) -> Config {
Config {
substitute: self.substitute.clone(),
processing: self.processing.clone(),
locale_override: None,
localize: self.localize.clone(),
multilingual: self.multilingual.clone(),
contributors: self.contributors.clone(),
dates: self.dates.clone(),
titles: self.titles.clone(),
locators: self.locators.clone(),
page_range_format: self.page_range_format.clone(),
links: self.links.clone(),
punctuation_in_quote: self.punctuation_in_quote,
volume_pages_delimiter: self.volume_pages_delimiter.clone(),
strip_periods: self.strip_periods,
notes: self.notes.clone(),
integral_name_memory: self.integral_name_memory.clone(),
org_abbreviation_memory: self.org_abbreviation_memory.clone(),
custom: self.custom.clone(),
unknown_fields: std::collections::BTreeMap::new(),
}
}
#[must_use]
pub fn merged_with(&self, base: &Config) -> Config {
Config::merged(base, &self.to_config())
}
pub fn merge(&mut self, other: &CitationOptions) {
crate::merge_options!(
self,
other,
processing,
localize,
multilingual,
dates,
titles,
locators,
page_range_format,
links,
volume_pages_delimiter,
strip_periods,
notes,
integral_name_memory,
org_abbreviation_memory,
label_wrap,
group_delimiter,
custom,
);
if let Some(other_substitute) = &other.substitute {
if let Some(this_substitute) = &self.substitute {
self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
} else {
self.substitute = Some(other_substitute.clone());
}
}
if let Some(other_contributors) = &other.contributors {
if let Some(this_contributors) = &mut self.contributors {
this_contributors.merge(other_contributors);
} else {
self.contributors = Some(other_contributors.clone());
}
}
if other.punctuation_in_quote {
self.punctuation_in_quote = true;
}
}
}
impl BibliographyOptions {
#[must_use]
pub fn to_bibliography_config(&self) -> BibliographyConfig {
BibliographyConfig {
article_journal: self.article_journal.clone(),
subsequent_author_substitute: self.subsequent_author_substitute.clone(),
subsequent_author_substitute_rule: self.subsequent_author_substitute_rule.clone(),
hanging_indent: self.hanging_indent,
entry_suffix: self.entry_suffix.clone(),
separator: self.separator.clone(),
suppress_period_after_url: self.suppress_period_after_url,
custom: None,
compound_numeric: self.compound_numeric.clone(),
sort_partitioning: self.sort_partitioning.clone(),
unknown_fields: std::collections::BTreeMap::new(),
}
}
#[must_use]
pub fn to_config(&self) -> Config {
Config {
substitute: self.substitute.clone(),
processing: self.processing.clone(),
locale_override: None,
localize: self.localize.clone(),
multilingual: self.multilingual.clone(),
contributors: self.contributors.clone(),
dates: self.dates.clone(),
titles: self.titles.clone(),
locators: None,
page_range_format: self.page_range_format.clone(),
links: self.links.clone(),
punctuation_in_quote: self.punctuation_in_quote,
volume_pages_delimiter: self.volume_pages_delimiter.clone(),
strip_periods: self.strip_periods,
notes: None,
integral_name_memory: None,
org_abbreviation_memory: None,
custom: self.custom.clone(),
unknown_fields: std::collections::BTreeMap::new(),
}
}
#[must_use]
pub fn merged_with(&self, base: &Config) -> Config {
Config::merged(base, &self.to_config())
}
pub fn merge(&mut self, other: &BibliographyOptions) {
crate::merge_options!(
self,
other,
processing,
localize,
multilingual,
dates,
titles,
page_range_format,
links,
volume_pages_delimiter,
strip_periods,
article_journal,
subsequent_author_substitute,
subsequent_author_substitute_rule,
hanging_indent,
entry_suffix,
separator,
compound_numeric,
sort_partitioning,
label_mode,
label_wrap,
date_position,
title_terminator,
repeated_author_rendering,
custom,
);
self.merge_shared_fields(other);
}
fn merge_shared_fields(&mut self, other: &BibliographyOptions) {
if let Some(other_substitute) = &other.substitute {
if let Some(this_substitute) = &self.substitute {
self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
} else {
self.substitute = Some(other_substitute.clone());
}
}
if let Some(other_contributors) = &other.contributors {
if let Some(this_contributors) = &mut self.contributors {
this_contributors.merge(other_contributors);
} else {
self.contributors = Some(other_contributors.clone());
}
}
if other.punctuation_in_quote {
self.punctuation_in_quote = true;
}
if other.suppress_period_after_url {
self.suppress_period_after_url = true;
}
for (key, value) in &other.unknown_fields {
self.unknown_fields.insert(key.clone(), value.clone());
}
}
}
fn deserialize_contributor_config<'de, D>(
deserializer: D,
) -> Result<Option<ContributorConfig>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: Option<ContributorConfigEntry> = Option::deserialize(deserializer)?;
Ok(value.map(|entry| entry.resolve()))
}
fn deserialize_date_config<'de, D>(deserializer: D) -> Result<Option<DateConfig>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: Option<DateConfigEntry> = Option::deserialize(deserializer)?;
Ok(value.map(|entry| entry.resolve()))
}
fn deserialize_titles_config<'de, D>(
deserializer: D,
) -> Result<Option<crate::options::titles::TitlesConfig>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: Option<crate::options::titles::TitlesConfigEntry> =
Option::deserialize(deserializer)?;
Ok(value.map(|entry| entry.resolve()))
}
fn deserialize_locator_config<'de, D>(deserializer: D) -> Result<Option<LocatorConfig>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: Option<LocatorConfigEntry> = Option::deserialize(deserializer)?;
Ok(value.map(|entry| entry.resolve()))
}
fn deserialize_multilingual_config<'de, D>(
deserializer: D,
) -> Result<Option<MultilingualConfig>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: Option<crate::presets::MultilingualConfigEntry> = Option::deserialize(deserializer)?;
Ok(value.map(|entry| entry.resolve()))
}
impl<'de> Deserialize<'de> for Config {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
struct ConfigWire {
#[serde(skip_serializing_if = "Option::is_none")]
substitute: Option<SubstituteConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
processing: Option<Processing>,
#[serde(skip_serializing_if = "Option::is_none")]
locale_override: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
localize: Option<Localize>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_multilingual_config",
default
)]
multilingual: Option<MultilingualConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_contributor_config",
default
)]
contributors: Option<ContributorConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_date_config",
default
)]
dates: Option<DateConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_titles_config",
default
)]
titles: Option<crate::options::titles::TitlesConfig>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_locator_config",
default
)]
locators: Option<LocatorConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
page_range_format: Option<PageRangeFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
links: Option<LinksConfig>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
punctuation_in_quote: bool,
#[serde(skip_serializing_if = "Option::is_none")]
volume_pages_delimiter: Option<DelimiterPunctuation>,
#[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
strip_periods: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
notes: Option<NoteConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
integral_name_memory: Option<IntegralNameMemoryConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
#[serde(default)]
profile: Option<serde_yaml::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
custom: Option<HashMap<String, serde_json::Value>>,
#[serde(flatten)]
unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
}
let wire = ConfigWire::deserialize(deserializer)?;
if wire.profile.is_some() {
return Err(serde::de::Error::custom(
"`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`",
));
}
Ok(Self {
substitute: wire.substitute,
processing: wire.processing,
locale_override: wire.locale_override,
localize: wire.localize,
multilingual: wire.multilingual,
contributors: wire.contributors,
dates: wire.dates,
titles: wire.titles,
locators: wire.locators,
page_range_format: wire.page_range_format,
links: wire.links,
punctuation_in_quote: wire.punctuation_in_quote,
volume_pages_delimiter: wire.volume_pages_delimiter,
strip_periods: wire.strip_periods,
notes: wire.notes,
integral_name_memory: wire.integral_name_memory,
org_abbreviation_memory: wire.org_abbreviation_memory,
custom: wire.custom,
unknown_fields: wire.unknown_fields,
})
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let config = Config::default();
assert!(config.substitute.is_none());
assert!(config.processing.is_none());
}
#[test]
fn test_author_date_processing() {
let processing = Processing::AuthorDate;
let config = processing.config();
assert!(config.disambiguate.unwrap().year_suffix);
assert_eq!(
processing.default_bibliography_sort(),
Some(crate::presets::SortPreset::AuthorDateTitle)
);
assert_eq!(
config.sort,
Some(SortEntry::Preset(
crate::presets::SortPreset::AuthorDateTitle
))
);
}
#[test]
fn test_processing_default_bibliography_sorts() {
assert_eq!(Processing::Numeric.default_bibliography_sort(), None);
assert_eq!(
Processing::Note.default_bibliography_sort(),
Some(crate::presets::SortPreset::AuthorTitleDate)
);
assert_eq!(
Processing::Label(LabelConfig::default()).default_bibliography_sort(),
Some(crate::presets::SortPreset::AuthorDateTitle)
);
}
#[test]
fn test_processing_default_citation_sort_policy_is_explicit_only() {
assert_eq!(
Processing::AuthorDate.default_citation_sort_policy(),
CitationSortPolicy::ExplicitOnly
);
assert_eq!(
Processing::Note.default_citation_sort_policy(),
CitationSortPolicy::ExplicitOnly
);
}
#[test]
fn test_substitute_default() {
let sub = Substitute::default();
assert_eq!(sub.template.len(), 3);
}
#[test]
fn test_config_yaml_roundtrip() {
let yaml = r#"
substitute:
contributor-role-form: short
template:
- editor
- title
processing: author-date
contributors:
display-as-sort: first
and: symbol
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert!(config.substitute.is_some());
assert_eq!(config.processing, Some(Processing::AuthorDate));
assert_eq!(
config.contributors.as_ref().unwrap().and,
Some(AndOptions::Symbol)
);
}
#[test]
fn test_contributor_config_preset() {
let yaml = r#"contributors: apa"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let contributors = config.contributors.unwrap();
assert_eq!(contributors.and, Some(AndOptions::Symbol));
assert_eq!(contributors.display_as_sort, Some(DisplayAsSort::First));
}
#[test]
fn test_role_label_presets_parse_and_resolve_precedence() {
let yaml = r#"
contributors:
role:
preset: short-suffix
roles:
editor:
preset: long-suffix
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let contributors = config.contributors.unwrap();
assert_eq!(
contributors.effective_role_label_preset(&crate::template::ContributorRole::Editor),
Some(RoleLabelPreset::LongSuffix)
);
assert_eq!(
contributors.effective_role_label_preset(&crate::template::ContributorRole::Translator),
Some(RoleLabelPreset::ShortSuffix)
);
let yaml_scalar = r#"
contributors:
role: short-suffix
"#;
let config2: Config = serde_yaml::from_str(yaml_scalar).unwrap();
let contributors2 = config2.contributors.unwrap();
assert_eq!(
contributors2
.effective_role_label_preset(&crate::template::ContributorRole::Translator),
Some(RoleLabelPreset::ShortSuffix)
);
}
#[test]
fn test_role_specific_name_order_override_is_available() {
let yaml = r#"
contributors:
role:
roles:
translator:
name-order: given-first
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let contributors = config.contributors.unwrap();
assert_eq!(
contributors.effective_role_name_order(&crate::template::ContributorRole::Translator),
Some(&crate::template::NameOrder::GivenFirst)
);
}
#[test]
fn test_date_config_preset() {
let yaml = r#"dates: long"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let dates = config.dates.unwrap();
assert_eq!(dates.month, MonthFormat::Long);
}
#[test]
fn test_titles_config_preset() {
let yaml = r#"titles: chicago"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let titles = config.titles.unwrap();
assert_eq!(titles.component.unwrap().quote, Some(true));
assert_eq!(titles.monograph.unwrap().emph, Some(true));
}
#[test]
fn test_substitute_config_preset() {
let yaml = r#"substitute: standard"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert!(config.substitute.is_some());
let resolved = config.substitute.unwrap().resolve();
assert_eq!(resolved.template.len(), 3);
assert_eq!(resolved.template[0], SubstituteKey::Editor);
}
#[test]
fn test_substitute_config_explicit() {
let yaml = r#"
substitute:
template:
- title
- editor
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let resolved = config.substitute.unwrap().resolve();
assert_eq!(resolved.template[0], SubstituteKey::Title);
assert_eq!(resolved.template[1], SubstituteKey::Editor);
}
#[test]
fn test_config_merge_precedence() {
let base_yaml = r#"
processing: author-date
locale-override: en-US-base
contributors:
display-as-sort: first
and: symbol
"#;
let mut base: Config = serde_yaml::from_str(base_yaml).unwrap();
let override_yaml = r#"
contributors:
and: text
locale-override: en-US-chicago
"#;
let override_config: Config = serde_yaml::from_str(override_yaml).unwrap();
base.merge(&override_config);
assert_eq!(base.processing, Some(Processing::AuthorDate));
assert_eq!(base.locale_override.as_deref(), Some("en-US-chicago"));
assert_eq!(
base.contributors.as_ref().unwrap().and,
Some(AndOptions::Text)
);
}
#[test]
fn test_config_deserializes_locale_override() {
let config: Config = serde_yaml::from_str("locale-override: en-US-chicago").unwrap();
assert_eq!(config.locale_override.as_deref(), Some("en-US-chicago"));
}
#[test]
fn test_config_merged_convenience() {
let base = Config {
processing: Some(Processing::AuthorDate),
..Default::default()
};
let override_config = Config {
punctuation_in_quote: true,
..Default::default()
};
let merged = Config::merged(&base, &override_config);
assert_eq!(merged.processing, Some(Processing::AuthorDate));
assert!(merged.punctuation_in_quote);
}
#[test]
fn test_citation_options_merge_overrides_citation_fields_only() {
let base = Config {
processing: Some(Processing::AuthorDate),
..Default::default()
};
let overrides = CitationOptions {
strip_periods: Some(true),
locators: Some(LocatorConfig::default()),
..Default::default()
};
let merged = overrides.merged_with(&base);
assert_eq!(merged.processing, Some(Processing::AuthorDate));
assert!(merged.strip_periods.unwrap_or(false));
assert!(merged.locators.is_some());
}
#[test]
fn test_bibliography_options_merge_projects_shared_fields_only() {
let base = Config {
processing: Some(Processing::AuthorDate),
..Default::default()
};
let overrides = BibliographyOptions {
entry_suffix: Some(".".to_string()),
separator: Some(", ".to_string()),
suppress_period_after_url: true,
..Default::default()
};
let merged = overrides.merged_with(&base);
assert_eq!(merged.processing, Some(Processing::AuthorDate));
assert!(merged.locators.is_none());
assert!(merged.notes.is_none());
let bibliography = overrides.to_bibliography_config();
assert_eq!(bibliography.entry_suffix.as_deref(), Some("."));
assert_eq!(bibliography.separator.as_deref(), Some(", "));
assert!(bibliography.suppress_period_after_url);
}
#[test]
fn test_bibliography_options_merge_leaves_shared_base_when_only_shared_overrides_exist() {
let base = Config {
processing: Some(Processing::AuthorDate),
..Default::default()
};
let overrides = BibliographyOptions {
contributors: Some(ContributorConfig::default()),
..Default::default()
};
let merged = overrides.merged_with(&base);
assert_eq!(merged.processing, Some(Processing::AuthorDate));
assert!(merged.contributors.is_some());
}
#[test]
fn citation_options_captures_unknown_fields_for_forward_compat() {
let yaml = "future-key: true\n";
let opts: CitationOptions = serde_yaml::from_str(yaml).unwrap();
assert!(opts.unknown_fields.contains_key("future-key"));
}
#[test]
fn bibliography_options_captures_unknown_fields_for_forward_compat() {
let yaml = "future-key: true\n";
let opts: BibliographyOptions = serde_yaml::from_str(yaml).unwrap();
assert!(opts.unknown_fields.contains_key("future-key"));
}
#[test]
fn note_config_captures_unknown_fields_for_forward_compat() {
let yaml = "punctuation: inside\nfuture-key: true\n";
let cfg: NoteConfig = serde_yaml::from_str(yaml).unwrap();
assert!(cfg.unknown_fields.contains_key("future-key"));
assert_eq!(cfg.punctuation, Some(NoteQuotePlacement::Inside));
}
#[test]
fn test_multilingual_preset_romanized_translated_parses_and_resolves() {
let yaml = r#"multilingual: romanized-translated"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let ml = config.multilingual.unwrap();
assert_eq!(ml.title_mode, Some(MultilingualMode::Combined));
assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
}
#[test]
fn test_multilingual_preset_romanized_only_parses_and_resolves() {
let yaml = r#"multilingual: romanized-only"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let ml = config.multilingual.unwrap();
assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
assert_eq!(ml.name_mode, Some(MultilingualMode::Transliterated));
assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
}
#[test]
fn test_multilingual_explicit_block_transliterated_roundtrips() {
let yaml = r#"
multilingual:
title-mode: transliterated
preferred-script: Latn
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let ml = config.multilingual.clone().unwrap();
assert_eq!(ml.title_mode, Some(MultilingualMode::Transliterated));
assert_eq!(ml.preferred_script.as_deref(), Some("Latn"));
let yaml2 = serde_yaml::to_string(&config).unwrap();
let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
assert_eq!(config2.multilingual, config.multilingual);
}
#[test]
fn test_multilingual_pattern_block_roundtrips() {
let yaml = r#"
multilingual:
title-mode:
pattern:
- view: original
- view: translated
wrap: brackets
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let ml = config.multilingual.clone().unwrap();
assert!(
matches!(ml.title_mode, Some(MultilingualMode::Pattern(_))),
"expected Pattern mode, got {:?}",
ml.title_mode
);
let yaml2 = serde_yaml::to_string(&config).unwrap();
let config2: Config = serde_yaml::from_str(&yaml2).unwrap();
assert_eq!(config2.multilingual, config.multilingual);
}
}