typst_library/model/
enum.rs

1use std::str::FromStr;
2
3use ecow::eco_format;
4use smallvec::SmallVec;
5
6use crate::diag::{bail, SourceResult};
7use crate::engine::Engine;
8use crate::foundations::{
9    cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
10    Styles, TargetElem,
11};
12use crate::html::{attr, tag, HtmlElem};
13use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
14use crate::model::{
15    ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem,
16};
17
18/// A numbered list.
19///
20/// Displays a sequence of items vertically and numbers them consecutively.
21///
22/// # Example
23/// ```example
24/// Automatically numbered:
25/// + Preparations
26/// + Analysis
27/// + Conclusions
28///
29/// Manually numbered:
30/// 2. What is the first step?
31/// 5. I am confused.
32/// +  Moving on ...
33///
34/// Multiple lines:
35/// + This enum item has multiple
36///   lines because the next line
37///   is indented.
38///
39/// Function call.
40/// #enum[First][Second]
41/// ```
42///
43/// You can easily switch all your enumerations to a different numbering style
44/// with a set rule.
45/// ```example
46/// #set enum(numbering: "a)")
47///
48/// + Starting off ...
49/// + Don't forget step two
50/// ```
51///
52/// You can also use [`enum.item`]($enum.item) to programmatically customize the
53/// number of each item in the enumeration:
54///
55/// ```example
56/// #enum(
57///   enum.item(1)[First step],
58///   enum.item(5)[Fifth step],
59///   enum.item(10)[Tenth step]
60/// )
61/// ```
62///
63/// # Syntax
64/// This functions also has dedicated syntax:
65///
66/// - Starting a line with a plus sign creates an automatically numbered
67///   enumeration item.
68/// - Starting a line with a number followed by a dot creates an explicitly
69///   numbered enumeration item.
70///
71/// Enumeration items can contain multiple paragraphs and other block-level
72/// content. All content that is indented more than an item's marker becomes
73/// part of that item.
74#[elem(scope, title = "Numbered List", Show)]
75pub struct EnumElem {
76    /// Defines the default [spacing]($enum.spacing) of the enumeration. If it
77    /// is `{false}`, the items are spaced apart with
78    /// [paragraph spacing]($par.spacing). If it is `{true}`, they use
79    /// [paragraph leading]($par.leading) instead. This makes the list more
80    /// compact, which can look better if the items are short.
81    ///
82    /// In markup mode, the value of this parameter is determined based on
83    /// whether items are separated with a blank line. If items directly follow
84    /// each other, this is set to `{true}`; if items are separated by a blank
85    /// line, this is set to `{false}`. The markup-defined tightness cannot be
86    /// overridden with set rules.
87    ///
88    /// ```example
89    /// + If an enum has a lot of text, and
90    ///   maybe other inline content, it
91    ///   should not be tight anymore.
92    ///
93    /// + To make an enum wide, simply
94    ///   insert a blank line between the
95    ///   items.
96    /// ```
97    #[default(true)]
98    pub tight: bool,
99
100    /// How to number the enumeration. Accepts a
101    /// [numbering pattern or function]($numbering).
102    ///
103    /// If the numbering pattern contains multiple counting symbols, they apply
104    /// to nested enums. If given a function, the function receives one argument
105    /// if `full` is `{false}` and multiple arguments if `full` is `{true}`.
106    ///
107    /// ```example
108    /// #set enum(numbering: "1.a)")
109    /// + Different
110    /// + Numbering
111    ///   + Nested
112    ///   + Items
113    /// + Style
114    ///
115    /// #set enum(numbering: n => super[#n])
116    /// + Superscript
117    /// + Numbering!
118    /// ```
119    #[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))]
120    #[borrowed]
121    pub numbering: Numbering,
122
123    /// Which number to start the enumeration with.
124    ///
125    /// ```example
126    /// #enum(
127    ///   start: 3,
128    ///   [Skipping],
129    ///   [Ahead],
130    /// )
131    /// ```
132    pub start: Smart<usize>,
133
134    /// Whether to display the full numbering, including the numbers of
135    /// all parent enumerations.
136    ///
137    ///
138    /// ```example
139    /// #set enum(numbering: "1.a)", full: true)
140    /// + Cook
141    ///   + Heat water
142    ///   + Add ingredients
143    /// + Eat
144    /// ```
145    #[default(false)]
146    pub full: bool,
147
148    /// Whether to reverse the numbering for this enumeration.
149    ///
150    /// ```example
151    /// #set enum(reversed: true)
152    /// + Coffee
153    /// + Tea
154    /// + Milk
155    /// ```
156    #[default(false)]
157    pub reversed: bool,
158
159    /// The indentation of each item.
160    #[resolve]
161    pub indent: Length,
162
163    /// The space between the numbering and the body of each item.
164    #[resolve]
165    #[default(Em::new(0.5).into())]
166    pub body_indent: Length,
167
168    /// The spacing between the items of the enumeration.
169    ///
170    /// If set to `{auto}`, uses paragraph [`leading`]($par.leading) for tight
171    /// enumerations and paragraph [`spacing`]($par.spacing) for wide
172    /// (non-tight) enumerations.
173    pub spacing: Smart<Length>,
174
175    /// The alignment that enum numbers should have.
176    ///
177    /// By default, this is set to `{end + top}`, which aligns enum numbers
178    /// towards end of the current text direction (in left-to-right script,
179    /// for example, this is the same as `{right}`) and at the top of the line.
180    /// The choice of `{end}` for horizontal alignment of enum numbers is
181    /// usually preferred over `{start}`, as numbers then grow away from the
182    /// text instead of towards it, avoiding certain visual issues. This option
183    /// lets you override this behaviour, however. (Also to note is that the
184    /// [unordered list]($list) uses a different method for this, by giving the
185    /// `marker` content an alignment directly.).
186    ///
187    /// ````example
188    /// #set enum(number-align: start + bottom)
189    ///
190    /// Here are some powers of two:
191    /// 1. One
192    /// 2. Two
193    /// 4. Four
194    /// 8. Eight
195    /// 16. Sixteen
196    /// 32. Thirty two
197    /// ````
198    #[default(HAlignment::End + VAlignment::Top)]
199    pub number_align: Alignment,
200
201    /// The numbered list's items.
202    ///
203    /// When using the enum syntax, adjacent items are automatically collected
204    /// into enumerations, even through constructs like for loops.
205    ///
206    /// ```example
207    /// #for phase in (
208    ///    "Launch",
209    ///    "Orbit",
210    ///    "Descent",
211    /// ) [+ #phase]
212    /// ```
213    #[variadic]
214    pub children: Vec<Packed<EnumItem>>,
215
216    /// The numbers of parent items.
217    #[internal]
218    #[fold]
219    #[ghost]
220    pub parents: SmallVec<[usize; 4]>,
221}
222
223#[scope]
224impl EnumElem {
225    #[elem]
226    type EnumItem;
227}
228
229impl Show for Packed<EnumElem> {
230    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
231        let tight = self.tight(styles);
232
233        if TargetElem::target_in(styles).is_html() {
234            let mut elem = HtmlElem::new(tag::ol);
235            if self.reversed(styles) {
236                elem = elem.with_attr(attr::reversed, "reversed");
237            }
238            if let Some(n) = self.start(styles).custom() {
239                elem = elem.with_attr(attr::start, eco_format!("{n}"));
240            }
241            let body = Content::sequence(self.children.iter().map(|item| {
242                let mut li = HtmlElem::new(tag::li);
243                if let Some(nr) = item.number(styles) {
244                    li = li.with_attr(attr::value, eco_format!("{nr}"));
245                }
246                // Text in wide enums shall always turn into paragraphs.
247                let mut body = item.body.clone();
248                if !tight {
249                    body += ParbreakElem::shared();
250                }
251                li.with_body(Some(body)).pack().spanned(item.span())
252            }));
253            return Ok(elem.with_body(Some(body)).pack().spanned(self.span()));
254        }
255
256        let mut realized =
257            BlockElem::multi_layouter(self.clone(), engine.routines.layout_enum)
258                .pack()
259                .spanned(self.span());
260
261        if tight {
262            let leading = ParElem::leading_in(styles);
263            let spacing =
264                VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
265            realized = spacing + realized;
266        }
267
268        Ok(realized)
269    }
270}
271
272/// An enumeration item.
273#[elem(name = "item", title = "Numbered List Item")]
274pub struct EnumItem {
275    /// The item's number.
276    #[positional]
277    pub number: Option<usize>,
278
279    /// The item's body.
280    #[required]
281    pub body: Content,
282}
283
284cast! {
285    EnumItem,
286    array: Array => {
287        let mut iter = array.into_iter();
288        let (number, body) = match (iter.next(), iter.next(), iter.next()) {
289            (Some(a), Some(b), None) => (a.cast()?, b.cast()?),
290            _ => bail!("array must contain exactly two entries"),
291        };
292        Self::new(body).with_number(number)
293    },
294    v: Content => v.unpack::<Self>().unwrap_or_else(Self::new),
295}
296
297impl ListLike for EnumElem {
298    type Item = EnumItem;
299
300    fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self {
301        Self::new(children).with_tight(tight)
302    }
303}
304
305impl ListItemLike for EnumItem {
306    fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> {
307        item.body.style_in_place(styles);
308        item
309    }
310}