typst_library/model/list.rs
1use comemo::Track;
2
3use crate::diag::{SourceResult, bail};
4use crate::engine::Engine;
5use crate::foundations::{
6 Array, Content, Context, Depth, Func, NativeElement, Packed, Smart, StyleChain,
7 Styles, Value, cast, elem, scope,
8};
9use crate::introspection::{Locatable, Tagged};
10use crate::layout::{Em, Length};
11use crate::text::TextElem;
12
13/// A bullet list.
14///
15/// Displays a sequence of items vertically, with each item introduced by a
16/// marker.
17///
18/// # Example
19/// ```example
20/// Normal list.
21/// - Text
22/// - Math
23/// - Layout
24/// - ...
25///
26/// Multiple lines.
27/// - This list item spans multiple
28/// lines because it is indented.
29///
30/// Function call.
31/// #list(
32/// [Foundations],
33/// [Calculate],
34/// [Construct],
35/// [Data Loading],
36/// )
37/// ```
38///
39/// # Syntax
40/// This functions also has dedicated syntax: Start a line with a hyphen,
41/// followed by a space to create a list item. A list item can contain multiple
42/// paragraphs and other block-level content. All content that is indented
43/// more than an item's marker becomes part of that item.
44#[elem(scope, title = "Bullet List", Locatable, Tagged)]
45pub struct ListElem {
46 /// Defines the default [spacing]($list.spacing) of the list. If it is
47 /// `{false}`, the items are spaced apart with
48 /// [paragraph spacing]($par.spacing). If it is `{true}`, they use
49 /// [paragraph leading]($par.leading) instead. This makes the list more
50 /// compact, which can look better if the items are short.
51 ///
52 /// In markup mode, the value of this parameter is determined based on
53 /// whether items are separated with a blank line. If items directly follow
54 /// each other, this is set to `{true}`; if items are separated by a blank
55 /// line, this is set to `{false}`. The markup-defined tightness cannot be
56 /// overridden with set rules.
57 ///
58 /// ```example
59 /// - If a list has a lot of text, and
60 /// maybe other inline content, it
61 /// should not be tight anymore.
62 ///
63 /// - To make a list wide, simply insert
64 /// a blank line between the items.
65 /// ```
66 #[default(true)]
67 pub tight: bool,
68
69 /// The marker which introduces each item.
70 ///
71 /// Instead of plain content, you can also pass an array with multiple
72 /// markers that should be used for nested lists. If the list nesting depth
73 /// exceeds the number of markers, the markers are cycled. For total
74 /// control, you may pass a function that maps the list's nesting depth
75 /// (starting from `{0}`) to a desired marker.
76 ///
77 /// ```example
78 /// #set list(marker: [--])
79 /// - A more classic list
80 /// - With en-dashes
81 ///
82 /// #set list(marker: ([•], [--]))
83 /// - Top-level
84 /// - Nested
85 /// - Items
86 /// - Items
87 /// ```
88 #[default(ListMarker::Content(vec![
89 // These are all available in the default font, vertically centered, and
90 // roughly of the same size (with the last one having slightly lower
91 // weight because it is not filled).
92 TextElem::packed('\u{2022}'), // Bullet
93 TextElem::packed('\u{2023}'), // Triangular Bullet
94 TextElem::packed('\u{2013}'), // En-dash
95 ]))]
96 pub marker: ListMarker,
97
98 /// The indent of each item.
99 pub indent: Length,
100
101 /// The spacing between the marker and the body of each item.
102 #[default(Em::new(0.5).into())]
103 pub body_indent: Length,
104
105 /// The spacing between the items of the list.
106 ///
107 /// If set to `{auto}`, uses paragraph [`leading`]($par.leading) for tight
108 /// lists and paragraph [`spacing`]($par.spacing) for wide (non-tight)
109 /// lists.
110 pub spacing: Smart<Length>,
111
112 /// The bullet list's children.
113 ///
114 /// When using the list syntax, adjacent items are automatically collected
115 /// into lists, even through constructs like for loops.
116 ///
117 /// ```example
118 /// #for letter in "ABC" [
119 /// - Letter #letter
120 /// ]
121 /// ```
122 #[variadic]
123 pub children: Vec<Packed<ListItem>>,
124
125 /// The nesting depth.
126 #[internal]
127 #[fold]
128 #[ghost]
129 pub depth: Depth,
130}
131
132#[scope]
133impl ListElem {
134 #[elem]
135 type ListItem;
136}
137
138/// A bullet list item.
139#[elem(name = "item", title = "Bullet List Item", Tagged)]
140pub struct ListItem {
141 /// The item's body.
142 #[required]
143 pub body: Content,
144}
145
146cast! {
147 ListItem,
148 v: Content => v.unpack::<Self>().unwrap_or_else(Self::new)
149}
150
151/// A list's marker.
152#[derive(Debug, Clone, PartialEq, Hash)]
153pub enum ListMarker {
154 Content(Vec<Content>),
155 Func(Func),
156}
157
158impl ListMarker {
159 /// Resolve the marker for the given depth.
160 pub fn resolve(
161 &self,
162 engine: &mut Engine,
163 styles: StyleChain,
164 depth: usize,
165 ) -> SourceResult<Content> {
166 Ok(match self {
167 Self::Content(list) => {
168 list.get(depth % list.len()).cloned().unwrap_or_default()
169 }
170 Self::Func(func) => func
171 .call(engine, Context::new(None, Some(styles)).track(), [depth])?
172 .display(),
173 })
174 }
175}
176
177cast! {
178 ListMarker,
179 self => match self {
180 Self::Content(vec) => if vec.len() == 1 {
181 vec.into_iter().next().unwrap().into_value()
182 } else {
183 vec.into_value()
184 },
185 Self::Func(func) => func.into_value(),
186 },
187 v: Content => Self::Content(vec![v]),
188 array: Array => {
189 if array.is_empty() {
190 bail!("array must contain at least one marker");
191 }
192 Self::Content(array.into_iter().map(Value::display).collect())
193 },
194 v: Func => Self::Func(v),
195}
196
197/// A list, enum, or term list.
198pub trait ListLike: NativeElement {
199 /// The kind of list item this list is composed of.
200 type Item: ListItemLike;
201
202 /// Create this kind of list from its children and tightness.
203 fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self;
204}
205
206/// A list item, enum item, or term list item.
207pub trait ListItemLike: NativeElement {
208 /// Apply styles to the element's body.
209 fn styled(item: Packed<Self>, styles: Styles) -> Packed<Self>;
210}
211
212impl ListLike for ListElem {
213 type Item = ListItem;
214
215 fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self {
216 Self::new(children).with_tight(tight)
217 }
218}
219
220impl ListItemLike for ListItem {
221 fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> {
222 item.body.style_in_place(styles);
223 item
224 }
225}