1use std::any::TypeId;
2use std::ffi::OsStr;
3use std::fmt::{self, Debug, Formatter};
4use std::num::NonZeroUsize;
5use std::path::Path;
6use std::sync::{Arc, LazyLock};
7
8use comemo::{Track, Tracked};
9use ecow::{EcoString, EcoVec, eco_format};
10use hayagriva::archive::ArchivedStyle;
11use hayagriva::io::BibLaTeXError;
12use hayagriva::{
13 BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, Library,
14 SpecificLocator, TransparentLocator, citationberg,
15};
16use indexmap::IndexMap;
17use rustc_hash::{FxBuildHasher, FxHashMap};
18use smallvec::SmallVec;
19use typst_syntax::{Span, Spanned, SyntaxMode};
20use typst_utils::{ManuallyHash, NonZeroExt, PicoStr};
21
22use crate::World;
23use crate::diag::{
24 At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos, SourceResult,
25 StrResult, bail, error, warning,
26};
27use crate::engine::{Engine, Sink};
28use crate::foundations::{
29 Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement,
30 OneOrMultiple, Packed, Reflect, Scope, ShowSet, Smart, StyleChain, Styles,
31 Synthesize, Value, elem,
32};
33use crate::introspection::{Introspector, Locatable, Location};
34use crate::layout::{BlockBody, BlockElem, Em, HElem, PadElem};
35use crate::loading::{DataSource, Load, LoadSource, Loaded, format_yaml_error};
36use crate::model::{
37 CitationForm, CiteGroup, Destination, DirectLinkElem, FootnoteElem, HeadingElem,
38 LinkElem, Url,
39};
40use crate::routines::Routines;
41use crate::text::{Lang, LocalName, Region, SmallcapsElem, SubElem, SuperElem, TextElem};
42
43#[elem(Locatable, Synthesize, ShowSet, LocalName)]
86pub struct BibliographyElem {
87 #[required]
96 #[parse(
97 let sources = args.expect("sources")?;
98 Bibliography::load(engine.world, sources)?
99 )]
100 pub sources: Derived<OneOrMultiple<DataSource>, Bibliography>,
101
102 pub title: Smart<Option<Content>>,
113
114 #[default(false)]
120 pub full: bool,
121
122 #[parse(match args.named::<Spanned<CslSource>>("style")? {
132 Some(source) => Some(CslStyle::load(engine, source)?),
133 None => None,
134 })]
135 #[default({
136 let default = ArchivedStyle::InstituteOfElectricalAndElectronicsEngineers;
137 Derived::new(CslSource::Named(default, None), CslStyle::from_archived(default))
138 })]
139 pub style: Derived<CslSource, CslStyle>,
140
141 #[internal]
143 #[synthesized]
144 pub lang: Lang,
145
146 #[internal]
148 #[synthesized]
149 pub region: Option<Region>,
150}
151
152impl BibliographyElem {
153 pub fn find(introspector: Tracked<Introspector>) -> StrResult<Packed<Self>> {
155 let query = introspector.query(&Self::ELEM.select());
156 let mut iter = query.iter();
157 let Some(elem) = iter.next() else {
158 bail!("the document does not contain a bibliography");
159 };
160
161 if iter.next().is_some() {
162 bail!("multiple bibliographies are not yet supported");
163 }
164
165 Ok(elem.to_packed::<Self>().unwrap().clone())
166 }
167
168 pub fn has(engine: &Engine, key: Label) -> bool {
170 engine
171 .introspector
172 .query(&Self::ELEM.select())
173 .iter()
174 .any(|elem| elem.to_packed::<Self>().unwrap().sources.derived.has(key))
175 }
176
177 pub fn keys(introspector: Tracked<Introspector>) -> Vec<(Label, Option<EcoString>)> {
179 let mut vec = vec![];
180 for elem in introspector.query(&Self::ELEM.select()).iter() {
181 let this = elem.to_packed::<Self>().unwrap();
182 for (key, entry) in this.sources.derived.iter() {
183 let detail = entry.title().map(|title| title.value.to_str().into());
184 vec.push((key, detail))
185 }
186 }
187 vec
188 }
189}
190
191impl Packed<BibliographyElem> {
192 pub fn realize_title(&self, styles: StyleChain) -> Option<Content> {
194 self.title
195 .get_cloned(styles)
196 .unwrap_or_else(|| {
197 Some(TextElem::packed(Packed::<BibliographyElem>::local_name_in(styles)))
198 })
199 .map(|title| {
200 HeadingElem::new(title)
201 .with_depth(NonZeroUsize::ONE)
202 .pack()
203 .spanned(self.span())
204 })
205 }
206}
207
208impl Synthesize for Packed<BibliographyElem> {
209 fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
210 let elem = self.as_mut();
211 elem.lang = Some(styles.get(TextElem::lang));
212 elem.region = Some(styles.get(TextElem::region));
213 Ok(())
214 }
215}
216
217impl ShowSet for Packed<BibliographyElem> {
218 fn show_set(&self, _: StyleChain) -> Styles {
219 const INDENT: Em = Em::new(1.0);
220 let mut out = Styles::new();
221 out.set(HeadingElem::numbering, None);
222 out.set(PadElem::left, INDENT.into());
223 out
224 }
225}
226
227impl LocalName for Packed<BibliographyElem> {
228 const KEY: &'static str = "bibliography";
229}
230
231#[derive(Clone, PartialEq, Hash)]
233pub struct Bibliography(
234 Arc<ManuallyHash<IndexMap<Label, hayagriva::Entry, FxBuildHasher>>>,
235);
236
237impl Bibliography {
238 fn load(
240 world: Tracked<dyn World + '_>,
241 sources: Spanned<OneOrMultiple<DataSource>>,
242 ) -> SourceResult<Derived<OneOrMultiple<DataSource>, Self>> {
243 let loaded = sources.load(world)?;
244 let bibliography = Self::decode(&loaded)?;
245 Ok(Derived::new(sources.v, bibliography))
246 }
247
248 #[comemo::memoize]
250 #[typst_macros::time(name = "load bibliography")]
251 fn decode(data: &[Loaded]) -> SourceResult<Bibliography> {
252 let mut map = IndexMap::default();
253 let mut duplicates = Vec::<EcoString>::new();
254
255 for d in data.iter() {
257 let library = decode_library(d)?;
258 for entry in library {
259 let label = Label::new(PicoStr::intern(entry.key()))
260 .ok_or("bibliography contains entry with empty key")
261 .at(d.source.span)?;
262
263 match map.entry(label) {
264 indexmap::map::Entry::Vacant(vacant) => {
265 vacant.insert(entry);
266 }
267 indexmap::map::Entry::Occupied(_) => {
268 duplicates.push(entry.key().into());
269 }
270 }
271 }
272 }
273
274 if !duplicates.is_empty() {
275 let span = data.first().unwrap().source.span;
279 bail!(span, "duplicate bibliography keys: {}", duplicates.join(", "));
280 }
281
282 Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data)))))
283 }
284
285 fn has(&self, key: Label) -> bool {
286 self.0.contains_key(&key)
287 }
288
289 fn get(&self, key: Label) -> Option<&hayagriva::Entry> {
290 self.0.get(&key)
291 }
292
293 fn iter(&self) -> impl Iterator<Item = (Label, &hayagriva::Entry)> {
294 self.0.iter().map(|(&k, v)| (k, v))
295 }
296}
297
298impl Debug for Bibliography {
299 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
300 f.debug_set().entries(self.0.keys()).finish()
301 }
302}
303
304fn decode_library(loaded: &Loaded) -> SourceResult<Library> {
306 let data = loaded.data.as_str().within(loaded)?;
307
308 if let LoadSource::Path(file_id) = loaded.source.v {
309 let ext = file_id
312 .vpath()
313 .as_rooted_path()
314 .extension()
315 .and_then(OsStr::to_str)
316 .unwrap_or_default();
317
318 match ext.to_lowercase().as_str() {
319 "yml" | "yaml" => hayagriva::io::from_yaml_str(data)
320 .map_err(format_yaml_error)
321 .within(loaded),
322 "bib" => hayagriva::io::from_biblatex_str(data)
323 .map_err(format_biblatex_error)
324 .within(loaded),
325 _ => bail!(
326 loaded.source.span,
327 "unknown bibliography format (must be .yaml/.yml or .bib)"
328 ),
329 }
330 } else {
331 let haya_err = match hayagriva::io::from_yaml_str(data) {
334 Ok(library) => return Ok(library),
335 Err(err) => err,
336 };
337
338 let bib_errs = match hayagriva::io::from_biblatex_str(data) {
340 Ok(library) if !library.is_empty() => return Ok(library),
344 Ok(_) => None,
345 Err(err) => Some(err),
346 };
347
348 let mut yaml = 0;
352 let mut biblatex = 0;
353 for c in data.chars() {
354 match c {
355 ':' => yaml += 1,
356 '{' => biblatex += 1,
357 _ => {}
358 }
359 }
360
361 match bib_errs {
362 Some(bib_errs) if biblatex >= yaml => {
363 Err(format_biblatex_error(bib_errs)).within(loaded)
364 }
365 _ => Err(format_yaml_error(haya_err)).within(loaded),
366 }
367 }
368}
369
370fn format_biblatex_error(errors: Vec<BibLaTeXError>) -> LoadError {
372 let Some(error) = errors.into_iter().next() else {
374 return LoadError::new(
376 ReportPos::None,
377 "failed to parse BibLaTeX",
378 "something went wrong",
379 );
380 };
381
382 let (range, msg) = match error {
383 BibLaTeXError::Parse(error) => (error.span, error.kind.to_string()),
384 BibLaTeXError::Type(error) => (error.span, error.kind.to_string()),
385 };
386
387 LoadError::new(range, "failed to parse BibLaTeX", msg)
388}
389
390#[derive(Debug, Clone, PartialEq, Hash)]
392pub struct CslStyle(Arc<ManuallyHash<citationberg::IndependentStyle>>);
393
394impl CslStyle {
395 pub fn load(
397 engine: &mut Engine,
398 Spanned { v: source, span }: Spanned<CslSource>,
399 ) -> SourceResult<Derived<CslSource, Self>> {
400 let style = match &source {
401 CslSource::Named(style, deprecation) => {
402 if let Some(message) = deprecation {
403 engine.sink.warn(warning!(span, "{message}"));
404 }
405 Self::from_archived(*style)
406 }
407 CslSource::Normal(source) => {
408 let loaded = Spanned::new(source, span).load(engine.world)?;
409 Self::from_data(&loaded.data).within(&loaded)?
410 }
411 };
412 Ok(Derived::new(source, style))
413 }
414
415 #[comemo::memoize]
417 pub fn from_archived(archived: ArchivedStyle) -> CslStyle {
418 match archived.get() {
419 citationberg::Style::Independent(style) => Self(Arc::new(ManuallyHash::new(
420 style,
421 typst_utils::hash128(&(TypeId::of::<ArchivedStyle>(), archived)),
422 ))),
423 _ => unreachable!("archive should not contain dependant styles"),
425 }
426 }
427
428 #[comemo::memoize]
430 pub fn from_data(bytes: &Bytes) -> LoadResult<CslStyle> {
431 let text = bytes.as_str()?;
432 citationberg::IndependentStyle::from_xml(text)
433 .map(|style| {
434 Self(Arc::new(ManuallyHash::new(
435 style,
436 typst_utils::hash128(&(TypeId::of::<Bytes>(), bytes)),
437 )))
438 })
439 .map_err(|err| {
440 LoadError::new(ReportPos::None, "failed to load CSL style", err)
441 })
442 }
443
444 pub fn get(&self) -> &citationberg::IndependentStyle {
446 self.0.as_ref()
447 }
448}
449
450#[derive(Debug, Clone, PartialEq, Hash)]
452pub enum CslSource {
453 Named(ArchivedStyle, Option<&'static str>),
455 Normal(DataSource),
457}
458
459impl Reflect for CslSource {
460 #[comemo::memoize]
461 fn input() -> CastInfo {
462 let source = std::iter::once(DataSource::input());
463
464 static ARCHIVED_STYLE_NAMES: LazyLock<Vec<(&&str, &'static str)>> =
466 LazyLock::new(|| {
467 ArchivedStyle::all()
468 .iter()
469 .flat_map(|name| {
470 let (main_name, aliases) = name
471 .names()
472 .split_first()
473 .expect("all ArchivedStyle should have at least one name");
474
475 std::iter::once((main_name, name.display_name())).chain(
476 aliases.iter().map(move |alias| {
477 let docs: &'static str = Box::leak(
479 format!("A short alias of `{main_name}`")
480 .into_boxed_str(),
481 );
482 (alias, docs)
483 }),
484 )
485 })
486 .collect()
487 });
488 let names = ARCHIVED_STYLE_NAMES
489 .iter()
490 .map(|(value, docs)| CastInfo::Value(value.into_value(), docs));
491
492 CastInfo::Union(source.into_iter().chain(names).collect())
493 }
494
495 fn output() -> CastInfo {
496 DataSource::output()
497 }
498
499 fn castable(value: &Value) -> bool {
500 DataSource::castable(value)
501 }
502}
503
504impl FromValue for CslSource {
505 fn from_value(value: Value) -> HintedStrResult<Self> {
506 if EcoString::castable(&value) {
507 let string = EcoString::from_value(value.clone())?;
508 if Path::new(string.as_str()).extension().is_none() {
509 let mut warning = None;
510 if string.as_str() == "chicago-fullnotes" {
511 warning = Some(
512 "style \"chicago-fullnotes\" has been deprecated \
513 in favor of \"chicago-notes\"",
514 );
515 } else if string.as_str() == "modern-humanities-research-association" {
516 warning = Some(
517 "style \"modern-humanities-research-association\" \
518 has been deprecated in favor of \
519 \"modern-humanities-research-association-notes\"",
520 );
521 }
522
523 let style = ArchivedStyle::by_name(&string)
524 .ok_or_else(|| eco_format!("unknown style: {}", string))?;
525 return Ok(CslSource::Named(style, warning));
526 }
527 }
528
529 DataSource::from_value(value).map(CslSource::Normal)
530 }
531}
532
533impl IntoValue for CslSource {
534 fn into_value(self) -> Value {
535 match self {
536 Self::Named(v, _) => v.names().last().unwrap().into_value(),
538 Self::Normal(v) => v.into_value(),
539 }
540 }
541}
542
543pub struct Works {
548 pub citations: FxHashMap<Location, SourceResult<Content>>,
550 pub references: Option<Vec<(Option<Content>, Content, Location)>>,
553 pub hanging_indent: bool,
555}
556
557impl Works {
558 pub fn generate(engine: &Engine) -> StrResult<Arc<Works>> {
560 Self::generate_impl(engine.routines, engine.world, engine.introspector)
561 }
562
563 #[comemo::memoize]
565 fn generate_impl(
566 routines: &Routines,
567 world: Tracked<dyn World + '_>,
568 introspector: Tracked<Introspector>,
569 ) -> StrResult<Arc<Works>> {
570 let mut generator = Generator::new(routines, world, introspector)?;
571 let rendered = generator.drive();
572 let works = generator.display(&rendered)?;
573 Ok(Arc::new(works))
574 }
575
576 pub fn references<'a>(
579 &'a self,
580 elem: &Packed<BibliographyElem>,
581 styles: StyleChain,
582 ) -> SourceResult<&'a [(Option<Content>, Content, Location)]> {
583 self.references
584 .as_deref()
585 .ok_or_else(|| match elem.style.get_ref(styles).source {
586 CslSource::Named(style, _) => eco_format!(
587 "CSL style \"{}\" is not suitable for bibliographies",
588 style.display_name()
589 ),
590 CslSource::Normal(..) => {
591 "CSL style is not suitable for bibliographies".into()
592 }
593 })
594 .at(elem.span())
595 }
596}
597
598struct Generator<'a> {
600 routines: &'a Routines,
602 world: Tracked<'a, dyn World + 'a>,
604 bibliography: Packed<BibliographyElem>,
606 groups: EcoVec<Content>,
608 infos: Vec<GroupInfo>,
611 failures: FxHashMap<Location, SourceResult<Content>>,
613}
614
615struct GroupInfo {
619 location: Location,
621 span: Span,
623 footnote: bool,
625 subinfos: SmallVec<[CiteInfo; 1]>,
627}
628
629struct CiteInfo {
631 key: Label,
633 supplement: Option<Content>,
635 hidden: bool,
637}
638
639impl<'a> Generator<'a> {
640 fn new(
642 routines: &'a Routines,
643 world: Tracked<'a, dyn World + 'a>,
644 introspector: Tracked<Introspector>,
645 ) -> StrResult<Self> {
646 let bibliography = BibliographyElem::find(introspector)?;
647 let groups = introspector.query(&CiteGroup::ELEM.select());
648 let infos = Vec::with_capacity(groups.len());
649 Ok(Self {
650 routines,
651 world,
652 bibliography,
653 groups,
654 infos,
655 failures: FxHashMap::default(),
656 })
657 }
658
659 fn drive(&mut self) -> hayagriva::Rendered {
661 static LOCALES: LazyLock<Vec<citationberg::Locale>> =
662 LazyLock::new(hayagriva::archive::locales);
663
664 let database = &self.bibliography.sources.derived;
665 let bibliography_style =
666 &self.bibliography.style.get_ref(StyleChain::default()).derived;
667
668 let mut driver = BibliographyDriver::new();
670 for elem in &self.groups {
671 let group = elem.to_packed::<CiteGroup>().unwrap();
672 let location = elem.location().unwrap();
673 let children = &group.children;
674
675 let Some(first) = children.first() else { continue };
677
678 let mut subinfos = SmallVec::with_capacity(children.len());
679 let mut items = Vec::with_capacity(children.len());
680 let mut errors = EcoVec::new();
681 let mut normal = true;
682
683 for child in children {
685 let Some(entry) = database.get(child.key) else {
686 errors.push(error!(
687 child.span(),
688 "key `{}` does not exist in the bibliography",
689 child.key.resolve()
690 ));
691 continue;
692 };
693
694 let supplement = child.supplement.get_cloned(StyleChain::default());
695 let locator = supplement.as_ref().map(|c| {
696 SpecificLocator(
697 citationberg::taxonomy::Locator::Custom,
698 hayagriva::LocatorPayload::Transparent(TransparentLocator::new(
699 c.clone(),
700 )),
701 )
702 });
703
704 let mut hidden = false;
705 let special_form = match child.form.get(StyleChain::default()) {
706 None => {
707 hidden = true;
708 None
709 }
710 Some(CitationForm::Normal) => None,
711 Some(CitationForm::Prose) => Some(hayagriva::CitePurpose::Prose),
712 Some(CitationForm::Full) => Some(hayagriva::CitePurpose::Full),
713 Some(CitationForm::Author) => Some(hayagriva::CitePurpose::Author),
714 Some(CitationForm::Year) => Some(hayagriva::CitePurpose::Year),
715 };
716
717 normal &= special_form.is_none();
718 subinfos.push(CiteInfo { key: child.key, supplement, hidden });
719 items.push(CitationItem::new(entry, locator, None, hidden, special_form));
720 }
721
722 if !errors.is_empty() {
723 self.failures.insert(location, Err(errors));
724 continue;
725 }
726
727 let style = match first.style.get_ref(StyleChain::default()) {
728 Smart::Auto => bibliography_style.get(),
729 Smart::Custom(style) => style.derived.get(),
730 };
731
732 self.infos.push(GroupInfo {
733 location,
734 subinfos,
735 span: first.span(),
736 footnote: normal
737 && style.settings.class == citationberg::StyleClass::Note,
738 });
739
740 driver.citation(CitationRequest::new(
741 items,
742 style,
743 Some(locale(first.lang.unwrap_or(Lang::ENGLISH), first.region.flatten())),
744 &LOCALES,
745 None,
746 ));
747 }
748
749 let locale = locale(
750 self.bibliography.lang.unwrap_or(Lang::ENGLISH),
751 self.bibliography.region.flatten(),
752 );
753
754 if self.bibliography.full.get(StyleChain::default()) {
757 for (_, entry) in database.iter() {
758 driver.citation(CitationRequest::new(
759 vec![CitationItem::new(entry, None, None, true, None)],
760 bibliography_style.get(),
761 Some(locale.clone()),
762 &LOCALES,
763 None,
764 ));
765 }
766 }
767
768 driver.finish(BibliographyRequest {
769 style: bibliography_style.get(),
770 locale: Some(locale),
771 locale_files: &LOCALES,
772 })
773 }
774
775 fn display(&mut self, rendered: &hayagriva::Rendered) -> StrResult<Works> {
777 let citations = self.display_citations(rendered)?;
778 let references = self.display_references(rendered)?;
779 let hanging_indent =
780 rendered.bibliography.as_ref().is_some_and(|b| b.hanging_indent);
781 Ok(Works { citations, references, hanging_indent })
782 }
783
784 fn display_citations(
786 &mut self,
787 rendered: &hayagriva::Rendered,
788 ) -> StrResult<FxHashMap<Location, SourceResult<Content>>> {
789 let mut links = FxHashMap::default();
792 if let Some(bibliography) = &rendered.bibliography {
793 let location = self.bibliography.location().unwrap();
794 for (k, item) in bibliography.items.iter().enumerate() {
795 links.insert(item.key.as_str(), location.variant(k + 1));
796 }
797 }
798
799 let mut output = std::mem::take(&mut self.failures);
800 for (info, citation) in self.infos.iter().zip(&rendered.citations) {
801 let supplement = |i: usize| info.subinfos.get(i)?.supplement.clone();
802 let link = |i: usize| {
803 links.get(info.subinfos.get(i)?.key.resolve().as_str()).copied()
804 };
805
806 let renderer = ElemRenderer {
807 routines: self.routines,
808 world: self.world,
809 span: info.span,
810 supplement: &supplement,
811 link: &link,
812 };
813
814 let content = if info.subinfos.iter().all(|sub| sub.hidden) {
815 Content::empty()
816 } else {
817 let mut content =
818 renderer.display_elem_children(&citation.citation, None, true)?;
819
820 if info.footnote {
821 content = FootnoteElem::with_content(content).pack();
822 }
823
824 content
825 };
826
827 output.insert(info.location, Ok(content));
828 }
829
830 Ok(output)
831 }
832
833 #[allow(clippy::type_complexity)]
835 fn display_references(
836 &self,
837 rendered: &hayagriva::Rendered,
838 ) -> StrResult<Option<Vec<(Option<Content>, Content, Location)>>> {
839 let Some(rendered) = &rendered.bibliography else { return Ok(None) };
840
841 let mut first_occurrences = FxHashMap::default();
844 for info in &self.infos {
845 for subinfo in &info.subinfos {
846 let key = subinfo.key.resolve();
847 first_occurrences.entry(key).or_insert(info.location);
848 }
849 }
850
851 let location = self.bibliography.location().unwrap();
853
854 let mut output = vec![];
855 for (k, item) in rendered.items.iter().enumerate() {
856 let renderer = ElemRenderer {
857 routines: self.routines,
858 world: self.world,
859 span: self.bibliography.span(),
860 supplement: &|_| None,
861 link: &|_| None,
862 };
863
864 let backlink = location.variant(k + 1);
868
869 let mut prefix = item
871 .first_field
872 .as_ref()
873 .map(|elem| renderer.display_elem_child(elem, None, false))
874 .transpose()?;
875
876 let reference = renderer.display_elem_children(
878 &item.content,
879 Some(&mut prefix),
880 false,
881 )?;
882
883 let prefix = prefix.map(|content| {
884 if let Some(location) = first_occurrences.get(item.key.as_str()) {
885 let alt = content.plain_text();
886 let body = content.spanned(self.bibliography.span());
887 DirectLinkElem::new(*location, body, Some(alt)).pack()
888 } else {
889 content
890 }
891 });
892
893 output.push((prefix, reference, backlink));
894 }
895
896 Ok(Some(output))
897 }
898}
899
900struct ElemRenderer<'a> {
902 routines: &'a Routines,
904 world: Tracked<'a, dyn World + 'a>,
906 span: Span,
908 supplement: &'a dyn Fn(usize) -> Option<Content>,
910 link: &'a dyn Fn(usize) -> Option<Location>,
912}
913
914impl ElemRenderer<'_> {
915 fn display_elem_children(
924 &self,
925 elems: &hayagriva::ElemChildren,
926 mut prefix: Option<&mut Option<Content>>,
927 is_citation: bool,
928 ) -> StrResult<Content> {
929 Ok(Content::sequence(
930 elems
931 .0
932 .iter()
933 .enumerate()
934 .map(|(i, elem)| {
935 self.display_elem_child(
936 elem,
937 prefix.as_deref_mut(),
938 is_citation && i == 0,
939 )
940 })
941 .collect::<StrResult<Vec<_>>>()?,
942 ))
943 }
944
945 fn display_elem_child(
947 &self,
948 elem: &hayagriva::ElemChild,
949 prefix: Option<&mut Option<Content>>,
950 trim_start: bool,
951 ) -> StrResult<Content> {
952 Ok(match elem {
953 hayagriva::ElemChild::Text(formatted) => {
954 self.display_formatted(formatted, trim_start)
955 }
956 hayagriva::ElemChild::Elem(elem) => self.display_elem(elem, prefix)?,
957 hayagriva::ElemChild::Markup(markup) => self.display_math(markup),
958 hayagriva::ElemChild::Link { text, url } => self.display_link(text, url)?,
959 hayagriva::ElemChild::Transparent { cite_idx, format } => {
960 self.display_transparent(*cite_idx, format)
961 }
962 })
963 }
964
965 fn display_elem(
967 &self,
968 elem: &hayagriva::Elem,
969 mut prefix: Option<&mut Option<Content>>,
970 ) -> StrResult<Content> {
971 use citationberg::Display;
972
973 let block_level = matches!(elem.display, Some(Display::Block | Display::Indent));
974
975 let mut content = self.display_elem_children(
976 &elem.children,
977 if block_level { None } else { prefix.as_deref_mut() },
978 false,
979 )?;
980
981 match elem.display {
982 Some(Display::Block) => {
983 content = BlockElem::new()
984 .with_body(Some(BlockBody::Content(content)))
985 .pack()
986 .spanned(self.span);
987 }
988 Some(Display::Indent) => {
989 content = CslIndentElem::new(content).pack().spanned(self.span);
990 }
991 Some(Display::LeftMargin) => {
992 if let Some(prefix) = prefix {
998 *prefix.get_or_insert_with(Default::default) += content;
999 return Ok(Content::empty());
1000 }
1001 }
1002 _ => {}
1003 }
1004
1005 content = content.spanned(self.span);
1006
1007 if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta
1008 && let Some(location) = (self.link)(i)
1009 {
1010 let alt = content.plain_text();
1011 content = DirectLinkElem::new(location, content, Some(alt)).pack();
1012 }
1013
1014 Ok(content)
1015 }
1016
1017 fn display_math(&self, math: &str) -> Content {
1019 (self.routines.eval_string)(
1020 self.routines,
1021 self.world,
1022 Sink::new().track_mut(),
1024 math,
1025 self.span,
1026 SyntaxMode::Math,
1027 Scope::new(),
1028 )
1029 .map(Value::display)
1030 .unwrap_or_else(|_| TextElem::packed(math).spanned(self.span))
1031 }
1032
1033 fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> StrResult<Content> {
1035 let dest = Destination::Url(Url::new(url)?);
1036 Ok(LinkElem::new(dest.into(), self.display_formatted(text, false))
1037 .pack()
1038 .spanned(self.span))
1039 }
1040
1041 fn display_transparent(&self, i: usize, format: &hayagriva::Formatting) -> Content {
1043 let content = (self.supplement)(i).unwrap_or_default();
1044 apply_formatting(content, format)
1045 }
1046
1047 fn display_formatted(
1049 &self,
1050 formatted: &hayagriva::Formatted,
1051 trim_start: bool,
1052 ) -> Content {
1053 let formatted_text = if trim_start {
1054 formatted.text.trim_start()
1055 } else {
1056 formatted.text.as_str()
1057 };
1058
1059 let content = TextElem::packed(formatted_text).spanned(self.span);
1060 apply_formatting(content, &formatted.formatting)
1061 }
1062}
1063
1064fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Content {
1066 match format.font_style {
1067 citationberg::FontStyle::Normal => {}
1068 citationberg::FontStyle::Italic => {
1069 content = content.emph();
1070 }
1071 }
1072
1073 match format.font_variant {
1074 citationberg::FontVariant::Normal => {}
1075 citationberg::FontVariant::SmallCaps => {
1076 content = SmallcapsElem::new(content).pack();
1077 }
1078 }
1079
1080 match format.font_weight {
1081 citationberg::FontWeight::Normal => {}
1082 citationberg::FontWeight::Bold => {
1083 content = content.strong();
1084 }
1085 citationberg::FontWeight::Light => {
1086 content = CslLightElem::new(content).pack();
1090 }
1091 }
1092
1093 match format.text_decoration {
1094 citationberg::TextDecoration::None => {}
1095 citationberg::TextDecoration::Underline => {
1096 content = content.underlined();
1097 }
1098 }
1099
1100 let span = content.span();
1101 match format.vertical_align {
1102 citationberg::VerticalAlign::None => {}
1103 citationberg::VerticalAlign::Baseline => {}
1104 citationberg::VerticalAlign::Sup => {
1105 content =
1107 HElem::hole().clone() + SuperElem::new(content).pack().spanned(span);
1108 }
1109 citationberg::VerticalAlign::Sub => {
1110 content = HElem::hole().clone() + SubElem::new(content).pack().spanned(span);
1111 }
1112 }
1113
1114 content
1115}
1116
1117fn locale(lang: Lang, region: Option<Region>) -> citationberg::LocaleCode {
1119 let mut value = String::with_capacity(5);
1120 value.push_str(lang.as_str());
1121 if let Some(region) = region {
1122 value.push('-');
1123 value.push_str(region.as_str())
1124 }
1125 citationberg::LocaleCode(value)
1126}
1127
1128#[elem]
1136pub struct CslLightElem {
1137 #[required]
1138 pub body: Content,
1139}
1140
1141#[elem]
1148pub struct CslIndentElem {
1149 #[required]
1150 pub body: Content,
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155 use super::*;
1156
1157 #[test]
1158 fn test_bibliography_load_builtin_styles() {
1159 for &archived in ArchivedStyle::all() {
1160 let _ = CslStyle::from_archived(archived);
1161 }
1162 }
1163
1164 #[test]
1165 fn test_csl_source_cast_info_include_all_names() {
1166 let CastInfo::Union(cast_info) = CslSource::input() else {
1167 panic!("the cast info of CslSource should be a union");
1168 };
1169
1170 let missing: Vec<_> = ArchivedStyle::all()
1171 .iter()
1172 .flat_map(|style| style.names())
1173 .filter(|name| {
1174 let found = cast_info.iter().any(|info| match info {
1175 CastInfo::Value(Value::Str(n), _) => n.as_str() == **name,
1176 _ => false,
1177 });
1178 !found
1179 })
1180 .collect();
1181
1182 assert!(
1183 missing.is_empty(),
1184 "missing style names in CslSource cast info: '{missing:?}'"
1185 );
1186 }
1187}