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