summaryrefslogtreecommitdiff
path: root/library/src/basics
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-11-29 13:37:25 +0100
committerLaurenz <laurmaedje@gmail.com>2022-11-29 14:18:13 +0100
commit0efe669278a5e1c3f2985eba2f3360e91159c54a (patch)
tree502712857c48f0decb5e698257c0a96d358a436e /library/src/basics
parent836692e73cff0356e409a9ba5b4887b86809d4ca (diff)
Reorganize library and tests
Diffstat (limited to 'library/src/basics')
-rw-r--r--library/src/basics/heading.rs62
-rw-r--r--library/src/basics/list.rs272
-rw-r--r--library/src/basics/mod.rs9
-rw-r--r--library/src/basics/table.rs136
4 files changed, 479 insertions, 0 deletions
diff --git a/library/src/basics/heading.rs b/library/src/basics/heading.rs
new file mode 100644
index 00000000..b251f27b
--- /dev/null
+++ b/library/src/basics/heading.rs
@@ -0,0 +1,62 @@
+use typst::font::FontWeight;
+
+use crate::layout::{BlockNode, VNode};
+use crate::prelude::*;
+use crate::text::{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, Finalize)]
+impl HeadingNode {
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self {
+ body: args.expect("body")?,
+ level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()),
+ }
+ .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,
+ }
+ }
+}
+
+impl Show for HeadingNode {
+ fn show(&self, _: Tracked<dyn World>, _: StyleChain) -> Content {
+ BlockNode(self.body.clone()).pack()
+ }
+}
+
+impl Finalize for HeadingNode {
+ fn finalize(&self, realized: Content) -> Content {
+ let scale = match self.level.get() {
+ 1 => 1.4,
+ 2 => 1.2,
+ _ => 1.0,
+ };
+
+ let size = Em::new(scale);
+ let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }) / scale;
+ let below = Em::new(0.66) / scale;
+
+ let mut map = StyleMap::new();
+ map.set(TextNode::SIZE, TextSize(size.into()));
+ map.set(TextNode::WEIGHT, FontWeight::BOLD);
+ map.set(BlockNode::ABOVE, VNode::block_around(above.into()));
+ map.set(BlockNode::BELOW, VNode::block_around(below.into()));
+ map.set(BlockNode::STICKY, true);
+ realized.styled_with_map(map)
+ }
+}
diff --git a/library/src/basics/list.rs b/library/src/basics/list.rs
new file mode 100644
index 00000000..c73ffea6
--- /dev/null
+++ b/library/src/basics/list.rs
@@ -0,0 +1,272 @@
+use crate::compute::NumberingPattern;
+use crate::layout::{BlockNode, GridNode, HNode, ParNode, Spacing, TrackSizing};
+use crate::prelude::*;
+use crate::text::{SpaceNode, TextNode};
+
+/// 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,
+ /// 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(Layout)]
+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 between the items of a wide (non-tight) list.
+ pub const SPACING: Smart<Spacing> = Smart::Auto;
+
+ fn construct(_: &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), items }.pack())
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "tight" => Some(Value::Bool(self.tight)),
+ "items" => {
+ Some(Value::Array(self.items.items().map(|item| item.encode()).collect()))
+ }
+ _ => None,
+ }
+ }
+}
+
+impl<const L: ListKind> Layout for ListNode<L> {
+ fn layout(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ regions: &Regions,
+ ) -> SourceResult<Fragment> {
+ 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).into()
+ } else {
+ styles
+ .get(Self::SPACING)
+ .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount)
+ };
+
+ 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::packed(':')).strong(),
+ SpaceNode.pack(),
+ item.body.clone(),
+ ]),
+ };
+
+ cells.push(body.styled_with_map(map.clone()));
+ number += 1;
+ }
+
+ GridNode {
+ tracks: Axes::with_x(vec![
+ TrackSizing::Relative(indent.into()),
+ TrackSizing::Auto,
+ TrackSizing::Relative(body_indent.into()),
+ TrackSizing::Auto,
+ ]),
+ gutter: Axes::with_y(vec![gutter.into()]),
+ cells,
+ }
+ .layout(world, styles, regions)
+ }
+}
+
+/// An item in a list.
+#[derive(Debug, Clone, 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,
+ }
+ }
+
+ /// 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()),
+ }),
+ }
+ }
+}
+
+#[node]
+impl ListItem {}
+
+/// A description list item.
+#[derive(Debug, Clone, 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 }
+ },
+}
+
+/// 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(NumberingPattern),
+ /// Bare content.
+ Content(Content),
+ /// A closure mapping from an item number to a value.
+ Func(Func, Span),
+}
+
+impl Label {
+ /// Resolve the label 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::packed('•'),
+ ENUM => TextNode::packed(format_eco!("{}.", number)),
+ DESC | _ => panic!("description lists don't have a label"),
+ },
+ Self::Pattern(pattern) => TextNode::packed(pattern.apply(number)),
+ 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()
+ }
+ })
+ }
+}
+
+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(v) => Ok(Self::Pattern(v.parse()?)),
+ Value::Content(v) => Ok(Self::Content(v)),
+ Value::Func(v) => Ok(Self::Func(v, value.span)),
+ v => Err(format_eco!(
+ "expected string, content or function, found {}",
+ v.type_name(),
+ )),
+ }
+ }
+}
diff --git a/library/src/basics/mod.rs b/library/src/basics/mod.rs
new file mode 100644
index 00000000..5916df6b
--- /dev/null
+++ b/library/src/basics/mod.rs
@@ -0,0 +1,9 @@
+//! Common document elements.
+
+mod heading;
+mod list;
+mod table;
+
+pub use self::heading::*;
+pub use self::list::*;
+pub use self::table::*;
diff --git a/library/src/basics/table.rs b/library/src/basics/table.rs
new file mode 100644
index 00000000..bb900f3d
--- /dev/null
+++ b/library/src/basics/table.rs
@@ -0,0 +1,136 @@
+use crate::layout::{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(Layout)]
+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();
+
+ fn construct(_: &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())
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "cells" => Some(Value::Array(
+ self.cells.iter().cloned().map(Value::Content).collect(),
+ )),
+ _ => None,
+ }
+ }
+}
+
+impl Layout for TableNode {
+ fn layout(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ regions: &Regions,
+ ) -> SourceResult<Fragment> {
+ 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<_>>()?;
+
+ GridNode {
+ tracks: self.tracks.clone(),
+ gutter: self.gutter.clone(),
+ cells,
+ }
+ .layout(world, styles, regions)
+ }
+}
+
+/// 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")),
+ }
+ }
+}