Configurable markers for nested lists

This commit is contained in:
Laurenz 2023-02-13 17:15:11 +01:00
parent 05c8c6045c
commit 28b5c55cd5
7 changed files with 112 additions and 14 deletions

View File

@ -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<Spacing> = Smart::Auto;
/// The numbers of parent items.

View File

@ -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<Spacing> = Smart::Auto;
/// The nesting depth.
#[property(skip, fold)]
const DEPTH: Depth = 0;
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self {
tight: args.named("tight")?.unwrap_or(true),
@ -126,7 +143,6 @@ impl Layout for ListNode {
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
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<Content>),
Func(Func),
}
impl Marker {
/// Resolve the marker for the given depth.
fn resolve(&self, world: Tracked<dyn World>, depth: usize) -> SourceResult<Content> {
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
}
}

View File

@ -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<Spacing> = Smart::Auto;
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
@ -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 }
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -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: ())

View File

@ -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.
-