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}