diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-03-11 11:58:56 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-03-11 11:58:56 +0100 |
| commit | e6b532391deb1e30dc356c4d20dd48199f748f29 (patch) | |
| tree | 7b631414931164b9a47c9d154172195fc0e1316c /src/library/structure | |
| parent | b71113d37a29bab5c7dc4b501c33ee9afbdb8213 (diff) | |
More restructuring
Diffstat (limited to 'src/library/structure')
| -rw-r--r-- | src/library/structure/heading.rs | 150 | ||||
| -rw-r--r-- | src/library/structure/list.rs | 208 | ||||
| -rw-r--r-- | src/library/structure/mod.rs | 9 | ||||
| -rw-r--r-- | src/library/structure/table.rs | 100 |
4 files changed, 467 insertions, 0 deletions
diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs new file mode 100644 index 00000000..a67f4f24 --- /dev/null +++ b/src/library/structure/heading.rs @@ -0,0 +1,150 @@ +use crate::library::prelude::*; +use crate::library::text::{FontFamily, TextNode}; + +/// A section heading. +#[derive(Debug, Hash)] +pub struct HeadingNode { + /// The logical nesting depth of the section, starting from one. In the + /// default style, this controls the text size of the heading. + pub level: usize, + /// The heading's contents. + pub body: Template, +} + +#[class] +impl HeadingNode { + /// The heading's font family. Just the normal text family if `auto`. + pub const FAMILY: Leveled<Smart<FontFamily>> = Leveled::Value(Smart::Auto); + /// The color of text in the heading. Just the normal text color if `auto`. + pub const FILL: Leveled<Smart<Paint>> = Leveled::Value(Smart::Auto); + /// The size of text in the heading. + pub const SIZE: Leveled<Linear> = Leveled::Mapping(|level| { + let upscale = (1.6 - 0.1 * level as f64).max(0.75); + Relative::new(upscale).into() + }); + /// Whether text in the heading is strengthend. + pub const STRONG: Leveled<bool> = Leveled::Value(true); + /// Whether text in the heading is emphasized. + pub const EMPH: Leveled<bool> = Leveled::Value(false); + /// Whether the heading is underlined. + pub const UNDERLINE: Leveled<bool> = Leveled::Value(false); + /// The extra padding above the heading. + pub const ABOVE: Leveled<Length> = Leveled::Value(Length::zero()); + /// The extra padding below the heading. + pub const BELOW: Leveled<Length> = Leveled::Value(Length::zero()); + /// Whether the heading is block-level. + pub const BLOCK: Leveled<bool> = Leveled::Value(true); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::show(Self { + body: args.expect("body")?, + level: args.named("level")?.unwrap_or(1), + })) + } +} + +impl Show for HeadingNode { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { + macro_rules! resolve { + ($key:expr) => { + styles.get_cloned($key).resolve(ctx, self.level)? + }; + } + + // Resolve the user recipe. + let mut body = styles + .show(self, ctx, [ + Value::Int(self.level as i64), + Value::Template(self.body.clone()), + ])? + .unwrap_or_else(|| self.body.clone()); + + let mut map = StyleMap::new(); + map.set(TextNode::SIZE, resolve!(Self::SIZE)); + + if let Smart::Custom(family) = resolve!(Self::FAMILY) { + map.set( + TextNode::FAMILY, + std::iter::once(family) + .chain(styles.get_ref(TextNode::FAMILY).iter().cloned()) + .collect(), + ); + } + + if let Smart::Custom(fill) = resolve!(Self::FILL) { + map.set(TextNode::FILL, fill); + } + + if resolve!(Self::STRONG) { + map.set(TextNode::STRONG, true); + } + + if resolve!(Self::EMPH) { + map.set(TextNode::EMPH, true); + } + + let mut seq = vec![]; + if resolve!(Self::UNDERLINE) { + body = body.underlined(); + } + + let above = resolve!(Self::ABOVE); + if !above.is_zero() { + seq.push(Template::Vertical(above.into())); + } + + seq.push(body); + + let below = resolve!(Self::BELOW); + if !below.is_zero() { + seq.push(Template::Vertical(below.into())); + } + + let mut template = Template::sequence(seq).styled_with_map(map); + if resolve!(Self::BLOCK) { + template = Template::block(template); + } + + Ok(template) + } +} + +/// Either the value or a closure mapping to the value. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Leveled<T> { + /// A bare value. + Value(T), + /// A simple mapping from a heading level to a value. + Mapping(fn(usize) -> T), + /// A closure mapping from a heading level to a value. + Func(Func, Span), +} + +impl<T: Cast> Leveled<T> { + /// Resolve the value based on the level. + pub fn resolve(self, ctx: &mut Context, level: usize) -> TypResult<T> { + Ok(match self { + Self::Value(value) => value, + Self::Mapping(mapping) => mapping(level), + Self::Func(func, span) => { + let args = Args::from_values(span, [Value::Int(level as i64)]); + func.call(ctx, args)?.cast().at(span)? + } + }) + } +} + +impl<T: Cast> Cast<Spanned<Value>> for Leveled<T> { + fn is(value: &Spanned<Value>) -> bool { + matches!(&value.v, Value::Func(_)) || T::is(&value.v) + } + + fn cast(value: Spanned<Value>) -> StrResult<Self> { + match value.v { + Value::Func(v) => Ok(Self::Func(v, value.span)), + v => T::cast(v) + .map(Self::Value) + .map_err(|msg| with_alternative(msg, "function")), + } + } +} diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs new file mode 100644 index 00000000..2c536e2a --- /dev/null +++ b/src/library/structure/list.rs @@ -0,0 +1,208 @@ +use crate::library::layout::{GridNode, TrackSizing}; +use crate::library::prelude::*; +use crate::library::text::{ParNode, TextNode}; +use crate::library::utility::Numbering; +use crate::parse::Scanner; + +/// An unordered (bulleted) or ordered (numbered) list. +#[derive(Debug, Hash)] +pub struct ListNode<const L: ListKind = UNORDERED> { + /// Where the list starts. + pub start: usize, + /// If true, there is paragraph spacing between the items, if false + /// there is list spacing between the items. + pub wide: bool, + /// The individual bulleted or numbered items. + pub items: Vec<ListItem>, +} + +/// An item in a list. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct ListItem { + /// The number of the item. + pub number: Option<usize>, + /// The node that produces the item's body. + pub body: Box<Template>, +} + +/// An ordered list. +pub type EnumNode = ListNode<ORDERED>; + +#[class] +impl<const L: ListKind> ListNode<L> { + /// How the list is labelled. + pub const LABEL: Label = Label::Default; + /// The spacing between the list items of a non-wide list. + pub const SPACING: Linear = Linear::zero(); + /// The indentation of each item's label. + pub const INDENT: Linear = Relative::new(0.0).into(); + /// The space between the label and the body of each item. + pub const BODY_INDENT: Linear = Relative::new(0.5).into(); + /// The extra padding above the list. + pub const ABOVE: Length = Length::zero(); + /// The extra padding below the list. + pub const BELOW: Length = Length::zero(); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::show(Self { + start: args.named("start")?.unwrap_or(0), + wide: args.named("wide")?.unwrap_or(false), + items: args + .all()? + .into_iter() + .map(|body| ListItem { number: None, body: Box::new(body) }) + .collect(), + })) + } +} + +impl<const L: ListKind> Show for ListNode<L> { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { + let template = if let Some(template) = styles.show( + self, + ctx, + self.items.iter().map(|item| Value::Template((*item.body).clone())), + )? { + template + } else { + let mut children = vec![]; + let mut number = self.start; + + let label = styles.get_ref(Self::LABEL); + + for item in &self.items { + number = item.number.unwrap_or(number); + children.push(LayoutNode::default()); + children.push(label.resolve(ctx, L, number)?.pack()); + children.push(LayoutNode::default()); + children.push((*item.body).clone().pack()); + number += 1; + } + + let em = styles.get(TextNode::SIZE).abs; + let leading = styles.get(ParNode::LEADING); + let spacing = if self.wide { + styles.get(ParNode::SPACING) + } else { + styles.get(Self::SPACING) + }; + + let gutter = (leading + spacing).resolve(em); + let indent = styles.get(Self::INDENT).resolve(em); + let body_indent = styles.get(Self::BODY_INDENT).resolve(em); + + Template::block(GridNode { + tracks: Spec::with_x(vec![ + TrackSizing::Linear(indent.into()), + TrackSizing::Auto, + TrackSizing::Linear(body_indent.into()), + TrackSizing::Auto, + ]), + gutter: Spec::with_y(vec![TrackSizing::Linear(gutter.into())]), + children, + }) + }; + + let mut seq = vec![]; + let above = styles.get(Self::ABOVE); + if !above.is_zero() { + seq.push(Template::Vertical(above.into())); + } + + seq.push(template); + + let below = styles.get(Self::BELOW); + if !below.is_zero() { + seq.push(Template::Vertical(below.into())); + } + + Ok(Template::sequence(seq)) + } +} + +impl<const L: ListKind> From<ListItem> for ListNode<L> { + fn from(item: ListItem) -> Self { + Self { items: vec![item], wide: false, start: 1 } + } +} + +/// 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; + +/// Either a template or a closure mapping to a template. +#[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), + /// A bare template. + Template(Template), + /// 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<Template> { + Ok(match self { + Self::Default => match kind { + UNORDERED => Template::Text('•'.into()), + ORDERED | _ => Template::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() }; + Template::Text(format_eco!("{}{}{}", prefix, mid, suffix)) + } + Self::Template(template) => template.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<Spanned<Value>> for Label { + fn is(value: &Spanned<Value>) -> bool { + matches!(&value.v, Value::Template(_) | Value::Func(_)) + } + + fn cast(value: Spanned<Value>) -> StrResult<Self> { + match value.v { + Value::Str(pattern) => { + let mut s = Scanner::new(&pattern); + let mut prefix; + let numbering = loop { + prefix = s.eaten(); + 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.prev(0).map_or(false, char::is_uppercase); + let suffix = s.rest().into(); + Ok(Self::Pattern(prefix.into(), numbering, upper, suffix)) + } + Value::Template(v) => Ok(Self::Template(v)), + Value::Func(v) => Ok(Self::Func(v, value.span)), + _ => Err("expected pattern, template or function")?, + } + } +} diff --git a/src/library/structure/mod.rs b/src/library/structure/mod.rs new file mode 100644 index 00000000..073d52e9 --- /dev/null +++ b/src/library/structure/mod.rs @@ -0,0 +1,9 @@ +//! Document structuring. + +mod heading; +mod list; +mod table; + +pub use heading::*; +pub use list::*; +pub use table::*; diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs new file mode 100644 index 00000000..555dcc44 --- /dev/null +++ b/src/library/structure/table.rs @@ -0,0 +1,100 @@ +use crate::library::layout::{GridNode, TrackSizing}; +use crate::library::prelude::*; + +/// A table of items. +#[derive(Debug, Hash)] +pub struct TableNode { + /// Defines sizing for content rows and columns. + pub tracks: Spec<Vec<TrackSizing>>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Spec<Vec<TrackSizing>>, + /// The nodes to be arranged in the table. + pub children: Vec<Template>, +} + +#[class] +impl TableNode { + /// The primary cell fill color. + pub const PRIMARY: Option<Paint> = None; + /// The secondary cell fill color. + pub const SECONDARY: Option<Paint> = None; + /// How the stroke the cells. + pub const STROKE: Option<Paint> = Some(Color::BLACK.into()); + /// The stroke's thickness. + pub const THICKNESS: Length = Length::pt(1.0); + /// How much to pad the cells's content. + pub const PADDING: Linear = Length::pt(5.0).into(); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let columns = args.named("columns")?.unwrap_or_default(); + let rows = args.named("rows")?.unwrap_or_default(); + let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default(); + let column_gutter = args.named("column-gutter")?; + let row_gutter = args.named("row-gutter")?; + Ok(Template::show(Self { + tracks: Spec::new(columns, rows), + gutter: Spec::new( + column_gutter.unwrap_or_else(|| base_gutter.clone()), + row_gutter.unwrap_or(base_gutter), + ), + children: args.all()?, + })) + } + + fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { + let fill = args.named("fill")?; + styles.set_opt(Self::PRIMARY, args.named("primary")?.or(fill)); + styles.set_opt(Self::SECONDARY, args.named("secondary")?.or(fill)); + styles.set_opt(Self::STROKE, args.named("stroke")?); + styles.set_opt(Self::THICKNESS, args.named("thickness")?); + styles.set_opt(Self::PADDING, args.named("padding")?); + Ok(()) + } +} + +impl Show for TableNode { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { + if let Some(template) = styles.show( + self, + ctx, + self.children.iter().map(|child| Value::Template(child.clone())), + )? { + return Ok(template); + } + + let primary = styles.get(Self::PRIMARY); + let secondary = styles.get(Self::SECONDARY); + let thickness = styles.get(Self::THICKNESS); + let stroke = styles.get(Self::STROKE).map(|paint| Stroke { paint, thickness }); + let padding = styles.get(Self::PADDING); + + let cols = self.tracks.x.len().max(1); + let children = self + .children + .iter() + .cloned() + .enumerate() + .map(|(i, child)| { + let mut child = child.pack().padded(Sides::splat(padding)); + + if let Some(stroke) = stroke { + child = child.stroked(stroke); + } + + let x = i % cols; + let y = i / cols; + if let Some(fill) = [primary, secondary][(x + y) % 2] { + child = child.filled(fill); + } + + child + }) + .collect(); + + Ok(Template::block(GridNode { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + children, + })) + } +} |
