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#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)]
89pub struct BibliographyElem {
90 #[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 pub title: Smart<Option<Content>>,
116
117 #[default(false)]
123 pub full: bool,
124
125 #[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 #[internal]
146 #[synthesized]
147 pub lang: Lang,
148
149 #[internal]
151 #[synthesized]
152 pub region: Option<Region>,
153}
154
155impl BibliographyElem {
156 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 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 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#[derive(Clone, PartialEq, Hash)]
289pub struct Bibliography(Arc<ManuallyHash<IndexMap<Label, hayagriva::Entry>>>);
290
291impl Bibliography {
292 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 #[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 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
353fn 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 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 let haya_err = match hayagriva::io::from_yaml_str(src) {
376 Ok(library) => return Ok(library),
377 Err(err) => err,
378 };
379
380 let bib_errs = match hayagriva::io::from_biblatex_str(src) {
382 Ok(library) => return Ok(library),
383 Err(err) => err,
384 };
385
386 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
407fn 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#[derive(Debug, Clone, PartialEq, Hash)]
434pub struct CslStyle(Arc<ManuallyHash<citationberg::IndependentStyle>>);
435
436impl CslStyle {
437 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 #[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 _ => unreachable!("archive should not contain dependant styles"),
462 }
463 }
464
465 #[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 pub fn get(&self) -> &citationberg::IndependentStyle {
481 self.0.as_ref()
482 }
483}
484
485#[derive(Debug, Clone, PartialEq, Hash)]
487pub enum CslSource {
488 Named(ArchivedStyle),
490 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 Self::Named(v) => v.names().last().unwrap().into_value(),
533 Self::Normal(v) => v.into_value(),
534 }
535 }
536}
537
538pub(super) struct Works {
543 pub citations: HashMap<Location, SourceResult<Content>>,
545 pub references: Option<Vec<(Option<Content>, Content)>>,
548 pub hanging_indent: bool,
550}
551
552impl Works {
553 pub fn generate(engine: &Engine) -> StrResult<Arc<Works>> {
555 Self::generate_impl(engine.routines, engine.world, engine.introspector)
556 }
557
558 #[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
572struct Generator<'a> {
574 routines: &'a Routines,
576 world: Tracked<'a, dyn World + 'a>,
578 bibliography: Packed<BibliographyElem>,
580 groups: EcoVec<Content>,
582 infos: Vec<GroupInfo>,
585 failures: HashMap<Location, SourceResult<Content>>,
587}
588
589struct GroupInfo {
593 location: Location,
595 span: Span,
597 footnote: bool,
599 subinfos: SmallVec<[CiteInfo; 1]>,
601}
602
603struct CiteInfo {
605 key: Label,
607 supplement: Option<Content>,
609 hidden: bool,
611}
612
613impl<'a> Generator<'a> {
614 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 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 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 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 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 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 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 fn display_citations(
760 &mut self,
761 rendered: &hayagriva::Rendered,
762 ) -> StrResult<HashMap<Location, SourceResult<Content>>> {
763 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 #[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 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 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 let backlink = location.variant(k + 1);
845
846 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 let mut reference =
863 renderer.display_elem_children(&item.content, &mut prefix, false)?;
864
865 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
876struct ElemRenderer<'a> {
878 routines: &'a Routines,
880 world: Tracked<'a, dyn World + 'a>,
882 span: Span,
884 supplement: &'a dyn Fn(usize) -> Option<Content>,
886 link: &'a dyn Fn(usize) -> Option<Location>,
888}
889
890impl ElemRenderer<'_> {
891 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 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 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 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 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 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 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
1042fn 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 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
1092fn 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}