summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/src/layout/spacing.rs6
-rw-r--r--library/src/layout/table.rs12
-rw-r--r--library/src/lib.rs1
-rw-r--r--library/src/meta/figure.rs120
-rw-r--r--library/src/meta/heading.rs35
-rw-r--r--library/src/meta/mod.rs2
-rw-r--r--library/src/meta/outline.rs18
-rw-r--r--library/src/meta/reference.rs28
-rw-r--r--library/src/visualize/image.rs12
-rw-r--r--src/eval/func.rs27
-rw-r--r--src/model/content.rs13
-rw-r--r--tests/ref/meta/figure.pngbin0 -> 25146 bytes
-rw-r--r--tests/typ/meta/figure.typ24
13 files changed, 243 insertions, 55 deletions
diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs
index dbdf0c11..e67fec03 100644
--- a/library/src/layout/spacing.rs
+++ b/library/src/layout/spacing.rs
@@ -191,6 +191,12 @@ impl From<Em> for Spacing {
}
}
+impl From<Length> for Spacing {
+ fn from(length: Length) -> Self {
+ Self::Rel(length.into())
+ }
+}
+
impl From<Fr> 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 fabe8c33..012e63ac 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<T: Into<Value>> From<Celled<T>> 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 36afdb2b..35cae526 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 00000000..6c43fa68
--- /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.
+/// ],
+/// ) <fig-lab>
+/// ```
+///
+/// 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<Content>,
+
+ /// 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<Numbering>,
+
+ /// 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<NonZeroUsize>,
+}
+
+impl FigureNode {
+ fn element(&self) -> NodeId {
+ let mut id = self.body().id();
+ if id != NodeId::of::<TableNode>() {
+ id = NodeId::of::<Self>();
+ }
+ 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::<Self>())
+ .into_iter()
+ .take_while(|&(id, _)| id != my_id)
+ .filter(|(_, node)| node.to::<Self>().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<Content> {
+ 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::<TableNode>() {
+ return body.with::<dyn LocalName>().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 1bff3af4..e82f80c6 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<Vec<NonZeroUsize>>,
}
@@ -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::<HeadingNode>())
+ .locate(Selector::node::<Self>())
.into_iter()
.take_while(|&(id, _)| id != my_id)
{
- let heading = node.to::<HeadingNode>().unwrap();
+ let heading = node.to::<Self>().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::<Self>().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 07f449a4..f5b9bf2f 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 a2b12511..81785212 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 a46198bd..55f2fc6e 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::<dyn LocalName>()
+ .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::<FigureNode>() {
+ 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::<HeadingNode>() {
- 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 78f477f6..29a04c96 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 8da5c6bc..26854240 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<NodeId> {
+ 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<StyleMap> {
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<Dict>) -> StrResult<Selector> {
- 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<NodeId> 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 be737331..012ad05f 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 00000000..b39f462f
--- /dev/null
+++ b/tests/ref/meta/figure.png
Binary files differ
diff --git a/tests/typ/meta/figure.typ b/tests/typ/meta/figure.typ
new file mode 100644
index 00000000..567e0431
--- /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.],
+) <tab-basic>
+
+#figure(
+ pad(y: -11pt, image("/cylinder.svg", height: 3cm)),
+ caption: [The basic shapes.],
+ numbering: "I",
+) <fig-cylinder>
+
+#figure(
+ table(columns: 3)[a][b][c][d][e][f],
+ caption: [The complex table.],
+) <tab-complex>