1pub(crate) mod labels;
12pub(crate) mod merged;
13pub mod names;
14pub(crate) mod substitute;
15
16use crate::reference::Reference;
17use crate::values::{ComponentValues, ProcHints, ProcValues, RenderContext, RenderOptions};
18use citum_schema::options::SubsequentNameForm;
19use citum_schema::template::{ContributorForm, ContributorRole, TemplateContributor};
20
21#[cfg(test)]
22pub(crate) use names::{NameFormatContext, format_single_name};
23pub use names::{NamesOverrides, format_contributors_short, format_names};
24
25pub(super) fn contributor_for_role(
31 reference: &Reference,
32 role: &ContributorRole,
33) -> Option<citum_schema::reference::Contributor> {
34 match role {
35 ContributorRole::Author => reference.author(),
36 ContributorRole::Editor => reference.editor(),
37 ContributorRole::Translator => reference.translator(),
38 _ => contributor_role_to_reference_role(role).and_then(|role| reference.contributor(role)),
39 }
40}
41
42pub(crate) fn contributor_role_to_reference_role(
44 role: &ContributorRole,
45) -> Option<citum_schema::reference::ContributorRole> {
46 match role {
47 ContributorRole::Author => Some(citum_schema::reference::ContributorRole::Author),
48 ContributorRole::Editor => Some(citum_schema::reference::ContributorRole::Editor),
49 ContributorRole::Translator => Some(citum_schema::reference::ContributorRole::Translator),
50 ContributorRole::Recipient => Some(citum_schema::reference::ContributorRole::Recipient),
51 ContributorRole::Chair => Some(citum_schema::reference::ContributorRole::Unknown(
52 "chair".to_string(),
53 )),
54 ContributorRole::Interviewer => Some(citum_schema::reference::ContributorRole::Interviewer),
55 ContributorRole::Guest => Some(citum_schema::reference::ContributorRole::Guest),
56 ContributorRole::Performer => Some(citum_schema::reference::ContributorRole::Performer),
57 ContributorRole::Director => Some(citum_schema::reference::ContributorRole::Director),
58 ContributorRole::Composer => Some(citum_schema::reference::ContributorRole::Composer),
59 ContributorRole::Writer => Some(citum_schema::reference::ContributorRole::Writer),
60 ContributorRole::Producer => Some(citum_schema::reference::ContributorRole::Producer),
61 ContributorRole::Illustrator => Some(citum_schema::reference::ContributorRole::Illustrator),
62 ContributorRole::Inventor => Some(citum_schema::reference::ContributorRole::Unknown(
63 "inventor".to_string(),
64 )),
65 ContributorRole::Counsel => Some(citum_schema::reference::ContributorRole::Unknown(
66 "counsel".to_string(),
67 )),
68 ContributorRole::CollectionEditor => Some(
69 citum_schema::reference::ContributorRole::Unknown("collection-editor".to_string()),
70 ),
71 ContributorRole::ContainerAuthor => Some(
72 citum_schema::reference::ContributorRole::Unknown("container-author".to_string()),
73 ),
74 ContributorRole::EditorialDirector => Some(
75 citum_schema::reference::ContributorRole::Unknown("editorial-director".to_string()),
76 ),
77 ContributorRole::TextualEditor => Some(citum_schema::reference::ContributorRole::Unknown(
78 "textual-editor".to_string(),
79 )),
80 ContributorRole::OriginalAuthor => Some(citum_schema::reference::ContributorRole::Unknown(
81 "original-author".to_string(),
82 )),
83 ContributorRole::ReviewedAuthor => Some(citum_schema::reference::ContributorRole::Unknown(
84 "reviewed-author".to_string(),
85 )),
86 ContributorRole::Unknown(role) => Some(match role.as_str() {
87 "compiler" => citum_schema::reference::ContributorRole::Compiler,
88 "performer" => citum_schema::reference::ContributorRole::Performer,
89 "narrator" => citum_schema::reference::ContributorRole::Narrator,
90 "host" => citum_schema::reference::ContributorRole::Host,
91 "producer" | "executive-producer" => citum_schema::reference::ContributorRole::Producer,
92 "writer" => citum_schema::reference::ContributorRole::Writer,
93 _ => citum_schema::reference::ContributorRole::Unknown(role.clone()),
94 }),
95 ContributorRole::Interviewee | ContributorRole::Publisher => None,
96 _ => None,
97 }
98}
99
100pub(super) fn is_role_label_omitted(options: &RenderOptions<'_>, role: &ContributorRole) -> bool {
104 options
105 .config
106 .contributors
107 .as_ref()
108 .and_then(|c| c.role.as_ref())
109 .is_some_and(|role_opts| {
110 role_opts
111 .omit
112 .iter()
113 .any(|entry| entry.eq_ignore_ascii_case(role.as_str()))
114 })
115}
116
117pub(super) fn format_role_term<F: crate::render::format::OutputFormat<Output = String>>(
122 term: &str,
123 fmt: &F,
124 effective_rendering: &citum_schema::template::Rendering,
125 options: &RenderOptions<'_>,
126 prefix: &str,
127 suffix: &str,
128) -> String {
129 let term_str = normalized_role_term(term, effective_rendering, options);
130 fmt.text(&format!("{prefix}{term_str}{suffix}"))
131}
132
133fn normalized_role_term(
134 term: &str,
135 effective_rendering: &citum_schema::template::Rendering,
136 options: &RenderOptions<'_>,
137) -> String {
138 let term_str = if crate::values::should_strip_periods(effective_rendering, options) {
139 crate::values::strip_trailing_periods(term)
140 } else {
141 term.to_string()
142 };
143 match effective_rendering.text_case {
149 Some(citum_schema::options::titles::TextCase::CapitalizeFirst) => {
150 crate::values::text_case::apply_text_case_with_language(
151 &term_str,
152 citum_schema::options::titles::TextCase::CapitalizeFirst,
153 Some(options.locale.locale.as_str()),
154 )
155 }
156 _ => term_str,
157 }
158}
159
160pub(super) fn format_wrapped_role_term<F: crate::render::format::OutputFormat<Output = String>>(
166 term: &str,
167 fmt: &F,
168 effective_rendering: &citum_schema::template::Rendering,
169 options: &RenderOptions<'_>,
170 affixes: (&str, &str),
171 wrap: Option<&citum_schema::template::WrapConfig>,
172 item_language: Option<&str>,
173) -> String {
174 let (prefix, suffix) = affixes;
175 let Some(wrap) = wrap else {
176 return format_role_term(term, fmt, effective_rendering, options, prefix, suffix);
177 };
178 let term = normalized_role_term(term, effective_rendering, options);
179 let content = fmt.text(&term);
180 let content = fmt.inner_affix(
181 wrap.inner_prefix.as_deref().unwrap_or_default(),
182 content,
183 wrap.inner_suffix.as_deref().unwrap_or_default(),
184 );
185 let marks = crate::render::format::QuoteMarks::from(&options.locale.grammar_options);
186 let (script, realization) = crate::values::punctuation_realization_context(
187 item_language,
188 options.config.multilingual.as_ref(),
189 options.locale.punctuation_realization.as_ref(),
190 );
191 let content = fmt.wrap_punctuation(
192 &wrap.punctuation,
193 content,
194 &marks,
195 script,
196 realization.as_deref(),
197 );
198 format!("{}{content}{}", fmt.text(prefix), fmt.text(suffix))
199}
200
201fn apply_integral_subsequent_form(
204 component: &mut TemplateContributor,
205 hints: &ProcHints,
206 options: &RenderOptions<'_>,
207) {
208 if options.context != RenderContext::Citation {
209 return;
210 }
211 if !matches!(options.mode, citum_schema::citation::CitationMode::Integral) {
212 return;
213 }
214 if !component.contributor.contains(&ContributorRole::Author) {
215 return;
216 }
217 if !matches!(
218 hints.integral_name_state,
219 Some(citum_schema::citation::IntegralNameState::Subsequent)
220 ) {
221 return;
222 }
223 let Some(memory) = options.config.integral_name_memory.as_ref() else {
224 return;
225 };
226 component.form = match memory.resolve().subsequent_form {
227 SubsequentNameForm::Short => ContributorForm::Short,
228 SubsequentNameForm::FamilyOnly => ContributorForm::FamilyOnly,
229 };
230}
231
232fn format_contributor_names(
234 component: &TemplateContributor,
235 role: &ContributorRole,
236 names_vec: &[crate::reference::FlatName],
237 reference: &Reference,
238 effective_rendering: &citum_schema::template::Rendering,
239 options: &RenderOptions<'_>,
240 hints: &ProcHints,
241) -> String {
242 let effective_name_order = component.name_order.as_ref().or_else(|| {
243 options
244 .config
245 .contributors
246 .as_ref()?
247 .effective_role_name_order(role)
248 });
249 let effective_shorten = component
250 .shorten
251 .as_ref()
252 .or_else(|| options.config.contributors.as_ref()?.shorten.as_ref());
253
254 let effective_name_form = component.name_form.or(effective_rendering.name_form);
259
260 let name_overrides = names::NamesOverrides {
261 name_order: effective_name_order,
262 sort_separator: component.sort_separator.as_ref(),
263 delimiter: component.delimiter.as_ref(),
264 shorten: effective_shorten,
265 and: component.and.as_ref(),
266 initialize_with: effective_rendering.initialize_with.as_ref(),
267 name_form: effective_name_form,
268 strip_periods: effective_rendering.strip_periods,
269 item_language: crate::values::effective_item_language(reference),
270 };
271 names::format_names(names_vec, &component.form, options, &name_overrides, hints)
272}
273
274fn resolve_author_fallback<F: crate::render::format::OutputFormat<Output = String>>(
281 component: &TemplateContributor,
282 reference: &Reference,
283 hints: &ProcHints,
284 options: &RenderOptions<'_>,
285 fmt: &F,
286) -> Option<ProcValues<F::Output>> {
287 let fallbacks = component.fallback.as_ref()?;
288 for fallback in fallbacks {
289 if let Some(values) = fallback.values::<F>(reference, hints, options) {
290 let output = crate::values::date::apply_fallback_component_rendering(
291 fmt,
292 &values.value,
293 values.pre_formatted,
294 fallback.rendering(),
295 reference,
296 options,
297 );
298 return Some(ProcValues {
299 value: output,
300 prefix: None,
301 suffix: None,
302 url: values.url,
303 substituted_key: values.substituted_key,
304 pre_formatted: true,
305 });
306 }
307 }
308 None
309}
310
311impl ComponentValues for TemplateContributor {
312 #[allow(
313 clippy::too_many_lines,
314 reason = "large match statement for contributor role dispatch"
315 )]
316 fn values<F: crate::render::format::OutputFormat<Output = String>>(
317 &self,
318 reference: &Reference,
319 hints: &ProcHints,
320 options: &RenderOptions<'_>,
321 ) -> Option<ProcValues<F::Output>> {
322 let fmt = F::default();
323
324 let mut component = self.clone();
325 let effective_rendering = self.rendering.clone();
326
327 apply_integral_subsequent_form(&mut component, hints, options);
329
330 if effective_rendering.suppress == Some(true) {
332 return None;
333 }
334
335 let Some(role) = component.contributor.as_single().cloned() else {
336 return merged::values::<F>(
337 &component,
338 reference,
339 hints,
340 options,
341 &effective_rendering,
342 &fmt,
343 );
344 };
345
346 if merged::is_role_suppressed(reference, &role, &options.config) {
347 return None;
348 }
349
350 let substitute = citum_schema::options::SubstituteConfig::resolve_or_default(
352 options.config.substitute.as_ref(),
353 );
354
355 if matches!(role, ContributorRole::Author) {
359 if options.suppress_author {
360 return None;
361 }
362 if let Some(values) = substitute::resolve_author_substitute::<F>(
363 &component,
364 hints,
365 options,
366 reference,
367 &effective_rendering,
368 &fmt,
369 substitute.as_ref(),
370 ) {
371 return Some(values);
372 }
373 return resolve_author_fallback::<F>(&component, reference, hints, options, &fmt);
374 }
375
376 let contributor = contributor_for_role(reference, &role);
377
378 if substitute::is_role_suppressed_by_substitute(&role, substitute.as_ref(), reference) {
382 return None;
383 }
384
385 let names_vec = if let Some(contrib) = contributor {
387 substitute::resolve_multilingual_for_contrib(&contrib, options)
388 } else {
389 Vec::new()
390 };
391
392 if names_vec.is_empty() {
394 return substitute::resolve_role_substitute::<F>(
395 &role,
396 &component,
397 hints,
398 options,
399 reference,
400 &effective_rendering,
401 &fmt,
402 substitute.as_ref(),
403 );
404 }
405
406 let formatted = format_contributor_names(
407 &component,
408 &role,
409 &names_vec,
410 reference,
411 &effective_rendering,
412 options,
413 hints,
414 );
415
416 let role_omitted = is_role_label_omitted(options, &role);
417 let (role_prefix, role_suffix) =
418 labels::resolve_role_labels::<F>(labels::RoleLabelContext {
419 component: &component,
420 role: &role,
421 reference,
422 names_count: names_vec.len(),
423 effective_rendering: &effective_rendering,
424 options,
425 fmt: &fmt,
426 role_omitted,
427 });
428
429 let is_pre_formatted = role_prefix.is_some() || role_suffix.is_some();
430 let formatted = crate::values::apply_abbreviation(formatted, options.abbreviation_map);
431 let final_value = if is_pre_formatted {
432 fmt.text(&formatted)
433 } else {
434 formatted
435 };
436
437 Some(ProcValues {
438 value: final_value,
439 prefix: role_prefix,
440 suffix: role_suffix,
441 url: crate::values::resolve_effective_url(
442 component.links.as_ref(),
443 options.config.links.as_ref(),
444 reference,
445 citum_schema::options::LinkAnchor::Component,
446 ),
447 substituted_key: None,
448 pre_formatted: is_pre_formatted,
449 })
450 }
451}