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}