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_filtered<F>(
381 &self,
382 all_entries: &[ProcEntry],
383 groups: &[BibliographyGroup],
384 selected: &HashSet<String>,
385 annotations: Option<&HashMap<String, String>>,
386 annotation_style: Option<&AnnotationStyle>,
387 ) -> String
388 where
389 F: OutputFormat<Output = String>,
390 {
391 let fmt = F::default();
392 let cited_ids = self.cited_ids.borrow();
393 let evaluator = SelectorEvaluator::new(&cited_ids);
394 let sorter = GroupSorter::new(&self.locale);
395
396 let mut assigned = HashSet::new();
397 let mut result = String::new();
398
399 let mut populated_groups: Vec<(&BibliographyGroup, Vec<ProcEntry>)> = Vec::new();
401
402 for group in groups {
403 let matching_refs =
404 self.collect_matching_group_refs(all_entries, &assigned, &evaluator, group);
405
406 let matching_refs: Vec<&Reference> = matching_refs
407 .into_iter()
408 .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
409 .collect();
410
411 if matching_refs.is_empty() {
412 continue;
413 }
414
415 Self::mark_group_members_assigned(&mut assigned, &matching_refs);
416
417 let sorted_refs = if let Some(sort_spec) = &group.sort {
418 sorter.sort_references(matching_refs, &sort_spec.resolve())
419 } else {
420 matching_refs
421 };
422 let local_hints = self.build_group_local_hints(&sorted_refs, group);
423 let entries = self.merge_compound_entries::<F>(self.render_group_entries::<F>(
424 all_entries,
425 sorted_refs,
426 group,
427 local_hints.as_ref(),
428 ));
429
430 populated_groups.push((group, entries));
431 }
432
433 let unassigned_refs: Vec<&Reference> = all_entries
435 .iter()
436 .filter(|entry| !assigned.contains(&entry.id) && selected.contains(&entry.id))
437 .filter_map(|entry| self.bibliography.get(&entry.id))
438 .collect();
439
440 let suppress_heading = populated_groups.len() == 1 && unassigned_refs.is_empty();
441
442 for (group, entries) in populated_groups {
444 self.append_rendered_group::<F>(
445 &mut result,
446 group,
447 entries,
448 annotations,
449 annotation_style,
450 suppress_heading,
451 );
452 }
453
454 self.append_unassigned_entries_filtered::<F>(
455 &mut result,
456 all_entries,
457 &assigned,
458 selected,
459 annotations,
460 annotation_style,
461 );
462 fmt.finish(result)
463 }
464
465 fn append_unassigned_entries_filtered<F>(
467 &self,
468 result: &mut String,
469 bibliography: &[ProcEntry],
470 assigned: &HashSet<String>,
471 selected: &HashSet<String>,
472 annotations: Option<&HashMap<String, String>>,
473 annotation_style: Option<&AnnotationStyle>,
474 ) where
475 F: OutputFormat<Output = String>,
476 {
477 let unassigned_refs: Vec<&Reference> = bibliography
478 .iter()
479 .filter(|entry| !assigned.contains(&entry.id) && selected.contains(&entry.id))
480 .filter_map(|entry| self.bibliography.get(&entry.id))
481 .collect();
482
483 if unassigned_refs.is_empty() {
484 return;
485 }
486
487 let unassigned = self.merge_compound_entries::<F>(self.process_sorted_refs::<_, F>(
490 unassigned_refs.into_iter(),
491 |reference, entry_number| {
492 self.process_bibliography_entry_with_format::<F>(reference, entry_number)
493 },
494 ));
495
496 if !result.is_empty() {
497 result.push_str("\n\n");
498 }
499
500 result.push_str(&crate::render::refs_to_string_with_format::<F>(
501 unassigned,
502 annotations,
503 annotation_style,
504 ));
505 }
506
507 fn render_with_legacy_grouping<F>(
509 &self,
510 bibliography: &[ProcEntry],
511 annotations: Option<&HashMap<String, String>>,
512 annotation_style: Option<&AnnotationStyle>,
513 ) -> String
514 where
515 F: OutputFormat<Output = String>,
516 {
517 let fmt = F::default();
518 let cited_ids = self.cited_ids.borrow();
519 let cited_entries: Vec<ProcEntry> = bibliography
520 .iter()
521 .filter(|entry| cited_ids.contains(&entry.id))
522 .cloned()
523 .collect();
524
525 let mut result = String::new();
526 if !cited_entries.is_empty() {
527 result.push_str(&crate::render::refs_to_string_with_format::<F>(
528 cited_entries,
529 annotations,
530 annotation_style,
531 ));
532 }
533
534 fmt.finish(result)
535 }
536
537 pub fn render_grouped_bibliography_with_format<F>(&self) -> String
545 where
546 F: OutputFormat<Output = String>,
547 {
548 self.render_grouped_bibliography_with_format_and_annotations::<F>(None, None)
549 }
550
551 pub fn render_grouped_bibliography_with_format_and_annotations<F>(
553 &self,
554 annotations: Option<&HashMap<String, String>>,
555 annotation_style: Option<&AnnotationStyle>,
556 ) -> String
557 where
558 F: OutputFormat<Output = String>,
559 {
560 self.render_grouped_bibliography_inner::<F>(false, annotations, annotation_style)
561 }
562
563 pub(crate) fn render_document_bibliography<F>(
577 &self,
578 restrict_to_cited: bool,
579 annotations: Option<&HashMap<String, String>>,
580 annotation_style: Option<&AnnotationStyle>,
581 ) -> super::DocumentBibliography
582 where
583 F: OutputFormat<Output = String>,
584 {
585 let content = self.render_grouped_bibliography_inner::<F>(
586 restrict_to_cited,
587 annotations,
588 annotation_style,
589 );
590 let cited_ids: Vec<String> = self.cited_ids.borrow().iter().cloned().collect();
592 let entries = if restrict_to_cited {
593 self.process_selected_references_with_format::<F, _>(cited_ids)
594 .bibliography
595 } else {
596 self.process_references_with_format::<F>().bibliography
597 };
598 super::DocumentBibliography { content, entries }
599 }
600
601 fn render_grouped_bibliography_inner<F>(
608 &self,
609 restrict_to_cited: bool,
610 annotations: Option<&HashMap<String, String>>,
611 annotation_style: Option<&AnnotationStyle>,
612 ) -> String
613 where
614 F: OutputFormat<Output = String>,
615 {
616 if let Some(groups) = self
617 .style
618 .bibliography
619 .as_ref()
620 .and_then(|bibliography| bibliography.groups.as_ref())
621 {
622 let id_stubs = self.sorted_id_stubs();
623 let selected = if restrict_to_cited {
624 let cited = self.cited_ids.borrow();
625 id_stubs
626 .iter()
627 .filter(|e| cited.contains(&e.id))
628 .map(|e| e.id.clone())
629 .collect::<HashSet<_>>()
630 } else {
631 id_stubs
632 .iter()
633 .map(|e| e.id.clone())
634 .collect::<HashSet<_>>()
635 };
636 return self.render_with_custom_groups_filtered::<F>(
637 &id_stubs,
638 groups,
639 &selected,
640 annotations,
641 annotation_style,
642 );
643 }
644
645 let bibliography_options = self.get_bibliography_options();
646 if let Some(partitioning) = bibliography_options.sort_partitioning.as_ref()
647 && crate::sort_partitioning::should_render_sections(partitioning)
648 {
649 self.initialize_numeric_bibliography_numbers();
650 let mut refs: Vec<&Reference> = self.bibliography.values().collect();
651 if restrict_to_cited {
652 let cited = self.cited_ids.borrow();
653 refs.retain(|r| r.id().as_deref().is_some_and(|id| cited.contains(id)));
654 }
655 let sorted_refs = self.sort_references(refs);
656 return self.render_with_partition_sections::<F>(
657 sorted_refs,
658 partitioning,
659 annotations,
660 annotation_style,
661 );
662 }
663
664 let all_entries = self.process_references_with_format::<F>().bibliography;
665 self.render_with_legacy_grouping::<F>(
666 &self.merge_compound_entries::<F>(all_entries),
667 annotations,
668 annotation_style,
669 )
670 }
671
672 fn entries_for_bibliography_group<F>(
677 &self,
678 group: &BibliographyGroup,
679 assigned: &mut HashSet<String>,
680 ) -> Vec<crate::render::ProcEntry>
681 where
682 F: OutputFormat<Output = String>,
683 {
684 let bibliography = self.sorted_id_stubs();
685 let cited_ids = self.cited_ids.borrow();
686 let evaluator = SelectorEvaluator::new(&cited_ids);
687 let sorter = GroupSorter::new(&self.locale);
688
689 let matching_refs =
690 self.collect_matching_group_refs(&bibliography, assigned, &evaluator, group);
691 Self::mark_group_members_assigned(assigned, &matching_refs);
692
693 if matching_refs.is_empty() {
694 return Vec::new();
695 }
696
697 let sorted_refs = if let Some(sort_spec) = &group.sort {
698 sorter.sort_references(matching_refs, &sort_spec.resolve())
699 } else {
700 matching_refs
701 };
702
703 let local_hints = self.build_group_local_hints(&sorted_refs, group);
704 self.merge_compound_entries::<F>(self.render_group_entries::<F>(
705 &bibliography,
706 sorted_refs,
707 group,
708 local_hints.as_ref(),
709 ))
710 }
711
712 pub(crate) fn render_document_bibliography_block<F>(
717 &self,
718 group: &BibliographyGroup,
719 assigned: &mut HashSet<String>,
720 annotations: Option<&HashMap<String, String>>,
721 annotation_style: Option<&AnnotationStyle>,
722 ) -> RenderedBibliographyGroup
723 where
724 F: OutputFormat<Output = String>,
725 {
726 let mut headingless = group.clone();
727 let heading = headingless
728 .heading
729 .take()
730 .and_then(|group_heading| self.resolve_group_heading(&group_heading));
731
732 let entries = self.entries_for_bibliography_group::<F>(&headingless, assigned);
733 let body = crate::render::refs_to_string_slice_with_format::<F>(
734 &entries,
735 annotations,
736 annotation_style,
737 );
738
739 RenderedBibliographyGroup {
740 heading,
741 body,
742 entries,
743 }
744 }
745
746 pub(crate) fn render_document_bibliography_blocks<F>(
751 &self,
752 groups: &[BibliographyGroup],
753 annotations: Option<&HashMap<String, String>>,
754 annotation_style: Option<&AnnotationStyle>,
755 ) -> Vec<RenderedBibliographyGroup>
756 where
757 F: OutputFormat<Output = String>,
758 {
759 let mut assigned = std::collections::HashSet::new();
760 groups
761 .iter()
762 .map(|group| {
763 self.render_document_bibliography_block::<F>(
764 group,
765 &mut assigned,
766 annotations,
767 annotation_style,
768 )
769 })
770 .collect()
771 }
772
773 pub(super) fn extract_metadata(&self, reference: &Reference) -> ProcEntryMetadata {
774 let bibliography_config = self.get_bibliography_config();
775 let options = RenderOptions {
776 config: &bibliography_config,
777 bibliography_config: Some(self.get_bibliography_options().into_owned()),
778 locale: &self.locale,
779 context: RenderContext::Bibliography,
780 mode: citum_schema::citation::CitationMode::NonIntegral,
781 suppress_author: false,
782 locator_raw: None,
783 ref_type: None,
784 show_semantics: self.show_semantics,
785 current_template_index: None,
786 abbreviation_map: self.abbreviation_map.as_ref(),
787 };
788
789 let ml = bibliography_config.multilingual.as_ref();
790 let preferred_transliteration = ml.and_then(|m| m.preferred_transliteration.as_deref());
791 let preferred_script = ml.and_then(|m| m.preferred_script.as_ref());
792
793 ProcEntryMetadata {
794 author: reference.author().map(|author| {
795 let names = resolve_multilingual_name(
796 &author,
797 ml.and_then(|m| m.name_mode.as_ref()),
798 preferred_transliteration,
799 preferred_script,
800 &self.locale.locale,
801 );
802 format_contributors_short(&names, &options)
803 }),
804 year: reference
805 .csl_issued_date()
806 .map(|issued| issued.year().clone()),
807 title: reference.title().map(|title| {
808 use citum_schema::reference::types::{MultilingualString, Title};
809 match &title {
810 Title::Multilingual(m) => resolve_multilingual_string(
811 &MultilingualString::Complex(m.clone()),
812 ml.and_then(|ml| ml.title_mode.as_ref()),
813 preferred_transliteration,
814 preferred_script,
815 &self.locale.locale,
816 ),
817 _ => title.to_string(),
818 }
819 }),
820 }
821 }
822
823 fn render_group_heading<F>(&self, heading: &str) -> String
824 where
825 F: OutputFormat<Output = String>,
826 {
827 if std::any::type_name::<F>() == std::any::type_name::<crate::render::html::Html>() {
828 return format!("<h2>{heading}</h2>\n\n");
829 }
830
831 format!("# {heading}\n\n")
832 }
833}