typst_library/model/
footnote.rs

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