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