typst/library/src/base/numbering.rs

147 lines
4.3 KiB
Rust

use std::str::FromStr;
use unscanny::Scanner;
use crate::prelude::*;
/// 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()))
}
/// 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<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 })
}
}
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 &[
("", 1000000),
("", 500000),
("", 100000),
("", 50000),
("", 10000),
("", 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()
}
}
}
}