diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-12-21 12:50:33 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-12-21 12:50:33 +0100 |
| commit | ba294e2670814243122c9e4f560e0f21a4bec13d (patch) | |
| tree | 36bdf987dc53729bb4a176713b40e2ca7c08a874 /library/src/basics | |
| parent | 959df6da3b6e3c5876e9d5627cb2970e32a0a37f (diff) | |
Split up list type into three separate types and document them
Diffstat (limited to 'library/src/basics')
| -rw-r--r-- | library/src/basics/desc.rs | 183 | ||||
| -rw-r--r-- | library/src/basics/enum.rs | 251 | ||||
| -rw-r--r-- | library/src/basics/heading.rs | 4 | ||||
| -rw-r--r-- | library/src/basics/list.rs | 316 | ||||
| -rw-r--r-- | library/src/basics/mod.rs | 5 |
5 files changed, 527 insertions, 232 deletions
diff --git a/library/src/basics/desc.rs b/library/src/basics/desc.rs new file mode 100644 index 00000000..2c764c59 --- /dev/null +++ b/library/src/basics/desc.rs @@ -0,0 +1,183 @@ +use crate::layout::{BlockNode, GridNode, HNode, ParNode, Spacing, TrackSizing}; +use crate::prelude::*; +use crate::text::{SpaceNode, TextNode}; + +/// # Description 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 description list +/// item. +/// +/// ## Example +/// ``` +/// / Ligature: A merged glyph. +/// / Kerning: A spacing adjustment +/// between two adjacent letters. +/// ``` +/// +/// ## Parameters +/// - items: Content (positional, variadic) +/// The descrition list's children. +/// +/// When using the description list syntax, adjacents items are automatically +/// collected into description 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 [description list +/// spacing](@desc/spacing). If it is `{true}`, they use normal +/// [leading](@par/leading) instead. This makes the description list more +/// compact, which can look better if the items are short. +/// +/// ### Example +/// ``` +/// / Fact: If a description 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 +/// basics +#[func] +#[capable(Layout)] +#[derive(Debug, Hash)] +pub struct DescNode { + /// If true, the items are separated by leading instead of list spacing. + pub tight: bool, + /// The individual bulleted or numbered items. + pub items: StyleVec<DescItem>, +} + +#[node] +impl DescNode { + /// The indentation of each item's term. + #[property(resolve)] + pub const INDENT: Length = Length::zero(); + + /// The hanging indent of the description. + /// + /// # Example + /// ``` + /// #set desc(hanging-indent: 0pt) + /// / Term: This description 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) description list. + /// + /// If set to `{auto}` uses the spacing [below blocks](@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 DescNode { + 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 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 description: Content, +} + +impl DescItem { + /// Encode the item into a value. + fn encode(&self) -> Value { + Value::Array(array![ + Value::Content(self.term.clone()), + Value::Content(self.description.clone()), + ]) + } +} + +castable! { + DescItem, + 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 } + }, +} diff --git a/library/src/basics/enum.rs b/library/src/basics/enum.rs new file mode 100644 index 00000000..f0ca217a --- /dev/null +++ b/library/src/basics/enum.rs @@ -0,0 +1,251 @@ +use std::str::FromStr; + +use crate::compute::NumberingPattern; +use crate::layout::{BlockNode, GridNode, ParNode, Spacing, TrackSizing}; +use crate::prelude::*; +use crate::text::TextNode; + +/// # Enumeration +/// An ordered list. +/// +/// Displays a sequence of items vertically and numbers them consecutively. +/// +/// ## 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. +/// +/// ## 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] +/// ``` +/// +/// ## Parameters +/// - items: Content (positional, variadic) +/// The enumeration's children. +/// +/// When using the enum syntax, adjacents 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](@enum/spacing). If it is `{true}`, they use normal +/// [leading](@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 +/// basics +#[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](@numbering). + /// + /// # Example + /// ``` + /// #set enum(numbering: "(a)") + /// + /// + Different + /// + Numbering + /// + Style + /// ``` + #[property(referenced)] + pub const NUMBERING: EnumNumbering = + EnumNumbering::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](@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.resolve(vt, number)?; + 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) + } +} + +/// How to number an enumeration. +#[derive(Debug, Clone, Hash)] +pub enum EnumNumbering { + /// A pattern with prefix, numbering, lower / upper case and suffix. + Pattern(NumberingPattern), + /// A closure mapping from an item's number to content. + Func(Func, Span), +} + +impl EnumNumbering { + /// Resolve the marker based on the number. + pub fn resolve(&self, vt: &Vt, number: NonZeroUsize) -> SourceResult<Content> { + Ok(match self { + Self::Pattern(pattern) => TextNode::packed(pattern.apply(&[number])), + Self::Func(func, span) => { + let args = Args::new(*span, [Value::Int(number.get() as i64)]); + func.call_detached(vt.world(), args)?.display() + } + }) + } +} + +impl Cast<Spanned<Value>> for EnumNumbering { + fn is(value: &Spanned<Value>) -> bool { + matches!(&value.v, Value::Content(_) | Value::Func(_)) + } + + fn cast(value: Spanned<Value>) -> StrResult<Self> { + match value.v { + Value::Str(v) => Ok(Self::Pattern(v.parse()?)), + Value::Func(v) => Ok(Self::Func(v, value.span)), + v => Self::error(v), + } + } + + fn describe() -> CastInfo { + CastInfo::Union(vec![CastInfo::Type("string"), CastInfo::Type("function")]) + } +} diff --git a/library/src/basics/heading.rs b/library/src/basics/heading.rs index 12a6ac66..925d23a2 100644 --- a/library/src/basics/heading.rs +++ b/library/src/basics/heading.rs @@ -11,8 +11,8 @@ use crate::text::{SpaceNode, TextNode, TextSize}; /// With headings, you can structure your document into sections. Each heading /// has a _level,_ which starts at one and is unbounded upwards. This level /// indicates the logical role of the following content (section, subsection, -/// etc.) Top-level heading indicates a top-level section of the document (not -/// the document's title). +/// etc.) A top-level heading indicates a top-level section of the document +/// (not the document's title). /// /// Typst can automatically number your headings for you. To enable numbering, /// specify how you want your headings to be numbered with a [numbering diff --git a/library/src/basics/list.rs b/library/src/basics/list.rs index ca60576c..eaba6594 100644 --- a/library/src/basics/list.rs +++ b/library/src/basics/list.rs @@ -1,28 +1,67 @@ -use crate::compute::NumberingPattern; -use crate::layout::{BlockNode, GridNode, HNode, ParNode, Spacing, TrackSizing}; +use crate::layout::{BlockNode, GridNode, ParNode, Spacing, TrackSizing}; use crate::prelude::*; -use crate::text::{SpaceNode, TextNode}; +use crate::text::TextNode; /// # List -/// An unordered (bulleted) or ordered (numbered) list. +/// An unordered list. +/// +/// Displays a sequence of items vertically, with each item introduced by a +/// marker. +/// +/// ## 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. +/// +/// ## Example +/// ``` +/// - *Content* +/// - Basics +/// - Text +/// - Math +/// - Layout +/// - Visualize +/// - Meta +/// +/// - *Compute* +/// #list( +/// [Foundations], +/// [Calculate], +/// [Create], +/// [Data Loading], +/// [Utility], +/// ) +/// ``` /// /// ## Parameters /// - items: Content (positional, variadic) -/// The contents of the list items. +/// The list's children. /// -/// - start: NonZeroUsize (named) -/// Which number to start the enumeration with. +/// When using the list syntax, adjacents items are automatically collected +/// into lists, even through constructs like for loops. +/// +/// ### Example +/// ``` +/// #for letter in "ABC" [ +/// - Letter #letter +/// ] +/// ``` /// /// - tight: bool (named) -/// Makes the list more compact, if enabled. This looks better if the items -/// fit into a single line each. +/// If this is `{false}`, the items are spaced apart with +/// [list spacing](@list/spacing). If it is `{true}`, they use normal +/// [leading](@par/leading) instead. This makes the list more compact, which +/// can look better if the items are short. /// /// ### Example /// ``` -/// #show columns.with(2) -/// #list(tight: true)[Tight][List] -/// #colbreak() -/// #list(tight: false)[Wide][List] +/// - 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 @@ -30,88 +69,67 @@ use crate::text::{SpaceNode, TextNode}; #[func] #[capable(Layout)] #[derive(Debug, Hash)] -pub struct ListNode<const L: ListKind = LIST> { +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<ListItem>, + pub items: StyleVec<Content>, } -/// An ordered list. -pub type EnumNode = ListNode<ENUM>; - -/// A description list. -pub type DescNode = ListNode<DESC>; - #[node] -impl<const L: ListKind> ListNode<L> { - /// How the list is labelled. +impl ListNode { + /// The marker which introduces each element. + /// + /// # Example + /// ``` + /// #set list(marker: [--]) + /// + /// - A more classic list + /// - With en-dashes + /// ``` #[property(referenced)] - pub const LABEL: ListLabel = ListLabel::Default; - /// The indentation of each item's label. + pub const MARKER: Content = TextNode::packed('•'); + + /// The indent of each item's marker. #[property(resolve)] pub const INDENT: Length = Length::zero(); - /// The space between the label and the body of each item. + + /// The spacing between the marker 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(); + 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](@block/below). 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: NonZeroUsize = - args.named("start")?.unwrap_or(NonZeroUsize::new(1).unwrap()); - args.all()? - .into_iter() - .map(|body| { - let item = ListItem::Enum(Some(number), Box::new(body)); - number = number.saturating_add(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()) + 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())) - } + "items" => Some(Value::Array( + self.items.items().cloned().map(Value::Content).collect(), + )), _ => None, } } } -impl<const L: ListKind> Layout for ListNode<L> { +impl Layout for ListNode { fn layout( &self, vt: &mut Vt, styles: StyleChain, regions: Regions, ) -> SourceResult<Fragment> { - let mut cells = vec![]; - let mut number = NonZeroUsize::new(1).unwrap(); - - let label = styles.get(Self::LABEL); + 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 { @@ -122,35 +140,12 @@ impl<const L: ListKind> Layout for ListNode<L> { .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount) }; + let mut cells = vec![]; 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(vt, L, number)?.styled_with_map(map.clone()) - } else { - Content::empty() - }; - - cells.push(label); + cells.push(marker.clone()); 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 = number.saturating_add(1); + cells.push(item.clone().styled_with_map(map.clone())); } GridNode { @@ -166,142 +161,3 @@ impl<const L: ListKind> Layout for ListNode<L> { .layout(vt, styles, regions) } } - -/// An item in a list. -#[capable] -#[derive(Debug, Clone, Hash)] -pub enum ListItem { - /// An item of an unordered list. - List(Box<Content>), - /// An item of an ordered list. - Enum(Option<NonZeroUsize>, 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.get() 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, - mut dict: Dict => { - let term: Content = dict.take("term")?.cast()?; - let body: Content = dict.take("body")?.cast()?; - dict.finish(&["term", "body"])?; - 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, Hash)] -pub enum ListLabel { - /// 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 ListLabel { - /// Resolve the label based on the level. - pub fn resolve( - &self, - vt: &Vt, - kind: ListKind, - number: NonZeroUsize, - ) -> 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.get() as i64)]); - func.call_detached(vt.world(), args)?.display() - } - }) - } -} - -impl Cast<Spanned<Value>> for ListLabel { - fn is(value: &Spanned<Value>) -> bool { - matches!( - &value.v, - Value::None | Value::Str(_) | 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 => Self::error(v), - } - } - - fn describe() -> CastInfo { - CastInfo::Union(vec![ - CastInfo::Type("string"), - CastInfo::Type("content"), - CastInfo::Type("function"), - ]) - } -} diff --git a/library/src/basics/mod.rs b/library/src/basics/mod.rs index 5916df6b..431cb004 100644 --- a/library/src/basics/mod.rs +++ b/library/src/basics/mod.rs @@ -1,9 +1,14 @@ //! Common document elements. +mod desc; +#[path = "enum.rs"] +mod enum_; mod heading; mod list; mod table; +pub use self::desc::*; +pub use self::enum_::*; pub use self::heading::*; pub use self::list::*; pub use self::table::*; |
