From 89f44f220de2972452dd816fe59836ba76953d59 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 14 Mar 2023 22:35:31 +0100 Subject: [PATCH] Bibliography and citations --- Cargo.lock | 199 +++++++++ assets/files/bad.bib | 6 + assets/files/{invalid-utf8.txt => bad.txt} | Bin assets/files/works.bib | 32 ++ library/Cargo.toml | 1 + library/src/lib.rs | 5 +- library/src/meta/bibliography.rs | 472 +++++++++++++++++++++ library/src/meta/heading.rs | 2 +- library/src/meta/mod.rs | 2 + library/src/meta/numbering.rs | 2 +- library/src/meta/reference.rs | 42 +- src/eval/library.rs | 9 +- src/ide/analyze.rs | 37 ++ src/ide/complete.rs | 32 +- src/ide/mod.rs | 11 +- src/ide/tooltip.rs | 43 +- src/model/typeset.rs | 21 +- tests/ref/meta/bibliography.png | Bin 0 -> 38227 bytes tests/typ/compute/data.typ | 4 +- tests/typ/meta/bibliography.typ | 27 ++ 20 files changed, 897 insertions(+), 50 deletions(-) create mode 100644 assets/files/bad.bib rename assets/files/{invalid-utf8.txt => bad.txt} (100%) create mode 100644 assets/files/works.bib create mode 100644 library/src/meta/bibliography.rs create mode 100644 tests/ref/meta/bibliography.png create mode 100644 tests/typ/meta/bibliography.typ diff --git a/Cargo.lock b/Cargo.lock index ac4c3a1d1..9547e3ac1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,19 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "biblatex" +version = "0.6.3" +source = "git+https://github.com/typst/biblatex#932ad283dd45dd88d4fa14dc5b9bda7a270ba027" +dependencies = [ + "chrono", + "numerals", + "paste", + "strum", + "unicode-normalization", + "unscanny", +] + [[package]] name = "bincode" version = "1.3.3" @@ -304,6 +317,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ecow" version = "0.1.0" @@ -363,6 +387,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -408,6 +441,26 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hayagriva" +version = "0.1.1" +source = "git+https://github.com/typst/hayagriva#992389b23f9765198ee8d3f1818d6dbdc8f46b60" +dependencies = [ + "biblatex", + "chrono", + "isolang", + "lazy_static", + "linked-hash-map", + "paste", + "regex", + "strum", + "thiserror", + "unic-langid", + "unicode-segmentation", + "url", + "yaml-rust", +] + [[package]] name = "heck" version = "0.4.1" @@ -452,6 +505,16 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "if_chain" version = "1.0.2" @@ -523,6 +586,15 @@ dependencies = [ "libc", ] +[[package]] +name = "isolang" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64fd6448ee8a45ce6e4365c58e4fa7d8740cba2ed70db3e9ab4879ebd93eaaa" +dependencies = [ + "phf", +] + [[package]] name = "itoa" version = "0.4.8" @@ -729,12 +801,24 @@ dependencies = [ "autocfg", ] +[[package]] +name = "numerals" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e25be21376a772d15f97ae789845340a9651d3c4246ff5ebb6a2b35f9c37bd31" + [[package]] name = "once_cell" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + [[package]] name = "pdf-writer" version = "0.6.0" @@ -746,6 +830,30 @@ dependencies = [ "ryu", ] +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + [[package]] name = "pico-args" version = "0.4.2" @@ -917,6 +1025,12 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + [[package]] name = "rustybuzz" version = "0.5.1" @@ -1033,6 +1147,28 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subsetter" version = "0.1.0" @@ -1141,6 +1277,30 @@ dependencies = [ "safe_arch", ] +[[package]] +name = "tinystr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef" +dependencies = [ + "displaydoc", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "ttf-parser" version = "0.15.2" @@ -1246,6 +1406,7 @@ dependencies = [ "comemo", "csv", "ecow", + "hayagriva", "hypher", "kurbo", "lipsum", @@ -1292,6 +1453,24 @@ dependencies = [ "walkdir", ] +[[package]] +name = "unic-langid" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398f9ad7239db44fd0f80fe068d12ff22d78354080332a5077dc6f52f14dcf2f" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35bfd2f2b8796545b55d7d3fd3e89a0613f68a0d1c8bc28cb7ff96b411a35ff" +dependencies = [ + "tinystr", +] + [[package]] name = "unicase" version = "2.6.0" @@ -1336,6 +1515,15 @@ name = "unicode-math-class" version = "0.1.0" source = "git+https://github.com/typst/unicode-math-class#a7ac7dd75cd79ab2e0bdb629036cb913371608d2" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-script" version = "0.5.5" @@ -1372,6 +1560,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "usvg" version = "0.22.0" diff --git a/assets/files/bad.bib b/assets/files/bad.bib new file mode 100644 index 000000000..41b4d63df --- /dev/null +++ b/assets/files/bad.bib @@ -0,0 +1,6 @@ +@article{arrgh, + title = {An‐arrgh‐chy: The Law and Economics of Pirate Organization}, + author = {Leeson, Peter T.}, + crossref = {polecon}, + date = {19XXX-XX-XX}, +} diff --git a/assets/files/invalid-utf8.txt b/assets/files/bad.txt similarity index 100% rename from assets/files/invalid-utf8.txt rename to assets/files/bad.txt diff --git a/assets/files/works.bib b/assets/files/works.bib new file mode 100644 index 000000000..72f06db1d --- /dev/null +++ b/assets/files/works.bib @@ -0,0 +1,32 @@ +@article{stupid, + title={At-scale impact of the {Net Wok}: A culinarically holistic investigation of distributed dumplings}, + author={Astley, Rick and Morris, Linda}, + journal={Armenian Journal of Proceedings}, + volume={61}, + pages={192--219}, + year={2020}, + publisher={Automattic Inc.} +} + +@www{issue201, + title={Use of ids field creates unstable references}, + author={{cfr42}}, + url={https://github.com/plk/biblatex/issues/201}, + date={2014-02-02/2014-02-07}, + ids={unstable, github} +} + +@article{arrgh, + title={The Pirate Organization}, + author={Leeson, Peter T.}, +} + +@article{quark, + title={The Quark Organization}, + author={Leeson, Peter T.}, +} + +@misc{cannonfodder, + title={An Insight into Bibliographical Distress}, + author={Aldrin, Buzz} +} diff --git a/library/Cargo.toml b/library/Cargo.toml index b532883be..3c1f79d45 100644 --- a/library/Cargo.toml +++ b/library/Cargo.toml @@ -14,6 +14,7 @@ typst = { path = ".." } comemo = { git = "https://github.com/typst/comemo" } csv = "1" ecow = "0.1" +hayagriva = { git = "https://github.com/typst/hayagriva" } hypher = "0.1" kurbo = "0.8" lipsum = { git = "https://github.com/reknih/lipsum" } diff --git a/library/src/lib.rs b/library/src/lib.rs index 5b114d9b3..c4a421d21 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -89,6 +89,8 @@ fn global(math: Module, calc: Module) -> Module { global.define("outline", meta::OutlineNode::id()); global.define("heading", meta::HeadingNode::id()); global.define("figure", meta::FigureNode::id()); + global.define("cite", meta::CiteNode::id()); + global.define("bibliography", meta::BibliographyNode::id()); global.define("numbering", meta::numbering); // Symbols. @@ -179,7 +181,7 @@ fn items() -> LangItems { raw: |text, lang, block| { let mut node = text::RawNode::new(text).with_block(block); if let Some(lang) = lang { - node = node.with_lang(Some(lang)); + node.push_lang(Some(lang)); } node.pack() }, @@ -194,6 +196,7 @@ fn items() -> LangItems { } node.pack() }, + bibliography_keys: meta::BibliographyNode::keys, heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(), list_item: |body| layout::ListItem::new(body).pack(), enum_item: |number, body| { diff --git a/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs new file mode 100644 index 000000000..a01f9eee1 --- /dev/null +++ b/library/src/meta/bibliography.rs @@ -0,0 +1,472 @@ +use std::collections::HashMap; +use std::ffi::OsStr; +use std::path::Path; +use std::sync::Arc; + +use ecow::EcoVec; +use hayagriva::io::{BibLaTeXError, YamlBibliographyError}; +use hayagriva::style::{self, Citation, Database, DisplayString, Formatting}; +use typst::font::{FontStyle, FontWeight}; + +use super::LocalName; +use crate::layout::{GridNode, ParNode, Sizing, TrackSizings, VNode}; +use crate::meta::HeadingNode; +use crate::prelude::*; +use crate::text::{Hyphenate, TextNode}; + +/// A bibliography / reference listing. +/// +/// Display: Bibliography +/// Category: meta +#[node(Locatable, Synthesize, Show, LocalName)] +pub struct BibliographyNode { + /// Path to a Hayagriva `.yml` or BibLaTeX `.bib` file. + #[required] + #[parse( + let Spanned { v: path, span } = + args.expect::>("path to bibliography file")?; + let path: EcoString = vm.locate(&path).at(span)?.to_string_lossy().into(); + let _ = load(vm.world(), &path).at(span)?; + path + )] + pub path: EcoString, + + /// The title of the bibliography. + /// + /// - When set to `{auto}`, an appropriate title for the [text + /// language]($func/text.lang) will be used. This is the default. + /// - When set to `{none}`, the bibliography will not have a title. + /// - A custom title can be set by passing content. + #[default(Some(Smart::Auto))] + pub title: Option>, + + /// The bibliography style. + #[default(BibliographyStyle::Ieee)] + pub style: BibliographyStyle, +} + +impl BibliographyNode { + /// Find the document's bibliography. + pub fn find(introspector: Tracked) -> StrResult { + let mut iter = introspector.locate(Selector::node::()).into_iter(); + let Some((_, node)) = iter.next() else { + return Err("the document does not contain a bibliography".into()); + }; + + if iter.next().is_some() { + Err("multiple bibliographies are not supported")?; + } + + Ok(node.to::().unwrap().clone()) + } + + /// Whether the bibliography contains the given key. + pub fn has(vt: &Vt, key: &str) -> bool { + vt.introspector + .locate(Selector::node::()) + .into_iter() + .flat_map(|(_, node)| load(vt.world(), &node.to::().unwrap().path())) + .flatten() + .any(|entry| entry.key() == key) + } + + /// Find all bibliography keys. + pub fn keys( + world: Tracked, + introspector: Tracked, + ) -> Vec<(EcoString, Option)> { + Self::find(introspector) + .and_then(|node| load(world, &node.path())) + .into_iter() + .flatten() + .map(|entry| { + let key = entry.key().into(); + let detail = + entry.title().map(|title| title.canonical.value.as_str().into()); + (key, detail) + }) + .collect() + } +} + +impl Synthesize for BibliographyNode { + fn synthesize(&mut self, _: &Vt, styles: StyleChain) { + self.push_style(self.style(styles)); + } +} + +impl Show for BibliographyNode { + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { + const COLUMN_GUTTER: Em = Em::new(0.65); + const ROW_GUTTER: Em = Em::new(1.0); + const INDENT: Em = Em::new(1.5); + + let works = match Works::new(vt) { + Ok(works) => works, + Err(error) => { + if vt.locatable() { + bail!(self.span(), error) + } else { + return Ok(TextNode::packed("bibliography")); + } + } + }; + + let mut seq = vec![]; + if let Some(title) = self.title(styles) { + let title = title.clone().unwrap_or_else(|| { + TextNode::packed(self.local_name(TextNode::lang_in(styles))) + }); + + seq.push( + HeadingNode::new(title) + .with_level(NonZeroUsize::new(1).unwrap()) + .with_numbering(None) + .pack(), + ); + } + + if works.references.iter().any(|(prefix, _)| prefix.is_some()) { + let mut cells = vec![]; + for (prefix, reference) in &works.references { + cells.push(prefix.clone().unwrap_or_default()); + cells.push(reference.clone()); + } + + seq.push( + GridNode::new(cells) + .with_columns(TrackSizings(vec![Sizing::Auto; 2])) + .with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()])) + .with_row_gutter(TrackSizings(vec![ROW_GUTTER.into()])) + .pack(), + ); + } else { + let mut entries = vec![]; + for (i, (_, reference)) in works.references.iter().enumerate() { + if i > 0 { + entries.push(VNode::new(ROW_GUTTER.into()).with_weakness(1).pack()); + } + entries.push(reference.clone()); + } + + seq.push( + Content::sequence(entries) + .styled(ParNode::set_hanging_indent(INDENT.into())), + ); + } + + Ok(Content::sequence(seq)) + } +} + +impl LocalName for BibliographyNode { + fn local_name(&self, lang: Lang) -> &'static str { + match lang { + Lang::GERMAN => "Bibliographie", + Lang::ENGLISH | _ => "Bibliography", + } + } +} + +/// 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. + AuthorDate, + /// 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, +} + +impl BibliographyStyle { + /// The default citation style for this bibliography style. + pub fn default_citation_style(self) -> CitationStyle { + match self { + Self::Apa => CitationStyle::AuthorDate, + Self::AuthorDate => CitationStyle::AuthorDate, + Self::Ieee => CitationStyle::Numerical, + Self::Mla => CitationStyle::AuthorDate, + } + } +} + +/// A citation of another work. +/// +/// Display: Citation +/// Category: meta +#[node(Locatable, Synthesize, Show)] +pub struct CiteNode { + /// The citation key. + #[required] + pub key: EcoString, + + /// A supplement for the citation such as page or chapter number. + #[positional] + pub supplement: Option, + + /// The citation style. + /// + /// When set to `{auto}`, automatically picks the preferred citation style + /// for the bibliography's style. + pub style: Smart, +} + +impl Synthesize for CiteNode { + fn synthesize(&mut self, _: &Vt, styles: StyleChain) { + self.push_supplement(self.supplement(styles)); + self.push_style(self.style(styles)); + } +} + +impl Show for CiteNode { + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult { + let id = self.0.stable_id().unwrap(); + let works = match Works::new(vt) { + Ok(works) => works, + Err(error) => { + if vt.locatable() { + bail!(self.span(), error) + } else { + return Ok(TextNode::packed("citation")); + } + } + }; + + let Some(citation) = works.citations.get(&id).cloned() else { + return Ok(TextNode::packed("citation")); + }; + + citation + .ok_or("bibliography does not contain this key") + .at(self.span()) + } +} + +/// 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, + /// The Chicago Author Date style. Based on the 17th edition of the Chicago + /// Manual of Style, Chapter 15. + AuthorDate, + /// A Chicago-like author-title format. Results could look like this: + /// Prokopov, “It Is Fast or It Is Wrong”. + AuthorTitle, + /// Citations that just consist of the entry keys. + Keys, +} + +/// Fully formatted citations and references. +pub struct Works { + citations: HashMap>, + references: Vec<(Option, Content)>, +} + +impl Works { + /// Prepare all things need to cite a work or format a bibliography. + pub fn new(vt: &Vt) -> StrResult> { + let bibliography = BibliographyNode::find(vt.introspector)?; + let style = bibliography.style(StyleChain::default()); + let citations = vt + .locate_node::() + .map(|(id, node)| { + ( + id, + node.key(), + node.supplement(StyleChain::default()), + node.style(StyleChain::default()) + .unwrap_or(style.default_citation_style()), + ) + }) + .collect(); + Ok(create(vt.world(), &bibliography.path(), style, citations)) + } +} + +/// Generate all citations and the whole bibliography. +#[comemo::memoize] +fn create( + world: Tracked, + path: &str, + style: BibliographyStyle, + citations: Vec<(StableId, EcoString, Option, CitationStyle)>, +) -> Arc { + let entries = load(world, path).unwrap(); + + let mut db = Database::new(); + let mut preliminary = vec![]; + + for (id, key, supplement, style) in citations { + let entry = entries.iter().find(|entry| entry.key() == key); + if let Some(entry) = &entry { + db.push(entry); + } + preliminary.push((id, entry, supplement, style)); + } + + let mut current = CitationStyle::Numerical; + let mut citation_style: Box = + Box::new(style::Numerical::new()); + + let citations = preliminary + .into_iter() + .map(|(id, result, supplement, style)| { + let formatted = result.map(|entry| { + if style != current { + current = style; + citation_style = match style { + CitationStyle::Numerical => Box::new(style::Numerical::new()), + CitationStyle::Alphanumerical => { + Box::new(style::Alphanumerical::new()) + } + CitationStyle::AuthorDate => { + Box::new(style::ChicagoAuthorDate::new()) + } + CitationStyle::AuthorTitle => Box::new(style::AuthorTitle::new()), + CitationStyle::Keys => Box::new(style::Keys::new()), + }; + } + + let citation = db.citation( + &mut *citation_style, + &[Citation { + entry, + supplement: supplement.is_some().then(|| SUPPLEMENT), + }], + ); + let bracketed = citation.display.with_default_brackets(&*citation_style); + format_display_string(&bracketed, supplement) + }); + (id, formatted) + }) + .collect(); + + let bibliography_style: Box = match style { + BibliographyStyle::Apa => Box::new(style::Apa::new()), + BibliographyStyle::AuthorDate => Box::new(style::ChicagoAuthorDate::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 prefix = reference.prefix.map(|prefix| { + let bracketed = prefix.with_default_brackets(&*citation_style); + format_display_string(&bracketed, None) + }); + let reference = format_display_string(&reference.display, None); + (prefix, reference) + }) + .collect(); + + Arc::new(Works { citations, references }) +} + +/// Load bibliography entries from a path. +#[comemo::memoize] +fn load(world: Tracked, path: &str) -> StrResult> { + let path = Path::new(path); + let buffer = world.file(path)?; + let src = std::str::from_utf8(&buffer).map_err(|_| "file is not valid utf-8")?; + let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default(); + let entries = match ext.to_lowercase().as_str() { + "yml" => 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(src, error)) + .unwrap_or_else(|| "failed to parse biblatex file".into()) + })?, + _ => return Err("unknown bibliography format".into()), + }; + Ok(entries.into_iter().collect()) +} + +/// Format a Hayagriva loading error. +fn format_hayagriva_error(error: YamlBibliographyError) -> EcoString { + eco_format!("{error}") +} + +/// Format a BibLaTeX loading error. +fn format_biblatex_error(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!("failed to parse biblatex file: {msg} in line {line}") +} + +/// 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"; + +/// Format a display string into content. +fn format_display_string( + string: &DisplayString, + mut supplement: Option, +) -> 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 styles = StyleMap::new(); + for (range, fmt) in &string.formatting { + if !range.contains(&start) { + continue; + } + + styles.set(match fmt { + Formatting::Bold => TextNode::set_weight(FontWeight::BOLD), + Formatting::Italic => TextNode::set_style(FontStyle::Italic), + Formatting::NoHyphenation => { + TextNode::set_hyphenate(Hyphenate(Smart::Custom(false))) + } + }); + } + + let content = if segment == SUPPLEMENT && supplement.is_some() { + supplement.take().unwrap_or_default() + } else { + TextNode::packed(segment) + }; + + seq.push(content.styled_with_map(styles)); + start = stop; + } + + Content::sequence(seq) +} diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index 4d1b87e65..48f6e2296 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -145,7 +145,7 @@ impl Finalize for HeadingNode { } } -/// Counters through headings with different levels. +/// Counts through headings with different levels. pub struct HeadingCounter(Vec); impl HeadingCounter { diff --git a/library/src/meta/mod.rs b/library/src/meta/mod.rs index 3cde2b8e0..ba74dac05 100644 --- a/library/src/meta/mod.rs +++ b/library/src/meta/mod.rs @@ -1,5 +1,6 @@ //! Interaction between document parts. +mod bibliography; mod document; mod figure; mod heading; @@ -8,6 +9,7 @@ mod numbering; mod outline; mod reference; +pub use self::bibliography::*; pub use self::document::*; pub use self::figure::*; pub use self::heading::*; diff --git a/library/src/meta/numbering.rs b/library/src/meta/numbering.rs index d71fb2339..c59766c80 100644 --- a/library/src/meta/numbering.rs +++ b/library/src/meta/numbering.rs @@ -65,7 +65,7 @@ pub fn numbering( numbering.apply(vm.world(), &numbers)? } -/// How to number an enumeration. +/// How to number a sequence of things. #[derive(Debug, Clone, Hash)] pub enum Numbering { /// A pattern with prefix, numbering, lower / upper case and suffix. diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index f63c7e4c7..b4cfa049f 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -1,4 +1,4 @@ -use super::{FigureNode, HeadingNode, LocalName, Numbering}; +use super::{BibliographyNode, CiteNode, FigureNode, HeadingNode, LocalName, Numbering}; use crate::prelude::*; use crate::text::TextNode; @@ -35,7 +35,7 @@ use crate::text::TextNode; /// /// Display: Reference /// Category: meta -#[node(Synthesize, Show)] +#[node(Show)] pub struct RefNode { /// The target label that should be referenced. #[required] @@ -60,27 +60,33 @@ pub struct RefNode { /// In @intro, we see how to turn /// Sections into Chapters. /// ``` - - /// All elements with the target label in the document. - #[synthesized] - pub matches: Vec, pub supplement: Smart>, } -impl Synthesize for RefNode { - fn synthesize(&mut self, vt: &Vt, _: StyleChain) { - let matches = vt - .locate(Selector::Label(self.target())) - .map(|(_, node)| node.clone()) - .collect(); - - self.push_matches(matches); - } -} - impl Show for RefNode { fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { - let matches = self.matches(); + let target = self.target(); + let supplement = self.supplement(styles); + + let matches: Vec<_> = vt + .locate(Selector::Label(self.target())) + .map(|(_, node)| node.clone()) + .collect(); + + if !vt.locatable() || BibliographyNode::has(vt, &target.0) { + if !matches.is_empty() { + bail!(self.span(), "label occurs in the document and its bibliography"); + } + + return Ok(CiteNode::new(target.0) + .with_supplement(match supplement { + Smart::Custom(Some(Supplement::Content(content))) => Some(content), + _ => None, + }) + .pack() + .spanned(self.span())); + } + let [target] = matches.as_slice() else { if vt.locatable() { bail!(self.span(), if matches.is_empty() { diff --git a/src/eval/library.rs b/src/eval/library.rs index d3f7547d6..1240d9bb2 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -2,6 +2,7 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::num::NonZeroUsize; +use comemo::Tracked; use ecow::EcoString; use once_cell::sync::OnceCell; @@ -9,8 +10,9 @@ use super::Module; use crate::diag::SourceResult; use crate::doc::Document; use crate::geom::{Abs, Dir}; -use crate::model::{Content, Label, NodeId, StyleChain, StyleMap, Vt}; +use crate::model::{Content, Introspector, Label, NodeId, StyleChain, StyleMap, Vt}; use crate::util::hash128; +use crate::World; /// Definition of Typst's standard library. #[derive(Debug, Clone, Hash)] @@ -61,6 +63,11 @@ pub struct LangItems { pub link: fn(url: EcoString) -> Content, /// A reference: `@target`, `@target[..]`. pub reference: fn(target: Label, supplement: Option) -> Content, + /// The keys contained in the bibliography and short descriptions of them. + pub bibliography_keys: fn( + world: Tracked, + introspector: Tracked, + ) -> Vec<(EcoString, Option)>, /// A section heading: `= Introduction`. pub heading: fn(level: NonZeroUsize, body: Content) -> Content, /// An item in a bullet list: `- ...`. diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs index 3c46cca15..7338ba57c 100644 --- a/src/ide/analyze.rs +++ b/src/ide/analyze.rs @@ -1,8 +1,11 @@ use std::path::PathBuf; use comemo::Track; +use ecow::EcoString; +use crate::doc::Frame; use crate::eval::{eval, Module, Route, Tracer, Value}; +use crate::model::{Introspector, Label}; use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::util::PathExt; use crate::World; @@ -64,3 +67,37 @@ pub fn analyze_import( let source = world.source(id); eval(world.track(), route.track(), tracer.track_mut(), source).ok() } + +/// Find all labels and details for them. +pub fn analyze_labels( + world: &(dyn World + 'static), + frames: &[Frame], +) -> (Vec<(Label, Option)>, usize) { + let mut output = vec![]; + let mut introspector = Introspector::new(); + let items = &world.library().items; + introspector.update(frames); + + // Labels in the document. + for node in introspector.iter() { + let Some(label) = node.label() else { continue }; + let details = node + .field("caption") + .or_else(|| node.field("body")) + .and_then(|field| match field { + Value::Content(content) => Some(content), + _ => None, + }) + .and_then(|content| (items.text_str)(content)); + output.push((label.clone(), details)); + } + + let split = output.len(); + + // Bibliography keys. + for (key, detail) in (items.bibliography_keys)(world.track(), introspector.track()) { + output.push((Label(key), detail)); + } + + (output, split) +} diff --git a/src/ide/complete.rs b/src/ide/complete.rs index de6f2b736..665901605 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -4,7 +4,9 @@ use ecow::{eco_format, EcoString}; use if_chain::if_chain; use unscanny::Scanner; +use super::analyze::analyze_labels; use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; +use crate::doc::Frame; use crate::eval::{methods_on, CastInfo, Library, Scope, Value}; use crate::syntax::{ ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, @@ -21,11 +23,12 @@ use crate::World; /// control and space or something similar. pub fn autocomplete( world: &(dyn World + 'static), + frames: &[Frame], source: &Source, cursor: usize, explicit: bool, ) -> Option<(usize, Vec)> { - let mut ctx = CompletionContext::new(world, source, cursor, explicit)?; + let mut ctx = CompletionContext::new(world, frames, source, cursor, explicit)?; let _ = complete_comments(&mut ctx) || complete_field_accesses(&mut ctx) @@ -78,7 +81,10 @@ fn complete_comments(ctx: &mut CompletionContext) -> bool { /// Complete in markup mode. fn complete_markup(ctx: &mut CompletionContext) -> bool { // Bail if we aren't even in markup. - if !matches!(ctx.leaf.parent_kind(), None | Some(SyntaxKind::Markup)) { + if !matches!( + ctx.leaf.parent_kind(), + None | Some(SyntaxKind::Markup) | Some(SyntaxKind::Ref) + ) { return false; } @@ -96,6 +102,13 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool { return true; } + // Start of an reference: "@|" or "@he|". + if ctx.leaf.kind() == SyntaxKind::RefMarker { + ctx.from = ctx.leaf.offset() + 1; + ctx.label_completions(); + return true; + } + // Behind a half-completed binding: "#let x = |". if_chain! { if let Some(prev) = ctx.leaf.prev_leaf(); @@ -850,6 +863,7 @@ fn code_completions(ctx: &mut CompletionContext, hashtag: bool) { /// Context for autocompletion. struct CompletionContext<'a> { world: &'a (dyn World + 'static), + frames: &'a [Frame], library: &'a Library, source: &'a Source, global: &'a Scope, @@ -869,6 +883,7 @@ impl<'a> CompletionContext<'a> { /// Create a new autocompletion context. fn new( world: &'a (dyn World + 'static), + frames: &'a [Frame], source: &'a Source, cursor: usize, explicit: bool, @@ -878,6 +893,7 @@ impl<'a> CompletionContext<'a> { let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; Some(Self { world, + frames, library, source, global: &library.global.scope(), @@ -955,6 +971,18 @@ impl<'a> CompletionContext<'a> { } } + /// Add completions for all labels. + fn label_completions(&mut self) { + for (label, detail) in analyze_labels(self.world, self.frames).0 { + self.completions.push(Completion { + kind: CompletionKind::Constant, + label: label.0, + apply: None, + detail, + }); + } + } + /// Add a completion for a specific value. fn value_completion( &mut self, diff --git a/src/ide/mod.rs b/src/ide/mod.rs index bee959cdf..38bede0bb 100644 --- a/src/ide/mod.rs +++ b/src/ide/mod.rs @@ -6,6 +6,7 @@ mod highlight; mod jump; mod tooltip; +pub use self::analyze::analyze_labels; pub use self::complete::*; pub use self::highlight::*; pub use self::jump::*; @@ -13,15 +14,17 @@ pub use self::tooltip::*; use std::fmt::Write; +use ecow::{eco_format, EcoString}; + use self::analyze::*; use crate::font::{FontInfo, FontStyle}; /// Extract the first sentence of plain text of a piece of documentation. /// /// Removes Markdown formatting. -fn plain_docs_sentence(docs: &str) -> String { +fn plain_docs_sentence(docs: &str) -> EcoString { let mut s = unscanny::Scanner::new(docs); - let mut output = String::new(); + let mut output = EcoString::new(); let mut link = false; while let Some(c) = s.eat() { match c { @@ -62,7 +65,7 @@ fn plain_docs_sentence(docs: &str) -> String { } /// Create a short description of a font family. -fn summarize_font_family<'a>(variants: impl Iterator) -> String { +fn summarize_font_family<'a>(variants: impl Iterator) -> EcoString { let mut infos: Vec<_> = variants.collect(); infos.sort_by_key(|info| info.variant); @@ -78,7 +81,7 @@ fn summarize_font_family<'a>(variants: impl Iterator) -> St let count = infos.len(); let s = if count == 1 { "" } else { "s" }; - let mut detail = format!("{count} variant{s}."); + let mut detail = eco_format!("{count} variant{s}."); if min_weight == max_weight { write!(detail, " Weight {min_weight}.").unwrap(); diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs index a32dfb0b2..0b37b7ca5 100644 --- a/src/ide/tooltip.rs +++ b/src/ide/tooltip.rs @@ -1,20 +1,22 @@ use std::fmt::Write; -use ecow::EcoString; +use ecow::{eco_format, EcoString}; use if_chain::if_chain; +use super::analyze::analyze_labels; use super::{analyze_expr, plain_docs_sentence, summarize_font_family}; +use crate::doc::Frame; use crate::eval::{CastInfo, Tracer, Value}; use crate::geom::{round_2, Length, Numeric}; -use crate::syntax::ast; -use crate::syntax::{LinkedNode, Source, SyntaxKind}; +use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::util::pretty_comma_list; use crate::World; /// Describe the item under the cursor. pub fn tooltip( world: &(dyn World + 'static), + frames: &[Frame], source: &Source, cursor: usize, ) -> Option { @@ -22,6 +24,7 @@ pub fn tooltip( named_param_tooltip(world, &leaf) .or_else(|| font_tooltip(world, &leaf)) + .or_else(|| ref_tooltip(world, frames, &leaf)) .or_else(|| expr_tooltip(world, &leaf)) } @@ -29,9 +32,9 @@ pub fn tooltip( #[derive(Debug, Clone)] pub enum Tooltip { /// A string of text. - Text(String), + Text(EcoString), /// A string of Typst code. - Code(String), + Code(EcoString), } /// Tooltip for a hovered expression. @@ -55,7 +58,7 @@ fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option Option Option { +fn length_tooltip(length: Length) -> Option { length.em.is_zero().then(|| { - format!( + Tooltip::Code(eco_format!( "{}pt = {}mm = {}cm = {}in", round_2(length.abs.to_pt()), round_2(length.abs.to_mm()), round_2(length.abs.to_cm()), round_2(length.abs.to_inches()) - ) + )) }) } +/// Tooltip for a hovered reference. +fn ref_tooltip( + world: &(dyn World + 'static), + frames: &[Frame], + leaf: &LinkedNode, +) -> Option { + if leaf.kind() != SyntaxKind::RefMarker { + return None; + } + + let target = leaf.text().trim_start_matches('@'); + for (label, detail) in analyze_labels(world, frames).0 { + if label.0 == target { + return Some(Tooltip::Text(detail?.into())); + } + } + + None +} + /// Tooltips for components of a named parameter. fn named_param_tooltip( world: &(dyn World + 'static), diff --git a/src/model/typeset.rs b/src/model/typeset.rs index 8719ea0c1..fe4332883 100644 --- a/src/model/typeset.rs +++ b/src/model/typeset.rs @@ -35,7 +35,7 @@ pub fn typeset(world: Tracked, content: &Content) -> SourceResult= 5 || introspector.update(&document) { + if iter >= 5 || introspector.update(&document.pages) { break; } } @@ -49,13 +49,10 @@ pub fn typeset(world: Tracked, content: &Content) -> SourceResult { /// The compilation environment. - #[doc(hidden)] pub world: Tracked<'a, dyn World>, /// Provides stable identities to nodes. - #[doc(hidden)] pub provider: TrackedMut<'a, StabilityProvider>, /// Provides access to information about the document. - #[doc(hidden)] pub introspector: Tracked<'a, Introspector>, } @@ -127,7 +124,6 @@ impl StabilityProvider { } /// Provides access to information about the document. -#[doc(hidden)] pub struct Introspector { init: bool, nodes: Vec<(StableId, Content)>, @@ -136,7 +132,7 @@ pub struct Introspector { impl Introspector { /// Create a new introspector. - fn new() -> Self { + pub fn new() -> Self { Self { init: false, nodes: vec![], @@ -146,10 +142,10 @@ impl Introspector { /// Update the information given new frames and return whether we can stop /// layouting. - fn update(&mut self, document: &Document) -> bool { + pub fn update(&mut self, frames: &[Frame]) -> bool { self.nodes.clear(); - for (i, frame) in document.pages.iter().enumerate() { + for (i, frame) in frames.iter().enumerate() { let page = NonZeroUsize::new(1 + i).unwrap(); self.extract(frame, page, Transform::identity()); } @@ -171,6 +167,11 @@ impl Introspector { true } + /// Iterate over all nodes. + pub fn iter(&self) -> impl Iterator { + self.nodes.iter().map(|(_, node)| node) + } + /// Extract metadata from a frame. fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) { for (pos, element) in frame.elements() { @@ -199,12 +200,12 @@ impl Introspector { #[comemo::track] impl Introspector { /// Whether this introspector is not yet initialized. - fn init(&self) -> bool { + pub fn init(&self) -> bool { self.init } /// Locate all metadata matches for the given selector. - fn locate(&self, selector: Selector) -> Vec<(StableId, &Content)> { + pub fn locate(&self, selector: Selector) -> Vec<(StableId, &Content)> { let nodes = self.locate_impl(&selector); let mut queries = self.queries.borrow_mut(); if !queries.iter().any(|(prev, _)| prev == &selector) { diff --git a/tests/ref/meta/bibliography.png b/tests/ref/meta/bibliography.png new file mode 100644 index 0000000000000000000000000000000000000000..3ff542d16db4722ad72d4fc5317dddedb1857c06 GIT binary patch literal 38227 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 z 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 DSG6$< literal 0 HcmV?d00001 diff --git a/tests/typ/compute/data.typ b/tests/typ/compute/data.typ index d80d4857b..43746e188 100644 --- a/tests/typ/compute/data.typ +++ b/tests/typ/compute/data.typ @@ -11,8 +11,8 @@ #let data = read("/missing.txt") --- -// Error: 18-37 file is not valid utf-8 -#let data = read("/invalid-utf8.txt") +// Error: 18-28 file is not valid utf-8 +#let data = read("/bad.txt") --- // Test reading CSV data. diff --git a/tests/typ/meta/bibliography.typ b/tests/typ/meta/bibliography.typ new file mode 100644 index 000000000..2e2ddd358 --- /dev/null +++ b/tests/typ/meta/bibliography.typ @@ -0,0 +1,27 @@ +// Test citations and bibliographies. + +--- +// Error: 15-25 failed to parse biblatex file: wrong number of digits in line 5 +#bibliography("/bad.bib") + +--- +// Test ambigious reference. += Introduction +// Error: 1-7 label occurs in the document and its bibliography +@arrgh +#bibliography("/works.bib") + +--- +#set page(width: 200pt) += Details +See also #cite("arrgh", [p. 22]), @arrgh[p. 4], and @cannonfodder[p. 5]. +#bibliography("/works.bib") + +--- +// Test unconventional order. +#set page(width: 200pt) +#bibliography("/works.bib", title: [Works to be cited], style: "author-date") +#line(length: 100%) +The net-work is a creature of its own. @stupid +This is close to piratery! @arrgh +And quark! @quark