Skip to main content

malachite_base/chars/
typst.rs

1// Copyright © 2026 Mikhail Hogrefe
2//
3// This file is part of Malachite.
4//
5// Malachite is free software: you can redistribute it and/or modify it under the terms of the GNU
6// Lesser General Public License (LGPL) as published by the Free Software Foundation; either version
7// 3 of the License, or (at your option) any later version. See <https://www.gnu.org/licenses/>.
8use crate::chars::latex_table::LATEX_TABLE;
9use crate::strings::typst::ToTypst;
10use core::fmt::{Formatter, Result, Write};
11
12// Gives whether a `char` is a superscript or a subscript, and what it stands for.
13//
14// Which characters these are, and what each one means, is a fact about Unicode rather than about
15// any one typesetting language, and the generated LaTeX table already records it: those characters
16// are exactly the ones it spells `^0`, `_1`, and so on. Reading the answer off that table keeps
17// there from being a second table to maintain alongside it.
18fn script(c: char) -> Option<(char, &'static str)> {
19    let i = LATEX_TABLE.binary_search_by_key(&c, |&(k, _, _)| k).ok()?;
20    let (_, is_math, latex) = LATEX_TABLE[i];
21    if !is_math {
22        return None;
23    }
24    match latex.as_bytes().first() {
25        Some(&k @ (b'^' | b'_')) => Some((char::from(k), &latex[1..])),
26        _ => None,
27    }
28}
29
30// Writes a sequence of `char`s as one Typst math-mode fragment.
31//
32// Typst reads Unicode natively, and a quoted string renders its contents as text, so almost every
33// string is one string literal and nothing else. Only `\` and `"` have to be escaped, and only
34// control characters have to be spelled rather than written, since a raw one in a string literal is
35// at best invisible.
36//
37// The superscript and subscript characters are the exception. A text font often lacks the
38// Superscripts and Subscripts block, so a literal `⁰` may not render at all, and a run of them is
39// what the reader means as one script in any case.
40pub(crate) fn fmt_typst_chars<I: Iterator<Item = char>>(cs: I, f: &mut Formatter) -> Result {
41    let mut cs = cs.peekable();
42    let mut in_string = false;
43    let mut any = false;
44    // Whether a script written next would have nothing to attach to: either nothing has been
45    // written yet, or what was written last is itself a script, and a second one on the same base
46    // would stack the two rather than set them side by side.
47    let mut needs_base = true;
48    while let Some(c) = cs.next() {
49        any = true;
50        if let Some((kind, head)) = script(c) {
51            if in_string {
52                f.write_char('"')?;
53                in_string = false;
54            }
55            if needs_base {
56                f.write_str("\"\"")?;
57            }
58            f.write_char(kind)?;
59            // A run of them is one script, so that "2¹⁰" is two raised to the tenth rather than
60            // two raised to the first and then to the zeroth. The parentheses are what make it one,
61            // and they group without being drawn.
62            //
63            // What they hold is a quoted string, like the rest of the fragment. That is not only
64            // for consistency: `'⁽'` stands for a parenthesis, and written bare it would close
65            // the script's own grouping, leaving `^(()`.
66            f.write_str("(\"")?;
67            f.write_str(head)?;
68            while let Some((k, tail)) = cs.peek().copied().and_then(script) {
69                if k != kind {
70                    break;
71                }
72                cs.next();
73                f.write_str(tail)?;
74            }
75            f.write_str("\")")?;
76            needs_base = true;
77            continue;
78        }
79        needs_base = false;
80        if !in_string {
81            f.write_char('"')?;
82            in_string = true;
83        }
84        match c {
85            '\\' => f.write_str("\\\\")?,
86            '"' => f.write_str("\\\"")?,
87            '\n' => f.write_str("\\n")?,
88            '\r' => f.write_str("\\r")?,
89            '\t' => f.write_str("\\t")?,
90            _ if c.is_control() => write!(f, "\\u{{{:x}}}", u32::from(c))?,
91            _ => f.write_char(c)?,
92        }
93    }
94    if in_string {
95        f.write_char('"')
96    } else if any {
97        Ok(())
98    } else {
99        // An empty sequence still says "this is a string", rather than vanishing.
100        f.write_str("\"\"")
101    }
102}
103
104impl ToTypst for char {
105    /// Writes a [`char`] as a Typst math-mode fragment.
106    ///
107    /// The character is written inside a quoted string, where Typst typesets it as itself. Typst
108    /// reads Unicode natively, so a character needs no spelling of its own; only `\` and `"`, and
109    /// the control characters, are written as escapes.
110    ///
111    /// A superscript or subscript character is the exception: it becomes a real script, and is
112    /// given an empty base to attach to, since on its own it has none. `'²'` becomes `""^(2)`.
113    ///
114    /// # Worst-case complexity
115    /// Constant time and additional memory.
116    ///
117    /// # Examples
118    /// ```
119    /// use malachite_base::strings::typst::ToTypst;
120    ///
121    /// assert_eq!('a'.to_typst_string(), r#""a""#);
122    /// assert_eq!('%'.to_typst_string(), r#""%""#);
123    /// assert_eq!('α'.to_typst_string(), r#""α""#);
124    /// assert_eq!('"'.to_typst_string(), r#""\"""#);
125    /// assert_eq!('²'.to_typst_string(), r#"""^("2")"#);
126    /// ```
127    ///
128    /// | value | fragment |
129    /// |-------|----------|
130    /// | `'a'` | `"a"`    |
131    /// | `'%'` | `"%"`    |
132    /// | `'α'` | `"α"`    |
133    /// | `'"'` | `"\""`   |
134    /// | `'²'` | `""^("2")` |
135    #[inline]
136    fn fmt_typst(&self, f: &mut Formatter) -> Result {
137        fmt_typst_chars(core::iter::once(*self), f)
138    }
139}