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