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        let sorted_refs = self.sort_references(self.bibliography.values().collect());
395        let bibliography = self.process_sorted_refs::<_, F>(sorted_refs.iter().copied(), run);
396        ProcessedReferences {
397            bibliography,
398            citations: None,
399        }
400    }
401
402    /// Process only the selected bibliography entries, in bibliography sort order.
403    ///
404    /// Mirrors the flat path inside
405    /// [`render_selected_bibliography_with_format_and_annotations`] so that
406    /// per-entry `text` and subsequent-author substitution are computed against
407    /// the same subset that produced `content` — not the full loaded
408    /// bibliography. This matters for subsequent-author substitution: an uncited
409    /// predecessor must not cause the first cited entry to receive `———`.
410    pub(crate) fn process_selected_references_with_format<F, I>(
411        &self,
412        item_ids: I,
413        run: &FinalizedRun,
414    ) -> ProcessedReferences
415    where
416        F: OutputFormat<Output = String>,
417        I: IntoIterator<Item = String>,
418    {
419        let selected: HashSet<String> = item_ids.into_iter().collect();
420        let sorted_refs = self.sort_references(self.bibliography.values().collect());
421        let bibliography = self.process_sorted_refs::<_, F>(
422            sorted_refs
423                .iter()
424                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
425                .copied(),
426            run,
427        );
428        ProcessedReferences {
429            bibliography,
430            citations: None,
431        }
432    }
433
434    /// Process and render a bibliography entry.
435    pub fn process_bibliography_entry(
436        &self,
437        reference: &Reference,
438        entry_number: usize,
439        run: &FinalizedRun,
440    ) -> Option<ProcTemplate> {
441        self.with_bibliography_renderer(run, |renderer| {
442            renderer.process_bibliography_entry(reference, entry_number)
443        })
444    }
445
446    /// Process a bibliography entry with specific format.
447    pub fn process_bibliography_entry_with_format<F>(
448        &self,
449        reference: &Reference,
450        entry_number: usize,
451        run: &FinalizedRun,
452    ) -> Option<ProcTemplate>
453    where
454        F: OutputFormat<Output = String>,
455    {
456        self.with_bibliography_renderer(run, |renderer| {
457            renderer.process_bibliography_entry_with_format::<F>(reference, entry_number)
458        })
459    }
460
461    /// Check whether primary contributors match between two references.
462    ///
463    /// Used for subsequent author substitution in bibliographies.
464    pub fn contributors_match(&self, prev: &Reference, current: &Reference) -> bool {
465        let matcher = Matcher::new(&self.style, &self.default_config);
466        matcher.contributors_match(prev, current)
467    }
468
469    /// Render the bibliography to a string using a specific format.
470    pub fn render_bibliography_with_format<F>(&self, run: &FinalizedRun) -> String
471    where
472        F: OutputFormat<Output = String>,
473    {
474        self.render_bibliography_with_format_and_annotations::<F>(None, None, run)
475    }
476
477    /// Render the bibliography to a string with annotations.
478    pub fn render_bibliography_with_format_and_annotations<F>(
479        &self,
480        annotations: Option<&HashMap<String, String>>,
481        annotation_style: Option<&AnnotationStyle>,
482        run: &FinalizedRun,
483    ) -> String
484    where
485        F: OutputFormat<Output = String>,
486    {
487        self.render_selected_bibliography_with_format_and_annotations::<F, _>(
488            self.bibliography.keys().cloned().collect::<Vec<_>>(),
489            annotations,
490            annotation_style,
491            run,
492        )
493    }
494
495    /// Render a selected bibliography subset to a string using a specific format.
496    pub fn render_selected_bibliography_with_format<F, I>(
497        &self,
498        item_ids: I,
499        run: &FinalizedRun,
500    ) -> String
501    where
502        F: OutputFormat<Output = String>,
503        I: IntoIterator<Item = String>,
504    {
505        self.render_selected_bibliography_with_format_and_annotations::<F, _>(
506            item_ids, None, None, run,
507        )
508    }
509
510    /// Render a selected bibliography subset to a string with annotations.
511    ///
512    /// Orchestrates the choice between:
513    /// 1. Custom bibliography groups (selectors and headings).
514    /// 2. Automatic sort partitioning with sections (headings only).
515    /// 3. Standard flat rendering.
516    pub fn render_selected_bibliography_with_format_and_annotations<F, I>(
517        &self,
518        item_ids: I,
519        annotations: Option<&HashMap<String, String>>,
520        annotation_style: Option<&AnnotationStyle>,
521        run: &FinalizedRun,
522    ) -> String
523    where
524        F: OutputFormat<Output = String>,
525        I: IntoIterator<Item = String>,
526    {
527        let selected: HashSet<String> = item_ids.into_iter().collect();
528
529        // 1. Check for custom bibliography groups
530        if let Some(groups) = self.effective_custom_groups() {
531            let all_entries = self.sorted_id_stubs();
532            return self.render_with_custom_groups_filtered::<F>(
533                &all_entries,
534                groups,
535                &selected,
536                annotations,
537                annotation_style,
538                run,
539            );
540        }
541
542        // 2. Check for automatic sort partitioning with sections
543        let bibliography_options = self.get_bibliography_options();
544        if let Some(partitioning) = bibliography_options.sort_partitioning.as_ref()
545            && crate::sort_partitioning::should_render_sections(partitioning)
546        {
547            let all_sorted = self.sort_references(self.bibliography.values().collect());
548            let selected_sorted: Vec<&Reference> = all_sorted
549                .into_iter()
550                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
551                .collect();
552            return self.render_with_partition_sections::<F>(
553                selected_sorted,
554                partitioning,
555                annotations,
556                annotation_style,
557                run,
558            );
559        }
560
561        // 3. Fallback to flat rendering
562        let sorted_refs = self.sort_references(self.bibliography.values().collect());
563
564        let bibliography = self.process_sorted_refs::<_, F>(
565            sorted_refs
566                .iter()
567                .filter(|r| r.id().as_deref().is_some_and(|id| selected.contains(id)))
568                .copied(),
569            run,
570        );
571
572        let bibliography = self.merge_compound_entries::<F>(bibliography, run);
573        crate::render::refs_to_string_with_format::<F>(bibliography, annotations, annotation_style)
574    }
575
576    /// Render the entire bibliography to a formatted string.
577    ///
578    /// One-shot convenience wrapper: begins a throwaway run internally, so
579    /// it has no continuity with any citations processed elsewhere. Use
580    /// [`Processor::render_bibliography_with_format`] with an explicit,
581    /// shared `FinalizedRun` for a bibliography that reflects prior citation
582    /// registration.
583    pub fn render_bibliography(&self) -> String {
584        let run = self.begin_run().finalize();
585        self.render_bibliography_with_format::<crate::render::plain::PlainText>(&run)
586    }
587
588    /// One-shot convenience for [`Processor::process_references_with_format`]:
589    /// begins a throwaway run internally.
590    pub fn process_references_with_format_standalone<F>(&self) -> ProcessedReferences
591    where
592        F: OutputFormat<Output = String>,
593    {
594        let run = self.begin_run().finalize();
595        self.process_references_with_format::<F>(&run)
596    }
597
598    /// One-shot convenience for [`Processor::process_bibliography_entry`]:
599    /// begins a throwaway run internally.
600    pub fn process_bibliography_entry_standalone(
601        &self,
602        reference: &Reference,
603        entry_number: usize,
604    ) -> Option<ProcTemplate> {
605        let run = self.begin_run().finalize();
606        self.process_bibliography_entry(reference, entry_number, &run)
607    }
608
609    /// One-shot convenience for [`Processor::process_bibliography_entry_with_format`]:
610    /// begins a throwaway run internally.
611    pub fn process_bibliography_entry_with_format_standalone<F>(
612        &self,
613        reference: &Reference,
614        entry_number: usize,
615    ) -> Option<ProcTemplate>
616    where
617        F: OutputFormat<Output = String>,
618    {
619        let run = self.begin_run().finalize();
620        self.process_bibliography_entry_with_format::<F>(reference, entry_number, &run)
621    }
622
623    /// One-shot convenience for [`Processor::render_bibliography_with_format`]:
624    /// begins a throwaway run internally.
625    pub fn render_bibliography_with_format_standalone<F>(&self) -> String
626    where
627        F: OutputFormat<Output = String>,
628    {
629        let run = self.begin_run().finalize();
630        self.render_bibliography_with_format::<F>(&run)
631    }
632
633    /// One-shot convenience for
634    /// [`Processor::render_bibliography_with_format_and_annotations`]:
635    /// begins a throwaway run internally.
636    pub fn render_bibliography_with_format_and_annotations_standalone<F>(
637        &self,
638        annotations: Option<&HashMap<String, String>>,
639        annotation_style: Option<&AnnotationStyle>,
640    ) -> String
641    where
642        F: OutputFormat<Output = String>,
643    {
644        let run = self.begin_run().finalize();
645        self.render_bibliography_with_format_and_annotations::<F>(
646            annotations,
647            annotation_style,
648            &run,
649        )
650    }
651
652    /// One-shot convenience for [`Processor::render_selected_bibliography_with_format`]:
653    /// begins a throwaway run internally.
654    pub fn render_selected_bibliography_with_format_standalone<F, I>(&self, item_ids: I) -> String
655    where
656        F: OutputFormat<Output = String>,
657        I: IntoIterator<Item = String>,
658    {
659        let run = self.begin_run().finalize();
660        self.render_selected_bibliography_with_format::<F, I>(item_ids, &run)
661    }
662
663    /// One-shot convenience for
664    /// [`Processor::render_selected_bibliography_with_format_and_annotations`]:
665    /// begins a throwaway run internally.
666    pub fn render_selected_bibliography_with_format_and_annotations_standalone<F, I>(
667        &self,
668        item_ids: I,
669        annotations: Option<&HashMap<String, String>>,
670        annotation_style: Option<&AnnotationStyle>,
671    ) -> String
672    where
673        F: OutputFormat<Output = String>,
674        I: IntoIterator<Item = String>,
675    {
676        let run = self.begin_run().finalize();
677        self.render_selected_bibliography_with_format_and_annotations::<F, I>(
678            item_ids,
679            annotations,
680            annotation_style,
681            &run,
682        )
683    }
684}