From 3cdd8bfa40fe5fdf0c676af905c3c2c1f614ef24 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 26 Nov 2022 12:49:30 +0100 Subject: Extract numbering pattern from list node --- library/src/base/string.rs | 99 ++------------------------- library/src/core/behave.rs | 128 ---------------------------------- library/src/core/ext.rs | 147 ---------------------------------------- library/src/core/mod.rs | 7 -- library/src/layout/mod.rs | 2 +- library/src/lib.rs | 2 +- library/src/prelude.rs | 4 +- library/src/shared/behave.rs | 128 ++++++++++++++++++++++++++++++++++ library/src/shared/ext.rs | 147 ++++++++++++++++++++++++++++++++++++++++ library/src/shared/mod.rs | 9 +++ library/src/shared/numbering.rs | 139 +++++++++++++++++++++++++++++++++++++ library/src/structure/list.rs | 33 ++------- src/model/eval.rs | 4 +- tests/typ/structure/enum.typ | 4 +- 14 files changed, 441 insertions(+), 412 deletions(-) delete mode 100644 library/src/core/behave.rs delete mode 100644 library/src/core/ext.rs delete mode 100644 library/src/core/mod.rs create mode 100644 library/src/shared/behave.rs create mode 100644 library/src/shared/ext.rs create mode 100644 library/src/shared/mod.rs create mode 100644 library/src/shared/numbering.rs diff --git a/library/src/base/string.rs b/library/src/base/string.rs index 058ee248..262dd5c6 100644 --- a/library/src/base/string.rs +++ b/library/src/base/string.rs @@ -1,6 +1,7 @@ use typst::model::Regex; use crate::prelude::*; +use crate::shared::NumberingKind; /// The string representation of a value. pub fn repr(_: &Vm, args: &mut Args) -> SourceResult { @@ -32,110 +33,20 @@ pub fn regex(_: &Vm, args: &mut Args) -> SourceResult { /// Converts an integer into one or multiple letters. pub fn letter(_: &Vm, args: &mut Args) -> SourceResult { - numbered(Numbering::Letter, args) + numbered(NumberingKind::Letter, args) } /// Converts an integer into a roman numeral. pub fn roman(_: &Vm, args: &mut Args) -> SourceResult { - numbered(Numbering::Roman, args) + numbered(NumberingKind::Roman, args) } /// Convert a number into a symbol. pub fn symbol(_: &Vm, args: &mut Args) -> SourceResult { - numbered(Numbering::Symbol, args) + numbered(NumberingKind::Symbol, args) } -fn numbered(numbering: Numbering, args: &mut Args) -> SourceResult { +fn numbered(numbering: NumberingKind, args: &mut Args) -> SourceResult { let n = args.expect::("non-negative integer")?; Ok(Value::Str(numbering.apply(n).into())) } - -/// Allows to convert a number into letters, roman numerals and symbols. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Numbering { - Arabic, - Letter, - Roman, - Symbol, -} - -impl Numbering { - /// Apply the numbering to the given number. - pub fn apply(self, mut n: usize) -> EcoString { - match self { - Self::Arabic => { - format_eco!("{}", n) - } - Self::Letter => { - if n == 0 { - return '-'.into(); - } - - n -= 1; - - let mut letters = vec![]; - loop { - letters.push(b'a' + (n % 26) as u8); - n /= 26; - if n == 0 { - break; - } - } - - letters.reverse(); - String::from_utf8(letters).unwrap().into() - } - Self::Roman => { - if n == 0 { - return 'N'.into(); - } - - // Adapted from Yann Villessuzanne's roman.rs under the Unlicense, at - // https://github.com/linfir/roman.rs/ - let mut fmt = EcoString::new(); - for &(name, value) in ROMANS { - while n >= value { - n -= value; - fmt.push_str(name); - } - } - - fmt - } - Self::Symbol => { - if n == 0 { - return '-'.into(); - } - - let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; - let amount = ((n - 1) / SYMBOLS.len()) + 1; - std::iter::repeat(symbol).take(amount).collect() - } - } - } -} - -const ROMANS: &[(&str, usize)] = &[ - ("M̅", 1000000), - ("D̅", 500000), - ("C̅", 100000), - ("L̅", 50000), - ("X̅", 10000), - ("V̅", 5000), - ("I̅V̅", 4000), - ("M", 1000), - ("CM", 900), - ("D", 500), - ("CD", 400), - ("C", 100), - ("XC", 90), - ("L", 50), - ("XL", 40), - ("X", 10), - ("IX", 9), - ("V", 5), - ("IV", 4), - ("I", 1), -]; - -const SYMBOLS: &[char] = &['*', '†', '‡', '§', '‖', '¶']; diff --git a/library/src/core/behave.rs b/library/src/core/behave.rs deleted file mode 100644 index ec8fade9..00000000 --- a/library/src/core/behave.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Node interaction. - -use typst::model::{capability, Content, StyleChain, StyleVec, StyleVecBuilder}; - -/// How a node interacts with other nodes. -#[capability] -pub trait Behave { - /// The node's interaction behaviour. - fn behaviour(&self) -> Behaviour; - - /// Whether this weak node is larger than a previous one and thus picked as - /// the maximum when the levels are the same. - #[allow(unused_variables)] - fn larger(&self, prev: &Content) -> bool { - false - } -} - -/// How a node interacts with other nodes in a stream. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Behaviour { - /// A weak node which only survives when a supportive node is before and - /// after it. Furthermore, per consecutive run of weak nodes, only one - /// survives: The one with the lowest weakness level (or the larger one if - /// there is a tie). - Weak(u8), - /// A node that enables adjacent weak nodes to exist. The default. - Supportive, - /// A node that destroys adjacent weak nodes. - Destructive, - /// A node that does not interact at all with other nodes, having the - /// same effect as if it didn't exist. - Ignorant, -} - -/// A wrapper around a [`StyleVecBuilder`] that allows items to interact. -pub struct BehavedBuilder<'a> { - /// The internal builder. - builder: StyleVecBuilder<'a, Content>, - /// Staged weak and ignorant items that we can't yet commit to the builder. - /// The option is `Some(_)` for weak items and `None` for ignorant items. - staged: Vec<(Content, Behaviour, StyleChain<'a>)>, - /// What the last non-ignorant item was. - last: Behaviour, -} - -impl<'a> BehavedBuilder<'a> { - /// Create a new style-vec builder. - pub fn new() -> Self { - Self { - builder: StyleVecBuilder::new(), - staged: vec![], - last: Behaviour::Destructive, - } - } - - /// Whether the builder is empty. - pub fn is_empty(&self) -> bool { - self.builder.is_empty() && self.staged.is_empty() - } - - /// Push an item into the sequence. - pub fn push(&mut self, item: Content, styles: StyleChain<'a>) { - let interaction = item - .with::() - .map_or(Behaviour::Supportive, Behave::behaviour); - - match interaction { - Behaviour::Weak(level) => { - if matches!(self.last, Behaviour::Weak(_)) { - let item = item.with::().unwrap(); - let i = self.staged.iter().position(|prev| { - let Behaviour::Weak(prev_level) = prev.1 else { return false }; - level < prev_level - || (level == prev_level && item.larger(&prev.0)) - }); - let Some(i) = i else { return }; - self.staged.remove(i); - } - - if self.last != Behaviour::Destructive { - self.staged.push((item, interaction, styles)); - self.last = interaction; - } - } - Behaviour::Supportive => { - self.flush(true); - self.builder.push(item, styles); - self.last = interaction; - } - Behaviour::Destructive => { - self.flush(false); - self.builder.push(item, styles); - self.last = interaction; - } - Behaviour::Ignorant => { - self.staged.push((item, interaction, styles)); - } - } - } - - /// Iterate over the contained items. - pub fn items(&self) -> impl DoubleEndedIterator { - self.builder.items().chain(self.staged.iter().map(|(item, ..)| item)) - } - - /// Return the finish style vec and the common prefix chain. - pub fn finish(mut self) -> (StyleVec, StyleChain<'a>) { - self.flush(false); - self.builder.finish() - } - - /// Push the staged items, filtering out weak items if `supportive` is - /// false. - fn flush(&mut self, supportive: bool) { - for (item, interaction, styles) in self.staged.drain(..) { - if supportive || interaction == Behaviour::Ignorant { - self.builder.push(item, styles); - } - } - } -} - -impl<'a> Default for BehavedBuilder<'a> { - fn default() -> Self { - Self::new() - } -} diff --git a/library/src/core/ext.rs b/library/src/core/ext.rs deleted file mode 100644 index f90260ad..00000000 --- a/library/src/core/ext.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Extension traits. - -use crate::prelude::*; - -/// Additional methods on content. -pub trait ContentExt { - /// Make this content strong. - fn strong(self) -> Self; - - /// Make this content emphasized. - fn emph(self) -> Self; - - /// Underline this content. - fn underlined(self) -> Self; - - /// Force a size for this content. - fn boxed(self, sizing: Axes>>) -> Self; - - /// Set alignments for this content. - fn aligned(self, aligns: Axes>) -> Self; - - /// Pad this content at the sides. - fn padded(self, padding: Sides>) -> Self; - - /// Transform this content's contents without affecting layout. - fn moved(self, delta: Axes>) -> Self; - - /// Fill the frames resulting from a content. - fn filled(self, fill: Paint) -> Self; - - /// Stroke the frames resulting from a content. - fn stroked(self, stroke: Stroke) -> Self; -} - -impl ContentExt for Content { - fn strong(self) -> Self { - crate::text::StrongNode(self).pack() - } - - fn emph(self) -> Self { - crate::text::EmphNode(self).pack() - } - - fn underlined(self) -> Self { - crate::text::DecoNode::<{ crate::text::UNDERLINE }>(self).pack() - } - - fn boxed(self, sizing: Axes>>) -> Self { - crate::layout::BoxNode { sizing, child: self }.pack() - } - - fn aligned(self, aligns: Axes>) -> Self { - crate::layout::AlignNode { aligns, child: self }.pack() - } - - fn padded(self, padding: Sides>) -> Self { - crate::layout::PadNode { padding, child: self }.pack() - } - - fn moved(self, delta: Axes>) -> Self { - crate::layout::MoveNode { delta, child: self }.pack() - } - - fn filled(self, fill: Paint) -> Self { - FillNode { fill, child: self }.pack() - } - - fn stroked(self, stroke: Stroke) -> Self { - StrokeNode { stroke, child: self }.pack() - } -} - -/// Additional methods for style maps. -pub trait StyleMapExt { - /// Set a font family composed of a preferred family and existing families - /// from a style chain. - fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain); -} - -impl StyleMapExt for StyleMap { - fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain) { - self.set( - crate::text::TextNode::FAMILY, - crate::text::FallbackList( - std::iter::once(preferred) - .chain(existing.get(crate::text::TextNode::FAMILY).0.iter().cloned()) - .collect(), - ), - ); - } -} - -/// Fill the frames resulting from content. -#[derive(Debug, Hash)] -struct FillNode { - /// How to fill the frames resulting from the `child`. - fill: Paint, - /// The content whose frames should be filled. - child: Content, -} - -#[node(LayoutBlock)] -impl FillNode {} - -impl LayoutBlock for FillNode { - fn layout_block( - &self, - world: Tracked, - styles: StyleChain, - regions: &Regions, - ) -> SourceResult> { - let mut frames = self.child.layout_block(world, styles, regions)?; - for frame in &mut frames { - let shape = Geometry::Rect(frame.size()).filled(self.fill); - frame.prepend(Point::zero(), Element::Shape(shape)); - } - Ok(frames) - } -} - -/// Stroke the frames resulting from content. -#[derive(Debug, Hash)] -struct StrokeNode { - /// How to stroke the frames resulting from the `child`. - stroke: Stroke, - /// The content whose frames should be stroked. - child: Content, -} - -#[node(LayoutBlock)] -impl StrokeNode {} - -impl LayoutBlock for StrokeNode { - fn layout_block( - &self, - world: Tracked, - styles: StyleChain, - regions: &Regions, - ) -> SourceResult> { - let mut frames = self.child.layout_block(world, styles, regions)?; - for frame in &mut frames { - let shape = Geometry::Rect(frame.size()).stroked(self.stroke); - frame.prepend(Point::zero(), Element::Shape(shape)); - } - Ok(frames) - } -} diff --git a/library/src/core/mod.rs b/library/src/core/mod.rs deleted file mode 100644 index 6cafa9fc..00000000 --- a/library/src/core/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Central definitions for the standard library. - -mod behave; -mod ext; - -pub use behave::*; -pub use ext::*; diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index 44b7b6d8..100e0611 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -37,8 +37,8 @@ use typst::model::{ }; use typst::World; -use crate::core::BehavedBuilder; use crate::prelude::*; +use crate::shared::BehavedBuilder; use crate::structure::{ DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST, }; diff --git a/library/src/lib.rs b/library/src/lib.rs index c2234446..dd527ed1 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -1,11 +1,11 @@ //! Typst's standard library. pub mod base; -pub mod core; pub mod graphics; pub mod layout; pub mod math; pub mod prelude; +pub mod shared; pub mod structure; pub mod text; diff --git a/library/src/prelude.rs b/library/src/prelude.rs index 36ff0561..d2d82d3b 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -26,7 +26,7 @@ pub use typst::util::{format_eco, EcoString}; #[doc(no_inline)] pub use typst::World; -#[doc(no_inline)] -pub use crate::core::{Behave, Behaviour, ContentExt, StyleMapExt}; #[doc(no_inline)] pub use crate::layout::{LayoutBlock, LayoutInline, Regions}; +#[doc(no_inline)] +pub use crate::shared::{Behave, Behaviour, ContentExt, StyleMapExt}; diff --git a/library/src/shared/behave.rs b/library/src/shared/behave.rs new file mode 100644 index 00000000..ec8fade9 --- /dev/null +++ b/library/src/shared/behave.rs @@ -0,0 +1,128 @@ +//! Node interaction. + +use typst::model::{capability, Content, StyleChain, StyleVec, StyleVecBuilder}; + +/// How a node interacts with other nodes. +#[capability] +pub trait Behave { + /// The node's interaction behaviour. + fn behaviour(&self) -> Behaviour; + + /// Whether this weak node is larger than a previous one and thus picked as + /// the maximum when the levels are the same. + #[allow(unused_variables)] + fn larger(&self, prev: &Content) -> bool { + false + } +} + +/// How a node interacts with other nodes in a stream. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Behaviour { + /// A weak node which only survives when a supportive node is before and + /// after it. Furthermore, per consecutive run of weak nodes, only one + /// survives: The one with the lowest weakness level (or the larger one if + /// there is a tie). + Weak(u8), + /// A node that enables adjacent weak nodes to exist. The default. + Supportive, + /// A node that destroys adjacent weak nodes. + Destructive, + /// A node that does not interact at all with other nodes, having the + /// same effect as if it didn't exist. + Ignorant, +} + +/// A wrapper around a [`StyleVecBuilder`] that allows items to interact. +pub struct BehavedBuilder<'a> { + /// The internal builder. + builder: StyleVecBuilder<'a, Content>, + /// Staged weak and ignorant items that we can't yet commit to the builder. + /// The option is `Some(_)` for weak items and `None` for ignorant items. + staged: Vec<(Content, Behaviour, StyleChain<'a>)>, + /// What the last non-ignorant item was. + last: Behaviour, +} + +impl<'a> BehavedBuilder<'a> { + /// Create a new style-vec builder. + pub fn new() -> Self { + Self { + builder: StyleVecBuilder::new(), + staged: vec![], + last: Behaviour::Destructive, + } + } + + /// Whether the builder is empty. + pub fn is_empty(&self) -> bool { + self.builder.is_empty() && self.staged.is_empty() + } + + /// Push an item into the sequence. + pub fn push(&mut self, item: Content, styles: StyleChain<'a>) { + let interaction = item + .with::() + .map_or(Behaviour::Supportive, Behave::behaviour); + + match interaction { + Behaviour::Weak(level) => { + if matches!(self.last, Behaviour::Weak(_)) { + let item = item.with::().unwrap(); + let i = self.staged.iter().position(|prev| { + let Behaviour::Weak(prev_level) = prev.1 else { return false }; + level < prev_level + || (level == prev_level && item.larger(&prev.0)) + }); + let Some(i) = i else { return }; + self.staged.remove(i); + } + + if self.last != Behaviour::Destructive { + self.staged.push((item, interaction, styles)); + self.last = interaction; + } + } + Behaviour::Supportive => { + self.flush(true); + self.builder.push(item, styles); + self.last = interaction; + } + Behaviour::Destructive => { + self.flush(false); + self.builder.push(item, styles); + self.last = interaction; + } + Behaviour::Ignorant => { + self.staged.push((item, interaction, styles)); + } + } + } + + /// Iterate over the contained items. + pub fn items(&self) -> impl DoubleEndedIterator { + self.builder.items().chain(self.staged.iter().map(|(item, ..)| item)) + } + + /// Return the finish style vec and the common prefix chain. + pub fn finish(mut self) -> (StyleVec, StyleChain<'a>) { + self.flush(false); + self.builder.finish() + } + + /// Push the staged items, filtering out weak items if `supportive` is + /// false. + fn flush(&mut self, supportive: bool) { + for (item, interaction, styles) in self.staged.drain(..) { + if supportive || interaction == Behaviour::Ignorant { + self.builder.push(item, styles); + } + } + } +} + +impl<'a> Default for BehavedBuilder<'a> { + fn default() -> Self { + Self::new() + } +} diff --git a/library/src/shared/ext.rs b/library/src/shared/ext.rs new file mode 100644 index 00000000..f90260ad --- /dev/null +++ b/library/src/shared/ext.rs @@ -0,0 +1,147 @@ +//! Extension traits. + +use crate::prelude::*; + +/// Additional methods on content. +pub trait ContentExt { + /// Make this content strong. + fn strong(self) -> Self; + + /// Make this content emphasized. + fn emph(self) -> Self; + + /// Underline this content. + fn underlined(self) -> Self; + + /// Force a size for this content. + fn boxed(self, sizing: Axes>>) -> Self; + + /// Set alignments for this content. + fn aligned(self, aligns: Axes>) -> Self; + + /// Pad this content at the sides. + fn padded(self, padding: Sides>) -> Self; + + /// Transform this content's contents without affecting layout. + fn moved(self, delta: Axes>) -> Self; + + /// Fill the frames resulting from a content. + fn filled(self, fill: Paint) -> Self; + + /// Stroke the frames resulting from a content. + fn stroked(self, stroke: Stroke) -> Self; +} + +impl ContentExt for Content { + fn strong(self) -> Self { + crate::text::StrongNode(self).pack() + } + + fn emph(self) -> Self { + crate::text::EmphNode(self).pack() + } + + fn underlined(self) -> Self { + crate::text::DecoNode::<{ crate::text::UNDERLINE }>(self).pack() + } + + fn boxed(self, sizing: Axes>>) -> Self { + crate::layout::BoxNode { sizing, child: self }.pack() + } + + fn aligned(self, aligns: Axes>) -> Self { + crate::layout::AlignNode { aligns, child: self }.pack() + } + + fn padded(self, padding: Sides>) -> Self { + crate::layout::PadNode { padding, child: self }.pack() + } + + fn moved(self, delta: Axes>) -> Self { + crate::layout::MoveNode { delta, child: self }.pack() + } + + fn filled(self, fill: Paint) -> Self { + FillNode { fill, child: self }.pack() + } + + fn stroked(self, stroke: Stroke) -> Self { + StrokeNode { stroke, child: self }.pack() + } +} + +/// Additional methods for style maps. +pub trait StyleMapExt { + /// Set a font family composed of a preferred family and existing families + /// from a style chain. + fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain); +} + +impl StyleMapExt for StyleMap { + fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain) { + self.set( + crate::text::TextNode::FAMILY, + crate::text::FallbackList( + std::iter::once(preferred) + .chain(existing.get(crate::text::TextNode::FAMILY).0.iter().cloned()) + .collect(), + ), + ); + } +} + +/// Fill the frames resulting from content. +#[derive(Debug, Hash)] +struct FillNode { + /// How to fill the frames resulting from the `child`. + fill: Paint, + /// The content whose frames should be filled. + child: Content, +} + +#[node(LayoutBlock)] +impl FillNode {} + +impl LayoutBlock for FillNode { + fn layout_block( + &self, + world: Tracked, + styles: StyleChain, + regions: &Regions, + ) -> SourceResult> { + let mut frames = self.child.layout_block(world, styles, regions)?; + for frame in &mut frames { + let shape = Geometry::Rect(frame.size()).filled(self.fill); + frame.prepend(Point::zero(), Element::Shape(shape)); + } + Ok(frames) + } +} + +/// Stroke the frames resulting from content. +#[derive(Debug, Hash)] +struct StrokeNode { + /// How to stroke the frames resulting from the `child`. + stroke: Stroke, + /// The content whose frames should be stroked. + child: Content, +} + +#[node(LayoutBlock)] +impl StrokeNode {} + +impl LayoutBlock for StrokeNode { + fn layout_block( + &self, + world: Tracked, + styles: StyleChain, + regions: &Regions, + ) -> SourceResult> { + let mut frames = self.child.layout_block(world, styles, regions)?; + for frame in &mut frames { + let shape = Geometry::Rect(frame.size()).stroked(self.stroke); + frame.prepend(Point::zero(), Element::Shape(shape)); + } + Ok(frames) + } +} diff --git a/library/src/shared/mod.rs b/library/src/shared/mod.rs new file mode 100644 index 00000000..55522190 --- /dev/null +++ b/library/src/shared/mod.rs @@ -0,0 +1,9 @@ +//! Shared definitions for the standard library. + +mod behave; +mod ext; +mod numbering; + +pub use behave::*; +pub use ext::*; +pub use numbering::*; diff --git a/library/src/shared/numbering.rs b/library/src/shared/numbering.rs new file mode 100644 index 00000000..739edafe --- /dev/null +++ b/library/src/shared/numbering.rs @@ -0,0 +1,139 @@ +use std::str::FromStr; + +use typst::model::{castable, Value}; +use typst::util::{format_eco, EcoString}; +use unscanny::Scanner; + +/// A numbering pattern for lists or headings. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct NumberingPattern { + prefix: EcoString, + numbering: NumberingKind, + upper: bool, + suffix: EcoString, +} + +impl NumberingPattern { + /// Apply the pattern to the given number. + pub fn apply(&self, n: usize) -> EcoString { + let fmt = self.numbering.apply(n); + let mid = if self.upper { fmt.to_uppercase() } else { fmt.to_lowercase() }; + format_eco!("{}{}{}", self.prefix, mid, self.suffix) + } +} + +impl FromStr for NumberingPattern { + type Err = &'static str; + + fn from_str(pattern: &str) -> Result { + let mut s = Scanner::new(pattern); + let mut prefix; + let numbering = loop { + prefix = s.before(); + match s.eat().map(|c| c.to_ascii_lowercase()) { + Some('1') => break NumberingKind::Arabic, + Some('a') => break NumberingKind::Letter, + Some('i') => break NumberingKind::Roman, + Some('*') => break NumberingKind::Symbol, + Some(_) => {} + None => Err("invalid numbering pattern")?, + } + }; + let upper = s.scout(-1).map_or(false, char::is_uppercase); + let suffix = s.after().into(); + Ok(Self { prefix: prefix.into(), numbering, upper, suffix }) + } +} + +castable! { + NumberingPattern, + Expected: "numbering pattern", + Value::Str(s) => s.parse()?, +} + +/// Different kinds of numberings. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberingKind { + Arabic, + Letter, + Roman, + Symbol, +} + +impl NumberingKind { + /// Apply the numbering to the given number. + pub fn apply(self, mut n: usize) -> EcoString { + match self { + Self::Arabic => { + format_eco!("{n}") + } + Self::Letter => { + if n == 0 { + return '-'.into(); + } + + n -= 1; + + let mut letters = vec![]; + loop { + letters.push(b'a' + (n % 26) as u8); + n /= 26; + if n == 0 { + break; + } + } + + letters.reverse(); + String::from_utf8(letters).unwrap().into() + } + Self::Roman => { + if n == 0 { + return 'N'.into(); + } + + // Adapted from Yann Villessuzanne's roman.rs under the + // Unlicense, at https://github.com/linfir/roman.rs/ + let mut fmt = EcoString::new(); + for &(name, value) in &[ + ("M̅", 1000000), + ("D̅", 500000), + ("C̅", 100000), + ("L̅", 50000), + ("X̅", 10000), + ("V̅", 5000), + ("I̅V̅", 4000), + ("M", 1000), + ("CM", 900), + ("D", 500), + ("CD", 400), + ("C", 100), + ("XC", 90), + ("L", 50), + ("XL", 40), + ("X", 10), + ("IX", 9), + ("V", 5), + ("IV", 4), + ("I", 1), + ] { + while n >= value { + n -= value; + fmt.push_str(name); + } + } + + fmt + } + Self::Symbol => { + if n == 0 { + return '-'.into(); + } + + const SYMBOLS: &[char] = &['*', '†', '‡', '§', '‖', '¶']; + let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; + let amount = ((n - 1) / SYMBOLS.len()) + 1; + std::iter::repeat(symbol).take(amount).collect() + } + } + } +} diff --git a/library/src/structure/list.rs b/library/src/structure/list.rs index c35ffd44..d1727087 100644 --- a/library/src/structure/list.rs +++ b/library/src/structure/list.rs @@ -1,8 +1,6 @@ -use unscanny::Scanner; - -use crate::base::Numbering; use crate::layout::{BlockNode, GridNode, HNode, Spacing, TrackSizing}; use crate::prelude::*; +use crate::shared::NumberingPattern; use crate::text::{ParNode, SpaceNode, TextNode}; /// An unordered (bulleted) or ordered (numbered) list. @@ -223,7 +221,7 @@ pub enum Label { /// The default labelling. Default, /// A pattern with prefix, numbering, lower / upper case and suffix. - Pattern(EcoString, Numbering, bool, EcoString), + Pattern(NumberingPattern), /// Bare content. Content(Content), /// A closure mapping from an item number to a value. @@ -231,7 +229,7 @@ pub enum Label { } impl Label { - /// Resolve the value based on the level. + /// Resolve the label based on the level. pub fn resolve( &self, world: Tracked, @@ -244,11 +242,7 @@ impl Label { ENUM => TextNode::packed(format_eco!("{}.", number)), DESC | _ => panic!("description lists don't have a label"), }, - Self::Pattern(prefix, numbering, upper, suffix) => { - let fmt = numbering.apply(number); - let mid = if *upper { fmt.to_uppercase() } else { fmt.to_lowercase() }; - TextNode::packed(format_eco!("{}{}{}", prefix, mid, suffix)) - } + Self::Pattern(pattern) => TextNode::packed(pattern.apply(number)), Self::Content(content) => content.clone(), Self::Func(func, span) => { let args = Args::new(*span, [Value::Int(number as i64)]); @@ -266,24 +260,7 @@ impl Cast> for Label { fn cast(value: Spanned) -> StrResult { match value.v { Value::None => Ok(Self::Content(Content::empty())), - Value::Str(pattern) => { - let mut s = Scanner::new(&pattern); - let mut prefix; - let numbering = loop { - prefix = s.before(); - match s.eat().map(|c| c.to_ascii_lowercase()) { - Some('1') => break Numbering::Arabic, - Some('a') => break Numbering::Letter, - Some('i') => break Numbering::Roman, - Some('*') => break Numbering::Symbol, - Some(_) => {} - None => Err("invalid pattern")?, - } - }; - let upper = s.scout(-1).map_or(false, char::is_uppercase); - let suffix = s.after().into(); - Ok(Self::Pattern(prefix.into(), numbering, upper, suffix)) - } + Value::Str(v) => Ok(Self::Pattern(v.parse()?)), Value::Content(v) => Ok(Self::Content(v)), Value::Func(v) => Ok(Self::Func(v, value.span)), v => Err(format!( diff --git a/src/model/eval.rs b/src/model/eval.rs index 6a93ca13..9ed6195e 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -59,7 +59,7 @@ pub fn eval( /// A virtual machine. /// -/// Holds the state needed to [evaluate](super::eval()) Typst sources. A new +/// Holds the state needed to [evaluate](eval) Typst sources. A new /// virtual machine is created for each module evaluation and function call. pub struct Vm<'a> { /// The compilation environment. @@ -78,7 +78,7 @@ pub struct Vm<'a> { impl<'a> Vm<'a> { /// Create a new virtual machine. - pub fn new( + pub(super) fn new( world: Tracked<'a, dyn World>, route: Tracked<'a, Route>, location: SourceId, diff --git a/tests/typ/structure/enum.typ b/tests/typ/structure/enum.typ index 4ff13ff4..40aad625 100644 --- a/tests/typ/structure/enum.typ +++ b/tests/typ/structure/enum.typ @@ -57,9 +57,9 @@ No enum --- -// Error: 18-20 invalid pattern +// Error: 18-20 invalid numbering pattern #set enum(label: "") --- -// Error: 18-24 invalid pattern +// Error: 18-24 invalid numbering pattern #set enum(label: "(())") -- cgit v1.2.3