diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/typst-library/Cargo.toml | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/mod.rs | 62 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/spacing.rs | 7 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/bibliography.rs | 1231 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/cite.rs | 156 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/footnote.rs | 40 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/mod.rs | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/reference.rs | 5 | ||||
| -rw-r--r-- | crates/typst-library/src/text/quote.rs | 52 | ||||
| -rw-r--r-- | crates/typst/src/model/styles.rs | 5 |
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 |
