diff --git a/library/src/layout/table.rs b/library/src/layout/table.rs index 4ba8a56e6..c9a67a1b2 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 82ad0a2f3..b07fc78f9 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, + ) -> SourceResult { + // 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 { + 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 635f1d8c0..7bc2ff7bd 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 e11e049db..3868040e2 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 fe93c5db6..aae13738e 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, + /// 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>, + + /// 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, + /// 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, + /// 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, +} + +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 { + let potential_elems = self.body().query(if require_supplement { + Selector::All(eco_vec![ + Selector::can::(), + Selector::can::() + ]) + } else { + Selector::can::() + }); + + potential_elems.into_iter().max_by_key(|elem| { + elem.with::() + .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 { + 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, + ) -> SourceResult> { + 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 { + 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::()).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 { + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { + // 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 { + // 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, + ) -> SourceResult { + // 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> { + // 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 { + 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 c9dd1f81f..e4339dc8d 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 + /// In @intro, we see how to turn + /// Sections into Chapters. And + /// in @intro[Part], it is done + /// manually. + /// ``` + #[default(Smart::Auto)] + pub supplement: Smart>, + /// 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::().ok_or("expected heading")?.clone(), } +impl Refable for HeadingElem { + fn reference( + &self, + vt: &mut Vt, + styles: StyleChain, + supplement: Option, + ) -> SourceResult { + // 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 { + self.numbering(styles) + } + + fn counter(&self, _styles: StyleChain) -> Counter { + Counter::of(Self::func()) + } + + fn outline(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult> { + // 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 9438225f1..f24df6539 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, + /// 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 { 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::().unwrap(); - let location = heading.0.location().unwrap(); - if !heading.outlined(StyleChain::default()) { + let Some(refable) = elem.with::() 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::()) + .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::().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 24687845a..81fd88b9a 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::() { + if !elem.can::() { bail!(self.span(), "cannot reference {}", elem.func().name()); } - let supplement = self.supplement(styles); - let mut supplement = match supplement { - Smart::Auto => elem - .with::() - .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 reference = elem + .with::() + .expect("element should be refable") + .reference(vt, styles, supplement)?; - let Some(numbering) = elem.cast_field::("numbering") else { - bail!(self.span(), "only numbered elements can be referenced"); - }; - - 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 { 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, + ) -> SourceResult { + 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 { + 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, + ) -> SourceResult; + + /// 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> { + 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; + + /// 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 3c9f86e5f..2324eb212 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( diff --git a/library/src/visualize/image.rs b/library/src/visualize/image.rs index 8a81a40ea..3e4d1adb1 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 { diff --git a/src/geom/smart.rs b/src/geom/smart.rs index c977d6519..d9f8fd16e 100644 --- a/src/geom/smart.rs +++ b/src/geom/smart.rs @@ -20,6 +20,15 @@ impl Smart { matches!(self, Self::Custom(_)) } + /// Returns a reference the contained custom value. + /// If the value is [`Smart::Auto`], `None` is returned. + pub fn as_custom(self) -> Option { + match self { + Self::Auto => None, + Self::Custom(x) => Some(x), + } + } + /// Map the contained custom value with `f`. pub fn map(self, f: F) -> Smart where diff --git a/src/model/content.rs b/src/model/content.rs index f95ce1042..db8677157 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -7,7 +7,7 @@ use ecow::{eco_format, EcoString, EcoVec}; use super::{ element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Label, Locatable, - Location, Recipe, Style, Styles, Synthesize, + Location, Recipe, Selector, Style, Styles, Synthesize, }; use crate::diag::{SourceResult, StrResult}; use crate::doc::Meta; @@ -105,6 +105,12 @@ impl Content { (self.func.0.vtable)(TypeId::of::()).is_some() } + /// Whether the contained element has the given capability. + /// Where the capability is given by a `TypeId`. + pub fn can_type_id(&self, type_id: TypeId) -> bool { + (self.func.0.vtable)(type_id).is_some() + } + /// Cast to a trait object if the contained element has the given /// capability. pub fn with(&self) -> Option<&C> @@ -347,6 +353,62 @@ impl Content { pub fn set_location(&mut self, location: Location) { self.attrs.push(Attr::Location(location)); } + + /// Gives an iterator over the children of this content + pub fn children(&self) -> impl Iterator { + self.attrs.iter().filter_map(Attr::child) + } + + /// Gives an iterator over the children of this content that are contained + /// within the arguments of the content. + pub fn children_in_args(&self) -> impl Iterator { + self.attrs + .iter() + .filter_map(Attr::value) + .filter_map(|value| match value { + Value::Content(content) => Some(content), + _ => None, + }) + } + + /// Queries the content tree for all elements that match the given selector. + /// + /// # Show rules + /// Elements produced in `show` rules will not be included in the results. + pub fn query(&self, selector: Selector) -> Vec { + let mut results = Vec::new(); + self.query_into(&selector, &mut results); + results + } + + /// Queries the content tree for all elements that match the given selector + /// and stores the results inside of the `results` vec. + fn query_into(&self, selector: &Selector, results: &mut Vec) { + if selector.matches(self) { + results.push(self.clone()); + } + + for attr in &self.attrs { + match attr { + Attr::Child(child) => child.query_into(selector, results), + Attr::Value(value) => walk_value(&value, selector, results), + _ => {} + } + } + + /// Walks a given value to find any content that matches the selector. + fn walk_value(value: &Value, selector: &Selector, results: &mut Vec) { + match value { + Value::Content(content) => content.query_into(selector, results), + Value::Array(array) => { + for value in array { + walk_value(value, selector, results); + } + } + _ => {} + } + } + } } impl Debug for Content { diff --git a/src/model/element.rs b/src/model/element.rs index c6738582a..4c825a202 100644 --- a/src/model/element.rs +++ b/src/model/element.rs @@ -63,6 +63,14 @@ impl ElemFunc { (self.0.construct)(vm, args) } + /// Whether the contained element has the given capability. + pub fn can(&self) -> bool + where + C: ?Sized + 'static, + { + (self.0.vtable)(TypeId::of::()).is_some() + } + /// Create a selector for elements of this function. pub fn select(self) -> Selector { Selector::Elem(self, None) diff --git a/src/model/realize.rs b/src/model/realize.rs index e96e0dc16..48a0fbdce 100644 --- a/src/model/realize.rs +++ b/src/model/realize.rs @@ -42,7 +42,7 @@ pub fn realize( } if let Some(elem) = elem.with_mut::() { - elem.synthesize(styles); + elem.synthesize(vt, styles)?; } elem.mark_prepared(); @@ -152,7 +152,7 @@ fn try_apply( } // Not supported here. - Some(Selector::Any(_)) => Ok(None), + Some(Selector::Any(_) | Selector::All(_) | Selector::Can(_)) => Ok(None), None => Ok(None), } @@ -165,7 +165,7 @@ pub trait Locatable {} /// rule. pub trait Synthesize { /// Prepare the element for show rule application. - fn synthesize(&mut self, styles: StyleChain); + fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()>; } /// The base recipe for an element. diff --git a/src/model/styles.rs b/src/model/styles.rs index 82ec2ed5d..4b309ed04 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -1,3 +1,4 @@ +use std::any::{Any, TypeId}; use std::fmt::{self, Debug, Formatter, Write}; use std::iter; use std::mem; @@ -260,8 +261,12 @@ pub enum Selector { Label(Label), /// Matches text elements through a regular expression. Regex(Regex), + /// Matches elements with a specific capability. + Can(TypeId), /// Matches if any of the subselectors match. Any(EcoVec), + /// Matches if all of the subselectors match. + All(EcoVec), } impl Selector { @@ -270,6 +275,11 @@ impl Selector { Self::Regex(Regex::new(®ex::escape(text)).unwrap()) } + /// Define a simple [`Selector::Can`] selector. + pub fn can() -> Self { + Self::Can(TypeId::of::()) + } + /// Whether the selector matches for the target. pub fn matches(&self, target: &Content) -> bool { match self { @@ -285,7 +295,9 @@ impl Selector { target.func() == item!(text_func) && item!(text_str)(target).map_or(false, |text| regex.is_match(&text)) } + Self::Can(cap) => target.can_type_id(*cap), Self::Any(selectors) => selectors.iter().any(|sel| sel.matches(target)), + Self::All(selectors) => selectors.iter().all(|sel| sel.matches(target)), } } } @@ -303,8 +315,9 @@ impl Debug for Selector { } Self::Label(label) => label.fmt(f), Self::Regex(regex) => regex.fmt(f), - Self::Any(selectors) => { - f.write_str("any")?; + Self::Can(cap) => cap.fmt(f), + Self::Any(selectors) | Self::All(selectors) => { + f.write_str(if matches!(self, Self::Any(_)) { "any" } else { "all" })?; let pieces: Vec<_> = selectors.iter().map(|sel| eco_format!("{sel:?}")).collect(); f.write_str(&pretty_array_like(&pieces, false)) diff --git a/tests/ref/meta/figure.png b/tests/ref/meta/figure.png index c1b518d3b..a86b4d6d6 100644 Binary files a/tests/ref/meta/figure.png and b/tests/ref/meta/figure.png differ diff --git a/tests/typ/meta/counter.typ b/tests/typ/meta/counter.typ index 32d16cae4..6b5797de0 100644 --- a/tests/typ/meta/counter.typ +++ b/tests/typ/meta/counter.typ @@ -45,8 +45,8 @@ At Beta, it was #locate(loc => { --- // Count figures. -#figure(numbering: "A", caption: [Four 'A's])[_AAAA!_] -#figure(numbering: none, caption: [Four 'B's])[_BBBB!_] -#figure(caption: [Four 'C's])[_CCCC!_] -#counter(figure).update(n => n + 3) -#figure(caption: [Four 'D's])[_DDDD!_] +#figure(numbering: "A", caption: [Four 'A's], kind: image, supplement: "Figure")[_AAAA!_] +#figure(numbering: none, caption: [Four 'B's], kind: image, supplement: "Figure")[_BBBB!_] +#figure(caption: [Four 'C's], kind: image, supplement: "Figure")[_CCCC!_] +#counter(figure.where(kind: image)).update(n => n + 3) +#figure(caption: [Four 'D's], kind: image, supplement: "Figure")[_DDDD!_] diff --git a/tests/typ/meta/figure.typ b/tests/typ/meta/figure.typ index 567e04310..3537bbe14 100644 --- a/tests/typ/meta/figure.typ +++ b/tests/typ/meta/figure.typ @@ -22,3 +22,83 @@ We can clearly see that @fig-cylinder and table(columns: 3)[a][b][c][d][e][f], caption: [The complex table.], ) + +--- + +// Testing figures with and without caption +#figure( + table( + columns: 2, + [First cylinder], + image("/cylinder.svg", height: 3cm), + ) +) + +#figure( + table( + columns: 2, + [Second cylinder], + image("/cylinder.svg", height: 3cm), + ), + caption: "A table containing images." +) + +--- + +// Testing show rules with figures with a simple theorem display +#show figure.where(kind: "theorem"): it => { + let name = none + if not it.caption == none { + name = [ #emph(it.caption)] + } else { + name = [] + } + + let title = none + if not it.numbering == none { + title = it.supplement + if not it.numbering == none { + title += " " + it.counter.display(it.numbering) + } + } + title = strong(title) + pad( + top: 0em, bottom: 0em, + block( + fill: green.lighten(90%), + stroke: 1pt + green, + inset: 10pt, + width: 100%, + radius: 5pt, + breakable: false, + [#title#name#h(0.1em):#h(0.2em)#it.body#v(0.5em)] + ) + ) +} + +#figure( + $a^2 + b^2 = c^2$, + supplement: "Theorem", + kind: "theorem", + caption: "Pythagoras' theorem.", + numbering: "1", +) + +#figure( + $a^2 + b^2 = c^2$, + supplement: "Theorem", + kind: "theorem", + caption: "Another Pythagoras' theorem.", + numbering: none, +) + +#figure( + caption: [Hello world in #emph[rust].], +)[ + #show raw: set align(left) + ```rust + fn main() { + println!("Hello, world!"); + } + ``` +] \ No newline at end of file diff --git a/tests/typ/meta/query.typ b/tests/typ/meta/query.typ index 8f8dcd3d7..7dfbbcfc4 100644 --- a/tests/typ/meta/query.typ +++ b/tests/typ/meta/query.typ @@ -61,6 +61,8 @@ #figure( rect[Just some stand-in text], + kind: image, + supplement: "Figure", caption: [Stand-in text], )