use std::collections::HashMap; use std::ffi::OsStr; use std::hash::{Hash, Hasher}; use std::path::Path; use std::sync::Arc; use comemo::Prehashed; use ecow::EcoVec; use hayagriva::citationberg; use hayagriva::io::BibLaTeXError; use hayagriva::{ BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, SpecificLocator, }; use indexmap::IndexMap; use once_cell::sync::Lazy; use smallvec::SmallVec; use typed_arena::Arena; use typst::diag::FileError; use typst::eval::{eval_string, Bytes, CastInfo, EvalMode, Reflect}; use typst::font::FontStyle; use typst::util::option_eq; use super::{CitationForm, CiteGroup, LocalName}; use crate::layout::{ BlockElem, GridElem, HElem, PadElem, ParElem, Sizing, TrackSizings, VElem, }; use crate::meta::{FootnoteElem, HeadingElem}; use crate::prelude::*; use crate::text::{Delta, SubElem, SuperElem, TextElem}; /// 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()]`). 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, Finalize, LocalName)] pub struct BibliographyElem { /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files. #[required] #[parse( let (paths, bibliography) = Bibliography::parse(vm, args)?; paths )] pub path: BibPaths, /// 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.")}` #[default(Some(Smart::Auto))] pub title: Option>, /// 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(vm, 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, } /// A list of bibliography file paths. #[derive(Debug, Default, Clone, Hash)] pub struct BibPaths(Vec); cast! { BibPaths, self => self.0.into_value(), v: EcoString => Self(vec![v]), v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), } impl BibliographyElem { /// Find the document's bibliography. pub fn find(introspector: Tracked) -> StrResult { 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::().unwrap().clone()) } /// Whether the bibliography contains the given key. pub fn has(vt: &Vt, key: &str) -> bool { vt.introspector .query(&Self::elem().select()) .iter() .any(|elem| elem.to::().unwrap().bibliography().has(key)) } /// Find all bibliography keys. pub fn keys( introspector: Tracked, ) -> Vec<(EcoString, Option)> { let mut vec = vec![]; for elem in introspector.query(&Self::elem().select()).iter() { let this = elem.to::().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 BibliographyElem { fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { self.push_full(self.full(styles)); self.push_style(self.style(styles)); self.push_lang(TextElem::lang_in(styles)); self.push_region(TextElem::region_in(styles)); Ok(()) } } impl Show for BibliographyElem { #[tracing::instrument(name = "BibliographyElem::show", skip_all)] fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { 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) { let title = title.unwrap_or_else(|| { TextElem::packed(self.local_name( TextElem::lang_in(styles), TextElem::region_in(styles), )) .spanned(self.span()) }); seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack()); } Ok(vt.delayed(|vt| { let span = self.span(); let works = Works::generate(vt.world, vt.introspector).at(span)?; let references = works .references .as_ref() .ok_or("CSL style is not suitable for bibliographies") .at(span)?; let row_gutter = BlockElem::below_in(styles).amount(); if references.iter().any(|(prefix, _)| prefix.is_some()) { let mut cells = vec![]; for (prefix, reference) in references { cells.push(prefix.clone().unwrap_or_default()); cells.push(reference.clone()); } seq.push(VElem::new(row_gutter).with_weakness(3).pack()); seq.push( GridElem::new(cells) .with_columns(TrackSizings(vec![Sizing::Auto; 2])) .with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()])) .with_row_gutter(TrackSizings(vec![row_gutter.into()])) .pack(), ); } else { for (_, reference) in references { seq.push(VElem::new(row_gutter).with_weakness(3).pack()); 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 Finalize for BibliographyElem { fn finalize(&self, realized: Content, _: StyleChain) -> Content { const INDENT: Em = Em::new(1.0); realized .styled(HeadingElem::set_numbering(None)) .styled(PadElem::set_left(INDENT.into())) } } impl LocalName for BibliographyElem { fn local_name(&self, lang: Lang, region: Option) -> &'static str { match lang { Lang::ALBANIAN => "Bibliografi", Lang::ARABIC => "المراجع", Lang::BOKMÅL => "Bibliografi", Lang::CHINESE if option_eq(region, "TW") => "書目", Lang::CHINESE => "参考文献", Lang::CZECH => "Bibliografie", Lang::DANISH => "Bibliografi", Lang::DUTCH => "Bibliografie", Lang::FILIPINO => "Bibliograpiya", Lang::FINNISH => "Viitteet", Lang::FRENCH => "Bibliographie", Lang::GERMAN => "Bibliographie", Lang::HUNGARIAN => "Irodalomjegyzék", Lang::ITALIAN => "Bibliografia", Lang::NYNORSK => "Bibliografi", Lang::POLISH => "Bibliografia", Lang::PORTUGUESE => "Bibliografia", Lang::ROMANIAN => "Bibliografie", Lang::RUSSIAN => "Библиография", Lang::SLOVENIAN => "Literatura", Lang::SPANISH => "Bibliografía", Lang::SWEDISH => "Bibliografi", Lang::TURKISH => "Kaynakça", Lang::UKRAINIAN => "Бібліографія", Lang::VIETNAMESE => "Tài liệu tham khảo", Lang::JAPANESE => "参考文献", Lang::ENGLISH | _ => "Bibliography", } } } /// A loaded bibliography. #[ty] #[derive(Debug, Clone, PartialEq)] pub struct Bibliography { map: Arc>, hash: u128, } impl Bibliography { /// Parse the bibliography argument. fn parse(vm: &mut Vm, args: &mut Args) -> SourceResult<(BibPaths, Bibliography)> { let Spanned { v: paths, span } = args.expect::>("path to bibliography file")?; // Load bibliography files. let data = paths .0 .iter() .map(|path| { let id = vm.resolve_path(path).at(span)?; vm.world().file(id).at(span) }) .collect::>>()?; // Parse. let bibliography = Self::load(&paths, &data).at(span)?; Ok((paths, bibliography)) } /// Load bibliography entries from paths. #[comemo::memoize] fn load(paths: &BibPaths, data: &[Bytes]) -> StrResult { let mut map = IndexMap::new(); let mut duplicates = Vec::::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::util::hash128(data), }) } fn has(&self, key: &str) -> bool { self.map.contains_key(key) } fn entries(&self) -> impl Iterator { self.map.values() } } impl Hash for Bibliography { fn hash(&self, state: &mut H) { self.hash.hash(state); } } impl Repr for Bibliography { fn repr(&self) -> EcoString { "..".into() } } cast! { type Bibliography, } /// Format a BibLaTeX loading error. fn format_biblatex_error(path: &str, src: &str, errors: Vec) -> 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] #[derive(Debug, Clone, PartialEq, Hash)] pub struct CslStyle { name: Option, style: Arc>, } impl CslStyle { /// Parse the style argument. pub fn parse(vm: &mut Vm, args: &mut Args) -> SourceResult> { let Some(Spanned { v: string, span }) = args.named::>("style")? else { return Ok(None); }; Ok(Some(Self::parse_impl(vm, &string).at(span)?)) } /// Parse the style argument with `Smart`. pub fn parse_smart( vm: &mut Vm, args: &mut Args, ) -> SourceResult>> { let Some(Spanned { v: smart, span }) = args.named::>>("style")? else { return Ok(None); }; Ok(Some(match smart { Smart::Auto => Smart::Auto, Smart::Custom(string) => { Smart::Custom(Self::parse_impl(vm, &string).at(span)?) } })) } /// Parse internally. fn parse_impl(vm: &mut Vm, string: &str) -> StrResult { let ext = Path::new(string) .extension() .and_then(OsStr::to_str) .unwrap_or_default() .to_lowercase(); if ext == "csl" { let id = vm.resolve_path(string)?; let data = vm.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 { match hayagriva::archive::style_by_name(name) { Some(citationberg::Style::Independent(style)) => Ok(Self { name: Some(name.into()), style: Arc::new(Prehashed::new(style)), }), _ => bail!("unknown style: `{name}`"), } } /// Load a CSL style from file contents. #[comemo::memoize] pub fn from_data(data: &Bytes) -> StrResult { 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(Prehashed::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::())); let options = hayagriva::archive::styles() .map(|style| CastInfo::Value(style.name.into_value(), style.full_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::() { return true; } } false } } impl FromValue for CslStyle { fn from_value(value: Value) -> StrResult { if let Value::Dyn(dynamic) = &value { if let Some(concrete) = dynamic.downcast::() { return Ok(concrete.clone()); } } Err(::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>, /// Lists all references in the bibliography, with optional prefix, or /// `None` if the citation style can't be used for bibliographies. pub references: Option, Content)>>, /// Whether the bibliography should have hanging indent. pub hanging_indent: bool, } impl Works { /// Generate all citations and the whole bibliography. #[comemo::memoize] pub fn generate( world: Tracked, introspector: Tracked, ) -> StrResult> { let mut generator = Generator::new(world, introspector)?; let rendered = generator.drive(); let works = generator.display(&rendered)?; Ok(Arc::new(works)) } } /// Context for generating the bibliography. struct Generator<'a> { /// The world that is used to evaluate mathematical material in citations. world: Tracked<'a, dyn World + 'a>, /// The document's bibliography. bibliography: BibliographyElem, /// The document's citation groups. groups: EcoVec>, /// Details about each group that are accumulated while driving hayagriva's /// bibliography driver and needed when processing hayagriva's output. infos: Vec, /// Citations with unresolved keys. failures: HashMap>, } /// 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, /// Whether this citation was hidden. hidden: bool, } impl<'a> Generator<'a> { /// Create a new generator. fn new( world: Tracked<'a, dyn World + 'a>, introspector: Tracked, ) -> StrResult { let bibliography = BibliographyElem::find(introspector)?; let groups = introspector.query(&CiteGroup::elem().select()); let infos = Vec::with_capacity(groups.len()); Ok(Self { world, bibliography, groups, infos, failures: HashMap::new(), }) } /// Drives hayagriva's citation driver. fn drive(&mut self) -> hayagriva::Rendered { static LOCALES: Lazy> = 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::().unwrap(); let location = group.0.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.0) else { errors.push(error!( child.span(), "key `{}` does not exist in the bibliography", key.0 )); 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(), first.region())), &LOCALES, None, )); } let locale = locale(self.bibliography.lang(), self.bibliography.region()); // 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 { let citations = self.display_citations(rendered); let references = self.display_references(rendered); let hanging_indent = rendered.bibliography.as_ref().map_or(false, |b| b.hanging_indent); Ok(Works { citations, references, hanging_indent }) } /// Display the citation groups. fn display_citations( &mut self, rendered: &hayagriva::Rendered, ) -> HashMap> { // 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.0.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.0.as_str()).copied(); let renderer = ElemRenderer { 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); if info.footnote { content = FootnoteElem::with_content(content).pack(); } content }; output.insert(info.location, Ok(content)); } output } /// Display the bibliography references. fn display_references( &self, rendered: &hayagriva::Rendered, ) -> Option, Content)>> { let rendered = rendered.bibliography.as_ref()?; // Determine for each citation key where it first occured, so that we // can link there. let mut first_occurances = HashMap::new(); for info in &self.infos { for subinfo in &info.subinfos { let key = subinfo.key.0.as_str(); first_occurances.entry(key).or_insert(info.location); } } // The location of the bibliography. let location = self.bibliography.0.location().unwrap(); let mut output = vec![]; for (k, item) in rendered.items.iter().enumerate() { let renderer = ElemRenderer { 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); if let Some(location) = first_occurances.get(item.key.as_str()) { let dest = Destination::Location(*location); content = content.linked(dest); } content.backlinked(backlink) }); // Render the main reference content. let reference = renderer .display_elem_children(&item.content, &mut prefix) .backlinked(backlink); output.push((prefix, reference)); } Some(output) } } /// Renders hayagriva elements into content. struct ElemRenderer<'a> { /// 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, /// Resolves where the i-th citation in the request should link to. link: &'a dyn Fn(usize) -> Option, } impl ElemRenderer<'_> { /// Display rendered hayagriva elements. /// /// The `prefix` can be a separate content storage where `left-margin` /// elements will be accumulated into. fn display_elem_children( &self, elems: &hayagriva::ElemChildren, prefix: &mut Option, ) -> Content { Content::sequence( elems.0.iter().map(|elem| self.display_elem_child(elem, prefix)), ) } /// Display a rendered hayagriva element. fn display_elem_child( &self, elem: &hayagriva::ElemChild, prefix: &mut Option, ) -> Content { match elem { hayagriva::ElemChild::Text(formatted) => self.display_formatted(formatted), 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 { 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 }, ); if let Some(prefix) = suf_prefix { const COLUMN_GUTTER: Em = Em::new(0.65); content = GridElem::new(vec![prefix, content]) .with_columns(TrackSizings(vec![Sizing::Auto; 2])) .with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()])) .pack(); } match elem.display { Some(Display::Block) => { content = BlockElem::new().with_body(Some(content)).pack(); } Some(Display::Indent) => { content = PadElem::new(content).pack(); } Some(Display::LeftMargin) => { *prefix.get_or_insert_with(Default::default) += content; return 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); } } content } /// Display math. fn display_math(&self, math: &str) -> Content { eval_string(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) -> Content { let dest = Destination::Url(url.into()); self.display_formatted(text).linked(dest) } /// 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) -> Content { let content = TextElem::packed(formatted.text.as_str()).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(Delta(300))); } citationberg::FontWeight::Light => { content = content.styled(TextElem::set_delta(Delta(-100))); } } match format.text_decoration { citationberg::TextDecoration::None => {} citationberg::TextDecoration::Underline => { content = content.underlined(); } } 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(); } citationberg::VerticalAlign::Sub => { content = HElem::hole().pack() + SubElem::new(content).pack(); } } content } /// Create a locale code from language and optionally region. fn locale(lang: Lang, region: Option) -> 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) }