diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs index dbdf0c11e..e67fec03d 100644 --- a/library/src/layout/spacing.rs +++ b/library/src/layout/spacing.rs @@ -191,6 +191,12 @@ impl From for Spacing { } } +impl From for Spacing { + fn from(length: Length) -> Self { + Self::Rel(length.into()) + } +} + impl From for Spacing { fn from(fr: Fr) -> Self { Self::Fr(fr) diff --git a/library/src/layout/table.rs b/library/src/layout/table.rs index fabe8c332..012e63acc 100644 --- a/library/src/layout/table.rs +++ b/library/src/layout/table.rs @@ -1,4 +1,5 @@ use crate::layout::{AlignNode, GridLayouter, TrackSizings}; +use crate::meta::LocalName; use crate::prelude::*; /// A table of items. @@ -31,7 +32,7 @@ use crate::prelude::*; /// /// Display: Table /// Category: layout -#[node(Layout)] +#[node(Layout, LocalName)] pub struct TableNode { /// Defines the column sizes. See the [grid documentation]($func/grid) for /// more information on track sizing. @@ -264,3 +265,12 @@ impl> From> for Value { } } } + +impl LocalName for TableNode { + fn local_name(&self, lang: Lang) -> &'static str { + match lang { + Lang::GERMAN => "Tabelle", + Lang::ENGLISH | _ => "Table", + } + } +} diff --git a/library/src/lib.rs b/library/src/lib.rs index 36afdb2be..35cae5265 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -88,6 +88,7 @@ fn global(math: Module, calc: Module) -> Module { global.define("link", meta::LinkNode::id()); global.define("outline", meta::OutlineNode::id()); global.define("heading", meta::HeadingNode::id()); + global.define("figure", meta::FigureNode::id()); global.define("numbering", meta::numbering); // Symbols. diff --git a/library/src/meta/figure.rs b/library/src/meta/figure.rs new file mode 100644 index 000000000..6c43fa686 --- /dev/null +++ b/library/src/meta/figure.rs @@ -0,0 +1,120 @@ +use std::str::FromStr; + +use super::{LocalName, Numbering, NumberingPattern}; +use crate::layout::{BlockNode, TableNode, VNode}; +use crate::prelude::*; +use crate::text::TextNode; + +/// A figure with an optional caption. +/// +/// ## Example +/// ```example +/// = Pipeline +/// @fig-lab shows the central step of +/// our molecular testing pipeline. +/// +/// #figure( +/// image("molecular.jpg", width: 80%), +/// caption: [ +/// The molecular testing pipeline. +/// ], +/// ) +/// ``` +/// +/// Display: Figure +/// Category: meta +#[node(Synthesize, Show, LocalName)] +pub struct FigureNode { + /// The content of the figure. Often, an [image]($func/image). + #[required] + pub body: Content, + + /// The figure's caption. + pub caption: Option, + + /// How to number the figure. Accepts a + /// [numbering pattern or function]($func/numbering). + #[default(Some(Numbering::Pattern(NumberingPattern::from_str("1").unwrap())))] + pub numbering: Option, + + /// The vertical gap between the body and caption. + #[default(Em::new(0.65).into())] + pub gap: Length, + + /// The figure's number. + #[synthesized] + pub number: Option, +} + +impl FigureNode { + fn element(&self) -> NodeId { + let mut id = self.body().id(); + if id != NodeId::of::() { + id = NodeId::of::(); + } + id + } +} + +impl Synthesize for FigureNode { + fn synthesize(&self, vt: &mut Vt, styles: StyleChain) -> Content { + let my_id = vt.identify(self); + let element = self.element(); + + let numbering = self.numbering(styles); + let mut number = None; + if numbering.is_some() { + number = NonZeroUsize::new( + 1 + vt + .locate(Selector::node::()) + .into_iter() + .take_while(|&(id, _)| id != my_id) + .filter(|(_, node)| node.to::().unwrap().element() == element) + .count(), + ); + } + + let node = self.clone().with_number(number).with_numbering(numbering).pack(); + let meta = Meta::Node(my_id, node.clone()); + node.styled(MetaNode::set_data(vec![meta])) + } +} + +impl Show for FigureNode { + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { + let mut realized = self.body(); + + if let Some(mut caption) = self.caption(styles) { + if let Some(numbering) = self.numbering(styles) { + let number = self.number().unwrap(); + let name = self.local_name(TextNode::lang_in(styles)); + caption = TextNode::packed(eco_format!("{name}\u{a0}")) + + numbering.apply(vt.world(), &[number])?.display() + + TextNode::packed(": ") + + caption; + } + + realized += VNode::weak(self.gap(styles).into()).pack(); + realized += caption; + } + + Ok(BlockNode::new() + .with_body(Some(realized)) + .pack() + .aligned(Axes::with_x(Some(Align::Center.into())))) + } +} + +impl LocalName for FigureNode { + fn local_name(&self, lang: Lang) -> &'static str { + let body = self.body(); + if body.is::() { + return body.with::().unwrap().local_name(lang); + } + + match lang { + Lang::GERMAN => "Abbildung", + Lang::ENGLISH | _ => "Figure", + } + } +} diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index 1bff3af4e..e82f80c6c 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -1,6 +1,6 @@ use typst::font::FontWeight; -use super::Numbering; +use super::{LocalName, Numbering}; use crate::layout::{BlockNode, HNode, VNode}; use crate::prelude::*; use crate::text::{TextNode, TextSize}; @@ -40,7 +40,7 @@ use crate::text::{TextNode, TextSize}; /// /// Display: Heading /// Category: meta -#[node(Synthesize, Show, Finalize)] +#[node(Synthesize, Show, Finalize, LocalName)] pub struct HeadingNode { /// The logical nesting depth of the heading, starting from one. #[default(NonZeroUsize::new(1).unwrap())] @@ -78,14 +78,6 @@ pub struct HeadingNode { pub body: Content, /// The heading's numbering numbers. - /// - /// ```example - /// #show heading: it => it.numbers - /// - /// = First - /// == Second - /// = Third - /// ``` #[synthesized] pub numbers: Option>, } @@ -93,17 +85,17 @@ pub struct HeadingNode { impl Synthesize for HeadingNode { fn synthesize(&self, vt: &mut Vt, styles: StyleChain) -> Content { let my_id = vt.identify(self); - let numbered = self.numbering(styles).is_some(); + let numbering = self.numbering(styles); let mut counter = HeadingCounter::new(); - if numbered { - // Advance passed existing headings. + if numbering.is_some() { + // Advance past existing headings. for (_, node) in vt - .locate(Selector::node::()) + .locate(Selector::node::()) .into_iter() .take_while(|&(id, _)| id != my_id) { - let heading = node.to::().unwrap(); + let heading = node.to::().unwrap(); if heading.numbering(StyleChain::default()).is_some() { counter.advance(heading); } @@ -116,8 +108,8 @@ impl Synthesize for HeadingNode { let node = self .clone() .with_outlined(self.outlined(styles)) - .with_numbering(self.numbering(styles)) - .with_numbers(numbered.then(|| counter.take())) + .with_numbers(numbering.is_some().then(|| counter.take())) + .with_numbering(numbering) .pack(); let meta = Meta::Node(my_id, node.clone()); @@ -196,3 +188,12 @@ cast_from_value! { HeadingNode, v: Content => v.to::().ok_or("expected heading")?.clone(), } + +impl LocalName for HeadingNode { + fn local_name(&self, lang: Lang) -> &'static str { + match lang { + Lang::GERMAN => "Abschnitt", + Lang::ENGLISH | _ => "Section", + } + } +} diff --git a/library/src/meta/mod.rs b/library/src/meta/mod.rs index 07f449a42..f5b9bf2f0 100644 --- a/library/src/meta/mod.rs +++ b/library/src/meta/mod.rs @@ -1,6 +1,7 @@ //! Interaction between document parts. mod document; +mod figure; mod heading; mod link; mod numbering; @@ -8,6 +9,7 @@ mod outline; mod reference; pub use self::document::*; +pub use self::figure::*; pub use self::heading::*; pub use self::link::*; pub use self::numbering::*; diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index a2b125110..81785212c 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -1,4 +1,4 @@ -use super::HeadingNode; +use super::{HeadingNode, LocalName}; use crate::layout::{BoxNode, HNode, HideNode, ParbreakNode, RepeatNode}; use crate::prelude::*; use crate::text::{LinebreakNode, SpaceNode, TextNode}; @@ -22,7 +22,7 @@ use crate::text::{LinebreakNode, SpaceNode, TextNode}; /// /// Display: Outline /// Category: meta -#[node(Synthesize, Show)] +#[node(Synthesize, Show, LocalName)] pub struct OutlineNode { /// The title of the outline. /// @@ -91,10 +91,7 @@ impl Show for OutlineNode { let mut seq = vec![ParbreakNode::new().pack()]; if let Some(title) = self.title(styles) { let title = title.clone().unwrap_or_else(|| { - TextNode::packed(match TextNode::lang_in(styles) { - Lang::GERMAN => "Inhaltsverzeichnis", - Lang::ENGLISH | _ => "Contents", - }) + TextNode::packed(self.local_name(TextNode::lang_in(styles))) }); seq.push( @@ -187,3 +184,12 @@ impl Show for OutlineNode { Ok(Content::sequence(seq)) } } + +impl LocalName for OutlineNode { + fn local_name(&self, lang: Lang) -> &'static str { + match lang { + Lang::GERMAN => "Inhaltsverzeichnis", + Lang::ENGLISH | _ => "Contents", + } + } +} diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index a46198bde..55f2fc6e4 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -1,4 +1,4 @@ -use super::{HeadingNode, Numbering}; +use super::{FigureNode, HeadingNode, Numbering}; use crate::prelude::*; use crate::text::TextNode; @@ -92,7 +92,9 @@ impl Show for RefNode { }; let mut prefix = match self.prefix(styles) { - Smart::Auto => prefix(target, TextNode::lang_in(styles)) + Smart::Auto => target + .with::() + .map(|node| node.local_name(TextNode::lang_in(styles))) .map(TextNode::packed) .unwrap_or_default(), Smart::Custom(None) => Content::empty(), @@ -113,6 +115,13 @@ impl Show for RefNode { } else { bail!(self.span(), "cannot reference unnumbered heading"); } + } else if let Some(figure) = target.to::() { + if let Some(numbering) = figure.numbering(StyleChain::default()) { + let number = figure.number().unwrap(); + numbered(vt, prefix, &numbering, &[number])? + } else { + bail!(self.span(), "cannot reference unnumbered figure"); + } } else { bail!(self.span(), "cannot reference {}", target.id().name); }; @@ -138,15 +147,8 @@ fn numbered( }) } -/// The default prefix. -fn prefix(node: &Content, lang: Lang) -> Option<&str> { - if node.is::() { - match lang { - Lang::ENGLISH => Some("Section"), - Lang::GERMAN => Some("Abschnitt"), - _ => None, - } - } else { - None - } +/// The named with which an element is referenced. +pub trait LocalName { + /// Get the name in the given language. + fn local_name(&self, lang: Lang) -> &'static str; } diff --git a/library/src/visualize/image.rs b/library/src/visualize/image.rs index 78f477f69..29a04c96b 100644 --- a/library/src/visualize/image.rs +++ b/library/src/visualize/image.rs @@ -11,11 +11,13 @@ use crate::prelude::*; /// /// ## Example /// ```example -/// #align(center)[ -/// #image("molecular.jpg", width: 80%) -/// *A step in the molecular testing -/// pipeline of our lab* -/// ] +/// #figure( +/// image("molecular.jpg", width: 80%), +/// caption: [ +/// A step in the molecular testing +/// pipeline of our lab. +/// ], +/// ) /// ``` /// /// Display: Image diff --git a/src/eval/func.rs b/src/eval/func.rs index 8da5c6bc1..268542402 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -142,6 +142,14 @@ impl Func { self.select(Some(fields)) } + /// The node id of this function if it is an element function. + pub fn id(&self) -> Option { + match **self.0 { + Repr::Node(id) => Some(id), + _ => None, + } + } + /// Execute the function's set rule and return the resulting style map. pub fn set(&self, mut args: Args) -> SourceResult { Ok(match &**self.0 { @@ -156,16 +164,15 @@ impl Func { /// Create a selector for this function's node type. pub fn select(&self, fields: Option) -> StrResult { - match **self.0 { - Repr::Node(id) => { - if id == item!(text_id) { - Err("to select text, please use a string or regex instead")?; - } + let Some(id) = self.id() else { + return Err("this function is not selectable".into()); + }; - Ok(Selector::Node(id, fields)) - } - _ => Err("this function is not selectable")?, + if id == item!(text_id) { + Err("to select text, please use a string or regex instead")?; } + + Ok(Selector::Node(id, fields)) } } @@ -196,10 +203,6 @@ impl From for Func { } } -cast_to_value! { - v: NodeId => Value::Func(v.into()) -} - /// A native Rust function. pub struct NativeFunc { /// The function's implementation. diff --git a/src/model/content.rs b/src/model/content.rs index be7373314..012ad05f9 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -10,7 +10,9 @@ use once_cell::sync::Lazy; use super::{node, Guard, Recipe, Style, StyleMap}; use crate::diag::{SourceResult, StrResult}; -use crate::eval::{cast_from_value, Args, Cast, FuncInfo, Str, Value, Vm}; +use crate::eval::{ + cast_from_value, cast_to_value, Args, Cast, Func, FuncInfo, Str, Value, Vm, +}; use crate::syntax::Span; use crate::util::pretty_array_like; use crate::World; @@ -382,6 +384,15 @@ impl Deref for NodeId { } } +cast_from_value! { + NodeId, + v: Func => v.id().ok_or("this function is not an element")? +} + +cast_to_value! { + v: NodeId => Value::Func(v.into()) +} + /// Static node for a node. pub struct NodeMeta { /// The node's name. diff --git a/tests/ref/meta/figure.png b/tests/ref/meta/figure.png new file mode 100644 index 000000000..b39f462fa Binary files /dev/null and b/tests/ref/meta/figure.png differ diff --git a/tests/typ/meta/figure.typ b/tests/typ/meta/figure.typ new file mode 100644 index 000000000..567e04310 --- /dev/null +++ b/tests/typ/meta/figure.typ @@ -0,0 +1,24 @@ +// Test figures. + +--- +#set page(width: 150pt) +#set figure(numbering: "I") + +We can clearly see that @fig-cylinder and +@tab-complex are relevant in this context. + +#figure( + table(columns: 2)[a][b], + caption: [The basic table.], +) + +#figure( + pad(y: -11pt, image("/cylinder.svg", height: 3cm)), + caption: [The basic shapes.], + numbering: "I", +) + +#figure( + table(columns: 3)[a][b][c][d][e][f], + caption: [The complex table.], +)