Skip to main content

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