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>, Option<String>)>
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>, Option<String>)>
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                    renderer.bibliography_marker_with_format::<F>(reference, entry_number),
223                )
224            })
225            .collect()
226    }
227
228    /// Render numbered references across the rayon thread pool, preserving
229    /// input order.
230    ///
231    /// Builds a fresh `Renderer` per task from `ctx`'s `Arc`s (cheap; see
232    /// [`EntryRenderContext`] for why one `Renderer` cannot be shared across
233    /// threads). `par_iter` over a slice is order-preserving under
234    /// `collect`, so the subsequent-author-substitution post-pass sees the
235    /// same sequence it would under
236    /// [`render_numbered_refs_sequential`](Self::render_numbered_refs_sequential).
237    #[cfg(feature = "parallel")]
238    fn render_numbered_refs_parallel<'a, F>(
239        &self,
240        numbered_refs: &[(&'a Reference, usize)],
241        ctx: &EntryRenderContext<'_>,
242    ) -> Vec<(&'a Reference, Option<ProcTemplate>, Option<String>)>
243    where
244        F: OutputFormat<Output = String>,
245    {
246        use rayon::prelude::*;
247        numbered_refs
248            .par_iter()
249            .map(|&(reference, entry_number)| {
250                let renderer = self.entry_renderer(ctx);
251                (
252                    reference,
253                    renderer.process_bibliography_entry_with_format::<F>(reference, entry_number),
254                    renderer.bibliography_marker_with_format::<F>(reference, entry_number),
255                )
256            })
257            .collect()
258    }
259
260    /// Create a bibliography renderer with effective shared and bibliography-only config.
261    fn with_bibliography_renderer<T>(
262        &self,
263        run: &FinalizedRun,
264        render: impl FnOnce(Renderer<'_>) -> T,
265    ) -> T {
266        let bibliography_shared_config = self.get_bibliography_config();
267        let bibliography_config = self.get_bibliography_options().into_owned();
268        let renderer = Renderer::new(
269            RendererResources {
270                style: &self.style,
271                bibliography: &self.bibliography,
272                locale: &self.locale,
273                config: Arc::new(bibliography_shared_config.into_owned()),
274                bibliography_config: Some(Arc::new(bibliography_config)),
275                first_note_by_id: None,
276            },
277            &self.hints,
278            &run.state().citation_numbers,
279            CompoundRenderData {
280                set_by_ref: &self.compound_set_by_ref,
281                member_index: &self.compound_member_index,
282                sets: &self.compound_sets,
283            },
284            self.show_semantics,
285            self.inject_ast_indices,
286            self.abbreviation_map.as_ref(),
287        );
288
289        render(renderer)
290    }
291
292    /// Process sorted references and apply subsequent-author substitution.
293    ///
294    /// Returns bibliography entries with optional author substitution applied.
295    ///
296    /// This is the core iterator for flat bibliography rendering. Entry
297    /// numbers are resolved sequentially first (a single pass over `run`'s
298    /// shared `citation_numbers` map), then entries render via
299    /// [`render_numbered_refs`](Self::render_numbered_refs) — sequentially,
300    /// or across the rayon thread pool once the bibliography is large enough
301    /// (see [`PARALLEL_MIN_ENTRIES`]) — and finally
302    /// [`apply_substitution_post_pass`](Self::apply_substitution_post_pass)
303    /// walks the (order-preserved) results sequentially to apply
304    /// subsequent-author substitution, which depends on cite-order.
305    fn process_sorted_refs<'a, I, F>(&self, sorted_refs: I, run: &FinalizedRun) -> Vec<ProcEntry>
306    where
307        I: Iterator<Item = &'a Reference>,
308        F: OutputFormat<Output = String>,
309    {
310        let ctx = self.flat_render_context(run);
311        let numbered_refs = number_sorted_refs(sorted_refs, run);
312        let rendered = self.render_numbered_refs::<F>(&numbered_refs, &ctx);
313
314        let substitute = ctx
315            .bibliography_config
316            .subsequent_author_substitute
317            .as_ref();
318        self.apply_substitution_post_pass::<F>(rendered, substitute, &ctx)
319    }
320
321    /// Apply subsequent-author substitution to already-rendered entries and
322    /// assemble [`ProcEntry`]s, in order.
323    ///
324    /// This is the sequential part of bibliography rendering: substitution
325    /// depends on the *previous successfully rendered* reference, so it
326    /// cannot itself run in parallel. `rendered` must already be in final
327    /// bibliography order (as produced by
328    /// [`render_numbered_refs`](Self::render_numbered_refs), parallel or
329    /// not); entries whose render produced `None` are skipped entirely and
330    /// do not advance the "previous reference" used for contributor
331    /// matching.
332    fn apply_substitution_post_pass<F>(
333        &self,
334        rendered: Vec<(&Reference, Option<ProcTemplate>, Option<String>)>,
335        substitute: Option<&String>,
336        ctx: &EntryRenderContext<'_>,
337    ) -> Vec<ProcEntry>
338    where
339        F: OutputFormat<Output = String>,
340    {
341        let renderer = substitute.map(|_| self.entry_renderer(ctx));
342        let mut bibliography = Vec::with_capacity(rendered.len());
343        let mut previous_reference: Option<&Reference> = None;
344
345        for (reference, processed, marker) in rendered {
346            let Some(mut processed) = processed else {
347                continue;
348            };
349
350            if let Some(substitute_string) = substitute
351                && let Some(renderer) = renderer.as_ref()
352                && let Some(previous) = previous_reference
353                && self.contributors_match(previous, reference)
354            {
355                renderer
356                    .apply_author_substitution_with_format::<F>(&mut processed, substitute_string);
357            }
358
359            let ref_id = reference.id().unwrap_or_default().to_string();
360            bibliography.push(ProcEntry {
361                id: ref_id,
362                marker,
363                template: processed,
364                metadata: self.extract_metadata(reference, ctx),
365            });
366            previous_reference = Some(reference);
367        }
368
369        bibliography
370    }
371
372    /// Process all bibliography references and render them.
373    ///
374    /// This is a one-shot convenience wrapper: it begins a throwaway
375    /// [`super::run_state::RunState`] internally, so it has no continuity
376    /// with any citations processed elsewhere. Use
377    /// [`Processor::process_references_with_format`] with an explicit,
378    /// shared `FinalizedRun` to render a bibliography that reflects prior
379    /// citation registration in the same document.
380    pub fn process_references(&self) -> ProcessedReferences {
381        let run = self.begin_run().finalize();
382        self.process_references_with_format::<crate::render::plain::PlainText>(&run)
383    }
384
385    /// Process all bibliography references using the requested output format.
386    ///
387    /// This preserves format-specific inline markup in per-entry API output.
388    /// `run` should reflect all citations already processed for this
389    /// document (or be a fresh [`Processor::begin_run`] for a standalone
390    /// bibliography with no citations); see
391    /// [`Processor::process_references_with_format_standalone`] for a
392    /// one-shot convenience.
393    pub fn process_references_with_format<F>(&self, run: &FinalizedRun) -> ProcessedReferences
394    where
395        F: OutputFormat<Output = String>,
396    {
397        if self.style.bibliography.is_none() {
398            return ProcessedReferences::default();
399        }
400        let sorted_refs = self.sort_references(self.bibliography.values().collect());
401        let bibliography = self.process_sorted_refs::<_, F>(sorted_refs.iter().copied(), run);
402        ProcessedReferences {
403            bibliography,
404            citations: None,
405        }
406    }
407
408    /// Process only the selected bibliography entries, in bibliography sort order.
409    ///
410    /// Mirrors the flat path inside
411    /// [`render_selected_bibliography_with_format_and_annotations`] so that
412    /// per-entry `text` and subsequent-author substitution are computed against
413    /// the same subset that produced `content` — not the full loaded
414    /// bibliography. This matters for subsequent-author substitution: an uncited
415    /// predecessor must not cause the first cited entry to receive `———`.
416    pub(crate) fn process_selected_references_with_format<F, I>(
417        &self,
418        item_ids: I,
419        run: &FinalizedRun,
420    ) -> ProcessedReferences
421    where
422        F: OutputFormat<Output = String>,
423        I: IntoIterator<Item = String>,
424    {
425        if self.style.bibliography.is_none() {
426            return ProcessedReferences::default();
427        }
428        let selected: HashSet<String> = item_ids.into_iter().collect();
429        let sorted_refs = self.sort_references(self.bibliography.values().collect());
430        let bibliography = self.process_sorted_refs::<_, F>(
431            sorted_refs
432                .iter()
433                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
434                .copied(),
435            run,
436        );
437        ProcessedReferences {
438            bibliography,
439            citations: None,
440        }
441    }
442
443    /// Process and render a bibliography entry.
444    pub fn process_bibliography_entry(
445        &self,
446        reference: &Reference,
447        entry_number: usize,
448        run: &FinalizedRun,
449    ) -> Option<ProcTemplate> {
450        self.style.bibliography.as_ref()?;
451        self.with_bibliography_renderer(run, |renderer| {
452            renderer.process_bibliography_entry(reference, entry_number)
453        })
454    }
455
456    /// Process a bibliography entry with specific format.
457    pub fn process_bibliography_entry_with_format<F>(
458        &self,
459        reference: &Reference,
460        entry_number: usize,
461        run: &FinalizedRun,
462    ) -> Option<ProcTemplate>
463    where
464        F: OutputFormat<Output = String>,
465    {
466        self.style.bibliography.as_ref()?;
467        self.with_bibliography_renderer(run, |renderer| {
468            renderer.process_bibliography_entry_with_format::<F>(reference, entry_number)
469        })
470    }
471
472    /// Check whether primary contributors match between two references.
473    ///
474    /// Used for subsequent author substitution in bibliographies.
475    pub fn contributors_match(&self, prev: &Reference, current: &Reference) -> bool {
476        let config = self.style.options.as_ref().unwrap_or(&self.default_config);
477        let matcher = Matcher::new(&self.style, config, &self.locale);
478        matcher.contributors_match(prev, current)
479    }
480
481    /// Render the bibliography to a string using a specific format.
482    pub fn render_bibliography_with_format<F>(&self, run: &FinalizedRun) -> String
483    where
484        F: OutputFormat<Output = String>,
485    {
486        self.render_bibliography_with_format_and_annotations::<F>(None, None, run)
487    }
488
489    /// Render the bibliography to a string with annotations.
490    pub fn render_bibliography_with_format_and_annotations<F>(
491        &self,
492        annotations: Option<&HashMap<String, String>>,
493        annotation_style: Option<&AnnotationStyle>,
494        run: &FinalizedRun,
495    ) -> String
496    where
497        F: OutputFormat<Output = String>,
498    {
499        self.render_selected_bibliography_with_format_and_annotations::<F, _>(
500            self.bibliography.keys().cloned().collect::<Vec<_>>(),
501            annotations,
502            annotation_style,
503            run,
504        )
505    }
506
507    /// Render a selected bibliography subset to a string using a specific format.
508    pub fn render_selected_bibliography_with_format<F, I>(
509        &self,
510        item_ids: I,
511        run: &FinalizedRun,
512    ) -> String
513    where
514        F: OutputFormat<Output = String>,
515        I: IntoIterator<Item = String>,
516    {
517        self.render_selected_bibliography_with_format_and_annotations::<F, _>(
518            item_ids, None, None, run,
519        )
520    }
521
522    /// Render a selected bibliography subset to a string with annotations.
523    ///
524    /// Orchestrates the choice between:
525    /// 1. Custom bibliography groups (selectors and headings).
526    /// 2. Automatic sort partitioning with sections (headings only).
527    /// 3. Standard flat rendering.
528    pub fn render_selected_bibliography_with_format_and_annotations<F, I>(
529        &self,
530        item_ids: I,
531        annotations: Option<&HashMap<String, String>>,
532        annotation_style: Option<&AnnotationStyle>,
533        run: &FinalizedRun,
534    ) -> String
535    where
536        F: OutputFormat<Output = String>,
537        I: IntoIterator<Item = String>,
538    {
539        if self.style.bibliography.is_none() {
540            return String::new();
541        }
542        let selected: HashSet<String> = item_ids.into_iter().collect();
543
544        // 1. Check for custom bibliography groups
545        if let Some(groups) = self.effective_custom_groups() {
546            let all_entries = self.sorted_id_stubs();
547            return self.render_with_custom_groups_filtered::<F>(
548                &all_entries,
549                groups,
550                &selected,
551                annotations,
552                annotation_style,
553                run,
554            );
555        }
556
557        // 2. Check for automatic sort partitioning with sections
558        let bibliography_options = self.get_bibliography_options();
559        if let Some(partitioning) = bibliography_options.sort_partitioning.as_ref()
560            && crate::sort_partitioning::should_render_sections(partitioning)
561        {
562            let all_sorted = self.sort_references(self.bibliography.values().collect());
563            let selected_sorted: Vec<&Reference> = all_sorted
564                .into_iter()
565                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
566                .collect();
567            return self.render_with_partition_sections::<F>(
568                selected_sorted,
569                partitioning,
570                annotations,
571                annotation_style,
572                run,
573            );
574        }
575
576        // 3. Fallback to flat rendering
577        let sorted_refs = self.sort_references(self.bibliography.values().collect());
578
579        let bibliography = self.process_sorted_refs::<_, F>(
580            sorted_refs
581                .iter()
582                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
583                .copied(),
584            run,
585        );
586
587        let bibliography = self.merge_compound_entries::<F>(bibliography, run);
588        crate::render::refs_to_string_with_format::<F>(bibliography, annotations, annotation_style)
589    }
590
591    /// Render the entire bibliography to a formatted string.
592    ///
593    /// One-shot convenience wrapper: begins a throwaway run internally, so
594    /// it has no continuity with any citations processed elsewhere. Use
595    /// [`Processor::render_bibliography_with_format`] with an explicit,
596    /// shared `FinalizedRun` for a bibliography that reflects prior citation
597    /// registration.
598    pub fn render_bibliography(&self) -> String {
599        let run = self.begin_run().finalize();
600        self.render_bibliography_with_format::<crate::render::plain::PlainText>(&run)
601    }
602
603    /// One-shot convenience for [`Processor::process_references_with_format`]:
604    /// begins a throwaway run internally.
605    pub fn process_references_with_format_standalone<F>(&self) -> ProcessedReferences
606    where
607        F: OutputFormat<Output = String>,
608    {
609        let run = self.begin_run().finalize();
610        self.process_references_with_format::<F>(&run)
611    }
612
613    /// One-shot convenience for [`Processor::process_bibliography_entry`]:
614    /// begins a throwaway run internally.
615    pub fn process_bibliography_entry_standalone(
616        &self,
617        reference: &Reference,
618        entry_number: usize,
619    ) -> Option<ProcTemplate> {
620        let run = self.begin_run().finalize();
621        self.process_bibliography_entry(reference, entry_number, &run)
622    }
623
624    /// One-shot convenience for [`Processor::process_bibliography_entry_with_format`]:
625    /// begins a throwaway run internally.
626    pub fn process_bibliography_entry_with_format_standalone<F>(
627        &self,
628        reference: &Reference,
629        entry_number: usize,
630    ) -> Option<ProcTemplate>
631    where
632        F: OutputFormat<Output = String>,
633    {
634        let run = self.begin_run().finalize();
635        self.process_bibliography_entry_with_format::<F>(reference, entry_number, &run)
636    }
637
638    /// One-shot convenience for [`Processor::render_bibliography_with_format`]:
639    /// begins a throwaway run internally.
640    pub fn render_bibliography_with_format_standalone<F>(&self) -> String
641    where
642        F: OutputFormat<Output = String>,
643    {
644        let run = self.begin_run().finalize();
645        self.render_bibliography_with_format::<F>(&run)
646    }
647
648    /// One-shot convenience for
649    /// [`Processor::render_bibliography_with_format_and_annotations`]:
650    /// begins a throwaway run internally.
651    pub fn render_bibliography_with_format_and_annotations_standalone<F>(
652        &self,
653        annotations: Option<&HashMap<String, String>>,
654        annotation_style: Option<&AnnotationStyle>,
655    ) -> String
656    where
657        F: OutputFormat<Output = String>,
658    {
659        let run = self.begin_run().finalize();
660        self.render_bibliography_with_format_and_annotations::<F>(
661            annotations,
662            annotation_style,
663            &run,
664        )
665    }
666
667    /// One-shot convenience for [`Processor::render_selected_bibliography_with_format`]:
668    /// begins a throwaway run internally.
669    pub fn render_selected_bibliography_with_format_standalone<F, I>(&self, item_ids: I) -> String
670    where
671        F: OutputFormat<Output = String>,
672        I: IntoIterator<Item = String>,
673    {
674        let run = self.begin_run().finalize();
675        self.render_selected_bibliography_with_format::<F, I>(item_ids, &run)
676    }
677
678    /// One-shot convenience for
679    /// [`Processor::render_selected_bibliography_with_format_and_annotations`]:
680    /// begins a throwaway run internally.
681    pub fn render_selected_bibliography_with_format_and_annotations_standalone<F, I>(
682        &self,
683        item_ids: I,
684        annotations: Option<&HashMap<String, String>>,
685        annotation_style: Option<&AnnotationStyle>,
686    ) -> String
687    where
688        F: OutputFormat<Output = String>,
689        I: IntoIterator<Item = String>,
690    {
691        let run = self.begin_run().finalize();
692        self.render_selected_bibliography_with_format_and_annotations::<F, I>(
693            item_ids,
694            annotations,
695            annotation_style,
696            &run,
697        )
698    }
699}