typst_library/model/terms.rs
1use crate::diag::{bail, warning};
2use crate::foundations::{
3 Array, Content, NativeElement, Packed, Reflect, Smart, Styles, cast, elem, scope,
4};
5use crate::introspection::{Locatable, Tagged};
6use crate::layout::{Em, HElem, Length};
7use crate::model::{ListItemLike, ListLike};
8
9/// A list of terms and their descriptions.
10///
11/// Displays a sequence of terms and their descriptions vertically. When the
12/// descriptions span over multiple lines, they use hanging indent to
13/// communicate the visual hierarchy.
14///
15/// = Example <example>
16/// ```example
17/// / Ligature: A merged glyph.
18/// / Kerning: A spacing adjustment
19/// between two adjacent letters.
20/// ```
21///
22/// = Syntax <syntax>
23/// This function also has dedicated syntax: Starting a line with a slash,
24/// followed by a term, a colon and a description creates a term list item.
25#[elem(scope, title = "Term List", Locatable, Tagged)]
26pub struct TermsElem {
27 /// Defines the default @terms.spacing[spacing] of the term list. If it is
28 /// `{false}`, the items are spaced apart with
29 /// @par.spacing[paragraph spacing]. If it is `{true}`, they use
30 /// @par.leading[paragraph leading] instead. This makes the list more
31 /// compact, which can look better if the items are short.
32 ///
33 /// In markup mode, the value of this parameter is determined based on
34 /// whether items are separated with a blank line. If items directly follow
35 /// each other, this is set to `{true}`; if items are separated by a blank
36 /// line, this is set to `{false}`. The markup-defined tightness cannot be
37 /// overridden with set rules.
38 ///
39 /// ```example
40 /// / Fact: If a term list has a lot
41 /// of text, and maybe other inline
42 /// content, it should not be tight
43 /// anymore.
44 ///
45 /// / Tip: To make it wide, simply
46 /// insert a blank line between the
47 /// items.
48 /// ```
49 #[default(true)]
50 pub tight: bool,
51
52 /// The separator between the item and the description.
53 ///
54 /// If you want to just separate them with a certain amount of space, use
55 /// `{h(2cm, weak: true)}` as the separator and replace `{2cm}` with your
56 /// desired amount of space.
57 ///
58 /// ```example
59 /// #set terms(separator: [: ])
60 ///
61 /// / Colon: A nice separator symbol.
62 /// ```
63 #[default(HElem::new(Em::new(0.6).into()).with_weak(true).pack())]
64 pub separator: Content,
65
66 /// The indentation of each item.
67 pub indent: Length,
68
69 /// The hanging indent of the description.
70 ///
71 /// This is in addition to the whole item's `indent`.
72 ///
73 /// ```example
74 /// #set terms(hanging-indent: 0pt)
75 /// / Term: This term list does not
76 /// make use of hanging indents.
77 /// ```
78 #[default(Em::new(2.0).into())]
79 pub hanging_indent: Length,
80
81 /// The spacing between the items of the term list.
82 ///
83 /// If set to `{auto}`, uses paragraph @par.leading[`leading`] for tight
84 /// term lists and paragraph @par.spacing[`spacing`] for wide (non-tight)
85 /// term lists.
86 pub spacing: Smart<Length>,
87
88 /// The term list's children.
89 ///
90 /// When using the term list syntax, adjacent items are automatically
91 /// collected into term lists, even through constructs like for loops.
92 ///
93 /// ```example
94 /// #for (year, product) in (
95 /// "1978": "TeX",
96 /// "1984": "LaTeX",
97 /// "2019": "Typst",
98 /// ) [/ #product: Born in #year.]
99 /// ```
100 #[variadic]
101 #[parse(
102 for item in args.items.iter() {
103 if item.name.is_none() && Array::castable(&item.value.v) {
104 engine.sink.warn(warning!(
105 item.value.span,
106 "implicit conversion from array to `terms.item` is deprecated";
107 hint: "use `terms.item(term, description)` instead";
108 hint: "this conversion was never documented and is being phased out";
109 ));
110 }
111 }
112 args.all()?
113 )]
114 pub children: Vec<Packed<TermItem>>,
115
116 /// Whether we are currently within a term list.
117 #[internal]
118 #[ghost]
119 pub within: bool,
120}
121
122#[scope]
123impl TermsElem {
124 #[elem]
125 type TermItem;
126}
127
128/// A term list item.
129#[elem(name = "item", title = "Term List Item", Tagged)]
130pub struct TermItem {
131 /// The term described by the list item.
132 #[required]
133 pub term: Content,
134
135 /// The description of the term.
136 #[required]
137 pub description: Content,
138}
139
140cast! {
141 TermItem,
142 array: Array => {
143 let mut iter = array.into_iter();
144 let (term, description) = match (iter.next(), iter.next(), iter.next()) {
145 (Some(a), Some(b), None) => (a.cast()?, b.cast()?),
146 _ => bail!("array must contain exactly two entries"),
147 };
148 Self::new(term, description)
149 },
150 v: Content => v.unpack::<Self>().map_err(|_| "expected term item or array")?,
151}
152
153impl ListLike for TermsElem {
154 type Item = TermItem;
155
156 fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self {
157 Self::new(children).with_tight(tight)
158 }
159}
160
161impl ListItemLike for TermItem {
162 fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> {
163 item.term.style_in_place(styles.clone());
164 item.description.style_in_place(styles);
165 item
166 }
167}