diff options
Diffstat (limited to 'crates/typst-library/src/layout/enum.rs')
| -rw-r--r-- | crates/typst-library/src/layout/enum.rs | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/crates/typst-library/src/layout/enum.rs b/crates/typst-library/src/layout/enum.rs new file mode 100644 index 00000000..d66477fc --- /dev/null +++ b/crates/typst-library/src/layout/enum.rs @@ -0,0 +1,318 @@ +use std::str::FromStr; + +use crate::layout::{BlockElem, ParElem, Sizing, Spacing}; +use crate::meta::{Numbering, NumberingPattern}; +use crate::prelude::*; +use crate::text::TextElem; + +use super::GridLayouter; + +/// A numbered list. +/// +/// Displays a sequence of items vertically and numbers them consecutively. +/// +/// ## Example { #example } +/// ```example +/// Automatically numbered: +/// + Preparations +/// + Analysis +/// + Conclusions +/// +/// Manually numbered: +/// 2. What is the first step? +/// 5. I am confused. +/// + Moving on ... +/// +/// Function call. +/// #enum[First][Second] +/// ``` +/// +/// You can easily switch all your enumerations to a different numbering style +/// with a set rule. +/// ```example +/// #set enum(numbering: "a)") +/// +/// + Starting off ... +/// + Don't forget step two +/// ``` +/// +/// You can also use [`enum.item`]($func/enum.item) to programmatically +/// customize the number of each item in the enumeration: +/// +/// ```example +/// #enum( +/// enum.item(1)[First step], +/// enum.item(5)[Fifth step], +/// enum.item(10)[Tenth step] +/// ) +/// ``` +/// +/// ## Syntax { #syntax } +/// This functions also has dedicated syntax: +/// +/// - Starting a line with a plus sign creates an automatically numbered +/// enumeration item. +/// - Starting a line with a number followed by a dot creates an explicitly +/// numbered enumeration item. +/// +/// Enumeration items can contain multiple paragraphs and other block-level +/// content. All content that is indented more than an item's plus sign or dot +/// becomes part of that item. +/// +/// Display: Numbered List +/// Category: layout +#[element(Layout)] +#[scope( + scope.define("item", EnumItem::func()); + scope +)] +pub struct EnumElem { + /// If this is `{false}`, the items are spaced apart with + /// [enum spacing]($func/enum.spacing). If it is `{true}`, they use normal + /// [leading]($func/par.leading) instead. This makes the enumeration 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}`. + /// + /// ```example + /// + If an enum has a lot of text, and + /// maybe other inline content, it + /// should not be tight anymore. + /// + /// + To make an enum wide, simply + /// insert a blank line between the + /// items. + /// ``` + #[default(true)] + pub tight: bool, + + /// How to number the enumeration. Accepts a + /// [numbering pattern or function]($func/numbering). + /// + /// If the numbering pattern contains multiple counting symbols, they apply + /// to nested enums. If given a function, the function receives one argument + /// if `full` is `{false}` and multiple arguments if `full` is `{true}`. + /// + /// ```example + /// #set enum(numbering: "1.a)") + /// + Different + /// + Numbering + /// + Nested + /// + Items + /// + Style + /// + /// #set enum(numbering: n => super[#n]) + /// + Superscript + /// + Numbering! + /// ``` + #[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))] + pub numbering: Numbering, + + /// Which number to start the enumeration with. + /// + /// ```example + /// #enum( + /// start: 3, + /// [Skipping], + /// [Ahead], + /// ) + /// ``` + #[default(1)] + pub start: usize, + + /// Whether to display the full numbering, including the numbers of + /// all parent enumerations. + /// + /// + /// ```example + /// #set enum(numbering: "1.a)", full: true) + /// + Cook + /// + Heat water + /// + Add integredients + /// + Eat + /// ``` + #[default(false)] + pub full: bool, + + /// The indentation of each item. + #[resolve] + pub indent: Length, + + /// The space between the numbering and the body of each item. + #[resolve] + #[default(Em::new(0.5).into())] + pub body_indent: Length, + + /// The spacing between the items of a wide (non-tight) enumeration. + /// + /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below). + pub spacing: Smart<Spacing>, + + /// The horizontal alignment that enum numbers should have. + /// + /// By default, this is set to `{end}`, which aligns enum numbers + /// towards end of the current text direction (in left-to-right script, + /// for example, this is the same as `{right}`). The choice of `{end}` + /// for horizontal alignment of enum numbers is usually preferred over + /// `{start}`, as numbers then grow away from the text instead of towards + /// it, avoiding certain visual issues. This option lets you override this + /// behavior, however. + /// + /// ````example + /// #set enum(number-align: start) + /// + /// Here are some powers of two: + /// 1. One + /// 2. Two + /// 4. Four + /// 8. Eight + /// 16. Sixteen + /// 32. Thirty two + /// ```` + #[default(HorizontalAlign(GenAlign::End))] + pub number_align: HorizontalAlign, + + /// The numbered list's items. + /// + /// When using the enum syntax, adjacent items are automatically collected + /// into enumerations, even through constructs like for loops. + /// + /// ```example + /// #for phase in ( + /// "Launch", + /// "Orbit", + /// "Descent", + /// ) [+ #phase] + /// ``` + #[variadic] + pub children: Vec<EnumItem>, + + /// The numbers of parent items. + #[internal] + #[fold] + parents: Parent, +} + +impl Layout for EnumElem { + #[tracing::instrument(name = "EnumElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let numbering = self.numbering(styles); + let indent = self.indent(styles); + let body_indent = self.body_indent(styles); + let gutter = if self.tight(styles) { + ParElem::leading_in(styles).into() + } else { + self.spacing(styles) + .unwrap_or_else(|| BlockElem::below_in(styles).amount()) + }; + + let mut cells = vec![]; + let mut number = self.start(styles); + let mut parents = self.parents(styles); + let full = self.full(styles); + + // Horizontally align based on the given respective parameter. + // Vertically align to the top to avoid inheriting 'horizon' or + // 'bottom' alignment from the context and having the number be + // displaced in relation to the item it refers to. + let number_align: Axes<Option<GenAlign>> = + Axes::new(self.number_align(styles).into(), Align::Top.into()).map(Some); + + for item in self.children() { + number = item.number(styles).unwrap_or(number); + + let resolved = if full { + parents.push(number); + let content = numbering.apply_vt(vt, &parents)?.display(); + parents.pop(); + content + } else { + match &numbering { + Numbering::Pattern(pattern) => { + TextElem::packed(pattern.apply_kth(parents.len(), number)) + } + other => other.apply_vt(vt, &[number])?.display(), + } + }; + + // Disable overhang as a workaround to end-aligned dots glitching + // and decreasing spacing between numbers and items. + let resolved = + resolved.aligned(number_align).styled(TextElem::set_overhang(false)); + + cells.push(Content::empty()); + cells.push(resolved); + cells.push(Content::empty()); + cells.push(item.body().styled(Self::set_parents(Parent(number)))); + number = number.saturating_add(1); + } + + let layouter = GridLayouter::new( + Axes::with_x(&[ + Sizing::Rel(indent.into()), + Sizing::Auto, + Sizing::Rel(body_indent.into()), + Sizing::Auto, + ]), + Axes::with_y(&[gutter.into()]), + &cells, + regions, + styles, + ); + + Ok(layouter.layout(vt)?.fragment) + } +} + +/// An enumeration item. +/// +/// Display: Numbered List Item +/// Category: layout +#[element] +pub struct EnumItem { + /// The item's number. + #[positional] + pub number: Option<usize>, + + /// The item's body. + #[required] + pub body: Content, +} + +cast! { + EnumItem, + array: Array => { + let mut iter = array.into_iter(); + let (number, body) = 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(body).with_number(number) + }, + v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), +} + +struct Parent(usize); + +cast! { + Parent, + self => self.0.into_value(), + v: usize => Self(v), +} + +impl Fold for Parent { + type Output = Vec<usize>; + + fn fold(self, mut outer: Self::Output) -> Self::Output { + outer.push(self.0); + outer + } +} |
