diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs index c173ef1a1..accd092a6 100644 --- a/library/src/layout/flow.rs +++ b/library/src/layout/flow.rs @@ -488,6 +488,11 @@ impl FlowLayouter<'_> { // Process footnotes one at a time. let mut k = 0; while k < notes.len() { + if notes[k].is_ref() { + k += 1; + continue; + } + if !self.has_footnotes { self.layout_footnote_separator(vt)?; } diff --git a/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs index 9bc88f280..0abcf5bb0 100644 --- a/library/src/meta/bibliography.rs +++ b/library/src/meta/bibliography.rs @@ -545,7 +545,7 @@ fn create( } if style == CitationStyle::ChicagoNotes { - content = FootnoteElem::new(content).pack(); + content = FootnoteElem::with_content(content).pack(); } (location, Some(content)) diff --git a/library/src/meta/footnote.rs b/library/src/meta/footnote.rs index 950057baf..31ec9fe91 100644 --- a/library/src/meta/footnote.rs +++ b/library/src/meta/footnote.rs @@ -1,15 +1,35 @@ +use comemo::Prehashed; use std::str::FromStr; use super::{Counter, Numbering, NumberingPattern}; use crate::layout::{HElem, ParElem}; +use crate::meta::{Count, CounterUpdate}; use crate::prelude::*; use crate::text::{SuperElem, TextElem, TextSize}; use crate::visualize::LineElem; +/// The body of a footnote can be either some content or a label referencing +/// another footnote. +#[derive(Debug)] +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), +} + /// A footnote. /// -/// Include additional remarks and references on the same page with footnotes. A -/// footnote will insert a superscript number that links to the note at the +/// 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. /// @@ -28,6 +48,15 @@ use crate::visualize::LineElem; /// there is a space before it in the markup. To force space, you can use the /// string `[#" "]` or explicit [horizontal spacing]($func/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] +/// Checkout Typst's website. @fn +/// And the online app. #footnote() +/// ``` +/// /// _Note:_ Set and show rules in the scope where `footnote` is called may not /// apply to the footnote's content. See [here][issue] more information. /// @@ -35,7 +64,7 @@ use crate::visualize::LineElem; /// /// Display: Footnote /// Category: meta -#[element(Locatable, Synthesize, Show)] +#[element(Locatable, Synthesize, Show, Count)] #[scope( scope.define("entry", FootnoteEntry::func()); scope @@ -58,9 +87,49 @@ pub struct FootnoteElem { #[default(Numbering::Pattern(NumberingPattern::from_str("1").unwrap()))] pub numbering: Numbering, - /// The content to put into the footnote. + /// The content to put into the footnote. Can also be the label of another + /// footnote this one should point to. #[required] - pub body: Content, + pub body: FootnoteBody, +} + +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)) + } + + /// 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 { + match self.body() { + FootnoteBody::Content(content) => Some(content), + _ => None, + } + } + + /// Returns the location of the definition of this footnote. + pub fn declaration_location(&self, vt: &Vt) -> StrResult { + match self.body() { + FootnoteBody::Reference(label) => { + let element: Prehashed = vt.introspector.query_label(&label)?; + let footnote = element + .to::() + .ok_or("referenced element should be a footnote")?; + footnote.declaration_location(vt) + } + _ => Ok(self.0.location().unwrap()), + } + } } impl Synthesize for FootnoteElem { @@ -73,14 +142,22 @@ impl Synthesize for FootnoteElem { impl Show for FootnoteElem { #[tracing::instrument(name = "FootnoteElem::show", skip_all)] fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { - let loc = self.0.location().unwrap(); - let numbering = self.numbering(styles); - let counter = Counter::of(Self::func()); - let num = counter.at(vt, loc)?.display(vt, &numbering)?; - let sup = SuperElem::new(num).pack(); - let hole = HElem::new(Abs::zero().into()).with_weak(true).pack(); - let loc = self.0.location().unwrap().variant(1); - Ok(hole + sup.linked(Destination::Location(loc))) + Ok(vt.delayed(|vt| { + let loc = self.declaration_location(vt).at(self.span())?; + let numbering = self.numbering(styles); + let counter = Counter::of(Self::func()); + let num = counter.at(vt, loc)?.display(vt, &numbering)?; + let sup = SuperElem::new(num).pack(); + let hole = HElem::new(Abs::zero().into()).with_weak(true).pack(); + let loc = loc.variant(1); + Ok(hole + sup.linked(Destination::Location(loc))) + })) + } +} + +impl Count for FootnoteElem { + fn update(&self) -> Option { + (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE)) } } @@ -201,7 +278,7 @@ impl Show for FootnoteEntry { HElem::new(self.indent(styles).into()).pack(), sup, HElem::new(number_gap.into()).with_weak(true).pack(), - note.body(), + note.body_content().unwrap(), ])) } } @@ -218,5 +295,5 @@ impl Finalize for FootnoteEntry { cast! { FootnoteElem, - v: Content => v.to::().cloned().unwrap_or_else(|| Self::new(v.clone())), + v: Content => v.to::().cloned().unwrap_or_else(|| Self::with_content(v.clone())), } diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index 26961ec56..908238711 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -1,4 +1,5 @@ use super::{BibliographyElem, CiteElem, Counter, Figurable, Numbering}; +use crate::meta::FootnoteElem; use crate::prelude::*; use crate::text::TextElem; @@ -11,11 +12,11 @@ use crate::text::TextElem; /// bibliography. /// /// Referenceable elements include [headings]($func/heading), -/// [figures]($func/figure), and [equations]($func/math.equation). To create a -/// custom referenceable element like a theorem, you can create a figure of a -/// custom [`kind`]($func/figure.kind) and write a show rule for it. In the -/// future, there might be a more direct way to define a custom referenceable -/// element. +/// [figures]($func/figure), [equations]($func/math.equation), and +/// [footnotes]($func/footnote). To create a custom referenceable element like a +/// theorem, you can create a figure of a custom [`kind`]($func/figure.kind) and +/// write a show rule for it. In the future, there might be a more direct way to +/// define a custom referenceable element. /// /// If you just want to link to a labelled element and not get an automatic /// textual reference, consider using the [`link`]($func/link) function instead. @@ -160,6 +161,11 @@ impl Show for RefElem { } let elem = elem.at(span)?; + + if elem.func() == FootnoteElem::func() { + return Ok(FootnoteElem::with_label(target).pack().spanned(span)); + } + let refable = elem .with::() .ok_or_else(|| { diff --git a/tests/ref/meta/footnote-refs.png b/tests/ref/meta/footnote-refs.png new file mode 100644 index 000000000..3fab7bd5d Binary files /dev/null and b/tests/ref/meta/footnote-refs.png differ diff --git a/tests/typ/meta/footnote-refs.typ b/tests/typ/meta/footnote-refs.typ new file mode 100644 index 000000000..0caee7bce --- /dev/null +++ b/tests/typ/meta/footnote-refs.typ @@ -0,0 +1,40 @@ +// Test references to footnotes. + +--- +A footnote #footnote[Hi] \ +A reference to it @fn + +--- +// Multiple footnotes are refs +First #footnote[A] \ +Second #footnote[B] \ +First ref @fn1 \ +Third #footnote[C] \ +Fourth #footnote[D] \ +Fourth ref @fn4 \ +Second ref @fn2 \ +Second ref again @fn2 + +--- +// Forward reference +Usage @fn \ +Definition #footnote[Hi] + +--- +// Footnote ref in footnote +#footnote[Reference to next @fn] +#footnote[Reference to myself @fn] +#footnote[Reference to previous @fn] + +--- +// Styling +#show footnote: text.with(fill: red) +Real #footnote[...] \ +Ref @fn + +--- +// Footnote call with label +#footnote() +#footnote[Hi] +#ref() +#footnote()