Skip to main content

cranpose_text/
font_feature.rs

1use std::{
2    fmt,
3    hash::{Hash, Hasher},
4    sync::Arc,
5};
6
7/// One OpenType feature setting: its tag and its value, 0 for off.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub struct FontFeature {
10    /// The feature's tag, such as `liga`.
11    pub tag: [u8; 4],
12    /// The value: 0 turns the feature off, 1 on, and larger values pick an
13    /// alternate.
14    pub value: u32,
15}
16
17/// Compose's `fontFeatureSettings`: a CSS `font-feature-settings` string,
18/// such as `smcp, liga 0`, kept with the settings it parses to, so that a
19/// layout reads them without parsing the string again. Two settings are
20/// equal when their strings are.
21#[derive(Clone)]
22pub struct FontFeatureSettings(Arc<ParsedSettings>);
23
24struct ParsedSettings {
25    text: Box<str>,
26    features: Box<[FontFeature]>,
27}
28
29impl FontFeatureSettings {
30    /// The settings `text` writes, parsed as [`font_feature_settings`] does.
31    pub fn new(text: &str) -> Self {
32        Self(Arc::new(ParsedSettings {
33            text: text.into(),
34            features: font_feature_settings(text).collect(),
35        }))
36    }
37
38    /// The string the settings are written as.
39    pub fn as_str(&self) -> &str {
40        &self.0.text
41    }
42
43    /// The settings, in order, invalid entries skipped.
44    pub fn features(&self) -> &[FontFeature] {
45        &self.0.features
46    }
47}
48
49impl From<&str> for FontFeatureSettings {
50    fn from(text: &str) -> Self {
51        Self::new(text)
52    }
53}
54
55impl From<String> for FontFeatureSettings {
56    fn from(text: String) -> Self {
57        Self::new(&text)
58    }
59}
60
61impl From<Arc<str>> for FontFeatureSettings {
62    fn from(text: Arc<str>) -> Self {
63        Self::new(&text)
64    }
65}
66
67impl PartialEq for FontFeatureSettings {
68    fn eq(&self, other: &Self) -> bool {
69        Arc::ptr_eq(&self.0, &other.0) || self.0.text == other.0.text
70    }
71}
72
73impl Eq for FontFeatureSettings {}
74
75impl Hash for FontFeatureSettings {
76    fn hash<H: Hasher>(&self, state: &mut H) {
77        self.as_str().hash(state);
78    }
79}
80
81impl fmt::Debug for FontFeatureSettings {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        f.debug_tuple("FontFeatureSettings")
84            .field(&self.as_str())
85            .finish()
86    }
87}
88
89/// The settings of a CSS `font-feature-settings` string, Compose's
90/// `fontFeatureSettings`, in order. Entries are comma separated: a quoted or
91/// bare four-letter tag, then nothing or `on` for 1, `off` for 0, or a
92/// number. Invalid entries are skipped.
93pub fn font_feature_settings(settings: &str) -> impl Iterator<Item = FontFeature> + '_ {
94    settings.split(',').filter_map(parse_font_feature)
95}
96
97#[inline(never)]
98fn parse_font_feature(entry: &str) -> Option<FontFeature> {
99    let (tag, rest) = split_feature_tag(entry.trim())?;
100    let tag: [u8; 4] = tag.as_bytes().try_into().ok()?;
101    if !tag.iter().all(|byte| (0x20..=0x7e).contains(byte)) {
102        return None;
103    }
104    Some(FontFeature {
105        tag,
106        value: parse_feature_value(rest.trim_start())?,
107    })
108}
109
110fn split_feature_tag(entry: &str) -> Option<(&str, &str)> {
111    match *entry.as_bytes().first()? {
112        quote @ (b'"' | b'\'') => {
113            if entry.as_bytes().get(5) != Some(&quote) {
114                return None;
115            }
116            Some((entry.get(1..5)?, entry.get(6..)?))
117        }
118        _ => {
119            let length = entry
120                .bytes()
121                .position(|byte| !byte.is_ascii_alphanumeric())
122                .unwrap_or(entry.len());
123            if length != 4 {
124                return None;
125            }
126            entry.split_at_checked(length)
127        }
128    }
129}
130
131fn parse_feature_value(value: &str) -> Option<u32> {
132    if value.is_empty() || value.eq_ignore_ascii_case("on") {
133        return Some(1);
134    }
135    if value.eq_ignore_ascii_case("off") {
136        return Some(0);
137    }
138    value
139        .bytes()
140        .all(|byte| byte.is_ascii_digit())
141        .then(|| value.parse().ok())
142        .flatten()
143}