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 if self.style.bibliography.is_none() {
595 return String::new();
596 }
597 self.render_grouped_bibliography_inner::<F>(false, annotations, annotation_style, run)
598 }
599
600 pub fn render_grouped_bibliography_with_format_standalone<F>(&self) -> String
603 where
604 F: OutputFormat<Output = String>,
605 {
606 let run = self.begin_run().finalize();
607 self.render_grouped_bibliography_with_format::<F>(&run)
608 }
609
610 pub fn render_grouped_bibliography_with_format_and_annotations_standalone<F>(
614 &self,
615 annotations: Option<&HashMap<String, String>>,
616 annotation_style: Option<&AnnotationStyle>,
617 ) -> String
618 where
619 F: OutputFormat<Output = String>,
620 {
621 let run = self.begin_run().finalize();
622 self.render_grouped_bibliography_with_format_and_annotations::<F>(
623 annotations,
624 annotation_style,
625 &run,
626 )
627 }
628
629 pub(crate) fn render_document_bibliography<F>(
654 &self,
655 restrict_to_cited: bool,
656 annotations: Option<&HashMap<String, String>>,
657 annotation_style: Option<&AnnotationStyle>,
658 run: &FinalizedRun,
659 ) -> super::DocumentBibliography
660 where
661 F: OutputFormat<Output = String>,
662 {
663 if self.style.bibliography.is_none() {
664 return super::DocumentBibliography::default();
665 }
666 let has_custom_groups = self.effective_custom_groups().is_some();
667
668 if has_custom_groups || !run.state().compound_groups.is_empty() {
669 let content = self.render_grouped_bibliography_inner::<F>(
670 restrict_to_cited,
671 annotations,
672 annotation_style,
673 run,
674 );
675 let cited_ids: Vec<String> = run.state().cited_ids.iter().cloned().collect();
676 let entries = if restrict_to_cited {
677 self.process_selected_references_with_format::<F, _>(cited_ids, run)
678 .bibliography
679 } else {
680 self.process_references_with_format::<F>(run).bibliography
681 };
682 return super::DocumentBibliography { content, entries };
683 }
684
685 let bibliography_options = self.get_bibliography_options();
686 let partitioning = bibliography_options
687 .sort_partitioning
688 .as_ref()
689 .filter(|partitioning| crate::sort_partitioning::should_render_sections(partitioning));
690
691 self.render_flat_bibliography::<F>(
692 restrict_to_cited,
693 partitioning,
694 FlatBibliographyOutput::ContentAndEntries,
695 annotations,
696 annotation_style,
697 run,
698 )
699 }
700
701 fn sorted_eligible_refs(&self, restrict_to_cited: bool, run: &FinalizedRun) -> Vec<&Reference> {
704 let mut refs: Vec<&Reference> = self.bibliography.values().collect();
705 if restrict_to_cited {
706 let cited = &run.state().cited_ids;
707 refs.retain(|reference| {
708 reference
709 .id()
710 .as_deref()
711 .is_some_and(|id| cited.contains(id))
712 });
713 }
714 self.sort_references(refs)
715 }
716
717 fn render_flat_bibliography<F>(
749 &self,
750 restrict_to_cited: bool,
751 partitioning: Option<&BibliographySortPartitioning>,
752 output: FlatBibliographyOutput,
753 annotations: Option<&HashMap<String, String>>,
754 annotation_style: Option<&AnnotationStyle>,
755 run: &FinalizedRun,
756 ) -> super::DocumentBibliography
757 where
758 F: OutputFormat<Output = String>,
759 {
760 let sorted_refs = self.sorted_eligible_refs(restrict_to_cited, run);
761
762 if output == FlatBibliographyOutput::ContentOnly
763 && let Some(partitioning) = partitioning
764 {
765 let content = self.render_with_partition_sections::<F>(
766 sorted_refs,
767 partitioning,
768 annotations,
769 annotation_style,
770 run,
771 );
772 return super::DocumentBibliography {
773 content,
774 entries: Vec::new(),
775 };
776 }
777
778 let ctx = self.flat_render_context(run);
779 let numbered_refs = super::number_sorted_refs(sorted_refs.iter().copied(), run);
780 let rendered = self.render_numbered_refs::<F>(&numbered_refs, &ctx);
781
782 let substitute = ctx
783 .bibliography_config
784 .subsequent_author_substitute
785 .as_ref();
786
787 if let Some(partitioning) = partitioning {
788 let entries =
792 self.apply_substitution_post_pass::<F>(rendered.clone(), substitute, &ctx);
793 let mut result = String::new();
794 for (partition_key, refs_in_section) in crate::sort_partitioning::partition_references(
795 sorted_refs,
796 &self.locale,
797 partitioning,
798 ) {
799 let heading = partition_key
800 .as_ref()
801 .and_then(|key| partitioning.headings.get(key));
802 let section_ids: HashSet<String> = refs_in_section
803 .iter()
804 .filter_map(|reference| reference.id().map(|id| id.to_string()))
805 .collect();
806 let section_rendered = rendered
807 .iter()
808 .filter(|(reference, _)| {
809 reference
810 .id()
811 .as_deref()
812 .is_some_and(|id| section_ids.contains(id))
813 })
814 .cloned()
815 .collect();
816 let section_entries =
817 self.apply_substitution_post_pass::<F>(section_rendered, substitute, &ctx);
818 self.append_rendered_partition::<F>(
819 &mut result,
820 heading,
821 section_entries,
822 annotations,
823 annotation_style,
824 );
825 }
826 return super::DocumentBibliography {
827 content: F::default().finish(result),
828 entries,
829 };
830 }
831
832 let rendered_entries = self.apply_substitution_post_pass::<F>(rendered, substitute, &ctx);
833 let content = if rendered_entries.is_empty() {
834 String::new()
835 } else {
836 crate::render::refs_to_string_slice_with_format::<F>(
837 &rendered_entries,
838 annotations,
839 annotation_style,
840 )
841 };
842 let entries = match output {
843 FlatBibliographyOutput::ContentAndEntries => rendered_entries,
844 FlatBibliographyOutput::ContentOnly => Vec::new(),
845 };
846
847 super::DocumentBibliography { content, entries }
848 }
849
850 fn render_grouped_bibliography_inner<F>(
857 &self,
858 restrict_to_cited: bool,
859 annotations: Option<&HashMap<String, String>>,
860 annotation_style: Option<&AnnotationStyle>,
861 run: &FinalizedRun,
862 ) -> String
863 where
864 F: OutputFormat<Output = String>,
865 {
866 if let Some(groups) = self.effective_custom_groups() {
867 let id_stubs = self.sorted_id_stubs();
868 let selected = if restrict_to_cited {
869 let cited = &run.state().cited_ids;
870 id_stubs
871 .iter()
872 .filter(|e| cited.contains(&e.id))
873 .map(|e| e.id.clone())
874 .collect::<HashSet<_>>()
875 } else {
876 id_stubs
877 .iter()
878 .map(|e| e.id.clone())
879 .collect::<HashSet<_>>()
880 };
881 return self.render_with_custom_groups_filtered::<F>(
882 &id_stubs,
883 groups,
884 &selected,
885 annotations,
886 annotation_style,
887 run,
888 );
889 }
890
891 let bibliography_options = self.get_bibliography_options();
892 let partitioning = bibliography_options
893 .sort_partitioning
894 .as_ref()
895 .filter(|partitioning| crate::sort_partitioning::should_render_sections(partitioning));
896
897 if !run.state().compound_groups.is_empty() {
898 if let Some(partitioning) = partitioning {
899 let sorted_refs = self.sorted_eligible_refs(restrict_to_cited, run);
900 return self.render_with_partition_sections::<F>(
901 sorted_refs,
902 partitioning,
903 annotations,
904 annotation_style,
905 run,
906 );
907 }
908
909 let all_entries = self.process_references_with_format::<F>(run).bibliography;
910 let merged = self.merge_compound_entries::<F>(all_entries, run);
911 return self.render_flat_compound_entries::<F>(
912 &merged,
913 restrict_to_cited,
914 annotations,
915 annotation_style,
916 run,
917 );
918 }
919
920 self.render_flat_bibliography::<F>(
921 restrict_to_cited,
922 partitioning,
923 FlatBibliographyOutput::ContentOnly,
924 annotations,
925 annotation_style,
926 run,
927 )
928 .content
929 }
930
931 fn entries_for_bibliography_group<F>(
942 &self,
943 spine: &[ProcEntry],
944 group: &BibliographyGroup,
945 assigned: &mut HashSet<String>,
946 run: &FinalizedRun,
947 ) -> Vec<crate::render::ProcEntry>
948 where
949 F: OutputFormat<Output = String>,
950 {
951 let cited_ids = &run.state().cited_ids;
952 let evaluator = SelectorEvaluator::new(cited_ids);
953 let bibliography_config = self.get_bibliography_config();
954 let mut sorter =
955 ReferenceSorter::with_bibliography_config(&self.locale, &bibliography_config);
956 if let Some(spec) = self.style.bibliography.as_ref() {
957 sorter = sorter.with_bibliography_spec(spec);
958 }
959
960 let matching_refs = self.collect_matching_group_refs(spine, assigned, &evaluator, group);
961 Self::mark_group_members_assigned(assigned, &matching_refs);
962
963 if matching_refs.is_empty() {
964 return Vec::new();
965 }
966
967 let sorted_refs = if let Some(sort_spec) = &group.sort {
968 sorter.sort_references(matching_refs, &sort_spec.resolve())
969 } else {
970 matching_refs
971 };
972
973 let local_hints = self.build_group_local_hints(&sorted_refs, group);
974 self.merge_compound_entries::<F>(
975 self.render_group_entries::<F>(spine, sorted_refs, group, local_hints.as_ref(), run),
976 run,
977 )
978 }
979
980 #[allow(
992 clippy::too_many_arguments,
993 reason = "internal helper, all params load-bearing"
994 )]
995 pub(crate) fn render_document_bibliography_block<F>(
996 &self,
997 spine: &[ProcEntry],
998 group: &BibliographyGroup,
999 assigned: &mut HashSet<String>,
1000 annotations: Option<&HashMap<String, String>>,
1001 annotation_style: Option<&AnnotationStyle>,
1002 run: &FinalizedRun,
1003 ) -> RenderedBibliographyGroup
1004 where
1005 F: OutputFormat<Output = String>,
1006 {
1007 let mut headingless = group.clone();
1008 let heading = headingless
1009 .heading
1010 .take()
1011 .and_then(|group_heading| self.resolve_group_heading(&group_heading));
1012
1013 let entries = self.entries_for_bibliography_group::<F>(spine, &headingless, assigned, run);
1014 let body = crate::render::refs_to_string_slice_with_format::<F>(
1015 &entries,
1016 annotations,
1017 annotation_style,
1018 );
1019
1020 RenderedBibliographyGroup {
1021 heading,
1022 body,
1023 entries,
1024 }
1025 }
1026
1027 pub(crate) fn render_document_bibliography_blocks<F>(
1036 &self,
1037 groups: &[BibliographyGroup],
1038 annotations: Option<&HashMap<String, String>>,
1039 annotation_style: Option<&AnnotationStyle>,
1040 run: &FinalizedRun,
1041 ) -> Vec<RenderedBibliographyGroup>
1042 where
1043 F: OutputFormat<Output = String>,
1044 {
1045 if self.style.bibliography.is_none() {
1046 return Vec::new();
1047 }
1048 let spine = self.sorted_id_stubs();
1049 let mut assigned = std::collections::HashSet::new();
1050 groups
1051 .iter()
1052 .map(|group| {
1053 self.render_document_bibliography_block::<F>(
1054 &spine,
1055 group,
1056 &mut assigned,
1057 annotations,
1058 annotation_style,
1059 run,
1060 )
1061 })
1062 .collect()
1063 }
1064
1065 pub(super) fn extract_metadata(
1066 &self,
1067 reference: &Reference,
1068 ctx: &EntryRenderContext<'_>,
1069 ) -> ProcEntryMetadata {
1070 let bibliography_config = &ctx.config;
1071 let options = RenderOptions {
1072 config: bibliography_config.clone(),
1073 bibliography_config: Some(ctx.bibliography_config.clone()),
1074 locale: &self.locale,
1075 context: RenderContext::Bibliography,
1076 mode: citum_schema::citation::CitationMode::NonIntegral,
1077 suppress_author: false,
1078 locator_raw: None,
1079 ref_type: None,
1080 show_semantics: self.show_semantics,
1081 current_template_index: None,
1082 abbreviation_map: self.abbreviation_map.as_ref(),
1083 };
1084
1085 let ml = bibliography_config.multilingual.as_ref();
1086 let preferred_transliteration = ml.and_then(|m| m.preferred_transliteration.as_deref());
1087 let preferred_script = ml.and_then(|m| m.preferred_script.as_ref());
1088
1089 ProcEntryMetadata {
1090 author: reference.author().map(|author| {
1091 let names = resolve_multilingual_name(
1092 &author,
1093 ml.and_then(|m| m.name_mode.as_ref()),
1094 preferred_transliteration,
1095 preferred_script,
1096 &self.locale.locale,
1097 );
1098 format_contributors_short(&names, &options)
1099 }),
1100 year: reference
1101 .effective_issued_date()
1102 .map(|issued| issued.year().clone()),
1103 title: reference.title().map(|title| {
1104 use citum_schema::reference::types::{MultilingualString, Title};
1105 match &title {
1106 Title::Multilingual(m) => resolve_multilingual_string(
1107 &MultilingualString::Complex(m.clone()),
1108 ml.and_then(|ml| ml.title_mode.as_ref()),
1109 preferred_transliteration,
1110 preferred_script,
1111 &self.locale.locale,
1112 ),
1113 _ => title.to_string(),
1114 }
1115 }),
1116 }
1117 }
1118
1119 fn render_group_heading<F>(&self, heading: &str) -> String
1120 where
1121 F: OutputFormat<Output = String>,
1122 {
1123 let fmt = F::default();
1124 fmt.finish(fmt.unnumbered_heading(2, fmt.text(heading)))
1125 }
1126}