mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Figures
This commit is contained in:
parent
529d3e10c6
commit
1a390deaea
@ -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)
|
||||
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
120
library/src/meta/figure.rs
Normal file
120
library/src/meta/figure.rs
Normal file
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,17 +164,16 @@ 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) => {
|
||||
let Some(id) = self.id() else {
|
||||
return Err("this function is not selectable".into());
|
||||
};
|
||||
|
||||
if id == item!(text_id) {
|
||||
Err("to select text, please use a string or regex instead")?;
|
||||
}
|
||||
|
||||
Ok(Selector::Node(id, fields))
|
||||
}
|
||||
_ => Err("this function is not selectable")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Func {
|
||||
@ -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.
|
||||
|
@ -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.
|
||||
|
BIN
tests/ref/meta/figure.png
Normal file
BIN
tests/ref/meta/figure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
24
tests/typ/meta/figure.typ
Normal file
24
tests/typ/meta/figure.typ
Normal file
@ -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>
|
Loading…
x
Reference in New Issue
Block a user