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