typst_library/foundations/
symbol.rs

1use std::cmp::Reverse;
2use std::collections::{BTreeSet, HashMap};
3use std::fmt::{self, Debug, Display, Formatter, Write};
4use std::sync::Arc;
5
6use ecow::{eco_format, EcoString};
7use serde::{Serialize, Serializer};
8use typst_syntax::{is_ident, Span, Spanned};
9use typst_utils::hash128;
10
11use crate::diag::{bail, SourceResult, StrResult};
12use crate::foundations::{
13    cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed,
14    PlainText, Repr as _,
15};
16
17/// A Unicode symbol.
18///
19/// Typst defines common symbols so that they can easily be written with
20/// standard keyboards. The symbols are defined in modules, from which they can
21/// be accessed using [field access notation]($scripting/#fields):
22///
23/// - General symbols are defined in the [`sym` module]($category/symbols/sym)
24/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji)
25///
26/// Moreover, you can define custom symbols with this type's constructor
27/// function.
28///
29/// ```example
30/// #sym.arrow.r \
31/// #sym.gt.eq.not \
32/// $gt.eq.not$ \
33/// #emoji.face.halo
34/// ```
35///
36/// Many symbols have different variants, which can be selected by appending the
37/// modifiers with dot notation. The order of the modifiers is not relevant.
38/// Visit the documentation pages of the symbol modules and click on a symbol to
39/// see its available variants.
40///
41/// ```example
42/// $arrow.l$ \
43/// $arrow.r$ \
44/// $arrow.t.quad$
45/// ```
46#[ty(scope, cast)]
47#[derive(Debug, Clone, Eq, PartialEq, Hash)]
48pub struct Symbol(Repr);
49
50/// The internal representation.
51#[derive(Clone, Eq, PartialEq, Hash)]
52enum Repr {
53    /// A native symbol that has no named variant.
54    Single(char),
55    /// A native symbol with multiple named variants.
56    Complex(&'static [(&'static str, char)]),
57    /// A symbol with multiple named variants, where some modifiers may have
58    /// been applied. Also used for symbols defined at runtime by the user with
59    /// no modifier applied.
60    Modified(Arc<(List, EcoString)>),
61}
62
63/// A collection of symbols.
64#[derive(Clone, Eq, PartialEq, Hash)]
65enum List {
66    Static(&'static [(&'static str, char)]),
67    Runtime(Box<[(EcoString, char)]>),
68}
69
70impl Symbol {
71    /// Create a new symbol from a single character.
72    pub const fn single(c: char) -> Self {
73        Self(Repr::Single(c))
74    }
75
76    /// Create a symbol with a static variant list.
77    #[track_caller]
78    pub const fn list(list: &'static [(&'static str, char)]) -> Self {
79        debug_assert!(!list.is_empty());
80        Self(Repr::Complex(list))
81    }
82
83    /// Create a symbol with a runtime variant list.
84    #[track_caller]
85    pub fn runtime(list: Box<[(EcoString, char)]>) -> Self {
86        debug_assert!(!list.is_empty());
87        Self(Repr::Modified(Arc::new((List::Runtime(list), EcoString::new()))))
88    }
89
90    /// Get the symbol's character.
91    pub fn get(&self) -> char {
92        match &self.0 {
93            Repr::Single(c) => *c,
94            Repr::Complex(_) => find(self.variants(), "").unwrap(),
95            Repr::Modified(arc) => find(self.variants(), &arc.1).unwrap(),
96        }
97    }
98
99    /// Try to get the function associated with the symbol, if any.
100    pub fn func(&self) -> StrResult<Func> {
101        match self.get() {
102            '⌈' => Ok(crate::math::ceil::func()),
103            '⌊' => Ok(crate::math::floor::func()),
104            '–' => Ok(crate::math::accent::dash::func()),
105            '⋅' | '\u{0307}' => Ok(crate::math::accent::dot::func()),
106            '¨' => Ok(crate::math::accent::dot_double::func()),
107            '\u{20db}' => Ok(crate::math::accent::dot_triple::func()),
108            '\u{20dc}' => Ok(crate::math::accent::dot_quad::func()),
109            '∼' => Ok(crate::math::accent::tilde::func()),
110            '´' => Ok(crate::math::accent::acute::func()),
111            '˝' => Ok(crate::math::accent::acute_double::func()),
112            '˘' => Ok(crate::math::accent::breve::func()),
113            'ˇ' => Ok(crate::math::accent::caron::func()),
114            '^' => Ok(crate::math::accent::hat::func()),
115            '`' => Ok(crate::math::accent::grave::func()),
116            '¯' => Ok(crate::math::accent::macron::func()),
117            '○' => Ok(crate::math::accent::circle::func()),
118            '→' => Ok(crate::math::accent::arrow::func()),
119            '←' => Ok(crate::math::accent::arrow_l::func()),
120            '↔' => Ok(crate::math::accent::arrow_l_r::func()),
121            '⇀' => Ok(crate::math::accent::harpoon::func()),
122            '↼' => Ok(crate::math::accent::harpoon_lt::func()),
123            _ => bail!("symbol {self} is not callable"),
124        }
125    }
126
127    /// Apply a modifier to the symbol.
128    pub fn modified(mut self, modifier: &str) -> StrResult<Self> {
129        if let Repr::Complex(list) = self.0 {
130            self.0 = Repr::Modified(Arc::new((List::Static(list), EcoString::new())));
131        }
132
133        if let Repr::Modified(arc) = &mut self.0 {
134            let (list, modifiers) = Arc::make_mut(arc);
135            if !modifiers.is_empty() {
136                modifiers.push('.');
137            }
138            modifiers.push_str(modifier);
139            if find(list.variants(), modifiers).is_some() {
140                return Ok(self);
141            }
142        }
143
144        bail!("unknown symbol modifier")
145    }
146
147    /// The characters that are covered by this symbol.
148    pub fn variants(&self) -> impl Iterator<Item = (&str, char)> {
149        match &self.0 {
150            Repr::Single(c) => Variants::Single(Some(*c).into_iter()),
151            Repr::Complex(list) => Variants::Static(list.iter()),
152            Repr::Modified(arc) => arc.0.variants(),
153        }
154    }
155
156    /// Possible modifiers.
157    pub fn modifiers(&self) -> impl Iterator<Item = &str> + '_ {
158        let mut set = BTreeSet::new();
159        let modifiers = match &self.0 {
160            Repr::Modified(arc) => arc.1.as_str(),
161            _ => "",
162        };
163        for modifier in self.variants().flat_map(|(name, _)| name.split('.')) {
164            if !modifier.is_empty() && !contained(modifiers, modifier) {
165                set.insert(modifier);
166            }
167        }
168        set.into_iter()
169    }
170}
171
172#[scope]
173impl Symbol {
174    /// Create a custom symbol with modifiers.
175    ///
176    /// ```example
177    /// #let envelope = symbol(
178    ///   "🖂",
179    ///   ("stamped", "🖃"),
180    ///   ("stamped.pen", "🖆"),
181    ///   ("lightning", "🖄"),
182    ///   ("fly", "🖅"),
183    /// )
184    ///
185    /// #envelope
186    /// #envelope.stamped
187    /// #envelope.stamped.pen
188    /// #envelope.lightning
189    /// #envelope.fly
190    /// ```
191    #[func(constructor)]
192    pub fn construct(
193        span: Span,
194        /// The variants of the symbol.
195        ///
196        /// Can be a just a string consisting of a single character for the
197        /// modifierless variant or an array with two strings specifying the modifiers
198        /// and the symbol. Individual modifiers should be separated by dots. When
199        /// displaying a symbol, Typst selects the first from the variants that have
200        /// all attached modifiers and the minimum number of other modifiers.
201        #[variadic]
202        variants: Vec<Spanned<SymbolVariant>>,
203    ) -> SourceResult<Symbol> {
204        if variants.is_empty() {
205            bail!(span, "expected at least one variant");
206        }
207
208        // Maps from canonicalized 128-bit hashes to indices of variants we've
209        // seen before.
210        let mut seen = HashMap::<u128, usize>::new();
211
212        // A list of modifiers, cleared & reused in each iteration.
213        let mut modifiers = Vec::new();
214
215        // Validate the variants.
216        for (i, &Spanned { ref v, span }) in variants.iter().enumerate() {
217            modifiers.clear();
218
219            if !v.0.is_empty() {
220                // Collect all modifiers.
221                for modifier in v.0.split('.') {
222                    if !is_ident(modifier) {
223                        bail!(span, "invalid symbol modifier: {}", modifier.repr());
224                    }
225                    modifiers.push(modifier);
226                }
227            }
228
229            // Canonicalize the modifier order.
230            modifiers.sort();
231
232            // Ensure that there are no duplicate modifiers.
233            if let Some(ms) = modifiers.windows(2).find(|ms| ms[0] == ms[1]) {
234                bail!(
235                    span, "duplicate modifier within variant: {}", ms[0].repr();
236                    hint: "modifiers are not ordered, so each one may appear only once"
237                )
238            }
239
240            // Check whether we had this set of modifiers before.
241            let hash = hash128(&modifiers);
242            if let Some(&i) = seen.get(&hash) {
243                if v.0.is_empty() {
244                    bail!(span, "duplicate default variant");
245                } else if v.0 == variants[i].v.0 {
246                    bail!(span, "duplicate variant: {}", v.0.repr());
247                } else {
248                    bail!(
249                        span, "duplicate variant: {}", v.0.repr();
250                        hint: "variants with the same modifiers are identical, regardless of their order"
251                    )
252                }
253            }
254
255            seen.insert(hash, i);
256        }
257
258        let list = variants.into_iter().map(|s| (s.v.0, s.v.1)).collect();
259        Ok(Symbol::runtime(list))
260    }
261}
262
263impl Display for Symbol {
264    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
265        f.write_char(self.get())
266    }
267}
268
269impl Debug for Repr {
270    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
271        match self {
272            Self::Single(c) => Debug::fmt(c, f),
273            Self::Complex(list) => list.fmt(f),
274            Self::Modified(lists) => lists.fmt(f),
275        }
276    }
277}
278
279impl Debug for List {
280    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
281        match self {
282            Self::Static(list) => list.fmt(f),
283            Self::Runtime(list) => list.fmt(f),
284        }
285    }
286}
287
288impl crate::foundations::Repr for Symbol {
289    fn repr(&self) -> EcoString {
290        match &self.0 {
291            Repr::Single(c) => eco_format!("symbol(\"{}\")", *c),
292            Repr::Complex(variants) => {
293                eco_format!("symbol{}", repr_variants(variants.iter().copied(), ""))
294            }
295            Repr::Modified(arc) => {
296                let (list, modifiers) = arc.as_ref();
297                if modifiers.is_empty() {
298                    eco_format!("symbol{}", repr_variants(list.variants(), ""))
299                } else {
300                    eco_format!("symbol{}", repr_variants(list.variants(), modifiers))
301                }
302            }
303        }
304    }
305}
306
307fn repr_variants<'a>(
308    variants: impl Iterator<Item = (&'a str, char)>,
309    applied_modifiers: &str,
310) -> String {
311    crate::foundations::repr::pretty_array_like(
312        &variants
313            .filter(|(variant, _)| {
314                // Only keep variants that can still be accessed, i.e., variants
315                // that contain all applied modifiers.
316                parts(applied_modifiers).all(|am| variant.split('.').any(|m| m == am))
317            })
318            .map(|(variant, c)| {
319                let trimmed_variant = variant
320                    .split('.')
321                    .filter(|&m| parts(applied_modifiers).all(|am| m != am));
322                if trimmed_variant.clone().all(|m| m.is_empty()) {
323                    eco_format!("\"{c}\"")
324                } else {
325                    let trimmed_modifiers = trimmed_variant.collect::<Vec<_>>().join(".");
326                    eco_format!("(\"{}\", \"{}\")", trimmed_modifiers, c)
327                }
328            })
329            .collect::<Vec<_>>(),
330        false,
331    )
332}
333
334impl Serialize for Symbol {
335    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
336    where
337        S: Serializer,
338    {
339        serializer.serialize_char(self.get())
340    }
341}
342
343impl List {
344    /// The characters that are covered by this list.
345    fn variants(&self) -> Variants<'_> {
346        match self {
347            List::Static(list) => Variants::Static(list.iter()),
348            List::Runtime(list) => Variants::Runtime(list.iter()),
349        }
350    }
351}
352
353/// A value that can be cast to a symbol.
354pub struct SymbolVariant(EcoString, char);
355
356cast! {
357    SymbolVariant,
358    c: char => Self(EcoString::new(), c),
359    array: Array => {
360        let mut iter = array.into_iter();
361        match (iter.next(), iter.next(), iter.next()) {
362            (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?),
363            _ => Err("variant array must contain exactly two entries")?,
364        }
365    },
366}
367
368/// Iterator over variants.
369enum Variants<'a> {
370    Single(std::option::IntoIter<char>),
371    Static(std::slice::Iter<'static, (&'static str, char)>),
372    Runtime(std::slice::Iter<'a, (EcoString, char)>),
373}
374
375impl<'a> Iterator for Variants<'a> {
376    type Item = (&'a str, char);
377
378    fn next(&mut self) -> Option<Self::Item> {
379        match self {
380            Self::Single(iter) => Some(("", iter.next()?)),
381            Self::Static(list) => list.next().copied(),
382            Self::Runtime(list) => list.next().map(|(s, c)| (s.as_str(), *c)),
383        }
384    }
385}
386
387/// Find the best symbol from the list.
388fn find<'a>(
389    variants: impl Iterator<Item = (&'a str, char)>,
390    modifiers: &str,
391) -> Option<char> {
392    let mut best = None;
393    let mut best_score = None;
394
395    // Find the best table entry with this name.
396    'outer: for candidate in variants {
397        for modifier in parts(modifiers) {
398            if !contained(candidate.0, modifier) {
399                continue 'outer;
400            }
401        }
402
403        let mut matching = 0;
404        let mut total = 0;
405        for modifier in parts(candidate.0) {
406            if contained(modifiers, modifier) {
407                matching += 1;
408            }
409            total += 1;
410        }
411
412        let score = (matching, Reverse(total));
413        if best_score.map_or(true, |b| score > b) {
414            best = Some(candidate.1);
415            best_score = Some(score);
416        }
417    }
418
419    best
420}
421
422/// Split a modifier list into its parts.
423fn parts(modifiers: &str) -> impl Iterator<Item = &str> {
424    modifiers.split('.').filter(|s| !s.is_empty())
425}
426
427/// Whether the modifier string contains the modifier `m`.
428fn contained(modifiers: &str, m: &str) -> bool {
429    parts(modifiers).any(|part| part == m)
430}
431
432/// A single character.
433#[elem(Repr, PlainText)]
434pub struct SymbolElem {
435    /// The symbol's character.
436    #[required]
437    pub text: char, // This is called `text` for consistency with `TextElem`.
438}
439
440impl SymbolElem {
441    /// Create a new packed symbol element.
442    pub fn packed(text: impl Into<char>) -> Content {
443        Self::new(text.into()).pack()
444    }
445}
446
447impl PlainText for Packed<SymbolElem> {
448    fn plain_text(&self, text: &mut EcoString) {
449        text.push(self.text);
450    }
451}
452
453impl crate::foundations::Repr for SymbolElem {
454    /// Use a custom repr that matches normal content.
455    fn repr(&self) -> EcoString {
456        eco_format!("[{}]", self.text)
457    }
458}