summaryrefslogtreecommitdiff
path: root/library/src/layout
diff options
context:
space:
mode:
Diffstat (limited to 'library/src/layout')
-rw-r--r--library/src/layout/enum.rs206
-rw-r--r--library/src/layout/list.rs160
-rw-r--r--library/src/layout/mod.rs10
-rw-r--r--library/src/layout/table.rs236
-rw-r--r--library/src/layout/terms.rs178
5 files changed, 789 insertions, 1 deletions
diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs
new file mode 100644
index 00000000..b1b18680
--- /dev/null
+++ b/library/src/layout/enum.rs
@@ -0,0 +1,206 @@
+use std::str::FromStr;
+
+use crate::compute::{Numbering, NumberingPattern};
+use crate::layout::{BlockNode, GridNode, ParNode, Spacing, TrackSizing};
+use crate::prelude::*;
+
+/// # Numbered List
+/// A numbered list.
+///
+/// Displays a sequence of items vertically and numbers them consecutively.
+///
+/// ## Example
+/// ```example
+/// Automatically numbered:
+/// + Preparations
+/// + Analysis
+/// + Conclusions
+///
+/// Manually numbered:
+/// 2. What is the first step?
+/// 5. I am confused.
+/// + Moving on ...
+///
+/// Function call.
+/// #enum[First][Second]
+/// ```
+///
+/// ## Syntax
+/// This functions also has dedicated syntax:
+///
+/// - Starting a line with a plus sign creates an automatically numbered
+/// enumeration item.
+/// - Start a line with a number followed by a dot creates an explicitly
+/// numbered enumeration item.
+///
+/// Enumeration items can contain multiple paragraphs and other block-level
+/// content. All content that is indented more than an item's plus sign or dot
+/// becomes part of that item.
+///
+/// ## Parameters
+/// - items: `Content` (positional, variadic)
+/// The enumeration's children.
+///
+/// When using the enum syntax, adjacent items are automatically collected
+/// into enumerations, even through constructs like for loops.
+///
+/// ```example
+/// #for phase in (
+/// "Launch",
+/// "Orbit",
+/// "Descent",
+/// ) [+ #phase]
+/// ```
+///
+/// - start: `NonZeroUsize` (named)
+/// Which number to start the enumeration with.
+///
+/// ```example
+/// #enum(
+/// start: 3,
+/// [Skipping],
+/// [Ahead],
+/// )
+/// ```
+///
+/// - tight: `bool` (named)
+/// If this is `{false}`, the items are spaced apart with
+/// [enum spacing]($func/enum.spacing). If it is `{true}`, they use normal
+/// [leading]($func/par.leading) instead. This makes the enumeration more
+/// compact, which can look better if the items are short.
+///
+/// ```example
+/// + If an enum has a lot of text, and
+/// maybe other inline content, it
+/// should not be tight anymore.
+///
+/// + To make an enum wide, simply
+/// insert a blank line between the
+/// items.
+/// ```
+///
+/// ## Category
+/// layout
+#[func]
+#[capable(Layout)]
+#[derive(Debug, Hash)]
+pub struct EnumNode {
+ /// If true, the items are separated by leading instead of list spacing.
+ pub tight: bool,
+ /// The individual numbered items.
+ pub items: StyleVec<(Option<NonZeroUsize>, Content)>,
+}
+
+#[node]
+impl EnumNode {
+ /// How to number the enumeration. Accepts a
+ /// [numbering pattern or function]($func/numbering).
+ ///
+ /// ```example
+ /// #set enum(numbering: "(a)")
+ ///
+ /// + Different
+ /// + Numbering
+ /// + Style
+ /// ```
+ #[property(referenced)]
+ pub const NUMBERING: Numbering =
+ Numbering::Pattern(NumberingPattern::from_str("1.").unwrap());
+
+ /// The indentation of each item's label.
+ #[property(resolve)]
+ pub const INDENT: Length = Length::zero();
+
+ /// The space between the numbering and the body of each item.
+ #[property(resolve)]
+ pub const BODY_INDENT: Length = Em::new(0.5).into();
+
+ /// The spacing between the items of a wide (non-tight) enumeration.
+ ///
+ /// If set to `{auto}` uses the spacing [below blocks]($func/block.below).
+ pub const SPACING: Smart<Spacing> = Smart::Auto;
+
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ let mut number: NonZeroUsize =
+ args.named("start")?.unwrap_or(NonZeroUsize::new(1).unwrap());
+
+ Ok(Self {
+ tight: args.named("tight")?.unwrap_or(true),
+ items: args
+ .all()?
+ .into_iter()
+ .map(|body| {
+ let item = (Some(number), body);
+ number = number.saturating_add(1);
+ item
+ })
+ .collect(),
+ }
+ .pack())
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "tight" => Some(Value::Bool(self.tight)),
+ "items" => Some(Value::Array(
+ self.items
+ .items()
+ .map(|(number, body)| {
+ Value::Dict(dict! {
+ "number" => match *number {
+ Some(n) => Value::Int(n.get() as i64),
+ None => Value::None,
+ },
+ "body" => Value::Content(body.clone()),
+ })
+ })
+ .collect(),
+ )),
+ _ => None,
+ }
+ }
+}
+
+impl Layout for EnumNode {
+ fn layout(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ let numbering = styles.get(Self::NUMBERING);
+ 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)
+ };
+
+ let mut cells = vec![];
+ let mut number = NonZeroUsize::new(1).unwrap();
+ for ((n, item), map) in self.items.iter() {
+ number = n.unwrap_or(number);
+ let resolved = numbering.apply(vt.world(), &[number])?.display();
+ cells.push(Content::empty());
+ cells.push(resolved.styled_with_map(map.clone()));
+ cells.push(Content::empty());
+ cells.push(item.clone().styled_with_map(map.clone()));
+ number = number.saturating_add(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(vt, styles, regions)
+ }
+}
diff --git a/library/src/layout/list.rs b/library/src/layout/list.rs
new file mode 100644
index 00000000..8bdbe737
--- /dev/null
+++ b/library/src/layout/list.rs
@@ -0,0 +1,160 @@
+use crate::layout::{BlockNode, GridNode, ParNode, Spacing, TrackSizing};
+use crate::prelude::*;
+use crate::text::TextNode;
+
+/// # Bullet List
+/// A bullet list.
+///
+/// Displays a sequence of items vertically, with each item introduced by a
+/// marker.
+///
+/// ## Example
+/// ```example
+/// - *Content*
+/// - Basics
+/// - Text
+/// - Math
+/// - Layout
+/// - Visualize
+/// - Meta
+///
+/// - *Compute*
+/// #list(
+/// [Foundations],
+/// [Calculate],
+/// [Create],
+/// [Data Loading],
+/// [Utility],
+/// )
+/// ```
+///
+/// ## Syntax
+/// This functions also has dedicated syntax: Start a line with a hyphen,
+/// followed by a space to create a list item. A list item can contain multiple
+/// paragraphs and other block-level content. All content that is indented
+/// more than an item's hyphen becomes part of that item.
+///
+/// ## Parameters
+/// - items: `Content` (positional, variadic)
+/// The list's children.
+///
+/// When using the list syntax, adjacent items are automatically collected
+/// into lists, even through constructs like for loops.
+///
+/// ```example
+/// #for letter in "ABC" [
+/// - Letter #letter
+/// ]
+/// ```
+///
+/// - tight: `bool` (named)
+/// If this is `{false}`, the items are spaced apart with [list
+/// spacing]($func/list.spacing). If it is `{true}`, they use normal
+/// [leading]($func/par.leading) instead. This makes the list more compact,
+/// which can look better if the items are short.
+///
+/// ```example
+/// - If a list has a lot of text, and
+/// maybe other inline content, it
+/// should not be tight anymore.
+///
+/// - To make a list wide, simply insert
+/// a blank line between the items.
+/// ```
+///
+/// ## Category
+/// layout
+#[func]
+#[capable(Layout)]
+#[derive(Debug, Hash)]
+pub struct ListNode {
+ /// If true, the items are separated by leading instead of list spacing.
+ pub tight: bool,
+ /// The individual bulleted or numbered items.
+ pub items: StyleVec<Content>,
+}
+
+#[node]
+impl ListNode {
+ /// The marker which introduces each element.
+ ///
+ /// ```example
+ /// #set list(marker: [--])
+ ///
+ /// - A more classic list
+ /// - With en-dashes
+ /// ```
+ #[property(referenced)]
+ pub const MARKER: Content = TextNode::packed('•');
+
+ /// The indent of each item's marker.
+ #[property(resolve)]
+ pub const INDENT: Length = Length::zero();
+
+ /// The spacing between the marker and the body of each item.
+ #[property(resolve)]
+ pub const BODY_INDENT: Length = Em::new(0.5).into();
+
+ /// The spacing between the items of a wide (non-tight) list.
+ ///
+ /// If set to `{auto}` uses the spacing [below blocks]($func/block.below).
+ pub const SPACING: Smart<Spacing> = Smart::Auto;
+
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self {
+ tight: args.named("tight")?.unwrap_or(true),
+ items: args.all()?.into_iter().collect(),
+ }
+ .pack())
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "tight" => Some(Value::Bool(self.tight)),
+ "items" => Some(Value::Array(
+ self.items.items().cloned().map(Value::Content).collect(),
+ )),
+ _ => None,
+ }
+ }
+}
+
+impl Layout for ListNode {
+ fn layout(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ let marker = styles.get(Self::MARKER);
+ 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)
+ };
+
+ let mut cells = vec![];
+ for (item, map) in self.items.iter() {
+ cells.push(Content::empty());
+ cells.push(marker.clone());
+ cells.push(Content::empty());
+ cells.push(item.clone().styled_with_map(map.clone()));
+ }
+
+ 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(vt, styles, regions)
+ }
+}
diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs
index d3a9370b..ab5ebaf8 100644
--- a/library/src/layout/mod.rs
+++ b/library/src/layout/mod.rs
@@ -3,9 +3,12 @@
mod align;
mod columns;
mod container;
+#[path = "enum.rs"]
+mod enum_;
mod flow;
mod grid;
mod hide;
+mod list;
mod pad;
mod page;
mod par;
@@ -13,14 +16,18 @@ mod place;
mod repeat;
mod spacing;
mod stack;
+mod table;
+mod terms;
mod transform;
pub use self::align::*;
pub use self::columns::*;
pub use self::container::*;
+pub use self::enum_::*;
pub use self::flow::*;
pub use self::grid::*;
pub use self::hide::*;
+pub use self::list::*;
pub use self::pad::*;
pub use self::page::*;
pub use self::par::*;
@@ -28,6 +35,8 @@ pub use self::place::*;
pub use self::repeat::*;
pub use self::spacing::*;
pub use self::stack::*;
+pub use self::table::*;
+pub use self::terms::*;
pub use self::transform::*;
use std::mem;
@@ -40,7 +49,6 @@ use typst::model::{
StyleVecBuilder, StyledNode,
};
-use crate::basics::{EnumNode, ListNode, TermItem, TermsNode};
use crate::math::{FormulaNode, LayoutMath};
use crate::meta::DocumentNode;
use crate::prelude::*;
diff --git a/library/src/layout/table.rs b/library/src/layout/table.rs
new file mode 100644
index 00000000..12d2455f
--- /dev/null
+++ b/library/src/layout/table.rs
@@ -0,0 +1,236 @@
+use crate::layout::{AlignNode, GridNode, TrackSizing, TrackSizings};
+use crate::prelude::*;
+
+/// # Table
+/// A table of items.
+///
+/// Tables are used to arrange content in cells. Cells can contain arbitrary
+/// content, including multiple paragraphs and are specified in row-major order.
+/// Because tables are just grids with configurable cell properties, refer to
+/// the [grid documentation]($func/grid) for more information on how to size the
+/// table tracks.
+///
+/// ## Example
+/// ```example
+/// #table(
+/// columns: (1fr, auto, auto),
+/// inset: 10pt,
+/// align: horizon,
+/// [], [*Area*], [*Parameters*],
+/// image("cylinder.svg"),
+/// $ pi h (D^2 - d^2) / 4 $,
+/// [
+/// $h$: height \
+/// $D$: outer radius \
+/// $d$: inner radius
+/// ],
+/// image("tetrahedron.svg"),
+/// $ sqrt(2) / 12 a^3 $,
+/// [$a$: edge length]
+/// )
+/// ```
+///
+/// ## Parameters
+/// - cells: `Content` (positional, variadic)
+/// The contents of the table cells.
+///
+/// - rows: `TrackSizings` (named)
+/// Defines the row sizes.
+/// See the [grid documentation]($func/grid) for more information on track
+/// sizing.
+///
+/// - columns: `TrackSizings` (named)
+/// Defines the column sizes.
+/// See the [grid documentation]($func/grid) for more information on track
+/// sizing.
+///
+/// - gutter: `TrackSizings` (named)
+/// Defines the gaps between rows & columns.
+/// See the [grid documentation]($func/grid) for more information on gutters.
+///
+/// - column-gutter: `TrackSizings` (named)
+/// Defines the gaps between columns. Takes precedence over `gutter`.
+/// See the [grid documentation]($func/grid) for more information on gutters.
+///
+/// - row-gutter: `TrackSizings` (named)
+/// Defines the gaps between rows. Takes precedence over `gutter`.
+/// See the [grid documentation]($func/grid) for more information on gutters.
+///
+/// ## Category
+/// layout
+#[func]
+#[capable(Layout)]
+#[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]
+impl TableNode {
+ /// How to fill the cells.
+ ///
+ /// This can be a color or a function that returns a color. The function is
+ /// passed the cell's column and row index, starting at zero. This can be
+ /// used to implement striped tables.
+ ///
+ /// ```example
+ /// #table(
+ /// fill: (col, _) => if calc.odd(col) { luma(240) } else { white },
+ /// align: (col, row) =>
+ /// if row == 0 { center }
+ /// else if col == 0 { left }
+ /// else { right },
+ /// columns: 4,
+ /// [], [*Q1*], [*Q2*], [*Q3*],
+ /// [Revenue:], [1000 €], [2000 €], [3000 €],
+ /// [Expenses:], [500 €], [1000 €], [1500 €],
+ /// [Profit:], [500 €], [1000 €], [1500 €],
+ /// )
+ /// ```
+ #[property(referenced)]
+ pub const FILL: Celled<Option<Paint>> = Celled::Value(None);
+
+ /// How to align the cell's content.
+ ///
+ /// This can either be a single alignment or a function that returns an
+ /// alignment. The function is passed the cell's column and row index,
+ /// starting at zero. If set to `{auto}`, the outer alignment is used.
+ #[property(referenced)]
+ pub const ALIGN: Celled<Smart<Axes<Option<GenAlign>>>> = Celled::Value(Smart::Auto);
+
+ /// How to stroke the cells.
+ ///
+ /// This can be a color, a stroke width, both, or `{none}` to disable
+ /// the stroke.
+ #[property(resolve, fold)]
+ pub const STROKE: Option<PartialStroke> = Some(PartialStroke::default());
+
+ /// How much to pad the cells's content.
+ ///
+ /// The default value is `{5pt}`.
+ pub const INSET: 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 {
+ "columns" => Some(TrackSizing::encode_slice(&self.tracks.x)),
+ "rows" => Some(TrackSizing::encode_slice(&self.tracks.y)),
+ "column-gutter" => Some(TrackSizing::encode_slice(&self.gutter.x)),
+ "row-gutter" => Some(TrackSizing::encode_slice(&self.gutter.y)),
+ "cells" => Some(Value::Array(
+ self.cells.iter().cloned().map(Value::Content).collect(),
+ )),
+ _ => None,
+ }
+ }
+}
+
+impl Layout for TableNode {
+ fn layout(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ let fill = styles.get(Self::FILL);
+ let stroke = styles.get(Self::STROKE).map(PartialStroke::unwrap_or_default);
+ let inset = styles.get(Self::INSET);
+ let align = styles.get(Self::ALIGN);
+
+ 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(inset));
+
+ let x = i % cols;
+ let y = i / cols;
+ if let Smart::Custom(alignment) = align.resolve(vt, x, y)? {
+ child = child.styled(AlignNode::ALIGNS, alignment)
+ }
+
+ if let Some(stroke) = stroke {
+ child = child.stroked(stroke);
+ }
+
+ if let Some(fill) = fill.resolve(vt, x, y)? {
+ child = child.filled(fill);
+ }
+
+ Ok(child)
+ })
+ .collect::<SourceResult<_>>()?;
+
+ GridNode {
+ tracks: self.tracks.clone(),
+ gutter: self.gutter.clone(),
+ cells,
+ }
+ .layout(vt, 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),
+}
+
+impl<T: Cast + Clone> Celled<T> {
+ /// Resolve the value based on the cell position.
+ pub fn resolve(&self, vt: &Vt, x: usize, y: usize) -> SourceResult<T> {
+ Ok(match self {
+ Self::Value(value) => value.clone(),
+ Self::Func(func) => {
+ let args =
+ Args::new(func.span(), [Value::Int(x as i64), Value::Int(y as i64)]);
+ func.call_detached(vt.world(), args)?.cast().at(func.span())?
+ }
+ })
+ }
+}
+
+impl<T: Cast> Cast for Celled<T> {
+ fn is(value: &Value) -> bool {
+ matches!(value, Value::Func(_)) || T::is(value)
+ }
+
+ fn cast(value: Value) -> StrResult<Self> {
+ match value {
+ Value::Func(v) => Ok(Self::Func(v)),
+ v if T::is(&v) => Ok(Self::Value(T::cast(v)?)),
+ v => <Self as Cast>::error(v),
+ }
+ }
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("function")
+ }
+}
diff --git a/library/src/layout/terms.rs b/library/src/layout/terms.rs
new file mode 100644
index 00000000..cf214084
--- /dev/null
+++ b/library/src/layout/terms.rs
@@ -0,0 +1,178 @@
+use crate::layout::{BlockNode, GridNode, HNode, ParNode, Spacing, TrackSizing};
+use crate::prelude::*;
+use crate::text::{SpaceNode, TextNode};
+
+/// # Term List
+/// A list of terms and their descriptions.
+///
+/// Displays a sequence of terms and their descriptions vertically. When the
+/// descriptions span over multiple lines, they use hanging indent to
+/// communicate the visual hierarchy.
+///
+/// ## Syntax
+/// This function also has dedicated syntax: Starting a line with a slash,
+/// followed by a term, a colon and a description creates a term list item.
+///
+/// ## Example
+/// ```example
+/// / Ligature: A merged glyph.
+/// / Kerning: A spacing adjustment
+/// between two adjacent letters.
+/// ```
+///
+/// ## Parameters
+/// - items: `Content` (positional, variadic)
+/// The term list's children.
+///
+/// When using the term list syntax, adjacent items are automatically
+/// collected into term lists, even through constructs like for loops.
+///
+/// ```example
+/// #for year, product in (
+/// "1978": "TeX",
+/// "1984": "LaTeX",
+/// "2019": "Typst",
+/// ) [/ #product: Born in #year.]
+/// ```
+///
+/// - tight: `bool` (named)
+/// If this is `{false}`, the items are spaced apart with [term list
+/// spacing]($func/terms.spacing). If it is `{true}`, they use normal
+/// [leading]($func/par.leading) instead. This makes the term list more
+/// compact, which can look better if the items are short.
+///
+/// ```example
+/// / Fact: If a term list has a lot
+/// of text, and maybe other inline
+/// content, it should not be tight
+/// anymore.
+///
+/// / Tip: To make it wide, simply
+/// insert a blank line between the
+/// items.
+/// ```
+///
+/// ## Category
+/// layout
+#[func]
+#[capable(Layout)]
+#[derive(Debug, Hash)]
+pub struct TermsNode {
+ /// If true, the items are separated by leading instead of list spacing.
+ pub tight: bool,
+ /// The individual bulleted or numbered items.
+ pub items: StyleVec<TermItem>,
+}
+
+#[node]
+impl TermsNode {
+ /// The indentation of each item's term.
+ #[property(resolve)]
+ pub const INDENT: Length = Length::zero();
+
+ /// The hanging indent of the description.
+ ///
+ /// ```example
+ /// #set terms(hanging-indent: 0pt)
+ /// / Term: This term list does not
+ /// make use of hanging indents.
+ /// ```
+ #[property(resolve)]
+ pub const HANGING_INDENT: Length = Em::new(1.0).into();
+
+ /// The spacing between the items of a wide (non-tight) term list.
+ ///
+ /// If set to `{auto}` uses the spacing [below blocks]($func/block.below).
+ pub const SPACING: Smart<Spacing> = Smart::Auto;
+
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self {
+ tight: args.named("tight")?.unwrap_or(true),
+ items: args.all()?.into_iter().collect(),
+ }
+ .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 Layout for TermsNode {
+ fn layout(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ let indent = styles.get(Self::INDENT);
+ let body_indent = styles.get(Self::HANGING_INDENT);
+ let gutter = if self.tight {
+ styles.get(ParNode::LEADING).into()
+ } else {
+ styles
+ .get(Self::SPACING)
+ .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount)
+ };
+
+ let mut cells = vec![];
+ for (item, map) in self.items.iter() {
+ let body = Content::sequence(vec![
+ HNode { amount: (-body_indent).into(), weak: false }.pack(),
+ (item.term.clone() + TextNode::packed(':')).strong(),
+ SpaceNode.pack(),
+ item.description.clone(),
+ ]);
+
+ cells.push(Content::empty());
+ cells.push(body.styled_with_map(map.clone()));
+ }
+
+ GridNode {
+ tracks: Axes::with_x(vec![
+ TrackSizing::Relative((indent + body_indent).into()),
+ TrackSizing::Auto,
+ ]),
+ gutter: Axes::with_y(vec![gutter.into()]),
+ cells,
+ }
+ .layout(vt, styles, regions)
+ }
+}
+
+/// A term list item.
+#[derive(Debug, Clone, Hash)]
+pub struct TermItem {
+ /// The term described by the list item.
+ pub term: Content,
+ /// The description of the term.
+ pub description: Content,
+}
+
+impl TermItem {
+ /// Encode the item into a value.
+ fn encode(&self) -> Value {
+ Value::Array(array![
+ Value::Content(self.term.clone()),
+ Value::Content(self.description.clone()),
+ ])
+ }
+}
+
+castable! {
+ TermItem,
+ array: Array => {
+ let mut iter = array.into_iter();
+ let (term, description) = match (iter.next(), iter.next(), iter.next()) {
+ (Some(a), Some(b), None) => (a.cast()?, b.cast()?),
+ _ => Err("array must contain exactly two entries")?,
+ };
+ Self { term, description }
+ },
+}