Skip to main content

citum_engine/processor/bibliography/
mod.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Bibliography processing and rendering.
7//!
8//! This module owns bibliography entry generation, grouped rendering,
9//! subsequent-author substitution, and the document-facing facade methods used
10//! by the document processor.
11
12mod compound;
13mod grouping;
14
15use super::matching::Matcher;
16use super::rendering::{CompoundRenderData, Renderer, RendererResources};
17use super::{ProcessedReferences, Processor};
18use crate::api::AnnotationStyle;
19use crate::reference::Reference;
20use crate::render::format::OutputFormat;
21use crate::render::{ProcEntry, ProcTemplate};
22use std::collections::{HashMap, HashSet};
23
24/// Rendered bibliography block data for document integration.
25#[derive(Debug, Clone, Default)]
26pub(crate) struct RenderedBibliographyGroup {
27    /// The resolved group heading, if one exists.
28    pub(crate) heading: Option<String>,
29    /// The rendered bibliography body without any document-level heading wrapper.
30    pub(crate) body: String,
31}
32
33impl Processor {
34    /// Create a bibliography renderer with effective shared and bibliography-only config.
35    fn with_bibliography_renderer<T>(&self, render: impl FnOnce(Renderer<'_>) -> T) -> T {
36        let bibliography_shared_config = self.get_bibliography_config();
37        let bibliography_config = self.get_bibliography_options().into_owned();
38        let renderer = Renderer::new(
39            RendererResources {
40                style: &self.style,
41                bibliography: &self.bibliography,
42                locale: &self.locale,
43                config: &bibliography_shared_config,
44                bibliography_config: Some(bibliography_config),
45            },
46            &self.hints,
47            &self.citation_numbers,
48            CompoundRenderData {
49                set_by_ref: &self.compound_set_by_ref,
50                member_index: &self.compound_member_index,
51                sets: &self.compound_sets,
52            },
53            self.show_semantics,
54            self.inject_ast_indices,
55            self.abbreviation_map.as_ref(),
56        );
57
58        render(renderer)
59    }
60
61    /// Process sorted references and apply subsequent-author substitution.
62    ///
63    /// Returns bibliography entries with optional author substitution applied.
64    ///
65    /// This is the core iterator for bibliography rendering, handling the choice
66    /// between entry-specific rendering and subsequent-author placeholders.
67    fn process_sorted_refs<'a, I, F>(
68        &self,
69        sorted_refs: I,
70        process_fn: impl Fn(&Reference, usize) -> Option<ProcTemplate>,
71    ) -> Vec<ProcEntry>
72    where
73        I: Iterator<Item = &'a Reference>,
74        F: OutputFormat<Output = String>,
75    {
76        let mut bibliography = Vec::new();
77        let mut previous_reference: Option<&Reference> = None;
78
79        let bibliography_options = self.get_bibliography_options();
80        let substitute = bibliography_options.subsequent_author_substitute.as_ref();
81
82        for (index, reference) in sorted_refs.enumerate() {
83            let ref_id = reference.id().unwrap_or_default().to_string();
84            let entry_number = self
85                .citation_numbers
86                .borrow()
87                .get(&ref_id)
88                .copied()
89                .unwrap_or(index + 1);
90
91            if let Some(mut processed) = process_fn(reference, entry_number) {
92                if let Some(substitute_string) = substitute
93                    && let Some(previous) = previous_reference
94                    && self.contributors_match(previous, reference)
95                {
96                    self.with_bibliography_renderer(|renderer| {
97                        renderer.apply_author_substitution_with_format::<F>(
98                            &mut processed,
99                            substitute_string,
100                        );
101                    });
102                }
103
104                bibliography.push(ProcEntry {
105                    id: ref_id,
106                    template: processed,
107                    metadata: self.extract_metadata(reference),
108                });
109                previous_reference = Some(reference);
110            }
111        }
112
113        bibliography
114    }
115
116    /// Process all bibliography references and render them.
117    ///
118    /// Returns sorted and formatted bibliography entries. For numeric styles,
119    /// citations must have been processed first to assign citation numbers.
120    pub fn process_references(&self) -> ProcessedReferences {
121        self.initialize_numeric_bibliography_numbers();
122        let sorted_refs = self.sort_references(self.bibliography.values().collect());
123        let bibliography = self.process_sorted_refs::<_, crate::render::plain::PlainText>(
124            sorted_refs.iter().copied(),
125            |reference, entry_number| self.process_bibliography_entry(reference, entry_number),
126        );
127
128        ProcessedReferences {
129            bibliography,
130            citations: None,
131        }
132    }
133
134    /// Process and render a bibliography entry.
135    pub fn process_bibliography_entry(
136        &self,
137        reference: &Reference,
138        entry_number: usize,
139    ) -> Option<ProcTemplate> {
140        self.with_bibliography_renderer(|renderer| {
141            renderer.process_bibliography_entry(reference, entry_number)
142        })
143    }
144
145    /// Process a bibliography entry with specific format.
146    pub fn process_bibliography_entry_with_format<F>(
147        &self,
148        reference: &Reference,
149        entry_number: usize,
150    ) -> Option<ProcTemplate>
151    where
152        F: OutputFormat<Output = String>,
153    {
154        self.with_bibliography_renderer(|renderer| {
155            renderer.process_bibliography_entry_with_format::<F>(reference, entry_number)
156        })
157    }
158
159    /// Check whether primary contributors match between two references.
160    ///
161    /// Used for subsequent author substitution in bibliographies.
162    pub fn contributors_match(&self, prev: &Reference, current: &Reference) -> bool {
163        let matcher = Matcher::new(&self.style, &self.default_config);
164        matcher.contributors_match(prev, current)
165    }
166
167    /// Replace the primary contributor with a substitution string.
168    ///
169    /// Used for subsequent author substitution (e.g., "———").
170    pub fn apply_author_substitution(&self, proc: &mut ProcTemplate, substitute: &str) {
171        self.with_bibliography_renderer(|renderer| {
172            renderer.apply_author_substitution(proc, substitute);
173        });
174    }
175
176    /// Render the bibliography to a string using a specific format.
177    pub fn render_bibliography_with_format<F>(&self) -> String
178    where
179        F: OutputFormat<Output = String>,
180    {
181        self.render_bibliography_with_format_and_annotations::<F>(None, None)
182    }
183
184    /// Render the bibliography to a string with annotations.
185    pub fn render_bibliography_with_format_and_annotations<F>(
186        &self,
187        annotations: Option<&HashMap<String, String>>,
188        annotation_style: Option<&AnnotationStyle>,
189    ) -> String
190    where
191        F: OutputFormat<Output = String>,
192    {
193        self.render_selected_bibliography_with_format_and_annotations::<F, _>(
194            self.bibliography.keys().cloned().collect::<Vec<_>>(),
195            annotations,
196            annotation_style,
197        )
198    }
199
200    /// Render a selected bibliography subset to a string using a specific format.
201    pub fn render_selected_bibliography_with_format<F, I>(&self, item_ids: I) -> String
202    where
203        F: OutputFormat<Output = String>,
204        I: IntoIterator<Item = String>,
205    {
206        self.render_selected_bibliography_with_format_and_annotations::<F, _>(item_ids, None, None)
207    }
208
209    /// Render a selected bibliography subset to a string with annotations.
210    ///
211    /// Orchestrates the choice between:
212    /// 1. Custom bibliography groups (selectors and headings).
213    /// 2. Automatic sort partitioning with sections (headings only).
214    /// 3. Standard flat rendering.
215    pub fn render_selected_bibliography_with_format_and_annotations<F, I>(
216        &self,
217        item_ids: I,
218        annotations: Option<&HashMap<String, String>>,
219        annotation_style: Option<&AnnotationStyle>,
220    ) -> String
221    where
222        F: OutputFormat<Output = String>,
223        I: IntoIterator<Item = String>,
224    {
225        let selected: HashSet<String> = item_ids.into_iter().collect();
226
227        // 1. Check for custom bibliography groups
228        if let Some(groups) = self
229            .style
230            .bibliography
231            .as_ref()
232            .filter(|bibliography| bibliography.groups_enabled)
233            .and_then(|bibliography| bibliography.groups.as_ref())
234        {
235            let all_entries = self.process_references().bibliography;
236            return self.render_with_custom_groups_filtered::<F>(
237                &all_entries,
238                groups,
239                &selected,
240                annotations,
241                annotation_style,
242            );
243        }
244
245        // 2. Check for automatic sort partitioning with sections
246        let bibliography_options = self.get_bibliography_options();
247        if let Some(partitioning) = bibliography_options.sort_partitioning.as_ref()
248            && crate::sort_partitioning::should_render_sections(partitioning)
249        {
250            self.initialize_numeric_bibliography_numbers();
251            let all_sorted = self.sort_references(self.bibliography.values().collect());
252            let selected_sorted: Vec<&Reference> = all_sorted
253                .into_iter()
254                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
255                .collect();
256            return self.render_with_partition_sections::<F>(
257                selected_sorted,
258                partitioning,
259                annotations,
260                annotation_style,
261            );
262        }
263
264        // 3. Fallback to flat rendering
265        self.initialize_numeric_bibliography_numbers();
266        let sorted_refs = self.sort_references(self.bibliography.values().collect());
267
268        let bibliography = self.process_sorted_refs::<_, F>(
269            sorted_refs
270                .iter()
271                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
272                .copied(),
273            |reference, entry_number| {
274                self.process_bibliography_entry_with_format::<F>(reference, entry_number)
275            },
276        );
277
278        let bibliography = self.merge_compound_entries::<F>(bibliography);
279        crate::render::refs_to_string_with_format::<F>(bibliography, annotations, annotation_style)
280    }
281
282    /// Render the entire bibliography to a formatted string.
283    pub fn render_bibliography(&self) -> String {
284        self.render_bibliography_with_format::<crate::render::plain::PlainText>()
285    }
286}