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                first_note_by_id: None,
46            },
47            &self.hints,
48            &self.citation_numbers,
49            CompoundRenderData {
50                set_by_ref: &self.compound_set_by_ref,
51                member_index: &self.compound_member_index,
52                sets: &self.compound_sets,
53            },
54            self.show_semantics,
55            self.inject_ast_indices,
56            self.abbreviation_map.as_ref(),
57        );
58
59        render(renderer)
60    }
61
62    /// Process sorted references and apply subsequent-author substitution.
63    ///
64    /// Returns bibliography entries with optional author substitution applied.
65    ///
66    /// This is the core iterator for bibliography rendering, handling the choice
67    /// between entry-specific rendering and subsequent-author placeholders.
68    fn process_sorted_refs<'a, I, F>(
69        &self,
70        sorted_refs: I,
71        process_fn: impl Fn(&Reference, usize) -> Option<ProcTemplate>,
72    ) -> Vec<ProcEntry>
73    where
74        I: Iterator<Item = &'a Reference>,
75        F: OutputFormat<Output = String>,
76    {
77        let mut bibliography = Vec::new();
78        let mut previous_reference: Option<&Reference> = None;
79
80        let bibliography_options = self.get_bibliography_options();
81        let substitute = bibliography_options.subsequent_author_substitute.as_ref();
82
83        for (index, reference) in sorted_refs.enumerate() {
84            let ref_id = reference.id().unwrap_or_default().to_string();
85            let entry_number = self
86                .citation_numbers
87                .borrow()
88                .get(&ref_id)
89                .copied()
90                .unwrap_or(index + 1);
91
92            if let Some(mut processed) = process_fn(reference, entry_number) {
93                if let Some(substitute_string) = substitute
94                    && let Some(previous) = previous_reference
95                    && self.contributors_match(previous, reference)
96                {
97                    self.with_bibliography_renderer(|renderer| {
98                        renderer.apply_author_substitution_with_format::<F>(
99                            &mut processed,
100                            substitute_string,
101                        );
102                    });
103                }
104
105                bibliography.push(ProcEntry {
106                    id: ref_id,
107                    template: processed,
108                    metadata: self.extract_metadata(reference),
109                });
110                previous_reference = Some(reference);
111            }
112        }
113
114        bibliography
115    }
116
117    /// Process all bibliography references and render them.
118    ///
119    /// Returns sorted and formatted bibliography entries. For numeric styles,
120    /// citations must have been processed first to assign citation numbers.
121    pub fn process_references(&self) -> ProcessedReferences {
122        self.process_references_with_format::<crate::render::plain::PlainText>()
123    }
124
125    /// Process all bibliography references using the requested output format.
126    ///
127    /// This preserves format-specific inline markup in per-entry API output.
128    pub fn process_references_with_format<F>(&self) -> ProcessedReferences
129    where
130        F: OutputFormat<Output = String>,
131    {
132        self.initialize_numeric_bibliography_numbers();
133        let sorted_refs = self.sort_references(self.bibliography.values().collect());
134        let bibliography = self.process_sorted_refs::<_, F>(
135            sorted_refs.iter().copied(),
136            |reference, entry_number| {
137                self.process_bibliography_entry_with_format::<F>(reference, entry_number)
138            },
139        );
140        ProcessedReferences {
141            bibliography,
142            citations: None,
143        }
144    }
145
146    /// Process and render a bibliography entry.
147    pub fn process_bibliography_entry(
148        &self,
149        reference: &Reference,
150        entry_number: usize,
151    ) -> Option<ProcTemplate> {
152        self.with_bibliography_renderer(|renderer| {
153            renderer.process_bibliography_entry(reference, entry_number)
154        })
155    }
156
157    /// Process a bibliography entry with specific format.
158    pub fn process_bibliography_entry_with_format<F>(
159        &self,
160        reference: &Reference,
161        entry_number: usize,
162    ) -> Option<ProcTemplate>
163    where
164        F: OutputFormat<Output = String>,
165    {
166        self.with_bibliography_renderer(|renderer| {
167            renderer.process_bibliography_entry_with_format::<F>(reference, entry_number)
168        })
169    }
170
171    /// Check whether primary contributors match between two references.
172    ///
173    /// Used for subsequent author substitution in bibliographies.
174    pub fn contributors_match(&self, prev: &Reference, current: &Reference) -> bool {
175        let matcher = Matcher::new(&self.style, &self.default_config);
176        matcher.contributors_match(prev, current)
177    }
178
179    /// Replace the primary contributor with a substitution string.
180    ///
181    /// Used for subsequent author substitution (e.g., "———").
182    pub fn apply_author_substitution(&self, proc: &mut ProcTemplate, substitute: &str) {
183        self.with_bibliography_renderer(|renderer| {
184            renderer.apply_author_substitution(proc, substitute);
185        });
186    }
187
188    /// Render the bibliography to a string using a specific format.
189    pub fn render_bibliography_with_format<F>(&self) -> String
190    where
191        F: OutputFormat<Output = String>,
192    {
193        self.render_bibliography_with_format_and_annotations::<F>(None, None)
194    }
195
196    /// Render the bibliography to a string with annotations.
197    pub fn render_bibliography_with_format_and_annotations<F>(
198        &self,
199        annotations: Option<&HashMap<String, String>>,
200        annotation_style: Option<&AnnotationStyle>,
201    ) -> String
202    where
203        F: OutputFormat<Output = String>,
204    {
205        self.render_selected_bibliography_with_format_and_annotations::<F, _>(
206            self.bibliography.keys().cloned().collect::<Vec<_>>(),
207            annotations,
208            annotation_style,
209        )
210    }
211
212    /// Render a selected bibliography subset to a string using a specific format.
213    pub fn render_selected_bibliography_with_format<F, I>(&self, item_ids: I) -> String
214    where
215        F: OutputFormat<Output = String>,
216        I: IntoIterator<Item = String>,
217    {
218        self.render_selected_bibliography_with_format_and_annotations::<F, _>(item_ids, None, None)
219    }
220
221    /// Render a selected bibliography subset to a string with annotations.
222    ///
223    /// Orchestrates the choice between:
224    /// 1. Custom bibliography groups (selectors and headings).
225    /// 2. Automatic sort partitioning with sections (headings only).
226    /// 3. Standard flat rendering.
227    pub fn render_selected_bibliography_with_format_and_annotations<F, I>(
228        &self,
229        item_ids: I,
230        annotations: Option<&HashMap<String, String>>,
231        annotation_style: Option<&AnnotationStyle>,
232    ) -> String
233    where
234        F: OutputFormat<Output = String>,
235        I: IntoIterator<Item = String>,
236    {
237        let selected: HashSet<String> = item_ids.into_iter().collect();
238
239        // 1. Check for custom bibliography groups
240        if let Some(groups) = self
241            .style
242            .bibliography
243            .as_ref()
244            .filter(|bibliography| bibliography.groups_enabled)
245            .and_then(|bibliography| bibliography.groups.as_ref())
246        {
247            let all_entries = self.process_references().bibliography;
248            return self.render_with_custom_groups_filtered::<F>(
249                &all_entries,
250                groups,
251                &selected,
252                annotations,
253                annotation_style,
254            );
255        }
256
257        // 2. Check for automatic sort partitioning with sections
258        let bibliography_options = self.get_bibliography_options();
259        if let Some(partitioning) = bibliography_options.sort_partitioning.as_ref()
260            && crate::sort_partitioning::should_render_sections(partitioning)
261        {
262            self.initialize_numeric_bibliography_numbers();
263            let all_sorted = self.sort_references(self.bibliography.values().collect());
264            let selected_sorted: Vec<&Reference> = all_sorted
265                .into_iter()
266                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
267                .collect();
268            return self.render_with_partition_sections::<F>(
269                selected_sorted,
270                partitioning,
271                annotations,
272                annotation_style,
273            );
274        }
275
276        // 3. Fallback to flat rendering
277        self.initialize_numeric_bibliography_numbers();
278        let sorted_refs = self.sort_references(self.bibliography.values().collect());
279
280        let bibliography = self.process_sorted_refs::<_, F>(
281            sorted_refs
282                .iter()
283                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
284                .copied(),
285            |reference, entry_number| {
286                self.process_bibliography_entry_with_format::<F>(reference, entry_number)
287            },
288        );
289
290        let bibliography = self.merge_compound_entries::<F>(bibliography);
291        crate::render::refs_to_string_with_format::<F>(bibliography, annotations, annotation_style)
292    }
293
294    /// Render the entire bibliography to a formatted string.
295    pub fn render_bibliography(&self) -> String {
296        self.render_bibliography_with_format::<crate::render::plain::PlainText>()
297    }
298}