summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/model/terms.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-10-27 19:04:55 +0100
committerGitHub <noreply@github.com>2024-10-27 18:04:55 +0000
commitbe7cfc85d08c545abfac08098b7b33b4bd71f37e (patch)
treef4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/model/terms.rs
parentb8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff)
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/model/terms.rs')
-rw-r--r--crates/typst-library/src/model/terms.rs204
1 files changed, 204 insertions, 0 deletions
diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs
new file mode 100644
index 00000000..036a03e2
--- /dev/null
+++ b/crates/typst-library/src/model/terms.rs
@@ -0,0 +1,204 @@
+use typst_utils::Numeric;
+
+use crate::diag::{bail, SourceResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
+ Styles,
+};
+use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem};
+use crate::model::{ListItemLike, ListLike, ParElem};
+use crate::text::TextElem;
+
+/// A list of terms and their descriptions.
+///
+/// Displays a sequence of terms and their descriptions vertically. When the
+/// descriptions span over multiple lines, they use hanging indent to
+/// communicate the visual hierarchy.
+///
+/// # Example
+/// ```example
+/// / Ligature: A merged glyph.
+/// / Kerning: A spacing adjustment
+/// between two adjacent letters.
+/// ```
+///
+/// # Syntax
+/// This function also has dedicated syntax: Starting a line with a slash,
+/// followed by a term, a colon and a description creates a term list item.
+#[elem(scope, title = "Term List", Show)]
+pub struct TermsElem {
+ /// Defines the default [spacing]($terms.spacing) of the term list. If it is
+ /// `{false}`, the items are spaced apart with
+ /// [paragraph spacing]($par.spacing). If it is `{true}`, they use
+ /// [paragraph leading]($par.leading) instead. This makes the list more
+ /// compact, which can look better if the items are short.
+ ///
+ /// In markup mode, the value of this parameter is determined based on
+ /// whether items are separated with a blank line. If items directly follow
+ /// each other, this is set to `{true}`; if items are separated by a blank
+ /// line, this is set to `{false}`. The markup-defined tightness cannot be
+ /// overridden with set rules.
+ ///
+ /// ```example
+ /// / Fact: If a term list has a lot
+ /// of text, and maybe other inline
+ /// content, it should not be tight
+ /// anymore.
+ ///
+ /// / Tip: To make it wide, simply
+ /// insert a blank line between the
+ /// items.
+ /// ```
+ #[default(true)]
+ pub tight: bool,
+
+ /// The separator between the item and the description.
+ ///
+ /// If you want to just separate them with a certain amount of space, use
+ /// `{h(2cm, weak: true)}` as the separator and replace `{2cm}` with your
+ /// desired amount of space.
+ ///
+ /// ```example
+ /// #set terms(separator: [: ])
+ ///
+ /// / Colon: A nice separator symbol.
+ /// ```
+ #[default(HElem::new(Em::new(0.6).into()).with_weak(true).pack())]
+ #[borrowed]
+ pub separator: Content,
+
+ /// The indentation of each item.
+ pub indent: Length,
+
+ /// The hanging indent of the description.
+ ///
+ /// This is in addition to the whole item's `indent`.
+ ///
+ /// ```example
+ /// #set terms(hanging-indent: 0pt)
+ /// / Term: This term list does not
+ /// make use of hanging indents.
+ /// ```
+ #[default(Em::new(2.0).into())]
+ pub hanging_indent: Length,
+
+ /// The spacing between the items of the term list.
+ ///
+ /// If set to `{auto}`, uses paragraph [`leading`]($par.leading) for tight
+ /// term lists and paragraph [`spacing`]($par.spacing) for wide
+ /// (non-tight) term lists.
+ pub spacing: Smart<Length>,
+
+ /// The term list's children.
+ ///
+ /// When using the term list syntax, adjacent items are automatically
+ /// collected into term lists, even through constructs like for loops.
+ ///
+ /// ```example
+ /// #for (year, product) in (
+ /// "1978": "TeX",
+ /// "1984": "LaTeX",
+ /// "2019": "Typst",
+ /// ) [/ #product: Born in #year.]
+ /// ```
+ #[variadic]
+ pub children: Vec<Packed<TermItem>>,
+}
+
+#[scope]
+impl TermsElem {
+ #[elem]
+ type TermItem;
+}
+
+impl Show for Packed<TermsElem> {
+ fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ let separator = self.separator(styles);
+ let indent = self.indent(styles);
+ let hanging_indent = self.hanging_indent(styles);
+ let gutter = self.spacing(styles).unwrap_or_else(|| {
+ if self.tight(styles) {
+ ParElem::leading_in(styles).into()
+ } else {
+ ParElem::spacing_in(styles).into()
+ }
+ });
+
+ let pad = hanging_indent + indent;
+ let unpad = (!hanging_indent.is_zero())
+ .then(|| HElem::new((-hanging_indent).into()).pack());
+
+ let mut children = vec![];
+ for child in self.children().iter() {
+ let mut seq = vec![];
+ seq.extend(unpad.clone());
+ seq.push(child.term().clone().strong());
+ seq.push((*separator).clone());
+ seq.push(child.description().clone());
+ children.push(StackChild::Block(Content::sequence(seq)));
+ }
+
+ let mut padding = Sides::default();
+ if TextElem::dir_in(styles) == Dir::LTR {
+ padding.left = pad.into();
+ } else {
+ padding.right = pad.into();
+ }
+
+ let mut realized = StackElem::new(children)
+ .with_spacing(Some(gutter.into()))
+ .pack()
+ .padded(padding);
+
+ if self.tight(styles) {
+ let leading = ParElem::leading_in(styles);
+ let spacing =
+ VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
+ realized = spacing + realized;
+ }
+
+ Ok(realized)
+ }
+}
+
+/// A term list item.
+#[elem(name = "item", title = "Term List Item")]
+pub struct TermItem {
+ /// The term described by the list item.
+ #[required]
+ pub term: Content,
+
+ /// The description of the term.
+ #[required]
+ pub description: Content,
+}
+
+cast! {
+ TermItem,
+ array: Array => {
+ let mut iter = array.into_iter();
+ let (term, description) = match (iter.next(), iter.next(), iter.next()) {
+ (Some(a), Some(b), None) => (a.cast()?, b.cast()?),
+ _ => bail!("array must contain exactly two entries"),
+ };
+ Self::new(term, description)
+ },
+ v: Content => v.unpack::<Self>().map_err(|_| "expected term item or array")?,
+}
+
+impl ListLike for TermsElem {
+ type Item = TermItem;
+
+ fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self {
+ Self::new(children).with_tight(tight)
+ }
+}
+
+impl ListItemLike for TermItem {
+ fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> {
+ item.term.style_in_place(styles.clone());
+ item.description.style_in_place(styles);
+ item
+ }
+}