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 mut 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 if let Some(spec) = self.style.citation.as_ref() {
205 disambiguator = disambiguator.with_citation_spec(spec);
206 }
207 if let Some(spec) = self.style.bibliography.as_ref() {
208 disambiguator = disambiguator.with_bibliography_spec(spec);
209 }
210
211 Some(disambiguator.calculate_hints())
212 }
213
214 fn effective_group_style<'a>(
216 &'a self,
217 group: &'a BibliographyGroup,
218 ) -> Cow<'a, citum_schema::Style> {
219 if let Some(group_template) = &group.template {
220 let mut local_style = self.style.clone();
221 if let Some(bibliography) = local_style.bibliography.as_mut() {
222 bibliography.template = Some(group_template.clone());
223 }
224 Cow::Owned(local_style)
225 } else {
226 Cow::Borrowed(&self.style)
227 }
228 }
229
230 fn render_group_entries<F>(
241 &self,
242 _bibliography: &[ProcEntry],
243 sorted_refs: Vec<&Reference>,
244 group: &BibliographyGroup,
245 local_hints: Option<&HashMap<String, ProcHints>>,
246 run: &FinalizedRun,
247 ) -> Vec<ProcEntry>
248 where
249 F: OutputFormat<Output = String>,
250 {
251 let effective_style = self.effective_group_style(group);
254 let ctx = EntryRenderContext {
255 style: &effective_style,
256 hints: local_hints.unwrap_or(&self.hints),
257 config: Arc::new(self.get_bibliography_config().into_owned()),
258 bibliography_config: Arc::new(self.get_bibliography_options().into_owned()),
259 run,
260 };
261
262 let numbered_refs = super::number_sorted_refs(sorted_refs.into_iter(), run);
263 let rendered = self.render_numbered_refs::<F>(&numbered_refs, &ctx);
264
265 let substitute = ctx
266 .bibliography_config
267 .subsequent_author_substitute
268 .as_ref();
269 self.apply_substitution_post_pass::<F>(rendered, substitute, &ctx)
270 }
271
272 fn append_rendered_group<F>(
274 &self,
275 result: &mut String,
276 group: &BibliographyGroup,
277 entries: Vec<ProcEntry>,
278 annotations: Option<&HashMap<String, String>>,
279 annotation_style: Option<&AnnotationStyle>,
280 suppress_heading: bool,
281 ) where
282 F: OutputFormat<Output = String>,
283 {
284 if !result.is_empty() {
285 result.push_str("\n\n");
286 }
287
288 if !suppress_heading
289 && let Some(heading) = group
290 .heading
291 .as_ref()
292 .and_then(|group_heading| self.resolve_group_heading(group_heading))
293 {
294 result.push_str(&self.render_group_heading::<F>(&heading));
295 }
296
297 result.push_str(&crate::render::refs_to_string_with_format::<F>(
298 entries,
299 annotations,
300 annotation_style,
301 ));
302 }
303
304 fn append_rendered_partition<F>(
306 &self,
307 result: &mut String,
308 heading: Option<&BibliographyPartitionHeading>,
309 entries: Vec<ProcEntry>,
310 annotations: Option<&HashMap<String, String>>,
311 annotation_style: Option<&AnnotationStyle>,
312 ) where
313 F: OutputFormat<Output = String>,
314 {
315 if !result.is_empty() {
316 result.push_str("\n\n");
317 }
318
319 if let Some(heading) =
320 heading.and_then(|group_heading| self.resolve_partition_heading(group_heading))
321 {
322 result.push_str(&self.render_group_heading::<F>(&heading));
323 }
324
325 result.push_str(&crate::render::refs_to_string_with_format::<F>(
326 entries,
327 annotations,
328 annotation_style,
329 ));
330 }
331
332 pub(super) fn render_with_partition_sections<F>(
334 &self,
335 sorted_refs: Vec<&Reference>,
336 partitioning: &BibliographySortPartitioning,
337 annotations: Option<&HashMap<String, String>>,
338 annotation_style: Option<&AnnotationStyle>,
339 run: &FinalizedRun,
340 ) -> String
341 where
342 F: OutputFormat<Output = String>,
343 {
344 let fmt = F::default();
345 let mut result = String::new();
346
347 for (partition_key, references) in
348 crate::sort_partitioning::partition_references(sorted_refs, &self.locale, partitioning)
349 {
350 let heading = partition_key
351 .as_ref()
352 .and_then(|key| partitioning.headings.get(key));
353 let entries = self.merge_compound_entries::<F>(
354 self.process_sorted_refs::<_, F>(references.into_iter(), run),
355 run,
356 );
357 self.append_rendered_partition::<F>(
358 &mut result,
359 heading,
360 entries,
361 annotations,
362 annotation_style,
363 );
364 }
365
366 fmt.finish(result)
367 }
368
369 pub(super) fn render_with_custom_groups_filtered<F>(
376 &self,
377 all_entries: &[ProcEntry],
378 groups: &[BibliographyGroup],
379 selected: &HashSet<String>,
380 annotations: Option<&HashMap<String, String>>,
381 annotation_style: Option<&AnnotationStyle>,
382 run: &FinalizedRun,
383 ) -> String
384 where
385 F: OutputFormat<Output = String>,
386 {
387 let fmt = F::default();
388 let cited_ids = &run.state().cited_ids;
389 let evaluator = SelectorEvaluator::new(cited_ids);
390 let bibliography_config = self.get_bibliography_config();
391 let mut sorter =
392 ReferenceSorter::with_bibliography_config(&self.locale, &bibliography_config);
393 if let Some(spec) = self.style.bibliography.as_ref() {
394 sorter = sorter.with_bibliography_spec(spec);
395 }
396
397 let mut assigned = HashSet::new();
398 let mut result = String::new();
399
400 let mut populated_groups: Vec<(&BibliographyGroup, Vec<ProcEntry>)> = Vec::new();
402
403 for group in groups {
404 let matching_refs =
405 self.collect_matching_group_refs(all_entries, &assigned, &evaluator, group);
406
407 let matching_refs: Vec<&Reference> = matching_refs
408 .into_iter()
409 .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
410 .collect();
411
412 if matching_refs.is_empty() {
413 continue;
414 }
415
416 Self::mark_group_members_assigned(&mut assigned, &matching_refs);
417
418 let sorted_refs = if let Some(sort_spec) = &group.sort {
419 sorter.sort_references(matching_refs, &sort_spec.resolve())
420 } else {
421 matching_refs
422 };
423 let local_hints = self.build_group_local_hints(&sorted_refs, group);
424 let entries = self.merge_compound_entries::<F>(
425 self.render_group_entries::<F>(
426 all_entries,
427 sorted_refs,
428 group,
429 local_hints.as_ref(),
430 run,
431 ),
432 run,
433 );
434
435 populated_groups.push((group, entries));
436 }
437
438 let unassigned_refs: Vec<&Reference> = all_entries
440 .iter()
441 .filter(|entry| !assigned.contains(&entry.id) && selected.contains(&entry.id))
442 .filter_map(|entry| self.bibliography.get(&entry.id))
443 .collect();
444
445 let suppress_heading = populated_groups.len() == 1 && unassigned_refs.is_empty();
446
447 for (group, entries) in populated_groups {
449 self.append_rendered_group::<F>(
450 &mut result,
451 group,
452 entries,
453 annotations,
454 annotation_style,
455 suppress_heading,
456 );
457 }
458
459 self.append_unassigned_entries_filtered::<F>(
460 &mut result,
461 all_entries,
462 &assigned,
463 selected,
464 annotations,
465 annotation_style,
466 run,
467 );
468 fmt.finish(result)
469 }
470
471 #[allow(
473 clippy::too_many_arguments,
474 reason = "internal helper, all params load-bearing"
475 )]
476 fn append_unassigned_entries_filtered<F>(
477 &self,
478 result: &mut String,
479 bibliography: &[ProcEntry],
480 assigned: &HashSet<String>,
481 selected: &HashSet<String>,
482 annotations: Option<&HashMap<String, String>>,
483 annotation_style: Option<&AnnotationStyle>,
484 run: &FinalizedRun,
485 ) where
486 F: OutputFormat<Output = String>,
487 {
488 let unassigned_refs: Vec<&Reference> = bibliography
489 .iter()
490 .filter(|entry| !assigned.contains(&entry.id) && selected.contains(&entry.id))
491 .filter_map(|entry| self.bibliography.get(&entry.id))
492 .collect();
493
494 if unassigned_refs.is_empty() {
495 return;
496 }
497
498 let unassigned = self.merge_compound_entries::<F>(
501 self.process_sorted_refs::<_, F>(unassigned_refs.into_iter(), run),
502 run,
503 );
504
505 if !result.is_empty() {
506 result.push_str("\n\n");
507 }
508
509 result.push_str(&crate::render::refs_to_string_with_format::<F>(
510 unassigned,
511 annotations,
512 annotation_style,
513 ));
514 }
515
516 fn render_flat_compound_entries<F>(
526 &self,
527 bibliography: &[ProcEntry],
528 restrict_to_cited: bool,
529 annotations: Option<&HashMap<String, String>>,
530 annotation_style: Option<&AnnotationStyle>,
531 run: &FinalizedRun,
532 ) -> String
533 where
534 F: OutputFormat<Output = String>,
535 {
536 let fmt = F::default();
537 let selected = if restrict_to_cited {
538 let cited_ids = &run.state().cited_ids;
539 let compound_groups = &run.state().compound_groups;
540 let ref_to_group = Self::build_compound_group_lookup(compound_groups);
541 let row_is_cited = |id: &str| match ref_to_group.get(id) {
542 Some(group_number) => compound_groups
543 .get(group_number)
544 .is_some_and(|members| members.iter().any(|member| cited_ids.contains(member))),
545 None => cited_ids.contains(id),
546 };
547 Cow::Owned(
548 bibliography
549 .iter()
550 .filter(|entry| row_is_cited(&entry.id))
551 .cloned()
552 .collect(),
553 )
554 } else {
555 Cow::Borrowed(bibliography)
556 };
557
558 let result = if selected.is_empty() {
559 String::new()
560 } else {
561 crate::render::refs_to_string_slice_with_format::<F>(
562 selected.as_ref(),
563 annotations,
564 annotation_style,
565 )
566 };
567
568 fmt.finish(result)
569 }
570
571 pub fn render_grouped_bibliography_with_format<F>(&self, run: &FinalizedRun) -> String
578 where
579 F: OutputFormat<Output = String>,
580 {
581 self.render_grouped_bibliography_with_format_and_annotations::<F>(None, None, run)
582 }
583
584 pub fn render_grouped_bibliography_with_format_and_annotations<F>(
586 &self,
587 annotations: Option<&HashMap<String, String>>,
588 annotation_style: Option<&AnnotationStyle>,
589 run: &FinalizedRun,
590 ) -> String
591 where
592 F: OutputFormat<Output = String>,
593 {
594 self.render_grouped_bibliography_inner::<F>(false, annotations, annotation_style, run)
595 }
596
597 pub fn render_grouped_bibliography_with_format_standalone<F>(&self) -> String
600 where
601 F: OutputFormat<Output = String>,
602 {
603 let run = self.begin_run().finalize();
604 self.render_grouped_bibliography_with_format::<F>(&run)
605 }
606
607 pub fn render_grouped_bibliography_with_format_and_annotations_standalone<F>(
611 &self,
612 annotations: Option<&HashMap<String, String>>,
613 annotation_style: Option<&AnnotationStyle>,
614 ) -> String
615 where
616 F: OutputFormat<Output = String>,
617 {
618 let run = self.begin_run().finalize();
619 self.render_grouped_bibliography_with_format_and_annotations::<F>(
620 annotations,
621 annotation_style,
622 &run,
623 )
624 }
625
626 pub(crate) fn render_document_bibliography<F>(
651 &self,
652 restrict_to_cited: bool,
653 annotations: Option<&HashMap<String, String>>,
654 annotation_style: Option<&AnnotationStyle>,
655 run: &FinalizedRun,
656 ) -> super::DocumentBibliography
657 where
658 F: OutputFormat<Output = String>,
659 {
660 let has_custom_groups = self.effective_custom_groups().is_some();
661
662 if has_custom_groups || !run.state().compound_groups.is_empty() {
663 let content = self.render_grouped_bibliography_inner::<F>(
664 restrict_to_cited,
665 annotations,
666 annotation_style,
667 run,
668 );
669 let cited_ids: Vec<String> = run.state().cited_ids.iter().cloned().collect();
670 let entries = if restrict_to_cited {
671 self.process_selected_references_with_format::<F, _>(cited_ids, run)
672 .bibliography
673 } else {
674 self.process_references_with_format::<F>(run).bibliography
675 };
676 return super::DocumentBibliography { content, entries };
677 }
678
679 let bibliography_options = self.get_bibliography_options();
680 let partitioning = bibliography_options
681 .sort_partitioning
682 .as_ref()
683 .filter(|partitioning| crate::sort_partitioning::should_render_sections(partitioning));
684
685 self.render_flat_bibliography::<F>(
686 restrict_to_cited,
687 partitioning,
688 FlatBibliographyOutput::ContentAndEntries,
689 annotations,
690 annotation_style,
691 run,
692 )
693 }
694
695 fn sorted_eligible_refs(&self, restrict_to_cited: bool, run: &FinalizedRun) -> Vec<&Reference> {
698 let mut refs: Vec<&Reference> = self.bibliography.values().collect();
699 if restrict_to_cited {
700 let cited = &run.state().cited_ids;
701 refs.retain(|reference| {
702 reference
703 .id()
704 .as_deref()
705 .is_some_and(|id| cited.contains(id))
706 });
707 }
708 self.sort_references(refs)
709 }
710
711 fn render_flat_bibliography<F>(
743 &self,
744 restrict_to_cited: bool,
745 partitioning: Option<&BibliographySortPartitioning>,
746 output: FlatBibliographyOutput,
747 annotations: Option<&HashMap<String, String>>,
748 annotation_style: Option<&AnnotationStyle>,
749 run: &FinalizedRun,
750 ) -> super::DocumentBibliography
751 where
752 F: OutputFormat<Output = String>,
753 {
754 let sorted_refs = self.sorted_eligible_refs(restrict_to_cited, run);
755
756 if output == FlatBibliographyOutput::ContentOnly
757 && let Some(partitioning) = partitioning
758 {
759 let content = self.render_with_partition_sections::<F>(
760 sorted_refs,
761 partitioning,
762 annotations,
763 annotation_style,
764 run,
765 );
766 return super::DocumentBibliography {
767 content,
768 entries: Vec::new(),
769 };
770 }
771
772 let ctx = self.flat_render_context(run);
773 let numbered_refs = super::number_sorted_refs(sorted_refs.iter().copied(), run);
774 let rendered = self.render_numbered_refs::<F>(&numbered_refs, &ctx);
775
776 let substitute = ctx
777 .bibliography_config
778 .subsequent_author_substitute
779 .as_ref();
780
781 if let Some(partitioning) = partitioning {
782 let entries =
786 self.apply_substitution_post_pass::<F>(rendered.clone(), substitute, &ctx);
787 let mut result = String::new();
788 for (partition_key, refs_in_section) in crate::sort_partitioning::partition_references(
789 sorted_refs,
790 &self.locale,
791 partitioning,
792 ) {
793 let heading = partition_key
794 .as_ref()
795 .and_then(|key| partitioning.headings.get(key));
796 let section_ids: HashSet<String> = refs_in_section
797 .iter()
798 .filter_map(|reference| reference.id().map(|id| id.to_string()))
799 .collect();
800 let section_rendered = rendered
801 .iter()
802 .filter(|(reference, _)| {
803 reference
804 .id()
805 .as_deref()
806 .is_some_and(|id| section_ids.contains(id))
807 })
808 .cloned()
809 .collect();
810 let section_entries =
811 self.apply_substitution_post_pass::<F>(section_rendered, substitute, &ctx);
812 self.append_rendered_partition::<F>(
813 &mut result,
814 heading,
815 section_entries,
816 annotations,
817 annotation_style,
818 );
819 }
820 return super::DocumentBibliography {
821 content: F::default().finish(result),
822 entries,
823 };
824 }
825
826 let rendered_entries = self.apply_substitution_post_pass::<F>(rendered, substitute, &ctx);
827 let content = if rendered_entries.is_empty() {
828 String::new()
829 } else {
830 crate::render::refs_to_string_slice_with_format::<F>(
831 &rendered_entries,
832 annotations,
833 annotation_style,
834 )
835 };
836 let entries = match output {
837 FlatBibliographyOutput::ContentAndEntries => rendered_entries,
838 FlatBibliographyOutput::ContentOnly => Vec::new(),
839 };
840
841 super::DocumentBibliography { content, entries }
842 }
843
844 fn render_grouped_bibliography_inner<F>(
851 &self,
852 restrict_to_cited: bool,
853 annotations: Option<&HashMap<String, String>>,
854 annotation_style: Option<&AnnotationStyle>,
855 run: &FinalizedRun,
856 ) -> String
857 where
858 F: OutputFormat<Output = String>,
859 {
860 if let Some(groups) = self.effective_custom_groups() {
861 let id_stubs = self.sorted_id_stubs();
862 let selected = if restrict_to_cited {
863 let cited = &run.state().cited_ids;
864 id_stubs
865 .iter()
866 .filter(|e| cited.contains(&e.id))
867 .map(|e| e.id.clone())
868 .collect::<HashSet<_>>()
869 } else {
870 id_stubs
871 .iter()
872 .map(|e| e.id.clone())
873 .collect::<HashSet<_>>()
874 };
875 return self.render_with_custom_groups_filtered::<F>(
876 &id_stubs,
877 groups,
878 &selected,
879 annotations,
880 annotation_style,
881 run,
882 );
883 }
884
885 let bibliography_options = self.get_bibliography_options();
886 let partitioning = bibliography_options
887 .sort_partitioning
888 .as_ref()
889 .filter(|partitioning| crate::sort_partitioning::should_render_sections(partitioning));
890
891 if !run.state().compound_groups.is_empty() {
892 if let Some(partitioning) = partitioning {
893 let sorted_refs = self.sorted_eligible_refs(restrict_to_cited, run);
894 return self.render_with_partition_sections::<F>(
895 sorted_refs,
896 partitioning,
897 annotations,
898 annotation_style,
899 run,
900 );
901 }
902
903 let all_entries = self.process_references_with_format::<F>(run).bibliography;
904 let merged = self.merge_compound_entries::<F>(all_entries, run);
905 return self.render_flat_compound_entries::<F>(
906 &merged,
907 restrict_to_cited,
908 annotations,
909 annotation_style,
910 run,
911 );
912 }
913
914 self.render_flat_bibliography::<F>(
915 restrict_to_cited,
916 partitioning,
917 FlatBibliographyOutput::ContentOnly,
918 annotations,
919 annotation_style,
920 run,
921 )
922 .content
923 }
924
925 fn entries_for_bibliography_group<F>(
936 &self,
937 spine: &[ProcEntry],
938 group: &BibliographyGroup,
939 assigned: &mut HashSet<String>,
940 run: &FinalizedRun,
941 ) -> Vec<crate::render::ProcEntry>
942 where
943 F: OutputFormat<Output = String>,
944 {
945 let cited_ids = &run.state().cited_ids;
946 let evaluator = SelectorEvaluator::new(cited_ids);
947 let bibliography_config = self.get_bibliography_config();
948 let mut sorter =
949 ReferenceSorter::with_bibliography_config(&self.locale, &bibliography_config);
950 if let Some(spec) = self.style.bibliography.as_ref() {
951 sorter = sorter.with_bibliography_spec(spec);
952 }
953
954 let matching_refs = self.collect_matching_group_refs(spine, assigned, &evaluator, group);
955 Self::mark_group_members_assigned(assigned, &matching_refs);
956
957 if matching_refs.is_empty() {
958 return Vec::new();
959 }
960
961 let sorted_refs = if let Some(sort_spec) = &group.sort {
962 sorter.sort_references(matching_refs, &sort_spec.resolve())
963 } else {
964 matching_refs
965 };
966
967 let local_hints = self.build_group_local_hints(&sorted_refs, group);
968 self.merge_compound_entries::<F>(
969 self.render_group_entries::<F>(spine, sorted_refs, group, local_hints.as_ref(), run),
970 run,
971 )
972 }
973
974 #[allow(
986 clippy::too_many_arguments,
987 reason = "internal helper, all params load-bearing"
988 )]
989 pub(crate) fn render_document_bibliography_block<F>(
990 &self,
991 spine: &[ProcEntry],
992 group: &BibliographyGroup,
993 assigned: &mut HashSet<String>,
994 annotations: Option<&HashMap<String, String>>,
995 annotation_style: Option<&AnnotationStyle>,
996 run: &FinalizedRun,
997 ) -> RenderedBibliographyGroup
998 where
999 F: OutputFormat<Output = String>,
1000 {
1001 let mut headingless = group.clone();
1002 let heading = headingless
1003 .heading
1004 .take()
1005 .and_then(|group_heading| self.resolve_group_heading(&group_heading));
1006
1007 let entries = self.entries_for_bibliography_group::<F>(spine, &headingless, assigned, run);
1008 let body = crate::render::refs_to_string_slice_with_format::<F>(
1009 &entries,
1010 annotations,
1011 annotation_style,
1012 );
1013
1014 RenderedBibliographyGroup {
1015 heading,
1016 body,
1017 entries,
1018 }
1019 }
1020
1021 pub(crate) fn render_document_bibliography_blocks<F>(
1030 &self,
1031 groups: &[BibliographyGroup],
1032 annotations: Option<&HashMap<String, String>>,
1033 annotation_style: Option<&AnnotationStyle>,
1034 run: &FinalizedRun,
1035 ) -> Vec<RenderedBibliographyGroup>
1036 where
1037 F: OutputFormat<Output = String>,
1038 {
1039 let spine = self.sorted_id_stubs();
1040 let mut assigned = std::collections::HashSet::new();
1041 groups
1042 .iter()
1043 .map(|group| {
1044 self.render_document_bibliography_block::<F>(
1045 &spine,
1046 group,
1047 &mut assigned,
1048 annotations,
1049 annotation_style,
1050 run,
1051 )
1052 })
1053 .collect()
1054 }
1055
1056 pub(super) fn extract_metadata(
1057 &self,
1058 reference: &Reference,
1059 ctx: &EntryRenderContext<'_>,
1060 ) -> ProcEntryMetadata {
1061 let bibliography_config = &ctx.config;
1062 let options = RenderOptions {
1063 config: bibliography_config.clone(),
1064 bibliography_config: Some(ctx.bibliography_config.clone()),
1065 locale: &self.locale,
1066 context: RenderContext::Bibliography,
1067 mode: citum_schema::citation::CitationMode::NonIntegral,
1068 suppress_author: false,
1069 locator_raw: None,
1070 ref_type: None,
1071 show_semantics: self.show_semantics,
1072 current_template_index: None,
1073 abbreviation_map: self.abbreviation_map.as_ref(),
1074 };
1075
1076 let ml = bibliography_config.multilingual.as_ref();
1077 let preferred_transliteration = ml.and_then(|m| m.preferred_transliteration.as_deref());
1078 let preferred_script = ml.and_then(|m| m.preferred_script.as_ref());
1079
1080 ProcEntryMetadata {
1081 author: reference.author().map(|author| {
1082 let names = resolve_multilingual_name(
1083 &author,
1084 ml.and_then(|m| m.name_mode.as_ref()),
1085 preferred_transliteration,
1086 preferred_script,
1087 &self.locale.locale,
1088 );
1089 format_contributors_short(&names, &options)
1090 }),
1091 year: reference
1092 .effective_issued_date()
1093 .map(|issued| issued.year().clone()),
1094 title: reference.title().map(|title| {
1095 use citum_schema::reference::types::{MultilingualString, Title};
1096 match &title {
1097 Title::Multilingual(m) => resolve_multilingual_string(
1098 &MultilingualString::Complex(m.clone()),
1099 ml.and_then(|ml| ml.title_mode.as_ref()),
1100 preferred_transliteration,
1101 preferred_script,
1102 &self.locale.locale,
1103 ),
1104 _ => title.to_string(),
1105 }
1106 }),
1107 }
1108 }
1109
1110 fn render_group_heading<F>(&self, heading: &str) -> String
1111 where
1112 F: OutputFormat<Output = String>,
1113 {
1114 let fmt = F::default();
1115 fmt.finish(fmt.unnumbered_heading(2, fmt.text(heading)))
1116 }
1117}