Multi-part numbering patterns
This commit is contained in:
parent
9bc90c371f
commit
56923ee472
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1182,7 +1182,6 @@ dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-math",
|
||||
"unicode-script",
|
||||
"unscanny",
|
||||
"xi-unicode",
|
||||
]
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
@ -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;
|
||||
|
@ -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 $`.
|
||||
|
@ -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,
|
||||
|
@ -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 $`.
|
||||
|
@ -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."));
|
||||
}
|
||||
|
||||
|
@ -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 |
@ -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],
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user