summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/model/reference.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/model/reference.rs')
-rw-r--r--crates/typst-library/src/model/reference.rs307
1 files changed, 307 insertions, 0 deletions
diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs
new file mode 100644
index 00000000..bc1919d6
--- /dev/null
+++ b/crates/typst-library/src/model/reference.rs
@@ -0,0 +1,307 @@
+use comemo::Track;
+use ecow::eco_format;
+
+use crate::diag::{bail, At, Hint, SourceResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, Content, Context, Func, IntoValue, Label, NativeElement, Packed, Show,
+ Smart, StyleChain, Synthesize,
+};
+use crate::introspection::{Counter, Locatable};
+use crate::math::EquationElem;
+use crate::model::{
+ BibliographyElem, CiteElem, Destination, Figurable, FootnoteElem, Numbering,
+};
+use crate::text::TextElem;
+
+/// A reference to a label or bibliography.
+///
+/// Produces a textual reference to a label. For example, a reference to a
+/// heading will yield an appropriate string such as "Section 1" for a reference
+/// to the first heading. The references are also links to the respective
+/// element. Reference syntax can also be used to [cite] from a bibliography.
+///
+/// Referenceable elements include [headings]($heading), [figures]($figure),
+/// [equations]($math.equation), and [footnotes]($footnote). To create a custom
+/// referenceable element like a theorem, you can create a figure of a custom
+/// [`kind`]($figure.kind) and write a show rule for it. In the future, there
+/// might be a more direct way to define a custom referenceable element.
+///
+/// If you just want to link to a labelled element and not get an automatic
+/// textual reference, consider using the [`link`] function instead.
+///
+/// # Example
+/// ```example
+/// #set heading(numbering: "1.")
+/// #set math.equation(numbering: "(1)")
+///
+/// = Introduction <intro>
+/// Recent developments in
+/// typesetting software have
+/// rekindled hope in previously
+/// frustrated researchers. @distress
+/// As shown in @results, we ...
+///
+/// = Results <results>
+/// We discuss our approach in
+/// comparison with others.
+///
+/// == Performance <perf>
+/// @slow demonstrates what slow
+/// software looks like.
+/// $ T(n) = O(2^n) $ <slow>
+///
+/// #bibliography("works.bib")
+/// ```
+///
+/// # Syntax
+/// This function also has dedicated syntax: A reference to a label can be
+/// created by typing an `@` followed by the name of the label (e.g.
+/// `[= Introduction <intro>]` can be referenced by typing `[@intro]`).
+///
+/// To customize the supplement, add content in square brackets after the
+/// reference: `[@intro[Chapter]]`.
+///
+/// # Customization
+/// If you write a show rule for references, you can access the referenced
+/// element through the `element` field of the reference. The `element` may
+/// be `{none}` even if it exists if Typst hasn't discovered it yet, so you
+/// always need to handle that case in your code.
+///
+/// ```example
+/// #set heading(numbering: "1.")
+/// #set math.equation(numbering: "(1)")
+///
+/// #show ref: it => {
+/// let eq = math.equation
+/// let el = it.element
+/// if el != none and el.func() == eq {
+/// // Override equation references.
+/// link(el.location(),numbering(
+/// el.numbering,
+/// ..counter(eq).at(el.location())
+/// ))
+/// } else {
+/// // Other references as usual.
+/// it
+/// }
+/// }
+///
+/// = Beginnings <beginning>
+/// In @beginning we prove @pythagoras.
+/// $ a^2 + b^2 = c^2 $ <pythagoras>
+/// ```
+#[elem(title = "Reference", Synthesize, Locatable, Show)]
+pub struct RefElem {
+ /// The target label that should be referenced.
+ ///
+ /// Can be a label that is defined in the document or an entry from the
+ /// [`bibliography`].
+ #[required]
+ pub target: Label,
+
+ /// A supplement for the reference.
+ ///
+ /// For references to headings or figures, this is added before the
+ /// referenced number. For citations, this can be used to add a page number.
+ ///
+ /// If a function is specified, it is passed the referenced element and
+ /// should return content.
+ ///
+ /// ```example
+ /// #set heading(numbering: "1.")
+ /// #set ref(supplement: it => {
+ /// if it.func() == heading {
+ /// "Chapter"
+ /// } else {
+ /// "Thing"
+ /// }
+ /// })
+ ///
+ /// = Introduction <intro>
+ /// In @intro, we see how to turn
+ /// Sections into Chapters. And
+ /// in @intro[Part], it is done
+ /// manually.
+ /// ```
+ #[borrowed]
+ pub supplement: Smart<Option<Supplement>>,
+
+ /// A synthesized citation.
+ #[synthesized]
+ pub citation: Option<Packed<CiteElem>>,
+
+ /// The referenced element.
+ #[synthesized]
+ pub element: Option<Content>,
+}
+
+impl Synthesize for Packed<RefElem> {
+ fn synthesize(
+ &mut self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ ) -> SourceResult<()> {
+ let citation = to_citation(self, engine, styles)?;
+
+ let elem = self.as_mut();
+ elem.push_citation(Some(citation));
+ elem.push_element(None);
+
+ let target = *elem.target();
+ if !BibliographyElem::has(engine, target) {
+ if let Ok(found) = engine.introspector.query_label(target).cloned() {
+ elem.push_element(Some(found));
+ return Ok(());
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl Show for Packed<RefElem> {
+ #[typst_macros::time(name = "ref", span = self.span())]
+ fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ let target = *self.target();
+ let elem = engine.introspector.query_label(target);
+ let span = self.span();
+
+ if BibliographyElem::has(engine, target) {
+ if elem.is_ok() {
+ bail!(span, "label occurs in the document and its bibliography");
+ }
+
+ return Ok(to_citation(self, engine, styles)?.pack().spanned(span));
+ }
+
+ let elem = elem.at(span)?;
+
+ if let Some(footnote) = elem.to_packed::<FootnoteElem>() {
+ return Ok(footnote.into_ref(target).pack().spanned(span));
+ }
+
+ let elem = elem.clone();
+ let refable = elem
+ .with::<dyn Refable>()
+ .ok_or_else(|| {
+ if elem.can::<dyn Figurable>() {
+ eco_format!(
+ "cannot reference {} directly, try putting it into a figure",
+ elem.func().name()
+ )
+ } else {
+ eco_format!("cannot reference {}", elem.func().name())
+ }
+ })
+ .at(span)?;
+
+ let numbering = refable
+ .numbering()
+ .ok_or_else(|| {
+ eco_format!("cannot reference {} without numbering", elem.func().name())
+ })
+ .hint(eco_format!(
+ "you can enable {} numbering with `#set {}(numbering: \"1.\")`",
+ elem.func().name(),
+ if elem.func() == EquationElem::elem() {
+ "math.equation"
+ } else {
+ elem.func().name()
+ }
+ ))
+ .at(span)?;
+
+ let loc = elem.location().unwrap();
+ let numbers = refable.counter().display_at_loc(
+ engine,
+ loc,
+ styles,
+ &numbering.clone().trimmed(),
+ )?;
+
+ let supplement = match self.supplement(styles).as_ref() {
+ Smart::Auto => refable.supplement(),
+ Smart::Custom(None) => Content::empty(),
+ Smart::Custom(Some(supplement)) => {
+ supplement.resolve(engine, styles, [elem])?
+ }
+ };
+
+ let mut content = numbers;
+ if !supplement.is_empty() {
+ content = supplement + TextElem::packed("\u{a0}") + content;
+ }
+
+ Ok(content.linked(Destination::Location(loc)))
+ }
+}
+
+/// Turn a reference into a citation.
+fn to_citation(
+ reference: &Packed<RefElem>,
+ engine: &mut Engine,
+ styles: StyleChain,
+) -> SourceResult<Packed<CiteElem>> {
+ let mut elem = Packed::new(CiteElem::new(*reference.target()).with_supplement(
+ match reference.supplement(styles).clone() {
+ Smart::Custom(Some(Supplement::Content(content))) => Some(content),
+ _ => None,
+ },
+ ));
+
+ if let Some(loc) = reference.location() {
+ elem.set_location(loc);
+ }
+
+ elem.synthesize(engine, styles)?;
+
+ Ok(elem)
+}
+
+/// Additional content for a reference.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum Supplement {
+ Content(Content),
+ Func(Func),
+}
+
+impl Supplement {
+ /// Tries to resolve the supplement into its content.
+ pub fn resolve<T: IntoValue>(
+ &self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ args: impl IntoIterator<Item = T>,
+ ) -> SourceResult<Content> {
+ Ok(match self {
+ Supplement::Content(content) => content.clone(),
+ Supplement::Func(func) => func
+ .call(engine, Context::new(None, Some(styles)).track(), args)?
+ .display(),
+ })
+ }
+}
+
+cast! {
+ Supplement,
+ self => match self {
+ Self::Content(v) => v.into_value(),
+ Self::Func(v) => v.into_value(),
+ },
+ v: Content => Self::Content(v),
+ v: Func => Self::Func(v),
+}
+
+/// Marks an element as being able to be referenced. This is used to implement
+/// the `@ref` element.
+pub trait Refable {
+ /// The supplement, if not overridden by the reference.
+ fn supplement(&self) -> Content;
+
+ /// Returns the counter of this element.
+ fn counter(&self) -> Counter;
+
+ /// Returns the numbering of this element.
+ fn numbering(&self) -> Option<&Numbering>;
+}