typst_library/model/
bibliography.rs

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