1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
|
use std::str::FromStr;
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;
use crate::visualize::ImageElem;
/// A figure with an optional caption.
///
/// Automatically detects its contents to select the correct counting track.
/// For example, figures containing images will be numbered separately from
/// figures containing tables.
///
/// ## Examples
/// The example below shows a basic figure with an image:
/// ```example
/// @glacier shows a glacier. Glaciers
/// are complex systems.
///
/// #figure(
/// image("glacier.jpg", width: 80%),
/// caption: [A curious figure.],
/// ) <glacier>
/// ```
///
/// You can also insert [tables]($func/table) into figures to give them a
/// caption. The figure will detect this and automatically use a separate
/// counter.
///
/// ```example
/// #figure(
/// table(
/// columns: 4,
/// [t], [1], [2], [3],
/// [y], [0.3s], [0.4s], [0.8s],
/// ),
/// caption: [Timing results],
/// )
/// ```
///
/// This behaviour can be overridden by explicitly specifying the figure's
/// `kind`. All figures of the same kind share a common counter.
///
/// ## Modifying the appearance
/// You can completely customize the look of your figures with a [show
/// rule]($styling/#show-rules). In the example below, we show the figure's
/// caption above its body and display its supplement and counter after the
/// caption.
///
/// ```example
/// #show figure: it => align(center)[
/// #it.caption |
/// #emph[
/// #it.supplement
/// #it.counter.display(it.numbering)
/// ]
/// #v(10pt, weak: true)
/// #it.body
/// ]
///
/// #figure(
/// image("molecular.jpg", width: 80%),
/// caption: [
/// The molecular testing pipeline.
/// ],
/// )
/// ```
///
/// Display: Figure
/// Category: meta
#[element(Locatable, Synthesize, Count, Show, Refable)]
pub struct FigureElem {
/// The content of the figure. Often, an [image]($func/image).
#[required]
pub body: Content,
/// The figure's caption.
pub caption: Option<Content>,
/// The kind of the figure this is.
///
/// If set to `{auto}`, the figure will try to automatically determine its
/// kind. All figures of the same kind share a common counter.
///
/// Setting this to something other than `{auto}` 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 [code]($func/raw),
/// - you want to force the figure to use a counter regardless of its
/// content.
///
/// You can set the kind to be an element function or a string. If you set
/// it to an element function that is not supported by the figure, you will
/// need to manually specify the figure's supplement.
///
/// The figure's automatic detection is based on a priority list to select
/// the element that is likely to be the most important one. If the figure's
/// body contains multiple valid elements, the one with the highest priority
/// is selected. The priority list is as follows:
/// - [image]($func/image) is the most important,
/// - [code]($func/raw) is the second most important,
/// - [table]($func/table) is the least important one.
///
/// ```example
/// #figure(
/// circle(radius: 10pt),
/// caption: [A curious atom.],
/// kind: "atom",
/// supplement: [Atom],
/// )
/// ```
#[default(Smart::Auto)]
pub kind: Smart<FigureKind>,
/// The figure's supplement.
///
/// If set to `{auto}`, the figure will try to automatically determine the
/// correct supplement based on the `kind` and the active [text
/// language]($func/text.lang). If you are using a custom figure type, you
/// will need to manually specify the supplement.
///
/// This can also be set to a function that receives the figure's body to
/// select the supplement based on the figure's contents.
///
/// ```example
/// #figure(
/// [The contents of my figure!],
/// caption: [My custom figure],
/// supplement: [Bar],
/// kind: "foo",
/// )
/// ```
#[default(Smart::Auto)]
pub supplement: Smart<Supplement>,
/// How to number the figure. Accepts a
/// [numbering pattern or function]($func/numbering).
///
/// Defaults to `{"1"}`.
#[default(Some(NumberingPattern::from_str("1").unwrap().into()))]
pub numbering: Option<Numbering>,
/// The vertical gap between the body and caption.
#[default(Em::new(0.65).into())]
pub gap: Length,
/// Whether the figure should appear in an [`outline`]($func/outline)
/// of figures.
///
/// Defaults to `{true}`.
#[default(true)]
pub outlined: bool,
/// Convenience field to get access to the counter for this figure.
///
/// The counter only depends on the `kind`:
/// - For (tables)[$func/table]: `{counter(figure.where(kind: table))}`
/// - For (images)[$func/image]: `{counter(figure.where(kind: image))}`
/// - For a custom kind: `{counter(figure.where(kind: kind))}`
///
/// These are the counters you'll need to modify if you want to skip a
/// number or reset the counter.
#[synthesized]
pub counter: Option<Counter>,
}
impl Synthesize for FigureElem {
fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
// Determine the figure's kind.
let kind = match self.kind(styles) {
Smart::Auto => self
.find_figurable(styles)
.map(|elem| FigureKind::Elem(elem.func()))
.unwrap_or_else(|| FigureKind::Elem(ImageElem::func())),
Smart::Custom(kind) => kind,
};
let content = match &kind {
FigureKind::Elem(func) => self.find_of_elem(*func),
FigureKind::Name(_) => None,
}
.unwrap_or_else(|| self.body());
let numbering = self.numbering(styles);
// We get the supplement or `None`. The supplement must either be set
// manually or the content identification must have succeeded.
let supplement = match self.supplement(styles) {
Smart::Auto => match &kind {
FigureKind::Elem(func) => {
let elem = Content::new(*func).with::<dyn LocalName>().map(|c| {
TextElem::packed(c.local_name(
TextElem::lang_in(styles),
TextElem::region_in(styles),
))
});
if numbering.is_some() {
Some(elem
.ok_or("unable to determine the figure's `supplement`, please specify it manually")
.at(self.span())?)
} else {
elem
}
}
FigureKind::Name(_) => {
if numbering.is_some() {
bail!(self.span(), "please specify the figure's supplement")
} else {
None
}
}
},
Smart::Custom(supp) => Some(supp.resolve(vt, [content.into()])?),
};
// Construct the figure's counter.
let counter = Counter::new(CounterKey::Selector(Selector::Elem(
Self::func(),
Some(dict! {
"kind" => kind.clone(),
}),
)));
self.push_caption(self.caption(styles));
self.push_kind(Smart::Custom(kind));
self.push_supplement(Smart::Custom(Supplement::Content(
supplement.unwrap_or_default(),
)));
self.push_numbering(numbering);
self.push_outlined(self.outlined(styles));
self.push_counter(Some(counter));
Ok(())
}
}
impl Show for FigureElem {
#[tracing::instrument(name = "FigureElem::show", skip_all)]
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
// We build the body of the figure.
let mut realized = self.body();
// We build the caption, if any.
if self.caption(styles).is_some() {
realized += VElem::weak(self.gap(styles).into()).pack();
realized += self.show_caption(vt)?;
}
// We wrap the contents in a block.
Ok(BlockElem::new()
.with_body(Some(realized))
.with_breakable(false)
.pack()
.aligned(Axes::with_x(Some(Align::Center.into()))))
}
}
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 Refable for FigureElem {
fn reference(
&self,
vt: &mut Vt,
supplement: Option<Content>,
_: Lang,
_: Option<Region>,
) -> 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, supplement)? else {
bail!(self.span(), "cannot reference unnumbered figure")
};
Ok(desc)
}
fn outline(
&self,
vt: &mut Vt,
_: Lang,
_: Option<Region>,
) -> SourceResult<Option<Content>> {
// If the figure is not outlined, it is not referenced.
if !self.outlined(StyleChain::default()) {
return Ok(None);
}
self.show_caption(vt).map(Some)
}
fn numbering(&self) -> Option<Numbering> {
self.numbering(StyleChain::default())
}
fn counter(&self) -> Counter {
self.counter().unwrap_or_else(|| Counter::of(Self::func()))
}
}
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 find_figurable(&self, styles: StyleChain) -> Option<Content> {
self.body()
.query(Selector::can::<dyn Figurable>())
.into_iter()
.max_by_key(|elem| elem.with::<dyn Figurable>().unwrap().priority(styles))
.cloned()
}
/// 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_of_elem(&self, func: ElemFunc) -> Option<Content> {
self.body()
.query(Selector::Elem(func, None))
.into_iter()
.next()
.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,
external_supplement: Option<Content>,
) -> SourceResult<Option<Content>> {
if let (Some(numbering), Some(supplement), Some(counter)) = (
self.numbering(StyleChain::default()),
self.supplement(StyleChain::default())
.as_custom()
.and_then(|s| s.as_content()),
self.counter(),
) {
let mut name = external_supplement.unwrap_or(supplement);
if !name.is_empty() {
name += TextElem::packed("\u{a0}");
}
let number = counter
.at(vt, self.0.location().unwrap())?
.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) -> SourceResult<Content> {
let Some(mut caption) = self.caption(StyleChain::default()) else {
return Ok(Content::empty());
};
if let Some(sup_and_num) = self.show_supplement_and_numbering(vt, None)? {
caption = sup_and_num + TextElem::packed(": ") + caption;
}
Ok(caption)
}
}
/// The `kind` parameter of a [`FigureElem`].
#[derive(Debug, Clone)]
pub enum FigureKind {
/// The kind is an element function.
Elem(ElemFunc),
/// The kind is a name.
Name(EcoString),
}
cast_from_value! {
FigureKind,
v: ElemFunc => Self::Elem(v),
v: EcoString => Self::Name(v),
}
cast_to_value! {
v: FigureKind => match v {
FigureKind::Elem(v) => v.into(),
FigureKind::Name(v) => v.into(),
}
}
/// An element that can be auto-detected in a figure.
///
/// This trait is used to determine the type of a figure. The element chosen as
/// the figure's content is the figurable descendant with the highest priority.
pub trait Figurable: LocalName {
/// The priority of this element.
fn priority(&self, styles: StyleChain) -> isize;
}
|