summaryrefslogtreecommitdiff
path: root/src/library/text/shift.rs
diff options
context:
space:
mode:
authorMartin Haug <mhaug@live.de>2022-05-31 22:56:08 +0200
committerMartin Haug <mhaug@live.de>2022-05-31 22:56:08 +0200
commit4e3036afcbdf2814889030413ffa81c8cc697341 (patch)
tree5a40018bb0fc39e726640184d988dcb0f2d3f86b /src/library/text/shift.rs
parent97858e5992a52459dd8a34be7a6b4786952b491a (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.rs181
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;