Bibliography and citations

This commit is contained in:
Laurenz 2023-03-14 22:35:31 +01:00
parent 2a86e4db0b
commit 89f44f220d
20 changed files with 897 additions and 50 deletions

199
Cargo.lock generated
View File

@ -56,6 +56,19 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 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]] [[package]]
name = "bincode" name = "bincode"
version = "1.3.3" version = "1.3.3"
@ -304,6 +317,17 @@ dependencies = [
"winapi", "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]] [[package]]
name = "ecow" name = "ecow"
version = "0.1.0" version = "0.1.0"
@ -363,6 +387,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 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]] [[package]]
name = "fsevent-sys" name = "fsevent-sys"
version = "4.1.0" version = "4.1.0"
@ -408,6 +441,26 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 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]] [[package]]
name = "heck" name = "heck"
version = "0.4.1" version = "0.4.1"
@ -452,6 +505,16 @@ dependencies = [
"cxx-build", "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]] [[package]]
name = "if_chain" name = "if_chain"
version = "1.0.2" version = "1.0.2"
@ -523,6 +586,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "isolang"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b64fd6448ee8a45ce6e4365c58e4fa7d8740cba2ed70db3e9ab4879ebd93eaaa"
dependencies = [
"phf",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.8" version = "0.4.8"
@ -729,12 +801,24 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "numerals"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e25be21376a772d15f97ae789845340a9651d3c4246ff5ebb6a2b35f9c37bd31"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.17.0" version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "paste"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
[[package]] [[package]]
name = "pdf-writer" name = "pdf-writer"
version = "0.6.0" version = "0.6.0"
@ -746,6 +830,30 @@ dependencies = [
"ryu", "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]] [[package]]
name = "pico-args" name = "pico-args"
version = "0.4.2" version = "0.4.2"
@ -917,6 +1025,12 @@ dependencies = [
"xmlparser", "xmlparser",
] ]
[[package]]
name = "rustversion"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
[[package]] [[package]]
name = "rustybuzz" name = "rustybuzz"
version = "0.5.1" version = "0.5.1"
@ -1033,6 +1147,28 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 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]] [[package]]
name = "subsetter" name = "subsetter"
version = "0.1.0" version = "0.1.0"
@ -1141,6 +1277,30 @@ dependencies = [
"safe_arch", "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]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.15.2" version = "0.15.2"
@ -1246,6 +1406,7 @@ dependencies = [
"comemo", "comemo",
"csv", "csv",
"ecow", "ecow",
"hayagriva",
"hypher", "hypher",
"kurbo", "kurbo",
"lipsum", "lipsum",
@ -1292,6 +1453,24 @@ dependencies = [
"walkdir", "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]] [[package]]
name = "unicase" name = "unicase"
version = "2.6.0" version = "2.6.0"
@ -1336,6 +1515,15 @@ name = "unicode-math-class"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/typst/unicode-math-class#a7ac7dd75cd79ab2e0bdb629036cb913371608d2" 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]] [[package]]
name = "unicode-script" name = "unicode-script"
version = "0.5.5" version = "0.5.5"
@ -1372,6 +1560,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" 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]] [[package]]
name = "usvg" name = "usvg"
version = "0.22.0" version = "0.22.0"

6
assets/files/bad.bib Normal file
View File

@ -0,0 +1,6 @@
@article{arrgh,
title = {Anarrghchy: The Law and Economics of Pirate Organization},
author = {Leeson, Peter T.},
crossref = {polecon},
date = {19XXX-XX-XX},
}

32
assets/files/works.bib Normal file
View File

@ -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}
}

View File

@ -14,6 +14,7 @@ typst = { path = ".." }
comemo = { git = "https://github.com/typst/comemo" } comemo = { git = "https://github.com/typst/comemo" }
csv = "1" csv = "1"
ecow = "0.1" ecow = "0.1"
hayagriva = { git = "https://github.com/typst/hayagriva" }
hypher = "0.1" hypher = "0.1"
kurbo = "0.8" kurbo = "0.8"
lipsum = { git = "https://github.com/reknih/lipsum" } lipsum = { git = "https://github.com/reknih/lipsum" }

View File

@ -89,6 +89,8 @@ fn global(math: Module, calc: Module) -> Module {
global.define("outline", meta::OutlineNode::id()); global.define("outline", meta::OutlineNode::id());
global.define("heading", meta::HeadingNode::id()); global.define("heading", meta::HeadingNode::id());
global.define("figure", meta::FigureNode::id()); global.define("figure", meta::FigureNode::id());
global.define("cite", meta::CiteNode::id());
global.define("bibliography", meta::BibliographyNode::id());
global.define("numbering", meta::numbering); global.define("numbering", meta::numbering);
// Symbols. // Symbols.
@ -179,7 +181,7 @@ fn items() -> LangItems {
raw: |text, lang, block| { raw: |text, lang, block| {
let mut node = text::RawNode::new(text).with_block(block); let mut node = text::RawNode::new(text).with_block(block);
if let Some(lang) = lang { if let Some(lang) = lang {
node = node.with_lang(Some(lang)); node.push_lang(Some(lang));
} }
node.pack() node.pack()
}, },
@ -194,6 +196,7 @@ fn items() -> LangItems {
} }
node.pack() node.pack()
}, },
bibliography_keys: meta::BibliographyNode::keys,
heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(), heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(),
list_item: |body| layout::ListItem::new(body).pack(), list_item: |body| layout::ListItem::new(body).pack(),
enum_item: |number, body| { enum_item: |number, body| {

View File

@ -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)
}

View File

@ -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>); pub struct HeadingCounter(Vec<NonZeroUsize>);
impl HeadingCounter { impl HeadingCounter {

View File

@ -1,5 +1,6 @@
//! Interaction between document parts. //! Interaction between document parts.
mod bibliography;
mod document; mod document;
mod figure; mod figure;
mod heading; mod heading;
@ -8,6 +9,7 @@ mod numbering;
mod outline; mod outline;
mod reference; mod reference;
pub use self::bibliography::*;
pub use self::document::*; pub use self::document::*;
pub use self::figure::*; pub use self::figure::*;
pub use self::heading::*; pub use self::heading::*;

View File

@ -65,7 +65,7 @@ pub fn numbering(
numbering.apply(vm.world(), &numbers)? numbering.apply(vm.world(), &numbers)?
} }
/// How to number an enumeration. /// How to number a sequence of things.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub enum Numbering { pub enum Numbering {
/// A pattern with prefix, numbering, lower / upper case and suffix. /// A pattern with prefix, numbering, lower / upper case and suffix.

View File

@ -1,4 +1,4 @@
use super::{FigureNode, HeadingNode, LocalName, Numbering}; use super::{BibliographyNode, CiteNode, FigureNode, HeadingNode, LocalName, Numbering};
use crate::prelude::*; use crate::prelude::*;
use crate::text::TextNode; use crate::text::TextNode;
@ -35,7 +35,7 @@ use crate::text::TextNode;
/// ///
/// Display: Reference /// Display: Reference
/// Category: meta /// Category: meta
#[node(Synthesize, Show)] #[node(Show)]
pub struct RefNode { pub struct RefNode {
/// The target label that should be referenced. /// The target label that should be referenced.
#[required] #[required]
@ -60,27 +60,33 @@ pub struct RefNode {
/// In @intro, we see how to turn /// In @intro, we see how to turn
/// Sections into Chapters. /// Sections into Chapters.
/// ``` /// ```
/// All elements with the target label in the document.
#[synthesized]
pub matches: Vec<Content>,
pub supplement: Smart<Option<Supplement>>, pub supplement: Smart<Option<Supplement>>,
} }
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 { impl Show for RefNode {
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
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 { let [target] = matches.as_slice() else {
if vt.locatable() { if vt.locatable() {
bail!(self.span(), if matches.is_empty() { bail!(self.span(), if matches.is_empty() {

View File

@ -2,6 +2,7 @@ use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use comemo::Tracked;
use ecow::EcoString; use ecow::EcoString;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -9,8 +10,9 @@ use super::Module;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::doc::Document; use crate::doc::Document;
use crate::geom::{Abs, Dir}; 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::util::hash128;
use crate::World;
/// Definition of Typst's standard library. /// Definition of Typst's standard library.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
@ -61,6 +63,11 @@ pub struct LangItems {
pub link: fn(url: EcoString) -> Content, pub link: fn(url: EcoString) -> Content,
/// A reference: `@target`, `@target[..]`. /// A reference: `@target`, `@target[..]`.
pub reference: fn(target: Label, supplement: Option<Content>) -> Content, 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`. /// A section heading: `= Introduction`.
pub heading: fn(level: NonZeroUsize, body: Content) -> Content, pub heading: fn(level: NonZeroUsize, body: Content) -> Content,
/// An item in a bullet list: `- ...`. /// An item in a bullet list: `- ...`.

View File

@ -1,8 +1,11 @@
use std::path::PathBuf; use std::path::PathBuf;
use comemo::Track; use comemo::Track;
use ecow::EcoString;
use crate::doc::Frame;
use crate::eval::{eval, Module, Route, Tracer, Value}; use crate::eval::{eval, Module, Route, Tracer, Value};
use crate::model::{Introspector, Label};
use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::syntax::{ast, LinkedNode, Source, SyntaxKind};
use crate::util::PathExt; use crate::util::PathExt;
use crate::World; use crate::World;
@ -64,3 +67,37 @@ pub fn analyze_import(
let source = world.source(id); let source = world.source(id);
eval(world.track(), route.track(), tracer.track_mut(), source).ok() 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)
}

View File

@ -4,7 +4,9 @@ use ecow::{eco_format, EcoString};
use if_chain::if_chain; use if_chain::if_chain;
use unscanny::Scanner; use unscanny::Scanner;
use super::analyze::analyze_labels;
use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; 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::eval::{methods_on, CastInfo, Library, Scope, Value};
use crate::syntax::{ use crate::syntax::{
ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, 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. /// control and space or something similar.
pub fn autocomplete( pub fn autocomplete(
world: &(dyn World + 'static), world: &(dyn World + 'static),
frames: &[Frame],
source: &Source, source: &Source,
cursor: usize, cursor: usize,
explicit: bool, explicit: bool,
) -> Option<(usize, Vec<Completion>)> { ) -> 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) let _ = complete_comments(&mut ctx)
|| complete_field_accesses(&mut ctx) || complete_field_accesses(&mut ctx)
@ -78,7 +81,10 @@ fn complete_comments(ctx: &mut CompletionContext) -> bool {
/// Complete in markup mode. /// Complete in markup mode.
fn complete_markup(ctx: &mut CompletionContext) -> bool { fn complete_markup(ctx: &mut CompletionContext) -> bool {
// Bail if we aren't even in markup. // 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; return false;
} }
@ -96,6 +102,13 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool {
return true; 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 = |". // Behind a half-completed binding: "#let x = |".
if_chain! { if_chain! {
if let Some(prev) = ctx.leaf.prev_leaf(); if let Some(prev) = ctx.leaf.prev_leaf();
@ -850,6 +863,7 @@ fn code_completions(ctx: &mut CompletionContext, hashtag: bool) {
/// Context for autocompletion. /// Context for autocompletion.
struct CompletionContext<'a> { struct CompletionContext<'a> {
world: &'a (dyn World + 'static), world: &'a (dyn World + 'static),
frames: &'a [Frame],
library: &'a Library, library: &'a Library,
source: &'a Source, source: &'a Source,
global: &'a Scope, global: &'a Scope,
@ -869,6 +883,7 @@ impl<'a> CompletionContext<'a> {
/// Create a new autocompletion context. /// Create a new autocompletion context.
fn new( fn new(
world: &'a (dyn World + 'static), world: &'a (dyn World + 'static),
frames: &'a [Frame],
source: &'a Source, source: &'a Source,
cursor: usize, cursor: usize,
explicit: bool, explicit: bool,
@ -878,6 +893,7 @@ impl<'a> CompletionContext<'a> {
let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
Some(Self { Some(Self {
world, world,
frames,
library, library,
source, source,
global: &library.global.scope(), 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. /// Add a completion for a specific value.
fn value_completion( fn value_completion(
&mut self, &mut self,

View File

@ -6,6 +6,7 @@ mod highlight;
mod jump; mod jump;
mod tooltip; mod tooltip;
pub use self::analyze::analyze_labels;
pub use self::complete::*; pub use self::complete::*;
pub use self::highlight::*; pub use self::highlight::*;
pub use self::jump::*; pub use self::jump::*;
@ -13,15 +14,17 @@ pub use self::tooltip::*;
use std::fmt::Write; use std::fmt::Write;
use ecow::{eco_format, EcoString};
use self::analyze::*; use self::analyze::*;
use crate::font::{FontInfo, FontStyle}; use crate::font::{FontInfo, FontStyle};
/// Extract the first sentence of plain text of a piece of documentation. /// Extract the first sentence of plain text of a piece of documentation.
/// ///
/// Removes Markdown formatting. /// 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 s = unscanny::Scanner::new(docs);
let mut output = String::new(); let mut output = EcoString::new();
let mut link = false; let mut link = false;
while let Some(c) = s.eat() { while let Some(c) = s.eat() {
match c { match c {
@ -62,7 +65,7 @@ fn plain_docs_sentence(docs: &str) -> String {
} }
/// Create a short description of a font family. /// 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(); let mut infos: Vec<_> = variants.collect();
infos.sort_by_key(|info| info.variant); 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 count = infos.len();
let s = if count == 1 { "" } else { "s" }; 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 { if min_weight == max_weight {
write!(detail, " Weight {min_weight}.").unwrap(); write!(detail, " Weight {min_weight}.").unwrap();

View File

@ -1,20 +1,22 @@
use std::fmt::Write; use std::fmt::Write;
use ecow::EcoString; use ecow::{eco_format, EcoString};
use if_chain::if_chain; use if_chain::if_chain;
use super::analyze::analyze_labels;
use super::{analyze_expr, plain_docs_sentence, summarize_font_family}; use super::{analyze_expr, plain_docs_sentence, summarize_font_family};
use crate::doc::Frame;
use crate::eval::{CastInfo, Tracer, Value}; use crate::eval::{CastInfo, Tracer, Value};
use crate::geom::{round_2, Length, Numeric}; use crate::geom::{round_2, Length, Numeric};
use crate::syntax::ast; use crate::syntax::{ast, LinkedNode, Source, SyntaxKind};
use crate::syntax::{LinkedNode, Source, SyntaxKind};
use crate::util::pretty_comma_list; use crate::util::pretty_comma_list;
use crate::World; use crate::World;
/// Describe the item under the cursor. /// Describe the item under the cursor.
pub fn tooltip( pub fn tooltip(
world: &(dyn World + 'static), world: &(dyn World + 'static),
frames: &[Frame],
source: &Source, source: &Source,
cursor: usize, cursor: usize,
) -> Option<Tooltip> { ) -> Option<Tooltip> {
@ -22,6 +24,7 @@ pub fn tooltip(
named_param_tooltip(world, &leaf) named_param_tooltip(world, &leaf)
.or_else(|| font_tooltip(world, &leaf)) .or_else(|| font_tooltip(world, &leaf))
.or_else(|| ref_tooltip(world, frames, &leaf))
.or_else(|| expr_tooltip(world, &leaf)) .or_else(|| expr_tooltip(world, &leaf))
} }
@ -29,9 +32,9 @@ pub fn tooltip(
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Tooltip { pub enum Tooltip {
/// A string of text. /// A string of text.
Text(String), Text(EcoString),
/// A string of Typst code. /// A string of Typst code.
Code(String), Code(EcoString),
} }
/// Tooltip for a hovered expression. /// 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 &Value::Length(length) = value {
if let Some(tooltip) = length_tooltip(length) { 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); 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. /// 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(|| { length.em.is_zero().then(|| {
format!( Tooltip::Code(eco_format!(
"{}pt = {}mm = {}cm = {}in", "{}pt = {}mm = {}cm = {}in",
round_2(length.abs.to_pt()), round_2(length.abs.to_pt()),
round_2(length.abs.to_mm()), round_2(length.abs.to_mm()),
round_2(length.abs.to_cm()), round_2(length.abs.to_cm()),
round_2(length.abs.to_inches()) 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. /// Tooltips for components of a named parameter.
fn named_param_tooltip( fn named_param_tooltip(
world: &(dyn World + 'static), world: &(dyn World + 'static),

View File

@ -35,7 +35,7 @@ pub fn typeset(world: Tracked<dyn World>, content: &Content) -> SourceResult<Doc
document = (library.items.layout)(&mut vt, content, styles)?; document = (library.items.layout)(&mut vt, content, styles)?;
iter += 1; iter += 1;
if iter >= 5 || introspector.update(&document) { if iter >= 5 || introspector.update(&document.pages) {
break; break;
} }
} }
@ -49,13 +49,10 @@ pub fn typeset(world: Tracked<dyn World>, content: &Content) -> SourceResult<Doc
/// [Vm](crate::eval::Vm) for typesetting. /// [Vm](crate::eval::Vm) for typesetting.
pub struct Vt<'a> { pub struct Vt<'a> {
/// The compilation environment. /// The compilation environment.
#[doc(hidden)]
pub world: Tracked<'a, dyn World>, pub world: Tracked<'a, dyn World>,
/// Provides stable identities to nodes. /// Provides stable identities to nodes.
#[doc(hidden)]
pub provider: TrackedMut<'a, StabilityProvider>, pub provider: TrackedMut<'a, StabilityProvider>,
/// Provides access to information about the document. /// Provides access to information about the document.
#[doc(hidden)]
pub introspector: Tracked<'a, Introspector>, pub introspector: Tracked<'a, Introspector>,
} }
@ -127,7 +124,6 @@ impl StabilityProvider {
} }
/// Provides access to information about the document. /// Provides access to information about the document.
#[doc(hidden)]
pub struct Introspector { pub struct Introspector {
init: bool, init: bool,
nodes: Vec<(StableId, Content)>, nodes: Vec<(StableId, Content)>,
@ -136,7 +132,7 @@ pub struct Introspector {
impl Introspector { impl Introspector {
/// Create a new introspector. /// Create a new introspector.
fn new() -> Self { pub fn new() -> Self {
Self { Self {
init: false, init: false,
nodes: vec![], nodes: vec![],
@ -146,10 +142,10 @@ impl Introspector {
/// Update the information given new frames and return whether we can stop /// Update the information given new frames and return whether we can stop
/// layouting. /// layouting.
fn update(&mut self, document: &Document) -> bool { pub fn update(&mut self, frames: &[Frame]) -> bool {
self.nodes.clear(); 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(); let page = NonZeroUsize::new(1 + i).unwrap();
self.extract(frame, page, Transform::identity()); self.extract(frame, page, Transform::identity());
} }
@ -171,6 +167,11 @@ impl Introspector {
true true
} }
/// Iterate over all nodes.
pub fn iter(&self) -> impl Iterator<Item = &Content> {
self.nodes.iter().map(|(_, node)| node)
}
/// Extract metadata from a frame. /// Extract metadata from a frame.
fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) { fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) {
for (pos, element) in frame.elements() { for (pos, element) in frame.elements() {
@ -199,12 +200,12 @@ impl Introspector {
#[comemo::track] #[comemo::track]
impl Introspector { impl Introspector {
/// Whether this introspector is not yet initialized. /// Whether this introspector is not yet initialized.
fn init(&self) -> bool { pub fn init(&self) -> bool {
self.init self.init
} }
/// Locate all metadata matches for the given selector. /// 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 nodes = self.locate_impl(&selector);
let mut queries = self.queries.borrow_mut(); let mut queries = self.queries.borrow_mut();
if !queries.iter().any(|(prev, _)| prev == &selector) { if !queries.iter().any(|(prev, _)| prev == &selector) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -11,8 +11,8 @@
#let data = read("/missing.txt") #let data = read("/missing.txt")
--- ---
// Error: 18-37 file is not valid utf-8 // Error: 18-28 file is not valid utf-8
#let data = read("/invalid-utf8.txt") #let data = read("/bad.txt")
--- ---
// Test reading CSV data. // Test reading CSV data.

View File

@ -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