mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
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-bidi",
|
||||||
"unicode-math",
|
"unicode-math",
|
||||||
"unicode-script",
|
"unicode-script",
|
||||||
"unscanny",
|
|
||||||
"xi-unicode",
|
"xi-unicode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -27,5 +27,4 @@ typed-arena = "2"
|
|||||||
unicode-bidi = "0.3.5"
|
unicode-bidi = "0.3.5"
|
||||||
unicode-math = { git = "https://github.com/s3bk/unicode-math/" }
|
unicode-math = { git = "https://github.com/s3bk/unicode-math/" }
|
||||||
unicode-script = "0.5"
|
unicode-script = "0.5"
|
||||||
unscanny = "0.1"
|
|
||||||
xi-unicode = "0.3"
|
xi-unicode = "0.3"
|
||||||
|
@ -44,12 +44,13 @@ impl<const L: ListKind> ListNode<L> {
|
|||||||
.map(|body| ListItem::List(Box::new(body)))
|
.map(|body| ListItem::List(Box::new(body)))
|
||||||
.collect(),
|
.collect(),
|
||||||
ENUM => {
|
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()?
|
args.all()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|body| {
|
.map(|body| {
|
||||||
let item = ListItem::Enum(Some(number), Box::new(body));
|
let item = ListItem::Enum(Some(number), Box::new(body));
|
||||||
number += 1;
|
number = number.saturating_add(1);
|
||||||
item
|
item
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@ -83,7 +84,7 @@ impl<const L: ListKind> Layout for ListNode<L> {
|
|||||||
regions: &Regions,
|
regions: &Regions,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
let mut cells = vec![];
|
let mut cells = vec![];
|
||||||
let mut number = 1;
|
let mut number = NonZeroUsize::new(1).unwrap();
|
||||||
|
|
||||||
let label = styles.get(Self::LABEL);
|
let label = styles.get(Self::LABEL);
|
||||||
let indent = styles.get(Self::INDENT);
|
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()));
|
cells.push(body.styled_with_map(map.clone()));
|
||||||
number += 1;
|
number = number.saturating_add(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
GridNode {
|
GridNode {
|
||||||
@ -147,7 +148,7 @@ pub enum ListItem {
|
|||||||
/// An item of an unordered list.
|
/// An item of an unordered list.
|
||||||
List(Box<Content>),
|
List(Box<Content>),
|
||||||
/// An item of an ordered list.
|
/// An item of an ordered list.
|
||||||
Enum(Option<usize>, Box<Content>),
|
Enum(Option<NonZeroUsize>, Box<Content>),
|
||||||
/// An item of a description list.
|
/// An item of a description list.
|
||||||
Desc(Box<DescItem>),
|
Desc(Box<DescItem>),
|
||||||
}
|
}
|
||||||
@ -168,7 +169,7 @@ impl ListItem {
|
|||||||
Self::List(body) => Value::Content(body.as_ref().clone()),
|
Self::List(body) => Value::Content(body.as_ref().clone()),
|
||||||
Self::Enum(number, body) => Value::Dict(dict! {
|
Self::Enum(number, body) => Value::Dict(dict! {
|
||||||
"number" => match *number {
|
"number" => match *number {
|
||||||
Some(n) => Value::Int(n as i64),
|
Some(n) => Value::Int(n.get() as i64),
|
||||||
None => Value::None,
|
None => Value::None,
|
||||||
},
|
},
|
||||||
"body" => Value::Content(body.as_ref().clone()),
|
"body" => Value::Content(body.as_ref().clone()),
|
||||||
@ -234,7 +235,7 @@ impl Label {
|
|||||||
&self,
|
&self,
|
||||||
vt: &Vt,
|
vt: &Vt,
|
||||||
kind: ListKind,
|
kind: ListKind,
|
||||||
number: usize,
|
number: NonZeroUsize,
|
||||||
) -> SourceResult<Content> {
|
) -> SourceResult<Content> {
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
Self::Default => match kind {
|
Self::Default => match kind {
|
||||||
@ -242,10 +243,10 @@ impl Label {
|
|||||||
ENUM => TextNode::packed(format_eco!("{}.", number)),
|
ENUM => TextNode::packed(format_eco!("{}.", number)),
|
||||||
DESC | _ => panic!("description lists don't have a label"),
|
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::Content(content) => content.clone(),
|
||||||
Self::Func(func, span) => {
|
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()
|
func.call_detached(vt.world(), args)?.display()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use unscanny::Scanner;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::text::Case;
|
||||||
|
|
||||||
/// Create a blind text string.
|
/// Create a blind text string.
|
||||||
pub fn lorem(_: &Vm, args: &mut Args) -> SourceResult<Value> {
|
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.
|
/// Apply a numbering pattern to a number.
|
||||||
pub fn numbering(_: &Vm, args: &mut Args) -> SourceResult<Value> {
|
pub fn numbering(_: &Vm, args: &mut Args) -> SourceResult<Value> {
|
||||||
let number = args.expect::<usize>("number")?;
|
|
||||||
let pattern = args.expect::<NumberingPattern>("pattern")?;
|
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.
|
/// How to turn a number into text.
|
||||||
@ -28,18 +27,34 @@ pub fn numbering(_: &Vm, args: &mut Args) -> SourceResult<Value> {
|
|||||||
/// - `(I)`
|
/// - `(I)`
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct NumberingPattern {
|
pub struct NumberingPattern {
|
||||||
prefix: EcoString,
|
pieces: Vec<(EcoString, NumberingKind, Case)>,
|
||||||
numbering: NumberingKind,
|
|
||||||
upper: bool,
|
|
||||||
suffix: EcoString,
|
suffix: EcoString,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NumberingPattern {
|
impl NumberingPattern {
|
||||||
/// Apply the pattern to the given number.
|
/// Apply the pattern to the given number.
|
||||||
pub fn apply(&self, n: usize) -> EcoString {
|
pub fn apply(&self, numbers: &[NonZeroUsize]) -> EcoString {
|
||||||
let fmt = self.numbering.apply(n);
|
let mut fmt = EcoString::new();
|
||||||
let mid = if self.upper { fmt.to_uppercase() } else { fmt.to_lowercase() };
|
let mut numbers = numbers.into_iter();
|
||||||
format_eco!("{}{}{}", self.prefix, mid, self.suffix)
|
|
||||||
|
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;
|
type Err = &'static str;
|
||||||
|
|
||||||
fn from_str(pattern: &str) -> Result<Self, Self::Err> {
|
fn from_str(pattern: &str) -> Result<Self, Self::Err> {
|
||||||
let mut s = Scanner::new(pattern);
|
let mut pieces = vec![];
|
||||||
let mut prefix;
|
let mut handled = 0;
|
||||||
let numbering = loop {
|
|
||||||
prefix = s.before();
|
for (i, c) in pattern.char_indices() {
|
||||||
match s.eat().map(|c| c.to_ascii_lowercase()) {
|
let kind = match c.to_ascii_lowercase() {
|
||||||
Some('1') => break NumberingKind::Arabic,
|
'1' => NumberingKind::Arabic,
|
||||||
Some('a') => break NumberingKind::Letter,
|
'a' => NumberingKind::Letter,
|
||||||
Some('i') => break NumberingKind::Roman,
|
'i' => NumberingKind::Roman,
|
||||||
Some('*') => break NumberingKind::Symbol,
|
'*' => NumberingKind::Symbol,
|
||||||
Some(_) => {}
|
_ => continue,
|
||||||
None => Err("invalid numbering pattern")?,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let upper = s.scout(-1).map_or(false, char::is_uppercase);
|
|
||||||
let suffix = s.after().into();
|
let prefix = pattern[handled..i].into();
|
||||||
Ok(Self { prefix: prefix.into(), numbering, upper, suffix })
|
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 {
|
impl NumberingKind {
|
||||||
/// Apply the numbering to the given number.
|
/// 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 {
|
match self {
|
||||||
Self::Arabic => {
|
Self::Arabic => {
|
||||||
format_eco!("{n}")
|
format_eco!("{n}")
|
||||||
}
|
}
|
||||||
Self::Letter => {
|
Self::Letter => {
|
||||||
if n == 0 {
|
|
||||||
return '-'.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
n -= 1;
|
n -= 1;
|
||||||
|
|
||||||
let mut letters = vec![];
|
let mut letters = vec![];
|
||||||
loop {
|
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;
|
n /= 26;
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
break;
|
break;
|
||||||
@ -108,10 +132,6 @@ impl NumberingKind {
|
|||||||
String::from_utf8(letters).unwrap().into()
|
String::from_utf8(letters).unwrap().into()
|
||||||
}
|
}
|
||||||
Self::Roman => {
|
Self::Roman => {
|
||||||
if n == 0 {
|
|
||||||
return 'N'.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adapted from Yann Villessuzanne's roman.rs under the
|
// Adapted from Yann Villessuzanne's roman.rs under the
|
||||||
// Unlicense, at https://github.com/linfir/roman.rs/
|
// Unlicense, at https://github.com/linfir/roman.rs/
|
||||||
let mut fmt = EcoString::new();
|
let mut fmt = EcoString::new();
|
||||||
@ -139,17 +159,18 @@ impl NumberingKind {
|
|||||||
] {
|
] {
|
||||||
while n >= value {
|
while n >= value {
|
||||||
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
|
fmt
|
||||||
}
|
}
|
||||||
Self::Symbol => {
|
Self::Symbol => {
|
||||||
if n == 0 {
|
|
||||||
return '-'.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖'];
|
const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖'];
|
||||||
let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
|
let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
|
||||||
let amount = ((n - 1) / SYMBOLS.len()) + 1;
|
let amount = ((n - 1) / SYMBOLS.len()) + 1;
|
||||||
|
@ -60,7 +60,7 @@ pub struct LangItems {
|
|||||||
/// An item in an unordered list: `- ...`.
|
/// An item in an unordered list: `- ...`.
|
||||||
pub list_item: fn(body: Content) -> Content,
|
pub list_item: fn(body: Content) -> Content,
|
||||||
/// An item in an enumeration (ordered list): `+ ...` or `1. ...`.
|
/// 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`.
|
/// An item in a description list: `/ Term: Details`.
|
||||||
pub desc_item: fn(term: Content, body: Content) -> Content,
|
pub desc_item: fn(term: Content, body: Content) -> Content,
|
||||||
/// A mathematical formula: `$x$`, `$ x^2 $`.
|
/// A mathematical formula: `$x$`, `$ x^2 $`.
|
||||||
|
@ -365,7 +365,7 @@ node! {
|
|||||||
|
|
||||||
impl EnumItem {
|
impl EnumItem {
|
||||||
/// The explicit numbering, if any: `23.`.
|
/// 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() {
|
self.0.children().find_map(|node| match node.kind() {
|
||||||
SyntaxKind::EnumNumbering(num) => Some(*num),
|
SyntaxKind::EnumNumbering(num) => Some(*num),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::geom::{AbsUnit, AngleUnit};
|
use crate::geom::{AbsUnit, AngleUnit};
|
||||||
@ -164,7 +165,7 @@ pub enum SyntaxKind {
|
|||||||
/// An item in an enumeration (ordered list): `+ ...` or `1. ...`.
|
/// An item in an enumeration (ordered list): `+ ...` or `1. ...`.
|
||||||
EnumItem,
|
EnumItem,
|
||||||
/// An explicit enumeration numbering: `23.`.
|
/// An explicit enumeration numbering: `23.`.
|
||||||
EnumNumbering(usize),
|
EnumNumbering(NonZeroUsize),
|
||||||
/// An item in a description list: `/ Term: Details`.
|
/// An item in a description list: `/ Term: Details`.
|
||||||
DescItem,
|
DescItem,
|
||||||
/// A mathematical formula: `$x$`, `$ x^2 $`.
|
/// A mathematical formula: `$x$`, `$ x^2 $`.
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use unicode_xid::UnicodeXID;
|
use unicode_xid::UnicodeXID;
|
||||||
@ -395,8 +396,11 @@ impl<'s> Tokens<'s> {
|
|||||||
self.s.eat_while(char::is_ascii_digit);
|
self.s.eat_while(char::is_ascii_digit);
|
||||||
let read = self.s.from(start);
|
let read = self.s.from(start);
|
||||||
if self.s.eat_if('.') {
|
if self.s.eat_if('.') {
|
||||||
if let Ok(number) = read.parse() {
|
if let Ok(number) = read.parse::<usize>() {
|
||||||
return SyntaxKind::EnumNumbering(number);
|
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["a "]: r"a--" => Text("a"), Shorthand('\u{2013}'));
|
||||||
t!(Markup["a1/"]: "- " => Minus, Space(0));
|
t!(Markup["a1/"]: "- " => Minus, Space(0));
|
||||||
t!(Markup[" "]: "+" => Plus);
|
t!(Markup[" "]: "+" => Plus);
|
||||||
t!(Markup[" "]: "1." => EnumNumbering(1));
|
t!(Markup[" "]: "1." => EnumNumbering(NonZeroUsize::new(1).unwrap()));
|
||||||
t!(Markup[" "]: "1.a" => EnumNumbering(1), Text("a"));
|
t!(Markup[" "]: "1.a" => EnumNumbering(NonZeroUsize::new(1).unwrap()), Text("a"));
|
||||||
t!(Markup[" /"]: "a1." => Text("a1."));
|
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 {
|
impl From<EcoString> for String {
|
||||||
fn from(s: EcoString) -> Self {
|
fn from(s: EcoString) -> Self {
|
||||||
match s.0 {
|
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.
|
// Test automatic numbering in summed content.
|
||||||
#for i in range(5) {
|
#for i in range(5) {
|
||||||
[+ #numbering(1 + i, "I")]
|
[+ #numbering("I", 1 + i)]
|
||||||
}
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -42,7 +42,7 @@
|
|||||||
start: 4,
|
start: 4,
|
||||||
spacing: 0.65em - 3pt,
|
spacing: 0.65em - 3pt,
|
||||||
tight: false,
|
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],
|
[Red], [Green], [Blue],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,13 +32,18 @@
|
|||||||
#lorem()
|
#lorem()
|
||||||
|
|
||||||
---
|
---
|
||||||
#for i in range(9) {
|
#for i in range(1, 9) {
|
||||||
numbering(i, "* and ")
|
numbering("*", i)
|
||||||
numbering(i, "I")
|
[ and ]
|
||||||
|
numbering("I.a", i, i)
|
||||||
[ for #i]
|
[ for #i]
|
||||||
parbreak()
|
parbreak()
|
||||||
}
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
// Error: 12-14 must be at least zero
|
// Error: 17-18 must be positive
|
||||||
#numbering(-1, "1")
|
#numbering("1", 0)
|
||||||
|
|
||||||
|
---
|
||||||
|
// Error: 17-19 must be positive
|
||||||
|
#numbering("1", -1)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user