typst_library/text/
shift.rs1use ecow::EcoString;
2
3use crate::diag::SourceResult;
4use crate::engine::Engine;
5use crate::foundations::{elem, Content, Packed, SequenceElem, Show, StyleChain};
6use crate::layout::{Em, Length};
7use crate::text::{variant, SpaceElem, TextElem, TextSize};
8use crate::World;
9
10#[elem(title = "Subscript", Show)]
19pub struct SubElem {
20 #[default(true)]
31 pub typographic: bool,
32
33 #[default(Em::new(0.2).into())]
37 pub baseline: Length,
38
39 #[default(TextSize(Em::new(0.6).into()))]
43 pub size: TextSize,
44
45 #[required]
47 pub body: Content,
48}
49
50impl Show for Packed<SubElem> {
51 #[typst_macros::time(name = "sub", span = self.span())]
52 fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
53 let body = self.body.clone();
54
55 if self.typographic(styles) {
56 if let Some(text) = convert_script(&body, true) {
57 if is_shapable(engine, &text, styles) {
58 return Ok(TextElem::packed(text));
59 }
60 }
61 };
62
63 Ok(body
64 .styled(TextElem::set_baseline(self.baseline(styles)))
65 .styled(TextElem::set_size(self.size(styles))))
66 }
67}
68
69#[elem(title = "Superscript", Show)]
78pub struct SuperElem {
79 #[default(true)]
90 pub typographic: bool,
91
92 #[default(Em::new(-0.5).into())]
96 pub baseline: Length,
97
98 #[default(TextSize(Em::new(0.6).into()))]
102 pub size: TextSize,
103
104 #[required]
106 pub body: Content,
107}
108
109impl Show for Packed<SuperElem> {
110 #[typst_macros::time(name = "super", span = self.span())]
111 fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
112 let body = self.body.clone();
113
114 if self.typographic(styles) {
115 if let Some(text) = convert_script(&body, false) {
116 if is_shapable(engine, &text, styles) {
117 return Ok(TextElem::packed(text));
118 }
119 }
120 };
121
122 Ok(body
123 .styled(TextElem::set_baseline(self.baseline(styles)))
124 .styled(TextElem::set_size(self.size(styles))))
125 }
126}
127
128fn convert_script(content: &Content, sub: bool) -> Option<EcoString> {
131 if content.is::<SpaceElem>() {
132 Some(' '.into())
133 } else if let Some(elem) = content.to_packed::<TextElem>() {
134 if sub {
135 elem.text.chars().map(to_subscript_codepoint).collect()
136 } else {
137 elem.text.chars().map(to_superscript_codepoint).collect()
138 }
139 } else if let Some(sequence) = content.to_packed::<SequenceElem>() {
140 sequence
141 .children
142 .iter()
143 .map(|item| convert_script(item, sub))
144 .collect()
145 } else {
146 None
147 }
148}
149
150fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool {
153 let world = engine.world;
154 for family in TextElem::font_in(styles) {
155 if let Some(font) = world
156 .book()
157 .select(family.as_str(), variant(styles))
158 .and_then(|id| world.font(id))
159 {
160 let covers = family.covers();
161 return text.chars().all(|c| {
162 covers.map_or(true, |cov| cov.is_match(c.encode_utf8(&mut [0; 4])))
163 && font.ttf().glyph_index(c).is_some()
164 });
165 }
166 }
167
168 false
169}
170
171fn to_superscript_codepoint(c: char) -> Option<char> {
173 match c {
174 '1' => Some('¹'),
175 '2' => Some('²'),
176 '3' => Some('³'),
177 '0' | '4'..='9' => char::from_u32(c as u32 - '0' as u32 + '⁰' as u32),
178 '+' => Some('⁺'),
179 '−' => Some('⁻'),
180 '=' => Some('⁼'),
181 '(' => Some('⁽'),
182 ')' => Some('⁾'),
183 'n' => Some('ⁿ'),
184 'i' => Some('ⁱ'),
185 ' ' => Some(' '),
186 _ => None,
187 }
188}
189
190fn to_subscript_codepoint(c: char) -> Option<char> {
192 match c {
193 '0'..='9' => char::from_u32(c as u32 - '0' as u32 + '₀' as u32),
194 '+' => Some('₊'),
195 '−' => Some('₋'),
196 '=' => Some('₌'),
197 '(' => Some('₍'),
198 ')' => Some('₎'),
199 'a' => Some('ₐ'),
200 'e' => Some('ₑ'),
201 'o' => Some('ₒ'),
202 'x' => Some('ₓ'),
203 'h' => Some('ₕ'),
204 'k' => Some('ₖ'),
205 'l' => Some('ₗ'),
206 'm' => Some('ₘ'),
207 'n' => Some('ₙ'),
208 'p' => Some('ₚ'),
209 's' => Some('ₛ'),
210 't' => Some('ₜ'),
211 ' ' => Some(' '),
212 _ => None,
213 }
214}