use crate::values::{ProcHints, RenderOptions};
use citum_schema::options::contributors::NameForm;
use citum_schema::options::{
AndOptions, AndOtherOptions, DemoteNonDroppingParticle, DisplayAsSort, ShortenListOptions,
};
use citum_schema::template::{ContributorForm, NameOrder};
pub(crate) struct NameFormatContext<'a> {
pub(crate) display_as_sort: Option<DisplayAsSort>,
pub(crate) name_order: Option<&'a NameOrder>,
pub(crate) initialize_with: Option<&'a String>,
pub(crate) initialize_with_hyphen: Option<bool>,
pub(crate) name_form: Option<NameForm>,
pub(crate) demote_ndp: Option<&'a DemoteNonDroppingParticle>,
pub(crate) sort_separator: Option<&'a String>,
pub(crate) integral_name_state: Option<citum_schema::citation::IntegralNameState>,
pub(crate) use_integral_short_name: bool,
pub(crate) short_name_display: Option<citum_schema::options::ShortNameDisplay>,
}
pub struct NamesOverrides<'a> {
pub name_order: Option<&'a NameOrder>,
pub sort_separator: Option<&'a String>,
pub shorten: Option<&'a ShortenListOptions>,
pub and: Option<&'a AndOptions>,
pub initialize_with: Option<&'a String>,
pub name_form: Option<NameForm>,
}
fn partition_et_al<'a>(
names: &'a [crate::reference::FlatName],
shorten: Option<&'a ShortenListOptions>,
hints: &'a ProcHints,
) -> (
Vec<&'a crate::reference::FlatName>,
bool,
Vec<&'a crate::reference::FlatName>,
) {
if let Some(opts) = shorten {
let is_subsequent = matches!(
hints.position,
Some(
citum_schema::citation::Position::Subsequent
| citum_schema::citation::Position::Ibid
| citum_schema::citation::Position::IbidWithLocator
)
);
let effective_min_threshold = if is_subsequent {
opts.subsequent_min.unwrap_or(opts.min) as usize
} else {
opts.min as usize
};
let effective_use_first = if is_subsequent {
opts.subsequent_use_first.unwrap_or(opts.use_first) as usize
} else {
opts.use_first as usize
};
let effective_min = if let Some(expanded) = hints.min_names_to_show {
expanded.max(effective_use_first)
} else {
effective_use_first
};
if names.len() >= effective_min_threshold {
if effective_min >= names.len() {
(names.iter().collect::<Vec<_>>(), false, Vec::new())
} else {
let first: Vec<&crate::reference::FlatName> =
names.iter().take(effective_min).collect();
let last: Vec<&crate::reference::FlatName> = if let Some(ul) = opts.use_last {
let take_last = ul as usize;
let skip = std::cmp::max(effective_min, names.len().saturating_sub(take_last));
names.iter().skip(skip).collect()
} else {
Vec::new()
};
(first, true, last)
}
} else {
(names.iter().collect::<Vec<_>>(), false, Vec::new())
}
} else {
(names.iter().collect::<Vec<_>>(), false, Vec::new())
}
}
fn join_names_with_conjunction(
formatted_first: &[String],
and_str: Option<&str>,
delimiter: &str,
delimiter_precedes_last: Option<&citum_schema::options::DelimiterPrecedesLast>,
first_names_len: usize,
ctx: &NameFormatContext,
context: crate::values::RenderContext,
) -> String {
use citum_schema::options::{DelimiterPrecedesLast, DisplayAsSort};
match and_str {
None => {
formatted_first.join(delimiter)
}
Some(conjunction) if formatted_first.len() == 2 => {
let use_delimiter = if context == crate::values::RenderContext::Bibliography {
if matches!(ctx.name_order, Some(NameOrder::GivenFirst)) {
false
} else {
match delimiter_precedes_last {
Some(DelimiterPrecedesLast::Always) => true,
Some(DelimiterPrecedesLast::Never) => false,
Some(DelimiterPrecedesLast::Contextual) | None => true, Some(DelimiterPrecedesLast::AfterInvertedName) => {
ctx.display_as_sort.as_ref().is_some_and(|das| {
matches!(das, DisplayAsSort::All | DisplayAsSort::First)
})
}
}
}
} else {
false
};
#[allow(clippy::indexing_slicing, reason = "length checked")]
if use_delimiter {
format!(
"{}{}{} {}",
formatted_first[0], delimiter, conjunction, formatted_first[1]
)
} else {
format!(
"{} {} {}",
formatted_first[0], conjunction, formatted_first[1]
)
}
}
Some(conjunction) => {
if let Some((last, rest)) = formatted_first.split_last() {
let use_delimiter = match delimiter_precedes_last {
Some(DelimiterPrecedesLast::Always) => true,
Some(DelimiterPrecedesLast::Never) => false,
Some(DelimiterPrecedesLast::Contextual) | None => true, Some(DelimiterPrecedesLast::AfterInvertedName) => {
ctx.display_as_sort.as_ref().is_some_and(|das| {
matches!(das, DisplayAsSort::All)
|| (matches!(das, DisplayAsSort::First) && first_names_len == 1)
})
}
};
if use_delimiter {
format!(
"{}{}{} {}",
rest.join(delimiter),
delimiter,
conjunction,
last
)
} else {
format!("{} {} {}", rest.join(delimiter), conjunction, last)
}
} else {
String::new()
}
}
}
}
struct EtAlContext<'a> {
and_others: AndOtherOptions,
delimiter: &'a str,
delimiter_precedes: Option<&'a citum_schema::options::DelimiterPrecedesLast>,
first_count: usize,
}
fn apply_et_al(
result: String,
formatted_last: &[String],
et_al: EtAlContext<'_>,
ctx: &NameFormatContext,
locale: &citum_schema::locale::Locale,
) -> String {
use citum_schema::options::DelimiterPrecedesLast;
if !formatted_last.is_empty() {
return format!("{} … {}", result, formatted_last.join(et_al.delimiter));
}
let use_delimiter = match et_al.delimiter_precedes {
Some(DelimiterPrecedesLast::Always) => true,
Some(DelimiterPrecedesLast::Never) => false,
Some(DelimiterPrecedesLast::AfterInvertedName) => {
ctx.display_as_sort.as_ref().is_some_and(|das| {
matches!(das, DisplayAsSort::All)
|| (matches!(das, DisplayAsSort::First) && et_al.first_count == 1)
})
}
Some(DelimiterPrecedesLast::Contextual) | None => {
et_al.first_count > 1
}
};
let and_others_term = match et_al.and_others {
AndOtherOptions::EtAl => locale.et_al(),
AndOtherOptions::Text => locale.et_al().trim_end_matches('.'),
};
if use_delimiter {
format!("{result}, {and_others_term}")
} else {
format!("{result} {and_others_term}")
}
}
#[must_use]
pub fn format_names(
names: &[crate::reference::FlatName],
form: &ContributorForm,
options: &RenderOptions<'_>,
overrides: &NamesOverrides<'_>,
hints: &ProcHints,
) -> String {
if names.is_empty() {
return String::new();
}
let config = options.config.contributors.as_ref();
let locale = options.locale;
let shorten = overrides
.shorten
.or_else(|| config.and_then(|c| c.shorten.as_ref()));
let and_others = shorten.map_or(AndOtherOptions::EtAl, |opts| opts.and_others);
let (first_names, use_et_al, last_names) = partition_et_al(names, shorten, hints);
let ctx = NameFormatContext {
display_as_sort: config.and_then(|c| c.display_as_sort),
name_order: overrides.name_order,
initialize_with: overrides
.initialize_with
.or_else(|| config.and_then(|c| c.initialize_with.as_ref())),
initialize_with_hyphen: config.and_then(|c| c.initialize_with_hyphen),
name_form: overrides
.name_form
.or_else(|| config.and_then(|c| c.name_form)),
demote_ndp: config.and_then(|c| c.demote_non_dropping_particle.as_ref()),
sort_separator: overrides
.sort_separator
.or_else(|| config.and_then(|c| c.sort_separator.as_ref())),
integral_name_state: hints.integral_name_state,
use_integral_short_name: matches!(
options.mode,
citum_schema::citation::CitationMode::Integral
),
short_name_display: options
.config
.integral_name_memory
.as_ref()
.map(|c| c.resolve().short_name_display),
};
let delimiter = config.and_then(|c| c.delimiter.as_deref()).unwrap_or(", ");
let formatted_first: Vec<String> = first_names
.iter()
.enumerate()
.map(|(i, name)| format_single_name(name, form, i, &ctx, hints.expand_given_names))
.collect();
let formatted_last: Vec<String> = last_names
.iter()
.enumerate()
.map(|(i, name)| {
let original_idx = names.len() - last_names.len() + i;
format_single_name(name, form, original_idx, &ctx, hints.expand_given_names)
})
.collect();
let and_option = overrides
.and
.or_else(|| config.and_then(|c| c.and.as_ref()));
let and_str = match and_option {
Some(AndOptions::Text) => Some(locale.and_term(false)),
Some(AndOptions::Symbol) => Some(locale.and_term(true)),
Some(AndOptions::None) | None => None, _ => None, };
let and_str = if use_et_al && formatted_last.is_empty() {
None
} else {
and_str
};
let delimiter_precedes_last = config.and_then(|c| c.delimiter_precedes_last.as_ref());
let result = if formatted_first.len() == 1 {
#[allow(clippy::unwrap_used, reason = "length checked")]
formatted_first.first().unwrap().clone()
} else {
join_names_with_conjunction(
&formatted_first,
and_str,
delimiter,
delimiter_precedes_last,
first_names.len(),
&ctx,
options.context,
)
};
if !use_et_al {
return result;
}
apply_et_al(
result,
&formatted_last,
EtAlContext {
and_others,
delimiter,
delimiter_precedes: config.and_then(|c| c.delimiter_precedes_et_al.as_ref()),
first_count: first_names.len(),
},
&ctx,
locale,
)
}
fn initialize_given_name(
given: &str,
initialize_with: Option<&String>,
initialize_with_hyphen: Option<bool>,
) -> String {
let init = initialize_with.map_or(". ", std::string::String::as_str);
let separators = if initialize_with_hyphen == Some(false) {
vec![' ', '\u{00A0}'] } else {
vec![' ', '-', '\u{00A0}']
};
let mut result = String::new();
let mut current_part = String::new();
for c in given.chars() {
if separators.contains(&c) {
if !current_part.is_empty() {
if let Some(first) = current_part.chars().next() {
result.push(first);
result.push_str(init);
}
current_part.clear();
}
if !c.is_whitespace() {
let trimmed_len = result.trim_end().len();
result.truncate(trimmed_len);
result.push(c);
}
} else {
current_part.push(c);
}
}
if !current_part.is_empty()
&& let Some(first) = current_part.chars().next()
{
result.push(first);
result.push_str(init);
}
result.trim().to_string()
}
fn assemble_long_name(
family_part: String,
given_part: String,
particle_part: String,
suffix: &str,
inverted: bool,
sort_separator: &str,
) -> String {
if inverted {
let mut suffix_part = String::new();
if !given_part.is_empty() {
suffix_part.push_str(&given_part);
}
if !particle_part.is_empty() {
if !suffix_part.is_empty() {
suffix_part.push(' ');
}
suffix_part.push_str(&particle_part);
}
if !suffix.is_empty() {
if !suffix_part.is_empty() {
suffix_part.push(' ');
}
suffix_part.push_str(suffix);
}
if suffix_part.is_empty() {
family_part
} else {
format!("{family_part}{sort_separator}{suffix_part}")
}
} else {
let mut parts = Vec::new();
if !given_part.is_empty() {
parts.push(given_part);
}
if !particle_part.is_empty() {
parts.push(particle_part);
}
if !family_part.is_empty() {
if let Some(last) = parts.last_mut()
&& last.ends_with('-')
{
last.push_str(&family_part);
} else {
parts.push(family_part);
}
}
if !suffix.is_empty() {
parts.push(suffix.to_string());
}
parts.join(" ")
}
}
fn format_literal_name(literal: &str, short: Option<&str>, ctx: &NameFormatContext) -> String {
if ctx.use_integral_short_name
&& let Some(short) = short
{
match ctx.integral_name_state {
Some(citum_schema::citation::IntegralNameState::First) => {
return match ctx.short_name_display {
Some(citum_schema::options::ShortNameDisplay::ShortThenBracketed) => {
format!("{short} [{literal}]")
}
_ => format!("{literal} ({short})"),
};
}
Some(citum_schema::citation::IntegralNameState::Subsequent) => {
return short.to_string();
}
_ => {}
}
}
literal.to_string()
}
pub(crate) fn format_single_name(
name: &crate::reference::FlatName,
form: &ContributorForm,
index: usize,
ctx: &NameFormatContext,
expand_given_names: bool,
) -> String {
fn join_particle_family(particle: &str, family: &str) -> String {
if particle.ends_with('-') {
format!("{particle}{family}")
} else {
format!("{particle} {family}")
}
}
if let Some(literal) = &name.literal {
return format_literal_name(literal, name.short_name.as_deref(), ctx);
}
let family = name.family.as_deref().unwrap_or("");
let given = name.given.as_deref().unwrap_or("");
let dp = name.dropping_particle.as_deref().unwrap_or("");
let ndp = name.non_dropping_particle.as_deref().unwrap_or("");
let suffix = name.suffix.as_deref().unwrap_or("");
let inverted = match ctx.name_order {
Some(NameOrder::GivenFirst) => false,
Some(NameOrder::FamilyFirst) => match ctx.display_as_sort {
Some(DisplayAsSort::First) => index == 0,
_ => true,
},
Some(NameOrder::FamilyFirstOnly) => index == 0,
None => match ctx.display_as_sort {
Some(DisplayAsSort::All) => true,
Some(DisplayAsSort::First) => index == 0,
_ => false,
},
};
let effective_form = if expand_given_names && matches!(form, ContributorForm::Short) {
&ContributorForm::Long
} else {
form
};
match effective_form {
ContributorForm::FamilyOnly => {
family.to_string()
}
ContributorForm::Short => {
if ndp.is_empty() {
family.to_string()
} else {
format!("{ndp} {family}")
}
}
ContributorForm::Long | ContributorForm::Verb | ContributorForm::VerbShort => {
let demote = matches!(
ctx.demote_ndp,
Some(DemoteNonDroppingParticle::DisplayAndSort)
);
let family_part = if !ndp.is_empty() && !demote {
join_particle_family(ndp, family)
} else {
family.to_string()
};
let effective_name_form = match ctx.name_form {
Some(f) => f,
None => NameForm::Full,
};
let given_part = match effective_name_form {
NameForm::FamilyOnly => String::new(),
NameForm::Initials => {
initialize_given_name(given, ctx.initialize_with, ctx.initialize_with_hyphen)
}
NameForm::Full => given.to_string(),
};
let mut particle_part = String::new();
if !dp.is_empty() {
particle_part.push_str(dp);
}
if demote && !ndp.is_empty() {
if !particle_part.is_empty() {
particle_part.push(' ');
}
particle_part.push_str(ndp);
}
let sep = ctx.sort_separator.map_or(", ", std::string::String::as_str);
assemble_long_name(
family_part,
given_part,
particle_part,
suffix,
inverted,
sep,
)
}
}
}
#[must_use]
pub fn format_contributors_short(
names: &[crate::reference::FlatName],
options: &RenderOptions<'_>,
) -> String {
format_names(
names,
&ContributorForm::Short,
options,
&NamesOverrides {
name_order: None,
sort_separator: None,
shorten: None,
and: None,
initialize_with: None,
name_form: None,
},
&ProcHints::default(),
)
}