typst_library/model/
reference.rs

1use comemo::Track;
2use ecow::eco_format;
3
4use crate::diag::{At, Hint, SourceResult, bail};
5use crate::engine::Engine;
6use crate::foundations::{
7    Cast, Content, Context, Func, IntoValue, Label, NativeElement, Packed, Repr, Smart,
8    StyleChain, Synthesize, cast, elem,
9};
10use crate::introspection::{Counter, CounterKey, Locatable, Tagged};
11use crate::math::EquationElem;
12use crate::model::{
13    BibliographyElem, CiteElem, DirectLinkElem, Figurable, FootnoteElem, Numbering,
14};
15use crate::text::TextElem;
16
17/// A reference to a label or bibliography.
18///
19/// Takes a label and cross-references it. There are two kind of references,
20/// determined by its [`form`]($ref.form): `{"normal"}` and `{"page"}`.
21///
22/// The default, a `{"normal"}` reference, produces a textual reference to a
23/// label. For example, a reference to a heading will yield an appropriate
24/// string such as "Section 1" for a reference to the first heading. The word
25/// "Section" depends on the [`lang`]($text.lang) setting and is localized
26/// accordingly. The references are also links to the respective element.
27/// Reference syntax can also be used to [cite] from a bibliography.
28///
29/// As the default form requires a supplement and numbering, the label must be
30/// attached to a _referenceable element_. Referenceable elements include
31/// [headings]($heading), [figures]($figure), [equations]($math.equation), and
32/// [footnotes]($footnote). To create a custom referenceable element like a
33/// theorem, you can create a figure of a custom [`kind`]($figure.kind) and
34/// write a show rule for it. In the future, there might be a more direct way
35/// to define a custom referenceable element.
36///
37/// If you just want to link to a labelled element and not get an automatic
38/// textual reference, consider using the [`link`] function instead.
39///
40/// A `{"page"}` reference produces a page reference to a label, displaying the
41/// page number at its location. You can use the
42/// [page's supplement]($page.supplement) to modify the text before the page
43/// number. Unlike a `{"normal"}` reference, the label can be attached to any
44/// element.
45///
46/// # Example
47/// ```example
48/// #set page(numbering: "1")
49/// #set heading(numbering: "1.")
50/// #set math.equation(numbering: "(1)")
51///
52/// = Introduction <intro>
53/// Recent developments in
54/// typesetting software have
55/// rekindled hope in previously
56/// frustrated researchers. @distress
57/// As shown in @results (see
58/// #ref(<results>, form: "page")),
59/// we ...
60///
61/// = Results <results>
62/// We discuss our approach in
63/// comparison with others.
64///
65/// == Performance <perf>
66/// @slow demonstrates what slow
67/// software looks like.
68/// $ T(n) = O(2^n) $ <slow>
69///
70/// #bibliography("works.bib")
71/// ```
72///
73/// # Syntax
74/// This function also has dedicated syntax: A `{"normal"}` reference to a
75/// label can be created by typing an `@` followed by the name of the label
76/// (e.g. `[= Introduction <intro>]` can be referenced by typing `[@intro]`).
77///
78/// To customize the supplement, add content in square brackets after the
79/// reference: `[@intro[Chapter]]`.
80///
81/// # Customization
82/// When you only ever need to reference pages of a figure/table/heading/etc. in
83/// a document, the default `form` field value can be changed to `{"page"}` with
84/// a set rule. If you prefer a short "p." supplement over "page", the
85/// [`page.supplement`] field can be used for changing this:
86///
87/// ```example
88/// #set page(
89///   numbering: "1",
90///   supplement: "p.",
91/// >>> margin: (bottom: 3em),
92/// >>> footer-descent: 1.25em,
93/// )
94/// #set ref(form: "page")
95///
96/// #figure(
97///   stack(
98///     dir: ltr,
99///     spacing: 1em,
100///     circle(),
101///     square(),
102///   ),
103///   caption: [Shapes],
104/// ) <shapes>
105///
106/// #pagebreak()
107///
108/// See @shapes for examples
109/// of different shapes.
110/// ```
111///
112/// If you write a show rule for references, you can access the referenced
113/// element through the `element` field of the reference. The `element` may
114/// be `{none}` even if it exists if Typst hasn't discovered it yet, so you
115/// always need to handle that case in your code.
116///
117/// ```example
118/// #set heading(numbering: "1.")
119/// #set math.equation(numbering: "(1)")
120///
121/// #show ref: it => {
122///   let eq = math.equation
123///   let el = it.element
124///   // Skip all other references.
125///   if el == none or el.func() != eq { return it }
126///   // Override equation references.
127///   link(el.location(), numbering(
128///     el.numbering,
129///     ..counter(eq).at(el.location())
130///   ))
131/// }
132///
133/// = Beginnings <beginning>
134/// In @beginning we prove @pythagoras.
135/// $ a^2 + b^2 = c^2 $ <pythagoras>
136/// ```
137#[elem(title = "Reference", Locatable, Tagged, Synthesize)]
138pub struct RefElem {
139    /// The target label that should be referenced.
140    ///
141    /// Can be a label that is defined in the document or, if the
142    /// [`form`]($ref.form) is set to `["normal"]`, an entry from the
143    /// [`bibliography`].
144    #[required]
145    pub target: Label,
146
147    /// A supplement for the reference.
148    ///
149    /// If the [`form`]($ref.form) is set to `{"normal"}`:
150    /// - For references to headings or figures, this is added before the
151    ///   referenced number.
152    /// - For citations, this can be used to add a page number.
153    ///
154    /// If the [`form`]($ref.form) is set to `{"page"}`, then this is added
155    /// before the page number of the label referenced.
156    ///
157    /// If a function is specified, it is passed the referenced element and
158    /// should return content.
159    ///
160    /// ```example
161    /// #set heading(numbering: "1.")
162    /// #show ref.where(
163    ///   form: "normal"
164    /// ): set ref(supplement: it => {
165    ///   if it.func() == heading {
166    ///     "Chapter"
167    ///   } else {
168    ///     "Thing"
169    ///   }
170    /// })
171    ///
172    /// = Introduction <intro>
173    /// In @intro, we see how to turn
174    /// Sections into Chapters. And
175    /// in @intro[Part], it is done
176    /// manually.
177    /// ```
178    pub supplement: Smart<Option<Supplement>>,
179
180    /// The kind of reference to produce.
181    ///
182    /// ```example
183    /// #set page(numbering: "1")
184    ///
185    /// Here <here> we are on
186    /// #ref(<here>, form: "page").
187    /// ```
188    #[default(RefForm::Normal)]
189    pub form: RefForm,
190
191    /// A synthesized citation.
192    #[synthesized]
193    pub citation: Option<Packed<CiteElem>>,
194
195    /// The referenced element.
196    #[synthesized]
197    pub element: Option<Content>,
198}
199
200impl Synthesize for Packed<RefElem> {
201    fn synthesize(
202        &mut self,
203        engine: &mut Engine,
204        styles: StyleChain,
205    ) -> SourceResult<()> {
206        let citation = to_citation(self, engine, styles)?;
207
208        let elem = self.as_mut();
209        elem.citation = Some(Some(citation));
210        elem.element = Some(None);
211
212        if !BibliographyElem::has(engine, elem.target)
213            && let Ok(found) = engine.introspector.query_label(elem.target).cloned()
214        {
215            elem.element = Some(Some(found));
216            return Ok(());
217        }
218
219        Ok(())
220    }
221}
222
223impl Packed<RefElem> {
224    /// Realize as a linked, textual reference.
225    pub fn realize(
226        &self,
227        engine: &mut Engine,
228        styles: StyleChain,
229    ) -> SourceResult<Content> {
230        let elem = engine.introspector.query_label(self.target);
231        let span = self.span();
232
233        let form = self.form.get(styles);
234        if form == RefForm::Page {
235            let elem = elem.at(span)?;
236            let elem = elem.clone();
237
238            let loc = elem.location().unwrap();
239            let numbering = engine
240                .introspector
241                .page_numbering(loc)
242                .ok_or_else(|| eco_format!("cannot reference without page numbering"))
243                .hint(eco_format!(
244                    "you can enable page numbering with `#set page(numbering: \"1\")`"
245                ))
246                .at(span)?;
247            let supplement = engine.introspector.page_supplement(loc);
248
249            return realize_reference(
250                self,
251                engine,
252                styles,
253                Counter::new(CounterKey::Page),
254                numbering.clone(),
255                supplement,
256                elem,
257            );
258        }
259        // RefForm::Normal
260
261        if BibliographyElem::has(engine, self.target) {
262            if let Ok(elem) = elem {
263                bail!(
264                    span,
265                    "label `{}` occurs both in the document and its bibliography",
266                    self.target.repr();
267                    hint: "change either the {}'s label or the \
268                           bibliography key to resolve the ambiguity",
269                    elem.func().name(),
270                );
271            }
272
273            return Ok(to_citation(self, engine, styles)?.pack().spanned(span));
274        }
275
276        let elem = elem.at(span)?;
277
278        if let Some(footnote) = elem.to_packed::<FootnoteElem>() {
279            return Ok(footnote.into_ref(self.target).pack().spanned(span));
280        }
281
282        let elem = elem.clone();
283        let refable = elem
284            .with::<dyn Refable>()
285            .ok_or_else(|| {
286                if elem.can::<dyn Figurable>() {
287                    eco_format!(
288                        "cannot reference {} directly, try putting it into a figure",
289                        elem.func().name()
290                    )
291                } else {
292                    eco_format!("cannot reference {}", elem.func().name())
293                }
294            })
295            .at(span)?;
296
297        let numbering = refable
298            .numbering()
299            .ok_or_else(|| {
300                eco_format!("cannot reference {} without numbering", elem.func().name())
301            })
302            .hint(eco_format!(
303                "you can enable {} numbering with `#set {}(numbering: \"1.\")`",
304                elem.func().name(),
305                if elem.func() == EquationElem::ELEM {
306                    "math.equation"
307                } else {
308                    elem.func().name()
309                }
310            ))
311            .at(span)?;
312
313        realize_reference(
314            self,
315            engine,
316            styles,
317            refable.counter(),
318            numbering.clone(),
319            refable.supplement(),
320            elem,
321        )
322    }
323}
324
325/// Show a reference.
326fn realize_reference(
327    reference: &Packed<RefElem>,
328    engine: &mut Engine,
329    styles: StyleChain,
330    counter: Counter,
331    numbering: Numbering,
332    supplement: Content,
333    elem: Content,
334) -> SourceResult<Content> {
335    let loc = elem.location().unwrap();
336    let numbers = counter.display_at_loc(engine, loc, styles, &numbering.trimmed())?;
337
338    let supplement = match reference.supplement.get_ref(styles) {
339        Smart::Auto => supplement,
340        Smart::Custom(None) => Content::empty(),
341        Smart::Custom(Some(supplement)) => supplement.resolve(engine, styles, [elem])?,
342    };
343
344    let alt = {
345        let supplement = supplement.plain_text();
346        let numbering = numbers.plain_text();
347        eco_format!("{supplement} {numbering}",)
348    };
349
350    let mut content = numbers;
351    if !supplement.is_empty() {
352        content = supplement + TextElem::packed("\u{a0}") + content;
353    }
354
355    content = content.spanned(reference.span());
356
357    Ok(DirectLinkElem::new(loc, content, Some(alt)).pack())
358}
359
360/// Turn a reference into a citation.
361fn to_citation(
362    reference: &Packed<RefElem>,
363    engine: &mut Engine,
364    styles: StyleChain,
365) -> SourceResult<Packed<CiteElem>> {
366    let mut elem = Packed::new(CiteElem::new(reference.target).with_supplement(
367        match reference.supplement.get_cloned(styles) {
368            Smart::Custom(Some(Supplement::Content(content))) => Some(content),
369            _ => None,
370        },
371    ));
372
373    elem.synthesize(engine, styles)?;
374
375    Ok(elem)
376}
377
378/// Additional content for a reference.
379#[derive(Debug, Clone, PartialEq, Hash)]
380pub enum Supplement {
381    Content(Content),
382    Func(Func),
383}
384
385impl Supplement {
386    /// Tries to resolve the supplement into its content.
387    pub fn resolve<T: IntoValue>(
388        &self,
389        engine: &mut Engine,
390        styles: StyleChain,
391        args: impl IntoIterator<Item = T>,
392    ) -> SourceResult<Content> {
393        Ok(match self {
394            Supplement::Content(content) => content.clone(),
395            Supplement::Func(func) => func
396                .call(engine, Context::new(None, Some(styles)).track(), args)?
397                .display(),
398        })
399    }
400}
401
402cast! {
403    Supplement,
404    self => match self {
405        Self::Content(v) => v.into_value(),
406        Self::Func(v) => v.into_value(),
407    },
408    v: Content => Self::Content(v),
409    v: Func => Self::Func(v),
410}
411
412/// The form of the reference.
413#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
414pub enum RefForm {
415    /// Produces a textual reference to a label.
416    #[default]
417    Normal,
418    /// Produces a page reference to a label.
419    Page,
420}
421
422/// Marks an element as being able to be referenced. This is used to implement
423/// the `@ref` element.
424pub trait Refable {
425    /// The supplement, if not overridden by the reference.
426    fn supplement(&self) -> Content;
427
428    /// Returns the counter of this element.
429    fn counter(&self) -> Counter;
430
431    /// Returns the numbering of this element.
432    fn numbering(&self) -> Option<&Numbering>;
433}