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::Annotator => Some(citum_schema::reference::ContributorRole::Annotator),
51 ContributorRole::Commentator => Some(citum_schema::reference::ContributorRole::Commentator),
52 ContributorRole::ForewordAuthor => {
53 Some(citum_schema::reference::ContributorRole::ForewordAuthor)
54 }
55 ContributorRole::IntroductionAuthor => {
56 Some(citum_schema::reference::ContributorRole::IntroductionAuthor)
57 }
58 ContributorRole::AfterwordAuthor => {
59 Some(citum_schema::reference::ContributorRole::AfterwordAuthor)
60 }
61 ContributorRole::Recipient => Some(citum_schema::reference::ContributorRole::Recipient),
62 ContributorRole::Chair => Some(citum_schema::reference::ContributorRole::Unknown(
63 "chair".to_string(),
64 )),
65 ContributorRole::Interviewer => Some(citum_schema::reference::ContributorRole::Interviewer),
66 ContributorRole::Guest => Some(citum_schema::reference::ContributorRole::Guest),
67 ContributorRole::Performer => Some(citum_schema::reference::ContributorRole::Performer),
68 ContributorRole::Director => Some(citum_schema::reference::ContributorRole::Director),
69 ContributorRole::Composer => Some(citum_schema::reference::ContributorRole::Composer),
70 ContributorRole::Writer => Some(citum_schema::reference::ContributorRole::Writer),
71 ContributorRole::Producer => Some(citum_schema::reference::ContributorRole::Producer),
72 ContributorRole::Illustrator => Some(citum_schema::reference::ContributorRole::Illustrator),
73 ContributorRole::Narrator => Some(citum_schema::reference::ContributorRole::Narrator),
74 ContributorRole::Inventor => Some(citum_schema::reference::ContributorRole::Unknown(
75 "inventor".to_string(),
76 )),
77 ContributorRole::Counsel => Some(citum_schema::reference::ContributorRole::Unknown(
78 "counsel".to_string(),
79 )),
80 ContributorRole::CollectionEditor => Some(
81 citum_schema::reference::ContributorRole::Unknown("collection-editor".to_string()),
82 ),
83 ContributorRole::ContainerAuthor => Some(
84 citum_schema::reference::ContributorRole::Unknown("container-author".to_string()),
85 ),
86 ContributorRole::EditorialDirector => Some(
87 citum_schema::reference::ContributorRole::Unknown("editorial-director".to_string()),
88 ),
89 ContributorRole::TextualEditor => Some(citum_schema::reference::ContributorRole::Unknown(
90 "textual-editor".to_string(),
91 )),
92 ContributorRole::OriginalAuthor => Some(citum_schema::reference::ContributorRole::Unknown(
93 "original-author".to_string(),
94 )),
95 ContributorRole::ReviewedAuthor => Some(citum_schema::reference::ContributorRole::Unknown(
96 "reviewed-author".to_string(),
97 )),
98 ContributorRole::Unknown(role) => Some(match role.as_str() {
99 "compiler" => citum_schema::reference::ContributorRole::Compiler,
100 "performer" => citum_schema::reference::ContributorRole::Performer,
101 "narrator" => citum_schema::reference::ContributorRole::Narrator,
102 "host" => citum_schema::reference::ContributorRole::Host,
103 "producer" | "executive-producer" => citum_schema::reference::ContributorRole::Producer,
104 "writer" => citum_schema::reference::ContributorRole::Writer,
105 _ => citum_schema::reference::ContributorRole::Unknown(role.clone()),
106 }),
107 ContributorRole::Interviewee | ContributorRole::Publisher => None,
108 _ => None,
109 }
110}
111
112pub(super) fn is_role_label_omitted(options: &RenderOptions<'_>, role: &ContributorRole) -> bool {
116 options
117 .config
118 .contributors
119 .as_ref()
120 .and_then(|c| c.role.as_ref())
121 .is_some_and(|role_opts| {
122 role_opts
123 .omit
124 .iter()
125 .any(|entry| entry.eq_ignore_ascii_case(role.as_str()))
126 })
127}
128
129pub(super) fn format_role_term<F: crate::render::format::OutputFormat<Output = String>>(
134 term: &str,
135 fmt: &F,
136 effective_rendering: &citum_schema::template::Rendering,
137 options: &RenderOptions<'_>,
138 prefix: &str,
139 suffix: &str,
140) -> String {
141 let term_str = normalized_role_term(term, effective_rendering, options);
142 fmt.text(&format!("{prefix}{term_str}{suffix}"))
143}
144
145fn normalized_role_term(
146 term: &str,
147 effective_rendering: &citum_schema::template::Rendering,
148 options: &RenderOptions<'_>,
149) -> String {
150 let term_str = if crate::values::should_strip_periods(effective_rendering, options) {
151 crate::values::strip_trailing_periods(term)
152 } else {
153 term.to_string()
154 };
155 match effective_rendering.text_case {
161 Some(citum_schema::options::titles::TextCase::CapitalizeFirst) => {
162 crate::values::text_case::apply_text_case_with_language(
163 &term_str,
164 citum_schema::options::titles::TextCase::CapitalizeFirst,
165 Some(options.locale.locale.as_str()),
166 )
167 }
168 _ => term_str,
169 }
170}
171
172pub(super) fn format_wrapped_role_term<F: crate::render::format::OutputFormat<Output = String>>(
178 term: &str,
179 fmt: &F,
180 effective_rendering: &citum_schema::template::Rendering,
181 options: &RenderOptions<'_>,
182 affixes: (&str, &str),
183 wrap: Option<&citum_schema::template::WrapConfig>,
184 item_language: Option<&str>,
185) -> String {
186 let (prefix, suffix) = affixes;
187 let Some(wrap) = wrap else {
188 return format_role_term(term, fmt, effective_rendering, options, prefix, suffix);
189 };
190 let term = normalized_role_term(term, effective_rendering, options);
191 let content = fmt.text(&term);
192 let content = fmt.inner_affix(
193 wrap.inner_prefix.as_deref().unwrap_or_default(),
194 content,
195 wrap.inner_suffix.as_deref().unwrap_or_default(),
196 );
197 let marks = crate::render::format::QuoteMarks::from(&options.locale.grammar_options);
198 let (script, realization) = crate::values::punctuation_realization_context(
199 item_language,
200 options.config.multilingual.as_ref(),
201 options.locale.punctuation_realization.as_ref(),
202 );
203 let content = fmt.wrap_punctuation(
204 &wrap.punctuation,
205 content,
206 &marks,
207 script,
208 realization.as_deref(),
209 );
210 format!("{}{content}{}", fmt.text(prefix), fmt.text(suffix))
211}
212
213fn apply_integral_subsequent_form(
216 component: &mut TemplateContributor,
217 hints: &ProcHints,
218 options: &RenderOptions<'_>,
219) {
220 if options.context != RenderContext::Citation {
221 return;
222 }
223 if !matches!(options.mode, citum_schema::citation::CitationMode::Integral) {
224 return;
225 }
226 if !component.contributor.contains(&ContributorRole::Author) {
227 return;
228 }
229 if !matches!(
230 hints.integral_name_state,
231 Some(citum_schema::citation::IntegralNameState::Subsequent)
232 ) {
233 return;
234 }
235 let Some(memory) = options.config.integral_name_memory.as_ref() else {
236 return;
237 };
238 component.form = match memory.resolve().subsequent_form {
239 SubsequentNameForm::Short => ContributorForm::Short,
240 SubsequentNameForm::FamilyOnly => ContributorForm::FamilyOnly,
241 };
242}
243
244fn format_contributor_names(
246 component: &TemplateContributor,
247 role: &ContributorRole,
248 names_vec: &[crate::reference::FlatName],
249 reference: &Reference,
250 effective_rendering: &citum_schema::template::Rendering,
251 options: &RenderOptions<'_>,
252 hints: &ProcHints,
253) -> String {
254 let effective_name_order = component.name_order.as_ref().or_else(|| {
255 options
256 .config
257 .contributors
258 .as_ref()?
259 .effective_role_name_order(role)
260 });
261 let effective_shorten = component
262 .shorten
263 .as_ref()
264 .or_else(|| options.config.contributors.as_ref()?.shorten.as_ref());
265
266 let effective_name_form = component.name_form.or(effective_rendering.name_form);
271
272 let name_overrides = names::NamesOverrides {
273 name_order: effective_name_order,
274 sort_separator: component.sort_separator.as_ref(),
275 delimiter: component.delimiter.as_ref(),
276 shorten: effective_shorten,
277 and: component.and.as_ref(),
278 initialize_with: effective_rendering.initialize_with.as_ref(),
279 name_form: effective_name_form,
280 strip_periods: effective_rendering.strip_periods,
281 item_language: crate::values::effective_item_language(reference),
282 };
283 names::format_names(names_vec, &component.form, options, &name_overrides, hints)
284}
285
286fn resolve_author_fallback<F: crate::render::format::OutputFormat<Output = String>>(
293 component: &TemplateContributor,
294 reference: &Reference,
295 hints: &ProcHints,
296 options: &RenderOptions<'_>,
297 fmt: &F,
298) -> Option<ProcValues<F::Output>> {
299 let fallbacks = component.fallback.as_ref()?;
300 for fallback in fallbacks {
301 if let Some(values) = fallback.values::<F>(reference, hints, options) {
302 let substituted_key = values.substituted_key.clone();
303 let output = crate::values::date::render_fallback_component(
304 fmt, fallback, values, reference, options,
305 );
306 if output.trim().is_empty() {
307 continue;
308 }
309 return Some(ProcValues {
310 value: output,
311 prefix: None,
312 suffix: None,
313 url: None,
314 substituted_key,
315 pre_formatted: true,
316 });
317 }
318 }
319 None
320}
321
322impl ComponentValues for TemplateContributor {
323 #[allow(
324 clippy::too_many_lines,
325 reason = "large match statement for contributor role dispatch"
326 )]
327 fn values<F: crate::render::format::OutputFormat<Output = String>>(
328 &self,
329 reference: &Reference,
330 hints: &ProcHints,
331 options: &RenderOptions<'_>,
332 ) -> Option<ProcValues<F::Output>> {
333 let fmt = F::default();
334
335 let mut component = self.clone();
336 let effective_rendering = self.rendering.clone();
337
338 apply_integral_subsequent_form(&mut component, hints, options);
340
341 if effective_rendering.suppress == Some(true) {
343 return None;
344 }
345
346 let Some(role) = component.contributor.as_single().cloned() else {
347 return merged::values::<F>(
348 &component,
349 reference,
350 hints,
351 options,
352 &effective_rendering,
353 &fmt,
354 );
355 };
356
357 if merged::is_role_suppressed(reference, &role, &options.config) {
358 return None;
359 }
360
361 let substitute = citum_schema::options::SubstituteConfig::resolve_or_default(
363 options.config.substitute.as_ref(),
364 );
365
366 if matches!(role, ContributorRole::Author) {
370 if options.suppress_author {
371 return None;
372 }
373 if let Some(values) = substitute::resolve_author_substitute::<F>(
374 &component,
375 hints,
376 options,
377 reference,
378 &effective_rendering,
379 &fmt,
380 substitute.as_ref(),
381 ) {
382 return Some(values);
383 }
384 return resolve_author_fallback::<F>(&component, reference, hints, options, &fmt);
385 }
386
387 let contributor = contributor_for_role(reference, &role);
388
389 if substitute::is_role_suppressed_by_substitute(&role, substitute.as_ref(), reference) {
393 return None;
394 }
395
396 let names_vec = if let Some(contrib) = contributor {
398 substitute::resolve_multilingual_for_contrib(&contrib, options)
399 } else {
400 Vec::new()
401 };
402
403 if names_vec.is_empty() {
405 return substitute::resolve_role_substitute::<F>(
406 &role,
407 &component,
408 hints,
409 options,
410 reference,
411 &effective_rendering,
412 &fmt,
413 substitute.as_ref(),
414 );
415 }
416
417 let formatted = format_contributor_names(
418 &component,
419 &role,
420 &names_vec,
421 reference,
422 &effective_rendering,
423 options,
424 hints,
425 );
426
427 let role_omitted = is_role_label_omitted(options, &role);
428 let (role_prefix, role_suffix) =
429 labels::resolve_role_labels::<F>(labels::RoleLabelContext {
430 component: &component,
431 role: &role,
432 reference,
433 names_count: names_vec.len(),
434 effective_rendering: &effective_rendering,
435 options,
436 fmt: &fmt,
437 role_omitted,
438 });
439
440 let is_pre_formatted = role_prefix.is_some() || role_suffix.is_some();
441 let formatted = crate::values::apply_abbreviation(formatted, options.abbreviation_map);
442 let final_value = if is_pre_formatted {
443 fmt.text(&formatted)
444 } else {
445 formatted
446 };
447
448 Some(ProcValues {
449 value: final_value,
450 prefix: role_prefix,
451 suffix: role_suffix,
452 url: crate::values::resolve_effective_url(
453 component.links.as_ref(),
454 options.config.links.as_ref(),
455 reference,
456 citum_schema::options::LinkAnchor::Component,
457 ),
458 substituted_key: None,
459 pre_formatted: is_pre_formatted,
460 })
461 }
462}