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 {}