1034 lines
35 KiB
Rust
1034 lines
35 KiB
Rust
use std::collections::HashMap;
|
|
use std::ffi::OsStr;
|
|
use std::hash::{Hash, Hasher};
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
use comemo::Prehashed;
|
|
use ecow::EcoVec;
|
|
use hayagriva::citationberg;
|
|
use hayagriva::io::BibLaTeXError;
|
|
use hayagriva::{
|
|
BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest,
|
|
SpecificLocator,
|
|
};
|
|
use indexmap::IndexMap;
|
|
use once_cell::sync::Lazy;
|
|
use smallvec::SmallVec;
|
|
use typed_arena::Arena;
|
|
use typst::diag::FileError;
|
|
use typst::eval::{eval_string, Bytes, CastInfo, EvalMode, Reflect};
|
|
use typst::font::FontStyle;
|
|
use typst::util::option_eq;
|
|
|
|
use super::{CitationForm, CiteGroup, LocalName};
|
|
use crate::layout::{
|
|
BlockElem, GridElem, HElem, PadElem, ParElem, Sizing, TrackSizings, VElem,
|
|
};
|
|
use crate::meta::{FootnoteElem, HeadingElem};
|
|
use crate::prelude::*;
|
|
use crate::text::{Delta, SubElem, SuperElem, TextElem};
|
|
|
|
/// A bibliography / reference listing.
|
|
///
|
|
/// You can create a new bibliography by calling this function with a path
|
|
/// to a bibliography file in either one of two formats:
|
|
///
|
|
/// - A Hayagriva `.yml` file. Hayagriva is a new bibliography file format
|
|
/// designed for use with Typst. Visit its
|
|
/// [documentation](https://github.com/typst/hayagriva/blob/main/docs/file-format.md)
|
|
/// for more details.
|
|
/// - A BibLaTeX `.bib` file.
|
|
///
|
|
/// As soon as you add a bibliography somewhere in your document, you can start
|
|
/// citing things with reference syntax (`[@key]`) or explicit calls to the
|
|
/// [citation]($cite) function (`[#cite(<key>)]`). The bibliography will only
|
|
/// show entries for works that were referenced in the document.
|
|
///
|
|
/// # Styles
|
|
/// Typst offers a wide selection of built-in
|
|
/// [citation and bibliography styles]($bibliography.style). Beyond those, you
|
|
/// can add and use custom [CSL](https://citationstyles.org/) (Citation Style
|
|
/// Language) files. Wondering which style to use? Here are some good defaults
|
|
/// based on what discipline you're working in:
|
|
///
|
|
/// | Fields | Typical Styles |
|
|
/// |-----------------|--------------------------------------------------------|
|
|
/// | Engineering, IT | `{"ieee"}` |
|
|
/// | Psychology, Life Sciences | `{"apa"}` |
|
|
/// | Social sciences | `{"chicago-author-date"}` |
|
|
/// | Humanities | `{"mla"}`, `{"chicago-notes"}`, `{"harvard-cite-them-right"}` |
|
|
/// | Economics | `{"harvard-cite-them-right"}` |
|
|
/// | Physics | `{"american-physics-society"}` |
|
|
///
|
|
/// # Example
|
|
/// ```example
|
|
/// This was already noted by
|
|
/// pirates long ago. @arrgh
|
|
///
|
|
/// Multiple sources say ...
|
|
/// @arrgh @netwok.
|
|
///
|
|
/// #bibliography("works.bib")
|
|
/// ```
|
|
#[elem(Locatable, Synthesize, Show, Finalize, LocalName)]
|
|
pub struct BibliographyElem {
|
|
/// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files.
|
|
#[required]
|
|
#[parse(
|
|
let (paths, bibliography) = Bibliography::parse(vm, args)?;
|
|
paths
|
|
)]
|
|
pub path: BibPaths,
|
|
|
|
/// The title of the bibliography.
|
|
///
|
|
/// - When set to `{auto}`, an appropriate title for the
|
|
/// [text language]($text.lang) will be used. This is the default.
|
|
/// - When set to `{none}`, the bibliography will not have a title.
|
|
/// - A custom title can be set by passing content.
|
|
///
|
|
/// The bibliography's heading will not be numbered by default, but you can
|
|
/// force it to be with a show-set rule:
|
|
/// `{show bibliography: set heading(numbering: "1.")}`
|
|
#[default(Some(Smart::Auto))]
|
|
pub title: Option<Smart<Content>>,
|
|
|
|
/// Whether to include all works from the given bibliography files, even
|
|
/// those that weren't cited in the document.
|
|
///
|
|
/// To selectively add individual cited works without showing them, you can
|
|
/// also use the `cite` function with [`form`]($cite.form) set to `{none}`.
|
|
#[default(false)]
|
|
pub full: bool,
|
|
|
|
/// The bibliography style.
|
|
///
|
|
/// Should be either one of the built-in styles (see below) or a path to
|
|
/// a [CSL file](https://citationstyles.org/). Some of the styles listed
|
|
/// below appear twice, once with their full name and once with a short
|
|
/// alias.
|
|
#[parse(CslStyle::parse(vm, args)?)]
|
|
#[default(CslStyle::from_name("ieee").unwrap())]
|
|
pub style: CslStyle,
|
|
|
|
/// The loaded bibliography.
|
|
#[internal]
|
|
#[required]
|
|
#[parse(bibliography)]
|
|
pub bibliography: Bibliography,
|
|
|
|
/// The language setting where the bibliography is.
|
|
#[internal]
|
|
#[synthesized]
|
|
pub lang: Lang,
|
|
|
|
/// The region setting where the bibliography is.
|
|
#[internal]
|
|
#[synthesized]
|
|
pub region: Option<Region>,
|
|
}
|
|
|
|
/// A list of bibliography file paths.
|
|
#[derive(Debug, Default, Clone, Hash)]
|
|
pub struct BibPaths(Vec<EcoString>);
|
|
|
|
cast! {
|
|
BibPaths,
|
|
self => self.0.into_value(),
|
|
v: EcoString => Self(vec![v]),
|
|
v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
|
|
}
|
|
|
|
impl BibliographyElem {
|
|
/// Find the document's bibliography.
|
|
pub fn find(introspector: Tracked<Introspector>) -> StrResult<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::<Self>().unwrap().clone())
|
|
}
|
|
|
|
/// Whether the bibliography contains the given key.
|
|
pub fn has(vt: &Vt, key: &str) -> bool {
|
|
vt.introspector
|
|
.query(&Self::elem().select())
|
|
.iter()
|
|
.any(|elem| elem.to::<Self>().unwrap().bibliography().has(key))
|
|
}
|
|
|
|
/// Find all bibliography keys.
|
|
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::<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 BibliographyElem {
|
|
fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
|
|
self.push_full(self.full(styles));
|
|
self.push_style(self.style(styles));
|
|
self.push_lang(TextElem::lang_in(styles));
|
|
self.push_region(TextElem::region_in(styles));
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Show for BibliographyElem {
|
|
#[tracing::instrument(name = "BibliographyElem::show", skip_all)]
|
|
fn show(&self, vt: &mut Vt, 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(
|
|
TextElem::lang_in(styles),
|
|
TextElem::region_in(styles),
|
|
))
|
|
.spanned(self.span())
|
|
});
|
|
|
|
seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack());
|
|
}
|
|
|
|
Ok(vt.delayed(|vt| {
|
|
let span = self.span();
|
|
let works = Works::generate(vt.world, vt.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(prefix.clone().unwrap_or_default());
|
|
cells.push(reference.clone());
|
|
}
|
|
|
|
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
|
|
seq.push(
|
|
GridElem::new(cells)
|
|
.with_columns(TrackSizings(vec![Sizing::Auto; 2]))
|
|
.with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()]))
|
|
.with_row_gutter(TrackSizings(vec![row_gutter.into()]))
|
|
.pack(),
|
|
);
|
|
} 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 Finalize for BibliographyElem {
|
|
fn finalize(&self, realized: Content, _: StyleChain) -> Content {
|
|
const INDENT: Em = Em::new(1.0);
|
|
realized
|
|
.styled(HeadingElem::set_numbering(None))
|
|
.styled(PadElem::set_left(INDENT.into()))
|
|
}
|
|
}
|
|
|
|
impl LocalName for BibliographyElem {
|
|
fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str {
|
|
match lang {
|
|
Lang::ALBANIAN => "Bibliografi",
|
|
Lang::ARABIC => "المراجع",
|
|
Lang::BOKMÅL => "Bibliografi",
|
|
Lang::CHINESE if option_eq(region, "TW") => "書目",
|
|
Lang::CHINESE => "参考文献",
|
|
Lang::CZECH => "Bibliografie",
|
|
Lang::DANISH => "Bibliografi",
|
|
Lang::DUTCH => "Bibliografie",
|
|
Lang::FILIPINO => "Bibliograpiya",
|
|
Lang::FINNISH => "Viitteet",
|
|
Lang::FRENCH => "Bibliographie",
|
|
Lang::GERMAN => "Bibliographie",
|
|
Lang::HUNGARIAN => "Irodalomjegyzék",
|
|
Lang::ITALIAN => "Bibliografia",
|
|
Lang::NYNORSK => "Bibliografi",
|
|
Lang::POLISH => "Bibliografia",
|
|
Lang::PORTUGUESE => "Bibliografia",
|
|
Lang::ROMANIAN => "Bibliografie",
|
|
Lang::RUSSIAN => "Библиография",
|
|
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",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A loaded bibliography.
|
|
#[ty]
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct Bibliography {
|
|
map: Arc<IndexMap<EcoString, hayagriva::Entry>>,
|
|
hash: u128,
|
|
}
|
|
|
|
impl Bibliography {
|
|
/// Parse the bibliography argument.
|
|
fn parse(vm: &mut Vm, args: &mut Args) -> SourceResult<(BibPaths, Bibliography)> {
|
|
let Spanned { v: paths, span } =
|
|
args.expect::<Spanned<BibPaths>>("path to bibliography file")?;
|
|
|
|
// Load bibliography files.
|
|
let data = paths
|
|
.0
|
|
.iter()
|
|
.map(|path| {
|
|
let id = vm.resolve_path(path).at(span)?;
|
|
vm.world().file(id).at(span)
|
|
})
|
|
.collect::<SourceResult<Vec<Bytes>>>()?;
|
|
|
|
// Parse.
|
|
let bibliography = Self::load(&paths, &data).at(span)?;
|
|
|
|
Ok((paths, bibliography))
|
|
}
|
|
|
|
/// Load bibliography entries from paths.
|
|
#[comemo::memoize]
|
|
fn load(paths: &BibPaths, data: &[Bytes]) -> StrResult<Bibliography> {
|
|
let mut map = IndexMap::new();
|
|
let mut duplicates = Vec::<EcoString>::new();
|
|
|
|
// We might have multiple bib/yaml files
|
|
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: typst::util::hash128(data),
|
|
})
|
|
}
|
|
|
|
fn has(&self, key: &str) -> bool {
|
|
self.map.contains_key(key)
|
|
}
|
|
|
|
fn entries(&self) -> impl Iterator<Item = &hayagriva::Entry> {
|
|
self.map.values()
|
|
}
|
|
}
|
|
|
|
impl Hash for Bibliography {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
self.hash.hash(state);
|
|
}
|
|
}
|
|
|
|
impl Repr for Bibliography {
|
|
fn repr(&self) -> EcoString {
|
|
"..".into()
|
|
}
|
|
}
|
|
|
|
cast! {
|
|
type Bibliography,
|
|
}
|
|
|
|
/// Format a BibLaTeX loading error.
|
|
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})")
|
|
}
|
|
|
|
/// A loaded CSL style.
|
|
#[ty]
|
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
|
pub struct CslStyle {
|
|
name: Option<EcoString>,
|
|
style: Arc<Prehashed<citationberg::IndependentStyle>>,
|
|
}
|
|
|
|
impl CslStyle {
|
|
/// Parse the style argument.
|
|
pub fn parse(vm: &mut Vm, 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(vm, &string).at(span)?))
|
|
}
|
|
|
|
/// Parse the style argument with `Smart`.
|
|
pub fn parse_smart(
|
|
vm: &mut Vm,
|
|
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(vm, &string).at(span)?)
|
|
}
|
|
}))
|
|
}
|
|
|
|
/// Parse internally.
|
|
fn parse_impl(vm: &mut Vm, string: &str) -> StrResult<CslStyle> {
|
|
let ext = Path::new(string)
|
|
.extension()
|
|
.and_then(OsStr::to_str)
|
|
.unwrap_or_default()
|
|
.to_lowercase();
|
|
|
|
if ext == "csl" {
|
|
let id = vm.resolve_path(string)?;
|
|
let data = vm.world().file(id)?;
|
|
CslStyle::from_data(&data)
|
|
} else {
|
|
CslStyle::from_name(string)
|
|
}
|
|
}
|
|
|
|
/// Load a built-in CSL style.
|
|
#[comemo::memoize]
|
|
pub fn from_name(name: &str) -> StrResult<Self> {
|
|
match hayagriva::archive::style_by_name(name) {
|
|
Some(citationberg::Style::Independent(style)) => Ok(Self {
|
|
name: Some(name.into()),
|
|
style: Arc::new(Prehashed::new(style)),
|
|
}),
|
|
_ => bail!("unknown style: `{name}`"),
|
|
}
|
|
}
|
|
|
|
/// Load a CSL style from file contents.
|
|
#[comemo::memoize]
|
|
pub fn from_data(data: &Bytes) -> StrResult<Self> {
|
|
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(Prehashed::new(style)) })
|
|
.map_err(|err| eco_format!("failed to load CSL style ({err})"))
|
|
}
|
|
|
|
/// Get the underlying independent style.
|
|
pub fn get(&self) -> &citationberg::IndependentStyle {
|
|
self.style.as_ref()
|
|
}
|
|
}
|
|
|
|
// This Reflect impl is technically a bit wrong because it doesn't say what
|
|
// FromValue and IntoValue really do. Instead, it says what the `style` argument
|
|
// on `bibliography` and `cite` expect (through manual parsing).
|
|
impl Reflect for CslStyle {
|
|
#[comemo::memoize]
|
|
fn input() -> CastInfo {
|
|
let ty = std::iter::once(CastInfo::Type(Type::of::<Str>()));
|
|
let options = hayagriva::archive::styles()
|
|
.map(|style| CastInfo::Value(style.name.into_value(), style.full_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())
|
|
}
|
|
}
|
|
|
|
/// Fully formatted citations and references, generated once (through
|
|
/// memoization) for the whole document. This setup is necessary because
|
|
/// citation formatting is inherently stateful and we need access to all
|
|
/// citations to do it.
|
|
pub(super) struct Works {
|
|
/// Maps from the location of a citation group to its rendered content.
|
|
pub citations: HashMap<Location, SourceResult<Content>>,
|
|
/// Lists all references in the bibliography, with optional prefix, or
|
|
/// `None` if the citation style can't be used for bibliographies.
|
|
pub references: Option<Vec<(Option<Content>, Content)>>,
|
|
/// Whether the bibliography should have hanging indent.
|
|
pub hanging_indent: bool,
|
|
}
|
|
|
|
impl Works {
|
|
/// Generate all citations and the whole bibliography.
|
|
#[comemo::memoize]
|
|
pub fn generate(
|
|
world: Tracked<dyn World + '_>,
|
|
introspector: Tracked<Introspector>,
|
|
) -> StrResult<Arc<Self>> {
|
|
let mut generator = Generator::new(world, introspector)?;
|
|
let rendered = generator.drive();
|
|
let works = generator.display(&rendered)?;
|
|
Ok(Arc::new(works))
|
|
}
|
|
}
|
|
|
|
/// Context for generating the bibliography.
|
|
struct Generator<'a> {
|
|
/// The world that is used to evaluate mathematical material in citations.
|
|
world: Tracked<'a, dyn World + 'a>,
|
|
/// The document's bibliography.
|
|
bibliography: BibliographyElem,
|
|
/// The document's citation groups.
|
|
groups: EcoVec<Prehashed<Content>>,
|
|
/// Details about each group that are accumulated while driving hayagriva's
|
|
/// bibliography driver and needed when processing hayagriva's output.
|
|
infos: Vec<GroupInfo>,
|
|
/// Citations with unresolved keys.
|
|
failures: HashMap<Location, SourceResult<Content>>,
|
|
}
|
|
|
|
/// Details about a group of merged citations. All citations are put into groups
|
|
/// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two).
|
|
/// Even single citations will be put into groups of length ones.
|
|
struct GroupInfo {
|
|
/// The group's location.
|
|
location: Location,
|
|
/// The group's span.
|
|
span: Span,
|
|
/// Whether the group should be displayed in a footnote.
|
|
footnote: bool,
|
|
/// Details about the groups citations.
|
|
subinfos: SmallVec<[CiteInfo; 1]>,
|
|
}
|
|
|
|
/// Details about a citation item in a request.
|
|
struct CiteInfo {
|
|
/// The citation's key.
|
|
key: Label,
|
|
/// The citation's supplement.
|
|
supplement: Option<Content>,
|
|
/// Whether this citation was hidden.
|
|
hidden: bool,
|
|
}
|
|
|
|
impl<'a> Generator<'a> {
|
|
/// Create a new generator.
|
|
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(),
|
|
})
|
|
}
|
|
|
|
/// Drives hayagriva's citation driver.
|
|
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();
|
|
|
|
// Process all citation groups.
|
|
let mut driver = BibliographyDriver::new();
|
|
for elem in &self.groups {
|
|
let group = elem.to::<CiteGroup>().unwrap();
|
|
let location = group.0.location().unwrap();
|
|
let children = group.children();
|
|
|
|
// Groups should never be empty.
|
|
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;
|
|
|
|
// Create infos and items for each child in the group.
|
|
for child in &children {
|
|
let key = child.key();
|
|
let Some(entry) = database.map.get(&key.0) else {
|
|
errors.push(error!(
|
|
child.span(),
|
|
"key `{}` does not exist in the bibliography", key.0
|
|
));
|
|
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(), first.region())),
|
|
&LOCALES,
|
|
None,
|
|
));
|
|
}
|
|
|
|
let locale = locale(self.bibliography.lang(), self.bibliography.region());
|
|
|
|
// Add hidden items for everything if we should print the whole
|
|
// bibliography.
|
|
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,
|
|
})
|
|
}
|
|
|
|
/// Displays hayagriva's output as content for the citations and references.
|
|
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().map_or(false, |b| b.hanging_indent);
|
|
Ok(Works { citations, references, hanging_indent })
|
|
}
|
|
|
|
/// Display the citation groups.
|
|
fn display_citations(
|
|
&mut self,
|
|
rendered: &hayagriva::Rendered,
|
|
) -> HashMap<Location, SourceResult<Content>> {
|
|
// Determine for each citation key where in the bibliography it is,
|
|
// so that we can link there.
|
|
let mut links = HashMap::new();
|
|
if let Some(bibliography) = &rendered.bibliography {
|
|
let location = self.bibliography.0.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.0.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
|
|
}
|
|
|
|
/// Display the bibliography references.
|
|
fn display_references(
|
|
&self,
|
|
rendered: &hayagriva::Rendered,
|
|
) -> Option<Vec<(Option<Content>, Content)>> {
|
|
let rendered = rendered.bibliography.as_ref()?;
|
|
|
|
// Determine for each citation key where it first occured, so that we
|
|
// can link there.
|
|
let mut first_occurances = HashMap::new();
|
|
for info in &self.infos {
|
|
for subinfo in &info.subinfos {
|
|
let key = subinfo.key.0.as_str();
|
|
first_occurances.entry(key).or_insert(info.location);
|
|
}
|
|
}
|
|
|
|
// The location of the bibliography.
|
|
let location = self.bibliography.0.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,
|
|
};
|
|
|
|
// Each reference is assigned a manually created well-known location
|
|
// that is derived from the bibliography's location. This way,
|
|
// citations can link to them.
|
|
let backlink = location.variant(k + 1);
|
|
|
|
// Render the first field.
|
|
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_occurances.get(item.key.as_str()) {
|
|
let dest = Destination::Location(*location);
|
|
content = content.linked(dest);
|
|
}
|
|
content.backlinked(backlink)
|
|
});
|
|
|
|
// Render the main reference content.
|
|
let reference = renderer
|
|
.display_elem_children(&item.content, &mut prefix)
|
|
.backlinked(backlink);
|
|
|
|
output.push((prefix, reference));
|
|
}
|
|
|
|
Some(output)
|
|
}
|
|
}
|
|
|
|
/// Renders hayagriva elements into content.
|
|
struct ElemRenderer<'a> {
|
|
/// The world that is used to evaluate mathematical material.
|
|
world: Tracked<'a, dyn World + 'a>,
|
|
/// The span that is attached to all of the resulting content.
|
|
span: Span,
|
|
/// Resolves the supplement of i-th citation in the request.
|
|
supplement: &'a dyn Fn(usize) -> Option<Content>,
|
|
/// Resolves where the i-th citation in the request should link to.
|
|
link: &'a dyn Fn(usize) -> Option<Location>,
|
|
}
|
|
|
|
impl ElemRenderer<'_> {
|
|
/// Display rendered hayagriva elements.
|
|
///
|
|
/// The `prefix` can be a separate content storage where `left-margin`
|
|
/// elements will be accumulated into.
|
|
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)),
|
|
)
|
|
}
|
|
|
|
/// Display a rendered hayagriva element.
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Display a block-level element.
|
|
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![prefix, content])
|
|
.with_columns(TrackSizings(vec![Sizing::Auto; 2]))
|
|
.with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()]))
|
|
.pack();
|
|
}
|
|
|
|
match elem.display {
|
|
Some(Display::Block) => {
|
|
content = BlockElem::new().with_body(Some(content)).pack();
|
|
}
|
|
Some(Display::Indent) => {
|
|
content = PadElem::new(content).pack();
|
|
}
|
|
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
|
|
}
|
|
|
|
/// Display math.
|
|
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))
|
|
}
|
|
|
|
/// Display a link.
|
|
fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> Content {
|
|
let dest = Destination::Url(url.into());
|
|
self.display_formatted(text).linked(dest)
|
|
}
|
|
|
|
/// Display transparent pass-through content.
|
|
fn display_transparent(&self, i: usize, format: &hayagriva::Formatting) -> Content {
|
|
let content = (self.supplement)(i).unwrap_or_default();
|
|
apply_formatting(content, format)
|
|
}
|
|
|
|
/// Display formatted hayagriva text as content.
|
|
fn display_formatted(&self, formatted: &hayagriva::Formatted) -> Content {
|
|
let content = TextElem::packed(formatted.text.as_str()).spanned(self.span);
|
|
apply_formatting(content, &formatted.formatting)
|
|
}
|
|
}
|
|
|
|
/// Applies formatting to content.
|
|
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(Delta(300)));
|
|
}
|
|
citationberg::FontWeight::Light => {
|
|
content = content.styled(TextElem::set_delta(Delta(-100)));
|
|
}
|
|
}
|
|
|
|
match format.text_decoration {
|
|
citationberg::TextDecoration::None => {}
|
|
citationberg::TextDecoration::Underline => {
|
|
content = content.underlined();
|
|
}
|
|
}
|
|
|
|
match format.vertical_align {
|
|
citationberg::VerticalAlign::None => {}
|
|
citationberg::VerticalAlign::Baseline => {}
|
|
citationberg::VerticalAlign::Sup => {
|
|
// Add zero-width weak spacing to make the superscript "sticky".
|
|
content = HElem::hole().pack() + SuperElem::new(content).pack();
|
|
}
|
|
citationberg::VerticalAlign::Sub => {
|
|
content = HElem::hole().pack() + SubElem::new(content).pack();
|
|
}
|
|
}
|
|
|
|
content
|
|
}
|
|
|
|
/// Create a locale code from language and optionally region.
|
|
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)
|
|
}
|