Skip to main content

typst_library/model/
bibliography.rs

1use std::any::TypeId;
2use std::fmt::{self, Debug, Formatter};
3use std::num::NonZeroUsize;
4use std::path::Path;
5use std::sync::{Arc, LazyLock};
6
7use comemo::{Track, Tracked, TrackedMut};
8use ecow::{EcoString, EcoVec, eco_format, eco_vec};
9use hayagriva::archive::ArchivedStyle;
10use hayagriva::io::BibLaTeXError;
11use hayagriva::{
12    BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, Library,
13    SpecificLocator, TransparentLocator, citationberg,
14};
15use indexmap::IndexMap;
16use rustc_hash::{FxBuildHasher, FxHashMap};
17use smallvec::SmallVec;
18use typst_syntax::{Span, Spanned, SyntaxMode};
19use typst_utils::{
20    LazyHash, ManuallyHash, NonZeroExt, PicoStr, Protected, ResolvedPicoStr,
21};
22
23use crate::World;
24use crate::diag::{
25    At, HintedStrResult, HintedString, LoadError, LoadResult, LoadedWithin,
26    ReportTextPos, SourceDiagnostic, SourceResult, StrResult, bail, error, warning,
27};
28use crate::engine::{Engine, Route, Sink, Traced};
29use crate::foundations::{
30    Bytes, CastInfo, Content, Context, Derived, FromValue, IntoValue, Label,
31    LocatableSelector, NativeElement, OneOrMultiple, Packed, Reflect, Repr, Scope,
32    Selector, ShowSet, Smart, StyleChain, Styles, Synthesize, Value, elem,
33};
34use crate::introspection::{
35    EmptyIntrospector, History, Introspect, Introspector, Locatable, Location,
36    QueryIntrospection,
37};
38use crate::layout::{BlockElem, Em, HElem, PadElem};
39use crate::loading::{DataSource, Load, LoadSource, Loaded, format_yaml_error};
40use crate::model::{
41    CitationForm, CiteElem, CiteGroup, Destination, DirectLinkElem, FootnoteElem,
42    HeadingElem, LinkElem, Url,
43};
44use crate::routines::SpanMode;
45use crate::text::{Lang, LocalName, Region, SmallcapsElem, SubElem, SuperElem, TextElem};
46
47/// A bibliography / reference listing.
48///
49/// You can create a new bibliography by calling this function with a path to a
50/// bibliography file in either one of two formats:
51///
52/// - A Hayagriva `.yaml`/`.yml` file. Hayagriva is a new bibliography file
53///   format designed for use with Typst. Visit its
54///   #link("https://github.com/typst/hayagriva/blob/main/docs/file-format.md")[documentation]
55///   for more details.
56/// - A BibLaTeX `.bib` file.
57///
58/// As soon as you add a bibliography somewhere in your document, you can start
59/// citing things with reference syntax (`[@key]`) or explicit calls to the
60/// @cite[citation] function (`[#cite(<key>)]`). The bibliography will only show
61/// entries for works that were referenced in the document.
62///
63/// = Example <example>
64/// ```example
65/// This was already noted by
66/// pirates long ago. @arrgh
67///
68/// Multiple sources say ...
69/// @arrgh @netwok.
70///
71/// #bibliography("works.bib")
72/// ```
73///
74/// = Styles <styles>
75/// Typst offers a wide selection of built-in
76/// @bibliography.style[citation and bibliography styles]. Beyond those, you can
77/// add and use custom #link("https://citationstyles.org/")[CSL] (Citation Style
78/// Language) files. Wondering which style to use? Here are some good defaults
79/// based on what discipline you're working in:
80///
81/// #docs-table(
82///   table.header[Fields][Typical Styles],
83///
84///   [Engineering, IT],
85///   [`{"ieee"}`],
86///
87///   [Psychology, Life Sciences],
88///   [`{"apa"}`],
89///
90///   [Social sciences],
91///   [`{"chicago-author-date"}`],
92///
93///   [Humanities],
94///   [`{"mla"}`, `{"chicago-notes"}`, `{"harvard-cite-them-right"}`],
95///
96///   [Economics],
97///   [`{"harvard-cite-them-right"}`],
98///
99///   [Physics],
100///   [`{"american-physics-society"}`],
101/// )
102///
103/// = Multiple bibliographies <multiple-bibliographies>
104/// When a Typst document contains multiple bibliographies, each citation is
105/// assigned to one of them. By default, Typst will automatically pick a
106/// suitable bibliography (typically, the closest following one that contains
107/// the referenced citation key). This covers common cases like by-chapter or
108/// thematic bibliographies. For more fine-grained control, citations can be
109/// explicitly targeted by a bibliography through a
110/// @bibliography.target[`target`] selector.
111#[elem(Locatable, Synthesize, ShowSet, LocalName)]
112pub struct BibliographyElem {
113    /// One or multiple paths to or raw bytes for Hayagriva `.yaml` and/or
114    /// BibLaTeX `.bib` files.
115    ///
116    /// This can be a:
117    /// - A path string or @path to load a bibliography file from.
118    /// - Raw bytes from which the bibliography should be decoded.
119    /// - An array where each item is one of the above.
120    #[required]
121    #[parse(
122        let sources = args.expect("sources")?;
123        Bibliography::load(engine.world, sources)?
124    )]
125    pub sources: Derived<OneOrMultiple<DataSource>, Bibliography>,
126
127    /// The title of the bibliography.
128    ///
129    /// - When set to `{auto}`, an appropriate title for the
130    ///   @text.lang[text language] will be used. This is the default.
131    /// - When set to `{none}`, the bibliography will not have a title.
132    /// - A custom title can be set by passing content.
133    ///
134    /// The bibliography's heading will not be numbered by default, but you can
135    /// force it to be with a show-set rule:
136    /// `{show bibliography: set heading(numbering: "1.")}`
137    pub title: Smart<Option<Content>>,
138
139    /// Whether to include all works from the given bibliography files, even
140    /// those that weren't cited in the document.
141    ///
142    /// To selectively add individual cited works without showing them, you can
143    /// also use the `cite` function with @cite.form[`form`] set to `{none}`.
144    #[default(false)]
145    pub full: bool,
146
147    /// The bibliography style.
148    ///
149    /// This can be:
150    /// - A string with the name of one of the built-in styles (see below). Some
151    ///   of the styles listed below appear twice, once with their full name and
152    ///   once with a short alias.
153    /// - A path string or @path to a
154    ///   #link("https://citationstyles.org/")[CSL file].
155    /// - Raw bytes from which a CSL style should be decoded.
156    #[parse(match args.named::<Spanned<CslSource>>("style")? {
157        Some(source) => Some(CslStyle::load(engine, source)?),
158        None => None,
159    })]
160    #[default({
161        let default = ArchivedStyle::InstituteOfElectricalAndElectronicsEngineers;
162        Derived::new(CslSource::Named(default, None), CslStyle::from_archived(default))
163    })]
164    pub style: Derived<CslSource, CslStyle>,
165
166    /// Defines which citations to include in the bibliography.
167    ///
168    /// Typst will automatically assign each citation in the document to a
169    /// bibliography. Concretely, a citation will be assigned to (in order of
170    /// precedence)
171    /// + the first bibliography that includes it in its `target` selector; or
172    ///   if no such bibliography exists
173    /// + the closest _following_ bibliography with `{target: auto}` that
174    ///   contains its key; or if no such bibliography follows
175    /// + the closest _preceding_ bibliography with `{target: auto}` that
176    ///   contains its key.
177    ///
178    /// #example(
179    ///   title: [Local bibliography],
180    ///   ```
181    ///   #let info(body) = block(
182    ///     stroke: (left: 1.5pt + blue),
183    ///     fill: aqua.lighten(50%),
184    ///     inset: 1em,
185    ///     context {
186    ///       body
187    ///       show divider: set block(spacing: 1.2em)
188    ///       divider()
189    ///       bibliography(
190    ///         "works.bib",
191    ///         title: none,
192    ///         target: selector(cite).within(here()),
193    ///         style: "mla",
194    ///       )
195    ///     }
196    ///   )
197    ///
198    ///   = On the matter of dumplings
199    ///   In recent years, we can observe an uptick in
200    ///   dumpling consumption across the board. @netwok
201    ///
202    ///   #info[
203    ///     Dumplings are particularly enjoyed
204    ///     among pirates. @arrgh
205    ///   ]
206    ///
207    ///   #bibliography("works.bib")
208    ///   ```
209    /// )
210    pub target: Smart<LocatableSelector>,
211
212    /// Conceptually groups this bibliography with other bibliographies for
213    /// numbering purposes. Bibliographies in the same group will assign
214    /// consecutive citation numbers.
215    ///
216    /// This can be:
217    /// - `{none}`: The bibliography will be numbered in isolation.
218    /// - `{auto}`: The bibliography will be consecutively numbered with all
219    ///   other bibliographies in the `{auto}` group.
220    /// - A @str[string]: The bibliography will be consecutively numbered with
221    ///   all other bibliographies with the same `group` value.
222    ///
223    /// The `{auto}` group works just like any string group, but it is the
224    /// canonical default group.
225    ///
226    /// #example(
227    ///   title: [Consecutive citation numbers],
228    ///   ```
229    ///   #show bibliography: set heading(
230    ///     offset: 1,
231    ///   )
232    ///
233    ///   = First part
234    ///   Starts at one: @netwok @arrgh
235    ///   #bibliography(
236    ///     "works.bib",
237    ///     style: "ieee",
238    ///   )
239    ///
240    ///   = Second part
241    ///   Continues with three: @distress
242    ///   #bibliography(
243    ///     "works.bib",
244    ///     style: "nlm-citation-sequence",
245    ///   )
246    ///   ```
247    /// )
248    ///
249    /// #example(
250    ///   title: [Separate citation numbers],
251    ///   ```
252    ///   #show bibliography: set heading(
253    ///     offset: 1,
254    ///   )
255    ///   #set bibliography(group: none)
256    ///
257    ///   = First part
258    ///   Starts at one: @netwok @arrgh
259    ///   #bibliography(
260    ///     "works.bib",
261    ///     style: "ieee",
262    ///   )
263    ///
264    ///   = Second part
265    ///   Resets to one: @distress
266    ///   #bibliography(
267    ///     "works.bib",
268    ///     style: "nlm-citation-sequence",
269    ///   )
270    ///   ```
271    /// )
272    #[default(Some(Smart::Auto))]
273    pub group: Option<Smart<EcoString>>,
274
275    /// The language setting where the bibliography is.
276    #[internal]
277    #[synthesized]
278    pub lang: Lang,
279
280    /// The region setting where the bibliography is.
281    #[internal]
282    #[synthesized]
283    pub region: Option<Region>,
284}
285
286impl BibliographyElem {
287    /// Whether any bibliography contains the given key.
288    pub fn has(engine: &mut Engine, key: Label, span: Span) -> bool {
289        engine
290            .introspect(QueryIntrospection(Self::ELEM.select(), span))
291            .iter()
292            .any(|elem| elem.to_packed::<Self>().unwrap().sources.derived.has(key))
293    }
294
295    /// Find all bibliography keys.
296    pub fn keys(
297        introspector: Tracked<dyn Introspector + '_>,
298    ) -> Vec<(Label, Option<EcoString>)> {
299        let mut vec = vec![];
300        for elem in introspector.query(&Self::ELEM.select()).iter() {
301            let this = elem.to_packed::<Self>().unwrap();
302            for (key, entry) in this.sources.derived.iter() {
303                let detail = entry.title().map(|title| title.value.to_str().into());
304                vec.push((key, detail))
305            }
306        }
307        vec
308    }
309}
310
311impl Packed<BibliographyElem> {
312    /// Produces the heading for the bibliography, if any.
313    pub fn realize_title(&self, styles: StyleChain) -> Option<Content> {
314        self.title
315            .get_cloned(styles)
316            .unwrap_or_else(|| {
317                Some(TextElem::packed(Packed::<BibliographyElem>::local_name_in(styles)))
318            })
319            .map(|title| {
320                HeadingElem::new(title)
321                    .with_depth(NonZeroUsize::ONE)
322                    .pack()
323                    .spanned(self.span())
324            })
325    }
326}
327
328impl Synthesize for Packed<BibliographyElem> {
329    fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
330        let elem = self.as_mut();
331        elem.lang = Some(styles.get(TextElem::lang));
332        elem.region = Some(styles.get(TextElem::region));
333        Ok(())
334    }
335}
336
337impl ShowSet for Packed<BibliographyElem> {
338    fn show_set(&self, _: StyleChain) -> Styles {
339        const INDENT: Em = Em::new(1.0);
340        let mut out = Styles::new();
341        out.set(HeadingElem::numbering, None);
342        out.set(PadElem::left, INDENT.into());
343        out
344    }
345}
346
347impl LocalName for Packed<BibliographyElem> {
348    const KEY: &'static str = "bibliography";
349}
350
351/// A loaded bibliography.
352#[derive(Clone, PartialEq, Hash)]
353pub struct Bibliography(
354    Arc<ManuallyHash<IndexMap<Label, hayagriva::Entry, FxBuildHasher>>>,
355);
356
357impl Bibliography {
358    /// Load a bibliography from data sources.
359    fn load(
360        world: Tracked<dyn World + '_>,
361        sources: Spanned<OneOrMultiple<DataSource>>,
362    ) -> SourceResult<Derived<OneOrMultiple<DataSource>, Self>> {
363        let loaded = sources.load(world)?;
364        let bibliography = Self::decode(&loaded)?;
365        Ok(Derived::new(sources.v, bibliography))
366    }
367
368    /// Decode a bibliography from loaded data sources.
369    #[comemo::memoize]
370    #[typst_macros::time(name = "load bibliography")]
371    fn decode(data: &[Loaded]) -> SourceResult<Bibliography> {
372        let mut map = IndexMap::default();
373        let mut duplicates = Vec::<EcoString>::new();
374
375        // We might have multiple bib/yaml files
376        for d in data.iter() {
377            let library = decode_library(d)?;
378            for entry in library {
379                let label = Label::new(PicoStr::intern(entry.key()))
380                    .ok_or("bibliography contains entry with empty key")
381                    .at(d.source.span)?;
382
383                match map.entry(label) {
384                    indexmap::map::Entry::Vacant(vacant) => {
385                        vacant.insert(entry);
386                    }
387                    indexmap::map::Entry::Occupied(_) => {
388                        duplicates.push(entry.key().into());
389                    }
390                }
391            }
392        }
393
394        if !duplicates.is_empty() {
395            // TODO: Store spans of entries for duplicate key error messages.
396            // Requires hayagriva entries to store their location, which should
397            // be fine, since they are 1kb anyway.
398            let span = data.first().unwrap().source.span;
399            bail!(span, "duplicate bibliography keys: {}", duplicates.join(", "));
400        }
401
402        Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data)))))
403    }
404
405    fn has(&self, key: Label) -> bool {
406        self.0.contains_key(&key)
407    }
408
409    fn get(&self, key: Label) -> Option<&hayagriva::Entry> {
410        self.0.get(&key)
411    }
412
413    fn iter(&self) -> impl Iterator<Item = (Label, &hayagriva::Entry)> {
414        self.0.iter().map(|(&k, v)| (k, v))
415    }
416}
417
418impl Debug for Bibliography {
419    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
420        f.debug_set().entries(self.0.keys()).finish()
421    }
422}
423
424/// Decode on library from one data source.
425fn decode_library(loaded: &Loaded) -> SourceResult<Library> {
426    let data = loaded.data.as_str().within(loaded)?;
427
428    if let LoadSource::Path(file_id) = loaded.source.v {
429        // If we got a path, use the extension to determine whether it is
430        // YAML or BibLaTeX.
431        let ext = file_id.vpath().extension().unwrap_or_default();
432        match ext.to_lowercase().as_str() {
433            "yml" | "yaml" => hayagriva::io::from_yaml_str(data)
434                .map_err(format_yaml_error)
435                .within(loaded),
436            "bib" => hayagriva::io::from_biblatex_str(data)
437                .map_err(format_biblatex_error)
438                .within(loaded),
439            _ => bail!(
440                loaded.source.span,
441                "unknown bibliography format (must be .yaml/.yml or .bib)"
442            ),
443        }
444    } else {
445        // If we just got bytes, we need to guess. If it can be decoded as
446        // hayagriva YAML, we'll use that.
447        let haya_err = match hayagriva::io::from_yaml_str(data) {
448            Ok(library) => return Ok(library),
449            Err(err) => err,
450        };
451
452        // If it can be decoded as BibLaTeX, we use that instead.
453        let bib_errs = match hayagriva::io::from_biblatex_str(data) {
454            // If the file is almost valid yaml, but contains no `@` character
455            // it will be successfully parsed as an empty BibLaTeX library,
456            // since BibLaTeX does support arbitrary text outside of entries.
457            Ok(library) if !library.is_empty() => return Ok(library),
458            Ok(_) => None,
459            Err(err) => Some(err),
460        };
461
462        // If neither decoded correctly, check whether `:` or `{` appears
463        // more often to guess whether it's more likely to be YAML or BibLaTeX
464        // and emit the more appropriate error.
465        let mut yaml = 0;
466        let mut biblatex = 0;
467        for c in data.chars() {
468            match c {
469                ':' => yaml += 1,
470                '{' => biblatex += 1,
471                _ => {}
472            }
473        }
474
475        match bib_errs {
476            Some(bib_errs) if biblatex >= yaml => {
477                Err(format_biblatex_error(bib_errs)).within(loaded)
478            }
479            _ => Err(format_yaml_error(haya_err)).within(loaded),
480        }
481    }
482}
483
484/// Format a BibLaTeX loading error.
485fn format_biblatex_error(errors: Vec<BibLaTeXError>) -> LoadError {
486    // TODO: return multiple errors?
487    let Some(error) = errors.into_iter().next() else {
488        // TODO: can this even happen, should we just unwrap?
489        return LoadError::text(
490            ReportTextPos::None,
491            "failed to parse BibLaTeX",
492            "something went wrong",
493        );
494    };
495
496    let (range, msg) = match error {
497        BibLaTeXError::Parse(error) => (error.span, error.kind.to_string()),
498        BibLaTeXError::Type(error) => (error.span, error.kind.to_string()),
499    };
500
501    LoadError::text(range, "failed to parse BibLaTeX", msg)
502}
503
504/// A loaded CSL style.
505#[derive(Debug, Clone, PartialEq, Hash)]
506pub struct CslStyle(Arc<ManuallyHash<citationberg::IndependentStyle>>);
507
508impl CslStyle {
509    /// Load a CSL style from a data source.
510    pub fn load(
511        engine: &mut Engine,
512        Spanned { v: source, span }: Spanned<CslSource>,
513    ) -> SourceResult<Derived<CslSource, Self>> {
514        let style = match &source {
515            CslSource::Named(style, deprecation) => {
516                if let Some(message) = deprecation {
517                    engine.sink.warn(SourceDiagnostic::warning(span, message.clone()));
518                }
519                Self::from_archived(*style)
520            }
521            CslSource::Normal(source) => {
522                let loaded = Spanned::new(source, span).load(engine.world)?;
523                Self::from_data(&loaded.data).within(&loaded)?
524            }
525        };
526        Ok(Derived::new(source, style))
527    }
528
529    /// Load a built-in CSL style.
530    #[comemo::memoize]
531    pub fn from_archived(archived: ArchivedStyle) -> CslStyle {
532        match archived.get() {
533            citationberg::Style::Independent(style) => Self(Arc::new(ManuallyHash::new(
534                style,
535                typst_utils::hash128(&(TypeId::of::<ArchivedStyle>(), archived)),
536            ))),
537            // Ensured by `test_bibliography_load_builtin_styles`.
538            _ => unreachable!("archive should not contain dependent styles"),
539        }
540    }
541
542    /// Load a CSL style from file contents.
543    #[comemo::memoize]
544    pub fn from_data(bytes: &Bytes) -> LoadResult<CslStyle> {
545        let text = bytes.as_str()?;
546        citationberg::IndependentStyle::from_xml(text)
547            .map(|style| {
548                Self(Arc::new(ManuallyHash::new(
549                    style,
550                    typst_utils::hash128(&(TypeId::of::<Bytes>(), bytes)),
551                )))
552            })
553            .map_err(|err| {
554                LoadError::text(ReportTextPos::None, "failed to load CSL style", err)
555            })
556    }
557
558    /// Get the underlying independent style.
559    pub fn get(&self) -> &citationberg::IndependentStyle {
560        self.0.as_ref()
561    }
562}
563
564/// Source for a CSL style.
565#[derive(Debug, Clone, PartialEq, Hash)]
566pub enum CslSource {
567    /// A predefined named style and potentially a deprecation warning.
568    Named(ArchivedStyle, Option<EcoString>),
569    /// A normal data source.
570    Normal(DataSource),
571}
572
573impl Reflect for CslSource {
574    #[comemo::memoize]
575    fn input() -> CastInfo {
576        let source = std::iter::once(DataSource::input());
577
578        /// All possible names and their short documentation for `ArchivedStyle`, including aliases.
579        static ARCHIVED_STYLE_NAMES: LazyLock<Vec<(&&str, &'static str)>> =
580            LazyLock::new(|| {
581                ArchivedStyle::all()
582                    .iter()
583                    .flat_map(|name| {
584                        let (main_name, aliases) = name
585                            .names()
586                            .split_first()
587                            .expect("all ArchivedStyle should have at least one name");
588
589                        std::iter::once((main_name, name.display_name())).chain(
590                            aliases.iter().map(move |alias| {
591                                // Leaking is okay here, because we are in a `LazyLock`.
592                                let docs: &'static str = Box::leak(
593                                    format!("A short alias of `{main_name}`")
594                                        .into_boxed_str(),
595                                );
596                                (alias, docs)
597                            }),
598                        )
599                    })
600                    .collect()
601            });
602        let names = ARCHIVED_STYLE_NAMES
603            .iter()
604            .map(|(value, docs)| CastInfo::Value(value.into_value(), docs));
605
606        CastInfo::Union(source.into_iter().chain(names).collect())
607    }
608
609    fn output() -> CastInfo {
610        DataSource::output()
611    }
612
613    fn castable(value: &Value) -> bool {
614        DataSource::castable(value)
615    }
616}
617
618impl FromValue for CslSource {
619    fn from_value(value: Value) -> HintedStrResult<Self> {
620        if EcoString::castable(&value) {
621            let string = EcoString::from_value(value.clone())?;
622            if Path::new(string.as_str()).extension().is_none() {
623                let replacement = replacement(&string);
624                let deprecation = replacement.map(|instead| {
625                    eco_format!(
626                        "style `{}` has been deprecated in favor of `{}`",
627                        string.repr(),
628                        instead.repr(),
629                    )
630                });
631                let style = ArchivedStyle::by_name(&string).ok_or_else(|| {
632                    deprecation
633                        .clone()
634                        .unwrap_or_else(|| eco_format!("unknown style: {string}"))
635                })?;
636                return Ok(CslSource::Named(style, deprecation));
637            }
638        }
639
640        DataSource::from_value(value).map(CslSource::Normal)
641    }
642}
643
644impl IntoValue for CslSource {
645    fn into_value(self) -> Value {
646        match self {
647            // We prefer the shorter names which are at the back of the array.
648            Self::Named(v, _) => v.names().last().unwrap().into_value(),
649            Self::Normal(v) => v.into_value(),
650        }
651    }
652}
653
654/// Maps from style names to their replacements.
655///
656/// TODO: Fully move this into hayagriva somehow.
657fn replacement(style: &str) -> Option<&'static str> {
658    Some(match style {
659        "chicago-fullnotes" => "chicago-notes",
660        "modern-humanities-research-association" => {
661            "modern-humanities-research-association-notes"
662        }
663        "council-of-science-editors" => "cse-citation-sequence-brackets-8th-edition",
664        "council-of-science-editors-author-date" => "cse-name-year",
665        "modern-language-association-8" | "mla-8" => "modern-language-association",
666        "vancouver" => "nlm-citation-sequence",
667        "vancouver-superscript" => "nlm-citation-sequence-superscript",
668        _ => return None,
669    })
670}
671
672/// Fully formatted citations and references, generated once (through
673/// memoization) for the whole document. This setup is necessary because
674/// citation formatting is inherently stateful and we need access to all
675/// citations to do it.
676/// Fully formatted citation groups and bibliographies, generated once (through
677/// memoization) for the whole document.
678///
679/// This setup is necessary because citation formatting is inherently stateful
680/// and we need access to all citations to do it.
681pub struct Works {
682    /// The document's rendered [`BibliographyElem`]s, keyed by their locations.
683    bibliographies: FxHashMap<Location, SourceResult<RenderedBibliography>>,
684    /// The document's rendered [`CiteGroup`]s, keyed by their locations.
685    groups: FxHashMap<Location, SourceResult<Content>>,
686}
687
688/// The rendered parts for a bibliography.
689pub struct RenderedBibliography {
690    /// Lists all entries in the bibliography, with optional prefix.
691    pub entries: Vec<RenderedEntry>,
692    /// Whether the bibliography should have hanging indent applied.
693    pub hanging_indent: bool,
694}
695
696/// The rendered parts for a bibliography entry.
697pub struct RenderedEntry {
698    /// An optional prefix. This is exposed separately because this will go into
699    /// its own column for grid-based styles.
700    pub prefix: Option<Content>,
701    /// The main content of the rendered bibliography entry.
702    pub body: Content,
703    /// A location that should be attached to the rendered entry in some way.
704    /// Citations will link there.
705    pub backlink: Location,
706}
707
708impl Works {
709    /// Generates and formats all bibliographies and citations.
710    pub fn generate(engine: &mut Engine, span: Span) -> SourceResult<Arc<Works>> {
711        let bibs_and_groups = engine.introspect(BibliographyIntrospection(span));
712        Self::generate_impl(
713            engine.world,
714            engine.library,
715            engine.introspector.into_raw(),
716            engine.traced,
717            TrackedMut::reborrow_mut(&mut engine.sink),
718            engine.route.track(),
719            &bibs_and_groups,
720        )
721        .at(span)
722    }
723
724    /// The internal implementation of [`Works::generate`].
725    #[comemo::memoize]
726    fn generate_impl(
727        world: Tracked<dyn World + '_>,
728        library: &LazyHash<crate::Library>,
729        introspector: Tracked<dyn Introspector + '_>,
730        traced: Tracked<Traced>,
731        sink: TrackedMut<Sink>,
732        route: Tracked<Route>,
733        bibs_and_groups: &[Content],
734    ) -> StrResult<Arc<Works>> {
735        let mut engine = Engine {
736            world,
737            library,
738            introspector: Protected::from_raw(introspector),
739            traced,
740            sink,
741            route: Route::extend(route),
742        };
743
744        // Prepare bibliographies and citation groups for rendering with
745        // hayagriva.
746        let p = prepare(&mut engine, bibs_and_groups);
747
748        // Render the bibliography and citations with hayagriva.
749        let mut offsets = FxHashMap::default();
750        let rendered =
751            p.bibs.iter().map(|bib| render(bib, &mut offsets)).collect::<Vec<_>>();
752
753        Ok(Arc::new(Works {
754            bibliographies: show_bibliographies(world, &p, &rendered),
755            groups: show_cite_groups(world, p, &rendered),
756        }))
757    }
758
759    /// Returns the shown content for a citation.
760    pub fn citation(&self, loc: Location, span: Span) -> SourceResult<Content> {
761        self.groups
762            .get(&loc)
763            .cloned()
764            .ok_or_else(citation_could_not_be_located)
765            .at(span)?
766    }
767
768    /// Returns the shown content for a bibliography.
769    pub fn bibliography(
770        &self,
771        loc: Location,
772        span: Span,
773    ) -> SourceResult<&RenderedBibliography> {
774        self.bibliographies
775            .get(&loc)
776            .ok_or_else(bibliography_could_not_be_located)
777            .at(span)?
778            .as_ref()
779            .map_err(Clone::clone)
780    }
781}
782
783/// Preprocessed information for all bibliographies and citation groups in the
784/// document, ready for rendering with hayagriva.
785struct Preparation<'a> {
786    /// Preprocessed information for all bibliographies in the document, in
787    /// document order.
788    bibs: Vec<PreparedBibliography<'a>>,
789    /// Preprocessed information for all [`CiteGroup`] elements in the document,
790    /// keyed by their [`Location`].
791    groups: FxHashMap<Location, SourceResult<PreparedCiteGroup<'a>>>,
792}
793
794/// Preprocessed information for a bibliography and the citations assigned to
795/// it, ready for processing by hayagriva.
796struct PreparedBibliography<'a> {
797    /// The underlying bibliography element.
798    elem: &'a Packed<BibliographyElem>,
799    /// Information about citation subgroups assigned to this bibliography, in
800    /// document order. Each subgroup turns into one citation sent to hayagriva,
801    /// A single [`CiteGroup`] can comprise multiple subgroups assigned to
802    /// different bibliographies.
803    subgroups: Vec<Subgroup<'a>>,
804}
805
806/// Holds consecutive citations from a `CiteGroup` that were assigned to the
807/// same bibliography.
808///
809/// See [`CiteGroup`] for more details on citation grouping.
810struct Subgroup<'a> {
811    /// The underlying citation group.
812    elem: &'a Packed<CiteGroup>,
813    /// The citations in this subgroup.
814    citations: SmallVec<[&'a Packed<CiteElem>; 1]>,
815    /// The style picked for this subgroup. Citations are not segmented by style
816    /// (at least currently); we simply pick the style of the first citation.
817    style: &'a CslStyle,
818}
819
820/// Preprocessed information for a [`CiteGroup`]. Can be used to show the group
821/// as [`Content`] after bibliographies are processed with hayagriva.
822struct PreparedCiteGroup<'a>(SmallVec<[GroupPart<'a>; 1]>);
823
824/// A segment in a preprocessed [`CiteGroup`].
825enum GroupPart<'a> {
826    /// This content should be displayed verbatim. In practice, this is only
827    /// ever a [`SpaceElem`](crate::text::SpaceElem) between subgroups.
828    Content(&'a Content),
829    /// Points to a subgroup in one of the bibliographies in the document and
830    /// should be substituted by the content produced for the citation.
831    Subgroup(BibIndex, SubgroupIndex),
832}
833
834/// The index of a bibliography among all bibliographies in `p.bibs` where
835/// `p: Preparation`.
836#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
837struct BibIndex(usize);
838
839/// The index of a subgroup in `bib.subgroups` where `bib: PreparedBibliography`
840/// among those assigned to the same bibliography.
841#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
842struct SubgroupIndex(usize);
843
844/// Prepares the document's bibliographies and citation groups for rendering
845/// with hayagriva.
846///
847/// Primarily, this involves
848/// - assigning citations to bibliographies (and while doing so splitting
849///   [`CiteGroup`]s into subgroups assigned to the same bibliography).
850/// - retaining data structures that can be used to show the hayagriva output as
851///   [`Content`] for bibliography and citation group elements.
852fn prepare<'a>(engine: &mut Engine, bibs_and_groups: &'a [Content]) -> Preparation<'a> {
853    // Maps from citations to bibliography indices for citations that were
854    // explicitly selected via a bibliography with a `target`.
855    let mut selected = FxHashMap::<Location, BibIndex>::default();
856
857    // First, we process bibliographies. This involves:
858    // - Creating a slot in `bibs` for each
859    // - Creating a mapping from citations to bibliography index for all
860    //   citations specifically targetted by a bibliography.
861    let mut bibs = Vec::<PreparedBibliography>::new();
862    for elem in bibs_and_groups {
863        let Some(bib) = elem.to_packed::<BibliographyElem>() else { continue };
864        let idx = BibIndex(bibs.len());
865        bibs.push(PreparedBibliography { elem: bib, subgroups: vec![] });
866
867        if let Smart::Custom(LocatableSelector(selector)) =
868            bib.target.get_cloned(StyleChain::default())
869        {
870            for citation in engine.introspect(QueryIntrospection(selector, bib.span())) {
871                selected.entry(citation.location().unwrap()).or_insert(idx);
872            }
873        }
874    }
875
876    // Then, we process citations groups. See the doc comment of
877    // `prepare_cite_group` for more information on which steps this involves.
878    let mut groups = FxHashMap::default();
879    let mut bib_cursor = 0;
880    for elem in bibs_and_groups {
881        let Some(group) = elem.to_packed::<CiteGroup>() else {
882            debug_assert!(elem.is::<BibliographyElem>());
883            bib_cursor += 1;
884            continue;
885        };
886
887        let loc = group.location().unwrap();
888        let result = prepare_cite_group(&mut bibs, bib_cursor, group, &selected);
889        groups.insert(loc, result);
890    }
891
892    Preparation { bibs, groups }
893}
894
895/// Prepares a [`CiteGroup`] by
896/// - splitting it into subgroups assigned to the same bibliography
897/// - storing a [`Subgroup`] for each of these in the approprate slot in `bibs`
898/// - creating a [`PreparedCiteGroup`] which can be used to stitch the shown
899///   content for the subgroups together after it was rendered with hayagriva.
900fn prepare_cite_group<'a>(
901    bibs: &mut [PreparedBibliography<'a>],
902    bib_cursor: usize,
903    group: &'a Packed<CiteGroup>,
904    selected: &FxHashMap<Location, BibIndex>,
905) -> SourceResult<PreparedCiteGroup<'a>> {
906    // Holds the collected segments of the citation group.
907    let mut parts = SmallVec::new();
908    // Citations that make up the current subgroup.
909    let mut subgroup = SmallVec::new();
910    // The bibliography index for the current subgroup. If `Some(_)`, then
911    // `subgroup` is not empty.
912    let mut subgroup_bib = None;
913    // The elements in `group.children[tail..i]` are interior spaces.
914    let mut tail = 0;
915    // Holds errors for any uncovered citation. We collect them instead of
916    // bailing early so that we can give multiple errors at once.
917    let mut errors = EcoVec::new();
918
919    for (i, child) in group.children.iter().enumerate() {
920        // The children are either citations or spaces. We skip interior spaces
921        // without updating `tail`, so that at any point we can produce a slice
922        // with the trailing spaces.
923        //
924        // Note that exterior spaces are not supported, but they also will never
925        // appear in groups in practice.
926        let Some(citation) = child.to_packed::<CiteElem>() else { continue };
927        let spaces = &group.children[tail..i];
928        tail = i + 1;
929
930        // Determine the pre-selected bibliography or assign an auto
931        // bibliography.
932        let bib_idx = if let Some(&idx) = selected.get(&citation.location().unwrap()) {
933            // Ensure that the bibliography contains the key.
934            let bib = &bibs[idx.0];
935            if !bib.elem.sources.derived.has(citation.key) {
936                errors.push(key_does_not_exist(citation, bib));
937                continue;
938            }
939            idx
940        } else if let Some(idx) = select_auto_bib(citation, bibs, bib_cursor) {
941            idx
942        } else {
943            errors.push(uncovered_citation(citation, bibs));
944            continue;
945        };
946
947        // If the assigned bibliography changes, flush the previous subgroup and
948        // the spaces between it and the current `child`.
949        if let Some(subgroup_bib) = subgroup_bib
950            && subgroup_bib != bib_idx
951        {
952            parts.push(save_subgroup(
953                bibs,
954                subgroup_bib,
955                group,
956                std::mem::take(&mut subgroup),
957            ));
958            parts.extend(spaces.iter().map(GroupPart::Content));
959        }
960
961        subgroup_bib = Some(bib_idx);
962        subgroup.push(citation);
963    }
964
965    // Flush the final subgroup if any.
966    if let Some(subgroup_bib) = subgroup_bib {
967        parts.push(save_subgroup(bibs, subgroup_bib, group, subgroup));
968    }
969
970    if !errors.is_empty() {
971        return Err(errors);
972    }
973
974    Ok(PreparedCiteGroup(parts))
975}
976
977/// Builds a [`Subgroup`] and stores it in the selected bibliography. Returns a
978/// [`GroupPart`] to remember that this subgroup belongs to the [`CiteGroup`]
979/// that's currently being prepared.
980fn save_subgroup<'a>(
981    bibs: &mut [PreparedBibliography<'a>],
982    bib_idx: BibIndex,
983    elem: &'a Packed<CiteGroup>,
984    citations: SmallVec<[&'a Packed<CiteElem>; 1]>,
985) -> GroupPart<'a> {
986    let bib = &mut bibs[bib_idx.0];
987    let style = if let Some(first) = citations.first()
988        && let Smart::Custom(style) = first.style.get_ref(StyleChain::default())
989    {
990        &style.derived
991    } else {
992        &bib.elem.style.get_ref(StyleChain::default()).derived
993    };
994
995    let sub_idx = SubgroupIndex(bib.subgroups.len());
996    bib.subgroups.push(Subgroup { elem, citations, style });
997
998    GroupPart::Subgroup(bib_idx, sub_idx)
999}
1000
1001/// Selects the appropriate bibliography for a citation that was not explicitly
1002/// targeted by a bibliography.
1003///
1004/// The priority is:
1005/// 1. First following auto bibliography containing the citation key
1006/// 2. First preceding auto bibliography containing the citation key
1007///
1008/// Returns `None` if no auto bibliography contains the key.
1009fn select_auto_bib(
1010    citation: &Packed<CiteElem>,
1011    bibs: &[PreparedBibliography],
1012    bib_cursor: usize,
1013) -> Option<BibIndex> {
1014    let bibs = bibs.iter().enumerate();
1015    let before = bibs.clone().take(bib_cursor);
1016    let after = bibs.skip(bib_cursor);
1017    after
1018        .chain(before.rev())
1019        .find(|(_, bib)| {
1020            bib.elem.target.get_ref(StyleChain::default()).is_auto()
1021                && bib.elem.sources.derived.has(citation.key)
1022        })
1023        .map(|(idx, _)| BibIndex(idx))
1024}
1025
1026/// Renders the bibliography and citation groups with hayagriva.
1027fn render<'a>(
1028    bib: &PreparedBibliography<'a>,
1029    offsets: &mut FxHashMap<Smart<&'a str>, usize>,
1030) -> hayagriva::Rendered {
1031    static LOCALES: LazyLock<Vec<citationberg::Locale>> =
1032        LazyLock::new(hayagriva::archive::locales);
1033
1034    let database = &bib.elem.sources.derived;
1035
1036    let mut driver = BibliographyDriver::new();
1037    let mut offset = bib
1038        .elem
1039        .group
1040        .get_ref(StyleChain::default())
1041        .as_ref()
1042        .map(|group| offsets.entry(group.as_deref()).or_insert(0));
1043
1044    if let Some(offset) = &mut offset {
1045        driver = driver.with_citation_number_offset(**offset);
1046    }
1047
1048    for group in &bib.subgroups {
1049        let items = group
1050            .citations
1051            .iter()
1052            .map(|child| {
1053                let entry = database.get(child.key).expect("entry to be present");
1054                citation_item(entry, child)
1055            })
1056            .collect::<Vec<_>>();
1057
1058        let first = &group.citations[0];
1059        let locale = locale(first.lang.unwrap_or(Lang::ENGLISH), first.region.flatten());
1060
1061        driver.citation(CitationRequest::new(
1062            items,
1063            group.style.get(),
1064            Some(locale),
1065            &LOCALES,
1066            None,
1067        ));
1068    }
1069
1070    let bib_style = &bib.elem.style.get_ref(StyleChain::default()).derived;
1071    let locale =
1072        locale(bib.elem.lang.unwrap_or(Lang::ENGLISH), bib.elem.region.flatten());
1073
1074    // Add hidden items for everything if we should print the whole
1075    // bibliography.
1076    if bib.elem.full.get(StyleChain::default()) {
1077        for (_, entry) in database.iter() {
1078            driver.citation(CitationRequest::new(
1079                vec![CitationItem::new(entry, None, None, true, None)],
1080                bib_style.get(),
1081                Some(locale.clone()),
1082                &LOCALES,
1083                None,
1084            ));
1085        }
1086    }
1087
1088    let rendered = driver.finish(BibliographyRequest {
1089        style: bib_style.get(),
1090        locale: Some(locale),
1091        locale_files: &LOCALES,
1092    });
1093
1094    if let Some(offset) = offset
1095        && let Some(bib) = &rendered.bibliography
1096        // Check whether the bibliography or any citation displays citation
1097        // numbers. Only then does the bibliography occupy a numbering range
1098        // that subsequent bibliographies in the same group must skip.
1099        && (bib.items.iter().any(displays_citation_number)
1100            || rendered.citations.iter().any(|rendered| {
1101                rendered
1102                    .citation
1103                    .find_meta(&hayagriva::ElemMeta::CitationNumber)
1104                    .is_some()
1105            }))
1106    {
1107        *offset += bib.items.len();
1108    }
1109
1110    rendered
1111}
1112
1113/// Whether a rendered bibliography item displays a citation number.
1114///
1115/// For styles with `second-field-align` (like IEEE), the number resides in
1116/// the item's first field rather than in its content.
1117fn displays_citation_number(item: &hayagriva::BibliographyItem) -> bool {
1118    item.content.find_meta(&hayagriva::ElemMeta::CitationNumber).is_some()
1119        || item.first_field.as_ref().is_some_and(|child| match child {
1120            hayagriva::ElemChild::Elem(elem) => {
1121                elem.meta == Some(hayagriva::ElemMeta::CitationNumber)
1122                    || elem
1123                        .children
1124                        .find_meta(&hayagriva::ElemMeta::CitationNumber)
1125                        .is_some()
1126            }
1127            _ => false,
1128        })
1129}
1130
1131/// Creates a hayagriva citation item for a citation element.
1132fn citation_item<'a>(
1133    entry: &'a hayagriva::Entry,
1134    child: &'a Packed<CiteElem>,
1135) -> CitationItem<'a, hayagriva::Entry> {
1136    let supplement = child.supplement.get_cloned(StyleChain::default());
1137    let locator = supplement.as_ref().map(|c| {
1138        SpecificLocator(
1139            citationberg::taxonomy::Locator::Custom,
1140            hayagriva::LocatorPayload::Transparent(TransparentLocator::new(c.clone())),
1141        )
1142    });
1143
1144    let mut hidden = false;
1145    let special_form = match child.form.get(StyleChain::default()) {
1146        None => {
1147            hidden = true;
1148            None
1149        }
1150        Some(CitationForm::Normal) => None,
1151        Some(CitationForm::Prose) => Some(hayagriva::CitePurpose::Prose),
1152        Some(CitationForm::Full) => Some(hayagriva::CitePurpose::Full),
1153        Some(CitationForm::Author) => Some(hayagriva::CitePurpose::Author),
1154        Some(CitationForm::Year) => Some(hayagriva::CitePurpose::Year),
1155    };
1156
1157    CitationItem::new(entry, locator, None, hidden, special_form)
1158}
1159
1160/// Produces the structured content for all [`BibliographyElem`]s in the
1161/// document. The output of this is directly stored in the [`Works`] and
1162/// consumed by the target-specific bibliography show rules.
1163fn show_bibliographies(
1164    world: Tracked<dyn World + '_>,
1165    p: &Preparation,
1166    rendered: &[hayagriva::Rendered],
1167) -> FxHashMap<Location, SourceResult<RenderedBibliography>> {
1168    p.bibs
1169        .iter()
1170        .zip(rendered)
1171        .map(|(bib, rendered)| {
1172            let loc = bib.elem.location().unwrap();
1173            let result = rendered
1174                .bibliography
1175                .as_ref()
1176                .ok_or_else(|| {
1177                    style_unsuitable(
1178                        &bib.elem.style.get_ref(StyleChain::default()).source,
1179                    )
1180                })
1181                .and_then(|rendered| show_bibliography(world, bib, rendered))
1182                .at(bib.elem.span());
1183            (loc, result)
1184        })
1185        .collect()
1186}
1187
1188/// Turns a bibliography rendered with hayagriva into a final
1189/// [`RenderedBibliography`].
1190fn show_bibliography(
1191    world: Tracked<dyn World + '_>,
1192    bib: &PreparedBibliography,
1193    rendered: &hayagriva::RenderedBibliography,
1194) -> StrResult<RenderedBibliography> {
1195    let to_citations = links_to_citations(&bib.subgroups);
1196
1197    let mut entries = Vec::with_capacity(rendered.items.len());
1198    for (k, item) in rendered.items.iter().enumerate() {
1199        let ctx = ShowCtx {
1200            world,
1201            span: bib.elem.span(),
1202            supplement: &|_| None,
1203            link: &|_| None,
1204        };
1205
1206        // Render the first field.
1207        let mut prefix = item
1208            .first_field
1209            .as_ref()
1210            .map(|elem| show_elem_child(&ctx, elem, None, false))
1211            .transpose()?;
1212
1213        // Render the main reference content.
1214        let body = show_elem_children(&ctx, &item.content, Some(&mut prefix), false)?;
1215
1216        // Attach link to citation to the prefix.
1217        let prefix = prefix.map(|content| {
1218            if let Some(location) = to_citations.get(item.key.as_str()) {
1219                let alt = content.plain_text();
1220                let body = content.spanned(ctx.span);
1221                DirectLinkElem::new(*location, body, Some(alt)).pack()
1222            } else {
1223                content
1224            }
1225        });
1226
1227        entries.push(RenderedEntry { prefix, body, backlink: entry_location(bib, k) });
1228    }
1229
1230    Ok(RenderedBibliography { entries, hanging_indent: rendered.hanging_indent })
1231}
1232
1233/// Produces the content for all [`CiteGroup`]s in the document. The output of
1234/// this is directly stored in the [`Works`] and consumed by [`CiteGroup`] show
1235/// rule.
1236fn show_cite_groups(
1237    world: Tracked<dyn World + '_>,
1238    p: Preparation,
1239    rendered: &[hayagriva::Rendered],
1240) -> FxHashMap<Location, SourceResult<Content>> {
1241    let to_entries = links_to_entries(&p, rendered);
1242    p.groups
1243        .into_iter()
1244        .map(|(loc, group)| {
1245            let result = group.and_then(|group| {
1246                show_cite_group(world, &group, &p.bibs, rendered, |idx, key| {
1247                    to_entries.get(&(idx, key)).copied()
1248                })
1249            });
1250            (loc, result)
1251        })
1252        .collect()
1253}
1254
1255/// Produces the content for a [`CiteGroup`] by stitching together the hayagriva
1256/// output for each subgroup and interspersing potential space elements that
1257/// were retained between subgroups.
1258fn show_cite_group(
1259    world: Tracked<dyn World + '_>,
1260    group: &PreparedCiteGroup,
1261    prepared: &[PreparedBibliography],
1262    rendered: &[hayagriva::Rendered],
1263    to_entry: impl Fn(BibIndex, &str) -> Option<Location>,
1264) -> SourceResult<Content> {
1265    let mut seq = vec![];
1266    for part in &group.0 {
1267        seq.push(match part {
1268            GroupPart::Content(c) => (**c).clone(),
1269            GroupPart::Subgroup(bib_idx, sub_idx) => {
1270                let subgroup = &prepared[bib_idx.0].subgroups[sub_idx.0];
1271                let item = &rendered[bib_idx.0].citations[sub_idx.0];
1272                show_subgroup(world, subgroup, item, |key| to_entry(*bib_idx, key))?
1273            }
1274        });
1275    }
1276    Ok(Content::sequence(seq))
1277}
1278
1279/// Displays a single citation subgroup.
1280fn show_subgroup(
1281    world: Tracked<dyn World + '_>,
1282    group: &Subgroup,
1283    citation: &hayagriva::RenderedCitation,
1284    to_entry: impl Fn(&str) -> Option<Location>,
1285) -> SourceResult<Content> {
1286    if group
1287        .citations
1288        .iter()
1289        .all(|sub| sub.form.get(StyleChain::default()).is_none())
1290    {
1291        return Ok(Content::empty());
1292    }
1293
1294    let span = Span::find(group.citations.iter().map(|elem| elem.span()));
1295    let supplement =
1296        |i: usize| group.citations.get(i)?.supplement.get_cloned(StyleChain::default());
1297    let link = |i: usize| to_entry(group.citations.get(i)?.key.resolve().as_str());
1298    let ctx = ShowCtx { world, span, supplement: &supplement, link: &link };
1299
1300    let mut realized =
1301        show_elem_children(&ctx, &citation.citation, None, true).at(span)?;
1302
1303    if group.style.get().settings.class == citationberg::StyleClass::Note
1304        && group.citations.iter().all(|sub| {
1305            matches!(
1306                sub.form.get(StyleChain::default()),
1307                None | Some(CitationForm::Normal)
1308            )
1309        })
1310    {
1311        realized = FootnoteElem::with_content(realized).pack();
1312    }
1313
1314    Ok(realized)
1315}
1316
1317/// Creates a map from citation keys to the citation group containing the first
1318/// citation assigned to a particular bibliography that references the key.
1319///
1320/// This is used by bibliography entries to link back to the first citation that
1321/// references them.
1322fn links_to_citations(groups: &[Subgroup]) -> FxHashMap<ResolvedPicoStr, Location> {
1323    let mut map = FxHashMap::default();
1324    for group in groups {
1325        for child in &group.citations {
1326            let key = child.key.resolve();
1327            map.entry(key).or_insert(group.elem.location().unwrap());
1328        }
1329    }
1330    map
1331}
1332
1333/// Creates a map from a bibliography index + citation key to the corresponding
1334/// entry in the bibliography.
1335///
1336/// This is used by citations to link forward to the bibliography entry they
1337/// reference.
1338fn links_to_entries<'a>(
1339    p: &Preparation,
1340    rendered: &'a [hayagriva::Rendered],
1341) -> FxHashMap<(BibIndex, &'a str), Location> {
1342    let mut links = FxHashMap::default();
1343    for (i, (bib, rendered)) in p.bibs.iter().zip(rendered).enumerate() {
1344        let Some(rendered) = &rendered.bibliography else { continue };
1345        for (k, item) in rendered.items.iter().enumerate() {
1346            links.insert((BibIndex(i), item.key.as_str()), entry_location(bib, k));
1347        }
1348    }
1349    links
1350}
1351
1352/// Each reference is assigned a manually created well-known location that is
1353/// derived from the bibliography's location. This way, citations can link to
1354/// them without having to query for them (which would incur an extra layout
1355/// iteration).
1356fn entry_location(bib: &PreparedBibliography, k: usize) -> Location {
1357    bib.elem.location().unwrap().variant(k + 1)
1358}
1359
1360/// Additional data needed to show hayagriva elements as content.
1361struct ShowCtx<'a> {
1362    /// The world that is used to evaluate mathematical material.
1363    world: Tracked<'a, dyn World + 'a>,
1364    /// The span that is attached to all of the resulting content.
1365    span: Span,
1366    /// Resolves the supplement of i-th citation in the request.
1367    supplement: &'a dyn Fn(usize) -> Option<Content>,
1368    /// Resolves where the i-th citation in the request should link to.
1369    link: &'a dyn Fn(usize) -> Option<Location>,
1370}
1371
1372/// Displays rendered hayagriva elements.
1373///
1374/// The `prefix` can be a separate content storage where `left-margin`
1375/// elements will be accumulated into.
1376///
1377/// `is_citation` dictates whether whitespace at the start of the citation
1378/// will be eliminated. Some CSL styles yield whitespace at the start of
1379/// their citations, which should instead be handled by Typst.
1380fn show_elem_children(
1381    ctx: &ShowCtx,
1382    elems: &hayagriva::ElemChildren,
1383    mut prefix: Option<&mut Option<Content>>,
1384    is_citation: bool,
1385) -> StrResult<Content> {
1386    Ok(Content::sequence(
1387        elems
1388            .0
1389            .iter()
1390            .enumerate()
1391            .map(|(i, elem)| {
1392                show_elem_child(ctx, elem, prefix.as_deref_mut(), is_citation && i == 0)
1393            })
1394            .collect::<StrResult<Vec<_>>>()?,
1395    ))
1396}
1397
1398/// Displays a rendered hayagriva element.
1399fn show_elem_child(
1400    ctx: &ShowCtx,
1401    elem: &hayagriva::ElemChild,
1402    prefix: Option<&mut Option<Content>>,
1403    trim_start: bool,
1404) -> StrResult<Content> {
1405    Ok(match elem {
1406        hayagriva::ElemChild::Text(formatted) => {
1407            show_formatted(ctx, formatted, trim_start)
1408        }
1409        hayagriva::ElemChild::Elem(elem) => show_elem(ctx, elem, prefix)?,
1410        hayagriva::ElemChild::Markup(markup) => show_math(ctx, markup),
1411        hayagriva::ElemChild::Link { text, url } => show_link(ctx, text, url)?,
1412        hayagriva::ElemChild::Transparent { cite_idx, format } => {
1413            show_transparent(ctx, *cite_idx, format)
1414        }
1415    })
1416}
1417
1418/// Displays a block-level element.
1419fn show_elem(
1420    ctx: &ShowCtx,
1421    elem: &hayagriva::Elem,
1422    mut prefix: Option<&mut Option<Content>>,
1423) -> StrResult<Content> {
1424    use citationberg::Display;
1425
1426    let block_level = matches!(elem.display, Some(Display::Block | Display::Indent));
1427
1428    let mut content = show_elem_children(
1429        ctx,
1430        &elem.children,
1431        if block_level { None } else { prefix.as_deref_mut() },
1432        false,
1433    )?;
1434
1435    match elem.display {
1436        Some(Display::Block) => {
1437            content = BlockElem::packed(content).spanned(ctx.span);
1438        }
1439        Some(Display::Indent) => {
1440            content = CslIndentElem::new(content).pack().spanned(ctx.span);
1441        }
1442        Some(Display::LeftMargin) => {
1443            // The `display="left-margin"` attribute is only supported at
1444            // the top-level (when prefix is `Some(_)`). Within a
1445            // block-level container, it is ignored. The CSL spec is not
1446            // specific about this, but it is in line with citeproc.js's
1447            // behaviour.
1448            if let Some(prefix) = prefix {
1449                *prefix.get_or_insert_with(Default::default) += content;
1450                return Ok(Content::empty());
1451            }
1452        }
1453        _ => {}
1454    }
1455
1456    content = content.spanned(ctx.span);
1457
1458    if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta
1459        && let Some(location) = (ctx.link)(i)
1460    {
1461        let alt = content.plain_text();
1462        content = DirectLinkElem::new(location, content, Some(alt)).pack();
1463    }
1464
1465    Ok(content)
1466}
1467
1468/// Displays math.
1469fn show_math(ctx: &ShowCtx, math: &str) -> Content {
1470    let library = ctx.world.library();
1471    (library.routines.eval_string)(
1472        ctx.world,
1473        library,
1474        // TODO: propagate warnings
1475        Sink::new().track_mut(),
1476        EmptyIntrospector.track(),
1477        Context::none().track(),
1478        math,
1479        SpanMode::Uniform(ctx.span),
1480        SyntaxMode::Math,
1481        Scope::new(),
1482    )
1483    .map(Value::display)
1484    .unwrap_or_else(|_| TextElem::packed(math).spanned(ctx.span))
1485}
1486
1487/// Displays a link.
1488fn show_link(
1489    ctx: &ShowCtx,
1490    text: &hayagriva::Formatted,
1491    url: &str,
1492) -> StrResult<Content> {
1493    let dest = Destination::Url(Url::new(url)?);
1494    Ok(LinkElem::new(dest.into(), show_formatted(ctx, text, false))
1495        .pack()
1496        .spanned(ctx.span))
1497}
1498
1499/// Displays transparent pass-through content.
1500fn show_transparent(ctx: &ShowCtx, i: usize, format: &hayagriva::Formatting) -> Content {
1501    let content = (ctx.supplement)(i).unwrap_or_default();
1502    show_with_formatting(content, format)
1503}
1504
1505/// Displays formatted hayagriva text as content.
1506fn show_formatted(
1507    ctx: &ShowCtx,
1508    formatted: &hayagriva::Formatted,
1509    trim_start: bool,
1510) -> Content {
1511    let formatted_text =
1512        if trim_start { formatted.text.trim_start() } else { formatted.text.as_str() };
1513
1514    let content = TextElem::packed(formatted_text).spanned(ctx.span);
1515    show_with_formatting(content, &formatted.formatting)
1516}
1517
1518/// Applies hayagriva formatting to content.
1519fn show_with_formatting(mut content: Content, format: &hayagriva::Formatting) -> Content {
1520    match format.font_style {
1521        citationberg::FontStyle::Normal => {}
1522        citationberg::FontStyle::Italic => {
1523            content = content.emph();
1524        }
1525    }
1526
1527    match format.font_variant {
1528        citationberg::FontVariant::Normal => {}
1529        citationberg::FontVariant::SmallCaps => {
1530            content = SmallcapsElem::new(content).pack();
1531        }
1532    }
1533
1534    match format.font_weight {
1535        citationberg::FontWeight::Normal => {}
1536        citationberg::FontWeight::Bold => {
1537            content = content.strong();
1538        }
1539        citationberg::FontWeight::Light => {
1540            // We don't have a semantic element for "light" and a `StrongElem`
1541            // with negative delta does not have the appropriate semantics, so
1542            // keeping this as a direct style.
1543            content = CslLightElem::new(content).pack();
1544        }
1545    }
1546
1547    match format.text_decoration {
1548        citationberg::TextDecoration::None => {}
1549        citationberg::TextDecoration::Underline => {
1550            content = content.underlined();
1551        }
1552    }
1553
1554    let span = content.span();
1555    match format.vertical_align {
1556        citationberg::VerticalAlign::None => {}
1557        citationberg::VerticalAlign::Baseline => {}
1558        citationberg::VerticalAlign::Sup => {
1559            // Add zero-width weak spacing to make the superscript "sticky".
1560            content =
1561                HElem::hole().clone() + SuperElem::new(content).pack().spanned(span);
1562        }
1563        citationberg::VerticalAlign::Sub => {
1564            content = HElem::hole().clone() + SubElem::new(content).pack().spanned(span);
1565        }
1566    }
1567
1568    content
1569}
1570
1571/// Creates a locale code from language and optionally region.
1572fn locale(lang: Lang, region: Option<Region>) -> citationberg::LocaleCode {
1573    let mut value = String::with_capacity(5);
1574    value.push_str(lang.as_str());
1575    if let Some(region) = region {
1576        value.push('-');
1577        value.push_str(region.as_str())
1578    }
1579    citationberg::LocaleCode(value)
1580}
1581
1582/// Translation of `font-weight="light"` in CSL.
1583///
1584/// We translate `font-weight: "bold"` to `<strong>` since it's likely that the
1585/// CSL spec just talks about bold because it has no notion of semantic
1586/// elements. The benefits of a strict reading of the spec are also rather
1587/// questionable, while using semantic elements makes the bibliography more
1588/// accessible, easier to style, and more portable across export targets.
1589#[elem]
1590pub struct CslLightElem {
1591    #[required]
1592    pub body: Content,
1593}
1594
1595/// Translation of `display="indent"` in CSL.
1596///
1597/// A `display="block"` is simply translated to a Typst `BlockElem`. Similarly,
1598/// we could translate `display="indent"` to a `PadElem`, but (a) it does not
1599/// yet have support in HTML and (b) a `PadElem` described a fixed padding while
1600/// CSL leaves the amount of padding user-defined so it's not a perfect fit.
1601#[elem]
1602pub struct CslIndentElem {
1603    #[required]
1604    pub body: Content,
1605}
1606
1607/// Retrieves all bibliographies and citation groups in the document.
1608///
1609/// This is separate from `QueryIntrospection` so that we can customize the
1610/// diagnostic as the `CiteGroup` is internal. The default query message is also
1611/// not that helpful in this case.
1612#[derive(Debug, Clone, PartialEq, Hash)]
1613struct BibliographyIntrospection(Span);
1614
1615impl Introspect for BibliographyIntrospection {
1616    type Output = EcoVec<Content>;
1617
1618    fn introspect(
1619        &self,
1620        _: &mut Engine,
1621        introspector: Tracked<dyn Introspector + '_>,
1622    ) -> Self::Output {
1623        introspector.query(&Selector::Or(eco_vec![
1624            BibliographyElem::ELEM.select(),
1625            CiteGroup::ELEM.select(),
1626        ]))
1627    }
1628
1629    fn diagnose(&self, _: &History<Self::Output>) -> SourceDiagnostic {
1630        warning!(self.0, "citations and bibliographies did not stabilize")
1631    }
1632}
1633
1634/// The diagnostic when a citation wasn't found in the pre-formatted list.
1635fn citation_could_not_be_located() -> HintedString {
1636    error!(
1637        "citation could not be located";
1638        hint: "this citation is not stably present in the document";
1639        hint: "this can be caused by measurement or introspection";
1640    )
1641}
1642
1643/// The diagnostic when a bibliography wasn't found in the pre-formatted list.
1644fn bibliography_could_not_be_located() -> HintedString {
1645    error!(
1646        "bibliography could not be located";
1647        hint: "this bibliography is not stably present in the document";
1648        hint: "this can be caused by measurement or introspection";
1649    )
1650}
1651
1652/// The diagnostic when a citation is not picked up by any bibliography.
1653fn uncovered_citation(
1654    citation: &Packed<CiteElem>,
1655    bibs: &[PreparedBibliography],
1656) -> SourceDiagnostic {
1657    let span = citation.span();
1658    let key = citation.key.resolve();
1659    if bibs.is_empty() {
1660        error!(span, "the document does not contain a bibliography")
1661    } else if let Some(bib) =
1662        bibs.iter().find(|bib| bib.elem.sources.derived.has(citation.key))
1663    {
1664        error!(
1665            span,
1666            "citation is not covered by any bibliography";
1667            hint[bib.elem.span()]:
1668            "a bibliography containing the key `{key}` exists, \
1669             but its `target` excludes this citation";
1670        )
1671    } else {
1672        error!(
1673            span,
1674            "citation key `{key}` is not present in {} bibliography",
1675            if bibs.len() == 1 { "the" } else { "any" },
1676        )
1677    }
1678}
1679
1680/// The diagnostic when a citation is explicitly targeted by a bibliography,
1681/// but its key does not exist in said bibliography.
1682fn key_does_not_exist(
1683    citation: &Packed<CiteElem>,
1684    bib: &PreparedBibliography,
1685) -> SourceDiagnostic {
1686    error!(
1687        citation.span(),
1688        "key `{}` does not exist in the bibliography",
1689        citation.key.resolve();
1690        hint[bib.elem.span()]: "the citation was assigned to this bibliography";
1691    )
1692}
1693
1694/// The error message when a CSL style cannot be used for bibliographies.
1695#[cold]
1696fn style_unsuitable(source: &CslSource) -> EcoString {
1697    match source {
1698        CslSource::Named(style, _) => eco_format!(
1699            "CSL style \"{}\" is not suitable for bibliographies",
1700            style.display_name()
1701        ),
1702        CslSource::Normal(..) => "CSL style is not suitable for bibliographies".into(),
1703    }
1704}
1705
1706#[cfg(test)]
1707mod tests {
1708    use super::*;
1709
1710    #[test]
1711    fn test_bibliography_load_builtin_styles() {
1712        for &archived in ArchivedStyle::all() {
1713            let _ = CslStyle::from_archived(archived);
1714        }
1715    }
1716
1717    #[test]
1718    fn test_csl_source_cast_info_include_all_names() {
1719        let CastInfo::Union(cast_info) = CslSource::input() else {
1720            panic!("the cast info of CslSource should be a union");
1721        };
1722
1723        let missing: Vec<_> = ArchivedStyle::all()
1724            .iter()
1725            .flat_map(|style| style.names())
1726            .filter(|name| {
1727                let found = cast_info.iter().any(|info| match info {
1728                    CastInfo::Value(Value::Str(n), _) => n.as_str() == **name,
1729                    _ => false,
1730                });
1731                !found
1732            })
1733            .collect();
1734
1735        assert!(
1736            missing.is_empty(),
1737            "missing style names in CslSource cast info: '{missing:?}'"
1738        );
1739    }
1740}