1use super::RenderedBibliographyGroup;
9use crate::api::AnnotationStyle;
10use crate::grouping::SelectorEvaluator;
11use crate::processor::FinalizedRun;
12use crate::processor::Processor;
13use crate::processor::disambiguation::Disambiguator;
14use crate::reference::{Bibliography, Reference};
15use crate::render::ProcEntry;
16use crate::render::format::{OutputFormat, ProcEntryMetadata};
17use crate::sorting::ReferenceSorter;
18use crate::values::{
19 ProcHints, RenderContext, RenderOptions, format_contributors_short, resolve_multilingual_name,
20 resolve_multilingual_string,
21};
22use citum_schema::grouping::{BibliographyGroup, DisambiguationScope, GroupHeading};
23use citum_schema::options::{BibliographyPartitionHeading, BibliographySortPartitioning};
24use std::borrow::Cow;
25use std::collections::{HashMap, HashSet};
26use std::sync::Arc;
27
28use super::EntryRenderContext;
29
30#[derive(Clone, Copy, PartialEq, Eq)]
32enum FlatBibliographyOutput {
33 ContentOnly,
35 ContentAndEntries,
37}
38
39impl Processor {
40 pub(super) fn resolve_group_heading(&self, heading: &GroupHeading) -> Option<String> {
42 match heading {
43 GroupHeading::Literal { literal } => Some(literal.clone()),
44 GroupHeading::Term { term, form } => self.locale.resolved_general_term(
45 term,
46 &form.clone().unwrap_or(citum_schema::locale::TermForm::Long),
47 None,
48 ),
49 GroupHeading::Localized { localized } => self.resolve_localized_heading(localized),
50 }
51 }
52
53 fn resolve_localized_heading(&self, localized: &HashMap<String, String>) -> Option<String> {
62 fn language_tag(locale: &str) -> &str {
63 locale.split('-').next().unwrap_or(locale)
64 }
65
66 let mut candidates = Vec::new();
67 let mut push_candidate = |locale: &str| {
68 let candidate = locale.to_string();
69 if !candidates.contains(&candidate) {
70 candidates.push(candidate);
71 }
72 };
73
74 push_candidate(&self.locale.locale);
75 push_candidate(language_tag(&self.locale.locale));
76
77 if let Some(default_locale) = self.style.info.default_locale.as_deref() {
78 push_candidate(default_locale);
79 push_candidate(language_tag(default_locale));
80 }
81
82 push_candidate("en-US");
83 push_candidate("en");
84
85 for locale in candidates {
86 if let Some(value) = localized.get(&locale) {
87 return Some(value.clone());
88 }
89 }
90
91 localized
92 .iter()
93 .min_by(|left, right| left.0.cmp(right.0))
94 .map(|(_locale, value)| value.clone())
95 }
96
97 fn resolve_partition_heading(&self, heading: &BibliographyPartitionHeading) -> Option<String> {
99 match heading {
100 BibliographyPartitionHeading::Literal { literal } => Some(literal.clone()),
101 BibliographyPartitionHeading::Term { term, form } => self.locale.resolved_general_term(
102 term,
103 &form.clone().unwrap_or(citum_schema::locale::TermForm::Long),
104 None,
105 ),
106 BibliographyPartitionHeading::Localized { localized } => {
107 self.resolve_localized_heading(localized)
108 }
109 }
110 }
111
112 fn collect_matching_group_refs<'a>(
114 &'a self,
115 bibliography: &'a [ProcEntry],
116 assigned: &HashSet<String>,
117 evaluator: &SelectorEvaluator<'_>,
118 group: &BibliographyGroup,
119 ) -> Vec<&'a Reference> {
120 bibliography
121 .iter()
122 .filter(|entry| !assigned.contains(&entry.id))
123 .filter_map(|entry| {
124 self.bibliography
125 .get(&entry.id)
126 .filter(|reference| evaluator.matches(reference, &group.selector))
127 })
128 .collect()
129 }
130
131 pub(crate) fn sorted_id_stubs(&self) -> Vec<ProcEntry> {
139 self.sort_references(self.bibliography.values().collect())
142 .into_iter()
143 .filter_map(|r| {
144 r.id().map(|id| ProcEntry {
145 id: id.to_string(),
146 template: vec![],
147 metadata: ProcEntryMetadata::default(),
148 })
149 })
150 .collect()
151 }
152
153 fn mark_group_members_assigned(assigned: &mut HashSet<String>, references: &[&Reference]) {
155 for reference in references {
156 if let Some(id) = reference.id() {
157 assigned.insert(id.to_string());
158 }
159 }
160 }
161
162 fn build_group_local_hints(
166 &self,
167 sorted_refs: &[&Reference],
168 group: &BibliographyGroup,
169 ) -> Option<HashMap<String, ProcHints>> {
170 if !matches!(group.disambiguate, Some(DisambiguationScope::Locally)) {
171 return None;
172 }
173
174 let mut group_bibliography = Bibliography::new();
175 for reference in sorted_refs {
176 group_bibliography.insert(
177 reference.id().unwrap_or_default().to_string(),
178 (*reference).clone(),
179 );
180 }
181
182 let resolved_sort = group
183 .sort
184 .as_ref()
185 .map(citum_schema::GroupSortEntry::resolve);
186 let bibliography_config = self.get_bibliography_config();
187 let disambiguator = if let Some(sort) = resolved_sort.as_ref() {
188 Disambiguator::with_group_sort(
189 &group_bibliography,
190 &bibliography_config,
191 &self.locale,
192 sort,
193 )
194 } else {
195 Disambiguator::new(&group_bibliography, &bibliography_config, &self.locale)
196 };
197
198 Some(disambiguator.calculate_hints())
199 }
200
201 fn effective_group_style<'a>(
203 &'a self,
204 group: &'a BibliographyGroup,
205 ) -> Cow<'a, citum_schema::Style> {
206 if let Some(group_template) = &group.template {
207 let mut local_style = self.style.clone();
208 if let Some(bibliography) = local_style.bibliography.as_mut() {
209 bibliography.template = Some(group_template.clone());
210 }
211 Cow::Owned(local_style)
212 } else {
213 Cow::Borrowed(&self.style)
214 }
215 }
216
217 fn render_group_entries<F>(
228 &self,
229 _bibliography: &[ProcEntry],
230 sorted_refs: Vec<&Reference>,
231 group: &BibliographyGroup,
232 local_hints: Option<&HashMap<String, ProcHints>>,
233 run: &FinalizedRun,
234 ) -> Vec<ProcEntry>
235 where
236 F: OutputFormat<Output = String>,
237 {
238 let effective_style = self.effective_group_style(group);
241 let ctx = EntryRenderContext {
242 style: &effective_style,
243 hints: local_hints.unwrap_or(&self.hints),
244 config: Arc::new(self.get_bibliography_config().into_owned()),
245 bibliography_config: Arc::new(self.get_bibliography_options().into_owned()),
246 run,
247 };
248
249 let numbered_refs = super::number_sorted_refs(sorted_refs.into_iter(), run);
250 let rendered = self.render_numbered_refs::<F>(&numbered_refs, &ctx);
251
252 let substitute = ctx
253 .bibliography_config
254 .subsequent_author_substitute
255 .as_ref();
256 self.apply_substitution_post_pass::<F>(rendered, substitute, &ctx)
257 }
258
259 fn append_rendered_group<F>(
261 &self,
262 result: &mut String,
263 group: &BibliographyGroup,
264 entries: Vec<ProcEntry>,
265 annotations: Option<&HashMap<String, String>>,
266 annotation_style: Option<&AnnotationStyle>,
267 suppress_heading: bool,
268 ) where
269 F: OutputFormat<Output = String>,
270 {
271 if !result.is_empty() {
272 result.push_str("\n\n");
273 }
274
275 if !suppress_heading
276 && let Some(heading) = group
277 .heading
278 .as_ref()
279 .and_then(|group_heading| self.resolve_group_heading(group_heading))
280 {
281 result.push_str(&self.render_group_heading::<F>(&heading));
282 }
283
284 result.push_str(&crate::render::refs_to_string_with_format::<F>(
285 entries,
286 annotations,
287 annotation_style,
288 ));
289 }
290
291 fn append_rendered_partition<F>(
293 &self,
294 result: &mut String,
295 heading: Option<&BibliographyPartitionHeading>,
296 entries: Vec<ProcEntry>,
297 annotations: Option<&HashMap<String, String>>,
298 annotation_style: Option<&AnnotationStyle>,
299 ) where
300 F: OutputFormat<Output = String>,
301 {
302 if !result.is_empty() {
303 result.push_str("\n\n");
304 }
305
306 if let Some(heading) =
307 heading.and_then(|group_heading| self.resolve_partition_heading(group_heading))
308 {
309 result.push_str(&self.render_group_heading::<F>(&heading));
310 }
311
312 result.push_str(&crate::render::refs_to_string_with_format::<F>(
313 entries,
314 annotations,
315 annotation_style,
316 ));
317 }
318
319 pub(super) fn render_with_partition_sections<F>(
321 &self,
322 sorted_refs: Vec<&Reference>,
323 partitioning: &BibliographySortPartitioning,
324 annotations: Option<&HashMap<String, String>>,
325 annotation_style: Option<&AnnotationStyle>,
326 run: &FinalizedRun,
327 ) -> String
328 where
329 F: OutputFormat<Output = String>,
330 {
331 let fmt = F::default();
332 let mut result = String::new();
333
334 for (partition_key, references) in
335 crate::sort_partitioning::partition_references(sorted_refs, &self.locale, partitioning)
336 {
337 let heading = partition_key
338 .as_ref()
339 .and_then(|key| partitioning.headings.get(key));
340 let entries = self.merge_compound_entries::<F>(
341 self.process_sorted_refs::<_, F>(references.into_iter(), run),
342 run,
343 );
344 self.append_rendered_partition::<F>(
345 &mut result,
346 heading,
347 entries,
348 annotations,
349 annotation_style,
350 );
351 }
352
353 fmt.finish(result)
354 }
355
356 pub(super) fn render_with_custom_groups_filtered<F>(
363 &self,
364 all_entries: &[ProcEntry],
365 groups: &[BibliographyGroup],
366 selected: &HashSet<String>,
367 annotations: Option<&HashMap<String, String>>,
368 annotation_style: Option<&AnnotationStyle>,
369 run: &FinalizedRun,
370 ) -> String
371 where
372 F: OutputFormat<Output = String>,
373 {
374 let fmt = F::default();
375 let cited_ids = &run.state().cited_ids;
376 let evaluator = SelectorEvaluator::new(cited_ids);
377 let bibliography_config = self.get_bibliography_config();
378 let sorter = ReferenceSorter::with_bibliography_config(&self.locale, &bibliography_config);
379
380 let mut assigned = HashSet::new();
381 let mut result = String::new();
382
383 let mut populated_groups: Vec<(&BibliographyGroup, Vec<ProcEntry>)> = Vec::new();
385
386 for group in groups {
387 let matching_refs =
388 self.collect_matching_group_refs(all_entries, &assigned, &evaluator, group);
389
390 let matching_refs: Vec<&Reference> = matching_refs
391 .into_iter()
392 .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
393 .collect();
394
395 if matching_refs.is_empty() {
396 continue;
397 }
398
399 Self::mark_group_members_assigned(&mut assigned, &matching_refs);
400
401 let sorted_refs = if let Some(sort_spec) = &group.sort {
402 sorter.sort_references(matching_refs, &sort_spec.resolve())
403 } else {
404 matching_refs
405 };
406 let local_hints = self.build_group_local_hints(&sorted_refs, group);
407 let entries = self.merge_compound_entries::<F>(
408 self.render_group_entries::<F>(
409 all_entries,
410 sorted_refs,
411 group,
412 local_hints.as_ref(),
413 run,
414 ),
415 run,
416 );
417
418 populated_groups.push((group, entries));
419 }
420
421 let unassigned_refs: Vec<&Reference> = all_entries
423 .iter()
424 .filter(|entry| !assigned.contains(&entry.id) && selected.contains(&entry.id))
425 .filter_map(|entry| self.bibliography.get(&entry.id))
426 .collect();
427
428 let suppress_heading = populated_groups.len() == 1 && unassigned_refs.is_empty();
429
430 for (group, entries) in populated_groups {
432 self.append_rendered_group::<F>(
433 &mut result,
434 group,
435 entries,
436 annotations,
437 annotation_style,
438 suppress_heading,
439 );
440 }
441
442 self.append_unassigned_entries_filtered::<F>(
443 &mut result,
444 all_entries,
445 &assigned,
446 selected,
447 annotations,
448 annotation_style,
449 run,
450 );
451 fmt.finish(result)
452 }
453
454 #[allow(
456 clippy::too_many_arguments,
457 reason = "internal helper, all params load-bearing"
458 )]
459 fn append_unassigned_entries_filtered<F>(
460 &self,
461 result: &mut String,
462 bibliography: &[ProcEntry],
463 assigned: &HashSet<String>,
464 selected: &HashSet<String>,
465 annotations: Option<&HashMap<String, String>>,
466 annotation_style: Option<&AnnotationStyle>,
467 run: &FinalizedRun,
468 ) where
469 F: OutputFormat<Output = String>,
470 {
471 let unassigned_refs: Vec<&Reference> = bibliography
472 .iter()
473 .filter(|entry| !assigned.contains(&entry.id) && selected.contains(&entry.id))
474 .filter_map(|entry| self.bibliography.get(&entry.id))
475 .collect();
476
477 if unassigned_refs.is_empty() {
478 return;
479 }
480
481 let unassigned = self.merge_compound_entries::<F>(
484 self.process_sorted_refs::<_, F>(unassigned_refs.into_iter(), run),
485 run,
486 );
487
488 if !result.is_empty() {
489 result.push_str("\n\n");
490 }
491
492 result.push_str(&crate::render::refs_to_string_with_format::<F>(
493 unassigned,
494 annotations,
495 annotation_style,
496 ));
497 }
498
499 fn render_flat_compound_entries<F>(
509 &self,
510 bibliography: &[ProcEntry],
511 restrict_to_cited: bool,
512 annotations: Option<&HashMap<String, String>>,
513 annotation_style: Option<&AnnotationStyle>,
514 run: &FinalizedRun,
515 ) -> String
516 where
517 F: OutputFormat<Output = String>,
518 {
519 let fmt = F::default();
520 let selected = if restrict_to_cited {
521 let cited_ids = &run.state().cited_ids;
522 let compound_groups = &run.state().compound_groups;
523 let ref_to_group = Self::build_compound_group_lookup(compound_groups);
524 let row_is_cited = |id: &str| match ref_to_group.get(id) {
525 Some(group_number) => compound_groups
526 .get(group_number)
527 .is_some_and(|members| members.iter().any(|member| cited_ids.contains(member))),
528 None => cited_ids.contains(id),
529 };
530 Cow::Owned(
531 bibliography
532 .iter()
533 .filter(|entry| row_is_cited(&entry.id))
534 .cloned()
535 .collect(),
536 )
537 } else {
538 Cow::Borrowed(bibliography)
539 };
540
541 let result = if selected.is_empty() {
542 String::new()
543 } else {
544 crate::render::refs_to_string_slice_with_format::<F>(
545 selected.as_ref(),
546 annotations,
547 annotation_style,
548 )
549 };
550
551 fmt.finish(result)
552 }
553
554 pub fn render_grouped_bibliography_with_format<F>(&self, run: &FinalizedRun) -> String
561 where
562 F: OutputFormat<Output = String>,
563 {
564 self.render_grouped_bibliography_with_format_and_annotations::<F>(None, None, run)
565 }
566
567 pub fn render_grouped_bibliography_with_format_and_annotations<F>(
569 &self,
570 annotations: Option<&HashMap<String, String>>,
571 annotation_style: Option<&AnnotationStyle>,
572 run: &FinalizedRun,
573 ) -> String
574 where
575 F: OutputFormat<Output = String>,
576 {
577 self.render_grouped_bibliography_inner::<F>(false, annotations, annotation_style, run)
578 }
579
580 pub fn render_grouped_bibliography_with_format_standalone<F>(&self) -> String
583 where
584 F: OutputFormat<Output = String>,
585 {
586 let run = self.begin_run().finalize();
587 self.render_grouped_bibliography_with_format::<F>(&run)
588 }
589
590 pub fn render_grouped_bibliography_with_format_and_annotations_standalone<F>(
594 &self,
595 annotations: Option<&HashMap<String, String>>,
596 annotation_style: Option<&AnnotationStyle>,
597 ) -> String
598 where
599 F: OutputFormat<Output = String>,
600 {
601 let run = self.begin_run().finalize();
602 self.render_grouped_bibliography_with_format_and_annotations::<F>(
603 annotations,
604 annotation_style,
605 &run,
606 )
607 }
608
609 pub(crate) fn render_document_bibliography<F>(
634 &self,
635 restrict_to_cited: bool,
636 annotations: Option<&HashMap<String, String>>,
637 annotation_style: Option<&AnnotationStyle>,
638 run: &FinalizedRun,
639 ) -> super::DocumentBibliography
640 where
641 F: OutputFormat<Output = String>,
642 {
643 let has_custom_groups = self.effective_custom_groups().is_some();
644
645 if has_custom_groups || !run.state().compound_groups.is_empty() {
646 let content = self.render_grouped_bibliography_inner::<F>(
647 restrict_to_cited,
648 annotations,
649 annotation_style,
650 run,
651 );
652 let cited_ids: Vec<String> = run.state().cited_ids.iter().cloned().collect();
653 let entries = if restrict_to_cited {
654 self.process_selected_references_with_format::<F, _>(cited_ids, run)
655 .bibliography
656 } else {
657 self.process_references_with_format::<F>(run).bibliography
658 };
659 return super::DocumentBibliography { content, entries };
660 }
661
662 let bibliography_options = self.get_bibliography_options();
663 let partitioning = bibliography_options
664 .sort_partitioning
665 .as_ref()
666 .filter(|partitioning| crate::sort_partitioning::should_render_sections(partitioning));
667
668 self.render_flat_bibliography::<F>(
669 restrict_to_cited,
670 partitioning,
671 FlatBibliographyOutput::ContentAndEntries,
672 annotations,
673 annotation_style,
674 run,
675 )
676 }
677
678 fn sorted_eligible_refs(&self, restrict_to_cited: bool, run: &FinalizedRun) -> Vec<&Reference> {
681 let mut refs: Vec<&Reference> = self.bibliography.values().collect();
682 if restrict_to_cited {
683 let cited = &run.state().cited_ids;
684 refs.retain(|reference| {
685 reference
686 .id()
687 .as_deref()
688 .is_some_and(|id| cited.contains(id))
689 });
690 }
691 self.sort_references(refs)
692 }
693
694 fn render_flat_bibliography<F>(
726 &self,
727 restrict_to_cited: bool,
728 partitioning: Option<&BibliographySortPartitioning>,
729 output: FlatBibliographyOutput,
730 annotations: Option<&HashMap<String, String>>,
731 annotation_style: Option<&AnnotationStyle>,
732 run: &FinalizedRun,
733 ) -> super::DocumentBibliography
734 where
735 F: OutputFormat<Output = String>,
736 {
737 let sorted_refs = self.sorted_eligible_refs(restrict_to_cited, run);
738
739 if output == FlatBibliographyOutput::ContentOnly
740 && let Some(partitioning) = partitioning
741 {
742 let content = self.render_with_partition_sections::<F>(
743 sorted_refs,
744 partitioning,
745 annotations,
746 annotation_style,
747 run,
748 );
749 return super::DocumentBibliography {
750 content,
751 entries: Vec::new(),
752 };
753 }
754
755 let ctx = self.flat_render_context(run);
756 let numbered_refs = super::number_sorted_refs(sorted_refs.iter().copied(), run);
757 let rendered = self.render_numbered_refs::<F>(&numbered_refs, &ctx);
758
759 let substitute = ctx
760 .bibliography_config
761 .subsequent_author_substitute
762 .as_ref();
763
764 if let Some(partitioning) = partitioning {
765 let entries =
769 self.apply_substitution_post_pass::<F>(rendered.clone(), substitute, &ctx);
770 let mut result = String::new();
771 for (partition_key, refs_in_section) in crate::sort_partitioning::partition_references(
772 sorted_refs,
773 &self.locale,
774 partitioning,
775 ) {
776 let heading = partition_key
777 .as_ref()
778 .and_then(|key| partitioning.headings.get(key));
779 let section_ids: HashSet<String> = refs_in_section
780 .iter()
781 .filter_map(|reference| reference.id().map(|id| id.to_string()))
782 .collect();
783 let section_rendered = rendered
784 .iter()
785 .filter(|(reference, _)| {
786 reference
787 .id()
788 .as_deref()
789 .is_some_and(|id| section_ids.contains(id))
790 })
791 .cloned()
792 .collect();
793 let section_entries =
794 self.apply_substitution_post_pass::<F>(section_rendered, substitute, &ctx);
795 self.append_rendered_partition::<F>(
796 &mut result,
797 heading,
798 section_entries,
799 annotations,
800 annotation_style,
801 );
802 }
803 return super::DocumentBibliography {
804 content: F::default().finish(result),
805 entries,
806 };
807 }
808
809 let rendered_entries = self.apply_substitution_post_pass::<F>(rendered, substitute, &ctx);
810 let content = if rendered_entries.is_empty() {
811 String::new()
812 } else {
813 crate::render::refs_to_string_slice_with_format::<F>(
814 &rendered_entries,
815 annotations,
816 annotation_style,
817 )
818 };
819 let entries = match output {
820 FlatBibliographyOutput::ContentAndEntries => rendered_entries,
821 FlatBibliographyOutput::ContentOnly => Vec::new(),
822 };
823
824 super::DocumentBibliography { content, entries }
825 }
826
827 fn render_grouped_bibliography_inner<F>(
834 &self,
835 restrict_to_cited: bool,
836 annotations: Option<&HashMap<String, String>>,
837 annotation_style: Option<&AnnotationStyle>,
838 run: &FinalizedRun,
839 ) -> String
840 where
841 F: OutputFormat<Output = String>,
842 {
843 if let Some(groups) = self.effective_custom_groups() {
844 let id_stubs = self.sorted_id_stubs();
845 let selected = if restrict_to_cited {
846 let cited = &run.state().cited_ids;
847 id_stubs
848 .iter()
849 .filter(|e| cited.contains(&e.id))
850 .map(|e| e.id.clone())
851 .collect::<HashSet<_>>()
852 } else {
853 id_stubs
854 .iter()
855 .map(|e| e.id.clone())
856 .collect::<HashSet<_>>()
857 };
858 return self.render_with_custom_groups_filtered::<F>(
859 &id_stubs,
860 groups,
861 &selected,
862 annotations,
863 annotation_style,
864 run,
865 );
866 }
867
868 let bibliography_options = self.get_bibliography_options();
869 let partitioning = bibliography_options
870 .sort_partitioning
871 .as_ref()
872 .filter(|partitioning| crate::sort_partitioning::should_render_sections(partitioning));
873
874 if !run.state().compound_groups.is_empty() {
875 if let Some(partitioning) = partitioning {
876 let sorted_refs = self.sorted_eligible_refs(restrict_to_cited, run);
877 return self.render_with_partition_sections::<F>(
878 sorted_refs,
879 partitioning,
880 annotations,
881 annotation_style,
882 run,
883 );
884 }
885
886 let all_entries = self.process_references_with_format::<F>(run).bibliography;
887 let merged = self.merge_compound_entries::<F>(all_entries, run);
888 return self.render_flat_compound_entries::<F>(
889 &merged,
890 restrict_to_cited,
891 annotations,
892 annotation_style,
893 run,
894 );
895 }
896
897 self.render_flat_bibliography::<F>(
898 restrict_to_cited,
899 partitioning,
900 FlatBibliographyOutput::ContentOnly,
901 annotations,
902 annotation_style,
903 run,
904 )
905 .content
906 }
907
908 fn entries_for_bibliography_group<F>(
919 &self,
920 spine: &[ProcEntry],
921 group: &BibliographyGroup,
922 assigned: &mut HashSet<String>,
923 run: &FinalizedRun,
924 ) -> Vec<crate::render::ProcEntry>
925 where
926 F: OutputFormat<Output = String>,
927 {
928 let cited_ids = &run.state().cited_ids;
929 let evaluator = SelectorEvaluator::new(cited_ids);
930 let bibliography_config = self.get_bibliography_config();
931 let sorter = ReferenceSorter::with_bibliography_config(&self.locale, &bibliography_config);
932
933 let matching_refs = self.collect_matching_group_refs(spine, assigned, &evaluator, group);
934 Self::mark_group_members_assigned(assigned, &matching_refs);
935
936 if matching_refs.is_empty() {
937 return Vec::new();
938 }
939
940 let sorted_refs = if let Some(sort_spec) = &group.sort {
941 sorter.sort_references(matching_refs, &sort_spec.resolve())
942 } else {
943 matching_refs
944 };
945
946 let local_hints = self.build_group_local_hints(&sorted_refs, group);
947 self.merge_compound_entries::<F>(
948 self.render_group_entries::<F>(spine, sorted_refs, group, local_hints.as_ref(), run),
949 run,
950 )
951 }
952
953 #[allow(
965 clippy::too_many_arguments,
966 reason = "internal helper, all params load-bearing"
967 )]
968 pub(crate) fn render_document_bibliography_block<F>(
969 &self,
970 spine: &[ProcEntry],
971 group: &BibliographyGroup,
972 assigned: &mut HashSet<String>,
973 annotations: Option<&HashMap<String, String>>,
974 annotation_style: Option<&AnnotationStyle>,
975 run: &FinalizedRun,
976 ) -> RenderedBibliographyGroup
977 where
978 F: OutputFormat<Output = String>,
979 {
980 let mut headingless = group.clone();
981 let heading = headingless
982 .heading
983 .take()
984 .and_then(|group_heading| self.resolve_group_heading(&group_heading));
985
986 let entries = self.entries_for_bibliography_group::<F>(spine, &headingless, assigned, run);
987 let body = crate::render::refs_to_string_slice_with_format::<F>(
988 &entries,
989 annotations,
990 annotation_style,
991 );
992
993 RenderedBibliographyGroup {
994 heading,
995 body,
996 entries,
997 }
998 }
999
1000 pub(crate) fn render_document_bibliography_blocks<F>(
1009 &self,
1010 groups: &[BibliographyGroup],
1011 annotations: Option<&HashMap<String, String>>,
1012 annotation_style: Option<&AnnotationStyle>,
1013 run: &FinalizedRun,
1014 ) -> Vec<RenderedBibliographyGroup>
1015 where
1016 F: OutputFormat<Output = String>,
1017 {
1018 let spine = self.sorted_id_stubs();
1019 let mut assigned = std::collections::HashSet::new();
1020 groups
1021 .iter()
1022 .map(|group| {
1023 self.render_document_bibliography_block::<F>(
1024 &spine,
1025 group,
1026 &mut assigned,
1027 annotations,
1028 annotation_style,
1029 run,
1030 )
1031 })
1032 .collect()
1033 }
1034
1035 pub(super) fn extract_metadata(
1036 &self,
1037 reference: &Reference,
1038 ctx: &EntryRenderContext<'_>,
1039 ) -> ProcEntryMetadata {
1040 let bibliography_config = &ctx.config;
1041 let options = RenderOptions {
1042 config: bibliography_config.clone(),
1043 bibliography_config: Some(ctx.bibliography_config.clone()),
1044 locale: &self.locale,
1045 context: RenderContext::Bibliography,
1046 mode: citum_schema::citation::CitationMode::NonIntegral,
1047 suppress_author: false,
1048 locator_raw: None,
1049 ref_type: None,
1050 show_semantics: self.show_semantics,
1051 current_template_index: None,
1052 abbreviation_map: self.abbreviation_map.as_ref(),
1053 };
1054
1055 let ml = bibliography_config.multilingual.as_ref();
1056 let preferred_transliteration = ml.and_then(|m| m.preferred_transliteration.as_deref());
1057 let preferred_script = ml.and_then(|m| m.preferred_script.as_ref());
1058
1059 ProcEntryMetadata {
1060 author: reference.author().map(|author| {
1061 let names = resolve_multilingual_name(
1062 &author,
1063 ml.and_then(|m| m.name_mode.as_ref()),
1064 preferred_transliteration,
1065 preferred_script,
1066 &self.locale.locale,
1067 );
1068 format_contributors_short(&names, &options)
1069 }),
1070 year: reference
1071 .effective_issued_date()
1072 .map(|issued| issued.year().clone()),
1073 title: reference.title().map(|title| {
1074 use citum_schema::reference::types::{MultilingualString, Title};
1075 match &title {
1076 Title::Multilingual(m) => resolve_multilingual_string(
1077 &MultilingualString::Complex(m.clone()),
1078 ml.and_then(|ml| ml.title_mode.as_ref()),
1079 preferred_transliteration,
1080 preferred_script,
1081 &self.locale.locale,
1082 ),
1083 _ => title.to_string(),
1084 }
1085 }),
1086 }
1087 }
1088
1089 fn render_group_heading<F>(&self, heading: &str) -> String
1090 where
1091 F: OutputFormat<Output = String>,
1092 {
1093 let fmt = F::default();
1094 fmt.finish(fmt.unnumbered_heading(2, fmt.text(heading)))
1095 }
1096}