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 { /// 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, } /// An ordered list. pub type EnumNode = ListNode; /// A description list. pub type DescNode = ListNode; #[node(Show)] impl ListNode { /// 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 = Some(Ratio::one().into()); /// The spacing below the list. #[property(resolve, shorthand(around))] pub const BELOW: Option = 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 { 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 Show for ListNode { fn unguard_parts(&self, sel: Selector) -> Content { Self { items: self.items.map(|item| item.unguard(sel)), ..*self } .pack() } fn field(&self, name: &str) -> Option { 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, styles: StyleChain, ) -> SourceResult { 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, styles: StyleChain, realized: Content, ) -> SourceResult { 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), /// An item of an ordered list. Enum(Option, Box), /// An item of a description list. Desc(Box), } 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, kind: ListKind, number: usize, ) -> SourceResult { 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> for Label { fn is(value: &Spanned) -> bool { matches!(&value.v, Value::Content(_) | Value::Func(_)) } fn cast(value: Spanned) -> StrResult { 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(), )), } } }