Skip to main content

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::{Alignment, Em, HAlignment, 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 <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 <syntax>
40/// This function 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 more
43/// 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 @list.spacing[spacing] of the list. If it is
47    /// `{false}`, the items are spaced apart with
48    /// @par.spacing[paragraph spacing]. If it is `{true}`, they use
49    /// @par.leading[paragraph 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 @par.leading[`leading`] for tight
108    /// lists and paragraph @par.spacing[`spacing`] for wide (non-tight) lists.
109    pub spacing: Smart<Length>,
110
111    /// Alignment to use for list markers.
112    ///
113    /// Vertical alignment is always relative to the height of the list items
114    /// they are attached to. By default, it is unspecified, which means that
115    /// each marker will vertically align with the baseline of the item it is
116    /// attached to (which is usually its first line of text, or otherwise its
117    /// top).
118    ///
119    /// Horizontal alignment, on the other hand, is relative to other markers at
120    /// the same list level. By default, it is set to `{end}`, meaning that
121    /// markers line up towards the end of the current text direction (`{right}`
122    /// for LTR, `{left}` for RTL text). However, since markers at each level
123    /// are usually identical, it is expected that horizontal alignment has no
124    /// actual effect, most of the time. Regardless, it is still possible for it
125    /// to make a difference in some cases, particularly if markers use counters
126    /// or other forms of state to display different content for each item.
127    ///
128    /// ```example
129    /// - Baseline \
130    ///   aligned
131    ///
132    /// #set list(marker-align: horizon)
133    ///
134    /// - Horizon \
135    ///   aligned
136    /// ```
137    #[default(Alignment::H(HAlignment::End))]
138    pub marker_align: Alignment,
139
140    /// The bullet list's children.
141    ///
142    /// When using the list syntax, adjacent items are automatically collected
143    /// into lists, even through constructs like for loops.
144    ///
145    /// ```example
146    /// #for letter in "ABC" [
147    ///   - Letter #letter
148    /// ]
149    /// ```
150    #[variadic]
151    pub children: Vec<Packed<ListItem>>,
152
153    /// The nesting depth.
154    #[internal]
155    #[fold]
156    #[ghost]
157    pub depth: Depth,
158}
159
160#[scope]
161impl ListElem {
162    #[elem]
163    type ListItem;
164}
165
166/// A bullet list item.
167#[elem(name = "item", title = "Bullet List Item", Tagged)]
168pub struct ListItem {
169    /// The item's body.
170    #[required]
171    pub body: Content,
172}
173
174cast! {
175    ListItem,
176    v: Content => v.unpack::<Self>().unwrap_or_else(Self::new)
177}
178
179/// A list's marker.
180#[derive(Debug, Clone, PartialEq, Hash)]
181pub enum ListMarker {
182    Content(Vec<Content>),
183    Func(Func),
184}
185
186impl ListMarker {
187    /// Resolve the marker for the given depth.
188    pub fn resolve(
189        &self,
190        engine: &mut Engine,
191        styles: StyleChain,
192        depth: usize,
193    ) -> SourceResult<Content> {
194        Ok(match self {
195            Self::Content(list) => {
196                list.get(depth % list.len()).cloned().unwrap_or_default()
197            }
198            Self::Func(func) => func
199                .call(engine, Context::new(None, Some(styles)).track(), [depth])?
200                .display(),
201        })
202    }
203}
204
205cast! {
206    ListMarker,
207    self => match self {
208        Self::Content(vec) => if vec.len() == 1 {
209            vec.into_iter().next().unwrap().into_value()
210        } else {
211            vec.into_value()
212        },
213        Self::Func(func) => func.into_value(),
214    },
215    v: Content => Self::Content(vec![v]),
216    array: Array => {
217        if array.is_empty() {
218            bail!("array must contain at least one marker");
219        }
220        Self::Content(array.into_iter().map(Value::display).collect())
221    },
222    v: Func => Self::Func(v),
223}
224
225/// A list, enum, or term list.
226pub trait ListLike: NativeElement {
227    /// The kind of list item this list is composed of.
228    type Item: ListItemLike;
229
230    /// Create this kind of list from its children and tightness.
231    fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self;
232}
233
234/// A list item, enum item, or term list item.
235pub trait ListItemLike: NativeElement {
236    /// Apply styles to the element's body.
237    fn styled(item: Packed<Self>, styles: Styles) -> Packed<Self>;
238}
239
240impl ListLike for ListElem {
241    type Item = ListItem;
242
243    fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self {
244        Self::new(children).with_tight(tight)
245    }
246}
247
248impl ListItemLike for ListItem {
249    fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> {
250        item.body.style_in_place(styles);
251        item
252    }
253}