diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-11-03 11:44:53 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-11-03 13:35:39 +0100 |
| commit | 37a7afddfaffd44cb9bc013c9506599267e08983 (patch) | |
| tree | 20e7d62d3c5418baff01a21d0406b91bf3096214 /library/src/text/shift.rs | |
| parent | 56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff) | |
Split crates
Diffstat (limited to 'library/src/text/shift.rs')
| -rw-r--r-- | library/src/text/shift.rs | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/library/src/text/shift.rs b/library/src/text/shift.rs new file mode 100644 index 00000000..e5f142dd --- /dev/null +++ b/library/src/text/shift.rs @@ -0,0 +1,188 @@ +use typst::model::SequenceNode; +use typst::util::EcoString; + +use super::{variant, SpaceNode, TextNode, TextSize}; +use crate::prelude::*; + +/// Sub or superscript text. +/// +/// The text is rendered smaller and its baseline is raised. To provide the best +/// typography possible, we first try to transform the text to superscript +/// codepoints. If that fails, we fall back to rendering shrunk normal letters +/// in a raised way. +#[derive(Debug, Hash)] +pub struct ShiftNode<const S: ScriptKind>(pub Content); + +/// Shift the text into superscript. +pub type SuperNode = ShiftNode<SUPERSCRIPT>; + +/// Shift the text into subscript. +pub type SubNode = ShiftNode<SUBSCRIPT>; + +#[node(Show)] +impl<const S: ScriptKind> ShiftNode<S> { + /// Whether to prefer the dedicated sub- and superscript characters of the + /// font. + pub const TYPOGRAPHIC: bool = true; + /// The baseline shift for synthetic sub- and superscripts. + pub const BASELINE: Length = + Em::new(if S == SUPERSCRIPT { -0.5 } else { 0.2 }).into(); + /// The font size for synthetic sub- and superscripts. + pub const SIZE: TextSize = TextSize(Em::new(0.6).into()); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl<const S: ScriptKind> Show for ShiftNode<S> { + fn unguard_parts(&self, _: Selector) -> Content { + Self(self.0.clone()).pack() + } + + fn field(&self, name: &str) -> Option<Value> { + match name { + "body" => Some(Value::Content(self.0.clone())), + _ => None, + } + } + + fn realize( + &self, + world: Tracked<dyn World>, + styles: StyleChain, + ) -> SourceResult<Content> { + let mut transformed = None; + if styles.get(Self::TYPOGRAPHIC) { + if let Some(text) = search_text(&self.0, S) { + if is_shapable(world, &text, styles) { + transformed = Some(TextNode(text).pack()); + } + } + }; + + Ok(transformed.unwrap_or_else(|| { + let mut map = StyleMap::new(); + map.set(TextNode::BASELINE, styles.get(Self::BASELINE)); + map.set(TextNode::SIZE, styles.get(Self::SIZE)); + self.0.clone().styled_with_map(map) + })) + } +} + +/// 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` leaf nodes. +fn search_text(content: &Content, mode: ScriptKind) -> Option<EcoString> { + if content.is_empty() { + Some(EcoString::new()) + } else if content.is::<SpaceNode>() { + Some(' '.into()) + } else if let Some(text) = content.downcast::<TextNode>() { + if let Some(sup) = convert_script(&text.0, mode) { + return Some(sup); + } + None + } else if let Some(seq) = content.downcast::<SequenceNode>() { + let mut full = EcoString::new(); + for item in seq.0.iter() { + match search_text(item, mode) { + Some(text) => full.push_str(&text), + None => return None, + } + } + Some(full) + } else { + None + } +} + +/// Checks whether the first retrievable family contains all code points of the +/// given string. +fn is_shapable(world: Tracked<dyn World>, text: &str, styles: StyleChain) -> bool { + for family in styles.get(TextNode::FAMILY).0.iter() { + if let Some(font) = world + .book() + .select(family.as_str(), variant(styles)) + .and_then(|id| world.font(id)) + { + return text.chars().all(|c| font.ttf().glyph_index(c).is_some()); + } + } + + false +} + +/// Convert a string to sub- or superscript codepoints if all characters +/// can be mapped to such a codepoint. +fn convert_script(text: &str, mode: ScriptKind) -> Option<EcoString> { + let mut result = EcoString::with_capacity(text.len()); + let converter = match mode { + SUPERSCRIPT => to_superscript_codepoint, + SUBSCRIPT | _ => to_subscript_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> { + 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, + }) +} + +/// Convert a character to its corresponding Unicode subscript. +fn to_subscript_codepoint(c: char) -> Option<char> { + 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, + }) +} + +/// A category of script. +pub type ScriptKind = usize; + +/// Text that is rendered smaller and raised, also known as superior. +const SUPERSCRIPT: ScriptKind = 0; + +/// Text that is rendered smaller and lowered, also known as inferior. +const SUBSCRIPT: ScriptKind = 1; |
