1use std::any::TypeId;
2use std::fmt::{self, Debug, Formatter};
3use std::num::NonZeroUsize;
4use std::path::Path;
5use std::sync::{Arc, LazyLock};
6
7use comemo::{Track, Tracked, TrackedMut};
8use ecow::{EcoString, EcoVec, eco_format, eco_vec};
9use hayagriva::archive::ArchivedStyle;
10use hayagriva::io::BibLaTeXError;
11use hayagriva::{
12 BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, Library,
13 SpecificLocator, TransparentLocator, citationberg,
14};
15use indexmap::IndexMap;
16use rustc_hash::{FxBuildHasher, FxHashMap};
17use smallvec::SmallVec;
18use typst_syntax::{Span, Spanned, SyntaxMode};
19use typst_utils::{
20 LazyHash, ManuallyHash, NonZeroExt, PicoStr, Protected, ResolvedPicoStr,
21};
22
23use crate::World;
24use crate::diag::{
25 At, HintedStrResult, HintedString, LoadError, LoadResult, LoadedWithin,
26 ReportTextPos, SourceDiagnostic, SourceResult, StrResult, bail, error, warning,
27};
28use crate::engine::{Engine, Route, Sink, Traced};
29use crate::foundations::{
30 Bytes, CastInfo, Content, Context, Derived, FromValue, IntoValue, Label,
31 LocatableSelector, NativeElement, OneOrMultiple, Packed, Reflect, Repr, Scope,
32 Selector, ShowSet, Smart, StyleChain, Styles, Synthesize, Value, elem,
33};
34use crate::introspection::{
35 EmptyIntrospector, History, Introspect, Introspector, Locatable, Location,
36 QueryIntrospection,
37};
38use crate::layout::{BlockElem, Em, HElem, PadElem};
39use crate::loading::{DataSource, Load, LoadSource, Loaded, format_yaml_error};
40use crate::model::{
41 CitationForm, CiteElem, CiteGroup, Destination, DirectLinkElem, FootnoteElem,
42 HeadingElem, LinkElem, Url,
43};
44use crate::routines::SpanMode;
45use crate::text::{Lang, LocalName, Region, SmallcapsElem, SubElem, SuperElem, TextElem};
46
47#[elem(Locatable, Synthesize, ShowSet, LocalName)]
112pub struct BibliographyElem {
113 #[required]
121 #[parse(
122 let sources = args.expect("sources")?;
123 Bibliography::load(engine.world, sources)?
124 )]
125 pub sources: Derived<OneOrMultiple<DataSource>, Bibliography>,
126
127 pub title: Smart<Option<Content>>,
138
139 #[default(false)]
145 pub full: bool,
146
147 #[parse(match args.named::<Spanned<CslSource>>("style")? {
157 Some(source) => Some(CslStyle::load(engine, source)?),
158 None => None,
159 })]
160 #[default({
161 let default = ArchivedStyle::InstituteOfElectricalAndElectronicsEngineers;
162 Derived::new(CslSource::Named(default, None), CslStyle::from_archived(default))
163 })]
164 pub style: Derived<CslSource, CslStyle>,
165
166 pub target: Smart<LocatableSelector>,
211
212 #[default(Some(Smart::Auto))]
273 pub group: Option<Smart<EcoString>>,
274
275 #[internal]
277 #[synthesized]
278 pub lang: Lang,
279
280 #[internal]
282 #[synthesized]
283 pub region: Option<Region>,
284}
285
286impl BibliographyElem {
287 pub fn has(engine: &mut Engine, key: Label, span: Span) -> bool {
289 engine
290 .introspect(QueryIntrospection(Self::ELEM.select(), span))
291 .iter()
292 .any(|elem| elem.to_packed::<Self>().unwrap().sources.derived.has(key))
293 }
294
295 pub fn keys(
297 introspector: Tracked<dyn Introspector + '_>,
298 ) -> Vec<(Label, Option<EcoString>)> {
299 let mut vec = vec![];
300 for elem in introspector.query(&Self::ELEM.select()).iter() {
301 let this = elem.to_packed::<Self>().unwrap();
302 for (key, entry) in this.sources.derived.iter() {
303 let detail = entry.title().map(|title| title.value.to_str().into());
304 vec.push((key, detail))
305 }
306 }
307 vec
308 }
309}
310
311impl Packed<BibliographyElem> {
312 pub fn realize_title(&self, styles: StyleChain) -> Option<Content> {
314 self.title
315 .get_cloned(styles)
316 .unwrap_or_else(|| {
317 Some(TextElem::packed(Packed::<BibliographyElem>::local_name_in(styles)))
318 })
319 .map(|title| {
320 HeadingElem::new(title)
321 .with_depth(NonZeroUsize::ONE)
322 .pack()
323 .spanned(self.span())
324 })
325 }
326}
327
328impl Synthesize for Packed<BibliographyElem> {
329 fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
330 let elem = self.as_mut();
331 elem.lang = Some(styles.get(TextElem::lang));
332 elem.region = Some(styles.get(TextElem::region));
333 Ok(())
334 }
335}
336
337impl ShowSet for Packed<BibliographyElem> {
338 fn show_set(&self, _: StyleChain) -> Styles {
339 const INDENT: Em = Em::new(1.0);
340 let mut out = Styles::new();
341 out.set(HeadingElem::numbering, None);
342 out.set(PadElem::left, INDENT.into());
343 out
344 }
345}
346
347impl LocalName for Packed<BibliographyElem> {
348 const KEY: &'static str = "bibliography";
349}
350
351#[derive(Clone, PartialEq, Hash)]
353pub struct Bibliography(
354 Arc<ManuallyHash<IndexMap<Label, hayagriva::Entry, FxBuildHasher>>>,
355);
356
357impl Bibliography {
358 fn load(
360 world: Tracked<dyn World + '_>,
361 sources: Spanned<OneOrMultiple<DataSource>>,
362 ) -> SourceResult<Derived<OneOrMultiple<DataSource>, Self>> {
363 let loaded = sources.load(world)?;
364 let bibliography = Self::decode(&loaded)?;
365 Ok(Derived::new(sources.v, bibliography))
366 }
367
368 #[comemo::memoize]
370 #[typst_macros::time(name = "load bibliography")]
371 fn decode(data: &[Loaded]) -> SourceResult<Bibliography> {
372 let mut map = IndexMap::default();
373 let mut duplicates = Vec::<EcoString>::new();
374
375 for d in data.iter() {
377 let library = decode_library(d)?;
378 for entry in library {
379 let label = Label::new(PicoStr::intern(entry.key()))
380 .ok_or("bibliography contains entry with empty key")
381 .at(d.source.span)?;
382
383 match map.entry(label) {
384 indexmap::map::Entry::Vacant(vacant) => {
385 vacant.insert(entry);
386 }
387 indexmap::map::Entry::Occupied(_) => {
388 duplicates.push(entry.key().into());
389 }
390 }
391 }
392 }
393
394 if !duplicates.is_empty() {
395 let span = data.first().unwrap().source.span;
399 bail!(span, "duplicate bibliography keys: {}", duplicates.join(", "));
400 }
401
402 Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data)))))
403 }
404
405 fn has(&self, key: Label) -> bool {
406 self.0.contains_key(&key)
407 }
408
409 fn get(&self, key: Label) -> Option<&hayagriva::Entry> {
410 self.0.get(&key)
411 }
412
413 fn iter(&self) -> impl Iterator<Item = (Label, &hayagriva::Entry)> {
414 self.0.iter().map(|(&k, v)| (k, v))
415 }
416}
417
418impl Debug for Bibliography {
419 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
420 f.debug_set().entries(self.0.keys()).finish()
421 }
422}
423
424fn decode_library(loaded: &Loaded) -> SourceResult<Library> {
426 let data = loaded.data.as_str().within(loaded)?;
427
428 if let LoadSource::Path(file_id) = loaded.source.v {
429 let ext = file_id.vpath().extension().unwrap_or_default();
432 match ext.to_lowercase().as_str() {
433 "yml" | "yaml" => hayagriva::io::from_yaml_str(data)
434 .map_err(format_yaml_error)
435 .within(loaded),
436 "bib" => hayagriva::io::from_biblatex_str(data)
437 .map_err(format_biblatex_error)
438 .within(loaded),
439 _ => bail!(
440 loaded.source.span,
441 "unknown bibliography format (must be .yaml/.yml or .bib)"
442 ),
443 }
444 } else {
445 let haya_err = match hayagriva::io::from_yaml_str(data) {
448 Ok(library) => return Ok(library),
449 Err(err) => err,
450 };
451
452 let bib_errs = match hayagriva::io::from_biblatex_str(data) {
454 Ok(library) if !library.is_empty() => return Ok(library),
458 Ok(_) => None,
459 Err(err) => Some(err),
460 };
461
462 let mut yaml = 0;
466 let mut biblatex = 0;
467 for c in data.chars() {
468 match c {
469 ':' => yaml += 1,
470 '{' => biblatex += 1,
471 _ => {}
472 }
473 }
474
475 match bib_errs {
476 Some(bib_errs) if biblatex >= yaml => {
477 Err(format_biblatex_error(bib_errs)).within(loaded)
478 }
479 _ => Err(format_yaml_error(haya_err)).within(loaded),
480 }
481 }
482}
483
484fn format_biblatex_error(errors: Vec<BibLaTeXError>) -> LoadError {
486 let Some(error) = errors.into_iter().next() else {
488 return LoadError::text(
490 ReportTextPos::None,
491 "failed to parse BibLaTeX",
492 "something went wrong",
493 );
494 };
495
496 let (range, msg) = match error {
497 BibLaTeXError::Parse(error) => (error.span, error.kind.to_string()),
498 BibLaTeXError::Type(error) => (error.span, error.kind.to_string()),
499 };
500
501 LoadError::text(range, "failed to parse BibLaTeX", msg)
502}
503
504#[derive(Debug, Clone, PartialEq, Hash)]
506pub struct CslStyle(Arc<ManuallyHash<citationberg::IndependentStyle>>);
507
508impl CslStyle {
509 pub fn load(
511 engine: &mut Engine,
512 Spanned { v: source, span }: Spanned<CslSource>,
513 ) -> SourceResult<Derived<CslSource, Self>> {
514 let style = match &source {
515 CslSource::Named(style, deprecation) => {
516 if let Some(message) = deprecation {
517 engine.sink.warn(SourceDiagnostic::warning(span, message.clone()));
518 }
519 Self::from_archived(*style)
520 }
521 CslSource::Normal(source) => {
522 let loaded = Spanned::new(source, span).load(engine.world)?;
523 Self::from_data(&loaded.data).within(&loaded)?
524 }
525 };
526 Ok(Derived::new(source, style))
527 }
528
529 #[comemo::memoize]
531 pub fn from_archived(archived: ArchivedStyle) -> CslStyle {
532 match archived.get() {
533 citationberg::Style::Independent(style) => Self(Arc::new(ManuallyHash::new(
534 style,
535 typst_utils::hash128(&(TypeId::of::<ArchivedStyle>(), archived)),
536 ))),
537 _ => unreachable!("archive should not contain dependent styles"),
539 }
540 }
541
542 #[comemo::memoize]
544 pub fn from_data(bytes: &Bytes) -> LoadResult<CslStyle> {
545 let text = bytes.as_str()?;
546 citationberg::IndependentStyle::from_xml(text)
547 .map(|style| {
548 Self(Arc::new(ManuallyHash::new(
549 style,
550 typst_utils::hash128(&(TypeId::of::<Bytes>(), bytes)),
551 )))
552 })
553 .map_err(|err| {
554 LoadError::text(ReportTextPos::None, "failed to load CSL style", err)
555 })
556 }
557
558 pub fn get(&self) -> &citationberg::IndependentStyle {
560 self.0.as_ref()
561 }
562}
563
564#[derive(Debug, Clone, PartialEq, Hash)]
566pub enum CslSource {
567 Named(ArchivedStyle, Option<EcoString>),
569 Normal(DataSource),
571}
572
573impl Reflect for CslSource {
574 #[comemo::memoize]
575 fn input() -> CastInfo {
576 let source = std::iter::once(DataSource::input());
577
578 static ARCHIVED_STYLE_NAMES: LazyLock<Vec<(&&str, &'static str)>> =
580 LazyLock::new(|| {
581 ArchivedStyle::all()
582 .iter()
583 .flat_map(|name| {
584 let (main_name, aliases) = name
585 .names()
586 .split_first()
587 .expect("all ArchivedStyle should have at least one name");
588
589 std::iter::once((main_name, name.display_name())).chain(
590 aliases.iter().map(move |alias| {
591 let docs: &'static str = Box::leak(
593 format!("A short alias of `{main_name}`")
594 .into_boxed_str(),
595 );
596 (alias, docs)
597 }),
598 )
599 })
600 .collect()
601 });
602 let names = ARCHIVED_STYLE_NAMES
603 .iter()
604 .map(|(value, docs)| CastInfo::Value(value.into_value(), docs));
605
606 CastInfo::Union(source.into_iter().chain(names).collect())
607 }
608
609 fn output() -> CastInfo {
610 DataSource::output()
611 }
612
613 fn castable(value: &Value) -> bool {
614 DataSource::castable(value)
615 }
616}
617
618impl FromValue for CslSource {
619 fn from_value(value: Value) -> HintedStrResult<Self> {
620 if EcoString::castable(&value) {
621 let string = EcoString::from_value(value.clone())?;
622 if Path::new(string.as_str()).extension().is_none() {
623 let replacement = replacement(&string);
624 let deprecation = replacement.map(|instead| {
625 eco_format!(
626 "style `{}` has been deprecated in favor of `{}`",
627 string.repr(),
628 instead.repr(),
629 )
630 });
631 let style = ArchivedStyle::by_name(&string).ok_or_else(|| {
632 deprecation
633 .clone()
634 .unwrap_or_else(|| eco_format!("unknown style: {string}"))
635 })?;
636 return Ok(CslSource::Named(style, deprecation));
637 }
638 }
639
640 DataSource::from_value(value).map(CslSource::Normal)
641 }
642}
643
644impl IntoValue for CslSource {
645 fn into_value(self) -> Value {
646 match self {
647 Self::Named(v, _) => v.names().last().unwrap().into_value(),
649 Self::Normal(v) => v.into_value(),
650 }
651 }
652}
653
654fn replacement(style: &str) -> Option<&'static str> {
658 Some(match style {
659 "chicago-fullnotes" => "chicago-notes",
660 "modern-humanities-research-association" => {
661 "modern-humanities-research-association-notes"
662 }
663 "council-of-science-editors" => "cse-citation-sequence-brackets-8th-edition",
664 "council-of-science-editors-author-date" => "cse-name-year",
665 "modern-language-association-8" | "mla-8" => "modern-language-association",
666 "vancouver" => "nlm-citation-sequence",
667 "vancouver-superscript" => "nlm-citation-sequence-superscript",
668 _ => return None,
669 })
670}
671
672pub struct Works {
682 bibliographies: FxHashMap<Location, SourceResult<RenderedBibliography>>,
684 groups: FxHashMap<Location, SourceResult<Content>>,
686}
687
688pub struct RenderedBibliography {
690 pub entries: Vec<RenderedEntry>,
692 pub hanging_indent: bool,
694}
695
696pub struct RenderedEntry {
698 pub prefix: Option<Content>,
701 pub body: Content,
703 pub backlink: Location,
706}
707
708impl Works {
709 pub fn generate(engine: &mut Engine, span: Span) -> SourceResult<Arc<Works>> {
711 let bibs_and_groups = engine.introspect(BibliographyIntrospection(span));
712 Self::generate_impl(
713 engine.world,
714 engine.library,
715 engine.introspector.into_raw(),
716 engine.traced,
717 TrackedMut::reborrow_mut(&mut engine.sink),
718 engine.route.track(),
719 &bibs_and_groups,
720 )
721 .at(span)
722 }
723
724 #[comemo::memoize]
726 fn generate_impl(
727 world: Tracked<dyn World + '_>,
728 library: &LazyHash<crate::Library>,
729 introspector: Tracked<dyn Introspector + '_>,
730 traced: Tracked<Traced>,
731 sink: TrackedMut<Sink>,
732 route: Tracked<Route>,
733 bibs_and_groups: &[Content],
734 ) -> StrResult<Arc<Works>> {
735 let mut engine = Engine {
736 world,
737 library,
738 introspector: Protected::from_raw(introspector),
739 traced,
740 sink,
741 route: Route::extend(route),
742 };
743
744 let p = prepare(&mut engine, bibs_and_groups);
747
748 let mut offsets = FxHashMap::default();
750 let rendered =
751 p.bibs.iter().map(|bib| render(bib, &mut offsets)).collect::<Vec<_>>();
752
753 Ok(Arc::new(Works {
754 bibliographies: show_bibliographies(world, &p, &rendered),
755 groups: show_cite_groups(world, p, &rendered),
756 }))
757 }
758
759 pub fn citation(&self, loc: Location, span: Span) -> SourceResult<Content> {
761 self.groups
762 .get(&loc)
763 .cloned()
764 .ok_or_else(citation_could_not_be_located)
765 .at(span)?
766 }
767
768 pub fn bibliography(
770 &self,
771 loc: Location,
772 span: Span,
773 ) -> SourceResult<&RenderedBibliography> {
774 self.bibliographies
775 .get(&loc)
776 .ok_or_else(bibliography_could_not_be_located)
777 .at(span)?
778 .as_ref()
779 .map_err(Clone::clone)
780 }
781}
782
783struct Preparation<'a> {
786 bibs: Vec<PreparedBibliography<'a>>,
789 groups: FxHashMap<Location, SourceResult<PreparedCiteGroup<'a>>>,
792}
793
794struct PreparedBibliography<'a> {
797 elem: &'a Packed<BibliographyElem>,
799 subgroups: Vec<Subgroup<'a>>,
804}
805
806struct Subgroup<'a> {
811 elem: &'a Packed<CiteGroup>,
813 citations: SmallVec<[&'a Packed<CiteElem>; 1]>,
815 style: &'a CslStyle,
818}
819
820struct PreparedCiteGroup<'a>(SmallVec<[GroupPart<'a>; 1]>);
823
824enum GroupPart<'a> {
826 Content(&'a Content),
829 Subgroup(BibIndex, SubgroupIndex),
832}
833
834#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
837struct BibIndex(usize);
838
839#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
842struct SubgroupIndex(usize);
843
844fn prepare<'a>(engine: &mut Engine, bibs_and_groups: &'a [Content]) -> Preparation<'a> {
853 let mut selected = FxHashMap::<Location, BibIndex>::default();
856
857 let mut bibs = Vec::<PreparedBibliography>::new();
862 for elem in bibs_and_groups {
863 let Some(bib) = elem.to_packed::<BibliographyElem>() else { continue };
864 let idx = BibIndex(bibs.len());
865 bibs.push(PreparedBibliography { elem: bib, subgroups: vec![] });
866
867 if let Smart::Custom(LocatableSelector(selector)) =
868 bib.target.get_cloned(StyleChain::default())
869 {
870 for citation in engine.introspect(QueryIntrospection(selector, bib.span())) {
871 selected.entry(citation.location().unwrap()).or_insert(idx);
872 }
873 }
874 }
875
876 let mut groups = FxHashMap::default();
879 let mut bib_cursor = 0;
880 for elem in bibs_and_groups {
881 let Some(group) = elem.to_packed::<CiteGroup>() else {
882 debug_assert!(elem.is::<BibliographyElem>());
883 bib_cursor += 1;
884 continue;
885 };
886
887 let loc = group.location().unwrap();
888 let result = prepare_cite_group(&mut bibs, bib_cursor, group, &selected);
889 groups.insert(loc, result);
890 }
891
892 Preparation { bibs, groups }
893}
894
895fn prepare_cite_group<'a>(
901 bibs: &mut [PreparedBibliography<'a>],
902 bib_cursor: usize,
903 group: &'a Packed<CiteGroup>,
904 selected: &FxHashMap<Location, BibIndex>,
905) -> SourceResult<PreparedCiteGroup<'a>> {
906 let mut parts = SmallVec::new();
908 let mut subgroup = SmallVec::new();
910 let mut subgroup_bib = None;
913 let mut tail = 0;
915 let mut errors = EcoVec::new();
918
919 for (i, child) in group.children.iter().enumerate() {
920 let Some(citation) = child.to_packed::<CiteElem>() else { continue };
927 let spaces = &group.children[tail..i];
928 tail = i + 1;
929
930 let bib_idx = if let Some(&idx) = selected.get(&citation.location().unwrap()) {
933 let bib = &bibs[idx.0];
935 if !bib.elem.sources.derived.has(citation.key) {
936 errors.push(key_does_not_exist(citation, bib));
937 continue;
938 }
939 idx
940 } else if let Some(idx) = select_auto_bib(citation, bibs, bib_cursor) {
941 idx
942 } else {
943 errors.push(uncovered_citation(citation, bibs));
944 continue;
945 };
946
947 if let Some(subgroup_bib) = subgroup_bib
950 && subgroup_bib != bib_idx
951 {
952 parts.push(save_subgroup(
953 bibs,
954 subgroup_bib,
955 group,
956 std::mem::take(&mut subgroup),
957 ));
958 parts.extend(spaces.iter().map(GroupPart::Content));
959 }
960
961 subgroup_bib = Some(bib_idx);
962 subgroup.push(citation);
963 }
964
965 if let Some(subgroup_bib) = subgroup_bib {
967 parts.push(save_subgroup(bibs, subgroup_bib, group, subgroup));
968 }
969
970 if !errors.is_empty() {
971 return Err(errors);
972 }
973
974 Ok(PreparedCiteGroup(parts))
975}
976
977fn save_subgroup<'a>(
981 bibs: &mut [PreparedBibliography<'a>],
982 bib_idx: BibIndex,
983 elem: &'a Packed<CiteGroup>,
984 citations: SmallVec<[&'a Packed<CiteElem>; 1]>,
985) -> GroupPart<'a> {
986 let bib = &mut bibs[bib_idx.0];
987 let style = if let Some(first) = citations.first()
988 && let Smart::Custom(style) = first.style.get_ref(StyleChain::default())
989 {
990 &style.derived
991 } else {
992 &bib.elem.style.get_ref(StyleChain::default()).derived
993 };
994
995 let sub_idx = SubgroupIndex(bib.subgroups.len());
996 bib.subgroups.push(Subgroup { elem, citations, style });
997
998 GroupPart::Subgroup(bib_idx, sub_idx)
999}
1000
1001fn select_auto_bib(
1010 citation: &Packed<CiteElem>,
1011 bibs: &[PreparedBibliography],
1012 bib_cursor: usize,
1013) -> Option<BibIndex> {
1014 let bibs = bibs.iter().enumerate();
1015 let before = bibs.clone().take(bib_cursor);
1016 let after = bibs.skip(bib_cursor);
1017 after
1018 .chain(before.rev())
1019 .find(|(_, bib)| {
1020 bib.elem.target.get_ref(StyleChain::default()).is_auto()
1021 && bib.elem.sources.derived.has(citation.key)
1022 })
1023 .map(|(idx, _)| BibIndex(idx))
1024}
1025
1026fn render<'a>(
1028 bib: &PreparedBibliography<'a>,
1029 offsets: &mut FxHashMap<Smart<&'a str>, usize>,
1030) -> hayagriva::Rendered {
1031 static LOCALES: LazyLock<Vec<citationberg::Locale>> =
1032 LazyLock::new(hayagriva::archive::locales);
1033
1034 let database = &bib.elem.sources.derived;
1035
1036 let mut driver = BibliographyDriver::new();
1037 let mut offset = bib
1038 .elem
1039 .group
1040 .get_ref(StyleChain::default())
1041 .as_ref()
1042 .map(|group| offsets.entry(group.as_deref()).or_insert(0));
1043
1044 if let Some(offset) = &mut offset {
1045 driver = driver.with_citation_number_offset(**offset);
1046 }
1047
1048 for group in &bib.subgroups {
1049 let items = group
1050 .citations
1051 .iter()
1052 .map(|child| {
1053 let entry = database.get(child.key).expect("entry to be present");
1054 citation_item(entry, child)
1055 })
1056 .collect::<Vec<_>>();
1057
1058 let first = &group.citations[0];
1059 let locale = locale(first.lang.unwrap_or(Lang::ENGLISH), first.region.flatten());
1060
1061 driver.citation(CitationRequest::new(
1062 items,
1063 group.style.get(),
1064 Some(locale),
1065 &LOCALES,
1066 None,
1067 ));
1068 }
1069
1070 let bib_style = &bib.elem.style.get_ref(StyleChain::default()).derived;
1071 let locale =
1072 locale(bib.elem.lang.unwrap_or(Lang::ENGLISH), bib.elem.region.flatten());
1073
1074 if bib.elem.full.get(StyleChain::default()) {
1077 for (_, entry) in database.iter() {
1078 driver.citation(CitationRequest::new(
1079 vec![CitationItem::new(entry, None, None, true, None)],
1080 bib_style.get(),
1081 Some(locale.clone()),
1082 &LOCALES,
1083 None,
1084 ));
1085 }
1086 }
1087
1088 let rendered = driver.finish(BibliographyRequest {
1089 style: bib_style.get(),
1090 locale: Some(locale),
1091 locale_files: &LOCALES,
1092 });
1093
1094 if let Some(offset) = offset
1095 && let Some(bib) = &rendered.bibliography
1096 && (bib.items.iter().any(displays_citation_number)
1100 || rendered.citations.iter().any(|rendered| {
1101 rendered
1102 .citation
1103 .find_meta(&hayagriva::ElemMeta::CitationNumber)
1104 .is_some()
1105 }))
1106 {
1107 *offset += bib.items.len();
1108 }
1109
1110 rendered
1111}
1112
1113fn displays_citation_number(item: &hayagriva::BibliographyItem) -> bool {
1118 item.content.find_meta(&hayagriva::ElemMeta::CitationNumber).is_some()
1119 || item.first_field.as_ref().is_some_and(|child| match child {
1120 hayagriva::ElemChild::Elem(elem) => {
1121 elem.meta == Some(hayagriva::ElemMeta::CitationNumber)
1122 || elem
1123 .children
1124 .find_meta(&hayagriva::ElemMeta::CitationNumber)
1125 .is_some()
1126 }
1127 _ => false,
1128 })
1129}
1130
1131fn citation_item<'a>(
1133 entry: &'a hayagriva::Entry,
1134 child: &'a Packed<CiteElem>,
1135) -> CitationItem<'a, hayagriva::Entry> {
1136 let supplement = child.supplement.get_cloned(StyleChain::default());
1137 let locator = supplement.as_ref().map(|c| {
1138 SpecificLocator(
1139 citationberg::taxonomy::Locator::Custom,
1140 hayagriva::LocatorPayload::Transparent(TransparentLocator::new(c.clone())),
1141 )
1142 });
1143
1144 let mut hidden = false;
1145 let special_form = match child.form.get(StyleChain::default()) {
1146 None => {
1147 hidden = true;
1148 None
1149 }
1150 Some(CitationForm::Normal) => None,
1151 Some(CitationForm::Prose) => Some(hayagriva::CitePurpose::Prose),
1152 Some(CitationForm::Full) => Some(hayagriva::CitePurpose::Full),
1153 Some(CitationForm::Author) => Some(hayagriva::CitePurpose::Author),
1154 Some(CitationForm::Year) => Some(hayagriva::CitePurpose::Year),
1155 };
1156
1157 CitationItem::new(entry, locator, None, hidden, special_form)
1158}
1159
1160fn show_bibliographies(
1164 world: Tracked<dyn World + '_>,
1165 p: &Preparation,
1166 rendered: &[hayagriva::Rendered],
1167) -> FxHashMap<Location, SourceResult<RenderedBibliography>> {
1168 p.bibs
1169 .iter()
1170 .zip(rendered)
1171 .map(|(bib, rendered)| {
1172 let loc = bib.elem.location().unwrap();
1173 let result = rendered
1174 .bibliography
1175 .as_ref()
1176 .ok_or_else(|| {
1177 style_unsuitable(
1178 &bib.elem.style.get_ref(StyleChain::default()).source,
1179 )
1180 })
1181 .and_then(|rendered| show_bibliography(world, bib, rendered))
1182 .at(bib.elem.span());
1183 (loc, result)
1184 })
1185 .collect()
1186}
1187
1188fn show_bibliography(
1191 world: Tracked<dyn World + '_>,
1192 bib: &PreparedBibliography,
1193 rendered: &hayagriva::RenderedBibliography,
1194) -> StrResult<RenderedBibliography> {
1195 let to_citations = links_to_citations(&bib.subgroups);
1196
1197 let mut entries = Vec::with_capacity(rendered.items.len());
1198 for (k, item) in rendered.items.iter().enumerate() {
1199 let ctx = ShowCtx {
1200 world,
1201 span: bib.elem.span(),
1202 supplement: &|_| None,
1203 link: &|_| None,
1204 };
1205
1206 let mut prefix = item
1208 .first_field
1209 .as_ref()
1210 .map(|elem| show_elem_child(&ctx, elem, None, false))
1211 .transpose()?;
1212
1213 let body = show_elem_children(&ctx, &item.content, Some(&mut prefix), false)?;
1215
1216 let prefix = prefix.map(|content| {
1218 if let Some(location) = to_citations.get(item.key.as_str()) {
1219 let alt = content.plain_text();
1220 let body = content.spanned(ctx.span);
1221 DirectLinkElem::new(*location, body, Some(alt)).pack()
1222 } else {
1223 content
1224 }
1225 });
1226
1227 entries.push(RenderedEntry { prefix, body, backlink: entry_location(bib, k) });
1228 }
1229
1230 Ok(RenderedBibliography { entries, hanging_indent: rendered.hanging_indent })
1231}
1232
1233fn show_cite_groups(
1237 world: Tracked<dyn World + '_>,
1238 p: Preparation,
1239 rendered: &[hayagriva::Rendered],
1240) -> FxHashMap<Location, SourceResult<Content>> {
1241 let to_entries = links_to_entries(&p, rendered);
1242 p.groups
1243 .into_iter()
1244 .map(|(loc, group)| {
1245 let result = group.and_then(|group| {
1246 show_cite_group(world, &group, &p.bibs, rendered, |idx, key| {
1247 to_entries.get(&(idx, key)).copied()
1248 })
1249 });
1250 (loc, result)
1251 })
1252 .collect()
1253}
1254
1255fn show_cite_group(
1259 world: Tracked<dyn World + '_>,
1260 group: &PreparedCiteGroup,
1261 prepared: &[PreparedBibliography],
1262 rendered: &[hayagriva::Rendered],
1263 to_entry: impl Fn(BibIndex, &str) -> Option<Location>,
1264) -> SourceResult<Content> {
1265 let mut seq = vec![];
1266 for part in &group.0 {
1267 seq.push(match part {
1268 GroupPart::Content(c) => (**c).clone(),
1269 GroupPart::Subgroup(bib_idx, sub_idx) => {
1270 let subgroup = &prepared[bib_idx.0].subgroups[sub_idx.0];
1271 let item = &rendered[bib_idx.0].citations[sub_idx.0];
1272 show_subgroup(world, subgroup, item, |key| to_entry(*bib_idx, key))?
1273 }
1274 });
1275 }
1276 Ok(Content::sequence(seq))
1277}
1278
1279fn show_subgroup(
1281 world: Tracked<dyn World + '_>,
1282 group: &Subgroup,
1283 citation: &hayagriva::RenderedCitation,
1284 to_entry: impl Fn(&str) -> Option<Location>,
1285) -> SourceResult<Content> {
1286 if group
1287 .citations
1288 .iter()
1289 .all(|sub| sub.form.get(StyleChain::default()).is_none())
1290 {
1291 return Ok(Content::empty());
1292 }
1293
1294 let span = Span::find(group.citations.iter().map(|elem| elem.span()));
1295 let supplement =
1296 |i: usize| group.citations.get(i)?.supplement.get_cloned(StyleChain::default());
1297 let link = |i: usize| to_entry(group.citations.get(i)?.key.resolve().as_str());
1298 let ctx = ShowCtx { world, span, supplement: &supplement, link: &link };
1299
1300 let mut realized =
1301 show_elem_children(&ctx, &citation.citation, None, true).at(span)?;
1302
1303 if group.style.get().settings.class == citationberg::StyleClass::Note
1304 && group.citations.iter().all(|sub| {
1305 matches!(
1306 sub.form.get(StyleChain::default()),
1307 None | Some(CitationForm::Normal)
1308 )
1309 })
1310 {
1311 realized = FootnoteElem::with_content(realized).pack();
1312 }
1313
1314 Ok(realized)
1315}
1316
1317fn links_to_citations(groups: &[Subgroup]) -> FxHashMap<ResolvedPicoStr, Location> {
1323 let mut map = FxHashMap::default();
1324 for group in groups {
1325 for child in &group.citations {
1326 let key = child.key.resolve();
1327 map.entry(key).or_insert(group.elem.location().unwrap());
1328 }
1329 }
1330 map
1331}
1332
1333fn links_to_entries<'a>(
1339 p: &Preparation,
1340 rendered: &'a [hayagriva::Rendered],
1341) -> FxHashMap<(BibIndex, &'a str), Location> {
1342 let mut links = FxHashMap::default();
1343 for (i, (bib, rendered)) in p.bibs.iter().zip(rendered).enumerate() {
1344 let Some(rendered) = &rendered.bibliography else { continue };
1345 for (k, item) in rendered.items.iter().enumerate() {
1346 links.insert((BibIndex(i), item.key.as_str()), entry_location(bib, k));
1347 }
1348 }
1349 links
1350}
1351
1352fn entry_location(bib: &PreparedBibliography, k: usize) -> Location {
1357 bib.elem.location().unwrap().variant(k + 1)
1358}
1359
1360struct ShowCtx<'a> {
1362 world: Tracked<'a, dyn World + 'a>,
1364 span: Span,
1366 supplement: &'a dyn Fn(usize) -> Option<Content>,
1368 link: &'a dyn Fn(usize) -> Option<Location>,
1370}
1371
1372fn show_elem_children(
1381 ctx: &ShowCtx,
1382 elems: &hayagriva::ElemChildren,
1383 mut prefix: Option<&mut Option<Content>>,
1384 is_citation: bool,
1385) -> StrResult<Content> {
1386 Ok(Content::sequence(
1387 elems
1388 .0
1389 .iter()
1390 .enumerate()
1391 .map(|(i, elem)| {
1392 show_elem_child(ctx, elem, prefix.as_deref_mut(), is_citation && i == 0)
1393 })
1394 .collect::<StrResult<Vec<_>>>()?,
1395 ))
1396}
1397
1398fn show_elem_child(
1400 ctx: &ShowCtx,
1401 elem: &hayagriva::ElemChild,
1402 prefix: Option<&mut Option<Content>>,
1403 trim_start: bool,
1404) -> StrResult<Content> {
1405 Ok(match elem {
1406 hayagriva::ElemChild::Text(formatted) => {
1407 show_formatted(ctx, formatted, trim_start)
1408 }
1409 hayagriva::ElemChild::Elem(elem) => show_elem(ctx, elem, prefix)?,
1410 hayagriva::ElemChild::Markup(markup) => show_math(ctx, markup),
1411 hayagriva::ElemChild::Link { text, url } => show_link(ctx, text, url)?,
1412 hayagriva::ElemChild::Transparent { cite_idx, format } => {
1413 show_transparent(ctx, *cite_idx, format)
1414 }
1415 })
1416}
1417
1418fn show_elem(
1420 ctx: &ShowCtx,
1421 elem: &hayagriva::Elem,
1422 mut prefix: Option<&mut Option<Content>>,
1423) -> StrResult<Content> {
1424 use citationberg::Display;
1425
1426 let block_level = matches!(elem.display, Some(Display::Block | Display::Indent));
1427
1428 let mut content = show_elem_children(
1429 ctx,
1430 &elem.children,
1431 if block_level { None } else { prefix.as_deref_mut() },
1432 false,
1433 )?;
1434
1435 match elem.display {
1436 Some(Display::Block) => {
1437 content = BlockElem::packed(content).spanned(ctx.span);
1438 }
1439 Some(Display::Indent) => {
1440 content = CslIndentElem::new(content).pack().spanned(ctx.span);
1441 }
1442 Some(Display::LeftMargin) => {
1443 if let Some(prefix) = prefix {
1449 *prefix.get_or_insert_with(Default::default) += content;
1450 return Ok(Content::empty());
1451 }
1452 }
1453 _ => {}
1454 }
1455
1456 content = content.spanned(ctx.span);
1457
1458 if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta
1459 && let Some(location) = (ctx.link)(i)
1460 {
1461 let alt = content.plain_text();
1462 content = DirectLinkElem::new(location, content, Some(alt)).pack();
1463 }
1464
1465 Ok(content)
1466}
1467
1468fn show_math(ctx: &ShowCtx, math: &str) -> Content {
1470 let library = ctx.world.library();
1471 (library.routines.eval_string)(
1472 ctx.world,
1473 library,
1474 Sink::new().track_mut(),
1476 EmptyIntrospector.track(),
1477 Context::none().track(),
1478 math,
1479 SpanMode::Uniform(ctx.span),
1480 SyntaxMode::Math,
1481 Scope::new(),
1482 )
1483 .map(Value::display)
1484 .unwrap_or_else(|_| TextElem::packed(math).spanned(ctx.span))
1485}
1486
1487fn show_link(
1489 ctx: &ShowCtx,
1490 text: &hayagriva::Formatted,
1491 url: &str,
1492) -> StrResult<Content> {
1493 let dest = Destination::Url(Url::new(url)?);
1494 Ok(LinkElem::new(dest.into(), show_formatted(ctx, text, false))
1495 .pack()
1496 .spanned(ctx.span))
1497}
1498
1499fn show_transparent(ctx: &ShowCtx, i: usize, format: &hayagriva::Formatting) -> Content {
1501 let content = (ctx.supplement)(i).unwrap_or_default();
1502 show_with_formatting(content, format)
1503}
1504
1505fn show_formatted(
1507 ctx: &ShowCtx,
1508 formatted: &hayagriva::Formatted,
1509 trim_start: bool,
1510) -> Content {
1511 let formatted_text =
1512 if trim_start { formatted.text.trim_start() } else { formatted.text.as_str() };
1513
1514 let content = TextElem::packed(formatted_text).spanned(ctx.span);
1515 show_with_formatting(content, &formatted.formatting)
1516}
1517
1518fn show_with_formatting(mut content: Content, format: &hayagriva::Formatting) -> Content {
1520 match format.font_style {
1521 citationberg::FontStyle::Normal => {}
1522 citationberg::FontStyle::Italic => {
1523 content = content.emph();
1524 }
1525 }
1526
1527 match format.font_variant {
1528 citationberg::FontVariant::Normal => {}
1529 citationberg::FontVariant::SmallCaps => {
1530 content = SmallcapsElem::new(content).pack();
1531 }
1532 }
1533
1534 match format.font_weight {
1535 citationberg::FontWeight::Normal => {}
1536 citationberg::FontWeight::Bold => {
1537 content = content.strong();
1538 }
1539 citationberg::FontWeight::Light => {
1540 content = CslLightElem::new(content).pack();
1544 }
1545 }
1546
1547 match format.text_decoration {
1548 citationberg::TextDecoration::None => {}
1549 citationberg::TextDecoration::Underline => {
1550 content = content.underlined();
1551 }
1552 }
1553
1554 let span = content.span();
1555 match format.vertical_align {
1556 citationberg::VerticalAlign::None => {}
1557 citationberg::VerticalAlign::Baseline => {}
1558 citationberg::VerticalAlign::Sup => {
1559 content =
1561 HElem::hole().clone() + SuperElem::new(content).pack().spanned(span);
1562 }
1563 citationberg::VerticalAlign::Sub => {
1564 content = HElem::hole().clone() + SubElem::new(content).pack().spanned(span);
1565 }
1566 }
1567
1568 content
1569}
1570
1571fn locale(lang: Lang, region: Option<Region>) -> citationberg::LocaleCode {
1573 let mut value = String::with_capacity(5);
1574 value.push_str(lang.as_str());
1575 if let Some(region) = region {
1576 value.push('-');
1577 value.push_str(region.as_str())
1578 }
1579 citationberg::LocaleCode(value)
1580}
1581
1582#[elem]
1590pub struct CslLightElem {
1591 #[required]
1592 pub body: Content,
1593}
1594
1595#[elem]
1602pub struct CslIndentElem {
1603 #[required]
1604 pub body: Content,
1605}
1606
1607#[derive(Debug, Clone, PartialEq, Hash)]
1613struct BibliographyIntrospection(Span);
1614
1615impl Introspect for BibliographyIntrospection {
1616 type Output = EcoVec<Content>;
1617
1618 fn introspect(
1619 &self,
1620 _: &mut Engine,
1621 introspector: Tracked<dyn Introspector + '_>,
1622 ) -> Self::Output {
1623 introspector.query(&Selector::Or(eco_vec![
1624 BibliographyElem::ELEM.select(),
1625 CiteGroup::ELEM.select(),
1626 ]))
1627 }
1628
1629 fn diagnose(&self, _: &History<Self::Output>) -> SourceDiagnostic {
1630 warning!(self.0, "citations and bibliographies did not stabilize")
1631 }
1632}
1633
1634fn citation_could_not_be_located() -> HintedString {
1636 error!(
1637 "citation could not be located";
1638 hint: "this citation is not stably present in the document";
1639 hint: "this can be caused by measurement or introspection";
1640 )
1641}
1642
1643fn bibliography_could_not_be_located() -> HintedString {
1645 error!(
1646 "bibliography could not be located";
1647 hint: "this bibliography is not stably present in the document";
1648 hint: "this can be caused by measurement or introspection";
1649 )
1650}
1651
1652fn uncovered_citation(
1654 citation: &Packed<CiteElem>,
1655 bibs: &[PreparedBibliography],
1656) -> SourceDiagnostic {
1657 let span = citation.span();
1658 let key = citation.key.resolve();
1659 if bibs.is_empty() {
1660 error!(span, "the document does not contain a bibliography")
1661 } else if let Some(bib) =
1662 bibs.iter().find(|bib| bib.elem.sources.derived.has(citation.key))
1663 {
1664 error!(
1665 span,
1666 "citation is not covered by any bibliography";
1667 hint[bib.elem.span()]:
1668 "a bibliography containing the key `{key}` exists, \
1669 but its `target` excludes this citation";
1670 )
1671 } else {
1672 error!(
1673 span,
1674 "citation key `{key}` is not present in {} bibliography",
1675 if bibs.len() == 1 { "the" } else { "any" },
1676 )
1677 }
1678}
1679
1680fn key_does_not_exist(
1683 citation: &Packed<CiteElem>,
1684 bib: &PreparedBibliography,
1685) -> SourceDiagnostic {
1686 error!(
1687 citation.span(),
1688 "key `{}` does not exist in the bibliography",
1689 citation.key.resolve();
1690 hint[bib.elem.span()]: "the citation was assigned to this bibliography";
1691 )
1692}
1693
1694#[cold]
1696fn style_unsuitable(source: &CslSource) -> EcoString {
1697 match source {
1698 CslSource::Named(style, _) => eco_format!(
1699 "CSL style \"{}\" is not suitable for bibliographies",
1700 style.display_name()
1701 ),
1702 CslSource::Normal(..) => "CSL style is not suitable for bibliographies".into(),
1703 }
1704}
1705
1706#[cfg(test)]
1707mod tests {
1708 use super::*;
1709
1710 #[test]
1711 fn test_bibliography_load_builtin_styles() {
1712 for &archived in ArchivedStyle::all() {
1713 let _ = CslStyle::from_archived(archived);
1714 }
1715 }
1716
1717 #[test]
1718 fn test_csl_source_cast_info_include_all_names() {
1719 let CastInfo::Union(cast_info) = CslSource::input() else {
1720 panic!("the cast info of CslSource should be a union");
1721 };
1722
1723 let missing: Vec<_> = ArchivedStyle::all()
1724 .iter()
1725 .flat_map(|style| style.names())
1726 .filter(|name| {
1727 let found = cast_info.iter().any(|info| match info {
1728 CastInfo::Value(Value::Str(n), _) => n.as_str() == **name,
1729 _ => false,
1730 });
1731 !found
1732 })
1733 .collect();
1734
1735 assert!(
1736 missing.is_empty(),
1737 "missing style names in CslSource cast info: '{missing:?}'"
1738 );
1739 }
1740}