use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::Arc;
use comemo::Tracked;
use ecow::{eco_format, EcoString, EcoVec};
use hayagriva::archive::ArchivedStyle;
use hayagriva::io::BibLaTeXError;
use hayagriva::{
citationberg, BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest,
SpecificLocator,
};
use indexmap::IndexMap;
use once_cell::sync::Lazy;
use smallvec::{smallvec, SmallVec};
use typed_arena::Arena;
use crate::diag::{bail, error, At, FileError, SourceResult, StrResult};
use crate::engine::Engine;
use crate::eval::{eval_string, EvalMode};
use crate::foundations::{
cast, elem, ty, Args, Array, Bytes, CastInfo, Content, FromValue, IntoValue, Label,
NativeElement, Packed, Reflect, Repr, Scope, Show, ShowSet, Smart, Str, StyleChain,
Styles, Synthesize, Type, Value,
};
use crate::introspection::{Introspector, Locatable, Location};
use crate::layout::{
BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, Sizing,
TrackSizings, VElem,
};
use crate::model::{
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
};
use crate::syntax::{Span, Spanned};
use crate::text::{
FontStyle, Lang, LocalName, Region, SubElem, SuperElem, TextElem, WeightDelta,
};
use crate::util::{option_eq, LazyHash, NonZeroExt, PicoStr};
use crate::World;
#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)]
pub struct BibliographyElem {
#[required]
#[parse(
let (paths, bibliography) = Bibliography::parse(engine, args)?;
paths
)]
pub path: BibliographyPaths,
#[default(Some(Smart::Auto))]
pub title: Option<Smart<Content>>,
#[default(false)]
pub full: bool,
#[parse(CslStyle::parse(engine, args)?)]
#[default(CslStyle::from_name("ieee").unwrap())]
pub style: CslStyle,
#[internal]
#[required]
#[parse(bibliography)]
pub bibliography: Bibliography,
#[internal]
#[synthesized]
pub lang: Lang,
#[internal]
#[synthesized]
pub region: Option<Region>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct BibliographyPaths(Vec<EcoString>);
cast! {
BibliographyPaths,
self => self.0.into_value(),
v: EcoString => Self(vec![v]),
v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
}
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: impl Into<PicoStr>) -> bool {
let key = key.into();
engine
.introspector
.query(&Self::elem().select())
.iter()
.any(|elem| elem.to_packed::<Self>().unwrap().bibliography().has(key))
}
pub fn keys(
introspector: Tracked<Introspector>,
) -> Vec<(EcoString, Option<EcoString>)> {
let mut vec = vec![];
for elem in introspector.query(&Self::elem().select()).iter() {
let this = elem.to_packed::<Self>().unwrap();
for entry in this.bibliography().entries() {
let key = entry.key().into();
let detail = entry.title().map(|title| title.value.to_str().into());
vec.push((key, detail))
}
}
vec
}
}
impl Synthesize for Packed<BibliographyElem> {
fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
let elem = self.as_mut();
elem.push_lang(TextElem::lang_in(styles));
elem.push_region(TextElem::region_in(styles));
Ok(())
}
}
impl Show for Packed<BibliographyElem> {
#[typst_macros::time(name = "bibliography", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
const COLUMN_GUTTER: Em = Em::new(0.65);
const INDENT: Em = Em::new(1.5);
let mut seq = vec![];
if let Some(title) = self.title(styles) {
let title = title.unwrap_or_else(|| {
TextElem::packed(Self::local_name_in(styles)).spanned(self.span())
});
seq.push(
HeadingElem::new(title)
.with_level(Smart::Custom(NonZeroUsize::ONE))
.pack()
.spanned(self.span()),
);
}
let span = self.span();
let works = Works::generate(engine.world, engine.introspector).at(span)?;
let references = works
.references
.as_ref()
.ok_or("CSL style is not suitable for bibliographies")
.at(span)?;
let row_gutter = *BlockElem::below_in(styles).amount();
if references.iter().any(|(prefix, _)| prefix.is_some()) {
let mut cells = vec![];
for (prefix, reference) in references {
cells.push(GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(prefix.clone().unwrap_or_default()))
.spanned(span),
)));
cells.push(GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(reference.clone())).spanned(span),
)));
}
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
seq.push(
GridElem::new(cells)
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
.with_row_gutter(TrackSizings(smallvec![(row_gutter).into()]))
.pack()
.spanned(self.span()),
);
} else {
for (_, reference) in references {
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
seq.push(reference.clone());
}
}
let mut content = Content::sequence(seq);
if works.hanging_indent {
content = content.styled(ParElem::set_hanging_indent(INDENT.into()));
}
Ok(content)
}
}
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::set_numbering(None));
out.set(PadElem::set_left(INDENT.into()));
out
}
}
impl LocalName for Packed<BibliographyElem> {
fn local_name(lang: Lang, region: Option<Region>) -> &'static str {
match lang {
Lang::ALBANIAN => "Bibliografi",
Lang::ARABIC => "المراجع",
Lang::BOKMÅL => "Bibliografi",
Lang::CATALAN => "Bibliografia",
Lang::CHINESE if option_eq(region, "TW") => "書目",
Lang::CHINESE => "参考文献",
Lang::CZECH => "Bibliografie",
Lang::DANISH => "Bibliografi",
Lang::DUTCH => "Bibliografie",
Lang::ESTONIAN => "Viited",
Lang::FILIPINO => "Bibliograpiya",
Lang::FINNISH => "Viitteet",
Lang::FRENCH => "Bibliographie",
Lang::GERMAN => "Bibliographie",
Lang::GREEK => "Βιβλιογραφία",
Lang::HUNGARIAN => "Irodalomjegyzék",
Lang::ITALIAN => "Bibliografia",
Lang::NYNORSK => "Bibliografi",
Lang::POLISH => "Bibliografia",
Lang::PORTUGUESE => "Bibliografia",
Lang::ROMANIAN => "Bibliografie",
Lang::RUSSIAN => "Библиография",
Lang::SERBIAN => "Литература",
Lang::SLOVENIAN => "Literatura",
Lang::SPANISH => "Bibliografía",
Lang::SWEDISH => "Bibliografi",
Lang::TURKISH => "Kaynakça",
Lang::UKRAINIAN => "Бібліографія",
Lang::VIETNAMESE => "Tài liệu tham khảo",
Lang::JAPANESE => "参考文献",
Lang::ENGLISH | _ => "Bibliography",
}
}
}
#[derive(Clone, PartialEq)]
pub struct Bibliography {
map: Arc<IndexMap<PicoStr, hayagriva::Entry>>,
hash: u128,
}
impl Bibliography {
fn parse(
engine: &mut Engine,
args: &mut Args,
) -> SourceResult<(BibliographyPaths, Bibliography)> {
let Spanned { v: paths, span } =
args.expect::<Spanned<BibliographyPaths>>("path to bibliography file")?;
let data = paths
.0
.iter()
.map(|path| {
let id = span.resolve_path(path).at(span)?;
engine.world.file(id).at(span)
})
.collect::<SourceResult<Vec<Bytes>>>()?;
let bibliography = Self::load(&paths, &data).at(span)?;
Ok((paths, bibliography))
}
#[comemo::memoize]
#[typst_macros::time(name = "load bibliography")]
fn load(paths: &BibliographyPaths, data: &[Bytes]) -> StrResult<Bibliography> {
let mut map = IndexMap::new();
let mut duplicates = Vec::<EcoString>::new();
for (path, bytes) in paths.0.iter().zip(data) {
let src = std::str::from_utf8(bytes).map_err(FileError::from)?;
let ext = Path::new(path.as_str())
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default();
let library = match ext.to_lowercase().as_str() {
"yml" | "yaml" => hayagriva::io::from_yaml_str(src)
.map_err(|err| eco_format!("failed to parse YAML ({err})"))?,
"bib" => hayagriva::io::from_biblatex_str(src)
.map_err(|errors| format_biblatex_error(path, src, errors))?,
_ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"),
};
for entry in library {
match map.entry(entry.key().into()) {
indexmap::map::Entry::Vacant(vacant) => {
vacant.insert(entry);
}
indexmap::map::Entry::Occupied(_) => {
duplicates.push(entry.key().into());
}
}
}
}
if !duplicates.is_empty() {
bail!("duplicate bibliography keys: {}", duplicates.join(", "));
}
Ok(Bibliography {
map: Arc::new(map),
hash: crate::util::hash128(data),
})
}
fn has(&self, key: impl Into<PicoStr>) -> bool {
self.map.contains_key(&key.into())
}
fn entries(&self) -> impl Iterator<Item = &hayagriva::Entry> {
self.map.values()
}
}
impl Debug for Bibliography {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_set().entries(self.map.keys()).finish()
}
}
impl Hash for Bibliography {
fn hash<H: Hasher>(&self, state: &mut H) {
self.hash.hash(state);
}
}
fn format_biblatex_error(path: &str, src: &str, errors: Vec<BibLaTeXError>) -> EcoString {
let Some(error) = errors.first() else {
return eco_format!("failed to parse BibLaTeX file ({path})");
};
let (span, msg) = match error {
BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()),
BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()),
};
let line = src.get(..span.start).unwrap_or_default().lines().count();
eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})")
}
#[ty(cast)]
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct CslStyle {
name: Option<EcoString>,
style: Arc<LazyHash<citationberg::IndependentStyle>>,
}
impl CslStyle {
pub fn parse(engine: &mut Engine, args: &mut Args) -> SourceResult<Option<CslStyle>> {
let Some(Spanned { v: string, span }) =
args.named::<Spanned<EcoString>>("style")?
else {
return Ok(None);
};
Ok(Some(Self::parse_impl(engine, &string, span).at(span)?))
}
pub fn parse_smart(
engine: &mut Engine,
args: &mut Args,
) -> SourceResult<Option<Smart<CslStyle>>> {
let Some(Spanned { v: smart, span }) =
args.named::<Spanned<Smart<EcoString>>>("style")?
else {
return Ok(None);
};
Ok(Some(match smart {
Smart::Auto => Smart::Auto,
Smart::Custom(string) => {
Smart::Custom(Self::parse_impl(engine, &string, span).at(span)?)
}
}))
}
fn parse_impl(engine: &mut Engine, string: &str, span: Span) -> StrResult<CslStyle> {
let ext = Path::new(string)
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
if ext == "csl" {
let id = span.resolve_path(string)?;
let data = engine.world.file(id)?;
CslStyle::from_data(&data)
} else {
CslStyle::from_name(string)
}
}
#[comemo::memoize]
pub fn from_name(name: &str) -> StrResult<CslStyle> {
match hayagriva::archive::ArchivedStyle::by_name(name).map(ArchivedStyle::get) {
Some(citationberg::Style::Independent(style)) => Ok(Self {
name: Some(name.into()),
style: Arc::new(LazyHash::new(style)),
}),
_ => bail!("unknown style: `{name}`"),
}
}
#[comemo::memoize]
pub fn from_data(data: &Bytes) -> StrResult<CslStyle> {
let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?;
citationberg::IndependentStyle::from_xml(text)
.map(|style| Self { name: None, style: Arc::new(LazyHash::new(style)) })
.map_err(|err| eco_format!("failed to load CSL style ({err})"))
}
pub fn get(&self) -> &citationberg::IndependentStyle {
self.style.as_ref()
}
}
impl Reflect for CslStyle {
#[comemo::memoize]
fn input() -> CastInfo {
let ty = std::iter::once(CastInfo::Type(Type::of::<Str>()));
let options = hayagriva::archive::ArchivedStyle::all().iter().map(|name| {
CastInfo::Value(name.names()[0].into_value(), name.display_name())
});
CastInfo::Union(ty.chain(options).collect())
}
fn output() -> CastInfo {
EcoString::output()
}
fn castable(value: &Value) -> bool {
if let Value::Dyn(dynamic) = &value {
if dynamic.is::<Self>() {
return true;
}
}
false
}
}
impl FromValue for CslStyle {
fn from_value(value: Value) -> StrResult<Self> {
if let Value::Dyn(dynamic) = &value {
if let Some(concrete) = dynamic.downcast::<Self>() {
return Ok(concrete.clone());
}
}
Err(<Self as Reflect>::error(&value))
}
}
impl IntoValue for CslStyle {
fn into_value(self) -> Value {
Value::dynamic(self)
}
}
impl Repr for CslStyle {
fn repr(&self) -> EcoString {
self.name
.as_ref()
.map(|name| name.repr())
.unwrap_or_else(|| "..".into())
}
}
pub(super) struct Works {
pub citations: HashMap<Location, SourceResult<Content>>,
pub references: Option<Vec<(Option<Content>, Content)>>,
pub hanging_indent: bool,
}
impl Works {
#[comemo::memoize]
pub fn generate(
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
) -> StrResult<Arc<Works>> {
let mut generator = Generator::new(world, introspector)?;
let rendered = generator.drive();
let works = generator.display(&rendered)?;
Ok(Arc::new(works))
}
}
struct Generator<'a> {
world: Tracked<'a, dyn World + 'a>,
bibliography: Packed<BibliographyElem>,
groups: EcoVec<Content>,
infos: Vec<GroupInfo>,
failures: HashMap<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(
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 {
world,
bibliography,
groups,
infos,
failures: HashMap::new(),
})
}
fn drive(&mut self) -> hayagriva::Rendered {
static LOCALES: Lazy<Vec<citationberg::Locale>> =
Lazy::new(hayagriva::archive::locales);
let database = self.bibliography.bibliography();
let bibliography_style = self.bibliography.style(StyleChain::default());
let styles = Arena::new();
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 key = *child.key();
let Some(entry) = database.map.get(&key.into_inner()) else {
errors.push(error!(
child.span(),
"key `{}` does not exist in the bibliography",
key.as_str()
));
continue;
};
let supplement = child.supplement(StyleChain::default());
let locator = supplement.as_ref().map(|_| {
SpecificLocator(
citationberg::taxonomy::Locator::Custom,
hayagriva::LocatorPayload::Transparent,
)
});
let mut hidden = false;
let special_form = match child.form(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, 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(StyleChain::default()) {
Smart::Auto => &bibliography_style.style,
Smart::Custom(style) => styles.alloc(style.style),
};
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().copied().unwrap_or(Lang::ENGLISH),
first.region().copied().flatten(),
)),
&LOCALES,
None,
));
}
let locale = locale(
self.bibliography.lang().copied().unwrap_or(Lang::ENGLISH),
self.bibliography.region().copied().flatten(),
);
if self.bibliography.full(StyleChain::default()) {
for entry in database.map.values() {
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,
) -> HashMap<Location, SourceResult<Content>> {
let mut links = HashMap::new();
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.as_str()).copied();
let renderer = ElemRenderer {
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, &mut None);
if info.footnote {
content = FootnoteElem::with_content(content).pack();
}
content
};
output.insert(info.location, Ok(content));
}
output
}
fn display_references(
&self,
rendered: &hayagriva::Rendered,
) -> Option<Vec<(Option<Content>, Content)>> {
let rendered = rendered.bibliography.as_ref()?;
let mut first_occurrences = HashMap::new();
for info in &self.infos {
for subinfo in &info.subinfos {
let key = subinfo.key.as_str();
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 {
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| {
let mut content = renderer.display_elem_child(elem, &mut None);
if let Some(location) = first_occurrences.get(item.key.as_str()) {
let dest = Destination::Location(*location);
content = content.linked(dest);
}
content.backlinked(backlink)
});
let reference = renderer
.display_elem_children(&item.content, &mut prefix)
.backlinked(backlink);
output.push((prefix, reference));
}
Some(output)
}
}
struct ElemRenderer<'a> {
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,
prefix: &mut Option<Content>,
) -> Content {
Content::sequence(
elems.0.iter().map(|elem| self.display_elem_child(elem, prefix)),
)
}
fn display_elem_child(
&self,
elem: &hayagriva::ElemChild,
prefix: &mut Option<Content>,
) -> Content {
match elem {
hayagriva::ElemChild::Text(formatted) => self.display_formatted(formatted),
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,
prefix: &mut Option<Content>,
) -> Content {
use citationberg::Display;
let block_level = matches!(elem.display, Some(Display::Block | Display::Indent));
let mut suf_prefix = None;
let mut content = self.display_elem_children(
&elem.children,
if block_level { &mut suf_prefix } else { prefix },
);
if let Some(prefix) = suf_prefix {
const COLUMN_GUTTER: Em = Em::new(0.65);
content = GridElem::new(vec![
GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(prefix)).spanned(self.span),
)),
GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(content)).spanned(self.span),
)),
])
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
.pack()
.spanned(self.span);
}
match elem.display {
Some(Display::Block) => {
content =
BlockElem::new().with_body(Some(content)).pack().spanned(self.span);
}
Some(Display::Indent) => {
content = PadElem::new(content).pack().spanned(self.span);
}
Some(Display::LeftMargin) => {
*prefix.get_or_insert_with(Default::default) += content;
return Content::empty();
}
_ => {}
}
if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta {
if let Some(location) = (self.link)(i) {
let dest = Destination::Location(location);
content = content.linked(dest);
}
}
content
}
fn display_math(&self, math: &str) -> Content {
eval_string(self.world, math, self.span, EvalMode::Math, Scope::new())
.map(Value::display)
.unwrap_or_else(|_| TextElem::packed(math).spanned(self.span))
}
fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> Content {
let dest = Destination::Url(url.into());
LinkElem::new(dest.into(), self.display_formatted(text))
.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) -> Content {
let content = TextElem::packed(formatted.text.as_str()).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.styled(TextElem::set_style(FontStyle::Italic));
}
}
match format.font_variant {
citationberg::FontVariant::Normal => {}
citationberg::FontVariant::SmallCaps => {
content = content.styled(TextElem::set_smallcaps(true));
}
}
match format.font_weight {
citationberg::FontWeight::Normal => {}
citationberg::FontWeight::Bold => {
content = content.styled(TextElem::set_delta(WeightDelta(300)));
}
citationberg::FontWeight::Light => {
content = content.styled(TextElem::set_delta(WeightDelta(-100)));
}
}
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().pack() + SuperElem::new(content).pack().spanned(span);
}
citationberg::VerticalAlign::Sub => {
content = HElem::hole().pack() + 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)
}