summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-03-14 22:35:31 +0100
committerLaurenz <laurmaedje@gmail.com>2023-03-14 22:53:54 +0100
commit89f44f220de2972452dd816fe59836ba76953d59 (patch)
tree2749428e8e10252588bb68c0e8e9bfd150f28a9c
parent2a86e4db0bb3894d1cc3b94e1a1af31a6cd87b80 (diff)
Bibliography and citations
-rw-r--r--Cargo.lock199
-rw-r--r--assets/files/bad.bib6
-rw-r--r--assets/files/bad.txt (renamed from assets/files/invalid-utf8.txt)bin70 -> 70 bytes
-rw-r--r--assets/files/works.bib32
-rw-r--r--library/Cargo.toml1
-rw-r--r--library/src/lib.rs5
-rw-r--r--library/src/meta/bibliography.rs472
-rw-r--r--library/src/meta/heading.rs2
-rw-r--r--library/src/meta/mod.rs2
-rw-r--r--library/src/meta/numbering.rs2
-rw-r--r--library/src/meta/reference.rs36
-rw-r--r--src/eval/library.rs9
-rw-r--r--src/ide/analyze.rs37
-rw-r--r--src/ide/complete.rs32
-rw-r--r--src/ide/mod.rs11
-rw-r--r--src/ide/tooltip.rs43
-rw-r--r--src/model/typeset.rs21
-rw-r--r--tests/ref/meta/bibliography.pngbin0 -> 38227 bytes
-rw-r--r--tests/typ/compute/data.typ4
-rw-r--r--tests/typ/meta/bibliography.typ27
20 files changed, 894 insertions, 47 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ac4c3a1d..9547e3ac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -57,6 +57,19 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -305,6 +318,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -364,6 +388,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -409,6 +442,26 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -453,6 +506,16 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -524,6 +587,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -730,12 +802,24 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -747,6 +831,30 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -918,6 +1026,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1034,6 +1148,28 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1142,6 +1278,30 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1246,6 +1406,7 @@ dependencies = [
"comemo",
"csv",
"ecow",
+ "hayagriva",
"hypher",
"kurbo",
"lipsum",
@@ -1293,6 +1454,24 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1337,6 +1516,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1373,6 +1561,17 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/assets/files/bad.bib b/assets/files/bad.bib
new file mode 100644
index 00000000..41b4d63d
--- /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
index 71308112..71308112 100644
--- a/assets/files/invalid-utf8.txt
+++ b/assets/files/bad.txt
Binary files differ
diff --git a/assets/files/works.bib b/assets/files/works.bib
new file mode 100644
index 00000000..72f06db1
--- /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 b532883b..3c1f79d4 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 5b114d9b..c4a421d2 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 00000000..a01f9eee
--- /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::<Spanned<EcoString>>("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<Smart<Content>>,
+
+ /// The bibliography style.
+ #[default(BibliographyStyle::Ieee)]
+ pub style: BibliographyStyle,
+}
+
+impl BibliographyNode {
+ /// Find the document's bibliography.
+ pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> {
+ let mut iter = introspector.locate(Selector::node::<Self>()).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::<Self>().unwrap().clone())
+ }
+
+ /// Whether the bibliography contains the given key.
+ pub fn has(vt: &Vt, key: &str) -> bool {
+ vt.introspector
+ .locate(Selector::node::<Self>())
+ .into_iter()
+ .flat_map(|(_, node)| load(vt.world(), &node.to::<Self>().unwrap().path()))
+ .flatten()
+ .any(|entry| entry.key() == key)
+ }
+
+ /// Find all bibliography keys.
+ pub fn keys(
+ world: Tracked<dyn World>,
+ introspector: Tracked<Introspector>,
+ ) -> Vec<(EcoString, Option<EcoString>)> {
+ 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<Content> {
+ 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<Content>,
+
+ /// The citation style.
+ ///
+ /// When set to `{auto}`, automatically picks the preferred citation style
+ /// for the bibliography's style.
+ pub style: Smart<CitationStyle>,
+}
+
+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<Content> {
+ 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<StableId, Option<Content>>,
+ references: Vec<(Option<Content>, Content)>,
+}
+
+impl Works {
+ /// Prepare all things need to cite a work or format a bibliography.
+ pub fn new(vt: &Vt) -> StrResult<Arc<Self>> {
+ let bibliography = BibliographyNode::find(vt.introspector)?;
+ let style = bibliography.style(StyleChain::default());
+ let citations = vt
+ .locate_node::<CiteNode>()
+ .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<dyn World>,
+ path: &str,
+ style: BibliographyStyle,
+ citations: Vec<(StableId, EcoString, Option<Content>, CitationStyle)>,
+) -> Arc<Works> {
+ 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<dyn style::CitationStyle> =
+ 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<dyn style::BibliographyStyle> = 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<dyn World>, path: &str) -> StrResult<EcoVec<hayagriva::Entry>> {
+ 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>,
+) -> 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 4d1b87e6..48f6e229 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<NonZeroUsize>);
impl HeadingCounter {
diff --git a/library/src/meta/mod.rs b/library/src/meta/mod.rs
index 3cde2b8e..ba74dac0 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 d71fb233..c59766c8 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 f63c7e4c..b4cfa049 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<Content>,
pub supplement: Smart<Option<Supplement>>,
}
-impl Synthesize for RefNode {
- fn synthesize(&mut self, vt: &Vt, _: StyleChain) {
- let matches = vt
+impl Show for RefNode {
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ let target = self.target();
+ let supplement = self.supplement(styles);
+
+ let matches: Vec<_> = vt
.locate(Selector::Label(self.target()))
.map(|(_, node)| node.clone())
.collect();
- self.push_matches(matches);
- }
-}
+ 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()));
+ }
-impl Show for RefNode {
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- let matches = self.matches();
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 d3f7547d..1240d9bb 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>) -> Content,
+ /// The keys contained in the bibliography and short descriptions of them.
+ pub bibliography_keys: fn(
+ world: Tracked<dyn World>,
+ introspector: Tracked<Introspector>,
+ ) -> Vec<(EcoString, Option<EcoString>)>,
/// 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 3c46cca1..7338ba57 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<EcoString>)>, 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 de6f2b73..66590160 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<Completion>)> {
- 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 bee959cd..38bede0b 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<Item = &'a FontInfo>) -> String {
+fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> 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<Item = &'a FontInfo>) -> 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 a32dfb0b..0b37b7ca 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<Tooltip> {
@@ -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<Tool
if let &Value::Length(length) = value {
if let Some(tooltip) = length_tooltip(length) {
- return Some(Tooltip::Code(tooltip));
+ return Some(tooltip);
}
}
}
@@ -85,22 +88,42 @@ fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option<Tool
}
let tooltip = pretty_comma_list(&pieces, false);
- (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip))
+ (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into()))
}
/// Tooltip text for a hovered length.
-fn length_tooltip(length: Length) -> Option<String> {
+fn length_tooltip(length: Length) -> Option<Tooltip> {
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<Tooltip> {
+ 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 8719ea0c..fe433288 100644
--- a/src/model/typeset.rs
+++ b/src/model/typeset.rs
@@ -35,7 +35,7 @@ pub fn typeset(world: Tracked<dyn World>, content: &Content) -> SourceResult<Doc
document = (library.items.layout)(&mut vt, content, styles)?;
iter += 1;
- if iter >= 5 || introspector.update(&document) {
+ if iter >= 5 || introspector.update(&document.pages) {
break;
}
}
@@ -49,13 +49,10 @@ pub fn typeset(world: Tracked<dyn World>, content: &Content) -> SourceResult<Doc
/// [Vm](crate::eval::Vm) for typesetting.
pub struct Vt<'a> {
/// 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<Item = &Content> {
+ 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 00000000..3ff542d1
--- /dev/null
+++ b/tests/ref/meta/bibliography.png
Binary files differ
diff --git a/tests/typ/compute/data.typ b/tests/typ/compute/data.typ
index d80d4857..43746e18 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 00000000..2e2ddd35
--- /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 <arrgh>
+// 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