1use crate::values::{ProcHints, RenderOptions};
9use citum_schema::options::contributors::NameForm;
10use citum_schema::options::{
11 AndOptions, AndOtherOptions, DemoteNonDroppingParticle, DisplayAsSort, ShortenListOptions,
12};
13use citum_schema::template::{ContributorForm, NameOrder};
14use unicode_script::{Script, UnicodeScript};
15
16pub(crate) struct NameFormatContext<'a> {
18 pub(crate) display_as_sort: Option<DisplayAsSort>,
19 pub(crate) name_order: Option<&'a NameOrder>,
20 pub(crate) initialize_with: Option<&'a String>,
21 pub(crate) initialize_with_hyphen: Option<bool>,
22 pub(crate) name_form: Option<NameForm>,
23 pub(crate) demote_ndp: Option<&'a DemoteNonDroppingParticle>,
24 pub(crate) sort_separator: Option<&'a String>,
25 pub(crate) component_sort_separator: Option<&'a String>,
26 pub(crate) script_configs:
27 Option<&'a std::collections::HashMap<String, citum_schema::options::ScriptConfig>>,
28 pub(crate) integral_name_state: Option<citum_schema::citation::IntegralNameState>,
29 pub(crate) org_abbreviation_state: Option<citum_schema::citation::IntegralNameState>,
30 pub(crate) use_integral_short_name: bool,
31 pub(crate) short_name_display: Option<citum_schema::options::ShortNameDisplay>,
32 pub(crate) subsequent_form: Option<citum_schema::options::SubsequentNameForm>,
33}
34
35pub struct NamesOverrides<'a> {
41 pub name_order: Option<&'a NameOrder>,
43 pub sort_separator: Option<&'a String>,
45 pub shorten: Option<&'a ShortenListOptions>,
47 pub and: Option<&'a AndOptions>,
49 pub initialize_with: Option<&'a String>,
51 pub name_form: Option<NameForm>,
53}
54
55fn partition_et_al<'a>(
57 names: &'a [crate::reference::FlatName],
58 shorten: Option<&'a ShortenListOptions>,
59 hints: &'a ProcHints,
60) -> (
61 Vec<&'a crate::reference::FlatName>,
62 bool,
63 Vec<&'a crate::reference::FlatName>,
64) {
65 if let Some(opts) = shorten {
66 let is_subsequent = matches!(
68 hints.position,
69 Some(
70 citum_schema::citation::Position::Subsequent
71 | citum_schema::citation::Position::Ibid
72 | citum_schema::citation::Position::IbidWithLocator
73 )
74 );
75 let effective_min_threshold = if is_subsequent {
76 opts.subsequent_min.unwrap_or(opts.min) as usize
77 } else {
78 opts.min as usize
79 };
80 let effective_use_first = if is_subsequent {
81 opts.subsequent_use_first.unwrap_or(opts.use_first) as usize
82 } else {
83 opts.use_first as usize
84 };
85
86 let effective_min = if let Some(expanded) = hints.min_names_to_show {
89 expanded.max(effective_use_first)
90 } else {
91 effective_use_first
92 };
93
94 if names.len() >= effective_min_threshold {
96 if effective_min >= names.len() {
97 (names.iter().collect::<Vec<_>>(), false, Vec::new())
98 } else {
99 let first: Vec<&crate::reference::FlatName> =
100 names.iter().take(effective_min).collect();
101 let last: Vec<&crate::reference::FlatName> = if let Some(ul) = opts.use_last {
102 let take_last = ul as usize;
103 let skip = std::cmp::max(effective_min, names.len().saturating_sub(take_last));
104 names.iter().skip(skip).collect()
105 } else {
106 Vec::new()
107 };
108 (first, true, last)
109 }
110 } else {
111 (names.iter().collect::<Vec<_>>(), false, Vec::new())
112 }
113 } else {
114 (names.iter().collect::<Vec<_>>(), false, Vec::new())
115 }
116}
117
118fn join_names_with_conjunction(
120 formatted_first: &[String],
121 and_str: Option<&str>,
122 delimiter: &str,
123 delimiter_precedes_last: Option<&citum_schema::options::DelimiterPrecedesLast>,
124 first_names_len: usize,
125 ctx: &NameFormatContext,
126 context: crate::values::RenderContext,
127) -> String {
128 use citum_schema::options::{DelimiterPrecedesLast, DisplayAsSort};
129
130 match and_str {
131 None => {
132 formatted_first.join(delimiter)
134 }
135 Some(conjunction) if formatted_first.len() == 2 => {
136 let use_delimiter = if context == crate::values::RenderContext::Bibliography {
139 if matches!(ctx.name_order, Some(NameOrder::GivenFirst)) {
140 false
141 } else {
142 match delimiter_precedes_last {
144 Some(DelimiterPrecedesLast::Always) => true,
145 Some(DelimiterPrecedesLast::Never) => false,
146 Some(DelimiterPrecedesLast::Contextual) | None => true, Some(DelimiterPrecedesLast::AfterInvertedName) => {
148 ctx.display_as_sort.as_ref().is_some_and(|das| {
149 matches!(das, DisplayAsSort::All | DisplayAsSort::First)
150 })
151 }
152 }
153 }
154 } else {
155 false
157 };
158
159 #[allow(clippy::indexing_slicing, reason = "length checked")]
160 if use_delimiter {
161 format!(
162 "{}{}{} {}",
163 formatted_first[0], delimiter, conjunction, formatted_first[1]
164 )
165 } else {
166 format!(
167 "{} {} {}",
168 formatted_first[0], conjunction, formatted_first[1]
169 )
170 }
171 }
172 Some(conjunction) => {
173 if let Some((last, rest)) = formatted_first.split_last() {
174 let use_delimiter = match delimiter_precedes_last {
176 Some(DelimiterPrecedesLast::Always) => true,
177 Some(DelimiterPrecedesLast::Never) => false,
178 Some(DelimiterPrecedesLast::Contextual) | None => true, Some(DelimiterPrecedesLast::AfterInvertedName) => {
180 ctx.display_as_sort.as_ref().is_some_and(|das| {
181 matches!(das, DisplayAsSort::All)
182 || (matches!(das, DisplayAsSort::First) && first_names_len == 1)
183 })
184 }
185 };
186 if use_delimiter {
187 format!(
188 "{}{}{} {}",
189 rest.join(delimiter),
190 delimiter,
191 conjunction,
192 last
193 )
194 } else {
195 format!("{} {} {}", rest.join(delimiter), conjunction, last)
196 }
197 } else {
198 String::new()
199 }
200 }
201 }
202}
203
204struct EtAlContext<'a> {
206 and_others: AndOtherOptions,
207 delimiter: &'a str,
208 delimiter_precedes: Option<&'a citum_schema::options::DelimiterPrecedesLast>,
209 first_count: usize,
210}
211
212fn apply_et_al(
214 result: String,
215 formatted_last: &[String],
216 et_al: EtAlContext<'_>,
217 ctx: &NameFormatContext,
218 locale: &citum_schema::locale::Locale,
219) -> String {
220 use citum_schema::options::DelimiterPrecedesLast;
221
222 if !formatted_last.is_empty() {
223 return format!("{} … {}", result, formatted_last.join(et_al.delimiter));
226 }
227
228 let use_delimiter = match et_al.delimiter_precedes {
230 Some(DelimiterPrecedesLast::Always) => true,
231 Some(DelimiterPrecedesLast::Never) => false,
232 Some(DelimiterPrecedesLast::AfterInvertedName) => {
233 ctx.display_as_sort.as_ref().is_some_and(|das| {
235 matches!(das, DisplayAsSort::All)
236 || (matches!(das, DisplayAsSort::First) && et_al.first_count == 1)
237 })
238 }
239 Some(DelimiterPrecedesLast::Contextual) | None => {
240 et_al.first_count > 1
242 }
243 };
244
245 let and_others_term = match et_al.and_others {
246 AndOtherOptions::EtAl => locale.et_al(),
247 AndOtherOptions::Text => locale.et_al().trim_end_matches('.'),
248 };
249
250 if use_delimiter {
251 format!("{result}, {and_others_term}")
252 } else {
253 format!("{result} {and_others_term}")
254 }
255}
256
257#[must_use]
265#[allow(
266 clippy::too_many_lines,
267 reason = "linear context-building pipeline; no clean split point"
268)]
269pub fn format_names(
270 names: &[crate::reference::FlatName],
271 form: &ContributorForm,
272 options: &RenderOptions<'_>,
273 overrides: &NamesOverrides<'_>,
274 hints: &ProcHints,
275) -> String {
276 if names.is_empty() {
277 return String::new();
278 }
279
280 let config = options.config.contributors.as_ref();
281 let locale = options.locale;
282
283 let shorten = overrides
287 .shorten
288 .or_else(|| config.and_then(|c| c.shorten.as_ref()));
289
290 let and_others = shorten.map_or(AndOtherOptions::EtAl, |opts| opts.and_others);
291
292 let (first_names, use_et_al, last_names) = partition_et_al(names, shorten, hints);
293
294 let ctx = NameFormatContext {
296 display_as_sort: config.and_then(|c| c.display_as_sort),
297 name_order: overrides.name_order,
298 initialize_with: overrides
299 .initialize_with
300 .or_else(|| config.and_then(|c| c.initialize_with.as_ref())),
301 initialize_with_hyphen: config.and_then(|c| c.initialize_with_hyphen),
302 name_form: overrides
303 .name_form
304 .or_else(|| config.and_then(|c| c.name_form)),
305 demote_ndp: config.and_then(|c| c.demote_non_dropping_particle.as_ref()),
306 sort_separator: overrides
307 .sort_separator
308 .or_else(|| config.and_then(|c| c.sort_separator.as_ref())),
309 component_sort_separator: overrides.sort_separator,
310 script_configs: options
311 .config
312 .multilingual
313 .as_ref()
314 .map(|multilingual| &multilingual.scripts),
315 integral_name_state: hints.integral_name_state,
316 org_abbreviation_state: hints.org_abbreviation_state,
317 use_integral_short_name: matches!(
318 options.mode,
319 citum_schema::citation::CitationMode::Integral
320 ),
321 short_name_display: options
322 .config
323 .org_abbreviation_memory
324 .as_ref()
325 .map(|c| c.resolve().short_name_display),
326 subsequent_form: options
327 .config
328 .integral_name_memory
329 .as_ref()
330 .map(|c| c.resolve().subsequent_form),
331 };
332
333 let delimiter = config.and_then(|c| c.delimiter.as_deref()).unwrap_or(", ");
334
335 let formatted_first: Vec<String> = first_names
336 .iter()
337 .enumerate()
338 .map(|(i, name)| {
339 let expand =
340 hints.expand_given_names && !(hints.expand_given_names_primary_only && i > 0);
341 format_single_name(name, form, i, &ctx, expand)
342 })
343 .collect();
344
345 let formatted_last: Vec<String> = last_names
346 .iter()
347 .enumerate()
348 .map(|(i, name)| {
349 let original_idx = names.len() - last_names.len() + i;
350 let expand = hints.expand_given_names
351 && !(hints.expand_given_names_primary_only && original_idx > 0);
352 format_single_name(name, form, original_idx, &ctx, expand)
353 })
354 .collect();
355
356 let and_option = overrides
358 .and
359 .or_else(|| config.and_then(|c| c.and.as_ref()));
360
361 let and_str = match and_option {
364 Some(AndOptions::Text) => Some(locale.and_term(false)),
365 Some(AndOptions::Symbol) => Some(locale.and_term(true)),
366 Some(AndOptions::None) | None => None, _ => None, };
369 let and_str = if use_et_al && formatted_last.is_empty() {
373 None
374 } else {
375 and_str
376 };
377
378 let delimiter_precedes_last = config.and_then(|c| c.delimiter_precedes_last.as_ref());
380
381 let result = if formatted_first.len() == 1 {
382 #[allow(clippy::unwrap_used, reason = "length checked")]
383 formatted_first.first().unwrap().clone()
384 } else {
385 join_names_with_conjunction(
386 &formatted_first,
387 and_str,
388 delimiter,
389 delimiter_precedes_last,
390 first_names.len(),
391 &ctx,
392 options.context,
393 )
394 };
395
396 if !use_et_al {
397 return result;
398 }
399
400 apply_et_al(
401 result,
402 &formatted_last,
403 EtAlContext {
404 and_others,
405 delimiter,
406 delimiter_precedes: config.and_then(|c| c.delimiter_precedes_et_al.as_ref()),
407 first_count: first_names.len(),
408 },
409 &ctx,
410 locale,
411 )
412}
413
414fn initialize_given_name(
419 given: &str,
420 initialize_with: Option<&String>,
421 initialize_with_hyphen: Option<bool>,
422) -> String {
423 let init = initialize_with.map_or(". ", std::string::String::as_str);
424 let separators = if initialize_with_hyphen == Some(false) {
425 vec![' ', '\u{00A0}'] } else {
427 vec![' ', '-', '\u{00A0}']
428 };
429
430 let mut result = String::new();
431 let mut current_part = String::new();
432
433 for c in given.chars() {
434 if separators.contains(&c) {
435 if !current_part.is_empty() {
436 if let Some(first) = current_part.chars().next() {
437 result.push(first);
438 result.push_str(init);
439 }
440 current_part.clear();
441 }
442 if !c.is_whitespace() {
446 let trimmed_len = result.trim_end().len();
447 result.truncate(trimmed_len);
448 result.push(c);
449 }
450 } else {
451 current_part.push(c);
452 }
453 }
454
455 if !current_part.is_empty()
456 && let Some(first) = current_part.chars().next()
457 {
458 result.push(first);
459 result.push_str(init);
460 }
461 result.trim().to_string()
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq)]
465enum NameAssemblyOrder {
466 GivenFirst,
467 NativeFamilyFirst,
468 Inverted,
469}
470
471#[derive(Debug, Default)]
472struct NameScriptFlags {
473 has_han: bool,
474 has_hiragana: bool,
475 has_katakana: bool,
476 has_hangul: bool,
477}
478
479impl NameScriptFlags {
480 fn record(&mut self, value: &str) {
481 for ch in value.chars() {
482 match ch.script() {
483 Script::Han => self.has_han = true,
484 Script::Hiragana => self.has_hiragana = true,
485 Script::Katakana => self.has_katakana = true,
486 Script::Hangul => self.has_hangul = true,
487 _ => {}
488 }
489 }
490 }
491
492 fn cjk_script_count(&self) -> usize {
493 usize::from(self.has_han)
494 + usize::from(self.has_hiragana)
495 + usize::from(self.has_katakana)
496 + usize::from(self.has_hangul)
497 }
498
499 fn candidate_keys(&self) -> Vec<&'static str> {
500 let count = self.cjk_script_count();
501 if count == 0 {
502 return Vec::new();
503 }
504 if count > 1
506 && !self.has_han
507 && !self.has_hangul
508 && (self.has_hiragana || self.has_katakana)
509 {
510 return vec!["kana", "Hrkt", "cjk"];
511 }
512 if count > 1 {
513 return vec!["cjk"];
514 }
515 if self.has_katakana {
516 return vec!["katakana", "Kana", "Hrkt", "kana", "cjk"];
517 }
518 if self.has_hiragana {
519 return vec!["hiragana", "Hira", "Hrkt", "kana", "cjk"];
520 }
521 if self.has_han {
522 return vec!["han", "Hani", "cjk"];
523 }
524 if self.has_hangul {
525 return vec!["hangul", "Hang", "cjk"];
526 }
527 Vec::new()
528 }
529}
530
531fn script_config_for_name<'a>(
532 name: &crate::reference::FlatName,
533 ctx: &'a NameFormatContext<'a>,
534) -> Option<&'a citum_schema::options::ScriptConfig> {
535 let configs = ctx.script_configs?;
536 if configs.is_empty() {
537 return None;
538 }
539
540 let mut flags = NameScriptFlags::default();
541 for part in [
542 name.family.as_deref(),
543 name.given.as_deref(),
544 name.dropping_particle.as_deref(),
545 name.non_dropping_particle.as_deref(),
546 name.suffix.as_deref(),
547 ]
548 .into_iter()
549 .flatten()
550 {
551 flags.record(part);
552 }
553
554 flags
555 .candidate_keys()
556 .into_iter()
557 .find_map(|key| configs.get(key))
558}
559
560fn assemble_long_name(
565 family_part: String,
566 given_part: String,
567 particle_part: String,
568 suffix: &str,
569 order: NameAssemblyOrder,
570 name_part_delimiter: &str,
571 sort_separator: &str,
572) -> String {
573 match order {
574 NameAssemblyOrder::Inverted => assemble_inverted_long_name(
575 family_part,
576 given_part,
577 particle_part,
578 suffix,
579 sort_separator,
580 ),
581 NameAssemblyOrder::NativeFamilyFirst => assemble_native_family_first_long_name(
582 family_part,
583 given_part,
584 particle_part,
585 suffix,
586 name_part_delimiter,
587 ),
588 NameAssemblyOrder::GivenFirst => assemble_given_first_long_name(
589 family_part,
590 given_part,
591 particle_part,
592 suffix,
593 name_part_delimiter,
594 ),
595 }
596}
597
598fn assemble_inverted_long_name(
599 family_part: String,
600 given_part: String,
601 particle_part: String,
602 suffix: &str,
603 sort_separator: &str,
604) -> String {
605 let mut suffix_part = String::new();
606 if !given_part.is_empty() {
607 suffix_part.push_str(&given_part);
608 }
609 if !particle_part.is_empty() {
610 if !suffix_part.is_empty() {
611 suffix_part.push(' ');
612 }
613 suffix_part.push_str(&particle_part);
614 }
615 if !suffix.is_empty() {
616 if !suffix_part.is_empty() {
617 suffix_part.push(' ');
618 }
619 suffix_part.push_str(suffix);
620 }
621
622 if suffix_part.is_empty() {
623 family_part
624 } else {
625 format!("{family_part}{sort_separator}{suffix_part}")
626 }
627}
628
629fn assemble_native_family_first_long_name(
630 family_part: String,
631 given_part: String,
632 particle_part: String,
633 suffix: &str,
634 name_part_delimiter: &str,
635) -> String {
636 let mut parts = Vec::new();
637 if !family_part.is_empty() {
638 parts.push(family_part);
639 }
640 if !particle_part.is_empty() {
641 parts.push(particle_part);
642 }
643 if !given_part.is_empty() {
644 parts.push(given_part);
645 }
646 if !suffix.is_empty() {
647 parts.push(suffix.to_string());
648 }
649 parts.join(name_part_delimiter)
650}
651
652fn assemble_given_first_long_name(
653 family_part: String,
654 given_part: String,
655 particle_part: String,
656 suffix: &str,
657 name_part_delimiter: &str,
658) -> String {
659 let mut parts = Vec::new();
660 if !given_part.is_empty() {
661 parts.push(given_part);
662 }
663 if !particle_part.is_empty() {
664 parts.push(particle_part);
665 }
666 if !family_part.is_empty() {
667 if let Some(last) = parts.last_mut()
668 && last.ends_with('-')
669 {
670 last.push_str(&family_part);
671 } else {
672 parts.push(family_part);
673 }
674 }
675 if !suffix.is_empty() {
676 parts.push(suffix.to_string());
677 }
678 parts.join(name_part_delimiter)
679}
680
681fn format_literal_name(literal: &str, short: Option<&str>, ctx: &NameFormatContext) -> String {
682 if ctx.use_integral_short_name
683 && let Some(short) = short
684 {
685 match ctx.org_abbreviation_state {
686 Some(citum_schema::citation::IntegralNameState::First) => {
687 return match ctx.short_name_display {
688 Some(citum_schema::options::ShortNameDisplay::ShortThenBracketed) => {
689 format!("{short} [{literal}]")
690 }
691 Some(citum_schema::options::ShortNameDisplay::ShortThenParenthetical) => {
692 format!("{short} ({literal})")
693 }
694 Some(citum_schema::options::ShortNameDisplay::FullThenBracketed) => {
695 format!("{literal} [{short}]")
696 }
697 _ => format!("{literal} ({short})"),
698 };
699 }
700 Some(citum_schema::citation::IntegralNameState::Subsequent) => {
701 return short.to_string();
702 }
703 _ => {}
704 }
705 }
706 literal.to_string()
707}
708
709fn is_inverted_name_order(index: usize, ctx: &NameFormatContext) -> bool {
710 match ctx.name_order {
711 Some(NameOrder::GivenFirst) => false,
712 Some(NameOrder::FamilyFirst) => match ctx.display_as_sort {
713 Some(DisplayAsSort::First) => index == 0,
714 _ => true,
715 },
716 Some(NameOrder::FamilyFirstOnly) => index == 0,
717 None => match ctx.display_as_sort {
718 Some(DisplayAsSort::All) => true,
719 Some(DisplayAsSort::First) => index == 0,
720 _ => false,
721 },
722 }
723}
724
725fn name_assembly_order(
726 inverted: bool,
727 script_config: Option<&citum_schema::options::ScriptConfig>,
728 ctx: &NameFormatContext,
729) -> NameAssemblyOrder {
730 if inverted {
731 return NameAssemblyOrder::Inverted;
732 }
733 let native_family_first =
734 ctx.name_order.is_none() && script_config.is_some_and(|config| config.use_native_ordering);
735 if native_family_first {
736 NameAssemblyOrder::NativeFamilyFirst
737 } else {
738 NameAssemblyOrder::GivenFirst
739 }
740}
741
742fn sort_separator_for_name<'a>(
743 script_config: Option<&'a citum_schema::options::ScriptConfig>,
744 ctx: &'a NameFormatContext<'a>,
745) -> &'a str {
746 ctx.component_sort_separator
747 .map_or_else(
748 || {
749 script_config
750 .and_then(|config| config.sort_separator.as_deref())
751 .or_else(|| ctx.sort_separator.map(std::string::String::as_str))
752 },
753 |separator| Some(separator.as_str()),
754 )
755 .unwrap_or(", ")
756}
757
758pub(crate) fn format_single_name(
760 name: &crate::reference::FlatName,
761 form: &ContributorForm,
762 index: usize,
763 ctx: &NameFormatContext,
764 expand_given_names: bool,
765) -> String {
766 fn join_particle_family(particle: &str, family: &str) -> String {
767 if particle.ends_with('-') {
768 format!("{particle}{family}")
769 } else {
770 format!("{particle} {family}")
771 }
772 }
773
774 if let Some(literal) = &name.literal {
776 return format_literal_name(literal, name.short_name.as_deref(), ctx);
777 }
778
779 let family = name.family.as_deref().unwrap_or("");
780 let given = name.given.as_deref().unwrap_or("");
781 let dp = name.dropping_particle.as_deref().unwrap_or("");
782 let ndp = name.non_dropping_particle.as_deref().unwrap_or("");
783 let suffix = name.suffix.as_deref().unwrap_or("");
784 let script_config = script_config_for_name(name, ctx);
785
786 let inverted = is_inverted_name_order(index, ctx);
790 let assembly_order = name_assembly_order(inverted, script_config, ctx);
791
792 let effective_form = if ctx.use_integral_short_name && ctx.subsequent_form.is_some() {
796 match ctx.integral_name_state {
797 Some(citum_schema::citation::IntegralNameState::First) => &ContributorForm::Long,
798 Some(citum_schema::citation::IntegralNameState::Subsequent) => {
799 match ctx.subsequent_form {
800 Some(citum_schema::options::SubsequentNameForm::FamilyOnly) => {
801 &ContributorForm::FamilyOnly
802 }
803 _ => &ContributorForm::Short,
804 }
805 }
806 _ => {
807 if expand_given_names && matches!(form, ContributorForm::Short) {
808 &ContributorForm::Long
809 } else {
810 form
811 }
812 }
813 }
814 } else if expand_given_names && matches!(form, ContributorForm::Short) {
815 &ContributorForm::Long
816 } else {
817 form
818 };
819
820 match effective_form {
821 ContributorForm::FamilyOnly => {
822 family.to_string()
824 }
825 ContributorForm::Short => {
826 if ndp.is_empty() {
832 family.to_string()
833 } else {
834 format!("{ndp} {family}")
835 }
836 }
837 ContributorForm::Long | ContributorForm::Verb | ContributorForm::VerbShort => {
838 let demote = matches!(
840 ctx.demote_ndp,
841 Some(DemoteNonDroppingParticle::DisplayAndSort)
842 );
843
844 let family_part = if !ndp.is_empty() && !demote {
845 join_particle_family(ndp, family)
846 } else {
847 family.to_string()
848 };
849
850 let effective_name_form = match ctx.name_form {
854 Some(f) => f,
855 None => NameForm::Full,
856 };
857
858 let given_part = match effective_name_form {
859 NameForm::FamilyOnly => String::new(),
860 NameForm::Initials => {
861 initialize_given_name(given, ctx.initialize_with, ctx.initialize_with_hyphen)
862 }
863 NameForm::Full => given.to_string(),
864 };
865
866 let mut particle_part = String::new();
868 if !dp.is_empty() {
869 particle_part.push_str(dp);
870 }
871 if demote && !ndp.is_empty() {
872 if !particle_part.is_empty() {
873 particle_part.push(' ');
874 }
875 particle_part.push_str(ndp);
876 }
877
878 let name_part_delimiter = script_config
879 .and_then(|config| config.delimiter.as_deref())
880 .unwrap_or(" ");
881 let sep = sort_separator_for_name(script_config, ctx);
882 assemble_long_name(
883 family_part,
884 given_part,
885 particle_part,
886 suffix,
887 assembly_order,
888 name_part_delimiter,
889 sep,
890 )
891 }
892 }
893}
894
895#[must_use]
897pub fn format_contributors_short(
898 names: &[crate::reference::FlatName],
899 options: &RenderOptions<'_>,
900) -> String {
901 format_names(
902 names,
903 &ContributorForm::Short,
904 options,
905 &NamesOverrides {
906 name_order: None,
907 sort_separator: None,
908 shorten: None,
909 and: None,
910 initialize_with: None,
911 name_form: None,
912 },
913 &ProcHints::default(),
914 )
915}