typst_library/model/
bibliography.rs

1use std::any::TypeId;
2use std::ffi::OsStr;
3use std::fmt::{self, Debug, Formatter};
4use std::num::NonZeroUsize;
5use std::path::Path;
6use std::sync::{Arc, LazyLock};
7
8use comemo::{Track, Tracked};
9use ecow::{EcoString, EcoVec, eco_format};
10use hayagriva::archive::ArchivedStyle;
11use hayagriva::io::BibLaTeXError;
12use hayagriva::{
13    BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, Library,
14    SpecificLocator, TransparentLocator, citationberg,
15};
16use indexmap::IndexMap;
17use rustc_hash::{FxBuildHasher, FxHashMap};
18use smallvec::SmallVec;
19use typst_syntax::{Span, Spanned, SyntaxMode};
20use typst_utils::{ManuallyHash, NonZeroExt, PicoStr};
21
22use crate::World;
23use crate::diag::{
24    At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos, SourceResult,
25    StrResult, bail, error, warning,
26};
27use crate::engine::{Engine, Sink};
28use crate::foundations::{
29    Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement,
30    OneOrMultiple, Packed, Reflect, Scope, ShowSet, Smart, StyleChain, Styles,
31    Synthesize, Value, elem,
32};
33use crate::introspection::{Introspector, Locatable, Location};
34use crate::layout::{BlockBody, BlockElem, Em, HElem, PadElem};
35use crate::loading::{DataSource, Load, LoadSource, Loaded, format_yaml_error};
36use crate::model::{
37    CitationForm, CiteGroup, Destination, DirectLinkElem, FootnoteElem, HeadingElem,
38    LinkElem, Url,
39};
40use crate::routines::Routines;
41use crate::text::{Lang, LocalName, Region, SmallcapsElem, SubElem, SuperElem, TextElem};
42
43/// A bibliography / reference listing.
44///
45/// You can create a new bibliography by calling this function with a path
46/// to a bibliography file in either one of two formats:
47///
48/// - A Hayagriva `.yaml`/`.yml` file. Hayagriva is a new bibliography
49///   file format designed for use with Typst. Visit its
50///   [documentation](https://github.com/typst/hayagriva/blob/main/docs/file-format.md)
51///   for more details.
52/// - A BibLaTeX `.bib` file.
53///
54/// As soon as you add a bibliography somewhere in your document, you can start
55/// citing things with reference syntax (`[@key]`) or explicit calls to the
56/// [citation]($cite) function (`[#cite(<key>)]`). The bibliography will only
57/// show entries for works that were referenced in the document.
58///
59/// # Styles
60/// Typst offers a wide selection of built-in
61/// [citation and bibliography styles]($bibliography.style). Beyond those, you
62/// can add and use custom [CSL](https://citationstyles.org/) (Citation Style
63/// Language) files. Wondering which style to use? Here are some good defaults
64/// based on what discipline you're working in:
65///
66/// | Fields          | Typical Styles                                         |
67/// |-----------------|--------------------------------------------------------|
68/// | Engineering, IT | `{"ieee"}`                                             |
69/// | Psychology, Life Sciences | `{"apa"}`                                    |
70/// | Social sciences | `{"chicago-author-date"}`                              |
71/// | Humanities      | `{"mla"}`, `{"chicago-notes"}`, `{"harvard-cite-them-right"}` |
72/// | Economics       | `{"harvard-cite-them-right"}`                          |
73/// | Physics         | `{"american-physics-society"}`                         |
74///
75/// # Example
76/// ```example
77/// This was already noted by
78/// pirates long ago. @arrgh
79///
80/// Multiple sources say ...
81/// @arrgh @netwok.
82///
83/// #bibliography("works.bib")
84/// ```
85#[elem(Locatable, Synthesize, ShowSet, LocalName)]
86pub struct BibliographyElem {
87    /// One or multiple paths to or raw bytes for Hayagriva `.yaml` and/or
88    /// BibLaTeX `.bib` files.
89    ///
90    /// This can be a:
91    /// - A path string to load a bibliography file from the given path. For
92    ///   more details about paths, see the [Paths section]($syntax/#paths).
93    /// - Raw bytes from which the bibliography should be decoded.
94    /// - An array where each item is one of the above.
95    #[required]
96    #[parse(
97        let sources = args.expect("sources")?;
98        Bibliography::load(engine.world, sources)?
99    )]
100    pub sources: Derived<OneOrMultiple<DataSource>, Bibliography>,
101
102    /// The title of the bibliography.
103    ///
104    /// - When set to `{auto}`, an appropriate title for the
105    ///   [text language]($text.lang) will be used. This is the default.
106    /// - When set to `{none}`, the bibliography will not have a title.
107    /// - A custom title can be set by passing content.
108    ///
109    /// The bibliography's heading will not be numbered by default, but you can
110    /// force it to be with a show-set rule:
111    /// `{show bibliography: set heading(numbering: "1.")}`
112    pub title: Smart<Option<Content>>,
113
114    /// Whether to include all works from the given bibliography files, even
115    /// those that weren't cited in the document.
116    ///
117    /// To selectively add individual cited works without showing them, you can
118    /// also use the `cite` function with [`form`]($cite.form) set to `{none}`.
119    #[default(false)]
120    pub full: bool,
121
122    /// The bibliography style.
123    ///
124    /// This can be:
125    /// - A string with the name of one of the built-in styles (see below). Some
126    ///   of the styles listed below appear twice, once with their full name and
127    ///   once with a short alias.
128    /// - A path string to a [CSL file](https://citationstyles.org/). For more
129    ///   details about paths, see the [Paths section]($syntax/#paths).
130    /// - Raw bytes from which a CSL style should be decoded.
131    #[parse(match args.named::<Spanned<CslSource>>("style")? {
132        Some(source) => Some(CslStyle::load(engine, source)?),
133        None => None,
134    })]
135    #[default({
136        let default = ArchivedStyle::InstituteOfElectricalAndElectronicsEngineers;
137        Derived::new(CslSource::Named(default, None), CslStyle::from_archived(default))
138    })]
139    pub style: Derived<CslSource, CslStyle>,
140
141    /// The language setting where the bibliography is.
142    #[internal]
143    #[synthesized]
144    pub lang: Lang,
145
146    /// The region setting where the bibliography is.
147    #[internal]
148    #[synthesized]
149    pub region: Option<Region>,
150}
151
152impl BibliographyElem {
153    /// Find the document's bibliography.
154    pub fn find(introspector: Tracked<Introspector>) -> StrResult<Packed<Self>> {
155        let query = introspector.query(&Self::ELEM.select());
156        let mut iter = query.iter();
157        let Some(elem) = iter.next() else {
158            bail!("the document does not contain a bibliography");
159        };
160
161        if iter.next().is_some() {
162            bail!("multiple bibliographies are not yet supported");
163        }
164
165        Ok(elem.to_packed::<Self>().unwrap().clone())
166    }
167
168    /// Whether the bibliography contains the given key.
169    pub fn has(engine: &Engine, key: Label) -> bool {
170        engine
171            .introspector
172            .query(&Self::ELEM.select())
173            .iter()
174            .any(|elem| elem.to_packed::<Self>().unwrap().sources.derived.has(key))
175    }
176
177    /// Find all bibliography keys.
178    pub fn keys(introspector: Tracked<Introspector>) -> Vec<(Label, Option<EcoString>)> {
179        let mut vec = vec![];
180        for elem in introspector.query(&Self::ELEM.select()).iter() {
181            let this = elem.to_packed::<Self>().unwrap();
182            for (key, entry) in this.sources.derived.iter() {
183                let detail = entry.title().map(|title| title.value.to_str().into());
184                vec.push((key, detail))
185            }
186        }
187        vec
188    }
189}
190
191impl Packed<BibliographyElem> {
192    /// Produces the heading for the bibliography, if any.
193    pub fn realize_title(&self, styles: StyleChain) -> Option<Content> {
194        self.title
195            .get_cloned(styles)
196            .unwrap_or_else(|| {
197                Some(TextElem::packed(Packed::<BibliographyElem>::local_name_in(styles)))
198            })
199            .map(|title| {
200                HeadingElem::new(title)
201                    .with_depth(NonZeroUsize::ONE)
202                    .pack()
203                    .spanned(self.span())
204            })
205    }
206}
207
208impl Synthesize for Packed<BibliographyElem> {
209    fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
210        let elem = self.as_mut();
211        elem.lang = Some(styles.get(TextElem::lang));
212        elem.region = Some(styles.get(TextElem::region));
213        Ok(())
214    }
215}
216
217impl ShowSet for Packed<BibliographyElem> {
218    fn show_set(&self, _: StyleChain) -> Styles {
219        const INDENT: Em = Em::new(1.0);
220        let mut out = Styles::new();
221        out.set(HeadingElem::numbering, None);
222        out.set(PadElem::left, INDENT.into());
223        out
224    }
225}
226
227impl LocalName for Packed<BibliographyElem> {
228    const KEY: &'static str = "bibliography";
229}
230
231/// A loaded bibliography.
232#[derive(Clone, PartialEq, Hash)]
233pub struct Bibliography(
234    Arc<ManuallyHash<IndexMap<Label, hayagriva::Entry, FxBuildHasher>>>,
235);
236
237impl Bibliography {
238    /// Load a bibliography from data sources.
239    fn load(
240        world: Tracked<dyn World + '_>,
241        sources: Spanned<OneOrMultiple<DataSource>>,
242    ) -> SourceResult<Derived<OneOrMultiple<DataSource>, Self>> {
243        let loaded = sources.load(world)?;
244        let bibliography = Self::decode(&loaded)?;
245        Ok(Derived::new(sources.v, bibliography))
246    }
247
248    /// Decode a bibliography from loaded data sources.
249    #[comemo::memoize]
250    #[typst_macros::time(name = "load bibliography")]
251    fn decode(data: &[Loaded]) -> SourceResult<Bibliography> {
252        let mut map = IndexMap::default();
253        let mut duplicates = Vec::<EcoString>::new();
254
255        // We might have multiple bib/yaml files
256        for d in data.iter() {
257            let library = decode_library(d)?;
258            for entry in library {
259                let label = Label::new(PicoStr::intern(entry.key()))
260                    .ok_or("bibliography contains entry with empty key")
261                    .at(d.source.span)?;
262
263                match map.entry(label) {
264                    indexmap::map::Entry::Vacant(vacant) => {
265                        vacant.insert(entry);
266                    }
267                    indexmap::map::Entry::Occupied(_) => {
268                        duplicates.push(entry.key().into());
269                    }
270                }
271            }
272        }
273
274        if !duplicates.is_empty() {
275            // TODO: Store spans of entries for duplicate key error messages.
276            // Requires hayagriva entries to store their location, which should
277            // be fine, since they are 1kb anyway.
278            let span = data.first().unwrap().source.span;
279            bail!(span, "duplicate bibliography keys: {}", duplicates.join(", "));
280        }
281
282        Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data)))))
283    }
284
285    fn has(&self, key: Label) -> bool {
286        self.0.contains_key(&key)
287    }
288
289    fn get(&self, key: Label) -> Option<&hayagriva::Entry> {
290        self.0.get(&key)
291    }
292
293    fn iter(&self) -> impl Iterator<Item = (Label, &hayagriva::Entry)> {
294        self.0.iter().map(|(&k, v)| (k, v))
295    }
296}
297
298impl Debug for Bibliography {
299    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
300        f.debug_set().entries(self.0.keys()).finish()
301    }
302}
303
304/// Decode on library from one data source.
305fn decode_library(loaded: &Loaded) -> SourceResult<Library> {
306    let data = loaded.data.as_str().within(loaded)?;
307
308    if let LoadSource::Path(file_id) = loaded.source.v {
309        // If we got a path, use the extension to determine whether it is
310        // YAML or BibLaTeX.
311        let ext = file_id
312            .vpath()
313            .as_rooted_path()
314            .extension()
315            .and_then(OsStr::to_str)
316            .unwrap_or_default();
317
318        match ext.to_lowercase().as_str() {
319            "yml" | "yaml" => hayagriva::io::from_yaml_str(data)
320                .map_err(format_yaml_error)
321                .within(loaded),
322            "bib" => hayagriva::io::from_biblatex_str(data)
323                .map_err(format_biblatex_error)
324                .within(loaded),
325            _ => bail!(
326                loaded.source.span,
327                "unknown bibliography format (must be .yaml/.yml or .bib)"
328            ),
329        }
330    } else {
331        // If we just got bytes, we need to guess. If it can be decoded as
332        // hayagriva YAML, we'll use that.
333        let haya_err = match hayagriva::io::from_yaml_str(data) {
334            Ok(library) => return Ok(library),
335            Err(err) => err,
336        };
337
338        // If it can be decoded as BibLaTeX, we use that instead.
339        let bib_errs = match hayagriva::io::from_biblatex_str(data) {
340            // If the file is almost valid yaml, but contains no `@` character
341            // it will be successfully parsed as an empty BibLaTeX library,
342            // since BibLaTeX does support arbitrary text outside of entries.
343            Ok(library) if !library.is_empty() => return Ok(library),
344            Ok(_) => None,
345            Err(err) => Some(err),
346        };
347
348        // If neither decoded correctly, check whether `:` or `{` appears
349        // more often to guess whether it's more likely to be YAML or BibLaTeX
350        // and emit the more appropriate error.
351        let mut yaml = 0;
352        let mut biblatex = 0;
353        for c in data.chars() {
354            match c {
355                ':' => yaml += 1,
356                '{' => biblatex += 1,
357                _ => {}
358            }
359        }
360
361        match bib_errs {
362            Some(bib_errs) if biblatex >= yaml => {
363                Err(format_biblatex_error(bib_errs)).within(loaded)
364            }
365            _ => Err(format_yaml_error(haya_err)).within(loaded),
366        }
367    }
368}
369
370/// Format a BibLaTeX loading error.
371fn format_biblatex_error(errors: Vec<BibLaTeXError>) -> LoadError {
372    // TODO: return multiple errors?
373    let Some(error) = errors.into_iter().next() else {
374        // TODO: can this even happen, should we just unwrap?
375        return LoadError::new(
376            ReportPos::None,
377            "failed to parse BibLaTeX",
378            "something went wrong",
379        );
380    };
381
382    let (range, msg) = match error {
383        BibLaTeXError::Parse(error) => (error.span, error.kind.to_string()),
384        BibLaTeXError::Type(error) => (error.span, error.kind.to_string()),
385    };
386
387    LoadError::new(range, "failed to parse BibLaTeX", msg)
388}
389
390/// A loaded CSL style.
391#[derive(Debug, Clone, PartialEq, Hash)]
392pub struct CslStyle(Arc<ManuallyHash<citationberg::IndependentStyle>>);
393
394impl CslStyle {
395    /// Load a CSL style from a data source.
396    pub fn load(
397        engine: &mut Engine,
398        Spanned { v: source, span }: Spanned<CslSource>,
399    ) -> SourceResult<Derived<CslSource, Self>> {
400        let style = match &source {
401            CslSource::Named(style, deprecation) => {
402                if let Some(message) = deprecation {
403                    engine.sink.warn(warning!(span, "{message}"));
404                }
405                Self::from_archived(*style)
406            }
407            CslSource::Normal(source) => {
408                let loaded = Spanned::new(source, span).load(engine.world)?;
409                Self::from_data(&loaded.data).within(&loaded)?
410            }
411        };
412        Ok(Derived::new(source, style))
413    }
414
415    /// Load a built-in CSL style.
416    #[comemo::memoize]
417    pub fn from_archived(archived: ArchivedStyle) -> CslStyle {
418        match archived.get() {
419            citationberg::Style::Independent(style) => Self(Arc::new(ManuallyHash::new(
420                style,
421                typst_utils::hash128(&(TypeId::of::<ArchivedStyle>(), archived)),
422            ))),
423            // Ensured by `test_bibliography_load_builtin_styles`.
424            _ => unreachable!("archive should not contain dependant styles"),
425        }
426    }
427
428    /// Load a CSL style from file contents.
429    #[comemo::memoize]
430    pub fn from_data(bytes: &Bytes) -> LoadResult<CslStyle> {
431        let text = bytes.as_str()?;
432        citationberg::IndependentStyle::from_xml(text)
433            .map(|style| {
434                Self(Arc::new(ManuallyHash::new(
435                    style,
436                    typst_utils::hash128(&(TypeId::of::<Bytes>(), bytes)),
437                )))
438            })
439            .map_err(|err| {
440                LoadError::new(ReportPos::None, "failed to load CSL style", err)
441            })
442    }
443
444    /// Get the underlying independent style.
445    pub fn get(&self) -> &citationberg::IndependentStyle {
446        self.0.as_ref()
447    }
448}
449
450/// Source for a CSL style.
451#[derive(Debug, Clone, PartialEq, Hash)]
452pub enum CslSource {
453    /// A predefined named style and potentially a deprecation warning.
454    Named(ArchivedStyle, Option<&'static str>),
455    /// A normal data source.
456    Normal(DataSource),
457}
458
459impl Reflect for CslSource {
460    #[comemo::memoize]
461    fn input() -> CastInfo {
462        let source = std::iter::once(DataSource::input());
463
464        /// All possible names and their short documentation for `ArchivedStyle`, including aliases.
465        static ARCHIVED_STYLE_NAMES: LazyLock<Vec<(&&str, &'static str)>> =
466            LazyLock::new(|| {
467                ArchivedStyle::all()
468                    .iter()
469                    .flat_map(|name| {
470                        let (main_name, aliases) = name
471                            .names()
472                            .split_first()
473                            .expect("all ArchivedStyle should have at least one name");
474
475                        std::iter::once((main_name, name.display_name())).chain(
476                            aliases.iter().map(move |alias| {
477                                // Leaking is okay here, because we are in a `LazyLock`.
478                                let docs: &'static str = Box::leak(
479                                    format!("A short alias of `{main_name}`")
480                                        .into_boxed_str(),
481                                );
482                                (alias, docs)
483                            }),
484                        )
485                    })
486                    .collect()
487            });
488        let names = ARCHIVED_STYLE_NAMES
489            .iter()
490            .map(|(value, docs)| CastInfo::Value(value.into_value(), docs));
491
492        CastInfo::Union(source.into_iter().chain(names).collect())
493    }
494
495    fn output() -> CastInfo {
496        DataSource::output()
497    }
498
499    fn castable(value: &Value) -> bool {
500        DataSource::castable(value)
501    }
502}
503
504impl FromValue for CslSource {
505    fn from_value(value: Value) -> HintedStrResult<Self> {
506        if EcoString::castable(&value) {
507            let string = EcoString::from_value(value.clone())?;
508            if Path::new(string.as_str()).extension().is_none() {
509                let mut warning = None;
510                if string.as_str() == "chicago-fullnotes" {
511                    warning = Some(
512                        "style \"chicago-fullnotes\" has been deprecated \
513                         in favor of \"chicago-notes\"",
514                    );
515                } else if string.as_str() == "modern-humanities-research-association" {
516                    warning = Some(
517                        "style \"modern-humanities-research-association\" \
518                         has been deprecated in favor of \
519                         \"modern-humanities-research-association-notes\"",
520                    );
521                }
522
523                let style = ArchivedStyle::by_name(&string)
524                    .ok_or_else(|| eco_format!("unknown style: {}", string))?;
525                return Ok(CslSource::Named(style, warning));
526            }
527        }
528
529        DataSource::from_value(value).map(CslSource::Normal)
530    }
531}
532
533impl IntoValue for CslSource {
534    fn into_value(self) -> Value {
535        match self {
536            // We prefer the shorter names which are at the back of the array.
537            Self::Named(v, _) => v.names().last().unwrap().into_value(),
538            Self::Normal(v) => v.into_value(),
539        }
540    }
541}
542
543/// Fully formatted citations and references, generated once (through
544/// memoization) for the whole document. This setup is necessary because
545/// citation formatting is inherently stateful and we need access to all
546/// citations to do it.
547pub struct Works {
548    /// Maps from the location of a citation group to its rendered content.
549    pub citations: FxHashMap<Location, SourceResult<Content>>,
550    /// Lists all references in the bibliography, with optional prefix, or
551    /// `None` if the citation style can't be used for bibliographies.
552    pub references: Option<Vec<(Option<Content>, Content, Location)>>,
553    /// Whether the bibliography should have hanging indent.
554    pub hanging_indent: bool,
555}
556
557impl Works {
558    /// Generate all citations and the whole bibliography.
559    pub fn generate(engine: &Engine) -> StrResult<Arc<Works>> {
560        Self::generate_impl(engine.routines, engine.world, engine.introspector)
561    }
562
563    /// The internal implementation of [`Works::generate`].
564    #[comemo::memoize]
565    fn generate_impl(
566        routines: &Routines,
567        world: Tracked<dyn World + '_>,
568        introspector: Tracked<Introspector>,
569    ) -> StrResult<Arc<Works>> {
570        let mut generator = Generator::new(routines, world, introspector)?;
571        let rendered = generator.drive();
572        let works = generator.display(&rendered)?;
573        Ok(Arc::new(works))
574    }
575
576    /// Extracts the generated references, failing with an error if none have
577    /// been generated.
578    pub fn references<'a>(
579        &'a self,
580        elem: &Packed<BibliographyElem>,
581        styles: StyleChain,
582    ) -> SourceResult<&'a [(Option<Content>, Content, Location)]> {
583        self.references
584            .as_deref()
585            .ok_or_else(|| match elem.style.get_ref(styles).source {
586                CslSource::Named(style, _) => eco_format!(
587                    "CSL style \"{}\" is not suitable for bibliographies",
588                    style.display_name()
589                ),
590                CslSource::Normal(..) => {
591                    "CSL style is not suitable for bibliographies".into()
592                }
593            })
594            .at(elem.span())
595    }
596}
597
598/// Context for generating the bibliography.
599struct Generator<'a> {
600    /// The routines that are used to evaluate mathematical material in citations.
601    routines: &'a Routines,
602    /// The world that is used to evaluate mathematical material in citations.
603    world: Tracked<'a, dyn World + 'a>,
604    /// The document's bibliography.
605    bibliography: Packed<BibliographyElem>,
606    /// The document's citation groups.
607    groups: EcoVec<Content>,
608    /// Details about each group that are accumulated while driving hayagriva's
609    /// bibliography driver and needed when processing hayagriva's output.
610    infos: Vec<GroupInfo>,
611    /// Citations with unresolved keys.
612    failures: FxHashMap<Location, SourceResult<Content>>,
613}
614
615/// Details about a group of merged citations. All citations are put into groups
616/// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two).
617/// Even single citations will be put into groups of length one.
618struct GroupInfo {
619    /// The group's location.
620    location: Location,
621    /// The group's span.
622    span: Span,
623    /// Whether the group should be displayed in a footnote.
624    footnote: bool,
625    /// Details about the groups citations.
626    subinfos: SmallVec<[CiteInfo; 1]>,
627}
628
629/// Details about a citation item in a request.
630struct CiteInfo {
631    /// The citation's key.
632    key: Label,
633    /// The citation's supplement.
634    supplement: Option<Content>,
635    /// Whether this citation was hidden.
636    hidden: bool,
637}
638
639impl<'a> Generator<'a> {
640    /// Create a new generator.
641    fn new(
642        routines: &'a Routines,
643        world: Tracked<'a, dyn World + 'a>,
644        introspector: Tracked<Introspector>,
645    ) -> StrResult<Self> {
646        let bibliography = BibliographyElem::find(introspector)?;
647        let groups = introspector.query(&CiteGroup::ELEM.select());
648        let infos = Vec::with_capacity(groups.len());
649        Ok(Self {
650            routines,
651            world,
652            bibliography,
653            groups,
654            infos,
655            failures: FxHashMap::default(),
656        })
657    }
658
659    /// Drives hayagriva's citation driver.
660    fn drive(&mut self) -> hayagriva::Rendered {
661        static LOCALES: LazyLock<Vec<citationberg::Locale>> =
662            LazyLock::new(hayagriva::archive::locales);
663
664        let database = &self.bibliography.sources.derived;
665        let bibliography_style =
666            &self.bibliography.style.get_ref(StyleChain::default()).derived;
667
668        // Process all citation groups.
669        let mut driver = BibliographyDriver::new();
670        for elem in &self.groups {
671            let group = elem.to_packed::<CiteGroup>().unwrap();
672            let location = elem.location().unwrap();
673            let children = &group.children;
674
675            // Groups should never be empty.
676            let Some(first) = children.first() else { continue };
677
678            let mut subinfos = SmallVec::with_capacity(children.len());
679            let mut items = Vec::with_capacity(children.len());
680            let mut errors = EcoVec::new();
681            let mut normal = true;
682
683            // Create infos and items for each child in the group.
684            for child in children {
685                let Some(entry) = database.get(child.key) else {
686                    errors.push(error!(
687                        child.span(),
688                        "key `{}` does not exist in the bibliography",
689                        child.key.resolve()
690                    ));
691                    continue;
692                };
693
694                let supplement = child.supplement.get_cloned(StyleChain::default());
695                let locator = supplement.as_ref().map(|c| {
696                    SpecificLocator(
697                        citationberg::taxonomy::Locator::Custom,
698                        hayagriva::LocatorPayload::Transparent(TransparentLocator::new(
699                            c.clone(),
700                        )),
701                    )
702                });
703
704                let mut hidden = false;
705                let special_form = match child.form.get(StyleChain::default()) {
706                    None => {
707                        hidden = true;
708                        None
709                    }
710                    Some(CitationForm::Normal) => None,
711                    Some(CitationForm::Prose) => Some(hayagriva::CitePurpose::Prose),
712                    Some(CitationForm::Full) => Some(hayagriva::CitePurpose::Full),
713                    Some(CitationForm::Author) => Some(hayagriva::CitePurpose::Author),
714                    Some(CitationForm::Year) => Some(hayagriva::CitePurpose::Year),
715                };
716
717                normal &= special_form.is_none();
718                subinfos.push(CiteInfo { key: child.key, supplement, hidden });
719                items.push(CitationItem::new(entry, locator, None, hidden, special_form));
720            }
721
722            if !errors.is_empty() {
723                self.failures.insert(location, Err(errors));
724                continue;
725            }
726
727            let style = match first.style.get_ref(StyleChain::default()) {
728                Smart::Auto => bibliography_style.get(),
729                Smart::Custom(style) => style.derived.get(),
730            };
731
732            self.infos.push(GroupInfo {
733                location,
734                subinfos,
735                span: first.span(),
736                footnote: normal
737                    && style.settings.class == citationberg::StyleClass::Note,
738            });
739
740            driver.citation(CitationRequest::new(
741                items,
742                style,
743                Some(locale(first.lang.unwrap_or(Lang::ENGLISH), first.region.flatten())),
744                &LOCALES,
745                None,
746            ));
747        }
748
749        let locale = locale(
750            self.bibliography.lang.unwrap_or(Lang::ENGLISH),
751            self.bibliography.region.flatten(),
752        );
753
754        // Add hidden items for everything if we should print the whole
755        // bibliography.
756        if self.bibliography.full.get(StyleChain::default()) {
757            for (_, entry) in database.iter() {
758                driver.citation(CitationRequest::new(
759                    vec![CitationItem::new(entry, None, None, true, None)],
760                    bibliography_style.get(),
761                    Some(locale.clone()),
762                    &LOCALES,
763                    None,
764                ));
765            }
766        }
767
768        driver.finish(BibliographyRequest {
769            style: bibliography_style.get(),
770            locale: Some(locale),
771            locale_files: &LOCALES,
772        })
773    }
774
775    /// Displays hayagriva's output as content for the citations and references.
776    fn display(&mut self, rendered: &hayagriva::Rendered) -> StrResult<Works> {
777        let citations = self.display_citations(rendered)?;
778        let references = self.display_references(rendered)?;
779        let hanging_indent =
780            rendered.bibliography.as_ref().is_some_and(|b| b.hanging_indent);
781        Ok(Works { citations, references, hanging_indent })
782    }
783
784    /// Display the citation groups.
785    fn display_citations(
786        &mut self,
787        rendered: &hayagriva::Rendered,
788    ) -> StrResult<FxHashMap<Location, SourceResult<Content>>> {
789        // Determine for each citation key where in the bibliography it is,
790        // so that we can link there.
791        let mut links = FxHashMap::default();
792        if let Some(bibliography) = &rendered.bibliography {
793            let location = self.bibliography.location().unwrap();
794            for (k, item) in bibliography.items.iter().enumerate() {
795                links.insert(item.key.as_str(), location.variant(k + 1));
796            }
797        }
798
799        let mut output = std::mem::take(&mut self.failures);
800        for (info, citation) in self.infos.iter().zip(&rendered.citations) {
801            let supplement = |i: usize| info.subinfos.get(i)?.supplement.clone();
802            let link = |i: usize| {
803                links.get(info.subinfos.get(i)?.key.resolve().as_str()).copied()
804            };
805
806            let renderer = ElemRenderer {
807                routines: self.routines,
808                world: self.world,
809                span: info.span,
810                supplement: &supplement,
811                link: &link,
812            };
813
814            let content = if info.subinfos.iter().all(|sub| sub.hidden) {
815                Content::empty()
816            } else {
817                let mut content =
818                    renderer.display_elem_children(&citation.citation, None, true)?;
819
820                if info.footnote {
821                    content = FootnoteElem::with_content(content).pack();
822                }
823
824                content
825            };
826
827            output.insert(info.location, Ok(content));
828        }
829
830        Ok(output)
831    }
832
833    /// Display the bibliography references.
834    #[allow(clippy::type_complexity)]
835    fn display_references(
836        &self,
837        rendered: &hayagriva::Rendered,
838    ) -> StrResult<Option<Vec<(Option<Content>, Content, Location)>>> {
839        let Some(rendered) = &rendered.bibliography else { return Ok(None) };
840
841        // Determine for each citation key where it first occurred, so that we
842        // can link there.
843        let mut first_occurrences = FxHashMap::default();
844        for info in &self.infos {
845            for subinfo in &info.subinfos {
846                let key = subinfo.key.resolve();
847                first_occurrences.entry(key).or_insert(info.location);
848            }
849        }
850
851        // The location of the bibliography.
852        let location = self.bibliography.location().unwrap();
853
854        let mut output = vec![];
855        for (k, item) in rendered.items.iter().enumerate() {
856            let renderer = ElemRenderer {
857                routines: self.routines,
858                world: self.world,
859                span: self.bibliography.span(),
860                supplement: &|_| None,
861                link: &|_| None,
862            };
863
864            // Each reference is assigned a manually created well-known location
865            // that is derived from the bibliography's location. This way,
866            // citations can link to them.
867            let backlink = location.variant(k + 1);
868
869            // Render the first field.
870            let mut prefix = item
871                .first_field
872                .as_ref()
873                .map(|elem| renderer.display_elem_child(elem, None, false))
874                .transpose()?;
875
876            // Render the main reference content.
877            let reference = renderer.display_elem_children(
878                &item.content,
879                Some(&mut prefix),
880                false,
881            )?;
882
883            let prefix = prefix.map(|content| {
884                if let Some(location) = first_occurrences.get(item.key.as_str()) {
885                    let alt = content.plain_text();
886                    let body = content.spanned(self.bibliography.span());
887                    DirectLinkElem::new(*location, body, Some(alt)).pack()
888                } else {
889                    content
890                }
891            });
892
893            output.push((prefix, reference, backlink));
894        }
895
896        Ok(Some(output))
897    }
898}
899
900/// Renders hayagriva elements into content.
901struct ElemRenderer<'a> {
902    /// The routines that is used to evaluate mathematical material in citations.
903    routines: &'a Routines,
904    /// The world that is used to evaluate mathematical material.
905    world: Tracked<'a, dyn World + 'a>,
906    /// The span that is attached to all of the resulting content.
907    span: Span,
908    /// Resolves the supplement of i-th citation in the request.
909    supplement: &'a dyn Fn(usize) -> Option<Content>,
910    /// Resolves where the i-th citation in the request should link to.
911    link: &'a dyn Fn(usize) -> Option<Location>,
912}
913
914impl ElemRenderer<'_> {
915    /// Display rendered hayagriva elements.
916    ///
917    /// The `prefix` can be a separate content storage where `left-margin`
918    /// elements will be accumulated into.
919    ///
920    /// `is_citation` dictates whether whitespace at the start of the citation
921    /// will be eliminated. Some CSL styles yield whitespace at the start of
922    /// their citations, which should instead be handled by Typst.
923    fn display_elem_children(
924        &self,
925        elems: &hayagriva::ElemChildren,
926        mut prefix: Option<&mut Option<Content>>,
927        is_citation: bool,
928    ) -> StrResult<Content> {
929        Ok(Content::sequence(
930            elems
931                .0
932                .iter()
933                .enumerate()
934                .map(|(i, elem)| {
935                    self.display_elem_child(
936                        elem,
937                        prefix.as_deref_mut(),
938                        is_citation && i == 0,
939                    )
940                })
941                .collect::<StrResult<Vec<_>>>()?,
942        ))
943    }
944
945    /// Display a rendered hayagriva element.
946    fn display_elem_child(
947        &self,
948        elem: &hayagriva::ElemChild,
949        prefix: Option<&mut Option<Content>>,
950        trim_start: bool,
951    ) -> StrResult<Content> {
952        Ok(match elem {
953            hayagriva::ElemChild::Text(formatted) => {
954                self.display_formatted(formatted, trim_start)
955            }
956            hayagriva::ElemChild::Elem(elem) => self.display_elem(elem, prefix)?,
957            hayagriva::ElemChild::Markup(markup) => self.display_math(markup),
958            hayagriva::ElemChild::Link { text, url } => self.display_link(text, url)?,
959            hayagriva::ElemChild::Transparent { cite_idx, format } => {
960                self.display_transparent(*cite_idx, format)
961            }
962        })
963    }
964
965    /// Display a block-level element.
966    fn display_elem(
967        &self,
968        elem: &hayagriva::Elem,
969        mut prefix: Option<&mut Option<Content>>,
970    ) -> StrResult<Content> {
971        use citationberg::Display;
972
973        let block_level = matches!(elem.display, Some(Display::Block | Display::Indent));
974
975        let mut content = self.display_elem_children(
976            &elem.children,
977            if block_level { None } else { prefix.as_deref_mut() },
978            false,
979        )?;
980
981        match elem.display {
982            Some(Display::Block) => {
983                content = BlockElem::new()
984                    .with_body(Some(BlockBody::Content(content)))
985                    .pack()
986                    .spanned(self.span);
987            }
988            Some(Display::Indent) => {
989                content = CslIndentElem::new(content).pack().spanned(self.span);
990            }
991            Some(Display::LeftMargin) => {
992                // The `display="left-margin"` attribute is only supported at
993                // the top-level (when prefix is `Some(_)`). Within a
994                // block-level container, it is ignored. The CSL spec is not
995                // specific about this, but it is in line with citeproc.js's
996                // behaviour.
997                if let Some(prefix) = prefix {
998                    *prefix.get_or_insert_with(Default::default) += content;
999                    return Ok(Content::empty());
1000                }
1001            }
1002            _ => {}
1003        }
1004
1005        content = content.spanned(self.span);
1006
1007        if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta
1008            && let Some(location) = (self.link)(i)
1009        {
1010            let alt = content.plain_text();
1011            content = DirectLinkElem::new(location, content, Some(alt)).pack();
1012        }
1013
1014        Ok(content)
1015    }
1016
1017    /// Display math.
1018    fn display_math(&self, math: &str) -> Content {
1019        (self.routines.eval_string)(
1020            self.routines,
1021            self.world,
1022            // TODO: propagate warnings
1023            Sink::new().track_mut(),
1024            math,
1025            self.span,
1026            SyntaxMode::Math,
1027            Scope::new(),
1028        )
1029        .map(Value::display)
1030        .unwrap_or_else(|_| TextElem::packed(math).spanned(self.span))
1031    }
1032
1033    /// Display a link.
1034    fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> StrResult<Content> {
1035        let dest = Destination::Url(Url::new(url)?);
1036        Ok(LinkElem::new(dest.into(), self.display_formatted(text, false))
1037            .pack()
1038            .spanned(self.span))
1039    }
1040
1041    /// Display transparent pass-through content.
1042    fn display_transparent(&self, i: usize, format: &hayagriva::Formatting) -> Content {
1043        let content = (self.supplement)(i).unwrap_or_default();
1044        apply_formatting(content, format)
1045    }
1046
1047    /// Display formatted hayagriva text as content.
1048    fn display_formatted(
1049        &self,
1050        formatted: &hayagriva::Formatted,
1051        trim_start: bool,
1052    ) -> Content {
1053        let formatted_text = if trim_start {
1054            formatted.text.trim_start()
1055        } else {
1056            formatted.text.as_str()
1057        };
1058
1059        let content = TextElem::packed(formatted_text).spanned(self.span);
1060        apply_formatting(content, &formatted.formatting)
1061    }
1062}
1063
1064/// Applies formatting to content.
1065fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Content {
1066    match format.font_style {
1067        citationberg::FontStyle::Normal => {}
1068        citationberg::FontStyle::Italic => {
1069            content = content.emph();
1070        }
1071    }
1072
1073    match format.font_variant {
1074        citationberg::FontVariant::Normal => {}
1075        citationberg::FontVariant::SmallCaps => {
1076            content = SmallcapsElem::new(content).pack();
1077        }
1078    }
1079
1080    match format.font_weight {
1081        citationberg::FontWeight::Normal => {}
1082        citationberg::FontWeight::Bold => {
1083            content = content.strong();
1084        }
1085        citationberg::FontWeight::Light => {
1086            // We don't have a semantic element for "light" and a `StrongElem`
1087            // with negative delta does not have the appropriate semantics, so
1088            // keeping this as a direct style.
1089            content = CslLightElem::new(content).pack();
1090        }
1091    }
1092
1093    match format.text_decoration {
1094        citationberg::TextDecoration::None => {}
1095        citationberg::TextDecoration::Underline => {
1096            content = content.underlined();
1097        }
1098    }
1099
1100    let span = content.span();
1101    match format.vertical_align {
1102        citationberg::VerticalAlign::None => {}
1103        citationberg::VerticalAlign::Baseline => {}
1104        citationberg::VerticalAlign::Sup => {
1105            // Add zero-width weak spacing to make the superscript "sticky".
1106            content =
1107                HElem::hole().clone() + SuperElem::new(content).pack().spanned(span);
1108        }
1109        citationberg::VerticalAlign::Sub => {
1110            content = HElem::hole().clone() + SubElem::new(content).pack().spanned(span);
1111        }
1112    }
1113
1114    content
1115}
1116
1117/// Create a locale code from language and optionally region.
1118fn locale(lang: Lang, region: Option<Region>) -> citationberg::LocaleCode {
1119    let mut value = String::with_capacity(5);
1120    value.push_str(lang.as_str());
1121    if let Some(region) = region {
1122        value.push('-');
1123        value.push_str(region.as_str())
1124    }
1125    citationberg::LocaleCode(value)
1126}
1127
1128/// Translation of `font-weight="light"` in CSL.
1129///
1130/// We translate `font-weight: "bold"` to `<strong>` since it's likely that the
1131/// CSL spec just talks about bold because it has no notion of semantic
1132/// elements. The benefits of a strict reading of the spec are also rather
1133/// questionable, while using semantic elements makes the bibliography more
1134/// accessible, easier to style, and more portable across export targets.
1135#[elem]
1136pub struct CslLightElem {
1137    #[required]
1138    pub body: Content,
1139}
1140
1141/// Translation of `display="indent"` in CSL.
1142///
1143/// A `display="block"` is simply translated to a Typst `BlockElem`. Similarly,
1144/// we could translate `display="indent"` to a `PadElem`, but (a) it does not
1145/// yet have support in HTML and (b) a `PadElem` described a fixed padding while
1146/// CSL leaves the amount of padding user-defined so it's not a perfect fit.
1147#[elem]
1148pub struct CslIndentElem {
1149    #[required]
1150    pub body: Content,
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155    use super::*;
1156
1157    #[test]
1158    fn test_bibliography_load_builtin_styles() {
1159        for &archived in ArchivedStyle::all() {
1160            let _ = CslStyle::from_archived(archived);
1161        }
1162    }
1163
1164    #[test]
1165    fn test_csl_source_cast_info_include_all_names() {
1166        let CastInfo::Union(cast_info) = CslSource::input() else {
1167            panic!("the cast info of CslSource should be a union");
1168        };
1169
1170        let missing: Vec<_> = ArchivedStyle::all()
1171            .iter()
1172            .flat_map(|style| style.names())
1173            .filter(|name| {
1174                let found = cast_info.iter().any(|info| match info {
1175                    CastInfo::Value(Value::Str(n), _) => n.as_str() == **name,
1176                    _ => false,
1177                });
1178                !found
1179            })
1180            .collect();
1181
1182        assert!(
1183            missing.is_empty(),
1184            "missing style names in CslSource cast info: '{missing:?}'"
1185        );
1186    }
1187}