1use super::RenderedBibliographyGroup;
9use crate::api::AnnotationStyle;
10use crate::grouping::SelectorEvaluator;
11use crate::processor::Processor;
12use crate::processor::disambiguation::Disambiguator;
13use crate::processor::rendering::{CompoundRenderData, Renderer, RendererResources};
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::rc::Rc;
27
28impl Processor {
29 pub(super) fn resolve_group_heading(&self, heading: &GroupHeading) -> Option<String> {
31 match heading {
32 GroupHeading::Literal { literal } => Some(literal.clone()),
33 GroupHeading::Term { term, form } => self.locale.resolved_general_term(
34 term,
35 &form.clone().unwrap_or(citum_schema::locale::TermForm::Long),
36 None,
37 ),
38 GroupHeading::Localized { localized } => self.resolve_localized_heading(localized),
39 }
40 }
41
42 fn resolve_localized_heading(&self, localized: &HashMap<String, String>) -> Option<String> {
51 fn language_tag(locale: &str) -> &str {
52 locale.split('-').next().unwrap_or(locale)
53 }
54
55 let mut candidates = Vec::new();
56 let mut push_candidate = |locale: &str| {
57 let candidate = locale.to_string();
58 if !candidates.contains(&candidate) {
59 candidates.push(candidate);
60 }
61 };
62
63 push_candidate(&self.locale.locale);
64 push_candidate(language_tag(&self.locale.locale));
65
66 if let Some(default_locale) = self.style.info.default_locale.as_deref() {
67 push_candidate(default_locale);
68 push_candidate(language_tag(default_locale));
69 }
70
71 push_candidate("en-US");
72 push_candidate("en");
73
74 for locale in candidates {
75 if let Some(value) = localized.get(&locale) {
76 return Some(value.clone());
77 }
78 }
79
80 localized
81 .iter()
82 .min_by(|left, right| left.0.cmp(right.0))
83 .map(|(_locale, value)| value.clone())
84 }
85
86 fn resolve_partition_heading(&self, heading: &BibliographyPartitionHeading) -> Option<String> {
88 match heading {
89 BibliographyPartitionHeading::Literal { literal } => Some(literal.clone()),
90 BibliographyPartitionHeading::Term { term, form } => self.locale.resolved_general_term(
91 term,
92 &form.clone().unwrap_or(citum_schema::locale::TermForm::Long),
93 None,
94 ),
95 BibliographyPartitionHeading::Localized { localized } => {
96 self.resolve_localized_heading(localized)
97 }
98 }
99 }
100
101 fn collect_matching_group_refs<'a>(
103 &'a self,
104 bibliography: &'a [ProcEntry],
105 assigned: &HashSet<String>,
106 evaluator: &SelectorEvaluator<'_>,
107 group: &BibliographyGroup,
108 ) -> Vec<&'a Reference> {
109 bibliography
110 .iter()
111 .filter(|entry| !assigned.contains(&entry.id))
112 .filter_map(|entry| {
113 self.bibliography
114 .get(&entry.id)
115 .filter(|reference| evaluator.matches(reference, &group.selector))
116 })
117 .collect()
118 }
119
120 pub(super) fn sorted_id_stubs(&self) -> Vec<ProcEntry> {
125 self.initialize_numeric_bibliography_numbers();
126 self.sort_references(self.bibliography.values().collect())
127 .into_iter()
128 .filter_map(|r| {
129 r.id().map(|id| ProcEntry {
130 id: id.to_string(),
131 template: vec![],
132 metadata: ProcEntryMetadata::default(),
133 })
134 })
135 .collect()
136 }
137
138 fn mark_group_members_assigned(assigned: &mut HashSet<String>, references: &[&Reference]) {
140 for reference in references {
141 if let Some(id) = reference.id() {
142 assigned.insert(id.to_string());
143 }
144 }
145 }
146
147 fn build_group_local_hints(
151 &self,
152 sorted_refs: &[&Reference],
153 group: &BibliographyGroup,
154 ) -> Option<HashMap<String, ProcHints>> {
155 if !matches!(group.disambiguate, Some(DisambiguationScope::Locally)) {
156 return None;
157 }
158
159 let mut group_bibliography = Bibliography::new();
160 for reference in sorted_refs {
161 group_bibliography.insert(
162 reference.id().unwrap_or_default().to_string(),
163 (*reference).clone(),
164 );
165 }
166
167 let resolved_sort = group
168 .sort
169 .as_ref()
170 .map(citum_schema::GroupSortEntry::resolve);
171 let bibliography_config = self.get_bibliography_config();
172 let disambiguator = if let Some(sort) = resolved_sort.as_ref() {
173 Disambiguator::with_group_sort(
174 &group_bibliography,
175 &bibliography_config,
176 &self.locale,
177 sort,
178 )
179 } else {
180 Disambiguator::new(&group_bibliography, &bibliography_config, &self.locale)
181 };
182
183 Some(disambiguator.calculate_hints())
184 }
185
186 fn effective_group_style<'a>(
188 &'a self,
189 group: &'a BibliographyGroup,
190 ) -> Cow<'a, citum_schema::Style> {
191 if let Some(group_template) = &group.template {
192 let mut local_style = self.style.clone();
193 if let Some(bibliography) = local_style.bibliography.as_mut() {
194 bibliography.template = Some(group_template.clone());
195 }
196 Cow::Owned(local_style)
197 } else {
198 Cow::Borrowed(&self.style)
199 }
200 }
201
202 fn render_group_entries<F>(
204 &self,
205 _bibliography: &[ProcEntry],
206 sorted_refs: Vec<&Reference>,
207 group: &BibliographyGroup,
208 local_hints: Option<&HashMap<String, ProcHints>>,
209 ) -> Vec<ProcEntry>
210 where
211 F: OutputFormat<Output = String>,
212 {
213 let hints = local_hints.unwrap_or(&self.hints);
216 let effective_style = self.effective_group_style(group);
217 let bibliography_config = self.get_bibliography_config();
218 let bibliography_options = self.get_bibliography_options().into_owned();
219 let substitute = bibliography_options.subsequent_author_substitute.clone();
220 let renderer = Renderer::new(
221 RendererResources {
222 style: &effective_style,
223 bibliography: &self.bibliography,
224 locale: &self.locale,
225 config: Rc::new(bibliography_config.into_owned()),
226 bibliography_config: Some(Rc::new(bibliography_options)),
227 first_note_by_id: None,
228 },
229 hints,
230 &self.citation_numbers,
231 CompoundRenderData {
232 set_by_ref: &self.compound_set_by_ref,
233 member_index: &self.compound_member_index,
234 sets: &self.compound_sets,
235 },
236 self.show_semantics,
237 self.inject_ast_indices,
238 self.abbreviation_map.as_ref(),
239 );
240
241 let mut entries = Vec::new();
242 let mut previous_reference: Option<&Reference> = None;
243
244 for (index, reference) in sorted_refs.into_iter().enumerate() {
245 let ref_id = reference.id().unwrap_or_default().to_string();
246 let entry_number = self
247 .citation_numbers
248 .borrow()
249 .get(&ref_id)
250 .copied()
251 .unwrap_or(index + 1);
252
253 if let Some(mut processed) =
254 renderer.process_bibliography_entry_with_format::<F>(reference, entry_number)
255 {
256 if let Some(substitute_string) = substitute.as_deref()
257 && let Some(previous) = previous_reference
258 && self.contributors_match(previous, reference)
259 {
260 renderer.apply_author_substitution_with_format::<F>(
261 &mut processed,
262 substitute_string,
263 );
264 }
265
266 entries.push(ProcEntry {
267 id: ref_id,
268 template: processed,
269 metadata: self.extract_metadata(reference),
270 });
271 previous_reference = Some(reference);
272 }
273 }
274
275 entries
276 }
277
278 fn append_rendered_group<F>(
280 &self,
281 result: &mut String,
282 group: &BibliographyGroup,
283 entries: Vec<ProcEntry>,
284 annotations: Option<&HashMap<String, String>>,
285 annotation_style: Option<&AnnotationStyle>,
286 suppress_heading: bool,
287 ) where
288 F: OutputFormat<Output = String>,
289 {
290 if !result.is_empty() {
291 result.push_str("\n\n");
292 }
293
294 if !suppress_heading
295 && let Some(heading) = group
296 .heading
297 .as_ref()
298 .and_then(|group_heading| self.resolve_group_heading(group_heading))
299 {
300 result.push_str(&self.render_group_heading::<F>(&heading));
301 }
302
303 result.push_str(&crate::render::refs_to_string_with_format::<F>(
304 entries,
305 annotations,
306 annotation_style,
307 ));
308 }
309
310 fn append_rendered_partition<F>(
312 &self,
313 result: &mut String,
314 heading: Option<&BibliographyPartitionHeading>,
315 entries: Vec<ProcEntry>,
316 annotations: Option<&HashMap<String, String>>,
317 annotation_style: Option<&AnnotationStyle>,
318 ) where
319 F: OutputFormat<Output = String>,
320 {
321 if !result.is_empty() {
322 result.push_str("\n\n");
323 }
324
325 if let Some(heading) =
326 heading.and_then(|group_heading| self.resolve_partition_heading(group_heading))
327 {
328 result.push_str(&self.render_group_heading::<F>(&heading));
329 }
330
331 result.push_str(&crate::render::refs_to_string_with_format::<F>(
332 entries,
333 annotations,
334 annotation_style,
335 ));
336 }
337
338 pub(super) fn render_with_partition_sections<F>(
340 &self,
341 sorted_refs: Vec<&Reference>,
342 partitioning: &BibliographySortPartitioning,
343 annotations: Option<&HashMap<String, String>>,
344 annotation_style: Option<&AnnotationStyle>,
345 ) -> String
346 where
347 F: OutputFormat<Output = String>,
348 {
349 let fmt = F::default();
350 let mut result = String::new();
351
352 for (partition_key, references) in
353 crate::sort_partitioning::partition_references(sorted_refs, &self.locale, partitioning)
354 {
355 let heading = partition_key
356 .as_ref()
357 .and_then(|key| partitioning.headings.get(key));
358 let entries = self.merge_compound_entries::<F>(self.process_sorted_refs::<_, F>(
359 references.into_iter(),
360 |reference, entry_number| {
361 self.process_bibliography_entry_with_format::<F>(reference, entry_number)
362 },
363 ));
364 self.append_rendered_partition::<F>(
365 &mut result,
366 heading,
367 entries,
368 annotations,
369 annotation_style,
370 );
371 }
372
373 fmt.finish(result)
374 }
375
376 pub(super) fn render_with_custom_groups_filtered<F>(
383 &self,
384 all_entries: &[ProcEntry],
385 groups: &[BibliographyGroup],
386 selected: &HashSet<String>,
387 annotations: Option<&HashMap<String, String>>,
388 annotation_style: Option<&AnnotationStyle>,
389 ) -> String
390 where
391 F: OutputFormat<Output = String>,
392 {
393 let fmt = F::default();
394 let cited_ids = self.cited_ids.borrow();
395 let evaluator = SelectorEvaluator::new(&cited_ids);
396 let sorter = ReferenceSorter::new(&self.locale);
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>(self.render_group_entries::<F>(
426 all_entries,
427 sorted_refs,
428 group,
429 local_hints.as_ref(),
430 ));
431
432 populated_groups.push((group, entries));
433 }
434
435 let unassigned_refs: Vec<&Reference> = all_entries
437 .iter()
438 .filter(|entry| !assigned.contains(&entry.id) && selected.contains(&entry.id))
439 .filter_map(|entry| self.bibliography.get(&entry.id))
440 .collect();
441
442 let suppress_heading = populated_groups.len() == 1 && unassigned_refs.is_empty();
443
444 for (group, entries) in populated_groups {
446 self.append_rendered_group::<F>(
447 &mut result,
448 group,
449 entries,
450 annotations,
451 annotation_style,
452 suppress_heading,
453 );
454 }
455
456 self.append_unassigned_entries_filtered::<F>(
457 &mut result,
458 all_entries,
459 &assigned,
460 selected,
461 annotations,
462 annotation_style,
463 );
464 fmt.finish(result)
465 }
466
467 fn append_unassigned_entries_filtered<F>(
469 &self,
470 result: &mut String,
471 bibliography: &[ProcEntry],
472 assigned: &HashSet<String>,
473 selected: &HashSet<String>,
474 annotations: Option<&HashMap<String, String>>,
475 annotation_style: Option<&AnnotationStyle>,
476 ) where
477 F: OutputFormat<Output = String>,
478 {
479 let unassigned_refs: Vec<&Reference> = bibliography
480 .iter()
481 .filter(|entry| !assigned.contains(&entry.id) && selected.contains(&entry.id))
482 .filter_map(|entry| self.bibliography.get(&entry.id))
483 .collect();
484
485 if unassigned_refs.is_empty() {
486 return;
487 }
488
489 let unassigned = self.merge_compound_entries::<F>(self.process_sorted_refs::<_, F>(
492 unassigned_refs.into_iter(),
493 |reference, entry_number| {
494 self.process_bibliography_entry_with_format::<F>(reference, entry_number)
495 },
496 ));
497
498 if !result.is_empty() {
499 result.push_str("\n\n");
500 }
501
502 result.push_str(&crate::render::refs_to_string_with_format::<F>(
503 unassigned,
504 annotations,
505 annotation_style,
506 ));
507 }
508
509 fn render_with_legacy_grouping<F>(
511 &self,
512 bibliography: &[ProcEntry],
513 annotations: Option<&HashMap<String, String>>,
514 annotation_style: Option<&AnnotationStyle>,
515 ) -> String
516 where
517 F: OutputFormat<Output = String>,
518 {
519 let fmt = F::default();
520 let cited_ids = self.cited_ids.borrow();
521 let cited_entries: Vec<ProcEntry> = bibliography
522 .iter()
523 .filter(|entry| cited_ids.contains(&entry.id))
524 .cloned()
525 .collect();
526
527 let mut result = String::new();
528 if !cited_entries.is_empty() {
529 result.push_str(&crate::render::refs_to_string_with_format::<F>(
530 cited_entries,
531 annotations,
532 annotation_style,
533 ));
534 }
535
536 fmt.finish(result)
537 }
538
539 pub fn render_grouped_bibliography_with_format<F>(&self) -> String
547 where
548 F: OutputFormat<Output = String>,
549 {
550 self.render_grouped_bibliography_with_format_and_annotations::<F>(None, None)
551 }
552
553 pub fn render_grouped_bibliography_with_format_and_annotations<F>(
555 &self,
556 annotations: Option<&HashMap<String, String>>,
557 annotation_style: Option<&AnnotationStyle>,
558 ) -> String
559 where
560 F: OutputFormat<Output = String>,
561 {
562 self.render_grouped_bibliography_inner::<F>(false, annotations, annotation_style)
563 }
564
565 pub(crate) fn render_document_bibliography<F>(
579 &self,
580 restrict_to_cited: bool,
581 annotations: Option<&HashMap<String, String>>,
582 annotation_style: Option<&AnnotationStyle>,
583 ) -> super::DocumentBibliography
584 where
585 F: OutputFormat<Output = String>,
586 {
587 let content = self.render_grouped_bibliography_inner::<F>(
588 restrict_to_cited,
589 annotations,
590 annotation_style,
591 );
592 let cited_ids: Vec<String> = self.cited_ids.borrow().iter().cloned().collect();
594 let entries = if restrict_to_cited {
595 self.process_selected_references_with_format::<F, _>(cited_ids)
596 .bibliography
597 } else {
598 self.process_references_with_format::<F>().bibliography
599 };
600 super::DocumentBibliography { content, entries }
601 }
602
603 fn render_grouped_bibliography_inner<F>(
610 &self,
611 restrict_to_cited: bool,
612 annotations: Option<&HashMap<String, String>>,
613 annotation_style: Option<&AnnotationStyle>,
614 ) -> String
615 where
616 F: OutputFormat<Output = String>,
617 {
618 if let Some(groups) = self
619 .style
620 .bibliography
621 .as_ref()
622 .and_then(|bibliography| bibliography.groups.as_ref())
623 {
624 let id_stubs = self.sorted_id_stubs();
625 let selected = if restrict_to_cited {
626 let cited = self.cited_ids.borrow();
627 id_stubs
628 .iter()
629 .filter(|e| cited.contains(&e.id))
630 .map(|e| e.id.clone())
631 .collect::<HashSet<_>>()
632 } else {
633 id_stubs
634 .iter()
635 .map(|e| e.id.clone())
636 .collect::<HashSet<_>>()
637 };
638 return self.render_with_custom_groups_filtered::<F>(
639 &id_stubs,
640 groups,
641 &selected,
642 annotations,
643 annotation_style,
644 );
645 }
646
647 let bibliography_options = self.get_bibliography_options();
648 if let Some(partitioning) = bibliography_options.sort_partitioning.as_ref()
649 && crate::sort_partitioning::should_render_sections(partitioning)
650 {
651 self.initialize_numeric_bibliography_numbers();
652 let mut refs: Vec<&Reference> = self.bibliography.values().collect();
653 if restrict_to_cited {
654 let cited = self.cited_ids.borrow();
655 refs.retain(|r| r.id().as_deref().is_some_and(|id| cited.contains(id)));
656 }
657 let sorted_refs = self.sort_references(refs);
658 return self.render_with_partition_sections::<F>(
659 sorted_refs,
660 partitioning,
661 annotations,
662 annotation_style,
663 );
664 }
665
666 let all_entries = self.process_references_with_format::<F>().bibliography;
667 self.render_with_legacy_grouping::<F>(
668 &self.merge_compound_entries::<F>(all_entries),
669 annotations,
670 annotation_style,
671 )
672 }
673
674 fn entries_for_bibliography_group<F>(
679 &self,
680 group: &BibliographyGroup,
681 assigned: &mut HashSet<String>,
682 ) -> Vec<crate::render::ProcEntry>
683 where
684 F: OutputFormat<Output = String>,
685 {
686 let bibliography = self.sorted_id_stubs();
687 let cited_ids = self.cited_ids.borrow();
688 let evaluator = SelectorEvaluator::new(&cited_ids);
689 let sorter = ReferenceSorter::new(&self.locale);
690
691 let matching_refs =
692 self.collect_matching_group_refs(&bibliography, assigned, &evaluator, group);
693 Self::mark_group_members_assigned(assigned, &matching_refs);
694
695 if matching_refs.is_empty() {
696 return Vec::new();
697 }
698
699 let sorted_refs = if let Some(sort_spec) = &group.sort {
700 sorter.sort_references(matching_refs, &sort_spec.resolve())
701 } else {
702 matching_refs
703 };
704
705 let local_hints = self.build_group_local_hints(&sorted_refs, group);
706 self.merge_compound_entries::<F>(self.render_group_entries::<F>(
707 &bibliography,
708 sorted_refs,
709 group,
710 local_hints.as_ref(),
711 ))
712 }
713
714 pub(crate) fn render_document_bibliography_block<F>(
719 &self,
720 group: &BibliographyGroup,
721 assigned: &mut HashSet<String>,
722 annotations: Option<&HashMap<String, String>>,
723 annotation_style: Option<&AnnotationStyle>,
724 ) -> RenderedBibliographyGroup
725 where
726 F: OutputFormat<Output = String>,
727 {
728 let mut headingless = group.clone();
729 let heading = headingless
730 .heading
731 .take()
732 .and_then(|group_heading| self.resolve_group_heading(&group_heading));
733
734 let entries = self.entries_for_bibliography_group::<F>(&headingless, assigned);
735 let body = crate::render::refs_to_string_slice_with_format::<F>(
736 &entries,
737 annotations,
738 annotation_style,
739 );
740
741 RenderedBibliographyGroup {
742 heading,
743 body,
744 entries,
745 }
746 }
747
748 pub(crate) fn render_document_bibliography_blocks<F>(
753 &self,
754 groups: &[BibliographyGroup],
755 annotations: Option<&HashMap<String, String>>,
756 annotation_style: Option<&AnnotationStyle>,
757 ) -> Vec<RenderedBibliographyGroup>
758 where
759 F: OutputFormat<Output = String>,
760 {
761 let mut assigned = std::collections::HashSet::new();
762 groups
763 .iter()
764 .map(|group| {
765 self.render_document_bibliography_block::<F>(
766 group,
767 &mut assigned,
768 annotations,
769 annotation_style,
770 )
771 })
772 .collect()
773 }
774
775 pub(super) fn extract_metadata(&self, reference: &Reference) -> ProcEntryMetadata {
776 let bibliography_config = Rc::new(self.get_bibliography_config().into_owned());
777 let options = RenderOptions {
778 config: bibliography_config.clone(),
779 bibliography_config: Some(Rc::new(self.get_bibliography_options().into_owned())),
780 locale: &self.locale,
781 context: RenderContext::Bibliography,
782 mode: citum_schema::citation::CitationMode::NonIntegral,
783 suppress_author: false,
784 locator_raw: None,
785 ref_type: None,
786 show_semantics: self.show_semantics,
787 current_template_index: None,
788 abbreviation_map: self.abbreviation_map.as_ref(),
789 };
790
791 let ml = bibliography_config.multilingual.as_ref();
792 let preferred_transliteration = ml.and_then(|m| m.preferred_transliteration.as_deref());
793 let preferred_script = ml.and_then(|m| m.preferred_script.as_ref());
794
795 ProcEntryMetadata {
796 author: reference.author().map(|author| {
797 let names = resolve_multilingual_name(
798 &author,
799 ml.and_then(|m| m.name_mode.as_ref()),
800 preferred_transliteration,
801 preferred_script,
802 &self.locale.locale,
803 );
804 format_contributors_short(&names, &options)
805 }),
806 year: reference
807 .effective_issued_date()
808 .map(|issued| issued.year().clone()),
809 title: reference.title().map(|title| {
810 use citum_schema::reference::types::{MultilingualString, Title};
811 match &title {
812 Title::Multilingual(m) => resolve_multilingual_string(
813 &MultilingualString::Complex(m.clone()),
814 ml.and_then(|ml| ml.title_mode.as_ref()),
815 preferred_transliteration,
816 preferred_script,
817 &self.locale.locale,
818 ),
819 _ => title.to_string(),
820 }
821 }),
822 }
823 }
824
825 fn render_group_heading<F>(&self, heading: &str) -> String
826 where
827 F: OutputFormat<Output = String>,
828 {
829 let fmt = F::default();
830 fmt.finish(fmt.unnumbered_heading(2, fmt.text(heading)))
831 }
832}