typst_library/model/
list.rs

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