typst_library/model/numbering.rs
1use std::fmt::{Display, Write};
2use std::str::FromStr;
3
4use codex::numeral_systems::{NamedNumeralSystem, RepresentationError};
5use comemo::Tracked;
6use ecow::{EcoString, EcoVec};
7use typst_syntax::Span;
8
9use crate::diag::{At, SourceResult, StrResult, bail, warning};
10use crate::engine::Engine;
11use crate::foundations::{Context, Func, Str, Value, cast, func};
12
13/// Applies a numbering to a sequence of numbers.
14///
15/// A numbering defines how a sequence of numbers should be displayed as
16/// content. It is defined either through a pattern string or an arbitrary
17/// function.
18///
19/// A numbering pattern consists of counting symbols, for which the actual
20/// number is substituted, their prefixes, and one suffix. The prefixes and the
21/// suffix are displayed as-is.
22///
23/// = Example <example>
24/// ```example
25/// #numbering("1.1)", 1, 2, 3) \
26/// #numbering("1.a.i", 1, 2) \
27/// #numbering("I – 1", 12, 2) \
28/// #numbering(
29/// (..nums) => nums
30/// .pos()
31/// .map(str)
32/// .join(".") + ")",
33/// 1, 2, 3,
34/// )
35/// ```
36///
37/// = Numbering patterns and numbering functions <patterns-and-functions>
38/// There are multiple instances where you can provide a numbering pattern or
39/// function in Typst. For example, when defining how to number
40/// @heading[headings] or @figure[figures]. Every time, the expected format is
41/// the same as the one described below for the
42/// @numbering.numbering[`numbering`] parameter.
43///
44/// The following example illustrates that a numbering function is just a
45/// regular @function[function] that accepts numbers and returns @content.
46///
47/// ```example
48/// #let unary(.., last) = "|" * last
49/// #set heading(numbering: unary)
50/// = First heading
51/// = Second heading
52/// = Third heading
53/// ```
54#[func]
55pub fn numbering(
56 engine: &mut Engine,
57 context: Tracked<Context>,
58 span: Span,
59 /// Defines how the numbering works.
60 ///
61 /// *Counting symbols* are `1`, `a`, `A`, `i`, `I`, `α`, `Α`, `一`, `壹`,
62 /// `あ`, `い`, `ア`, `イ`, `א`, `가`, `ㄱ`, `*`, `١`, `۱`, `१`, `১`, `ক`,
63 /// `①`, and `⓵`. They are replaced by the number in the sequence,
64 /// preserving the original case.
65 ///
66 /// The `*` character means that symbols should be used to count, in the
67 /// order of `*`, `†`, `‡`, `§`, `¶`, `‖`. If there are more than six items,
68 /// the number is represented using repeated symbols.
69 ///
70 /// *Suffixes* are all characters after the last counting symbol. They are
71 /// displayed as-is at the end of any rendered number.
72 ///
73 /// *Prefixes* are all characters that are neither counting symbols nor
74 /// suffixes. They are displayed as-is at in front of their rendered
75 /// equivalent of their counting symbol.
76 ///
77 /// This parameter can also be an arbitrary function that gets each number
78 /// as an individual argument. When given a function, the `numbering`
79 /// function just forwards the arguments to that function. While this is not
80 /// particularly useful in itself, it means that you can just give arbitrary
81 /// numberings to the `numbering` function without caring whether they are
82 /// defined as a pattern or function.
83 numbering: Numbering,
84 /// The numbers to apply the numbering to. Must be non-negative.
85 ///
86 /// In general, numbers are counted from one. A number of zero indicates
87 /// that the first element has not yet appeared.
88 ///
89 /// If `numbering` is a pattern and more numbers than counting symbols are
90 /// given, the last counting symbol with its prefix is repeated.
91 #[variadic]
92 numbers: Vec<u64>,
93) -> SourceResult<Value> {
94 numbering.apply(engine, context, span, &numbers)
95}
96
97/// How to number a sequence of things.
98#[derive(Debug, Clone, PartialEq, Hash)]
99pub enum Numbering {
100 /// A pattern with prefix, numbering, lower / upper case and suffix.
101 Pattern(NumberingPattern),
102 /// A closure mapping from an item's number to content.
103 Func(Func),
104}
105
106impl Numbering {
107 /// Apply the pattern to the given numbers.
108 pub fn apply(
109 &self,
110 engine: &mut Engine,
111 context: Tracked<Context>,
112 span: Span,
113 numbers: &[u64],
114 ) -> SourceResult<Value> {
115 Ok(match self {
116 Self::Pattern(pattern) => {
117 Value::Str(pattern.apply(Some((engine, span)), numbers).at(span)?.into())
118 }
119 Self::Func(func) => func.call(engine, context, numbers.iter().copied())?,
120 })
121 }
122
123 /// Trim the prefix suffix if this is a pattern.
124 pub fn trimmed(mut self) -> Self {
125 if let Self::Pattern(pattern) = &mut self {
126 pattern.trimmed = true;
127 }
128 self
129 }
130}
131
132impl From<NumberingPattern> for Numbering {
133 fn from(pattern: NumberingPattern) -> Self {
134 Self::Pattern(pattern)
135 }
136}
137
138cast! {
139 Numbering,
140 self => match self {
141 Self::Pattern(pattern) => pattern.into_value(),
142 Self::Func(func) => func.into_value(),
143 },
144 v: NumberingPattern => Self::Pattern(v),
145 v: Func => Self::Func(v),
146}
147
148/// How to turn a number into text.
149///
150/// A pattern consists of a prefix, followed by one of the counter symbols (see
151/// [`numbering()`] docs), and then a suffix.
152///
153/// Examples of valid patterns:
154/// - `1)`
155/// - `a.`
156/// - `(I)`
157#[derive(Debug, Clone, Eq, PartialEq, Hash)]
158pub struct NumberingPattern {
159 pub pieces: EcoVec<(EcoString, NamedNumeralSystem)>,
160 pub suffix: EcoString,
161 trimmed: bool,
162}
163
164impl NumberingPattern {
165 /// Apply the pattern to the given number.
166 ///
167 /// If `warning_context` is not [`None`], when an error would normally be
168 /// returned, a warning is emitted instead and the returned value uses
169 /// Arabic numerals in place of the numeral system that caused the error.
170 pub fn apply(
171 &self,
172 warning_context: Option<(&mut Engine, Span)>,
173 numbers: &[u64],
174 ) -> StrResult<EcoString> {
175 if let Some((engine, span)) = warning_context {
176 self.apply_with(numbers, |system, n| {
177 Ok(apply_system_with_fallback(engine, span, system, n))
178 })
179 } else {
180 self.apply_with(numbers, apply_system)
181 }
182 }
183
184 /// Auxiliary method for [`NumberingPattern::apply`].
185 ///
186 /// Can be removed when the deprecation warnings are turned into hard
187 /// errors.
188 fn apply_with<D: Display>(
189 &self,
190 numbers: &[u64],
191 mut apply_system: impl FnMut(NamedNumeralSystem, u64) -> StrResult<D>,
192 ) -> StrResult<EcoString> {
193 let mut fmt = EcoString::new();
194 let mut numbers = numbers.iter();
195
196 for (i, ((prefix, system), &n)) in
197 self.pieces.iter().zip(&mut numbers).enumerate()
198 {
199 if i > 0 || !self.trimmed {
200 fmt.push_str(prefix);
201 }
202 write!(fmt, "{}", apply_system(*system, n)?).unwrap();
203 }
204
205 for ((prefix, system), &n) in self.pieces.last().into_iter().cycle().zip(numbers)
206 {
207 if prefix.is_empty() {
208 fmt.push_str(&self.suffix);
209 } else {
210 fmt.push_str(prefix);
211 }
212 write!(fmt, "{}", apply_system(*system, n)?).unwrap();
213 }
214
215 if !self.trimmed {
216 fmt.push_str(&self.suffix);
217 }
218
219 Ok(fmt)
220 }
221
222 /// Apply only the k-th segment of the pattern to a number.
223 pub fn apply_kth(
224 &self,
225 engine: &mut Engine,
226 span: Span,
227 k: usize,
228 number: u64,
229 ) -> EcoString {
230 let mut fmt = EcoString::new();
231 if let Some((prefix, _)) = self.pieces.first() {
232 fmt.push_str(prefix);
233 }
234 if let Some((_, system)) = self
235 .pieces
236 .iter()
237 .chain(self.pieces.last().into_iter().cycle())
238 .nth(k)
239 {
240 let represented_number =
241 apply_system_with_fallback(engine, span, *system, number);
242 write!(fmt, "{represented_number}").unwrap()
243 }
244 fmt.push_str(&self.suffix);
245 fmt
246 }
247
248 /// How many counting symbols this pattern has.
249 pub fn pieces(&self) -> usize {
250 self.pieces.len()
251 }
252}
253
254fn apply_system(system: NamedNumeralSystem, number: u64) -> StrResult<impl Display> {
255 match system.system().represent(number) {
256 Ok(represented) => Ok(represented),
257 Err(RepresentationError::Zero) => {
258 bail!("the numeral system `{}` cannot represent zero", system.name())
259 }
260 Err(RepresentationError::TooLarge) => {
261 bail!(
262 "the number {} is too large to be represented with the `{}` numeral system",
263 number,
264 system.name(),
265 )
266 }
267 }
268}
269
270/// Applies a numeral system to a number. In case of an error, fall back to
271/// Arabic numerals.
272///
273/// This is a temporary function that should be replaced by [`apply_system`]
274/// when the deprecation warning is turned into a hard error.
275fn apply_system_with_fallback(
276 engine: &mut Engine,
277 span: Span,
278 system: NamedNumeralSystem,
279 number: u64,
280) -> impl Display + use<> {
281 apply_system(system, number).unwrap_or_else(|err| {
282 engine.sink.warn(warning!(
283 span,
284 "{err}";
285 hint: "this will become a hard error in the future";
286 ));
287 apply_system(NamedNumeralSystem::Arabic, number)
288 .unwrap_or_else(|_| panic!("`arabic` should be able to represent {number}"))
289 })
290}
291
292impl FromStr for NumberingPattern {
293 type Err = &'static str;
294
295 fn from_str(pattern: &str) -> Result<Self, Self::Err> {
296 let mut pieces = EcoVec::new();
297 let mut handled = 0;
298
299 for (i, c) in pattern.char_indices() {
300 let Some(kind) =
301 NamedNumeralSystem::from_shorthand(c.encode_utf8(&mut [0; 4]))
302 else {
303 continue;
304 };
305
306 let prefix = pattern[handled..i].into();
307 pieces.push((prefix, kind));
308 handled = c.len_utf8() + i;
309 }
310
311 let suffix = pattern[handled..].into();
312 if pieces.is_empty() {
313 return Err("invalid numbering pattern");
314 }
315
316 Ok(Self { pieces, suffix, trimmed: false })
317 }
318}
319
320cast! {
321 NumberingPattern,
322 self => {
323 let mut pat = EcoString::new();
324 for (prefix, system) in &self.pieces {
325 pat.push_str(prefix);
326 pat.push_str(
327 system
328 .shorthand()
329 .expect("it is not possible to construct numbering systems that don't have a shorthand within Typst for now"),
330 );
331 }
332 pat.push_str(&self.suffix);
333 pat.into_value()
334 },
335 v: Str => v.parse()?,
336}