diff --git a/crates/typst/src/text/shift.rs b/crates/typst/src/text/shift.rs index 0029a3035..003ecf47c 100644 --- a/crates/typst/src/text/shift.rs +++ b/crates/typst/src/text/shift.rs @@ -51,19 +51,18 @@ impl Show for Packed { #[typst_macros::time(name = "sub", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { let body = self.body().clone(); - let mut transformed = None; + if self.typographic(styles) { - if let Some(text) = search_text(&body, true) { + if let Some(text) = convert_script(&body, true) { if is_shapable(engine, &text, styles) { - transformed = Some(TextElem::packed(text)); + return Ok(TextElem::packed(text)); } } }; - Ok(transformed.unwrap_or_else(|| { - body.styled(TextElem::set_baseline(self.baseline(styles))) - .styled(TextElem::set_size(self.size(styles))) - })) + Ok(body + .styled(TextElem::set_baseline(self.baseline(styles))) + .styled(TextElem::set_size(self.size(styles)))) } } @@ -111,38 +110,38 @@ impl Show for Packed { #[typst_macros::time(name = "super", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { let body = self.body().clone(); - let mut transformed = None; + if self.typographic(styles) { - if let Some(text) = search_text(&body, false) { + if let Some(text) = convert_script(&body, false) { if is_shapable(engine, &text, styles) { - transformed = Some(TextElem::packed(text)); + return Ok(TextElem::packed(text)); } } }; - Ok(transformed.unwrap_or_else(|| { - body.styled(TextElem::set_baseline(self.baseline(styles))) - .styled(TextElem::set_size(self.size(styles))) - })) + Ok(body + .styled(TextElem::set_baseline(self.baseline(styles))) + .styled(TextElem::set_size(self.size(styles)))) } } /// Find and transform the text contained in `content` to the given script kind /// if and only if it only consists of `Text`, `Space`, and `Empty` leaves. -fn search_text(content: &Content, sub: bool) -> Option { +fn convert_script(content: &Content, sub: bool) -> Option { if content.is::() { Some(' '.into()) } else if let Some(elem) = content.to_packed::() { - convert_script(elem.text(), sub) - } else if let Some(sequence) = content.to_packed::() { - let mut full = EcoString::new(); - for item in &sequence.children { - match search_text(item, sub) { - Some(text) => full.push_str(&text), - None => return None, - } + if sub { + elem.text().chars().map(to_subscript_codepoint).collect() + } else { + elem.text().chars().map(to_superscript_codepoint).collect() } - Some(full) + } else if let Some(sequence) = content.to_packed::() { + sequence + .children + .iter() + .map(|item| convert_script(item, sub)) + .collect() } else { None } @@ -165,65 +164,47 @@ fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool { false } -/// Convert a string to sub- or superscript codepoints if all characters -/// can be mapped to such a codepoint. -fn convert_script(text: &str, sub: bool) -> Option { - let mut result = EcoString::with_capacity(text.len()); - let converter = if sub { to_subscript_codepoint } else { to_superscript_codepoint }; - - for c in text.chars() { - match converter(c) { - Some(c) => result.push(c), - None => return None, - } - } - - Some(result) -} - /// Convert a character to its corresponding Unicode superscript. fn to_superscript_codepoint(c: char) -> Option { - char::from_u32(match c { - '0' => 0x2070, - '1' => 0x00B9, - '2' => 0x00B2, - '3' => 0x00B3, - '4'..='9' => 0x2070 + (c as u32 + 4 - '4' as u32), - '+' => 0x207A, - '-' => 0x207B, - '=' => 0x207C, - '(' => 0x207D, - ')' => 0x207E, - 'n' => 0x207F, - 'i' => 0x2071, - ' ' => 0x0020, - _ => return None, - }) + match c { + '1' => Some('¹'), + '2' => Some('²'), + '3' => Some('³'), + '0' | '4'..='9' => char::from_u32(c as u32 - '0' as u32 + '⁰' as u32), + '+' => Some('⁺'), + '−' => Some('⁻'), + '=' => Some('⁼'), + '(' => Some('⁽'), + ')' => Some('⁾'), + 'n' => Some('ⁿ'), + 'i' => Some('ⁱ'), + ' ' => Some(' '), + _ => None, + } } /// Convert a character to its corresponding Unicode subscript. fn to_subscript_codepoint(c: char) -> Option { - char::from_u32(match c { - '0' => 0x2080, - '1'..='9' => 0x2080 + (c as u32 - '0' as u32), - '+' => 0x208A, - '-' => 0x208B, - '=' => 0x208C, - '(' => 0x208D, - ')' => 0x208E, - 'a' => 0x2090, - 'e' => 0x2091, - 'o' => 0x2092, - 'x' => 0x2093, - 'h' => 0x2095, - 'k' => 0x2096, - 'l' => 0x2097, - 'm' => 0x2098, - 'n' => 0x2099, - 'p' => 0x209A, - 's' => 0x209B, - 't' => 0x209C, - ' ' => 0x0020, - _ => return None, - }) + match c { + '0'..='9' => char::from_u32(c as u32 - '0' as u32 + '₀' as u32), + '+' => Some('₊'), + '−' => Some('₋'), + '=' => Some('₌'), + '(' => Some('₍'), + ')' => Some('₎'), + 'a' => Some('ₐ'), + 'e' => Some('ₑ'), + 'o' => Some('ₒ'), + 'x' => Some('ₓ'), + 'h' => Some('ₕ'), + 'k' => Some('ₖ'), + 'l' => Some('ₗ'), + 'm' => Some('ₘ'), + 'n' => Some('ₙ'), + 'p' => Some('ₚ'), + 's' => Some('ₛ'), + 't' => Some('ₜ'), + ' ' => Some(' '), + _ => None, + } }