summaryrefslogtreecommitdiff
path: root/library/src
diff options
context:
space:
mode:
Diffstat (limited to 'library/src')
-rw-r--r--library/src/layout/columns.rs1
-rw-r--r--library/src/layout/flow.rs271
-rw-r--r--library/src/layout/page.rs6
-rw-r--r--library/src/layout/par.rs4
-rw-r--r--library/src/layout/regions.rs13
-rw-r--r--library/src/layout/spacing.rs8
-rw-r--r--library/src/meta/bibliography.rs29
-rw-r--r--library/src/meta/footnote.rs217
-rw-r--r--library/src/meta/mod.rs3
-rw-r--r--library/src/shared/ext.rs11
10 files changed, 512 insertions, 51 deletions
diff --git a/library/src/layout/columns.rs b/library/src/layout/columns.rs
index 06e6a7e0..984c9673 100644
--- a/library/src/layout/columns.rs
+++ b/library/src/layout/columns.rs
@@ -83,6 +83,7 @@ impl Layout for ColumnsElem {
backlog: &backlog,
last: regions.last,
expand: Axes::new(true, regions.expand.y),
+ root: regions.root,
};
// Layout the children.
diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs
index 64d1d509..a21f8142 100644
--- a/library/src/layout/flow.rs
+++ b/library/src/layout/flow.rs
@@ -1,4 +1,9 @@
-use super::{AlignElem, BlockElem, ColbreakElem, ParElem, PlaceElem, Spacing, VElem};
+use std::mem;
+
+use super::{
+ AlignElem, BlockElem, ColbreakElem, ColumnsElem, ParElem, PlaceElem, Spacing, VElem,
+};
+use crate::meta::{FootnoteElem, FootnoteEntry};
use crate::prelude::*;
use crate::visualize::{
CircleElem, EllipseElem, ImageElem, PathElem, PolygonElem, RectElem, SquareElem,
@@ -26,7 +31,7 @@ impl Layout for FlowElem {
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
- let mut layouter = FlowLayouter::new(regions);
+ let mut layouter = FlowLayouter::new(regions, styles);
for mut child in &self.children() {
let outer = styles;
@@ -37,7 +42,7 @@ impl Layout for FlowElem {
}
if let Some(elem) = child.to::<VElem>() {
- layouter.layout_spacing(elem, styles);
+ layouter.layout_spacing(vt, elem, styles)?;
} else if let Some(elem) = child.to::<ParElem>() {
layouter.layout_par(vt, elem, styles)?;
} else if child.is::<RectElem>()
@@ -63,21 +68,25 @@ impl Layout for FlowElem {
} else if child.is::<ColbreakElem>() {
if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some()
{
- layouter.finish_region();
+ layouter.finish_region()?;
}
} else {
bail!(child.span(), "unexpected flow child");
}
}
- Ok(layouter.finish())
+ layouter.finish()
}
}
/// Performs flow layout.
struct FlowLayouter<'a> {
+ /// Whether this is the root flow.
+ root: bool,
/// The regions to layout children into.
regions: Regions<'a>,
+ /// The shared styles.
+ styles: StyleChain<'a>,
/// Whether the flow should expand to fill the region.
expand: Axes<bool>,
/// The initial size of `regions.size` that was available before we started
@@ -85,12 +94,23 @@ struct FlowLayouter<'a> {
initial: Size,
/// Whether the last block was a paragraph.
last_was_par: bool,
- /// Spacing and layouted blocks.
+ /// Spacing and layouted blocks for the current region.
items: Vec<FlowItem>,
+ /// Whether we have any footnotes in the current region.
+ has_footnotes: bool,
+ /// Footnote configuration.
+ footnote_config: FootnoteConfig,
/// Finished frames for previous regions.
finished: Vec<Frame>,
}
+/// Cached footnote configuration.
+struct FootnoteConfig {
+ separator: Content,
+ clearance: Abs,
+ gap: Abs,
+}
+
/// A prepared item in a flow layout.
#[derive(Debug)]
enum FlowItem {
@@ -102,36 +122,55 @@ enum FlowItem {
Frame(Frame, Axes<Align>, bool),
/// An absolutely placed frame.
Placed(Frame),
+ /// A footnote frame (can also be the separator).
+ Footnote(Frame),
}
impl<'a> FlowLayouter<'a> {
/// Create a new flow layouter.
- fn new(mut regions: Regions<'a>) -> Self {
+ fn new(mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self {
let expand = regions.expand;
- // Disable vertical expansion for children.
+ // Disable vertical expansion & root for children.
regions.expand.y = false;
+ let root = mem::replace(&mut regions.root, false);
Self {
+ root,
regions,
+ styles,
expand,
initial: regions.size,
last_was_par: false,
items: vec![],
+ has_footnotes: false,
+ footnote_config: FootnoteConfig {
+ separator: FootnoteEntry::separator_in(styles),
+ clearance: FootnoteEntry::clearance_in(styles),
+ gap: FootnoteEntry::gap_in(styles),
+ },
finished: vec![],
}
}
/// Layout vertical spacing.
#[tracing::instrument(name = "FlowLayouter::layout_spacing", skip_all)]
- fn layout_spacing(&mut self, v: &VElem, styles: StyleChain) {
- self.layout_item(match v.amount() {
- Spacing::Rel(rel) => FlowItem::Absolute(
- rel.resolve(styles).relative_to(self.initial.y),
- v.weakness(styles) > 0,
- ),
- Spacing::Fr(fr) => FlowItem::Fractional(fr),
- });
+ fn layout_spacing(
+ &mut self,
+ vt: &mut Vt,
+ v: &VElem,
+ styles: StyleChain,
+ ) -> SourceResult<()> {
+ self.layout_item(
+ vt,
+ match v.amount() {
+ Spacing::Rel(rel) => FlowItem::Absolute(
+ rel.resolve(styles).relative_to(self.initial.y),
+ v.weakness(styles) > 0,
+ ),
+ Spacing::Fr(fr) => FlowItem::Fractional(fr),
+ },
+ )
}
/// Layout a paragraph.
@@ -145,7 +184,7 @@ impl<'a> FlowLayouter<'a> {
let aligns = AlignElem::alignment_in(styles).resolve(styles);
let leading = ParElem::leading_in(styles);
let consecutive = self.last_was_par;
- let frames = par
+ let lines = par
.layout(vt, styles, consecutive, self.regions.base(), self.regions.expand.x)?
.into_frames();
@@ -158,26 +197,25 @@ impl<'a> FlowLayouter<'a> {
}
}
- if let [first, ..] = frames.as_slice() {
+ if let Some(first) = lines.first() {
if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() {
let carry: Vec<_> = self.items.drain(sticky..).collect();
- self.finish_region();
+ self.finish_region()?;
for item in carry {
- self.layout_item(item);
+ self.layout_item(vt, item)?;
}
}
}
- for (i, frame) in frames.into_iter().enumerate() {
+ for (i, frame) in lines.into_iter().enumerate() {
if i > 0 {
- self.layout_item(FlowItem::Absolute(leading, true));
+ self.layout_item(vt, FlowItem::Absolute(leading, true))?;
}
- self.layout_item(FlowItem::Frame(frame, aligns, false));
+ self.layout_item(vt, FlowItem::Frame(frame, aligns, false))?;
}
self.last_was_par = true;
-
Ok(())
}
@@ -193,7 +231,7 @@ impl<'a> FlowLayouter<'a> {
let sticky = BlockElem::sticky_in(styles);
let pod = Regions::one(self.regions.base(), Axes::splat(false));
let frame = content.layout(vt, styles, pod)?.into_frame();
- self.layout_item(FlowItem::Frame(frame, aligns, sticky));
+ self.layout_item(vt, FlowItem::Frame(frame, aligns, sticky))?;
self.last_was_par = false;
Ok(())
}
@@ -210,7 +248,7 @@ impl<'a> FlowLayouter<'a> {
if let Some(placed) = block.to::<PlaceElem>() {
if placed.out_of_flow(styles) {
let frame = block.layout(vt, styles, self.regions)?.into_frame();
- self.layout_item(FlowItem::Placed(frame));
+ self.layout_item(vt, FlowItem::Placed(frame))?;
return Ok(());
}
}
@@ -225,16 +263,22 @@ impl<'a> FlowLayouter<'a> {
}
.resolve(styles);
+ let is_columns = block.is::<ColumnsElem>();
+
// Layout the block itself.
let sticky = BlockElem::sticky_in(styles);
let fragment = block.layout(vt, styles, self.regions)?;
+ self.regions.root = self.root && is_columns;
+
for (i, frame) in fragment.into_iter().enumerate() {
if i > 0 {
- self.finish_region();
+ self.finish_region()?;
}
- self.layout_item(FlowItem::Frame(frame, aligns, sticky));
+
+ self.layout_item(vt, FlowItem::Frame(frame, aligns, sticky))?;
}
+ self.regions.root = false;
self.last_was_par = false;
Ok(())
@@ -242,26 +286,38 @@ impl<'a> FlowLayouter<'a> {
/// Layout a finished frame.
#[tracing::instrument(name = "FlowLayouter::layout_item", skip_all)]
- fn layout_item(&mut self, item: FlowItem) {
+ fn layout_item(&mut self, vt: &mut Vt, item: FlowItem) -> SourceResult<()> {
match item {
- FlowItem::Absolute(v, _) => self.regions.size.y -= v,
+ FlowItem::Absolute(v, weak) => {
+ if weak
+ && !self.items.iter().any(|item| matches!(item, FlowItem::Frame(..)))
+ {
+ return Ok(());
+ }
+ self.regions.size.y -= v
+ }
FlowItem::Fractional(_) => {}
FlowItem::Frame(ref frame, ..) => {
let size = frame.size();
if !self.regions.size.y.fits(size.y) && !self.regions.in_last() {
- self.finish_region();
+ self.finish_region()?;
}
self.regions.size.y -= size.y;
+ if self.root {
+ return self.handle_footnotes(vt, item, size.y);
+ }
}
FlowItem::Placed(_) => {}
+ FlowItem::Footnote(_) => {}
}
self.items.push(item);
+ Ok(())
}
/// Finish the frame for one region.
- fn finish_region(&mut self) {
+ fn finish_region(&mut self) -> SourceResult<()> {
// Trim weak spacing.
while self
.items
@@ -274,6 +330,8 @@ impl<'a> FlowLayouter<'a> {
// Determine the used size.
let mut fr = Fr::zero();
let mut used = Size::zero();
+ let mut footnote_height = Abs::zero();
+ let mut first_footnote = true;
for item in &self.items {
match item {
FlowItem::Absolute(v, _) => used.y += *v,
@@ -284,19 +342,31 @@ impl<'a> FlowLayouter<'a> {
used.x.set_max(size.x);
}
FlowItem::Placed(_) => {}
+ FlowItem::Footnote(frame) => {
+ let size = frame.size();
+ footnote_height += size.y;
+ if !first_footnote {
+ footnote_height += self.footnote_config.gap;
+ }
+ first_footnote = false;
+ used.x.set_max(size.x);
+ }
}
}
+ used.y += footnote_height;
// Determine the size of the flow in this region depending on whether
- // the region expands. Also account for fractional spacing.
+ // the region expands. Also account for fractional spacing and
+ // footnotes.
let mut size = self.expand.select(self.initial, used).min(self.initial);
- if fr.get() > 0.0 && self.initial.y.is_finite() {
+ if (fr.get() > 0.0 || self.has_footnotes) && self.initial.y.is_finite() {
size.y = self.initial.y;
}
let mut output = Frame::new(size);
let mut offset = Abs::zero();
let mut ruler = Align::Top;
+ let mut footnote_offset = size.y - footnote_height;
// Place all frames.
for item in self.items.drain(..) {
@@ -316,6 +386,11 @@ impl<'a> FlowLayouter<'a> {
offset += frame.height();
output.push_frame(pos, frame);
}
+ FlowItem::Footnote(frame) => {
+ let pos = Point::with_y(footnote_offset);
+ footnote_offset += frame.height() + self.footnote_config.gap;
+ output.push_frame(pos, frame);
+ }
FlowItem::Placed(frame) => {
output.push_frame(Point::zero(), frame);
}
@@ -326,17 +401,137 @@ impl<'a> FlowLayouter<'a> {
self.finished.push(output);
self.regions.next();
self.initial = self.regions.size;
+ self.has_footnotes = false;
+ Ok(())
}
/// Finish layouting and return the resulting fragment.
- fn finish(mut self) -> Fragment {
+ fn finish(mut self) -> SourceResult<Fragment> {
if self.expand.y {
while !self.regions.backlog.is_empty() {
- self.finish_region();
+ self.finish_region()?;
}
}
- self.finish_region();
- Fragment::frames(self.finished)
+ self.finish_region()?;
+ Ok(Fragment::frames(self.finished))
+ }
+}
+
+impl FlowLayouter<'_> {
+ /// Processes all footnotes in the frame.
+ #[tracing::instrument(skip_all)]
+ fn handle_footnotes(
+ &mut self,
+ vt: &mut Vt,
+ item: FlowItem,
+ height: Abs,
+ ) -> SourceResult<()> {
+ // Find footnotes in the frame.
+ let mut notes = Vec::new();
+ if let FlowItem::Frame(frame, ..) = &item {
+ find_footnotes(&mut notes, frame);
+ }
+
+ self.items.push(item);
+
+ // No new footnotes.
+ if notes.is_empty() {
+ return Ok(());
+ }
+
+ // The currently handled footnote.
+ let mut k = 0;
+
+ // Whether we can still skip one region to ensure that the footnote
+ // and its entry are on the same page.
+ let mut can_skip = true;
+
+ // Process footnotes.
+ 'outer: while k < notes.len() {
+ let had_footnotes = self.has_footnotes;
+ if !self.has_footnotes {
+ self.layout_footnote_separator(vt)?;
+ }
+
+ self.regions.size.y -= self.footnote_config.gap;
+ let frames = FootnoteEntry::new(notes[k].clone())
+ .pack()
+ .layout(vt, self.styles, self.regions.with_root(false))?
+ .into_frames();
+
+ // If the entries didn't fit, undo the separator layout, move the
+ // item into the next region (to keep footnote and entry together)
+ // and try again.
+ if can_skip && frames.first().map_or(false, Frame::is_empty) {
+ // Remove separator
+ if !had_footnotes {
+ self.items.pop();
+ }
+ let item = self.items.pop();
+ self.finish_region()?;
+ self.items.extend(item);
+ self.regions.size.y -= height;
+ can_skip = false;
+ continue 'outer;
+ }
+
+ let prev = notes.len();
+ for (i, frame) in frames.into_iter().enumerate() {
+ find_footnotes(&mut notes, &frame);
+ if i > 0 {
+ self.finish_region()?;
+ self.layout_footnote_separator(vt)?;
+ self.regions.size.y -= self.footnote_config.gap;
+ }
+ self.regions.size.y -= frame.height();
+ self.items.push(FlowItem::Footnote(frame));
+ }
+
+ k += 1;
+
+ // Process the nested notes before dealing with further notes.
+ let nested = notes.len() - prev;
+ if nested > 0 {
+ notes[k..].rotate_right(nested);
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Layout and save the footnote separator, typically a line.
+ #[tracing::instrument(skip_all)]
+ fn layout_footnote_separator(&mut self, vt: &mut Vt) -> SourceResult<()> {
+ let expand = Axes::new(self.regions.expand.x, false);
+ let pod = Regions::one(self.regions.base(), expand);
+ let separator = &self.footnote_config.separator;
+
+ let mut frame = separator.layout(vt, self.styles, pod)?.into_frame();
+ frame.size_mut().y += self.footnote_config.clearance;
+ frame.translate(Point::with_y(self.footnote_config.clearance));
+
+ self.has_footnotes = true;
+ self.regions.size.y -= frame.height();
+ self.items.push(FlowItem::Footnote(frame));
+
+ Ok(())
+ }
+}
+
+/// Finds all footnotes in the frame.
+#[tracing::instrument(skip_all)]
+fn find_footnotes(notes: &mut Vec<FootnoteElem>, frame: &Frame) {
+ for (_, item) in frame.items() {
+ match item {
+ FrameItem::Group(group) => find_footnotes(notes, &group.frame),
+ FrameItem::Meta(Meta::Elem(content), _)
+ if !notes.iter().any(|note| note.0.location() == content.location()) =>
+ {
+ let Some(footnote) = content.to::<FootnoteElem>() else { continue };
+ notes.push(footnote.clone());
+ }
+ _ => {}
+ }
}
}
diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs
index cfddc446..6510bd58 100644
--- a/library/src/layout/page.rs
+++ b/library/src/layout/page.rs
@@ -315,9 +315,11 @@ impl PageElem {
child = ColumnsElem::new(child).with_count(columns).pack();
}
- // Layout the child.
let area = size - margin.sum_by_axis();
- let regions = Regions::repeat(area, area.map(Abs::is_finite));
+ let mut regions = Regions::repeat(area, area.map(Abs::is_finite));
+ regions.root = true;
+
+ // Layout the child.
let mut fragment = child.layout(vt, styles, regions)?;
let fill = self.fill(styles);
diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs
index f0dcbb11..e056b1c9 100644
--- a/library/src/layout/par.rs
+++ b/library/src/layout/par.rs
@@ -569,6 +569,10 @@ fn collect<'a>(
}
Segment::Text(full.len() - prev)
} else if let Some(elem) = child.to::<HElem>() {
+ if elem.amount().is_zero() {
+ continue;
+ }
+
full.push(SPACING_REPLACE);
Segment::Spacing(elem.amount())
} else if let Some(elem) = child.to::<LinebreakElem>() {
diff --git a/library/src/layout/regions.rs b/library/src/layout/regions.rs
index 5a4db178..6dd549b0 100644
--- a/library/src/layout/regions.rs
+++ b/library/src/layout/regions.rs
@@ -17,6 +17,11 @@ pub struct Regions<'a> {
/// Whether elements should expand to fill the regions instead of shrinking
/// to fit the content.
pub expand: Axes<bool>,
+ /// Whether these are the root regions or direct descendants.
+ ///
+ /// True for the padded page regions and columns directly in the page,
+ /// false otherwise.
+ pub root: bool,
}
impl Regions<'_> {
@@ -28,6 +33,7 @@ impl Regions<'_> {
backlog: &[],
last: None,
expand,
+ root: false,
}
}
@@ -39,6 +45,7 @@ impl Regions<'_> {
backlog: &[],
last: Some(size.y),
expand,
+ root: false,
}
}
@@ -67,6 +74,7 @@ impl Regions<'_> {
backlog,
last: self.last.map(|y| f(Size::new(x, y)).y),
expand: self.expand,
+ root: false,
}
}
@@ -82,6 +90,11 @@ impl Regions<'_> {
self.backlog.is_empty() && self.last.map_or(true, |height| self.size.y == height)
}
+ /// The same regions, but with different `root` configuration.
+ pub fn with_root(self, root: bool) -> Self {
+ Self { root, ..self }
+ }
+
/// Advance to the next region if there is any.
pub fn next(&mut self) {
if let Some(height) = self
diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs
index 42d2b443..588f9f29 100644
--- a/library/src/layout/spacing.rs
+++ b/library/src/layout/spacing.rs
@@ -177,6 +177,14 @@ impl Spacing {
pub fn is_fractional(self) -> bool {
matches!(self, Self::Fr(_))
}
+
+ /// Whether the spacing is actually no spacing.
+ pub fn is_zero(&self) -> bool {
+ match self {
+ Self::Rel(rel) => rel.is_zero(),
+ Self::Fr(fr) => fr.is_zero(),
+ }
+ }
}
impl From<Abs> for Spacing {
diff --git a/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs
index cdb6a5d6..2bf0e6f4 100644
--- a/library/src/meta/bibliography.rs
+++ b/library/src/meta/bibliography.rs
@@ -11,7 +11,7 @@ use typst::util::option_eq;
use super::{LinkElem, LocalName, RefElem};
use crate::layout::{BlockElem, GridElem, ParElem, Sizing, TrackSizings, VElem};
-use crate::meta::HeadingElem;
+use crate::meta::{FootnoteElem, HeadingElem};
use crate::prelude::*;
use crate::text::TextElem;
@@ -243,6 +243,9 @@ pub enum BibliographyStyle {
/// The Chicago Author Date style. Based on the 17th edition of the Chicago
/// Manual of Style, Chapter 15.
ChicagoAuthorDate,
+ /// The Chicago Notes style. Based on the 17th edition of the Chicago
+ /// Manual of Style, Chapter 14.
+ ChicagoNotes,
/// The style of the Institute of Electrical and Electronics Engineers.
/// Based on the 2018 IEEE Reference Guide.
Ieee,
@@ -257,6 +260,7 @@ impl BibliographyStyle {
match self {
Self::Apa => CitationStyle::ChicagoAuthorDate,
Self::ChicagoAuthorDate => CitationStyle::ChicagoAuthorDate,
+ Self::ChicagoNotes => CitationStyle::ChicagoNotes,
Self::Ieee => CitationStyle::Numerical,
Self::Mla => CitationStyle::ChicagoAuthorDate,
}
@@ -385,7 +389,10 @@ pub enum CitationStyle {
/// The Chicago Author Date style. Based on the 17th edition of the Chicago
/// Manual of Style, Chapter 15.
ChicagoAuthorDate,
- /// The Chicago-like author-title format. Results could look like this:
+ /// The Chicago Notes style. Based on the 17th edition of the Chicago
+ /// Manual of Style, Chapter 14.
+ ChicagoNotes,
+ /// A Chicago-like author-title format. Results could look like this:
/// Prokopov, “It Is Fast or It Is Wrong”.
ChicagoAuthorTitle,
}
@@ -487,6 +494,7 @@ fn create(
CitationStyle::ChicagoAuthorDate => {
Box::new(style::ChicagoAuthorDate::new())
}
+ CitationStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()),
CitationStyle::ChicagoAuthorTitle => {
Box::new(style::AuthorTitle::new())
}
@@ -537,6 +545,10 @@ fn create(
};
}
+ if style == CitationStyle::ChicagoNotes {
+ content = FootnoteElem::new(content).pack();
+ }
+
(location, Some(content))
})
.collect();
@@ -544,6 +556,7 @@ fn create(
let bibliography_style: Box<dyn style::BibliographyStyle> = match style {
BibliographyStyle::Apa => Box::new(style::Apa::new()),
BibliographyStyle::ChicagoAuthorDate => Box::new(style::ChicagoAuthorDate::new()),
+ BibliographyStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()),
BibliographyStyle::Ieee => Box::new(style::Ieee::new()),
BibliographyStyle::Mla => Box::new(style::Mla::new()),
};
@@ -552,24 +565,18 @@ fn create(
.bibliography(&*bibliography_style, None)
.into_iter()
.map(|reference| {
- // Make link from citation to here work.
- let backlink = {
- let mut content = Content::empty();
- content.set_location(ref_location(reference.entry));
- MetaElem::set_data(vec![Meta::Elem(content)])
- };
-
+ let backlink = ref_location(reference.entry);
let prefix = reference.prefix.map(|prefix| {
// Format and link to first citation.
let bracketed = prefix.with_default_brackets(&*citation_style);
format_display_string(&bracketed, None, span)
.linked(Destination::Location(ids[reference.entry.key()]))
- .styled(backlink.clone())
+ .backlinked(backlink)
});
let mut reference = format_display_string(&reference.display, None, span);
if prefix.is_none() {
- reference = reference.styled(backlink);
+ reference = reference.backlinked(backlink);
}
(prefix, reference)
diff --git a/library/src/meta/footnote.rs b/library/src/meta/footnote.rs
new file mode 100644
index 00000000..22de91c3
--- /dev/null
+++ b/library/src/meta/footnote.rs
@@ -0,0 +1,217 @@
+use std::str::FromStr;
+
+use super::{Counter, Numbering, NumberingPattern};
+use crate::layout::{HElem, ParElem};
+use crate::prelude::*;
+use crate::text::{SuperElem, TextElem, TextSize};
+use crate::visualize::LineElem;
+
+/// 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
+/// 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`]($func/footnote.entry). The footnote itself is realized as
+/// a normal superscript, so you can use a set rule on the
+/// [`super`]($func/super) function to customize it.
+///
+/// ## Example { #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]($func/h).
+///
+/// Display: Footnote
+/// Category: meta
+#[element(Locatable, Synthesize, Show)]
+#[scope(
+ scope.define("entry", FootnoteEntry::func());
+ scope
+)]
+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]($func/counter) in the page [header]($func/page.header). In the
+ /// future, there might be a simpler way to achieve this.
+ ///
+ /// ```example
+ /// #set footnote(numbering: "*")
+ ///
+ /// Footnotes:
+ /// #footnote[Star],
+ /// #footnote[Dagger]
+ /// ```
+ #[default(Numbering::Pattern(NumberingPattern::from_str("1").unwrap()))]
+ pub numbering: Numbering,
+
+ /// The content to put into the footnote.
+ #[required]
+ pub body: Content,
+}
+
+impl Synthesize for FootnoteElem {
+ fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ self.push_numbering(self.numbering(styles));
+ Ok(())
+ }
+}
+
+impl Show for FootnoteElem {
+ #[tracing::instrument(name = "FootnoteElem::show", skip_all)]
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ 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)))
+ }
+}
+
+/// 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 { #example }
+/// ```example
+/// #show footnote.entry: set text(red)
+///
+/// My footnote listing
+/// #footnote[It's down here]
+/// has red text!
+/// ```
+///
+/// Display: Footnote Entry
+/// Category: meta
+#[element(Show, Finalize)]
+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: 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(PartialStroke {
+ 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 FootnoteEntry {
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ let note = self.note();
+ let number_gap = Em::new(0.05);
+ let numbering = note.numbering(StyleChain::default());
+ let counter = Counter::of(FootnoteElem::func());
+ let loc = note.0.location().unwrap();
+ let num = counter.at(vt, loc)?.display(vt, &numbering)?;
+ let sup = SuperElem::new(num)
+ .pack()
+ .linked(Destination::Location(loc))
+ .backlinked(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(),
+ ]))
+ }
+}
+
+impl Finalize for FootnoteEntry {
+ fn finalize(&self, realized: Content, _: StyleChain) -> Content {
+ let text_size = Em::new(0.85);
+ let leading = Em::new(0.5);
+ realized
+ .styled(ParElem::set_leading(leading.into()))
+ .styled(TextElem::set_size(TextSize(text_size.into())))
+ }
+}
+
+cast_from_value! {
+ FootnoteElem,
+ v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())),
+}
diff --git a/library/src/meta/mod.rs b/library/src/meta/mod.rs
index 61028bf5..0cbbafff 100644
--- a/library/src/meta/mod.rs
+++ b/library/src/meta/mod.rs
@@ -5,6 +5,7 @@ mod context;
mod counter;
mod document;
mod figure;
+mod footnote;
mod heading;
mod link;
mod numbering;
@@ -18,6 +19,7 @@ pub use self::context::*;
pub use self::counter::*;
pub use self::document::*;
pub use self::figure::*;
+pub use self::footnote::*;
pub use self::heading::*;
pub use self::link::*;
pub use self::numbering::*;
@@ -36,6 +38,7 @@ pub(super) fn define(global: &mut Scope) {
global.define("outline", OutlineElem::func());
global.define("heading", HeadingElem::func());
global.define("figure", FigureElem::func());
+ global.define("footnote", FootnoteElem::func());
global.define("cite", CiteElem::func());
global.define("bibliography", BibliographyElem::func());
global.define("locate", locate);
diff --git a/library/src/shared/ext.rs b/library/src/shared/ext.rs
index 72a82749..d7c80a30 100644
--- a/library/src/shared/ext.rs
+++ b/library/src/shared/ext.rs
@@ -18,6 +18,11 @@ pub trait ContentExt {
/// Link the content somewhere.
fn linked(self, dest: Destination) -> Self;
+ /// Make the content linkable by `.linked(Destination::Location(loc))`.
+ ///
+ /// Should be used in combination with [`Location::variant`].
+ fn backlinked(self, loc: Location) -> Self;
+
/// Set alignments for this content.
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self;
@@ -45,6 +50,12 @@ impl ContentExt for Content {
self.styled(MetaElem::set_data(vec![Meta::Link(dest)]))
}
+ fn backlinked(self, loc: Location) -> Self {
+ let mut backlink = Content::empty();
+ backlink.set_location(loc);
+ self.styled(MetaElem::set_data(vec![Meta::Elem(backlink)]))
+ }
+
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
self.styled(AlignElem::set_alignment(aligns))
}