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    /// Individual entries rendered in this block.
32    pub(crate) entries: Vec<crate::render::ProcEntry>,
33}
34
35/// Combined document bibliography rendering output.
36///
37/// Returned by [`Processor::render_document_bibliography`] — the unified facade
38/// used by the batch, session, and document-string rendering paths. Both fields
39/// are computed from the same cited subset so subsequent-author substitution
40/// stays consistent between the rendered string and the per-entry data.
41#[derive(Debug, Clone, Default)]
42pub(crate) struct DocumentBibliography {
43    /// The full rendered bibliography string for the document.
44    pub(crate) content: String,
45    /// Flat per-entry data, one entry per cited reference.
46    pub(crate) entries: Vec<crate::render::ProcEntry>,
47}
48
49impl Processor {
50    /// Create a bibliography renderer with effective shared and bibliography-only config.
51    fn with_bibliography_renderer<T>(&self, render: impl FnOnce(Renderer<'_>) -> T) -> T {
52        let bibliography_shared_config = self.get_bibliography_config();
53        let bibliography_config = self.get_bibliography_options().into_owned();
54        let renderer = Renderer::new(
55            RendererResources {
56                style: &self.style,
57                bibliography: &self.bibliography,
58                locale: &self.locale,
59                config: &bibliography_shared_config,
60                bibliography_config: Some(bibliography_config),
61                first_note_by_id: None,
62            },
63            &self.hints,
64            &self.citation_numbers,
65            CompoundRenderData {
66                set_by_ref: &self.compound_set_by_ref,
67                member_index: &self.compound_member_index,
68                sets: &self.compound_sets,
69            },
70            self.show_semantics,
71            self.inject_ast_indices,
72            self.abbreviation_map.as_ref(),
73        );
74
75        render(renderer)
76    }
77
78    /// Process sorted references and apply subsequent-author substitution.
79    ///
80    /// Returns bibliography entries with optional author substitution applied.
81    ///
82    /// This is the core iterator for bibliography rendering, handling the choice
83    /// between entry-specific rendering and subsequent-author placeholders.
84    fn process_sorted_refs<'a, I, F>(
85        &self,
86        sorted_refs: I,
87        process_fn: impl Fn(&Reference, usize) -> Option<ProcTemplate>,
88    ) -> Vec<ProcEntry>
89    where
90        I: Iterator<Item = &'a Reference>,
91        F: OutputFormat<Output = String>,
92    {
93        let mut bibliography = Vec::new();
94        let mut previous_reference: Option<&Reference> = None;
95
96        let bibliography_options = self.get_bibliography_options();
97        let substitute = bibliography_options.subsequent_author_substitute.as_ref();
98
99        for (index, reference) in sorted_refs.enumerate() {
100            let ref_id = reference.id().unwrap_or_default().to_string();
101            let entry_number = self
102                .citation_numbers
103                .borrow()
104                .get(&ref_id)
105                .copied()
106                .unwrap_or(index + 1);
107
108            if let Some(mut processed) = process_fn(reference, entry_number) {
109                if let Some(substitute_string) = substitute
110                    && let Some(previous) = previous_reference
111                    && self.contributors_match(previous, reference)
112                {
113                    self.with_bibliography_renderer(|renderer| {
114                        renderer.apply_author_substitution_with_format::<F>(
115                            &mut processed,
116                            substitute_string,
117                        );
118                    });
119                }
120
121                bibliography.push(ProcEntry {
122                    id: ref_id,
123                    template: processed,
124                    metadata: self.extract_metadata(reference),
125                });
126                previous_reference = Some(reference);
127            }
128        }
129
130        bibliography
131    }
132
133    /// Process all bibliography references and render them.
134    ///
135    /// Returns sorted and formatted bibliography entries. For numeric styles,
136    /// citations must have been processed first to assign citation numbers.
137    pub fn process_references(&self) -> ProcessedReferences {
138        self.process_references_with_format::<crate::render::plain::PlainText>()
139    }
140
141    /// Process all bibliography references using the requested output format.
142    ///
143    /// This preserves format-specific inline markup in per-entry API output.
144    pub fn process_references_with_format<F>(&self) -> ProcessedReferences
145    where
146        F: OutputFormat<Output = String>,
147    {
148        self.initialize_numeric_bibliography_numbers();
149        let sorted_refs = self.sort_references(self.bibliography.values().collect());
150        let bibliography = self.process_sorted_refs::<_, F>(
151            sorted_refs.iter().copied(),
152            |reference, entry_number| {
153                self.process_bibliography_entry_with_format::<F>(reference, entry_number)
154            },
155        );
156        ProcessedReferences {
157            bibliography,
158            citations: None,
159        }
160    }
161
162    /// Process only the selected bibliography entries, in bibliography sort order.
163    ///
164    /// Mirrors the flat path inside
165    /// [`render_selected_bibliography_with_format_and_annotations`] so that
166    /// per-entry `text` and subsequent-author substitution are computed against
167    /// the same subset that produced `content` — not the full loaded
168    /// bibliography. This matters for subsequent-author substitution: an uncited
169    /// predecessor must not cause the first cited entry to receive `———`.
170    pub(crate) fn process_selected_references_with_format<F, I>(
171        &self,
172        item_ids: I,
173    ) -> ProcessedReferences
174    where
175        F: OutputFormat<Output = String>,
176        I: IntoIterator<Item = String>,
177    {
178        self.initialize_numeric_bibliography_numbers();
179        let selected: HashSet<String> = item_ids.into_iter().collect();
180        let sorted_refs = self.sort_references(self.bibliography.values().collect());
181        let bibliography = self.process_sorted_refs::<_, F>(
182            sorted_refs
183                .iter()
184                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
185                .copied(),
186            |reference, entry_number| {
187                self.process_bibliography_entry_with_format::<F>(reference, entry_number)
188            },
189        );
190        ProcessedReferences {
191            bibliography,
192            citations: None,
193        }
194    }
195
196    /// Process and render a bibliography entry.
197    pub fn process_bibliography_entry(
198        &self,
199        reference: &Reference,
200        entry_number: usize,
201    ) -> Option<ProcTemplate> {
202        self.with_bibliography_renderer(|renderer| {
203            renderer.process_bibliography_entry(reference, entry_number)
204        })
205    }
206
207    /// Process a bibliography entry with specific format.
208    pub fn process_bibliography_entry_with_format<F>(
209        &self,
210        reference: &Reference,
211        entry_number: usize,
212    ) -> Option<ProcTemplate>
213    where
214        F: OutputFormat<Output = String>,
215    {
216        self.with_bibliography_renderer(|renderer| {
217            renderer.process_bibliography_entry_with_format::<F>(reference, entry_number)
218        })
219    }
220
221    /// Check whether primary contributors match between two references.
222    ///
223    /// Used for subsequent author substitution in bibliographies.
224    pub fn contributors_match(&self, prev: &Reference, current: &Reference) -> bool {
225        let matcher = Matcher::new(&self.style, &self.default_config);
226        matcher.contributors_match(prev, current)
227    }
228
229    /// Replace the primary contributor with a substitution string.
230    ///
231    /// Used for subsequent author substitution (e.g., "———").
232    pub fn apply_author_substitution(&self, proc: &mut ProcTemplate, substitute: &str) {
233        self.with_bibliography_renderer(|renderer| {
234            renderer.apply_author_substitution(proc, substitute);
235        });
236    }
237
238    /// Render the bibliography to a string using a specific format.
239    pub fn render_bibliography_with_format<F>(&self) -> String
240    where
241        F: OutputFormat<Output = String>,
242    {
243        self.render_bibliography_with_format_and_annotations::<F>(None, None)
244    }
245
246    /// Render the bibliography to a string with annotations.
247    pub fn render_bibliography_with_format_and_annotations<F>(
248        &self,
249        annotations: Option<&HashMap<String, String>>,
250        annotation_style: Option<&AnnotationStyle>,
251    ) -> String
252    where
253        F: OutputFormat<Output = String>,
254    {
255        self.render_selected_bibliography_with_format_and_annotations::<F, _>(
256            self.bibliography.keys().cloned().collect::<Vec<_>>(),
257            annotations,
258            annotation_style,
259        )
260    }
261
262    /// Render a selected bibliography subset to a string using a specific format.
263    pub fn render_selected_bibliography_with_format<F, I>(&self, item_ids: I) -> String
264    where
265        F: OutputFormat<Output = String>,
266        I: IntoIterator<Item = String>,
267    {
268        self.render_selected_bibliography_with_format_and_annotations::<F, _>(item_ids, None, None)
269    }
270
271    /// Render a selected bibliography subset to a string with annotations.
272    ///
273    /// Orchestrates the choice between:
274    /// 1. Custom bibliography groups (selectors and headings).
275    /// 2. Automatic sort partitioning with sections (headings only).
276    /// 3. Standard flat rendering.
277    pub fn render_selected_bibliography_with_format_and_annotations<F, I>(
278        &self,
279        item_ids: I,
280        annotations: Option<&HashMap<String, String>>,
281        annotation_style: Option<&AnnotationStyle>,
282    ) -> String
283    where
284        F: OutputFormat<Output = String>,
285        I: IntoIterator<Item = String>,
286    {
287        let selected: HashSet<String> = item_ids.into_iter().collect();
288
289        // 1. Check for custom bibliography groups
290        if let Some(groups) = self
291            .style
292            .bibliography
293            .as_ref()
294            .filter(|bibliography| bibliography.groups_enabled)
295            .and_then(|bibliography| bibliography.groups.as_ref())
296        {
297            let all_entries = self.process_references().bibliography;
298            return self.render_with_custom_groups_filtered::<F>(
299                &all_entries,
300                groups,
301                &selected,
302                annotations,
303                annotation_style,
304            );
305        }
306
307        // 2. Check for automatic sort partitioning with sections
308        let bibliography_options = self.get_bibliography_options();
309        if let Some(partitioning) = bibliography_options.sort_partitioning.as_ref()
310            && crate::sort_partitioning::should_render_sections(partitioning)
311        {
312            self.initialize_numeric_bibliography_numbers();
313            let all_sorted = self.sort_references(self.bibliography.values().collect());
314            let selected_sorted: Vec<&Reference> = all_sorted
315                .into_iter()
316                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
317                .collect();
318            return self.render_with_partition_sections::<F>(
319                selected_sorted,
320                partitioning,
321                annotations,
322                annotation_style,
323            );
324        }
325
326        // 3. Fallback to flat rendering
327        self.initialize_numeric_bibliography_numbers();
328        let sorted_refs = self.sort_references(self.bibliography.values().collect());
329
330        let bibliography = self.process_sorted_refs::<_, F>(
331            sorted_refs
332                .iter()
333                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
334                .copied(),
335            |reference, entry_number| {
336                self.process_bibliography_entry_with_format::<F>(reference, entry_number)
337            },
338        );
339
340        let bibliography = self.merge_compound_entries::<F>(bibliography);
341        crate::render::refs_to_string_with_format::<F>(bibliography, annotations, annotation_style)
342    }
343
344    /// Render the entire bibliography to a formatted string.
345    pub fn render_bibliography(&self) -> String {
346        self.render_bibliography_with_format::<crate::render::plain::PlainText>()
347    }
348}