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}