mirror of
https://github.com/typst/typst
synced 2025-05-20 03:55:29 +08:00
396 lines
13 KiB
Rust
396 lines
13 KiB
Rust
use std::str::FromStr;
|
|
|
|
use super::{
|
|
Count, Counter, CounterKey, CounterUpdate, LocalName, Numbering, NumberingPattern,
|
|
};
|
|
use crate::layout::{BlockElem, VElem};
|
|
use crate::meta::{Refable, Supplement};
|
|
use crate::prelude::*;
|
|
use crate::text::TextElem;
|
|
use crate::visualize::ImageElem;
|
|
|
|
/// A figure with an optional caption.
|
|
///
|
|
/// Automatically detects its contents to select the correct counting track.
|
|
/// For example, figures containing images will be numbered separately from
|
|
/// figures containing tables.
|
|
///
|
|
/// ## Examples { #examples }
|
|
/// The example below shows a basic figure with an image:
|
|
/// ```example
|
|
/// @glacier shows a glacier. Glaciers
|
|
/// are complex systems.
|
|
///
|
|
/// #figure(
|
|
/// image("glacier.jpg", width: 80%),
|
|
/// caption: [A curious figure.],
|
|
/// ) <glacier>
|
|
/// ```
|
|
///
|
|
/// You can also insert [tables]($func/table) into figures to give them a
|
|
/// caption. The figure will detect this and automatically use a separate
|
|
/// counter.
|
|
///
|
|
/// ```example
|
|
/// #figure(
|
|
/// table(
|
|
/// columns: 4,
|
|
/// [t], [1], [2], [3],
|
|
/// [y], [0.3s], [0.4s], [0.8s],
|
|
/// ),
|
|
/// caption: [Timing results],
|
|
/// )
|
|
/// ```
|
|
///
|
|
/// This behaviour can be overridden by explicitly specifying the figure's
|
|
/// `kind`. All figures of the same kind share a common counter.
|
|
///
|
|
/// ## Modifying the appearance { #modifying-appearance }
|
|
/// You can completely customize the look of your figures with a [show
|
|
/// rule]($styling/#show-rules). In the example below, we show the figure's
|
|
/// caption above its body and display its supplement and counter after the
|
|
/// caption.
|
|
///
|
|
/// ```example
|
|
/// #show figure: it => align(center)[
|
|
/// #it.caption |
|
|
/// #emph[
|
|
/// #it.supplement
|
|
/// #it.counter.display(it.numbering)
|
|
/// ]
|
|
/// #v(10pt, weak: true)
|
|
/// #it.body
|
|
/// ]
|
|
///
|
|
/// #figure(
|
|
/// image("molecular.jpg", width: 80%),
|
|
/// caption: [
|
|
/// The molecular testing pipeline.
|
|
/// ],
|
|
/// )
|
|
/// ```
|
|
///
|
|
/// If your figure is too large and its contents are breakable across pages
|
|
/// (e.g. if it contains a large table), then you can make the figure breakable
|
|
/// across pages as well by using `#show figure: set block(breakable: true)`
|
|
/// (see the [block]($func/block) documentation for more information).
|
|
///
|
|
/// Display: Figure
|
|
/// Category: meta
|
|
#[element(Locatable, Synthesize, Count, Show, Finalize, Refable)]
|
|
pub struct FigureElem {
|
|
/// The content of the figure. Often, an [image]($func/image).
|
|
#[required]
|
|
pub body: Content,
|
|
|
|
/// The figure's caption.
|
|
pub caption: Option<Content>,
|
|
|
|
/// The kind of the figure this is.
|
|
///
|
|
/// If set to `{auto}`, the figure will try to automatically determine its
|
|
/// kind. All figures of the same kind share a common counter.
|
|
///
|
|
/// Setting this to something other than `{auto}` will override the
|
|
/// automatic detection. This can be useful if
|
|
/// - you wish to create a custom figure type that is not an
|
|
/// [image]($func/image), a [table]($func/table) or [code]($func/raw),
|
|
/// - you want to force the figure to use a specific counter regardless of
|
|
/// its content.
|
|
///
|
|
/// You can set the kind to be an element function or a string. If you set
|
|
/// it to an element function that is not supported by the figure, you will
|
|
/// need to manually specify the figure's supplement.
|
|
///
|
|
/// ```example
|
|
/// #figure(
|
|
/// circle(radius: 10pt),
|
|
/// caption: [A curious atom.],
|
|
/// kind: "atom",
|
|
/// supplement: [Atom],
|
|
/// )
|
|
/// ```
|
|
#[default(Smart::Auto)]
|
|
pub kind: Smart<FigureKind>,
|
|
|
|
/// The figure's supplement.
|
|
///
|
|
/// If set to `{auto}`, the figure will try to automatically determine the
|
|
/// correct supplement based on the `kind` and the active [text
|
|
/// language]($func/text.lang). If you are using a custom figure type, you
|
|
/// will need to manually specify the supplement.
|
|
///
|
|
/// This can also be set to a function that receives the figure's body to
|
|
/// select the supplement based on the figure's contents.
|
|
///
|
|
/// ```example
|
|
/// #figure(
|
|
/// [The contents of my figure!],
|
|
/// caption: [My custom figure],
|
|
/// supplement: [Bar],
|
|
/// kind: "foo",
|
|
/// )
|
|
/// ```
|
|
#[default(Smart::Auto)]
|
|
pub supplement: Smart<Supplement>,
|
|
|
|
/// How to number the figure. Accepts a
|
|
/// [numbering pattern or function]($func/numbering).
|
|
#[default(Some(NumberingPattern::from_str("1").unwrap().into()))]
|
|
pub numbering: Option<Numbering>,
|
|
|
|
/// The vertical gap between the body and caption.
|
|
#[default(Em::new(0.65).into())]
|
|
pub gap: Length,
|
|
|
|
/// Whether the figure should appear in an [`outline`]($func/outline)
|
|
/// of figures.
|
|
#[default(true)]
|
|
pub outlined: bool,
|
|
|
|
/// Convenience field to get access to the counter for this figure.
|
|
///
|
|
/// The counter only depends on the `kind`:
|
|
/// - For (tables)[$func/table]: `{counter(figure.where(kind: table))}`
|
|
/// - For (images)[$func/image]: `{counter(figure.where(kind: image))}`
|
|
/// - For a custom kind: `{counter(figure.where(kind: kind))}`
|
|
///
|
|
/// These are the counters you'll need to modify if you want to skip a
|
|
/// number or reset the counter.
|
|
#[synthesized]
|
|
pub counter: Option<Counter>,
|
|
}
|
|
|
|
impl Synthesize for FigureElem {
|
|
fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
|
|
// Determine the figure's kind.
|
|
let kind = match self.kind(styles) {
|
|
Smart::Auto => self
|
|
.find_figurable()
|
|
.map(|elem| FigureKind::Elem(elem.func()))
|
|
.unwrap_or_else(|| FigureKind::Elem(ImageElem::func())),
|
|
Smart::Custom(kind) => kind,
|
|
};
|
|
|
|
let content = match &kind {
|
|
FigureKind::Elem(func) => self.find_of_elem(*func),
|
|
FigureKind::Name(_) => None,
|
|
}
|
|
.unwrap_or_else(|| self.body());
|
|
|
|
let numbering = self.numbering(styles);
|
|
|
|
// We get the supplement or `None`. The supplement must either be set
|
|
// manually or the content identification must have succeeded.
|
|
let supplement = match self.supplement(styles) {
|
|
Smart::Auto => match &kind {
|
|
FigureKind::Elem(func) => {
|
|
let elem = Content::new(*func).with::<dyn LocalName>().map(|c| {
|
|
TextElem::packed(c.local_name(
|
|
TextElem::lang_in(styles),
|
|
TextElem::region_in(styles),
|
|
))
|
|
});
|
|
|
|
if numbering.is_some() {
|
|
Some(elem
|
|
.ok_or("unable to determine the figure's `supplement`, please specify it manually")
|
|
.at(self.span())?)
|
|
} else {
|
|
elem
|
|
}
|
|
}
|
|
FigureKind::Name(_) => {
|
|
if numbering.is_some() {
|
|
bail!(self.span(), "please specify the figure's supplement")
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
},
|
|
Smart::Custom(supp) => Some(supp.resolve(vt, [content.into()])?),
|
|
};
|
|
|
|
// Construct the figure's counter.
|
|
let counter = Counter::new(CounterKey::Selector(Selector::Elem(
|
|
Self::func(),
|
|
Some(dict! {
|
|
"kind" => kind.clone(),
|
|
}),
|
|
)));
|
|
|
|
self.push_caption(self.caption(styles));
|
|
self.push_kind(Smart::Custom(kind));
|
|
self.push_supplement(Smart::Custom(Supplement::Content(
|
|
supplement.unwrap_or_default(),
|
|
)));
|
|
self.push_numbering(numbering);
|
|
self.push_outlined(self.outlined(styles));
|
|
self.push_counter(Some(counter));
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Show for FigureElem {
|
|
#[tracing::instrument(name = "FigureElem::show", skip_all)]
|
|
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
|
// We build the body of the figure.
|
|
let mut realized = self.body();
|
|
|
|
// We build the caption, if any.
|
|
if self.caption(styles).is_some() {
|
|
realized += VElem::weak(self.gap(styles).into()).pack();
|
|
realized += self.show_caption(vt)?;
|
|
}
|
|
|
|
// We wrap the contents in a block.
|
|
Ok(BlockElem::new()
|
|
.with_body(Some(realized))
|
|
.pack()
|
|
.aligned(Axes::with_x(Some(Align::Center.into()))))
|
|
}
|
|
}
|
|
|
|
impl Finalize for FigureElem {
|
|
fn finalize(&self, realized: Content, _: StyleChain) -> Content {
|
|
// Allow breakable figures with `show figure: set block(breakable: true)`.
|
|
realized.styled(BlockElem::set_breakable(false))
|
|
}
|
|
}
|
|
|
|
impl Count for FigureElem {
|
|
fn update(&self) -> Option<CounterUpdate> {
|
|
// If the figure is numbered, step the counter by one.
|
|
// This steps the `counter(figure)` which is global to all numbered figures.
|
|
self.numbering(StyleChain::default())
|
|
.is_some()
|
|
.then(|| CounterUpdate::Step(NonZeroUsize::ONE))
|
|
}
|
|
}
|
|
|
|
impl Refable for FigureElem {
|
|
fn reference(
|
|
&self,
|
|
vt: &mut Vt,
|
|
supplement: Option<Content>,
|
|
_: Lang,
|
|
_: Option<Region>,
|
|
) -> SourceResult<Content> {
|
|
// If the figure is not numbered, we cannot reference it.
|
|
// Otherwise we build the supplement and numbering scheme.
|
|
let Some(desc) = self.show_supplement_and_numbering(vt, supplement)? else {
|
|
bail!(self.span(), "cannot reference unnumbered figure")
|
|
};
|
|
|
|
Ok(desc)
|
|
}
|
|
|
|
fn outline(
|
|
&self,
|
|
vt: &mut Vt,
|
|
_: Lang,
|
|
_: Option<Region>,
|
|
) -> SourceResult<Option<Content>> {
|
|
// If the figure is not outlined, it is not referenced.
|
|
if !self.outlined(StyleChain::default()) {
|
|
return Ok(None);
|
|
}
|
|
|
|
self.show_caption(vt).map(Some)
|
|
}
|
|
|
|
fn numbering(&self) -> Option<Numbering> {
|
|
self.numbering(StyleChain::default())
|
|
}
|
|
|
|
fn counter(&self) -> Counter {
|
|
self.counter().unwrap_or_else(|| Counter::of(Self::func()))
|
|
}
|
|
}
|
|
|
|
impl FigureElem {
|
|
/// Determines the type of the figure by looking at the content, finding all
|
|
/// [`Figurable`] elements and sorting them by priority then returning the highest.
|
|
pub fn find_figurable(&self) -> Option<Content> {
|
|
self.body().query_first(Selector::can::<dyn Figurable>()).cloned()
|
|
}
|
|
|
|
/// Finds the element with the given function in the figure's content.
|
|
/// Returns `None` if no element with the given function is found.
|
|
pub fn find_of_elem(&self, func: ElemFunc) -> Option<Content> {
|
|
self.body().query_first(Selector::Elem(func, None)).cloned()
|
|
}
|
|
|
|
/// Builds the supplement and numbering of the figure. Returns [`None`] if
|
|
/// there is no numbering.
|
|
pub fn show_supplement_and_numbering(
|
|
&self,
|
|
vt: &mut Vt,
|
|
external_supplement: Option<Content>,
|
|
) -> SourceResult<Option<Content>> {
|
|
if let (Some(numbering), Some(supplement), Some(counter)) = (
|
|
self.numbering(StyleChain::default()),
|
|
self.supplement(StyleChain::default())
|
|
.as_custom()
|
|
.and_then(|s| s.as_content()),
|
|
self.counter(),
|
|
) {
|
|
let mut name = external_supplement.unwrap_or(supplement);
|
|
if !name.is_empty() {
|
|
name += TextElem::packed("\u{a0}");
|
|
}
|
|
|
|
let number = counter
|
|
.at(vt, self.0.location().unwrap())?
|
|
.display(vt, &numbering)?
|
|
.spanned(self.span());
|
|
|
|
Ok(Some(name + number))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// Builds the caption for the figure. If there is a numbering, will also
|
|
/// try to show the supplement and the numbering.
|
|
pub fn show_caption(&self, vt: &mut Vt) -> SourceResult<Content> {
|
|
let Some(mut caption) = self.caption(StyleChain::default()) else {
|
|
return Ok(Content::empty());
|
|
};
|
|
|
|
if let Some(sup_and_num) = self.show_supplement_and_numbering(vt, None)? {
|
|
caption = sup_and_num + TextElem::packed(": ") + caption;
|
|
}
|
|
|
|
Ok(caption)
|
|
}
|
|
}
|
|
|
|
/// The `kind` parameter of a [`FigureElem`].
|
|
#[derive(Debug, Clone)]
|
|
pub enum FigureKind {
|
|
/// The kind is an element function.
|
|
Elem(ElemFunc),
|
|
/// The kind is a name.
|
|
Name(EcoString),
|
|
}
|
|
|
|
cast_from_value! {
|
|
FigureKind,
|
|
v: ElemFunc => Self::Elem(v),
|
|
v: EcoString => Self::Name(v),
|
|
}
|
|
|
|
cast_to_value! {
|
|
v: FigureKind => match v {
|
|
FigureKind::Elem(v) => v.into(),
|
|
FigureKind::Name(v) => v.into(),
|
|
}
|
|
}
|
|
|
|
/// An element that can be auto-detected in a figure.
|
|
///
|
|
/// This trait is used to determine the type of a figure.
|
|
pub trait Figurable: LocalName {}
|