use super::contributor_role_to_reference_role;
use crate::reference::Reference;
use crate::render::format::OutputFormat;
use crate::values::{RenderContext, RenderOptions};
use citum_schema::locale::{GrammaticalGender, TermForm};
use citum_schema::options::RoleLabelPreset;
use citum_schema::reference::ContributorGender;
use citum_schema::template::{ContributorForm, ContributorRole, Rendering, TemplateContributor};
fn map_contributor_gender(gender: ContributorGender) -> GrammaticalGender {
match gender {
ContributorGender::Masculine => GrammaticalGender::Masculine,
ContributorGender::Feminine => GrammaticalGender::Feminine,
ContributorGender::Neuter => GrammaticalGender::Neuter,
ContributorGender::Common => GrammaticalGender::Common,
}
}
#[derive(Debug, Clone)]
pub(super) enum RoleGenderRequest {
Specific(GrammaticalGender),
Neutral,
}
#[derive(Debug, Clone, Default)]
pub(super) struct RoleLabelTermOptions {
pub gender: Option<RoleGenderRequest>,
pub text_case: Option<citum_schema::options::titles::TextCase>,
}
pub(super) struct RoleLabelContext<'a, 'options, F: OutputFormat<Output = String>> {
pub(super) component: &'a TemplateContributor,
pub(super) role: &'a ContributorRole,
pub(super) reference: &'a Reference,
pub(super) names_count: usize,
pub(super) effective_rendering: &'a Rendering,
pub(super) options: &'a RenderOptions<'options>,
pub(super) fmt: &'a F,
pub(super) role_omitted: bool,
}
fn requested_role_gender(
component: &TemplateContributor,
role: &ContributorRole,
reference: &Reference,
) -> Option<RoleGenderRequest> {
if let Some(gender) = &component.gender {
return Some(RoleGenderRequest::Specific(gender.clone()));
}
let data_role = contributor_role_to_reference_role(role)?;
let entries = reference.contributor_entries(&data_role);
let mut genders = entries
.iter()
.filter_map(|entry| entry.gender.map(map_contributor_gender));
let first = genders.next()?;
if genders.all(|gender| gender == first) {
Some(RoleGenderRequest::Specific(first))
} else {
Some(RoleGenderRequest::Neutral)
}
}
fn resolve_role_term_by_request(
locale: &citum_schema::locale::Locale,
role: &ContributorRole,
plural: bool,
term_form: TermForm,
requested_gender: Option<RoleGenderRequest>,
) -> Option<String> {
match requested_gender {
Some(RoleGenderRequest::Specific(gender)) => {
locale.resolved_role_term(role, plural, &term_form, Some(gender))
}
Some(RoleGenderRequest::Neutral) => {
locale.resolved_role_term_neutral(role, plural, &term_form)
}
None => locale.resolved_role_term(role, plural, &term_form, None),
}
}
pub(super) fn resolve_role_label_preset<F: OutputFormat<Output = String>>(
role: &ContributorRole,
preset: RoleLabelPreset,
names_count: usize,
term_opts: RoleLabelTermOptions,
effective_rendering: &Rendering,
options: &RenderOptions<'_>,
fmt: &F,
) -> (Option<String>, Option<String>) {
let plural = names_count > 1;
let language = options.locale.locale.as_str();
let RoleLabelTermOptions {
gender: requested_gender,
text_case,
} = term_opts;
match preset {
RoleLabelPreset::None => (None, None),
RoleLabelPreset::VerbPrefix => {
let term = options
.locale
.resolved_role_term(role, plural, &TermForm::Verb, None);
(
term.map(|t| {
super::format_role_term::<F>(&t, fmt, effective_rendering, options, "", " ")
}),
None,
)
}
RoleLabelPreset::VerbShortPrefix => {
let term = options
.locale
.resolved_role_term(role, plural, &TermForm::VerbShort, None);
(
term.map(|t| {
super::format_role_term::<F>(&t, fmt, effective_rendering, options, "", " ")
}),
None,
)
}
RoleLabelPreset::ShortSuffix => {
let term = resolve_role_term_by_request(
options.locale,
role,
plural,
TermForm::Short,
requested_gender,
)
.map(|t| apply_label_case(t, text_case, language));
(
None,
term.map(|t| {
super::format_role_term::<F>(&t, fmt, effective_rendering, options, " (", ")")
}),
)
}
RoleLabelPreset::ShortSuffixComma => {
let term = resolve_role_term_by_request(
options.locale,
role,
plural,
TermForm::Short,
requested_gender,
)
.map(|t| apply_label_case(t, text_case, language));
(
None,
term.map(|t| {
super::format_role_term::<F>(&t, fmt, effective_rendering, options, ", ", "")
}),
)
}
RoleLabelPreset::LongSuffix => {
let term = resolve_role_term_by_request(
options.locale,
role,
plural,
TermForm::Long,
requested_gender,
)
.map(|t| apply_label_case(t, text_case, language));
(
None,
term.map(|t| {
super::format_role_term::<F>(&t, fmt, effective_rendering, options, ", ", "")
}),
)
}
}
}
pub(super) fn apply_label_case(
term: String,
text_case: Option<citum_schema::options::titles::TextCase>,
language: &str,
) -> String {
match text_case {
Some(case) => {
let resolved = crate::values::text_case::resolve_text_case(case, Some(language));
crate::values::text_case::apply_text_case(&term, resolved)
}
None => term,
}
}
pub(crate) const RECOGNIZED_LABEL_TERMS: &[&str] = &["chair", "editor", "translator"];
fn resolve_explicit_label<F: OutputFormat<Output = String>>(
label_config: &citum_schema::template::RoleLabel,
context: &RoleLabelContext<'_, '_, F>,
) -> (Option<String>, Option<String>) {
use citum_schema::template::RoleLabelForm;
let plural = context.names_count > 1;
let term_form = match label_config.form {
RoleLabelForm::Short => TermForm::Short,
RoleLabelForm::Long => TermForm::Long,
};
let role = match label_config.term.as_str() {
"chair" => Some(ContributorRole::Chair),
"editor" => Some(ContributorRole::Editor),
"translator" => Some(ContributorRole::Translator),
_ => Some(context.role.clone()),
};
let requested_gender =
requested_role_gender(context.component, context.role, context.reference);
let term_text = role
.and_then(|r| {
resolve_role_term_by_request(
context.options.locale,
&r,
plural,
term_form,
requested_gender,
)
})
.map(|t| {
apply_label_case(
t,
label_config.text_case,
context.options.locale.locale.as_str(),
)
});
place_explicit_term::<F>(
label_config,
term_text,
context.effective_rendering,
context.options,
context.fmt,
crate::values::effective_item_language(context.reference).as_deref(),
)
}
pub(super) fn realize_label_affix(
affix: Option<&citum_schema::template::DelimiterPunctuation>,
fallback: &str,
options: &RenderOptions<'_>,
item_language: Option<&str>,
position: crate::render::format::PunctuationPosition,
) -> String {
let Some(affix) = affix else {
return fallback.to_string();
};
let (script, realization) = crate::values::punctuation_realization_context(
item_language,
options.config.multilingual.as_ref(),
);
crate::render::format::realize_punctuation(affix, script, realization, position).into_owned()
}
pub(super) fn place_explicit_term<F: OutputFormat<Output = String>>(
label_config: &citum_schema::template::RoleLabel,
term_text: Option<String>,
effective_rendering: &Rendering,
options: &RenderOptions<'_>,
fmt: &F,
item_language: Option<&str>,
) -> (Option<String>, Option<String>) {
use citum_schema::template::LabelPlacement;
let (default_before, default_after) = match (&label_config.placement, &label_config.wrap) {
(LabelPlacement::Suffix, Some(_)) => (" ", ""),
(LabelPlacement::Prefix, _) => ("", " "),
(LabelPlacement::Suffix, None) => (", ", ""),
};
let before = realize_label_affix(
label_config.prefix.as_ref(),
default_before,
options,
item_language,
crate::render::format::PunctuationPosition::Prefix,
);
let after = realize_label_affix(
label_config.suffix.as_ref(),
default_after,
options,
item_language,
crate::render::format::PunctuationPosition::Suffix,
);
match label_config.placement {
LabelPlacement::Prefix => (
term_text.map(|t| {
super::format_wrapped_role_term::<F>(
&t,
fmt,
effective_rendering,
options,
(&before, &after),
label_config.wrap.as_deref(),
item_language,
)
}),
None,
),
LabelPlacement::Suffix => (
None,
term_text.map(|t| {
super::format_wrapped_role_term::<F>(
&t,
fmt,
effective_rendering,
options,
(&before, &after),
label_config.wrap.as_deref(),
item_language,
)
}),
),
}
}
fn configured_structural_label<F: OutputFormat<Output = String>>(
context: &RoleLabelContext<'_, '_, F>,
) -> Option<citum_schema::template::RoleLabel> {
if context.options.context != RenderContext::Bibliography {
return None;
}
let contributors = context.options.config.contributors.as_ref()?;
let build = |presentation: &citum_schema::options::RoleLabelPresentation| {
citum_schema::template::RoleLabel {
term: context.role.as_str().to_string(),
form: presentation.form.clone(),
placement: presentation.placement.clone(),
text_case: presentation.text_case,
wrap: presentation.wrap.clone(),
prefix: presentation.prefix.clone().map(Into::into),
suffix: presentation.suffix.clone().map(Into::into),
}
};
if let Some(presentation) = contributors.role_label_presentation(context.role) {
return Some(build(presentation));
}
contributors
.role
.as_ref()?
.defaults?
.presentation_for(context.role)
.map(|presentation| build(&presentation))
}
pub(super) fn resolve_role_labels<F: OutputFormat<Output = String>>(
context: RoleLabelContext<'_, '_, F>,
) -> (Option<String>, Option<String>) {
if let Some(label_config) = &context.component.label {
return resolve_explicit_label(label_config, &context);
}
let verb_form = matches!(
context.component.form,
ContributorForm::Verb | ContributorForm::VerbShort
);
if (!context.role_omitted || verb_form)
&& let Some(label) = configured_structural_label(&context)
{
return resolve_explicit_label(&label, &context);
}
let RoleLabelContext {
component,
role,
reference,
names_count,
effective_rendering,
options,
fmt,
role_omitted,
} = context;
if role_omitted
&& !matches!(
component.form,
ContributorForm::Verb | ContributorForm::VerbShort
)
{
return (None, None);
}
if let Some(preset) = options
.config
.contributors
.as_ref()
.and_then(|contributors| contributors.effective_role_label_preset(role))
{
let requested_gender = requested_role_gender(component, role, reference);
return resolve_role_label_preset(
role,
preset,
names_count,
RoleLabelTermOptions {
gender: requested_gender,
text_case: None,
},
effective_rendering,
options,
fmt,
);
}
if options.context == RenderContext::Bibliography
&& !matches!(
component.form,
ContributorForm::Verb | ContributorForm::VerbShort
)
&& let Some(preset) = options
.config
.contributors
.as_ref()
.and_then(|contributors| contributors.default_role_label_preset(role))
{
let requested_gender = requested_role_gender(component, role, reference);
return resolve_role_label_preset(
role,
preset,
names_count,
RoleLabelTermOptions {
gender: requested_gender,
text_case: None,
},
effective_rendering,
options,
fmt,
);
}
match &component.form {
ContributorForm::Verb | ContributorForm::VerbShort => {
let plural = names_count > 1;
let term_form = match component.form {
ContributorForm::VerbShort => TermForm::VerbShort,
_ => TermForm::Verb,
};
let term = options
.locale
.resolved_role_term(role, plural, &term_form, None);
(
term.map(|t| {
super::format_role_term::<F>(&t, fmt, effective_rendering, options, "", " ")
}),
None,
)
}
_ => (None, None),
}
}
#[cfg(test)]
mod tests {
use super::apply_label_case;
use citum_schema::options::titles::TextCase;
#[test]
fn capitalize_first_uppercases_label_initial() {
let out = apply_label_case("eds.".to_string(), Some(TextCase::CapitalizeFirst), "en-US");
assert_eq!(out, "Eds.");
}
#[test]
fn no_text_case_leaves_term_unchanged() {
let out = apply_label_case("eds.".to_string(), None, "en-US");
assert_eq!(out, "eds.");
}
}