Extract numbering pattern from list node

This commit is contained in:
Laurenz 2022-11-26 12:49:30 +01:00
parent bf5edbbbbb
commit 3cdd8bfa40
12 changed files with 166 additions and 137 deletions

View File

@ -1,6 +1,7 @@
use typst::model::Regex;
use crate::prelude::*;
use crate::shared::NumberingKind;
/// The string representation of a value.
pub fn repr(_: &Vm, args: &mut Args) -> SourceResult<Value> {
@ -32,110 +33,20 @@ pub fn regex(_: &Vm, args: &mut Args) -> SourceResult<Value> {
/// Converts an integer into one or multiple letters.
pub fn letter(_: &Vm, args: &mut Args) -> SourceResult<Value> {
numbered(Numbering::Letter, args)
numbered(NumberingKind::Letter, args)
}
/// Converts an integer into a roman numeral.
pub fn roman(_: &Vm, args: &mut Args) -> SourceResult<Value> {
numbered(Numbering::Roman, args)
numbered(NumberingKind::Roman, args)
}
/// Convert a number into a symbol.
pub fn symbol(_: &Vm, args: &mut Args) -> SourceResult<Value> {
numbered(Numbering::Symbol, args)
numbered(NumberingKind::Symbol, args)
}
fn numbered(numbering: Numbering, args: &mut Args) -> SourceResult<Value> {
fn numbered(numbering: NumberingKind, args: &mut Args) -> SourceResult<Value> {
let n = args.expect::<usize>("non-negative integer")?;
Ok(Value::Str(numbering.apply(n).into()))
}
/// Allows to convert a number into letters, roman numerals and symbols.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Numbering {
Arabic,
Letter,
Roman,
Symbol,
}
impl Numbering {
/// 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 ROMANS {
while n >= value {
n -= value;
fmt.push_str(name);
}
}
fmt
}
Self::Symbol => {
if n == 0 {
return '-'.into();
}
let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
let amount = ((n - 1) / SYMBOLS.len()) + 1;
std::iter::repeat(symbol).take(amount).collect()
}
}
}
}
const ROMANS: &[(&str, usize)] = &[
("", 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),
];
const SYMBOLS: &[char] = &['*', '†', '‡', '§', '‖', '¶'];

View File

@ -1,7 +0,0 @@
//! Central definitions for the standard library.
mod behave;
mod ext;
pub use behave::*;
pub use ext::*;

View File

@ -37,8 +37,8 @@ use typst::model::{
};
use typst::World;
use crate::core::BehavedBuilder;
use crate::prelude::*;
use crate::shared::BehavedBuilder;
use crate::structure::{
DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST,
};

View File

@ -1,11 +1,11 @@
//! Typst's standard library.
pub mod base;
pub mod core;
pub mod graphics;
pub mod layout;
pub mod math;
pub mod prelude;
pub mod shared;
pub mod structure;
pub mod text;

View File

@ -26,7 +26,7 @@ pub use typst::util::{format_eco, EcoString};
#[doc(no_inline)]
pub use typst::World;
#[doc(no_inline)]
pub use crate::core::{Behave, Behaviour, ContentExt, StyleMapExt};
#[doc(no_inline)]
pub use crate::layout::{LayoutBlock, LayoutInline, Regions};
#[doc(no_inline)]
pub use crate::shared::{Behave, Behaviour, ContentExt, StyleMapExt};

View File

@ -0,0 +1,9 @@
//! Shared definitions for the standard library.
mod behave;
mod ext;
mod numbering;
pub use behave::*;
pub use ext::*;
pub use numbering::*;

View File

@ -0,0 +1,139 @@
use std::str::FromStr;
use typst::model::{castable, Value};
use typst::util::{format_eco, EcoString};
use unscanny::Scanner;
/// 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()
}
}
}
}

View File

@ -1,8 +1,6 @@
use unscanny::Scanner;
use crate::base::Numbering;
use crate::layout::{BlockNode, GridNode, HNode, Spacing, TrackSizing};
use crate::prelude::*;
use crate::shared::NumberingPattern;
use crate::text::{ParNode, SpaceNode, TextNode};
/// An unordered (bulleted) or ordered (numbered) list.
@ -223,7 +221,7 @@ pub enum Label {
/// The default labelling.
Default,
/// A pattern with prefix, numbering, lower / upper case and suffix.
Pattern(EcoString, Numbering, bool, EcoString),
Pattern(NumberingPattern),
/// Bare content.
Content(Content),
/// A closure mapping from an item number to a value.
@ -231,7 +229,7 @@ pub enum Label {
}
impl Label {
/// Resolve the value based on the level.
/// Resolve the label based on the level.
pub fn resolve(
&self,
world: Tracked<dyn World>,
@ -244,11 +242,7 @@ impl Label {
ENUM => TextNode::packed(format_eco!("{}.", number)),
DESC | _ => panic!("description lists don't have a label"),
},
Self::Pattern(prefix, numbering, upper, suffix) => {
let fmt = numbering.apply(number);
let mid = if *upper { fmt.to_uppercase() } else { fmt.to_lowercase() };
TextNode::packed(format_eco!("{}{}{}", prefix, mid, suffix))
}
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)]);
@ -266,24 +260,7 @@ impl Cast<Spanned<Value>> for Label {
fn cast(value: Spanned<Value>) -> StrResult<Self> {
match value.v {
Value::None => Ok(Self::Content(Content::empty())),
Value::Str(pattern) => {
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 Numbering::Arabic,
Some('a') => break Numbering::Letter,
Some('i') => break Numbering::Roman,
Some('*') => break Numbering::Symbol,
Some(_) => {}
None => Err("invalid pattern")?,
}
};
let upper = s.scout(-1).map_or(false, char::is_uppercase);
let suffix = s.after().into();
Ok(Self::Pattern(prefix.into(), numbering, upper, suffix))
}
Value::Str(v) => Ok(Self::Pattern(v.parse()?)),
Value::Content(v) => Ok(Self::Content(v)),
Value::Func(v) => Ok(Self::Func(v, value.span)),
v => Err(format!(

View File

@ -59,7 +59,7 @@ pub fn eval(
/// A virtual machine.
///
/// Holds the state needed to [evaluate](super::eval()) Typst sources. A new
/// Holds the state needed to [evaluate](eval) Typst sources. A new
/// virtual machine is created for each module evaluation and function call.
pub struct Vm<'a> {
/// The compilation environment.
@ -78,7 +78,7 @@ pub struct Vm<'a> {
impl<'a> Vm<'a> {
/// Create a new virtual machine.
pub fn new(
pub(super) fn new(
world: Tracked<'a, dyn World>,
route: Tracked<'a, Route>,
location: SourceId,

View File

@ -57,9 +57,9 @@
No enum
---
// Error: 18-20 invalid pattern
// Error: 18-20 invalid numbering pattern
#set enum(label: "")
---
// Error: 18-24 invalid pattern
// Error: 18-24 invalid numbering pattern
#set enum(label: "(())")