diff options
Diffstat (limited to 'library/src/structure')
| -rw-r--r-- | library/src/structure/doc.rs | 29 | ||||
| -rw-r--r-- | library/src/structure/heading.rs | 176 | ||||
| -rw-r--r-- | library/src/structure/list.rs | 371 | ||||
| -rw-r--r-- | library/src/structure/mod.rs | 13 | ||||
| -rw-r--r-- | library/src/structure/reference.rs | 29 | ||||
| -rw-r--r-- | library/src/structure/table.rs | 160 |
6 files changed, 778 insertions, 0 deletions
diff --git a/library/src/structure/doc.rs b/library/src/structure/doc.rs new file mode 100644 index 00000000..ac12c3ab --- /dev/null +++ b/library/src/structure/doc.rs @@ -0,0 +1,29 @@ +use crate::layout::PageNode; +use crate::prelude::*; + +/// A sequence of page runs. +#[derive(Hash)] +pub struct DocNode(pub StyleVec<PageNode>); + +impl DocNode { + /// Layout the document into a sequence of frames, one per page. + pub fn layout( + &self, + world: Tracked<dyn World>, + styles: StyleChain, + ) -> SourceResult<Vec<Frame>> { + let mut frames = vec![]; + for (page, map) in self.0.iter() { + let number = 1 + frames.len(); + frames.extend(page.layout(world, number, map.chain(&styles))?); + } + Ok(frames) + } +} + +impl Debug for DocNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Doc ")?; + self.0.fmt(f) + } +} diff --git a/library/src/structure/heading.rs b/library/src/structure/heading.rs new file mode 100644 index 00000000..62a67000 --- /dev/null +++ b/library/src/structure/heading.rs @@ -0,0 +1,176 @@ +use crate::layout::{BlockNode, BlockSpacing}; +use crate::prelude::*; +use crate::text::{FontFamily, TextNode, TextSize}; + +/// 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: NonZeroUsize, + /// The heading's contents. + pub body: Content, +} + +#[node(Show)] +impl HeadingNode { + /// The heading's font family. Just the normal text family if `auto`. + #[property(referenced)] + pub const FAMILY: Leveled<Smart<FontFamily>> = Leveled::Value(Smart::Auto); + /// The color of text in the heading. Just the normal text color if `auto`. + #[property(referenced)] + pub const FILL: Leveled<Smart<Paint>> = Leveled::Value(Smart::Auto); + /// The size of text in the heading. + #[property(referenced)] + pub const SIZE: Leveled<TextSize> = Leveled::Mapping(|level| { + let size = match level.get() { + 1 => 1.4, + 2 => 1.2, + _ => 1.0, + }; + TextSize(Em::new(size).into()) + }); + + /// Whether text in the heading is strengthend. + #[property(referenced)] + pub const STRONG: Leveled<bool> = Leveled::Value(true); + /// Whether text in the heading is emphasized. + #[property(referenced)] + pub const EMPH: Leveled<bool> = Leveled::Value(false); + /// Whether the heading is underlined. + #[property(referenced)] + pub const UNDERLINE: Leveled<bool> = Leveled::Value(false); + + /// The spacing above the heading. + #[property(referenced, shorthand(around))] + pub const ABOVE: Leveled<Option<BlockSpacing>> = Leveled::Mapping(|level| { + let ratio = match level.get() { + 1 => 1.5, + _ => 1.2, + }; + Some(Ratio::new(ratio).into()) + }); + /// The spacing below the heading. + #[property(referenced, shorthand(around))] + pub const BELOW: Leveled<Option<BlockSpacing>> = + Leveled::Value(Some(Ratio::new(0.55).into())); + + /// Whether the heading appears in the outline. + pub const OUTLINED: bool = true; + /// Whether the heading is numbered. + pub const NUMBERED: bool = true; + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { + Ok(Self { + body: args.expect("body")?, + level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()), + } + .pack()) + } +} + +impl Show for HeadingNode { + fn unguard_parts(&self, sel: Selector) -> Content { + Self { body: self.body.unguard(sel), ..*self }.pack() + } + + fn field(&self, name: &str) -> Option<Value> { + match name { + "level" => Some(Value::Int(self.level.get() as i64)), + "body" => Some(Value::Content(self.body.clone())), + _ => None, + } + } + + fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> { + Ok(BlockNode(self.body.clone()).pack()) + } + + fn finalize( + &self, + world: Tracked<dyn World>, + styles: StyleChain, + mut realized: Content, + ) -> SourceResult<Content> { + macro_rules! resolve { + ($key:expr) => { + styles.get($key).resolve(world, self.level)? + }; + } + + let mut map = StyleMap::new(); + map.set(TextNode::SIZE, resolve!(Self::SIZE)); + + if let Smart::Custom(family) = resolve!(Self::FAMILY) { + map.set_family(family, styles); + } + + if let Smart::Custom(fill) = resolve!(Self::FILL) { + map.set(TextNode::FILL, fill); + } + + if resolve!(Self::STRONG) { + realized = realized.strong(); + } + + if resolve!(Self::EMPH) { + realized = realized.emph(); + } + + if resolve!(Self::UNDERLINE) { + realized = realized.underlined(); + } + + realized = realized.styled_with_map(map); + realized = realized.spaced( + resolve!(Self::ABOVE).resolve(styles), + resolve!(Self::BELOW).resolve(styles), + ); + + Ok(realized) + } +} + +/// 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(NonZeroUsize) -> T), + /// A closure mapping from a heading level to a value. + Func(Func, Span), +} + +impl<T: Cast + Clone> Leveled<T> { + /// Resolve the value based on the level. + pub fn resolve( + &self, + world: Tracked<dyn World>, + level: NonZeroUsize, + ) -> SourceResult<T> { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Mapping(mapping) => mapping(level), + Self::Func(func, span) => { + let args = Args::new(*span, [Value::Int(level.get() as i64)]); + func.call_detached(world, 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/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(), + )), + } + } +} diff --git a/library/src/structure/mod.rs b/library/src/structure/mod.rs new file mode 100644 index 00000000..088d1e6c --- /dev/null +++ b/library/src/structure/mod.rs @@ -0,0 +1,13 @@ +//! Document structuring. + +mod doc; +mod heading; +mod list; +mod reference; +mod table; + +pub use doc::*; +pub use heading::*; +pub use list::*; +pub use reference::*; +pub use table::*; diff --git a/library/src/structure/reference.rs b/library/src/structure/reference.rs new file mode 100644 index 00000000..632ecba5 --- /dev/null +++ b/library/src/structure/reference.rs @@ -0,0 +1,29 @@ +use crate::prelude::*; + +/// A reference to a label. +#[derive(Debug, Hash)] +pub struct RefNode(pub EcoString); + +#[node(Show)] +impl RefNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { + Ok(Self(args.expect("label")?).pack()) + } +} + +impl Show for RefNode { + fn unguard_parts(&self, _: Selector) -> Content { + Self(self.0.clone()).pack() + } + + fn field(&self, name: &str) -> Option<Value> { + match name { + "label" => Some(Value::Str(self.0.clone().into())), + _ => None, + } + } + + fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> { + Ok(TextNode(format_eco!("@{}", self.0)).pack()) + } +} diff --git a/library/src/structure/table.rs b/library/src/structure/table.rs new file mode 100644 index 00000000..722f11e6 --- /dev/null +++ b/library/src/structure/table.rs @@ -0,0 +1,160 @@ +use crate::layout::{BlockSpacing, GridNode, TrackSizing, TrackSizings}; +use crate::prelude::*; + +/// A table of items. +#[derive(Debug, Hash)] +pub struct TableNode { + /// Defines sizing for content rows and columns. + pub tracks: Axes<Vec<TrackSizing>>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Axes<Vec<TrackSizing>>, + /// The content to be arranged in the table. + pub cells: Vec<Content>, +} + +#[node(Show)] +impl TableNode { + /// How to fill the cells. + #[property(referenced)] + pub const FILL: Celled<Option<Paint>> = Celled::Value(None); + /// How to stroke the cells. + #[property(resolve, fold)] + pub const STROKE: Option<PartialStroke> = Some(PartialStroke::default()); + /// How much to pad the cells's content. + pub const PADDING: Rel<Length> = Abs::pt(5.0).into(); + + /// The spacing above the table. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into()); + /// The spacing below the table. + #[property(resolve, shorthand(around))] + pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into()); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { + let TrackSizings(columns) = args.named("columns")?.unwrap_or_default(); + let TrackSizings(rows) = args.named("rows")?.unwrap_or_default(); + let TrackSizings(base_gutter) = args.named("gutter")?.unwrap_or_default(); + let column_gutter = args.named("column-gutter")?.map(|TrackSizings(v)| v); + let row_gutter = args.named("row-gutter")?.map(|TrackSizings(v)| v); + Ok(Self { + tracks: Axes::new(columns, rows), + gutter: Axes::new( + column_gutter.unwrap_or_else(|| base_gutter.clone()), + row_gutter.unwrap_or(base_gutter), + ), + cells: args.all()?, + } + .pack()) + } +} + +impl Show for TableNode { + fn unguard_parts(&self, sel: Selector) -> Content { + Self { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + cells: self.cells.iter().map(|cell| cell.unguard(sel)).collect(), + } + .pack() + } + + fn field(&self, name: &str) -> Option<Value> { + match name { + "cells" => Some(Value::Array( + self.cells.iter().cloned().map(Value::Content).collect(), + )), + _ => None, + } + } + + fn realize( + &self, + world: Tracked<dyn World>, + styles: StyleChain, + ) -> SourceResult<Content> { + let fill = styles.get(Self::FILL); + let stroke = styles.get(Self::STROKE).map(PartialStroke::unwrap_or_default); + let padding = styles.get(Self::PADDING); + + let cols = self.tracks.x.len().max(1); + let cells = self + .cells + .iter() + .cloned() + .enumerate() + .map(|(i, child)| { + let mut child = child.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) = fill.resolve(world, x, y)? { + child = child.filled(fill); + } + + Ok(child) + }) + .collect::<SourceResult<_>>()?; + + Ok(GridNode { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + cells, + } + .pack()) + } + + fn finalize( + &self, + _: Tracked<dyn World>, + styles: StyleChain, + realized: Content, + ) -> SourceResult<Content> { + Ok(realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))) + } +} + +/// A value that can be configured per cell. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Celled<T> { + /// A bare value, the same for all cells. + Value(T), + /// A closure mapping from cell coordinates to a value. + Func(Func, Span), +} + +impl<T: Cast + Clone> Celled<T> { + /// Resolve the value based on the cell position. + pub fn resolve( + &self, + world: Tracked<dyn World>, + x: usize, + y: usize, + ) -> SourceResult<T> { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Func(func, span) => { + let args = Args::new(*span, [Value::Int(x as i64), Value::Int(y as i64)]); + func.call_detached(world, args)?.cast().at(*span)? + } + }) + } +} + +impl<T: Cast> Cast<Spanned<Value>> for Celled<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")), + } + } +} |
