diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-11-03 11:44:53 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-11-03 13:35:39 +0100 |
| commit | 37a7afddfaffd44cb9bc013c9506599267e08983 (patch) | |
| tree | 20e7d62d3c5418baff01a21d0406b91bf3096214 /library/src/structure/list.rs | |
| parent | 56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff) | |
Split crates
Diffstat (limited to 'library/src/structure/list.rs')
| -rw-r--r-- | library/src/structure/list.rs | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/library/src/structure/list.rs b/library/src/structure/list.rs new file mode 100644 index 00000000..2015f19b --- /dev/null +++ b/library/src/structure/list.rs @@ -0,0 +1,371 @@ +use unscanny::Scanner; + +use crate::layout::{BlockSpacing, GridNode, HNode, TrackSizing}; +use crate::prelude::*; +use crate::text::{ParNode, SpaceNode}; +use crate::utility::Numbering; + +/// An unordered (bulleted) or ordered (numbered) list. +#[derive(Debug, Hash)] +pub struct ListNode<const L: ListKind = LIST> { + /// If true, the items are separated by leading instead of list spacing. + pub tight: bool, + /// If true, the spacing above the list is leading instead of above spacing. + pub attached: bool, + /// The individual bulleted or numbered items. + pub items: StyleVec<ListItem>, +} + +/// An ordered list. +pub type EnumNode = ListNode<ENUM>; + +/// A description list. +pub type DescNode = ListNode<DESC>; + +#[node(Show)] +impl<const L: ListKind> ListNode<L> { + /// How the list is labelled. + #[property(referenced)] + pub const LABEL: Label = Label::Default; + /// The indentation of each item's label. + #[property(resolve)] + pub const INDENT: Length = Length::zero(); + /// The space between the label and the body of each item. + #[property(resolve)] + pub const BODY_INDENT: Length = Em::new(match L { + LIST | ENUM => 0.5, + DESC | _ => 1.0, + }) + .into(); + + /// The spacing above the list. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into()); + /// The spacing below the list. + #[property(resolve, shorthand(around))] + pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into()); + /// The spacing between the items of a wide (non-tight) list. + #[property(resolve)] + pub const SPACING: BlockSpacing = Ratio::one().into(); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { + let items = match L { + LIST => args + .all()? + .into_iter() + .map(|body| ListItem::List(Box::new(body))) + .collect(), + ENUM => { + let mut number: usize = args.named("start")?.unwrap_or(1); + args.all()? + .into_iter() + .map(|body| { + let item = ListItem::Enum(Some(number), Box::new(body)); + number += 1; + item + }) + .collect() + } + DESC | _ => args + .all()? + .into_iter() + .map(|item| ListItem::Desc(Box::new(item))) + .collect(), + }; + + Ok(Self { + tight: args.named("tight")?.unwrap_or(true), + attached: args.named("attached")?.unwrap_or(false), + items, + } + .pack()) + } +} + +impl<const L: ListKind> Show for ListNode<L> { + fn unguard_parts(&self, sel: Selector) -> Content { + Self { + items: self.items.map(|item| item.unguard(sel)), + ..*self + } + .pack() + } + + fn field(&self, name: &str) -> Option<Value> { + match name { + "tight" => Some(Value::Bool(self.tight)), + "attached" => Some(Value::Bool(self.attached)), + "items" => Some(Value::Array( + self.items.items().map(|item| item.encode()).collect(), + )), + _ => None, + } + } + + fn realize( + &self, + world: Tracked<dyn World>, + styles: StyleChain, + ) -> SourceResult<Content> { + let mut cells = vec![]; + let mut number = 1; + + let label = styles.get(Self::LABEL); + let indent = styles.get(Self::INDENT); + let body_indent = styles.get(Self::BODY_INDENT); + let gutter = if self.tight { + styles.get(ParNode::LEADING) + } else { + styles.get(Self::SPACING) + }; + + for (item, map) in self.items.iter() { + if let &ListItem::Enum(Some(n), _) = item { + number = n; + } + + cells.push(Content::empty()); + + let label = if L == LIST || L == ENUM { + label.resolve(world, L, number)?.styled_with_map(map.clone()) + } else { + Content::empty() + }; + + cells.push(label); + cells.push(Content::empty()); + + let body = match &item { + ListItem::List(body) => body.as_ref().clone(), + ListItem::Enum(_, body) => body.as_ref().clone(), + ListItem::Desc(item) => Content::sequence(vec![ + HNode { + amount: (-body_indent).into(), + weak: false, + } + .pack(), + (item.term.clone() + TextNode(':'.into()).pack()).strong(), + SpaceNode.pack(), + item.body.clone(), + ]), + }; + + cells.push(body.styled_with_map(map.clone())); + number += 1; + } + + Ok(GridNode { + tracks: Axes::with_x(vec![ + TrackSizing::Relative(indent.into()), + TrackSizing::Auto, + TrackSizing::Relative(body_indent.into()), + TrackSizing::Auto, + ]), + gutter: Axes::with_y(vec![TrackSizing::Relative(gutter.into())]), + cells, + } + .pack()) + } + + fn finalize( + &self, + _: Tracked<dyn World>, + styles: StyleChain, + realized: Content, + ) -> SourceResult<Content> { + let mut above = styles.get(Self::ABOVE); + let mut below = styles.get(Self::BELOW); + + if self.attached { + if above.is_some() { + above = Some(styles.get(ParNode::LEADING)); + } + if below.is_some() { + below = Some(styles.get(ParNode::SPACING)); + } + } + + Ok(realized.spaced(above, below)) + } +} + +/// An item in a list. +#[derive(Clone, PartialEq, Hash)] +pub enum ListItem { + /// An item of an unordered list. + List(Box<Content>), + /// An item of an ordered list. + Enum(Option<usize>, Box<Content>), + /// An item of a description list. + Desc(Box<DescItem>), +} + +impl ListItem { + /// What kind of item this is. + pub fn kind(&self) -> ListKind { + match self { + Self::List(_) => LIST, + Self::Enum { .. } => ENUM, + Self::Desc { .. } => DESC, + } + } + + fn unguard(&self, sel: Selector) -> Self { + match self { + Self::List(body) => Self::List(Box::new(body.unguard(sel))), + Self::Enum(number, body) => Self::Enum(*number, Box::new(body.unguard(sel))), + Self::Desc(item) => Self::Desc(Box::new(DescItem { + term: item.term.unguard(sel), + body: item.body.unguard(sel), + })), + } + } + + /// Encode the item into a value. + fn encode(&self) -> Value { + match self { + Self::List(body) => Value::Content(body.as_ref().clone()), + Self::Enum(number, body) => Value::Dict(dict! { + "number" => match *number { + Some(n) => Value::Int(n as i64), + None => Value::None, + }, + "body" => Value::Content(body.as_ref().clone()), + }), + Self::Desc(item) => Value::Dict(dict! { + "term" => Value::Content(item.term.clone()), + "body" => Value::Content(item.body.clone()), + }), + } + } +} + +impl Debug for ListItem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::List(body) => write!(f, "- {body:?}"), + Self::Enum(number, body) => match number { + Some(n) => write!(f, "{n}. {body:?}"), + None => write!(f, "+ {body:?}"), + }, + Self::Desc(item) => item.fmt(f), + } + } +} + +#[node] +impl ListItem {} + +/// A description list item. +#[derive(Clone, PartialEq, Hash)] +pub struct DescItem { + /// The term described by the list item. + pub term: Content, + /// The description of the term. + pub body: Content, +} + +castable! { + DescItem, + Expected: "dictionary with `term` and `body` keys", + Value::Dict(dict) => { + let term: Content = dict.get("term")?.clone().cast()?; + let body: Content = dict.get("body")?.clone().cast()?; + Self { term, body } + }, +} + +impl Debug for DescItem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "/ {:?}: {:?}", self.term, self.body) + } +} + +/// How to label a list. +pub type ListKind = usize; + +/// An unordered list. +pub const LIST: ListKind = 0; + +/// An ordered list. +pub const ENUM: ListKind = 1; + +/// A description list. +pub const DESC: ListKind = 2; + +/// How to label a list or enumeration. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Label { + /// The default labelling. + Default, + /// A pattern with prefix, numbering, lower / upper case and suffix. + Pattern(EcoString, Numbering, bool, EcoString), + /// Bare content. + Content(Content), + /// A closure mapping from an item number to a value. + Func(Func, Span), +} + +impl Label { + /// Resolve the value based on the level. + pub fn resolve( + &self, + world: Tracked<dyn World>, + kind: ListKind, + number: usize, + ) -> SourceResult<Content> { + Ok(match self { + Self::Default => match kind { + LIST => TextNode('•'.into()).pack(), + ENUM => TextNode(format_eco!("{}.", number)).pack(), + DESC | _ => panic!("description lists don't have a label"), + }, + Self::Pattern(prefix, numbering, upper, suffix) => { + let fmt = numbering.apply(number); + let mid = if *upper { fmt.to_uppercase() } else { fmt.to_lowercase() }; + TextNode(format_eco!("{}{}{}", prefix, mid, suffix)).pack() + } + Self::Content(content) => content.clone(), + Self::Func(func, span) => { + let args = Args::new(*span, [Value::Int(number as i64)]); + func.call_detached(world, args)?.display(world) + } + }) + } +} + +impl Cast<Spanned<Value>> for Label { + fn is(value: &Spanned<Value>) -> bool { + matches!(&value.v, Value::Content(_) | Value::Func(_)) + } + + fn cast(value: Spanned<Value>) -> StrResult<Self> { + match value.v { + Value::None => Ok(Self::Content(Content::empty())), + Value::Str(pattern) => { + let mut s = Scanner::new(&pattern); + let mut prefix; + let numbering = loop { + prefix = s.before(); + match s.eat().map(|c| c.to_ascii_lowercase()) { + Some('1') => break Numbering::Arabic, + Some('a') => break Numbering::Letter, + Some('i') => break Numbering::Roman, + Some('*') => break Numbering::Symbol, + Some(_) => {} + None => Err("invalid pattern")?, + } + }; + let upper = s.scout(-1).map_or(false, char::is_uppercase); + let suffix = s.after().into(); + Ok(Self::Pattern(prefix.into(), numbering, upper, suffix)) + } + Value::Content(v) => Ok(Self::Content(v)), + Value::Func(v) => Ok(Self::Func(v, value.span)), + v => Err(format!( + "expected string, content or function, found {}", + v.type_name(), + )), + } + } +} |
