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 fn render_bibliography_for_group<F>(
552 &self,
553 group: &BibliographyGroup,
554 annotations: Option<&HashMap<String, String>>,
555 annotation_style: Option<&AnnotationStyle>,
556 ) -> String
557 where
558 F: OutputFormat<Output = String>,
559 {
560 let bibliography = self.sorted_id_stubs();
561 let fmt = F::default();
562 let cited_ids = self.cited_ids.borrow();
563 let evaluator = SelectorEvaluator::new(&cited_ids);
564 let sorter = GroupSorter::new(&self.locale);
565
566 let matching_refs =
567 self.collect_matching_group_refs(&bibliography, &HashSet::new(), &evaluator, group);
568
569 if matching_refs.is_empty() {
570 return fmt.finish(String::new());
571 }
572
573 let sorted_refs = if let Some(sort_spec) = &group.sort {
574 sorter.sort_references(matching_refs, &sort_spec.resolve())
575 } else {
576 matching_refs
577 };
578
579 let local_hints = self.build_group_local_hints(&sorted_refs, group);
580 let entries = self.merge_compound_entries::<F>(self.render_group_entries::<F>(
581 &bibliography,
582 sorted_refs,
583 group,
584 local_hints.as_ref(),
585 ));
586
587 fmt.finish(crate::render::refs_to_string_with_format::<F>(
588 entries,
589 annotations,
590 annotation_style,
591 ))
592 }
593
594 pub fn render_grouped_bibliography_with_format<F>(&self) -> String
602 where
603 F: OutputFormat<Output = String>,
604 {
605 self.render_grouped_bibliography_with_format_and_annotations::<F>(None, None)
606 }
607
608 pub fn render_grouped_bibliography_with_format_and_annotations<F>(
610 &self,
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 = id_stubs
625 .iter()
626 .map(|e| e.id.clone())
627 .collect::<HashSet<_>>();
628 return self.render_with_custom_groups_filtered::<F>(
629 &id_stubs,
630 groups,
631 &selected,
632 annotations,
633 annotation_style,
634 );
635 }
636
637 let bibliography_options = self.get_bibliography_options();
638 if let Some(partitioning) = bibliography_options.sort_partitioning.as_ref()
639 && crate::sort_partitioning::should_render_sections(partitioning)
640 {
641 self.initialize_numeric_bibliography_numbers();
642 let sorted_refs = self.sort_references(self.bibliography.values().collect());
643 return self.render_with_partition_sections::<F>(
644 sorted_refs,
645 partitioning,
646 annotations,
647 annotation_style,
648 );
649 }
650
651 let all_entries = self.process_references().bibliography;
652 self.render_with_legacy_grouping::<F>(
653 &self.merge_compound_entries::<F>(all_entries),
654 annotations,
655 annotation_style,
656 )
657 }
658
659 pub(crate) fn render_document_bibliography_groups<F>(
664 &self,
665 groups: &[BibliographyGroup],
666 ) -> String
667 where
668 F: OutputFormat<Output = String>,
669 {
670 let all_entries = self.sorted_id_stubs();
671 self.render_with_custom_groups::<F>(&all_entries, groups)
672 }
673
674 pub(crate) fn render_document_bibliography_block<F>(
679 &self,
680 group: &BibliographyGroup,
681 ) -> RenderedBibliographyGroup
682 where
683 F: OutputFormat<Output = String>,
684 {
685 let mut headingless = group.clone();
686 let heading = headingless
687 .heading
688 .take()
689 .and_then(|group_heading| self.resolve_group_heading(&group_heading));
690 let body = self.render_bibliography_for_group::<F>(&headingless, None, None);
691
692 RenderedBibliographyGroup { heading, body }
693 }
694
695 pub(super) fn extract_metadata(&self, reference: &Reference) -> ProcEntryMetadata {
696 let bibliography_config = self.get_bibliography_config();
697 let options = RenderOptions {
698 config: &bibliography_config,
699 bibliography_config: Some(self.get_bibliography_options().into_owned()),
700 locale: &self.locale,
701 context: RenderContext::Bibliography,
702 mode: citum_schema::citation::CitationMode::NonIntegral,
703 suppress_author: false,
704 locator_raw: None,
705 ref_type: None,
706 show_semantics: self.show_semantics,
707 current_template_index: None,
708 abbreviation_map: self.abbreviation_map.as_ref(),
709 };
710
711 let ml = bibliography_config.multilingual.as_ref();
712 let preferred_transliteration = ml.and_then(|m| m.preferred_transliteration.as_deref());
713 let preferred_script = ml.and_then(|m| m.preferred_script.as_ref());
714
715 ProcEntryMetadata {
716 author: reference.author().map(|author| {
717 let names = resolve_multilingual_name(
718 &author,
719 ml.and_then(|m| m.name_mode.as_ref()),
720 preferred_transliteration,
721 preferred_script,
722 &self.locale.locale,
723 );
724 format_contributors_short(&names, &options)
725 }),
726 year: reference
727 .csl_issued_date()
728 .map(|issued| issued.year().clone()),
729 title: reference.title().map(|title| {
730 use citum_schema::reference::types::{MultilingualString, Title};
731 match &title {
732 Title::Multilingual(m) => resolve_multilingual_string(
733 &MultilingualString::Complex(m.clone()),
734 ml.and_then(|ml| ml.title_mode.as_ref()),
735 preferred_transliteration,
736 preferred_script,
737 &self.locale.locale,
738 ),
739 _ => title.to_string(),
740 }
741 }),
742 }
743 }
744
745 fn render_group_heading<F>(&self, heading: &str) -> String
746 where
747 F: OutputFormat<Output = String>,
748 {
749 if std::any::type_name::<F>() == std::any::type_name::<crate::render::html::Html>() {
750 return format!("<h2>{heading}</h2>\n\n");
751 }
752
753 format!("# {heading}\n\n")
754 }
755}