diff options
Diffstat (limited to 'crates/typst-library/src/model/bibliography.rs')
| -rw-r--r-- | crates/typst-library/src/model/bibliography.rs | 1101 |
1 files changed, 1101 insertions, 0 deletions
diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs new file mode 100644 index 00000000..d11055b9 --- /dev/null +++ b/crates/typst-library/src/model/bibliography.rs @@ -0,0 +1,1101 @@ +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::num::NonZeroUsize; +use std::path::Path; +use std::sync::Arc; + +use comemo::Tracked; +use ecow::{eco_format, EcoString, EcoVec}; +use hayagriva::archive::ArchivedStyle; +use hayagriva::io::BibLaTeXError; +use hayagriva::{ + citationberg, BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, + SpecificLocator, +}; +use indexmap::IndexMap; +use once_cell::sync::Lazy; +use smallvec::{smallvec, SmallVec}; +use typed_arena::Arena; +use typst_syntax::{Span, Spanned}; +use typst_utils::{LazyHash, NonZeroExt, PicoStr}; + +use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, ty, Args, Array, Bytes, CastInfo, Content, FromValue, IntoValue, Label, + NativeElement, Packed, Reflect, Repr, Scope, Show, ShowSet, Smart, Str, StyleChain, + Styles, Synthesize, Type, Value, +}; +use crate::introspection::{Introspector, Locatable, Location}; +use crate::layout::{ + BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, + Sizing, TrackSizings, VElem, +}; +use crate::model::{ + CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, + Url, +}; +use crate::routines::{EvalMode, Routines}; +use crate::text::{ + FontStyle, Lang, LocalName, Region, SubElem, SuperElem, TextElem, WeightDelta, +}; +use crate::World; + +/// A bibliography / reference listing. +/// +/// You can create a new bibliography by calling this function with a path +/// to a bibliography file in either one of two formats: +/// +/// - A Hayagriva `.yml` file. Hayagriva is a new bibliography file format +/// designed for use with Typst. Visit its +/// [documentation](https://github.com/typst/hayagriva/blob/main/docs/file-format.md) +/// for more details. +/// - A BibLaTeX `.bib` file. +/// +/// As soon as you add a bibliography somewhere in your document, you can start +/// citing things with reference syntax (`[@key]`) or explicit calls to the +/// [citation]($cite) function (`[#cite(<key>)]`). The bibliography will only +/// show entries for works that were referenced in the document. +/// +/// # Styles +/// Typst offers a wide selection of built-in +/// [citation and bibliography styles]($bibliography.style). Beyond those, you +/// can add and use custom [CSL](https://citationstyles.org/) (Citation Style +/// Language) files. Wondering which style to use? Here are some good defaults +/// based on what discipline you're working in: +/// +/// | Fields | Typical Styles | +/// |-----------------|--------------------------------------------------------| +/// | Engineering, IT | `{"ieee"}` | +/// | Psychology, Life Sciences | `{"apa"}` | +/// | Social sciences | `{"chicago-author-date"}` | +/// | Humanities | `{"mla"}`, `{"chicago-notes"}`, `{"harvard-cite-them-right"}` | +/// | Economics | `{"harvard-cite-them-right"}` | +/// | Physics | `{"american-physics-society"}` | +/// +/// # Example +/// ```example +/// This was already noted by +/// pirates long ago. @arrgh +/// +/// Multiple sources say ... +/// @arrgh @netwok. +/// +/// #bibliography("works.bib") +/// ``` +#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)] +pub struct BibliographyElem { + /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files. + #[required] + #[parse( + let (paths, bibliography) = Bibliography::parse(engine, args)?; + paths + )] + pub path: BibliographyPaths, + + /// The title of the bibliography. + /// + /// - When set to `{auto}`, an appropriate title for the + /// [text language]($text.lang) will be used. This is the default. + /// - When set to `{none}`, the bibliography will not have a title. + /// - A custom title can be set by passing content. + /// + /// The bibliography's heading will not be numbered by default, but you can + /// force it to be with a show-set rule: + /// `{show bibliography: set heading(numbering: "1.")}` + pub title: Smart<Option<Content>>, + + /// Whether to include all works from the given bibliography files, even + /// those that weren't cited in the document. + /// + /// To selectively add individual cited works without showing them, you can + /// also use the `cite` function with [`form`]($cite.form) set to `{none}`. + #[default(false)] + pub full: bool, + + /// The bibliography style. + /// + /// Should be either one of the built-in styles (see below) or a path to + /// a [CSL file](https://citationstyles.org/). Some of the styles listed + /// below appear twice, once with their full name and once with a short + /// alias. + #[parse(CslStyle::parse(engine, args)?)] + #[default(CslStyle::from_name("ieee").unwrap())] + pub style: CslStyle, + + /// The loaded bibliography. + #[internal] + #[required] + #[parse(bibliography)] + pub bibliography: Bibliography, + + /// The language setting where the bibliography is. + #[internal] + #[synthesized] + pub lang: Lang, + + /// The region setting where the bibliography is. + #[internal] + #[synthesized] + pub region: Option<Region>, +} + +/// A list of bibliography file paths. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct BibliographyPaths(Vec<EcoString>); + +cast! { + BibliographyPaths, + self => self.0.into_value(), + v: EcoString => Self(vec![v]), + v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?), +} + +impl BibliographyElem { + /// Find the document's bibliography. + pub fn find(introspector: Tracked<Introspector>) -> StrResult<Packed<Self>> { + let query = introspector.query(&Self::elem().select()); + let mut iter = query.iter(); + let Some(elem) = iter.next() else { + bail!("the document does not contain a bibliography"); + }; + + if iter.next().is_some() { + bail!("multiple bibliographies are not yet supported"); + } + + Ok(elem.to_packed::<Self>().unwrap().clone()) + } + + /// Whether the bibliography contains the given key. + pub fn has(engine: &Engine, key: impl Into<PicoStr>) -> bool { + let key = key.into(); + engine + .introspector + .query(&Self::elem().select()) + .iter() + .any(|elem| elem.to_packed::<Self>().unwrap().bibliography().has(key)) + } + + /// Find all bibliography keys. + pub fn keys( + introspector: Tracked<Introspector>, + ) -> Vec<(EcoString, Option<EcoString>)> { + let mut vec = vec![]; + for elem in introspector.query(&Self::elem().select()).iter() { + let this = elem.to_packed::<Self>().unwrap(); + for entry in this.bibliography().entries() { + let key = entry.key().into(); + let detail = entry.title().map(|title| title.value.to_str().into()); + vec.push((key, detail)) + } + } + vec + } +} + +impl Synthesize for Packed<BibliographyElem> { + fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { + let elem = self.as_mut(); + elem.push_lang(TextElem::lang_in(styles)); + elem.push_region(TextElem::region_in(styles)); + Ok(()) + } +} + +impl Show for Packed<BibliographyElem> { + #[typst_macros::time(name = "bibliography", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + const COLUMN_GUTTER: Em = Em::new(0.65); + const INDENT: Em = Em::new(1.5); + + let mut seq = vec![]; + if let Some(title) = self.title(styles).unwrap_or_else(|| { + Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) + }) { + seq.push( + HeadingElem::new(title) + .with_depth(NonZeroUsize::ONE) + .pack() + .spanned(self.span()), + ); + } + + let span = self.span(); + let works = Works::generate(engine).at(span)?; + let references = works + .references + .as_ref() + .ok_or("CSL style is not suitable for bibliographies") + .at(span)?; + + let row_gutter = ParElem::spacing_in(styles); + let row_gutter_elem = VElem::new(row_gutter.into()).with_weak(true).pack(); + + if references.iter().any(|(prefix, _)| prefix.is_some()) { + let mut cells = vec![]; + for (prefix, reference) in references { + cells.push(GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(prefix.clone().unwrap_or_default())) + .spanned(span), + ))); + cells.push(GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(reference.clone())).spanned(span), + ))); + } + seq.push( + GridElem::new(cells) + .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) + .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) + .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) + .pack() + .spanned(self.span()), + ); + } else { + for (i, (_, reference)) in references.iter().enumerate() { + if i > 0 { + seq.push(row_gutter_elem.clone()); + } + seq.push(reference.clone()); + } + } + + let mut content = Content::sequence(seq); + if works.hanging_indent { + content = content.styled(ParElem::set_hanging_indent(INDENT.into())); + } + + Ok(content) + } +} + +impl ShowSet for Packed<BibliographyElem> { + fn show_set(&self, _: StyleChain) -> Styles { + const INDENT: Em = Em::new(1.0); + let mut out = Styles::new(); + out.set(HeadingElem::set_numbering(None)); + out.set(PadElem::set_left(INDENT.into())); + out + } +} + +impl LocalName for Packed<BibliographyElem> { + const KEY: &'static str = "bibliography"; +} + +/// A loaded bibliography. +#[derive(Clone, PartialEq)] +pub struct Bibliography { + map: Arc<IndexMap<PicoStr, hayagriva::Entry>>, + hash: u128, +} + +impl Bibliography { + /// Parse the bibliography argument. + fn parse( + engine: &mut Engine, + args: &mut Args, + ) -> SourceResult<(BibliographyPaths, Bibliography)> { + let Spanned { v: paths, span } = + args.expect::<Spanned<BibliographyPaths>>("path to bibliography file")?; + + // Load bibliography files. + let data = paths + .0 + .iter() + .map(|path| { + let id = span.resolve_path(path).at(span)?; + engine.world.file(id).at(span) + }) + .collect::<SourceResult<Vec<Bytes>>>()?; + + // Parse. + let bibliography = Self::load(&paths, &data).at(span)?; + + Ok((paths, bibliography)) + } + + /// Load bibliography entries from paths. + #[comemo::memoize] + #[typst_macros::time(name = "load bibliography")] + fn load(paths: &BibliographyPaths, data: &[Bytes]) -> StrResult<Bibliography> { + let mut map = IndexMap::new(); + let mut duplicates = Vec::<EcoString>::new(); + + // We might have multiple bib/yaml files + for (path, bytes) in paths.0.iter().zip(data) { + let src = std::str::from_utf8(bytes).map_err(FileError::from)?; + + let ext = Path::new(path.as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + let library = match ext.to_lowercase().as_str() { + "yml" | "yaml" => hayagriva::io::from_yaml_str(src) + .map_err(|err| eco_format!("failed to parse YAML ({err})"))?, + "bib" => hayagriva::io::from_biblatex_str(src) + .map_err(|errors| format_biblatex_error(path, src, errors))?, + _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), + }; + + for entry in library { + match map.entry(entry.key().into()) { + indexmap::map::Entry::Vacant(vacant) => { + vacant.insert(entry); + } + indexmap::map::Entry::Occupied(_) => { + duplicates.push(entry.key().into()); + } + } + } + } + + if !duplicates.is_empty() { + bail!("duplicate bibliography keys: {}", duplicates.join(", ")); + } + + Ok(Bibliography { + map: Arc::new(map), + hash: typst_utils::hash128(data), + }) + } + + fn has(&self, key: impl Into<PicoStr>) -> bool { + self.map.contains_key(&key.into()) + } + + fn entries(&self) -> impl Iterator<Item = &hayagriva::Entry> { + self.map.values() + } +} + +impl Debug for Bibliography { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_set().entries(self.map.keys()).finish() + } +} + +impl Hash for Bibliography { + fn hash<H: Hasher>(&self, state: &mut H) { + self.hash.hash(state); + } +} + +/// Format a BibLaTeX loading error. +fn format_biblatex_error(path: &str, src: &str, errors: Vec<BibLaTeXError>) -> EcoString { + let Some(error) = errors.first() else { + return eco_format!("failed to parse BibLaTeX file ({path})"); + }; + + let (span, msg) = match error { + BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()), + BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()), + }; + let line = src.get(..span.start).unwrap_or_default().lines().count(); + eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})") +} + +/// A loaded CSL style. +#[ty(cast)] +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct CslStyle { + name: Option<EcoString>, + style: Arc<LazyHash<citationberg::IndependentStyle>>, +} + +impl CslStyle { + /// Parse the style argument. + pub fn parse(engine: &mut Engine, args: &mut Args) -> SourceResult<Option<CslStyle>> { + let Some(Spanned { v: string, span }) = + args.named::<Spanned<EcoString>>("style")? + else { + return Ok(None); + }; + + Ok(Some(Self::parse_impl(engine, &string, span).at(span)?)) + } + + /// Parse the style argument with `Smart`. + pub fn parse_smart( + engine: &mut Engine, + args: &mut Args, + ) -> SourceResult<Option<Smart<CslStyle>>> { + let Some(Spanned { v: smart, span }) = + args.named::<Spanned<Smart<EcoString>>>("style")? + else { + return Ok(None); + }; + + Ok(Some(match smart { + Smart::Auto => Smart::Auto, + Smart::Custom(string) => { + Smart::Custom(Self::parse_impl(engine, &string, span).at(span)?) + } + })) + } + + /// Parse internally. + fn parse_impl(engine: &mut Engine, string: &str, span: Span) -> StrResult<CslStyle> { + let ext = Path::new(string) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default() + .to_lowercase(); + + if ext == "csl" { + let id = span.resolve_path(string)?; + let data = engine.world.file(id)?; + CslStyle::from_data(&data) + } else { + CslStyle::from_name(string) + } + } + + /// Load a built-in CSL style. + #[comemo::memoize] + pub fn from_name(name: &str) -> StrResult<CslStyle> { + match hayagriva::archive::ArchivedStyle::by_name(name).map(ArchivedStyle::get) { + Some(citationberg::Style::Independent(style)) => Ok(Self { + name: Some(name.into()), + style: Arc::new(LazyHash::new(style)), + }), + _ => bail!("unknown style: `{name}`"), + } + } + + /// Load a CSL style from file contents. + #[comemo::memoize] + pub fn from_data(data: &Bytes) -> StrResult<CslStyle> { + let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?; + citationberg::IndependentStyle::from_xml(text) + .map(|style| Self { name: None, style: Arc::new(LazyHash::new(style)) }) + .map_err(|err| eco_format!("failed to load CSL style ({err})")) + } + + /// Get the underlying independent style. + pub fn get(&self) -> &citationberg::IndependentStyle { + self.style.as_ref() + } +} + +// This Reflect impl is technically a bit wrong because it doesn't say what +// FromValue and IntoValue really do. Instead, it says what the `style` argument +// on `bibliography` and `cite` expect (through manual parsing). +impl Reflect for CslStyle { + #[comemo::memoize] + fn input() -> CastInfo { + let ty = std::iter::once(CastInfo::Type(Type::of::<Str>())); + let options = hayagriva::archive::ArchivedStyle::all().iter().map(|name| { + CastInfo::Value(name.names()[0].into_value(), name.display_name()) + }); + CastInfo::Union(ty.chain(options).collect()) + } + + fn output() -> CastInfo { + EcoString::output() + } + + fn castable(value: &Value) -> bool { + if let Value::Dyn(dynamic) = &value { + if dynamic.is::<Self>() { + return true; + } + } + + false + } +} + +impl FromValue for CslStyle { + fn from_value(value: Value) -> HintedStrResult<Self> { + if let Value::Dyn(dynamic) = &value { + if let Some(concrete) = dynamic.downcast::<Self>() { + return Ok(concrete.clone()); + } + } + + Err(<Self as Reflect>::error(&value)) + } +} + +impl IntoValue for CslStyle { + fn into_value(self) -> Value { + Value::dynamic(self) + } +} + +impl Repr for CslStyle { + fn repr(&self) -> EcoString { + self.name + .as_ref() + .map(|name| name.repr()) + .unwrap_or_else(|| "..".into()) + } +} + +/// Fully formatted citations and references, generated once (through +/// memoization) for the whole document. This setup is necessary because +/// citation formatting is inherently stateful and we need access to all +/// citations to do it. +pub(super) struct Works { + /// Maps from the location of a citation group to its rendered content. + pub citations: HashMap<Location, SourceResult<Content>>, + /// Lists all references in the bibliography, with optional prefix, or + /// `None` if the citation style can't be used for bibliographies. + pub references: Option<Vec<(Option<Content>, Content)>>, + /// Whether the bibliography should have hanging indent. + pub hanging_indent: bool, +} + +impl Works { + /// Generate all citations and the whole bibliography. + pub fn generate(engine: &Engine) -> StrResult<Arc<Works>> { + Self::generate_impl(engine.routines, engine.world, engine.introspector) + } + + /// The internal implementation of [`Works::generate`]. + #[comemo::memoize] + fn generate_impl( + routines: &Routines, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + ) -> StrResult<Arc<Works>> { + let mut generator = Generator::new(routines, world, introspector)?; + let rendered = generator.drive(); + let works = generator.display(&rendered)?; + Ok(Arc::new(works)) + } +} + +/// Context for generating the bibliography. +struct Generator<'a> { + /// The routines that is used to evaluate mathematical material in citations. + routines: &'a Routines, + /// The world that is used to evaluate mathematical material in citations. + world: Tracked<'a, dyn World + 'a>, + /// The document's bibliography. + bibliography: Packed<BibliographyElem>, + /// The document's citation groups. + groups: EcoVec<Content>, + /// Details about each group that are accumulated while driving hayagriva's + /// bibliography driver and needed when processing hayagriva's output. + infos: Vec<GroupInfo>, + /// Citations with unresolved keys. + failures: HashMap<Location, SourceResult<Content>>, +} + +/// Details about a group of merged citations. All citations are put into groups +/// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two). +/// Even single citations will be put into groups of length ones. +struct GroupInfo { + /// The group's location. + location: Location, + /// The group's span. + span: Span, + /// Whether the group should be displayed in a footnote. + footnote: bool, + /// Details about the groups citations. + subinfos: SmallVec<[CiteInfo; 1]>, +} + +/// Details about a citation item in a request. +struct CiteInfo { + /// The citation's key. + key: Label, + /// The citation's supplement. + supplement: Option<Content>, + /// Whether this citation was hidden. + hidden: bool, +} + +impl<'a> Generator<'a> { + /// Create a new generator. + fn new( + routines: &'a Routines, + world: Tracked<'a, dyn World + 'a>, + introspector: Tracked<Introspector>, + ) -> StrResult<Self> { + let bibliography = BibliographyElem::find(introspector)?; + let groups = introspector.query(&CiteGroup::elem().select()); + let infos = Vec::with_capacity(groups.len()); + Ok(Self { + routines, + world, + bibliography, + groups, + infos, + failures: HashMap::new(), + }) + } + + /// Drives hayagriva's citation driver. + fn drive(&mut self) -> hayagriva::Rendered { + static LOCALES: Lazy<Vec<citationberg::Locale>> = + Lazy::new(hayagriva::archive::locales); + + let database = self.bibliography.bibliography(); + let bibliography_style = self.bibliography.style(StyleChain::default()); + let styles = Arena::new(); + + // Process all citation groups. + let mut driver = BibliographyDriver::new(); + for elem in &self.groups { + let group = elem.to_packed::<CiteGroup>().unwrap(); + let location = elem.location().unwrap(); + let children = group.children(); + + // Groups should never be empty. + let Some(first) = children.first() else { continue }; + + let mut subinfos = SmallVec::with_capacity(children.len()); + let mut items = Vec::with_capacity(children.len()); + let mut errors = EcoVec::new(); + let mut normal = true; + + // Create infos and items for each child in the group. + for child in children { + let key = *child.key(); + let Some(entry) = database.map.get(&key.into_inner()) else { + errors.push(error!( + child.span(), + "key `{}` does not exist in the bibliography", + key.as_str() + )); + continue; + }; + + let supplement = child.supplement(StyleChain::default()); + let locator = supplement.as_ref().map(|_| { + SpecificLocator( + citationberg::taxonomy::Locator::Custom, + hayagriva::LocatorPayload::Transparent, + ) + }); + + let mut hidden = false; + let special_form = match child.form(StyleChain::default()) { + None => { + hidden = true; + None + } + Some(CitationForm::Normal) => None, + Some(CitationForm::Prose) => Some(hayagriva::CitePurpose::Prose), + Some(CitationForm::Full) => Some(hayagriva::CitePurpose::Full), + Some(CitationForm::Author) => Some(hayagriva::CitePurpose::Author), + Some(CitationForm::Year) => Some(hayagriva::CitePurpose::Year), + }; + + normal &= special_form.is_none(); + subinfos.push(CiteInfo { key, supplement, hidden }); + items.push(CitationItem::new(entry, locator, None, hidden, special_form)); + } + + if !errors.is_empty() { + self.failures.insert(location, Err(errors)); + continue; + } + + let style = match first.style(StyleChain::default()) { + Smart::Auto => &bibliography_style.style, + Smart::Custom(style) => styles.alloc(style.style), + }; + + self.infos.push(GroupInfo { + location, + subinfos, + span: first.span(), + footnote: normal + && style.settings.class == citationberg::StyleClass::Note, + }); + + driver.citation(CitationRequest::new( + items, + style, + Some(locale( + first.lang().copied().unwrap_or(Lang::ENGLISH), + first.region().copied().flatten(), + )), + &LOCALES, + None, + )); + } + + let locale = locale( + self.bibliography.lang().copied().unwrap_or(Lang::ENGLISH), + self.bibliography.region().copied().flatten(), + ); + + // Add hidden items for everything if we should print the whole + // bibliography. + if self.bibliography.full(StyleChain::default()) { + for entry in database.map.values() { + driver.citation(CitationRequest::new( + vec![CitationItem::new(entry, None, None, true, None)], + bibliography_style.get(), + Some(locale.clone()), + &LOCALES, + None, + )); + } + } + + driver.finish(BibliographyRequest { + style: bibliography_style.get(), + locale: Some(locale), + locale_files: &LOCALES, + }) + } + + /// Displays hayagriva's output as content for the citations and references. + fn display(&mut self, rendered: &hayagriva::Rendered) -> StrResult<Works> { + let citations = self.display_citations(rendered)?; + let references = self.display_references(rendered)?; + let hanging_indent = + rendered.bibliography.as_ref().is_some_and(|b| b.hanging_indent); + Ok(Works { citations, references, hanging_indent }) + } + + /// Display the citation groups. + fn display_citations( + &mut self, + rendered: &hayagriva::Rendered, + ) -> StrResult<HashMap<Location, SourceResult<Content>>> { + // Determine for each citation key where in the bibliography it is, + // so that we can link there. + let mut links = HashMap::new(); + if let Some(bibliography) = &rendered.bibliography { + let location = self.bibliography.location().unwrap(); + for (k, item) in bibliography.items.iter().enumerate() { + links.insert(item.key.as_str(), location.variant(k + 1)); + } + } + + let mut output = std::mem::take(&mut self.failures); + for (info, citation) in self.infos.iter().zip(&rendered.citations) { + let supplement = |i: usize| info.subinfos.get(i)?.supplement.clone(); + let link = |i: usize| links.get(info.subinfos.get(i)?.key.as_str()).copied(); + + let renderer = ElemRenderer { + routines: self.routines, + world: self.world, + span: info.span, + supplement: &supplement, + link: &link, + }; + + let content = if info.subinfos.iter().all(|sub| sub.hidden) { + Content::empty() + } else { + let mut content = renderer.display_elem_children( + &citation.citation, + &mut None, + true, + )?; + + if info.footnote { + content = FootnoteElem::with_content(content).pack(); + } + + content + }; + + output.insert(info.location, Ok(content)); + } + + Ok(output) + } + + /// Display the bibliography references. + #[allow(clippy::type_complexity)] + fn display_references( + &self, + rendered: &hayagriva::Rendered, + ) -> StrResult<Option<Vec<(Option<Content>, Content)>>> { + let Some(rendered) = &rendered.bibliography else { return Ok(None) }; + + // Determine for each citation key where it first occurred, so that we + // can link there. + let mut first_occurrences = HashMap::new(); + for info in &self.infos { + for subinfo in &info.subinfos { + let key = subinfo.key.as_str(); + first_occurrences.entry(key).or_insert(info.location); + } + } + + // The location of the bibliography. + let location = self.bibliography.location().unwrap(); + + let mut output = vec![]; + for (k, item) in rendered.items.iter().enumerate() { + let renderer = ElemRenderer { + routines: self.routines, + world: self.world, + span: self.bibliography.span(), + supplement: &|_| None, + link: &|_| None, + }; + + // Each reference is assigned a manually created well-known location + // that is derived from the bibliography's location. This way, + // citations can link to them. + let backlink = location.variant(k + 1); + + // Render the first field. + let mut prefix = item + .first_field + .as_ref() + .map(|elem| { + let mut content = + renderer.display_elem_child(elem, &mut None, false)?; + if let Some(location) = first_occurrences.get(item.key.as_str()) { + let dest = Destination::Location(*location); + content = content.linked(dest); + } + StrResult::Ok(content) + }) + .transpose()?; + + // Render the main reference content. + let mut reference = + renderer.display_elem_children(&item.content, &mut prefix, false)?; + + // Attach a backlink to either the prefix or the reference so that + // we can link to the bibliography entry. + prefix.as_mut().unwrap_or(&mut reference).set_location(backlink); + + output.push((prefix, reference)); + } + + Ok(Some(output)) + } +} + +/// Renders hayagriva elements into content. +struct ElemRenderer<'a> { + /// The routines that is used to evaluate mathematical material in citations. + routines: &'a Routines, + /// The world that is used to evaluate mathematical material. + world: Tracked<'a, dyn World + 'a>, + /// The span that is attached to all of the resulting content. + span: Span, + /// Resolves the supplement of i-th citation in the request. + supplement: &'a dyn Fn(usize) -> Option<Content>, + /// Resolves where the i-th citation in the request should link to. + link: &'a dyn Fn(usize) -> Option<Location>, +} + +impl ElemRenderer<'_> { + /// Display rendered hayagriva elements. + /// + /// The `prefix` can be a separate content storage where `left-margin` + /// elements will be accumulated into. + /// + /// `is_citation` dictates whether whitespace at the start of the citation + /// will be eliminated. Some CSL styles yield whitespace at the start of + /// their citations, which should instead be handled by Typst. + fn display_elem_children( + &self, + elems: &hayagriva::ElemChildren, + prefix: &mut Option<Content>, + is_citation: bool, + ) -> StrResult<Content> { + Ok(Content::sequence( + elems + .0 + .iter() + .enumerate() + .map(|(i, elem)| { + self.display_elem_child(elem, prefix, is_citation && i == 0) + }) + .collect::<StrResult<Vec<_>>>()?, + )) + } + + /// Display a rendered hayagriva element. + fn display_elem_child( + &self, + elem: &hayagriva::ElemChild, + prefix: &mut Option<Content>, + trim_start: bool, + ) -> StrResult<Content> { + Ok(match elem { + hayagriva::ElemChild::Text(formatted) => { + self.display_formatted(formatted, trim_start) + } + hayagriva::ElemChild::Elem(elem) => self.display_elem(elem, prefix)?, + hayagriva::ElemChild::Markup(markup) => self.display_math(markup), + hayagriva::ElemChild::Link { text, url } => self.display_link(text, url)?, + hayagriva::ElemChild::Transparent { cite_idx, format } => { + self.display_transparent(*cite_idx, format) + } + }) + } + + /// Display a block-level element. + fn display_elem( + &self, + elem: &hayagriva::Elem, + prefix: &mut Option<Content>, + ) -> StrResult<Content> { + use citationberg::Display; + + let block_level = matches!(elem.display, Some(Display::Block | Display::Indent)); + + let mut suf_prefix = None; + let mut content = self.display_elem_children( + &elem.children, + if block_level { &mut suf_prefix } else { prefix }, + false, + )?; + + if let Some(prefix) = suf_prefix { + const COLUMN_GUTTER: Em = Em::new(0.65); + content = GridElem::new(vec![ + GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(prefix)).spanned(self.span), + )), + GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(content)).spanned(self.span), + )), + ]) + .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) + .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) + .pack() + .spanned(self.span); + } + + match elem.display { + Some(Display::Block) => { + content = BlockElem::new() + .with_body(Some(BlockBody::Content(content))) + .pack() + .spanned(self.span); + } + Some(Display::Indent) => { + content = PadElem::new(content).pack().spanned(self.span); + } + Some(Display::LeftMargin) => { + *prefix.get_or_insert_with(Default::default) += content; + return Ok(Content::empty()); + } + _ => {} + } + + if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta { + if let Some(location) = (self.link)(i) { + let dest = Destination::Location(location); + content = content.linked(dest); + } + } + + Ok(content) + } + + /// Display math. + fn display_math(&self, math: &str) -> Content { + (self.routines.eval_string)( + self.routines, + self.world, + math, + self.span, + EvalMode::Math, + Scope::new(), + ) + .map(Value::display) + .unwrap_or_else(|_| TextElem::packed(math).spanned(self.span)) + } + + /// Display a link. + fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> StrResult<Content> { + let dest = Destination::Url(Url::new(url)?); + Ok(LinkElem::new(dest.into(), self.display_formatted(text, false)) + .pack() + .spanned(self.span)) + } + + /// Display transparent pass-through content. + fn display_transparent(&self, i: usize, format: &hayagriva::Formatting) -> Content { + let content = (self.supplement)(i).unwrap_or_default(); + apply_formatting(content, format) + } + + /// Display formatted hayagriva text as content. + fn display_formatted( + &self, + formatted: &hayagriva::Formatted, + trim_start: bool, + ) -> Content { + let formatted_text = if trim_start { + formatted.text.trim_start() + } else { + formatted.text.as_str() + }; + + let content = TextElem::packed(formatted_text).spanned(self.span); + apply_formatting(content, &formatted.formatting) + } +} + +/// Applies formatting to content. +fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Content { + match format.font_style { + citationberg::FontStyle::Normal => {} + citationberg::FontStyle::Italic => { + content = content.styled(TextElem::set_style(FontStyle::Italic)); + } + } + + match format.font_variant { + citationberg::FontVariant::Normal => {} + citationberg::FontVariant::SmallCaps => { + content = content.styled(TextElem::set_smallcaps(true)); + } + } + + match format.font_weight { + citationberg::FontWeight::Normal => {} + citationberg::FontWeight::Bold => { + content = content.styled(TextElem::set_delta(WeightDelta(300))); + } + citationberg::FontWeight::Light => { + content = content.styled(TextElem::set_delta(WeightDelta(-100))); + } + } + + match format.text_decoration { + citationberg::TextDecoration::None => {} + citationberg::TextDecoration::Underline => { + content = content.underlined(); + } + } + + let span = content.span(); + match format.vertical_align { + citationberg::VerticalAlign::None => {} + citationberg::VerticalAlign::Baseline => {} + citationberg::VerticalAlign::Sup => { + // Add zero-width weak spacing to make the superscript "sticky". + content = HElem::hole().pack() + SuperElem::new(content).pack().spanned(span); + } + citationberg::VerticalAlign::Sub => { + content = HElem::hole().pack() + SubElem::new(content).pack().spanned(span); + } + } + + content +} + +/// Create a locale code from language and optionally region. +fn locale(lang: Lang, region: Option<Region>) -> citationberg::LocaleCode { + let mut value = String::with_capacity(5); + value.push_str(lang.as_str()); + if let Some(region) = region { + value.push('-'); + value.push_str(region.as_str()) + } + citationberg::LocaleCode(value) +} |
