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