diff --git a/Cargo.lock b/Cargo.lock index 0bd6322a8..222e1c985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1182,7 +1182,6 @@ dependencies = [ "unicode-bidi", "unicode-math", "unicode-script", - "unscanny", "xi-unicode", ] diff --git a/library/Cargo.toml b/library/Cargo.toml index e0a0c1f61..92bf84a28 100644 --- a/library/Cargo.toml +++ b/library/Cargo.toml @@ -27,5 +27,4 @@ typed-arena = "2" unicode-bidi = "0.3.5" unicode-math = { git = "https://github.com/s3bk/unicode-math/" } unicode-script = "0.5" -unscanny = "0.1" xi-unicode = "0.3" diff --git a/library/src/basics/list.rs b/library/src/basics/list.rs index f5a589772..16c2ba646 100644 --- a/library/src/basics/list.rs +++ b/library/src/basics/list.rs @@ -44,12 +44,13 @@ impl ListNode { .map(|body| ListItem::List(Box::new(body))) .collect(), ENUM => { - let mut number: usize = args.named("start")?.unwrap_or(1); + 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 += 1; + number = number.saturating_add(1); item }) .collect() @@ -83,7 +84,7 @@ impl Layout for ListNode { regions: &Regions, ) -> SourceResult { let mut cells = vec![]; - let mut number = 1; + let mut number = NonZeroUsize::new(1).unwrap(); let label = styles.get(Self::LABEL); let indent = styles.get(Self::INDENT); @@ -124,7 +125,7 @@ impl Layout for ListNode { }; cells.push(body.styled_with_map(map.clone())); - number += 1; + number = number.saturating_add(1); } GridNode { @@ -147,7 +148,7 @@ pub enum ListItem { /// An item of an unordered list. List(Box), /// An item of an ordered list. - Enum(Option, Box), + Enum(Option, Box), /// An item of a description list. Desc(Box), } @@ -168,7 +169,7 @@ impl ListItem { Self::List(body) => Value::Content(body.as_ref().clone()), Self::Enum(number, body) => Value::Dict(dict! { "number" => match *number { - Some(n) => Value::Int(n as i64), + Some(n) => Value::Int(n.get() as i64), None => Value::None, }, "body" => Value::Content(body.as_ref().clone()), @@ -234,7 +235,7 @@ impl Label { &self, vt: &Vt, kind: ListKind, - number: usize, + number: NonZeroUsize, ) -> SourceResult { Ok(match self { Self::Default => match kind { @@ -242,10 +243,10 @@ impl Label { ENUM => TextNode::packed(format_eco!("{}.", number)), DESC | _ => panic!("description lists don't have a label"), }, - Self::Pattern(pattern) => TextNode::packed(pattern.apply(number)), + 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)]); + let args = Args::new(*span, [Value::Int(number.get() as i64)]); func.call_detached(vt.world(), args)?.display() } }) diff --git a/library/src/compute/utility.rs b/library/src/compute/utility.rs index 2b04dfd6f..196f83685 100644 --- a/library/src/compute/utility.rs +++ b/library/src/compute/utility.rs @@ -1,8 +1,7 @@ use std::str::FromStr; -use unscanny::Scanner; - use crate::prelude::*; +use crate::text::Case; /// Create a blind text string. pub fn lorem(_: &Vm, args: &mut Args) -> SourceResult { @@ -12,9 +11,9 @@ pub fn lorem(_: &Vm, args: &mut Args) -> SourceResult { /// Apply a numbering pattern to a number. pub fn numbering(_: &Vm, args: &mut Args) -> SourceResult { - let number = args.expect::("number")?; let pattern = args.expect::("pattern")?; - Ok(Value::Str(pattern.apply(number).into())) + let numbers = args.all::()?; + Ok(Value::Str(pattern.apply(&numbers).into())) } /// How to turn a number into text. @@ -28,18 +27,34 @@ pub fn numbering(_: &Vm, args: &mut Args) -> SourceResult { /// - `(I)` #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct NumberingPattern { - prefix: EcoString, - numbering: NumberingKind, - upper: bool, + pieces: Vec<(EcoString, NumberingKind, Case)>, 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) + pub fn apply(&self, numbers: &[NonZeroUsize]) -> EcoString { + let mut fmt = EcoString::new(); + let mut numbers = numbers.into_iter(); + + for ((prefix, kind, case), &n) in self.pieces.iter().zip(&mut numbers) { + fmt.push_str(prefix); + fmt.push_str(&kind.apply(n, *case)); + } + + for ((prefix, kind, case), &n) in + self.pieces.last().into_iter().cycle().zip(numbers) + { + if prefix.is_empty() { + fmt.push_str(&self.suffix); + } else { + fmt.push_str(prefix); + } + fmt.push_str(&kind.apply(n, *case)); + } + + fmt.push_str(&self.suffix); + fmt } } @@ -47,22 +62,30 @@ 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 }) + let mut pieces = vec![]; + let mut handled = 0; + + for (i, c) in pattern.char_indices() { + let kind = match c.to_ascii_lowercase() { + '1' => NumberingKind::Arabic, + 'a' => NumberingKind::Letter, + 'i' => NumberingKind::Roman, + '*' => NumberingKind::Symbol, + _ => continue, + }; + + let prefix = pattern[handled..i].into(); + let case = if c.is_uppercase() { Case::Upper } else { Case::Lower }; + pieces.push((prefix, kind, case)); + handled = i + 1; + } + + let suffix = pattern[handled..].into(); + if pieces.is_empty() { + Err("invalid numbering pattern")?; + } + + Ok(Self { pieces, suffix }) } } @@ -83,21 +106,22 @@ enum NumberingKind { impl NumberingKind { /// Apply the numbering to the given number. - pub fn apply(self, mut n: usize) -> EcoString { + pub fn apply(self, n: NonZeroUsize, case: Case) -> EcoString { + let mut n = n.get(); 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); + let c = b'a' + (n % 26) as u8; + letters.push(match case { + Case::Lower => c, + Case::Upper => c.to_ascii_uppercase(), + }); n /= 26; if n == 0 { break; @@ -108,10 +132,6 @@ impl NumberingKind { 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(); @@ -139,17 +159,18 @@ impl NumberingKind { ] { while n >= value { n -= value; - fmt.push_str(name); + for c in name.chars() { + match case { + Case::Lower => fmt.extend(c.to_lowercase()), + Case::Upper => fmt.push(c), + } + } } } 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; diff --git a/src/model/library.rs b/src/model/library.rs index eee696753..c890fef17 100644 --- a/src/model/library.rs +++ b/src/model/library.rs @@ -60,7 +60,7 @@ pub struct LangItems { /// An item in an unordered list: `- ...`. pub list_item: fn(body: Content) -> Content, /// An item in an enumeration (ordered list): `+ ...` or `1. ...`. - pub enum_item: fn(number: Option, body: Content) -> Content, + pub enum_item: fn(number: Option, body: Content) -> Content, /// An item in a description list: `/ Term: Details`. pub desc_item: fn(term: Content, body: Content) -> Content, /// A mathematical formula: `$x$`, `$ x^2 $`. diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 81ddd596f..3c60acbb8 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -365,7 +365,7 @@ node! { impl EnumItem { /// The explicit numbering, if any: `23.`. - pub fn number(&self) -> Option { + pub fn number(&self) -> Option { self.0.children().find_map(|node| match node.kind() { SyntaxKind::EnumNumbering(num) => Some(*num), _ => None, diff --git a/src/syntax/kind.rs b/src/syntax/kind.rs index b7ee6a793..a7425d70a 100644 --- a/src/syntax/kind.rs +++ b/src/syntax/kind.rs @@ -1,4 +1,5 @@ use std::hash::{Hash, Hasher}; +use std::num::NonZeroUsize; use std::sync::Arc; use crate::geom::{AbsUnit, AngleUnit}; @@ -164,7 +165,7 @@ pub enum SyntaxKind { /// An item in an enumeration (ordered list): `+ ...` or `1. ...`. EnumItem, /// An explicit enumeration numbering: `23.`. - EnumNumbering(usize), + EnumNumbering(NonZeroUsize), /// An item in a description list: `/ Term: Details`. DescItem, /// A mathematical formula: `$x$`, `$ x^2 $`. diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index e0ef2fa1a..e7015bb2e 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -1,3 +1,4 @@ +use std::num::NonZeroUsize; use std::sync::Arc; use unicode_xid::UnicodeXID; @@ -395,8 +396,11 @@ impl<'s> Tokens<'s> { self.s.eat_while(char::is_ascii_digit); let read = self.s.from(start); if self.s.eat_if('.') { - if let Ok(number) = read.parse() { - return SyntaxKind::EnumNumbering(number); + if let Ok(number) = read.parse::() { + return match NonZeroUsize::new(number) { + Some(number) => SyntaxKind::EnumNumbering(number), + None => SyntaxKind::Error(ErrorPos::Full, "must be positive".into()), + }; } } @@ -933,8 +937,8 @@ mod tests { t!(Markup["a "]: r"a--" => Text("a"), Shorthand('\u{2013}')); t!(Markup["a1/"]: "- " => Minus, Space(0)); t!(Markup[" "]: "+" => Plus); - t!(Markup[" "]: "1." => EnumNumbering(1)); - t!(Markup[" "]: "1.a" => EnumNumbering(1), Text("a")); + t!(Markup[" "]: "1." => EnumNumbering(NonZeroUsize::new(1).unwrap())); + t!(Markup[" "]: "1.a" => EnumNumbering(NonZeroUsize::new(1).unwrap()), Text("a")); t!(Markup[" /"]: "a1." => Text("a1.")); } diff --git a/src/util/eco.rs b/src/util/eco.rs index 5a4d76293..8f5195044 100644 --- a/src/util/eco.rs +++ b/src/util/eco.rs @@ -368,6 +368,14 @@ impl FromIterator for EcoString { } } +impl Extend for EcoString { + fn extend>(&mut self, iter: T) { + for c in iter { + self.push(c); + } + } +} + impl From for String { fn from(s: EcoString) -> Self { match s.0 { diff --git a/tests/ref/compute/utility.png b/tests/ref/compute/utility.png index 035ce431d..79e3096d4 100644 Binary files a/tests/ref/compute/utility.png and b/tests/ref/compute/utility.png differ diff --git a/tests/typ/basics/enum.typ b/tests/typ/basics/enum.typ index d4c30385f..0c62a2de9 100644 --- a/tests/typ/basics/enum.typ +++ b/tests/typ/basics/enum.typ @@ -12,7 +12,7 @@ --- // Test automatic numbering in summed content. #for i in range(5) { - [+ #numbering(1 + i, "I")] + [+ #numbering("I", 1 + i)] } --- @@ -42,7 +42,7 @@ start: 4, spacing: 0.65em - 3pt, tight: false, - label: n => text(fill: (red, green, blue)(mod(n, 3)), numbering(n, "A")), + label: n => text(fill: (red, green, blue)(mod(n, 3)), numbering("A", n)), [Red], [Green], [Blue], ) diff --git a/tests/typ/compute/utility.typ b/tests/typ/compute/utility.typ index f042c7697..cfc2e8af1 100644 --- a/tests/typ/compute/utility.typ +++ b/tests/typ/compute/utility.typ @@ -32,13 +32,18 @@ #lorem() --- -#for i in range(9) { - numbering(i, "* and ") - numbering(i, "I") +#for i in range(1, 9) { + numbering("*", i) + [ and ] + numbering("I.a", i, i) [ for #i] parbreak() } --- -// Error: 12-14 must be at least zero -#numbering(-1, "1") +// Error: 17-18 must be positive +#numbering("1", 0) + +--- +// Error: 17-19 must be positive +#numbering("1", -1)