diff options
| author | Martin Haug <mhaug@live.de> | 2022-05-31 22:56:08 +0200 |
|---|---|---|
| committer | Martin Haug <mhaug@live.de> | 2022-05-31 22:56:08 +0200 |
| commit | 4e3036afcbdf2814889030413ffa81c8cc697341 (patch) | |
| tree | 5a40018bb0fc39e726640184d988dcb0f2d3f86b /src/library/text/shift.rs | |
| parent | 97858e5992a52459dd8a34be7a6b4786952b491a (diff) | |
Sub- and superscripts
Fixes to PDF export I guess
Also improved rendition for non-Latin scripts
Diffstat (limited to 'src/library/text/shift.rs')
| -rw-r--r-- | src/library/text/shift.rs | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/src/library/text/shift.rs b/src/library/text/shift.rs new file mode 100644 index 00000000..3efa3720 --- /dev/null +++ b/src/library/text/shift.rs @@ -0,0 +1,181 @@ +use super::{variant, TextNode, TextSize}; +use crate::font::FontStore; +use crate::library::prelude::*; +use crate::util::EcoString; + +/// 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(showable)] +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: RawLength = + 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 Machine, args: &mut Args) -> TypResult<Content> { + Ok(Content::show(Self(args.expect("body")?))) + } +} + +impl<const S: ScriptKind> Show for ShiftNode<S> { + fn unguard(&self, sel: Selector) -> ShowNode { + Self(self.0.unguard(sel)).pack() + } + + fn encode(&self, _: StyleChain) -> Dict { + dict! { "body" => Value::Content(self.0.clone()) } + } + + fn realize(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Content> { + let mut transformed = None; + if styles.get(ShiftNode::<S>::TYPOGRAPHIC) { + if let Some(text) = search_text(&self.0, S) { + if check_str_in_family(&mut ctx.fonts, &text, styles) { + transformed = Some(Content::Text(text)); + } + } + }; + + Ok(transformed.unwrap_or_else(|| { + let mut map = StyleMap::new(); + map.set(TextNode::BASELINE, styles.get(ShiftNode::<S>::BASELINE)); + map.set(TextNode::SIZE, styles.get(ShiftNode::<S>::SIZE)); + self.0.clone().styled_with_map(map) + })) + } +} + +/// Find and transform the text contained in `content` iff it only consists of +/// `Text`, `Space`, and `Empty` leaf nodes. The text is transformed to the +/// given script kind. +fn search_text(content: &Content, mode: ScriptKind) -> Option<EcoString> { + match content { + Content::Text(_) => { + if let Content::Text(t) = content { + if let Some(sup) = convert_script(t, mode) { + return Some(sup); + } + } + None + } + Content::Space => Some(EcoString::from(" ")), + Content::Empty => Some(EcoString::new()), + Content::Styled(arc) => search_text(&arc.0, mode), + Content::Sequence(seq) => { + let mut full = EcoString::new(); + for item in seq.iter() { + match search_text(item, mode) { + Some(text) => full.push_str(&text), + None => return None, + } + } + Some(full) + } + _ => None, + } +} + +/// Checks whether the first retrievable family contains all code points of the +/// given string. +fn check_str_in_family(fonts: &mut FontStore, text: &str, styles: StyleChain) -> bool { + for family in styles.get(TextNode::FAMILY).iter() { + if let Some(face_id) = fonts.select(family.as_str(), variant(styles)) { + let face = fonts.get(face_id); + let ttf = face.ttf(); + return text.chars().all(|c| 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, + _ => panic!("unknown script kind"), + }; + + 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; |
