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 &bibliography_config,
192 &self.locale,
193 sort,
194 )
195 } else {
196 Disambiguator::new(
197 &group_bibliography,
198 &bibliography_config,
199 &bibliography_config,
200 &self.locale,
201 )
202 };
203
204 Some(disambiguator.calculate_hints())
205 }
206
207 fn effective_group_style<'a>(
209 &'a self,
210 group: &'a BibliographyGroup,
211 ) -> Cow<'a, citum_schema::Style> {
212 if let Some(group_template) = &group.template {
213 let mut local_style = self.style.clone();
214 if let Some(bibliography) = local_style.bibliography.as_mut() {
215 bibliography.template = Some(group_template.clone());
216 }
217 Cow::Owned(local_style)
218 } else {
219 Cow::Borrowed(&self.style)
220 }
221 }
222
223 fn render_group_entries<F>(
234 &self,
235 _bibliography: &[ProcEntry],
236 sorted_refs: Vec<&Reference>,
237 group: &BibliographyGroup,
238 local_hints: Option<&HashMap<String, ProcHints>>,
239 run: &FinalizedRun,
240 ) -> Vec<ProcEntry>
241 where
242 F: OutputFormat<Output = String>,
243 {
244 let effective_style = self.effective_group_style(group);
247 let ctx = EntryRenderContext {
248 style: &effective_style,
249 hints: local_hints.unwrap_or(&self.hints),
250 config: Arc::new(self.get_bibliography_config().into_owned()),
251 bibliography_config: Arc::new(self.get_bibliography_options().into_owned()),
252 run,
253 };
254
255 let numbered_refs = super::number_sorted_refs(sorted_refs.into_iter(), run);
256 let rendered = self.render_numbered_refs::<F>(&numbered_refs, &ctx);
257
258 let substitute = ctx
259 .bibliography_config
260 .subsequent_author_substitute
261 .as_ref();
262 self.apply_substitution_post_pass::<F>(rendered, substitute, &ctx)
263 }
264
265 fn append_rendered_group<F>(
267 &self,
268 result: &mut String,
269 group: &BibliographyGroup,
270 entries: Vec<ProcEntry>,
271 annotations: Option<&HashMap<String, String>>,
272 annotation_style: Option<&AnnotationStyle>,
273 suppress_heading: bool,
274 ) where
275 F: OutputFormat<Output = String>,
276 {
277 if !result.is_empty() {
278 result.push_str("\n\n");
279 }
280
281 if !suppress_heading
282 && let Some(heading) = group
283 .heading
284 .as_ref()
285 .and_then(|group_heading| self.resolve_group_heading(group_heading))
286 {
287 result.push_str(&self.render_group_heading::<F>(&heading));
288 }
289
290 result.push_str(&crate::render::refs_to_string_with_format::<F>(
291 entries,
292 annotations,
293 annotation_style,
294 ));
295 }
296
297 fn append_rendered_partition<F>(
299 &self,
300 result: &mut String,
301 heading: Option<&BibliographyPartitionHeading>,
302 entries: Vec<ProcEntry>,
303 annotations: Option<&HashMap<String, String>>,
304 annotation_style: Option<&AnnotationStyle>,
305 ) where
306 F: OutputFormat<Output = String>,
307 {
308 if !result.is_empty() {
309 result.push_str("\n\n");
310 }
311
312 if let Some(heading) =
313 heading.and_then(|group_heading| self.resolve_partition_heading(group_heading))
314 {
315 result.push_str(&self.render_group_heading::<F>(&heading));
316 }
317
318 result.push_str(&crate::render::refs_to_string_with_format::<F>(
319 entries,
320 annotations,
321 annotation_style,
322 ));
323 }
324
325 pub(super) fn render_with_partition_sections<F>(
327 &self,
328 sorted_refs: Vec<&Reference>,
329 partitioning: &BibliographySortPartitioning,
330 annotations: Option<&HashMap<String, String>>,
331 annotation_style: Option<&AnnotationStyle>,
332 run: &FinalizedRun,
333 ) -> String
334 where
335 F: OutputFormat<Output = String>,
336 {
337 let fmt = F::default();
338 let mut result = String::new();
339
340 for (partition_key, references) in
341 crate::sort_partitioning::partition_references(sorted_refs, &self.locale, partitioning)
342 {
343 let heading = partition_key
344 .as_ref()
345 .and_then(|key| partitioning.headings.get(key));
346 let entries = self.merge_compound_entries::<F>(
347 self.process_sorted_refs::<_, F>(references.into_iter(), run),
348 run,
349 );
350 self.append_rendered_partition::<F>(
351 &mut result,
352 heading,
353 entries,
354 annotations,
355 annotation_style,
356 );
357 }
358
359 fmt.finish(result)
360 }
361
362 pub(super) fn render_with_custom_groups_filtered<F>(
369 &self,
370 all_entries: &[ProcEntry],
371 groups: &[BibliographyGroup],
372 selected: &HashSet<String>,
373 annotations: Option<&HashMap<String, String>>,
374 annotation_style: Option<&AnnotationStyle>,
375 run: &FinalizedRun,
376 ) -> String
377 where
378 F: OutputFormat<Output = String>,
379 {
380 let fmt = F::default();
381 let cited_ids = &run.state().cited_ids;
382 let evaluator = SelectorEvaluator::new(cited_ids);
383 let bibliography_config = self.get_bibliography_config();
384 let sorter = ReferenceSorter::with_bibliography_config(&self.locale, &bibliography_config);
385
386 let mut assigned = HashSet::new();
387 let mut result = String::new();
388
389 let mut populated_groups: Vec<(&BibliographyGroup, Vec<ProcEntry>)> = Vec::new();
391
392 for group in groups {
393 let matching_refs =
394 self.collect_matching_group_refs(all_entries, &assigned, &evaluator, group);
395
396 let matching_refs: Vec<&Reference> = matching_refs
397 .into_iter()
398 .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
399 .collect();
400
401 if matching_refs.is_empty() {
402 continue;
403 }
404
405 Self::mark_group_members_assigned(&mut assigned, &matching_refs);
406
407 let sorted_refs = if let Some(sort_spec) = &group.sort {
408 sorter.sort_references(matching_refs, &sort_spec.resolve())
409 } else {
410 matching_refs
411 };
412 let local_hints = self.build_group_local_hints(&sorted_refs, group);
413 let entries = self.merge_compound_entries::<F>(
414 self.render_group_entries::<F>(
415 all_entries,
416 sorted_refs,
417 group,
418 local_hints.as_ref(),
419 run,
420 ),
421 run,
422 );
423
424 populated_groups.push((group, entries));
425 }
426
427 let unassigned_refs: Vec<&Reference> = all_entries
429 .iter()
430 .filter(|entry| !assigned.contains(&entry.id) && selected.contains(&entry.id))
431 .filter_map(|entry| self.bibliography.get(&entry.id))
432 .collect();
433
434 let suppress_heading = populated_groups.len() == 1 && unassigned_refs.is_empty();
435
436 for (group, entries) in populated_groups {
438 self.append_rendered_group::<F>(
439 &mut result,
440 group,
441 entries,
442 annotations,
443 annotation_style,
444 suppress_heading,
445 );
446 }
447
448 self.append_unassigned_entries_filtered::<F>(
449 &mut result,
450 all_entries,
451 &assigned,
452 selected,
453 annotations,
454 annotation_style,
455 run,
456 );
457 fmt.finish(result)
458 }
459
460 #[allow(
462 clippy::too_many_arguments,
463 reason = "internal helper, all params load-bearing"
464 )]
465 fn append_unassigned_entries_filtered<F>(
466 &self,
467 result: &mut String,
468 bibliography: &[ProcEntry],
469 assigned: &HashSet<String>,
470 selected: &HashSet<String>,
471 annotations: Option<&HashMap<String, String>>,
472 annotation_style: Option<&AnnotationStyle>,
473 run: &FinalizedRun,
474 ) where
475 F: OutputFormat<Output = String>,
476 {
477 let unassigned_refs: Vec<&Reference> = bibliography
478 .iter()
479 .filter(|entry| !assigned.contains(&entry.id) && selected.contains(&entry.id))
480 .filter_map(|entry| self.bibliography.get(&entry.id))
481 .collect();
482
483 if unassigned_refs.is_empty() {
484 return;
485 }
486
487 let unassigned = self.merge_compound_entries::<F>(
490 self.process_sorted_refs::<_, F>(unassigned_refs.into_iter(), run),
491 run,
492 );
493
494 if !result.is_empty() {
495 result.push_str("\n\n");
496 }
497
498 result.push_str(&crate::render::refs_to_string_with_format::<F>(
499 unassigned,
500 annotations,
501 annotation_style,
502 ));
503 }
504
505 fn render_flat_compound_entries<F>(
515 &self,
516 bibliography: &[ProcEntry],
517 restrict_to_cited: bool,
518 annotations: Option<&HashMap<String, String>>,
519 annotation_style: Option<&AnnotationStyle>,
520 run: &FinalizedRun,
521 ) -> String
522 where
523 F: OutputFormat<Output = String>,
524 {
525 let fmt = F::default();
526 let selected = if restrict_to_cited {
527 let cited_ids = &run.state().cited_ids;
528 let compound_groups = &run.state().compound_groups;
529 let ref_to_group = Self::build_compound_group_lookup(compound_groups);
530 let row_is_cited = |id: &str| match ref_to_group.get(id) {
531 Some(group_number) => compound_groups
532 .get(group_number)
533 .is_some_and(|members| members.iter().any(|member| cited_ids.contains(member))),
534 None => cited_ids.contains(id),
535 };
536 Cow::Owned(
537 bibliography
538 .iter()
539 .filter(|entry| row_is_cited(&entry.id))
540 .cloned()
541 .collect(),
542 )
543 } else {
544 Cow::Borrowed(bibliography)
545 };
546
547 let result = if selected.is_empty() {
548 String::new()
549 } else {
550 crate::render::refs_to_string_slice_with_format::<F>(
551 selected.as_ref(),
552 annotations,
553 annotation_style,
554 )
555 };
556
557 fmt.finish(result)
558 }
559
560 pub fn render_grouped_bibliography_with_format<F>(&self, run: &FinalizedRun) -> String
567 where
568 F: OutputFormat<Output = String>,
569 {
570 self.render_grouped_bibliography_with_format_and_annotations::<F>(None, None, run)
571 }
572
573 pub fn render_grouped_bibliography_with_format_and_annotations<F>(
575 &self,
576 annotations: Option<&HashMap<String, String>>,
577 annotation_style: Option<&AnnotationStyle>,
578 run: &FinalizedRun,
579 ) -> String
580 where
581 F: OutputFormat<Output = String>,
582 {
583 self.render_grouped_bibliography_inner::<F>(false, annotations, annotation_style, run)
584 }
585
586 pub fn render_grouped_bibliography_with_format_standalone<F>(&self) -> String
589 where
590 F: OutputFormat<Output = String>,
591 {
592 let run = self.begin_run().finalize();
593 self.render_grouped_bibliography_with_format::<F>(&run)
594 }
595
596 pub fn render_grouped_bibliography_with_format_and_annotations_standalone<F>(
600 &self,
601 annotations: Option<&HashMap<String, String>>,
602 annotation_style: Option<&AnnotationStyle>,
603 ) -> String
604 where
605 F: OutputFormat<Output = String>,
606 {
607 let run = self.begin_run().finalize();
608 self.render_grouped_bibliography_with_format_and_annotations::<F>(
609 annotations,
610 annotation_style,
611 &run,
612 )
613 }
614
615 pub(crate) fn render_document_bibliography<F>(
640 &self,
641 restrict_to_cited: bool,
642 annotations: Option<&HashMap<String, String>>,
643 annotation_style: Option<&AnnotationStyle>,
644 run: &FinalizedRun,
645 ) -> super::DocumentBibliography
646 where
647 F: OutputFormat<Output = String>,
648 {
649 let has_custom_groups = self.effective_custom_groups().is_some();
650
651 if has_custom_groups || !run.state().compound_groups.is_empty() {
652 let content = self.render_grouped_bibliography_inner::<F>(
653 restrict_to_cited,
654 annotations,
655 annotation_style,
656 run,
657 );
658 let cited_ids: Vec<String> = run.state().cited_ids.iter().cloned().collect();
659 let entries = if restrict_to_cited {
660 self.process_selected_references_with_format::<F, _>(cited_ids, run)
661 .bibliography
662 } else {
663 self.process_references_with_format::<F>(run).bibliography
664 };
665 return super::DocumentBibliography { content, entries };
666 }
667
668 let bibliography_options = self.get_bibliography_options();
669 let partitioning = bibliography_options
670 .sort_partitioning
671 .as_ref()
672 .filter(|partitioning| crate::sort_partitioning::should_render_sections(partitioning));
673
674 self.render_flat_bibliography::<F>(
675 restrict_to_cited,
676 partitioning,
677 FlatBibliographyOutput::ContentAndEntries,
678 annotations,
679 annotation_style,
680 run,
681 )
682 }
683
684 fn sorted_eligible_refs(&self, restrict_to_cited: bool, run: &FinalizedRun) -> Vec<&Reference> {
687 let mut refs: Vec<&Reference> = self.bibliography.values().collect();
688 if restrict_to_cited {
689 let cited = &run.state().cited_ids;
690 refs.retain(|reference| {
691 reference
692 .id()
693 .as_deref()
694 .is_some_and(|id| cited.contains(id))
695 });
696 }
697 self.sort_references(refs)
698 }
699
700 fn render_flat_bibliography<F>(
732 &self,
733 restrict_to_cited: bool,
734 partitioning: Option<&BibliographySortPartitioning>,
735 output: FlatBibliographyOutput,
736 annotations: Option<&HashMap<String, String>>,
737 annotation_style: Option<&AnnotationStyle>,
738 run: &FinalizedRun,
739 ) -> super::DocumentBibliography
740 where
741 F: OutputFormat<Output = String>,
742 {
743 let sorted_refs = self.sorted_eligible_refs(restrict_to_cited, run);
744
745 if output == FlatBibliographyOutput::ContentOnly
746 && let Some(partitioning) = partitioning
747 {
748 let content = self.render_with_partition_sections::<F>(
749 sorted_refs,
750 partitioning,
751 annotations,
752 annotation_style,
753 run,
754 );
755 return super::DocumentBibliography {
756 content,
757 entries: Vec::new(),
758 };
759 }
760
761 let ctx = self.flat_render_context(run);
762 let numbered_refs = super::number_sorted_refs(sorted_refs.iter().copied(), run);
763 let rendered = self.render_numbered_refs::<F>(&numbered_refs, &ctx);
764
765 let substitute = ctx
766 .bibliography_config
767 .subsequent_author_substitute
768 .as_ref();
769
770 if let Some(partitioning) = partitioning {
771 let entries =
775 self.apply_substitution_post_pass::<F>(rendered.clone(), substitute, &ctx);
776 let mut result = String::new();
777 for (partition_key, refs_in_section) in crate::sort_partitioning::partition_references(
778 sorted_refs,
779 &self.locale,
780 partitioning,
781 ) {
782 let heading = partition_key
783 .as_ref()
784 .and_then(|key| partitioning.headings.get(key));
785 let section_ids: HashSet<String> = refs_in_section
786 .iter()
787 .filter_map(|reference| reference.id().map(|id| id.to_string()))
788 .collect();
789 let section_rendered = rendered
790 .iter()
791 .filter(|(reference, _)| {
792 reference
793 .id()
794 .as_deref()
795 .is_some_and(|id| section_ids.contains(id))
796 })
797 .cloned()
798 .collect();
799 let section_entries =
800 self.apply_substitution_post_pass::<F>(section_rendered, substitute, &ctx);
801 self.append_rendered_partition::<F>(
802 &mut result,
803 heading,
804 section_entries,
805 annotations,
806 annotation_style,
807 );
808 }
809 return super::DocumentBibliography {
810 content: F::default().finish(result),
811 entries,
812 };
813 }
814
815 let rendered_entries = self.apply_substitution_post_pass::<F>(rendered, substitute, &ctx);
816 let content = if rendered_entries.is_empty() {
817 String::new()
818 } else {
819 crate::render::refs_to_string_slice_with_format::<F>(
820 &rendered_entries,
821 annotations,
822 annotation_style,
823 )
824 };
825 let entries = match output {
826 FlatBibliographyOutput::ContentAndEntries => rendered_entries,
827 FlatBibliographyOutput::ContentOnly => Vec::new(),
828 };
829
830 super::DocumentBibliography { content, entries }
831 }
832
833 fn render_grouped_bibliography_inner<F>(
840 &self,
841 restrict_to_cited: bool,
842 annotations: Option<&HashMap<String, String>>,
843 annotation_style: Option<&AnnotationStyle>,
844 run: &FinalizedRun,
845 ) -> String
846 where
847 F: OutputFormat<Output = String>,
848 {
849 if let Some(groups) = self.effective_custom_groups() {
850 let id_stubs = self.sorted_id_stubs();
851 let selected = if restrict_to_cited {
852 let cited = &run.state().cited_ids;
853 id_stubs
854 .iter()
855 .filter(|e| cited.contains(&e.id))
856 .map(|e| e.id.clone())
857 .collect::<HashSet<_>>()
858 } else {
859 id_stubs
860 .iter()
861 .map(|e| e.id.clone())
862 .collect::<HashSet<_>>()
863 };
864 return self.render_with_custom_groups_filtered::<F>(
865 &id_stubs,
866 groups,
867 &selected,
868 annotations,
869 annotation_style,
870 run,
871 );
872 }
873
874 let bibliography_options = self.get_bibliography_options();
875 let partitioning = bibliography_options
876 .sort_partitioning
877 .as_ref()
878 .filter(|partitioning| crate::sort_partitioning::should_render_sections(partitioning));
879
880 if !run.state().compound_groups.is_empty() {
881 if let Some(partitioning) = partitioning {
882 let sorted_refs = self.sorted_eligible_refs(restrict_to_cited, run);
883 return self.render_with_partition_sections::<F>(
884 sorted_refs,
885 partitioning,
886 annotations,
887 annotation_style,
888 run,
889 );
890 }
891
892 let all_entries = self.process_references_with_format::<F>(run).bibliography;
893 let merged = self.merge_compound_entries::<F>(all_entries, run);
894 return self.render_flat_compound_entries::<F>(
895 &merged,
896 restrict_to_cited,
897 annotations,
898 annotation_style,
899 run,
900 );
901 }
902
903 self.render_flat_bibliography::<F>(
904 restrict_to_cited,
905 partitioning,
906 FlatBibliographyOutput::ContentOnly,
907 annotations,
908 annotation_style,
909 run,
910 )
911 .content
912 }
913
914 fn entries_for_bibliography_group<F>(
925 &self,
926 spine: &[ProcEntry],
927 group: &BibliographyGroup,
928 assigned: &mut HashSet<String>,
929 run: &FinalizedRun,
930 ) -> Vec<crate::render::ProcEntry>
931 where
932 F: OutputFormat<Output = String>,
933 {
934 let cited_ids = &run.state().cited_ids;
935 let evaluator = SelectorEvaluator::new(cited_ids);
936 let bibliography_config = self.get_bibliography_config();
937 let sorter = ReferenceSorter::with_bibliography_config(&self.locale, &bibliography_config);
938
939 let matching_refs = self.collect_matching_group_refs(spine, assigned, &evaluator, group);
940 Self::mark_group_members_assigned(assigned, &matching_refs);
941
942 if matching_refs.is_empty() {
943 return Vec::new();
944 }
945
946 let sorted_refs = if let Some(sort_spec) = &group.sort {
947 sorter.sort_references(matching_refs, &sort_spec.resolve())
948 } else {
949 matching_refs
950 };
951
952 let local_hints = self.build_group_local_hints(&sorted_refs, group);
953 self.merge_compound_entries::<F>(
954 self.render_group_entries::<F>(spine, sorted_refs, group, local_hints.as_ref(), run),
955 run,
956 )
957 }
958
959 #[allow(
971 clippy::too_many_arguments,
972 reason = "internal helper, all params load-bearing"
973 )]
974 pub(crate) fn render_document_bibliography_block<F>(
975 &self,
976 spine: &[ProcEntry],
977 group: &BibliographyGroup,
978 assigned: &mut HashSet<String>,
979 annotations: Option<&HashMap<String, String>>,
980 annotation_style: Option<&AnnotationStyle>,
981 run: &FinalizedRun,
982 ) -> RenderedBibliographyGroup
983 where
984 F: OutputFormat<Output = String>,
985 {
986 let mut headingless = group.clone();
987 let heading = headingless
988 .heading
989 .take()
990 .and_then(|group_heading| self.resolve_group_heading(&group_heading));
991
992 let entries = self.entries_for_bibliography_group::<F>(spine, &headingless, assigned, run);
993 let body = crate::render::refs_to_string_slice_with_format::<F>(
994 &entries,
995 annotations,
996 annotation_style,
997 );
998
999 RenderedBibliographyGroup {
1000 heading,
1001 body,
1002 entries,
1003 }
1004 }
1005
1006 pub(crate) fn render_document_bibliography_blocks<F>(
1015 &self,
1016 groups: &[BibliographyGroup],
1017 annotations: Option<&HashMap<String, String>>,
1018 annotation_style: Option<&AnnotationStyle>,
1019 run: &FinalizedRun,
1020 ) -> Vec<RenderedBibliographyGroup>
1021 where
1022 F: OutputFormat<Output = String>,
1023 {
1024 let spine = self.sorted_id_stubs();
1025 let mut assigned = std::collections::HashSet::new();
1026 groups
1027 .iter()
1028 .map(|group| {
1029 self.render_document_bibliography_block::<F>(
1030 &spine,
1031 group,
1032 &mut assigned,
1033 annotations,
1034 annotation_style,
1035 run,
1036 )
1037 })
1038 .collect()
1039 }
1040
1041 pub(super) fn extract_metadata(
1042 &self,
1043 reference: &Reference,
1044 ctx: &EntryRenderContext<'_>,
1045 ) -> ProcEntryMetadata {
1046 let bibliography_config = &ctx.config;
1047 let options = RenderOptions {
1048 config: bibliography_config.clone(),
1049 bibliography_config: Some(ctx.bibliography_config.clone()),
1050 locale: &self.locale,
1051 context: RenderContext::Bibliography,
1052 mode: citum_schema::citation::CitationMode::NonIntegral,
1053 suppress_author: false,
1054 locator_raw: None,
1055 ref_type: None,
1056 show_semantics: self.show_semantics,
1057 current_template_index: None,
1058 abbreviation_map: self.abbreviation_map.as_ref(),
1059 };
1060
1061 let ml = bibliography_config.multilingual.as_ref();
1062 let preferred_transliteration = ml.and_then(|m| m.preferred_transliteration.as_deref());
1063 let preferred_script = ml.and_then(|m| m.preferred_script.as_ref());
1064
1065 ProcEntryMetadata {
1066 author: reference.author().map(|author| {
1067 let names = resolve_multilingual_name(
1068 &author,
1069 ml.and_then(|m| m.name_mode.as_ref()),
1070 preferred_transliteration,
1071 preferred_script,
1072 &self.locale.locale,
1073 );
1074 format_contributors_short(&names, &options)
1075 }),
1076 year: reference
1077 .effective_issued_date()
1078 .map(|issued| issued.year().clone()),
1079 title: reference.title().map(|title| {
1080 use citum_schema::reference::types::{MultilingualString, Title};
1081 match &title {
1082 Title::Multilingual(m) => resolve_multilingual_string(
1083 &MultilingualString::Complex(m.clone()),
1084 ml.and_then(|ml| ml.title_mode.as_ref()),
1085 preferred_transliteration,
1086 preferred_script,
1087 &self.locale.locale,
1088 ),
1089 _ => title.to_string(),
1090 }
1091 }),
1092 }
1093 }
1094
1095 fn render_group_heading<F>(&self, heading: &str) -> String
1096 where
1097 F: OutputFormat<Output = String>,
1098 {
1099 let fmt = F::default();
1100 fmt.finish(fmt.unnumbered_heading(2, fmt.text(heading)))
1101 }
1102}