typst_library/model/
terms.rs

1use typst_utils::{Get, Numeric};
2
3use crate::diag::{bail, SourceResult};
4use crate::engine::Engine;
5use crate::foundations::{
6    cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
7    Styles, TargetElem,
8};
9use crate::html::{tag, HtmlElem};
10use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
11use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem};
12use crate::text::TextElem;
13
14/// A list of terms and their descriptions.
15///
16/// Displays a sequence of terms and their descriptions vertically. When the
17/// descriptions span over multiple lines, they use hanging indent to
18/// communicate the visual hierarchy.
19///
20/// # Example
21/// ```example
22/// / Ligature: A merged glyph.
23/// / Kerning: A spacing adjustment
24///   between two adjacent letters.
25/// ```
26///
27/// # Syntax
28/// This function also has dedicated syntax: Starting a line with a slash,
29/// followed by a term, a colon and a description creates a term list item.
30#[elem(scope, title = "Term List", Show)]
31pub struct TermsElem {
32    /// Defines the default [spacing]($terms.spacing) of the term list. If it is
33    /// `{false}`, the items are spaced apart with
34    /// [paragraph spacing]($par.spacing). If it is `{true}`, they use
35    /// [paragraph leading]($par.leading) instead. This makes the list more
36    /// compact, which can look better if the items are short.
37    ///
38    /// In markup mode, the value of this parameter is determined based on
39    /// whether items are separated with a blank line. If items directly follow
40    /// each other, this is set to `{true}`; if items are separated by a blank
41    /// line, this is set to `{false}`. The markup-defined tightness cannot be
42    /// overridden with set rules.
43    ///
44    /// ```example
45    /// / Fact: If a term list has a lot
46    ///   of text, and maybe other inline
47    ///   content, it should not be tight
48    ///   anymore.
49    ///
50    /// / Tip: To make it wide, simply
51    ///   insert a blank line between the
52    ///   items.
53    /// ```
54    #[default(true)]
55    pub tight: bool,
56
57    /// The separator between the item and the description.
58    ///
59    /// If you want to just separate them with a certain amount of space, use
60    /// `{h(2cm, weak: true)}` as the separator and replace `{2cm}` with your
61    /// desired amount of space.
62    ///
63    /// ```example
64    /// #set terms(separator: [: ])
65    ///
66    /// / Colon: A nice separator symbol.
67    /// ```
68    #[default(HElem::new(Em::new(0.6).into()).with_weak(true).pack())]
69    #[borrowed]
70    pub separator: Content,
71
72    /// The indentation of each item.
73    pub indent: Length,
74
75    /// The hanging indent of the description.
76    ///
77    /// This is in addition to the whole item's `indent`.
78    ///
79    /// ```example
80    /// #set terms(hanging-indent: 0pt)
81    /// / Term: This term list does not
82    ///   make use of hanging indents.
83    /// ```
84    #[default(Em::new(2.0).into())]
85    pub hanging_indent: Length,
86
87    /// The spacing between the items of the term list.
88    ///
89    /// If set to `{auto}`, uses paragraph [`leading`]($par.leading) for tight
90    /// term lists and paragraph [`spacing`]($par.spacing) for wide
91    /// (non-tight) term lists.
92    pub spacing: Smart<Length>,
93
94    /// The term list's children.
95    ///
96    /// When using the term list syntax, adjacent items are automatically
97    /// collected into term lists, even through constructs like for loops.
98    ///
99    /// ```example
100    /// #for (year, product) in (
101    ///   "1978": "TeX",
102    ///   "1984": "LaTeX",
103    ///   "2019": "Typst",
104    /// ) [/ #product: Born in #year.]
105    /// ```
106    #[variadic]
107    pub children: Vec<Packed<TermItem>>,
108
109    /// Whether we are currently within a term list.
110    #[internal]
111    #[ghost]
112    pub within: bool,
113}
114
115#[scope]
116impl TermsElem {
117    #[elem]
118    type TermItem;
119}
120
121impl Show for Packed<TermsElem> {
122    fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
123        let span = self.span();
124        let tight = self.tight(styles);
125
126        if TargetElem::target_in(styles).is_html() {
127            return Ok(HtmlElem::new(tag::dl)
128                .with_body(Some(Content::sequence(self.children.iter().flat_map(
129                    |item| {
130                        // Text in wide term lists shall always turn into paragraphs.
131                        let mut description = item.description.clone();
132                        if !tight {
133                            description += ParbreakElem::shared();
134                        }
135
136                        [
137                            HtmlElem::new(tag::dt)
138                                .with_body(Some(item.term.clone()))
139                                .pack()
140                                .spanned(item.term.span()),
141                            HtmlElem::new(tag::dd)
142                                .with_body(Some(description))
143                                .pack()
144                                .spanned(item.description.span()),
145                        ]
146                    },
147                ))))
148                .pack());
149        }
150
151        let separator = self.separator(styles);
152        let indent = self.indent(styles);
153        let hanging_indent = self.hanging_indent(styles);
154        let gutter = self.spacing(styles).unwrap_or_else(|| {
155            if tight {
156                ParElem::leading_in(styles).into()
157            } else {
158                ParElem::spacing_in(styles).into()
159            }
160        });
161
162        let pad = hanging_indent + indent;
163        let unpad = (!hanging_indent.is_zero())
164            .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span));
165
166        let mut children = vec![];
167        for child in self.children.iter() {
168            let mut seq = vec![];
169            seq.extend(unpad.clone());
170            seq.push(child.term.clone().strong());
171            seq.push((*separator).clone());
172            seq.push(child.description.clone());
173
174            // Text in wide term lists shall always turn into paragraphs.
175            if !tight {
176                seq.push(ParbreakElem::shared().clone());
177            }
178
179            children.push(StackChild::Block(Content::sequence(seq)));
180        }
181
182        let padding = Sides::default().with(TextElem::dir_in(styles).start(), pad.into());
183
184        let mut realized = StackElem::new(children)
185            .with_spacing(Some(gutter.into()))
186            .pack()
187            .spanned(span)
188            .padded(padding)
189            .styled(TermsElem::set_within(true));
190
191        if tight {
192            let leading = ParElem::leading_in(styles);
193            let spacing = VElem::new(leading.into())
194                .with_weak(true)
195                .with_attach(true)
196                .pack()
197                .spanned(span);
198            realized = spacing + realized;
199        }
200
201        Ok(realized)
202    }
203}
204
205/// A term list item.
206#[elem(name = "item", title = "Term List Item")]
207pub struct TermItem {
208    /// The term described by the list item.
209    #[required]
210    pub term: Content,
211
212    /// The description of the term.
213    #[required]
214    pub description: Content,
215}
216
217cast! {
218    TermItem,
219    array: Array => {
220        let mut iter = array.into_iter();
221        let (term, description) = match (iter.next(), iter.next(), iter.next()) {
222            (Some(a), Some(b), None) => (a.cast()?, b.cast()?),
223            _ => bail!("array must contain exactly two entries"),
224        };
225        Self::new(term, description)
226    },
227    v: Content => v.unpack::<Self>().map_err(|_| "expected term item or array")?,
228}
229
230impl ListLike for TermsElem {
231    type Item = TermItem;
232
233    fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self {
234        Self::new(children).with_tight(tight)
235    }
236}
237
238impl ListItemLike for TermItem {
239    fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> {
240        item.term.style_in_place(styles.clone());
241        item.description.style_in_place(styles);
242        item
243    }
244}