mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Split up list type into three separate types and document them
This commit is contained in:
parent
959df6da3b
commit
ba294e2670
183
library/src/basics/desc.rs
Normal file
183
library/src/basics/desc.rs
Normal 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
251
library/src/basics/enum.rs
Normal 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")])
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {}
|
||||
|
@ -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(),
|
||||
|
@ -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]. \
|
||||
|
@ -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)
|
||||
|
@ -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("+") {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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`.
|
||||
|
@ -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 |
@ -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
|
||||
|
@ -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: "(())")
|
||||
|
@ -48,6 +48,6 @@ _Shopping list_
|
||||
- B with 2 tabs
|
||||
|
||||
---
|
||||
#set list(label: [-])
|
||||
#set list(marker: [-])
|
||||
- Bare hyphen
|
||||
- is not a list
|
||||
|
@ -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])
|
||||
|
@ -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))
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user