summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/model/footnote.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-10-27 19:04:55 +0100
committerGitHub <noreply@github.com>2024-10-27 18:04:55 +0000
commitbe7cfc85d08c545abfac08098b7b33b4bd71f37e (patch)
treef4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/model/footnote.rs
parentb8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff)
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/model/footnote.rs')
-rw-r--r--crates/typst-library/src/model/footnote.rs326
1 files changed, 326 insertions, 0 deletions
diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs
new file mode 100644
index 00000000..d9971dd1
--- /dev/null
+++ b/crates/typst-library/src/model/footnote.rs
@@ -0,0 +1,326 @@
+use std::num::NonZeroUsize;
+use std::str::FromStr;
+
+use typst_utils::NonZeroExt;
+
+use crate::diag::{bail, At, SourceResult, StrResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, scope, Content, Label, NativeElement, Packed, Show, ShowSet, Smart,
+ StyleChain, Styles,
+};
+use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location};
+use crate::layout::{Abs, Em, HElem, Length, Ratio};
+use crate::model::{Destination, Numbering, NumberingPattern, ParElem};
+use crate::text::{SuperElem, TextElem, TextSize};
+use crate::visualize::{LineElem, Stroke};
+
+/// A footnote.
+///
+/// Includes additional remarks and references on the same page with footnotes.
+/// A footnote will insert a superscript number that links to the note at the
+/// bottom of the page. Notes are numbered sequentially throughout your document
+/// and can break across multiple pages.
+///
+/// To customize the appearance of the entry in the footnote listing, see
+/// [`footnote.entry`]($footnote.entry). The footnote itself is realized as a
+/// normal superscript, so you can use a set rule on the [`super`] function to
+/// customize it. You can also apply a show rule to customize only the footnote
+/// marker (superscript number) in the running text.
+///
+/// # Example
+/// ```example
+/// Check the docs for more details.
+/// #footnote[https://typst.app/docs]
+/// ```
+///
+/// The footnote automatically attaches itself to the preceding word, even if
+/// there is a space before it in the markup. To force space, you can use the
+/// string `[#" "]` or explicit [horizontal spacing]($h).
+///
+/// By giving a label to a footnote, you can have multiple references to it.
+///
+/// ```example
+/// You can edit Typst documents online.
+/// #footnote[https://typst.app/app] <fn>
+/// Checkout Typst's website. @fn
+/// And the online app. #footnote(<fn>)
+/// ```
+///
+/// _Note:_ Set and show rules in the scope where `footnote` is called may not
+/// apply to the footnote's content. See [here][issue] for more information.
+///
+/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440
+#[elem(scope, Locatable, Show, Count)]
+pub struct FootnoteElem {
+ /// How to number footnotes.
+ ///
+ /// By default, the footnote numbering continues throughout your document.
+ /// If you prefer per-page footnote numbering, you can reset the footnote
+ /// [counter] in the page [header]($page.header). In the future, there might
+ /// be a simpler way to achieve this.
+ ///
+ /// ```example
+ /// #set footnote(numbering: "*")
+ ///
+ /// Footnotes:
+ /// #footnote[Star],
+ /// #footnote[Dagger]
+ /// ```
+ #[borrowed]
+ #[default(Numbering::Pattern(NumberingPattern::from_str("1").unwrap()))]
+ pub numbering: Numbering,
+
+ /// The content to put into the footnote. Can also be the label of another
+ /// footnote this one should point to.
+ #[required]
+ pub body: FootnoteBody,
+}
+
+#[scope]
+impl FootnoteElem {
+ #[elem]
+ type FootnoteEntry;
+}
+
+impl FootnoteElem {
+ /// Creates a new footnote that the passed content as its body.
+ pub fn with_content(content: Content) -> Self {
+ Self::new(FootnoteBody::Content(content))
+ }
+
+ /// Creates a new footnote referencing the footnote with the specified label.
+ pub fn with_label(label: Label) -> Self {
+ Self::new(FootnoteBody::Reference(label))
+ }
+
+ /// Creates a new footnote referencing the footnote with the specified label,
+ /// with the other fields from the current footnote cloned.
+ pub fn into_ref(&self, label: Label) -> Self {
+ Self {
+ body: FootnoteBody::Reference(label),
+ ..self.clone()
+ }
+ }
+
+ /// Tests if this footnote is a reference to another footnote.
+ pub fn is_ref(&self) -> bool {
+ matches!(self.body(), FootnoteBody::Reference(_))
+ }
+
+ /// Returns the content of the body of this footnote if it is not a ref.
+ pub fn body_content(&self) -> Option<&Content> {
+ match self.body() {
+ FootnoteBody::Content(content) => Some(content),
+ _ => None,
+ }
+ }
+}
+
+impl Packed<FootnoteElem> {
+ /// Returns the location of the definition of this footnote.
+ pub fn declaration_location(&self, engine: &Engine) -> StrResult<Location> {
+ match self.body() {
+ FootnoteBody::Reference(label) => {
+ let element = engine.introspector.query_label(*label)?;
+ let footnote = element
+ .to_packed::<FootnoteElem>()
+ .ok_or("referenced element should be a footnote")?;
+ if self.location() == footnote.location() {
+ bail!("footnote cannot reference itself");
+ }
+ footnote.declaration_location(engine)
+ }
+ _ => Ok(self.location().unwrap()),
+ }
+ }
+}
+
+impl Show for Packed<FootnoteElem> {
+ #[typst_macros::time(name = "footnote", span = self.span())]
+ fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ let span = self.span();
+ let loc = self.declaration_location(engine).at(span)?;
+ let numbering = self.numbering(styles);
+ let counter = Counter::of(FootnoteElem::elem());
+ let num = counter.display_at_loc(engine, loc, styles, numbering)?;
+ let sup = SuperElem::new(num).pack().spanned(span);
+ let loc = loc.variant(1);
+ // Add zero-width weak spacing to make the footnote "sticky".
+ Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc)))
+ }
+}
+
+impl Count for Packed<FootnoteElem> {
+ fn update(&self) -> Option<CounterUpdate> {
+ (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE))
+ }
+}
+
+/// The body of a footnote can be either some content or a label referencing
+/// another footnote.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum FootnoteBody {
+ Content(Content),
+ Reference(Label),
+}
+
+cast! {
+ FootnoteBody,
+ self => match self {
+ Self::Content(v) => v.into_value(),
+ Self::Reference(v) => v.into_value(),
+ },
+ v: Content => Self::Content(v),
+ v: Label => Self::Reference(v),
+}
+
+/// An entry in a footnote list.
+///
+/// This function is not intended to be called directly. Instead, it is used in
+/// set and show rules to customize footnote listings.
+///
+/// ```example
+/// #show footnote.entry: set text(red)
+///
+/// My footnote listing
+/// #footnote[It's down here]
+/// has red text!
+/// ```
+///
+/// _Note:_ Footnote entry properties must be uniform across each page run (a
+/// page run is a sequence of pages without an explicit pagebreak in between).
+/// For this reason, set and show rules for footnote entries should be defined
+/// before any page content, typically at the very start of the document.
+#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)]
+pub struct FootnoteEntry {
+ /// The footnote for this entry. It's location can be used to determine
+ /// the footnote counter state.
+ ///
+ /// ```example
+ /// #show footnote.entry: it => {
+ /// let loc = it.note.location()
+ /// numbering(
+ /// "1: ",
+ /// ..counter(footnote).at(loc),
+ /// )
+ /// it.note.body
+ /// }
+ ///
+ /// Customized #footnote[Hello]
+ /// listing #footnote[World! 🌏]
+ /// ```
+ #[required]
+ pub note: Packed<FootnoteElem>,
+
+ /// The separator between the document body and the footnote listing.
+ ///
+ /// ```example
+ /// #set footnote.entry(
+ /// separator: repeat[.]
+ /// )
+ ///
+ /// Testing a different separator.
+ /// #footnote[
+ /// Unconventional, but maybe
+ /// not that bad?
+ /// ]
+ /// ```
+ #[default(
+ LineElem::new()
+ .with_length(Ratio::new(0.3).into())
+ .with_stroke(Stroke {
+ thickness: Smart::Custom(Abs::pt(0.5).into()),
+ ..Default::default()
+ })
+ .pack()
+ )]
+ pub separator: Content,
+
+ /// The amount of clearance between the document body and the separator.
+ ///
+ /// ```example
+ /// #set footnote.entry(clearance: 3em)
+ ///
+ /// Footnotes also need ...
+ /// #footnote[
+ /// ... some space to breathe.
+ /// ]
+ /// ```
+ #[default(Em::new(1.0).into())]
+ #[resolve]
+ pub clearance: Length,
+
+ /// The gap between footnote entries.
+ ///
+ /// ```example
+ /// #set footnote.entry(gap: 0.8em)
+ ///
+ /// Footnotes:
+ /// #footnote[Spaced],
+ /// #footnote[Apart]
+ /// ```
+ #[default(Em::new(0.5).into())]
+ #[resolve]
+ pub gap: Length,
+
+ /// The indent of each footnote entry.
+ ///
+ /// ```example
+ /// #set footnote.entry(indent: 0em)
+ ///
+ /// Footnotes:
+ /// #footnote[No],
+ /// #footnote[Indent]
+ /// ```
+ #[default(Em::new(1.0).into())]
+ pub indent: Length,
+}
+
+impl Show for Packed<FootnoteEntry> {
+ #[typst_macros::time(name = "footnote.entry", span = self.span())]
+ fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ let span = self.span();
+ let note = self.note();
+ let number_gap = Em::new(0.05);
+ let default = StyleChain::default();
+ let numbering = note.numbering(default);
+ let counter = Counter::of(FootnoteElem::elem());
+ let Some(loc) = note.location() else {
+ bail!(
+ span, "footnote entry must have a location";
+ hint: "try using a query or a show rule to customize the footnote instead"
+ );
+ };
+
+ let num = counter.display_at_loc(engine, loc, styles, numbering)?;
+ let sup = SuperElem::new(num)
+ .pack()
+ .spanned(span)
+ .linked(Destination::Location(loc))
+ .located(loc.variant(1));
+
+ Ok(Content::sequence([
+ HElem::new(self.indent(styles).into()).pack(),
+ sup,
+ HElem::new(number_gap.into()).with_weak(true).pack(),
+ note.body_content().unwrap().clone(),
+ ]))
+ }
+}
+
+impl ShowSet for Packed<FootnoteEntry> {
+ fn show_set(&self, _: StyleChain) -> Styles {
+ let text_size = Em::new(0.85);
+ let leading = Em::new(0.5);
+ let mut out = Styles::new();
+ out.set(ParElem::set_leading(leading.into()));
+ out.set(TextElem::set_size(TextSize(text_size.into())));
+ out
+ }
+}
+
+cast! {
+ FootnoteElem,
+ v: Content => v.unpack::<Self>().unwrap_or_else(Self::with_content)
+}