summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-10-31 01:33:06 +0100
committerLaurenz <laurmaedje@gmail.com>2023-10-31 01:33:06 +0100
commitab5dd2a1cb5e6e038f92fbdca0996f602e42f16a (patch)
tree90de80cbf5b43c0f525dab20a4b3e901a8933fce /crates
parent27ab2bb9a2e49bd6b8190eb39b18957b27aca222 (diff)
CSL support
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-library/Cargo.toml3
-rw-r--r--crates/typst-library/src/layout/mod.rs62
-rw-r--r--crates/typst-library/src/layout/spacing.rs7
-rw-r--r--crates/typst-library/src/meta/bibliography.rs1231
-rw-r--r--crates/typst-library/src/meta/cite.rs156
-rw-r--r--crates/typst-library/src/meta/footnote.rs40
-rw-r--r--crates/typst-library/src/meta/mod.rs2
-rw-r--r--crates/typst-library/src/meta/reference.rs5
-rw-r--r--crates/typst-library/src/text/quote.rs52
-rw-r--r--crates/typst/src/model/styles.rs5
10 files changed, 1040 insertions, 523 deletions
diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml
index 34214eb5..b348596b 100644
--- a/crates/typst-library/Cargo.toml
+++ b/crates/typst-library/Cargo.toml
@@ -23,13 +23,14 @@ chinese-number = { version = "0.7.2", default-features = false, features = ["num
comemo = "0.3"
csv = "1"
ecow = { version = "0.2", features = ["serde"] }
-hayagriva = "0.3.2"
+hayagriva = "0.4"
hypher = "0.1.4"
icu_properties = { version = "1.3", features = ["serde"] }
icu_provider = { version = "1.3", features = ["sync"] }
icu_provider_adapters = "1.3"
icu_provider_blob = "1.3"
icu_segmenter = { version = "1.3", features = ["serde"] }
+indexmap = "2"
kurbo = "0.9"
lipsum = "0.9"
log = "0.4"
diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs
index 4ff0bc92..2ff4c928 100644
--- a/crates/typst-library/src/layout/mod.rs
+++ b/crates/typst-library/src/layout/mod.rs
@@ -51,11 +51,10 @@ use std::mem;
use typed_arena::Arena;
use typst::diag::SourceResult;
use typst::eval::Tracer;
-use typst::model::DelayedErrors;
-use typst::model::{applicable, realize, StyleVecBuilder};
+use typst::model::{applicable, realize, DelayedErrors, StyleVecBuilder};
use crate::math::{EquationElem, LayoutMath};
-use crate::meta::DocumentElem;
+use crate::meta::{CiteElem, CiteGroup, DocumentElem};
use crate::prelude::*;
use crate::shared::BehavedBuilder;
use crate::text::{LinebreakElem, SmartquoteElem, SpaceElem, TextElem};
@@ -302,6 +301,8 @@ struct Builder<'a, 'v, 't> {
par: ParBuilder<'a>,
/// The current list building state.
list: ListBuilder<'a>,
+ /// The current citation grouping state.
+ cites: CiteGroupBuilder<'a>,
}
/// Temporary storage arenas for building.
@@ -322,6 +323,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
flow: FlowBuilder::default(),
par: ParBuilder::default(),
list: ListBuilder::default(),
+ cites: CiteGroupBuilder::default(),
}
}
@@ -351,6 +353,12 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
return Ok(());
}
+ if self.cites.accept(content, styles) {
+ return Ok(());
+ }
+
+ self.interrupt_cites()?;
+
if self.list.accept(content, styles) {
return Ok(());
}
@@ -438,7 +446,21 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
Ok(())
}
+ fn interrupt_cites(&mut self) -> SourceResult<()> {
+ if !self.cites.items.is_empty() {
+ let staged = mem::take(&mut self.cites.staged);
+ let (group, styles) = mem::take(&mut self.cites).finish();
+ let stored = self.scratch.content.alloc(group);
+ self.accept(stored, styles)?;
+ for (content, styles) in staged {
+ self.accept(content, styles)?;
+ }
+ }
+ Ok(())
+ }
+
fn interrupt_list(&mut self) -> SourceResult<()> {
+ self.interrupt_cites()?;
if !self.list.items.is_empty() {
let staged = mem::take(&mut self.list.staged);
let (list, styles) = mem::take(&mut self.list).finish();
@@ -713,3 +735,37 @@ impl Default for ListBuilder<'_> {
}
}
}
+
+/// Accepts citations.
+#[derive(Default)]
+struct CiteGroupBuilder<'a> {
+ /// The citations.
+ items: StyleVecBuilder<'a, CiteElem>,
+ /// Trailing content for which it is unclear whether it is part of the list.
+ staged: Vec<(&'a Content, StyleChain<'a>)>,
+}
+
+impl<'a> CiteGroupBuilder<'a> {
+ fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
+ if !self.items.is_empty()
+ && (content.is::<SpaceElem>() || content.is::<MetaElem>())
+ {
+ self.staged.push((content, styles));
+ return true;
+ }
+
+ if let Some(citation) = content.to::<CiteElem>() {
+ self.items.push(citation.clone(), styles);
+ return true;
+ }
+
+ false
+ }
+
+ fn finish(self) -> (Content, StyleChain<'a>) {
+ let (items, styles) = self.items.finish();
+ let items = items.into_items();
+ let span = items.first().map(|cite| cite.span()).unwrap_or(Span::detached());
+ (CiteGroup::new(items).pack().spanned(span), styles)
+ }
+}
diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs
index 9af54237..37f1ed35 100644
--- a/crates/typst-library/src/layout/spacing.rs
+++ b/crates/typst-library/src/layout/spacing.rs
@@ -51,6 +51,13 @@ pub struct HElem {
pub weak: bool,
}
+impl HElem {
+ /// Zero-width horizontal weak spacing that eats surrounding spaces.
+ pub fn hole() -> Self {
+ Self::new(Abs::zero().into()).with_weak(true)
+ }
+}
+
impl Behave for HElem {
fn behaviour(&self) -> Behaviour {
if self.amount().is_fractional() {
diff --git a/crates/typst-library/src/meta/bibliography.rs b/crates/typst-library/src/meta/bibliography.rs
index 0521a9c7..d429d343 100644
--- a/crates/typst-library/src/meta/bibliography.rs
+++ b/crates/typst-library/src/meta/bibliography.rs
@@ -1,21 +1,33 @@
use std::collections::HashMap;
use std::ffi::OsStr;
+use std::hash::{Hash, Hasher};
use std::path::Path;
use std::sync::Arc;
-use ecow::{eco_vec, EcoVec};
-use hayagriva::io::{BibLaTeXError, YamlBibliographyError};
-use hayagriva::style::{self, Brackets, Citation, Database, DisplayString, Formatting};
-use hayagriva::Entry;
+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::Bytes;
+use typst::eval::{eval_string, Bytes, CastInfo, EvalMode, Reflect};
+use typst::font::FontStyle;
use typst::util::option_eq;
-use super::{LinkElem, LocalName, RefElem};
-use crate::layout::{BlockElem, GridElem, ParElem, Sizing, TrackSizings, VElem};
+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::TextElem;
+use crate::text::{Delta, SubElem, SuperElem, TextElem};
/// A bibliography / reference listing.
///
@@ -30,49 +42,45 @@ use crate::text::TextElem;
///
/// 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
+/// [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 ...
-/// #cite("arrgh", "netwok").
+/// @arrgh @netwok.
///
/// #bibliography("works.bib")
/// ```
#[elem(Locatable, Synthesize, Show, Finalize, LocalName)]
pub struct BibliographyElem {
- /// Path to a Hayagriva `.yml` or BibLaTeX `.bib` file.
+ /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files.
#[required]
#[parse(
- 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>>>()?;
-
- // Check that parsing works.
- let _ = load(&paths, &data).at(span)?;
-
+ let (paths, bibliography) = Bibliography::parse(vm, args)?;
paths
)]
pub path: BibPaths,
- /// The raw file buffers.
- #[internal]
- #[required]
- #[parse(data)]
- pub data: Vec<Bytes>,
-
/// The title of the bibliography.
///
/// - When set to `{auto}`, an appropriate title for the
@@ -86,9 +94,39 @@ pub struct BibliographyElem {
#[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.
- #[default(BibliographyStyle::Ieee)]
- pub style: BibliographyStyle,
+ ///
+ /// 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.
@@ -112,7 +150,7 @@ impl BibliographyElem {
};
if iter.next().is_some() {
- bail!("multiple bibliographies are not supported");
+ bail!("multiple bibliographies are not yet supported");
}
Ok(elem.to::<Self>().unwrap().clone())
@@ -123,35 +161,32 @@ impl BibliographyElem {
vt.introspector
.query(&Self::elem().select())
.iter()
- .flat_map(|elem| {
- let elem = elem.to::<Self>().unwrap();
- load(&elem.path(), &elem.data())
- })
- .flatten()
- .any(|entry| entry.key() == key)
+ .any(|elem| elem.to::<Self>().unwrap().bibliography().has(key))
}
/// Find all bibliography keys.
pub fn keys(
introspector: Tracked<Introspector>,
) -> Vec<(EcoString, Option<EcoString>)> {
- Self::find(introspector)
- .and_then(|elem| load(&elem.path(), &elem.data()))
- .iter()
- .flatten()
- .map(|entry| {
+ 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.canonical.value.as_str().into());
- (key, detail)
- })
- .collect()
+ 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(())
}
}
@@ -177,12 +212,18 @@ impl Show for BibliographyElem {
}
Ok(vt.delayed(|vt| {
- let works = Works::new(vt).at(self.span())?;
+ 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 works.references.iter().any(|(prefix, _)| prefix.is_some()) {
+ if references.iter().any(|(prefix, _)| prefix.is_some()) {
let mut cells = vec![];
- for (prefix, reference) in &works.references {
+ for (prefix, reference) in references {
cells.push(prefix.clone().unwrap_or_default());
cells.push(reference.clone());
}
@@ -196,26 +237,28 @@ impl Show for BibliographyElem {
.pack(),
);
} else {
- let mut entries = vec![];
- for (_, reference) in &works.references {
- entries.push(VElem::new(row_gutter).with_weakness(3).pack());
- entries.push(reference.clone());
+ for (_, reference) in references {
+ seq.push(VElem::new(row_gutter).with_weakness(3).pack());
+ seq.push(reference.clone());
}
+ }
- seq.push(
- Content::sequence(entries)
- .styled(ParElem::set_hanging_indent(INDENT.into())),
- );
+ let mut content = Content::sequence(seq);
+ if works.hanging_indent {
+ content = content.styled(ParElem::set_hanging_indent(INDENT.into()));
}
- Ok(Content::sequence(seq))
+ Ok(content)
}))
}
}
impl Finalize for BibliographyElem {
fn finalize(&self, realized: Content, _: StyleChain) -> Content {
- realized.styled(HeadingElem::set_numbering(None))
+ const INDENT: Em = Em::new(1.0);
+ realized
+ .styled(HeadingElem::set_numbering(None))
+ .styled(PadElem::set_left(INDENT.into()))
}
}
@@ -253,470 +296,738 @@ impl LocalName for BibliographyElem {
}
}
-/// A bibliography style.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum BibliographyStyle {
- /// Follows guidance of the American Psychological Association. Based on the
- /// 7th edition of the APA Publication Manual.
- Apa,
- /// The Chicago Author Date style. Based on the 17th edition of the Chicago
- /// Manual of Style, Chapter 15.
- ChicagoAuthorDate,
- /// The Chicago Notes style. Based on the 17th edition of the Chicago
- /// Manual of Style, Chapter 14.
- ChicagoNotes,
- /// The style of the Institute of Electrical and Electronics Engineers.
- /// Based on the 2018 IEEE Reference Guide.
- Ieee,
- /// Follows guidance of the Modern Language Association. Based on the 8th
- /// edition of the MLA Handbook.
- Mla,
+/// A loaded bibliography.
+#[ty]
+#[derive(Debug, Clone, PartialEq)]
+pub struct Bibliography {
+ map: Arc<IndexMap<EcoString, hayagriva::Entry>>,
+ hash: u128,
}
-impl BibliographyStyle {
- /// The default citation style for this bibliography style.
- pub fn default_citation_style(self) -> CitationStyle {
- match self {
- Self::Apa => CitationStyle::ChicagoAuthorDate,
- Self::ChicagoAuthorDate => CitationStyle::ChicagoAuthorDate,
- Self::ChicagoNotes => CitationStyle::ChicagoNotes,
- Self::Ieee => CitationStyle::Numerical,
- Self::Mla => CitationStyle::ChicagoAuthorDate,
+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),
+ })
}
-}
-/// Cite a work from the bibliography.
-///
-/// Before you starting citing, you need to add a [bibliography]($bibliography)
-/// somewhere in your document.
-///
-/// # Example
-/// ```example
-/// This was already noted by
-/// pirates long ago. @arrgh
-///
-/// Multiple sources say ...
-/// #cite("arrgh", "netwok").
-///
-/// #bibliography("works.bib")
-/// ```
-///
-/// # Syntax
-/// This function indirectly has dedicated syntax. [References]($ref) can be
-/// used to cite works from the bibliography. The label then corresponds to the
-/// citation key.
-#[elem(Locatable, Synthesize, Show)]
-pub struct CiteElem {
- /// The citation keys that identify the elements that shall be cited in
- /// the bibliography.
- ///
- /// Reference syntax supports only a single key.
- #[variadic]
- pub keys: Vec<EcoString>,
+ fn has(&self, key: &str) -> bool {
+ self.map.contains_key(key)
+ }
- /// A supplement for the citation such as page or chapter number.
- ///
- /// In reference syntax, the supplement can be added in square brackets:
- ///
- /// ```example
- /// This has been proven over and
- /// over again. @distress[p.~7]
- ///
- /// #bibliography("works.bib")
- /// ```
- pub supplement: Option<Content>,
+ fn entries(&self) -> impl Iterator<Item = &hayagriva::Entry> {
+ self.map.values()
+ }
+}
- /// Whether the citation should include brackets.
- ///
- /// ```example
- /// #set cite(brackets: false)
- ///
- /// @netwok follow these methods
- /// in their work ...
- ///
- /// #bibliography(
- /// "works.bib",
- /// style: "chicago-author-date",
- /// )
- /// ```
- #[default(true)]
- pub brackets: bool,
-
- /// The citation style.
- ///
- /// When set to `{auto}`, automatically picks the preferred citation style
- /// for the bibliography's style.
- ///
- /// ```example
- /// #set cite(style: "alphanumerical")
- /// Alphanumerical references.
- /// @netwok
- ///
- /// #bibliography("works.bib")
- /// ```
- pub style: Smart<CitationStyle>,
+impl Hash for Bibliography {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.hash.hash(state);
+ }
}
-impl Synthesize for CiteElem {
- fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- self.push_supplement(self.supplement(styles));
- self.push_brackets(self.brackets(styles));
- self.push_style(self.style(styles));
- Ok(())
+impl Repr for Bibliography {
+ fn repr(&self) -> EcoString {
+ "..".into()
}
}
-impl Show for CiteElem {
- #[tracing::instrument(name = "CiteElem::show", skip(self, vt))]
- fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- Ok(vt.delayed(|vt| {
- let works = Works::new(vt).at(self.span())?;
- let location = self.0.location().unwrap();
- works
- .citations
- .get(&location)
- .cloned()
- .flatten()
- .ok_or("bibliography does not contain this key")
- .at(self.span())
+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()
+ }
}
-cast! {
- CiteElem,
- v: Content => v.to::<Self>().cloned().ok_or("expected citation")?,
+// 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))
+ }
}
-/// A citation style.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum CitationStyle {
- /// IEEE-style numerical reference markers.
- Numerical,
- /// A simple alphanumerical style. For example, the output could be Rass97
- /// or MKG+21.
- Alphanumerical,
- /// Citations that just consist of the entry keys.
- Keys,
- /// The Chicago Author Date style. Based on the 17th edition of the Chicago
- /// Manual of Style, Chapter 15.
- ChicagoAuthorDate,
- /// The Chicago Notes style. Based on the 17th edition of the Chicago
- /// Manual of Style, Chapter 14.
- ChicagoNotes,
- /// A Chicago-like author-title format. Results could look like this:
- /// Prokopov, “It Is Fast or It Is Wrong”.
- ChicagoAuthorTitle,
+impl IntoValue for CslStyle {
+ fn into_value(self) -> Value {
+ Value::dynamic(self)
+ }
}
-impl CitationStyle {
- fn is_short(self) -> bool {
- matches!(self, Self::Numerical | Self::Alphanumerical | Self::Keys)
+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.
-#[derive(Default)]
-struct Works {
- citations: HashMap<Location, Option<Content>>,
- references: Vec<(Option<Content>, Content)>,
+/// 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 {
- /// Prepare all things need to cite a work or format a bibliography.
- fn new(vt: &Vt) -> StrResult<Arc<Self>> {
- let bibliography = BibliographyElem::find(vt.introspector)?;
- let citations = vt
- .introspector
- .query(&Selector::Or(eco_vec![
- RefElem::elem().select(),
- CiteElem::elem().select(),
- ]))
- .into_iter()
- .map(|elem| match elem.to::<RefElem>() {
- Some(reference) => reference.citation().unwrap(),
- _ => elem.to::<CiteElem>().unwrap().clone(),
- })
- .collect();
- Ok(create(bibliography, citations))
+ /// 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))
}
}
-/// Generate all citations and the whole bibliography.
-#[comemo::memoize]
-fn create(bibliography: BibliographyElem, citations: Vec<CiteElem>) -> Arc<Works> {
- let span = bibliography.span();
- let entries = load(&bibliography.path(), &bibliography.data()).unwrap();
- let style = bibliography.style(StyleChain::default());
- let bib_location = bibliography.0.location().unwrap();
- let ref_location = |target: &Entry| {
- let i = entries
- .iter()
- .position(|entry| entry.key() == target.key())
- .unwrap_or_default();
- bib_location.variant(i)
- };
-
- let mut db = Database::new();
- let mut ids = HashMap::new();
- let mut preliminary = vec![];
-
- for citation in citations {
- let cite_id = citation.0.location().unwrap();
- let entries = citation
- .keys()
- .into_iter()
- .map(|key| {
- let entry = entries.iter().find(|entry| entry.key() == key)?;
- ids.entry(entry.key()).or_insert(cite_id);
- db.push(entry);
- Some(entry)
- })
- .collect::<Option<Vec<_>>>();
- preliminary.push((citation, entries));
- }
-
- let mut current = CitationStyle::Numerical;
- let mut citation_style: Box<dyn style::CitationStyle> =
- Box::new(style::Numerical::new());
-
- let citations = preliminary
- .into_iter()
- .map(|(citation, cited)| {
- let location = citation.0.location().unwrap();
- let Some(cited) = cited else { return (location, None) };
-
- let mut supplement = citation.supplement(StyleChain::default());
- let brackets = citation.brackets(StyleChain::default());
- let style = citation
- .style(StyleChain::default())
- .unwrap_or(style.default_citation_style());
-
- if style != current {
- current = style;
- citation_style = match style {
- CitationStyle::Numerical => Box::new(style::Numerical::new()),
- CitationStyle::Alphanumerical => {
- Box::new(style::Alphanumerical::new())
- }
- CitationStyle::ChicagoAuthorDate => {
- Box::new(style::ChicagoAuthorDate::new())
- }
- CitationStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()),
- CitationStyle::ChicagoAuthorTitle => {
- Box::new(style::AuthorTitle::new())
- }
- CitationStyle::Keys => Box::new(style::Keys::new()),
- };
- }
+/// 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>>,
+}
- let len = cited.len();
- let mut content = Content::empty();
- for (i, entry) in cited.into_iter().enumerate() {
- let supplement = if i + 1 == len { supplement.take() } else { None };
- let mut display = db
- .citation(
- &mut *citation_style,
- &[Citation {
- entry,
- supplement: supplement.is_some().then_some(SUPPLEMENT),
- }],
- )
- .display;
+/// 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]>,
+}
- if style.is_short() {
- display.value = display.value.replace(' ', "\u{a0}");
- }
+/// 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,
+}
- if brackets && len == 1 {
- display = display.with_default_brackets(&*citation_style);
- }
+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(),
+ })
+ }
- if i > 0 {
- content += TextElem::packed(",\u{a0}");
- }
+ /// 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;
+ };
- // Format and link to the reference entry.
- content += format_display_string(&display, supplement, citation.span())
- .linked(Destination::Location(ref_location(entry)));
- }
+ let supplement = child.supplement(StyleChain::default());
+ let locator = supplement.as_ref().map(|_| {
+ SpecificLocator(
+ citationberg::taxonomy::Locator::Custom,
+ hayagriva::LocatorPayload::Transparent,
+ )
+ });
- if brackets && len > 1 {
- content = match citation_style.brackets() {
- Brackets::None => content,
- Brackets::Round => {
- TextElem::packed('(') + content + TextElem::packed(')')
- }
- Brackets::Square => {
- TextElem::packed('[') + content + TextElem::packed(']')
+ 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 style == CitationStyle::ChicagoNotes {
- content = FootnoteElem::with_content(content).pack();
+ if !errors.is_empty() {
+ self.failures.insert(location, Err(errors));
+ continue;
}
- (location, Some(content))
- })
- .collect();
-
- let bibliography_style: Box<dyn style::BibliographyStyle> = match style {
- BibliographyStyle::Apa => Box::new(style::Apa::new()),
- BibliographyStyle::ChicagoAuthorDate => Box::new(style::ChicagoAuthorDate::new()),
- BibliographyStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()),
- BibliographyStyle::Ieee => Box::new(style::Ieee::new()),
- BibliographyStyle::Mla => Box::new(style::Mla::new()),
- };
+ let style = match first.style(StyleChain::default()) {
+ Smart::Auto => &bibliography_style.style,
+ Smart::Custom(style) => styles.alloc(style.style),
+ };
- let references = db
- .bibliography(&*bibliography_style, None)
- .into_iter()
- .map(|reference| {
- let backlink = ref_location(reference.entry);
- let prefix = reference.prefix.map(|prefix| {
- // Format and link to first citation.
- let bracketed = prefix.with_default_brackets(&*citation_style);
- format_display_string(&bracketed, None, span)
- .linked(Destination::Location(ids[reference.entry.key()]))
- .backlinked(backlink)
+ self.infos.push(GroupInfo {
+ location,
+ subinfos,
+ span: first.span(),
+ footnote: normal
+ && style.settings.class == citationberg::StyleClass::Note,
});
- let mut reference = format_display_string(&reference.display, None, span);
- if prefix.is_none() {
- reference = reference.backlinked(backlink);
+ 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,
+ ));
}
+ }
- (prefix, reference)
+ driver.finish(BibliographyRequest {
+ style: bibliography_style.get(),
+ locale: Some(locale),
+ locale_files: &LOCALES,
})
- .collect();
+ }
- Arc::new(Works { citations, references })
-}
+ /// 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 })
+ }
-/// Load bibliography entries from a path.
-#[comemo::memoize]
-fn load(paths: &BibPaths, data: &[Bytes]) -> StrResult<EcoVec<hayagriva::Entry>> {
- let mut result = EcoVec::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 entries = parse_bib(path, src)?;
- result.extend(entries);
- }
-
- // Biblatex only checks for duplicate keys within files
- // -> We have to do this between files again
- let mut keys = result.iter().map(|r| r.key()).collect::<Vec<_>>();
- keys.sort_unstable();
- // Waiting for `slice_partition_dedup` #54279
- let mut duplicates = Vec::new();
- for pair in keys.windows(2) {
- if pair[0] == pair[1] {
- duplicates.push(pair[0]);
+ /// 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));
}
- }
- if !duplicates.is_empty() {
- Err(eco_format!("duplicate bibliography keys: {}", duplicates.join(", ")))
- } else {
- Ok(result)
+ output
}
-}
-/// Parse a bibliography file (bib/yml/yaml)
-fn parse_bib(path_str: &str, src: &str) -> StrResult<Vec<hayagriva::Entry>> {
- let path = Path::new(path_str);
- let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default();
- match ext.to_lowercase().as_str() {
- "yml" | "yaml" => {
- hayagriva::io::from_yaml_str(src).map_err(format_hayagriva_error)
+ /// 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));
}
- "bib" => hayagriva::io::from_biblatex_str(src).map_err(|err| {
- err.into_iter()
- .next()
- .map(|error| format_biblatex_error(path_str, src, error))
- .unwrap_or_else(|| eco_format!("failed to parse {path_str}"))
- }),
- _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"),
+
+ Some(output)
}
}
-/// Format a Hayagriva loading error.
-fn format_hayagriva_error(err: YamlBibliographyError) -> EcoString {
- eco_format!("{err}")
+/// 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>,
}
-/// Format a BibLaTeX loading error.
-fn format_biblatex_error(path: &str, src: &str, error: BibLaTeXError) -> EcoString {
- 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!("parsing failed at {path}:{line}: {msg}")
-}
+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)),
+ )
+ }
-/// Hayagriva only supports strings, but we have a content supplement. To deal
-/// with this, we pass this string to hayagriva instead of our content, find it
-/// in the output and replace it with the content.
-const SUPPLEMENT: &str = "cdc579c45cf3d648905c142c7082683f";
+ /// 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)
+ }
+ }
+ }
-/// Format a display string into content.
-fn format_display_string(
- string: &DisplayString,
- mut supplement: Option<Content>,
- span: Span,
-) -> Content {
- let mut stops: Vec<_> = string
- .formatting
- .iter()
- .flat_map(|(range, _)| [range.start, range.end])
- .collect();
-
- if let Some(i) = string.value.find(SUPPLEMENT) {
- stops.push(i);
- stops.push(i + SUPPLEMENT.len());
- }
-
- stops.sort();
- stops.dedup();
- stops.push(string.value.len());
-
- let mut start = 0;
- let mut seq = vec![];
- for stop in stops {
- let segment = string.value.get(start..stop).unwrap_or_default();
- if segment.is_empty() {
- continue;
+ /// 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();
}
- let mut content = if segment == SUPPLEMENT && supplement.is_some() {
- supplement.take().unwrap_or_default()
- } else {
- TextElem::packed(segment).spanned(span)
- };
+ 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();
+ }
+ _ => {}
+ }
- for (range, fmt) in &string.formatting {
- if !range.contains(&start) {
- continue;
+ 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 = match fmt {
- Formatting::Bold => content.strong(),
- Formatting::Italic => content.emph(),
- Formatting::Link(link) => {
- LinkElem::new(Destination::Url(link.as_str().into()).into(), content)
- .pack()
- }
- };
+ 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)));
}
+ }
- seq.push(content);
- start = stop;
+ match format.text_decoration {
+ citationberg::TextDecoration::None => {}
+ citationberg::TextDecoration::Underline => {
+ content = content.underlined();
+ }
}
- Content::sequence(seq)
+ 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)
}
diff --git a/crates/typst-library/src/meta/cite.rs b/crates/typst-library/src/meta/cite.rs
new file mode 100644
index 00000000..2e3c4155
--- /dev/null
+++ b/crates/typst-library/src/meta/cite.rs
@@ -0,0 +1,156 @@
+use super::bibliography::Works;
+use super::CslStyle;
+use crate::prelude::*;
+use crate::text::TextElem;
+
+/// Cite a work from the bibliography.
+///
+/// Before you starting citing, you need to add a [bibliography]($bibliography)
+/// somewhere in your document.
+///
+/// # Example
+/// ```example
+/// This was already noted by
+/// pirates long ago. @arrgh
+///
+/// Multiple sources say ...
+/// @arrgh @netwok.
+///
+/// You can also call `cite`
+/// explicitly. #cite(<arrgh>)
+///
+/// #bibliography("works.bib")
+/// ```
+///
+/// # Syntax
+/// This function indirectly has dedicated syntax. [References]($ref) can be
+/// used to cite works from the bibliography. The label then corresponds to the
+/// citation key.
+#[elem(Synthesize)]
+pub struct CiteElem {
+ /// The citation key that identifies the entry in the bibliography that
+ /// shall be cited, as a label.
+ ///
+ /// ```example
+ /// // All the same
+ /// @netwok \
+ /// #cite(<netwok>) \
+ /// #cite(label("netwok"))
+ /// >>> #set text(0pt)
+ /// >>> #bibliography("works.bib", style: "apa")
+ /// ```
+ #[required]
+ pub key: Label,
+
+ /// A supplement for the citation such as page or chapter number.
+ ///
+ /// In reference syntax, the supplement can be added in square brackets:
+ ///
+ /// ```example
+ /// This has been proven. @distress[p.~7]
+ ///
+ /// #bibliography("works.bib")
+ /// ```
+ pub supplement: Option<Content>,
+
+ /// The kind of citation to produce. Different forms are useful in different
+ /// scenarios: A normal citation is useful as a source at the end of a
+ /// sentence, while a "prose" citation is more suitable for inclusion in the
+ /// flow of text.
+ ///
+ /// If set to `{none}`, the cited work is included in the bibliography, but
+ /// nothing will be displayed.
+ ///
+ /// ```example
+ /// #cite(<netwok>, form: "prose")
+ /// show the outsized effects of
+ /// pirate life on the human psyche.
+ /// >>> #set text(0pt)
+ /// >>> #bibliography("works.bib", style: "apa")
+ /// ```
+ #[default(Some(CitationForm::Normal))]
+ pub form: Option<CitationForm>,
+
+ /// The citation style.
+ ///
+ /// Should be either `{auto}`, 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.
+ ///
+ /// When set to `{auto}`, automatically use the
+ /// [bibliography's style]($bibliography.style) for the citations.
+ #[parse(CslStyle::parse_smart(vm, args)?)]
+ pub style: Smart<CslStyle>,
+
+ /// The text language setting where the citation is.
+ #[internal]
+ #[synthesized]
+ pub lang: Lang,
+
+ /// The text region setting where the citation is.
+ #[internal]
+ #[synthesized]
+ pub region: Option<Region>,
+}
+
+impl Synthesize for CiteElem {
+ fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ self.push_supplement(self.supplement(styles));
+ self.push_form(self.form(styles));
+ self.push_style(self.style(styles));
+ self.push_lang(TextElem::lang_in(styles));
+ self.push_region(TextElem::region_in(styles));
+ Ok(())
+ }
+}
+
+cast! {
+ CiteElem,
+ v: Content => v.to::<Self>().cloned().ok_or("expected citation")?,
+}
+
+/// The form of the citation.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum CitationForm {
+ /// Display in the standard way for the active style.
+ #[default]
+ Normal,
+ /// Produces a citation that is suitable for inclusion in a sentence.
+ Prose,
+ /// Mimics a bibliography entry, with full information about the cited work.
+ Full,
+ /// Shows only the cited work's author(s).
+ Author,
+ /// Shows only the cited work's year.
+ Year,
+}
+
+/// A group of citations.
+///
+/// This is automatically created from adjacent citations during show rule
+/// application.
+#[elem(Locatable, Show)]
+pub struct CiteGroup {
+ /// The citations.
+ #[required]
+ pub children: Vec<CiteElem>,
+}
+
+impl Show for CiteGroup {
+ #[tracing::instrument(name = "CiteGroup::show", skip(self, vt))]
+ fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
+ Ok(vt.delayed(|vt| {
+ let location = self.0.location().unwrap();
+ let span = self.span();
+ Works::generate(vt.world, vt.introspector)
+ .at(span)?
+ .citations
+ .get(&location)
+ .cloned()
+ .unwrap_or_else(|| {
+ bail!(span, "failed to format citation (this is a bug)")
+ })
+ }))
+ }
+}
diff --git a/crates/typst-library/src/meta/footnote.rs b/crates/typst-library/src/meta/footnote.rs
index ed7242bb..a8f0b4dd 100644
--- a/crates/typst-library/src/meta/footnote.rs
+++ b/crates/typst-library/src/meta/footnote.rs
@@ -8,24 +8,6 @@ use crate::prelude::*;
use crate::text::{SuperElem, TextElem, TextSize};
use crate::visualize::LineElem;
-/// The body of a footnote can be either some content or a label referencing
-/// another footnote.
-#[derive(Debug)]
-pub enum FootnoteBody {
- Content(Content),
- Reference(Label),
-}
-
-cast! {
- FootnoteBody,
- self => match self {
- Self::Content(v) => v.into_value(),
- Self::Reference(v) => v.into_value(),
- },
- v: Content => Self::Content(v),
- v: Label => Self::Reference(v),
-}
-
/// A footnote.
///
/// Includes additional remarks and references on the same page with footnotes.
@@ -147,9 +129,9 @@ impl Show for FootnoteElem {
let counter = Counter::of(Self::elem());
let num = counter.at(vt, loc)?.display(vt, &numbering)?;
let sup = SuperElem::new(num).pack();
- let hole = HElem::new(Abs::zero().into()).with_weak(true).pack();
let loc = loc.variant(1);
- Ok(hole + sup.linked(Destination::Location(loc)))
+ // Add zero-width weak spacing to make the footnote "sticky".
+ Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc)))
}))
}
}
@@ -160,6 +142,24 @@ impl Count for FootnoteElem {
}
}
+/// The body of a footnote can be either some content or a label referencing
+/// another footnote.
+#[derive(Debug)]
+pub enum FootnoteBody {
+ Content(Content),
+ Reference(Label),
+}
+
+cast! {
+ FootnoteBody,
+ self => match self {
+ Self::Content(v) => v.into_value(),
+ Self::Reference(v) => v.into_value(),
+ },
+ v: Content => Self::Content(v),
+ v: Label => Self::Reference(v),
+}
+
/// An entry in a footnote list.
///
/// This function is not intended to be called directly. Instead, it is used
diff --git a/crates/typst-library/src/meta/mod.rs b/crates/typst-library/src/meta/mod.rs
index 659cb5a3..1019864f 100644
--- a/crates/typst-library/src/meta/mod.rs
+++ b/crates/typst-library/src/meta/mod.rs
@@ -1,6 +1,7 @@
//! Interaction between document parts.
mod bibliography;
+mod cite;
mod context;
mod counter;
mod document;
@@ -18,6 +19,7 @@ mod reference;
mod state;
pub use self::bibliography::*;
+pub use self::cite::*;
pub use self::context::*;
pub use self::counter::*;
pub use self::document::*;
diff --git a/crates/typst-library/src/meta/reference.rs b/crates/typst-library/src/meta/reference.rs
index 7f05bfcc..12e13929 100644
--- a/crates/typst-library/src/meta/reference.rs
+++ b/crates/typst-library/src/meta/reference.rs
@@ -85,6 +85,9 @@ use crate::text::TextElem;
#[elem(title = "Reference", Synthesize, Locatable, Show)]
pub struct RefElem {
/// The target label that should be referenced.
+ ///
+ /// Can be a label that is defined in the document or an entry from the
+ /// [`bibliography`]($bibliography).
#[required]
pub target: Label,
@@ -222,7 +225,7 @@ impl Show for RefElem {
impl RefElem {
/// Turn the reference into a citation.
pub fn to_citation(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<CiteElem> {
- let mut elem = CiteElem::new(vec![self.target().0]);
+ let mut elem = CiteElem::new(self.target());
elem.0.set_location(self.0.location().unwrap());
elem.synthesize(vt, styles)?;
elem.push_supplement(match self.supplement(styles) {
diff --git a/crates/typst-library/src/text/quote.rs b/crates/typst-library/src/text/quote.rs
index 01d94114..d42dc2f2 100644
--- a/crates/typst-library/src/text/quote.rs
+++ b/crates/typst-library/src/text/quote.rs
@@ -1,6 +1,6 @@
use super::{SmartquoteElem, SpaceElem, TextElem};
use crate::layout::{BlockElem, HElem, PadElem, Spacing, VElem};
-use crate::meta::{BibliographyElem, BibliographyStyle, CiteElem};
+use crate::meta::{CitationForm, CiteElem};
use crate::prelude::*;
/// Displays a quote alongside it's author.
@@ -125,22 +125,16 @@ cast! {
}
impl Show for QuoteElem {
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body();
let block = self.block(styles);
if self.quotes(styles) == Smart::Custom(true) || !block {
- // use h(0pt, weak: true) to make the quotes "sticky"
+ // Add zero-width weak spacing to make the quotes "sticky".
+ let hole = HElem::hole().pack();
let quote = SmartquoteElem::new().with_double(true).pack();
- let weak_h = HElem::new(Spacing::Rel(Rel::zero())).with_weak(true).pack();
-
- realized = Content::sequence([
- quote.clone(),
- weak_h.clone(),
- realized,
- weak_h,
- quote,
- ]);
+ realized =
+ Content::sequence([quote.clone(), hole.clone(), realized, hole, quote]);
}
if block {
@@ -154,41 +148,23 @@ impl Show for QuoteElem {
seq.push(content);
}
Attribution::Label(label) => {
- let citation = vt.delayed(|vt| {
- let citation = CiteElem::new(vec![label.0]);
- let bib =
- BibliographyElem::find(vt.introspector).at(self.span())?;
-
- // TODO: these should use the citation-format attribute, once CSL
- // is implemented and retrieve the authors for non-author formats
- // themselves, see:
- // - https://github.com/typst/typst/pull/2252#issuecomment-1741146989
- // - https://github.com/typst/typst/pull/2252#issuecomment-1744634132
- Ok(match bib.style(styles) {
- // author-date and author
- BibliographyStyle::Apa
- | BibliographyStyle::Mla
- | BibliographyStyle::ChicagoAuthorDate => {
- citation.with_brackets(false).pack()
- }
- // notes, label and numeric
- BibliographyStyle::ChicagoNotes
- | BibliographyStyle::Ieee => citation.pack(),
- })
- });
-
- seq.push(citation);
+ seq.push(
+ CiteElem::new(label)
+ .with_form(Some(CitationForm::Prose))
+ .pack(),
+ );
}
}
- // use v(0.9em, weak: true) bring the attribution closer to the quote
+ // Use v(0.9em, weak: true) bring the attribution closer to the
+ // quote.
let weak_v = VElem::weak(Spacing::Rel(Em::new(0.9).into())).pack();
realized += weak_v + Content::sequence(seq).aligned(Align::END);
}
realized = PadElem::new(realized).pack();
} else if let Some(Attribution::Label(label)) = self.attribution(styles) {
- realized += SpaceElem::new().pack() + CiteElem::new(vec![label.0]).pack();
+ realized += SpaceElem::new().pack() + CiteElem::new(label).pack();
}
Ok(realized)
diff --git a/crates/typst/src/model/styles.rs b/crates/typst/src/model/styles.rs
index 118f5f45..8429fecf 100644
--- a/crates/typst/src/model/styles.rs
+++ b/crates/typst/src/model/styles.rs
@@ -564,6 +564,11 @@ impl<T> StyleVec<T> {
self.items.iter()
}
+ /// Extract the contained items.
+ pub fn into_items(self) -> Vec<T> {
+ self.items
+ }
+
/// Iterate over the contained style lists. Note that zipping this with
/// `items()` does not yield the same result as calling `iter()` because
/// this method only returns lists once that are shared by consecutive