Multi-part numbering patterns

This commit is contained in:
Laurenz 2022-12-02 15:47:25 +01:00
parent 9bc90c371f
commit 56923ee472
12 changed files with 105 additions and 67 deletions

1
Cargo.lock generated
View File

@ -1182,7 +1182,6 @@ dependencies = [
"unicode-bidi",
"unicode-math",
"unicode-script",
"unscanny",
"xi-unicode",
]

View File

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

View File

@ -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()
}
})

View File

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

View File

@ -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 $`.

View File

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

View File

@ -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 $`.

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -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],
)

View File

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