1use std::fmt;
2use std::str::FromStr;
3
4const DEFAULT_VALUE: i32 = 1;
5const INFINITY_END: i32 = -1;
6
7#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9pub struct FontFeature {
10 pub tag: String,
12 pub value: i32,
14 pub start: i32,
16 pub end: i32,
18}
19
20impl Default for FontFeature {
21 fn default() -> Self {
23 Self { tag: String::new(), value: DEFAULT_VALUE, start: 0, end: INFINITY_END }
24 }
25}
26
27struct FeatureMatch<'a> {
29 tag: &'a str,
30 value: &'a str,
31 start: &'a str,
32 end: &'a str,
33 has_separator: bool,
34}
35
36struct Cursor<'a> {
38 s: &'a str,
39 position: usize,
40}
41
42impl<'a> Cursor<'a> {
43 fn rest(&self) -> &'a str {
44 &self.s[self.position..]
45 }
46
47 fn skip_whitespace(&mut self) {
48 let rest = self.rest();
49 self.position += rest.len() - rest.trim_start().len();
50 }
51
52 fn eat(&mut self, c: char) -> bool {
53 if self.rest().starts_with(c) {
54 self.position += c.len_utf8();
55 true
56 } else {
57 false
58 }
59 }
60
61 fn digits(&mut self) -> &'a str {
62 let rest = self.rest();
63 let length = rest.find(|c: char| !c.is_numeric()).unwrap_or(rest.len());
64 self.position += length;
65 &rest[..length]
66 }
67}
68
69fn is_word_char(c: char) -> bool {
70 c.is_alphanumeric() || c == '_'
71}
72
73fn match_feature(s: &str) -> Option<FeatureMatch<'_>> {
76 let mut cursor = Cursor { s, position: 0 };
77
78 cursor.skip_whitespace();
79
80 let mut value = "";
81 if cursor.rest().starts_with(['+', '-']) {
82 value = &cursor.rest()[..1];
83 cursor.position += 1;
84 }
85
86 cursor.skip_whitespace();
87
88 let tag = {
89 let rest = cursor.rest();
90 let mut end = 0;
91 let mut count = 0;
92 for c in rest.chars() {
93 if count == 4 || !is_word_char(c) {
94 break;
95 }
96 end += c.len_utf8();
97 count += 1;
98 }
99 if count != 4 {
100 return None;
101 }
102 cursor.position += end;
103 &rest[..end]
104 };
105
106 cursor.skip_whitespace();
107
108 let mut start = "";
109 let mut end = "";
110 let mut has_separator = false;
111
112 if cursor.eat('[') {
113 cursor.skip_whitespace();
114 start = cursor.digits();
115
116 let before_separator = cursor.position;
117 cursor.skip_whitespace();
118 if cursor.eat(':') {
119 has_separator = true;
120 cursor.skip_whitespace();
121 } else {
122 cursor.position = before_separator;
123 }
124
125 end = cursor.digits();
126 cursor.skip_whitespace();
127
128 if !cursor.eat(']') {
129 return None;
130 }
131 }
132
133 cursor.skip_whitespace();
134
135 if value.is_empty() && cursor.eat('=') {
136 cursor.skip_whitespace();
137 let digits = cursor.digits();
138 if !digits.is_empty() {
139 value = digits;
140 } else if cursor.rest().starts_with("on") {
141 value = "on";
142 cursor.position += 2;
143 } else if cursor.rest().starts_with("off") {
144 value = "off";
145 cursor.position += 3;
146 } else {
147 return None;
148 }
149 }
150
151 cursor.skip_whitespace();
152
153 if !cursor.rest().is_empty() {
154 return None;
155 }
156
157 Some(FeatureMatch { tag, value, start, end, has_separator })
158}
159
160fn try_parse_digits(s: &str) -> Option<i32> {
162 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
163 return None;
164 }
165 s.parse().ok()
166}
167
168impl FontFeature {
169 pub fn parse(s: &str) -> FontFeature {
190 let Some(matched) = match_feature(s) else {
191 return FontFeature::default();
192 };
193
194 let start = try_parse_digits(matched.start);
195 let end = try_parse_digits(matched.end);
196
197 let mut string_value = matched.value;
198
199 if string_value == "-" || string_value.eq_ignore_ascii_case("OFF") {
200 string_value = "0";
201 }
202
203 if string_value == "+" || string_value.eq_ignore_ascii_case("ON") {
204 string_value = "1";
205 }
206
207 FontFeature {
208 tag: matched.tag.to_owned(),
209 start: start.unwrap_or(0),
210 end: match (end, start) {
211 (Some(end), _) => end,
212 (None, Some(start)) if !matched.has_separator => start + 1,
213 _ => INFINITY_END,
214 },
215 value: try_parse_digits(string_value).unwrap_or(DEFAULT_VALUE),
216 }
217 }
218}
219
220impl FromStr for FontFeature {
221 type Err = std::convert::Infallible;
222
223 fn from_str(s: &str) -> Result<Self, Self::Err> {
224 Ok(FontFeature::parse(s))
225 }
226}
227
228impl fmt::Display for FontFeature {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 if self.value == 0 {
232 f.write_str("-")?;
233 }
234 f.write_str(&self.tag)?;
235
236 if self.start != 0 || self.end != INFINITY_END {
237 f.write_str("[")?;
238
239 if self.start > 0 {
240 write!(f, "{}", self.start)?;
241 }
242
243 if self.end != self.start + 1 {
244 f.write_str(":")?;
245 if self.end != INFINITY_END {
246 write!(f, "{}", self.end)?;
247 }
248 }
249
250 f.write_str("]")?;
251 }
252
253 if self.value == DEFAULT_VALUE || self.value == 0 {
254 return Ok(());
255 }
256
257 write!(f, "={}", self.value)
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 fn feature(tag: &str, value: i32, start: i32, end: i32) -> FontFeature {
266 FontFeature { tag: tag.to_owned(), value, start, end }
267 }
268
269 #[test]
270 fn parses_documented_syntax() {
271 assert_eq!(FontFeature::parse("kern"), feature("kern", 1, 0, -1));
272 assert_eq!(FontFeature::parse("+kern"), feature("kern", 1, 0, -1));
273 assert_eq!(FontFeature::parse("-kern"), feature("kern", 0, 0, -1));
274 assert_eq!(FontFeature::parse("kern=0"), feature("kern", 0, 0, -1));
275 assert_eq!(FontFeature::parse("kern=1"), feature("kern", 1, 0, -1));
276 assert_eq!(FontFeature::parse("aalt=2"), feature("aalt", 2, 0, -1));
277 assert_eq!(FontFeature::parse("kern[]"), feature("kern", 1, 0, -1));
278 assert_eq!(FontFeature::parse("kern[:]"), feature("kern", 1, 0, -1));
279 assert_eq!(FontFeature::parse("kern[5:]"), feature("kern", 1, 5, -1));
280 assert_eq!(FontFeature::parse("kern[:5]"), feature("kern", 1, 0, 5));
281 assert_eq!(FontFeature::parse("kern[3:5]"), feature("kern", 1, 3, 5));
282 assert_eq!(FontFeature::parse("kern[3]"), feature("kern", 1, 3, 4));
283 assert_eq!(FontFeature::parse("aalt[3:5]=2"), feature("aalt", 2, 3, 5));
284 assert_eq!(FontFeature::parse(" liga = off "), feature("liga", 0, 0, -1));
285 assert_eq!(FontFeature::parse("liga=on"), feature("liga", 1, 0, -1));
286 }
287
288 #[test]
289 fn invalid_input_gives_default() {
290 assert_eq!(FontFeature::parse("kerning"), FontFeature::default());
291 assert_eq!(FontFeature::parse("ker"), FontFeature::default());
292 assert_eq!(FontFeature::parse("+kern=1"), FontFeature::default());
293 assert_eq!(FontFeature::parse("kern[1 2]"), FontFeature::default());
294 }
295
296 #[test]
297 fn display_round_trips() {
298 for s in ["kern", "-kern", "aalt=2", "kern[5:]", "kern[:5]", "kern[3:5]", "kern[3]", "aalt[3:5]=2"] {
299 assert_eq!(FontFeature::parse(s).to_string(), s);
300 }
301 }
302}