summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/text/shift.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/text/shift.rs
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/text/shift.rs')
-rw-r--r--crates/typst-library/src/text/shift.rs229
1 files changed, 229 insertions, 0 deletions
diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs
new file mode 100644
index 00000000..65e309e1
--- /dev/null
+++ b/crates/typst-library/src/text/shift.rs
@@ -0,0 +1,229 @@
+use super::{variant, SpaceElem, TextElem, TextSize};
+use crate::prelude::*;
+
+/// Renders text in subscript.
+///
+/// The text is rendered smaller and its baseline is lowered.
+///
+/// ## Example { #example }
+/// ```example
+/// Revenue#sub[yearly]
+/// ```
+///
+/// Display: Subscript
+/// Category: text
+#[element(Show)]
+pub struct SubElem {
+ /// Whether to prefer the dedicated subscript characters of the font.
+ ///
+ /// If this is enabled, Typst first tries to transform the text to subscript
+ /// codepoints. If that fails, it falls back to rendering lowered and shrunk
+ /// normal letters.
+ ///
+ /// ```example
+ /// N#sub(typographic: true)[1]
+ /// N#sub(typographic: false)[1]
+ /// ```
+ #[default(true)]
+ pub typographic: bool,
+
+ /// The baseline shift for synthetic subscripts. Does not apply if
+ /// `typographic` is true and the font has subscript codepoints for the
+ /// given `body`.
+ #[default(Em::new(0.2).into())]
+ pub baseline: Length,
+
+ /// The font size for synthetic subscripts. Does not apply if
+ /// `typographic` is true and the font has subscript codepoints for the
+ /// given `body`.
+ #[default(TextSize(Em::new(0.6).into()))]
+ pub size: TextSize,
+
+ /// The text to display in subscript.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for SubElem {
+ #[tracing::instrument(name = "SubElem::show", skip_all)]
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ let body = self.body();
+ let mut transformed = None;
+ if self.typographic(styles) {
+ if let Some(text) = search_text(&body, true) {
+ if is_shapable(vt, &text, styles) {
+ transformed = Some(TextElem::packed(text));
+ }
+ }
+ };
+
+ Ok(transformed.unwrap_or_else(|| {
+ body.styled(TextElem::set_baseline(self.baseline(styles)))
+ .styled(TextElem::set_size(self.size(styles)))
+ }))
+ }
+}
+
+/// Renders text in superscript.
+///
+/// The text is rendered smaller and its baseline is raised.
+///
+/// ## Example { #example }
+/// ```example
+/// 1#super[st] try!
+/// ```
+///
+/// Display: Superscript
+/// Category: text
+#[element(Show)]
+pub struct SuperElem {
+ /// Whether to prefer the dedicated superscript characters of the font.
+ ///
+ /// If this is enabled, Typst first tries to transform the text to
+ /// superscript codepoints. If that fails, it falls back to rendering
+ /// raised and shrunk normal letters.
+ ///
+ /// ```example
+ /// N#super(typographic: true)[1]
+ /// N#super(typographic: false)[1]
+ /// ```
+ #[default(true)]
+ pub typographic: bool,
+
+ /// The baseline shift for synthetic superscripts. Does not apply if
+ /// `typographic` is true and the font has superscript codepoints for the
+ /// given `body`.
+ #[default(Em::new(-0.5).into())]
+ pub baseline: Length,
+
+ /// The font size for synthetic superscripts. Does not apply if
+ /// `typographic` is true and the font has superscript codepoints for the
+ /// given `body`.
+ #[default(TextSize(Em::new(0.6).into()))]
+ pub size: TextSize,
+
+ /// The text to display in superscript.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for SuperElem {
+ #[tracing::instrument(name = "SuperElem::show", skip_all)]
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ let body = self.body();
+ let mut transformed = None;
+ if self.typographic(styles) {
+ if let Some(text) = search_text(&body, false) {
+ if is_shapable(vt, &text, styles) {
+ transformed = Some(TextElem::packed(text));
+ }
+ }
+ };
+
+ Ok(transformed.unwrap_or_else(|| {
+ 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` leafs.
+fn search_text(content: &Content, sub: bool) -> Option<EcoString> {
+ if content.is::<SpaceElem>() {
+ Some(' '.into())
+ } else if let Some(elem) = content.to::<TextElem>() {
+ convert_script(&elem.text(), sub)
+ } else if let Some(children) = content.to_sequence() {
+ let mut full = EcoString::new();
+ for item in children {
+ match search_text(item, sub) {
+ 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(vt: &Vt, text: &str, styles: StyleChain) -> bool {
+ let world = vt.world;
+ for family in TextElem::font_in(styles) {
+ 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, sub: bool) -> Option<EcoString> {
+ 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> {
+ 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,
+ })
+}