summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-12-02 15:47:25 +0100
committerLaurenz <laurmaedje@gmail.com>2022-12-02 15:47:25 +0100
commit56923ee472f1eaa67d3543e19372823139205885 (patch)
treeaccd9e05fb5875457967c5b456626767ff3e9c9e
parent9bc90c371fb41a2d6dc08eb4673e5be15f829514 (diff)
Multi-part numbering patterns
-rw-r--r--Cargo.lock1
-rw-r--r--library/Cargo.toml1
-rw-r--r--library/src/basics/list.rs19
-rw-r--r--library/src/compute/utility.rs105
-rw-r--r--src/model/library.rs2
-rw-r--r--src/syntax/ast.rs2
-rw-r--r--src/syntax/kind.rs3
-rw-r--r--src/syntax/tokens.rs12
-rw-r--r--src/util/eco.rs8
-rw-r--r--tests/ref/compute/utility.pngbin34441 -> 34604 bytes
-rw-r--r--tests/typ/basics/enum.typ4
-rw-r--r--tests/typ/compute/utility.typ15
12 files changed, 105 insertions, 67 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 0bd6322a..222e1c98 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 e0a0c1f6..92bf84a2 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 f5a58977..16c2ba64 100644
--- a/library/src/basics/list.rs
+++ b/library/src/basics/list.rs
@@ -44,12 +44,13 @@ impl<const L: ListKind> ListNode<L> {
.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<const L: ListKind> Layout for ListNode<L> {
regions: &Regions,
) -> SourceResult<Fragment> {
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<const L: ListKind> Layout for ListNode<L> {
};
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<Content>),
/// An item of an ordered list.
- Enum(Option<usize>, Box<Content>),
+ Enum(Option<NonZeroUsize>, Box<Content>),
/// An item of a description list.
Desc(Box<DescItem>),
}
@@ -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<Content> {
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 2b04dfd6..196f8368 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<Value> {
@@ -12,9 +11,9 @@ pub fn lorem(_: &Vm, args: &mut Args) -> SourceResult<Value> {
/// Apply a numbering pattern to a number.
pub fn numbering(_: &Vm, args: &mut Args) -> SourceResult<Value> {
- let number = args.expect::<usize>("number")?;
let pattern = args.expect::<NumberingPattern>("pattern")?;
- Ok(Value::Str(pattern.apply(number).into()))
+ let numbers = args.all::<NonZeroUsize>()?;
+ 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<Value> {
/// - `(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<Self, Self::Err> {
- 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 eee69675..c890fef1 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<usize>, body: Content) -> Content,
+ 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,
/// A mathematical formula: `$x$`, `$ x^2 $`.
diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs
index 81ddd596..3c60acbb 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<usize> {
+ pub fn number(&self) -> Option<NonZeroUsize> {
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 b7ee6a79..a7425d70 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 e0ef2fa1..e7015bb2 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::<usize>() {
+ 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 5a4d7629..8f519504 100644
--- a/src/util/eco.rs
+++ b/src/util/eco.rs
@@ -368,6 +368,14 @@ impl FromIterator<Self> for EcoString {
}
}
+impl Extend<char> for EcoString {
+ fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
+ for c in iter {
+ self.push(c);
+ }
+ }
+}
+
impl From<EcoString> 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 035ce431..79e3096d 100644
--- a/tests/ref/compute/utility.png
+++ b/tests/ref/compute/utility.png
Binary files differ
diff --git a/tests/typ/basics/enum.typ b/tests/typ/basics/enum.typ
index d4c30385..0c62a2de 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 f042c769..cfc2e8af 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)