mirror of
https://github.com/typst/typst
synced 2025-05-15 01:25:28 +08:00
Footnotes
This commit is contained in:
parent
35b883cfd9
commit
1321862cd5
@ -6,6 +6,13 @@ description: |
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
- Footnotes
|
||||||
|
- Implemented support for footnotes
|
||||||
|
- The [`footnote`]($func/footnote) function inserts a footnote
|
||||||
|
- The [`footnote.entry`]($func/footnote.entry) function can be used to
|
||||||
|
customize the footnote listing
|
||||||
|
- The `{"chicago-notes"}` [citation style]($func/cite.style) is now available
|
||||||
|
|
||||||
- Documentation
|
- Documentation
|
||||||
- Added [guide for LaTeX users]($guides/guide-for-latex-users)
|
- Added [guide for LaTeX users]($guides/guide-for-latex-users)
|
||||||
- Now shows default values for optional arguments
|
- Now shows default values for optional arguments
|
||||||
|
@ -83,6 +83,7 @@ impl Layout for ColumnsElem {
|
|||||||
backlog: &backlog,
|
backlog: &backlog,
|
||||||
last: regions.last,
|
last: regions.last,
|
||||||
expand: Axes::new(true, regions.expand.y),
|
expand: Axes::new(true, regions.expand.y),
|
||||||
|
root: regions.root,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Layout the children.
|
// Layout the children.
|
||||||
|
@ -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::prelude::*;
|
||||||
use crate::visualize::{
|
use crate::visualize::{
|
||||||
CircleElem, EllipseElem, ImageElem, PathElem, PolygonElem, RectElem, SquareElem,
|
CircleElem, EllipseElem, ImageElem, PathElem, PolygonElem, RectElem, SquareElem,
|
||||||
@ -26,7 +31,7 @@ impl Layout for FlowElem {
|
|||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
regions: Regions,
|
regions: Regions,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
let mut layouter = FlowLayouter::new(regions);
|
let mut layouter = FlowLayouter::new(regions, styles);
|
||||||
|
|
||||||
for mut child in &self.children() {
|
for mut child in &self.children() {
|
||||||
let outer = styles;
|
let outer = styles;
|
||||||
@ -37,7 +42,7 @@ impl Layout for FlowElem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(elem) = child.to::<VElem>() {
|
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>() {
|
} else if let Some(elem) = child.to::<ParElem>() {
|
||||||
layouter.layout_par(vt, elem, styles)?;
|
layouter.layout_par(vt, elem, styles)?;
|
||||||
} else if child.is::<RectElem>()
|
} else if child.is::<RectElem>()
|
||||||
@ -63,21 +68,25 @@ impl Layout for FlowElem {
|
|||||||
} else if child.is::<ColbreakElem>() {
|
} else if child.is::<ColbreakElem>() {
|
||||||
if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some()
|
if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some()
|
||||||
{
|
{
|
||||||
layouter.finish_region();
|
layouter.finish_region()?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bail!(child.span(), "unexpected flow child");
|
bail!(child.span(), "unexpected flow child");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(layouter.finish())
|
layouter.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs flow layout.
|
/// Performs flow layout.
|
||||||
struct FlowLayouter<'a> {
|
struct FlowLayouter<'a> {
|
||||||
|
/// Whether this is the root flow.
|
||||||
|
root: bool,
|
||||||
/// The regions to layout children into.
|
/// The regions to layout children into.
|
||||||
regions: Regions<'a>,
|
regions: Regions<'a>,
|
||||||
|
/// The shared styles.
|
||||||
|
styles: StyleChain<'a>,
|
||||||
/// Whether the flow should expand to fill the region.
|
/// Whether the flow should expand to fill the region.
|
||||||
expand: Axes<bool>,
|
expand: Axes<bool>,
|
||||||
/// The initial size of `regions.size` that was available before we started
|
/// The initial size of `regions.size` that was available before we started
|
||||||
@ -85,12 +94,23 @@ struct FlowLayouter<'a> {
|
|||||||
initial: Size,
|
initial: Size,
|
||||||
/// Whether the last block was a paragraph.
|
/// Whether the last block was a paragraph.
|
||||||
last_was_par: bool,
|
last_was_par: bool,
|
||||||
/// Spacing and layouted blocks.
|
/// Spacing and layouted blocks for the current region.
|
||||||
items: Vec<FlowItem>,
|
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 frames for previous regions.
|
||||||
finished: Vec<Frame>,
|
finished: Vec<Frame>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cached footnote configuration.
|
||||||
|
struct FootnoteConfig {
|
||||||
|
separator: Content,
|
||||||
|
clearance: Abs,
|
||||||
|
gap: Abs,
|
||||||
|
}
|
||||||
|
|
||||||
/// A prepared item in a flow layout.
|
/// A prepared item in a flow layout.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum FlowItem {
|
enum FlowItem {
|
||||||
@ -102,36 +122,55 @@ enum FlowItem {
|
|||||||
Frame(Frame, Axes<Align>, bool),
|
Frame(Frame, Axes<Align>, bool),
|
||||||
/// An absolutely placed frame.
|
/// An absolutely placed frame.
|
||||||
Placed(Frame),
|
Placed(Frame),
|
||||||
|
/// A footnote frame (can also be the separator).
|
||||||
|
Footnote(Frame),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FlowLayouter<'a> {
|
impl<'a> FlowLayouter<'a> {
|
||||||
/// Create a new flow layouter.
|
/// 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;
|
let expand = regions.expand;
|
||||||
|
|
||||||
// Disable vertical expansion for children.
|
// Disable vertical expansion & root for children.
|
||||||
regions.expand.y = false;
|
regions.expand.y = false;
|
||||||
|
let root = mem::replace(&mut regions.root, false);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
root,
|
||||||
regions,
|
regions,
|
||||||
|
styles,
|
||||||
expand,
|
expand,
|
||||||
initial: regions.size,
|
initial: regions.size,
|
||||||
last_was_par: false,
|
last_was_par: false,
|
||||||
items: vec![],
|
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![],
|
finished: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout vertical spacing.
|
/// Layout vertical spacing.
|
||||||
#[tracing::instrument(name = "FlowLayouter::layout_spacing", skip_all)]
|
#[tracing::instrument(name = "FlowLayouter::layout_spacing", skip_all)]
|
||||||
fn layout_spacing(&mut self, v: &VElem, styles: StyleChain) {
|
fn layout_spacing(
|
||||||
self.layout_item(match v.amount() {
|
&mut self,
|
||||||
Spacing::Rel(rel) => FlowItem::Absolute(
|
vt: &mut Vt,
|
||||||
rel.resolve(styles).relative_to(self.initial.y),
|
v: &VElem,
|
||||||
v.weakness(styles) > 0,
|
styles: StyleChain,
|
||||||
),
|
) -> SourceResult<()> {
|
||||||
Spacing::Fr(fr) => FlowItem::Fractional(fr),
|
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.
|
/// Layout a paragraph.
|
||||||
@ -145,7 +184,7 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
let aligns = AlignElem::alignment_in(styles).resolve(styles);
|
let aligns = AlignElem::alignment_in(styles).resolve(styles);
|
||||||
let leading = ParElem::leading_in(styles);
|
let leading = ParElem::leading_in(styles);
|
||||||
let consecutive = self.last_was_par;
|
let consecutive = self.last_was_par;
|
||||||
let frames = par
|
let lines = par
|
||||||
.layout(vt, styles, consecutive, self.regions.base(), self.regions.expand.x)?
|
.layout(vt, styles, consecutive, self.regions.base(), self.regions.expand.x)?
|
||||||
.into_frames();
|
.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() {
|
if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() {
|
||||||
let carry: Vec<_> = self.items.drain(sticky..).collect();
|
let carry: Vec<_> = self.items.drain(sticky..).collect();
|
||||||
self.finish_region();
|
self.finish_region()?;
|
||||||
for item in carry {
|
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 {
|
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;
|
self.last_was_par = true;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +231,7 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
let sticky = BlockElem::sticky_in(styles);
|
let sticky = BlockElem::sticky_in(styles);
|
||||||
let pod = Regions::one(self.regions.base(), Axes::splat(false));
|
let pod = Regions::one(self.regions.base(), Axes::splat(false));
|
||||||
let frame = content.layout(vt, styles, pod)?.into_frame();
|
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;
|
self.last_was_par = false;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -210,7 +248,7 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
if let Some(placed) = block.to::<PlaceElem>() {
|
if let Some(placed) = block.to::<PlaceElem>() {
|
||||||
if placed.out_of_flow(styles) {
|
if placed.out_of_flow(styles) {
|
||||||
let frame = block.layout(vt, styles, self.regions)?.into_frame();
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,16 +263,22 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
}
|
}
|
||||||
.resolve(styles);
|
.resolve(styles);
|
||||||
|
|
||||||
|
let is_columns = block.is::<ColumnsElem>();
|
||||||
|
|
||||||
// Layout the block itself.
|
// Layout the block itself.
|
||||||
let sticky = BlockElem::sticky_in(styles);
|
let sticky = BlockElem::sticky_in(styles);
|
||||||
let fragment = block.layout(vt, styles, self.regions)?;
|
let fragment = block.layout(vt, styles, self.regions)?;
|
||||||
|
self.regions.root = self.root && is_columns;
|
||||||
|
|
||||||
for (i, frame) in fragment.into_iter().enumerate() {
|
for (i, frame) in fragment.into_iter().enumerate() {
|
||||||
if i > 0 {
|
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;
|
self.last_was_par = false;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -242,26 +286,38 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
|
|
||||||
/// Layout a finished frame.
|
/// Layout a finished frame.
|
||||||
#[tracing::instrument(name = "FlowLayouter::layout_item", skip_all)]
|
#[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 {
|
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::Fractional(_) => {}
|
||||||
FlowItem::Frame(ref frame, ..) => {
|
FlowItem::Frame(ref frame, ..) => {
|
||||||
let size = frame.size();
|
let size = frame.size();
|
||||||
if !self.regions.size.y.fits(size.y) && !self.regions.in_last() {
|
if !self.regions.size.y.fits(size.y) && !self.regions.in_last() {
|
||||||
self.finish_region();
|
self.finish_region()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.regions.size.y -= size.y;
|
self.regions.size.y -= size.y;
|
||||||
|
if self.root {
|
||||||
|
return self.handle_footnotes(vt, item, size.y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FlowItem::Placed(_) => {}
|
FlowItem::Placed(_) => {}
|
||||||
|
FlowItem::Footnote(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.items.push(item);
|
self.items.push(item);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish the frame for one region.
|
/// Finish the frame for one region.
|
||||||
fn finish_region(&mut self) {
|
fn finish_region(&mut self) -> SourceResult<()> {
|
||||||
// Trim weak spacing.
|
// Trim weak spacing.
|
||||||
while self
|
while self
|
||||||
.items
|
.items
|
||||||
@ -274,6 +330,8 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
// Determine the used size.
|
// Determine the used size.
|
||||||
let mut fr = Fr::zero();
|
let mut fr = Fr::zero();
|
||||||
let mut used = Size::zero();
|
let mut used = Size::zero();
|
||||||
|
let mut footnote_height = Abs::zero();
|
||||||
|
let mut first_footnote = true;
|
||||||
for item in &self.items {
|
for item in &self.items {
|
||||||
match item {
|
match item {
|
||||||
FlowItem::Absolute(v, _) => used.y += *v,
|
FlowItem::Absolute(v, _) => used.y += *v,
|
||||||
@ -284,19 +342,31 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
used.x.set_max(size.x);
|
used.x.set_max(size.x);
|
||||||
}
|
}
|
||||||
FlowItem::Placed(_) => {}
|
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
|
// 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);
|
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;
|
size.y = self.initial.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut output = Frame::new(size);
|
let mut output = Frame::new(size);
|
||||||
let mut offset = Abs::zero();
|
let mut offset = Abs::zero();
|
||||||
let mut ruler = Align::Top;
|
let mut ruler = Align::Top;
|
||||||
|
let mut footnote_offset = size.y - footnote_height;
|
||||||
|
|
||||||
// Place all frames.
|
// Place all frames.
|
||||||
for item in self.items.drain(..) {
|
for item in self.items.drain(..) {
|
||||||
@ -316,6 +386,11 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
offset += frame.height();
|
offset += frame.height();
|
||||||
output.push_frame(pos, frame);
|
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) => {
|
FlowItem::Placed(frame) => {
|
||||||
output.push_frame(Point::zero(), frame);
|
output.push_frame(Point::zero(), frame);
|
||||||
}
|
}
|
||||||
@ -326,17 +401,137 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
self.finished.push(output);
|
self.finished.push(output);
|
||||||
self.regions.next();
|
self.regions.next();
|
||||||
self.initial = self.regions.size;
|
self.initial = self.regions.size;
|
||||||
|
self.has_footnotes = false;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish layouting and return the resulting fragment.
|
/// Finish layouting and return the resulting fragment.
|
||||||
fn finish(mut self) -> Fragment {
|
fn finish(mut self) -> SourceResult<Fragment> {
|
||||||
if self.expand.y {
|
if self.expand.y {
|
||||||
while !self.regions.backlog.is_empty() {
|
while !self.regions.backlog.is_empty() {
|
||||||
self.finish_region();
|
self.finish_region()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.finish_region();
|
self.finish_region()?;
|
||||||
Fragment::frames(self.finished)
|
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());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -315,9 +315,11 @@ impl PageElem {
|
|||||||
child = ColumnsElem::new(child).with_count(columns).pack();
|
child = ColumnsElem::new(child).with_count(columns).pack();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout the child.
|
|
||||||
let area = size - margin.sum_by_axis();
|
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 mut fragment = child.layout(vt, styles, regions)?;
|
||||||
|
|
||||||
let fill = self.fill(styles);
|
let fill = self.fill(styles);
|
||||||
|
@ -569,6 +569,10 @@ fn collect<'a>(
|
|||||||
}
|
}
|
||||||
Segment::Text(full.len() - prev)
|
Segment::Text(full.len() - prev)
|
||||||
} else if let Some(elem) = child.to::<HElem>() {
|
} else if let Some(elem) = child.to::<HElem>() {
|
||||||
|
if elem.amount().is_zero() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
full.push(SPACING_REPLACE);
|
full.push(SPACING_REPLACE);
|
||||||
Segment::Spacing(elem.amount())
|
Segment::Spacing(elem.amount())
|
||||||
} else if let Some(elem) = child.to::<LinebreakElem>() {
|
} else if let Some(elem) = child.to::<LinebreakElem>() {
|
||||||
|
@ -17,6 +17,11 @@ pub struct Regions<'a> {
|
|||||||
/// Whether elements should expand to fill the regions instead of shrinking
|
/// Whether elements should expand to fill the regions instead of shrinking
|
||||||
/// to fit the content.
|
/// to fit the content.
|
||||||
pub expand: Axes<bool>,
|
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<'_> {
|
impl Regions<'_> {
|
||||||
@ -28,6 +33,7 @@ impl Regions<'_> {
|
|||||||
backlog: &[],
|
backlog: &[],
|
||||||
last: None,
|
last: None,
|
||||||
expand,
|
expand,
|
||||||
|
root: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +45,7 @@ impl Regions<'_> {
|
|||||||
backlog: &[],
|
backlog: &[],
|
||||||
last: Some(size.y),
|
last: Some(size.y),
|
||||||
expand,
|
expand,
|
||||||
|
root: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +74,7 @@ impl Regions<'_> {
|
|||||||
backlog,
|
backlog,
|
||||||
last: self.last.map(|y| f(Size::new(x, y)).y),
|
last: self.last.map(|y| f(Size::new(x, y)).y),
|
||||||
expand: self.expand,
|
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)
|
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.
|
/// Advance to the next region if there is any.
|
||||||
pub fn next(&mut self) {
|
pub fn next(&mut self) {
|
||||||
if let Some(height) = self
|
if let Some(height) = self
|
||||||
|
@ -177,6 +177,14 @@ impl Spacing {
|
|||||||
pub fn is_fractional(self) -> bool {
|
pub fn is_fractional(self) -> bool {
|
||||||
matches!(self, Self::Fr(_))
|
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 {
|
impl From<Abs> for Spacing {
|
||||||
|
@ -11,7 +11,7 @@ use typst::util::option_eq;
|
|||||||
|
|
||||||
use super::{LinkElem, LocalName, RefElem};
|
use super::{LinkElem, LocalName, RefElem};
|
||||||
use crate::layout::{BlockElem, GridElem, ParElem, Sizing, TrackSizings, VElem};
|
use crate::layout::{BlockElem, GridElem, ParElem, Sizing, TrackSizings, VElem};
|
||||||
use crate::meta::HeadingElem;
|
use crate::meta::{FootnoteElem, HeadingElem};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
|
|
||||||
@ -243,6 +243,9 @@ pub enum BibliographyStyle {
|
|||||||
/// The Chicago Author Date style. Based on the 17th edition of the Chicago
|
/// The Chicago Author Date style. Based on the 17th edition of the Chicago
|
||||||
/// Manual of Style, Chapter 15.
|
/// Manual of Style, Chapter 15.
|
||||||
ChicagoAuthorDate,
|
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.
|
/// The style of the Institute of Electrical and Electronics Engineers.
|
||||||
/// Based on the 2018 IEEE Reference Guide.
|
/// Based on the 2018 IEEE Reference Guide.
|
||||||
Ieee,
|
Ieee,
|
||||||
@ -257,6 +260,7 @@ impl BibliographyStyle {
|
|||||||
match self {
|
match self {
|
||||||
Self::Apa => CitationStyle::ChicagoAuthorDate,
|
Self::Apa => CitationStyle::ChicagoAuthorDate,
|
||||||
Self::ChicagoAuthorDate => CitationStyle::ChicagoAuthorDate,
|
Self::ChicagoAuthorDate => CitationStyle::ChicagoAuthorDate,
|
||||||
|
Self::ChicagoNotes => CitationStyle::ChicagoNotes,
|
||||||
Self::Ieee => CitationStyle::Numerical,
|
Self::Ieee => CitationStyle::Numerical,
|
||||||
Self::Mla => CitationStyle::ChicagoAuthorDate,
|
Self::Mla => CitationStyle::ChicagoAuthorDate,
|
||||||
}
|
}
|
||||||
@ -385,7 +389,10 @@ pub enum CitationStyle {
|
|||||||
/// The Chicago Author Date style. Based on the 17th edition of the Chicago
|
/// The Chicago Author Date style. Based on the 17th edition of the Chicago
|
||||||
/// Manual of Style, Chapter 15.
|
/// Manual of Style, Chapter 15.
|
||||||
ChicagoAuthorDate,
|
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”.
|
/// Prokopov, “It Is Fast or It Is Wrong”.
|
||||||
ChicagoAuthorTitle,
|
ChicagoAuthorTitle,
|
||||||
}
|
}
|
||||||
@ -487,6 +494,7 @@ fn create(
|
|||||||
CitationStyle::ChicagoAuthorDate => {
|
CitationStyle::ChicagoAuthorDate => {
|
||||||
Box::new(style::ChicagoAuthorDate::new())
|
Box::new(style::ChicagoAuthorDate::new())
|
||||||
}
|
}
|
||||||
|
CitationStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()),
|
||||||
CitationStyle::ChicagoAuthorTitle => {
|
CitationStyle::ChicagoAuthorTitle => {
|
||||||
Box::new(style::AuthorTitle::new())
|
Box::new(style::AuthorTitle::new())
|
||||||
}
|
}
|
||||||
@ -537,6 +545,10 @@ fn create(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if style == CitationStyle::ChicagoNotes {
|
||||||
|
content = FootnoteElem::new(content).pack();
|
||||||
|
}
|
||||||
|
|
||||||
(location, Some(content))
|
(location, Some(content))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -544,6 +556,7 @@ fn create(
|
|||||||
let bibliography_style: Box<dyn style::BibliographyStyle> = match style {
|
let bibliography_style: Box<dyn style::BibliographyStyle> = match style {
|
||||||
BibliographyStyle::Apa => Box::new(style::Apa::new()),
|
BibliographyStyle::Apa => Box::new(style::Apa::new()),
|
||||||
BibliographyStyle::ChicagoAuthorDate => Box::new(style::ChicagoAuthorDate::new()),
|
BibliographyStyle::ChicagoAuthorDate => Box::new(style::ChicagoAuthorDate::new()),
|
||||||
|
BibliographyStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()),
|
||||||
BibliographyStyle::Ieee => Box::new(style::Ieee::new()),
|
BibliographyStyle::Ieee => Box::new(style::Ieee::new()),
|
||||||
BibliographyStyle::Mla => Box::new(style::Mla::new()),
|
BibliographyStyle::Mla => Box::new(style::Mla::new()),
|
||||||
};
|
};
|
||||||
@ -552,24 +565,18 @@ fn create(
|
|||||||
.bibliography(&*bibliography_style, None)
|
.bibliography(&*bibliography_style, None)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|reference| {
|
.map(|reference| {
|
||||||
// Make link from citation to here work.
|
let backlink = ref_location(reference.entry);
|
||||||
let backlink = {
|
|
||||||
let mut content = Content::empty();
|
|
||||||
content.set_location(ref_location(reference.entry));
|
|
||||||
MetaElem::set_data(vec![Meta::Elem(content)])
|
|
||||||
};
|
|
||||||
|
|
||||||
let prefix = reference.prefix.map(|prefix| {
|
let prefix = reference.prefix.map(|prefix| {
|
||||||
// Format and link to first citation.
|
// Format and link to first citation.
|
||||||
let bracketed = prefix.with_default_brackets(&*citation_style);
|
let bracketed = prefix.with_default_brackets(&*citation_style);
|
||||||
format_display_string(&bracketed, None, span)
|
format_display_string(&bracketed, None, span)
|
||||||
.linked(Destination::Location(ids[reference.entry.key()]))
|
.linked(Destination::Location(ids[reference.entry.key()]))
|
||||||
.styled(backlink.clone())
|
.backlinked(backlink)
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut reference = format_display_string(&reference.display, None, span);
|
let mut reference = format_display_string(&reference.display, None, span);
|
||||||
if prefix.is_none() {
|
if prefix.is_none() {
|
||||||
reference = reference.styled(backlink);
|
reference = reference.backlinked(backlink);
|
||||||
}
|
}
|
||||||
|
|
||||||
(prefix, reference)
|
(prefix, reference)
|
||||||
|
217
library/src/meta/footnote.rs
Normal file
217
library/src/meta/footnote.rs
Normal file
@ -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())),
|
||||||
|
}
|
@ -5,6 +5,7 @@ mod context;
|
|||||||
mod counter;
|
mod counter;
|
||||||
mod document;
|
mod document;
|
||||||
mod figure;
|
mod figure;
|
||||||
|
mod footnote;
|
||||||
mod heading;
|
mod heading;
|
||||||
mod link;
|
mod link;
|
||||||
mod numbering;
|
mod numbering;
|
||||||
@ -18,6 +19,7 @@ pub use self::context::*;
|
|||||||
pub use self::counter::*;
|
pub use self::counter::*;
|
||||||
pub use self::document::*;
|
pub use self::document::*;
|
||||||
pub use self::figure::*;
|
pub use self::figure::*;
|
||||||
|
pub use self::footnote::*;
|
||||||
pub use self::heading::*;
|
pub use self::heading::*;
|
||||||
pub use self::link::*;
|
pub use self::link::*;
|
||||||
pub use self::numbering::*;
|
pub use self::numbering::*;
|
||||||
@ -36,6 +38,7 @@ pub(super) fn define(global: &mut Scope) {
|
|||||||
global.define("outline", OutlineElem::func());
|
global.define("outline", OutlineElem::func());
|
||||||
global.define("heading", HeadingElem::func());
|
global.define("heading", HeadingElem::func());
|
||||||
global.define("figure", FigureElem::func());
|
global.define("figure", FigureElem::func());
|
||||||
|
global.define("footnote", FootnoteElem::func());
|
||||||
global.define("cite", CiteElem::func());
|
global.define("cite", CiteElem::func());
|
||||||
global.define("bibliography", BibliographyElem::func());
|
global.define("bibliography", BibliographyElem::func());
|
||||||
global.define("locate", locate);
|
global.define("locate", locate);
|
||||||
|
@ -18,6 +18,11 @@ pub trait ContentExt {
|
|||||||
/// Link the content somewhere.
|
/// Link the content somewhere.
|
||||||
fn linked(self, dest: Destination) -> Self;
|
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.
|
/// Set alignments for this content.
|
||||||
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self;
|
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)]))
|
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 {
|
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
|
||||||
self.styled(AlignElem::set_alignment(aligns))
|
self.styled(AlignElem::set_alignment(aligns))
|
||||||
}
|
}
|
||||||
|
13
src/doc.rs
13
src/doc.rs
@ -608,7 +608,7 @@ cast_to_value! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Meta information that isn't visible or renderable.
|
/// Meta information that isn't visible or renderable.
|
||||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
#[derive(Clone, PartialEq, Hash)]
|
||||||
pub enum Meta {
|
pub enum Meta {
|
||||||
/// An internal or external link to a destination.
|
/// An internal or external link to a destination.
|
||||||
Link(Destination),
|
Link(Destination),
|
||||||
@ -623,6 +623,17 @@ pub enum Meta {
|
|||||||
Hide,
|
Hide,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for Meta {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Link(dest) => write!(f, "Link({dest:?})"),
|
||||||
|
Self::Elem(content) => write!(f, "Elem({:?})", content.func()),
|
||||||
|
Self::PageNumbering(value) => write!(f, "PageNumbering({value:?})"),
|
||||||
|
Self::Hide => f.pad("Hide"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cast_from_value! {
|
cast_from_value! {
|
||||||
Meta: "meta",
|
Meta: "meta",
|
||||||
}
|
}
|
||||||
|
@ -80,12 +80,6 @@ impl From<Em> for Length {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Abs> for Rel<Length> {
|
|
||||||
fn from(abs: Abs) -> Self {
|
|
||||||
Rel::from(Length::from(abs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Neg for Length {
|
impl Neg for Length {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
|
@ -73,6 +73,18 @@ impl<T: Numeric> Debug for Rel<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Abs> for Rel<Length> {
|
||||||
|
fn from(abs: Abs) -> Self {
|
||||||
|
Rel::from(Length::from(abs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Em> for Rel<Length> {
|
||||||
|
fn from(em: Em) -> Self {
|
||||||
|
Rel::from(Length::from(em))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Numeric> From<T> for Rel<T> {
|
impl<T: Numeric> From<T> for Rel<T> {
|
||||||
fn from(abs: T) -> Self {
|
fn from(abs: T) -> Self {
|
||||||
Self { rel: Ratio::zero(), abs }
|
Self { rel: Ratio::zero(), abs }
|
||||||
|
BIN
tests/ref/meta/cite-footnote.png
Normal file
BIN
tests/ref/meta/cite-footnote.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
BIN
tests/ref/meta/footnote-break.png
Normal file
BIN
tests/ref/meta/footnote-break.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
BIN
tests/ref/meta/footnote-container.png
Normal file
BIN
tests/ref/meta/footnote-container.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
BIN
tests/ref/meta/footnote-invariant.png
Normal file
BIN
tests/ref/meta/footnote-invariant.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
tests/ref/meta/footnote.png
Normal file
BIN
tests/ref/meta/footnote.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
5
tests/typ/meta/cite-footnote.typ
Normal file
5
tests/typ/meta/cite-footnote.typ
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Hello @netwok
|
||||||
|
And again: @netwok
|
||||||
|
|
||||||
|
#pagebreak()
|
||||||
|
#bibliography("/works.bib", style: "chicago-notes")
|
16
tests/typ/meta/footnote-break.typ
Normal file
16
tests/typ/meta/footnote-break.typ
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Test footnotes that break across pages.
|
||||||
|
|
||||||
|
---
|
||||||
|
#set page(height: 200pt)
|
||||||
|
|
||||||
|
#lorem(5)
|
||||||
|
#footnote[ // 1
|
||||||
|
A simple footnote.
|
||||||
|
#footnote[Well, not that simple ...] // 2
|
||||||
|
]
|
||||||
|
#lorem(15)
|
||||||
|
#footnote[Another footnote: #lorem(30)] // 3
|
||||||
|
#lorem(15)
|
||||||
|
#footnote[My fourth footnote: #lorem(50)] // 4
|
||||||
|
#lorem(15)
|
||||||
|
#footnote[And a final footnote.] // 5
|
32
tests/typ/meta/footnote-container.typ
Normal file
32
tests/typ/meta/footnote-container.typ
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Test footnotes in containers.
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test footnote in caption.
|
||||||
|
Read the docs #footnote[https://typst.app/docs]!
|
||||||
|
#figure(
|
||||||
|
image("/graph.png", width: 70%),
|
||||||
|
caption: [
|
||||||
|
A graph #footnote[A _graph_ is a structure with nodes and edges.]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
More #footnote[just for ...] footnotes #footnote[... testing. :)]
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test duplicate footnotes.
|
||||||
|
#let lang = footnote[Languages.]
|
||||||
|
#let nums = footnote[Numbers.]
|
||||||
|
|
||||||
|
/ "Hello": A word #lang
|
||||||
|
/ "123": A number #nums
|
||||||
|
|
||||||
|
- "Hello" #lang
|
||||||
|
- "123" #nums
|
||||||
|
|
||||||
|
+ "Hello" #lang
|
||||||
|
+ "123" #nums
|
||||||
|
|
||||||
|
#table(
|
||||||
|
columns: 2,
|
||||||
|
[Hello], [A word #lang],
|
||||||
|
[123], [A number #nums],
|
||||||
|
)
|
9
tests/typ/meta/footnote-invariant.typ
Normal file
9
tests/typ/meta/footnote-invariant.typ
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Ensure that a footnote and the first line of its entry
|
||||||
|
// always end up on the same page.
|
||||||
|
|
||||||
|
---
|
||||||
|
#set page(height: 120pt)
|
||||||
|
|
||||||
|
#lorem(13)
|
||||||
|
|
||||||
|
There #footnote(lorem(20))
|
34
tests/typ/meta/footnote.typ
Normal file
34
tests/typ/meta/footnote.typ
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Test footnotes.
|
||||||
|
|
||||||
|
---
|
||||||
|
#footnote[Hi]
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test space collapsing before footnote.
|
||||||
|
A#footnote[A] \
|
||||||
|
A #footnote[A]
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test nested footnotes.
|
||||||
|
First \
|
||||||
|
Second #footnote[A, #footnote[B, #footnote[C]]] \
|
||||||
|
Third #footnote[D, #footnote[E]] \
|
||||||
|
Fourth
|
||||||
|
|
||||||
|
---
|
||||||
|
// Currently, numbers a bit out of order if a nested footnote ends up in the
|
||||||
|
// same frame as another one. :(
|
||||||
|
#footnote[A, #footnote[B]], #footnote[C]
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test customization.
|
||||||
|
#show footnote: set text(red)
|
||||||
|
#show footnote.entry: set text(8pt, style: "italic")
|
||||||
|
#set footnote.entry(
|
||||||
|
indent: 0pt,
|
||||||
|
gap: 0.6em,
|
||||||
|
clearance: 0.3em,
|
||||||
|
separator: repeat[.],
|
||||||
|
)
|
||||||
|
|
||||||
|
Beautiful footnotes. #footnote[Wonderful, aren't they?]
|
Loading…
x
Reference in New Issue
Block a user