typst_library/model/
footnote.rs

1use std::num::NonZeroUsize;
2use std::str::FromStr;
3
4use typst_utils::NonZeroExt;
5
6use crate::diag::{bail, At, SourceResult, StrResult};
7use crate::engine::Engine;
8use crate::foundations::{
9    cast, elem, scope, Content, Label, NativeElement, Packed, Show, ShowSet, Smart,
10    StyleChain, Styles,
11};
12use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location};
13use crate::layout::{Abs, Em, HElem, Length, Ratio};
14use crate::model::{Destination, Numbering, NumberingPattern, ParElem};
15use crate::text::{SuperElem, TextElem, TextSize};
16use crate::visualize::{LineElem, Stroke};
17
18/// A footnote.
19///
20/// Includes additional remarks and references on the same page with footnotes.
21/// A footnote will insert a superscript number that links to the note at the
22/// bottom of the page. Notes are numbered sequentially throughout your document
23/// and can break across multiple pages.
24///
25/// To customize the appearance of the entry in the footnote listing, see
26/// [`footnote.entry`]($footnote.entry). The footnote itself is realized as a
27/// normal superscript, so you can use a set rule on the [`super`] function to
28/// customize it. You can also apply a show rule to customize only the footnote
29/// marker (superscript number) in the running text.
30///
31/// # Example
32/// ```example
33/// Check the docs for more details.
34/// #footnote[https://typst.app/docs]
35/// ```
36///
37/// The footnote automatically attaches itself to the preceding word, even if
38/// there is a space before it in the markup. To force space, you can use the
39/// string `[#" "]` or explicit [horizontal spacing]($h).
40///
41/// By giving a label to a footnote, you can have multiple references to it.
42///
43/// ```example
44/// You can edit Typst documents online.
45/// #footnote[https://typst.app/app] <fn>
46/// Checkout Typst's website. @fn
47/// And the online app. #footnote(<fn>)
48/// ```
49///
50/// _Note:_ Set and show rules in the scope where `footnote` is called may not
51/// apply to the footnote's content. See [here][issue] for more information.
52///
53/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440
54#[elem(scope, Locatable, Show, Count)]
55pub struct FootnoteElem {
56    /// How to number footnotes.
57    ///
58    /// By default, the footnote numbering continues throughout your document.
59    /// If you prefer per-page footnote numbering, you can reset the footnote
60    /// [counter] in the page [header]($page.header). In the future, there might
61    /// be a simpler way to achieve this.
62    ///
63    /// ```example
64    /// #set footnote(numbering: "*")
65    ///
66    /// Footnotes:
67    /// #footnote[Star],
68    /// #footnote[Dagger]
69    /// ```
70    #[borrowed]
71    #[default(Numbering::Pattern(NumberingPattern::from_str("1").unwrap()))]
72    pub numbering: Numbering,
73
74    /// The content to put into the footnote. Can also be the label of another
75    /// footnote this one should point to.
76    #[required]
77    pub body: FootnoteBody,
78}
79
80#[scope]
81impl FootnoteElem {
82    #[elem]
83    type FootnoteEntry;
84}
85
86impl FootnoteElem {
87    /// Creates a new footnote that the passed content as its body.
88    pub fn with_content(content: Content) -> Self {
89        Self::new(FootnoteBody::Content(content))
90    }
91
92    /// Creates a new footnote referencing the footnote with the specified label.
93    pub fn with_label(label: Label) -> Self {
94        Self::new(FootnoteBody::Reference(label))
95    }
96
97    /// Creates a new footnote referencing the footnote with the specified label,
98    /// with the other fields from the current footnote cloned.
99    pub fn into_ref(&self, label: Label) -> Self {
100        Self {
101            body: FootnoteBody::Reference(label),
102            ..self.clone()
103        }
104    }
105
106    /// Tests if this footnote is a reference to another footnote.
107    pub fn is_ref(&self) -> bool {
108        matches!(self.body, FootnoteBody::Reference(_))
109    }
110
111    /// Returns the content of the body of this footnote if it is not a ref.
112    pub fn body_content(&self) -> Option<&Content> {
113        match &self.body {
114            FootnoteBody::Content(content) => Some(content),
115            _ => None,
116        }
117    }
118}
119
120impl Packed<FootnoteElem> {
121    /// Returns the location of the definition of this footnote.
122    pub fn declaration_location(&self, engine: &Engine) -> StrResult<Location> {
123        match self.body {
124            FootnoteBody::Reference(label) => {
125                let element = engine.introspector.query_label(label)?;
126                let footnote = element
127                    .to_packed::<FootnoteElem>()
128                    .ok_or("referenced element should be a footnote")?;
129                if self.location() == footnote.location() {
130                    bail!("footnote cannot reference itself");
131                }
132                footnote.declaration_location(engine)
133            }
134            _ => Ok(self.location().unwrap()),
135        }
136    }
137}
138
139impl Show for Packed<FootnoteElem> {
140    #[typst_macros::time(name = "footnote", span = self.span())]
141    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
142        let span = self.span();
143        let loc = self.declaration_location(engine).at(span)?;
144        let numbering = self.numbering(styles);
145        let counter = Counter::of(FootnoteElem::elem());
146        let num = counter.display_at_loc(engine, loc, styles, numbering)?;
147        let sup = SuperElem::new(num).pack().spanned(span);
148        let loc = loc.variant(1);
149        // Add zero-width weak spacing to make the footnote "sticky".
150        Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc)))
151    }
152}
153
154impl Count for Packed<FootnoteElem> {
155    fn update(&self) -> Option<CounterUpdate> {
156        (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE))
157    }
158}
159
160/// The body of a footnote can be either some content or a label referencing
161/// another footnote.
162#[derive(Debug, Clone, PartialEq, Hash)]
163pub enum FootnoteBody {
164    Content(Content),
165    Reference(Label),
166}
167
168cast! {
169    FootnoteBody,
170    self => match self {
171        Self::Content(v) => v.into_value(),
172        Self::Reference(v) => v.into_value(),
173    },
174    v: Content => Self::Content(v),
175    v: Label => Self::Reference(v),
176}
177
178/// An entry in a footnote list.
179///
180/// This function is not intended to be called directly. Instead, it is used in
181/// set and show rules to customize footnote listings.
182///
183/// ```example
184/// #show footnote.entry: set text(red)
185///
186/// My footnote listing
187/// #footnote[It's down here]
188/// has red text!
189/// ```
190///
191/// _Note:_ Footnote entry properties must be uniform across each page run (a
192/// page run is a sequence of pages without an explicit pagebreak in between).
193/// For this reason, set and show rules for footnote entries should be defined
194/// before any page content, typically at the very start of the document.
195#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)]
196pub struct FootnoteEntry {
197    /// The footnote for this entry. Its location can be used to determine
198    /// the footnote counter state.
199    ///
200    /// ```example
201    /// #show footnote.entry: it => {
202    ///   let loc = it.note.location()
203    ///   numbering(
204    ///     "1: ",
205    ///     ..counter(footnote).at(loc),
206    ///   )
207    ///   it.note.body
208    /// }
209    ///
210    /// Customized #footnote[Hello]
211    /// listing #footnote[World! 🌏]
212    /// ```
213    #[required]
214    pub note: Packed<FootnoteElem>,
215
216    /// The separator between the document body and the footnote listing.
217    ///
218    /// ```example
219    /// #set footnote.entry(
220    ///   separator: repeat[.]
221    /// )
222    ///
223    /// Testing a different separator.
224    /// #footnote[
225    ///   Unconventional, but maybe
226    ///   not that bad?
227    /// ]
228    /// ```
229    #[default(
230        LineElem::new()
231            .with_length(Ratio::new(0.3).into())
232            .with_stroke(Stroke {
233                thickness: Smart::Custom(Abs::pt(0.5).into()),
234                ..Default::default()
235            })
236            .pack()
237    )]
238    pub separator: Content,
239
240    /// The amount of clearance between the document body and the separator.
241    ///
242    /// ```example
243    /// #set footnote.entry(clearance: 3em)
244    ///
245    /// Footnotes also need ...
246    /// #footnote[
247    ///   ... some space to breathe.
248    /// ]
249    /// ```
250    #[default(Em::new(1.0).into())]
251    #[resolve]
252    pub clearance: Length,
253
254    /// The gap between footnote entries.
255    ///
256    /// ```example
257    /// #set footnote.entry(gap: 0.8em)
258    ///
259    /// Footnotes:
260    /// #footnote[Spaced],
261    /// #footnote[Apart]
262    /// ```
263    #[default(Em::new(0.5).into())]
264    #[resolve]
265    pub gap: Length,
266
267    /// The indent of each footnote entry.
268    ///
269    /// ```example
270    /// #set footnote.entry(indent: 0em)
271    ///
272    /// Footnotes:
273    /// #footnote[No],
274    /// #footnote[Indent]
275    /// ```
276    #[default(Em::new(1.0).into())]
277    pub indent: Length,
278}
279
280impl Show for Packed<FootnoteEntry> {
281    #[typst_macros::time(name = "footnote.entry", span = self.span())]
282    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
283        let span = self.span();
284        let number_gap = Em::new(0.05);
285        let default = StyleChain::default();
286        let numbering = self.note.numbering(default);
287        let counter = Counter::of(FootnoteElem::elem());
288        let Some(loc) = self.note.location() else {
289            bail!(
290                span, "footnote entry must have a location";
291                hint: "try using a query or a show rule to customize the footnote instead"
292            );
293        };
294
295        let num = counter.display_at_loc(engine, loc, styles, numbering)?;
296        let sup = SuperElem::new(num)
297            .pack()
298            .spanned(span)
299            .linked(Destination::Location(loc))
300            .located(loc.variant(1));
301
302        Ok(Content::sequence([
303            HElem::new(self.indent(styles).into()).pack(),
304            sup,
305            HElem::new(number_gap.into()).with_weak(true).pack(),
306            self.note.body_content().unwrap().clone(),
307        ]))
308    }
309}
310
311impl ShowSet for Packed<FootnoteEntry> {
312    fn show_set(&self, _: StyleChain) -> Styles {
313        let mut out = Styles::new();
314        out.set(ParElem::set_leading(Em::new(0.5).into()));
315        out.set(TextElem::set_size(TextSize(Em::new(0.85).into())));
316        out
317    }
318}
319
320cast! {
321    FootnoteElem,
322    v: Content => v.unpack::<Self>().unwrap_or_else(Self::with_content)
323}