diff --git a/Cargo.lock b/Cargo.lock index b751a1611..99cf937af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,17 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.6" @@ -151,11 +162,10 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "biblatex" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9fd60378277e44cd400ec5f35e768ce0d5a63d8d18ac7b1a9231196251dae5" +checksum = "2e41df82f0d1c4919d946bb0c7c3d179b6071246243d308a1bdee6cfecee3bc7" dependencies = [ - "chrono", "numerals", "paste", "strum", @@ -229,6 +239,28 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytecheck" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.14.0" @@ -345,6 +377,16 @@ dependencies = [ "inout", ] +[[package]] +name = "citationberg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c857faf24e89710f105b623c174508070a9e11e056a749f251ca4c56f59ad88" +dependencies = [ + "quick-xml 0.28.2", + "serde", +] + [[package]] name = "clap" version = "4.4.7" @@ -476,9 +518,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -884,6 +926,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.7", +] [[package]] name = "hashbrown" @@ -893,23 +938,24 @@ checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" [[package]] name = "hayagriva" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065e90e53aa502be868a307f58ca6b46e31143641e809047c689de75619d8cea" +checksum = "c5af3d464a6b5ae882f15fe1da4e696fd96b77fee78ded933e0ad81d1d87cbc5" dependencies = [ "biblatex", - "chrono", - "isolang", - "lazy_static", - "linked-hash-map", + "ciborium", + "citationberg", + "indexmap 2.0.2", + "numerals", "paste", - "regex", - "strum", + "rkyv", + "serde", + "serde_yaml 0.9.27", "thiserror", "unic-langid", "unicode-segmentation", + "unscanny", "url", - "yaml-rust", ] [[package]] @@ -1034,9 +1080,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.3.2" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8bb3b67a8347e94d580434369e5c7ee89999b9309d04b7cfc88dfaa0f31b59" +checksum = "98507b488098f45eb95ef495612a2012e4d8ad6095dda86cb2f1728aa2204a60" [[package]] name = "icu_provider" @@ -1208,7 +1254,7 @@ version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c50453ec3a6555fad17b1cd1a80d16af5bc7cb35094f64e429fd46549018c6a3" dependencies = [ - "ahash", + "ahash 0.8.6", "clap", "crossbeam-channel", "crossbeam-utils", @@ -1293,15 +1339,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "isolang" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f80f221db1bc708b71128757b9396727c04de86968081e18e89b0575e03be071" -dependencies = [ - "phf", -] - [[package]] name = "itoa" version = "1.0.9" @@ -1729,24 +1766,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - [[package]] name = "pico-args" version = "0.5.0" @@ -1842,6 +1861,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pulldown-cmark" version = "0.9.3" @@ -1863,6 +1902,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quick-xml" version = "0.30.0" @@ -2011,6 +2060,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rend" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" +dependencies = [ + "bytecheck", +] + [[package]] name = "resvg" version = "0.36.0" @@ -2030,9 +2088,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.36" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" dependencies = [ "bytemuck", ] @@ -2051,6 +2109,34 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rkyv" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" +dependencies = [ + "bitvec", + "bytecheck", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "roff" version = "0.2.1" @@ -2083,9 +2169,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.20" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ "bitflags 2.4.1", "errno", @@ -2184,6 +2270,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "self-replace" version = "1.3.5" @@ -2202,18 +2294,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", @@ -2222,9 +2314,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -2254,9 +2346,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.25" +version = "0.9.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" dependencies = [ "indexmap 2.0.2", "itoa", @@ -2302,6 +2394,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + [[package]] name = "simplecss" version = "0.2.1" @@ -2508,13 +2606,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand 2.0.1", - "redox_syscall 0.3.5", + "redox_syscall 0.4.1", "rustix", "windows-sys", ] @@ -2641,9 +2739,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef75d881185fd2df4a040793927c153d863651108a93c7e17a9e591baa95cc6" +checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" dependencies = [ "serde", "serde_spanned", @@ -2662,9 +2760,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.4" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380f9e8120405471f7c9ad1860a713ef5ece6a670c7eae39225e477340f32fc4" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ "indexmap 2.0.2", "serde", @@ -2845,7 +2943,7 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_yaml 0.9.25", + "serde_yaml 0.9.27", "siphasher", "tar", "tempfile", @@ -2871,7 +2969,7 @@ dependencies = [ "once_cell", "pulldown-cmark", "serde", - "serde_yaml 0.9.25", + "serde_yaml 0.9.27", "syntect", "typed-arena", "typst", @@ -2911,6 +3009,7 @@ dependencies = [ "icu_provider_adapters", "icu_provider_blob", "icu_segmenter", + "indexmap 2.0.2", "kurbo", "lipsum", "log", @@ -2918,7 +3017,7 @@ dependencies = [ "roxmltree", "rustybuzz", "serde_json", - "serde_yaml 0.9.25", + "serde_yaml 0.9.27", "smallvec", "syntect", "time", @@ -2992,6 +3091,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e35bfd2f2b8796545b55d7d3fd3e89a0613f68a0d1c8bc28cb7ff96b411a35ff" dependencies = [ + "serde", "tinystr", ] @@ -3130,6 +3230,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -3205,6 +3306,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" + [[package]] name = "valuable" version = "0.1.0" @@ -3447,9 +3554,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" +checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32" dependencies = [ "memchr", ] @@ -3550,18 +3657,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.15" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" +checksum = "dd66a62464e3ffd4e37bd09950c2b9dd6c4f8767380fabba0d523f9a775bc85a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.15" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" +checksum = "255c4596d41e6916ced49cfafea18727b24d67878fa180ddfd69b9df34fd1726" dependencies = [ "proc-macro2", "quote", diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index 34214eb5e..b348596b2 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 4ff0bc92f..2ff4c9284 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::() || content.is::()) + { + self.staged.push((content, styles)); + return true; + } + + if let Some(citation) = content.to::() { + 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 9af542372..37f1ed355 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 0521a9c7f..d429d3436 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()]`). 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::>("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::>>()?; - - // 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, - /// 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>, + /// 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, } /// 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::().unwrap().clone()) @@ -123,35 +161,32 @@ impl BibliographyElem { vt.introspector .query(&Self::elem().select()) .iter() - .flat_map(|elem| { - let elem = elem.to::().unwrap(); - load(&elem.path(), &elem.data()) - }) - .flatten() - .any(|entry| entry.key() == key) + .any(|elem| elem.to::().unwrap().bibliography().has(key)) } /// Find all bibliography keys. pub fn keys( introspector: Tracked, ) -> Vec<(EcoString, Option)> { - 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::().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())), - ); } - Ok(Content::sequence(seq)) + let mut content = Content::sequence(seq); + if works.hanging_indent { + content = content.styled(ParElem::set_hanging_indent(INDENT.into())); + } + + Ok(content) })) } } impl Finalize for BibliographyElem { fn finalize(&self, realized: Content, _: StyleChain) -> Content { - 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>, + 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::>("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::>>()?; + + // 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 { + let mut map = IndexMap::new(); + let mut duplicates = Vec::::new(); + + // We might have multiple bib/yaml files + for (path, bytes) in paths.0.iter().zip(data) { + let src = std::str::from_utf8(bytes).map_err(FileError::from)?; + + let ext = Path::new(path.as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + let library = match ext.to_lowercase().as_str() { + "yml" | "yaml" => hayagriva::io::from_yaml_str(src) + .map_err(|err| eco_format!("failed to parse YAML ({err})"))?, + "bib" => hayagriva::io::from_biblatex_str(src) + .map_err(|errors| format_biblatex_error(path, src, errors))?, + _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), + }; + + for entry in library { + match map.entry(entry.key().into()) { + indexmap::map::Entry::Vacant(vacant) => { + vacant.insert(entry); + } + indexmap::map::Entry::Occupied(_) => { + duplicates.push(entry.key().into()); + } + } + } } + + if !duplicates.is_empty() { + bail!("duplicate bibliography keys: {}", duplicates.join(", ")); + } + + Ok(Bibliography { + map: Arc::new(map), + hash: typst::util::hash128(data), + }) + } + + fn has(&self, key: &str) -> bool { + self.map.contains_key(key) + } + + fn entries(&self) -> impl Iterator { + self.map.values() } } -/// 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, - - /// 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, - - /// 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, -} - -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 Hash for Bibliography { + fn hash(&self, state: &mut H) { + self.hash.hash(state); } } -impl Show for CiteElem { - #[tracing::instrument(name = "CiteElem::show", skip(self, vt))] - fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult { - 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()) - })) +impl Repr for Bibliography { + fn repr(&self) -> EcoString { + "..".into() } } cast! { - CiteElem, - v: Content => v.to::().cloned().ok_or("expected citation")?, -} - -/// 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 CitationStyle { - fn is_short(self) -> bool { - matches!(self, Self::Numerical | Self::Alphanumerical | Self::Keys) - } -} - -/// Fully formatted citations and references. -#[derive(Default)] -struct Works { - citations: HashMap>, - references: Vec<(Option, Content)>, -} - -impl Works { - /// Prepare all things need to cite a work or format a bibliography. - fn new(vt: &Vt) -> StrResult> { - 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::() { - Some(reference) => reference.citation().unwrap(), - _ => elem.to::().unwrap().clone(), - }) - .collect(); - Ok(create(bibliography, citations)) - } -} - -/// Generate all citations and the whole bibliography. -#[comemo::memoize] -fn create(bibliography: BibliographyElem, citations: Vec) -> Arc { - 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::>>(); - preliminary.push((citation, entries)); - } - - let mut current = CitationStyle::Numerical; - let mut citation_style: Box = - 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()), - }; - } - - 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; - - if style.is_short() { - display.value = display.value.replace(' ', "\u{a0}"); - } - - if brackets && len == 1 { - display = display.with_default_brackets(&*citation_style); - } - - if i > 0 { - content += TextElem::packed(",\u{a0}"); - } - - // Format and link to the reference entry. - content += format_display_string(&display, supplement, citation.span()) - .linked(Destination::Location(ref_location(entry))); - } - - 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(']') - } - }; - } - - if style == CitationStyle::ChicagoNotes { - content = FootnoteElem::with_content(content).pack(); - } - - (location, Some(content)) - }) - .collect(); - - let bibliography_style: Box = 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 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) - }); - - let mut reference = format_display_string(&reference.display, None, span); - if prefix.is_none() { - reference = reference.backlinked(backlink); - } - - (prefix, reference) - }) - .collect(); - - Arc::new(Works { citations, references }) -} - -/// Load bibliography entries from a path. -#[comemo::memoize] -fn load(paths: &BibPaths, data: &[Bytes]) -> StrResult> { - 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::>(); - 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]); - } - } - - if !duplicates.is_empty() { - Err(eco_format!("duplicate bibliography keys: {}", duplicates.join(", "))) - } else { - Ok(result) - } -} - -/// Parse a bibliography file (bib/yml/yaml) -fn parse_bib(path_str: &str, src: &str) -> StrResult> { - 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) - } - "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)"), - } -} - -/// Format a Hayagriva loading error. -fn format_hayagriva_error(err: YamlBibliographyError) -> EcoString { - eco_format!("{err}") + type Bibliography, } /// Format a BibLaTeX loading error. -fn format_biblatex_error(path: &str, src: &str, error: BibLaTeXError) -> EcoString { +fn format_biblatex_error(path: &str, src: &str, errors: Vec) -> 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()), + 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}") + eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})") } -/// 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"; +/// A loaded CSL style. +#[ty] +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct CslStyle { + name: Option, + style: Arc>, +} -/// Format a display string into content. -fn format_display_string( - string: &DisplayString, - mut supplement: Option, - 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; - } - - let mut content = if segment == SUPPLEMENT && supplement.is_some() { - supplement.take().unwrap_or_default() - } else { - TextElem::packed(segment).spanned(span) +impl CslStyle { + /// Parse the style argument. + pub fn parse(vm: &mut Vm, args: &mut Args) -> SourceResult> { + let Some(Spanned { v: string, span }) = + args.named::>("style")? + else { + return Ok(None); }; - for (range, fmt) in &string.formatting { - if !range.contains(&start) { + 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>> { + let Some(Spanned { v: smart, span }) = + args.named::>>("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 { + 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 { + 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 { + let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?; + citationberg::IndependentStyle::from_xml(text) + .map(|style| Self { name: None, style: Arc::new(Prehashed::new(style)) }) + .map_err(|err| eco_format!("failed to load CSL style ({err})")) + } + + /// Get the underlying independent style. + pub fn get(&self) -> &citationberg::IndependentStyle { + self.style.as_ref() + } +} + +// This Reflect impl is technically a bit wrong because it doesn't say what +// FromValue and IntoValue really do. Instead, it says what the `style` argument +// on `bibliography` and `cite` expect (through manual parsing). +impl Reflect for CslStyle { + #[comemo::memoize] + fn input() -> CastInfo { + let ty = std::iter::once(CastInfo::Type(Type::of::())); + 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::() { + return true; + } + } + + false + } +} + +impl FromValue for CslStyle { + fn from_value(value: Value) -> StrResult { + if let Value::Dyn(dynamic) = &value { + if let Some(concrete) = dynamic.downcast::() { + return Ok(concrete.clone()); + } + } + + Err(::error(&value)) + } +} + +impl IntoValue for CslStyle { + fn into_value(self) -> Value { + Value::dynamic(self) + } +} + +impl Repr for CslStyle { + fn repr(&self) -> EcoString { + self.name + .as_ref() + .map(|name| name.repr()) + .unwrap_or_else(|| "..".into()) + } +} + +/// Fully formatted citations and references, generated once (through +/// memoization) for the whole document. This setup is necessary because +/// citation formatting is inherently stateful and we need access to all +/// citations to do it. +pub(super) struct Works { + /// Maps from the location of a citation group to its rendered content. + pub citations: HashMap>, + /// Lists all references in the bibliography, with optional prefix, or + /// `None` if the citation style can't be used for bibliographies. + pub references: Option, Content)>>, + /// Whether the bibliography should have hanging indent. + pub hanging_indent: bool, +} + +impl Works { + /// Generate all citations and the whole bibliography. + #[comemo::memoize] + pub fn generate( + world: Tracked, + introspector: Tracked, + ) -> StrResult> { + let mut generator = Generator::new(world, introspector)?; + let rendered = generator.drive(); + let works = generator.display(&rendered)?; + Ok(Arc::new(works)) + } +} + +/// Context for generating the bibliography. +struct Generator<'a> { + /// The world that is used to evaluate mathematical material in citations. + world: Tracked<'a, dyn World + 'a>, + /// The document's bibliography. + bibliography: BibliographyElem, + /// The document's citation groups. + groups: EcoVec>, + /// Details about each group that are accumulated while driving hayagriva's + /// bibliography driver and needed when processing hayagriva's output. + infos: Vec, + /// Citations with unresolved keys. + failures: HashMap>, +} + +/// Details about a group of merged citations. All citations are put into groups +/// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two). +/// Even single citations will be put into groups of length ones. +struct GroupInfo { + /// The group's location. + location: Location, + /// The group's span. + span: Span, + /// Whether the group should be displayed in a footnote. + footnote: bool, + /// Details about the groups citations. + subinfos: SmallVec<[CiteInfo; 1]>, +} + +/// Details about a citation item in a request. +struct CiteInfo { + /// The citation's key. + key: Label, + /// The citation's supplement. + supplement: Option, + /// Whether this citation was hidden. + hidden: bool, +} + +impl<'a> Generator<'a> { + /// Create a new generator. + fn new( + world: Tracked<'a, dyn World + 'a>, + introspector: Tracked, + ) -> StrResult { + let bibliography = BibliographyElem::find(introspector)?; + let groups = introspector.query(&CiteGroup::elem().select()); + let infos = Vec::with_capacity(groups.len()); + Ok(Self { + world, + bibliography, + groups, + infos, + failures: HashMap::new(), + }) + } + + /// Drives hayagriva's citation driver. + fn drive(&mut self) -> hayagriva::Rendered { + static LOCALES: Lazy> = + 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::().unwrap(); + let location = group.0.location().unwrap(); + let children = group.children(); + + // Groups should never be empty. + let Some(first) = children.first() else { continue }; + + let mut subinfos = SmallVec::with_capacity(children.len()); + let mut items = Vec::with_capacity(children.len()); + let mut errors = EcoVec::new(); + let mut normal = true; + + // Create infos and items for each child in the group. + for child in &children { + let key = child.key(); + let Some(entry) = database.map.get(&key.0) else { + errors.push(error!( + child.span(), + "key `{}` does not exist in the bibliography", key.0 + )); + continue; + }; + + let supplement = child.supplement(StyleChain::default()); + let locator = supplement.as_ref().map(|_| { + SpecificLocator( + citationberg::taxonomy::Locator::Custom, + hayagriva::LocatorPayload::Transparent, + ) + }); + + let mut hidden = false; + let special_form = match child.form(StyleChain::default()) { + None => { + hidden = true; + None + } + Some(CitationForm::Normal) => None, + Some(CitationForm::Prose) => Some(hayagriva::CitePurpose::Prose), + Some(CitationForm::Full) => Some(hayagriva::CitePurpose::Full), + Some(CitationForm::Author) => Some(hayagriva::CitePurpose::Author), + Some(CitationForm::Year) => Some(hayagriva::CitePurpose::Year), + }; + + normal &= special_form.is_none(); + subinfos.push(CiteInfo { key, supplement, hidden }); + items.push(CitationItem::new(entry, locator, None, hidden, special_form)); + } + + if !errors.is_empty() { + self.failures.insert(location, Err(errors)); continue; } - 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() - } + let style = match first.style(StyleChain::default()) { + Smart::Auto => &bibliography_style.style, + Smart::Custom(style) => styles.alloc(style.style), }; + + self.infos.push(GroupInfo { + location, + subinfos, + span: first.span(), + footnote: normal + && style.settings.class == citationberg::StyleClass::Note, + }); + + driver.citation(CitationRequest::new( + items, + style, + Some(locale(first.lang(), first.region())), + &LOCALES, + None, + )); } - seq.push(content); - start = stop; + let locale = locale(self.bibliography.lang(), self.bibliography.region()); + + // Add hidden items for everything if we should print the whole + // bibliography. + if self.bibliography.full(StyleChain::default()) { + for entry in database.map.values() { + driver.citation(CitationRequest::new( + vec![CitationItem::new(entry, None, None, true, None)], + bibliography_style.get(), + Some(locale.clone()), + &LOCALES, + None, + )); + } + } + + driver.finish(BibliographyRequest { + style: bibliography_style.get(), + locale: Some(locale), + locale_files: &LOCALES, + }) } - Content::sequence(seq) + /// Displays hayagriva's output as content for the citations and references. + fn display(&mut self, rendered: &hayagriva::Rendered) -> StrResult { + let citations = self.display_citations(rendered); + let references = self.display_references(rendered); + let hanging_indent = + rendered.bibliography.as_ref().map_or(false, |b| b.hanging_indent); + Ok(Works { citations, references, hanging_indent }) + } + + /// Display the citation groups. + fn display_citations( + &mut self, + rendered: &hayagriva::Rendered, + ) -> HashMap> { + // Determine for each citation key where in the bibliography it is, + // so that we can link there. + let mut links = HashMap::new(); + if let Some(bibliography) = &rendered.bibliography { + let location = self.bibliography.0.location().unwrap(); + for (k, item) in bibliography.items.iter().enumerate() { + links.insert(item.key.as_str(), location.variant(k + 1)); + } + } + + let mut output = std::mem::take(&mut self.failures); + for (info, citation) in self.infos.iter().zip(&rendered.citations) { + let supplement = |i: usize| info.subinfos.get(i)?.supplement.clone(); + let link = + |i: usize| links.get(info.subinfos.get(i)?.key.0.as_str()).copied(); + + let renderer = ElemRenderer { + world: self.world, + span: info.span, + supplement: &supplement, + link: &link, + }; + + let content = if info.subinfos.iter().all(|sub| sub.hidden) { + Content::empty() + } else { + let mut content = + renderer.display_elem_children(&citation.citation, &mut None); + + if info.footnote { + content = FootnoteElem::with_content(content).pack(); + } + + content + }; + + output.insert(info.location, Ok(content)); + } + + output + } + + /// Display the bibliography references. + fn display_references( + &self, + rendered: &hayagriva::Rendered, + ) -> Option, Content)>> { + let rendered = rendered.bibliography.as_ref()?; + + // Determine for each citation key where it first occured, so that we + // can link there. + let mut first_occurances = HashMap::new(); + for info in &self.infos { + for subinfo in &info.subinfos { + let key = subinfo.key.0.as_str(); + first_occurances.entry(key).or_insert(info.location); + } + } + + // The location of the bibliography. + let location = self.bibliography.0.location().unwrap(); + + let mut output = vec![]; + for (k, item) in rendered.items.iter().enumerate() { + let renderer = ElemRenderer { + world: self.world, + span: self.bibliography.span(), + supplement: &|_| None, + link: &|_| None, + }; + + // Each reference is assigned a manually created well-known location + // that is derived from the bibliography's location. This way, + // citations can link to them. + let backlink = location.variant(k + 1); + + // Render the first field. + let mut prefix = item.first_field.as_ref().map(|elem| { + let mut content = renderer.display_elem_child(elem, &mut None); + if let Some(location) = first_occurances.get(item.key.as_str()) { + let dest = Destination::Location(*location); + content = content.linked(dest); + } + content.backlinked(backlink) + }); + + // Render the main reference content. + let reference = renderer + .display_elem_children(&item.content, &mut prefix) + .backlinked(backlink); + + output.push((prefix, reference)); + } + + Some(output) + } +} + +/// Renders hayagriva elements into content. +struct ElemRenderer<'a> { + /// The world that is used to evaluate mathematical material. + world: Tracked<'a, dyn World + 'a>, + /// The span that is attached to all of the resulting content. + span: Span, + /// Resolves the supplement of i-th citation in the request. + supplement: &'a dyn Fn(usize) -> Option, + /// Resolves where the i-th citation in the request should link to. + link: &'a dyn Fn(usize) -> Option, +} + +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::sequence( + elems.0.iter().map(|elem| self.display_elem_child(elem, prefix)), + ) + } + + /// Display a rendered hayagriva element. + fn display_elem_child( + &self, + elem: &hayagriva::ElemChild, + prefix: &mut Option, + ) -> Content { + match elem { + hayagriva::ElemChild::Text(formatted) => self.display_formatted(formatted), + hayagriva::ElemChild::Elem(elem) => self.display_elem(elem, prefix), + hayagriva::ElemChild::Markup(markup) => self.display_math(markup), + hayagriva::ElemChild::Link { text, url } => self.display_link(text, url), + hayagriva::ElemChild::Transparent { cite_idx, format } => { + self.display_transparent(*cite_idx, format) + } + } + } + + /// Display a block-level element. + fn display_elem( + &self, + elem: &hayagriva::Elem, + prefix: &mut Option, + ) -> Content { + use citationberg::Display; + + let block_level = matches!(elem.display, Some(Display::Block | Display::Indent)); + + let mut suf_prefix = None; + let mut content = self.display_elem_children( + &elem.children, + if block_level { &mut suf_prefix } else { prefix }, + ); + + if let Some(prefix) = suf_prefix { + const COLUMN_GUTTER: Em = Em::new(0.65); + content = GridElem::new(vec![prefix, content]) + .with_columns(TrackSizings(vec![Sizing::Auto; 2])) + .with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()])) + .pack(); + } + + match elem.display { + Some(Display::Block) => { + content = BlockElem::new().with_body(Some(content)).pack(); + } + Some(Display::Indent) => { + content = PadElem::new(content).pack(); + } + Some(Display::LeftMargin) => { + *prefix.get_or_insert_with(Default::default) += content; + return Content::empty(); + } + _ => {} + } + + if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta { + if let Some(location) = (self.link)(i) { + let dest = Destination::Location(location); + content = content.linked(dest); + } + } + + content + } + + /// Display math. + fn display_math(&self, math: &str) -> Content { + eval_string(self.world, math, self.span, EvalMode::Math, Scope::new()) + .map(Value::display) + .unwrap_or_else(|_| TextElem::packed(math).spanned(self.span)) + } + + /// Display a link. + fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> Content { + let dest = Destination::Url(url.into()); + self.display_formatted(text).linked(dest) + } + + /// Display transparent pass-through content. + fn display_transparent(&self, i: usize, format: &hayagriva::Formatting) -> Content { + let content = (self.supplement)(i).unwrap_or_default(); + apply_formatting(content, format) + } + + /// Display formatted hayagriva text as content. + fn display_formatted(&self, formatted: &hayagriva::Formatted) -> Content { + let content = TextElem::packed(formatted.text.as_str()).spanned(self.span); + apply_formatting(content, &formatted.formatting) + } +} + +/// Applies formatting to content. +fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Content { + match format.font_style { + citationberg::FontStyle::Normal => {} + citationberg::FontStyle::Italic => { + content = content.styled(TextElem::set_style(FontStyle::Italic)); + } + } + + match format.font_variant { + citationberg::FontVariant::Normal => {} + citationberg::FontVariant::SmallCaps => { + content = content.styled(TextElem::set_smallcaps(true)); + } + } + + match format.font_weight { + citationberg::FontWeight::Normal => {} + citationberg::FontWeight::Bold => { + content = content.styled(TextElem::set_delta(Delta(300))); + } + citationberg::FontWeight::Light => { + content = content.styled(TextElem::set_delta(Delta(-100))); + } + } + + match format.text_decoration { + citationberg::TextDecoration::None => {} + citationberg::TextDecoration::Underline => { + content = content.underlined(); + } + } + + match format.vertical_align { + citationberg::VerticalAlign::None => {} + citationberg::VerticalAlign::Baseline => {} + citationberg::VerticalAlign::Sup => { + // Add zero-width weak spacing to make the superscript "sticky". + content = HElem::hole().pack() + SuperElem::new(content).pack(); + } + citationberg::VerticalAlign::Sub => { + content = HElem::hole().pack() + SubElem::new(content).pack(); + } + } + + content +} + +/// Create a locale code from language and optionally region. +fn locale(lang: Lang, region: Option) -> 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 000000000..2e3c41552 --- /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() +/// +/// #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() \ + /// #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, + + /// 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(, 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, + + /// 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, + + /// 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, +} + +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::().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, +} + +impl Show for CiteGroup { + #[tracing::instrument(name = "CiteGroup::show", skip(self, vt))] + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult { + 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 ed7242bbb..a8f0b4dd0 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 659cb5a3c..1019864f0 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 7f05bfcc4..12e13929a 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 { - 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 01d941144..d42dc2f25 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 { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult { 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 118f5f453..8429fecf3 100644 --- a/crates/typst/src/model/styles.rs +++ b/crates/typst/src/model/styles.rs @@ -564,6 +564,11 @@ impl StyleVec { self.items.iter() } + /// Extract the contained items. + pub fn into_items(self) -> Vec { + 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 diff --git a/tests/ref/bugs/cite-locate.png b/tests/ref/bugs/cite-locate.png index 396726f3b..36246393d 100644 Binary files a/tests/ref/bugs/cite-locate.png and b/tests/ref/bugs/cite-locate.png differ diff --git a/tests/ref/bugs/hide-meta.png b/tests/ref/bugs/hide-meta.png index 860617d79..76b4671a0 100644 Binary files a/tests/ref/bugs/hide-meta.png and b/tests/ref/bugs/hide-meta.png differ diff --git a/tests/ref/meta/bibliography-full.png b/tests/ref/meta/bibliography-full.png new file mode 100644 index 000000000..391738961 Binary files /dev/null and b/tests/ref/meta/bibliography-full.png differ diff --git a/tests/ref/meta/bibliography-ordering.png b/tests/ref/meta/bibliography-ordering.png index a3f1ab377..675d2f68e 100644 Binary files a/tests/ref/meta/bibliography-ordering.png and b/tests/ref/meta/bibliography-ordering.png differ diff --git a/tests/ref/meta/bibliography.png b/tests/ref/meta/bibliography.png index 22d7355c2..cb00f8968 100644 Binary files a/tests/ref/meta/bibliography.png and b/tests/ref/meta/bibliography.png differ diff --git a/tests/ref/meta/cite-footnote.png b/tests/ref/meta/cite-footnote.png index 067d2c2ea..bd027cc24 100644 Binary files a/tests/ref/meta/cite-footnote.png and b/tests/ref/meta/cite-footnote.png differ diff --git a/tests/ref/meta/cite-form.png b/tests/ref/meta/cite-form.png new file mode 100644 index 000000000..7394b89ef Binary files /dev/null and b/tests/ref/meta/cite-form.png differ diff --git a/tests/ref/text/linebreak-obj.png b/tests/ref/text/linebreak-obj.png index 7159aae6c..665bfa001 100644 Binary files a/tests/ref/text/linebreak-obj.png and b/tests/ref/text/linebreak-obj.png differ diff --git a/tests/ref/text/quote.png b/tests/ref/text/quote.png index 77e6d1426..653f2d17a 100644 Binary files a/tests/ref/text/quote.png and b/tests/ref/text/quote.png differ diff --git a/tests/typ/meta/bibliography-full.typ b/tests/typ/meta/bibliography-full.typ new file mode 100644 index 000000000..0adc0b85a --- /dev/null +++ b/tests/typ/meta/bibliography-full.typ @@ -0,0 +1,5 @@ +// Test the full bibliography. + +--- +#set page(paper: "a6") +#bibliography("/files/works.bib", full: true) diff --git a/tests/typ/meta/bibliography.typ b/tests/typ/meta/bibliography.typ index e3d746e67..ec90a96ae 100644 --- a/tests/typ/meta/bibliography.typ +++ b/tests/typ/meta/bibliography.typ @@ -2,8 +2,9 @@ --- #set page(width: 200pt) + = Details -See also #cite("arrgh", "distress", supplement: [p. 22]), @arrgh[p. 4], and @distress[p. 5]. +See also @arrgh #cite(, supplement: [p.~22]), @arrgh[p.~4], and @distress[p.~5]. #bibliography("/files/works.bib") --- @@ -16,23 +17,18 @@ See also #cite("arrgh", "distress", supplement: [p. 22]), @arrgh[p. 4], and @dis ) #line(length: 100%) -#[#set cite(brackets: false) -As described by @netwok], +As described by #cite(, form: "prose"), the net-work is a creature of its own. This is close to piratery! @arrgh And quark! @quark ---- -// Error: 15-55 duplicate bibliography keys: arrgh, distress, glacier-melt, issue201, mcintosh_anxiety, netwok, psychology25, quark, restful, sharing, tolkien54 -#bibliography(("/files/works.bib", "/files/works.bib")) - --- #set page(width: 200pt) #set heading(numbering: "1.") #show bibliography: set heading(numbering: "1.") = Multiple Bibs -Now we have multiple bibliographies containing #cite("glacier-melt", "keshav2007read") +Now we have multiple bibliographies containing @glacier-melt @keshav2007read #bibliography(("/files/works.bib", "/files/works_too.bib")) --- @@ -43,3 +39,6 @@ Now we have multiple bibliographies containing #cite("glacier-melt", "keshav2007 @arrgh #bibliography("/files/works.bib") +--- +// Error: 15-55 duplicate bibliography keys: netwok, issue201, arrgh, quark, distress, glacier-melt, tolkien54, sharing, restful, mcintosh_anxiety, psychology25 +#bibliography(("/files/works.bib", "/files/works.bib")) diff --git a/tests/typ/meta/cite-form.typ b/tests/typ/meta/cite-form.typ new file mode 100644 index 000000000..3c11e1f63 --- /dev/null +++ b/tests/typ/meta/cite-form.typ @@ -0,0 +1,10 @@ +// Test citation forms. + +--- +#set page(width: 200pt) + +Nothing: #cite(, form: none) + +#cite(, form: "prose") say stuff. + +#bibliography("/files/works.bib", style: "apa") diff --git a/tests/typ/meta/outline-entry.typ b/tests/typ/meta/outline-entry.typ index 8f2901573..74a785a9c 100644 --- a/tests/typ/meta/outline-entry.typ +++ b/tests/typ/meta/outline-entry.typ @@ -54,7 +54,6 @@ === Lower heading --- -// Error: 2-23 cannot outline cite -#outline(target: cite) -#cite("arrgh", "distress", supplement: [p. 22]) -#bibliography("/files/works.bib") +// Error: 2-27 cannot outline metadata +#outline(target: metadata) +#metadata("hello") diff --git a/tests/typ/text/quote.typ b/tests/typ/text/quote.typ index b815b0327..0fd96ff58 100644 --- a/tests/typ/text/quote.typ +++ b/tests/typ/text/quote.typ @@ -18,6 +18,7 @@ And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum]. --- // Spacing with other blocks #set quote(block: true) +#set text(8pt) #lorem(10) #quote(lorem(10)) @@ -25,27 +26,35 @@ And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum]. --- // Inline citation -#bibliography("/files/works.bib") - +#set text(8pt) #quote(attribution: )[In a hole in the ground there lived a hobbit.] +#set text(0pt) +#bibliography("/files/works.bib") + --- // Citation-format: label or numeric +#set text(8pt) #set quote(block: true) -#bibliography("/files/works.bib", style: "ieee") - #quote(attribution: )[In a hole in the ground there lived a hobbit.] +#set text(0pt) +#bibliography("/files/works.bib", style: "ieee") + --- // Citation-format: note +#set text(8pt) #set quote(block: true) -#bibliography("/files/works.bib", style: "chicago-notes") - #quote(attribution: )[In a hole in the ground there lived a hobbit.] +#set text(0pt) +#bibliography("/files/works.bib", style: "chicago-notes") + --- // Citation-format: author-date or author +#set text(8pt) #set quote(block: true) -#bibliography("/files/works.bib", style: "apa") - #quote(attribution: )[In a hole in the ground there lived a hobbit.] + +#set text(0pt) +#bibliography("/files/works.bib", style: "apa")