mirror of
https://github.com/typst/typst
synced 2025-05-13 12:36:23 +08:00
Bibliography and citations
This commit is contained in:
parent
2a86e4db0b
commit
89f44f220d
199
Cargo.lock
generated
199
Cargo.lock
generated
@ -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"
|
||||
|
6
assets/files/bad.bib
Normal file
6
assets/files/bad.bib
Normal file
@ -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},
|
||||
}
|
32
assets/files/works.bib
Normal file
32
assets/files/works.bib
Normal 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}
|
||||
}
|
@ -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" }
|
||||
|
@ -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| {
|
||||
|
472
library/src/meta/bibliography.rs
Normal file
472
library/src/meta/bibliography.rs
Normal 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)
|
||||
}
|
@ -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 {
|
||||
|
@ -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::*;
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
.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<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 {
|
||||
if vt.locatable() {
|
||||
bail!(self.span(), if matches.is_empty() {
|
||||
|
@ -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: `- ...`.
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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),
|
||||
|
@ -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) {
|
||||
|
BIN
tests/ref/meta/bibliography.png
Normal file
BIN
tests/ref/meta/bibliography.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
@ -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.
|
||||
|
27
tests/typ/meta/bibliography.typ
Normal file
27
tests/typ/meta/bibliography.typ
Normal 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
|
Loading…
x
Reference in New Issue
Block a user