use std::any::TypeId;
use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::{Arc, LazyLock};
use comemo::{Track, Tracked, TrackedMut};
use ecow::{EcoString, EcoVec, eco_format, eco_vec};
use hayagriva::archive::ArchivedStyle;
use hayagriva::io::BibLaTeXError;
use hayagriva::{
BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, Library,
SpecificLocator, TransparentLocator, citationberg,
};
use indexmap::IndexMap;
use rustc_hash::{FxBuildHasher, FxHashMap};
use smallvec::SmallVec;
use typst_syntax::{Span, Spanned, SyntaxMode};
use typst_utils::{
LazyHash, ManuallyHash, NonZeroExt, PicoStr, Protected, ResolvedPicoStr,
};
use crate::World;
use crate::diag::{
At, HintedStrResult, HintedString, LoadError, LoadResult, LoadedWithin,
ReportTextPos, SourceDiagnostic, SourceResult, StrResult, bail, error, warning,
};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{
Bytes, CastInfo, Content, Context, Derived, FromValue, IntoValue, Label,
LocatableSelector, NativeElement, OneOrMultiple, Packed, Reflect, Repr, Scope,
Selector, ShowSet, Smart, StyleChain, Styles, Synthesize, Value, elem,
};
use crate::introspection::{
EmptyIntrospector, History, Introspect, Introspector, Locatable, Location,
QueryIntrospection,
};
use crate::layout::{BlockElem, Em, HElem, PadElem};
use crate::loading::{DataSource, Load, LoadSource, Loaded, format_yaml_error};
use crate::model::{
CitationForm, CiteElem, CiteGroup, Destination, DirectLinkElem, FootnoteElem,
HeadingElem, LinkElem, Url,
};
use crate::routines::SpanMode;
use crate::text::{Lang, LocalName, Region, SmallcapsElem, SubElem, SuperElem, TextElem};
#[elem(Locatable, Synthesize, ShowSet, LocalName)]
pub struct BibliographyElem {
#[required]
#[parse(
let sources = args.expect("sources")?;
Bibliography::load(engine.world, sources)?
)]
pub sources: Derived<OneOrMultiple<DataSource>, Bibliography>,
pub title: Smart<Option<Content>>,
#[default(false)]
pub full: bool,
#[parse(match args.named::<Spanned<CslSource>>("style")? {
Some(source) => Some(CslStyle::load(engine, source)?),
None => None,
})]
#[default({
let default = ArchivedStyle::InstituteOfElectricalAndElectronicsEngineers;
Derived::new(CslSource::Named(default, None), CslStyle::from_archived(default))
})]
pub style: Derived<CslSource, CslStyle>,
pub target: Smart<LocatableSelector>,
#[default(Some(Smart::Auto))]
pub group: Option<Smart<EcoString>>,
#[internal]
#[synthesized]
pub lang: Lang,
#[internal]
#[synthesized]
pub region: Option<Region>,
}
impl BibliographyElem {
pub fn has(engine: &mut Engine, key: Label, span: Span) -> bool {
engine
.introspect(QueryIntrospection(Self::ELEM.select(), span))
.iter()
.any(|elem| elem.to_packed::<Self>().unwrap().sources.derived.has(key))
}
pub fn keys(
introspector: Tracked<dyn Introspector + '_>,
) -> Vec<(Label, Option<EcoString>)> {
let mut vec = vec![];
for elem in introspector.query(&Self::ELEM.select()).iter() {
let this = elem.to_packed::<Self>().unwrap();
for (key, entry) in this.sources.derived.iter() {
let detail = entry.title().map(|title| title.value.to_str().into());
vec.push((key, detail))
}
}
vec
}
}
impl Packed<BibliographyElem> {
pub fn realize_title(&self, styles: StyleChain) -> Option<Content> {
self.title
.get_cloned(styles)
.unwrap_or_else(|| {
Some(TextElem::packed(Packed::<BibliographyElem>::local_name_in(styles)))
})
.map(|title| {
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(self.span())
})
}
}
impl Synthesize for Packed<BibliographyElem> {
fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
let elem = self.as_mut();
elem.lang = Some(styles.get(TextElem::lang));
elem.region = Some(styles.get(TextElem::region));
Ok(())
}
}
impl ShowSet for Packed<BibliographyElem> {
fn show_set(&self, _: StyleChain) -> Styles {
const INDENT: Em = Em::new(1.0);
let mut out = Styles::new();
out.set(HeadingElem::numbering, None);
out.set(PadElem::left, INDENT.into());
out
}
}
impl LocalName for Packed<BibliographyElem> {
const KEY: &'static str = "bibliography";
}
#[derive(Clone, PartialEq, Hash)]
pub struct Bibliography(
Arc<ManuallyHash<IndexMap<Label, hayagriva::Entry, FxBuildHasher>>>,
);
impl Bibliography {
fn load(
world: Tracked<dyn World + '_>,
sources: Spanned<OneOrMultiple<DataSource>>,
) -> SourceResult<Derived<OneOrMultiple<DataSource>, Self>> {
let loaded = sources.load(world)?;
let bibliography = Self::decode(&loaded)?;
Ok(Derived::new(sources.v, bibliography))
}
#[comemo::memoize]
#[typst_macros::time(name = "load bibliography")]
fn decode(data: &[Loaded]) -> SourceResult<Bibliography> {
let mut map = IndexMap::default();
let mut duplicates = Vec::<EcoString>::new();
for d in data.iter() {
let library = decode_library(d)?;
for entry in library {
let label = Label::new(PicoStr::intern(entry.key()))
.ok_or("bibliography contains entry with empty key")
.at(d.source.span)?;
match map.entry(label) {
indexmap::map::Entry::Vacant(vacant) => {
vacant.insert(entry);
}
indexmap::map::Entry::Occupied(_) => {
duplicates.push(entry.key().into());
}
}
}
}
if !duplicates.is_empty() {
let span = data.first().unwrap().source.span;
bail!(span, "duplicate bibliography keys: {}", duplicates.join(", "));
}
Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data)))))
}
fn has(&self, key: Label) -> bool {
self.0.contains_key(&key)
}
fn get(&self, key: Label) -> Option<&hayagriva::Entry> {
self.0.get(&key)
}
fn iter(&self) -> impl Iterator<Item = (Label, &hayagriva::Entry)> {
self.0.iter().map(|(&k, v)| (k, v))
}
}
impl Debug for Bibliography {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_set().entries(self.0.keys()).finish()
}
}
fn decode_library(loaded: &Loaded) -> SourceResult<Library> {
let data = loaded.data.as_str().within(loaded)?;
if let LoadSource::Path(file_id) = loaded.source.v {
let ext = file_id.vpath().extension().unwrap_or_default();
match ext.to_lowercase().as_str() {
"yml" | "yaml" => hayagriva::io::from_yaml_str(data)
.map_err(format_yaml_error)
.within(loaded),
"bib" => hayagriva::io::from_biblatex_str(data)
.map_err(format_biblatex_error)
.within(loaded),
_ => bail!(
loaded.source.span,
"unknown bibliography format (must be .yaml/.yml or .bib)"
),
}
} else {
let haya_err = match hayagriva::io::from_yaml_str(data) {
Ok(library) => return Ok(library),
Err(err) => err,
};
let bib_errs = match hayagriva::io::from_biblatex_str(data) {
Ok(library) if !library.is_empty() => return Ok(library),
Ok(_) => None,
Err(err) => Some(err),
};
let mut yaml = 0;
let mut biblatex = 0;
for c in data.chars() {
match c {
':' => yaml += 1,
'{' => biblatex += 1,
_ => {}
}
}
match bib_errs {
Some(bib_errs) if biblatex >= yaml => {
Err(format_biblatex_error(bib_errs)).within(loaded)
}
_ => Err(format_yaml_error(haya_err)).within(loaded),
}
}
}
fn format_biblatex_error(errors: Vec<BibLaTeXError>) -> LoadError {
let Some(error) = errors.into_iter().next() else {
return LoadError::text(
ReportTextPos::None,
"failed to parse BibLaTeX",
"something went wrong",
);
};
let (range, msg) = match error {
BibLaTeXError::Parse(error) => (error.span, error.kind.to_string()),
BibLaTeXError::Type(error) => (error.span, error.kind.to_string()),
};
LoadError::text(range, "failed to parse BibLaTeX", msg)
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct CslStyle(Arc<ManuallyHash<citationberg::IndependentStyle>>);
impl CslStyle {
pub fn load(
engine: &mut Engine,
Spanned { v: source, span }: Spanned<CslSource>,
) -> SourceResult<Derived<CslSource, Self>> {
let style = match &source {
CslSource::Named(style, deprecation) => {
if let Some(message) = deprecation {
engine.sink.warn(SourceDiagnostic::warning(span, message.clone()));
}
Self::from_archived(*style)
}
CslSource::Normal(source) => {
let loaded = Spanned::new(source, span).load(engine.world)?;
Self::from_data(&loaded.data).within(&loaded)?
}
};
Ok(Derived::new(source, style))
}
#[comemo::memoize]
pub fn from_archived(archived: ArchivedStyle) -> CslStyle {
match archived.get() {
citationberg::Style::Independent(style) => Self(Arc::new(ManuallyHash::new(
style,
typst_utils::hash128(&(TypeId::of::<ArchivedStyle>(), archived)),
))),
_ => unreachable!("archive should not contain dependent styles"),
}
}
#[comemo::memoize]
pub fn from_data(bytes: &Bytes) -> LoadResult<CslStyle> {
let text = bytes.as_str()?;
citationberg::IndependentStyle::from_xml(text)
.map(|style| {
Self(Arc::new(ManuallyHash::new(
style,
typst_utils::hash128(&(TypeId::of::<Bytes>(), bytes)),
)))
})
.map_err(|err| {
LoadError::text(ReportTextPos::None, "failed to load CSL style", err)
})
}
pub fn get(&self) -> &citationberg::IndependentStyle {
self.0.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum CslSource {
Named(ArchivedStyle, Option<EcoString>),
Normal(DataSource),
}
impl Reflect for CslSource {
#[comemo::memoize]
fn input() -> CastInfo {
let source = std::iter::once(DataSource::input());
static ARCHIVED_STYLE_NAMES: LazyLock<Vec<(&&str, &'static str)>> =
LazyLock::new(|| {
ArchivedStyle::all()
.iter()
.flat_map(|name| {
let (main_name, aliases) = name
.names()
.split_first()
.expect("all ArchivedStyle should have at least one name");
std::iter::once((main_name, name.display_name())).chain(
aliases.iter().map(move |alias| {
let docs: &'static str = Box::leak(
format!("A short alias of `{main_name}`")
.into_boxed_str(),
);
(alias, docs)
}),
)
})
.collect()
});
let names = ARCHIVED_STYLE_NAMES
.iter()
.map(|(value, docs)| CastInfo::Value(value.into_value(), docs));
CastInfo::Union(source.into_iter().chain(names).collect())
}
fn output() -> CastInfo {
DataSource::output()
}
fn castable(value: &Value) -> bool {
DataSource::castable(value)
}
}
impl FromValue for CslSource {
fn from_value(value: Value) -> HintedStrResult<Self> {
if EcoString::castable(&value) {
let string = EcoString::from_value(value.clone())?;
if Path::new(string.as_str()).extension().is_none() {
let replacement = replacement(&string);
let deprecation = replacement.map(|instead| {
eco_format!(
"style `{}` has been deprecated in favor of `{}`",
string.repr(),
instead.repr(),
)
});
let style = ArchivedStyle::by_name(&string).ok_or_else(|| {
deprecation
.clone()
.unwrap_or_else(|| eco_format!("unknown style: {string}"))
})?;
return Ok(CslSource::Named(style, deprecation));
}
}
DataSource::from_value(value).map(CslSource::Normal)
}
}
impl IntoValue for CslSource {
fn into_value(self) -> Value {
match self {
Self::Named(v, _) => v.names().last().unwrap().into_value(),
Self::Normal(v) => v.into_value(),
}
}
}
fn replacement(style: &str) -> Option<&'static str> {
Some(match style {
"chicago-fullnotes" => "chicago-notes",
"modern-humanities-research-association" => {
"modern-humanities-research-association-notes"
}
"council-of-science-editors" => "cse-citation-sequence-brackets-8th-edition",
"council-of-science-editors-author-date" => "cse-name-year",
"modern-language-association-8" | "mla-8" => "modern-language-association",
"vancouver" => "nlm-citation-sequence",
"vancouver-superscript" => "nlm-citation-sequence-superscript",
_ => return None,
})
}
pub struct Works {
bibliographies: FxHashMap<Location, SourceResult<RenderedBibliography>>,
groups: FxHashMap<Location, SourceResult<Content>>,
}
pub struct RenderedBibliography {
pub entries: Vec<RenderedEntry>,
pub hanging_indent: bool,
}
pub struct RenderedEntry {
pub prefix: Option<Content>,
pub body: Content,
pub backlink: Location,
}
impl Works {
pub fn generate(engine: &mut Engine, span: Span) -> SourceResult<Arc<Works>> {
let bibs_and_groups = engine.introspect(BibliographyIntrospection(span));
Self::generate_impl(
engine.world,
engine.library,
engine.introspector.into_raw(),
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
&bibs_and_groups,
)
.at(span)
}
#[comemo::memoize]
fn generate_impl(
world: Tracked<dyn World + '_>,
library: &LazyHash<crate::Library>,
introspector: Tracked<dyn Introspector + '_>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
bibs_and_groups: &[Content],
) -> StrResult<Arc<Works>> {
let mut engine = Engine {
world,
library,
introspector: Protected::from_raw(introspector),
traced,
sink,
route: Route::extend(route),
};
let p = prepare(&mut engine, bibs_and_groups);
let mut offsets = FxHashMap::default();
let rendered =
p.bibs.iter().map(|bib| render(bib, &mut offsets)).collect::<Vec<_>>();
Ok(Arc::new(Works {
bibliographies: show_bibliographies(world, &p, &rendered),
groups: show_cite_groups(world, p, &rendered),
}))
}
pub fn citation(&self, loc: Location, span: Span) -> SourceResult<Content> {
self.groups
.get(&loc)
.cloned()
.ok_or_else(citation_could_not_be_located)
.at(span)?
}
pub fn bibliography(
&self,
loc: Location,
span: Span,
) -> SourceResult<&RenderedBibliography> {
self.bibliographies
.get(&loc)
.ok_or_else(bibliography_could_not_be_located)
.at(span)?
.as_ref()
.map_err(Clone::clone)
}
}
struct Preparation<'a> {
bibs: Vec<PreparedBibliography<'a>>,
groups: FxHashMap<Location, SourceResult<PreparedCiteGroup<'a>>>,
}
struct PreparedBibliography<'a> {
elem: &'a Packed<BibliographyElem>,
subgroups: Vec<Subgroup<'a>>,
}
struct Subgroup<'a> {
elem: &'a Packed<CiteGroup>,
citations: SmallVec<[&'a Packed<CiteElem>; 1]>,
style: &'a CslStyle,
}
struct PreparedCiteGroup<'a>(SmallVec<[GroupPart<'a>; 1]>);
enum GroupPart<'a> {
Content(&'a Content),
Subgroup(BibIndex, SubgroupIndex),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
struct BibIndex(usize);
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
struct SubgroupIndex(usize);
fn prepare<'a>(engine: &mut Engine, bibs_and_groups: &'a [Content]) -> Preparation<'a> {
let mut selected = FxHashMap::<Location, BibIndex>::default();
let mut bibs = Vec::<PreparedBibliography>::new();
for elem in bibs_and_groups {
let Some(bib) = elem.to_packed::<BibliographyElem>() else { continue };
let idx = BibIndex(bibs.len());
bibs.push(PreparedBibliography { elem: bib, subgroups: vec![] });
if let Smart::Custom(LocatableSelector(selector)) =
bib.target.get_cloned(StyleChain::default())
{
for citation in engine.introspect(QueryIntrospection(selector, bib.span())) {
selected.entry(citation.location().unwrap()).or_insert(idx);
}
}
}
let mut groups = FxHashMap::default();
let mut bib_cursor = 0;
for elem in bibs_and_groups {
let Some(group) = elem.to_packed::<CiteGroup>() else {
debug_assert!(elem.is::<BibliographyElem>());
bib_cursor += 1;
continue;
};
let loc = group.location().unwrap();
let result = prepare_cite_group(&mut bibs, bib_cursor, group, &selected);
groups.insert(loc, result);
}
Preparation { bibs, groups }
}
fn prepare_cite_group<'a>(
bibs: &mut [PreparedBibliography<'a>],
bib_cursor: usize,
group: &'a Packed<CiteGroup>,
selected: &FxHashMap<Location, BibIndex>,
) -> SourceResult<PreparedCiteGroup<'a>> {
let mut parts = SmallVec::new();
let mut subgroup = SmallVec::new();
let mut subgroup_bib = None;
let mut tail = 0;
let mut errors = EcoVec::new();
for (i, child) in group.children.iter().enumerate() {
let Some(citation) = child.to_packed::<CiteElem>() else { continue };
let spaces = &group.children[tail..i];
tail = i + 1;
let bib_idx = if let Some(&idx) = selected.get(&citation.location().unwrap()) {
let bib = &bibs[idx.0];
if !bib.elem.sources.derived.has(citation.key) {
errors.push(key_does_not_exist(citation, bib));
continue;
}
idx
} else if let Some(idx) = select_auto_bib(citation, bibs, bib_cursor) {
idx
} else {
errors.push(uncovered_citation(citation, bibs));
continue;
};
if let Some(subgroup_bib) = subgroup_bib
&& subgroup_bib != bib_idx
{
parts.push(save_subgroup(
bibs,
subgroup_bib,
group,
std::mem::take(&mut subgroup),
));
parts.extend(spaces.iter().map(GroupPart::Content));
}
subgroup_bib = Some(bib_idx);
subgroup.push(citation);
}
if let Some(subgroup_bib) = subgroup_bib {
parts.push(save_subgroup(bibs, subgroup_bib, group, subgroup));
}
if !errors.is_empty() {
return Err(errors);
}
Ok(PreparedCiteGroup(parts))
}
fn save_subgroup<'a>(
bibs: &mut [PreparedBibliography<'a>],
bib_idx: BibIndex,
elem: &'a Packed<CiteGroup>,
citations: SmallVec<[&'a Packed<CiteElem>; 1]>,
) -> GroupPart<'a> {
let bib = &mut bibs[bib_idx.0];
let style = if let Some(first) = citations.first()
&& let Smart::Custom(style) = first.style.get_ref(StyleChain::default())
{
&style.derived
} else {
&bib.elem.style.get_ref(StyleChain::default()).derived
};
let sub_idx = SubgroupIndex(bib.subgroups.len());
bib.subgroups.push(Subgroup { elem, citations, style });
GroupPart::Subgroup(bib_idx, sub_idx)
}
fn select_auto_bib(
citation: &Packed<CiteElem>,
bibs: &[PreparedBibliography],
bib_cursor: usize,
) -> Option<BibIndex> {
let bibs = bibs.iter().enumerate();
let before = bibs.clone().take(bib_cursor);
let after = bibs.skip(bib_cursor);
after
.chain(before.rev())
.find(|(_, bib)| {
bib.elem.target.get_ref(StyleChain::default()).is_auto()
&& bib.elem.sources.derived.has(citation.key)
})
.map(|(idx, _)| BibIndex(idx))
}
fn render<'a>(
bib: &PreparedBibliography<'a>,
offsets: &mut FxHashMap<Smart<&'a str>, usize>,
) -> hayagriva::Rendered {
static LOCALES: LazyLock<Vec<citationberg::Locale>> =
LazyLock::new(hayagriva::archive::locales);
let database = &bib.elem.sources.derived;
let mut driver = BibliographyDriver::new();
let mut offset = bib
.elem
.group
.get_ref(StyleChain::default())
.as_ref()
.map(|group| offsets.entry(group.as_deref()).or_insert(0));
if let Some(offset) = &mut offset {
driver = driver.with_citation_number_offset(**offset);
}
for group in &bib.subgroups {
let items = group
.citations
.iter()
.map(|child| {
let entry = database.get(child.key).expect("entry to be present");
citation_item(entry, child)
})
.collect::<Vec<_>>();
let first = &group.citations[0];
let locale = locale(first.lang.unwrap_or(Lang::ENGLISH), first.region.flatten());
driver.citation(CitationRequest::new(
items,
group.style.get(),
Some(locale),
&LOCALES,
None,
));
}
let bib_style = &bib.elem.style.get_ref(StyleChain::default()).derived;
let locale =
locale(bib.elem.lang.unwrap_or(Lang::ENGLISH), bib.elem.region.flatten());
if bib.elem.full.get(StyleChain::default()) {
for (_, entry) in database.iter() {
driver.citation(CitationRequest::new(
vec![CitationItem::new(entry, None, None, true, None)],
bib_style.get(),
Some(locale.clone()),
&LOCALES,
None,
));
}
}
let rendered = driver.finish(BibliographyRequest {
style: bib_style.get(),
locale: Some(locale),
locale_files: &LOCALES,
});
if let Some(offset) = offset
&& let Some(bib) = &rendered.bibliography
&& (bib.items.iter().any(displays_citation_number)
|| rendered.citations.iter().any(|rendered| {
rendered
.citation
.find_meta(&hayagriva::ElemMeta::CitationNumber)
.is_some()
}))
{
*offset += bib.items.len();
}
rendered
}
fn displays_citation_number(item: &hayagriva::BibliographyItem) -> bool {
item.content.find_meta(&hayagriva::ElemMeta::CitationNumber).is_some()
|| item.first_field.as_ref().is_some_and(|child| match child {
hayagriva::ElemChild::Elem(elem) => {
elem.meta == Some(hayagriva::ElemMeta::CitationNumber)
|| elem
.children
.find_meta(&hayagriva::ElemMeta::CitationNumber)
.is_some()
}
_ => false,
})
}
fn citation_item<'a>(
entry: &'a hayagriva::Entry,
child: &'a Packed<CiteElem>,
) -> CitationItem<'a, hayagriva::Entry> {
let supplement = child.supplement.get_cloned(StyleChain::default());
let locator = supplement.as_ref().map(|c| {
SpecificLocator(
citationberg::taxonomy::Locator::Custom,
hayagriva::LocatorPayload::Transparent(TransparentLocator::new(c.clone())),
)
});
let mut hidden = false;
let special_form = match child.form.get(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),
};
CitationItem::new(entry, locator, None, hidden, special_form)
}
fn show_bibliographies(
world: Tracked<dyn World + '_>,
p: &Preparation,
rendered: &[hayagriva::Rendered],
) -> FxHashMap<Location, SourceResult<RenderedBibliography>> {
p.bibs
.iter()
.zip(rendered)
.map(|(bib, rendered)| {
let loc = bib.elem.location().unwrap();
let result = rendered
.bibliography
.as_ref()
.ok_or_else(|| {
style_unsuitable(
&bib.elem.style.get_ref(StyleChain::default()).source,
)
})
.and_then(|rendered| show_bibliography(world, bib, rendered))
.at(bib.elem.span());
(loc, result)
})
.collect()
}
fn show_bibliography(
world: Tracked<dyn World + '_>,
bib: &PreparedBibliography,
rendered: &hayagriva::RenderedBibliography,
) -> StrResult<RenderedBibliography> {
let to_citations = links_to_citations(&bib.subgroups);
let mut entries = Vec::with_capacity(rendered.items.len());
for (k, item) in rendered.items.iter().enumerate() {
let ctx = ShowCtx {
world,
span: bib.elem.span(),
supplement: &|_| None,
link: &|_| None,
};
let mut prefix = item
.first_field
.as_ref()
.map(|elem| show_elem_child(&ctx, elem, None, false))
.transpose()?;
let body = show_elem_children(&ctx, &item.content, Some(&mut prefix), false)?;
let prefix = prefix.map(|content| {
if let Some(location) = to_citations.get(item.key.as_str()) {
let alt = content.plain_text();
let body = content.spanned(ctx.span);
DirectLinkElem::new(*location, body, Some(alt)).pack()
} else {
content
}
});
entries.push(RenderedEntry { prefix, body, backlink: entry_location(bib, k) });
}
Ok(RenderedBibliography { entries, hanging_indent: rendered.hanging_indent })
}
fn show_cite_groups(
world: Tracked<dyn World + '_>,
p: Preparation,
rendered: &[hayagriva::Rendered],
) -> FxHashMap<Location, SourceResult<Content>> {
let to_entries = links_to_entries(&p, rendered);
p.groups
.into_iter()
.map(|(loc, group)| {
let result = group.and_then(|group| {
show_cite_group(world, &group, &p.bibs, rendered, |idx, key| {
to_entries.get(&(idx, key)).copied()
})
});
(loc, result)
})
.collect()
}
fn show_cite_group(
world: Tracked<dyn World + '_>,
group: &PreparedCiteGroup,
prepared: &[PreparedBibliography],
rendered: &[hayagriva::Rendered],
to_entry: impl Fn(BibIndex, &str) -> Option<Location>,
) -> SourceResult<Content> {
let mut seq = vec![];
for part in &group.0 {
seq.push(match part {
GroupPart::Content(c) => (**c).clone(),
GroupPart::Subgroup(bib_idx, sub_idx) => {
let subgroup = &prepared[bib_idx.0].subgroups[sub_idx.0];
let item = &rendered[bib_idx.0].citations[sub_idx.0];
show_subgroup(world, subgroup, item, |key| to_entry(*bib_idx, key))?
}
});
}
Ok(Content::sequence(seq))
}
fn show_subgroup(
world: Tracked<dyn World + '_>,
group: &Subgroup,
citation: &hayagriva::RenderedCitation,
to_entry: impl Fn(&str) -> Option<Location>,
) -> SourceResult<Content> {
if group
.citations
.iter()
.all(|sub| sub.form.get(StyleChain::default()).is_none())
{
return Ok(Content::empty());
}
let span = Span::find(group.citations.iter().map(|elem| elem.span()));
let supplement =
|i: usize| group.citations.get(i)?.supplement.get_cloned(StyleChain::default());
let link = |i: usize| to_entry(group.citations.get(i)?.key.resolve().as_str());
let ctx = ShowCtx { world, span, supplement: &supplement, link: &link };
let mut realized =
show_elem_children(&ctx, &citation.citation, None, true).at(span)?;
if group.style.get().settings.class == citationberg::StyleClass::Note
&& group.citations.iter().all(|sub| {
matches!(
sub.form.get(StyleChain::default()),
None | Some(CitationForm::Normal)
)
})
{
realized = FootnoteElem::with_content(realized).pack();
}
Ok(realized)
}
fn links_to_citations(groups: &[Subgroup]) -> FxHashMap<ResolvedPicoStr, Location> {
let mut map = FxHashMap::default();
for group in groups {
for child in &group.citations {
let key = child.key.resolve();
map.entry(key).or_insert(group.elem.location().unwrap());
}
}
map
}
fn links_to_entries<'a>(
p: &Preparation,
rendered: &'a [hayagriva::Rendered],
) -> FxHashMap<(BibIndex, &'a str), Location> {
let mut links = FxHashMap::default();
for (i, (bib, rendered)) in p.bibs.iter().zip(rendered).enumerate() {
let Some(rendered) = &rendered.bibliography else { continue };
for (k, item) in rendered.items.iter().enumerate() {
links.insert((BibIndex(i), item.key.as_str()), entry_location(bib, k));
}
}
links
}
fn entry_location(bib: &PreparedBibliography, k: usize) -> Location {
bib.elem.location().unwrap().variant(k + 1)
}
struct ShowCtx<'a> {
world: Tracked<'a, dyn World + 'a>,
span: Span,
supplement: &'a dyn Fn(usize) -> Option<Content>,
link: &'a dyn Fn(usize) -> Option<Location>,
}
fn show_elem_children(
ctx: &ShowCtx,
elems: &hayagriva::ElemChildren,
mut prefix: Option<&mut Option<Content>>,
is_citation: bool,
) -> StrResult<Content> {
Ok(Content::sequence(
elems
.0
.iter()
.enumerate()
.map(|(i, elem)| {
show_elem_child(ctx, elem, prefix.as_deref_mut(), is_citation && i == 0)
})
.collect::<StrResult<Vec<_>>>()?,
))
}
fn show_elem_child(
ctx: &ShowCtx,
elem: &hayagriva::ElemChild,
prefix: Option<&mut Option<Content>>,
trim_start: bool,
) -> StrResult<Content> {
Ok(match elem {
hayagriva::ElemChild::Text(formatted) => {
show_formatted(ctx, formatted, trim_start)
}
hayagriva::ElemChild::Elem(elem) => show_elem(ctx, elem, prefix)?,
hayagriva::ElemChild::Markup(markup) => show_math(ctx, markup),
hayagriva::ElemChild::Link { text, url } => show_link(ctx, text, url)?,
hayagriva::ElemChild::Transparent { cite_idx, format } => {
show_transparent(ctx, *cite_idx, format)
}
})
}
fn show_elem(
ctx: &ShowCtx,
elem: &hayagriva::Elem,
mut prefix: Option<&mut Option<Content>>,
) -> StrResult<Content> {
use citationberg::Display;
let block_level = matches!(elem.display, Some(Display::Block | Display::Indent));
let mut content = show_elem_children(
ctx,
&elem.children,
if block_level { None } else { prefix.as_deref_mut() },
false,
)?;
match elem.display {
Some(Display::Block) => {
content = BlockElem::packed(content).spanned(ctx.span);
}
Some(Display::Indent) => {
content = CslIndentElem::new(content).pack().spanned(ctx.span);
}
Some(Display::LeftMargin) => {
if let Some(prefix) = prefix {
*prefix.get_or_insert_with(Default::default) += content;
return Ok(Content::empty());
}
}
_ => {}
}
content = content.spanned(ctx.span);
if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta
&& let Some(location) = (ctx.link)(i)
{
let alt = content.plain_text();
content = DirectLinkElem::new(location, content, Some(alt)).pack();
}
Ok(content)
}
fn show_math(ctx: &ShowCtx, math: &str) -> Content {
let library = ctx.world.library();
(library.routines.eval_string)(
ctx.world,
library,
Sink::new().track_mut(),
EmptyIntrospector.track(),
Context::none().track(),
math,
SpanMode::Uniform(ctx.span),
SyntaxMode::Math,
Scope::new(),
)
.map(Value::display)
.unwrap_or_else(|_| TextElem::packed(math).spanned(ctx.span))
}
fn show_link(
ctx: &ShowCtx,
text: &hayagriva::Formatted,
url: &str,
) -> StrResult<Content> {
let dest = Destination::Url(Url::new(url)?);
Ok(LinkElem::new(dest.into(), show_formatted(ctx, text, false))
.pack()
.spanned(ctx.span))
}
fn show_transparent(ctx: &ShowCtx, i: usize, format: &hayagriva::Formatting) -> Content {
let content = (ctx.supplement)(i).unwrap_or_default();
show_with_formatting(content, format)
}
fn show_formatted(
ctx: &ShowCtx,
formatted: &hayagriva::Formatted,
trim_start: bool,
) -> Content {
let formatted_text =
if trim_start { formatted.text.trim_start() } else { formatted.text.as_str() };
let content = TextElem::packed(formatted_text).spanned(ctx.span);
show_with_formatting(content, &formatted.formatting)
}
fn show_with_formatting(mut content: Content, format: &hayagriva::Formatting) -> Content {
match format.font_style {
citationberg::FontStyle::Normal => {}
citationberg::FontStyle::Italic => {
content = content.emph();
}
}
match format.font_variant {
citationberg::FontVariant::Normal => {}
citationberg::FontVariant::SmallCaps => {
content = SmallcapsElem::new(content).pack();
}
}
match format.font_weight {
citationberg::FontWeight::Normal => {}
citationberg::FontWeight::Bold => {
content = content.strong();
}
citationberg::FontWeight::Light => {
content = CslLightElem::new(content).pack();
}
}
match format.text_decoration {
citationberg::TextDecoration::None => {}
citationberg::TextDecoration::Underline => {
content = content.underlined();
}
}
let span = content.span();
match format.vertical_align {
citationberg::VerticalAlign::None => {}
citationberg::VerticalAlign::Baseline => {}
citationberg::VerticalAlign::Sup => {
content =
HElem::hole().clone() + SuperElem::new(content).pack().spanned(span);
}
citationberg::VerticalAlign::Sub => {
content = HElem::hole().clone() + SubElem::new(content).pack().spanned(span);
}
}
content
}
fn locale(lang: Lang, region: Option<Region>) -> citationberg::LocaleCode {
let mut value = String::with_capacity(5);
value.push_str(lang.as_str());
if let Some(region) = region {
value.push('-');
value.push_str(region.as_str())
}
citationberg::LocaleCode(value)
}
#[elem]
pub struct CslLightElem {
#[required]
pub body: Content,
}
#[elem]
pub struct CslIndentElem {
#[required]
pub body: Content,
}
#[derive(Debug, Clone, PartialEq, Hash)]
struct BibliographyIntrospection(Span);
impl Introspect for BibliographyIntrospection {
type Output = EcoVec<Content>;
fn introspect(
&self,
_: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> Self::Output {
introspector.query(&Selector::Or(eco_vec![
BibliographyElem::ELEM.select(),
CiteGroup::ELEM.select(),
]))
}
fn diagnose(&self, _: &History<Self::Output>) -> SourceDiagnostic {
warning!(self.0, "citations and bibliographies did not stabilize")
}
}
fn citation_could_not_be_located() -> HintedString {
error!(
"citation could not be located";
hint: "this citation is not stably present in the document";
hint: "this can be caused by measurement or introspection";
)
}
fn bibliography_could_not_be_located() -> HintedString {
error!(
"bibliography could not be located";
hint: "this bibliography is not stably present in the document";
hint: "this can be caused by measurement or introspection";
)
}
fn uncovered_citation(
citation: &Packed<CiteElem>,
bibs: &[PreparedBibliography],
) -> SourceDiagnostic {
let span = citation.span();
let key = citation.key.resolve();
if bibs.is_empty() {
error!(span, "the document does not contain a bibliography")
} else if let Some(bib) =
bibs.iter().find(|bib| bib.elem.sources.derived.has(citation.key))
{
error!(
span,
"citation is not covered by any bibliography";
hint[bib.elem.span()]:
"a bibliography containing the key `{key}` exists, \
but its `target` excludes this citation";
)
} else {
error!(
span,
"citation key `{key}` is not present in {} bibliography",
if bibs.len() == 1 { "the" } else { "any" },
)
}
}
fn key_does_not_exist(
citation: &Packed<CiteElem>,
bib: &PreparedBibliography,
) -> SourceDiagnostic {
error!(
citation.span(),
"key `{}` does not exist in the bibliography",
citation.key.resolve();
hint[bib.elem.span()]: "the citation was assigned to this bibliography";
)
}
#[cold]
fn style_unsuitable(source: &CslSource) -> EcoString {
match source {
CslSource::Named(style, _) => eco_format!(
"CSL style \"{}\" is not suitable for bibliographies",
style.display_name()
),
CslSource::Normal(..) => "CSL style is not suitable for bibliographies".into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bibliography_load_builtin_styles() {
for &archived in ArchivedStyle::all() {
let _ = CslStyle::from_archived(archived);
}
}
#[test]
fn test_csl_source_cast_info_include_all_names() {
let CastInfo::Union(cast_info) = CslSource::input() else {
panic!("the cast info of CslSource should be a union");
};
let missing: Vec<_> = ArchivedStyle::all()
.iter()
.flat_map(|style| style.names())
.filter(|name| {
let found = cast_info.iter().any(|info| match info {
CastInfo::Value(Value::Str(n), _) => n.as_str() == **name,
_ => false,
});
!found
})
.collect();
assert!(
missing.is_empty(),
"missing style names in CslSource cast info: '{missing:?}'"
);
}
}