summaryrefslogtreecommitdiff
path: root/library/src
diff options
context:
space:
mode:
authorSébastien d'Herbais de Thun <sebastien.d.herbais@gmail.com>2023-04-04 19:21:25 +0200
committerGitHub <noreply@github.com>2023-04-04 19:21:25 +0200
commitf347ed4314e32383dc09ff234180e8ea6fef7b8b (patch)
treedba4296812a131e52da4eb0079fd0d240860d832 /library/src
parent5b0297464efc131beb7be84fa7a02b9a8670b5dd (diff)
Improved figure numbering, labelling and referencing (#491)
Diffstat (limited to 'library/src')
-rw-r--r--library/src/layout/table.rs10
-rw-r--r--library/src/math/mod.rs48
-rw-r--r--library/src/meta/bibliography.rs8
-rw-r--r--library/src/meta/counter.rs3
-rw-r--r--library/src/meta/figure.rs377
-rw-r--r--library/src/meta/heading.rs100
-rw-r--r--library/src/meta/outline.rs90
-rw-r--r--library/src/meta/reference.rs114
-rw-r--r--library/src/text/raw.rs25
-rw-r--r--library/src/visualize/image.rs25
10 files changed, 694 insertions, 106 deletions
diff --git a/library/src/layout/table.rs b/library/src/layout/table.rs
index 4ba8a56e..c9a67a1b 100644
--- a/library/src/layout/table.rs
+++ b/library/src/layout/table.rs
@@ -1,5 +1,5 @@
use crate::layout::{AlignElem, GridLayouter, TrackSizings};
-use crate::meta::LocalName;
+use crate::meta::{Figurable, LocalName};
use crate::prelude::*;
/// A table of items.
@@ -32,7 +32,7 @@ use crate::prelude::*;
///
/// Display: Table
/// Category: layout
-#[element(Layout, LocalName)]
+#[element(Layout, LocalName, Figurable)]
pub struct TableElem {
/// Defines the column sizes. See the [grid documentation]($func/grid) for
/// more information on track sizing.
@@ -293,3 +293,9 @@ impl LocalName for TableElem {
}
}
}
+
+impl Figurable for TableElem {
+ fn priority(&self, _styles: StyleChain) -> isize {
+ -1000
+ }
+}
diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs
index 82ad0a2f..b07fc78f 100644
--- a/library/src/math/mod.rs
+++ b/library/src/math/mod.rs
@@ -39,6 +39,7 @@ use self::fragment::*;
use self::row::*;
use self::spacing::*;
use crate::layout::{HElem, ParElem, Spacing};
+use crate::meta::Refable;
use crate::meta::{Count, Counter, CounterUpdate, LocalName, Numbering};
use crate::prelude::*;
use crate::text::{
@@ -134,7 +135,9 @@ pub fn module() -> Module {
///
/// Display: Equation
/// Category: math
-#[element(Locatable, Synthesize, Show, Finalize, Layout, LayoutMath, Count, LocalName)]
+#[element(
+ Locatable, Synthesize, Show, Finalize, Layout, LayoutMath, Count, LocalName, Refable
+)]
pub struct EquationElem {
/// Whether the equation is displayed as a separate block.
#[default(false)]
@@ -159,9 +162,11 @@ pub struct EquationElem {
}
impl Synthesize for EquationElem {
- fn synthesize(&mut self, styles: StyleChain) {
+ fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
self.push_block(self.block(styles));
self.push_numbering(self.numbering(styles));
+
+ Ok(())
}
}
@@ -278,6 +283,45 @@ impl LocalName for EquationElem {
}
}
+impl Refable for EquationElem {
+ fn reference(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ supplement: Option<Content>,
+ ) -> SourceResult<Content> {
+ // first we create the supplement of the heading
+ let mut supplement = supplement.unwrap_or_else(|| {
+ TextElem::packed(self.local_name(TextElem::lang_in(styles)))
+ });
+
+ // we append a space if the supplement is not empty
+ if !supplement.is_empty() {
+ supplement += TextElem::packed('\u{a0}')
+ };
+
+ // we check for a numbering
+ let Some(numbering) = self.numbering(styles) else {
+ bail!(self.span(), "only numbered equations can be referenced");
+ };
+
+ // we get the counter and display it
+ let numbers = Counter::of(Self::func())
+ .at(vt, self.0.location().expect("missing location"))?
+ .display(vt, &numbering.trimmed())?;
+
+ Ok(supplement + numbers)
+ }
+
+ fn numbering(&self, styles: StyleChain) -> Option<Numbering> {
+ self.numbering(styles)
+ }
+
+ fn counter(&self, _styles: StyleChain) -> Counter {
+ Counter::of(Self::func())
+ }
+}
+
pub trait LayoutMath {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>;
}
diff --git a/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs
index 635f1d8c..7bc2ff7b 100644
--- a/library/src/meta/bibliography.rs
+++ b/library/src/meta/bibliography.rs
@@ -133,8 +133,10 @@ impl BibliographyElem {
}
impl Synthesize for BibliographyElem {
- fn synthesize(&mut self, styles: StyleChain) {
+ fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
self.push_style(self.style(styles));
+
+ Ok(())
}
}
@@ -316,10 +318,12 @@ pub struct CiteElem {
}
impl Synthesize for CiteElem {
- fn synthesize(&mut self, styles: StyleChain) {
+ fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
self.push_supplement(self.supplement(styles));
self.push_brackets(self.brackets(styles));
self.push_style(self.style(styles));
+
+ Ok(())
}
}
diff --git a/library/src/meta/counter.rs b/library/src/meta/counter.rs
index e11e049d..3868040e 100644
--- a/library/src/meta/counter.rs
+++ b/library/src/meta/counter.rs
@@ -495,6 +495,9 @@ cast_from_value! {
}
Self::Selector(Selector::Elem(element, None))
+ },
+ selector: Selector => {
+ Self::Selector(selector)
}
}
diff --git a/library/src/meta/figure.rs b/library/src/meta/figure.rs
index fe93c5db..aae13738 100644
--- a/library/src/meta/figure.rs
+++ b/library/src/meta/figure.rs
@@ -1,12 +1,84 @@
use std::str::FromStr;
-use super::{Count, Counter, CounterUpdate, LocalName, Numbering, NumberingPattern};
+use ecow::eco_vec;
+
+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;
/// A figure with an optional caption.
///
+/// ## Content detection
+/// By default, the figure will attempt to automatically detect the content
+/// and use a priority list to detect which content is likely
+/// to be the most important. The priority list is as follows:
+/// - [image]($func/image) are the most important
+/// - [equations]($func/equation) are the second most important
+/// - [code]($func/raw) are the third most important
+/// - [table]($func/table) are the fourth most important.
+///
+/// There can be a variety of content within a figure and only the first element
+/// of the most important category will be used. For example, if a figure contains
+/// an image and a table, the image will be used. This behaviour can be overridden
+/// using the `kind` parameter. By setting it, you can force the figure to use a
+/// specific type of content. Note however that if the figure does not contain said
+/// element, or the `kind` is set to a string, you will need to manually specify
+/// the supplement to be able to make an outline or reference it.
+///
+/// ```example
+/// #figure(caption: [ Hello, world! ], kind: table)[
+/// #table(
+/// columns: (auto, 1fr),
+/// image("molecular.jpg", width: 32pt),
+/// [ A first picture ],
+/// image("molecular.jpg", width: 32pt),
+/// [ A second picture ],
+/// )
+/// ]
+/// ```
+///
+/// If you use an element that is not supported by the figure, and set it as its `content` parameter,
+/// to be able to make an outline or reference it, you will need to manually specify the supplement
+/// and counter. Otherwise the figure will produce an error.
+///
+/// ## Counting and supplement
+/// Based on the `kind` parameter or the detected content, the figure will chose
+/// the appropriate counter and supplement. These can be overridden by using the
+/// `kind` and `supplement` parameters respectively.
+///
+/// The overriding of these values is done as follows:
+/// ```example
+/// #figure(caption: [ Hello, world! ], kind: "hello", supplement: "Molecule")[
+/// #image("molecular.jpg", width: 32pt)
+/// ]
+/// ```
+///
+/// The default counters are defined as follows:
+/// - for (tables)[$func/table]: `counter(figure.where(kind: table))`
+/// - for (equations)[$func/equation]: `counter(figure.where(kind: math.equation))`
+/// - for (raw text)[$func/raw]: `counter(figure.where(kind: raw))`
+/// - for (images)[$func/image]: `counter(figure.where(kind: image))`
+/// - for a custom kind: `counter(figure.where(kind: kind))`
+///
+/// These are the counters you need to use if you want to change the
+/// counting behaviour of figures.
+///
+/// ## Numbering
+/// By default, the figure will be numbered using the `1` [numbering pattern]($func/numbering).
+/// This can be overridden by using the `numbering` parameter.
+///
+/// ## Outline
+/// By default, the figure will be outlined in the list of figures/tables/code. This can be disabled by
+/// setting the `outlined` parameter to `false`.
+///
+/// ## Global figure counter
+/// There is a global figure counter which can be accessed which counts all numbered figures in the document
+/// regardless of its type. This counter can be accessed using the `counter(figure)` function.
+///
/// ## Example
/// ```example
/// = Pipeline
@@ -23,7 +95,7 @@ use crate::text::TextElem;
///
/// Display: Figure
/// Category: meta
-#[element(Locatable, Synthesize, Count, Show, LocalName)]
+#[element(Locatable, Synthesize, Count, Show, Refable)]
pub struct FigureElem {
/// The content of the figure. Often, an [image]($func/image).
#[required]
@@ -32,41 +104,241 @@ pub struct FigureElem {
/// The figure's caption.
pub caption: Option<Content>,
+ /// The figure's supplement, if not provided, the figure will attempt to
+ /// automatically detect the counter from the content.
+ ///
+ /// ## Custom figure type
+ /// If you are using a custom figure type and would like to figure to be
+ /// referenced, you will need to manually specify the supplement, using either
+ /// a function or a string.
+ ///
+ /// ```example
+ /// #figure(caption: "My custom figure", kind: "foo", supplement: "Bar")[
+ /// #block[ The inside of my custom figure! ]
+ /// ]
+ /// ```
+ #[default(Smart::Auto)]
+ pub supplement: Smart<Option<Supplement>>,
+
+ /// Whether the figure should appear in the list of figures/tables/code.
+ /// Defaults to `true`.
+ #[default(true)]
+ pub outlined: bool,
+
/// 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 type of the figure. Setting this 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 a [code]($func/raw). Or if
+ /// you want to force the figure to use a specific type regardless of its content.
+ ///
+ /// You can set the kind to be an element, or a string. If you set it to be
+ /// a string or an element that is not supported by the figure, you will need to
+ /// manually specify the supplement if you wish to number the figure.
+ #[default(Smart::Auto)]
+ pub kind: Smart<ContentParam>,
+
/// The vertical gap between the body and caption.
#[default(Em::new(0.65).into())]
pub gap: Length,
+
+ /// Convenience field to get access to the figures counter, if any.
+ /// If the figure is not numbered, this will be `none`.
+ /// Otherwise it will be set to the counter being used by this figure.
+ #[synthesized]
+ #[internal]
+ pub counter: Option<Counter>,
+}
+
+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 determine_type(
+ &self,
+ styles: StyleChain,
+ require_supplement: bool,
+ ) -> Option<Content> {
+ let potential_elems = self.body().query(if require_supplement {
+ Selector::All(eco_vec![
+ Selector::can::<dyn Figurable>(),
+ Selector::can::<dyn LocalName>()
+ ])
+ } else {
+ Selector::can::<dyn Figurable>()
+ });
+
+ potential_elems.into_iter().max_by_key(|elem| {
+ elem.with::<dyn Figurable>()
+ .expect("should be figurable")
+ .priority(styles)
+ })
+ }
+
+ /// 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_elem(&self, func: ElemFunc) -> Option<Content> {
+ self.body().query(Selector::Elem(func, None)).first().cloned()
+ }
+
+ /// Builds the supplement and numbering of the figure.
+ /// If there is no numbering, returns [`None`].
+ ///
+ /// # Errors
+ /// If a numbering is specified but the [`Self::data()`] is `None`.
+ pub fn show_supplement_and_numbering(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ external_supp: Option<Content>,
+ ) -> SourceResult<Option<Content>> {
+ if let (Some(numbering), Some(supplement), Some(counter)) = (
+ self.numbering(styles),
+ self.supplement(styles)
+ .as_custom()
+ .and_then(|s| s.and_then(Supplement::as_content)),
+ self.counter(),
+ ) {
+ let mut name = external_supp.unwrap_or(supplement);
+
+ if !name.is_empty() {
+ name += TextElem::packed("\u{a0}");
+ }
+
+ let number = counter
+ .at(vt, self.0.location().expect("missing location"))?
+ .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.
+ ///
+ /// # Errors
+ /// If a numbering is specified but the [`Self::element`] is `None`.
+ pub fn show_caption(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ let Some(mut caption) = self.caption(styles) else {
+ return Ok(Content::empty());
+ };
+
+ if let Some(sup_and_num) = self.show_supplement_and_numbering(vt, styles, None)? {
+ caption = sup_and_num + TextElem::packed(": ") + caption;
+ }
+
+ Ok(caption)
+ }
}
impl Synthesize for FigureElem {
- fn synthesize(&mut self, styles: StyleChain) {
+ fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
self.push_numbering(self.numbering(styles));
+
+ // We get the numbering or `None`.
+ let numbering = self.numbering(styles);
+ let supplement = self.supplement(styles);
+
+ // We get the content or `None`.
+ let content = match self.kind(styles) {
+ Smart::Auto => match self.determine_type(styles, supplement.is_auto()) {
+ Some(ty) => Some(ty),
+ None => bail!(
+ self.span(),
+ "unable to determine figure type, use `kind` to manually specify it"
+ ),
+ },
+ Smart::Custom(ContentParam::Elem(ty)) => self.find_elem(ty),
+ Smart::Custom(ContentParam::Name(_)) => None,
+ };
+
+ if self.kind(styles).is_auto() {
+ if let Some(content) = &content {
+ self.push_kind(Smart::Custom(ContentParam::Elem(content.func())));
+ }
+ }
+
+ // The list of choices is the following:
+ // 1. If there is a detected content, we use the counter `counter(figure.where(kind: detected_content))`
+ // 2. If there is a name/elem, we use the counter `counter(figure.where(kind: name/elem))`
+ // 4. We return None.
+ let counter = content
+ .as_ref()
+ .map(Content::func)
+ .map(Value::from)
+ .or_else(|| self.kind(styles).as_custom().map(Value::from))
+ .map(|content| {
+ Counter::new(CounterKey::Selector(Selector::Elem(
+ Self::func(),
+ Some(dict! {
+ "kind" => content,
+ }),
+ )))
+ });
+
+ // We get the supplement or `None`.
+ // The supplement must either be set manually or the content identification
+ // must have succeeded.
+ let supplement = match supplement {
+ Smart::Auto => {
+ content.as_ref().and_then(|c| c.with::<dyn LocalName>()).map(|c| {
+ Supplement::Content(TextElem::packed(
+ c.local_name(TextElem::lang_in(styles)),
+ ))
+ })
+ }
+ Smart::Custom(supp) => supp,
+ };
+
+ // When the user wishes to number their figure, we check whether there is a
+ // counter and a supplement. If so, we push the element, which is just a
+ // summary of the caption properties. We also push all of the components
+ // of the summary for convenient access by the user for `show` rules.
+ if let Some(numbering) = numbering {
+ let Some(counter) = counter else {
+ bail!(self.span(), "numbering a figure requires that is has a kind");
+ };
+
+ let Some(supplement) = supplement else {
+ bail!(self.span(), "numbering a figure requires that is has a supplement");
+ };
+
+ let supplement = supplement
+ .resolve(vt, [content.unwrap_or_else(|| self.body()).into()])?;
+
+ self.push_supplement(Smart::Custom(Some(Supplement::Content(
+ supplement.clone(),
+ ))));
+ self.push_counter(Some(counter.clone()));
+ self.push_numbering(Some(numbering.clone()));
+ } else {
+ self.push_supplement(Smart::Custom(None));
+ self.push_counter(None);
+ self.push_numbering(None);
+ }
+
+ Ok(())
}
}
impl Show for FigureElem {
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ // We build the body of the figure.
let mut realized = self.body();
- if let Some(mut caption) = self.caption(styles) {
- if let Some(numbering) = self.numbering(styles) {
- let name = self.local_name(TextElem::lang_in(styles));
- caption = TextElem::packed(eco_format!("{name}\u{a0}"))
- + Counter::of(Self::func())
- .display(Some(numbering), false)
- .spanned(self.span())
- + TextElem::packed(": ")
- + caption;
- }
-
+ // We build the caption, if any.
+ if self.caption(styles).is_some() {
realized += VElem::weak(self.gap(styles).into()).pack();
- realized += caption;
+ realized += self.show_caption(vt, styles)?;
}
+ // We wrap the contents in a block.
Ok(BlockElem::new()
.with_body(Some(realized))
.with_breakable(false)
@@ -77,21 +349,76 @@ impl Show for FigureElem {
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 LocalName for FigureElem {
- fn local_name(&self, lang: Lang) -> &'static str {
- match lang {
- Lang::CHINESE => "图",
- Lang::GERMAN => "Abbildung",
- Lang::ITALIAN => "Figura",
- Lang::PORTUGUESE => "Figura",
- Lang::RUSSIAN => "Рисунок",
- Lang::ENGLISH | Lang::FRENCH | _ => "Figure",
+impl Refable for FigureElem {
+ fn reference(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ supplement: Option<Content>,
+ ) -> 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, styles, supplement)? else {
+ bail!(self.span(), "cannot reference unnumbered figure")
+ };
+
+ Ok(desc)
+ }
+
+ fn outline(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Option<Content>> {
+ // If the figure is not outlined, it is not referenced.
+ if !self.outlined(styles) {
+ return Ok(None);
}
+
+ self.show_caption(vt, styles).map(Some)
+ }
+
+ fn numbering(&self, styles: StyleChain) -> Option<Numbering> {
+ self.numbering(styles)
+ }
+
+ fn counter(&self, _styles: StyleChain) -> Counter {
+ self.counter().unwrap_or_else(|| Counter::of(Self::func()))
+ }
+}
+
+/// The `kind` parameter of [`FigureElem`].
+#[derive(Debug, Clone)]
+pub enum ContentParam {
+ /// The content is an element function.
+ Elem(ElemFunc),
+
+ /// The content is a name.
+ Name(EcoString),
+}
+
+cast_from_value! {
+ ContentParam,
+ v: ElemFunc => Self::Elem(v),
+ v: EcoString => Self::Name(v),
+}
+
+cast_to_value! {
+ v: ContentParam => match v {
+ ContentParam::Elem(v) => v.into(),
+ ContentParam::Name(v) => v.into(),
}
}
+
+/// An element that can be autodetected in a figure.
+/// This trait is used to determine the type of a figure, its counter, its numbering pattern
+/// and the supplement to use for referencing it and creating the caption.
+/// The element chosen as the figure's content is the one with the highest priority.
+pub trait Figurable {
+ /// The priority of this element.
+ fn priority(&self, styles: StyleChain) -> isize;
+}
diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs
index c9dd1f81..e4339dc8 100644
--- a/library/src/meta/heading.rs
+++ b/library/src/meta/heading.rs
@@ -1,10 +1,10 @@
use typst::font::FontWeight;
-use super::{Counter, CounterUpdate, LocalName, Numbering};
+use super::{Counter, CounterUpdate, LocalName, Numbering, Refable};
use crate::layout::{BlockElem, HElem, VElem};
-use crate::meta::Count;
+use crate::meta::{Count, Supplement};
use crate::prelude::*;
-use crate::text::{TextElem, TextSize};
+use crate::text::{SpaceElem, TextElem, TextSize};
/// A section heading.
///
@@ -41,7 +41,7 @@ use crate::text::{TextElem, TextSize};
///
/// Display: Heading
/// Category: meta
-#[element(Locatable, Synthesize, Count, Show, Finalize, LocalName)]
+#[element(Locatable, Synthesize, Count, Show, Finalize, LocalName, Refable)]
pub struct HeadingElem {
/// The logical nesting depth of the heading, starting from one.
#[default(NonZeroUsize::ONE)]
@@ -74,16 +74,35 @@ pub struct HeadingElem {
#[default(true)]
pub outlined: bool,
+ /// A supplement for the heading.
+ ///
+ /// For references to headings, this is added before the
+ /// referenced number.
+ ///
+ /// ```example
+ /// #set heading(numbering: "1.", supplement: "Chapter")
+ ///
+ /// = Introduction <intro>
+ /// In @intro, we see how to turn
+ /// Sections into Chapters. And
+ /// in @intro[Part], it is done
+ /// manually.
+ /// ```
+ #[default(Smart::Auto)]
+ pub supplement: Smart<Option<Supplement>>,
+
/// The heading's title.
#[required]
pub body: Content,
}
impl Synthesize for HeadingElem {
- fn synthesize(&mut self, styles: StyleChain) {
+ fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
self.push_level(self.level(styles));
self.push_numbering(self.numbering(styles));
self.push_outlined(self.outlined(styles));
+
+ Ok(())
}
}
@@ -137,6 +156,77 @@ cast_from_value! {
v: Content => v.to::<Self>().ok_or("expected heading")?.clone(),
}
+impl Refable for HeadingElem {
+ fn reference(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ supplement: Option<Content>,
+ ) -> SourceResult<Content> {
+ // first we create the supplement of the heading
+ let mut supplement = if let Some(supplement) = supplement {
+ supplement
+ } else {
+ match self.supplement(styles) {
+ Smart::Auto => {
+ TextElem::packed(self.local_name(TextElem::lang_in(styles)))
+ }
+ Smart::Custom(None) => Content::empty(),
+ Smart::Custom(Some(supplement)) => {
+ supplement.resolve(vt, std::iter::once(Value::from(self.clone())))?
+ }
+ }
+ };
+
+ // we append a space if the supplement is not empty
+ if !supplement.is_empty() {
+ supplement += TextElem::packed('\u{a0}')
+ };
+
+ // we check for a numbering
+ let Some(numbering) = self.numbering(styles) else {
+ bail!(self.span(), "only numbered headings can be referenced");
+ };
+
+ // we get the counter and display it
+ let numbers = Counter::of(Self::func())
+ .at(vt, self.0.location().expect("missing location"))?
+ .display(vt, &numbering.trimmed())?;
+
+ Ok(supplement + numbers)
+ }
+
+ fn level(&self, styles: StyleChain) -> usize {
+ self.level(styles).get()
+ }
+
+ fn numbering(&self, styles: StyleChain) -> Option<Numbering> {
+ self.numbering(styles)
+ }
+
+ fn counter(&self, _styles: StyleChain) -> Counter {
+ Counter::of(Self::func())
+ }
+
+ fn outline(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Option<Content>> {
+ // we check if the heading is outlined
+ if !self.outlined(styles) {
+ return Ok(None);
+ }
+
+ // We build the numbering followed by the title
+ let mut start = self.body();
+ if let Some(numbering) = self.numbering(StyleChain::default()) {
+ let numbers = Counter::of(HeadingElem::func())
+ .at(vt, self.0.location().expect("missing location"))?
+ .display(vt, &numbering)?;
+ start = numbers + SpaceElem::new().pack() + start;
+ };
+
+ Ok(Some(start))
+ }
+}
+
impl LocalName for HeadingElem {
fn local_name(&self, lang: Lang) -> &'static str {
match lang {
diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs
index 9438225f..f24df653 100644
--- a/library/src/meta/outline.rs
+++ b/library/src/meta/outline.rs
@@ -1,14 +1,18 @@
-use super::{Counter, CounterKey, HeadingElem, LocalName};
+use super::{Counter, CounterKey, HeadingElem, LocalName, Refable};
use crate::layout::{BoxElem, HElem, HideElem, ParbreakElem, RepeatElem};
use crate::prelude::*;
use crate::text::{LinebreakElem, SpaceElem, TextElem};
-/// A section outline / table of contents.
+/// A section outline / table of contents / table of figures / table of tables / etc.
///
/// This function generates a list of all headings in the document, up to a
/// given depth. The [heading]($func/heading) numbering will be reproduced
/// within the outline.
///
+/// Alternatively, by setting the `target` parameter, the outline can be used to
+/// generate a list of all figures, tables, code blocks, etc. When the `target` parameter
+/// is set, the `depth` parameter is ignored unless it is set to `heading`.
+///
/// ## Example
/// ```example
/// #outline()
@@ -20,6 +24,15 @@ use crate::text::{LinebreakElem, SpaceElem, TextElem};
/// #lorem(10)
/// ```
///
+/// ## Example: List of figures
+/// ```example
+/// #outline(target: figure.where(kind: image), title: "Table of Figures")
+///
+/// #figure(caption: "A nice figure!")[
+/// #image("/tiger.jpg")
+/// ]
+/// ```
+///
/// Display: Outline
/// Category: meta
#[element(Show, LocalName)]
@@ -37,6 +50,10 @@ pub struct OutlineElem {
/// this argument is `{none}`, all headings are included.
pub depth: Option<NonZeroUsize>,
+ /// The type of element to include in the outline.
+ #[default(Selector::Elem(HeadingElem::func(), Some(dict! { "outlined" => true })))]
+ pub target: Selector,
+
/// Whether to indent the subheadings to align the start of their numbering
/// with the title of their parents. This will only have an effect if a
/// [heading numbering]($func/heading.numbering) is set.
@@ -72,6 +89,7 @@ pub struct OutlineElem {
impl Show for OutlineElem {
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
let mut seq = vec![ParbreakElem::new().pack()];
+ // Build the outline title.
if let Some(title) = self.title(styles) {
let title = title.unwrap_or_else(|| {
TextElem::packed(self.local_name(TextElem::lang_in(styles)))
@@ -88,30 +106,36 @@ impl Show for OutlineElem {
}
let indent = self.indent(styles);
- let depth = self.depth(styles);
+ let depth = self.depth(styles).map_or(usize::MAX, NonZeroUsize::get);
- let mut ancestors: Vec<&HeadingElem> = vec![];
- let elems = vt.introspector.query(Selector::Elem(
- HeadingElem::func(),
- Some(dict! { "outlined" => true }),
- ));
+ let mut ancestors: Vec<&Content> = vec![];
+ let elems = vt.introspector.query(self.target(styles));
for elem in &elems {
- let heading = elem.to::<HeadingElem>().unwrap();
- let location = heading.0.location().unwrap();
- if !heading.outlined(StyleChain::default()) {
+ let Some(refable) = elem.with::<dyn Refable>() else {
+ bail!(elem.span(), "outlined elements must be referenceable");
+ };
+
+ let location = elem.location().expect("missing location");
+
+ if depth < refable.level(styles) {
continue;
}
- if let Some(depth) = depth {
- if depth < heading.level(StyleChain::default()) {
- continue;
- }
- }
+ let Some(outline) = refable.outline(vt, styles)? else {
+ continue;
+ };
- while ancestors.last().map_or(false, |last| {
- last.level(StyleChain::default()) >= heading.level(StyleChain::default())
- }) {
+ // Deals with the ancestors of the current element.
+ // This is only applicable for elements with a hierarchy/level.
+ while ancestors
+ .last()
+ .and_then(|ancestor| ancestor.with::<dyn Refable>())
+ .map_or(false, |last| {
+ last.level(StyleChain::default())
+ >= refable.level(StyleChain::default())
+ })
+ {
ancestors.pop();
}
@@ -119,10 +143,16 @@ impl Show for OutlineElem {
if indent {
let mut hidden = Content::empty();
for ancestor in &ancestors {
- if let Some(numbering) = ancestor.numbering(StyleChain::default()) {
- let numbers = Counter::of(HeadingElem::func())
- .at(vt, ancestor.0.location().unwrap())?
+ let ancestor_refable = ancestor.with::<dyn Refable>().unwrap();
+
+ if let Some(numbering) =
+ ancestor_refable.numbering(StyleChain::default())
+ {
+ let numbers = ancestor_refable
+ .counter(styles)
+ .at(vt, ancestor.location().unwrap())?
.display(vt, &numbering)?;
+
hidden += numbers + SpaceElem::new().pack();
};
}
@@ -133,17 +163,8 @@ impl Show for OutlineElem {
}
}
- // Format the numbering.
- let mut start = heading.body();
- if let Some(numbering) = heading.numbering(StyleChain::default()) {
- let numbers = Counter::of(HeadingElem::func())
- .at(vt, location)?
- .display(vt, &numbering)?;
- start = numbers + SpaceElem::new().pack() + start;
- };
-
- // Add the numbering and section name.
- seq.push(start.linked(Destination::Location(location)));
+ // Add the outline of the element.
+ seq.push(outline.linked(Destination::Location(location)));
// Add filler symbols between the section name and page number.
if let Some(filler) = self.fill(styles) {
@@ -167,7 +188,8 @@ impl Show for OutlineElem {
let end = TextElem::packed(eco_format!("{page}"));
seq.push(end.linked(Destination::Location(location)));
seq.push(LinebreakElem::new().pack());
- ancestors.push(heading);
+
+ ancestors.push(elem);
}
seq.push(ParbreakElem::new().pack());
diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs
index 24687845..81fd88b9 100644
--- a/library/src/meta/reference.rs
+++ b/library/src/meta/reference.rs
@@ -1,6 +1,5 @@
-use super::{BibliographyElem, CiteElem, Counter, LocalName, Numbering};
+use super::{BibliographyElem, CiteElem, Counter, Numbering};
use crate::prelude::*;
-use crate::text::TextElem;
/// A reference to a label or bibliography.
///
@@ -83,9 +82,10 @@ pub struct RefElem {
}
impl Synthesize for RefElem {
- fn synthesize(&mut self, styles: StyleChain) {
- let citation = self.to_citation(styles);
+ fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ let citation = self.to_citation(vt, styles)?;
self.push_citation(Some(citation));
+ Ok(())
}
}
@@ -103,55 +103,42 @@ impl Show for RefElem {
bail!(self.span(), "label occurs in the document and its bibliography");
}
- return Ok(self.to_citation(styles).pack());
+ return Ok(self.to_citation(vt, styles)?.pack());
}
let elem = elem.at(self.span())?;
- if !elem.can::<dyn Locatable>() {
+ if !elem.can::<dyn Refable>() {
bail!(self.span(), "cannot reference {}", elem.func().name());
}
- let supplement = self.supplement(styles);
- let mut supplement = match supplement {
- Smart::Auto => elem
- .with::<dyn LocalName>()
- .map(|elem| elem.local_name(TextElem::lang_in(styles)))
- .map(TextElem::packed)
- .unwrap_or_default(),
- Smart::Custom(None) => Content::empty(),
- Smart::Custom(Some(Supplement::Content(content))) => content,
- Smart::Custom(Some(Supplement::Func(func))) => {
- func.call_vt(vt, [elem.clone().into()])?.display()
+ let supplement = match self.supplement(styles) {
+ Smart::Auto | Smart::Custom(None) => None,
+ Smart::Custom(Some(supplement)) => {
+ Some(supplement.resolve(vt, [elem.clone().into()])?)
}
};
- if !supplement.is_empty() {
- supplement += TextElem::packed('\u{a0}');
- }
-
- let Some(numbering) = elem.cast_field::<Numbering>("numbering") else {
- bail!(self.span(), "only numbered elements can be referenced");
- };
+ let reference = elem
+ .with::<dyn Refable>()
+ .expect("element should be refable")
+ .reference(vt, styles, supplement)?;
- let numbers = Counter::of(elem.func())
- .at(vt, elem.location().unwrap())?
- .display(vt, &numbering.trimmed())?;
-
- Ok((supplement + numbers).linked(Destination::Location(elem.location().unwrap())))
+ Ok(reference.linked(Destination::Location(elem.location().unwrap())))
}
}
impl RefElem {
/// Turn the reference into a citation.
- pub fn to_citation(&self, styles: StyleChain) -> CiteElem {
+ pub fn to_citation(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<CiteElem> {
let mut elem = CiteElem::new(vec![self.target().0]);
elem.0.set_location(self.0.location().unwrap());
- elem.synthesize(styles);
+ elem.synthesize(vt, styles)?;
elem.push_supplement(match self.supplement(styles) {
Smart::Custom(Some(Supplement::Content(content))) => Some(content),
_ => None,
});
- elem
+
+ Ok(elem)
}
}
@@ -161,6 +148,29 @@ pub enum Supplement {
Func(Func),
}
+impl Supplement {
+ /// Tries to resolve the supplement into its content.
+ pub fn resolve(
+ &self,
+ vt: &mut Vt,
+ args: impl IntoIterator<Item = Value>,
+ ) -> SourceResult<Content> {
+ match self {
+ Supplement::Content(content) => Ok(content.clone()),
+ Supplement::Func(func) => func.call_vt(vt, args).map(|v| v.display()),
+ }
+ }
+
+ /// Tries to get the content of the supplement.
+ /// Returns `None` if the supplement is a function.
+ pub fn as_content(self) -> Option<Content> {
+ match self {
+ Supplement::Content(content) => Some(content),
+ _ => None,
+ }
+ }
+}
+
cast_from_value! {
Supplement,
v: Content => Self::Content(v),
@@ -173,3 +183,43 @@ cast_to_value! {
Supplement::Func(v) => v.into(),
}
}
+
+/// Marks an element as being able to be referenced.
+/// This is used to implement the `@ref` macro.
+/// It is expected to build the [`Content`] that gets linked
+/// by the [`RefElement`].
+pub trait Refable {
+ /// Tries to build a reference content for this element.
+ ///
+ /// # Arguments
+ /// - `vt` - The virtual typesetter.
+ /// - `styles` - The styles of the reference.
+ /// - `location` - The location where the reference is being created.
+ /// - `supplement` - The supplement of the reference.
+ fn reference(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ supplement: Option<Content>,
+ ) -> SourceResult<Content>;
+
+ /// Tries to build an outline element for this element.
+ /// If this returns `None`, the outline will not include this element.
+ /// By default this just calls [`Refable::reference`].
+ fn outline(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Option<Content>> {
+ self.reference(vt, styles, None).map(Some)
+ }
+
+ /// Returns the level of this element.
+ /// This is used to determine the level of the outline.
+ /// By default this returns `0`.
+ fn level(&self, _styles: StyleChain) -> usize {
+ 0
+ }
+
+ /// Returns the numbering of this element.
+ fn numbering(&self, styles: StyleChain) -> Option<Numbering>;
+
+ /// Returns the counter of this element.
+ fn counter(&self, styles: StyleChain) -> Counter;
+}
diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs
index 3c9f86e5..2324eb21 100644
--- a/library/src/text/raw.rs
+++ b/library/src/text/raw.rs
@@ -6,6 +6,7 @@ use super::{
FontFamily, FontList, Hyphenate, LinebreakElem, SmartQuoteElem, TextElem, TextSize,
};
use crate::layout::BlockElem;
+use crate::meta::{Figurable, LocalName};
use crate::prelude::*;
/// Raw text with optional syntax highlighting.
@@ -35,7 +36,7 @@ use crate::prelude::*;
///
/// Display: Raw Text / Code
/// Category: text
-#[element(Synthesize, Show, Finalize)]
+#[element(Synthesize, Show, Finalize, LocalName, Figurable)]
pub struct RawElem {
/// The raw text.
///
@@ -121,8 +122,10 @@ impl RawElem {
}
impl Synthesize for RawElem {
- fn synthesize(&mut self, styles: StyleChain) {
+ fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
self.push_lang(self.lang(styles));
+
+ Ok(())
}
}
@@ -197,6 +200,24 @@ impl Finalize for RawElem {
}
}
+impl LocalName for RawElem {
+ fn local_name(&self, lang: Lang) -> &'static str {
+ match lang {
+ Lang::CHINESE => "代码",
+ Lang::ITALIAN => "Codice",
+ Lang::RUSSIAN => "код",
+ Lang::FRENCH => "Liste",
+ Lang::ENGLISH | Lang::GERMAN | _ => "Listing",
+ }
+ }
+}
+
+impl Figurable for RawElem {
+ fn priority(&self, _styles: StyleChain) -> isize {
+ 500
+ }
+}
+
/// Highlight a syntax node in a theme by calling `f` with ranges and their
/// styles.
fn highlight_themed<F>(
diff --git a/library/src/visualize/image.rs b/library/src/visualize/image.rs
index 8a81a40e..3e4d1adb 100644
--- a/library/src/visualize/image.rs
+++ b/library/src/visualize/image.rs
@@ -3,7 +3,10 @@ use std::path::Path;
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
-use crate::prelude::*;
+use crate::{
+ meta::{Figurable, LocalName},
+ prelude::*,
+};
/// A raster or vector graphic.
///
@@ -22,7 +25,7 @@ use crate::prelude::*;
///
/// Display: Image
/// Category: visualize
-#[element(Layout)]
+#[element(Layout, LocalName, Figurable)]
pub struct ImageElem {
/// Path to an image file.
#[required]
@@ -112,6 +115,24 @@ impl Layout for ImageElem {
}
}
+impl LocalName for ImageElem {
+ fn local_name(&self, lang: Lang) -> &'static str {
+ match lang {
+ Lang::CHINESE => "图",
+ Lang::GERMAN => "Abbildung",
+ Lang::ITALIAN | Lang::PORTUGUESE => "Figura",
+ Lang::RUSSIAN => "Рисунок",
+ Lang::ENGLISH | Lang::FRENCH | _ => "Figure",
+ }
+ }
+}
+
+impl Figurable for ImageElem {
+ fn priority(&self, _styles: StyleChain) -> isize {
+ 1000
+ }
+}
+
/// How an image should adjust itself to a given area.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum ImageFit {