Skip to main content

typst_library/model/
footnote.rs

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