use std::any::TypeId;
use std::ffi::OsStr;
use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::{Arc, LazyLock};
use comemo::{Track, Tracked};
use ecow::{EcoString, EcoVec, eco_format};
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::{ManuallyHash, NonZeroExt, PicoStr};
use crate::World;
use crate::diag::{
At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos, SourceResult,
StrResult, bail, error, warning,
};
use crate::engine::{Engine, Sink};
use crate::foundations::{
Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement,
OneOrMultiple, Packed, Reflect, Scope, ShowSet, Smart, StyleChain, Styles,
Synthesize, Value, elem,
};
use crate::introspection::{Introspector, Locatable, Location};
use crate::layout::{BlockBody, BlockElem, Em, HElem, PadElem};
use crate::loading::{DataSource, Load, LoadSource, Loaded, format_yaml_error};
use crate::model::{
CitationForm, CiteGroup, Destination, DirectLinkElem, FootnoteElem, HeadingElem,
LinkElem, Url,
};
use crate::routines::Routines;
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>,
#[internal]
#[synthesized]
pub lang: Lang,
#[internal]
#[synthesized]
pub region: Option<Region>,
}
impl BibliographyElem {
pub fn find(introspector: Tracked<Introspector>) -> StrResult<Packed<Self>> {
let query = introspector.query(&Self::ELEM.select());
let mut iter = query.iter();
let Some(elem) = iter.next() else {
bail!("the document does not contain a bibliography");
};
if iter.next().is_some() {
bail!("multiple bibliographies are not yet supported");
}
Ok(elem.to_packed::<Self>().unwrap().clone())
}
pub fn has(engine: &Engine, key: Label) -> bool {
engine
.introspector
.query(&Self::ELEM.select())
.iter()
.any(|elem| elem.to_packed::<Self>().unwrap().sources.derived.has(key))
}
pub fn keys(introspector: Tracked<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()
.as_rooted_path()
.extension()
.and_then(OsStr::to_str)
.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::new(
ReportPos::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::new(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(warning!(span, "{message}"));
}
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 dependant 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::new(ReportPos::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<&'static str>),
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 mut warning = None;
if string.as_str() == "chicago-fullnotes" {
warning = Some(
"style \"chicago-fullnotes\" has been deprecated \
in favor of \"chicago-notes\"",
);
} else if string.as_str() == "modern-humanities-research-association" {
warning = Some(
"style \"modern-humanities-research-association\" \
has been deprecated in favor of \
\"modern-humanities-research-association-notes\"",
);
}
let style = ArchivedStyle::by_name(&string)
.ok_or_else(|| eco_format!("unknown style: {}", string))?;
return Ok(CslSource::Named(style, warning));
}
}
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(),
}
}
}
pub struct Works {
pub citations: FxHashMap<Location, SourceResult<Content>>,
pub references: Option<Vec<(Option<Content>, Content, Location)>>,
pub hanging_indent: bool,
}
impl Works {
pub fn generate(engine: &Engine) -> StrResult<Arc<Works>> {
Self::generate_impl(engine.routines, engine.world, engine.introspector)
}
#[comemo::memoize]
fn generate_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
) -> StrResult<Arc<Works>> {
let mut generator = Generator::new(routines, world, introspector)?;
let rendered = generator.drive();
let works = generator.display(&rendered)?;
Ok(Arc::new(works))
}
pub fn references<'a>(
&'a self,
elem: &Packed<BibliographyElem>,
styles: StyleChain,
) -> SourceResult<&'a [(Option<Content>, Content, Location)]> {
self.references
.as_deref()
.ok_or_else(|| match elem.style.get_ref(styles).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()
}
})
.at(elem.span())
}
}
struct Generator<'a> {
routines: &'a Routines,
world: Tracked<'a, dyn World + 'a>,
bibliography: Packed<BibliographyElem>,
groups: EcoVec<Content>,
infos: Vec<GroupInfo>,
failures: FxHashMap<Location, SourceResult<Content>>,
}
struct GroupInfo {
location: Location,
span: Span,
footnote: bool,
subinfos: SmallVec<[CiteInfo; 1]>,
}
struct CiteInfo {
key: Label,
supplement: Option<Content>,
hidden: bool,
}
impl<'a> Generator<'a> {
fn new(
routines: &'a Routines,
world: Tracked<'a, dyn World + 'a>,
introspector: Tracked<Introspector>,
) -> StrResult<Self> {
let bibliography = BibliographyElem::find(introspector)?;
let groups = introspector.query(&CiteGroup::ELEM.select());
let infos = Vec::with_capacity(groups.len());
Ok(Self {
routines,
world,
bibliography,
groups,
infos,
failures: FxHashMap::default(),
})
}
fn drive(&mut self) -> hayagriva::Rendered {
static LOCALES: LazyLock<Vec<citationberg::Locale>> =
LazyLock::new(hayagriva::archive::locales);
let database = &self.bibliography.sources.derived;
let bibliography_style =
&self.bibliography.style.get_ref(StyleChain::default()).derived;
let mut driver = BibliographyDriver::new();
for elem in &self.groups {
let group = elem.to_packed::<CiteGroup>().unwrap();
let location = elem.location().unwrap();
let children = &group.children;
let Some(first) = children.first() else { continue };
let mut subinfos = SmallVec::with_capacity(children.len());
let mut items = Vec::with_capacity(children.len());
let mut errors = EcoVec::new();
let mut normal = true;
for child in children {
let Some(entry) = database.get(child.key) else {
errors.push(error!(
child.span(),
"key `{}` does not exist in the bibliography",
child.key.resolve()
));
continue;
};
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),
};
normal &= special_form.is_none();
subinfos.push(CiteInfo { key: child.key, supplement, hidden });
items.push(CitationItem::new(entry, locator, None, hidden, special_form));
}
if !errors.is_empty() {
self.failures.insert(location, Err(errors));
continue;
}
let style = match first.style.get_ref(StyleChain::default()) {
Smart::Auto => bibliography_style.get(),
Smart::Custom(style) => style.derived.get(),
};
self.infos.push(GroupInfo {
location,
subinfos,
span: first.span(),
footnote: normal
&& style.settings.class == citationberg::StyleClass::Note,
});
driver.citation(CitationRequest::new(
items,
style,
Some(locale(first.lang.unwrap_or(Lang::ENGLISH), first.region.flatten())),
&LOCALES,
None,
));
}
let locale = locale(
self.bibliography.lang.unwrap_or(Lang::ENGLISH),
self.bibliography.region.flatten(),
);
if self.bibliography.full.get(StyleChain::default()) {
for (_, entry) in database.iter() {
driver.citation(CitationRequest::new(
vec![CitationItem::new(entry, None, None, true, None)],
bibliography_style.get(),
Some(locale.clone()),
&LOCALES,
None,
));
}
}
driver.finish(BibliographyRequest {
style: bibliography_style.get(),
locale: Some(locale),
locale_files: &LOCALES,
})
}
fn display(&mut self, rendered: &hayagriva::Rendered) -> StrResult<Works> {
let citations = self.display_citations(rendered)?;
let references = self.display_references(rendered)?;
let hanging_indent =
rendered.bibliography.as_ref().is_some_and(|b| b.hanging_indent);
Ok(Works { citations, references, hanging_indent })
}
fn display_citations(
&mut self,
rendered: &hayagriva::Rendered,
) -> StrResult<FxHashMap<Location, SourceResult<Content>>> {
let mut links = FxHashMap::default();
if let Some(bibliography) = &rendered.bibliography {
let location = self.bibliography.location().unwrap();
for (k, item) in bibliography.items.iter().enumerate() {
links.insert(item.key.as_str(), location.variant(k + 1));
}
}
let mut output = std::mem::take(&mut self.failures);
for (info, citation) in self.infos.iter().zip(&rendered.citations) {
let supplement = |i: usize| info.subinfos.get(i)?.supplement.clone();
let link = |i: usize| {
links.get(info.subinfos.get(i)?.key.resolve().as_str()).copied()
};
let renderer = ElemRenderer {
routines: self.routines,
world: self.world,
span: info.span,
supplement: &supplement,
link: &link,
};
let content = if info.subinfos.iter().all(|sub| sub.hidden) {
Content::empty()
} else {
let mut content =
renderer.display_elem_children(&citation.citation, None, true)?;
if info.footnote {
content = FootnoteElem::with_content(content).pack();
}
content
};
output.insert(info.location, Ok(content));
}
Ok(output)
}
#[allow(clippy::type_complexity)]
fn display_references(
&self,
rendered: &hayagriva::Rendered,
) -> StrResult<Option<Vec<(Option<Content>, Content, Location)>>> {
let Some(rendered) = &rendered.bibliography else { return Ok(None) };
let mut first_occurrences = FxHashMap::default();
for info in &self.infos {
for subinfo in &info.subinfos {
let key = subinfo.key.resolve();
first_occurrences.entry(key).or_insert(info.location);
}
}
let location = self.bibliography.location().unwrap();
let mut output = vec![];
for (k, item) in rendered.items.iter().enumerate() {
let renderer = ElemRenderer {
routines: self.routines,
world: self.world,
span: self.bibliography.span(),
supplement: &|_| None,
link: &|_| None,
};
let backlink = location.variant(k + 1);
let mut prefix = item
.first_field
.as_ref()
.map(|elem| renderer.display_elem_child(elem, None, false))
.transpose()?;
let reference = renderer.display_elem_children(
&item.content,
Some(&mut prefix),
false,
)?;
let prefix = prefix.map(|content| {
if let Some(location) = first_occurrences.get(item.key.as_str()) {
let alt = content.plain_text();
let body = content.spanned(self.bibliography.span());
DirectLinkElem::new(*location, body, Some(alt)).pack()
} else {
content
}
});
output.push((prefix, reference, backlink));
}
Ok(Some(output))
}
}
struct ElemRenderer<'a> {
routines: &'a Routines,
world: Tracked<'a, dyn World + 'a>,
span: Span,
supplement: &'a dyn Fn(usize) -> Option<Content>,
link: &'a dyn Fn(usize) -> Option<Location>,
}
impl ElemRenderer<'_> {
fn display_elem_children(
&self,
elems: &hayagriva::ElemChildren,
mut prefix: Option<&mut Option<Content>>,
is_citation: bool,
) -> StrResult<Content> {
Ok(Content::sequence(
elems
.0
.iter()
.enumerate()
.map(|(i, elem)| {
self.display_elem_child(
elem,
prefix.as_deref_mut(),
is_citation && i == 0,
)
})
.collect::<StrResult<Vec<_>>>()?,
))
}
fn display_elem_child(
&self,
elem: &hayagriva::ElemChild,
prefix: Option<&mut Option<Content>>,
trim_start: bool,
) -> StrResult<Content> {
Ok(match elem {
hayagriva::ElemChild::Text(formatted) => {
self.display_formatted(formatted, trim_start)
}
hayagriva::ElemChild::Elem(elem) => self.display_elem(elem, prefix)?,
hayagriva::ElemChild::Markup(markup) => self.display_math(markup),
hayagriva::ElemChild::Link { text, url } => self.display_link(text, url)?,
hayagriva::ElemChild::Transparent { cite_idx, format } => {
self.display_transparent(*cite_idx, format)
}
})
}
fn display_elem(
&self,
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 = self.display_elem_children(
&elem.children,
if block_level { None } else { prefix.as_deref_mut() },
false,
)?;
match elem.display {
Some(Display::Block) => {
content = BlockElem::new()
.with_body(Some(BlockBody::Content(content)))
.pack()
.spanned(self.span);
}
Some(Display::Indent) => {
content = CslIndentElem::new(content).pack().spanned(self.span);
}
Some(Display::LeftMargin) => {
if let Some(prefix) = prefix {
*prefix.get_or_insert_with(Default::default) += content;
return Ok(Content::empty());
}
}
_ => {}
}
content = content.spanned(self.span);
if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta
&& let Some(location) = (self.link)(i)
{
let alt = content.plain_text();
content = DirectLinkElem::new(location, content, Some(alt)).pack();
}
Ok(content)
}
fn display_math(&self, math: &str) -> Content {
(self.routines.eval_string)(
self.routines,
self.world,
Sink::new().track_mut(),
math,
self.span,
SyntaxMode::Math,
Scope::new(),
)
.map(Value::display)
.unwrap_or_else(|_| TextElem::packed(math).spanned(self.span))
}
fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> StrResult<Content> {
let dest = Destination::Url(Url::new(url)?);
Ok(LinkElem::new(dest.into(), self.display_formatted(text, false))
.pack()
.spanned(self.span))
}
fn display_transparent(&self, i: usize, format: &hayagriva::Formatting) -> Content {
let content = (self.supplement)(i).unwrap_or_default();
apply_formatting(content, format)
}
fn display_formatted(
&self,
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(self.span);
apply_formatting(content, &formatted.formatting)
}
}
fn apply_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,
}
#[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:?}'"
);
}
}