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
15#[cfg(test)]
16#[allow(
17    clippy::unwrap_used,
18    clippy::expect_used,
19    clippy::panic,
20    clippy::indexing_slicing,
21    clippy::todo,
22    clippy::unimplemented,
23    clippy::unreachable,
24    clippy::get_unwrap,
25    reason = "Panicking is acceptable and often desired in tests."
26)]
27mod tests;
28
29use super::matching::Matcher;
30use super::rendering::{CompoundRenderData, Renderer, RendererResources};
31use super::run_state::FinalizedRun;
32use super::{ProcessedReferences, Processor};
33use crate::api::AnnotationStyle;
34use crate::reference::Reference;
35use crate::render::format::OutputFormat;
36use crate::render::{ProcEntry, ProcTemplate};
37use crate::values::ProcHints;
38use citum_schema::grouping::BibliographyGroup;
39use citum_schema::options::{Config, bibliography::BibliographyConfig};
40use std::collections::{HashMap, HashSet};
41use std::sync::Arc;
42
43/// Rendered bibliography block data for document integration.
44#[derive(Debug, Clone, Default)]
45pub(crate) struct RenderedBibliographyGroup {
46    /// The resolved group heading, if one exists.
47    pub(crate) heading: Option<String>,
48    /// The rendered bibliography body without any document-level heading wrapper.
49    pub(crate) body: String,
50    /// Individual entries rendered in this block.
51    pub(crate) entries: Vec<crate::render::ProcEntry>,
52}
53
54/// Combined document bibliography rendering output.
55///
56/// Returned by [`Processor::render_document_bibliography`] — the unified facade
57/// used by the batch, session, and document-string rendering paths. Both fields
58/// are computed from the same eligible subset so subsequent-author substitution
59/// stays consistent between the rendered string and the per-entry data.
60#[derive(Debug, Clone, Default)]
61pub(crate) struct DocumentBibliography {
62    /// The full rendered bibliography string for the document.
63    pub(crate) content: String,
64    /// Flat per-entry data, one entry per eligible reference.
65    pub(crate) entries: Vec<crate::render::ProcEntry>,
66}
67
68/// Bibliography entries render in parallel (behind the opt-in `parallel`
69/// feature) once a bibliography reaches this many entries; below the
70/// threshold, thread-pool dispatch overhead isn't worth paying and entries
71/// render sequentially instead. Measurements on an 8-core desktop found the
72/// parallel path performance-neutral at 10–400 entries (rendering is
73/// allocation-bound, not compute-bound); see
74/// `docs/specs/PARALLEL_BIBLIOGRAPHY_RENDERING.md` for the numbers.
75#[cfg(feature = "parallel")]
76pub(crate) const PARALLEL_MIN_ENTRIES: usize = 32;
77
78/// Resolve `(reference, entry_number)` pairs for `sorted_refs`, in order.
79///
80/// Reads each reference's already-assigned citation number from `run` when
81/// present — numeric styles pre-assign these at `begin_run`
82/// (`initialize_numeric_bibliography_numbers`) — and falls back to its
83/// 1-based position in `sorted_refs` otherwise. This is a sequential,
84/// read-only pass over `run`'s shared `citation_numbers` map, done up front
85/// so the render step that follows (parallel or not) is free of further
86/// lock contention.
87fn number_sorted_refs<'a>(
88    sorted_refs: impl Iterator<Item = &'a Reference>,
89    run: &FinalizedRun,
90) -> Vec<(&'a Reference, usize)> {
91    let numbers = run
92        .state()
93        .citation_numbers
94        .read()
95        .unwrap_or_else(std::sync::PoisonError::into_inner);
96    sorted_refs
97        .enumerate()
98        .map(|(index, reference)| {
99            let entry_number = numbers
100                .get(reference.id().unwrap_or_default().as_str())
101                .copied()
102                .unwrap_or(index + 1);
103            (reference, entry_number)
104        })
105        .collect()
106}
107
108/// Resources needed to build a per-entry [`Renderer`] for one bibliography
109/// render pass (flat or grouped), resolved and `Arc`-wrapped once per pass.
110///
111/// Hoisting the config merge and `Arc` construction out of the per-entry
112/// loop matters twice over: it removes an O(entries) deep-clone cost from
113/// the sequential path (the follow-up deferred in bean `csl26-qi7l`), and
114/// it keeps the parallel path (see
115/// [`render_numbered_refs_parallel`](Processor::render_numbered_refs_parallel))
116/// from hammering the allocator with per-entry config clones across
117/// threads. Each parallel task clones only the `Arc`s into a fresh
118/// `Renderer`, never sharing one across threads — `Renderer` holds a
119/// per-render scratch `RefCell` (`filtered_to_original_index`) that is
120/// intentionally not `Sync`.
121struct EntryRenderContext<'a> {
122    /// The style to render with (group-overridden for grouped passes).
123    style: &'a citum_schema::Style,
124    /// Pre-calculated processing hints, group-scoped when applicable.
125    hints: &'a HashMap<String, ProcHints>,
126    /// The effective shared configuration, merged once per pass.
127    config: Arc<Config>,
128    /// The effective bibliography-only configuration, merged once per pass.
129    bibliography_config: Arc<BibliographyConfig>,
130    /// The finalized run providing citation numbers and note-order state.
131    run: &'a FinalizedRun,
132}
133
134impl Processor {
135    /// Return the manual bibliography groups that are currently enabled.
136    ///
137    /// Keeping the `groups_enabled` gate here ensures every bibliography
138    /// rendering surface interprets a retained but disabled `groups:` block
139    /// identically.
140    fn effective_custom_groups(&self) -> Option<&[BibliographyGroup]> {
141        self.style
142            .bibliography
143            .as_ref()
144            .filter(|bibliography| bibliography.groups_enabled)
145            .and_then(|bibliography| bibliography.groups.as_deref())
146    }
147
148    /// Build the [`EntryRenderContext`] for a flat (ungrouped) bibliography
149    /// pass: processor-level style, hints, and merged configs.
150    fn flat_render_context<'a>(&'a self, run: &'a FinalizedRun) -> EntryRenderContext<'a> {
151        EntryRenderContext {
152            style: &self.style,
153            hints: &self.hints,
154            config: Arc::new(self.get_bibliography_config().into_owned()),
155            bibliography_config: Arc::new(self.get_bibliography_options().into_owned()),
156            run,
157        }
158    }
159
160    /// Build the `Renderer` used for one bibliography entry.
161    fn entry_renderer<'a>(&'a self, ctx: &EntryRenderContext<'a>) -> Renderer<'a> {
162        Renderer::new(
163            RendererResources {
164                style: ctx.style,
165                bibliography: &self.bibliography,
166                locale: &self.locale,
167                config: ctx.config.clone(),
168                bibliography_config: Some(ctx.bibliography_config.clone()),
169                first_note_by_id: None,
170            },
171            ctx.hints,
172            &ctx.run.state().citation_numbers,
173            CompoundRenderData {
174                set_by_ref: &self.compound_set_by_ref,
175                member_index: &self.compound_member_index,
176                sets: &self.compound_sets,
177            },
178            self.show_semantics,
179            self.inject_ast_indices,
180            self.abbreviation_map.as_ref(),
181        )
182    }
183
184    /// Choose the sequential or parallel render path for `numbered_refs`
185    /// and apply it.
186    ///
187    /// Parallel rendering requires both the `parallel` feature and
188    /// `numbered_refs.len() >= PARALLEL_MIN_ENTRIES`; otherwise this falls
189    /// back to the single-shared-`Renderer` sequential path.
190    fn render_numbered_refs<'a, F>(
191        &self,
192        numbered_refs: &[(&'a Reference, usize)],
193        ctx: &EntryRenderContext<'_>,
194    ) -> Vec<(&'a Reference, Option<ProcTemplate>)>
195    where
196        F: OutputFormat<Output = String>,
197    {
198        #[cfg(feature = "parallel")]
199        if numbered_refs.len() >= PARALLEL_MIN_ENTRIES {
200            return self.render_numbered_refs_parallel::<F>(numbered_refs, ctx);
201        }
202        self.render_numbered_refs_sequential::<F>(numbered_refs, ctx)
203    }
204
205    /// Render numbered references through one shared `Renderer`, preserving
206    /// input order.
207    fn render_numbered_refs_sequential<'a, F>(
208        &self,
209        numbered_refs: &[(&'a Reference, usize)],
210        ctx: &EntryRenderContext<'_>,
211    ) -> Vec<(&'a Reference, Option<ProcTemplate>)>
212    where
213        F: OutputFormat<Output = String>,
214    {
215        let renderer = self.entry_renderer(ctx);
216        numbered_refs
217            .iter()
218            .map(|&(reference, entry_number)| {
219                (
220                    reference,
221                    renderer.process_bibliography_entry_with_format::<F>(reference, entry_number),
222                )
223            })
224            .collect()
225    }
226
227    /// Render numbered references across the rayon thread pool, preserving
228    /// input order.
229    ///
230    /// Builds a fresh `Renderer` per task from `ctx`'s `Arc`s (cheap; see
231    /// [`EntryRenderContext`] for why one `Renderer` cannot be shared across
232    /// threads). `par_iter` over a slice is order-preserving under
233    /// `collect`, so the subsequent-author-substitution post-pass sees the
234    /// same sequence it would under
235    /// [`render_numbered_refs_sequential`](Self::render_numbered_refs_sequential).
236    #[cfg(feature = "parallel")]
237    fn render_numbered_refs_parallel<'a, F>(
238        &self,
239        numbered_refs: &[(&'a Reference, usize)],
240        ctx: &EntryRenderContext<'_>,
241    ) -> Vec<(&'a Reference, Option<ProcTemplate>)>
242    where
243        F: OutputFormat<Output = String>,
244    {
245        use rayon::prelude::*;
246        numbered_refs
247            .par_iter()
248            .map(|&(reference, entry_number)| {
249                let renderer = self.entry_renderer(ctx);
250                (
251                    reference,
252                    renderer.process_bibliography_entry_with_format::<F>(reference, entry_number),
253                )
254            })
255            .collect()
256    }
257
258    /// Create a bibliography renderer with effective shared and bibliography-only config.
259    fn with_bibliography_renderer<T>(
260        &self,
261        run: &FinalizedRun,
262        render: impl FnOnce(Renderer<'_>) -> T,
263    ) -> T {
264        let bibliography_shared_config = self.get_bibliography_config();
265        let bibliography_config = self.get_bibliography_options().into_owned();
266        let renderer = Renderer::new(
267            RendererResources {
268                style: &self.style,
269                bibliography: &self.bibliography,
270                locale: &self.locale,
271                config: Arc::new(bibliography_shared_config.into_owned()),
272                bibliography_config: Some(Arc::new(bibliography_config)),
273                first_note_by_id: None,
274            },
275            &self.hints,
276            &run.state().citation_numbers,
277            CompoundRenderData {
278                set_by_ref: &self.compound_set_by_ref,
279                member_index: &self.compound_member_index,
280                sets: &self.compound_sets,
281            },
282            self.show_semantics,
283            self.inject_ast_indices,
284            self.abbreviation_map.as_ref(),
285        );
286
287        render(renderer)
288    }
289
290    /// Process sorted references and apply subsequent-author substitution.
291    ///
292    /// Returns bibliography entries with optional author substitution applied.
293    ///
294    /// This is the core iterator for flat bibliography rendering. Entry
295    /// numbers are resolved sequentially first (a single pass over `run`'s
296    /// shared `citation_numbers` map), then entries render via
297    /// [`render_numbered_refs`](Self::render_numbered_refs) — sequentially,
298    /// or across the rayon thread pool once the bibliography is large enough
299    /// (see [`PARALLEL_MIN_ENTRIES`]) — and finally
300    /// [`apply_substitution_post_pass`](Self::apply_substitution_post_pass)
301    /// walks the (order-preserved) results sequentially to apply
302    /// subsequent-author substitution, which depends on cite-order.
303    fn process_sorted_refs<'a, I, F>(&self, sorted_refs: I, run: &FinalizedRun) -> Vec<ProcEntry>
304    where
305        I: Iterator<Item = &'a Reference>,
306        F: OutputFormat<Output = String>,
307    {
308        let ctx = self.flat_render_context(run);
309        let numbered_refs = number_sorted_refs(sorted_refs, run);
310        let rendered = self.render_numbered_refs::<F>(&numbered_refs, &ctx);
311
312        let substitute = ctx
313            .bibliography_config
314            .subsequent_author_substitute
315            .as_ref();
316        self.apply_substitution_post_pass::<F>(rendered, substitute, &ctx)
317    }
318
319    /// Apply subsequent-author substitution to already-rendered entries and
320    /// assemble [`ProcEntry`]s, in order.
321    ///
322    /// This is the sequential part of bibliography rendering: substitution
323    /// depends on the *previous successfully rendered* reference, so it
324    /// cannot itself run in parallel. `rendered` must already be in final
325    /// bibliography order (as produced by
326    /// [`render_numbered_refs`](Self::render_numbered_refs), parallel or
327    /// not); entries whose render produced `None` are skipped entirely and
328    /// do not advance the "previous reference" used for contributor
329    /// matching.
330    fn apply_substitution_post_pass<F>(
331        &self,
332        rendered: Vec<(&Reference, Option<ProcTemplate>)>,
333        substitute: Option<&String>,
334        ctx: &EntryRenderContext<'_>,
335    ) -> Vec<ProcEntry>
336    where
337        F: OutputFormat<Output = String>,
338    {
339        let renderer = substitute.map(|_| self.entry_renderer(ctx));
340        let mut bibliography = Vec::with_capacity(rendered.len());
341        let mut previous_reference: Option<&Reference> = None;
342
343        for (reference, processed) in rendered {
344            let Some(mut processed) = processed else {
345                continue;
346            };
347
348            if let Some(substitute_string) = substitute
349                && let Some(renderer) = renderer.as_ref()
350                && let Some(previous) = previous_reference
351                && self.contributors_match(previous, reference)
352            {
353                renderer
354                    .apply_author_substitution_with_format::<F>(&mut processed, substitute_string);
355            }
356
357            let ref_id = reference.id().unwrap_or_default().to_string();
358            bibliography.push(ProcEntry {
359                id: ref_id,
360                template: processed,
361                metadata: self.extract_metadata(reference, ctx),
362            });
363            previous_reference = Some(reference);
364        }
365
366        bibliography
367    }
368
369    /// Process all bibliography references and render them.
370    ///
371    /// This is a one-shot convenience wrapper: it begins a throwaway
372    /// [`super::run_state::RunState`] internally, so it has no continuity
373    /// with any citations processed elsewhere. Use
374    /// [`Processor::process_references_with_format`] with an explicit,
375    /// shared `FinalizedRun` to render a bibliography that reflects prior
376    /// citation registration in the same document.
377    pub fn process_references(&self) -> ProcessedReferences {
378        let run = self.begin_run().finalize();
379        self.process_references_with_format::<crate::render::plain::PlainText>(&run)
380    }
381
382    /// Process all bibliography references using the requested output format.
383    ///
384    /// This preserves format-specific inline markup in per-entry API output.
385    /// `run` should reflect all citations already processed for this
386    /// document (or be a fresh [`Processor::begin_run`] for a standalone
387    /// bibliography with no citations); see
388    /// [`Processor::process_references_with_format_standalone`] for a
389    /// one-shot convenience.
390    pub fn process_references_with_format<F>(&self, run: &FinalizedRun) -> ProcessedReferences
391    where
392        F: OutputFormat<Output = String>,
393    {
394        if self.style.bibliography.is_none() {
395            return ProcessedReferences::default();
396        }
397        let sorted_refs = self.sort_references(self.bibliography.values().collect());
398        let bibliography = self.process_sorted_refs::<_, F>(sorted_refs.iter().copied(), run);
399        ProcessedReferences {
400            bibliography,
401            citations: None,
402        }
403    }
404
405    /// Process only the selected bibliography entries, in bibliography sort order.
406    ///
407    /// Mirrors the flat path inside
408    /// [`render_selected_bibliography_with_format_and_annotations`] so that
409    /// per-entry `text` and subsequent-author substitution are computed against
410    /// the same subset that produced `content` — not the full loaded
411    /// bibliography. This matters for subsequent-author substitution: an uncited
412    /// predecessor must not cause the first cited entry to receive `———`.
413    pub(crate) fn process_selected_references_with_format<F, I>(
414        &self,
415        item_ids: I,
416        run: &FinalizedRun,
417    ) -> ProcessedReferences
418    where
419        F: OutputFormat<Output = String>,
420        I: IntoIterator<Item = String>,
421    {
422        if self.style.bibliography.is_none() {
423            return ProcessedReferences::default();
424        }
425        let selected: HashSet<String> = item_ids.into_iter().collect();
426        let sorted_refs = self.sort_references(self.bibliography.values().collect());
427        let bibliography = self.process_sorted_refs::<_, F>(
428            sorted_refs
429                .iter()
430                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
431                .copied(),
432            run,
433        );
434        ProcessedReferences {
435            bibliography,
436            citations: None,
437        }
438    }
439
440    /// Process and render a bibliography entry.
441    pub fn process_bibliography_entry(
442        &self,
443        reference: &Reference,
444        entry_number: usize,
445        run: &FinalizedRun,
446    ) -> Option<ProcTemplate> {
447        self.style.bibliography.as_ref()?;
448        self.with_bibliography_renderer(run, |renderer| {
449            renderer.process_bibliography_entry(reference, entry_number)
450        })
451    }
452
453    /// Process a bibliography entry with specific format.
454    pub fn process_bibliography_entry_with_format<F>(
455        &self,
456        reference: &Reference,
457        entry_number: usize,
458        run: &FinalizedRun,
459    ) -> Option<ProcTemplate>
460    where
461        F: OutputFormat<Output = String>,
462    {
463        self.style.bibliography.as_ref()?;
464        self.with_bibliography_renderer(run, |renderer| {
465            renderer.process_bibliography_entry_with_format::<F>(reference, entry_number)
466        })
467    }
468
469    /// Check whether primary contributors match between two references.
470    ///
471    /// Used for subsequent author substitution in bibliographies.
472    pub fn contributors_match(&self, prev: &Reference, current: &Reference) -> bool {
473        let config = self.style.options.as_ref().unwrap_or(&self.default_config);
474        let matcher = Matcher::new(&self.style, config, &self.locale);
475        matcher.contributors_match(prev, current)
476    }
477
478    /// Render the bibliography to a string using a specific format.
479    pub fn render_bibliography_with_format<F>(&self, run: &FinalizedRun) -> String
480    where
481        F: OutputFormat<Output = String>,
482    {
483        self.render_bibliography_with_format_and_annotations::<F>(None, None, run)
484    }
485
486    /// Render the bibliography to a string with annotations.
487    pub fn render_bibliography_with_format_and_annotations<F>(
488        &self,
489        annotations: Option<&HashMap<String, String>>,
490        annotation_style: Option<&AnnotationStyle>,
491        run: &FinalizedRun,
492    ) -> String
493    where
494        F: OutputFormat<Output = String>,
495    {
496        self.render_selected_bibliography_with_format_and_annotations::<F, _>(
497            self.bibliography.keys().cloned().collect::<Vec<_>>(),
498            annotations,
499            annotation_style,
500            run,
501        )
502    }
503
504    /// Render a selected bibliography subset to a string using a specific format.
505    pub fn render_selected_bibliography_with_format<F, I>(
506        &self,
507        item_ids: I,
508        run: &FinalizedRun,
509    ) -> String
510    where
511        F: OutputFormat<Output = String>,
512        I: IntoIterator<Item = String>,
513    {
514        self.render_selected_bibliography_with_format_and_annotations::<F, _>(
515            item_ids, None, None, run,
516        )
517    }
518
519    /// Render a selected bibliography subset to a string with annotations.
520    ///
521    /// Orchestrates the choice between:
522    /// 1. Custom bibliography groups (selectors and headings).
523    /// 2. Automatic sort partitioning with sections (headings only).
524    /// 3. Standard flat rendering.
525    pub fn render_selected_bibliography_with_format_and_annotations<F, I>(
526        &self,
527        item_ids: I,
528        annotations: Option<&HashMap<String, String>>,
529        annotation_style: Option<&AnnotationStyle>,
530        run: &FinalizedRun,
531    ) -> String
532    where
533        F: OutputFormat<Output = String>,
534        I: IntoIterator<Item = String>,
535    {
536        if self.style.bibliography.is_none() {
537            return String::new();
538        }
539        let selected: HashSet<String> = item_ids.into_iter().collect();
540
541        // 1. Check for custom bibliography groups
542        if let Some(groups) = self.effective_custom_groups() {
543            let all_entries = self.sorted_id_stubs();
544            return self.render_with_custom_groups_filtered::<F>(
545                &all_entries,
546                groups,
547                &selected,
548                annotations,
549                annotation_style,
550                run,
551            );
552        }
553
554        // 2. Check for automatic sort partitioning with sections
555        let bibliography_options = self.get_bibliography_options();
556        if let Some(partitioning) = bibliography_options.sort_partitioning.as_ref()
557            && crate::sort_partitioning::should_render_sections(partitioning)
558        {
559            let all_sorted = self.sort_references(self.bibliography.values().collect());
560            let selected_sorted: Vec<&Reference> = all_sorted
561                .into_iter()
562                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
563                .collect();
564            return self.render_with_partition_sections::<F>(
565                selected_sorted,
566                partitioning,
567                annotations,
568                annotation_style,
569                run,
570            );
571        }
572
573        // 3. Fallback to flat rendering
574        let sorted_refs = self.sort_references(self.bibliography.values().collect());
575
576        let bibliography = self.process_sorted_refs::<_, F>(
577            sorted_refs
578                .iter()
579                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
580                .copied(),
581            run,
582        );
583
584        let bibliography = self.merge_compound_entries::<F>(bibliography, run);
585        crate::render::refs_to_string_with_format::<F>(bibliography, annotations, annotation_style)
586    }
587
588    /// Render the entire bibliography to a formatted string.
589    ///
590    /// One-shot convenience wrapper: begins a throwaway run internally, so
591    /// it has no continuity with any citations processed elsewhere. Use
592    /// [`Processor::render_bibliography_with_format`] with an explicit,
593    /// shared `FinalizedRun` for a bibliography that reflects prior citation
594    /// registration.
595    pub fn render_bibliography(&self) -> String {
596        let run = self.begin_run().finalize();
597        self.render_bibliography_with_format::<crate::render::plain::PlainText>(&run)
598    }
599
600    /// One-shot convenience for [`Processor::process_references_with_format`]:
601    /// begins a throwaway run internally.
602    pub fn process_references_with_format_standalone<F>(&self) -> ProcessedReferences
603    where
604        F: OutputFormat<Output = String>,
605    {
606        let run = self.begin_run().finalize();
607        self.process_references_with_format::<F>(&run)
608    }
609
610    /// One-shot convenience for [`Processor::process_bibliography_entry`]:
611    /// begins a throwaway run internally.
612    pub fn process_bibliography_entry_standalone(
613        &self,
614        reference: &Reference,
615        entry_number: usize,
616    ) -> Option<ProcTemplate> {
617        let run = self.begin_run().finalize();
618        self.process_bibliography_entry(reference, entry_number, &run)
619    }
620
621    /// One-shot convenience for [`Processor::process_bibliography_entry_with_format`]:
622    /// begins a throwaway run internally.
623    pub fn process_bibliography_entry_with_format_standalone<F>(
624        &self,
625        reference: &Reference,
626        entry_number: usize,
627    ) -> Option<ProcTemplate>
628    where
629        F: OutputFormat<Output = String>,
630    {
631        let run = self.begin_run().finalize();
632        self.process_bibliography_entry_with_format::<F>(reference, entry_number, &run)
633    }
634
635    /// One-shot convenience for [`Processor::render_bibliography_with_format`]:
636    /// begins a throwaway run internally.
637    pub fn render_bibliography_with_format_standalone<F>(&self) -> String
638    where
639        F: OutputFormat<Output = String>,
640    {
641        let run = self.begin_run().finalize();
642        self.render_bibliography_with_format::<F>(&run)
643    }
644
645    /// One-shot convenience for
646    /// [`Processor::render_bibliography_with_format_and_annotations`]:
647    /// begins a throwaway run internally.
648    pub fn render_bibliography_with_format_and_annotations_standalone<F>(
649        &self,
650        annotations: Option<&HashMap<String, String>>,
651        annotation_style: Option<&AnnotationStyle>,
652    ) -> String
653    where
654        F: OutputFormat<Output = String>,
655    {
656        let run = self.begin_run().finalize();
657        self.render_bibliography_with_format_and_annotations::<F>(
658            annotations,
659            annotation_style,
660            &run,
661        )
662    }
663
664    /// One-shot convenience for [`Processor::render_selected_bibliography_with_format`]:
665    /// begins a throwaway run internally.
666    pub fn render_selected_bibliography_with_format_standalone<F, I>(&self, item_ids: I) -> String
667    where
668        F: OutputFormat<Output = String>,
669        I: IntoIterator<Item = String>,
670    {
671        let run = self.begin_run().finalize();
672        self.render_selected_bibliography_with_format::<F, I>(item_ids, &run)
673    }
674
675    /// One-shot convenience for
676    /// [`Processor::render_selected_bibliography_with_format_and_annotations`]:
677    /// begins a throwaway run internally.
678    pub fn render_selected_bibliography_with_format_and_annotations_standalone<F, I>(
679        &self,
680        item_ids: I,
681        annotations: Option<&HashMap<String, String>>,
682        annotation_style: Option<&AnnotationStyle>,
683    ) -> String
684    where
685        F: OutputFormat<Output = String>,
686        I: IntoIterator<Item = String>,
687    {
688        let run = self.begin_run().finalize();
689        self.render_selected_bibliography_with_format_and_annotations::<F, I>(
690            item_ids,
691            annotations,
692            annotation_style,
693            &run,
694        )
695    }
696}