typst_library/text/
shift.rs

1use 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/// Renders text in subscript.
11///
12/// The text is rendered smaller and its baseline is lowered.
13///
14/// # Example
15/// ```example
16/// Revenue#sub[yearly]
17/// ```
18#[elem(title = "Subscript", Show)]
19pub struct SubElem {
20    /// Whether to prefer the dedicated subscript characters of the font.
21    ///
22    /// If this is enabled, Typst first tries to transform the text to subscript
23    /// codepoints. If that fails, it falls back to rendering lowered and shrunk
24    /// normal letters.
25    ///
26    /// ```example
27    /// N#sub(typographic: true)[1]
28    /// N#sub(typographic: false)[1]
29    /// ```
30    #[default(true)]
31    pub typographic: bool,
32
33    /// The baseline shift for synthetic subscripts. Does not apply if
34    /// `typographic` is true and the font has subscript codepoints for the
35    /// given `body`.
36    #[default(Em::new(0.2).into())]
37    pub baseline: Length,
38
39    /// The font size for synthetic subscripts. Does not apply if
40    /// `typographic` is true and the font has subscript codepoints for the
41    /// given `body`.
42    #[default(TextSize(Em::new(0.6).into()))]
43    pub size: TextSize,
44
45    /// The text to display in subscript.
46    #[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/// Renders text in superscript.
70///
71/// The text is rendered smaller and its baseline is raised.
72///
73/// # Example
74/// ```example
75/// 1#super[st] try!
76/// ```
77#[elem(title = "Superscript", Show)]
78pub struct SuperElem {
79    /// Whether to prefer the dedicated superscript characters of the font.
80    ///
81    /// If this is enabled, Typst first tries to transform the text to
82    /// superscript codepoints. If that fails, it falls back to rendering
83    /// raised and shrunk normal letters.
84    ///
85    /// ```example
86    /// N#super(typographic: true)[1]
87    /// N#super(typographic: false)[1]
88    /// ```
89    #[default(true)]
90    pub typographic: bool,
91
92    /// The baseline shift for synthetic superscripts. Does not apply if
93    /// `typographic` is true and the font has superscript codepoints for the
94    /// given `body`.
95    #[default(Em::new(-0.5).into())]
96    pub baseline: Length,
97
98    /// The font size for synthetic superscripts. Does not apply if
99    /// `typographic` is true and the font has superscript codepoints for the
100    /// given `body`.
101    #[default(TextSize(Em::new(0.6).into()))]
102    pub size: TextSize,
103
104    /// The text to display in superscript.
105    #[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
128/// Find and transform the text contained in `content` to the given script kind
129/// if and only if it only consists of `Text`, `Space`, and `Empty` leaves.
130fn 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
150/// Checks whether the first retrievable family contains all code points of the
151/// given string.
152fn 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
171/// Convert a character to its corresponding Unicode superscript.
172fn 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
190/// Convert a character to its corresponding Unicode subscript.
191fn 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}