cranpose_text/
font_feature.rs1use std::{
2 fmt,
3 hash::{Hash, Hasher},
4 sync::Arc,
5};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub struct FontFeature {
10 pub tag: [u8; 4],
12 pub value: u32,
15}
16
17#[derive(Clone)]
22pub struct FontFeatureSettings(Arc<ParsedSettings>);
23
24struct ParsedSettings {
25 text: Box<str>,
26 features: Box<[FontFeature]>,
27}
28
29impl FontFeatureSettings {
30 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 pub fn as_str(&self) -> &str {
40 &self.0.text
41 }
42
43 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
89pub 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("e) {
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}