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