diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs index 9b7ea1996..866a6195f 100644 --- a/library/src/layout/enum.rs +++ b/library/src/layout/enum.rs @@ -150,7 +150,7 @@ impl EnumNode { /// The spacing between the items of a wide (non-tight) enumeration. /// - /// If set to `{auto}` uses the spacing [below blocks]($func/block.below). + /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below). pub const SPACING: Smart = Smart::Auto; /// The numbers of parent items. diff --git a/library/src/layout/list.rs b/library/src/layout/list.rs index eab835cae..38816933c 100644 --- a/library/src/layout/list.rs +++ b/library/src/layout/list.rs @@ -76,16 +76,29 @@ pub struct ListNode { #[node] impl ListNode { - /// The marker which introduces each element. + /// The marker which introduces each item. + /// + /// Instead of plain content, you can also pass an array with multiple + /// markers that should be used for nested lists. If the list nesting depth + /// exceeds the number of markers, the last one is repeated. For total + /// control, you may pass a function that maps the list's nesting depth + /// (starting from `{0}`) to a desired marker. + /// + /// Default: `•` /// /// ```example /// #set list(marker: [--]) - /// /// - A more classic list /// - With en-dashes + /// + /// #set list(marker: ([•], [--])) + /// - Top-level + /// - Nested + /// - Items + /// - Items /// ``` #[property(referenced)] - pub const MARKER: Content = TextNode::packed('•'); + pub const MARKER: Marker = Marker::Content(vec![]); /// The indent of each item's marker. #[property(resolve)] @@ -97,9 +110,13 @@ impl ListNode { /// The spacing between the items of a wide (non-tight) list. /// - /// If set to `{auto}` uses the spacing [below blocks]($func/block.below). + /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below). pub const SPACING: Smart = Smart::Auto; + /// The nesting depth. + #[property(skip, fold)] + const DEPTH: Depth = 0; + fn construct(_: &Vm, args: &mut Args) -> SourceResult { Ok(Self { tight: args.named("tight")?.unwrap_or(true), @@ -126,7 +143,6 @@ impl Layout for ListNode { styles: StyleChain, regions: Regions, ) -> SourceResult { - let marker = styles.get(Self::MARKER); let indent = styles.get(Self::INDENT); let body_indent = styles.get(Self::BODY_INDENT); let gutter = if self.tight { @@ -137,12 +153,17 @@ impl Layout for ListNode { .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount) }; + let depth = styles.get(Self::DEPTH); + let marker = styles.get(Self::MARKER).resolve(vt.world(), depth)?; + let mut cells = vec![]; for (item, map) in self.items.iter() { cells.push(Content::empty()); cells.push(marker.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::DEPTH, Depth), + ); } GridNode { @@ -158,3 +179,51 @@ impl Layout for ListNode { .layout(vt, styles, regions) } } + +/// A list's marker. +#[derive(Debug, Clone, Hash)] +pub enum Marker { + Content(Vec), + Func(Func), +} + +impl Marker { + /// Resolve the marker for the given depth. + fn resolve(&self, world: Tracked, depth: usize) -> SourceResult { + Ok(match self { + Self::Content(list) => list + .get(depth) + .or(list.last()) + .cloned() + .unwrap_or_else(|| TextNode::packed('•')), + Self::Func(func) => { + let args = Args::new(func.span(), [Value::Int(depth as i64)]); + func.call_detached(world, args)?.display() + } + }) + } +} + +castable! { + Marker, + v: Content => Self::Content(vec![v]), + array: Array => { + if array.len() == 0 { + Err("must contain at least one marker")?; + } + Self::Content(array.into_iter().map(Value::display).collect()) + }, + v: Func => Self::Func(v), +} + +#[derive(Debug, Clone, Hash)] +struct Depth; + +impl Fold for Depth { + type Output = usize; + + fn fold(self, mut outer: Self::Output) -> Self::Output { + outer += 1; + outer + } +} diff --git a/library/src/layout/terms.rs b/library/src/layout/terms.rs index b1d399dba..f2902b808 100644 --- a/library/src/layout/terms.rs +++ b/library/src/layout/terms.rs @@ -82,7 +82,7 @@ impl TermsNode { /// The spacing between the items of a wide (non-tight) term list. /// - /// If set to `{auto}` uses the spacing [below blocks]($func/block.below). + /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below). pub const SPACING: Smart = Smart::Auto; fn construct(_: &Vm, args: &mut Args) -> SourceResult { @@ -171,7 +171,7 @@ castable! { 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()?), - _ => Err("array must contain exactly two entries")?, + _ => Err("term array must contain exactly two entries")?, }; Self { term, description } }, diff --git a/tests/ref/layout/list-marker.png b/tests/ref/layout/list-marker.png new file mode 100644 index 000000000..cd0eab440 Binary files /dev/null and b/tests/ref/layout/list-marker.png differ diff --git a/tests/ref/layout/list.png b/tests/ref/layout/list.png index 6db5fb711..b184aefa8 100644 Binary files a/tests/ref/layout/list.png and b/tests/ref/layout/list.png differ diff --git a/tests/typ/layout/list-marker.typ b/tests/typ/layout/list-marker.typ new file mode 100644 index 000000000..0d223b58a --- /dev/null +++ b/tests/typ/layout/list-marker.typ @@ -0,0 +1,34 @@ +// Test list marker configuraiton. + +--- +// Test en-dash. +#set list(marker: [--]) +- A +- B + +--- +// Test that last item is repeated. +#set list(marker: ([--], [•])) +- A + - B + - C + +--- +// Test function. +#set list(marker: n => if n == 1 [--] else [•]) +- A +- B + - C + - D + - E +- F + +--- +// Test that bare hyphen doesn't lead to cycles and crashes. +#set list(marker: [-]) +- Bare hyphen is +- a bad marker + +--- +// Error: 19-21 must contain at least one marker +#set list(marker: ()) diff --git a/tests/typ/layout/list.typ b/tests/typ/layout/list.typ index 3fd9ddb1d..71ccfe351 100644 --- a/tests/typ/layout/list.typ +++ b/tests/typ/layout/list.typ @@ -44,11 +44,6 @@ _Shopping list_ - A with 2 spaces - B with 2 tabs ---- -#set list(marker: [-]) -- Bare hyphen -- is not a list - --- // Edge cases. -