use std::fmt::Write; use unscanny::Scanner; use crate::library::layout::{GridNode, TrackSizing}; use crate::library::prelude::*; use crate::library::text::ParNode; use crate::library::utility::Numbering; /// An unordered (bulleted) or ordered (numbered) list. #[derive(Debug, Hash)] pub struct ListNode { /// Where the list starts. pub start: usize, /// If false, there is paragraph spacing between the items, if true /// there is list spacing between the items. pub tight: bool, /// The individual bulleted or numbered items. pub items: StyleVec, } /// An item in a list. #[derive(Clone, PartialEq, Hash)] pub struct ListItem { /// The kind of item. pub kind: ListKind, /// The number of the item. pub number: Option, /// The node that produces the item's body. pub body: Box, } /// An ordered list. pub type EnumNode = ListNode; #[node(showable)] impl ListNode { /// How the list is labelled. #[property(referenced)] pub const LABEL: Label = Label::Default; /// The spacing between the list items of a non-wide list. #[property(resolve)] pub const SPACING: RawLength = RawLength::zero(); /// The indentation of each item's label. #[property(resolve)] pub const INDENT: RawLength = RawLength::zero(); /// The space between the label and the body of each item. #[property(resolve)] pub const BODY_INDENT: RawLength = Em::new(0.5).into(); /// The extra padding above the list. #[property(resolve)] pub const ABOVE: RawLength = RawLength::zero(); /// The extra padding below the list. #[property(resolve)] pub const BELOW: RawLength = RawLength::zero(); fn construct(_: &mut Context, args: &mut Args) -> TypResult { Ok(Content::show(Self { start: args.named("start")?.unwrap_or(1), tight: args.named("tight")?.unwrap_or(true), items: args .all()? .into_iter() .map(|body| ListItem { kind: L, number: None, body: Box::new(body), }) .collect(), })) } } impl Show for ListNode { fn encode(&self) -> Dict { dict! { "start" => Value::Int(self.start as i64), "tight" => Value::Bool(self.tight), "items" => Value::Array( self.items .items() .map(|item| Value::Content((*item.body).clone())) .collect() ), } } fn realize(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { let mut cells = vec![]; let mut number = self.start; let label = styles.get(Self::LABEL); for (item, map) in self.items.iter() { number = item.number.unwrap_or(number); cells.push(LayoutNode::default()); cells .push(label.resolve(ctx, L, number)?.styled_with_map(map.clone()).pack()); cells.push(LayoutNode::default()); cells.push((*item.body).clone().styled_with_map(map.clone()).pack()); number += 1; } let leading = styles.get(ParNode::LEADING); let spacing = if self.tight { styles.get(Self::SPACING) } else { styles.get(ParNode::SPACING) }; let gutter = leading + spacing; let indent = styles.get(Self::INDENT); let body_indent = styles.get(Self::BODY_INDENT); Ok(Content::block(GridNode { tracks: Spec::with_x(vec![ TrackSizing::Relative(indent.into()), TrackSizing::Auto, TrackSizing::Relative(body_indent.into()), TrackSizing::Auto, ]), gutter: Spec::with_y(vec![TrackSizing::Relative(gutter.into())]), cells, })) } fn finalize( &self, _: &mut Context, styles: StyleChain, realized: Content, ) -> TypResult { Ok(realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))) } } impl Debug for ListItem { fn fmt(&self, f: &mut Formatter) -> fmt::Result { if self.kind == UNORDERED { f.write_char('-')?; } else { if let Some(number) = self.number { write!(f, "{}", number)?; } f.write_char('.')?; } f.write_char(' ')?; self.body.fmt(f) } } /// How to label a list. pub type ListKind = usize; /// Unordered list labelling style. pub const UNORDERED: ListKind = 0; /// Ordered list labelling style. pub const ORDERED: ListKind = 1; /// 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, ctx: &mut Context, kind: ListKind, number: usize, ) -> TypResult { Ok(match self { Self::Default => match kind { UNORDERED => Content::Text('•'.into()), ORDERED | _ => Content::Text(format_eco!("{}.", number)), }, Self::Pattern(prefix, numbering, upper, suffix) => { let fmt = numbering.apply(number); let mid = if *upper { fmt.to_uppercase() } else { fmt.to_lowercase() }; Content::Text(format_eco!("{}{}{}", prefix, mid, suffix)) } Self::Content(content) => content.clone(), Self::Func(func, span) => { let args = Args::from_values(*span, [Value::Int(number as i64)]); func.call(ctx, args)?.cast().at(*span)? } }) } } 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::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)), _ => Err("expected pattern, content or function")?, } } }