diff --git a/library/src/compute/utility.rs b/library/src/compute/utility.rs index cb7655776..2220c890b 100644 --- a/library/src/compute/utility.rs +++ b/library/src/compute/utility.rs @@ -176,6 +176,24 @@ impl NumberingPattern { fmt.push_str(&self.suffix); fmt } + + /// Apply only the k-th segment of the pattern to a number. + pub fn apply_kth(&self, k: usize, number: NonZeroUsize) -> EcoString { + let mut fmt = EcoString::new(); + if let Some((prefix, _, _)) = self.pieces.first() { + fmt.push_str(prefix); + } + if let Some((_, kind, case)) = self + .pieces + .iter() + .chain(self.pieces.last().into_iter().cycle()) + .nth(k) + { + fmt.push_str(&kind.apply(number, *case)); + } + fmt.push_str(&self.suffix); + fmt + } } impl FromStr for NumberingPattern { diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs index 585b20e5d..9b7ea1996 100644 --- a/library/src/layout/enum.rs +++ b/library/src/layout/enum.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use crate::compute::{Numbering, NumberingPattern}; use crate::layout::{BlockNode, GridNode, ParNode, Sizing, Spacing}; use crate::prelude::*; +use crate::text::TextNode; /// # Numbered List /// A numbered list. @@ -105,10 +106,16 @@ impl EnumNode { /// 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: "(a)") + /// #set enum(numbering: "1.a)") /// + Different /// + Numbering + /// + Nested + /// + Items /// + Style /// /// #set enum(numbering: n => super[#n]) @@ -119,6 +126,20 @@ impl EnumNode { pub const NUMBERING: Numbering = Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()); + /// Whether to display the full numbering, including the numbers of + /// all parent enumerations. + /// + /// Defaults to `{false}`. + /// + /// ```example + /// #set enum(numbering: "1.a)", full: true) + /// + Cook + /// + Heat water + /// + Add integredients + /// + Eat + /// ``` + pub const FULL: bool = false; + /// The indentation of each item's label. #[property(resolve)] pub const INDENT: Length = Length::zero(); @@ -132,6 +153,10 @@ impl EnumNode { /// If set to `{auto}` uses the spacing [below blocks]($func/block.below). pub const SPACING: Smart = Smart::Auto; + /// The numbers of parent items. + #[property(skip, fold)] + const PARENTS: Parent = vec![]; + fn construct(_: &Vm, args: &mut Args) -> SourceResult { let mut number: NonZeroUsize = args.named("start")?.unwrap_or(NonZeroUsize::new(1).unwrap()); @@ -193,13 +218,34 @@ impl Layout for EnumNode { let mut cells = vec![]; let mut number = NonZeroUsize::new(1).unwrap(); + let mut parents = styles.get(Self::PARENTS); + let full = styles.get(Self::FULL); + for ((n, item), map) in self.items.iter() { number = n.unwrap_or(number); - let resolved = numbering.apply(vt.world(), &[number])?.display(); + + let resolved = if full { + parents.push(number); + let content = numbering.apply(vt.world(), &parents)?.display(); + parents.pop(); + content + } else { + match numbering { + Numbering::Pattern(pattern) => { + TextNode::packed(pattern.apply_kth(parents.len(), number)) + } + other => other.apply(vt.world(), &[number])?.display(), + } + }; + cells.push(Content::empty()); cells.push(resolved.styled_with_map(map.clone())); cells.push(Content::empty()); - cells.push(item.clone().styled_with_map(map.clone())); + cells.push( + item.clone() + .styled_with_map(map.clone()) + .styled(Self::PARENTS, Parent(number)), + ); number = number.saturating_add(1); } @@ -216,3 +262,15 @@ impl Layout for EnumNode { .layout(vt, styles, regions) } } + +#[derive(Debug, Clone, Hash)] +struct Parent(NonZeroUsize); + +impl Fold for Parent { + type Output = Vec; + + fn fold(self, mut outer: Self::Output) -> Self::Output { + outer.push(self.0); + outer + } +} diff --git a/tests/ref/layout/enum-numbering.png b/tests/ref/layout/enum-numbering.png new file mode 100644 index 000000000..6745a2f5f Binary files /dev/null and b/tests/ref/layout/enum-numbering.png differ diff --git a/tests/typ/layout/enum-numbering.typ b/tests/typ/layout/enum-numbering.typ new file mode 100644 index 000000000..1d905f69a --- /dev/null +++ b/tests/typ/layout/enum-numbering.typ @@ -0,0 +1,55 @@ +// Test enum numbering styles. + +--- +// Test numbering pattern. +#set enum(numbering: "(1.a.*)") ++ First ++ Second + 2. Nested + + Deep ++ Normal + +--- +// Test full numbering. +#set enum(numbering: "1.a.", full: true) ++ First + + Nested + +--- +// Test numbering with closure. +#enum( + start: 3, + spacing: 0.65em - 3pt, + tight: false, + numbering: n => text( + fill: (red, green, blue).at(calc.mod(n, 3)), + numbering("A", n), + ), + [Red], [Green], [Blue], [Red], +) + +--- +// Test numbering with closure and nested lists. +#set enum(numbering: n => super[#n]) ++ A + + B ++ C + +--- +// Test numbering with closure and nested lists. +#set text("Latin Modern Roman") +#set enum(numbering: (..args) => math.mat(args.pos()), full: true) ++ A + + B + + C + + D ++ E ++ F + +--- +// Error: 22-24 invalid numbering pattern +#set enum(numbering: "") + +--- +// Error: 22-28 invalid numbering pattern +#set enum(numbering: "(())")