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::Inventor => Some(citum_schema::reference::ContributorRole::Unknown(
74 "inventor".to_string(),
75 )),
76 ContributorRole::Counsel => Some(citum_schema::reference::ContributorRole::Unknown(
77 "counsel".to_string(),
78 )),
79 ContributorRole::CollectionEditor => Some(
80 citum_schema::reference::ContributorRole::Unknown("collection-editor".to_string()),
81 ),
82 ContributorRole::ContainerAuthor => Some(
83 citum_schema::reference::ContributorRole::Unknown("container-author".to_string()),
84 ),
85 ContributorRole::EditorialDirector => Some(
86 citum_schema::reference::ContributorRole::Unknown("editorial-director".to_string()),
87 ),
88 ContributorRole::TextualEditor => Some(citum_schema::reference::ContributorRole::Unknown(
89 "textual-editor".to_string(),
90 )),
91 ContributorRole::OriginalAuthor => Some(citum_schema::reference::ContributorRole::Unknown(
92 "original-author".to_string(),
93 )),
94 ContributorRole::ReviewedAuthor => Some(citum_schema::reference::ContributorRole::Unknown(
95 "reviewed-author".to_string(),
96 )),
97 ContributorRole::Unknown(role) => Some(match role.as_str() {
98 "compiler" => citum_schema::reference::ContributorRole::Compiler,
99 "performer" => citum_schema::reference::ContributorRole::Performer,
100 "narrator" => citum_schema::reference::ContributorRole::Narrator,
101 "host" => citum_schema::reference::ContributorRole::Host,
102 "producer" | "executive-producer" => citum_schema::reference::ContributorRole::Producer,
103 "writer" => citum_schema::reference::ContributorRole::Writer,
104 _ => citum_schema::reference::ContributorRole::Unknown(role.clone()),
105 }),
106 ContributorRole::Interviewee | ContributorRole::Publisher => None,
107 _ => None,
108 }
109}
110
111pub(super) fn is_role_label_omitted(options: &RenderOptions<'_>, role: &ContributorRole) -> bool {
115 options
116 .config
117 .contributors
118 .as_ref()
119 .and_then(|c| c.role.as_ref())
120 .is_some_and(|role_opts| {
121 role_opts
122 .omit
123 .iter()
124 .any(|entry| entry.eq_ignore_ascii_case(role.as_str()))
125 })
126}
127
128pub(super) fn format_role_term<F: crate::render::format::OutputFormat<Output = String>>(
133 term: &str,
134 fmt: &F,
135 effective_rendering: &citum_schema::template::Rendering,
136 options: &RenderOptions<'_>,
137 prefix: &str,
138 suffix: &str,
139) -> String {
140 let term_str = normalized_role_term(term, effective_rendering, options);
141 fmt.text(&format!("{prefix}{term_str}{suffix}"))
142}
143
144fn normalized_role_term(
145 term: &str,
146 effective_rendering: &citum_schema::template::Rendering,
147 options: &RenderOptions<'_>,
148) -> String {
149 let term_str = if crate::values::should_strip_periods(effective_rendering, options) {
150 crate::values::strip_trailing_periods(term)
151 } else {
152 term.to_string()
153 };
154 match effective_rendering.text_case {
160 Some(citum_schema::options::titles::TextCase::CapitalizeFirst) => {
161 crate::values::text_case::apply_text_case_with_language(
162 &term_str,
163 citum_schema::options::titles::TextCase::CapitalizeFirst,
164 Some(options.locale.locale.as_str()),
165 )
166 }
167 _ => term_str,
168 }
169}
170
171pub(super) fn format_wrapped_role_term<F: crate::render::format::OutputFormat<Output = String>>(
177 term: &str,
178 fmt: &F,
179 effective_rendering: &citum_schema::template::Rendering,
180 options: &RenderOptions<'_>,
181 affixes: (&str, &str),
182 wrap: Option<&citum_schema::template::WrapConfig>,
183 item_language: Option<&str>,
184) -> String {
185 let (prefix, suffix) = affixes;
186 let Some(wrap) = wrap else {
187 return format_role_term(term, fmt, effective_rendering, options, prefix, suffix);
188 };
189 let term = normalized_role_term(term, effective_rendering, options);
190 let content = fmt.text(&term);
191 let content = fmt.inner_affix(
192 wrap.inner_prefix.as_deref().unwrap_or_default(),
193 content,
194 wrap.inner_suffix.as_deref().unwrap_or_default(),
195 );
196 let marks = crate::render::format::QuoteMarks::from(&options.locale.grammar_options);
197 let (script, realization) = crate::values::punctuation_realization_context(
198 item_language,
199 options.config.multilingual.as_ref(),
200 options.locale.punctuation_realization.as_ref(),
201 );
202 let content = fmt.wrap_punctuation(
203 &wrap.punctuation,
204 content,
205 &marks,
206 script,
207 realization.as_deref(),
208 );
209 format!("{}{content}{}", fmt.text(prefix), fmt.text(suffix))
210}
211
212fn apply_integral_subsequent_form(
215 component: &mut TemplateContributor,
216 hints: &ProcHints,
217 options: &RenderOptions<'_>,
218) {
219 if options.context != RenderContext::Citation {
220 return;
221 }
222 if !matches!(options.mode, citum_schema::citation::CitationMode::Integral) {
223 return;
224 }
225 if !component.contributor.contains(&ContributorRole::Author) {
226 return;
227 }
228 if !matches!(
229 hints.integral_name_state,
230 Some(citum_schema::citation::IntegralNameState::Subsequent)
231 ) {
232 return;
233 }
234 let Some(memory) = options.config.integral_name_memory.as_ref() else {
235 return;
236 };
237 component.form = match memory.resolve().subsequent_form {
238 SubsequentNameForm::Short => ContributorForm::Short,
239 SubsequentNameForm::FamilyOnly => ContributorForm::FamilyOnly,
240 };
241}
242
243fn format_contributor_names(
245 component: &TemplateContributor,
246 role: &ContributorRole,
247 names_vec: &[crate::reference::FlatName],
248 reference: &Reference,
249 effective_rendering: &citum_schema::template::Rendering,
250 options: &RenderOptions<'_>,
251 hints: &ProcHints,
252) -> String {
253 let effective_name_order = component.name_order.as_ref().or_else(|| {
254 options
255 .config
256 .contributors
257 .as_ref()?
258 .effective_role_name_order(role)
259 });
260 let effective_shorten = component
261 .shorten
262 .as_ref()
263 .or_else(|| options.config.contributors.as_ref()?.shorten.as_ref());
264
265 let effective_name_form = component.name_form.or(effective_rendering.name_form);
270
271 let name_overrides = names::NamesOverrides {
272 name_order: effective_name_order,
273 sort_separator: component.sort_separator.as_ref(),
274 delimiter: component.delimiter.as_ref(),
275 shorten: effective_shorten,
276 and: component.and.as_ref(),
277 initialize_with: effective_rendering.initialize_with.as_ref(),
278 name_form: effective_name_form,
279 strip_periods: effective_rendering.strip_periods,
280 item_language: crate::values::effective_item_language(reference),
281 };
282 names::format_names(names_vec, &component.form, options, &name_overrides, hints)
283}
284
285fn resolve_author_fallback<F: crate::render::format::OutputFormat<Output = String>>(
292 component: &TemplateContributor,
293 reference: &Reference,
294 hints: &ProcHints,
295 options: &RenderOptions<'_>,
296 fmt: &F,
297) -> Option<ProcValues<F::Output>> {
298 let fallbacks = component.fallback.as_ref()?;
299 for fallback in fallbacks {
300 if let Some(values) = fallback.values::<F>(reference, hints, options) {
301 let output = crate::values::date::apply_fallback_component_rendering(
302 fmt,
303 &values.value,
304 values.pre_formatted,
305 fallback.rendering(),
306 reference,
307 options,
308 );
309 return Some(ProcValues {
310 value: output,
311 prefix: None,
312 suffix: None,
313 url: values.url,
314 substituted_key: values.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}