Split up list type into three separate types and document them

This commit is contained in:
Laurenz 2022-12-21 12:50:33 +01:00
parent 959df6da3b
commit ba294e2670
21 changed files with 623 additions and 291 deletions

183
library/src/basics/desc.rs Normal file
View File

@ -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 }
},
}

251
library/src/basics/enum.rs Normal file
View File

@ -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")])
}
}

View File

@ -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

View File

@ -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.
///
/// - tight: bool (named)
/// Makes the list more compact, if enabled. This looks better if the items
/// fit into a single line each.
/// When using the list syntax, adjacents items are automatically collected
/// into lists, even through constructs like for loops.
///
/// ### Example
/// ```
/// #show columns.with(2)
/// #list(tight: true)[Tight][List]
/// #colbreak()
/// #list(tight: false)[Wide][List]
/// #for letter in "ABC" [
/// - Letter #letter
/// ]
/// ```
///
/// - tight: bool (named)
/// 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
/// ```
/// - 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()
Ok(Self {
tight: args.named("tight")?.unwrap_or(true),
items: args.all()?.into_iter().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())
.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"),
])
}
}

View File

@ -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::*;

View File

@ -129,8 +129,7 @@ fn format_csv_error(error: csv::Error) -> String {
/// )
/// #h(6pt)
/// #set text(22pt, baseline: -8pt)
/// {day.temperature}
/// °{day.unit}
/// {day.temperature} °{day.unit}
/// ]
///
/// #forecast(json("monday.json"))
@ -180,10 +179,7 @@ fn convert_json(value: serde_json::Value) -> Value {
/// Format the user-facing JSON error message.
fn format_json_error(error: serde_json::Error) -> String {
assert!(error.is_syntax() || error.is_eof());
format!(
"failed to parse json file: syntax error in line {}",
error.line()
)
format!("failed to parse json file: syntax error in line {}", error.line())
}
/// # XML

View File

@ -41,7 +41,8 @@ pub fn type_(args: &mut Args) -> SourceResult<Value> {
/// ```
/// { none } vs #repr(none) \
/// { "hello" } vs #repr("hello") \
/// { (1, 2) } vs #repr((1, 2))
/// { (1, 2) } vs #repr((1, 2)) \
/// { [*Hi*] } vs #repr([*Hi*])
/// ```
///
/// ## Parameters

View File

@ -40,7 +40,7 @@ use typst::model::{
StyleVecBuilder, StyledNode,
};
use crate::basics::{DescNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST};
use crate::basics::{DescItem, DescNode, EnumNode, ListNode};
use crate::meta::DocumentNode;
use crate::prelude::*;
use crate::shared::BehavedBuilder;
@ -589,12 +589,9 @@ impl<'a> ListBuilder<'a> {
}
if let Some(item) = content.to::<ListItem>() {
if self
.items
.items()
.next()
.map_or(true, |first| item.kind() == first.kind())
{
if self.items.items().next().map_or(true, |first| {
std::mem::discriminant(item) == std::mem::discriminant(first)
}) {
self.items.push(item.clone(), styles);
self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakNode>());
return true;
@ -607,10 +604,31 @@ impl<'a> ListBuilder<'a> {
fn finish(self) -> (Content, StyleChain<'a>) {
let (items, shared) = self.items.finish();
let item = items.items().next().unwrap();
let output = match item.kind() {
LIST => ListNode::<LIST> { tight: self.tight, items }.pack(),
ENUM => ListNode::<ENUM> { tight: self.tight, items }.pack(),
DESC | _ => ListNode::<DESC> { tight: self.tight, items }.pack(),
let output = match item {
ListItem::List(_) => ListNode {
tight: self.tight,
items: items.map(|item| match item {
ListItem::List(item) => item.clone(),
_ => panic!("wrong list item"),
}),
}
.pack(),
ListItem::Enum(..) => EnumNode {
tight: self.tight,
items: items.map(|item| match item {
ListItem::Enum(number, body) => (*number, body.clone()),
_ => panic!("wrong list item"),
}),
}
.pack(),
ListItem::Desc(_) => DescNode {
tight: self.tight,
items: items.map(|item| match item {
ListItem::Desc(item) => item.clone(),
_ => panic!("wrong list item"),
}),
}
.pack(),
};
(output, shared)
}
@ -625,3 +643,18 @@ impl Default for ListBuilder<'_> {
}
}
}
/// An item in a list.
#[capable]
#[derive(Debug, Clone, Hash)]
pub enum ListItem {
/// An item of an unordered list.
List(Content),
/// An item of an ordered list.
Enum(Option<NonZeroUsize>, Content),
/// An item of a description list.
Desc(DescItem),
}
#[node]
impl ListItem {}

View File

@ -198,10 +198,10 @@ fn items() -> LangItems {
link: |url| meta::LinkNode::from_url(url).pack(),
ref_: |target| meta::RefNode(target).pack(),
heading: |level, body| basics::HeadingNode { level, title: body }.pack(),
list_item: |body| basics::ListItem::List(Box::new(body)).pack(),
enum_item: |number, body| basics::ListItem::Enum(number, Box::new(body)).pack(),
desc_item: |term, body| {
basics::ListItem::Desc(Box::new(basics::DescItem { term, body })).pack()
list_item: |body| layout::ListItem::List(body).pack(),
enum_item: |number, body| layout::ListItem::Enum(number, body).pack(),
desc_item: |term, description| {
layout::ListItem::Desc(basics::DescItem { term, description }).pack()
},
math: |children, block| math::MathNode { children, block }.pack(),
math_atom: |atom| math::AtomNode(atom).pack(),

View File

@ -224,6 +224,9 @@ impl StrikeNode {
/// How to stroke the line. The text color and thickness are read from the
/// font tables if `{auto}`.
///
/// _Note:_ Please don't use this for real redaction as you can still
/// copy paste the text.
///
/// # Example
/// ```
/// This is #strike(stroke: 1.5pt + red)[very stricken through]. \

View File

@ -43,8 +43,8 @@ use crate::prelude::*;
/// ```
///
/// ## Parameters
/// - family: EcoString (positional, variadic, settable) A prioritized sequence
/// of font families.
/// - family: EcoString (positional, variadic, settable)
/// A prioritized sequence of font families.
///
/// When processing text, Typst tries all specified font families in order
/// until it finds a font that has the necessary glyphs. In the example below,
@ -63,8 +63,8 @@ use crate::prelude::*;
///
/// ```
///
/// - body: Content (positional, required) Content in which all text is styled
/// according to the other arguments.
/// - body: Content (positional, required)
/// Content in which all text is styled according to the other arguments.
///
/// ## Category
/// text
@ -141,6 +141,7 @@ impl TextNode {
/// #text(weight: "light")[Light] \
/// #text(weight: "regular")[Regular] \
/// #text(weight: "medium")[Medium] \
/// #text(weight: 500)[Medium] \
/// #text(weight: "bold")[Bold]
/// ```
pub const WEIGHT: FontWeight = FontWeight::REGULAR;
@ -296,10 +297,10 @@ impl TextNode {
/// - `{rtl}`: Layout text from right to left.
///
/// When writing in right-to-left scripts like Arabic or Hebrew, you should
/// set the language or direction. While individual runs of text are
/// automatically layouted in the correct direction, setting the dominant
/// direction gives the bidirectional reordering algorithm the necessary
/// information to correctly place punctuation and inline objects.
/// set the [text language](@text/lang) or direction. While individual runs
/// of text are automatically layouted in the correct direction, setting the
/// dominant direction gives the bidirectional reordering algorithm the
/// necessary information to correctly place punctuation and inline objects.
/// Furthermore, setting the direction affects the alignment values `start`
/// and `end`, which are equivalent to `left` and `right` in `ltr` text and
/// the other way around in `rtl` text.
@ -319,6 +320,9 @@ impl TextNode {
/// Whether to hyphenate text to improve line breaking. When `{auto}`, text
/// will be hyphenated if and only if justification is enabled.
///
/// Setting the [text language](@text/lang) ensures that the correct
/// hyphenation patterns are used.
///
/// # Example
/// ```
/// #set par(justify: true)

View File

@ -41,8 +41,8 @@ use crate::prelude::*;
///
/// ### Example
/// ````
/// // Parse numbers in raw blocks with the `mydsl` tag and
/// // sum them up.
/// // Parse numbers in raw blocks with the
/// // `mydsl` tag and sum them up.
/// #show raw.where(lang: "mydsl"): it => {
/// let sum = 0
/// for part in it.text.split("+") {

View File

@ -398,8 +398,8 @@ impl Eval for ast::DescItem {
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let term = self.term().eval(vm)?;
let body = self.body().eval(vm)?;
Ok((vm.items.desc_item)(term, body))
let description = self.description().eval(vm)?;
Ok((vm.items.desc_item)(term, description))
}
}

View File

@ -64,7 +64,7 @@ pub struct LangItems {
/// An item in an enumeration (ordered list): `+ ...` or `1. ...`.
pub enum_item: fn(number: Option<NonZeroUsize>, body: Content) -> Content,
/// An item in a description list: `/ Term: Details`.
pub desc_item: fn(term: Content, body: Content) -> Content,
pub desc_item: fn(term: Content, description: Content) -> Content,
/// A mathematical formula: `$x$`, `$ x^2 $`.
pub math: fn(children: Vec<Content>, block: bool) -> Content,
/// An atom in a formula: `x`, `+`, `12`.

View File

@ -407,10 +407,10 @@ impl DescItem {
}
/// The description of the term.
pub fn body(&self) -> Markup {
pub fn description(&self) -> Markup {
self.0
.cast_last_child()
.expect("description list item is missing body")
.expect("description list item is missing description")
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -8,8 +8,8 @@ No: list \
---
// Test with constructor.
#desc(
(term: [One], body: [First]),
(term: [Two], body: [Second]),
([One], [First]),
([Two], [Second]),
)
---
@ -32,7 +32,7 @@ No: list \
#set text(8pt)
/ First list: #lorem(4)
#set desc(body-indent: 30pt)
#set desc(hanging-indent: 30pt)
/ Second list: #lorem(4)
---
@ -40,7 +40,7 @@ No: list \
#show desc: it => table(
columns: 2,
padding: 3pt,
..it.items.map(item => (emph(item.term), item.body)).flatten(),
..it.items.map(item => (emph(item(0)), item(1))).flatten(),
)
/ A: One letter

View File

@ -17,16 +17,16 @@
---
// Test label pattern.
#set enum(label: "~ A:")
#set enum(numbering: "~ A:")
1. First
+ Second
#set enum(label: "(*)")
#set enum(numbering: "(*)")
+ A
+ B
+ C
#set enum(label: "i)")
#set enum(numbering: "i)")
+ A
+ B
@ -37,17 +37,17 @@
/ Desc: List
---
// Test label closure.
// Test numbering with closure.
#enum(
start: 4,
spacing: 0.65em - 3pt,
tight: false,
label: n => text(fill: (red, green, blue)(mod(n, 3)), numbering("A", n)),
numbering: n => text(fill: (red, green, blue)(mod(n, 3)), numbering("A", n)),
[Red], [Green], [Blue],
)
---
#set enum(label: n => n > 1)
#set enum(numbering: n => n > 1)
+ A
+ B
@ -57,9 +57,9 @@
No enum
---
// Error: 18-20 invalid numbering pattern
#set enum(label: "")
// Error: 22-24 invalid numbering pattern
#set enum(numbering: "")
---
// Error: 18-24 invalid numbering pattern
#set enum(label: "(())")
// Error: 22-28 invalid numbering pattern
#set enum(numbering: "(())")

View File

@ -48,6 +48,6 @@ _Shopping list_
- B with 2 tabs
---
#set list(label: [-])
#set list(marker: [-])
- Bare hyphen
- is not a list

View File

@ -10,8 +10,8 @@
// Ensure that constructor styles win, but not over outer styles.
// The outer paragraph should be right-aligned,
// but the B should be center-aligned.
#set list(label: [>])
#list(label: [--])[
#set list(marker: [>])
#list(marker: [--])[
#rect(width: 2cm, fill: conifer, inset: 4pt, list[A])
]
@ -27,5 +27,5 @@ A #rect(fill: yellow, inset: 5pt, rect()) B
---
// The constructor property should still work
// when there are recursive show rules.
#show list: set text(blue)
#list(label: "(a)", [A], list[B])
#show enum: set text(blue)
#enum(numbering: "(a)", [A], enum[B])

View File

@ -39,7 +39,7 @@ Hello *{x}*
---
// Test relative path resolving in layout phase.
#let choice = ("monkey.svg", "rhino.png", "tiger.jpg")
#set enum(label: n => {
#set enum(numbering: n => {
let path = "../../res/" + choice(n - 1)
move(dy: -0.15em, image(path, width: 1em, height: 1em))
})