Skip to main content

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}