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